diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 6f7f60a95f9..2873a63147d 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -5253,6 +5253,7 @@ jobs: shell: bash run: | set -euo pipefail + printf 'NEMOCLAW_PROTECTED_RUNTIME_STARTED_AT_MS=%s000\n' "$(date +%s)" >> "$GITHUB_ENV" [[ "$REPOSITORY" == "NVIDIA/NemoClaw" && ( ( "$EVENT_NAME" == "push" && "$REF" == "refs/heads/main" ) || ( "$EVENT_NAME" == "workflow_dispatch" && "$REF" == refs/heads/* ) ) ]] || { echo "::error::Protected managed-image runtime must run from NVIDIA/NemoClaw" >&2 exit 1 @@ -5539,6 +5540,7 @@ jobs: npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/managed-image-protected-runtime.test.ts - name: Remove isolated protected runtime registry + timeout-minutes: 2 if: always() shell: bash run: | @@ -5565,6 +5567,7 @@ jobs: fi - name: Restore trusted OpenShell sources + timeout-minutes: 1 if: ${{ always() && steps.openshell-projection.outcome == 'success' }} shell: bash env: @@ -5575,6 +5578,7 @@ jobs: node tools/e2e/protected-openshell-workspace.mts restore > "$E2E_ARTIFACT_DIR/openshell-projection-restored.json" - name: Upload protected managed-image runtime artifacts + timeout-minutes: 4 if: always() uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 with: @@ -5582,6 +5586,7 @@ jobs: path: e2e-artifacts/live/managed-image-protected-runtime/ - name: Clean up Docker auth + timeout-minutes: 1 if: always() shell: bash run: bash .github/scripts/docker-auth-cleanup.sh diff --git a/ci/e2e-assertion-budget.json b/ci/e2e-assertion-budget.json index 1ff7f84a466..c15695e5515 100644 --- a/ci/e2e-assertion-budget.json +++ b/ci/e2e-assertion-budget.json @@ -1,5 +1,5 @@ { - "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0\n\nInitial no-growth baseline for issue #10934 and Epic #10920. The epic reference remains fixed at fe18b2f004e2304c52db33cf7bb5895828197f9a. Current main is lower because #10722 removed the live Shields assertion suites before this ratchet landed. PR #10341 adds the accepted public deferred-install and same-home credential continuation boundary for Hermes and Deep Agents Code; existing per-file budgets remain unchanged. PR #13055 adds five explicitly approved assertions for the native Deep Agents Code OpenRouter request in the existing hosted OpenRouter target. PR #13293 consolidates native-local cases into the existing OpenClaw switching owner. Removing duplicated deterministic metadata and direct mock requests lowers suite totals while retaining real authenticated and sentinel agent turns, lifecycle, model switching, credential isolation and sibling denial. Per-file transitive counts include existing imported helpers; no live files or suite assertion points are added. PR #13596 adds four assertions to the existing double-onboard target for #13595: the managed OpenClaw state volume exists before `nemoclaw uninstall`, uninstall succeeds, the volume is gone afterwards, and same-name onboarding succeeds.", + "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0\n\nInitial no-growth baseline for issue #10934 and Epic #10920. The epic reference remains fixed at fe18b2f004e2304c52db33cf7bb5895828197f9a. Current main is lower because #10722 removed the live Shields assertion suites before this ratchet landed. PR #10341 adds the accepted public deferred-install and same-home credential continuation boundary for Hermes and Deep Agents Code; existing per-file budgets remain unchanged. PR #13055 adds five explicitly approved assertions for the native Deep Agents Code OpenRouter request in the existing hosted OpenRouter target. PR #13293 consolidates native-local cases into the existing OpenClaw switching owner. Removing duplicated deterministic metadata and direct mock requests lowers suite totals while retaining real authenticated and sentinel agent turns, lifecycle, model switching, credential isolation and sibling denial. Per-file transitive counts include existing imported helpers; no live files or suite assertion points are added.\n\nPR #13596 adds four assertions to the existing double-onboard target for #13595: the managed OpenClaw state volume exists before `nemoclaw uninstall`, uninstall succeeds, the volume is gone afterwards, and same-name onboarding succeeds.", "schemaVersion": 1, "issue": 10934, "epic": 10920, @@ -13,7 +13,7 @@ }, "limits": { "testFileCount": 77, - "liveFileCount": 186, + "liveFileCount": 187, "direct": { "expectCalls": 1270, "matcherAssertions": 1250, @@ -32,11 +32,11 @@ "nodeAssertions": 131, "namedAssertionHelpers": 668, "failCalls": 1, - "throwGuards": 526, + "throwGuards": 531, "objectFieldAssertions": 266, - "assertionPoints": 3265, - "generatedProbeBlocks": 207, - "generatedProbeConditions": 712 + "assertionPoints": 3270, + "generatedProbeBlocks": 208, + "generatedProbeConditions": 713 }, "fileMetricOrder": [ "directExpectCalls", @@ -83,7 +83,7 @@ "test/e2e/live/llama-cpp-generic-gpu.test.ts": [17,35,33,61,2], "test/e2e/live/managed-image-activation-e2e.test.ts": [0,0,12,25,0], "test/e2e/live/managed-image-multiarch-startup.test.ts": [1,1,1,7,0], - "test/e2e/live/managed-image-protected-runtime.test.ts": [0,0,39,76,15], + "test/e2e/live/managed-image-protected-runtime.test.ts": [0,0,39,81,16], "test/e2e/live/mcp-bridge.test.ts": [39,138,193,518,46], "test/e2e/live/messaging-compatible-endpoint.test.ts": [13,19,13,28,2], "test/e2e/live/messaging-providers.test.ts": [0,11,6,51,11], diff --git a/ci/e2e-assertion-growth-exceptions.json b/ci/e2e-assertion-growth-exceptions.json index fe306567e25..227e902d8b6 100644 --- a/ci/e2e-assertion-growth-exceptions.json +++ b/ci/e2e-assertion-growth-exceptions.json @@ -1,5 +1,5 @@ { - "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0\n\nMaintainer-approved exception for the exact PR #10341 budget transition. Adds only deferred-onboarding.test.ts with [12,14,12,14,0]; existing per-file budgets remain unchanged. Approval: https://github.com/NVIDIA/NemoClaw/pull/10341#issuecomment-5799263232. Local hooks and candidate CI may use this branch policy. The independent PR check reads only trusted base policy, so its expected growth failure is explicitly waived for #10341. A changed base or candidate budget invalidates this exception. Remove after #10341 merges.\n\nPR #12305 adds 19 live assertion points for the approved stopped-container recovery proof while retaining healthy-rebuild coverage. Exact budget-transition approval: https://github.com/NVIDIA/NemoClaw/pull/12305#issuecomment-5803852010. No other budget transition is accepted by this entry.\n\nThe same 19-assertion helper is also exercised by rebuild-openclaw, independently of the inherited private-MCP failure. Unique assertion counts are unchanged; one helper file is extracted. Revised exact-transition approval: https://github.com/NVIDIA/NemoClaw/pull/12305#issuecomment-5807159038.\n\nProposed exact budget transition for PR #12591: replace a restart progress-message check with a native gateway-owner observation before and after inference switching. Adds one generated probe and five probe conditions; direct and unique expectation counts do not exceed main. The imported helper also appears in the full-e2e transitive inventory. This entry requires maintainer review before landing on main. Remove it after #12591 merges.\n\nPR #12718 is the prerequisite for the PR #12382 budget transition recorded below. The exception matches only PR #12382 and both budget digests. A changed base or candidate budget invalidates it. The independent check reads trusted base policy, so this prerequisite must land on main before #12382 uses the exception. Remove after #12382 merges.\n\nMaintainer Prekshi Vyas (@prekshivyas) approved this exact refreshed PR #12382 transition on 2026-10-08: base budget SHA-256 53c51c4229d387633165771730f8f1a8373f84e9f3303b2144280027a56e3441 to candidate budget SHA-256 8162408384d7a6b97ef0062042c73d4bc9486f54d2dfa6c0c5732e42d1060e2a. Acceptance record: https://github.com/NVIDIA/NemoClaw/pull/12840#issuecomment-6066319444. It includes one additional unique assertion point and one generated probe condition. This refresh preserves the #12608 concurrent-gateway-ports reduction and the previously reviewed runtime assertion changes. The independent check is not waived: the refreshed exception must land on main before enforcement can accept it. A different PR or budget digest remains rejected. Remove after #12382 merges.\n\nPR #12777 adds exactly two approved live assertions for native host-local inference success and sibling provider isolation. Deterministic behavior remains in unit/integration tests. Approval transcription: https://github.com/NVIDIA/NemoClaw/pull/12777#issuecomment-6048385152. The independent check reads trusted base policy; this entry does not disable it. Remove after #12777 merges.\n\nThe #12777 budget digests account for main #12608 removing seven assertions from concurrent-gateway-ports. The candidate preserves that reduction and adds only the same two approved inference-routing assertions. No other file budget increases.\n\nPR #12886 approved the provider-smoke transition extracted by #12955. Approval: https://github.com/NVIDIA/NemoClaw/pull/12886#issuecomment-6083839176. The unchanged change digest binds the same nine assertion points and companion inventory. This entry supports local preliminary validation and the Slice 2 consumer. Independent CI for the prerequisite requires its own maintainer approval record; this entry does not approve that PR.\n\nPR #12958 extracts the same approved provider-smoke budget transition for #12955. Its exact, unedited maintainer approval is https://github.com/NVIDIA/NemoClaw/pull/12958#issuecomment-6093851571. This candidate entry supports static hooks; the independent check continues to verify the PR-specific approval on GitHub. Remove after #12958 merges.\n\nPR #13203 adds the approved 19 assertion points for real authenticated and credentialless DCode native-local requests, credential isolation and sibling denial. Approval: https://github.com/NVIDIA/NemoClaw/pull/13203#issuecomment-6180239679. This exact change digest supports local hooks and candidate CI. Independent enforcement still verifies the PR-specific maintainer approval on GitHub. Remove after #13203 merges.\n\nPR #13201 adds one generated probe block and one counted probe condition in the existing full E2E owner to verify the selected sandbox agent endpoint, API, model and credential reference. Rebecca Sliter approved this exact delta: https://github.com/NVIDIA/NemoClaw/pull/13201#issuecomment-6180464605. This entry supports local validation; the independent check must verify the maintainer record. Remove after #13201 merges.\n\nPR #13293 consolidates OpenClaw native-local scenarios into the existing switching owner and removes duplicated deterministic metadata checks and the redundant mock curl probe. Suite totals fall by 16 expect calls and 10 assertion points without adding live files. Only the owning file transitive allocation grows because it now imports existing live helpers. Maintainer authorization: https://github.com/NVIDIA/NemoClaw/pull/13293#issuecomment-6211036786. This exact transition supports candidate CI; independent enforcement still verifies the maintainer record.\n\nPR #13007 retains the same ten Pi native-local live assertion points approved at https://github.com/NVIDIA/NemoClaw/pull/13007#issuecomment-6175787892. The exact change digest is unchanged against its replacement OpenClaw parent #13293. This entry supports local checks against that stacked base; independent enforcement still checks the authenticated PR approval.\n\nPR #13582 adds eight live assertion points for the WSL2 IPv6 native-local policy boundary. Only openclaw-inference-switch.test.ts increases, by seven direct expectations and the existing onboard-success helper. Deterministic CIDR and receipt coverage remain in source tests. Maintainer approval: https://github.com/NVIDIA/NemoClaw/pull/13582#issuecomment-6308860348. Independent enforcement verifies that PR-specific record. Remove this preliminary local exception after merge.\n\nPR #13651 moves the native NVIDIA Discord rebuild proof from hermes-discord to hermes-inference-switch, which receives the public endpoint credential. The native switch target also checks status through its installed credential verifier after rebuild. This adds four assertion points for the live status and restored-provider boundary; deterministic rejection and command-failure cases remain in fast tests. This entry permits the approved transition in local and candidate checks; independent enforcement verifies the maintainer record at https://github.com/NVIDIA/NemoClaw/pull/13651#issuecomment-6377201610. The typed helper extraction retains every live predicate and the same unique and transitive totals; the direct census decreases because the phase now lives in the existing helper.\n\nPR #13596 adds four live assertion points to the existing double-onboard target for #13595: the managed OpenClaw state volume exists before `nemoclaw uninstall`, uninstall succeeds, the volume is gone afterwards, and same-name onboarding succeeds. Deterministic removal and failure cases remain in fast tests. This entry permits the approved transition in local and candidate checks; independent enforcement verifies the maintainer record at https://github.com/NVIDIA/NemoClaw/pull/13596#issuecomment-6365742624. Remove after #13596 merges.", + "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0\n\nMaintainer-approved exception for the exact PR #10341 budget transition. Adds only deferred-onboarding.test.ts with [12,14,12,14,0]; existing per-file budgets remain unchanged. Approval: https://github.com/NVIDIA/NemoClaw/pull/10341#issuecomment-5799263232. Local hooks and candidate CI may use this branch policy. The independent PR check reads only trusted base policy, so its expected growth failure is explicitly waived for #10341. A changed base or candidate budget invalidates this exception. Remove after #10341 merges.\n\nPR #12305 adds 19 live assertion points for the approved stopped-container recovery proof while retaining healthy-rebuild coverage. Exact budget-transition approval: https://github.com/NVIDIA/NemoClaw/pull/12305#issuecomment-5803852010. No other budget transition is accepted by this entry.\n\nThe same 19-assertion helper is also exercised by rebuild-openclaw, independently of the inherited private-MCP failure. Unique assertion counts are unchanged; one helper file is extracted. Revised exact-transition approval: https://github.com/NVIDIA/NemoClaw/pull/12305#issuecomment-5807159038.\n\nProposed exact budget transition for PR #12591: replace a restart progress-message check with a native gateway-owner observation before and after inference switching. Adds one generated probe and five probe conditions; direct and unique expectation counts do not exceed main. The imported helper also appears in the full-e2e transitive inventory. This entry requires maintainer review before landing on main. Remove it after #12591 merges.\n\nPR #12718 is the prerequisite for the PR #12382 budget transition recorded below. The exception matches only PR #12382 and both budget digests. A changed base or candidate budget invalidates it. The independent check reads trusted base policy, so this prerequisite must land on main before #12382 uses the exception. Remove after #12382 merges.\n\nMaintainer Prekshi Vyas (@prekshivyas) approved this exact refreshed PR #12382 transition on 2026-10-08: base budget SHA-256 53c51c4229d387633165771730f8f1a8373f84e9f3303b2144280027a56e3441 to candidate budget SHA-256 8162408384d7a6b97ef0062042c73d4bc9486f54d2dfa6c0c5732e42d1060e2a. Acceptance record: https://github.com/NVIDIA/NemoClaw/pull/12840#issuecomment-6066319444. It includes one additional unique assertion point and one generated probe condition. This refresh preserves the #12608 concurrent-gateway-ports reduction and the previously reviewed runtime assertion changes. The independent check is not waived: the refreshed exception must land on main before enforcement can accept it. A different PR or budget digest remains rejected. Remove after #12382 merges.\n\nPR #12777 adds exactly two approved live assertions for native host-local inference success and sibling provider isolation. Deterministic behavior remains in unit/integration tests. Approval transcription: https://github.com/NVIDIA/NemoClaw/pull/12777#issuecomment-6048385152. The independent check reads trusted base policy; this entry does not disable it. Remove after #12777 merges.\n\nThe #12777 budget digests account for main #12608 removing seven assertions from concurrent-gateway-ports. The candidate preserves that reduction and adds only the same two approved inference-routing assertions. No other file budget increases.\n\nPR #12886 approved the provider-smoke transition extracted by #12955. Approval: https://github.com/NVIDIA/NemoClaw/pull/12886#issuecomment-6083839176. The unchanged change digest binds the same nine assertion points and companion inventory. This entry supports local preliminary validation and the Slice 2 consumer. Independent CI for the prerequisite requires its own maintainer approval record; this entry does not approve that PR.\n\nPR #12958 extracts the same approved provider-smoke budget transition for #12955. Its exact, unedited maintainer approval is https://github.com/NVIDIA/NemoClaw/pull/12958#issuecomment-6093851571. This candidate entry supports static hooks; the independent check continues to verify the PR-specific approval on GitHub. Remove after #12958 merges.\n\nPR #13203 adds the approved 19 assertion points for real authenticated and credentialless DCode native-local requests, credential isolation and sibling denial. Approval: https://github.com/NVIDIA/NemoClaw/pull/13203#issuecomment-6180239679. This exact change digest supports local hooks and candidate CI. Independent enforcement still verifies the PR-specific maintainer approval on GitHub. Remove after #13203 merges.\n\nPR #13201 adds one generated probe block and one counted probe condition in the existing full E2E owner to verify the selected sandbox agent endpoint, API, model and credential reference. Rebecca Sliter approved this exact delta: https://github.com/NVIDIA/NemoClaw/pull/13201#issuecomment-6180464605. This entry supports local validation; the independent check must verify the maintainer record. Remove after #13201 merges.\n\nPR #13293 consolidates OpenClaw native-local scenarios into the existing switching owner and removes duplicated deterministic metadata checks and the redundant mock curl probe. Suite totals fall by 16 expect calls and 10 assertion points without adding live files. Only the owning file transitive allocation grows because it now imports existing live helpers. Maintainer authorization: https://github.com/NVIDIA/NemoClaw/pull/13293#issuecomment-6211036786. This exact transition supports candidate CI; independent enforcement still verifies the maintainer record.\n\nPR #13007 retains the same ten Pi native-local live assertion points approved at https://github.com/NVIDIA/NemoClaw/pull/13007#issuecomment-6175787892. The exact change digest is unchanged against its replacement OpenClaw parent #13293. This entry supports local checks against that stacked base; independent enforcement still checks the authenticated PR approval.\n\nPR #13582 adds eight live assertion points for the WSL2 IPv6 native-local policy boundary. Only openclaw-inference-switch.test.ts increases, by seven direct expectations and the existing onboard-success helper. Deterministic CIDR and receipt coverage remain in source tests. Maintainer approval: https://github.com/NVIDIA/NemoClaw/pull/13582#issuecomment-6308860348. Independent enforcement verifies that PR-specific record. Remove this preliminary local exception after merge.\n\nPR #13651 moves the native NVIDIA Discord rebuild proof from hermes-discord to hermes-inference-switch, which receives the public endpoint credential. The native switch target also checks status through its installed credential verifier after rebuild. This adds four assertion points for the live status and restored-provider boundary; deterministic rejection and command-failure cases remain in fast tests. This entry permits the approved transition in local and candidate checks; independent enforcement verifies the maintainer record at https://github.com/NVIDIA/NemoClaw/pull/13651#issuecomment-6377201610. The typed helper extraction retains every live predicate and the same unique and transitive totals; the direct census decreases because the phase now lives in the existing helper.\n\nPR #13635 adds the approved protected-runtime deadline companion and five unique assertion points, one generated probe block, and one probe condition. Exact maintainer approval: https://github.com/NVIDIA/NemoClaw/pull/13635#issuecomment-6383152261. This entry supports local hooks and candidate CI; independent enforcement verifies the PR-specific GitHub approval. Remove after #13635 merges.\n\nPR #13596 adds four live assertion points to the existing double-onboard target for #13595: the managed OpenClaw state volume exists before `nemoclaw uninstall`, uninstall succeeds, the volume is gone afterwards, and same-name onboarding succeeds. Deterministic removal and failure cases remain in fast tests. This entry permits the approved transition in local and candidate checks; independent enforcement verifies the maintainer record at https://github.com/NVIDIA/NemoClaw/pull/13596#issuecomment-6365742624. Remove after #13596 merges.", "schemaVersion": 1, "exceptions": [ { @@ -75,6 +75,10 @@ "pullRequest": 13651, "changeSha256": "3faad34162420fe9b37b336d1b011994840875cbc7e9d7a2be04464362cb36d5" }, + { + "pullRequest": 13635, + "changeSha256": "dd79510039aa512cf8ee078f53aeaf402d0e735556d338a97a477c43b584497f" + }, { "pullRequest": 13596, "changeSha256": "82995b4d29552dbb5922aa72003bc495f7786cbf927932773acaa192bc072b1c" diff --git a/ci/pi-agent-qualification-v1-linux-amd64.json b/ci/pi-agent-qualification-v1-linux-amd64.json index ad87b8a5d53..134121b51fc 100644 --- a/ci/pi-agent-qualification-v1-linux-amd64.json +++ b/ci/pi-agent-qualification-v1-linux-amd64.json @@ -3,13 +3,13 @@ "agent": "pi", "platform": "linux/amd64", "image": "ghcr.io/nvidia/nemoclaw/pi-sandbox", - "digest": "sha256:f7774c105ecb12d6f4b3e2c4aed3bf44a34c7dd227408729eefb4fb39965f045", - "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:f7774c105ecb12d6f4b3e2c4aed3bf44a34c7dd227408729eefb4fb39965f045", + "digest": "sha256:b529abf6e8d896ead20c4ad00ff0da268f18349a7cdc5478857d094bbf304b54", + "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:b529abf6e8d896ead20c4ad00ff0da268f18349a7cdc5478857d094bbf304b54", "source": { "repository": "NVIDIA/NemoClaw", - "revision": "9e8d4a90c9265a7304f86ae49dfb850d8a772fa0", + "revision": "063849c04f811af760c827b118190ee9a8581483", "release": "v0.1.0", - "cohort": "ghrun-40123781968-1" + "cohort": "ghrun-40335218142-1" }, "startupProfileContractVersion": 1, "capabilityContractVersion": 1 diff --git a/ci/pi-agent-qualification-v1-linux-arm64.json b/ci/pi-agent-qualification-v1-linux-arm64.json index 24cdaa927fe..9068c639ed7 100644 --- a/ci/pi-agent-qualification-v1-linux-arm64.json +++ b/ci/pi-agent-qualification-v1-linux-arm64.json @@ -3,13 +3,13 @@ "agent": "pi", "platform": "linux/arm64", "image": "ghcr.io/nvidia/nemoclaw/pi-sandbox", - "digest": "sha256:119936eab2d834f8a1f3ca857a1b318e04903b39ed7b07227c5dfcbf35428694", - "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:119936eab2d834f8a1f3ca857a1b318e04903b39ed7b07227c5dfcbf35428694", + "digest": "sha256:bd5a54699b8bea585091ee837ccfc13d9723eb6a338572dda829b424ec3255bb", + "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:bd5a54699b8bea585091ee837ccfc13d9723eb6a338572dda829b424ec3255bb", "source": { "repository": "NVIDIA/NemoClaw", - "revision": "9e8d4a90c9265a7304f86ae49dfb850d8a772fa0", + "revision": "063849c04f811af760c827b118190ee9a8581483", "release": "v0.1.0", - "cohort": "ghrun-40123781968-1" + "cohort": "ghrun-40335218142-1" }, "startupProfileContractVersion": 1, "capabilityContractVersion": 1 diff --git a/scripts/checks/managed-image-protected-runtime-contract.ts b/scripts/checks/managed-image-protected-runtime-contract.ts index c2dbf1a144d..3d685e89e9d 100644 --- a/scripts/checks/managed-image-protected-runtime-contract.ts +++ b/scripts/checks/managed-image-protected-runtime-contract.ts @@ -1,10 +1,77 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { createManagedProviderAdapter } from "../../src/lib/adapters/openshell/managed-provider-adapter.ts"; +import { parseOpenShellVersionFromText } from "../../src/lib/adapters/openshell/version-text.ts"; +import { + ensureNativeLocalProvider, + verifyNativeLocalProviderAttachment, + type NativeLocalProviderAttachment, +} from "../../src/lib/inference/native-local/profile.ts"; +import { + NATIVE_LOCAL_AUTH_HEADER_SCRIPT, + NATIVE_LOCAL_CREDENTIAL_ENV, +} from "../../src/lib/inference/native-local/agent-config.ts"; import { createCliOpenShellSandboxLifecycleFromRunner } from "../../src/lib/adapters/openshell/sandbox-lifecycle-cli.ts"; import { createDockerGpuDiagnosticRedactor } from "../../src/lib/onboard/docker-gpu-diagnostic-redaction.ts"; import type { ShippedManagedImageAgent } from "../../src/lib/onboard/managed-image/contract.ts"; import type { ManagedStartupProfile } from "../../src/lib/onboard/managed-startup/profile.ts"; +import { gatewayIdForStateDir } from "../../src/lib/onboard/gateway/process-environment.ts"; + +const MANAGED_IMAGE_HARNESS_OPENSHELL_VERSIONS = ["0.0.116", "0.1.3"] as const; +export type ManagedImageHarnessOpenShellVersion = + (typeof MANAGED_IMAGE_HARNESS_OPENSHELL_VERSIONS)[number]; + +export function requireManagedImageHarnessOpenShellVersion( + version: string | null | undefined, +): ManagedImageHarnessOpenShellVersion { + if ( + MANAGED_IMAGE_HARNESS_OPENSHELL_VERSIONS.includes( + version as ManagedImageHarnessOpenShellVersion, + ) + ) { + return version as ManagedImageHarnessOpenShellVersion; + } + throw new Error(`Unsupported protected OpenShell runtime version: ${version ?? "unavailable"}`); +} + +export function requireManagedImageHarnessOpenShellVersionFromOutput( + versionOutput: string, +): ManagedImageHarnessOpenShellVersion { + return requireManagedImageHarnessOpenShellVersion(parseOpenShellVersionFromText(versionOutput)); +} + +export function isManagedImageHarnessWorkload( + record: + | { + Config?: { Labels?: Record }; + Image?: string; + HostConfig?: { NetworkMode?: string }; + NetworkSettings?: { Networks?: Record }; + } + | undefined, + expected: { + sandbox: string; + stateDir: string; + imageContentId: string; + runtimeVersion: ManagedImageHarnessOpenShellVersion; + networkName?: string; + }, +): boolean { + const labels = record?.Config?.Labels; + return ( + labels?.["openshell.ai/managed-by"] === "openshell" && + labels["openshell.ai/sandbox-name"] === expected.sandbox && + record?.Image === expected.imageContentId && + (expected.runtimeVersion === "0.1.3" + ? labels["openshell.ai/isolation-role"] === "sandbox" && + labels["openshell.ai/sandbox-namespace"] === gatewayIdForStateDir(expected.stateDir) && + record.HostConfig?.NetworkMode === "none" + : typeof expected.networkName === "string" && + expected.networkName.length > 0 && + Object.hasOwn(record.NetworkSettings?.Networks ?? {}, expected.networkName)) + ); +} export { PROTECTED_MANAGED_IMAGE_AGENTS, @@ -123,8 +190,9 @@ export function withManagedImageLocalInferenceProfile( upstreamProvider: route.providerName, model, primaryModelRef, - routedBaseUrl: "https://inference.local/v1", - upstreamEndpointUrl: null, + routedBaseUrl: route.defaultBaseUrl, + upstreamEndpointUrl: + profile.agent === "langchain-deepagents-code" ? route.defaultBaseUrl : null, api: "openai-completions", }, } as ManagedStartupProfile; @@ -164,3 +232,246 @@ export function createManagedImageSandboxWithDiagnostics( return result; }; } + +export type ManagedImageOpenShellE2eInputs = { + agent: ShippedManagedImageAgent; + image: string; + sandbox: string; + gpu?: true; + localProvider?: ManagedImageLocalInferenceKind; + model?: string; + failureInjection?: "bootstrap-completion"; +}; + +export function managedImageLocalInferenceBaseUrl( + localProvider: ManagedImageLocalInferenceKind, + configuredValue = process.env.NEMOCLAW_E2E_LOCAL_INFERENCE_BASE_URL, +): string { + const route = resolveManagedImageLocalInferenceRoute(localProvider); + const configured = String(configuredValue ?? "").trim(); + const value = configured || route.defaultBaseUrl; + let parsed: URL; + try { + parsed = new URL(value); + } catch { + throw new Error("protected local inference base URL is invalid"); + } + if ( + parsed.protocol !== "http:" || + parsed.hostname !== "host.openshell.internal" || + !/^[1-9][0-9]{0,4}$/u.test(parsed.port) || + parsed.pathname.replace(/\/+$/u, "") !== "/v1" || + parsed.username || + parsed.password || + parsed.search || + parsed.hash + ) { + throw new Error("protected local inference must use http://host.openshell.internal:/v1"); + } + return value.replace(/\/+$/u, ""); +} + +function localInferenceBaseUrl(input: ManagedImageOpenShellE2eInputs): string { + if (!input.localProvider) throw new Error("local provider is required"); + return managedImageLocalInferenceBaseUrl(input.localProvider); +} + +export async function configureLocalInferenceRoute( + onboard: { + runOpenshell( + args: string[], + options?: Record, + ): ReturnType[0]>>; + }, + input: ManagedImageOpenShellE2eInputs, + env: NodeJS.ProcessEnv, + adapter: Parameters< + typeof ensureNativeLocalProvider + >[0]["adapter"] = createManagedProviderAdapter(onboard.runOpenshell), +): Promise { + if (!input.localProvider || !input.model) return; + const route = resolveManagedImageLocalInferenceRoute(input.localProvider); + const credential = String(env[route.credentialEnv] ?? "").trim(); + if (!credential || /[\0\r\n]/u.test(credential)) { + throw new Error(`${route.credentialEnv} is required for protected local inference`); + } + let authority: NativeLocalProviderAttachment | undefined; + return ensureNativeLocalProvider({ + binding: { + provider: route.providerName, + ...(input.agent === "langchain-deepagents-code" ? {} : { agent: input.agent }), + endpointUrl: localInferenceBaseUrl(input), + credentialEnv: NATIVE_LOCAL_CREDENTIAL_ENV, + authMode: + input.localProvider === "nim" || input.localProvider === "vllm" + ? "sentinel" + : "authenticated", + gatewayName: "nemoclaw", + sandboxName: input.sandbox, + }, + adapter, + credentialValue: credential, + policyCommand: async (args) => onboard.runOpenshell(args, { ignoreError: true, env }), + // This invocation owns a fresh private gateway. Never adopt an existing provider. + readAuthority: () => authority, + writeAuthority: (receipt) => { + authority = receipt; + }, + }); +} + +export function localInferenceProbe(input: ManagedImageOpenShellE2eInputs): [string, ...string[]] { + if (!input.model) throw new Error("local inference model is required"); + const payload = JSON.stringify({ + model: input.model, + messages: [{ role: "user", content: "Reply with exactly one word: PONG" }], + reasoning_effort: "none", + max_tokens: 32, + }); + const script = [ + "set -eu", + NATIVE_LOCAL_AUTH_HEADER_SCRIPT, + 'response="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-managed-image-inference.XXXXXX")"', + "trap 'rm -f \"$response\"' EXIT", + 'curl -fsS --max-time 180 "$1" -H "$AUTH_HEADER" -H \'Content-Type: application/json\' --data "$2" > "$response"', + "node - \"$response\" <<'NODE'", + 'const fs = require("node:fs");', + 'const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));', + "const choice = Array.isArray(body.choices) ? body.choices[0] : null;", + 'const text = choice && choice.message && typeof choice.message.content === "string"', + " ? choice.message.content", + ' : choice && typeof choice.text === "string" ? choice.text : "";', + 'if (!/pong/i.test(text)) throw new Error("local inference did not return PONG");', + "NODE", + ].join("\n"); + return [ + "/bin/sh", + "-eu", + "-c", + script, + "native-local-inference", + `${localInferenceBaseUrl(input)}/chat/completions`, + payload, + ]; +} + +export function localInferenceAgentCommand(input: ManagedImageOpenShellE2eInputs): string[] { + const prompt = "Reply with exactly one word: PONG"; + switch (input.agent) { + case "openclaw": + return [ + "openclaw", + "agent", + "--agent", + "main", + "--json", + "--thinking", + "off", + "--session-id", + `protected-${input.sandbox}`, + "-m", + prompt, + ]; + case "hermes": + return ["hermes", "-z", prompt]; + case "langchain-deepagents-code": + return ["dcode", "-n", prompt, "--json"]; + } +} + +export async function waitForLocalInferenceAgentPairing< + T extends { + readonly status: number | null; + readonly stderr: string | null; + readonly error?: Error; + }, +>( + agent: ShippedManagedImageAgent, + run: () => T, + sleep: (milliseconds: number) => Promise = (milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds)), +): Promise { + for (let attempt = 0; ; attempt += 1) { + const result = run(); + if ( + agent !== "openclaw" || + result.error || + result.status !== 1 || + attempt === 3 || + !/scope upgrade pending approval/iu.test(result.stderr ?? "") + ) + return result; + // The normal watcher approves allowlisted scopes asynchronously. + await sleep(5_000); + } +} + +export function assertLocalInferenceAgentResponse( + input: ManagedImageOpenShellE2eInputs, + stdout: string, + env: NodeJS.ProcessEnv = process.env, +): void { + let response: unknown = stdout; + if (input.agent === "openclaw") { + const envelope = JSON.parse(stdout); + const result = envelope?.result; + if ( + envelope?.status !== "ok" || + result?.meta?.aborted !== false || + result?.meta?.agentMeta?.provider !== "inference" || + result?.meta?.agentMeta?.model !== input.model + ) + throw new Error("OpenClaw did not complete through its configured local model"); + response = result.payloads?.map((payload: { text?: string }) => payload.text ?? "").join("\n"); + } else if (input.agent === "langchain-deepagents-code") { + const envelope = JSON.parse(stdout); + if (envelope?.data?.status !== "success" || envelope?.data?.exit_code !== 0) + throw new Error("LangChain Deep Agents Code did not complete its local inference turn"); + response = envelope.data.response; + } + // Hermes oneshot stdout contains only its final response. JSON agents expose + // assistant output separately; a prompt echoed in metadata cannot pass. + if (typeof response !== "string" || !/^PONG[.!]?$/iu.test(response.trim())) + throw new Error( + `${input.agent} local inference did not return PONG; response=${JSON.stringify(managedImageFailureDetail(response, env).slice(0, 512))}`, + ); +} + +export async function verifyManagedImageLocalInferenceRoute( + run: NonNullable[0]>, + sandboxName: string, + expected: NativeLocalProviderAttachment, +): Promise { + await verifyNativeLocalProviderAttachment({ + adapter: createManagedProviderAdapter(run), + sandboxName, + expected, + requireUnexpiredCredential: true, + }); +} + +export function managedImageHarnessContainerListArgs( + sandbox: string, + includeStopped: boolean, + runtimeVersion: ManagedImageHarnessOpenShellVersion, +): string[] { + return [ + "docker", + "ps", + includeStopped ? "-aq" : "-q", + "--no-trunc", + "--filter", + "label=openshell.ai/managed-by=openshell", + "--filter", + `label=openshell.ai/sandbox-name=${sandbox}`, + ...(runtimeVersion === "0.1.3" + ? ["--filter", "label=openshell.ai/isolation-role=sandbox"] + : []), + ]; +} + +export function managedImageSandboxListLimitFlag( + runtimeVersion: ManagedImageHarnessOpenShellVersion, +): string { + return runtimeVersion === "0.1.3" ? "--page-size" : "--limit"; +} diff --git a/scripts/checks/run-managed-image-openshell-e2e.ts b/scripts/checks/run-managed-image-openshell-e2e.ts index 509900d2214..1ed99b5d674 100644 --- a/scripts/checks/run-managed-image-openshell-e2e.ts +++ b/scripts/checks/run-managed-image-openshell-e2e.ts @@ -14,6 +14,7 @@ import { isOpenShellSandboxId, NEMOCLAW_CREATE_ATTEMPT_LABEL, parseOpenShellSandboxId, + parseStrictOpenShellSandboxListJson, resolveCreatedOpenShellSandboxId, } from "../../src/lib/adapters/openshell/sandbox-identity.ts"; import { createCliOpenShellSandboxCommandExecutor } from "../../src/lib/adapters/openshell/sandbox-command-cli.ts"; @@ -54,7 +55,18 @@ import { managedStartupE2eProfile, } from "./generate-managed-startup-profile-fixture.mts"; import { + managedImageHarnessContainerListArgs, + managedImageSandboxListLimitFlag, + requireManagedImageHarnessOpenShellVersionFromOutput, + configureLocalInferenceRoute, + assertLocalInferenceAgentResponse, + waitForLocalInferenceAgentPairing, + localInferenceAgentCommand, + localInferenceProbe, + verifyManagedImageLocalInferenceRoute, + type ManagedImageOpenShellE2eInputs, createManagedImageSandboxWithDiagnostics, + isManagedImageHarnessWorkload, isManagedImageLocalInferenceKind, managedImageFailureDetail, type ManagedImageLocalInferenceKind, @@ -64,8 +76,8 @@ import { // This executable owns one protected qualification transaction from sandbox // creation through exact cleanup. Keep its stateful orchestration and cleanup -// together so no cross-module return path can bypass rollback. Stateless policy -// and diagnostics remain in managed-image-protected-runtime-contract.ts. +// together so no cross-module return path can bypass rollback. Native inference +// preparation and diagnostics use managed-image-protected-runtime-contract.ts. const MANAGED_AGENTS = new Set(SHIPPED_MANAGED_IMAGE_AGENTS); const REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); @@ -95,15 +107,7 @@ function compactText(value = ""): string { return String(value).replace(/\s+/gu, " ").trim(); } -export type ManagedImageOpenShellE2eInputs = { - agent: ShippedManagedImageAgent; - image: string; - sandbox: string; - gpu?: true; - localProvider?: ManagedImageLocalInferenceKind; - model?: string; - failureInjection?: "bootstrap-completion"; -}; +export type { ManagedImageOpenShellE2eInputs } from "./managed-image-protected-runtime-contract.ts"; export type ManagedImageOpenShellRecoveryScope = { readonly owner: string; @@ -236,6 +240,12 @@ function confirmManagedSandboxAbsent( onboard: Pick, nonce: string, ): void { + const versionOutput = onboard.runCaptureOpenshell(["--version"], { + ignoreError: false, + timeout: 30_000, + maxBuffer: 1024 * 1024, + }); + const runtimeVersion = requireManagedImageHarnessOpenShellVersionFromOutput(versionOutput); const output = onboard.runCaptureOpenshell( [ "sandbox", @@ -246,14 +256,13 @@ function confirmManagedSandboxAbsent( `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${nonce}`, "--output", "json", - "--limit", + managedImageSandboxListLimitFlag(runtimeVersion), "2", ], { ignoreError: false, timeout: 30000, maxBuffer: 1024 * 1024 }, ); - const rows: unknown = JSON.parse(output); - if (!Array.isArray(rows) || rows.length !== 0) - throw new Error("managed recovery sandbox absence is unproven"); + const rows = parseStrictOpenShellSandboxListJson(output); + if (!rows || rows.length !== 0) throw new Error("managed recovery sandbox absence is unproven"); } export function createManagedImageSandboxWithRecovery( @@ -498,12 +507,13 @@ export async function cleanupInterruptedManagedImageOpenShellE2e( )); if (removeGateway.status !== 0 && !gatewayAlreadyAbsent) throw new Error("managed recovery gateway removal failed"); - const containers = exactHarnessContainerIds(input, scope.networkName, process.env); + const gatewayStateDir = path.join(scope.stateDir, "gateway-state"); + const containers = exactHarnessContainerIds(input, gatewayStateDir, process.env); if (containers.exactIds.length > 1) throw new Error("managed recovery container identity is ambiguous"); if (containers.exactIds.length === 1) { commandResult(["docker", "rm", "--force", containers.exactIds[0]!], process.env, 15000); - if (exactHarnessContainerIds(input, scope.networkName, process.env).exactIds.length !== 0) + if (exactHarnessContainerIds(input, gatewayStateDir, process.env).exactIds.length !== 0) throw new Error("managed recovery container cleanup is unproven"); } const provider = createDockerRuntimeProviderBundle(); @@ -519,8 +529,7 @@ export async function cleanupInterruptedManagedImageOpenShellE2e( onboard, runtimeProvider: provider, roots, - scope: null, - committed: true, + preparationAttempted: true, }); if (failures.length) throw new Error("managed recovery volume cleanup is unproven"); commandResult(["docker", "network", "rm", scope.networkName], process.env, 15000); @@ -623,7 +632,7 @@ type ProtectedManagedStateVolumeMount = { type ProtectedManagedStateVolumeCleanupResult = | { readonly status: "not-applicable" | "absent" | "removed" } | { - readonly status: "not-owned" | "failed"; + readonly status: "not-owned" | "failed" | "retained"; readonly detail: string; readonly volumeName: string; }; @@ -722,25 +731,25 @@ export function resolveManagedImageOnboardModule(onboardImport: unknown): Onboar return candidate as OnboardModule; } -function cleanupProtectedManagedStateVolumes(input: { +export function cleanupProtectedManagedStateVolumes(input: { readonly onboard: OnboardModule | null; readonly runtimeProvider: RuntimeProviderBundle | null; readonly roots: readonly ProtectedManagedStateRoot[]; - readonly scope: ProtectedManagedStateVolumeScope | null; - readonly committed: boolean; + readonly preparationAttempted: boolean; }): string[] { try { let results: readonly ProtectedManagedStateVolumeCleanupResult[] = []; - if (input.committed && input.onboard && input.runtimeProvider) { + // This is the fixture owner's explicit teardown after sandbox/container + // cleanup, including injected bootstrap failures. Automatic create rollback + // preserves named volumes and cannot establish an empty fixture inventory. + if (input.preparationAttempted && input.onboard && input.runtimeProvider) { results = input.onboard.managedWorkloadOnboard.removeManagedStateVolumes( { roots: input.roots }, { runtimeProvider: input.runtimeProvider }, ); - } else if (!input.committed) { - results = input.scope?.cleanupIncompleteCreate() ?? []; } return results.flatMap((result) => - result.status === "not-owned" || result.status === "failed" + result.status === "not-owned" || result.status === "failed" || result.status === "retained" ? [`managed state volume ${result.volumeName} cleanup ${result.status}: ${result.detail}`] : [], ); @@ -857,9 +866,13 @@ function commandDetail(result: ManagedImageCommandResult): string { .slice(-8_000); } -function printExactContainerFailureState(containerId: string | null, env: NodeJS.ProcessEnv): void { +export function printExactContainerFailureState( + containerId: string | null, + env: NodeJS.ProcessEnv, + runCommand: ManagedImageCommandRunner = commandResult, +): void { if (!containerId || !/^[a-f0-9]{64}$/u.test(containerId)) return; - const state = commandResult( + const state = runCommand( [ "docker", "container", @@ -874,7 +887,7 @@ function printExactContainerFailureState(containerId: string | null, env: NodeJS console.error( `Managed-image exact container state: ${managedImageFailureDetail(commandDetail(state))}`, ); - const processState = commandResult( + const processState = runCommand( [ "docker", "exec", @@ -891,6 +904,38 @@ function printExactContainerFailureState(containerId: string | null, env: NodeJS console.error( `Managed-image exact container processes: ${managedImageFailureDetail(commandDetail(processState))}`, ); + // The canonical process can exit while OpenShell PID 1 stays alive. Read only + // its fixed diagnostic logs as the sandbox owner before fixture cleanup. + const startupLogs = runCommand( + [ + "docker", + "exec", + "--user", + "sandbox", + containerId, + "/usr/local/bin/node", + "-e", + `const fs = require("node:fs"); +for (const file of ["/tmp/nemoclaw-start.log", "/tmp/gateway.log", "/tmp/dashboard.log"]) { + let fd; + process.stdout.write(file + "\\n"); + try { + fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); + const stat = fs.fstatSync(fd); + if (!stat.isFile() || stat.size > 262144) throw new Error("unsafe or oversized log"); + const bytes = Buffer.alloc(Math.min(stat.size, 262144)); + const count = fs.readSync(fd, bytes, 0, bytes.length, 0); + process.stdout.write(bytes.subarray(0, count)); + } catch { process.stdout.write("diagnostic log unavailable\\n"); } + finally { if (fd !== undefined) fs.closeSync(fd); } +}`, + ], + env, + 5_000, + ); + console.error( + `Managed-image startup logs (bounded): ${managedImageFailureDetail(`${startupLogs.error?.message ?? ""}\n${startupLogs.stdout ?? ""}\n${startupLogs.stderr ?? ""}`, env)}`, + ); } function isDockerNotFound(result: ManagedImageCommandResult): boolean { @@ -1235,118 +1280,11 @@ async function waitForCommittedSandboxProbe( ); } -export function managedImageLocalInferenceBaseUrl( - localProvider: ManagedImageLocalInferenceKind, - configuredValue = process.env.NEMOCLAW_E2E_LOCAL_INFERENCE_BASE_URL, -): string { - const route = resolveManagedImageLocalInferenceRoute(localProvider); - const configured = String(configuredValue ?? "").trim(); - const value = configured || route.defaultBaseUrl; - let parsed: URL; - try { - parsed = new URL(value); - } catch { - throw new Error("protected local inference base URL is invalid"); - } - if ( - parsed.protocol !== "http:" || - parsed.hostname !== "host.openshell.internal" || - !/^[1-9][0-9]{0,4}$/u.test(parsed.port) || - parsed.pathname.replace(/\/+$/u, "") !== "/v1" || - parsed.username || - parsed.password || - parsed.search || - parsed.hash - ) { - throw new Error("protected local inference must use http://host.openshell.internal:/v1"); - } - return value.replace(/\/+$/u, ""); -} - -function localInferenceBaseUrl(input: Inputs): string { - if (!input.localProvider) throw new Error("local provider is required"); - return managedImageLocalInferenceBaseUrl(input.localProvider); -} - -function configureLocalInferenceRoute( +async function assertProtectedLocalInference( onboard: OnboardModule, input: Inputs, env: NodeJS.ProcessEnv, -): void { - if (!input.localProvider || !input.model) return; - const route = resolveManagedImageLocalInferenceRoute(input.localProvider); - const credential = String(env[route.credentialEnv] ?? "").trim(); - if (!credential || /[\0\r\n]/u.test(credential)) { - throw new Error(`${route.credentialEnv} is required for protected local inference`); - } - const commandEnv = { ...env, [route.credentialEnv]: credential }; - const create = onboard.runOpenshell( - [ - "provider", - "create", - "--name", - route.providerName, - "--type", - "openai", - "--credential", - route.credentialEnv, - "--config", - `OPENAI_BASE_URL=${localInferenceBaseUrl(input)}`, - ], - { ignoreError: true, env: commandEnv, stdio: ["ignore", "pipe", "pipe"] }, - ); - if (create.status !== 0) { - throw new Error(`protected local inference provider creation failed: ${commandDetail(create)}`); - } - const setRoute = onboard.runOpenshell( - [ - "inference", - "set", - "--no-verify", - "--provider", - route.providerName, - "--model", - input.model, - "--timeout", - "120", - ], - { ignoreError: true, env: commandEnv, stdio: ["ignore", "pipe", "pipe"] }, - ); - if (setRoute.status !== 0) { - throw new Error(`protected local inference route failed: ${commandDetail(setRoute)}`); - } -} - -function localInferenceProbe(input: Inputs): string { - if (!input.model) throw new Error("local inference model is required"); - const payload = JSON.stringify({ - model: input.model, - messages: [{ role: "user", content: "Reply with exactly one word: PONG" }], - reasoning_effort: "none", - max_tokens: 32, - }); - return [ - "set -eu", - "response=/tmp/nemoclaw-managed-image-inference.json", - `curl -fsS --max-time 180 https://inference.local/v1/chat/completions -H 'Content-Type: application/json' --data ${JSON.stringify(payload)} > "$response"`, - "node - \"$response\" <<'NODE'", - 'const fs = require("node:fs");', - 'const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));', - "const choice = Array.isArray(body.choices) ? body.choices[0] : null;", - 'const text = choice && choice.message && typeof choice.message.content === "string"', - " ? choice.message.content", - ' : choice && typeof choice.text === "string" ? choice.text : "";', - 'if (!/pong/i.test(text)) throw new Error("local inference did not return PONG");', - "NODE", - 'rm -f "$response"', - ].join("\n"); -} - -function assertProtectedLocalInference( - onboard: OnboardModule, - input: Inputs, - env: NodeJS.ProcessEnv, -): void { +): Promise { const result = commandResult( onboard.openshellArgv([ "sandbox", @@ -1354,44 +1292,57 @@ function assertProtectedLocalInference( "--name", input.sandbox, "--", - "/bin/sh", - "-eu", - "-c", - localInferenceProbe(input), + ...localInferenceProbe(input), ]), env, 210_000, ); if (result.status !== 0) { - throw new Error(`sandbox inference.local completion failed: ${commandDetail(result)}`); + throw new Error(`sandbox native local completion failed: ${commandDetail(result)}`); } + // The fresh sandbox has one verified provider attachment. Use the generated + // agent configuration without endpoint, credential, or model overrides. + const agentResult = await waitForLocalInferenceAgentPairing(input.agent, () => + commandResult( + onboard.openshellArgv([ + "sandbox", + "exec", + "--name", + input.sandbox, + "--", + ...localInferenceAgentCommand(input), + ]), + env, + 210_000, + ), + ); + if (agentResult.status !== 0) { + throw new Error(`sandbox native agent completion failed: ${commandDetail(agentResult)}`); + } + assertLocalInferenceAgentResponse(input, String(agentResult.stdout ?? ""), env); } -export function managedImageHarnessContainerListArgs( - sandbox: string, - includeStopped: boolean, -): string[] { - return [ - "docker", - "ps", - includeStopped ? "-aq" : "-q", - "--no-trunc", - "--filter", - "label=openshell.ai/managed-by=openshell", - "--filter", - `label=openshell.ai/sandbox-name=${sandbox}`, - ]; -} +export { managedImageHarnessContainerListArgs } from "./managed-image-protected-runtime-contract.ts"; function exactHarnessContainerIds( input: Inputs, - networkName: string, + stateDir: string, env: NodeJS.ProcessEnv, includeStopped = true, runCommand: ManagedImageCommandRunner = commandResult, ): { candidateCount: number; exactIds: string[] } { const expectedContentId = resolveLocalImageContentId(input.image, env, runCommand); - const list = runCommand(managedImageHarnessContainerListArgs(input.sandbox, includeStopped), env); + const version = runCommand(["openshell", "--version"], env); + if (version.status !== 0) { + throw new Error(`could not resolve the protected OpenShell version: ${commandDetail(version)}`); + } + const runtimeVersion = requireManagedImageHarnessOpenShellVersionFromOutput( + String(version.stdout ?? ""), + ); + const list = runCommand( + managedImageHarnessContainerListArgs(input.sandbox, includeStopped, runtimeVersion), + env, + ); if (list.status !== 0) { throw new Error(`could not resolve the OpenShell sandbox container: ${commandDetail(list)}`); } @@ -1407,14 +1358,17 @@ function exactHarnessContainerIds( const records = JSON.parse(String(inspect.stdout ?? "")) as Array<{ Config?: { Labels?: Record }; Image?: string; - NetworkSettings?: { Networks?: Record }; + HostConfig?: { NetworkMode?: string }; }>; const record = records.length === 1 ? records[0] : undefined; if ( - record?.Config?.Labels?.["openshell.ai/managed-by"] === "openshell" && - record.Config.Labels["openshell.ai/sandbox-name"] === input.sandbox && - record.Image === expectedContentId && - Object.hasOwn(record.NetworkSettings?.Networks ?? {}, networkName) + isManagedImageHarnessWorkload(record, { + sandbox: input.sandbox, + stateDir, + imageContentId: expectedContentId, + runtimeVersion, + networkName: env.OPENSHELL_DOCKER_NETWORK_NAME, + }) ) { exactIds.push(candidate); } @@ -1427,14 +1381,16 @@ function exactHarnessContainerIds( export function assertExactSandboxImage( input: Inputs, - networkName: string, + stateDir: string, env: NodeJS.ProcessEnv, runCommand: ManagedImageCommandRunner = commandResult, ): string { // A managed-bootstrap transaction keeps its stopped rollback backup until // commit. Qualify the one running replacement here; rollback cleanup below // continues to inspect every stopped and running container. - const resolved = exactHarnessContainerIds(input, networkName, env, false, runCommand); + // OpenShell 0.1.2 separates supervisor and workload containers and puts the + // workload on network=none. Bind ownership to our private gateway namespace. + const resolved = exactHarnessContainerIds(input, stateDir, env, false, runCommand); if (resolved.candidateCount !== 1 || resolved.exactIds.length !== 1) { throw new Error( `OpenShell did not launch exactly one running harness-owned PR image container: found ${resolved.candidateCount} running labeled and ${resolved.exactIds.length} running exact`, @@ -1568,7 +1524,7 @@ async function run { }); describe("operation-scoped container engine command", () => { + it("forwards bounded private lifecycle pipes without changing default command limits", () => { + const capture = vi.fn(() => ({ status: 0, stdout: "ok", stderr: "" })); + const guard = vi.fn(); + const engine = createContainerEngineCommand({ + operation: "sandbox-lifecycle", + engineId: "podman", + displayName: "Podman", + authorityId: "test:podman-socket", + executable: "podman", + capture, + guard, + }); + const input = Buffer.alloc(1024 * 1024 + 1); + const bounds = { maxInputBytes: input.length, maxOutputBytes: 2048 }; + engine.capture(["container", "exec", "owned-id", "runtime"], 1234, input, bounds); + expect(capture).toHaveBeenCalledExactlyOnceWith( + "podman", + ["container", "exec", "owned-id", "runtime"], + 1234, + input, + undefined, + 2048, + ); + expect(guard.mock.calls).toEqual([["before"], ["after"]]); + expect(() => engine.capture(["container", "exec", "owned-id", "runtime"], 1234, input)).toThrow( + /byte bound/u, + ); + expect(() => engine.capture(["info"], 1234, input, bounds)).toThrow( + /bounded sandbox lifecycle/u, + ); + expect(() => + engine.capture(["container", "exec"], 1234, input, { + ...bounds, + maxInputBytes: input.length - 1, + }), + ).toThrow(/byte bound/u); + capture.mockReturnValue({ status: 0, stdout: "private".repeat(1000), stderr: "" }); + expect(() => engine.capture(["container", "exec"], 1234, input, bounds)).toThrow( + "Private container output exceeds its byte bound.", + ); + }); + + it.each([0, -1, 1.5, NaN, Infinity, 48 * 1024 * 1024 + 1])( + "rejects the invalid private output bound %s before engine access", + (maxOutputBytes) => { + const capture = vi.fn(); + const engine = createContainerEngineCommand({ + operation: "sandbox-lifecycle", + engineId: "podman", + displayName: "Podman", + authorityId: "test:podman-socket", + executable: "podman", + capture, + }); + const input = Buffer.alloc(1024 * 1024 + 1); + expect(() => + engine.capture(["container", "exec"], 1234, input, { + maxInputBytes: input.length, + maxOutputBytes, + }), + ).toThrow(/bounded sandbox lifecycle/u); + expect(capture).not.toHaveBeenCalled(); + }, + ); + + it("applies the selected private output bound to the actual child process", () => { + const spawn = vi.mocked(spawnSync).mockReturnValueOnce({ + pid: 1, + output: [null, "ok", ""], + stdout: "ok", + stderr: "", + status: 0, + signal: null, + }); + const engine = createContainerEngineCommand({ + operation: "sandbox-lifecycle", + engineId: "podman", + displayName: "Podman", + authorityId: "test:podman-socket", + executable: "podman", + }); + engine.capture(["container", "exec", "owned-id", "runtime"], 1234, undefined, { + maxInputBytes: 1, + maxOutputBytes: 2048, + }); + expect(spawn).toHaveBeenLastCalledWith( + "podman", + ["container", "exec", "owned-id", "runtime"], + expect.objectContaining({ maxBuffer: 2048, timeout: 1234, shell: false }), + ); + }); + + it.each([ + "host-doctor", + "host-local-inference", + "workload-cleanup", + "gateway-inspection", + "external-image-preparation", + ] as const)("does not enable private lifecycle transport for %s", (operation) => { + const capture = vi.fn(); + const engine = createContainerEngineCommand({ + operation, + engineId: "podman", + displayName: "Podman", + authorityId: "test:podman-socket", + executable: "podman", + capture, + }); + expect(() => + engine.capture(["container", "exec"], 1000, undefined, { + maxInputBytes: 1, + maxOutputBytes: 2048, + }), + ).toThrow(/bounded sandbox lifecycle/u); + expect(capture).not.toHaveBeenCalled(); + }); it("hard-kills provider commands that exceed their deadline", () => { const engine = createContainerEngineCommand({ operation: "sandbox-lifecycle", diff --git a/src/lib/adapters/container-engine.ts b/src/lib/adapters/container-engine.ts index ee702099fac..4d2599315a2 100644 --- a/src/lib/adapters/container-engine.ts +++ b/src/lib/adapters/container-engine.ts @@ -25,8 +25,14 @@ export type ContainerEngineCommandCapture = ( timeoutMs: number, input?: Buffer, environment?: Readonly>, + maxOutputBytes?: number, ) => ContainerEngineCommandResult; +export interface ContainerEnginePrivateTransport { + readonly maxInputBytes: number; + readonly maxOutputBytes: number; +} + /** * Immutable command boundary injected into one provider operation. The * executable and endpoint prefix cannot change after construction, and no @@ -47,6 +53,7 @@ export interface ContainerEngine { args: readonly string[], timeoutMs?: number, input?: Buffer, + privateTransport?: ContainerEnginePrivateTransport, ) => ContainerEngineCommandResult; /** Available only to a host-local-inference engine; values remain operation-scoped. */ readonly captureWithEnvironment?: ( @@ -82,6 +89,9 @@ const MAX_ARGUMENTS = 512; const MAX_ARGUMENT_BYTES = 16 * 1024; const MAX_OUTPUT_BYTES = 1024 * 1024; const MAX_INPUT_BYTES = 1024 * 1024; +// Encoded bounded startup snapshots can exceed the normal command pipe size. +// This opt-in transport does not change snapshot, file or default command bounds. +const MAX_PRIVATE_TRANSPORT_BYTES = 48 * 1024 * 1024; const MAX_ENVIRONMENT_ENTRIES = 32; const MAX_ENVIRONMENT_BYTES = 64 * 1024; const ENGINE_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}$/u; @@ -292,13 +302,14 @@ function defaultCapture( timeoutMs: number, input?: Buffer, environment?: Readonly>, + maxOutputBytes = MAX_OUTPUT_BYTES, ): ContainerEngineCommandResult { const result = spawnSync(executable, [...args], { cwd: process.cwd(), env: environment ?? containerEngineCommandEnvironment(), encoding: "utf8", killSignal: "SIGKILL", - maxBuffer: MAX_OUTPUT_BYTES, + maxBuffer: maxOutputBytes, shell: false, stdio: [input ? "pipe" : "ignore", "pipe", "pipe"], ...(input ? { input } : {}), @@ -373,15 +384,41 @@ export function createContainerEngineCommand( endpoint: boolean, input?: Buffer, environment?: Readonly>, + privateTransport?: ContainerEnginePrivateTransport, ) => { const normalized = normalizedArguments(args, "Container engine command arguments"); const commandArgs = endpoint ? [...endpointArgs, ...normalized] : [...normalized]; - if (input !== undefined && (!Buffer.isBuffer(input) || input.length > MAX_INPUT_BYTES)) { + if ( + privateTransport !== undefined && + (options.operation !== "sandbox-lifecycle" || + !endpoint || + normalized[0] !== "container" || + normalized[1] !== "exec" || + ![privateTransport.maxInputBytes, privateTransport.maxOutputBytes].every( + (bound) => + Number.isSafeInteger(bound) && bound > 0 && bound <= MAX_PRIVATE_TRANSPORT_BYTES, + )) + ) + throw new Error("Private container transport requires bounded sandbox lifecycle execution."); + if ( + input !== undefined && + (!Buffer.isBuffer(input) || + input.length > (privateTransport?.maxInputBytes ?? MAX_INPUT_BYTES)) + ) { throw new Error("Container engine command input is invalid or exceeds its byte bound."); } const boundedTimeout = positiveTimeout(timeoutMs); - return invokeGuarded(options.guard, () => { + const result = invokeGuarded(options.guard, () => { const selectedEnvironment = environment ?? commandEnvironment; + if (privateTransport) + return capture( + executable, + commandArgs, + boundedTimeout, + input === undefined ? undefined : Buffer.from(input), + selectedEnvironment, + privateTransport.maxOutputBytes, + ); if (selectedEnvironment !== undefined) { return capture( executable, @@ -395,6 +432,13 @@ export function createContainerEngineCommand( ? capture(executable, commandArgs, boundedTimeout) : capture(executable, commandArgs, boundedTimeout, Buffer.from(input)); }); + if ( + privateTransport && + (Buffer.byteLength(result.stdout) > privateTransport.maxOutputBytes || + Buffer.byteLength(result.stderr) > privateTransport.maxOutputBytes) + ) + throw new Error("Private container output exceeds its byte bound."); + return result; }; return Object.freeze({ @@ -403,8 +447,12 @@ export function createContainerEngineCommand( displayName, authorityId: options.authorityId, endpointAuthorityId, - capture: (args: readonly string[], timeoutMs = DEFAULT_TIMEOUT_MS, input?: Buffer) => - run(args, timeoutMs, true, input), + capture: ( + args: readonly string[], + timeoutMs = DEFAULT_TIMEOUT_MS, + input?: Buffer, + privateTransport?: ContainerEnginePrivateTransport, + ) => run(args, timeoutMs, true, input, undefined, privateTransport), ...(options.operation === "host-local-inference" ? { captureWithEnvironment: ( diff --git a/src/lib/adapters/openshell/provider-adapter-cli.ts b/src/lib/adapters/openshell/provider-adapter-cli.ts index 1010de00dfc..72aa7707b6c 100644 --- a/src/lib/adapters/openshell/provider-adapter-cli.ts +++ b/src/lib/adapters/openshell/provider-adapter-cli.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "../../onboard/openshell-version"; + import fs from "node:fs"; import { isDeepStrictEqual } from "node:util"; import { NAME_MAX_LENGTH, NAME_VALID_PATTERN } from "../../name-validation"; @@ -746,6 +748,9 @@ export function createCliOpenShellProviderAdapter( } if (!request.includeCredentialExpirations) return success(metadata); + let pageToken = ""; + const seenPageTokens = new Set(); + const projectedRuntime = String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3"; for ( let offset = 0; offset < 10 * PROVIDER_INVENTORY_LIMIT; @@ -755,10 +760,9 @@ export function createCliOpenShellProviderAdapter( [ "provider", "list", - "--limit", - String(PROVIDER_INVENTORY_LIMIT), - "--offset", - String(offset), + ...(projectedRuntime + ? ["--page-size", String(PROVIDER_INVENTORY_LIMIT), "--page-token", pageToken] + : ["--limit", String(PROVIDER_INVENTORY_LIMIT), "--offset", String(offset)]), "--output", "json", ], @@ -774,7 +778,18 @@ export function createCliOpenShellProviderAdapter( commandStdout(inventory), request.providerName, ); - if (credentialState === undefined) continue; + if (credentialState && "nextPageToken" in credentialState) { + if (!projectedRuntime || seenPageTokens.has(credentialState.nextPageToken)) { + return failure({ + kind: "schema", + message: "OpenShell returned invalid provider inventory pagination.", + }); + } + pageToken = credentialState.nextPageToken; + seenPageTokens.add(pageToken); + continue; + } + if (credentialState === undefined && !projectedRuntime) continue; return credentialState ? success({ ...metadata, diff --git a/src/lib/adapters/openshell/provider-inventory-projected.test.ts b/src/lib/adapters/openshell/provider-inventory-projected.test.ts new file mode 100644 index 00000000000..b907573a1af --- /dev/null +++ b/src/lib/adapters/openshell/provider-inventory-projected.test.ts @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, it, vi } from "vitest"; +import { createCliOpenShellProviderAdapter, type RunProviderCommand } from "./provider-adapter-cli"; + +vi.mock("../../onboard/openshell-version", async (original) => ({ + ...(await original()), + SUPPORTED_OPENSHELL_FALLBACK_VERSION: "0.1.3", +})); + +it.each([false, true])( + "reads projected expiry pages and rejects a repeated token: %s", + async (repeat) => { + const first = { status: 0, stdout: JSON.stringify({ providers: [], next_page_token: "next" }) }; + const run = vi + .fn() + .mockReturnValueOnce({ + status: 0, + stdout: + "Name: local-provider\nType: local-profile\nCredential keys: API_KEY\nConfig keys: ", + }) + .mockReturnValueOnce(first) + .mockReturnValueOnce( + repeat + ? first + : { + status: 0, + stdout: JSON.stringify({ + providers: [ + { + name: "local-provider", + credential_keys: ["API_KEY"], + credential_expires_at_ms: { API_KEY: 123 }, + }, + ], + next_page_token: "", + }), + }, + ); + const result = await createCliOpenShellProviderAdapter({ run }).getProvider({ + target: { kind: "named", gatewayName: "selected" }, + providerName: "local-provider", + includeCredentialExpirations: true, + }); + expect(result).toMatchObject( + repeat ? { ok: false } : { ok: true, value: { credentialExpiresAtMs: { API_KEY: 123 } } }, + ); + expect(run.mock.calls[2]?.[0]).toEqual([ + "provider", + "list", + "-g", + "selected", + "--page-size", + "1000", + "--page-token", + "next", + "--output", + "json", + ]); + expect(run).toHaveBeenCalledTimes(3); + }, +); diff --git a/src/lib/adapters/openshell/provider-metadata-cli.ts b/src/lib/adapters/openshell/provider-metadata-cli.ts index 0d43557db6f..0d209c89d27 100644 --- a/src/lib/adapters/openshell/provider-metadata-cli.ts +++ b/src/lib/adapters/openshell/provider-metadata-cli.ts @@ -156,7 +156,7 @@ function parseProviderKeyArray(value: unknown): readonly string[] | null { export function parseCliOpenShellProviderCredentialState( output: string, providerName: string, -): CliOpenShellProviderCredentialState | null | undefined { +): CliOpenShellProviderCredentialState | { nextPageToken: string } | null | undefined { if ( !isValidCliOpenShellProviderIdentifier(providerName) || Buffer.byteLength(output, "utf8") > MAX_PROVIDER_INVENTORY_OUTPUT_BYTES @@ -170,6 +170,19 @@ export function parseCliOpenShellProviderCredentialState( } catch { return null; } + let nextPageToken: string | undefined; + if (!Array.isArray(parsed)) { + if (!parsed || typeof parsed !== "object") return null; + const page = parsed as Record; + if ( + typeof page.next_page_token !== "string" || + page.next_page_token.length > 4096 || + page.next_page_token.includes("\0") + ) + return null; + nextPageToken = page.next_page_token; + parsed = page.providers; + } if (!Array.isArray(parsed) || parsed.length > MAX_PROVIDER_INVENTORY_ENTRIES) return null; const matchingProviders: Record[] = []; @@ -181,8 +194,13 @@ export function parseCliOpenShellProviderCredentialState( } if (record.name === providerName) matchingProviders.push(record); } - // A full page without the target permits the adapter to request the next page. - if (matchingProviders.length === 0 && parsed.length === MAX_PROVIDER_INVENTORY_ENTRIES) { + if (matchingProviders.length === 0 && nextPageToken) return { nextPageToken }; + // A full legacy page without the target permits the adapter to request the next page. + if ( + matchingProviders.length === 0 && + nextPageToken === undefined && + parsed.length === MAX_PROVIDER_INVENTORY_ENTRIES + ) { return undefined; } if (matchingProviders.length !== 1) return null; diff --git a/src/lib/adapters/openshell/provider-policy-projected.test.ts b/src/lib/adapters/openshell/provider-policy-projected.test.ts new file mode 100644 index 00000000000..b5e728c8d20 --- /dev/null +++ b/src/lib/adapters/openshell/provider-policy-projected.test.ts @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; +import { initializeNativeProviderPolicy, requireNativeProviderPolicy } from "./provider-policy"; + +vi.mock("../../onboard/openshell-version", async (original) => ({ + ...(await original()), + SUPPORTED_OPENSHELL_FALLBACK_VERSION: "0.1.3", +})); + +describe.each([requireNativeProviderPolicy, initializeNativeProviderPolicy])( + "rejects invalid policy evidence without changing the projected gateway", + (check) => { + it.each([ + { status: 1, stdout: "{}" }, + { status: null, stdout: "{}" }, + { status: 0, stdout: "not-json" }, + { status: 0, stdout: JSON.stringify({ revisions: [], next_page_token: "more" }) }, + { + status: 0, + stdout: JSON.stringify({ revisions: [], next_page_token: "" }), + error: new Error("lost transport"), + }, + ])("rejects invalid response %j", async (response) => { + const run = vi.fn(async () => response); + await expect(check("selected", run)).rejects.toThrow(); + expect(run).toHaveBeenCalledExactlyOnceWith([ + "policy", + "list", + "-g", + "selected", + "--global", + "--page-size", + "1", + "--output", + "json", + ]); + }); + }, +); + +it.each(["loaded", "pending", "failed", "superseded"])( + "accepts only a superseded global override in the projected runtime: %s", + async (status) => { + const run = vi.fn(async (args: string[]) => ({ + status: 0, + stdout: JSON.stringify( + args[1] === "list" + ? { revisions: [{ version: 1 }], next_page_token: "" } + : { scope: "global", status }, + ), + })); + const result = requireNativeProviderPolicy("selected", run); + const observation = await result.then( + () => null, + (error: Error) => error.message, + ); + expect(observation).toEqual( + status === "superseded" ? null : expect.stringMatching(/global policy override/), + ); + expect(run.mock.calls.map(([args]) => args[1])).toEqual(["list", "get"]); + }, +); diff --git a/src/lib/adapters/openshell/provider-policy.ts b/src/lib/adapters/openshell/provider-policy.ts index b51e9583f02..b46305db055 100644 --- a/src/lib/adapters/openshell/provider-policy.ts +++ b/src/lib/adapters/openshell/provider-policy.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "../../onboard/openshell-version"; import { isValidName } from "../../../../nemoclaw/dist/shared/sandbox-name.cjs"; import { assertNoOpenShellGatewayEndpointOverride, @@ -56,15 +57,33 @@ function document(result: PolicyCommandResult): Record { } async function requireSandboxPolicyAuthority(run: PolicyCommand): Promise { - const result = await run(["policy", "list", "--global", "--limit", "1"]); - requireSuccessfulCommand(result); - // The pinned server returns NotFound for get when no revision exists. - // Only a successful history read proves absence without treating errors as permission. - if ( - !String(result.stdout ?? "").trim() && - String(result.stderr ?? "").trim() === "No global policy history found" - ) - return; + if (String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3") { + const history = document( + await run(["policy", "list", "--global", "--page-size", "1", "--output", "json"]), + ); + if ( + !Array.isArray(history.revisions) || + history.revisions.length > 1 || + typeof history.next_page_token !== "string" + ) { + throw new Error("OpenShell returned invalid global policy history."); + } + if (history.revisions.length === 0) { + if (history.next_page_token !== "") + throw new Error("OpenShell returned incomplete global policy history."); + return; + } + } else { + const result = await run(["policy", "list", "--global", "--limit", "1"]); + requireSuccessfulCommand(result); + // The pinned server returns NotFound for get when no revision exists. + // Only a successful history read proves absence without treating errors as permission. + if ( + !String(result.stdout ?? "").trim() && + String(result.stderr ?? "").trim() === "No global policy history found" + ) + return; + } const policy = document(await run(["policy", "get", "--global", "--output", "json"])); if (policy.scope === "global" && policy.status === "superseded") return; throw new Error( @@ -78,6 +97,12 @@ export async function requireNativeProviderPolicy( command?: PolicyCommand, ): Promise { const run = commandForGateway(gatewayName, command); + // The trusted controller projects this pin with the reviewed runtime. + // A mismatched server must still pass the strict policy response checks. + if (String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3") { + await requireSandboxPolicyAuthority(run); + return; + } const state = document(await run(["settings", "get", "--global", "--json"])); const settings = state.settings; if ( @@ -106,6 +131,12 @@ export async function initializeNativeProviderPolicy( command?: PolicyCommand, ): Promise<"disabled" | void> { const run = commandForGateway(gatewayName, command); + // The trusted controller projects this pin with the reviewed runtime. + // A mismatched server must still pass the strict policy response checks. + if (String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3") { + await requireSandboxPolicyAuthority(run); + return; + } await requireSandboxPolicyAuthority(run); await run([ "settings", diff --git a/src/lib/adapters/openshell/sandbox-identity.ts b/src/lib/adapters/openshell/sandbox-identity.ts index fefd427a0fa..f56138199d9 100644 --- a/src/lib/adapters/openshell/sandbox-identity.ts +++ b/src/lib/adapters/openshell/sandbox-identity.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "../../onboard/openshell-version"; + import { performance } from "node:perf_hooks"; import { fingerprintOpenShellSandboxId, @@ -65,7 +67,11 @@ function parseOpenShellSandboxListJson(output: string): readonly unknown[] | nul } catch { return null; } - return Array.isArray(rows) ? rows : null; + if (Array.isArray(rows)) return rows; + if (!rows || typeof rows !== "object") return null; + const page = rows as Record; + // A partial inventory cannot prove absence or unique identity. + return page.next_page_token === "" && Array.isArray(page.sandboxes) ? page.sandboxes : null; } export type OpenShellSandboxIdObservation = @@ -236,7 +242,7 @@ export function observeCreatedOpenShellSandboxId( `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${input.createAttemptNonce}`, "--output", "json", - "--limit", + String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3" ? "--page-size" : "--limit", "2", ], { diff --git a/src/lib/adapters/openshell/sandbox-lifecycle-sdk.test.ts b/src/lib/adapters/openshell/sandbox-lifecycle-sdk.test.ts index 66082768c09..3e9f5bbd422 100644 --- a/src/lib/adapters/openshell/sandbox-lifecycle-sdk.test.ts +++ b/src/lib/adapters/openshell/sandbox-lifecycle-sdk.test.ts @@ -51,6 +51,24 @@ describe("OpenShell SDK sandbox lifecycle", () => { expect(get).toHaveBeenCalledWith("alpha", { signal: expect.any(AbortSignal) }); }); + it.each([ + { action: "start" as const, expectedTimeoutMs: 135_000, settledPhase: "ready" }, + { action: "stop" as const, expectedTimeoutMs: 75_000, settledPhase: "stopped" }, + ])("uses the $action default timeout", async ({ action, expectedTimeoutMs, settledPhase }) => { + const test = harness(); + test.get.mockResolvedValue({ id: sandboxId, phase: settledPhase }); + const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); + + try { + await expect(test.lifecycle[`${action}Sandbox`](request)).resolves.toEqual({ + kind: "accepted", + }); + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expectedTimeoutMs); + } finally { + setTimeoutSpy.mockRestore(); + } + }); + it("rejects an invalid name before it connects", async () => { const { connect, lifecycle } = harness(); @@ -79,21 +97,21 @@ describe("OpenShell SDK sandbox lifecycle", () => { expect(test.waitReady).not.toHaveBeenCalled(); }); - it("accepts Ready after twenty initial Error observations", async () => { + it("accepts Ready after forty initial Error observations", async () => { const test = harness(); - const phases = ["stopped", ...Array(20).fill("Error"), "Ready"]; + const phases = ["stopped", ...Array(40).fill("Error"), "Ready"]; test.get.mockImplementation(async () => ({ id: sandboxId, phase: phases.shift() ?? "Error", })); await expect(test.lifecycle.startSandbox(request)).resolves.toEqual({ kind: "accepted" }); - expect(test.waitForStartPoll).toHaveBeenCalledTimes(20); + expect(test.waitForStartPoll).toHaveBeenCalledTimes(40); expect(test.startSandbox).toHaveBeenCalledOnce(); }); it.each([ - { firstPhase: "Error", expectedPolls: 20 }, + { firstPhase: "Error", expectedPolls: 40 }, { firstPhase: "Provisioning", expectedPolls: 1 }, ])("fails on Error after $firstPhase", async ({ firstPhase, expectedPolls }) => { const test = harness(); diff --git a/src/lib/adapters/openshell/sandbox-lifecycle-sdk.ts b/src/lib/adapters/openshell/sandbox-lifecycle-sdk.ts index feb3170fb25..ea1a47a6432 100644 --- a/src/lib/adapters/openshell/sandbox-lifecycle-sdk.ts +++ b/src/lib/adapters/openshell/sandbox-lifecycle-sdk.ts @@ -55,7 +55,13 @@ export type SdkOpenShellSandboxStateLifecycleDeps = Readonly<{ waitForStopPoll?: (signal: AbortSignal) => Promise; }>; +// Start polling tolerates forty stale Error snapshots at three-second intervals. A +// managed ARM64 activation reproduced ContainerExited just after the first +// minute while the immutable workload container was already running. The +// lifecycle still requires the same sandbox identity to publish Ready. +const SDK_START_INITIAL_ERROR_GRACE_POLLS = 40; const DEFAULT_MUTATION_TIMEOUT_MS = 75_000; +const DEFAULT_START_TIMEOUT_MS = 135_000; function lifecycleError( error: unknown, @@ -133,7 +139,8 @@ async function mutate( const controller = new AbortController(); const timeout = setTimeout( () => controller.abort(), - request.timeoutMs ?? DEFAULT_MUTATION_TIMEOUT_MS, + request.timeoutMs ?? + (action === "start" ? DEFAULT_START_TIMEOUT_MS : DEFAULT_MUTATION_TIMEOUT_MS), ); const aborted = new Promise((_resolve, reject) => { controller.signal.addEventListener( @@ -181,7 +188,10 @@ async function mutate( }, }; } - const allowInitialError = createSandboxStartErrorGrace(action === "start"); + const allowInitialError = createSandboxStartErrorGrace( + action === "start", + SDK_START_INITIAL_ERROR_GRACE_POLLS, + ); for (;;) { const current = await Promise.race([ client.sandbox.get(request.sandboxName, { signal: controller.signal }), diff --git a/src/lib/agent/candidate-authority.ts b/src/lib/agent/candidate-authority.ts index 3edfb7f2ecf..610c8e01eac 100644 --- a/src/lib/agent/candidate-authority.ts +++ b/src/lib/agent/candidate-authority.ts @@ -13,8 +13,8 @@ export const CANDIDATE_QUALIFICATION_RECEIPT_DIGESTS: Readonly< Record > = Object.freeze({ pi: Object.freeze([ - "ed38379ad8a5e75ecca11d7067835ad36e38e8a97ff132e6f25567c3035e2279", - "89a5fb8addf9e10a1c1d8ee958d0863e7bcf59e041aa397fdf02653fe43a9797", + "de24d8cc89a9b3c2fa66f3175b6076e66bc7f26feb0d3f92cddf38776bb41ef2", + "d681123016c1b52adabd847be35e6744887317ed02aa2a33ca764a5d5b6399ba", ]), }); diff --git a/src/lib/domain/lifecycle/sandbox-start-error-grace.ts b/src/lib/domain/lifecycle/sandbox-start-error-grace.ts index 968993a0b76..c725ec1f964 100644 --- a/src/lib/domain/lifecycle/sandbox-start-error-grace.ts +++ b/src/lib/domain/lifecycle/sandbox-start-error-grace.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // OpenShell can transiently report Error after start before advancing to Ready. -// Opt into twenty initial Error observations only after starting the sandbox; +// Opt into twenty initial Error observations by default only after starting the sandbox; // observing another phase ends the grace. Remove this compatibility exception // when OpenShell exposes a structured restart signal or guarantees that restart // recovery never emits the terminal Error phase. @@ -10,8 +10,9 @@ const START_INITIAL_ERROR_GRACE_POLLS = 20; export function createSandboxStartErrorGrace( enabled: boolean, + maximumPolls = START_INITIAL_ERROR_GRACE_POLLS, ): (phase: string | null | undefined) => boolean { - let remainingPolls = enabled ? START_INITIAL_ERROR_GRACE_POLLS : 0; + let remainingPolls = enabled ? maximumPolls : 0; return (phase) => { const allowed = phase?.toLowerCase() === "error" && remainingPolls > 0; remainingPolls = allowed ? remainingPolls - 1 : 0; diff --git a/src/lib/inference/native-local/profile.ts b/src/lib/inference/native-local/profile.ts index f206d186d9a..944f77c9687 100644 --- a/src/lib/inference/native-local/profile.ts +++ b/src/lib/inference/native-local/profile.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "../../onboard/openshell-version"; + import { requireNativeProviderPolicy } from "../../adapters/openshell/provider-policy"; import fs from "node:fs"; import os from "node:os"; @@ -77,6 +79,11 @@ export function prepareNativeLocalProfile(input: NativeLocalBinding) { binaries: [ // The provider-neutral onboarding smoke probe uses Python urllib. "/usr/bin/python3", + // The projected runtime matches the pinned images' resolved interpreter. + ...(String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3" && + (binding.agent === "hermes" || binding.agent === "openclaw" || binding.agent === "pi") + ? ["/usr/bin/python3.13"] + : []), ...(binding.agent === "hermes" ? ["/opt/hermes/.venv/bin/python", "/opt/hermes/.venv/bin/python3"] : []), @@ -157,7 +164,9 @@ async function checkedAttachment(input: AttachmentInput, requirePolicy = true) { return { expected, target, profile: { ...prepared, profilePath: "", label: "host-local" } }; } -export async function verifyNativeLocalProviderAttachment(input: AttachmentInput) { +export async function verifyNativeLocalProviderAttachment( + input: AttachmentInput & { requireUnexpiredCredential?: boolean }, +) { const checked = await checkedAttachment(input); await verifyNativeProviderAttachment({ ...input, ...checked }); return checked.expected; diff --git a/src/lib/inference/native-provider/lifecycle-expiry.test.ts b/src/lib/inference/native-provider/lifecycle-expiry.test.ts new file mode 100644 index 00000000000..8a6b02953f6 --- /dev/null +++ b/src/lib/inference/native-provider/lifecycle-expiry.test.ts @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, it, vi } from "vitest"; +import { verifyNativeProviderAttachment } from "./lifecycle"; +import type { OpenShellProviderAdapter } from "../../adapters/openshell/provider-adapter"; + +it.each(["never", "future", "expired", "missing", "invalid", "identity", "revision"])( + "binds credential expiry to the exact provider: %s", + async (scenario) => { + const metadata = { + name: "local-provider", + type: "local-profile", + credentialKeys: ["API_KEY"], + configKeys: [], + revision: { id: "owned", resourceVersion: 1 }, + ...(scenario === "missing" + ? {} + : { + credentialExpiresAtMs: { + API_KEY: + scenario === "expired" + ? 1 + : scenario === "invalid" + ? -1 + : scenario === "future" + ? Date.now() + 60_000 + : 0, + }, + }), + }; + const getProvider = vi + .fn() + .mockResolvedValueOnce({ ok: true, value: metadata }) + .mockResolvedValueOnce({ + ok: true, + value: { + ...metadata, + revision: { + id: scenario === "identity" ? "replacement" : "owned", + resourceVersion: scenario === "revision" ? 2 : 1, + }, + }, + }); + const adapter = { + getProvider, + listProviderAttachments: vi.fn(async () => ({ + ok: true, + value: { names: ["local-provider"] }, + })), + } as unknown as OpenShellProviderAdapter; + const result = verifyNativeProviderAttachment({ + profile: { + providerName: "local-provider", + profileId: "local-profile", + credentialEnv: "API_KEY", + label: "host-local", + profilePath: "", + }, + adapter, + target: { kind: "named", gatewayName: "selected" }, + sandboxName: "sandbox", + expected: { + schemaVersion: 1, + profileId: "local-profile", + providerName: "local-provider", + providerId: "owned", + }, + requireUnexpiredCredential: true, + }); + const observation = await result.then( + (value) => ({ providerId: value.providerId }), + (error: Error) => ({ error: error.message }), + ); + expect(observation).toEqual( + scenario === "never" || scenario === "future" + ? { providerId: "owned" } + : { error: expect.stringMatching(/credential/) }, + ); + expect(getProvider).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ includeCredentialExpirations: true }), + ); + expect(getProvider).toHaveBeenCalledTimes(2); + }, +); diff --git a/src/lib/inference/native-provider/lifecycle.ts b/src/lib/inference/native-provider/lifecycle.ts index 9831e0f3dde..3cc066786a0 100644 --- a/src/lib/inference/native-provider/lifecycle.ts +++ b/src/lib/inference/native-provider/lifecycle.ts @@ -28,6 +28,12 @@ export class NativeProviderError extends Error { } } +export class NativeProviderCredentialExpiredError extends NativeProviderError { + constructor() { + super("Native provider credential is expired."); + } +} + function assertExpectedProfile( profile: NativeProviderProfile, expected?: NativeProviderAttachment, @@ -81,10 +87,12 @@ async function inspectNativeProvider( adapter: OpenShellProviderAdapter, target: OpenShellGatewayTarget, profile: NativeProviderProfile, + includeCredentialExpirations = false, ): Promise { const observed = await adapter.getProvider({ target, providerName: profile.providerName, + ...(includeCredentialExpirations ? { includeCredentialExpirations: true } : {}), }); if (observed.ok) return observed.value; if (observed.error.kind === "command" && observed.error.reason === "not_found") return null; @@ -307,12 +315,18 @@ export async function verifyNativeProviderAttachment(input: { target: OpenShellGatewayTarget; sandboxName: string; expected?: NativeProviderAttachment; + requireUnexpiredCredential?: boolean; }): Promise { const profile = input.profile; if (!profile) throw new NativeProviderError("Native inference requires a recorded provider identity."); assertExpectedProfile(profile, input.expected); - const provider = await inspectNativeProvider(input.adapter, input.target, profile); + const provider = await inspectNativeProvider( + input.adapter, + input.target, + profile, + input.requireUnexpiredCredential, + ); if (!provider) { throw new NativeProviderError( `OpenShell provider '${profile.providerName}' is missing. Recreate the sandbox to restore native ${profile.label} inference.`, @@ -338,6 +352,23 @@ export async function verifyNativeProviderAttachment(input: { `Sandbox '${input.sandboxName}' does not have its native ${profile.label} inference provider attached. Recreate the sandbox; NemoClaw does not migrate existing beta sandboxes automatically.`, ); } + if (input.requireUnexpiredCredential) { + // Expiry inventory is read separately from provider metadata. Bind that + // observation to the same immutable provider and resource version. + const current = await inspectNativeProvider(input.adapter, input.target, profile); + if ( + !current || + !exactNativeProvider(current, profile) || + current.revision?.id !== provider.revision?.id || + current.revision?.resourceVersion !== provider.revision?.resourceVersion || + provider.credentialExpiresAtMs === undefined + ) + throw new NativeProviderError("Native provider credential expiry could not be verified."); + const expiresAt = provider.credentialExpiresAtMs[profile.credentialEnv] ?? 0; + if (!Number.isSafeInteger(expiresAt) || expiresAt < 0) + throw new NativeProviderError("Native provider credential expiry is invalid."); + if (expiresAt > 0 && expiresAt <= Date.now()) throw new NativeProviderCredentialExpiredError(); + } return receipt; } diff --git a/src/lib/onboard/__test-helpers__/hermes-state-volume.ts b/src/lib/onboard/__test-helpers__/hermes-state-volume.ts index 9f916d6930a..8311907e7dd 100644 --- a/src/lib/onboard/__test-helpers__/hermes-state-volume.ts +++ b/src/lib/onboard/__test-helpers__/hermes-state-volume.ts @@ -20,6 +20,7 @@ export function createHermesStateVolumeDockerHarness(initial: VolumeState = null } : { status: 1, stderr: `Error response from daemon: get ${argv.at(-1)}: no such volume` }; case "create": { + if (volume) return { status: 0, stdout: `${volume.name}\n` }; const labels: Record = {}; for (let index = 1; index < argv.length - 1; index += 1) { switch (argv[index]) { diff --git a/src/lib/onboard/docker-driver-gateway-config-projected.test.ts b/src/lib/onboard/docker-driver-gateway-config-projected.test.ts new file mode 100644 index 00000000000..d92fbd52cb1 --- /dev/null +++ b/src/lib/onboard/docker-driver-gateway-config-projected.test.ts @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { parse } from "smol-toml"; +import { expect, it, vi } from "vitest"; +import { baseGatewayEnv } from "../../../test/support/openshell-gateway-config-helpers"; +import { + buildDockerDriverGatewayConfigToml, + ensureDockerDriverGatewayJwtBundle, +} from "./docker-driver-gateway-config"; +import { assertDockerDriverGatewayAuthConfigSafe } from "./docker-driver-gateway-env"; +import { prepareNativePodmanGatewayHostRuntime } from "./runtime-provider/podman-runtime-surfaces"; + +vi.mock("./openshell-version", async (original) => ({ + ...(await original()), + SUPPORTED_OPENSHELL_FALLBACK_VERSION: "0.1.3", +})); + +it.each(["docker", "podman"] as const)( + "writes the projected gateway schema for %s without legacy credential fields", + (driver) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "gateway-projected-")); + try { + const env = baseGatewayEnv(root); + const runtime = + driver === "podman" + ? prepareNativePodmanGatewayHostRuntime({ + environment: { ...env, OPENSHELL_DRIVERS: "podman" }, + platform: "linux", + socketPath: path.join(root, "podman.sock"), + }) + : undefined; + const toml = buildDockerDriverGatewayConfigToml( + env, + "/usr/bin/openshell-sandbox", + ensureDockerDriverGatewayJwtBundle(root), + "projected-gateway", + runtime, + ); + const configPath = path.join(root, "openshell-gateway.toml"); + fs.writeFileSync(configPath, toml); + const validate = (environment: NodeJS.ProcessEnv = {}) => + assertDockerDriverGatewayAuthConfigSafe( + { ...env, OPENSHELL_GATEWAY_CONFIG: configPath }, + environment, + "linux", + runtime, + ); + expect(() => validate()).not.toThrow(); + expect(() => validate({ OPENSHELL_OIDC_ISSUER: "https://issuer.example" })).toThrow( + "without OIDC overrides", + ); + fs.writeFileSync(configPath, toml); + const parsed = parse(toml) as unknown as { + openshell: { + version: number; + gateway: Record; + drivers: Record>; + }; + }; + expect(parsed.openshell).toMatchObject({ + version: 2, + gateway: { + compute_driver: driver, + disable_tls: false, + guest_tls_ca: path.join(root, "tls", "ca.crt"), + mtls_auth: { enabled: true }, + auth: { allow_unauthenticated_users: false }, + }, + }); + expect(toml).not.toMatch( + /compute_drivers|guest_tls_cert|guest_tls_key|require_client_auth|ttl_secs|sandbox_namespace/, + ); + expect(parsed.openshell.drivers[driver]).toMatchObject({ allow_driver_config: true }); + expect(parsed.openshell.drivers[driver].network_name).toBe( + driver === "podman" ? env.OPENSHELL_DOCKER_NETWORK_NAME : undefined, + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }, +); + +it.each([ + ["legacy schema", "version = 2", "version = 1", "openshell.version=2"], + ["disabled TLS", "disable_tls = false", "disable_tls = true", "disable_tls=false"], + ["disabled mTLS", "enabled = true", "enabled = false", "mtls_auth.enabled=true"], + [ + "unauthenticated users", + "allow_unauthenticated_users = false", + "allow_unauthenticated_users = true", + "allow_unauthenticated_users=false", + ], + [ + "retired client-auth switch", + "[openshell.gateway.tls]", + "[openshell.gateway.tls]\nrequire_client_auth = true", + "without OIDC overrides", + ], + [ + "explicit TTL", + "[openshell.gateway.gateway_jwt]", + "[openshell.gateway.gateway_jwt]\nttl_secs = 0", + "must omit gateway_jwt.ttl_secs", + ], + [ + "malformed OIDC issuer", + "[openshell.gateway.tls]", + '[openshell.gateway.oidc]\nissuer = ["invalid"]\n[openshell.gateway.tls]', + "without OIDC overrides", + ], + [ + "quoted OIDC issuer", + "[openshell.gateway.tls]", + '[openshell.gateway.oidc] # alternate syntax\n"issuer" = "https://issuer.example"\n[openshell.gateway.tls]', + "without OIDC overrides", + ], + [ + "quoted TTL", + "[openshell.gateway.gateway_jwt]", + '[openshell.gateway.gateway_jwt] # quoted field\n"ttl_secs" = 0', + "must omit gateway_jwt.ttl_secs", + ], + [ + "relative client CA", + /client_ca_path = "[^"\n]+"/, + 'client_ca_path = "relative.crt"', + "must be an absolute TLS path", + ], +] as const)("rejects projected gateway config with %s", (_label, source, replacement, error) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "gateway-auth-projected-")); + try { + const env = baseGatewayEnv(root); + const toml = buildDockerDriverGatewayConfigToml( + env, + "/usr/bin/openshell-sandbox", + ensureDockerDriverGatewayJwtBundle(root), + "projected-gateway", + ); + const invalid = toml.replace(source, replacement); + expect(invalid).not.toBe(toml); + const configPath = path.join(root, "openshell-gateway.toml"); + fs.writeFileSync(configPath, invalid); + expect(() => + assertDockerDriverGatewayAuthConfigSafe( + { ...env, OPENSHELL_GATEWAY_CONFIG: configPath }, + {}, + "linux", + ), + ).toThrow(error); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/src/lib/onboard/docker-driver-gateway-config.ts b/src/lib/onboard/docker-driver-gateway-config.ts index e7824c18d45..dcfc6b0d15a 100644 --- a/src/lib/onboard/docker-driver-gateway-config.ts +++ b/src/lib/onboard/docker-driver-gateway-config.ts @@ -34,6 +34,7 @@ import { import type { ExternalComponentGatewayPreparation } from "./external-component/activation"; import { externalComponentDockerSocket } from "./external-component/network"; import type { RuntimeProviderGatewayHostRuntime } from "./runtime-provider/contract"; +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "./openshell-version"; import { resolveConfiguredRuntimeProvider, resolveRegisteredRuntimeProvider, @@ -110,7 +111,7 @@ function alternateGatewayRuntimeProjection( driver: string, driverConfig: Record, ): RuntimeProviderGatewayHostRuntime { - const namespace = driverConfig.sandbox_namespace; + const namespace = driverConfig.sandbox_label ?? driverConfig.sandbox_namespace; const hostGatewayIp = driverConfig.host_gateway_ip; const socketPath = driverConfig.socket_path; return { @@ -122,6 +123,9 @@ function alternateGatewayRuntimeProjection( sandboxNamespace: isNonEmptyString(namespace) ? "scoped" : "omitted", hostGatewayIp: isNonEmptyString(hostGatewayIp) ? hostGatewayIp : null, includeSupervisorBin: isNonEmptyString(driverConfig.supervisor_bin), + driverConfigLayout: isNonEmptyString(driverConfig.network_name) + ? "inline-supervisor" + : "split-supervisor", }, }; } @@ -686,23 +690,39 @@ function existingGatewayIdentityFromConfig( if (!parsed || !openshell || !gateway || !gatewayJwt || !drivers || !driverConfig) { throw ambiguousGatewayConfig(configPath, "the config does not match NemoClaw's schema"); } + const schemaVersion = openshell.version; + if (schemaVersion !== 1 && schemaVersion !== 2) { + throw ambiguousGatewayConfig(configPath, "unsupported gateway schema version"); + } + const legacyDriver = + schemaVersion === 1 || runtime.gatewayConfig.driverConfigLayout === "inline-supervisor"; const requiredDriverFields = [ "grpc_endpoint", - ...(runtime.gatewayConfig.hostGatewayIp === null ? [] : ["host_gateway_ip"]), + ...(!legacyDriver || runtime.gatewayConfig.hostGatewayIp === null ? [] : ["host_gateway_ip"]), ...(runtime.socketPath === null ? [] : ["socket_path"]), - "network_name", + ...(legacyDriver ? ["network_name"] : []), "supervisor_image", - "guest_tls_ca", - "guest_tls_cert", - "guest_tls_key", ]; if (!requiredDriverFields.every((field) => isNonEmptyString(driverConfig[field]))) { throw ambiguousGatewayConfig(configPath, "the driver config is incomplete"); } + const guestTlsOwner = schemaVersion === 1 ? driverConfig : gateway; + const legacyGuestCredentials = + schemaVersion === 1 || + guestTlsOwner.guest_tls_cert !== undefined || + guestTlsOwner.guest_tls_key !== undefined; + const requiredGuestTlsFields = legacyGuestCredentials + ? ["guest_tls_ca", "guest_tls_cert", "guest_tls_key"] + : ["guest_tls_ca"]; + if (!requiredGuestTlsFields.every((field) => isNonEmptyString(guestTlsOwner[field]))) { + throw ambiguousGatewayConfig(configPath, "the guest TLS config is incomplete"); + } + const configuredGatewayId = gatewayJwt.gateway_id; const configuredGatewayJwtTtl = gatewayJwt.ttl_secs; - const namespace = driverConfig.sandbox_namespace; + const namespace = + schemaVersion === 2 ? driverConfig.sandbox_label : driverConfig.sandbox_namespace; const legacyGatewayId = legacyGatewayIdForStateDir(stateDir); const scopedGatewayId = gatewayIdForStateDir(stateDir); const hasLegacyNamespace = @@ -735,7 +755,14 @@ function existingGatewayIdentityFromConfig( } assignStringEnv(parsedEnv, "OPENSHELL_GRPC_ENDPOINT", driverConfig.grpc_endpoint); assignStringEnv(parsedEnv, "OPENSHELL_PODMAN_SOCKET", driverConfig.socket_path); - assignStringEnv(parsedEnv, "OPENSHELL_DOCKER_NETWORK_NAME", driverConfig.network_name); + // Docker 0.1.2 rejects network_name as a driver setting. Keep the + // component's inspected host bridge as canonical NemoClaw metadata instead. + const componentNetwork = legacyDriver + ? driverConfig.network_name + : originalToml.match( + /^# nemoclaw-external-component-network = "([a-zA-Z0-9][a-zA-Z0-9_.-]{0,254})"$/mu, + )?.[1]; + assignStringEnv(parsedEnv, "OPENSHELL_DOCKER_NETWORK_NAME", componentNetwork); assignStringEnv(parsedEnv, "OPENSHELL_DOCKER_SUPERVISOR_IMAGE", driverConfig.supervisor_image); const configuredSandboxBin = typeof driverConfig.supervisor_bin === "string" ? driverConfig.supervisor_bin : undefined; @@ -752,6 +779,8 @@ function existingGatewayIdentityFromConfig( canonicalGatewayJwtTtl, configuredRuntime, externalComponent, + schemaVersion, + legacyGuestCredentials, ); if (originalToml !== canonicalToml) { throw ambiguousGatewayConfig( @@ -846,27 +875,49 @@ function buildDockerDriverGatewayConfigTomlForIdentity( gatewayJwtTtlSecs = DOCKER_DRIVER_GATEWAY_JWT_TTL_SECS, projectedRuntime?: RuntimeProviderGatewayHostRuntime, externalComponent?: ExternalComponentGatewayConfiguration | null, + schemaVersion: 1 | 2 = String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3" ? 2 : 1, + legacyGuestCredentials = schemaVersion === 1, ): string { const runtime = resolveGatewayRuntimeProjection(gatewayEnv, projectedRuntime); const driver = runtime.openShellDriver; + const legacyDriver = + schemaVersion === 1 || runtime.gatewayConfig.driverConfigLayout === "inline-supervisor"; const localTlsDir = jwtBundle ? gatewayLocalTlsDir(gatewayEnv) : undefined; + const guestTlsConfig = localTlsDir + ? [ + `guest_tls_ca = ${tomlString(path.join(localTlsDir, "ca.crt"))}`, + // Reconstruct old bytes only for ownership verification, never for a new config. + ...(legacyGuestCredentials + ? [ + `guest_tls_cert = ${tomlString(path.join(localTlsDir, "client", "tls.crt"))}`, + `guest_tls_key = ${tomlString(path.join(localTlsDir, "client", "tls.key"))}`, + ] + : []), + ] + : []; const dockerEntries: [string, string | boolean | undefined][] = [ + // NemoClaw create plans use driver JSON for managed state, tmpfs and GPU selection. + // Opt in on our gateway only; upstream resource admission and bind-mount gates remain. + ["allow_driver_config", schemaVersion === 2 ? true : undefined], ["enable_bind_mounts", gatewayEnv.NEMOCLAW_DOCKER_ENABLE_BIND_MOUNTS === "1" || undefined], [ - "sandbox_namespace", + schemaVersion === 2 ? "sandbox_label" : "sandbox_namespace", runtime.gatewayConfig.sandboxNamespace === "scoped" ? (sandboxNamespace ?? undefined) : undefined, ], ["grpc_endpoint", gatewayEnv.OPENSHELL_GRPC_ENDPOINT], - ["host_gateway_ip", runtime.gatewayConfig.hostGatewayIp ?? undefined], + [ + "host_gateway_ip", + legacyDriver ? (runtime.gatewayConfig.hostGatewayIp ?? undefined) : undefined, + ], [ "socket_path", externalComponent && "interceptor" in externalComponent ? externalComponentDockerSocket(runtime, gatewayEnv) : (runtime.socketPath ?? undefined), ], - ["network_name", gatewayEnv.OPENSHELL_DOCKER_NETWORK_NAME], + ["network_name", legacyDriver ? gatewayEnv.OPENSHELL_DOCKER_NETWORK_NAME : undefined], ["supervisor_image", gatewayEnv.OPENSHELL_DOCKER_SUPERVISOR_IMAGE], // OpenShell 0.0.99 accepts supervisor_bin only for the Docker driver. // The Podman schema rejects the entire driver table when this Docker-only @@ -875,9 +926,6 @@ function buildDockerDriverGatewayConfigTomlForIdentity( "supervisor_bin", runtime.gatewayConfig.includeSupervisorBin ? (sandboxBin ?? undefined) : undefined, ], - ["guest_tls_ca", localTlsDir ? path.join(localTlsDir, "ca.crt") : undefined], - ["guest_tls_cert", localTlsDir ? path.join(localTlsDir, "client", "tls.crt") : undefined], - ["guest_tls_key", localTlsDir ? path.join(localTlsDir, "client", "tls.key") : undefined], ]; const dockerConfig = dockerEntries .filter( @@ -892,11 +940,14 @@ function buildDockerDriverGatewayConfigTomlForIdentity( const sections = [ "[openshell]", - "version = 1", + `version = ${schemaVersion}`, "", "[openshell.gateway]", - `compute_drivers = [${tomlString(driver)}]`, + schemaVersion === 2 + ? `compute_driver = ${tomlString(driver)}` + : `compute_drivers = [${tomlString(driver)}]`, "disable_tls = false", + ...(schemaVersion === 2 ? guestTlsConfig : []), "", ]; if ( @@ -917,7 +968,7 @@ function buildDockerDriverGatewayConfigTomlForIdentity( `cert_path = ${tomlString(path.join(tlsDir, "server", "tls.crt"))}`, `key_path = ${tomlString(path.join(tlsDir, "server", "tls.key"))}`, `client_ca_path = ${tomlString(path.join(tlsDir, "ca.crt"))}`, - "require_client_auth = true", + ...(schemaVersion === 1 ? ["require_client_auth = true"] : []), "", "[openshell.gateway.mtls_auth]", "enabled = true", @@ -927,7 +978,8 @@ function buildDockerDriverGatewayConfigTomlForIdentity( `public_key_path = ${tomlString(jwtBundle.publicKeyPath)}`, `kid_path = ${tomlString(jwtBundle.kidPath)}`, `gateway_id = ${tomlString(gatewayId)}`, - `ttl_secs = ${gatewayJwtTtlSecs}`, + // Schema 2 omits TTL for non-expiring sandbox sessions; explicit zero is invalid. + ...(schemaVersion === 1 ? [`ttl_secs = ${gatewayJwtTtlSecs}`] : []), "", "[openshell.gateway.auth]", "allow_unauthenticated_users = false", @@ -939,6 +991,8 @@ function buildDockerDriverGatewayConfigTomlForIdentity( if (!jwtBundle) throw new ExternalComponentContractError("declaration_invalid"); const settings = validateExternalComponentGatewaySettings(externalComponent); const network = externalComponentGatewayNetwork(gatewayEnv, runtime, settings); + if (!legacyDriver) + sections.push(`# nemoclaw-external-component-network = ${tomlString(network.name)}`); sections.push(...renderExternalComponentConnections(settings, network.gatewayIp)); } else if (externalComponent) { sections.push( @@ -965,6 +1019,7 @@ function buildDockerDriverGatewayConfigTomlForIdentity( sections.push(`[openshell.drivers.${driver}]`); if (dockerConfig) sections.push(dockerConfig); + if (schemaVersion === 1) sections.push(...guestTlsConfig); sections.push(""); return sections.join("\n"); diff --git a/src/lib/onboard/docker-driver-gateway-env.ts b/src/lib/onboard/docker-driver-gateway-env.ts index e9c94464c9b..9beca9edf45 100644 --- a/src/lib/onboard/docker-driver-gateway-env.ts +++ b/src/lib/onboard/docker-driver-gateway-env.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { parse as parseToml } from "smol-toml"; import { dockerCapture, dockerRun } from "../adapters/docker/run"; import { openRegularFileNoFollow } from "../adapters/fs/regular-file"; @@ -48,6 +49,7 @@ import type { RuntimeProviderGatewayHostRuntime, RuntimeProviderGatewaySurface, } from "./runtime-provider/contract"; +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "./openshell-version"; import { resolveConfiguredRuntimeProvider } from "./runtime-provider/selection"; export { getGatewayHttpsEndpoint, startPackageManagedDockerDriverGateway }; @@ -168,6 +170,7 @@ function preparePortableGatewayHostRuntime( sandboxNamespace: "omitted", hostGatewayIp: PORTABLE_HOST_GATEWAY_IP, includeSupervisorBin: false, + driverConfigLayout: "inline-supervisor", processOwnership: "scoped-namespace", }, network: { @@ -326,42 +329,22 @@ export function assertDockerDriverGatewayBindAddressSafe( ); } -type TomlScalar = boolean | number | string; - -function parseTomlScalar(raw: string): TomlScalar | undefined { - const booleanMatch = raw.match(/^(true|false)(?:\s+#.*)?$/); - if (booleanMatch?.[1]) return booleanMatch[1] === "true"; - const integerMatch = raw.match(/^(\d+)(?:\s+#.*)?$/); - if (integerMatch?.[1]) return Number(integerMatch[1]); - const stringMatch = raw.match(/^("(?:[^"\\]|\\.)*")(?:\s+#.*)?$/); - if (!stringMatch?.[1]) return undefined; - try { - const value: unknown = JSON.parse(stringMatch[1]); - return typeof value === "string" ? value : undefined; - } catch { - return undefined; - } -} - -function parseTomlScalarValues(toml: string): Map { - const values = new Map(); - let section = ""; - for (const rawLine of toml.split("\n")) { - const line = rawLine.trim(); - const sectionMatch = line.match(/^\[([A-Za-z0-9_.-]+)\]$/); - if (sectionMatch?.[1]) { - section = sectionMatch[1]; - continue; +function parseTomlScalarValues(toml: string) { + const parsed = parseToml(toml); + const get = (key: string): unknown => { + let value: unknown = parsed; + for (const part of key.split(".")) { + if (!value || typeof value !== "object" || !Object.hasOwn(value, part)) return undefined; + value = (value as Record)[part]; } - const assignmentMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/); - if (!assignmentMatch?.[1] || !assignmentMatch[2]) continue; - const value = parseTomlScalar(assignmentMatch[2]); - if (value !== undefined) values.set(`${section}.${assignmentMatch[1]}`, value); - } - return values; + return value; + }; + return { get, has: (key: string) => get(key) !== undefined }; } -function assertTomlBoolean(values: Map, key: string, expected: boolean): void { +type TomlValues = ReturnType; + +function assertTomlBoolean(values: TomlValues, key: string, expected: boolean): void { const actual = values.get(key); if (actual === expected) return; throw new Error( @@ -371,13 +354,13 @@ function assertTomlBoolean(values: Map, key: string, expecte ); } -function assertTomlString(values: Map, key: string): string { +function assertTomlString(values: TomlValues, key: string): string { const actual = values.get(key); if (typeof actual === "string" && actual.trim()) return actual; throw new Error(`OpenShell Docker-driver gateway config must set non-empty ${key}`); } -function assertTomlInteger(values: Map, key: string, expected: number): void { +function assertTomlInteger(values: TomlValues, key: string, expected: number): void { const actual = values.get(key); if (actual === expected) return; throw new Error( @@ -418,7 +401,28 @@ export function assertDockerDriverGatewayAuthConfigSafe( const toml = fs.readFileSync(configPath, "utf-8"); const values = parseTomlScalarValues(toml); assertTomlBoolean(values, "openshell.gateway.disable_tls", false); - assertTomlBoolean(values, "openshell.gateway.tls.require_client_auth", true); + const nativeSchema = String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3"; + if (nativeSchema) { + assertTomlInteger(values, "openshell.version", 2); + for (const key of ["cert_path", "key_path", "client_ca_path"] as const) { + const fullKey = `openshell.gateway.tls.${key}`; + if (!path.isAbsolute(assertTomlString(values, fullKey))) { + throw new Error(`OpenShell gateway ${fullKey} must be an absolute TLS path`); + } + } + // Schema 2 derives client authentication from its CA and application auth settings. + if ( + values.has("openshell.gateway.tls.require_client_auth") || + values.has("openshell.gateway.oidc.issuer") || + (gatewayEnv.OPENSHELL_OIDC_ISSUER ?? environment.OPENSHELL_OIDC_ISSUER) !== undefined + ) { + throw new Error( + "OpenShell gateway requires schema 2 client-CA authentication without OIDC overrides", + ); + } + } else { + assertTomlBoolean(values, "openshell.gateway.tls.require_client_auth", true); + } assertTomlBoolean(values, "openshell.gateway.mtls_auth.enabled", true); assertTomlBoolean(values, "openshell.gateway.auth.allow_unauthenticated_users", false); for (const key of ["signing_key_path", "public_key_path", "kid_path"] as const) { @@ -426,11 +430,19 @@ export function assertDockerDriverGatewayAuthConfigSafe( assertGatewayJwtFile(fullKey, assertTomlString(values, fullKey)); } assertTomlString(values, "openshell.gateway.gateway_jwt.gateway_id"); - assertTomlInteger( - values, - "openshell.gateway.gateway_jwt.ttl_secs", - DOCKER_DRIVER_GATEWAY_JWT_TTL_SECS, - ); + if (nativeSchema) { + if (values.has("openshell.gateway.gateway_jwt.ttl_secs")) { + throw new Error( + "OpenShell gateway config must omit gateway_jwt.ttl_secs for non-expiring sandbox sessions", + ); + } + } else { + assertTomlInteger( + values, + "openshell.gateway.gateway_jwt.ttl_secs", + DOCKER_DRIVER_GATEWAY_JWT_TTL_SECS, + ); + } } export function getDockerDriverGatewayEndpoint(gatewayPort: number = GATEWAY_PORT): string { diff --git a/src/lib/onboard/initial-policy-landlock.test.ts b/src/lib/onboard/initial-policy-landlock.test.ts new file mode 100644 index 00000000000..ade9b31a781 --- /dev/null +++ b/src/lib/onboard/initial-policy-landlock.test.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import path from "node:path"; +import { expect, it, vi } from "vitest"; +import YAML from "yaml"; + +const runtime = vi.hoisted(() => ({ version: "0.0.116" })); +vi.mock("./openshell-version", async (importOriginal) => ({ + ...(await importOriginal()), + get SUPPORTED_OPENSHELL_FALLBACK_VERSION() { + return runtime.version; + }, +})); + +import { prepareInitialSandboxCreatePolicy } from "./initial-policy"; + +it.each([ + { agent: "langchain-deepagents-code", version: "0.0.116", mode: "strict" }, + { agent: "langchain-deepagents-code", version: "0.1.3", mode: "hard_requirement" }, + { agent: "pi", version: "0.0.116", mode: "strict" }, + { agent: "pi", version: "0.1.3", mode: "hard_requirement" }, +])("preserves required Landlock enforcement for $agent on $version", ({ agent, version, mode }) => { + runtime.version = version; + const source = path.resolve( + import.meta.dirname, + "../../../agents", + agent, + "policy-additions.yaml", + ); + const original = fs.readFileSync(source, "utf8"); + const policy = prepareInitialSandboxCreatePolicy(source, [], { agentName: agent }); + try { + const effective = YAML.parse(fs.readFileSync(policy.policyPath, "utf8")); + const baseline = YAML.parse(original); + expect(effective.landlock).toEqual({ ...baseline.landlock, compatibility: mode }); + expect(effective.filesystem_policy).toEqual(baseline.filesystem_policy); + expect(fs.readFileSync(source, "utf8")).toBe(original); + } finally { + policy.cleanup?.(); + } + expect(fs.existsSync(policy.policyPath)).toBe(policy.policyPath === source); +}); diff --git a/src/lib/onboard/initial-policy.ts b/src/lib/onboard/initial-policy.ts index df08dd4809a..aa6c5dec9df 100644 --- a/src/lib/onboard/initial-policy.ts +++ b/src/lib/onboard/initial-policy.ts @@ -35,6 +35,7 @@ import { requiredOpenclawOtelPolicyPresets } from "./openclaw-otel-policy-preset import { filterSuppressedAgentRequiredPresets } from "./policy-tier-suppression"; import { cleanupTempDir, createExactTempFileCleanup, secureTempFile } from "./temp-files"; import { isPortableExperimentalProfile } from "./experimental/portable-profile"; +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "./openshell-version"; export type InitialSandboxPolicy = { policyPath: string; @@ -503,6 +504,18 @@ function resolveInitialSandboxCreatePolicy( } }; try { + if (String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) === "0.1.3") { + const policy = YAML.parse(basePolicy); + if ( + isObjectRecord(policy) && + isObjectRecord(policy.landlock) && + policy.landlock.compatibility === "strict" + ) { + // OpenShell 0.1.3 renamed the fail-closed Landlock mode. + policy.landlock.compatibility = "hard_requirement"; + adoptPolicy(YAML.stringify(policy), "nemoclaw-landlock-policy"); + } + } if (options.inferenceProvider === NVIDIA_HOSTED_NATIVE_PROVIDER) { adoptPolicy(buildNativeNvidiaSandboxPolicy(basePolicy), "nemoclaw-native-inference-policy"); } diff --git a/src/lib/onboard/machine/handlers/provider-inference-managed-llama-resume.test.ts b/src/lib/onboard/machine/handlers/provider-inference-managed-llama-resume.test.ts index df11c0c88c4..3a769507b79 100644 --- a/src/lib/onboard/machine/handlers/provider-inference-managed-llama-resume.test.ts +++ b/src/lib/onboard/machine/handlers/provider-inference-managed-llama-resume.test.ts @@ -3,7 +3,8 @@ import { describe, expect, it, vi } from "vitest"; -import { getSandboxStatusReport } from "../../../actions/sandbox/status"; +// This suite checks profile projection; lifecycle locking belongs to the status action tests. +import { getSandboxStatusReport } from "../../../actions/sandbox/status-snapshot"; import type { VllmProfile } from "../../../inference/vllm"; import { loadServingCatalog } from "../../../inference/serving/catalog-loader"; import { servingProfileModel } from "../../../inference/serving/requested-profile-model"; @@ -18,6 +19,12 @@ import { createSetupNim, type SetupNimFlowDeps } from "../../setup-nim-flow"; import { handleProviderInferenceState } from "./provider-inference"; import { baseOptions, baseSelection, createDeps } from "./provider-inference.test-support"; +// Resume reservation must not read the operator's registry or wait on host state. +vi.mock("../../../state/registry", async (importOriginal) => ({ + ...(await importOriginal()), + getSandbox: vi.fn(() => null), +})); + const llamaCppProfile: ServingProfileProvenance = { schemaVersion: 1, catalogDigest: `sha256:${"a".repeat(64)}`, diff --git a/src/lib/onboard/machine/runtime-effectful-preflight.test.ts b/src/lib/onboard/machine/runtime-effectful-preflight.test.ts index 612a79a000d..d017f0f34ce 100644 --- a/src/lib/onboard/machine/runtime-effectful-preflight.test.ts +++ b/src/lib/onboard/machine/runtime-effectful-preflight.test.ts @@ -36,6 +36,7 @@ function gatewayRuntime( sandboxNamespace: "scoped", hostGatewayIp: null, includeSupervisorBin: true, + driverConfigLayout: "split-supervisor", processOwnership: "scoped-namespace", }, network: { diff --git a/src/lib/onboard/managed-startup/hold.ts b/src/lib/onboard/managed-startup/hold.ts index 8aecbb43c18..6ded331c35f 100644 --- a/src/lib/onboard/managed-startup/hold.ts +++ b/src/lib/onboard/managed-startup/hold.ts @@ -1,5 +1,26 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { getReadyCheckOutputPatternsForAgent } from "../../sandbox/create-stream-ready-gate"; + export const MANAGED_STARTUP_HOLD_EXECUTABLE = "/usr/local/bin/nemoclaw-managed-startup-hold"; export const MANAGED_STARTUP_EXECUTABLE = "/usr/local/bin/nemoclaw-start"; + +export function managedStartupCreateReadiness(input: { + startupCommand: readonly string[]; + isTerminalAgent: boolean; + env: NodeJS.ProcessEnv; + deferPostCreateEffects: boolean; +}): { readyCheckOutputPatterns: readonly RegExp[]; waitForReadyTermination: boolean } { + // The post-create transaction releases the hold, so create cannot wait for + // a native startup banner while the hold is active. + const held = input.startupCommand.includes(MANAGED_STARTUP_HOLD_EXECUTABLE); + return { + readyCheckOutputPatterns: getReadyCheckOutputPatternsForAgent({ + isTerminalAgent: input.isTerminalAgent, + startupRunsDuringCreate: !held, + env: input.env, + }), + waitForReadyTermination: held || input.deferPostCreateEffects, + }; +} diff --git a/src/lib/onboard/managed-startup/image-runtime.ts b/src/lib/onboard/managed-startup/image-runtime.ts index 9263ada0b2a..a5ff5063d6d 100644 --- a/src/lib/onboard/managed-startup/image-runtime.ts +++ b/src/lib/onboard/managed-startup/image-runtime.ts @@ -78,6 +78,8 @@ const HERMES_MANAGED_CONFIG_FILES = [ const HERMES_GENERATED_MANAGED_POLICY_FILE = "/sandbox/.hermes/managed-policy.json"; const HERMES_INSTALLED_MANAGED_POLICY_FILE = "/usr/local/share/nemoclaw/hermes-managed-policy.json"; const MAX_HERMES_MANAGED_POLICY_BYTES = 4 * 1024 * 1024; +export const MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES = + 4 * Math.ceil(MAX_HERMES_MANAGED_POLICY_BYTES / 3) + 4096; const FIXED_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; const SHA256_RE = /^[a-f0-9]{64}$/u; export const MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION = 1; diff --git a/src/lib/onboard/managed-startup/provider-root-apply.test.ts b/src/lib/onboard/managed-startup/provider-root-apply.test.ts index ecf3b21a382..f6817e2636a 100644 --- a/src/lib/onboard/managed-startup/provider-root-apply.test.ts +++ b/src/lib/onboard/managed-startup/provider-root-apply.test.ts @@ -8,7 +8,11 @@ import { managedStartupE2eProfile, } from "../../../../scripts/checks/generate-managed-startup-profile-fixture.mts"; import { createPodmanRuntimeProviderBundle } from "../runtime-provider/podman"; -import type { RuntimeProviderBundle } from "../runtime-provider/contract"; +import type { + RuntimeProviderBundle, + RuntimeProviderPrivilegedSandboxCommandInput, +} from "../runtime-provider/contract"; +import { MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES } from "./shared-state-transaction"; import { DirectSandboxContainerNotFoundError, DirectSandboxFallbackUnavailableError, @@ -35,8 +39,13 @@ const IMAGE_ID = `sha256:${"b".repeat(64)}`; const SANDBOX_ID = "sandbox-podman-managed"; const SANDBOX_NAME = "managed-podman"; +type LifecycleFixtureProvider = RuntimeProviderBundle & { + lifecycle: Extract; +}; + function createDockerRootApplyFixture( resolveTarget: (input: { readonly timeoutMs?: number }) => { resourceHandle: string }, + agent: Parameters[0]["agent"] = "openclaw", ) { const labels = { "openshell.ai/managed-by": "openshell", @@ -45,15 +54,17 @@ function createDockerRootApplyFixture( "openshell.ai/sandbox-workspace": "default", }; const execute = vi.fn((input: { readonly command: readonly string[] }) => ({ - status: 0, + status: input.command.includes("--startup-protocol") ? 1 : 0, stdout: input.command.includes("--shared-state-transaction-status") ? "pending\n" : "", - stderr: "", + stderr: input.command.includes("--startup-protocol") + ? "usage: managed-startup-image-runtime --release-startup-hold" + : "", })); const runtimeProvider = { identity: { id: "docker" }, lifecycle: { supported: true, - privilegedSandboxControl: { resolveTarget, execute }, + privilegedSandboxControl: { resolveTarget, execute, prepareManagedStartupWorkspace: vi.fn() }, }, containerEngine: { supported: true, @@ -71,18 +82,269 @@ function createDockerRootApplyFixture( stderr: "", }), }, - } as unknown as RuntimeProviderBundle; + } as unknown as LifecycleFixtureProvider; const request = createManagedStartupRootApplyRequest({ - agent: "openclaw", + agent, corporateCaB64: Buffer.from(MANAGED_STARTUP_E2E_CORPORATE_CA_PEM, "utf8").toString("base64"), - encodedProfile: encodeManagedStartupProfile( - managedStartupE2eProfile("openclaw", false, true, true), - ), + encodedProfile: encodeManagedStartupProfile(managedStartupE2eProfile(agent, false, true, true)), }); return { request, runtimeProvider }; } +function delegatedFixture( + agent: Parameters[1] = "openclaw", + phase: "none" | "pending" | "committed" = "none", +) { + const fixture = createDockerRootApplyFixture(() => ({ resourceHandle: CONTAINER_ID }), agent); + const calls: Array = []; + const output: Record = { + "--startup-protocol": "delegated-owner-v1\n", + "--prepare-delegated-transaction-stdin": phase, + "--capture-shared-state-stdin": "private original capture", + "--begin-shared-state-from-capture-stdin": "created\n", + "--prepare-delegated-startup-stdin": + phase === "committed" ? "already-committed\n" : "prepared\n", + "--apply-owner-startup-stdin": "private generated result", + "--finish-delegated-startup-stdin": "CA tool output\napplied\n", + "--export-shared-restoration-profile-stdin": "private original capture", + "--restore-shared-state-stdin": "private restored observation", + "--finish-shared-restoration-stdin": "restored\n", + }; + const failures = new Set(); + const execute = vi.fn( + (input: RuntimeProviderPrivilegedSandboxCommandInput & { sandboxUser?: string }) => { + calls.push(input); + const operation = input.command.find((value) => value.startsWith("--"))!; + return { + status: failures.has(operation) ? 1 : 0, + signal: null, + stdout: Buffer.from(output[operation] ?? ""), + stderr: Buffer.from(failures.has(operation) ? "private-error-sentinel" : ""), + }; + }, + ); + fixture.runtimeProvider.lifecycle.privilegedSandboxControl.execute = execute; + fixture.runtimeProvider.lifecycle.privilegedSandboxControl.executeAsSandboxUser = execute; + const input = { + ...fixture, + sandboxName: SANDBOX_NAME, + sandboxId: SANDBOX_ID, + bootstrapIdentity: "c".repeat(64), + environment: {}, + }; + return { input, calls, output, failures, execute }; +} + +describe("provider delegated private startup", () => { + it.each([ + ["openclaw", "998:998"], + ["hermes", "998:999"], + ["langchain-deepagents-code", "999:999"], + ["pi", "999:999"], + ] as const)( + "orders %s capture, protected admission, generation and root finish", + (agent, sandboxUser) => { + const { input, calls } = delegatedFixture(agent); + const transaction = applyProviderManagedStartupRootRequest(input); + expect(transaction).toMatchObject({ + protocol: "identity-bound", + delegatedProfile: input.request.encodedProfile, + }); + expect(calls.map(({ command }) => command.find((value) => value.startsWith("--")))).toEqual([ + "--startup-protocol", + "--prepare-delegated-transaction-stdin", + "--capture-shared-state-stdin", + "--begin-shared-state-from-capture-stdin", + "--prepare-delegated-startup-stdin", + "--apply-owner-startup-stdin", + "--finish-delegated-startup-stdin", + ]); + expect(calls.filter((call) => call.sandboxUser).map((call) => call.sandboxUser)).toEqual([ + sandboxUser, + sandboxUser, + ]); + expect(calls.every((call) => call.expectedResourceHandle === CONTAINER_ID)).toBe(true); + expect(calls[2]?.maxOutputBytes).toBe(MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES); + expect(JSON.parse(String(calls[3]?.input))).toEqual({ + encodedProfile: input.request.encodedProfile, + capturedState: "private original capture", + }); + expect(JSON.parse(String(calls[6]?.input)).ownerResult).toBe("private generated result"); + expect(JSON.stringify(transaction)).not.toContain("private"); + expect(calls.flatMap((call) => call.command)).not.toContain("--release-startup-hold"); + }, + ); + + it.each(["pending", "committed"] as const)( + "keeps the original snapshot on %s recovery", + (phase) => { + const { input, calls } = delegatedFixture("openclaw", phase); + applyProviderManagedStartupRootRequest(input); + expect(calls.flatMap((call) => call.command)).not.toContain("--capture-shared-state-stdin"); + expect(calls.flatMap((call) => call.command)).not.toContain( + "--begin-shared-state-from-capture-stdin", + ); + const generation = calls.find((call) => call.command.includes("--apply-owner-startup-stdin")); + expect(generation?.command.includes("--replay")).toBe(phase === "committed"); + }, + ); + + it.each([ + "--prepare-delegated-transaction-stdin", + "--capture-shared-state-stdin", + "--begin-shared-state-from-capture-stdin", + "--prepare-delegated-startup-stdin", + "--apply-owner-startup-stdin", + "--finish-delegated-startup-stdin", + ])("retains recovery without leaking private output or falling back after %s fails", (action) => { + const { input, calls, failures } = delegatedFixture(); + failures.add(action); + let failure: Error & { managedStartupTransaction?: unknown }; + try { + applyProviderManagedStartupRootRequest(input); + throw new Error("expected failure"); + } catch (error) { + failure = error as typeof failure; + } + expect(failure.message).toContain("retained for recovery"); + expect(failure.message).not.toContain("private-error-sentinel"); + expect(failure.managedStartupTransaction).toMatchObject({ + delegatedProfile: input.request.encodedProfile, + }); + expect(calls.at(-1)?.command).toContain(action); + expect(calls.flatMap((call) => call.command)).not.toContain("--apply-root-stdin"); + }); + + it("rejects unknown protocol output before workspace changes", () => { + const { input, output, calls } = delegatedFixture(); + output["--startup-protocol"] = "unknown"; + const prepare = vi.fn(); + input.runtimeProvider.lifecycle.privilegedSandboxControl.prepareManagedStartupWorkspace = + prepare; + expect(() => applyProviderManagedStartupRootRequest(input)).toThrow(/unknown protocol/u); + expect(prepare).not.toHaveBeenCalled(); + expect(calls).toHaveLength(1); + }); + + it("restores through the native owner and retires only its returned observation", () => { + const { input, calls } = delegatedFixture(); + const transaction = applyProviderManagedStartupRootRequest(input); + calls.length = 0; + expect( + finalizeProviderManagedStartupSharedState({ ...input, transaction, supervisorReady: false }), + ).toEqual({ supervisorReady: false, failure: null }); + expect(calls.map(({ command }) => command.find((value) => value.startsWith("--")))).toEqual([ + "--export-shared-restoration-profile-stdin", + "--restore-shared-state-stdin", + "--finish-shared-restoration-stdin", + ]); + expect(String(calls[0]?.input)).toBe(input.request.encodedProfile); + expect(calls[0]?.maxOutputBytes).toBe(MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES); + expect(calls[1]?.sandboxUser).toBe("998:998"); + expect(JSON.parse(String(calls[2]?.input))).toEqual({ + encodedProfile: input.request.encodedProfile, + capturedState: "private restored observation", + }); + }); + + it.each([ + "--export-shared-restoration-profile-stdin", + "--restore-shared-state-stdin", + "--finish-shared-restoration-stdin", + ])("retains recovery without root fallback when %s fails", (action) => { + const { input, calls, failures } = delegatedFixture(); + const transaction = applyProviderManagedStartupRootRequest(input); + calls.length = 0; + failures.add(action); + expect(() => + finalizeProviderManagedStartupSharedState({ ...input, transaction, supervisorReady: false }), + ).toThrow(/recovery receipt are retained/u); + expect(calls.at(-1)?.command).toContain(action); + expect(calls.flatMap((call) => call.command)).not.toContain( + "--rollback-shared-state-transaction", + ); + }); +}); + describe("provider-owned managed startup root application", () => { + it.each([ + ["openclaw", { uid: 998, gid: 998, mode: 0o755 }], + ["hermes", { uid: 998, gid: 999, mode: 0o755 }], + ["langchain-deepagents-code", { uid: 0, gid: 999, mode: 0o1775 }], + ["pi", { uid: 999, gid: 999, mode: 0o755 }], + ] as const)( + "prepares %s workspace metadata before the first root application", + (agent, workspace) => { + const { request, runtimeProvider } = createDockerRootApplyFixture( + () => ({ + resourceHandle: CONTAINER_ID, + }), + agent, + ); + const control = runtimeProvider.lifecycle.privilegedSandboxControl; + const prepare = vi.fn(); + control.prepareManagedStartupWorkspace = prepare; + applyProviderManagedStartupRootRequest({ + runtimeProvider, + sandboxName: SANDBOX_NAME, + sandboxId: SANDBOX_ID, + bootstrapIdentity: "c".repeat(64), + request, + environment: {}, + }); + expect(prepare).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + expectedResourceHandle: CONTAINER_ID, + workspace, + }), + ); + expect(prepare.mock.invocationCallOrder[0]).toBeLessThan( + vi.mocked(control.execute).mock.invocationCallOrder[1]!, + ); + }, + ); + + it("rejects a provider without workspace preparation before root application", () => { + const { request, runtimeProvider } = createDockerRootApplyFixture(() => ({ + resourceHandle: CONTAINER_ID, + })); + const control = runtimeProvider.lifecycle.privilegedSandboxControl; + delete control.prepareManagedStartupWorkspace; + expect(() => + applyProviderManagedStartupRootRequest({ + runtimeProvider, + request, + sandboxName: SANDBOX_NAME, + sandboxId: SANDBOX_ID, + bootstrapIdentity: "c".repeat(64), + environment: {}, + }), + ).toThrow(/cannot prepare the managed startup workspace/u); + expect(control.execute).toHaveBeenCalledOnce(); + expect(vi.mocked(control.execute).mock.calls[0]?.[0].command).toContain("--startup-protocol"); + }); + + it("does not start root application after workspace preparation fails", () => { + const { request, runtimeProvider } = createDockerRootApplyFixture(() => ({ + resourceHandle: CONTAINER_ID, + })); + const control = runtimeProvider.lifecycle.privilegedSandboxControl; + control.prepareManagedStartupWorkspace = () => { + throw new Error("workspace changed"); + }; + expect(() => + applyProviderManagedStartupRootRequest({ + runtimeProvider, + sandboxName: SANDBOX_NAME, + sandboxId: SANDBOX_ID, + bootstrapIdentity: "c".repeat(64), + request, + environment: {}, + }), + ).toThrow(/workspace changed/u); + expect(control.execute).toHaveBeenCalledOnce(); + expect(vi.mocked(control.execute).mock.calls[0]?.[0].command).toContain("--startup-protocol"); + }); it("waits for the exact OpenShell container to appear after create returns", () => { const resolveTarget = vi .fn<() => { resourceHandle: string }>() @@ -192,6 +454,7 @@ describe("provider-owned managed startup root application", () => { const calls: Array<{ args: readonly string[]; input?: Buffer }> = []; let committed = false; let releaseAttempts = 0; + let workspace = { uid: 0, gid: 0, mode: 0o700, dev: "1", ino: "2", mtime: 100 }; const inspect = JSON.stringify([ { Id: CONTAINER_ID, @@ -213,11 +476,18 @@ describe("provider-owned managed startup root application", () => { const capture = vi.fn((args: readonly string[], _timeoutMs?: number, input?: Buffer) => { calls.push({ args, ...(input ? { input } : {}) }); const operation = [ + "--startup-protocol", "--shared-state-transaction-status", "--commit-shared-state-transaction", "--release-startup-hold", ].find((candidate) => args.includes(candidate)); switch (operation) { + case "--startup-protocol": + return { + status: 1, + stdout: "", + stderr: "usage: managed-startup-image-runtime --release-startup-hold", + }; case "--shared-state-transaction-status": return { status: 0, stdout: committed ? "committed\n" : "pending\n", stderr: "" }; case "--commit-shared-state-transaction": @@ -232,6 +502,11 @@ describe("provider-owned managed startup root application", () => { break; } switch (`${String(args[0])}:${String(args[1])}`) { + case "container:cp": + workspace = { ...workspace, uid: 998, gid: 998, mode: 0o755 }; + return { status: 0, stdout: "", stderr: "" }; + case "container:exec": + return { status: 0, stdout: JSON.stringify(workspace), stderr: "" }; case "ps:--all": return { status: 0, stdout: `${CONTAINER_ID}\n`, stderr: "" }; case "container:inspect": @@ -271,6 +546,20 @@ describe("provider-owned managed startup root application", () => { environment: {}, }); + const copyIndex = calls.findIndex(({ args }) => args[1] === "cp"); + expect(copyIndex).toBeGreaterThan(0); + expect(calls[copyIndex]?.args).toEqual([ + "container", + "cp", + "--archive=false", + "-", + `${CONTAINER_ID}:/`, + ]); + expect(calls[copyIndex]?.input?.length).toBe(1536); + expect(copyIndex).toBeLessThan( + calls.findIndex(({ args }) => args.includes("--apply-root-stdin")), + ); + expect(workspace).toEqual({ uid: 998, gid: 998, mode: 0o755, dev: "1", ino: "2", mtime: 100 }); expect(transaction).toMatchObject({ containerId: CONTAINER_ID, image: IMAGE_ID, @@ -348,11 +637,19 @@ describe("provider-owned managed startup root application", () => { })); const execute = vi.fn((input: { readonly command: readonly string[] }) => { const operation = [ + "--startup-protocol", "--apply-root-stdin", "--commit-shared-state-transaction", "--release-startup-hold", ].find((candidate) => input.command.includes(candidate)); switch (`${String(operation)}:${String(input.command.includes("--bootstrap-identity"))}`) { + case "--startup-protocol:false": + case "--startup-protocol:true": + return { + status: 1, + stdout: "", + stderr: "usage: managed-startup-image-runtime --apply-root-stdin", + }; case "--apply-root-stdin:true": return { status: 1, @@ -373,6 +670,7 @@ describe("provider-owned managed startup root application", () => { supported: true, privilegedSandboxControl: { resolveTarget: () => ({ resourceHandle: CONTAINER_ID }), + prepareManagedStartupWorkspace: vi.fn(), execute, }, }, @@ -417,8 +715,8 @@ describe("provider-owned managed startup root application", () => { profileFingerprint: request.profileFingerprint, }); - expect(execute).toHaveBeenCalledTimes(3); - expect(execute.mock.calls[2]?.[0].command).not.toContain("--bootstrap-identity"); + expect(execute).toHaveBeenCalledTimes(4); + expect(execute.mock.calls[3]?.[0].command).not.toContain("--bootstrap-identity"); expect( execute.mock.calls.some(([input]) => input.command.includes("--release-startup-hold")), ).toBe(false); diff --git a/src/lib/onboard/managed-startup/provider-root-apply.ts b/src/lib/onboard/managed-startup/provider-root-apply.ts index ff323f70ac5..87ecf4112c4 100644 --- a/src/lib/onboard/managed-startup/provider-root-apply.ts +++ b/src/lib/onboard/managed-startup/provider-root-apply.ts @@ -15,7 +15,13 @@ import type { import type { SandboxEntry } from "../../state/registry/types"; import { DirectSandboxContainerNotFoundError } from "../runtime-provider/privileged-sandbox-control-errors"; import { waitUntil } from "../readiness-wait"; -import { MANAGED_STARTUP_RUNTIME_EXECUTABLE } from "./image-runtime"; +import { + MANAGED_STARTUP_RUNTIME_EXECUTABLE, + MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES, +} from "./image-runtime"; +import { MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES } from "./shared-state-transaction"; +import { managedImageRuntimeIdentity } from "../managed-image/agents"; +import { managedStartupWorkspaceRoot } from "./state-roots"; import { type ManagedStartupRootApplyRequest, selectManagedStartupApplicationRuntimeEnvironment, @@ -48,6 +54,8 @@ export interface ProviderManagedStartupTransaction { readonly containerId: string; readonly image: string; readonly protocol: "identity-bound" | "legacy-unbound"; + /** In-memory recovery input; private captured files never enter the transaction. */ + readonly delegatedProfile?: string; readonly providerId: "docker" | "podman"; } @@ -230,7 +238,11 @@ function inspectExactCreatedRuntime( function executeExact( runtime: ProviderManagedStartupRuntime, command: readonly string[], - options: { readonly input?: Buffer; readonly timeoutMs: number }, + options: { + readonly input?: Buffer; + readonly timeoutMs: number; + readonly maxOutputBytes?: number; + }, ) { return runtime.control.execute({ registeredSandboxNames: [runtime.sandbox.name], @@ -241,9 +253,232 @@ function executeExact( expectedResourceHandle: runtime.transaction.containerId, sanitizeEnvironment: false, timeoutMs: options.timeoutMs, + ...(options.maxOutputBytes ? { maxOutputBytes: options.maxOutputBytes } : {}), + }); +} + +function delegatedCommand( + runtime: ProviderManagedStartupRuntime, + action: string, + environment: readonly string[] = [], + replay = false, +): readonly string[] { + return [ + "/usr/bin/env", + "-i", + ...FIXED_ROOT_ENV, + ...environment, + "/usr/local/bin/node", + MANAGED_STARTUP_RUNTIME_EXECUTABLE, + action, + "--agent", + runtime.transaction.agent, + "--bootstrap-identity", + runtime.transaction.bootstrapIdentity, + ...(replay ? ["--replay"] : []), + ]; +} + +function executeOwner( + runtime: ProviderManagedStartupRuntime, + command: readonly string[], + input: Buffer, + maxOutputBytes: number, +) { + if (!runtime.control.executeAsSandboxUser) + throw new Error("Managed startup provider cannot execute as the native state owner."); + const identity = managedImageRuntimeIdentity(runtime.transaction.agent); + return runtime.control.executeAsSandboxUser({ + registeredSandboxNames: [runtime.sandbox.name], + sandbox: runtime.sandbox, + sandboxName: runtime.sandbox.name, + command, + input, + sandboxUser: `${identity.uid}:${identity.gid}`, + expectedResourceHandle: runtime.transaction.containerId, + sanitizeEnvironment: false, + timeoutMs: ROOT_APPLY_TIMEOUT_MS, + maxOutputBytes, }); } +function requirePrivateResult( + result: ReturnType, + maxOutputBytes: number, +): string { + // These outputs can contain private configuration. Never include command details in errors. + if (result.status !== 0 || result.error || result.signal || result.stdout.length > maxOutputBytes) + throw new Error("Private managed-startup operation did not complete."); + return String(result.stdout); +} + +function usesDelegatedStartup(runtime: ProviderManagedStartupRuntime): boolean { + const probe = executeExact( + runtime, + [ + "/usr/bin/env", + "-i", + ...FIXED_ROOT_ENV, + "/usr/local/bin/node", + MANAGED_STARTUP_RUNTIME_EXECUTABLE, + "--startup-protocol", + ], + { timeoutMs: 30_000 }, + ); + if (probe.error || probe.signal) throw new Error("Managed startup protocol inspection failed."); + if (probe.status === 0 && String(probe.stdout).trim() === "delegated-owner-v1") return true; + if (probe.status !== 0 && String(probe.stderr).includes("usage: managed-startup-image-runtime")) + return false; + throw new Error("Managed startup image returned an unknown protocol."); +} + +function applyDelegatedStartup( + runtime: ProviderManagedStartupRuntime, + request: ManagedStartupRootApplyRequest, + environment: readonly string[], +): ProviderManagedStartupTransaction { + const transaction = { ...runtime.transaction, delegatedProfile: request.encodedProfile }; + runtime = { ...runtime, transaction }; + let phase = "transaction preparation"; + try { + if (!runtime.control.executeAsSandboxUser) throw new Error("Native execution unavailable."); + const serialized = serializeManagedStartupRootApplyRequest(request); + const input = Buffer.from(serialized, "utf8"); + const status = requirePrivateResult( + executeExact(runtime, delegatedCommand(runtime, "--prepare-delegated-transaction-stdin"), { + input, + timeoutMs: 30_000, + }), + 1024, + ).trim(); + if (status === "none") { + phase = "native state capture"; + const capturedState = requirePrivateResult( + executeOwner( + runtime, + delegatedCommand(runtime, "--capture-shared-state-stdin"), + input, + MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES, + ), + MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES, + ); + phase = "protected snapshot admission"; + const admitted = requirePrivateResult( + executeExact( + runtime, + delegatedCommand(runtime, "--begin-shared-state-from-capture-stdin"), + { + input: Buffer.from( + JSON.stringify({ encodedProfile: request.encodedProfile, capturedState }), + ), + timeoutMs: 30_000, + }, + ), + 1024, + ).trim(); + if (admitted !== "created" && admitted !== "pending") + throw new Error("Invalid snapshot status."); + } else if (status !== "pending" && status !== "committed") + throw new Error("Invalid transaction status."); + phase = "root preparation"; + const prepared = requirePrivateResult( + executeExact( + runtime, + delegatedCommand(runtime, "--prepare-delegated-startup-stdin", environment), + { + input, + timeoutMs: ROOT_APPLY_TIMEOUT_MS, + }, + ), + 1024, + ).trim(); + if (prepared !== "prepared" && prepared !== "already-committed") + throw new Error("Invalid preparation status."); + phase = "native generation and validation"; + const ownerResult = requirePrivateResult( + executeOwner( + runtime, + delegatedCommand( + runtime, + "--apply-owner-startup-stdin", + environment, + prepared === "already-committed", + ), + input, + MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES, + ), + MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES, + ); + phase = "root completion"; + requirePrivateResult( + executeExact( + runtime, + delegatedCommand(runtime, "--finish-delegated-startup-stdin", environment), + { + input: Buffer.from(JSON.stringify({ request: serialized, ownerResult })), + timeoutMs: ROOT_APPLY_TIMEOUT_MS, + }, + ), + 1024 * 1024, + ); + return transaction; + } catch { + const error = new Error( + `Managed startup ${phase} failed; the exact sandbox is retained for recovery.`, + ); + ( + error as Error & { managedStartupTransaction: ProviderManagedStartupTransaction } + ).managedStartupTransaction = transaction; + throw error; + } +} + +function rollbackSharedState(runtime: ProviderManagedStartupRuntime) { + const encodedProfile = runtime.transaction.delegatedProfile; + if (encodedProfile === undefined) + return executeExact(runtime, sharedStateTransactionCommand("rollback", runtime.transaction), { + timeoutMs: 30_000, + }); + try { + const capturedState = requirePrivateResult( + executeExact( + runtime, + delegatedCommand(runtime, "--export-shared-restoration-profile-stdin"), + { + input: Buffer.from(encodedProfile), + timeoutMs: 30_000, + maxOutputBytes: MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES, + }, + ), + MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES, + ); + if (capturedState.trim() !== "absent") { + const observed = requirePrivateResult( + executeOwner( + runtime, + delegatedCommand(runtime, "--restore-shared-state-stdin"), + Buffer.from(JSON.stringify({ encodedProfile, capturedState })), + MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES, + ), + MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES, + ); + const finished = requirePrivateResult( + executeExact(runtime, delegatedCommand(runtime, "--finish-shared-restoration-stdin"), { + input: Buffer.from(JSON.stringify({ encodedProfile, capturedState: observed })), + timeoutMs: 30_000, + }), + 1024, + ).trim(); + if (finished !== "restored") throw new Error("Restoration was not verified."); + } + return { status: 0, stdout: "", stderr: "", error: undefined }; + } catch { + throw new Error( + "Private managed-startup restoration failed; the exact sandbox and recovery receipt are retained.", + ); + } +} + function inspectExactTransactionRuntime(input: { readonly runtimeProvider: RuntimeProviderBundle; readonly sandboxName: string; @@ -367,6 +602,23 @@ export function applyProviderManagedStartupRootRequest( const applicationRuntimeEnvironment = Object.entries( selectManagedStartupApplicationRuntimeEnvironment(input.environment ?? process.env), ).map(([name, value]) => `${name}=${value}`); + const delegated = usesDelegatedStartup(runtime); + if (!runtime.control.prepareManagedStartupWorkspace) { + throw new Error("Runtime provider cannot prepare the managed startup workspace."); + } + runtime.control.prepareManagedStartupWorkspace({ + sandbox: runtime.sandbox, + sandboxName: input.sandboxName, + registeredSandboxNames: [input.sandboxName], + expectedResourceHandle: runtime.transaction.containerId, + timeoutMs: 30_000, + workspace: managedStartupWorkspaceRoot({ + agent: input.request.agent, + agentIdentity: managedImageRuntimeIdentity(input.request.agent), + }), + }); + if (delegated) + return applyDelegatedStartup(runtime, input.request, applicationRuntimeEnvironment); const command = rootApplyCommand( input.request.agent, applicationRuntimeEnvironment, @@ -455,11 +707,7 @@ export function finalizeProviderManagedStartupSharedState(input: { `${failure.message}. Commit completion is ambiguous; the exact sandbox is retained for recovery.`, ); } - const rollback = executeExact( - runtime, - sharedStateTransactionCommand("rollback", input.transaction), - { timeoutMs: 30_000 }, - ); + const rollback = rollbackSharedState(runtime); if (rollback.status !== 0 || rollback.error) { throw new Error( `Managed-startup shared-state commit failed and exact in-sandbox rollback did not complete: ${commandDetail(rollback)}`, @@ -467,11 +715,7 @@ export function finalizeProviderManagedStartupSharedState(input: { } return { supervisorReady: false, failure }; } - const rollback = executeExact( - runtime, - sharedStateTransactionCommand("rollback", input.transaction), - { timeoutMs: 30_000 }, - ); + const rollback = rollbackSharedState(runtime); if (rollback.status !== 0 || rollback.error) { throw new Error(`Exact in-sandbox managed-startup rollback failed: ${commandDetail(rollback)}`); } diff --git a/src/lib/onboard/managed-startup/shared-state-transaction.ts b/src/lib/onboard/managed-startup/shared-state-transaction.ts index 448e481143b..38b43dcd6e3 100644 --- a/src/lib/onboard/managed-startup/shared-state-transaction.ts +++ b/src/lib/onboard/managed-startup/shared-state-transaction.ts @@ -23,6 +23,9 @@ const MAX_TRANSACTION_FILE_BYTES = 8 * 1024 * 1024; const MAX_TRANSACTION_TOTAL_BYTES = 32 * 1024 * 1024; const MAX_MANIFEST_BYTES = 256 * 1024; const MAX_COMMIT_RECEIPT_BYTES = 4096; +// Base64 of the existing backup budget, canonical manifest and JSON framing. +export const MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES = + 4 * Math.ceil(MAX_TRANSACTION_TOTAL_BYTES / 3) + MAX_MANIFEST_BYTES * 2; const TRANSACTION_PARENT_DIRECTORY_MODE = 0o755; const TRANSACTION_DIRECTORY_MODE = 0o700; const TRANSACTION_FILE_MODE = 0o400; diff --git a/src/lib/onboard/managed-workload-rebuild-transaction.test.ts b/src/lib/onboard/managed-workload-rebuild-transaction.test.ts index 53b1361d1ae..57cf11d7a6e 100644 --- a/src/lib/onboard/managed-workload-rebuild-transaction.test.ts +++ b/src/lib/onboard/managed-workload-rebuild-transaction.test.ts @@ -232,6 +232,7 @@ function bundle(providerId: string): RuntimeProviderBundle { sandboxNamespace: "scoped", hostGatewayIp: null, includeSupervisorBin: true, + driverConfigLayout: "split-supervisor", processOwnership: "scoped-namespace", }, network: { diff --git a/src/lib/onboard/managed-workload/hermes-state-volume.test.ts b/src/lib/onboard/managed-workload/hermes-state-volume.test.ts index 6b3d079221b..ce30e5280e4 100644 --- a/src/lib/onboard/managed-workload/hermes-state-volume.test.ts +++ b/src/lib/onboard/managed-workload/hermes-state-volume.test.ts @@ -63,7 +63,8 @@ describe("managed Hermes state volume", () => { }); exitCleanup!(); - expect(docker.volume).toBeNull(); + expect(docker.volume).not.toBeNull(); + expect(docker.calls.filter((args) => args[0] === "rm")).toEqual([]); expect(unregister).not.toHaveBeenCalled(); }); diff --git a/src/lib/onboard/managed-workload/managed-state-volumes-projected.test.ts b/src/lib/onboard/managed-workload/managed-state-volumes-projected.test.ts new file mode 100644 index 00000000000..6311ed914f4 --- /dev/null +++ b/src/lib/onboard/managed-workload/managed-state-volumes-projected.test.ts @@ -0,0 +1,293 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, expect, it, vi } from "vitest"; +import { createHermesStateVolumeDockerHarness } from "../__test-helpers__/hermes-state-volume"; +import { managedStartupStateRoots } from "../managed-startup/state-roots"; +import { managedImageRuntimeIdentity } from "../managed-image/agents"; +import { prepareManagedStateVolumes } from "./managed-state-volumes"; +import { createManagedStateVolumeOnboardLifecycle } from "./onboard-orchestration"; + +vi.mock("../openshell-version", async (original) => ({ + ...(await original()), + SUPPORTED_OPENSHELL_FALLBACK_VERSION: "0.1.3", +})); + +const roots = managedStartupStateRoots({ + agent: "openclaw", + sandboxName: "alpha", + agentIdentity: managedImageRuntimeIdentity("openclaw"), +}); + +afterEach(() => vi.unstubAllEnvs()); + +it.each([undefined, ""])("uses the default workspace when the environment is %j", (workspace) => { + vi.stubEnv("OPENSHELL_WORKSPACE", workspace); + const docker = createHermesStateVolumeDockerHarness(); + prepareManagedStateVolumes( + { roots }, + { runContainerEngine: docker.runDocker, registerExitCleanup: () => () => undefined }, + ); + expect(docker.volume?.labels["openshell.ai/sandbox-attachable-workspace"]).toBe("default"); +}); + +it("rejects an explicitly empty workspace before accessing volumes", () => { + const docker = createHermesStateVolumeDockerHarness(); + expect(() => + prepareManagedStateVolumes({ roots }, { workspace: "", runContainerEngine: docker.runDocker }), + ).toThrow("requires an exact OpenShell workspace"); + expect(docker.runDocker).not.toHaveBeenCalled(); +}); + +it("creates workspace-approved volumes and retains incomplete creation for explicit cleanup", () => { + const docker = createHermesStateVolumeDockerHarness(); + const scope = prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + registerExitCleanup: () => () => undefined, + }, + ); + expect(docker.volume?.labels).toEqual({ + ...roots[0]!.ownershipLabels, + "openshell.ai/sandbox-attachable": "true", + "openshell.ai/sandbox-attachable-workspace": "qualification", + "io.nvidia.nemoclaw.state-volume.creation-id": expect.any(String), + }); + expect(scope?.reused).toEqual([false]); + expect(scope?.cleanupIncompleteCreate()).toMatchObject([{ status: "retained" }]); + expect(docker.volume).not.toBeNull(); + expect(docker.calls.filter(([command]) => command === "rm")).toEqual([]); +}); + +it("binds lifecycle volume approval to the selected workspace", () => { + vi.stubEnv("OPENSHELL_WORKSPACE", "ambient"); + const docker = createHermesStateVolumeDockerHarness(); + createManagedStateVolumeOnboardLifecycle( + { + roots, + workspace: "selected", + runtimeProvider: { + identity: { id: "docker" }, + workload: { managedStateMountDriverId: "docker" }, + containerEngine: { + supported: true, + identities: [ + { operation: "sandbox-lifecycle", engineId: "docker", displayName: "Docker" }, + ], + }, + } as never, + }, + { runContainerEngine: docker.runDocker }, + ); + expect(docker.volume?.labels["openshell.ai/sandbox-attachable-workspace"]).toBe("selected"); +}); + +it.each>([ + {}, + { + "openshell.ai/sandbox-attachable": "true", + "openshell.ai/sandbox-attachable-workspace": "another-workspace", + }, +])("preserves retained state without matching workspace approval %j", (approval) => { + const initial = { + name: roots[0]!.resourceIdentity, + labels: { ...roots[0]!.ownershipLabels, ...approval }, + }; + const docker = createHermesStateVolumeDockerHarness(initial); + expect(() => + prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + }, + ), + ).toThrow("Retained data was not changed"); + expect(docker.volume).toEqual(initial); + expect(docker.calls.map(([command]) => command)).toEqual(["inspect", "inspect"]); +}); + +it("reuses approved retained state without removing it during rollback", () => { + const initial = { + name: roots[0]!.resourceIdentity, + labels: { + ...roots[0]!.ownershipLabels, + "openshell.ai/sandbox-attachable": "true", + "openshell.ai/sandbox-attachable-workspace": "qualification", + }, + }; + const docker = createHermesStateVolumeDockerHarness(initial); + const scope = prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + }, + ); + expect(scope?.reused).toEqual([true]); + expect(scope?.cleanupIncompleteCreate()).toEqual([]); + expect(docker.volume).toEqual(initial); +}); + +it("rejects a competing volume from another workspace without deleting it", () => { + const initial = { + name: roots[0]!.resourceIdentity, + labels: { + ...roots[0]!.ownershipLabels, + "openshell.ai/sandbox-attachable": "true", + "openshell.ai/sandbox-attachable-workspace": "another-workspace", + "io.nvidia.nemoclaw.state-volume.creation-id": "competing-invocation", + }, + }; + const docker = createHermesStateVolumeDockerHarness(initial); + // Another invocation creates this volume between the first inspect and create. + docker.runDocker.mockImplementationOnce((args) => { + expect(args).toEqual(["inspect", "--format", "{{json .}}", initial.name]); + return { status: 1, stderr: "no such volume" }; + }); + const registerExitCleanup = vi.fn(() => () => undefined); + const prepare = () => + prepareManagedStateVolumes( + { roots }, + { workspace: "qualification", runContainerEngine: docker.runDocker, registerExitCleanup }, + ); + expect(prepare).toThrow("Retained data was not changed"); + expect(docker.volume).toEqual(initial); + expect(registerExitCleanup).not.toHaveBeenCalled(); + expect(docker.calls.filter(([command]) => command === "rm")).toEqual([]); +}); + +it("reuses an approved competing volume without arming rollback", () => { + const initial = { + name: roots[0]!.resourceIdentity, + labels: { + ...roots[0]!.ownershipLabels, + "openshell.ai/sandbox-attachable": "true", + "openshell.ai/sandbox-attachable-workspace": "qualification", + "io.nvidia.nemoclaw.state-volume.creation-id": "competing-invocation", + }, + }; + const docker = createHermesStateVolumeDockerHarness(initial); + // Another invocation creates this volume between the first inspect and create. + docker.runDocker.mockImplementationOnce((args) => { + expect(args).toEqual(["inspect", "--format", "{{json .}}", initial.name]); + return { status: 1, stderr: "no such volume" }; + }); + const registerExitCleanup = vi.fn(() => () => undefined); + const prepare = () => + prepareManagedStateVolumes( + { roots }, + { workspace: "qualification", runContainerEngine: docker.runDocker, registerExitCleanup }, + ); + const scope = prepare(); + expect(scope?.reused).toEqual([true]); + expect(scope?.cleanupIncompleteCreate()).toEqual([]); + expect(docker.volume).toEqual(initial); + expect(registerExitCleanup).not.toHaveBeenCalled(); + expect(docker.calls.filter(([command]) => command === "rm")).toEqual([]); +}); + +it("preserves a replacement volume during rollback cleanup", () => { + const docker = createHermesStateVolumeDockerHarness(); + let exitCleanup: () => void = () => undefined; + const scope = prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + registerExitCleanup: (callback) => { + exitCleanup = callback; + return () => undefined; + }, + }, + ); + expect(scope?.reused).toEqual([false]); + docker.runDocker(["rm", roots[0]!.resourceIdentity]); + const competitor = prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + registerExitCleanup: () => () => undefined, + }, + ); + competitor!.commit(); + const replacement = structuredClone(docker.volume); + const callsBeforeCleanup = docker.calls.length; + expect(scope?.cleanupIncompleteCreate()).toMatchObject([{ status: "not-owned" }]); + exitCleanup(); + expect(docker.volume).toEqual(replacement); + expect(docker.calls.slice(callsBeforeCleanup).map(([command]) => command)).toEqual([ + "inspect", + "inspect", + ]); +}); + +it("preserves a replacement volume during exit cleanup", () => { + const docker = createHermesStateVolumeDockerHarness(); + let exitCleanup: () => void = () => undefined; + const scope = prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + registerExitCleanup: (callback) => { + exitCleanup = callback; + return () => undefined; + }, + }, + ); + expect(scope?.reused).toEqual([false]); + docker.runDocker(["rm", roots[0]!.resourceIdentity]); + const competitor = prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + registerExitCleanup: () => () => undefined, + }, + ); + competitor!.commit(); + const replacement = structuredClone(docker.volume); + const callsBeforeCleanup = docker.calls.length; + exitCleanup(); + expect(docker.volume).toEqual(replacement); + expect(docker.calls.slice(callsBeforeCleanup).map(([command]) => command)).toEqual(["inspect"]); +}); + +it.each(["rollback", "exit"] as const)( + "preserves replacement after the final %s cleanup inspection", + (cleanupPath) => { + const docker = createHermesStateVolumeDockerHarness(); + let exitCleanup = () => undefined as void; + const scope = prepareManagedStateVolumes( + { roots }, + { + workspace: "qualification", + runContainerEngine: docker.runDocker, + registerExitCleanup: (callback) => { + exitCleanup = callback; + return () => undefined; + }, + }, + )!; + const originalRun = docker.runDocker.getMockImplementation()!; + const replacementLabel = "io.nvidia.nemoclaw.state-volume.creation-id=competitor"; + docker.runDocker.mockImplementationOnce((args) => { + expect(args).toEqual(["inspect", "--format", "{{json .}}", roots[0]!.resourceIdentity]); + const staleObservation = originalRun(args); + originalRun(["rm", roots[0]!.resourceIdentity]); + originalRun(["create", "--label", replacementLabel, roots[0]!.resourceIdentity]); + return staleObservation; + }); + docker.runDocker.mockClear(); + const actions = { rollback: () => scope.cleanupIncompleteCreate(), exit: exitCleanup }; + actions[cleanupPath](); + expect(docker.volume?.labels).toEqual({ + "io.nvidia.nemoclaw.state-volume.creation-id": "competitor", + }); + expect(docker.runDocker.mock.calls.map(([args]) => args[0])).toEqual(["inspect"]); + }, +); diff --git a/src/lib/onboard/managed-workload/managed-state-volumes.ts b/src/lib/onboard/managed-workload/managed-state-volumes.ts index b8ecb80ecdf..3237ebdc7b6 100644 --- a/src/lib/onboard/managed-workload/managed-state-volumes.ts +++ b/src/lib/onboard/managed-workload/managed-state-volumes.ts @@ -1,11 +1,16 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { randomUUID } from "node:crypto"; + +import { OPENSHELL_DEFAULT_WORKSPACE } from "../../adapters/openshell/sandbox-ssh-host"; +import { SUPPORTED_OPENSHELL_FALLBACK_VERSION } from "../openshell-version"; import type { ManagedStartupStateRoot } from "../managed-startup/state-roots"; import type { RuntimeProviderBundle } from "../runtime-provider/contract"; const MISSING_VOLUME_PATTERN = /\bno such volume\b/iu; const COMMAND_TIMEOUT_MS = 30_000; +const CREATION_LABEL = "io.nvidia.nemoclaw.state-volume.creation-id"; export type ManagedStateVolumeMount = { readonly type: "volume"; @@ -17,7 +22,7 @@ export type ManagedStateVolumeMount = { export type ManagedStateVolumeCleanupResult = | { readonly status: "not-applicable" | "absent" | "removed" } | { - readonly status: "not-owned" | "failed"; + readonly status: "not-owned" | "failed" | "retained"; readonly detail: string; readonly volumeName: string; }; @@ -42,6 +47,7 @@ type ContainerEngineRun = ( ) => ContainerEngineRunResult; export type ManagedStateVolumeDeps = { + readonly workspace?: string; readonly runContainerEngine?: ContainerEngineRun; readonly runtimeProvider?: RuntimeProviderBundle; readonly registerExitCleanup?: (cleanup: () => void) => () => void; @@ -154,6 +160,7 @@ function inspectVolume(root: ManagedStartupStateRoot, run: ContainerEngineRun): function removeOwnedVolume( root: ManagedStartupStateRoot, run: ContainerEngineRun, + preserve = false, ): ManagedStateVolumeCleanupResult { const observation = inspectVolume(root, run); if (observation.status === "absent") return { status: "absent" }; @@ -171,6 +178,15 @@ function removeOwnedVolume( volumeName: root.resourceIdentity, }; } + // Named volumes have no immutable deletion handle. Even matching labels can + // become stale before rm, so automatic rollback must retain the named state. + if (preserve) { + return { + status: "retained", + detail: "automatic cleanup preserves named state; explicit operator cleanup is required", + volumeName: root.resourceIdentity, + }; + } const result = run(["rm", root.resourceIdentity], { ignoreError: true, suppressOutput: true, @@ -189,6 +205,19 @@ function supportsManagedStateVolumes(provider?: RuntimeProviderBundle): boolean return provider?.containerEngine.supported !== false; } +function managedStateVolumeApproval(deps: ManagedStateVolumeDeps): Record { + if (String(SUPPORTED_OPENSHELL_FALLBACK_VERSION) !== "0.1.3") return {}; + const workspace = + deps.workspace ?? (process.env.OPENSHELL_WORKSPACE || OPENSHELL_DEFAULT_WORKSPACE); + if (!workspace || workspace.trim() !== workspace || /[\0\r\n]/u.test(workspace)) { + throw new Error("Managed state volume requires an exact OpenShell workspace."); + } + return { + "openshell.ai/sandbox-attachable": "true", + "openshell.ai/sandbox-attachable-workspace": workspace, + }; +} + export function prepareManagedStateVolumes( input: { readonly roots: readonly ManagedStartupStateRoot[]; @@ -206,6 +235,8 @@ export function prepareManagedStateVolumes( : (() => { throw new Error("Managed state volumes require runtime provider authority."); })()); + const approvalLabels = managedStateVolumeApproval(deps); + const creationId = randomUUID(); const created: ManagedStartupStateRoot[] = []; const reused: boolean[] = []; try { @@ -218,9 +249,11 @@ export function prepareManagedStateVolumes( } if (before.status === "absent") { const createArgs = ["create"]; - for (const [name, value] of Object.entries(root.ownershipLabels).sort(([left], [right]) => - left.localeCompare(right), - )) { + for (const [name, value] of Object.entries({ + ...root.ownershipLabels, + ...approvalLabels, + [CREATION_LABEL]: creationId, + }).sort(([left], [right]) => left.localeCompare(right))) { createArgs.push("--label", `${name}=${value}`); } createArgs.push(root.resourceIdentity); @@ -234,9 +267,21 @@ export function prepareManagedStateVolumes( `Cannot create managed state volume '${root.resourceIdentity}': ${boundedDetail(result)}`, ); } - created.push(root); } const verified = inspectVolume(root, run); + // Named volume creation is idempotent: a competing invocation may have + // created this name after our first inspection. The creation label only + // identifies reuse; it cannot authorize deletion by a reusable name. + const createdHere = + before.status === "absent" && + verified.status === "observed" && + verified.labels[CREATION_LABEL] === creationId; + if (createdHere) { + created.push({ + ...root, + ownershipLabels: { ...root.ownershipLabels, [CREATION_LABEL]: creationId }, + }); + } if (verified.status !== "observed" || !labelsMatch(verified.labels, root.ownershipLabels)) { const detail = verified.status === "failed" @@ -246,15 +291,21 @@ export function prepareManagedStateVolumes( : "the exact NemoClaw ownership labels do not match"; throw new Error(`Cannot use managed state volume '${root.resourceIdentity}': ${detail}.`); } - reused.push(before.status === "observed"); + if (!labelsMatch(verified.labels, approvalLabels)) { + throw new Error( + "Managed state volume lacks OpenShell attachment approval for the selected workspace. " + + "Retained data was not changed; operator reconciliation is required.", + ); + } + reused.push(!createdHere); } } catch (error) { - for (const root of [...created].reverse()) removeOwnedVolume(root, run); + for (const root of [...created].reverse()) removeOwnedVolume(root, run, true); throw error; } let committed = false; const cleanup = (): readonly ManagedStateVolumeCleanupResult[] => - committed ? [] : [...created].reverse().map((root) => removeOwnedVolume(root, run)); + committed ? [] : [...created].reverse().map((root) => removeOwnedVolume(root, run, true)); const unregisterExitCleanup = created.length > 0 ? (deps.registerExitCleanup ?? defaultRegisterExitCleanup)(() => { diff --git a/src/lib/onboard/managed-workload/onboard-orchestration.test.ts b/src/lib/onboard/managed-workload/onboard-orchestration.test.ts index da851c41004..bb5159637a1 100644 --- a/src/lib/onboard/managed-workload/onboard-orchestration.test.ts +++ b/src/lib/onboard/managed-workload/onboard-orchestration.test.ts @@ -492,7 +492,7 @@ describe("managed workload onboard orchestration", () => { expect(ensurePreparedProfile).not.toHaveBeenCalled(); }); - it("keeps failure cleanup armed until the caller commits registration", () => { + it("preserves prepared named state when exit cleanup runs before registration", () => { const docker = createHermesStateVolumeDockerHarness(); let exitCleanup: (() => void) | null = null; @@ -513,6 +513,7 @@ describe("managed workload onboard orchestration", () => { ], }, } as never, + workspace: "default", }, { runContainerEngine: docker.runDocker as never, @@ -532,8 +533,8 @@ describe("managed workload onboard orchestration", () => { }); exitCleanup!(); - expect(docker.volume).toBeNull(); - expect(docker.calls.some((args) => args[0] === "rm")).toBe(true); + expect(docker.volume?.name).toBe("nemoclaw-hermes-state-v1-alpha"); + expect(docker.calls.some((args) => args[0] === "rm")).toBe(false); }); it("keeps deferred Hermes Portable launch on a semantic create request", () => { diff --git a/src/lib/onboard/managed-workload/onboard-orchestration.ts b/src/lib/onboard/managed-workload/onboard-orchestration.ts index 97eaa54e420..64f5cf8ec1f 100644 --- a/src/lib/onboard/managed-workload/onboard-orchestration.ts +++ b/src/lib/onboard/managed-workload/onboard-orchestration.ts @@ -148,6 +148,7 @@ export function createManagedStateVolumeOnboardLifecycle( input: { readonly roots: readonly import("../managed-startup/state-roots").ManagedStartupStateRoot[]; readonly runtimeProvider: RuntimeProviderBundle | null; + readonly workspace: string; }, deps: ManagedStateVolumeDeps = {}, ): ManagedStateVolumeOnboardLifecycle { @@ -155,6 +156,7 @@ export function createManagedStateVolumeOnboardLifecycle( { roots: input.roots }, { ...deps, + workspace: input.workspace, ...(input.runtimeProvider ? { runtimeProvider: input.runtimeProvider } : {}), }, ); diff --git a/src/lib/onboard/runtime-provider/contract.ts b/src/lib/onboard/runtime-provider/contract.ts index 67f1121b4c4..f6d05f0fd2e 100644 --- a/src/lib/onboard/runtime-provider/contract.ts +++ b/src/lib/onboard/runtime-provider/contract.ts @@ -159,6 +159,7 @@ export interface RuntimeProviderGatewayHostRuntime { readonly sandboxNamespace: "scoped" | "omitted"; readonly hostGatewayIp: string | null; readonly includeSupervisorBin: boolean; + readonly driverConfigLayout: "split-supervisor" | "inline-supervisor"; readonly processOwnership: "scoped-namespace" | "runtime-marker"; }; readonly network: { @@ -450,6 +451,17 @@ export interface RuntimeProviderStoppedNativeHomeCleanupInput { } export interface RuntimeProviderPrivilegedSandboxControl { + /** Restore only the declared workspace-directory metadata before managed bootstrap. */ + prepareManagedStartupWorkspace?( + input: Pick< + RuntimeProviderPrivilegedSandboxCommandInput, + "sandbox" | "sandboxName" | "registeredSandboxNames" | "timeoutMs" + > & { + readonly expectedResourceHandle: string; + readonly workspace: { readonly uid: number; readonly gid: number; readonly mode: number }; + }, + ): void; + resolveTarget( input: Pick< RuntimeProviderPrivilegedSandboxCommandInput, diff --git a/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.test.ts b/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.test.ts index 9c9527d5b10..c568481aa5c 100644 --- a/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.test.ts +++ b/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.test.ts @@ -1,21 +1,27 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ dockerCapture: vi.fn(), dockerSpawnSync: vi.fn(), + portableTarget: vi.fn(), })); vi.mock("../../adapters/docker/run", () => ({ dockerCapture: mocks.dockerCapture })); vi.mock("../../adapters/docker/exec", () => ({ dockerSpawnSync: mocks.dockerSpawnSync })); +vi.mock("../experimental/portable-demo-lifecycle", async (original) => ({ + ...(await original()), + resolvePortableDemoPrivilegedExecTarget: mocks.portableTarget, +})); + import { createDockerPrivilegedSandboxControl } from "./docker-privileged-sandbox-control"; const CONTAINER_ID = "a".repeat(64); -function exactInspect(running = true, sandboxName = "alpha"): string { +function exactInspect(running = true, sandboxName = "alpha", role?: string): string { return [ CONTAINER_ID, `/openshell-default--${sandboxName}-sandbox-id`, @@ -23,13 +29,173 @@ function exactInspect(running = true, sandboxName = "alpha"): string { JSON.stringify({ "openshell.ai/managed-by": "openshell", "openshell.ai/sandbox-name": sandboxName, + ...(role === undefined ? {} : { "openshell.ai/isolation-role": role }), }), ].join("\t"); } describe("Docker privileged exact target", () => { + beforeEach(() => { + vi.resetAllMocks(); + mocks.portableTarget.mockReturnValue(null); + }); + + it("restores workspace metadata through exact-ID archive copy without adding capabilities", () => { + mocks.dockerCapture.mockReturnValue(exactInspect()); + const observed = { uid: 999, gid: 999, mode: 0o700, dev: "1", ino: "2", mtime: 3 }; + mocks.dockerSpawnSync + .mockReturnValueOnce({ + status: 0, + signal: null, + stdout: Buffer.from(JSON.stringify(observed)), + stderr: Buffer.alloc(0), + }) + .mockReturnValueOnce({ + status: 0, + signal: null, + stdout: Buffer.alloc(0), + stderr: Buffer.alloc(0), + }) + .mockReturnValueOnce({ + status: 0, + signal: null, + stdout: Buffer.from(JSON.stringify({ ...observed, uid: 0, mode: 0o1775 })), + stderr: Buffer.alloc(0), + }); + createDockerPrivilegedSandboxControl().prepareManagedStartupWorkspace!({ + sandbox: { name: "alpha", openshellDriver: "docker" }, + sandboxName: "alpha", + registeredSandboxNames: ["alpha"], + expectedResourceHandle: CONTAINER_ID, + timeoutMs: 30000, + workspace: { uid: 0, gid: 999, mode: 0o1775 }, + }); + expect(mocks.dockerCapture).toHaveBeenCalledTimes(3); + const [copyArgs, copyOptions] = mocks.dockerSpawnSync.mock.calls[1]!; + expect(copyArgs).toEqual(["cp", "-", `${CONTAINER_ID}:/`]); + expect(copyOptions.input.byteLength).toBe(1536); + const allArguments = mocks.dockerSpawnSync.mock.calls.flatMap(([args]) => args); + expect(allArguments).not.toContain("--privileged"); + expect(allArguments).not.toContain("--cap-add"); + }); + + it("refuses the workspace copy when the pinned runtime stops after observation", () => { + mocks.dockerCapture.mockReturnValueOnce(exactInspect()).mockReturnValue(exactInspect(false)); + mocks.dockerSpawnSync.mockReturnValue({ + status: 0, + signal: null, + stdout: Buffer.from( + JSON.stringify({ uid: 999, gid: 999, mode: 0o700, dev: "1", ino: "2", mtime: 3 }), + ), + stderr: Buffer.alloc(0), + }); + expect(() => + createDockerPrivilegedSandboxControl().prepareManagedStartupWorkspace!({ + sandbox: { name: "alpha", openshellDriver: "docker" }, + sandboxName: "alpha", + registeredSandboxNames: ["alpha"], + expectedResourceHandle: CONTAINER_ID, + timeoutMs: 30000, + workspace: { uid: 0, gid: 999, mode: 0o1775 }, + }), + ).toThrow(/identity changed/u); + expect(mocks.dockerSpawnSync).toHaveBeenCalledOnce(); + }); + + function portableWorkspaceFixture() { + const observed = { uid: 999, gid: 999, mode: 0o700, dev: "1", ino: "2", mtime: 3 }; + mocks.dockerSpawnSync + .mockReturnValueOnce({ + status: 0, + signal: null, + stdout: Buffer.from(JSON.stringify(observed)), + stderr: Buffer.alloc(0), + }) + .mockReturnValueOnce({ + status: 0, + signal: null, + stdout: Buffer.alloc(0), + stderr: Buffer.alloc(0), + }) + .mockReturnValueOnce({ + status: 0, + signal: null, + stdout: Buffer.from(JSON.stringify({ ...observed, uid: 0, mode: 0o1775 })), + stderr: Buffer.alloc(0), + }); + mocks.dockerCapture.mockImplementation(() => { + throw new Error("default Docker daemon must not be accessed"); + }); + const portable = { + containerId: CONTAINER_ID, + dockerHost: "unix:///run/user/1000/podman/podman.sock", + assertRuntimeAuthority: vi.fn(), + }; + mocks.portableTarget.mockReturnValue(portable); + const input = { + sandbox: { name: "alpha", openshellDriver: "docker" }, + sandboxName: "alpha", + registeredSandboxNames: ["alpha"], + expectedResourceHandle: CONTAINER_ID, + timeoutMs: 1234, + workspace: { uid: 0, gid: 999, mode: 0o1775 as const }, + }; + return { input, portable }; + } + + it("copies workspace metadata through the same receipt-owned endpoint as both reads", () => { + const { input, portable } = portableWorkspaceFixture(); + createDockerPrivilegedSandboxControl().prepareManagedStartupWorkspace!(input); + expect(mocks.dockerCapture).not.toHaveBeenCalled(); + expect(portable.assertRuntimeAuthority).toHaveBeenCalledTimes(3); + expect(mocks.dockerSpawnSync.mock.calls.map(([args]) => args.slice(0, 2))).toEqual([ + ["--host", portable.dockerHost], + ["--host", portable.dockerHost], + ["--host", portable.dockerHost], + ]); + const [copyArgs, copyOptions] = mocks.dockerSpawnSync.mock.calls[1]!; + expect(copyArgs).toEqual(["--host", portable.dockerHost, "cp", "-", `${CONTAINER_ID}:/`]); + expect(copyOptions).toEqual({ + encoding: null, + input: expect.any(Buffer), + maxBuffer: 4096, + stdio: ["pipe", "pipe", "pipe"], + timeout: 1234, + }); + expect(copyOptions.input.byteLength).toBe(1536); + }); + + it("refuses a portable workspace copy if the receipt resolves a replacement container", () => { + const { input, portable } = portableWorkspaceFixture(); + mocks.portableTarget + .mockReturnValueOnce(portable) + .mockReturnValue({ ...portable, containerId: "b".repeat(64) }); + expect(() => + createDockerPrivilegedSandboxControl().prepareManagedStartupWorkspace!(input), + ).toThrow(/identity changed/u); + expect(mocks.dockerSpawnSync).toHaveBeenCalledOnce(); + expect(mocks.dockerCapture).not.toHaveBeenCalled(); + }); + + it("refuses a portable workspace copy if receipt authority changes after the first read", () => { + const { input, portable } = portableWorkspaceFixture(); + portable.assertRuntimeAuthority + .mockImplementationOnce(() => undefined) + .mockImplementation(() => { + throw new Error("receipt authority changed"); + }); + expect(() => + createDockerPrivilegedSandboxControl().prepareManagedStartupWorkspace!(input), + ).toThrow("receipt authority changed"); + expect(mocks.dockerSpawnSync).toHaveBeenCalledOnce(); + expect(mocks.dockerCapture).not.toHaveBeenCalled(); + }); + it("bounds direct target discovery by the caller's remaining deadline", () => { - mocks.dockerCapture.mockReturnValue(`${CONTAINER_ID}\topenshell-default--alpha-sandbox-id`); + mocks.dockerCapture.mockReturnValue( + `${CONTAINER_ID}\topenshell-default--alpha-sandbox-id\tsandbox\n` + + `${"b".repeat(64)}\topenshell-default--alpha-sandbox-id-supervisor\tsupervisor`, + ); const control = createDockerPrivilegedSandboxControl(); expect( @@ -40,14 +206,48 @@ describe("Docker privileged exact target", () => { timeoutMs: 1_250, }), ).toEqual({ providerId: "docker", resourceHandle: CONTAINER_ID }); - expect(mocks.dockerCapture).toHaveBeenCalledWith(expect.any(Array), { timeout: 1_250 }); + expect(mocks.dockerCapture).toHaveBeenCalledWith( + expect.arrayContaining([ + "--format", + '{{.ID}}\t{{.Names}}\t{{.Label "openshell.ai/isolation-role"}}', + ]), + { timeout: 1_250 }, + ); }); - it("executes in the pinned running replacement without mutable-name discovery (#11905)", () => { - mocks.dockerCapture.mockReturnValue(exactInspect()); + it.each([undefined, "sandbox"])( + "executes in the pinned workload with role %s (#11905)", + (role) => { + mocks.dockerCapture.mockReturnValue(exactInspect(true, "alpha", role)); + const control = createDockerPrivilegedSandboxControl(); + + expect( + control.buildLegacyDockerArgv?.({ + sandbox: { name: "alpha", openshellDriver: "docker" }, + sandboxName: "alpha", + registeredSandboxNames: ["alpha"], + command: ["true"], + expectedResourceHandle: CONTAINER_ID, + sanitizeEnvironment: false, + }), + ).toEqual(["exec", "--user", "root", CONTAINER_ID, "true"]); + expect(mocks.dockerCapture).toHaveBeenCalledWith( + expect.arrayContaining(["inspect", CONTAINER_ID]), + expect.any(Object), + ); + }, + ); + + it.each([ + ["stopped", exactInspect(false)], + ["wrong sandbox", exactInspect(true, "replacement")], + ["supervisor", exactInspect(true, "alpha", "supervisor")], + ["unknown role", exactInspect(true, "alpha", "other")], + ])("rejects a %s pinned target", (_label, inspect) => { + mocks.dockerCapture.mockReturnValue(inspect); const control = createDockerPrivilegedSandboxControl(); - expect( + expect(() => control.buildLegacyDockerArgv?.({ sandbox: { name: "alpha", openshellDriver: "docker" }, sandboxName: "alpha", @@ -56,29 +256,93 @@ describe("Docker privileged exact target", () => { expectedResourceHandle: CONTAINER_ID, sanitizeEnvironment: false, }), - ).toEqual(["exec", "--user", "root", CONTAINER_ID, "true"]); - expect(mocks.dockerCapture).toHaveBeenCalledWith( + ).toThrow(/identity changed/u); + }); + + it("executes native-home cleanup as the pinned nonroot owner without added capabilities", () => { + mocks.dockerCapture.mockReturnValue(exactInspect()); + mocks.dockerSpawnSync.mockReturnValue({ + status: 0, + signal: null, + stdout: Buffer.from("done"), + stderr: Buffer.alloc(0), + }); + const control = createDockerPrivilegedSandboxControl(); + const input = Buffer.from("synthetic input"); + + expect( + control.executeAsSandboxUser?.({ + sandbox: { name: "alpha", openshellDriver: "docker" }, + sandboxName: "alpha", + registeredSandboxNames: ["alpha"], + expectedResourceHandle: CONTAINER_ID, + sandboxUser: "999:999", + command: ["/usr/bin/true"], + sanitizeEnvironment: true, + input, + timeoutMs: 1234, + maxOutputBytes: 4096, + }), + ).toEqual({ status: 0, signal: null, stdout: Buffer.from("done"), stderr: Buffer.alloc(0) }); + const [argv, options] = mocks.dockerSpawnSync.mock.calls[0]!; + expect(argv.slice(-4)).toEqual(["--user", "999:999", CONTAINER_ID, "/usr/bin/true"]); + expect(argv.slice(0, 2)).toEqual(["exec", "-i"]); + expect(argv).toContain("--env"); + expect(argv).not.toContain("--privileged"); + expect(argv).not.toContain("--cap-add"); + expect(options).toEqual({ + encoding: null, + input, + maxBuffer: 4096, + stdio: ["pipe", "pipe", "pipe"], + timeout: 1234, + }); + expect(mocks.dockerCapture).toHaveBeenCalledExactlyOnceWith( expect.arrayContaining(["inspect", CONTAINER_ID]), expect.any(Object), ); }); + it.each(["0:999", "999:0", "root", "sandbox", "--privileged", "999:999\n"])( + "rejects unsafe sandbox identity %j before engine access", + (sandboxUser) => { + const control = createDockerPrivilegedSandboxControl(); + expect(() => + control.executeAsSandboxUser?.({ + sandbox: { name: "alpha", openshellDriver: "docker" }, + sandboxName: "alpha", + registeredSandboxNames: ["alpha"], + expectedResourceHandle: CONTAINER_ID, + sandboxUser, + command: ["true"], + sanitizeEnvironment: true, + timeoutMs: 1000, + }), + ).toThrow(/non-root numeric UID and GID/u); + expect(mocks.dockerCapture).not.toHaveBeenCalled(); + expect(mocks.dockerSpawnSync).not.toHaveBeenCalled(); + }, + ); + it.each([ - ["stopped", exactInspect(false)], - ["wrong sandbox", exactInspect(true, "replacement")], - ])("rejects a %s pinned target", (_label, inspect) => { + exactInspect(false), + exactInspect(true, "replacement"), + exactInspect(true, "alpha", "supervisor"), + ])("refuses sandbox-user execution after target identity changes", (inspect) => { mocks.dockerCapture.mockReturnValue(inspect); const control = createDockerPrivilegedSandboxControl(); - expect(() => - control.buildLegacyDockerArgv?.({ + control.executeAsSandboxUser?.({ sandbox: { name: "alpha", openshellDriver: "docker" }, sandboxName: "alpha", registeredSandboxNames: ["alpha"], - command: ["true"], expectedResourceHandle: CONTAINER_ID, - sanitizeEnvironment: false, + sandboxUser: "999:999", + command: ["true"], + sanitizeEnvironment: true, + timeoutMs: 1000, }), ).toThrow(/identity changed/u); + expect(mocks.dockerSpawnSync).not.toHaveBeenCalled(); }); }); diff --git a/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.ts b/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.ts index 80e28a6608f..310e7873e2e 100644 --- a/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.ts +++ b/src/lib/onboard/runtime-provider/docker-privileged-sandbox-control.ts @@ -19,8 +19,12 @@ import { DirectSandboxFallbackUnavailableError, PinnedSandboxResourceIdentityChangedError, } from "./privileged-sandbox-control-errors"; -import { selectDockerPrivilegedSandboxTarget } from "./docker-privileged-sandbox-identity"; +import { + selectDockerPrivilegedSandboxTarget, + selectDockerPrivilegedSandboxTargetRecord, +} from "./docker-privileged-sandbox-identity"; import { createDockerOperationAuthority } from "./docker-operation-authority"; +import { prepareManagedWorkspaceMetadata } from "./managed-workspace-metadata"; import { clearStoppedNativeHomeWithEngine, clearStoppedSandboxStateWithEngine, @@ -33,6 +37,8 @@ import { const OPENSHELL_MANAGED_BY_LABEL = "openshell.ai/managed-by"; const OPENSHELL_MANAGED_BY_VALUE = "openshell"; const OPENSHELL_SANDBOX_NAME_LABEL = "openshell.ai/sandbox-name"; +const OPENSHELL_ISOLATION_ROLE_LABEL = "openshell.ai/isolation-role"; +const SANDBOX_CONTAINER_FORMAT = `{{.ID}}\t{{.Names}}\t{{.Label "${OPENSHELL_ISOLATION_ROLE_LABEL}"}}`; const DIRECT_SANDBOX_DISCOVERY_TIMEOUT_MS = 5000; type SandboxEntry = import("../../state/registry").SandboxEntry; @@ -54,7 +60,7 @@ function findDirectSandboxContainer( "--filter", `label=${OPENSHELL_SANDBOX_NAME_LABEL}=${sandboxName}`, "--format", - "{{.ID}}\t{{.Names}}", + SANDBOX_CONTAINER_FORMAT, ], { timeout: Math.max(1, Math.min(DIRECT_SANDBOX_DISCOVERY_TIMEOUT_MS, timeoutMs)) }, ); @@ -129,7 +135,9 @@ function resolvePinnedDockerTarget( selected !== expectedResourceHandle || running !== "true" || labels[OPENSHELL_MANAGED_BY_LABEL] !== OPENSHELL_MANAGED_BY_VALUE || - labels[OPENSHELL_SANDBOX_NAME_LABEL] !== input.sandboxName + labels[OPENSHELL_SANDBOX_NAME_LABEL] !== input.sandboxName || + (labels[OPENSHELL_ISOLATION_ROLE_LABEL] !== undefined && + labels[OPENSHELL_ISOLATION_ROLE_LABEL] !== "sandbox") ) { return refuse(); } @@ -175,8 +183,9 @@ function resolveDockerTarget( function executeDockerCommand( input: RuntimeProviderPrivilegedSandboxCommandInput, + sandboxUser?: string, ): RuntimeProviderPrivilegedSandboxCommandResult { - const argv = buildLegacyDockerArgv(input); + const argv = buildLegacyDockerArgv(input, sandboxUser); const result = dockerSpawnSync(argv, { encoding: null, input: input.input, @@ -195,6 +204,7 @@ function executeDockerCommand( function buildLegacyDockerArgv( input: Omit, + sandboxUser?: string, ): string[] { const portable = portableTarget(input.sandboxName, input.sandbox); const target = portable @@ -217,7 +227,7 @@ function buildLegacyDockerArgv( ...(input.input ? ["-i"] : []), ...environment, "--user", - portable ? "0" : "root", + sandboxUser ?? (portable ? "0" : "root"), target, ...input.command, ]; @@ -246,7 +256,7 @@ function observeStoppedDockerTarget( "--filter", `label=${OPENSHELL_SANDBOX_NAME_LABEL}=${input.sandboxName}`, "--format", - "{{.ID}}\t{{.Names}}", + SANDBOX_CONTAINER_FORMAT, ], DIRECT_SANDBOX_DISCOVERY_TIMEOUT_MS, ); @@ -254,9 +264,9 @@ function observeStoppedDockerTarget( return { failure: "runtime-discovery-failed" }; } if (lookup.status !== 0 || lookup.error) return { failure: "runtime-discovery-failed" }; - let resourceHandle: string | null; + let targetRecord; try { - resourceHandle = selectDockerPrivilegedSandboxTarget( + targetRecord = selectDockerPrivilegedSandboxTargetRecord( input.sandboxName, lookup.stdout, input.registeredSandboxNames, @@ -267,13 +277,14 @@ function observeStoppedDockerTarget( if ( "expectedResourceHandle" in input && input.expectedResourceHandle !== undefined && - input.expectedResourceHandle !== resourceHandle + input.expectedResourceHandle !== targetRecord?.resourceHandle ) { return { failure: "runtime-ownership-invalid" }; } - if (!resourceHandle || /-nemoclaw-gpu-backup-\d+$/u.test(lookup.stdout)) { + if (!targetRecord || /-nemoclaw-gpu-backup-\d+$/u.test(targetRecord.containerName)) { return { failure: "no-eligible-stopped-runtime" }; } + const resourceHandle = targetRecord.resourceHandle; const inspected = engine.capture( ["inspect", "--format", "{{.Id}}\t{{.State.Running}}\t{{json .Mounts}}", resourceHandle], 30_000, @@ -328,8 +339,58 @@ function clearStoppedNativeHome(input: RuntimeProviderStoppedNativeHomeCleanupIn export function createDockerPrivilegedSandboxControl(): RuntimeProviderPrivilegedSandboxControl { return Object.freeze({ + prepareManagedStartupWorkspace: ( + input: Parameters< + NonNullable + >[0], + ) => + prepareManagedWorkspaceMetadata(input.workspace, { + execute: (command) => + executeDockerCommand({ + ...input, + command, + sanitizeEnvironment: true, + maxOutputBytes: 4096, + }), + copyDirectory: (archive) => { + const portable = portableTarget(input.sandboxName, input.sandbox); + if (portable) portable.assertRuntimeAuthority(); + const target = portable + ? portable.containerId + : resolvePinnedDockerTarget(input, input.expectedResourceHandle); + if (target !== input.expectedResourceHandle) { + throw new PinnedSandboxResourceIdentityChangedError(input.sandboxName); + } + // Do not pass -a: Docker then substitutes Config.User for archive ownership. + const result = dockerSpawnSync( + [...(portable ? ["--host", portable.dockerHost] : []), "cp", "-", `${target}:/`], + { + encoding: null, + input: archive, + maxBuffer: 4096, + stdio: ["pipe", "pipe", "pipe"], + timeout: input.timeoutMs, + }, + ); + return { + ...result, + stdout: Buffer.from(result.stdout ?? ""), + stderr: Buffer.from(result.stderr ?? ""), + }; + }, + }), resolveTarget: resolveDockerTarget, execute: executeDockerCommand, + // Capability-free root cannot remove private sandbox-owned native state. + // Retain the same target checks and sanitized transport for its owner. + executeAsSandboxUser: ( + input: RuntimeProviderPrivilegedSandboxCommandInput & { readonly sandboxUser: string }, + ) => { + if (!/^[1-9][0-9]{0,9}:[1-9][0-9]{0,9}$/u.test(input.sandboxUser)) { + throw new Error("Docker sandbox-user cleanup requires a non-root numeric UID and GID."); + } + return executeDockerCommand(input, input.sandboxUser); + }, clearStoppedStateRoots, clearStoppedNativeHome, buildLegacyDockerArgv, diff --git a/src/lib/onboard/runtime-provider/docker-privileged-sandbox-identity.ts b/src/lib/onboard/runtime-provider/docker-privileged-sandbox-identity.ts index 1aabdb749b0..98c05470801 100644 --- a/src/lib/onboard/runtime-provider/docker-privileged-sandbox-identity.ts +++ b/src/lib/onboard/runtime-provider/docker-privileged-sandbox-identity.ts @@ -8,6 +8,11 @@ type LabeledSandboxContainer = { readonly name: string; }; +export type DockerPrivilegedSandboxTargetRecord = { + readonly resourceHandle: string; + readonly containerName: string; +}; + export function dockerContainerNameMatchesSandbox( containerName: string, sandboxName: string, @@ -29,20 +34,27 @@ function parseLabeledSandboxContainers(output: string): LabeledSandboxContainer[ .split(/\r?\n/u) .map((line) => line.trim()) .filter(Boolean) - .map((line) => { - const [id, name, ...unexpected] = line.split("\t"); - if (!id || !name || unexpected.length > 0 || /\s/u.test(id)) { + .flatMap((line) => { + const [id, name, role = "", ...unexpected] = line.split("\t"); + if ( + !id || + !name || + unexpected.length > 0 || + /\s/u.test(id) || + !["", "sandbox", "supervisor"].includes(role) + ) { throw new Error("Docker returned malformed OpenShell sandbox container metadata."); } - return { id, name }; + // Legacy workloads have no role label; 0.1.3 companions share the sandbox label. + return role === "supervisor" ? [] : [{ id, name }]; }); } -export function selectDockerPrivilegedSandboxTarget( +export function selectDockerPrivilegedSandboxTargetRecord( sandboxName: string, labeledContainerRows: string, registeredNames: readonly string[] = [sandboxName], -): string | null { +): DockerPrivilegedSandboxTargetRecord | null { const names = Array.from(new Set([...registeredNames, sandboxName])).sort( (left, right) => right.length - left.length || left.localeCompare(right), ); @@ -65,5 +77,19 @@ export function selectDockerPrivilegedSandboxTarget( "refusing ambiguous lifecycle execution.", ); } - return candidates[0]?.id ?? null; + const selected = candidates[0]; + return selected + ? Object.freeze({ resourceHandle: selected.id, containerName: selected.name }) + : null; +} + +export function selectDockerPrivilegedSandboxTarget( + sandboxName: string, + labeledContainerRows: string, + registeredNames: readonly string[] = [sandboxName], +): string | null { + return ( + selectDockerPrivilegedSandboxTargetRecord(sandboxName, labeledContainerRows, registeredNames) + ?.resourceHandle ?? null + ); } diff --git a/src/lib/onboard/runtime-provider/docker.ts b/src/lib/onboard/runtime-provider/docker.ts index a76408711a3..7cbacd84280 100644 --- a/src/lib/onboard/runtime-provider/docker.ts +++ b/src/lib/onboard/runtime-provider/docker.ts @@ -378,6 +378,7 @@ export function createDockerRuntimeProviderBundle( sandboxNamespace: "scoped", hostGatewayIp: null, includeSupervisorBin: true, + driverConfigLayout: "split-supervisor", processOwnership: "scoped-namespace", }, network: { diff --git a/src/lib/onboard/runtime-provider/managed-workspace-metadata.test.ts b/src/lib/onboard/runtime-provider/managed-workspace-metadata.test.ts new file mode 100644 index 00000000000..09c3f0e8cb0 --- /dev/null +++ b/src/lib/onboard/runtime-provider/managed-workspace-metadata.test.ts @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { runInNewContext } from "node:vm"; +import { describe, expect, it, vi } from "vitest"; +import { prepareManagedWorkspaceMetadata } from "./managed-workspace-metadata"; + +const wanted = { uid: 0, gid: 999, mode: 0o1775 }; +const before = { uid: 999, gid: 999, mode: 0o700, dev: "100", ino: "200", mtime: 1791870000 }; +const result = (value: unknown, status = 0) => ({ + status, + signal: null, + stdout: Buffer.from(JSON.stringify(value)), + stderr: Buffer.alloc(0), +}); + +function fixture(after = { ...before, ...wanted }) { + return { + execute: vi.fn().mockReturnValueOnce(result(before)).mockReturnValue(result(after)), + copyDirectory: vi.fn().mockReturnValue(result(null)), + }; +} + +describe("managed workspace directory metadata", () => { + it("reads large filesystem identities without rounding or rejecting them", () => { + const lstatSync = vi + .fn() + .mockReturnValueOnce({ uid: 0, mode: 0o755, isDirectory: () => true }) + .mockReturnValueOnce({ + uid: 0n, + gid: 999n, + mode: 0o1775n, + dev: 9007199254740993n, + ino: 18446744073709551615n, + mtimeMs: 1791870000123n, + isDirectory: () => true, + isSymbolicLink: () => false, + }); + let output = ""; + const operations = fixture(); + operations.execute.mockReset().mockImplementation((command: readonly string[]) => { + runInNewContext(command[2]!, { + require: () => ({ lstatSync }), + process: { + stdout: { + write: (value: string) => { + output += value; + }, + }, + }, + }); + return { ...result(null), stdout: Buffer.from(output) }; + }); + prepareManagedWorkspaceMetadata(wanted, operations); + expect(lstatSync).toHaveBeenNthCalledWith(2, "/sandbox", { bigint: true }); + expect(JSON.parse(output)).toEqual({ + ...wanted, + dev: "9007199254740993", + ino: "18446744073709551615", + mtime: before.mtime, + }); + expect(operations.copyDirectory).not.toHaveBeenCalled(); + }); + + it("rejects distinct large inodes that round to the same JavaScript number", () => { + const operations = fixture(); + operations.execute + .mockReset() + .mockReturnValueOnce(result({ ...before, ino: "9007199254740992" })) + .mockReturnValueOnce(result({ ...before, ...wanted, ino: "9007199254740993" })); + expect(() => prepareManagedWorkspaceMetadata(wanted, operations)).toThrow( + /retain its identity and declared metadata/u, + ); + }); + + it("copies only the declared directory header, then verifies the same inode and metadata", () => { + const operations = fixture(); + prepareManagedWorkspaceMetadata(wanted, operations); + expect(operations.execute).toHaveBeenCalledTimes(2); + const archive: Buffer = operations.copyDirectory.mock.calls[0]![0]; + expect(archive.byteLength).toBe(1536); + const field = (offset: number, length: number) => + archive + .subarray(offset, offset + length) + .toString("ascii") + .replace(/\0.*$/su, ""); + expect(field(0, 100)).toBe("sandbox/"); + expect(field(100, 8)).toBe("0001775"); + expect(field(108, 8)).toBe("0000000"); + expect(field(116, 8)).toBe("0001747"); + expect(Number.parseInt(field(124, 12), 8)).toBe(0); + expect(Number.parseInt(field(136, 12), 8)).toBe(before.mtime); + expect(field(156, 1)).toBe("5"); + expect(field(157, 100)).toBe(""); + expect(field(257, 6)).toBe("ustar"); + expect(archive.subarray(512).equals(Buffer.alloc(1024))).toBe(true); + const header = Buffer.from(archive.subarray(0, 512)); + header.fill(0x20, 148, 156); + expect(Number.parseInt(field(148, 8), 8)).toBe(header.reduce((sum, byte) => sum + byte, 0)); + }); + + it("does not rewrite already-correct ownership or permissions", () => { + const operations = fixture(); + operations.execute.mockReset().mockReturnValue(result({ ...before, ...wanted })); + prepareManagedWorkspaceMetadata(wanted, operations); + expect(operations.copyDirectory).not.toHaveBeenCalled(); + }); + + it.each([ + ["inode", { ino: "201" }], + ["device", { dev: "101" }], + ["UID", { uid: 999 }], + ["GID", { gid: 998 }], + ["mode", { mode: 0o700 }], + ])("rejects a changed %s after engine copy", (_label, changed) => { + const operations = fixture({ ...before, ...wanted, ...changed }); + expect(() => prepareManagedWorkspaceMetadata(wanted, operations)).toThrow( + /retain its identity and declared metadata/u, + ); + }); + + it("does not copy after an unsafe directory or failed inspection", () => { + const operations = fixture(); + operations.execute.mockReset().mockReturnValue(result(null, 20)); + expect(() => prepareManagedWorkspaceMetadata(wanted, operations)).toThrow(/Could not inspect/u); + expect(operations.copyDirectory).not.toHaveBeenCalled(); + }); + + it.each([null, {}, { ...before, ino: -1 }, { ...before, ino: "01" }, { ...before, dev: "1e3" }])( + "rejects invalid observed metadata %j", + (value) => { + const operations = fixture(); + operations.execute.mockReset().mockReturnValue(result(value)); + expect(() => prepareManagedWorkspaceMetadata(wanted, operations)).toThrow( + /metadata was invalid/u, + ); + expect(operations.copyDirectory).not.toHaveBeenCalled(); + }, + ); + + it("reports failed engine copies without advancing to root application", () => { + const operations = fixture(); + operations.copyDirectory.mockReturnValue(result(null, 1)); + expect(() => prepareManagedWorkspaceMetadata(wanted, operations)).toThrow( + /could not be restored/u, + ); + expect(operations.execute).toHaveBeenCalledOnce(); + }); + + it.each([ + { ...wanted, uid: -1 }, + { ...wanted, gid: 0 }, + { ...wanted, mode: 0o777 }, + ])("rejects invalid declarations before observation %j", (workspace) => { + const operations = fixture(); + expect(() => prepareManagedWorkspaceMetadata(workspace, operations)).toThrow( + /declared numeric ownership and mode/u, + ); + expect(operations.execute).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/onboard/runtime-provider/managed-workspace-metadata.ts b/src/lib/onboard/runtime-provider/managed-workspace-metadata.ts new file mode 100644 index 00000000000..c9926eb4eed --- /dev/null +++ b/src/lib/onboard/runtime-provider/managed-workspace-metadata.ts @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { RuntimeProviderPrivilegedSandboxCommandResult } from "./contract"; + +type Workspace = { readonly uid: number; readonly gid: number; readonly mode: number }; +type Observation = Workspace & { + readonly dev: string; + readonly ino: string; + readonly mtime: number; +}; + +// Read metadata only. The root-owned parent prevents the workload from replacing +// /sandbox between this observation and the directory-only engine archive copy. +const INSPECT_WORKSPACE = ` +const fs = require('node:fs'); +const parent = fs.lstatSync('/'); +const s = fs.lstatSync('/sandbox', { bigint: true }); +if (!parent.isDirectory() || parent.uid !== 0 || (parent.mode & 0o022) !== 0 || + !s.isDirectory() || s.isSymbolicLink()) process.exit(20); +process.stdout.write(JSON.stringify({uid:Number(s.uid),gid:Number(s.gid),mode:Number(s.mode & 0o7777n), + dev:String(s.dev),ino:String(s.ino),mtime:Math.floor(Number(s.mtimeMs) / 1000)})); +`; + +function observe( + execute: (command: readonly string[]) => RuntimeProviderPrivilegedSandboxCommandResult, +): Observation { + const result = execute(["/usr/local/bin/node", "-e", INSPECT_WORKSPACE]); + if (result.status !== 0 || result.error || result.signal) { + throw new Error("Could not inspect the exact managed workspace directory."); + } + let value: Observation; + try { + value = JSON.parse(result.stdout.toString("utf8")); + } catch { + throw new Error("Managed workspace metadata was unreadable."); + } + if ( + !value || + [value.uid, value.gid, value.mode, value.mtime].some( + (field) => !Number.isSafeInteger(field) || field < 0, + ) || + [value.dev, value.ino].some( + (field) => typeof field !== "string" || !/^(0|[1-9][0-9]*)$/u.test(field), + ) + ) { + throw new Error("Managed workspace metadata was invalid."); + } + return value; +} + +function directoryArchive(workspace: Workspace, mtime: number): Buffer { + // One USTAR directory header plus two end blocks; never include workspace + // contents, links, OpenShell control material, or caller-selected paths. + const archive = Buffer.alloc(1536); + archive.write("sandbox/", 0, "ascii"); + for (const [offset, size, value] of [ + [100, 8, workspace.mode], + [108, 8, workspace.uid], + [116, 8, workspace.gid], + [124, 12, 0], + [136, 12, mtime], + ]) { + const octal = value.toString(8); + if (octal.length >= size) throw new Error("Managed workspace metadata exceeds USTAR limits."); + archive.write(`${octal.padStart(size - 1, "0")}\0`, offset, size, "ascii"); + } + archive[156] = "5".charCodeAt(0); + archive.write("ustar\0", 257, 6, "ascii"); + archive.write("00", 263, 2, "ascii"); + archive.fill(0x20, 148, 156); + const checksum = archive.subarray(0, 512).reduce((sum, byte) => sum + byte, 0); + archive.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, 8, "ascii"); + return archive; +} + +/** Engine archive transport restores directory metadata without workload capabilities. */ +export function prepareManagedWorkspaceMetadata( + workspace: Workspace, + operations: { + readonly execute: (command: readonly string[]) => RuntimeProviderPrivilegedSandboxCommandResult; + readonly copyDirectory: (archive: Buffer) => RuntimeProviderPrivilegedSandboxCommandResult; + }, +): void { + if ( + !Number.isSafeInteger(workspace.uid) || + workspace.uid < 0 || + workspace.uid > 0o7777777 || + !Number.isSafeInteger(workspace.gid) || + workspace.gid <= 0 || + workspace.gid > 0o7777777 || + (workspace.mode !== 0o755 && workspace.mode !== 0o1775) + ) { + throw new Error("Managed workspace requires declared numeric ownership and mode."); + } + const before = observe(operations.execute); + if ( + before.uid === workspace.uid && + before.gid === workspace.gid && + before.mode === workspace.mode + ) + return; + const copied = operations.copyDirectory(directoryArchive(workspace, before.mtime)); + if (copied.status !== 0 || copied.error || copied.signal) { + throw new Error("Managed workspace directory metadata could not be restored."); + } + const after = observe(operations.execute); + if ( + after.dev !== before.dev || + after.ino !== before.ino || + after.uid !== workspace.uid || + after.gid !== workspace.gid || + after.mode !== workspace.mode + ) { + throw new Error( + "Managed workspace directory did not retain its identity and declared metadata.", + ); + } +} diff --git a/src/lib/onboard/runtime-provider/podman-privileged-sandbox-control.ts b/src/lib/onboard/runtime-provider/podman-privileged-sandbox-control.ts index 3502250b18c..893deda339f 100644 --- a/src/lib/onboard/runtime-provider/podman-privileged-sandbox-control.ts +++ b/src/lib/onboard/runtime-provider/podman-privileged-sandbox-control.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { prepareManagedWorkspaceMetadata } from "./managed-workspace-metadata"; import { SANITIZED_PRIVILEGED_ENV } from "./privileged-sandbox-environment"; import type { PodmanContainerEngine } from "../../adapters/podman"; import type { @@ -79,6 +80,14 @@ function execute( ], input.timeoutMs, input.input, + ...(input.maxOutputBytes !== undefined || (input.input?.length ?? 0) > 1024 * 1024 + ? [ + { + maxInputBytes: Math.max(1, input.input?.length ?? 0), + maxOutputBytes: input.maxOutputBytes ?? 1024 * 1024, + }, + ] + : []), ); return Object.freeze({ status: result.status, @@ -139,6 +148,33 @@ export function createPodmanPrivilegedSandboxControl( throw new Error("Podman stopped-state cleanup requires its workload-cleanup engine."); } return Object.freeze({ + prepareManagedStartupWorkspace: ( + input: Parameters< + NonNullable + >[0], + ) => + prepareManagedWorkspaceMetadata(input.workspace, { + execute: (command) => + execute(engine, { ...input, command, sanitizeEnvironment: true, maxOutputBytes: 4096 }), + copyDirectory: (archive) => { + const target = resolveTarget(engine, input); + if (target.resourceHandle !== input.expectedResourceHandle) { + throw new PinnedSandboxResourceIdentityChangedError(input.sandboxName); + } + // Preserve the declared UID/GID instead of substituting the container user. + const result = engine.capture( + ["container", "cp", "--archive=false", "-", `${target.resourceHandle}:/`], + input.timeoutMs, + archive, + ); + return { + ...result, + signal: null, + stdout: Buffer.from(result.stdout, "utf8"), + stderr: Buffer.from(result.stderr, "utf8"), + }; + }, + }), resolveTarget: ( input: Pick< RuntimeProviderPrivilegedSandboxCommandInput, diff --git a/src/lib/onboard/runtime-provider/podman-runtime-surfaces.test.ts b/src/lib/onboard/runtime-provider/podman-runtime-surfaces.test.ts index 4c1a2ce59e7..1bd689c6275 100644 --- a/src/lib/onboard/runtime-provider/podman-runtime-surfaces.test.ts +++ b/src/lib/onboard/runtime-provider/podman-runtime-surfaces.test.ts @@ -90,6 +90,55 @@ function exactLabels(): Readonly> { } describe("current Podman runtime provider", () => { + it("preserves private transport bounds while resolving the current engine", async () => { + vi.resetModules(); + const capture = vi.fn(() => ({ status: 0, stdout: "ok", stderr: "" })); + vi.doMock("../../adapters/podman", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + capturePodmanSocketAuthority: vi.fn(() => ({})), + createPodmanContainerEngine: vi.fn( + () => + ({ + operation: "sandbox-lifecycle", + engineId: "podman", + displayName: "Podman", + authorityId: "podman:test-current", + endpointAuthorityId: "podman-path-sha256:test-current", + capture, + captureHost: vi.fn(() => ({ status: 0, stdout: "", stderr: "" })), + assertAuthority: vi.fn(), + }) satisfies PodmanBoundContainerEngine, + ), + }; + }); + + try { + const { createCurrentPodmanOperationEngine } = await import("./podman-runtime-surfaces"); + const engine = createCurrentPodmanOperationEngine("sandbox-lifecycle", { + HOME: "/tmp/nemoclaw-test-home", + PATH: "/usr/bin:/bin", + XDG_RUNTIME_DIR: "/run/user/1000", + }); + const input = Buffer.from("private startup state", "utf8"); + const privateTransport = { maxInputBytes: 4096, maxOutputBytes: 8192 }; + + expect( + engine.capture(["container", "exec", "sandbox"], 12_345, input, privateTransport), + ).toEqual({ status: 0, stdout: "ok", stderr: "" }); + expect(capture).toHaveBeenCalledWith( + ["container", "exec", "sandbox"], + 12_345, + input, + privateTransport, + ); + } finally { + vi.doUnmock("../../adapters/podman"); + vi.resetModules(); + } + }); + it("binds destroy continuity to the full Podman ownership identity", () => { let labels = exactLabels(); const engine = runtimeEngine(() => labels); @@ -236,6 +285,7 @@ describe("current Podman runtime provider", () => { hostGatewayIp: NATIVE_PODMAN_SANDBOX_HOST_ADDRESS, includeSupervisorBin: false, processOwnership: "runtime-marker", + driverConfigLayout: "inline-supervisor", }, network: { sandboxSourceCidrs: expect.any(Function), diff --git a/src/lib/onboard/runtime-provider/podman-runtime-surfaces.ts b/src/lib/onboard/runtime-provider/podman-runtime-surfaces.ts index f2c7d0669b6..c3e6369accf 100644 --- a/src/lib/onboard/runtime-provider/podman-runtime-surfaces.ts +++ b/src/lib/onboard/runtime-provider/podman-runtime-surfaces.ts @@ -343,6 +343,7 @@ export function prepareNativePodmanGatewayHostRuntime( sandboxNamespace: "omitted" as const, hostGatewayIp: NATIVE_PODMAN_SANDBOX_HOST_ADDRESS, includeSupervisorBin: false, + driverConfigLayout: "inline-supervisor" as const, processOwnership: "runtime-marker" as const, }), network: Object.freeze({ @@ -417,7 +418,8 @@ export function createCurrentPodmanOperationEngine( args: Parameters[0], timeout?: number, input?: Buffer, - ) => resolve().capture(args, timeout, input), + privateTransport?: Parameters[3], + ) => resolve().capture(args, timeout, input, privateTransport), captureHost: ( args: Parameters[0], timeout?: number, diff --git a/src/lib/onboard/sandbox-create/orchestration.ts b/src/lib/onboard/sandbox-create/orchestration.ts index 6bced3ff8fa..25edb8293e5 100644 --- a/src/lib/onboard/sandbox-create/orchestration.ts +++ b/src/lib/onboard/sandbox-create/orchestration.ts @@ -2187,6 +2187,7 @@ export function createSandboxWithBaseImageResolution(runtime: SandboxCreateOrche return managedWorkloadOnboard.createManagedStateVolumeOnboardLifecycle({ roots: managedStateRoots, runtimeProvider: requireManagedWorkloadRuntime().runtimeProvider, + workspace: process.env.OPENSHELL_WORKSPACE || "default", }); }; const finalizeRecreatedSourceHermesVolume = ( diff --git a/src/lib/onboard/sandbox-gpu-create-flow.test.ts b/src/lib/onboard/sandbox-gpu-create-flow.test.ts index c6a56dbd658..93ea4698199 100644 --- a/src/lib/onboard/sandbox-gpu-create-flow.test.ts +++ b/src/lib/onboard/sandbox-gpu-create-flow.test.ts @@ -822,6 +822,22 @@ describe("runSandboxGpuCreateFlow native failure and readiness", () => { ); }); + it("detaches a managed startup hold before post-create setup without waiting for its banner", async () => { + const input = createInput(); + input.sandboxStartupCommand = ["/usr/local/bin/nemoclaw-managed-startup-hold"]; + input.sandboxEnv = { OPENSHELL_DRIVERS: "docker" }; + + await expect(runSandboxGpuCreateFlow(input, createDeps())).resolves.toMatchObject({ + route: "native", + }); + expect(mocks.streamSandboxCreate.mock.calls[0]?.[3]).toEqual( + expect.objectContaining({ + readyCheckOutputPatterns: [], + waitForReadyTermination: true, + }), + ); + }); + it("waits for native non-terminal startup output before detaching the create client", async () => { const input = createInput(); input.sandboxEnv = { OPENSHELL_DRIVERS: "docker" }; diff --git a/src/lib/onboard/sandbox-gpu-create-run-attempt.ts b/src/lib/onboard/sandbox-gpu-create-run-attempt.ts index 22a752f69b6..fd8faa53098 100644 --- a/src/lib/onboard/sandbox-gpu-create-run-attempt.ts +++ b/src/lib/onboard/sandbox-gpu-create-run-attempt.ts @@ -15,7 +15,7 @@ import { import { namedOpenShellGateway } from "../adapters/openshell/sandbox-observer"; import { printSandboxCreateRecoveryHints } from "../build-context"; import type { StreamSandboxCreateResult } from "../sandbox/create-stream"; -import { getReadyCheckOutputPatternsForAgent } from "../sandbox/create-stream-ready-gate"; +import { managedStartupCreateReadiness } from "./managed-startup/hold"; import type { SandboxGpuProofResult } from "../state/registry"; import { classifySandboxCreateFailure } from "../validation"; import { @@ -804,14 +804,14 @@ export function createSandboxGpuCreateAttemptRunner( if (!deferRestartSafeCutover) void runtimePatch.maybeApplyDuringCreate(); }, }), - readyCheckOutputPatterns: getReadyCheckOutputPatternsForAgent({ + ...managedStartupCreateReadiness({ + startupCommand: input.sandboxStartupCommand, isTerminalAgent: input.terminalAgent, - startupRunsDuringCreate: true, env: input.sandboxEnv, + deferPostCreateEffects: deferRestartSafeCutover || deferPostCreateEffects, }), failureCheck: runtimePatch.createFailureMessage, traceEvent: addTraceEvent, - waitForReadyTermination: deferRestartSafeCutover || deferPostCreateEffects, initialPhase: compatibility && (input.prebuild.imageRef || state.compatibilityRequest) ? "create" diff --git a/src/lib/sandbox/privileged-exec.test.ts b/src/lib/sandbox/privileged-exec.test.ts index 77679c9733e..1999c842e4d 100644 --- a/src/lib/sandbox/privileged-exec.test.ts +++ b/src/lib/sandbox/privileged-exec.test.ts @@ -229,9 +229,10 @@ describe("privileged sandbox exec routing", () => { ).toBe("abc123"); }); - it("clears stopped OpenClaw WeChat state through an isolated immutable-image helper", () => { + it("clears stopped state when an unrelated GPU recovery backup is also discovered", () => { const containerId = "a".repeat(64); const helperId = "b".repeat(64); + const backupId = "d".repeat(64); const mounts = JSON.stringify([ { Type: "volume", @@ -304,7 +305,9 @@ describe("privileged sandbox exec routing", () => { withPrivilegedExecMocks( { - dockerCapture: () => `${containerId}\topenshell-alpha\n`, + dockerCapture: () => + `${containerId}\topenshell-alpha\tsandbox\n` + + `${backupId}\topenshell-default--alpha-nemoclaw-gpu-backup-1780491860342\tsupervisor\n`, dockerRun: runDocker, getSandbox: () => ({ name: "alpha", openshellDriver: "docker" }), listSandboxes: () => ({ sandboxes: [{ name: "alpha" }], defaultSandbox: "alpha" }), @@ -502,6 +505,32 @@ describe("privileged sandbox exec routing", () => { ); }); + it("rejects a GPU recovery backup when its discovery row includes a role", () => { + const containerId = "a".repeat(64); + const captureDocker = vi.fn( + () => `${containerId}\topenshell-default--alpha-nemoclaw-gpu-backup-1780491860342\tsandbox\n`, + ); + const runDocker = vi.fn(); + + withPrivilegedExecMocks( + { + dockerCapture: captureDocker, + dockerRun: runDocker, + getSandbox: () => ({ name: "alpha", openshellDriver: "docker" }), + listSandboxes: () => ({ sandboxes: [{ name: "alpha" }], defaultSandbox: "alpha" }), + }, + ({ clearStoppedSandboxStateRoots }) => { + expect(clearStoppedSandboxStateRoots("alpha", EXPECTED_WECHAT_STATE_PATHS)).toEqual({ + cleared: false, + failure: "no-eligible-stopped-runtime", + }); + }, + ); + + expect(captureDocker).toHaveBeenCalledOnce(); + expect(runDocker).not.toHaveBeenCalled(); + }); + it("classifies invalid stopped-container ownership metadata", () => { withPrivilegedExecMocks( { @@ -952,7 +981,7 @@ describe("privileged sandbox exec routing", () => { "--filter", "label=openshell.ai/sandbox-name=alpha", "--format", - "{{.ID}}\t{{.Names}}", + '{{.ID}}\t{{.Names}}\t{{.Label "openshell.ai/isolation-role"}}', ], timeout: 5000, }, diff --git a/test/e2e/README.md b/test/e2e/README.md index 0d05e92f4f8..7a5ad9dc001 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -2329,6 +2329,13 @@ or automatically replay the base. For the default same-repository PR revision selection, leave `jobs` and `targets` empty and keep `include_staging_brev_launchable=false`. Keep `allow_jetson_dispatch=false` for the default PR revision selection. To select the protected managed-image runtime qualification, set `jobs=managed-image-protected-runtime`. +The 75-minute job records its start before checkout. Setup and provider qualification share the first +45 minutes; rollback and final inventory share the next 15 minutes. Commands retain their individual +limits and are capped by the remaining phase budget. The fixture reserves five minutes for cleanup, +and the workflow reserves eight minutes for registry removal, source restoration, artifact upload, +and Docker credential cleanup, with two minutes of runner headroom. A late setup or exhausted phase +fails qualification; it cannot publish a passing receipt by skipping agent or cleanup assertions. +NIM login and image preparation run in a supervised child so registry waits obey the same deadline. Leave `targets` empty. Keep `include_staging_brev_launchable=false`. The candidate must contain `ci/protected-managed-image-multiarch-activation-v1.json` and `ci/protected-managed-image-runtime-activation-v1.json`. diff --git a/test/e2e/live/managed-image-protected-runtime-budget.ts b/test/e2e/live/managed-image-protected-runtime-budget.ts new file mode 100644 index 00000000000..9b6c87e6a1f --- /dev/null +++ b/test/e2e/live/managed-image-protected-runtime-budget.ts @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { HostCliClient } from "../fixtures/clients/host.ts"; + +const MINUTE = 60_000; +export const PROTECTED_RUNTIME_JOB_MINUTES = 75; +export const PROTECTED_RUNTIME_CLEANUP_MS = 5 * MINUTE; +export const PROTECTED_RUNTIME_EVIDENCE_MS = 8 * MINUTE; +export const PROTECTED_RUNTIME_RUNNER_HEADROOM_MS = 2 * MINUTE; +export const PROTECTED_RUNTIME_ROLLBACK_MS = 15 * MINUTE; +const BODY_MS = + PROTECTED_RUNTIME_JOB_MINUTES * MINUTE - + PROTECTED_RUNTIME_CLEANUP_MS - + PROTECTED_RUNTIME_EVIDENCE_MS - + PROTECTED_RUNTIME_RUNNER_HEADROOM_MS; + +export function protectedRuntimeDeadlines(startedAt: string | undefined, now = Date.now()) { + const start = Number(startedAt); + if (!Number.isSafeInteger(start) || start <= 0 || start > now) { + throw new Error("protected runtime job start must be a past epoch millisecond timestamp"); + } + if (now >= start + BODY_MS - PROTECTED_RUNTIME_ROLLBACK_MS) { + throw new Error( + "protected runtime setup exhausted the provider budget; qualification was not started", + ); + } + return { + providers: start + BODY_MS - PROTECTED_RUNTIME_ROLLBACK_MS, + rollback: start + BODY_MS, + testTimeoutMs: Math.max(1, start + BODY_MS - now), + }; +} + +// Preserve each command's failure guard while sharing the suite's remaining +// wall-clock budget. The underlying ShellProbe owns process-group termination +// and artifact capture; cleanup uses the original host with its own signal. +export function protectedRuntimeDeadlineHost( + host: Pick, + deadline: number, + now = Date.now, +): HostCliClient { + return new HostCliClient({ + async run(command, options = {}) { + const remaining = deadline - now(); + // Leave the existing supervisor's termination grace inside the deadline. + const killGraceMs = options.killGraceMs ?? 5_000; + if (remaining <= killGraceMs) { + throw new Error( + "protected runtime suite deadline exhausted; reserved cleanup and evidence remain", + ); + } + const result = await host.command(command.command, [...command.args], { + ...options, + killGraceMs, + timeoutMs: Math.min(options.timeoutMs ?? 60_000, remaining - killGraceMs), + }); + if (result.timedOut || now() >= deadline) { + throw new Error( + `protected runtime command exceeded its budget; see ${result.artifacts.result}`, + ); + } + return result; + }, + }); +} diff --git a/test/e2e/live/managed-image-protected-runtime-helpers.ts b/test/e2e/live/managed-image-protected-runtime-helpers.ts index 2e967f2f9c6..369b9087f42 100644 --- a/test/e2e/live/managed-image-protected-runtime-helpers.ts +++ b/test/e2e/live/managed-image-protected-runtime-helpers.ts @@ -14,15 +14,11 @@ import { parseProtectedManagedImageContracts, } from "../../../scripts/checks/managed-image-protected-runtime-contract.ts"; import { PROTECTED_MANAGED_IMAGE_COHORT_PATTERN } from "../../../scripts/checks/protected-managed-image-contract.ts"; -import { - adoptServedModelId, - dockerLoginNgc, - pullNimImage, -} from "../../../src/lib/inference/nim.ts"; +import { adoptServedModelId, getImageForModel } from "../../../src/lib/inference/nim.ts"; import { getOllamaProxyToken, killStaleProxy, - persistAndProbeOllamaProxy, + persistProxyToken, startOllamaAuthProxy, } from "../../../src/lib/inference/ollama/proxy.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; @@ -38,6 +34,11 @@ import { REPO_ROOT, } from "./gpu-e2e-helpers.ts"; +import { + protectedRuntimeDeadlineHost, + protectedRuntimeDeadlines, +} from "./managed-image-protected-runtime-budget.ts"; + const OLLAMA_MODEL = "qwen3.5:9b"; const VLLM_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"; const VLLM_IMAGE = @@ -430,7 +431,9 @@ export async function startProtectedOllama(host: HostCliClient): Promise expect(startOllamaAuthProxy(), "Ollama auth proxy must start").toBe(true); const proxyToken = getOllamaProxyToken(); expect(proxyToken).toMatch(/^[a-f0-9]{48}$/u); - await persistAndProbeOllamaProxy(proxyToken!); + // Each child proves inference through its own native provider and private gateway. + // The legacy default-bridge probe cannot validate that route. + persistProxyToken(proxyToken!); return proxyToken!; } @@ -595,8 +598,11 @@ async function startProtectedVllm( VLLM_MODEL, "--served-model-name", VLLM_MODEL, + "--enable-auto-tool-choice", + "--tool-call-parser", + "hermes", "--max-model-len", - "2048", + "32768", "--gpu-memory-utilization", "0.45", ], @@ -720,10 +726,33 @@ async function startProtectedNim( state: ProtectedProviderContainerState, apiKey: string, ): Promise { - expect(dockerLoginNgc(apiKey), "NGC login must succeed for protected NIM qualification").toBe( - true, + const image = getImageForModel(NIM_CATALOG_MODEL); + if (!image) throw new Error("protected NIM catalog model has no image"); + // NIM's production login and platform-aware pull are synchronous. Run them + // in the supervised child so a slow registry cannot block the suite deadline. + const prepare = await host.command( + "node", + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` + import * as importedNim from './src/lib/inference/nim.ts'; + const { dockerLoginNgc, pullNimImage } = importedNim.default ?? importedNim; + if (!dockerLoginNgc(process.env.NGC_API_KEY)) throw new Error('NGC login failed'); + pullNimImage(${JSON.stringify(NIM_CATALOG_MODEL)}); + `, + ], + { + artifactName: "prepare-protected-nim-image", + cwd: REPO_ROOT, + env: { ...buildAvailabilityProbeEnv(), NGC_API_KEY: apiKey }, + redactionValues: [apiKey], + timeoutMs: 20 * 60_000, + }, ); - const image = pullNimImage(NIM_CATALOG_MODEL); + expect(prepare.exitCode, resultText(prepare)).toBe(0); const start = await host.command( "docker", [ @@ -745,6 +774,8 @@ async function startProtectedNim( "NGC_API_KEY", "--env", "NIM_NGC_API_KEY", + "--env", + "NIM_PASSTHROUGH_ARGS=--enable-auto-tool-choice --tool-call-parser qwen3_coder --reasoning-parser nemotron_v3", image, ], { @@ -916,8 +947,11 @@ async function proveOwnedRuntimeInventoryClean( export async function qualifyProtectedManagedImageRuntime( fixtures: RuntimeFixtures, ngcApiKeyInput: string, + deadlines = protectedRuntimeDeadlines(process.env.NEMOCLAW_PROTECTED_RUNTIME_STARTED_AT_MS), ): Promise { - const { artifacts, cleanup, host, progress } = fixtures; + const { artifacts, cleanup, progress } = fixtures; + const host = protectedRuntimeDeadlineHost(fixtures.host, deadlines.providers); + const rollbackHost = protectedRuntimeDeadlineHost(fixtures.host, deadlines.rollback); const contracts = imageContracts(); const ngcApiKey = requiredNgcApiKey(ngcApiKeyInput); const cohort = process.env.NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT ?? ""; @@ -926,14 +960,14 @@ export async function qualifyProtectedManagedImageRuntime( const providerStates = [vllmState, nimState] as const; cleanup.trackDisposable("remove protected vLLM container", async () => { - await removeProtectedProviderContainer(host, vllmState, "cleanup-vllm-container"); + await removeProtectedProviderContainer(fixtures.host, vllmState, "cleanup-vllm-container"); }); cleanup.trackDisposable("remove protected NIM container", async () => { - await removeProtectedProviderContainer(host, nimState, "cleanup-nim-container"); + await removeProtectedProviderContainer(fixtures.host, nimState, "cleanup-nim-container"); }); cleanup.trackDisposable("stop protected Ollama runtime", async () => { killStaleProxy(); - const result = await cleanupOllama(host, "cleanup-managed-image-ollama"); + const result = await cleanupOllama(fixtures.host, "cleanup-managed-image-ollama"); expect(result.exitCode, resultText(result)).toBe(0); }); @@ -995,8 +1029,8 @@ export async function qualifyProtectedManagedImageRuntime( activePhase = "prove all-agent managed bootstrap rollback and exact cleanup"; progress.phase("prove all-agent managed bootstrap rollback and exact cleanup"); - await qualifyEveryRollback(host, contracts); - await proveOwnedRuntimeInventoryClean(host, cohort, providerStates); + await qualifyEveryRollback(rollbackHost, contracts); + await proveOwnedRuntimeInventoryClean(rollbackHost, cohort, providerStates); const providerContainers = providerStates.map(({ authority }) => { if (!authority) throw new Error("protected provider authority is missing from the receipt"); return authority; diff --git a/test/e2e/live/managed-image-protected-runtime.test.ts b/test/e2e/live/managed-image-protected-runtime.test.ts index 5a3b8164d29..7911a67cc4d 100644 --- a/test/e2e/live/managed-image-protected-runtime.test.ts +++ b/test/e2e/live/managed-image-protected-runtime.test.ts @@ -5,13 +5,23 @@ import { PROTECTED_MANAGED_IMAGE_AGENTS } from "../../../scripts/checks/managed- import { test } from "../fixtures/e2e-test.ts"; import { qualifyProtectedManagedImageRuntime } from "./managed-image-protected-runtime-helpers.ts"; -const TIMEOUT_MS = 220 * 60_000; +import { + protectedRuntimeDeadlines, + PROTECTED_RUNTIME_CLEANUP_MS, +} from "./managed-image-protected-runtime-budget.ts"; + +// Discovery runs outside the protected workflow. Execution below still requires +// its real start timestamp before declaring or mutating a fixture. +const discoveryTimeout = protectedRuntimeDeadlines( + process.env.NEMOCLAW_PROTECTED_RUNTIME_STARTED_AT_MS ?? String(Date.now()), +).testTimeoutMs; test( "exact all-agent managed images retain GPU, Ollama, NIM, vLLM, rollback, and cleanup (#7744)", { - timeout: TIMEOUT_MS, + timeout: discoveryTimeout, meta: { + e2eCleanupTimeoutMs: PROTECTED_RUNTIME_CLEANUP_MS, e2ePhases: [ "qualify all managed agents with GPU-backed Ollama", "qualify all managed agents with GPU-backed vLLM", @@ -21,6 +31,9 @@ test( }, }, async ({ artifacts, cleanup, host, progress, secrets }) => { + const deadlines = protectedRuntimeDeadlines( + process.env.NEMOCLAW_PROTECTED_RUNTIME_STARTED_AT_MS, + ); await artifacts.target.declare({ id: "managed-image-protected-runtime", boundary: @@ -33,6 +46,7 @@ test( await qualifyProtectedManagedImageRuntime( { artifacts, cleanup, host, progress }, secrets.optional("NVIDIA_API_KEY") ?? "", + deadlines, ); }, ); diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index 3a579f1b661..f38b80ee8e8 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -91,7 +91,10 @@ }, { "live": "test/e2e/live/managed-image-protected-runtime.test.ts", - "liveSources": ["test/e2e/live/managed-image-protected-runtime-helpers.ts"], + "liveSources": [ + "test/e2e/live/managed-image-protected-runtime-helpers.ts", + "test/e2e/live/managed-image-protected-runtime-budget.ts" + ], "fast": [ "src/lib/inference/nim.test.ts", "src/lib/onboard/sandbox-gpu-create-flow.test.ts", @@ -99,7 +102,8 @@ "test/e2e/support/managed-image-protected-runtime-workflow.test.ts", "test/inference/managed/managed-image-protected-runtime-contract.test.ts", "test/inference/llama/llama-cpp-dgx-spark-qualification-runner.test.ts", - "test/automation/pull-requests/pr-risk-plan.test.ts" + "test/automation/pull-requests/pr-risk-plan.test.ts", + "test/e2e/support/managed-image-protected-runtime-budget.test.ts" ] }, { diff --git a/test/e2e/support/managed-image-legacy-container-contract.test.ts b/test/e2e/support/managed-image-legacy-container-contract.test.ts new file mode 100644 index 00000000000..6ad0088c8f3 --- /dev/null +++ b/test/e2e/support/managed-image-legacy-container-contract.test.ts @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, it } from "vitest"; +import { + isManagedImageHarnessWorkload, + requireManagedImageHarnessOpenShellVersion, +} from "../../../scripts/checks/managed-image-protected-runtime-contract.ts"; +import { gatewayIdForStateDir } from "../../../src/lib/onboard/gateway/process-environment.ts"; +import { managedImageHarnessContainerListArgs } from "../../../scripts/checks/run-managed-image-openshell-e2e.ts"; + +it("requires the exact private network and image for a 0.0.116 workload", () => { + const expected = { + sandbox: "nmc-mi-oc-ol", + stateDir: "/tmp/private/gateway-state", + networkName: "private-network", + imageContentId: `sha256:${"a".repeat(64)}`, + runtimeVersion: "0.0.116" as const, + }; + const record = { + Config: { + Labels: { + "openshell.ai/managed-by": "openshell", + "openshell.ai/sandbox-name": expected.sandbox, + }, + }, + Image: expected.imageContentId, + NetworkSettings: { Networks: { [expected.networkName]: {} } }, + }; + expect(isManagedImageHarnessWorkload(record, expected)).toBe(true); + expect(isManagedImageHarnessWorkload(record, { ...expected, networkName: "foreign" })).toBe( + false, + ); + expect(isManagedImageHarnessWorkload(record, { ...expected, networkName: undefined })).toBe( + false, + ); + expect( + isManagedImageHarnessWorkload({ ...record, Image: `sha256:${"b".repeat(64)}` }, expected), + ).toBe(false); + expect(isManagedImageHarnessWorkload(record, { ...expected, sandbox: "foreign" })).toBe(false); + expect( + managedImageHarnessContainerListArgs(expected.sandbox, true, expected.runtimeVersion), + ).toEqual([ + "docker", + "ps", + "-aq", + "--no-trunc", + "--filter", + "label=openshell.ai/managed-by=openshell", + "--filter", + `label=openshell.ai/sandbox-name=${expected.sandbox}`, + ]); +}); + +it("rejects a runtime without a reviewed container identity contract", () => { + expect(() => requireManagedImageHarnessOpenShellVersion("9.9.9")).toThrow( + "Unsupported protected OpenShell runtime version", + ); +}); + +it("requires the native isolation identity for a 0.1.3 workload", () => { + const expected = { + sandbox: "nmc-mi-oc-ol", + stateDir: "/tmp/private/gateway-state", + imageContentId: `sha256:${"a".repeat(64)}`, + runtimeVersion: "0.1.3" as const, + }; + const record = { + Config: { + Labels: { + "openshell.ai/managed-by": "openshell", + "openshell.ai/sandbox-name": expected.sandbox, + "openshell.ai/isolation-role": "sandbox", + "openshell.ai/sandbox-namespace": gatewayIdForStateDir(expected.stateDir), + }, + }, + Image: expected.imageContentId, + HostConfig: { NetworkMode: "none" }, + NetworkSettings: { Networks: { "legacy-network": {} } }, + }; + expect(isManagedImageHarnessWorkload(record, expected)).toBe(true); + expect( + isManagedImageHarnessWorkload( + { ...record, HostConfig: { NetworkMode: "legacy-network" } }, + expected, + ), + ).toBe(false); + expect( + managedImageHarnessContainerListArgs(expected.sandbox, false, expected.runtimeVersion), + ).toEqual([ + "docker", + "ps", + "-q", + "--no-trunc", + "--filter", + "label=openshell.ai/managed-by=openshell", + "--filter", + `label=openshell.ai/sandbox-name=${expected.sandbox}`, + "--filter", + "label=openshell.ai/isolation-role=sandbox", + ]); +}); diff --git a/test/e2e/support/managed-image-pairing-wait.test.ts b/test/e2e/support/managed-image-pairing-wait.test.ts new file mode 100644 index 00000000000..4a644fd0c29 --- /dev/null +++ b/test/e2e/support/managed-image-pairing-wait.test.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, it, vi } from "vitest"; +import { waitForLocalInferenceAgentPairing } from "../../../scripts/checks/managed-image-protected-runtime-contract"; + +const pending = { status: 1, stderr: "scope upgrade pending approval" }; +it("waits for normal OpenClaw pairing before accepting completion", async () => { + const success = { status: 0, stderr: "", stdout: "completion" }; + const run = vi.fn().mockReturnValueOnce(pending).mockReturnValueOnce(success); + const sleep = vi.fn().mockResolvedValue(undefined); + expect(await waitForLocalInferenceAgentPairing("openclaw", run, sleep)).toBe(success); + expect(run).toHaveBeenCalledTimes(2); + expect(sleep).toHaveBeenCalledExactlyOnceWith(5_000); +}); + +it("returns the failure after four pending-approval attempts", async () => { + const run = vi.fn().mockReturnValue(pending); + const sleep = vi.fn().mockResolvedValue(undefined); + expect(await waitForLocalInferenceAgentPairing("openclaw", run, sleep)).toBe(pending); + expect(run).toHaveBeenCalledTimes(4); + expect(sleep).toHaveBeenCalledTimes(3); +}); + +it.each([ + ["hermes", pending], + ["langchain-deepagents-code", pending], + ["openclaw", { status: 1, stderr: "inference request failed" }], + ["openclaw", { status: null, stderr: pending.stderr }], + ["openclaw", { ...pending, error: new Error("transport failed") }], +] as const)("does not retry unrelated failure for %s: %j", async (agent, result) => { + const run = vi.fn().mockReturnValue(result); + const sleep = vi.fn().mockResolvedValue(undefined); + expect(await waitForLocalInferenceAgentPairing(agent, run, sleep)).toBe(result); + expect(run).toHaveBeenCalledOnce(); + expect(sleep).not.toHaveBeenCalled(); +}); diff --git a/test/e2e/support/managed-image-protected-runtime-budget.test.ts b/test/e2e/support/managed-image-protected-runtime-budget.test.ts new file mode 100644 index 00000000000..73c99909821 --- /dev/null +++ b/test/e2e/support/managed-image-protected-runtime-budget.test.ts @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, it, vi } from "vitest"; +import type { ShellProbeResult, ShellProbeRunOptions } from "../fixtures/shell-probe.ts"; +import { + protectedRuntimeDeadlines, + protectedRuntimeDeadlineHost, + PROTECTED_RUNTIME_CLEANUP_MS, + PROTECTED_RUNTIME_EVIDENCE_MS, + PROTECTED_RUNTIME_JOB_MINUTES, + PROTECTED_RUNTIME_ROLLBACK_MS, + PROTECTED_RUNTIME_RUNNER_HEADROOM_MS, +} from "../live/managed-image-protected-runtime-budget.ts"; + +const MINUTE = 60_000; +const START = 1_700_000_000_000; +const result: ShellProbeResult = { + command: ["test"], + exitCode: 0, + signal: null, + timedOut: false, + stdout: "", + stderr: "", + artifacts: { stdout: "stdout", stderr: "stderr", result: "command-result.json" }, +}; + +it("charges setup to the job budget and reserves rollback, cleanup, and evidence within 75 minutes", () => { + const setup = 12 * MINUTE; + const deadlines = protectedRuntimeDeadlines(String(START), START + setup); + expect(deadlines.providers - START).toBe(45 * MINUTE); + expect(deadlines.rollback - deadlines.providers).toBe(PROTECTED_RUNTIME_ROLLBACK_MS); + expect(deadlines.testTimeoutMs).toBe(48 * MINUTE); + expect( + setup + + deadlines.testTimeoutMs + + PROTECTED_RUNTIME_CLEANUP_MS + + PROTECTED_RUNTIME_EVIDENCE_MS + + PROTECTED_RUNTIME_RUNNER_HEADROOM_MS, + ).toBe(PROTECTED_RUNTIME_JOB_MINUTES * MINUTE); +}); + +it.each([undefined, "", "0", "NaN", "Infinity", "-1", String(START + 1)])( + "rejects an unavailable or invalid job-start timestamp %s", + (value) => expect(() => protectedRuntimeDeadlines(value, START)).toThrow("job start"), +); + +it("bounds nine slow serial transactions before they consume rollback or cleanup time", async () => { + let now = START + 12 * MINUTE; + const deadlines = protectedRuntimeDeadlines(String(START), now); + const command = vi.fn( + async (_command: string, _args: string[], options: ShellProbeRunOptions) => { + const duration = Math.min(420_000, options.timeoutMs!); + now += duration; + return { ...result, timedOut: duration < 420_000 }; + }, + ); + const host = protectedRuntimeDeadlineHost({ command }, deadlines.providers, () => now); + await host.command("qualification-1", [], { timeoutMs: 10 * MINUTE }); + await host.command("qualification-2", [], { timeoutMs: 10 * MINUTE }); + await host.command("qualification-3", [], { timeoutMs: 10 * MINUTE }); + await host.command("qualification-4", [], { timeoutMs: 10 * MINUTE }); + await expect(host.command("qualification-5", [], { timeoutMs: 10 * MINUTE })).rejects.toThrow( + "exceeded its budget", + ); + expect(command).toHaveBeenCalledTimes(5); + expect(now).toBeLessThan(deadlines.providers); + expect(deadlines.rollback - now).toBeGreaterThanOrEqual(PROTECTED_RUNTIME_ROLLBACK_MS); +}); + +it("keeps shorter command guards and gives rollback its separate reserved window", async () => { + const deadlines = protectedRuntimeDeadlines(String(START), START); + const command = vi.fn().mockResolvedValue(result); + const providers = protectedRuntimeDeadlineHost( + { command }, + deadlines.providers, + () => deadlines.providers, + ); + await expect(providers.command("late-provider")).rejects.toThrow("deadline exhausted"); + expect(command).not.toHaveBeenCalled(); + const rollback = protectedRuntimeDeadlineHost( + { command }, + deadlines.rollback, + () => deadlines.providers, + ); + await rollback.command("rollback", [], { timeoutMs: 30_000 }); + expect(command).toHaveBeenCalledWith( + "rollback", + [], + expect.objectContaining({ timeoutMs: 30_000 }), + ); +}); + +it("rejects a command that finishes after the shared deadline even when it exits zero", async () => { + const now = vi + .fn() + .mockReturnValueOnce(START) + .mockReturnValue(START + MINUTE); + const command = vi.fn().mockResolvedValue(result); + const host = protectedRuntimeDeadlineHost({ command }, START + MINUTE, now); + await expect(host.command("slow-success")).rejects.toThrow("exceeded its budget"); +}); + +it("refuses late setup before qualification can register or mutate runtime resources", () => { + expect(() => protectedRuntimeDeadlines(String(START), START + 45 * MINUTE)).toThrow( + "setup exhausted", + ); +}); diff --git a/test/e2e/support/managed-image-protected-runtime-readiness.test.ts b/test/e2e/support/managed-image-protected-runtime-readiness.test.ts index c0bb54372c1..0f8c6022802 100644 --- a/test/e2e/support/managed-image-protected-runtime-readiness.test.ts +++ b/test/e2e/support/managed-image-protected-runtime-readiness.test.ts @@ -37,7 +37,7 @@ vi.mock("../../../src/lib/inference/ollama/proxy.ts", () => ({ getOllamaProxyToken: () => undefined, isProxyHealthy: () => false, killStaleProxy: () => undefined, - persistAndProbeOllamaProxy: async () => undefined, + persistProxyToken: () => undefined, startOllamaAuthProxy: () => false, })); vi.mock("../fixtures/e2e-test.ts", () => ({ diff --git a/test/e2e/support/managed-image-protected-runtime-workflow.test.ts b/test/e2e/support/managed-image-protected-runtime-workflow.test.ts index 6e4335392d3..c6ac6cc5423 100644 --- a/test/e2e/support/managed-image-protected-runtime-workflow.test.ts +++ b/test/e2e/support/managed-image-protected-runtime-workflow.test.ts @@ -56,6 +56,20 @@ describe("protected managed-image runtime workflow", () => { expect(validateManagedImageProtectedRuntimeWorkflow(workflow())).toEqual([]); }); + it("accepts the protected finalization limits through the shared workflow boundary", () => { + expect(validateE2eWorkflow(workflow())).toEqual([]); + }); + + it.each([ + ["Upload protected managed-image runtime artifacts", 3], + ["Upload protected managed-image runtime artifacts", 5], + ["Clean up Docker auth", 2], + ])("rejects changed finalization timeout for %s", (name, minutes) => { + const value = workflow(); + namedStep(value, String(name))["timeout-minutes"] = minutes; + expect(validateE2eWorkflow(value)).not.toEqual([]); + }); + it.each([ ["Bind reviewed candidate OpenShell runtime", "if", "false"], ["Bind reviewed candidate OpenShell runtime", "continue-on-error", true], @@ -170,6 +184,27 @@ describe("protected managed-image runtime workflow", () => { ); }); + it("rejects omission of the job start used by the shared runtime budget", () => { + const value = workflow(); + const guard = namedStep(value, "Validate protected runtime exact-head dispatch"); + guard.run = String(guard.run) + .split("\n") + .filter((line) => !line.includes("NEMOCLAW_PROTECTED_RUNTIME_STARTED_AT_MS")) + .join("\n"); + expect(validateManagedImageProtectedRuntimeWorkflow(value)).not.toEqual([]); + }); + + it.each([ + "Remove isolated protected runtime registry", + "Restore trusted OpenShell sources", + "Upload protected managed-image runtime artifacts", + "Clean up Docker auth", + ])("rejects unbounded finalization step %s", (name) => { + const value = workflow(); + delete namedStep(value, name)["timeout-minutes"]; + expect(validateManagedImageProtectedRuntimeWorkflow(value)).not.toEqual([]); + }); + // source-shape-contract: security -- The isolated registry must be gone before reporter-backed validations can publish passing risk evidence test("cleans the protected registry before passing risk evidence", () => { const steps = multiarchJob(workflow()).steps as Array>; diff --git a/test/helpers/runtime-provider-bundle.ts b/test/helpers/runtime-provider-bundle.ts index 64302e2c84d..e1a4a9076ae 100644 --- a/test/helpers/runtime-provider-bundle.ts +++ b/test/helpers/runtime-provider-bundle.ts @@ -94,6 +94,7 @@ export function createInMemoryRuntimeProviderBundle({ sandboxNamespace: "scoped" as const, hostGatewayIp: null, includeSupervisorBin: true, + driverConfigLayout: "split-supervisor" as const, processOwnership: "scoped-namespace" as const, }, network: { diff --git a/test/inference/managed/managed-image-interrupted-recovery.test.ts b/test/inference/managed/managed-image-interrupted-recovery.test.ts index 9297f0feef4..33d21b7c304 100644 --- a/test/inference/managed/managed-image-interrupted-recovery.test.ts +++ b/test/inference/managed/managed-image-interrupted-recovery.test.ts @@ -130,6 +130,7 @@ beforeEach(async () => { stdout: `sha256:${"b".repeat(64)}`, stderr: "", } as any) + .mockReturnValueOnce({ status: 0, stdout: "openshell 0.0.116", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ @@ -137,13 +138,15 @@ beforeEach(async () => { stdout: "", stderr: `Error: No such network: ${scope.networkName}`, } as any); - mocks.capture.mockImplementation(() => - commands.mock.calls.some( - ([command, args]: [string, readonly string[]]) => - command === "openshell" && args[0] === "sandbox" && args[1] === "delete", - ) - ? "[]" - : sandboxObservation(), + mocks.capture.mockImplementation((args: readonly string[]) => + args[0] === "--version" + ? "openshell 0.0.116" + : commands.mock.calls.some( + ([command, args]: [string, readonly string[]]) => + command === "openshell" && args[0] === "sandbox" && args[1] === "delete", + ) + ? "[]" + : sandboxObservation(), ); }); afterEach(() => { @@ -166,6 +169,7 @@ it("recovers only the marked isolated sandbox, gateway, volumes and network", as ["openshell", "sandbox", "delete", "-g", "nemoclaw", input.sandbox], ["openshell", "gateway", "remove", "nemoclaw"], ["docker", "image", "inspect", "--format", "{{.Id}}", input.image], + ["openshell", "--version"], [ "docker", "ps", @@ -205,9 +209,80 @@ it("recovers only the marked isolated sandbox, gateway, volumes and network", as expect(fs.existsSync(foreign)).toBe(true); expect(process.env).toEqual(previous); await cleanupInterruptedManagedImageOpenShellE2e(input, scope); - expect(commands).toHaveBeenCalledTimes(6); + expect(commands).toHaveBeenCalledTimes(7); }); +it.each([ + ["0.0.116", "--limit"], + ["0.1.3", "--page-size"], +])("confirms strict absence with OpenShell %s before continuing cleanup", async (version, flag) => { + mocks.capture + .mockReset() + .mockReturnValueOnce(sandboxObservation()) + .mockImplementationOnce((args) => { + expect(commands).toHaveBeenCalledTimes(1); + expect(args).toEqual(["--version"]); + return `openshell ${version}`; + }) + .mockReturnValueOnce("[]"); + await cleanupInterruptedManagedImageOpenShellE2e(input, scope); + expect(mocks.capture.mock.calls.slice(1)).toEqual([ + [["--version"], { ignoreError: false, timeout: 30000, maxBuffer: 1024 * 1024 }], + [ + [ + "sandbox", + "list", + "-g", + "nemoclaw", + "--selector", + `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${createAttemptNonce}`, + "--output", + "json", + flag, + "2", + ], + { ignoreError: false, timeout: 30000, maxBuffer: 1024 * 1024 }, + ], + ]); + expect(commands).toHaveBeenCalledTimes(7); + expect(mocks.stopGateway).toHaveBeenCalledOnce(); + expect(mocks.removeVolumes).toHaveBeenCalledOnce(); + expect(fs.existsSync(scope.stateDir)).toBe(false); +}); + +it.each(["not-json", "{}", "null", sandboxObservation(), "[]\n[]"])( + "preserves recovery evidence for rejected 0.1.3 absence response %s", + async (response) => { + mocks.capture + .mockReset() + .mockReturnValueOnce(sandboxObservation()) + .mockReturnValueOnce("openshell 0.1.3") + .mockReturnValueOnce(response); + await expect(cleanupInterruptedManagedImageOpenShellE2e(input, scope)).rejects.toThrow( + "sandbox identity or deletion is unproven", + ); + expect(mocks.capture.mock.calls.at(-1)?.[0]).toEqual([ + "sandbox", + "list", + "-g", + "nemoclaw", + "--selector", + `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${createAttemptNonce}`, + "--output", + "json", + "--page-size", + "2", + ]); + expect(commands).toHaveBeenCalledTimes(1); + expect(mocks.stopGateway).not.toHaveBeenCalled(); + expect(mocks.removeVolumes).not.toHaveBeenCalled(); + expect(fs.existsSync(path.join(scope.stateDir, "qualification-sandbox-deleted.json"))).toBe( + false, + ); + expect(fs.existsSync(scope.stateDir)).toBe(true); + }, +); + it.each(["foreign marker", "oversized marker"])( "rejects %s before any external mutation", async (reason) => { @@ -292,6 +367,7 @@ it("rejects ambiguous network inspection instead of deleting recovery state", as stdout: `sha256:${"b".repeat(64)}`, stderr: "", } as any) + .mockReturnValueOnce({ status: 0, stdout: "openshell 0.0.116", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any); await expect(cleanupInterruptedManagedImageOpenShellE2e(input, scope)).rejects.toThrow( @@ -320,7 +396,7 @@ it("preserves state when the existing volume verifier rejects a foreign volume", ["inspect", "--format", "{{json .}}", "owned-volume"], expect.anything(), ); - expect(commands).toHaveBeenCalledTimes(4); + expect(commands).toHaveBeenCalledTimes(5); expect(fs.existsSync(path.join(scope.stateDir, "qualification-owner.json"))).toBe(true); }); @@ -372,6 +448,7 @@ it("continues cleanup after a post-deletion failure without deleting the sandbox stdout: `sha256:${"b".repeat(64)}`, stderr: "", } as any) + .mockReturnValueOnce({ status: 0, stdout: "openshell 0.0.116", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ @@ -387,6 +464,7 @@ it("continues cleanup after a post-deletion failure without deleting the sandbox ).toEqual([ ["openshell", "gateway", "remove", "nemoclaw"], ["docker", "image", "inspect", "--format", "{{.Id}}", input.image], + ["openshell", "--version"], [ "docker", "ps", @@ -567,6 +645,7 @@ it("cleans setup before any create submission without issuing sandbox deletion", stdout: `sha256:${"b".repeat(64)}`, stderr: "", } as any) + .mockReturnValueOnce({ status: 0, stdout: "openshell 0.0.116", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ @@ -642,6 +721,7 @@ it("cleans partial setup when no exact sandbox container was created", async () stdout: `sha256:${"b".repeat(64)}`, stderr: "", } as any) + .mockReturnValueOnce({ status: 0, stdout: "openshell 0.0.116", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ @@ -651,7 +731,7 @@ it("cleans partial setup when no exact sandbox container was created", async () } as any); await cleanupInterruptedManagedImageOpenShellE2e(input, scope); expect(fs.existsSync(scope.stateDir)).toBe(false); - expect(commands).toHaveBeenCalledTimes(6); + expect(commands).toHaveBeenCalledTimes(7); }); it("removes the exact leftover container without removing a foreign container", async () => { @@ -685,6 +765,7 @@ it("removes the exact leftover container without removing a foreign container", .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: content, stderr: "" } as any) + .mockReturnValueOnce({ status: 0, stdout: "openshell 0.0.116", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "owned-container foreign-container", @@ -694,6 +775,7 @@ it("removes the exact leftover container without removing a foreign container", .mockReturnValueOnce({ status: 0, stdout: foreign, stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: content, stderr: "" } as any) + .mockReturnValueOnce({ status: 0, stdout: "openshell 0.0.116", stderr: "" } as any) .mockReturnValueOnce({ status: 0, stdout: "foreign-container", diff --git a/test/inference/managed/managed-image-protected-runtime-contract.test.ts b/test/inference/managed/managed-image-protected-runtime-contract.test.ts index 40109e06411..e0ef49e38f5 100644 --- a/test/inference/managed/managed-image-protected-runtime-contract.test.ts +++ b/test/inference/managed/managed-image-protected-runtime-contract.test.ts @@ -5,11 +5,23 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { runInNewContext } from "node:vm"; import { describe, expect, it, vi } from "vitest"; +// This suite exercises the controller after reviewed 0.1.3 projection. +vi.mock("../../../src/lib/onboard/openshell-version.ts", async (original) => ({ + ...(await original()), + SUPPORTED_OPENSHELL_FALLBACK_VERSION: "0.1.3", +})); + import { managedStartupE2eProfile } from "../../../scripts/checks/generate-managed-startup-profile-fixture.mts"; import { + configureLocalInferenceRoute, + assertLocalInferenceAgentResponse, + localInferenceAgentCommand, + localInferenceProbe, + managedImageLocalInferenceBaseUrl, createManagedImageSandboxWithDiagnostics, MANAGED_IMAGE_LOCAL_INFERENCE_KINDS, MANAGED_IMAGE_PROTECTED_SANDBOX_PREFIX, @@ -20,17 +32,18 @@ import { withManagedImageLocalInferenceProfile, } from "../../../scripts/checks/managed-image-protected-runtime-contract.ts"; import { + assertExactSandboxImage, assertOpenClawHeartbeatStart, createBootstrapCompletionFailureInjection, managedOpenClawHeartbeatLogProbe, assertFailedSandboxOwnerCleanupRetention, type ManagedImageCommandRunner, - managedImageLocalInferenceBaseUrl, managedImageOpenShellBasePolicyPath, managedImageOpenShellCommittedProbe, managedImageOpenShellProbe, managedOpenClawHeartbeatProbe, parseManagedImageOpenShellE2eInputs, + printExactContainerFailureState, protectedManagedStateRootDriverConfig, removeManagedImageGatewayStateIfSafe, resolveManagedImageOnboardModule, @@ -41,9 +54,442 @@ import { } from "../../../src/lib/onboard/managed-startup/profile.ts"; import { createManagedStartupRootApplyRequest } from "../../../src/lib/onboard/managed-startup/root-apply.ts"; import type { RuntimeProviderBundle } from "../../../src/lib/onboard/runtime-provider/contract.ts"; +import { gatewayIdForStateDir } from "../../../src/lib/onboard/gateway/process-environment.ts"; +import { prepareNativeLocalProfile } from "../../../src/lib/inference/native-local/profile.ts"; +import type { OpenShellProviderAdapter } from "../../../src/lib/adapters/openshell/provider-adapter.ts"; const IMAGE = `localhost:5000/nemoclaw-managed-protected/openclaw@sha256:${"a".repeat(64)}`; const VALID_SANDBOX = "managed-openclaw"; + +function nativeQualificationFixture( + agent: "openclaw" | "hermes" | "langchain-deepagents-code", + kind: "ollama" | "nim" | "vllm" | "llama-cpp", +) { + const route = resolveManagedImageLocalInferenceRoute(kind); + const binding = { + provider: route.providerName, + ...(agent === "langchain-deepagents-code" ? {} : { agent }), + endpointUrl: route.defaultBaseUrl, + credentialEnv: "NEMOCLAW_LOCAL_INFERENCE_TOKEN", + authMode: + kind === "nim" || kind === "vllm" ? ("sentinel" as const) : ("authenticated" as const), + gatewayName: "nemoclaw", + sandboxName: VALID_SANDBOX, + }; + const prepared = prepareNativeLocalProfile(binding); + const metadata = { + name: prepared.providerName, + type: prepared.profileId, + credentialKeys: [binding.credentialEnv], + configKeys: [], + revision: { id: "fixture-owned-id", resourceVersion: 1 }, + }; + const imported: unknown[] = []; + const adapter = { + importProviderProfile: vi.fn(async ({ profilePath }) => { + imported.push(JSON.parse(fs.readFileSync(profilePath, "utf8"))); + return { ok: true as const }; + }), + getProvider: vi + .fn() + .mockResolvedValueOnce({ + ok: false, + error: { kind: "command", reason: "not_found", message: "absent" }, + }) + .mockResolvedValue({ ok: true, value: metadata }), + createProvider: vi.fn(async () => ({ ok: true as const })), + updateProvider: vi.fn(), + deleteProvider: vi.fn(), + inspectProviderProfile: vi.fn(), + listProviderAttachments: vi.fn(), + attachProvider: vi.fn(), + detachProvider: vi.fn(), + listProviders: vi.fn(), + ensureProviderPolicyComposition: vi.fn(), + configureProviderRefresh: vi.fn(), + getProviderRefreshStatus: vi.fn(), + } satisfies OpenShellProviderAdapter; + const runOpenshell = vi.fn((_args: string[]) => ({ + status: 0, + stdout: JSON.stringify({ revisions: [], next_page_token: "" }), + stderr: "", + })); + const input = { + agent, + image: IMAGE, + sandbox: VALID_SANDBOX, + localProvider: kind, + model: "fixture-model", + }; + return { adapter, binding, imported, input, metadata, prepared, route, runOpenshell }; +} + +it.each([ + ["openclaw", "ollama"], + ["hermes", "ollama"], + ["langchain-deepagents-code", "ollama"], + ["openclaw", "nim"], + ["hermes", "nim"], + ["langchain-deepagents-code", "nim"], + ["openclaw", "vllm"], + ["hermes", "vllm"], + ["langchain-deepagents-code", "vllm"], + ["openclaw", "llama-cpp"], +] as const)( + "registers the exact %s %s native boundary without a global inference route", + async (agent, kind) => { + const f = nativeQualificationFixture(agent, kind); + expect(f.prepared.document.binaries).toContain( + agent === "langchain-deepagents-code" ? "/opt/venv/bin/python3" : "/usr/bin/python3.13", + ); + const receipt = await configureLocalInferenceRoute( + f, + f.input, + { [f.route.credentialEnv]: "fixture-local-token" }, + f.adapter, + ); + expect(receipt).toEqual({ + ...f.binding, + profileId: f.prepared.profileId, + providerName: f.prepared.providerName, + schemaVersion: 1, + providerId: "fixture-owned-id", + }); + expect(f.imported).toEqual([f.prepared.document]); + expect(f.adapter.createProvider).toHaveBeenCalledWith({ + target: { kind: "named", gatewayName: "nemoclaw" }, + name: f.prepared.providerName, + type: f.prepared.profileId, + credentials: [{ name: f.binding.credentialEnv, value: "fixture-local-token" }], + config: [], + fromExisting: false, + }); + expect(f.runOpenshell.mock.calls[0][0]).toEqual([ + "policy", + "list", + "-g", + "nemoclaw", + "--global", + "--page-size", + "1", + "--output", + "json", + ]); + expect(f.runOpenshell).toHaveBeenCalledTimes(1); + }, +); + +it("refuses an existing qualification provider without adopting or rotating it", async () => { + const f = nativeQualificationFixture("openclaw", "ollama"); + f.adapter.getProvider.mockReset().mockResolvedValue({ ok: true, value: f.metadata }); + await expect( + configureLocalInferenceRoute( + f, + f.input, + { [f.route.credentialEnv]: "fixture-token" }, + f.adapter, + ), + ).rejects.toThrow("without a matching NemoClaw ownership receipt"); + expect(f.adapter.createProvider).not.toHaveBeenCalled(); + expect(f.adapter.updateProvider).not.toHaveBeenCalled(); +}); + +it.each(["", "raw\ncredential"])( + "rejects invalid qualification credential %j before importing a profile", + async (credential) => { + const f = nativeQualificationFixture("hermes", "ollama"); + await expect( + configureLocalInferenceRoute(f, f.input, { [f.route.credentialEnv]: credential }, f.adapter), + ).rejects.toThrow("required for protected local inference"); + expect(f.adapter.importProviderProfile).not.toHaveBeenCalled(); + expect(f.adapter.createProvider).not.toHaveBeenCalled(); + }, +); + +function runNativeQualificationProbe(handle: string, model = "fixture-model") { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "native-qualification-")); + try { + fs.writeFileSync( + path.join(directory, "curl"), + '#!/bin/sh\nprintf "%s\\n" "$@" > "$PROBE_ARGS"\nprintf \'{"choices":[{"message":{"content":"PONG"}}]}\'\n', + { mode: 0o700 }, + ); + const argsPath = path.join(directory, "args"); + const [command, ...commandArgs] = localInferenceProbe({ + agent: "openclaw", + image: IMAGE, + sandbox: VALID_SANDBOX, + localProvider: "ollama", + model, + }); + const result = spawnSync(command, commandArgs, { + encoding: "utf8", + env: { + PATH: `${directory}:${process.env.PATH}`, + TMPDIR: directory, + PROBE_ARGS: argsPath, + NEMOCLAW_LOCAL_INFERENCE_TOKEN: handle, + }, + }); + return { + result, + args: fs.existsSync(argsPath) ? fs.readFileSync(argsPath, "utf8") : null, + remaining: fs + .readdirSync(directory) + .filter((name) => name.startsWith("nemoclaw-managed-image-inference.")), + }; + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +} + +describe("protected local inference agent response", () => { + it.each([ + { response: "", expected: 'response=""' }, + { response: "unexpected answer", expected: 'response="unexpected answer"' }, + { response: "fixture-private-value", expected: 'response=""' }, + { response: "x".repeat(600), expected: `response="${"x".repeat(512)}"` }, + ])("reports bounded, redacted rejected output [%#]", ({ response, expected }) => { + expect(() => + assertLocalInferenceAgentResponse( + { + agent: "hermes", + image: IMAGE, + sandbox: VALID_SANDBOX, + localProvider: "ollama", + model: "fixture-model", + }, + response, + { NVIDIA_API_KEY: "fixture-private-value" }, + ), + ).toThrow(expected); + }); + const openclaw = { + status: "ok", + result: { + meta: { aborted: false, agentMeta: { provider: "inference", model: "fixture-model" } }, + payloads: [{ text: "PONG" }], + }, + }; + const dcode = { data: { status: "success", exit_code: 0, response: "PONG" } }; + const input = (agent: "openclaw" | "hermes" | "langchain-deepagents-code") => ({ + agent, + image: IMAGE, + sandbox: VALID_SANDBOX, + localProvider: "ollama" as const, + model: "fixture-model", + }); + + it.each([ + ["openclaw", JSON.stringify(openclaw), "openclaw"], + ["hermes", "PONG\n", "hermes"], + ["langchain-deepagents-code", JSON.stringify(dcode), "dcode"], + ] as const)( + "uses %s's configured route and accepts its assistant response", + (agent, stdout, binary) => { + const command = localInferenceAgentCommand(input(agent)); + expect(command[0]).toBe(binary); + expect(command).not.toContain("--model"); + expect(command).not.toContain("--provider"); + expect(() => assertLocalInferenceAgentResponse(input(agent), stdout)).not.toThrow(); + }, + ); + + it.each([ + [ + "openclaw", + JSON.stringify({ ...openclaw, result: { ...openclaw.result, payloads: [], prompt: "PONG" } }), + ], + ["hermes", "request failed; prompt was PONG"], + [ + "langchain-deepagents-code", + JSON.stringify({ data: { ...dcode.data, response: "", prompt: "PONG" } }), + ], + [ + "langchain-deepagents-code", + JSON.stringify({ data: { ...dcode.data, status: "turn_limit" } }), + ], + ["langchain-deepagents-code", JSON.stringify({ data: { ...dcode.data, exit_code: 1 } })], + ["openclaw", "not JSON"], + ["langchain-deepagents-code", "null"], + ] as const)("rejects %s diagnostics or an incomplete turn", (agent, stdout) => { + expect(() => assertLocalInferenceAgentResponse(input(agent), stdout)).toThrow(); + }); + + it.each([ + ["aborted", { ...openclaw.result.meta, aborted: true }], + [ + "wrong provider", + { ...openclaw.result.meta, agentMeta: { provider: "nvidia", model: "fixture-model" } }, + ], + [ + "wrong model", + { ...openclaw.result.meta, agentMeta: { provider: "inference", model: "other-model" } }, + ], + ])("rejects an OpenClaw %s result even when its text is PONG", (_reason, meta) => { + expect(() => + assertLocalInferenceAgentResponse( + input("openclaw"), + JSON.stringify({ + ...openclaw, + result: { ...openclaw.result, meta }, + }), + ), + ).toThrow("configured local model"); + }); +}); + +it.each([ + "openshell:resolve:env:v7_NEMOCLAW_LOCAL_INFERENCE_TOKEN", + `openshell:resolve:env:s${"a".repeat(64)}_NEMOCLAW_LOCAL_INFERENCE_TOKEN`, +])("uses the runtime-scoped local handle %s for the actual endpoint", (handle) => { + const { result, args, remaining } = runNativeQualificationProbe(handle); + expect(result.status, result.stderr).toBe(0); + expect(args).toContain("http://host.openshell.internal:11435/v1/chat/completions\n"); + expect(args).toContain(`Authorization: Bearer ${handle}\n`); + expect(remaining).toEqual([]); +}); + +it("passes shell syntax in the local model unchanged to curl", () => { + const model = "model'$(printf substituted)`printf expanded`$HOME"; + const { result, args, remaining } = runNativeQualificationProbe( + "openshell:resolve:env:v7_NEMOCLAW_LOCAL_INFERENCE_TOKEN", + model, + ); + expect(result.status, result.stderr).toBe(0); + const argv = args!.split("\n"); + expect(JSON.parse(argv[argv.indexOf("--data") + 1]).model).toBe(model); + expect(remaining).toEqual([]); +}); + +it.each([ + "raw-token", + "openshell:resolve:env:NEMOCLAW_LOCAL_INFERENCE_TOKEN", + "openshell:resolve:env:v7_NEMOCLAW_OTHER_TOKEN", + "openshell:resolve:env:v7_NEMOCLAW_LOCAL_INFERENCE_TOKEN\nraw", +])("rejects unscoped or invalid native probe credential %j before curl", (handle) => { + const { result, args, remaining } = runNativeQualificationProbe(handle); + expect(result.status).toBe(2); + expect(args).toBeNull(); + expect(remaining).toEqual([]); +}); + +it("captures bounded owner startup logs and redacts secrets before cleanup", () => { + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const runner = vi.fn(() => ({ + status: 0, + stdout: "fixture-api-secret", + stderr: "", + })); + try { + printExactContainerFailureState("c".repeat(64), { API_KEY: "fixture-api-secret" }, runner); + const [argv, env, timeout] = runner.mock.calls[2]; + expect(argv.slice(0, 7)).toEqual([ + "docker", + "exec", + "--user", + "sandbox", + "c".repeat(64), + "/usr/local/bin/node", + "-e", + ]); + expect(env.API_KEY).toBe("fixture-api-secret"); + expect(timeout).toBe(5000); + expect(String(error.mock.calls.at(-1))).not.toContain("fixture-api-secret"); + const writes: unknown[] = []; + const close = vi.fn(); + const read = vi.fn((_fd, bytes) => { + bytes.write("safe"); + return 4; + }); + const open = vi + .fn() + .mockReturnValueOnce(1) + .mockImplementationOnce(() => { + throw new Error("symlink"); + }) + .mockReturnValueOnce(3); + runInNewContext(argv[7], { + Buffer, + process: { stdout: { write: (value: unknown) => writes.push(value) } }, + require: () => ({ + constants: { O_RDONLY: 0, O_NOFOLLOW: 2, O_NONBLOCK: 4 }, + openSync: open, + fstatSync: (fd: number) => ({ isFile: () => true, size: fd === 1 ? 4 : 262145 }), + readSync: read, + closeSync: close, + }), + }); + expect(open.mock.calls).toEqual( + ["/tmp/nemoclaw-start.log", "/tmp/gateway.log", "/tmp/dashboard.log"].map((file) => [ + file, + 6, + ]), + ); + expect(read).toHaveBeenCalledTimes(1); + expect(close.mock.calls).toEqual([[1], [3]]); + expect(writes.map(String).join("")).toContain("safe"); + expect(writes.filter((value) => value === "diagnostic log unavailable\n")).toHaveLength(2); + runner.mockClear(); + printExactContainerFailureState("short-id", {}, runner); + expect(runner).not.toHaveBeenCalled(); + } finally { + error.mockRestore(); + } +}); + +function observeWorkload(scenario: string): string { + const stateDir = path.join(os.tmpdir(), "managed-workload-owner"); + const containerId = "c".repeat(64); + const contentId = `sha256:${"d".repeat(64)}`; + const runCommand: ManagedImageCommandRunner = (argv) => { + const stdout = + argv[1] === "--version" + ? "openshell 0.1.3\n" + : argv[1] === "image" + ? contentId + : argv[1] === "ps" + ? argv.includes("label=openshell.ai/isolation-role=sandbox") && scenario !== "duplicate" + ? containerId + : `${containerId}\n${"e".repeat(64)}` + : JSON.stringify([ + { + Image: scenario === "image" ? "sha256:wrong" : contentId, + Config: { + Labels: { + "openshell.ai/managed-by": "openshell", + "openshell.ai/sandbox-name": VALID_SANDBOX, + "openshell.ai/sandbox-namespace": + scenario === "namespace" ? "other" : gatewayIdForStateDir(stateDir), + "openshell.ai/isolation-role": scenario === "role" ? "supervisor" : "sandbox", + }, + }, + HostConfig: { NetworkMode: scenario === "network" ? "bridge" : "none" }, + }, + ]); + return { status: 0, stdout, stderr: "" }; + }; + return assertExactSandboxImage( + { agent: "openclaw", image: IMAGE, sandbox: VALID_SANDBOX }, + stateDir, + {}, + runCommand, + ); +} + +it("accepts one exact isolated workload", () => { + expect(observeWorkload("valid")).toBe("c".repeat(64)); +}); + +it.each(["namespace", "role", "image", "network", "duplicate"])( + "rejects a workload with mismatched %s evidence", + (scenario) => { + expect(() => observeWorkload(scenario)).toThrow( + "exactly one running harness-owned PR image container", + ); + }, +); + const MANAGED_IMAGE_ONBOARD = resolveManagedImageOnboardModule( await import("../../../src/lib/onboard.ts"), ); @@ -757,6 +1203,7 @@ describe("protected managed-image runtime contract", () => { ["openclaw", true], ["hermes", false], ["hermes", true], + ["langchain-deepagents-code", false], ] as const)("supplies a valid local route for %s with providerless=%s", (agent, providerless) => { const original = managedStartupE2eProfile(agent, false, true, true); const profile = providerless @@ -775,9 +1222,9 @@ describe("protected managed-image runtime contract", () => { inference: { api: "openai-completions", model: "nvidia/nemotron-3-nano", - routedBaseUrl: "https://inference.local/v1", + routedBaseUrl: "http://host.openshell.internal:8000/v1", routeProvider: "inference", - upstreamEndpointUrl: null, + upstreamEndpointUrl: agent === "langchain-deepagents-code" ? route.defaultBaseUrl : null, upstreamProvider: "vllm-local", primaryModelRef: agent === "openclaw" ? "inference/nvidia/nemotron-3-nano" : null, compatibility: profile.inference?.compatibility ?? (agent === "openclaw" ? {} : null), diff --git a/test/inference/managed/managed-image-state-volume-cleanup.test.ts b/test/inference/managed/managed-image-state-volume-cleanup.test.ts new file mode 100644 index 00000000000..5bce416eb4f --- /dev/null +++ b/test/inference/managed/managed-image-state-volume-cleanup.test.ts @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, expect, it, vi } from "vitest"; +import { cleanupProtectedManagedStateVolumes } from "../../../scripts/checks/run-managed-image-openshell-e2e"; +import { createHermesStateVolumeDockerHarness } from "../../../src/lib/onboard/__test-helpers__/hermes-state-volume"; +import { managedStartupStateRoots } from "../../../src/lib/onboard/managed-startup/state-roots"; +import { managedImageRuntimeIdentity } from "../../../src/lib/onboard/managed-image/agents"; +import { + prepareManagedStateVolumes, + removeManagedStateVolumes, +} from "../../../src/lib/onboard/managed-workload/managed-state-volumes"; +import { createManagedStateVolumeCleanupFixture } from "../../support/managed-state-volume-cleanup"; +import { createDockerRuntimeProviderBundle } from "../../../src/lib/onboard/runtime-provider/docker"; + +it("explicitly tears down fixture volumes retained by incomplete bootstrap rollback", () => { + const docker = createHermesStateVolumeDockerHarness(); + const roots = managedStartupStateRoots({ + agent: "openclaw", + sandboxName: "fixture", + agentIdentity: managedImageRuntimeIdentity("openclaw"), + }); + const scope = prepareManagedStateVolumes( + { roots }, + { + runContainerEngine: docker.runDocker, + registerExitCleanup: () => () => undefined, + }, + )!; + expect(scope.cleanupIncompleteCreate()).toMatchObject([{ status: "retained" }]); + expect(docker.volume).not.toBeNull(); + const remove = vi.fn((input: Parameters[0]) => + removeManagedStateVolumes(input, { runContainerEngine: docker.runDocker }), + ); + const onboard = { managedWorkloadOnboard: { removeManagedStateVolumes: remove } } as never; + expect( + cleanupProtectedManagedStateVolumes({ + onboard, + roots, + preparationAttempted: true, + runtimeProvider: createDockerRuntimeProviderBundle(), + }), + ).toEqual([]); + expect(remove).toHaveBeenCalledOnce(); + expect(docker.volume).toBeNull(); +}); + +const mocks = vi.hoisted(() => ({ + prepare: vi.fn(), + remove: vi.fn(), + roots: vi.fn(), + startGateway: vi.fn(), + command: vi.fn(), +})); +vi.mock("node:child_process", async (original) => ({ + ...(await original()), + spawnSync: mocks.command, +})); +vi.mock("../../../src/lib/onboard/host-gateway-process", () => ({ + stopHostGatewayProcesses: () => ({ failed: [], ownershipFailures: [] }), +})); +vi.mock("../../../src/lib/onboard/initial-policy", () => ({ + prepareInitialSandboxCreatePolicy: () => ({ policyPath: "/fixture/policy.yaml" }), +})); +vi.mock("../../../src/lib/onboard.ts", () => ({ + default: { + openshellArgv: (args: string[]) => ["openshell", ...args], + runOpenshell: vi.fn(), + runCaptureOpenshell: vi.fn(), + sleepSeconds: vi.fn(), + startGatewayForRecovery: mocks.startGateway, + createForwardPortObserver: vi.fn(), + managedWorkloadOnboard: { + completeProviderManagedStartup: vi.fn(), + managedStartupStateRoots: mocks.roots, + managedStartupWorkspaceRoot: vi.fn(), + prepareManagedStateVolumes: mocks.prepare, + removeManagedStateVolumes: mocks.remove, + }, + }, +})); + +afterEach(() => { + vi.restoreAllMocks(); + vi.resetAllMocks(); +}); + +function fixture(failure: Parameters[0]) { + const setup = createManagedStateVolumeCleanupFixture(failure); + mocks.roots.mockReturnValue(setup.roots); + mocks.prepare.mockImplementation(setup.prepare); + mocks.remove.mockImplementation(setup.remove); + mocks.startGateway.mockImplementation(setup.startGateway); + mocks.command.mockImplementation(setup.command); + return setup; +} + +it.each(["second root", "verification"] as const)( + "removes its partial fixture volume after %s fails", + async (failure) => { + const setup = fixture(failure); + try { + await expect(setup.run()).rejects.toThrow(/injected/); + expect(mocks.prepare).toHaveBeenCalledOnce(); + expect(mocks.remove).toHaveBeenCalledOnce(); + expect(setup.docker.volume).toBeNull(); + expect(setup.docker.calls.filter(([operation]) => operation === "rm")).toEqual([ + ["rm", setup.roots[0]!.resourceIdentity], + ]); + expect(setup.remainingState()).toEqual([]); + } finally { + setup.dispose(); + } + }, +); + +it("does not attempt volume removal after a failure before preparation", async () => { + const setup = fixture("before preparation"); + try { + await expect(setup.run()).rejects.toThrow("injected early failure"); + expect(mocks.prepare).not.toHaveBeenCalled(); + expect(mocks.remove).not.toHaveBeenCalled(); + expect(setup.docker.volume).toBeNull(); + expect(setup.docker.calls).toEqual([]); + expect(setup.remainingState()).toEqual([]); + } finally { + setup.dispose(); + } +}); + +it("preserves an older same-name volume with ordinary NemoClaw ownership labels", async () => { + const setup = fixture("foreign ownership"); + try { + await expect(setup.run()).rejects.toThrow(/cleanup not-owned/); + expect(mocks.prepare).toHaveBeenCalledOnce(); + expect(mocks.remove).toHaveBeenCalledOnce(); + expect(setup.docker.volume).toEqual({ + name: setup.roots[0]!.resourceIdentity, + labels: { ...setup.roots[0]!.ownershipLabels }, + }); + expect(setup.docker.calls.filter(([operation]) => operation === "rm")).toEqual([]); + expect(setup.remainingState()).toEqual([]); + } finally { + setup.dispose(); + } +}); diff --git a/test/mcp/mcp-tool-discovery-image-contract.test.ts b/test/mcp/mcp-tool-discovery-image-contract.test.ts index db8badb5608..3026209a9f4 100644 --- a/test/mcp/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp/mcp-tool-discovery-image-contract.test.ts @@ -214,7 +214,7 @@ describe("MCP tool discovery image contract", () => { // source-shape-contract: security -- Exact reviewed runtime digests reject substituted executable and license artifacts before managed image construction. it.each([ { - expectedHash: "23fc86eefdd37dcfce995c7f89ac9510abb792d95190aa53ca8ffdcc5e5e5591", + expectedHash: "18cda1ab5f57d3c4cdab0b6de5abad09f73bea11151bc149d141de26a593b398", relativePath: "managed-startup-direct-image-runtime.bundle", }, { diff --git a/test/support/managed-state-volume-cleanup.ts b/test/support/managed-state-volume-cleanup.ts new file mode 100644 index 00000000000..9d30da4d1b0 --- /dev/null +++ b/test/support/managed-state-volume-cleanup.ts @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import net from "node:net"; +import { vi } from "vitest"; +import { runManagedImageOpenShellE2e } from "../../scripts/checks/run-managed-image-openshell-e2e"; +import { createHermesStateVolumeDockerHarness } from "../../src/lib/onboard/__test-helpers__/hermes-state-volume"; +import { managedStartupStateRoots } from "../../src/lib/onboard/managed-startup/state-roots"; +import { managedImageRuntimeIdentity } from "../../src/lib/onboard/managed-image/agents"; +import { + prepareManagedStateVolumes, + removeManagedStateVolumes, +} from "../../src/lib/onboard/managed-workload/managed-state-volumes"; + +type PreparationFailure = + | "second root" + | "verification" + | "before preparation" + | "foreign ownership"; + +export function createManagedStateVolumeCleanupFixture(failure: PreparationFailure) { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), "partial-state-volume-")); + vi.stubEnv("RUNNER_TEMP", temp); + vi.spyOn(console, "error").mockImplementation(() => undefined); + vi.spyOn(net, "createServer").mockReturnValue({ + unref() {}, + once() {}, + listen(_port: number, _host: string, callback: () => void) { + callback(); + }, + close(callback: () => void) { + callback(); + }, + } as unknown as net.Server); + const roots = managedStartupStateRoots({ + agent: "openclaw", + sandboxName: "partial-fixture", + agentIdentity: managedImageRuntimeIdentity("openclaw"), + }); + const extraRoot = { ...roots[0]!, resourceIdentity: "second-fixture-volume" }; + const foreignVolume = + failure === "foreign ownership" + ? { name: roots[0]!.resourceIdentity, labels: { ...roots[0]!.ownershipLabels } } + : null; + const docker = createHermesStateVolumeDockerHarness(foreignVolume); + let cleanupStarted = false; + const runDocker = (args: readonly string[]) => { + if (args.at(-1) === extraRoot.resourceIdentity) { + return args[0] === "create" + ? { status: 1, stderr: "injected second-root failure" } + : { status: 1, stderr: "no such volume" }; + } + if (failure === "verification" && args[0] === "inspect" && docker.volume && !cleanupStarted) { + return { status: 1, stderr: "injected verification failure" }; + } + return docker.runDocker(args); + }; + return { + roots: failure === "second root" ? [...roots, extraRoot] : roots, + docker, + prepare: (input: Parameters[0]) => + prepareManagedStateVolumes(input, { + runContainerEngine: runDocker, + registerExitCleanup: () => () => undefined, + }), + remove: (input: Parameters[0]) => { + cleanupStarted = true; + return removeManagedStateVolumes(input, { runContainerEngine: runDocker }); + }, + startGateway: async () => { + if (failure === "before preparation") throw new Error("injected early failure"); + }, + command: (command: string, args: string[]) => { + const ok = { status: 0, stdout: "", stderr: "" }; + if (command === "tail") return ok; + if (command === "openshell") + return args[0] === "--version" ? { ...ok, stdout: "openshell 0.0.116" } : ok; + if (command === "docker" && args[0] === "image") + return { ...ok, stdout: `sha256:${"b".repeat(64)}` }; + if (command === "docker" && args[0] === "ps") return ok; + if (command === "docker" && args[0] === "network") + return args[1] === "inspect" + ? { status: 1, stderr: `Error: No such network: ${args[2]}` } + : ok; + throw new Error(`Unexpected command: ${command} ${args.join(" ")}`); + }, + run: () => + runManagedImageOpenShellE2e({ + agent: "openclaw", + image: `ghcr.io/nvidia/nemoclaw/openclaw@sha256:${"a".repeat(64)}`, + sandbox: "partial-fixture", + }), + remainingState: () => fs.readdirSync(temp), + dispose: () => fs.rmSync(temp, { recursive: true, force: true }), + }; +} diff --git a/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts b/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts index 8f2f0087e5c..09ab30350c5 100644 --- a/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts +++ b/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts @@ -157,6 +157,10 @@ export function validateManagedImageProtectedRuntimeWorkflow(workflow: WorkflowR workflowSteps, "Validate protected runtime exact-head dispatch", ); + if (workflowSteps[0] !== guard) errors.push(`${JOB_ID} must record its deadline before setup`); + requireFragments(errors, guard, [ + `printf 'NEMOCLAW_PROTECTED_RUNTIME_STARTED_AT_MS=%s000\\n' "$(date +%s)" >> "$GITHUB_ENV"`, + ]); requireValues(errors, `${JOB_ID} exact-head guard env`, record(guard?.env), { BASE_SHA: "${{ inputs.base_sha || github.event.before || github.sha }}", CHECKOUT_SHA: "${{ inputs.checkout_sha || github.sha }}", @@ -529,6 +533,17 @@ export function validateManagedImageProtectedRuntimeWorkflow(workflow: WorkflowR path: "e2e-artifacts/live/managed-image-protected-runtime/", }); requireStep(errors, workflowSteps, "Clean up Docker auth"); + for (const [name, minutes] of [ + ["Remove isolated protected runtime registry", 2], + ["Restore trusted OpenShell sources", 1], + ["Upload protected managed-image runtime artifacts", 4], + ["Clean up Docker auth", 1], + ] as const) { + const step = workflowSteps.find((candidate) => candidate.name === name); + if (step?.["timeout-minutes"] !== minutes) { + errors.push(`${JOB_ID} step '${name}' must fit its ${minutes} minute finalization budget`); + } + } requireOrderedSteps(errors, workflowSteps, [ "Validate protected runtime exact-head dispatch", "Checkout trusted protected runtime qualification", diff --git a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts index c593fb0a417..2afe0cd6ee4 100644 --- a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts +++ b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts @@ -615,6 +615,13 @@ export function validateUploadE2eArtifactsInvocations(workflow: WorkflowRecord): const upload = uploadSteps[0]; const explicitContract = EXPLICIT_UPLOAD_CONTRACTS.get(jobName); const allowedKeys = explicitContract ? ["if", "name", "uses", "with"] : ["if", "name", "uses"]; + if (jobName === "managed-image-protected-runtime") { + allowedKeys.push("timeout-minutes"); + allowedKeys.sort(); + if (upload["timeout-minutes"] !== 4) { + errors.push(`${jobName} artifact upload must retain its four-minute finalization budget`); + } + } if (!isDeepStrictEqual(sortedKeys(upload), allowedKeys)) { errors.push(`${jobName} upload-e2e-artifacts invocation must not override its contract`); } diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 312a6d1170d..066a8c745d3 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -1632,10 +1632,18 @@ function requireCanonicalDockerHubCleanupRun( ): void { if (!cleanupStep) return; + const expectedKeys = [...DOCKER_HUB_CLEANUP_KEYS]; + if (jobName === "managed-image-protected-runtime") { + expectedKeys.push("timeout-minutes"); + expectedKeys.sort(); + if (cleanupStep["timeout-minutes"] !== 1) { + errors.push(`${jobName} Docker Hub cleanup must retain its one-minute finalization budget`); + } + } const cleanupKeys = Object.keys(cleanupStep).sort(); if ( - cleanupKeys.length !== DOCKER_HUB_CLEANUP_KEYS.length || - cleanupKeys.some((key, index) => key !== DOCKER_HUB_CLEANUP_KEYS[index]) + cleanupKeys.length !== expectedKeys.length || + cleanupKeys.some((key, index) => key !== expectedKeys[index]) ) { errors.push(`${jobName} Docker Hub cleanup step must contain exactly name, if, shell, and run`); } diff --git a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle index cc552a7affd..1b170cee26f 100644 --- a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle +++ b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle @@ -1,13 +1,13 @@ -"use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:true}):target,mod));var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var managed_startup_direct_entry_exports={};__export(managed_startup_direct_entry_exports,{MANAGED_STARTUP_CA_ENV:()=>MANAGED_STARTUP_CA_ENV,MANAGED_STARTUP_COMPLETION_FILE:()=>MANAGED_STARTUP_COMPLETION_FILE,MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION:()=>MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION,MANAGED_STARTUP_MERGED_CA_FILE:()=>MANAGED_STARTUP_MERGED_CA_FILE,MANAGED_STARTUP_PROFILE_ENV:()=>MANAGED_STARTUP_PROFILE_ENV,MANAGED_STARTUP_RUNTIME_ENV_FILE:()=>MANAGED_STARTUP_RUNTIME_ENV_FILE,MANAGED_STARTUP_RUNTIME_EXECUTABLE:()=>MANAGED_STARTUP_RUNTIME_EXECUTABLE,ManagedStartupImageActionPlanError:()=>ManagedStartupImageActionPlanError,ManagedStartupImageRuntimeError:()=>ManagedStartupImageRuntimeError,NVIDIA_HOSTED_CREDENTIAL_ENV:()=>NVIDIA_HOSTED_CREDENTIAL_ENV,NVIDIA_HOSTED_NATIVE_ENDPOINT:()=>NVIDIA_HOSTED_NATIVE_ENDPOINT,applyManagedStartupCommandEnvironmentPlan:()=>applyManagedStartupCommandEnvironmentPlan,applyManagedStartupImageProfile:()=>applyManagedStartupImageProfile,applyManagedStartupRootRequest:()=>applyManagedStartupRootRequest,atomicWriteRootFile:()=>atomicWriteRootFile,buildManagedStartupImageActionPlan:()=>buildManagedStartupImageActionPlan,hostedNativeProvider:()=>hostedNativeProvider,installCorporateCaSystemAnchors:()=>installCorporateCaSystemAnchors,installHermesManagedPolicy:()=>installHermesManagedPolicy,main:()=>main,managedStartupSandboxPrefix:()=>managedStartupSandboxPrefix,normalizeHermesManagedConfigDescriptor:()=>normalizeHermesManagedConfigDescriptor,normalizeManagedStartupWorkspaceRoot:()=>normalizeManagedStartupWorkspaceRoot,publishManagedStartupCompletionAfterCommit:()=>publishManagedStartupCompletionAfterCommit,readStableRegularFile:()=>readStableRegularFile,readStableRegularFileSnapshot:()=>readStableRegularFileSnapshot,serializeManagedStartupCompletionMarker:()=>serializeManagedStartupCompletionMarker,serializeManagedStartupRuntimeEnvironment:()=>serializeManagedStartupRuntimeEnvironment,validateManagedStartupApplicationRuntimePlan:()=>validateManagedStartupApplicationRuntimePlan,verifyManagedStartupImageCompletion:()=>verifyManagedStartupImageCompletion,waitForManagedStartupImageCompletion:()=>waitForManagedStartupImageCompletion});module.exports=__toCommonJS(managed_startup_direct_entry_exports);var NVIDIA_HOSTED_NATIVE_ENDPOINT="https://integrate.api.nvidia.com/v1";var NVIDIA_HOSTED_CREDENTIAL_ENV="NVIDIA_INFERENCE_API_KEY";var NATIVE_NVIDIA_AUTH_HEADER_SCRIPT=['case "${NVIDIA_INFERENCE_API_KEY:-}" in *[!a-zA-Z0-9:_]*) exit 2 ;; esac',`printf '%s' "\${NVIDIA_INFERENCE_API_KEY:-}" | LC_ALL=C grep -Eq '^openshell:resolve:env:((v[0-9]{1,20}|s[a-f0-9]{64})_)?NVIDIA_INFERENCE_API_KEY$' || exit 2`,'AUTH_HEADER="Authorization: Bearer ${NVIDIA_INFERENCE_API_KEY}"'].join("; ");var import_node_crypto=require("node:crypto");var HOSTED_NATIVE_PROVIDERS=[{logicalProvider:"openai-api",label:"OpenAI",profileId:"nemoclaw-openai-inference-v1",providerName:"nemoclaw-openai-api-v1",credentialEnv:"OPENAI_API_KEY",endpoint:"https://api.openai.com/v1",api:"openai-completions"},{logicalProvider:"anthropic-prod",label:"Anthropic",profileId:"nemoclaw-anthropic-inference-v1",providerName:"nemoclaw-anthropic-prod-v1",credentialEnv:"ANTHROPIC_API_KEY",endpoint:"https://api.anthropic.com",api:"anthropic-messages"},{logicalProvider:"gemini-api",label:"Gemini",profileId:"nemoclaw-gemini-inference-v1",providerName:"nemoclaw-gemini-api-v1",credentialEnv:"GEMINI_API_KEY",endpoint:"https://generativelanguage.googleapis.com/v1beta/openai/",api:"openai-completions"},{logicalProvider:"openrouter-api",label:"OpenRouter",profileId:"nemoclaw-openrouter-inference-v1",providerName:"nemoclaw-openrouter-api-v1",credentialEnv:"OPENROUTER_API_KEY",endpoint:"https://openrouter.ai/api/v1",api:"openai-completions"},{logicalProvider:"hermes-provider",label:"Hermes Provider",profileId:"nemoclaw-hermes-inference-v1",providerName:"nemoclaw-hermes-provider-v1",credentialEnv:"OPENAI_API_KEY",endpoint:"https://inference-api.nousresearch.com/v1",api:"openai-completions"}];function canonicalHermesNativeEndpoint(value){const endpoint=new URL(value);if(endpoint.hostname==="inference.local"||endpoint.hostname==="localhost"||endpoint.hostname.endsWith(".internal")||endpoint.protocol!=="https:"||endpoint.username||endpoint.password||endpoint.search||endpoint.hash||/[\\\s]/u.test(value)||/%2f|%5c/iu.test(endpoint.pathname)){throw new Error("Hermes inference requires an HTTPS endpoint without credentials, query, fragment, or encoded separators")}return`${endpoint.origin}${endpoint.pathname.replace(/\/+$/,"")}`}function hostedNativeProvider(provider,endpointUrl){const definition=HOSTED_NATIVE_PROVIDERS.find(entry=>entry.logicalProvider===provider?.trim());if(!definition||definition.logicalProvider!=="hermes-provider"||!endpointUrl)return definition;const endpoint=canonicalHermesNativeEndpoint(endpointUrl);if(endpoint===definition.endpoint)return definition;const suffix=(0,import_node_crypto.createHash)("sha256").update(endpoint).digest("hex").slice(0,20);return{...definition,endpoint,endpointUrl:endpoint,profileId:`nemoclaw-hermes-inference-${suffix}-v1`,providerName:`nemoclaw-hermes-${suffix}-v1`}}var import_node_child_process=require("node:child_process");var import_node_crypto7=require("node:crypto");var import_node_fs3=__toESM(require("node:fs"));var import_node_path3=__toESM(require("node:path"));var MAX_CORPORATE_CA_BYTES=128*1024;var PEM_CERTIFICATE_RE_GLOBAL=/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g;var deep_agents_code_runtime_identity_default={uid:999,gid:999};var DEEP_AGENTS_CODE_RUNTIME_IDENTITY=Object.freeze(deep_agents_code_runtime_identity_default);var DEEP_AGENTS_CODE_SANDBOX_USER=`${DEEP_AGENTS_CODE_RUNTIME_IDENTITY.uid}:${DEEP_AGENTS_CODE_RUNTIME_IDENTITY.gid}`;var SHIPPED_MANAGED_IMAGE_AGENTS=["openclaw","hermes","langchain-deepagents-code"];var CANDIDATE_MANAGED_IMAGE_AGENTS=["pi"];var MANAGED_IMAGE_AGENTS=[...SHIPPED_MANAGED_IMAGE_AGENTS,...CANDIDATE_MANAGED_IMAGE_AGENTS];var MANAGED_IMAGE_RUNTIME_IDENTITIES=Object.freeze({openclaw:Object.freeze({uid:998,gid:998,workdir:"/sandbox"}),hermes:Object.freeze({uid:998,gid:999,workdir:"/sandbox"}),"langchain-deepagents-code":Object.freeze({...DEEP_AGENTS_CODE_RUNTIME_IDENTITY,workdir:"/sandbox"}),pi:Object.freeze({uid:999,gid:999,workdir:"/sandbox"})});function managedImageRuntimeIdentity(agent){return MANAGED_IMAGE_RUNTIME_IDENTITIES[agent]}var import_node_buffer2=require("node:buffer");var PROVIDERLESS_INFERENCE_ENV=Object.freeze({NEMOCLAW_MODEL:"",NEMOCLAW_INFERENCE_PROVIDER_ID:"",NEMOCLAW_UPSTREAM_PROVIDER:"",NEMOCLAW_INFERENCE_BASE_URL:"",NEMOCLAW_INFERENCE_API:""});function isObjectRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}var ChannelManifestRegistry=class{manifests=new Map;constructor(manifests=[]){for(const manifest of manifests){this.register(manifest)}}register(manifest){if(this.manifests.has(manifest.id)){throw new Error(`Duplicate channel manifest id '${manifest.id}'`)}this.manifests.set(manifest.id,manifest);return this}get(channelId){return this.manifests.get(channelId)}list(){return Array.from(this.manifests.values())}listAvailable(ctx={}){const supportedChannelIds=Array.isArray(ctx.supportedChannelIds)?new Set(ctx.supportedChannelIds):null;return this.list().filter(manifest=>{if(ctx.agent&&!manifest.supportedAgents.includes(ctx.agent)){return false}if(supportedChannelIds&&!supportedChannelIds.has(manifest.id)){return false}return true})}};function createChannelManifestRegistry(manifests=[]){return new ChannelManifestRegistry(manifests)}var discordManifest={schemaVersion:1,id:"discord",displayName:"Discord",description:"Discord bot messaging",supportedAgents:["openclaw","hermes"],state:{hermes:["platforms/discord"]},auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"DISCORD_BOT_TOKEN",formatPattern:"^(?!$)\\S+$",formatHint:"Replace the documentation placeholder with your real Discord bot token.",prompt:{label:"Discord Bot Token",help:"Discord Developer Portal \u2192 Applications \u2192 Bot \u2192 Reset/Copy Token."}},{id:"serverId",kind:"config",required:false,envKey:"DISCORD_SERVER_ID",statePath:"discordGuilds.serverId",prompt:{label:"Discord Server ID (for guild workspace access)",help:"Enable Developer Mode in Discord, then right-click your server and copy the Server ID.",emptyValueMessage:"guild channels stay disabled"}},{id:"requireMention",kind:"config",required:false,envKey:"DISCORD_REQUIRE_MENTION",statePath:"discordGuilds.requireMention",promptWhenInput:"serverId",validValues:["0","1"],defaultValue:"1",prompt:{label:"Discord mention mode",help:"Choose whether the bot should reply only when @mentioned or to all messages in this server."}},{id:"userId",kind:"config",required:false,envKey:"DISCORD_USER_ID",statePath:"discordGuilds.userIds",promptWhenInput:"serverId",prompt:{label:"Discord User ID (optional guild allowlist)",help:"Optional: enable Developer Mode in Discord, then right-click your user/avatar and copy the User ID. Leave blank to allow any member of the configured server to message the bot.",emptyValueMessage:"any member in the configured server can message the bot"}}],credentials:[{id:"discordBotToken",sourceInput:"botToken",providerName:"{sandboxName}-discord-bridge",providerEnvKey:"DISCORD_BOT_TOKEN",placeholder:"openshell:resolve:env:DISCORD_BOT_TOKEN"}],policyPresets:[{name:"discord",requiredAtCreate:true,validationWarningLines:["For Discord preset validation, do not use curl as the success signal:","curl is not in the preset binary allowlist, so curl probes can fail even","when the policy is working. Validate the configured messaging bridge/gateway path.",'DNS-only checks such as dns.resolve("gateway.discord.gg")',"can also be inconclusive behind a proxy.","The agent-specific gateway probe prints an HTTP status when it reaches Discord.","Any HTTP response confirms reachability. A transport error or OpenShell policy","denial means validation failed."],validationWarningLinesByAgent:{openclaw:["OpenClaw validation uses its Node runtime:",`node -e "require('node:https').get('https://discord.com/api/v10/gateway',r=>console.log(r.statusCode)).on('error',e=>{console.error(e.message);process.exitCode=1})"`],hermes:["Hermes validation uses its virtual-environment Python runtime:",`nemohermes exec -- /opt/hermes/.venv/bin/python -c "import urllib.error, urllib.request; u='https://discord.com/api/v10/gateway'; +"use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:true}):target,mod));var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var managed_startup_direct_entry_exports={};__export(managed_startup_direct_entry_exports,{MANAGED_STARTUP_CA_ENV:()=>MANAGED_STARTUP_CA_ENV,MANAGED_STARTUP_COMPLETION_FILE:()=>MANAGED_STARTUP_COMPLETION_FILE,MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION:()=>MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION,MANAGED_STARTUP_MERGED_CA_FILE:()=>MANAGED_STARTUP_MERGED_CA_FILE,MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES:()=>MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES,MANAGED_STARTUP_PROFILE_ENV:()=>MANAGED_STARTUP_PROFILE_ENV,MANAGED_STARTUP_RUNTIME_ENV_FILE:()=>MANAGED_STARTUP_RUNTIME_ENV_FILE,MANAGED_STARTUP_RUNTIME_EXECUTABLE:()=>MANAGED_STARTUP_RUNTIME_EXECUTABLE,ManagedStartupImageActionPlanError:()=>ManagedStartupImageActionPlanError,ManagedStartupImageRuntimeError:()=>ManagedStartupImageRuntimeError,NVIDIA_HOSTED_CREDENTIAL_ENV:()=>NVIDIA_HOSTED_CREDENTIAL_ENV,NVIDIA_HOSTED_NATIVE_ENDPOINT:()=>NVIDIA_HOSTED_NATIVE_ENDPOINT,applyManagedStartupCommandEnvironmentPlan:()=>applyManagedStartupCommandEnvironmentPlan,applyManagedStartupImageProfile:()=>applyManagedStartupImageProfile,applyManagedStartupRootRequest:()=>applyManagedStartupRootRequest,atomicWriteRootFile:()=>atomicWriteRootFile,buildManagedStartupImageActionPlan:()=>buildManagedStartupImageActionPlan,hostedNativeProvider:()=>hostedNativeProvider,installCorporateCaSystemAnchors:()=>installCorporateCaSystemAnchors,installHermesManagedPolicy:()=>installHermesManagedPolicy,main:()=>main,managedStartupSandboxPrefix:()=>managedStartupSandboxPrefix,normalizeHermesManagedConfigDescriptor:()=>normalizeHermesManagedConfigDescriptor,normalizeManagedStartupWorkspaceRoot:()=>normalizeManagedStartupWorkspaceRoot,publishManagedStartupCompletionAfterCommit:()=>publishManagedStartupCompletionAfterCommit,readStableRegularFile:()=>readStableRegularFile,readStableRegularFileSnapshot:()=>readStableRegularFileSnapshot,serializeManagedStartupCompletionMarker:()=>serializeManagedStartupCompletionMarker,serializeManagedStartupRuntimeEnvironment:()=>serializeManagedStartupRuntimeEnvironment,validateManagedStartupApplicationRuntimePlan:()=>validateManagedStartupApplicationRuntimePlan,verifyManagedStartupImageCompletion:()=>verifyManagedStartupImageCompletion,waitForManagedStartupImageCompletion:()=>waitForManagedStartupImageCompletion});module.exports=__toCommonJS(managed_startup_direct_entry_exports);var NVIDIA_HOSTED_NATIVE_ENDPOINT="https://integrate.api.nvidia.com/v1";var NVIDIA_HOSTED_CREDENTIAL_ENV="NVIDIA_INFERENCE_API_KEY";var NATIVE_NVIDIA_AUTH_HEADER_SCRIPT=['case "${NVIDIA_INFERENCE_API_KEY:-}" in *[!a-zA-Z0-9:_]*) exit 2 ;; esac',`printf '%s' "\${NVIDIA_INFERENCE_API_KEY:-}" | LC_ALL=C grep -Eq '^openshell:resolve:env:((v[0-9]{1,20}|s[a-f0-9]{64})_)?NVIDIA_INFERENCE_API_KEY$' || exit 2`,'AUTH_HEADER="Authorization: Bearer ${NVIDIA_INFERENCE_API_KEY}"'].join("; ");var import_node_crypto=require("node:crypto");var HOSTED_NATIVE_PROVIDERS=[{logicalProvider:"openai-api",label:"OpenAI",profileId:"nemoclaw-openai-inference-v1",providerName:"nemoclaw-openai-api-v1",credentialEnv:"OPENAI_API_KEY",endpoint:"https://api.openai.com/v1",api:"openai-completions"},{logicalProvider:"anthropic-prod",label:"Anthropic",profileId:"nemoclaw-anthropic-inference-v1",providerName:"nemoclaw-anthropic-prod-v1",credentialEnv:"ANTHROPIC_API_KEY",endpoint:"https://api.anthropic.com",api:"anthropic-messages"},{logicalProvider:"gemini-api",label:"Gemini",profileId:"nemoclaw-gemini-inference-v1",providerName:"nemoclaw-gemini-api-v1",credentialEnv:"GEMINI_API_KEY",endpoint:"https://generativelanguage.googleapis.com/v1beta/openai/",api:"openai-completions"},{logicalProvider:"openrouter-api",label:"OpenRouter",profileId:"nemoclaw-openrouter-inference-v1",providerName:"nemoclaw-openrouter-api-v1",credentialEnv:"OPENROUTER_API_KEY",endpoint:"https://openrouter.ai/api/v1",api:"openai-completions"},{logicalProvider:"hermes-provider",label:"Hermes Provider",profileId:"nemoclaw-hermes-inference-v1",providerName:"nemoclaw-hermes-provider-v1",credentialEnv:"OPENAI_API_KEY",endpoint:"https://inference-api.nousresearch.com/v1",api:"openai-completions"}];function canonicalHermesNativeEndpoint(value){const endpoint=new URL(value);if(endpoint.hostname==="inference.local"||endpoint.hostname==="localhost"||endpoint.hostname.endsWith(".internal")||endpoint.protocol!=="https:"||endpoint.username||endpoint.password||endpoint.search||endpoint.hash||/[\\\s]/u.test(value)||/%2f|%5c/iu.test(endpoint.pathname)){throw new Error("Hermes inference requires an HTTPS endpoint without credentials, query, fragment, or encoded separators")}return`${endpoint.origin}${endpoint.pathname.replace(/\/+$/,"")}`}function hostedNativeProvider(provider,endpointUrl){const definition=HOSTED_NATIVE_PROVIDERS.find(entry=>entry.logicalProvider===provider?.trim());if(!definition||definition.logicalProvider!=="hermes-provider"||!endpointUrl)return definition;const endpoint=canonicalHermesNativeEndpoint(endpointUrl);if(endpoint===definition.endpoint)return definition;const suffix=(0,import_node_crypto.createHash)("sha256").update(endpoint).digest("hex").slice(0,20);return{...definition,endpoint,endpointUrl:endpoint,profileId:`nemoclaw-hermes-inference-${suffix}-v1`,providerName:`nemoclaw-hermes-${suffix}-v1`}}var import_node_child_process=require("node:child_process");var import_node_crypto7=require("node:crypto");var import_node_fs3=__toESM(require("node:fs"));var import_node_path3=__toESM(require("node:path"));var MAX_CORPORATE_CA_BYTES=128*1024;var PEM_CERTIFICATE_RE_GLOBAL=/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g;var deep_agents_code_runtime_identity_default={uid:999,gid:999};var DEEP_AGENTS_CODE_RUNTIME_IDENTITY=Object.freeze(deep_agents_code_runtime_identity_default);var DEEP_AGENTS_CODE_SANDBOX_USER=`${DEEP_AGENTS_CODE_RUNTIME_IDENTITY.uid}:${DEEP_AGENTS_CODE_RUNTIME_IDENTITY.gid}`;var SHIPPED_MANAGED_IMAGE_AGENTS=["openclaw","hermes","langchain-deepagents-code"];var CANDIDATE_MANAGED_IMAGE_AGENTS=["pi"];var MANAGED_IMAGE_AGENTS=[...SHIPPED_MANAGED_IMAGE_AGENTS,...CANDIDATE_MANAGED_IMAGE_AGENTS];var MANAGED_IMAGE_RUNTIME_IDENTITIES=Object.freeze({openclaw:Object.freeze({uid:998,gid:998,workdir:"/sandbox"}),hermes:Object.freeze({uid:998,gid:999,workdir:"/sandbox"}),"langchain-deepagents-code":Object.freeze({...DEEP_AGENTS_CODE_RUNTIME_IDENTITY,workdir:"/sandbox"}),pi:Object.freeze({uid:999,gid:999,workdir:"/sandbox"})});function managedImageRuntimeIdentity(agent){return MANAGED_IMAGE_RUNTIME_IDENTITIES[agent]}var import_node_buffer2=require("node:buffer");var PROVIDERLESS_INFERENCE_ENV=Object.freeze({NEMOCLAW_MODEL:"",NEMOCLAW_INFERENCE_PROVIDER_ID:"",NEMOCLAW_UPSTREAM_PROVIDER:"",NEMOCLAW_INFERENCE_BASE_URL:"",NEMOCLAW_INFERENCE_API:""});function isObjectRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}var ChannelManifestRegistry=class{manifests=new Map;constructor(manifests=[]){for(const manifest of manifests){this.register(manifest)}}register(manifest){if(this.manifests.has(manifest.id)){throw new Error(`Duplicate channel manifest id '${manifest.id}'`)}this.manifests.set(manifest.id,manifest);return this}get(channelId){return this.manifests.get(channelId)}list(){return Array.from(this.manifests.values())}listAvailable(ctx={}){const supportedChannelIds=Array.isArray(ctx.supportedChannelIds)?new Set(ctx.supportedChannelIds):null;return this.list().filter(manifest=>{if(ctx.agent&&!manifest.supportedAgents.includes(ctx.agent)){return false}if(supportedChannelIds&&!supportedChannelIds.has(manifest.id)){return false}return true})}};function createChannelManifestRegistry(manifests=[]){return new ChannelManifestRegistry(manifests)}var discordManifest={schemaVersion:1,id:"discord",displayName:"Discord",description:"Discord bot messaging",supportedAgents:["openclaw","hermes"],state:{hermes:["platforms/discord"]},auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"DISCORD_BOT_TOKEN",formatPattern:"^(?!$)\\S+$",formatHint:"Replace the documentation placeholder with your real Discord bot token.",prompt:{label:"Discord Bot Token",help:"Discord Developer Portal \u2192 Applications \u2192 Bot \u2192 Reset/Copy Token."}},{id:"serverId",kind:"config",required:false,envKey:"DISCORD_SERVER_ID",statePath:"discordGuilds.serverId",prompt:{label:"Discord Server ID (for guild workspace access)",help:"Enable Developer Mode in Discord, then right-click your server and copy the Server ID.",emptyValueMessage:"guild channels stay disabled"}},{id:"requireMention",kind:"config",required:false,envKey:"DISCORD_REQUIRE_MENTION",statePath:"discordGuilds.requireMention",promptWhenInput:"serverId",validValues:["0","1"],defaultValue:"1",prompt:{label:"Discord mention mode",help:"Choose whether the bot should reply only when @mentioned or to all messages in this server."}},{id:"userId",kind:"config",required:false,envKey:"DISCORD_USER_ID",statePath:"discordGuilds.userIds",promptWhenInput:"serverId",prompt:{label:"Discord User ID (optional guild allowlist)",help:"Optional: enable Developer Mode in Discord, then right-click your user/avatar and copy the User ID. Leave blank to allow any member of the configured server to message the bot.",emptyValueMessage:"any member in the configured server can message the bot"}}],credentials:[{id:"discordBotToken",sourceInput:"botToken",providerName:"{sandboxName}-discord-bridge",providerEnvKey:"DISCORD_BOT_TOKEN",placeholder:"openshell:resolve:env:DISCORD_BOT_TOKEN"}],policyPresets:[{name:"discord",requiredAtCreate:true,validationWarningLines:["For Discord preset validation, do not use curl as the success signal:","curl is not in the preset binary allowlist, so curl probes can fail even","when the policy is working. Validate the configured messaging bridge/gateway path.",'DNS-only checks such as dns.resolve("gateway.discord.gg")',"can also be inconclusive behind a proxy.","The agent-specific gateway probe prints an HTTP status when it reaches Discord.","Any HTTP response confirms reachability. A transport error or OpenShell policy","denial means validation failed."],validationWarningLinesByAgent:{openclaw:["OpenClaw validation uses its Node runtime:",`node -e "require('node:https').get('https://discord.com/api/v10/gateway',r=>console.log(r.statusCode)).on('error',e=>{console.error(e.message);process.exitCode=1})"`],hermes:["Hermes validation uses its virtual-environment Python runtime:",`nemohermes exec -- /opt/hermes/.venv/bin/python -c "import urllib.error, urllib.request; u='https://discord.com/api/v10/gateway'; try: print(urllib.request.urlopen(u, timeout=20).status) except urllib.error.HTTPError as error: print(error.code)"`]}}],render:[{id:"discord-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.discord",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},proxy:"{{discordProxyUrl}}",dmPolicy:"{{discord.allowedUsers.dmPolicy}}",allowFrom:"{{discord.allowedUsers.values}}"}}}}},{id:"discord-openclaw-guilds",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",when:"{{discord.hasGuilds}}",fragment:{path:"channels.discord",value:{groupPolicy:"allowlist",guilds:"{{discord.guilds}}"}}},{id:"discord-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.discord",value:{enabled:true}}},{id:"discord-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["NEMOCLAW_DISCORD_GUILD_IDS={{discord.guildIds.csv}}","DISCORD_ALLOWED_USERS={{discord.allowedUsers.csv}}","DISCORD_ALLOW_ALL_USERS={{discord.allowAllUsers}}"]},{id:"discord-hermes-config",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"discord",value:{require_mention:"{{discord.requireMention}}",free_response_channels:"",allowed_channels:"",auto_thread:true,reactions:true,channel_prompts:{}}}},{id:"discord-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.discord",value:{enabled:true}}}],runtime:{openclaw:{channelName:"discord",visibility:{configKeys:["discord"],logPatterns:["discord"]}}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/discord@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-j+fSHxbXA+DSxwbL8SvtsDNL6tvBX4+RmH+EerVW6dCbeIwSconXj6J/+AaN/mhzZI4g0jPPj30TUhdCRRbc2w==","2026.9.1":"sha512-qNmN2a8A9dET4igPp0RML171sEn8PDMyNCYNp/DqcJ4tn3XTHpacSOTkqBmv5yXTycJRC9rfFP8FT/SdW0Rldg=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.1.tgz"},required:true}],hooks:[{id:"discord-openclaw-bridge-health",phase:"health-check",handler:"discord.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"discord-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"discord-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"serverId",kind:"config"},{id:"requireMention",kind:"config"},{id:"userId",kind:"config"}]}]};var googlechatManifest={schemaVersion:1,id:"googlechat",displayName:"Google Chat",description:"Google Chat (Chat API) bot messaging (experimental)",supportedAgents:["openclaw","hermes"],state:{hermes:["platforms/googlechat"]},auth:{mode:"token-paste"},inputs:[{id:"serviceAccount",kind:"secret",required:true,envKey:"GOOGLECHAT_SERVICE_ACCOUNT",allowLineBreaks:true,maskCap:40,formatHint:"Paste the entire service-account JSON key on one line (minified), or set GOOGLECHAT_SERVICE_ACCOUNT to the downloaded JSON, including line breaks.",maxTokenAttempts:3,prompt:{label:"Google Chat service account JSON",help:["\u2503 GOOGLE CHAT \u2014 service account key","\u2503","\u2503 Google Cloud Console \u2192 IAM & Admin \u2192 Service Accounts","\u2503 \u2192 your bot's SA \u2192 Keys \u2192 Add key \u2192 Create new key \u2192 JSON","\u2503","\u2503 A .json file downloads. Paste its contents below as ONE line (minified).","\u2503 Alternatively, GOOGLECHAT_SERVICE_ACCOUNT accepts the formatted JSON file contents.",""].join("\n")}},{id:"audienceType",kind:"config",required:false,envKey:"GOOGLECHAT_AUDIENCE_TYPE",statePath:"googlechatConfig.audienceType",validValues:["app-url","project-number"],defaultValue:"app-url"},{id:"audience",kind:"config",required:false,envKey:"GOOGLECHAT_AUDIENCE",statePath:"googlechatConfig.audience",prompt:{label:"Google Chat webhook audience",help:"Usually filled automatically from the public tunnel URL. For audienceType 'project-number', enter your GCP project number instead.",emptyValueMessage:"inbound webhook verification will be unconfigured"}},{id:"appPrincipal",kind:"config",required:false,envKey:"GOOGLECHAT_APP_PRINCIPAL",statePath:"googlechatConfig.appPrincipal",formatPattern:"^[0-9]{6,32}$",formatHint:"appPrincipal is the add-on's numeric OAuth client ID (uniqueId, ~21 digits), not an email.",prompt:{label:"Google Chat appPrincipal",help:[" Workspace account \u2192 leave blank, done."," Personal Gmail \u2192 needs the add-on's ~21-digit ID (not an email), stable across rebuilds.",""," If you already know it, paste it at the prompt and you're done."," If not, leave it blank \u2014 the first DM reveals it once the sandbox is live:",""," 1. Watch the gateway log:",' nemoclaw logs --follow | grep "unexpected add-on principal"'," 2. DM the bot once \u2014 it won't reply yet, that's expected. The log prints:"," unexpected add-on principal: "," 3. Save that and rebuild:"," GOOGLECHAT_APP_PRINCIPAL= nemoclaw channels add googlechat"," nemoclaw rebuild --yes"].join("\n"),emptyValueMessage:"Workspace accounts do not need it; personal accounts must set it later"}},{id:"allowFrom",kind:"config",required:false,envKey:"GOOGLECHAT_ALLOWED_USERS",statePath:"allowedIds.googlechat",prompt:{label:"Google Chat DM allowlist (comma-separated)",help:["Optional: restrict who can DM the bot."," OpenClaw: users/NNN (emails ignored)"," Hermes: email (users/NNN ignored)"," Blank: pairing mode (recommended) \u2014 OpenClaw's pairing reply shows your users/NNN"," Filling this switches DM policy to allowlist \u2014 a wrong-form entry is dropped silently, with no pairing code."].join("\n"),emptyValueMessage:"bot will require manual pairing"}},{id:"projectId",kind:"config",required:false,envKey:"GOOGLE_CHAT_PROJECT_ID",statePath:"googlechatConfig.projectId",prompt:{label:"Google Chat GCP project ID (Hermes Pub/Sub pull)",help:"The Google Cloud project that owns the Pub/Sub subscription Hermes pulls Chat events from. OpenClaw ignores this.",emptyValueMessage:"required for the Hermes Google Chat channel"}},{id:"subscriptionName",kind:"config",required:false,envKey:"GOOGLE_CHAT_SUBSCRIPTION_NAME",statePath:"googlechatConfig.subscriptionName",prompt:{label:"Google Chat Pub/Sub subscription (projects/

/subscriptions/)",help:["The pull subscription bound to the Chat events topic. Hermes pulls from it over the Pub/Sub REST API; the gateway-minted token is scoped to both chat.bot and pubsub."," Its topic must grant roles/pubsub.publisher to the app's push account:"," Interactive features service-@gcp-sa-gsuiteaddons.iam.gserviceaccount.com"," Classic bot chat-api-push@system.gserviceaccount.com"," Shown at Chat API \u2192 Configuration \u2192 Connection settings"," Missing it channel connects, no event arrives, Chat says the bot is not responding"].join("\n"),emptyValueMessage:"required for the Hermes Google Chat channel"}}],credentials:[],policyPresets:[{name:"googlechat",policyKeys:["googlechat"],agentPolicyKeys:{hermes:["googlechat_hermes"]}}],render:[{id:"googlechat-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.googlechat",value:{enabled:true,serviceAccount:{},audienceType:"{{googlechatConfig.audienceType}}",audience:"{{googlechatConfig.audience}}",appPrincipal:"{{googlechatConfig.appPrincipal}}",webhookPath:"/googlechat",healthMonitor:{enabled:false},dmPolicy:"{{allowedIds.googlechat.dmPolicy}}",allowFrom:"{{allowedIds.googlechat.values}}"}}},{id:"googlechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.googlechat",value:{enabled:true}}},{id:"googlechat-openclaw-gateway-reload-off",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"gateway.reload",value:{mode:"off"}}},{id:"googlechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["GOOGLE_CHAT_PROJECT_ID={{googlechatConfig.projectId}}","GOOGLE_CHAT_SUBSCRIPTION_NAME={{googlechatConfig.subscriptionName}}","GOOGLE_CHAT_ALLOWED_USERS={{allowedIds.googlechat.csv}}"]},{id:"googlechat-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.google_chat",value:{enabled:true}}}],runtime:{openclaw:{channelName:"googlechat",visibility:{configKeys:["googlechat"],logPatterns:["googlechat"]},nodePreloads:[{module:"googlechat-trusted-proxy-fetch",injectInto:["boot"],optional:false,installMessage:"[channels] Installing Google Chat trusted-proxy-fetch patch (route googleapis via trusted env proxy)",installedMessage:"[channels] Google Chat trusted-proxy-fetch patch installed (NODE_OPTIONS updated)"},{module:"googlechat-outbound-auth",injectInto:["boot"],optional:false,installMessage:"[channels] Installing Google Chat outbound-auth patch (gateway-minted bearer)",installedMessage:"[channels] Google Chat outbound-auth patch installed (NODE_OPTIONS updated)"}],secretScans:[{path:"/sandbox/.openclaw/openclaw.json",pattern:"-----BEGIN (?:RSA )?PRIVATE KEY-----",message:"[SECURITY] Google Chat service account private key leaked into {path} - refusing to serve",exitCode:78}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/googlechat@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-LUO8Lg07IhzJfEzxn+GSij8WMS/uX3hTmv0SydTy/0fKSN7iZksf74TaZg50kOoBi8FzeqeXo/zoGVuk0Mj1Ig==","2026.9.1":"sha512-Q5VTAJpfcrI7BSEw5Ugq3wf7JEg5QhTBwpi+BByGbfZsTTVjwZc7OIvNbKsVTh16I5/EWqHEnD+0WNeHqsteqw=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.1.tgz"},required:true},{id:"hermesGooglePubsubPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-cloud-pubsub==2.39.0",required:true},{id:"hermesGoogleApiClientPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-api-python-client==2.194.0",required:true},{id:"hermesGoogleAuthPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-auth==2.55.1",required:true}],hooks:[{id:"googlechat-tunnel-audience-gate",phase:"enroll",handler:"googlechat.tunnelAudienceGate",agents:["openclaw"],inputs:["audienceType","audience"],outputs:[{id:"audience",kind:"config"}],onFailure:"skip-channel"},{id:"googlechat-service-account",phase:"enroll",handler:"googlechat.tokenPaste",outputs:[{id:"serviceAccount",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"googlechat-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowFrom",kind:"config"}]},{id:"googlechat-openclaw-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["openclaw"],outputs:[{id:"appPrincipal",kind:"config"}]},{id:"googlechat-hermes-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["hermes"],outputs:[{id:"projectId",kind:"config"},{id:"subscriptionName",kind:"config"}]}]};var slackManifest={schemaVersion:1,id:"slack",displayName:"Slack",description:"Slack bot messaging",supportedAgents:["openclaw","hermes"],state:{hermes:["platforms/slack"]},auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"SLACK_BOT_TOKEN",formatPattern:"^xoxb-[A-Za-z0-9_-]+$",formatHint:"Slack bot tokens start with 'xoxb-' (e.g. xoxb---).",prompt:{label:"Slack Bot Token",help:"Slack API \u2192 Your Apps \u2192 OAuth & Permissions \u2192 Bot User OAuth Token (xoxb-...)."}},{id:"appToken",kind:"secret",required:true,envKey:"SLACK_APP_TOKEN",formatPattern:"^xapp-[A-Za-z0-9_-]+$",formatHint:"Slack app tokens start with 'xapp-' (e.g. xapp----).",prompt:{label:"Slack App Token (Socket Mode)",help:"Slack API \u2192 Your Apps \u2192 Basic Information \u2192 App-Level Tokens (xapp-...)."}},{id:"allowedUsers",kind:"config",required:false,envKey:"SLACK_ALLOWED_USERS",statePath:"allowedIds.slack",prompt:{label:"Slack Member IDs (comma-separated allowlist)",help:"In Slack, open each allowed human user's profile -> More -> Copy member ID. Enter one or more comma-separated member IDs, not the app or bot user ID. Member IDs look like U01ABC2DEF3.",emptyValueMessage:"bot will require manual pairing"}},{id:"allowedChannels",kind:"config",required:false,envKey:"SLACK_ALLOWED_CHANNELS",statePath:"slackConfig.allowedChannels",prompt:{label:"Slack Channel IDs (comma-separated allowlist)",help:"Optional: enter comma-separated Slack channel IDs where the bot may answer @mentions. Channel IDs look like C012AB3CD.",emptyValueMessage:"channel @mentions stay unrestricted by channel ID"}}],credentials:[{id:"slackBotToken",sourceInput:"botToken",providerName:"{sandboxName}-slack-bridge",providerEnvKey:"SLACK_BOT_TOKEN",placeholder:"xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN",primary:true},{id:"slackAppToken",sourceInput:"appToken",providerName:"{sandboxName}-slack-app",providerEnvKey:"SLACK_APP_TOKEN",placeholder:"xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN"}],policyPresets:[{name:"slack",requiredAtCreate:true}],render:[{id:"slack-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.slack",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},dmPolicy:"{{allowedIds.slack.dmPolicy}}",allowFrom:"{{allowedIds.slack.values}}",groupPolicy:"{{allowedIds.slack.groupPolicy}}",channels:"{{allowedIds.slack.channels}}"}}}}},{id:"slack-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.slack",value:{enabled:true}}},{id:"slack-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["SLACK_ALLOWED_USERS={{allowedIds.slack.csv}}","SLACK_ALLOWED_CHANNELS={{slackConfig.allowedChannels.csv}}"]},{id:"slack-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.slack",value:{enabled:true,extra:{rich_blocks:true}}}}],runtime:{openclaw:{channelName:"slack",visibility:{configKeys:["slack"],logPatterns:["slack"]},nodePreloads:[{module:"slack-channel-guard",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Slack channel guard (unhandled-rejection safety net)",installedMessage:"[channels] Slack channel guard installed (NODE_OPTIONS updated)"}],secretScans:[{path:"/sandbox/.openclaw/openclaw.json",pattern:"(?:xoxb|xapp)-(?!OPENSHELL-RESOLVE-ENV-)",message:"[SECURITY] Slack token leaked into {path} - refusing to serve",exitCode:78}]},hermes:{}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/slack@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-6M1M6gL3iXahpalNsYAUuA+wvnV8lbMlNNH2ToegFvaSJcIll4S9kFa5mv3GFoquhMRHQjEjnkXPHP/pXwaWcA==","2026.9.1":"sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.1.tgz"},required:true}],hooks:[{id:"slack-socket-mode-gateway-conflict",phase:"pre-enable",handler:"slack.socketModeGatewayConflict",onFailure:"abort"},{id:"slack-openclaw-bridge-health",phase:"health-check",handler:"slack.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"slack-socket-mode-gateway-status",phase:"status",handler:"slack.socketModeGatewayStatus",outputs:[{id:"gatewayOverlaps",kind:"status"}]},{id:"slack-status-health",phase:"status",handler:"slack.statusHealth",providesReadiness:true,agents:["openclaw"],outputs:[{id:"channelHealth",kind:"status"}]},{id:"slack-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true},{id:"appToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"slack-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowedUsers",kind:"config"},{id:"allowedChannels",kind:"config"}]},{id:"slack-credential-validation",phase:"reachability-check",handler:"slack.validateCredentials",inputs:["botToken","appToken"],onFailure:"skip-channel"}]};var TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT={channelId:"teams",renderId:"teams-openclaw-channel",hookId:"teams-openclaw-channel",handlerId:"common.staticOutputs",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",configPath:"channels.msteams",webhookPath:"/api/messages"};function authorizeTeamsOpenClawWebhookField(entry){if(!isPlainDataObject(entry))return[];const contract=TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT;if(ownDataPropertyValue(entry,"channelId")!==contract.channelId||ownDataPropertyValue(entry,"renderId")!==contract.renderId||ownDataPropertyValue(entry,"hookId")!==contract.hookId||ownDataPropertyValue(entry,"handler")!==contract.handlerId||ownDataPropertyValue(entry,"kind")!==contract.kind||ownDataPropertyValue(entry,"agent")!==contract.agent||ownDataPropertyValue(entry,"target")!==contract.target||ownDataPropertyValue(entry,"path")!==contract.configPath){return[]}const value=ownDataPropertyValue(entry,"value");if(!isPlainDataObject(value))return[];const webhook=ownDataPropertyValue(value,"webhook");if(!isPlainDataObject(webhook)||!hasExactlyOwnDataProperties(webhook,["path","port"])||!isTcpPort(ownDataPropertyValue(webhook,"port"))||ownDataPropertyValue(webhook,"path")!==contract.webhookPath){return[]}return[{path:["value","webhook"],value:webhook}]}function isPlainDataObject(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function hasExactlyOwnDataProperties(value,expected){const actual=Object.getOwnPropertyNames(value).sort();return actual.length===expected.length&&actual.every((key,index)=>key===expected[index])}function isTcpPort(value){return Number.isInteger(value)&&value>=1&&value<=65535}var teamsManifest={schemaVersion:1,id:"teams",displayName:"Microsoft Teams",description:"Microsoft Teams bot messaging (experimental)",enrollmentNotes:["Microsoft Teams requires a public HTTPS webhook endpoint at /api/messages; expose the configured Teams webhook port before installing the Teams app.","Use Azure AD object IDs in TEAMS_ALLOWED_USERS so only authorized users can interact with the bot."],supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"appId",kind:"config",required:true,envKey:"MSTEAMS_APP_ID",statePath:"teamsConfig.appId",prompt:{label:"Microsoft Teams Client ID",help:"Run `teams app create --endpoint https:///api/messages`, then copy CLIENT_ID."}},{id:"clientSecret",kind:"secret",required:true,envKey:"MSTEAMS_APP_PASSWORD",prompt:{label:"Microsoft Teams Client Secret",help:"Use the CLIENT_SECRET printed by `teams app create`. It is shown once; rotate it in Entra ID if it was lost."}},{id:"tenantId",kind:"config",required:true,envKey:"MSTEAMS_TENANT_ID",statePath:"teamsConfig.tenantId",prompt:{label:"Microsoft Teams Tenant ID",help:"Use the TENANT_ID printed by `teams app create` or shown by `teams status --verbose`."}},{id:"allowedUsers",kind:"config",required:false,envKey:"TEAMS_ALLOWED_USERS",statePath:"allowedIds.teams",prompt:{label:"Microsoft Teams AAD Object IDs (comma-separated allowlist)",help:"Recommended: run `teams status --verbose` and enter the Azure AD object IDs allowed to use the bot."}},{id:"webhookPort",kind:"config",required:false,envKey:"MSTEAMS_PORT",statePath:"teamsConfig.webhookPort",defaultValue:"3978",prompt:{label:"Microsoft Teams webhook port",help:"Local bot webhook port to expose publicly. Defaults to 3978 and serves /api/messages."}},{id:"requireMention",kind:"config",required:false,envKey:"TEAMS_REQUIRE_MENTION",statePath:"teamsConfig.requireMention",validValues:["0","1"],defaultValue:"1",prompt:{label:"Microsoft Teams mention mode",help:"Controls OpenClaw group and channel behavior only. Direct messages are unaffected."}}],credentials:[{id:"teamsClientSecret",sourceInput:"clientSecret",providerName:"{sandboxName}-teams-bridge",providerEnvKey:"MSTEAMS_APP_PASSWORD",placeholder:"openshell:resolve:env:MSTEAMS_APP_PASSWORD",primary:true}],policyPresets:[{name:"teams",policyKeys:["teams"],requiredAtCreate:true}],hostForward:{port:"{{teamsConfig.webhookPort}}",label:"Microsoft Teams webhook"},render:[{id:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.renderId,kind:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.kind,agent:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.agent,target:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.target,fragment:{path:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.configPath,value:{enabled:true,appId:"{{teamsConfig.appId}}",tenantId:"{{teamsConfig.tenantId}}",webhook:{port:"{{teamsConfig.webhookPort}}",path:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.webhookPath},healthMonitor:{enabled:false},streaming:{mode:"off"},dmPolicy:"{{allowedIds.teams.dmPolicy}}",allowFrom:"{{allowedIds.teams.values}}",groupPolicy:"open",requireMention:"{{teamsConfig.requireMention}}"}}},{id:"teams-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.msteams",value:{enabled:true}}},{id:"teams-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["TEAMS_CLIENT_ID={{teamsConfig.appId}}","TEAMS_TENANT_ID={{teamsConfig.tenantId}}","TEAMS_ALLOWED_USERS={{allowedIds.teams.csv}}","TEAMS_PORT={{teamsConfig.webhookPort}}"]},{id:"teams-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.teams",value:{enabled:true}}}],runtime:{openclaw:{channelName:"msteams",visibility:{configKeys:["msteams"],logPatterns:["msteams","teams"]},nodePreloads:[{module:"msteams-message-hints",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Microsoft Teams message hint patch (native mentions)",installedMessage:"[channels] Microsoft Teams message hint patch installed (NODE_OPTIONS updated)"}]},hermes:{envAliases:[{envKey:"MSTEAMS_APP_PASSWORD",targetEnvKey:"TEAMS_CLIENT_SECRET",match:"^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_MSTEAMS_APP_PASSWORD$",value:"openshell:resolve:env:MSTEAMS_APP_PASSWORD"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/msteams@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og==","2026.9.1":"sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.1.tgz"},required:true},{id:"hermesTeamsAppsPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"microsoft-teams-apps==2.0.13.4",required:true}],hooks:[{id:"teams-host-forward-port-conflict",phase:"pre-enable",handler:"teams.hostForwardPortConflict",inputs:["webhookPort"],onFailure:"abort"},{id:"teams-host-forward-port-status",phase:"status",handler:"teams.hostForwardPortStatus",outputs:[{id:"hostForwardPortOverlaps",kind:"status"}]},{id:"teams-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"clientSecret",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"teams-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"appId",kind:"config",required:true},{id:"tenantId",kind:"config",required:true},{id:"allowedUsers",kind:"config"},{id:"webhookPort",kind:"config"},{id:"requireMention",kind:"config"}]}],state:{}};var telegramManifest={schemaVersion:1,id:"telegram",displayName:"Telegram",description:"Telegram bot messaging",diagnosticsProbe:"log-tail",enrollmentNotes:["For Telegram group chats, disable privacy mode in @BotFather (/setprivacy -> your bot -> Disable).","After changing privacy mode, remove and re-add the bot to each group before testing @mentions."],supportedAgents:["openclaw","hermes"],state:{openclaw:["telegram"],hermes:["platforms/telegram"]},auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"TELEGRAM_BOT_TOKEN",prompt:{label:"Telegram Bot Token",help:"Create a bot via @BotFather on Telegram, then copy the token."}},{id:"allowedIds",kind:"config",required:false,envKey:"TELEGRAM_ALLOWED_IDS",statePath:"allowedIds.telegram",prompt:{label:"Telegram User ID (for DM access)",help:"Send /start to @userinfobot on Telegram to get your numeric user ID.",emptyValueMessage:"bot will require manual pairing"}},{id:"requireMention",kind:"config",required:false,envKey:"TELEGRAM_REQUIRE_MENTION",statePath:"telegramConfig.requireMention",validValues:["0","1"],defaultValue:"1",prompt:{label:"Telegram group mention mode",help:"Controls Telegram group-chat behavior only \u2014 reply only when @mentioned vs. to all group messages. Direct messages are unaffected by this setting and remain subject to pairing and TELEGRAM_ALLOWED_IDS."}},{id:"groupPolicy",kind:"config",required:false,envKey:"TELEGRAM_GROUP_POLICY",statePath:"telegramConfig.groupPolicy",validValues:["open","allowlist","disabled"],defaultValue:"open",prompt:{label:"Telegram group policy",help:"Controls OpenClaw Telegram group access. Hermes does not expose an equivalent disable-groups policy."}}],credentials:[{id:"telegramBotToken",sourceInput:"botToken",providerName:"{sandboxName}-telegram-bridge",providerEnvKey:"TELEGRAM_BOT_TOKEN",placeholder:"openshell:resolve:env:TELEGRAM_BOT_TOKEN"}],policyPresets:[{name:"telegram",requiredAtCreate:true,policyKeys:["telegram_bot"],agentPolicyKeys:{hermes:["telegram"]}}],render:[{id:"telegram-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.telegram",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},proxy:"{{proxyUrl}}",groupPolicy:"{{telegramConfig.groupPolicy}}",dmPolicy:"{{allowedIds.telegram.dmPolicy}}",allowFrom:"{{allowedIds.telegram.values}}"}}}}},{id:"telegram-openclaw-groups",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",when:"{{telegramConfig.openclawGroups}}",fragment:{path:"channels.telegram.groups",value:"{{telegramConfig.openclawGroups}}"}},{id:"telegram-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.telegram",value:{enabled:true}}},{id:"telegram-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["TELEGRAM_ALLOWED_USERS={{allowedIds.telegram.csv}}"]},{id:"telegram-hermes-config",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"telegram",value:{require_mention:"{{telegramConfig.requireMention}}"}}},{id:"telegram-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.telegram",value:{enabled:true}}}],runtime:{openclaw:{channelName:"telegram",visibility:{configKeys:["telegram"],logPatterns:["telegram"]},nodePreloads:[{module:"telegram-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Telegram diagnostics (provider readiness + inference errors)",installedMessage:"[channels] Telegram diagnostics installed (NODE_OPTIONS updated)"}]}},hooks:[{id:"telegram-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"telegram-allowlist-aliases",phase:"enroll",handler:"telegram.allowlistAliases",outputs:[{id:"allowedIds",kind:"config"}]},{id:"telegram-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"requireMention",kind:"config"},{id:"allowedIds",kind:"config"}]},{id:"telegram-openclaw-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["openclaw"],outputs:[{id:"groupPolicy",kind:"config"}]},{id:"telegram-get-me-reachability",phase:"reachability-check",handler:"telegram.getMeReachability",inputs:["botToken"],onFailure:"skip-channel"},{id:"telegram-openclaw-bridge-health",phase:"health-check",handler:"telegram.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"telegram-gateway-conflict-status",phase:"status",handler:"telegram.gatewayConflictStatus",outputs:[{id:"bridgeHealth",kind:"status"}]},{id:"telegram-status-health",phase:"status",handler:"telegram.statusHealth",agents:["openclaw"],outputs:[{id:"channelHealth",kind:"status"}]}]};var WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT={channelId:"wechat",planHookId:"wechat-seed-openclaw-account",handlerId:"wechat.seedOpenClawAccount",outputId:"openclawWeixinAccountFile",kind:"build-file",required:true,mode:"0600"};var WECHAT_SEED_OPENCLAW_ACCOUNT_HOOK_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.handlerId;var WECHAT_SEED_OPENCLAW_ACCOUNT_PLAN_HOOK_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.planHookId;var WECHAT_OPENCLAW_ACCOUNT_FILE_OUTPUT_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.outputId;var WECHAT_TOKEN_PLACEHOLDER="openshell:resolve:env:WECHAT_BOT_TOKEN";function authorizeWechatAccountFilePlaceholders(value){const content=isPlainDataObject2(value)?ownDataPropertyValue2(value,"content"):void 0;if(!isPlainDataObject2(value)||!hasExactlyOwnDataProperties2(value,["content","mode","path"])||!isWechatAccountFilePath(ownDataPropertyValue2(value,"path"))||ownDataPropertyValue2(value,"mode")!==WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.mode||!isPlainDataObject2(content)||!hasOnlyOwnDataProperties(content,["baseUrl","savedAt","token","userId"])||!hasOwnDataProperty(content,"savedAt")||!hasOwnDataProperty(content,"token")||ownDataPropertyValue2(content,"token")!==WECHAT_TOKEN_PLACEHOLDER||!isNonEmptyString(ownDataPropertyValue2(content,"savedAt"))||!isOptionalNonEmptyString(content,"baseUrl")||!isOptionalNonEmptyString(content,"userId")){return[]}return[{path:["content","token"],value:WECHAT_TOKEN_PLACEHOLDER}]}function isWechatAccountFilePath(value){if(typeof value!=="string")return false;const prefix="openclaw-weixin/accounts/";const suffix=".json";if(!value.startsWith(prefix)||!value.endsWith(suffix))return false;const accountId=value.slice(prefix.length,-suffix.length);return accountId===accountId.trim()&&isSafeWechatAccountId(accountId)}function isSafeWechatAccountId(accountId){return accountId.length>0&&accountId!=="."&&accountId!==".."&&!/[\\/\0-\x1F\x7F]/.test(accountId)&&!accountId.includes("..")}function isPlainDataObject2(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue2(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function hasOwnDataProperty(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor!==void 0&&"value"in descriptor}function hasExactlyOwnDataProperties2(value,expected){const actual=Object.getOwnPropertyNames(value).sort();return actual.length===expected.length&&actual.every((key,index)=>key===expected[index])}function hasOnlyOwnDataProperties(value,allowed){return Object.getOwnPropertyNames(value).every(key=>allowed.includes(key))}function isNonEmptyString(value){return typeof value==="string"&&value.length>0}function isOptionalNonEmptyString(value,key){return!hasOwnDataProperty(value,key)||isNonEmptyString(ownDataPropertyValue2(value,key))}var wechatManifest={schemaVersion:1,id:"wechat",displayName:"WeChat",description:"WeChat (personal) bot messaging",enrollmentHelp:"Captured automatically via a host-side QR scan during onboard \u2014 pair the bot by scanning the QR with WeChat on your phone (Discover \u2192 Scan). DM-only.",supportedAgents:["openclaw","hermes"],auth:{mode:"host-qr"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"WECHAT_BOT_TOKEN",prompt:{label:"WeChat Bot Token",help:"Captured automatically via a host-side QR scan during onboard \u2014 pair the bot by scanning the QR with WeChat on your phone (Discover \u2192 Scan). DM-only."}},{id:"accountId",kind:"config",required:true,envKey:"WECHAT_ACCOUNT_ID",statePath:"wechatConfig.accountId"},{id:"baseUrl",kind:"config",required:false,envKey:"WECHAT_BASE_URL",statePath:"wechatConfig.baseUrl"},{id:"userId",kind:"config",required:false,envKey:"WECHAT_USER_ID",statePath:"wechatConfig.userId"},{id:"allowedIds",kind:"config",required:false,envKey:"WECHAT_ALLOWED_IDS",statePath:"allowedIds.wechat",prompt:{label:"WeChat User ID(s) (DM allowlist)",help:"Optional: restrict who can DM the bot. The WeChat user id of the operator who scanned is added automatically; supply additional ids as a comma-separated list.",emptyValueMessage:"bot will require manual pairing"}}],credentials:[{id:"wechatBotToken",sourceInput:"botToken",providerName:"{sandboxName}-wechat-bridge",providerEnvKey:"WECHAT_BOT_TOKEN",placeholder:"openshell:resolve:env:WECHAT_BOT_TOKEN"}],state:{openclaw:["wechat","openclaw-weixin"],hermes:["platforms/wechat"]},policyPresets:[{name:"wechat",policyKeys:["wechat_bridge"],requiredAtCreate:true}],render:[{id:"wechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.openclaw-weixin",value:{enabled:true}}},{id:"wechat-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.openclaw-weixin",value:{enabled:true}}},{id:"wechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WEIXIN_ACCOUNT_ID={{wechatConfig.accountId}}","WEIXIN_BASE_URL={{wechatConfig.baseUrl}}","WEIXIN_ALLOWED_USERS={{allowedIds.wechat.csv}}"]},{id:"wechat-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.weixin",value:{enabled:true}}}],runtime:{openclaw:{channelName:"openclaw-weixin",visibility:{configKeys:["openclaw-weixin"],logPatterns:["wechat","openclaw-weixin"]},nodePreloads:[{module:"wechat-account-placeholder",injectInto:["boot"],optional:false},{module:"wechat-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing WeChat diagnostics (provider readiness + inference errors)",installedMessage:"[channels] WeChat diagnostics installed (NODE_OPTIONS updated)"}]},hermes:{envAliases:[{envKey:"WECHAT_BOT_TOKEN",targetEnvKey:"WEIXIN_TOKEN",match:"^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_WECHAT_BOT_TOKEN$",value:"openshell:resolve:env:WECHAT_BOT_TOKEN"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@tencent-weixin/openclaw-weixin@2.4.9",pin:true,integrity:"sha512-SfaYehR1Cwq2VV5HxJBp9sVilMms420VfZlMbF4YjRbWomr5+GxfXp9HkeU6y5TbnOc4Ysq0qPw1yBvJwbenBA==",tarballUrl:"https://registry.npmjs.org/@tencent-weixin/openclaw-weixin/-/openclaw-weixin-2.4.9.tgz",runtimeLock:{cachePath:"/usr/local/share/nemoclaw/wechat-npm-cache",installCacheEnvKey:"NEMOCLAW_WECHAT_NPM_INSTALL_CACHE",lockFile:"/usr/local/lib/nemoclaw/wechat-runtime/package-lock.json",projectsRoot:"/sandbox/.openclaw/npm/projects",verifierPath:"/usr/local/lib/nemoclaw/verify-wechat-runtime-lock.mts",offline:true,legacyPeerDeps:true},required:true}],hooks:[{id:"wechat-host-qr",phase:"enroll",handler:"wechat.ilinkLogin",inputs:["allowedIds"],outputs:[{id:"botToken",kind:"secret",required:true},{id:"accountId",kind:"config",required:true},{id:"baseUrl",kind:"config"},{id:"userId",kind:"config"},{id:"allowedIds",kind:"config"}],onFailure:"skip-channel"},{id:"wechat-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowedIds",kind:"config"}]},{id:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.planHookId,phase:"post-agent-install",handler:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.handlerId,agents:["openclaw"],inputs:["wechatConfig.accountId","wechatConfig.baseUrl","wechatConfig.userId","credential.wechatBotToken.placeholder"],outputs:[{id:"openclawWeixinAccountsIndex",kind:"build-file",required:true},{id:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.outputId,kind:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.kind,required:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.required},{id:"openclawConfigPatch",kind:"build-file",required:true}],onFailure:"abort"},{id:"wechat-health-check",phase:"health-check",handler:"wechat.healthCheck",inputs:["wechatConfig.accountId"],onFailure:"abort"}]};var whatsappManifest={schemaVersion:1,id:"whatsapp",displayName:"WhatsApp",description:"WhatsApp Web messaging (QR pairing)",enrollmentHelp:"WhatsApp Web pairs via QR code scanned with your phone \u2014 no host-side token. After the sandbox is running, run `openshell term` and then use `openclaw channels login --channel whatsapp` for OpenClaw or `hermes whatsapp` for Hermes to display the QR.",enrollmentNotes:["After pairing, run `nemoclaw channels status --channel whatsapp`. OpenClaw reports inbound delivery evidence; Hermes reports gateway and dashboard session-path diagnostics."],supportedAgents:["openclaw","hermes"],auth:{mode:"in-sandbox-qr"},inputs:[{id:"mode",kind:"config",required:false,envKey:"WHATSAPP_MODE",statePath:"whatsappConfig.mode",validValues:["self-chat","bot"],defaultValue:"self-chat",prompt:{label:"WhatsApp reply mode",help:"self-chat replies only to messages the paired account sends to itself. bot replies to other senders and stops replying to that self-chat: an unknown sender receives a pairing code you approve with `hermes pairing approve whatsapp `, unless you set WHATSAPP_ALLOWED_IDS to a fixed sender list before this command.",emptyValueMessage:"the sandbox replies only in your own self-chat"}},{id:"allowedIds",kind:"config",required:false,envKey:"WHATSAPP_ALLOWED_IDS",statePath:"allowedIds.whatsapp"}],credentials:[],state:{openclaw:["whatsapp"],hermes:["platforms/whatsapp","profiles/dashboard-home/platforms/whatsapp/session","dashboard-home/platforms/whatsapp/session"]},policyPresets:["whatsapp"],render:[{id:"whatsapp-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.whatsapp",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false}}}}}},{id:"whatsapp-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.whatsapp",value:{enabled:true}}},{id:"whatsapp-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WHATSAPP_ENABLED=true","WHATSAPP_MODE={{whatsappConfig.mode}}","WHATSAPP_DM_POLICY={{whatsappConfig.dmPolicy}}","WHATSAPP_ALLOWED_USERS={{allowedIds.whatsapp.csv}}"]},{id:"whatsapp-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.whatsapp",value:{enabled:true}}}],runtime:{openclaw:{channelName:"whatsapp",visibility:{configKeys:["whatsapp"],logPatterns:["whatsapp"]},nodePreloads:[{module:"whatsapp-qr-compact",injectInto:["connect"],optional:true,installMessage:"[channels] Installing WhatsApp compact-QR renderer (scan-friendly pairing)"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/whatsapp@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-vOWQIk7FpLHrhMmO+FaLi+pnFB82hiWNJJFJONkBuofERh2SMEz7EMut/vECFFEjFnmOZSVlYfRlxhbNkd/R6g==","2026.9.1":"sha512-llIcoMa6FM4SgYn7GG1FQIeTTA5JDdcHW5D7PT+3aGYT3/E2eLFutKwDvD/w7G0hvDwSftzZgLi3iA8dzK7a3A=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.1.tgz"},required:true}],hooks:[{id:"whatsapp-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["hermes"],outputs:[{id:"mode",kind:"config"}]},{id:"whatsapp-status-health",phase:"status",handler:"whatsapp.statusHealth",agents:["openclaw","hermes"],outputs:[{id:"channelHealth",kind:"status"}]}]};var BUILT_IN_CHANNEL_MANIFESTS=[telegramManifest,discordManifest,wechatManifest,slackManifest,whatsappManifest,teamsManifest,googlechatManifest];function createBuiltInChannelManifestRegistry(){return createChannelManifestRegistry(BUILT_IN_CHANNEL_MANIFESTS)}var EXACT_TEMPLATE_PATTERN=/^\{\{\s*([^}]+?)\s*\}\}$/;var TEMPLATE_REFERENCE_PATTERN=/\{\{\s*([^}]+?)\s*\}\}/g;function resolvedRenderTemplateReference(value){return{matched:true,value}}function resolveSandboxNameTemplate(value,sandboxName){return value.replaceAll("{sandboxName}",sandboxName)}function resolveRenderTemplatesInValue(value,context){if(typeof value==="string")return resolveRenderTemplatesInString(value,context);if(Array.isArray(value)){if(value.length===0)return value;const resolved=value.map(entry=>resolveRenderTemplatesInValue(entry,context)).filter(entry=>entry!==void 0);return resolved.length>0?resolved:void 0}if(value&&typeof value==="object"){const sourceEntries=Object.entries(value);if(sourceEntries.length===0)return value;const entries=sourceEntries.map(([key,entry])=>[key,resolveRenderTemplatesInValue(entry,context)]).filter(entry=>entry[1]!==void 0);return entries.length>0?Object.fromEntries(entries):void 0}return value}function isTruthyRenderTemplate(value,context){if(!value)return true;const resolved=resolveRenderTemplatesInString(value,context);if(resolved===void 0||resolved===null||resolved===false)return false;if(Array.isArray(resolved))return resolved.length>0;if(typeof resolved==="object")return Object.keys(resolved).length>0;if(typeof resolved==="string")return resolved.trim().length>0;return true}function resolveRenderTemplatesInString(value,context){const exact=value.match(EXACT_TEMPLATE_PATTERN);if(exact?.[1])return resolveTemplateReference(exact[1].trim(),context);let omitted=false;const resolved=value.replace(TEMPLATE_REFERENCE_PATTERN,(match,reference)=>{const replacement=resolveTemplateReference(reference.trim(),context);if(replacement===void 0||replacement===null){omitted=true;return""}if(Array.isArray(replacement))return replacement.map(String).join(",");if(typeof replacement==="object")return JSON.stringify(replacement);return String(replacement)});return omitted?void 0:resolved}function resolveTemplateReference(reference,context){const resolved=context.referenceResolver?.(reference,context);return resolved?.matched?resolved.value:"{{"+reference+"}}"}function allowedIds(context,channel){return parseList(stateValue(context,`allowedIds.${channel}`))}function stateValue(context,path4){const stateInput=context.inputs.find(input=>input.statePath===path4);if(stateInput?.value!==void 0)return stateInput.value;const inputId=path4.split(".").at(-1);return context.inputs.find(input=>input.inputId===inputId)?.value}function parseList(value){if(Array.isArray(value))return unique(value.map(String).map(cleanString).filter(Boolean));const text=cleanString(value);if(!text)return[];return unique(text.split(",").map(cleanString).filter(Boolean))}function parseBoolean(value){if(typeof value==="boolean")return value;const text=cleanString(value)?.toLowerCase();if(text==="1"||text==="true"||text==="yes"||text==="on")return true;if(text==="0"||text==="false"||text==="no"||text==="off")return false;return void 0}function nonEmptyString(value){return cleanString(value)||void 0}function cleanString(value){const text=String(value??"");if(/[\r\n]/.test(text)){throw new Error("Messaging template values must not contain line breaks.")}return text.trim()}function nonEmptyArray(values){return values.length>0?[...values]:void 0}function nonEmptyCsv(values){return values.length>0?values.join(","):void 0}function nonEmptyObject(value){return Object.keys(value).length>0?value:void 0}function unique(values){return[...new Set(values)]}var resolveDiscordTemplateReference=(reference,context)=>{if(reference==="discordProxyUrl")return resolvedRenderTemplateReference(void 0);switch(reference){case"discord.guilds":return resolvedRenderTemplateReference(nonEmptyObject(discordGuilds(context)));case"discord.hasGuilds":return resolvedRenderTemplateReference(Object.keys(discordGuilds(context)).length>0);case"discord.guildIds.csv":return resolvedRenderTemplateReference(nonEmptyCsv(Object.keys(discordGuilds(context))));case"discord.allowedUsers.values":return resolvedRenderTemplateReference(nonEmptyArray(discordAllowedUsers(context)));case"discord.allowedUsers.csv":return resolvedRenderTemplateReference(nonEmptyCsv(discordAllowedUsers(context)));case"discord.allowedUsers.dmPolicy":return resolvedRenderTemplateReference(discordAllowedUsers(context).length>0?"allowlist":void 0);case"discord.allowAllUsers":return resolvedRenderTemplateReference(Object.keys(discordGuilds(context)).length>0&&discordAllowedUsers(context).length===0?true:void 0);case"discord.requireMention":return resolvedRenderTemplateReference(discordRequireMention(context));default:return void 0}};function discordGuilds(context){const serverIds=parseList(stateValue(context,"discordGuilds.serverId"));if(serverIds.length===0)return{};const users=parseList(stateValue(context,"discordGuilds.userIds"));const requireMention=parseBoolean(stateValue(context,"discordGuilds.requireMention"))??true;return Object.fromEntries(serverIds.map(serverId=>[serverId,{requireMention,...users.length>0?{users}:{}}]))}function discordAllowedUsers(context){const users=new Set(allowedIds(context,"discord"));for(const guild of Object.values(discordGuilds(context))){for(const user of guild.users??[])users.add(String(user))}return[...users]}function discordRequireMention(context){for(const guild of Object.values(discordGuilds(context))){if(typeof guild.requireMention==="boolean")return guild.requireMention}return true}var DEFAULT_AUDIENCE_TYPE="app-url";var APP_PRINCIPAL_DISCOVERY_SENTINEL="000000000000000000000";var resolveGooglechatTemplateReference=(reference,context)=>{switch(reference){case"googlechatConfig.audienceType":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.audienceType"))??DEFAULT_AUDIENCE_TYPE);case"googlechatConfig.audience":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.audience")));case"googlechatConfig.appPrincipal":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.appPrincipal"))??APP_PRINCIPAL_DISCOVERY_SENTINEL);case"googlechatConfig.projectId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.projectId")));case"googlechatConfig.subscriptionName":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.subscriptionName")));default:break}const allowReference=reference.match(/^allowedIds[.]googlechat[.](values|dmPolicy|csv)$/);if(!allowReference?.[1])return void 0;const ids=allowedIds(context,"googlechat");switch(allowReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);case"csv":return resolvedRenderTemplateReference(ids.length>0?ids.join(","):void 0);default:return void 0}};var resolveSlackTemplateReference=(reference,context)=>{if(reference==="slackConfig.allowedChannels.csv"){return resolvedRenderTemplateReference(nonEmptyCsv(slackAllowedChannels(context)))}const allowedIdsReference=reference.match(/^allowedIds[.]slack[.](values|csv|dmPolicy|groupPolicy|channels)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"slack");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);case"groupPolicy":return resolvedRenderTemplateReference(ids.length>0||slackAllowedChannels(context).length>0?"allowlist":void 0);case"channels":return resolvedRenderTemplateReference(slackChannelConfig(context,ids));default:return void 0}};function slackChannelConfig(context,users){const allowedChannels=slackAllowedChannels(context);const entry={enabled:true,requireMention:true,...users.length>0?{users:[...users]}:{}};if(allowedChannels.length>0){return Object.fromEntries(allowedChannels.map(channelId=>[channelId,{...entry}]))}return users.length>0?{"*":entry}:void 0}function slackAllowedChannels(context){return parseList(stateValue(context,"slackConfig.allowedChannels"))}var DEFAULT_TEAMS_WEBHOOK_PORT=3978;var resolveTeamsTemplateReference=(reference,context)=>{switch(reference){case"teamsConfig.appId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"teamsConfig.appId")));case"teamsConfig.tenantId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"teamsConfig.tenantId")));case"teamsConfig.webhookPort":return resolvedRenderTemplateReference(teamsWebhookPort(context));case"teamsConfig.requireMention":return resolvedRenderTemplateReference(parseBoolean(stateValue(context,"teamsConfig.requireMention")));default:break}const allowedIdsReference=reference.match(/^allowedIds[.]teams[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"teams");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function teamsWebhookPort(context){const raw=nonEmptyString(stateValue(context,"teamsConfig.webhookPort"));if(!raw)return DEFAULT_TEAMS_WEBHOOK_PORT;const port=Number(raw);if(!Number.isInteger(port)||port<1||port>65535){throw new Error("Microsoft Teams webhook port must be an integer TCP port between 1 and 65535.")}return port}var DEFAULT_PROXY_HOST="10.200.0.1";var DEFAULT_PROXY_PORT="3128";var DEFAULT_TELEGRAM_GROUP_POLICY="open";var TELEGRAM_GROUP_POLICIES=new Set(["open","allowlist","disabled"]);var resolveTelegramTemplateReference=(reference,context)=>{if(reference==="proxyUrl")return resolvedRenderTemplateReference(proxyUrl(context.env));if(reference==="telegramConfig.groupPolicy"){return resolvedRenderTemplateReference(telegramGroupPolicy(context))}if(reference==="telegramConfig.openclawGroups"){return resolvedRenderTemplateReference(telegramOpenClawGroups(context))}if(reference==="telegramConfig.requireMention"){return resolvedRenderTemplateReference(parseBoolean(stateValue(context,"telegramConfig.requireMention")))}const allowedIdsReference=reference.match(/^allowedIds[.]telegram[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"telegram");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function proxyUrl(env){const host=nonEmptyString(env?.NEMOCLAW_PROXY_HOST)??DEFAULT_PROXY_HOST;const port=nonEmptyString(env?.NEMOCLAW_PROXY_PORT)??DEFAULT_PROXY_PORT;return`http://${host}:${port}`}function telegramGroupPolicy(context){const value=nonEmptyString(stateValue(context,"telegramConfig.groupPolicy"));return value&&TELEGRAM_GROUP_POLICIES.has(value)?value:DEFAULT_TELEGRAM_GROUP_POLICY}function telegramOpenClawGroups(context){if(telegramGroupPolicy(context)!=="open")return void 0;const requireMention=parseBoolean(stateValue(context,"telegramConfig.requireMention"));return requireMention===true?{"*":{requireMention:true}}:void 0}var WECHAT_ILINK_HOSTS=new Set(["ilinkai.weixin.qq.com","ilinkai.wechat.com"]);var WECHAT_ILINK_IDC_HOST_PATTERN=/^idc-[0-9]+[.]weixin[.]qq[.]com$/u;function normalizeWechatIlinkBaseUrl(value){const raw=String(value??"");if(/[\r\n]/.test(raw)){throw new Error("WeChat baseUrl must not contain line breaks.")}const text=raw.trim();if(!text)return void 0;let url;try{url=new URL(text)}catch{throw new Error("WeChat baseUrl must be a valid URL.")}if(url.protocol!=="https:"){throw new Error("WeChat baseUrl must use HTTPS.")}if(url.username||url.password){throw new Error("WeChat baseUrl must not include credentials.")}const authority=text.match(/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/iu)?.[1]??"";if(authority.includes(":")){throw new Error("WeChat baseUrl must not include an explicit port.")}if(!isWechatIlinkHost(url.hostname)){throw new Error("WeChat baseUrl must use an expected iLink host.")}if(url.pathname&&url.pathname!=="/"||url.search||url.hash){throw new Error("WeChat baseUrl must be an iLink origin URL.")}return url.origin}function isWechatIlinkHost(hostname){const normalized=hostname.toLowerCase();return WECHAT_ILINK_HOSTS.has(normalized)||isWechatIlinkIdcHost(normalized)}function isWechatIlinkIdcHost(hostname){return WECHAT_ILINK_IDC_HOST_PATTERN.test(hostname.toLowerCase())}var resolveWechatTemplateReference=(reference,context)=>{const wechatConfig=reference.match(/^wechatConfig[.](accountId|baseUrl|userId)$/);if(wechatConfig?.[1]){if(wechatConfig[1]==="baseUrl"){return resolvedRenderTemplateReference(normalizeWechatIlinkBaseUrl(stateValue(context,"wechatConfig.baseUrl")))}return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"wechatConfig."+wechatConfig[1])))}const allowedIdsReference=reference.match(/^allowedIds[.]wechat[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=wechatAllowedIds(context);switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function wechatAllowedIds(context){const ids=allowedIds(context,"wechat");const userId=nonEmptyString(stateValue(context,"wechatConfig.userId"));return userId&&!ids.includes(userId)?[userId,...ids]:ids}var DEFAULT_WHATSAPP_MODE="self-chat";var BOT_WHATSAPP_MODE="bot";var WHATSAPP_MODES=new Set([DEFAULT_WHATSAPP_MODE,BOT_WHATSAPP_MODE]);var resolveWhatsappTemplateReference=(reference,context)=>{if(reference==="whatsappConfig.mode"){return resolvedRenderTemplateReference(whatsappMode(context))}if(reference==="whatsappConfig.dmPolicy"){return resolvedRenderTemplateReference(whatsappDmPolicy(context))}const allowedIdsReference=reference.match(/^allowedIds[.]whatsapp[.](values|csv)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"whatsapp");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));default:return void 0}};function whatsappMode(context){const value=nonEmptyString(stateValue(context,"whatsappConfig.mode"));return value&&WHATSAPP_MODES.has(value)?value:DEFAULT_WHATSAPP_MODE}function whatsappDmPolicy(context){if(whatsappMode(context)!==BOT_WHATSAPP_MODE)return void 0;return allowedIds(context,"whatsapp").length>0?"allowlist":"pairing"}var BUILT_IN_TEMPLATE_REFERENCE_RESOLVERS=[resolveTelegramTemplateReference,resolveDiscordTemplateReference,resolveWechatTemplateReference,resolveSlackTemplateReference,resolveWhatsappTemplateReference,resolveTeamsTemplateReference,resolveGooglechatTemplateReference];function createBuiltInRenderTemplateResolver(){return(reference,context)=>{for(const resolver of BUILT_IN_TEMPLATE_REFERENCE_RESOLVERS){const resolved=resolver(reference,context);if(resolved)return resolved}return void 0}}var import_node_crypto2=__toESM(require("node:crypto"));function hashCredential(value){const normalized=String(value??"").trim();if(!normalized)return null;return import_node_crypto2.default.createHash("sha256").update(normalized).digest("hex")}function planCredentialBindings(manifest,context,inputs,environment=process.env){return manifest.credentials.map(credential=>{const sourceInput=inputs.find(input=>input.inputId===credential.sourceInput);const credentialAvailable=sourceInput?.credentialAvailable===true||context.credentialAvailability?.[credential.id]===true||context.credentialAvailability?.[`${manifest.id}.${credential.id}`]===true;const envKey=sourceInput?.sourceEnv??credential.providerEnvKey;const credentialHash=credentialAvailable?hashCredential(environment[envKey])??void 0:void 0;return{channelId:manifest.id,credentialId:credential.id,sourceInput:credential.sourceInput,providerName:resolveSandboxNameTemplate(credential.providerName,context.sandboxName),providerEnvKey:credential.providerEnvKey,placeholder:credential.placeholder,credentialAvailable,...credentialHash!==void 0?{credentialHash}:{}}})}function planHostForward(manifest,inputs,active,referenceResolver,environment=process.env){if(!active||!manifest.hostForward)return void 0;const context={inputs,env:environment,referenceResolver};if(!isTruthyRenderTemplate(manifest.hostForward.when,context))return void 0;const portValue=resolveRenderTemplatesInValue(manifest.hostForward.port,context);const port=normalizeForwardPort(manifest.id,portValue);return{channelId:manifest.id,port,label:manifest.hostForward.label}}function normalizeForwardPort(channelId,value){const port=typeof value==="number"?value:Number(String(value??"").trim());if(!Number.isInteger(port)||port<1||port>65535){throw new Error(`Channel manifest '${channelId}' declares invalid host forward port '${String(value)}'.`)}return port}var OPENSHELL_ENV_PLACEHOLDER_PREFIX="openshell:resolve:env:";var OPENSHELL_ALIAS_PLACEHOLDER_RE=/^[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-(.+)$/;function normalizeProviderPlaceholderForEnvKey(value,envKey){if(value.startsWith(OPENSHELL_ENV_PLACEHOLDER_PREFIX)){return placeholderSuffixMatchesEnvKey(value.slice(OPENSHELL_ENV_PLACEHOLDER_PREFIX.length),envKey)?`${OPENSHELL_ENV_PLACEHOLDER_PREFIX}${envKey}`:null}const aliasMatch=value.match(OPENSHELL_ALIAS_PLACEHOLDER_RE);if(!aliasMatch||!placeholderSuffixMatchesEnvKey(aliasMatch[1],envKey)){return null}return value.replace(/-OPENSHELL-RESOLVE-ENV-.+$/,`-OPENSHELL-RESOLVE-ENV-${envKey}`)}function placeholderSuffixMatchesEnvKey(suffix,envKey){if(suffix===envKey)return true;const generationMatch=suffix.match(/^(?:v[0-9]{1,20}|s[a-f0-9]{64})_(.+)$/);return generationMatch?.[1]===envKey}function hasFullPersistedCredentialBindingShape(binding){return typeof binding.channelId==="string"&&typeof binding.credentialId==="string"&&typeof binding.sourceInput==="string"&&typeof binding.providerName==="string"&&typeof binding.providerEnvKey==="string"&&typeof binding.placeholder==="string"&&typeof binding.credentialAvailable==="boolean"}function normalizeFullPersistedCredentialBindings(bindings){return bindings.map(binding=>({channelId:binding.channelId,credentialId:binding.credentialId,sourceInput:binding.sourceInput,providerName:binding.providerName,providerEnvKey:binding.providerEnvKey,placeholder:normalizeProviderPlaceholderForEnvKey(binding.placeholder,binding.providerEnvKey)??binding.placeholder,credentialAvailable:binding.credentialAvailable===true,...typeof binding.credentialHash==="string"?{credentialHash:binding.credentialHash}:{}}))}function normalizePersistedAgentCredentialPlaceholders(render,credentialBindings){const credentialEnvKeys=new Set(credentialBindings.map(binding=>binding.providerEnvKey).filter(Boolean));if(credentialEnvKeys.size===0)return[...render];return render.map(entry=>{if(entry.kind!=="env-lines")return entry;return{...entry,lines:entry.lines.map(line=>normalizeCredentialEnvLine(line,credentialEnvKeys))}})}function normalizeCredentialEnvLine(line,credentialEnvKeys){const index=line.indexOf("=");if(index<=0)return line;const envKey=line.slice(0,index).trim();if(!credentialEnvKeys.has(envKey))return line;const value=line.slice(index+1);const normalized=normalizeProviderPlaceholderForEnvKey(value,envKey);return normalized?`${envKey}=${normalized}`:line}function normalizePersistedSandboxMessagingPlanShape(plan,environment=process.env){const manifestRegistry=createBuiltInChannelManifestRegistry();const disabledChannels=plan.disabledChannels.filter(channelId=>typeof channelId==="string");const disabledSet=new Set(disabledChannels);const channels=plan.channels.map(channel=>normalizePersistedChannel(channel,disabledSet,manifestRegistry.get(channel.channelId),environment));const credentialBindings=normalizePersistedCredentialBindings(plan,channels,manifestRegistry,environment);const normalizedPlan={...plan,channels,disabledChannels,credentialBindings,networkPolicy:plan.networkPolicy&&Array.isArray(plan.networkPolicy.entries)?plan.networkPolicy:{presets:[],entries:[]},agentRender:normalizePersistedAgentCredentialPlaceholders(Array.isArray(plan.agentRender)?[...plan.agentRender]:[],credentialBindings),buildSteps:Array.isArray(plan.buildSteps)?[...plan.buildSteps]:[],...plan.runtimeSetup!==void 0?{runtimeSetup:normalizeRuntimeSetup(plan.runtimeSetup)}:{},stateUpdates:Array.isArray(plan.stateUpdates)?[...plan.stateUpdates]:[],healthChecks:Array.isArray(plan.healthChecks)?[...plan.healthChecks]:[]};return normalizedPlan}function normalizePersistedChannel(channel,disabledSet,manifest,environment){const disabled=channel.disabled??disabledSet.has(channel.channelId);const configured=channel.configured??true;const hasFullShape=hasFullChannelShape(channel);const inputs=hasFullShape?normalizeFullInputs(channel.channelId,channel.inputs??[]):normalizePersistedInputs(channel,manifest);const active=channel.active??(configured&&!disabled&&requiredInputsAvailable(manifest,inputs));const hostForward=manifest?planHostForward(manifest,inputs,active&&!disabled,createBuiltInRenderTemplateResolver(),environment):void 0;return{channelId:channel.channelId,displayName:channel.displayName??manifest?.displayName??channel.channelId,authMode:channel.authMode??manifest?.auth.mode??"none",active,selected:channel.selected??configured,configured,disabled,...channel.pendingRemoval===true?{pendingRemoval:true}:{},inputs,...hostForward?{hostForward}:{},hooks:Array.isArray(channel.hooks)?[...channel.hooks]:[]}}function normalizePersistedInputs(channel,manifest){const persistedById=new Map((channel.inputs??[]).filter(input=>typeof input.inputId==="string").map(input=>[input.inputId,input]));const fromManifest=(manifest?.inputs??[]).map(input=>inputReferenceFromManifest(channel.channelId,input,persistedById.get(input.id)));const manifestInputIds=new Set((manifest?.inputs??[]).map(input=>input.id));const unknownInputs=[...persistedById.values()].flatMap(input=>{if(!input.inputId||manifestInputIds.has(input.inputId))return[];return[normalizeUnknownInput(channel.channelId,input)]});return[...fromManifest,...unknownInputs]}function normalizeFullInputs(channelId,inputs){return inputs.filter(input=>typeof input.inputId==="string").map(input=>({channelId:typeof input.channelId==="string"?input.channelId:channelId,inputId:input.inputId,kind:input.kind==="secret"||input.kind==="config"?input.kind:"config",required:typeof input.required==="boolean"?input.required:false,...typeof input.sourceEnv==="string"?{sourceEnv:input.sourceEnv}:{},...typeof input.statePath==="string"?{statePath:input.statePath}:{},...input.credentialAvailable!==void 0?{credentialAvailable:input.credentialAvailable}:{},...input.value!==void 0?{value:input.value}:{}}))}function inputReferenceFromManifest(channelId,input,persisted){return{channelId,inputId:input.id,kind:input.kind,required:input.required,...input.envKey?{sourceEnv:input.envKey}:{},...input.kind==="config"&&input.statePath?{statePath:input.statePath}:{},...persisted?.credentialAvailable!==void 0?{credentialAvailable:persisted.credentialAvailable}:{},...persisted?.value!==void 0?{value:persisted.value}:{}}}function normalizeUnknownInput(channelId,input){const kind=input.kind==="secret"||input.kind==="config"?input.kind:"config";return{channelId,inputId:input.inputId,kind,required:input.required===true,...typeof input.sourceEnv==="string"?{sourceEnv:input.sourceEnv}:{},...typeof input.statePath==="string"?{statePath:input.statePath}:{},...input.credentialAvailable!==void 0?{credentialAvailable:input.credentialAvailable}:{},...input.value!==void 0?{value:input.value}:{}}}function requiredInputsAvailable(manifest,inputs){if(!manifest)return true;return manifest.inputs.every(manifestInput=>{if(!manifestInput.required)return true;const input=inputs.find(entry=>entry.inputId===manifestInput.id);if(!input)return false;if(input.kind==="secret")return input.credentialAvailable===true;if(input.value===void 0)return false;return typeof input.value==="string"?input.value.trim().length>0:true})}function normalizePersistedCredentialBindings(plan,channels,manifestRegistry,environment){const persisted=plan.credentialBindings??[];if(Array.isArray(plan.credentialBindings)&&plan.channels.every(hasFullChannelShape)&&persisted.every(hasFullPersistedCredentialBindingShape)){return normalizeFullPersistedCredentialBindings(persisted)}const manifests=channels.flatMap(channel=>{const manifest=manifestRegistry.get(channel.channelId);return manifest?[manifest]:[]});const planForBindings={...plan,channels,credentialBindings:[],networkPolicy:{presets:[],entries:[]},agentRender:[],buildSteps:[],runtimeSetup:{nodePreloads:[],envAliases:[],secretScans:[]},stateUpdates:[],healthChecks:[]};const generated=credentialBindingsFromManifests(planForBindings,manifests,new Map(channels.map(channel=>[channel.channelId,channel.inputs])),environment);return generated.map(binding=>overlayPersistedCredentialBinding(binding,persisted))}function credentialBindingsFromManifests(plan,manifests,inputRegistry,environment){const context=compilerContext(plan);return manifests.flatMap(manifest=>planCredentialBindings(manifest,context,inputRegistry.get(manifest.id)??[],environment).map(binding=>overlayPersistedCredentialBinding(binding,plan.credentialBindings)))}function overlayPersistedCredentialBinding(binding,persisted){const match=persisted.find(candidate=>credentialBindingMatches(binding,candidate));if(!match)return binding;return{...binding,credentialAvailable:typeof match.credentialAvailable==="boolean"?match.credentialAvailable:binding.credentialAvailable,...typeof match.credentialHash==="string"&&match.credentialHash.length>0?{credentialHash:match.credentialHash}:binding.credentialHash?{credentialHash:binding.credentialHash}:{}}}function credentialBindingMatches(binding,candidate){if(candidate.channelId&&candidate.channelId!==binding.channelId)return false;if(candidate.providerEnvKey&&candidate.providerEnvKey===binding.providerEnvKey)return true;if(candidate.credentialId&&candidate.credentialId===binding.credentialId)return true;if(candidate.sourceInput&&candidate.sourceInput===binding.sourceInput)return true;return false}function hasFullChannelShape(channel){return typeof channel.displayName==="string"&&typeof channel.authMode==="string"&&typeof channel.active==="boolean"&&typeof channel.selected==="boolean"&&typeof channel.configured==="boolean"&&typeof channel.disabled==="boolean"&&Array.isArray(channel.inputs)}function normalizeRuntimeSetup(setup){return{nodePreloads:Array.isArray(setup?.nodePreloads)?[...setup.nodePreloads]:[],envAliases:Array.isArray(setup?.envAliases)?[...setup.envAliases]:[],secretScans:Array.isArray(setup?.secretScans)?[...setup.secretScans]:[]}}function compilerContext(plan){return{sandboxName:plan.sandboxName,agent:plan.agent,workflow:plan.workflow,isInteractive:false,configuredChannels:plan.channels.map(channel=>channel.channelId),disabledChannels:plan.disabledChannels,credentialAvailability:credentialAvailabilityFromPlan(plan)}}function credentialAvailabilityFromPlan(plan){const availability={};for(const channel of plan.channels){for(const input of channel.inputs){if(input.kind!=="secret"||input.credentialAvailable!==true)continue;availability[`${channel.channelId}.${input.inputId}`]=true;if(input.sourceEnv)availability[input.sourceEnv]=true}}for(const credential of plan.credentialBindings){if(!credential.credentialAvailable)continue;availability[credential.credentialId]=true;availability[`${credential.channelId}.${credential.credentialId}`]=true;availability[`${credential.channelId}.${credential.sourceInput}`]=true;availability[credential.providerEnvKey]=true}return availability}function normalizeMessagingChannelId(channelId){return channelId.trim().toLowerCase()}function enabledPlanChannels(plan){const disabled=new Set((plan.disabledChannels??[]).map(normalizeMessagingChannelId).filter(Boolean));return plan.channels.filter(channel=>{const channelId=normalizeMessagingChannelId(channel.channelId);return channelId.length>0&&channel.active&&!channel.disabled&&!disabled.has(channelId)})}function selectActiveMessagingChannelIds(plan){const seen=new Set;const channels=[];for(const item of enabledPlanChannels(plan)){const channel=normalizeMessagingChannelId(item.channelId);if(!channel||seen.has(channel))continue;seen.add(channel);channels.push(channel)}return channels}function selectEnabledMessagingAgentRender(plan){const active=new Set(selectActiveMessagingChannelIds(plan));return plan.agentRender.filter(render=>render.agent===plan.agent&&active.has(normalizeMessagingChannelId(render.channelId)))}function selectEnabledPostAgentInstallBuildFiles(plan){const active=new Set(selectActiveMessagingChannelIds(plan));const channels=enabledPlanChannels(plan);return plan.buildSteps.filter(step=>{const channelId=normalizeMessagingChannelId(step.channelId);if(!active.has(channelId)||step.kind!=="build-file")return false;if(!step.hookId)return true;const matchingChannels=channels.filter(channel=>normalizeMessagingChannelId(channel.channelId)===channelId);if(matchingChannels.length!==1)return false;const matchedHook=matchingChannels[0]?.hooks?.find(hook=>hook.id===step.hookId);return matchedHook!==void 0&&matchedHook.phase==="post-agent-install"})}function parseSandboxMessagingPlan(value,options={}){if(!isObjectRecord(value)||value.schemaVersion!==1||typeof value.sandboxName!=="string"||typeof value.agent!=="string"||typeof value.workflow!=="string"||!Array.isArray(value.channels)||!Array.isArray(value.disabledChannels)||!isOptionalObjectArray(value,"credentialBindings")||Object.hasOwn(value,"networkPolicy")&&!isObjectRecord(value.networkPolicy)||!isOptionalObjectArray(value,"agentRender")||!isOptionalObjectArray(value,"buildSteps")||!isRuntimeSetup(value.runtimeSetup)||!isOptionalObjectArray(value,"stateUpdates")||!isOptionalObjectArray(value,"healthChecks")){return null}if(options.sandboxName&&value.sandboxName!==options.sandboxName)return null;if(options.agent&&value.agent!==options.agent)return null;const supported=Array.isArray(options.supportedChannelIds)?new Set(options.supportedChannelIds):null;const normalizedChannelIds=new Set;for(const channel of value.channels){if(!isObjectRecord(channel)||typeof channel.channelId!=="string")return null;const normalizedChannelId=normalizeMessagingChannelId(channel.channelId);if(!normalizedChannelId||normalizedChannelId!==channel.channelId||normalizedChannelIds.has(normalizedChannelId)){return null}if(Object.hasOwn(channel,"configured")&&typeof channel.configured!=="boolean"){return null}if(Object.hasOwn(channel,"active")&&typeof channel.active!=="boolean")return null;if(Object.hasOwn(channel,"disabled")&&typeof channel.disabled!=="boolean")return null;if(Object.hasOwn(channel,"pendingRemoval")&&typeof channel.pendingRemoval!=="boolean"){return null}if(Object.hasOwn(channel,"inputs")&&!Array.isArray(channel.inputs))return null;if(Object.hasOwn(channel,"hostForward")&&!isHostForward(channel.hostForward))return null;if(Object.hasOwn(channel,"hooks")&&!Array.isArray(channel.hooks))return null;if(Array.isArray(channel.inputs)&&channel.inputs.some(input=>!isObjectRecord(input)||typeof input.inputId!=="string"||Object.hasOwn(input,"channelId")&&input.channelId!==normalizedChannelId)){return null}if(Array.isArray(channel.hooks)&&channel.hooks.some(hook=>!isObjectRecord(hook)||Object.hasOwn(hook,"channelId")&&hook.channelId!==normalizedChannelId)){return null}if(Object.hasOwn(channel,"hostForward")&&isObjectRecord(channel.hostForward)&&channel.hostForward.channelId!==normalizedChannelId){return null}if(supported&&!supported.has(channel.channelId))return null;normalizedChannelIds.add(normalizedChannelId)}if(!value.disabledChannels.every(isCanonicalMessagingChannelId))return null;const disabledChannelIds=new Set(value.disabledChannels);if(disabledChannelIds.size!==value.disabledChannels.length||[...disabledChannelIds].some(channelId=>!normalizedChannelIds.has(channelId))||value.channels.some(channel=>isObjectRecord(channel)&&channel.disabled===true!==disabledChannelIds.has(String(channel.channelId)))){return null}if(!hasCanonicalChannelReferences(value.credentialBindings)||!hasMatchingAgentRenderEntries(value.agentRender,value.agent)||!hasCanonicalChannelReferences(value.agentRender)||!hasCanonicalChannelReferences(value.buildSteps)||!hasCanonicalChannelReferences(value.stateUpdates)||!hasCanonicalChannelReferences(value.healthChecks)||!hasCanonicalNetworkPolicyReferences(value.networkPolicy)||!hasCanonicalRuntimeSetupReferences(value.runtimeSetup)){return null}return cloneSandboxMessagingPlan(normalizePersistedSandboxMessagingPlanShape(value,options.environment))}function hasMatchingAgentRenderEntries(value,agent){return!Array.isArray(value)||value.every(render=>isObjectRecord(render)&&render.agent===agent)}function hasCanonicalNetworkPolicyReferences(value){if(!isObjectRecord(value)||!Object.hasOwn(value,"entries"))return true;return hasCanonicalChannelReferences(value.entries)}function cloneSandboxMessagingPlan(plan){return JSON.parse(JSON.stringify(plan))}function isOptionalObjectArray(value,key){if(!Object.hasOwn(value,key))return true;const entries=value[key];return Array.isArray(entries)&&entries.every(isObjectRecord)}function isHostForward(value){return isObjectRecord(value)&&typeof value.channelId==="string"&&typeof value.port==="number"&&Number.isInteger(value.port)&&value.port>=1&&value.port<=65535&&typeof value.label==="string"}function isRuntimeSetup(value){if(value===void 0)return true;return isObjectRecord(value)&&Array.isArray(value.nodePreloads)&&Array.isArray(value.envAliases)&&Array.isArray(value.secretScans)&&value.nodePreloads.every(isObjectRecord)&&value.envAliases.every(isObjectRecord)&&value.secretScans.every(isObjectRecord)}function isCanonicalMessagingChannelId(value){return typeof value==="string"&&value.length>0&&normalizeMessagingChannelId(value)===value}function hasCanonicalChannelReferences(value){return value===void 0||Array.isArray(value)&&value.every(entry=>isObjectRecord(entry)&&isCanonicalMessagingChannelId(entry.channelId))}function hasCanonicalRuntimeSetupReferences(value){if(value===void 0)return true;if(!isObjectRecord(value))return false;return["nodePreloads","envAliases","secretScans"].every(field=>hasCanonicalChannelReferences(value[field]))}var import_node_buffer=require("node:buffer");var import_node_crypto3=require("node:crypto");var import_node_util=require("node:util");function isLoopbackHostname(hostname=""){const normalized=String(hostname||"").trim().toLowerCase().replace(/^\[|\]$/g,"");return normalized==="localhost"||normalized==="::1"||/^127(?:\.\d{1,3}){3}$/.test(normalized)}function isLoopbackDashboardUrl(value){return isLoopbackHostname(new URL(value).hostname)}function listMessagingCredentialEnvAssignments(options={}){return selectManifests(options).flatMap(manifest=>{const credentialsByTemplate=new Map(manifest.credentials.map(credential=>[`{{credential.${credential.id}.placeholder}}`,credential]));const renderedAssignments=manifest.render.flatMap(render=>{if(options.agent&&render.agent!==options.agent)return[];if(render.kind!=="env-lines")return[];return render.lines.flatMap(line=>{const separator=line.indexOf("=");if(separator<=0)return[];const credential=credentialsByTemplate.get(line.slice(separator+1));if(!credential)return[];return[{channelId:manifest.id,agent:render.agent,sourceEnvKey:credential.providerEnvKey,targetEnvKey:line.slice(0,separator),placeholder:credential.placeholder}]})});const runtimeAssignments=["openclaw","hermes"].flatMap(agent=>{if(options.agent&&agent!==options.agent)return[];if(!manifest.supportedAgents.includes(agent))return[];return(manifest.runtime?.[agent]?.envAliases??[]).flatMap(alias=>{if(!alias.targetEnvKey)return[];const credential=manifest.credentials.find(candidate=>candidate.providerEnvKey===alias.envKey);if(!credential)return[];return[{channelId:manifest.id,agent,sourceEnvKey:alias.envKey,targetEnvKey:alias.targetEnvKey,placeholder:credential.placeholder}]})});return[...renderedAssignments,...runtimeAssignments]})}function selectManifests(options){const manifests=options.manifests??BUILT_IN_CHANNEL_MANIFESTS;const agent=options.agent;const selected=agent?manifests.filter(manifest=>manifest.supportedAgents.includes(agent)):manifests;return[...selected]}function authorizeMessagingManagedStartupFields(entry,section){if(section==="agentRender")return authorizeTeamsOpenClawWebhookField(entry);if(!isPlainDataObject3(entry))return[];const contract=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT;if(ownDataPropertyValue3(entry,"channelId")!==contract.channelId||ownDataPropertyValue3(entry,"hookId")!==contract.planHookId||ownDataPropertyValue3(entry,"handler")!==contract.handlerId||ownDataPropertyValue3(entry,"outputId")!==contract.outputId||ownDataPropertyValue3(entry,"kind")!==contract.kind||ownDataPropertyValue3(entry,"required")!==contract.required){return[]}return authorizeWechatAccountFilePlaceholders(ownDataPropertyValue3(entry,"value")).map(authorization=>({...authorization,path:["value",...authorization.path]}))}function isPlainDataObject3(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue3(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}var DCODE_UPSTREAM_PROVIDER_RE=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;function isValidDcodeUpstreamProvider(value){return DCODE_UPSTREAM_PROVIDER_RE.test(value)}var LEGACY_MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=1;var MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=2;var MANAGED_STARTUP_PROFILE_MAX_BYTES=64*1024;var MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES=Math.ceil(MANAGED_STARTUP_PROFILE_MAX_BYTES/3)*4;var MAX_IDENTIFIER_BYTES=256;var MAX_MODEL_BYTES=1024;var MAX_URL_BYTES=2048;var MAX_LIST_ITEMS=128;var MAX_JSON_NODES=4096;var MAX_JSON_DEPTH=32;var MAX_TUNING_INTEGER=1e9;var MIN_HERMES_CONTEXT_WINDOW=64e3;var SHA256_RE=/^[a-f0-9]{64}$/;var CONTROL_CHARACTER_RE=/[\u0000-\u001f\u007f-\u009f]/u;var BASE64URL_RE=/^[A-Za-z0-9_-]+$/;var RAW_CA_PEM_RE=/-----BEGIN (?:TRUSTED )?CERTIFICATE-----/iu;var RAW_CA_PEM_BASE64_RE=/^LS0tLS1CRUdJTi(?:BDRVJUSUZJQ0FURS0tLS0t|BUlVTVEVEIENFUlRJRklDQVRFLS0tLS0)/u;var RAW_CA_DER_BASE64_RE=/^MII[A-Za-z0-9+/=\r\n]{253,}$/u;var RAW_CA_DATA_URI_RE=/data:application\/(?:pkix-cert|x-x509-ca-cert);base64,MII[A-Za-z0-9+/=]{253,}/iu;var URL_CANDIDATE_RE=/[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'<>]+/gu;var UTF8_DECODER=new import_node_util.TextDecoder("utf-8",{fatal:true});var CREDENTIAL_SHAPED_NAME_PATTERN=/(?:^|[_-])(?:api[_-]?key|access[_-]?key|secret[_-]?key|auth[_-]?token|refresh[_-]?token|access[_-]?token|client[_-]?secret|private[_-]?key|pass[_-]?code|personal[_-]?access[_-]?token|connection[_-]?string|webhook(?:[_-]?url)?|key|secret|token|password|passwd|passcode|auth|authorization|credential|credentials|bearer|bearer[_-]?token|cookie|cookies|pat|private|privatekey|pin|webhookurl|dsn|connectionstring)(?:$|[_-])/iu;var CREDENTIAL_COMPOUND_NAME_PATTERN=/^(?:access|refresh|client|bearer|auth|api|private|signing|session|bot|app|resolved)(?:token|key|secret|password)$/iu;var CREDENTIAL_CAMEL_SUFFIX_PATTERN=/(?:apiKey|accessKey|secretKey|authToken|refreshToken|accessToken|clientSecret|privateKey|passcode|password|passwd|passphrase|bearerToken|botToken|appToken|sessionToken|signingKey|secretPublicKey|personalAccessToken|connectionString|webhookUrl)$/iu;var CREDENTIAL_CAMEL_BOUNDARY_PATTERN=/[a-z0-9](?:Token|Key|Secret|Password|Passphrase|Pat)$/u;var CREDENTIAL_ENV_NAME_PATTERN=/^(?:[A-Z0-9]+_)*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD|PASS|PASSPHRASE|CREDENTIAL)S?$/u;var CREDENTIAL_HEADER_NAME_PATTERN=/^(?:authorization|proxy-authorization|cookie|set-cookie|.+-(?:key|token|secret|password|passphrase|credential|auth)s?)$/iu;var PUBLIC_KEY_NAME_PATTERN=/^public[-_]?keys?$/iu;var PASS_CREDENTIAL_NAME_PATTERN=/(?:^|[-_])pass(?:wd)?$/iu;var NON_SECRET_KEY_METADATA_NAMES=new Set(["envKey","installCacheEnvKey","providerEnvKey","stateKey","targetEnvKey"]);var MESSAGING_CREDENTIAL_PLACEHOLDER_RE=/^(?:openshell:resolve:env:|[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-)(?:(?:v[0-9]{1,20}|s[a-f0-9]{64})_)?[A-Z][A-Z0-9_]*$/u;var MESSAGING_CREDENTIAL_ENV_ALIASES=new Set(listMessagingCredentialEnvAssignments().filter(({sourceEnvKey,targetEnvKey})=>sourceEnvKey!==targetEnvKey).map(({agent,sourceEnvKey,targetEnvKey})=>`${agent}\0${sourceEnvKey}\0${targetEnvKey}`));var MESSAGING_CREDENTIAL_RUNTIME_ENV_ALIASES=new Set(listMessagingCredentialEnvAssignments().filter(({sourceEnvKey,targetEnvKey})=>sourceEnvKey!==targetEnvKey).map(({agent,channelId,sourceEnvKey,targetEnvKey})=>`${agent}\0${channelId}\0${sourceEnvKey}\0${targetEnvKey}`));var JSON_ARRAY_INDEX_SEGMENT_RE=/^\[(?:0|[1-9][0-9]*)\]$/u;var SECRET_VALUE_PATTERNS=[/nvapi-[A-Za-z0-9_-]{10,}/u,/nvcf-[A-Za-z0-9_-]{10,}/u,/ghp_[A-Za-z0-9_-]{10,}/u,/github_pat_[A-Za-z0-9_]{30,}/u,/sk-(?:proj-|ant-)?[A-Za-z0-9_-]{10,}/u,/(?:xox[bpas]|xapp)-[A-Za-z0-9-]{10,}/u,/A(?:K|S)IA[A-Z0-9]{16}/u,/hf_[A-Za-z0-9]{10,}/u,/glpat-[A-Za-z0-9_-]{10,}/u,/gsk_[A-Za-z0-9]{10,}/u,/pypi-[A-Za-z0-9_-]{10,}/u,/tvly-[A-Za-z0-9_-]{10,}/u,/lsv2_(?:pt|sk)_[A-Za-z0-9]{10,}(?:_[A-Za-z0-9]+)*/u,/\bbot\d{8,10}:[A-Za-z0-9_-]{35}\b/u,/\b\d{8,10}:[A-Za-z0-9_-]{35}\b/u,/\b[A-Za-z0-9]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/u,/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{10,}\b/u,/\bBearer\s+[A-Za-z0-9_.+/=-]{10,}/iu,/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----/u];var MANAGED_STARTUP_INFERENCE_APIS=["openai-completions","openai-responses","anthropic-messages"];var MANAGED_STARTUP_REASONING_EFFORTS=["default","low","medium","high"];var MANAGED_STARTUP_DCODE_AUTO_APPROVAL_MODES=["disabled","thread-opt-in"];var MANAGED_STARTUP_HERMES_TOOL_GATEWAYS=["nous-web","nous-image","nous-audio","nous-browser","nous-code"];var MANAGED_STARTUP_AGENTS=["openclaw","hermes","langchain-deepagents-code","pi"];var MANAGED_STARTUP_MESSAGING_AGENTS=["openclaw","hermes"];function freezeAgentCapabilities(capabilities){return Object.freeze({...capabilities,inferenceApis:Object.freeze([...capabilities.inferenceApis]),dashboardModes:Object.freeze([...capabilities.dashboardModes]),inputModalities:Object.freeze([...capabilities.inputModalities]),webSearchProviders:Object.freeze([...capabilities.webSearchProviders]),toolGateways:Object.freeze([...capabilities.toolGateways]),tuningFields:Object.freeze([...capabilities.tuningFields])})}var PROFILE_CAPABILITIES={openclaw:{inferenceApis:[...MANAGED_STARTUP_INFERENCE_APIS],dashboardModes:["loopback","remote"],inputModalities:["text","image"],webSearchProviders:["brave","tavily"],toolGateways:[],tuningFields:["contextWindow","maxTokens","reasoning","reasoningEffort"],supportsMessaging:true,supportsInferenceCompatibility:true,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:true,supportsAgentTimeout:true,supportsHeartbeat:true,supportsExtraAgents:true,supportsDeviceAuth:false,observability:"openclaw-otel",supportsMinimalBootstrap:true},hermes:{inferenceApis:[...MANAGED_STARTUP_INFERENCE_APIS],dashboardModes:["disabled","loopback-forwarded"],inputModalities:[],webSearchProviders:["tavily"],toolGateways:[...MANAGED_STARTUP_HERMES_TOOL_GATEWAYS],tuningFields:["contextWindow"],supportsMessaging:true,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"none",supportsMinimalBootstrap:false},"langchain-deepagents-code":{inferenceApis:["openai-completions"],dashboardModes:["disabled"],inputModalities:[],webSearchProviders:[],toolGateways:[],tuningFields:["reasoningEffort"],supportsMessaging:false,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:true,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"dcode-marker",supportsMinimalBootstrap:false},pi:{inferenceApis:["openai-completions"],dashboardModes:["disabled"],inputModalities:[],webSearchProviders:[],toolGateways:[],tuningFields:["contextWindow","maxTokens","reasoning"],supportsMessaging:false,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"none",supportsMinimalBootstrap:false}};for(const agent of MANAGED_STARTUP_AGENTS){Object.defineProperty(PROFILE_CAPABILITIES,agent,{configurable:false,enumerable:true,value:freezeAgentCapabilities(PROFILE_CAPABILITIES[agent]),writable:false})}var MANAGED_STARTUP_PROFILE_CAPABILITIES=Object.freeze(PROFILE_CAPABILITIES);function affordance(input,profilePath,source="docker-arg",representation="value"){return{input,profilePath,source,representation}}var HOST_PROXY_AFFORDANCES=[affordance("HTTP_PROXY","proxy.hostHttpUrl","runtime-env"),affordance("http_proxy","proxy.hostHttpUrl","runtime-env","derived"),affordance("HTTPS_PROXY","proxy.hostHttpsUrl","runtime-env"),affordance("https_proxy","proxy.hostHttpsUrl","runtime-env","derived"),affordance("NO_PROXY","proxy.hostNoProxy","runtime-env"),affordance("no_proxy","proxy.hostNoProxy","runtime-env","derived")];var MANAGED_STARTUP_PROFILE_AFFORDANCE_INVENTORY={openclaw:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_SERVING_PRESET","inference.servingPreset"),affordance("NEMOCLAW_PRIMARY_MODEL_REF","inference.primaryModelRef"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_INFERENCE_COMPAT_B64","inference.compatibility"),affordance("NEMOCLAW_INFERENCE_INPUTS","inference.inputModalities"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_MAX_TOKENS","tuning.maxTokens"),affordance("NEMOCLAW_REASONING","tuning.reasoning"),affordance("NEMOCLAW_REASONING_EFFORT","tuning.reasoningEffort"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_AGENT_TIMEOUT","agentConfig.agentTimeoutSeconds"),affordance("NEMOCLAW_AGENT_HEARTBEAT_EVERY","agentConfig.heartbeatEvery"),affordance("NEMOCLAW_EXTRA_AGENTS_JSON_B64","agentConfig.extraAgents"),affordance("NEMOCLAW_WEB_SEARCH_ENABLED","agentConfig.webSearch.enabled"),affordance("NEMOCLAW_WEB_SEARCH_PROVIDER","agentConfig.webSearch.provider"),affordance("NEMOCLAW_OPENCLAW_OTEL","agentConfig.otel.enabled"),affordance("NEMOCLAW_OPENCLAW_OTEL_ENDPOINT","agentConfig.otel.endpointUrl"),affordance("NEMOCLAW_OPENCLAW_OTEL_SERVICE_NAME","agentConfig.otel.serviceName"),affordance("NEMOCLAW_OPENCLAW_OTEL_SAMPLE_RATE","agentConfig.otel.sampleRate"),affordance("CHAT_UI_URL","dashboard.url"),affordance("NEMOCLAW_DASHBOARD_BIND","dashboard.bindAddress"),affordance("NEMOCLAW_WSL_DASHBOARD_EXPOSURE","dashboard.wslExposure"),affordance("NEMOCLAW_DASHBOARD_PORT","dashboard.port","runtime-env"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_MESSAGING_PLAN_B64","messaging.plan"),affordance("NEMOCLAW_MINIMAL_BOOTSTRAP","agentConfig.minimalBootstrap","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],hermes:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER","tools.enabledGateways","docker-arg","derived"),affordance("NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64","tools.enabledGateways"),affordance("NEMOCLAW_WEB_SEARCH_ENABLED","agentConfig.webSearch.enabled"),affordance("NEMOCLAW_WEB_SEARCH_PROVIDER","agentConfig.webSearch.provider"),affordance("NEMOCLAW_MESSAGING_PLAN_B64","messaging.plan"),affordance("CHAT_UI_URL","dashboard.browserUrl"),affordance("NEMOCLAW_DASHBOARD_PORT","dashboard.publicPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD","dashboard.mode","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_PORT","dashboard.publicPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_INTERNAL_PORT","dashboard.internalPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_TUI","dashboard.tuiEnabled","runtime-env"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost","runtime-env"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],"langchain-deepagents-code":[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_UPSTREAM_ENDPOINT_URL","inference.upstreamEndpointUrl"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_REASONING_EFFORT","tuning.reasoningEffort"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_DCODE_AUTO_APPROVAL","agentConfig.autoApprovalMode"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_OBSERVABILITY","agentConfig.observabilityEnabled","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],pi:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_MAX_TOKENS","tuning.maxTokens"),affordance("NEMOCLAW_REASONING","tuning.reasoning"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES]};function deferredRuntimeInput(input,owner,reason,admission="managed-launch-forwarded"){return Object.freeze({input,owner,admission,reason})}var MANAGED_STARTUP_PROFILE_DEFERRED_RUNTIME_INPUTS=Object.freeze({openclaw:Object.freeze([deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_DEADLINE_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_MCP_SHADOW_DIAGNOSTICS","application-environment","operator shadow-diagnostics tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_MCP_TOOLS_LIST_TIMEOUT_MS","application-environment","operator MCP discovery timeout tuning is applied by the application environment transaction"),deferredRuntimeInput("OPENCLAW_HOME","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("OPENCLAW_STATE_DIR","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("OPENCLAW_WORKSPACE_DIR","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),hermes:Object.freeze([deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),"langchain-deepagents-code":Object.freeze([deferredRuntimeInput("NEMOCLAW_SANDBOX_NAME","engine-identity","the lifecycle engine owns instance identity outside reusable startup intent"),deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),pi:Object.freeze([deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")])});function runtimeCleanupObligation(input,emittedFor,supportedFor,reason){return Object.freeze({input,emittedFor:Object.freeze([...emittedFor]),supportedFor:Object.freeze([...supportedFor]),owner:"application-environment",reason})}var MANAGED_STARTUP_RUNTIME_CLEANUP_OBLIGATIONS=Object.freeze([runtimeCleanupObligation("NEMOCLAW_DASHBOARD_BIND",["hermes"],["openclaw"],"generic managed-dashboard construction currently emits the OpenClaw-only bind control for Hermes"),runtimeCleanupObligation("NEMOCLAW_MINIMAL_BOOTSTRAP",["hermes","langchain-deepagents-code"],["openclaw"],"generic host-proxy construction currently emits the OpenClaw-only bootstrap control for other agents")]);var ManagedStartupProfileError=class extends Error{constructor(message){super(`Invalid managed startup profile: ${message}`);this.name="ManagedStartupProfileError"}};var PROFILE_KEYS=new Set(["schemaVersion","agent","agentConfig","inference","proxy","dashboard","tools","messaging","tuning","corporateCa"]);var INFERENCE_KEYS=new Set(["routeProvider","upstreamProvider","servingPreset","model","routedBaseUrl","upstreamEndpointUrl","api","primaryModelRef","compatibility","inputModalities"]);var PROXY_KEYS=new Set(["managedHost","managedPort","hostHttpUrl","hostHttpsUrl","hostNoProxy"]);var OPENCLAW_DASHBOARD_KEYS=new Set(["agent","mode","url","port","bindAddress","wslExposure"]);var HERMES_DASHBOARD_KEYS=new Set(["agent","mode","url","browserUrl","publicPort","internalPort","tuiEnabled"]);var DCODE_DASHBOARD_KEYS=new Set(["agent","mode"]);var TOOLS_KEYS=new Set(["disclosure","enabledGateways"]);var MESSAGING_KEYS=new Set(["plan"]);var TUNING_FIELD_ORDER=["contextWindow","maxTokens","reasoning","reasoningEffort"];var TUNING_KEYS=new Set(TUNING_FIELD_ORDER);var CORPORATE_CA_KEYS=new Set(["bundleSha256"]);var OPENCLAW_CONFIG_KEYS=new Set(["agent","webSearch","otel","agentTimeoutSeconds","heartbeatEvery","extraAgents","minimalBootstrap"]);var LEGACY_OPENCLAW_CONFIG_KEYS=new Set([...OPENCLAW_CONFIG_KEYS,"deviceAuth"]);var LEGACY_DEVICE_AUTH_KEYS=new Set(["disabled","optOutSource"]);var LEGACY_DEVICE_AUTH_OPT_OUT_SOURCES=new Set(["operator","managed-onboard"]);var HERMES_CONFIG_KEYS=new Set(["agent","webSearch"]);var DCODE_CONFIG_KEYS=new Set(["agent","autoApprovalMode","observabilityEnabled"]);var PI_CONFIG_KEYS=new Set(["agent"]);var PI_DASHBOARD_KEYS=new Set(["agent","mode"]);var WEB_SEARCH_KEYS=new Set(["enabled","provider"]);var OTEL_KEYS=new Set(["enabled","endpointUrl","serviceName","sampleRate"]);var EXTRA_AGENTS_KEYS=new Set(["agents","defaults","main"]);var MANAGED_STARTUP_AGENT_SET=new Set(MANAGED_STARTUP_AGENTS);var DCODE_AUTO_APPROVAL_MODE_SET=new Set(MANAGED_STARTUP_DCODE_AUTO_APPROVAL_MODES);var REASONING_EFFORT_SET=new Set(MANAGED_STARTUP_REASONING_EFFORTS);var HERMES_INTERNAL_API_PORT=18642;var HERMES_API_PORT_RANGE_START=8642;var HERMES_API_PORT_RANGE_END=8652;function isHermesApiPort(port){return port>=HERMES_API_PORT_RANGE_START&&port<=HERMES_API_PORT_RANGE_END}function isHermesReservedApiPort(port){return port===HERMES_INTERNAL_API_PORT||isHermesApiPort(port)}var HERMES_RESERVED_API_PORT_LABEL=`${HERMES_API_PORT_RANGE_START}-${HERMES_API_PORT_RANGE_END} or ${HERMES_INTERNAL_API_PORT}`;function isPlainObject(value){if(typeof value!=="object"||value===null||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function isCredentialShapedName(name){if(PUBLIC_KEY_NAME_PATTERN.test(name)||NON_SECRET_KEY_METADATA_NAMES.has(name))return false;return CREDENTIAL_SHAPED_NAME_PATTERN.test(name)||CREDENTIAL_COMPOUND_NAME_PATTERN.test(name)||CREDENTIAL_CAMEL_SUFFIX_PATTERN.test(name)||CREDENTIAL_CAMEL_BOUNDARY_PATTERN.test(name)||CREDENTIAL_ENV_NAME_PATTERN.test(name)||CREDENTIAL_HEADER_NAME_PATTERN.test(name)||PASS_CREDENTIAL_NAME_PATTERN.test(name)}function valueLooksLikeSecret(value){for(let index=0;index=5&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="agentRender"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")&&path4[4]==="value";const isAuthorizedBuildStepPlaceholder=allowedBuildStepPlaceholders.has(buildStepPlaceholderKey(path4,value));return isCredentialBindingPlaceholder||isAgentRenderValuePlaceholder||isAuthorizedBuildStepPlaceholder}function requiresMessagingSchemaFieldAuthorization(path4){const fieldName=path4[path4.length-1];return fieldName==="webhook"}function messagingAuthorizedFieldKey(path4){return JSON.stringify(path4)}function buildStepPlaceholderKey(path4,value){return JSON.stringify([path4,value])}function messagingCredentialPlaceholderEnvKey(value){if(!MESSAGING_CREDENTIAL_PLACEHOLDER_RE.test(value))return null;const marker=value.startsWith("openshell:resolve:env:")?"openshell:resolve:env:":"-OPENSHELL-RESOLVE-ENV-";const key=value.slice(value.indexOf(marker)+marker.length);return key.replace(/^(?:v[0-9]{1,20}|s[a-f0-9]{64})_/u,"")}function containsMessagingCredentialPlaceholder(value){return value.includes("openshell:resolve:env:")||value.includes("-OPENSHELL-RESOLVE-ENV-")}function isMessagingCredentialPlaceholderAssignment(selectedAgent,path4,value){if(path4.length!==6||path4[0]!=="messaging"||path4[1]!=="plan"||path4[2]!=="agentRender"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")||path4[4]!=="lines"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[5]??"")){return false}const separator=value.indexOf("=");if(separator<=0||value.indexOf("=",separator+1)!==-1)return false;const envKey=value.slice(0,separator);const placeholder=value.slice(separator+1);const placeholderEnvKey=messagingCredentialPlaceholderEnvKey(placeholder);return CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&placeholderEnvKey!==null&&(envKey===placeholderEnvKey||typeof selectedAgent==="string"&&MESSAGING_CREDENTIAL_ENV_ALIASES.has(`${selectedAgent}\0${placeholderEnvKey}\0${envKey}`))}function isMessagingRuntimeEnvAliasPath(path4){return path4.length===5&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="runtimeSetup"&&path4[3]==="envAliases"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[4]??"")}function ownDataPropertyValue4(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function isCanonicalMessagingRuntimeEnvAlias(selectedAgent,path4,value){if(!isMessagingRuntimeEnvAliasPath(path4))return false;const channelId=ownDataPropertyValue4(value,"channelId");const envKey=ownDataPropertyValue4(value,"envKey");const targetEnvKey=ownDataPropertyValue4(value,"targetEnvKey");const match=ownDataPropertyValue4(value,"match");const placeholder=ownDataPropertyValue4(value,"value");const expectedMatch=targetEnvKey===void 0?`^openshell:resolve:env:((?:v[0-9]{1,20}|s[a-f0-9]{64})_)?${envKey}$`:`^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_${envKey}$`;return typeof envKey==="string"&&CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&match===expectedMatch&&typeof placeholder==="string"&&messagingCredentialPlaceholderEnvKey(placeholder)===envKey&&(targetEnvKey===void 0||typeof selectedAgent==="string"&&typeof channelId==="string"&&typeof targetEnvKey==="string"&&CREDENTIAL_ENV_NAME_PATTERN.test(targetEnvKey)&&MESSAGING_CREDENTIAL_RUNTIME_ENV_ALIASES.has(`${selectedAgent}\0${channelId}\0${envKey}\0${targetEnvKey}`))}function isAllowedMessagingRuntimeAliasStringPath(path4,allowedAliasIndexes){return path4.length===6&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="runtimeSetup"&&path4[3]==="envAliases"&&allowedAliasIndexes.has(path4[4]??"")&&(path4[5]==="match"||path4[5]==="value"||path4[5]==="targetEnvKey")}function isMessagingPackagePin(path4,value){return path4.length===6&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="buildSteps"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")&&path4[4]==="value"&&path4[5]==="pin"&&typeof value==="boolean"}function containsUrlWithCredentialMaterial(value){const candidates=value.match(URL_CANDIDATE_RE)??[];for(let index=0;index{if(isCredentialShapedName(key))credentialQuery=true});const fragment=url.hash.startsWith("#")?url.hash.slice(1):url.hash;const queryStart=fragment.indexOf("?");const fragmentParameters=new URLSearchParams(queryStart>=0?fragment.slice(queryStart+1):fragment);let credentialFragment=false;fragmentParameters.forEach((_fragmentValue,key)=>{if(isCredentialShapedName(key))credentialFragment=true});if(url.username||url.password||credentialQuery||credentialFragment)return true}catch{}}return false}function invalid(reason){throw new ManagedStartupProfileError(reason)}function payloadPath(path4){return path4.reduce((result,segment)=>segment.startsWith("[")?`${result}${segment}`:`${result}${result?".":""}${segment}`,"")}function mapArrayByIndex(values,mapper){const mapped=[];for(let index=0;index0&&values[insertion-1]>selected){Object.defineProperty(values,String(insertion),{configurable:true,enumerable:true,value:values[insertion-1],writable:true});insertion-=1}Object.defineProperty(values,String(insertion),{configurable:true,enumerable:true,value:selected,writable:true})}return values}function requireRecord(value,where){if(!isPlainObject(value))invalid(`${where} must be an object`);return value}function rejectUnknownKeys(value,allowed,where){const keys=Object.keys(value);for(let index=0;indexmaxBytes||CONTROL_CHARACTER_RE.test(value)){invalid(`${where} must be a bounded, non-empty string without control characters`)}return value}function requireStringEnum(value,allowed,where){const normalized=requireBoundedString(value,where);if(!allowed.has(normalized))invalid(`${where} is not supported`);return normalized}function requireNullablePositiveInteger(value,where){if(value===null)return null;if(typeof value!=="number"||!Number.isSafeInteger(value)||value<1||value>MAX_TUNING_INTEGER){invalid(`${where} must be null or a bounded positive integer`)}return value}function requirePositiveInteger(value,where,maximum=MAX_TUNING_INTEGER){if(typeof value!=="number"||!Number.isSafeInteger(value)||value<1||value>maximum){invalid(`${where} must be a bounded positive integer`)}return value}function requirePort(value,where,minimum=1){if(typeof value!=="number"||!Number.isInteger(value)||value<1||value>65535){invalid(`${where} must be a valid TCP port`)}if(valueMAX_LIST_ITEMS){invalid(`${where} must be a bounded string list`)}const items=mapArrayByIndex(value,item=>requireBoundedString(item,`${where} item`));const unique2=new Set;for(let index=0;index{if(depth>MAX_JSON_DEPTH)invalid(`${where} exceeds the JSON depth limit`);if(current===null||typeof current==="string"||typeof current==="boolean"){return current}if(typeof current==="number"){if(!Number.isFinite(current))invalid(`${where} contains a non-finite number`);return current}if(Array.isArray(current)){return mapArrayByIndex(current,item=>clone(item,depth+1))}if(!isPlainObject(current))invalid(`${where} contains a non-JSON value`);const result=options.nullPrototypeObjects?Object.create(null):{};const keys=Object.getOwnPropertyNames(current);for(let index=0;indexMAX_IDENTIFIER_BYTES||CONTROL_CHARACTER_RE.test(key)){invalid(`${where} contains an invalid object key`)}const descriptor=Object.getOwnPropertyDescriptor(current,key);if(!descriptor||!("value"in descriptor)){invalid(`${where} contains a non-JSON value`)}Object.defineProperty(result,key,{configurable:true,enumerable:true,value:clone(descriptor.value,depth+1),writable:true})}return result};return clone(value,0)}function requireJsonObjectOrNull(value,where){if(value===null)return null;if(!isPlainObject(value))invalid(`${where} must be null or a plain JSON object`);return cloneJsonValue(value,where,{nullPrototypeObjects:true})}function requireJsonObject(value,where){const object=requireJsonObjectOrNull(value,where);if(object===null)invalid(`${where} must be a plain JSON object`);return object}function requireHttpUrl(value,where){const raw=requireBoundedString(value,where,MAX_URL_BYTES);let parsed;try{parsed=new URL(raw)}catch{invalid(`${where} must be a valid HTTP(S) URL`)}if(parsed.protocol!=="http:"&&parsed.protocol!=="https:"||parsed.username||parsed.password||parsed.search||parsed.hash){invalid(`${where} must be a credential-free HTTP(S) URL without query or fragment data`)}const pathname=parsed.pathname.replace(/\/+$/u,"");return pathname===""?parsed.origin:`${parsed.origin}${pathname}`}function requireProxyUrl(value,allowedSchemes,where){if(value===null)return null;const raw=requireBoundedString(value,where,MAX_URL_BYTES);let parsed;try{parsed=new URL(raw)}catch{invalid(`${where} must be a valid HTTP(S) proxy URL`)}if(!allowedSchemes.has(parsed.protocol)||parsed.username||parsed.password||parsed.pathname!=="/"||parsed.search||parsed.hash){invalid(`${where} must be a credential-free HTTP(S) proxy origin`)}return parsed.origin}function requireManagedProxyHost(value,where){const host=requireBoundedString(value,where);if(!/^[A-Za-z0-9._-]+$/u.test(host)){invalid(`${where} must be a hostname or IPv4 address without a scheme or separators`)}return host}function configuredDashboardPort(value){const explicit=new URL(value).port;return explicit===""?18789:Number(explicit)}function requireSampleRate(value,where){if(typeof value!=="number"||!Number.isFinite(value)||value<0||value>1){invalid(`${where} must be a number between 0 and 1`)}return value}function assertPayloadStructureAndCredentialShapes(root){const pending=[{value:root,depth:0,path:[]}];const allowedRuntimeAliasIndexes=new Set;const allowedMessagingCredentialFields=new Set;const allowedBuildStepPlaceholders=new Set;const selectedAgent=isPlainObject(root)?ownDataPropertyValue4(root,"agent"):void 0;let discoveredNodes=1;let observedBytes=0;const observeText=value=>{observedBytes+=import_node_buffer.Buffer.byteLength(value,"utf8");if(observedBytes>MANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}};const reserveNode=depth=>{discoveredNodes+=1;if(discoveredNodes>MAX_JSON_NODES||depth>MAX_JSON_DEPTH){invalid("payload structure exceeds the complexity limit")}observedBytes+=1};while(pending.length>0){const current=pending.pop();if(!current)break;if(current.depth>MAX_JSON_DEPTH){invalid("payload structure exceeds the complexity limit")}if(typeof current.value==="string"){observeText(current.value);if(!isAllowedMessagingRuntimeAliasStringPath(current.path,allowedRuntimeAliasIndexes)&&!isMessagingCredentialPlaceholder(current.path,current.value,allowedBuildStepPlaceholders,allowedMessagingCredentialFields)&&!isMessagingCredentialPlaceholderAssignment(selectedAgent,current.path,current.value)&&(valueLooksLikeSecret(current.value)||containsMessagingCredentialPlaceholder(current.value))){invalid(`payload field ${payloadPath(current.path)} contains credential-shaped string data`)}if(RAW_CA_PEM_RE.test(current.value)||RAW_CA_PEM_BASE64_RE.test(current.value)||RAW_CA_DER_BASE64_RE.test(current.value)||RAW_CA_DATA_URI_RE.test(current.value)){invalid(`payload field ${payloadPath(current.path)} contains raw certificate data; provide only the CA SHA-256 digest`)}if(containsUrlWithCredentialMaterial(current.value)){invalid(`payload field ${payloadPath(current.path)} contains a URL with embedded credentials`)}continue}if(Array.isArray(current.value)){if(Object.getPrototypeOf(current.value)!==Array.prototype){invalid("payload arrays must use the standard JSON prototype")}if("toJSON"in current.value){invalid("payload must not define a custom JSON serializer")}if(Object.getOwnPropertySymbols(current.value).length>0||Object.getOwnPropertyNames(current.value).length!==current.value.length+1){invalid("payload arrays must contain only indexed JSON values")}for(let index=0;index0||discoveredNodes+keys.length>MAX_JSON_NODES){invalid("payload structure exceeds the complexity limit")}for(let index=0;indexMANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}}function validateWebSearch(value,agent){const webSearch=requireRecord(value,"agentConfig.webSearch");rejectUnknownKeys(webSearch,WEB_SEARCH_KEYS,"agentConfig.webSearch");const provider=requireStringEnum(webSearch.provider,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].webSearchProviders),"agentConfig.webSearch.provider");return{enabled:requireBoolean(webSearch.enabled,"agentConfig.webSearch.enabled"),provider}}function validateOpenClawOtel(value){const otel=requireRecord(value,"agentConfig.otel");rejectUnknownKeys(otel,OTEL_KEYS,"agentConfig.otel");return{enabled:requireBoolean(otel.enabled,"agentConfig.otel.enabled"),endpointUrl:requireHttpUrl(otel.endpointUrl,"agentConfig.otel.endpointUrl"),serviceName:requireBoundedString(otel.serviceName,"agentConfig.otel.serviceName",MAX_IDENTIFIER_BYTES),sampleRate:requireSampleRate(otel.sampleRate,"agentConfig.otel.sampleRate")}}function validateExtraAgents(value){const extraAgents=requireRecord(value,"agentConfig.extraAgents");rejectUnknownKeys(extraAgents,EXTRA_AGENTS_KEYS,"agentConfig.extraAgents");if(!Array.isArray(extraAgents.agents)||extraAgents.agents.length>MAX_LIST_ITEMS){invalid("agentConfig.extraAgents.agents must be a bounded JSON object list")}return{agents:mapArrayByIndex(extraAgents.agents,(agent,index)=>requireJsonObject(agent,`agentConfig.extraAgents.agents[${String(index)}]`)),defaults:requireJsonObject(extraAgents.defaults,"agentConfig.extraAgents.defaults"),main:requireJsonObject(extraAgents.main,"agentConfig.extraAgents.main")}}function validateAgentConfig(value,expectedAgent){const config=requireRecord(value,"agentConfig");const agent=requireStringEnum(config.agent,MANAGED_STARTUP_AGENT_SET,"agentConfig.agent");if(agent!==expectedAgent)invalid("agentConfig.agent must match agent");if(agent==="openclaw"){rejectUnknownKeys(config,OPENCLAW_CONFIG_KEYS,"agentConfig");const heartbeatEvery=config.heartbeatEvery===null?null:requireBoundedString(config.heartbeatEvery,"agentConfig.heartbeatEvery",MAX_IDENTIFIER_BYTES);if(heartbeatEvery!==null&&!/^\d+(?:s|m|h)$/u.test(heartbeatEvery)){invalid("agentConfig.heartbeatEvery must be null or a duration ending in s, m, or h")}return{agent,webSearch:validateWebSearch(config.webSearch,agent),otel:validateOpenClawOtel(config.otel),agentTimeoutSeconds:requirePositiveInteger(config.agentTimeoutSeconds,"agentConfig.agentTimeoutSeconds"),heartbeatEvery,extraAgents:validateExtraAgents(config.extraAgents),minimalBootstrap:requireBoolean(config.minimalBootstrap,"agentConfig.minimalBootstrap")}}if(agent==="hermes"){rejectUnknownKeys(config,HERMES_CONFIG_KEYS,"agentConfig");return{agent,webSearch:validateWebSearch(config.webSearch,agent)}}if(agent==="pi"){rejectUnknownKeys(config,PI_CONFIG_KEYS,"agentConfig");return{agent}}rejectUnknownKeys(config,DCODE_CONFIG_KEYS,"agentConfig");return{agent,autoApprovalMode:requireStringEnum(config.autoApprovalMode,DCODE_AUTO_APPROVAL_MODE_SET,"agentConfig.autoApprovalMode"),observabilityEnabled:requireBoolean(config.observabilityEnabled,"agentConfig.observabilityEnabled")}}function validateDashboard(value,expectedAgent){const dashboard=requireRecord(value,"dashboard");const agent=requireStringEnum(dashboard.agent,MANAGED_STARTUP_AGENT_SET,"dashboard.agent");if(agent!==expectedAgent)invalid("dashboard.agent must match agent");if(agent==="openclaw"){rejectUnknownKeys(dashboard,OPENCLAW_DASHBOARD_KEYS,"dashboard");const mode=requireStringEnum(dashboard.mode,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].dashboardModes),"dashboard.mode");const url=requireHttpUrl(dashboard.url,"dashboard.url");const bindAddress=requireStringEnum(dashboard.bindAddress,new Set(["127.0.0.1","0.0.0.0"]),"dashboard.bindAddress");const wslExposure=requireBoolean(dashboard.wslExposure,"dashboard.wslExposure");const hasRemoteExposure=!isLoopbackDashboardUrl(url)||bindAddress==="0.0.0.0"||wslExposure;if(mode==="remote"!==hasRemoteExposure){invalid("OpenClaw dashboard.mode must reflect its URL, bind address, and WSL exposure")}const port=requirePort(dashboard.port,"dashboard.port",1024);if(isHermesApiPort(port))invalid(`OpenClaw dashboard.port must not use a reserved Hermes API port (${HERMES_API_PORT_RANGE_START}-${HERMES_API_PORT_RANGE_END})`);if(configuredDashboardPort(url)!==port){invalid("OpenClaw dashboard.port must match dashboard.url")}return{agent,mode,url,port,bindAddress,wslExposure}}if(agent==="hermes"){rejectUnknownKeys(dashboard,HERMES_DASHBOARD_KEYS,"dashboard");const mode=requireStringEnum(dashboard.mode,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].dashboardModes),"dashboard.mode");const url=requireHttpUrl(dashboard.url,"dashboard.url");if(!isLoopbackDashboardUrl(url)){invalid("Hermes dashboard.url must remain loopback; OpenShell owns the host forward")}const browserUrl=dashboard.browserUrl===void 0?void 0:requireHttpUrl(dashboard.browserUrl,"dashboard.browserUrl");if(browserUrl!==void 0&&!isLoopbackDashboardUrl(browserUrl)&&new URL(browserUrl).protocol!=="https:"){invalid("Hermes dashboard.browserUrl must use HTTPS unless it is loopback")}if(mode==="disabled"){if(dashboard.publicPort!==null||dashboard.internalPort!==null||dashboard.tuiEnabled!==false){invalid("disabled Hermes dashboard must not configure ports or TUI")}return{agent,mode,url,...browserUrl===void 0?{}:{browserUrl},publicPort:null,internalPort:null,tuiEnabled:false}}const publicPort=requirePort(dashboard.publicPort,"dashboard.publicPort",1024);const internalPort=requirePort(dashboard.internalPort,"dashboard.internalPort",1024);if(publicPort===internalPort){invalid("Hermes dashboard publicPort and internalPort must differ")}if(isHermesReservedApiPort(publicPort)||isHermesReservedApiPort(internalPort)){invalid(`Hermes dashboard ports must not use reserved API ports ${HERMES_RESERVED_API_PORT_LABEL}`)}if(configuredDashboardPort(url)!==publicPort){invalid("Hermes dashboard.publicPort must match dashboard.url")}if(browserUrl!==void 0&&isLoopbackDashboardUrl(browserUrl)&&configuredDashboardPort(browserUrl)!==publicPort){invalid("Hermes dashboard.publicPort must match dashboard.browserUrl")}return{agent,mode,url,...browserUrl===void 0?{}:{browserUrl},publicPort,internalPort,tuiEnabled:requireBoolean(dashboard.tuiEnabled,"dashboard.tuiEnabled")}}if(agent==="pi"){rejectUnknownKeys(dashboard,PI_DASHBOARD_KEYS,"dashboard");if(dashboard.mode!=="disabled")invalid("pi dashboard.mode must be disabled");return{agent,mode:"disabled"}}rejectUnknownKeys(dashboard,DCODE_DASHBOARD_KEYS,"dashboard");if(dashboard.mode!=="disabled"){invalid("langchain-deepagents-code dashboard.mode must be disabled")}return{agent,mode:"disabled"}}function validateInference(value,agent){if(value===null){if(agent!=="openclaw"&&agent!=="hermes")invalid(`${agent} requires inference configuration`);return null}const inference=requireRecord(value,"inference");rejectUnknownKeys(inference,INFERENCE_KEYS,"inference");const routeProvider=requireBoundedString(inference.routeProvider,"inference.routeProvider");const upstreamProvider=requireBoundedString(inference.upstreamProvider,"inference.upstreamProvider");const servingPreset=inference.servingPreset===void 0?void 0:inference.servingPreset===null?null:requireBoundedString(inference.servingPreset,"inference.servingPreset");const model=requireBoundedString(inference.model,"inference.model",MAX_MODEL_BYTES);const api=requireStringEnum(inference.api,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].inferenceApis),"inference.api");const upstreamEndpointUrl=inference.upstreamEndpointUrl===null?null:requireHttpUrl(inference.upstreamEndpointUrl,"inference.upstreamEndpointUrl");const primaryModelRef=inference.primaryModelRef===null?null:requireBoundedString(inference.primaryModelRef,"inference.primaryModelRef",MAX_MODEL_BYTES);const compatibility=requireJsonObjectOrNull(inference.compatibility,"inference.compatibility");const inputModalities=inference.inputModalities===null?null:requireEnumList(inference.inputModalities,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].inputModalities),"inference.inputModalities",{allowEmpty:false});if(upstreamEndpointUrl!==null&&!MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].supportsUpstreamEndpoint){invalid(`inference.upstreamEndpointUrl must be null for ${agent}`)}if(agent==="openclaw"){if(primaryModelRef===null||inputModalities===null){invalid("openclaw requires primaryModelRef and inputModalities")}if(primaryModelRef!==`${routeProvider}/${model}`){invalid("openclaw primaryModelRef must match routeProvider and model")}}else{if(primaryModelRef!==null||compatibility!==null||inputModalities!==null){invalid(`${agent} does not support primaryModelRef, compatibility, or inputModalities`)}if(agent==="langchain-deepagents-code"&&!isValidDcodeUpstreamProvider(upstreamProvider)){invalid("inference.upstreamProvider must start with an ASCII letter or digit and contain 1-64 ASCII letters, digits, dots, underscores, or hyphens for DCode")}}return{routeProvider,upstreamProvider,...servingPreset===void 0?{}:{servingPreset},model,routedBaseUrl:requireHttpUrl(inference.routedBaseUrl,"inference.routedBaseUrl"),upstreamEndpointUrl,api,primaryModelRef,compatibility,inputModalities}}function validateProxy(value,agent){const proxy=requireRecord(value,"proxy");rejectUnknownKeys(proxy,PROXY_KEYS,"proxy");const hostHttpUrl=requireProxyUrl(proxy.hostHttpUrl,new Set(["http:"]),"proxy.hostHttpUrl");const hostHttpsUrl=requireProxyUrl(proxy.hostHttpsUrl,new Set(["http:","https:"]),"proxy.hostHttpsUrl");const hostNoProxy=requireStringList(proxy.hostNoProxy,"proxy.hostNoProxy");if(!MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].supportsHostProxyIntent&&(hostHttpUrl!==null||hostHttpsUrl!==null||hostNoProxy.length>0)){invalid(`${agent} rejects host proxy intent and accepts only its root-owned managed route`)}return{managedHost:requireManagedProxyHost(proxy.managedHost,"proxy.managedHost"),managedPort:requirePort(proxy.managedPort,"proxy.managedPort"),hostHttpUrl,hostHttpsUrl,hostNoProxy}}function validateTools(value,agent){const tools=requireRecord(value,"tools");rejectUnknownKeys(tools,TOOLS_KEYS,"tools");const enabledGateways=requireEnumList(tools.enabledGateways,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].toolGateways),"tools.enabledGateways",{allowEmpty:true});return{disclosure:requireStringEnum(tools.disclosure,new Set(["progressive","direct"]),"tools.disclosure"),enabledGateways}}function validateTuning(value,agent){const tuning=requireRecord(value,"tuning");rejectUnknownKeys(tuning,TUNING_KEYS,"tuning");const result={contextWindow:requireNullablePositiveInteger(tuning.contextWindow,"tuning.contextWindow"),maxTokens:requireNullablePositiveInteger(tuning.maxTokens,"tuning.maxTokens"),reasoning:requireNullableBoolean(tuning.reasoning,"tuning.reasoning"),reasoningEffort:tuning.reasoningEffort===null?null:requireStringEnum(tuning.reasoningEffort,REASONING_EFFORT_SET,"tuning.reasoningEffort")};const advertised=new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].tuningFields);const unsupported=TUNING_FIELD_ORDER.filter(field=>result[field]!==null&&!advertised.has(field));if(unsupported.length>0){invalid(`${agent} does not support startup tuning fields: ${unsupported.join(", ")}`)}if(agent==="openclaw"){const missing=TUNING_FIELD_ORDER.filter(field=>advertised.has(field)&&result[field]===null);if(missing.length>0){invalid(`openclaw requires ${missing.join(", ")} tuning`)}}if(agent==="hermes"&&result.contextWindow!==null&&result.contextWindowcanonicalizeJson(item));if(!isPlainObject(value))return value;const result={};const keys=sortStrings(Object.keys(value));for(let index=0;indexMANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`canonical payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}return serialized}function decodeManagedStartupProfile(encoded){if(typeof encoded!=="string"||encoded.length===0||import_node_buffer.Buffer.byteLength(encoded,"ascii")>MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES||!BASE64URL_RE.test(encoded)||encoded.length%4===1){invalid("encoded payload is malformed or exceeds the size limit")}const bytes=import_node_buffer.Buffer.from(encoded,"base64url");if(bytes.length===0||bytes.length>MANAGED_STARTUP_PROFILE_MAX_BYTES||bytes.toString("base64url")!==encoded){invalid("encoded payload is malformed or exceeds the size limit")}let raw;try{raw=UTF8_DECODER.decode(bytes)}catch{invalid("payload is not valid UTF-8")}let parsed;try{parsed=JSON.parse(raw)}catch{invalid("payload is not valid JSON")}const migration=migrateDecodedManagedStartupProfile(parsed);const profile=validateManagedStartupProfile(migration.value);const canonicalPayload=migration.migratedLegacyProfile?JSON.stringify(canonicalizeJson(parsed)):serializeManagedStartupProfile(profile);if(canonicalPayload!==raw){invalid("payload is not in canonical form")}return profile}function fingerprintManagedStartupProfile(profile){return(0,import_node_crypto3.createHash)("sha256").update(serializeManagedStartupProfile(profile),"utf8").digest("hex")}var ManagedStartupAgentEnvironmentError=class extends Error{constructor(message){super(`Cannot map managed startup profile: ${message}`);this.name="ManagedStartupAgentEnvironmentError"}};var EMPTY_APPLICATION_ENVIRONMENT=Object.freeze({});var OPENCLAW_APPLICATION_RUNTIME_INPUTS=Object.freeze([["NEMOCLAW_AUTO_PAIR_DEADLINE_SECS","positive-finite-seconds"],["NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS","positive-finite-seconds"],["NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS","positive-finite-seconds"],["NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS","positive-safe-integer"],["NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS","positive-finite-seconds"],["NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS","positive-finite-seconds"]]);function booleanFlag(value){return value?"1":"0"}function canonicalizeJson2(value){if(Array.isArray(value))return value.map(item=>canonicalizeJson2(item));if(value===null||typeof value!=="object")return value;const record=value;return Object.fromEntries(Object.keys(record).sort().map(key=>[key,canonicalizeJson2(record[key])]))}function encodeCanonicalJson(value){return import_node_buffer2.Buffer.from(JSON.stringify(canonicalizeJson2(value)),"utf8").toString("base64")}function sortedEnvironment(environment){return Object.freeze(Object.fromEntries(Object.entries(environment).sort(([left],[right])=>leftright?1:0)))}function canonicalApplicationRuntimeValue(name,raw,kind){if(raw.includes("\0")||/[\r\n]/u.test(raw)){throw new ManagedStartupAgentEnvironmentError(`${name} must be single-line text`)}const value=Number(raw.trim());const valid=kind==="positive-safe-integer"?Number.isSafeInteger(value)&&value>0:Number.isFinite(value)&&value>0;if(!valid){throw new ManagedStartupAgentEnvironmentError(`${name} must be ${kind==="positive-safe-integer"?"a positive safe integer":"finite positive seconds"}`)}return String(value)}function applicationRuntimePlan(profile,environment){const exportEnvironment={};if(profile.agent==="openclaw"){for(const[name,kind]of OPENCLAW_APPLICATION_RUNTIME_INPUTS){const raw=environment[name];if(raw!==void 0){exportEnvironment[name]=canonicalApplicationRuntimeValue(name,raw,kind)}}}const unsetEnvironment=new Set(MANAGED_STARTUP_RUNTIME_CLEANUP_OBLIGATIONS.filter(({supportedFor})=>!supportedFor.includes(profile.agent)).map(({input})=>input));if(profile.agent!=="openclaw"){for(const[name]of OPENCLAW_APPLICATION_RUNTIME_INPUTS){unsetEnvironment.add(name)}}return Object.freeze({exportEnvironment:sortedEnvironment(exportEnvironment),unsetEnvironment:Object.freeze([...unsetEnvironment].sort())})}function commonConfigurationEnvironment(profile){if(profile.inference===null)return{...PROVIDERLESS_INFERENCE_ENV,NEMOCLAW_TOOL_DISCLOSURE:profile.tools.disclosure};return{NEMOCLAW_INFERENCE_API:profile.inference.api,NEMOCLAW_INFERENCE_BASE_URL:profile.inference.routedBaseUrl,NEMOCLAW_INFERENCE_PROVIDER_ID:profile.inference.routeProvider,NEMOCLAW_MODEL:profile.inference.model,NEMOCLAW_TOOL_DISCLOSURE:profile.tools.disclosure,NEMOCLAW_UPSTREAM_PROVIDER:profile.inference.upstreamProvider}}function appendHostProxyEnvironment(environment,profile,options={}){if(options.preserveAmbientWhenAbsent===true&&profile.proxy.hostHttpUrl===null&&profile.proxy.hostHttpsUrl===null&&profile.proxy.hostNoProxy.length===0){return}const httpProxy=profile.proxy.hostHttpUrl??"";const httpsProxy=profile.proxy.hostHttpsUrl??"";const noProxy=profile.proxy.hostNoProxy.join(",");environment.HTTP_PROXY=httpProxy;environment.HTTPS_PROXY=httpsProxy;environment.NO_PROXY=noProxy;environment.http_proxy=httpProxy;environment.https_proxy=httpsProxy;environment.no_proxy=noProxy}function messagingEnvironment(profile,expectedAgent){if(profile.messaging.plan===null)return{};const plan=parseSandboxMessagingPlan(profile.messaging.plan,{agent:expectedAgent});if(!plan){throw new ManagedStartupAgentEnvironmentError(`messaging.plan must contain a validated ${expectedAgent} messaging plan`)}const{workflow:_workflow,...imageBuildPlan}=plan;return{NEMOCLAW_MESSAGING_PLAN_B64:encodeCanonicalJson(imageBuildPlan)}}function corporateCaMaterial(profile){return Object.freeze({kind:"corporate-ca-handoff",legacyInput:"NEMOCLAW_CORPORATE_CA_B64",expectedSha256:profile.corporateCa.bundleSha256})}function rootOwnedFile(legacyInput,path4,value){return Object.freeze({kind:"root-owned-file",legacyInput,path:path4,contents:`${value} `,owner:"root",group:"root",mode:292})}function dashboardAction(dashboard){return Object.freeze({kind:"configure-dashboard",dashboard:Object.freeze(structuredClone(dashboard))})}function applicationActions(profile,messagingAgent){const actions=[];if(messagingAgent!==null){actions.push(Object.freeze({kind:"apply-messaging-plan",agent:messagingAgent,mode:profile.messaging.plan===null?"clear":"apply",phase:"runtime-setup",runAs:"root"}))}actions.push(Object.freeze({kind:"generate-agent-config",agent:profile.agent,runAs:"sandbox"}));if(messagingAgent!==null){actions.push(Object.freeze({kind:"apply-messaging-plan",agent:messagingAgent,mode:profile.messaging.plan===null?"clear":"apply",phase:"post-agent-install",runAs:"sandbox"}))}actions.push(dashboardAction(profile.dashboard));return Object.freeze(actions)}function mapOpenClawProfile(profile,environment){if(profile.agent!=="openclaw"||profile.agentConfig.agent!=="openclaw"||profile.dashboard.agent!=="openclaw"||profile.inference!==null&&(profile.inference.primaryModelRef===null||profile.inference.inputModalities===null)||profile.tuning.contextWindow===null||profile.tuning.maxTokens===null||profile.tuning.reasoning===null||profile.tuning.reasoningEffort===null){throw new ManagedStartupAgentEnvironmentError("OpenClaw profile state is inconsistent")}const configurationEnvironment={...commonConfigurationEnvironment(profile),...messagingEnvironment(profile,"openclaw"),CHAT_UI_URL:profile.dashboard.url,NEMOCLAW_AGENT_HEARTBEAT_EVERY:profile.agentConfig.heartbeatEvery??"",NEMOCLAW_AGENT_TIMEOUT:String(profile.agentConfig.agentTimeoutSeconds),NEMOCLAW_CONTEXT_WINDOW:String(profile.tuning.contextWindow),NEMOCLAW_DASHBOARD_BIND:profile.dashboard.bindAddress==="0.0.0.0"?profile.dashboard.bindAddress:"",NEMOCLAW_EXTRA_AGENTS_JSON_B64:encodeCanonicalJson(profile.agentConfig.extraAgents),NEMOCLAW_INFERENCE_COMPAT_B64:encodeCanonicalJson(profile.inference?.compatibility??{}),NEMOCLAW_INFERENCE_INPUTS:profile.inference?.inputModalities?.join(",")??"text",NEMOCLAW_MAX_TOKENS:String(profile.tuning.maxTokens),NEMOCLAW_OPENCLAW_OTEL:booleanFlag(profile.agentConfig.otel.enabled),NEMOCLAW_OPENCLAW_OTEL_ENDPOINT:profile.agentConfig.otel.endpointUrl,NEMOCLAW_OPENCLAW_OTEL_SAMPLE_RATE:String(profile.agentConfig.otel.sampleRate),NEMOCLAW_OPENCLAW_OTEL_SERVICE_NAME:profile.agentConfig.otel.serviceName,NEMOCLAW_PRIMARY_MODEL_REF:profile.inference?.primaryModelRef??"",NEMOCLAW_PROXY_HOST:profile.proxy.managedHost,NEMOCLAW_PROXY_PORT:String(profile.proxy.managedPort),NEMOCLAW_REASONING:String(profile.tuning.reasoning),NEMOCLAW_REASONING_EFFORT:profile.tuning.reasoningEffort,NEMOCLAW_SERVING_PRESET:profile.inference?.servingPreset??"",NEMOCLAW_WEB_SEARCH_ENABLED:booleanFlag(profile.agentConfig.webSearch.enabled),NEMOCLAW_WEB_SEARCH_PROVIDER:profile.agentConfig.webSearch.provider,NEMOCLAW_WSL_DASHBOARD_EXPOSURE:booleanFlag(profile.dashboard.wslExposure)};const runtimeEnvironment={...configurationEnvironment};delete runtimeEnvironment.NEMOCLAW_MESSAGING_PLAN_B64;runtimeEnvironment.NEMOCLAW_DASHBOARD_PORT=String(profile.dashboard.port);runtimeEnvironment.NEMOCLAW_MINIMAL_BOOTSTRAP=booleanFlag(profile.agentConfig.minimalBootstrap);appendHostProxyEnvironment(runtimeEnvironment,profile,{preserveAmbientWhenAbsent:true});return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials:Object.freeze([corporateCaMaterial(profile)]),actions:applicationActions(profile,"openclaw")})}function mapHermesProfile(profile,environment){if(profile.agent!=="hermes"||profile.agentConfig.agent!=="hermes"||profile.dashboard.agent!=="hermes"){throw new ManagedStartupAgentEnvironmentError("Hermes profile state is inconsistent")}let chatUiUrl=profile.dashboard.browserUrl??profile.dashboard.url;if(profile.dashboard.mode==="loopback-forwarded"){if(profile.dashboard.browserUrl===void 0){throw new ManagedStartupAgentEnvironmentError("Cannot start the Hermes dashboard because its managed startup profile has no recorded browser URL. Rerun onboarding before starting the sandbox.")}chatUiUrl=profile.dashboard.browserUrl}const configurationEnvironment={...commonConfigurationEnvironment(profile),...messagingEnvironment(profile,"hermes"),CHAT_UI_URL:chatUiUrl,NEMOCLAW_CONTEXT_WINDOW:profile.tuning.contextWindow===null?"":String(profile.tuning.contextWindow),NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER:booleanFlag(profile.tools.enabledGateways.length>0),NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64:encodeCanonicalJson(profile.tools.enabledGateways),NEMOCLAW_WEB_SEARCH_ENABLED:booleanFlag(profile.agentConfig.webSearch.enabled),NEMOCLAW_WEB_SEARCH_PROVIDER:profile.agentConfig.webSearch.provider};const runtimeEnvironment={...configurationEnvironment,HERMES_HOME:"/sandbox/.hermes",HERMES_LAZY_INSTALL_TARGET:"/sandbox/.hermes/lazy-packages"};delete runtimeEnvironment.NEMOCLAW_MESSAGING_PLAN_B64;runtimeEnvironment.NEMOCLAW_DASHBOARD_PORT=profile.dashboard.publicPort===null?"":String(profile.dashboard.publicPort);runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD=profile.dashboard.mode==="loopback-forwarded"?"1":"0";runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD_INTERNAL_PORT=profile.dashboard.internalPort===null?"":String(profile.dashboard.internalPort);runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD_PORT=profile.dashboard.publicPort===null?"":String(profile.dashboard.publicPort);runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD_TUI=booleanFlag(profile.dashboard.tuiEnabled);runtimeEnvironment.NEMOCLAW_PROXY_HOST=profile.proxy.managedHost;runtimeEnvironment.NEMOCLAW_PROXY_PORT=String(profile.proxy.managedPort);appendHostProxyEnvironment(runtimeEnvironment,profile,{preserveAmbientWhenAbsent:true});return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials:Object.freeze([corporateCaMaterial(profile)]),actions:applicationActions(profile,"hermes")})}function mapDcodeProfile(profile,environment){if(profile.agent!=="langchain-deepagents-code"||profile.agentConfig.agent!=="langchain-deepagents-code"||profile.dashboard.agent!=="langchain-deepagents-code"||profile.messaging.plan!==null||profile.inference===null){throw new ManagedStartupAgentEnvironmentError("LangChain Deep Agents Code profile state is inconsistent")}const reasoningEffort=profile.tuning.reasoningEffort===null||profile.tuning.reasoningEffort==="default"?"":profile.tuning.reasoningEffort;const configurationEnvironment={...commonConfigurationEnvironment(profile),NEMOCLAW_REASONING_EFFORT:reasoningEffort,NEMOCLAW_UPSTREAM_ENDPOINT_URL:profile.inference.upstreamEndpointUrl??""};appendHostProxyEnvironment(configurationEnvironment,profile);const runtimeEnvironment={...configurationEnvironment,NEMOCLAW_OBSERVABILITY:booleanFlag(profile.agentConfig.observabilityEnabled)};delete runtimeEnvironment.NEMOCLAW_INFERENCE_BASE_URL;delete runtimeEnvironment.NEMOCLAW_REASONING_EFFORT;delete runtimeEnvironment.NEMOCLAW_UPSTREAM_PROVIDER;for(const name of["HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy"]){delete runtimeEnvironment[name]}const materials=Object.freeze([corporateCaMaterial(profile),rootOwnedFile("NEMOCLAW_DCODE_AUTO_APPROVAL","/usr/local/share/nemoclaw/dcode-auto-approval",profile.agentConfig.autoApprovalMode),rootOwnedFile("NEMOCLAW_INFERENCE_BASE_URL","/usr/local/share/nemoclaw/dcode-inference-base-url",profile.inference.routedBaseUrl),rootOwnedFile("NEMOCLAW_UPSTREAM_PROVIDER","/usr/local/share/nemoclaw/dcode-upstream-provider",profile.inference.upstreamProvider),rootOwnedFile("NEMOCLAW_PROXY_HOST","/usr/local/share/nemoclaw/dcode-proxy-host",profile.proxy.managedHost),rootOwnedFile("NEMOCLAW_PROXY_PORT","/usr/local/share/nemoclaw/dcode-proxy-port",String(profile.proxy.managedPort)),rootOwnedFile("NEMOCLAW_REASONING_EFFORT","/usr/local/share/nemoclaw/dcode-reasoning-effort",reasoningEffort)]);return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials,actions:applicationActions(profile,null)})}function mapPiProfile(profile,environment){if(profile.agent!=="pi"||profile.agentConfig.agent!=="pi"||profile.dashboard.agent!=="pi"||profile.messaging.plan!==null){throw new ManagedStartupAgentEnvironmentError("Pi profile state is inconsistent")}const configurationEnvironment={...commonConfigurationEnvironment(profile),NEMOCLAW_CONTEXT_WINDOW:profile.tuning.contextWindow===null?"":String(profile.tuning.contextWindow),NEMOCLAW_MAX_TOKENS:profile.tuning.maxTokens===null?"":String(profile.tuning.maxTokens),NEMOCLAW_REASONING:profile.tuning.reasoning===null?"":String(profile.tuning.reasoning)};appendHostProxyEnvironment(configurationEnvironment,profile);const runtimeEnvironment={...configurationEnvironment};delete runtimeEnvironment.NEMOCLAW_INFERENCE_BASE_URL;delete runtimeEnvironment.NEMOCLAW_CONTEXT_WINDOW;delete runtimeEnvironment.NEMOCLAW_MAX_TOKENS;delete runtimeEnvironment.NEMOCLAW_REASONING;for(const name of["HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy"]){delete runtimeEnvironment[name]}const materials=Object.freeze([corporateCaMaterial(profile),rootOwnedFile("NEMOCLAW_PROXY_HOST","/usr/local/share/nemoclaw/pi-proxy-host",profile.proxy.managedHost),rootOwnedFile("NEMOCLAW_PROXY_PORT","/usr/local/share/nemoclaw/pi-proxy-port",String(profile.proxy.managedPort))]);return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials,actions:applicationActions(profile,null)})}function mapManagedStartupProfileToAgentEnvironment(profile,environment=EMPTY_APPLICATION_ENVIRONMENT){const validated=validateManagedStartupProfile(profile);switch(validated.agent){case"openclaw":return mapOpenClawProfile(validated,environment);case"hermes":return mapHermesProfile(validated,environment);case"langchain-deepagents-code":return mapDcodeProfile(validated,environment);case"pi":return mapPiProfile(validated,environment)}}var import_node_buffer3=require("node:buffer");var import_node_crypto4=require("node:crypto");var import_node_fs=__toESM(require("node:fs"));var import_node_path=__toESM(require("node:path"));var import_node_util2=require("node:util");var MANAGED_STARTUP_APPLICATION_STATE_DIR="/var/lib/nemoclaw/startup-profile";var MANAGED_STARTUP_CA_MAX_BYTES=128*1024;var MANAGED_STARTUP_CA_MAX_CERTIFICATES=24;var STATE_SCHEMA_VERSION=1;var STATE_DIRECTORY_MODE=448;var STATE_FILE_MODE=384;var MAX_CONTROL_FILE_BYTES=512;var MAX_STATE_ENTRIES=32;var SHA256_RE2=/^[a-f0-9]{64}$/u;var GENERATION_RE=/^generation-([a-f0-9]{64})$/u;var PREPARE_TEMP_RE=/^\.prepare-[0-9]+-[a-f0-9]{24}$/u;var CONTROL_TEMP_RE=/^\.(?:committed|pending)\.json-[a-f0-9]{24}\.tmp$/u;var PEM_CERTIFICATE_RE=/-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+?-----END CERTIFICATE-----/gu;var UTF8_DECODER2=new import_node_util2.TextDecoder("utf-8",{fatal:true});var DEFAULT_RUNTIME={rootUid:0,rootGid:0};var ManagedStartupApplicationError=class extends Error{constructor(message){super(`Managed startup application failed: ${message}`);this.name="ManagedStartupApplicationError"}};function fail(message){throw new ManagedStartupApplicationError(message)}function runtimeFor(override){return override??DEFAULT_RUNTIME}function requireContainerRoot(){if(process.geteuid?.()!==0){fail("the image-side applicator must run with effective uid 0")}}function modeOf(stat){return stat.mode&511}function requireOwner(stat,target,runtime){if(stat.uid!==runtime.rootUid||stat.gid!==runtime.rootGid){fail(`${target} must be owned by root:root`)}}function requireSecureDirectory(target,runtime,exactMode){let stat;try{stat=import_node_fs.default.lstatSync(target)}catch{fail(`state directory component is missing or unreadable: ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail(`state directory component must be a real directory: ${target}`)}const runtimeOwned=stat.uid===runtime.rootUid&&stat.gid===runtime.rootGid;const systemRootOwned=stat.uid===0&&stat.gid===0;if(exactMode){requireOwner(stat,target,runtime)}else if(!runtimeOwned&&!systemRootOwned){fail(`state directory ancestor is not owned by a trusted identity: ${target}`)}const mode=modeOf(stat);const writableByUntrustedIdentity=(mode&18)!==0;const trustedStickyRoot=(stat.mode&512)!==0&&(runtimeOwned||systemRootOwned);if(exactMode&&mode!==STATE_DIRECTORY_MODE||!exactMode&&writableByUntrustedIdentity&&!trustedStickyRoot){fail(exactMode?`${target} must have mode 0700`:`${target} is a replaceable group- or world-writable ancestor`)}}function requireSecureAncestors(target,runtime){const root=import_node_path.default.parse(target).root;let current=root;requireSecureDirectory(current,runtime,false);for(const segment of import_node_path.default.relative(root,target).split(import_node_path.default.sep).filter(Boolean)){current=import_node_path.default.join(current,segment);let stat;try{stat=import_node_fs.default.lstatSync(current)}catch{fail(`state directory component is missing or unreadable: ${current}`)}if(stat.isSymbolicLink()){const runtimeOwned=stat.uid===runtime.rootUid&&stat.gid===runtime.rootGid;const systemRootOwned=stat.uid===0&&stat.gid===0;if(!runtimeOwned&&!systemRootOwned){fail(`state directory ancestor is a replaceable symlink: ${current}`)}let resolved;try{resolved=import_node_fs.default.realpathSync(current)}catch{fail(`state directory symlink is missing or unreadable: ${current}`)}requireSecureAncestors(resolved,runtime);continue}requireSecureDirectory(current,runtime,false)}}function ensureStateDirectory(rawStateDirectory,runtime){const stateDirectory=rawStateDirectory??MANAGED_STARTUP_APPLICATION_STATE_DIR;if(!import_node_path.default.isAbsolute(stateDirectory)||stateDirectory.includes("\0")){fail("stateDirectory must be an absolute path")}const normalized=import_node_path.default.resolve(stateDirectory);const parent=import_node_path.default.dirname(normalized);requireSecureAncestors(parent,runtime);try{import_node_fs.default.mkdirSync(normalized,{mode:STATE_DIRECTORY_MODE});import_node_fs.default.chownSync(normalized,runtime.rootUid,runtime.rootGid);import_node_fs.default.chmodSync(normalized,STATE_DIRECTORY_MODE)}catch(error){if(error.code!=="EEXIST"){fail(`could not create the managed startup state directory: ${normalized}`)}}requireSecureDirectory(normalized,runtime,true);return normalized}function requireSecureRegularFileStat(stat,target,runtime){if(!stat.isFile()||stat.isSymbolicLink()){fail(`${target} must be a regular file`)}if(stat.nlink!==1){fail(`${target} must not be hardlinked`)}requireOwner(stat,target,runtime);if(modeOf(stat)!==STATE_FILE_MODE){fail(`${target} must have mode 0600`)}}function readSecureFile(target,maxBytes,runtime){let descriptor;try{descriptor=import_node_fs.default.openSync(target,import_node_fs.default.constants.O_RDONLY|import_node_fs.default.constants.O_NOFOLLOW)}catch{fail(`state file is missing, unreadable, or a symlink: ${target}`)}try{const stat=import_node_fs.default.fstatSync(descriptor);requireSecureRegularFileStat(stat,target,runtime);if(stat.size<1||stat.size>maxBytes){fail(`${target} is empty or exceeds its size limit`)}const content=import_node_fs.default.readFileSync(descriptor);if(content.length!==stat.size){fail(`${target} changed while it was being read`)}return content}finally{import_node_fs.default.closeSync(descriptor)}}function writeSecureNewFile(target,content,runtime){let descriptor;try{descriptor=import_node_fs.default.openSync(target,import_node_fs.default.constants.O_CREAT|import_node_fs.default.constants.O_EXCL|import_node_fs.default.constants.O_WRONLY|import_node_fs.default.constants.O_NOFOLLOW,STATE_FILE_MODE)}catch{fail(`refused to replace an existing state file: ${target}`)}try{import_node_fs.default.fchownSync(descriptor,runtime.rootUid,runtime.rootGid);import_node_fs.default.fchmodSync(descriptor,STATE_FILE_MODE);import_node_fs.default.writeFileSync(descriptor,content);import_node_fs.default.fsyncSync(descriptor)}finally{import_node_fs.default.closeSync(descriptor)}}function syncDirectory(target){const descriptor=import_node_fs.default.openSync(target,import_node_fs.default.constants.O_RDONLY);try{import_node_fs.default.fsyncSync(descriptor)}finally{import_node_fs.default.closeSync(descriptor)}}function randomToken(){return(0,import_node_crypto4.randomBytes)(12).toString("hex")}function stateControl(fingerprint){return{schemaVersion:STATE_SCHEMA_VERSION,fingerprint,generation:`generation-${fingerprint}`}}function serializeStateControl(control){return JSON.stringify({fingerprint:control.fingerprint,generation:control.generation,schemaVersion:control.schemaVersion})}function parseStateControl(target,runtime){const bytes=readSecureFile(target,MAX_CONTROL_FILE_BYTES,runtime);let raw;try{raw=UTF8_DECODER2.decode(bytes)}catch{fail(`${target} is not valid UTF-8`)}let parsed;try{parsed=JSON.parse(raw)}catch{fail(`${target} is not valid JSON`)}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail(`${target} does not contain a valid state control`)}const record=parsed;if(Object.keys(record).sort().join(",")!=="fingerprint,generation,schemaVersion"||record.schemaVersion!==STATE_SCHEMA_VERSION||typeof record.fingerprint!=="string"||!SHA256_RE2.test(record.fingerprint)||record.generation!==`generation-${record.fingerprint}`){fail(`${target} does not contain a valid state control`)}const control=stateControl(record.fingerprint);if(serializeStateControl(control)!==raw){fail(`${target} is not in canonical form`)}return control}function publishStateControlIfAbsent(stateDirectory,basename,control,runtime){const target=import_node_path.default.join(stateDirectory,basename);const temporary=import_node_path.default.join(stateDirectory,`.${basename}-${randomToken()}.tmp`);writeSecureNewFile(temporary,serializeStateControl(control),runtime);try{import_node_fs.default.linkSync(temporary,target)}catch(error){try{unlinkSecureControlOrTemp(temporary,runtime)}catch{}if(error.code==="EEXIST"){return{control:parseStateControl(target,runtime),created:false}}fail(`could not atomically publish ${basename}`)}try{import_node_fs.default.unlinkSync(temporary)}catch(error){if(error.code!=="ENOENT"){fail(`could not finalize atomic publication of ${basename}`)}}syncDirectory(stateDirectory);return{control,created:true}}function validateCorporateCaBytes(bytes){if(bytes.length<1||bytes.length>MANAGED_STARTUP_CA_MAX_BYTES){fail(`corporate CA bundle must contain 1-${String(MANAGED_STARTUP_CA_MAX_BYTES)} bytes`)}let pem;try{pem=UTF8_DECODER2.decode(bytes)}catch{fail("corporate CA bundle must be valid UTF-8 PEM")}const matches=[...pem.matchAll(PEM_CERTIFICATE_RE)];if(matches.length<1||matches.length>MANAGED_STARTUP_CA_MAX_CERTIFICATES||matches[0]?.index!==0){fail(`corporate CA bundle must contain 1-${String(MANAGED_STARTUP_CA_MAX_CERTIFICATES)} PEM CA certificates`)}let cursor=0;for(const match of matches){const index=match.index;if(index===void 0||!/^(?:\r?\n)+$/u.test(pem.slice(cursor,index))&&index!==0){fail("corporate CA bundle contains non-PEM material between certificates")}const block=match[0];let certificate;try{certificate=new import_node_crypto4.X509Certificate(block)}catch{fail("corporate CA bundle contains an invalid X.509 certificate")}if(!certificate.ca){fail("corporate CA bundle contains a certificate without basicConstraints CA:TRUE")}cursor=index+block.length}if(!/^(?:\r?\n)?$/u.test(pem.slice(cursor))){fail("corporate CA bundle contains trailing non-PEM material")}}function validateManagedStartupCorporateCaTransport(encoded,profile){const expectedDigest=profile.corporateCa.bundleSha256;if(expectedDigest===null){if(encoded!==void 0){fail("corporate CA transport must be absent when the profile has no CA digest")}return null}if(typeof encoded!=="string"||encoded.length===0||encoded.length>Math.ceil(MANAGED_STARTUP_CA_MAX_BYTES/3)*4||!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(encoded)){fail("corporate CA transport must be canonical standard base64")}const bytes=import_node_buffer3.Buffer.from(encoded,"base64");if(bytes.toString("base64")!==encoded){fail("corporate CA transport must be canonical standard base64")}validateCorporateCaBytes(bytes);const actualDigest=(0,import_node_crypto4.createHash)("sha256").update(bytes).digest("hex");if(actualDigest!==expectedDigest){fail("corporate CA bundle does not match the profile SHA-256 digest")}return bytes}function readCanonicalProfile(profilePath,runtime){const bytes=readSecureFile(profilePath,MANAGED_STARTUP_PROFILE_MAX_BYTES,runtime);let raw;try{raw=UTF8_DECODER2.decode(bytes)}catch{fail(`${profilePath} is not valid UTF-8`)}let parsed;try{parsed=JSON.parse(raw)}catch{fail(`${profilePath} is not valid JSON`)}let profile;try{profile=validateManagedStartupProfile(parsed)}catch(error){fail(`${profilePath} is invalid: ${error.message}`)}if(serializeManagedStartupProfile(profile)!==raw){fail(`${profilePath} is not a canonical managed startup profile`)}return{profile,fingerprint:fingerprintManagedStartupProfile(profile)}}function validateGeneration(stateDirectory,control,runtime,expectedAgent){if(!GENERATION_RE.test(control.generation)){fail("state control names an invalid generation")}const directory=import_node_path.default.join(stateDirectory,control.generation);requireSecureDirectory(directory,runtime,true);const entries=import_node_fs.default.readdirSync(directory).sort();if(entries.some(entry=>entry!=="profile.json"&&entry!=="corporate-ca.pem")||!entries.includes("profile.json")){fail(`${directory} contains missing or unsupported state files`)}const profilePath=import_node_path.default.join(directory,"profile.json");const{profile,fingerprint}=readCanonicalProfile(profilePath,runtime);if(fingerprint!==control.fingerprint){fail(`${directory} does not match its recorded profile fingerprint`)}if(expectedAgent!==void 0&&profile.agent!==expectedAgent){fail(`managed startup profile targets ${profile.agent}, expected ${expectedAgent}`)}const caPath=import_node_path.default.join(directory,"corporate-ca.pem");let corporateCaPath=null;if(profile.corporateCa.bundleSha256===null){if(entries.includes("corporate-ca.pem")){fail(`${directory} contains a CA bundle that is absent from the profile`)}}else{if(!entries.includes("corporate-ca.pem")){fail(`${directory} is missing the CA bundle recorded by the profile`)}const caBytes=readSecureFile(caPath,MANAGED_STARTUP_CA_MAX_BYTES,runtime);validateCorporateCaBytes(caBytes);if((0,import_node_crypto4.createHash)("sha256").update(caBytes).digest("hex")!==profile.corporateCa.bundleSha256){fail(`${directory} contains a CA bundle with the wrong SHA-256 digest`)}corporateCaPath=caPath}return{directory,profilePath,corporateCaPath,profile,fingerprint}}function validateDisposableDirectory(target,runtime){requireSecureDirectory(target,runtime,true);const entries=import_node_fs.default.readdirSync(target);if(entries.length>2||entries.some(entry=>entry!=="profile.json"&&entry!=="corporate-ca.pem")){fail(`${target} is not a recognized disposable generation`)}for(const entry of entries){const file=import_node_path.default.join(target,entry);const stat=import_node_fs.default.lstatSync(file);requireSecureRegularFileStat(stat,file,runtime)}}function discardDirectory(target,runtime){validateDisposableDirectory(target,runtime);import_node_fs.default.rmSync(target,{recursive:true})}function discardDirectoryIfPresent(target,runtime){try{import_node_fs.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return false;fail(`could not inspect disposable generation ${target}`)}discardDirectory(target,runtime);return true}function unlinkSecureControlOrTemp(target,runtime){const stat=import_node_fs.default.lstatSync(target);requireSecureRegularFileStat(stat,target,runtime);if(stat.size>MAX_CONTROL_FILE_BYTES){fail(`${target} exceeds the state-control size limit`)}import_node_fs.default.unlinkSync(target)}function listStateEntries(stateDirectory){const entries=import_node_fs.default.readdirSync(stateDirectory).sort();if(entries.length>MAX_STATE_ENTRIES){fail(`state directory exceeds ${String(MAX_STATE_ENTRIES)} entries`)}return entries}function unlinkRecoverableControlTemp(stateDirectory,entry,runtime){const temporary=import_node_path.default.join(stateDirectory,entry);const stat=import_node_fs.default.lstatSync(temporary);if(stat.nlink===1){unlinkSecureControlOrTemp(temporary,runtime);return}const basename=entry.startsWith(".committed.json-")?"committed.json":entry.startsWith(".pending.json-")?"pending.json":null;const target=basename===null?null:import_node_path.default.join(stateDirectory,basename);let targetStat=null;try{targetStat=target===null?null:import_node_fs.default.lstatSync(target)}catch{fail(`refused to remove an unpaired atomic-control temporary file: ${temporary}`)}if(stat.nlink!==2||targetStat===null||stat.dev!==targetStat.dev||stat.ino!==targetStat.ino||!stat.isFile()||stat.isSymbolicLink()||modeOf(stat)!==STATE_FILE_MODE||stat.size<1||stat.size>MAX_CONTROL_FILE_BYTES){fail(`refused to remove an unpaired atomic-control temporary file: ${temporary}`)}requireOwner(stat,temporary,runtime);requireOwner(targetStat,target,runtime);import_node_fs.default.unlinkSync(temporary)}function cleanAtomicTemps(stateDirectory,entries,runtime){let changed=false;for(const entry of entries){const target=import_node_path.default.join(stateDirectory,entry);if(PREPARE_TEMP_RE.test(entry)){discardDirectory(target,runtime);changed=true}else if(CONTROL_TEMP_RE.test(entry)){unlinkRecoverableControlTemp(stateDirectory,entry,runtime);changed=true}}if(changed)syncDirectory(stateDirectory)}function requireKnownStateEntries(stateDirectory,entries){for(const entry of entries){if(entry==="committed.json"||entry==="pending.json"||GENERATION_RE.test(entry)||PREPARE_TEMP_RE.test(entry)||CONTROL_TEMP_RE.test(entry)){continue}fail(`${stateDirectory} contains unsupported state component ${entry}`)}}function discardGenerationsExcept(stateDirectory,keepGeneration,runtime){for(const entry of listStateEntries(stateDirectory)){if(GENERATION_RE.test(entry)&&entry!==keepGeneration){discardDirectoryIfPresent(import_node_path.default.join(stateDirectory,entry),runtime)}}}function optionalStateControl(stateDirectory,basename,runtime){const target=import_node_path.default.join(stateDirectory,basename);try{import_node_fs.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return null;fail(`could not inspect ${target}`)}return parseStateControl(target,runtime)}function removePendingControl(stateDirectory,runtime){try{unlinkSecureControlOrTemp(import_node_path.default.join(stateDirectory,"pending.json"),runtime)}catch(error){if(error.code==="ENOENT")return;throw error}syncDirectory(stateDirectory)}function stateControlsMatch(left,right){return left.fingerprint===right.fingerprint&&left.generation===right.generation}function recoverCommittedState(stateDirectory,committedControl,pendingControl,requested,expectedAgent,runtime){const committed=validateGeneration(stateDirectory,committedControl,runtime,expectedAgent);if(pendingControl)removePendingControl(stateDirectory,runtime);discardGenerationsExcept(stateDirectory,committedControl.generation,runtime);syncDirectory(stateDirectory);if(!stateControlsMatch(committedControl,requested)){fail("a different startup profile is already committed; recreate the sandbox to change it")}return committed}function recoverState(stateDirectory,requested,expectedAgent,runtime){const initialEntries=listStateEntries(stateDirectory);requireKnownStateEntries(stateDirectory,initialEntries);cleanAtomicTemps(stateDirectory,initialEntries,runtime);const initiallyCommittedControl=optionalStateControl(stateDirectory,"committed.json",runtime);const pendingControl=optionalStateControl(stateDirectory,"pending.json",runtime);const committedAfterPendingRead=optionalStateControl(stateDirectory,"committed.json",runtime);const committedControl=committedAfterPendingRead??initiallyCommittedControl;if(committedControl){return{committed:recoverCommittedState(stateDirectory,committedControl,pendingControl,requested,expectedAgent,runtime),pending:null}}if(pendingControl){if(stateControlsMatch(pendingControl,requested)){const pending=validateGeneration(stateDirectory,pendingControl,runtime,expectedAgent);const committedAfterPendingValidation=optionalStateControl(stateDirectory,"committed.json",runtime);if(committedAfterPendingValidation){return{committed:recoverCommittedState(stateDirectory,committedAfterPendingValidation,pendingControl,requested,expectedAgent,runtime),pending:null}}discardGenerationsExcept(stateDirectory,pendingControl.generation,runtime);return{committed:null,pending}}fail("a different startup profile is already pending; wait for it to commit or recreate")}return{committed:null,pending:null}}function createGeneration(stateDirectory,control,profileJson,corporateCa,runtime){const temporaryName=`.prepare-${String(process.pid)}-${randomToken()}`;const temporary=import_node_path.default.join(stateDirectory,temporaryName);const generation=import_node_path.default.join(stateDirectory,control.generation);let renameAttempted=false;try{import_node_fs.default.mkdirSync(temporary,{mode:STATE_DIRECTORY_MODE});import_node_fs.default.chownSync(temporary,runtime.rootUid,runtime.rootGid);import_node_fs.default.chmodSync(temporary,STATE_DIRECTORY_MODE);writeSecureNewFile(import_node_path.default.join(temporary,"profile.json"),profileJson,runtime);if(corporateCa){writeSecureNewFile(import_node_path.default.join(temporary,"corporate-ca.pem"),corporateCa,runtime)}syncDirectory(temporary);renameAttempted=true;import_node_fs.default.renameSync(temporary,generation);syncDirectory(stateDirectory)}catch(error){try{import_node_fs.default.lstatSync(temporary);discardDirectory(temporary,runtime)}catch{}if(error instanceof ManagedStartupApplicationError)throw error;if(renameAttempted&&(error.code==="EEXIST"||error.code==="ENOTEMPTY")){return validateGeneration(stateDirectory,control,runtime)}fail(`could not atomically prepare generation ${control.generation}`)}return validateGeneration(stateDirectory,control,runtime)}function toPrepared(status,stateDirectory,generation,expectedAgent){return{status,stateDirectory,generationDirectory:generation.directory,profilePath:generation.profilePath,corporateCaPath:generation.corporateCaPath,fingerprint:generation.fingerprint,expectedAgent,profile:generation.profile}}function prepareManagedStartupApplication(input,testRuntime){const runtime=runtimeFor(testRuntime);requireContainerRoot();let profile;try{profile=decodeManagedStartupProfile(input.encodedProfile)}catch(error){fail(error.message)}if(profile.agent!==input.expectedAgent){fail(`managed startup profile targets ${profile.agent}, expected ${input.expectedAgent}`)}const corporateCa=validateManagedStartupCorporateCaTransport(input.corporateCaB64,profile);const profileJson=serializeManagedStartupProfile(profile);const control=stateControl(fingerprintManagedStartupProfile(profile));const stateDirectory=ensureStateDirectory(input.stateDirectory,runtime);const recovered=recoverState(stateDirectory,control,input.expectedAgent,runtime);if(recovered.committed){return toPrepared("already-committed",stateDirectory,recovered.committed,input.expectedAgent)}if(recovered.pending){return toPrepared("prepared",stateDirectory,recovered.pending,input.expectedAgent)}const generation=createGeneration(stateDirectory,control,profileJson,corporateCa,runtime);const publication=publishStateControlIfAbsent(stateDirectory,"pending.json",control,runtime);if(publication.control.fingerprint!==control.fingerprint||publication.control.generation!==control.generation){discardDirectoryIfPresent(generation.directory,runtime);syncDirectory(stateDirectory);fail("a different startup profile won the pending-state transaction")}const committedAfterPublication=optionalStateControl(stateDirectory,"committed.json",runtime);if(committedAfterPublication){if(committedAfterPublication.fingerprint!==control.fingerprint||committedAfterPublication.generation!==control.generation){if(publication.created){removePendingControl(stateDirectory,runtime);discardDirectoryIfPresent(generation.directory,runtime);syncDirectory(stateDirectory)}fail("a different startup profile committed during pending-state publication")}const committedGeneration=validateGeneration(stateDirectory,committedAfterPublication,runtime,input.expectedAgent);removePendingControl(stateDirectory,runtime);discardGenerationsExcept(stateDirectory,committedAfterPublication.generation,runtime);return toPrepared("already-committed",stateDirectory,committedGeneration,input.expectedAgent)}const activeGeneration=publication.created?generation:validateGeneration(stateDirectory,publication.control,runtime,input.expectedAgent);return toPrepared("prepared",stateDirectory,activeGeneration,input.expectedAgent)}function validatePreparedHandle(handle){if(!import_node_path.default.isAbsolute(handle.stateDirectory)||!SHA256_RE2.test(handle.fingerprint)||handle.generationDirectory!==import_node_path.default.join(handle.stateDirectory,`generation-${handle.fingerprint}`)||handle.profilePath!==import_node_path.default.join(handle.generationDirectory,"profile.json")||handle.corporateCaPath!==null&&handle.corporateCaPath!==import_node_path.default.join(handle.generationDirectory,"corporate-ca.pem")){fail("prepared startup handle is malformed")}return stateControl(handle.fingerprint)}function commitManagedStartupApplication(prepared,testRuntime){const runtime=runtimeFor(testRuntime);requireContainerRoot();const requested=validatePreparedHandle(prepared);const stateDirectory=ensureStateDirectory(prepared.stateDirectory,runtime);const committedControl=optionalStateControl(stateDirectory,"committed.json",runtime);if(committedControl){if(committedControl.fingerprint!==requested.fingerprint||committedControl.generation!==requested.generation){fail("a different startup profile is already committed")}const generation2=validateGeneration(stateDirectory,committedControl,runtime,prepared.expectedAgent);return{...toPrepared("already-committed",stateDirectory,generation2,prepared.expectedAgent),status:"committed"}}const pendingControl=optionalStateControl(stateDirectory,"pending.json",runtime);if(!pendingControl||pendingControl.fingerprint!==requested.fingerprint||pendingControl.generation!==requested.generation){fail("the prepared startup generation is not the active pending generation")}const generation=validateGeneration(stateDirectory,pendingControl,runtime,prepared.expectedAgent);const publication=publishStateControlIfAbsent(stateDirectory,"committed.json",pendingControl,runtime);if(publication.control.fingerprint!==requested.fingerprint||publication.control.generation!==requested.generation){fail("a different startup profile won the committed-state transaction")}removePendingControl(stateDirectory,runtime);discardGenerationsExcept(stateDirectory,publication.control.generation,runtime);syncDirectory(stateDirectory);return{...toPrepared("already-committed",stateDirectory,generation,prepared.expectedAgent),status:"committed"}}var SHIPPED_AGENT_SET=new Set(MANAGED_STARTUP_AGENTS);var DEFAULT_DEPENDENCIES={prepareApplication:input=>prepareManagedStartupApplication(input),commitApplication:prepared=>commitManagedStartupApplication(prepared)};var ManagedStartupCoordinatorError=class extends Error{constructor(message){super(`Managed startup coordination failed: ${message}`);this.name="ManagedStartupCoordinatorError"}};function fail2(message){throw new ManagedStartupCoordinatorError(message)}function createAdapterRegistry(adapters2){const byAgent=new Map;for(const adapter of adapters2){if(typeof adapter!=="object"||adapter===null||!SHIPPED_AGENT_SET.has(adapter.agent)||typeof adapter.apply!=="function"){fail2("every adapter must identify one shipped agent and provide an apply function")}if(byAgent.has(adapter.agent)){fail2(`duplicate adapter registered for ${adapter.agent}`)}byAgent.set(adapter.agent,adapter)}const missing=MANAGED_STARTUP_AGENTS.filter(agent=>!byAgent.has(agent));if(missing.length>0){fail2(`missing adapter for ${missing.join(", ")}`)}if(byAgent.size!==MANAGED_STARTUP_AGENTS.length){fail2("adapter registry must contain exactly the shipped agents")}return Object.freeze(Object.fromEntries(MANAGED_STARTUP_AGENTS.map(agent=>{const adapter=byAgent.get(agent);if(!adapter)fail2(`missing adapter for ${agent}`);return[agent,adapter]})))}function requirePreparedIdentity(prepared,requestedAgent){if(prepared.expectedAgent!==requestedAgent||prepared.profile.agent!==requestedAgent){fail2(`prepared profile targets ${prepared.profile.agent}, expected ${requestedAgent}`)}}function adapterContext(prepared){return Object.freeze({agent:prepared.profile.agent,profile:prepared.profile,fingerprint:prepared.fingerprint,generationDirectory:prepared.generationDirectory,profilePath:prepared.profilePath,corporateCaPath:prepared.corporateCaPath})}async function coordinateManagedStartupApplication(input,adapters2,dependencies=DEFAULT_DEPENDENCIES){const registry=createAdapterRegistry(adapters2);const prepared=await dependencies.prepareApplication(input);requirePreparedIdentity(prepared,input.expectedAgent);if(prepared.status==="already-committed"){return{adapterApplied:false,application:await dependencies.commitApplication(prepared)}}const adapter=registry[prepared.profile.agent];if(adapter.agent!==prepared.profile.agent){fail2(`adapter registry cross-dispatch detected for ${prepared.profile.agent}`)}await adapter.apply(adapterContext(prepared));return{adapterApplied:true,application:await dependencies.commitApplication(prepared)}}var import_node_crypto5=require("node:crypto");var MANAGED_STARTUP_ROOT_APPLY_SCHEMA_VERSION=1;var MANAGED_STARTUP_ROOT_APPLY_MAX_BYTES=320*1024;var MAX_CORPORATE_CA_ENCODED_BYTES=4*Math.ceil(128*1024/3);var SHA256_RE3=/^[a-f0-9]{64}$/u;var STANDARD_BASE64_RE=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;var MCP_SHADOW_DIAGNOSTICS_ENV="NEMOCLAW_MCP_SHADOW_DIAGNOSTICS";var MANAGED_STARTUP_APPLICATION_RUNTIME_ENV_KEYS=Object.freeze(MANAGED_STARTUP_PROFILE_DEFERRED_RUNTIME_INPUTS.openclaw.filter(({admission,owner})=>admission==="managed-launch-forwarded"&&owner==="application-environment").map(({input})=>input));function selectManagedStartupApplicationRuntimeEnvironment(environment){const selected={};for(const name of MANAGED_STARTUP_APPLICATION_RUNTIME_ENV_KEYS){const value=environment[name];if(name===MCP_SHADOW_DIAGNOSTICS_ENV){if(value?.trim()==="1")selected[name]="1";continue}if(value!==void 0)selected[name]=value}return Object.freeze(selected)}function fail3(message){throw new Error(`Managed startup root application request is invalid: ${message}`)}function isManagedStartupRootApplyAgent(value){return typeof value==="string"&&MANAGED_STARTUP_AGENTS.includes(value)}function exactAgent(value){if(isManagedStartupRootApplyAgent(value))return value;return fail3("agent is unsupported")}function createManagedStartupRootApplyRequest(input){const agent=exactAgent(input.agent);if(input.encodedProfile.length===0||input.encodedProfile.length>MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES){fail3("encoded profile exceeds its bounded transport")}const profile=decodeManagedStartupProfile(input.encodedProfile);if(profile.agent!==agent){fail3(`profile targets ${profile.agent}, expected ${agent}`)}const corporateCaB64=input.corporateCaB64??null;if(corporateCaB64!==null&&(corporateCaB64.length===0||corporateCaB64.length>MAX_CORPORATE_CA_ENCODED_BYTES||!STANDARD_BASE64_RE.test(corporateCaB64)||Buffer.from(corporateCaB64,"base64").toString("base64")!==corporateCaB64)){fail3("corporate CA is not canonical bounded base64")}if(profile.corporateCa.bundleSha256!==null!==(corporateCaB64!==null)){fail3("corporate CA transport does not match the profile")}if(corporateCaB64!==null&&(0,import_node_crypto5.createHash)("sha256").update(Buffer.from(corporateCaB64,"base64")).digest("hex")!==profile.corporateCa.bundleSha256){fail3("corporate CA does not match the profile digest")}return Object.freeze({schemaVersion:MANAGED_STARTUP_ROOT_APPLY_SCHEMA_VERSION,agent,encodedProfile:input.encodedProfile,profileFingerprint:fingerprintManagedStartupProfile(profile),corporateCaB64})}function serializeManagedStartupRootApplyRequest(request){const normalized=createManagedStartupRootApplyRequest({agent:request.agent,encodedProfile:request.encodedProfile,...request.corporateCaB64===null?{}:{corporateCaB64:request.corporateCaB64}});if(request.schemaVersion!==MANAGED_STARTUP_ROOT_APPLY_SCHEMA_VERSION||request.profileFingerprint!==normalized.profileFingerprint||!SHA256_RE3.test(request.profileFingerprint)){fail3("schema version or profile fingerprint is invalid")}const serialized=`${JSON.stringify({agent:normalized.agent,corporateCaB64:normalized.corporateCaB64,encodedProfile:normalized.encodedProfile,profileFingerprint:normalized.profileFingerprint,schemaVersion:normalized.schemaVersion})} -`;if(Buffer.byteLength(serialized,"utf8")>MANAGED_STARTUP_ROOT_APPLY_MAX_BYTES){fail3("serialized request exceeds its bounded transport")}return serialized}function parseManagedStartupRootApplyRequest(text){if(text.length===0||Buffer.byteLength(text,"utf8")>MANAGED_STARTUP_ROOT_APPLY_MAX_BYTES){fail3("serialized request is empty or too large")}let parsed;try{parsed=JSON.parse(text)}catch{fail3("serialized request is not valid JSON")}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail3("serialized request must be an object")}const record=parsed;const expectedKeys=["agent","corporateCaB64","encodedProfile","profileFingerprint","schemaVersion"];if(Object.keys(record).sort().join(",")!==expectedKeys.sort().join(",")||record.schemaVersion!==MANAGED_STARTUP_ROOT_APPLY_SCHEMA_VERSION||typeof record.encodedProfile!=="string"||typeof record.profileFingerprint!=="string"||record.corporateCaB64!==null&&typeof record.corporateCaB64!=="string"){fail3("serialized request has an invalid schema")}const request=createManagedStartupRootApplyRequest({agent:exactAgent(record.agent),encodedProfile:record.encodedProfile,...record.corporateCaB64===null?{}:{corporateCaB64:record.corporateCaB64}});if(record.profileFingerprint!==request.profileFingerprint||!SHA256_RE3.test(record.profileFingerprint)){fail3("profile fingerprint does not match the encoded profile")}if(serializeManagedStartupRootApplyRequest(request)!==text){fail3("serialized request is not canonical")}return request}var import_node_crypto6=require("node:crypto");var import_node_fs2=__toESM(require("node:fs"));var import_node_path2=__toESM(require("node:path"));var MANAGED_HERMES_STATE_ROOT="/sandbox/.hermes";var MANAGED_OPENCLAW_STATE_ROOT="/sandbox/.openclaw";var HERMES_STATE_VOLUME_NAME_PREFIX="nemoclaw-hermes-state-v1";var OPENCLAW_STATE_VOLUME_NAME_PREFIX="nemoclaw-openclaw-state-v1";var MANAGED_AGENT_STATE_ROOTS=Object.freeze({openclaw:Object.freeze([Object.freeze({mountTarget:MANAGED_OPENCLAW_STATE_ROOT,resourceIdentity:sandboxName=>`${OPENCLAW_STATE_VOLUME_NAME_PREFIX}-${sandboxName}`,ownershipLabels:(sandboxName,mountTarget)=>Object.freeze({"io.nvidia.nemoclaw.openclaw-state.managed":"true","io.nvidia.nemoclaw.openclaw-state.schema":"1","io.nvidia.nemoclaw.openclaw-state.sandbox":sandboxName,"io.nvidia.nemoclaw.openclaw-state.target":mountTarget}),uidAuthority:"agent",gidAuthority:"agent",mode:1528,readWrite:true})]),hermes:Object.freeze([Object.freeze({mountTarget:MANAGED_HERMES_STATE_ROOT,resourceIdentity:sandboxName=>`${HERMES_STATE_VOLUME_NAME_PREFIX}-${sandboxName}`,ownershipLabels:(sandboxName,mountTarget)=>Object.freeze({"io.nvidia.nemoclaw.hermes-state.managed":"true","io.nvidia.nemoclaw.hermes-state.schema":"1","io.nvidia.nemoclaw.hermes-state.sandbox":sandboxName,"io.nvidia.nemoclaw.hermes-state.target":mountTarget}),uidAuthority:"agent",gidAuthority:"agent",mode:2040,readWrite:true})]),"langchain-deepagents-code":Object.freeze([]),pi:Object.freeze([])});function managedStartupStateRootMountTargets(agent){return Object.freeze(MANAGED_AGENT_STATE_ROOTS[agent].map(({mountTarget})=>mountTarget))}var MANAGED_AGENT_WORKSPACE_ROOTS=Object.freeze({openclaw:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493}),hermes:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493}),"langchain-deepagents-code":Object.freeze({uidAuthority:"root",gidAuthority:"agent",mode:1021}),pi:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493})});function exactAgentIdentity(value,label){if(!Number.isSafeInteger(value)||value<0||value>2147483647){throw new Error(`Managed startup state-root ${label} authority is invalid.`)}return value}function managedStartupWorkspaceRoot(input){const uid=exactAgentIdentity(input.agentIdentity.uid,"workspace UID");const gid=exactAgentIdentity(input.agentIdentity.gid,"workspace GID");const declaration=MANAGED_AGENT_WORKSPACE_ROOTS[input.agent];return Object.freeze({uid:declaration.uidAuthority==="root"?0:uid,gid,mode:declaration.mode})}var TRANSACTION_SCHEMA_VERSION=1;var MAX_TRANSACTION_FILES=128;var MAX_TRANSACTION_FILE_BYTES=8*1024*1024;var MAX_TRANSACTION_TOTAL_BYTES=32*1024*1024;var MAX_MANIFEST_BYTES=256*1024;var MAX_COMMIT_RECEIPT_BYTES=4096;var TRANSACTION_PARENT_DIRECTORY_MODE=493;var TRANSACTION_DIRECTORY_MODE=448;var TRANSACTION_FILE_MODE=256;var ATOMIC_TEMPORARY_FILE_MODE=384;var MANAGED_STARTUP_SHARED_TRANSACTION_DIRECTORY="/var/lib/nemoclaw/managed-startup-shared-state-transaction-v1";var MANAGED_STARTUP_SHARED_ROLLBACK_RECEIPT_DIRECTORY="/run/nemoclaw/managed-startup-shared-rollback-receipt-v1";var MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY="/var/lib/nemoclaw/managed-startup-shared-state-commit-v1";var MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE="receipt.json";function fail4(message){throw new Error(`Managed startup shared-state transaction failed: ${message}`)}function resolveOptions(options={}){const sandboxRoot=import_node_path2.default.resolve(options.sandboxRoot??"/sandbox");const transactionDirectory=import_node_path2.default.resolve(options.transactionDirectory??MANAGED_STARTUP_SHARED_TRANSACTION_DIRECTORY);const commitReceiptDirectory=import_node_path2.default.resolve(options.commitReceiptDirectory??(options.transactionDirectory?import_node_path2.default.join(import_node_path2.default.dirname(transactionDirectory),import_node_path2.default.basename(MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY)):MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY));if(transactionDirectory===sandboxRoot||transactionDirectory.startsWith(`${sandboxRoot}${import_node_path2.default.sep}`)||commitReceiptDirectory===sandboxRoot||commitReceiptDirectory.startsWith(`${sandboxRoot}${import_node_path2.default.sep}`)||import_node_path2.default.dirname(commitReceiptDirectory)!==import_node_path2.default.dirname(transactionDirectory)||commitReceiptDirectory===transactionDirectory){fail4("transaction and commit receipts require distinct paths outside sandbox-shared state")}const bootstrapIdentity=options.bootstrapIdentity??null;if(bootstrapIdentity!==null&&!/^[a-f0-9]{64}$/u.test(bootstrapIdentity)){fail4("bootstrap identity must encode 32 lowercase-hex bytes")}return{sandboxRoot,transactionParentDirectory:import_node_path2.default.dirname(transactionDirectory),transactionDirectory,backupDirectory:import_node_path2.default.join(transactionDirectory,"backups"),manifestFile:import_node_path2.default.join(transactionDirectory,"manifest.json"),commitReceiptDirectory,commitReceiptFile:import_node_path2.default.join(commitReceiptDirectory,MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE),trustedUid:options.trustedUid??0,trustedGid:options.trustedGid??0,readOnlyReceipt:options.readOnlyReceipt??false,bootstrapIdentity}}function modeOf2(stat){if(typeof stat.mode==="bigint"){return Number(stat.mode&0o7777n)}return stat.mode&4095}function requireTransactionIdentity(options){const expectedUid=options.readOnlyReceipt?0:options.trustedUid;const expectedGid=options.readOnlyReceipt?0:options.trustedGid;if(process.geteuid?.()!==expectedUid||process.getegid?.()!==expectedGid){fail4("transaction control requires the trusted effective identity")}}function pathExistsNoFollow(target){try{import_node_fs2.default.lstatSync(target);return true}catch(error){if(error.code==="ENOENT")return false;fail4(`could not inspect ${target}`)}}function requireDirectory(target,options,expectedMode=null){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch{fail4(`required directory is missing: ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`required directory is unsafe: ${target}`)}if(expectedMode!==null&&(stat.uid!==options.trustedUid||stat.gid!==options.trustedGid||modeOf2(stat)!==expectedMode)){fail4(`${target} must be ${options.trustedUid}:${options.trustedGid} mode ${expectedMode.toString(8)}`)}return stat}function requireTransactionBoundaries(options){requireDirectory(options.sandboxRoot,options);requireDirectory(options.transactionParentDirectory,options,TRANSACTION_PARENT_DIRECTORY_MODE)}function sameStableMetadata(left,right){return left.dev===right.dev&&left.ino===right.ino&&left.mode===right.mode&&left.nlink===right.nlink&&left.uid===right.uid&&left.gid===right.gid&&left.size===right.size&&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs}function readStableFile(target,maxBytes){const noFollow=import_node_fs2.default.constants.O_NOFOLLOW;if(typeof noFollow!=="number")fail4("O_NOFOLLOW is unavailable");let descriptor;try{descriptor=import_node_fs2.default.openSync(target,import_node_fs2.default.constants.O_RDONLY|noFollow)}catch{fail4(`could not safely open ${target}`)}try{const before=import_node_fs2.default.fstatSync(descriptor,{bigint:true});if(!before.isFile()||before.nlink!==1n||before.size<0n||before.size>BigInt(maxBytes)){fail4(`refusing unsafe or oversized transaction file ${target}`)}const bytes=Buffer.alloc(Number(before.size));let offset=0;while(offset!segment||segment==="."||segment==="..")){fail4(`unsafe transaction path ${JSON.stringify(value)}`)}return segments.join("/")}function absoluteTarget(relativePath,options){const safe=safeRelativePath(relativePath);const target=import_node_path2.default.resolve(options.sandboxRoot,safe);if(!target.startsWith(`${options.sandboxRoot}${import_node_path2.default.sep}`)){fail4(`transaction target escapes the sandbox root: ${relativePath}`)}return target}function relativeTarget(target,options){return safeRelativePath(import_node_path2.default.relative(options.sandboxRoot,target))}function isDeclaredAgentStateRoot(expectedAgent,outputRoot,options){const relative=import_node_path2.default.relative(options.sandboxRoot,outputRoot).split(import_node_path2.default.sep).join("/");const canonicalTarget=import_node_path2.default.posix.join("/sandbox",relative);return managedStartupStateRootMountTargets(expectedAgent).includes(canonicalTarget)}function validateExistingAncestors(target,expectedAgent,options){const relative=relativeTarget(target,options);const sandboxStat=requireDirectory(options.sandboxRoot,options);const outputRoot=agentRoot(expectedAgent,options.sandboxRoot);if(target!==outputRoot&&!target.startsWith(`${outputRoot}${import_node_path2.default.sep}`)){fail4(`transaction target escapes the ${expectedAgent} state root: ${target}`)}let current=options.sandboxRoot;let expectedDevice=sandboxStat.dev;const segments=relative.split("/").slice(0,-1);for(const segment of segments){current=import_node_path2.default.join(current,segment);let stat;try{stat=import_node_fs2.default.lstatSync(current)}catch(error){if(error.code==="ENOENT")return;fail4(`could not inspect transaction path ancestor ${current}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`transaction path ancestor is unsafe: ${current}`)}if(current===outputRoot&&isDeclaredAgentStateRoot(expectedAgent,outputRoot,options)){expectedDevice=stat.dev}else if(stat.dev!==expectedDevice){fail4(`transaction path crosses a nested filesystem mount: ${current}`)}}}function managedOutputDevice(expectedAgent,options){const sandboxStat=requireDirectory(options.sandboxRoot,options);const outputRoot=agentRoot(expectedAgent,options.sandboxRoot);let stat;try{stat=import_node_fs2.default.lstatSync(outputRoot)}catch(error){if(error.code==="ENOENT")return sandboxStat.dev;fail4(`could not inspect managed output root ${outputRoot}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`managed output root is unsafe: ${outputRoot}`)}if(!isDeclaredAgentStateRoot(expectedAgent,outputRoot,options)&&stat.dev!==sandboxStat.dev){fail4(`managed output root crosses a nested filesystem mount: ${outputRoot}`)}return stat.dev}function agentRoot(agent,sandboxRoot){switch(agent){case"openclaw":return import_node_path2.default.join(sandboxRoot,".openclaw");case"hermes":return import_node_path2.default.join(sandboxRoot,".hermes");case"langchain-deepagents-code":return import_node_path2.default.join(sandboxRoot,".deepagents");case"pi":return import_node_path2.default.join(sandboxRoot,".pi")}}function resolveUnderAgentRoot(root,relativePath){const safe=safeRelativePath(relativePath);const target=import_node_path2.default.resolve(root,safe);if(!target.startsWith(`${root}${import_node_path2.default.sep}`)){fail4(`managed output escapes the agent root: ${relativePath}`)}return target}function renderTarget(root,agent,target){if(agent==="openclaw"&&target==="openclaw.json"){return import_node_path2.default.join(root,"openclaw.json")}const prefix=agent==="openclaw"?"~/.openclaw/":agent==="hermes"?"~/.hermes/":null;if(!prefix||!target.startsWith(prefix)){fail4(`unsupported managed messaging render target ${JSON.stringify(target)}`)}return resolveUnderAgentRoot(root,target.slice(prefix.length))}function managedOutputTargets(profile,options){const root=agentRoot(profile.agent,options.sandboxRoot);const files=new Set;const directories=new Set([root]);switch(profile.agent){case"openclaw":files.add(import_node_path2.default.join(root,"openclaw.json"));break;case"hermes":files.add(import_node_path2.default.join(root,"config.yaml"));files.add(import_node_path2.default.join(root,".env"));files.add(import_node_path2.default.join(root,".config-hash"));break;case"langchain-deepagents-code":files.add(import_node_path2.default.join(root,"config.toml"));directories.add(import_node_path2.default.join(root,".state"));break;case"pi":directories.add(import_node_path2.default.join(root,"agent"));files.add(import_node_path2.default.join(root,"agent","models.json"));files.add(import_node_path2.default.join(root,"agent","settings.json"));break}if(profile.messaging.plan!==null){const plan=parseSandboxMessagingPlan(profile.messaging.plan,{agent:profile.agent});if(!plan)fail4("managed messaging plan is invalid");for(const render of selectEnabledMessagingAgentRender(plan)){if(typeof render.target!=="string")continue;files.add(renderTarget(root,profile.agent,render.target))}for(const step of selectEnabledPostAgentInstallBuildFiles(plan)){if(typeof step.value!=="object"||step.value===null){continue}const outputPath=step.value.path;if(typeof outputPath==="string"){files.add(resolveUnderAgentRoot(root,outputPath))}}}for(const file of files){let parent=import_node_path2.default.dirname(file);while(parent!==options.sandboxRoot&&parent.startsWith(`${root}${import_node_path2.default.sep}`)){directories.add(parent);if(parent===root)break;parent=import_node_path2.default.dirname(parent)}}return{files:[...files].sort(),directories:[...directories].sort((left,right)=>left.split(import_node_path2.default.sep).length-right.split(import_node_path2.default.sep).length)}}function snapshotFile(target,index,expectedAgent,options){validateExistingAncestors(target,expectedAgent,options);let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT"){return{receipt:{path:relativeTarget(target,options),state:"absent"},bytes:null}}fail4(`could not inspect managed output ${target}`)}if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1){fail4(`managed output is not a safe regular file: ${target}`)}if(stat.dev!==managedOutputDevice(expectedAgent,options)){fail4(`managed output crosses a nested filesystem mount: ${target}`)}const stable=readStableFile(target,MAX_TRANSACTION_FILE_BYTES);const size=Number(stable.stat.size);const backup=`${String(index).padStart(3,"0")}.bin`;return{receipt:{path:relativeTarget(target,options),state:"file",backup,sha256:(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex"),size,uid:Number(stable.stat.uid),gid:Number(stable.stat.gid),mode:Number(stable.stat.mode&0o7777n)},bytes:stable.bytes}}function snapshotDirectory(target,expectedAgent,options){validateExistingAncestors(import_node_path2.default.join(target,".receipt"),expectedAgent,options);let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT"){return{path:relativeTarget(target,options),state:"absent"}}fail4(`could not inspect managed output directory ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`managed output directory is unsafe: ${target}`)}if(stat.dev!==managedOutputDevice(expectedAgent,options)){fail4(`managed output directory crosses a nested filesystem mount: ${target}`)}return{path:relativeTarget(target,options),state:"directory",uid:stat.uid,gid:stat.gid,mode:modeOf2(stat)}}function atomicWriteTrustedFile(target,contents,mode,uid,gid){const parent=import_node_path2.default.dirname(target);const temporary=import_node_path2.default.join(parent,`.${import_node_path2.default.basename(target)}.${(0,import_node_crypto6.randomBytes)(12).toString("hex")}`);let descriptor;try{descriptor=import_node_fs2.default.openSync(temporary,import_node_fs2.default.constants.O_CREAT|import_node_fs2.default.constants.O_EXCL|import_node_fs2.default.constants.O_WRONLY|import_node_fs2.default.constants.O_NOFOLLOW,384);import_node_fs2.default.writeFileSync(descriptor,contents);import_node_fs2.default.fchownSync(descriptor,uid,gid);import_node_fs2.default.fchmodSync(descriptor,mode);import_node_fs2.default.fsyncSync(descriptor);import_node_fs2.default.closeSync(descriptor);descriptor=void 0;import_node_fs2.default.renameSync(temporary,target)}catch(error){if(descriptor!==void 0)import_node_fs2.default.closeSync(descriptor);try{import_node_fs2.default.unlinkSync(temporary)}catch{}fail4(`could not atomically write ${target}: ${error.message}`)}}function fsyncDirectory(directory){const descriptor=import_node_fs2.default.openSync(directory,import_node_fs2.default.constants.O_RDONLY);try{import_node_fs2.default.fsyncSync(descriptor)}finally{import_node_fs2.default.closeSync(descriptor)}}function canonicalManifest(manifest){return`${JSON.stringify(manifest,null,2)} +`;if(Buffer.byteLength(serialized,"utf8")>MANAGED_STARTUP_ROOT_APPLY_MAX_BYTES){fail3("serialized request exceeds its bounded transport")}return serialized}function parseManagedStartupRootApplyRequest(text){if(text.length===0||Buffer.byteLength(text,"utf8")>MANAGED_STARTUP_ROOT_APPLY_MAX_BYTES){fail3("serialized request is empty or too large")}let parsed;try{parsed=JSON.parse(text)}catch{fail3("serialized request is not valid JSON")}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail3("serialized request must be an object")}const record=parsed;const expectedKeys=["agent","corporateCaB64","encodedProfile","profileFingerprint","schemaVersion"];if(Object.keys(record).sort().join(",")!==expectedKeys.sort().join(",")||record.schemaVersion!==MANAGED_STARTUP_ROOT_APPLY_SCHEMA_VERSION||typeof record.encodedProfile!=="string"||typeof record.profileFingerprint!=="string"||record.corporateCaB64!==null&&typeof record.corporateCaB64!=="string"){fail3("serialized request has an invalid schema")}const request=createManagedStartupRootApplyRequest({agent:exactAgent(record.agent),encodedProfile:record.encodedProfile,...record.corporateCaB64===null?{}:{corporateCaB64:record.corporateCaB64}});if(record.profileFingerprint!==request.profileFingerprint||!SHA256_RE3.test(record.profileFingerprint)){fail3("profile fingerprint does not match the encoded profile")}if(serializeManagedStartupRootApplyRequest(request)!==text){fail3("serialized request is not canonical")}return request}var import_node_crypto6=require("node:crypto");var import_node_fs2=__toESM(require("node:fs"));var import_node_path2=__toESM(require("node:path"));var MANAGED_HERMES_STATE_ROOT="/sandbox/.hermes";var MANAGED_OPENCLAW_STATE_ROOT="/sandbox/.openclaw";var HERMES_STATE_VOLUME_NAME_PREFIX="nemoclaw-hermes-state-v1";var OPENCLAW_STATE_VOLUME_NAME_PREFIX="nemoclaw-openclaw-state-v1";var MANAGED_AGENT_STATE_ROOTS=Object.freeze({openclaw:Object.freeze([Object.freeze({mountTarget:MANAGED_OPENCLAW_STATE_ROOT,resourceIdentity:sandboxName=>`${OPENCLAW_STATE_VOLUME_NAME_PREFIX}-${sandboxName}`,ownershipLabels:(sandboxName,mountTarget)=>Object.freeze({"io.nvidia.nemoclaw.openclaw-state.managed":"true","io.nvidia.nemoclaw.openclaw-state.schema":"1","io.nvidia.nemoclaw.openclaw-state.sandbox":sandboxName,"io.nvidia.nemoclaw.openclaw-state.target":mountTarget}),uidAuthority:"agent",gidAuthority:"agent",mode:1528,readWrite:true})]),hermes:Object.freeze([Object.freeze({mountTarget:MANAGED_HERMES_STATE_ROOT,resourceIdentity:sandboxName=>`${HERMES_STATE_VOLUME_NAME_PREFIX}-${sandboxName}`,ownershipLabels:(sandboxName,mountTarget)=>Object.freeze({"io.nvidia.nemoclaw.hermes-state.managed":"true","io.nvidia.nemoclaw.hermes-state.schema":"1","io.nvidia.nemoclaw.hermes-state.sandbox":sandboxName,"io.nvidia.nemoclaw.hermes-state.target":mountTarget}),uidAuthority:"agent",gidAuthority:"agent",mode:2040,readWrite:true})]),"langchain-deepagents-code":Object.freeze([]),pi:Object.freeze([])});function managedStartupStateRootMountTargets(agent){return Object.freeze(MANAGED_AGENT_STATE_ROOTS[agent].map(({mountTarget})=>mountTarget))}var MANAGED_AGENT_WORKSPACE_ROOTS=Object.freeze({openclaw:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493}),hermes:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493}),"langchain-deepagents-code":Object.freeze({uidAuthority:"root",gidAuthority:"agent",mode:1021}),pi:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493})});function exactAgentIdentity(value,label){if(!Number.isSafeInteger(value)||value<0||value>2147483647){throw new Error(`Managed startup state-root ${label} authority is invalid.`)}return value}function managedStartupWorkspaceRoot(input){const uid=exactAgentIdentity(input.agentIdentity.uid,"workspace UID");const gid=exactAgentIdentity(input.agentIdentity.gid,"workspace GID");const declaration=MANAGED_AGENT_WORKSPACE_ROOTS[input.agent];return Object.freeze({uid:declaration.uidAuthority==="root"?0:uid,gid,mode:declaration.mode})}var TRANSACTION_SCHEMA_VERSION=1;var MAX_TRANSACTION_FILES=128;var MAX_TRANSACTION_FILE_BYTES=8*1024*1024;var MAX_TRANSACTION_TOTAL_BYTES=32*1024*1024;var MAX_MANIFEST_BYTES=256*1024;var MAX_COMMIT_RECEIPT_BYTES=4096;var MANAGED_STARTUP_SHARED_CAPTURE_MAX_BYTES=4*Math.ceil(MAX_TRANSACTION_TOTAL_BYTES/3)+MAX_MANIFEST_BYTES*2;var TRANSACTION_PARENT_DIRECTORY_MODE=493;var TRANSACTION_DIRECTORY_MODE=448;var TRANSACTION_FILE_MODE=256;var ATOMIC_TEMPORARY_FILE_MODE=384;var MANAGED_STARTUP_SHARED_TRANSACTION_DIRECTORY="/var/lib/nemoclaw/managed-startup-shared-state-transaction-v1";var MANAGED_STARTUP_SHARED_ROLLBACK_RECEIPT_DIRECTORY="/run/nemoclaw/managed-startup-shared-rollback-receipt-v1";var MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY="/var/lib/nemoclaw/managed-startup-shared-state-commit-v1";var MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE="receipt.json";function fail4(message){throw new Error(`Managed startup shared-state transaction failed: ${message}`)}function resolveOptions(options={}){const sandboxRoot=import_node_path2.default.resolve(options.sandboxRoot??"/sandbox");const transactionDirectory=import_node_path2.default.resolve(options.transactionDirectory??MANAGED_STARTUP_SHARED_TRANSACTION_DIRECTORY);const commitReceiptDirectory=import_node_path2.default.resolve(options.commitReceiptDirectory??(options.transactionDirectory?import_node_path2.default.join(import_node_path2.default.dirname(transactionDirectory),import_node_path2.default.basename(MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY)):MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY));if(transactionDirectory===sandboxRoot||transactionDirectory.startsWith(`${sandboxRoot}${import_node_path2.default.sep}`)||commitReceiptDirectory===sandboxRoot||commitReceiptDirectory.startsWith(`${sandboxRoot}${import_node_path2.default.sep}`)||import_node_path2.default.dirname(commitReceiptDirectory)!==import_node_path2.default.dirname(transactionDirectory)||commitReceiptDirectory===transactionDirectory){fail4("transaction and commit receipts require distinct paths outside sandbox-shared state")}const bootstrapIdentity=options.bootstrapIdentity??null;if(bootstrapIdentity!==null&&!/^[a-f0-9]{64}$/u.test(bootstrapIdentity)){fail4("bootstrap identity must encode 32 lowercase-hex bytes")}return{sandboxRoot,transactionParentDirectory:import_node_path2.default.dirname(transactionDirectory),transactionDirectory,backupDirectory:import_node_path2.default.join(transactionDirectory,"backups"),manifestFile:import_node_path2.default.join(transactionDirectory,"manifest.json"),commitReceiptDirectory,commitReceiptFile:import_node_path2.default.join(commitReceiptDirectory,MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE),trustedUid:options.trustedUid??0,trustedGid:options.trustedGid??0,readOnlyReceipt:options.readOnlyReceipt??false,bootstrapIdentity}}function modeOf2(stat){if(typeof stat.mode==="bigint"){return Number(stat.mode&0o7777n)}return stat.mode&4095}function requireTransactionIdentity(options){const expectedUid=options.readOnlyReceipt?0:options.trustedUid;const expectedGid=options.readOnlyReceipt?0:options.trustedGid;if(process.geteuid?.()!==expectedUid||process.getegid?.()!==expectedGid){fail4("transaction control requires the trusted effective identity")}}function pathExistsNoFollow(target){try{import_node_fs2.default.lstatSync(target);return true}catch(error){if(error.code==="ENOENT")return false;fail4(`could not inspect ${target}`)}}function requireDirectory(target,options,expectedMode=null){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch{fail4(`required directory is missing: ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`required directory is unsafe: ${target}`)}if(expectedMode!==null&&(stat.uid!==options.trustedUid||stat.gid!==options.trustedGid||modeOf2(stat)!==expectedMode)){fail4(`${target} must be ${options.trustedUid}:${options.trustedGid} mode ${expectedMode.toString(8)}`)}return stat}function requireTransactionBoundaries(options){requireDirectory(options.sandboxRoot,options);requireDirectory(options.transactionParentDirectory,options,TRANSACTION_PARENT_DIRECTORY_MODE)}function sameStableMetadata(left,right){return left.dev===right.dev&&left.ino===right.ino&&left.mode===right.mode&&left.nlink===right.nlink&&left.uid===right.uid&&left.gid===right.gid&&left.size===right.size&&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs}function readStableFile(target,maxBytes){const noFollow=import_node_fs2.default.constants.O_NOFOLLOW;if(typeof noFollow!=="number")fail4("O_NOFOLLOW is unavailable");let descriptor;try{descriptor=import_node_fs2.default.openSync(target,import_node_fs2.default.constants.O_RDONLY|noFollow)}catch{fail4(`could not safely open ${target}`)}try{const before=import_node_fs2.default.fstatSync(descriptor,{bigint:true});if(!before.isFile()||before.nlink!==1n||before.size<0n||before.size>BigInt(maxBytes)){fail4(`refusing unsafe or oversized transaction file ${target}`)}const bytes=Buffer.alloc(Number(before.size));let offset=0;while(offset!segment||segment==="."||segment==="..")){fail4(`unsafe transaction path ${JSON.stringify(value)}`)}return segments.join("/")}function absoluteTarget(relativePath,options){const safe=safeRelativePath(relativePath);const target=import_node_path2.default.resolve(options.sandboxRoot,safe);if(!target.startsWith(`${options.sandboxRoot}${import_node_path2.default.sep}`)){fail4(`transaction target escapes the sandbox root: ${relativePath}`)}return target}function relativeTarget(target,options){return safeRelativePath(import_node_path2.default.relative(options.sandboxRoot,target))}function isDeclaredAgentStateRoot(expectedAgent,outputRoot,options){const relative=import_node_path2.default.relative(options.sandboxRoot,outputRoot).split(import_node_path2.default.sep).join("/");const canonicalTarget=import_node_path2.default.posix.join("/sandbox",relative);return managedStartupStateRootMountTargets(expectedAgent).includes(canonicalTarget)}function validateExistingAncestors(target,expectedAgent,options){const relative=relativeTarget(target,options);const sandboxStat=requireDirectory(options.sandboxRoot,options);const outputRoot=agentRoot(expectedAgent,options.sandboxRoot);if(target!==outputRoot&&!target.startsWith(`${outputRoot}${import_node_path2.default.sep}`)){fail4(`transaction target escapes the ${expectedAgent} state root: ${target}`)}let current=options.sandboxRoot;let expectedDevice=sandboxStat.dev;const segments=relative.split("/").slice(0,-1);for(const segment of segments){current=import_node_path2.default.join(current,segment);let stat;try{stat=import_node_fs2.default.lstatSync(current)}catch(error){if(error.code==="ENOENT")return;fail4(`could not inspect transaction path ancestor ${current}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`transaction path ancestor is unsafe: ${current}`)}if(current===outputRoot&&isDeclaredAgentStateRoot(expectedAgent,outputRoot,options)){expectedDevice=stat.dev}else if(stat.dev!==expectedDevice){fail4(`transaction path crosses a nested filesystem mount: ${current}`)}}}function managedOutputDevice(expectedAgent,options){const sandboxStat=requireDirectory(options.sandboxRoot,options);const outputRoot=agentRoot(expectedAgent,options.sandboxRoot);let stat;try{stat=import_node_fs2.default.lstatSync(outputRoot)}catch(error){if(error.code==="ENOENT")return sandboxStat.dev;fail4(`could not inspect managed output root ${outputRoot}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`managed output root is unsafe: ${outputRoot}`)}if(!isDeclaredAgentStateRoot(expectedAgent,outputRoot,options)&&stat.dev!==sandboxStat.dev){fail4(`managed output root crosses a nested filesystem mount: ${outputRoot}`)}return stat.dev}function agentRoot(agent,sandboxRoot){switch(agent){case"openclaw":return import_node_path2.default.join(sandboxRoot,".openclaw");case"hermes":return import_node_path2.default.join(sandboxRoot,".hermes");case"langchain-deepagents-code":return import_node_path2.default.join(sandboxRoot,".deepagents");case"pi":return import_node_path2.default.join(sandboxRoot,".pi")}}function resolveUnderAgentRoot(root,relativePath){const safe=safeRelativePath(relativePath);const target=import_node_path2.default.resolve(root,safe);if(!target.startsWith(`${root}${import_node_path2.default.sep}`)){fail4(`managed output escapes the agent root: ${relativePath}`)}return target}function renderTarget(root,agent,target){if(agent==="openclaw"&&target==="openclaw.json"){return import_node_path2.default.join(root,"openclaw.json")}const prefix=agent==="openclaw"?"~/.openclaw/":agent==="hermes"?"~/.hermes/":null;if(!prefix||!target.startsWith(prefix)){fail4(`unsupported managed messaging render target ${JSON.stringify(target)}`)}return resolveUnderAgentRoot(root,target.slice(prefix.length))}function managedOutputTargets(profile,options){const root=agentRoot(profile.agent,options.sandboxRoot);const files=new Set;const directories=new Set([root]);switch(profile.agent){case"openclaw":files.add(import_node_path2.default.join(root,"openclaw.json"));break;case"hermes":files.add(import_node_path2.default.join(root,"config.yaml"));files.add(import_node_path2.default.join(root,".env"));files.add(import_node_path2.default.join(root,".config-hash"));break;case"langchain-deepagents-code":files.add(import_node_path2.default.join(root,"config.toml"));directories.add(import_node_path2.default.join(root,".state"));break;case"pi":directories.add(import_node_path2.default.join(root,"agent"));files.add(import_node_path2.default.join(root,"agent","models.json"));files.add(import_node_path2.default.join(root,"agent","settings.json"));break}if(profile.messaging.plan!==null){const plan=parseSandboxMessagingPlan(profile.messaging.plan,{agent:profile.agent});if(!plan)fail4("managed messaging plan is invalid");for(const render of selectEnabledMessagingAgentRender(plan)){if(typeof render.target!=="string")continue;files.add(renderTarget(root,profile.agent,render.target))}for(const step of selectEnabledPostAgentInstallBuildFiles(plan)){if(typeof step.value!=="object"||step.value===null){continue}const outputPath=step.value.path;if(typeof outputPath==="string"){files.add(resolveUnderAgentRoot(root,outputPath))}}}for(const file of files){let parent=import_node_path2.default.dirname(file);while(parent!==options.sandboxRoot&&parent.startsWith(`${root}${import_node_path2.default.sep}`)){directories.add(parent);if(parent===root)break;parent=import_node_path2.default.dirname(parent)}}return{files:[...files].sort(),directories:[...directories].sort((left,right)=>left.split(import_node_path2.default.sep).length-right.split(import_node_path2.default.sep).length)}}function snapshotFile(target,index,expectedAgent,options){validateExistingAncestors(target,expectedAgent,options);let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT"){return{receipt:{path:relativeTarget(target,options),state:"absent"},bytes:null}}fail4(`could not inspect managed output ${target}`)}if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1){fail4(`managed output is not a safe regular file: ${target}`)}if(stat.dev!==managedOutputDevice(expectedAgent,options)){fail4(`managed output crosses a nested filesystem mount: ${target}`)}const stable=readStableFile(target,MAX_TRANSACTION_FILE_BYTES);const size=Number(stable.stat.size);const backup=`${String(index).padStart(3,"0")}.bin`;return{receipt:{path:relativeTarget(target,options),state:"file",backup,sha256:(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex"),size,uid:Number(stable.stat.uid),gid:Number(stable.stat.gid),mode:Number(stable.stat.mode&0o7777n)},bytes:stable.bytes}}function snapshotDirectory(target,expectedAgent,options){validateExistingAncestors(import_node_path2.default.join(target,".receipt"),expectedAgent,options);let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT"){return{path:relativeTarget(target,options),state:"absent"}}fail4(`could not inspect managed output directory ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`managed output directory is unsafe: ${target}`)}if(stat.dev!==managedOutputDevice(expectedAgent,options)){fail4(`managed output directory crosses a nested filesystem mount: ${target}`)}return{path:relativeTarget(target,options),state:"directory",uid:stat.uid,gid:stat.gid,mode:modeOf2(stat)}}function atomicWriteTrustedFile(target,contents,mode,uid,gid){const parent=import_node_path2.default.dirname(target);const temporary=import_node_path2.default.join(parent,`.${import_node_path2.default.basename(target)}.${(0,import_node_crypto6.randomBytes)(12).toString("hex")}`);let descriptor;try{descriptor=import_node_fs2.default.openSync(temporary,import_node_fs2.default.constants.O_CREAT|import_node_fs2.default.constants.O_EXCL|import_node_fs2.default.constants.O_WRONLY|import_node_fs2.default.constants.O_NOFOLLOW,384);import_node_fs2.default.writeFileSync(descriptor,contents);import_node_fs2.default.fchownSync(descriptor,uid,gid);import_node_fs2.default.fchmodSync(descriptor,mode);import_node_fs2.default.fsyncSync(descriptor);import_node_fs2.default.closeSync(descriptor);descriptor=void 0;import_node_fs2.default.renameSync(temporary,target)}catch(error){if(descriptor!==void 0)import_node_fs2.default.closeSync(descriptor);try{import_node_fs2.default.unlinkSync(temporary)}catch{}fail4(`could not atomically write ${target}: ${error.message}`)}}function fsyncDirectory(directory){const descriptor=import_node_fs2.default.openSync(directory,import_node_fs2.default.constants.O_RDONLY);try{import_node_fs2.default.fsyncSync(descriptor)}finally{import_node_fs2.default.closeSync(descriptor)}}function canonicalManifest(manifest){return`${JSON.stringify(manifest,null,2)} `}function canonicalLegacyManifest(manifest){return`${JSON.stringify({schemaVersion:manifest.schemaVersion,agent:manifest.agent,profileFingerprint:manifest.profileFingerprint,files:manifest.files,directories:manifest.directories},null,2)} `}function canonicalCommitReceipt(receipt){return`${JSON.stringify(receipt,null,2)} -`}function requireExactKeys(record,keys){if(Object.keys(record).sort().join(",")!==[...keys].sort().join(",")){fail4("transaction manifest contains unexpected fields")}}function parseCommitReceipt(text){let parsed;try{parsed=JSON.parse(text)}catch{fail4("commit receipt is not valid JSON")}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail4("commit receipt must be an object")}const record=parsed;requireExactKeys(record,["agent","bootstrapIdentity","profileFingerprint","schemaVersion"]);if(record.schemaVersion!==TRANSACTION_SCHEMA_VERSION||!MANAGED_STARTUP_AGENTS.includes(String(record.agent))||typeof record.profileFingerprint!=="string"||!/^[a-f0-9]{64}$/u.test(record.profileFingerprint)||typeof record.bootstrapIdentity!=="string"||!/^[a-f0-9]{64}$/u.test(record.bootstrapIdentity)){fail4("commit receipt has an invalid envelope")}const receipt={schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:record.agent,profileFingerprint:record.profileFingerprint,bootstrapIdentity:record.bootstrapIdentity};if(canonicalCommitReceipt(receipt)!==text){fail4("commit receipt is not canonical")}return receipt}function safeMetadata(value){return Number.isSafeInteger(value)&&value>=0}function parseManifest(text){let parsed;try{parsed=JSON.parse(text)}catch{fail4("transaction manifest is not valid JSON")}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail4("transaction manifest must be an object")}const record=parsed;const hasBootstrapIdentity=Object.hasOwn(record,"bootstrapIdentity");requireExactKeys(record,hasBootstrapIdentity?["agent","bootstrapIdentity","directories","files","profileFingerprint","schemaVersion"]:["agent","directories","files","profileFingerprint","schemaVersion"]);const bootstrapIdentity=hasBootstrapIdentity?record.bootstrapIdentity:null;if(record.schemaVersion!==TRANSACTION_SCHEMA_VERSION||!MANAGED_STARTUP_AGENTS.includes(String(record.agent))||typeof record.profileFingerprint!=="string"||!/^[a-f0-9]{64}$/u.test(record.profileFingerprint)||!(bootstrapIdentity===null||typeof bootstrapIdentity==="string"&&/^[a-f0-9]{64}$/u.test(bootstrapIdentity))||!Array.isArray(record.files)||!Array.isArray(record.directories)||record.files.length>MAX_TRANSACTION_FILES||record.directories.length>MAX_TRANSACTION_FILES*4){fail4("transaction manifest has an invalid envelope")}const files=record.files.map(value=>{if(typeof value!=="object"||value===null||Array.isArray(value)){return fail4("transaction file receipt must be an object")}const receipt=value;if(typeof receipt.path!=="string"){return fail4("transaction file receipt path must be a string")}const receiptPath=safeRelativePath(receipt.path);if(receipt.state==="absent"){requireExactKeys(receipt,["path","state"]);return{path:receiptPath,state:"absent"}}requireExactKeys(receipt,["backup","gid","mode","path","sha256","size","state","uid"]);if(receipt.state!=="file"||typeof receipt.backup!=="string"||!/^[0-9]{3}\.bin$/u.test(receipt.backup)||typeof receipt.sha256!=="string"||!/^[a-f0-9]{64}$/u.test(receipt.sha256)||!safeMetadata(receipt.size)||receipt.size>MAX_TRANSACTION_FILE_BYTES||!safeMetadata(receipt.uid)||!safeMetadata(receipt.gid)||!safeMetadata(receipt.mode)||receipt.mode>4095){return fail4("transaction file receipt is invalid")}return{path:receiptPath,state:"file",backup:receipt.backup,sha256:receipt.sha256,size:receipt.size,uid:receipt.uid,gid:receipt.gid,mode:receipt.mode}});const directories=record.directories.map(value=>{if(typeof value!=="object"||value===null||Array.isArray(value)){return fail4("transaction directory receipt must be an object")}const receipt=value;if(typeof receipt.path!=="string"){return fail4("transaction directory receipt path must be a string")}const receiptPath=safeRelativePath(receipt.path);if(receipt.state==="absent"){requireExactKeys(receipt,["path","state"]);return{path:receiptPath,state:"absent"}}requireExactKeys(receipt,["gid","mode","path","state","uid"]);if(receipt.state!=="directory"||!safeMetadata(receipt.uid)||!safeMetadata(receipt.gid)||!safeMetadata(receipt.mode)||receipt.mode>4095){return fail4("transaction directory receipt is invalid")}return{path:receiptPath,state:"directory",uid:receipt.uid,gid:receipt.gid,mode:receipt.mode}});const filePaths=files.map(receipt=>receipt.path);const directoryPaths=directories.map(receipt=>receipt.path);const backupNames=files.filter(receipt=>receipt.state==="file").map(receipt=>receipt.backup);if(new Set(filePaths).size!==filePaths.length||new Set(directoryPaths).size!==directoryPaths.length||new Set(backupNames).size!==backupNames.length){fail4("transaction manifest contains duplicate receipts")}const manifest={schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:record.agent,profileFingerprint:record.profileFingerprint,bootstrapIdentity,files,directories};const canonical=hasBootstrapIdentity?canonicalManifest(manifest):canonicalLegacyManifest(manifest);if(canonical!==text){fail4("transaction manifest is not canonical")}return manifest}function requireTrustedTransactionPath(target,mode,options){const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||(mode===TRANSACTION_DIRECTORY_MODE?!stat.isDirectory():!stat.isFile())||!options.readOnlyReceipt&&(stat.uid!==options.trustedUid||stat.gid!==options.trustedGid)||modeOf2(stat)!==mode){fail4(`transaction artifact has unsafe metadata: ${target}`)}}function requireReadOnlyReceiptMount(target,options){if(!options.readOnlyReceipt)return;const probe=import_node_path2.default.join(target,".nemoclaw-write-probe");let descriptor;try{descriptor=import_node_fs2.default.openSync(probe,import_node_fs2.default.constants.O_CREAT|import_node_fs2.default.constants.O_EXCL|import_node_fs2.default.constants.O_WRONLY|import_node_fs2.default.constants.O_NOFOLLOW,384);import_node_fs2.default.closeSync(descriptor);descriptor=void 0;import_node_fs2.default.unlinkSync(probe)}catch(error){if(descriptor!==void 0)import_node_fs2.default.closeSync(descriptor);if(error.code==="EROFS")return;fail4("copied receipt must be mounted on a read-only filesystem")}fail4("copied receipt mount is writable")}function loadManifest(options){requireTransactionBoundaries(options);if(!pathExistsNoFollow(options.transactionDirectory))return null;requireTrustedTransactionPath(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE,options);requireReadOnlyReceiptMount(options.transactionDirectory,options);requireTrustedTransactionPath(options.backupDirectory,TRANSACTION_DIRECTORY_MODE,options);requireTrustedTransactionPath(options.manifestFile,TRANSACTION_FILE_MODE,options);const stable=readStableFile(options.manifestFile,MAX_MANIFEST_BYTES);if(!options.readOnlyReceipt&&(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid)||Number(stable.stat.mode&0o7777n)!==TRANSACTION_FILE_MODE){fail4("transaction manifest ownership changed while it was read")}return parseManifest(stable.bytes.toString("utf8"))}function transactionOptionsAt(options,transactionDirectory){return{...options,transactionDirectory,backupDirectory:import_node_path2.default.join(transactionDirectory,"backups"),manifestFile:import_node_path2.default.join(transactionDirectory,"manifest.json")}}function loadCommitReceipt(options){requireTransactionBoundaries(options);if(!pathExistsNoFollow(options.commitReceiptDirectory))return null;requireTrustedTransactionPath(options.commitReceiptDirectory,TRANSACTION_DIRECTORY_MODE,options);if(pathExistsNoFollow(options.commitReceiptFile)){requireReadOnlyReceiptMount(options.commitReceiptDirectory,options);requireTrustedTransactionPath(options.commitReceiptFile,TRANSACTION_FILE_MODE,options);const stable=readStableFile(options.commitReceiptFile,MAX_COMMIT_RECEIPT_BYTES);if(!options.readOnlyReceipt&&(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid)||Number(stable.stat.mode&0o7777n)!==TRANSACTION_FILE_MODE){fail4("commit receipt ownership changed while it was read")}return{receipt:parseCommitReceipt(stable.bytes.toString("utf8")),compact:true}}const stagedOptions=transactionOptionsAt(options,options.commitReceiptDirectory);const staged=loadManifest(stagedOptions);if(!staged||staged.bootstrapIdentity===null){fail4("durable commit staging receipt is incomplete")}verifyAllBackups(staged.files,stagedOptions);return{receipt:{schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:staged.agent,profileFingerprint:staged.profileFingerprint,bootstrapIdentity:staged.bootstrapIdentity},compact:false}}function verifyBackup(receipt,options){const backupPath=import_node_path2.default.join(options.backupDirectory,receipt.backup);requireTrustedTransactionPath(backupPath,TRANSACTION_FILE_MODE,options);const stable=readStableFile(backupPath,MAX_TRANSACTION_FILE_BYTES);const digest=(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex");if(stable.bytes.length!==receipt.size||digest!==receipt.sha256){fail4(`transaction backup does not match its receipt: ${receipt.path}`)}return stable.bytes}function verifyAllBackups(receipts,options){const backups=new Map;for(const receipt of receipts){if(receipt.state==="file"){backups.set(receipt.path,verifyBackup(receipt,options))}}return backups}function fileMatchesReceipt(target,receipt){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return false;fail4(`could not inspect managed output ${target}`)}if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1)return false;const stable=readStableFile(target,MAX_TRANSACTION_FILE_BYTES);return stable.bytes.length===receipt.size&&(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex")===receipt.sha256&&Number(stable.stat.uid)===receipt.uid&&Number(stable.stat.gid)===receipt.gid&&Number(stable.stat.mode&0o7777n)===receipt.mode}function directoryMatchesReceipt(target,receipt){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return false;fail4(`could not inspect managed output directory ${target}`)}return!stat.isSymbolicLink()&&stat.isDirectory()&&stat.uid===receipt.uid&&stat.gid===receipt.gid&&modeOf2(stat)===receipt.mode}function removeTransactionDirectory(options){requireTrustedTransactionPath(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE,options);import_node_fs2.default.rmSync(options.transactionDirectory,{force:false,recursive:true});fsyncDirectory(options.transactionParentDirectory);if(pathExistsNoFollow(options.transactionDirectory)){fail4("transaction directory remained after cleanup")}}function assertCommitReceiptMatches(receipt,expected){if(receipt.agent!==expected.agent||expected.profileFingerprint!==void 0&&receipt.profileFingerprint!==expected.profileFingerprint||receipt.bootstrapIdentity!==expected.bootstrapIdentity){fail4("durable commit receipt belongs to a different bootstrap attempt")}}function loadCommitStagingManifest(options){if(!pathExistsNoFollow(options.manifestFile))return null;requireTrustedTransactionPath(options.manifestFile,TRANSACTION_FILE_MODE,options);const stable=readStableFile(options.manifestFile,MAX_MANIFEST_BYTES);if(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid||Number(stable.stat.mode&0o7777n)!==TRANSACTION_FILE_MODE){fail4("durable commit staging manifest ownership changed while it was read")}return parseManifest(stable.bytes.toString("utf8"))}function retireInterruptedCommitReceiptWrites(receipt,options){const temporaryPattern=new RegExp(`^\\.${MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE.replace(".","\\.")}\\.[a-f0-9]{24}$`,"u");for(const entry of import_node_fs2.default.readdirSync(options.commitReceiptDirectory)){if(!temporaryPattern.test(entry))continue;const target=import_node_path2.default.join(options.commitReceiptDirectory,entry);const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==options.trustedUid||stat.gid!==options.trustedGid||![ATOMIC_TEMPORARY_FILE_MODE,TRANSACTION_FILE_MODE].includes(modeOf2(stat))){fail4("interrupted durable commit receipt write has unsafe metadata")}const stable=readStableFile(target,MAX_COMMIT_RECEIPT_BYTES);const mode=Number(stable.stat.mode&0o7777n);if(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid||![ATOMIC_TEMPORARY_FILE_MODE,TRANSACTION_FILE_MODE].includes(mode)){fail4("interrupted durable commit receipt write changed during verification")}if(stable.bytes.length>0){let interruptedReceipt=null;try{interruptedReceipt=parseCommitReceipt(stable.bytes.toString("utf8"))}catch{}if(interruptedReceipt)assertCommitReceiptMatches(interruptedReceipt,receipt)}import_node_fs2.default.unlinkSync(target);fsyncDirectory(options.commitReceiptDirectory)}}function compactDurableCommitReceipt(state,options){if(!state.compact){atomicWriteTrustedFile(options.commitReceiptFile,canonicalCommitReceipt(state.receipt),TRANSACTION_FILE_MODE,options.trustedUid,options.trustedGid);fsyncDirectory(options.commitReceiptDirectory)}retireInterruptedCommitReceiptWrites(state.receipt,options);const stagedOptions=transactionOptionsAt(options,options.commitReceiptDirectory);const manifestExists=pathExistsNoFollow(stagedOptions.manifestFile);const backupsExist=pathExistsNoFollow(stagedOptions.backupDirectory);const unexpectedBeforeCleanup=import_node_fs2.default.readdirSync(options.commitReceiptDirectory).filter(entry=>![MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE,import_node_path2.default.basename(stagedOptions.backupDirectory),import_node_path2.default.basename(stagedOptions.manifestFile)].includes(entry));if(unexpectedBeforeCleanup.length!==0){fail4("durable commit receipt directory contains unexpected artifacts")}if(manifestExists){const staged=loadCommitStagingManifest(stagedOptions);if(!staged||staged.bootstrapIdentity===null){fail4("durable commit staging receipt disappeared during cleanup")}assertCommitReceiptMatches(state.receipt,{agent:staged.agent,profileFingerprint:staged.profileFingerprint,bootstrapIdentity:staged.bootstrapIdentity})}if(backupsExist){requireTrustedTransactionPath(stagedOptions.backupDirectory,TRANSACTION_DIRECTORY_MODE,options);import_node_fs2.default.rmSync(stagedOptions.backupDirectory,{force:false,recursive:true});fsyncDirectory(options.commitReceiptDirectory)}if(manifestExists){requireTrustedTransactionPath(stagedOptions.manifestFile,TRANSACTION_FILE_MODE,options);import_node_fs2.default.unlinkSync(stagedOptions.manifestFile);fsyncDirectory(options.commitReceiptDirectory)}const unexpected=import_node_fs2.default.readdirSync(options.commitReceiptDirectory).filter(entry=>entry!==MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE);if(unexpected.length!==0){fail4("durable commit receipt directory contains unexpected artifacts")}const verified=loadCommitReceipt(options);if(!verified?.compact)fail4("durable commit receipt did not compact successfully");assertCommitReceiptMatches(verified.receipt,state.receipt)}function beginManagedStartupSharedStateTransaction(profile,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);if(options.readOnlyReceipt){fail4("cannot begin a transaction from a read-only rollback receipt")}requireTransactionBoundaries(options);const profileFingerprint=fingerprintManagedStartupProfile(profile);const committed=loadCommitReceipt(options);if(committed){if(options.bootstrapIdentity===null){fail4("a durable managed bootstrap commit receipt already exists")}assertCommitReceiptMatches(committed.receipt,{agent:profile.agent,profileFingerprint,bootstrapIdentity:options.bootstrapIdentity});fail4("this managed bootstrap attempt is already durably committed")}const pending=loadManifest(options);if(pending){if(pending.agent!==profile.agent||pending.profileFingerprint!==profileFingerprint||pending.bootstrapIdentity!==options.bootstrapIdentity){fail4("a pending managed startup transaction belongs to a different agent, profile fingerprint, or bootstrap attempt")}verifyAllBackups(pending.files,options);return false}const targets=managedOutputTargets(profile,options);if(targets.files.length>MAX_TRANSACTION_FILES){fail4("managed startup transaction has too many file targets")}const snapshots=targets.files.map((target,index)=>snapshotFile(target,index,profile.agent,options));const totalBytes=snapshots.reduce((sum,snapshot)=>sum+(snapshot.bytes?.length??0),0);if(totalBytes>MAX_TRANSACTION_TOTAL_BYTES){fail4("managed startup transaction backup exceeds the total size limit")}const directories=targets.directories.map(target=>snapshotDirectory(target,profile.agent,options));const manifest={schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:profile.agent,profileFingerprint,bootstrapIdentity:options.bootstrapIdentity,files:snapshots.map(({receipt})=>receipt),directories};let createdTransactionIdentity;try{import_node_fs2.default.mkdirSync(options.transactionDirectory,{mode:TRANSACTION_DIRECTORY_MODE});const created=import_node_fs2.default.lstatSync(options.transactionDirectory,{bigint:true});if(!created.isDirectory()||created.isSymbolicLink()){fail4("new transaction path is not a directory")}createdTransactionIdentity={dev:created.dev,ino:created.ino,uid:created.uid,gid:created.gid};import_node_fs2.default.chownSync(options.transactionDirectory,options.trustedUid,options.trustedGid);import_node_fs2.default.chmodSync(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE);fsyncDirectory(options.transactionParentDirectory);import_node_fs2.default.mkdirSync(options.backupDirectory,{mode:TRANSACTION_DIRECTORY_MODE});import_node_fs2.default.chownSync(options.backupDirectory,options.trustedUid,options.trustedGid);import_node_fs2.default.chmodSync(options.backupDirectory,TRANSACTION_DIRECTORY_MODE);fsyncDirectory(options.transactionDirectory);for(const snapshot of snapshots){if(snapshot.receipt.state!=="file"||snapshot.bytes===null)continue;atomicWriteTrustedFile(import_node_path2.default.join(options.backupDirectory,snapshot.receipt.backup),snapshot.bytes,TRANSACTION_FILE_MODE,options.trustedUid,options.trustedGid)}fsyncDirectory(options.backupDirectory);atomicWriteTrustedFile(options.manifestFile,canonicalManifest(manifest),TRANSACTION_FILE_MODE,options.trustedUid,options.trustedGid);fsyncDirectory(options.transactionDirectory);loadManifest(options)}catch(error){try{if(createdTransactionIdentity&&pathExistsNoFollow(options.transactionDirectory)){const current=import_node_fs2.default.lstatSync(options.transactionDirectory,{bigint:true});if(!current.isSymbolicLink()&¤t.isDirectory()&¤t.dev===createdTransactionIdentity.dev&¤t.ino===createdTransactionIdentity.ino&¤t.uid===createdTransactionIdentity.uid&¤t.gid===createdTransactionIdentity.gid){import_node_fs2.default.chmodSync(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE);import_node_fs2.default.chownSync(options.transactionDirectory,options.trustedUid,options.trustedGid)}requireTrustedTransactionPath(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE,options);import_node_fs2.default.rmSync(options.transactionDirectory,{force:true,recursive:true})}}catch{}throw error}return true}function ensureOriginalDirectories(receipts,expectedAgent,options){for(const receipt of receipts){if(receipt.state!=="directory")continue;const target=absoluteTarget(receipt.path,options);validateExistingAncestors(import_node_path2.default.join(target,".restore"),expectedAgent,options);let stat=null;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code!=="ENOENT"){fail4(`could not inspect restore directory ${target}`)}}if(stat&&(stat.isSymbolicLink()||!stat.isDirectory())){fail4(`restore directory is unsafe: ${target}`)}if(stat&&directoryMatchesReceipt(target,receipt))continue;if(!stat)import_node_fs2.default.mkdirSync(target,{mode:receipt.mode});import_node_fs2.default.chownSync(target,receipt.uid,receipt.gid);import_node_fs2.default.chmodSync(target,receipt.mode)}}function restoreFiles(receipts,backups,expectedAgent,options){for(const receipt of receipts){const target=absoluteTarget(receipt.path,options);validateExistingAncestors(target,expectedAgent,options);if(receipt.state==="absent"){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")continue;fail4(`could not inspect new managed output ${target}`)}if(stat.isDirectory()){fail4(`new managed output unexpectedly became a directory: ${target}`)}import_node_fs2.default.unlinkSync(target);continue}if(fileMatchesReceipt(target,receipt))continue;const bytes=backups.get(receipt.path);if(!bytes)fail4(`verified transaction backup is missing: ${receipt.path}`);let current=null;try{current=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code!=="ENOENT"){fail4(`could not inspect managed output before restore: ${target}`)}}if(current?.isDirectory()){fail4(`managed output unexpectedly became a directory: ${target}`)}atomicWriteTrustedFile(target,bytes,receipt.mode,receipt.uid,receipt.gid)}}function restoreDirectoryMetadata(receipts,options){for(const receipt of[...receipts].reverse()){const target=absoluteTarget(receipt.path,options);if(receipt.state==="absent"){try{import_node_fs2.default.rmdirSync(target)}catch(error){if(error.code==="ENOENT")continue;fail4(`could not remove newly created managed directory ${target}`)}continue}if(directoryMatchesReceipt(target,receipt))continue;const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`managed directory changed type during restore: ${target}`)}import_node_fs2.default.chownSync(target,receipt.uid,receipt.gid);import_node_fs2.default.chmodSync(target,receipt.mode)}}function verifyRestoration(manifest,options){for(const receipt of manifest.files){const target=absoluteTarget(receipt.path,options);if(receipt.state==="absent"){if(pathExistsNoFollow(target)){fail4(`new managed output remained after rollback: ${target}`)}continue}const stable=readStableFile(target,MAX_TRANSACTION_FILE_BYTES);if(stable.bytes.length!==receipt.size||(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex")!==receipt.sha256||Number(stable.stat.uid)!==receipt.uid||Number(stable.stat.gid)!==receipt.gid||Number(stable.stat.mode&0o7777n)!==receipt.mode){fail4(`managed output was not restored exactly: ${target}`)}}for(const receipt of manifest.directories){const target=absoluteTarget(receipt.path,options);if(receipt.state==="absent"){if(pathExistsNoFollow(target)){fail4(`new managed directory remained after rollback: ${target}`)}continue}const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isDirectory()||stat.uid!==receipt.uid||stat.gid!==receipt.gid||modeOf2(stat)!==receipt.mode){fail4(`managed directory metadata was not restored exactly: ${target}`)}}}function rollbackManagedStartupSharedStateTransaction(expectedAgent,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);const committed=loadCommitReceipt(options);if(committed){if(options.bootstrapIdentity===null){fail4("shared state is already durably committed")}assertCommitReceiptMatches(committed.receipt,{agent:expectedAgent,bootstrapIdentity:options.bootstrapIdentity});fail4("shared state is already durably committed and cannot be rolled back")}const manifest=loadManifest(options);if(!manifest)return false;if(manifest.agent!==expectedAgent){fail4(`pending transaction targets ${manifest.agent}, expected ${expectedAgent}`)}if(manifest.bootstrapIdentity!==options.bootstrapIdentity){fail4("pending transaction belongs to a different bootstrap attempt")}const backups=verifyAllBackups(manifest.files,options);ensureOriginalDirectories(manifest.directories,expectedAgent,options);restoreFiles(manifest.files,backups,expectedAgent,options);restoreDirectoryMetadata(manifest.directories,options);verifyRestoration(manifest,options);if(!options.readOnlyReceipt){removeTransactionDirectory(options)}return true}function commitManagedStartupSharedStateTransaction(expectedAgent,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);if(options.readOnlyReceipt){fail4("cannot commit a read-only rollback receipt")}const committed=loadCommitReceipt(options);if(committed){if(options.bootstrapIdentity===null){fail4("durable commit receipt is missing its expected bootstrap identity")}assertCommitReceiptMatches(committed.receipt,{agent:expectedAgent,bootstrapIdentity:options.bootstrapIdentity});compactDurableCommitReceipt(committed,options);return true}const manifest=loadManifest(options);if(!manifest)return false;if(manifest.agent!==expectedAgent){fail4(`pending transaction targets ${manifest.agent}, expected ${expectedAgent}`)}if(manifest.bootstrapIdentity!==options.bootstrapIdentity){fail4("pending transaction belongs to a different bootstrap attempt")}if(manifest.bootstrapIdentity===null){removeTransactionDirectory(options);return true}verifyAllBackups(manifest.files,options);if(pathExistsNoFollow(options.commitReceiptDirectory)){fail4("durable commit receipt path appeared before transaction commit")}try{import_node_fs2.default.renameSync(options.transactionDirectory,options.commitReceiptDirectory);fsyncDirectory(options.transactionParentDirectory)}catch(error){fail4(`could not atomically establish durable commit state: ${error.message}`)}const renamed=loadCommitReceipt(options);if(!renamed)fail4("durable commit state disappeared after atomic rename");assertCommitReceiptMatches(renamed.receipt,{agent:expectedAgent,profileFingerprint:manifest.profileFingerprint,bootstrapIdentity:manifest.bootstrapIdentity});compactDurableCommitReceipt(renamed,options);return true}function clearManagedStartupSharedStateCommitReceipt(expectedAgent,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);if(options.readOnlyReceipt){fail4("cannot clear a durable commit from a read-only receipt")}if(options.bootstrapIdentity===null){fail4("durable commit cleanup requires its bootstrap identity")}const committed=loadCommitReceipt(options);if(!committed)return false;assertCommitReceiptMatches(committed.receipt,{agent:expectedAgent,bootstrapIdentity:options.bootstrapIdentity});compactDurableCommitReceipt(committed,options);requireTrustedTransactionPath(options.commitReceiptDirectory,TRANSACTION_DIRECTORY_MODE,options);requireTrustedTransactionPath(options.commitReceiptFile,TRANSACTION_FILE_MODE,options);const entries=import_node_fs2.default.readdirSync(options.commitReceiptDirectory);if(entries.length!==1||entries[0]!==MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE){fail4("durable commit receipt directory contains unexpected artifacts")}import_node_fs2.default.rmSync(options.commitReceiptDirectory,{force:false,recursive:true});fsyncDirectory(options.transactionParentDirectory);if(pathExistsNoFollow(options.commitReceiptDirectory)){fail4("durable commit receipt remained after cleanup")}return true}function getManagedStartupSharedStateTransactionStatus(expected,inputOptions={}){const options=resolveOptions({...inputOptions,bootstrapIdentity:expected.bootstrapIdentity});requireTransactionIdentity(options);const manifest=loadManifest(options);if(manifest){if(manifest.agent!==expected.agent||manifest.profileFingerprint!==expected.profileFingerprint||manifest.bootstrapIdentity!==expected.bootstrapIdentity){fail4("pending transaction does not match the expected agent, profile fingerprint, or bootstrap identity")}verifyAllBackups(manifest.files,options);return"pending"}const committed=loadCommitReceipt(options);if(!committed)return"none";assertCommitReceiptMatches(committed.receipt,expected);return"committed"}var MANAGED_STARTUP_PROFILE_ENV="NEMOCLAW_STARTUP_PROFILE_B64";var MANAGED_STARTUP_CA_ENV="NEMOCLAW_CORPORATE_CA_B64";var MANAGED_STARTUP_EXCHANGE_DIRECTORY="/tmp";var MANAGED_STARTUP_RUNTIME_ENV_FILE=`${MANAGED_STARTUP_EXCHANGE_DIRECTORY}/nemoclaw-managed-startup-runtime.env`;var MANAGED_STARTUP_RUNTIME_EXECUTABLE="/usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs";var MANAGED_STARTUP_MERGED_CA_FILE=`${MANAGED_STARTUP_EXCHANGE_DIRECTORY}/nemoclaw-managed-startup-ca-bundle.pem`;var MANAGED_STARTUP_COMPLETION_FILE=`${MANAGED_STARTUP_EXCHANGE_DIRECTORY}/nemoclaw-managed-startup-complete.json`;var MANAGED_STARTUP_CORPORATE_CA_FILE="/usr/local/share/nemoclaw/corporate-ca.pem";var MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY="/usr/local/share/ca-certificates";var MANAGED_STARTUP_SYSTEM_CA_ANCHOR_RE=/^nemoclaw-corporate-ca-[0-9]{2}\.crt$/u;var SYSTEM_CA_BUNDLE_FILE="/etc/ssl/certs/ca-certificates.crt";var UPDATE_CA_CERTIFICATES_EXECUTABLE="/usr/sbin/update-ca-certificates";var MANAGED_STARTUP_TLS_ENV_NAMES=new Set(["CURL_CA_BUNDLE","GIT_SSL_CAINFO","NODE_EXTRA_CA_CERTS","REQUESTS_CA_BUNDLE","SSL_CERT_FILE"]);var MESSAGING_RUNTIME_PLAN_FILE="/usr/local/share/nemoclaw/messaging-runtime-plan.json";var ROOT_STATE_PARENT="/var/lib/nemoclaw";var ROOT_RUNTIME_DIRECTORY="/run/nemoclaw";var ROOT_OWNED_DIRECTORY_MODE=493;var MAX_TRUST_BUNDLE_BYTES=4*1024*1024;var HERMES_MANAGED_CONFIG_FILES=["/sandbox/.hermes/config.yaml","/sandbox/.hermes/.env"];var HERMES_GENERATED_MANAGED_POLICY_FILE="/sandbox/.hermes/managed-policy.json";var HERMES_INSTALLED_MANAGED_POLICY_FILE="/usr/local/share/nemoclaw/hermes-managed-policy.json";var MAX_HERMES_MANAGED_POLICY_BYTES=4*1024*1024;var FIXED_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";var SHA256_RE4=/^[a-f0-9]{64}$/u;var MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION=1;var MAX_MANAGED_STARTUP_COMPLETION_BYTES=4096;var MAX_MANAGED_STARTUP_RUNTIME_ENVIRONMENT_BYTES=512*1024;var defaultWorkspaceRootApplyDeps={lstat:target=>import_node_fs3.default.lstatSync(target),chown:(target,uid,gid)=>import_node_fs3.default.chownSync(target,uid,gid),chmod:(target,mode)=>import_node_fs3.default.chmodSync(target,mode)};function normalizeManagedStartupWorkspaceRoot(agent,deps=defaultWorkspaceRootApplyDeps){const target=managedImageRuntimeIdentity(agent).workdir;const before=deps.lstat(target);if(before.isSymbolicLink()||!before.isDirectory()){fail5("managed startup workspace root must be one real directory")}const expected=managedStartupWorkspaceRoot({agent,agentIdentity:managedImageRuntimeIdentity(agent)});try{deps.chown(target,expected.uid,expected.gid);deps.chmod(target,expected.mode)}catch{fail5("managed startup workspace-root normalization failed")}const after=deps.lstat(target);if(after.isSymbolicLink()||!after.isDirectory()||after.uid!==expected.uid||after.gid!==expected.gid||(after.mode&4095)!==expected.mode){fail5("managed startup workspace root does not match its declared posture")}}var ManagedStartupImageActionPlanError=class extends Error{constructor(message){super(`Cannot build managed startup image action plan: ${message}`);this.name="ManagedStartupImageActionPlanError"}};var ManagedStartupImageRuntimeError=class extends Error{constructor(message){super(`Managed startup image application failed: ${message}`);this.name="ManagedStartupImageRuntimeError"}};function failActionPlan(message){throw new ManagedStartupImageActionPlanError(message)}function exactActionPlanAgent(value){if(MANAGED_STARTUP_AGENTS.includes(value)){return value}return failActionPlan(`unsupported agent ${JSON.stringify(value)}`)}function fail5(message){throw new ManagedStartupImageRuntimeError(message)}function validateManagedStartupApplicationRuntimePlan(plan){if(typeof plan!=="object"||plan===null){return fail5("application runtime plan must be an object")}const exportEnvironment=plan.exportEnvironment;const unsetEnvironment=plan.unsetEnvironment;if(typeof exportEnvironment!=="object"||exportEnvironment===null||Array.isArray(exportEnvironment)||!Array.isArray(unsetEnvironment)){return fail5("application runtime plan must contain exports and unsets")}const exports2={};for(const[name,value]of Object.entries(exportEnvironment)){if(!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)){return fail5(`invalid application runtime environment key ${JSON.stringify(name)}`)}if(typeof value!=="string"||value.includes("\0")||/[\r\n]/u.test(value)){return fail5(`application runtime environment value for ${name} must be single-line text`)}exports2[name]=value}const unsets=new Set;for(const name of unsetEnvironment){if(typeof name!=="string"||!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)){return fail5(`invalid application runtime unset ${JSON.stringify(name)}`)}if(unsets.has(name)){return fail5(`duplicate application runtime unset ${name}`)}if(Object.hasOwn(exports2,name)){return fail5(`application runtime cannot both export and unset ${name}`)}unsets.add(name)}return Object.freeze({exportEnvironment:Object.freeze(Object.fromEntries(Object.entries(exports2).sort(([left],[right])=>left.localeCompare(right)))),unsetEnvironment:Object.freeze([...unsets].sort())})}function applyManagedStartupCommandEnvironmentPlan(environment,plan){const validated=validateManagedStartupApplicationRuntimePlan(plan);const applied={...environment};for(const name of[...Object.keys(validated.exportEnvironment),...validated.unsetEnvironment]){delete applied[name]}return applied}function exactAgent2(value){if(MANAGED_STARTUP_AGENTS.includes(value)){return value}return fail5(`unsupported agent ${JSON.stringify(value)}`)}function managedTransactionProfile(expectedAgentInput,env=process.env){requireRoot();const expectedAgent=exactAgent2(expectedAgentInput);if(env.NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION!=="1"){fail5("shared-state transactions require a complete managed image")}const encodedProfile=env[MANAGED_STARTUP_PROFILE_ENV];if(!encodedProfile)fail5(`${MANAGED_STARTUP_PROFILE_ENV} is required`);const profile=decodeManagedStartupProfile(encodedProfile);if(profile.agent!==expectedAgent){fail5(`shared-state transaction profile targets ${profile.agent}, expected ${expectedAgent}`)}return profile}function requireRoot(){if(process.geteuid?.()!==0){fail5("managed startup requires container effective uid 0")}}function modeOf3(stat){return stat.mode&511}function requireManagedStartupExchangeDirectory(exchangeDirectory=MANAGED_STARTUP_EXCHANGE_DIRECTORY){const stat=import_node_fs3.default.lstatSync(exchangeDirectory);if(stat.isSymbolicLink()||!stat.isDirectory()||stat.uid!==0||stat.gid!==0||(stat.mode&4095)!==1023){fail5("managed startup exchange directory must be root:root mode 1777")}}function requireRootOwnedDirectory(target,mode){let stat;try{stat=import_node_fs3.default.lstatSync(target)}catch{fail5(`required root-owned directory is missing: ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()||stat.uid!==0||stat.gid!==0||modeOf3(stat)!==mode){fail5(`${target} must be a root:root directory with mode ${mode.toString(8)}`)}}function ensureRootOwnedDirectory(target,mode=ROOT_OWNED_DIRECTORY_MODE){const parent=import_node_path3.default.dirname(target);const parentStat=import_node_fs3.default.lstatSync(parent);if(parentStat.isSymbolicLink()||!parentStat.isDirectory()||parentStat.uid!==0||parentStat.gid!==0||(modeOf3(parentStat)&18)!==0){fail5(`refusing unsafe parent directory for ${target}`)}try{import_node_fs3.default.mkdirSync(target,{mode});import_node_fs3.default.chownSync(target,0,0);import_node_fs3.default.chmodSync(target,mode)}catch(error){if(error.code!=="EEXIST"){fail5(`could not create ${target}`)}}requireRootOwnedDirectory(target,mode)}function requireSafeExistingRootTarget(target){let stat;try{stat=import_node_fs3.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return;fail5(`could not inspect ${target}`)}if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==0||stat.gid!==0){fail5(`refusing to replace unsafe root-owned file ${target}`)}}function atomicWriteRootFile(target,contents,mode){const parent=import_node_path3.default.dirname(target);const parentStat=import_node_fs3.default.lstatSync(parent);const trustedStickyExchange=parent===MANAGED_STARTUP_EXCHANGE_DIRECTORY&&(parentStat.mode&4095)===1023;if(parentStat.isSymbolicLink()||!parentStat.isDirectory()||parentStat.uid!==0||parentStat.gid!==0||(modeOf3(parentStat)&18)!==0&&!trustedStickyExchange){fail5(`refusing unsafe root-owned file parent ${parent}`)}requireSafeExistingRootTarget(target);const temporary=import_node_path3.default.join(parent,`.${import_node_path3.default.basename(target)}.${(0,import_node_crypto7.randomBytes)(12).toString("hex")}`);let descriptor;try{descriptor=import_node_fs3.default.openSync(temporary,import_node_fs3.default.constants.O_CREAT|import_node_fs3.default.constants.O_EXCL|import_node_fs3.default.constants.O_WRONLY|import_node_fs3.default.constants.O_NOFOLLOW,384);import_node_fs3.default.fchownSync(descriptor,0,0);import_node_fs3.default.writeFileSync(descriptor,contents);import_node_fs3.default.fchmodSync(descriptor,mode);import_node_fs3.default.fsyncSync(descriptor);import_node_fs3.default.closeSync(descriptor);descriptor=void 0;import_node_fs3.default.renameSync(temporary,target)}catch(error){if(descriptor!==void 0)import_node_fs3.default.closeSync(descriptor);try{import_node_fs3.default.unlinkSync(temporary)}catch{}fail5(`could not atomically write ${target}: ${error.message}`)}const stat=import_node_fs3.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==0||stat.gid!==0||modeOf3(stat)!==mode){fail5(`root-owned output failed metadata verification: ${target}`)}}function removeSafeRootFile(target){requireSafeExistingRootTarget(target);try{import_node_fs3.default.unlinkSync(target)}catch(error){if(error.code!=="ENOENT"){fail5(`could not remove ${target}`)}}}function trustedExecutable(target){try{const stat=import_node_fs3.default.lstatSync(target);return!stat.isSymbolicLink()&&stat.isFile()&&stat.uid===0&&stat.gid===0&&(modeOf3(stat)&18)===0&&(modeOf3(stat)&73)!==0}catch{return false}}function readSandboxIdentity(){const readId=flag=>{const result=(0,import_node_child_process.spawnSync)("/usr/bin/id",[flag,"sandbox"],{encoding:"utf8",env:{PATH:FIXED_PATH}});const value=result.stdout.trim();if(result.status!==0||!/^[1-9][0-9]*$/u.test(value)){fail5("could not resolve the sandbox account")}return value};return{uid:readId("-u"),gid:readId("-g")}}function managedStartupSandboxPrefix(){if(trustedExecutable("/usr/bin/setpriv")){const identity=readSandboxIdentity();return["/usr/bin/setpriv",`--reuid=${identity.uid}`,`--regid=${identity.gid}`,"--init-groups","--"]}return fail5("a trusted setpriv executable is required")}function commandEnvironment(configurationEnvironment,applicationRuntime){const env=applyManagedStartupCommandEnvironmentPlan({...process.env,...configurationEnvironment,HOME:"/sandbox",PATH:FIXED_PATH,NPM_CONFIG_OFFLINE:"true",npm_config_offline:"true",PIP_DISABLE_PIP_VERSION_CHECK:"1",PIP_NO_INDEX:"1",UV_OFFLINE:"1"},applicationRuntime);delete env[MANAGED_STARTUP_PROFILE_ENV];delete env[MANAGED_STARTUP_CA_ENV];return env}function execute(argv,runAs,configurationEnvironment,applicationRuntime,capture=false){if(argv.length===0)fail5("refusing an empty managed startup command");const command=runAs==="sandbox"?[...managedStartupSandboxPrefix(),...argv]:[...argv];const result=(0,import_node_child_process.spawnSync)(command[0],command.slice(1),{encoding:"utf8",env:commandEnvironment(configurationEnvironment,applicationRuntime),stdio:capture?"pipe":"inherit"});if(result.error){fail5(`could not execute ${argv[0]}: ${result.error.message}`)}if(result.status!==0){const detail=capture?`: ${(result.stderr||result.stdout).trim()}`:"";fail5(`${argv[0]} exited with status ${String(result.status??"unknown")}${detail}`)}return{status:result.status,stdout:result.stdout??"",stderr:result.stderr??""}}function generatorCommand(agent){switch(agent){case"openclaw":return["/usr/local/bin/node","/scripts/generate-openclaw-config.mts"];case"hermes":return["/usr/local/bin/node","/opt/nemoclaw-hermes-config/generate-config.ts"];case"langchain-deepagents-code":return["/usr/local/bin/node","/opt/nemoclaw-deepagents-code/generate-config.ts"];case"pi":return["/usr/local/bin/node","/opt/nemoclaw-pi/generate-config.ts"]}}function messagingCommand(agent,phase,mode){return["/usr/local/bin/node","/src/lib/messaging/applier/build/messaging-build-applier.mts","--agent",agent,"--phase",phase,"--mode",mode,...phase==="post-agent-install"?["--managed-startup-runtime"]:[]]}function assertActionAgent(inputAgent,actionAgent){if(inputAgent!==actionAgent){failActionPlan(`action for ${actionAgent} cannot be used by ${inputAgent}`)}}function buildManagedStartupImageActionPlan(input){const inputAgent=exactActionPlanAgent(input.agent);const commands=[];let dashboardActions=0;let generateActions=0;let runtimeMessagingActions=0;let postMessagingActions=0;for(const action of input.actions){switch(action.kind){case"configure-dashboard":{if(action.dashboard.agent!==input.agent){failActionPlan(`dashboard for ${action.dashboard.agent} cannot be used by ${input.agent}`)}dashboardActions+=1;break}case"generate-agent-config":{assertActionAgent(inputAgent,exactActionPlanAgent(action.agent));if(action.runAs!=="sandbox"){failActionPlan("agent configuration generation must run as sandbox")}generateActions+=1;commands.push({action:"generate-agent-config",runAs:action.runAs,argv:generatorCommand(action.agent)});break}case"apply-messaging-plan":{assertActionAgent(inputAgent,exactActionPlanAgent(action.agent));if(action.mode!=="apply"&&action.mode!=="clear"){failActionPlan("messaging intent must be apply or clear")}if(action.phase==="runtime-setup"){if(action.runAs!=="root"){failActionPlan("messaging runtime setup must run as root")}runtimeMessagingActions+=1;commands.push({action:"messaging-runtime-setup",runAs:action.runAs,argv:messagingCommand(action.agent,action.phase,action.mode)})}else if(action.phase==="post-agent-install"){if(action.runAs!=="sandbox"){failActionPlan("messaging post-agent configuration must run as sandbox")}postMessagingActions+=1;commands.push({action:"messaging-post-agent-install",runAs:action.runAs,argv:messagingCommand(action.agent,action.phase,action.mode)})}else{failActionPlan("unsupported messaging construction phase")}break}default:failActionPlan("unsupported managed startup construction action")}}if(dashboardActions!==1){failActionPlan("exactly one dashboard construction action is required")}if(generateActions!==1){failActionPlan("exactly one agent config construction action is required")}const supportsMessaging=MANAGED_STARTUP_MESSAGING_AGENTS.includes(inputAgent);const expectedMessagingActions=supportsMessaging?1:0;if(runtimeMessagingActions!==expectedMessagingActions||postMessagingActions!==expectedMessagingActions){failActionPlan(`${inputAgent} requires ${String(expectedMessagingActions)} action for each messaging phase`)}const expectedOrder=supportsMessaging?["messaging-runtime-setup","generate-agent-config","messaging-post-agent-install"]:["generate-agent-config"];if(commands.some((command,index)=>command.action!==expectedOrder[index])){failActionPlan(`${inputAgent} image actions are not in the required construction order`)}return Object.freeze(commands.map(command=>Object.freeze({...command,argv:Object.freeze([...command.argv])})))}function prepareMessagingRuntimeTarget(mode){if(mode==="clear"){removeSafeRootFile(MESSAGING_RUNTIME_PLAN_FILE);return}requireSafeExistingRootTarget(MESSAGING_RUNTIME_PLAN_FILE);try{import_node_fs3.default.unlinkSync(MESSAGING_RUNTIME_PLAN_FILE)}catch(error){if(error.code!=="ENOENT"){fail5("could not prepare the messaging runtime-plan target")}}}function verifyMessagingRuntimeTarget(mode){if(mode==="clear"){if(import_node_fs3.default.existsSync(MESSAGING_RUNTIME_PLAN_FILE)){fail5("clear messaging profile left a runtime-plan artifact")}return}const stat=import_node_fs3.default.lstatSync(MESSAGING_RUNTIME_PLAN_FILE);if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==0||stat.gid!==0||modeOf3(stat)!==420){fail5("messaging runtime-plan artifact failed root ownership validation")}}function runInternalSandboxAction(action,configurationEnvironment,applicationRuntime,extraEnvironment={}){execute(["/usr/local/bin/node",MANAGED_STARTUP_RUNTIME_EXECUTABLE,`--internal-${action}`],"sandbox",{...configurationEnvironment,...extraEnvironment},applicationRuntime)}function validateOpenClawConfiguration(configurationEnvironment,applicationRuntime){const validation=execute(["/usr/local/bin/openclaw","config","validate","--json"],"sandbox",{...configurationEnvironment,OPENCLAW_CONFIG_PATH:"/sandbox/.openclaw/openclaw.json"},applicationRuntime,true);let parsed;try{parsed=JSON.parse(validation.stdout)}catch{fail5("OpenClaw config validation did not emit JSON")}if(typeof parsed!=="object"||parsed===null||parsed.valid!==true){fail5("OpenClaw rejected the generated managed startup config")}}function sameStableFileMetadata(left,right){return left.dev===right.dev&&left.ino===right.ino&&left.mode===right.mode&&left.nlink===right.nlink&&left.uid===right.uid&&left.gid===right.gid&&left.size===right.size&&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs}function readStableRegularFileSnapshot(target,maxBytes){if(typeof import_node_fs3.default.constants.O_NOFOLLOW!=="number"){fail5("O_NOFOLLOW is unavailable for managed startup file reads")}const nonblock=typeof import_node_fs3.default.constants.O_NONBLOCK==="number"?import_node_fs3.default.constants.O_NONBLOCK:0;let descriptor;try{descriptor=import_node_fs3.default.openSync(target,import_node_fs3.default.constants.O_RDONLY|import_node_fs3.default.constants.O_NOFOLLOW|nonblock)}catch(error){if(error.code==="ENOENT")throw error;fail5(`refusing unsafe or unreadable file ${target}`)}try{const before=import_node_fs3.default.fstatSync(descriptor,{bigint:true});if(!before.isFile()||before.nlink!==1n||before.size<1n||before.size>BigInt(maxBytes)){fail5(`refusing unsafe or oversized file ${target}`)}const bytes=Buffer.alloc(Number(before.size));let offset=0;while(offset`${block.trim()} +`}function requireExactKeys(record,keys){if(Object.keys(record).sort().join(",")!==[...keys].sort().join(",")){fail4("transaction manifest contains unexpected fields")}}function parseCommitReceipt(text){let parsed;try{parsed=JSON.parse(text)}catch{fail4("commit receipt is not valid JSON")}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail4("commit receipt must be an object")}const record=parsed;requireExactKeys(record,["agent","bootstrapIdentity","profileFingerprint","schemaVersion"]);if(record.schemaVersion!==TRANSACTION_SCHEMA_VERSION||!MANAGED_STARTUP_AGENTS.includes(String(record.agent))||typeof record.profileFingerprint!=="string"||!/^[a-f0-9]{64}$/u.test(record.profileFingerprint)||typeof record.bootstrapIdentity!=="string"||!/^[a-f0-9]{64}$/u.test(record.bootstrapIdentity)){fail4("commit receipt has an invalid envelope")}const receipt={schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:record.agent,profileFingerprint:record.profileFingerprint,bootstrapIdentity:record.bootstrapIdentity};if(canonicalCommitReceipt(receipt)!==text){fail4("commit receipt is not canonical")}return receipt}function safeMetadata(value){return Number.isSafeInteger(value)&&value>=0}function parseManifest(text){let parsed;try{parsed=JSON.parse(text)}catch{fail4("transaction manifest is not valid JSON")}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail4("transaction manifest must be an object")}const record=parsed;const hasBootstrapIdentity=Object.hasOwn(record,"bootstrapIdentity");requireExactKeys(record,hasBootstrapIdentity?["agent","bootstrapIdentity","directories","files","profileFingerprint","schemaVersion"]:["agent","directories","files","profileFingerprint","schemaVersion"]);const bootstrapIdentity=hasBootstrapIdentity?record.bootstrapIdentity:null;if(record.schemaVersion!==TRANSACTION_SCHEMA_VERSION||!MANAGED_STARTUP_AGENTS.includes(String(record.agent))||typeof record.profileFingerprint!=="string"||!/^[a-f0-9]{64}$/u.test(record.profileFingerprint)||!(bootstrapIdentity===null||typeof bootstrapIdentity==="string"&&/^[a-f0-9]{64}$/u.test(bootstrapIdentity))||!Array.isArray(record.files)||!Array.isArray(record.directories)||record.files.length>MAX_TRANSACTION_FILES||record.directories.length>MAX_TRANSACTION_FILES*4){fail4("transaction manifest has an invalid envelope")}const files=record.files.map(value=>{if(typeof value!=="object"||value===null||Array.isArray(value)){return fail4("transaction file receipt must be an object")}const receipt=value;if(typeof receipt.path!=="string"){return fail4("transaction file receipt path must be a string")}const receiptPath=safeRelativePath(receipt.path);if(receipt.state==="absent"){requireExactKeys(receipt,["path","state"]);return{path:receiptPath,state:"absent"}}requireExactKeys(receipt,["backup","gid","mode","path","sha256","size","state","uid"]);if(receipt.state!=="file"||typeof receipt.backup!=="string"||!/^[0-9]{3}\.bin$/u.test(receipt.backup)||typeof receipt.sha256!=="string"||!/^[a-f0-9]{64}$/u.test(receipt.sha256)||!safeMetadata(receipt.size)||receipt.size>MAX_TRANSACTION_FILE_BYTES||!safeMetadata(receipt.uid)||!safeMetadata(receipt.gid)||!safeMetadata(receipt.mode)||receipt.mode>4095){return fail4("transaction file receipt is invalid")}return{path:receiptPath,state:"file",backup:receipt.backup,sha256:receipt.sha256,size:receipt.size,uid:receipt.uid,gid:receipt.gid,mode:receipt.mode}});const directories=record.directories.map(value=>{if(typeof value!=="object"||value===null||Array.isArray(value)){return fail4("transaction directory receipt must be an object")}const receipt=value;if(typeof receipt.path!=="string"){return fail4("transaction directory receipt path must be a string")}const receiptPath=safeRelativePath(receipt.path);if(receipt.state==="absent"){requireExactKeys(receipt,["path","state"]);return{path:receiptPath,state:"absent"}}requireExactKeys(receipt,["gid","mode","path","state","uid"]);if(receipt.state!=="directory"||!safeMetadata(receipt.uid)||!safeMetadata(receipt.gid)||!safeMetadata(receipt.mode)||receipt.mode>4095){return fail4("transaction directory receipt is invalid")}return{path:receiptPath,state:"directory",uid:receipt.uid,gid:receipt.gid,mode:receipt.mode}});const filePaths=files.map(receipt=>receipt.path);const directoryPaths=directories.map(receipt=>receipt.path);const backupNames=files.filter(receipt=>receipt.state==="file").map(receipt=>receipt.backup);if(new Set(filePaths).size!==filePaths.length||new Set(directoryPaths).size!==directoryPaths.length||new Set(backupNames).size!==backupNames.length){fail4("transaction manifest contains duplicate receipts")}const manifest={schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:record.agent,profileFingerprint:record.profileFingerprint,bootstrapIdentity,files,directories};const canonical=hasBootstrapIdentity?canonicalManifest(manifest):canonicalLegacyManifest(manifest);if(canonical!==text){fail4("transaction manifest is not canonical")}return manifest}function requireTrustedTransactionPath(target,mode,options){const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||(mode===TRANSACTION_DIRECTORY_MODE?!stat.isDirectory():!stat.isFile())||!options.readOnlyReceipt&&(stat.uid!==options.trustedUid||stat.gid!==options.trustedGid)||modeOf2(stat)!==mode){fail4(`transaction artifact has unsafe metadata: ${target}`)}}function requireReadOnlyReceiptMount(target,options){if(!options.readOnlyReceipt)return;const probe=import_node_path2.default.join(target,".nemoclaw-write-probe");let descriptor;try{descriptor=import_node_fs2.default.openSync(probe,import_node_fs2.default.constants.O_CREAT|import_node_fs2.default.constants.O_EXCL|import_node_fs2.default.constants.O_WRONLY|import_node_fs2.default.constants.O_NOFOLLOW,384);import_node_fs2.default.closeSync(descriptor);descriptor=void 0;import_node_fs2.default.unlinkSync(probe)}catch(error){if(descriptor!==void 0)import_node_fs2.default.closeSync(descriptor);if(error.code==="EROFS")return;fail4("copied receipt must be mounted on a read-only filesystem")}fail4("copied receipt mount is writable")}function loadManifest(options){requireTransactionBoundaries(options);if(!pathExistsNoFollow(options.transactionDirectory))return null;requireTrustedTransactionPath(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE,options);requireReadOnlyReceiptMount(options.transactionDirectory,options);requireTrustedTransactionPath(options.backupDirectory,TRANSACTION_DIRECTORY_MODE,options);requireTrustedTransactionPath(options.manifestFile,TRANSACTION_FILE_MODE,options);const stable=readStableFile(options.manifestFile,MAX_MANIFEST_BYTES);if(!options.readOnlyReceipt&&(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid)||Number(stable.stat.mode&0o7777n)!==TRANSACTION_FILE_MODE){fail4("transaction manifest ownership changed while it was read")}return parseManifest(stable.bytes.toString("utf8"))}function transactionOptionsAt(options,transactionDirectory){return{...options,transactionDirectory,backupDirectory:import_node_path2.default.join(transactionDirectory,"backups"),manifestFile:import_node_path2.default.join(transactionDirectory,"manifest.json")}}function loadCommitReceipt(options){requireTransactionBoundaries(options);if(!pathExistsNoFollow(options.commitReceiptDirectory))return null;requireTrustedTransactionPath(options.commitReceiptDirectory,TRANSACTION_DIRECTORY_MODE,options);if(pathExistsNoFollow(options.commitReceiptFile)){requireReadOnlyReceiptMount(options.commitReceiptDirectory,options);requireTrustedTransactionPath(options.commitReceiptFile,TRANSACTION_FILE_MODE,options);const stable=readStableFile(options.commitReceiptFile,MAX_COMMIT_RECEIPT_BYTES);if(!options.readOnlyReceipt&&(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid)||Number(stable.stat.mode&0o7777n)!==TRANSACTION_FILE_MODE){fail4("commit receipt ownership changed while it was read")}return{receipt:parseCommitReceipt(stable.bytes.toString("utf8")),compact:true}}const stagedOptions=transactionOptionsAt(options,options.commitReceiptDirectory);const staged=loadManifest(stagedOptions);if(!staged||staged.bootstrapIdentity===null){fail4("durable commit staging receipt is incomplete")}verifyAllBackups(staged.files,stagedOptions);return{receipt:{schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:staged.agent,profileFingerprint:staged.profileFingerprint,bootstrapIdentity:staged.bootstrapIdentity},compact:false}}function verifyBackup(receipt,options){const backupPath=import_node_path2.default.join(options.backupDirectory,receipt.backup);requireTrustedTransactionPath(backupPath,TRANSACTION_FILE_MODE,options);const stable=readStableFile(backupPath,MAX_TRANSACTION_FILE_BYTES);const digest=(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex");if(stable.bytes.length!==receipt.size||digest!==receipt.sha256){fail4(`transaction backup does not match its receipt: ${receipt.path}`)}return stable.bytes}function verifyAllBackups(receipts,options){const backups=new Map;for(const receipt of receipts){if(receipt.state==="file"){backups.set(receipt.path,verifyBackup(receipt,options))}}return backups}function fileMatchesReceipt(target,receipt){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return false;fail4(`could not inspect managed output ${target}`)}if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1)return false;const stable=readStableFile(target,MAX_TRANSACTION_FILE_BYTES);return stable.bytes.length===receipt.size&&(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex")===receipt.sha256&&Number(stable.stat.uid)===receipt.uid&&Number(stable.stat.gid)===receipt.gid&&Number(stable.stat.mode&0o7777n)===receipt.mode}function directoryMatchesReceipt(target,receipt){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return false;fail4(`could not inspect managed output directory ${target}`)}return!stat.isSymbolicLink()&&stat.isDirectory()&&stat.uid===receipt.uid&&stat.gid===receipt.gid&&modeOf2(stat)===receipt.mode}function removeTransactionDirectory(options){requireTrustedTransactionPath(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE,options);import_node_fs2.default.rmSync(options.transactionDirectory,{force:false,recursive:true});fsyncDirectory(options.transactionParentDirectory);if(pathExistsNoFollow(options.transactionDirectory)){fail4("transaction directory remained after cleanup")}}function assertCommitReceiptMatches(receipt,expected){if(receipt.agent!==expected.agent||expected.profileFingerprint!==void 0&&receipt.profileFingerprint!==expected.profileFingerprint||receipt.bootstrapIdentity!==expected.bootstrapIdentity){fail4("durable commit receipt belongs to a different bootstrap attempt")}}function loadCommitStagingManifest(options){if(!pathExistsNoFollow(options.manifestFile))return null;requireTrustedTransactionPath(options.manifestFile,TRANSACTION_FILE_MODE,options);const stable=readStableFile(options.manifestFile,MAX_MANIFEST_BYTES);if(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid||Number(stable.stat.mode&0o7777n)!==TRANSACTION_FILE_MODE){fail4("durable commit staging manifest ownership changed while it was read")}return parseManifest(stable.bytes.toString("utf8"))}function retireInterruptedCommitReceiptWrites(receipt,options){const temporaryPattern=new RegExp(`^\\.${MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE.replace(".","\\.")}\\.[a-f0-9]{24}$`,"u");for(const entry of import_node_fs2.default.readdirSync(options.commitReceiptDirectory)){if(!temporaryPattern.test(entry))continue;const target=import_node_path2.default.join(options.commitReceiptDirectory,entry);const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==options.trustedUid||stat.gid!==options.trustedGid||![ATOMIC_TEMPORARY_FILE_MODE,TRANSACTION_FILE_MODE].includes(modeOf2(stat))){fail4("interrupted durable commit receipt write has unsafe metadata")}const stable=readStableFile(target,MAX_COMMIT_RECEIPT_BYTES);const mode=Number(stable.stat.mode&0o7777n);if(Number(stable.stat.uid)!==options.trustedUid||Number(stable.stat.gid)!==options.trustedGid||![ATOMIC_TEMPORARY_FILE_MODE,TRANSACTION_FILE_MODE].includes(mode)){fail4("interrupted durable commit receipt write changed during verification")}if(stable.bytes.length>0){let interruptedReceipt=null;try{interruptedReceipt=parseCommitReceipt(stable.bytes.toString("utf8"))}catch{}if(interruptedReceipt)assertCommitReceiptMatches(interruptedReceipt,receipt)}import_node_fs2.default.unlinkSync(target);fsyncDirectory(options.commitReceiptDirectory)}}function compactDurableCommitReceipt(state,options){if(!state.compact){atomicWriteTrustedFile(options.commitReceiptFile,canonicalCommitReceipt(state.receipt),TRANSACTION_FILE_MODE,options.trustedUid,options.trustedGid);fsyncDirectory(options.commitReceiptDirectory)}retireInterruptedCommitReceiptWrites(state.receipt,options);const stagedOptions=transactionOptionsAt(options,options.commitReceiptDirectory);const manifestExists=pathExistsNoFollow(stagedOptions.manifestFile);const backupsExist=pathExistsNoFollow(stagedOptions.backupDirectory);const unexpectedBeforeCleanup=import_node_fs2.default.readdirSync(options.commitReceiptDirectory).filter(entry=>![MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE,import_node_path2.default.basename(stagedOptions.backupDirectory),import_node_path2.default.basename(stagedOptions.manifestFile)].includes(entry));if(unexpectedBeforeCleanup.length!==0){fail4("durable commit receipt directory contains unexpected artifacts")}if(manifestExists){const staged=loadCommitStagingManifest(stagedOptions);if(!staged||staged.bootstrapIdentity===null){fail4("durable commit staging receipt disappeared during cleanup")}assertCommitReceiptMatches(state.receipt,{agent:staged.agent,profileFingerprint:staged.profileFingerprint,bootstrapIdentity:staged.bootstrapIdentity})}if(backupsExist){requireTrustedTransactionPath(stagedOptions.backupDirectory,TRANSACTION_DIRECTORY_MODE,options);import_node_fs2.default.rmSync(stagedOptions.backupDirectory,{force:false,recursive:true});fsyncDirectory(options.commitReceiptDirectory)}if(manifestExists){requireTrustedTransactionPath(stagedOptions.manifestFile,TRANSACTION_FILE_MODE,options);import_node_fs2.default.unlinkSync(stagedOptions.manifestFile);fsyncDirectory(options.commitReceiptDirectory)}const unexpected=import_node_fs2.default.readdirSync(options.commitReceiptDirectory).filter(entry=>entry!==MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE);if(unexpected.length!==0){fail4("durable commit receipt directory contains unexpected artifacts")}const verified=loadCommitReceipt(options);if(!verified?.compact)fail4("durable commit receipt did not compact successfully");assertCommitReceiptMatches(verified.receipt,state.receipt)}function beginManagedStartupSharedStateTransaction(profile,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);if(options.readOnlyReceipt){fail4("cannot begin a transaction from a read-only rollback receipt")}requireTransactionBoundaries(options);const profileFingerprint=fingerprintManagedStartupProfile(profile);const committed=loadCommitReceipt(options);if(committed){if(options.bootstrapIdentity===null){fail4("a durable managed bootstrap commit receipt already exists")}assertCommitReceiptMatches(committed.receipt,{agent:profile.agent,profileFingerprint,bootstrapIdentity:options.bootstrapIdentity});fail4("this managed bootstrap attempt is already durably committed")}const pending=loadManifest(options);if(pending){if(pending.agent!==profile.agent||pending.profileFingerprint!==profileFingerprint||pending.bootstrapIdentity!==options.bootstrapIdentity){fail4("a pending managed startup transaction belongs to a different agent, profile fingerprint, or bootstrap attempt")}verifyAllBackups(pending.files,options);return false}const targets=managedOutputTargets(profile,options);if(targets.files.length>MAX_TRANSACTION_FILES){fail4("managed startup transaction has too many file targets")}const snapshots=targets.files.map((target,index)=>snapshotFile(target,index,profile.agent,options));const totalBytes=snapshots.reduce((sum,snapshot)=>sum+(snapshot.bytes?.length??0),0);if(totalBytes>MAX_TRANSACTION_TOTAL_BYTES){fail4("managed startup transaction backup exceeds the total size limit")}const directories=targets.directories.map(target=>snapshotDirectory(target,profile.agent,options));const manifest={schemaVersion:TRANSACTION_SCHEMA_VERSION,agent:profile.agent,profileFingerprint,bootstrapIdentity:options.bootstrapIdentity,files:snapshots.map(({receipt})=>receipt),directories};let createdTransactionIdentity;try{import_node_fs2.default.mkdirSync(options.transactionDirectory,{mode:TRANSACTION_DIRECTORY_MODE});const created=import_node_fs2.default.lstatSync(options.transactionDirectory,{bigint:true});if(!created.isDirectory()||created.isSymbolicLink()){fail4("new transaction path is not a directory")}createdTransactionIdentity={dev:created.dev,ino:created.ino,uid:created.uid,gid:created.gid};import_node_fs2.default.chownSync(options.transactionDirectory,options.trustedUid,options.trustedGid);import_node_fs2.default.chmodSync(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE);fsyncDirectory(options.transactionParentDirectory);import_node_fs2.default.mkdirSync(options.backupDirectory,{mode:TRANSACTION_DIRECTORY_MODE});import_node_fs2.default.chownSync(options.backupDirectory,options.trustedUid,options.trustedGid);import_node_fs2.default.chmodSync(options.backupDirectory,TRANSACTION_DIRECTORY_MODE);fsyncDirectory(options.transactionDirectory);for(const snapshot of snapshots){if(snapshot.receipt.state!=="file"||snapshot.bytes===null)continue;atomicWriteTrustedFile(import_node_path2.default.join(options.backupDirectory,snapshot.receipt.backup),snapshot.bytes,TRANSACTION_FILE_MODE,options.trustedUid,options.trustedGid)}fsyncDirectory(options.backupDirectory);atomicWriteTrustedFile(options.manifestFile,canonicalManifest(manifest),TRANSACTION_FILE_MODE,options.trustedUid,options.trustedGid);fsyncDirectory(options.transactionDirectory);loadManifest(options)}catch(error){try{if(createdTransactionIdentity&&pathExistsNoFollow(options.transactionDirectory)){const current=import_node_fs2.default.lstatSync(options.transactionDirectory,{bigint:true});if(!current.isSymbolicLink()&¤t.isDirectory()&¤t.dev===createdTransactionIdentity.dev&¤t.ino===createdTransactionIdentity.ino&¤t.uid===createdTransactionIdentity.uid&¤t.gid===createdTransactionIdentity.gid){import_node_fs2.default.chmodSync(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE);import_node_fs2.default.chownSync(options.transactionDirectory,options.trustedUid,options.trustedGid)}requireTrustedTransactionPath(options.transactionDirectory,TRANSACTION_DIRECTORY_MODE,options);import_node_fs2.default.rmSync(options.transactionDirectory,{force:true,recursive:true})}}catch{}throw error}return true}function ensureOriginalDirectories(receipts,expectedAgent,options){for(const receipt of receipts){if(receipt.state!=="directory")continue;const target=absoluteTarget(receipt.path,options);validateExistingAncestors(import_node_path2.default.join(target,".restore"),expectedAgent,options);let stat=null;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code!=="ENOENT"){fail4(`could not inspect restore directory ${target}`)}}if(stat&&(stat.isSymbolicLink()||!stat.isDirectory())){fail4(`restore directory is unsafe: ${target}`)}if(stat&&directoryMatchesReceipt(target,receipt))continue;if(!stat)import_node_fs2.default.mkdirSync(target,{mode:receipt.mode});import_node_fs2.default.chownSync(target,receipt.uid,receipt.gid);import_node_fs2.default.chmodSync(target,receipt.mode)}}function restoreFiles(receipts,backups,expectedAgent,options){for(const receipt of receipts){const target=absoluteTarget(receipt.path,options);validateExistingAncestors(target,expectedAgent,options);if(receipt.state==="absent"){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")continue;fail4(`could not inspect new managed output ${target}`)}if(stat.isDirectory()){fail4(`new managed output unexpectedly became a directory: ${target}`)}import_node_fs2.default.unlinkSync(target);continue}if(fileMatchesReceipt(target,receipt))continue;const bytes=backups.get(receipt.path);if(!bytes)fail4(`verified transaction backup is missing: ${receipt.path}`);let current=null;try{current=import_node_fs2.default.lstatSync(target)}catch(error){if(error.code!=="ENOENT"){fail4(`could not inspect managed output before restore: ${target}`)}}if(current?.isDirectory()){fail4(`managed output unexpectedly became a directory: ${target}`)}atomicWriteTrustedFile(target,bytes,receipt.mode,receipt.uid,receipt.gid)}}function restoreDirectoryMetadata(receipts,options){for(const receipt of[...receipts].reverse()){const target=absoluteTarget(receipt.path,options);if(receipt.state==="absent"){try{import_node_fs2.default.rmdirSync(target)}catch(error){if(error.code==="ENOENT")continue;fail4(`could not remove newly created managed directory ${target}`)}continue}if(directoryMatchesReceipt(target,receipt))continue;const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`managed directory changed type during restore: ${target}`)}import_node_fs2.default.chownSync(target,receipt.uid,receipt.gid);import_node_fs2.default.chmodSync(target,receipt.mode)}}function verifyRestoration(manifest,options){for(const receipt of manifest.files){const target=absoluteTarget(receipt.path,options);if(receipt.state==="absent"){if(pathExistsNoFollow(target)){fail4(`new managed output remained after rollback: ${target}`)}continue}const stable=readStableFile(target,MAX_TRANSACTION_FILE_BYTES);if(stable.bytes.length!==receipt.size||(0,import_node_crypto6.createHash)("sha256").update(stable.bytes).digest("hex")!==receipt.sha256||Number(stable.stat.uid)!==receipt.uid||Number(stable.stat.gid)!==receipt.gid||Number(stable.stat.mode&0o7777n)!==receipt.mode){fail4(`managed output was not restored exactly: ${target}`)}}for(const receipt of manifest.directories){const target=absoluteTarget(receipt.path,options);if(receipt.state==="absent"){if(pathExistsNoFollow(target)){fail4(`new managed directory remained after rollback: ${target}`)}continue}const stat=import_node_fs2.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isDirectory()||stat.uid!==receipt.uid||stat.gid!==receipt.gid||modeOf2(stat)!==receipt.mode){fail4(`managed directory metadata was not restored exactly: ${target}`)}}}function rollbackManagedStartupSharedStateTransaction(expectedAgent,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);const committed=loadCommitReceipt(options);if(committed){if(options.bootstrapIdentity===null){fail4("shared state is already durably committed")}assertCommitReceiptMatches(committed.receipt,{agent:expectedAgent,bootstrapIdentity:options.bootstrapIdentity});fail4("shared state is already durably committed and cannot be rolled back")}const manifest=loadManifest(options);if(!manifest)return false;if(manifest.agent!==expectedAgent){fail4(`pending transaction targets ${manifest.agent}, expected ${expectedAgent}`)}if(manifest.bootstrapIdentity!==options.bootstrapIdentity){fail4("pending transaction belongs to a different bootstrap attempt")}const backups=verifyAllBackups(manifest.files,options);ensureOriginalDirectories(manifest.directories,expectedAgent,options);restoreFiles(manifest.files,backups,expectedAgent,options);restoreDirectoryMetadata(manifest.directories,options);verifyRestoration(manifest,options);if(!options.readOnlyReceipt){removeTransactionDirectory(options)}return true}function commitManagedStartupSharedStateTransaction(expectedAgent,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);if(options.readOnlyReceipt){fail4("cannot commit a read-only rollback receipt")}const committed=loadCommitReceipt(options);if(committed){if(options.bootstrapIdentity===null){fail4("durable commit receipt is missing its expected bootstrap identity")}assertCommitReceiptMatches(committed.receipt,{agent:expectedAgent,bootstrapIdentity:options.bootstrapIdentity});compactDurableCommitReceipt(committed,options);return true}const manifest=loadManifest(options);if(!manifest)return false;if(manifest.agent!==expectedAgent){fail4(`pending transaction targets ${manifest.agent}, expected ${expectedAgent}`)}if(manifest.bootstrapIdentity!==options.bootstrapIdentity){fail4("pending transaction belongs to a different bootstrap attempt")}if(manifest.bootstrapIdentity===null){removeTransactionDirectory(options);return true}verifyAllBackups(manifest.files,options);if(pathExistsNoFollow(options.commitReceiptDirectory)){fail4("durable commit receipt path appeared before transaction commit")}try{import_node_fs2.default.renameSync(options.transactionDirectory,options.commitReceiptDirectory);fsyncDirectory(options.transactionParentDirectory)}catch(error){fail4(`could not atomically establish durable commit state: ${error.message}`)}const renamed=loadCommitReceipt(options);if(!renamed)fail4("durable commit state disappeared after atomic rename");assertCommitReceiptMatches(renamed.receipt,{agent:expectedAgent,profileFingerprint:manifest.profileFingerprint,bootstrapIdentity:manifest.bootstrapIdentity});compactDurableCommitReceipt(renamed,options);return true}function clearManagedStartupSharedStateCommitReceipt(expectedAgent,inputOptions={}){const options=resolveOptions(inputOptions);requireTransactionIdentity(options);if(options.readOnlyReceipt){fail4("cannot clear a durable commit from a read-only receipt")}if(options.bootstrapIdentity===null){fail4("durable commit cleanup requires its bootstrap identity")}const committed=loadCommitReceipt(options);if(!committed)return false;assertCommitReceiptMatches(committed.receipt,{agent:expectedAgent,bootstrapIdentity:options.bootstrapIdentity});compactDurableCommitReceipt(committed,options);requireTrustedTransactionPath(options.commitReceiptDirectory,TRANSACTION_DIRECTORY_MODE,options);requireTrustedTransactionPath(options.commitReceiptFile,TRANSACTION_FILE_MODE,options);const entries=import_node_fs2.default.readdirSync(options.commitReceiptDirectory);if(entries.length!==1||entries[0]!==MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE){fail4("durable commit receipt directory contains unexpected artifacts")}import_node_fs2.default.rmSync(options.commitReceiptDirectory,{force:false,recursive:true});fsyncDirectory(options.transactionParentDirectory);if(pathExistsNoFollow(options.commitReceiptDirectory)){fail4("durable commit receipt remained after cleanup")}return true}function getManagedStartupSharedStateTransactionStatus(expected,inputOptions={}){const options=resolveOptions({...inputOptions,bootstrapIdentity:expected.bootstrapIdentity});requireTransactionIdentity(options);const manifest=loadManifest(options);if(manifest){if(manifest.agent!==expected.agent||manifest.profileFingerprint!==expected.profileFingerprint||manifest.bootstrapIdentity!==expected.bootstrapIdentity){fail4("pending transaction does not match the expected agent, profile fingerprint, or bootstrap identity")}verifyAllBackups(manifest.files,options);return"pending"}const committed=loadCommitReceipt(options);if(!committed)return"none";assertCommitReceiptMatches(committed.receipt,expected);return"committed"}var MANAGED_STARTUP_PROFILE_ENV="NEMOCLAW_STARTUP_PROFILE_B64";var MANAGED_STARTUP_CA_ENV="NEMOCLAW_CORPORATE_CA_B64";var MANAGED_STARTUP_EXCHANGE_DIRECTORY="/tmp";var MANAGED_STARTUP_RUNTIME_ENV_FILE=`${MANAGED_STARTUP_EXCHANGE_DIRECTORY}/nemoclaw-managed-startup-runtime.env`;var MANAGED_STARTUP_RUNTIME_EXECUTABLE="/usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs";var MANAGED_STARTUP_MERGED_CA_FILE=`${MANAGED_STARTUP_EXCHANGE_DIRECTORY}/nemoclaw-managed-startup-ca-bundle.pem`;var MANAGED_STARTUP_COMPLETION_FILE=`${MANAGED_STARTUP_EXCHANGE_DIRECTORY}/nemoclaw-managed-startup-complete.json`;var MANAGED_STARTUP_CORPORATE_CA_FILE="/usr/local/share/nemoclaw/corporate-ca.pem";var MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY="/usr/local/share/ca-certificates";var MANAGED_STARTUP_SYSTEM_CA_ANCHOR_RE=/^nemoclaw-corporate-ca-[0-9]{2}\.crt$/u;var SYSTEM_CA_BUNDLE_FILE="/etc/ssl/certs/ca-certificates.crt";var UPDATE_CA_CERTIFICATES_EXECUTABLE="/usr/sbin/update-ca-certificates";var MANAGED_STARTUP_TLS_ENV_NAMES=new Set(["CURL_CA_BUNDLE","GIT_SSL_CAINFO","NODE_EXTRA_CA_CERTS","REQUESTS_CA_BUNDLE","SSL_CERT_FILE"]);var MESSAGING_RUNTIME_PLAN_FILE="/usr/local/share/nemoclaw/messaging-runtime-plan.json";var ROOT_STATE_PARENT="/var/lib/nemoclaw";var ROOT_RUNTIME_DIRECTORY="/run/nemoclaw";var ROOT_OWNED_DIRECTORY_MODE=493;var MAX_TRUST_BUNDLE_BYTES=4*1024*1024;var HERMES_MANAGED_CONFIG_FILES=["/sandbox/.hermes/config.yaml","/sandbox/.hermes/.env"];var HERMES_GENERATED_MANAGED_POLICY_FILE="/sandbox/.hermes/managed-policy.json";var HERMES_INSTALLED_MANAGED_POLICY_FILE="/usr/local/share/nemoclaw/hermes-managed-policy.json";var MAX_HERMES_MANAGED_POLICY_BYTES=4*1024*1024;var MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES=4*Math.ceil(MAX_HERMES_MANAGED_POLICY_BYTES/3)+4096;var FIXED_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";var SHA256_RE4=/^[a-f0-9]{64}$/u;var MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION=1;var MAX_MANAGED_STARTUP_COMPLETION_BYTES=4096;var MAX_MANAGED_STARTUP_RUNTIME_ENVIRONMENT_BYTES=512*1024;var defaultWorkspaceRootApplyDeps={lstat:target=>import_node_fs3.default.lstatSync(target),chown:(target,uid,gid)=>import_node_fs3.default.chownSync(target,uid,gid),chmod:(target,mode)=>import_node_fs3.default.chmodSync(target,mode)};function normalizeManagedStartupWorkspaceRoot(agent,deps=defaultWorkspaceRootApplyDeps){const target=managedImageRuntimeIdentity(agent).workdir;const before=deps.lstat(target);if(before.isSymbolicLink()||!before.isDirectory()){fail5("managed startup workspace root must be one real directory")}const expected=managedStartupWorkspaceRoot({agent,agentIdentity:managedImageRuntimeIdentity(agent)});try{deps.chown(target,expected.uid,expected.gid);deps.chmod(target,expected.mode)}catch{fail5("managed startup workspace-root normalization failed")}const after=deps.lstat(target);if(after.isSymbolicLink()||!after.isDirectory()||after.uid!==expected.uid||after.gid!==expected.gid||(after.mode&4095)!==expected.mode){fail5("managed startup workspace root does not match its declared posture")}}var ManagedStartupImageActionPlanError=class extends Error{constructor(message){super(`Cannot build managed startup image action plan: ${message}`);this.name="ManagedStartupImageActionPlanError"}};var ManagedStartupImageRuntimeError=class extends Error{constructor(message){super(`Managed startup image application failed: ${message}`);this.name="ManagedStartupImageRuntimeError"}};function failActionPlan(message){throw new ManagedStartupImageActionPlanError(message)}function exactActionPlanAgent(value){if(MANAGED_STARTUP_AGENTS.includes(value)){return value}return failActionPlan(`unsupported agent ${JSON.stringify(value)}`)}function fail5(message){throw new ManagedStartupImageRuntimeError(message)}function validateManagedStartupApplicationRuntimePlan(plan){if(typeof plan!=="object"||plan===null){return fail5("application runtime plan must be an object")}const exportEnvironment=plan.exportEnvironment;const unsetEnvironment=plan.unsetEnvironment;if(typeof exportEnvironment!=="object"||exportEnvironment===null||Array.isArray(exportEnvironment)||!Array.isArray(unsetEnvironment)){return fail5("application runtime plan must contain exports and unsets")}const exports2={};for(const[name,value]of Object.entries(exportEnvironment)){if(!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)){return fail5(`invalid application runtime environment key ${JSON.stringify(name)}`)}if(typeof value!=="string"||value.includes("\0")||/[\r\n]/u.test(value)){return fail5(`application runtime environment value for ${name} must be single-line text`)}exports2[name]=value}const unsets=new Set;for(const name of unsetEnvironment){if(typeof name!=="string"||!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)){return fail5(`invalid application runtime unset ${JSON.stringify(name)}`)}if(unsets.has(name)){return fail5(`duplicate application runtime unset ${name}`)}if(Object.hasOwn(exports2,name)){return fail5(`application runtime cannot both export and unset ${name}`)}unsets.add(name)}return Object.freeze({exportEnvironment:Object.freeze(Object.fromEntries(Object.entries(exports2).sort(([left],[right])=>left.localeCompare(right)))),unsetEnvironment:Object.freeze([...unsets].sort())})}function applyManagedStartupCommandEnvironmentPlan(environment,plan){const validated=validateManagedStartupApplicationRuntimePlan(plan);const applied={...environment};for(const name of[...Object.keys(validated.exportEnvironment),...validated.unsetEnvironment]){delete applied[name]}return applied}function exactAgent2(value){if(MANAGED_STARTUP_AGENTS.includes(value)){return value}return fail5(`unsupported agent ${JSON.stringify(value)}`)}function managedTransactionProfile(expectedAgentInput,env=process.env){requireRoot();const expectedAgent=exactAgent2(expectedAgentInput);if(env.NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION!=="1"){fail5("shared-state transactions require a complete managed image")}const encodedProfile=env[MANAGED_STARTUP_PROFILE_ENV];if(!encodedProfile)fail5(`${MANAGED_STARTUP_PROFILE_ENV} is required`);const profile=decodeManagedStartupProfile(encodedProfile);if(profile.agent!==expectedAgent){fail5(`shared-state transaction profile targets ${profile.agent}, expected ${expectedAgent}`)}return profile}function requireRoot(){if(process.geteuid?.()!==0){fail5("managed startup requires container effective uid 0")}}function modeOf3(stat){return stat.mode&511}function requireManagedStartupExchangeDirectory(exchangeDirectory=MANAGED_STARTUP_EXCHANGE_DIRECTORY){const stat=import_node_fs3.default.lstatSync(exchangeDirectory);if(stat.isSymbolicLink()||!stat.isDirectory()||stat.uid!==0||stat.gid!==0||(stat.mode&4095)!==1023){fail5("managed startup exchange directory must be root:root mode 1777")}}function requireRootOwnedDirectory(target,mode){let stat;try{stat=import_node_fs3.default.lstatSync(target)}catch{fail5(`required root-owned directory is missing: ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()||stat.uid!==0||stat.gid!==0||modeOf3(stat)!==mode){fail5(`${target} must be a root:root directory with mode ${mode.toString(8)}`)}}function ensureRootOwnedDirectory(target,mode=ROOT_OWNED_DIRECTORY_MODE){const parent=import_node_path3.default.dirname(target);const parentStat=import_node_fs3.default.lstatSync(parent);if(parentStat.isSymbolicLink()||!parentStat.isDirectory()||parentStat.uid!==0||parentStat.gid!==0||(modeOf3(parentStat)&18)!==0){fail5(`refusing unsafe parent directory for ${target}`)}try{import_node_fs3.default.mkdirSync(target,{mode});import_node_fs3.default.chownSync(target,0,0);import_node_fs3.default.chmodSync(target,mode)}catch(error){if(error.code!=="EEXIST"){fail5(`could not create ${target}`)}}requireRootOwnedDirectory(target,mode)}function requireSafeExistingRootTarget(target){let stat;try{stat=import_node_fs3.default.lstatSync(target)}catch(error){if(error.code==="ENOENT")return;fail5(`could not inspect ${target}`)}if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==0||stat.gid!==0){fail5(`refusing to replace unsafe root-owned file ${target}`)}}function atomicWriteRootFile(target,contents,mode){const parent=import_node_path3.default.dirname(target);const parentStat=import_node_fs3.default.lstatSync(parent);const trustedStickyExchange=parent===MANAGED_STARTUP_EXCHANGE_DIRECTORY&&(parentStat.mode&4095)===1023;if(parentStat.isSymbolicLink()||!parentStat.isDirectory()||parentStat.uid!==0||parentStat.gid!==0||(modeOf3(parentStat)&18)!==0&&!trustedStickyExchange){fail5(`refusing unsafe root-owned file parent ${parent}`)}requireSafeExistingRootTarget(target);const temporary=import_node_path3.default.join(parent,`.${import_node_path3.default.basename(target)}.${(0,import_node_crypto7.randomBytes)(12).toString("hex")}`);let descriptor;try{descriptor=import_node_fs3.default.openSync(temporary,import_node_fs3.default.constants.O_CREAT|import_node_fs3.default.constants.O_EXCL|import_node_fs3.default.constants.O_WRONLY|import_node_fs3.default.constants.O_NOFOLLOW,384);import_node_fs3.default.fchownSync(descriptor,0,0);import_node_fs3.default.writeFileSync(descriptor,contents);import_node_fs3.default.fchmodSync(descriptor,mode);import_node_fs3.default.fsyncSync(descriptor);import_node_fs3.default.closeSync(descriptor);descriptor=void 0;import_node_fs3.default.renameSync(temporary,target)}catch(error){if(descriptor!==void 0)import_node_fs3.default.closeSync(descriptor);try{import_node_fs3.default.unlinkSync(temporary)}catch{}fail5(`could not atomically write ${target}: ${error.message}`)}const stat=import_node_fs3.default.lstatSync(target);if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==0||stat.gid!==0||modeOf3(stat)!==mode){fail5(`root-owned output failed metadata verification: ${target}`)}}function removeSafeRootFile(target){requireSafeExistingRootTarget(target);try{import_node_fs3.default.unlinkSync(target)}catch(error){if(error.code!=="ENOENT"){fail5(`could not remove ${target}`)}}}function trustedExecutable(target){try{const stat=import_node_fs3.default.lstatSync(target);return!stat.isSymbolicLink()&&stat.isFile()&&stat.uid===0&&stat.gid===0&&(modeOf3(stat)&18)===0&&(modeOf3(stat)&73)!==0}catch{return false}}function readSandboxIdentity(){const readId=flag=>{const result=(0,import_node_child_process.spawnSync)("/usr/bin/id",[flag,"sandbox"],{encoding:"utf8",env:{PATH:FIXED_PATH}});const value=result.stdout.trim();if(result.status!==0||!/^[1-9][0-9]*$/u.test(value)){fail5("could not resolve the sandbox account")}return value};return{uid:readId("-u"),gid:readId("-g")}}function managedStartupSandboxPrefix(){if(trustedExecutable("/usr/bin/setpriv")){const identity=readSandboxIdentity();return["/usr/bin/setpriv",`--reuid=${identity.uid}`,`--regid=${identity.gid}`,"--init-groups","--"]}return fail5("a trusted setpriv executable is required")}function commandEnvironment(configurationEnvironment,applicationRuntime){const env=applyManagedStartupCommandEnvironmentPlan({...process.env,...configurationEnvironment,HOME:"/sandbox",PATH:FIXED_PATH,NPM_CONFIG_OFFLINE:"true",npm_config_offline:"true",PIP_DISABLE_PIP_VERSION_CHECK:"1",PIP_NO_INDEX:"1",UV_OFFLINE:"1"},applicationRuntime);delete env[MANAGED_STARTUP_PROFILE_ENV];delete env[MANAGED_STARTUP_CA_ENV];return env}function execute(argv,runAs,configurationEnvironment,applicationRuntime,capture=false){if(argv.length===0)fail5("refusing an empty managed startup command");const command=runAs==="sandbox"?[...managedStartupSandboxPrefix(),...argv]:[...argv];const result=(0,import_node_child_process.spawnSync)(command[0],command.slice(1),{encoding:"utf8",env:commandEnvironment(configurationEnvironment,applicationRuntime),stdio:capture?"pipe":"inherit"});if(result.error){fail5(`could not execute ${argv[0]}: ${result.error.message}`)}if(result.status!==0){const detail=capture?`: ${(result.stderr||result.stdout).trim()}`:"";fail5(`${argv[0]} exited with status ${String(result.status??"unknown")}${detail}`)}return{status:result.status,stdout:result.stdout??"",stderr:result.stderr??""}}function generatorCommand(agent){switch(agent){case"openclaw":return["/usr/local/bin/node","/scripts/generate-openclaw-config.mts"];case"hermes":return["/usr/local/bin/node","/opt/nemoclaw-hermes-config/generate-config.ts"];case"langchain-deepagents-code":return["/usr/local/bin/node","/opt/nemoclaw-deepagents-code/generate-config.ts"];case"pi":return["/usr/local/bin/node","/opt/nemoclaw-pi/generate-config.ts"]}}function messagingCommand(agent,phase,mode){return["/usr/local/bin/node","/src/lib/messaging/applier/build/messaging-build-applier.mts","--agent",agent,"--phase",phase,"--mode",mode,...phase==="post-agent-install"?["--managed-startup-runtime"]:[]]}function assertActionAgent(inputAgent,actionAgent){if(inputAgent!==actionAgent){failActionPlan(`action for ${actionAgent} cannot be used by ${inputAgent}`)}}function buildManagedStartupImageActionPlan(input){const inputAgent=exactActionPlanAgent(input.agent);const commands=[];let dashboardActions=0;let generateActions=0;let runtimeMessagingActions=0;let postMessagingActions=0;for(const action of input.actions){switch(action.kind){case"configure-dashboard":{if(action.dashboard.agent!==input.agent){failActionPlan(`dashboard for ${action.dashboard.agent} cannot be used by ${input.agent}`)}dashboardActions+=1;break}case"generate-agent-config":{assertActionAgent(inputAgent,exactActionPlanAgent(action.agent));if(action.runAs!=="sandbox"){failActionPlan("agent configuration generation must run as sandbox")}generateActions+=1;commands.push({action:"generate-agent-config",runAs:action.runAs,argv:generatorCommand(action.agent)});break}case"apply-messaging-plan":{assertActionAgent(inputAgent,exactActionPlanAgent(action.agent));if(action.mode!=="apply"&&action.mode!=="clear"){failActionPlan("messaging intent must be apply or clear")}if(action.phase==="runtime-setup"){if(action.runAs!=="root"){failActionPlan("messaging runtime setup must run as root")}runtimeMessagingActions+=1;commands.push({action:"messaging-runtime-setup",runAs:action.runAs,argv:messagingCommand(action.agent,action.phase,action.mode)})}else if(action.phase==="post-agent-install"){if(action.runAs!=="sandbox"){failActionPlan("messaging post-agent configuration must run as sandbox")}postMessagingActions+=1;commands.push({action:"messaging-post-agent-install",runAs:action.runAs,argv:messagingCommand(action.agent,action.phase,action.mode)})}else{failActionPlan("unsupported messaging construction phase")}break}default:failActionPlan("unsupported managed startup construction action")}}if(dashboardActions!==1){failActionPlan("exactly one dashboard construction action is required")}if(generateActions!==1){failActionPlan("exactly one agent config construction action is required")}const supportsMessaging=MANAGED_STARTUP_MESSAGING_AGENTS.includes(inputAgent);const expectedMessagingActions=supportsMessaging?1:0;if(runtimeMessagingActions!==expectedMessagingActions||postMessagingActions!==expectedMessagingActions){failActionPlan(`${inputAgent} requires ${String(expectedMessagingActions)} action for each messaging phase`)}const expectedOrder=supportsMessaging?["messaging-runtime-setup","generate-agent-config","messaging-post-agent-install"]:["generate-agent-config"];if(commands.some((command,index)=>command.action!==expectedOrder[index])){failActionPlan(`${inputAgent} image actions are not in the required construction order`)}return Object.freeze(commands.map(command=>Object.freeze({...command,argv:Object.freeze([...command.argv])})))}function prepareMessagingRuntimeTarget(mode){if(mode==="clear"){removeSafeRootFile(MESSAGING_RUNTIME_PLAN_FILE);return}requireSafeExistingRootTarget(MESSAGING_RUNTIME_PLAN_FILE);try{import_node_fs3.default.unlinkSync(MESSAGING_RUNTIME_PLAN_FILE)}catch(error){if(error.code!=="ENOENT"){fail5("could not prepare the messaging runtime-plan target")}}}function verifyMessagingRuntimeTarget(mode){if(mode==="clear"){if(import_node_fs3.default.existsSync(MESSAGING_RUNTIME_PLAN_FILE)){fail5("clear messaging profile left a runtime-plan artifact")}return}const stat=import_node_fs3.default.lstatSync(MESSAGING_RUNTIME_PLAN_FILE);if(stat.isSymbolicLink()||!stat.isFile()||stat.nlink!==1||stat.uid!==0||stat.gid!==0||modeOf3(stat)!==420){fail5("messaging runtime-plan artifact failed root ownership validation")}}function runInternalSandboxAction(action,configurationEnvironment,applicationRuntime,extraEnvironment={}){execute(["/usr/local/bin/node",MANAGED_STARTUP_RUNTIME_EXECUTABLE,`--internal-${action}`],"sandbox",{...configurationEnvironment,...extraEnvironment},applicationRuntime)}function validateOpenClawConfiguration(configurationEnvironment,applicationRuntime){const validation=execute(["/usr/local/bin/openclaw","config","validate","--json"],"sandbox",{...configurationEnvironment,OPENCLAW_CONFIG_PATH:"/sandbox/.openclaw/openclaw.json"},applicationRuntime,true);let parsed;try{parsed=JSON.parse(validation.stdout)}catch{fail5("OpenClaw config validation did not emit JSON")}if(typeof parsed!=="object"||parsed===null||parsed.valid!==true){fail5("OpenClaw rejected the generated managed startup config")}}function sameStableFileMetadata(left,right){return left.dev===right.dev&&left.ino===right.ino&&left.mode===right.mode&&left.nlink===right.nlink&&left.uid===right.uid&&left.gid===right.gid&&left.size===right.size&&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs}function readStableRegularFileSnapshot(target,maxBytes){if(typeof import_node_fs3.default.constants.O_NOFOLLOW!=="number"){fail5("O_NOFOLLOW is unavailable for managed startup file reads")}const nonblock=typeof import_node_fs3.default.constants.O_NONBLOCK==="number"?import_node_fs3.default.constants.O_NONBLOCK:0;let descriptor;try{descriptor=import_node_fs3.default.openSync(target,import_node_fs3.default.constants.O_RDONLY|import_node_fs3.default.constants.O_NOFOLLOW|nonblock)}catch(error){if(error.code==="ENOENT")throw error;fail5(`refusing unsafe or unreadable file ${target}`)}try{const before=import_node_fs3.default.fstatSync(descriptor,{bigint:true});if(!before.isFile()||before.nlink!==1n||before.size<1n||before.size>BigInt(maxBytes)){fail5(`refusing unsafe or oversized file ${target}`)}const bytes=Buffer.alloc(Number(before.size));let offset=0;while(offset`${block.trim()} `)}function managedSystemCaAnchorNames(){try{import_node_fs3.default.lstatSync(MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY)}catch(error){if(error.code==="ENOENT")return[];fail5("could not inspect the managed system CA anchor directory")}requireRootOwnedDirectory(MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY,ROOT_OWNED_DIRECTORY_MODE);try{return import_node_fs3.default.readdirSync(MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY).filter(name=>MANAGED_STARTUP_SYSTEM_CA_ANCHOR_RE.test(name)).sort()}catch(error){fail5("could not inspect the managed system CA anchors")}}function refreshSystemCaBundle(){if(!trustedExecutable(UPDATE_CA_CERTIFICATES_EXECUTABLE)){fail5(`a trusted ${UPDATE_CA_CERTIFICATES_EXECUTABLE} executable is required`)}const result=(0,import_node_child_process.spawnSync)(UPDATE_CA_CERTIFICATES_EXECUTABLE,[],{encoding:"utf8",env:{PATH:FIXED_PATH},stdio:"inherit"});if(result.error){fail5(`could not execute ${UPDATE_CA_CERTIFICATES_EXECUTABLE}: ${result.error.message}`)}if(result.status!==0){fail5(`${UPDATE_CA_CERTIFICATES_EXECUTABLE} exited with status ${String(result.status??"unknown")}`)}}function requireSystemCaBundleContains(blocks){const systemBundle=safeTrustBundle(SYSTEM_CA_BUNDLE_FILE);if(systemBundle===null)fail5("the refreshed system CA bundle is missing");const systemBlocks=systemBundle.toString("utf8").match(PEM_CERTIFICATE_RE_GLOBAL)??[];const systemFingerprints=new Set;for(const block of systemBlocks){try{systemFingerprints.add(new import_node_crypto7.X509Certificate(block).fingerprint256)}catch{fail5("the refreshed system CA bundle contains an invalid certificate")}}for(const block of blocks){if(!systemFingerprints.has(new import_node_crypto7.X509Certificate(block).fingerprint256)){fail5("the refreshed system CA bundle does not contain the corporate CA")}}}function installCorporateCaSystemAnchors(corporateCaPath){const existingNames=managedSystemCaAnchorNames();if(corporateCaPath===null){for(const name of existingNames){removeSafeRootFile(import_node_path3.default.join(MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY,name))}refreshSystemCaBundle();return}ensureRootOwnedDirectory(MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY);const blocks=corporateCaCertificateBlocks(corporateCaPath);const expectedNames=blocks.map((_block,index)=>`nemoclaw-corporate-ca-${String(index+1).padStart(2,"0")}.crt`);for(const name of existingNames){if(!expectedNames.includes(name)){removeSafeRootFile(import_node_path3.default.join(MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY,name))}}for(const[index,name]of expectedNames.entries()){atomicWriteRootFile(import_node_path3.default.join(MANAGED_STARTUP_SYSTEM_CA_ANCHOR_DIRECTORY,name),blocks[index],292)}refreshSystemCaBundle();requireSystemCaBundleContains(blocks)}function safeTrustBundle(target){try{const{bytes,stat}=readStableRegularFileSnapshot(target,MAX_TRUST_BUNDLE_BYTES);if(Number(stat.mode&0o022n)!==0){fail5(`refusing unsafe trust bundle ${target}`)}return bytes}catch(error){if(error.code==="ENOENT")return null;throw error}}function mergeCorporateCa(corporateCaPath){if(corporateCaPath===null){removeSafeRootFile(MANAGED_STARTUP_MERGED_CA_FILE);return false}const corporate=readStableRegularFile(corporateCaPath,128*1024);const candidates=["/etc/openshell-tls/ca-bundle.pem",process.env.SSL_CERT_FILE??"","/etc/ssl/certs/ca-certificates.crt"].filter((candidate,index,values)=>candidate&&candidate!==MANAGED_STARTUP_MERGED_CA_FILE&&values.indexOf(candidate)===index);let base=null;for(const candidate of candidates){base=safeTrustBundle(candidate);if(base)break}const merged=Buffer.concat([...base?[base,Buffer.from("\n","utf8")]:[],corporate,...corporate.at(-1)===10?[]:[Buffer.from("\n","utf8")]]);atomicWriteRootFile(MANAGED_STARTUP_MERGED_CA_FILE,merged,292);return true}function shellSingleQuote(value){if(value.includes("\0")||/[\r\n]/u.test(value)){fail5("runtime environment values must be single-line text")}return`'${value.replaceAll("'",`'"'"'`)}'`}function serializeManagedStartupRuntimeEnvironment(environment,corporateCaMerged,configurationEnvironment={},applicationRuntime={exportEnvironment:{},unsetEnvironment:[]}){const{output,unsetNames}=materializeManagedStartupRuntimeEnvironment(environment,corporateCaMerged,configurationEnvironment,applicationRuntime);const unsetLines=unsetNames.map(name=>`unset ${name}`);const exportLines=Object.entries(output).sort(([left],[right])=>left.localeCompare(right)).map(([name,value])=>`export ${name}=${shellSingleQuote(value)}`);return`${[...unsetLines,...exportLines].join("\n")} `}function materializeManagedStartupRuntimeEnvironment(environment,corporateCaMerged,configurationEnvironment={},applicationRuntime={exportEnvironment:{},unsetEnvironment:[]}){const validatedApplicationRuntime=validateManagedStartupApplicationRuntimePlan(applicationRuntime);const output={...environment,...validatedApplicationRuntime.exportEnvironment,NEMOCLAW_MANAGED_STARTUP_APPLIED:"1"};if(corporateCaMerged){for(const name of MANAGED_STARTUP_TLS_ENV_NAMES){delete output[name]}output._NEMOCLAW_CORPORATE_CA_MERGED="1"}for(const name of[...Object.keys(configurationEnvironment),...Object.keys(output)]){if(!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)){fail5(`invalid runtime environment key ${JSON.stringify(name)}`)}}const unsetNames=new Set([...Object.keys(configurationEnvironment).filter(name=>!Object.hasOwn(output,name)&&(!corporateCaMerged||!MANAGED_STARTUP_TLS_ENV_NAMES.has(name))),...validatedApplicationRuntime.unsetEnvironment.filter(name=>!corporateCaMerged||!MANAGED_STARTUP_TLS_ENV_NAMES.has(name))]);for(const name of validatedApplicationRuntime.unsetEnvironment){if(Object.hasOwn(output,name)){fail5(`runtime environment cannot both export and unset ${name}`)}}for(const name of unsetNames){if(!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)){fail5(`invalid runtime environment key ${JSON.stringify(name)}`)}}return{output,unsetNames:[...unsetNames].sort()}}function serializeManagedStartupCompletionMarker(marker){if(marker.schemaVersion!==MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION||!MANAGED_STARTUP_AGENTS.includes(marker.agent)||!SHA256_RE4.test(marker.profileFingerprint)||!SHA256_RE4.test(marker.runtimeEnvironmentSha256)||typeof marker.corporateCaMerged!=="boolean"){fail5("managed startup completion marker is invalid")}return`${JSON.stringify({agent:marker.agent,corporateCaMerged:marker.corporateCaMerged,profileFingerprint:marker.profileFingerprint,runtimeEnvironmentSha256:marker.runtimeEnvironmentSha256,schemaVersion:marker.schemaVersion})} `}function publishManagedStartupImageCompletion(agent,profileFingerprint,runtimeEnvironment,corporateCaMerged,completionFile=MANAGED_STARTUP_COMPLETION_FILE){atomicWriteRootFile(completionFile,serializeManagedStartupCompletionMarker({schemaVersion:MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION,agent,profileFingerprint,runtimeEnvironmentSha256:(0,import_node_crypto7.createHash)("sha256").update(runtimeEnvironment).digest("hex"),corporateCaMerged}),292)}function parseManagedStartupCompletionMarker(text){let parsed;try{parsed=JSON.parse(text)}catch{fail5("managed startup completion marker is not valid JSON")}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed)){fail5("managed startup completion marker must be an object")}const record=parsed;const expectedKeys=["agent","corporateCaMerged","profileFingerprint","runtimeEnvironmentSha256","schemaVersion"];if(Object.keys(record).sort().join(",")!==expectedKeys.sort().join(",")||record.schemaVersion!==MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION||typeof record.agent!=="string"||!MANAGED_STARTUP_AGENTS.includes(record.agent)||typeof record.profileFingerprint!=="string"||!SHA256_RE4.test(record.profileFingerprint)||typeof record.runtimeEnvironmentSha256!=="string"||!SHA256_RE4.test(record.runtimeEnvironmentSha256)||typeof record.corporateCaMerged!=="boolean"){fail5("managed startup completion marker has an invalid schema")}const marker={schemaVersion:MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION,agent:record.agent,profileFingerprint:record.profileFingerprint,runtimeEnvironmentSha256:record.runtimeEnvironmentSha256,corporateCaMerged:record.corporateCaMerged};if(serializeManagedStartupCompletionMarker(marker)!==text){fail5("managed startup completion marker is not canonical")}return marker}function verifyManagedStartupImageCompletion(expectedAgentInput,expectedFingerprint,completionFile=MANAGED_STARTUP_COMPLETION_FILE,runtimeEnvironmentFile=MANAGED_STARTUP_RUNTIME_ENV_FILE){const exchangeDirectory=import_node_path3.default.dirname(completionFile);if(import_node_path3.default.dirname(runtimeEnvironmentFile)!==exchangeDirectory){fail5("managed startup handoff files must share one exchange directory")}requireManagedStartupExchangeDirectory(exchangeDirectory);const expectedAgent=exactAgent2(expectedAgentInput);if(!SHA256_RE4.test(expectedFingerprint)){fail5("startup completion expected profile fingerprint is invalid")}const{bytes,stat}=readStableRegularFileSnapshot(completionFile,MAX_MANAGED_STARTUP_COMPLETION_BYTES);if(stat.nlink!==1n||stat.uid!==0n||stat.gid!==0n||Number(stat.mode&0o777n)!==292){fail5("managed startup completion marker must be root:root mode 0444")}const marker=parseManagedStartupCompletionMarker(bytes.toString("utf8"));if(marker.agent!==expectedAgent||marker.profileFingerprint!==expectedFingerprint){fail5("managed startup completion marker does not match the requested profile")}const runtimeEnvironment=readStableRegularFileSnapshot(runtimeEnvironmentFile,MAX_MANAGED_STARTUP_RUNTIME_ENVIRONMENT_BYTES);if(runtimeEnvironment.stat.nlink!==1n||runtimeEnvironment.stat.uid!==0n||runtimeEnvironment.stat.gid!==0n||Number(runtimeEnvironment.stat.mode&0o777n)!==292){fail5("managed startup runtime environment must be root:root mode 0444")}const runtimeEnvironmentSha256=(0,import_node_crypto7.createHash)("sha256").update(runtimeEnvironment.bytes).digest("hex");if(runtimeEnvironmentSha256!==marker.runtimeEnvironmentSha256){fail5("managed startup completion marker runtime environment digest mismatch")}return{agent:expectedAgent,fingerprint:expectedFingerprint}}function waitForManagedStartupImageCompletion(expectedAgentInput,expectedFingerprint,timeoutSeconds=600){if(!Number.isSafeInteger(timeoutSeconds)||timeoutSeconds<1||timeoutSeconds>3600){fail5("startup completion wait timeout must be an integer from 1 to 3600 seconds")}const deadline=Date.now()+timeoutSeconds*1e3;while(true){try{return verifyManagedStartupImageCompletion(expectedAgentInput,expectedFingerprint)}catch(error){if(error.code!=="ENOENT")throw error;if(Date.now()>=deadline){fail5(`startup completion was not published within ${String(timeoutSeconds)} seconds`)}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,250)}}}function publishManagedStartupCompletionAfterCommit(expectedAgentInput,expectedFingerprint,bootstrapIdentity,completionFile=MANAGED_STARTUP_COMPLETION_FILE,runtimeEnvironmentFile=MANAGED_STARTUP_RUNTIME_ENV_FILE){requireRoot();requireManagedStartupExchangeDirectory();const expectedAgent=exactAgent2(expectedAgentInput);if(getManagedStartupSharedStateTransactionStatus({agent:expectedAgent,profileFingerprint:expectedFingerprint,bootstrapIdentity})!=="committed"){fail5("managed startup completion requires a committed shared-state transaction")}try{verifyManagedStartupImageCompletion(expectedAgent,expectedFingerprint,completionFile,runtimeEnvironmentFile);return}catch(error){if(error.code!=="ENOENT")throw error}const runtimeEnvironment=readStableRegularFileSnapshot(runtimeEnvironmentFile,MAX_MANAGED_STARTUP_RUNTIME_ENVIRONMENT_BYTES);if(runtimeEnvironment.stat.nlink!==1n||runtimeEnvironment.stat.uid!==0n||runtimeEnvironment.stat.gid!==0n||Number(runtimeEnvironment.stat.mode&0o777n)!==292){fail5("managed startup runtime environment must be root:root mode 0444")}const corporateCaMerged=runtimeEnvironment.bytes.toString("utf8").split("\n").includes("export _NEMOCLAW_CORPORATE_CA_MERGED='1'");publishManagedStartupImageCompletion(expectedAgent,expectedFingerprint,runtimeEnvironment.bytes,corporateCaMerged,completionFile);verifyManagedStartupImageCompletion(expectedAgent,expectedFingerprint,completionFile,runtimeEnvironmentFile)}function applyAdapter(context,mapped){if(mapped.agent!==context.agent){fail5(`mapped ${mapped.agent} environment for ${context.agent}`)}const commandPlan=buildManagedStartupImageActionPlan({agent:mapped.agent,actions:mapped.actions});let commandIndex=0;for(const action of mapped.actions){if(action.kind==="configure-dashboard")continue;const command=commandPlan[commandIndex];if(!command)fail5(`missing image command for ${action.kind}`);commandIndex+=1;if(action.kind==="apply-messaging-plan"){if(action.phase==="runtime-setup"){prepareMessagingRuntimeTarget(action.mode)}execute(command.argv,command.runAs,mapped.configurationEnvironment,mapped.applicationRuntime);if(action.phase==="runtime-setup"){verifyMessagingRuntimeTarget(action.mode)}continue}execute(command.argv,command.runAs,mapped.configurationEnvironment,mapped.applicationRuntime)}if(commandIndex!==commandPlan.length){fail5("image action plan contains an unmatched command")}switch(context.agent){case"openclaw":validateOpenClawConfiguration(mapped.configurationEnvironment,mapped.applicationRuntime);break;case"hermes":installHermesManagedPolicy();sealHermesConfiguration(mapped.configurationEnvironment,mapped.applicationRuntime);normalizeHermesManagedConfiguration();break;case"langchain-deepagents-code":break}installRootOwnedMaterials(mapped.materials);installCorporateCa(context.corporateCaPath);installCorporateCaSystemAnchors(context.corporateCaPath);mergeCorporateCa(context.corporateCaPath)}function adapters(mapped){return MANAGED_STARTUP_AGENTS.map(agent=>({agent,apply:context=>applyAdapter(context,mapped)}))}async function applyManagedStartupImageProfile(expectedAgentInput,env=process.env,options={}){requireRoot();const expectedAgent=exactAgent2(expectedAgentInput);if(env.NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION!=="1"){fail5("startup profiles require a complete managed image")}const encodedProfile=env[MANAGED_STARTUP_PROFILE_ENV];if(!encodedProfile)fail5(`${MANAGED_STARTUP_PROFILE_ENV} is required`);let profile;try{profile=decodeManagedStartupProfile(encodedProfile)}catch(error){fail5(error.message)}if(profile.agent!==expectedAgent){fail5(`managed startup profile targets ${profile.agent}, expected ${expectedAgent}`)}requireManagedStartupExchangeDirectory();const mapped=mapManagedStartupProfileToAgentEnvironment(profile,env);validateManagedStartupApplicationRuntimePlan(mapped.applicationRuntime);ensureRootOwnedDirectory(ROOT_STATE_PARENT);ensureRootOwnedDirectory(ROOT_RUNTIME_DIRECTORY);const result=await coordinateManagedStartupApplication({encodedProfile,expectedAgent,...env[MANAGED_STARTUP_CA_ENV]===void 0?{}:{corporateCaB64:env[MANAGED_STARTUP_CA_ENV]}},adapters(mapped));if(mapped.agent!==result.application.profile.agent){fail5(`mapped ${mapped.agent} environment for ${result.application.profile.agent}`)}if(expectedAgent==="hermes"&&!result.adapterApplied){normalizeHermesManagedConfiguration()}let corporateCaMerged;if(result.adapterApplied){corporateCaMerged=result.application.corporateCaPath!==null}else{verifyRootOwnedMaterials(mapped.materials);if(result.application.corporateCaPath===null){if(import_node_fs3.default.existsSync(MANAGED_STARTUP_CORPORATE_CA_FILE)){fail5("committed profile without a corporate CA has a stale CA material")}}else{const expected=readStableRegularFile(result.application.corporateCaPath,128*1024);const installed=readStableRegularFile(MANAGED_STARTUP_CORPORATE_CA_FILE,128*1024);if(!expected.equals(installed)){fail5("committed corporate CA material drifted")}}installCorporateCaSystemAnchors(result.application.corporateCaPath);corporateCaMerged=mergeCorporateCa(result.application.corporateCaPath)}const runtimeEnvironment=serializeManagedStartupRuntimeEnvironment(mapped.runtimeEnvironment,corporateCaMerged,mapped.configurationEnvironment,mapped.applicationRuntime);atomicWriteRootFile(MANAGED_STARTUP_RUNTIME_ENV_FILE,runtimeEnvironment,292);if(options.publishCompletion!==false){publishManagedStartupImageCompletion(expectedAgent,result.application.fingerprint,runtimeEnvironment,corporateCaMerged)}return{agent:expectedAgent,adapterApplied:result.adapterApplied,fingerprint:result.application.fingerprint,runtimeEnvironmentFile:MANAGED_STARTUP_RUNTIME_ENV_FILE}}function completionAlreadyPublished(request){try{verifyManagedStartupImageCompletion(request.agent,request.profileFingerprint);return true}catch(error){if(error.code==="ENOENT")return false;throw error}}async function applyManagedStartupRootRequest(request,env=process.env,options={}){requireRoot();const profile=decodeManagedStartupProfile(request.encodedProfile);if(profile.agent!==request.agent||fingerprintManagedStartupProfile(profile)!==request.profileFingerprint){fail5("root application request identity does not match its profile")}const imageEnvironment={HOME:"/root",PATH:FIXED_PATH,NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION:"1",...selectManagedStartupApplicationRuntimeEnvironment(env),[MANAGED_STARTUP_PROFILE_ENV]:request.encodedProfile,...request.corporateCaB64===null?{}:{[MANAGED_STARTUP_CA_ENV]:request.corporateCaB64}};mapManagedStartupProfileToAgentEnvironment(profile,imageEnvironment);normalizeManagedStartupWorkspaceRoot(request.agent);const alreadyPublished=completionAlreadyPublished(request);const bootstrapIdentity=options.bootstrapIdentity??null;const transactionStatus=alreadyPublished&&bootstrapIdentity!==null?getManagedStartupSharedStateTransactionStatus({agent:request.agent,profileFingerprint:request.profileFingerprint,bootstrapIdentity}):null;if(transactionStatus==="none"){fail5("completed startup profile has no shared-state authority for this bootstrap attempt")}if(!alreadyPublished){ensureRootOwnedDirectory(ROOT_STATE_PARENT);beginManagedStartupSharedStateTransaction(profile,{bootstrapIdentity})}const result=await applyManagedStartupImageProfile(request.agent,imageEnvironment,{publishCompletion:bootstrapIdentity===null||alreadyPublished});return{...result,transactionPending:!alreadyPublished||transactionStatus==="pending"}}function readBoundedRootApplyStdin(){const chunks=[];let total=0;while(true){const chunk=Buffer.alloc(16*1024);const read=import_node_fs3.default.readSync(0,chunk,0,chunk.length,null);if(read===0)break;total+=read;if(total>MANAGED_STARTUP_ROOT_APPLY_MAX_BYTES){fail5("root application stdin exceeds its bounded transport")}chunks.push(chunk.subarray(0,read))}const bytes=Buffer.concat(chunks,total);const text=bytes.toString("utf8");if(text.includes("\0")||!Buffer.from(text,"utf8").equals(bytes)){fail5("root application stdin must be valid UTF-8 without NUL bytes")}return text}function writeSandboxFileAtomically(target,contents,mode){const parent=import_node_path3.default.dirname(target);const parentStat=import_node_fs3.default.lstatSync(parent);if(parentStat.isSymbolicLink()||!parentStat.isDirectory()||parentStat.uid!==process.geteuid?.()||parentStat.gid!==process.getegid?.()){fail5(`refusing unsafe sandbox-owned directory ${parent}`)}const temporary=import_node_path3.default.join(parent,`.${import_node_path3.default.basename(target)}.${(0,import_node_crypto7.randomBytes)(12).toString("hex")}`);let descriptor;try{descriptor=import_node_fs3.default.openSync(temporary,import_node_fs3.default.constants.O_CREAT|import_node_fs3.default.constants.O_EXCL|import_node_fs3.default.constants.O_WRONLY|import_node_fs3.default.constants.O_NOFOLLOW,384);import_node_fs3.default.writeFileSync(descriptor,contents);import_node_fs3.default.fchmodSync(descriptor,mode);import_node_fs3.default.fsyncSync(descriptor);import_node_fs3.default.closeSync(descriptor);descriptor=void 0;import_node_fs3.default.renameSync(temporary,target)}catch(error){if(descriptor!==void 0)import_node_fs3.default.closeSync(descriptor);try{import_node_fs3.default.unlinkSync(temporary)}catch{}fail5(`could not write sandbox-owned file ${target}: ${error.message}`)}}function internalWriteHermesCompatHash(){if(process.geteuid?.()===0)fail5("sandbox hash writer must not run as root");const encoded=process.env.NEMOCLAW_MANAGED_HERMES_HASH_B64??"";if(encoded.length===0||encoded.length>4096||!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(encoded)){fail5("Hermes compatibility hash transport is invalid")}const decoded=Buffer.from(encoded,"base64");if(decoded.toString("base64")!==encoded){fail5("Hermes compatibility hash transport is non-canonical")}writeSandboxFileAtomically("/sandbox/.hermes/.config-hash",decoded.toString("utf8"),416)}function readCliAgent(argv,expectedLength=2){const index=argv.indexOf("--agent");if(index<0||index+1>=argv.length||argv.length!==expectedLength){fail5("usage: managed-startup-image-runtime [--apply-root-stdin|--release-startup-hold|--wait-for-completion|--verify-completion|--begin-shared-state-transaction|--commit-shared-state-transaction|--rollback-shared-state-transaction|--clear-shared-state-commit-receipt|--shared-state-transaction-status] --agent ")}return argv[index+1]}function readCliFingerprint(argv){const index=argv.indexOf("--profile-fingerprint");if(index<0||index+1>=argv.length){fail5("managed startup profile fingerprint argument is missing")}return argv[index+1]}function readCliBootstrapIdentity(argv){const index=argv.indexOf("--bootstrap-identity");if(index<0||index+1>=argv.length||!SHA256_RE4.test(String(argv[index+1]??""))){fail5("managed bootstrap identity argument is missing or invalid")}return argv[index+1]}async function main(argv=process.argv.slice(2)){if(argv.length===1&&argv[0]==="--internal-write-hermes-compat-hash"){internalWriteHermesCompatHash();return}if((argv.length===3||argv.length===5)&&argv[0]==="--apply-root-stdin"){const expectedAgent=exactAgent2(readCliAgent(argv,argv.length));const request=parseManagedStartupRootApplyRequest(readBoundedRootApplyStdin());if(request.agent!==expectedAgent){fail5(`root application request targets ${request.agent}, expected ${expectedAgent}`)}const result2=await applyManagedStartupRootRequest(request,process.env,{bootstrapIdentity:argv.length===5?readCliBootstrapIdentity(argv):null});console.log(result2.transactionPending?`[managed-startup] applied ${result2.agent} profile ${result2.fingerprint}; transaction pending`:`[managed-startup] ${result2.agent} profile ${result2.fingerprint} was already complete`);return}if(argv.length===7&&argv[0]==="--release-startup-hold"){const agent=readCliAgent(argv,7);const fingerprint=readCliFingerprint(argv);const bootstrapIdentity=readCliBootstrapIdentity(argv);publishManagedStartupCompletionAfterCommit(agent,fingerprint,bootstrapIdentity);console.log(`[managed-startup] released ${agent} profile ${fingerprint} startup hold`);return}if((argv.length===5||argv.length===7)&&(argv[0]==="--verify-completion"||argv[0]==="--wait-for-completion")){const agent=readCliAgent(argv,argv.length);const fingerprint=readCliFingerprint(argv);if(argv.length===7)readCliBootstrapIdentity(argv);const result2=argv[0]==="--wait-for-completion"?waitForManagedStartupImageCompletion(agent,fingerprint):verifyManagedStartupImageCompletion(agent,fingerprint);console.log(`[managed-startup] verified ${result2.agent} profile ${result2.fingerprint} completion`);return}if(argv.length===3&&argv[0]==="--begin-shared-state-transaction"){const profile=managedTransactionProfile(readCliAgent(argv,3));ensureRootOwnedDirectory(ROOT_STATE_PARENT);const created=beginManagedStartupSharedStateTransaction(profile);process.stdout.write(created?"created\n":"pending\n");return}if((argv.length===4||argv.length===6)&&argv[0]==="--rollback-shared-state-transaction"&&argv[argv.length-1]==="--read-only-receipt"){requireRoot();const agent=exactAgent2(readCliAgent(argv,argv.length));const rolledBack=rollbackManagedStartupSharedStateTransaction(agent,{transactionDirectory:MANAGED_STARTUP_SHARED_ROLLBACK_RECEIPT_DIRECTORY,readOnlyReceipt:true,bootstrapIdentity:argv.length===6?readCliBootstrapIdentity(argv):null});if(!rolledBack)fail5("read-only shared-state rollback receipt is missing");console.log(`[managed-startup] verified and restored ${agent} shared state`);return}if((argv.length===3||argv.length===5)&&argv[0]==="--rollback-shared-state-transaction"){requireRoot();const agent=exactAgent2(readCliAgent(argv,argv.length));const rolledBack=rollbackManagedStartupSharedStateTransaction(agent,{bootstrapIdentity:argv.length===5?readCliBootstrapIdentity(argv):null});if(!rolledBack)fail5("managed startup transaction is missing at rollback");console.log(`[managed-startup] verified and restored ${agent} shared state`);return}if((argv.length===3||argv.length===5)&&argv[0]==="--commit-shared-state-transaction"){requireRoot();const agent=exactAgent2(readCliAgent(argv,argv.length));if(!commitManagedStartupSharedStateTransaction(agent,{bootstrapIdentity:argv.length===5?readCliBootstrapIdentity(argv):null})){fail5("managed startup transaction is missing at commit")}console.log(`[managed-startup] committed ${agent} shared state`);return}if(argv.length===5&&argv[0]==="--clear-shared-state-commit-receipt"){requireRoot();const agent=exactAgent2(readCliAgent(argv,5));const bootstrapIdentity=readCliBootstrapIdentity(argv);if(!clearManagedStartupSharedStateCommitReceipt(agent,{bootstrapIdentity})){fail5("managed startup durable commit receipt is missing at cleanup")}console.log(`[managed-startup] cleared ${agent} durable shared-state commit receipt`);return}if(argv.length===7&&argv[0]==="--shared-state-transaction-status"){requireRoot();const agent=exactAgent2(readCliAgent(argv,7));const profileFingerprint=readCliFingerprint(argv);const bootstrapIdentity=readCliBootstrapIdentity(argv);process.stdout.write(`${getManagedStartupSharedStateTransactionStatus({agent,profileFingerprint,bootstrapIdentity})} `);return}if(argv.length===8&&argv[0]==="--shared-state-transaction-status"&&argv[7]==="--read-only-receipt"){requireRoot();const agent=exactAgent2(readCliAgent(argv,8));const profileFingerprint=readCliFingerprint(argv);const bootstrapIdentity=readCliBootstrapIdentity(argv);process.stdout.write(`${getManagedStartupSharedStateTransactionStatus({agent,profileFingerprint,bootstrapIdentity},{readOnlyReceipt:true})} -`);return}const result=await applyManagedStartupImageProfile(readCliAgent(argv));console.log(result.adapterApplied?`[managed-startup] applied ${result.agent} profile ${result.fingerprint}`:`[managed-startup] ${result.agent} profile ${result.fingerprint} is already committed`)}if(require.main===module){void main().catch(error=>{console.error(error instanceof Error?error.message:String(error));process.exitCode=1})}0&&(module.exports={MANAGED_STARTUP_CA_ENV,MANAGED_STARTUP_COMPLETION_FILE,MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION,MANAGED_STARTUP_MERGED_CA_FILE,MANAGED_STARTUP_PROFILE_ENV,MANAGED_STARTUP_RUNTIME_ENV_FILE,MANAGED_STARTUP_RUNTIME_EXECUTABLE,ManagedStartupImageActionPlanError,ManagedStartupImageRuntimeError,NVIDIA_HOSTED_CREDENTIAL_ENV,NVIDIA_HOSTED_NATIVE_ENDPOINT,applyManagedStartupCommandEnvironmentPlan,applyManagedStartupImageProfile,applyManagedStartupRootRequest,atomicWriteRootFile,buildManagedStartupImageActionPlan,hostedNativeProvider,installCorporateCaSystemAnchors,installHermesManagedPolicy,main,managedStartupSandboxPrefix,normalizeHermesManagedConfigDescriptor,normalizeManagedStartupWorkspaceRoot,publishManagedStartupCompletionAfterCommit,readStableRegularFile,readStableRegularFileSnapshot,serializeManagedStartupCompletionMarker,serializeManagedStartupRuntimeEnvironment,validateManagedStartupApplicationRuntimePlan,verifyManagedStartupImageCompletion,waitForManagedStartupImageCompletion}); +`);return}const result=await applyManagedStartupImageProfile(readCliAgent(argv));console.log(result.adapterApplied?`[managed-startup] applied ${result.agent} profile ${result.fingerprint}`:`[managed-startup] ${result.agent} profile ${result.fingerprint} is already committed`)}if(require.main===module){void main().catch(error=>{console.error(error instanceof Error?error.message:String(error));process.exitCode=1})}0&&(module.exports={MANAGED_STARTUP_CA_ENV,MANAGED_STARTUP_COMPLETION_FILE,MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION,MANAGED_STARTUP_MERGED_CA_FILE,MANAGED_STARTUP_OWNER_RESULT_MAX_BYTES,MANAGED_STARTUP_PROFILE_ENV,MANAGED_STARTUP_RUNTIME_ENV_FILE,MANAGED_STARTUP_RUNTIME_EXECUTABLE,ManagedStartupImageActionPlanError,ManagedStartupImageRuntimeError,NVIDIA_HOSTED_CREDENTIAL_ENV,NVIDIA_HOSTED_NATIVE_ENDPOINT,applyManagedStartupCommandEnvironmentPlan,applyManagedStartupImageProfile,applyManagedStartupRootRequest,atomicWriteRootFile,buildManagedStartupImageActionPlan,hostedNativeProvider,installCorporateCaSystemAnchors,installHermesManagedPolicy,main,managedStartupSandboxPrefix,normalizeHermesManagedConfigDescriptor,normalizeManagedStartupWorkspaceRoot,publishManagedStartupCompletionAfterCommit,readStableRegularFile,readStableRegularFileSnapshot,serializeManagedStartupCompletionMarker,serializeManagedStartupRuntimeEnvironment,validateManagedStartupApplicationRuntimePlan,verifyManagedStartupImageCompletion,waitForManagedStartupImageCompletion});