From 642fe776244d09a99232daba8be20a34522f2fdf Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 12 Aug 2026 05:54:15 +0800 Subject: [PATCH] refactor(gateway): unify terminal restriction tracking (#122228) --- src/gateway/terminal/launch.test.ts | 34 ++++++++++++++ src/gateway/terminal/launch.ts | 69 ++++++++++++----------------- 2 files changed, 63 insertions(+), 40 deletions(-) diff --git a/src/gateway/terminal/launch.test.ts b/src/gateway/terminal/launch.test.ts index d24c3056f2c4..1a7de498b33f 100644 --- a/src/gateway/terminal/launch.test.ts +++ b/src/gateway/terminal/launch.test.ts @@ -81,6 +81,40 @@ describe("createTerminalLaunchPolicy", () => { } }); + it("keeps restart and commit restrictions isolated across agents", () => { + const baseConfig: OpenClawConfig = { + agents: { list: [{ id: "alpha" }, { id: "beta" }] }, + }; + const policy = createTerminalLaunchPolicy(baseConfig); + + policy.prepareConfig( + { + agents: { + list: [{ id: "alpha", sandbox: { mode: "all" } }, { id: "beta" }], + }, + }, + { restartPending: true }, + ); + policy.prepareConfig( + { + agents: { + list: [{ id: "alpha" }, { id: "beta", sandbox: { mode: "all" } }], + }, + }, + { restartPending: false }, + ); + + expect(policy.resolve("alpha").ok).toBe(false); + expect(policy.resolve("beta").ok).toBe(false); + + policy.acceptConfig({ retireRejectedRestart: false }); + expect(policy.resolve("alpha").ok).toBe(false); + expect(policy.resolve("beta").ok).toBe(true); + + policy.acceptConfig({ retireRejectedRestart: true }); + expect(policy.resolve("alpha").ok).toBe(true); + }); + it("keeps current launch details until a restart-bound change takes effect", () => { const workspace = tempDirs.make("term-policy-"); const policy = createTerminalLaunchPolicy({ diff --git a/src/gateway/terminal/launch.ts b/src/gateway/terminal/launch.ts index 95fe403a58a1..55d56c29ae3c 100644 --- a/src/gateway/terminal/launch.ts +++ b/src/gateway/terminal/launch.ts @@ -120,12 +120,14 @@ function resolveTerminalLaunch(params: { export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): TerminalLaunchPolicy { let activeConfig = initialConfig; let hasPendingRestart = false; - let terminalDisabledUntilRestart = false; let preparedConfig: OpenClawConfig | null = null; let appliedConfigWhileRestartPending: OpenClawConfig | null = null; - let terminalDisabledUntilCommit = false; - const blockedAgentsUntilRestart = new Map(); - const blockedAgentsUntilCommit = new Map(); + const createRestrictions = () => ({ + disabled: false, + blockedAgents: new Map(), + }); + const restartRestrictions = createRestrictions(); + const commitRestrictions = createRestrictions(); const preserveTerminalConfig = (config: OpenClawConfig, owner: OpenClawConfig) => { const { terminal: _ignored, ...gateway } = config.gateway ?? {}; const terminal = owner.gateway?.terminal; @@ -146,9 +148,12 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi configuredShell: terminalConfig?.shell, }); }; - const accumulateRestartRestrictions = (config: OpenClawConfig) => { + const accumulateRestrictions = ( + config: OpenClawConfig, + restrictions: ReturnType, + ) => { if (!isTerminalConfigEnabled(config)) { - terminalDisabledUntilRestart = true; + restrictions.disabled = true; return; } const activeAgentIds = new Set([ @@ -158,25 +163,13 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi for (const agentId of activeAgentIds) { const candidate = resolveForConfig(config, agentId); if (!candidate.ok) { - blockedAgentsUntilRestart.set(agentId, candidate.block); + restrictions.blockedAgents.set(agentId, candidate.block); } } }; - const accumulateCommitRestrictions = (config: OpenClawConfig) => { - if (!isTerminalConfigEnabled(config)) { - terminalDisabledUntilCommit = true; - return; - } - const activeAgentIds = new Set([ - ...listAgentIds(activeConfig), - resolveDefaultAgentId(activeConfig), - ]); - for (const agentId of activeAgentIds) { - const candidate = resolveForConfig(config, agentId); - if (!candidate.ok) { - blockedAgentsUntilCommit.set(agentId, candidate.block); - } - } + const clearRestrictions = (restrictions: ReturnType) => { + restrictions.disabled = false; + restrictions.blockedAgents.clear(); }; return { @@ -185,14 +178,14 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi if (!active.ok) { return active; } - if (terminalDisabledUntilRestart) { + if (restartRestrictions.disabled) { return { ok: false, block: { kind: "disabled" } }; } - const pendingBlock = blockedAgentsUntilRestart.get(active.plan.agentId); + const pendingBlock = restartRestrictions.blockedAgents.get(active.plan.agentId); if (pendingBlock) { return { ok: false, block: pendingBlock }; } - const preparedBlock = blockedAgentsUntilCommit.get(active.plan.agentId); + const preparedBlock = commitRestrictions.blockedAgents.get(active.plan.agentId); if (preparedBlock) { return { ok: false, block: preparedBlock }; } @@ -207,8 +200,8 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi }, isEnabled: () => isTerminalConfigEnabled(activeConfig) && - !terminalDisabledUntilRestart && - !terminalDisabledUntilCommit && + !restartRestrictions.disabled && + !commitRestrictions.disabled && (preparedConfig === null || isTerminalConfigEnabled(preparedConfig)), prepareConfig: (config, options) => { if (options.restartPending) { @@ -216,7 +209,7 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi // Keep an older candidate fail-closed only until this transaction is // accepted; do not mix its restrictions into the restart-owned bucket. preparedConfig = null; - accumulateRestartRestrictions(config); + accumulateRestrictions(config, restartRestrictions); return; } // No-op/hot plans may arrive with restart-only terminal fields that an @@ -224,11 +217,11 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi // terminal subtree already owned by the active or pending process. if (hasPendingRestart) { preparedConfig = preserveTerminalConfig(config, activeConfig); - accumulateCommitRestrictions(preparedConfig); + accumulateRestrictions(preparedConfig, commitRestrictions); return; } preparedConfig = preserveTerminalConfig(config, activeConfig); - accumulateCommitRestrictions(preparedConfig); + accumulateRestrictions(preparedConfig, commitRestrictions); }, commitConfig: () => { if (hasPendingRestart) { @@ -238,10 +231,9 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi appliedConfigWhileRestartPending = preparedConfig; } preparedConfig = null; - terminalDisabledUntilCommit = false; - blockedAgentsUntilCommit.clear(); + clearRestrictions(commitRestrictions); if (appliedConfigWhileRestartPending) { - accumulateCommitRestrictions(appliedConfigWhileRestartPending); + accumulateRestrictions(appliedConfigWhileRestartPending, commitRestrictions); } return; } @@ -249,20 +241,17 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi activeConfig = preparedConfig; } preparedConfig = null; - terminalDisabledUntilCommit = false; - blockedAgentsUntilCommit.clear(); + clearRestrictions(commitRestrictions); }, acceptConfig: (options) => { // Baseline acceptance retires an un-published candidate, including config // intentionally skipped by reload policy. Only onConfigApplied may stage // runtime truth for promotion after a rejected restart. preparedConfig = null; - terminalDisabledUntilCommit = false; - blockedAgentsUntilCommit.clear(); + clearRestrictions(commitRestrictions); if (options.retireRejectedRestart) { hasPendingRestart = false; - terminalDisabledUntilRestart = false; - blockedAgentsUntilRestart.clear(); + clearRestrictions(restartRestrictions); if (appliedConfigWhileRestartPending) { activeConfig = appliedConfigWhileRestartPending; } @@ -270,7 +259,7 @@ export function createTerminalLaunchPolicy(initialConfig: OpenClawConfig): Termi return; } if (appliedConfigWhileRestartPending) { - accumulateCommitRestrictions(appliedConfigWhileRestartPending); + accumulateRestrictions(appliedConfigWhileRestartPending, commitRestrictions); } }, };