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
b150040
fix(mcp): pin provider inspection target
apurvvkumaria Aug 30, 2026
ab24e8d
fix(mcp): pin collision inspection target
apurvvkumaria Aug 30, 2026
66c37e4
fix(mcp): pin provider lifecycle target
apurvvkumaria Aug 31, 2026
52e0890
fix(mcp): pin provider lifecycle commands
apurvvkumaria Aug 31, 2026
8aa8e1f
test(mcp): seed Hermes startup target
apurvvkumaria Aug 31, 2026
32bb524
test(mcp): assert Hermes startup target
apurvvkumaria Aug 31, 2026
627e9e3
merge: resolve conflicts with main
github-actions[bot] Aug 31, 2026
b7366ad
fix(mcp): bind lifecycle operations to sandbox target
apurvvkumaria Aug 31, 2026
9b21776
merge: reconcile current main into provider target hardening
apurvvkumaria Aug 31, 2026
3d8de5d
merge: reconcile Hermes lifecycle package API
apurvvkumaria Aug 31, 2026
b980933
fix(rebuild): bind authoritative preflight target
apurvvkumaria Aug 31, 2026
f1e6024
test(ci): isolate runtime target fixtures
apurvvkumaria Aug 31, 2026
b96a765
fix(openshell): replace ambient runtime selectors
apurvvkumaria Sep 1, 2026
c84baf4
fix(openshell): explain target-drift recovery
apurvvkumaria Sep 1, 2026
3199053
merge: resolve conflicts with main
github-actions[bot] Sep 1, 2026
31d7fb6
merge: integrate current main into provider target hardening
apurvvkumaria Sep 1, 2026
0bbe283
fix(mcp): complete runtime target review evidence
apurvvkumaria Sep 1, 2026
341db9a
test(sandbox): preserve runtime mock exports
apurvvkumaria Sep 1, 2026
4ccffcb
merge(main): sync current main
apurvvkumaria Sep 1, 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
3 changes: 3 additions & 0 deletions src/lib/actions/sandbox/backup-shields-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { RD as _RD, G, R, YW } from "../../cli/terminal-style";
import type { OpenShellRuntimeSelection } from "../../adapters/openshell/runtime";
import * as shields from "../../shields";
import { isShieldsTimerDeadlineExpired } from "../../state/mcp-lifecycle-lock/shields-timer-authority";

Expand All @@ -18,6 +19,7 @@ export interface BackupShieldsWindowOptions {
shieldsUpCommand: string;
deferAutoRestoreWhileOwnerAlive?: boolean;
allowLegacyHermesProtocol?: boolean;
runtimeSelection?: OpenShellRuntimeSelection;
}

export function openBackupShieldsWindow(
Expand Down Expand Up @@ -102,6 +104,7 @@ export function relockBackupShieldsWindow(
throwOnError: true,
...(options.allowLegacyHermesProtocol ? { allowLegacyHermesProtocol: true } : {}),
...(policySnapshotRecovery ? { policySnapshotRecovery } : {}),
...(options.runtimeSelection ? { runtimeSelection: options.runtimeSelection } : {}),
});
console.log(` ${G}✓${R} Shields restored to UP`);
window.relocked = true;
Expand Down
19 changes: 19 additions & 0 deletions src/lib/actions/sandbox/destroy-confirmation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ describe("destroy confirmation", () => {
expect(prompt).not.toHaveBeenCalled();
});

it("uses the recorded OpenShell target for active-session detection (#10514)", async () => {
const createSessionDeps = vi.spyOn(sandboxSession, "createSystemDeps");
stubActiveSessions([]);
vi.spyOn(console, "log").mockImplementation(() => undefined);
const runtimeSelection = {
gatewayName: "nemoclaw-9090",
workspace: "default",
localTlsDir: "/authority/tls",
};

await expect(
confirmSandboxDestroy("test-sb", { yes: true }, runtimeSelection),
).resolves.toBe(true);

expect(createSessionDeps).toHaveBeenCalledWith("/usr/bin/openshell", {
runtimeSelection,
});
});

