Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
38329e8
feat(onboard): add managed startup profile schema
ericksoa Jul 30, 2026
7f3859a
docs(onboard): clarify startup profile transport encoding
ericksoa Jul 30, 2026
32561a9
fix(onboard): harden managed startup profile
ericksoa Jul 30, 2026
087a9a5
chore(stack): sync PR3.2 with current main
ericksoa Jul 30, 2026
e1845dd
fix(onboard): close startup profile contract gaps
ericksoa Jul 30, 2026
244e3e6
chore(stack): sync PR3.2 with current main
ericksoa Jul 30, 2026
2403316
chore(stack): sync PR3.2 with current main
ericksoa Jul 30, 2026
ea5bd8a
chore(stack): sync PR3.2 with current main
ericksoa Jul 31, 2026
357081a
feat(onboard): construct managed startup profiles
ericksoa Jul 31, 2026
25d32e0
feat(onboard): map and coordinate startup profiles
ericksoa Jul 31, 2026
094d177
feat(onboard): apply startup profiles in managed images
ericksoa Jul 31, 2026
06f67e5
feat(onboard): add managed shared-state transactions
ericksoa Jul 31, 2026
2a048ae
chore(stack): restack PR3.3 on merged PR3.2
ericksoa Jul 31, 2026
498e521
chore(stack): restack PR3.4a on PR3.3
ericksoa Jul 31, 2026
3206926
chore(stack): restack PR3.4b on PR3.4a
ericksoa Jul 31, 2026
f3081cc
chore(stack): restack PR3.5 on PR3.4b
ericksoa Jul 31, 2026
6da5eb2
test(onboard): keep managed startup tests branchless
ericksoa Jul 31, 2026
51126c4
chore(stack): carry PR3.4b guardrail fix into PR3.5
ericksoa Jul 31, 2026
bb7a260
chore(stack): restack PR3.4a after PR3.3 merge
ericksoa Jul 31, 2026
fafdbd1
chore(stack): restack PR3.4b after PR3.3 merge
ericksoa Jul 31, 2026
0edd158
chore(stack): restack PR3.5 after PR3.3 merge
ericksoa Jul 31, 2026
b14013f
test(onboard): cover managed startup fixture CLI
ericksoa Jul 31, 2026
46fbe19
feat(onboard): apply startup profiles in managed images
ericksoa Jul 31, 2026
b2027a9
feat(onboard): add managed shared-state transactions
ericksoa Jul 31, 2026
c95bdf7
fix(onboard): serialize managed startup transactions
ericksoa Jul 31, 2026
3f3bdbb
feat(onboard): apply startup profiles in managed images
ericksoa Jul 31, 2026
8ff8094
feat(onboard): add managed shared-state transactions
ericksoa Jul 31, 2026
7d1668b
test(onboard): keep transaction races branchless
ericksoa Jul 31, 2026
540494c
chore(stack): inherit PR3.4a guardrail fix
ericksoa Jul 31, 2026
1463687
chore(stack): inherit PR3.4b restack
ericksoa Jul 31, 2026
3a03eeb
merge: resolve conflicts with main
github-actions[bot] Jul 31, 2026
d97b259
chore(stack): restack PR3.5 after PR3.4a merge
ericksoa Jul 31, 2026
deb1581
chore(stack): refresh PR3.4b validation
ericksoa Jul 31, 2026
0ad91a5
chore(stack): inherit PR3.4b validation refresh
ericksoa Jul 31, 2026
a0f12f1
fix(onboard): harden managed startup inputs
ericksoa Jul 31, 2026
27c72f5
fix(runtime): close prior review debt
ericksoa Jul 31, 2026
1c824cc
test(runtime): keep env cleanup branchless
ericksoa Jul 31, 2026
5c6b3f7
fix(runtime): preserve direct image execution
ericksoa Jul 31, 2026
611fdb6
fix(messaging): reject ambiguous channel identities
ericksoa Jul 31, 2026
76c54d4
fix(messaging): require canonical persisted channel ids
ericksoa Jul 31, 2026
e41521b
chore(stack): refresh PR3.4b on current main
ericksoa Jul 31, 2026
bebfc6c
chore(stack): refresh PR3.5 on current PR3.4b
ericksoa Jul 31, 2026
864e249
chore(stack): refresh PR3.5a on current PR3.5
ericksoa Jul 31, 2026
7cf2a8d
chore(stack): refresh PR3.5b on current PR3.5a
ericksoa Jul 31, 2026
5f9d97d
test(e2e): add cross-runtime foundation
ericksoa Jul 31, 2026
abf45b3
test(e2e): keep runtime foundation branchless
ericksoa Jul 31, 2026
ed479ae
refactor(runtime): unify provider bundle registry
ericksoa Jul 31, 2026
005b3a7
fix(runtime): harden provider contract boundaries
ericksoa Jul 31, 2026
05cf2a5
fix(runtime): fail closed on managed image cleanup
ericksoa Jul 31, 2026
5c29046
fix(messaging): reject unresolved build hooks
ericksoa Jul 31, 2026
534773b
fix(runtime): close provider lifecycle authority gaps
ericksoa Jul 31, 2026
847e827
chore(stack): refresh PR3.5c on current PR3.5b
ericksoa Jul 31, 2026
77559f7
test(runtime): keep provider parity branchless
ericksoa Jul 31, 2026
a2afe77
fix(e2e): harden runtime foundation contracts
ericksoa Jul 31, 2026
13932b9
chore(stack): refresh PR3.4b on current main
ericksoa Jul 31, 2026
5111410
chore(stack): refresh PR3.5 on current PR3.4b
ericksoa Jul 31, 2026
7d51947
chore(stack): refresh PR3.5a on current PR3.5
ericksoa Jul 31, 2026
004b060
test(e2e): verify fixture profiles stay unregistered
ericksoa Jul 31, 2026
771f48c
chore(stack): refresh PR3.5b on current PR3.5a
ericksoa Jul 31, 2026
c67afd3
chore(stack): refresh PR3.5c on current PR3.5b
ericksoa Jul 31, 2026
4788d28
test(e2e): expose runtime profile registration state
ericksoa Jul 31, 2026
64f562f
fix(runtime): preserve unproven cleanup authority
ericksoa Jul 31, 2026
15eef1b
chore(stack): refresh PR3.6 on repaired base
ericksoa Jul 31, 2026
d73f13a
test(runtime): cover unknown provider doctor result
ericksoa Jul 31, 2026
8f8bcd5
test(runtime): prove provider recovery boundaries
ericksoa Jul 31, 2026
f77bff8
fix(runtime): fail closed on retained cleanup ownership
ericksoa Jul 31, 2026
bf2fd19
test(rebuild): type cleanup ownership receipt
ericksoa Jul 31, 2026
4316a18
fix(runtime): prove cleanup authority before deletion
ericksoa Jul 31, 2026
062d66b
test(snapshot): mock destructive cleanup authority
ericksoa Jul 31, 2026
75730cf
test(runtime): prove cleanup planning is inert
ericksoa Jul 31, 2026
0b21b79
feat(rebuild): add atomic managed workload replacement
ericksoa Jul 31, 2026
438f79f
fix(rebuild): fail closed across authority cutover
ericksoa Jul 31, 2026
412d69d
docs(rebuild): define deferred recovery ownership
ericksoa Jul 31, 2026
cfe84cd
fix(rebuild): bind retained workload authority
ericksoa Jul 31, 2026
827f545
test(rebuild): keep failure matrices linear
ericksoa Jul 31, 2026
add3965
test(rebuild): cover invalid provider artifacts
ericksoa Jul 31, 2026
9bbf75d
fix(rebuild): address review feedback
ericksoa Jul 31, 2026
8212bf9
merge: resolve conflicts with main
github-actions[bot] Aug 1, 2026
4960f4d
chore(stack): refresh managed rebuild on current main
ericksoa Aug 1, 2026
bf94633
feat(snapshot): preserve managed runtime authority
ericksoa Jul 31, 2026
3a4321d
fix(snapshot): enforce managed authority boundaries
ericksoa Jul 31, 2026
1a91ef2
test(snapshot): keep provider authority cases linear
ericksoa Aug 1, 2026
844bfb6
fix(snapshot): pin files during authority hashing
ericksoa Aug 1, 2026
ed60882
fix(snapshot): address restore review findings
ericksoa Aug 1, 2026
9e64898
docs(snapshot): clarify legacy restore retention
ericksoa Aug 1, 2026
e97ecce
merge(stack): refresh managed workload rebuild parity
ericksoa Aug 1, 2026
6b2c464
merge(stack): refresh managed snapshot parity
ericksoa Aug 1, 2026
362a70c
chore(stack): refresh managed workload rebuild parity on current main
ericksoa Aug 1, 2026
b2374ee
chore(stack): refresh managed snapshot parity parent
ericksoa Aug 1, 2026
b349b98
merge: resolve conflicts with main
github-actions[bot] Aug 1, 2026
33d1756
merge(stack): restack #8031 onto current main
ericksoa Aug 2, 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
6 changes: 3 additions & 3 deletions ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"src/lib/adapters/docker/index.ts": 43,
"src/lib/adapters/openshell/client.ts": 23,
"src/lib/adapters/openshell/resolve.ts": 27,
"src/lib/adapters/openshell/runtime.ts": 51,
"src/lib/adapters/openshell/runtime.ts": 52,
"src/lib/adapters/openshell/timeouts.ts": 37,
"src/lib/agent/defs.ts": 32,
"src/lib/cli/branding.ts": 84,
Expand All @@ -23,7 +23,7 @@
"src/lib/inference/config.ts": 29,
"src/lib/inference/web-search.ts": 21,
"src/lib/messaging/channels/index.ts": 26,
"src/lib/onboard/gateway-binding.ts": 47,
"src/lib/onboard/gateway-binding.ts": 48,
"src/lib/runner.ts": 89,
"src/lib/security/redact.ts": 51,
"src/lib/state/onboard-session.ts": 36,
Expand All @@ -43,7 +43,7 @@
"src/lib/actions/sandbox/policy-channel.ts": 29,
"src/lib/actions/sandbox/process-recovery.ts": 22,
"src/lib/actions/sandbox/rebuild-pipeline.ts": 28,
"src/lib/actions/sandbox/snapshot.ts": 38,
"src/lib/actions/sandbox/snapshot.ts": 39,
"src/lib/actions/uninstall/run-plan.ts": 25,
"src/lib/inference/onboard-probes.ts": 21,
"src/lib/inference/vllm.ts": 23,
Expand Down
47 changes: 47 additions & 0 deletions src/lib/actions/maintenance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
listSandboxes: vi.fn(),
getSandbox: vi.fn(),
backupSandboxState: vi.fn(),
captureSandboxListWithGatewayPreflightOrExit: vi.fn(),
parseReadySandboxNames: vi.fn(),
Expand All @@ -18,17 +19,25 @@ const mocks = vi.hoisted(() => ({
isSandboxContainerDefinitivelyAbsent: vi.fn(),
openBackupShieldsWindow: vi.fn(),
relockBackupShieldsWindow: vi.fn(),
withSandboxMutationLock: vi.fn(),
}));

