-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(snapshot): prepare immutable managed clone handoff #8032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
107 commits
Select commit
Hold shift + click to select a range
38329e8
feat(onboard): add managed startup profile schema
ericksoa 7f3859a
docs(onboard): clarify startup profile transport encoding
ericksoa 32561a9
fix(onboard): harden managed startup profile
ericksoa 087a9a5
chore(stack): sync PR3.2 with current main
ericksoa e1845dd
fix(onboard): close startup profile contract gaps
ericksoa 244e3e6
chore(stack): sync PR3.2 with current main
ericksoa 2403316
chore(stack): sync PR3.2 with current main
ericksoa ea5bd8a
chore(stack): sync PR3.2 with current main
ericksoa 357081a
feat(onboard): construct managed startup profiles
ericksoa 25d32e0
feat(onboard): map and coordinate startup profiles
ericksoa 094d177
feat(onboard): apply startup profiles in managed images
ericksoa 06f67e5
feat(onboard): add managed shared-state transactions
ericksoa 2a048ae
chore(stack): restack PR3.3 on merged PR3.2
ericksoa 498e521
chore(stack): restack PR3.4a on PR3.3
ericksoa 3206926
chore(stack): restack PR3.4b on PR3.4a
ericksoa f3081cc
chore(stack): restack PR3.5 on PR3.4b
ericksoa 6da5eb2
test(onboard): keep managed startup tests branchless
ericksoa 51126c4
chore(stack): carry PR3.4b guardrail fix into PR3.5
ericksoa bb7a260
chore(stack): restack PR3.4a after PR3.3 merge
ericksoa fafdbd1
chore(stack): restack PR3.4b after PR3.3 merge
ericksoa 0edd158
chore(stack): restack PR3.5 after PR3.3 merge
ericksoa b14013f
test(onboard): cover managed startup fixture CLI
ericksoa 46fbe19
feat(onboard): apply startup profiles in managed images
ericksoa b2027a9
feat(onboard): add managed shared-state transactions
ericksoa c95bdf7
fix(onboard): serialize managed startup transactions
ericksoa 3f3bdbb
feat(onboard): apply startup profiles in managed images
ericksoa 8ff8094
feat(onboard): add managed shared-state transactions
ericksoa 7d1668b
test(onboard): keep transaction races branchless
ericksoa 540494c
chore(stack): inherit PR3.4a guardrail fix
ericksoa 1463687
chore(stack): inherit PR3.4b restack
ericksoa 3a03eeb
merge: resolve conflicts with main
github-actions[bot] d97b259
chore(stack): restack PR3.5 after PR3.4a merge
ericksoa deb1581
chore(stack): refresh PR3.4b validation
ericksoa 0ad91a5
chore(stack): inherit PR3.4b validation refresh
ericksoa a0f12f1
fix(onboard): harden managed startup inputs
ericksoa 27c72f5
fix(runtime): close prior review debt
ericksoa 1c824cc
test(runtime): keep env cleanup branchless
ericksoa 5c6b3f7
fix(runtime): preserve direct image execution
ericksoa 611fdb6
fix(messaging): reject ambiguous channel identities
ericksoa 76c54d4
fix(messaging): require canonical persisted channel ids
ericksoa e41521b
chore(stack): refresh PR3.4b on current main
ericksoa bebfc6c
chore(stack): refresh PR3.5 on current PR3.4b
ericksoa 864e249
chore(stack): refresh PR3.5a on current PR3.5
ericksoa 7cf2a8d
chore(stack): refresh PR3.5b on current PR3.5a
ericksoa 5f9d97d
test(e2e): add cross-runtime foundation
ericksoa abf45b3
test(e2e): keep runtime foundation branchless
ericksoa ed479ae
refactor(runtime): unify provider bundle registry
ericksoa 005b3a7
fix(runtime): harden provider contract boundaries
ericksoa 05cf2a5
fix(runtime): fail closed on managed image cleanup
ericksoa 5c29046
fix(messaging): reject unresolved build hooks
ericksoa 534773b
fix(runtime): close provider lifecycle authority gaps
ericksoa 847e827
chore(stack): refresh PR3.5c on current PR3.5b
ericksoa 77559f7
test(runtime): keep provider parity branchless
ericksoa a2afe77
fix(e2e): harden runtime foundation contracts
ericksoa 13932b9
chore(stack): refresh PR3.4b on current main
ericksoa 5111410
chore(stack): refresh PR3.5 on current PR3.4b
ericksoa 7d51947
chore(stack): refresh PR3.5a on current PR3.5
ericksoa 004b060
test(e2e): verify fixture profiles stay unregistered
ericksoa 771f48c
chore(stack): refresh PR3.5b on current PR3.5a
ericksoa c67afd3
chore(stack): refresh PR3.5c on current PR3.5b
ericksoa 4788d28
test(e2e): expose runtime profile registration state
ericksoa 64f562f
fix(runtime): preserve unproven cleanup authority
ericksoa 15eef1b
chore(stack): refresh PR3.6 on repaired base
ericksoa d73f13a
test(runtime): cover unknown provider doctor result
ericksoa 8f8bcd5
test(runtime): prove provider recovery boundaries
ericksoa f77bff8
fix(runtime): fail closed on retained cleanup ownership
ericksoa bf2fd19
test(rebuild): type cleanup ownership receipt
ericksoa 4316a18
fix(runtime): prove cleanup authority before deletion
ericksoa 062d66b
test(snapshot): mock destructive cleanup authority
ericksoa 75730cf
test(runtime): prove cleanup planning is inert
ericksoa 0b21b79
feat(rebuild): add atomic managed workload replacement
ericksoa 438f79f
fix(rebuild): fail closed across authority cutover
ericksoa 412d69d
docs(rebuild): define deferred recovery ownership
ericksoa cfe84cd
fix(rebuild): bind retained workload authority
ericksoa 827f545
test(rebuild): keep failure matrices linear
ericksoa add3965
test(rebuild): cover invalid provider artifacts
ericksoa 9bbf75d
fix(rebuild): address review feedback
ericksoa 8212bf9
merge: resolve conflicts with main
github-actions[bot] 4960f4d
chore(stack): refresh managed rebuild on current main
ericksoa bf94633
feat(snapshot): preserve managed runtime authority
ericksoa 3a4321d
fix(snapshot): enforce managed authority boundaries
ericksoa 8b19581
feat(snapshot): prepare immutable managed clone handoff
ericksoa 1a91ef2
test(snapshot): keep provider authority cases linear
ericksoa 410d4c9
chore(stack): refresh clone contract on PR3.8
ericksoa fabad32
test(snapshot): keep clone authority cases linear
ericksoa 844bfb6
fix(snapshot): pin files during authority hashing
ericksoa aeddd24
chore(stack): refresh clone handoff on snapshot security fix
ericksoa ed60882
fix(snapshot): address restore review findings
ericksoa 7c823d4
fix(snapshot): address clone handoff review findings
ericksoa e962454
merge: update PR3.8 review fixes
ericksoa 9e64898
docs(snapshot): clarify legacy restore retention
ericksoa 738e680
merge: refresh PR3.8 documentation
ericksoa 0013772
test(messaging): keep staged channel fixture canonical
ericksoa e97ecce
merge(stack): refresh managed workload rebuild parity
ericksoa 6b2c464
merge(stack): refresh managed snapshot parity
ericksoa 91c807c
merge(stack): refresh managed clone handoff contract
ericksoa 362a70c
chore(stack): refresh managed workload rebuild parity on current main
ericksoa b2374ee
chore(stack): refresh managed snapshot parity parent
ericksoa e09ce8a
chore(stack): refresh managed clone handoff parent
ericksoa b349b98
merge: resolve conflicts with main
github-actions[bot] 33d1756
merge(stack): restack #8031 onto current main
ericksoa 65f4d68
merge(stack): restack #8032 onto #8031
ericksoa a41d7e3
merge: resolve conflicts with main
github-actions[bot] 4e123ad
fix(snapshot): close clone handoff review feedback
ericksoa 4ce7dff
merge(stack): append snapshot sanitizer fix
ericksoa 7802271
test(snapshot): complete clone messaging fixture
ericksoa 6b6605b
chore(ci): refresh exact protected E2E evidence
ericksoa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
70 changes: 70 additions & 0 deletions
70
src/lib/actions/sandbox/snapshot-managed-clone-handoff-dormancy.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { createHash } from "node:crypto"; | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| import { managedStartupE2eProfile } from "../../../../scripts/checks/generate-managed-startup-profile-fixture.mts"; | ||
| import { MANAGED_IMAGE_REPOSITORIES } from "../../onboard/managed-image/contract"; | ||
| import { encodeManagedStartupProfile } from "../../onboard/managed-startup/profile"; | ||
| import * as fixture from "./snapshot-restore-test-fixture"; | ||
|
|
||
| beforeEach(() => fixture.resetSnapshotRestoreMocks()); | ||
| afterEach(() => fixture.cleanupSnapshotRestoreMocks()); | ||
|
|
||
| describe("managed snapshot clone activation boundary", () => { | ||
| it("rejects managed cross-sandbox restore before destination effects (#7744)", async () => { | ||
| const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); | ||
| const encodedProfile = encodeManagedStartupProfile(managedStartupE2eProfile("openclaw")); | ||
| fixture.getLatestBackupMock.mockReturnValue({ | ||
| snapshotVersion: 4, | ||
| timestamp: "2026-07-30T00:00:00.000Z", | ||
| backupPath: "/tmp/backup-alpha", | ||
| agentType: "openclaw", | ||
| workload: { | ||
| schemaVersion: 1, | ||
| kind: "managed-image", | ||
| reference: `${MANAGED_IMAGE_REPOSITORIES.openclaw}@sha256:${"a".repeat(64)}`, | ||
| platform: "linux/amd64", | ||
| release: "v0.0.100", | ||
| sourceRevision: "b".repeat(40), | ||
| sourceCohort: "ghrun-123-1", | ||
| capabilityContractVersion: 1, | ||
| startupProfileContractVersion: 1, | ||
| encodedProfile, | ||
| startupProfileSha256: createHash("sha256").update(encodedProfile, "utf8").digest("hex"), | ||
| credentialProxyReplayRequired: false, | ||
| shared: true, | ||
| }, | ||
| runtimeSnapshot: { | ||
| schemaVersion: 1, | ||
| providerId: "docker", | ||
| providerHandle: "snapshot-provider-handle", | ||
| lifecycleState: "running", | ||
| lifecycleGeneration: "snapshot-generation", | ||
| runtime: { | ||
| schemaVersion: 1, | ||
| providerId: "docker", | ||
| runtime: { kind: "docker-container", handle: "container-id" }, | ||
| acceleration: { kind: "none" }, | ||
| }, | ||
| }, | ||
| }); | ||
| fixture.getSandboxMock.mockImplementation((name) => | ||
| name === "alpha" ? { name: "alpha", agent: "openclaw", openshellDriver: "docker" } : null, | ||
| ); | ||
| const { runSandboxSnapshot } = await import("./snapshot"); | ||
|
|
||
| await expect( | ||
| runSandboxSnapshot("alpha", { kind: "restore", to: "beta", force: true, yes: true }), | ||
| ).rejects.toMatchObject({ exitCode: 1 }); | ||
|
|
||
| expect(consoleError.mock.calls.flat().join("\n")).toContain( | ||
| "requires managed-profile clone rebind", | ||
| ); | ||
| expect(fixture.lifecycleMock.events).not.toContain("delete"); | ||
| expect(fixture.streamSandboxCreateMock).not.toHaveBeenCalled(); | ||
| expect(fixture.restoreSandboxStateMock).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { cloneAndDeepFreeze } from "../core/immutable"; | ||
| import { isValidName } from "../name-validation"; | ||
| import { createBuiltInChannelManifestRegistry } from "./channels/built-ins"; | ||
| import { resolveSandboxNameTemplate } from "./compiler/engines/template"; | ||
| import { hydrateDerivedSandboxMessagingPlanFields } from "./hydration"; | ||
| import type { MessagingAgentId, SandboxMessagingPlan } from "./manifest"; | ||
| import { compactSandboxMessagingPlanForPersistence } from "./persistence"; | ||
| import { parseSandboxMessagingPlan } from "./plan-validation"; | ||
|
|
||
| export interface SandboxMessagingCloneRebindInput { | ||
| readonly sourceSandboxName: string; | ||
| readonly destinationSandboxName: string; | ||
| readonly agent: MessagingAgentId; | ||
| readonly sourcePlan: unknown; | ||
| /** | ||
| * Explicit non-secret inputs used by manifest renderers. Clone rebinding | ||
| * never consults process.env, so an unrelated host credential cannot change | ||
| * the destination plan or enter its fingerprints. | ||
| */ | ||
| readonly environment?: Readonly<Record<string, string | undefined>>; | ||
| } | ||
|
|
||
| export class SandboxMessagingCloneRebindError extends Error { | ||
| constructor(message: string) { | ||
| super(`Cannot rebind managed messaging plan: ${message}`); | ||
| this.name = "SandboxMessagingCloneRebindError"; | ||
| } | ||
| } | ||
|
|
||
| function fail(message: string): never { | ||
| throw new SandboxMessagingCloneRebindError(message); | ||
| } | ||
|
|
||
| function requireSandboxName(value: string, label: string): string { | ||
| if (!isValidName(value)) fail(`${label} sandbox name is invalid`); | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * Recompile one secret-free managed messaging plan for a destination sandbox. | ||
| * | ||
| * Compact persistence data is the retained intent boundary. All target-bound | ||
| * provider names and executable derived fields are rebuilt from current | ||
| * built-in manifests with an explicit, credential-free environment. | ||
| */ | ||
| export function rebindSandboxMessagingPlanForClone( | ||
| input: SandboxMessagingCloneRebindInput, | ||
| ): SandboxMessagingPlan { | ||
| const sourceSandboxName = requireSandboxName(input.sourceSandboxName, "source"); | ||
| const destinationSandboxName = requireSandboxName(input.destinationSandboxName, "destination"); | ||
| if (sourceSandboxName === destinationSandboxName) { | ||
| fail("source and destination sandbox names must differ"); | ||
| } | ||
|
|
||
| const manifestRegistry = createBuiltInChannelManifestRegistry(); | ||
| const supportedChannelIds = manifestRegistry | ||
| .listAvailable({ agent: input.agent }) | ||
| .map((manifest) => manifest.id); | ||
| const environment = Object.freeze({ ...(input.environment ?? {}) }); | ||
| const sourcePlan = parseSandboxMessagingPlan(input.sourcePlan, { | ||
| sandboxName: sourceSandboxName, | ||
| agent: input.agent, | ||
| supportedChannelIds, | ||
| environment, | ||
| }); | ||
| if (!sourcePlan) fail("source plan is invalid or uses a non-built-in channel"); | ||
| const sourceChannelIds = new Set(sourcePlan.channels.map((channel) => channel.channelId)); | ||
| if (sourcePlan.disabledChannels.some((channelId) => !sourceChannelIds.has(channelId))) { | ||
| fail("source plan disables a channel that is not configured"); | ||
| } | ||
| for (const channel of sourcePlan.channels) { | ||
| const manifest = manifestRegistry.get(channel.channelId); | ||
| if (!manifest || !manifest.supportedAgents.includes(input.agent)) { | ||
| fail(`source channel ${channel.channelId} is not a supported built-in`); | ||
| } | ||
| const listedDisabled = sourcePlan.disabledChannels.includes(channel.channelId); | ||
| if (channel.disabled !== listedDisabled || (channel.active && !channel.configured)) { | ||
| fail(`source channel ${channel.channelId} has inconsistent lifecycle state`); | ||
| } | ||
| const seenInputIds = new Set<string>(); | ||
| for (const planInput of channel.inputs) { | ||
| if (seenInputIds.has(planInput.inputId)) { | ||
| fail(`source channel ${channel.channelId} repeats input ${planInput.inputId}`); | ||
| } | ||
| seenInputIds.add(planInput.inputId); | ||
| const manifestInput = manifest.inputs.find((candidate) => candidate.id === planInput.inputId); | ||
| if (!manifestInput || manifestInput.kind !== planInput.kind) { | ||
| fail( | ||
| `source channel ${channel.channelId} input ${planInput.inputId} is not manifest-owned`, | ||
| ); | ||
| } | ||
| if (planInput.kind === "secret" && planInput.value !== undefined) { | ||
| fail("source plan contains a raw secret input value"); | ||
| } | ||
| } | ||
| if (channel.active && !channel.disabled) { | ||
| for (const requiredInput of manifest.inputs.filter((candidate) => candidate.required)) { | ||
| const retained = channel.inputs.find((candidate) => candidate.inputId === requiredInput.id); | ||
| const available = | ||
| requiredInput.kind === "secret" | ||
| ? retained?.credentialAvailable === true | ||
| : retained?.value !== undefined; | ||
| if (!available) { | ||
| fail( | ||
| `active source channel ${channel.channelId} is missing required input ${requiredInput.id}`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const compact = compactSandboxMessagingPlanForPersistence(sourcePlan); | ||
| const credentialBindings = (compact.credentialBindings ?? []).map( | ||
| ({ credentialHash: _sourceCredentialHash, ...binding }) => binding, | ||
| ); | ||
| const targetIntent = { | ||
| ...compact, | ||
| // A source hash describes the source gateway credential, not the explicit | ||
| // credential that clone provisioning will write into the destination. | ||
| credentialBindings, | ||
| sandboxName: destinationSandboxName, | ||
| // These fields are executable output, not retained clone intent. | ||
| networkPolicy: { presets: [], entries: [] }, | ||
| agentRender: [], | ||
| buildSteps: [], | ||
| runtimeSetup: { nodePreloads: [], envAliases: [], secretScans: [] }, | ||
| stateUpdates: [], | ||
| healthChecks: [], | ||
| } as const; | ||
| const normalized = parseSandboxMessagingPlan(targetIntent, { | ||
| sandboxName: destinationSandboxName, | ||
| agent: input.agent, | ||
| supportedChannelIds, | ||
| environment, | ||
| }); | ||
| if (!normalized) fail("destination intent could not be normalized"); | ||
| const hydrated = hydrateDerivedSandboxMessagingPlanFields(normalized, { environment }); | ||
| const rebound = parseSandboxMessagingPlan(hydrated, { | ||
| sandboxName: destinationSandboxName, | ||
| agent: input.agent, | ||
| supportedChannelIds, | ||
| environment, | ||
| }); | ||
| if (!rebound) fail("destination plan could not be validated after manifest hydration"); | ||
| const secretProvenanceNeutralRebound = { | ||
| ...rebound, | ||
| credentialBindings: rebound.credentialBindings.map( | ||
| ({ credentialHash: _ambientCredentialHash, ...binding }) => binding, | ||
| ), | ||
| }; | ||
| for (const binding of secretProvenanceNeutralRebound.credentialBindings) { | ||
| const credential = manifestRegistry | ||
| .get(binding.channelId) | ||
| ?.credentials.find((candidate) => candidate.id === binding.credentialId); | ||
| const expectedProviderName = | ||
| credential === undefined | ||
| ? undefined | ||
| : resolveSandboxNameTemplate(credential.providerName, destinationSandboxName); | ||
| if (!expectedProviderName || binding.providerName !== expectedProviderName) { | ||
| fail(`destination provider identity for ${binding.channelId} could not be proven`); | ||
| } | ||
| } | ||
| return cloneAndDeepFreeze(secretProvenanceNeutralRebound); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.