it("warns about active sessions when --force skips the prompt (#9855)", async () => {
stubActiveSessions([4242, 4243]);
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
Expand Down
14 changes: 11 additions & 3 deletions src/lib/actions/sandbox/destroy-confirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { resolveOpenshell } from "../../adapters/openshell/resolve";
import type { OpenShellRuntimeSelection } from "../../adapters/openshell/runtime-selection";
import { R, YW } from "../../cli/terminal-style";
import { prompt as askPrompt } from "../../credentials/store";
import type { DestroySandboxOptions } from "../../domain/lifecycle/options";
Expand All @@ -12,11 +13,17 @@ import {
type SandboxSession,
} from "../../state/sandbox-session";

function findActiveSandboxSessions(sandboxName: string): SandboxSession[] {
function findActiveSandboxSessions(
sandboxName: string,
runtimeSelection?: OpenShellRuntimeSelection,
): SandboxSession[] {
const opsBin = resolveOpenshell();
if (!opsBin) return [];
try {
const result = getActiveSandboxSessions(sandboxName, createSessionDeps(opsBin));
const result = getActiveSandboxSessions(
sandboxName,
createSessionDeps(opsBin, runtimeSelection ? { runtimeSelection } : {}),
);
return result.detected ? result.sessions : [];
} catch {
return [];
Expand Down Expand Up @@ -47,8 +54,9 @@ export function assertSandboxDestroyCommandAvailable(sandboxName: string): void
export async function confirmSandboxDestroy(
sandboxName: string,
options: DestroySandboxOptions,
runtimeSelection?: OpenShellRuntimeSelection,
): Promise<boolean> {
const activeSessions = findActiveSandboxSessions(sandboxName);
const activeSessions = findActiveSandboxSessions(sandboxName, runtimeSelection);
// #9855: --yes/--force waives the confirmation prompt, not the notice that
// this destroy is about to break somebody else's live SSH session. Without
// this the operator sees no warning and the connected terminal just gets a
Expand Down
103 changes: 93 additions & 10 deletions src/lib/actions/sandbox/destroy-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import { isDeepStrictEqual } from "node:util";

import { buildSelectedOpenShellSubprocessEnv } from "../../adapters/openshell/runtime-selection";
import type { OpenShellRuntimeSelection } from "../../adapters/openshell/runtime-selection";
import { getSandboxDeleteOutcome } from "../../domain/sandbox/destroy";
import { inspectOpenShellSandboxIdentityFingerprint } from "../../adapters/openshell/policy-state";
import { R, YW } from "../../cli/terminal-style";
Expand Down Expand Up @@ -67,6 +69,7 @@ type SandboxDestroyExecutionInput = {
getSandbox?: (sandboxName: string) => SandboxEntry | null;
listSandboxes?: () => { sandboxes: SandboxEntry[] };
runOpenshell: DestroyRunOpenshell;
mcpRuntimeSelection?: McpDestroyPreparation["runtimeSelection"];
sandbox: SandboxEntry | null;
sandboxConfirmedAbsent: boolean;
sandboxName: string;
Expand Down Expand Up @@ -94,6 +97,7 @@ export type SandboxDestroyExecutionResult =
deleteResult: ReturnType<DestroyRunOpenshell>;
detachOutcome: DetachSandboxProvidersResult;
forcedLocalCleanup: boolean;
runtimeSelection?: OpenShellRuntimeSelection;
/** Common lifecycle conclusively retired this row's explicit llama.cpp claim. */
commonLlamaCppAuthorityRetired?: true;
}
Expand All @@ -119,13 +123,16 @@ type HardenedDeleteState = {
timerProcessToken?: string;
};

function emptyMcpDestroyPreparation(): McpDestroyPreparation {
function emptyMcpDestroyPreparation(
runtimeSelection?: McpDestroyPreparation["runtimeSelection"],
): McpDestroyPreparation {
return {
entries: [],
detachedProviderEntries: [],
scrubbedAdapterEntries: [],
destroyAlreadyPrepared: false,
destroyAlreadyPending: false,
...(runtimeSelection ? { runtimeSelection } : {}),
};
}

Expand All @@ -134,13 +141,20 @@ async function prepareMcpDestroy(
sandbox: SandboxEntry | null,
sandboxConfirmedAbsent: boolean,
force: boolean,
runtimeSelection?: McpDestroyPreparation["runtimeSelection"],
): Promise<McpDestroyPreparation> {
if (Object.keys(sandbox?.mcp?.bridges ?? {}).length === 0) {
return emptyMcpDestroyPreparation();
return emptyMcpDestroyPreparation(runtimeSelection);
}
const preparation = sandboxConfirmedAbsent
? await prepareMcpBridgesForAbsentSandboxDestroy(sandboxName, { force })
: await prepareMcpBridgesForDestroy(sandboxName, { force });
? await prepareMcpBridgesForAbsentSandboxDestroy(sandboxName, {
force,
...(runtimeSelection ? { runtimeSelection } : {}),
})
: await prepareMcpBridgesForDestroy(sandboxName, {
force,
...(runtimeSelection ? { runtimeSelection } : {}),
});
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 All @@ -154,12 +168,17 @@ function wipeAndHardenLiveSandbox(
sandboxRuntimeConfirmedAbsent: boolean,
cliName: string,
deps: NonNullable<SandboxDestroyExecutionInput["deps"]> = {},
selectedRunOpenshell?: DestroyRunOpenshell,
runtimeSelection?: OpenShellRuntimeSelection,
): HardenedDeleteState {
if (sandboxRuntimeConfirmedAbsent) return { hardenedForDelete: false, hardeningFailed: false };

// Wipe before delete while the retained volume is still mounted. The caller
// holds the timer-bound lock across this phase and all following teardown.
(deps.wipeSandboxState ?? wipeSandboxState)(sandboxName);
(deps.wipeSandboxState ?? wipeSandboxState)(
sandboxName,
selectedRunOpenshell ? { runOpenshell: selectedRunOpenshell } : {},
);
const timerMarker = (deps.readTimerMarker ?? readTimerMarker)(sandboxName);
if (!timerMarker) return { hardenedForDelete: false, hardeningFailed: false };

Expand All @@ -171,6 +190,7 @@ function wipeAndHardenLiveSandbox(
shieldsUp(sandboxName, {
throwOnError: true,
allowLegacyHermesProtocol: true,
...(runtimeSelection ? { runtimeSelection } : {}),
});
} catch (error) {
/**
Expand Down Expand Up @@ -251,6 +271,9 @@ async function restoreMcpAfterDeleteAbort(
allowLegacyHermesProtocol: true,
deferAutoRestoreWhileOwnerAlive: true,
processToken: hardened.timerProcessToken,
...(preparation.runtimeSelection
? { runtimeSelection: preparation.runtimeSelection }
: {}),
});
openedRollbackWindow = true;
}
Expand All @@ -264,6 +287,9 @@ async function restoreMcpAfterDeleteAbort(
shieldsUp(sandboxName, {
throwOnError: true,
allowLegacyHermesProtocol: true,
...(preparation.runtimeSelection
? { runtimeSelection: preparation.runtimeSelection }
: {}),
});
} catch (error) {
const detail = redactDestroyError(error);
Expand Down Expand Up @@ -302,6 +328,7 @@ export async function executeSandboxDestroy({
getSandbox,
listSandboxes,
runOpenshell,
mcpRuntimeSelection,
sandbox,
sandboxConfirmedAbsent,
sandboxName,
Expand All @@ -313,6 +340,7 @@ export async function executeSandboxDestroy({
deps = {},
}: SandboxDestroyExecutionInput): Promise<SandboxDestroyExecutionResult> {
return withTimerBoundShieldsMutationLockAsync(sandboxName, "destroy sandbox", async () => {
let destroyRuntimeSelection = mcpRuntimeSelection;
type IdentityContinuity =
| { status: "match" }
| { status: "changed"; subject?: string }
Expand Down Expand Up @@ -359,6 +387,7 @@ export async function executeSandboxDestroy({
const liveFingerprint = inspectIdentity({
sandboxName,
gatewayName: pendingCreateIdentity.gatewayName,
...(destroyRuntimeSelection ? { runtimeSelection: destroyRuntimeSelection } : {}),
});
if (
liveFingerprint !== pendingCreateIdentity.sandboxIdentityFingerprint ||
Expand Down Expand Up @@ -475,7 +504,13 @@ export async function executeSandboxDestroy({
}
let mcpPreparation: McpDestroyPreparation;
try {
mcpPreparation = await prepareMcpDestroy(sandboxName, sandbox, sandboxConfirmedAbsent, force);
mcpPreparation = await prepareMcpDestroy(
sandboxName,
sandbox,
sandboxConfirmedAbsent,
force,
mcpRuntimeSelection,
);
} catch (error) {
if (error instanceof McpBridgeError) {
return {
Expand All @@ -490,6 +525,29 @@ export async function executeSandboxDestroy({
}
throw error;
}
if (
mcpRuntimeSelection &&
!isDeepStrictEqual(mcpPreparation.runtimeSelection, mcpRuntimeSelection)
) {
return {
ok: false as const,
deleteOutput: "MCP destroy target changed after preflight.",
exitCode: 1,
gatewayUnreachable: false,
hostLocalInferenceOwnershipRequiresGateway: false,
mcpOwnershipRequiresGateway: false,
shieldsRelockRequiresGateway: false,
};
}
destroyRuntimeSelection = mcpPreparation.runtimeSelection;
const selectedRunOpenshell: DestroyRunOpenshell = destroyRuntimeSelection
? (args, options = {}) =>
runOpenshell(args, {
...options,
env: buildSelectedOpenShellSubprocessEnv(destroyRuntimeSelection!),
replaceEnv: true,
})
: runOpenshell;
// Prepared-only/incomplete adds have no external resources and are safely
// discarded during preparation. Remaining entries are the durable exact
// provider ownership manifest and must survive an unconfirmed delete.
Expand Down Expand Up @@ -557,6 +615,8 @@ export async function executeSandboxDestroy({
sandboxRuntimeConfirmedAbsent,
cliName,
deps,
selectedRunOpenshell,
destroyRuntimeSelection,
);
} catch (error) {
const mcpRecoveryFailure = await restoreMcpForAbort(notHardened);
Expand All @@ -581,7 +641,10 @@ export async function executeSandboxDestroy({
};
}
const detachProviders = (): DetachSandboxProvidersResult =>
runSandboxProviderPreDeleteCleanup(sandboxName, { runOpenshell, redact });
runSandboxProviderPreDeleteCleanup(sandboxName, {
runOpenshell: selectedRunOpenshell,
redact,
});
const preProviderContinuity = inspectIdentityContinuity();
if (preProviderContinuity.status !== "match") {
const mcpRecoveryFailure = await restoreMcpForAbort(hardened);
Expand Down Expand Up @@ -614,15 +677,34 @@ export async function executeSandboxDestroy({
` Managed inference cleanup and workspace wipe or hardening may already have run; inspect those resources before retrying.${detachedDetail}`,
);
}
const deleteArgs = pendingCreateIdentity
? ["sandbox", "delete", "-g", pendingCreateIdentity.gatewayName, sandboxName]
const deleteRuntimeSelection = destroyRuntimeSelection;
if (
pendingCreateIdentity &&
deleteRuntimeSelection &&
pendingCreateIdentity.gatewayName !== deleteRuntimeSelection.gatewayName
) {
const mcpRecoveryFailure = await restoreMcpForAbort(hardened);
return {
ok: false as const,
deleteOutput: "Sandbox delete target changed during destroy preparation.",
exitCode: 1,
gatewayUnreachable: false,
hostLocalInferenceOwnershipRequiresGateway: false,
mcpOwnershipRequiresGateway: false,
mcpRecoveryFailure,
shieldsRelockRequiresGateway: hardened.hardeningFailed,
};
}
const deleteGatewayName = pendingCreateIdentity?.gatewayName ?? deleteRuntimeSelection?.gatewayName;
const deleteArgs = deleteGatewayName
? ["sandbox", "delete", "-g", deleteGatewayName, sandboxName]
: ["sandbox", "delete", sandboxName];
// A successful preflight absence is already the required OpenShell
// lifecycle proof. Do not issue a later mutable-name delete that could
// target a same-name replacement created after that observation.
const deleteResult: ReturnType<DestroyRunOpenshell> = sandboxConfirmedAbsent
? { status: 0, stdout: "", stderr: "" }
: runOpenshell(deleteArgs, {
: selectedRunOpenshell(deleteArgs, {
ignoreError: true,
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "pipe"],
Expand Down Expand Up @@ -770,6 +852,7 @@ export async function executeSandboxDestroy({
deleteResult,
alreadyGone,
forcedLocalCleanup,
...(destroyRuntimeSelection ? { runtimeSelection: destroyRuntimeSelection } : {}),
...(commonLlamaCppAuthorityRetired ? { commonLlamaCppAuthorityRetired: true as const } : {}),
};
});
Expand Down
Loading
Loading