From 219e75f3991f30069f680ffa3e8c39a11bcbb5ee Mon Sep 17 00:00:00 2001 From: Dan Shapiro <3732858+danshapiro@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:22:59 -0700 Subject: [PATCH 1/3] fix(runtime-protocol): approve current Claude --settings recipes and pin them with a drift guard 0908a4624 rewrote the Claude bell commands in freshell-platform without updating this crate's approved --settings digests, so every managed Claude launch was rejected ("managed provider arguments must use an approved non-secret recipe") for 10 days. The cloud vitest lanes never run test/integration/server, which masked it. - Add the current Unix/Windows recipe digests; keep the superseded ones so durable argv persisted under them keeps validating. - Extract APPROVED_CLAUDE_SETTINGS_DIGESTS. - Add a dev-dep on freshell-platform and a drift-guard test that validates the CURRENT recipes through validate_managed_terminal_args, so recipe drift fails rust-gate instead of production. --- crates/freshell-runtime-protocol/Cargo.toml | 5 ++ crates/freshell-runtime-protocol/src/lib.rs | 51 +++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/crates/freshell-runtime-protocol/Cargo.toml b/crates/freshell-runtime-protocol/Cargo.toml index 0d6952761e..d4a8298ab6 100644 --- a/crates/freshell-runtime-protocol/Cargo.toml +++ b/crates/freshell-runtime-protocol/Cargo.toml @@ -13,3 +13,8 @@ uuid = { version = "1", features = ["v4"] } sha2 = "0.10" hmac = "0.12" thiserror = "2" + +[dev-dependencies] +# Dev-only: the drift guard test asserts that the recipes built by +# freshell-platform are the ones this crate's approved digest list accepts. +freshell-platform = { path = "../freshell-platform" } diff --git a/crates/freshell-runtime-protocol/src/lib.rs b/crates/freshell-runtime-protocol/src/lib.rs index 445c5019ee..0cd33a1326 100644 --- a/crates/freshell-runtime-protocol/src/lib.rs +++ b/crates/freshell-runtime-protocol/src/lib.rs @@ -1977,6 +1977,23 @@ impl TerminalLaunchSpec { } } +/// Approved SHA-256 digests of the managed Claude `--settings` recipe +/// (`freshell_platform::cli_launch::claude_settings_json`). The recipe is +/// pinned by digest (not raw text) so the list stays a fixed-size constant. +/// Superseded recipe revisions stay listed so durable argv persisted under +/// them keeps validating. When `freshell-platform` changes the recipe, add +/// the new digest here — the drift-guard test +/// (`approved_claude_settings_digests_track_current_recipes`) fails with the +/// computed digest until you do. +const APPROVED_CLAUDE_SETTINGS_DIGESTS: [&str; 4] = [ + // Current recipes (since 0908a4624, Oct 4 2026: bell via hook response). + "41b86fd2aed9fa85ad0c3e99695de49fdadc36fcd6dc80d9fb53a2305b95cd71", // Unix + "b48c5d93ff2129438e2e10dc9eadc45849ec1b93c683a1ca08aebaa55d46a786", // Windows + // Superseded recipes (pre-0908a4624: bell via /dev/tty and CONOUT$). + "aa8900bab7bd9bcbb64634d60593a9217c1639b539cea23318122b3428d123c6", // Unix + "d2dcb96c036160b2469bcd732fdc8d767dd481f9c0c864ce47d58ed057652c4d", // Windows +]; + /// Durable argv accepts only provider launch selectors. Credential-bearing /// switches must be resolved through the child-only OneCLI path instead. pub fn validate_managed_terminal_args(mode: &str, args: &[String]) -> Result<(), RuntimeError> { @@ -2017,11 +2034,7 @@ pub fn validate_managed_terminal_args(mode: &str, args: &[String]) -> Result<(), let allowed = match (mode, flag) { ("claude", "--settings") => { let digest = format!("{:x}", sha2::Sha256::digest(value.as_bytes())); - matches!( - digest.as_str(), - "aa8900bab7bd9bcbb64634d60593a9217c1639b539cea23318122b3428d123c6" - | "d2dcb96c036160b2469bcd732fdc8d767dd481f9c0c864ce47d58ed057652c4d" - ) + APPROVED_CLAUDE_SETTINGS_DIGESTS.contains(&digest.as_str()) } ("claude", "--mcp-config") => matches!( value.as_str(), @@ -3046,6 +3059,34 @@ where mod tests { use super::*; + /// The managed Claude `--settings` payload is BUILT by freshell-platform + /// (`cli_launch::claude_settings_json`) but APPROVED by digest in this + /// crate. The two once drifted (0908a4624 rewrote the bell commands + /// without updating the approved digests) and every managed Claude launch + /// was rejected for 10 days. This guard pins the crates together: if the + /// recipe changes without the digest list, rust-gate fails HERE. + #[test] + fn approved_claude_settings_digests_track_current_recipes() { + let mut drift = Vec::new(); + for target in [ + freshell_platform::cli_launch::ProviderTarget::Unix, + freshell_platform::cli_launch::ProviderTarget::Windows, + ] { + let recipe = freshell_platform::cli_launch::claude_settings_json(target); + let digest = format!("{:x}", sha2::Sha256::digest(recipe.as_bytes())); + let args = vec!["--settings".to_string(), recipe]; + if validate_managed_terminal_args("claude", &args).is_err() { + drift.push(format!("{target:?}: {digest}")); + } + } + assert!( + drift.is_empty(), + "claude --settings recipe digests not in the approved list: \ + {drift:#?}; update APPROVED_CLAUDE_SETTINGS_DIGESTS in this \ + crate to match freshell-platform's current recipe" + ); + } + #[test] fn durable_terminal_argument_recipe_preserves_provider_options_and_legacy_modes() { for (provider, args) in [ From 29b601ed55ebd88efbd1b458bc0c118aa4d13aaf Mon Sep 17 00:00:00 2001 From: Dan Shapiro <3732858+danshapiro@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:22:59 -0700 Subject: [PATCH 2/3] test: un-retire test/integration/server and document its cloud-image exclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The directory was re-added 2026-09-28 as the live managed-provider parity suites but stayed on retiredNodeBackendPaths (failing every LOCAL architecture run since Oct 3) and kept its stale 'retired roots' comment in .dockerignore. Remove the stale list entry; document that the cloud-image exclusion is deliberate — the parity suites need a locally built Rust server binary and a Docker daemon, so local lanes cover them. --- .dockerignore | 7 ++++++- test/unit/architecture/fresh-agent-only-runtime.test.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index d682daf496..0ffef04518 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,10 +8,15 @@ playwright-report/ coverage/ .nyc_output/ -# Retired backend source/test roots must not enter the Rust-only source image. +# Retired Node backend roots must not enter the Rust-only source image. /server/ /test/server/ /test/unit/server/ +# /test/integration/server/ is NOT retired (live managed-provider parity +# suites) but stays OUT of the Cloud Run image deliberately: its tests build +# and spawn the real Rust server and need a Docker daemon (workload image), +# neither of which exists in the cloud container. They are covered by LOCAL +# lanes: the default vitest config (test:unit / base gate / pre-push). /test/integration/server/ # Version control and CI diff --git a/test/unit/architecture/fresh-agent-only-runtime.test.ts b/test/unit/architecture/fresh-agent-only-runtime.test.ts index 83772f4bff..f6e2fd6b49 100644 --- a/test/unit/architecture/fresh-agent-only-runtime.test.ts +++ b/test/unit/architecture/fresh-agent-only-runtime.test.ts @@ -13,7 +13,12 @@ const retiredNodeBackendPaths = [ 'server', 'test/server', 'test/unit/server', - 'test/integration/server', + // NOTE: 'test/integration/server' was retired with the Node backend, then + // RE-ADDED (2026-09-28) as the live managed-provider parity suites — it is + // a production test root now, NOT retired, and must NOT return to this + // list. Its suites need a locally built Rust server binary + Docker, so + // they run in local lanes (default vitest config, base gate) and are + // deliberately excluded from the Cloud Run image (.dockerignore). 'test/integration/session-repair.test.ts', 'tsconfig.server.json', 'dist/server', From 549aacd58ff4662574349ff0b70fe38f3ceae702 Mon Sep 17 00:00:00 2001 From: Dan Shapiro <3732858+danshapiro@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:23:04 -0700 Subject: [PATCH 3/3] chore: update Cargo.lock for the freshell-runtime-protocol dev-dependency --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 987056878b..150ee391b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1422,6 +1422,7 @@ dependencies = [ name = "freshell-runtime-protocol" version = "0.1.0" dependencies = [ + "freshell-platform", "hmac", "serde", "serde_json",