Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
998fc1e
mason: port the shared test kit and rig to Windows
ualtinok Oct 10, 2026
024bd1c
Merge (CI only): Windows test kit and rig
ualtinok Oct 10, 2026
3f896a5
mason: fix measured Windows test-kit and rig build failures
ualtinok Oct 10, 2026
585b98f
Merge (CI only): Windows test kit fix
ualtinok Oct 10, 2026
94973a0
mason: align native Windows path fixtures with verified roots
ualtinok Oct 10, 2026
3b88388
Merge (CI only): 94973a0d36cc700a2c8f5d090dd4ee2188d260dd
ualtinok Oct 10, 2026
5c45fcf
Merge (CI only): Windows module fixes for manifest roots and launcher…
ualtinok Oct 10, 2026
dc5a30b
Re-anchor the worker dependency control on basal-rig's Windows lockfi…
ualtinok Oct 10, 2026
42270ca
Merge (CI only): Windows probes and the worker stack reserve
ualtinok Oct 10, 2026
feb4ab4
mason: assert the Windows raw-path dispatch refusal code
ualtinok Oct 10, 2026
2418893
Merge (CI only): test kit dot-dot refusal on Windows
ualtinok Oct 10, 2026
98c4a54
mason: exercise native rig process discovery with a real image
ualtinok Oct 10, 2026
97fe601
Merge (CI only): Windows rig process-path control
ualtinok Oct 10, 2026
2249366
mason: protect the rig image locator raw spelling
ualtinok Oct 10, 2026
bbfa982
Merge (CI only): exact Windows rig image path spelling
ualtinok Oct 10, 2026
251cb46
mason: assert the raw-path refusal kind in its audit row
ualtinok Oct 10, 2026
97e08fd
Merge (CI only): integration/windows
ualtinok Oct 10, 2026
96017cd
Merge (CI only): Windows dispatch audit diagnostic
ualtinok Oct 10, 2026
2d3a87b
mason: use the public git surface in host privacy regressions
ualtinok Oct 10, 2026
79819d1
Merge (CI only): portable host regression tests on Windows
ualtinok Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,16 @@ jobs:
continue-on-error: true
timeout-minutes: 20
run: cargo test -p basal-host --lib --locked -- spawn_lock_tests --show-output 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-spawn-lock.log"
- name: Test the shared test kit on its own
id: test-testkit
continue-on-error: true
timeout-minutes: 30
run: cargo test -p basal-testkit --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-testkit.log"
- name: Test the integration rig on its own
id: test-rig
continue-on-error: true
timeout-minutes: 30
run: cargo test -p basal-rig --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-rig.log"
- name: Check every workspace target
id: check
continue-on-error: true
Expand Down Expand Up @@ -473,6 +483,11 @@ jobs:
continue-on-error: true
timeout-minutes: 30
run: cargo test -p basal-host --lib --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-host.log"
- name: Test the host integration regressions on their own
id: test-host-regressions
continue-on-error: true
timeout-minutes: 20
run: cargo test -p basal-host --test builtin_host_regressions --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-host-regressions.log"
- name: Summarize the baseline
if: always()
run: |
Expand All @@ -484,6 +499,8 @@ jobs:
echo "- cargo test -p basal-launch --features deviations --locked: ${{ steps.test-launch.outcome }}"
echo "- cargo test -p basal-module -p basal-core --locked --no-fail-fast: ${{ steps.test-module.outcome }}"
echo "- cargo test -p basal-host --lib --locked -- spawn_lock_tests: ${{ steps.test-spawn-lock.outcome }}"
echo "- cargo test -p basal-testkit --locked --no-fail-fast: ${{ steps.test-testkit.outcome }}"
echo "- cargo test -p basal-rig --locked --no-fail-fast: ${{ steps.test-rig.outcome }}"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 24 additions & 6 deletions crates/basal-host/tests/builtin_host_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use basal_host::builtins::{BuiltinHost, Grant, codes, envelope, fs, git, net};
use basal_host::{CallClass, CallRequest, Dispatched, Host};
use basal_proto::{CallKind, JsonText, Primitive, Settlement};
use serde_json::json;
#[cfg(unix)]
use std::os::unix::fs::{PermissionsExt, symlink};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
Expand Down Expand Up @@ -31,7 +32,7 @@ impl Drop for Tree {
#[test]
fn escaped_800kb_file_is_a_typed_refusal_not_successful_null() {
let tree = Tree::new();
let path = tree.0.join("root/escaped");
let path = tree.0.join("root").join("escaped");
std::fs::write(&path, vec![0; 800 * 1024]).unwrap();
let request = CallRequest {
flow_id: "f".into(),
Expand Down Expand Up @@ -63,6 +64,7 @@ fn escaped_800kb_file_is_a_typed_refusal_not_successful_null() {
}

#[test]
#[cfg(unix)]
fn escaped_listing_exceeds_encoded_cap_as_a_typed_refusal() {
let tree = Tree::new();
let dir = tree.0.join("root");
Expand Down Expand Up @@ -106,13 +108,16 @@ fn outside_missing_and_symlink_targets_have_identical_refusals() {
let tree = Tree::new();
let existing = tree.0.join("secret");
std::fs::write(&existing, "secret").unwrap();
#[cfg(unix)]
let link = tree.0.join("root/link");
#[cfg(unix)]
symlink(&existing, &link).unwrap();
let missing = tree.0.join("absent/child");
let missing = tree.0.join("absent").join("child");
for purpose in [fs::Purpose::Read, fs::Purpose::Write] {
let a = fs::resolve(existing.to_str().unwrap(), &tree.roots(), purpose).unwrap_err();
let b = fs::resolve(missing.to_str().unwrap(), &tree.roots(), purpose).unwrap_err();
assert_eq!(a, b);
#[cfg(unix)]
if purpose == fs::Purpose::Read {
assert_eq!(
a,
Expand All @@ -122,16 +127,20 @@ fn outside_missing_and_symlink_targets_have_identical_refusals() {
assert_eq!(a.code, codes::DENIED);
assert!(!a.message.contains(tree.0.to_str().unwrap()));
}
let op = git::Op::RevParse {
reference: "HEAD".into(),
};
assert_eq!(
git::repo(existing.to_str().unwrap(), &tree.roots()).unwrap_err(),
git::repo(missing.to_str().unwrap(), &tree.roots()).unwrap_err()
git::run(existing.to_str().unwrap(), &tree.roots(), &op).unwrap_err(),
git::run(missing.to_str().unwrap(), &tree.roots(), &op).unwrap_err()
);
}

#[test]
#[cfg(unix)]
fn atomic_write_drops_special_permission_bits() {
let tree = Tree::new();
let path = tree.0.join("root/file");
let path = tree.0.join("root").join("file");
std::fs::write(&path, "old").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o6755)).unwrap();
fs::write(path.to_str().unwrap(), &tree.roots(), "").unwrap();
Expand Down Expand Up @@ -179,6 +188,7 @@ fn method_override_and_compression_headers_are_denied() {
}

#[test]
#[cfg(unix)]
fn git_reaps_descendants_that_keep_its_output_pipe_open() {
use std::process::{Command, Stdio};
use std::time::Duration;
Expand Down Expand Up @@ -219,13 +229,21 @@ fn git_reaps_descendants_that_keep_its_output_pipe_open() {
fn tag_patterns_use_git_globs_including_hierarchical_names() {
let tree = Tree::new();
let repo = tree.0.join("root");
let global_config = tree.0.join("empty.gitconfig");
std::fs::write(&global_config, []).unwrap();
let git_command = |args: &[&str]| {
let result = std::process::Command::new("git")
.env_clear()
.envs(std::env::vars_os().filter(|(key, _)| {
["PATH", "HOME", "SystemRoot", "USERPROFILE"]
.iter()
.any(|kept| key.to_string_lossy().eq_ignore_ascii_case(kept))
}))
.arg("-C")
.arg(&repo)
.args(args)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_GLOBAL", &global_config)
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
Expand Down
9 changes: 9 additions & 0 deletions crates/basal-rig/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ subc-client-rs.workspace = true
subc-protocol.workspace = true
tokio = { workspace = true, features = ["sync", "time"] }

[target.'cfg(windows)'.dependencies]
basal-launch.workspace = true
rusqlite = { workspace = true, features = ["bundled"] }
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Threading",
] }

[dev-dependencies]
basal-host.workspace = true
basal-proto.workspace = true
9 changes: 9 additions & 0 deletions crates/basal-rig/src/bin/basal-rig-contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ mod ownership;
mod package_cases;
#[path = "basal-rig-contract/seeded.rs"]
mod seeded;
#[cfg(windows)]
#[path = "basal-rig-contract/windows.rs"]
mod windows;

const CORE: &str = "prefrontal-core";
const BASAL: &str = "basal";
Expand Down Expand Up @@ -1394,6 +1397,7 @@ async fn relay_refusals(rig: &Rig, flow: &Flow, agent: &Agent) -> Case {
case
}

#[cfg(unix)]
fn basal_pid() -> Option<String> {
let out = std::process::Command::new("ps")
.args(["-axo", "pid=,command="])
Expand All @@ -1411,6 +1415,11 @@ fn basal_pid() -> Option<String> {
})
}

#[cfg(windows)]
fn basal_pid() -> Option<String> {
windows::basal_pid()
}

async fn crash(rig: &Rig, flow: &Flow, agent: &Agent, since: i64) -> Case {
let mut case = Case::new(CRASH_CASE);
// Arm the kill switch before approving, so the flow's first run cannot
Expand Down
43 changes: 42 additions & 1 deletion crates/basal-rig/src/bin/basal-rig-contract/seeded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,30 @@ pub(super) fn module_cli_path(home: &std::path::Path) -> Result<PathBuf, String>
{
return Err("module control requires the isolated ckdev-flows home".into());
}
Ok(root.join("bin/ckdev-ck"))
#[cfg(unix)]
{
Ok(root.join("bin/ckdev-ck"))
}
#[cfg(windows)]
{
Ok(root.join("bin").join("ckdev-ck.exe"))
}
}

pub(super) fn ck(args: &[&str]) -> Result<Value, String> {
let connection =
std::env::var_os("SUBC_CONNECTION_FILE").ok_or("no isolated connection file")?;
let home = PathBuf::from(std::env::var_os("HOME").ok_or("missing isolated HOME")?);
#[cfg(unix)]
let output = std::process::Command::new(module_cli_path(&home)?)
.arg("--subc")
.arg(connection)
.arg("--json")
.args(args)
.output()
.map_err(|e| e.to_string())?;
#[cfg(windows)]
let output = super::windows::ck_output(module_cli_path(&home)?, connection, args)?;
if !output.status.success() {
return Err(format!(
"ckdev-ck {args:?}: {} {}",
Expand Down Expand Up @@ -195,6 +205,7 @@ pub(super) async fn stale(
case
}

#[cfg(unix)]
pub(super) fn no_store_handles(path: &std::path::Path) -> Result<(), String> {
let mut command = std::process::Command::new("lsof");
command.arg("-F").arg("p").arg(path);
Expand All @@ -212,3 +223,33 @@ pub(super) fn no_store_handles(path: &std::path::Path) -> Result<(), String> {
}
Ok(())
}

#[cfg(windows)]
pub(super) fn no_store_handles(path: &std::path::Path) -> Result<(), String> {
use std::os::windows::fs::OpenOptionsExt;
let mut files = vec![path.to_path_buf()];
for suffix in ["-wal", "-shm"] {
let sidecar = PathBuf::from(format!("{}{suffix}", path.display()));
if sidecar.exists() {
files.push(sidecar);
}
}
// No-sharing opens refuse while any other reader or writer owns the file.
// Hold all three together so the observation covers the store and sidecars.
let _handles = files
.iter()
.map(|file| {
std::fs::OpenOptions::new()
.read(true)
.share_mode(0)
.open(file)
.map_err(|e| {
format!(
"cannot prove {} has no open store handles: {e}",
file.display()
)
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}
65 changes: 50 additions & 15 deletions crates/basal-rig/src/bin/basal-rig-contract/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,19 @@ fn seeded_fixture_requires_a_terminal_disabled_module_and_no_pid() {

#[test]
fn seeded_fixture_rejects_foreign_paths_and_symlinked_stores() {
let output = std::process::Command::new("mktemp")
.arg("-d")
.arg(std::env::temp_dir().join("basal-rig-fixture.XXXXXXXX"))
.output()
.unwrap();
assert!(output.status.success());
let root = PathBuf::from(String::from_utf8(output.stdout).unwrap().trim())
.canonicalize()
.unwrap();
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let root = loop {
let root = std::env::temp_dir().join(format!(
"basal-rig-fixture-{}-{}",
std::process::id(),
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
match std::fs::create_dir(&root) {
Ok(()) => break root.canonicalize().unwrap(),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => panic!("create isolated fixture: {error}"),
}
};
let rig = root.join("ckdev-flows");
let home = rig.join("home");
let store = rig.join("data/cortexkit/basal/store.db");
Expand All @@ -151,9 +155,12 @@ fn seeded_fixture_rejects_foreign_paths_and_symlinked_stores() {
let foreign = root.join("production.db");
std::fs::write(&foreign, []).unwrap();
assert!(seeded::fixture_path_for(&home, &foreign, "basal").is_err());
std::fs::remove_file(&store).unwrap();
std::os::unix::fs::symlink(&foreign, &store).unwrap();
assert!(seeded::fixture_path_for(&home, &store, "basal").is_err());
#[cfg(unix)]
{
std::fs::remove_file(&store).unwrap();
std::os::unix::fs::symlink(&foreign, &store).unwrap();
assert!(seeded::fixture_path_for(&home, &store, "basal").is_err());
}
assert!(seeded::fixture_path_for(&home, &store, "other").is_err());
std::fs::remove_dir_all(&root).unwrap();
}
Expand Down Expand Up @@ -191,10 +198,38 @@ fn authorship_fingerprint_allows_only_the_one_author_cell() {

#[test]
fn fixture_module_control_uses_only_the_absolute_isolated_cli() {
let root = std::env::temp_dir().join("rig").join("ckdev-flows");
let home = root.join("home");
assert_eq!(
seeded::module_cli_path(std::path::Path::new("/rig/ckdev-flows/home")).unwrap(),
PathBuf::from("/rig/ckdev-flows/bin/ckdev-ck")
seeded::module_cli_path(&home).unwrap(),
root.join("bin").join(if cfg!(windows) {
"ckdev-ck.exe"
} else {
"ckdev-ck"
})
);
assert!(seeded::module_cli_path(std::path::Path::new("/rig/home")).is_err());
assert!(seeded::module_cli_path(&std::env::temp_dir().join("rig").join("home")).is_err());
assert!(seeded::module_cli_path(std::path::Path::new("ckdev-flows/home")).is_err());
}

#[cfg(windows)]
#[test]
fn store_handle_probe_refuses_an_open_database_or_sidecar() {
let directory =
std::env::temp_dir().join(format!("basal-rig-handle-probe-{}", std::process::id()));
std::fs::create_dir(&directory).unwrap();
let store = directory.join("store.db");
std::fs::write(&store, []).unwrap();
for suffix in ["", "-wal", "-shm"] {
let path = PathBuf::from(format!("{}{suffix}", store.display()));
std::fs::write(&path, []).unwrap();
let held = std::fs::File::open(&path).unwrap();
assert!(
seeded::no_store_handles(&store).is_err(),
"open {suffix:?} was not refused"
);
drop(held);
assert!(seeded::no_store_handles(&store).is_ok());
}
std::fs::remove_dir_all(directory).unwrap();
}
Loading
Loading