Repository navigation
feat(agents): support native launch protection providers - #415
Conversation
8676c72 to
caf36d3
Compare
| let tools = tempfile::tempdir().unwrap(); | ||
| let mut saved = agent(dir.path()); | ||
| let runtime = bundle(tools.path()); | ||
| if worker == "goose" { |
There was a problem hiding this comment.
This bit could be further generalised
| }); | ||
| controller.action(&saved.id, Action::Stop).unwrap(); | ||
| let provider = dir.path().join("provider"); | ||
| fs::write( |
There was a problem hiding this comment.
Why do this?
caf36d3 to
7cf819d
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested. Three actionable launch-boundary findings are detailed inline. Address them with regression coverage through real ACP, including delayed launch and respawn; the current recording fixture replaces that boundary.
Reviewed head 7cf819d8e0c4fb56a8d535044d9dd1316dc2179a against base 6b4de53359b852e14f4c5f31afb1bb61793e3dc2. Independent security and lifecycle lanes reconciled. Hosted CI, including Rust/tool integration, JavaScript, browser checks and DCO, is green; Windows validation was skipped.
Validation: disposable fake workers under the hash-verified pinned ACP demonstrate direct/wrapped argument divergence. A disposable Seatbelt probe confirms the protected-path write conflict. The delayed-executable issue is source-traced. No live agents, credentials or relay were used; actual enforcement-plugin and live Goose UI behavior remain unverified.
| let mut protected_paths = self.protection_paths.clone()?; | ||
| protected_paths.push(self.store.root().to_path_buf()); | ||
| protected_paths.push( | ||
| provider | ||
| .executable | ||
| .parent() | ||
| .ok_or("Invalid protection launcher")? | ||
| .to_path_buf(), | ||
| ); | ||
| protected_paths.push(temporary.to_path_buf()); |
There was a problem hiding this comment.
[P1] Separate protected control data from writable runtime state
This list marks both the entire controller store and temporary as protected, but start_with_key sets BUZZ_AGENT_CONFIG_DIR=store.root() and TMPDIR/TMP/TEMP=temporary (runtime.rs:808-814). The pinned bundled worker stores its OAuth cache beneath $BUZZ_AGENT_CONFIG_DIR/buzz-agent/oauth; refresh requires creating/writing lock and cache files (auth.rs). A provider honoring these directory protections therefore breaks Databricks refresh and tools that write temporary files. A disposable Seatbelt probe denying writes beneath these supplied paths rejects both operations with Operation not permitted.
Keep writable temp/OAuth state outside the protected control-plane directories, or supply an explicit safe per-path contract. In particular, move launch-protection.json out of the worker-writable temp directory before allowing those writes: mode 0400 does not prevent same-user unlink/replacement through a writable parent, and subsequent workers reuse this context. Add a representative enforcing-provider test covering required writes while the launch context remains immutable.
| command | ||
| .env("BUZZ_ACP_AGENT_COMMAND", &provider.executable) | ||
| .env( | ||
| "BUZZ_ACP_AGENT_ARGS", | ||
| format!("--launch,{}", path.display()), |
There was a problem hiding this comment.
[P2] Preserve the worker identity when inserting the launcher
BUZZ_ACP_AGENT_COMMAND is not merely the executable path: the pinned ACP uses its basename for harness argument normalization and runtime-specific setup. Replacing it with the provider hides the actual worker before those steps, while the JSON context freezes the raw, pre-normalized arguments.
Concrete case: save a Goose agent with Advanced args [] (accepted by the controller), then enable protection. Unprotected, ACP supplies acp; protected, the provider receives args: [] and launches Goose without its ACP subcommand. An isolated probe with the actual hash-verified pinned ACP and a recording worker produced ['acp'] directly versus [] through the documented launcher protocol. The normalization is in config.rs:871-895; other identity-dependent setup is affected too.
Keep actual harness identity separate from the launch prefix, preferably at ACP's existing worker-spawn boundary, so wrapping happens without losing its normalization/environment behavior. Test direct/protected parity through real ACP; the new shell substitute bypasses this logic entirely.
| if executable_digest(&provider.executable)? != provider.digest { | ||
| return Err("Protection launcher changed; re-enable the plugin before starting".into()); | ||
| } |
There was a problem hiding this comment.
[P2] Bind delayed worker launches to the verified launcher bytes
This digest is checked only when starting the listener, not when the provider executable is actually run. The controller always sets BUZZ_ACP_LAZY_POOL=true and a 900-second idle sleep. The pinned ACP retains the provider pathname and executes it later on first work, idle wake and crash respawn (lib.rs:3036 onward). None of those paths returns through this check.
Register launcher A, start a protected listener without sending work, then replace/update the executable to B before the first event: B is launched without re-registration or digest refusal. The same occurs after idle or crash recovery. This is an unbounded lifecycle gap, not a narrow concurrent file-swap race, and contradicts this PR's changed-executable refusal contract.
Ensure each worker launch uses the verified version (or revalidates in a trusted spawn boundary). Add a gated regression that changes the launcher after listener start but before first worker launch, then exercises a later respawn. Existing running workers need not be terminated to fix this.
## Summary Replacing `BUZZ_ACP_AGENT_COMMAND` with a sandbox launcher hides the real adapter from Buzz: for example, Goose loses its default `acp` argument. Add optional `BUZZ_ACP_LAUNCH_PREFIX`, a JSON argument array applied at the shared subprocess boundary, after normal worker configuration. On Unix, every spawn runs `prefix... worker args...` without shell interpolation. Worker identity, environment setup, stdio and existing process cleanup are preserved. Invalid or unavailable prefixes fail launch without falling back to the worker; unset keeps direct launch on every platform. Configured prefixes fail closed on non-Unix because per-worker process-tree cleanup is unavailable. Desktop reserves the key against saved user-environment overrides, and spawn failures name the executable. ### Related issue Prerequisite for [Buzz-App #415](block/buzz-app#415). One commit directly on `main`, independent of #7942–#7944. No duplicate launch-prefix PR found. Policy enforcement, verified launcher staging, protected paths and the supporting runtime pin remain Buzz-App/plugin work. ### Testing - Original head `9ae9d82f8`: complete ACP suite (990 passed, three existing ignored), shipped local-task test, real-Goose probe, and full repository-wide `just ci` passed. - Review fixes: five launch tests and 44 Desktop environment-filter tests pass locally, covering the real production platform gate, save rejection, both merge layers, case variants, and executable error context. - Subprocess tests cover direct/wrapped argument defaults, Pi skills, Hermes/Codex environment, adapter identity, repeated spawns and invalid/missing prefix refusal. - Shipped `buzz-acp run` entrypoint completes a task through the wrapper and deterministic ACP peer. - Real Goose 1.52.0: direct and wrapped `auth-methods` return identical results; the wrapper receives `goose acp` with initially empty worker args. - Independent agent review: no blocking findings. - Required push checks passed on the updated head. The existing native Windows CI job now explicitly runs the platform-contract tests. Its result is pending; the non-Unix refusal branch was not exercised on this macOS host. Repeated-spawn coverage is not a full lazy-pool wake/crash test. Protected Goose in the Buzz-App UI and human acceptance remain pending. To try the hook after building `buzz-acp`, use an installed Goose path: ```sh BUZZ_ACP_LAUNCH_PREFIX='["/usr/bin/env"]' target/debug/buzz-acp auth-methods \ --agent-command /absolute/path/to/goose --agent-args '' --json ``` Expect the same authentication methods as without the prefix. Changing the prefix to `["/missing-launcher"]` must fail. Draft pending human testing. Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
7cf819d to
5be6a64
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: one P2 remains, detailed inline. The previous worker-identity and delayed-launch snapshot findings are resolved in source. OAuth/scratch writes are restored, but the control-isolation correction needs multi-run coverage before that finding is fully addressed.
Reviewed head 5be6a642eb901e50431a8f4ad79f56bf367380e4 against base 9ab4a1792b0d2cc60c9734baf1dc45911ee8bc83. Independent runtime and security lanes reconciled; static review and hosted evidence only, with no local runtime or enforcement probes in this pass.
Native CI passed on merge 3376bc0 of these exact revisions. Required CI is still red because WebKit 4/6 was cancelled during setup, before its journeys ran. App Windows validation was skipped. The real-provider test is macOS-only/opt-in; the PR’s reported local results cover the combined stack rather than this isolated head. Live-listener recovery and human acceptance remain separate, unverified gates.
Exit criteria: address the inline control-path omission with multi-run regression coverage and obtain a complete required CI run. This review does not attest live acceptance.
| let mut protected_paths = self.protection_paths.clone()?; | ||
| protected_paths.extend(self.store.protected_control_paths()); | ||
| protected_paths.push(self.ownership_root.clone()); | ||
| protected_paths.push(directory.to_path_buf()); | ||
| protected_paths.push(control.clone()); |
There was a problem hiding this comment.
[P2] Protect control snapshots across concurrent runs
protected_paths.push(control.clone()) protects only the current run. The other store entries cover settings/staging, not the shared runs namespace. Because start_with_key places every agent’s run under the same store (runtime.rs:843-850), a provider enforcing exactly the supplied paths can still permit writes to another run’s host-owned control snapshots when its policy allows writes beneath the app profile. That leaves the verified-launcher/context immutability contract incomplete for concurrent agents. The file modes are not a substitute for provider-enforced isolation between processes owned by the same user.
Give every protected worker coverage of all current and future run controls, for example by separating them into a shared protected control namespace, while keeping OAuth and per-run scratch writable and preserving supervisor-owned cleanup. Add a two-run regression asserting that each launch protects the other run’s controls without denying either run’s required writable state. Enumerating only currently existing runs would miss later launches.
This is a source-confirmed omission from the host/provider contract; I did not execute an enforcement-bypass probe or claim every provider policy permits the affected writes.
5be6a64 to
f5d41be
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Prior P2 addressed; no remaining blocking findings in this re-review. The correction meets the 9/10 bar. Every protected launch now includes the shared run-controls namespace, covering existing and future sibling snapshots while keeping runtime scratch and OAuth separate. Ownership transfers to the existing supervisor, which removes both per-run directories only after confirmed teardown and attempts both removals on cleanup failure. Independent security/path review agrees.
Carl, an automated reviewer, commenting via Wes’s GitHub account. COMMENT only, not approval. This supersedes the outstanding code finding in my previous review.
Reviewed head f5d41bea2038f054325d8ebe5f1414aee5983f91 against base 61d0bd57e097aae8755d306f6228ef76404056b4, concentrating on the changes since 5be6a642 and their lifecycle consequences.
- Verified evidence: hosted native CI checked merge
4dbfd910bbab29ce366733957712522dce3647c9of those exact revisions. The full workspace run passed, including controller 127 passed/1 ignored and the updated start/restart/stop, startup-abort, app-death, and cleanup-error regressions. Required CI and DCO are green; Windows validation is skipped. - Enforcement/recovery coverage: the new opt-in macOS fixture explicitly orders A's worker startup before B's controls exist, checks both directions, exercises staged launchers across lazy startup and worker crash replacement, and preserves the other run on Stop. I inspected the fixture, not executed it. The reported 26.48-second pass and fail-then-pass evidence belong to combined stack
bbe8e3d7; ordinary hosted CI does not exercise that macOS-only test. - Remaining acceptance: native-app/real-Goose and human acceptance remain outstanding, as disclosed. No local tests, live agents, or enforcement probes were run in this pass. The green browser rerun is not evidence that its intermittent clock failure was repaired.
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 thanks for turning this around. This head, f5d41bea, got two independent code reviews and an end-to-end run on macOS. None of them found a blocker.
All of the earlier feedback checks out in the code:
- OAuth/temp vs protected state:
TMPDIRis nowruns/agent-*/tmp, the launch context lives underrun-controls, and only the specific store control files are protected (plus thecontrol-writestaging dir), not the profile or scratch - worker identity: wrapping goes through ACP's
BUZZ_ACP_LAUNCH_PREFIXfrom block/buzz#7985 (the95b018c8pin). The prefix is applied at ACP's spawn boundary, after the worker's identity and args are normalized - delayed launches: the launcher is hashed and staged from a single read, and lazy starts and respawns run the staged copy
- cross-run controls: every launch protects the whole
run-controlsnamespace, and the supervisor removes the snapshot along with scratch after confirmed teardown - your two questions on the test file are answered by the
PROTECTION_WORKERStable and the comment on the fake provider, so those threads can be resolved
Gurney ran both opt-in tests that CI skips, at this head, with the real pinned ACP and an enforcing Seatbelt launcher:
real_listeners_protect_future_run_controls_and_recover_worker_crashespassed. Current and future peer controls resisted chmod/unlink/overwrite/atomic replace/rename. OAuth locking, atomic cache replacement and scratch writes still worked. A Goose-named worker got its normalizedacpargs. The first lazy launch and a crash replacement both used the staged bytes after the original launcher was swapped out, recovery and a follow-up prompt completed, and stopping one run left its peer aloneactual_bundled_acp_lazy_listener_start_restart_stop_and_quit_cleanuppassed- with both enabled, the full controller package ran 147 tests: all passed, none skipped
That closes the "inspected, not executed" gap on the macOS test. The workers are still synthetic, so this doesn't cover real Goose/inference, native UI, Databricks refresh, or enforcement on other OSes.
Two small things inline (nonblocking), plus:
- the description still says "Draft pending native-app and human acceptance", but the PR is marked ready and doesn't have
buzz-review-completed. Either move it back to draft or update the description once human acceptance is done - nonblocking:
docs/agent-security.md, which documents this layer's provider contract and how to run the opt-in test, lands in #421. If #421 trails, main has the contract without docs, so it might be worth moving the doc into this PR
| assert_eq!(context["policy"], json!({"synthetic":true})); | ||
| assert_eq!(context["workspace"], saved.workspace); | ||
| let paths = context["protectedPaths"].as_array().unwrap(); | ||
| for path in [ |
There was a problem hiding this comment.
this checks the paths that have to be protected, but nothing checks that writable state stays out of the list. right now only the opt-in macOS test would catch someone bringing back the broad profile/temp protection from the first review. could we also assert that no protectedPaths entry equals, or is an ancestor of, config/buzz-agent/oauth or this run's TMPDIR? nonblocking
| } => { | ||
| validate(&binding)?; | ||
| self.require_provider(&binding)?; | ||
| let mut doc = self.store.read()?; |
There was a problem hiding this comment.
nonblocking: every other agent mutation lives on Store (save, use_here, enabled, ...), but this one does its own read/check/write, which is why Document/read/write had to become pub(crate). the revision bump here also skips the 9_007_199_254_740_991 cap that use_here and apply enforce, so hitting it fails later in validation with a generic error. moving this into something like Store::set_launch_protection(id, revision, binding) would keep read/write private and pick up the same cap. the Defaults request in #420 could go the same way
wesbillman
left a comment
There was a problem hiding this comment.
No blocking findings in this re-review. The delta meets the 9/10 bar. Store now owns protection validation/persistence without changing the public security::Binding API; the revision-limit regression preserves saved bytes, and both worker fixtures check writable OAuth/temp paths on Start and Restart. The provider contract documentation is included in this layer. Independent persistence and test/docs reviews reconciled.
Carl, an automated reviewer, commenting via Wes’s GitHub account. COMMENT only, not approval.
Reviewed head 15370100e405d07faab81a16d534c1763af786ff against reported base 61d0bd57e097aae8755d306f6228ef76404056b4, concentrating on changes since the previously cleared f5d41bea2038f054325d8ebe5f1414aee5983f91.
- Validation: hosted CI and DCO passed. Native CI checked merge
c0be46eabc253dbd5fa188b39036cc45bf3103d3of this head into73064e34dfa5f14c472c3661589d05f509c17884; the full Rust workspace passed, including controller 128 passed / 1 ignored and both changed regression tests. Those controller/native-agent sources are unchanged between the reported base and CI base. Clean working tree and diff checks; no local tests or enforcement probes run in this pass. - Remaining gates: GitHub currently reports merge conflicts; resolve them and validate the integrated head before merge. Native-app/real-Goose and human acceptance, plus non-macOS enforcement, remain unverified as disclosed. Windows CI was skipped; Linux CI does not exercise the opt-in macOS enforcing-provider test. These are integration/acceptance limits, not new code findings.
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
1537010 to
a65b1d8
Compare
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
* origin/main: (82 commits) Test provider connections before model selection (#500) Bundle Goose ACP with Buzz (#497) Discover saved identities across joined communities with names, pictures and retry (#291) Clarify design-system documentation and unify component examples (#498) feat(composer): convert typed Markdown live and refuse control characters committed as text (#455) fix(messages): stop three timeline scroll races that flake CI (#456) Improve Agent defaults pickers and provider keys (#392) fix(threads): keep thread history painted after scroll corrections (#493) feat(plugins): expose the agent protection service (#421) perf(sidebar): re-render only the changed row on a channel-list publish (#480) feat(agents): copy protection defaults into new agents (#420) feat(agents): support native launch protection providers (#415) fix(composer): prevent WebKit overpainting mention selections (#490) fix(composer): prevent arrow keys from inserting control characters (#488) perf(channels): fall back to one exact roster read when confirming agent adds (#485) fix(media): pause video only on comment composer focus (#483) fix(channels): dismiss management modals with outside clicks (#479) perf: reuse message date formats and stable reaction shortcuts (#477) feat(profile): run an unattended scenario file in web profiling (#476) feat(channels): administer channel members and roles (#453) ... Signed-off-by: John Tennant <jtennant@block.xyz> # Conflicts: # src/app/shell/usePanelLauncher.ts # src/bundled/agents/AgentsPage.tsx # src/bundled/agents/InventoryIdentityCard.tsx # src/bundled/agents/InventoryView.tsx # src/bundled/agents/UnifiedInventory.tsx # src/bundled/agents/index.tsx
* origin/main: (82 commits) Test provider connections before model selection (#500) Bundle Goose ACP with Buzz (#497) Discover saved identities across joined communities with names, pictures and retry (#291) Clarify design-system documentation and unify component examples (#498) feat(composer): convert typed Markdown live and refuse control characters committed as text (#455) fix(messages): stop three timeline scroll races that flake CI (#456) Improve Agent defaults pickers and provider keys (#392) fix(threads): keep thread history painted after scroll corrections (#493) feat(plugins): expose the agent protection service (#421) perf(sidebar): re-render only the changed row on a channel-list publish (#480) feat(agents): copy protection defaults into new agents (#420) feat(agents): support native launch protection providers (#415) fix(composer): prevent WebKit overpainting mention selections (#490) fix(composer): prevent arrow keys from inserting control characters (#488) perf(channels): fall back to one exact roster read when confirming agent adds (#485) fix(media): pause video only on comment composer focus (#483) fix(channels): dismiss management modals with outside clicks (#479) perf: reuse message date formats and stable reaction shortcuts (#477) feat(profile): run an unattended scenario file in web profiling (#476) feat(channels): administer channel members and roles (#453) ... Signed-off-by: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz> # Conflicts: # src/app/shell/usePanelLauncher.ts # src/bundled/agents/AgentsPage.tsx # src/bundled/agents/InventoryIdentityCard.tsx # src/bundled/agents/InventoryView.tsx # src/bundled/agents/UnifiedInventory.tsx # src/bundled/agents/index.tsx
…ad-on-send * origin/main: (155 commits) Discover saved identities across joined communities with names, pictures and retry (#291) Clarify design-system documentation and unify component examples (#498) feat(composer): convert typed Markdown live and refuse control characters committed as text (#455) fix(messages): stop three timeline scroll races that flake CI (#456) Improve Agent defaults pickers and provider keys (#392) fix(threads): keep thread history painted after scroll corrections (#493) feat(plugins): expose the agent protection service (#421) perf(sidebar): re-render only the changed row on a channel-list publish (#480) feat(agents): copy protection defaults into new agents (#420) feat(agents): support native launch protection providers (#415) fix(composer): prevent WebKit overpainting mention selections (#490) fix(composer): prevent arrow keys from inserting control characters (#488) perf(channels): fall back to one exact roster read when confirming agent adds (#485) fix(media): pause video only on comment composer focus (#483) fix(channels): dismiss management modals with outside clicks (#479) perf: reuse message date formats and stable reaction shortcuts (#477) feat(profile): run an unattended scenario file in web profiling (#476) feat(channels): administer channel members and roles (#453) fix(shell): simplify top-bar controls and refine profile dropdown (#435) Fix macOS window dragging during identity setup (#424) ... # Conflicts: # src/features/messages/MessageComposer.tsx
## Summary Replacing `BUZZ_ACP_AGENT_COMMAND` with a sandbox launcher hides the real adapter from Buzz: for example, Goose loses its default `acp` argument. Add optional `BUZZ_ACP_LAUNCH_PREFIX`, a JSON argument array applied at the shared subprocess boundary, after normal worker configuration. On Unix, every spawn runs `prefix... worker args...` without shell interpolation. Worker identity, environment setup, stdio and existing process cleanup are preserved. Invalid or unavailable prefixes fail launch without falling back to the worker; unset keeps direct launch on every platform. Configured prefixes fail closed on non-Unix because per-worker process-tree cleanup is unavailable. Desktop reserves the key against saved user-environment overrides, and spawn failures name the executable. ### Related issue Prerequisite for [Buzz-App block#415](block/buzz-app#415). One commit directly on `main`, independent of block#7942–block#7944. No duplicate launch-prefix PR found. Policy enforcement, verified launcher staging, protected paths and the supporting runtime pin remain Buzz-App/plugin work. ### Testing - Original head `9ae9d82f8`: complete ACP suite (990 passed, three existing ignored), shipped local-task test, real-Goose probe, and full repository-wide `just ci` passed. - Review fixes: five launch tests and 44 Desktop environment-filter tests pass locally, covering the real production platform gate, save rejection, both merge layers, case variants, and executable error context. - Subprocess tests cover direct/wrapped argument defaults, Pi skills, Hermes/Codex environment, adapter identity, repeated spawns and invalid/missing prefix refusal. - Shipped `buzz-acp run` entrypoint completes a task through the wrapper and deterministic ACP peer. - Real Goose 1.52.0: direct and wrapped `auth-methods` return identical results; the wrapper receives `goose acp` with initially empty worker args. - Independent agent review: no blocking findings. - Required push checks passed on the updated head. The existing native Windows CI job now explicitly runs the platform-contract tests. Its result is pending; the non-Unix refusal branch was not exercised on this macOS host. Repeated-spawn coverage is not a full lazy-pool wake/crash test. Protected Goose in the Buzz-App UI and human acceptance remain pending. To try the hook after building `buzz-acp`, use an installed Goose path: ```sh BUZZ_ACP_LAUNCH_PREFIX='["/usr/bin/env"]' target/debug/buzz-acp auth-methods \ --agent-command /absolute/path/to/goose --agent-args '' --json ``` Expect the same authentication methods as without the prefix. Changing the prefix to `["/missing-launcher"]` must fail. Draft pending human testing. Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Protected local agents run through a verified launcher snapshot using the merged ACP launch-prefix hook from block/buzz#7985. The bundled runtime and native Buzz dependency are pinned to
95b018c8. Missing or changed providers fail closed; the host has no sandbox-engine dependency.The existing supervisor owns staged launcher/context and scratch directories until confirmed teardown. Workers receive the shared
run-controlsnamespace, covering concurrent and future runs while OAuth caches and scratch remain writable.Storeowns protection persistence, validation, atomic writes and JavaScript-safe revision bounds. The native provider contract ships indocs/agent-security.md.Stack
mainAll three PRs are merged into upstream
mainin this order. Each successor was rebased and retargeted tomainafter its parent was squash-merged.Validation
Rebased onto
mainat5d5094e2; the launch-path conflict preserves both upstream Pi preflight verification and protected scratch setup. Native validation ata65b1d8d; final head95e1a1adadds only the one-line CI repair below:dismissOnOutsideClickattribute in the channel-members dialog. Removing that duplicate preserves behavior and fixes the same lint failure present on main. Full lint, TypeScript, 162 related frontend tests and design checks passed after this one-line repair.The enforcing integration uses synthetic relay/workers. Real Goose/inference, native UI/human acceptance and non-macOS enforcement have not been verified by this work.
Hosted CI
All required CI lanes passed at
95e1a1ad, including all twelve browser shards. DCO and security checks passed. Squash-merged as5e241502.