vi.mock("../state/registry", () => ({
isRouteOnlySandboxReservation: (entry: { pendingRouteReservation?: true; createdAt?: string }) =>
entry.pendingRouteReservation === true && entry.createdAt === undefined,
listSandboxes: mocks.listSandboxes,
getSandbox: mocks.getSandbox,
}));
vi.mock("../state/sandbox", () => ({
backupSandboxState: mocks.backupSandboxState,
BackupResult: {},
}));
vi.mock("../state/mcp-lifecycle-lock", () => ({
withSandboxMutationLock: mocks.withSandboxMutationLock,
}));
vi.mock("./sandbox/snapshot/backup-authority", () => ({
backupSandboxStateWithManagedAuthority: (name: string) => mocks.backupSandboxState(name),
}));
vi.mock("../openshell-sandbox-list", () => ({
captureSandboxListWithGatewayPreflightOrExit: mocks.captureSandboxListWithGatewayPreflightOrExit,
}));
Expand Down Expand Up @@ -100,6 +109,7 @@ describe("backupAll", () => {
wasLocked: false,
}));
mocks.relockBackupShieldsWindow.mockReturnValue(true);
mocks.withSandboxMutationLock.mockImplementation((_name, callback) => callback());
});

afterEach(() => {
Expand Down Expand Up @@ -217,6 +227,43 @@ describe("backupAll", () => {
logSpy.mockRestore();
});

it("counts a mutation-lock acquisition failure and continues with later sandboxes", async () => {
mocks.listSandboxes.mockReturnValue({
sandboxes: [{ name: "alpha" }, { name: "beta" }],
defaultSandbox: "alpha",
});
mocks.parseReadySandboxNames.mockReturnValue(new Set(["alpha", "beta"]));
mocks.withSandboxMutationLock
.mockRejectedValueOnce(new Error("Timed out waiting for the sandbox mutation lock"))
.mockImplementationOnce((_name, callback) => callback());
mocks.backupSandboxState.mockReturnValue({
success: true,
backedUpDirs: ["workspace"],
failedDirs: [],
backedUpFiles: [],
failedFiles: [],
manifest: { backupPath: "/backups/beta/timestamp" },
});
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`exit:${code}`);
}) as never);

