Skip to content

Commit b9b8d2a

Browse files
laitingshengclaudecjagwanicv
authored
feat(state): add key-level restore ownership to agent manifests (#6445)
<!-- markdownlint-disable MD041 --> ## Summary Agent manifests can now declare key-level restore ownership for mixed-ownership state files. Managed restore preserves only manifest-owned user preferences while the current target manifest remains authoritative for generated configuration. Deep Agents Code is migrated to this schema, and the restore path now fails closed on unsafe manifests, paths, TOML, filesystem races, and target/backup mismatches. ## Related Issue Resolves #6334 ## Changes - Add a strict, discriminated state_files restore schema for key-allowlist and openclaw-config strategies, including canonical relative-path validation and rejection of duplicate or ancestor-overlapping ownership declarations. - Require the current target manifest to authorize the backup agent, config directory, state-file path, strategy, and restore ownership before SSH or filesystem mutation. - Make managed key-level merge the default. Whole-file restore is available only through the explicit allowCustomImageWholeStateFileRestore capability for custom-image restore. - Read bounded backup TOML directly from Python stdin, parse with tomllib, create a private unpredictable O_EXCL stage, traverse parent directories without following symlinks, and revalidate current/stage metadata before atomic replacement. - Preserve all safe leading comments, preserve non-dictionary fresh ancestors instead of clobbering them, and keep fresh managed tables and headers authoritative. - Migrate the Deep Agents Code manifest and remove the agent-specific restore-policy callback. - Document managed key-level restore, target-manifest authority, and the custom-Dockerfile whole-file exception. ## Type of Change - [x] Code change (feature, bug fix, or refactor) - [x] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Quality Gates - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [x] Docs updated for user-facing behavior changes - [ ] Docs not applicable — justification: - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: maintainer-assisted review of the behavior at dd65850 passed all nine security categories; current head f9e4537 adds only a formatting-only blank-line deletion to satisfy the onboard.ts net-growth guardrail. - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: ## Verification - [x] PR description includes the DCO sign-off declaration and every commit appears as Verified in GitHub - [x] Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run check:diff passed when hooks were skipped or unavailable - [x] Targeted behavior tests pass for the current change set, or tests are marked not applicable above — 175 focused CLI assertions plus 71 affected integration assertions passed; the final phase/contract rerun passed 23/23. npm run typecheck:cli, npm run build:cli, npm run test-size:check, and git diff --check also passed. - [ ] Applicable broad gate passed — not applicable locally; targeted CLI/integration coverage exercised every changed restore path, and repository CI is running on the exact head. - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [ ] npm run docs builds without warnings (doc changes only) — build passed with 0 errors and 2 pre-existing hidden-page warnings - [x] Doc pages follow the style guide (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) — no new pages --- Signed-off-by: Tinson Lai <tinsonl@nvidia.com> Signed-off-by: Charan Jagwani <cjagwani@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added manifest-driven, fine-grained state-file restore rules with typed user key allowlists and “freshness” requirements. * Custom Dockerfile sandboxes can restore whole state files during rebuilds when enabled. * **Bug Fixes** * Snapshot restore now fails closed for unknown, unsafe, stale, or conflicting state data, with stronger atomic replacement safeguards. * **Documentation** * Updated sandbox backup/restore guidance to reflect key-level ownership semantics, allowed preference preservation, and restore failure behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Tinson Lai <tinsonl@nvidia.com> Signed-off-by: Charan Jagwani <cjagwani@nvidia.com> Signed-off-by: Carlos Villela <cvillela@nvidia.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Charan Jagwani <cjagwani@nvidia.com> Co-authored-by: Carlos Villela <cvillela@nvidia.com>
1 parent 1cbcbd3 commit b9b8d2a

47 files changed

Lines changed: 3699 additions & 1001 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agents/langchain-deepagents-code/manifest.yaml

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,11 @@ state_dirs:
4848

4949
# ── Top-level durable state files ───────────────────────────────
5050
# config.toml mixes DCode preferences with NemoClaw-managed model routing.
51-
# Managed re-onboard restore carries forward only boolean ui.show_scrollbar,
52-
# ui.show_url_open_toast, and threads.relative_time preferences, plus
53-
# threads.sort_order when it is updated_at or created_at. Fresh models/update
54-
# tables and provider metadata remain authoritative. All other backup keys,
55-
# including ui.theme and behavior-bearing, unknown, or security-sensitive keys,
56-
# are dropped.
51+
# Ownership is split into three explicit buckets. Users own only the allowlisted
52+
# ui.show_scrollbar, ui.show_url_open_toast, threads.relative_time, and
53+
# threads.sort_order preferences below. NemoClaw owns the fresh models/update
54+
# tables and generated provider headers. Agent-runtime, unknown, executable, and
55+
# security-sensitive backup keys are not restorable and are dropped.
5756
# .env and user-authored .deepagents/.mcp.json content are intentionally omitted
5857
# because they may contain service credentials. NemoClaw writes only direct-HTTP
5958
# bridge endpoint config and OpenShell placeholders to its separate
@@ -62,6 +61,29 @@ state_dirs:
6261
# not user-authored durable state.
6362
state_files:
6463
- path: config.toml
64+
restore:
65+
merge: key-allowlist
66+
require_fresh_tables:
67+
- models
68+
- update
69+
# Checked positionally: the first N leading '#' lines (N = entries below)
70+
# must match. Further leading comments are safety-checked and preserved.
71+
require_fresh_headers:
72+
- "# Generated by NemoClaw. This file contains no provider secrets."
73+
- match: prefix
74+
value: "# NemoClaw provider route: "
75+
user_keys:
76+
- key: ui.show_scrollbar
77+
type: boolean
78+
- key: ui.show_url_open_toast
79+
type: boolean
80+
- key: threads.relative_time
81+
type: boolean
82+
- key: threads.sort_order
83+
type: enum
84+
values:
85+
- updated_at
86+
- created_at
6587
user_managed_files:
6688
- .deepagents/.env
6789
- .deepagents/.mcp.json

agents/openclaw/manifest.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ state_dirs:
6868
# credentials.
6969
state_files:
7070
- path: openclaw.json
71+
restore:
72+
merge: openclaw-config
7173
user_managed_files:
7274
- .env
7375
- .mcp.json

docs/manage-sandboxes/backup-restore.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ The Hermes database can contain session metadata and message history needed for
6161
</AgentOnly>
6262
Snapshots also preserve sandbox registry metadata that affects rebuild behavior, including custom policy presets applied with `policy-add --from-file` or `policy-add --from-dir`.
6363
When you restore a snapshot, NemoClaw replays those recorded custom presets with their stored YAML content, so you do not need the original preset files on disk for the restored sandbox to keep the same policy state.
64+
The target sandbox's current agent manifest remains authoritative for state-file restore behavior.
65+
NemoClaw rejects the restore when the snapshot's agent, config directory, state-file path, or state-file strategy conflicts with that manifest.
66+
For managed images, NemoClaw applies the current manifest's managed config merge rules by default and does not fall back to whole-file replacement.
67+
For Deep Agents targets, whole-file config replacement is limited to sandboxes created from a custom Dockerfile.
6468

6569
```bash
6670
$$nemoclaw my-assistant snapshot create
@@ -126,7 +130,11 @@ NemoClaw rejects unsafe symlinks and hard links inside sandbox state during back
126130
Credential-bearing Deep Agents files such as `.deepagents/.env` and user-authored `.deepagents/.mcp.json` are intentionally excluded from snapshots.
127131
Deep Agents auth state files such as `.deepagents/.state/auth.json` and `.deepagents/.state/chatgpt-auth.json` are also excluded because the managed launcher refuses to start when upstream credential state is present.
128132
The managed `.deepagents/.nemoclaw-mcp.json` projection and `hooks.json` are also excluded because NemoClaw reconstructs managed MCP state and disables executable Deep Agents Code hooks in the managed harness.
129-
NemoClaw recreates generated `config.toml`, managed MCP projection state, and provider credentials from host-side onboarding and OpenShell provider state during rebuild.
133+
NemoClaw recreates the current inference route headers, `models` and `update` tables, managed MCP projection state, and provider credentials from host-side onboarding and OpenShell provider state during rebuild.
134+
For a NemoClaw-managed Deep Agents image, it restores only the allowlisted `ui.show_scrollbar`, `ui.show_url_open_toast`, `threads.relative_time`, and `threads.sort_order` preferences from the previous `config.toml` when their values pass validation.
135+
Unknown, runtime-controlled, executable, and security-sensitive backup keys are dropped instead of replacing freshly generated settings on that managed path.
136+
A Deep Agents target created from a custom Dockerfile restores `config.toml` as a whole file because the custom image owns its config schema, so review that backup before restoring it.
137+
On the managed key-level restore path, if either config is malformed, required managed data is missing, or the restore detects an unsafe link or file replacement, NemoClaw marks the restore as failed and does not fall back to copying the backed-up file wholesale.
130138
Before a Deep Agents rebuild changes the sandbox, NemoClaw verifies the recorded inference route, provider, model, reasoning settings, web search selection, base image, and policy inputs.
131139
If a late check fails, NemoClaw restores the previous MCP state and keeps the existing sandbox intact.
132140

docs/manage-sandboxes/workspace-files.mdx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,13 @@ The Deep Agents manifest declares these durable directories:
192192
/sandbox/.deepagents/agent/skills/
193193
```
194194

195-
It also declares `/sandbox/.deepagents/config.toml` as a durable top-level state file because the file contains non-secret NemoClaw-generated provider and model configuration.
195+
It also declares `/sandbox/.deepagents/config.toml` as a durable top-level state file with key-level ownership.
196+
The target sandbox's current Deep Agents manifest defines this ownership policy, so a snapshot cannot weaken it.
197+
NemoClaw keeps the newly generated inference route headers and the `models` and `update` tables authoritative during rebuild.
198+
On a NemoClaw-managed image, only the allowlisted `ui.show_scrollbar`, `ui.show_url_open_toast`, `threads.relative_time`, and `threads.sort_order` preferences can be restored from the previous file.
199+
Runtime-controlled, unknown, executable, and security-sensitive backup keys are dropped on that managed path.
200+
A Deep Agents target created from a custom Dockerfile restores `config.toml` as a whole file because the custom image owns its config schema.
201+
On the managed key-level restore path, if config validation or safe atomic replacement fails, NemoClaw marks the restore as failed instead of falling back to a whole-file copy.
196202
Credential-bearing files such as `.deepagents/.env` and user-authored `.deepagents/.mcp.json` are intentionally omitted from snapshots.
197203
Managed MCP state is rebuilt from the host-side NemoClaw registry and OpenShell provider state instead of treated as user-authored durable state.
198204
Memory files such as `/sandbox/.deepagents/agent/AGENTS.md` and `/sandbox/.deepagents/agent/memories/` are upstream Deep Agents Code files.
@@ -204,7 +210,7 @@ Back up important Deep Agents state before destroying the sandbox.
204210
## Editing State
205211

206212
Prefer NemoClaw host commands for generated configuration such as model, provider, managed MCP, and policy settings.
207-
Direct edits to `/sandbox/.deepagents/config.toml` can be overwritten by rebuilds because NemoClaw owns the managed inference route.
213+
Direct edits to NemoClaw-owned or non-allowlisted keys in `/sandbox/.deepagents/config.toml` can be overwritten by rebuilds.
208214
Use `$$nemoclaw <name> connect` when you need to inspect runtime files interactively, or use `openshell sandbox download` and `openshell sandbox upload` for manual file transfer.
209215

210216
Use Deep Agents Code commands for upstream-managed memories and skills.

src/lib/actions/sandbox/rebuild-pipeline.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ async function rebuildSandboxUnlocked(
299299

300300
const restored = runRebuildRestorePhase({
301301
sandboxName,
302+
targetAgentType: rebuildAgent || "openclaw",
303+
targetImageIsCustom: Boolean(fromDockerfile),
302304
backupManifest: backup.backupManifest,
303305
policyPresets: targetPolicyPresets,
304306
customPolicies:
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { afterEach, describe, expect, it, vi } from "vitest";
5+
6+
import * as sandboxState from "../../state/sandbox";
7+
import { runRebuildRestorePhase } from "./rebuild-restore-phase";
8+
9+
afterEach(() => {
10+
vi.restoreAllMocks();
11+
});
12+
13+
describe("rebuild restore target forwarding", () => {
14+
it("forwards the recreated target identity and explicit custom-image capability", () => {
15+
vi.spyOn(console, "log").mockImplementation(() => undefined);
16+
const restoreRecreatedSandboxState = vi
17+
.spyOn(sandboxState, "restoreRecreatedSandboxState")
18+
.mockReturnValue({
19+
success: true,
20+
restoredDirs: [],
21+
restoredFiles: [],
22+
failedDirs: [],
23+
failedFiles: [],
24+
});
25+
26+
runRebuildRestorePhase({
27+
sandboxName: "alpha",
28+
targetAgentType: "langchain-deepagents-code",
29+
targetImageIsCustom: true,
30+
backupManifest: { agentType: "openclaw", backupPath: "/tmp/rebuild-backup" } as never,
31+
policyPresets: [],
32+
customPolicies: [],
33+
reconcileManagedDcodeObservability: false,
34+
log: vi.fn(),
35+
});
36+
37+
expect(restoreRecreatedSandboxState).toHaveBeenCalledWith("alpha", "/tmp/rebuild-backup", {
38+
targetAgentType: "langchain-deepagents-code",
39+
allowCustomImageWholeStateFileRestore: true,
40+
});
41+
});
42+
});

src/lib/actions/sandbox/rebuild-restore-phase.test.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ import { runRebuildRestorePhase } from "./rebuild-restore-phase";
1515
const BUILTIN_OBSERVABILITY_CONTENT =
1616
"network_policies:\n observability-otlp-local:\n name: observability-otlp-local\n";
1717

18+
type StandardRestoreOptions = Omit<
19+
Parameters<typeof runRebuildRestorePhase>[0],
20+
"targetAgentType" | "targetImageIsCustom"
21+
>;
22+
23+
function runStandardRebuildRestorePhase(options: StandardRestoreOptions) {
24+
return runRebuildRestorePhase({
25+
...options,
26+
targetAgentType: "openclaw",
27+
targetImageIsCustom: false,
28+
});
29+
}
30+
1831
describe("rebuild policy restore fidelity", () => {
1932
beforeEach(() => {
2033
vi.spyOn(policies, "loadPresetForSandbox").mockImplementation((_sandboxName, presetName) =>
@@ -40,7 +53,7 @@ describe("rebuild policy restore fidelity", () => {
4053
error: "could not read fresh OpenClaw plugin install registry",
4154
});
4255

43-
const result = runRebuildRestorePhase({
56+
const result = runStandardRebuildRestorePhase({
4457
sandboxName: "alpha",
4558
backupManifest: { agentType: "openclaw", backupPath: "/tmp/rebuild-backup" } as never,
4659
policyPresets: [],
@@ -78,7 +91,7 @@ describe("rebuild policy restore fidelity", () => {
7891
content: `network_policies:\n ${name}-custom:\n name: ${name}-custom\n`,
7992
sourcePath: `/tmp/${name}.yaml`,
8093
}));
81-
const result = runRebuildRestorePhase({
94+
const result = runStandardRebuildRestorePhase({
8295
sandboxName: "alpha",
8396
backupManifest: {
8497
agentType: "openclaw",
@@ -126,7 +139,7 @@ describe("rebuild policy restore fidelity", () => {
126139
sourcePath: "/tmp/custom-egress.yaml",
127140
},
128141
];
129-
const result = runRebuildRestorePhase({
142+
const result = runStandardRebuildRestorePhase({
130143
sandboxName: "alpha",
131144
backupManifest: null,
132145
policyPresets: [],
@@ -159,7 +172,7 @@ describe("rebuild policy restore fidelity", () => {
159172
"network_policies:\n mcp-bridge-search:\n endpoints:\n - host: mcp.example.com\n allowed_ips: [203.0.113.10]\n",
160173
sourcePath: MCP_BRIDGE_POLICY_SOURCE,
161174
};
162-
const result = runRebuildRestorePhase({
175+
const result = runStandardRebuildRestorePhase({
163176
sandboxName: "alpha",
164177
backupManifest: null,
165178
policyPresets: [],
@@ -188,7 +201,7 @@ describe("rebuild policy restore fidelity", () => {
188201
.mockReturnValueOnce("absent");
189202
const removePreset = vi.spyOn(policies, "removePreset").mockReturnValue(true);
190203

191-
const result = runRebuildRestorePhase({
204+
const result = runStandardRebuildRestorePhase({
192205
sandboxName: "alpha",
193206
backupManifest: null,
194207
policyPresets: ["npm"],
@@ -213,7 +226,7 @@ describe("rebuild policy restore fidelity", () => {
213226
.mockReturnValueOnce(null);
214227
vi.spyOn(policies, "removePreset").mockReturnValue(true);
215228

216-
const result = runRebuildRestorePhase({
229+
const result = runStandardRebuildRestorePhase({
217230
sandboxName: "alpha",
218231
backupManifest: null,
219232
policyPresets: ["npm"],
@@ -243,7 +256,7 @@ describe("rebuild policy restore fidelity", () => {
243256
throw new Error("apply failed");
244257
});
245258

246-
const result = runRebuildRestorePhase({
259+
const result = runStandardRebuildRestorePhase({
247260
sandboxName: "alpha",
248261
backupManifest: null,
249262
policyPresets: ["npm", "bad", "throw"],
@@ -264,7 +277,7 @@ describe("rebuild policy restore fidelity", () => {
264277
vi.spyOn(console, "error").mockImplementation(() => undefined);
265278
vi.spyOn(policies, "applyPreset").mockReturnValue(true);
266279

267-
const result = runRebuildRestorePhase({
280+
const result = runStandardRebuildRestorePhase({
268281
sandboxName: "alpha",
269282
backupManifest: null,
270283
policyPresets: ["observability-otlp-local"],
@@ -285,7 +298,7 @@ describe("rebuild policy restore fidelity", () => {
285298
vi.spyOn(policies, "applyPreset").mockReturnValue(true);
286299
vi.spyOn(policies, "getPresetContentGatewayState").mockReturnValue(null);
287300

288-
const result = runRebuildRestorePhase({
301+
const result = runStandardRebuildRestorePhase({
289302
sandboxName: "alpha",
290303
backupManifest: null,
291304
policyPresets: ["observability-otlp-local"],
@@ -301,7 +314,7 @@ describe("rebuild policy restore fidelity", () => {
301314
it("does not remove or persist DCode base-policy keys detected as broad presets", () => {
302315
const removePreset = vi.spyOn(policies, "removePreset");
303316

304-
const result = runRebuildRestorePhase({
317+
const result = runStandardRebuildRestorePhase({
305318
sandboxName: "alpha",
306319
backupManifest: null,
307320
policyPresets: [],
@@ -326,7 +339,7 @@ describe("rebuild policy restore fidelity", () => {
326339
sourcePath: "/tmp/operator-collector.yaml",
327340
};
328341

329-
const result = runRebuildRestorePhase({
342+
const result = runStandardRebuildRestorePhase({
330343
sandboxName: "alpha",
331344
backupManifest: null,
332345
policyPresets: [],
@@ -358,7 +371,7 @@ describe("rebuild policy restore fidelity", () => {
358371
sourcePath: "/tmp/corp-otel.yaml",
359372
};
360373

361-
const result = runRebuildRestorePhase({
374+
const result = runStandardRebuildRestorePhase({
362375
sandboxName: "alpha",
363376
backupManifest: null,
364377
policyPresets: [],
@@ -392,7 +405,7 @@ describe("rebuild policy restore fidelity", () => {
392405
.mockReturnValueOnce("absent");
393406
const removePreset = vi.spyOn(policies, "removePreset").mockReturnValue(true);
394407

395-
const result = runRebuildRestorePhase({
408+
const result = runStandardRebuildRestorePhase({
396409
sandboxName: "alpha",
397410
backupManifest: null,
398411
policyPresets: ["observability-otlp-local"],
@@ -421,7 +434,7 @@ describe("rebuild policy restore fidelity", () => {
421434
.mockReturnValueOnce("absent");
422435
const removePreset = vi.spyOn(policies, "removePreset").mockReturnValue(true);
423436

424-
const result = runRebuildRestorePhase({
437+
const result = runStandardRebuildRestorePhase({
425438
sandboxName: "alpha",
426439
backupManifest: null,
427440
policyPresets: [],
@@ -449,7 +462,7 @@ describe("rebuild policy restore fidelity", () => {
449462
vi.spyOn(policies, "getPresetContentGatewayState").mockReturnValue("drift");
450463
const removePreset = vi.spyOn(policies, "removePreset");
451464

452-
const result = runRebuildRestorePhase({
465+
const result = runStandardRebuildRestorePhase({
453466
sandboxName: "alpha",
454467
backupManifest: null,
455468
policyPresets: [],
@@ -476,7 +489,7 @@ describe("rebuild policy restore fidelity", () => {
476489
.mockReturnValueOnce(null);
477490
vi.spyOn(policies, "removePreset").mockReturnValue(true);
478491

479-
const result = runRebuildRestorePhase({
492+
const result = runStandardRebuildRestorePhase({
480493
sandboxName: "alpha",
481494
backupManifest: null,
482495
policyPresets: ["observability-otlp-local"],

src/lib/actions/sandbox/rebuild-restore-phase.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import type { RebuildSandboxEntry } from "./rebuild-flow-helpers";
1616

1717
export interface RebuildRestorePhaseInput {
1818
sandboxName: string;
19+
targetAgentType: string;
20+
targetImageIsCustom: boolean;
1921
backupManifest: RebuildBackupManifest;
2022
policyPresets: string[];
2123
customPolicies: NonNullable<RebuildSandboxEntry["customPolicies"]>;
@@ -176,6 +178,8 @@ function reconcileFinalManagedObservability(
176178
export function runRebuildRestorePhase(input: RebuildRestorePhaseInput): RebuildRestorePhaseResult {
177179
const {
178180
sandboxName,
181+
targetAgentType,
182+
targetImageIsCustom,
179183
backupManifest,
180184
policyPresets,
181185
customPolicies,
@@ -190,7 +194,10 @@ export function runRebuildRestorePhase(input: RebuildRestorePhaseInput): Rebuild
190194
const restore = sandboxState.restoreRecreatedSandboxState(
191195
sandboxName,
192196
backupManifest.backupPath,
193-
{ targetAgentType: backupManifest.agentType },
197+
{
198+
targetAgentType,
199+
...(targetImageIsCustom ? { allowCustomImageWholeStateFileRestore: true } : {}),
200+
},
194201
);
195202
log(
196203
`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}` : ""}`,

src/lib/actions/sandbox/snapshot-auto-create-failure.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ vi.mock("../../inference/nim", () => ({
4848
stopNimContainer: vi.fn(),
4949
stopNimContainerByName: vi.fn(),
5050
}));
51-
vi.mock("../../messaging/channels", () => ({ listMessagingProviderSuffixes: vi.fn(() => []) }));
51+
vi.mock("../../messaging/channels", () => ({
52+
listMessagingProviderSuffixes: vi.fn(() => []),
53+
listMessagingCredentialMetadata: vi.fn(() => []),
54+
}));
5255
vi.mock("../../policy", () => ({
5356
applyPreset: vi.fn(() => true),
5457
applyPresetContent: vi.fn(() => true),

0 commit comments

Comments
 (0)