diff --git a/.alfonso/release-notes/next/runon-macos.md b/.alfonso/release-notes/next/runon-macos.md new file mode 100644 index 0000000000..36a1e202d7 --- /dev/null +++ b/.alfonso/release-notes/next/runon-macos.md @@ -0,0 +1 @@ +bash can now send build and test commands to a macOS machine with runon: "macos" (fixed size, offline), so macOS-only tests no longer have to run on the local machine. diff --git a/benchmarks/aft-search/slice-descriptors/land-runon-macos.json b/benchmarks/aft-search/slice-descriptors/land-runon-macos.json new file mode 100644 index 0000000000..ebd574711e --- /dev/null +++ b/benchmarks/aft-search/slice-descriptors/land-runon-macos.json @@ -0,0 +1,8 @@ +{ + "targeted_mechanism": "none", + "kind": "harness", + "slice_class": "non_ranking", + "fixtures": [ + "harness-goldens" + ] +} diff --git a/crates/aft/src/bash_background/remote_tests.rs b/crates/aft/src/bash_background/remote_tests.rs index 05e332aaca..4fe49bb840 100644 --- a/crates/aft/src/bash_background/remote_tests.rs +++ b/crates/aft/src/bash_background/remote_tests.rs @@ -5737,6 +5737,26 @@ async fn runon_sends_the_whole_compound_line_remote_exactly_as_written() { ); } +#[tokio::test] +async fn runon_macos_reaches_the_wire_request_as_macos_platform() { + let daemon = daemon(Script::Plain, "exec-remote/v1").await; + let dir = tempfile::tempdir().unwrap(); + let ctx = restarted_context(dir.path()); + let response = handle_with_policy( + &ctx, + Some(launch(daemon.connection.clone())), + serde_json::json!({"command": "uname -a", "runon": "macos", "compressed": false}), + ); + assert!(response.success, "{response:?}"); + let task_id = response.data["task_id"].as_str().unwrap().to_string(); + let _done = terminal(ctx.bash_background(), &task_id).await; + let runs = exec_runs(&daemon); + assert_eq!(runs.len(), 1, "{runs:?}"); + assert_eq!(runs[0]["platform"], "macos"); + assert!(runs[0].get("weight_hint").is_none(), "{runs:?}"); + assert!(runs[0].get("network").is_none(), "{runs:?}"); +} + #[tokio::test] async fn runon_vcpu_count_is_sent_as_weight_hint_on_the_wire() { let daemon = daemon(Script::Plain, "exec-remote/v1").await; @@ -5798,6 +5818,9 @@ async fn malformed_runon_vcpu_requests_are_refused_before_dispatch() { "windows,4c", "windows,net", "windows,wat", + "macos,4c", + "macos,net", + "macos,wat", ] { let response = handle_with_policy( &restarted_context(dir.path()), @@ -5807,9 +5830,9 @@ async fn malformed_runon_vcpu_requests_are_refused_before_dispatch() { assert!(!response.success, "{runon}: {response:?}"); let message = response.data["message"].as_str().unwrap_or_default(); assert!(message.contains(runon), "{runon}: {response:?}"); - let reason = if runon == "windows,4c" { + let reason = if runon == "windows,4c" || runon == "macos,4c" { "fixed at 4 vCPUs" - } else if runon == "windows,net" { + } else if runon == "windows,net" || runon == "macos,net" { "no network" } else { "not valid" diff --git a/crates/aft/src/exec_remote/policy.rs b/crates/aft/src/exec_remote/policy.rs index 3aef86fbe3..cf36ada584 100644 --- a/crates/aft/src/exec_remote/policy.rs +++ b/crates/aft/src/exec_remote/policy.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; /// Runner platforms whose demands `runon` accepts; options are parsed separately. -pub const KNOWN_DEMANDS: &[&str] = &["linux", "windows"]; +pub const KNOWN_DEMANDS: &[&str] = &["linux", "windows", "macos"]; #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(deny_unknown_fields)] @@ -120,12 +120,17 @@ pub fn parse_demand(demand: &str) -> Result { let mut resolved = ResolvedDemand::default(); if platform == "windows" { resolved.platform = Some(super::types::Platform::Windows); + } else if platform == "macos" { + resolved.platform = Some(super::types::Platform::Macos); } for token in tokens { if token == "net" { if platform == "windows" { return Err(format!("runon {demand:?} is not valid: Windows jobs have no network; windows,net is not supported")); } + if platform == "macos" { + return Err(format!("runon {demand:?} is not valid: macOS jobs have no network; macos,net is not supported")); + } if resolved.network { return Err(format!( "runon {demand:?} is not valid: duplicate net option" @@ -146,6 +151,9 @@ pub fn parse_demand(demand: &str) -> Result { if platform == "windows" { return Err(format!("runon {demand:?} is not valid: the Windows slot is fixed at 4 vCPUs; use plain windows without a vCPU count")); } + if platform == "macos" { + return Err(format!("runon {demand:?} is not valid: the macOS slot is fixed at 4 vCPUs; use plain macos without a vCPU count")); + } if resolved.weight_hint.is_some() { return Err(format!( "runon {demand:?} is not valid: duplicate vCPU count" @@ -160,6 +168,9 @@ fn invalid_vcpu_demand(demand: &str) -> String { if demand.starts_with("windows,") { return format!("runon {demand:?} is not valid: Windows jobs accept no options; use plain windows (fixed at 4 vCPUs, no network)"); } + if demand.starts_with("macos,") { + return format!("runon {demand:?} is not valid: macOS jobs accept no options; use plain macos (fixed at 4 vCPUs, no network)"); + } format!("runon {demand:?} is not valid: options are net and a vCPU count as Nc, for example \"linux,4c,net\"") } @@ -200,6 +211,40 @@ mod tests { } } + #[test] + fn macos_demand_accepts_only_the_fixed_offline_slot() { + let resolved = resolve_demand("macos", None).unwrap(); + assert_eq!( + resolved.platform, + Some(super::super::types::Platform::Macos) + ); + assert_eq!(resolved.weight_hint, None); + assert!(!resolved.network); + assert_eq!(resolve_demand("", Some("macos")).unwrap(), resolved); + for (demand, reason) in [ + ( + "macos,4c", + "the macOS slot is fixed at 4 vCPUs; use plain macos without a vCPU count", + ), + ( + "macos,net", + "macOS jobs have no network; macos,net is not supported", + ), + ( + "macos,4c,net", + "the macOS slot is fixed at 4 vCPUs; use plain macos without a vCPU count", + ), + ( + "macos,wat", + "macOS jobs accept no options; use plain macos (fixed at 4 vCPUs, no network)", + ), + ] { + let error = resolve_demand(demand, None).unwrap_err(); + assert!(error.contains(demand), "{error}"); + assert!(error.contains(reason), "{error}"); + } + } + #[test] fn network_demand_options_parse_in_any_order_and_refuse_duplicates_by_name() { for demand in ["linux,net", "linux,4c,net", "linux,net,4c"] { diff --git a/crates/aft/src/subc_tool_remote_schemas.json b/crates/aft/src/subc_tool_remote_schemas.json index ebe0d9b258..e04e42b0b9 100644 --- a/crates/aft/src/subc_tool_remote_schemas.json +++ b/crates/aft/src/subc_tool_remote_schemas.json @@ -75,13 +75,13 @@ }, "runon": { "type": "string", - "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options)." + "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). `macos` sends the command to a macOS machine for builds and tests that need macOS (no vCPU or network options)." } }, "required": [ "command" ], - "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; the task keeps running after the call returns, a completion reminder arrives when it exits, and bash_status reports its state and output. Use bash({wait:true}) instead when the result is needed before anything else. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, aft_outline, or aft_zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete." + "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; the task keeps running after the call returns, a completion reminder arrives when it exits, and bash_status reports its state and output. Use bash({wait:true}) instead when the result is needed before anything else. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: \"macos\" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, aft_outline, or aft_zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete." } }, "worker": { @@ -160,13 +160,13 @@ }, "runon": { "type": "string", - "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options)." + "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). `macos` sends the command to a macOS machine for builds and tests that need macOS (no vCPU or network options)." } }, "required": [ "command" ], - "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so a long command moves to the background while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; then wait on it with bash_watch before you report a result: a background task never wakes you, and a watch without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. Never background a command and immediately bash_watch it (that wastes a turn for what foreground returns in one); never loop bash_status to wait. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, aft_outline, or aft_zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete." + "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so a long command moves to the background while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; then wait on it with bash_watch before you report a result: a background task never wakes you, and a watch without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. Never background a command and immediately bash_watch it (that wastes a turn for what foreground returns in one); never loop bash_status to wait. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: \"macos\" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, aft_outline, or aft_zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete." } } } diff --git a/crates/aft/tests/fixtures/tool_provider_catalog_presets.json b/crates/aft/tests/fixtures/tool_provider_catalog_presets.json index 4d543025f9..495ad62533 100644 --- a/crates/aft/tests/fixtures/tool_provider_catalog_presets.json +++ b/crates/aft/tests/fixtures/tool_provider_catalog_presets.json @@ -3427,8 +3427,8 @@ "powershell_available": false, "remote_runs": true, "reply": { - "generation": "e460fd0d87318283d67f60864b956db4eb22930a5e2cd68fc852f8b36c9c186d", - "catalog_digest": "e460fd0d87318283d67f60864b956db4eb22930a5e2cd68fc852f8b36c9c186d", + "generation": "e5ff7b552fbdb37a38dd2a072811d768c25c9215417115eb102f44844336b256", + "catalog_digest": "e5ff7b552fbdb37a38dd2a072811d768c25c9215417115eb102f44844336b256", "tools": [ { "name": "status", @@ -3461,7 +3461,7 @@ "capabilities": [ "shell.exec/v1" ], - "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so a long command moves to the background while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; then wait on it with bash_watch before you report a result: a background task never wakes you, and a watch without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. Never background a command and immediately bash_watch it (that wastes a turn for what foreground returns in one); never loop bash_status to wait. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, outline, or zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete.", + "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so a long command moves to the background while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; then wait on it with bash_watch before you report a result: a background task never wakes you, and a watch without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. Never background a command and immediately bash_watch it (that wastes a turn for what foreground returns in one); never loop bash_status to wait. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: \"macos\" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, outline, or zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete.", "reply": { "max_ms": 2040000 }, @@ -3522,7 +3522,7 @@ }, "runon": { "type": "string", - "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options)." + "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). `macos` sends the command to a macOS machine for builds and tests that need macOS (no vCPU or network options)." } }, "required": [ @@ -4658,8 +4658,8 @@ "powershell_available": true, "remote_runs": true, "reply": { - "generation": "6b9c4a9e3b9910a3092718c20be891567a5515c697b9da526bb3cc0c2e0f6b6b", - "catalog_digest": "6b9c4a9e3b9910a3092718c20be891567a5515c697b9da526bb3cc0c2e0f6b6b", + "generation": "47fd95902f7ce34aa4dbcf26b846cd2f8a405d86cc9d65ddc1a60bfcb080ec53", + "catalog_digest": "47fd95902f7ce34aa4dbcf26b846cd2f8a405d86cc9d65ddc1a60bfcb080ec53", "tools": [ { "name": "status", @@ -4692,7 +4692,7 @@ "capabilities": [ "shell.exec/v1" ], - "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so a long command moves to the background while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; then wait on it with bash_watch before you report a result: a background task never wakes you, and a watch without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. Never background a command and immediately bash_watch it (that wastes a turn for what foreground returns in one); never loop bash_status to wait. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, outline, or zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete.", + "description": "Execute shell commands. Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks. Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting (it blocks up to bash.worker_wait_max_ms, 30 minutes by default, then reports the command is still running; watch again to keep waiting); Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`. Use it when you need the result before doing anything else; keep it off otherwise so a long command moves to the background while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; then wait on it with bash_watch before you report a result: a background task never wakes you, and a watch without a timeout waits up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. Never background a command and immediately bash_watch it (that wastes a turn for what foreground returns in one); never loop bash_status to wait. Do not change or delete files a background command uses until it finishes. A `nohup … &` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background, and is driven with bash_status({ outputMode: \"screen\" }) plus bash_write. Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report. `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive. When remote runs are available, put `runon: \"linux\"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: \"macos\" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.\n\nDO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — use the grep tool, read, outline, or zoom instead. When a list is cut, the reply ends with `shown N of M () · narrow: `; absence of that line means the list is complete.", "reply": { "max_ms": 2040000 }, @@ -4753,7 +4753,7 @@ }, "runon": { "type": "string", - "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options)." + "description": "Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add \",net\" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). `macos` sends the command to a macOS machine for builds and tests that need macOS (no vCPU or network options)." } }, "required": [ diff --git a/docs/tools.md b/docs/tools.md index d734062b97..5a6ef50dbc 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -306,7 +306,7 @@ recommended tool surface; experimental flags gate advanced behavior, not the too | `compressed` | boolean | Opt in/out of output compression for this call (default true; requires compression flag) | | `pty` | boolean | Run in a real PTY for interactive programs. Implies `background: true`. | | `ptyRows` / `ptyCols` | number | PTY dimensions (max 60 rows / 140 cols). Soft-ignored on non-PTY calls. | -| `runon` | string | Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). | +| `runon` | string | Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). `macos` sends the command to a macOS machine for builds and tests that need macOS (no vCPU or network options). | Calls admitted under the `worker` catalog preset set `NEXTEST_TEST_THREADS` and `RUST_TEST_THREADS` for local bash children from `../.cargo/alfonso-test-threads` (one decimal integer plus newline), defaulting to 4 when the file is missing, invalid, or above 256; valid values through 256 are preserved. Existing inherited or per-call values take precedence, and shell command prefixes still override them; head sessions and unscoped plugin worker flags receive no defaults, and exec-remote requests never carry these variables. @@ -346,7 +346,10 @@ build server (ck-motor, reached through the Subconscious daemon's `exec-remote/v command starts. Without a count, the job gets 2 vCPUs. Add `,net` for outbound internet: `linux,net`, `linux,4c,net` and `linux,net,4c` are accepted; remote jobs are offline by default. A plan's `default_demand` may include these options too. `runon: "windows"` sends the command to a Windows -build machine and runs it in Git Bash; it does not accept vCPU or network options. Duplicate +build machine and runs it in Git Bash; it does not accept vCPU or network options. `runon: "macos"` +sends the command to a macOS machine; it takes no vCPU or network options. Use it for builds and +tests that need macOS (its sandbox, code signing or file watching) instead of running them on this +machine; anything needing a service manager agent (such as a launchd agent) or a GUI session stays local. Duplicate options, unknown tokens and unsupported platforms are refused by name before dispatch. It runs there under `bash -c` in the same working directory, with the same timeout and the same environment, minus the secret-shaped and AFT/CortexKit control variables AFT strips before any @@ -357,7 +360,7 @@ Explicit whole-line `runon` also requires the user-only live safety switch `bash.runon_enabled: true` (default false); projects cannot enable it. Turning that switch off hides and refuses `runon` without disabling legacy prefix routing. -When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output here. +When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: "macos" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output here. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build. Who is offered `runon`: @@ -379,7 +382,7 @@ setting says. A call that sets `runon` is refused by name, and runs nowhere, when it cannot run remotely: the project turned remote runs off (`remote runs are off for this project`), the session has no remote runner (`this session has no remote runner`), the demand is not one AFT knows -(`unknown runner demand`; the server only runs Linux), an option is malformed (for example, +(`unknown runner demand`; AFT only runs Linux, macOS, and Windows jobs), an option is malformed (for example, `runon "linux,4" is not valid: options are net and a vCPU count as Nc, for example "linux,4c,net"`), or the call also sets `pty: true`, a PowerShell shell, or `sandbox: "host"`. On Windows `runon` is refused by name: remote dispatch diff --git a/packages/aft-bridge/src/bash-host-fallback.ts b/packages/aft-bridge/src/bash-host-fallback.ts index dbff8388e8..e7af5d555d 100644 --- a/packages/aft-bridge/src/bash-host-fallback.ts +++ b/packages/aft-bridge/src/bash-host-fallback.ts @@ -17,11 +17,11 @@ export const BASH_HOST_FALLBACK_REFUSAL = "AFT module connection is down"; * the rest. Shared by the OpenCode and Pi tools and the module catalog. */ export const BASH_RUNON_DESCRIPTION = - 'Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options).'; + 'Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). `macos` sends the command to a macOS machine for builds and tests that need macOS (no vCPU or network options).'; /** Guidance served only beside an available remote-run parameter. */ export const BASH_RUNON_GUIDANCE = - 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine\'s own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.'; + 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine\'s own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: "macos" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.'; export interface BashHostFallbackOptions { command: string; diff --git a/packages/opencode-plugin/src/__tests__/bash.test.ts b/packages/opencode-plugin/src/__tests__/bash.test.ts index 786b02a6fe..8aa5ac63bc 100644 --- a/packages/opencode-plugin/src/__tests__/bash.test.ts +++ b/packages/opencode-plugin/src/__tests__/bash.test.ts @@ -304,7 +304,7 @@ describe("OpenCode bash adapter", () => { "Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks)", ); expect(bash.description).toContain( - "or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree.", + 'or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: "macos" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree.', ); }, ); diff --git a/packages/opencode-plugin/src/__tests__/subc-tool-schemas-fresh.test.ts b/packages/opencode-plugin/src/__tests__/subc-tool-schemas-fresh.test.ts index da657bb645..9eb9a5e8ba 100644 --- a/packages/opencode-plugin/src/__tests__/subc-tool-schemas-fresh.test.ts +++ b/packages/opencode-plugin/src/__tests__/subc-tool-schemas-fresh.test.ts @@ -18,7 +18,7 @@ const PRESETS_PATH = path.join(REPO_ROOT, "crates", "aft", "src", "subc_tool_pre const REMOTE_PATH = path.join(REPO_ROOT, "crates", "aft", "src", "subc_tool_remote_schemas.json"); const REMOTE_GUIDANCE = - 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine\'s own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.'; + 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine\'s own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: "macos" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.'; const PLACEHOLDER = JSON.stringify({ type: "object" }); @@ -43,7 +43,7 @@ describe("subc tool schemas artifact", () => { expect((bash.properties as Record).runon).toMatchObject({ type: "string" }); expect((bash.properties as Record).runon).toMatchObject({ description: - 'Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options).', + 'Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options). `macos` sends the command to a macOS machine for builds and tests that need macOS (no vCPU or network options).', }); expect((bash.description as string).split(REMOTE_GUIDANCE)).toHaveLength(2); } diff --git a/packages/pi-plugin/src/__tests__/bash.test.ts b/packages/pi-plugin/src/__tests__/bash.test.ts index 61813eab7e..a8b6586bae 100644 --- a/packages/pi-plugin/src/__tests__/bash.test.ts +++ b/packages/pi-plugin/src/__tests__/bash.test.ts @@ -389,7 +389,7 @@ describe("bash tool adapter", () => { "Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks)", ); expect(bash.description).toContain( - "or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree.", + 'or runs a binary that already exists on this machine; a build or test that needs only macOS (its sandbox, code signing or file watching) can use runon: "macos" instead; building and testing still go remote, and a remote build leaves no binaries or target/ output in the local worktree.', ); }, );