diff --git a/apps/desktop/src/main/engine/local-models/run-policy.ts b/apps/desktop/src/main/engine/local-models/run-policy.ts index 5d36cac0..1613d087 100644 --- a/apps/desktop/src/main/engine/local-models/run-policy.ts +++ b/apps/desktop/src/main/engine/local-models/run-policy.ts @@ -110,6 +110,8 @@ export function admitLocalConfiguration(read: ConfigRead, requirements: Requirem want("model_reasoning_summary", config.model_reasoning_summary ?? null, null); const features = record(config.features); want("features.view_image", features?.view_image ?? null, false); + want("features.multi_agent", features?.multi_agent ?? null, false); + want("features.multi_agent_v2", features?.multi_agent_v2 ?? null, false); const servers = record(config.mcp_servers); if (servers && Object.keys(servers).length) refuse(`MCP servers are configured (${Object.keys(servers).join(", ")}).`); return { sessionFlags, layers: summary, managedRequirements: false }; diff --git a/apps/desktop/test/local-model-execution.test.ts b/apps/desktop/test/local-model-execution.test.ts index 35a0dead..00cfeca5 100644 --- a/apps/desktop/test/local-model-execution.test.ts +++ b/apps/desktop/test/local-model-execution.test.ts @@ -279,6 +279,10 @@ test("a configuration another layer governs, managed requirements or a Codex wit const cases: [string, Parameters[0], RegExp][] = [ ["managed base_url", { configRead: r => { r.origins["model_providers.modex_local.base_url"] = { name: { type: "system", file: "/etc/codex/config.toml" }, version: "sha256:m" }; return r; } }, /the system configuration at \/etc\/codex\/config.toml sets `model_providers.modex_local.base_url`/], ["effective destination", { configRead: r => { r.config.model_providers.modex_local.base_url = "https://api.openai.com/v1"; return r; } }, /`model_providers.modex_local.base_url` is "https:\/\/api.openai.com\/v1"/], + ["enabled multi_agent", { configRead: r => { r.config.features.multi_agent = true; return r; } }, /`features\.multi_agent` is true/], + ["missing multi_agent", { configRead: r => { delete r.config.features.multi_agent; return r; } }, /`features\.multi_agent` is null/], + ["enabled multi_agent_v2", { configRead: r => { r.config.features.multi_agent_v2 = true; return r; } }, /`features\.multi_agent_v2` is true/], + ["missing multi_agent_v2", { configRead: r => { delete r.config.features.multi_agent_v2; return r; } }, /`features\.multi_agent_v2` is null/], ["requirements", { requirements: { allowedSandboxModes: ["read-only"] } }, /managed requirements/], ["old codex", { configRead: () => new Error("Method not found: config/read") }, /did not report its effective configuration \(Method not found: config\/read\)\. Local execution needs codex-cli 0.164.0 or later/], ]; diff --git a/apps/desktop/test/local-run-policy.test.ts b/apps/desktop/test/local-run-policy.test.ts index eaae1d95..7aa25ba2 100644 --- a/apps/desktop/test/local-run-policy.test.ts +++ b/apps/desktop/test/local-run-policy.test.ts @@ -6,20 +6,20 @@ const EXPECTED = { provider: "modex_local", origin: "http://127.0.0.1:18501", co const FLAGS = "sha256:b3932b4abe963781145150a5f8db02bf0afb5fe03385d52c15122db85463fce3"; const EMPTY = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; -/** The shape codex-cli 0.164.0 returned for an LM-3 child on 2026-10-27 (docs/reviews/2026-10-27-lm5-destination-policy.md), trimmed to what the policy reads. */ +/** The shape codex-cli 0.164.0 returned for an LM-3 child on 2026-10-27 (docs/reviews/2026-10-27-lm5-destination-policy.md), trimmed to what the policy reads, with subagent flags verified by the 2026-10-30 codex-cli 0.166.0 configuration-only probe. */ function read(): ConfigRead { const flags = { model_provider: "modex_local", model_providers: { modex_local: { name: "Modex local", base_url: "http://127.0.0.1:18501/v1", wire_api: "responses", requires_openai_auth: false, env_key: "MODEX_LOCAL_TOKEN", request_max_retries: 0 } }, - model_context_window: 32768, web_search: "disabled", features: { view_image: false }, shell_environment_policy: { exclude: ["MODEX_LOCAL_TOKEN"] }, + model_context_window: 32768, web_search: "disabled", features: { view_image: false, multi_agent: false, multi_agent_v2: false }, shell_environment_policy: { exclude: ["MODEX_LOCAL_TOKEN"] }, }; const origin = { name: { type: "sessionFlags" }, version: FLAGS }; return { config: { model: null, model_provider: "modex_local", model_context_window: 32768, web_search: "disabled", model_reasoning_summary: null, openai_base_url: null, mcp_servers: {}, model_providers: { modex_local: { ...flags.model_providers.modex_local, experimental_bearer_token: null, auth: null, http_headers: null, env_http_headers: null, query_params: null } }, - features: { view_image: false, mentions_v2: true, remote_control: false }, file_opener: "vscode" }, + features: { view_image: false, multi_agent: false, multi_agent_v2: false, mentions_v2: true, remote_control: false }, file_opener: "vscode" }, origins: Object.fromEntries(["model_provider", "model_providers.modex_local.name", "model_providers.modex_local.base_url", "model_providers.modex_local.wire_api", "model_providers.modex_local.requires_openai_auth", - "model_providers.modex_local.env_key", "model_providers.modex_local.request_max_retries", "model_context_window", "web_search", "features.view_image", "shell_environment_policy.exclude.0"].map(k => [k, origin])), + "model_providers.modex_local.env_key", "model_providers.modex_local.request_max_retries", "model_context_window", "web_search", "features.view_image", "features.multi_agent", "features.multi_agent_v2.enabled", "shell_environment_policy.exclude.0"].map(k => [k, origin])), layers: [{ name: { type: "sessionFlags" }, version: FLAGS, config: flags }, { name: { type: "user", file: "/tmp/process-x/.codex/config.toml", profile: null }, version: EMPTY, config: {} }, { name: { type: "system", file: "/etc/codex/config.toml" }, version: EMPTY, config: {} }], }; } @@ -94,3 +94,17 @@ test("governed keys and layer descriptions", () => { assert.equal(describeLayer({ type: "sessionFlags" }), "Modex's session settings"); assert.equal(describeLayer({ type: "something_new" }), "a something_new configuration layer"); }); + +for (const flag of ["multi_agent", "multi_agent_v2"] as const) { + for (const value of [true, undefined, null, "false", { enabled: true }]) { + test(`local admission refuses ${flag}=${JSON.stringify(value) ?? "missing"}`, () => { + const reported = read(); + const features = reported.config.features as Record; + if (value === undefined) delete features[flag]; + else features[flag] = value; + assert.throws(() => admitLocalConfiguration(reported, { requirements: null }, EXPECTED), + error => error instanceof Error && error.message.includes(`features.${flag}`), + "unqualified native subagents must be refused before any thread opens"); + }); + } +} diff --git a/docs/reviews/2026-10-30-lm5-subagent-policy.md b/docs/reviews/2026-10-30-lm5-subagent-policy.md new file mode 100644 index 00000000..5b8ef3b9 --- /dev/null +++ b/docs/reviews/2026-10-30-lm5-subagent-policy.md @@ -0,0 +1,13 @@ +# LM-5 effective subagent admission — 2026-10-30 + +Issue #380 requires unqualified native subagents to stay disabled. LocalExecution already passes `features.multi_agent=false` and `features.multi_agent_v2=false`, but admission verified only the effective image flag. A pure-policy probe using a modified historical receipt admitted either subagent flag as true. This establishes a missing effective-value check, not observed native subagent execution. + +Admission now requires both effective subagent flags to be explicitly false. True, missing, null, string and object values are refused by key before a native thread opens. Existing layer/provenance, destination and credential checks remain in force. A CLI that omits either flag is refused rather than treated as qualified; product local execution remains unadmitted. + +A configuration-only probe of installed codex-cli 0.166.0 in a temporary empty home reported both flags false and their origins as session flags. `features list` also reported both stable and false. The probe created no thread, sent no prompts and ran no inference. Its [trimmed receipt](2026-10-30-lm5-subagent-policy/config-receipt.json) supports the reported configuration shape, not offline execution or qualification of older CLI versions. + +## Verification + +The clean baseline passed build and full unit checks (2,244 passes, 26 skips). Ten new pure-policy cases first failed because no exception was thrown. After the two effective-value checks, the focused policy/execution suites passed 26 tests with no failures or skips. Execution fixtures also verify that enabled and missing values produce a failed turn with no `thread/start`, no `turn/start` and no admitted provenance. Final `pnpm build && pnpm test` completed with exit 0: 2,254 passes, 26 skips and zero failures. Independent read-only review found no blocking findings. Protected delivery remains pending. + +Logs are preserved outside the worktree under `~/Documents/Codex/artifacts/modex-land-and-prune/lm5-subagent-*`. This repair addresses an admission invariant only. The native offline canary, full auxiliary matrix, capacity/runtime qualification, entry-point policy, context evidence and human acceptance remain open; #380 is not complete. diff --git a/docs/reviews/2026-10-30-lm5-subagent-policy/config-receipt.json b/docs/reviews/2026-10-30-lm5-subagent-policy/config-receipt.json new file mode 100644 index 00000000..d8744dc9 --- /dev/null +++ b/docs/reviews/2026-10-30-lm5-subagent-policy/config-receipt.json @@ -0,0 +1,50 @@ +{ + "cli": "0.166.0", + "scope": "configuration-only in temporary empty home; no thread or inference", + "features": { + "multi_agent_v2": false, + "network_proxy": null, + "multi_agent": false, + "api_key_cyber_access_programs": true, + "api_key_model_discovery": true, + "auth_elicitation": true, + "background_paginated_rollout_migration": false, + "codex_apps_mcp_2026_07_28": false, + "mcp_2026_07_28": false, + "mcp_connection_sharing": false, + "memories": false, + "mentions_v2": true, + "remote_control": false, + "remote_plugin": true, + "tool_suggest": true, + "windows_sandbox_service": false + }, + "origins": { + "features.multi_agent": { + "name": { + "type": "sessionFlags" + }, + "version": "sha256:a9409150a66a83ac8ffe500daa6eba5fe887b9beb82608cc9a59f0f8ddf58548" + }, + "features.multi_agent_v2.enabled": { + "name": { + "type": "sessionFlags" + }, + "version": "sha256:a9409150a66a83ac8ffe500daa6eba5fe887b9beb82608cc9a59f0f8ddf58548" + } + }, + "session_flags": [ + { + "name": { + "type": "sessionFlags" + }, + "version": "sha256:a9409150a66a83ac8ffe500daa6eba5fe887b9beb82608cc9a59f0f8ddf58548", + "config": { + "features": { + "multi_agent": false, + "multi_agent_v2": false + } + } + } + ] +} diff --git a/docs/status.md b/docs/status.md index b0583e26..343614a1 100644 --- a/docs/status.md +++ b/docs/status.md @@ -233,6 +233,8 @@ The published application source is `2eda138` (v0.4.0). Later source deliveries - **Notes history identity repair.** Repeated history toggles could duplicate revision rows or leave history visible after closing because the history and editor siblings shared a React key. The isolated `notes-history-identity` repair gives them distinct identities; three regression repetitions failed before the fix and passed afterward. Independent review found no blockers. Build and full unit/E2E checks passed (2,191 unit passed, 23 skipped; 470 desktop passed, seven skipped; seven site passed). Protected delivery landed in #429 (`7a01df2`); its successful merge-queue run passed 473 desktop tests with four skips and seven site tests. See [repair evidence](reviews/2026-10-28-notes-history-identity.md). +- **LM-5 effective subagent admission.** A reproduced policy gap accepted enabled native subagent flags despite disabled launch settings. The repair requires both effective values to be false; ten regressions failed before the fix, and 26 focused policy/execution tests now pass, including refusal before a native thread opens. A configuration-only CLI 0.166.0 probe confirms the explicit false report shape. Build and full unit validation passed (2,254 passes, 26 skips); independent review found no blocking findings. Protected delivery remains pending; native qualification stays separate. [Evidence](reviews/2026-10-30-lm5-subagent-policy.md). + - **LM-2 credential repair, included in v0.4.0.** PR #418 delivered the credential-copy and stale-discovery fixes from closed #401, together with isolation that prevents fake child PIDs from reaching OS signals, at `6c9000f`. Build, full unit tests, typechecks and the Electron IPC journey are recorded in the [reconciliation evidence](reviews/2026-10-26-lm2-credential-repair.md). This source delivery is separate from local-runtime qualification and product admission. - **v0.4.0 post-publication acceptance and Companion 0.4.0 (9).** Desktop publication is complete. The v0.3.0-to-v0.4.0 background upgrade remains unverified: the installed Mac already reports signed 0.4.0 and was left unchanged. Companion build 9 has not been delivered by this release session; its TestFlight upload and physical-device checks remain separate. Human accessibility, real-provider and native-platform account/session checks retain their existing gates. [Publication evidence and limits](reviews/2026-10-24-v0.4.0-release-review.md#published--2026-10-30-utc).