Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3f6d1e6
fix(mcp): preserve external policy authority
apurvvkumaria Aug 24, 2026
7d8081e
fix(mcp): qualify policy before server removal
apurvvkumaria Aug 24, 2026
e27cae3
merge: update channel authority base
apurvvkumaria Aug 24, 2026
9a49b57
merge: refresh channel authority base
apurvvkumaria Aug 24, 2026
230dfb2
merge: update channel authority base
apurvvkumaria Aug 24, 2026
4408ffc
Merge branch 'codex/9833-channel-authority' into codex/9833-mcp-autho…
cv Aug 24, 2026
b20f50e
Merge branch 'codex/9833-channel-authority' into codex/9833-mcp-autho…
cv Aug 24, 2026
699dfe8
Merge branch 'codex/9833-channel-authority' into codex/9833-mcp-autho…
cv Aug 24, 2026
d5c023b
merge: refresh MCP authority base
apurvvkumaria Aug 24, 2026
e4cb412
Merge remote-tracking branch 'origin/codex/9833-channel-authority' in…
apurvvkumaria Aug 24, 2026
289540d
Merge branch 'codex/9833-channel-authority' into codex/9833-mcp-autho…
apurvvkumaria Aug 24, 2026
552a75e
Merge branch 'codex/9833-channel-authority' into codex/9833-mcp-autho…
apurvvkumaria Aug 24, 2026
49bb3a2
merge: restack channel authority
apurvvkumaria Aug 25, 2026
f2a1122
merge: restack channel authority
apurvvkumaria Aug 25, 2026
884b42e
merge: restack channel registry ownership
apurvvkumaria Aug 25, 2026
a5166e4
Merge branch 'codex/9833-channel-authority' into codex/9833-mcp-autho…
cv Aug 28, 2026
375332b
merge: sync channel authority into PR #10119
cv Aug 28, 2026
42f3b65
merge: refresh channel authority for PR #10119
cv Aug 28, 2026
bded320
ci: retrigger PR review advisor
cv Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions src/lib/actions/sandbox/destroy-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type SandboxDestroyExecutionInput = {
expectedContainerIdentity?: SandboxNameLabeledContainer | null;
portableContainerAuthority?: PreparedPortableDemoSandboxDestroyAuthority;
stopInferenceResources: () => void;
validateMcpPolicyAuthorityReceipt?: () => Promise<void>;
runtimeProviders?: RuntimeProviderBundleRegistry;
deps?: {
hostLocalInferenceLifecycleOptions?: HostLocalInferenceLifecycleOptions;
Expand Down Expand Up @@ -131,13 +132,16 @@ async function prepareMcpDestroy(
sandbox: SandboxEntry | null,
sandboxConfirmedAbsent: boolean,
force: boolean,
validateContainingPolicyReceipt?: () => Promise<void>,
): Promise<McpDestroyPreparation> {
if (Object.keys(sandbox?.mcp?.bridges ?? {}).length === 0) {
return emptyMcpDestroyPreparation();
}
const preparation = sandboxConfirmedAbsent
? await prepareMcpBridgesForAbsentSandboxDestroy(sandboxName, { force })
: await prepareMcpBridgesForDestroy(sandboxName);
: validateContainingPolicyReceipt
? await prepareMcpBridgesForDestroy(sandboxName, validateContainingPolicyReceipt)
: await prepareMcpBridgesForDestroy(sandboxName);
if (sandboxConfirmedAbsent && preparation.entries.length > 0) {
console.warn(
` ${YW}⚠${R} Sandbox '${sandboxName}' is already absent, so its retained-volume MCP adapter entry cannot be scrubbed in place. Exact OpenShell providers will be deleted so any stale credential placeholder cannot authenticate; same-name onboarding may need to replace stale MCP adapter config.`,
Expand Down Expand Up @@ -225,6 +229,7 @@ async function restoreMcpAfterDeleteAbort(
sandboxName: string,
preparation: McpDestroyPreparation,
hardened: HardenedDeleteState,
validateContainingPolicyReceipt?: () => Promise<void>,
): Promise<string | undefined> {
let recoveryFailure: string | undefined;
let openedRollbackWindow = false;
Expand All @@ -246,7 +251,13 @@ async function restoreMcpAfterDeleteAbort(
});
openedRollbackWindow = true;
}
await restoreMcpBridgesAfterDestroyAbort(sandboxName, preparation);
await (validateContainingPolicyReceipt
? restoreMcpBridgesAfterDestroyAbort(
sandboxName,
preparation,
validateContainingPolicyReceipt,
)
: restoreMcpBridgesAfterDestroyAbort(sandboxName, preparation));
} catch (error) {
recoveryFailure = redactDestroyError(error);
} finally {
Expand Down Expand Up @@ -287,6 +298,15 @@ async function finalizeMcpDestroy(
}
}

async function readMcpPolicyRefusal(revalidate?: () => Promise<void>): Promise<string | undefined> {
try {
await revalidate?.();
return undefined;
} catch (error) {
return redactDestroyError(error);
}
}

export async function executeSandboxDestroy({
cleanupShieldsArtifacts,
force,
Expand All @@ -299,6 +319,7 @@ export async function executeSandboxDestroy({
expectedContainerIdentity,
portableContainerAuthority,
stopInferenceResources,
validateMcpPolicyAuthorityReceipt,
runtimeProviders = CURRENT_RUNTIME_PROVIDER_BUNDLES,
deps = {},
}: SandboxDestroyExecutionInput): Promise<SandboxDestroyExecutionResult> {
Expand Down Expand Up @@ -440,7 +461,13 @@ export async function executeSandboxDestroy({
}
let mcpPreparation: McpDestroyPreparation;
try {
mcpPreparation = await prepareMcpDestroy(sandboxName, sandbox, sandboxConfirmedAbsent, force);
mcpPreparation = await prepareMcpDestroy(
sandboxName,
sandbox,
sandboxConfirmedAbsent,
force,
validateMcpPolicyAuthorityReceipt,
);
} catch (error) {
if (error instanceof McpBridgeError) {
return {
Expand Down Expand Up @@ -468,7 +495,12 @@ export async function executeSandboxDestroy({
): Promise<string | undefined> =>
sandboxConfirmedAbsent
? undefined
: await restoreMcpAfterDeleteAbort(sandboxName, mcpPreparation, hardenedState);
: await restoreMcpAfterDeleteAbort(
sandboxName,
mcpPreparation,
hardenedState,
validateMcpPolicyAuthorityReceipt,
);
const preparedContinuity = inspectIdentityContinuity();
if (preparedContinuity.status !== "match") {
const mcpRecoveryFailure = await restoreMcpForAbort(notHardened);
Expand Down Expand Up @@ -566,6 +598,23 @@ export async function executeSandboxDestroy({
` Managed inference cleanup and workspace wipe or hardening may already have run; inspect those resources before retrying.${detachedDetail}`,
);
}
try {
await mcpPreparation.revalidateBeforeDelete?.();
} catch (error) {
const mcpRecoveryFailure = await restoreMcpForAbort(hardened);
return {
ok: false as const,
deleteOutput:
`MCP policy authority changed at the sandbox delete boundary: ${redactDestroyError(error)}. ` +
"No sandbox delete was attempted.",
exitCode: error instanceof McpBridgeError ? error.exitCode : 1,
gatewayUnreachable: false,
hostLocalInferenceOwnershipRequiresGateway: false,
mcpOwnershipRequiresGateway: false,
mcpRecoveryFailure,
shieldsRelockRequiresGateway: false,
};
}
const deleteArgs = pendingPolicyVerification
? ["sandbox", "delete", "-g", pendingPolicyVerification.gatewayName, sandboxName]
: ["sandbox", "delete", sandboxName];
Expand Down Expand Up @@ -603,12 +652,25 @@ export async function executeSandboxDestroy({
!hardened.hardeningFailed;

if (deleteResult.status !== 0 && !alreadyGone && !forcedLocalCleanup) {
let policyRefusal: string | undefined;
try {
await mcpPreparation.revalidateBeforeDelete?.();
} catch (error) {
policyRefusal = redactDestroyError(error);
}
const mcpRecoveryFailure = sandboxConfirmedAbsent
? undefined
: await restoreMcpAfterDeleteAbort(sandboxName, mcpPreparation, hardened);
: await restoreMcpAfterDeleteAbort(
sandboxName,
mcpPreparation,
hardened,
validateMcpPolicyAuthorityReceipt,
);
return {
ok: false as const,
deleteOutput,
deleteOutput: policyRefusal
? `${deleteOutput}\nMCP policy authority revalidation also refused cleanup: ${policyRefusal}`
: deleteOutput,
exitCode: deleteResult.status || 1,
gatewayUnreachable,
...(timedOut ? { timedOut: true as const } : {}),
Expand All @@ -622,6 +684,10 @@ export async function executeSandboxDestroy({
};
}

let finalPolicyRefusal = forcedLocalCleanup
? undefined
: await readMcpPolicyRefusal(mcpPreparation.revalidateAfterDelete);

if (!forcedLocalCleanup && (portableContainerAuthority || expectedContainerIdentity)) {
try {
if (portableContainerAuthority) {
Expand Down Expand Up @@ -702,6 +768,26 @@ export async function executeSandboxDestroy({
};
}
}
if (!forcedLocalCleanup) {
const successEdgePolicyRefusal = await readMcpPolicyRefusal(
mcpPreparation.revalidateBeforeSuccess,
);
finalPolicyRefusal ??= successEdgePolicyRefusal;
}
if (finalPolicyRefusal) {
return {
ok: false as const,
deleteOutput:
`OpenShell reported sandbox '${sandboxName}' absent, but final MCP policy authority ` +
`revalidation refused success publication: ${finalPolicyRefusal}`,
exitCode: 1,
gatewayUnreachable: false,
hostLocalInferenceOwnershipRequiresGateway: false,
mcpOwnershipRequiresGateway: false,
shieldsRelockRequiresGateway: false,
deleteConfirmed: true,
};
}
return {
ok: true as const,
detachOutcome,
Expand Down
69 changes: 68 additions & 1 deletion src/lib/actions/sandbox/destroy-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,10 @@ describe("destroySandbox flow", () => {

await expect(harness.destroySandbox("alpha", { yes: true })).resolves.toBeUndefined();

expect(harness.prepareMcpBridgesForDestroySpy).toHaveBeenCalledWith("alpha");
expect(harness.prepareMcpBridgesForDestroySpy).toHaveBeenCalledWith(
"alpha",
expect.any(Function),
);
});

it("does not require mutable Hermes config for absent-sandbox cleanup", async () => {
Expand Down Expand Up @@ -1325,6 +1328,70 @@ describe("destroySandbox flow", () => {
expectMcpFinalizeAfterDelete(harness);
});

it("restores MCP state and withholds delete when policy authority drifts after preparation (#9833)", async () => {
const harness = createDestroyHarness({
mcpServers: ["github"],
revalidateMcpPolicyAuthority: async () => {
throw new Error("current MCP policy requirements changed");
},
});

await expect(harness.destroySandbox("alpha", { yes: true })).rejects.toThrow("process.exit(1)");

expect(harness.events).toContain("mcp-revalidate");
expect(harness.events).toContain("mcp-restore");
expect(harness.events).not.toContain("delete");
expect(harness.finalizeMcpBridgesAfterSandboxDeleteSpy).not.toHaveBeenCalled();
expect(harness.removeSandboxSpy).not.toHaveBeenCalled();
expect(harness.errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
"current MCP policy requirements changed",
);
});

it("finishes exact MCP cleanup but withholds success after a final authority refusal (#9833)", async () => {
const harness = createDestroyHarness({
mcpPolicyAuthorityAfterDeleteError: "first retained authority refusal",
mcpServers: ["github"],
policyAuthority: "externally-managed",
policyAuthorityDuringMcpFinalization: "nemoclaw-managed",
});

await expect(harness.destroySandbox("alpha", { yes: true })).rejects.toThrow("process.exit(1)");

expect(harness.events).toContain("delete");
expect(harness.finalizeMcpBridgesAfterSandboxDeleteSpy).toHaveBeenCalledOnce();
expect(harness.removeSandboxSpy).not.toHaveBeenCalled();
expect(harness.logSpy.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain(
"Sandbox destroyed",
);
expect(harness.errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
"first retained authority refusal",
);
expect(
harness.errorSpy.mock.calls.map(([message]) => String(message)).join("\n"),
).not.toContain("policy authority changed during destroy");
});

it("withholds success when policy authority drifts during MCP finalization (#9833)", async () => {
const harness = createDestroyHarness({
mcpServers: ["github"],
policyAuthority: "externally-managed",
policyAuthorityDuringMcpFinalization: "nemoclaw-managed",
});

await expect(harness.destroySandbox("alpha", { yes: true })).rejects.toThrow("process.exit(1)");

expect(harness.events).toContain("delete");
expect(harness.finalizeMcpBridgesAfterSandboxDeleteSpy).toHaveBeenCalledOnce();
expect(harness.removeSandboxSpy).not.toHaveBeenCalled();
expect(harness.logSpy.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain(
"Sandbox destroyed",
);
expect(harness.errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
"policy authority changed during destroy",
);
});

it("restores MCP runtime state when sandbox delete fails", async () => {
const harness = createDestroyHarness({
activeTimer: true,
Expand Down
23 changes: 23 additions & 0 deletions src/lib/actions/sandbox/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,24 @@ function requestSandboxDestroyExit(exitCode: number): never {
throw new SandboxDestroyExitRequest(exitCode);
}

function bindMcpDestroyPolicyAuthority(
sandboxName: string,
sandbox: registry.SandboxEntry | null,
): (() => Promise<void>) | undefined {
if (!sandbox || Object.keys(sandbox.mcp?.bridges ?? {}).length === 0) return undefined;
let authority = sandbox.policyAuthority;
return async () => {
const current = registry.getSandbox(sandboxName);
if (!current) {
throw new Error(`sandbox '${sandboxName}' is no longer registered`);
}
if (authority === undefined) authority = current.policyAuthority;
else if (current.policyAuthority !== authority) {
throw new Error(`sandbox '${sandboxName}' policy authority changed during destroy`);
}
};
}

export async function destroySandbox(
sandboxName: string,
options: string[] | DestroySandboxOptions = {},
Expand Down Expand Up @@ -594,6 +612,10 @@ async function destroySandboxUnlocked(
let destroyPreflight: ReturnType<typeof prepareSandboxDestroy>;
destroyPreflight = abortPreparedCleanupOnError(() => prepareSandboxDestroy(sandboxName));
const { cleanupGatewayName, runOpenshell, sandbox, sandboxConfirmedAbsent } = destroyPreflight;
const validateMcpPolicyAuthorityReceipt = bindMcpDestroyPolicyAuthority(
sandboxName,
sandboxConfirmedAbsent ? null : sandbox,
);
// Recheck identity after pre-delete qualification and recoverable journal
// publication reconciliation, before any sandbox runtime mutation.
if (portableContainerAuthority) {
Expand Down Expand Up @@ -637,6 +659,7 @@ async function destroySandboxUnlocked(
expectedContainerIdentity: initialIdentity?.identity,
...(portableContainerAuthority ? { portableContainerAuthority } : {}),
stopInferenceResources: () => stopSandboxInferenceResources(sandboxName, sandbox),
...(validateMcpPolicyAuthorityReceipt ? { validateMcpPolicyAuthorityReceipt } : {}),
});
} catch (error) {
preparedManagedLlamaCppCleanup?.abort();
Expand Down
Loading
Loading