diff --git a/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index 9ee8d603e7d..26616ed0e34 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -37,6 +37,7 @@ on: - "nemoclaw-blueprint/**" - "scripts/**" - "test/e2e/live/managed-image-activation-e2e*.ts" + - "src/lib/actions/sandbox/mcp-bridge-*.ts" - "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json" - "src/lib/core/json-types.ts" - "src/lib/core/ports.ts" diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 3da2bd425b3..71d7d6791b8 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -42,6 +42,7 @@ on: - "nemoclaw-blueprint/**" - "scripts/**" - "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json" + - "src/lib/actions/sandbox/mcp-bridge-*.ts" - "src/lib/core/json-types.ts" - "src/lib/core/ports.ts" - "src/lib/messaging/**" @@ -832,12 +833,12 @@ jobs: } reference="${IMAGE}@${DIGEST}" raw="$RUNNER_TEMP/${AGENT}-published-manifest.raw" + scripts/checks/pull-public-exact-digest.sh "$reference" linux/amd64 docker buildx imagetools inspect "$reference" --raw > "$raw" [[ "sha256:$(sha256sum "$raw" | awk '{print $1}')" == "$DIGEST" ]] || { echo "ERROR: published PR manifest bytes do not match the build digest" >&2 exit 1 } - scripts/checks/pull-public-exact-digest.sh "$reference" linux/amd64 release="v$(node -p 'require("./package.json").version')" contract_dir="$RUNNER_TEMP/managed-pr-contract" mkdir -p "$contract_dir" @@ -957,7 +958,7 @@ jobs: retention-days: 1 pr-openclaw-mcp-discovery: - name: PR exact OpenClaw trusted-private MCP discovery (pass ${{ matrix.pass }}) + name: PR exact OpenClaw managed-image MCP discovery (pass ${{ matrix.pass }}) needs: pr-build-and-entrypoint if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-24.04 @@ -979,11 +980,12 @@ jobs: NEMOCLAW_E2E_REQUIRE_EXECUTED_TEST: "1" NEMOCLAW_E2E_SHARD: openclaw NEMOCLAW_MCP_BRIDGE_AGENT: openclaw + NEMOCLAW_MCP_BRIDGE_E2E_SCOPE: managed-image-discovery NEMOCLAW_NON_INTERACTIVE: "1" NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF: "1" NEMOCLAW_RUN_LIVE_E2E: "1" NEMOCLAW_SANDBOX_NAME: e2e-pr-exact-mcp-${{ matrix.pass }} - OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:b58be5e40c788977ffa0e8305a8cad9c656efdf1a3fe182582a00ca870bb0edb + OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:722f44669722961b7f432b0b81de25b91a58f34a61d6403bef967acaf2b3af01 steps: - name: Checkout exact PR head uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -1056,7 +1058,7 @@ jobs: NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1" run: bash scripts/install-openshell.sh - - name: Run exact OpenClaw trusted-private MCP discovery + - name: Run exact OpenClaw managed-image MCP discovery shell: bash run: | set -euo pipefail diff --git a/ci/onboard-entry-composition-budget.json b/ci/onboard-entry-composition-budget.json index 6d0814d184e..3acda42d863 100644 --- a/ci/onboard-entry-composition-budget.json +++ b/ci/onboard-entry-composition-budget.json @@ -13,7 +13,7 @@ "policy": { "createOnboardPolicyApplication.getRecordedPolicyTier": 1, "preflightAuthoritativeRebuildTarget": 1, - "runOnboard": 6, + "runOnboard": 5, "sandboxCreateIntentResolver.getAgentPolicyPath": 1 }, "provider": { diff --git a/nemoclaw-blueprint/provider-profiles/openai.yaml b/nemoclaw-blueprint/provider-profiles/openai.yaml new file mode 100644 index 00000000000..d8cd7699720 --- /dev/null +++ b/nemoclaw-blueprint/provider-profiles/openai.yaml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +id: openai +display_name: OpenAI-compatible gateway route +description: Endpointless profile for credentials consumed by the OpenShell inference gateway +category: inference +credentials: [] +endpoints: [] +binaries: [] +inference_capable: true diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.test.ts index fdddbab4f8c..cc892b8dc0d 100644 --- a/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.test.ts +++ b/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.test.ts @@ -18,6 +18,7 @@ import { OPENCLAW_MCPORTER_ROOT, } from "./mcp-bridge-adapter-openclaw"; import { + entryHeaders, buildOpenClawMcporterInspectCommand, mcporterHeadersMatchExpected, openClawMcporterRoot, @@ -55,6 +56,15 @@ describe("OpenClaw mcporter MCP adapter", testTimeoutOptions(20_000), () => { ), ).toBe(true); expect(mcporterHeadersMatchExpected(expected, expected)).toBe(true); + expect( + mcporterHeadersMatchExpected( + { + Authorization: "Bearer openshell:resolve:env:v1442987827285932589_GITHUB_TOKEN", + accept: "application/json, text/event-stream", + }, + expected, + ), + ).toBe(true); expect( mcporterHeadersMatchExpected( { @@ -85,6 +95,51 @@ describe("OpenClaw mcporter MCP adapter", testTimeoutOptions(20_000), () => { ).toBe(false); }); + it.each([ + "Bearer openshell:resolve:env:v_GITHUB_TOKEN", + "Bearer openshell:resolve:env:v42_OTHER_TOKEN", + "Bearer openshell:resolve:env:v42x_GITHUB_TOKEN", + `Bearer openshell:resolve:env:v${"1".repeat(21)}_GITHUB_TOKEN`, + ])("rejects an unsafe revisioned mcporter Authorization header: %s", (authorization) => { + expect( + mcporterHeadersMatchExpected( + { Authorization: authorization }, + { Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" }, + ), + ).toBe(false); + }); + + it("projects the live OpenShell credential revision into mcporter config", () => { + const command = buildOpenClawMcporterRegisterCommand( + baseEntry, + false, + OPENCLAW_MCPORTER_ROOT, + "v1442987827285932589", + ); + + expect(command).toContain( + "Authorization=Bearer openshell:resolve:env:v1442987827285932589_GITHUB_TOKEN", + ); + expect(command).not.toContain("Authorization=Bearer openshell:resolve:env:GITHUB_TOKEN'"); + }); + + it("matches the exact readiness-proven revision during post-write inspection", () => { + const expectedV12 = entryHeaders(baseEntry, "v12"); + + expect( + mcporterHeadersMatchExpected( + { Authorization: "Bearer openshell:resolve:env:v12_GITHUB_TOKEN" }, + expectedV12, + ), + ).toBe(true); + expect( + mcporterHeadersMatchExpected( + { Authorization: "Bearer openshell:resolve:env:v11_GITHUB_TOKEN" }, + expectedV12, + ), + ).toBe(false); + }); + it("registers, inspects, and removes the OpenClaw workspace project config", () => { const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcporter-owner-")); try { diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts index 6cedfb5f3fd..7edfaab81e3 100644 --- a/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts +++ b/src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts @@ -20,6 +20,7 @@ import { } from "./mcp-bridge-adapter-status"; import { McpBridgeError } from "./mcp-bridge-contracts"; import { redactBridgeSecretsForDisplay } from "./mcp-bridge-output"; +import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness"; import { getAgentConfigDir } from "./mcp-bridge-state"; import { executeSandboxCommand } from "./process-recovery"; @@ -50,9 +51,10 @@ export function buildOpenClawMcporterRegisterCommand( entry: McpBridgeEntry, replaceExisting = false, root = OPENCLAW_MCPORTER_ROOT, + credentialRevision?: McpAttachedCredentialRevision, ): string { const args = mcporterArgs(root, "config", "add", entry.server, "--url", entry.url); - const authorization = authorizationValue(entry); + const authorization = authorizationValue(entry, credentialRevision); if (authorization) args.push("--header", `Authorization=${authorization}`); args.push("--scope", "project"); const addCommand = args.map(shellQuote).join(" "); @@ -142,12 +144,13 @@ export function registerOpenClawAdapter( entry: McpBridgeEntry, envValues: Record = {}, replaceExisting = false, + credentialRevision?: McpAttachedCredentialRevision, ): void { ensureMcporter(sandboxName); const root = mcporterRootForEntry(entry); const result = executeSandboxCommand( sandboxName, - buildOpenClawMcporterRegisterCommand(entry, replaceExisting, root), + buildOpenClawMcporterRegisterCommand(entry, replaceExisting, root, credentialRevision), ); const output = redactBridgeSecretsForDisplay( [result?.stdout, result?.stderr].filter(Boolean).join("\n").trim(), @@ -164,7 +167,7 @@ export function registerOpenClawAdapter( // from the URL and opaque OpenShell placeholder NemoClaw intended. const verification = executeSandboxCommand( sandboxName, - buildOpenClawMcporterInspectCommand(entry, true, root), + buildOpenClawMcporterInspectCommand(entry, true, root, credentialRevision), ); const verificationOutput = redactBridgeSecretsForDisplay( [verification?.stdout, verification?.stderr].filter(Boolean).join("\n").trim(), diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-registration.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-registration.test.ts index 8e21ce5f0c7..d4d35ccc0a5 100644 --- a/src/lib/actions/sandbox/mcp-bridge-adapter-registration.test.ts +++ b/src/lib/actions/sandbox/mcp-bridge-adapter-registration.test.ts @@ -26,6 +26,11 @@ import { buildHermesMcpStatusCommand, registerAgentAdapter, } from "./mcp-bridge-adapters"; +import { registerOpenClawAdapter } from "./mcp-bridge-adapter-openclaw"; +import { + entryHeaders, + mcporterHeadersMatchExpected, +} from "./mcp-bridge-adapter-status"; const baseEntry: McpBridgeEntry = { server: "github", @@ -114,3 +119,38 @@ describe.each(adapterCases)("$name MCP adapter registration", (adapterCase) => { ).toThrow(`${adapterCase.adapter} config verification failed after adding 'github': mismatch.`); }); }); + +describe("OpenClaw MCP adapter registration", () => { + beforeEach(() => { + mocks.executeSandboxCommand.mockReset(); + }); + + it("rejects a v11 post-write observation after registering the readiness-proven v12", () => { + const entry: McpBridgeEntry = { + ...baseEntry, + agent: "openclaw", + adapter: "mcporter", + }; + const actualV11Headers = { + Authorization: "Bearer openshell:resolve:env:v11_GITHUB_TOKEN", + }; + const verification = mcporterHeadersMatchExpected(actualV11Headers, entryHeaders(entry, "v12")) + ? registered + : mismatch; + mocks.executeSandboxCommand + .mockReturnValueOnce({ status: 0, stdout: "/usr/bin/mcporter\n", stderr: "" }) + .mockReturnValueOnce(commandSuccess) + .mockReturnValueOnce(verification); + + expect(() => + registerOpenClawAdapter("alpha", entry, { GITHUB_TOKEN: "host-only-secret" }, false, "v12"), + ).toThrow("mcporter config verification failed after adding 'github': mismatch"); + + expect(mocks.executeSandboxCommand.mock.calls[1]?.[1]).toContain( + "Authorization=Bearer openshell:resolve:env:v12_GITHUB_TOKEN", + ); + expect(mocks.executeSandboxCommand.mock.calls[2]?.[1]).toContain( + "Bearer openshell:resolve:env:v12_GITHUB_TOKEN", + ); + }); +}); diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts index 309e2dfc3f2..5764e81c2d1 100644 --- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts +++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { McpBridgeEntry } from "../../state/registry"; +import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness"; import { DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS, DEEPAGENTS_STRICT_JSON_HELPERS, @@ -21,18 +22,30 @@ export const OPENCLAW_MCPORTER_ROOT = openClawMcporterRoot(); const DEFAULT_AUTH_HEADER = "Authorization"; const DEFAULT_AUTH_SCHEME = "Bearer"; -function authPlaceholder(entry: Pick): string | null { +function authPlaceholder( + entry: Pick, + credentialRevision?: McpAttachedCredentialRevision, +): string | null { const envName = entry.env[0]; - return envName ? `openshell:resolve:env:${envName}` : null; + if (!envName) return null; + const revision = + credentialRevision && credentialRevision !== "canonical" ? `${credentialRevision}_` : ""; + return `openshell:resolve:env:${revision}${envName}`; } -export function authorizationValue(entry: Pick): string | null { - const placeholder = authPlaceholder(entry); +export function authorizationValue( + entry: Pick, + credentialRevision?: McpAttachedCredentialRevision, +): string | null { + const placeholder = authPlaceholder(entry, credentialRevision); return placeholder ? `${DEFAULT_AUTH_SCHEME} ${placeholder}` : null; } -export function entryHeaders(entry: Pick): Record { - const authorization = authorizationValue(entry); +export function entryHeaders( + entry: Pick, + credentialRevision?: McpAttachedCredentialRevision, +): Record { + const authorization = authorizationValue(entry, credentialRevision); return authorization ? { [DEFAULT_AUTH_HEADER]: authorization } : {}; } @@ -45,7 +58,9 @@ export function pythonJsonLiteral(value: unknown): string { * `config get --json` with an `accept: application/json, text/event-stream` * header, even when that header is absent from the persisted config. Treat * only that synthesized header as equivalent; every persisted/other header - * remains part of the ownership fingerprint. + * remains part of the ownership fingerprint. When the expected placeholder is + * canonical, a strictly bounded revisioned form of the same credential is also + * equivalent. A revisioned expectation remains exact. * * This function is also serialized into the in-sandbox inspection commands, * so keep it self-contained (no references to module-scope values). @@ -59,7 +74,23 @@ export function mcporterHeadersMatchExpected( } const actualHeaders = actual as Record; for (const [name, value] of Object.entries(expected)) { - if (actualHeaders[name] !== value) return false; + const actualValue = actualHeaders[name]; + if (actualValue === value) continue; + if (name.toLowerCase() !== "authorization") return false; + const prefix = "Bearer openshell:resolve:env:"; + if ( + typeof actualValue !== "string" || + !value.startsWith(prefix) || + !actualValue.startsWith(prefix) + ) { + return false; + } + const envName = value.slice(prefix.length); + const versioned = actualValue.slice(prefix.length); + const suffix = `_${envName}`; + if (!versioned.startsWith("v") || !versioned.endsWith(suffix)) return false; + const revision = versioned.slice(1, -suffix.length); + if (!/^[0-9]{1,20}$/u.test(revision)) return false; } const extraNames = Object.keys(actualHeaders).filter((name) => !Object.hasOwn(expected, name)); if (extraNames.length === 0) return true; @@ -164,11 +195,12 @@ export function buildOpenClawMcporterInspectCommand( entry: McpBridgeEntry, failOnMismatch: boolean, root = OPENCLAW_MCPORTER_ROOT, + credentialRevision?: McpAttachedCredentialRevision, ): string { const payload = { server: entry.server, url: entry.url, - headers: entryHeaders(entry), + headers: entryHeaders(entry, credentialRevision), failOnMismatch, root, }; diff --git a/src/lib/actions/sandbox/mcp-bridge-adapters.ts b/src/lib/actions/sandbox/mcp-bridge-adapters.ts index 7f342fe55f8..d705b5341c2 100644 --- a/src/lib/actions/sandbox/mcp-bridge-adapters.ts +++ b/src/lib/actions/sandbox/mcp-bridge-adapters.ts @@ -26,6 +26,7 @@ import { registerOpenClawAdapter, unregisterOpenClawAdapter, } from "./mcp-bridge-adapter-openclaw"; +import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness"; export { buildDeepAgentsMcpRegisterCommand, @@ -125,11 +126,21 @@ export function registerAgentAdapter( adapter: AgentMcpAdapter, entry: McpBridgeEntry, envValues: Record = {}, - options: { replaceExisting?: boolean; teardownRollback?: boolean } = {}, + options: { + replaceExisting?: boolean; + teardownRollback?: boolean; + credentialRevision?: McpAttachedCredentialRevision; + } = {}, ): void { switch (adapter) { case "mcporter": - registerOpenClawAdapter(sandboxName, entry, envValues, options.replaceExisting === true); + registerOpenClawAdapter( + sandboxName, + entry, + envValues, + options.replaceExisting === true, + options.credentialRevision, + ); return; case "hermes-config": registerHermesAdapter(sandboxName, entry, envValues, options.replaceExisting === true); diff --git a/src/lib/actions/sandbox/mcp-bridge-add-restart.ts b/src/lib/actions/sandbox/mcp-bridge-add-restart.ts index c1a1b4773f2..77f8274e02b 100644 --- a/src/lib/actions/sandbox/mcp-bridge-add-restart.ts +++ b/src/lib/actions/sandbox/mcp-bridge-add-restart.ts @@ -446,7 +446,7 @@ async function addMcpBridgeUnlocked( requireExisting: true, }); } - waitForAttachedMcpCredential(sandboxName, entry, { + const credentialRevision = waitForAttachedMcpCredential(sandboxName, entry, { ...(providerResult.action === "updated" ? { previousRevision: previousCredentialRevision, @@ -482,7 +482,10 @@ async function addMcpBridgeUnlocked( registerAgentAdapter(sandboxName, adapter, entry, adapterEnvValues, { // An exact adapter entry is evidence of a post-commit process death. // Replacing it is idempotent and, for Hermes, re-verifies runtime reload. + // Mcporter must project the same live revision OpenShell will recognize + // at egress; its canonical, unversioned placeholder is not sufficient. replaceExisting: resumingPreflightedAdd && adapterInspection.state === "registered", + credentialRevision, }); if (adapter === "hermes-config") assertHermesMcpRuntimeIntent(sandboxName); const { addState: _completedAddState, ...committedEntry } = entry; diff --git a/src/lib/actions/sandbox/mcp-bridge-policy.test.ts b/src/lib/actions/sandbox/mcp-bridge-policy.test.ts index 35a7a6430da..23315de2c5a 100644 --- a/src/lib/actions/sandbox/mcp-bridge-policy.test.ts +++ b/src/lib/actions/sandbox/mcp-bridge-policy.test.ts @@ -113,6 +113,32 @@ describe("MCP OpenShell policy", () => { expect(removePreset).not.toHaveBeenCalled(); }); + it("can remove the live policy while preserving rebuild journal ownership (#9792)", () => { + const entry = githubBridgeEntry(); + const registration = { + name: entry.policyName, + content: "network_policies:\n mcp_bridge_github: {}\n", + sourcePath: MCP_BRIDGE_POLICY_SOURCE, + }; + vi.spyOn(registry, "getCustomPolicies").mockReturnValue([registration]); + vi.spyOn(policies, "getPresetContentGatewayState") + .mockReturnValueOnce("match") + .mockReturnValueOnce("absent"); + const removePreset = vi.spyOn(policies, "removePreset").mockReturnValue(true); + const removeOwnership = vi.spyOn(registry, "removeCustomPolicyByName"); + + expect(() => + removeGeneratedPolicy("alpha", entry, { preserveRegistryOwnership: true }), + ).not.toThrow(); + + expect(removePreset).toHaveBeenCalledWith( + "alpha", + entry.policyName, + expect.objectContaining({ skipRegistryUpdate: true }), + ); + expect(removeOwnership).not.toHaveBeenCalled(); + }); + it("pins DNS answers while constraining the generic mcporter Node grant", () => { const policyName = buildMcpBridgePolicyName("GitHub_Server"); const policy = YAML.parse( diff --git a/src/lib/actions/sandbox/mcp-bridge-policy.ts b/src/lib/actions/sandbox/mcp-bridge-policy.ts index f89b0db93e8..b6424d61c6c 100644 --- a/src/lib/actions/sandbox/mcp-bridge-policy.ts +++ b/src/lib/actions/sandbox/mcp-bridge-policy.ts @@ -891,7 +891,7 @@ export function assertGeneratedPolicyExactReadOnly( export function removeGeneratedPolicy( sandboxName: string, entry: McpBridgeEntry, - options: { bestEffort?: boolean } = {}, + options: { bestEffort?: boolean; preserveRegistryOwnership?: boolean } = {}, ): void { const policyName = entry.policyName; const registeredPolicy = registry @@ -910,7 +910,7 @@ export function removeGeneratedPolicy( ? policies.getPresetContentGatewayState(sandboxName, content) : getUnownedGeneratedPolicyState(sandboxName, entry)); if (gatewayState === "absent") { - if (ownsRegistration) { + if (ownsRegistration && !options.preserveRegistryOwnership) { registry.removeCustomPolicyByName(sandboxName, policyName); } return; @@ -937,7 +937,9 @@ export function removeGeneratedPolicy( } const activeState = policies.getPresetContentGatewayState(sandboxName, content); if (activeState === "absent") { - registry.removeCustomPolicyByName(sandboxName, policyName); + if (!options.preserveRegistryOwnership) { + registry.removeCustomPolicyByName(sandboxName, policyName); + } return; } // Keep (or defensively restore) the last reconciled ownership record when diff --git a/src/lib/actions/sandbox/mcp-bridge-provider-mutation.ts b/src/lib/actions/sandbox/mcp-bridge-provider-mutation.ts index b2a249051ef..c7ba8c52dca 100644 --- a/src/lib/actions/sandbox/mcp-bridge-provider-mutation.ts +++ b/src/lib/actions/sandbox/mcp-bridge-provider-mutation.ts @@ -44,26 +44,88 @@ export { providerDetachChangedState, } from "./mcp-bridge-provider-attachments"; -function profileHasExpectedCredentialBoundary(output: string): boolean { +const OPENAI_GATEWAY_PROVIDER_TYPE = "openai"; + +function profileHasExpectedCredentialBoundary( + output: string, + expected: { id: string; inferenceCapable: boolean }, +): boolean { try { const parsed = JSON.parse(output) as Record; return ( - parsed.id === MCP_BRIDGE_PROVIDER_TYPE && + parsed.id === expected.id && Array.isArray(parsed.credentials) && parsed.credentials.length === 0 && Array.isArray(parsed.endpoints) && parsed.endpoints.length === 0 && Array.isArray(parsed.binaries) && parsed.binaries.length === 0 && - parsed.inference_capable === false + parsed.inference_capable === expected.inferenceCapable ); } catch { return false; } } +/** + * OpenShell 0.0.106 still accepts the legacy `openai` provider type without a + * declarative profile. Its static-credential resolver then emits the provider + * key without endpoint metadata, causing the supervisor to reject the whole + * provider environment as unclassified when an MCP provider is attached. + * Registering an endpointless profile makes the gateway-only inference key + * explicitly non-injectable while preserving OpenShell's inference route. + */ +function ensureOpenAiGatewayProviderProfile(): void { + const profilePath = path.resolve( + __dirname, + "../../../..", + "nemoclaw-blueprint", + "provider-profiles", + "openai.yaml", + ); + const imported = runOpenshellProviderCommand( + ["provider", "profile", "import", "--file", profilePath], + { + ignoreError: true, + stdio: ["ignore", "pipe", "pipe"], + }, + ) as OpenShellCommandResult; + if (imported.status === 0) return; + + const importOutput = commandOutput(imported); + if (!/already exists/i.test(importOutput)) { + throw new McpBridgeError( + importOutput || "Could not import the OpenShell OpenAI gateway provider profile.", + ); + } + + const exported = runOpenshellProviderCommand( + ["provider", "profile", "export", OPENAI_GATEWAY_PROVIDER_TYPE, "--output", "json"], + { + ignoreError: true, + stdio: ["ignore", "pipe", "pipe"], + }, + ) as OpenShellCommandResult; + if (exported.status !== 0) { + throw new McpBridgeError( + `OpenShell provider profile '${OPENAI_GATEWAY_PROVIDER_TYPE}' already exists but could not be exported for validation. Refusing to classify gateway inference credentials with it.`, + ); + } + if ( + !profileHasExpectedCredentialBoundary(String(exported.stdout), { + id: OPENAI_GATEWAY_PROVIDER_TYPE, + inferenceCapable: true, + }) + ) { + throw new McpBridgeError( + `OpenShell provider profile '${OPENAI_GATEWAY_PROVIDER_TYPE}' already exists but does not match NemoClaw's gateway-only endpointless credential contract. Refusing to classify gateway inference credentials with it.`, + ); + } +} + /** Ensure the endpointless profile required by OpenShell static credential binding. */ export function ensureMcpBridgeProviderProfile(): void { + ensureOpenAiGatewayProviderProfile(); const profilePath = path.resolve( __dirname, "../../../..", @@ -94,7 +156,13 @@ export function ensureMcpBridgeProviderProfile(): void { stdio: ["ignore", "pipe", "pipe"], }, ) as OpenShellCommandResult; - if (exported.status !== 0 || !profileHasExpectedCredentialBoundary(String(exported.stdout))) { + if ( + exported.status !== 0 || + !profileHasExpectedCredentialBoundary(String(exported.stdout), { + id: MCP_BRIDGE_PROVIDER_TYPE, + inferenceCapable: false, + }) + ) { throw new McpBridgeError( `OpenShell provider profile '${MCP_BRIDGE_PROVIDER_TYPE}' already exists but does not match NemoClaw's endpointless credential contract. Refusing to attach MCP credentials to it.`, ); diff --git a/src/lib/actions/sandbox/mcp-bridge-provider-profile.test.ts b/src/lib/actions/sandbox/mcp-bridge-provider-profile.test.ts index ea56e0bc1cf..9b598e0c324 100644 --- a/src/lib/actions/sandbox/mcp-bridge-provider-profile.test.ts +++ b/src/lib/actions/sandbox/mcp-bridge-provider-profile.test.ts @@ -14,37 +14,55 @@ afterEach(() => { setProviderCommandRuntimeHooksForTest({}); }); +function exportedEndpointlessProfile(id: string, inferenceCapable: boolean): string { + return JSON.stringify({ + id, + credentials: [], + endpoints: [], + binaries: [], + inference_capable: inferenceCapable, + }); +} + describe("OpenShell MCP provider profile", () => { it("imports the endpointless profile before managed provider use", () => { const runOpenshell = vi.fn(() => ({ status: 0, stdout: "Imported", stderr: "" })); setProviderCommandRuntimeHooksForTest({ runOpenshell: runOpenshell as never }); expect(() => ensureMcpBridgeProviderProfile()).not.toThrow(); - expect(runOpenshell).toHaveBeenCalledOnce(); + expect(runOpenshell).toHaveBeenCalledTimes(2); + expect(runOpenshell).toHaveBeenCalledWith( + ["provider", "profile", "import", "--file", expect.stringMatching(/openai\.yaml$/)], + expect.any(Object), + ); expect(runOpenshell).toHaveBeenCalledWith( ["provider", "profile", "import", "--file", expect.stringMatching(/nemoclaw-mcp-v1\.yaml$/)], expect.any(Object), ); }); - it("accepts an existing profile only after proving the exact endpointless boundary", () => { + it("accepts existing profiles only after proving both exact endpointless boundaries", () => { const runOpenshell = vi .fn() .mockReturnValueOnce({ status: 1, stdout: "", stderr: "already exists" }) .mockReturnValueOnce({ status: 0, - stdout: JSON.stringify({ - id: MCP_BRIDGE_PROVIDER_TYPE, - credentials: [], - endpoints: [], - binaries: [], - inference_capable: false, - }), + stdout: exportedEndpointlessProfile("openai", true), + stderr: "", + }) + .mockReturnValueOnce({ status: 1, stdout: "", stderr: "already exists" }) + .mockReturnValueOnce({ + status: 0, + stdout: exportedEndpointlessProfile(MCP_BRIDGE_PROVIDER_TYPE, false), stderr: "", }); setProviderCommandRuntimeHooksForTest({ runOpenshell: runOpenshell as never }); expect(() => ensureMcpBridgeProviderProfile()).not.toThrow(); + expect(runOpenshell).toHaveBeenCalledWith( + ["provider", "profile", "export", "openai", "--output", "json"], + expect.any(Object), + ); expect(runOpenshell).toHaveBeenCalledWith( ["provider", "profile", "export", MCP_BRIDGE_PROVIDER_TYPE, "--output", "json"], expect.any(Object), @@ -55,6 +73,12 @@ describe("OpenShell MCP provider profile", () => { const runOpenshell = vi .fn() .mockReturnValueOnce({ status: 1, stdout: "", stderr: "already exists" }) + .mockReturnValueOnce({ + status: 0, + stdout: exportedEndpointlessProfile("openai", true), + stderr: "", + }) + .mockReturnValueOnce({ status: 1, stdout: "", stderr: "already exists" }) .mockReturnValueOnce({ status: 0, stdout: JSON.stringify({ @@ -72,4 +96,64 @@ describe("OpenShell MCP provider profile", () => { /does not match NemoClaw's endpointless credential contract/, ); }); + + it("fails closed when the gateway-only OpenAI profile cannot be registered", () => { + const runOpenshell = vi.fn(() => ({ status: 1, stdout: "", stderr: "import rejected" })); + setProviderCommandRuntimeHooksForTest({ runOpenshell: runOpenshell as never }); + + expect(() => ensureMcpBridgeProviderProfile()).toThrow("import rejected"); + expect(runOpenshell).toHaveBeenCalledOnce(); + }); + + it.each([ + [ + "endpoint authority", + JSON.stringify({ + id: "openai", + credentials: [], + endpoints: [{ host: "api.example.test", port: 443 }], + binaries: [], + inference_capable: true, + }), + ], + [ + "credential authority", + JSON.stringify({ + id: "openai", + credentials: [{ env: "OPENAI_API_KEY" }], + endpoints: [], + binaries: [], + inference_capable: true, + }), + ], + ["malformed export output", "not-json"], + ])("rejects an existing OpenAI profile with %s before MCP setup", (_case, stdout) => { + const runOpenshell = vi + .fn() + .mockReturnValueOnce({ status: 1, stdout: "", stderr: "already exists" }) + .mockReturnValueOnce({ status: 0, stdout, stderr: "" }); + setProviderCommandRuntimeHooksForTest({ runOpenshell: runOpenshell as never }); + + expect(() => ensureMcpBridgeProviderProfile()).toThrow( + /does not match NemoClaw's gateway-only endpointless credential contract/, + ); + expect(runOpenshell).toHaveBeenCalledTimes(2); + expect(runOpenshell).toHaveBeenLastCalledWith( + ["provider", "profile", "export", "openai", "--output", "json"], + expect.any(Object), + ); + }); + + it("fails closed with a distinct diagnostic when an existing OpenAI profile cannot be exported", () => { + const runOpenshell = vi + .fn() + .mockReturnValueOnce({ status: 1, stdout: "", stderr: "already exists" }) + .mockReturnValueOnce({ status: 1, stdout: "", stderr: "export rejected" }); + setProviderCommandRuntimeHooksForTest({ runOpenshell: runOpenshell as never }); + + expect(() => ensureMcpBridgeProviderProfile()).toThrow( + /already exists but could not be exported for validation/, + ); + expect(runOpenshell).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/lib/actions/sandbox/mcp-bridge-provider-readiness.ts b/src/lib/actions/sandbox/mcp-bridge-provider-readiness.ts index 59f3145fddb..22510bb1d1c 100644 --- a/src/lib/actions/sandbox/mcp-bridge-provider-readiness.ts +++ b/src/lib/actions/sandbox/mcp-bridge-provider-readiness.ts @@ -15,6 +15,13 @@ import { executeSandboxExecCommand } from "./process-recovery"; const MCP_CREDENTIAL_REVISION_OBSERVATION_RE = /^(?:absent|canonical|v[0-9]{1,20})$/; export type McpCredentialRevisionObservation = "absent" | "canonical" | `v${number}`; +export type McpAttachedCredentialRevision = Exclude; + +type McpCredentialRevisionAttempt = + | { kind: "observation"; observation: McpCredentialRevisionObservation } + | { kind: "transport-unavailable" } + | { kind: "command-failed"; status: number } + | { kind: "invalid-output" }; /** * Provider synchronization proofs must observe a fresh OpenShell-mediated exec @@ -93,13 +100,30 @@ function parseMcpCredentialRevisionObservation( function tryObserveMcpCredentialRevision( sandboxName: string, envName: string, -): McpCredentialRevisionObservation | null { +): McpCredentialRevisionAttempt { const result = executeMcpCredentialProofCommand( sandboxName, buildMcpCredentialRevisionObservationCommand(envName), ); - if (!result || result.status !== 0) return null; - return parseMcpCredentialRevisionObservation(result.stdout); + if (!result) return { kind: "transport-unavailable" }; + if (result.status !== 0) return { kind: "command-failed", status: result.status }; + const observation = parseMcpCredentialRevisionObservation(result.stdout); + return observation === null + ? { kind: "invalid-output" } + : { kind: "observation", observation }; +} + +function describeMcpCredentialRevisionAttempt(attempt: McpCredentialRevisionAttempt): string { + switch (attempt.kind) { + case "observation": + return attempt.observation; + case "transport-unavailable": + return "transport-unavailable"; + case "command-failed": + return `proof-command-exit-${attempt.status}`; + case "invalid-output": + return "invalid-bounded-output"; + } } export function observeMcpCredentialRevision( @@ -107,13 +131,13 @@ export function observeMcpCredentialRevision( entry: McpBridgeEntry, ): McpCredentialRevisionObservation { assertAuthenticatedBridgeEntry(entry); - const observation = tryObserveMcpCredentialRevision(sandboxName, entry.env[0]); - if (observation === null) { + const attempt = tryObserveMcpCredentialRevision(sandboxName, entry.env[0]); + if (attempt.kind !== "observation") { throw new McpBridgeError( `Could not observe the current OpenShell credential revision for sandbox '${sandboxName}'.`, ); } - return observation; + return attempt.observation; } export function waitForAttachedMcpCredential( @@ -123,7 +147,7 @@ export function waitForAttachedMcpCredential( previousRevision?: McpCredentialRevisionObservation; refreshAfterObservedAbsence?: () => void; } = {}, -): void { +): McpAttachedCredentialRevision { assertAuthenticatedBridgeEntry(entry); const envName = entry.env[0]; if ( @@ -137,35 +161,48 @@ export function waitForAttachedMcpCredential( 10, ); let refreshedAfterObservedAbsence = false; + let lastAttempt: McpCredentialRevisionAttempt = { kind: "transport-unavailable" }; + let attachedRevision: McpAttachedCredentialRevision | undefined; const ready = waitUntil( () => { // Each exec is a fresh OpenShell process. Only the bounded placeholder // classification crosses back to the host, where the comparison cannot // be influenced by a same-UID sandbox process rewriting a snapshot file. - let observation = tryObserveMcpCredentialRevision(sandboxName, envName); + let attempt = tryObserveMcpCredentialRevision(sandboxName, envName); + lastAttempt = attempt; if ( - observation === "absent" && + attempt.kind === "observation" && + attempt.observation === "absent" && !refreshedAfterObservedAbsence && options.refreshAfterObservedAbsence ) { refreshedAfterObservedAbsence = true; options.refreshAfterObservedAbsence(); - observation = tryObserveMcpCredentialRevision(sandboxName, envName); + attempt = tryObserveMcpCredentialRevision(sandboxName, envName); + lastAttempt = attempt; } - return ( + const observation = attempt.kind === "observation" ? attempt.observation : null; + const attached = observation !== null && observation !== "absent" && - (options.previousRevision === undefined || observation !== options.previousRevision) - ); + (options.previousRevision === undefined || observation !== options.previousRevision); + if (attached) attachedRevision = observation; + return attached; }, Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds : 30, 1_000, ); if (!ready) { throw new McpBridgeError( - `OpenShell did not synchronize the expected credential revision for placeholder '${envName}' into sandbox '${sandboxName}' after provider attachment or update.`, + `OpenShell did not synchronize the expected credential revision for placeholder '${envName}' into sandbox '${sandboxName}' after provider attachment or update (last bounded observation: ${describeMcpCredentialRevisionAttempt(lastAttempt)}; post-policy refresh attempted: ${refreshedAfterObservedAbsence ? "yes" : "no"}).`, + ); + } + if (attachedRevision === undefined) { + throw new McpBridgeError( + `OpenShell reported credential readiness without a usable revision for placeholder '${envName}' in sandbox '${sandboxName}'.`, ); } + return attachedRevision; } export function buildMcpCredentialDetachedCommand(envName: string): string { diff --git a/src/lib/actions/sandbox/mcp-bridge-provider.test.ts b/src/lib/actions/sandbox/mcp-bridge-provider.test.ts index e92343729d7..e02a2434eb9 100644 --- a/src/lib/actions/sandbox/mcp-bridge-provider.test.ts +++ b/src/lib/actions/sandbox/mcp-bridge-provider.test.ts @@ -353,7 +353,7 @@ alpha-mcp-slack generic 1 0 }); const refreshAfterObservedAbsence = vi.fn(); - waitForAttachedMcpCredential( + const revision = waitForAttachedMcpCredential( "alpha", { server: "github", @@ -375,6 +375,7 @@ alpha-mcp-slack generic 1 0 expect(proofCommand).toContain("GITHUB_TOKEN"); expect(proofCommand).not.toContain("base64 -d"); expect(refreshAfterObservedAbsence).not.toHaveBeenCalled(); + expect(revision).toBe("canonical"); }); it("refreshes once after a fresh exec reports the credential absent (#9764)", () => { @@ -395,10 +396,13 @@ alpha-mcp-slack generic 1 0 .mockReturnValueOnce({ status: 0, stdout: "v12", stderr: "" }); const refreshAfterObservedAbsence = vi.fn(); - waitForAttachedMcpCredential("alpha", entry, { refreshAfterObservedAbsence }); + const revision = waitForAttachedMcpCredential("alpha", entry, { + refreshAfterObservedAbsence, + }); expect(refreshAfterObservedAbsence).toHaveBeenCalledTimes(1); expect(exec).toHaveBeenCalledTimes(2); + expect(revision).toBe("v12"); }); it("does not repeat the refresh when the credential remains absent (#9764)", () => { @@ -427,21 +431,23 @@ alpha-mcp-slack generic 1 0 }, { refreshAfterObservedAbsence }, ), - ).toThrow(/did not synchronize the expected credential revision/); + ).toThrow(/last bounded observation: absent; post-policy refresh attempted: yes/); expect(refreshAfterObservedAbsence).toHaveBeenCalledTimes(1); expect(exec).toHaveBeenCalledTimes(2); }); it.each([ - ["unavailable", null], - ["malformed", { status: 0, stdout: "raw-secret", stderr: "" }], - ])("does not refresh when a credential observation is %s (#9764)", (_case, result) => { + ["unavailable", null, "transport-unavailable"], + ["malformed", { status: 0, stdout: "raw-secret", stderr: "" }, "invalid-bounded-output"], + ["rejected", { status: 1, stdout: "", stderr: "" }, "proof-command-exit-1"], + ])("does not refresh when a credential observation is %s (#9764)", (_case, result, diagnostic) => { vi.stubEnv("NEMOCLAW_MCP_PROVIDER_SYNC_TIMEOUT_SECONDS", "1"); vi.spyOn(processRecovery, "executeSandboxExecCommand").mockReturnValue(result); vi.spyOn(Date, "now").mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValue(1_000); const refreshAfterObservedAbsence = vi.fn(); - expect(() => + let failure: unknown; + try { waitForAttachedMcpCredential( "alpha", { @@ -456,8 +462,15 @@ alpha-mcp-slack generic 1 0 addedAt: "2026-06-01T00:00:00.000Z", }, { refreshAfterObservedAbsence }, - ), - ).toThrow(/did not synchronize the expected credential revision/); + ); + } catch (error) { + failure = error; + } + expect(failure).toBeInstanceOf(Error); + expect((failure as Error).message).toContain( + `last bounded observation: ${diagnostic}; post-policy refresh attempted: no`, + ); + expect((failure as Error).message).not.toContain("raw-secret"); expect(refreshAfterObservedAbsence).not.toHaveBeenCalled(); }); @@ -516,7 +529,7 @@ alpha-mcp-slack generic 1 0 }, { previousRevision: "v11", refreshAfterObservedAbsence }, ), - ).toThrow(/did not synchronize the expected credential revision/); + ).toThrow(/last bounded observation: v11; post-policy refresh attempted: yes/); expect(refreshAfterObservedAbsence).toHaveBeenCalledTimes(1); expect(exec).toHaveBeenCalledTimes(2); }); @@ -566,7 +579,7 @@ alpha-mcp-slack generic 1 0 stderr: "", }); - waitForAttachedMcpCredential("alpha", entry, { previousRevision: "v11" }); + expect(waitForAttachedMcpCredential("alpha", entry, { previousRevision: "v11" })).toBe("v12"); expect(exec).toHaveBeenCalledTimes(1); vi.stubEnv("NEMOCLAW_MCP_PROVIDER_SYNC_TIMEOUT_SECONDS", "1"); diff --git a/src/lib/actions/sandbox/mcp-bridge-rebuild.ts b/src/lib/actions/sandbox/mcp-bridge-rebuild.ts index a909fed4352..f5dcf11f8a7 100644 --- a/src/lib/actions/sandbox/mcp-bridge-rebuild.ts +++ b/src/lib/actions/sandbox/mcp-bridge-rebuild.ts @@ -144,7 +144,11 @@ export async function prepareMcpBridgesForRebuild( scrubbedAdapters.push(entry); } for (const entry of entries) { - removeGeneratedPolicy(sandboxName, entry); + // The same-name replacement journal fingerprints this source row before + // MCP teardown. Keep exact generated-policy ownership in that preserved + // row while removing only the live policy; inner onboarding excludes the + // generated name and post-rebuild restoration reuses this ownership. + removeGeneratedPolicy(sandboxName, entry, { preserveRegistryOwnership: true }); removedPolicies.push(entry); } for (const entry of entries) { diff --git a/src/lib/actions/sandbox/mcp-bridge-restart.ts b/src/lib/actions/sandbox/mcp-bridge-restart.ts index 11d5d3d51e3..bdee7dee3bf 100644 --- a/src/lib/actions/sandbox/mcp-bridge-restart.ts +++ b/src/lib/actions/sandbox/mcp-bridge-restart.ts @@ -173,7 +173,7 @@ async function restartMcpBridgeUnlocked(sandboxName: string, server?: string): P attachProvider(sandboxName, entry); applyGeneratedPolicy(sandboxName, entry, target); refreshMcpProviderEnvironment(entry); - waitForAttachedMcpCredential(sandboxName, entry, { + const credentialRevision = waitForAttachedMcpCredential(sandboxName, entry, { ...(providerResult.action === "updated" ? { previousRevision: previousCredentialRevision } : {}), @@ -183,7 +183,7 @@ async function restartMcpBridgeUnlocked(sandboxName: string, server?: string): P (entry.adapter as AgentMcpAdapter | undefined) ?? adapter, entry, adapterEnvValues, - { replaceExisting: true }, + { replaceExisting: true, credentialRevision }, ); writeBridgeEntry(sandboxName, { ...entry, @@ -242,7 +242,7 @@ export async function restoreExistingMcpBridgeRuntime( attachProvider(sandboxName, entry); applyGeneratedPolicy(sandboxName, entry, resolvedTargetPins(resolvedByServer, entry)); refreshMcpProviderEnvironment(entry); - waitForAttachedMcpCredential(sandboxName, entry); + const credentialRevision = waitForAttachedMcpCredential(sandboxName, entry); const adapter = (entry.adapter as AgentMcpAdapter | undefined) ?? defaultAdapter; registerAgentAdapter( sandboxName, @@ -252,6 +252,7 @@ export async function restoreExistingMcpBridgeRuntime( { replaceExisting: true, teardownRollback: options.lifecyclePhase === "teardown-rollback", + credentialRevision, }, ); writeBridgeEntry(sandboxName, { ...entry, adapter, updatedAt: nowIso() }); diff --git a/src/lib/actions/sandbox/mcp-bridge-tool-discovery.ts b/src/lib/actions/sandbox/mcp-bridge-tool-discovery.ts index 078272bc128..a50db124efb 100644 --- a/src/lib/actions/sandbox/mcp-bridge-tool-discovery.ts +++ b/src/lib/actions/sandbox/mcp-bridge-tool-discovery.ts @@ -70,9 +70,9 @@ export function buildMcpToolDiscoveryCommand( ): McpToolDiscoveryCommand | null { const credentialEnv = entry.env[0]; if (!credentialEnv) return null; - // The runtime receives only the validated provider key name and constructs - // the OpenShell placeholder itself. OpenShell injects the real credential - // below this command boundary when the request crosses the policy boundary. + // The runtime receives only the validated provider key name. It reads the + // current revisioned OpenShell placeholder from its fresh process environment + // and rejects anything else before a request crosses the policy boundary. // Under the approved trusted-configured-endpoint contract, advertised names // remain untrusted and bounded display text, but may be credential-derived; // parser validation is not a confidentiality proof for a malicious server. diff --git a/src/lib/actions/sandbox/mcp-tool-discovery-runtime.test.ts b/src/lib/actions/sandbox/mcp-tool-discovery-runtime.test.ts index 493a0087d3e..fbb9ff42fcc 100644 --- a/src/lib/actions/sandbox/mcp-tool-discovery-runtime.test.ts +++ b/src/lib/actions/sandbox/mcp-tool-discovery-runtime.test.ts @@ -34,30 +34,41 @@ describe("shared MCP tool discovery runtime", () => { "arbitrary-format-secret-that-the-server-would-echo", ]), ).toThrow("invalid arguments"); - [ - "EXAMPLE_MCP_TOKEN", - "lowercase_token", - "_TOKEN", - `A${"a".repeat(127)}`, - ].forEach((credentialEnv) => { - expect(() => validateMcpCredentialEnvName(credentialEnv)).not.toThrow(); - expect( - parseMcpToolDiscoveryArguments([ - "--url", - "https://example.test/mcp", - "--credential-env", + ["EXAMPLE_MCP_TOKEN", "lowercase_token", "_TOKEN", `A${"a".repeat(127)}`].forEach( + (credentialEnv) => { + expect(() => validateMcpCredentialEnvName(credentialEnv)).not.toThrow(); + expect( + parseMcpToolDiscoveryArguments([ + "--url", + "https://example.test/mcp", + "--credential-env", + credentialEnv, + ]), + ).toEqual({ + url: new URL("https://example.test/mcp"), credentialEnv, - ]), - ).toEqual({ - url: new URL("https://example.test/mcp"), - credentialEnv, - }); - }); - expect(buildMcpToolDiscoveryAuthorizationPlaceholder("EXAMPLE_MCP_TOKEN")).toBe( - "Bearer openshell:resolve:env:EXAMPLE_MCP_TOKEN", + }); + }, ); - + expect( + buildMcpToolDiscoveryAuthorizationPlaceholder( + "EXAMPLE_MCP_TOKEN", + "openshell:resolve:env:EXAMPLE_MCP_TOKEN", + ), + ).toBe("Bearer openshell:resolve:env:EXAMPLE_MCP_TOKEN"); + expect( + buildMcpToolDiscoveryAuthorizationPlaceholder( + "EXAMPLE_MCP_TOKEN", + "openshell:resolve:env:v14429878272859325890_EXAMPLE_MCP_TOKEN", + ), + ).toBe("Bearer openshell:resolve:env:v14429878272859325890_EXAMPLE_MCP_TOKEN"); expect(() => validateMcpCredentialEnvName(credentialEnv)).toThrow(); + expect( + buildMcpToolDiscoveryAuthorizationPlaceholder( + credentialEnv, + `openshell:resolve:env:${credentialEnv}`, + ), + ).toBeNull(); expect(() => parseMcpToolDiscoveryArguments([ "--url", @@ -69,6 +80,19 @@ describe("shared MCP tool discovery runtime", () => { }, ); + it.each([ + undefined, + "raw-secret", + "openshell:resolve:env:v42_OTHER_MCP_TOKEN", + "openshell:resolve:env:vbad_EXAMPLE_MCP_TOKEN", + "openshell:resolve:env:v144298782728593258901_EXAMPLE_MCP_TOKEN", + "openshell:resolve:env:v42_EXAMPLE_MCP_TOKEN\nAuthorization: Bearer raw-secret", + ])("rejects unsafe live credential values [case %#]", (runtimeValue) => { + expect( + buildMcpToolDiscoveryAuthorizationPlaceholder("EXAMPLE_MCP_TOKEN", runtimeValue), + ).toBeNull(); + }); + it("enumerates every page and returns deterministic names only", async () => { const loadPage = vi .fn() diff --git a/src/lib/actions/sandbox/rebuild-backup-phase.ts b/src/lib/actions/sandbox/rebuild-backup-phase.ts index f141490bb13..4e6db352d1b 100644 --- a/src/lib/actions/sandbox/rebuild-backup-phase.ts +++ b/src/lib/actions/sandbox/rebuild-backup-phase.ts @@ -47,6 +47,18 @@ export interface RebuildBackupPhaseResult { sessionPolicyPresets: string[] | null; } +export function excludePolicyPresetsByName( + presets: readonly string[], + excludedNames: readonly (string | undefined)[], +): string[] { + const excluded = new Set( + excludedNames.filter( + (name): name is string => typeof name === "string" && name.length > 0, + ), + ); + return presets.filter((name) => !excluded.has(name)); +} + function bailForUnsafeOpenClawPluginProvenance(input: RebuildBackupPhaseInput): never { console.error( " Custom-image OpenClaw plugin provenance is missing or invalid; rebuild cannot safely distinguish image-owned plugins from user state.", diff --git a/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts b/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts index 9a4c10cb596..13fcf4d9815 100644 --- a/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts +++ b/src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts @@ -69,7 +69,12 @@ describe("rebuildSandbox flow: lifecycle", () => { }; const harness = createRebuildFlowHarness({ applyPreset: () => true, - sandboxEntry: { policyPresetsFinalized: true, policyTier: "balanced" }, + backupPolicyPresets: ["npm", "bad", "throw", "mcp-bridge-github"], + sandboxEntry: { + policies: ["npm", "mcp-bridge-github"], + policyPresetsFinalized: true, + policyTier: "balanced", + }, mcpPreparation: { entries: [mcpEntry], detachedProviderEntries: [mcpEntry], @@ -102,6 +107,7 @@ describe("rebuildSandbox flow: lifecycle", () => { nonInteractive: true, recreateSandbox: true, authoritativeResumeConfig: true, + rebuildPolicyPresets: ["npm", "bad", "throw"], autoYes: true, }), ); @@ -140,6 +146,7 @@ describe("rebuildSandbox flow: lifecycle", () => { expect(harness.applyPresetSpy).toHaveBeenCalledWith("alpha", "npm"); expect(harness.applyPresetSpy).toHaveBeenCalledWith("alpha", "bad"); expect(harness.applyPresetSpy).toHaveBeenCalledWith("alpha", "throw"); + expect(harness.applyPresetSpy).not.toHaveBeenCalledWith("alpha", "mcp-bridge-github"); expect(harness.registryUpdateSpy).toHaveBeenCalledWith("alpha", { agentVersion: "0.2.0", policies: ["npm", "bad", "throw"], diff --git a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts index b75d7407373..7eea6fd5562 100644 --- a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts +++ b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts @@ -134,6 +134,7 @@ export type RebuildRecreateOnboardOpts = { preparedImageRebuild?: PreparedImageRebuildHandoff; managedWorkloadRebuild?: ManagedWorkloadRebuildHandoff; rebuildPreservedEnv?: readonly PreservedEnvFile[]; + rebuildPolicyPresets?: readonly string[]; hostMounts?: readonly import("../../state/registry/types").SandboxHostMount[]; autoYes: boolean; toolDisclosure: ToolDisclosure; diff --git a/src/lib/actions/sandbox/rebuild-pipeline.ts b/src/lib/actions/sandbox/rebuild-pipeline.ts index 2b54541b335..51003e38a2d 100644 --- a/src/lib/actions/sandbox/rebuild-pipeline.ts +++ b/src/lib/actions/sandbox/rebuild-pipeline.ts @@ -15,7 +15,11 @@ import { withPortableOnboardRetirementBoundary } from "../../onboard/portable-re import { withMcpLifecycleLock } from "../../state/mcp-lifecycle-lock"; import * as onboardSession from "../../state/onboard-session"; import { load as loadRegistry, REGISTRY_FILE } from "../../state/registry/persistence"; -import { normalizeRebuildTargetPolicyPresets, runRebuildBackupPhase } from "./rebuild-backup-phase"; +import { + excludePolicyPresetsByName, + normalizeRebuildTargetPolicyPresets, + runRebuildBackupPhase, +} from "./rebuild-backup-phase"; import { buildRefreshMutableOpenClawConfigHashCommand } from "./rebuild-config-hash"; import { DCODE_AGENT_NAME } from "./rebuild-dcode-target"; import { runRebuildDestroyPhase } from "./rebuild-destroy-phase"; @@ -466,13 +470,16 @@ async function rebuildSandboxUnlocked( Array.isArray(completedInnerSession.policyPresets) ? completedInnerSession.policyPresets : []; - const targetPolicyPresets = normalizeRebuildTargetPolicyPresets( - [...backup.policyPresets, ...freshInnerOnboardPolicyPresets], - { - ...sandboxEntry, - observabilityEnabled: recreateOptions.observabilityEnabled, - }, - durableConfig.webSearchConfig, + const targetPolicyPresets = excludePolicyPresetsByName( + normalizeRebuildTargetPolicyPresets( + [...backup.policyPresets, ...freshInnerOnboardPolicyPresets], + { + ...sandboxEntry, + observabilityEnabled: recreateOptions.observabilityEnabled, + }, + durableConfig.webSearchConfig, + ), + mcpPreparation.entries.map((entry) => entry.policyName), ); const capturedCustomPolicies = backup.backupManifest?.customPolicies?.map((entry) => ({ ...entry })) ?? diff --git a/src/lib/actions/sandbox/rebuild-recreate-phase.ts b/src/lib/actions/sandbox/rebuild-recreate-phase.ts index fa8060763b7..eb2c9dd742e 100644 --- a/src/lib/actions/sandbox/rebuild-recreate-phase.ts +++ b/src/lib/actions/sandbox/rebuild-recreate-phase.ts @@ -15,7 +15,10 @@ import type { Session } from "../../state/onboard-session"; import * as onboardSession from "../../state/onboard-session"; import * as registry from "../../state/registry"; import { cloneSandboxHostMounts } from "../../state/registry/host-mount"; -import type { RebuildBackupManifest } from "./rebuild-backup-phase"; +import { + excludePolicyPresetsByName, + type RebuildBackupManifest, +} from "./rebuild-backup-phase"; import type { RebuildBail, RebuildLog } from "./rebuild-credential-preflight"; import type { RebuildDurableConfig } from "./rebuild-durable-config"; import { isolateAmbientRecreateEnv } from "./rebuild-env-isolation"; @@ -105,6 +108,13 @@ export async function runRebuildRecreatePhase(input: RebuildRecreatePhaseInput): console.log(""); console.log(" Creating new sandbox with current image..."); + const recreatePolicyPresets = Array.isArray(rebuildSessionPolicyPresets) + ? excludePolicyPresetsByName( + rebuildSessionPolicyPresets, + rebuildMcpEntries.map((entry) => entry.policyName), + ) + : null; + const rebuildGpuOverrides = getRebuildSandboxGpuOverrides(sb); log( `Session before update: sandboxName=${sessionBefore?.sandboxName}, status=${sessionBefore?.status}, resumable=${sessionBefore?.resumable}, provider=${sessionBefore?.provider}, model=${sessionBefore?.model}, sessionMatch=${sessionMatchesSandbox}`, @@ -195,7 +205,11 @@ export async function runRebuildRecreatePhase(input: RebuildRecreatePhaseInput): s.agent = rebuildAgent; s.messagingPlan = rebuildMessagingPlan; s.hermesToolGateways = rebuildsHermesSandbox ? rebuildHermesToolGateways : []; - s.policyPresets = rebuildSessionPolicyPresets; + // MCP preparation removes these generated policies before sandbox delete, + // and the dedicated post-rebuild phase restores them with their provider + // bindings. Do not ask inner onboarding to resolve their stale preset names + // as built-ins while the generated definitions are intentionally absent. + s.policyPresets = recreatePolicyPresets; s.gpuPassthrough = rebuildGpuOverrides.sessionGpuPassthrough; s.metadata.fromDockerfile = storedFromDockerfile; s.provider = resumeConfig.provider; @@ -274,6 +288,7 @@ export async function runRebuildRecreatePhase(input: RebuildRecreatePhaseInput): await rebuildOnboardDependencies.onboard({ ...recreateOptions, rebuildGatewayAuthority, + ...(Array.isArray(recreatePolicyPresets) ? { rebuildPolicyPresets: recreatePolicyPresets } : {}), ...(rebuildsHermesSandbox && backupManifest?.preservedEnv ? { rebuildPreservedEnv: backupManifest.preservedEnv } : {}), diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 4889989645d..d04c6c29ac3 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3233,9 +3233,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { gatewayName: GATEWAY_NAME, hermesPortableLifecycle: lockedRuntime.portableRuntimeContext !== null && agent?.name === "hermes", - authoritativeResumeConfig: opts.authoritativeResumeConfig === true, - authoritativePolicyTier: - opts.authoritativeResumeConfig === true ? (opts.policyTier ?? null) : undefined, + ...authoritativeRebuildTarget.authoritativeRebuildSandboxFlowOptions(opts), recreateJournalTargetIntentFingerprint: opts.recreateJournalTargetIntentFingerprint ?? null, resumeAgentChanged, diff --git a/src/lib/onboard/authoritative-rebuild-target.test.ts b/src/lib/onboard/authoritative-rebuild-target.test.ts index ce013d6fb62..34cd3ab28c8 100644 --- a/src/lib/onboard/authoritative-rebuild-target.test.ts +++ b/src/lib/onboard/authoritative-rebuild-target.test.ts @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { + authoritativeRebuildSandboxFlowOptions, authoritativeRebuildRuntimePreflightOptions, type AuthoritativeRebuildTargetDeps, type AuthoritativeRebuildPreflightOptions, @@ -26,6 +27,31 @@ const target = { }; const originalGateway = process.env.OPENSHELL_GATEWAY; +describe("authoritative rebuild sandbox flow options", () => { + it("clones authoritative policy state and ignores non-authoritative injection", () => { + const rebuildPolicyPresets = ["github"]; + const projected = authoritativeRebuildSandboxFlowOptions({ + authoritativeResumeConfig: true, + policyTier: "balanced", + rebuildPolicyPresets, + }); + + expect(projected).toEqual({ + authoritativeResumeConfig: true, + authoritativePolicyTier: "balanced", + rebuildPolicyPresets: ["github"], + }); + expect(projected.rebuildPolicyPresets).not.toBe(rebuildPolicyPresets); + expect( + authoritativeRebuildSandboxFlowOptions({ + authoritativeResumeConfig: false, + policyTier: "balanced", + rebuildPolicyPresets: ["mcp-bridge-fake"], + }), + ).toEqual({ authoritativeResumeConfig: false }); + }); +}); + describe("authoritative rebuild runtime preflight options", () => { it("carries only target GPU state and recorded N1x preview intent (#9292)", () => { const options = { diff --git a/src/lib/onboard/authoritative-rebuild-target.ts b/src/lib/onboard/authoritative-rebuild-target.ts index b50f45336da..cb6bcb6d5f4 100644 --- a/src/lib/onboard/authoritative-rebuild-target.ts +++ b/src/lib/onboard/authoritative-rebuild-target.ts @@ -15,6 +15,23 @@ import type { OnboardOptions } from "./types"; export type AuthoritativeOnboardGatewayBinding = { name: string; port: number }; +export function authoritativeRebuildSandboxFlowOptions( + opts: Pick, +): { + authoritativeResumeConfig: boolean; + authoritativePolicyTier?: string | null; + rebuildPolicyPresets?: readonly string[]; +} { + if (opts.authoritativeResumeConfig !== true) return { authoritativeResumeConfig: false }; + return { + authoritativeResumeConfig: true, + authoritativePolicyTier: opts.policyTier ?? null, + ...(Array.isArray(opts.rebuildPolicyPresets) + ? { rebuildPolicyPresets: [...opts.rebuildPolicyPresets] } + : {}), + }; +} + export type AuthoritativeGatewayOptions = Pick< OnboardOptions, "authoritativeResumeConfig" | "targetGatewayName" | "targetGatewayPort" | "onboardLockAlreadyHeld" diff --git a/src/lib/onboard/experimental/hermes-portable-build-context-files.ts b/src/lib/onboard/experimental/hermes-portable-build-context-files.ts index 2e2c8e93d20..868103c92b0 100644 --- a/src/lib/onboard/experimental/hermes-portable-build-context-files.ts +++ b/src/lib/onboard/experimental/hermes-portable-build-context-files.ts @@ -114,6 +114,7 @@ export const HERMES_PORTABLE_BUILD_CONTEXT_FILES = [ { path: "nemoclaw-blueprint/provider-profiles/brave.yaml", mode: "100644" }, { path: "nemoclaw-blueprint/provider-profiles/entra-runtime-v1.yaml", mode: "100644" }, { path: "nemoclaw-blueprint/provider-profiles/nemoclaw-mcp-v1.yaml", mode: "100644" }, + { path: "nemoclaw-blueprint/provider-profiles/openai.yaml", mode: "100644" }, { path: "nemoclaw-blueprint/provider-profiles/okta-runtime-v1.yaml", mode: "100644" }, { path: "nemoclaw-blueprint/provider-profiles/tavily-hermes-v1.yaml", mode: "100644" }, { path: "nemoclaw-blueprint/provider-profiles/tavily.yaml", mode: "100644" }, diff --git a/src/lib/onboard/machine/core-flow-phases.test.ts b/src/lib/onboard/machine/core-flow-phases.test.ts index 6f736f5b941..443871dfcf2 100644 --- a/src/lib/onboard/machine/core-flow-phases.test.ts +++ b/src/lib/onboard/machine/core-flow-phases.test.ts @@ -516,7 +516,7 @@ describe("core onboard flow phases", () => { }); }); - it("carries rebuild-preserved environment assignments into sandbox creation (#7803)", async () => { + it("carries authoritative rebuild state into sandbox creation (#7803)", async () => { const createSandbox = vi.fn(async () => "created-sandbox"); const rebuildPreservedEnv = [ { @@ -524,8 +524,9 @@ describe("core onboard flow phases", () => { assignments: ["SLACK_HOME_CHANNEL=C0123"], }, ]; + const rebuildPolicyPresets = ["github"]; const { providerInference: providerPhase, sandbox: sandboxPhase } = createPhases({ - sandboxOptions: { rebuildPreservedEnv }, + sandboxOptions: { rebuildPreservedEnv, rebuildPolicyPresets }, sandboxDeps: { createSandbox }, }); @@ -534,6 +535,7 @@ describe("core onboard flow phases", () => { expect(createSandbox.mock.calls[0]?.at(-1)).toMatchObject({ rebuildPreservedEnv, + rebuildPolicyPresets, }); }); diff --git a/src/lib/onboard/machine/core-flow-phases.ts b/src/lib/onboard/machine/core-flow-phases.ts index ff67b03eaec..00d1c4762e6 100644 --- a/src/lib/onboard/machine/core-flow-phases.ts +++ b/src/lib/onboard/machine/core-flow-phases.ts @@ -77,6 +77,7 @@ export interface SandboxOnboardFlowPhaseOptions< requestedObservabilityEnabled?: boolean | null; requestedDcodeAutoApprovalMode?: DcodeAutoApprovalMode | null; rebuildPreservedEnv?: readonly import("../../state/preserved-env").PreservedEnvFile[]; + rebuildPolicyPresets?: readonly string[]; hostMounts?: readonly import("../../state/registry/types").SandboxHostMount[]; endpointProvenance: EndpointProvenanceOptions; recreateSandbox: (requested?: boolean) => boolean; @@ -236,6 +237,7 @@ export function createSandboxOnboardFlowPhase< requestedObservabilityEnabled: options.requestedObservabilityEnabled, requestedDcodeAutoApprovalMode: options.requestedDcodeAutoApprovalMode, rebuildPreservedEnv: options.rebuildPreservedEnv, + rebuildPolicyPresets: options.rebuildPolicyPresets, hostMounts: options.hostMounts, recreateSandbox: options.recreateSandbox, session: context.session, diff --git a/src/lib/onboard/machine/handlers/sandbox-create-intent-boundary.test.ts b/src/lib/onboard/machine/handlers/sandbox-create-intent-boundary.test.ts index 1f0bcac4a63..937a2605cc8 100644 --- a/src/lib/onboard/machine/handlers/sandbox-create-intent-boundary.test.ts +++ b/src/lib/onboard/machine/handlers/sandbox-create-intent-boundary.test.ts @@ -117,6 +117,55 @@ describe("sandbox create intent machine boundary", () => { expect(resolvedIntents[2]).toEqual(resolvedIntents[0]); }); + it("replaces a stale resumed create-intent policy with the authoritative rebuild selection (#9792)", async () => { + const session = createSession({ + sandboxName: "saved", + policyPresets: ["github"], + }); + const { deps, calls } = createDeps(); + calls.resolveCreateIntent.mockResolvedValue({ + sandboxName: "saved", + inferenceProvider: "provider", + activeMessagingChannels: [], + messagingProviderRequests: [], + reusableMessagingProviders: [], + extraProviders: [], + staleExtraProviders: [], + hermesToolGateways: [], + policy: { + basePolicyPath: "/repo/policy.yaml", + activeMessagingChannels: [], + options: { + directGpu: false, + additionalPresets: ["mcp-bridge-fake"], + policyTier: null, + baselineExclusions: [], + }, + }, + gpuCreateArgs: [], + resourceCreateArgs: [], + gpuRoutePlan: "none", + sandboxGpuLogMessage: null, + disabledChannelNames: [], + extraPlaceholderKeys: [], + } as never); + + await handleSandboxState({ + ...baseOptions(deps, session), + authoritativeResumeConfig: true, + rebuildPolicyPresets: ["github"], + resume: true, + sandboxName: "saved", + }); + + expect(calls.createSandbox.mock.calls[0]?.at(-1)).toMatchObject({ + rebuildPolicyPresets: ["github"], + resolved: { + policy: { options: { additionalPresets: ["github"] } }, + }, + }); + }); + it("carries an explicit recreate request through a fresh sandbox decision (#8847)", async () => { const session = createSession({ sandboxName: "same-sandbox" }); const { deps, calls } = createDeps({ getSandboxReuseState: () => "ready" }); diff --git a/src/lib/onboard/machine/handlers/sandbox-recreate-journal.test.ts b/src/lib/onboard/machine/handlers/sandbox-recreate-journal.test.ts index 0f5bfab492a..5d6490080d3 100644 --- a/src/lib/onboard/machine/handlers/sandbox-recreate-journal.test.ts +++ b/src/lib/onboard/machine/handlers/sandbox-recreate-journal.test.ts @@ -216,8 +216,9 @@ it.each([ expect(retireReplacedSandboxWorkload).toHaveBeenCalledOnce(); }); -it("continues an outer rebuild journal after the outer rebuild deletes the source sandbox", async () => { +it("carries filtered presets through post-delete onboard resume", async () => { const session = createSession({ sandboxName: "saved", agent: "openclaw" }); + session.policyPresets = ["github"]; session.steps.sandbox.status = "complete"; session.machine.state = "agent_setup"; session.checkpoint = { @@ -246,6 +247,8 @@ it("continues an outer rebuild journal after the outer rebuild deletes the sourc hermesAuthMethod: null, gatewayName: "nemoclaw", gatewayPort: 8080, + policies: ["github", "mcp-bridge-fake"], + policyPresetsFinalized: true, }; const targetIntentFingerprint = fingerprintSandboxRecreateValue({ sandboxName: "saved", @@ -279,6 +282,10 @@ it("continues an outer rebuild journal after the outer rebuild deletes the sourc const createIntent = args.at(-1); expect(createIntent).toMatchObject({ recreate: true, + rebuildPolicyPresets: ["github"], + resolved: { + policy: { options: { additionalPresets: ["github"] } }, + }, recreateTransaction: { id: transaction.id, targetGeneration: transaction.targetGeneration, diff --git a/src/lib/onboard/machine/handlers/sandbox.ts b/src/lib/onboard/machine/handlers/sandbox.ts index dcd527d5abb..4150fc3ce29 100644 --- a/src/lib/onboard/machine/handlers/sandbox.ts +++ b/src/lib/onboard/machine/handlers/sandbox.ts @@ -201,6 +201,7 @@ export interface SandboxStateOptions< requestedObservabilityEnabled?: boolean | null; requestedDcodeAutoApprovalMode?: DcodeAutoApprovalMode | null; rebuildPreservedEnv?: readonly import("../../../state/preserved-env").PreservedEnvFile[]; + rebuildPolicyPresets?: readonly string[]; hostMounts?: readonly import("../../../state/registry/types").SandboxHostMount[]; recreateSandbox: (requested?: boolean) => boolean; gatewayName: string; @@ -481,6 +482,41 @@ function compatibleEndpointReasoningForCreateIntent( return value === "true" || value === "false" ? { compatibleEndpointReasoning: value } : {}; } +function rebuildPolicyPresetsForCreateIntent( + value: readonly string[] | undefined, + session: Session | null, + sandboxName: string, +): Pick { + // A later `onboard --resume` no longer has the outer rebuild's in-memory + // options. The matching recreate journal makes its filtered session value + // the durable replacement target instead of the preserved source row. + const journaledValue = + session?.checkpoint?.sandboxRecreate?.sandboxName === sandboxName && + Array.isArray(session.policyPresets) + ? session.policyPresets + : undefined; + const selectedValue = Array.isArray(value) ? value : journaledValue; + return Array.isArray(selectedValue) ? { rebuildPolicyPresets: [...selectedValue] } : {}; +} + +/** Replace a resumed create-plan snapshot with the outer rebuild's normalized built-ins. */ +function applyAuthoritativeRebuildPolicyPresets( + intent: ResolvedSandboxCreateIntent, + rebuildPolicyPresets: readonly string[] | undefined, +): ResolvedSandboxCreateIntent { + if (!Array.isArray(rebuildPolicyPresets)) return intent; + return { + ...intent, + policy: { + ...intent.policy, + options: { + ...intent.policy.options, + additionalPresets: [...rebuildPolicyPresets], + }, + }, + }; +} + type SandboxCreationDecision = Exclude; type CompleteSandboxCreateIntent = SandboxCreateIntent & { readonly resolved: ResolvedSandboxCreateIntent; @@ -1558,25 +1594,33 @@ class SandboxStateFlow< hermesToolGateways: readonly string[], ): Promise { const reuseRegisteredCredentials = this.resumesSandboxPrompts && this.options.resume; - const resolved = await this.deps.resolveSandboxCreateIntent({ + const rebuildPolicyPresetSelection = rebuildPolicyPresetsForCreateIntent( + this.options.rebuildPolicyPresets, + state.session, sandboxName, - inferenceProvider: this.options.provider, - hostLocalInferenceRouteOnly: this.options.hostLocalInferenceRouteOnly === true, - enabledChannels: state.selectedMessagingChannels, - webSearchConfig: state.webSearchConfig, - agent: this.options.agent, - sandboxGpuConfig: this.options.sandboxGpuConfig, - resourceProfile, - hermesToolGateways, - extraProviders, - staleExtraProviders, - hostMounts: this.options.hostMounts, - baselineExclusions: baselineExclusionsForCreate(sandboxName), - ...(reuseRegisteredCredentials ? { reuseRegisteredCredentials: true } : {}), - ...(this.options.authoritativePolicyTier !== undefined - ? { policyTier: this.options.authoritativePolicyTier } - : {}), - }); + ); + const resolved = applyAuthoritativeRebuildPolicyPresets( + await this.deps.resolveSandboxCreateIntent({ + sandboxName, + inferenceProvider: this.options.provider, + hostLocalInferenceRouteOnly: this.options.hostLocalInferenceRouteOnly === true, + enabledChannels: state.selectedMessagingChannels, + webSearchConfig: state.webSearchConfig, + agent: this.options.agent, + sandboxGpuConfig: this.options.sandboxGpuConfig, + resourceProfile, + hermesToolGateways, + extraProviders, + staleExtraProviders, + hostMounts: this.options.hostMounts, + baselineExclusions: baselineExclusionsForCreate(sandboxName), + ...(reuseRegisteredCredentials ? { reuseRegisteredCredentials: true } : {}), + ...(this.options.authoritativePolicyTier !== undefined + ? { policyTier: this.options.authoritativePolicyTier } + : {}), + }), + rebuildPolicyPresetSelection.rebuildPolicyPresets, + ); return { resolved, recreate: requiresSandboxRecreation(decision, this.options.recreateSandbox(false)), @@ -1604,6 +1648,7 @@ class SandboxStateFlow< ...(this.options.rebuildPreservedEnv ? { rebuildPreservedEnv: this.options.rebuildPreservedEnv } : {}), + ...rebuildPolicyPresetSelection, extraProviders, }; } diff --git a/src/lib/onboard/policy-preset-persistence.test.ts b/src/lib/onboard/policy-preset-persistence.test.ts index 7098aa8aa8b..8e2e82df684 100644 --- a/src/lib/onboard/policy-preset-persistence.test.ts +++ b/src/lib/onboard/policy-preset-persistence.test.ts @@ -171,6 +171,69 @@ describe("applyRecreatePolicyCarryForward (#4621)", () => { expect(note).not.toHaveBeenCalled(); }); + it("keeps the matching recreate journal selection instead of stale source presets", () => { + vi.spyOn(registry, "getSandbox").mockReturnValue({ + name: "sb", + policies: ["github", "mcp-bridge-fake"], + policyPresetsFinalized: true, + } as ReturnType); + vi.spyOn(onboardSession, "loadSession").mockReturnValue({ + policyPresets: ["github"], + checkpoint: { sandboxRecreate: { sandboxName: "sb" } }, + } as never); + const updateSession = vi + .spyOn(onboardSession, "updateSession") + .mockReturnValue(undefined as never); + const note = vi.fn(); + + applyRecreatePolicyCarryForward("sb", true, note); + + expect(readSeededPresets(updateSession)).toEqual(["github"]); + expect(note).not.toHaveBeenCalled(); + }); + + it("uses an explicit rebuild selection instead of stale source presets", () => { + vi.spyOn(registry, "getSandbox").mockReturnValue({ + name: "sb", + policies: ["github", "mcp-bridge-fake"], + policyPresetsFinalized: true, + } as ReturnType); + vi.spyOn(onboardSession, "loadSession").mockReturnValue(null); + const updateSession = vi + .spyOn(onboardSession, "updateSession") + .mockReturnValue(undefined as never); + const note = vi.fn(); + process.env.NEMOCLAW_POLICY_PRESETS = "pypi"; + + try { + applyRecreatePolicyCarryForward("sb", true, note, ["github"]); + } finally { + delete process.env.NEMOCLAW_POLICY_PRESETS; + } + + expect(readSeededPresets(updateSession)).toEqual(["github"]); + expect(note).not.toHaveBeenCalled(); + }); + + it("does not carry a different sandbox journal selection across targets", () => { + vi.spyOn(registry, "getSandbox").mockReturnValue({ + name: "sb", + policies: ["github"], + policyPresetsFinalized: true, + } as ReturnType); + vi.spyOn(onboardSession, "loadSession").mockReturnValue({ + policyPresets: ["npm"], + checkpoint: { sandboxRecreate: { sandboxName: "other" } }, + } as never); + const updateSession = vi + .spyOn(onboardSession, "updateSession") + .mockReturnValue(undefined as never); + + applyRecreatePolicyCarryForward("sb", true, vi.fn()); + + expect(readSeededPresets(updateSession)).toEqual(["github"]); + }); + it("prints the override note when an env override clears the selection", () => { vi.spyOn(registry, "getSandbox").mockReturnValue({ name: "sb", diff --git a/src/lib/onboard/policy-preset-persistence.ts b/src/lib/onboard/policy-preset-persistence.ts index b965e1bcb64..e30aec0986a 100644 --- a/src/lib/onboard/policy-preset-persistence.ts +++ b/src/lib/onboard/policy-preset-persistence.ts @@ -133,19 +133,42 @@ export function seedReusedSandboxPolicyPresets( /** * Recreate path: seed the session from the previous entry's recorded selection * (carrying forward, or honoring a finalized empty set), then print any env - * override note. See resolveRecreatePolicyPresets. + * override note. An explicit outer-rebuild selection is already normalized and + * cannot be replaced by the preserved source row or ambient environment. + * See resolveRecreatePolicyPresets. */ export function applyRecreatePolicyCarryForward( sandboxName: string, nonInteractive: boolean, note: (message: string) => void, + rebuildPolicyPresets?: readonly string[], ): void { const previousEntry = registry.getSandbox(sandboxName); + const session = onboardSession.loadSession(); + const journaledSessionPolicies = + session?.checkpoint?.sandboxRecreate?.sandboxName === sandboxName && + Array.isArray(session.policyPresets) + ? [...session.policyPresets] + : null; + const authoritativeRebuildPolicies = Array.isArray(rebuildPolicyPresets) + ? [...rebuildPolicyPresets] + : null; + // A matching recreate journal owns the replacement target. In particular, + // rebuild can remove generated MCP policies before deleting the old sandbox + // while retaining their registry metadata for crash recovery. Re-reading the + // preserved source row here would replace the already-normalized target with + // a generated policy name whose definition is intentionally absent. + const previousPolicies = + authoritativeRebuildPolicies ?? journaledSessionPolicies ?? previousEntry?.policies; const { policyPresets, overrideNote } = resolveRecreatePolicyPresets( - previousEntry?.policies, - previousEntry?.policyPresetsFinalized === true, - (previousEntry?.customPolicies?.length ?? 0) > 0, - process.env, + previousPolicies, + authoritativeRebuildPolicies !== null || + journaledSessionPolicies !== null || + previousEntry?.policyPresetsFinalized === true, + authoritativeRebuildPolicies === null && + journaledSessionPolicies === null && + (previousEntry?.customPolicies?.length ?? 0) > 0, + authoritativeRebuildPolicies === null ? process.env : {}, nonInteractive, ); onboardSession.updateSession((current: Session) => { diff --git a/src/lib/onboard/sandbox-create/orchestration.test.ts b/src/lib/onboard/sandbox-create/orchestration.test.ts index 3174b3d6564..65de4f73dff 100644 --- a/src/lib/onboard/sandbox-create/orchestration.test.ts +++ b/src/lib/onboard/sandbox-create/orchestration.test.ts @@ -5,10 +5,77 @@ import { describe, expect, it, vi } from "vitest"; import type { SandboxEntry } from "../../state/registry"; import { + applyAbsentSandboxRebuildPolicyCarryForward, completeHermesPortableSandboxRegistration, + proveRecreateSourceBeforePolicyCarryForward, readManagedDcodeCreateSelectionDrift, } from "./orchestration"; +describe("authoritative rebuild policy carry-forward", () => { + it("proves the journaled source before mutating its preserved policy row (#9792)", () => { + const events: string[] = []; + const runtime = { acceptedTarget: false }; + + expect( + proveRecreateSourceBeforePolicyCarryForward({ + createRecreateRuntime: () => { + events.push("prove-source"); + return runtime; + }, + carryForward: () => events.push("carry-forward"), + }), + ).toBe(runtime); + expect(events).toEqual(["prove-source", "carry-forward"]); + }); + + it("replaces stale resumed presets after the outer rebuild deletes the source sandbox (#9792)", () => { + const note = vi.fn(); + const applyRecreatePolicyCarryForward = vi.fn(); + const filteredPolicyPresets = ["github"]; + + applyAbsentSandboxRebuildPolicyCarryForward( + { + sandboxName: "alpha", + liveExists: false, + nonInteractive: true, + note, + rebuildPolicyPresets: filteredPolicyPresets, + }, + applyRecreatePolicyCarryForward, + ); + + expect(applyRecreatePolicyCarryForward).toHaveBeenCalledExactlyOnceWith( + "alpha", + true, + note, + filteredPolicyPresets, + ); + }); + + it("preserves an intentionally empty preset selection after the outer delete (#9792)", () => { + const note = vi.fn(); + const applyRecreatePolicyCarryForward = vi.fn(); + + applyAbsentSandboxRebuildPolicyCarryForward( + { + sandboxName: "alpha", + liveExists: false, + nonInteractive: true, + note, + rebuildPolicyPresets: [], + }, + applyRecreatePolicyCarryForward, + ); + + expect(applyRecreatePolicyCarryForward).toHaveBeenCalledExactlyOnceWith( + "alpha", + true, + note, + [], + ); + }); +}); + describe("managed DCode sandbox create selection", () => { it.each([null, "https://openrouter.ai/api/v1"])( "passes the selected endpoint to live drift validation: %s (#9555)", diff --git a/src/lib/onboard/sandbox-create/orchestration.ts b/src/lib/onboard/sandbox-create/orchestration.ts index 3d38ceab270..e12d85456eb 100644 --- a/src/lib/onboard/sandbox-create/orchestration.ts +++ b/src/lib/onboard/sandbox-create/orchestration.ts @@ -109,6 +109,42 @@ export async function completeHermesPortableSandboxRegistration(input: { return registered; } +type ApplyRecreatePolicyCarryForward = ( + sandboxName: string, + nonInteractive: boolean, + note: (message: string) => void, + rebuildPolicyPresets?: readonly string[], +) => void; + +/** Reseed an outer rebuild after its owned delete leaves no live source branch. */ +export function applyAbsentSandboxRebuildPolicyCarryForward( + input: { + readonly sandboxName: string; + readonly liveExists: boolean; + readonly nonInteractive: boolean; + readonly note: (message: string) => void; + readonly rebuildPolicyPresets?: readonly string[]; + }, + applyRecreatePolicyCarryForward: ApplyRecreatePolicyCarryForward, +): void { + if (input.liveExists || !Array.isArray(input.rebuildPolicyPresets)) return; + applyRecreatePolicyCarryForward( + input.sandboxName, + input.nonInteractive, + input.note, + input.rebuildPolicyPresets, + ); +} + +export function proveRecreateSourceBeforePolicyCarryForward(input: { + readonly createRecreateRuntime: () => T; + readonly carryForward: () => void; +}): T { + const runtime = input.createRecreateRuntime(); + input.carryForward(); + return runtime; +} + export function createSandboxWithBaseImageResolution(runtime: SandboxCreateOrchestrationRuntime) { return async function createSandboxWithBaseImageResolution( baseImageResolutionContext: import("../base-image-resolution-flow").BaseImageResolutionContext, @@ -345,17 +381,35 @@ export function createSandboxWithBaseImageResolution(runtime: SandboxCreateOrche inspectSandboxForCreate, createIntent?.toolDisclosure ?? null, ); + // Prove the preserved source row before replacing its stale preset list. + // Policy carry-forward is an owned post-delete mutation, but applying it + // before recreate recovery makes the journal correctly reject that row as + // changed before the replacement can be created. let recreateRuntime: | import("../sandbox-recreate-transaction").SandboxRecreateRuntime - | OwnedSandboxRecreateRuntime = sandboxRecreateTransaction.createSandboxRecreateRuntime( - onboardSession, - createIntent?.recreateTransaction, - sandboxName, - GATEWAY_NAME, - existingEntry, - getSandboxRecreateObservation, - note, - ); + | OwnedSandboxRecreateRuntime = proveRecreateSourceBeforePolicyCarryForward({ + createRecreateRuntime: () => + sandboxRecreateTransaction.createSandboxRecreateRuntime( + onboardSession, + createIntent?.recreateTransaction, + sandboxName, + GATEWAY_NAME, + existingEntry, + getSandboxRecreateObservation, + note, + ), + carryForward: () => + applyAbsentSandboxRebuildPolicyCarryForward( + { + sandboxName, + liveExists, + nonInteractive: isNonInteractive(), + note, + rebuildPolicyPresets: createIntent?.rebuildPolicyPresets, + }, + policyPresetCarry.applyRecreatePolicyCarryForward, + ), + }); const restoreReusedSandboxDashboard = async (selectionVerified: boolean): Promise => { await dashboardPortReservationScope.release(); ({ chatUiUrl } = sandboxReuse.applyReusedSandboxDashboardState({ @@ -753,7 +807,12 @@ export function createSandboxWithBaseImageResolution(runtime: SandboxCreateOrche baseImageResolutionContext, previousEntry?.imageTag, ); - policyPresetCarry.applyRecreatePolicyCarryForward(sandboxName, isNonInteractive(), note); + policyPresetCarry.applyRecreatePolicyCarryForward( + sandboxName, + isNonInteractive(), + note, + createIntent?.rebuildPolicyPresets, + ); const noRestorePending = pendingStateRestore === null && pendingStateRestoreBackupPath === null; diff --git a/src/lib/onboard/types.ts b/src/lib/onboard/types.ts index 693646d4521..9ebecb204f0 100644 --- a/src/lib/onboard/types.ts +++ b/src/lib/onboard/types.ts @@ -82,6 +82,8 @@ export interface SandboxCreateIntent { }; /** Validated non-secret Hermes environment assignments carried by a rebuild. */ readonly rebuildPreservedEnv?: readonly import("../state/preserved-env").PreservedEnvFile[]; + /** Built-in policy presets owned by the outer authoritative rebuild lifecycle. */ + readonly rebuildPolicyPresets?: readonly string[]; } /** Durable onboarding-session identity that owns the pending inference route. */ @@ -130,6 +132,8 @@ export type OnboardOptions = { managedWorkloadRebuild?: import("./workload/rebuild").ManagedWorkloadRebuildHandoff; /** Internal validated non-secret Hermes environment assignments carried by a rebuild. */ rebuildPreservedEnv?: readonly import("../state/preserved-env").PreservedEnvFile[]; + /** Internal authoritative policy selection carried across sandbox recreation. */ + rebuildPolicyPresets?: readonly string[]; /** Internal hint for resolving the sandbox base image without repeating remote discovery. */ baseImageResolutionHint?: | import("../sandbox-base-image").SandboxBaseImageResolutionMetadata diff --git a/test/e2e/README.md b/test/e2e/README.md index 169c4f7d79c..3af2b56383f 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -97,11 +97,14 @@ Each live E2E consumer verifies that the catalog source revision matches `checko does not change a managed-image workflow path keeps the released catalog behavior. The GitHub token is available only to the trusted planner job and is not included in the candidate CLI artifact. -The same-repository `Images / Managed Images` PR workflow also runs the complete OpenClaw -`mcp-bridge` shard in two independent matrix jobs. Each job assembles one exact candidate catalog -from the workflow's published contracts, uses a fresh runner and sandbox, records the existing -trusted-private discovery diagnostics, scans the evidence for fixture credentials, and must pass. +The same-repository `Images / Managed Images` PR workflow also runs the OpenClaw managed-image MCP +discovery and lifecycle scope in two independent matrix jobs. Each job assembles one exact candidate +catalog from the workflow's published contracts, uses a fresh runner and sandbox, records the +authenticated discovery diagnostics, scans the evidence for fixture credentials, and must pass. These are two required acceptance executions, not retries; either failure remains a failed check. +The managed-image scope does not claim trusted-private DNS-rebinding coverage: host and sandbox +`/etc/hosts` fixtures do not control the OpenShell supervisor's egress resolver. Full MCP bridge E2E +coverage retains that assertion for environments with supervisor-authoritative DNS. #### Timing Baseline diff --git a/test/e2e/RETRY_INVENTORY.md b/test/e2e/RETRY_INVENTORY.md index 10d61b096b1..8a982d90ca2 100644 --- a/test/e2e/RETRY_INVENTORY.md +++ b/test/e2e/RETRY_INVENTORY.md @@ -21,7 +21,7 @@ Exhaustion remains failed. | `protected-managed-image-buildkit-transport` | One agent's exact protected managed-image `docker buildx build`; `scripts/checks/build-protected-managed-images.sh` | Final nonempty line exactly matches BuildKit's `ERROR: failed to build: failed to solve: stream error: stream ID ; INTERNAL_ERROR; received from peer`; every near-match and other Docker failure is terminal | 2 attempts; fixed 2s delay | Before retry, a read-only local registry `HEAD` request confirms that the isolated revision tag is absent. The script then repeats the same agent, inputs, revision tag, and content-addressed push. A present tag or any inconclusive registry result is terminal. Existing digest, registry-byte, and image-contract checks still gate success. | BuildKit transport | `passed-first-attempt`, `passed-after-retry`, `failed-no-retry`, or `exhausted` | Existing Docker attempt output remains in the job log. Sanitized job-log annotations retain the agent, attempt, outcome, retry delay or Docker exit status, failure class, and revision-tag state without image content. | Eligible bounded operation retry for the exact pre-export transport failure observed in #9763; repeated exact failure remains failed and no workflow rerun occurs | | `hermes-pinned-source-archive` | Hermes base-image source archive; `scripts/checks/download-hermes-source-archive.sh`, `agents/hermes/Dockerfile.base` | Final HTTP 429 only; curl transport failures and every other HTTP status are terminal | 3 total attempts; fixed 1s then 2s | Each attempt repeats the same read-only GET for one version-pinned archive. A failed or partial file is removed before another attempt, and the existing SHA-256 check remains before extraction. | GitHub source archive | `passed-first-attempt`, `passed-after-retry`, `failed-no-retry`, or `exhausted` | The build log records only the attempt number, total attempts, outcome, retry delay, and bounded failure class. Curl output, response content, URLs, paths, headers, and environment values are excluded. | Eligible bounded external read after HTTP 429 in both native Hermes base-image builds in #9815; checksum, archive, extraction, and patch failures remain terminal, and no workflow or platform-build retry occurs | | `trusted-controller-collaborator-permission-read` | Collaborator-permission reads for manual PR dispatch and Launchable E2E dispatch; `.github/workflows/e2e.yaml` | Curl exit 5, 6, 7, 16, 18, 28, 35, 52, 55, 56, 92, 95, or 96; HTTP 408, 429, or 5xx | 3 attempts; linear 1s then 2s | Read-only GitHub API request | GitHub API | Transient API read versus terminal authentication, authorization, actor, or response failure | Operation name, attempt number, and sanitized failure class or HTTP status; no response body, header, or token | Eligible bounded read; HTTP 401, 403, 404, and 422, malformed responses, actor failures, and insufficient roles remain terminal; no cached permission or workflow rerun | -| `pr-exact-openclaw-mcp-repetition` | Complete OpenClaw trusted-private MCP bridge acceptance; `.github/workflows/managed-images.yaml`, `test/e2e/live/mcp-bridge.test.ts` | Either independent matrix execution fails | 2 required executions on fresh runners; 0 workflow or test retries | Each execution creates and cleans up its own sandbox against the same exact candidate publication cohort | NemoClaw | Each execution passes or fails independently; both must pass | Existing redacted MCP diagnostics, request ledger, cleanup evidence, and fixture-credential scan for each matrix pass | Fixed acceptance repetition required by #8746; not a retry, and one pass never masks the other | +| `pr-exact-openclaw-mcp-repetition` | Exact managed-image OpenClaw MCP discovery and lifecycle acceptance; `.github/workflows/managed-images.yaml`, `test/e2e/live/mcp-bridge.test.ts` | Either independent matrix execution fails | 2 required executions on fresh runners; 0 workflow or test retries | Each execution creates and cleans up its own sandbox against the same exact candidate publication cohort | NemoClaw | Each execution passes or fails independently; both must pass | Existing redacted MCP diagnostics, request ledger, cleanup evidence, and fixture-credential scan for each matrix pass | Fixed acceptance repetition required by #8746; not a retry, and one pass never masks the other; trusted-private DNS-rebinding remains in full E2E where the supervisor resolver is authoritative | | `github-exact-artifact-content-read` | Bound base-image or PR managed-image contract artifact; `tools/e2e/exact-artifact-download.mts`, `tools/e2e/pr-managed-image-publication.mts` | Transport failure, HTTP 408, HTTP 429, or HTTP 5xx while reading one pre-bound artifact ID | 3 attempts; Retry-After or linear delay capped at 10s | Read-only request against one immutable artifact ID, name, size, digest, producer run, attempt, and producer commit | GitHub artifact service | `passed-first-attempt`, `passed-after-retry`, `exhausted` for transient exhaustion, or `failed-no-retry` for terminal HTTP; identity, size, digest, archive, and contract failures throw without an aggregate outcome or `failureClass` | Content-read attempts log only the sanitized operation, attempt, HTTP status or transport class, and outcome; thrown validation failures expose only their bounded error message, never headers, body, token, signed URL, or artifact content | Standalone bounded content read; it does not use `retry-policy.ts` or `RetryEvidence`, and all identity, integrity, archive, and contract failures remain terminal | | `inference-set-route-convergence` | Sandbox inference probe after one OpenShell route selection; `src/lib/actions/inference-set-provider.ts`, `src/lib/actions/inference-set.ts` | HTTP 400 or 404 only when the selected API family changes; authentication, authorization, unsafe or malformed input, every other HTTP status, transport failure, and probe failure are terminal | Initial 6s route-cache wait after a provider/model change; then up to 3 probes with 2s and 4s retry delays | Each retry repeats only the read-only sandbox inference probe after one route mutation | OpenShell route cache | Converged, terminal failure, or exhausted rollback | Retry progress records only HTTP status, attempt number, and delay; the final command error stays redacted, and focused tests assert the exact attempt count and rollback | The initial wait covers one full 5s OpenShell 0.0.106 cache-refresh interval even when the stale route returns a valid 2xx; exhaustion restores the prior route, removes the uncommitted provider, and remains failed | | `inference-switch-ts` | Verified inference route update; `test/e2e/fixtures/inference-switch-retry.ts` | Timeout, reset, DNS/connectivity/connect error, request transport error, or exact 502/503/504 status; authentication, authorization, policy, malformed-input, and invalid-request signals take precedence | 1-10 attempts; linear 5s | Setting the same desired provider/model is idempotent | Inference provider | Shared `RetryEvidence` classifications | Every attempt classification and aggregate outcome; command artifacts remain separate and redacted | Uses `runBoundedRetry`; deterministic verification mismatches stop; no `--no-verify` exhaustion bypass | diff --git a/test/e2e/live/mcp-bridge-agent-selection.ts b/test/e2e/live/mcp-bridge-agent-selection.ts index 5cc45eaf8f0..f322356a162 100644 --- a/test/e2e/live/mcp-bridge-agent-selection.ts +++ b/test/e2e/live/mcp-bridge-agent-selection.ts @@ -4,6 +4,9 @@ export const MCP_BRIDGE_SHARDS = ["openclaw", "hermes", "deepagents"] as const; export type McpBridgeShard = (typeof MCP_BRIDGE_SHARDS)[number]; +export const MCP_BRIDGE_E2E_SCOPES = ["full", "managed-image-discovery"] as const; +export type McpBridgeE2eScope = (typeof MCP_BRIDGE_E2E_SCOPES)[number]; + export function resolveMcpBridgeShard( value: string | undefined = process.env.NEMOCLAW_MCP_BRIDGE_AGENT, ): McpBridgeShard { @@ -13,3 +16,21 @@ export function resolveMcpBridgeShard( } return selected as McpBridgeShard; } + +export function resolveMcpBridgeE2eScope( + value: string | undefined = process.env.NEMOCLAW_MCP_BRIDGE_E2E_SCOPE, +): McpBridgeE2eScope { + const selected = value ?? "full"; + if (!MCP_BRIDGE_E2E_SCOPES.includes(selected as McpBridgeE2eScope)) { + throw new Error(`Unsupported NEMOCLAW_MCP_BRIDGE_E2E_SCOPE: ${selected}`); + } + return selected as McpBridgeE2eScope; +} + +export async function runFullMcpBridgeE2eCoverage( + scope: McpBridgeE2eScope, + operation: () => Promise, +): Promise { + if (scope !== "full") return undefined; + return operation(); +} diff --git a/test/e2e/live/mcp-bridge-onboard-env.ts b/test/e2e/live/mcp-bridge-onboard-env.ts index 22dc52f752e..c75d35273f8 100644 --- a/test/e2e/live/mcp-bridge-onboard-env.ts +++ b/test/e2e/live/mcp-bridge-onboard-env.ts @@ -10,10 +10,61 @@ const EXACT_MAIN_OVERLAY_KEYS = new Set([ "NEMOCLAW_OPENSHELL_SANDBOX_BIN", ]); +const MCP_BRIDGE_QUALIFICATION_ENV_KEYS = [ + "NEMOCLAW_E2E_EXPECTED_SHA", + "NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG", + "NEMOCLAW_RUN_LIVE_E2E", + "OPENSHELL_DOCKER_SUPERVISOR_IMAGE", +] as const; + +const MCP_BRIDGE_ONBOARD_ARGS = [ + "onboard", + "--non-interactive", + "--yes", + "--yes-i-accept-third-party-software", +] as const; + +export function buildMcpBridgeOnboardArgs( + environment: NodeJS.ProcessEnv = process.env, +): string[] { + const catalogPath = environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG?.trim(); + return catalogPath + ? [ + "onboard", + "--temp-managed-runtime", + "--temp-managed-runtime-catalog", + catalogPath, + ...MCP_BRIDGE_ONBOARD_ARGS.slice(1), + ] + : [...MCP_BRIDGE_ONBOARD_ARGS]; +} + +export function assertMcpBridgeManagedImageReceipt(options: { + environment?: NodeJS.ProcessEnv; + workload?: Record; +}): void { + const environment = options.environment ?? process.env; + if (!environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG?.trim()) return; + + const expectedRevision = environment.NEMOCLAW_E2E_EXPECTED_SHA?.trim() ?? ""; + if (!/^[0-9a-f]{40}$/u.test(expectedRevision)) { + throw new Error("managed-image MCP qualification requires an exact candidate revision"); + } + if ( + options.workload?.kind !== "managed-image" || + options.workload.sourceRevision !== expectedRevision + ) { + throw new Error( + "MCP qualification must use the exact managed image instead of a Dockerfile build", + ); + } +} + export function buildMcpBridgeExactMainEnv(options: { baseEnv?: NodeJS.ProcessEnv; envOverlay?: NodeJS.ProcessEnv; }): NodeJS.ProcessEnv { + const baseEnv = options.baseEnv ?? process.env; const envOverlay = options.envOverlay ?? {}; for (const key of Object.keys(envOverlay)) { if (!EXACT_MAIN_OVERLAY_KEYS.has(key)) { @@ -21,8 +72,14 @@ export function buildMcpBridgeExactMainEnv(options: { } } + const qualificationEnv = Object.fromEntries( + MCP_BRIDGE_QUALIFICATION_ENV_KEYS.flatMap((key) => + baseEnv[key] === undefined ? [] : [[key, baseEnv[key]]], + ), + ); return { - ...buildAvailabilityProbeEnv(options.baseEnv), + ...buildAvailabilityProbeEnv(baseEnv), + ...qualificationEnv, ...envOverlay, }; } diff --git a/test/e2e/live/mcp-bridge.test.ts b/test/e2e/live/mcp-bridge.test.ts index d57e10f78b7..9ebf6cfafe9 100644 --- a/test/e2e/live/mcp-bridge.test.ts +++ b/test/e2e/live/mcp-bridge.test.ts @@ -20,7 +20,12 @@ import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clien import { test as e2eTest, expect } from "../fixtures/e2e-test.ts"; import { MCP_BRIDGE_TEST_CREDENTIALS } from "../fixtures/mcp-bridge-credentials.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; -import { type McpBridgeShard, resolveMcpBridgeShard } from "./mcp-bridge-agent-selection.ts"; +import { + type McpBridgeShard, + resolveMcpBridgeE2eScope, + resolveMcpBridgeShard, + runFullMcpBridgeE2eCoverage, +} from "./mcp-bridge-agent-selection.ts"; import { cleanupMcpBridge, MCP_MUTATION_TIMEOUT_MS, @@ -43,7 +48,9 @@ import { reopenHermesMcpMaintenanceWindow, } from "./mcp-bridge-hermes-lifecycle.ts"; import { + assertMcpBridgeManagedImageReceipt, buildMcpBridgeExactMainEnv, + buildMcpBridgeOnboardArgs, buildMcpBridgeOnboardEnv, requireMcpBridgeTlsCaCert, } from "./mcp-bridge-onboard-env.ts"; @@ -89,11 +96,22 @@ const COMPATIBLE_MODEL = "mock/mcp-bridge"; const TOOL_CHALLENGE = "nemoclaw-authenticated-mcp-proof"; const REGISTRY_FILE = path.join(process.env.HOME ?? os.homedir(), ".nemoclaw", "sandboxes.json"); const selectedMcpBridgeShard = resolveMcpBridgeShard(); +const mcpBridgeE2eScope = resolveMcpBridgeE2eScope(); function mcpBridgeShardTest(shard: McpBridgeShard) { return selectedMcpBridgeShard === shard ? e2eTest : e2eTest.skip; } const test = mcpBridgeShardTest("openclaw"); type McpAgent = "openclaw" | "hermes" | "langchain-deepagents-code"; + +function expectManagedImageQualificationReceipt(sandboxName: string): void { + const registry = JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf8")) as { + sandboxes?: Record }>; + }; + assertMcpBridgeManagedImageReceipt({ + workload: registry.sandboxes?.[sandboxName]?.workload, + }); +} + async function onboardAgent( host: HostCliClient, cleanup: CleanupRegistry, @@ -115,7 +133,7 @@ async function onboardAgent( timeoutMs: 15 * 60_000, }); const result = await host.nemoclaw( - ["onboard", "--non-interactive", "--yes", "--yes-i-accept-third-party-software"], + buildMcpBridgeOnboardArgs(), { artifactName: options.artifactName, env: buildMcpBridgeOnboardEnv({ @@ -132,6 +150,7 @@ async function onboardAgent( }, ); expectExitZero(result, `onboard ${options.agent} sandbox for MCP bridge`); + expectManagedImageQualificationReceipt(options.sandboxName); } async function assertSecretAbsentFromSandbox( sandbox: SandboxClient, @@ -672,6 +691,7 @@ async function rebuildWithoutMcpHostSecret( env: { ...buildMcpBridgeExactMainEnv({ envOverlay }), COMPATIBLE_API_KEY: COMPATIBLE_KEY, + NEMOCLAW_REBUILD_VERBOSE: "1", NVIDIA_INFERENCE_API_KEY: COMPATIBLE_KEY, }, redactionValues: [COMPATIBLE_KEY, HOST_SECRET, ROTATED_HOST_SECRET], @@ -687,6 +707,7 @@ test("mcp-bridge", { await artifacts.writeJson("scenario.json", { id: "mcp-bridge", sandbox: OPENCLAW_SANDBOX_NAME, + scope: mcpBridgeE2eScope, server: SERVER_NAME, }); const compatibleMock = await startCompatibleMock({ @@ -721,18 +742,20 @@ test("mcp-bridge", { artifactName: "onboard-openclaw-mcp-bridge", }); // Exercise the raw OpenShell `allowed_ips` boundary before any NemoClaw MCP - // mutation. The helper uses a direct curl request with a /** binary grant, - // then restores this sandbox's exact base policy before returning, so this - // proof is independent of both the CLI implementation and adapter identity. - await assertRawOpenShellAllowedIpsRebindingDenied({ - artifacts, - env: buildAvailabilityProbeEnv(), - host, - policySettleMs: 5_000, - sandbox, - sandboxName: OPENCLAW_SANDBOX_NAME, - timeoutMs: 120_000, - }); + // mutation in full-scope topologies. The helper uses a direct curl request + // with a /** binary grant, then restores this sandbox's exact base policy + // before returning, so this proof is independent of the CLI and adapter. + await runFullMcpBridgeE2eCoverage(mcpBridgeE2eScope, () => + assertRawOpenShellAllowedIpsRebindingDenied({ + artifacts, + env: buildAvailabilityProbeEnv(), + host, + policySettleMs: 5_000, + sandbox, + sandboxName: OPENCLAW_SANDBOX_NAME, + timeoutMs: 120_000, + }), + ); cleanup.add("remove MCP bridge", () => cleanupMcpBridge(host, OPENCLAW_SANDBOX_NAME, SERVER_NAME, "mcporter"), @@ -863,18 +886,20 @@ test("mcp-bridge", { }, ); - await assertTrustedPrivateMcpRebindingDenied(host, sandbox, cleanup, { - adapter: "mcporter", - artifacts, - artifactPrefix: "openclaw", - assertSecretAbsent: assertSecretAbsentFromSandbox, - cleanupBridge: cleanupMcpBridge, - mutationTimeoutMs: MCP_MUTATION_TIMEOUT_MS.mcporter, - sandboxName: OPENCLAW_SANDBOX_NAME, - secretPaths: ["/sandbox/.openclaw", "/sandbox/.mcp.json"], - survivingMcpUrl: mcpUrl, - progress, - }); + await runFullMcpBridgeE2eCoverage(mcpBridgeE2eScope, () => + assertTrustedPrivateMcpRebindingDenied(host, sandbox, cleanup, { + adapter: "mcporter", + artifacts, + artifactPrefix: "openclaw", + assertSecretAbsent: assertSecretAbsentFromSandbox, + cleanupBridge: cleanupMcpBridge, + mutationTimeoutMs: MCP_MUTATION_TIMEOUT_MS.mcporter, + sandboxName: OPENCLAW_SANDBOX_NAME, + secretPaths: ["/sandbox/.openclaw", "/sandbox/.mcp.json"], + survivingMcpUrl: mcpUrl, + progress, + }), + ); const requestCountBeforeAllowedNodeProof = fakeMcp.requests.length; const allowedNodeCall = await runNodeMcpProbe( @@ -1047,6 +1072,7 @@ mcpBridgeShardTest("hermes")( await artifacts.writeJson("scenario.json", { id: "mcp-bridge-hermes", sandbox: HERMES_SANDBOX_NAME, + scope: mcpBridgeE2eScope, server: SERVER_NAME, }); const hermesResult = `MCP_AUTH_REWRITE_OK::${TOOL_CHALLENGE}`; @@ -1151,18 +1177,20 @@ mcpBridgeShardTest("hermes")( expectedSecret: HOST_SECRET, label: "Hermes MCP rediscovery after explicit restart", }; - await assertTrustedPrivateMcpRebindingDenied(host, sandbox, cleanup, { - adapter: "hermes-config", - artifacts, - artifactPrefix: "hermes", - assertSecretAbsent: assertSecretAbsentFromSandbox, - cleanupBridge: cleanupMcpBridge, - mutationTimeoutMs: MCP_MUTATION_TIMEOUT_MS["hermes-config"], - sandboxName: HERMES_SANDBOX_NAME, - secretPaths: ["/sandbox/.hermes"], - survivingMcpUrl: mcpUrl, - progress, - }); + await runFullMcpBridgeE2eCoverage(mcpBridgeE2eScope, () => + assertTrustedPrivateMcpRebindingDenied(host, sandbox, cleanup, { + adapter: "hermes-config", + artifacts, + artifactPrefix: "hermes", + assertSecretAbsent: assertSecretAbsentFromSandbox, + cleanupBridge: cleanupMcpBridge, + mutationTimeoutMs: MCP_MUTATION_TIMEOUT_MS["hermes-config"], + sandboxName: HERMES_SANDBOX_NAME, + secretPaths: ["/sandbox/.hermes"], + survivingMcpUrl: mcpUrl, + progress, + }), + ); await assertHermesToolCall("hermes-real-mcp-tool-call-after-dns-rebinding-remove"); const survivingDiscoveryOffset = fakeMcp.requests.length; await restartBridgeWithoutHostSecret(host, HERMES_SANDBOX_NAME, "hermes"); @@ -1281,6 +1309,7 @@ mcpBridgeShardTest("deepagents")( await artifacts.writeJson("scenario.json", { id: "mcp-bridge-deepagents", sandbox: DEEPAGENTS_SANDBOX_NAME, + scope: mcpBridgeE2eScope, server: SERVER_NAME, }); const deepAgentsResult = `MCP_AUTH_REWRITE_OK::${TOOL_CHALLENGE}`; @@ -1355,18 +1384,20 @@ mcpBridgeShardTest("deepagents")( }); await assertDeepAgentsConfig(sandbox, DEEPAGENTS_SANDBOX_NAME, mcpUrl); await assertSecretAbsentFromSandbox(sandbox, DEEPAGENTS_SANDBOX_NAME, ["/sandbox/.deepagents"]); - await assertTrustedPrivateMcpRebindingDenied(host, sandbox, cleanup, { - adapter: "deepagents-config", - artifacts, - artifactPrefix: "deepagents", - assertSecretAbsent: assertSecretAbsentFromSandbox, - cleanupBridge: cleanupMcpBridge, - mutationTimeoutMs: MCP_MUTATION_TIMEOUT_MS["deepagents-config"], - sandboxName: DEEPAGENTS_SANDBOX_NAME, - secretPaths: ["/sandbox/.deepagents"], - survivingMcpUrl: mcpUrl, - progress, - }); + await runFullMcpBridgeE2eCoverage(mcpBridgeE2eScope, () => + assertTrustedPrivateMcpRebindingDenied(host, sandbox, cleanup, { + adapter: "deepagents-config", + artifacts, + artifactPrefix: "deepagents", + assertSecretAbsent: assertSecretAbsentFromSandbox, + cleanupBridge: cleanupMcpBridge, + mutationTimeoutMs: MCP_MUTATION_TIMEOUT_MS["deepagents-config"], + sandboxName: DEEPAGENTS_SANDBOX_NAME, + secretPaths: ["/sandbox/.deepagents"], + survivingMcpUrl: mcpUrl, + progress, + }), + ); progress.phase("exercise lifecycle and confirm Deep Agents bridge removal"); await assertRealAdapterToolCall(sandbox, fakeMcp, { agent: "langchain-deepagents-code", diff --git a/test/e2e/live/mcp-provider-rewrite-probe.ts b/test/e2e/live/mcp-provider-rewrite-probe.ts index ae6cc18a511..67b06323bb9 100644 --- a/test/e2e/live/mcp-provider-rewrite-probe.ts +++ b/test/e2e/live/mcp-provider-rewrite-probe.ts @@ -1,11 +1,32 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +export function buildMcpProviderRewriteAuthorization( + credentialKey: string, + runtimeValue: string | undefined, +): string | null { + if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(credentialKey) || runtimeValue === undefined) { + return null; + } + const escapedCredentialKey = credentialKey.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); + const placeholderPattern = new RegExp( + `^openshell:resolve:env:(?:v[0-9]{1,20}_)?${escapedCredentialKey}$`, + "u", + ); + return placeholderPattern.test(runtimeValue) ? `Bearer ${runtimeValue}` : null; +} + export const MCP_PROVIDER_REWRITE_PROBE_SOURCE = `const https = require("node:https"); +const buildMcpProviderRewriteAuthorization = ${buildMcpProviderRewriteAuthorization.toString()}; const url = new URL(process.argv[2]); const method = process.argv[3]; const expectation = process.argv[4]; const credentialKey = process.argv[5] || "FAKE_MCP_SECRET"; +const authorization = buildMcpProviderRewriteAuthorization(credentialKey, process.env[credentialKey]); +if (authorization === null) { + console.error("OpenShell did not project the expected revisioned MCP credential placeholder"); + process.exit(2); +} const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method }); const req = https.request({ hostname: url.hostname, @@ -15,7 +36,7 @@ const req = https.request({ headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body), - "authorization": "Bearer openshell:resolve:env:" + credentialKey + "authorization": authorization } }, (res) => { let data = ""; diff --git a/test/e2e/support/mcp-bridge-agent-selection.test.ts b/test/e2e/support/mcp-bridge-agent-selection.test.ts index 842c590daf8..d26359250a6 100644 --- a/test/e2e/support/mcp-bridge-agent-selection.test.ts +++ b/test/e2e/support/mcp-bridge-agent-selection.test.ts @@ -1,9 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; -import { MCP_BRIDGE_SHARDS, resolveMcpBridgeShard } from "../live/mcp-bridge-agent-selection.ts"; +import { + MCP_BRIDGE_E2E_SCOPES, + MCP_BRIDGE_SHARDS, + resolveMcpBridgeE2eScope, + resolveMcpBridgeShard, + runFullMcpBridgeE2eCoverage, +} from "../live/mcp-bridge-agent-selection.ts"; describe("MCP bridge agent selection", () => { it("keeps local runs on the existing OpenClaw default", () => { @@ -20,3 +26,35 @@ describe("MCP bridge agent selection", () => { ); }); }); + +describe("MCP bridge E2E scope", () => { + it("keeps ordinary live runs on full coverage", () => { + expect(resolveMcpBridgeE2eScope(undefined)).toBe("full"); + }); + + it.each(MCP_BRIDGE_E2E_SCOPES)("accepts the reviewed %s scope", (scope) => { + expect(resolveMcpBridgeE2eScope(scope)).toBe(scope); + }); + + it("fails closed for an unreviewed scope", () => { + expect(() => resolveMcpBridgeE2eScope("skip-trusted-private")).toThrow( + "Unsupported NEMOCLAW_MCP_BRIDGE_E2E_SCOPE: skip-trusted-private", + ); + }); + + it("runs trusted-private coverage for the full scope", async () => { + const operation = vi.fn().mockResolvedValue("complete"); + + await expect(runFullMcpBridgeE2eCoverage("full", operation)).resolves.toBe("complete"); + expect(operation).toHaveBeenCalledOnce(); + }); + + it("omits trusted-private coverage from managed-image discovery", async () => { + const operation = vi.fn().mockResolvedValue("unexpected"); + + await expect( + runFullMcpBridgeE2eCoverage("managed-image-discovery", operation), + ).resolves.toBeUndefined(); + expect(operation).not.toHaveBeenCalled(); + }); +}); diff --git a/test/e2e/support/mcp-bridge-onboard-env.test.ts b/test/e2e/support/mcp-bridge-onboard-env.test.ts index 480b97564f6..1aa599a1920 100644 --- a/test/e2e/support/mcp-bridge-onboard-env.test.ts +++ b/test/e2e/support/mcp-bridge-onboard-env.test.ts @@ -4,7 +4,9 @@ import { describe, expect, it } from "vitest"; import { + assertMcpBridgeManagedImageReceipt, buildMcpBridgeExactMainEnv, + buildMcpBridgeOnboardArgs, buildMcpBridgeOnboardEnv, requireMcpBridgeTlsCaCert, } from "../live/mcp-bridge-onboard-env.ts"; @@ -42,6 +44,70 @@ describe("MCP bridge onboarding environment", () => { }); }); + it("passes managed-image qualification inputs to MCP child commands", () => { + const env = buildMcpBridgeExactMainEnv({ + baseEnv: { + GITHUB_ACTIONS: "true", + HOME: "/tmp/home", + PATH: "/usr/bin", + NEMOCLAW_E2E_EXPECTED_SHA: "a".repeat(40), + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: "/tmp/managed-pr-catalog.json", + NEMOCLAW_RUN_LIVE_E2E: "1", + OPENSHELL_DOCKER_SUPERVISOR_IMAGE: "supervisor@sha256:test", + UNRELATED_PARENT_VALUE: "must-not-leak", + }, + }); + + expect(env).toMatchObject({ + GITHUB_ACTIONS: "true", + NEMOCLAW_E2E_EXPECTED_SHA: "a".repeat(40), + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: "/tmp/managed-pr-catalog.json", + NEMOCLAW_RUN_LIVE_E2E: "1", + OPENSHELL_DOCKER_SUPERVISOR_IMAGE: "supervisor@sha256:test", + }); + expect(env.UNRELATED_PARENT_VALUE).toBeUndefined(); + }); + + it("rejects a Dockerfile workload in managed-image MCP qualification", () => { + expect(() => + assertMcpBridgeManagedImageReceipt({ + environment: { + NEMOCLAW_E2E_EXPECTED_SHA: "a".repeat(40), + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: "/tmp/managed-pr-catalog.json", + }, + workload: { kind: "dockerfile" }, + }), + ).toThrow("must use the exact managed image instead of a Dockerfile build"); + }); + + it("rejects a managed image from a different candidate revision", () => { + expect(() => + assertMcpBridgeManagedImageReceipt({ + environment: { + NEMOCLAW_E2E_EXPECTED_SHA: "a".repeat(40), + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: "/tmp/managed-pr-catalog.json", + }, + workload: { kind: "managed-image", sourceRevision: "b".repeat(40) }, + }), + ).toThrow("must use the exact managed image instead of a Dockerfile build"); + }); + + it("activates the exact managed runtime when the qualification catalog is present", () => { + expect( + buildMcpBridgeOnboardArgs({ + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: "/tmp/managed-pr-catalog.json", + }), + ).toEqual([ + "onboard", + "--temp-managed-runtime", + "--temp-managed-runtime-catalog", + "/tmp/managed-pr-catalog.json", + "--non-interactive", + "--yes", + "--yes-i-accept-third-party-software", + ]); + }); + it("passes only exact-main OpenShell overrides after fixed onboarding values", () => { const env = buildMcpBridgeOnboardEnv({ ...ONBOARD_OPTIONS, diff --git a/test/e2e/support/mcp-bridge-sandbox.test.ts b/test/e2e/support/mcp-bridge-sandbox.test.ts index 4067aa0c013..c245cac8dab 100644 --- a/test/e2e/support/mcp-bridge-sandbox.test.ts +++ b/test/e2e/support/mcp-bridge-sandbox.test.ts @@ -185,7 +185,6 @@ describe("MCP curl policy denial classification", SUITE_OPTIONS, () => { expect(citations.every((citation) => docs.includes(citation))).toBe(true); expect(docs).toContain("proxy_connect_by_hostname"); expect(docs).toContain("reopens proxy-side DNS resolution"); - }); it("adds one raw MCP policy with an exact public IP pin and no adapter identity", () => { @@ -284,7 +283,9 @@ network_policies: "utf8", ); expect( - mcpBridgeSource.match(/await assertRawOpenShellAllowedIpsRebindingDenied/g), + mcpBridgeSource.match( + /await runFullMcpBridgeE2eCoverage\(\s*mcpBridgeE2eScope,\s*\(\) =>\s*assertRawOpenShellAllowedIpsRebindingDenied\(/gu, + ), ).toHaveLength(1); expect(networkPolicySource).not.toContain("assertRawOpenShellAllowedIpsRebindingDenied"); expect(contractSource).toContain('["policy", "set", "--policy"'); diff --git a/test/e2e/support/mcp-provider-rewrite-probe.test.ts b/test/e2e/support/mcp-provider-rewrite-probe.test.ts new file mode 100644 index 00000000000..6fe09fb6c8d --- /dev/null +++ b/test/e2e/support/mcp-provider-rewrite-probe.test.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import vm from "node:vm"; + +import { describe, expect, it } from "vitest"; + +import { + buildMcpProviderRewriteAuthorization, + MCP_PROVIDER_REWRITE_PROBE_SOURCE, +} from "../live/mcp-provider-rewrite-probe.ts"; + +describe("managed MCP provider rewrite probe", () => { + it.each([ + "openshell:resolve:env:FAKE_MCP_SECRET", + "openshell:resolve:env:v0_FAKE_MCP_SECRET", + "openshell:resolve:env:v1_FAKE_MCP_SECRET", + "openshell:resolve:env:v14429878272859325890_FAKE_MCP_SECRET", + ])("uses only an exact OpenShell placeholder value [case %#]", (runtimeValue) => { + expect(buildMcpProviderRewriteAuthorization("FAKE_MCP_SECRET", runtimeValue)).toBe( + `Bearer ${runtimeValue}`, + ); + }); + + it.each([ + undefined, + "raw-secret", + "openshell:resolve:env:v_FAKE_MCP_SECRET", + "openshell:resolve:env:v144298782728593258901_FAKE_MCP_SECRET", + "openshell:resolve:env:v1_OTHER_MCP_SECRET", + "openshell:resolve:env:vbad_FAKE_MCP_SECRET", + "openshell:resolve:env:v1_FAKE_MCP_SECRET\nAuthorization: Bearer raw-secret", + ])("rejects an absent or unsafe runtime value [case %#]", (runtimeValue) => { + expect(buildMcpProviderRewriteAuthorization("FAKE_MCP_SECRET", runtimeValue)).toBeNull(); + }); + + it("embeds the reviewed helper and reads the fresh child environment", () => { + expect(() => new vm.Script(MCP_PROVIDER_REWRITE_PROBE_SOURCE)).not.toThrow(); + expect(MCP_PROVIDER_REWRITE_PROBE_SOURCE).toContain("process.env[credentialKey]"); + expect(MCP_PROVIDER_REWRITE_PROBE_SOURCE).not.toContain( + '"Bearer openshell:resolve:env:" + credentialKey', + ); + }); +}); diff --git a/test/e2e/support/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index ebc4cfebed9..5344ecd9dcb 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -85,6 +85,12 @@ describe("exact PR managed-image publication (#8746, #9464)", () => { expect( managedImagePublicationRequired(["tools/mcp-tool-discovery-runtime/server.mts"], patterns), ).toBe(true); + expect( + managedImagePublicationRequired( + ["src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts"], + patterns, + ), + ).toBe(true); expect(managedImagePublicationRequired(["docs/My Guide.md"], patterns)).toBe(false); expect(() => managedImagePublicationRequired(["src/lib/onboard/file.ts\nother"], patterns), diff --git a/test/helpers/managed-image-publication-workflow-boundary.ts b/test/helpers/managed-image-publication-workflow-boundary.ts index 9f5b02d3e06..8c5b8fb4a5e 100644 --- a/test/helpers/managed-image-publication-workflow-boundary.ts +++ b/test/helpers/managed-image-publication-workflow-boundary.ts @@ -20,6 +20,7 @@ const managedInputPaths = [ "nemoclaw/**", "nemoclaw-blueprint/**", "scripts/**", + "src/lib/actions/sandbox/mcp-bridge-*.ts", "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json", "src/lib/core/json-types.ts", "src/lib/core/ports.ts", diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index a3e60bc1e8c..495d633ebd4 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -579,8 +579,12 @@ describe("complete managed-image publication workflow", () => { expect(exportContract.if).toBe(sameRepository); expect(uploadContract.if).toBe(sameRepository); expect(steps.indexOf(logout)).toBeLessThan(steps.indexOf(exportContract)); - expect(exportContract.run).toContain("scripts/checks/pull-public-exact-digest.sh"); - expect(exportContract.run).toContain("revision: $revision"); + const exportContractRun = exportContract.run ?? ""; + expect(exportContractRun).toContain("scripts/checks/pull-public-exact-digest.sh"); + expect(exportContractRun.indexOf("scripts/checks/pull-public-exact-digest.sh")).toBeLessThan( + exportContractRun.indexOf('docker buildx imagetools inspect "$reference" --raw'), + ); + expect(exportContractRun).toContain("revision: $revision"); expect(JSON.stringify(prBuilder).match(/secrets\.GITHUB_TOKEN/gu)).toHaveLength(1); expect(JSON.stringify(prBuilder)).not.toContain("github.token"); }); @@ -621,9 +625,13 @@ describe("complete managed-image publication workflow", () => { expect(steps.map(({ name }) => name)).toContain("Upload managed runtime activation evidence"); }); - it("passes the reported OpenClaw trusted-private MCP discovery twice on one exact PR cohort (#8746)", () => { + it("passes the reported OpenClaw managed-image MCP discovery twice on one exact PR cohort (#8746)", () => { const workflow = readWorkflow("managed-images.yaml"); const discovery = managedPrOpenClawMcpDiscovery(workflow); + const stableMcp = required( + readWorkflow("e2e.yaml").jobs?.["mcp-bridge"], + "unified E2E workflow is missing its stable MCP job", + ); expect(discovery.needs).toBe("pr-build-and-entrypoint"); expect(discovery.if).toContain( "github.event.pull_request.head.repo.full_name == github.repository", @@ -637,9 +645,19 @@ describe("complete managed-image publication workflow", () => { ); expect(discovery.env?.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG).toContain("managed-pr-catalog.json"); expect(discovery.env?.NEMOCLAW_MCP_BRIDGE_AGENT).toBe("openclaw"); + expect(discovery.env?.NEMOCLAW_MCP_BRIDGE_E2E_SCOPE).toBe("managed-image-discovery"); expect(discovery.env?.NEMOCLAW_E2E_REQUIRE_EXECUTED_TEST).toBe("1"); expect(discovery.env?.NEMOCLAW_E2E_SHARD).toBe("openclaw"); expect(discovery.env?.NEMOCLAW_RUN_LIVE_E2E).toBe("1"); + const stableSupervisorImage = required( + stableMcp.env?.OPENSHELL_DOCKER_SUPERVISOR_IMAGE, + "stable MCP job is missing OPENSHELL_DOCKER_SUPERVISOR_IMAGE", + ); + const discoverySupervisorImage = required( + discovery.env?.OPENSHELL_DOCKER_SUPERVISOR_IMAGE, + "OpenClaw MCP discovery is missing OPENSHELL_DOCKER_SUPERVISOR_IMAGE", + ); + expect(discoverySupervisorImage).toBe(stableSupervisorImage); expect(discovery.env).not.toHaveProperty("E2E_MANAGED_IMAGE_REVISION"); expect(JSON.stringify(discovery)).not.toContain("secrets."); expect(JSON.stringify(discovery)).not.toContain("github.token"); @@ -651,7 +669,7 @@ describe("complete managed-image publication workflow", () => { expect(assemble).toMatch( /npm ci --ignore-scripts[\s\S]*pr-managed-image-publication\.mts assemble[\s\S]*"\$CANDIDATE_SHA"[\s\S]*"\$\{contracts\[@\]\}"/u, ); - const run = step(discovery, "Run exact OpenClaw trusted-private MCP discovery").run ?? ""; + const run = step(discovery, "Run exact OpenClaw managed-image MCP discovery").run ?? ""; expect(run).toContain('[[ "$(git rev-parse --verify HEAD)" == "$CANDIDATE_SHA" ]]'); expect(JSON.stringify(discovery)).not.toContain("jq "); expect(run).toMatch(/npx --no-install tsx[\s\S]*test\/e2e\/live\/mcp-bridge\.test\.ts/u); diff --git a/test/mcp-tool-discovery-image-contract.test.ts b/test/mcp-tool-discovery-image-contract.test.ts index 6bf0794e5f3..f1f72032668 100644 --- a/test/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp-tool-discovery-image-contract.test.ts @@ -210,13 +210,13 @@ describe("MCP tool discovery image contract", () => { ); const expectedHashes = { "managed-startup-image-runtime.bundle": - "3b0effec4edb0b139cd6f7b7f410c4d54092aa87d8aa350b22f8e5eaf76c9db8", + "7c16aeeba1b1cd613878c7ebd706cf0af57519a8d49e778866a07d4972a3e602", "mcp-tool-discovery/BUNDLED_PACKAGES.json": "df5dc8f167101085a8e73c444aa56854b2a4716a0bb7de9886fec4e50f402601", "mcp-tool-discovery/THIRD_PARTY_LICENSES.txt": "ae0820debd0e33a10baa3a9c6c7ea831e8ad32a43f8500d52c7dc961ba5513a5", "mcp-tool-discovery/mcp-tool-discovery.bundle": - "defdba693829bfdfad16ce2edaad6b0a454388a32f15113854850e652a950012", + "5622323afbace37445582fa889da4cfbae31bf8ecb2a5bab571026f9cc479fdb", } as const; Object.entries(expectedHashes).forEach(([relativePath, expectedHash]) => { diff --git a/test/pr-risk-plan.test.ts b/test/pr-risk-plan.test.ts index 1ee04657f16..fde0803ea12 100644 --- a/test/pr-risk-plan.test.ts +++ b/test/pr-risk-plan.test.ts @@ -403,6 +403,7 @@ describe("deterministic PR risk plan", () => { "agents/hermes/Dockerfile", "agents/langchain-deepagents-code/Dockerfile", "scripts/checks/run-managed-image-direct-e2e.ts", + "src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts", "src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.106.json", "src/lib/onboard/managed-startup/image-runtime.ts", ]; @@ -432,6 +433,7 @@ describe("deterministic PR risk plan", () => { "nemoclaw/src/index.ts", "nemoclaw-blueprint/blueprint.yaml", "scripts/checks/build-protected-managed-images.sh", + "src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts", "src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.106.json", "src/lib/core/json-types.ts", "src/lib/core/ports.ts", diff --git a/tools/advisors/risk-plan.mts b/tools/advisors/risk-plan.mts index f843a49ab35..e96013c2316 100644 --- a/tools/advisors/risk-plan.mts +++ b/tools/advisors/risk-plan.mts @@ -132,6 +132,7 @@ const MANAGED_IMAGE_MULTIARCH_INPUT_PREFIXES = [ "nemoclaw/", "nemoclaw-blueprint/", "scripts/", + "src/lib/actions/sandbox/mcp-bridge-", "src/lib/messaging/", "src/lib/onboard/managed-startup/", "tools/mcp-tool-discovery-runtime/", diff --git a/tools/e2e/base-image-publication.mts b/tools/e2e/base-image-publication.mts index 65de753e55f..c226cae5eab 100644 --- a/tools/e2e/base-image-publication.mts +++ b/tools/e2e/base-image-publication.mts @@ -41,6 +41,10 @@ const REVIEWED_PATH_GLOBS = new Map([ "test/e2e/live/managed-image-activation-e2e*.ts", /^test\/e2e\/live\/managed-image-activation-e2e[^/]*[.]ts$/u, ], + [ + "src/lib/actions/sandbox/mcp-bridge-*.ts", + /^src\/lib\/actions\/sandbox\/mcp-bridge-[^/]*[.]ts$/u, + ], [ "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json", /^src\/lib\/actions\/sandbox\/openshell-child-visible-credentials[.]v[^/]*[.]json$/u, diff --git a/tools/mcp-tool-discovery-runtime/mcp-tool-discovery.ts b/tools/mcp-tool-discovery-runtime/mcp-tool-discovery.ts index 8a9a54caa65..7139cd6593b 100644 --- a/tools/mcp-tool-discovery-runtime/mcp-tool-discovery.ts +++ b/tools/mcp-tool-discovery-runtime/mcp-tool-discovery.ts @@ -36,13 +36,28 @@ async function main(): Promise { const deadlineSignal = AbortSignal.timeout(MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs); const boundedFetch = createBoundedMcpFetch(globalThis.fetch, deadlineSignal); + // check-direct-credential-env-ignore -- this boundary accepts only the exact + // key-bound OpenShell placeholder syntax below; raw credentials fail closed + // and are never placed in argv, output, or a network request. + const authorization = buildMcpToolDiscoveryAuthorizationPlaceholder( + runtimeArguments.credentialEnv, + process.env[runtimeArguments.credentialEnv], + ); + if (!authorization) { + writeResult({ + ok: false, + count: 0, + tools: [], + truncated: false, + detail: "managed MCP credential placeholder is unavailable", + }); + return; + } const transport = new StreamableHTTPClientTransport(runtimeArguments.url, { fetch: boundedFetch, requestInit: { headers: { - authorization: buildMcpToolDiscoveryAuthorizationPlaceholder( - runtimeArguments.credentialEnv, - ), + authorization, }, redirect: "manual", }, diff --git a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle index caf98b557bb..94ccc3389ab 100644 --- a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle +++ b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle @@ -1,4 +1,4 @@ -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 image_runtime_exports={};__export(image_runtime_exports,{applyManagedBootstrapEnvelope:()=>applyManagedBootstrapEnvelope,main:()=>main2,managedBootstrapEnvelopeClaimPaths:()=>managedBootstrapEnvelopeClaimPaths,readManagedBootstrapEnvelope:()=>readManagedBootstrapEnvelope,recoverManagedBootstrapEnvelopeClaim:()=>recoverManagedBootstrapEnvelopeClaim,verifyManagedBootstrapImageCompletion:()=>verifyManagedBootstrapImageCompletion,waitForManagedBootstrapImageCompletion:()=>waitForManagedBootstrapImageCompletion});module.exports=__toCommonJS(image_runtime_exports);var import_node_fs4=__toESM(require("node:fs"));var import_node_path4=__toESM(require("node:path"));var import_node_child_process=require("node:child_process");var import_node_crypto6=require("node:crypto");var import_node_fs3=__toESM(require("node:fs"));var import_node_path3=__toESM(require("node:path"));var import_node_buffer2=require("node:buffer");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"],auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"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",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. Use Node HTTPS against","https://discord.com/api/v10/gateway or validate the configured",'messaging bridge/gateway path. DNS-only checks such as dns.resolve("gateway.discord.gg")',"can also be inconclusive behind a proxy."]}],render:[{id:"discord-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.discord",value:{enabled:true,accounts:{default:{token:"{{credential.discordBotToken.placeholder}}",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:["DISCORD_BOT_TOKEN={{credential.discordBotToken.placeholder}}","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.7.1":"sha512-tZfdC1YA8oVLvc2BK1w0F6rUljS5ugCOp2uWe0vPsbG1fbzVVIO4V32RoqZznGHe5u2R9u4n1aV5Z/qa1m2oFg=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/discord/-/discord-2026.7.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)",enrollmentNotes:["\u2503 GOOGLE CHAT \u2014 appPrincipal","\u2503","\u2503 Workspace account \u2192 leave blank, done.","\u2503 Personal Gmail \u2192 needs the add-on's ~21-digit ID (not an email), stable across rebuilds.","\u2503","\u2503 If you already know it, paste it at the prompt and you're done.","\u2503 If not, leave it blank \u2014 the first DM reveals it once the sandbox is live:","\u2503","\u2503 1. Watch the gateway log:",'\u2503 nemoclaw logs --follow | grep "unexpected add-on principal"',"\u2503 2. DM the bot once \u2014 it won't reply yet, that's expected. The log prints:","\u2503 unexpected add-on principal: ","\u2503 3. Save that and rebuild:","\u2503 GOOGLECHAT_APP_PRINCIPAL= nemoclaw channels add googlechat","\u2503 nemoclaw rebuild --yes"],supportedAgents:["openclaw"],auth:{mode:"token-paste"},inputs:[{id:"serviceAccount",kind:"secret",required:true,envKey:"GOOGLECHAT_SERVICE_ACCOUNT",maskCap:40,formatHint:"Paste the entire service-account JSON key on one line (minified) \u2014 the whole downloaded JSON file.",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).",""].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",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 user IDs)",help:"Optional: restrict who can DM the bot. Enter Google Chat user IDs (users/NNN) \u2014 NOT emails: the bot matches IDs only by default, so an email entry is ignored. Leave blank to require pairing (recommended).",emptyValueMessage:"bot will require manual pairing"}}],credentials:[],policyPresets:[{name:"googlechat",policyKeys:["googlechat"]}],render:[{id:"googlechat-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.googlechat",value:{enabled:true,serviceAccountFile:"/nonexistent/googlechat-gateway-minted-no-service-account-file",audienceType:"{{googlechatConfig.audienceType}}",audience:"{{googlechatConfig.audience}}",appPrincipal:"{{googlechatConfig.appPrincipal}}",webhookPath:"/googlechat",healthMonitor:{enabled:false},dm:{policy:"{{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"}}}],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.7.1":"sha512-Dv0xOmcxAThEr6hoK+ioofHNu18hfbIceQrEHX3AHZPpOUiTJvToVpA5eX87NQINewwfSJf0gVhE6kSbSk2Aew=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.7.1.tgz"},required:true}],hooks:[{id:"googlechat-tunnel-audience-gate",phase:"enroll",handler:"googlechat.tunnelAudienceGate",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:"appPrincipal",kind:"config"},{id:"allowFrom",kind:"config"}]}]};var slackRuntimeEnvAliases=[{envKey:"SLACK_BOT_TOKEN",match:"^openshell:resolve:env:(v[0-9]+_)?SLACK_BOT_TOKEN$",value:"xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN",message:"[channels] Normalized SLACK_BOT_TOKEN runtime placeholder to the Bolt-compatible alias"},{envKey:"SLACK_APP_TOKEN",match:"^openshell:resolve:env:(v[0-9]+_)?SLACK_APP_TOKEN$",value:"xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN",message:"[channels] Normalized SLACK_APP_TOKEN runtime placeholder to the Bolt-compatible alias"}];var slackManifest={schemaVersion:1,id:"slack",displayName:"Slack",description:"Slack bot messaging",supportedAgents:["openclaw","hermes"],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:{botToken:"{{credential.slackBotToken.placeholder}}",appToken:"{{credential.slackAppToken.placeholder}}",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_BOT_TOKEN={{credential.slackBotToken.placeholder}}","SLACK_APP_TOKEN={{credential.slackAppToken.placeholder}}","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"]},envAliases:slackRuntimeEnvAliases,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:{envAliases:slackRuntimeEnvAliases}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/slack@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.7.1":"sha512-dwVGEVCmoTQrOIeZaSCIOPg8pT7hB883QQEXdp9EZUDzTGuvSc+KxH2iERSOV/59hROQctYdcobGn/vdB1H4XA=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/slack/-/slack-2026.7.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 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"]}],hostForward:{port:"{{teamsConfig.webhookPort}}",label:"Microsoft Teams webhook"},render:[{id:"teams-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.msteams",value:{enabled:true,appId:"{{teamsConfig.appId}}",appPassword:"{{credential.teamsClientSecret.placeholder}}",tenantId:"{{teamsConfig.tenantId}}",webhook:{port:"{{teamsConfig.webhookPort}}",path:"/api/messages"},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_CLIENT_SECRET={{credential.teamsClientSecret.placeholder}}","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)"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/msteams@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.7.1":"sha512-gG/Yk6HZAguHwrmKjsqdONbFz5WNy126PEAXQWNW/TulO1kIifQ6tktM16BQPNLnkmWqLbj+TrrO55Cjas1aFg=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.7.1.tgz"},required:true},{id:"hermesTeamsAppsPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"microsoft-teams-apps==2.0.13.4",required:true},{id:"hermesAiohttpPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"aiohttp==3.14.3",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"}]}]};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"],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",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:{botToken:"{{credential.telegramBotToken.placeholder}}",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_BOT_TOKEN={{credential.telegramBotToken.placeholder}}","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 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"}],policyPresets:[{name:"wechat",policyKeys:["wechat_bridge"]}],render:[{id:"wechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.openclaw-weixin",value:{enabled:true}}},{id:"wechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WEIXIN_TOKEN={{credential.wechatBotToken.placeholder}}","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-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing WeChat diagnostics (provider readiness + inference errors)",installedMessage:"[channels] WeChat diagnostics installed (NODE_OPTIONS updated)"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@tencent-weixin/openclaw-weixin@2.4.3",pin:true,integrity:"sha512-dPQbidUNWigC6V10vGW4i+GLH09x+6zUhafZRjuxkJ9GDu8o62WBsnUTojp4KqUH756hz+t2v9khiCRSi0dBDw==",tarballUrl:"https://registry.npmjs.org/@tencent-weixin/openclaw-weixin/-/openclaw-weixin-2.4.3.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-seed-openclaw-account",phase:"post-agent-install",handler:"wechat.seedOpenClawAccount",agents:["openclaw"],inputs:["wechatConfig.accountId","wechatConfig.baseUrl","wechatConfig.userId","credential.wechatBotToken.placeholder"],outputs:[{id:"openclawWeixinAccountsIndex",kind:"build-file",required:true},{id:"openclawWeixinAccountFile",kind:"build-file",required:true},{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:[],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.7.1":"sha512-wLY/Omc5fleRpl2lKGN8sxt/8hYfHGwLRezmWsk8oCbea5pRKUPE6ZX+wJO1O52NOJkAGCuiXvS7x0qIeKxXbQ=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.7.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,path5){const stateInput=context.inputs.find(input=>input.statePath===path5);if(stateInput?.value!==void 0)return stateInput.value;const inputId=path5.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);default:break}const allowReference=reference.match(/^allowedIds[.]googlechat[.](values|dmPolicy)$/);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);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$/;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.")}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)||WECHAT_ILINK_IDC_HOST_PATTERN.test(normalized)}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_crypto=__toESM(require("node:crypto"));function hashCredential(value){const normalized=String(value??"").trim();if(!normalized)return null;return import_node_crypto.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 revisionMatch=suffix.match(/^v[0-9]+_(.+)$/);return revisionMatch?.[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,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[input.inputId]=true;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.sourceInput]=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,"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)||!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 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 hasCanonicalNetworkPolicyReferences(value){if(!isObjectRecord(value)||!Object.hasOwn(value,"entries"))return true;return hasCanonicalChannelReferences(value.entries)}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_crypto2=require("node:crypto");var import_node_util=require("node:util");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 MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=1;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"]);var MESSAGING_CREDENTIAL_PLACEHOLDER_RE=/^(?:openshell:resolve:env:|[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-)(?:v[0-9]+_)?[A-Z][A-Z0-9_]*$/u;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:true,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_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_DISABLE_DEVICE_AUTH","agentConfig.deviceAuth.disabled"),affordance("NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE","agentConfig.deviceAuth.optOutSource"),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.url"),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","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","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","deviceAuth","minimalBootstrap"]);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 DEVICE_AUTH_KEYS=new Set(["disabled","optOutSource"]);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&&path5[0]==="messaging"&&path5[1]==="plan"&&path5[2]==="agentRender"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[3]??"")&&path5[4]==="value";return isCredentialBindingPlaceholder||isAgentRenderValuePlaceholder}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]+_/u,"")}function containsMessagingCredentialPlaceholder(value){return value.includes("openshell:resolve:env:")||value.includes("-OPENSHELL-RESOLVE-ENV-")}function isMessagingCredentialPlaceholderAssignment(path5,value){if(path5.length!==6||path5[0]!=="messaging"||path5[1]!=="plan"||path5[2]!=="agentRender"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[3]??"")||path5[4]!=="lines"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[5]??"")){return false}const separator=value.indexOf("=");if(separator<=0||value.indexOf("=",separator+1)!==-1)return false;const envKey=value.slice(0,separator);const placeholderEnvKey=messagingCredentialPlaceholderEnvKey(value.slice(separator+1));return CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&envKey===placeholderEnvKey}function isMessagingPackagePin(path5,value){return path5.length===6&&path5[0]==="messaging"&&path5[1]==="plan"&&path5[2]==="buildSteps"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[3]??"")&&path5[4]==="value"&&path5[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(path5){return path5.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 isLoopbackUrl(value){const hostname=new URL(value).hostname.toLowerCase();return hostname==="localhost"||hostname==="127.0.0.1"||hostname==="::1"||hostname==="[::1]"}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:[]}];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(!isMessagingCredentialPlaceholder(current.path,current.value)&&!isMessagingCredentialPlaceholderAssignment(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 validateDeviceAuth(value){const deviceAuth=requireRecord(value,"agentConfig.deviceAuth");rejectUnknownKeys(deviceAuth,DEVICE_AUTH_KEYS,"agentConfig.deviceAuth");return{disabled:requireBoolean(deviceAuth.disabled,"agentConfig.deviceAuth.disabled"),optOutSource:requireStringEnum(deviceAuth.optOutSource,new Set(["operator","managed-onboard"]),"agentConfig.deviceAuth.optOutSource")}}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),deviceAuth:validateDeviceAuth(config.deviceAuth),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=!isLoopbackUrl(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(!isLoopbackUrl(url)){invalid("Hermes dashboard.url must remain loopback; OpenShell owns the host forward")}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,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")}return{agent,mode,url,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){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 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,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 profile=validateManagedStartupProfile(parsed);if(serializeManagedStartupProfile(profile)!==raw){invalid("payload is not in canonical form")}return profile}function fingerprintManagedStartupProfile(profile){return(0,import_node_crypto2.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){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,path5,value){return Object.freeze({kind:"root-owned-file",legacyInput,path:path5,contents:`${value} +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 image_runtime_exports={};__export(image_runtime_exports,{applyManagedBootstrapEnvelope:()=>applyManagedBootstrapEnvelope,main:()=>main2,managedBootstrapEnvelopeClaimPaths:()=>managedBootstrapEnvelopeClaimPaths,readManagedBootstrapEnvelope:()=>readManagedBootstrapEnvelope,recoverManagedBootstrapEnvelopeClaim:()=>recoverManagedBootstrapEnvelopeClaim,verifyManagedBootstrapImageCompletion:()=>verifyManagedBootstrapImageCompletion,waitForManagedBootstrapImageCompletion:()=>waitForManagedBootstrapImageCompletion});module.exports=__toCommonJS(image_runtime_exports);var import_node_fs4=__toESM(require("node:fs"));var import_node_path4=__toESM(require("node:path"));var import_node_child_process=require("node:child_process");var import_node_crypto6=require("node:crypto");var import_node_fs3=__toESM(require("node:fs"));var import_node_path3=__toESM(require("node:path"));var import_node_buffer2=require("node:buffer");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"],auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"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",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. Use Node HTTPS against","https://discord.com/api/v10/gateway or validate the configured",'messaging bridge/gateway path. DNS-only checks such as dns.resolve("gateway.discord.gg")',"can also be inconclusive behind a proxy."]}],render:[{id:"discord-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.discord",value:{enabled:true,accounts:{default:{token:"{{credential.discordBotToken.placeholder}}",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:["DISCORD_BOT_TOKEN={{credential.discordBotToken.placeholder}}","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.7.1":"sha512-tZfdC1YA8oVLvc2BK1w0F6rUljS5ugCOp2uWe0vPsbG1fbzVVIO4V32RoqZznGHe5u2R9u4n1aV5Z/qa1m2oFg=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/discord/-/discord-2026.7.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"],auth:{mode:"token-paste"},inputs:[{id:"serviceAccount",kind:"secret",required:true,envKey:"GOOGLECHAT_SERVICE_ACCOUNT",maskCap:40,formatHint:"Paste the entire service-account JSON key on one line (minified) \u2014 the whole downloaded JSON file.",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).",""].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,serviceAccountFile:"/nonexistent/googlechat-gateway-minted-no-service-account-file",audienceType:"{{googlechatConfig.audienceType}}",audience:"{{googlechatConfig.audience}}",appPrincipal:"{{googlechatConfig.appPrincipal}}",webhookPath:"/googlechat",healthMonitor:{enabled:false},dm:{policy:"{{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.7.1":"sha512-Dv0xOmcxAThEr6hoK+ioofHNu18hfbIceQrEHX3AHZPpOUiTJvToVpA5eX87NQINewwfSJf0gVhE6kSbSk2Aew=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.7.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 slackRuntimeEnvAliases=[{envKey:"SLACK_BOT_TOKEN",match:"^openshell:resolve:env:(v[0-9]+_)?SLACK_BOT_TOKEN$",value:"xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN",message:"[channels] Normalized SLACK_BOT_TOKEN runtime placeholder to the Bolt-compatible alias"},{envKey:"SLACK_APP_TOKEN",match:"^openshell:resolve:env:(v[0-9]+_)?SLACK_APP_TOKEN$",value:"xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN",message:"[channels] Normalized SLACK_APP_TOKEN runtime placeholder to the Bolt-compatible alias"}];var slackManifest={schemaVersion:1,id:"slack",displayName:"Slack",description:"Slack bot messaging",supportedAgents:["openclaw","hermes"],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:{botToken:"{{credential.slackBotToken.placeholder}}",appToken:"{{credential.slackAppToken.placeholder}}",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_BOT_TOKEN={{credential.slackBotToken.placeholder}}","SLACK_APP_TOKEN={{credential.slackAppToken.placeholder}}","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"]},envAliases:slackRuntimeEnvAliases,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:{envAliases:slackRuntimeEnvAliases}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/slack@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.7.1":"sha512-dwVGEVCmoTQrOIeZaSCIOPg8pT7hB883QQEXdp9EZUDzTGuvSc+KxH2iERSOV/59hROQctYdcobGn/vdB1H4XA=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/slack/-/slack-2026.7.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 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"]}],hostForward:{port:"{{teamsConfig.webhookPort}}",label:"Microsoft Teams webhook"},render:[{id:"teams-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.msteams",value:{enabled:true,appId:"{{teamsConfig.appId}}",appPassword:"{{credential.teamsClientSecret.placeholder}}",tenantId:"{{teamsConfig.tenantId}}",webhook:{port:"{{teamsConfig.webhookPort}}",path:"/api/messages"},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_CLIENT_SECRET={{credential.teamsClientSecret.placeholder}}","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)"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/msteams@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.7.1":"sha512-gG/Yk6HZAguHwrmKjsqdONbFz5WNy126PEAXQWNW/TulO1kIifQ6tktM16BQPNLnkmWqLbj+TrrO55Cjas1aFg=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.7.1.tgz"},required:true},{id:"hermesTeamsAppsPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"microsoft-teams-apps==2.0.13.4",required:true},{id:"hermesAiohttpPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"aiohttp==3.14.3",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"}]}]};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"],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",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:{botToken:"{{credential.telegramBotToken.placeholder}}",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_BOT_TOKEN={{credential.telegramBotToken.placeholder}}","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){if(!isPlainDataObject(value)||!isWechatAccountFilePath(ownDataPropertyValue(value,"path"))||ownDataPropertyValue(value,"mode")!==WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.mode||!isPlainDataObject(ownDataPropertyValue(value,"content"))){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 isPlainDataObject(value){return value!==null&&typeof value==="object"&&Object.getPrototypeOf(value)===Object.prototype}function ownDataPropertyValue(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}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"}],policyPresets:[{name:"wechat",policyKeys:["wechat_bridge"]}],render:[{id:"wechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.openclaw-weixin",value:{enabled:true}}},{id:"wechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WEIXIN_TOKEN={{credential.wechatBotToken.placeholder}}","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-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing WeChat diagnostics (provider readiness + inference errors)",installedMessage:"[channels] WeChat diagnostics installed (NODE_OPTIONS updated)"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@tencent-weixin/openclaw-weixin@2.4.3",pin:true,integrity:"sha512-dPQbidUNWigC6V10vGW4i+GLH09x+6zUhafZRjuxkJ9GDu8o62WBsnUTojp4KqUH756hz+t2v9khiCRSi0dBDw==",tarballUrl:"https://registry.npmjs.org/@tencent-weixin/openclaw-weixin/-/openclaw-weixin-2.4.3.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:[],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.7.1":"sha512-wLY/Omc5fleRpl2lKGN8sxt/8hYfHGwLRezmWsk8oCbea5pRKUPE6ZX+wJO1O52NOJkAGCuiXvS7x0qIeKxXbQ=="},tarballUrlByVersion:{"2026.7.1":"https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.7.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,path5){const stateInput=context.inputs.find(input=>input.statePath===path5);if(stateInput?.value!==void 0)return stateInput.value;const inputId=path5.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$/;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.")}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)||WECHAT_ILINK_IDC_HOST_PATTERN.test(normalized)}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_crypto=__toESM(require("node:crypto"));function hashCredential(value){const normalized=String(value??"").trim();if(!normalized)return null;return import_node_crypto.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 revisionMatch=suffix.match(/^v[0-9]+_(.+)$/);return revisionMatch?.[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,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,"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 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 hasCanonicalNetworkPolicyReferences(value){if(!isObjectRecord(value)||!Object.hasOwn(value,"entries"))return true;return hasCanonicalChannelReferences(value.entries)}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_crypto2=require("node:crypto");var import_node_util=require("node:util");function listMessagingCredentialEnvAssignments(options={}){return selectManifests(options).flatMap(manifest=>{const credentialsByTemplate=new Map(manifest.credentials.map(credential=>[`{{credential.${credential.id}.placeholder}}`,credential]));return 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}]})})})}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 authorizeMessagingManagedStartupPlaceholders(step){if(!isPlainDataObject2(step))return[];const contract=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT;if(ownDataPropertyValue2(step,"channelId")!==contract.channelId||ownDataPropertyValue2(step,"hookId")!==contract.planHookId||ownDataPropertyValue2(step,"handler")!==contract.handlerId||ownDataPropertyValue2(step,"outputId")!==contract.outputId||ownDataPropertyValue2(step,"kind")!==contract.kind||ownDataPropertyValue2(step,"required")!==contract.required){return[]}return authorizeWechatAccountFilePlaceholders(ownDataPropertyValue2(step,"value")).map(authorization=>({...authorization,path:["value",...authorization.path]}))}function isPlainDataObject2(value){return value!==null&&typeof value==="object"&&Object.getPrototypeOf(value)===Object.prototype}function ownDataPropertyValue2(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 MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=1;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"]);var MESSAGING_CREDENTIAL_PLACEHOLDER_RE=/^(?:openshell:resolve:env:|[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-)(?:v[0-9]+_)?[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 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:true,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_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_DISABLE_DEVICE_AUTH","agentConfig.deviceAuth.disabled"),affordance("NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE","agentConfig.deviceAuth.optOutSource"),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.url"),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","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","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","deviceAuth","minimalBootstrap"]);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 DEVICE_AUTH_KEYS=new Set(["disabled","optOutSource"]);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&&path5[0]==="messaging"&&path5[1]==="plan"&&path5[2]==="agentRender"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[3]??"")&&path5[4]==="value";const isAuthorizedBuildStepPlaceholder=allowedBuildStepPlaceholders.has(buildStepPlaceholderKey(path5,value));return isCredentialBindingPlaceholder||isAgentRenderValuePlaceholder||isAuthorizedBuildStepPlaceholder}function buildStepPlaceholderKey(path5,value){return JSON.stringify([path5,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]+_/u,"")}function containsMessagingCredentialPlaceholder(value){return value.includes("openshell:resolve:env:")||value.includes("-OPENSHELL-RESOLVE-ENV-")}function isMessagingCredentialPlaceholderAssignment(selectedAgent,path5,value){if(path5.length!==6||path5[0]!=="messaging"||path5[1]!=="plan"||path5[2]!=="agentRender"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[3]??"")||path5[4]!=="lines"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[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(path5){return path5.length===5&&path5[0]==="messaging"&&path5[1]==="plan"&&path5[2]==="runtimeSetup"&&path5[3]==="envAliases"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[4]??"")}function ownDataPropertyValue3(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function isStockTeamsOpenClawWebhook(root,path5,value){if(path5.length!==6||path5[0]!=="messaging"||path5[1]!=="plan"||path5[2]!=="agentRender"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[3]??"")||path5[4]!=="value"||path5[5]!=="webhook"||!isPlainObject(root)||ownDataPropertyValue3(root,"agent")!=="openclaw"){return false}const messaging=ownDataPropertyValue3(root,"messaging");if(!isPlainObject(messaging))return false;const plan=ownDataPropertyValue3(messaging,"plan");if(!isPlainObject(plan)||ownDataPropertyValue3(plan,"agent")!=="openclaw")return false;const agentRender=ownDataPropertyValue3(plan,"agentRender");if(!Array.isArray(agentRender))return false;const entryIndex=path5[3].slice(1,-1);const entryDescriptor=Object.getOwnPropertyDescriptor(agentRender,entryIndex);const entry=entryDescriptor&&"value"in entryDescriptor?entryDescriptor.value:void 0;if(!isPlainObject(entry))return false;const renderValue=ownDataPropertyValue3(entry,"value");if(!isPlainObject(renderValue)||ownDataPropertyValue3(renderValue,"webhook")!==value){return false}if(ownDataPropertyValue3(entry,"channelId")!=="teams"||ownDataPropertyValue3(entry,"renderId")!=="teams-openclaw-channel"||ownDataPropertyValue3(entry,"hookId")!=="teams-openclaw-channel"||ownDataPropertyValue3(entry,"handler")!=="common.staticOutputs"||ownDataPropertyValue3(entry,"kind")!=="json-fragment"||ownDataPropertyValue3(entry,"agent")!=="openclaw"||ownDataPropertyValue3(entry,"target")!=="openclaw.json"||ownDataPropertyValue3(entry,"path")!=="channels.msteams"||!isPlainObject(value)){return false}const keys=Object.getOwnPropertyNames(value);if(keys.length!==2||!keys.includes("port")||!keys.includes("path"))return false;const port=ownDataPropertyValue3(value,"port");return typeof port==="number"&&Number.isInteger(port)&&port>=1&&port<=65535&&ownDataPropertyValue3(value,"path")==="/api/messages"}function isCanonicalMessagingRuntimeEnvAlias(path5,value){if(!isMessagingRuntimeEnvAliasPath(path5))return false;const envKey=ownDataPropertyValue3(value,"envKey");const match=ownDataPropertyValue3(value,"match");const placeholder=ownDataPropertyValue3(value,"value");return typeof envKey==="string"&&CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&match===`^openshell:resolve:env:(v[0-9]+_)?${envKey}$`&&typeof placeholder==="string"&&messagingCredentialPlaceholderEnvKey(placeholder)===envKey}function isAllowedMessagingRuntimeAliasStringPath(path5,allowedAliasIndexes){return path5.length===6&&path5[0]==="messaging"&&path5[1]==="plan"&&path5[2]==="runtimeSetup"&&path5[3]==="envAliases"&&allowedAliasIndexes.has(path5[4]??"")&&(path5[5]==="match"||path5[5]==="value")}function isMessagingPackagePin(path5,value){return path5.length===6&&path5[0]==="messaging"&&path5[1]==="plan"&&path5[2]==="buildSteps"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path5[3]??"")&&path5[4]==="value"&&path5[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(path5){return path5.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 isLoopbackUrl(value){const hostname=new URL(value).hostname.toLowerCase();return hostname==="localhost"||hostname==="127.0.0.1"||hostname==="::1"||hostname==="[::1]"}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 allowedBuildStepPlaceholders=new Set;const selectedAgent=isPlainObject(root)?ownDataPropertyValue3(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)&&!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 validateDeviceAuth(value){const deviceAuth=requireRecord(value,"agentConfig.deviceAuth");rejectUnknownKeys(deviceAuth,DEVICE_AUTH_KEYS,"agentConfig.deviceAuth");return{disabled:requireBoolean(deviceAuth.disabled,"agentConfig.deviceAuth.disabled"),optOutSource:requireStringEnum(deviceAuth.optOutSource,new Set(["operator","managed-onboard"]),"agentConfig.deviceAuth.optOutSource")}}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),deviceAuth:validateDeviceAuth(config.deviceAuth),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=!isLoopbackUrl(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(!isLoopbackUrl(url)){invalid("Hermes dashboard.url must remain loopback; OpenShell owns the host forward")}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,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")}return{agent,mode,url,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){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 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,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 profile=validateManagedStartupProfile(parsed);if(serializeManagedStartupProfile(profile)!==raw){invalid("payload is not in canonical form")}return profile}function fingerprintManagedStartupProfile(profile){return(0,import_node_crypto2.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){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,path5,value){return Object.freeze({kind:"root-owned-file",legacyInput,path:path5,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.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_DISABLE_DEVICE_AUTH:booleanFlag(profile.agentConfig.deviceAuth.disabled),NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE:profile.agentConfig.deviceAuth.optOutSource,NEMOCLAW_EXTRA_AGENTS_JSON_B64:encodeCanonicalJson(profile.agentConfig.extraAgents),NEMOCLAW_INFERENCE_COMPAT_B64:encodeCanonicalJson(profile.inference.compatibility),NEMOCLAW_INFERENCE_INPUTS:profile.inference.inputModalities.join(","),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_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")}const configurationEnvironment={...commonConfigurationEnvironment(profile),...messagingEnvironment(profile,"hermes"),CHAT_UI_URL:profile.dashboard.url,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};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){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_crypto3=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_crypto3.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_crypto3.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_crypto3.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_crypto3.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_crypto4=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 exactAgent(value){if(typeof value==="string"&&MANAGED_STARTUP_AGENTS.includes(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_crypto4.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_crypto5=require("node:crypto");var import_node_fs2=__toESM(require("node:fs"));var import_node_path2=__toESM(require("node:path"));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 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&&expectedAgent==="hermes"){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(expectedAgent!=="hermes"&&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"));files.add(import_node_path2.default.join(root,".config-hash"));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"));directories.add(import_node_path2.default.join(root,"skills"));break;case"pi":directories.add(import_node_path2.default.join(root,"agent"));files.add(import_node_path2.default.join(root,"agent","models.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_crypto5.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_crypto5.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)} diff --git a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle index d704c0a611a..fa06d9180a5 100644 --- a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle +++ b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle @@ -74,8 +74,8 @@ ${value}`,dataLines++}else isEventPrefix(chunk,searchIndex,firstCharCode)?eventT `,searchIndex)}return chunk.slice(searchIndex)}for(;searchIndex20?`${field.slice(0,20)}\u2026`:field}"`,{type:"unknown-field",field,value,line}));break}}function dispatchEvent(){dataLines>0&&onEvent({id,event:eventType,data}),id=void 0,data="",dataLines=0,eventType=void 0}function reset(options={}){if(options.consume&&pendingFragments.length>0){const incompleteLine=pendingFragments.join("");parseLine(incompleteLine,0,incompleteLine.length)}isFirstChunk=true,id=void 0,data="",dataLines=0,eventType=void 0,pendingFragments.length=0,pendingFragmentsLength=0,terminated=false}return{feed,reset}}function isDataPrefix(chunk,i,firstCharCode){return firstCharCode===100&&chunk.charCodeAt(i+1)===97&&chunk.charCodeAt(i+2)===116&&chunk.charCodeAt(i+3)===97&&chunk.charCodeAt(i+4)===58}function isEventPrefix(chunk,i,firstCharCode){return firstCharCode===101&&chunk.charCodeAt(i+1)===118&&chunk.charCodeAt(i+2)===101&&chunk.charCodeAt(i+3)===110&&chunk.charCodeAt(i+4)===116&&chunk.charCodeAt(i+5)===58}var EventSourceParserStream=class extends TransformStream{constructor({onError,onRetry,onComment,maxBufferSize}={}){let parser;super({start(controller){parser=createParser({onEvent:event=>{controller.enqueue(event)},onError(error2){typeof onError=="function"&&onError(error2),(onError==="terminate"||error2.type==="max-buffer-size-exceeded")&&controller.error(error2)},onRetry,onComment,maxBufferSize})},transform(chunk){parser.feed(chunk)}})}};var DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS={initialReconnectionDelay:1e3,maxReconnectionDelay:3e4,reconnectionDelayGrowFactor:1.5,maxRetries:2};var StreamableHTTPError=class extends Error{constructor(code,message){super(`Streamable HTTP error: ${message}`);this.code=code}};var StreamableHTTPClientTransport=class{constructor(url2,opts){this._hasCompletedAuthFlow=false;this._url=url2;this._resourceMetadataUrl=void 0;this._scope=void 0;this._requestInit=opts?.requestInit;this._authProvider=opts?.authProvider;this._fetch=opts?.fetch;this._fetchWithInit=createFetchWithInit(opts?.fetch,opts?.requestInit);this._sessionId=opts?.sessionId;this._reconnectionOptions=opts?.reconnectionOptions??DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS}async _authThenStart(){if(!this._authProvider){throw new UnauthorizedError("No auth provider")}let result;try{result=await auth(this._authProvider,{serverUrl:this._url,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetchWithInit})}catch(error2){this.onerror?.(error2);throw error2}if(result!=="AUTHORIZED"){throw new UnauthorizedError}return await this._startOrAuthSse({resumptionToken:void 0})}async _commonHeaders(){const headers={};if(this._authProvider){const tokens=await this._authProvider.tokens();if(tokens){headers["Authorization"]=`Bearer ${tokens.access_token}`}}if(this._sessionId){headers["mcp-session-id"]=this._sessionId}if(this._protocolVersion){headers["mcp-protocol-version"]=this._protocolVersion}const extraHeaders=normalizeHeaders(this._requestInit?.headers);return new Headers({...headers,...extraHeaders})}async _startOrAuthSse(options){const{resumptionToken}=options;try{const headers=await this._commonHeaders();headers.set("Accept","text/event-stream");if(resumptionToken){headers.set("last-event-id",resumptionToken)}const response=await(this._fetch??fetch)(this._url,{method:"GET",headers,signal:this._abortController?.signal});if(!response.ok){await response.body?.cancel();if(response.status===401&&this._authProvider){return await this._authThenStart()}if(response.status===405){return}throw new StreamableHTTPError(response.status,`Failed to open SSE stream: ${response.statusText}`)}this._handleSseStream(response.body,options,true)}catch(error2){this.onerror?.(error2);throw error2}}_getNextReconnectionDelay(attempt){if(this._serverRetryMs!==void 0){return this._serverRetryMs}const initialDelay=this._reconnectionOptions.initialReconnectionDelay;const growFactor=this._reconnectionOptions.reconnectionDelayGrowFactor;const maxDelay=this._reconnectionOptions.maxReconnectionDelay;return Math.min(initialDelay*Math.pow(growFactor,attempt),maxDelay)}_scheduleReconnection(options,attemptCount=0){const maxRetries=this._reconnectionOptions.maxRetries;if(attemptCount>=maxRetries){this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));return}const delay=this._getNextReconnectionDelay(attemptCount);this._reconnectionTimeout=setTimeout(()=>{this._startOrAuthSse(options).catch(error2=>{this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error2 instanceof Error?error2.message:String(error2)}`));this._scheduleReconnection(options,attemptCount+1)})},delay)}_handleSseStream(stream,options,isReconnectable){if(!stream){return}const{onresumptiontoken,replayMessageId}=options;let lastEventId;let hasPrimingEvent=false;let receivedResponse=false;const processStream=async()=>{try{const reader=stream.pipeThrough(new TextDecoderStream).pipeThrough(new EventSourceParserStream({onRetry:retryMs=>{this._serverRetryMs=retryMs}})).getReader();while(true){const{value:event,done}=await reader.read();if(done){break}if(event.id){lastEventId=event.id;hasPrimingEvent=true;onresumptiontoken?.(event.id)}if(!event.data){continue}if(!event.event||event.event==="message"){try{const message=JSONRPCMessageSchema.parse(JSON.parse(event.data));if(isJSONRPCResultResponse(message)){receivedResponse=true;if(replayMessageId!==void 0){message.id=replayMessageId}}this.onmessage?.(message)}catch(error2){this.onerror?.(error2)}}}const canResume=isReconnectable||hasPrimingEvent;const needsReconnect=canResume&&!receivedResponse;if(needsReconnect&&this._abortController&&!this._abortController.signal.aborted){this._scheduleReconnection({resumptionToken:lastEventId,onresumptiontoken,replayMessageId},0)}}catch(error2){this.onerror?.(new Error(`SSE stream disconnected: ${error2}`));const canResume=isReconnectable||hasPrimingEvent;const needsReconnect=canResume&&!receivedResponse;if(needsReconnect&&this._abortController&&!this._abortController.signal.aborted){try{this._scheduleReconnection({resumptionToken:lastEventId,onresumptiontoken,replayMessageId},0)}catch(error3){this.onerror?.(new Error(`Failed to reconnect: ${error3 instanceof Error?error3.message:String(error3)}`))}}}};processStream()}async start(){if(this._abortController){throw new Error("StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically.")}this._abortController=new AbortController}async finishAuth(authorizationCode){if(!this._authProvider){throw new UnauthorizedError("No auth provider")}const result=await auth(this._authProvider,{serverUrl:this._url,authorizationCode,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetchWithInit});if(result!=="AUTHORIZED"){throw new UnauthorizedError("Failed to authorize")}}async close(){if(this._reconnectionTimeout){clearTimeout(this._reconnectionTimeout);this._reconnectionTimeout=void 0}this._abortController?.abort();this.onclose?.()}async send(message,options){try{const{resumptionToken,onresumptiontoken}=options||{};if(resumptionToken){this._startOrAuthSse({resumptionToken,replayMessageId:isJSONRPCRequest(message)?message.id:void 0}).catch(err=>this.onerror?.(err));return}const headers=await this._commonHeaders();headers.set("content-type","application/json");headers.set("accept","application/json, text/event-stream");const init={...this._requestInit,method:"POST",headers,body:JSON.stringify(message),signal:this._abortController?.signal};const response=await(this._fetch??fetch)(this._url,init);const sessionId=response.headers.get("mcp-session-id");if(sessionId){this._sessionId=sessionId}if(!response.ok){const text=await response.text().catch(()=>null);if(response.status===401&&this._authProvider){if(this._hasCompletedAuthFlow){throw new StreamableHTTPError(401,"Server returned 401 after successful authentication")}const{resourceMetadataUrl,scope}=extractWWWAuthenticateParams(response);this._resourceMetadataUrl=resourceMetadataUrl;this._scope=scope;const result=await auth(this._authProvider,{serverUrl:this._url,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetchWithInit});if(result!=="AUTHORIZED"){throw new UnauthorizedError}this._hasCompletedAuthFlow=true;return this.send(message)}if(response.status===403&&this._authProvider){const{resourceMetadataUrl,scope,error:error2}=extractWWWAuthenticateParams(response);if(error2==="insufficient_scope"){const wwwAuthHeader=response.headers.get("WWW-Authenticate");if(this._lastUpscopingHeader===wwwAuthHeader){throw new StreamableHTTPError(403,"Server returned 403 after trying upscoping")}if(scope){this._scope=scope}if(resourceMetadataUrl){this._resourceMetadataUrl=resourceMetadataUrl}this._lastUpscopingHeader=wwwAuthHeader??void 0;const result=await auth(this._authProvider,{serverUrl:this._url,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetch});if(result!=="AUTHORIZED"){throw new UnauthorizedError}return this.send(message)}}throw new StreamableHTTPError(response.status,`Error POSTing to endpoint: ${text}`)}this._hasCompletedAuthFlow=false;this._lastUpscopingHeader=void 0;if(response.status===202){await response.body?.cancel();if(isInitializedNotification(message)){this._startOrAuthSse({resumptionToken:void 0}).catch(err=>this.onerror?.(err))}return}const messages=Array.isArray(message)?message:[message];const hasRequests=messages.filter(msg=>"method"in msg&&"id"in msg&&msg.id!==void 0).length>0;const contentType2=response.headers.get("content-type");const responseMediaType=mediaTypeEssence(contentType2);if(hasRequests){if(responseMediaType==="text/event-stream"){this._handleSseStream(response.body,{onresumptiontoken},false)}else if(responseMediaType==="application/json"){const data=await response.json();const responseMessages=Array.isArray(data)?data.map(msg=>JSONRPCMessageSchema.parse(msg)):[JSONRPCMessageSchema.parse(data)];for(const msg of responseMessages){this.onmessage?.(msg)}}else{await response.body?.cancel();throw new StreamableHTTPError(-1,`Unexpected content type: ${contentType2}`)}}else{await response.body?.cancel()}}catch(error2){this.onerror?.(error2);throw error2}}get sessionId(){return this._sessionId}async terminateSession(){if(!this._sessionId){return}try{const headers=await this._commonHeaders();const init={...this._requestInit,method:"DELETE",headers,signal:this._abortController?.signal};const response=await(this._fetch??fetch)(this._url,init);await response.body?.cancel();if(!response.ok&&response.status!==405){throw new StreamableHTTPError(response.status,`Failed to terminate session: ${response.statusText}`)}this._sessionId=void 0}catch(error2){this.onerror?.(error2);throw error2}}setProtocolVersion(version2){this._protocolVersion=version2}get protocolVersion(){return this._protocolVersion}async resumeStream(lastEventId,options){await this._startOrAuthSse({resumptionToken:lastEventId,onresumptiontoken:options?.onresumptiontoken})}};var MCP_TOOL_DISCOVERY_PROTOCOL=1;var MCP_TOOL_DISCOVERY_LIMITS={maxTotalTimeMs:1e4,maxRequestTimeMs:5e3,maxResponseBytes:1048576,maxPages:20,maxTools:500,maxCursorBytes:2048,maxToolNameBytes:256};function parseMcpToolDiscoveryArguments(args){if(args.length!==4||args[0]!=="--url"||args[2]!=="--credential-env"){throw new Error("invalid arguments")}const url2=new URL(args[1]);const credentialEnv=args[3];if(url2.protocol!=="https:"||url2.username!==""||url2.password!==""||url2.hash!==""||!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(credentialEnv)){throw new Error("invalid arguments")}return{url:url2,credentialEnv}}function buildMcpToolDiscoveryAuthorizationPlaceholder(credentialEnv){return`Bearer openshell:resolve:env:${credentialEnv}`}function normalizeMcpToolPage(page){return{tools:page.tools,...page.nextCursor!==void 0?{nextCursor:page.nextCursor}:{}}}var ToolDiscoveryRuntimeError=class extends Error{code;httpStatus;constructor(code,httpStatus){super(code);this.name="ToolDiscoveryRuntimeError";this.code=code;this.httpStatus=httpStatus}};function utf8Bytes(value){return new TextEncoder().encode(value).byteLength}function compareNames(left,right){return leftright?1:0}var UNSAFE_PROTOCOL_TEXT=/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;function validToolName(name){return typeof name==="string"&&name.length>0&&utf8Bytes(name)<=MCP_TOOL_DISCOVERY_LIMITS.maxToolNameBytes&&!UNSAFE_PROTOCOL_TEXT.test(name)}function validateCursor(cursor){return typeof cursor==="string"&&cursor.length>0&&utf8Bytes(cursor)<=MCP_TOOL_DISCOVERY_LIMITS.maxCursorBytes&&!UNSAFE_PROTOCOL_TEXT.test(cursor)}function truncatedResult(tools,detail){const sorted=[...tools].sort(compareNames);return{ok:false,count:sorted.length,tools:sorted,truncated:true,detail}}async function enumerateMcpToolNames(loadPage){const names=[];const seenNames=new Set;const seenCursors=new Set;let cursor;for(let pageNumber=1;pageNumber<=MCP_TOOL_DISCOVERY_LIMITS.maxPages;pageNumber+=1){const page=await loadPage(cursor);if(!page||!Array.isArray(page.tools)){throw new ToolDiscoveryRuntimeError("invalid-response")}for(const tool of page.tools){if(!tool||!validToolName(tool.name)||seenNames.has(tool.name)){throw new ToolDiscoveryRuntimeError("invalid-response")}seenNames.add(tool.name);if(names.lengthMCP_TOOL_DISCOVERY_LIMITS.maxTools){return truncatedResult(names,`tool discovery exceeded the ${MCP_TOOL_DISCOVERY_LIMITS.maxTools}-tool safety limit`)}const sorted=[...names].sort(compareNames);return{ok:true,count:sorted.length,tools:sorted,truncated:false}}if(!validateCursor(nextCursor)||seenCursors.has(nextCursor)){throw new ToolDiscoveryRuntimeError("invalid-response")}seenCursors.add(nextCursor);cursor=nextCursor;if(seenNames.size>=MCP_TOOL_DISCOVERY_LIMITS.maxTools){return truncatedResult(names,`tool discovery reached the ${MCP_TOOL_DISCOVERY_LIMITS.maxTools}-tool safety limit before pagination completed`)}if(pageNumber===MCP_TOOL_DISCOVERY_LIMITS.maxPages){return truncatedResult(names,`tool discovery reached the ${MCP_TOOL_DISCOVERY_LIMITS.maxPages}-page safety limit`)}}throw new ToolDiscoveryRuntimeError("invalid-response")}async function runMcpToolDiscoverySession(session){try{await session.connect();session.publishResult(await enumerateMcpToolNames(session.loadPage))}catch(error2){session.publishResult({ok:false,count:0,tools:[],truncated:false,detail:safeToolDiscoveryErrorDetail(error2)})}finally{if(session.hasSession()){try{await session.terminateSession()}catch{}}try{await session.close()}catch{}}}function combinedSignal(left,right){return left?AbortSignal.any([left,right]):right}function createBoundedMcpFetch(fetchImpl,deadlineSignal){let responseBytes=0;return async(input,init={})=>{let response;try{response=await fetchImpl(input,{...init,redirect:"manual",signal:combinedSignal(init.signal,deadlineSignal)})}catch(error2){if(deadlineSignal.aborted||error2 instanceof Error&&error2.name==="AbortError"){throw new ToolDiscoveryRuntimeError("timeout")}throw error2}if(response.status>=300&&response.status<400){await response.body?.cancel();throw new ToolDiscoveryRuntimeError("redirect")}if(response.status<200||response.status>=300){await response.body?.cancel();throw new ToolDiscoveryRuntimeError("http-error",response.status)}const contentLength=response.headers.get("content-length");if(contentLength!==null&&/^\d+$/u.test(contentLength)){const declaredBytes=Number(contentLength);if(!Number.isSafeInteger(declaredBytes)||responseBytes+declaredBytes>MCP_TOOL_DISCOVERY_LIMITS.maxResponseBytes){await response.body?.cancel();throw new ToolDiscoveryRuntimeError("response-too-large")}}if(!response.body)return response;const reader=response.body.getReader();const boundedBody=new ReadableStream({async pull(controller){try{const{value,done}=await reader.read();if(done){controller.close();return}responseBytes+=value.byteLength;if(responseBytes>MCP_TOOL_DISCOVERY_LIMITS.maxResponseBytes){await reader.cancel();controller.error(new ToolDiscoveryRuntimeError("response-too-large"));return}controller.enqueue(value)}catch(error2){controller.error(error2)}},cancel(reason){return reader.cancel(reason)}});return new Response(boundedBody,{status:response.status,statusText:response.statusText,headers:response.headers})}}function safeToolDiscoveryErrorDetail(error2){if(error2 instanceof ToolDiscoveryRuntimeError){switch(error2.code){case"http-error":return typeof error2.httpStatus==="number"?`MCP endpoint rejected tool discovery (HTTP ${error2.httpStatus})`:"MCP endpoint rejected tool discovery";case"invalid-response":return"MCP endpoint returned an invalid tool-list response";case"redirect":return"MCP endpoint redirect was rejected";case"response-too-large":return`MCP responses exceeded the ${MCP_TOOL_DISCOVERY_LIMITS.maxResponseBytes}-byte safety limit`;case"timeout":return`tool discovery timed out after ${MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs/1e3}s`}}if(error2 instanceof Error){if(error2.name==="AbortError"||/(?:request|maximum total) timeout|timed out/iu.test(error2.message)){return`tool discovery timed out after ${MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs/1e3}s`}}return"MCP tool discovery request failed"}function writeResult(result){process.stdout.write(`${JSON.stringify({protocol:MCP_TOOL_DISCOVERY_PROTOCOL,...result})} -`)}async function main(){let runtimeArguments;try{runtimeArguments=parseMcpToolDiscoveryArguments(process.argv.slice(2))}catch{writeResult({ok:false,count:0,tools:[],truncated:false,detail:"tool discovery received invalid runtime arguments"});return}const deadlineSignal=AbortSignal.timeout(MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs);const boundedFetch=createBoundedMcpFetch(globalThis.fetch,deadlineSignal);const transport=new StreamableHTTPClientTransport(runtimeArguments.url,{fetch:boundedFetch,requestInit:{headers:{authorization:buildMcpToolDiscoveryAuthorizationPlaceholder(runtimeArguments.credentialEnv)},redirect:"manual"},reconnectionOptions:{maxReconnectionDelay:1,initialReconnectionDelay:1,reconnectionDelayGrowFactor:1,maxRetries:0}});const client=new Client({name:"nemoclaw-mcp-tool-discovery",version:"1.0.0"},{capabilities:{}});const requestOptions={signal:deadlineSignal,timeout:MCP_TOOL_DISCOVERY_LIMITS.maxRequestTimeMs,maxTotalTimeout:MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs};await runMcpToolDiscoverySession({connect:()=>client.connect(transport,requestOptions),loadPage:async cursor=>{const page=await client.listTools(cursor?{cursor}:void 0,requestOptions);return normalizeMcpToolPage(page)},hasSession:()=>Boolean(transport.sessionId),terminateSession:()=>transport.terminateSession(),close:()=>client.close(),publishResult:writeResult})}await main(); +${value}`,dataLines++;break;case"id":id=value.includes("\0")?void 0:value;break;case"retry":/^\d+$/.test(value)?onRetry(parseInt(value,10)):onError(new ParseError(`Invalid \`retry\` value: "${value}"`,{type:"invalid-retry",value,line}));break;default:onError(new ParseError(`Unknown field "${field.length>20?`${field.slice(0,20)}\u2026`:field}"`,{type:"unknown-field",field,value,line}));break}}function dispatchEvent(){dataLines>0&&onEvent({id,event:eventType,data}),id=void 0,data="",dataLines=0,eventType=void 0}function reset(options={}){if(options.consume&&pendingFragments.length>0){const incompleteLine=pendingFragments.join("");parseLine(incompleteLine,0,incompleteLine.length)}isFirstChunk=true,id=void 0,data="",dataLines=0,eventType=void 0,pendingFragments.length=0,pendingFragmentsLength=0,terminated=false}return{feed,reset}}function isDataPrefix(chunk,i,firstCharCode){return firstCharCode===100&&chunk.charCodeAt(i+1)===97&&chunk.charCodeAt(i+2)===116&&chunk.charCodeAt(i+3)===97&&chunk.charCodeAt(i+4)===58}function isEventPrefix(chunk,i,firstCharCode){return firstCharCode===101&&chunk.charCodeAt(i+1)===118&&chunk.charCodeAt(i+2)===101&&chunk.charCodeAt(i+3)===110&&chunk.charCodeAt(i+4)===116&&chunk.charCodeAt(i+5)===58}var EventSourceParserStream=class extends TransformStream{constructor({onError,onRetry,onComment,maxBufferSize}={}){let parser;super({start(controller){parser=createParser({onEvent:event=>{controller.enqueue(event)},onError(error2){typeof onError=="function"&&onError(error2),(onError==="terminate"||error2.type==="max-buffer-size-exceeded")&&controller.error(error2)},onRetry,onComment,maxBufferSize})},transform(chunk){parser.feed(chunk)}})}};var DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS={initialReconnectionDelay:1e3,maxReconnectionDelay:3e4,reconnectionDelayGrowFactor:1.5,maxRetries:2};var StreamableHTTPError=class extends Error{constructor(code,message){super(`Streamable HTTP error: ${message}`);this.code=code}};var StreamableHTTPClientTransport=class{constructor(url2,opts){this._hasCompletedAuthFlow=false;this._url=url2;this._resourceMetadataUrl=void 0;this._scope=void 0;this._requestInit=opts?.requestInit;this._authProvider=opts?.authProvider;this._fetch=opts?.fetch;this._fetchWithInit=createFetchWithInit(opts?.fetch,opts?.requestInit);this._sessionId=opts?.sessionId;this._reconnectionOptions=opts?.reconnectionOptions??DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS}async _authThenStart(){if(!this._authProvider){throw new UnauthorizedError("No auth provider")}let result;try{result=await auth(this._authProvider,{serverUrl:this._url,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetchWithInit})}catch(error2){this.onerror?.(error2);throw error2}if(result!=="AUTHORIZED"){throw new UnauthorizedError}return await this._startOrAuthSse({resumptionToken:void 0})}async _commonHeaders(){const headers={};if(this._authProvider){const tokens=await this._authProvider.tokens();if(tokens){headers["Authorization"]=`Bearer ${tokens.access_token}`}}if(this._sessionId){headers["mcp-session-id"]=this._sessionId}if(this._protocolVersion){headers["mcp-protocol-version"]=this._protocolVersion}const extraHeaders=normalizeHeaders(this._requestInit?.headers);return new Headers({...headers,...extraHeaders})}async _startOrAuthSse(options){const{resumptionToken}=options;try{const headers=await this._commonHeaders();headers.set("Accept","text/event-stream");if(resumptionToken){headers.set("last-event-id",resumptionToken)}const response=await(this._fetch??fetch)(this._url,{method:"GET",headers,signal:this._abortController?.signal});if(!response.ok){await response.body?.cancel();if(response.status===401&&this._authProvider){return await this._authThenStart()}if(response.status===405){return}throw new StreamableHTTPError(response.status,`Failed to open SSE stream: ${response.statusText}`)}this._handleSseStream(response.body,options,true)}catch(error2){this.onerror?.(error2);throw error2}}_getNextReconnectionDelay(attempt){if(this._serverRetryMs!==void 0){return this._serverRetryMs}const initialDelay=this._reconnectionOptions.initialReconnectionDelay;const growFactor=this._reconnectionOptions.reconnectionDelayGrowFactor;const maxDelay=this._reconnectionOptions.maxReconnectionDelay;return Math.min(initialDelay*Math.pow(growFactor,attempt),maxDelay)}_scheduleReconnection(options,attemptCount=0){const maxRetries=this._reconnectionOptions.maxRetries;if(attemptCount>=maxRetries){this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));return}const delay=this._getNextReconnectionDelay(attemptCount);this._reconnectionTimeout=setTimeout(()=>{this._startOrAuthSse(options).catch(error2=>{this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error2 instanceof Error?error2.message:String(error2)}`));this._scheduleReconnection(options,attemptCount+1)})},delay)}_handleSseStream(stream,options,isReconnectable){if(!stream){return}const{onresumptiontoken,replayMessageId}=options;let lastEventId;let hasPrimingEvent=false;let receivedResponse=false;const processStream=async()=>{try{const reader=stream.pipeThrough(new TextDecoderStream).pipeThrough(new EventSourceParserStream({onRetry:retryMs=>{this._serverRetryMs=retryMs}})).getReader();while(true){const{value:event,done}=await reader.read();if(done){break}if(event.id){lastEventId=event.id;hasPrimingEvent=true;onresumptiontoken?.(event.id)}if(!event.data){continue}if(!event.event||event.event==="message"){try{const message=JSONRPCMessageSchema.parse(JSON.parse(event.data));if(isJSONRPCResultResponse(message)){receivedResponse=true;if(replayMessageId!==void 0){message.id=replayMessageId}}this.onmessage?.(message)}catch(error2){this.onerror?.(error2)}}}const canResume=isReconnectable||hasPrimingEvent;const needsReconnect=canResume&&!receivedResponse;if(needsReconnect&&this._abortController&&!this._abortController.signal.aborted){this._scheduleReconnection({resumptionToken:lastEventId,onresumptiontoken,replayMessageId},0)}}catch(error2){this.onerror?.(new Error(`SSE stream disconnected: ${error2}`));const canResume=isReconnectable||hasPrimingEvent;const needsReconnect=canResume&&!receivedResponse;if(needsReconnect&&this._abortController&&!this._abortController.signal.aborted){try{this._scheduleReconnection({resumptionToken:lastEventId,onresumptiontoken,replayMessageId},0)}catch(error3){this.onerror?.(new Error(`Failed to reconnect: ${error3 instanceof Error?error3.message:String(error3)}`))}}}};processStream()}async start(){if(this._abortController){throw new Error("StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically.")}this._abortController=new AbortController}async finishAuth(authorizationCode){if(!this._authProvider){throw new UnauthorizedError("No auth provider")}const result=await auth(this._authProvider,{serverUrl:this._url,authorizationCode,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetchWithInit});if(result!=="AUTHORIZED"){throw new UnauthorizedError("Failed to authorize")}}async close(){if(this._reconnectionTimeout){clearTimeout(this._reconnectionTimeout);this._reconnectionTimeout=void 0}this._abortController?.abort();this.onclose?.()}async send(message,options){try{const{resumptionToken,onresumptiontoken}=options||{};if(resumptionToken){this._startOrAuthSse({resumptionToken,replayMessageId:isJSONRPCRequest(message)?message.id:void 0}).catch(err=>this.onerror?.(err));return}const headers=await this._commonHeaders();headers.set("content-type","application/json");headers.set("accept","application/json, text/event-stream");const init={...this._requestInit,method:"POST",headers,body:JSON.stringify(message),signal:this._abortController?.signal};const response=await(this._fetch??fetch)(this._url,init);const sessionId=response.headers.get("mcp-session-id");if(sessionId){this._sessionId=sessionId}if(!response.ok){const text=await response.text().catch(()=>null);if(response.status===401&&this._authProvider){if(this._hasCompletedAuthFlow){throw new StreamableHTTPError(401,"Server returned 401 after successful authentication")}const{resourceMetadataUrl,scope}=extractWWWAuthenticateParams(response);this._resourceMetadataUrl=resourceMetadataUrl;this._scope=scope;const result=await auth(this._authProvider,{serverUrl:this._url,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetchWithInit});if(result!=="AUTHORIZED"){throw new UnauthorizedError}this._hasCompletedAuthFlow=true;return this.send(message)}if(response.status===403&&this._authProvider){const{resourceMetadataUrl,scope,error:error2}=extractWWWAuthenticateParams(response);if(error2==="insufficient_scope"){const wwwAuthHeader=response.headers.get("WWW-Authenticate");if(this._lastUpscopingHeader===wwwAuthHeader){throw new StreamableHTTPError(403,"Server returned 403 after trying upscoping")}if(scope){this._scope=scope}if(resourceMetadataUrl){this._resourceMetadataUrl=resourceMetadataUrl}this._lastUpscopingHeader=wwwAuthHeader??void 0;const result=await auth(this._authProvider,{serverUrl:this._url,resourceMetadataUrl:this._resourceMetadataUrl,scope:this._scope,fetchFn:this._fetch});if(result!=="AUTHORIZED"){throw new UnauthorizedError}return this.send(message)}}throw new StreamableHTTPError(response.status,`Error POSTing to endpoint: ${text}`)}this._hasCompletedAuthFlow=false;this._lastUpscopingHeader=void 0;if(response.status===202){await response.body?.cancel();if(isInitializedNotification(message)){this._startOrAuthSse({resumptionToken:void 0}).catch(err=>this.onerror?.(err))}return}const messages=Array.isArray(message)?message:[message];const hasRequests=messages.filter(msg=>"method"in msg&&"id"in msg&&msg.id!==void 0).length>0;const contentType2=response.headers.get("content-type");const responseMediaType=mediaTypeEssence(contentType2);if(hasRequests){if(responseMediaType==="text/event-stream"){this._handleSseStream(response.body,{onresumptiontoken},false)}else if(responseMediaType==="application/json"){const data=await response.json();const responseMessages=Array.isArray(data)?data.map(msg=>JSONRPCMessageSchema.parse(msg)):[JSONRPCMessageSchema.parse(data)];for(const msg of responseMessages){this.onmessage?.(msg)}}else{await response.body?.cancel();throw new StreamableHTTPError(-1,`Unexpected content type: ${contentType2}`)}}else{await response.body?.cancel()}}catch(error2){this.onerror?.(error2);throw error2}}get sessionId(){return this._sessionId}async terminateSession(){if(!this._sessionId){return}try{const headers=await this._commonHeaders();const init={...this._requestInit,method:"DELETE",headers,signal:this._abortController?.signal};const response=await(this._fetch??fetch)(this._url,init);await response.body?.cancel();if(!response.ok&&response.status!==405){throw new StreamableHTTPError(response.status,`Failed to terminate session: ${response.statusText}`)}this._sessionId=void 0}catch(error2){this.onerror?.(error2);throw error2}}setProtocolVersion(version2){this._protocolVersion=version2}get protocolVersion(){return this._protocolVersion}async resumeStream(lastEventId,options){await this._startOrAuthSse({resumptionToken:lastEventId,onresumptiontoken:options?.onresumptiontoken})}};var MCP_TOOL_DISCOVERY_PROTOCOL=1;var MCP_TOOL_DISCOVERY_LIMITS={maxTotalTimeMs:1e4,maxRequestTimeMs:5e3,maxResponseBytes:1048576,maxPages:20,maxTools:500,maxCursorBytes:2048,maxToolNameBytes:256};function parseMcpToolDiscoveryArguments(args){if(args.length!==4||args[0]!=="--url"||args[2]!=="--credential-env"){throw new Error("invalid arguments")}const url2=new URL(args[1]);const credentialEnv=args[3];if(url2.protocol!=="https:"||url2.username!==""||url2.password!==""||url2.hash!==""||!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(credentialEnv)){throw new Error("invalid arguments")}return{url:url2,credentialEnv}}function buildMcpToolDiscoveryAuthorizationPlaceholder(credentialEnv,runtimeValue){if(!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(credentialEnv)||runtimeValue===void 0){return null}const escapedCredentialEnv=credentialEnv.replace(/[.*+?^${}()|[\]\\]/gu,"\\$&");const placeholderPattern=new RegExp(`^openshell:resolve:env:(?:v[0-9]{1,20}_)?${escapedCredentialEnv}$`,"u");return placeholderPattern.test(runtimeValue)?`Bearer ${runtimeValue}`:null}function normalizeMcpToolPage(page){return{tools:page.tools,...page.nextCursor!==void 0?{nextCursor:page.nextCursor}:{}}}var ToolDiscoveryRuntimeError=class extends Error{code;httpStatus;constructor(code,httpStatus){super(code);this.name="ToolDiscoveryRuntimeError";this.code=code;this.httpStatus=httpStatus}};function utf8Bytes(value){return new TextEncoder().encode(value).byteLength}function compareNames(left,right){return leftright?1:0}var UNSAFE_PROTOCOL_TEXT=/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;function validToolName(name){return typeof name==="string"&&name.length>0&&utf8Bytes(name)<=MCP_TOOL_DISCOVERY_LIMITS.maxToolNameBytes&&!UNSAFE_PROTOCOL_TEXT.test(name)}function validateCursor(cursor){return typeof cursor==="string"&&cursor.length>0&&utf8Bytes(cursor)<=MCP_TOOL_DISCOVERY_LIMITS.maxCursorBytes&&!UNSAFE_PROTOCOL_TEXT.test(cursor)}function truncatedResult(tools,detail){const sorted=[...tools].sort(compareNames);return{ok:false,count:sorted.length,tools:sorted,truncated:true,detail}}async function enumerateMcpToolNames(loadPage){const names=[];const seenNames=new Set;const seenCursors=new Set;let cursor;for(let pageNumber=1;pageNumber<=MCP_TOOL_DISCOVERY_LIMITS.maxPages;pageNumber+=1){const page=await loadPage(cursor);if(!page||!Array.isArray(page.tools)){throw new ToolDiscoveryRuntimeError("invalid-response")}for(const tool of page.tools){if(!tool||!validToolName(tool.name)||seenNames.has(tool.name)){throw new ToolDiscoveryRuntimeError("invalid-response")}seenNames.add(tool.name);if(names.lengthMCP_TOOL_DISCOVERY_LIMITS.maxTools){return truncatedResult(names,`tool discovery exceeded the ${MCP_TOOL_DISCOVERY_LIMITS.maxTools}-tool safety limit`)}const sorted=[...names].sort(compareNames);return{ok:true,count:sorted.length,tools:sorted,truncated:false}}if(!validateCursor(nextCursor)||seenCursors.has(nextCursor)){throw new ToolDiscoveryRuntimeError("invalid-response")}seenCursors.add(nextCursor);cursor=nextCursor;if(seenNames.size>=MCP_TOOL_DISCOVERY_LIMITS.maxTools){return truncatedResult(names,`tool discovery reached the ${MCP_TOOL_DISCOVERY_LIMITS.maxTools}-tool safety limit before pagination completed`)}if(pageNumber===MCP_TOOL_DISCOVERY_LIMITS.maxPages){return truncatedResult(names,`tool discovery reached the ${MCP_TOOL_DISCOVERY_LIMITS.maxPages}-page safety limit`)}}throw new ToolDiscoveryRuntimeError("invalid-response")}async function runMcpToolDiscoverySession(session){try{await session.connect();session.publishResult(await enumerateMcpToolNames(session.loadPage))}catch(error2){session.publishResult({ok:false,count:0,tools:[],truncated:false,detail:safeToolDiscoveryErrorDetail(error2)})}finally{if(session.hasSession()){try{await session.terminateSession()}catch{}}try{await session.close()}catch{}}}function combinedSignal(left,right){return left?AbortSignal.any([left,right]):right}function createBoundedMcpFetch(fetchImpl,deadlineSignal){let responseBytes=0;return async(input,init={})=>{let response;try{response=await fetchImpl(input,{...init,redirect:"manual",signal:combinedSignal(init.signal,deadlineSignal)})}catch(error2){if(deadlineSignal.aborted||error2 instanceof Error&&error2.name==="AbortError"){throw new ToolDiscoveryRuntimeError("timeout")}throw error2}if(response.status>=300&&response.status<400){await response.body?.cancel();throw new ToolDiscoveryRuntimeError("redirect")}if(response.status<200||response.status>=300){await response.body?.cancel();throw new ToolDiscoveryRuntimeError("http-error",response.status)}const contentLength=response.headers.get("content-length");if(contentLength!==null&&/^\d+$/u.test(contentLength)){const declaredBytes=Number(contentLength);if(!Number.isSafeInteger(declaredBytes)||responseBytes+declaredBytes>MCP_TOOL_DISCOVERY_LIMITS.maxResponseBytes){await response.body?.cancel();throw new ToolDiscoveryRuntimeError("response-too-large")}}if(!response.body)return response;const reader=response.body.getReader();const boundedBody=new ReadableStream({async pull(controller){try{const{value,done}=await reader.read();if(done){controller.close();return}responseBytes+=value.byteLength;if(responseBytes>MCP_TOOL_DISCOVERY_LIMITS.maxResponseBytes){await reader.cancel();controller.error(new ToolDiscoveryRuntimeError("response-too-large"));return}controller.enqueue(value)}catch(error2){controller.error(error2)}},cancel(reason){return reader.cancel(reason)}});return new Response(boundedBody,{status:response.status,statusText:response.statusText,headers:response.headers})}}function safeToolDiscoveryErrorDetail(error2){if(error2 instanceof ToolDiscoveryRuntimeError){switch(error2.code){case"http-error":return typeof error2.httpStatus==="number"?`MCP endpoint rejected tool discovery (HTTP ${error2.httpStatus})`:"MCP endpoint rejected tool discovery";case"invalid-response":return"MCP endpoint returned an invalid tool-list response";case"redirect":return"MCP endpoint redirect was rejected";case"response-too-large":return`MCP responses exceeded the ${MCP_TOOL_DISCOVERY_LIMITS.maxResponseBytes}-byte safety limit`;case"timeout":return`tool discovery timed out after ${MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs/1e3}s`}}if(error2 instanceof Error){if(error2.name==="AbortError"||/(?:request|maximum total) timeout|timed out/iu.test(error2.message)){return`tool discovery timed out after ${MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs/1e3}s`}}return"MCP tool discovery request failed"}function writeResult(result){process.stdout.write(`${JSON.stringify({protocol:MCP_TOOL_DISCOVERY_PROTOCOL,...result})} +`)}async function main(){let runtimeArguments;try{runtimeArguments=parseMcpToolDiscoveryArguments(process.argv.slice(2))}catch{writeResult({ok:false,count:0,tools:[],truncated:false,detail:"tool discovery received invalid runtime arguments"});return}const deadlineSignal=AbortSignal.timeout(MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs);const boundedFetch=createBoundedMcpFetch(globalThis.fetch,deadlineSignal);const authorization=buildMcpToolDiscoveryAuthorizationPlaceholder(runtimeArguments.credentialEnv,process.env[runtimeArguments.credentialEnv]);if(!authorization){writeResult({ok:false,count:0,tools:[],truncated:false,detail:"managed MCP credential placeholder is unavailable"});return}const transport=new StreamableHTTPClientTransport(runtimeArguments.url,{fetch:boundedFetch,requestInit:{headers:{authorization},redirect:"manual"},reconnectionOptions:{maxReconnectionDelay:1,initialReconnectionDelay:1,reconnectionDelayGrowFactor:1,maxRetries:0}});const client=new Client({name:"nemoclaw-mcp-tool-discovery",version:"1.0.0"},{capabilities:{}});const requestOptions={signal:deadlineSignal,timeout:MCP_TOOL_DISCOVERY_LIMITS.maxRequestTimeMs,maxTotalTimeout:MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs};await runMcpToolDiscoverySession({connect:()=>client.connect(transport,requestOptions),loadPage:async cursor=>{const page=await client.listTools(cursor?{cursor}:void 0,requestOptions);return normalizeMcpToolPage(page)},hasSession:()=>Boolean(transport.sessionId),terminateSession:()=>transport.terminateSession(),close:()=>client.close(),publishResult:writeResult})}await main(); /*! Bundled license information: content-type/index.js: diff --git a/tools/mcp-tool-discovery-runtime/streamable-http-client.test.ts b/tools/mcp-tool-discovery-runtime/streamable-http-client.test.ts index 1136be00916..4671c84c254 100644 --- a/tools/mcp-tool-discovery-runtime/streamable-http-client.test.ts +++ b/tools/mcp-tool-discovery-runtime/streamable-http-client.test.ts @@ -107,13 +107,18 @@ test("discovers tools from case-variant SSE response media types (#7726)", async await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address() as AddressInfo; const deadlineSignal = AbortSignal.timeout(MCP_TOOL_DISCOVERY_LIMITS.maxTotalTimeMs); + const authorization = buildMcpToolDiscoveryAuthorizationPlaceholder( + "EXAMPLE_MCP_TOKEN", + "openshell:resolve:env:v42_EXAMPLE_MCP_TOKEN", + ); + assert.ok(authorization); const transport = new StreamableHTTPClientTransport( new URL(`http://127.0.0.1:${address.port}/mcp`), { fetch: createBoundedMcpFetch(globalThis.fetch, deadlineSignal), requestInit: { headers: { - authorization: buildMcpToolDiscoveryAuthorizationPlaceholder("EXAMPLE_MCP_TOKEN"), + authorization, }, redirect: "manual", }, @@ -162,7 +167,7 @@ test("discovers tools from case-variant SSE response media types (#7726)", async }); const initialize = observed.find((request) => request.rpcMethod === "initialize"); assert.equal(initialize?.accept, "application/json, text/event-stream"); - assert.equal(initialize?.authorization, "Bearer openshell:resolve:env:EXAMPLE_MCP_TOKEN"); + assert.equal(initialize?.authorization, "Bearer openshell:resolve:env:v42_EXAMPLE_MCP_TOKEN"); const toolsList = observed.find((request) => request.rpcMethod === "tools/list"); assert.equal(toolsList?.sessionId, sessionId); const initialized = observed.find((request) => request.rpcMethod === "notifications/initialized"); diff --git a/tools/mcp-tool-discovery-runtime/tool-discovery-core.ts b/tools/mcp-tool-discovery-runtime/tool-discovery-core.ts index afcd6ddaf3b..05abc36ee8e 100644 --- a/tools/mcp-tool-discovery-runtime/tool-discovery-core.ts +++ b/tools/mcp-tool-discovery-runtime/tool-discovery-core.ts @@ -49,8 +49,19 @@ export function parseMcpToolDiscoveryArguments(args: string[]): McpToolDiscovery return { url, credentialEnv }; } -export function buildMcpToolDiscoveryAuthorizationPlaceholder(credentialEnv: string): string { - return `Bearer openshell:resolve:env:${credentialEnv}`; +export function buildMcpToolDiscoveryAuthorizationPlaceholder( + credentialEnv: string, + runtimeValue: string | undefined, +): string | null { + if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(credentialEnv) || runtimeValue === undefined) { + return null; + } + const escapedCredentialEnv = credentialEnv.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); + const placeholderPattern = new RegExp( + `^openshell:resolve:env:(?:v[0-9]{1,20}_)?${escapedCredentialEnv}$`, + "u", + ); + return placeholderPattern.test(runtimeValue) ? `Bearer ${runtimeValue}` : null; } export type McpToolPageLoader = (cursor?: string) => Promise;