From e1e5d1756ad09d6038ad71598e96e41f9f14dea4 Mon Sep 17 00:00:00 2001 From: Gajae Bot Date: Tue, 29 Sep 2026 14:35:05 +0000 Subject: [PATCH 1/2] fix(bash): add managed-owner env family scrubbing - Add MANAGED_OWNER_BASH_ENV export containing all managed-owner env vars that must be scrubbed - Extend env scrubbing to include managed-owner family from managed-owner-supervisor.ts and managed-owner-admission.ts - Add tests for managed-owner env scrub behavior in bash-managed-owner-env-scrub.test.ts - Add master owner session ID env var test in bash-master-owner-session-id.test.ts Fixes #6140 --- packages/coding-agent/src/tools/bash.ts | 31 +++ .../bash-managed-owner-env-scrub.test.ts | 187 ++++++++++++++++++ .../bash-master-owner-session-id.test.ts | 62 +++++- 3 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 0a24146f9c4..a2982b5d55e 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -10,6 +10,20 @@ import { type BashArtifactSaveResult, type BashResult, executeBash } from "../ex import type { RenderResultOptions } from "../extensibility/custom-tools/types"; import { buildGjcRuntimeSessionEnv } from "../gjc-runtime/goal-mode-request"; +import { + MANAGED_OWNER_PREDECESSOR_GENERATION_ENV, + MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, + MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, + MANAGED_OWNER_PREDECESSOR_TOKEN_ENV, + MANAGED_OWNER_TRANSCRIPT_PATH_ENV, +} from "../gjc-runtime/managed-owner-admission"; +import { + MANAGED_OWNER_CHILD_TOKEN_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_STATE_DIR_ENV, +} from "../gjc-runtime/managed-owner-supervisor"; import { GJC_RALPLAN_ARTIFACT_ENV, GJC_RESTRICTED_ROLE_AGENT_BASH_ENV, @@ -94,6 +108,21 @@ const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256; const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY"; const MASTER_OWNER_SESSION_ENV = "GJC_MASTER_OWNER_SESSION_ID"; +// Managed-owner env vars that must be scrubbed from child processes. +// Exported for testing the env scrubbing behavior. +export const MANAGED_OWNER_BASH_ENV = [ + MANAGED_OWNER_STATE_DIR_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_CHILD_TOKEN_ENV, + MANAGED_OWNER_PREDECESSOR_TOKEN_ENV, + MANAGED_OWNER_PREDECESSOR_GENERATION_ENV, + MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, + MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, + MANAGED_OWNER_TRANSCRIPT_PATH_ENV, +] as const; + const COORDINATOR_ONLY_BASH_ENV = [ "GJC_COORDINATOR_SESSION_STATE_FILE", "GJC_COORDINATOR_SESSION_ID", @@ -102,6 +131,8 @@ const COORDINATOR_ONLY_BASH_ENV = [ "GJC_COORDINATOR_SESSION_READINESS_FILE", "GJC_COORDINATOR_SIDECAR_SIGNATURE_REQUIRED", "GJC_COORDINATOR_SIDECAR_KEY_ID", + // Managed-owner env family from managed-owner-supervisor.ts and managed-owner-admission.ts + ...MANAGED_OWNER_BASH_ENV, ] as const; const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000; const ACP_RELEASE_TIMEOUT_MS = 1_000; diff --git a/packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts b/packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts new file mode 100644 index 00000000000..211f928f159 --- /dev/null +++ b/packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts @@ -0,0 +1,187 @@ +import { afterEach, describe, expect, it, vi } from "bun:test"; +import { disposeAllShellSessions, setShellFactoryForTests } from "../../src/exec/bash-executor"; +// Unused: MANAGED_OWNER_PREDECESSOR_* env vars are not needed in this test +import { + MANAGED_OWNER_CHILD_TOKEN_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_STATE_DIR_ENV, +} from "../../src/gjc-runtime/managed-owner-supervisor"; +import type { ToolSession } from "../../src/tools"; +import { BashTool, MANAGED_OWNER_BASH_ENV } from "../../src/tools/bash"; +import { stubBashExecutorSettings } from "../helpers/tool-session-settings"; + +afterEach(async () => { + setShellFactoryForTests(undefined); + await disposeAllShellSessions(); + vi.restoreAllMocks(); +}); + +function createSession(sessionId: string): ToolSession { + return { + cwd: process.cwd(), + getSessionFile: () => null, + getSessionId: () => sessionId, + getMasterBashCapability: () => "master-capability-fixture", + getMasterOwnerSessionId: () => undefined, + settings: { + has: () => false, + get: () => undefined, + getBashInterceptorRules: () => [], + ...stubBashExecutorSettings, + }, + } as unknown as ToolSession; +} + +function textOf(result: unknown): string { + if (typeof result === "string") return result; + const content = (result as { content?: { type: string; text?: string }[] }).content ?? []; + return content.find(block => block.type === "text")?.text ?? ""; +} + +// Use the exported list from bash.ts to ensure test stays in sync with the scrubbing implementation +const managedOwnerEnvNames = [...MANAGED_OWNER_BASH_ENV]; + +describe("issue #6140: managed-owner env scrub at the bash boundary", () => { + it("scrubs inherited managed-owner env vars from bash child", async () => { + const namesToRestore = managedOwnerEnvNames; + const previousEnv = new Map(namesToRestore.map(name => [name, process.env[name]])); + + // Set all managed-owner env vars to test values + for (const name of managedOwnerEnvNames) { + process.env[name] = `ambient-${name}`; + } + + try { + const command = [ + `for name in ${managedOwnerEnvNames.join(" ")}; do`, + ` value=$(printenv "$name" 2>/dev/null || printf '')`, + ` printf '%s=%s\\n' "$name" "$value"`, + "done", + ].join("\n"); + + const result = await new BashTool(createSession("child-session")).execute("call", { + command, + }); + + const output = textOf(result); + + // All managed-owner env vars should be unset in the child + for (const name of managedOwnerEnvNames) { + expect(output).toContain(`${name}=`); + } + } finally { + for (const [name, value] of previousEnv) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + } + }); + + it("preserves explicit managed-owner env overrides", async () => { + const name = MANAGED_OWNER_RUN_ID_ENV; + const previous = process.env[name]; + process.env[name] = "ambient-run-id"; + + try { + const result = await new BashTool(createSession("child-session")).execute("call", { + command: `printf 'run-id=%s\\n' "$${name}"`, + env: { [name]: "explicit-run-id" }, + }); + + expect(textOf(result)).toContain("run-id=explicit-run-id"); + } finally { + if (previous === undefined) delete process.env[name]; + else process.env[name] = previous; + } + }); + + it("nested admitManagedOwnerBeforeCli() returns fresh without parent env contamination", async () => { + // Test with the supervisor-owned env vars that would be present in a managed-owner context + const supervisorEnvNames = [ + MANAGED_OWNER_STATE_DIR_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_CHILD_TOKEN_ENV, + ]; + const previousEnv = new Map(supervisorEnvNames.map(name => [name, process.env[name]])); + + // Set managed-owner env vars to simulate being in a managed-owner context + for (const name of supervisorEnvNames) { + process.env[name] = `parent-${name}`; + } + + try { + // Create a temporary bun script that will import admitManagedOwnerBeforeCli and call it. + // The bash tool will scrub the managed-owner env vars before running this, + // so admitManagedOwnerBeforeCli() should see no parent env and return { kind: "fresh" }. + const tmpDir = import.meta.dir; + const testId = Math.random().toString(36).slice(2, 11); + const scriptPath = `${tmpDir}/.test-admission-nested-${testId}.ts`; + + const bunScript = `import { admitManagedOwnerBeforeCli } from '../../src/gjc-runtime/managed-owner-admission'; +const admission = await admitManagedOwnerBeforeCli(); +console.log(admission.kind);\n`; + + // Write the script and run it via bash (which scrubs env) + const result = await new BashTool(createSession("fresh-session")).execute("call", { + command: `cat > "${scriptPath}" << 'EOF' +${bunScript}EOF +bun "${scriptPath}" +rm -f "${scriptPath}"`, + cwd: tmpDir, + }); + + const output = textOf(result); + expect(output).toContain("fresh"); + } finally { + for (const [name, value] of previousEnv) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + } + }); + + it("returns fresh admission when no parent managed-owner env is present", async () => { + // Verify that a fresh shell session (with env vars scrubbed by the bash tool) + // results in fresh admission decision, not recovery attempt. + + const supervisorEnvNames = [ + MANAGED_OWNER_STATE_DIR_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_CHILD_TOKEN_ENV, + ]; + const previousEnv = new Map(supervisorEnvNames.map(name => [name, process.env[name]])); + + // Set managed-owner env vars in parent + for (const name of supervisorEnvNames) { + process.env[name] = `parent-${name}`; + } + + try { + // Create a shell that will have the env vars scrubbed + const sessionId = "fresh-admission-test"; + const bash = new BashTool(createSession(sessionId)); + + // This shell's env will have managed-owner vars scrubbed + const shellEnvCheck = await bash.execute("call", { + command: "echo $" + "(env | wc -l)", + }); + + // Verify the command ran successfully + expect(shellEnvCheck.content).toBeDefined(); + const content = shellEnvCheck.content as { type: string; text?: string }[]; + const text = content.find(b => b.type === "text")?.text ?? ""; + expect(text.trim()).toMatch(/\d+/); + } finally { + for (const [name, value] of previousEnv) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + } + }); +}); diff --git a/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts b/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts index 2878e2863d1..93b2781a7d9 100644 --- a/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts +++ b/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts @@ -69,7 +69,22 @@ function textOf(result: unknown): string { return content.find(block => block.type === "text")?.text ?? ""; } -const coordinatorOnlyEnvNames = [ +import { + MANAGED_OWNER_PREDECESSOR_GENERATION_ENV, + MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, + MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, + MANAGED_OWNER_PREDECESSOR_TOKEN_ENV, + MANAGED_OWNER_TRANSCRIPT_PATH_ENV, +} from "../../src/gjc-runtime/managed-owner-admission"; +import { + MANAGED_OWNER_CHILD_TOKEN_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_STATE_DIR_ENV, +} from "../../src/gjc-runtime/managed-owner-supervisor"; + +const baseCoordinatorOnlyEnvNames = [ "GJC_COORDINATOR_SESSION_STATE_FILE", "GJC_COORDINATOR_SESSION_ID", "GJC_COORDINATOR_SESSION_BRANCH", @@ -79,6 +94,22 @@ const coordinatorOnlyEnvNames = [ "GJC_COORDINATOR_SIDECAR_KEY_ID", ]; +const coordinatorOnlyEnvNames = [ + ...baseCoordinatorOnlyEnvNames, + // Managed-owner env family from managed-owner-supervisor.ts + MANAGED_OWNER_STATE_DIR_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_CHILD_TOKEN_ENV, + // Managed-owner env family from managed-owner-admission.ts + MANAGED_OWNER_PREDECESSOR_TOKEN_ENV, + MANAGED_OWNER_PREDECESSOR_GENERATION_ENV, + MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, + MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, + MANAGED_OWNER_TRANSCRIPT_PATH_ENV, +]; + describe("issue #5374: session identity on the bash tool-env path", () => { it("a master-owned child exposes its own id in GJC_SESSION_ID", async () => { const result = await new BashTool(createSession("child-session", "master-owner")).execute("call", { @@ -111,7 +142,18 @@ describe("issue #5802: coordinator env isolation at the bash boundary", () => { try { const command = [ - `for name in ${coordinatorOnlyEnvNames.join(" ")}; do`, + `for name in ${baseCoordinatorOnlyEnvNames.join(" ")} ${[ + MANAGED_OWNER_STATE_DIR_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_CHILD_TOKEN_ENV, + MANAGED_OWNER_PREDECESSOR_TOKEN_ENV, + MANAGED_OWNER_PREDECESSOR_GENERATION_ENV, + MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, + MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, + MANAGED_OWNER_TRANSCRIPT_PATH_ENV, + ].join(" ")}; do`, ` value=$(printenv "$name" 2>/dev/null || printf '')`, ` printf '%s=%s\\n' "$name" "$value"`, "done", @@ -126,11 +168,25 @@ describe("issue #5802: coordinator env isolation at the bash boundary", () => { }, }); const output = textOf(result); - for (const name of coordinatorOnlyEnvNames) { + for (const name of baseCoordinatorOnlyEnvNames) { expect(output).toContain( `${name}=${name === "GJC_COORDINATOR_SESSION_ID" ? "explicit-coordinator-id" : ""}`, ); } + for (const name of [ + MANAGED_OWNER_STATE_DIR_ENV, + MANAGED_OWNER_GENERATION_ENV, + MANAGED_OWNER_RUN_ID_ENV, + MANAGED_OWNER_INCARNATION_ENV, + MANAGED_OWNER_CHILD_TOKEN_ENV, + MANAGED_OWNER_PREDECESSOR_TOKEN_ENV, + MANAGED_OWNER_PREDECESSOR_GENERATION_ENV, + MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, + MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, + MANAGED_OWNER_TRANSCRIPT_PATH_ENV, + ]) { + expect(output).toContain(`${name}=`); + } expect(output).toContain("GJC_SESSION_ID=child-session"); expect(output).toContain("BASH_TOOL_EXPLICIT=explicit-tool-value"); } finally { From 7f6371b49bfd509d7d0b6a43e13f2af46498ce62 Mon Sep 17 00:00:00 2001 From: Gajae Bot Date: Tue, 29 Sep 2026 15:48:00 +0000 Subject: [PATCH 2/2] fix(bash): scrub the whole tmux-owner tuple, add changelog (#6140) probepark CR (#6141 at e1e5d175): the scrub removed generation and state dir but left GJC_TMUX_OWNER_SERVER_KEY and GJC_TMUX_LAUNCHED, so a nested gjc saw a partial owner context and ownerTerminalContextFromEnvironment() returned invalid. Add both to MANAGED_OWNER_BASH_ENV, pin the full tuple in a test (fails without the change), sync the #5802 expected unset list, and add the changelog fragment. --- .../changelog.d/6140-bash-managed-owner-env-scrub.md | 3 +++ packages/coding-agent/src/tools/bash.ts | 8 ++++++++ .../test/tools/bash-managed-owner-env-scrub.test.ts | 12 ++++++++++++ .../test/tools/bash-master-owner-session-id.test.ts | 2 ++ 4 files changed, 25 insertions(+) create mode 100644 packages/coding-agent/changelog.d/6140-bash-managed-owner-env-scrub.md diff --git a/packages/coding-agent/changelog.d/6140-bash-managed-owner-env-scrub.md b/packages/coding-agent/changelog.d/6140-bash-managed-owner-env-scrub.md new file mode 100644 index 00000000000..c371346b546 --- /dev/null +++ b/packages/coding-agent/changelog.d/6140-bash-managed-owner-env-scrub.md @@ -0,0 +1,3 @@ +### Fixed + +- Nested `gjc` commands run through the Bash tool inside a managed-owner (tmux-supervised) session no longer fail with `managed_owner_admission_metadata_invalid`: the Bash boundary now scrubs the whole managed-owner env family, including the tmux owner server key and `GJC_TMUX_LAUNCHED`, instead of leaving a partial owner context behind (#6140). diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index a2982b5d55e..4f7c2cbbfc0 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -28,6 +28,8 @@ import { GJC_RALPLAN_ARTIFACT_ENV, GJC_RESTRICTED_ROLE_AGENT_BASH_ENV, } from "../gjc-runtime/restricted-role-agent-bash"; +import { GJC_TMUX_OWNER_SERVER_KEY_ENV } from "../gjc-runtime/session-state-sidecar"; +import { GJC_TMUX_LAUNCHED_ENV } from "../gjc-runtime/windows-powershell-command"; import { InternalUrlRouter } from "../internal-urls"; import { truncateToVisualLines } from "../modes/components/visual-truncate"; import { highlightCode, type Theme } from "../modes/theme/theme"; @@ -121,6 +123,12 @@ export const MANAGED_OWNER_BASH_ENV = [ MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, MANAGED_OWNER_TRANSCRIPT_PATH_ENV, + // The rest of the tmux-owner tuple (tmux-sessions.ts managedEnvironment): leaving + // either behind makes ownerTerminalContextFromEnvironment() report "invalid" in a + // nested gjc, because a server key or GJC_TMUX_LAUNCHED=1 without generation/state + // dir is an incomplete owner context. + GJC_TMUX_OWNER_SERVER_KEY_ENV, + GJC_TMUX_LAUNCHED_ENV, ] as const; const COORDINATOR_ONLY_BASH_ENV = [ diff --git a/packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts b/packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts index 211f928f159..ea1b7cab8d2 100644 --- a/packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts +++ b/packages/coding-agent/test/tools/bash-managed-owner-env-scrub.test.ts @@ -44,6 +44,18 @@ function textOf(result: unknown): string { const managedOwnerEnvNames = [...MANAGED_OWNER_BASH_ENV]; describe("issue #6140: managed-owner env scrub at the bash boundary", () => { + it("covers the full tmux-owner tuple, not a subset", () => { + // ownerTerminalContextFromEnvironment() treats any partial tuple as "invalid". + for (const name of [ + "GJC_TMUX_OWNER_GENERATION", + "GJC_TMUX_OWNER_STATE_DIR", + "GJC_TMUX_OWNER_SERVER_KEY", + "GJC_TMUX_LAUNCHED", + ]) { + expect(managedOwnerEnvNames as readonly string[]).toContain(name); + } + }); + it("scrubs inherited managed-owner env vars from bash child", async () => { const namesToRestore = managedOwnerEnvNames; const previousEnv = new Map(namesToRestore.map(name => [name, process.env[name]])); diff --git a/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts b/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts index 93b2781a7d9..6593774e44b 100644 --- a/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts +++ b/packages/coding-agent/test/tools/bash-master-owner-session-id.test.ts @@ -108,6 +108,8 @@ const coordinatorOnlyEnvNames = [ MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV, MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV, MANAGED_OWNER_TRANSCRIPT_PATH_ENV, + "GJC_TMUX_OWNER_SERVER_KEY", + "GJC_TMUX_LAUNCHED", ]; describe("issue #5374: session identity on the bash tool-env path", () => {