await expect(backupAll()).rejects.toThrow("exit:1");

expect(mocks.withSandboxMutationLock.mock.calls.map(([name]) => name)).toEqual([
"alpha",
"beta",
]);
expect(mocks.backupSandboxState).toHaveBeenCalledOnce();
expect(mocks.backupSandboxState).toHaveBeenCalledWith("beta");
expect(logSpy.mock.calls.flat().join("\n")).toContain("1 backed up, 1 failed, 0 skipped");
expect(errorSpy.mock.calls.flat().join("\n")).toContain(
"alpha: backup failed (mutation lock: Timed out waiting for the sandbox mutation lock)",
);
});

it("does not back up when gateway preflight exits", async () => {
mocks.listSandboxes.mockReturnValue({
sandboxes: [{ name: "sb-good" }],
Expand Down
42 changes: 34 additions & 8 deletions src/lib/actions/maintenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SANDBOX_IMAGE_REPOS } from "../domain/sandbox/image-tag";
import { resolveGatewayName, resolveSandboxGatewayName } from "../onboard/gateway-binding";
import { captureSandboxListWithGatewayPreflightOrExit } from "../openshell-sandbox-list";
import { parseLiveSandboxNames, parseReadySandboxNames } from "../runtime-recovery";
import { withSandboxMutationLock } from "../state/mcp-lifecycle-lock";
import * as registry from "../state/registry";
import * as sandboxState from "../state/sandbox";
import { nemoclawStateRoot, resolveHome } from "../state/state-root";
Expand All @@ -30,6 +31,7 @@ import {
openBackupShieldsWindow,
relockBackupShieldsWindow,
} from "./sandbox/backup-shields-window";
import * as snapshotBackup from "./sandbox/snapshot/backup-authority";
import {
backupStartedSandboxState,
isSandboxContainerDefinitivelyAbsent,
Expand Down Expand Up @@ -198,7 +200,7 @@ export async function backupAll(): Promise<void> {
let unreachableRunning = 0;
let notRunningSkipped = 0;
const strandedOrphans: string[] = [];
for (const sb of sandboxes) {
const backupRegisteredSandbox = async (sb: (typeof sandboxes)[number]): Promise<void> => {
// A registered docker-driver sandbox whose container is merely stopped is
// backupable: start it for the duration of the backup and return it to
// its stopped state after (#6500). Anything else that is not Ready keeps
Expand All @@ -211,12 +213,12 @@ export async function backupAll(): Promise<void> {
// Tracked separately from `skipped` so the strict gate stays
// untripped: there is nothing to back up and nothing to start.
strandedOrphans.push(sb.name);
continue;
return;
}
console.log(` ${D}${notRunningBackupSkipMessage(sb.name)}${R}`);
skipped++;
notRunningSkipped++;
continue;
return;
}
console.log(` Starting stopped sandbox '${sb.name}' to back it up...`);
}
Expand All @@ -229,7 +231,13 @@ export async function backupAll(): Promise<void> {
const attempt = await backupSandboxWithinShieldsWindow(sb.name, () =>
startedForBackup
? backupStartedSandboxState(sb.name)
: sandboxState.backupSandboxState(sb.name),
: snapshotBackup.backupSandboxStateWithManagedAuthority(
sb.name,
{},
{
getSandbox: registry.getSandbox,
},
),
);
result = attempt.result;
orphanManifestMessage = attempt.orphanManifestMessage;
Expand All @@ -248,17 +256,17 @@ export async function backupAll(): Promise<void> {
}
if (!returnedToStopped) {
failed++;
continue;
return;
}
if (!shieldsWindowOpened) {
console.error(` ${RD}✗${R} ${sb.name}: backup failed (could not safely unlock shields)`);
failed++;
continue;
return;
}
if (orphanManifestMessage) {
console.log(` ${YW}⚠${R} Skipped '${sb.name}' (orphan manifest): ${orphanManifestMessage}`);
skipped++;
continue;
return;
}
if (!result) throw new Error(`Backup for '${sb.name}' completed without a result`);
if (result.success) {
Expand All @@ -273,7 +281,7 @@ export async function backupAll(): Promise<void> {
` ${YW}⚠${R} Skipped '${sb.name}' (running but SSH-unreachable; NEMOCLAW_SKIP_UNREACHABLE_SANDBOX_BACKUP=1 set). Any uncommitted state since the last successful backup will be lost.`,
);
skipped++;
continue;
return;
}
unreachableRunning++;
}
Expand All @@ -284,6 +292,24 @@ export async function backupAll(): Promise<void> {
console.error(` ${RD}✗${R} ${sb.name}: backup failed (${failedItems})`);
failed++;
}
};
for (const sb of sandboxes) {
let enteredMutationLock = false;
try {
await withSandboxMutationLock(sb.name, () => {
enteredMutationLock = true;
return backupRegisteredSandbox(sb);
});
} catch (error) {
// Callback failures retain the existing fail-fast behavior. A lock that
// could not be acquired is instead one failed sandbox attempt so the
// remaining backups, orphan confirmation, summary, and strict gate all
// still run.
if (enteredMutationLock) throw error;
const detail = error instanceof Error ? error.message : String(error);
console.error(` ${RD}✗${R} ${sb.name}: backup failed (mutation lock: ${detail})`);
failed++;
}
}
// The classification above is only as fresh as the pre-loop listing, and
// the backup loop can run for minutes. Confirm with a second pinned listing
Expand Down
7 changes: 5 additions & 2 deletions src/lib/actions/sandbox/rebuild-flow-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as gatewayRuntime from "../../gateway-runtime-action";
import type { SandboxBaseImageResolutionMetadata } from "../../sandbox-base-image";
import * as sandboxState from "../../state/sandbox";
import * as userManagedFilesProbe from "../../state/user-managed-files-probe";
import * as snapshotBackup from "./snapshot/backup-authority";
import {
backupSandboxStateForRebuild,
disposeRebuildAgentBaseImagePreflight,
Expand Down Expand Up @@ -549,7 +550,7 @@ describe("backupSandboxStateForRebuild with --force", () => {
errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
vi.spyOn(console, "log").mockImplementation(() => undefined);

backupSpy = vi.spyOn(sandboxState, "backupSandboxState");
backupSpy = vi.spyOn(snapshotBackup, "backupSandboxStateWithManagedAuthority");
});

afterEach(() => {
Expand Down Expand Up @@ -843,7 +844,9 @@ describe("warnUnpreservedUserManagedFiles", () => {
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);

backupSpy = vi.spyOn(sandboxState, "backupSandboxState").mockReturnValue(makeBackupResult());
backupSpy = vi
.spyOn(snapshotBackup, "backupSandboxStateWithManagedAuthority")
.mockReturnValue(makeBackupResult());
probeSpy = vi.spyOn(userManagedFilesProbe, "probeUserManagedFiles").mockReturnValue({
declared: [],
existing: [],
Expand Down
9 changes: 8 additions & 1 deletion src/lib/actions/sandbox/rebuild-flow-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
printWrongGatewayActiveGuidance,
} from "./gateway-state";
import { openRebuildShieldsWindow, type RebuildShieldsWindow } from "./rebuild-shields";
import * as snapshotBackup from "./snapshot/backup-authority";

export type RebuildSandboxEntry = SandboxEntry & { agents?: unknown[] };

Expand Down Expand Up @@ -446,7 +447,13 @@ export function backupSandboxStateForRebuild(

console.log(" Backing up sandbox state...");
log(`Agent type: ${sb.agent || "openclaw"}, stateDirs from manifest`);
const backup = sandboxState.backupSandboxState(sandboxName);
const backup = snapshotBackup.backupSandboxStateWithManagedAuthority(
sandboxName,
{},
{
getSandbox: (name) => loadRegistry().sandboxes[name] ?? null,
},
);
log(
`Backup result: success=${backup.success}, backed=${backup.backedUpDirs.join(",")}; files=${backup.backedUpFiles.join(",")}, failed=${backup.failedDirs.join(",")}; failedFiles=${backup.failedFiles.join(",")}`,
);
Expand Down
17 changes: 11 additions & 6 deletions src/lib/actions/sandbox/rebuild-restore-forwarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import { afterEach, describe, expect, it, vi } from "vitest";

import * as sandboxState from "../../state/sandbox";
import { runRebuildRestorePhase } from "./rebuild-restore-phase";
import * as snapshotRestore from "./snapshot/restore-authority";

afterEach(() => {
vi.restoreAllMocks();
Expand All @@ -14,7 +14,7 @@ describe("rebuild restore target forwarding", () => {
it("forwards the recreated target identity and explicit custom-image capability", () => {
vi.spyOn(console, "log").mockImplementation(() => undefined);
const restoreRecreatedSandboxState = vi
.spyOn(sandboxState, "restoreRecreatedSandboxState")
.spyOn(snapshotRestore, "restoreRecreatedSandboxStateWithManagedAuthority")
.mockReturnValue({
success: true,
restoredDirs: [],
Expand All @@ -34,9 +34,14 @@ describe("rebuild restore target forwarding", () => {
log: vi.fn(),
});

expect(restoreRecreatedSandboxState).toHaveBeenCalledWith("alpha", "/tmp/rebuild-backup", {
targetAgentType: "langchain-deepagents-code",
allowCustomImageWholeStateFileRestore: true,
});
expect(restoreRecreatedSandboxState).toHaveBeenCalledWith(
"alpha",
expect.objectContaining({ backupPath: "/tmp/rebuild-backup" }),
{
targetAgentType: "langchain-deepagents-code",
allowCustomImageWholeStateFileRestore: true,
},
{ getSandbox: expect.any(Function) },
);
});
});
16 changes: 11 additions & 5 deletions src/lib/actions/sandbox/rebuild-restore-phase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
resolveRestoredPolicyRegistryState,
} from "./rebuild-post-restore-phase";
import { runRebuildRestorePhase } from "./rebuild-restore-phase";
import * as snapshotRestore from "./snapshot/restore-authority";

const BUILTIN_OBSERVABILITY_CONTENT =
"network_policies:\n observability-otlp-local:\n name: observability-otlp-local\n";
Expand Down Expand Up @@ -44,7 +45,7 @@ describe("rebuild policy restore fidelity", () => {
vi.spyOn(console, "log").mockImplementation(() => undefined);
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
const log = vi.fn();
vi.spyOn(sandboxState, "restoreRecreatedSandboxState").mockReturnValue({
vi.spyOn(snapshotRestore, "restoreRecreatedSandboxStateWithManagedAuthority").mockReturnValue({
success: false,
restoredDirs: [],
restoredFiles: [],
Expand Down Expand Up @@ -76,7 +77,7 @@ describe("rebuild policy restore fidelity", () => {
vi.spyOn(console, "error").mockImplementation(() => undefined);
const parsePresetPolicyKeys = vi.spyOn(policies, "parsePresetPolicyKeys");
const restoreRecreatedSandboxState = vi
.spyOn(sandboxState, "restoreRecreatedSandboxState")
.spyOn(snapshotRestore, "restoreRecreatedSandboxStateWithManagedAuthority")
.mockReturnValue({
success: true,
restoredDirs: [],
Expand Down Expand Up @@ -104,9 +105,14 @@ describe("rebuild policy restore fidelity", () => {
log: vi.fn(),
});

expect(restoreRecreatedSandboxState).toHaveBeenCalledWith("alpha", "/tmp/rebuild-backup", {
targetAgentType: "openclaw",
});
expect(restoreRecreatedSandboxState).toHaveBeenCalledWith(
"alpha",
expect.objectContaining({ backupPath: "/tmp/rebuild-backup" }),
{
targetAgentType: "openclaw",
},
{ getSandbox: expect.any(Function) },
);
expect(applyPreset).toHaveBeenCalledOnce();
expect(applyPreset).toHaveBeenCalledWith("alpha", "npm");
for (const entry of customPolicies) {
Expand Down
9 changes: 7 additions & 2 deletions src/lib/actions/sandbox/rebuild-restore-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
OBSERVABILITY_POLICY_BINDING,
} from "../../onboard/observability-policy-presets";
import * as policies from "../../policy";
import { load as loadRegistry } from "../../state/registry/persistence";
import * as sandboxState from "../../state/sandbox";
import { MCP_BRIDGE_POLICY_SOURCE } from "./mcp-bridge-contracts";
import type { RebuildBackupManifest } from "./rebuild-backup-phase";
import type { RebuildLog } from "./rebuild-credential-preflight";
import type { RebuildSandboxEntry } from "./rebuild-flow-helpers";
import * as snapshotRestore from "./snapshot/restore-authority";

export interface RebuildRestorePhaseInput {
sandboxName: string;
Expand Down Expand Up @@ -191,13 +193,16 @@ export function runRebuildRestorePhase(input: RebuildRestorePhaseInput): Rebuild
console.log("");
console.log(" Restoring workspace state...");
log(`Restoring from: ${backupManifest.backupPath} into sandbox: ${sandboxName}`);
const restore = sandboxState.restoreRecreatedSandboxState(
const restore = snapshotRestore.restoreRecreatedSandboxStateWithManagedAuthority(
sandboxName,
backupManifest.backupPath,
backupManifest,
{
targetAgentType,
...(targetImageIsCustom ? { allowCustomImageWholeStateFileRestore: true } : {}),
},
{
getSandbox: (name) => loadRegistry().sandboxes[name] ?? null,
},
);
log(
`Restore result: success=${restore.success}, restored=${restore.restoredDirs.join(",")}; files=${restore.restoredFiles.join(",")}, failed=${restore.failedDirs.join(",")}; failedFiles=${restore.failedFiles.join(",")}${restore.error ? `; error=${restore.error}` : ""}`,
Expand Down
Loading
Loading