From 2b97e0712ee9e9f515568e7a99fef3ea41f473d5 Mon Sep 17 00:00:00 2001 From: Yimo Jiang Date: Sat, 12 Sep 2026 09:16:10 +0000 Subject: [PATCH 01/12] fix(onboard): bind CHAT_UI_URL dashboard port by loopback interface and persist external URL When CHAT_UI_URL points at an external HTTPS reverse proxy bound only to an external interface, onboard fell back to a different dashboard port and never recorded the browser-facing external URL, so status, dashboard-url, and list --json reported only http://127.0.0.1:/. Two facets of the same interface-blindness are fixed: - The dashboard-port probe (isPortBoundOnHost) now decides loopback-bind availability from the 127.0.0.1 bind probe and only treats loopback/wildcard lsof hits as blocking; an external-interface-only listener no longer forces a fallback. docker-proxy / 0.0.0.0 detection (#3260) is preserved, and an operator-opted 0.0.0.0 remote bind still counts every interface (#3259). - The OpenShell forward-ownership probe (isForwardServiceListenerOwner) is made interface-specific the same way, so a loopback forward is recognized as sole owner even when the port number is also bound on an external interface. The resolved external dashboard URL (host+scheme with the effective port) is now persisted in the sandbox registry when CHAT_UI_URL supplies an external origin, across the fresh-create and reuse/resume paths, and surfaced by status, dashboard-url, and list (text + JSON), falling back to the loopback form only when no external origin was configured. A single shared predicate validates the URL on both write and read so a persisted value can always be read back. Fixes #11439 Signed-off-by: Yimo Jiang --- src/commands/sandbox/dashboard-url.ts | 4 +- .../openshell/forward-service.test.ts | 110 +++++++++++++++++- src/lib/adapters/openshell/forward-service.ts | 108 ++++++++++++++--- src/lib/dashboard-url-command.test.ts | 46 ++++++++ src/lib/dashboard-url-command.ts | 25 +++- src/lib/dashboard/url.test.ts | 74 +++++++++++- src/lib/dashboard/url.ts | 75 ++++++++++++ src/lib/inventory/index.ts | 33 +++++- .../created-sandbox-finalization.test.ts | 2 + .../onboard/created-sandbox-finalization.ts | 5 + src/lib/onboard/dashboard-port.test.ts | 50 ++++++++ src/lib/onboard/dashboard-port.ts | 79 +++++++++++-- src/lib/onboard/dashboard.ts | 8 +- src/lib/onboard/sandbox-registration.ts | 9 ++ src/lib/onboard/sandbox-reuse.test.ts | 42 +++++++ src/lib/onboard/sandbox-reuse.ts | 14 +++ src/lib/state/gateway-registry.test.ts | 57 +++++++++ src/lib/state/gateway-registry.ts | 12 ++ src/lib/state/registry-normalization.test.ts | 34 ++++++ src/lib/state/registry.ts | 1 + src/lib/state/registry/types.ts | 7 ++ 21 files changed, 758 insertions(+), 37 deletions(-) diff --git a/src/commands/sandbox/dashboard-url.ts b/src/commands/sandbox/dashboard-url.ts index 9e03ec61564..52a121ed097 100644 --- a/src/commands/sandbox/dashboard-url.ts +++ b/src/commands/sandbox/dashboard-url.ts @@ -9,7 +9,9 @@ import type { SandboxEntry } from "../../lib/state/registry"; type DashboardUrlRuntimeBridge = { fetchGatewayAuthTokenFromSandbox: (sandboxName: string) => string | null; - getSandbox: (sandboxName: string) => Pick | null; + getSandbox: ( + sandboxName: string, + ) => Pick | null; getAccessUrl?: (port: number) => string | null; }; diff --git a/src/lib/adapters/openshell/forward-service.test.ts b/src/lib/adapters/openshell/forward-service.test.ts index 95c4bbd11c5..cb8183699fb 100644 --- a/src/lib/adapters/openshell/forward-service.test.ts +++ b/src/lib/adapters/openshell/forward-service.test.ts @@ -77,14 +77,49 @@ function createLinuxOwnerFixture(actualExecutable?: string) { return { procRoot, target: { ...target, executable } }; } +// A loopback forward (127.0.0.1 on 0x4965 = 18789) whose port is also bound on +// an external interface by a different process (a reverse proxy). The external +// entry's inode belongs to pid 9876; ownership must ignore it and count only +// the loopback owner pid 4321 (#11439). +function createLinuxOwnerFixtureWithExternalCoListener() { + const root = mkdtempSync(path.join(os.tmpdir(), "nemoclaw-forward-owner-ext-")); + temporaryDirectories.push(root); + const procRoot = path.join(root, "proc"); + const binRoot = path.join(root, "bin"); + mkdirSync(path.join(procRoot, "net"), { recursive: true }); + mkdirSync(path.join(procRoot, "4321", "fd"), { recursive: true }); + mkdirSync(path.join(procRoot, "9876", "fd"), { recursive: true }); + mkdirSync(binRoot); + const executable = path.join(binRoot, "openshell"); + writeFileSync(executable, ""); + writeFileSync( + path.join(procRoot, "net", "tcp"), + // Loopback listener (0100007F) owned by the forward, plus an + // external-interface listener (0F90630A) owned by the reverse proxy. + " 0: 0100007F:4965 00000000:0000 0A 00000000:00000000 00:00000000 00000000 998 0 12345 1\n" + + " 1: 0F90630A:4965 00000000:0000 0A 00000000:00000000 00:00000000 00000000 998 0 55555 1\n", + ); + writeFileSync(path.join(procRoot, "net", "tcp6"), ""); + symlinkSync("socket:[12345]", path.join(procRoot, "4321", "fd", "7")); + symlinkSync("socket:[55555]", path.join(procRoot, "9876", "fd", "8")); + symlinkSync(executable, path.join(procRoot, "4321", "exe")); + return { procRoot, target: { ...target, executable } }; +} + +// Loopback forwards probe with `lsof -FpPn`, pairing each pid with its bind +// endpoint so external-interface-only listeners are excluded (#11439). +function loopbackListenerField(pid: string, port = 18_789): string { + return `p${pid}\nPTCP\nn127.0.0.1:${port}\n`; +} + function darwinOwnerProbe( commandLine: string, - finalListener = "4321\n", + finalListener = loopbackListenerField("4321"), executable = process.execPath, ) { return vi .fn() - .mockReturnValueOnce({ status: 0, stdout: "4321\n" }) + .mockReturnValueOnce({ status: 0, stdout: loopbackListenerField("4321") }) .mockReturnValueOnce({ status: 0, stdout: `${executable}\n/mach_kernel\n` }) .mockReturnValueOnce({ status: 0, stdout: commandLine }) .mockReturnValueOnce({ status: 0, stdout: finalListener }); @@ -255,7 +290,10 @@ describe("OpenShell forward service", () => { it("rejects a foreign Darwin executable even when it maps the trusted binary", () => { const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); const responses = new Map([ - [JSON.stringify(["lsof", "-ti4TCP:18789", "-sTCP:LISTEN"]), { status: 0, stdout: "4321\n" }], + [ + JSON.stringify(["lsof", "-i4TCP:18789", "-sTCP:LISTEN", "-P", "-n", "-FpPn"]), + { status: 0, stdout: loopbackListenerField("4321") }, + ], [ JSON.stringify(["lsof", "-a", "-p", "4321", "-d", "txt", "-Fn"]), { @@ -299,7 +337,7 @@ describe("OpenShell forward service", () => { it("rejects ambiguous or changing listener ownership", () => { const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - const probe = darwinOwnerProbe(`${expected}\n`, "9876\n"); + const probe = darwinOwnerProbe(`${expected}\n`, loopbackListenerField("9876")); expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); }); @@ -312,7 +350,7 @@ describe("OpenShell forward service", () => { const psTimeout = vi .fn() - .mockReturnValueOnce({ status: 0, stdout: "4321\n" }) + .mockReturnValueOnce({ status: 0, stdout: loopbackListenerField("4321") }) .mockReturnValueOnce({ status: 0, stdout: `${process.execPath}\n/mach_kernel\n` }) .mockReturnValueOnce({ status: null, stdout: "" }); expect( @@ -341,10 +379,70 @@ describe("OpenShell forward service", () => { }), ).toBe(true); expect(probe).toHaveBeenCalledTimes(3); - expect(probe).toHaveBeenCalledWith("lsof", ["-ti4TCP:18789", "-sTCP:LISTEN"]); + expect(probe).toHaveBeenCalledWith("lsof", [ + "-i4TCP:18789", + "-sTCP:LISTEN", + "-P", + "-n", + "-FpPn", + ]); expect(probe).toHaveBeenCalledWith("ps", ["-ww", "-p", "4321", "-o", "args="]); }); + it("ignores an external-interface-only co-listener for a loopback forward (#11439)", () => { + const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); + // Field-mode lsof reports the reverse proxy on an external interface plus + // NemoClaw's own loopback forward. Only the loopback pid may count. + const listenerField = `p3529439\nPTCP\nn10.63.144.115:18789\n` + loopbackListenerField("4321"); + const probe = vi + .fn() + .mockReturnValueOnce({ status: 0, stdout: listenerField }) + .mockReturnValueOnce({ status: 0, stdout: `${ownerTarget.executable}\n/mach_kernel\n` }) + .mockReturnValueOnce({ status: 0, stdout: `${expected}\n` }) + .mockReturnValueOnce({ status: 0, stdout: listenerField }); + + expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(true); + }); + + it("still rejects a genuine second loopback co-listener for a loopback forward", () => { + const listenerField = loopbackListenerField("4321") + loopbackListenerField("9999"); + const probe = vi.fn().mockReturnValue({ status: 0, stdout: listenerField }); + + expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); + }); + + it("counts every interface for a remote-exposed (0.0.0.0) forward", () => { + const remoteTarget: ForwardServiceTarget = { ...ownerTarget, localHost: "0.0.0.0" }; + // A 0.0.0.0 forward must keep the interface-agnostic `-ti` count so any + // co-listener still blocks ownership. + const probe = vi.fn().mockReturnValue({ status: 0, stdout: "4321\n3529439\n" }); + + expect(isForwardServiceListenerOwner(remoteTarget, { platform: "darwin", probe })).toBe(false); + expect(probe).toHaveBeenCalledWith("lsof", ["-ti4TCP:18789", "-sTCP:LISTEN"]); + }); + + it("ignores an external-interface-only /proc listener for a loopback forward (#11439)", () => { + const fixture = createLinuxOwnerFixtureWithExternalCoListener(); + const expected = [fixture.target.executable, ...buildForwardServiceArgs(fixture.target)].join( + " ", + ); + const responses = { + lsof: { status: null, stdout: "" }, + ps: { status: 0, stdout: `${expected}\n` }, + }; + const probe = vi.fn( + (executable: string) => responses[executable as keyof typeof responses] ?? responses.lsof, + ); + + expect( + isForwardServiceListenerOwner(fixture.target, { + platform: "linux", + probe, + procRoot: fixture.procRoot, + }), + ).toBe(true); + }); + it("rejects spoofed arguments when the Linux executable is different", () => { const fixture = createLinuxOwnerFixture("python3"); const expected = [fixture.target.executable, ...buildForwardServiceArgs(fixture.target)].join( diff --git a/src/lib/adapters/openshell/forward-service.ts b/src/lib/adapters/openshell/forward-service.ts index c6d48b9d160..723b01afe30 100644 --- a/src/lib/adapters/openshell/forward-service.ts +++ b/src/lib/adapters/openshell/forward-service.ts @@ -218,21 +218,68 @@ function captureProcess(executable: string, args: readonly string[]) { return { status: result.status, stdout: result.stdout ?? "" }; } -function lsofListenerPids(port: number, probe: ForwardServiceOwnerProbe): string[] | null { - const result = probe("lsof", [`-ti4TCP:${String(port)}`, "-sTCP:LISTEN"]); +/** + * A loopback forward binds `127.0.0.1`, so only loopback and wildcard listeners + * actually contend for that socket. An `lsof -n` NAME reporting a listener bound + * solely to an external interface (e.g. a TLS reverse proxy fronting + * `CHAT_UI_URL` on the same port number) does not, and must not count toward + * forward ownership on the loopback bind (#11439). Wildcard binds (`*`, + * `0.0.0.0`) include loopback and always count; docker-proxy / loopback + * detection (#3260) is preserved. When the forward itself binds `0.0.0.0` + * (operator-opted remote exposure), every listener on the port contends, so no + * filtering applies. + */ +function lsofNameContendsWithLoopback(name: string): boolean { + // NAME is the bind endpoint, e.g. "127.0.0.1:18789", "*:18789", or + // "10.0.0.5:18789". Strip the trailing ":port" (IPv4 only here). + const address = name.replace(/:\d+$/u, ""); + if (address === "*" || address === "0.0.0.0") return true; + return address.startsWith("127."); +} + +function lsofListenerPids( + port: number, + probe: ForwardServiceOwnerProbe, + loopbackOnly: boolean, +): string[] | null { + if (!loopbackOnly) { + const result = probe("lsof", [`-ti4TCP:${String(port)}`, "-sTCP:LISTEN"]); + if (result.status === null) return null; + if (result.status !== 0) return []; + return [ + ...new Set( + result.stdout + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean), + ), + ]; + } + // Field-mode output pairs each PID (`p`) with the bind endpoint + // (`n:`) so external-interface-only listeners can be excluded + // before counting owners. + const result = probe("lsof", [`-i4TCP:${String(port)}`, "-sTCP:LISTEN", "-P", "-n", "-FpPn"]); if (result.status === null) return null; if (result.status !== 0) return []; - return [ - ...new Set( - result.stdout - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean), - ), - ]; + const pids = new Set(); + let currentPid: string | null = null; + for (const rawLine of result.stdout.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (line.startsWith("p")) { + currentPid = /^p[1-9]\d*$/u.test(line) ? line.slice(1) : null; + } else if (line.startsWith("n") && currentPid !== null) { + if (lsofNameContendsWithLoopback(line.slice(1))) pids.add(currentPid); + } + } + return [...pids]; } -function linuxListenerPids(port: number, procRoot: string, workLimit: number): string[] { +function linuxListenerPids( + port: number, + procRoot: string, + workLimit: number, + loopbackOnly: boolean, +): string[] { if (!Number.isSafeInteger(workLimit) || workLimit < 1) return []; const portSuffix = `:${port.toString(16).padStart(4, "0").toUpperCase()}`; const socketInodes = new Set(); @@ -244,6 +291,17 @@ function linuxListenerPids(port: number, procRoot: string, workLimit: number): s fields[1]?.toUpperCase().endsWith(portSuffix) && /^\d+$/u.test(fields[9] ?? "") ) { + // Local address is HEXIP:HEXPORT (little-endian IP). A loopback forward + // only contends with wildcard (`00000000`) or loopback (`7F......`) + // binds; an external-interface-only listener on the same port is + // ignored (#11439). + if (loopbackOnly) { + const hexIp = (fields[1] ?? "").split(":")[0]?.toUpperCase() ?? ""; + const isWildcard = hexIp === "00000000"; + // 127.0.0.0/8 → least-significant byte 0x7F in little-endian order. + const isLoopback = hexIp.length === 8 && hexIp.endsWith("7F"); + if (!isWildcard && !isLoopback) continue; + } socketInodes.add(fields[9]); } } @@ -284,10 +342,11 @@ function listenerPids( procRoot: string, procWorkLimit: number, probe: ForwardServiceOwnerProbe, + loopbackOnly: boolean, ): string[] { - const lsof = lsofListenerPids(port, probe); + const lsof = lsofListenerPids(port, probe, loopbackOnly); if (lsof !== null || platform !== "linux") return lsof ?? []; - return linuxListenerPids(port, procRoot, procWorkLimit); + return linuxListenerPids(port, procRoot, procWorkLimit, loopbackOnly); } function executableMatches(actualExecutable: string, expectedExecutable: string): boolean { @@ -330,7 +389,19 @@ export function isForwardServiceListenerOwner( const probe = options.probe ?? captureProcess; const procRoot = options.procRoot ?? "/proc"; const procWorkLimit = options.procWorkLimit ?? LINUX_PROC_WORK_LIMIT; - const before = listenerPids(target.localPort, platform, procRoot, procWorkLimit, probe); + // A loopback forward only owns the loopback socket; an external-interface-only + // listener on the same port number (e.g. a reverse proxy fronting CHAT_UI_URL) + // must not defeat ownership. A remote-exposed (`0.0.0.0`) forward keeps the + // interface-agnostic count so any co-listener still blocks ownership (#11439). + const loopbackOnly = target.localHost === "127.0.0.1"; + const before = listenerPids( + target.localPort, + platform, + procRoot, + procWorkLimit, + probe, + loopbackOnly, + ); const [pid] = before; if (before.length !== 1 || pid === undefined || !/^[1-9]\d*$/u.test(pid)) return false; if (!processExecutableMatches(pid, target, platform, procRoot, probe)) return false; @@ -338,7 +409,14 @@ export function isForwardServiceListenerOwner( if (commandLine.status !== 0) return false; const expected = [target.executable, ...buildForwardServiceArgs(target)].join(" "); if (commandLine.stdout.trim() !== expected) return false; - const after = listenerPids(target.localPort, platform, procRoot, procWorkLimit, probe); + const after = listenerPids( + target.localPort, + platform, + procRoot, + procWorkLimit, + probe, + loopbackOnly, + ); return after.length === 1 && after[0] === pid; } diff --git a/src/lib/dashboard-url-command.test.ts b/src/lib/dashboard-url-command.test.ts index 0f1d818afb4..8a9ef2fb76f 100644 --- a/src/lib/dashboard-url-command.test.ts +++ b/src/lib/dashboard-url-command.test.ts @@ -183,6 +183,52 @@ describe("dashboard-url command helpers", () => { expect(sinks.err).toEqual([]); }); + it("prints the persisted external dashboard URL for a session-auth agent (#11439)", () => { + const sinks = makeSinks(); + + runDashboardUrlCommand( + "hermes", + { quiet: true }, + { + fetchToken: () => null, + getSandbox: () => ({ + agent: "hermes", + dashboardPort: 18789, + dashboardExternalUrl: "https://dash.example.com:18789", + }), + getAgentDashboardAuth: () => "session", + // A host access URL must not override the persisted external origin. + getAccessUrl: () => "http://172.22.1.1:18789", + log: sinks.log, + error: sinks.error, + }, + ); + + expect(sinks.out).toEqual(["https://dash.example.com:18789/"]); + }); + + it("embeds the token in the persisted external dashboard URL for token-auth agents (#11439)", () => { + const sinks = makeSinks(); + + runDashboardUrlCommand( + "agent-ui", + { quiet: true }, + { + fetchToken: () => "agent-token", + getSandbox: () => ({ + agent: "agent-ui", + dashboardPort: 19001, + dashboardExternalUrl: "https://dash.example.com:19001", + }), + getAgentDashboardAuth: () => "url_token", + log: sinks.log, + error: sinks.error, + }, + ); + + expect(sinks.out).toEqual(["https://dash.example.com:19001/#token=agent-token"]); + }); + it("fetches a token for non-OpenClaw agents with token-auth dashboards", () => { const sinks = makeSinks(); const fetchToken = vi.fn(() => "agent-token"); diff --git a/src/lib/dashboard-url-command.ts b/src/lib/dashboard-url-command.ts index fe69af22425..2edb0c8f0d8 100644 --- a/src/lib/dashboard-url-command.ts +++ b/src/lib/dashboard-url-command.ts @@ -17,7 +17,9 @@ export interface DashboardUrlCommandDeps { /** Pull gateway.auth.token from the sandbox config (host-side helper). */ fetchToken: (sandboxName: string) => string | null; /** Read sandbox metadata such as agent name and recorded dashboard port. */ - getSandbox?: (sandboxName: string) => Pick | null; + getSandbox?: ( + sandboxName: string, + ) => Pick | null; /** Resolve the browser-facing dashboard base URL for this host, when known. */ getAccessUrl?: (port: number) => string | null; /** Resolve a registered agent's dashboard auth contract. */ @@ -65,6 +67,21 @@ function resolveDashboardPort(sandbox: Pick | nul : DASHBOARD_PORT; } +/** + * Prefer the persisted external dashboard URL (the browser-facing HTTPS reverse + * proxy origin resolved from `CHAT_UI_URL` at onboard time) over any + * host-derived access URL, falling back to the loopback form only when no + * external origin was configured (#11439). + */ +function resolveDashboardBaseUrl( + sandbox: Pick | null, + accessUrl: string | null, +): string | null { + const external = sandbox?.dashboardExternalUrl; + if (typeof external === "string" && external.length > 0) return external; + return accessUrl; +} + export function buildDashboardUrl( token: string, port = DASHBOARD_PORT, @@ -141,7 +158,7 @@ export function runDashboardUrlCommand( for (const line of hint) log(line); }; - let sandbox: Pick | null = null; + let sandbox: Pick | null = null; if (deps.getSandbox) { try { sandbox = deps.getSandbox(sandboxName); @@ -170,7 +187,7 @@ export function runDashboardUrlCommand( } if (dashboardAuth === "session" || dashboardAuth === "none") { const port = resolveDashboardPort(sandbox); - const accessUrl = deps.getAccessUrl?.(port) ?? null; + const accessUrl = resolveDashboardBaseUrl(sandbox, deps.getAccessUrl?.(port) ?? null); const url = buildPlainDashboardUrl(port, accessUrl ?? undefined); if (options.quiet) { log(url); @@ -197,7 +214,7 @@ export function runDashboardUrlCommand( } const port = resolveDashboardPort(sandbox); - const accessUrl = deps.getAccessUrl?.(port) ?? null; + const accessUrl = resolveDashboardBaseUrl(sandbox, deps.getAccessUrl?.(port) ?? null); const url = buildDashboardUrl(token, port, accessUrl ?? undefined); if (options.quiet) { log(url); diff --git a/src/lib/dashboard/url.test.ts b/src/lib/dashboard/url.test.ts index e68ecb8a795..37fd06fd9ea 100644 --- a/src/lib/dashboard/url.test.ts +++ b/src/lib/dashboard/url.test.ts @@ -3,7 +3,12 @@ import { describe, expect, it } from "vitest"; -import { rebindLoopbackDashboardUrlPort } from "./url"; +import { + isValidDashboardExternalUrl, + rebindLoopbackDashboardUrlPort, + resolveExternalDashboardUrl, + resolveExternalDashboardUrlForPort, +} from "./url"; describe("rebindLoopbackDashboardUrlPort", () => { it.each([ @@ -14,3 +19,70 @@ describe("rebindLoopbackDashboardUrlPort", () => { expect(rebindLoopbackDashboardUrlPort(input, 29_443)).toBe(expected); }); }); + +describe("resolveExternalDashboardUrl (#11439)", () => { + it("returns a genuine external origin, trimming a trailing slash", () => { + expect(resolveExternalDashboardUrl("https://dash.example.com:18789/")).toBe( + "https://dash.example.com:18789", + ); + expect(resolveExternalDashboardUrl("https://dash.example.com:18789")).toBe( + "https://dash.example.com:18789", + ); + }); + + it("returns null for loopback dashboard URLs (loopback reported from dashboardPort)", () => { + expect(resolveExternalDashboardUrl("http://127.0.0.1:18789")).toBeNull(); + expect(resolveExternalDashboardUrl("http://localhost:18789/")).toBeNull(); + }); + + it("returns null for empty or malformed input", () => { + expect(resolveExternalDashboardUrl(null)).toBeNull(); + expect(resolveExternalDashboardUrl(undefined)).toBeNull(); + expect(resolveExternalDashboardUrl("")).toBeNull(); + expect(resolveExternalDashboardUrl("not a url")).toBeNull(); + }); + + it("returns null for non-http(s), credentialed, or over-long origins so a persisted value can be read back", () => { + // These would otherwise be rejected at registry read time and brick list/status. + expect(resolveExternalDashboardUrl("ws://proxy.example.com:18789")).toBeNull(); + expect(resolveExternalDashboardUrl("ftp://proxy.example.com:18789")).toBeNull(); + expect(resolveExternalDashboardUrl("https://user:pass@dash.example.com:18789")).toBeNull(); + expect(resolveExternalDashboardUrl(`https://dash.example.com/${"a".repeat(3000)}`)).toBeNull(); + }); +}); + +describe("isValidDashboardExternalUrl shared write/read predicate (#11439)", () => { + it("accepts absolute http(s) origins without credentials", () => { + expect(isValidDashboardExternalUrl("https://dash.example.com:18789")).toBe(true); + expect(isValidDashboardExternalUrl("http://dash.example.com/path")).toBe(true); + }); + + it("rejects non-http(s), credentialed, control-char, and over-long values", () => { + expect(isValidDashboardExternalUrl("ws://dash.example.com:18789")).toBe(false); + expect(isValidDashboardExternalUrl("https://user:pass@dash.example.com")).toBe(false); + expect(isValidDashboardExternalUrl("https://dash.example.com/\u0000")).toBe(false); + expect(isValidDashboardExternalUrl(`https://dash.example.com/${"a".repeat(3000)}`)).toBe(false); + expect(isValidDashboardExternalUrl("")).toBe(false); + expect(isValidDashboardExternalUrl("dash.example.com:18789")).toBe(false); + }); +}); + +describe("resolveExternalDashboardUrlForPort (#11439)", () => { + it("rebinds the CHAT_UI_URL origin to the effective dashboard port", () => { + expect(resolveExternalDashboardUrlForPort("https://dash.example.com:18789", 18790)).toBe( + "https://dash.example.com:18790", + ); + }); + + it("adds a scheme to a bare host:port origin before rebinding", () => { + expect(resolveExternalDashboardUrlForPort("dash.example.com:18789", 18790)).toBe( + "http://dash.example.com:18790", + ); + }); + + it("returns null for a loopback or missing origin", () => { + expect(resolveExternalDashboardUrlForPort("http://127.0.0.1:18789", 18790)).toBeNull(); + expect(resolveExternalDashboardUrlForPort(null, 18790)).toBeNull(); + expect(resolveExternalDashboardUrlForPort("", 18790)).toBeNull(); + }); +}); diff --git a/src/lib/dashboard/url.ts b/src/lib/dashboard/url.ts index e6f36dcaf66..39b1792c74b 100644 --- a/src/lib/dashboard/url.ts +++ b/src/lib/dashboard/url.ts @@ -15,3 +15,78 @@ export function rebindLoopbackDashboardUrlPort(value: string, port: number): str parsed.port = String(port); return parsed.toString(); } + +const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/u; + +/** + * A persistable external dashboard URL must be an absolute http(s) URL with a + * host and no embedded credentials, matching what onboarding derives from + * `CHAT_UI_URL`. This is the single source of truth shared by the writer + * (`resolveExternalDashboardUrl`) and the registry read-side validator so a + * value that persists can always be read back (#11439). Userinfo is rejected so + * an operator-facing address is never a secret; control characters and + * unbounded length are rejected as registry-hardening. + */ +export function isValidDashboardExternalUrl(value: string): boolean { + if (value.length === 0 || value.length > 2048 || CONTROL_CHARACTER.test(value)) return false; + let parsed: URL; + try { + parsed = new URL(value); + } catch { + return false; + } + return ( + (parsed.protocol === "http:" || parsed.protocol === "https:") && + parsed.hostname.length > 0 && + parsed.username === "" && + parsed.password === "" + ); +} + +/** + * Resolve the external dashboard URL to persist from the operator's + * `CHAT_UI_URL`, rebinding its port to the effective dashboard port. Returns + * null when no external origin is configured, the origin is loopback, or the + * value is not a valid persistable external origin. Shared by the fresh-create, + * reuse/resume, and OpenClaw-forward persistence paths so all record the same + * value (#11439). + */ +export function resolveExternalDashboardUrlForPort( + chatUiUrlEnv: string | null | undefined, + effectivePort: number, +): string | null { + if (!chatUiUrlEnv) return null; + const normalized = chatUiUrlEnv.includes("://") ? chatUiUrlEnv : `http://${chatUiUrlEnv}`; + let rebound: string; + try { + const parsed = new URL(normalized); + parsed.port = String(effectivePort); + rebound = parsed.toString(); + } catch { + return null; + } + return resolveExternalDashboardUrl(rebound); +} + +/** + * Return the browser-facing external dashboard URL to persist for a sandbox, or + * null when the resolved dashboard URL is a plain loopback address or is not a + * valid persistable external origin. A loopback URL adds nothing over the + * persisted `dashboardPort`, so only a genuine external origin (e.g. the HTTPS + * reverse proxy behind `CHAT_UI_URL`) is worth recording so `status`, + * `dashboard-url`, and `list` can report it later (#11439). The value is + * validated with the same predicate the registry read-side enforces, so a + * persisted value can always be read back. Malformed or non-http(s) input + * yields null. + */ +export function resolveExternalDashboardUrl(chatUiUrl: string | null | undefined): string | null { + if (!chatUiUrl) return null; + const normalized = chatUiUrl.replace(/\/$/, ""); + if (!isValidDashboardExternalUrl(normalized)) return null; + try { + if (isLoopbackDashboardUrl(normalized)) return null; + } catch { + return null; + } + return normalized; +} diff --git a/src/lib/inventory/index.ts b/src/lib/inventory/index.ts index 1828177d56f..68aefd06b83 100644 --- a/src/lib/inventory/index.ts +++ b/src/lib/inventory/index.ts @@ -30,6 +30,8 @@ export interface SandboxEntry { messaging?: SandboxMessagingState | null; agent?: string | null; dashboardPort?: number | null; + /** Browser-facing external dashboard URL persisted from CHAT_UI_URL (#11439). */ + dashboardExternalUrl?: string | null; // Passthrough of the durable registry reservation marker so list and status // hide registrations that have not committed their lifecycle yet. pendingRouteReservation?: true; @@ -106,6 +108,8 @@ export interface SandboxInventoryRow { policies: string[]; agent: string; dashboardPort?: number | null; + /** Browser-facing external dashboard URL when CHAT_UI_URL set an external origin (#11439). */ + dashboardExternalUrl?: string | null; isDefault: boolean; activeSessionCount: number | null; // #5714: row recovered display-only from the live gateway. Its agent/GPU/ @@ -198,6 +202,8 @@ export interface StatusSandboxRow { agent: string; phase?: "pending" | "configuring" | "active"; dashboardPort?: number | null; + /** Browser-facing external dashboard URL when CHAT_UI_URL set an external origin (#11439). */ + dashboardExternalUrl?: string | null; isDefault: boolean; } @@ -226,6 +232,19 @@ function safeStatusString(value: string | null | undefined): string | null { return redactFull(value); } +/** + * Resolve the persisted browser-facing external dashboard URL for a sandbox, + * or null when none was configured (loopback-only dashboards report the + * `dashboardPort`-derived loopback form instead). Redaction is not applied: the + * external URL is an operator-facing address, not a secret (#11439). + */ +function resolveDashboardExternalUrl( + sandbox: Pick, +): string | null { + const external = sandbox.dashboardExternalUrl; + return typeof external === "string" && external.length > 0 ? external : null; +} + function projectIncompleteOnboarding( sandboxes: readonly SandboxEntry[], session: OnboardingSessionSummary | null | undefined, @@ -321,6 +340,9 @@ async function buildSandboxInventoryRow( policies: (await getPolicyPresets?.(sandbox.name)) ?? [], agent: resolveDisplayAgent(sandbox), ...(sandbox.dashboardPort != null ? { dashboardPort: sandbox.dashboardPort } : {}), + ...(resolveDashboardExternalUrl(sandbox) != null + ? { dashboardExternalUrl: resolveDashboardExternalUrl(sandbox) } + : {}), isDefault: sandbox.name === defaultSandbox, activeSessionCount, ...(sandbox.recoveredFromGateway ? { recoveredFromGateway: true } : {}), @@ -462,7 +484,9 @@ export function renderSandboxInventoryText( if (providerDrifted) parts.push(`provider=${sandbox.provider || "unknown"}`); log(` (live OpenShell gateway differs from onboarded: ${parts.join(", ")})`); } - if (sandbox.dashboardPort != null) { + if (sandbox.dashboardExternalUrl != null) { + log(` dashboard: ${sandbox.dashboardExternalUrl}`); + } else if (sandbox.dashboardPort != null) { log(` dashboard: http://127.0.0.1:${sandbox.dashboardPort}/`); } } @@ -514,6 +538,9 @@ async function buildStatusSandboxRow( agent: redactFull(resolveDisplayAgent(sandbox)), ...(portablePhase ? { phase: portablePhase } : {}), ...(dashboardPort != null ? { dashboardPort } : {}), + ...(resolveDashboardExternalUrl(sandbox) != null + ? { dashboardExternalUrl: resolveDashboardExternalUrl(sandbox) } + : {}), isDefault, }; } @@ -676,6 +703,10 @@ export function showStatusCommand(deps: ShowStatusCommandDeps): void { const provider = liveProvider || inference.provider; const portSuffix = sb.dashboardPort != null ? ` :${sb.dashboardPort}` : ""; log(` ${sb.name}${def}${model ? ` (${model})` : ""}${portSuffix}`); + const externalDashboardUrl = resolveDashboardExternalUrl(sb); + if (externalDashboardUrl) { + log(` Dashboard URL: ${externalDashboardUrl}`); + } const portablePhase = portablePhases.get(sb.name); if (portablePhase) log(` agent: hermes phase: ${portablePhase}`); if (isDefault && liveModel && liveModel !== inference.model) { diff --git a/src/lib/onboard/created-sandbox-finalization.test.ts b/src/lib/onboard/created-sandbox-finalization.test.ts index ecd2e3b193f..ef6aff88971 100644 --- a/src/lib/onboard/created-sandbox-finalization.test.ts +++ b/src/lib/onboard/created-sandbox-finalization.test.ts @@ -1406,6 +1406,8 @@ describe("created sandbox completion actions", () => { imageTag: "hermes:test", hermesPortableLifecycle: schema5, dashboardPort: manageDashboard ? 8643 : 0, + // Loopback chatUiUrl -> no external URL persisted (#11439). + dashboardExternalUrl: null, lifecycleGeneration: "generation-1", lifecycleLiveIdentityFingerprint: "a".repeat(64), inferenceSelection: inferenceRouteReservation.authority.selection, diff --git a/src/lib/onboard/created-sandbox-finalization.ts b/src/lib/onboard/created-sandbox-finalization.ts index 21f6a5bbe0c..5d2d79b01df 100644 --- a/src/lib/onboard/created-sandbox-finalization.ts +++ b/src/lib/onboard/created-sandbox-finalization.ts @@ -7,6 +7,7 @@ import { isDeepStrictEqual } from "node:util"; import { restoreRecreatedSandboxStateWithManagedAuthority } from "../actions/sandbox/snapshot/restore-authority"; import type { OpenShellSandboxBufferedCommandExecutor } from "../adapters/openshell/sandbox-command"; import * as buildContext from "../build-context"; +import { resolveExternalDashboardUrl } from "../dashboard/url"; import { resolveSandboxImageTagFromCreateOutput } from "../domain/sandbox/image-tag"; import type { OpenClawImagePluginInstall, @@ -111,6 +112,7 @@ type RegistrationSeed = Omit< | "openclawImagePluginInstalls" | "hermesDashboardState" | "dashboardPort" + | "dashboardExternalUrl" | "lifecycleGeneration" | "lifecycleLiveIdentityFingerprint" | "inferenceRouteReservation" @@ -362,6 +364,7 @@ export function createCreatedSandboxCompletionActions( ): CreatedSandboxCompletionActions { let chatUiUrl = options.dashboard.chatUiUrl; let dashboardPort = 0; + let dashboardExternalUrl: string | null = null; let hermesDashboardState = options.dashboard.initialHermesState; async function verifyCreatedProviderGpu(created: SandboxGpuCreateFlowResult): Promise { await dockerGpuLocalInference.verifyGpuSandboxLocalInferenceAndCommitAfterReady( @@ -401,6 +404,7 @@ export function createCreatedSandboxCompletionActions( ); } process.env.CHAT_UI_URL = chatUiUrl; + dashboardExternalUrl = resolveExternalDashboardUrl(chatUiUrl); hermesDashboardState = options.dashboard.resolveHermesState(dashboardPort); deps.revalidateSandboxIdentity?.( `recording Hermes dashboard capability for sandbox '${options.finalization.sandboxName}'`, @@ -507,6 +511,7 @@ export function createCreatedSandboxCompletionActions( openclawImagePluginInstalls, hermesDashboardState, dashboardPort, + dashboardExternalUrl, ...currentLifecycle, inferenceRouteReservation: verifiedInferenceRouteReservation, verifiedCreate, diff --git a/src/lib/onboard/dashboard-port.test.ts b/src/lib/onboard/dashboard-port.test.ts index 4a9ba90feaa..3107ebf83fe 100644 --- a/src/lib/onboard/dashboard-port.test.ts +++ b/src/lib/onboard/dashboard-port.test.ts @@ -17,6 +17,7 @@ import { findAvailableDashboardPort, findDashboardForwardOwner, getRegistryOccupiedDashboardPorts, + lsofOutputBlocksLoopbackBind, preflightDashboardPortRangeAvailability, reserveCreateSandboxDashboardPort, reserveDashboardPort, @@ -57,6 +58,55 @@ async function unusedLoopbackPort(): Promise { return address.port; } +describe("lsofOutputBlocksLoopbackBind interface-specific loopback probe (#11439)", () => { + const loopback = (port: number) => + `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nx 1 u 3u IPv4 1 0t0 TCP 127.0.0.1:${port} (LISTEN)`; + const external = (port: number) => + `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nsocat 1 u 6u IPv4 1 0t0 TCP 10.63.144.115:${port} (LISTEN)`; + const wildcard = (port: number) => + `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nx 1 u 3u IPv4 1 0t0 TCP *:${port} (LISTEN)`; + + it("treats an external-interface-only listener as non-blocking for the loopback bind", () => { + expect(lsofOutputBlocksLoopbackBind(external(18789), 18789)).toBe(false); + }); + + it("treats a loopback listener as blocking", () => { + expect(lsofOutputBlocksLoopbackBind(loopback(18789), 18789)).toBe(true); + }); + + it("treats a wildcard 0.0.0.0 or star listener as blocking, preserving docker-proxy detection (#3260)", () => { + expect(lsofOutputBlocksLoopbackBind(wildcard(18789), 18789)).toBe(true); + expect( + lsofOutputBlocksLoopbackBind("x 1 u 3u IPv4 1 0t0 TCP 0.0.0.0:18789 (LISTEN)", 18789), + ).toBe(true); + }); + + it("treats an IPv6 loopback listener as blocking", () => { + expect( + lsofOutputBlocksLoopbackBind("x 1 u 3u IPv6 1 0t0 TCP [::1]:18789 (LISTEN)", 18789), + ).toBe(true); + }); + + it("only matches the requested port", () => { + expect(lsofOutputBlocksLoopbackBind(loopback(18790), 18789)).toBe(false); + }); + + it("returns false for empty or missing output", () => { + expect(lsofOutputBlocksLoopbackBind("", 18789)).toBe(false); + expect(lsofOutputBlocksLoopbackBind(null, 18789)).toBe(false); + expect(lsofOutputBlocksLoopbackBind(undefined, 18789)).toBe(false); + }); + + it("ignores non-LISTEN rows", () => { + expect( + lsofOutputBlocksLoopbackBind( + "x 1 u 3u IPv4 1 0t0 TCP 127.0.0.1:18789->10.0.0.2:5000 (ESTABLISHED)", + 18789, + ), + ).toBe(false); + }); +}); + describe("findDashboardForwardOwner", () => { it("parses openshell forward list column format (#2169)", () => { const forwardList = [ diff --git a/src/lib/onboard/dashboard-port.ts b/src/lib/onboard/dashboard-port.ts index 44301dc4e9a..ac833161dff 100644 --- a/src/lib/onboard/dashboard-port.ts +++ b/src/lib/onboard/dashboard-port.ts @@ -125,26 +125,89 @@ export function probePortBoundSync(port: number): boolean { } /** - * Synchronous check whether a TCP port has an active listener on the host. + * Classify an `lsof -n` NAME bind address as one that would block a loopback + * dashboard bind. NemoClaw binds its dashboard forward on `127.0.0.1` + * (see {@link reserveDashboardPort} and {@link probePortBoundSync}), so a + * listener only conflicts when it already owns the loopback interface or a + * wildcard address that includes it. A listener bound solely to an external + * interface (e.g. a TLS reverse proxy in front of `CHAT_UI_URL`) leaves the + * loopback socket free and must not be treated as blocking (#11439). + */ +function bindAddressBlocksLoopback(address: string): boolean { + const addr = address.replace(/^\[/, "").replace(/\]$/, "").toLowerCase(); + // Wildcard binds (all interfaces) always include loopback. + if (addr === "*" || addr === "0.0.0.0" || addr === "::" || addr === "0:0:0:0:0:0:0:0") { + return true; + } + // IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1), including the + // IPv4-mapped form docker-proxy can report. + if (addr.startsWith("127.")) return true; + if (addr === "::1" || addr === "0:0:0:0:0:0:0:1") return true; + if (addr.startsWith("::ffff:127.")) return true; + return false; +} + +/** + * Decide whether `lsof -sTCP:LISTEN -P -n` output shows a listener on `port` + * that would block a loopback dashboard bind. Only loopback and wildcard binds + * count; an external-interface-only listener does not. Returns false when no + * matching loopback/wildcard listener is present so callers fall through to the + * authoritative `127.0.0.1` bind probe (#11439). Preserves docker-proxy / + * loopback / `0.0.0.0` detection (#3260), which report wildcard or loopback + * addresses. + */ +export function lsofOutputBlocksLoopbackBind( + output: string | null | undefined, + port: number, +): boolean { + if (!output) return false; + for (const rawLine of output.split("\n")) { + const line = rawLine.trim(); + if (!/\(LISTEN\)$/.test(line)) continue; + // NAME column holds the bind endpoint, e.g. "TCP 127.0.0.1:18789 (LISTEN)", + // "TCP *:18789 (LISTEN)", or "TCP [::1]:18789 (LISTEN)". + const match = line.match(/\s(\S+):(\d+)\s+\(LISTEN\)$/); + if (!match) continue; + if (Number(match[2]) !== port) continue; + if (bindAddressBlocksLoopback(match[1])) return true; + } + return false; +} + +/** + * Synchronous check whether a TCP port has an active listener that would block + * NemoClaw's dashboard bind. + * + * The default loopback dashboard forward binds `127.0.0.1`, so the decision is + * interface-specific: an external-interface-only listener on the same port + * number does not count. When `loopbackOnly` is false — an operator opted into a + * remote (`0.0.0.0`) bind via `NEMOCLAW_DASHBOARD_BIND` — the forward binds all + * interfaces, so any listener on the port (including an external-interface-only + * one) genuinely conflicts and counts (#3259, #11439). * * Detection chain — any positive signal short-circuits: * 1. `lsof` — finds listeners owned by the current user. * 2. `sudo -n lsof` — catches root-owned listeners (e.g., docker-proxy on - * macOS) that the unprivileged lsof can't see. Silently no-ops when - * the user can't escalate non-interactively. - * 3. Node `net` bind probe — authoritative fallback when both lsof - * invocations come up empty, mirroring the direct ForwardTcp bind. + * macOS) that the unprivileged lsof can't see. Silently no-ops when the + * user can't escalate non-interactively. + * 3. Node `net` bind probe — authoritative `127.0.0.1` check, run whenever the + * lsof invocations show no blocking listener, mirroring the direct + * ForwardTcp loopback bind. * * Returns false (optimistic) when every probe is inconclusive. The detached * OpenShell launch performs the final bind check. */ -export function isPortBoundOnHost(port: number): boolean { +export function isPortBoundOnHost(port: number, loopbackOnly = true): boolean { + const blocks = (output: ReturnType): boolean => + loopbackOnly + ? lsofOutputBlocksLoopbackBind(output, port) + : Boolean(output && output.trim().length > 0); try { const out: ReturnType = runCapture( ["lsof", "-i", `:${port}`, "-sTCP:LISTEN", "-P", "-n"], { ignoreError: true }, ); - if (out && out.trim().length > 0) return true; + if (blocks(out)) return true; } catch { /* fall through to the next probe */ } @@ -154,7 +217,7 @@ export function isPortBoundOnHost(port: number): boolean { ["sudo", "-n", "lsof", "-i", `:${port}`, "-sTCP:LISTEN", "-P", "-n"], { ignoreError: true }, ); - if (sudoOut && sudoOut.trim().length > 0) return true; + if (blocks(sudoOut)) return true; } catch { /* fall through to the bind probe */ } diff --git a/src/lib/onboard/dashboard.ts b/src/lib/onboard/dashboard.ts index 007d1087ef8..c7e0dc546c8 100644 --- a/src/lib/onboard/dashboard.ts +++ b/src/lib/onboard/dashboard.ts @@ -445,7 +445,13 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa ["forward", "list", "--gateway", forwardGateway], { ignoreError: true }, ); - const isPortBound = deps.isPortBoundOnHost ?? isPortBoundOnHost; + // The dashboard forward binds `127.0.0.1` by default, so an + // external-interface-only listener on the port does not conflict. When the + // operator opts into a remote (`0.0.0.0`) bind, every interface conflicts, + // so the host-port probe must count listeners on any interface (#3259, #11439). + const forwardBindsAllInterfaces = getDashboardForwardTarget(chatUiUrl).startsWith("0.0.0.0:"); + const isPortBoundRaw = deps.isPortBoundOnHost ?? isPortBoundOnHost; + const isPortBound = (port: number): boolean => isPortBoundRaw(port, !forwardBindsAllInterfaces); const persistedPort = getPersistedDashboardPort(sandboxName, listSandboxes); const registryOccupiedPorts = new Map([ ...getRegistryOccupiedDashboardPorts(sandboxName, listSandboxes), diff --git a/src/lib/onboard/sandbox-registration.ts b/src/lib/onboard/sandbox-registration.ts index d724bd22a65..d18b9d77b97 100644 --- a/src/lib/onboard/sandbox-registration.ts +++ b/src/lib/onboard/sandbox-registration.ts @@ -91,6 +91,12 @@ export interface CreatedSandboxRegistryEntryInput { /** True only when schema-5 receipt authority owns this Hermes registration. */ hermesPortableLifecycle?: boolean; dashboardPort: number; + /** + * Browser-facing external dashboard URL resolved from `CHAT_UI_URL`, or null + * when the dashboard is a plain loopback address. Persisted so post-onboard + * commands can report the external origin (#11439). + */ + dashboardExternalUrl?: string | null; dashboardRemoteBindPrepared?: boolean; lifecycleGeneration?: string; lifecycleLiveIdentityFingerprint?: string; @@ -290,6 +296,9 @@ export function buildCreatedSandboxRegistryEntry( })) : undefined, dashboardPort: input.dashboardPort, + ...(input.dashboardExternalUrl != null + ? { dashboardExternalUrl: input.dashboardExternalUrl } + : {}), dashboardRemoteBindPrepared: input.dashboardRemoteBindPrepared === true, lifecycleGeneration: input.lifecycleGeneration, lifecycleLiveIdentityFingerprint: input.lifecycleLiveIdentityFingerprint, diff --git a/src/lib/onboard/sandbox-reuse.test.ts b/src/lib/onboard/sandbox-reuse.test.ts index 33a5b25f920..9f92fe41670 100644 --- a/src/lib/onboard/sandbox-reuse.test.ts +++ b/src/lib/onboard/sandbox-reuse.test.ts @@ -75,6 +75,8 @@ describe("applyReusedSandboxDashboardState", () => { hermesDashboardPort: enabled ? 18789 : undefined, hermesDashboardInternalPort: enabled ? 19119 : undefined, hermesDashboardTui: undefined, + // No external CHAT_UI_URL configured -> loopback stays the reported form. + dashboardExternalUrl: null, gatewayName: "nemoclaw", gatewayPort: 8080, }); @@ -85,6 +87,46 @@ describe("applyReusedSandboxDashboardState", () => { }); }); + it("persists the external dashboard URL rebound to the effective port on reuse (#11439)", () => { + const updateSandbox = vi.fn(); + const ensureDashboardForward = vi.fn(() => 18790); + const sandboxGpuConfig: SandboxGpuConfig = { + hostGpuDetected: false, + hostGpuPlatform: null, + sandboxGpuEnabled: false, + mode: "auto", + sandboxGpuDevice: null, + errors: [], + }; + + applyReusedSandboxDashboardState({ + sandboxName: "reuse-me", + chatUiUrl: "http://127.0.0.1:18790", + env: { CHAT_UI_URL: "https://dash.example.com:18789" }, + agent: loadAgent("hermes"), + model: "test-model", + provider: "openai-compatible", + selectionVerified: true, + sandboxGpuConfig, + gatewayName: "nemoclaw", + gatewayPort: 8080, + ensureDashboardForward, + hermesDashboardForwarding: { + resolveStateForPort: vi.fn(() => ({ enabled: false, config: null })), + ensureForState: vi.fn(), + }, + updateSandbox, + updateReusedSandboxMetadata: vi.fn(), + }); + + expect(updateSandbox).toHaveBeenCalledWith( + "reuse-me", + expect.objectContaining({ + dashboardExternalUrl: "https://dash.example.com:18790", + }), + ); + }); + it("skips dashboard forwarding while preserving reuse metadata for terminal agents", () => { const updateSandbox = vi.fn(); const env: NodeJS.ProcessEnv = { CHAT_UI_URL: "https://chat.example.test:19000" }; diff --git a/src/lib/onboard/sandbox-reuse.ts b/src/lib/onboard/sandbox-reuse.ts index f32bab9816a..84520a15df6 100644 --- a/src/lib/onboard/sandbox-reuse.ts +++ b/src/lib/onboard/sandbox-reuse.ts @@ -4,6 +4,7 @@ import type { AgentDefinition } from "../agent/defs"; import type { SandboxEntry } from "../state/registry"; import * as registry from "../state/registry"; +import { resolveExternalDashboardUrlForPort } from "../dashboard/url"; import { canReuseDashboardForwardForAgent } from "./dashboard-runtime"; import { getHermesDashboardRegistryFields, @@ -135,6 +136,10 @@ export function applyReusedSandboxDashboardState( input: ReusedSandboxDashboardStateInput, ): ReusedSandboxDashboardStateResult { const manageDashboard = input.manageDashboard ?? true; + // Capture the operator's external origin before the loopback rewrite below + // overwrites `input.env.CHAT_UI_URL`, so the persisted external URL reflects + // the browser-facing address rather than the internal loopback bind (#11439). + const externalDashboardOrigin = input.env.CHAT_UI_URL; if ( manageDashboard && input.env.NEMOCLAW_DASHBOARD_BIND === "0.0.0.0" && @@ -191,8 +196,17 @@ export function applyReusedSandboxDashboardState( input.revalidateSandboxIdentity?.( `record reused dashboard state for sandbox '${input.sandboxName}'`, ); + // Persist (or clear) the browser-facing external dashboard URL derived from + // the operator's `CHAT_UI_URL`, rebinding its port to the effective dashboard + // port, so a re-onboard that adds, changes, or removes an external origin + // keeps status/dashboard-url/list accurate rather than reporting a stale or + // missing URL (#11439). Only meaningful when this run manages the dashboard. + const externalDashboardUrl = manageDashboard + ? resolveExternalDashboardUrlForPort(externalDashboardOrigin, dashboardPort) + : null; (input.updateSandbox ?? registry.updateSandbox)(input.sandboxName, { ...getHermesDashboardRegistryFields(hermesDashboardState), + ...(manageDashboard ? { dashboardExternalUrl: externalDashboardUrl } : {}), gatewayName: input.gatewayName, gatewayPort: input.gatewayPort, }); diff --git a/src/lib/state/gateway-registry.test.ts b/src/lib/state/gateway-registry.test.ts index 2dde76a4959..602571a3d4d 100644 --- a/src/lib/state/gateway-registry.test.ts +++ b/src/lib/state/gateway-registry.test.ts @@ -69,6 +69,63 @@ describe("host gateway registry index", () => { } }); + it("rejects a malformed persisted dashboardExternalUrl (#11439)", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gateway-index-exturl-")); + try { + const root = path.join(home, ".nemoclaw", "gateways", "9123"); + fs.mkdirSync(root, { recursive: true }); + fs.writeFileSync( + path.join(root, "sandboxes.json"), + JSON.stringify({ + defaultSandbox: "instance-a", + sandboxes: { + "instance-a": { + name: "instance-a", + gatewayName: "nemoclaw-9123", + gatewayPort: 9123, + dashboardPort: 18789, + dashboardExternalUrl: "not a url", + }, + }, + }), + ); + + expect(() => listHostGatewayRegistryEntries(home)).toThrow(/invalid dashboardExternalUrl/); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + + it("accepts a valid persisted dashboardExternalUrl (#11439)", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gateway-index-exturl-ok-")); + try { + const root = path.join(home, ".nemoclaw", "gateways", "9123"); + fs.mkdirSync(root, { recursive: true }); + fs.writeFileSync( + path.join(root, "sandboxes.json"), + JSON.stringify({ + defaultSandbox: "instance-a", + sandboxes: { + "instance-a": { + name: "instance-a", + gatewayName: "nemoclaw-9123", + gatewayPort: 9123, + dashboardPort: 18789, + dashboardExternalUrl: "https://dash.example.com:18789", + }, + }, + }), + ); + + const entries = listHostGatewayRegistryEntries(home); + + expect(entries).toHaveLength(1); + expect(entries[0].entry.dashboardExternalUrl).toBe("https://dash.example.com:18789"); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + it("treats a zero persisted dashboard port as no dashboard instead of blocking the registry (#7020)", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gateway-index-zero-port-")); try { diff --git a/src/lib/state/gateway-registry.ts b/src/lib/state/gateway-registry.ts index 1d86f67c39a..137a72983a1 100644 --- a/src/lib/state/gateway-registry.ts +++ b/src/lib/state/gateway-registry.ts @@ -8,6 +8,7 @@ import { isErrnoException } from "../core/errno"; import { isObjectRecord } from "../core/json-types"; import { DEFAULT_GATEWAY_PORT } from "../core/ports"; import { NAME_MAX_LENGTH, NAME_VALID_PATTERN } from "../name-validation"; +import { isValidDashboardExternalUrl } from "../dashboard/url"; import { resolveGatewayName, resolveGatewayPortFromName } from "../onboard/gateway-binding"; import { GATEWAYS_SUBDIR, nemoclawStateRoot } from "./state-root"; @@ -27,6 +28,7 @@ const MAX_GATEWAY_DIRECTORY_ENTRIES = 1024; export interface GatewayRegistryEntry extends Record { name: string; dashboardPort?: number | null; + dashboardExternalUrl?: string | null; hermesApiPort?: number | null; gatewayName?: string | null; gatewayPort?: number | null; @@ -113,6 +115,16 @@ function parseRegistry(filePath: string, raw: string): GatewayRegistryDocument { throw stateError(`${filePath} has an invalid ${field} for sandbox ${JSON.stringify(name)}`); } } + const externalUrl = value.dashboardExternalUrl; + if ( + externalUrl !== undefined && + externalUrl !== null && + (typeof externalUrl !== "string" || !isValidDashboardExternalUrl(externalUrl)) + ) { + throw stateError( + `${filePath} has an invalid dashboardExternalUrl for sandbox ${JSON.stringify(name)}`, + ); + } sandboxes[name] = value.dashboardPort === 0 ? { ...(value as GatewayRegistryEntry), dashboardPort: null } diff --git a/src/lib/state/registry-normalization.test.ts b/src/lib/state/registry-normalization.test.ts index cd33eb800f8..14da93e80eb 100644 --- a/src/lib/state/registry-normalization.test.ts +++ b/src/lib/state/registry-normalization.test.ts @@ -216,6 +216,40 @@ describe("sandbox registry normalization", () => { }); }); + it("round-trips the persisted external dashboard URL (#11439)", async () => { + const registry = await loadRegistryWith({}); + registry.registerSandbox({ + name: "proxied", + dashboardPort: 18_789, + dashboardExternalUrl: "https://dash.example.com:18789", + }); + + vi.resetModules(); + const reloadedRegistry = await import("./registry"); + expect(reloadedRegistry.getSandbox("proxied")).toMatchObject({ + dashboardPort: 18_789, + dashboardExternalUrl: "https://dash.example.com:18789", + }); + }); + + it("preserves a persisted external dashboard URL when only the port is updated (#11439)", async () => { + const registry = await loadRegistryWith({}); + registry.registerSandbox({ + name: "proxied", + dashboardPort: 18_789, + dashboardExternalUrl: "https://dash.example.com:18789", + }); + + // Mirror the non-clearing persistDashboardPort update: a loopback re-onboard + // must not overwrite the external URL with null. + registry.updateSandbox("proxied", { dashboardPort: 18_790 }); + + expect(registry.getSandbox("proxied")).toMatchObject({ + dashboardPort: 18_790, + dashboardExternalUrl: "https://dash.example.com:18789", + }); + }); + it("backfills a lifecycle generation only for the unchanged legacy Docker row (#8584)", async () => { const registry = await loadRegistryWith({}); const { compareAndSetLegacySandboxLifecycleGeneration } = diff --git a/src/lib/state/registry.ts b/src/lib/state/registry.ts index e5ec0c0f12d..547247168aa 100644 --- a/src/lib/state/registry.ts +++ b/src/lib/state/registry.ts @@ -538,6 +538,7 @@ export function registerSandbox( hermesDashboardTui: entry.hermesDashboardTui === true ? true : undefined, hermesApiPort: entry.hermesApiPort ?? undefined, dashboardPort: entry.dashboardPort ?? undefined, + dashboardExternalUrl: entry.dashboardExternalUrl ?? undefined, dashboardRemoteBindPrepared: entry.dashboardRemoteBindPrepared === true ? true : undefined, gatewayName: entry.gatewayName ?? undefined, gatewayPort: entry.gatewayPort ?? undefined, diff --git a/src/lib/state/registry/types.ts b/src/lib/state/registry/types.ts index f37b64cadd7..3cdea9ea0ef 100644 --- a/src/lib/state/registry/types.ts +++ b/src/lib/state/registry/types.ts @@ -149,6 +149,13 @@ export interface SandboxEntry extends Partial { */ hermesApiPort?: number | null; dashboardPort?: number | null; + /** + * Browser-facing external dashboard URL resolved from `CHAT_UI_URL` at + * onboard time (host + scheme with the effective dashboard port). Persisted + * only when an external origin was configured; a plain loopback dashboard is + * left unset and reported as `http://127.0.0.1:/` (#11439). + */ + dashboardExternalUrl?: string | null; /** Remote dashboard exposure was included in the sandbox's generated config. */ dashboardRemoteBindPrepared?: boolean; /** Generation proving which durable same-name recreate registered this row. */ From ca4bd86b1523c6037bab8241af22ce58de6f5b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 13:29:17 -0700 Subject: [PATCH 02/12] fix(onboard): align dashboard merge with current forwarding owner Prepare the reviewed conflict resolution for the current main integration. The final dashboard projection has passed focused tests and publication validation. Signed-off-by: Aaron Erickson --- src/commands/sandbox/dashboard-url.ts | 8 +- .../openshell/forward-service.test.ts | 1362 ----------------- src/lib/adapters/openshell/forward-service.ts | 770 ---------- src/lib/dashboard-url-command.test.ts | 102 +- src/lib/inventory/index.ts | 252 +-- src/lib/onboard/dashboard-port.test.ts | 422 +++-- src/lib/onboard/dashboard.ts | 436 +++--- src/lib/state/gateway-registry.ts | 144 +- 8 files changed, 782 insertions(+), 2714 deletions(-) delete mode 100644 src/lib/adapters/openshell/forward-service.test.ts delete mode 100644 src/lib/adapters/openshell/forward-service.ts diff --git a/src/commands/sandbox/dashboard-url.ts b/src/commands/sandbox/dashboard-url.ts index 52a121ed097..6fec127bfed 100644 --- a/src/commands/sandbox/dashboard-url.ts +++ b/src/commands/sandbox/dashboard-url.ts @@ -8,10 +8,8 @@ import { DashboardUrlCommandError, runDashboardUrlCommand } from "../../lib/dash import type { SandboxEntry } from "../../lib/state/registry"; type DashboardUrlRuntimeBridge = { - fetchGatewayAuthTokenFromSandbox: (sandboxName: string) => string | null; - getSandbox: ( - sandboxName: string, - ) => Pick | null; + fetchGatewayAuthTokenFromSandbox: (sandboxName: string) => Promise; + getSandbox: (sandboxName: string) => Pick | null; getAccessUrl?: (port: number) => string | null; }; @@ -88,7 +86,7 @@ export default class DashboardUrlCliCommand extends NemoClawCommand { const runtime = getRuntimeBridge(); try { - runDashboardUrlCommand( + await runDashboardUrlCommand( args.sandboxName, { quiet: flags.quiet === true }, { diff --git a/src/lib/adapters/openshell/forward-service.test.ts b/src/lib/adapters/openshell/forward-service.test.ts deleted file mode 100644 index d75f9257c7e..00000000000 --- a/src/lib/adapters/openshell/forward-service.test.ts +++ /dev/null @@ -1,1362 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { type ChildProcess, spawn } from "node:child_process"; -import { EventEmitter, once } from "node:events"; -import { createOpenShellOperationDeadline } from "./operation-deadline"; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - realpathSync, - rmSync, - symlinkSync, - writeFileSync, -} from "node:fs"; -import { type AddressInfo, createServer } from "node:net"; -import os from "node:os"; -import path from "node:path"; - -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { - buildForwardServiceArgs, - createForwardServiceTarget, - ForwardServiceStartupCleanupError, - isForwardServiceListenerOwner, - isTrustedTaskkillExecutable, - launchForwardService, - terminateForwardServiceProcessTree, - type ForwardServiceLaunchOptions, - type ForwardServiceOwnership, - type ForwardServiceTarget, -} from "./forward-service"; -import { probeLocalForwardListener } from "./local-forward-listener"; - -const target: ForwardServiceTarget = { - executable: "/usr/local/bin/openshell", - gatewayEndpoint: "https://127.0.0.1:8080", - gatewayName: "nemoclaw", - workspace: "default", - sandboxName: "demo", - localHost: "127.0.0.1", - localPort: 18_789, - targetHost: "127.0.0.1", - targetPort: 18_789, -}; - -const ownerTarget: ForwardServiceTarget = { ...target, executable: process.execPath }; -const detachedChildPid = process.pid + 1; -const temporaryDirectories: string[] = []; -const startedProcessGroups: number[] = []; -const processSleepBuffer = new Int32Array(new SharedArrayBuffer(4)); - -function createLinuxOwnerFixture(actualExecutable?: string) { - const root = mkdtempSync(path.join(os.tmpdir(), "nemoclaw-forward-owner-")); - temporaryDirectories.push(root); - const procRoot = path.join(root, "proc"); - const binRoot = path.join(root, "bin"); - mkdirSync(path.join(procRoot, "net"), { recursive: true }); - mkdirSync(path.join(procRoot, "4321", "fd"), { recursive: true }); - mkdirSync(path.join(procRoot, "9876", "fd"), { recursive: true }); - mkdirSync(binRoot); - const executable = path.join(binRoot, "openshell"); - const runtime = actualExecutable ? path.join(binRoot, actualExecutable) : executable; - writeFileSync(executable, ""); - writeFileSync(runtime, ""); - writeFileSync( - path.join(procRoot, "net", "tcp"), - " 0: 0100007F:4965 00000000:0000 0A 00000000:00000000 00:00000000 00000000 998 0 12345 1\n", - ); - writeFileSync( - path.join(procRoot, "net", "tcp6"), - " 1: 00000000000000000000000001000000:4965 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 998 0 67890 1\n", - ); - symlinkSync("socket:[12345]", path.join(procRoot, "4321", "fd", "7")); - symlinkSync("socket:[67890]", path.join(procRoot, "9876", "fd", "8")); - symlinkSync(runtime, path.join(procRoot, "4321", "exe")); - return { procRoot, target: { ...target, executable } }; -} - -// A loopback forward (127.0.0.1 on 0x4965 = 18789) whose port is also bound on -// an external interface by a different process (a reverse proxy). The external -// entry's inode belongs to pid 9876; ownership must ignore it and count only -// the loopback owner pid 4321 (#11439). -function createLinuxOwnerFixtureWithExternalCoListener() { - const root = mkdtempSync(path.join(os.tmpdir(), "nemoclaw-forward-owner-ext-")); - temporaryDirectories.push(root); - const procRoot = path.join(root, "proc"); - const binRoot = path.join(root, "bin"); - mkdirSync(path.join(procRoot, "net"), { recursive: true }); - mkdirSync(path.join(procRoot, "4321", "fd"), { recursive: true }); - mkdirSync(path.join(procRoot, "9876", "fd"), { recursive: true }); - mkdirSync(binRoot); - const executable = path.join(binRoot, "openshell"); - writeFileSync(executable, ""); - writeFileSync( - path.join(procRoot, "net", "tcp"), - // Loopback listener (0100007F) owned by the forward, plus an - // external-interface listener (0F90630A) owned by the reverse proxy. - " 0: 0100007F:4965 00000000:0000 0A 00000000:00000000 00:00000000 00000000 998 0 12345 1\n" + - " 1: 0F90630A:4965 00000000:0000 0A 00000000:00000000 00:00000000 00000000 998 0 55555 1\n", - ); - writeFileSync(path.join(procRoot, "net", "tcp6"), ""); - symlinkSync("socket:[12345]", path.join(procRoot, "4321", "fd", "7")); - symlinkSync("socket:[55555]", path.join(procRoot, "9876", "fd", "8")); - symlinkSync(executable, path.join(procRoot, "4321", "exe")); - return { procRoot, target: { ...target, executable } }; -} - -// Loopback forwards probe with `lsof -FpPn`, pairing each pid with its bind -// endpoint so external-interface-only listeners are excluded (#11439). -function loopbackListenerField(pid: string, port = 18_789): string { - return `p${pid}\nPTCP\nn127.0.0.1:${port}\n`; -} - -function darwinOwnerProbe( - commandLine: string, - finalListener = loopbackListenerField("4321"), - executable = process.execPath, -) { - return vi - .fn() - .mockReturnValueOnce({ status: 0, stdout: loopbackListenerField("4321") }) - .mockReturnValueOnce({ status: 0, stdout: `${executable}\n/mach_kernel\n` }) - .mockReturnValueOnce({ status: 0, stdout: commandLine }) - .mockReturnValueOnce({ status: 0, stdout: finalListener }); -} - -afterEach(() => { - vi.unstubAllEnvs(); - for (const processGroup of startedProcessGroups.splice(0)) { - try { - process.kill(-processGroup, "SIGKILL"); - } catch { - // Best effort only — the owned process group may already be gone. - } - } - for (const directory of temporaryDirectories.splice(0)) { - rmSync(directory, { recursive: true, force: true }); - } -}); - -function isRunning(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -async function waitForExit(pid: number): Promise { - for (let attempt = 0; attempt < 200 && isRunning(pid); attempt += 1) { - await new Promise((resolve) => setTimeout(resolve, 25)); - } - return !isRunning(pid); -} - -async function availableLoopbackPort(): Promise { - const server = createServer(); - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", resolve); - }); - const address = server.address() as AddressInfo; - await new Promise((resolve, reject) => - server.close((error) => (error ? reject(error) : resolve())), - ); - return address.port; -} - -describe("retained forward process ownership", () => { - it("retains ownership across asynchronous startup and cleanup (#11649)", async () => { - const child = Object.assign(new EventEmitter(), { - pid: detachedChildPid, - exitCode: null, - signalCode: null, - unref() {}, - }); - const terminate = vi.fn(); - let ownership: ForwardServiceOwnership | undefined; - await launchForwardService(target, { - isReachable: vi.fn().mockReturnValueOnce(false).mockReturnValue(true), - spawnDetached: () => child, - terminateProcessTree: terminate, - retainOwnership: (value) => { - ownership = value; - }, - }); - await Promise.resolve(); - - await Promise.all([ownership!.terminate(), ownership!.terminate()]); - expect(terminate).toHaveBeenCalledOnce(); - }); - - it.each(["exit", "error", "pid-changed", "exit-code", "signal-code"] as const)( - "rejects termination after %s invalidates the original child (#11649)", - async (change) => { - const child = Object.assign(new EventEmitter(), { - pid: detachedChildPid, - exitCode: null as number | null, - signalCode: null as NodeJS.Signals | null, - unref() {}, - }); - const terminate = vi.fn(); - let ownership: ForwardServiceOwnership | undefined; - await launchForwardService(target, { - isReachable: vi.fn().mockReturnValueOnce(false).mockReturnValue(true), - spawnDetached: () => child, - terminateProcessTree: terminate, - retainOwnership: (value) => { - ownership = value; - }, - }); - const mutations = { - exit: () => child.emit("exit"), - error: () => child.emit("error"), - "pid-changed": () => { - child.pid += 1; - }, - "exit-code": () => { - child.exitCode = 0; - }, - "signal-code": () => { - child.signalCode = "SIGTERM"; - }, - }; - mutations[change](); - await expect(ownership!.terminate()).rejects.toThrow( - "child lifetime can no longer be proved", - ); - expect(terminate).not.toHaveBeenCalled(); - }, - ); - - it.each(["exit", "authority"] as const)( - "checks %s changes while cleanup waits for exit callbacks (#11649)", - async (change) => { - const child = Object.assign(new EventEmitter(), { - pid: detachedChildPid, - exitCode: null, - signalCode: null, - unref() {}, - }); - const terminate = vi.fn(); - let ownership: ForwardServiceOwnership | undefined; - await launchForwardService(target, { - isReachable: vi.fn().mockReturnValueOnce(false).mockReturnValue(true), - spawnDetached: () => child, - terminateProcessTree: terminate, - retainOwnership: (value) => { - ownership = value; - }, - }); - const assertCurrent = vi.fn(); - const cleanup = ownership!.terminate(assertCurrent); - expect(terminate).not.toHaveBeenCalled(); - const invalidate = { - exit: () => child.emit("exit", 0, null), - authority: () => - assertCurrent.mockImplementation(() => { - throw new Error("rollback authority changed"); - }), - }; - invalidate[change](); - await expect(cleanup).rejects.toThrow( - change === "exit" ? "child lifetime can no longer be proved" : "rollback authority changed", - ); - expect(terminate).not.toHaveBeenCalled(); - }, - ); - - it("retires a retained real listener and makes repeated cleanup harmless (#11649)", async () => { - const port = await availableLoopbackPort(); - let child: ChildProcess | undefined; - let closed: Promise | undefined; - let ownership: ForwardServiceOwnership | undefined; - try { - await launchForwardService( - { ...target, executable: process.execPath, localPort: port, targetPort: port }, - { - spawnDetached: () => { - child = spawn( - process.execPath, - ["-e", `require("node:net").createServer().listen(${port}, "127.0.0.1")`], - { detached: true, stdio: "ignore" }, - ); - closed = once(child, "close"); - return child; - }, - retainOwnership: (value) => { - ownership = value; - }, - }, - ); - expect(probeLocalForwardListener(port, 100)).toBe(true); - await ownership!.terminate(); - await ownership!.terminate(); - await closed; - expect(child!.exitCode !== null || child!.signalCode !== null).toBe(true); - expect(probeLocalForwardListener(port, 100)).toBe(false); - } finally { - child?.exitCode === null && - child.signalCode === null && - terminateForwardServiceProcessTree(child); - await closed; - } - }, 10_000); -}); - -describe("forward startup allowance", () => { - it.each([0, 100])( - "rejects an exhausted %i ms allowance before probing or spawning (#11652)", - async (timeoutMs) => { - const isReachable = vi.fn(() => false); - const spawnDetached = vi.fn(() => ({ pid: detachedChildPid, unref: vi.fn() })); - await expect( - launchForwardService(target, { - timeoutMs, - now: vi.fn().mockReturnValueOnce(0).mockReturnValue(100), - isReachable, - spawnDetached, - terminateProcessTree: vi.fn(), - }), - ).rejects.toThrow("did not bind"); - expect(isReachable).not.toHaveBeenCalled(); - expect(spawnDetached).not.toHaveBeenCalled(); - }, - ); - - it("does not spawn after the initial probe exhausts the allowance (#11652)", async () => { - let elapsed = 0; - const isReachable = vi - .fn() - .mockImplementationOnce(() => { - elapsed = 100; - return false; - }) - .mockReturnValue(false); - const spawnDetached = vi.fn(() => ({ pid: detachedChildPid, unref: vi.fn() })); - await expect( - launchForwardService(target, { - timeoutMs: 100, - now: () => elapsed, - isReachable, - spawnDetached, - terminateProcessTree: vi.fn(), - }), - ).rejects.toThrow("did not bind"); - expect(spawnDetached).not.toHaveBeenCalled(); - expect(isReachable).toHaveBeenCalledExactlyOnceWith(target.localPort, 100); - }); - - it("cleans up the child when a startup probe throws at the deadline (#11652)", async () => { - const child = { pid: detachedChildPid, unref: vi.fn() }; - const terminateProcessTree = vi.fn(); - const isReachable = vi - .fn() - .mockReturnValueOnce(false) - .mockImplementationOnce(() => { - throw new Error("startup allowance exhausted"); - }) - .mockReturnValue(false); - await expect( - launchForwardService(target, { - isReachable, - spawnDetached: () => child, - terminateProcessTree, - }), - ).rejects.toThrow("startup allowance exhausted"); - expect(terminateProcessTree).toHaveBeenCalledExactlyOnceWith(child); - expect(child.unref).not.toHaveBeenCalled(); - }); - - it("rejects readiness that finishes after the startup allowance (#11652)", async () => { - let elapsed = 0; - const child = { pid: detachedChildPid, unref: vi.fn() }; - const terminateProcessTree = vi.fn(); - await expect( - launchForwardService(target, { - timeoutMs: 100, - now: () => elapsed, - isReachable: vi - .fn() - .mockReturnValueOnce(false) - .mockReturnValueOnce(true) - .mockReturnValue(false), - verifyReady: () => { - elapsed = 100; - }, - spawnDetached: () => child, - terminateProcessTree, - }), - ).rejects.toThrow("did not bind"); - expect(terminateProcessTree).toHaveBeenCalledExactlyOnceWith(child); - expect(child.unref).not.toHaveBeenCalled(); - }); - - it("shares the allowance between the initial port probe and startup polling (#11652)", async () => { - let elapsed = 0; - const allowances: number[] = []; - const terminateProcessTree = vi.fn(); - const startupProbe = (_port: number, allowance?: number) => { - allowances.push(allowance!); - elapsed += Math.min(60, allowance!); - return false; - }; - await expect( - launchForwardService(target, { - timeoutMs: 100, - now: () => elapsed, - isReachable: vi - .fn() - .mockImplementationOnce(startupProbe) - .mockImplementationOnce(startupProbe) - .mockReturnValue(false), - sleep: (milliseconds) => { - elapsed += milliseconds; - }, - spawnDetached: () => ({ pid: detachedChildPid, unref() {} }), - terminateProcessTree, - }), - ).rejects.toThrow("did not bind"); - expect(allowances).toEqual([100, 40]); - expect(elapsed).toBe(100); - }); - - it("uses monotonic time when the wall clock moves backwards (#11652)", async () => { - let elapsed = 0; - let wall = 10_000; - const wallClock = vi.spyOn(Date, "now").mockImplementation(() => wall); - try { - await expect( - launchForwardService(target, { - timeoutMs: 100, - now: () => elapsed, - isReachable: () => false, - spawnDetached: () => ({ pid: detachedChildPid, unref() {} }), - terminateProcessTree: () => undefined, - sleep: (milliseconds) => { - wall += milliseconds - (elapsed === 0 ? 1_000 : 0); - elapsed += milliseconds; - }, - }), - ).rejects.toThrow("did not bind"); - expect(elapsed).toBe(100); - } finally { - wallClock.mockRestore(); - } - }); -}); - -describe("OpenShell forward service", () => { - it("builds the direct ForwardTcp command with explicit gateway authority", () => { - expect(buildForwardServiceArgs(target)).toEqual([ - "--gateway", - "nemoclaw", - "--gateway-endpoint", - "https://127.0.0.1:8080", - "--workspace", - "default", - "forward", - "service", - "demo", - "--target-port", - "18789", - "--target-host", - "127.0.0.1", - "--local", - "127.0.0.1:18789", - ]); - }); - - it("builds the direct ForwardTcp command for a selected non-default workspace", () => { - expect(buildForwardServiceArgs({ ...target, workspace: "review-workspace" })).toContain( - "review-workspace", - ); - }); - - it("derives and validates the endpoint for a managed non-default gateway", () => { - const selected = createForwardServiceTarget( - { - executable: target.executable, - gatewayName: "nemoclaw-19080", - workspace: target.workspace, - sandboxName: target.sandboxName, - localHost: target.localHost, - }, - target.localPort, - ); - - expect(selected.gatewayEndpoint).toBe("https://127.0.0.1:19080"); - expect(buildForwardServiceArgs(selected)).toContain("https://127.0.0.1:19080"); - expect(() => - buildForwardServiceArgs({ - ...selected, - gatewayEndpoint: "https://attacker.invalid:19080", - }), - ).toThrow(/bare loopback origin matching its gateway port/u); - }); - - it.each(["http://127.0.0.1:19080", "https://[::1]:19080"])( - "accepts the authority-bound local gateway endpoint %s", - (gatewayEndpoint) => { - const selected = createForwardServiceTarget( - { - executable: target.executable, - gatewayEndpoint, - gatewayName: "nemoclaw-19080", - workspace: target.workspace, - sandboxName: target.sandboxName, - localHost: target.localHost, - }, - target.localPort, - ); - - expect(buildForwardServiceArgs(selected)).toContain(gatewayEndpoint); - }, - ); - - it("normalizes the managed HTTPS default port to a bare origin", () => { - expect( - createForwardServiceTarget( - { - executable: target.executable, - gatewayName: "nemoclaw-443", - workspace: target.workspace, - sandboxName: target.sandboxName, - localHost: target.localHost, - }, - target.localPort, - ).gatewayEndpoint, - ).toBe("https://127.0.0.1"); - }); - - it.each([ - "http://localhost:19080", - "http://127.0.0.1:19081", - "http://127.0.0.1:19080/path", - "http://user@127.0.0.1:19080", - ])("rejects an endpoint outside the exact local gateway authority: %s", (gatewayEndpoint) => { - expect(() => - createForwardServiceTarget( - { - executable: target.executable, - gatewayEndpoint, - gatewayName: "nemoclaw-19080", - workspace: target.workspace, - sandboxName: target.sandboxName, - localHost: target.localHost, - }, - target.localPort, - ), - ).toThrow(/bare loopback origin matching its gateway port/u); - }); - - it("shares the remaining allowance across ownership subprocesses (#11652)", () => { - const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - const response = darwinOwnerProbe(`${expected}\n`); - let elapsed = 0; - const allowances: number[] = []; - const probe = (executable: string, args: readonly string[], timeoutMs = 5_000) => { - allowances.push(timeoutMs); - elapsed += Math.min(60, timeoutMs); - return response(executable, args); - }; - const deadline = createOpenShellOperationDeadline(100, () => elapsed); - const remainingMs = (maximumMs: number) => deadline.remaining(maximumMs, "ownership"); - expect(() => - isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe, remainingMs }), - ).toThrow("operation allowance exhausted"); - expect(allowances).toEqual([100, 40]); - expect(elapsed).toBe(100); - }); - - it("proves the exact direct ForwardTcp listener before reuse", () => { - const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - const probe = darwinOwnerProbe(`${expected}\n`); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(true); - expect(probe).toHaveBeenCalledTimes(4); - expect(probe).toHaveBeenNthCalledWith(2, "codesign", ["-h", "4321"]); - }); - - it("rejects a listener whose process does not match the direct ForwardTcp target", () => { - const probe = darwinOwnerProbe("/usr/bin/node foreign-listener.js\n"); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); - }); - - it("rejects a foreign Darwin executable even when it maps the trusted binary", () => { - const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - const responses = new Map([ - [ - JSON.stringify(["lsof", "-i4TCP:18789", "-sTCP:LISTEN", "-P", "-n", "-FpPn"]), - { status: 0, stdout: loopbackListenerField("4321") }, - ], - [ - JSON.stringify(["lsof", "-a", "-p", "4321", "-d", "txt", "-Fn"]), - { - status: 0, - stdout: `p4321\nftxt\nn/usr/bin/python3\nn${ownerTarget.executable}\n`, - }, - ], - [ - JSON.stringify(["codesign", "-h", "4321"]), - { status: 0, stdout: "/usr/bin/python3\n/mach_kernel\n" }, - ], - [ - JSON.stringify(["ps", "-ww", "-p", "4321", "-o", "args="]), - { status: 0, stdout: `${expected}\n` }, - ], - ]); - const probe = vi.fn( - (executable: string, args: readonly string[]) => - responses.get(JSON.stringify([executable, ...args])) ?? { status: null, stdout: "" }, - ); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); - expect(probe).not.toHaveBeenCalledWith("lsof", ["-a", "-p", "4321", "-d", "txt", "-Fn"]); - }); - - it("rejects an otherwise exact listener without managed gateway endpoint authority", () => { - const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - const commandLine = expected.replace(" --gateway-endpoint https://127.0.0.1:8080", ""); - const probe = darwinOwnerProbe(`${commandLine}\n`); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); - }); - - it("rejects an otherwise exact listener with a different gateway endpoint", () => { - const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - const commandLine = expected.replace("https://127.0.0.1:8080", "https://127.0.0.1:8081"); - const probe = darwinOwnerProbe(`${commandLine}\n`); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); - }); - - it("rejects ambiguous or changing listener ownership", () => { - const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - const probe = darwinOwnerProbe(`${expected}\n`, loopbackListenerField("9876")); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); - }); - - it("rejects ownership when a host probe times out", () => { - const lsofTimeout = vi.fn(() => ({ status: null, stdout: "" })); - expect( - isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe: lsofTimeout }), - ).toBe(false); - - const psTimeout = vi - .fn() - .mockReturnValueOnce({ status: 0, stdout: loopbackListenerField("4321") }) - .mockReturnValueOnce({ status: 0, stdout: `${process.execPath}\n/mach_kernel\n` }) - .mockReturnValueOnce({ status: null, stdout: "" }); - expect( - isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe: psTimeout }), - ).toBe(false); - }); - - it("proves Linux IPv4 ownership while ignoring an IPv6-only listener", () => { - const fixture = createLinuxOwnerFixture(); - const expected = [fixture.target.executable, ...buildForwardServiceArgs(fixture.target)].join( - " ", - ); - const responses = { - lsof: { status: null, stdout: "" }, - ps: { status: 0, stdout: `${expected}\n` }, - }; - const probe = vi.fn( - (executable: string) => responses[executable as keyof typeof responses] ?? responses.lsof, - ); - - expect( - isForwardServiceListenerOwner(fixture.target, { - platform: "linux", - probe, - procRoot: fixture.procRoot, - }), - ).toBe(true); - expect(probe).toHaveBeenCalledTimes(3); - expect(probe).toHaveBeenCalledWith("lsof", [ - "-i4TCP:18789", - "-sTCP:LISTEN", - "-P", - "-n", - "-FpPn", - ]); - expect(probe).toHaveBeenCalledWith("ps", ["-ww", "-p", "4321", "-o", "args="]); - }); - - it("ignores an external-interface-only co-listener for a loopback forward (#11439)", () => { - const expected = [ownerTarget.executable, ...buildForwardServiceArgs(ownerTarget)].join(" "); - // Field-mode lsof reports the reverse proxy on an external interface plus - // NemoClaw's own loopback forward. Only the loopback pid may count. - const listenerField = `p3529439\nPTCP\nn10.63.144.115:18789\n` + loopbackListenerField("4321"); - const probe = vi - .fn() - .mockReturnValueOnce({ status: 0, stdout: listenerField }) - .mockReturnValueOnce({ status: 0, stdout: `${ownerTarget.executable}\n/mach_kernel\n` }) - .mockReturnValueOnce({ status: 0, stdout: `${expected}\n` }) - .mockReturnValueOnce({ status: 0, stdout: listenerField }); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(true); - }); - - it("still rejects a genuine second loopback co-listener for a loopback forward", () => { - const listenerField = loopbackListenerField("4321") + loopbackListenerField("9999"); - const probe = vi.fn().mockReturnValue({ status: 0, stdout: listenerField }); - - expect(isForwardServiceListenerOwner(ownerTarget, { platform: "darwin", probe })).toBe(false); - }); - - it("counts every interface for a remote-exposed (0.0.0.0) forward", () => { - const remoteTarget: ForwardServiceTarget = { ...ownerTarget, localHost: "0.0.0.0" }; - // A 0.0.0.0 forward must keep the interface-agnostic `-ti` count so any - // co-listener still blocks ownership. - const probe = vi.fn().mockReturnValue({ status: 0, stdout: "4321\n3529439\n" }); - - expect(isForwardServiceListenerOwner(remoteTarget, { platform: "darwin", probe })).toBe(false); - expect(probe).toHaveBeenCalledWith("lsof", ["-ti4TCP:18789", "-sTCP:LISTEN"]); - }); - - it("ignores an external-interface-only /proc listener for a loopback forward (#11439)", () => { - const fixture = createLinuxOwnerFixtureWithExternalCoListener(); - const expected = [fixture.target.executable, ...buildForwardServiceArgs(fixture.target)].join( - " ", - ); - const responses = { - lsof: { status: null, stdout: "" }, - ps: { status: 0, stdout: `${expected}\n` }, - }; - const probe = vi.fn( - (executable: string) => responses[executable as keyof typeof responses] ?? responses.lsof, - ); - - expect( - isForwardServiceListenerOwner(fixture.target, { - platform: "linux", - probe, - procRoot: fixture.procRoot, - }), - ).toBe(true); - }); - - it("rejects spoofed arguments when the Linux executable is different", () => { - const fixture = createLinuxOwnerFixture("python3"); - const expected = [fixture.target.executable, ...buildForwardServiceArgs(fixture.target)].join( - " ", - ); - const responses = { - lsof: { status: null, stdout: "" }, - ps: { status: 0, stdout: `${expected}\n` }, - }; - const probe = vi.fn( - (executable: string) => responses[executable as keyof typeof responses] ?? responses.lsof, - ); - - expect( - isForwardServiceListenerOwner(fixture.target, { - platform: "linux", - probe, - procRoot: fixture.procRoot, - }), - ).toBe(false); - expect(probe).toHaveBeenCalledOnce(); - }); - - it("denies Linux ownership when the /proc work limit is reached", () => { - const fixture = createLinuxOwnerFixture(); - const probe = vi.fn(() => ({ status: null, stdout: "" })); - - expect( - isForwardServiceListenerOwner(fixture.target, { - platform: "linux", - probe, - procRoot: fixture.procRoot, - procWorkLimit: 1, - }), - ).toBe(false); - expect(probe).toHaveBeenCalledOnce(); - }); - - it.each([ - { wait: "default", sleep: undefined }, - { wait: "synchronous hook", sleep: () => undefined }, - { wait: "settled hook", sleep: async () => undefined }, - ])("returns a missing executable error with $wait (#11648)", async ({ sleep }) => { - const root = mkdtempSync(path.join(os.tmpdir(), "nemoclaw-forward-spawn-")); - temporaryDirectories.push(root); - const terminateProcessTree = vi.fn(); - - await expect( - launchForwardService( - { ...target, executable: path.join(root, "missing") }, - { - isReachable: () => false, - terminateProcessTree, - timeoutMs: 1_000, - sleep, - }, - ), - ).rejects.toMatchObject({ code: "ENOENT" }); - - expect(terminateProcessTree).not.toHaveBeenCalled(); - }); - - it.skipIf(process.platform === "win32")( - "returns permission-denied spawn failures to the caller (#11648)", - async () => { - const root = mkdtempSync(path.join(os.tmpdir(), "nemoclaw-forward-spawn-")); - temporaryDirectories.push(root); - const executable = path.join(root, "openshell"); - writeFileSync(executable, "#!/bin/sh\nexit 0\n", { mode: 0o600 }); - const terminateProcessTree = vi.fn(); - - await expect( - launchForwardService( - { ...target, executable }, - { - isReachable: () => false, - terminateProcessTree, - timeoutMs: 1_000, - }, - ), - ).rejects.toMatchObject({ code: "EACCES" }); - - expect(terminateProcessTree).not.toHaveBeenCalled(); - }, - ); - - it.skipIf(process.platform === "win32")( - "reports a real child exit before the bind timeout (#11648)", - async () => { - await expect( - launchForwardService(ownerTarget, { - isReachable: () => false, - spawnDetached: () => - spawn(process.execPath, ["-e", "process.exit(23)"], { - detached: true, - stdio: "ignore", - }), - timeoutMs: 2_000, - }), - ).rejects.toThrow(/exited before binding .*status 23/u); - }, - ); - - it.each([ - { - mode: "throw", - fail: (error: Error): never => { - throw error; - }, - }, - { mode: "reject", fail: (error: Error) => Promise.reject(error) }, - ])( - "cleans up the started child when polling sleep fails with $mode (#11648)", - async ({ fail }) => { - const child = { pid: 12345, unref: vi.fn() }; - const terminateProcessTree = vi.fn(); - const error = new Error("polling failed"); - await expect( - launchForwardService(target, { - spawnDetached: () => child, - isReachable: () => false, - terminateProcessTree, - sleep: () => fail(error), - }), - ).rejects.toBe(error); - expect(terminateProcessTree).toHaveBeenCalledExactlyOnceWith(child); - expect(child.unref).not.toHaveBeenCalled(); - }, - ); - - it("preserves a child error when polling and cleanup also fail (#11648)", async () => { - const child = Object.assign(new EventEmitter(), { pid: 12345, unref: vi.fn() }); - const childError = Object.assign(new Error("spawn failed"), { code: "EACCES" }); - const cleanupError = new Error("cleanup failed"); - const terminateProcessTree = vi.fn(() => { - throw cleanupError; - }); - await expect( - launchForwardService(target, { - spawnDetached: () => child, - isReachable: () => false, - terminateProcessTree, - sleep: () => { - child.emit("error", childError); - throw new Error("polling failed"); - }, - }), - ).rejects.toMatchObject({ errors: [childError, cleanupError] }); - expect(terminateProcessTree).toHaveBeenCalledExactlyOnceWith(child); - expect(child.unref).not.toHaveBeenCalled(); - }); - - it("detaches the OpenShell child and waits for its local port", async () => { - const unref = vi.fn(); - const verifyReady = vi.fn(() => expect(unref).not.toHaveBeenCalled()); - const spawnDetached = vi.fn(() => ({ unref })); - const terminateProcessTree = vi.fn(); - let probes = 0; - - await launchForwardService(target, { - isReachable: () => ++probes >= 3, - sleep: () => {}, - spawnDetached, - terminateProcessTree, - verifyReady, - timeoutMs: 1_000, - }); - - expect(spawnDetached).toHaveBeenCalledWith( - target.executable, - buildForwardServiceArgs(target), - expect.any(Object), - ); - expect(unref).toHaveBeenCalledOnce(); - expect(verifyReady).toHaveBeenCalledOnce(); - expect(terminateProcessTree).not.toHaveBeenCalled(); - }); - - it("uses the selected OpenShell configuration without exposing credentials (#11084)", async () => { - const spawnDetached = vi.fn>(() => ({ - unref: vi.fn(), - })); - - await launchForwardService(target, { - isReachable: vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true), - sleep: () => {}, - sourceEnvironment: { - HOME: "/tmp/isolated-home", - NVIDIA_INFERENCE_API_KEY: "secret-value", - OPENSHELL_GATEWAY: "nemoclaw", - OPENSHELL_GATEWAY_ENDPOINT: "https://hostile.invalid", - OPENSHELL_GATEWAY_INSECURE: "true", - OPENSHELL_LOCAL_TLS_DIR: "/tmp/selected-openshell-tls", - OPENSHELL_TOKEN: "hostile-token", - OPENSHELL_WORKSPACE: "default", - PATH: "/usr/bin", - XDG_CONFIG_HOME: "/tmp/selected-openshell-config", - }, - spawnDetached, - }); - - expect(spawnDetached).toHaveBeenCalledWith(target.executable, buildForwardServiceArgs(target), { - HOME: "/tmp/isolated-home", - OPENSHELL_GATEWAY: "nemoclaw", - OPENSHELL_LOCAL_TLS_DIR: "/tmp/selected-openshell-tls", - OPENSHELL_WORKSPACE: "default", - PATH: "/usr/bin", - XDG_CONFIG_HOME: "/tmp/selected-openshell-config", - }); - }); - - it("rejects explicit OpenShell selectors that disagree with the forward target", async () => { - const spawnDetached = vi.fn(); - - await expect( - launchForwardService(target, { - isReachable: () => false, - sourceEnvironment: { - OPENSHELL_GATEWAY: "nemoclaw-19080", - OPENSHELL_WORKSPACE: "default", - }, - spawnDetached, - }), - ).rejects.toThrow(/OPENSHELL_GATEWAY disagrees with its target/u); - expect(spawnDetached).not.toHaveBeenCalled(); - }); - - it("does not inherit ambient OpenShell selectors in a default forward child", async () => { - vi.stubEnv("OPENSHELL_GATEWAY", "hostile-gateway"); - vi.stubEnv("OPENSHELL_GATEWAY_ENDPOINT", "https://hostile.invalid"); - vi.stubEnv("OPENSHELL_GATEWAY_INSECURE", "true"); - vi.stubEnv("OPENSHELL_LOCAL_TLS_DIR", "/tmp/hostile-tls"); - vi.stubEnv("OPENSHELL_TOKEN", "hostile-token"); - vi.stubEnv("OPENSHELL_WORKSPACE", "hostile-workspace"); - const spawnDetached = vi.fn>(() => ({ - unref: vi.fn(), - })); - - await launchForwardService(target, { - isReachable: vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true), - sleep: () => {}, - spawnDetached, - }); - - const childEnvironment = spawnDetached.mock.calls[0]?.[2]; - expect(childEnvironment).not.toHaveProperty("OPENSHELL_GATEWAY"); - expect(childEnvironment).not.toHaveProperty("OPENSHELL_GATEWAY_ENDPOINT"); - expect(childEnvironment).not.toHaveProperty("OPENSHELL_GATEWAY_INSECURE"); - expect(childEnvironment).not.toHaveProperty("OPENSHELL_LOCAL_TLS_DIR"); - expect(childEnvironment).not.toHaveProperty("OPENSHELL_TOKEN"); - expect(childEnvironment).not.toHaveProperty("OPENSHELL_WORKSPACE"); - }); - - it("refuses an occupied port without launching or adopting its listener", async () => { - const spawnDetached = vi.fn(); - - await expect( - launchForwardService(target, { isReachable: () => true, spawnDetached }), - ).rejects.toThrow(/already occupied/u); - expect(spawnDetached).not.toHaveBeenCalled(); - }); - - it("does not adopt a foreign listener that wins the bind race after launch", async () => { - const child = { pid: detachedChildPid, unref: vi.fn() }; - const terminateProcessTree = vi.fn(); - const verifyReady = vi.fn(() => { - throw new Error("Forward ownership changed"); - }); - const isReachable = vi.fn().mockReturnValueOnce(false).mockReturnValue(true); - - await expect( - launchForwardService(target, { - isReachable, - spawnDetached: () => child, - terminateProcessTree, - verifyReady, - }), - ).rejects.toThrow(ForwardServiceStartupCleanupError); - expect(verifyReady).toHaveBeenCalledOnce(); - expect(terminateProcessTree).toHaveBeenCalledExactlyOnceWith(child); - expect(child.unref).not.toHaveBeenCalled(); - }); - - it.each([ - { - cleanup: "terminated", - terminate: () => {}, - remains: false, - expected: /Forward ownership changed/u, - }, - { - cleanup: "termination-failed", - terminate: () => { - throw new Error("Child remained alive"); - }, - remains: false, - expected: ForwardServiceStartupCleanupError, - }, - { - cleanup: "listener-remained", - terminate: () => {}, - remains: true, - expected: ForwardServiceStartupCleanupError, - }, - ])( - "rejects failed startup verification with cleanup $cleanup", - async ({ terminate, remains, expected }) => { - const child = { pid: detachedChildPid, unref: vi.fn() }; - const verificationError = new Error("Forward ownership changed"); - const terminateProcessTree = vi.fn(terminate); - const launch = async () => - await launchForwardService(target, { - isReachable: vi - .fn() - .mockReturnValueOnce(false) - .mockReturnValueOnce(true) - .mockReturnValue(remains), - spawnDetached: () => child, - terminateProcessTree, - verifyReady: () => { - throw verificationError; - }, - }); - - await expect(launch()).rejects.toThrow(expected); - expect(terminateProcessTree).toHaveBeenCalledExactlyOnceWith(child); - expect(child.unref).not.toHaveBeenCalled(); - }, - ); - - it("terminates a detached service that does not bind before the deadline", async () => { - let startupElapsed = 0; - const child = { pid: detachedChildPid, unref: vi.fn() }; - const terminateProcessTree = vi.fn(); - - await expect( - launchForwardService(target, { - isReachable: () => false, - now: () => startupElapsed, - spawnDetached: () => { - startupElapsed = 100; - return child; - }, - terminateProcessTree, - timeoutMs: 100, - }), - ).rejects.toThrow(/did not bind/u); - expect(terminateProcessTree).toHaveBeenCalledWith(child); - expect(child.unref).not.toHaveBeenCalled(); - }); - - it("fails closed when timeout cleanup cannot be proved", async () => { - let startupElapsed = 0; - const cleanupError = new Error("tree remained live"); - - await expect( - launchForwardService(target, { - isReachable: () => false, - now: () => startupElapsed, - spawnDetached: () => { - startupElapsed = 100; - return { pid: detachedChildPid, unref: vi.fn() }; - }, - terminateProcessTree: () => { - throw cleanupError; - }, - timeoutMs: 100, - }), - ).rejects.toThrow( - expect.objectContaining({ - errors: [ - expect.objectContaining({ message: expect.stringMatching(/did not bind/u) }), - cleanupError, - ], - name: ForwardServiceStartupCleanupError.name, - }), - ); - }); - - it("targets the exact detached process group on POSIX", () => { - const signalProcess = vi.fn(); - - terminateForwardServiceProcessTree( - { pid: detachedChildPid, unref: vi.fn() }, - { - platform: "linux", - processGroupHasRunnableMember: () => false, - signalProcess, - }, - ); - - expect(signalProcess).toHaveBeenCalledWith(-detachedChildPid, "SIGKILL"); - }); - - it("fails closed when POSIX process-group settlement is not proved", async () => { - let startupElapsed = 0; - const signalProcess = vi.fn(); - const now = vi.fn().mockReturnValueOnce(0).mockReturnValue(5_000); - - await expect( - launchForwardService(target, { - isReachable: () => false, - now: () => startupElapsed, - spawnDetached: () => { - startupElapsed = 100; - return { pid: detachedChildPid, unref: vi.fn() }; - }, - terminateProcessTree: (child) => - terminateForwardServiceProcessTree(child, { - now, - platform: "linux", - processGroupHasRunnableMember: () => true, - signalProcess, - sleep: () => {}, - }), - timeoutMs: 100, - }), - ).rejects.toThrow(expect.objectContaining({ name: ForwardServiceStartupCleanupError.name })); - expect(signalProcess).toHaveBeenCalledWith(-detachedChildPid, "SIGKILL"); - }); - - it("resolves Windows taskkill from SystemRoot while PATH is poisoned", () => { - const signalProcess = vi.fn(); - const taskkill = vi.fn(() => ({ status: 0 })); - const trustedTaskkill = "C:\\Windows\\System32\\taskkill.exe"; - - terminateForwardServiceProcessTree( - { pid: detachedChildPid, unref: vi.fn() }, - { - environment: { - PATH: "C:\\attacker-controlled", - SystemRoot: "C:\\Windows", - }, - isTrustedTaskkillExecutable: (executable) => executable === trustedTaskkill, - platform: "win32", - signalProcess, - taskkill, - }, - ); - - expect(taskkill).toHaveBeenCalledWith(trustedTaskkill, [ - "/PID", - String(detachedChildPid), - "/T", - "/F", - ]); - expect(signalProcess).not.toHaveBeenCalled(); - }); - - it("qualifies the real taskkill file and rejects a symlink with the default verifier", () => { - const root = realpathSync(mkdtempSync(path.join(os.tmpdir(), "nemoclaw-taskkill-trust-"))); - temporaryDirectories.push(root); - const executable = path.join(root, "taskkill.exe"); - const symlink = path.join(root, "taskkill-link.exe"); - writeFileSync(executable, "fixture"); - symlinkSync(executable, symlink); - - expect(isTrustedTaskkillExecutable(executable)).toBe(true); - expect(isTrustedTaskkillExecutable(symlink)).toBe(false); - expect(isTrustedTaskkillExecutable(path.join(root, "missing.exe"))).toBe(false); - }); - - it("fails closed when the trusted Windows taskkill executable is unavailable", () => { - const taskkill = vi.fn(() => ({ status: 0 })); - - expect(() => - terminateForwardServiceProcessTree( - { pid: detachedChildPid, unref: vi.fn() }, - { - environment: { - PATH: "C:\\attacker-controlled", - SystemRoot: "C:\\Windows", - }, - isTrustedTaskkillExecutable: () => false, - platform: "win32", - taskkill, - }, - ), - ).toThrow(/Trusted Windows taskkill executable is unavailable/u); - expect(taskkill).not.toHaveBeenCalled(); - }); - - it.each([undefined, "Windows", "\\\\attacker\\share", "C:\\Windows\\..\\poison"])( - "fails closed for an invalid Windows SystemRoot: %s", - (systemRoot) => { - const taskkill = vi.fn(() => ({ status: 0 })); - - expect(() => - terminateForwardServiceProcessTree( - { pid: detachedChildPid, unref: vi.fn() }, - { - environment: { - PATH: "C:\\attacker-controlled", - SystemRoot: systemRoot, - }, - isTrustedTaskkillExecutable: () => true, - platform: "win32", - taskkill, - }, - ), - ).toThrow(/Trusted Windows SystemRoot is unavailable/u); - expect(taskkill).not.toHaveBeenCalled(); - }, - ); - - it("fails closed when Windows process-tree termination is not proved", () => { - const noSuchProcess = Object.assign(new Error("not found"), { code: "ESRCH" }); - - expect(() => - terminateForwardServiceProcessTree( - { pid: detachedChildPid, unref: vi.fn() }, - { - environment: { SystemRoot: "C:\\Windows" }, - isTrustedTaskkillExecutable: () => true, - platform: "win32", - signalProcess: () => { - throw noSuchProcess; - }, - taskkill: () => ({ status: 1 }), - }, - ), - ).toThrow(/process-tree termination failed/u); - }); - - it.skipIf(process.platform === "win32")( - "kills a delayed listener and its detached process group before reporting timeout", - async () => { - const port = await availableLoopbackPort(); - const root = mkdtempSync(path.join(os.tmpdir(), "nemoclaw-forward-timeout-")); - temporaryDirectories.push(root); - const markerPath = path.join(root, "pids.json"); - const releasePath = path.join(root, "release"); - const bindDelayMs = 2_500; - const descendantScript = ` -const fs = require("node:fs"); -const net = require("node:net"); -const server = net.createServer(() => {}); -const releasePoll = setInterval(() => { - if (!fs.existsSync(${JSON.stringify(releasePath)})) return; - clearInterval(releasePoll); - setTimeout(() => server.listen(${String(port)}, "127.0.0.1"), ${String(bindDelayMs)}); -}, 10); -setInterval(() => {}, 1000); -`; - const leaderScript = ` -const fs = require("node:fs"); -const { spawn } = require("node:child_process"); -const descendant = spawn(process.execPath, ["-e", ${JSON.stringify(descendantScript)}], { - stdio: "ignore", -}); -fs.writeFileSync( - ${JSON.stringify(markerPath)}, - JSON.stringify({ leader: process.pid, descendant: descendant.pid }), -); -setInterval(() => {}, 1000); -`; - let spawned: ChildProcess | undefined; - let spawnedClose: Promise | undefined; - const unref = vi.fn(); - const runtimeTarget = { - ...target, - executable: process.execPath, - localPort: port, - targetPort: port, - }; - - let launchError: unknown; - try { - await launchForwardService(runtimeTarget, { - sleep: (milliseconds) => { - writeFileSync(releasePath, "ready"); - Atomics.wait(processSleepBuffer, 0, 0, milliseconds); - }, - spawnDetached: () => { - spawned = spawn(process.execPath, ["-e", leaderScript], { - detached: true, - stdio: "ignore", - }); - expect(spawned.pid).toBeTypeOf("number"); - const processGroup = spawned.pid!; - startedProcessGroups.push(processGroup); - spawnedClose = once(spawned, "close"); - const markerDeadline = Date.now() + 5_000; - while (!existsSync(markerPath) && Date.now() < markerDeadline) { - Atomics.wait(processSleepBuffer, 0, 0, 25); - } - expect(existsSync(markerPath)).toBe(true); - return { pid: processGroup, unref }; - }, - timeoutMs: 1_000, - }); - } catch (error) { - launchError = error; - } - - expect(launchError).toEqual( - expect.objectContaining({ message: expect.stringMatching(/did not bind/u) }), - ); - expect(existsSync(markerPath)).toBe(true); - expect(existsSync(releasePath)).toBe(true); - const pids = JSON.parse(readFileSync(markerPath, "utf8")) as { - descendant: number; - leader: number; - }; - await spawnedClose; - expect(spawned?.signalCode).toBe("SIGKILL"); - expect(await waitForExit(pids.leader)).toBe(true); - expect(await waitForExit(pids.descendant)).toBe(true); - await new Promise((resolve) => setTimeout(resolve, bindDelayMs)); - expect(probeLocalForwardListener(port, 100)).toBe(false); - expect(unref).not.toHaveBeenCalled(); - }, - 15_000, - ); -}); diff --git a/src/lib/adapters/openshell/forward-service.ts b/src/lib/adapters/openshell/forward-service.ts deleted file mode 100644 index 6f940c401af..00000000000 --- a/src/lib/adapters/openshell/forward-service.ts +++ /dev/null @@ -1,770 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { spawn, spawnSync } from "node:child_process"; -import { lstatSync, readFileSync, readdirSync, readlinkSync, realpathSync } from "node:fs"; -import path from "node:path"; -import { setImmediate as nextCheckPhase, setTimeout as delay } from "node:timers/promises"; - -import { isValidName } from "../../name-validation"; -import { buildOpenShellSubprocessEnv } from "./resolve-shared"; -import { probeLocalForwardListener } from "./local-forward-listener"; - -const START_TIMEOUT_MS = 30_000; -const POLL_INTERVAL_MS = 100; -const LISTENER_PROBE_TIMEOUT_MS = 1_000; -const PROCESS_TREE_TERMINATION_TIMEOUT_MS = 5_000; -const PROCESS_TREE_TERMINATION_POLL_MS = 25; -const sleepBuffer = new Int32Array(new SharedArrayBuffer(4)); - -export interface ForwardServiceTarget { - readonly executable: string; - readonly gatewayEndpoint: string; - readonly gatewayName: string; - readonly workspace: string; - readonly sandboxName: string; - readonly localHost: "127.0.0.1" | "0.0.0.0"; - readonly localPort: number; - readonly targetHost: "127.0.0.1"; - readonly targetPort: number; -} - -export interface ForwardServiceLaunchOptions { - readonly isReachable?: (port: number, timeoutMs?: number) => boolean; - readonly sleep?: (milliseconds: number) => void | Promise; - readonly sourceEnvironment?: NodeJS.ProcessEnv; - readonly spawnDetached?: ( - executable: string, - args: readonly string[], - environment: NodeJS.ProcessEnv, - ) => ForwardServiceChild; - readonly terminateProcessTree?: (child: ForwardServiceChild) => void; - /** Verify the bound forward before releasing the child from startup cleanup. */ - readonly verifyReady?: () => void; - readonly timeoutMs?: number; - /** Retain this child for transaction rollback after readiness succeeds. */ - readonly retainOwnership?: (ownership: ForwardServiceOwnership) => void; - readonly now?: () => number; -} - -export interface ForwardServiceOwnership { - readonly terminate: (assertCurrent?: () => void) => void | Promise; -} - -export interface ForwardServiceChild { - readonly pid?: number; - readonly exitCode?: number | null; - readonly signalCode?: NodeJS.Signals | null; - once?(event: "exit" | "error", listener: () => void): unknown; - unref(): void; - on?(event: "error", listener: (error: Error) => void): unknown; - on?( - event: "exit", - listener: (code: number | null, signal: NodeJS.Signals | null) => void, - ): unknown; -} - -export interface ForwardServiceProcessTreeTerminationDependencies { - readonly environment?: NodeJS.ProcessEnv; - readonly isTrustedTaskkillExecutable?: (executable: string) => boolean; - readonly now?: () => number; - readonly platform?: NodeJS.Platform; - readonly processGroupHasRunnableMember?: (pid: number) => boolean; - readonly signalProcess?: (pid: number, signal: NodeJS.Signals | number) => void; - readonly sleep?: (milliseconds: number) => void; - readonly taskkill?: ( - executable: string, - args: readonly string[], - ) => { readonly error?: Error; readonly status: number | null }; -} - -export class ForwardServiceEarlyExitError extends Error { - constructor( - target: ForwardServiceTarget, - readonly exitCode: number | null, - readonly signal: NodeJS.Signals | null, - ) { - super( - `OpenShell forward service exited before binding ${target.localHost}:${String(target.localPort)} (${signal ? `signal ${signal}` : `status ${String(exitCode)}`})`, - ); - } -} - -export class ForwardServiceStartupCleanupError extends AggregateError { - constructor(startupError: Error, cleanupError: unknown) { - super( - [startupError, cleanupError], - "OpenShell forward service startup cleanup could not be proved", - ); - this.name = "ForwardServiceStartupCleanupError"; - } -} - -type ForwardServiceOwnerProbe = ( - executable: string, - args: readonly string[], - timeoutMs?: number, -) => { status: number | null; stdout: string }; - -export interface ForwardServiceOwnerOptions { - /** Remaining time supplied by the operation that owns this verification. */ - readonly remainingMs?: (maximumMs: number) => number; - readonly platform?: NodeJS.Platform; - readonly probe?: ForwardServiceOwnerProbe; - readonly procRoot?: string; - readonly procWorkLimit?: number; -} - -const FORWARD_OWNER_PROBE_TIMEOUT_MS = 5_000; -const LINUX_PROC_WORK_LIMIT = 50_000; - -function isPort(value: unknown): value is number { - return Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= 65_535; -} - -function canonicalNemoClawGatewayPort(value: string): number | null { - if (value === "nemoclaw") return 8_080; - const match = /^nemoclaw-([1-9]\d{0,4})$/u.exec(value); - if (!match) return null; - const port = Number(match[1]); - return port >= 1 && port <= 65_535 && port !== 8_080 ? port : null; -} - -function managedGatewayEndpoint(gatewayName: string): string | null { - const port = canonicalNemoClawGatewayPort(gatewayName); - if (port === null) return null; - return port === 443 ? "https://127.0.0.1" : `https://127.0.0.1:${String(port)}`; -} - -function isAuthorityBoundGatewayEndpoint(endpoint: string, gatewayName: string): boolean { - let parsed: URL; - try { - parsed = new URL(endpoint); - } catch { - return false; - } - const gatewayPort = canonicalNemoClawGatewayPort(gatewayName); - let endpointPort = 80; - if (parsed.port) { - endpointPort = Number(parsed.port); - } else if (parsed.protocol === "https:") { - endpointPort = 443; - } - return ( - gatewayPort !== null && - endpoint === parsed.origin && - (parsed.protocol === "http:" || parsed.protocol === "https:") && - (parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1") && - !parsed.username && - !parsed.password && - endpointPort === gatewayPort - ); -} - -export function validateForwardServiceTarget(target: ForwardServiceTarget): ForwardServiceTarget { - if (!path.isAbsolute(target.executable) || target.executable.includes("\0")) { - throw new Error("OpenShell forward service executable must be an absolute path"); - } - if (canonicalNemoClawGatewayPort(target.gatewayName) === null) { - throw new Error("OpenShell forward service gateway must be a canonical NemoClaw gateway"); - } - if (!isAuthorityBoundGatewayEndpoint(target.gatewayEndpoint, target.gatewayName)) { - throw new Error( - "OpenShell forward service endpoint must be a bare loopback origin matching its gateway port", - ); - } - if (!isValidName(target.workspace)) { - throw new Error("OpenShell forward service workspace is invalid"); - } - if (!isValidName(target.sandboxName)) { - throw new Error("OpenShell forward service sandbox name is invalid"); - } - if (target.localHost !== "127.0.0.1" && target.localHost !== "0.0.0.0") { - throw new Error("OpenShell forward service local host must be IPv4 loopback or all interfaces"); - } - if (!isPort(target.localPort) || !isPort(target.targetPort)) { - throw new Error("OpenShell forward service ports must be between 1 and 65535"); - } - if (target.targetHost !== "127.0.0.1") { - throw new Error("OpenShell forward service target host must be IPv4 loopback"); - } - return target; -} - -export function createForwardServiceTarget( - target: Pick< - ForwardServiceTarget, - "executable" | "gatewayName" | "workspace" | "sandboxName" | "localHost" - > & - Partial>, - port: number, -): ForwardServiceTarget { - return validateForwardServiceTarget({ - ...target, - gatewayEndpoint: target.gatewayEndpoint ?? managedGatewayEndpoint(target.gatewayName) ?? "", - localPort: port, - targetHost: "127.0.0.1", - targetPort: port, - }); -} - -/** Build the direct ForwardTcp command introduced in OpenShell 0.0.106. */ -export function buildForwardServiceArgs(target: ForwardServiceTarget): string[] { - validateForwardServiceTarget(target); - return [ - "--gateway", - target.gatewayName, - "--gateway-endpoint", - target.gatewayEndpoint, - "--workspace", - target.workspace, - "forward", - "service", - target.sandboxName, - "--target-port", - String(target.targetPort), - "--target-host", - target.targetHost, - "--local", - `${target.localHost}:${String(target.localPort)}`, - ]; -} - -function trustedHostProbeExecutable(executable: string): string | null { - if (executable === "ps") return "/bin/ps"; - if (executable === "codesign" && process.platform === "darwin") return "/usr/bin/codesign"; - if (executable === "lsof") { - return process.platform === "darwin" ? "/usr/sbin/lsof" : "/usr/bin/lsof"; - } - return null; -} - -function captureProcess( - executable: string, - args: readonly string[], - timeoutMs = FORWARD_OWNER_PROBE_TIMEOUT_MS, -) { - const trustedExecutable = trustedHostProbeExecutable(executable); - if (!trustedExecutable) return { status: null, stdout: "" }; - const result = spawnSync(trustedExecutable, [...args], { - encoding: "utf8", - env: buildOpenShellSubprocessEnv(process.env), - timeout: timeoutMs, - }); - return { status: result.status, stdout: result.stdout ?? "" }; -} - -/** - * A loopback forward binds `127.0.0.1`, so only loopback and wildcard listeners - * actually contend for that socket. An `lsof -n` NAME reporting a listener bound - * solely to an external interface (e.g. a TLS reverse proxy fronting - * `CHAT_UI_URL` on the same port number) does not, and must not count toward - * forward ownership on the loopback bind (#11439). Wildcard binds (`*`, - * `0.0.0.0`) include loopback and always count; docker-proxy / loopback - * detection (#3260) is preserved. When the forward itself binds `0.0.0.0` - * (operator-opted remote exposure), every listener on the port contends, so no - * filtering applies. - */ -function lsofNameContendsWithLoopback(name: string): boolean { - // NAME is the bind endpoint, e.g. "127.0.0.1:18789", "*:18789", or - // "10.0.0.5:18789". Strip the trailing ":port" (IPv4 only here). - const address = name.replace(/:\d+$/u, ""); - if (address === "*" || address === "0.0.0.0") return true; - return address.startsWith("127."); -} - -function lsofListenerPids( - port: number, - probe: ForwardServiceOwnerProbe, - loopbackOnly: boolean, -): string[] | null { - if (!loopbackOnly) { - const result = probe("lsof", [`-ti4TCP:${String(port)}`, "-sTCP:LISTEN"]); - if (result.status === null) return null; - if (result.status !== 0) return []; - return [ - ...new Set( - result.stdout - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean), - ), - ]; - } - // Field-mode output pairs each PID (`p`) with the bind endpoint - // (`n:`) so external-interface-only listeners can be excluded - // before counting owners. - const result = probe("lsof", [`-i4TCP:${String(port)}`, "-sTCP:LISTEN", "-P", "-n", "-FpPn"]); - if (result.status === null) return null; - if (result.status !== 0) return []; - const pids = new Set(); - let currentPid: string | null = null; - for (const rawLine of result.stdout.split(/\r?\n/u)) { - const line = rawLine.trim(); - if (line.startsWith("p")) { - currentPid = /^p[1-9]\d*$/u.test(line) ? line.slice(1) : null; - } else if (line.startsWith("n") && currentPid !== null) { - if (lsofNameContendsWithLoopback(line.slice(1))) pids.add(currentPid); - } - } - return [...pids]; -} - -function linuxListenerPids( - port: number, - procRoot: string, - workLimit: number, - loopbackOnly: boolean, - assertBudget: () => void, -): string[] { - if (!Number.isSafeInteger(workLimit) || workLimit < 1) return []; - const portSuffix = `:${port.toString(16).padStart(4, "0").toUpperCase()}`; - const socketInodes = new Set(); - try { - assertBudget(); - for (const line of readFileSync(path.join(procRoot, "net", "tcp"), "utf8").split("\n")) { - assertBudget(); - const fields = line.trim().split(/\s+/u); - if ( - fields[3] === "0A" && - fields[1]?.toUpperCase().endsWith(portSuffix) && - /^\d+$/u.test(fields[9] ?? "") - ) { - // Local address is HEXIP:HEXPORT (little-endian IP). A loopback forward - // only contends with wildcard (`00000000`) or loopback (`7F......`) - // binds; an external-interface-only listener on the same port is - // ignored (#11439). - if (loopbackOnly) { - const hexIp = (fields[1] ?? "").split(":")[0]?.toUpperCase() ?? ""; - const isWildcard = hexIp === "00000000"; - // 127.0.0.0/8 → least-significant byte 0x7F in little-endian order. - const isLoopback = hexIp.length === 8 && hexIp.endsWith("7F"); - if (!isWildcard && !isLoopback) continue; - } - socketInodes.add(fields[9]); - } - } - } catch { - // A missing or unreadable IPv4 table cannot prove ownership. - } - if (socketInodes.size === 0) return []; - - const pids = new Set(); - let inspected = 0; - try { - assertBudget(); - for (const entry of readdirSync(procRoot, { withFileTypes: true })) { - assertBudget(); - if (!entry.isDirectory() || !/^[1-9]\d*$/u.test(entry.name)) continue; - if (++inspected > workLimit) return []; - try { - assertBudget(); - for (const descriptor of readdirSync(path.join(procRoot, entry.name, "fd"))) { - assertBudget(); - if (++inspected > workLimit) return []; - const link = readlinkSync(path.join(procRoot, entry.name, "fd", descriptor)); - const match = /^socket:\[(\d+)\]$/u.exec(link); - if (match && socketInodes.has(match[1])) { - pids.add(entry.name); - break; - } - } - } catch { - // Processes can exit or deny access while /proc is being inspected. - } - } - } catch { - return []; - } - return [...pids]; -} - -function listenerPids( - port: number, - platform: NodeJS.Platform, - procRoot: string, - procWorkLimit: number, - probe: ForwardServiceOwnerProbe, - loopbackOnly: boolean, - assertBudget: () => void, -): string[] { - const lsof = lsofListenerPids(port, probe, loopbackOnly); - if (lsof !== null || platform !== "linux") return lsof ?? []; - return linuxListenerPids(port, procRoot, procWorkLimit, loopbackOnly, assertBudget); -} - -function executableMatches(actualExecutable: string, expectedExecutable: string): boolean { - try { - return realpathSync(actualExecutable) === realpathSync(expectedExecutable); - } catch { - return false; - } -} - -function processExecutableMatches( - pid: string, - target: ForwardServiceTarget, - platform: NodeJS.Platform, - procRoot: string, - probe: ForwardServiceOwnerProbe, -): boolean { - if (platform === "linux") { - return executableMatches(path.join(procRoot, pid, "exe"), target.executable); - } - if (platform !== "darwin") return false; - // codesign reports the kernel-selected code hosting chain. Its first path is - // the main executable rather than the caller-controlled argv[0]. - const result = probe("codesign", ["-h", pid]); - if (result.status !== 0) return false; - const [hostingExecutable] = result.stdout - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean); - return hostingExecutable !== undefined && executableMatches(hostingExecutable, target.executable); -} - -/** Prove that the current listener is the exact direct ForwardTcp command. */ -export function isForwardServiceListenerOwner( - target: ForwardServiceTarget, - options: ForwardServiceOwnerOptions = {}, -): boolean { - validateForwardServiceTarget(target); - const platform = options.platform ?? process.platform; - const capture = options.probe ?? captureProcess; - const remaining = options.remainingMs; - const assertBudget = () => { - remaining?.(1); - }; - const probe: ForwardServiceOwnerProbe = remaining - ? (executable, args) => { - const result = capture(executable, args, remaining(FORWARD_OWNER_PROBE_TIMEOUT_MS)); - assertBudget(); - return result; - } - : capture; - const procRoot = options.procRoot ?? "/proc"; - const procWorkLimit = options.procWorkLimit ?? LINUX_PROC_WORK_LIMIT; - // A loopback forward only owns the loopback socket; an external-interface-only - // listener on the same port number (e.g. a reverse proxy fronting CHAT_UI_URL) - // must not defeat ownership. A remote-exposed (`0.0.0.0`) forward keeps the - // interface-agnostic count so any co-listener still blocks ownership (#11439). - const loopbackOnly = target.localHost === "127.0.0.1"; - const before = listenerPids( - target.localPort, - platform, - procRoot, - procWorkLimit, - probe, - loopbackOnly, - assertBudget, - ); - assertBudget(); - const [pid] = before; - if (before.length !== 1 || pid === undefined || !/^[1-9]\d*$/u.test(pid)) return false; - if (!processExecutableMatches(pid, target, platform, procRoot, probe)) return false; - const commandLine = probe("ps", ["-ww", "-p", pid, "-o", "args="]); - if (commandLine.status !== 0) return false; - const expected = [target.executable, ...buildForwardServiceArgs(target)].join(" "); - if (commandLine.stdout.trim() !== expected) return false; - const after = listenerPids( - target.localPort, - platform, - procRoot, - procWorkLimit, - probe, - loopbackOnly, - assertBudget, - ); - assertBudget(); - return after.length === 1 && after[0] === pid; -} - -function forwardServiceEnvironment( - source: NodeJS.ProcessEnv, - target: ForwardServiceTarget, - preserveExplicitRuntimeSelection: boolean, -): NodeJS.ProcessEnv { - const environment = buildOpenShellSubprocessEnv(source); - const configHome = source.XDG_CONFIG_HOME?.trim(); - if (configHome && path.isAbsolute(configHome)) environment.XDG_CONFIG_HOME = configHome; - if (!preserveExplicitRuntimeSelection) return environment; - for (const [name, expected] of [ - ["OPENSHELL_GATEWAY", target.gatewayName], - ["OPENSHELL_WORKSPACE", target.workspace], - ] as const) { - const actual = source[name]; - if (actual === undefined) continue; - if (actual !== expected) { - throw new Error(`OpenShell forward service ${name} disagrees with its target`); - } - environment[name] = actual; - } - const localTlsDir = source.OPENSHELL_LOCAL_TLS_DIR; - if (localTlsDir !== undefined) { - if (localTlsDir.includes("\0") || !path.isAbsolute(localTlsDir)) { - throw new Error("OpenShell forward service local TLS directory is invalid"); - } - environment.OPENSHELL_LOCAL_TLS_DIR = localTlsDir; - } - return environment; -} - -function noSuchProcess(error: unknown): boolean { - return (error as NodeJS.ErrnoException | undefined)?.code === "ESRCH"; -} - -function normalizedWindowsPath(value: string): string { - return path.win32.normalize(value.replace(/^\\\\\?\\/u, "")).toLowerCase(); -} - -export function isTrustedTaskkillExecutable(executable: string): boolean { - try { - const metadata = lstatSync(executable); - if (!metadata.isFile() || metadata.isSymbolicLink()) return false; - return ( - normalizedWindowsPath(realpathSync.native(executable)) === normalizedWindowsPath(executable) - ); - } catch { - return false; - } -} - -function resolveTrustedTaskkillExecutable( - environment: NodeJS.ProcessEnv, - verify: (executable: string) => boolean, -): string { - const systemRoot = environment.SystemRoot?.trim(); - if ( - !systemRoot || - systemRoot.includes("\0") || - !/^[a-z]:[\\/]/iu.test(systemRoot) || - systemRoot.split(/[\\/]/u).includes("..") - ) { - throw new Error("Trusted Windows SystemRoot is unavailable"); - } - const executable = path.win32.join(path.win32.normalize(systemRoot), "System32", "taskkill.exe"); - if (!verify(executable)) { - throw new Error("Trusted Windows taskkill executable is unavailable"); - } - return executable; -} - -function processGroupHasRunnableMember(pid: number): boolean { - const result = spawnSync("/bin/ps", ["-axo", "pgid=,stat="], { - encoding: "utf8", - env: buildOpenShellSubprocessEnv(process.env), - timeout: FORWARD_OWNER_PROBE_TIMEOUT_MS, - }); - if (result.error || result.status !== 0) { - throw new Error("OpenShell forward service process-group settlement probe failed", { - cause: result.error, - }); - } - return (result.stdout ?? "") - .split(/\r?\n/u) - .map((line) => /^\s*(\d+)\s+(\S+)/u.exec(line)) - .some((match) => Number(match?.[1]) === pid && !match?.[2]?.startsWith("Z")); -} - -/** Terminate only the detached child process group created for this forward launch. */ -export function terminateForwardServiceProcessTree( - child: ForwardServiceChild, - dependencies: ForwardServiceProcessTreeTerminationDependencies = {}, -): void { - const pid = child.pid; - if (!Number.isSafeInteger(pid) || Number(pid) <= 1 || pid === process.pid) { - throw new Error("OpenShell forward service child PID is unavailable"); - } - - const signalProcess = dependencies.signalProcess ?? process.kill.bind(process); - if ((dependencies.platform ?? process.platform) !== "win32") { - try { - signalProcess(-Number(pid), "SIGKILL"); - } catch (error) { - if (!noSuchProcess(error)) { - throw new Error("OpenShell forward service process-group termination failed", { - cause: error, - }); - } - } - const now = dependencies.now ?? (() => performance.now()); - const sleep = - dependencies.sleep ?? - ((milliseconds: number) => Atomics.wait(sleepBuffer, 0, 0, milliseconds)); - const hasRunnableMember = - dependencies.processGroupHasRunnableMember ?? processGroupHasRunnableMember; - const deadline = now() + PROCESS_TREE_TERMINATION_TIMEOUT_MS; - while (now() < deadline) { - if (!hasRunnableMember(Number(pid))) return; - sleep(PROCESS_TREE_TERMINATION_POLL_MS); - } - if (!hasRunnableMember(Number(pid))) return; - throw new Error("OpenShell forward service process group did not terminate"); - } - - const taskkill = - dependencies.taskkill ?? - ((executable: string, args: readonly string[]) => { - const result = spawnSync(executable, [...args], { - env: buildOpenShellSubprocessEnv(dependencies.environment ?? process.env), - stdio: "ignore", - timeout: FORWARD_OWNER_PROBE_TIMEOUT_MS, - windowsHide: true, - }); - return { error: result.error, status: result.status }; - }); - const taskkillExecutable = resolveTrustedTaskkillExecutable( - dependencies.environment ?? process.env, - dependencies.isTrustedTaskkillExecutable ?? isTrustedTaskkillExecutable, - ); - const result = taskkill(taskkillExecutable, ["/PID", String(pid), "/T", "/F"]); - if (!result.error && result.status === 0) return; - throw new Error("OpenShell forward service process-tree termination failed", { - cause: result.error, - }); -} - -/** Retain the spawned child and drain exit callbacks before using its process identity. */ -function retainForwardServiceChild( - child: ForwardServiceChild, - terminate: (child: ForwardServiceChild) => void, -): ForwardServiceOwnership { - const pid = child.pid; - let exited = false; - let terminated = false; - child.once?.("exit", () => { - exited = true; - }); - child.once?.("error", () => { - exited = true; - }); - return Object.freeze({ - terminate: async (assertCurrent?: () => void) => { - // libuv can reap several children before calling their exit handlers. A check-phase - // boundary lets that batch finish before we inspect this child, even when cleanup - // was requested from another child's exit callback or its promise continuation. - await nextCheckPhase(); - if (terminated) return; - assertCurrent?.(); - // Do not yield between this proof and termination. A POSIX child that exits in - // this stack remains unreaped; Windows retains the spawned process handle. - if ( - !child.once || - exited || - child.exitCode !== null || - child.signalCode !== null || - child.pid !== pid - ) { - throw new Error("OpenShell forward child lifetime can no longer be proved"); - } - terminate(child); - terminated = true; - }, - }); -} - -/** Launch one foreground OpenShell service forward as a detached host child. */ -export async function launchForwardService( - target: ForwardServiceTarget, - options: ForwardServiceLaunchOptions = {}, -): Promise { - validateForwardServiceTarget(target); - const now = options.now ?? (() => performance.now()); - const deadline = now() + (options.timeoutMs ?? START_TIMEOUT_MS); - const timeoutError = new Error( - `OpenShell forward service did not bind ${target.localHost}:${String(target.localPort)}`, - ); - const probeAllowance = () => { - const remaining = Math.floor(deadline - now()); - if (remaining <= 0) throw timeoutError; - return Math.min(LISTENER_PROBE_TIMEOUT_MS, remaining); - }; - const isReachable = options.isReachable ?? probeLocalForwardListener; - if (isReachable(target.localPort, probeAllowance())) { - throw new Error(`Host port ${String(target.localPort)} is already occupied`); - } - const spawnDetached = - options.spawnDetached ?? - ((executable, args, environment) => - spawn(executable, [...args], { detached: true, env: environment, stdio: "ignore" })); - const args = buildForwardServiceArgs(target); - const environment = forwardServiceEnvironment( - options.sourceEnvironment ?? process.env, - target, - options.sourceEnvironment !== undefined, - ); - // Recheck after the initial probe and command preparation, before creating a child. - probeAllowance(); - const child: ForwardServiceChild = spawnDetached(target.executable, args, environment); - - const ownership = options.retainOwnership - ? retainForwardServiceChild( - child, - options.terminateProcessTree ?? terminateForwardServiceProcessTree, - ) - : null; - - let childFailure: Error | undefined; - let notifyFailure: () => void = () => {}; - const failed = new Promise((resolve) => { - notifyFailure = resolve; - }); - // Spawn failures arrive asynchronously, including failures with no child PID. - // Keep the error listener installed after handoff so a late child error cannot - // become an uncaught EventEmitter error in the caller. - child.on?.("error", (error) => { - childFailure ??= error; - notifyFailure(); - }); - child.on?.("exit", (code, signal) => { - childFailure ??= new ForwardServiceEarlyExitError(target, code, signal); - notifyFailure(); - }); - let startupError = timeoutError; - try { - while (now() < deadline) { - if (childFailure) { - startupError = childFailure; - break; - } - if (isReachable(target.localPort, probeAllowance())) { - if (now() >= deadline) break; - options.verifyReady?.(); - if (now() >= deadline) break; - if (ownership) options.retainOwnership?.(ownership); - child.unref(); - return; - } - const sleepMs = Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - now())); - const controller = new AbortController(); - try { - await Promise.race([ - options.sleep - ? Promise.resolve(options.sleep(sleepMs)).then(() => - delay(0, undefined, { signal: controller.signal }), - ) - : delay(sleepMs, undefined, { signal: controller.signal }), - failed, - ]); - } finally { - controller.abort(); - } - } - } catch (error) { - startupError = error instanceof Error ? error : new Error(String(error)); - } - startupError = childFailure ?? startupError; - try { - // A failed spawn never created a process group to terminate. - if (child.pid !== undefined) { - (options.terminateProcessTree ?? terminateForwardServiceProcessTree)(child); - } - if (isReachable(target.localPort)) { - throw new Error("OpenShell forward service listener remained reachable after termination"); - } - } catch (cleanupError) { - throw new ForwardServiceStartupCleanupError(startupError, cleanupError); - } - // Keep the failed child referenced until Node observes its exit. - throw startupError; -} diff --git a/src/lib/dashboard-url-command.test.ts b/src/lib/dashboard-url-command.test.ts index 8a9ef2fb76f..8f64c34cc1c 100644 --- a/src/lib/dashboard-url-command.test.ts +++ b/src/lib/dashboard-url-command.test.ts @@ -33,12 +33,12 @@ describe("dashboard-url command helpers", () => { expect(() => buildDashboardUrl("", 18790)).toThrow(/token is required/); }); - it("prints only the URL in quiet mode", () => { + it("prints only the URL in quiet mode", async () => { const sinks = makeSinks(); const fetchToken = vi.fn(() => "secret-token"); const getSandbox = vi.fn(() => ({ agent: "openclaw", dashboardPort: 19000 })); - runDashboardUrlCommand( + await runDashboardUrlCommand( "alpha", { quiet: true }, { fetchToken, getSandbox, log: sinks.log, error: sinks.error }, @@ -50,10 +50,10 @@ describe("dashboard-url command helpers", () => { expect(sinks.err).toEqual([]); }); - it("prints the resolved access URL when provided", () => { + it("prints the resolved access URL when provided", async () => { const sinks = makeSinks(); - runDashboardUrlCommand( + await runDashboardUrlCommand( "alpha", { quiet: true }, { @@ -68,9 +68,9 @@ describe("dashboard-url command helpers", () => { expect(sinks.out).toEqual(["http://172.22.1.1:19000/#token=secret-token"]); }); - it("prints a human label and warning outside quiet mode", () => { + it("prints a human label and warning outside quiet mode", async () => { const sinks = makeSinks(); - runDashboardUrlCommand( + await runDashboardUrlCommand( "alpha", { quiet: false }, { @@ -86,9 +86,9 @@ describe("dashboard-url command helpers", () => { expect(sinks.err.join("\n")).toContain("Treat this URL like a password"); }); - it("appends an SSH port-forward hint when run over SSH (#5925)", () => { + it("appends an SSH port-forward hint when run over SSH (#5925)", async () => { const sinks = makeSinks(); - runDashboardUrlCommand( + await runDashboardUrlCommand( "alpha", { quiet: false }, { @@ -104,11 +104,11 @@ describe("dashboard-url command helpers", () => { expect(sinks.out).toContain(" ssh -L 18790:127.0.0.1:18790 spark@"); }); - it("appends the SSH hint in the plain-URL (session-auth) branch over SSH (#5925)", () => { + it("appends the SSH hint in the plain-URL (session-auth) branch over SSH (#5925)", async () => { const sinks = makeSinks(); const fetchToken = vi.fn(() => "should-not-fetch"); - runDashboardUrlCommand( + await runDashboardUrlCommand( "hermes", { quiet: false }, { @@ -128,9 +128,9 @@ describe("dashboard-url command helpers", () => { expect(sinks.out).toContain(" ssh -L 18790:127.0.0.1:18790 spark@"); }); - it("omits the SSH port-forward hint outside an SSH session", () => { + it("omits the SSH port-forward hint outside an SSH session", async () => { const sinks = makeSinks(); - runDashboardUrlCommand( + await runDashboardUrlCommand( "alpha", { quiet: false }, { @@ -145,9 +145,9 @@ describe("dashboard-url command helpers", () => { expect(sinks.out.join("\n")).not.toContain("Remote access"); }); - it("does not print the SSH hint in quiet mode even over SSH (#5925)", () => { + it("does not print the SSH hint in quiet mode even over SSH (#5925)", async () => { const sinks = makeSinks(); - runDashboardUrlCommand( + await runDashboardUrlCommand( "alpha", { quiet: true }, { @@ -162,11 +162,11 @@ describe("dashboard-url command helpers", () => { expect(sinks.out).toEqual(["http://127.0.0.1:18790/#token=secret-token"]); }); - it("prints a plain dashboard URL for session-auth non-OpenClaw agents without fetching a token", () => { + it("prints a plain dashboard URL for session-auth non-OpenClaw agents without fetching a token", async () => { const sinks = makeSinks(); const fetchToken = vi.fn(() => "should-not-fetch"); - runDashboardUrlCommand( + await runDashboardUrlCommand( "hermes", { quiet: true }, { @@ -183,57 +183,11 @@ describe("dashboard-url command helpers", () => { expect(sinks.err).toEqual([]); }); - it("prints the persisted external dashboard URL for a session-auth agent (#11439)", () => { - const sinks = makeSinks(); - - runDashboardUrlCommand( - "hermes", - { quiet: true }, - { - fetchToken: () => null, - getSandbox: () => ({ - agent: "hermes", - dashboardPort: 18789, - dashboardExternalUrl: "https://dash.example.com:18789", - }), - getAgentDashboardAuth: () => "session", - // A host access URL must not override the persisted external origin. - getAccessUrl: () => "http://172.22.1.1:18789", - log: sinks.log, - error: sinks.error, - }, - ); - - expect(sinks.out).toEqual(["https://dash.example.com:18789/"]); - }); - - it("embeds the token in the persisted external dashboard URL for token-auth agents (#11439)", () => { - const sinks = makeSinks(); - - runDashboardUrlCommand( - "agent-ui", - { quiet: true }, - { - fetchToken: () => "agent-token", - getSandbox: () => ({ - agent: "agent-ui", - dashboardPort: 19001, - dashboardExternalUrl: "https://dash.example.com:19001", - }), - getAgentDashboardAuth: () => "url_token", - log: sinks.log, - error: sinks.error, - }, - ); - - expect(sinks.out).toEqual(["https://dash.example.com:19001/#token=agent-token"]); - }); - - it("fetches a token for non-OpenClaw agents with token-auth dashboards", () => { + it("fetches a token for non-OpenClaw agents with token-auth dashboards", async () => { const sinks = makeSinks(); const fetchToken = vi.fn(() => "agent-token"); - runDashboardUrlCommand( + await runDashboardUrlCommand( "agent-ui", { quiet: true }, { @@ -249,10 +203,10 @@ describe("dashboard-url command helpers", () => { expect(sinks.out).toEqual(["http://127.0.0.1:19001/#token=agent-token"]); }); - it("fails when non-OpenClaw agent dashboard metadata cannot be resolved", () => { + it("fails when non-OpenClaw agent dashboard metadata cannot be resolved", async () => { const sinks = makeSinks(); - expect(() => + await expect( runDashboardUrlCommand( "agent-ui", { quiet: true }, @@ -264,15 +218,15 @@ describe("dashboard-url command helpers", () => { error: sinks.error, }, ), - ).toThrow(/Could not resolve dashboard metadata/); + ).rejects.toThrow(/Could not resolve dashboard metadata/); expect(sinks.out).toEqual([]); }); - it("explains terminal-runtime sandboxes have no dashboard instead of a token error (#5727)", () => { + it("explains terminal-runtime sandboxes have no dashboard instead of a token error (#5727)", async () => { const sinks = makeSinks(); const fetchToken = vi.fn(() => null); - expect(() => + await expect( runDashboardUrlCommand( "dcode-status", { quiet: false }, @@ -287,14 +241,16 @@ describe("dashboard-url command helpers", () => { error: sinks.error, }, ), - ).toThrow(/terminal runtime \(LangChain Deep Agents Code\) and does not have a dashboard/); + ).rejects.toThrow( + /terminal runtime \(LangChain Deep Agents Code\) and does not have a dashboard/, + ); expect(fetchToken).not.toHaveBeenCalled(); expect(sinks.out).toEqual([]); }); - it("fails when the token cannot be retrieved", () => { + it("fails when the token cannot be retrieved", async () => { const sinks = makeSinks(); - expect(() => + await expect( runDashboardUrlCommand( "alpha", { quiet: false }, @@ -305,7 +261,7 @@ describe("dashboard-url command helpers", () => { error: sinks.error, }, ), - ).toThrow(/Could not retrieve/); + ).rejects.toThrow(/Could not retrieve/); expect(sinks.out).toEqual([]); }); }); diff --git a/src/lib/inventory/index.ts b/src/lib/inventory/index.ts index e396817b562..8081289a42d 100644 --- a/src/lib/inventory/index.ts +++ b/src/lib/inventory/index.ts @@ -6,7 +6,7 @@ import { gatewayStartGuidance } from "../gateway-start-guidance"; import type { GatewayInference } from "../inference/config"; import { getActiveChannelIdsFromPlan } from "../messaging/plan-validation"; import type { GatewayOwnerDescription } from "../onboard/gateway-ownership"; -import { redactFull } from "../security/redact"; +import { redactFullWithUrls } from "../security/redact"; import { getSandboxEntryDisplayInference, isPendingReservationForSession, @@ -30,8 +30,6 @@ export interface SandboxEntry { messaging?: SandboxMessagingState | null; agent?: string | null; dashboardPort?: number | null; - /** Browser-facing external dashboard URL persisted from CHAT_UI_URL (#11439). */ - dashboardExternalUrl?: string | null; // Passthrough of the durable registry reservation marker so list and status // hide registrations that have not committed their lifecycle yet. pendingRouteReservation?: true; @@ -80,7 +78,7 @@ export interface IncompleteOnboarding { export interface ListSandboxesCommandDeps { recoverRegistryEntries: () => Promise; - getLiveInference: () => GatewayInference | null; + getLiveInference: () => GatewayInference | null | Promise; /** * Returns the last onboard session's sandbox name and step state. The * step state is needed to filter out phantom names from interrupted @@ -108,8 +106,6 @@ export interface SandboxInventoryRow { policies: string[]; agent: string; dashboardPort?: number | null; - /** Browser-facing external dashboard URL when CHAT_UI_URL set an external origin (#11439). */ - dashboardExternalUrl?: string | null; isDefault: boolean; activeSessionCount: number | null; // #5714: row recovered display-only from the live gateway. Its agent/GPU/ @@ -148,7 +144,7 @@ export interface GatewayHealth { export interface ShowStatusCommandDeps { listSandboxes: () => { sandboxes: SandboxEntry[]; defaultSandbox?: string | null }; - getLiveInference: () => GatewayInference | null; + getLiveInference: () => GatewayInference | null | Promise; showServiceStatus: (options: { sandboxName?: string }) => void; getServiceStatuses?: (options: { sandboxName?: string }) => StatusServiceRow[]; /** @@ -200,10 +196,12 @@ export interface StatusSandboxRow { openshellVersion: string | null; policies: string[]; agent: string; + configuredInference: { + provider: string | null; + model: string | null; + }; phase?: "pending" | "configuring" | "active"; dashboardPort?: number | null; - /** Browser-facing external dashboard URL when CHAT_UI_URL set an external origin (#11439). */ - dashboardExternalUrl?: string | null; isDefault: boolean; } @@ -229,20 +227,7 @@ export interface StatusReport { function safeStatusString(value: string | null | undefined): string | null { if (typeof value !== "string" || value.length === 0) return null; - return redactFull(value); -} - -/** - * Resolve the persisted browser-facing external dashboard URL for a sandbox, - * or null when none was configured (loopback-only dashboards report the - * `dashboardPort`-derived loopback form instead). Redaction is not applied: the - * external URL is an operator-facing address, not a secret (#11439). - */ -function resolveDashboardExternalUrl( - sandbox: Pick, -): string | null { - const external = sandbox.dashboardExternalUrl; - return typeof external === "string" && external.length > 0 ? external : null; + return redactFullWithUrls(value); } function projectIncompleteOnboarding( @@ -266,7 +251,7 @@ function projectIncompleteOnboarding( return null; } return { - name: reservation.name, + name: safeStatusString(reservation.name) ?? reservation.name, status: session.status, step: safeStatusString( session.failure?.step ?? session.lastStepStarted ?? session.lastCompletedStep, @@ -308,6 +293,40 @@ function resolveDisplayAgent(sandbox: SandboxEntry): string { return sandbox.recoveredFromGateway ? "unknown" : "openclaw"; } +type PublicSandboxFields = Omit; + +async function projectPublicSandboxFields( + sandbox: SandboxEntry, + inference: { model?: string | null; provider?: string | null }, + getPolicyPresets?: (sandboxName: string) => string[] | Promise, +): Promise { + const sandboxGpuEnabled = + typeof sandbox.sandboxGpuEnabled === "boolean" + ? sandbox.sandboxGpuEnabled + : sandbox.gpuEnabled === true; + const dashboardPort = + typeof sandbox.dashboardPort === "number" && Number.isFinite(sandbox.dashboardPort) + ? sandbox.dashboardPort + : null; + return { + name: safeStatusString(sandbox.name) ?? sandbox.name, + model: safeStatusString(inference.model), + provider: safeStatusString(inference.provider), + gpuEnabled: sandbox.gpuEnabled === true, + hostGpuDetected: sandbox.hostGpuDetected === true, + sandboxGpuEnabled, + sandboxGpuMode: safeStatusString(sandbox.sandboxGpuMode), + sandboxGpuDevice: safeStatusString(sandbox.sandboxGpuDevice), + openshellDriver: safeStatusString(sandbox.openshellDriver), + openshellVersion: safeStatusString(sandbox.openshellVersion), + policies: ((await getPolicyPresets?.(sandbox.name)) ?? []).map( + (policy) => safeStatusString(policy) ?? policy, + ), + agent: safeStatusString(resolveDisplayAgent(sandbox)) ?? "unknown", + ...(dashboardPort != null ? { dashboardPort } : {}), + }; +} + /** * Project a stored or recovered {@link SandboxEntry} into a display row, * resolving inference/GPU fields and marking gateway-recovered rows so unknown @@ -320,40 +339,25 @@ async function buildSandboxInventoryRow( getPolicyPresets?: (sandboxName: string) => string[] | Promise, ): Promise { const activeSessionCount = getActiveSessionCount ? getActiveSessionCount(sandbox.name) : null; - const sandboxGpuEnabled = - typeof sandbox.sandboxGpuEnabled === "boolean" - ? sandbox.sandboxGpuEnabled - : sandbox.gpuEnabled === true; const inference = getSandboxEntryDisplayInference(sandbox); + const publicFields = await projectPublicSandboxFields(sandbox, inference, getPolicyPresets); - return { - name: sandbox.name, - model: inference.model, - provider: inference.provider, - gpuEnabled: sandbox.gpuEnabled === true, - hostGpuDetected: sandbox.hostGpuDetected === true, - sandboxGpuEnabled, - sandboxGpuMode: safeStatusString(sandbox.sandboxGpuMode || null), - sandboxGpuDevice: safeStatusString(sandbox.sandboxGpuDevice || null), - openshellDriver: safeStatusString(sandbox.openshellDriver || null), - openshellVersion: safeStatusString(sandbox.openshellVersion || null), - policies: (await getPolicyPresets?.(sandbox.name)) ?? [], - agent: resolveDisplayAgent(sandbox), - ...(sandbox.dashboardPort != null ? { dashboardPort: sandbox.dashboardPort } : {}), - ...(resolveDashboardExternalUrl(sandbox) != null - ? { dashboardExternalUrl: resolveDashboardExternalUrl(sandbox) } - : {}), + const row: SandboxInventoryRow = { + ...publicFields, isDefault: sandbox.name === defaultSandbox, activeSessionCount, ...(sandbox.recoveredFromGateway ? { recoveredFromGateway: true } : {}), - ...(sandbox.recoveredFromGateway ? { livePhase: sandbox.livePhase ?? null } : {}), + ...(sandbox.recoveredFromGateway + ? { livePhase: safeStatusString(sandbox.livePhase ?? null) } + : {}), }; + return row; } -export async function getSandboxInventory( +async function buildSandboxInventory( deps: ListSandboxesCommandDeps, + recovery: RecoveryResult, ): Promise { - const recovery = await deps.recoverRegistryEntries(); const resolvedDefault = resolveDefaultSandboxName(() => ({ defaultSandbox: recovery.defaultSandbox ?? null })) ?? null; const lastSession = deps.loadLastSession(); @@ -380,12 +384,12 @@ export async function getSandboxInventory( ); return { schemaVersion: 1, - defaultSandbox: resolvedDefault, + defaultSandbox: safeStatusString(resolvedDefault), recovery: { recoveredFromSession: recovery.recoveredFromSession === true, recoveredFromGateway: recovery.recoveredFromGateway || 0, }, - lastOnboardedSandbox, + lastOnboardedSandbox: safeStatusString(lastOnboardedSandbox), incompleteOnboarding, // Pending rows are internal lifecycle state. They remain readable by their // recovery authority, but must not appear as completed sandboxes. @@ -393,6 +397,17 @@ export async function getSandboxInventory( }; } +export async function getSandboxInventory( + deps: ListSandboxesCommandDeps, +): Promise { + return buildSandboxInventory(deps, await deps.recoverRegistryEntries()); +} + +interface InventoryRouteDrift { + model: boolean; + provider: boolean; +} + /** * Render the `nemoclaw list` output. For the default sandbox (the one the * cluster-wide gateway is currently serving) the live gateway `model`/ @@ -408,6 +423,7 @@ export function renderSandboxInventoryText( inventory: SandboxInventoryResult, log: (message?: string) => void = console.log, liveInference: GatewayInference | null = null, + routeDrift?: InventoryRouteDrift, ): void { if (inventory.sandboxes.length === 0) { log(""); @@ -445,20 +461,17 @@ export function renderSandboxInventoryText( } log(" Sandboxes:"); for (const sandbox of inventory.sandboxes) { - const useLive = sandbox.isDefault && liveInference; + const liveModel = sandbox.isDefault ? safeStatusString(liveInference?.model) : null; + const liveProvider = sandbox.isDefault ? safeStatusString(liveInference?.provider) : null; const def = sandbox.isDefault ? " *" : ""; - const model = (useLive && liveInference.model) || sandbox.model || "unknown"; - const provider = (useLive && liveInference.provider) || sandbox.provider || "unknown"; - const modelDrifted = !!( - useLive && - liveInference.model && - liveInference.model !== sandbox.model - ); - const providerDrifted = !!( - useLive && - liveInference.provider && - liveInference.provider !== sandbox.provider - ); + const model = liveModel || sandbox.model || "unknown"; + const provider = liveProvider || sandbox.provider || "unknown"; + const modelDrifted = sandbox.isDefault + ? (routeDrift?.model ?? !!(liveModel && liveModel !== sandbox.model)) + : false; + const providerDrifted = sandbox.isDefault + ? (routeDrift?.provider ?? !!(liveProvider && liveProvider !== sandbox.provider)) + : false; // #5714: a gateway-recovered row's GPU state is unknown — the gateway // sandbox list does not expose it — so don't assert "CPU sandbox" (which // would mislead DGX users whose GPU sandbox's registry entry was lost). @@ -484,9 +497,7 @@ export function renderSandboxInventoryText( if (providerDrifted) parts.push(`provider=${sandbox.provider || "unknown"}`); log(` (live OpenShell gateway differs from onboarded: ${parts.join(", ")})`); } - if (sandbox.dashboardExternalUrl != null) { - log(` dashboard: ${sandbox.dashboardExternalUrl}`); - } else if (sandbox.dashboardPort != null) { + if (sandbox.dashboardPort != null) { log(` dashboard: http://127.0.0.1:${sandbox.dashboardPort}/`); } } @@ -497,9 +508,23 @@ export function renderSandboxInventoryText( export async function listSandboxesCommand(deps: ListSandboxesCommandDeps): Promise { const log = deps.log ?? console.log; - const inventory = await getSandboxInventory(deps); - const liveInference = inventory.sandboxes.length > 0 ? deps.getLiveInference() : null; - renderSandboxInventoryText(inventory, log, liveInference); + const recovery = await deps.recoverRegistryEntries(); + const inventory = await buildSandboxInventory(deps, recovery); + const liveInference = inventory.sandboxes.length > 0 ? await deps.getLiveInference() : null; + const resolvedDefault = resolveDefaultSandboxName(() => ({ + defaultSandbox: recovery.defaultSandbox ?? null, + })); + const defaultEntry = recovery.sandboxes.find((sandbox) => sandbox.name === resolvedDefault); + const storedInference = defaultEntry ? getSandboxEntryDisplayInference(defaultEntry) : null; + const routeDrift = liveInference + ? { + model: !!(liveInference.model && liveInference.model !== storedInference?.model), + provider: !!( + liveInference.provider && liveInference.provider !== storedInference?.provider + ), + } + : undefined; + renderSandboxInventoryText(inventory, log, liveInference, routeDrift); } async function buildStatusSandboxRow( @@ -512,35 +537,24 @@ async function buildStatusSandboxRow( const isDefault = sandbox.name === defaultSandbox; const liveModel = isDefault ? liveInference?.model : null; const liveProvider = isDefault ? liveInference?.provider : null; - const inference = getSandboxEntryDisplayInference(sandbox); - const dashboardPort = - typeof sandbox.dashboardPort === "number" && Number.isFinite(sandbox.dashboardPort) - ? sandbox.dashboardPort - : null; - const sandboxGpuEnabled = - typeof sandbox.sandboxGpuEnabled === "boolean" - ? sandbox.sandboxGpuEnabled - : sandbox.gpuEnabled === true; + const configuredInference = getSandboxEntryDisplayInference(sandbox); + // Preserve schema-version-1 `model`/`provider` semantics for existing + // consumers while publishing this sandbox's recorded route explicitly. + const publicFields = await projectPublicSandboxFields( + sandbox, + { + model: liveModel || configuredInference.model, + provider: liveProvider || configuredInference.provider, + }, + getPolicyPresets, + ); return { - name: safeStatusString(sandbox.name) || sandbox.name, - model: safeStatusString(liveModel || inference.model), - provider: safeStatusString(liveProvider || inference.provider), - gpuEnabled: sandbox.gpuEnabled === true, - hostGpuDetected: sandbox.hostGpuDetected === true, - sandboxGpuEnabled, - sandboxGpuMode: safeStatusString(sandbox.sandboxGpuMode || null), - sandboxGpuDevice: safeStatusString(sandbox.sandboxGpuDevice || null), - openshellDriver: safeStatusString(sandbox.openshellDriver || null), - openshellVersion: safeStatusString(sandbox.openshellVersion || null), - policies: ((await getPolicyPresets?.(sandbox.name)) ?? []).map( - (policy) => safeStatusString(policy) || policy, - ), - agent: redactFull(resolveDisplayAgent(sandbox)), + ...publicFields, + configuredInference: { + provider: safeStatusString(configuredInference.provider), + model: safeStatusString(configuredInference.model), + }, ...(portablePhase ? { phase: portablePhase } : {}), - ...(dashboardPort != null ? { dashboardPort } : {}), - ...(resolveDashboardExternalUrl(sandbox) != null - ? { dashboardExternalUrl: resolveDashboardExternalUrl(sandbox) } - : {}), isDefault, }; } @@ -614,7 +628,8 @@ export async function getStatusReport(deps: ShowStatusCommandDeps): Promise 0 && !hasHermesPortable ? deps.getLiveInference() : null; + const liveInference = + sandboxes.length > 0 && !hasHermesPortable ? await deps.getLiveInference() : null; const gatewayHealth = deps.getGatewayHealth && sandboxes.length > 0 && !hasHermesPortable ? await deps.getGatewayHealth() @@ -662,6 +677,10 @@ export async function getStatusReport(deps: ShowStatusCommandDeps): Promise { const log = deps.log ?? console.log; @@ -689,36 +708,45 @@ export async function showStatusCommand(deps: ShowStatusCommandDeps): Promise 0) { - const live = hasHermesPortable ? null : deps.getLiveInference(); + const live = hasHermesPortable ? null : await deps.getLiveInference(); log(" Sandboxes:"); for (const sb of sandboxes) { const isDefault = sb.name === resolvedDefault; const def = isDefault ? " *" : ""; // Prefer the live gateway model for the default sandbox so `status` // agrees with `openshell inference get` (#2369). - const liveModel = isDefault && live ? live.model : null; - const liveProvider = isDefault && live ? live.provider : null; + const liveModel = safeStatusString(isDefault && live ? live.model : null); + const liveProvider = safeStatusString(isDefault && live ? live.provider : null); const inference = getSandboxEntryDisplayInference(sb); - const model = liveModel || inference.model; - const provider = liveProvider || inference.provider; + const storedModel = safeStatusString(inference.model); + const storedProvider = safeStatusString(inference.provider); + const liveRouteDrifted = Boolean( + (liveModel && liveModel !== storedModel) || + (liveProvider && liveProvider !== storedProvider), + ); + const model = liveModel || storedModel; + const name = safeStatusString(sb.name) ?? "unknown"; const portSuffix = sb.dashboardPort != null ? ` :${sb.dashboardPort}` : ""; - log(` ${sb.name}${def}${model ? ` (${model})` : ""}${portSuffix}`); - const externalDashboardUrl = resolveDashboardExternalUrl(sb); - if (externalDashboardUrl) { - log(` Dashboard URL: ${externalDashboardUrl}`); - } + log(` ${name}${def}${model ? ` (${model})` : ""}${portSuffix}`); const portablePhase = portablePhases.get(sb.name); if (portablePhase) log(` agent: hermes phase: ${portablePhase}`); - if (isDefault && liveModel && liveModel !== inference.model) { - log(` (onboarded: ${inference.model || "unknown"})`); + if (isDefault && live?.model && live.model !== inference.model) { + log(` (onboarded: ${storedModel || "unknown"})`); } // #2604: surface the configured Inference (provider/model) and the // SSH-session count as labeled fields. Bare `nemoclaw status` previously // only had the model in parens above — users had to run // `nemoclaw status` to see provider and session state. - if (provider || model) { - const parts = [provider, model].filter(Boolean).join(" / "); - log(` Inference (configured): ${parts}`); + // #11412: this line is documented and labeled "configured", so it must + // always show this sandbox's own recorded provider/model, never the + // shared live gateway route (that would silently show one sandbox's + // "configured" line as another sandbox's route after a stop/start + // realigns the one shared route). + const configuredParts = [storedProvider ?? "unknown", storedModel ?? "unknown"]; + log(` Inference (configured): ${configuredParts.join(" / ")}`); + if (liveRouteDrifted) { + const parts = [liveProvider, liveModel].filter(Boolean).join(" / "); + log(` Inference (live): ${parts}`); } if (deps.getActiveSessionCount && !portablePhase) { const count = deps.getActiveSessionCount(sb.name); diff --git a/src/lib/onboard/dashboard-port.test.ts b/src/lib/onboard/dashboard-port.test.ts index 3107ebf83fe..39bce763270 100644 --- a/src/lib/onboard/dashboard-port.test.ts +++ b/src/lib/onboard/dashboard-port.test.ts @@ -10,23 +10,73 @@ import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import type { + OpenShellForwardAdapter, + OpenShellForwardObservation, +} from "../adapters/openshell/forward"; import { withGatewayRouteMutationLock } from "../inference/gateway-route-mutation-lock"; import { + createOpenShellForwardPortObserver, createDashboardPortScopedSandboxEntryPoints, type DashboardPortReservationScope, - findAvailableDashboardPort, - findDashboardForwardOwner, + findAvailableDashboardPortFromObservations, getRegistryOccupiedDashboardPorts, - lsofOutputBlocksLoopbackBind, + hasExplicitDashboardPortOverride, preflightDashboardPortRangeAvailability, reserveCreateSandboxDashboardPort, reserveDashboardPort, reservePortAfterOwnedForwardDelete, - resolveCreateSandboxDashboardPort, + resolveCreateSandboxDashboardPortFromObservations, withDashboardPortReservationLock, withDashboardPortReservationScope, } from "./dashboard-port"; +describe("dashboard-port override intent", () => { + it.each([ + [undefined, false], + ["", false], + [" \t ", false], + ["18789", true], + [" 18789 ", true], + ] as const)("classifies %j as explicit=%s", (value, expected) => { + expect(hasExplicitDashboardPortOverride(value)).toBe(expected); + }); +}); + +function forwardObservation( + sandboxName: string, + port: number, + state: "absent" | "foreign" | "owned" | "stale" | "indeterminate", +): OpenShellForwardObservation { + const forward = { + gatewayEndpoint: "https://127.0.0.1:9090", + gatewayName: "nemoclaw-9090", + workspace: "default", + sandboxName, + localHost: "127.0.0.1" as const, + port, + }; + return state === "indeterminate" + ? { + state, + forward, + error: { + kind: "ownership", + message: "NemoClaw could not prove OpenShell forward ownership.", + }, + } + : { state, forward }; +} + +function observePorts( + sandboxName: string, + states: ReadonlyMap[2]> = new Map(), +) { + return vi.fn(async (ports: readonly number[]) => + ports.map((port) => forwardObservation(sandboxName, port, states.get(port) ?? "absent")), + ); +} + async function listenOnLoopback(port: number): Promise { const server = createServer(); await new Promise((resolve, reject) => { @@ -58,150 +108,179 @@ async function unusedLoopbackPort(): Promise { return address.port; } -describe("lsofOutputBlocksLoopbackBind interface-specific loopback probe (#11439)", () => { - const loopback = (port: number) => - `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nx 1 u 3u IPv4 1 0t0 TCP 127.0.0.1:${port} (LISTEN)`; - const external = (port: number) => - `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nsocat 1 u 6u IPv4 1 0t0 TCP 10.63.144.115:${port} (LISTEN)`; - const wildcard = (port: number) => - `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nx 1 u 3u IPv4 1 0t0 TCP *:${port} (LISTEN)`; +describe("typed OpenShell dashboard-port observation", () => { + it("checks gateway authority once for each multi-port batch (#11963)", async () => { + const observeForwards = vi.fn( + async ({ assertCurrent, forwards }) => { + expect(assertCurrent).toBeUndefined(); + return forwards.map((forward) => ({ state: "absent" as const, forward })); + }, + ); + const assertCurrent = vi.fn(async () => undefined); + const observer = createOpenShellForwardPortObserver({ + adapter: { observeForwards }, + assertCurrent, + forwardForPort: (port) => ({ + gatewayEndpoint: "https://127.0.0.1:9090", + gatewayName: "nemoclaw-9090", + workspace: "default", + sandboxName: "cursor", + localHost: "0.0.0.0", + port, + }), + }); - it("treats an external-interface-only listener as non-blocking for the loopback bind", () => { - expect(lsofOutputBlocksLoopbackBind(external(18789), 18789)).toBe(false); - }); + await expect(observer([18789, 18790])).resolves.toEqual([ + { + state: "absent", + forward: { + gatewayEndpoint: "https://127.0.0.1:9090", + gatewayName: "nemoclaw-9090", + workspace: "default", + sandboxName: "cursor", + localHost: "0.0.0.0", + port: 18789, + }, + }, + { + state: "absent", + forward: { + gatewayEndpoint: "https://127.0.0.1:9090", + gatewayName: "nemoclaw-9090", + workspace: "default", + sandboxName: "cursor", + localHost: "0.0.0.0", + port: 18790, + }, + }, + ]); + expect(observeForwards).toHaveBeenCalledOnce(); + expect(assertCurrent).toHaveBeenCalledOnce(); - it("treats a loopback listener as blocking", () => { - expect(lsofOutputBlocksLoopbackBind(loopback(18789), 18789)).toBe(true); + await observer([18789, 18790]); + expect(assertCurrent).toHaveBeenCalledTimes(2); }); - it("treats a wildcard 0.0.0.0 or star listener as blocking, preserving docker-proxy detection (#3260)", () => { - expect(lsofOutputBlocksLoopbackBind(wildcard(18789), 18789)).toBe(true); - expect( - lsofOutputBlocksLoopbackBind("x 1 u 3u IPv4 1 0t0 TCP 0.0.0.0:18789 (LISTEN)", 18789), - ).toBe(true); - }); + it("rejects a batch when gateway authority is stale before collection (#11963)", async () => { + const observeForwards = vi.fn( + async ({ forwards }) => forwards.map((forward) => ({ state: "absent" as const, forward })), + ); + const assertCurrent = vi.fn(async () => { + throw new Error("gateway authority changed"); + }); + const observer = createOpenShellForwardPortObserver({ + adapter: { observeForwards }, + assertCurrent, + forwardForPort: (port) => ({ + gatewayEndpoint: "https://127.0.0.1:9090", + gatewayName: "nemoclaw-9090", + workspace: "default", + sandboxName: "cursor", + localHost: "127.0.0.1", + port, + }), + }); - it("treats an IPv6 loopback listener as blocking", () => { - expect( - lsofOutputBlocksLoopbackBind("x 1 u 3u IPv6 1 0t0 TCP [::1]:18789 (LISTEN)", 18789), - ).toBe(true); + await expect(observer([18789, 18790])).rejects.toThrow(/gateway authority changed/); + expect(observeForwards).toHaveBeenCalledOnce(); + expect(assertCurrent).toHaveBeenCalledOnce(); }); - it("only matches the requested port", () => { - expect(lsofOutputBlocksLoopbackBind(loopback(18790), 18789)).toBe(false); - }); + it("rejects a batch when gateway authority changes during collection (#11963)", async () => { + let observationComplete = false; + const observeForwards = vi.fn( + async ({ forwards }) => { + observationComplete = true; + return forwards.map((forward) => ({ state: "absent" as const, forward })); + }, + ); + const assertCurrent = vi.fn(async () => { + expect(observationComplete).toBe(true); + throw new Error("gateway authority changed"); + }); + const observer = createOpenShellForwardPortObserver({ + adapter: { observeForwards }, + assertCurrent, + forwardForPort: (port) => ({ + gatewayEndpoint: "https://127.0.0.1:9090", + gatewayName: "nemoclaw-9090", + workspace: "default", + sandboxName: "cursor", + localHost: "127.0.0.1", + port, + }), + }); - it("returns false for empty or missing output", () => { - expect(lsofOutputBlocksLoopbackBind("", 18789)).toBe(false); - expect(lsofOutputBlocksLoopbackBind(null, 18789)).toBe(false); - expect(lsofOutputBlocksLoopbackBind(undefined, 18789)).toBe(false); + await expect(observer([18789, 18790])).rejects.toThrow(/gateway authority changed/); + expect(observeForwards).toHaveBeenCalledOnce(); + expect(assertCurrent).toHaveBeenCalledOnce(); }); - it("ignores non-LISTEN rows", () => { + it.each(["owned", "stale"] as const)("reuses an exact %s forward", (state) => { expect( - lsofOutputBlocksLoopbackBind( - "x 1 u 3u IPv4 1 0t0 TCP 127.0.0.1:18789->10.0.0.2:5000 (ESTABLISHED)", - 18789, - ), - ).toBe(false); - }); -}); - -describe("findDashboardForwardOwner", () => { - it("parses openshell forward list column format (#2169)", () => { - const forwardList = [ - "SANDBOX BIND PORT PID STATUS", - "test21 127.0.0.1 18789 42101 active", - "other 127.0.0.1 18790 42102 active", - "stopped 127.0.0.1 18792 42103 stopped", - "ansi 127.0.0.1 18793 42104 \u001b[32mrunning\u001b[0m", - ].join("\n"); - - assert.equal(findDashboardForwardOwner(forwardList, "18789"), "test21"); - assert.equal(findDashboardForwardOwner(forwardList, "18790"), "other"); - assert.equal(findDashboardForwardOwner(forwardList, "18791"), null); - assert.equal(findDashboardForwardOwner(forwardList, "18792"), null); - assert.equal(findDashboardForwardOwner(forwardList, "18793"), "ansi"); - assert.equal(findDashboardForwardOwner("", "18789"), null); - assert.equal(findDashboardForwardOwner(null, "18789"), null); - assert.equal(findDashboardForwardOwner(undefined, "18789"), null); - const falsePositive = "sandbox18789 127.0.0.1 42001 9999 active"; - assert.equal(findDashboardForwardOwner(falsePositive, "18789"), null); - }); -}); - -describe("findAvailableDashboardPort port-conflict detection (#3260)", () => { - const stubBound = (...bound: number[]) => { - const set = new Set(bound); - return (port: number) => set.has(port); - }; - - it("returns the preferred port when no forward owns it and the host says it is free", () => { - assert.equal(findAvailableDashboardPort("cursor", 18789, "", stubBound()), 18789); - }); - - it("skips the preferred port when host reports it bound and falls through to the range scan", () => { - assert.equal(findAvailableDashboardPort("cursor", 18789, "", stubBound(18789)), 18790); + findAvailableDashboardPortFromObservations("cursor", 18789, [ + forwardObservation("cursor", 18789, state), + ]), + ).toBe(18789); }); - it("skips ports owned by other sandboxes and host-bound ports together", () => { - const forwardList = [ - "SANDBOX BIND PORT PID STATUS", - "alpha 127.0.0.1 18789 111 running", - ].join("\n"); - assert.equal(findAvailableDashboardPort("cursor", 18789, forwardList, stubBound(18790)), 18791); + it("skips a preferred port with foreign ownership", () => { + expect( + findAvailableDashboardPortFromObservations("cursor", 18789, [ + forwardObservation("cursor", 18789, "foreign"), + forwardObservation("cursor", 18790, "absent"), + ]), + ).toBe(18790); }); - it("returns the preferred port when this sandbox already owns it", () => { - const forwardList = [ - "SANDBOX BIND PORT PID STATUS", - "cursor 127.0.0.1 18789 111 running", - ].join("\n"); - assert.equal(findAvailableDashboardPort("cursor", 18789, forwardList, stubBound(18789)), 18789); + it("blocks allocation when ownership is indeterminate", () => { + expect(() => + findAvailableDashboardPortFromObservations("cursor", 18789, [ + forwardObservation("cursor", 18789, "indeterminate"), + forwardObservation("cursor", 18790, "absent"), + ]), + ).toThrow(/could not prove OpenShell forward ownership/i); }); - it("throws when every port in the range is occupied by other sandboxes", () => { - const lines = ["SANDBOX BIND PORT PID STATUS"]; - for (let p = 18789; p <= 18799; p++) { - lines.push(`other${p} 127.0.0.1 ${p} ${p} running`); - } - assert.throws( - () => findAvailableDashboardPort("cursor", 18789, lines.join("\n"), stubBound()), - /All dashboard ports in range 18789-18799 are occupied/, + it("treats missing observations as unverified instead of absent", () => { + expect(() => findAvailableDashboardPortFromObservations("cursor", 18789, [])).toThrow( + /unverified OpenShell forward ownership/, ); }); - it("includes host-bound ports in the exhaustion error so users know what's blocking them", () => { - const allBound = new Set(); - for (let p = 18789; p <= 18799; p++) allBound.add(p); - assert.throws( - () => findAvailableDashboardPort("cursor", 18789, "", (p) => allBound.has(p)), - /18789 → non-OpenShell host listener/, - ); - }); + it("rejects an adapter response that does not match its requested identities", async () => { + const assertCurrent = vi.fn(async () => undefined); + const observer = createOpenShellForwardPortObserver({ + adapter: { + observeForwards: async () => [forwardObservation("other", 18789, "absent")], + }, + assertCurrent, + forwardForPort: (port) => ({ + gatewayEndpoint: "https://127.0.0.1:9090", + gatewayName: "nemoclaw-9090", + workspace: "default", + sandboxName: "cursor", + localHost: "127.0.0.1", + port, + }), + }); - it("probes each port at most once even when the preferred port is in the range", () => { - const seen: number[] = []; - const stub = (port: number) => { - seen.push(port); - return port === 18789; - }; - findAvailableDashboardPort("cursor", 18789, "", stub); - assert.deepEqual(seen, [18789, 18790]); + await expect(observer([18789])).rejects.toThrow(/incomplete forward ownership evidence/); + expect(assertCurrent).not.toHaveBeenCalled(); }); }); -describe("resolveCreateSandboxDashboardPort", () => { +describe("resolveCreateSandboxDashboardPortFromObservations", () => { it("lets --control-ui-port override CHAT_UI_URL, registry, agent, and default ports", () => { let preferredSeen: number | null = null; - const result = resolveCreateSandboxDashboardPort({ + const result = resolveCreateSandboxDashboardPortFromObservations({ sandboxName: "cursor", controlUiPort: 19000, chatUiUrlEnv: "http://127.0.0.1:18790", persistedPort: 18791, agentForwardPort: 18792, defaultPort: 18793, - forwardListOutput: "", + forwardObservations: [], findAvailablePort: (_sandboxName, preferredPort) => { preferredSeen = preferredPort; return preferredPort; @@ -216,18 +295,18 @@ describe("resolveCreateSandboxDashboardPort", () => { it("uses CHAT_UI_URL port before registry and rewrites the URL to the allocated port", () => { const warnings: string[] = []; - const result = resolveCreateSandboxDashboardPort({ + const result = resolveCreateSandboxDashboardPortFromObservations({ sandboxName: "cursor", controlUiPort: null, chatUiUrlEnv: "https://chat.example.test:18790/ui/", persistedPort: 18791, agentForwardPort: 18792, defaultPort: 18793, - forwardListOutput: "FORWARDS", - findAvailablePort: (sandboxName, preferredPort, forwardListOutput) => { + forwardObservations: [], + findAvailablePort: (sandboxName, preferredPort, forwardObservations) => { assert.equal(sandboxName, "cursor"); assert.equal(preferredPort, 18790); - assert.equal(forwardListOutput, "FORWARDS"); + assert.deepEqual(forwardObservations, []); return 18794; }, warn: (message) => warnings.push(message), @@ -242,14 +321,14 @@ describe("resolveCreateSandboxDashboardPort", () => { it("falls back through registry, agent, and default ports", () => { const preferredPorts: number[] = []; const resolve = (persistedPort: number | null, agentForwardPort: number | null | undefined) => - resolveCreateSandboxDashboardPort({ + resolveCreateSandboxDashboardPortFromObservations({ sandboxName: "cursor", controlUiPort: null, chatUiUrlEnv: null, persistedPort, agentForwardPort, defaultPort: 18793, - forwardListOutput: "", + forwardObservations: [], findAvailablePort: (_sandboxName, preferredPort) => { preferredPorts.push(preferredPort); return preferredPort; @@ -263,14 +342,14 @@ describe("resolveCreateSandboxDashboardPort", () => { }); it("normalizes schemeless CHAT_UI_URL values before preserving their host", () => { - const result = resolveCreateSandboxDashboardPort({ + const result = resolveCreateSandboxDashboardPortFromObservations({ sandboxName: "cursor", controlUiPort: null, chatUiUrlEnv: "remote.example.test:18790", persistedPort: null, agentForwardPort: null, defaultPort: 18789, - forwardListOutput: "", + forwardObservations: [], findAvailablePort: (_sandboxName, preferredPort) => preferredPort, }); @@ -279,14 +358,14 @@ describe("resolveCreateSandboxDashboardPort", () => { }); it("ignores malformed CHAT_UI_URL when rewriting the dashboard URL", () => { - const result = resolveCreateSandboxDashboardPort({ + const result = resolveCreateSandboxDashboardPortFromObservations({ sandboxName: "cursor", controlUiPort: null, chatUiUrlEnv: "https://example.test:abc", persistedPort: 18791, agentForwardPort: null, defaultPort: 18789, - forwardListOutput: "", + forwardObservations: [], findAvailablePort: (_sandboxName, preferredPort) => preferredPort, }); @@ -295,14 +374,14 @@ describe("resolveCreateSandboxDashboardPort", () => { }); it("ignores malformed CHAT_UI_URL when --control-ui-port supplies the URL", () => { - const result = resolveCreateSandboxDashboardPort({ + const result = resolveCreateSandboxDashboardPortFromObservations({ sandboxName: "cursor", controlUiPort: 19000, chatUiUrlEnv: "https://example.test:abc", persistedPort: 18791, agentForwardPort: null, defaultPort: 18789, - forwardListOutput: "", + forwardObservations: [], findAvailablePort: (_sandboxName, preferredPort) => preferredPort, }); @@ -466,9 +545,9 @@ describe("dashboard port reservation", () => { persistedPort: null, agentForwardPort: null, defaultPort: 18789, - forwardListOutput: "", + observeForwardPorts: observePorts("cursor"), registryOccupiedPorts: new Map(), - findAvailablePort: (_sandboxName, preferredPort, _forwardList, _bound, occupied) => + findAvailablePort: (_sandboxName, preferredPort, _observations, occupied) => occupied?.has(String(preferredPort)) ? 18790 : preferredPort, warn: (message) => warnings.push(message), }, @@ -495,9 +574,8 @@ describe("dashboard port reservation", () => { }); it("defers a persisted port reservation only for the exact owned forward", async () => { - const findAvailablePort = vi.fn(() => 18790); const reservePort = vi.fn(); - const ownsExistingForward = vi.fn((port: number) => port === 18789); + const observeForwardPorts = observePorts("cursor", new Map([[18789, "owned"]])); const result = await reserveCreateSandboxDashboardPort( { @@ -506,10 +584,8 @@ describe("dashboard port reservation", () => { chatUiUrlEnv: null, persistedPort: 18789, agentForwardPort: null, - forwardListOutput: "", + observeForwardPorts, registryOccupiedPorts: new Map(), - findAvailablePort, - ownsExistingForward, }, reservePort, ); @@ -519,8 +595,7 @@ describe("dashboard port reservation", () => { preferredPort: 18789, reservation: null, }); - expect(ownsExistingForward).toHaveBeenCalledExactlyOnceWith(18789); - expect(findAvailablePort).not.toHaveBeenCalled(); + expect(observeForwardPorts).toHaveBeenCalledOnce(); expect(reservePort).not.toHaveBeenCalled(); }); @@ -572,17 +647,20 @@ describe("dashboard port reservation", () => { }); }); -describe("findAvailableDashboardPort multi-gateway registry occupancy", () => { - const stubBound = (...bound: number[]) => { - const set = new Set(bound); - return (port: number) => set.has(port); - }; - - it("treats ports persisted to sibling sandboxes in the registry as occupied even when the active gateway's forward list does not see them", () => { +describe("typed dashboard-port multi-gateway registry occupancy", () => { + it("treats ports persisted to sibling sandboxes in the registry as occupied", () => { const registryOccupied = new Map([["18789", "instance-a"]]); assert.equal( - findAvailableDashboardPort("instance-b", 18789, "", stubBound(), registryOccupied), + findAvailableDashboardPortFromObservations( + "instance-b", + 18789, + [ + forwardObservation("instance-b", 18789, "absent"), + forwardObservation("instance-b", 18790, "absent"), + ], + registryOccupied, + ), 18790, ); }); @@ -591,7 +669,12 @@ describe("findAvailableDashboardPort multi-gateway registry occupancy", () => { const registryOccupied = new Map([["18789", "instance-a"]]); assert.equal( - findAvailableDashboardPort("instance-a", 18789, "", stubBound(), registryOccupied), + findAvailableDashboardPortFromObservations( + "instance-a", + 18789, + [forwardObservation("instance-a", 18789, "owned")], + registryOccupied, + ), 18789, ); }); @@ -599,41 +682,36 @@ describe("findAvailableDashboardPort multi-gateway registry occupancy", () => { it("ignores registry entries with null or invalid dashboard ports", () => { const noPorts = new Map(); - assert.equal(findAvailableDashboardPort("instance-b", 18789, "", stubBound(), noPorts), 18789); + assert.equal( + findAvailableDashboardPortFromObservations( + "instance-b", + 18789, + [forwardObservation("instance-b", 18789, "absent")], + noPorts, + ), + 18789, + ); }); it("includes registry-owned ports in the exhaustion error so the operator can see who holds them", () => { - const lines = ["SANDBOX BIND PORT PID STATUS"]; - for (let p = 18789; p <= 18798; p++) { - lines.push(`forwarded${p} 127.0.0.1 ${p} ${p} running`); + const registryOccupied = new Map(); + const observations: OpenShellForwardObservation[] = []; + for (let port = 18789; port <= 18799; port += 1) { + registryOccupied.set(String(port), port === 18799 ? "instance-z" : `instance-${port}`); + observations.push(forwardObservation("instance-y", port, "absent")); } - const registryOccupied = new Map([["18799", "instance-z"]]); assert.throws( () => - findAvailableDashboardPort( + findAvailableDashboardPortFromObservations( "instance-y", 18789, - lines.join("\n"), - stubBound(), + observations, registryOccupied, ), /18799 → instance-z/, ); }); - - it("lets the active gateway's forward-list entry win when both views see the same port", () => { - const forwardList = [ - "SANDBOX BIND PORT PID STATUS", - "live 127.0.0.1 18789 111 running", - ].join("\n"); - const registryOccupied = new Map([["18789", "stale"]]); - - assert.throws( - () => findAvailableDashboardPort("fresh", 18789, forwardList, () => true, registryOccupied), - /18789 → live/, - ); - }); }); describe("getRegistryOccupiedDashboardPorts", () => { @@ -676,7 +754,15 @@ describe("getRegistryOccupiedDashboardPorts", () => { const occupied = getRegistryOccupiedDashboardPorts("instance-b"); assert.equal(occupied.get("18789"), "instance-a (gateway 9123)"); assert.equal( - findAvailableDashboardPort("instance-b", 18789, "", () => false, occupied), + findAvailableDashboardPortFromObservations( + "instance-b", + 18789, + [ + forwardObservation("instance-b", 18789, "absent"), + forwardObservation("instance-b", 18790, "absent"), + ], + occupied, + ), 18790, ); } finally { diff --git a/src/lib/onboard/dashboard.ts b/src/lib/onboard/dashboard.ts index 52bbb92c3e7..c230f6477c4 100644 --- a/src/lib/onboard/dashboard.ts +++ b/src/lib/onboard/dashboard.ts @@ -4,13 +4,19 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { buildSelectedOpenShellSubprocessEnv } from "../adapters/openshell/command-argv"; +import JSON5 from "json5"; import { - createForwardServiceTarget, - isForwardServiceListenerOwner, - launchForwardService, - type ForwardServiceTarget, -} from "../adapters/openshell/forward-service"; + formatOpenShellForwardStartFailure, + type OpenShellForwardAdapter, + type OpenShellForwardIdentity, +} from "../adapters/openshell/forward"; +import { + createOpenShellForwardAdapterForAuthority, + openShellForwardIdentity, + type OpenShellForwardRuntimeAuthority, +} from "../adapters/openshell/forward-runtime"; +import { createCliOpenShellSandboxTransferExecutor } from "../adapters/openshell/sandbox-transfer-cli"; +import type { OpenShellSandboxTransferExecutor } from "../adapters/openshell/sandbox-transfer"; import type { AgentDefinition } from "../agent/defs"; import { getInteractiveAgentCommand } from "../agent/gateway-restart-scripts"; import { DASHBOARD_PORT } from "../core/ports"; @@ -29,14 +35,15 @@ import { normalizeDashboardForwardOptions, } from "./dashboard-forward-control"; import { - findAvailableDashboardPort, + createOpenShellForwardPortObserver, + findAvailableDashboardPortFromObserver, getPersistedDashboardPort, getRegistryOccupiedDashboardPorts, getRegistryOccupiedHermesApiPorts, - isPortBoundOnHost, type ListSandboxesFn, + type OpenShellForwardPortObserver, } from "./dashboard-port"; -import { canReuseDashboardForwardForAgent } from "./dashboard-runtime"; +import { canReuseDashboardForwardForAgent, resolveDashboardForwardBind } from "./dashboard-runtime"; import { ensureMessagingHostForwardForSandbox, productionForwardServiceRegistryContext, @@ -49,17 +56,10 @@ function looksLikeForwardPortConflict(diagnostic: string): boolean { return /eaddrinuse|address already in use|port .* in use|bind: .*in use/iu.test(diagnostic); } -type CommandResult = { status: number | null }; - -type DashboardForwardRuntimeAuthority = { - readonly gatewayEndpoint?: string; - readonly localTlsDir?: string; -}; +type DashboardForwardRuntimeAuthority = OpenShellForwardRuntimeAuthority; export interface OnboardDashboardDeps { - runOpenshell(args: string[], opts?: Record): CommandResult; runCaptureOpenshell(args: string[], opts?: Record): string | null; - openshellArgv(args: string[]): string[]; runCapture?: typeof defaultRunCapture; cliName(): string; agentProductName(): string; @@ -71,6 +71,7 @@ export interface OnboardDashboardDeps { isWsl(): boolean; redact(value: unknown): string; sleep(seconds: number): void; + sandboxTransferExecutor?: OpenShellSandboxTransferExecutor; productionForwardService?: boolean; /** Endpoint and optional client TLS bundle selected by the bound gateway authority. */ getGatewayForwardRuntimeAuthority?(): { @@ -84,14 +85,13 @@ export interface OnboardDashboardDeps { // never reads the runner's real `~/.nemoclaw/sandboxes.json`; production // callers leave it unset and the helper falls back to the live registry. listSandboxes?: ListSandboxesFn; - /** Host-listener probe injected by forward release race tests. */ - isPortBoundOnHost?: typeof isPortBoundOnHost; /** Sandbox lookup used to resolve the per-sandbox Hermes API port. */ getSandbox?(name: string): | { gatewayName?: string | null; gatewayPort?: number | null; dashboardPort?: number | null; + dashboardRemoteBindPrepared?: boolean; hermesApiPort?: number | null; hermesDashboardPort?: number | null; lifecycleLiveIdentityFingerprint?: string; @@ -99,15 +99,13 @@ export interface OnboardDashboardDeps { } | null | undefined; - /** Direct ForwardTcp launcher. */ - forwardService?: { - executable(): string; - owns?(target: ForwardServiceTarget): boolean; - launch?: (...args: Parameters) => void | Promise; - resolveGatewayName( - sandbox: { gatewayName?: string | null; gatewayPort?: number | null } | null | undefined, - ): string; - }; + /** Typed forwarding adapter factory. Tests inject a fake; production uses the CLI adapter. */ + forwardAdapterForAuthority?: ( + authority: DashboardForwardRuntimeAuthority, + ) => OpenShellForwardAdapter; + resolveForwardGatewayName?( + sandbox: { gatewayName?: string | null; gatewayPort?: number | null } | null | undefined, + ): string; printAgentDashboardUi( sandboxName: string, token: string | null, @@ -171,7 +169,7 @@ export interface OnboardDashboardHelpers { label: string, revalidateSandboxIdentity?: (operation: string) => void, ): Promise; - fetchGatewayAuthTokenFromSandbox(sandboxName: string): string | null; + fetchGatewayAuthTokenFromSandbox(sandboxName: string): Promise; fetchAgentWebAuthTokenFromSandbox(sandboxName: string, agent: AgentDefinition): string | null; getDashboardForwardPort( chatUiUrl?: string, @@ -181,11 +179,10 @@ export interface OnboardDashboardHelpers { chatUiUrl?: string, options?: Parameters[1], ): string; - ownsForwardServicePort( + createForwardPortObserver( sandboxName: string, - port: number, targetKind?: "dashboard" | "loopback", - ): boolean; + ): OpenShellForwardPortObserver; printDashboard( sandboxName: string, model: string, @@ -193,7 +190,7 @@ export interface OnboardDashboardHelpers { nimContainer?: string | null, agent?: AgentDefinition | null, ready?: boolean, - ): void; + ): Promise; stopAllDashboardForwards(): void; } @@ -227,122 +224,88 @@ function printWslFallback(fallbackDashboardUrls: string[], indent: string): void export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): OnboardDashboardHelpers { const runCapture = deps.runCapture ?? defaultRunCapture; - const productionForwardService = deps.productionForwardService + const productionForwardRegistry = deps.productionForwardService ? productionForwardServiceRegistryContext() : null; - const getSandbox = deps.getSandbox ?? productionForwardService?.getSandbox; - const listSandboxes = deps.listSandboxes ?? productionForwardService?.listSandboxes; - const forwardService: OnboardDashboardDeps["forwardService"] = - deps.forwardService ?? - (productionForwardService - ? { - executable: () => { - const executable = deps.openshellArgv([])[0]; - if (!executable) throw new Error("OpenShell is unavailable"); - return executable; - }, - owns: isForwardServiceListenerOwner, - resolveGatewayName: productionForwardService.resolveGatewayName, - } - : undefined); + const getSandbox = deps.getSandbox ?? productionForwardRegistry?.getSandbox; + const listSandboxes = deps.listSandboxes ?? productionForwardRegistry?.listSandboxes; + const resolveGatewayName = + deps.resolveForwardGatewayName ?? productionForwardRegistry?.resolveGatewayName; + const forwardAdapterForAuthority = + deps.forwardAdapterForAuthority ?? createOpenShellForwardAdapterForAuthority; + function resolveForwardServiceGateway( sandboxName: string, options: DashboardForwardOptions = {}, ): string | null { - if (!forwardService) return null; + if (!resolveGatewayName) return null; const sandbox = getSandbox?.(sandboxName); options.revalidateSandboxIdentity?.(`launch ForwardTcp service for sandbox '${sandboxName}'`); - return options.gatewayName ?? forwardService.resolveGatewayName(sandbox); + return options.gatewayName ?? resolveGatewayName(sandbox); } - function forwardTarget( + function getForwardRuntimeAuthority( sandboxName: string, gatewayName: string, - port: number, - target: string, - authority: DashboardForwardRuntimeAuthority, - ): ForwardServiceTarget { - return createForwardServiceTarget( - { - executable: forwardService!.executable(), - ...(authority.gatewayEndpoint ? { gatewayEndpoint: authority.gatewayEndpoint } : {}), - gatewayName, - workspace: "default", - sandboxName, - localHost: target.startsWith("0.0.0.0:") ? "0.0.0.0" : "127.0.0.1", - }, - port, - ); - } - - function getForwardRuntimeAuthority(): DashboardForwardRuntimeAuthority { - return deps.getGatewayForwardRuntimeAuthority?.() ?? {}; - } - - function forwardSourceEnvironment( - gatewayName: string, - authority: DashboardForwardRuntimeAuthority, - ): Record { - return buildSelectedOpenShellSubprocessEnv({ + ): DashboardForwardRuntimeAuthority { + const authority = deps.getGatewayForwardRuntimeAuthority?.(); + if (!authority) { + throw new Error(`ForwardTcp gateway authority is unavailable for '${sandboxName}'`); + } + return { + gatewayEndpoint: authority.gatewayEndpoint, gatewayName, workspace: "default", ...(authority.localTlsDir ? { localTlsDir: authority.localTlsDir } : {}), - }); + }; } - function assertForwardGatewayCurrent(expected: DashboardForwardRuntimeAuthority): void { - if (!deps.getGatewayForwardRuntimeAuthority) return; - const current = deps.getGatewayForwardRuntimeAuthority(); + async function assertForwardGatewayCurrent( + expected: DashboardForwardRuntimeAuthority, + revalidateSandboxIdentity?: (operation: string) => void, + operation?: string, + ): Promise { + const current = deps.getGatewayForwardRuntimeAuthority?.(); if ( + !current || current.gatewayEndpoint !== expected.gatewayEndpoint || current.localTlsDir !== expected.localTlsDir ) { throw new Error("ForwardTcp gateway authority changed during launch"); } + if (operation) revalidateSandboxIdentity?.(operation); } - function ownsDashboardForward( + function forwardIdentity( sandboxName: string, - gatewayName: string, + authority: DashboardForwardRuntimeAuthority, port: number, - chatUiUrl: string, - ): boolean { - const authority = getForwardRuntimeAuthority(); - const target = forwardTarget( - sandboxName, - gatewayName, - port, - getDashboardForwardTarget(chatUiUrl, { isWsl: deps.isWsl() }), - authority, - ); - if (forwardService?.owns?.(target) !== true) return false; - assertForwardGatewayCurrent(authority); - return true; + targetKind: "dashboard" | "loopback", + ): OpenShellForwardIdentity { + const localHost = + targetKind === "dashboard" + ? resolveDashboardForwardBind(getSandbox?.(sandboxName), { + requestedBind: process.env.NEMOCLAW_DASHBOARD_BIND, + wsl: deps.isWsl(), + }) + : "127.0.0.1"; + return openShellForwardIdentity(authority, sandboxName, localHost, port); } - function ownsForwardServicePort( + function createForwardPortObserver( sandboxName: string, - port: number, targetKind: "dashboard" | "loopback" = "dashboard", - ): boolean { + ): OpenShellForwardPortObserver { const gatewayName = resolveForwardServiceGateway(sandboxName); - if (gatewayName === null) return false; - const target = - targetKind === "loopback" - ? `127.0.0.1:${String(port)}` - : buildChain({ - chatUiUrl: `http://127.0.0.1:${String(port)}`, - port, - ...dashboardAccess.resolveDashboardPlatformHints({ - isWsl: deps.isWsl(), - runCapture: deps.runCapture, - }), - }).forwardTarget; - const authority = getForwardRuntimeAuthority(); - const serviceTarget = forwardTarget(sandboxName, gatewayName, port, target, authority); - if (forwardService?.owns?.(serviceTarget) !== true) return false; - assertForwardGatewayCurrent(authority); - return true; + if (gatewayName === null) { + throw new Error(`ForwardTcp gateway selection is unavailable for '${sandboxName}'`); + } + const authority = getForwardRuntimeAuthority(sandboxName, gatewayName); + return createOpenShellForwardPortObserver({ + adapter: forwardAdapterForAuthority(authority), + forwardForPort: (port) => forwardIdentity(sandboxName, authority, port, targetKind), + assertCurrent: () => assertForwardGatewayCurrent(authority), + }); } function getDashboardForwardPort( @@ -427,6 +390,61 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa process.exit(1); } + function forwardResultMessage( + result: + | Awaited> + | Awaited>, + ): string { + if ("error" in result) { + const failure = "failure" in result ? result.failure : undefined; + const suffix = failure ? ` [${formatOpenShellForwardStartFailure(failure)}]` : ""; + return `${result.error.message}${suffix}`; + } + if ("observation" in result) { + return result.observation.state === "foreign" + ? "The host port is owned by a foreign listener." + : "NemoClaw could not prove the forward state."; + } + return "NemoClaw could not reconcile the forward state."; + } + + async function reconcileForward( + sandboxName: string, + authority: DashboardForwardRuntimeAuthority, + forward: OpenShellForwardIdentity, + label: string, + revalidateSandboxIdentity?: (operation: string) => void, + ): Promise { + const adapter = forwardAdapterForAuthority(authority); + const assertCurrent = () => + assertForwardGatewayCurrent( + authority, + revalidateSandboxIdentity, + `accept ${label} forward ${String(forward.port)} for sandbox '${sandboxName}'`, + ); + let result = await adapter.startForward({ forward, assertCurrent }); + if (result.state === "refused" && result.observation.state === "stale") { + const retirement = await adapter.retireLegacyForward({ + forward, + assertCurrent, + authorize: async () => { + await assertForwardGatewayCurrent( + authority, + revalidateSandboxIdentity, + `retire legacy ${label} forward ${String(forward.port)} for sandbox '${sandboxName}'`, + ); + }, + }); + if (retirement.state !== "retired" && retirement.state !== "not_needed") { + throw new Error(forwardResultMessage(retirement)); + } + result = await adapter.startForward({ forward, assertCurrent }); + } + if (result.state !== "started" && result.state !== "reused") { + throw new Error(forwardResultMessage(result)); + } + } + async function ensureDashboardForward( sandboxName: string, chatUiUrl = `http://127.0.0.1:${CONTROL_UI_PORT}`, @@ -441,17 +459,18 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa if (!forwardGateway) { throw new Error(`ForwardTcp authority is unavailable for '${sandboxName}'`); } - const existingForwards = deps.runCaptureOpenshell( - ["forward", "list", "--gateway", forwardGateway], - { ignoreError: true }, - ); - // The dashboard forward binds `127.0.0.1` by default, so an - // external-interface-only listener on the port does not conflict. When the - // operator opts into a remote (`0.0.0.0`) bind, every interface conflicts, - // so the host-port probe must count listeners on any interface (#3259, #11439). - const forwardBindsAllInterfaces = getDashboardForwardTarget(chatUiUrl).startsWith("0.0.0.0:"); - const isPortBoundRaw = deps.isPortBoundOnHost ?? isPortBoundOnHost; - const isPortBound = (port: number): boolean => isPortBoundRaw(port, !forwardBindsAllInterfaces); + const authority = getForwardRuntimeAuthority(sandboxName, forwardGateway); + const forwardAdapter = forwardAdapterForAuthority(authority); + const observeForwardPorts = createOpenShellForwardPortObserver({ + adapter: forwardAdapter, + forwardForPort: (port) => forwardIdentity(sandboxName, authority, port, "dashboard"), + assertCurrent: () => + assertForwardGatewayCurrent( + authority, + revalidateSandboxIdentity, + `inspect dashboard forwards for sandbox '${sandboxName}'`, + ), + }); const persistedPort = getPersistedDashboardPort(sandboxName, listSandboxes); const registryOccupiedPorts = new Map([ ...getRegistryOccupiedDashboardPorts(sandboxName, listSandboxes), @@ -463,30 +482,16 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa `Port ${String(preferredPort)} is not available for '${sandboxName}'; another sandbox registered it.`, ); } - if (fixedPort && isPortBound(preferredPort)) { - if ( - reuseExistingForward && - ownsDashboardForward(sandboxName, forwardGateway, preferredPort, chatUiUrl) - ) { - revalidateSandboxIdentity?.( - `retain dashboard forward ${String(preferredPort)} for sandbox '${sandboxName}'`, - ); - return preferredPort; - } - throw new Error( - `Registered dashboard port ${String(preferredPort)} is already occupied; it cannot be reallocated or adopted. ` + - "NemoClaw will not stop an unverified listener. Stop the owning service or OpenShell gateway to release the port, then retry onboarding.", - ); - } let actualPort: number; try { - actualPort = findAvailableDashboardPort( - sandboxName, - preferredPort, - existingForwards, - isPortBound, - registryOccupiedPorts, - ); + actualPort = ( + await findAvailableDashboardPortFromObserver( + sandboxName, + preferredPort, + observeForwardPorts, + registryOccupiedPorts, + ) + ).port; } catch (err) { if (!rollbackSandboxOnFailure) throw err; rollbackSandboxAndExit(sandboxName, err, options.gatewayName); @@ -510,9 +515,6 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa console.warn(` ! Port ${preferredPort} is taken. Using port ${actualPort} instead.`); } - const parsedUrl = new URL(chatUiUrl.includes("://") ? chatUiUrl : `http://${chatUiUrl}`); - parsedUrl.port = String(actualPort); - const actualTarget = getDashboardForwardTarget(parsedUrl.toString()); const actualGateway = resolveForwardServiceGateway(sandboxName, options); let fwdOk = false; let fwdDiagnostic = ""; @@ -521,35 +523,14 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa revalidateSandboxIdentity?.( `start dashboard forward ${String(actualPort)} for sandbox '${sandboxName}'`, ); - const authority = getForwardRuntimeAuthority(); - const target = forwardTarget( + const actualAuthority = getForwardRuntimeAuthority(sandboxName, actualGateway); + await reconcileForward( sandboxName, - actualGateway, - actualPort, - actualTarget, - authority, + actualAuthority, + forwardIdentity(sandboxName, actualAuthority, actualPort, "dashboard"), + "dashboard", + revalidateSandboxIdentity, ); - let readinessVerified = false; - await (forwardService?.launch ?? launchForwardService)(target, { - sourceEnvironment: forwardSourceEnvironment(actualGateway, authority), - verifyReady: () => { - if (forwardService?.owns?.(target) !== true) { - throw new Error( - `Could not verify forward ownership on port ${String(actualPort)} for '${sandboxName}'.`, - ); - } - assertForwardGatewayCurrent(authority); - revalidateSandboxIdentity?.( - `accept dashboard forward ${String(actualPort)} for sandbox '${sandboxName}'`, - ); - readinessVerified = true; - }, - }); - if (!readinessVerified) { - throw new Error( - `Forward readiness verification did not run on port ${String(actualPort)} for '${sandboxName}'.`, - ); - } fwdOk = true; } catch (error) { fwdDiagnostic = error instanceof Error ? error.message : String(error); @@ -690,29 +671,14 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa revalidateSandboxIdentity?.( `start ${label} forward ${String(port)} for sandbox '${sandboxName}'`, ); - const authority = getForwardRuntimeAuthority(); - const target = forwardTarget(sandboxName, gatewayName, port, String(port), authority); - let readinessVerified = false; - await (forwardService?.launch ?? launchForwardService)(target, { - sourceEnvironment: forwardSourceEnvironment(gatewayName, authority), - verifyReady: () => { - if (forwardService?.owns?.(target) !== true) { - throw new Error( - `Could not verify ${label} forward ownership on port ${String(port)} for '${sandboxName}'.`, - ); - } - assertForwardGatewayCurrent(authority); - revalidateSandboxIdentity?.( - `accept ${label} forward ${String(port)} for sandbox '${sandboxName}'`, - ); - readinessVerified = true; - }, - }); - if (!readinessVerified) { - throw new Error( - `Forward readiness verification did not run on port ${String(port)} for '${sandboxName}'.`, - ); - } + const authority = getForwardRuntimeAuthority(sandboxName, gatewayName); + await reconcileForward( + sandboxName, + authority, + forwardIdentity(sandboxName, authority, port, "loopback"), + label, + revalidateSandboxIdentity, + ); return true; } catch (error) { const diagnostic = error instanceof Error ? error.message : String(error); @@ -739,29 +705,41 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa return fetchAgentWebAuthToken(deps.runCaptureOpenshell, sandboxName, agent); } - function fetchGatewayAuthTokenFromSandbox(sandboxName: string): string | null { + async function fetchGatewayAuthTokenFromSandbox(sandboxName: string): Promise { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-token-")); + let completion: Awaited> | undefined; + let token: string | null = null; try { const destDir = `${tmpDir}${path.sep}`; - const result = deps.runOpenshell( - ["sandbox", "download", sandboxName, "/sandbox/.openclaw/openclaw.json", destDir], - { ignoreError: true, stdio: ["ignore", "ignore", "ignore"] }, - ); - if (result.status !== 0) return null; + completion = await ( + deps.sandboxTransferExecutor ?? createCliOpenShellSandboxTransferExecutor() + ).run({ + direction: "download", + sandboxName, + target: { kind: "selected" }, + source: "/sandbox/.openclaw/openclaw.json", + destination: destDir, + output: "suppress", + }); + if (completion.outcome.kind !== "completed" || completion.outcome.exitCode !== 0) return null; + if (completion.wasInterrupted()) return null; const jsonPath = findOpenclawJsonPath(tmpDir); if (!jsonPath) return null; - const cfg = JSON.parse(fs.readFileSync(jsonPath, "utf-8")); - const token = cfg && cfg.gateway && cfg.gateway.auth && cfg.gateway.auth.token; - return typeof token === "string" && token.length > 0 ? token : null; + const cfg = JSON5.parse(fs.readFileSync(jsonPath, "utf-8")); + const parsedToken = cfg && cfg.gateway && cfg.gateway.auth && cfg.gateway.auth.token; + token = typeof parsedToken === "string" && parsedToken.length > 0 ? parsedToken : null; } catch { - return null; + token = null; } finally { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { // ignore cleanup errors } + if (completion?.wasInterrupted()) token = null; + completion?.release(); } + return token; } /** @@ -785,14 +763,14 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa console.log(`${indent} then run the configured interactive agent command`); } - function printDashboard( + async function printDashboard( sandboxName: string, model: string, provider: string, nimContainer: string | null = null, agent: AgentDefinition | null = null, ready = true, - ): void { + ): Promise { const nimStatus = deps.nimStatus ?? nim.nimStatus; const nimStatusByName = deps.nimStatusByName ?? nim.nimStatusByName; const shouldShowNimLine = deps.shouldShowNimLine ?? nim.shouldShowNimLine; @@ -802,7 +780,7 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa const providerLabel = deps.getProviderLabel(provider); const token = !agent || agent.dashboard.auth === "url_token" - ? fetchGatewayAuthTokenFromSandbox(sandboxName) + ? await fetchGatewayAuthTokenFromSandbox(sandboxName) : null; const chatUiUrl = process.env.CHAT_UI_URL || `http://127.0.0.1:${CONTROL_UI_PORT}`; const chain = buildChain({ @@ -911,10 +889,34 @@ export function createOnboardDashboardHelpers(deps: OnboardDashboardDeps): Onboa ensureAgentFixedForward, fetchGatewayAuthTokenFromSandbox, fetchAgentWebAuthTokenFromSandbox, + createForwardPortObserver, getDashboardForwardPort, getDashboardForwardTarget, - ownsForwardServicePort, printDashboard, stopAllDashboardForwards, }; } + +const HOST_PROBE_MAX_SECONDS = "3"; + +/** + * Probe a dashboard-chain port on the host through its forward, for the + * onboarding deployment verification. Returns the HTTP status, or 0 when the + * forward is down. + */ +export function probeVerificationHostPort(port: number, probePath: string): number { + const result = defaultRunCapture( + [ + "curl", + "-so", + "/dev/null", + "-w", + "%{http_code}", + "--max-time", + HOST_PROBE_MAX_SECONDS, + `http://127.0.0.1:${port}${probePath}`, + ], + { ignoreError: true }, + ); + return parseInt(result.trim(), 10) || 0; +} diff --git a/src/lib/state/gateway-registry.ts b/src/lib/state/gateway-registry.ts index 62613943238..0c4b1ebbc2d 100644 --- a/src/lib/state/gateway-registry.ts +++ b/src/lib/state/gateway-registry.ts @@ -6,14 +6,16 @@ import path from "node:path"; import { isErrnoException } from "../core/errno"; import { isObjectRecord } from "../core/json-types"; +import { resolveLegacyModelRouterPort } from "../core/model-router-port"; import { DEFAULT_GATEWAY_PORT } from "../core/ports"; import { NAME_MAX_LENGTH, NAME_VALID_PATTERN } from "../name-validation"; -import { isValidDashboardExternalUrl } from "../dashboard/url"; import { resolveGatewayName, resolveGatewayPortFromName } from "../onboard/gateway-binding"; import { GATEWAYS_SUBDIR, nemoclawStateRoot } from "./state-root"; +import { normalizePendingSandboxCreateIdentity } from "./registry/pending-create-identity"; export { GATEWAYS_SUBDIR, resolveHome } from "./state-root"; export { DEFAULT_GATEWAY_PORT } from "../core/ports"; +export { isValidName } from "../name-validation"; export { releaseManagedGatewayStateLifecycleLock, tryAcquireManagedGatewayStateLifecycleLock, @@ -23,20 +25,22 @@ export { export { withRegistryLockAt } from "./registry/lock"; const MAX_REGISTRY_BYTES = 16 * 1024 * 1024; +const MAX_ONBOARD_SESSION_BYTES = 1024 * 1024; const MAX_GATEWAY_ROOTS = 256; const MAX_GATEWAY_DIRECTORY_ENTRIES = 1024; export interface GatewayRegistryEntry extends Record { name: string; dashboardPort?: number | null; - dashboardExternalUrl?: string | null; hermesApiPort?: number | null; gatewayName?: string | null; gatewayPort?: number | null; + openshellGatewayStateDir?: string | null; } export interface GatewayRegistryDocument extends Record { defaultSandbox: string | null; + defaultSelectionRevision?: number; sandboxes: Record; } @@ -95,6 +99,14 @@ function parseRegistry(filePath: string, raw: string): GatewayRegistryDocument { ) { throw stateError(`${filePath} has an invalid defaultSandbox`); } + if ( + parsed.defaultSelectionRevision !== undefined && + (typeof parsed.defaultSelectionRevision !== "number" || + !Number.isSafeInteger(parsed.defaultSelectionRevision) || + parsed.defaultSelectionRevision < 0) + ) { + throw stateError(`${filePath} has an invalid defaultSelectionRevision`); + } const sandboxes: Record = {}; for (const [name, value] of Object.entries(parsed.sandboxes)) { @@ -116,14 +128,17 @@ function parseRegistry(filePath: string, raw: string): GatewayRegistryDocument { throw stateError(`${filePath} has an invalid ${field} for sandbox ${JSON.stringify(name)}`); } } - const externalUrl = value.dashboardExternalUrl; + const gatewayStateDir = value.openshellGatewayStateDir; if ( - externalUrl !== undefined && - externalUrl !== null && - (typeof externalUrl !== "string" || !isValidDashboardExternalUrl(externalUrl)) + gatewayStateDir !== undefined && + gatewayStateDir !== null && + (typeof gatewayStateDir !== "string" || + gatewayStateDir.length === 0 || + !path.isAbsolute(gatewayStateDir) || + path.resolve(gatewayStateDir) !== gatewayStateDir) ) { throw stateError( - `${filePath} has an invalid dashboardExternalUrl for sandbox ${JSON.stringify(name)}`, + `${filePath} has an invalid openshellGatewayStateDir for sandbox ${JSON.stringify(name)}`, ); } sandboxes[name] = @@ -194,6 +209,53 @@ export function registryEntryGatewayPort(entry: GatewayRegistryEntry): number { return DEFAULT_GATEWAY_PORT; } +/** Recover one unambiguous onboard-time custom OpenShell state directory for a gateway port. */ +export function registryOpenShellGatewayStateDir( + registry: GatewayRegistryDocument, + gatewayPort: number, +): string | null { + const recorded = new Set(); + for (const entry of Object.values(registry.sandboxes)) { + const entryPort = registryEntryGatewayPort(entry); + const pending = normalizePendingSandboxCreateIdentity(entry.pendingCreateIdentity); + if ( + pending && + (entry.pendingRouteReservation !== true || + typeof entry.reservationSessionId !== "string" || + !entry.reservationSessionId.trim() || + pending.sandboxName !== entry.name || + pending.gatewayName !== resolveGatewayName(entryPort) || + pending.gatewayPort !== entryPort || + pending.lifecycleGeneration !== entry.lifecycleGeneration || + pending.sandboxIdentityFingerprint !== entry.lifecycleLiveIdentityFingerprint) + ) { + throw stateError( + `sandbox ${JSON.stringify(entry.name)} has a conflicting pending create identity`, + ); + } + if (entryPort !== gatewayPort) continue; + if (typeof entry.openshellGatewayStateDir === "string") { + recorded.add(entry.openshellGatewayStateDir); + } + if (pending?.openshellGatewayStateDir) recorded.add(pending.openshellGatewayStateDir); + } + if (recorded.size > 1) { + throw stateError( + `gateway port ${String(gatewayPort)} has conflicting OpenShell state directories`, + ); + } + return recorded.values().next().value ?? null; +} + +/** Read one port's recorded custom OpenShell state directory from its canonical registry. */ +export function readGatewayOpenShellStateDir(home: string, gatewayPort: number): string | null { + const registry = readGatewayRegistryFile( + home, + path.join(nemoclawStateRoot(home, gatewayPort), "sandboxes.json"), + ); + return registry ? registryOpenShellGatewayStateDir(registry, gatewayPort) : null; +} + /** Enumerate the default root plus bounded, real, numeric non-default gateway roots. */ export function listGatewayStateRoots(home: string): GatewayStateRoot[] { const sharedRoot = nemoclawStateRoot(home, DEFAULT_GATEWAY_PORT); @@ -265,3 +327,71 @@ export function listHostGatewayRegistryEntries(home: string): HostGatewayRegistr } return result; } + +/** + * Enumerate every gateway port represented by durable host state. + * + * State-root names protect gateways that do not yet have a sandbox row, while + * registry bindings retain ports recorded by legacy layouts. Consumers that + * expose a host loopback service must treat the complete inventory as control + * plane, even when the current process selects another gateway. + */ +export function listRecordedGatewayPorts(home: string): number[] { + const ports = new Set(listGatewayStateRoots(home).map(({ gatewayPort }) => gatewayPort)); + for (const { gatewayPort } of listHostGatewayRegistryEntries(home)) ports.add(gatewayPort); + return [...ports].sort((left, right) => left - right); +} + +/** Enumerate exact Model Router ports retained by onboarding state on this host. */ +export function listRecordedModelRouterPorts(home: string): number[] { + const ports = new Set(); + for (const state of listGatewayStateRoots(home)) { + const sessionFile = path.join(state.root, "onboard-session.json"); + assertGatewayStatePathSafe(home, path.dirname(sessionFile)); + let fd: number; + try { + fd = openReadOnlyNoFollow(sessionFile); + } catch (error) { + if (isErrnoException(error) && error.code === "ENOENT") continue; + throw error; + } + try { + const stat = fs.fstatSync(fd); + if (!stat.isFile()) throw stateError(`${sessionFile} is not a regular file`); + if (stat.size > MAX_ONBOARD_SESSION_BYTES) { + throw stateError( + `${sessionFile} exceeds the ${String(MAX_ONBOARD_SESSION_BYTES)} byte limit`, + ); + } + const parsed: unknown = JSON.parse(fs.readFileSync(fd, "utf8")); + if (!isObjectRecord(parsed)) throw stateError(`${sessionFile} is not an object`); + const routerPort = parsed.routerPort; + if (routerPort === null && parsed.routerPid === null && parsed.routerCredentialHash === null) + continue; + if (routerPort === undefined || routerPort === null) { + const legacyPort = resolveLegacyModelRouterPort(parsed); + if (legacyPort !== null) ports.add(legacyPort); + continue; + } + if ( + typeof routerPort !== "number" || + !Number.isInteger(routerPort) || + routerPort < 1 || + routerPort > 65535 + ) { + throw stateError(`${sessionFile} has an invalid routerPort`); + } + ports.add(routerPort); + } catch (error) { + if (error instanceof SyntaxError) throw stateError(`${sessionFile} is not valid JSON`); + throw error; + } finally { + fs.closeSync(fd); + } + } + for (const { entry } of listHostGatewayRegistryEntries(home)) { + const port = resolveLegacyModelRouterPort(entry); + if (port !== null) ports.add(port); + } + return [...ports].sort((left, right) => left - right); +} From 362c0df032c22692fbeaae9c4f6fd9cdd1ca2c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 13:31:50 -0700 Subject: [PATCH 03/12] fix(onboard): retain dashboard URLs with current forwarding ownership Restore the reviewed dashboard projection after integrating current main. Preserve OpenShell-owned forwarding, asynchronous dashboard commands, and redacted public inventory output. Signed-off-by: Aaron Erickson --- src/commands/sandbox/dashboard-url.ts | 4 ++- src/lib/dashboard-url-command.test.ts | 46 ++++++++++++++++++++++++ src/lib/inventory/index.ts | 13 ++++++- src/lib/onboard/dashboard-port.test.ts | 50 ++++++++++++++++++++++++++ src/lib/state/gateway-registry.ts | 12 +++++++ 5 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/commands/sandbox/dashboard-url.ts b/src/commands/sandbox/dashboard-url.ts index 6fec127bfed..8b2d37fb79d 100644 --- a/src/commands/sandbox/dashboard-url.ts +++ b/src/commands/sandbox/dashboard-url.ts @@ -9,7 +9,9 @@ import type { SandboxEntry } from "../../lib/state/registry"; type DashboardUrlRuntimeBridge = { fetchGatewayAuthTokenFromSandbox: (sandboxName: string) => Promise; - getSandbox: (sandboxName: string) => Pick | null; + getSandbox: ( + sandboxName: string, + ) => Pick | null; getAccessUrl?: (port: number) => string | null; }; diff --git a/src/lib/dashboard-url-command.test.ts b/src/lib/dashboard-url-command.test.ts index 8f64c34cc1c..6177cdc306d 100644 --- a/src/lib/dashboard-url-command.test.ts +++ b/src/lib/dashboard-url-command.test.ts @@ -183,6 +183,52 @@ describe("dashboard-url command helpers", () => { expect(sinks.err).toEqual([]); }); + it("prints the persisted external dashboard URL for a session-auth agent (#11439)", async () => { + const sinks = makeSinks(); + + await runDashboardUrlCommand( + "hermes", + { quiet: true }, + { + fetchToken: () => null, + getSandbox: () => ({ + agent: "hermes", + dashboardPort: 18789, + dashboardExternalUrl: "https://dash.example.com:18789", + }), + getAgentDashboardAuth: () => "session", + // A host access URL must not override the persisted external origin. + getAccessUrl: () => "http://172.22.1.1:18789", + log: sinks.log, + error: sinks.error, + }, + ); + + expect(sinks.out).toEqual(["https://dash.example.com:18789/"]); + }); + + it("embeds the token in the persisted external dashboard URL for token-auth agents (#11439)", async () => { + const sinks = makeSinks(); + + await runDashboardUrlCommand( + "agent-ui", + { quiet: true }, + { + fetchToken: () => "agent-token", + getSandbox: () => ({ + agent: "agent-ui", + dashboardPort: 19001, + dashboardExternalUrl: "https://dash.example.com:19001", + }), + getAgentDashboardAuth: () => "url_token", + log: sinks.log, + error: sinks.error, + }, + ); + + expect(sinks.out).toEqual(["https://dash.example.com:19001/#token=agent-token"]); + }); + it("fetches a token for non-OpenClaw agents with token-auth dashboards", async () => { const sinks = makeSinks(); const fetchToken = vi.fn(() => "agent-token"); diff --git a/src/lib/inventory/index.ts b/src/lib/inventory/index.ts index 8081289a42d..dceb14c5d80 100644 --- a/src/lib/inventory/index.ts +++ b/src/lib/inventory/index.ts @@ -30,6 +30,7 @@ export interface SandboxEntry { messaging?: SandboxMessagingState | null; agent?: string | null; dashboardPort?: number | null; + dashboardExternalUrl?: string | null; // Passthrough of the durable registry reservation marker so list and status // hide registrations that have not committed their lifecycle yet. pendingRouteReservation?: true; @@ -106,6 +107,7 @@ export interface SandboxInventoryRow { policies: string[]; agent: string; dashboardPort?: number | null; + dashboardExternalUrl?: string | null; isDefault: boolean; activeSessionCount: number | null; // #5714: row recovered display-only from the live gateway. Its agent/GPU/ @@ -202,6 +204,7 @@ export interface StatusSandboxRow { }; phase?: "pending" | "configuring" | "active"; dashboardPort?: number | null; + dashboardExternalUrl?: string | null; isDefault: boolean; } @@ -308,6 +311,7 @@ async function projectPublicSandboxFields( typeof sandbox.dashboardPort === "number" && Number.isFinite(sandbox.dashboardPort) ? sandbox.dashboardPort : null; + const dashboardExternalUrl = safeStatusString(sandbox.dashboardExternalUrl); return { name: safeStatusString(sandbox.name) ?? sandbox.name, model: safeStatusString(inference.model), @@ -324,6 +328,7 @@ async function projectPublicSandboxFields( ), agent: safeStatusString(resolveDisplayAgent(sandbox)) ?? "unknown", ...(dashboardPort != null ? { dashboardPort } : {}), + ...(dashboardExternalUrl != null ? { dashboardExternalUrl } : {}), }; } @@ -497,7 +502,9 @@ export function renderSandboxInventoryText( if (providerDrifted) parts.push(`provider=${sandbox.provider || "unknown"}`); log(` (live OpenShell gateway differs from onboarded: ${parts.join(", ")})`); } - if (sandbox.dashboardPort != null) { + if (sandbox.dashboardExternalUrl != null) { + log(` dashboard: ${sandbox.dashboardExternalUrl}`); + } else if (sandbox.dashboardPort != null) { log(` dashboard: http://127.0.0.1:${sandbox.dashboardPort}/`); } } @@ -728,6 +735,10 @@ export async function showStatusCommand(deps: ShowStatusCommandDeps): Promise { return address.port; } +describe("lsofOutputBlocksLoopbackBind interface-specific loopback probe (#11439)", () => { + const loopback = (port: number) => + `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nx 1 u 3u IPv4 1 0t0 TCP 127.0.0.1:${port} (LISTEN)`; + const external = (port: number) => + `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nsocat 1 u 6u IPv4 1 0t0 TCP 10.63.144.115:${port} (LISTEN)`; + const wildcard = (port: number) => + `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nx 1 u 3u IPv4 1 0t0 TCP *:${port} (LISTEN)`; + + it("treats an external-interface-only listener as non-blocking for the loopback bind", () => { + expect(lsofOutputBlocksLoopbackBind(external(18789), 18789)).toBe(false); + }); + + it("treats a loopback listener as blocking", () => { + expect(lsofOutputBlocksLoopbackBind(loopback(18789), 18789)).toBe(true); + }); + + it("treats a wildcard 0.0.0.0 or star listener as blocking, preserving docker-proxy detection (#3260)", () => { + expect(lsofOutputBlocksLoopbackBind(wildcard(18789), 18789)).toBe(true); + expect( + lsofOutputBlocksLoopbackBind("x 1 u 3u IPv4 1 0t0 TCP 0.0.0.0:18789 (LISTEN)", 18789), + ).toBe(true); + }); + + it("treats an IPv6 loopback listener as blocking", () => { + expect( + lsofOutputBlocksLoopbackBind("x 1 u 3u IPv6 1 0t0 TCP [::1]:18789 (LISTEN)", 18789), + ).toBe(true); + }); + + it("only matches the requested port", () => { + expect(lsofOutputBlocksLoopbackBind(loopback(18790), 18789)).toBe(false); + }); + + it("returns false for empty or missing output", () => { + expect(lsofOutputBlocksLoopbackBind("", 18789)).toBe(false); + expect(lsofOutputBlocksLoopbackBind(null, 18789)).toBe(false); + expect(lsofOutputBlocksLoopbackBind(undefined, 18789)).toBe(false); + }); + + it("ignores non-LISTEN rows", () => { + expect( + lsofOutputBlocksLoopbackBind( + "x 1 u 3u IPv4 1 0t0 TCP 127.0.0.1:18789->10.0.0.2:5000 (ESTABLISHED)", + 18789, + ), + ).toBe(false); + }); +}); + describe("typed OpenShell dashboard-port observation", () => { it("checks gateway authority once for each multi-port batch (#11963)", async () => { const observeForwards = vi.fn( diff --git a/src/lib/state/gateway-registry.ts b/src/lib/state/gateway-registry.ts index 0c4b1ebbc2d..ab4ab1b3c90 100644 --- a/src/lib/state/gateway-registry.ts +++ b/src/lib/state/gateway-registry.ts @@ -8,6 +8,7 @@ import { isErrnoException } from "../core/errno"; import { isObjectRecord } from "../core/json-types"; import { resolveLegacyModelRouterPort } from "../core/model-router-port"; import { DEFAULT_GATEWAY_PORT } from "../core/ports"; +import { isValidDashboardExternalUrl } from "../dashboard/url"; import { NAME_MAX_LENGTH, NAME_VALID_PATTERN } from "../name-validation"; import { resolveGatewayName, resolveGatewayPortFromName } from "../onboard/gateway-binding"; import { GATEWAYS_SUBDIR, nemoclawStateRoot } from "./state-root"; @@ -32,6 +33,7 @@ const MAX_GATEWAY_DIRECTORY_ENTRIES = 1024; export interface GatewayRegistryEntry extends Record { name: string; dashboardPort?: number | null; + dashboardExternalUrl?: string | null; hermesApiPort?: number | null; gatewayName?: string | null; gatewayPort?: number | null; @@ -128,6 +130,16 @@ function parseRegistry(filePath: string, raw: string): GatewayRegistryDocument { throw stateError(`${filePath} has an invalid ${field} for sandbox ${JSON.stringify(name)}`); } } + const externalUrl = value.dashboardExternalUrl; + if ( + externalUrl !== undefined && + externalUrl !== null && + (typeof externalUrl !== "string" || !isValidDashboardExternalUrl(externalUrl)) + ) { + throw stateError( + `${filePath} has an invalid dashboardExternalUrl for sandbox ${JSON.stringify(name)}`, + ); + } const gatewayStateDir = value.openshellGatewayStateDir; if ( gatewayStateDir !== undefined && From 6b0acb521f2644fb48f8a735fde875ff798d9047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 14:41:50 -0700 Subject: [PATCH 04/12] ci(e2e): renew retained native Podman toolchain references Reuse unchanged Podman 6.1.0 binaries from successful staging job 109299796467 in run 36534476155. Both archive digests, seven internal checksums per architecture, and architecture manifests were verified. Preserve all artifact identity and expiry checks. Signed-off-by: Aaron Erickson --- .../action.yaml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/actions/stage-native-podman-e2e-toolchains/action.yaml b/.github/actions/stage-native-podman-e2e-toolchains/action.yaml index cb5a3d48ff2..933f90eb76e 100644 --- a/.github/actions/stage-native-podman-e2e-toolchains/action.yaml +++ b/.github/actions/stage-native-podman-e2e-toolchains/action.yaml @@ -21,7 +21,7 @@ runs: shell: bash env: GH_TOKEN: ${{ inputs.github-token }} - SOURCE_RUN_ID: "33211526093" + SOURCE_RUN_ID: "36534476155" run: | set -euo pipefail verify_artifact() { @@ -39,13 +39,13 @@ runs: END { exit found ? 0 : 1 }' } verify_artifact \ - 10385514729 \ - native-runtime-podman-toolchain-amd64 \ - sha256:1f73b66ef2a70862e860b0897e191ed05b8976d3b10b0f84b41e76d8f1cb58ba + 11018865557 \ + native-podman-e2e-toolchain-amd64 \ + sha256:673dbb608ba6595e76ba8479d0e0680b41340546101c7938f28160470846fa8b verify_artifact \ - 10386378988 \ - native-runtime-podman-toolchain-arm64 \ - sha256:470487563f801c77b95f1665510fe1a11894c913e2787ebfb9994f56bac8cf56 + 11019020387 \ + native-podman-e2e-toolchain-arm64 \ + sha256:c6a23c1132b8c3011252035dc2dae73e2ed47ff97151ba41380e3b07acfe6cdc - name: Download immutable native Podman amd64 toolchain if: ${{ inputs.enabled == 'true' }} @@ -53,8 +53,8 @@ runs: with: github-token: ${{ inputs.github-token }} repository: NVIDIA/NemoClaw - run-id: "33211526093" - artifact-ids: "10385514729" + run-id: "36534476155" + artifact-ids: "11018865557" path: ${{ runner.temp }}/native-podman-e2e-toolchain-amd64 - name: Download immutable native Podman arm64 toolchain @@ -63,8 +63,8 @@ runs: with: github-token: ${{ inputs.github-token }} repository: NVIDIA/NemoClaw - run-id: "33211526093" - artifact-ids: "10386378988" + run-id: "36534476155" + artifact-ids: "11019020387" path: ${{ runner.temp }}/native-podman-e2e-toolchain-arm64 - name: Publish native Podman amd64 toolchain for this run From 5a257c7b3c350270b4fb27bc02619630c66d54c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 15:03:34 -0700 Subject: [PATCH 05/12] ci(e2e): activate renewed immutable Podman toolchain staging Pin the signed staging action and its reviewed content digest. Update existing mutation fixtures to the renewed IDs without changing assertions or adding cases. Application and managed-image inputs are unchanged from 362c0df032c22692fbeaae9c4f6fd9cdd1ca2c75. Signed-off-by: Aaron Erickson --- .github/workflows/e2e.yaml | 2 +- test/e2e/support/shared-e2e-workflow-boundary.test.ts | 4 ++-- tools/e2e/workflow-boundary-policy.mts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 8389e456d1a..e78789f395f 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -699,7 +699,7 @@ jobs: # Publish immutable source-run toolchains before candidate checkout or # candidate-controlled workspace preparation can execute on this runner. - name: Stage immutable native Podman E2E toolchains - uses: NVIDIA/NemoClaw/.github/actions/stage-native-podman-e2e-toolchains@8d7409d66a0e664829f9ddab177aa8460974291f + uses: NVIDIA/NemoClaw/.github/actions/stage-native-podman-e2e-toolchains@6b0acb521f2644fb48f8a735fde875ff798d9047 with: enabled: ${{ contains(format(',{0},', inputs.gateway_runtimes || inputs.gateway_runtime || 'docker'), ',podman,') && 'true' || 'false' }} github-token: ${{ github.token }} diff --git a/test/e2e/support/shared-e2e-workflow-boundary.test.ts b/test/e2e/support/shared-e2e-workflow-boundary.test.ts index ecadc8173a4..ec31a49c161 100644 --- a/test/e2e/support/shared-e2e-workflow-boundary.test.ts +++ b/test/e2e/support/shared-e2e-workflow-boundary.test.ts @@ -144,9 +144,9 @@ const stagingReferenceVariants = [ ]; const actionMutations: Array<[string, (source: string) => string]> = [ - ["artifact-id", (source) => source.replace('artifact-ids: "10385514729"', 'artifact-ids: "1"')], + ["artifact-id", (source) => source.replace('artifact-ids: "11018865557"', 'artifact-ids: "1"')], ["digest", (source) => source.replace(/sha256:[a-f0-9]{64}/, "sha256:" + "0".repeat(64))], - ["source-run", (source) => source.replace('run-id: "33211526093"', 'run-id: "1"')], + ["source-run", (source) => source.replace('run-id: "36534476155"', 'run-id: "1"')], [ "verification-order", (source) => { diff --git a/tools/e2e/workflow-boundary-policy.mts b/tools/e2e/workflow-boundary-policy.mts index 1b415ae26a7..152cc9917d5 100644 --- a/tools/e2e/workflow-boundary-policy.mts +++ b/tools/e2e/workflow-boundary-policy.mts @@ -23,8 +23,8 @@ export const E2E_ACTION_PROVENANCE = { }, stageNativePodmanToolchains: { reference: - "NVIDIA/NemoClaw/.github/actions/stage-native-podman-e2e-toolchains@8d7409d66a0e664829f9ddab177aa8460974291f", - contentSha256: "4178d1938477d197033b5e73cca34417eb9b31302af3714a2a7c584c2b0f2810", + "NVIDIA/NemoClaw/.github/actions/stage-native-podman-e2e-toolchains@6b0acb521f2644fb48f8a735fde875ff798d9047", + contentSha256: "83416ddcd1db9e9517c93e896a6204d281eac9725ec7e0892cd3318d6b7a824f", }, restoreCliArtifact: { reference: From 0461d683709f13aff7a6f1ca2a33b55d8b37461b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 15:36:59 -0700 Subject: [PATCH 06/12] fix(e2e): align CLI restoration with the current package Use the restore helper after the retired plugin migration module was removed. Keep the existing artifact identity, digest, and path checks intact. Validated with 304 existing support tests and restoration of the failed-run artifact. Signed-off-by: Aaron Erickson --- .github/workflows/e2e-standard-profile.yaml | 2 +- .github/workflows/e2e.yaml | 22 ++++++++++----------- tools/e2e/workflow-boundary-policy.mts | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/e2e-standard-profile.yaml b/.github/workflows/e2e-standard-profile.yaml index 97eac75e49c..73e5f2033d9 100644 --- a/.github/workflows/e2e-standard-profile.yaml +++ b/.github/workflows/e2e-standard-profile.yaml @@ -430,7 +430,7 @@ jobs: - name: Restore exact-commit CLI artifact if: ${{ inputs.restore_cli }} - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ inputs.cli_artifact_provenance }} diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index e78789f395f..d8417d6f983 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -2842,7 +2842,7 @@ jobs: "${test_evidence_dir}/dcode-base-image.json" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3050,7 +3050,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3306,7 +3306,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3382,7 +3382,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3677,7 +3677,7 @@ jobs: uses: NVIDIA/NemoClaw/.github/actions/install-reviewed-openshell-sdk@f880dd17b871a9a9440aa8468b55e96a4541dfd6 - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3834,7 +3834,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4117,7 +4117,7 @@ jobs: # The restore action executes the candidate CLI for its final identity # check. Candidate-controlled state starts with dependency preparation. - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5125,7 +5125,7 @@ jobs: uses: NVIDIA/NemoClaw/.github/actions/install-reviewed-openshell-sdk@f880dd17b871a9a9440aa8468b55e96a4541dfd6 - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5304,7 +5304,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5592,7 +5592,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5735,7 +5735,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} diff --git a/tools/e2e/workflow-boundary-policy.mts b/tools/e2e/workflow-boundary-policy.mts index 152cc9917d5..562ce89ae38 100644 --- a/tools/e2e/workflow-boundary-policy.mts +++ b/tools/e2e/workflow-boundary-policy.mts @@ -28,7 +28,7 @@ export const E2E_ACTION_PROVENANCE = { }, restoreCliArtifact: { reference: - "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4e9f579183477b984c009cce0f47a1361e5eddef", + "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@b1494a0828a80a8d5dc862effc9e85ac987f8b4a", contentSha256: "4a6a6b21993e579855916dfb897995a3f35dc4461d04666094af7eddb8676077", }, reviewedSdkInstall: { From 98aaedda4804010b0ef2277dee3ad8de14a6f030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 16:11:35 -0700 Subject: [PATCH 07/12] fix(ci): align the MCP restore sequence digest Record the reviewed restore-action pin in the existing MCP sequence digest. The sequence and its strict validation are otherwise unchanged. Signed-off-by: Aaron Erickson --- tools/e2e/mcp-dev-workflow-boundary-digests.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/e2e/mcp-dev-workflow-boundary-digests.mts b/tools/e2e/mcp-dev-workflow-boundary-digests.mts index e3c7c4627c7..2d075229ae6 100644 --- a/tools/e2e/mcp-dev-workflow-boundary-digests.mts +++ b/tools/e2e/mcp-dev-workflow-boundary-digests.mts @@ -12,7 +12,7 @@ export const MCP_DEV_TRUSTED_NODE_SETUP_CONTENT_SHA256 = export const MCP_DEV_TRUSTED_PREFIX_CONTENT_SHA256 = "a99862977077321de6f23184485a71e8dbbc2a40afb6d69aa39c82c38410646d"; export const MCP_DEV_POST_INSTALL_TRANSITION_CONTENT_SHA256 = - "ee24c392467389e40ef3a5880ea72f30bdbbed4e5715f7452ec38df7306fddb2"; + "876f82d9e4c623a404a9fff27a46a549690d7d6218626463c4af2774d2ef976a"; export function contentSha256(value: unknown): string { return createHash("sha256") From 7157a864fb9d52ad9ae6ad1656e45da030842006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 18:29:21 -0700 Subject: [PATCH 08/12] fix(deps): apply reviewed Undici audit repair Reuse the dependency-only repair from #12502 while retaining OpenClaw 2026.9.1. Pin the audit implementation to verified commit b0af4ef. Reuse verified AMD64 and ARM64 receipts with identical Pi image inputs. Existing tests, audit thresholds, and native plugin provenance stay intact. Signed-off-by: Aaron Erickson --- .github/workflows/managed-images.yaml | 8 +- .github/workflows/pr.yaml | 2 +- Dockerfile | 5 +- Dockerfile.base | 2 +- .../package-lock.json | 21 ++- .../package.json | 6 +- .../openclaw-runtime/package-lock.json | 6 +- agents/openclaw/openclaw-runtime/package.json | 3 +- ci/pi-agent-qualification-v1-linux-amd64.json | 8 +- ci/pi-agent-qualification-v1-linux-arm64.json | 8 +- ci/reviewed-npm-audit.json | 2 +- package-lock.json | 8 +- package.json | 2 +- scripts/lib/openclaw-npm-remediation.mts | 131 +++++++++++++++++- src/lib/agent/candidate-authority.ts | 4 +- .../openclaw/openclaw-locked-install.test.ts | 4 +- ...aw-managed-messaging-offline-build.test.ts | 1 + .../releases/reviewed-npm-audit-fixtures.ts | 5 +- ...managed-image-publication-workflow.test.ts | 2 +- .../managed-image-registry-transport.test.ts | 4 +- 20 files changed, 187 insertions(+), 45 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index d5c34ca41dc..2ace070d884 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -101,7 +101,7 @@ jobs: - name: Checkout npm audit code from the base commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: d60ee0bb36e582f83846f41a1b7e94719fcd89f6 + ref: b0af4ef64b8257372f2ad390ce6c843c96bbfebb path: .trusted-reviewed-npm-audit persist-credentials: false sparse-checkout: | @@ -121,7 +121,7 @@ jobs: shell: bash env: CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }} - REVIEWED_AUDIT_SHA: d60ee0bb36e582f83846f41a1b7e94719fcd89f6 + REVIEWED_AUDIT_SHA: b0af4ef64b8257372f2ad390ce6c843c96bbfebb run: | set -euo pipefail [[ "$CANDIDATE_SHA" =~ ^[a-f0-9]{40}$ ]] @@ -544,7 +544,7 @@ jobs: if: matrix.agent == 'openclaw' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: d60ee0bb36e582f83846f41a1b7e94719fcd89f6 + ref: b0af4ef64b8257372f2ad390ce6c843c96bbfebb path: .trusted-mcporter-audit persist-credentials: false sparse-checkout: | @@ -572,7 +572,7 @@ jobs: raw="$RUNNER_TEMP/reviewed-npm-audit/mcporter-runtime.raw.json" policy="$RUNNER_TEMP/reviewed-npm-audit/mcporter-runtime.policy.json" trusted_root="$GITHUB_WORKSPACE/.trusted-mcporter-audit" - test "$(git -C "$trusted_root" rev-parse --verify HEAD)" = 'd60ee0bb36e582f83846f41a1b7e94719fcd89f6' + test "$(git -C "$trusted_root" rev-parse --verify HEAD)" = 'b0af4ef64b8257372f2ad390ce6c843c96bbfebb' node --experimental-strip-types --no-warnings \ "$trusted_root/scripts/lib/npm-audit-receipt.mts" \ --receipt "$receipt" \ diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 58a5d9ef961..77c05b3fbd2 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -532,7 +532,7 @@ jobs: sparse-checkout-cone-mode: false - name: Audit reviewed production npm graphs - uses: NVIDIA/NemoClaw/.github/actions/ci-reviewed-npm-audit@d60ee0bb36e582f83846f41a1b7e94719fcd89f6 + uses: NVIDIA/NemoClaw/.github/actions/ci-reviewed-npm-audit@b0af4ef64b8257372f2ad390ce6c843c96bbfebb with: target-root: ${{ github.workspace }} report-dir: artifacts/reviewed-npm-audit diff --git a/Dockerfile b/Dockerfile index 6a8de04e243..ca6afd23e9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -522,7 +522,8 @@ ADD --chmod=0444 --checksum=sha256:f3fb42099ea7a0efa2753b3e770fa0d505714e1c7d75f FROM scratch AS openclaw-managed-messaging-npm-common-archives-5 ADD --chmod=0444 --checksum=sha256:65834dc9ce7ecceff4334a14796c85960cbf665d09364698bf3196ceed04d677 https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz /uint8array-extras-1.5.0.tgz -ADD --chmod=0444 --checksum=sha256:9d72c56c17ad2b3d66f006d53945374cc0d2bc68f322439495b972269f4de6bc https://registry.npmjs.org/undici/-/undici-8.10.0.tgz /undici-8.10.0.tgz +ADD --chmod=0444 --checksum=sha256:93b3abe22a9d2858938b5f3829a9fd8952392348ffed9057662ae846e2e46d54 https://registry.npmjs.org/undici/-/undici-7.29.1.tgz /undici-7.29.1.tgz +ADD --chmod=0444 --checksum=sha256:740638ae32d78d2646a6727950e365fa26b6fa87913fa096e60ed4afeb4634aa https://registry.npmjs.org/undici/-/undici-8.10.2.tgz /undici-8.10.2.tgz ADD --chmod=0444 --checksum=sha256:07a721cb2cd0dd798c24757de34d14e8b640ff8fddef85d662e00b392562a1f2 https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz /undici-types-8.3.0.tgz ADD --chmod=0444 --checksum=sha256:e4bfbbe867144ff24f73198367479378c8b6cffc798a2ec0756a81097606908e https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz /unicorn-magic-0.3.0.tgz ADD --chmod=0444 --checksum=sha256:2dfb5e06d1d4bf1fe9f0fa7f633c4a2fde04d8b41cf0b9bd249a42561d5edfb6 https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz /unpipe-1.0.0.tgz @@ -943,7 +944,7 @@ RUN --mount=type=secret,id=nemoclaw-mcporter-audit-receipt,required=false \ OPENCLAW_LOCK_SHA256=none-legacy-fixture; \ OPENCLAW_RECIPE='ignore-scripts+reviewed-lifecycle-v1'; \ if [ "$OPENCLAW_VERSION" = "2026.9.1" ]; then \ - OPENCLAW_LOCK_SHA256=c015570ccccf56986c3d92a85de6f7aa507110f6a11cc2eedd80752672589f9b; \ + OPENCLAW_LOCK_SHA256=71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113; \ ACTUAL_OPENCLAW_LOCK_SHA256="$(sha256sum /usr/local/lib/nemoclaw/openclaw-runtime/package-lock.json | awk '{print $1}')"; \ [ "$ACTUAL_OPENCLAW_LOCK_SHA256" = "$OPENCLAW_LOCK_SHA256" ] \ || { echo "ERROR: OpenClaw lock SHA-256 mismatch (expected $OPENCLAW_LOCK_SHA256, found $ACTUAL_OPENCLAW_LOCK_SHA256)" >&2; exit 1; }; \ diff --git a/Dockerfile.base b/Dockerfile.base index edd2f4aeea4..17c143c6fb1 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -515,7 +515,7 @@ RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/bluep OPENCLAW_LOCK_SHA256=none-legacy-fixture; \ OPENCLAW_RECIPE='ignore-scripts+reviewed-lifecycle-v1'; \ if [ "$OPENCLAW_VERSION" = "2026.9.1" ]; then \ - OPENCLAW_LOCK_SHA256=c015570ccccf56986c3d92a85de6f7aa507110f6a11cc2eedd80752672589f9b; \ + OPENCLAW_LOCK_SHA256=71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113; \ ACTUAL_OPENCLAW_LOCK_SHA256="$(sha256sum /usr/local/lib/nemoclaw/openclaw-runtime/package-lock.json | awk '{print $1}')"; \ [ "$ACTUAL_OPENCLAW_LOCK_SHA256" = "$OPENCLAW_LOCK_SHA256" ] \ || { echo "Error: OpenClaw lock SHA-256 mismatch (expected $OPENCLAW_LOCK_SHA256, found $ACTUAL_OPENCLAW_LOCK_SHA256)"; exit 1; }; \ diff --git a/agents/openclaw/managed-image-messaging-runtime/package-lock.json b/agents/openclaw/managed-image-messaging-runtime/package-lock.json index c5f33757146..0e2f60d3883 100644 --- a/agents/openclaw/managed-image-messaging-runtime/package-lock.json +++ b/agents/openclaw/managed-image-messaging-runtime/package-lock.json @@ -24,9 +24,10 @@ "nemoclaw-openclaw-override-ip-address": "npm:ip-address@10.5.0", "nemoclaw-openclaw-override-protobufjs": "npm:protobufjs@8.7.2", "nemoclaw-openclaw-override-qs": "npm:qs@6.15.3", + "nemoclaw-openclaw-override-undici7": "npm:undici@7.29.1", "nemoclaw-openclaw-override-uuid": "npm:uuid@14.0.2", "openclaw": "2026.9.1", - "undici": "8.10.0" + "undici": "8.10.2" }, "engines": { "node": ">=22.19.0" @@ -7435,6 +7436,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/nemoclaw-openclaw-override-undici7": { + "name": "undici", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/nemoclaw-openclaw-override-uuid": { "name": "uuid", "version": "14.0.2", @@ -7746,7 +7757,7 @@ "tslog": "4.11.0", "typebox": "1.3.17", "typescript": "6.0.3", - "undici": "8.10.0", + "undici": "8.10.2", "web-push": "3.6.7", "web-tree-sitter": "0.26.13", "ws": "8.21.3", @@ -8868,9 +8879,9 @@ } }, "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/agents/openclaw/managed-image-messaging-runtime/package.json b/agents/openclaw/managed-image-messaging-runtime/package.json index d6c60274349..f3c37022a58 100644 --- a/agents/openclaw/managed-image-messaging-runtime/package.json +++ b/agents/openclaw/managed-image-messaging-runtime/package.json @@ -21,9 +21,10 @@ "nemoclaw-openclaw-override-ip-address": "npm:ip-address@10.5.0", "nemoclaw-openclaw-override-protobufjs": "npm:protobufjs@8.7.2", "nemoclaw-openclaw-override-qs": "npm:qs@6.15.3", + "nemoclaw-openclaw-override-undici7": "npm:undici@7.29.1", "nemoclaw-openclaw-override-uuid": "npm:uuid@14.0.2", "openclaw": "2026.9.1", - "undici": "8.10.0" + "undici": "8.10.2" }, "engines": { "node": ">=22.19.0" @@ -39,6 +40,7 @@ "file-type": "22.0.2", "protobufjs": "8.7.2" } - } + }, + "undici@8.10.0": "8.10.2" } } diff --git a/agents/openclaw/openclaw-runtime/package-lock.json b/agents/openclaw/openclaw-runtime/package-lock.json index 8b12e7cc10c..76b3d67330f 100644 --- a/agents/openclaw/openclaw-runtime/package-lock.json +++ b/agents/openclaw/openclaw-runtime/package-lock.json @@ -4462,9 +4462,9 @@ } }, "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/agents/openclaw/openclaw-runtime/package.json b/agents/openclaw/openclaw-runtime/package.json index 41d71bbfe20..ab61e7a02a2 100644 --- a/agents/openclaw/openclaw-runtime/package.json +++ b/agents/openclaw/openclaw-runtime/package.json @@ -11,7 +11,8 @@ "overrides": { "hono": "4.12.34", "tar": "7.5.21", - "fast-uri": "3.1.7" + "fast-uri": "3.1.7", + "undici": "8.10.2" }, "engines": { "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0" diff --git a/ci/pi-agent-qualification-v1-linux-amd64.json b/ci/pi-agent-qualification-v1-linux-amd64.json index 8641a360d9f..703d0df19db 100644 --- a/ci/pi-agent-qualification-v1-linux-amd64.json +++ b/ci/pi-agent-qualification-v1-linux-amd64.json @@ -3,13 +3,13 @@ "agent": "pi", "platform": "linux/amd64", "image": "ghcr.io/nvidia/nemoclaw/pi-sandbox", - "digest": "sha256:2585d6f48d1a6d6c059b59e8c273fb08274b4e8af8ba9d0db59f0df45fc4f479", - "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:2585d6f48d1a6d6c059b59e8c273fb08274b4e8af8ba9d0db59f0df45fc4f479", + "digest": "sha256:fb574463bf5c328f183eda582d9270259198574a0291358f00b6bb4d67e769f0", + "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:fb574463bf5c328f183eda582d9270259198574a0291358f00b6bb4d67e769f0", "source": { "repository": "NVIDIA/NemoClaw", - "revision": "d60ee0bb36e582f83846f41a1b7e94719fcd89f6", + "revision": "b0af4ef64b8257372f2ad390ce6c843c96bbfebb", "release": "v0.1.0", - "cohort": "ghrun-36504446248-1" + "cohort": "ghrun-36649400053-1" }, "startupProfileContractVersion": 1, "capabilityContractVersion": 1 diff --git a/ci/pi-agent-qualification-v1-linux-arm64.json b/ci/pi-agent-qualification-v1-linux-arm64.json index 596cda3930c..0b9da42216d 100644 --- a/ci/pi-agent-qualification-v1-linux-arm64.json +++ b/ci/pi-agent-qualification-v1-linux-arm64.json @@ -3,13 +3,13 @@ "agent": "pi", "platform": "linux/arm64", "image": "ghcr.io/nvidia/nemoclaw/pi-sandbox", - "digest": "sha256:de600347e74f8bd4be17b572b079159f98037200477505c718b5b0f3f47b66fb", - "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:de600347e74f8bd4be17b572b079159f98037200477505c718b5b0f3f47b66fb", + "digest": "sha256:4fc50eddab478f83af90e452be85fe1cffcf9c5a11f1ac54a5c2189a3e6136df", + "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:4fc50eddab478f83af90e452be85fe1cffcf9c5a11f1ac54a5c2189a3e6136df", "source": { "repository": "NVIDIA/NemoClaw", - "revision": "d60ee0bb36e582f83846f41a1b7e94719fcd89f6", + "revision": "b0af4ef64b8257372f2ad390ce6c843c96bbfebb", "release": "v0.1.0", - "cohort": "ghrun-36504446248-1" + "cohort": "ghrun-36649400053-1" }, "startupProfileContractVersion": 1, "capabilityContractVersion": 1 diff --git a/ci/reviewed-npm-audit.json b/ci/reviewed-npm-audit.json index b3cf720e3f5..fe86da71421 100644 --- a/ci/reviewed-npm-audit.json +++ b/ci/reviewed-npm-audit.json @@ -101,7 +101,7 @@ "integrity": "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w==", "tarballUrl": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz", "directory": "agents/openclaw/openclaw-runtime", - "lockSha256": "c015570ccccf56986c3d92a85de6f7aa507110f6a11cc2eedd80752672589f9b" + "lockSha256": "71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113" }, { "id": "mcporter-runtime", diff --git a/package-lock.json b/package-lock.json index 3d4c9a1bea2..d88e46d474c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "qrcode-terminal": "^0.12.0", "smol-toml": "1.8.0", "typebox": "1.1.38", - "undici": "8.10.0", + "undici": "8.10.2", "yaml": "2.8.3" }, "bin": { @@ -8515,9 +8515,9 @@ } }, "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/package.json b/package.json index a4beb97d85a..99fc3f04759 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "qrcode-terminal": "^0.12.0", "smol-toml": "1.8.0", "typebox": "1.1.38", - "undici": "8.10.0", + "undici": "8.10.2", "yaml": "2.8.3" }, "optionalDependencies": { diff --git a/scripts/lib/openclaw-npm-remediation.mts b/scripts/lib/openclaw-npm-remediation.mts index 39d1411f59e..eef0a35761b 100755 --- a/scripts/lib/openclaw-npm-remediation.mts +++ b/scripts/lib/openclaw-npm-remediation.mts @@ -27,8 +27,8 @@ type JsonObject = Record; type Remediation = Readonly<{ expectedPatchedMetadataIntegrity?: string; expectedPatchedTreeIntegrity?: string; - kind: "axios" | "core" | "current-core" | "jaeger" | "legacy-core" | "undici"; - version: "2026.3.11" | "2026.6.10" | "2026.7.1"; + kind: "axios" | "core" | "current-core" | "jaeger" | "legacy-core" | "undici" | "undici-security"; + version: "2026.3.11" | "2026.6.10" | "2026.7.1" | "2026.9.1"; }>; type RemediationRequest = Readonly<{ @@ -107,6 +107,47 @@ const CURRENT_UNDICI_VERSION = "8.10.0"; const CURRENT_UNDICI_INTEGRITY = "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="; const CURRENT_UNDICI_TARBALL = "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz"; +const UNDICI_SECURITY_TARGETS: Readonly< + Record< + string, + Readonly<{ + name: string; + previous: string; + version: string; + integrity: string; + node: string; + bundled: boolean; + }> + > +> = Object.freeze({ + "openclaw@2026.9.1": { + name: "openclaw", + previous: "8.10.0", + version: "8.10.2", + bundled: false, + integrity: + "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + node: ">=22.19.0", + }, + "@openclaw/discord@2026.9.1": { + name: "@openclaw/discord", + previous: "8.10.0", + version: "8.10.2", + bundled: true, + integrity: + "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + node: ">=22.19.0", + }, + "@openclaw/slack@2026.9.1": { + name: "@openclaw/slack", + previous: "7.29.0", + version: "7.29.1", + bundled: true, + integrity: + "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + node: ">=20.18.1", + }, +}); const CURRENT_IP_ADDRESS_VERSION = "10.3.1"; const CURRENT_IP_ADDRESS_INTEGRITY = "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g=="; @@ -126,6 +167,25 @@ const OTEL_CORE_INTEGRITY = const OTEL_CORE_TARBALL = "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz"; const REMEDIATIONS: Readonly> = Object.freeze({ + // Retire these after a reviewed OpenClaw release ships patched Undici dependencies and bundles. + "openclaw@2026.9.1": { + kind: "undici-security", + version: "2026.9.1", + expectedPatchedTreeIntegrity: + "sha512-XLwwECWs8nL8/WJ/PplTcL0GyG6VGt0nN8K9idVv3/lKZiMAsJM1aw7SKj68b9Co9UD2iEhWfQKaGC6Pgoeniw==", + }, + "@openclaw/discord@2026.9.1": { + kind: "undici-security", + version: "2026.9.1", + expectedPatchedTreeIntegrity: + "sha512-UFBks0k94yKmtnD/D6I908I5vQwlUSohBFdjLdHFhf3cS0SeBpTDMoflBixsj6C3I/QWg6xhBPWJD1o4riDtsQ==", + }, + "@openclaw/slack@2026.9.1": { + kind: "undici-security", + version: "2026.9.1", + expectedPatchedTreeIntegrity: + "sha512-iLZXmYOy8g++8oqMEP95lCYLH0OgD5jm0QiqoEQTMYgMiMPrBRjwpX406n5SKIqTYW8bnTGDlTVoTEjMO/dJYQ==", + }, "@openclaw/diagnostics-otel@2026.6.10": { expectedPatchedMetadataIntegrity: "sha512-ByLYBs3KXz3u0mPuj9DcP/xPTJNgQaLTPxazybhyIC1VjyftEmKQuoZufPZ8z8CjwBsOPm6NbjMQB2BfX36TTg==", @@ -1124,6 +1184,69 @@ function packReplacement( }); } +export function patchOpenClawUndiciDependency(packageDirectory: string, packageSpec: string): void { + const target = UNDICI_SECURITY_TARGETS[packageSpec]; + if (!target) throw new Error(`No Undici security update is defined for ${packageSpec}`); + const manifestPath = join(packageDirectory, "package.json"); + const manifest = readJson(manifestPath); + requirePackageIdentity(manifest, target.name, "2026.9.1", packageSpec); + const bundledPath = join(packageDirectory, "node_modules", "undici"); + const bundles = manifest.bundleDependencies ?? manifest.bundledDependencies ?? []; + if ( + manifest.dependencies?.undici !== target.previous || + !Array.isArray(bundles) || + bundles.includes("undici") !== target.bundled || + existsSync(bundledPath) !== target.bundled || + existsSync(join(packageDirectory, "npm-shrinkwrap.json")) + ) { + throw new Error(`${packageSpec} Undici dependency contract changed after review`); + } + if (target.bundled) { + requirePackageIdentity( + readJson(join(bundledPath, "package.json")), + "undici", + target.previous, + packageSpec, + ); + } + manifest.dependencies.undici = target.version; + writeJson(manifestPath, manifest); +} + +function remediateSecurityUndici( + sourcePackage: string, + packageSpec: string, + remediationRoot: string, + env: NodeJS.ProcessEnv, +): void { + const target = UNDICI_SECURITY_TARGETS[packageSpec]; + if (!target) throw new Error(`No Undici security update is defined for ${packageSpec}`); + const replacement = packReplacement( + `undici@${target.version}`, + target.integrity, + `https://registry.npmjs.org/undici/-/undici-${target.version}.tgz`, + remediationRoot, + env, + ); + const directory = extractArchive( + replacement.archivePath, + join(remediationRoot, "undici"), + remediationRoot, + env, + ); + const manifest = readJson(join(directory, "package.json")); + requirePackageIdentity(manifest, "undici", target.version, "Undici security update"); + if ( + Object.keys(manifest.dependencies ?? {}).length !== 0 || + manifest.engines?.node !== target.node + ) { + throw new Error(`undici@${target.version} package contract changed after review`); + } + patchOpenClawUndiciDependency(sourcePackage, packageSpec); + if (target.bundled) + copyReplacementPackage(directory, join(sourcePackage, "node_modules", "undici")); +} + export function buildRemediatedOpenClawPluginArchive( request: BuildRequest, ): Extract { @@ -1149,7 +1272,9 @@ export function buildRemediatedOpenClawPluginArchive( remediationRoot, env, ); - if (remediation.kind === "core") { + if (remediation.kind === "undici-security") { + remediateSecurityUndici(sourcePackage, request.packageSpec, remediationRoot, env); + } else if (remediation.kind === "core") { const fsSafeArchive = packReplacement( `@openclaw/fs-safe@${FS_SAFE_VERSION}`, FS_SAFE_INTEGRITY, diff --git a/src/lib/agent/candidate-authority.ts b/src/lib/agent/candidate-authority.ts index ab1d87be2c0..a83655eec88 100644 --- a/src/lib/agent/candidate-authority.ts +++ b/src/lib/agent/candidate-authority.ts @@ -13,8 +13,8 @@ export const CANDIDATE_QUALIFICATION_RECEIPT_DIGESTS: Readonly< Record > = Object.freeze({ pi: Object.freeze([ - "d15d60a0dc53885c825acde83ad426b87b61dc942036446438846f471d90fb57", - "20d963079d246a59f5015d1c9324b5531301336f2d366a7f7b3da54f1fa12075", + "cc2d180d0a7f145e2320aa92ff00bf06a377c623375508f2704998f8b21c5f6e", + "96aaaefb99852685aa34cc76feb7e7deca4e18b80d818344ecab94f50e934e64", ]), }); diff --git a/test/agents/openclaw/openclaw-locked-install.test.ts b/test/agents/openclaw/openclaw-locked-install.test.ts index d2b424c80da..d3e2146bd35 100644 --- a/test/agents/openclaw/openclaw-locked-install.test.ts +++ b/test/agents/openclaw/openclaw-locked-install.test.ts @@ -19,7 +19,7 @@ const PACKAGE_SPEC = "openclaw@2026.9.1"; const INTEGRITY = "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w=="; const TARBALL = "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz"; -const LOCK_SHA256 = "c015570ccccf56986c3d92a85de6f7aa507110f6a11cc2eedd80752672589f9b"; +const LOCK_SHA256 = "71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113"; const roots: string[] = []; function sha256(file: string): string { @@ -180,7 +180,7 @@ describe("locked OpenClaw production installation (#5896)", () => { expect(verified).toContain("ip-address@10.7.0"); expect(verified).toContain("tar@7.5.21"); expect(verified).not.toContain("tar@7.5.19"); - expect(verified).toContain("undici@8.10.0"); + expect(verified).toContain("undici@8.10.2"); expect(sha256(LOCKFILE)).toBe(LOCK_SHA256); }); diff --git a/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts b/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts index a9e009859c5..a415d62e73e 100644 --- a/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts +++ b/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts @@ -78,6 +78,7 @@ describe("OpenClaw managed messaging offline image build", () => { }; expect(runtimeManifest.overrides).toEqual({ + "undici@8.10.0": "8.10.2", "@openclaw/discord@2026.9.1": { "@discord/embedded-app-sdk@2.5.0": { uuid: bundledVersion( diff --git a/test/automation/releases/reviewed-npm-audit-fixtures.ts b/test/automation/releases/reviewed-npm-audit-fixtures.ts index 04781cc3e2c..4c01268b1d6 100644 --- a/test/automation/releases/reviewed-npm-audit-fixtures.ts +++ b/test/automation/releases/reviewed-npm-audit-fixtures.ts @@ -12,7 +12,7 @@ export type LockedGraphFixture = Readonly<{ manifest: Buffer; }>; -export function openClawReplacementGraphFixture( +export function openClawReplacementGraphFixture( repoRoot: string, graph: T, ): LockedGraphFixture { @@ -27,7 +27,8 @@ export function openClawReplacementGraphFixture( packages: { "": Record }; }; return { - graph, + // The historical fixture keeps its original graph when production dependencies advance. + graph: { ...graph, lockSha256: createHash("sha256").update(lock).digest("hex") }, lock, manifest: Buffer.from(`${JSON.stringify(parsedLock.packages[""], null, 2)}\n`), }; diff --git a/test/inference/managed/managed-image-publication-workflow.test.ts b/test/inference/managed/managed-image-publication-workflow.test.ts index 6e25a2e6ce9..51fcb48b39e 100644 --- a/test/inference/managed/managed-image-publication-workflow.test.ts +++ b/test/inference/managed/managed-image-publication-workflow.test.ts @@ -32,7 +32,7 @@ import type { Job, Workflow } from "../../helpers/managed-image-publication-work const fullShaAction = /^[^@]+@[0-9a-f]{40}$/iu; const reviewedAuditAction = "NVIDIA/NemoClaw/.github/actions/ci-reviewed-npm-audit@"; -const reviewedAuditSha = "d60ee0bb36e582f83846f41a1b7e94719fcd89f6"; +const reviewedAuditSha = "b0af4ef64b8257372f2ad390ce6c843c96bbfebb"; function needsOutput(job: string, output: string): string { return `\${{ needs.${job}.outputs.${output} }}`; diff --git a/test/package-contract/managed-image-registry-transport.test.ts b/test/package-contract/managed-image-registry-transport.test.ts index 340f2f127a1..7e2669fc477 100644 --- a/test/package-contract/managed-image-registry-transport.test.ts +++ b/test/package-contract/managed-image-registry-transport.test.ts @@ -35,7 +35,7 @@ describe("managed image registry transport package contract", () => { const productionDependencies = JSON.parse(productionTree.stdout) as { dependencies?: { undici?: { version?: string } }; }; - expect(productionDependencies.dependencies?.undici?.version).toBe("8.10.0"); + expect(productionDependencies.dependencies?.undici?.version).toBe("8.10.2"); const fixtureRoot = createPackageFixture({ prefix: "nemoclaw-managed-registry-pack-", @@ -101,7 +101,7 @@ describe("managed image registry transport package contract", () => { const installedProductionDependencies = JSON.parse(installedProductionTree.stdout) as { dependencies?: { undici?: { version?: string } }; }; - expect(installedProductionDependencies.dependencies?.undici?.version).toBe("8.10.0"); + expect(installedProductionDependencies.dependencies?.undici?.version).toBe("8.10.2"); const probe = spawnSync( process.execPath, From 788dab469770c4593d9aa4c12fcf2ce3be345c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 18:34:12 -0700 Subject: [PATCH 09/12] chore(images): retain the existing Dockerfile size budget Use the concise runtime-install comments from the reviewed dependency repair. Preserve every executable instruction and the existing size limits. Signed-off-by: Aaron Erickson --- Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index ca6afd23e9f..bb56b4ebcf3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -908,10 +908,8 @@ COPY --from=codex-acp-runtime /usr/local/lib/node_modules/@zed-industries/ /usr/ COPY --from=codex-acp-runtime /usr/local/bin/codex-acp /usr/local/bin/codex-acp RUN command -v codex-acp >/dev/null -# Upgrade stale bases. Reuse is restricted to matching provenance from an -# official digest-pinned base; mutable/custom bases reinstall the locked graphs. -# OPENCLAW_VERSION is the NemoClaw runtime build target and must meet the blueprint minimum. -# Reviewed archives retain registry and packed-byte SRI, basename, local-only install, and cleanup gates. +# OPENCLAW_VERSION is the NemoClaw runtime build target; enforce the blueprint minimum. +# Reuse matching official digest-pinned bases; reinstall other verified locked graphs. # hadolint ignore=DL3059,DL4006,DL3016,SC2015 RUN --mount=type=secret,id=nemoclaw-mcporter-audit-receipt,required=false \ --mount=type=secret,id=nemoclaw-mcporter-audit-raw-report,required=false \ From b800475a030da1c299dbbb7e92a7fa3c2fdb9909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 19:54:14 -0700 Subject: [PATCH 10/12] fix(onboard): persist the selected native gateway port Write the selected startup port alongside the gateway token through the existing config owner and atomic write. Native pairing clears environment overrides, so its config must match the gateway's actual listening port. Keep one-shot token repair unchanged and supply the selected port in the existing late-startup fixture. Add no test cases. Signed-off-by: Aaron Erickson --- scripts/nemoclaw-start.sh | 17 +++++++++++++---- .../openclaw/runtime/nemoclaw-start.test.ts | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index bc8cb4258ac..c7991504ec8 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -1492,7 +1492,7 @@ ensure_gateway_token() { local _write_rc=0 run_openclaw_config_as_owner /usr/local/bin/node - \ - "$config_file" <<'NODETOKEN' || _write_rc=$? + "$config_file" "${1:-}" <<'NODETOKEN' || _write_rc=$? const crypto = require("crypto"); const fs = require("fs"); const pathModule = require("path"); @@ -1545,6 +1545,15 @@ function makeTempPath(dirPath) { try { const cfg = parseConfig(fs.readFileSync(path, "utf8")); const gateway = cfg.gateway && typeof cfg.gateway === "object" ? cfg.gateway : (cfg.gateway = {}); + // Pairing commands clear environment overrides and read this native port. + const startupPort = process.argv[3]; + if (startupPort) { + const port = Number(startupPort); + if (!Number.isInteger(port) || port < 1024 || port > 65535) { + throw new Error("invalid selected gateway startup port"); + } + gateway.port = port; + } const auth = gateway.auth && typeof gateway.auth === "object" ? gateway.auth : (gateway.auth = {}); auth.token = tokenUrlSafe(32); // OpenClaw 2026.9.1 rejects the legacy timestamp key. Scrub it defensively @@ -1641,7 +1650,7 @@ needs_gateway_token_for_current_command() { prepare_gateway_token_for_current_command() { if [ ${#NEMOCLAW_CMD[@]} -eq 0 ]; then - ensure_gateway_token + ensure_gateway_token "${1:-}" return $? fi @@ -4811,7 +4820,7 @@ if [ "$(id -u)" -ne 0 ]; then _nemoclaw_capture_epoch_realtime _NEMOCLAW_GATEWAY_CONFIG_FINISHED_EPOCH _nemoclaw_capture_epoch_realtime _NEMOCLAW_GATEWAY_PROVIDER_FINISHED_EPOCH refresh_openclaw_provider_placeholders - prepare_gateway_token_for_current_command + prepare_gateway_token_for_current_command "$_DASHBOARD_PORT" export_gateway_token _nemoclaw_capture_epoch_realtime _NEMOCLAW_GATEWAY_TOKEN_FINISHED_EPOCH write_messaging_runtime_setup_plan @@ -4910,7 +4919,7 @@ if is_managed_inference_route; then fi run_requested_openclaw_post_upgrade_doctor || exit 1 refresh_openclaw_provider_placeholders -prepare_gateway_token_for_current_command +prepare_gateway_token_for_current_command "$_DASHBOARD_PORT" export_gateway_token write_messaging_runtime_setup_plan write_runtime_shell_env diff --git a/test/agents/openclaw/runtime/nemoclaw-start.test.ts b/test/agents/openclaw/runtime/nemoclaw-start.test.ts index daa7b087c11..8367a00bd1a 100644 --- a/test/agents/openclaw/runtime/nemoclaw-start.test.ts +++ b/test/agents/openclaw/runtime/nemoclaw-start.test.ts @@ -2964,12 +2964,12 @@ describe("Telegram diagnostics (#2766)", () => { "chown_tree_no_symlink_follow() { :; }", "start_persistent_gateway_log_mirror() { :; }", 'setpriv() { while [ "$1" != "--" ]; do shift; done; shift; "$@"; }', - // Test scaffolding skips sandbox-init.sh, so define the shared - // privilege-transition prefixes here. + // This fixture skips sandbox-init.sh and early startup selection. "STEP_DOWN_PREFIX_SANDBOX=(setpriv --reuid=sandbox --regid=sandbox --init-groups --)", "STEP_DOWN_PREFIX_GATEWAY=(setpriv --reuid=gateway --regid=gateway --init-groups --)", 'validate_tmp_permissions() { printf "VALIDATE:%s\\n" "$*"; }', "_SANDBOX_HOME=/sandbox", + "_DASHBOARD_PORT=18789", `_SANDBOX_SAFETY_NET=${JSON.stringify(path.join(tmpDir, "safety.js"))}`, `_PROXY_FIX_SCRIPT=${JSON.stringify(path.join(tmpDir, "proxy-fix.js"))}`, `_NEMOTRON_FIX_SCRIPT=${JSON.stringify(path.join(tmpDir, "nemotron-fix.js"))}`, From adb6a958273d8bfef7663114c9bc1a30a1706add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Tue, 29 Sep 2026 21:12:35 -0700 Subject: [PATCH 11/12] fix(onboard): restart external images through OpenShell Reload initial external-image inference configuration through the existing OpenShell stop/start API. Hold the lifecycle lock and bind both transitions to the captured sandbox identity, then wait for the native gateway. This avoids requiring additional grants for the agent's CLI device during initial setup. Other workload kinds retain their native restart path. Signed-off-by: Aaron Erickson --- .../openclaw/initial-inference-route.ts | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/lib/onboard/openclaw/initial-inference-route.ts b/src/lib/onboard/openclaw/initial-inference-route.ts index b2de3a7b872..b757f7323b4 100644 --- a/src/lib/onboard/openclaw/initial-inference-route.ts +++ b/src/lib/onboard/openclaw/initial-inference-route.ts @@ -11,6 +11,12 @@ const initialOpenclawInferenceRouteRuntime = { loadSandboxConfig: () => require("../../sandbox/config") as typeof import("../../sandbox/config"), loadFinalizationDeps: () => require("../machine/finalization-deps") as typeof import("../machine/finalization-deps"), + loadRegistry: () => + require("../../state/registry/persistence") as typeof import("../../state/registry/persistence"), + loadLifecycleLock: () => + require("../../actions/sandbox/lifecycle/lock") as typeof import("../../actions/sandbox/lifecycle/lock"), + loadOpenShellLifecycle: () => + require("../../adapters/openshell/sandbox-lifecycle-sdk") as typeof import("../../adapters/openshell/sandbox-lifecycle-sdk"), }; export interface InitialOpenclawInferenceRouteDeps { @@ -53,6 +59,58 @@ export type InitializeOpenclawInferenceRoute = ( revalidateSandboxIdentity?: (operation: string) => void, ) => Promise; +async function restartInitialExternalOpenclawSandbox( + sandboxName: string, + gatewayName: string, + expectedIdentity: string | undefined, +): ReturnType { + const runtime = initialOpenclawInferenceRouteRuntime; + return runtime.loadLifecycleLock().withSandboxLifecycleLock(sandboxName, async () => { + const sandbox = runtime.loadRegistry().load().sandboxes[sandboxName]; + if ( + sandbox?.workload?.kind !== "external-image" || + sandbox.openshellDriver !== "docker" || + !expectedIdentity || + sandbox.lifecycleLiveIdentityFingerprint !== expectedIdentity || + (sandbox.gatewayName && sandbox.gatewayName !== gatewayName) + ) { + return { + ok: false as const, + failureLayer: "OpenShell lifecycle identity", + detail: "External-image sandbox ownership changed before initial restart.", + }; + } + const lifecycle = runtime.loadOpenShellLifecycle().createSdkOpenShellSandboxStateLifecycle(); + const request = { + sandboxName, + sandboxIdentityFingerprint: expectedIdentity, + target: { kind: "named" as const, gatewayName }, + }; + // Initial setup must not require an administrative grant for the agent's CLI device. + // OpenShell owns the sandbox lifecycle and verifies the same identity at both transitions. + for (const action of ["stop", "start"] as const) { + const result = await lifecycle[`${action}Sandbox`](request); + if (result.kind === "failed") { + return { + ok: false as const, + failureLayer: "OpenShell lifecycle", + detail: `OpenShell ${action} failed: ${result.error.message}`, + }; + } + } + const ready = await runtime + .loadFinalizationDeps() + .finalizationHandlerDeps.waitForStartedOpenclawGatewayProcess(sandboxName, gatewayName); + return ready === true + ? { ok: true as const } + : { + ok: false as const, + failureLayer: "native gateway startup", + detail: "OpenClaw did not become ready after the initial OpenShell restart.", + }; + }); +} + export function createInitialOpenclawInferenceRoute( deps: InitialOpenclawInferenceRouteDeps, ): InitializeOpenclawInferenceRoute { @@ -113,8 +171,18 @@ export const initializeOpenclawInferenceRoute = createInitialOpenclawInferenceRo gatewayName, ); }, - restartNativeGateway: (sandboxName, gatewayName) => - initialOpenclawInferenceRouteRuntime - .loadFinalizationDeps() - .restartNativeGatewayForInitialSetup(sandboxName, gatewayName), + restartNativeGateway: (sandboxName, gatewayName) => { + const sandbox = initialOpenclawInferenceRouteRuntime.loadRegistry().load().sandboxes[ + sandboxName + ]; + return sandbox?.workload?.kind === "external-image" + ? restartInitialExternalOpenclawSandbox( + sandboxName, + gatewayName, + sandbox.lifecycleLiveIdentityFingerprint, + ) + : initialOpenclawInferenceRouteRuntime + .loadFinalizationDeps() + .restartNativeGatewayForInitialSetup(sandboxName, gatewayName); + }, }); From 76707785670d072bf42e70b2a50338da66e2d7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20Erickson=20=F0=9F=A6=9E?= Date: Thu, 1 Oct 2026 00:30:57 -0700 Subject: [PATCH 12/12] merge: resolve main integration for dashboard URL fix Take the merged audit and runtime repair from #12507. Remove superseded dependency and restart workarounds. Preserve the dashboard feature and Podman toolchain refresh. Signed-off-by: Aaron Erickson --- .../resolve-hermes-base-image/action.yaml | 27 + .github/workflows/managed-images.yaml | 53 +- .github/workflows/pr.yaml | 22 +- .github/workflows/sandbox-images.yaml | 30 +- Dockerfile | 205 ++-- Dockerfile.base | 21 +- agents/hermes/Dockerfile | 4 +- agents/hermes/Dockerfile.base | 9 +- agents/langchain-deepagents-code/Dockerfile | 4 +- .../langchain-deepagents-code/Dockerfile.base | 9 +- .../package-lock.json | 950 +++++++++++++++--- .../package.json | 22 +- agents/openclaw/manifest.yaml | 2 +- .../openclaw-runtime/package-lock.json | 148 +-- agents/openclaw/openclaw-runtime/package.json | 5 +- agents/pi/Dockerfile | 4 +- agents/pi/Dockerfile.base | 9 +- ci/pi-agent-qualification-v1-linux-amd64.json | 8 +- ci/pi-agent-qualification-v1-linux-arm64.json | 8 +- ci/reviewed-npm-audit.json | 66 +- ci/reviewed-npm-lifecycle-allowlist.json | 18 +- .../add-channels-after-onboarding.mdx | 3 +- docs/reference/commands.mdx | 4 +- docs/reference/troubleshooting.mdx | 5 +- nemoclaw/package.json | 2 +- package-lock.json | 12 +- .../materialize-locked-npm-cache-seed.mts | 14 +- scripts/lib/openclaw-npm-remediation.mts | 131 +-- .../lib/patch-openclaw-container-restart.mts | 68 +- .../lib/patch-openclaw-npm12-pack-json.mts | 5 + ...openclaw-secondary-main-session-delete.mts | 4 +- scripts/nemoclaw-start.sh | 25 +- .../patch-openclaw-device-self-approval.mts | 149 ++- scripts/patch-openclaw-mcp-reliability.mts | 63 +- .../patch-openclaw-mcp-tools-list-timeout.mts | 4 +- scripts/validate-openclaw-tool-search.mts | 8 + .../exec-googlechat-pairing-restart.test.ts | 2 +- .../actions/sandbox/gateway-restart.test.ts | 2 +- src/lib/actions/sandbox/gateway-restart.ts | 2 +- .../sandbox/rebuild-preflight-target-phase.ts | 2 +- src/lib/agent/candidate-authority.ts | 4 +- .../applier/build/messaging-build-applier.mts | 29 + .../messaging/channels/discord/manifest.ts | 3 + .../messaging/channels/googlechat/manifest.ts | 3 + src/lib/messaging/channels/metadata.test.ts | 10 +- src/lib/messaging/channels/slack/manifest.ts | 3 + src/lib/messaging/channels/teams/manifest.ts | 3 + .../messaging/channels/whatsapp/manifest.ts | 3 + ...ckerfile-remote-dashboard-bind-contract.ts | 5 +- src/lib/onboard/machine/final-flow-phases.ts | 2 +- .../onboard/machine/handlers/agent-setup.ts | 8 + ...d-startup-profile-release-contract.test.ts | 8 +- src/lib/onboard/managed-startup/profile.ts | 8 +- .../openclaw/initial-inference-route.ts | 76 +- .../sandbox-workload-authority.test.ts | 36 + src/lib/onboard/workload/authority.ts | 10 + .../sandbox-base-image/security-inventory.ts | 2 +- .../openclaw-container-restart-patch.test.ts | 61 ++ .../openclaw-dependency-review.test.ts | 8 +- ...device-self-approval-patch-upgrade.test.ts | 13 +- ...penclaw-device-self-approval-patch.test.ts | 16 +- .../openclaw/openclaw-integrity-pin-suite.ts | 56 +- .../openclaw-lifecycle-policy.test.ts | 2 +- .../openclaw/openclaw-locked-install.test.ts | 20 +- ...aw-managed-messaging-offline-build.test.ts | 11 +- ...nclaw-mcp-tools-list-timeout-patch.test.ts | 2 +- .../openclaw-optional-plugin-build.test.ts | 16 +- ...openclaw-real-patched-dist-harness.test.ts | 9 +- ...claw-tool-search-runtime-validator.test.ts | 27 +- .../nemoclaw-start-restored-token.test.ts | 1 + .../openclaw/runtime/nemoclaw-start.test.ts | 12 +- .../releases/reviewed-npm-audit-fixtures.ts | 15 +- .../reviewed-npm-audit-workflow.test.ts | 11 + .../issue-4434-error-fields.test.ts | 12 +- test/e2e/live/llama-cpp-generic-gpu.test.ts | 3 +- .../managed-image-activation-e2e-helpers.ts | 13 +- ...naged-image-activation-diagnostics.test.ts | 9 +- test/helpers/base-apt-security-functions.ts | 2 +- test/helpers/fetch-guard-patch-harness.ts | 2 +- test/helpers/onboard-script-mocks.cjs | 2 +- ...penclaw-real-device-self-approval-proof.ts | 104 +- .../openclaw-real-mcp-start-retry-proof.ts | 2 +- ...managed-image-publication-workflow.test.ts | 31 +- .../materialize-locked-npm-cache-seed.test.ts | 26 + test/install/native-security-packages.test.ts | 2 +- .../mcp-tool-discovery-image-contract.test.ts | 2 +- .../effective-policy-contracts.test.ts | 4 +- .../msteams-message-hints-preload.test.ts | 10 +- .../images/base-image-resolver-helper.test.ts | 110 +- ...messaging-build-applier-googlechat.test.ts | 17 +- .../sandbox-base-security-packages.test.ts | 2 +- .../fetch-guard-patch-regression.test.ts | 24 +- ...anaged-startup-direct-image-runtime.bundle | 2 +- 93 files changed, 2067 insertions(+), 934 deletions(-) diff --git a/.github/actions/resolve-hermes-base-image/action.yaml b/.github/actions/resolve-hermes-base-image/action.yaml index 935107a9302..fed924b3f43 100644 --- a/.github/actions/resolve-hermes-base-image/action.yaml +++ b/.github/actions/resolve-hermes-base-image/action.yaml @@ -33,6 +33,25 @@ runs: fi expected_semver="${expected_semvers[0]}" + expected_libssl="$(sed -nE 's/^[[:space:]]+"libssl3t64=([0-9A-Za-z.+:~_-]+)".*/\1/p' agents/hermes/Dockerfile.base)" + [[ "$expected_libssl" =~ ^[0-9A-Za-z.+:~_-]+$ ]] || { + echo "::error::Expected exactly one OpenSSL runtime inventory pin in agents/hermes/Dockerfile.base" >&2 + exit 1 + } + + openssl_identity_ok() { + docker run --rm \ + --network none \ + --cap-drop ALL \ + --security-opt no-new-privileges \ + --read-only \ + --user sandbox \ + --entrypoint /bin/sh "$1" -ec ' + test "$(dpkg-query -W -f="\${Version}" libssl3t64)" = "$1" + grep -Fx "libssl3t64=$1" /usr/local/share/nemoclaw/security-packages.txt >/dev/null + ' sh "$expected_libssl" >/dev/null 2>&1 + } + runtime_identity_ok() { local ref="$1" docker run --rm \ @@ -78,6 +97,10 @@ runs: echo "::warning::Hermes sandbox base image ${ref} contains retired sandbox state; trying another candidate" return 1 fi + if ! openssl_identity_ok "$digest_ref"; then + echo "::warning::Hermes sandbox base image ${ref} does not match OpenSSL ${expected_libssl}; trying another candidate" + return 1 + fi if ! runtime_identity_ok "$digest_ref"; then echo "::warning::Hermes sandbox base image ${ref} does not match Hermes ${expected_semver} with the required MCP Streamable HTTP and ACP 0.9.0 runtimes" return 1 @@ -124,6 +147,10 @@ runs: echo "::error::Local Hermes sandbox base image contains retired sandbox state" exit 1 fi + if ! openssl_identity_ok nemoclaw-hermes-base-local; then + echo "::error::Local Hermes sandbox base image does not match OpenSSL ${expected_libssl}" + exit 1 + fi if ! runtime_identity_ok nemoclaw-hermes-base-local; then echo "::error::Local Hermes sandbox base image does not match Hermes ${expected_semver} with the required MCP Streamable HTTP and ACP 0.9.0 runtimes" exit 1 diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 2ace070d884..53801103b0f 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -98,39 +98,8 @@ jobs: path: candidate persist-credentials: false - - name: Checkout npm audit code from the base commit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: b0af4ef64b8257372f2ad390ce6c843c96bbfebb - path: .trusted-reviewed-npm-audit - persist-credentials: false - sparse-checkout: | - .github/actions/setup-reviewed-npm - ci/reviewed-npm-audit.json - .github/actions/ci-reviewed-npm-audit - ci/npm-audit-exceptions.json - scripts/audit-reviewed-npm-graph.mts - scripts/lib/npm-audit-receipt.mts - scripts/lib/repository-input-path.mts - scripts/lib/openclaw-npm-remediation.mts - scripts/lib/reviewed-npm-archive.mts - scripts/lib/reviewed-npm-audit.mts - sparse-checkout-cone-mode: false - - - name: Verify exact audit source and target - shell: bash - env: - CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }} - REVIEWED_AUDIT_SHA: b0af4ef64b8257372f2ad390ce6c843c96bbfebb - run: | - set -euo pipefail - [[ "$CANDIDATE_SHA" =~ ^[a-f0-9]{40}$ ]] - [[ "$REVIEWED_AUDIT_SHA" =~ ^[a-f0-9]{40}$ ]] - test "$(git -C .trusted-reviewed-npm-audit rev-parse --verify HEAD)" = "$REVIEWED_AUDIT_SHA" - test "$(git -C candidate rev-parse --verify HEAD)" = "$CANDIDATE_SHA" - - name: Audit exact PR production npm graphs - uses: ./.trusted-reviewed-npm-audit/.github/actions/ci-reviewed-npm-audit + uses: ./candidate/.github/actions/ci-reviewed-npm-audit with: target-root: ${{ github.workspace }}/candidate report-dir: artifacts/reviewed-npm-audit @@ -544,7 +513,7 @@ jobs: if: matrix.agent == 'openclaw' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: b0af4ef64b8257372f2ad390ce6c843c96bbfebb + ref: ${{ github.event.pull_request.base.sha }} path: .trusted-mcporter-audit persist-credentials: false sparse-checkout: | @@ -566,13 +535,15 @@ jobs: if: matrix.agent == 'openclaw' id: mcporter-audit shell: bash + env: + REVIEWED_AUDIT_SHA: ${{ github.event.pull_request.base.sha }} run: | set -euo pipefail receipt="$RUNNER_TEMP/reviewed-npm-audit/mcporter-runtime.receipt.json" raw="$RUNNER_TEMP/reviewed-npm-audit/mcporter-runtime.raw.json" policy="$RUNNER_TEMP/reviewed-npm-audit/mcporter-runtime.policy.json" trusted_root="$GITHUB_WORKSPACE/.trusted-mcporter-audit" - test "$(git -C "$trusted_root" rev-parse --verify HEAD)" = 'b0af4ef64b8257372f2ad390ce6c843c96bbfebb' + test "$(git -C "$trusted_root" rev-parse --verify HEAD)" = "$REVIEWED_AUDIT_SHA" node --experimental-strip-types --no-warnings \ "$trusted_root/scripts/lib/npm-audit-receipt.mts" \ --receipt "$receipt" \ @@ -2382,14 +2353,14 @@ jobs: fs.readFileSync("/sandbox/.openclaw/openclaw.json", "utf8"), ); const packages = { - "diagnostics-otel": ["@openclaw/diagnostics-otel", "2026.9.1"], - brave: ["@openclaw/brave-plugin", "2026.9.1"], - discord: ["@openclaw/discord", "2026.9.1"], + "diagnostics-otel": ["@openclaw/diagnostics-otel", "2026.9.2"], + brave: ["@openclaw/brave-plugin", "2026.9.2"], + discord: ["@openclaw/discord", "2026.9.2"], "openclaw-weixin": ["@tencent-weixin/openclaw-weixin", "2.4.9"], - slack: ["@openclaw/slack", "2026.9.1"], - whatsapp: ["@openclaw/whatsapp", "2026.9.1"], - msteams: ["@openclaw/msteams", "2026.9.1"], - googlechat: ["@openclaw/googlechat", "2026.9.1"], + slack: ["@openclaw/slack", "2026.9.2"], + whatsapp: ["@openclaw/whatsapp", "2026.9.2"], + msteams: ["@openclaw/msteams", "2026.9.2"], + googlechat: ["@openclaw/googlechat", "2026.9.2"], }; const projectsRoot = "/sandbox/.openclaw/npm/projects"; const projectRoots = fs diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 77c05b3fbd2..d5633d6e2ce 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -510,29 +510,11 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - - name: Checkout npm audit code from the base commit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.event.pull_request.base.sha }} - path: .trusted-reviewed-npm-audit - persist-credentials: false - sparse-checkout: | - .github/actions/ci-reviewed-npm-audit - .github/actions/setup-reviewed-npm - ci/npm-audit-exceptions.json - ci/reviewed-npm-audit.json - scripts/audit-reviewed-npm-graph.mts - scripts/lib/openclaw-npm-remediation.mts - scripts/lib/reviewed-npm-archive.mts - scripts/lib/reviewed-npm-audit.mts - scripts/lib/npm-audit-receipt.mts - scripts/lib/repository-input-path.mts - sparse-checkout-cone-mode: false - - name: Audit reviewed production npm graphs - uses: NVIDIA/NemoClaw/.github/actions/ci-reviewed-npm-audit@b0af4ef64b8257372f2ad390ce6c843c96bbfebb + uses: ./.github/actions/ci-reviewed-npm-audit with: target-root: ${{ github.workspace }} report-dir: artifacts/reviewed-npm-audit diff --git a/.github/workflows/sandbox-images.yaml b/.github/workflows/sandbox-images.yaml index 77555347d1b..4e6f014916e 100644 --- a/.github/workflows/sandbox-images.yaml +++ b/.github/workflows/sandbox-images.yaml @@ -134,9 +134,6 @@ jobs: - name: Resolve Hermes base image uses: ./.github/actions/resolve-hermes-base-image - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - &hermes-export-swap name: Add swap for Hermes image export shell: bash @@ -155,14 +152,6 @@ jobs: df -h / /mnt docker system df - - name: Validate Hermes production build args - env: - HERMES_BASE_IMAGE: ${{ env.HERMES_BASE_IMAGE }} - run: | - set -euo pipefail - build_args=(-f agents/hermes/Dockerfile --build-arg "BASE_IMAGE=${HERMES_BASE_IMAGE}") - scripts/check-production-build-args.sh "${build_args[@]}" - - name: Record resources before Hermes image build shell: bash run: | @@ -172,16 +161,15 @@ jobs: docker system df || true - name: Build Hermes production image - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - file: agents/hermes/Dockerfile - load: true - push: false - tags: nemoclaw-hermes-production - build-args: BASE_IMAGE=${{ env.HERMES_BASE_IMAGE }} - cache-from: type=gha,scope=hermes-production-${{ runner.os }}-${{ runner.arch }} - cache-to: type=gha,mode=max,scope=hermes-production-${{ runner.os }}-${{ runner.arch }} + env: + HERMES_BASE_IMAGE: ${{ env.HERMES_BASE_IMAGE }} + shell: bash + run: | + set -euo pipefail + build_args=(-f agents/hermes/Dockerfile --build-arg "BASE_IMAGE=${HERMES_BASE_IMAGE}") + scripts/check-production-build-args.sh "${build_args[@]}" + # The resolver can load its fallback base into the Docker daemon's image store. + docker build --builder default "${build_args[@]}" -t nemoclaw-hermes-production . # The production build intentionally omits NEMOCLAW_CORPORATE_CA_B64. A # successful final stage therefore proves its registry remediations and diff --git a/Dockerfile b/Dockerfile index bb56b4ebcf3..8cdab0bccb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,9 +65,8 @@ COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery FROM scratch AS managed-startup-runtime-builder COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle /out/managed-startup-image-runtime.cjs -# Fetch immutable reviewed archives outside RUN instructions. The protected -# GPU rebuild imports these checksum-addressed source records from the -# amd64 build cache, while every package-materialization RUN remains offline. +# GPU builds import checksum-pinned archives from the amd64 cache. +# Package installs stay offline. FROM scratch AS wechat-npm-archives ADD --checksum=sha256:467e8047f7114e45944961fcd3eda9421843c9c65db61ea24176e252ab800ee4 https://registry.npmjs.org/@tencent-weixin/openclaw-weixin/-/openclaw-weixin-2.4.9.tgz /openclaw-weixin-2.4.9.tgz @@ -88,8 +87,8 @@ ADD --checksum=sha256:0ec75f1cd0bd6011b687d0aac25478f3123ffa81ec299281bcb1747dd3 FROM scratch AS openclaw-optional-plugin-archives -ADD --chmod=0444 --checksum=sha256:df2c7f5f880da6ab13a43d0cf2efdd8f196802db9ebbffb9492cf81d32b15a62 https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.1.tgz /diagnostics-otel-2026.9.1.tgz -ADD --chmod=0444 --checksum=sha256:f679af12fa00947d994e6a8454aded205b5bf2454dce0674bff88f741dfb9af8 https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.1.tgz /brave-plugin-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:fe5baa1d9bbe53b3cf616a13ff7dcb0f21d6ce7a7d6d9856b9909e81c53a884c https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.2.tgz /diagnostics-otel-2026.9.2.tgz +ADD --chmod=0444 --checksum=sha256:40c0cf23e8373f2285034b8f0a575cc51ec1ed53d081b0e1592d219dd411e54d https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.2.tgz /brave-plugin-2026.9.2.tgz # hadolint ignore=DL3006 FROM codex-acp-${TARGETARCH}-archive AS codex-acp-platform-archive @@ -147,13 +146,14 @@ RUN --network=none install -d -o root -g root -m 0755 /out/wechat-npm-cache \ FROM scratch AS openclaw-managed-messaging-npm-common-archives-1 +ADD --chmod=0444 --checksum=sha256:fa254fb316dd23ddcb2beebd533b23788aec4cf6a3dba58af34150170435c472 https://registry.npmjs.org/send/-/send-1.2.1.tgz /4abaa9d09b604c814ea70d7c3554a3340a6e76f85c15b4173d5f8364c1d25462-send-1.2.1.tgz ADD --chmod=0444 --checksum=sha256:9d6a926982795204bed8fb5d02537a08b74d0b8f85ec715808fe713e48d14a79 https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz /abort-controller-2.2.0.tgz ADD --chmod=0444 --checksum=sha256:d2e249d5d010eb18e57c12c610d63e3ca3fa9dd0a5378009c1f465e21f50ab2f https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz /abort-controller-3.0.0.tgz ADD --chmod=0444 --checksum=sha256:173d915f7d88df8cd4db2129a030c3b1c9cafd3b7aee5b89465bf3ad18372542 https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz /accepts-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:0ad4c0f28f9bc5bb6f3eb879b4fd38265def6d7e1e5d61f96f78ee6a8a7be94a https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz /acorn-8.18.0.tgz ADD --chmod=0444 --checksum=sha256:bc6da06f2a2e6bc80fa5878bd7227bd0318812976d45f47f17e1aafcec2be831 https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz /agent-base-6.0.2.tgz ADD --chmod=0444 --checksum=sha256:7dd4a61668a9a4e8d4e903f1a254f94d53dafd3f316f2b9b597c5ad8c79cb57e https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz /agent-base-7.1.4.tgz -ADD --chmod=0444 --checksum=sha256:ce2f6c3e6b9f465775bb03625ca4c9dc51c45fce6a81723475681b9e3034c4bd https://registry.npmjs.org/@openclaw/ai/-/ai-2026.9.1.tgz /ai-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:6f6253fb563e8f97748a0523600c2dd363863e5b7106c461049f1c08e31b9935 https://registry.npmjs.org/@openclaw/ai/-/ai-2026.9.2.tgz /ai-2026.9.2.tgz ADD --chmod=0444 --checksum=sha256:b2f0b3a893bbb8cc5efb6814f08b1499e19e31d5dd73683f5893382f48f6e7b3 https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz /ajv-8.20.0.tgz ADD --chmod=0444 --checksum=sha256:f4d6980fd367381fd29199066911e863db8d97496613b6c2c5b91563a150acc5 https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz /ajv-formats-3.0.1.tgz ADD --chmod=0444 --checksum=sha256:0e0eadcdaada805db5d85b53ad5cdca0760b996ee199ec9658e7b34aa6c8e0d9 https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz /ansi-regex-5.0.1.tgz @@ -163,16 +163,18 @@ ADD --chmod=0444 --checksum=sha256:0041878b8209f2fa4bcc5e0666355ebc96ff97f360c30 ADD --chmod=0444 --checksum=sha256:8c254f30f70792645042e4d71f590ec49f8e386a475772f7430c73b964b57dcf https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz /asynckit-0.4.0.tgz ADD --chmod=0444 --checksum=sha256:a511049fdaec40a320368b3ee965079b3e14481f82d052584f746bbdc3f01ede https://registry.npmjs.org/axios/-/axios-1.19.0.tgz /axios-1.19.0.tgz ADD --chmod=0444 --checksum=sha256:5aa2dc9a5d6ced926e4b6ca8ef8e0253b118867e240db8650c49379e972c12ac https://registry.npmjs.org/axios/-/axios-1.20.0.tgz /axios-1.20.0.tgz +ADD --chmod=0444 --checksum=sha256:71e1a0370bc9e996ad13cee06506bad037818f5618e5377bd9912df8d97ab26e https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz /bafa97c7ca9873a4fb4fb5794b712a7a306a442e8dfbbc00d62265e576f2bcbb-send-1.2.1.tgz ADD --chmod=0444 --checksum=sha256:9025508d9125eee531bbc49ce3ae560183975ad595f058c378bd56af4152fb16 https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz /balanced-match-4.0.4.tgz ADD --chmod=0444 --checksum=sha256:0130711d2e0d3f87436c7825db1f35bd6134fba2eda64b0df43d781f9b6a596a https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz /base64-1.0.1.tgz ADD --chmod=0444 --checksum=sha256:d67e6ee6e1445512478cdfc34c12144f579bdd9f06529eef3ef8d88f84031a6a https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz /base64-1.1.2.tgz ADD --chmod=0444 --checksum=sha256:b1b7a945b52685269083425216d6597e33d97bf21699d656e92fdb3eb5210a85 https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz /base64-js-1.5.1.tgz ADD --chmod=0444 --checksum=sha256:f5a943ea290e66f64cb9adaaed2ff1b7c4ee02a4cca9d709d9c9c6c222512e82 https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz /bignumber.js-9.3.1.tgz ADD --chmod=0444 --checksum=sha256:50c550f01680444f2d5985b78bab8976ea17c3f43963a8fdc39bdbb4489fb5fb https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz /bn.js-4.12.5.tgz +ADD --chmod=0444 --checksum=sha256:640c729c03c2527aca389e1e134d342a5ccee6b394b700e297141f580fe89f8a https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz /body-parser-1.19.6.tgz ADD --chmod=0444 --checksum=sha256:031d7f6c5142e31be91d36a43f541f02a505943e3b871aa44ef5fb6939be258e https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz /body-parser-2.3.0.tgz ADD --chmod=0444 --checksum=sha256:9c8433ec18090ee5b75246976b368169aa7af7685626fdb41deaffdbe683fb92 https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz /boolbase-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:a62dcc8a1260148fde067d36acc601a20532a34626e3b862b5f9eea140d97060 https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz /bottleneck-2.19.5.tgz -ADD --chmod=0444 --checksum=sha256:5d06001fddd25cbee90c96db4dc5b7b57711b984c3141e28d10f143deb52dbaf https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz /brace-expansion-5.0.9.tgz +ADD --chmod=0444 --checksum=sha256:ef8448ec78f20b692f04fa6d01f39b5ab34c66404bea3429f5a39c6c9e0be8b4 https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz /brace-expansion-5.0.12.tgz ADD --chmod=0444 --checksum=sha256:8f455159e342103e7854ed6a4cc73edbab144d857917c88edefea862f09fe75a https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz /buffer-equal-constant-time-1.0.1.tgz ADD --chmod=0444 --checksum=sha256:9c2b03d59eca8f463a1927e07273ddaa87785fe3f61626c42b005540e962e343 https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz /buffer-from-1.1.2.tgz ADD --chmod=0444 --checksum=sha256:35e49d4240c91cbe4ca29926139feea848302e9eea317f31d9e81b972ce90911 https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz /bundle-name-4.1.0.tgz @@ -190,8 +192,10 @@ ADD --chmod=0444 --checksum=sha256:defad1e25e8a349ea9cdd1066abf5e1e762f7f909de72 ADD --chmod=0444 --checksum=sha256:20bafed1221bcba23a2450a841998edaef9a56bc2101d6e38c2117dd58a13a01 https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz /codegen-2.0.5.tgz ADD --chmod=0444 --checksum=sha256:920fa43538c019a085dbbf04cb6f72cc337624e5f5217519f0e7b2ef784e7ce1 https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz /color-convert-2.0.1.tgz ADD --chmod=0444 --checksum=sha256:507b7c4461e8eb941355af9a59e9a7e02cd0e7c6176b48d1809766344f3f1708 https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz /color-name-1.1.4.tgz +ADD --chmod=0444 --checksum=sha256:6c2df3ac33d4b8647191ad8942a579e6004be00c25846677eab85f04702d85b1 https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz /colour-1.1.0.tgz ADD --chmod=0444 --checksum=sha256:b6be5aabe53e90635beb77cd0e0ba7ae6a25c8cf903b15fcc342353e732e1512 https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz /combined-stream-1.0.8.tgz ADD --chmod=0444 --checksum=sha256:632c1e039b31e98fa79c4fae5b10a5ffbbf9df0f21c9ffb3d74e95734b30696f https://registry.npmjs.org/commander/-/commander-15.0.0.tgz /commander-15.0.0.tgz +ADD --chmod=0444 --checksum=sha256:87b4950375c16d097bc4609fde1d3b5a891227f06675be8221251e63b6ce2b08 https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz /connect-3.4.38.tgz ADD --chmod=0444 --checksum=sha256:2f8b1925a8b123a86606c11322fc10aafc1dd85f2860fffe32893e20aef4093c https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz /content-disposition-1.1.0.tgz ADD --chmod=0444 --checksum=sha256:ac31d098405f0242dd712218f38a14a6202bd4eb01067db05db765d9a9bd12c8 https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz /content-type-1.0.5.tgz ADD --chmod=0444 --checksum=sha256:47a08ee5ddf87a96dd263aa942c5e04b2c5d26251e04affa8ace6804b450d758 https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz /content-type-2.1.0.tgz @@ -222,15 +226,19 @@ ADD --chmod=0444 --checksum=sha256:3c94fbe0d90b610de6dc068180c780cbb7ef0ade6d2f3 ADD --chmod=0444 --checksum=sha256:bbe9fe67a229c64ff9b8c77ace12278e2d44048a2a5af96e5fc95abbc94c49b5 https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz /define-lazy-prop-3.0.0.tgz ADD --chmod=0444 --checksum=sha256:ac38fce4217dfb1d772427c7d8d0d073e35ecd832915e97a61d9ab5c504129d3 https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz /delayed-stream-1.0.0.tgz ADD --chmod=0444 --checksum=sha256:28a58a2056093441f1d00d677d95918d2e4b3e98bac86237159101cae315d4a7 https://registry.npmjs.org/depd/-/depd-2.0.0.tgz /depd-2.0.0.tgz +ADD --chmod=0444 --checksum=sha256:270dec0fc06cff86481da8af2dd8f18dee6b602790b14ef0e1c2c18d7da39427 https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz /detect-libc-2.1.2.tgz ADD --chmod=0444 --checksum=sha256:b898bf23c95594607576e25ddd4013f1d51ed0e862aaf0732815830c87b3b58f https://registry.npmjs.org/diff/-/diff-9.0.0.tgz /diff-9.0.0.tgz ADD --chmod=0444 --checksum=sha256:07149886ab98299c227b8de61912770b24b8a17b250996a4b5727c9f8bff4c00 https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz /dijkstrajs-1.0.3.tgz -ADD --chmod=0444 --checksum=sha256:4437fb157829af52cdfe2acc19cc03378db5052f8e521862ee702293e01c28ac https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.1.tgz /discord-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:358b88763e7697e29eb1807240ab5cdc04e38662462fb93c14e2b563ca7521d6 https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.2.tgz /discord-2026.9.2.tgz ADD --chmod=0444 --checksum=sha256:e34511f144fc6b34ce536bd60fb1ed27dd965f07a7c317407e79dd6be9e8f399 https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz /dom-serializer-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:12272f96b8a76363d78b67d7695b73410f339171f56a6cc5793cb4bfc6b15aa0 https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz /dom-serializer-3.1.1.tgz ADD --chmod=0444 --checksum=sha256:c67164b4a994eaeaecbd968c2e5e5415407ae6fb4486bbb470594154e25feb45 https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz /domelementtype-2.3.0.tgz ADD --chmod=0444 --checksum=sha256:078a496be3f33f3268f6749b3a5d45629f4b98beca1e53e3ef6d1ba2040811d5 https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz /domelementtype-3.0.0.tgz ADD --chmod=0444 --checksum=sha256:f3952abb7e2635d8e942822d68cfb1fdaba61148d17a1a5692bdd163d6ca4784 https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz /domhandler-5.0.3.tgz ADD --chmod=0444 --checksum=sha256:c42bd0d96c5a10ebcfd938fa1fd97db12b9f592a485fb75d9aba5fa66e66d93b https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz /domhandler-6.0.1.tgz + +FROM scratch AS openclaw-managed-messaging-npm-common-archives-2 + ADD --chmod=0444 --checksum=sha256:272918a13e7e093ddc983666954164e098fb2443f06f476f34bf47005e12c140 https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz /domutils-3.2.2.tgz ADD --chmod=0444 --checksum=sha256:64922a8f80c4c31a0d146e563ba054de86453c0d78e50fba66e4e8c8462a95ac https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz /domutils-4.0.2.tgz ADD --chmod=0444 --checksum=sha256:8648852be8209110b34dca75dcc3ed12ce7fae9fcc8edd1ef9e180e708af1398 https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz /dotenv-17.4.2.tgz @@ -238,9 +246,6 @@ ADD --chmod=0444 --checksum=sha256:ed1342228c82c10df9921c59d684df516a0cd6ed25b61 ADD --chmod=0444 --checksum=sha256:487cb94dff2414772c3bb648a5e4e41c03cbbcc64263d08a56e36d735fc848fe https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz /ecdsa-sig-formatter-1.0.11.tgz ADD --chmod=0444 --checksum=sha256:5148e8eb7e222b2a09127618bbdb5033daf6262cfc735d3101ea98620128b99c https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz /ee-first-1.1.1.tgz ADD --chmod=0444 --checksum=sha256:b5ccd9fbfb08098eefbeb6b6b4b40db6db3acf9243e327e039925aa8661cb107 https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz /emoji-regex-8.0.0.tgz - -FROM scratch AS openclaw-managed-messaging-npm-common-archives-2 - ADD --chmod=0444 --checksum=sha256:9b2e418b8851b8f9e7a13d5ada3bd4d3c5ef042885867261f556347d4bbefb29 https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz /encodeurl-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:6b0c60f7351a0b65bb1fc8dbb9299f09e7ecf6d89103d0651bf369e6f463a632 https://registry.npmjs.org/entities/-/entities-4.5.0.tgz /entities-4.5.0.tgz ADD --chmod=0444 --checksum=sha256:554b7e2a79fa9eda0439714011fa42d77c6829842faa91e022070015bfd483a0 https://registry.npmjs.org/entities/-/entities-7.0.1.tgz /entities-7.0.1.tgz @@ -257,14 +262,16 @@ ADD --chmod=0444 --checksum=sha256:5536b98cb7062e771c1dadd1828e352ebe40034f14808 ADD --chmod=0444 --checksum=sha256:7c62d4bb196e59b39c5af79e550d6fe4261649a74d9f5e605b071e1da6081c92 https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz /eventsource-3.0.7.tgz ADD --chmod=0444 --checksum=sha256:44a0a0ca6cecea76ac47de3e73414ceb32dbbfb8f3b6408906d81170c68e36ed https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz /eventsource-parser-3.1.1.tgz ADD --chmod=0444 --checksum=sha256:b2f53cb1b3da8f1e3f27007641cdd419df34215eb704dfccfe0899603da64cc8 https://registry.npmjs.org/execa/-/execa-10.0.1.tgz /execa-10.0.1.tgz +ADD --chmod=0444 --checksum=sha256:c88bbca1dc366bb28fc6da3af746c14529c303f7eb13330705f92cb190c0e8bc https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz /express-5.0.6.tgz ADD --chmod=0444 --checksum=sha256:1773a16c02b4422653479b9c4d211268f7022bdac0d817b5698535bb485dd005 https://registry.npmjs.org/express/-/express-5.2.1.tgz /express-5.2.1.tgz ADD --chmod=0444 --checksum=sha256:1e3ed770c901156477986dfc189fb0b5bd8d8a8e6481393954ed4f6265d139b0 https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz /express-rate-limit-8.7.0.tgz +ADD --chmod=0444 --checksum=sha256:5da047f251ae4b2cedbccc18f059fb65f59b24a997df631d027d3075eb4070e0 https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz /express-serve-static-core-5.1.3.tgz ADD --chmod=0444 --checksum=sha256:1d91d0b0faa50cba223fa937c7b5a4a662968b1d78b3e59dca5c917dd5cf72b2 https://registry.npmjs.org/extend/-/extend-3.0.2.tgz /extend-3.0.2.tgz ADD --chmod=0444 --checksum=sha256:b019a0980f27638dc3f85836b0e478f188e00d7a6e5852c0819fa86f56e47b8f https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz /fast-deep-equal-3.1.3.tgz ADD --chmod=0444 --checksum=sha256:4f897ea2594dc9cfb1250e7d4d0f65b4f105952a802fff2d06990bf1dc2c84f6 https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz /fast-sha256-1.3.0.tgz ADD --chmod=0444 --checksum=sha256:b5dace35423470b453ed5dba20419052124b8a65cb233833f17e5483523b5eb1 https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz /fast-string-truncated-width-3.0.3.tgz ADD --chmod=0444 --checksum=sha256:72daf113df209b0e55a227ff5f62683ca99da07b59c5da36a2da62920cb3752a https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz /fast-string-width-3.0.2.tgz -ADD --chmod=0444 --checksum=sha256:3fa380284be4ecbf471c1dbb8c5da6f517c95f54279f88c2037985d03fdc6d92 https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz /fast-uri-3.1.7.tgz +ADD --chmod=0444 --checksum=sha256:86be033b406a7737c0521edc8fe3e15c7ac0cb6b5e509478cc9539a2efaa086c https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz /fast-uri-3.1.8.tgz ADD --chmod=0444 --checksum=sha256:678d765d4c73db3173506593cae33d6a8437ed32a95ac02dc878a5f0b03bca5c https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.4.tgz /fast-uri-4.1.4.tgz ADD --chmod=0444 --checksum=sha256:f6b4a10f346b4405f01a1734be4c29b1aba315977f409ed11ea12ff2d3fae051 https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz /fast-wrap-ansi-0.2.2.tgz ADD --chmod=0444 --checksum=sha256:54481d9c62debce1c38b0239f2358eeb3b73f7bb1ba3105bd6123fd81b8b7268 https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz /fetch-1.1.1.tgz @@ -280,7 +287,7 @@ ADD --chmod=0444 --checksum=sha256:1ff73b4138ea33f0fd0f41b67910409a2c8eb1b71a4cf ADD --chmod=0444 --checksum=sha256:9b5a5de95fb85fcb58db5e4fcd94ce8ab9f0476d02202e20a5225cec60431c99 https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz /forwarded-0.2.0.tgz ADD --chmod=0444 --checksum=sha256:ad08397ab05f62b2b507682e23aad699cf8cc33922e0030be0cb640a23277ad7 https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz /fresh-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:7ac286e3cccc1ea8980e79e2039def6bb97d3182e17951cd9094f0400ed98236 https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz /fs-minipass-4.0.1.tgz -ADD --chmod=0444 --checksum=sha256:bf8e6564a22636bae6a96efc6935482902c12ea8fea94799b00794406be52b54 https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.7.0.tgz /fs-safe-0.7.0.tgz +ADD --chmod=0444 --checksum=sha256:effddeca2b5c8edddd0ac467169a4b7dc5b42f185556405b6c42b5c253f5f817 https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.8.1.tgz /fs-safe-0.8.1.tgz ADD --chmod=0444 --checksum=sha256:704402651b02a1454f17d445fc7dd716efc282d059407126d58ef30a47e807aa https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz /function-bind-1.1.2.tgz ADD --chmod=0444 --checksum=sha256:439c4c3b435011c92f9c725bd461e2253a4e0c6d780f1b7057a45f591c69999c https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz /gaxios-7.3.1.tgz ADD --chmod=0444 --checksum=sha256:f9c3f2c868755c074152ecd291733c56c536678b0284c34c3613365bc730db94 https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz /gcp-metadata-8.1.2.tgz @@ -292,19 +299,20 @@ ADD --chmod=0444 --checksum=sha256:eb2cc52afb1f1fd82c5fc2a58c2380f0f16fdcdb56315 ADD --chmod=0444 --checksum=sha256:8e676f6d730ce38f01d0772936df1b33750c38e5dec6ad18c522a6a1622124c6 https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz /get-stream-9.0.1.tgz ADD --chmod=0444 --checksum=sha256:499530b85428ea27785a8ea1772458d6b821d2c917cbc1ae8f8843dca9b5327a https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz /google-auth-library-10.9.1.tgz ADD --chmod=0444 --checksum=sha256:3a921c0d4e333f94be726fc2c0ce10025f8d87f8fae5affa01a52b2da7970bbd https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz /google-logging-utils-1.1.3.tgz -ADD --chmod=0444 --checksum=sha256:e546bf34ceb7c7e68a72fe2653e7a8a1a6580a0d94f9c9586b8e67e4b54ac06b https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.1.tgz /googlechat-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:20f36f0b22fefc634e833b54abb308c4aa35c9785dde2497886cf1149b55da86 https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.2.tgz /googlechat-2026.9.2.tgz ADD --chmod=0444 --checksum=sha256:d536d0de4dd285dc1468fbb7f39334a47ee0eec9c27f9b626a6e71466c9fda82 https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz /gopd-1.2.0.tgz ADD --chmod=0444 --checksum=sha256:458f09c6841494e240e64c3b0d2fab86aa84387d9a4d1abb44909a39c1e857cb https://registry.npmjs.org/grammy/-/grammy-1.46.0.tgz /grammy-1.46.0.tgz ADD --chmod=0444 --checksum=sha256:4460c7532f28b8df2ddc9a1ec17816d43c24d4b9591dc6c5936b82f7f86ae7c5 https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz /has-symbols-1.1.0.tgz ADD --chmod=0444 --checksum=sha256:dc1c74e3f1179a6271f84747d72c89f258aa46ad3e6464fae0e41737a7f0ef7b https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz /has-tostringtag-1.0.2.tgz ADD --chmod=0444 --checksum=sha256:e9d2b03f95573600e1c13124ce618e3142ed2c538d164595bedcd4408b8a4e4c https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz /hasown-2.0.4.tgz ADD --chmod=0444 --checksum=sha256:accbfaaab745088609b4eea2bdca2ad62f1f1dd27304e0f8df65cfe0fe042143 https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz /highlight.js-11.12.0.tgz -ADD --chmod=0444 --checksum=sha256:f65df37793984664c02158c11575ebaf922bc50ef2de36dfa96f311519c5e95d https://registry.npmjs.org/hono/-/hono-4.13.7.tgz /hono-4.13.7.tgz +ADD --chmod=0444 --checksum=sha256:91c9517129a2da4fb0da1cd979ffa74cf918f68dc4f56b108d1f6a856793c8c9 https://registry.npmjs.org/hono/-/hono-4.13.12.tgz /hono-4.13.12.tgz ADD --chmod=0444 --checksum=sha256:4ebca2d4a11bf7fcf6898fb17fb3ba6d7ac9bbda226a9064bc1a4488bbe8a0be https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz /hosted-git-info-10.1.1.tgz ADD --chmod=0444 --checksum=sha256:19c5627ca8032d56a0ddbf2c80132ee8f5ab257161e3767b608cb5e3b96dd109 https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz /html-escaper-3.0.3.tgz ADD --chmod=0444 --checksum=sha256:0651eb776dbf530c8c77fb4ca6ad39fc14863a44eac2486981e78d004fad877d https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz /htmlparser2-10.1.0.tgz ADD --chmod=0444 --checksum=sha256:24d56ba3da8f09b34544eccbe34634b38683457c665569cb9c70b94a8eb7706e https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz /http_ece-1.2.0.tgz ADD --chmod=0444 --checksum=sha256:ad62bbb11baf079699a3f269ed089efdb589be16083ceed94a1117801e1a6c61 https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz /http-errors-2.0.1.tgz +ADD --chmod=0444 --checksum=sha256:b50ad978126972cee037841ca7f5147af3bb6e74105f6a14d8c7e0af34dbd740 https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz /http-errors-2.0.5.tgz ADD --chmod=0444 --checksum=sha256:785f73faa92bfba8d61da20bf59325ab2b3dca1bbc0bbac523406f404d8a6f02 https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz /http-proxy-agent-7.0.2.tgz ADD --chmod=0444 --checksum=sha256:6da16fb44331f2e5d30bd21bf880aa934c1ad4fe7da7187910ef2b2509712019 https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz /https-proxy-agent-5.0.1.tgz ADD --chmod=0444 --checksum=sha256:960f89e8e5240882f64249d04a538421dd39d62ffacc138544647cc3251bc0e0 https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz /https-proxy-agent-7.0.6.tgz @@ -317,10 +325,13 @@ ADD --chmod=0444 --checksum=sha256:98c792f39650b00818c05dcc407902034dc4092f36859 ADD --chmod=0444 --checksum=sha256:41f6a60b13cf29eebdd06723223dc68ff1d47721d56e4fef93d2d450167d9dc0 https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz /inflate-0.4.1.tgz ADD --chmod=0444 --checksum=sha256:d94dbc6c1bb3c5ac0fb12a73ade187108fc60de273a1b754f55044eb5e24afaf https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz /inherits-2.0.4.tgz ADD --chmod=0444 --checksum=sha256:35e23227dfeca9179f03f899a9e3a21faf542a8079821bce95d5620642d75873 https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz /ip-address-10.5.0.tgz -ADD --chmod=0444 --checksum=sha256:25a406ee4388fa3d47380ad57b816087fa82a681cc710cccbfe9162cffa8a57a https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz /ip-address-10.7.0.tgz +ADD --chmod=0444 --checksum=sha256:4301746e43e8a85a6a41e268f02178b27e6ba58e78e6913ab105d3871618083b https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz /ip-address-10.7.2.tgz ADD --chmod=0444 --checksum=sha256:7441d9623f67fe4160eccfd82ae9a404dcd55e1e4f1b68e06e2374dade4e8fee https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz /ipaddr.js-1.9.1.tgz ADD --chmod=0444 --checksum=sha256:1a230b0b25c81eff06bdee3856a742fd17260169b0bf958de9368c4b3ce2ddee https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz /is-docker-3.0.0.tgz ADD --chmod=0444 --checksum=sha256:6f415dae5dc6070f1b42daee6165eab941a97101982305facc8bafdaf300bc4a https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz /is-fullwidth-code-point-3.0.0.tgz + +FROM scratch AS openclaw-managed-messaging-npm-common-archives-3 + ADD --chmod=0444 --checksum=sha256:dbde95b8434fc4757624974d4139c4f32391d08c4153565ae91a5f3fd772e07b https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz /is-inside-container-1.0.0.tgz ADD --chmod=0444 --checksum=sha256:c71d874f7ab7cd560329b080ce790f9768dea503337fe2e2719a18e28be621f7 https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz /is-plain-obj-4.1.0.tgz ADD --chmod=0444 --checksum=sha256:853891173876fa03b8762cf63e7f0c0d60e524947f4e4d5852d94c22acb445a7 https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz /is-promise-4.0.0.tgz @@ -331,10 +342,7 @@ ADD --chmod=0444 --checksum=sha256:e23c76f14f5222e07e39d89858b61e8e33f96956de9e0 ADD --chmod=0444 --checksum=sha256:47cfe872e088e28c53b736fef305324b57cc1cfc9f72a9b0f769f92731cb8359 https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz /isexe-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:8dd0d365fb49c0e7cc42d6a00df6fb2da9056fc24492094346fc34ecdbcf28ca https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz /jiti-2.7.0.tgz ADD --chmod=0444 --checksum=sha256:15d92c0711c570e8a900770ca4545fbf872fed252ce153c263e0e030f21ddaa0 https://registry.npmjs.org/jose/-/jose-4.15.9.tgz /jose-4.15.9.tgz - -FROM scratch AS openclaw-managed-messaging-npm-common-archives-3 - -ADD --chmod=0444 --checksum=sha256:81685e7abf868ab5aa0ad917fefda5ab45c78ad0ba9272b977702686ffb646df https://registry.npmjs.org/jose/-/jose-6.2.11.tgz /jose-6.2.11.tgz +ADD --chmod=0444 --checksum=sha256:32d9da2aaa0e110cacef2ae21e8dae866201f1428424f839001085fe7351ebfe https://registry.npmjs.org/jose/-/jose-6.2.12.tgz /jose-6.2.12.tgz ADD --chmod=0444 --checksum=sha256:4c4f502953cfb36cfe1c6c4989676bf9b76899a253237fc0220814b88ff903b6 https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz /json-bigint-1.0.0.tgz ADD --chmod=0444 --checksum=sha256:f6f34e4e453aca8753e9f644ad4fa297ae7011030bfff9e909bf34e31c4cc364 https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz /json-schema-to-ts-3.1.1.tgz ADD --chmod=0444 --checksum=sha256:023222622df29fc274bde5d3590e47aa1d4a8e3c1d6e2aba029948ed79799b21 https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz /json-schema-traverse-1.0.0.tgz @@ -387,7 +395,7 @@ ADD --chmod=0444 --checksum=sha256:5546b0cf78281cac72871dcf90bfe13a9a88eb21afa87 ADD --chmod=0444 --checksum=sha256:11da04f8879df73e8af2cab3a92403e58207980505c876384bdd024b8e688bc5 https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.13.0.tgz /msal-common-16.13.0.tgz ADD --chmod=0444 --checksum=sha256:fb1bf35e12a5f8c8b5d795bd9d89e90c278e4929ea9d99c21e452753b84d51f5 https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.0.tgz /msal-common-16.14.0.tgz ADD --chmod=0444 --checksum=sha256:a2a7d8872dda8f65fd89bef21576315b1cd472f4478e0ba8c068d3a41848c32e https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.6.0.tgz /msal-node-5.6.0.tgz -ADD --chmod=0444 --checksum=sha256:ed05a10788e4bec3cc2d6926b8ac4be817c601e16992019ebfa408e3d665c834 https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.1.tgz /msteams-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:d89c4b97a49f3fa1f66b8262077c4835565579d99284f44fa50ed0cb13f0896e https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.2.tgz /msteams-2026.9.2.tgz ADD --chmod=0444 --checksum=sha256:04ada283b29ea69189a5eac97fa3815f20480255fa4667258366c31e1d92ced4 https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz /negotiator-1.1.0.tgz ADD --chmod=0444 --checksum=sha256:e5c18f3cfc46d072f9aa23439644c9c18fac62729e6385aadcc937e458507a09 https://registry.npmjs.org/@ubjs/node/-/node-0.31.0-3.tgz /node-0.31.0-3.tgz ADD --chmod=0444 --checksum=sha256:869f053ddf77958e8581e104179f7604a0b058fda70c4aaf338aecec9c6c1289 https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz /node-26.4.1.tgz @@ -398,7 +406,7 @@ ADD --chmod=0444 --checksum=sha256:a70348669b01db602faf140e984e61b01c4380f9b4bf5 ADD --chmod=0444 --checksum=sha256:615af90e363f8f276b4b54f8e6c163cf3686dce1d8867dd7e52cbed4d38d2dab https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz /node-fetch-3.3.2.tgz ADD --chmod=0444 --checksum=sha256:940450fb4158bddc23ae156432a67338a4d7ab6a585b639c61b3b0a14d2bac24 https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz /node-gyp-build-4.8.4.tgz ADD --chmod=0444 --checksum=sha256:c28df2b8de694493420c9f090c53f1cc9d087b64ba7b6b59e262588198688ef5 https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.2.0-beta.15.tgz /node-pty-1.2.0-beta.15.tgz -ADD --chmod=0444 --checksum=sha256:f65675c6fc745a4f15a2abd316883e715ab53afbc4b4fbb2ff57ed280360b9b6 https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz /node-server-2.1.1.tgz +ADD --chmod=0444 --checksum=sha256:45d02627ce61f2eeb27bb0f6b7480dfe5e2206bb1edee68e39518e049fad1082 https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz /node-server-2.1.3.tgz ADD --chmod=0444 --checksum=sha256:82163aa3e3a46ef2a49f8d20f21b67af52724b5be35246d685c1180b9f918ddf https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz /npm-run-path-6.0.0.tgz ADD --chmod=0444 --checksum=sha256:db23d012df85d2c0308c7b3fd3bd538664d9e0e1dca1aa96e659641b76457a8f https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz /nth-check-3.0.1.tgz ADD --chmod=0444 --checksum=sha256:782d726a263ba7b26cced612af97b80035516df4b0cd788524e7b2cebc4e29ed https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz /object-assign-4.1.1.tgz @@ -407,13 +415,16 @@ ADD --chmod=0444 --checksum=sha256:f64d42f1049c386cdac5204737e09564271639b2b7d20 ADD --chmod=0444 --checksum=sha256:cf51460ba370c698f68b976e514d113497339ba018b6003e8e8eb569c6fccfcf https://registry.npmjs.org/once/-/once-1.4.0.tgz /once-1.4.0.tgz ADD --chmod=0444 --checksum=sha256:b5b60d1271802682a5c8e0ed1cc8e825d3be7fd610afaaf3d4d8ce799e825be9 https://registry.npmjs.org/open/-/open-10.2.0.tgz /open-10.2.0.tgz ADD --chmod=0444 --checksum=sha256:8d1b89c7bdb749d834c502e94d0ece4909aaac213dab2bc53bbd16119f23f6dd https://registry.npmjs.org/openai/-/openai-7.5.0.tgz /openai-7.5.0.tgz -ADD --chmod=0444 --checksum=sha256:1bfcac877d53f1e41b69d15c24e081895b2f07d6ff2ffdfe0bf8a7336ab00e59 https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz /openclaw-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:3431f4cd2d8dbd6b936def2694ac27e19fa0256295cf4ada0f652ecf1c9ee520 https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz /openclaw-2026.9.2.tgz ADD --chmod=0444 --checksum=sha256:467e8047f7114e45944961fcd3eda9421843c9c65db61ea24176e252ab800ee4 https://registry.npmjs.org/@tencent-weixin/openclaw-weixin/-/openclaw-weixin-2.4.9.tgz /openclaw-weixin-2.4.9.tgz ADD --chmod=0444 --checksum=sha256:384b452409cfeb5c6fa82dc68ebfa498b24717b74fb8d3fe6eb2bb89908db295 https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz /p-limit-2.3.0.tgz ADD --chmod=0444 --checksum=sha256:284dcc4cc5b485b5793be28d0716f0a1270fb0eeb9f1f4c7cff7f320cfe8e21e https://registry.npmjs.org/p-limit/-/p-limit-7.3.1.tgz /p-limit-7.3.1.tgz ADD --chmod=0444 --checksum=sha256:d95a6ae462e3d967deb0c250bda1c3bbebfe86a58832d27b204c7b74a76fa5f0 https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz /p-locate-4.1.0.tgz ADD --chmod=0444 --checksum=sha256:b52ce5684950a7e5792d67c2bec28125695c55c1d689a778ee6c64efdb15b5f0 https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz /p-map-7.0.6.tgz ADD --chmod=0444 --checksum=sha256:21112bb484de3120e9e85f1ebe6a66125ecfda48072ae48b0d202693337fb558 https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz /p-retry-4.6.2.tgz + +FROM scratch AS openclaw-managed-messaging-npm-common-archives-4 + ADD --chmod=0444 --checksum=sha256:a390b2b89899df950afc0304eaba7cd1f5e3746b2e370758a9b50f177e713790 https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz /p-try-2.2.0.tgz ADD --chmod=0444 --checksum=sha256:0d4028dc0352a740c30cbfd772917f2744986d42bfa0b06ec7642bffe7ad3941 https://registry.npmjs.org/pako/-/pako-1.0.11.tgz /pako-1.0.11.tgz ADD --chmod=0444 --checksum=sha256:abbc7e193f7bcd9d26f9fe994f846ee3fb442b0ec215e7f61fa1080a29b7fb68 https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz /parse-ms-4.0.0.tgz @@ -424,11 +435,8 @@ ADD --chmod=0444 --checksum=sha256:dbb535c9302ce9b3f777ece3ff055cc8d88890a1e1ded ADD --chmod=0444 --checksum=sha256:4b8999acb914830edcd3c5b8fec632b32c6bc759ac3edc86336f5a9e08ba7b92 https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz /path-key-3.1.1.tgz ADD --chmod=0444 --checksum=sha256:aea29a2c9a0986a2eadb6d872c4e5537995612ea9babcbd8da3c2d74b3f049a7 https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz /path-key-4.0.0.tgz ADD --chmod=0444 --checksum=sha256:e8712a9c53b0a2a27cfecc7b80c54df92afb4643c01351e2b2ebb7784bcabd78 https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz /path-to-regexp-8.4.2.tgz - -FROM scratch AS openclaw-managed-messaging-npm-common-archives-4 - ADD --chmod=0444 --checksum=sha256:5a23015c1cd2c38e3c492dd96929985247b92f52d2ff0fb948d29edca52bc50a https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz /photon-node-0.3.4.tgz -ADD --chmod=0444 --checksum=sha256:3abec26d852a9574fd341b8b4984277fc76dabb57a0360df4c19cc1fc0df993e https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz /pi-tui-0.84.2.tgz +ADD --chmod=0444 --checksum=sha256:e5314d46b03b6735bfa0c9227c09bac54f9e0df134409f8b86e5650b2ec0285c https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.3.tgz /pi-tui-0.84.3.tgz ADD --chmod=0444 --checksum=sha256:d1fcbbae5bc05562d13de7c520c2951699e8262a8317fa6c8bbcd8dcff3bea70 https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz /pkce-challenge-5.0.1.tgz ADD --chmod=0444 --checksum=sha256:954be1e183d0ddb9748fe0d2d08b0b66a9210c74dd75c397aeb70303b9f08a00 https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz /playwright-core-1.62.1.tgz ADD --chmod=0444 --checksum=sha256:4d960bbbe078022d7a36822e2874f884c7410ead111f3603d69d70fc7af36f20 https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz /pngjs-5.0.0.tgz @@ -443,9 +451,11 @@ ADD --chmod=0444 --checksum=sha256:e9c52dbf1e382319d5da00b8d964805859b7eb1424450 ADD --chmod=0444 --checksum=sha256:cf7d916cade644852293de603369f2f3ef13171e3f78cc3baf9a1bd6854190bd https://registry.npmjs.org/@openclaw/proxyline/-/proxyline-0.3.7.tgz /proxyline-0.3.7.tgz ADD --chmod=0444 --checksum=sha256:0c7274f0c299f39c2fddf54a2e0039b785977b0173c02d0b3f65fad68923e2b0 https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz /qrcode-1.5.4.tgz ADD --chmod=0444 --checksum=sha256:3a6260c4e0d80bd527a3f930e90ea2348c03646621f25aa0bd960ee205a0a706 https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz /qrcode-terminal-0.12.0.tgz +ADD --chmod=0444 --checksum=sha256:eaa6226f03b9046e4825f2627be3bd857e91dce9f105d953f94c5795322f1e4c https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz /qs-6.15.1.tgz ADD --chmod=0444 --checksum=sha256:c0278b636e7a016d6e835cd8f194a63c276dff430620e4a04344a4ba8892c0f9 https://registry.npmjs.org/qs/-/qs-6.15.3.tgz /qs-6.15.3.tgz ADD --chmod=0444 --checksum=sha256:f7a1bfc96c3a0c1172f1f3ef3c280f5ce8054841922e715f0d686da62d7beba4 https://registry.npmjs.org/qs/-/qs-6.16.0.tgz /qs-6.16.0.tgz ADD --chmod=0444 --checksum=sha256:67f300077af91aa29497cfffbcf7f83d8cda7de39c4010b94372da2ab1ea796f https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-3.5.0.tgz /quickjs-wasi-3.5.0.tgz +ADD --chmod=0444 --checksum=sha256:c2fc511ff7741008fed196c91d9b95254dcd27c12eedf4c29c160f90118b72fb https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz /range-parser-1.2.7.tgz ADD --chmod=0444 --checksum=sha256:51b79ec072db6788b132680256e9e733af8bb091df4f8ce8562ca631118f0fae https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz /range-parser-1.3.0.tgz ADD --chmod=0444 --checksum=sha256:35256483616db7537a37c689b7d377b38dd0b152b59e88442cefeb2c730d74b9 https://registry.npmjs.org/rastermill/-/rastermill-0.3.2.tgz /rastermill-0.3.2.tgz ADD --chmod=0444 --checksum=sha256:66de2a025036de58bbe50ab1d42a24ec6d33eda338b8115a3ebf942dae8419db https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz /raw-body-3.0.2.tgz @@ -462,7 +472,7 @@ ADD --chmod=0444 --checksum=sha256:7521d8445e845475e888ccb7af473c4afb17aabafefe3 ADD --chmod=0444 --checksum=sha256:b144af37b39a9517f7a89f1d867e9c2cf29f13f4147d3e80c499fe6ffab69461 https://registry.npmjs.org/router/-/router-2.2.0.tgz /router-2.2.0.tgz ADD --chmod=0444 --checksum=sha256:d29ace7117aaa0d6b119027e9a157c238e6899bbb35d03f508ae8d4fa9ca8c9d https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz /run-applescript-7.1.0.tgz ADD --chmod=0444 --checksum=sha256:65b1049d7858c8d00adefe07a03671a218b439d9b7ee55a8a1af9fca1a19e759 https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz /runner-2.0.3.tgz -ADD --chmod=0444 --checksum=sha256:0acb45d7992e5fba729bb1d8f2586af7e522518aebd9b2859441b387ef890ad8 https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz /runtime-1.11.1.tgz +ADD --chmod=0444 --checksum=sha256:e6eb8913a08b551436d65f9a5cfbdcc8006f58e6798226d24e695c25679b5e49 https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz /runtime-1.11.3.tgz ADD --chmod=0444 --checksum=sha256:4d7f1bd502a1a64d47625cc738d13284865f0666d2ed01f244de0adf05b69aa5 https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz /runtime-7.29.7.tgz ADD --chmod=0444 --checksum=sha256:e09206c60fccafb952c854af7629cbb031a98d6da2e143fb3aa3c8a48402aa22 https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz /safe-buffer-5.1.2.tgz ADD --chmod=0444 --checksum=sha256:5d181804516c4a693a384272a7bd0e42d17e0d4b301ccfbe408669ccafdcb3e8 https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz /safe-buffer-5.2.1.tgz @@ -472,11 +482,12 @@ ADD --chmod=0444 --checksum=sha256:2cac3f3e38fec2815ed9efafa2947faf8c6957310684f ADD --chmod=0444 --checksum=sha256:57beb0f7705b09406e5bcc984d1f6a141940680b4c42755be026f77f64365a37 https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.4.0.tgz /sdk-1.4.0.tgz ADD --chmod=0444 --checksum=sha256:4465839df9cf25046eacb64e37a38e7a2d033546356335190234bad60bd85d42 https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz /semantic-conventions-1.43.0.tgz ADD --chmod=0444 --checksum=sha256:d85045d4300d7d57c891336b95df532e73f34c22ffcd222452b6d08b9d127d5d https://registry.npmjs.org/semver/-/semver-7.8.5.tgz /semver-7.8.5.tgz -ADD --chmod=0444 --checksum=sha256:fa254fb316dd23ddcb2beebd533b23788aec4cf6a3dba58af34150170435c472 https://registry.npmjs.org/send/-/send-1.2.1.tgz /send-1.2.1.tgz +ADD --chmod=0444 --checksum=sha256:5bda46e7ff5ed20273da6d640219a18fe05e426c38dd7fb2716a37c463e87f23 https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz /serve-static-2.2.0.tgz ADD --chmod=0444 --checksum=sha256:36d4f72bb59372eb18202fee25ff3d8bf46655f0121830fbe32e32cbdc625f43 https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz /serve-static-2.2.1.tgz ADD --chmod=0444 --checksum=sha256:d934aee7db9e09da09e87724743315ffe888130aa6e04fbbdecac985f6ae693d https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz /set-blocking-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:5cb9fc22698364ed42c02d6aa3dc50ffeafa68452ae84699672e3dfd74922c9e https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz /setimmediate-1.0.5.tgz ADD --chmod=0444 --checksum=sha256:c83bcc6ea632567e3f6928a83a1c0c7073519aaca9b88b847a3b404417eadfe2 https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz /setprototypeof-1.2.0.tgz +ADD --chmod=0444 --checksum=sha256:3b6bbbe6b308f5c383938ca4a31926cd5d79c4651ed8c5fda16a97da2014a9ac https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz /sharp-0.35.5.tgz ADD --chmod=0444 --checksum=sha256:9acba5bd18a51e9cdf5898380e4df63f803e1844def64ae1a46f88cff86d556e https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz /shebang-command-2.0.0.tgz ADD --chmod=0444 --checksum=sha256:fedbabaa6db26c6be0183f82777dfa852d59a62f8885de93bd32ebc28758958f https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz /shebang-regex-3.0.0.tgz ADD --chmod=0444 --checksum=sha256:e6edbc8f203901612a3cd938f940ed520333923986d5427b95c87aa1882e7bd5 https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz /side-channel-1.1.1.tgz @@ -485,7 +496,7 @@ ADD --chmod=0444 --checksum=sha256:3b256b6421300bcc962d891b1588fd4b64e84e339b9c2 ADD --chmod=0444 --checksum=sha256:3b2a54f0c5e7ad898c8f0ffda2a6805fb2cc5d68f53addf0b4a9ec0db9d0d06e https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz /side-channel-weakmap-1.0.2.tgz ADD --chmod=0444 --checksum=sha256:9d3b58a811ecf6a641537387289274cd14f5bb912a27e4f1f2a74182bca8b795 https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz /signal-exit-4.1.0.tgz ADD --chmod=0444 --checksum=sha256:9e4d29b24315611de5a1767ca1b09716f40bb04534836295fe36d80c643974b3 https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz /sisteransi-1.0.5.tgz -ADD --chmod=0444 --checksum=sha256:34d729873e80c4ba023ca475f174fa504eca3746202c31fc290d72d00abd36f5 https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.1.tgz /slack-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:79ad40233b041c2081408efe59c93c2d38ca508429e016dd9843cccffc8cf49a https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.2.tgz /slack-2026.9.2.tgz ADD --chmod=0444 --checksum=sha256:bdbca10d17ff5a5802d5acfc7b2f22f9f9bf587632a95650d3c5f513c7092b86 https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz /source-map-0.6.1.tgz ADD --chmod=0444 --checksum=sha256:5d9b04ef3e6824fdcf91cfcc03ab427fae486bc6859735805593f51b3554f636 https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz /source-map-support-0.5.21.tgz ADD --chmod=0444 --checksum=sha256:99ae8b2159aa2d25a0186b7b07d8ef21478370af2f9755d905f8055f8b67307b https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz /sqlite-vec-0.1.9.tgz @@ -504,6 +515,9 @@ ADD --chmod=0444 --checksum=sha256:0fa46b9f499211334d093015322901649c53d94908a0f ADD --chmod=0444 --checksum=sha256:452f2544ddfaf8db6034a108066009f588d6e8db9d8cde52aceb3fa5e9fef782 https://registry.npmjs.org/@microsoft/teams.graph/-/teams.graph-2.0.15.tgz /teams.graph-2.0.15.tgz ADD --chmod=0444 --checksum=sha256:991d87763add805a12d5b3e67b201476681a5b738d8dcb9229bed1df755acba0 https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz /text-codec-0.2.2.tgz ADD --chmod=0444 --checksum=sha256:186fcc77488de327daf911d362d4e773bab9909f1df2a5f0c20b875205b92e08 https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz /toidentifier-1.0.1.tgz + +FROM scratch AS openclaw-managed-messaging-npm-common-archives-5 + ADD --chmod=0444 --checksum=sha256:911758ceca239c8e5372700eedfbbd514f16d3c117b5af0a648f6e720487c209 https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz /token-0.3.0.tgz ADD --chmod=0444 --checksum=sha256:eb4820714d28f6dad949d392e7b74ec919ae3b120421240a032027bf2bd25f41 https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz /token-types-6.1.2.tgz ADD --chmod=0444 --checksum=sha256:164ae1eb32cea353551bbc7f9358dcaae4ffabbe65ec37a92ca464a9570a2a0a https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz /tr46-0.0.3.tgz @@ -514,15 +528,11 @@ ADD --chmod=0444 --checksum=sha256:af0bef7c0eb54ba5fbb71040149b91decfa2d7b5099eb ADD --chmod=0444 --checksum=sha256:66f635d5eeabae44807534976913a102cf615b9a045368359c9f79ae6ee2119e https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz /tslib-2.8.1.tgz ADD --chmod=0444 --checksum=sha256:9ce5696fad6f29d8cc1ac86b4c2701e97121286645cd98b35fbbaafe160215ed https://registry.npmjs.org/tslog/-/tslog-4.11.0.tgz /tslog-4.11.0.tgz ADD --chmod=0444 --checksum=sha256:9a53088d69cd488e0c2cb4fcee5a983089c0d492404cf212161c77501fb302fc https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz /type-is-2.1.0.tgz -ADD --chmod=0444 --checksum=sha256:2ea093eb4d893c30633d3b8405b767e7857bf64cb7d29ac33a6861abe779087d https://registry.npmjs.org/typebox/-/typebox-1.3.17.tgz /typebox-1.3.17.tgz +ADD --chmod=0444 --checksum=sha256:a078d4f68962c576def32ced7038a4cfe1ab7df7dde3be77a7dade86d259005d https://registry.npmjs.org/typebox/-/typebox-1.3.18.tgz /typebox-1.3.18.tgz ADD --chmod=0444 --checksum=sha256:bd128caf48915fc9be919de1b05e37debd43258aca266140dde80fca0a9db928 https://registry.npmjs.org/@grammyjs/types/-/types-5.0.0.tgz /types-5.0.0.tgz ADD --chmod=0444 --checksum=sha256:33cd0ee1beaa8c9e9d15a9da836c62ddea4c34a42d7c2d349dbc80d94165d22a https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz /typescript-6.0.3.tgz ADD --chmod=0444 --checksum=sha256:f3fb42099ea7a0efa2753b3e770fa0d505714e1c7d75fc1fa6c5aac9ba1baad1 https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz /uhyphen-0.2.0.tgz - -FROM scratch AS openclaw-managed-messaging-npm-common-archives-5 - ADD --chmod=0444 --checksum=sha256:65834dc9ce7ecceff4334a14796c85960cbf665d09364698bf3196ceed04d677 https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz /uint8array-extras-1.5.0.tgz -ADD --chmod=0444 --checksum=sha256:93b3abe22a9d2858938b5f3829a9fd8952392348ffed9057662ae846e2e46d54 https://registry.npmjs.org/undici/-/undici-7.29.1.tgz /undici-7.29.1.tgz ADD --chmod=0444 --checksum=sha256:740638ae32d78d2646a6727950e365fa26b6fa87913fa096e60ed4afeb4634aa https://registry.npmjs.org/undici/-/undici-8.10.2.tgz /undici-8.10.2.tgz ADD --chmod=0444 --checksum=sha256:07a721cb2cd0dd798c24757de34d14e8b640ff8fddef85d662e00b392562a1f2 https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz /undici-types-8.3.0.tgz ADD --chmod=0444 --checksum=sha256:e4bfbbe867144ff24f73198367479378c8b6cffc798a2ec0756a81097606908e https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz /unicorn-magic-0.3.0.tgz @@ -536,7 +546,7 @@ ADD --chmod=0444 --checksum=sha256:85774fffee09f70bde084cebcebae20b3cf6f48239f61 ADD --chmod=0444 --checksum=sha256:1ee138d3dc0263ead35c40604da75d7d56c4fa0ef32dc2e3a7fbac10480ebb54 https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz /web-streams-polyfill-3.3.3.tgz ADD --chmod=0444 --checksum=sha256:adf5677e04711c597200058971a299fc9fd4133891ee72ec02acf4932e659fdf https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.13.tgz /web-tree-sitter-0.26.13.tgz ADD --chmod=0444 --checksum=sha256:e4dfc34b40947c2cf0038cd95fa6de21f4dac93224a7ad8e169205f5c2e22da8 https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz /webidl-conversions-3.0.1.tgz -ADD --chmod=0444 --checksum=sha256:ff945ddd5edc39d26e6000d15fdb329b94e8a227515338f80c98d474557f2aa4 https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.1.tgz /whatsapp-2026.9.1.tgz +ADD --chmod=0444 --checksum=sha256:aebf644aef491accdc255d37d8fb9752232f886f5c067d6aa9c88028111b0001 https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.2.tgz /whatsapp-2026.9.2.tgz ADD --chmod=0444 --checksum=sha256:b09dc471f573a876eeac3902b8c1da62af5cdbbca2c6fba4a06f119f89cb7ed3 https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz /whatwg-url-5.0.0.tgz ADD --chmod=0444 --checksum=sha256:a13adf5fddeb769655edce551e81fbb11904b9c9be76d95e41da8c4c499d4edc https://registry.npmjs.org/which/-/which-2.0.2.tgz /which-2.0.2.tgz ADD --chmod=0444 --checksum=sha256:9ece3c301c82005618410fc338bde9f0e2e38f226dbeebdc3a1c79e1e55636dd https://registry.npmjs.org/which-command/-/which-command-0.1.0.tgz /which-command-0.1.0.tgz @@ -571,20 +581,24 @@ COPY --from=openclaw-managed-messaging-npm-common-archives-5 / / FROM openclaw-managed-messaging-npm-common-archives AS openclaw-managed-messaging-npm-amd64-archives ADD --chmod=0444 --checksum=sha256:d58787dcf1d9d64c852ee448cd0e6228047eded78e7c5837fbcbecf9a93385a5 https://registry.npmjs.org/@trycua/cua-driver-linux-x64-gnu/-/cua-driver-linux-x64-gnu-0.22.0.tgz /cua-driver-linux-x64-gnu-0.22.0.tgz -ADD --chmod=0444 --checksum=sha256:cb31bdaecad5fb5eeac085cf215b0981b9f00c74bd0e0680dc459af8724e88fa https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-gnu/-/fs-safe-linux-x64-gnu-0.7.0.tgz /fs-safe-linux-x64-gnu-0.7.0.tgz +ADD --chmod=0444 --checksum=sha256:bffcf876c8da897621a873273c2c8119b1923fe2d5bf656569519ccf783844ab https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-gnu/-/fs-safe-linux-x64-gnu-0.8.1.tgz /fs-safe-linux-x64-gnu-0.8.1.tgz ADD --chmod=0444 --checksum=sha256:82f38580fe47fdf9f06f854920329eca54b5514997bf202397ef89a27ec82cab https://registry.npmjs.org/@koromix/koffi-linux-x64/-/koffi-linux-x64-3.1.6.tgz /koffi-linux-x64-3.1.6.tgz ADD --chmod=0444 --checksum=sha256:08e05d837c6b3faefdd3e77ac3155c2654392a62d241961bcccc221f3783170a https://registry.npmjs.org/@ubjs/node-linux-x64-gnu/-/node-linux-x64-gnu-0.31.0-3.tgz /node-linux-x64-gnu-0.31.0-3.tgz ADD --chmod=0444 --checksum=sha256:754dae77f06207acbb65423fd45d4482d20a563200c520196c8222f2a6f5ba3c https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.2.0-beta.15.tgz /node-pty-linux-x64-1.2.0-beta.15.tgz +ADD --chmod=0444 --checksum=sha256:e371cb713fc8822c96a0d3fb60a3d092799d0aefab6d7f1fb3b72d8d75f1d109 https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz /sharp-libvips-linux-x64-1.3.4.tgz +ADD --chmod=0444 --checksum=sha256:556787285d3244c07686a355de5b5a4c4d0019a12785bf24764e756a96ea3334 https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz /sharp-linux-x64-0.35.5.tgz ADD --chmod=0444 --checksum=sha256:d75c33662b3ce690d122a5f4285a3acf2f6ba288c46331a7b1fad593b6da2908 https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz /sqlite-vec-linux-x64-0.1.9.tgz # hadolint ignore=DL3006 FROM openclaw-managed-messaging-npm-common-archives AS openclaw-managed-messaging-npm-arm64-archives ADD --chmod=0444 --checksum=sha256:675c48bfbe39b1e49e975f0537b4d3b751ad372368e3a696c92b8f8e91c9133a https://registry.npmjs.org/@trycua/cua-driver-linux-arm64-gnu/-/cua-driver-linux-arm64-gnu-0.22.0.tgz /cua-driver-linux-arm64-gnu-0.22.0.tgz -ADD --chmod=0444 --checksum=sha256:87092f5333da5e349b7c73f7079a169e14c5147266e6daa4d28e42502d4c9211 https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-gnu/-/fs-safe-linux-arm64-gnu-0.7.0.tgz /fs-safe-linux-arm64-gnu-0.7.0.tgz +ADD --chmod=0444 --checksum=sha256:c245dd159c5a285aaf8ac468e0e324bbdf6bf5eea7e4ed5baeb8a07dae853056 https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-gnu/-/fs-safe-linux-arm64-gnu-0.8.1.tgz /fs-safe-linux-arm64-gnu-0.8.1.tgz ADD --chmod=0444 --checksum=sha256:17c3bfb024cf04595786c10578dce21d69eda8e35ecaff7d8a2805b0619dd836 https://registry.npmjs.org/@koromix/koffi-linux-arm64/-/koffi-linux-arm64-3.1.6.tgz /koffi-linux-arm64-3.1.6.tgz ADD --chmod=0444 --checksum=sha256:31c4c8ecbd26484f660c03576fb1c0de7883f45b0018df4b76fd316845b96a52 https://registry.npmjs.org/@ubjs/node-linux-arm64-gnu/-/node-linux-arm64-gnu-0.31.0-3.tgz /node-linux-arm64-gnu-0.31.0-3.tgz ADD --chmod=0444 --checksum=sha256:f9da59f77496d1f3065d368b61c3af0b9c6785f0b22c5a70144283fcdf6b3036 https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.2.0-beta.15.tgz /node-pty-linux-arm64-1.2.0-beta.15.tgz +ADD --chmod=0444 --checksum=sha256:495e6c63c797c6b080a9dbd827a8cc0790b77f8f7d5d82f1764dadfc5068a845 https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz /sharp-libvips-linux-arm64-1.3.4.tgz +ADD --chmod=0444 --checksum=sha256:2ba164e9d4d2f56bfc2a1b8badff07f49d1c98c061dde23efbf97617d23be07d https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz /sharp-linux-arm64-0.35.5.tgz ADD --chmod=0444 --checksum=sha256:96a03e2ac0906b035085ec4e2307dd8076fb02673b8f5bb6659a5f4feeacd892 https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz /sqlite-vec-linux-arm64-0.1.9.tgz # hadolint ignore=DL3006 @@ -820,11 +834,11 @@ RUN set -eu; \ # Rebuild from prepared system inputs before copying locked npm seeds. FROM openclaw-system ARG BASE_IMAGE -ARG OPENCLAW_VERSION=2026.9.1 -ARG OPENCLAW_2026_9_1_INTEGRITY=sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w== -ARG OPENCLAW_2026_9_1_TARBALL=https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz -ARG OPENCLAW_DIAGNOSTICS_OTEL_2026_9_1_INTEGRITY=sha512-3MWLli9L6HTVdrjqHmwOvNvIr6emsnuNQe4iE2sDqb8E5wn4Vq1rcsz+InL1YFudbStr089ZtS0tNAQ6qU+tnA== -ARG OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY=sha512-4+j+eQTToV3k7Cb25MUL6h2uL8cJYyuLytfpd/sJK/HjR43dgKBqKpBsb1+I3w1Jr6PLpnjSf6/I3//3K0cdnA== +ARG OPENCLAW_VERSION=2026.9.2 +ARG OPENCLAW_2026_9_2_INTEGRITY=sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg== +ARG OPENCLAW_2026_9_2_TARBALL=https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz +ARG OPENCLAW_DIAGNOSTICS_OTEL_2026_9_2_INTEGRITY=sha512-yilG4G1Fd1yvW65Qy+qNPR+x6tBjwVmTWv+7BULoN70HY3BJqt9YVOL42PvWXHYpaTs/LwUlisqxjbJRfYyi9A== +ARG OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY=sha512-6416aPlfnAKlu8IBrrjgfoiss/10xB32ywFwnIf/fkVMQE61qsmzA/qxUniQuDwOB6EBFNEkNs54DhIT7g3UVg== # E2E-only legacy fixture pins used by stale-sandbox/rebuild tests that # intentionally build an older OpenClaw base image before proving upgrade # behavior. Production workflows reject the fixture flag, both legacy version @@ -882,9 +896,7 @@ RUN test -f /usr/local/bin/node \ && test -z "$node_unsafe" \ && json5_unsafe="$(find -L /opt/nemoclaw/node_modules/json5 \( ! -user root -o -perm /022 \) -print -quit)" \ && test -z "$json5_unsafe" -# Reviewed-archive invariants (#5896): the dedicated build stage materializes -# the committed lock, seeds resolver metadata, and re-packs every archive offline -# before this root-owned immutable cache enters the final image. +# Reviewed-archive invariants (#5896): locked install, metadata seeding, offline re-packing, root-owned immutable cache. COPY --from=wechat-npm-cache /out/wechat-npm-cache/ /usr/local/share/nemoclaw/wechat-npm-cache/ COPY --from=openclaw-patch-payload / / @@ -908,8 +920,9 @@ COPY --from=codex-acp-runtime /usr/local/lib/node_modules/@zed-industries/ /usr/ COPY --from=codex-acp-runtime /usr/local/bin/codex-acp /usr/local/bin/codex-acp RUN command -v codex-acp >/dev/null +# Matching official digest-pinned bases reuse graphs; others reinstall. # OPENCLAW_VERSION is the NemoClaw runtime build target; enforce the blueprint minimum. -# Reuse matching official digest-pinned bases; reinstall other verified locked graphs. +# Keep archive SRI, basename, local-only install, and cleanup checks. # hadolint ignore=DL3059,DL4006,DL3016,SC2015 RUN --mount=type=secret,id=nemoclaw-mcporter-audit-receipt,required=false \ --mount=type=secret,id=nemoclaw-mcporter-audit-raw-report,required=false \ @@ -933,7 +946,7 @@ RUN --mount=type=secret,id=nemoclaw-mcporter-audit-receipt,required=false \ fi; \ EXPECTED_INTEGRITY=""; \ EXPECTED_TARBALL=""; \ - if [ "$OPENCLAW_VERSION" = "2026.9.1" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_9_1_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_9_1_TARBALL"; fi; \ + if [ "$OPENCLAW_VERSION" = "2026.9.2" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_9_2_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_9_2_TARBALL"; fi; \ if [ "$OPENCLAW_VERSION" = "2026.3.11" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_3_11_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_3_11_TARBALL"; fi; \ if [ "$OPENCLAW_VERSION" = "2026.4.24" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_4_24_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_4_24_TARBALL"; fi; \ if [ -z "$EXPECTED_INTEGRITY" ]; then \ @@ -941,8 +954,8 @@ RUN --mount=type=secret,id=nemoclaw-mcporter-audit-receipt,required=false \ fi; \ OPENCLAW_LOCK_SHA256=none-legacy-fixture; \ OPENCLAW_RECIPE='ignore-scripts+reviewed-lifecycle-v1'; \ - if [ "$OPENCLAW_VERSION" = "2026.9.1" ]; then \ - OPENCLAW_LOCK_SHA256=71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113; \ + if [ "$OPENCLAW_VERSION" = "2026.9.2" ]; then \ + OPENCLAW_LOCK_SHA256=cbcfdd15430b81f50ada9f39858a694815e570c8bb3e6b4bb9f1c0b53bf0ef4a; \ ACTUAL_OPENCLAW_LOCK_SHA256="$(sha256sum /usr/local/lib/nemoclaw/openclaw-runtime/package-lock.json | awk '{print $1}')"; \ [ "$ACTUAL_OPENCLAW_LOCK_SHA256" = "$OPENCLAW_LOCK_SHA256" ] \ || { echo "ERROR: OpenClaw lock SHA-256 mismatch (expected $OPENCLAW_LOCK_SHA256, found $ACTUAL_OPENCLAW_LOCK_SHA256)" >&2; exit 1; }; \ @@ -1026,7 +1039,7 @@ RUN --mount=type=secret,id=nemoclaw-mcporter-audit-receipt,required=false \ # install spans image layers. Removing it first also prevents unreviewed # files from surviving a same-version reinstall. rm -rf /usr/local/lib/node_modules/openclaw /usr/local/bin/openclaw; \ - if [ "$OPENCLAW_VERSION" = "2026.9.1" ]; then \ + if [ "$OPENCLAW_VERSION" = "2026.9.2" ]; then \ node /scripts/lib/reviewed-npm-archive.mts --verify-lock \ --lock-sha256 "$OPENCLAW_LOCK_SHA256" \ --lockfile /usr/local/lib/nemoclaw/openclaw-runtime/package-lock.json \ @@ -1260,13 +1273,8 @@ RUN set -eu; \ fi; \ fi; \ # --- Patch 2b: allow OpenShell host gateway only through web_fetch trusted env proxy --- \ - # Reviewed against openclaw@2026.9.1 dist: fetchWithWebToolsNetworkGuard \ - # passes useEnvProxy into withTrustedEnvProxyGuardedFetchMode(resolved), and \ - # the SSRF guard consumes policy.allowedHostnames to skip private-network \ - # checks for a normalized hostname. hostnameAllowlist only gates \ - # hostname pattern matching and does not bypass .internal/private blocking. \ - # Executable fixture proof lives in test/security/fetch-guard-patch-regression.test.ts; \ - # the live network-policy E2E exercises this path in the assembled image. \ + # Only allow host.openshell.internal for sandbox web_fetch through the env proxy. \ + # allowedHostnames bypasses private-network rejection; hostnameAllowlist does not. \ web_guard_files="$(grep -RIlE --include='*.js' 'function fetchWithWebToolsNetworkGuard\(params\)' "$OC_DIST" || true)"; \ if [ -n "$web_guard_files" ]; then \ patched_host_gateway=0; \ @@ -1296,13 +1304,8 @@ RUN set -eu; \ fi; \ fi; \ # --- Patch 4: route unconfigured strict fetches through the sandbox egress proxy (#4687) --- \ - # Reviewed against openclaw@2026.9.1 dist fetch-guard: the STRICT-mode \ - # managed-proxy gate is `mode === GUARDED_FETCH_MODE.STRICT && \ - # isManagedProxyActive()`. Extend activation to OPENSHELL_SANDBOX=1 only \ - # for fetches with no explicit dispatcherPolicy so \ - # the per-request direct dispatcher reuses the env proxy (EnvHttpProxyAgent) \ - # like the managed-proxy path already does; explicit-proxy / direct dispatcher \ - # policies and out-of-sandbox behavior are unchanged. \ + # Activate the env proxy for sandbox STRICT requests without dispatcherPolicy. \ + # Preserve explicit-proxy/direct overrides and behavior outside the sandbox. \ mp_files="$(grep -RIlF --include='*.js' 'const isStrictManagedProxyActive = mode === GUARDED_FETCH_MODE.STRICT && isManagedProxyActive();' "$OC_DIST" || true)"; \ if [ -n "$mp_files" ]; then \ patched_managed_proxy=0; \ @@ -1330,32 +1333,12 @@ RUN set -eu; \ fi; \ fi; \ # --- Patch 6: cron model-provider preflight opts into trusted env-proxy mode --- \ - # Reviewed against openclaw@2026.9.1 dist: the cron isolated-agent preflight \ - # (`probeLocalProviderEndpoint`) calls `fetchWithSsrFGuard` with \ - # `auditContext: "cron-model-provider-preflight"` and a narrow hostname-allowlist \ - # SsrFPolicy from `buildLocalProviderSsrFPolicy`, but does not pass a `mode`. \ - # Default STRICT mode pins DNS for the managed inference hostname \ - # (`inference.local`), which is intentionally only resolvable through the \ - # OpenShell L7 proxy — pinned `dns.lookup` therefore fails with EAI_AGAIN and \ - # the scheduler permanently skips every cron run. Inject \ - # `mode: "trusted_env_proxy"` so the call uses the env proxy dispatcher; SSRF \ - # protection is retained through the existing hostname allowlist and the \ - # proxy's own ACLs. \ - # \ - # The patch keys on the co-located shape of the reviewed preflight call: in \ - # any file that mentions the audit context literal, both the \ - # `fetchWithSsrFGuard(` helper and the `buildLocalProviderSsrFPolicy` policy \ - # builder must appear. The audit-property matcher tolerates quote and same-line \ - # whitespace changes; the audit literal itself must appear exactly once; and \ - # after patching exactly one patched literal must remain. Any ambiguous \ - # multi-callsite or mixed patched/unpatched layout fails the image build \ - # rather than silently widening the rewrite. \ - # \ - # Removal condition: drop this block (and any related `OC_VERSION` floor bump) \ - # once an OpenClaw release sets `mode: "trusted_env_proxy"` directly at the \ - # preflight call site or otherwise routes the managed inference base URL \ - # through the env-proxy dispatcher by default. The reviewed shape lives at \ - # `src/cron/isolated-agent/model-preflight.runtime.ts` in the openclaw repo. \ + # Route inference.local through the OpenShell env proxy while retaining the \ + # local-provider hostname allowlist and proxy ACLs. Require one audit literal, \ + # fetchWithSsrFGuard, and buildLocalProviderSsrFPolicy in the same file. \ + # Reject ambiguous call sites and mixed patch states. \ + # Remove when cron/isolated-agent/model-preflight.runtime.ts uses the \ + # env-proxy dispatcher natively; also remove any related OC_VERSION floor. \ preflight_files="$(grep -RIlF --include='*.js' 'cron-model-provider-preflight' "$OC_DIST" || true)"; \ if [ -n "$preflight_files" ]; then \ patched_preflight=0; \ @@ -1398,11 +1381,8 @@ RUN set -eu; \ fi; \ fi; \ # --- Patch 3: follow symlinks in plugin-install path checks (#2203) --- \ - # Legacy OpenClaw install-safe-path and install-package-dir layouts reject \ - # symlinked directories via lstat. Change those exact shapes to stat while \ - # retaining realpath containment. OpenClaw 2026.9.1 delegates safe-path \ - # enforcement to @openclaw/fs-safe and already uses stat plus realpath in \ - # install-package-dir; accept only those reviewed replacement shapes. \ + # Replace legacy lstat checks with stat while retaining realpath containment. \ + # Current safe-path delegates to fs-safe; directory guards remain native. \ isp_file="$(grep -RIlE --include='*.js' 'const baseLstat = await fs\.(lstat|stat)\(baseDir\)' "$OC_DIST/install-safe-path-"*.js || true)"; \ if [ -n "$isp_file" ]; then \ sed -i 's/const baseLstat = await fs\.lstat(baseDir)/const baseLstat = await fs.stat(baseDir)/' "$isp_file"; \ @@ -1411,8 +1391,8 @@ RUN set -eu; \ else \ isp_delegate_file="$(grep -RIlF --include='*.js' 'from "@openclaw/fs-safe/advanced"' "$OC_DIST/install-safe-path-"*.js || true)"; \ isp_delegate_count="$(printf '%s\n' "$isp_delegate_file" | awk 'NF { count++ } END { print count + 0 }')"; \ - if [ "$OC_VERSION" != "2026.9.1" ] || [ "$isp_delegate_count" -ne 1 ]; then \ - patch_fail "Patch 3a target missing without the single reviewed 2026.9.1 @openclaw/fs-safe delegation"; \ + if [ "$OC_VERSION" != "2026.9.2" ] || [ "$isp_delegate_count" -ne 1 ]; then \ + patch_fail "Patch 3a target missing without the single reviewed 2026.9.2 @openclaw/fs-safe delegation"; \ fi; \ if ! grep -Fq 'assertCanonicalPathWithinBase' "$isp_delegate_file" \ || ! grep -Fq 'resolveSafeInstallDir' "$isp_delegate_file"; then \ @@ -1432,27 +1412,16 @@ RUN set -eu; \ if ! grep -q 'const baseLstat = await fs\.stat(params\.installBaseDir)' "$ipd_file" && ! grep -q 'await fs\.stat(params\.installBaseDir)).isDirectory()' "$ipd_file"; then echo "ERROR: Patch 3b (install-package-dir) did not find patched/safe installBaseDir stat call" >&2; exit 1; fi; \ if grep -q 'baseLstat\.isSymbolicLink()' "$ipd_file"; then echo "ERROR: Patch 3b (install-package-dir) left baseLstat symlink check" >&2; exit 1; fi; \ else \ - grep -Fq 'if (!(await fs.stat(params.installBaseDir)).isDirectory())' "$ipd_file" \ + grep -Eq 'if \(!\(await fs(\$1)?\.stat\(params\.installBaseDir\)\)\.isDirectory\(\)\)' "$ipd_file" \ || patch_fail "Patch 3b current install-package-dir lacks the reviewed directory stat guard"; \ - grep -Fq 'await fs.realpath(params.installBaseDir) !== params.expectedRealPath' "$ipd_file" \ + grep -Eq 'await fs(\$1)?\.realpath\(params\.installBaseDir\) !== params.expectedRealPath' "$ipd_file" \ || patch_fail "Patch 3b current install-package-dir lacks the reviewed realpath stability guard"; \ echo "INFO: OpenClaw ${OC_VERSION} install-package-dir already uses stat plus realpath stability; Patch 3b not needed"; \ fi; \ # --- Patch 5: bump default WS handshake timeout 10s -> 60s (#2484) --- \ - # OpenClaw's WS connect handshake has a hard-coded 10s timeout on both \ - # client and server. Server-side connect-handler processing can exceed \ - # that limit under load (multiple concurrent connects on slow CI infra), \ - # causing `openclaw agent --json` to fail with "gateway timeout after \ - # ms" and TC-SBX-02 to hit its 90s SSH timeout. \ - # \ - # Both env vars (OPENCLAW_HANDSHAKE_TIMEOUT_MS, \ - # OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS) are clamped at the same \ - # DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS constant, so we patch the \ - # constant itself. Affects both client.js (used by openclaw CLI) and \ - # server.impl.js (gateway side). \ - # \ - # Removal criteria: drop when openclaw fixes the underlying connect \ - # latency, or exposes the timeout as an unbounded env override. \ + # Slow gateway admission can exceed the shared client/server timeout. \ + # Both handshake env overrides are capped by DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS. \ + # Remove when upstream fixes connect latency or exposes an uncapped override. \ hto_files="$(grep -RIlE --include='*.js' 'DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = (1e4|15e3|6e4)' "$OC_DIST" || true)"; \ test -n "$hto_files" || { echo "ERROR: handshake-timeout constant not found" >&2; exit 1; }; \ printf '%s\n' "$hto_files" | xargs sed -i -E 's#DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = (1e4|15e3)#DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 6e4#g'; \ @@ -1823,8 +1792,8 @@ RUN --network=none --mount=from=openclaw-optional-plugin-archives,target=/opt/ne expected_tarball=""; \ archive_name=""; \ case "$plugin_spec" in \ - "@openclaw/diagnostics-otel@2026.9.1") expected_integrity="$OPENCLAW_DIAGNOSTICS_OTEL_2026_9_1_INTEGRITY"; expected_tarball="https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.1.tgz"; archive_name="diagnostics-otel-2026.9.1.tgz" ;; \ - "@openclaw/brave-plugin@2026.9.1") expected_integrity="$OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY"; expected_tarball="https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.1.tgz"; archive_name="brave-plugin-2026.9.1.tgz" ;; \ + "@openclaw/diagnostics-otel@2026.9.2") expected_integrity="$OPENCLAW_DIAGNOSTICS_OTEL_2026_9_2_INTEGRITY"; expected_tarball="https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.2.tgz"; archive_name="diagnostics-otel-2026.9.2.tgz" ;; \ + "@openclaw/brave-plugin@2026.9.2") expected_integrity="$OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY"; expected_tarball="https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.2.tgz"; archive_name="brave-plugin-2026.9.2.tgz" ;; \ esac; \ if [ -z "$expected_integrity" ]; then \ echo "ERROR: OpenClaw plugin ${plugin_spec} has no committed npm integrity pin" >&2; exit 1; \ @@ -2497,7 +2466,7 @@ RUN set -eu; \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" \ "perl-base=5.44.0-1nemoclaw1" \ "perl=5.44.0-1nemoclaw1" \ @@ -2510,7 +2479,7 @@ RUN set -eu; \ test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2"; \ - test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2"; \ + test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3"; \ test "$(dpkg-query -W -f='${Version}' nemoclaw-python3.13-htmlparser-fix)" = "3.13.5-2+deb13u5+nemoclaw1"; \ test "$(dpkg-query -W -f='${Version}' perl-base)" = "5.44.0-1nemoclaw1"; \ test "$(dpkg-query -W -f='${Version}' perl)" = "5.44.0-1nemoclaw1"; \ diff --git a/Dockerfile.base b/Dockerfile.base index 17c143c6fb1..d2ba862ca0a 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -67,7 +67,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates=20250419 \ curl=8.14.1-2+deb13u5 \ git=1:2.47.3-0+deb13u1 \ - libssl-dev=3.5.7-1~deb13u2 \ + libssl-dev=3.5.7-1~deb13u3 \ + libssl3t64=3.5.7-1~deb13u3 \ openssh-server=1:10.0p1-7+deb13u4 \ xz-utils=5.8.1-1+deb13u1 \ zlib1g-dev=1:1.3.dfsg+really1.3.1-1+b1 \ @@ -128,7 +129,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ iptables=1.8.11-2 \ nftables=1.1.3-1 \ libcap2-bin=1:2.75-10+deb13u1+b3 \ - libssl3t64=3.5.7-1~deb13u2 \ + libssl3t64=3.5.7-1~deb13u3 \ util-linux=2.41-5 \ procps=2:4.0.4-9 \ e2fsprogs=1.47.2-3+b12 \ @@ -220,7 +221,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2" \ - && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2" \ + && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3" \ && test "$(dpkg-query -W -f='${Version}' lsof)" = "4.99.4+dfsg-2" \ && test "$(dpkg-query -W -f='${Version}' nemoclaw-python3.13-htmlparser-fix)" = "3.13.5-2+deb13u5+nemoclaw1" \ && test "$(dpkg-query -W -f='${Version}' perl)" = "$perl_version_before" \ @@ -249,7 +250,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" \ > /usr/local/share/nemoclaw/security-packages.txt \ && chown root:root /usr/local/share/nemoclaw/security-packages.txt \ @@ -397,9 +398,9 @@ RUN chmod 444 /usr/local/lib/nemoclaw/sandbox-rlimits.sh \ # OpenClaw version: change the OPENCLAW_VERSION ARG default so CI rebuilds # the base image on push to main, or use workflow_dispatch on base-image.yaml # with the openclaw_version input for a one-off build without editing this file. -ARG OPENCLAW_VERSION=2026.9.1 -ARG OPENCLAW_2026_9_1_INTEGRITY=sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w== -ARG OPENCLAW_2026_9_1_TARBALL=https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz +ARG OPENCLAW_VERSION=2026.9.2 +ARG OPENCLAW_2026_9_2_INTEGRITY=sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg== +ARG OPENCLAW_2026_9_2_TARBALL=https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz # E2E-only legacy fixture pins used by stale-sandbox/rebuild tests that # intentionally build an older OpenClaw base image before proving upgrade # behavior. Production workflows reject the fixture flag, both legacy version @@ -506,7 +507,7 @@ RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/bluep fi; \ EXPECTED_INTEGRITY=""; \ EXPECTED_TARBALL=""; \ - if [ "$OPENCLAW_VERSION" = "2026.9.1" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_9_1_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_9_1_TARBALL"; fi; \ + if [ "$OPENCLAW_VERSION" = "2026.9.2" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_9_2_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_9_2_TARBALL"; fi; \ if [ "$OPENCLAW_VERSION" = "2026.3.11" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_3_11_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_3_11_TARBALL"; fi; \ if [ "$OPENCLAW_VERSION" = "2026.4.24" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_4_24_INTEGRITY"; EXPECTED_TARBALL="$OPENCLAW_2026_4_24_TARBALL"; fi; \ if [ -z "$EXPECTED_INTEGRITY" ]; then \ @@ -514,8 +515,8 @@ RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/bluep fi; \ OPENCLAW_LOCK_SHA256=none-legacy-fixture; \ OPENCLAW_RECIPE='ignore-scripts+reviewed-lifecycle-v1'; \ - if [ "$OPENCLAW_VERSION" = "2026.9.1" ]; then \ - OPENCLAW_LOCK_SHA256=71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113; \ + if [ "$OPENCLAW_VERSION" = "2026.9.2" ]; then \ + OPENCLAW_LOCK_SHA256=cbcfdd15430b81f50ada9f39858a694815e570c8bb3e6b4bb9f1c0b53bf0ef4a; \ ACTUAL_OPENCLAW_LOCK_SHA256="$(sha256sum /usr/local/lib/nemoclaw/openclaw-runtime/package-lock.json | awk '{print $1}')"; \ [ "$ACTUAL_OPENCLAW_LOCK_SHA256" = "$OPENCLAW_LOCK_SHA256" ] \ || { echo "Error: OpenClaw lock SHA-256 mismatch (expected $OPENCLAW_LOCK_SHA256, found $ACTUAL_OPENCLAW_LOCK_SHA256)"; exit 1; }; \ diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index 0d95ed0a452..17a4df37a13 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -1460,7 +1460,7 @@ RUN set -eu; \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=$python_fix_version" \ "perl-base=5.44.0-1nemoclaw1" \ "perl=5.44.0-1nemoclaw1" \ @@ -1472,7 +1472,7 @@ RUN set -eu; \ test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2"; \ - test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2"; \ + test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3"; \ test "$(dpkg-query -W -f='${Version}' perl-base)" = "5.44.0-1nemoclaw1"; \ test "$(dpkg-query -W -f='${Version}' perl)" = "5.44.0-1nemoclaw1"; \ test "$(perl -e 'print $^V')" = "v5.44.0"; \ diff --git a/agents/hermes/Dockerfile.base b/agents/hermes/Dockerfile.base index d619e91e7a8..769e800dc9c 100644 --- a/agents/hermes/Dockerfile.base +++ b/agents/hermes/Dockerfile.base @@ -31,7 +31,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates=20250419 \ curl=8.14.1-2+deb13u5 \ git=1:2.47.3-0+deb13u1 \ - libssl-dev=3.5.7-1~deb13u2 \ + libssl-dev=3.5.7-1~deb13u3 \ + libssl3t64=3.5.7-1~deb13u3 \ openssh-server=1:10.0p1-7+deb13u4 \ xz-utils=5.8.1-1+deb13u1 \ zlib1g-dev=1:1.3.dfsg+really1.3.1-1+b1 \ @@ -100,7 +101,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ iptables=1.8.11-2 \ nftables=1.1.3-1 \ libcap2-bin=1:2.75-10+deb13u1+b3 \ - libssl3t64=3.5.7-1~deb13u2 \ + libssl3t64=3.5.7-1~deb13u3 \ util-linux=2.41-5 \ procps=2:4.0.4-9 \ e2fsprogs=1.47.2-3+b12 \ @@ -184,7 +185,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2" \ - && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2" \ + && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3" \ && test "$(dpkg-query -W -f='${Version}' nemoclaw-python3.13-htmlparser-fix)" = "3.13.5-2+deb13u5+nemoclaw1" \ && test "$(jq --version)" = "jq-1.8.2" \ && ldd /usr/bin/jq | grep -Eq 'libonig[.]so[.]5' \ @@ -219,7 +220,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" \ "perl-base=5.44.0-1nemoclaw1" \ "perl=5.44.0-1nemoclaw1" \ diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index 589ef8673f1..f398a388fea 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -376,7 +376,7 @@ RUN set -eu; \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" \ "perl-base=5.44.0-1nemoclaw1" \ "perl=5.44.0-1nemoclaw1" \ @@ -388,7 +388,7 @@ RUN set -eu; \ test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2"; \ - test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2"; \ + test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3"; \ test "$(dpkg-query -W -f='${Version}' nemoclaw-python3.13-htmlparser-fix)" = "3.13.5-2+deb13u5+nemoclaw1"; \ test "$(dpkg-query -W -f='${Version}' perl-base)" = "5.44.0-1nemoclaw1"; \ test "$(dpkg-query -W -f='${Version}' perl)" = "5.44.0-1nemoclaw1"; \ diff --git a/agents/langchain-deepagents-code/Dockerfile.base b/agents/langchain-deepagents-code/Dockerfile.base index a0cadba0bbf..c643701d4ad 100644 --- a/agents/langchain-deepagents-code/Dockerfile.base +++ b/agents/langchain-deepagents-code/Dockerfile.base @@ -23,7 +23,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates=20250419 \ curl=8.14.1-2+deb13u5 \ git=1:2.47.3-0+deb13u1 \ - libssl-dev=3.5.7-1~deb13u2 \ + libssl-dev=3.5.7-1~deb13u3 \ + libssl3t64=3.5.7-1~deb13u3 \ openssh-server=1:10.0p1-7+deb13u4 \ xz-utils=5.8.1-1+deb13u1 \ zlib1g-dev=1:1.3.dfsg+really1.3.1-1+b1 \ @@ -97,7 +98,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ iptables=1.8.11-2 \ nftables=1.1.3-1 \ libcap2-bin=1:2.75-10+deb13u1+b3 \ - libssl3t64=3.5.7-1~deb13u2 \ + libssl3t64=3.5.7-1~deb13u3 \ util-linux=2.41-5 \ procps=2:4.0.4-9 \ e2fsprogs=1.47.2-3+b12 \ @@ -193,7 +194,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2" \ - && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2" \ + && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3" \ && test "$(dpkg-query -W -f='${Version}' nemoclaw-python3.13-htmlparser-fix)" = "3.13.5-2+deb13u5+nemoclaw1" \ && test "$(jq --version)" = "jq-1.8.2" \ && ldd /usr/bin/jq | grep -Eq 'libonig[.]so[.]5' \ @@ -228,7 +229,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" \ "perl-base=5.44.0-1nemoclaw1" \ "perl=5.44.0-1nemoclaw1" \ diff --git a/agents/openclaw/managed-image-messaging-runtime/package-lock.json b/agents/openclaw/managed-image-messaging-runtime/package-lock.json index 0e2f60d3883..7a2b806c1d2 100644 --- a/agents/openclaw/managed-image-messaging-runtime/package-lock.json +++ b/agents/openclaw/managed-image-messaging-runtime/package-lock.json @@ -10,12 +10,12 @@ "license": "Apache-2.0", "dependencies": { "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@openclaw/discord": "2026.9.1", - "@openclaw/googlechat": "2026.9.1", - "@openclaw/msteams": "2026.9.1", - "@openclaw/slack": "2026.9.1", - "@openclaw/whatsapp": "2026.9.1", + "@emnapi/runtime": "1.11.3", + "@openclaw/discord": "2026.9.2", + "@openclaw/googlechat": "2026.9.2", + "@openclaw/msteams": "2026.9.2", + "@openclaw/slack": "2026.9.2", + "@openclaw/whatsapp": "2026.9.2", "@tencent-weixin/openclaw-weixin": "2.4.9", "agent-base": "6.0.2", "axios": "1.19.0", @@ -24,9 +24,8 @@ "nemoclaw-openclaw-override-ip-address": "npm:ip-address@10.5.0", "nemoclaw-openclaw-override-protobufjs": "npm:protobufjs@8.7.2", "nemoclaw-openclaw-override-qs": "npm:qs@6.15.3", - "nemoclaw-openclaw-override-undici7": "npm:undici@7.29.1", "nemoclaw-openclaw-override-uuid": "npm:uuid@14.0.2", - "openclaw": "2026.9.1", + "openclaw": "2026.9.2", "undici": "8.10.2" }, "engines": { @@ -287,9 +286,9 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.84.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz", - "integrity": "sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==", + "version": "0.84.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.3.tgz", + "integrity": "sha512-fS6OEQKEEALnKa6Uw8LcgZZ+9CWck7f3MQSCETQp6leUgIFwMEDtKmOUnL9nsYm+RIPmy7OmplVxYRbV6hiaFg==", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -310,9 +309,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "dependencies": { "tslib": "^2.4.0" @@ -403,9 +402,9 @@ } }, "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz", + "integrity": "sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==", "license": "MIT", "engines": { "node": ">=20" @@ -414,6 +413,581 @@ "hono": "^4" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -925,9 +1499,9 @@ } }, "node_modules/@modelcontextprotocol/sdk/node_modules/jose": { - "version": "6.2.11", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.11.tgz", - "integrity": "sha512-A5NPn7g8EAzGU3IzRs+Yiq8K5n3ypYS75M5+KKiVHdUexfpWK1kP4ZMq7QnTGDoMj6TJ1dtcEJjW60yZDXS4hg==", + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -943,9 +1517,9 @@ } }, "node_modules/@openclaw/ai": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/@openclaw/ai/-/ai-2026.9.1.tgz", - "integrity": "sha512-Am/7miiZZjbXv0MxzmOsaCAB9YXlz3vHl1Nuq1j1rOWj+7MwO83P5rnXdk10vc3jXxaXq2snlDl/La8xkd2IeA==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/@openclaw/ai/-/ai-2026.9.2.tgz", + "integrity": "sha512-VsRzawylkkKTvzKgVM3XrRSe6LVqKM2t8M25TfiK114MB3lRRDqVwE8eGZ4mF+w3IKPwPDqvpzir0ipiP0EVCQ==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.120.0", @@ -953,16 +1527,16 @@ "@mistralai/mistralai": "2.6.4", "openai": "7.5.0", "partial-json": "0.1.7", - "typebox": "1.3.17" + "typebox": "1.3.18" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@openclaw/discord": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.1.tgz", - "integrity": "sha512-qNmN2a8A9dET4igPp0RML171sEn8PDMyNCYNp/DqcJ4tn3XTHpacSOTkqBmv5yXTycJRC9rfFP8FT/SdW0Rldg==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.2.tgz", + "integrity": "sha512-j+fSHxbXA+DSxwbL8SvtsDNL6tvBX4+RmH+EerVW6dCbeIwSconXj6J/+AaN/mhzZI4g0jPPj30TUhdCRRbc2w==", "bundleDependencies": [ "@discord/embedded-app-sdk", "@discordjs/voice", @@ -980,13 +1554,13 @@ "discord-api-types": "0.38.53", "libopus-wasm": "0.2.0", "mdast-util-from-markdown": "2.0.3", - "typebox": "1.3.17", - "undici": "8.10.0", + "typebox": "1.3.18", + "undici": "8.10.2", "ws": "8.21.3", "zod": "4.4.3" }, "peerDependencies": { - "openclaw": ">=2026.9.1" + "openclaw": ">=2026.9.2" }, "peerDependenciesMeta": { "openclaw": { @@ -1153,7 +1727,7 @@ } }, "node_modules/@openclaw/discord/node_modules/@discordjs/voice/node_modules/@types/node": { - "version": "26.2.0", + "version": "26.3.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -2018,12 +2592,12 @@ "optional": true }, "node_modules/@openclaw/discord/node_modules/typebox": { - "version": "1.3.17", + "version": "1.3.18", "inBundle": true, "license": "MIT" }, "node_modules/@openclaw/discord/node_modules/undici": { - "version": "8.10.0", + "version": "8.10.2", "inBundle": true, "license": "MIT", "engines": { @@ -2059,29 +2633,29 @@ } }, "node_modules/@openclaw/fs-safe": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.7.0.tgz", - "integrity": "sha512-i+0a4yQYa4ThUahaSLMledG99AL38XdAn8q/ppVMr49zr0dsOO+1gf8Jz6jgI0ggdcq6U4IhgsNunACsSymjyA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.8.1.tgz", + "integrity": "sha512-I11v+xiet4RCE1G1bmWFLEMmd9nBnZMmKqHPT9gkqVtoqacY7GtFvpt1O7cffUDD6C2YlAt7Z5Z2Vlbq5rYxDg==", "license": "MIT", "engines": { "node": ">=22" }, "optionalDependencies": { - "@openclaw/fs-safe-darwin-arm64": "0.7.0", - "@openclaw/fs-safe-darwin-x64": "0.7.0", - "@openclaw/fs-safe-linux-arm64-gnu": "0.7.0", - "@openclaw/fs-safe-linux-arm64-musl": "0.7.0", - "@openclaw/fs-safe-linux-x64-gnu": "0.7.0", - "@openclaw/fs-safe-linux-x64-musl": "0.7.0", - "@openclaw/fs-safe-win32-x64-msvc": "0.7.0", + "@openclaw/fs-safe-darwin-arm64": "0.8.1", + "@openclaw/fs-safe-darwin-x64": "0.8.1", + "@openclaw/fs-safe-linux-arm64-gnu": "0.8.1", + "@openclaw/fs-safe-linux-arm64-musl": "0.8.1", + "@openclaw/fs-safe-linux-x64-gnu": "0.8.1", + "@openclaw/fs-safe-linux-x64-musl": "0.8.1", + "@openclaw/fs-safe-win32-x64-msvc": "0.8.1", "jszip": "^3.10.1", "tar": "7.5.22" } }, "node_modules/@openclaw/fs-safe-darwin-arm64": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-arm64/-/fs-safe-darwin-arm64-0.7.0.tgz", - "integrity": "sha512-11z1Tv1ZVa31M+aoC56EX/DUACVuq4GiYSOpbv/ZDqwFPCdy+N/Vi2x/hcwCLjhb6UOsMWRsMsIAMjQAR2qHmA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-arm64/-/fs-safe-darwin-arm64-0.8.1.tgz", + "integrity": "sha512-fCXsPrEmqkKBEDG2nHndsbUrlxXVHT9VAT/oLCCqkfNaiJxQ5POS6YexUoshDfpqWYWL7ggWAZ+kiLukffk/fQ==", "cpu": [ "arm64" ], @@ -2095,9 +2669,9 @@ } }, "node_modules/@openclaw/fs-safe-darwin-x64": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-x64/-/fs-safe-darwin-x64-0.7.0.tgz", - "integrity": "sha512-LS89eU+x7Kq0kugOR1HLsdVcuTNwCeMU+UQoWZUsWbWg9XTtw5ohOTU7bqewPNVGmB0cdihPfdorKc5wZgLTVw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-x64/-/fs-safe-darwin-x64-0.8.1.tgz", + "integrity": "sha512-ZnYE9v7HYTBOwY1HZLK1epQLt6Qk///BK2OvTHdWtPAB/TlTm5djFE3RQqNF/DDvO5HaKpgNFYjzKGel/peaGg==", "cpu": [ "x64" ], @@ -2111,9 +2685,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-arm64-gnu": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-gnu/-/fs-safe-linux-arm64-gnu-0.7.0.tgz", - "integrity": "sha512-3UZDqtYJSMMN4Rid68EuTmCYjHyGHilQ7BFfm+ZKGloJoJLEvXoaAbDAqsazvWjrQKecfxlXJTC9qbLa5RYJEg==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-gnu/-/fs-safe-linux-arm64-gnu-0.8.1.tgz", + "integrity": "sha512-JHvbIVkK7Mq/43WLYBkF+xn8YpYV3rP55KBDpKAN0MYjDYF/YRQVruzWOVOatiZdOWep48/wicysi9eqY94QRA==", "cpu": [ "arm64" ], @@ -2130,9 +2704,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-arm64-musl": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-musl/-/fs-safe-linux-arm64-musl-0.7.0.tgz", - "integrity": "sha512-rX1m2ft84KOb61/EhJmkiC0hqH4QCd1knQFcDU8tiA49sbiXsghNfSXRd2K07GlA7spXtqQfSX3ZvMh4f7JhBw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-musl/-/fs-safe-linux-arm64-musl-0.8.1.tgz", + "integrity": "sha512-DEsbhMNSDGVoksXnXXrvjkSz+4gE+5njFVU/kwtQffHWOsT0qv4xS+uUyqiGumiQL2iYYDpKARfXyNNX1bDFkA==", "cpu": [ "arm64" ], @@ -2149,9 +2723,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-x64-gnu": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-gnu/-/fs-safe-linux-x64-gnu-0.7.0.tgz", - "integrity": "sha512-lTn5h0lkJjL1yiOxh+kgY2L4JDAXo2fNHXXZqxKACQdhB1+YFcc8YyUPTjK2b8aWlsgpRwfq/7JJ1eDHRbDcrg==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-gnu/-/fs-safe-linux-x64-gnu-0.8.1.tgz", + "integrity": "sha512-oyduwu1ZjU2DcGxnGatUhpMa/uetv+CbYGxlqP67vZyXDvutXoTSrl0srJZ6mnzx6ENtROT+z4v+r213Y2jakA==", "cpu": [ "x64" ], @@ -2168,9 +2742,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-x64-musl": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-musl/-/fs-safe-linux-x64-musl-0.7.0.tgz", - "integrity": "sha512-e/LcF75zEQzg9yII5jv7WBZ3w7dNGBVpiOxlbm1dpy5XX+ap8pjsqWC0c0UGJdIbwWQInzKpb4/B0mwED/obYw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-musl/-/fs-safe-linux-x64-musl-0.8.1.tgz", + "integrity": "sha512-bsR9XhMzY/vi4ejv3s8A0titbsDcsfIQnIS5SxgfRmA+fEUcDqcgJyvkbDKG9YbGoyxVtoFPEflB5loZk82xTg==", "cpu": [ "x64" ], @@ -2187,9 +2761,9 @@ } }, "node_modules/@openclaw/fs-safe-win32-x64-msvc": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-win32-x64-msvc/-/fs-safe-win32-x64-msvc-0.7.0.tgz", - "integrity": "sha512-FiVNgckcHBuRov6vRytRjx8AdRBPxl56qZ8Dr0aAaAxfo+yhSJCGPmhnpgqUirBxkKQyrABIrP3LdIyscJMgWA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-win32-x64-msvc/-/fs-safe-win32-x64-msvc-0.8.1.tgz", + "integrity": "sha512-rOkDKnLx61xvio+slHc8kG0IzpVVxt6JIJu9Q+1CrBmKSyf9YPZYqVJcjXReqA+J/c4vvVLHp5sbwQerOAqT/w==", "cpu": [ "x64" ], @@ -2203,9 +2777,9 @@ } }, "node_modules/@openclaw/googlechat": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.1.tgz", - "integrity": "sha512-Q5VTAJpfcrI7BSEw5Ugq3wf7JEg5QhTBwpi+BByGbfZsTTVjwZc7OIvNbKsVTh16I5/EWqHEnD+0WNeHqsteqw==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.2.tgz", + "integrity": "sha512-LUO8Lg07IhzJfEzxn+GSij8WMS/uX3hTmv0SydTy/0fKSN7iZksf74TaZg50kOoBi8FzeqeXo/zoGVuk0Mj1Ig==", "bundleDependencies": [ "google-auth-library", "zod" @@ -2215,7 +2789,7 @@ "zod": "4.4.3" }, "peerDependencies": { - "openclaw": ">=2026.9.1" + "openclaw": ">=2026.9.2" }, "peerDependenciesMeta": { "openclaw": { @@ -2489,19 +3063,19 @@ } }, "node_modules/@openclaw/msteams": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.1.tgz", - "integrity": "sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.2.tgz", + "integrity": "sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og==", "dependencies": { "@azure/identity": "4.13.2", "@microsoft/teams.api": "2.0.15", "@microsoft/teams.apps": "2.0.15", "express": "5.2.1", - "typebox": "1.3.17", + "typebox": "1.3.18", "zod": "4.4.3" }, "peerDependencies": { - "openclaw": ">=2026.9.1" + "openclaw": ">=2026.9.2" }, "peerDependenciesMeta": { "openclaw": { @@ -2522,9 +3096,9 @@ } }, "node_modules/@openclaw/slack": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.1.tgz", - "integrity": "sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.2.tgz", + "integrity": "sha512-6M1M6gL3iXahpalNsYAUuA+wvnV8lbMlNNH2ToegFvaSJcIll4S9kFa5mv3GFoquhMRHQjEjnkXPHP/pXwaWcA==", "bundleDependencies": [ "@slack/bolt", "@slack/socket-mode", @@ -2542,13 +3116,13 @@ "@slack/types": "3.0.0", "@slack/web-api": "8.0.0", "get-east-asian-width": "1.6.0", - "typebox": "1.3.17", - "undici": "7.29.0", + "typebox": "1.3.18", + "undici": "7.29.1", "ws": "8.21.3", "zod": "4.4.3" }, "peerDependencies": { - "openclaw": ">=2026.9.1" + "openclaw": ">=2026.9.2" }, "peerDependenciesMeta": { "openclaw": { @@ -3744,12 +4318,12 @@ } }, "node_modules/@openclaw/slack/node_modules/typebox": { - "version": "1.3.17", + "version": "1.3.18", "inBundle": true, "license": "MIT" }, "node_modules/@openclaw/slack/node_modules/undici": { - "version": "7.29.0", + "version": "7.29.1", "inBundle": true, "license": "MIT", "engines": { @@ -3785,9 +4359,9 @@ } }, "node_modules/@openclaw/whatsapp": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.1.tgz", - "integrity": "sha512-llIcoMa6FM4SgYn7GG1FQIeTTA5JDdcHW5D7PT+3aGYT3/E2eLFutKwDvD/w7G0hvDwSftzZgLi3iA8dzK7a3A==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.2.tgz", + "integrity": "sha512-vOWQIk7FpLHrhMmO+FaLi+pnFB82hiWNJJFJONkBuofERh2SMEz7EMut/vECFFEjFnmOZSVlYfRlxhbNkd/R6g==", "bundleDependencies": [ "audio-decode", "baileys", @@ -3796,10 +4370,10 @@ "dependencies": { "audio-decode": "2.2.3", "baileys": "7.0.0-rc14", - "typebox": "1.3.17" + "typebox": "1.3.18" }, "peerDependencies": { - "openclaw": ">=2026.9.1" + "openclaw": ">=2026.9.2" }, "peerDependenciesMeta": { "openclaw": { @@ -4575,7 +5149,7 @@ } }, "node_modules/@openclaw/whatsapp/node_modules/typebox": { - "version": "1.3.17", + "version": "1.3.18", "inBundle": true, "license": "MIT" }, @@ -4816,6 +5390,59 @@ "win32" ] }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT", + "peer": true + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", @@ -4841,12 +5468,47 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT", + "peer": true + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", @@ -5283,9 +5945,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -5697,6 +6359,16 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", @@ -6093,9 +6765,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -6528,9 +7200,9 @@ } }, "node_modules/hono": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", - "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", + "version": "4.13.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.12.tgz", + "integrity": "sha512-6E2QDAc9Ick9Sq77ZrGS/dk2WUYni91aufTw6LJKpV7w8kW5/GxVUc650FOADOmlwg3K+f7Pun6XlV+pYmW6gw==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -6792,9 +7464,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "license": "MIT", "engines": { "node": ">= 12" @@ -7436,16 +8108,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/nemoclaw-openclaw-override-undici7": { - "name": "undici", - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", - "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/nemoclaw-openclaw-override-uuid": { "name": "uuid", "version": "14.0.2", @@ -7692,9 +8354,9 @@ } }, "node_modules/openclaw": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz", - "integrity": "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz", + "integrity": "sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -7702,7 +8364,7 @@ "@anthropic-ai/sdk": "0.120.0", "@clack/core": "1.4.3", "@clack/prompts": "1.7.0", - "@earendil-works/pi-tui": "0.84.2", + "@earendil-works/pi-tui": "0.84.3", "@google/genai": "2.18.0", "@grammyjs/runner": "2.0.3", "@grammyjs/transformer-throttler": "1.2.1", @@ -7711,8 +8373,8 @@ "@mistralai/mistralai": "2.6.4", "@modelcontextprotocol/sdk": "1.30.0", "@mozilla/readability": "0.6.0", - "@openclaw/ai": "2026.9.1", - "@openclaw/fs-safe": "0.7.0", + "@openclaw/ai": "2026.9.2", + "@openclaw/fs-safe": "0.8.1", "@openclaw/proxyline": "0.3.7", "@silvia-odwyer/photon-node": "0.3.4", "@trycua/cua-driver": "0.22.0", @@ -7755,7 +8417,7 @@ "tar": "7.5.22", "tree-sitter-bash": "0.25.1", "tslog": "4.11.0", - "typebox": "1.3.17", + "typebox": "1.3.18", "typescript": "6.0.3", "undici": "8.10.2", "web-push": "3.6.7", @@ -8387,6 +9049,56 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8842,9 +9554,9 @@ } }, "node_modules/typebox": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.17.tgz", - "integrity": "sha512-20PsSaZV1pN7pIfM/YEUHZNTv8X21+1ilPo/HN+6GtFbhCaQhLrIoKCkAkcBwIva3nYI+Ao0MxM1iDj5H3SOhw==", + "version": "1.3.18", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.18.tgz", + "integrity": "sha512-/wYPoDqxWZSxV/XD8Eskzr3YluXC9CaWJOuUYMkj+lLVLkyeEIQKzHvMuS/IRc3OLTIBC32LAtHgXo/WFEOMHQ==", "license": "MIT" }, "node_modules/typescript": { diff --git a/agents/openclaw/managed-image-messaging-runtime/package.json b/agents/openclaw/managed-image-messaging-runtime/package.json index f3c37022a58..ab79210c2f5 100644 --- a/agents/openclaw/managed-image-messaging-runtime/package.json +++ b/agents/openclaw/managed-image-messaging-runtime/package.json @@ -7,12 +7,12 @@ "license": "Apache-2.0", "dependencies": { "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@openclaw/discord": "2026.9.1", - "@openclaw/googlechat": "2026.9.1", - "@openclaw/msteams": "2026.9.1", - "@openclaw/slack": "2026.9.1", - "@openclaw/whatsapp": "2026.9.1", + "@emnapi/runtime": "1.11.3", + "@openclaw/discord": "2026.9.2", + "@openclaw/googlechat": "2026.9.2", + "@openclaw/msteams": "2026.9.2", + "@openclaw/slack": "2026.9.2", + "@openclaw/whatsapp": "2026.9.2", "@tencent-weixin/openclaw-weixin": "2.4.9", "agent-base": "6.0.2", "axios": "1.19.0", @@ -21,26 +21,24 @@ "nemoclaw-openclaw-override-ip-address": "npm:ip-address@10.5.0", "nemoclaw-openclaw-override-protobufjs": "npm:protobufjs@8.7.2", "nemoclaw-openclaw-override-qs": "npm:qs@6.15.3", - "nemoclaw-openclaw-override-undici7": "npm:undici@7.29.1", "nemoclaw-openclaw-override-uuid": "npm:uuid@14.0.2", - "openclaw": "2026.9.1", + "openclaw": "2026.9.2", "undici": "8.10.2" }, "engines": { "node": ">=22.19.0" }, "overrides": { - "@openclaw/discord@2026.9.1": { + "@openclaw/discord@2026.9.2": { "@discord/embedded-app-sdk@2.5.0": { "uuid": "14.0.2" } }, - "@openclaw/whatsapp@2026.9.1": { + "@openclaw/whatsapp@2026.9.2": { "baileys@7.0.0-rc14": { "file-type": "22.0.2", "protobufjs": "8.7.2" } - }, - "undici@8.10.0": "8.10.2" + } } } diff --git a/agents/openclaw/manifest.yaml b/agents/openclaw/manifest.yaml index fb3ba260fe1..334f2ac24c2 100644 --- a/agents/openclaw/manifest.yaml +++ b/agents/openclaw/manifest.yaml @@ -19,7 +19,7 @@ homepage: "https://openclaw.ai" install_method: npm # npm install -g openclaw@ binary_path: /usr/local/bin/openclaw version_command: "openclaw --version" -expected_version: "2026.9.1" +expected_version: "2026.9.2" version_scheme: calendar gateway_command: "openclaw gateway run" # Interactive entry point a user types inside the sandbox. OpenClaw stays a diff --git a/agents/openclaw/openclaw-runtime/package-lock.json b/agents/openclaw/openclaw-runtime/package-lock.json index 76b3d67330f..0bbb5a8c674 100644 --- a/agents/openclaw/openclaw-runtime/package-lock.json +++ b/agents/openclaw/openclaw-runtime/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { - "openclaw": "2026.9.1" + "openclaw": "2026.9.2" }, "engines": { "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0" @@ -93,9 +93,9 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.84.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz", - "integrity": "sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==", + "version": "0.84.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.3.tgz", + "integrity": "sha512-fS6OEQKEEALnKa6Uw8LcgZZ+9CWck7f3MQSCETQp6leUgIFwMEDtKmOUnL9nsYm+RIPmy7OmplVxYRbV6hiaFg==", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -151,9 +151,9 @@ } }, "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz", + "integrity": "sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==", "license": "MIT", "engines": { "node": ">=20" @@ -579,9 +579,9 @@ } }, "node_modules/@openclaw/ai": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/@openclaw/ai/-/ai-2026.9.1.tgz", - "integrity": "sha512-Am/7miiZZjbXv0MxzmOsaCAB9YXlz3vHl1Nuq1j1rOWj+7MwO83P5rnXdk10vc3jXxaXq2snlDl/La8xkd2IeA==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/@openclaw/ai/-/ai-2026.9.2.tgz", + "integrity": "sha512-VsRzawylkkKTvzKgVM3XrRSe6LVqKM2t8M25TfiK114MB3lRRDqVwE8eGZ4mF+w3IKPwPDqvpzir0ipiP0EVCQ==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.120.0", @@ -589,36 +589,36 @@ "@mistralai/mistralai": "2.6.4", "openai": "7.5.0", "partial-json": "0.1.7", - "typebox": "1.3.17" + "typebox": "1.3.18" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@openclaw/fs-safe": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.7.0.tgz", - "integrity": "sha512-i+0a4yQYa4ThUahaSLMledG99AL38XdAn8q/ppVMr49zr0dsOO+1gf8Jz6jgI0ggdcq6U4IhgsNunACsSymjyA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.8.1.tgz", + "integrity": "sha512-I11v+xiet4RCE1G1bmWFLEMmd9nBnZMmKqHPT9gkqVtoqacY7GtFvpt1O7cffUDD6C2YlAt7Z5Z2Vlbq5rYxDg==", "license": "MIT", "engines": { "node": ">=22" }, "optionalDependencies": { - "@openclaw/fs-safe-darwin-arm64": "0.7.0", - "@openclaw/fs-safe-darwin-x64": "0.7.0", - "@openclaw/fs-safe-linux-arm64-gnu": "0.7.0", - "@openclaw/fs-safe-linux-arm64-musl": "0.7.0", - "@openclaw/fs-safe-linux-x64-gnu": "0.7.0", - "@openclaw/fs-safe-linux-x64-musl": "0.7.0", - "@openclaw/fs-safe-win32-x64-msvc": "0.7.0", + "@openclaw/fs-safe-darwin-arm64": "0.8.1", + "@openclaw/fs-safe-darwin-x64": "0.8.1", + "@openclaw/fs-safe-linux-arm64-gnu": "0.8.1", + "@openclaw/fs-safe-linux-arm64-musl": "0.8.1", + "@openclaw/fs-safe-linux-x64-gnu": "0.8.1", + "@openclaw/fs-safe-linux-x64-musl": "0.8.1", + "@openclaw/fs-safe-win32-x64-msvc": "0.8.1", "jszip": "^3.10.1", "tar": "7.5.22" } }, "node_modules/@openclaw/fs-safe-darwin-arm64": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-arm64/-/fs-safe-darwin-arm64-0.7.0.tgz", - "integrity": "sha512-11z1Tv1ZVa31M+aoC56EX/DUACVuq4GiYSOpbv/ZDqwFPCdy+N/Vi2x/hcwCLjhb6UOsMWRsMsIAMjQAR2qHmA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-arm64/-/fs-safe-darwin-arm64-0.8.1.tgz", + "integrity": "sha512-fCXsPrEmqkKBEDG2nHndsbUrlxXVHT9VAT/oLCCqkfNaiJxQ5POS6YexUoshDfpqWYWL7ggWAZ+kiLukffk/fQ==", "cpu": [ "arm64" ], @@ -632,9 +632,9 @@ } }, "node_modules/@openclaw/fs-safe-darwin-x64": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-x64/-/fs-safe-darwin-x64-0.7.0.tgz", - "integrity": "sha512-LS89eU+x7Kq0kugOR1HLsdVcuTNwCeMU+UQoWZUsWbWg9XTtw5ohOTU7bqewPNVGmB0cdihPfdorKc5wZgLTVw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-x64/-/fs-safe-darwin-x64-0.8.1.tgz", + "integrity": "sha512-ZnYE9v7HYTBOwY1HZLK1epQLt6Qk///BK2OvTHdWtPAB/TlTm5djFE3RQqNF/DDvO5HaKpgNFYjzKGel/peaGg==", "cpu": [ "x64" ], @@ -648,9 +648,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-arm64-gnu": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-gnu/-/fs-safe-linux-arm64-gnu-0.7.0.tgz", - "integrity": "sha512-3UZDqtYJSMMN4Rid68EuTmCYjHyGHilQ7BFfm+ZKGloJoJLEvXoaAbDAqsazvWjrQKecfxlXJTC9qbLa5RYJEg==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-gnu/-/fs-safe-linux-arm64-gnu-0.8.1.tgz", + "integrity": "sha512-JHvbIVkK7Mq/43WLYBkF+xn8YpYV3rP55KBDpKAN0MYjDYF/YRQVruzWOVOatiZdOWep48/wicysi9eqY94QRA==", "cpu": [ "arm64" ], @@ -667,9 +667,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-arm64-musl": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-musl/-/fs-safe-linux-arm64-musl-0.7.0.tgz", - "integrity": "sha512-rX1m2ft84KOb61/EhJmkiC0hqH4QCd1knQFcDU8tiA49sbiXsghNfSXRd2K07GlA7spXtqQfSX3ZvMh4f7JhBw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-musl/-/fs-safe-linux-arm64-musl-0.8.1.tgz", + "integrity": "sha512-DEsbhMNSDGVoksXnXXrvjkSz+4gE+5njFVU/kwtQffHWOsT0qv4xS+uUyqiGumiQL2iYYDpKARfXyNNX1bDFkA==", "cpu": [ "arm64" ], @@ -686,9 +686,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-x64-gnu": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-gnu/-/fs-safe-linux-x64-gnu-0.7.0.tgz", - "integrity": "sha512-lTn5h0lkJjL1yiOxh+kgY2L4JDAXo2fNHXXZqxKACQdhB1+YFcc8YyUPTjK2b8aWlsgpRwfq/7JJ1eDHRbDcrg==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-gnu/-/fs-safe-linux-x64-gnu-0.8.1.tgz", + "integrity": "sha512-oyduwu1ZjU2DcGxnGatUhpMa/uetv+CbYGxlqP67vZyXDvutXoTSrl0srJZ6mnzx6ENtROT+z4v+r213Y2jakA==", "cpu": [ "x64" ], @@ -705,9 +705,9 @@ } }, "node_modules/@openclaw/fs-safe-linux-x64-musl": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-musl/-/fs-safe-linux-x64-musl-0.7.0.tgz", - "integrity": "sha512-e/LcF75zEQzg9yII5jv7WBZ3w7dNGBVpiOxlbm1dpy5XX+ap8pjsqWC0c0UGJdIbwWQInzKpb4/B0mwED/obYw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-musl/-/fs-safe-linux-x64-musl-0.8.1.tgz", + "integrity": "sha512-bsR9XhMzY/vi4ejv3s8A0titbsDcsfIQnIS5SxgfRmA+fEUcDqcgJyvkbDKG9YbGoyxVtoFPEflB5loZk82xTg==", "cpu": [ "x64" ], @@ -724,9 +724,9 @@ } }, "node_modules/@openclaw/fs-safe-win32-x64-msvc": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-win32-x64-msvc/-/fs-safe-win32-x64-msvc-0.7.0.tgz", - "integrity": "sha512-FiVNgckcHBuRov6vRytRjx8AdRBPxl56qZ8Dr0aAaAxfo+yhSJCGPmhnpgqUirBxkKQyrABIrP3LdIyscJMgWA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-win32-x64-msvc/-/fs-safe-win32-x64-msvc-0.8.1.tgz", + "integrity": "sha512-rOkDKnLx61xvio+slHc8kG0IzpVVxt6JIJu9Q+1CrBmKSyf9YPZYqVJcjXReqA+J/c4vvVLHp5sbwQerOAqT/w==", "cpu": [ "x64" ], @@ -973,12 +973,12 @@ ] }, "node_modules/@types/node": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", - "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/retry": { @@ -1293,9 +1293,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2404,9 +2404,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "license": "MIT", "engines": { "node": ">= 12" @@ -2476,9 +2476,9 @@ "license": "ISC" }, "node_modules/jose": { - "version": "6.2.11", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.11.tgz", - "integrity": "sha512-A5NPn7g8EAzGU3IzRs+Yiq8K5n3ypYS75M5+KKiVHdUexfpWK1kP4ZMq7QnTGDoMj6TJ1dtcEJjW60yZDXS4hg==", + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -2926,9 +2926,9 @@ } }, "node_modules/openclaw": { - "version": "2026.9.1", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz", - "integrity": "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w==", + "version": "2026.9.2", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz", + "integrity": "sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2936,7 +2936,7 @@ "@anthropic-ai/sdk": "0.120.0", "@clack/core": "1.4.3", "@clack/prompts": "1.7.0", - "@earendil-works/pi-tui": "0.84.2", + "@earendil-works/pi-tui": "0.84.3", "@google/genai": "2.18.0", "@grammyjs/runner": "2.0.3", "@grammyjs/transformer-throttler": "1.2.1", @@ -2945,8 +2945,8 @@ "@mistralai/mistralai": "2.6.4", "@modelcontextprotocol/sdk": "1.30.0", "@mozilla/readability": "0.6.0", - "@openclaw/ai": "2026.9.1", - "@openclaw/fs-safe": "0.7.0", + "@openclaw/ai": "2026.9.2", + "@openclaw/fs-safe": "0.8.1", "@openclaw/proxyline": "0.3.7", "@silvia-odwyer/photon-node": "0.3.4", "@trycua/cua-driver": "0.22.0", @@ -2989,9 +2989,9 @@ "tar": "7.5.22", "tree-sitter-bash": "0.25.1", "tslog": "4.11.0", - "typebox": "1.3.17", + "typebox": "1.3.18", "typescript": "6.0.3", - "undici": "8.10.0", + "undici": "8.10.2", "web-push": "3.6.7", "web-tree-sitter": "0.26.13", "ws": "8.21.3", @@ -3917,9 +3917,9 @@ } }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -3927,6 +3927,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/qs": { @@ -4438,9 +4442,9 @@ } }, "node_modules/typebox": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.17.tgz", - "integrity": "sha512-20PsSaZV1pN7pIfM/YEUHZNTv8X21+1ilPo/HN+6GtFbhCaQhLrIoKCkAkcBwIva3nYI+Ao0MxM1iDj5H3SOhw==", + "version": "1.3.18", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.18.tgz", + "integrity": "sha512-/wYPoDqxWZSxV/XD8Eskzr3YluXC9CaWJOuUYMkj+lLVLkyeEIQKzHvMuS/IRc3OLTIBC32LAtHgXo/WFEOMHQ==", "license": "MIT" }, "node_modules/uhyphen": { @@ -4471,9 +4475,9 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "license": "MIT" }, "node_modules/unicorn-magic": { diff --git a/agents/openclaw/openclaw-runtime/package.json b/agents/openclaw/openclaw-runtime/package.json index ab61e7a02a2..4974ddfa0b0 100644 --- a/agents/openclaw/openclaw-runtime/package.json +++ b/agents/openclaw/openclaw-runtime/package.json @@ -6,13 +6,12 @@ "description": "Locked production dependency graph for the OpenClaw runtime", "license": "Apache-2.0", "dependencies": { - "openclaw": "2026.9.1" + "openclaw": "2026.9.2" }, "overrides": { "hono": "4.12.34", "tar": "7.5.21", - "fast-uri": "3.1.7", - "undici": "8.10.2" + "fast-uri": "3.1.7" }, "engines": { "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0" diff --git a/agents/pi/Dockerfile b/agents/pi/Dockerfile index 388a28c78c8..cdfb37e5b17 100644 --- a/agents/pi/Dockerfile +++ b/agents/pi/Dockerfile @@ -232,7 +232,7 @@ RUN set -eu; \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" \ "perl-base=5.44.0-1nemoclaw1" \ "perl=5.44.0-1nemoclaw1" \ @@ -244,7 +244,7 @@ RUN set -eu; \ test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1"; \ test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2"; \ - test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2"; \ + test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3"; \ test "$(dpkg-query -W -f='${Version}' nemoclaw-python3.13-htmlparser-fix)" = "3.13.5-2+deb13u5+nemoclaw1"; \ test "$(dpkg-query -W -f='${Version}' perl-base)" = "5.44.0-1nemoclaw1"; \ test "$(dpkg-query -W -f='${Version}' perl)" = "5.44.0-1nemoclaw1"; \ diff --git a/agents/pi/Dockerfile.base b/agents/pi/Dockerfile.base index 9888652e87f..c884a677977 100644 --- a/agents/pi/Dockerfile.base +++ b/agents/pi/Dockerfile.base @@ -27,7 +27,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates=20250419 \ curl=8.14.1-2+deb13u5 \ git=1:2.47.3-0+deb13u1 \ - libssl-dev=3.5.7-1~deb13u2 \ + libssl-dev=3.5.7-1~deb13u3 \ + libssl3t64=3.5.7-1~deb13u3 \ openssh-server=1:10.0p1-7+deb13u4 \ xz-utils=5.8.1-1+deb13u1 \ zlib1g-dev=1:1.3.dfsg+really1.3.1-1+b1 \ @@ -101,7 +102,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ iptables=1.8.11-2 \ nftables=1.1.3-1 \ libcap2-bin=1:2.75-10+deb13u1+b3 \ - libssl3t64=3.5.7-1~deb13u2 \ + libssl3t64=3.5.7-1~deb13u3 \ util-linux=2.41-5 \ procps=2:4.0.4-9 \ e2fsprogs=1.47.2-3+b12 \ @@ -197,7 +198,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0858-1" \ && test "$(dpkg-query -W -f='${Version}' libssh2-1t64)" = "1.11.1-1+deb13u1+nemoclaw2" \ - && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u2" \ + && test "$(dpkg-query -W -f='${Version}' libssl3t64)" = "3.5.7-1~deb13u3" \ && test "$(dpkg-query -W -f='${Version}' nemoclaw-python3.13-htmlparser-fix)" = "3.13.5-2+deb13u5+nemoclaw1" \ && test "$(jq --version)" = "jq-1.8.2" \ && ldd /usr/bin/jq | grep -Eq 'libonig[.]so[.]5' \ @@ -232,7 +233,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ "vim-common=2:9.2.0858-1" \ "vim-tiny=2:9.2.0858-1" \ "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" \ - "libssl3t64=3.5.7-1~deb13u2" \ + "libssl3t64=3.5.7-1~deb13u3" \ "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" \ "perl-base=5.44.0-1nemoclaw1" \ "perl=5.44.0-1nemoclaw1" \ diff --git a/ci/pi-agent-qualification-v1-linux-amd64.json b/ci/pi-agent-qualification-v1-linux-amd64.json index 703d0df19db..4e002501d24 100644 --- a/ci/pi-agent-qualification-v1-linux-amd64.json +++ b/ci/pi-agent-qualification-v1-linux-amd64.json @@ -3,13 +3,13 @@ "agent": "pi", "platform": "linux/amd64", "image": "ghcr.io/nvidia/nemoclaw/pi-sandbox", - "digest": "sha256:fb574463bf5c328f183eda582d9270259198574a0291358f00b6bb4d67e769f0", - "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:fb574463bf5c328f183eda582d9270259198574a0291358f00b6bb4d67e769f0", + "digest": "sha256:c2a61a16fb8960535d23ee9cc70fcc9ab34f444c80fbba94c26ed14326894c1a", + "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:c2a61a16fb8960535d23ee9cc70fcc9ab34f444c80fbba94c26ed14326894c1a", "source": { "repository": "NVIDIA/NemoClaw", - "revision": "b0af4ef64b8257372f2ad390ce6c843c96bbfebb", + "revision": "ddf20ef245ed59eca4fe507f3a999fc9c34bb925", "release": "v0.1.0", - "cohort": "ghrun-36649400053-1" + "cohort": "ghrun-36757441033-1" }, "startupProfileContractVersion": 1, "capabilityContractVersion": 1 diff --git a/ci/pi-agent-qualification-v1-linux-arm64.json b/ci/pi-agent-qualification-v1-linux-arm64.json index 0b9da42216d..a979ee6d5de 100644 --- a/ci/pi-agent-qualification-v1-linux-arm64.json +++ b/ci/pi-agent-qualification-v1-linux-arm64.json @@ -3,13 +3,13 @@ "agent": "pi", "platform": "linux/arm64", "image": "ghcr.io/nvidia/nemoclaw/pi-sandbox", - "digest": "sha256:4fc50eddab478f83af90e452be85fe1cffcf9c5a11f1ac54a5c2189a3e6136df", - "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:4fc50eddab478f83af90e452be85fe1cffcf9c5a11f1ac54a5c2189a3e6136df", + "digest": "sha256:46e4d6b96c5d74df873ca0a4db998e448734589dbf5aaa426d279f6387d02a59", + "reference": "ghcr.io/nvidia/nemoclaw/pi-sandbox@sha256:46e4d6b96c5d74df873ca0a4db998e448734589dbf5aaa426d279f6387d02a59", "source": { "repository": "NVIDIA/NemoClaw", - "revision": "b0af4ef64b8257372f2ad390ce6c843c96bbfebb", + "revision": "ddf20ef245ed59eca4fe507f3a999fc9c34bb925", "release": "v0.1.0", - "cohort": "ghrun-36649400053-1" + "cohort": "ghrun-36757441033-1" }, "startupProfileContractVersion": 1, "capabilityContractVersion": 1 diff --git a/ci/reviewed-npm-audit.json b/ci/reviewed-npm-audit.json index fe86da71421..bacfc66aaa6 100644 --- a/ci/reviewed-npm-audit.json +++ b/ci/reviewed-npm-audit.json @@ -39,10 +39,10 @@ "artifactDirectory": "coverage/reviewed-npm-audit", "archivePackages": [ { - "label": "OpenClaw 2026.9.1", - "packageSpec": "openclaw@2026.9.1", - "integrity": "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w==", - "tarballUrl": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz" + "label": "OpenClaw 2026.9.2", + "packageSpec": "openclaw@2026.9.2", + "integrity": "sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg==", + "tarballUrl": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz" }, { "label": "Codex ACP 0.11.1", @@ -51,40 +51,40 @@ "tarballUrl": "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.11.1.tgz" }, { - "label": "OpenClaw diagnostics OTEL 2026.9.1", - "packageSpec": "@openclaw/diagnostics-otel@2026.9.1", - "integrity": "sha512-3MWLli9L6HTVdrjqHmwOvNvIr6emsnuNQe4iE2sDqb8E5wn4Vq1rcsz+InL1YFudbStr089ZtS0tNAQ6qU+tnA==", - "tarballUrl": "https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.1.tgz" + "label": "OpenClaw diagnostics OTEL 2026.9.2", + "packageSpec": "@openclaw/diagnostics-otel@2026.9.2", + "integrity": "sha512-yilG4G1Fd1yvW65Qy+qNPR+x6tBjwVmTWv+7BULoN70HY3BJqt9YVOL42PvWXHYpaTs/LwUlisqxjbJRfYyi9A==", + "tarballUrl": "https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.2.tgz" }, { - "label": "OpenClaw Brave plugin 2026.9.1", - "packageSpec": "@openclaw/brave-plugin@2026.9.1", - "integrity": "sha512-4+j+eQTToV3k7Cb25MUL6h2uL8cJYyuLytfpd/sJK/HjR43dgKBqKpBsb1+I3w1Jr6PLpnjSf6/I3//3K0cdnA==", - "tarballUrl": "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.1.tgz" + "label": "OpenClaw Brave plugin 2026.9.2", + "packageSpec": "@openclaw/brave-plugin@2026.9.2", + "integrity": "sha512-6416aPlfnAKlu8IBrrjgfoiss/10xB32ywFwnIf/fkVMQE61qsmzA/qxUniQuDwOB6EBFNEkNs54DhIT7g3UVg==", + "tarballUrl": "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.2.tgz" }, { - "label": "OpenClaw Discord plugin 2026.9.1", - "packageSpec": "@openclaw/discord@2026.9.1", - "integrity": "sha512-qNmN2a8A9dET4igPp0RML171sEn8PDMyNCYNp/DqcJ4tn3XTHpacSOTkqBmv5yXTycJRC9rfFP8FT/SdW0Rldg==", - "tarballUrl": "https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.1.tgz" + "label": "OpenClaw Discord plugin 2026.9.2", + "packageSpec": "@openclaw/discord@2026.9.2", + "integrity": "sha512-j+fSHxbXA+DSxwbL8SvtsDNL6tvBX4+RmH+EerVW6dCbeIwSconXj6J/+AaN/mhzZI4g0jPPj30TUhdCRRbc2w==", + "tarballUrl": "https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.2.tgz" }, { - "label": "OpenClaw Slack plugin 2026.9.1", - "packageSpec": "@openclaw/slack@2026.9.1", - "integrity": "sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg==", - "tarballUrl": "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.1.tgz" + "label": "OpenClaw Slack plugin 2026.9.2", + "packageSpec": "@openclaw/slack@2026.9.2", + "integrity": "sha512-6M1M6gL3iXahpalNsYAUuA+wvnV8lbMlNNH2ToegFvaSJcIll4S9kFa5mv3GFoquhMRHQjEjnkXPHP/pXwaWcA==", + "tarballUrl": "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.2.tgz" }, { - "label": "OpenClaw WhatsApp plugin 2026.9.1", - "packageSpec": "@openclaw/whatsapp@2026.9.1", - "integrity": "sha512-llIcoMa6FM4SgYn7GG1FQIeTTA5JDdcHW5D7PT+3aGYT3/E2eLFutKwDvD/w7G0hvDwSftzZgLi3iA8dzK7a3A==", - "tarballUrl": "https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.1.tgz" + "label": "OpenClaw WhatsApp plugin 2026.9.2", + "packageSpec": "@openclaw/whatsapp@2026.9.2", + "integrity": "sha512-vOWQIk7FpLHrhMmO+FaLi+pnFB82hiWNJJFJONkBuofERh2SMEz7EMut/vECFFEjFnmOZSVlYfRlxhbNkd/R6g==", + "tarballUrl": "https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.2.tgz" }, { - "label": "OpenClaw Microsoft Teams plugin 2026.9.1", - "packageSpec": "@openclaw/msteams@2026.9.1", - "integrity": "sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong==", - "tarballUrl": "https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.1.tgz" + "label": "OpenClaw Microsoft Teams plugin 2026.9.2", + "packageSpec": "@openclaw/msteams@2026.9.2", + "integrity": "sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og==", + "tarballUrl": "https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.2.tgz" }, { "label": "Tencent WeChat plugin 2.4.9", @@ -96,12 +96,12 @@ "lockedGraphs": [ { "id": "openclaw-runtime", - "label": "OpenClaw 2026.9.1 locked runtime graph", - "packageSpec": "openclaw@2026.9.1", - "integrity": "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w==", - "tarballUrl": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz", + "label": "OpenClaw 2026.9.2 locked runtime graph", + "packageSpec": "openclaw@2026.9.2", + "integrity": "sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg==", + "tarballUrl": "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz", "directory": "agents/openclaw/openclaw-runtime", - "lockSha256": "71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113" + "lockSha256": "cbcfdd15430b81f50ada9f39858a694815e570c8bb3e6b4bb9f1c0b53bf0ef4a" }, { "id": "mcporter-runtime", diff --git a/ci/reviewed-npm-lifecycle-allowlist.json b/ci/reviewed-npm-lifecycle-allowlist.json index 8fdf1a7fbde..0af1d723740 100644 --- a/ci/reviewed-npm-lifecycle-allowlist.json +++ b/ci/reviewed-npm-lifecycle-allowlist.json @@ -2,18 +2,18 @@ "schemaVersion": 1, "defaultPolicy": "deny", "reviewedArchivePackages": [ - "@openclaw/brave-plugin@2026.9.1", - "@openclaw/diagnostics-otel@2026.9.1", - "@openclaw/discord@2026.9.1", - "@openclaw/googlechat@2026.9.1", - "@openclaw/msteams@2026.9.1", - "@openclaw/slack@2026.9.1", - "@openclaw/whatsapp@2026.9.1", + "@openclaw/brave-plugin@2026.9.2", + "@openclaw/diagnostics-otel@2026.9.2", + "@openclaw/discord@2026.9.2", + "@openclaw/googlechat@2026.9.2", + "@openclaw/msteams@2026.9.2", + "@openclaw/slack@2026.9.2", + "@openclaw/whatsapp@2026.9.2", "@tencent-weixin/openclaw-weixin@2.4.9", "@zed-industries/codex-acp@0.11.1", "openclaw@2026.3.11", "openclaw@2026.4.24", - "openclaw@2026.9.1" + "openclaw@2026.9.2" ], "allowedLifecycleScripts": [ { @@ -23,7 +23,7 @@ "explicitCommand": "node /usr/local/lib/node_modules/openclaw/scripts/postinstall-bundled-plugins.mjs" }, { - "packageSpec": "openclaw@2026.9.1", + "packageSpec": "openclaw@2026.9.2", "event": "postinstall", "manifestCommand": "node scripts/postinstall-bundled-plugins.mjs", "explicitCommand": "node /usr/local/lib/nemoclaw/openclaw-runtime/node_modules/openclaw/scripts/postinstall-bundled-plugins.mjs" diff --git a/docs/manage-sandboxes/add-channels-after-onboarding.mdx b/docs/manage-sandboxes/add-channels-after-onboarding.mdx index 446bd27c55e..1b084f057df 100644 --- a/docs/manage-sandboxes/add-channels-after-onboarding.mdx +++ b/docs/manage-sandboxes/add-channels-after-onboarding.mdx @@ -95,8 +95,7 @@ After a successful rebuild, `channels add` checks the selected Telegram, Discord An official-plugin provenance failure stops the image build before these runtime checks. -Report the failure diagnostic, plugin name and NemoClaw version to a maintainer; NemoClaw manages the package pins and build cache. -Refer to [Official messaging plugin verification stops the build](../../reference/troubleshooting#official-messaging-plugin-verification-stops-the-build). +Follow the recovery procedure in [Official messaging plugin verification stops the build](../../reference/troubleshooting#official-messaging-plugin-verification-stops-the-build). If you defer the rebuild, apply the change later: diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 9293e348839..3db4bc9811b 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -654,7 +654,7 @@ Use `--fresh` to ignore any saved onboarding session and restart the wizard from The installer detects existing sandbox sessions before onboarding and prints a warning if any are found. To make the installer abort instead of continuing, set `NEMOCLAW_SINGLE_SESSION=1`: ```bash -NEMOCLAW_SINGLE_SESSION=1 curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash +curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_SINGLE_SESSION=1 bash ``` When existing sandboxes were created with OpenShell earlier than `0.0.37`, the installer prompts before running the automatic gateway upgrade path. @@ -2434,7 +2434,7 @@ When the gateway is reachable, run the `$$nemoclaw channels remove (); const byResolved = new Map(); const byIntegrity = new Map(); + const collidingArchives = new Set(); for (const packagePath of reachablePackagePaths(packages, target)) { const entry = record(packages[packagePath], `package-lock entry ${packagePath}`); @@ -285,7 +286,7 @@ export function lockedArchives( const sameResolved = byResolved.get(resolved); const sameIntegrity = byIntegrity.get(integrity); if (sameArchive && JSON.stringify(sameArchive) !== JSON.stringify(candidate)) { - throw new Error(`package-lock archive name is ambiguous: ${archive}`); + collidingArchives.add(archive); } if (sameResolved && sameResolved.integrity !== integrity) { throw new Error(`package-lock URL has more than one integrity: ${resolved}`); @@ -299,7 +300,16 @@ export function lockedArchives( } if (byArchive.size === 0) throw new Error("package-lock.json contains no registry archives"); - return [...byArchive.values()].sort((left, right) => left.archive.localeCompare(right.archive)); + return [...byResolved.values()] + .map((entry) => + collidingArchives.has(entry.archive) + ? { + ...entry, + archive: `${crypto.createHash("sha256").update(entry.resolved).digest("hex")}-${entry.archive}`, + } + : entry, + ) + .sort((left, right) => left.archive.localeCompare(right.archive)); } async function exactFileSource(file: string, label: string): Promise { diff --git a/scripts/lib/openclaw-npm-remediation.mts b/scripts/lib/openclaw-npm-remediation.mts index eef0a35761b..39d1411f59e 100755 --- a/scripts/lib/openclaw-npm-remediation.mts +++ b/scripts/lib/openclaw-npm-remediation.mts @@ -27,8 +27,8 @@ type JsonObject = Record; type Remediation = Readonly<{ expectedPatchedMetadataIntegrity?: string; expectedPatchedTreeIntegrity?: string; - kind: "axios" | "core" | "current-core" | "jaeger" | "legacy-core" | "undici" | "undici-security"; - version: "2026.3.11" | "2026.6.10" | "2026.7.1" | "2026.9.1"; + kind: "axios" | "core" | "current-core" | "jaeger" | "legacy-core" | "undici"; + version: "2026.3.11" | "2026.6.10" | "2026.7.1"; }>; type RemediationRequest = Readonly<{ @@ -107,47 +107,6 @@ const CURRENT_UNDICI_VERSION = "8.10.0"; const CURRENT_UNDICI_INTEGRITY = "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="; const CURRENT_UNDICI_TARBALL = "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz"; -const UNDICI_SECURITY_TARGETS: Readonly< - Record< - string, - Readonly<{ - name: string; - previous: string; - version: string; - integrity: string; - node: string; - bundled: boolean; - }> - > -> = Object.freeze({ - "openclaw@2026.9.1": { - name: "openclaw", - previous: "8.10.0", - version: "8.10.2", - bundled: false, - integrity: - "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", - node: ">=22.19.0", - }, - "@openclaw/discord@2026.9.1": { - name: "@openclaw/discord", - previous: "8.10.0", - version: "8.10.2", - bundled: true, - integrity: - "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", - node: ">=22.19.0", - }, - "@openclaw/slack@2026.9.1": { - name: "@openclaw/slack", - previous: "7.29.0", - version: "7.29.1", - bundled: true, - integrity: - "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", - node: ">=20.18.1", - }, -}); const CURRENT_IP_ADDRESS_VERSION = "10.3.1"; const CURRENT_IP_ADDRESS_INTEGRITY = "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g=="; @@ -167,25 +126,6 @@ const OTEL_CORE_INTEGRITY = const OTEL_CORE_TARBALL = "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz"; const REMEDIATIONS: Readonly> = Object.freeze({ - // Retire these after a reviewed OpenClaw release ships patched Undici dependencies and bundles. - "openclaw@2026.9.1": { - kind: "undici-security", - version: "2026.9.1", - expectedPatchedTreeIntegrity: - "sha512-XLwwECWs8nL8/WJ/PplTcL0GyG6VGt0nN8K9idVv3/lKZiMAsJM1aw7SKj68b9Co9UD2iEhWfQKaGC6Pgoeniw==", - }, - "@openclaw/discord@2026.9.1": { - kind: "undici-security", - version: "2026.9.1", - expectedPatchedTreeIntegrity: - "sha512-UFBks0k94yKmtnD/D6I908I5vQwlUSohBFdjLdHFhf3cS0SeBpTDMoflBixsj6C3I/QWg6xhBPWJD1o4riDtsQ==", - }, - "@openclaw/slack@2026.9.1": { - kind: "undici-security", - version: "2026.9.1", - expectedPatchedTreeIntegrity: - "sha512-iLZXmYOy8g++8oqMEP95lCYLH0OgD5jm0QiqoEQTMYgMiMPrBRjwpX406n5SKIqTYW8bnTGDlTVoTEjMO/dJYQ==", - }, "@openclaw/diagnostics-otel@2026.6.10": { expectedPatchedMetadataIntegrity: "sha512-ByLYBs3KXz3u0mPuj9DcP/xPTJNgQaLTPxazybhyIC1VjyftEmKQuoZufPZ8z8CjwBsOPm6NbjMQB2BfX36TTg==", @@ -1184,69 +1124,6 @@ function packReplacement( }); } -export function patchOpenClawUndiciDependency(packageDirectory: string, packageSpec: string): void { - const target = UNDICI_SECURITY_TARGETS[packageSpec]; - if (!target) throw new Error(`No Undici security update is defined for ${packageSpec}`); - const manifestPath = join(packageDirectory, "package.json"); - const manifest = readJson(manifestPath); - requirePackageIdentity(manifest, target.name, "2026.9.1", packageSpec); - const bundledPath = join(packageDirectory, "node_modules", "undici"); - const bundles = manifest.bundleDependencies ?? manifest.bundledDependencies ?? []; - if ( - manifest.dependencies?.undici !== target.previous || - !Array.isArray(bundles) || - bundles.includes("undici") !== target.bundled || - existsSync(bundledPath) !== target.bundled || - existsSync(join(packageDirectory, "npm-shrinkwrap.json")) - ) { - throw new Error(`${packageSpec} Undici dependency contract changed after review`); - } - if (target.bundled) { - requirePackageIdentity( - readJson(join(bundledPath, "package.json")), - "undici", - target.previous, - packageSpec, - ); - } - manifest.dependencies.undici = target.version; - writeJson(manifestPath, manifest); -} - -function remediateSecurityUndici( - sourcePackage: string, - packageSpec: string, - remediationRoot: string, - env: NodeJS.ProcessEnv, -): void { - const target = UNDICI_SECURITY_TARGETS[packageSpec]; - if (!target) throw new Error(`No Undici security update is defined for ${packageSpec}`); - const replacement = packReplacement( - `undici@${target.version}`, - target.integrity, - `https://registry.npmjs.org/undici/-/undici-${target.version}.tgz`, - remediationRoot, - env, - ); - const directory = extractArchive( - replacement.archivePath, - join(remediationRoot, "undici"), - remediationRoot, - env, - ); - const manifest = readJson(join(directory, "package.json")); - requirePackageIdentity(manifest, "undici", target.version, "Undici security update"); - if ( - Object.keys(manifest.dependencies ?? {}).length !== 0 || - manifest.engines?.node !== target.node - ) { - throw new Error(`undici@${target.version} package contract changed after review`); - } - patchOpenClawUndiciDependency(sourcePackage, packageSpec); - if (target.bundled) - copyReplacementPackage(directory, join(sourcePackage, "node_modules", "undici")); -} - export function buildRemediatedOpenClawPluginArchive( request: BuildRequest, ): Extract { @@ -1272,9 +1149,7 @@ export function buildRemediatedOpenClawPluginArchive( remediationRoot, env, ); - if (remediation.kind === "undici-security") { - remediateSecurityUndici(sourcePackage, request.packageSpec, remediationRoot, env); - } else if (remediation.kind === "core") { + if (remediation.kind === "core") { const fsSafeArchive = packReplacement( `@openclaw/fs-safe@${FS_SAFE_VERSION}`, FS_SAFE_INTEGRITY, diff --git a/scripts/lib/patch-openclaw-container-restart.mts b/scripts/lib/patch-openclaw-container-restart.mts index 0f83b3d42a0..cc799ac8c33 100755 --- a/scripts/lib/patch-openclaw-container-restart.mts +++ b/scripts/lib/patch-openclaw-container-restart.mts @@ -10,7 +10,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -const VERSION = "2026.9.1"; +const VERSION = "2026.9.2"; const MARKER = "// nemoclaw: reload sandbox plugins with a fresh process image"; const ORIGINAL = `\treturn { \t\tmode: "disabled", @@ -50,20 +50,70 @@ export function patchContainerRestart(source: string): string { return source.replace(originalFunction, () => patchedFunction); } +const HOST_RESTART_MARKER = "// nemoclaw: authenticate host safe restart as the local backend"; +const HOST_RESTART_TARGET = + '\tconst result = await callGatewayCli({\n\t\tmethod: "gateway.restart.request",\n\t\tparams,'; +const HOST_RESTART_REPLACEMENT = `${HOST_RESTART_TARGET} +\t\t${HOST_RESTART_MARKER} +\t\t...process.env.OPENSHELL_SANDBOX === "1" && process.env.NEMOCLAW_OPENCLAW_HOST_RESTART === "1" ? { +\t\t\tclientName: "gateway-client", +\t\t\tmode: "backend", +\t\t\trequireLocalBackendSharedAuth: true, +\t\t\tsharedStateMode: "read-only" +\t\t} : {},`; + +export function patchHostSafeRestart(source: string): string { + const matches = [ + ...source.matchAll( + /async function runSafeGatewayRestart\(opts(?:, target)?\) \{[\s\S]*?\n\}/gu, + ), + ]; + if (matches.length !== 1) throw new Error("Expected one native safe restart function"); + const original = matches[0]![0]; + if (source.includes(HOST_RESTART_MARKER)) { + if ( + source.split(HOST_RESTART_MARKER).length !== 2 || + original.split(HOST_RESTART_REPLACEMENT).length !== 2 + ) + throw new Error("Incomplete OpenClaw host safe restart patch"); + return source; + } + if (original.split(HOST_RESTART_TARGET).length !== 2) + throw new Error("Unrecognized OpenClaw safe restart boundary"); + return source.replace(original, () => + original.replace(HOST_RESTART_TARGET, HOST_RESTART_REPLACEMENT), + ); +} + export function patchOpenClawContainerRestart(distDir: string, audit = false): void { const metadata = JSON.parse(fs.readFileSync(path.join(distDir, "..", "package.json"), "utf8")); // The Dockerfile restricts these pins to explicitly selected legacy E2E fixtures. if (["2026.3.11", "2026.4.24"].includes(metadata.version)) return; - if (metadata.version !== VERSION) { + if (metadata.version !== VERSION && metadata.version !== "2026.9.1") { throw new Error(`Unsupported OpenClaw version: ${metadata.version}`); } - const target = path.join(distDir, "cli", "gateway-lifecycle.runtime.js"); - const source = fs.readFileSync(target, "utf8"); - const patched = patchContainerRestart(source); - if (audit) { - if (patched !== source) throw new Error("OpenClaw container restart patch is missing"); - } else if (patched !== source) { - fs.writeFileSync(target, patched); + const safeRestartFiles = fs + .readdirSync(distDir) + .filter((name) => name.startsWith("lifecycle-") && name.endsWith(".js")) + .map((name) => path.join(distDir, name)) + .filter((file) => + fs.readFileSync(file, "utf8").includes("async function runSafeGatewayRestart("), + ); + if (safeRestartFiles.length !== 1) throw new Error("Expected one native safe restart module"); + const patches = [ + { + target: path.join(distDir, "cli", "gateway-lifecycle.runtime.js"), + patch: patchContainerRestart, + }, + { target: safeRestartFiles[0]!, patch: patchHostSafeRestart }, + ].map(({ target, patch }) => { + const source = fs.readFileSync(target, "utf8"); + return { target, source, patched: patch(source) }; + }); + for (const { target, source, patched } of patches) { + if (audit) { + if (patched !== source) throw new Error("OpenClaw container restart patch is missing"); + } else if (patched !== source) fs.writeFileSync(target, patched); } } diff --git a/scripts/lib/patch-openclaw-npm12-pack-json.mts b/scripts/lib/patch-openclaw-npm12-pack-json.mts index 993b4cb4999..2ed856169e9 100755 --- a/scripts/lib/patch-openclaw-npm12-pack-json.mts +++ b/scripts/lib/patch-openclaw-npm12-pack-json.mts @@ -41,6 +41,11 @@ const REVIEWED_LAYOUTS = { filename: /^install-source-utils-[A-Za-z0-9_-]+\.js$/, mode: "native", }, + "2026.9.2": { + expectedFiles: 1, + filename: /^install-source-utils-[A-Za-z0-9_-]+\.js$/, + mode: "native", + }, } as const; function occurrences(contents: string, needle: string): number { diff --git a/scripts/lib/patch-openclaw-secondary-main-session-delete.mts b/scripts/lib/patch-openclaw-secondary-main-session-delete.mts index 01e21bf018e..9c6508cae3b 100755 --- a/scripts/lib/patch-openclaw-secondary-main-session-delete.mts +++ b/scripts/lib/patch-openclaw-secondary-main-session-delete.mts @@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url"; const SCRIPT_PATH = fileURLToPath(import.meta.url); -export const SUPPORTED_OPENCLAW_VERSION = "2026.9.1"; +export const SUPPORTED_OPENCLAW_VERSION = "2026.9.2"; export const MARKER = "/* nemoclaw secondary-agent main-session delete compatibility */"; export const WORKER_MARKER = "/* nemoclaw worker secondary-agent main-session delete compatibility */"; @@ -129,7 +129,7 @@ function resolveTargets(distDir: string): [string, string] { export function patchOpenClawSecondaryAgentMainSessionDelete(distDir: string): PatchRunResult { const resolvedDist = path.resolve(distDir); const version = readVersion(resolvedDist); - if (version !== SUPPORTED_OPENCLAW_VERSION) { + if (version !== SUPPORTED_OPENCLAW_VERSION && version !== "2026.9.1") { if (["2026.3.11", "2026.4.24"].includes(version)) { return { status: "skipped-unsupported-version", version }; } diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index c7991504ec8..fa8613ea1f5 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -1492,7 +1492,7 @@ ensure_gateway_token() { local _write_rc=0 run_openclaw_config_as_owner /usr/local/bin/node - \ - "$config_file" "${1:-}" <<'NODETOKEN' || _write_rc=$? + "$config_file" "$_DASHBOARD_PORT" <<'NODETOKEN' || _write_rc=$? const crypto = require("crypto"); const fs = require("fs"); const pathModule = require("path"); @@ -1543,17 +1543,16 @@ function makeTempPath(dirPath) { } try { + const gatewayPort = Number(process.argv[3]); + if (!Number.isInteger(gatewayPort) || gatewayPort < 1024 || gatewayPort > 65535) { + throw new Error("selected gateway port is invalid"); + } const cfg = parseConfig(fs.readFileSync(path, "utf8")); const gateway = cfg.gateway && typeof cfg.gateway === "object" ? cfg.gateway : (cfg.gateway = {}); - // Pairing commands clear environment overrides and read this native port. - const startupPort = process.argv[3]; - if (startupPort) { - const port = Number(startupPort); - if (!Number.isInteger(port) || port < 1024 || port > 65535) { - throw new Error("invalid selected gateway startup port"); - } - gateway.port = port; - } + // Pairing commands intentionally drop environment overrides. Their native + // config must resolve the same port passed to gateway run, including when an + // external image carries a different baked default. + gateway.port = gatewayPort; const auth = gateway.auth && typeof gateway.auth === "object" ? gateway.auth : (gateway.auth = {}); auth.token = tokenUrlSafe(32); // OpenClaw 2026.9.1 rejects the legacy timestamp key. Scrub it defensively @@ -1650,7 +1649,7 @@ needs_gateway_token_for_current_command() { prepare_gateway_token_for_current_command() { if [ ${#NEMOCLAW_CMD[@]} -eq 0 ]; then - ensure_gateway_token "${1:-}" + ensure_gateway_token return $? fi @@ -4820,7 +4819,7 @@ if [ "$(id -u)" -ne 0 ]; then _nemoclaw_capture_epoch_realtime _NEMOCLAW_GATEWAY_CONFIG_FINISHED_EPOCH _nemoclaw_capture_epoch_realtime _NEMOCLAW_GATEWAY_PROVIDER_FINISHED_EPOCH refresh_openclaw_provider_placeholders - prepare_gateway_token_for_current_command "$_DASHBOARD_PORT" + prepare_gateway_token_for_current_command export_gateway_token _nemoclaw_capture_epoch_realtime _NEMOCLAW_GATEWAY_TOKEN_FINISHED_EPOCH write_messaging_runtime_setup_plan @@ -4919,7 +4918,7 @@ if is_managed_inference_route; then fi run_requested_openclaw_post_upgrade_doctor || exit 1 refresh_openclaw_provider_placeholders -prepare_gateway_token_for_current_command "$_DASHBOARD_PORT" +prepare_gateway_token_for_current_command export_gateway_token write_messaging_runtime_setup_plan write_runtime_shell_env diff --git a/scripts/patch-openclaw-device-self-approval.mts b/scripts/patch-openclaw-device-self-approval.mts index 4fe55c13a17..789f96fd223 100644 --- a/scripts/patch-openclaw-device-self-approval.mts +++ b/scripts/patch-openclaw-device-self-approval.mts @@ -495,6 +495,19 @@ const CLI_TARGET = [ ].join("\n"); const CLI_HELPER_ANCHOR = "function resolveApprovePairingScopesForRequest(request, paired) {"; +const CLI_SPLIT_SCOPE_ANCHOR = "function resolvePairingCallScopes(operatorScopes) {"; +const CLI_SPLIT_SCOPE_ADAPTER = [ + "function isKnownNonAdminOperatorScope(scope) {", + '\treturn scope !== "operator.admin" && isOperatorScope(scope);', + "}", + CLI_HELPER_ANCHOR, + "\tconst operatorScopes = resolvePendingOperatorApprovalScopes(request, paired);", + "\tif (operatorScopes.length === 0) return;", + "\tconst out = new Set([PAIRING_SCOPE]);", + CLI_TARGET, + "}", + "", +].join("\n"); const CLI_HELPER = [ "function resolveNemoClawCanonicalPairingDescriptor(name) {", "\tconst nemoclawRawDescriptor = process.env[name];", @@ -967,6 +980,9 @@ const AUTH_DEVICE_TOKEN_SQLITE_REPLACEMENT = AUTH_DEVICE_TOKEN_SQLITE_REPLACEMEN const AUTH_INLINE_APPROVAL_TARGET = "\t\t\tconst inlineApprovalAttempted = trustedProxyApprovalScopes !== null || pairing.request.silent === true;"; +const AUTH_LOCAL_PAIRING_TARGET = "\t\t\t\tplan.allowSilentLocalPairing === true &&"; +const AUTH_LOCAL_PAIRING_REPLACEMENT = + '\t\t\t\t(plan.allowSilentLocalPairing === true || plan.localApproval === "silent") &&'; const AUTH_INLINE_APPROVAL_REPLACEMENT_PREVIOUS = [ "\t\t\tconst nemoclawExistingScopes = normalizeSortedUniqueTrimmedStringList(existingPairedDevice ? resolvePairedAccessScopes(existingPairedDevice) : []);", "\t\t\tconst nemoclawRequestedScopes = normalizeSortedUniqueTrimmedStringList(scopes);", @@ -975,7 +991,7 @@ const AUTH_INLINE_APPROVAL_REPLACEMENT_PREVIOUS = [ '\t\t\t\treason === "scope-upgrade" &&', "\t\t\t\tpairing.request.isRepair === true &&", "\t\t\t\tpairing.request.silent === true &&", - "\t\t\t\tplan.allowSilentLocalPairing === true &&", + AUTH_LOCAL_PAIRING_REPLACEMENT, '\t\t\t\t(authMethod === "device-token" || authMethod === "token") &&', "\t\t\t\tconnectParams.client.id === GATEWAY_CLIENT_IDS.CLI &&", "\t\t\t\tconnectParams.client.mode === GATEWAY_CLIENT_MODES.CLI &&", @@ -1024,6 +1040,12 @@ const AUTH_INLINE_APPROVAL_REPLACEMENT = AUTH_INLINE_APPROVAL_REPLACEMENT_PREVIO ].join("\n"), ); +const HANDLER_DEVICE_TOKEN_TARGET = + '\tconst deviceToken = typeof client?.connect?.auth?.token === "string" ? client.connect.auth.token.trim() : "";'; +const HANDLER_DEVICE_TOKEN_REPLACEMENT = [ + '\tconst explicitDeviceToken = typeof client?.connect?.auth?.deviceToken === "string" ? client.connect.auth.deviceToken.trim() : "";', + '\tconst deviceToken = explicitDeviceToken || (typeof client?.connect?.auth?.token === "string" ? client.connect.auth.token.trim() : "");', +].join("\n"); const HANDLER_HELPER = [ "function resolveNemoClawSelfApprovalIdentity(pending, authz, client) {", "\tif (authz.isAdminCaller || client?.isDeviceTokenAuth !== true) return null;", @@ -1035,7 +1057,7 @@ const HANDLER_HELPER = [ '\tconst clientRole = typeof client?.connect?.role === "string" ? client.connect.role.trim() : "";', '\tconst clientId = typeof client?.connect?.client?.id === "string" ? client.connect.client.id.trim() : "";', '\tconst clientMode = typeof client?.connect?.client?.mode === "string" ? client.connect.client.mode.trim() : "";', - '\tconst deviceToken = typeof client?.connect?.auth?.token === "string" ? client.connect.auth.token.trim() : "";', + HANDLER_DEVICE_TOKEN_REPLACEMENT, '\tconst pendingClientId = typeof pending?.clientId === "string" ? pending.clientId.trim() : "";', '\tconst pendingClientMode = typeof pending?.clientMode === "string" ? pending.clientMode.trim() : "";', "\tif (", @@ -1650,6 +1672,8 @@ const STATE_SQLITE_PENDING_REPLACEMENT = [ "\t\tconst nemoclawSelfApprovalScopes = resolveNemoClawSelfApprovalScopes(pendingRecord, options?.callerScopes, options?.nemoclawSelfApprovalIdentity);", "\t\tconst autoApproveScopes = options?.autoApproveNewDeviceScopes;", ].join("\n"); +const STATE_SQLITE_APPROVAL_CALLBACK = + "\treturn await withPendingDevicePairingApproval(requestId, options, baseDir, (state, pendingRecord, existing) => {"; const STATE_SQLITE_CALLER_REPLACEMENT = STATE_CALLER_REPLACEMENT; const STATE_SQLITE_COMMIT_TARGET = [ "\t\t\t}),", @@ -1744,8 +1768,30 @@ const STATE_SQLITE_AUTH_UPDATE_REPLACEMENT = [ ].join("\n"); function patchCurrentDevicesCli(source: string, file: string): PatchResult { + // 2026.9.2 shares its native scope resolver with token management. Keep that + // resolver intact and attach bounded self-approval only to the approval path. + let normalized = source; + if (!source.includes(CLI_HELPER_ANCHOR) && source.includes(CLI_SPLIT_SCOPE_ANCHOR)) { + for (const [target, replacement] of [ + [CLI_SPLIT_SCOPE_ANCHOR, `${CLI_SPLIT_SCOPE_ADAPTER}${CLI_SPLIT_SCOPE_ANCHOR}`], + [ + "scopes: resolvePairingCallScopes(resolvePendingOperatorApprovalScopes(request, lookupPairedDevice(indexPairedDevices(list.paired), request)))", + "scopes: resolveApprovePairingScopesForRequest(request, lookupPairedDevice(indexPairedDevices(list.paired), request))", + ], + ] as const) { + const adapted = replaceExactlyOnce( + normalized, + target, + replacement, + "split approval-scope adapter", + file, + ); + if (adapted.error) return { source, status: "no-match", error: adapted.error }; + normalized = adapted.source; + } + } let result: ReplacementResult = replaceExactlyOnce( - source, + normalized, CLI_HELPER_ANCHOR, `${CLI_HELPER_SQLITE}${CLI_HELPER_ANCHOR}`, "bounded SQLite devices CLI classifier anchor", @@ -1804,8 +1850,14 @@ function patchCurrentPairingState(source: string, file: string): PatchResult { file, ); if (result.error) return { source, status: "no-match", error: result.error }; + const pendingTarget = source.includes(STATE_SQLITE_APPROVAL_CALLBACK) + ? `${STATE_SQLITE_APPROVAL_CALLBACK}\n\t\tconst autoApproveScopes = options?.autoApproveNewDeviceScopes;` + : STATE_SQLITE_PENDING_TARGET; + const pendingReplacement = source.includes(STATE_SQLITE_APPROVAL_CALLBACK) + ? `${STATE_SQLITE_APPROVAL_CALLBACK}\n\t\tconst nemoclawSelfApprovalScopes = resolveNemoClawSelfApprovalScopes(pendingRecord, options?.callerScopes, options?.nemoclawSelfApprovalIdentity);\n\t\tconst autoApproveScopes = options?.autoApproveNewDeviceScopes;` + : STATE_SQLITE_PENDING_REPLACEMENT; for (const [target, replacement, label] of [ - [STATE_SQLITE_PENDING_TARGET, STATE_SQLITE_PENDING_REPLACEMENT, "SQLite pending target"], + [pendingTarget, pendingReplacement, "SQLite pending target"], [STATE_CALLER_TARGET, STATE_SQLITE_CALLER_REPLACEMENT, "SQLite caller-scope target"], [STATE_SQLITE_COMMIT_TARGET, STATE_SQLITE_COMMIT_REPLACEMENT, "SQLite approval target"], [ @@ -1987,10 +2039,17 @@ const BASE_FILE_SPECS: FileSpec[] = [ return ( (source.includes("async function approvePairingWithFallback(opts, requestId)") || source.includes("async function approvePairingWithFallback(opts, requestId, context)")) && - source.includes("function resolveApprovePairingScopesForRequest(request, paired)") && + (source.includes(CLI_HELPER_ANCHOR) || + (sqliteLayout && + source.includes(CLI_SPLIT_SCOPE_ANCHOR) && + source.includes("function resolvePendingOperatorApprovalScopes(request, paired)"))) && source.includes('callGatewayCli("device.pair.approve"') && (sqliteLayout ? CLI_SELECTOR_SQLITE_DEPENDENCIES : CLI_SELECTOR_DEPENDENCIES).every( - (dependency) => source.includes(dependency), + (dependency) => + source.includes(dependency) || + (dependency === "isKnownNonAdminOperatorScope" && + source.includes(CLI_SPLIT_SCOPE_ANCHOR) && + source.includes("isOperatorScope")), ) ); }, @@ -2186,6 +2245,23 @@ const BASE_FILE_SPECS: FileSpec[] = [ result.source.includes(AUTH_DEVICE_TOKEN_SQLITE_TARGET) || result.source.includes(AUTH_INLINE_APPROVAL_TARGET) || result.source.includes(AUTH_DEFER_SILENT_SCOPE_UPGRADE_MARKER); + // 2026.9.2 returns a localApproval policy instead of the former boolean. + // Keep the deferral limited to silent local approval, excluding trusted CIDRs. + if ( + sqliteLayout && + result.source.includes(AUTH_DEFER_SILENT_SCOPE_UPGRADE_MARKER) && + result.source.includes(AUTH_LOCAL_PAIRING_TARGET) + ) { + result = replaceExactlyOnce( + result.source, + AUTH_LOCAL_PAIRING_TARGET, + AUTH_LOCAL_PAIRING_REPLACEMENT, + "gateway silent local approval policy", + file, + ); + if (result.error) return { source, status: "no-match", error: result.error }; + changed = true; + } if (result.source.includes(AUTH_SCOPE_UPGRADE_MARKER)) { const appliedReplacement = sqliteLayout ? AUTH_DEVICE_TOKEN_SQLITE_REPLACEMENT @@ -2310,7 +2386,20 @@ const BASE_FILE_SPECS: FileSpec[] = [ ); }, patch(source, file) { - if (source.includes(HANDLER_MARKER)) return { source, status: "already-applied" }; + if (source.includes(HANDLER_MARKER)) { + if (!source.includes(HANDLER_DEVICE_TOKEN_TARGET)) + return { source, status: "already-applied" }; + const upgraded = replaceExactlyOnce( + source, + HANDLER_DEVICE_TOKEN_TARGET, + HANDLER_DEVICE_TOKEN_REPLACEMENT, + "gateway authenticated device credential", + file, + ); + return upgraded.error + ? { source, status: "no-match", error: upgraded.error } + : { source: upgraded.source, status: "would-apply" }; + } let result = replaceExactlyOnce( source, HANDLER_HELPER_ANCHOR, @@ -2357,11 +2446,15 @@ const BASE_FILE_SPECS: FileSpec[] = [ ((source.includes("const withLock = createAsyncLock();") && source.includes('await persistState(state, baseDir, "both")')) || source.includes(STATE_SQLITE_PERSIST_CALL_TARGET) || - (source.includes(STATE_MARKER) && source.includes("approveDevicePairingWithOptions"))) + (source.includes(STATE_MARKER) && + (source.includes("approveDevicePairingWithOptions") || + source.includes(STATE_SQLITE_APPROVAL_CALLBACK)))) ); }, patch(source, file) { - const sqliteLayout = source.includes("approveDevicePairingWithOptions"); + const sqliteLayout = + source.includes("approveDevicePairingWithOptions") || + source.includes(STATE_SQLITE_APPROVAL_CALLBACK); if (sqliteLayout) { const markerCount = countOccurrences(source, STATE_MARKER); if (markerCount === 1) return { source, status: "already-applied" }; @@ -2567,10 +2660,22 @@ const ADMIN_CONFIRM_READONLY_REPLACEMENT = ADMIN_CONFIRM_SILENT_IDENTITY_REPLACE "catch {}", 'catch { throw new Error("Admin approval completed, but its token handoff could not read the local device identity. Repair local OpenClaw state, then retry the intended admin command."); }', ); -const ADMIN_CONFIRM_REPLACEMENT = ADMIN_CONFIRM_READONLY_REPLACEMENT.replace( +const ADMIN_CONFIRM_TRANSPORT_REPLACEMENT = ADMIN_CONFIRM_READONLY_REPLACEMENT.replace( 'await callGatewayCli("device.pair.list", opts, {}, { scopes: [ADMIN_SCOPE] });', 'await callGatewayFromCliWithTransport("device.pair.list", opts, {}, { label: "Devices device.pair.list", defaultTimeoutMs: DEFAULT_DEVICES_TIMEOUT_MS, scopes: [ADMIN_SCOPE] });', ); +const ADMIN_CONFIRM_REPLACEMENT = ADMIN_CONFIRM_TRANSPORT_REPLACEMENT.replace( + '\t\t\tawait callGatewayFromCliWithTransport("device.pair.list", opts, {},', + [ + "\t\t\tconst nemoclawApprovedDevice = approvalContext.nemoclawLocallyApprovedDevice;", + "\t\t\tconst nemoclawApprovedOperator = nemoclawApprovedDevice?.tokens?.operator;", + '\t\t\tconst nemoclawApprovedToken = nemoclawApprovedDevice?.deviceId === nemoclawApprovingIdentity.deviceId && nemoclawApprovedOperator?.role === "operator" && !nemoclawApprovedOperator.revokedAtMs && nemoclawApprovedOperator.scopes?.includes(ADMIN_SCOPE) && typeof nemoclawApprovedOperator.token === "string" ? nemoclawApprovedOperator.token.trim() : void 0;', + '\t\t\tawait callGatewayFromCliWithTransport("device.pair.list", nemoclawApprovedToken ? { ...opts, token: nemoclawApprovedToken, password: void 0 } : opts, {},', + ].join("\n"), +); +const ADMIN_APPROVAL_RESULT_TARGET = + 'if (approved.status === "forbidden") throw new Error(formatDevicePairingForbiddenMessage(approved), { cause: error });'; +const ADMIN_APPROVAL_RESULT_REPLACEMENT = `${ADMIN_APPROVAL_RESULT_TARGET}\n\t\tcontext.nemoclawLocallyApprovedDevice = approved.device; // nemoclaw: retain approved token privately for confirmation`; const ADMIN_CONFIRM_FUNCTION = "async function runDevicesApproveCommand(requestId, opts) {"; const ADMIN_CONFIRM_SPEC: FileSpec = { id: "explicit-admin-token-confirmation", @@ -2580,6 +2685,20 @@ const ADMIN_CONFIRM_SPEC: FileSpec = { return source.includes(ADMIN_CONFIRM_FUNCTION); }, patch(source, file) { + const retained = countOccurrences(source, ADMIN_APPROVAL_RESULT_REPLACEMENT); + if ( + countOccurrences(source, ADMIN_APPROVAL_RESULT_TARGET) !== 2 || + (retained !== 0 && retained !== 2) + ) + return { + source, + status: "no-match", + error: `explicit admin token handoff in ${file}: local approval result changed`, + }; + const retainedSource = + retained === 2 + ? source + : source.replaceAll(ADMIN_APPROVAL_RESULT_TARGET, ADMIN_APPROVAL_RESULT_REPLACEMENT); const gatewayCall = listJsFiles(distDir) .map((candidate) => fs.readFileSync(candidate, "utf8")) .find((candidate) => @@ -2602,6 +2721,7 @@ const ADMIN_CONFIRM_SPEC: FileSpec = { const previousIdentity = [ ADMIN_CONFIRM_SILENT_IDENTITY_REPLACEMENT, ADMIN_CONFIRM_READONLY_REPLACEMENT, + ADMIN_CONFIRM_TRANSPORT_REPLACEMENT, ].find((replacement) => countOccurrences(source, replacement) === 1); if (source.includes(ADMIN_CONFIRM_MARKER)) { if ( @@ -2609,14 +2729,17 @@ const ADMIN_CONFIRM_SPEC: FileSpec = { countOccurrences(source, ADMIN_CONFIRM_REPLACEMENT) === 1 && countOccurrences(source, importedFunction) === 1 ) - return { source, status: "already-applied" }; + return { + source: retainedSource, + status: retained === 2 ? "already-applied" : "would-apply", + }; if ( countOccurrences(source, ADMIN_CONFIRM_MARKER) === 1 && previousIdentity && countOccurrences(source, importedFunction) === 1 ) return { - source: source.replace(previousIdentity, ADMIN_CONFIRM_REPLACEMENT), + source: retainedSource.replace(previousIdentity, ADMIN_CONFIRM_REPLACEMENT), status: "would-apply", }; if (!previous || countOccurrences(source, ADMIN_CONFIRM_MARKER) !== 1) @@ -2633,7 +2756,7 @@ const ADMIN_CONFIRM_SPEC: FileSpec = { error: `explicit admin token handoff in ${file}: partial identity import`, }; const imported = replaceExactlyOnce( - source, + retainedSource, ADMIN_CONFIRM_FUNCTION, importedFunction, "explicit admin token identity reader", diff --git a/scripts/patch-openclaw-mcp-reliability.mts b/scripts/patch-openclaw-mcp-reliability.mts index 0970a4546f0..1db2bb8656b 100755 --- a/scripts/patch-openclaw-mcp-reliability.mts +++ b/scripts/patch-openclaw-mcp-reliability.mts @@ -231,6 +231,63 @@ const SHAPE_20260901 = { ], } as const; +// 2026.9.2 owns connection and catalog state per server. Preserve that ownership +// while applying the same bounded startup retry around one server's load. +const SERVER_TRANSPORT_PATTERN = [ + "\t\t\tconst resolved = resolveMcpTransport(serverName, transportSource, {", + "\t\t\t\tcfg: params.cfg,", + "\t\t\t\tagentDir: params.agentDir,", + "\t\t\t\tprepareDataDir: loaded.prepareDataDirsByServer?.[serverName]?.dataDir,", + "\t\t\t\trequesterScope: params.requesterScope", + "\t\t\t});", +].join("\n"); +const SERVER_ATTEMPT_OPEN_PATTERN = [ + "\t\t\t\ttools: []", + "\t\t\t};", + "\t\t\tconst safeServerName = params.safeServerNamesByServer?.get(serverName) ?? serverName;", +].join("\n"); +const SERVER_ATTEMPT_OPEN_REPLACEMENT = SERVER_ATTEMPT_OPEN_PATTERN.replace( + "\t\t\tconst safeServerName", + [ + "\t\t\treturn await nemoClawWithMcpStartRetry({", + "\t\t\t\tserverName,", + "\t\t\t\tinitialResolved: resolved,", + "\t\t\t\tresolveTransport: () => resolveMcpTransport(serverName, transportSource, {", + "\t\t\t\t\tcfg: params.cfg,", + "\t\t\t\t\tagentDir: params.agentDir,", + "\t\t\t\t\tprepareDataDir: loaded.prepareDataDirsByServer?.[serverName]?.dataDir,", + "\t\t\t\t\trequesterScope: params.requesterScope", + "\t\t\t\t}),", + "\t\t\t\tattempt: async (resolved) => {", + "\t\t\tconst safeServerName", + ].join("\n"), +); +const SERVER_ATTEMPT_CLOSE_PATTERN = "\t\t\t}\n\t\t})();\n\t\tcatalogInFlight = inFlight;"; +const SERVER_ATTEMPT_CLOSE_REPLACEMENT = + "\t\t\t}\n\t\t\t\t}\n\t\t\t})();\n\t\t})();\n\t\tcatalogInFlight = inFlight;"; +const SERVER_FAILURE_PATTERN = "\t\t\t\t\tdiagnostics: diags\n\t\t\t\t};"; +const SERVER_FAILURE_REPLACEMENT = [ + "\t\t\t\t\tdiagnostics: diags,", + "\t\t\t\t\t[NEMOCLAW_MCP_START_FAILURE]: { error, reusedSession }", + "\t\t\t\t};", +].join("\n"); +const SERVER_REPLACEMENTS = [ + [SERVER_ATTEMPT_OPEN_PATTERN, SERVER_ATTEMPT_OPEN_REPLACEMENT], + [SERVER_ATTEMPT_CLOSE_PATTERN, SERVER_ATTEMPT_CLOSE_REPLACEMENT], + [SERVER_FAILURE_PATTERN, SERVER_FAILURE_REPLACEMENT], + [ACQUIRE_LEASE_PATTERN, ACQUIRE_LEASE_REPLACEMENT], +] as const; +const SHAPE_20260902 = { + unpatched: SERVER_REPLACEMENTS.map(([upstream]) => upstream), + required: [ + "function createServerMcpRuntime(params) {", + SERVER_TRANSPORT_PATTERN, + ...SERVER_REPLACEMENTS.map(([upstream]) => upstream), + ], + patched: [MARKER, ...SERVER_REPLACEMENTS.map(([, patched]) => patched)], + replacements: SERVER_REPLACEMENTS, +} as const; + /** * Injected compatibility runtime for OpenClaw `bundle-mcp`. * @@ -433,7 +490,11 @@ export function patchBundleMcpRuntimeText(source: string, filePath: string): Pat source.includes(TASK_OPEN_20260901_PATTERN) || source.includes(TASK_OPEN_20260901_REPLACEMENT) || (source.includes("policyToolEntries: []") && source.includes("launchDescription")); - const shape = currentLayout ? SHAPE_20260901 : LEGACY_SHAPE; + const shape = source.includes("function createServerMcpRuntime(params) {") + ? SHAPE_20260902 + : currentLayout + ? SHAPE_20260901 + : LEGACY_SHAPE; if (source.includes(MARKER)) { for (const pattern of shape.patched) { const count = countOccurrences(source, pattern); diff --git a/scripts/patch-openclaw-mcp-tools-list-timeout.mts b/scripts/patch-openclaw-mcp-tools-list-timeout.mts index 2801b22f10b..58dc061c72a 100755 --- a/scripts/patch-openclaw-mcp-tools-list-timeout.mts +++ b/scripts/patch-openclaw-mcp-tools-list-timeout.mts @@ -12,7 +12,7 @@ export const MARKER = "/* nemoclaw MCP tools/list timeout override */"; export const TOOLS_LIST_TIMEOUT_ENV = "NEMOCLAW_MCP_TOOLS_LIST_TIMEOUT_MS"; export const TOOLS_LIST_TIMEOUT_MIN_MS = 1500; export const TOOLS_LIST_TIMEOUT_MAX_MS = 10_000; -export const SUPPORTED_OPENCLAW_VERSION = "2026.9.1"; +export const SUPPORTED_OPENCLAW_VERSION = "2026.9.2"; const LEGACY_FIXTURE_OPENCLAW_VERSIONS = new Set(["2026.3.11", "2026.4.24"]); /** Client identity that only the compiled bundle-mcp session runtime carries. */ @@ -194,7 +194,7 @@ function resolveBundleMcpRuntimeFile(distDir: string): string { export function patchOpenClawMcpToolsListTimeout(distDir: string): PatchRunResult { const resolvedDist = path.resolve(distDir); const version = readOpenClawVersion(resolvedDist); - if (version !== SUPPORTED_OPENCLAW_VERSION) { + if (version !== SUPPORTED_OPENCLAW_VERSION && version !== "2026.9.1") { if (LEGACY_FIXTURE_OPENCLAW_VERSIONS.has(version)) { return { status: "skipped-unsupported-version", version }; } diff --git a/scripts/validate-openclaw-tool-search.mts b/scripts/validate-openclaw-tool-search.mts index c6035c512e5..b471e103050 100755 --- a/scripts/validate-openclaw-tool-search.mts +++ b/scripts/validate-openclaw-tool-search.mts @@ -47,6 +47,14 @@ const RUNTIME_MODULE_FILE_PATTERNS = new Map { expect(exitCode).toBe(0); expect(fs.readFileSync(restartLog, "utf8")).toBe( - "env -u OPENCLAW_HOME -u OPENCLAW_STATE_DIR -u OPENCLAW_CONFIG_PATH openclaw gateway restart --safe --skip-deferral --json\n", + "env -u OPENCLAW_HOME -u OPENCLAW_STATE_DIR -u OPENCLAW_CONFIG_PATH NEMOCLAW_OPENCLAW_HOST_RESTART=1 openclaw gateway restart --safe --skip-deferral --json\n", ); expect(nextDm.status, nextDm.stderr).toBe(0); } finally { diff --git a/src/lib/actions/sandbox/gateway-restart.test.ts b/src/lib/actions/sandbox/gateway-restart.test.ts index d9f02ea561d..5654d98679e 100644 --- a/src/lib/actions/sandbox/gateway-restart.test.ts +++ b/src/lib/actions/sandbox/gateway-restart.test.ts @@ -111,7 +111,7 @@ describe("restartSandboxGateway native lifecycle", () => { }); expect(deps.executeSandboxExecCommand).toHaveBeenCalledWith( "alpha", - "env -u OPENCLAW_HOME -u OPENCLAW_STATE_DIR -u OPENCLAW_CONFIG_PATH openclaw gateway restart --safe --skip-deferral --json", + "env -u OPENCLAW_HOME -u OPENCLAW_STATE_DIR -u OPENCLAW_CONFIG_PATH NEMOCLAW_OPENCLAW_HOST_RESTART=1 openclaw gateway restart --safe --skip-deferral --json", 210000, ); }); diff --git a/src/lib/actions/sandbox/gateway-restart.ts b/src/lib/actions/sandbox/gateway-restart.ts index 94eaacfb49b..2588017bd51 100644 --- a/src/lib/actions/sandbox/gateway-restart.ts +++ b/src/lib/actions/sandbox/gateway-restart.ts @@ -455,7 +455,7 @@ export async function restartSandboxGatewayWithDeps( } const nativeCommand = agentName === "openclaw" - ? "env -u OPENCLAW_HOME -u OPENCLAW_STATE_DIR -u OPENCLAW_CONFIG_PATH openclaw gateway restart --safe --skip-deferral --json" + ? "env -u OPENCLAW_HOME -u OPENCLAW_STATE_DIR -u OPENCLAW_CONFIG_PATH NEMOCLAW_OPENCLAW_HOST_RESTART=1 openclaw gateway restart --safe --skip-deferral --json" : `${agentName} gateway restart`; let restartResult: GatewayRestartCommandResult | null; try { diff --git a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts index 0b91e14eeeb..75c76f2f980 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts @@ -245,7 +245,7 @@ export async function prepareRebuildTargetPreflights(args: { } if (fromImage) { preflightExternalImageRebuild({ - agentName: rebuildAgent, + agentName: rebuildAgent ?? "openclaw", expectedToolDisclosure: durableConfig.toolDisclosure, receipt: sandboxEntry.workload, runtime, diff --git a/src/lib/agent/candidate-authority.ts b/src/lib/agent/candidate-authority.ts index a83655eec88..1516b9bd523 100644 --- a/src/lib/agent/candidate-authority.ts +++ b/src/lib/agent/candidate-authority.ts @@ -13,8 +13,8 @@ export const CANDIDATE_QUALIFICATION_RECEIPT_DIGESTS: Readonly< Record > = Object.freeze({ pi: Object.freeze([ - "cc2d180d0a7f145e2320aa92ff00bf06a377c623375508f2704998f8b21c5f6e", - "96aaaefb99852685aa34cc76feb7e7deca4e18b80d818344ecab94f50e934e64", + "e4f525895c24fbb5321973a410140fe9c5c0f18cfa6f88f09742f5bedcdf13bb", + "e587ed7e09fa4f172d797ba86985c11588e977f9283a4734547a14c28d9f7e23", ]), }); diff --git a/src/lib/messaging/applier/build/messaging-build-applier.mts b/src/lib/messaging/applier/build/messaging-build-applier.mts index 8fa59a4a8b6..f4af22c6eb7 100755 --- a/src/lib/messaging/applier/build/messaging-build-applier.mts +++ b/src/lib/messaging/applier/build/messaging-build-applier.mts @@ -1386,6 +1386,35 @@ function runCommand( maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], }); + if ((result.error || result.status !== 0) && options.emitOutput !== false) { + const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`; + const knownCodes = new Set( + "ENOTCACHED EALLOWREMOTE EALLOWGIT ERESOLVE ETARGET E404 E403 E401 EINTEGRITY EBADPLATFORM EACCES EPERM ENOENT ENOSPC ETIMEDOUT".split( + " ", + ), + ); + const npmCodes = [...output.matchAll(/\bnpm (?:ERR!|error) code (E[A-Z0-9_]+)\b/g)] + .map((match) => match[1]!) + .filter((code) => knownCodes.has(code)); + // Emit only fixed status fields and public registry paths. Child output can + // contain credentials or configuration and must never be printed verbatim. + const registryPaths = [ + ...output.matchAll(/https:\/\/registry\.npmjs\.org(\/[@a-zA-Z0-9%._/-]+)/g), + ] + .map((match) => match[1]!) + .filter((value) => value.length <= 250); + console.error( + `Messaging build command diagnostic: ${JSON.stringify({ + command: args[0], + exitCode: result.status, + signal: result.signal, + errorCode: (result.error as NodeJS.ErrnoException | undefined)?.code ?? null, + npmCodes: [...new Set(npmCodes)], + timedOut: /\b(?:timed out|ETIMEDOUT|termination (?:no-output-)?timeout)\b/i.test(output), + registryPaths: [...new Set(registryPaths)].slice(0, 8), + })}`, + ); + } if ((result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT") throw new MessagingBuildCommandTimeoutError(); if (result.error) throw new MessagingBuildCommandError(); diff --git a/src/lib/messaging/channels/discord/manifest.ts b/src/lib/messaging/channels/discord/manifest.ts index 8aa64357885..a1401eb73f2 100644 --- a/src/lib/messaging/channels/discord/manifest.ts +++ b/src/lib/messaging/channels/discord/manifest.ts @@ -214,10 +214,13 @@ export const discordManifest = { spec: "npm:@openclaw/discord@{{openclaw.version}}", pin: true, integrityByVersion: { + "2026.9.2": + "sha512-j+fSHxbXA+DSxwbL8SvtsDNL6tvBX4+RmH+EerVW6dCbeIwSconXj6J/+AaN/mhzZI4g0jPPj30TUhdCRRbc2w==", "2026.9.1": "sha512-qNmN2a8A9dET4igPp0RML171sEn8PDMyNCYNp/DqcJ4tn3XTHpacSOTkqBmv5yXTycJRC9rfFP8FT/SdW0Rldg==", }, tarballUrlByVersion: { + "2026.9.2": "https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.2.tgz", "2026.9.1": "https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.1.tgz", }, required: true, diff --git a/src/lib/messaging/channels/googlechat/manifest.ts b/src/lib/messaging/channels/googlechat/manifest.ts index 308fda187df..f89ad3bb8be 100644 --- a/src/lib/messaging/channels/googlechat/manifest.ts +++ b/src/lib/messaging/channels/googlechat/manifest.ts @@ -348,10 +348,13 @@ export const googlechatManifest = { spec: "npm:@openclaw/googlechat@{{openclaw.version}}", pin: true, integrityByVersion: { + "2026.9.2": + "sha512-LUO8Lg07IhzJfEzxn+GSij8WMS/uX3hTmv0SydTy/0fKSN7iZksf74TaZg50kOoBi8FzeqeXo/zoGVuk0Mj1Ig==", "2026.9.1": "sha512-Q5VTAJpfcrI7BSEw5Ugq3wf7JEg5QhTBwpi+BByGbfZsTTVjwZc7OIvNbKsVTh16I5/EWqHEnD+0WNeHqsteqw==", }, tarballUrlByVersion: { + "2026.9.2": "https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.2.tgz", "2026.9.1": "https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.1.tgz", }, required: true, diff --git a/src/lib/messaging/channels/metadata.test.ts b/src/lib/messaging/channels/metadata.test.ts index 1690c8a826f..55f606efed7 100644 --- a/src/lib/messaging/channels/metadata.test.ts +++ b/src/lib/messaging/channels/metadata.test.ts @@ -304,7 +304,7 @@ describe("built-in messaging channel metadata", () => { { packageKey: "discord/openclawPluginPackage", committedIntegrity: - "sha512-qNmN2a8A9dET4igPp0RML171sEn8PDMyNCYNp/DqcJ4tn3XTHpacSOTkqBmv5yXTycJRC9rfFP8FT/SdW0Rldg==", + "sha512-j+fSHxbXA+DSxwbL8SvtsDNL6tvBX4+RmH+EerVW6dCbeIwSconXj6J/+AaN/mhzZI4g0jPPj30TUhdCRRbc2w==", }, { packageKey: "wechat/openclawPluginPackage", @@ -314,22 +314,22 @@ describe("built-in messaging channel metadata", () => { { packageKey: "slack/openclawPluginPackage", committedIntegrity: - "sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg==", + "sha512-6M1M6gL3iXahpalNsYAUuA+wvnV8lbMlNNH2ToegFvaSJcIll4S9kFa5mv3GFoquhMRHQjEjnkXPHP/pXwaWcA==", }, { packageKey: "whatsapp/openclawPluginPackage", committedIntegrity: - "sha512-llIcoMa6FM4SgYn7GG1FQIeTTA5JDdcHW5D7PT+3aGYT3/E2eLFutKwDvD/w7G0hvDwSftzZgLi3iA8dzK7a3A==", + "sha512-vOWQIk7FpLHrhMmO+FaLi+pnFB82hiWNJJFJONkBuofERh2SMEz7EMut/vECFFEjFnmOZSVlYfRlxhbNkd/R6g==", }, { packageKey: "teams/openclawPluginPackage", committedIntegrity: - "sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong==", + "sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og==", }, { packageKey: "googlechat/openclawPluginPackage", committedIntegrity: - "sha512-Q5VTAJpfcrI7BSEw5Ugq3wf7JEg5QhTBwpi+BByGbfZsTTVjwZc7OIvNbKsVTh16I5/EWqHEnD+0WNeHqsteqw==", + "sha512-LUO8Lg07IhzJfEzxn+GSij8WMS/uX3hTmv0SydTy/0fKSN7iZksf74TaZg50kOoBi8FzeqeXo/zoGVuk0Mj1Ig==", }, ]); }); diff --git a/src/lib/messaging/channels/slack/manifest.ts b/src/lib/messaging/channels/slack/manifest.ts index 56404042929..c1a5f069b51 100644 --- a/src/lib/messaging/channels/slack/manifest.ts +++ b/src/lib/messaging/channels/slack/manifest.ts @@ -185,10 +185,13 @@ export const slackManifest = { spec: "npm:@openclaw/slack@{{openclaw.version}}", pin: true, integrityByVersion: { + "2026.9.2": + "sha512-6M1M6gL3iXahpalNsYAUuA+wvnV8lbMlNNH2ToegFvaSJcIll4S9kFa5mv3GFoquhMRHQjEjnkXPHP/pXwaWcA==", "2026.9.1": "sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg==", }, tarballUrlByVersion: { + "2026.9.2": "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.2.tgz", "2026.9.1": "https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.1.tgz", }, required: true, diff --git a/src/lib/messaging/channels/teams/manifest.ts b/src/lib/messaging/channels/teams/manifest.ts index 65575cded53..3e37bf6eb0d 100644 --- a/src/lib/messaging/channels/teams/manifest.ts +++ b/src/lib/messaging/channels/teams/manifest.ts @@ -222,10 +222,13 @@ export const teamsManifest = { spec: "npm:@openclaw/msteams@{{openclaw.version}}", pin: true, integrityByVersion: { + "2026.9.2": + "sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og==", "2026.9.1": "sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong==", }, tarballUrlByVersion: { + "2026.9.2": "https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.2.tgz", "2026.9.1": "https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.1.tgz", }, required: true, diff --git a/src/lib/messaging/channels/whatsapp/manifest.ts b/src/lib/messaging/channels/whatsapp/manifest.ts index 8327b2484e8..66389bafd80 100644 --- a/src/lib/messaging/channels/whatsapp/manifest.ts +++ b/src/lib/messaging/channels/whatsapp/manifest.ts @@ -129,10 +129,13 @@ export const whatsappManifest = { spec: "npm:@openclaw/whatsapp@{{openclaw.version}}", pin: true, integrityByVersion: { + "2026.9.2": + "sha512-vOWQIk7FpLHrhMmO+FaLi+pnFB82hiWNJJFJONkBuofERh2SMEz7EMut/vECFFEjFnmOZSVlYfRlxhbNkd/R6g==", "2026.9.1": "sha512-llIcoMa6FM4SgYn7GG1FQIeTTA5JDdcHW5D7PT+3aGYT3/E2eLFutKwDvD/w7G0hvDwSftzZgLi3iA8dzK7a3A==", }, tarballUrlByVersion: { + "2026.9.2": "https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.2.tgz", "2026.9.1": "https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.1.tgz", }, required: true, diff --git a/src/lib/onboard/dockerfile-remote-dashboard-bind-contract.ts b/src/lib/onboard/dockerfile-remote-dashboard-bind-contract.ts index 7696f94313e..509ff34ef75 100644 --- a/src/lib/onboard/dockerfile-remote-dashboard-bind-contract.ts +++ b/src/lib/onboard/dockerfile-remote-dashboard-bind-contract.ts @@ -69,7 +69,8 @@ const CANONICAL_POST_GENERATOR_INSTRUCTION_SHA256 = new Set([ "c682148fc7efec9f947c326c6029181cd879b7cba3e8361246aba7d0e6fe70a3", "2801e488822e10a39a5586bd150279e54df4612e30c2fa782453534a466def59", "8f0861e48c0cec37faa662fccd130ab21f972ac3ed2a0ce5f4e5a1e9ec223130", - "6364b77bae0a2a4449737beefac36c439333a5e37993ac404c02e375aa170515", + // Security inventory verification with matching Debian OpenSSL u3 pins. + "8d0214ab5fec6f6c255e5177ba91c579b36be81ae98c4c5301c483effd750d30", // Sandbox-user native OpenClaw state modes with root-mode shared access; // this exact instruction changes filesystem metadata, not dashboard config. "402ffef36760a20e70316a145fa37908c99774496d3dcd0da5f8547b9ac80071", @@ -88,6 +89,8 @@ const CANONICAL_POST_GENERATOR_INSTRUCTION_SHA256 = new Set([ // The same reviewed install with npm forced offline for every optional // plugin command; it still preserves the generated dashboard config. "a72a06b293274fb997f5a4b8b1c61cf3daa8a7cc4b8385baa0d9dc63400b8d52", + // The same offline optional-plugin install with reviewed 2026.9.2 pins. + "8754faf5ce97000259b81e36ec447e9fd13051260a8be1f5018e5db11d6414b9", // Reviewed local NemoClaw plugin installation with explicit capability // acceptance; the following inspect and pruning steps are unchanged. "464abc5ff104c8bdeae57e6fdb775b7bda7dd756cba0dd1a7752b7d03e8f8372", diff --git a/src/lib/onboard/machine/final-flow-phases.ts b/src/lib/onboard/machine/final-flow-phases.ts index d3e298b588e..fa44d434d70 100644 --- a/src/lib/onboard/machine/final-flow-phases.ts +++ b/src/lib/onboard/machine/final-flow-phases.ts @@ -64,7 +64,7 @@ export function shouldInitializeNativeOpenclawInferenceRoute( return ( context.agent === null && (context.fromDockerfile !== null || Boolean(context.session?.metadata?.fromImage)) && - !preserveRebuildLivePolicy && + (!preserveRebuildLivePolicy || Boolean(context.session?.metadata?.fromImage)) && context.session?.steps.openclaw?.status !== "complete" ); } diff --git a/src/lib/onboard/machine/handlers/agent-setup.ts b/src/lib/onboard/machine/handlers/agent-setup.ts index ee4b24aab90..a9e9cce6b62 100644 --- a/src/lib/onboard/machine/handlers/agent-setup.ts +++ b/src/lib/onboard/machine/handlers/agent-setup.ts @@ -104,6 +104,14 @@ export async function handleAgentSetupState({ const agentSetupContext = deps.agentSetupContext(); const initializeOpenclawInferenceRoute = async (): Promise => { if (!initializeNativeInferenceRoute) return; + if ( + settleOpenclawStartupBeforeConfiguration && + !(await deps.settleStartedOpenclawGatewayForConfiguration(sandboxName)) + ) { + throw new Error( + `External-image OpenClaw pairing did not settle after configuration for sandbox '${sandboxName}'.`, + ); + } await (deps.initializeOpenclawInferenceRoute ?? initializeDefaultOpenclawInferenceRoute)( sandboxName, model, diff --git a/src/lib/onboard/managed-startup-profile-release-contract.test.ts b/src/lib/onboard/managed-startup-profile-release-contract.test.ts index cbd9a912088..6a98417fd4c 100644 --- a/src/lib/onboard/managed-startup-profile-release-contract.test.ts +++ b/src/lib/onboard/managed-startup-profile-release-contract.test.ts @@ -35,14 +35,14 @@ describe("managed startup profile release contract", () => { it("tracks the active OpenClaw release pins outside runtime startup intent", () => { expect(MANAGED_STARTUP_PROFILE_EXCLUDED_DOCKER_INPUTS.openclaw).toEqual( expect.arrayContaining([ - { input: "OPENCLAW_2026_9_1_INTEGRITY", reason: "integrity-pin" }, - { input: "OPENCLAW_2026_9_1_TARBALL", reason: "release-composition" }, + { input: "OPENCLAW_2026_9_2_INTEGRITY", reason: "integrity-pin" }, + { input: "OPENCLAW_2026_9_2_TARBALL", reason: "release-composition" }, { - input: "OPENCLAW_DIAGNOSTICS_OTEL_2026_9_1_INTEGRITY", + input: "OPENCLAW_DIAGNOSTICS_OTEL_2026_9_2_INTEGRITY", reason: "integrity-pin", }, { - input: "OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY", + input: "OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY", reason: "integrity-pin", }, ]), diff --git a/src/lib/onboard/managed-startup/profile.ts b/src/lib/onboard/managed-startup/profile.ts index 55bf51834d9..517d8a30dfe 100644 --- a/src/lib/onboard/managed-startup/profile.ts +++ b/src/lib/onboard/managed-startup/profile.ts @@ -807,14 +807,14 @@ export const MANAGED_STARTUP_PROFILE_EXCLUDED_DOCKER_INPUTS = { openclaw: [ { input: "BASE_IMAGE", reason: "release-composition" }, { input: "OPENCLAW_VERSION", reason: "release-composition" }, - { input: "OPENCLAW_2026_9_1_INTEGRITY", reason: "integrity-pin" }, - { input: "OPENCLAW_2026_9_1_TARBALL", reason: "release-composition" }, + { input: "OPENCLAW_2026_9_2_INTEGRITY", reason: "integrity-pin" }, + { input: "OPENCLAW_2026_9_2_TARBALL", reason: "release-composition" }, { - input: "OPENCLAW_DIAGNOSTICS_OTEL_2026_9_1_INTEGRITY", + input: "OPENCLAW_DIAGNOSTICS_OTEL_2026_9_2_INTEGRITY", reason: "integrity-pin", }, { - input: "OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY", + input: "OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY", reason: "integrity-pin", }, { diff --git a/src/lib/onboard/openclaw/initial-inference-route.ts b/src/lib/onboard/openclaw/initial-inference-route.ts index b757f7323b4..b2de3a7b872 100644 --- a/src/lib/onboard/openclaw/initial-inference-route.ts +++ b/src/lib/onboard/openclaw/initial-inference-route.ts @@ -11,12 +11,6 @@ const initialOpenclawInferenceRouteRuntime = { loadSandboxConfig: () => require("../../sandbox/config") as typeof import("../../sandbox/config"), loadFinalizationDeps: () => require("../machine/finalization-deps") as typeof import("../machine/finalization-deps"), - loadRegistry: () => - require("../../state/registry/persistence") as typeof import("../../state/registry/persistence"), - loadLifecycleLock: () => - require("../../actions/sandbox/lifecycle/lock") as typeof import("../../actions/sandbox/lifecycle/lock"), - loadOpenShellLifecycle: () => - require("../../adapters/openshell/sandbox-lifecycle-sdk") as typeof import("../../adapters/openshell/sandbox-lifecycle-sdk"), }; export interface InitialOpenclawInferenceRouteDeps { @@ -59,58 +53,6 @@ export type InitializeOpenclawInferenceRoute = ( revalidateSandboxIdentity?: (operation: string) => void, ) => Promise; -async function restartInitialExternalOpenclawSandbox( - sandboxName: string, - gatewayName: string, - expectedIdentity: string | undefined, -): ReturnType { - const runtime = initialOpenclawInferenceRouteRuntime; - return runtime.loadLifecycleLock().withSandboxLifecycleLock(sandboxName, async () => { - const sandbox = runtime.loadRegistry().load().sandboxes[sandboxName]; - if ( - sandbox?.workload?.kind !== "external-image" || - sandbox.openshellDriver !== "docker" || - !expectedIdentity || - sandbox.lifecycleLiveIdentityFingerprint !== expectedIdentity || - (sandbox.gatewayName && sandbox.gatewayName !== gatewayName) - ) { - return { - ok: false as const, - failureLayer: "OpenShell lifecycle identity", - detail: "External-image sandbox ownership changed before initial restart.", - }; - } - const lifecycle = runtime.loadOpenShellLifecycle().createSdkOpenShellSandboxStateLifecycle(); - const request = { - sandboxName, - sandboxIdentityFingerprint: expectedIdentity, - target: { kind: "named" as const, gatewayName }, - }; - // Initial setup must not require an administrative grant for the agent's CLI device. - // OpenShell owns the sandbox lifecycle and verifies the same identity at both transitions. - for (const action of ["stop", "start"] as const) { - const result = await lifecycle[`${action}Sandbox`](request); - if (result.kind === "failed") { - return { - ok: false as const, - failureLayer: "OpenShell lifecycle", - detail: `OpenShell ${action} failed: ${result.error.message}`, - }; - } - } - const ready = await runtime - .loadFinalizationDeps() - .finalizationHandlerDeps.waitForStartedOpenclawGatewayProcess(sandboxName, gatewayName); - return ready === true - ? { ok: true as const } - : { - ok: false as const, - failureLayer: "native gateway startup", - detail: "OpenClaw did not become ready after the initial OpenShell restart.", - }; - }); -} - export function createInitialOpenclawInferenceRoute( deps: InitialOpenclawInferenceRouteDeps, ): InitializeOpenclawInferenceRoute { @@ -171,18 +113,8 @@ export const initializeOpenclawInferenceRoute = createInitialOpenclawInferenceRo gatewayName, ); }, - restartNativeGateway: (sandboxName, gatewayName) => { - const sandbox = initialOpenclawInferenceRouteRuntime.loadRegistry().load().sandboxes[ - sandboxName - ]; - return sandbox?.workload?.kind === "external-image" - ? restartInitialExternalOpenclawSandbox( - sandboxName, - gatewayName, - sandbox.lifecycleLiveIdentityFingerprint, - ) - : initialOpenclawInferenceRouteRuntime - .loadFinalizationDeps() - .restartNativeGatewayForInitialSetup(sandboxName, gatewayName); - }, + restartNativeGateway: (sandboxName, gatewayName) => + initialOpenclawInferenceRouteRuntime + .loadFinalizationDeps() + .restartNativeGatewayForInitialSetup(sandboxName, gatewayName), }); diff --git a/src/lib/onboard/sandbox-workload-authority.test.ts b/src/lib/onboard/sandbox-workload-authority.test.ts index 56d0c77850c..3b5fc72b955 100644 --- a/src/lib/onboard/sandbox-workload-authority.test.ts +++ b/src/lib/onboard/sandbox-workload-authority.test.ts @@ -96,6 +96,42 @@ describe("managed workload authority", () => { ).toBeNull(); }); + it.each(["openclaw", "hermes"] as const)( + "preserves explicit external %s adoption only with a valid matching receipt", + (agent) => { + const reference = `${MANAGED_IMAGE_REPOSITORIES[agent]}@sha256:${"a".repeat(64)}`; + const external = { + agent, + imageTag: reference, + fromDockerfile: null, + workload: { + schemaVersion: 1, + kind: "external-image", + reference, + platform: "linux/amd64", + runtimeImageContentId: `sha256:${"b".repeat(64)}`, + shared: true, + }, + } as const; + expect(readManagedWorkloadAuthority(external)).toBeNull(); + expect(() => + readManagedWorkloadAuthority({ + ...external, + imageTag: `${MANAGED_IMAGE_REPOSITORIES[agent]}@sha256:${"c".repeat(64)}`, + }), + ).toThrow(ManagedWorkloadAuthorityError); + expect(() => + readManagedWorkloadAuthority({ ...external, fromDockerfile: "/tmp/Dockerfile" }), + ).toThrow(ManagedWorkloadAuthorityError); + expect(() => + readManagedWorkloadAuthority({ + ...external, + workload: { ...external.workload, runtimeImageContentId: "invalid" }, + }), + ).toThrow(ManagedWorkloadAuthorityError); + }, + ); + it("rejects missing explicit agent identity", () => { expect(() => readManagedWorkloadAuthority({ diff --git a/src/lib/onboard/workload/authority.ts b/src/lib/onboard/workload/authority.ts index e157fd329f1..7d294528071 100644 --- a/src/lib/onboard/workload/authority.ts +++ b/src/lib/onboard/workload/authority.ts @@ -137,6 +137,16 @@ function corporateCaFromReceipt( export function readManagedWorkloadAuthority( entry: Pick, ): ManagedWorkloadAuthority | null { + // Explicit external adoption remains external even in a managed-image repository. + if (entry.workload?.kind === "external-image") { + const external = cloneSandboxWorkloadReceipt(entry.workload); + if ( + external?.kind === "external-image" && + entry.imageTag === external.reference && + entry.fromDockerfile == null + ) + return null; + } const managedLooking = isManagedImageReference(entry.imageTag) || entry.workload?.kind === "managed-image"; if (!managedLooking) return null; diff --git a/src/lib/sandbox-base-image/security-inventory.ts b/src/lib/sandbox-base-image/security-inventory.ts index c41a58aceb7..4a5bd6483b8 100644 --- a/src/lib/sandbox-base-image/security-inventory.ts +++ b/src/lib/sandbox-base-image/security-inventory.ts @@ -17,7 +17,7 @@ export const SANDBOX_BASE_SECURITY_PACKAGE_INVENTORY = [ "vim-common=2:9.2.0858-1", "vim-tiny=2:9.2.0858-1", "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2", - "libssl3t64=3.5.7-1~deb13u2", + "libssl3t64=3.5.7-1~deb13u3", CURRENT_PYTHON_HTMLPARSER_FIX_PACKAGE, "perl-base=5.44.0-1nemoclaw1", "perl=5.44.0-1nemoclaw1", diff --git a/test/agents/openclaw/openclaw-container-restart-patch.test.ts b/test/agents/openclaw/openclaw-container-restart-patch.test.ts index 039b8579017..a7f4937bf88 100644 --- a/test/agents/openclaw/openclaw-container-restart-patch.test.ts +++ b/test/agents/openclaw/openclaw-container-restart-patch.test.ts @@ -11,6 +11,7 @@ import { describe, expect, it, vi } from "vitest"; import { patchContainerRestart, + patchHostSafeRestart, patchOpenClawContainerRestart, } from "../../../scripts/lib/patch-openclaw-container-restart.mts"; @@ -18,6 +19,15 @@ const nativeRestart = fs.readFileSync( path.join(import.meta.dirname, "fixtures/gateway-container-restart.js.txt"), "utf8", ); +const nativeSafeRestart = `async function runSafeGatewayRestart(opts, target) { +\tconst params = { target, skipDeferral: opts.skipDeferral }; +\tconst result = await callGatewayCli({ +\t\tmethod: "gateway.restart.request", +\t\tparams, +\t\ttimeoutMs: 1e4 +\t}); +\treturn result; +}`; function restartHarness( options: { sandbox?: boolean; @@ -59,6 +69,56 @@ function restartHarness( } describe("OpenClaw sandbox restart patch", () => { + it.each([ + [{ OPENSHELL_SANDBOX: "1", NEMOCLAW_OPENCLAW_HOST_RESTART: "1" }, true], + [{ OPENSHELL_SANDBOX: "1" }, false], + [{ NEMOCLAW_OPENCLAW_HOST_RESTART: "1" }, false], + ] as const)( + "limits backend authentication to an explicit sandbox host restart: %j", + async (env, hostRestart) => { + const restart = vm.runInNewContext( + `${patchHostSafeRestart(nativeSafeRestart)}; runSafeGatewayRestart`, + { + process: { env }, + callGatewayCli: async (opts: unknown) => opts, + }, + ); + const target = { pid: 123, ownerId: "native-owner", port: 18791 }; + expect(await restart({ skipDeferral: true }, target)).toEqual({ + method: "gateway.restart.request", + params: { target, skipDeferral: true }, + timeoutMs: 10000, + ...(hostRestart + ? { + clientName: "gateway-client", + mode: "backend", + requireLocalBackendSharedAuth: true, + sharedStateMode: "read-only", + } + : {}), + }); + }, + ); + it("audits the host restart adaptation and rejects native drift", () => { + const patched = patchHostSafeRestart(nativeSafeRestart); + expect(patchHostSafeRestart(patched)).toBe(patched); + expect(() => + patchHostSafeRestart( + patched.replace( + "requireLocalBackendSharedAuth: true", + "requireLocalBackendSharedAuth: false", + ), + ), + ).toThrow("Incomplete"); + expect(() => + patchHostSafeRestart( + nativeSafeRestart.replace('"gateway.restart.request"', '"different.method"'), + ), + ).toThrow("Unrecognized"); + expect(() => patchHostSafeRestart(nativeSafeRestart + nativeSafeRestart)).toThrow( + "Expected one", + ); + }); it.each([true, false])( "replaces the OpenShell process when generic container detection is %s", (container) => { @@ -143,6 +203,7 @@ describe("OpenClaw sandbox restart patch", () => { fs.mkdirSync(path.join(dist, "cli"), { recursive: true }); const target = path.join(dist, "cli/gateway-lifecycle.runtime.js"); fs.writeFileSync(target, nativeRestart); + fs.writeFileSync(path.join(dist, "lifecycle-fixture.js"), nativeSafeRestart); fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "unknown" })); expect(() => patchOpenClawContainerRestart(dist)).toThrow("Unsupported"); expect(fs.readFileSync(target, "utf8")).toBe(nativeRestart); diff --git a/test/agents/openclaw/openclaw-dependency-review.test.ts b/test/agents/openclaw/openclaw-dependency-review.test.ts index b68d41eb3ec..edbfda42972 100644 --- a/test/agents/openclaw/openclaw-dependency-review.test.ts +++ b/test/agents/openclaw/openclaw-dependency-review.test.ts @@ -12,7 +12,7 @@ import { readYaml, type WorkflowJob, type WorkflowStep } from "../../helpers/e2e const REPO_ROOT = path.join(import.meta.dirname, "../../.."); const CODEX_ACP_TARBALL = "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.11.1.tgz"; -const OPENCLAW_TARBALL = "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz"; +const OPENCLAW_TARBALL = "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz"; const MESSAGING_BUILD_APPLIER = path.join( REPO_ROOT, "src", @@ -127,7 +127,7 @@ function runBaseImageBuildArgGuard( } } -describe("OpenClaw 2026.9.1 dependency review contract", () => { +describe("OpenClaw 2026.9.2 dependency review contract", () => { it("keeps every reviewed archive boundary on the shared invariant matrix (#5896)", () => { const result = spawnSync( "bash", @@ -179,8 +179,8 @@ for dockerfile in Dockerfile Dockerfile.base; do Dockerfile) end_marker='# Patch OpenClaw media fetch' ;; Dockerfile.base) end_marker='# Baseline health check.' ;; esac - openclaw_block="$(sed -n "/ARG OPENCLAW_VERSION=2026.9.1/,/$end_marker/p" "$dockerfile")" - check_contains "$openclaw_block" "ARG OPENCLAW_2026_9_1_TARBALL=${OPENCLAW_TARBALL}" "$dockerfile tarball arg" + openclaw_block="$(sed -n "/ARG OPENCLAW_VERSION=2026.9.2/,/$end_marker/p" "$dockerfile")" + check_contains "$openclaw_block" "ARG OPENCLAW_2026_9_2_TARBALL=${OPENCLAW_TARBALL}" "$dockerfile tarball arg" check_contains "$openclaw_block" '/scripts/lib/reviewed-npm-archive.mts' "$dockerfile shared helper" check_contains "$openclaw_block" '--package-spec "openclaw@\${OPENCLAW_VERSION}" --integrity "$EXPECTED_INTEGRITY"' "$dockerfile reviewed identity" check_contains "$openclaw_block" '--tarball-url "$EXPECTED_TARBALL"' "$dockerfile reviewed tarball" diff --git a/test/agents/openclaw/openclaw-device-self-approval-patch-upgrade.test.ts b/test/agents/openclaw/openclaw-device-self-approval-patch-upgrade.test.ts index c5178cf7cfc..6cec0f9248e 100644 --- a/test/agents/openclaw/openclaw-device-self-approval-patch-upgrade.test.ts +++ b/test/agents/openclaw/openclaw-device-self-approval-patch-upgrade.test.ts @@ -286,7 +286,10 @@ describe("OpenClaw device self-approval patch upgrades (#4462)", () => { } }); - it("adds watcher deferral to an earlier patched current gateway runtime (#9844)", () => { + it.each([ + ["2026.9.1", { allowSilentLocalPairing: true }], + ["2026.9.2", { localApproval: "silent" }], + ] as const)("adds watcher deferral to the %s gateway runtime (#9844)", (_version, plan) => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-device-defer-upgrade-")); const dist = path.join(tmp, "dist"); fs.mkdirSync(dist); @@ -324,8 +327,14 @@ describe("OpenClaw device self-approval patch upgrades (#4462)", () => { const resolvePairingOutcome = vm.runInNewContext(`${upgraded}\nresolvePairingOutcome`) as ( input: Record, ) => "approved" | "pending"; - const boundedUpgrade = boundedScopeUpgrade(); + const boundedUpgrade = { ...boundedScopeUpgrade(), plan }; expect(resolvePairingOutcome(boundedUpgrade)).toBe("pending"); + expect( + resolvePairingOutcome({ + ...boundedUpgrade, + plan: { localApproval: "trusted-cidr" }, + }), + ).toBe("approved"); expect( resolvePairingOutcome({ ...boundedUpgrade, diff --git a/test/agents/openclaw/openclaw-device-self-approval-patch.test.ts b/test/agents/openclaw/openclaw-device-self-approval-patch.test.ts index e4df6f330be..df38d88ad0c 100644 --- a/test/agents/openclaw/openclaw-device-self-approval-patch.test.ts +++ b/test/agents/openclaw/openclaw-device-self-approval-patch.test.ts @@ -860,7 +860,12 @@ describe("OpenClaw bounded device self-approval patch (#4462)", () => { } }); - it("passes authenticated identity for a pre-convergence write request to the canonical approver", async () => { + it.each([ + ["legacy token", { token: "token-before" }], + ["explicit device token", { deviceToken: "token-before" }], + ["explicit token priority", { token: "other-token", deviceToken: "token-before" }], + ["empty explicit token", { token: "token-before", deviceToken: " " }], + ])("passes authenticated identity using %s to the canonical approver", async (_label, auth) => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-device-handler-")); const dist = path.join(tmp, "dist"); fs.mkdirSync(dist); @@ -878,7 +883,7 @@ describe("OpenClaw bounded device self-approval patch (#4462)", () => { const broadcasts: unknown[] = []; await runtime.deviceHandlers["device.pair.approve"]({ params: { requestId: "request-1" }, - client: validClient(), + client: validClient({ connect: { ...validClient().connect, auth } }), respond: (...args: unknown[]) => responses.push(args), context: { logGateway: { warn() {}, info() {} }, @@ -910,6 +915,13 @@ describe("OpenClaw bounded device self-approval patch (#4462)", () => { it.each([ ["shared auth", validClient({ isDeviceTokenAuth: false })], + [ + "shared auth with explicit device token", + validClient({ + isDeviceTokenAuth: false, + connect: { ...validClient().connect, auth: { deviceToken: "token-before" } }, + }), + ], [ "missing caller identity", validClient({ diff --git a/test/agents/openclaw/openclaw-integrity-pin-suite.ts b/test/agents/openclaw/openclaw-integrity-pin-suite.ts index ef68bf26b09..b87075a5c1b 100644 --- a/test/agents/openclaw/openclaw-integrity-pin-suite.ts +++ b/test/agents/openclaw/openclaw-integrity-pin-suite.ts @@ -37,11 +37,11 @@ const REVIEWED_NPM_ARCHIVE_HELPER = path.join( ); const OPENCLAW_VERSION_EXTRACTOR = path.join(REPO_ROOT, "scripts", "extract-semver.sh"); const REVIEWED_NPM_AUDIT_HELPER = path.join(REPO_ROOT, "scripts", "lib", "reviewed-npm-audit.mts"); -const UNPINNED_OPENCLAW_VERSION = "2026.9.2"; -const PINNED_OPENCLAW_VERSION = "2026.9.1"; +const UNPINNED_OPENCLAW_VERSION = "2026.9.3"; +const PINNED_OPENCLAW_VERSION = "2026.9.2"; const PINNED_OPENCLAW_INTEGRITY = - "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w=="; -const PINNED_OPENCLAW_TARBALL = "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz"; + "sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg=="; +const PINNED_OPENCLAW_TARBALL = "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz"; const OPENCLAW_RUNTIME_LOCKFILE = path.join( REPO_ROOT, "agents", @@ -104,17 +104,17 @@ function requiredDockerArg(name: string): string { } const PINNED_OPENCLAW_DIAGNOSTICS_OTEL_INTEGRITY = - "sha512-3MWLli9L6HTVdrjqHmwOvNvIr6emsnuNQe4iE2sDqb8E5wn4Vq1rcsz+InL1YFudbStr089ZtS0tNAQ6qU+tnA=="; + "sha512-yilG4G1Fd1yvW65Qy+qNPR+x6tBjwVmTWv+7BULoN70HY3BJqt9YVOL42PvWXHYpaTs/LwUlisqxjbJRfYyi9A=="; const PINNED_OPENCLAW_DIAGNOSTICS_OTEL_TARBALL = - "https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.1.tgz"; + "https://registry.npmjs.org/@openclaw/diagnostics-otel/-/diagnostics-otel-2026.9.2.tgz"; const PINNED_OPENCLAW_BRAVE_PLUGIN_INTEGRITY = - "sha512-4+j+eQTToV3k7Cb25MUL6h2uL8cJYyuLytfpd/sJK/HjR43dgKBqKpBsb1+I3w1Jr6PLpnjSf6/I3//3K0cdnA=="; + "sha512-6416aPlfnAKlu8IBrrjgfoiss/10xB32ywFwnIf/fkVMQE61qsmzA/qxUniQuDwOB6EBFNEkNs54DhIT7g3UVg=="; const PINNED_OPENCLAW_BRAVE_PLUGIN_TARBALL = - "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.1.tgz"; + "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.2.tgz"; const PINNED_OPENCLAW_SLACK_INTEGRITY = - "sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg=="; + "sha512-6M1M6gL3iXahpalNsYAUuA+wvnV8lbMlNNH2ToegFvaSJcIll4S9kFa5mv3GFoquhMRHQjEjnkXPHP/pXwaWcA=="; const PINNED_OPENCLAW_MSTEAMS_INTEGRITY = - "sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong=="; + "sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og=="; const LEGACY_REBUILD_OPENCLAW_VERSION = "2026.3.11"; const LEGACY_REBUILD_OPENCLAW_INTEGRITY = "sha512-bxwiBmHPakwfpY5tqC9lrV5TCu5PKf0c1bHNc3nhrb+pqKcPEWV4zOjDVFLQUHr98ihgWA+3pacy4b3LQ8wduQ=="; @@ -370,8 +370,8 @@ function runInstallBlock( `BASE_IMAGE=${JSON.stringify(baseImage)}`, `openclaw_provenance_path=${JSON.stringify(provenancePath)}`, `openclaw_provenance_metadata=${JSON.stringify(baseProvenanceMetadata)}`, - `OPENCLAW_2026_9_1_INTEGRITY=${JSON.stringify(committedIntegrity)}`, - `OPENCLAW_2026_9_1_TARBALL=${JSON.stringify(PINNED_OPENCLAW_TARBALL)}`, + `OPENCLAW_2026_9_2_INTEGRITY=${JSON.stringify(committedIntegrity)}`, + `OPENCLAW_2026_9_2_TARBALL=${JSON.stringify(PINNED_OPENCLAW_TARBALL)}`, `NEMOCLAW_E2E_FIXTURE_LEGACY_OPENCLAW=${allowLegacyFixture ? "1" : "0"}`, `OPENCLAW_2026_3_11_INTEGRITY=${JSON.stringify(LEGACY_REBUILD_OPENCLAW_INTEGRITY)}`, `OPENCLAW_2026_3_11_TARBALL=${JSON.stringify(LEGACY_REBUILD_OPENCLAW_TARBALL)}`, @@ -606,8 +606,8 @@ function runOptionalOpenClawPluginBlock( "set -euo pipefail", `call_log=${JSON.stringify(log)}`, `OPENCLAW_VERSION=${JSON.stringify(openclawVersion)}`, - `OPENCLAW_DIAGNOSTICS_OTEL_2026_9_1_INTEGRITY=${JSON.stringify(PINNED_OPENCLAW_DIAGNOSTICS_OTEL_INTEGRITY)}`, - `OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY=${JSON.stringify(PINNED_OPENCLAW_BRAVE_PLUGIN_INTEGRITY)}`, + `OPENCLAW_DIAGNOSTICS_OTEL_2026_9_2_INTEGRITY=${JSON.stringify(PINNED_OPENCLAW_DIAGNOSTICS_OTEL_INTEGRITY)}`, + `OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY=${JSON.stringify(PINNED_OPENCLAW_BRAVE_PLUGIN_INTEGRITY)}`, `NEMOCLAW_OPENCLAW_OTEL=${otel ? "1" : "0"}`, `NEMOCLAW_WEB_SEARCH_ENABLED=${webSearch ? "1" : "0"}`, `export NEMOCLAW_REVIEWED_NPM_EXECUTABLE=${JSON.stringify(reviewedNpmExecutable)}`, @@ -710,7 +710,7 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes ).toBe(false); }); - it("keeps the Teams OpenClaw plugin manifest pinned to the reviewed 2026.9.1 integrity", () => { + it("keeps the Teams OpenClaw plugin manifest pinned to the reviewed 2026.9.2 integrity", () => { const teamsManifest = createBuiltInChannelManifestRegistry().get("teams"); const teamsPackage = teamsManifest?.agentPackages?.find( (agentPackage) => @@ -797,10 +797,10 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes if (group === "plugin-install") { it("verifies optional non-messaging OpenClaw plugin integrity before install", () => { - expect(requiredDockerArg("OPENCLAW_DIAGNOSTICS_OTEL_2026_9_1_INTEGRITY")).toBe( + expect(requiredDockerArg("OPENCLAW_DIAGNOSTICS_OTEL_2026_9_2_INTEGRITY")).toBe( PINNED_OPENCLAW_DIAGNOSTICS_OTEL_INTEGRITY, ); - expect(requiredDockerArg("OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY")).toBe( + expect(requiredDockerArg("OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY")).toBe( PINNED_OPENCLAW_BRAVE_PLUGIN_INTEGRITY, ); const { result, calls } = runOptionalOpenClawPluginBlock(); @@ -816,7 +816,7 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes `npm pack @openclaw/diagnostics-otel@${PINNED_OPENCLAW_VERSION} --pack-destination`, ); expect(calls).toMatch( - /openclaw plugins install --force --accept-capabilities npm-pack:\S*\/diagnostics-otel-2026\.9\.1\.tgz\n/, + /openclaw plugins install --force --accept-capabilities npm-pack:\S*\/diagnostics-otel-2026\.9\.2\.tgz\n/, ); expect(calls).not.toContain(`remediate --archive`); expect(calls).toContain( @@ -829,7 +829,7 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes `npm pack @openclaw/brave-plugin@${PINNED_OPENCLAW_VERSION} --pack-destination`, ); expect(calls).toMatch( - /openclaw plugins install --force --accept-capabilities npm-pack:\S*\/brave-plugin-2026\.9\.1\.tgz\n/, + /openclaw plugins install --force --accept-capabilities npm-pack:\S*\/brave-plugin-2026\.9\.2\.tgz\n/, ); expect(calls).toContain("openclaw-env true true"); }); @@ -856,7 +856,7 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes it("fails closed before optional OpenClaw plugin install when the registry tarball URL drifts", () => { const driftedTarball = - "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.2.tgz"; + "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.3.tgz"; const { result, calls } = runOptionalOpenClawPluginBlock({ otel: false, braveRegistryTarball: driftedTarball, @@ -1345,7 +1345,7 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes }, ); const optionalPlugin = runOptionalOpenClawPluginBlock({ - pluginPackFilename: "../diagnostics-otel-2026.9.1.tgz", + pluginPackFilename: "../diagnostics-otel-2026.9.2.tgz", }); for (const item of [ @@ -1358,7 +1358,7 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes { label: "optional OpenClaw plugin Dockerfile", outcome: optionalPlugin, - unsafeFilename: "../diagnostics-otel-2026.9.1.tgz", + unsafeFilename: "../diagnostics-otel-2026.9.2.tgz", blockedCommand: "openclaw plugins install", }, ]) { @@ -1573,10 +1573,10 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes "OPENCLAW_2026_3_11_TARBALL", "OPENCLAW_2026_4_24_INTEGRITY", "OPENCLAW_2026_4_24_TARBALL", - "OPENCLAW_2026_9_1_INTEGRITY", - "OPENCLAW_2026_9_1_TARBALL", - "OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY", - "OPENCLAW_DIAGNOSTICS_OTEL_2026_9_1_INTEGRITY", + "OPENCLAW_2026_9_2_INTEGRITY", + "OPENCLAW_2026_9_2_TARBALL", + "OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY", + "OPENCLAW_DIAGNOSTICS_OTEL_2026_9_2_INTEGRITY", ]); const futurePinArgNames = [ @@ -1775,10 +1775,10 @@ export function registerOpenClawIntegrityPinTests(group: OpenClawIntegrityPinTes ); expect(archiveBlock).toContain( - "ADD --chmod=0444 --checksum=sha256:df2c7f5f880da6ab13a43d0cf2efdd8f196802db9ebbffb9492cf81d32b15a62", + "ADD --chmod=0444 --checksum=sha256:fe5baa1d9bbe53b3cf616a13ff7dcb0f21d6ce7a7d6d9856b9909e81c53a884c", ); expect(archiveBlock).toContain( - "ADD --chmod=0444 --checksum=sha256:f679af12fa00947d994e6a8454aded205b5bf2454dce0674bff88f741dfb9af8", + "ADD --chmod=0444 --checksum=sha256:40c0cf23e8373f2285034b8f0a575cc51ec1ed53d081b0e1592d219dd411e54d", ); expect(archiveBlock).not.toContain("propagator-jaeger-2.9.0.tgz"); expect(archiveBlock).not.toContain("core-2.9.0.tgz"); diff --git a/test/agents/openclaw/openclaw-lifecycle-policy.test.ts b/test/agents/openclaw/openclaw-lifecycle-policy.test.ts index 97373350c74..9210096eaf7 100644 --- a/test/agents/openclaw/openclaw-lifecycle-policy.test.ts +++ b/test/agents/openclaw/openclaw-lifecycle-policy.test.ts @@ -175,7 +175,7 @@ describe("reviewed npm lifecycle policy", () => { const messagingPackageSpecs = Object.keys( reviewedOpenClawPluginIntegrityByPackageSpec({ - OPENCLAW_VERSION: "2026.9.1", + OPENCLAW_VERSION: "2026.9.2", }), ); const result = spawnSync(process.execPath, ["-e", PRODUCTION_BOUNDARY_AUDIT], { diff --git a/test/agents/openclaw/openclaw-locked-install.test.ts b/test/agents/openclaw/openclaw-locked-install.test.ts index d3e2146bd35..366f829f6dd 100644 --- a/test/agents/openclaw/openclaw-locked-install.test.ts +++ b/test/agents/openclaw/openclaw-locked-install.test.ts @@ -15,11 +15,11 @@ import { const REPO_ROOT = path.join(import.meta.dirname, "../../.."); const RUNTIME_DIRECTORY = path.join(REPO_ROOT, "agents", "openclaw", "openclaw-runtime"); const LOCKFILE = path.join(RUNTIME_DIRECTORY, "package-lock.json"); -const PACKAGE_SPEC = "openclaw@2026.9.1"; +const PACKAGE_SPEC = "openclaw@2026.9.2"; const INTEGRITY = - "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w=="; -const TARBALL = "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz"; -const LOCK_SHA256 = "71f87f397d8f628c40daefb0cc80d7b35a3be0353884f8275824a46568dc3113"; + "sha512-M6C7UsnX815nv26qBJFYGe6aGzv+ftZLRzV6S9oRXUtXg2Yn67eVntpssT94kgkquKVSeUxerUg0j1ONp4WYQg=="; +const TARBALL = "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.2.tgz"; +const LOCK_SHA256 = "cbcfdd15430b81f50ada9f39858a694815e570c8bb3e6b4bb9f1c0b53bf0ef4a"; const roots: string[] = []; function sha256(file: string): string { @@ -30,7 +30,7 @@ function lockRequest(lockfilePath = LOCKFILE, expectedLockSha256 = LOCK_SHA256) return { expectedIntegrity: INTEGRITY, expectedLockSha256, - label: "OpenClaw 2026.9.1 locked runtime graph", + label: "OpenClaw 2026.9.2 locked runtime graph", lockfilePath, packageSpec: PACKAGE_SPEC, registryOrigin: "https://registry.npmjs.org/", @@ -172,12 +172,12 @@ describe("locked OpenClaw production installation (#5896)", () => { const verified = verifyReviewedNpmLock(lockRequest(), reviewedMetadata); expect(verified).toHaveLength(366); expect(verified).toContain(PACKAGE_SPEC); - expect(verified).toContain("brace-expansion@5.0.9"); + expect(verified).toContain("brace-expansion@5.0.12"); expect(verified).toContain("fast-uri@3.1.7"); expect(verified).not.toContain("fast-uri@3.1.6"); expect(verified).toContain("hono@4.12.34"); - expect(verified).toContain("ip-address@10.7.0"); + expect(verified).toContain("ip-address@10.7.2"); expect(verified).toContain("tar@7.5.21"); expect(verified).not.toContain("tar@7.5.19"); expect(verified).toContain("undici@8.10.2"); @@ -202,21 +202,21 @@ describe("locked OpenClaw production installation (#5896)", () => { // source-shape-contract: security -- Mutating the shipped lock proves every reviewed transitive identity remains bound to committed production bytes it.each([ { - expected: "root must depend only on openclaw@2026.9.1", + expected: "root must depend only on openclaw@2026.9.2", mutate: (lock: any) => { lock.packages[""].dependencies.openclaw = "2026.7.2"; }, name: "root version drift", }, { - expected: "root must depend only on openclaw@2026.9.1", + expected: "root must depend only on openclaw@2026.9.2", mutate: (lock: any) => { lock.packages[""].optionalDependencies = { "left-pad": "1.3.0" }; }, name: "root optional dependency injection", }, { - expected: "lock integrity mismatch for openclaw@2026.9.1", + expected: "lock integrity mismatch for openclaw@2026.9.2", mutate: (lock: any) => { lock.packages["node_modules/openclaw"].integrity = `sha512-${"B".repeat(88)}`; }, diff --git a/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts b/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts index a415d62e73e..a7fe48a2c04 100644 --- a/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts +++ b/test/agents/openclaw/openclaw-managed-messaging-offline-build.test.ts @@ -78,15 +78,14 @@ describe("OpenClaw managed messaging offline image build", () => { }; expect(runtimeManifest.overrides).toEqual({ - "undici@8.10.0": "8.10.2", - "@openclaw/discord@2026.9.1": { + "@openclaw/discord@2026.9.2": { "@discord/embedded-app-sdk@2.5.0": { uuid: bundledVersion( "node_modules/@openclaw/discord/node_modules/@discord/embedded-app-sdk/node_modules/uuid", ), }, }, - "@openclaw/whatsapp@2026.9.1": { + "@openclaw/whatsapp@2026.9.2": { "baileys@7.0.0-rc14": { "file-type": bundledVersion( "node_modules/@openclaw/whatsapp/node_modules/baileys/node_modules/file-type", @@ -103,11 +102,11 @@ describe("OpenClaw managed messaging offline image build", () => { }); expect(runtimeManifest.dependencies).toMatchObject({ "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", + "@emnapi/runtime": "1.11.3", }); expect(runtimeLock.packages[""].dependencies).toMatchObject({ "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", + "@emnapi/runtime": "1.11.3", }); expect(runtimeLock.packages["node_modules/@emnapi/core"]).toMatchObject({ version: "1.11.1", @@ -116,7 +115,7 @@ describe("OpenClaw managed messaging offline image build", () => { }, }); expect(runtimeLock.packages["node_modules/@emnapi/runtime"]).toMatchObject({ - version: "1.11.1", + version: "1.11.3", }); expect(runtimeLock.packages["node_modules/@emnapi/wasi-threads"]).toMatchObject({ version: "1.2.2", diff --git a/test/agents/openclaw/openclaw-mcp-tools-list-timeout-patch.test.ts b/test/agents/openclaw/openclaw-mcp-tools-list-timeout-patch.test.ts index fdd2d46b1e1..c4d36a7deae 100644 --- a/test/agents/openclaw/openclaw-mcp-tools-list-timeout-patch.test.ts +++ b/test/agents/openclaw/openclaw-mcp-tools-list-timeout-patch.test.ts @@ -219,7 +219,7 @@ describe("patchOpenClawMcpToolsListTimeout", () => { ); it("keeps the exact-shape patch enabled for the supported OpenClaw version", () => { - expect(SUPPORTED_OPENCLAW_VERSION).toBe("2026.9.1"); + expect(SUPPORTED_OPENCLAW_VERSION).toBe("2026.9.2"); }); it("fails closed for an unreviewed OpenClaw version", () => { diff --git a/test/agents/openclaw/openclaw-optional-plugin-build.test.ts b/test/agents/openclaw/openclaw-optional-plugin-build.test.ts index dc0d59dd5ba..c652783513a 100644 --- a/test/agents/openclaw/openclaw-optional-plugin-build.test.ts +++ b/test/agents/openclaw/openclaw-optional-plugin-build.test.ts @@ -11,9 +11,9 @@ import { writeReviewedNpmFixture } from "../../helpers/reviewed-npm-fixture"; const ROOT = path.resolve(import.meta.dirname, "../../.."); const BRAVE_INTEGRITY = - "sha512-4+j+eQTToV3k7Cb25MUL6h2uL8cJYyuLytfpd/sJK/HjR43dgKBqKpBsb1+I3w1Jr6PLpnjSf6/I3//3K0cdnA=="; + "sha512-6416aPlfnAKlu8IBrrjgfoiss/10xB32ywFwnIf/fkVMQE61qsmzA/qxUniQuDwOB6EBFNEkNs54DhIT7g3UVg=="; const BRAVE_TARBALL = - "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.1.tgz"; + "https://registry.npmjs.org/@openclaw/brave-plugin/-/brave-plugin-2026.9.2.tgz"; it("pins Brave web-search and preserves its placeholder during build-time doctor", () => { const dockerfile = fs.readFileSync(path.join(ROOT, "Dockerfile"), "utf-8"); @@ -29,7 +29,7 @@ it("pins Brave web-search and preserves its placeholder during build-time doctor writeReviewedNpmFixture(npmFixture, log, [ { integrity: BRAVE_INTEGRITY, - packageSpec: "@openclaw/brave-plugin@2026.9.1", + packageSpec: "@openclaw/brave-plugin@2026.9.2", tarballUrl: BRAVE_TARBALL, }, ]); @@ -59,14 +59,14 @@ it("pins Brave web-search and preserves its placeholder during build-time doctor NEMOCLAW_WEB_SEARCH_ENABLED: "1", NEMOCLAW_WEB_SEARCH_PROVIDER: "brave", NODE_OPTIONS: "", - OPENCLAW_BRAVE_PLUGIN_2026_9_1_INTEGRITY: BRAVE_INTEGRITY, - OPENCLAW_VERSION: "2026.9.1", + OPENCLAW_BRAVE_PLUGIN_2026_9_2_INTEGRITY: BRAVE_INTEGRITY, + OPENCLAW_VERSION: "2026.9.2", }, }); const calls = fs.readFileSync(log, "utf-8"); expect(result.status, result.stderr).toBe(0); - expect(calls).toContain("npm view @openclaw/brave-plugin@2026.9.1 dist.integrity"); - expect(calls).toContain("npm pack @openclaw/brave-plugin@2026.9.1 --pack-destination"); + expect(calls).toContain("npm view @openclaw/brave-plugin@2026.9.2 dist.integrity"); + expect(calls).toContain("npm pack @openclaw/brave-plugin@2026.9.2 --pack-destination"); expect(calls).toContain("plugins install --force --accept-capabilities npm-pack:"); expect(calls).toContain( "doctor --fix --non-interactive|BRAVE_API_KEY=openshell:resolve:env:BRAVE_API_KEY", @@ -146,7 +146,7 @@ it.each([ { env: { NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION: union, - OPENCLAW_VERSION: "2026.9.1", + OPENCLAW_VERSION: "2026.9.2", }, }, ); diff --git a/test/agents/openclaw/openclaw-real-patched-dist-harness.test.ts b/test/agents/openclaw/openclaw-real-patched-dist-harness.test.ts index 507e19eeb77..de9dc651c8e 100644 --- a/test/agents/openclaw/openclaw-real-patched-dist-harness.test.ts +++ b/test/agents/openclaw/openclaw-real-patched-dist-harness.test.ts @@ -486,8 +486,8 @@ describe.skipIf(process.env.NEMOCLAW_REAL_OPENCLAW_DIST_HARNESS !== "1")( "OpenClaw real patched-dist npm runtime", ); const version = readRequiredDockerArg("OPENCLAW_VERSION"); - const integrity = readRequiredDockerArg("OPENCLAW_2026_9_1_INTEGRITY"); - const tarballUrl = readRequiredDockerArg("OPENCLAW_2026_9_1_TARBALL"); + const integrity = readRequiredDockerArg("OPENCLAW_2026_9_2_INTEGRITY"); + const tarballUrl = readRequiredDockerArg("OPENCLAW_2026_9_2_TARBALL"); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-real-dist-")); try { const tarballPath = materializeReviewedTarball(tarballUrl, tmp, integrity); @@ -794,7 +794,10 @@ describe.skipIf(process.env.NEMOCLAW_REAL_OPENCLAW_DIST_HARNESS !== "1")( (source.includes('from "@openclaw/fs-safe/secret";') && source.includes("PRIVATE_SECRET_DIR_MODE") && source.includes("PRIVATE_SECRET_FILE_MODE") && - source.includes("writeSecretFileAtomic as writePrivateSecretFileAtomic")) + (source.includes("writeSecretFileAtomic as writePrivateSecretFileAtomic") || + (source.includes("async function writePrivateSecretFileAtomic(params) {") && + source.includes("await tightenSecretDirectoryModes(params);") && + source.includes("await writeSecretFileAtomic(params);")))) ); }); requireRuntimeEqual( diff --git a/test/agents/openclaw/openclaw-tool-search-runtime-validator.test.ts b/test/agents/openclaw/openclaw-tool-search-runtime-validator.test.ts index 15d56629c86..005ef0eae56 100644 --- a/test/agents/openclaw/openclaw-tool-search-runtime-validator.test.ts +++ b/test/agents/openclaw/openclaw-tool-search-runtime-validator.test.ts @@ -235,18 +235,21 @@ describe("OpenClaw Tool Search pinned-runtime validator", () => { expect(result.visibleToolNames).toEqual(["nemoclaw_runtime_validator_probe"]); }); - it("selects the exact 2026.9.1 agent-tools and local-model-lean runtime layout", async () => { - const fixture = writeFixture({ - runtimeFileName: "agent-tools-fixture.js", - source: AGENT_TOOLS_FIXTURE_SOURCE, - secondRuntimeFileName: "local-model-lean-fixture.js", - secondSource: LOCAL_MODEL_LEAN_FIXTURE_SOURCE, - version: "2026.9.1", - }); - const result = await validateFixture(fixture, "progressive", "2026.9.1"); - - expect(result.runtimeModulePath).toMatch(/agent-tools-fixture\.js$/); - }); + it.each(["2026.9.1", "2026.9.2"])( + "selects the exact %s agent-tools and local-model-lean runtime layout", + async (version) => { + const fixture = writeFixture({ + runtimeFileName: "agent-tools-fixture.js", + source: AGENT_TOOLS_FIXTURE_SOURCE, + secondRuntimeFileName: "local-model-lean-fixture.js", + secondSource: LOCAL_MODEL_LEAN_FIXTURE_SOURCE, + version, + }); + const result = await validateFixture(fixture, "progressive", version); + + expect(result.runtimeModulePath).toMatch(/agent-tools-fixture\.js$/); + }, + ); it("fails closed when package metadata does not match the expected pin", async () => { const fixture = writeFixture({ version: "2026.5.28" }); diff --git a/test/agents/openclaw/runtime/nemoclaw-start-restored-token.test.ts b/test/agents/openclaw/runtime/nemoclaw-start-restored-token.test.ts index a158ba2b8a3..c2962efb82f 100644 --- a/test/agents/openclaw/runtime/nemoclaw-start-restored-token.test.ts +++ b/test/agents/openclaw/runtime/nemoclaw-start-restored-token.test.ts @@ -54,6 +54,7 @@ it("rotates a restored redaction marker before exporting gateway auth (#11764)", adapt("ensure_gateway_token_if_missing"), adapt("export_gateway_token"), 'run_openclaw_config_as_owner() { "$@"; }', + "_DASHBOARD_PORT=18789", 'export OPENCLAW_GATEWAY_TOKEN="stale-token"', "ensure_gateway_token_if_missing", "export_gateway_token", diff --git a/test/agents/openclaw/runtime/nemoclaw-start.test.ts b/test/agents/openclaw/runtime/nemoclaw-start.test.ts index 8367a00bd1a..4cca048c5c1 100644 --- a/test/agents/openclaw/runtime/nemoclaw-start.test.ts +++ b/test/agents/openclaw/runtime/nemoclaw-start.test.ts @@ -309,6 +309,7 @@ describe("nemoclaw-start gateway token export (#1114)", () => { readToken, ...(ensureToken ? ["id() { echo 0; }"] : []), configWriteHelperStubs, + `_DASHBOARD_PORT=${JSON.stringify(port)}`, ...(ensureToken ? [ensureGatewayToken, "ensure_gateway_token"] : []), exportToken, printDashboard, @@ -2964,12 +2965,12 @@ describe("Telegram diagnostics (#2766)", () => { "chown_tree_no_symlink_follow() { :; }", "start_persistent_gateway_log_mirror() { :; }", 'setpriv() { while [ "$1" != "--" ]; do shift; done; shift; "$@"; }', - // This fixture skips sandbox-init.sh and early startup selection. + // Test scaffolding skips sandbox-init.sh, so define the shared + // privilege-transition prefixes here. "STEP_DOWN_PREFIX_SANDBOX=(setpriv --reuid=sandbox --regid=sandbox --init-groups --)", "STEP_DOWN_PREFIX_GATEWAY=(setpriv --reuid=gateway --regid=gateway --init-groups --)", 'validate_tmp_permissions() { printf "VALIDATE:%s\\n" "$*"; }', "_SANDBOX_HOME=/sandbox", - "_DASHBOARD_PORT=18789", `_SANDBOX_SAFETY_NET=${JSON.stringify(path.join(tmpDir, "safety.js"))}`, `_PROXY_FIX_SCRIPT=${JSON.stringify(path.join(tmpDir, "proxy-fix.js"))}`, `_NEMOTRON_FIX_SCRIPT=${JSON.stringify(path.join(tmpDir, "nemotron-fix.js"))}`, @@ -3251,7 +3252,7 @@ process.stderr.write('FailoverError: token=123456:LATER\\n'); describe("native configuration during simulated root startup", () => { const src = fs.readFileSync(START_SCRIPT, "utf-8"); - it("retains native settings and retired hash state while preparing gateway authentication", () => { + it("persists the selected gateway port while retaining native settings and retired hash state", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-direct-root-")); const configDir = path.join(tmpDir, "openclaw"); const proxyEnvFile = path.join(tmpDir, "nemoclaw-proxy-env.sh"); @@ -3311,6 +3312,7 @@ describe("native configuration during simulated root startup", () => { "set -euo pipefail", 'id() { if [ "${1:-}" = "-u" ]; then printf "0"; else command id "$@"; fi; }', "NEMOCLAW_CMD=()", + "_DASHBOARD_PORT=18791", '_PROXY_URL=""', '_NO_PROXY_VAL=""', "STEP_DOWN_PREFIX_SANDBOX=(env)", @@ -3358,14 +3360,12 @@ describe("native configuration during simulated root startup", () => { expect(hashContents).toBe("placeholder"); expect((fs.statSync(hashPath).mode & 0o777).toString(8)).toBe("444"); - expect(fs.existsSync(proxyEnvFile)).toBe(true); const proxyEnv = fs.readFileSync(proxyEnvFile, "utf-8"); - expect(proxyEnv).toMatch(/OPENCLAW_GATEWAY_TOKEN='[A-Za-z0-9_-]{20,}'/); expect(proxyEnv).toContain("export OPENCLAW_GATEWAY_TOKEN"); const updatedConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")); expect(updatedConfig.custom).toEqual({ retained: true }); - expect(updatedConfig.gateway?.port).toBe(18789); + expect(updatedConfig.gateway?.port).toBe(18791); expect(fs.existsSync(path.join(configDir, "openclaw.json.nemoclaw-baseline"))).toBe(false); expect(updatedConfig.gateway?.auth?.token).toMatch(/^[A-Za-z0-9_-]{20,}$/); expect(proxyEnv).toContain(`OPENCLAW_GATEWAY_TOKEN='${updatedConfig.gateway.auth.token}'`); diff --git a/test/automation/releases/reviewed-npm-audit-fixtures.ts b/test/automation/releases/reviewed-npm-audit-fixtures.ts index 4c01268b1d6..a9a53dce7ba 100644 --- a/test/automation/releases/reviewed-npm-audit-fixtures.ts +++ b/test/automation/releases/reviewed-npm-audit-fixtures.ts @@ -12,7 +12,7 @@ export type LockedGraphFixture = Readonly<{ manifest: Buffer; }>; -export function openClawReplacementGraphFixture( +export function openClawReplacementGraphFixture( repoRoot: string, graph: T, ): LockedGraphFixture { @@ -27,8 +27,17 @@ export function openClawReplacementGraphFixture }; }; return { - // The historical fixture keeps its original graph when production dependencies advance. - graph: { ...graph, lockSha256: createHash("sha256").update(lock).digest("hex") }, + graph: { + ...graph, + replacement: { + label: "OpenClaw 2026.9.1 locked runtime graph", + packageSpec: "openclaw@2026.9.1", + integrity: + "sha512-0Ve0631CdgkJDwd4NNG1BawIdF5yCL2sO+Tts8amStw+H6vKURTj0K4rOa4+hFpJk1Dnw5LyKl5twzwX1VtA2w==", + tarballUrl: "https://registry.npmjs.org/openclaw/-/openclaw-2026.9.1.tgz", + lockSha256: createHash("sha256").update(lock).digest("hex"), + }, + }, lock, manifest: Buffer.from(`${JSON.stringify(parsedLock.packages[""], null, 2)}\n`), }; diff --git a/test/automation/releases/reviewed-npm-audit-workflow.test.ts b/test/automation/releases/reviewed-npm-audit-workflow.test.ts index df8876503d8..b68731ba157 100644 --- a/test/automation/releases/reviewed-npm-audit-workflow.test.ts +++ b/test/automation/releases/reviewed-npm-audit-workflow.test.ts @@ -9,6 +9,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import YAML from "yaml"; +import { readWorkflow, required, step } from "../../helpers/managed-image-publication-workflow"; import { assertReviewedAuditReportsPass, NPM_AUDIT_SIGNATURE_ARGV, @@ -304,6 +305,16 @@ function writeProductionSourceGraph( } describe("trusted npm audit workflow (#5896)", () => { + it("runs the PR audit from its exact head commit", () => { + const job = required(readWorkflow("pr.yaml").jobs?.["reviewed-npm-audit"], "missing PR audit"); + expect(step(job, "Checkout").with).toMatchObject({ + ref: "${{ github.event.pull_request.head.sha }}", + "persist-credentials": false, + }); + expect(step(job, "Audit reviewed production npm graphs").uses).toBe( + "./.github/actions/ci-reviewed-npm-audit", + ); + }); // source-shape-contract: security -- Composite audit inputs must cross into executable shell only through the step environment it("passes the cache identity target root without interpolating it into shell source", () => { const action = YAML.parse( diff --git a/test/e2e-runtime/issue-4434-error-fields.test.ts b/test/e2e-runtime/issue-4434-error-fields.test.ts index a3313f8279d..0ea871a097b 100644 --- a/test/e2e-runtime/issue-4434-error-fields.test.ts +++ b/test/e2e-runtime/issue-4434-error-fields.test.ts @@ -12,8 +12,8 @@ import { const REPO_ROOT = path.join(import.meta.dirname, "../.."); const DOCKERFILE = path.join(REPO_ROOT, "Dockerfile"); -const CURRENT_REVIEWED_OPENCLAW_VERSION = "2026.9.1"; -const PATCHED_OPENCLAW_2026_9_1_ISSUE_4434_TUI_ERROR_OUTPUT = [ +const CURRENT_REVIEWED_OPENCLAW_VERSION = "2026.9.2"; +const PATCHED_OPENCLAW_2026_9_2_ISSUE_4434_TUI_ERROR_OUTPUT = [ "run error: LLM request timed out.", "Cause: timed out while reaching the upstream API.", "Reporting layer: gateway proxy / upstream API.", @@ -21,7 +21,7 @@ const PATCHED_OPENCLAW_2026_9_1_ISSUE_4434_TUI_ERROR_OUTPUT = [ "1m 04s | error", ].join("\n"); -const UPSTREAM_OPENCLAW_2026_9_1_ISSUE_4434_TUI_ERROR_OUTPUT = [ +const UPSTREAM_OPENCLAW_2026_9_2_ISSUE_4434_TUI_ERROR_OUTPUT = [ "run error: LLM request timed out.", "1m 04s | error", ].join("\n"); @@ -49,17 +49,17 @@ describe("full OpenClaw TUI error guard (#4434)", () => { it("requires the reviewed patched output to include all full-acceptance fields", () => { expect(readDockerfileOpenClawVersion()).toBe(CURRENT_REVIEWED_OPENCLAW_VERSION); expect( - detectIssue4434AcceptanceFields(PATCHED_OPENCLAW_2026_9_1_ISSUE_4434_TUI_ERROR_OUTPUT), + detectIssue4434AcceptanceFields(PATCHED_OPENCLAW_2026_9_2_ISSUE_4434_TUI_ERROR_OUTPUT), ).toEqual({ httpStatusOrCause: true, reportingLayer: true, recoveryHint: true, }); expect( - missingIssue4434AcceptanceFields(PATCHED_OPENCLAW_2026_9_1_ISSUE_4434_TUI_ERROR_OUTPUT), + missingIssue4434AcceptanceFields(PATCHED_OPENCLAW_2026_9_2_ISSUE_4434_TUI_ERROR_OUTPUT), ).toEqual([]); expect( - missingIssue4434AcceptanceFields(UPSTREAM_OPENCLAW_2026_9_1_ISSUE_4434_TUI_ERROR_OUTPUT), + missingIssue4434AcceptanceFields(UPSTREAM_OPENCLAW_2026_9_2_ISSUE_4434_TUI_ERROR_OUTPUT), ).toEqual(["httpStatusOrCause", "reportingLayer", "recoveryHint"]); }); }); diff --git a/test/e2e/live/llama-cpp-generic-gpu.test.ts b/test/e2e/live/llama-cpp-generic-gpu.test.ts index 748ae12e4ea..51980d766e1 100644 --- a/test/e2e/live/llama-cpp-generic-gpu.test.ts +++ b/test/e2e/live/llama-cpp-generic-gpu.test.ts @@ -5,6 +5,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { GATEWAY_PORT } from "../../../src/lib/core/ports.ts"; import { loadManagedLlamaCppApiKey, @@ -415,7 +416,7 @@ NODE`), runtimeOwnerSandboxName: SANDBOX_NAME, expectedModel: recipe.spec.model.servedName, expectedReceipt: receipt, - gatewayPort: recipe.spec.serve.port, + gatewayPort: GATEWAY_PORT, homeDir: os.homedir(), environment: destroyEnv, operation: runtimeProvider.hostLocalInference.createOperation({ diff --git a/test/e2e/live/managed-image-activation-e2e-helpers.ts b/test/e2e/live/managed-image-activation-e2e-helpers.ts index d9f2a0c1d83..520689ac185 100644 --- a/test/e2e/live/managed-image-activation-e2e-helpers.ts +++ b/test/e2e/live/managed-image-activation-e2e-helpers.ts @@ -5,6 +5,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; +import { resolveSandboxHealthProbeUrl } from "../../../src/lib/actions/sandbox/forward-recovery.ts"; import { shellQuote } from "../../../src/lib/core/shell-quote.ts"; import { resolveGatewayLogPathForPort } from "../../../src/lib/onboard/gateway/state-dir.ts"; import { @@ -251,6 +252,7 @@ export function managedActivationPostRestartAgentTurnScript( agent: ShippedManagedImageAgent, phase: "before" | "boundary" | "after", command: readonly string[], + healthProbeUrl = "http://127.0.0.1:18789/health", ): TrustedSandboxShellScript | null { if (agent !== "openclaw" || phase !== "after") return null; @@ -258,7 +260,7 @@ export function managedActivationPostRestartAgentTurnScript( deadline=$(( $(date +%s) + ${OPENCLAW_POST_RESTART_READY_TIMEOUT_SECONDS} )) last_status=000 while [ "$(date +%s)" -lt "$deadline" ]; do - last_status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --max-time 2 http://127.0.0.1:18789/health || true)" + last_status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --max-time 2 ${shellQuote(healthProbeUrl)} || true)" case "$last_status" in 200|401) break ;; esac @@ -381,7 +383,14 @@ async function runAgentTurn( env: NodeJS.ProcessEnv, ): Promise { const command = agentTurnCommand(agent, `managed-${agent}-${phase}-${Date.now()}`); - const postRestartScript = managedActivationPostRestartAgentTurnScript(agent, phase, command); + const postRestartScript = managedActivationPostRestartAgentTurnScript( + agent, + phase, + command, + agent === "openclaw" && phase === "after" + ? resolveSandboxHealthProbeUrl(sandboxName) + : undefined, + ); const options = { artifactName: `${agent}-agent-turn-${phase}-restart`, env, diff --git a/test/e2e/support/managed-image-activation-diagnostics.test.ts b/test/e2e/support/managed-image-activation-diagnostics.test.ts index fa1ad6bdc24..45096dc7805 100644 --- a/test/e2e/support/managed-image-activation-diagnostics.test.ts +++ b/test/e2e/support/managed-image-activation-diagnostics.test.ts @@ -683,9 +683,14 @@ ${adminApprovalConnectScript("nemoclaw", "fixture-sandbox", "managed-cron", outp }); it("gates only the post-restart OpenClaw turn on inner gateway readiness (#7744)", () => { const command = ["openclaw", "agent", "--session-id", "quoted session"]; - const script = managedActivationPostRestartAgentTurnScript("openclaw", "after", command); + const script = managedActivationPostRestartAgentTurnScript( + "openclaw", + "after", + command, + "http://127.0.0.1:18791/health", + ); - expect(script).toContain("http://127.0.0.1:18789/health"); + expect(script).toContain("http://127.0.0.1:18791/health"); expect(script).toContain("OpenClaw gateway did not become ready after OpenShell restart"); expect(script).toContain("exec 'openclaw' 'agent' '--session-id' 'quoted session'"); expect(managedActivationPostRestartAgentTurnScript("openclaw", "before", command)).toBeNull(); diff --git a/test/helpers/base-apt-security-functions.ts b/test/helpers/base-apt-security-functions.ts index 46087b96e34..2d9909a8a33 100644 --- a/test/helpers/base-apt-security-functions.ts +++ b/test/helpers/base-apt-security-functions.ts @@ -64,7 +64,7 @@ export function baseAptSecurityFunctions(architecture: DebianArchitecture): stri ' perl) if [[ "${perl_installed:-0}" == "1" ]]; then printf "5.44.0-1nemoclaw1"; else printf "5.40.1-6"; fi ;;', ' vim-common|vim-tiny) printf "2:9.2.0858-1" ;;', ' libssh2-1t64) printf "1.11.1-1+deb13u1+nemoclaw2" ;;', - ' libssl3t64) printf "3.5.7-1~deb13u2" ;;', + ' libssl3t64) printf "3.5.7-1~deb13u3" ;;', ' lsof) printf "4.99.4+dfsg-2" ;;', ' nemoclaw-python3.13-htmlparser-fix) printf "3.13.5-2+deb13u5+nemoclaw1" ;;', " *) return 64 ;;", diff --git a/test/helpers/fetch-guard-patch-harness.ts b/test/helpers/fetch-guard-patch-harness.ts index dd1f7febc60..77c6fc6648d 100644 --- a/test/helpers/fetch-guard-patch-harness.ts +++ b/test/helpers/fetch-guard-patch-harness.ts @@ -14,7 +14,7 @@ const OPENCLAW_VERSION_EXTRACTOR = path.join( "extract-semver.sh", ); -export const CURRENT_REVIEWED_OPENCLAW_PATCH_CLASSIFIER_VERSION = "2026.9.1"; +export const CURRENT_REVIEWED_OPENCLAW_PATCH_CLASSIFIER_VERSION = "2026.9.2"; export function dockerRunCommandBetween(startMarker: string, endMarker: string): string { const dockerfile = fs.readFileSync(DOCKERFILE, "utf-8"); diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 274d47b4afe..f4912f118f3 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -432,7 +432,7 @@ const OPENCLAW_SECURITY_INVENTORY_PROBE = [ 'test -f "$security_inventory"', 'test ! -L "$security_inventory"', `test "$(stat -c '%u:%g:%a' "$security_inventory")" = "0:0:444"`, - `printf '%s\\n' "architecture=$arch" "libexpat1=2.8.3-1" "libonig5=6.9.9-1+b1" "libjq1=1.8.2-1" "jq=1.8.2-1" "vim-common=2:9.2.0858-1" "vim-tiny=2:9.2.0858-1" "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" "libssl3t64=3.5.7-1~deb13u2" "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" "perl-base=5.44.0-1nemoclaw1" "perl=5.44.0-1nemoclaw1" "libevent-core-2.1-7t64=2.1.13-stable-1" | cmp -s - "$security_inventory"`, + `printf '%s\\n' "architecture=$arch" "libexpat1=2.8.3-1" "libonig5=6.9.9-1+b1" "libjq1=1.8.2-1" "jq=1.8.2-1" "vim-common=2:9.2.0858-1" "vim-tiny=2:9.2.0858-1" "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2" "libssl3t64=3.5.7-1~deb13u3" "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1" "perl-base=5.44.0-1nemoclaw1" "perl=5.44.0-1nemoclaw1" "libevent-core-2.1-7t64=2.1.13-stable-1" | cmp -s - "$security_inventory"`, `printf '%s\\n' "nemoclaw-security-inventory-ok"`, ].join("; "); diff --git a/test/helpers/openclaw-real-device-self-approval-proof.ts b/test/helpers/openclaw-real-device-self-approval-proof.ts index 5f2dabfdf47..86cad1b8a55 100644 --- a/test/helpers/openclaw-real-device-self-approval-proof.ts +++ b/test/helpers/openclaw-real-device-self-approval-proof.ts @@ -77,6 +77,11 @@ function requireRealDeviceTokenAuthLinkage(sources: DistSource[]): string { ), ); if (sqliteGatewayLayout) { + const dispatcherSignature = sources.some(({ source }) => + source.includes("async function handleGatewayRequest(opts, diagnostics)"), + ) + ? "async function handleGatewayRequest(opts, diagnostics)" + : "async function handleGatewayRequest(opts)"; const producer = requireExactlyOneDistSource(sources, "SQLite device-token session producer", [ 'const loadGatewayServerMethods = createLazyPromise(() => import("./authenticated-request-dispatch.server-methods.runtime.js"))', "const nextClient = {", @@ -87,7 +92,7 @@ function requireRealDeviceTokenAuthLinkage(sources: DistSource[]): string { ]); const dispatcher = requireExactlyOneDistSource(sources, "SQLite gateway request dispatcher", [ "function createLazyCoreHandlers(params)", - "async function handleGatewayRequest(opts)", + dispatcherSignature, 'devices: () => import("./devices-', ]); const handler = requireExactlyOneDistSource(sources, "SQLite device pairing gateway handler", [ @@ -123,7 +128,7 @@ function requireRealDeviceTokenAuthLinkage(sources: DistSource[]): string { [ "function createLazyCoreHandlers(params)", `devices: () => import("./${path.basename(handler.file)}")`, - "async function handleGatewayRequest(opts)", + dispatcherSignature, ], "SQLite dispatcher-to-device-handler linkage", ); @@ -299,7 +304,11 @@ function requireRealStoredDeviceAuthLinkage(sources: DistSource[], cliSource: Di "const requestedStoredDeviceAuth = opts.useStoredDeviceAuth === true;", "const hasExplicitAuth = Boolean(context.explicitAuth.token || context.explicitAuth.password);", "const useStoredDeviceAuth = requestedStoredDeviceAuth && !hasExplicitAuth;", - "skipImplicitAuth: useStoredDeviceAuth,", + gatewayCall.source.includes( + "skipImplicitAuth: useStoredDeviceAuth || opts.skipImplicitAuth === true,", + ) + ? "skipImplicitAuth: useStoredDeviceAuth || opts.skipImplicitAuth === true," + : "skipImplicitAuth: useStoredDeviceAuth,", "storedAuth = loadStoredOperatorDeviceAuthToken(deviceIdentity, deviceAuthScope, opts.sharedStateMode);", "opts.requiredStoredDeviceAuthScopes", "scopes: requestedStoredDeviceAuth && hasExplicitAuth && opts.requiredStoredDeviceAuthScopes ? opts.requiredStoredDeviceAuthScopes : useStoredDeviceAuth ? void 0 : scopes,", @@ -1190,8 +1199,19 @@ const exactlyOne = (pattern, label, sourceMarker) => { return pathToFileURL(path.join(dist, files[0])).href; }; const pairing = await import(exactlyOne(/^device-pairing-[^.]+[.]js$/, "pairing", "async function requestDevicePairing(req, baseDir)")); -const approval = await import(exactlyOne(/^device-pairing-approval-[^.]+[.]js$/, "approval", "async function approveDevicePairingWithOptions")); +const approval = await import(exactlyOne(/^device-pairing-approval-[^.]+[.]js$/, "approval", "async function approveDevicePairing(requestId, optionsOrBaseDir, maybeBaseDir)")); const auth = await import(exactlyOne(/^device-auth-store-[^.]+[.]js$/, "stored auth", "function loadDeviceAuth")); +const { deviceHandlers } = await import(exactlyOne(/^devices-[^.]+[.]js$/, "device handlers", '"device.pair.approve": async')); +const clientSource = fs.readFileSync(new URL(exactlyOne(/^client-[^.]+[.]js$/, "client authentication", "function buildGatewayConnectAuth(selected)")), "utf8"); +const clientLines = clientSource.split(String.fromCharCode(10)); +const authFunctions = ["normalized", "selectGatewayConnectAuth", "buildGatewayConnectAuth"].map((name) => { + const start = clientLines.findIndex((line) => line.startsWith("function " + name + "(")); + const end = clientLines.findIndex((line, index) => index > start && line === "}"); + if (start < 0 || end < start) throw new Error("native client authentication function missing: " + name); + return clientLines.slice(start, end + 1).join(String.fromCharCode(10)); +}); +const { runInNewContext } = await import("node:vm"); +const nativeConnectAuth = runInNewContext(authFunctions.join(String.fromCharCode(10)) + "; token => buildGatewayConnectAuth(selectGatewayConnectAuth({ storedToken: token }))"); if (typeof pairing.h !== "function" || typeof pairing.c !== "function" || typeof approval.n !== "function" || typeof auth.l !== "function" || typeof auth.r !== "function") { throw new Error("reviewed SQLite device-pairing exports missing"); } @@ -1229,12 +1249,25 @@ const identity = { clientMode: "cli", deviceToken: initialToken.token, }; -const upgraded = await approval.n(upgrade.requestId, { - callerScopes: ["operator.pairing"], - nemoclawSelfApprovalIdentity: identity, -}, stateDir); -if (upgraded?.status !== "approved") throw new Error("bounded SQLite self-approval failed"); -const nextToken = upgraded.device?.tokens?.operator; +const approveThroughGateway = async (isDeviceTokenAuth) => { + let response; + await deviceHandlers["device.pair.approve"]({ + params: { requestId: upgrade.requestId }, + client: { isDeviceTokenAuth, connect: { + role: "operator", scopes: ["operator.pairing"], + device: { id: deviceId, publicKey }, client: { id: "cli", mode: "cli" }, + auth: nativeConnectAuth(initialToken.token), + } }, + context: { logGateway: { info() {}, warn() {} }, broadcast() {} }, + respond: (ok, payload, error) => { response = { ok, payload, error }; }, + }); + return response; +}; +if ((await approveThroughGateway(false))?.ok !== false) throw new Error("shared auth was allowed to self-approve"); +const upgraded = await approveThroughGateway(true); +if (upgraded?.ok !== true) throw new Error("bounded SQLite gateway self-approval failed: " + upgraded?.error?.message); +const approvedDevice = (await pairing.c(stateDir)).paired.find((device) => device.deviceId === deviceId); +const nextToken = approvedDevice?.tokens?.operator; const expectedScopes = ["operator.pairing", "operator.read", "operator.write"]; if (!nextToken?.token || nextToken.token === initialToken.token || JSON.stringify([...nextToken.scopes].toSorted()) !== JSON.stringify(expectedScopes)) throw new Error("bounded SQLite token rotation invalid"); const afterList = await pairing.c(stateDir); @@ -1358,6 +1391,7 @@ export async function proveRealOpenClawAdminApprovalHandoff(dist: string): Promi const adminScopes = ["operator.admin", "operator.read", "operator.write"]; for (const outcome of [ "admin", + "remote-admin", "other-device", "missing-identity", "unreadable-identity", @@ -1367,7 +1401,7 @@ export async function proveRealOpenClawAdminApprovalHandoff(dist: string): Promi ] as const) { const events: string[] = []; const approved = !["baseline", "missing"].includes(outcome); - const confirmationRequired = outcome === "admin" || outcome === "confirmation-denied"; + const confirmationRequired = ["admin", "remote-admin", "confirmation-denied"].includes(outcome); const options = { json: true, url: "wss://reviewed-gateway.example" }; let cached = { storedToken: "previous-token", @@ -1405,8 +1439,23 @@ export async function proveRealOpenClawAdminApprovalHandoff(dist: string): Promi return outcome === "missing-identity" ? null : { deviceId: "calling-device" }; }, resolveApprovePairingGatewayContext: async () => ({}), - approvePairingWithFallback: async () => { + approvePairingWithFallback: async ( + _opts: unknown, + _id: string, + context: Record, + ) => { events.push("approve-exact-request"); + if (outcome !== "remote-admin") + context.nemoclawLocallyApprovedDevice = { + deviceId: outcome === "other-device" ? "other-device" : "calling-device", + tokens: { + operator: { + role: "operator", + token: "rotated-token", + scopes: approved ? adminScopes : ["operator.write"], + }, + }, + }; return outcome === "missing" ? null : { @@ -1421,6 +1470,7 @@ export async function proveRealOpenClawAdminApprovalHandoff(dist: string): Promi url: string; scopes: string[]; timeoutMs: number; + token?: string; sharedStateMode?: string; }) => { requireLiveProof( @@ -1428,6 +1478,11 @@ export async function proveRealOpenClawAdminApprovalHandoff(dist: string): Promi "approval confirmation changed gateway or timeout", ); requireJsonEqual(call.scopes, ["operator.admin"], "approval confirmation scope"); + if (call.sharedStateMode !== "read-only") + requireLiveProof( + call.token === (outcome === "remote-admin" ? undefined : "rotated-token"), + "local approval confirmation did not use its newly issued token", + ); events.push(call.method); if (outcome === "confirmation-denied") throw new Error("confirmation denied"); const next = receiveToken(cached, adminScopes); @@ -1440,7 +1495,13 @@ export async function proveRealOpenClawAdminApprovalHandoff(dist: string): Promi formatCliCommand: (value: string) => value, findQueryPendingNodeApprovalNotices: () => [], defaultRuntime: { - writeJson: () => events.push("output"), + writeJson: (value: unknown) => { + requireLiveProof( + !JSON.stringify(value).includes("rotated-token"), + "approval output exposed the private token", + ); + events.push("output"); + }, error: () => events.push("error"), exit: (code: number) => events.push(`exit-${code}`), }, @@ -1619,7 +1680,7 @@ export async function runRealOpenClawDeviceSelfApprovalProof(options: ProofOptio sqlitePairingLayout ? [ "nemoclaw: validate bounded self-approval inside pairing lock", - "approveDevicePairingWithOptions", + "async function approveDevicePairing(requestId, optionsOrBaseDir, maybeBaseDir)", "nemoclawSelfApprovalIdentity", ] : [ @@ -1631,6 +1692,21 @@ export async function runRealOpenClawDeviceSelfApprovalProof(options: ProofOptio requireRealStoredDeviceAuthLinkage(sources, cliSource); const deviceHandlerFile = requireRealDeviceTokenAuthLinkage(sources); if (sqlitePairingLayout) { + if (pairingStateSource.source.includes("async function withPendingDevicePairingApproval")) { + requireOrderedMarkers( + pairingStateSource.source, + [ + "async function withPendingDevicePairingApproval", + "return await withDevicePairingLock(async () => {", + "const state = await loadDevicePairingState(baseDir);", + "return approve(state, pending, existing);", + "async function approveDevicePairing(requestId, optionsOrBaseDir, maybeBaseDir)", + "return await withPendingDevicePairingApproval(requestId, options, baseDir, (state, pendingRecord, existing) => {", + "const nemoclawSelfApprovalScopes = resolveNemoClawSelfApprovalScopes(pendingRecord,", + ], + "self-approval classifier remains inside the native pairing lock", + ); + } proveRealOpenClawAgentScopes(options.dist); await proveRealOpenClawAdminApprovalHandoff(options.dist); requireExactlyOneDistSource(sources, "patched atomic SQLite pairing persistence runtime", [ diff --git a/test/helpers/openclaw-real-mcp-start-retry-proof.ts b/test/helpers/openclaw-real-mcp-start-retry-proof.ts index 3f6e4226baf..21ef8dc8a94 100644 --- a/test/helpers/openclaw-real-mcp-start-retry-proof.ts +++ b/test/helpers/openclaw-real-mcp-start-retry-proof.ts @@ -247,7 +247,7 @@ async function runScenario(mode) { const before = posts(); const materialized = await materializeBundleMcpToolsForRun({ runtime, reservedToolNames: new Set() }); const catalog = runtime.peekCatalog(); - materialized.dispose?.(); + await materialized.dispose?.(); return { toolCount: materialized.tools.length, attempts: posts() - before, diff --git a/test/inference/managed/managed-image-publication-workflow.test.ts b/test/inference/managed/managed-image-publication-workflow.test.ts index 51fcb48b39e..e75889d95d9 100644 --- a/test/inference/managed/managed-image-publication-workflow.test.ts +++ b/test/inference/managed/managed-image-publication-workflow.test.ts @@ -31,8 +31,8 @@ import { import type { Job, Workflow } from "../../helpers/managed-image-publication-workflow-types"; const fullShaAction = /^[^@]+@[0-9a-f]{40}$/iu; -const reviewedAuditAction = "NVIDIA/NemoClaw/.github/actions/ci-reviewed-npm-audit@"; -const reviewedAuditSha = "b0af4ef64b8257372f2ad390ce6c843c96bbfebb"; +const reviewedAuditAction = "./.github/actions/ci-reviewed-npm-audit"; +const reviewedAuditSha = "${{ github.event.pull_request.base.sha }}"; function needsOutput(job: string, output: string): string { return `\${{ needs.${job}.outputs.${output} }}`; @@ -147,7 +147,7 @@ describe("complete managed-image publication workflow", () => { "Audit exact PR production npm graphs", ); expect(prAudit.with?.["cache-directory"]).toBe("${{ runner.temp }}/reviewed-npm-audit-cache"); - expect(prAudit.uses).toBe(reviewedAuditAction + reviewedAuditSha); + expect(prAudit.uses).toBe(reviewedAuditAction); expect(managedAudit.with?.["cache-directory"]).toBe( "${{ runner.temp }}/reviewed-npm-audit-cache", ); @@ -486,28 +486,8 @@ describe("complete managed-image publication workflow", () => { path: "candidate", "persist-credentials": false, }); - const trustedCheckout = step(reviewedAudit, "Checkout npm audit code from the base commit"); - expect(trustedCheckout.with).toMatchObject({ - ref: reviewedAuditSha, - path: ".trusted-reviewed-npm-audit", - "persist-credentials": false, - "sparse-checkout-cone-mode": false, - }); - expect(trustedCheckout.with?.["sparse-checkout"]).toContain( - ".github/actions/ci-reviewed-npm-audit", - ); - expect(trustedCheckout.with?.["sparse-checkout"]).toContain("ci/reviewed-npm-audit.json"); - const verifyAuditIdentities = step(reviewedAudit, "Verify exact audit source and target"); - expect(verifyAuditIdentities.env).toEqual({ - CANDIDATE_SHA: "${{ github.event.pull_request.head.sha }}", - REVIEWED_AUDIT_SHA: reviewedAuditSha, - }); - expect(verifyAuditIdentities.run).toContain( - "git -C .trusted-reviewed-npm-audit rev-parse --verify HEAD", - ); - expect(verifyAuditIdentities.run).toContain("git -C candidate rev-parse --verify HEAD"); expect(step(reviewedAudit, "Audit exact PR production npm graphs")).toMatchObject({ - uses: "./.trusted-reviewed-npm-audit/.github/actions/ci-reviewed-npm-audit", + uses: "./candidate/.github/actions/ci-reviewed-npm-audit", with: { "cache-directory": "${{ runner.temp }}/reviewed-npm-audit-cache", "report-dir": "artifacts/reviewed-npm-audit", @@ -538,7 +518,8 @@ describe("complete managed-image publication workflow", () => { const auditVerifierCheckout = step(prBuilder, "Checkout trusted mcporter audit verifier"); expect(auditVerifierCheckout.with?.ref).toBe(reviewedAuditSha); const prepareAuditEvidence = step(prBuilder, "Prepare same-run mcporter audit evidence"); - expect(prepareAuditEvidence.run).toContain(`rev-parse --verify HEAD)" = '${reviewedAuditSha}'`); + expect(prepareAuditEvidence.env?.REVIEWED_AUDIT_SHA).toBe(reviewedAuditSha); + expect(prepareAuditEvidence.run).toContain('rev-parse --verify HEAD)" = "$REVIEWED_AUDIT_SHA"'); expect(prepareAuditEvidence.run).not.toMatch(/--legacy-(?:audit|npmjs)/u); const matrixByAgent = new Map(matrix.map((entry) => [entry.agent, entry])); expect([...matrixByAgent.keys()].sort()).toEqual([ diff --git a/test/install/materialize-locked-npm-cache-seed.test.ts b/test/install/materialize-locked-npm-cache-seed.test.ts index 2fe7d88872b..b8371073eac 100644 --- a/test/install/materialize-locked-npm-cache-seed.test.ts +++ b/test/install/materialize-locked-npm-cache-seed.test.ts @@ -83,6 +83,32 @@ afterEach(() => { }); describe("locked npm cache seed materialization", () => { + it("preserves distinct archive contents when registry URLs share a filename", async () => { + const alpha = archive("alpha", "alpha archive"); + const beta = archive("beta", "beta archive"); + beta.locked.resolved = "https://registry.npmjs.org/beta/-/alpha-1.0.0.tgz"; + const sources = new Map([ + [alpha.locked.resolved, alpha.bytes], + [beta.locked.resolved, beta.bytes], + ]); + const lockfile = writeLock(testRoot, [alpha.locked, beta.locked]); + const seed = path.join(testRoot, "seed"); + const copied = path.join(testRoot, "copied"); + const manifest = await materializeLockedNpmCacheSeed({ + downloadArchive: async (entry) => sources.get(entry.resolved)!, + lockfile, + output: seed, + target: TARGET, + }); + await verifyAndCopyLockedNpmCacheSeed({ lockfile, output: copied, seed, target: TARGET }); + expect(new Set(manifest.archives.map(({ archive }) => archive)).size).toBe(2); + expect( + readdirSync(copied) + .map((name) => readFileSync(path.join(copied, name), "utf8")) + .sort(), + ).toEqual(["alpha archive", "beta archive"]); + }); + it("materializes the reviewed OpenClaw 2026.9.1 archive size", async () => { const bytes = Buffer.alloc(55_564_082, 0x61); const locked: LockedArchive = { diff --git a/test/install/native-security-packages.test.ts b/test/install/native-security-packages.test.ts index a33285e179f..29eb6713bc9 100644 --- a/test/install/native-security-packages.test.ts +++ b/test/install/native-security-packages.test.ts @@ -301,7 +301,7 @@ describe("native security package remediation", () => { "/tmp/nemoclaw-native-security/nemoclaw-python3.13-htmlparser-fix.deb", ); expect(content).toContain("libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2"); - expect(content).toContain("libssl3t64=3.5.7-1~deb13u2"); + expect(content).toContain("libssl3t64=3.5.7-1~deb13u3"); expect(content).toContain("nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1"); expect(content).toContain("4ff43a8578bda2f14686c67911b64c18e869841973722b1c623b5727491bdaf7"); expect(content).toContain("[p.feed('') for _ in range(20000)]"); diff --git a/test/mcp/mcp-tool-discovery-image-contract.test.ts b/test/mcp/mcp-tool-discovery-image-contract.test.ts index 50968669bdc..6738f5d418c 100644 --- a/test/mcp/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp/mcp-tool-discovery-image-contract.test.ts @@ -214,7 +214,7 @@ describe("MCP tool discovery image contract", () => { // source-shape-contract: security -- Exact reviewed runtime digests reject substituted executable and license artifacts before managed image construction. it.each([ { - expectedHash: "12c787ebf562b63523210030949a8227a44e4db389dc012a546c020049a3217f", + expectedHash: "747f2a5182b7cc754df1e5850951cdc93b7bb3b523f8f223b59d98183aab5b73", relativePath: "managed-startup-direct-image-runtime.bundle", }, { diff --git a/test/onboarding/effective-policy-contracts.test.ts b/test/onboarding/effective-policy-contracts.test.ts index caef0a1bc88..4be3aa2498f 100644 --- a/test/onboarding/effective-policy-contracts.test.ts +++ b/test/onboarding/effective-policy-contracts.test.ts @@ -203,13 +203,13 @@ describe("effective built-in policy contracts", () => { ); }); - // The published 2026.7.1 and 2026.9.1 archives contain byte-identical + // The published 2026.7.1, 2026.9.1, and 2026.9.2 archives contain byte-identical // skills/weather/SKILL.md content (SHA-256 62ab4821aa873949d1c1091836be1659a42b32caadce4bd145f5505a1ceaeec1), // so the reviewed read-only egress contract remains unchanged. expect( loadAgent("openclaw").expectedVersion, "Revalidate the bundled OpenClaw weather skill before changing its reviewed egress contract", - ).toBe("2026.9.1"); + ).toBe("2026.9.2"); }); it("uses raw L4 tunnels only for protocols that cannot be REST-inspected", () => { diff --git a/test/package-contract/msteams-message-hints-preload.test.ts b/test/package-contract/msteams-message-hints-preload.test.ts index 339d2d7b32c..773f43afdd6 100644 --- a/test/package-contract/msteams-message-hints-preload.test.ts +++ b/test/package-contract/msteams-message-hints-preload.test.ts @@ -22,17 +22,17 @@ const compiledPreload = path.join( // Reviewed from the published @openclaw/msteams artifact, not inferred from // NemoClaw source. The integrity is npm's dist.integrity; the SHA-256 values -// identify the exact runtime entry and plugin entry reviewed for 2026.9.1. +// identify the exact runtime entry and plugin entry reviewed for 2026.9.2. // This fixture intentionally models only that package/load boundary. It does // not vendor or claim to test the upstream Bot Framework send/parser code. const REVIEWED_MSTEAMS_CONTRACT = { - version: "2026.9.1", + version: "2026.9.2", npmIntegrity: - "sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong==", + "sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og==", runtimeExtension: "./dist/index.cjs", pluginSpecifier: "./channel-plugin-api.cjs", indexSha256: "7b5ba63fb0abc15c606c95b165e50e09bb31c37036489781cfcca685ae1d79dd", - pluginEntrySha256: "aae2fdcaa996a98f3887c7417e2f7480fe7d8fc2436165a94697a690fd45993e", + pluginEntrySha256: "4dc5eca629e615948695b984fffedf5d7674bc0ae72b6fd55727aed1f011031a", } as const; function readPinnedOpenClawVersion(): string { @@ -58,7 +58,7 @@ function writeReviewedPackageShape(root: string, version: string): string { fs.writeFileSync( path.join(distDir, "reviewed-channel-entry-contract.cjs"), // The published package's runtime extension delegates to - // defineBundledChannelEntry. OpenClaw 2026.9.1 then uses CommonJS require for + // defineBundledChannelEntry. OpenClaw 2026.9.2 then uses CommonJS require for // built dist/*.cjs plugin entries. Preserve that reviewed loader seam here // without copying the upstream Teams sender or parser implementation. [ diff --git a/test/platform/images/base-image-resolver-helper.test.ts b/test/platform/images/base-image-resolver-helper.test.ts index 5f56f550b30..13c07d42a58 100644 --- a/test/platform/images/base-image-resolver-helper.test.ts +++ b/test/platform/images/base-image-resolver-helper.test.ts @@ -8,7 +8,11 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { type CompositeAction, readYaml } from "../../helpers/e2e-workflow-contract"; +import { + type CompositeAction, + type WorkflowJob, + readYaml, +} from "../../helpers/e2e-workflow-contract"; import { execTimeout } from "../../helpers/timeouts"; const repoRoot = path.resolve(import.meta.dirname, "../../.."); @@ -93,7 +97,7 @@ if [[ "$1" == run ]]; then shift done if [[ "$entrypoint" == /usr/bin/ldd ]]; then printf "ldd (Ubuntu GLIBC 2.39) 2.39\\n"; exit 0; fi - if [[ "$entrypoint" == sh ]]; then exit 0; fi + if [[ "$entrypoint" == sh || "$entrypoint" == /bin/sh ]]; then exit 0; fi if [[ "$entrypoint" == /opt/hermes/.venv/bin/python ]]; then probe="\${@: -1}" [[ "$probe" == *'import mcp'* ]] @@ -181,7 +185,7 @@ if [[ "$1" == run ]]; then shift done if [[ "$entrypoint" == /usr/bin/ldd ]]; then printf "ldd (Ubuntu GLIBC 2.39) 2.39\\n"; exit 0; fi - if [[ "$entrypoint" == sh ]]; then exit 0; fi + if [[ "$entrypoint" == sh || "$entrypoint" == /bin/sh ]]; then exit 0; fi if [[ "$entrypoint" == /opt/hermes/.venv/bin/python ]]; then probe="\${@: -1}" [[ "$probe" == *'import mcp'* ]] @@ -248,6 +252,106 @@ exit 2`); expect(localProbe).toBeGreaterThan(localBuild); }); + it.each([ + { + name: "builds a compatible local base", + localStatus: "0", + status: 0, + exported: "HERMES_BASE_IMAGE=nemoclaw-hermes-base-local\n", + }, + { name: "rejects an incompatible local base", localStatus: "1", status: 1, exported: "" }, + ])("rejects an obsolete Hermes OpenSSL base and $name", ({ localStatus, status, exported }) => { + const remoteDigest = `ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:${"c".repeat(64)}`; + const bin = fakeDocker(` +printf "%s\\0" "$@" >> "$DOCKER_LOG" +printf "\\0" >> "$DOCKER_LOG" +if [[ "$1" == pull || "$1" == build ]]; then exit 0; fi +if [[ "$1" == image && "$2" == inspect ]]; then printf "%s\\n" "$REMOTE_DIGEST"; exit 0; fi +if [[ "$1" == run ]]; then + while (($#)); do + if [[ "$1" == --entrypoint ]]; then entrypoint="$2"; image="$3"; break; fi + shift + done + if [[ "$entrypoint" == /usr/bin/ldd ]]; then printf "ldd (GLIBC 2.39) 2.39\\n"; exit 0; fi + if [[ "$entrypoint" == /bin/sh ]]; then + [[ "\${@: -1}" == "3.5.7-1~deb13u3" ]] + if [[ "$image" == "$REMOTE_DIGEST" ]]; then exit 1; fi + exit "$LOCAL_STATUS" + fi + exit 0 +fi +exit 2`); + const dockerLog = path.join(bin, "docker.log"); + const githubEnv = path.join(bin, "github.env"); + writeFileSync(githubEnv, ""); + const resolver = hermesAction.runs.steps.find( + (step) => step.name === "Resolve Hermes sandbox base image", + )?.run; + const result = spawnSync("bash", ["--noprofile", "--norc", "-c", resolver ?? ""], { + cwd: repoRoot, + encoding: "utf8", + timeout: execTimeout(), + env: { + ...process.env, + DOCKER_LOG: dockerLog, + GITHUB_ACTION_PATH: path.join(repoRoot, ".github/actions/resolve-hermes-base-image"), + GITHUB_ENV: githubEnv, + GITHUB_SHA: "1".repeat(40), + PATH: `${bin}:${process.env.PATH}`, + REMOTE_DIGEST: remoteDigest, + LOCAL_STATUS: localStatus, + }, + }); + expect(result.status, result.stderr).toBe(status); + expect(result.stdout).toContain("does not match OpenSSL 3.5.7-1~deb13u3"); + expect(readFileSync(githubEnv, "utf8")).toBe(exported); + const calls = readFileSync(dockerLog, "utf8") + .split("\0\0") + .filter(Boolean) + .map((call) => call.split("\0").filter(Boolean)); + expect(calls.filter((args) => args[0] === "build")).toEqual([ + ["build", "-f", "agents/hermes/Dockerfile.base", "-t", "nemoclaw-hermes-base-local", "."], + ]); + const probes = calls.filter((args) => args.includes("/bin/sh")); + expect(probes[0]).toContain(remoteDigest); + expect(probes.at(-1)).toContain("nemoclaw-hermes-base-local"); + }); + + it("builds the Hermes final image with the daemon that owns the resolved local base", () => { + const workflow = readYaml<{ jobs: Record }>( + ".github/workflows/sandbox-images.yaml", + ); + const build = workflow.jobs["build-hermes-sandbox-image"]?.steps?.find( + (step) => step.name === "Build Hermes production image", + ); + const bin = fakeDocker(`printf "%s\\0" "$@" > "$DOCKER_LOG"`); + const dockerLog = path.join(bin, "docker.log"); + const result = spawnSync("bash", ["--noprofile", "--norc", "-c", build?.run ?? "exit 99"], { + cwd: repoRoot, + encoding: "utf8", + timeout: execTimeout(), + env: { + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + DOCKER_LOG: dockerLog, + HERMES_BASE_IMAGE: "nemoclaw-hermes-base-local", + }, + }); + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(dockerLog, "utf8").split("\0").filter(Boolean)).toEqual([ + "build", + "--builder", + "default", + "-f", + "agents/hermes/Dockerfile", + "--build-arg", + "BASE_IMAGE=nemoclaw-hermes-base-local", + "-t", + "nemoclaw-hermes-production", + ".", + ]); + }); + it("pulls a remote image and accepts a compatible glibc version", () => { const bin = fakeDocker(` if [[ "$1" == pull ]]; then exit 0; fi diff --git a/test/runtime/messaging/messaging-build-applier-googlechat.test.ts b/test/runtime/messaging/messaging-build-applier-googlechat.test.ts index 164c10701e2..cbc6d15f760 100644 --- a/test/runtime/messaging/messaging-build-applier-googlechat.test.ts +++ b/test/runtime/messaging/messaging-build-applier-googlechat.test.ts @@ -56,7 +56,7 @@ function officialPluginFixture(channelId: string) { 'const fs = require("node:fs");', "const args = process.argv.slice(2);", 'fs.appendFileSync(process.env.OPENCLAW_TRACE, `openclaw|${args.join("|")}|offline=${process.env.NPM_CONFIG_OFFLINE || ""}/${process.env.npm_config_offline || ""}\\n`);', - 'if (args[0] === "plugins" && args[1] === "install" && process.env.OPENCLAW_CACHE_MISS === "1") { if (process.env.NPM_CONFIG_OFFLINE !== "true" || process.env.npm_config_offline !== "true") fs.appendFileSync(process.env.OPENCLAW_TRACE, "registry-fallback\\n"); process.exit(44); }', + 'if (args[0] === "plugins" && args[1] === "install" && process.env.OPENCLAW_CACHE_MISS === "1") { if (process.env.NPM_CONFIG_OFFLINE !== "true" || process.env.npm_config_offline !== "true") fs.appendFileSync(process.env.OPENCLAW_TRACE, "registry-fallback\\n"); process.stdout.write(process.env.OPENCLAW_INSPECTION_CANARY || ""); process.stderr.write("npm error code ENOTCACHED\\nnpm error request to https://registry.npmjs.org/@openclaw%2fdiscord?token=" + process.env.OPENCLAW_INSPECTION_CANARY); process.exit(44); }', 'if (args[0] === "plugins" && args[1] === "install") process.exit(args[4] === `npm:${process.env.OPENCLAW_PLUGIN_SPEC}` ? 0 : 41);', 'if (args[1] === "inspect" && process.env.OPENCLAW_INSPECTION_HANG === "1") { setInterval(() => {}, 1000); return; }', 'if (args[0] === "plugins" && args[1] === "inspect") { process.stderr.write(process.env.OPENCLAW_INSPECTION_CANARY || ""); process.stdout.write(JSON.stringify({ plugin: { id: process.env.OPENCLAW_PLUGIN_ID, trustedOfficialInstall: process.env.OPENCLAW_TRUSTED !== "false", diagnostic: process.env.OPENCLAW_INSPECTION_CANARY }, install: { ...(process.env.OPENCLAW_ARCHIVE_FIELD ? { [process.env.OPENCLAW_ARCHIVE_FIELD]: "retained-local-archive" } : {}), source: "npm", resolvedSpec: process.env.OPENCLAW_PLUGIN_SPEC, integrity: process.env.OPENCLAW_PLUGIN_INTEGRITY } })); process.exit(0); }', @@ -165,12 +165,15 @@ it.each(["slack", "discord", "teams", "whatsapp", "googlechat"])( "Report this failure, the plugin name and your NemoClaw version", ); expect(failedInspection.stdout + failedInspection.stderr).not.toContain(canary); - expect(() => - applyMessagingBuildPhase(serializedPlan, "agent-install", { - ...env, - OPENCLAW_CACHE_MISS: "1", - }), - ).toThrow(); + const failedInstall = spawnSync(process.execPath, CLI_ARGS, { + encoding: "utf8", + env: { ...env, OPENCLAW_CACHE_MISS: "1", OPENCLAW_INSPECTION_CANARY: canary }, + }); + expect(failedInstall.status).toBe(2); + expect(failedInstall.stderr).toContain('"exitCode":44'); + expect(failedInstall.stderr).toContain('"npmCodes":["ENOTCACHED"]'); + expect(failedInstall.stderr).toContain('"registryPaths":["/@openclaw%2fdiscord"]'); + expect(failedInstall.stdout + failedInstall.stderr).not.toContain(canary); expect(fs.readFileSync(tracePath, "utf8")).not.toContain("registry-fallback"); expect(remainingPackedDirectories(packedDirectories)).toEqual([]); } finally { diff --git a/test/runtime/sandbox/sandbox-base-security-packages.test.ts b/test/runtime/sandbox/sandbox-base-security-packages.test.ts index 79948325bfd..ee2ee2615c1 100644 --- a/test/runtime/sandbox/sandbox-base-security-packages.test.ts +++ b/test/runtime/sandbox/sandbox-base-security-packages.test.ts @@ -68,7 +68,7 @@ const EXPECTED_SECURITY_PACKAGE_INVENTORY = [ "vim-common=2:9.2.0858-1", "vim-tiny=2:9.2.0858-1", "libssh2-1t64=1.11.1-1+deb13u1+nemoclaw2", - "libssl3t64=3.5.7-1~deb13u2", + "libssl3t64=3.5.7-1~deb13u3", "nemoclaw-python3.13-htmlparser-fix=3.13.5-2+deb13u5+nemoclaw1", "perl-base=5.44.0-1nemoclaw1", "perl=5.44.0-1nemoclaw1", diff --git a/test/security/fetch-guard-patch-regression.test.ts b/test/security/fetch-guard-patch-regression.test.ts index bf56544bcd9..f3347048597 100644 --- a/test/security/fetch-guard-patch-regression.test.ts +++ b/test/security/fetch-guard-patch-regression.test.ts @@ -90,7 +90,7 @@ function readDockerfileMcporterIntegrity(): string { function readDockerfileOpenClawIntegrity(): string { return readRequiredMatch( DOCKERFILE, - /^ARG OPENCLAW_2026_9_1_INTEGRITY=([^\s]+)/m, + /^ARG OPENCLAW_2026_9_2_INTEGRITY=([^\s]+)/m, "OpenClaw runtime integrity", ); } @@ -98,7 +98,7 @@ function readDockerfileOpenClawIntegrity(): string { function readDockerfileOpenClawTarball(): string { return readRequiredMatch( DOCKERFILE, - /^ARG OPENCLAW_2026_9_1_TARBALL=([^\s]+)/m, + /^ARG OPENCLAW_2026_9_2_TARBALL=([^\s]+)/m, "OpenClaw runtime tarball", ); } @@ -183,8 +183,8 @@ function runOpenClawUpgradeBlock(currentVersion: string) { `OPENCLAW_VERSION=${JSON.stringify(openclawVersion)}`, `BASE_IMAGE=${JSON.stringify("registry.example/nemoclaw-test-base:latest")}`, `MCPORTER_VERSION=${JSON.stringify(expectedMcporterVersion)}`, - `OPENCLAW_2026_9_1_INTEGRITY=${JSON.stringify(openclawIntegrity)}`, - `OPENCLAW_2026_9_1_TARBALL=${JSON.stringify(openclawTarball)}`, + `OPENCLAW_2026_9_2_INTEGRITY=${JSON.stringify(openclawIntegrity)}`, + `OPENCLAW_2026_9_2_TARBALL=${JSON.stringify(openclawTarball)}`, `MCPORTER_0_7_3_INTEGRITY=${JSON.stringify(mcporterIntegrity)}`, `MCPORTER_0_7_3_TARBALL=${JSON.stringify(mcporterTarball)}`, "node() {", @@ -208,8 +208,8 @@ function runOpenClawUpgradeBlock(currentVersion: string) { " return 0;", " fi", ' [ "$#" -eq 10 ] && [ "${3:-}" = "--package-spec" ] && [ "${4:-}" = "openclaw@${OPENCLAW_VERSION}" ] || return 95;', - ' [ "${5:-}" = "--integrity" ] && [ "${6:-}" = "$OPENCLAW_2026_9_1_INTEGRITY" ] || return 96;', - ' [ "${7:-}" = "--tarball-url" ] && [ "${8:-}" = "$OPENCLAW_2026_9_1_TARBALL" ] || return 97;', + ' [ "${5:-}" = "--integrity" ] && [ "${6:-}" = "$OPENCLAW_2026_9_2_INTEGRITY" ] || return 96;', + ' [ "${7:-}" = "--tarball-url" ] && [ "${8:-}" = "$OPENCLAW_2026_9_2_TARBALL" ] || return 97;', ' [ "${9:-}" = "--label" ] && [ "${10:-}" = "OpenClaw ${OPENCLAW_VERSION}" ] || return 98;', ' printf "npm pack %s --pack-destination reviewed-temp\\n" "${8:-}" >> "$call_log";', ' printf "%s\\n" "$reviewed_archive"; return 0;', @@ -221,7 +221,7 @@ function runOpenClawUpgradeBlock(currentVersion: string) { "npm() {", ' printf "npm %s\\n" "$*" >> "$call_log";', ' if [ "${1:-}" = "view" ] && [ "${2:-}" = "openclaw@${OPENCLAW_VERSION}" ] && [ "${3:-}" = "dist.integrity" ]; then', - ' printf "%s\\n" "$OPENCLAW_2026_9_1_INTEGRITY";', + ' printf "%s\\n" "$OPENCLAW_2026_9_2_INTEGRITY";', " return 0", " fi", ' if [ "${1:-}" = "view" ] && [ "${2:-}" = "mcporter@${MCPORTER_VERSION}" ] && [ "${3:-}" = "dist.integrity" ]; then', @@ -229,7 +229,7 @@ function runOpenClawUpgradeBlock(currentVersion: string) { " return 0", " fi", ' if [ "${1:-}" = "view" ] && [ "${2:-}" = "openclaw@${OPENCLAW_VERSION}" ] && [ "${3:-}" = "dist.tarball" ]; then', - ' printf "%s\\n" "$OPENCLAW_2026_9_1_TARBALL";', + ' printf "%s\\n" "$OPENCLAW_2026_9_2_TARBALL";', " return 0", " fi", ' if [ "${1:-}" = "pack" ]; then', @@ -241,7 +241,7 @@ function runOpenClawUpgradeBlock(currentVersion: string) { ' test -n "$pack_dir";', ' pack_file="openclaw-${OPENCLAW_VERSION}.tgz";', ' printf "fake openclaw tarball" > "$pack_dir/$pack_file";', - ' printf \'[{"filename":"%s","integrity":"%s"}]\\n\' "$pack_file" "$OPENCLAW_2026_9_1_INTEGRITY";', + ' printf \'[{"filename":"%s","integrity":"%s"}]\\n\' "$pack_file" "$OPENCLAW_2026_9_2_INTEGRITY";', " return 0", " fi", ' if [ "${1:-}" = "install" ]; then return 0; fi', @@ -371,7 +371,7 @@ describe("fetch-guard patch regression guard", () => { expect(current.calls).not.toContain("npm install -g"); expect(current.calls).not.toContain("npm pack"); - const newer = runOpenClawUpgradeBlock("2026.9.2"); + const newer = runOpenClawUpgradeBlock("2026.9.3"); expect(newer.result.status).toBe(1); expect(newer.result.stderr).toContain( "newer than reviewed target " + CURRENT_REVIEWED_OPENCLAW_PATCH_CLASSIFIER_VERSION, @@ -1316,7 +1316,7 @@ if (!blocked) throw new Error('private IP literal was not blocked');`, const patch = runFetchGuardPatchBlock(dist, tmp); expect(patch.status, `${patch.stdout}${patch.stderr}`).toBe(0); expect(patch.stdout).toContain( - "Patch 6 applied to OpenClaw 2026.9.1 cron preflight trusted env-proxy", + "Patch 6 applied to OpenClaw 2026.9.2 cron preflight trusted env-proxy", ); const patched = fs.readFileSync(preflightPath, "utf-8"); expect( @@ -1372,7 +1372,7 @@ if (!blocked) throw new Error('private IP literal was not blocked');`, const patch = runFetchGuardPatchBlock(dist, tmp); expect(patch.status, `${patch.stdout}${patch.stderr}`).toBe(0); expect(patch.stdout).toContain( - "OpenClaw 2026.9.1 has no cron model-provider preflight; Patch 6 not needed", + "OpenClaw 2026.9.2 has no cron model-provider preflight; Patch 6 not needed", ); } finally { fs.rmSync(tmp, { recursive: true, force: true }); diff --git a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle index b9aa39d76fd..919662cf0d2 100644 --- a/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle +++ b/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-direct-image-runtime.bundle @@ -1,6 +1,6 @@ "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:true}):target,mod));var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var managed_startup_direct_entry_exports={};__export(managed_startup_direct_entry_exports,{MANAGED_STARTUP_CA_ENV:()=>MANAGED_STARTUP_CA_ENV,MANAGED_STARTUP_COMPLETION_FILE:()=>MANAGED_STARTUP_COMPLETION_FILE,MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION:()=>MANAGED_STARTUP_COMPLETION_SCHEMA_VERSION,MANAGED_STARTUP_MERGED_CA_FILE:()=>MANAGED_STARTUP_MERGED_CA_FILE,MANAGED_STARTUP_PROFILE_ENV:()=>MANAGED_STARTUP_PROFILE_ENV,MANAGED_STARTUP_RUNTIME_ENV_FILE:()=>MANAGED_STARTUP_RUNTIME_ENV_FILE,MANAGED_STARTUP_RUNTIME_EXECUTABLE:()=>MANAGED_STARTUP_RUNTIME_EXECUTABLE,ManagedStartupImageActionPlanError:()=>ManagedStartupImageActionPlanError,ManagedStartupImageRuntimeError:()=>ManagedStartupImageRuntimeError,applyManagedStartupCommandEnvironmentPlan:()=>applyManagedStartupCommandEnvironmentPlan,applyManagedStartupImageProfile:()=>applyManagedStartupImageProfile,applyManagedStartupRootRequest:()=>applyManagedStartupRootRequest,atomicWriteRootFile:()=>atomicWriteRootFile,buildManagedStartupImageActionPlan:()=>buildManagedStartupImageActionPlan,installCorporateCaSystemAnchors:()=>installCorporateCaSystemAnchors,installHermesManagedPolicy:()=>installHermesManagedPolicy,main:()=>main,managedStartupSandboxPrefix:()=>managedStartupSandboxPrefix,normalizeHermesManagedConfigDescriptor:()=>normalizeHermesManagedConfigDescriptor,normalizeManagedStartupWorkspaceRoot:()=>normalizeManagedStartupWorkspaceRoot,publishManagedStartupCompletionAfterCommit:()=>publishManagedStartupCompletionAfterCommit,readStableRegularFile:()=>readStableRegularFile,readStableRegularFileSnapshot:()=>readStableRegularFileSnapshot,serializeManagedStartupCompletionMarker:()=>serializeManagedStartupCompletionMarker,serializeManagedStartupRuntimeEnvironment:()=>serializeManagedStartupRuntimeEnvironment,validateManagedStartupApplicationRuntimePlan:()=>validateManagedStartupApplicationRuntimePlan,verifyManagedStartupImageCompletion:()=>verifyManagedStartupImageCompletion,waitForManagedStartupImageCompletion:()=>waitForManagedStartupImageCompletion});module.exports=__toCommonJS(managed_startup_direct_entry_exports);var 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 MAX_CORPORATE_CA_BYTES=128*1024;var PEM_CERTIFICATE_RE_GLOBAL=/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g;var SHIPPED_MANAGED_IMAGE_AGENTS=["openclaw","hermes","langchain-deepagents-code"];var CANDIDATE_MANAGED_IMAGE_AGENTS=["pi"];var MANAGED_IMAGE_AGENTS=[...SHIPPED_MANAGED_IMAGE_AGENTS,...CANDIDATE_MANAGED_IMAGE_AGENTS];var MANAGED_IMAGE_RUNTIME_IDENTITIES=Object.freeze({openclaw:Object.freeze({uid:998,gid:998,workdir:"/sandbox"}),hermes:Object.freeze({uid:998,gid:999,workdir:"/sandbox"}),"langchain-deepagents-code":Object.freeze({uid:999,gid:999,workdir:"/sandbox"}),pi:Object.freeze({uid:999,gid:999,workdir:"/sandbox"})});function managedImageRuntimeIdentity(agent){return MANAGED_IMAGE_RUNTIME_IDENTITIES[agent]}var import_node_buffer2=require("node:buffer");var PROVIDERLESS_INFERENCE_ENV=Object.freeze({NEMOCLAW_MODEL:"",NEMOCLAW_INFERENCE_PROVIDER_ID:"",NEMOCLAW_UPSTREAM_PROVIDER:"",NEMOCLAW_INFERENCE_BASE_URL:"",NEMOCLAW_INFERENCE_API:""});function isObjectRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}var ChannelManifestRegistry=class{manifests=new Map;constructor(manifests=[]){for(const manifest of manifests){this.register(manifest)}}register(manifest){if(this.manifests.has(manifest.id)){throw new Error(`Duplicate channel manifest id '${manifest.id}'`)}this.manifests.set(manifest.id,manifest);return this}get(channelId){return this.manifests.get(channelId)}list(){return Array.from(this.manifests.values())}listAvailable(ctx={}){const supportedChannelIds=Array.isArray(ctx.supportedChannelIds)?new Set(ctx.supportedChannelIds):null;return this.list().filter(manifest=>{if(ctx.agent&&!manifest.supportedAgents.includes(ctx.agent)){return false}if(supportedChannelIds&&!supportedChannelIds.has(manifest.id)){return false}return true})}};function createChannelManifestRegistry(manifests=[]){return new ChannelManifestRegistry(manifests)}var discordManifest={schemaVersion:1,id:"discord",displayName:"Discord",description:"Discord bot messaging",supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"DISCORD_BOT_TOKEN",formatPattern:"^(?!$)\\S+$",formatHint:"Replace the documentation placeholder with your real Discord bot token.",prompt:{label:"Discord Bot Token",help:"Discord Developer Portal \u2192 Applications \u2192 Bot \u2192 Reset/Copy Token."}},{id:"serverId",kind:"config",required:false,envKey:"DISCORD_SERVER_ID",statePath:"discordGuilds.serverId",prompt:{label:"Discord Server ID (for guild workspace access)",help:"Enable Developer Mode in Discord, then right-click your server and copy the Server ID.",emptyValueMessage:"guild channels stay disabled"}},{id:"requireMention",kind:"config",required:false,envKey:"DISCORD_REQUIRE_MENTION",statePath:"discordGuilds.requireMention",promptWhenInput:"serverId",validValues:["0","1"],defaultValue:"1",prompt:{label:"Discord mention mode",help:"Choose whether the bot should reply only when @mentioned or to all messages in this server."}},{id:"userId",kind:"config",required:false,envKey:"DISCORD_USER_ID",statePath:"discordGuilds.userIds",promptWhenInput:"serverId",prompt:{label:"Discord User ID (optional guild allowlist)",help:"Optional: enable Developer Mode in Discord, then right-click your user/avatar and copy the User ID. Leave blank to allow any member of the configured server to message the bot.",emptyValueMessage:"any member in the configured server can message the bot"}}],credentials:[{id:"discordBotToken",sourceInput:"botToken",providerName:"{sandboxName}-discord-bridge",providerEnvKey:"DISCORD_BOT_TOKEN",placeholder:"openshell:resolve:env:DISCORD_BOT_TOKEN"}],policyPresets:[{name:"discord",requiredAtCreate:true,validationWarningLines:["For Discord preset validation, do not use curl as the success signal:","curl is not in the preset binary allowlist, so curl probes can fail even","when the policy is working. Validate the configured messaging bridge/gateway path.",'DNS-only checks such as dns.resolve("gateway.discord.gg")',"can also be inconclusive behind a proxy.","The agent-specific gateway probe prints an HTTP status when it reaches Discord.","Any HTTP response confirms reachability. A transport error or OpenShell policy","denial means validation failed."],validationWarningLinesByAgent:{openclaw:["OpenClaw validation uses its Node runtime:",`node -e "require('node:https').get('https://discord.com/api/v10/gateway',r=>console.log(r.statusCode)).on('error',e=>{console.error(e.message);process.exitCode=1})"`],hermes:["Hermes validation uses its virtual-environment Python runtime:",`nemohermes exec -- /opt/hermes/.venv/bin/python -c "import urllib.error, urllib.request; u='https://discord.com/api/v10/gateway'; try: print(urllib.request.urlopen(u, timeout=20).status) -except urllib.error.HTTPError as error: print(error.code)"`]}}],render:[{id:"discord-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.discord",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},proxy:"{{discordProxyUrl}}",dmPolicy:"{{discord.allowedUsers.dmPolicy}}",allowFrom:"{{discord.allowedUsers.values}}"}}}}},{id:"discord-openclaw-guilds",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",when:"{{discord.hasGuilds}}",fragment:{path:"channels.discord",value:{groupPolicy:"allowlist",guilds:"{{discord.guilds}}"}}},{id:"discord-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.discord",value:{enabled:true}}},{id:"discord-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["NEMOCLAW_DISCORD_GUILD_IDS={{discord.guildIds.csv}}","DISCORD_ALLOWED_USERS={{discord.allowedUsers.csv}}","DISCORD_ALLOW_ALL_USERS={{discord.allowAllUsers}}"]},{id:"discord-hermes-config",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"discord",value:{require_mention:"{{discord.requireMention}}",free_response_channels:"",allowed_channels:"",auto_thread:true,reactions:true,channel_prompts:{}}}},{id:"discord-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.discord",value:{enabled:true}}}],runtime:{openclaw:{channelName:"discord",visibility:{configKeys:["discord"],logPatterns:["discord"]}}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/discord@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.1":"sha512-qNmN2a8A9dET4igPp0RML171sEn8PDMyNCYNp/DqcJ4tn3XTHpacSOTkqBmv5yXTycJRC9rfFP8FT/SdW0Rldg=="},tarballUrlByVersion:{"2026.9.1":"https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.1.tgz"},required:true}],hooks:[{id:"discord-openclaw-bridge-health",phase:"health-check",handler:"discord.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"discord-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"discord-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"serverId",kind:"config"},{id:"requireMention",kind:"config"},{id:"userId",kind:"config"}]}]};var googlechatManifest={schemaVersion:1,id:"googlechat",displayName:"Google Chat",description:"Google Chat (Chat API) bot messaging (experimental)",supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"serviceAccount",kind:"secret",required:true,envKey:"GOOGLECHAT_SERVICE_ACCOUNT",allowLineBreaks:true,maskCap:40,formatHint:"Paste the entire service-account JSON key on one line (minified), or set GOOGLECHAT_SERVICE_ACCOUNT to the downloaded JSON, including line breaks.",maxTokenAttempts:3,prompt:{label:"Google Chat service account JSON",help:["\u2503 GOOGLE CHAT \u2014 service account key","\u2503","\u2503 Google Cloud Console \u2192 IAM & Admin \u2192 Service Accounts","\u2503 \u2192 your bot's SA \u2192 Keys \u2192 Add key \u2192 Create new key \u2192 JSON","\u2503","\u2503 A .json file downloads. Paste its contents below as ONE line (minified).","\u2503 Alternatively, GOOGLECHAT_SERVICE_ACCOUNT accepts the formatted JSON file contents.",""].join("\n")}},{id:"audienceType",kind:"config",required:false,envKey:"GOOGLECHAT_AUDIENCE_TYPE",statePath:"googlechatConfig.audienceType",validValues:["app-url","project-number"],defaultValue:"app-url"},{id:"audience",kind:"config",required:false,envKey:"GOOGLECHAT_AUDIENCE",statePath:"googlechatConfig.audience",prompt:{label:"Google Chat webhook audience",help:"Usually filled automatically from the public tunnel URL. For audienceType 'project-number', enter your GCP project number instead.",emptyValueMessage:"inbound webhook verification will be unconfigured"}},{id:"appPrincipal",kind:"config",required:false,envKey:"GOOGLECHAT_APP_PRINCIPAL",statePath:"googlechatConfig.appPrincipal",formatPattern:"^[0-9]{6,32}$",formatHint:"appPrincipal is the add-on's numeric OAuth client ID (uniqueId, ~21 digits), not an email.",prompt:{label:"Google Chat appPrincipal",help:[" Workspace account \u2192 leave blank, done."," Personal Gmail \u2192 needs the add-on's ~21-digit ID (not an email), stable across rebuilds.",""," If you already know it, paste it at the prompt and you're done."," If not, leave it blank \u2014 the first DM reveals it once the sandbox is live:",""," 1. Watch the gateway log:",' nemoclaw logs --follow | grep "unexpected add-on principal"'," 2. DM the bot once \u2014 it won't reply yet, that's expected. The log prints:"," unexpected add-on principal: "," 3. Save that and rebuild:"," GOOGLECHAT_APP_PRINCIPAL= nemoclaw channels add googlechat"," nemoclaw rebuild --yes"].join("\n"),emptyValueMessage:"Workspace accounts do not need it; personal accounts must set it later"}},{id:"allowFrom",kind:"config",required:false,envKey:"GOOGLECHAT_ALLOWED_USERS",statePath:"allowedIds.googlechat",prompt:{label:"Google Chat DM allowlist (comma-separated)",help:["Optional: restrict who can DM the bot."," OpenClaw: users/NNN (emails ignored)"," Hermes: email (users/NNN ignored)"," Blank: pairing mode (recommended) \u2014 OpenClaw's pairing reply shows your users/NNN"," Filling this switches DM policy to allowlist \u2014 a wrong-form entry is dropped silently, with no pairing code."].join("\n"),emptyValueMessage:"bot will require manual pairing"}},{id:"projectId",kind:"config",required:false,envKey:"GOOGLE_CHAT_PROJECT_ID",statePath:"googlechatConfig.projectId",prompt:{label:"Google Chat GCP project ID (Hermes Pub/Sub pull)",help:"The Google Cloud project that owns the Pub/Sub subscription Hermes pulls Chat events from. OpenClaw ignores this.",emptyValueMessage:"required for the Hermes Google Chat channel"}},{id:"subscriptionName",kind:"config",required:false,envKey:"GOOGLE_CHAT_SUBSCRIPTION_NAME",statePath:"googlechatConfig.subscriptionName",prompt:{label:"Google Chat Pub/Sub subscription (projects/

/subscriptions/)",help:["The pull subscription bound to the Chat events topic. Hermes pulls from it over the Pub/Sub REST API; the gateway-minted token is scoped to both chat.bot and pubsub."," Its topic must grant roles/pubsub.publisher to the app's push account:"," Interactive features service-@gcp-sa-gsuiteaddons.iam.gserviceaccount.com"," Classic bot chat-api-push@system.gserviceaccount.com"," Shown at Chat API \u2192 Configuration \u2192 Connection settings"," Missing it channel connects, no event arrives, Chat says the bot is not responding"].join("\n"),emptyValueMessage:"required for the Hermes Google Chat channel"}}],credentials:[],policyPresets:[{name:"googlechat",policyKeys:["googlechat"],agentPolicyKeys:{hermes:["googlechat_hermes"]}}],render:[{id:"googlechat-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.googlechat",value:{enabled:true,serviceAccount:{},audienceType:"{{googlechatConfig.audienceType}}",audience:"{{googlechatConfig.audience}}",appPrincipal:"{{googlechatConfig.appPrincipal}}",webhookPath:"/googlechat",healthMonitor:{enabled:false},dmPolicy:"{{allowedIds.googlechat.dmPolicy}}",allowFrom:"{{allowedIds.googlechat.values}}"}}},{id:"googlechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.googlechat",value:{enabled:true}}},{id:"googlechat-openclaw-gateway-reload-off",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"gateway.reload",value:{mode:"off"}}},{id:"googlechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["GOOGLE_CHAT_PROJECT_ID={{googlechatConfig.projectId}}","GOOGLE_CHAT_SUBSCRIPTION_NAME={{googlechatConfig.subscriptionName}}","GOOGLE_CHAT_ALLOWED_USERS={{allowedIds.googlechat.csv}}"]},{id:"googlechat-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.google_chat",value:{enabled:true}}}],runtime:{openclaw:{channelName:"googlechat",visibility:{configKeys:["googlechat"],logPatterns:["googlechat"]},nodePreloads:[{module:"googlechat-trusted-proxy-fetch",injectInto:["boot"],optional:false,installMessage:"[channels] Installing Google Chat trusted-proxy-fetch patch (route googleapis via trusted env proxy)",installedMessage:"[channels] Google Chat trusted-proxy-fetch patch installed (NODE_OPTIONS updated)"},{module:"googlechat-outbound-auth",injectInto:["boot"],optional:false,installMessage:"[channels] Installing Google Chat outbound-auth patch (gateway-minted bearer)",installedMessage:"[channels] Google Chat outbound-auth patch installed (NODE_OPTIONS updated)"}],secretScans:[{path:"/sandbox/.openclaw/openclaw.json",pattern:"-----BEGIN (?:RSA )?PRIVATE KEY-----",message:"[SECURITY] Google Chat service account private key leaked into {path} - refusing to serve",exitCode:78}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/googlechat@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.1":"sha512-Q5VTAJpfcrI7BSEw5Ugq3wf7JEg5QhTBwpi+BByGbfZsTTVjwZc7OIvNbKsVTh16I5/EWqHEnD+0WNeHqsteqw=="},tarballUrlByVersion:{"2026.9.1":"https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.1.tgz"},required:true},{id:"hermesGooglePubsubPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-cloud-pubsub==2.39.0",required:true},{id:"hermesGoogleApiClientPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-api-python-client==2.194.0",required:true},{id:"hermesGoogleAuthPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-auth==2.55.1",required:true}],hooks:[{id:"googlechat-tunnel-audience-gate",phase:"enroll",handler:"googlechat.tunnelAudienceGate",agents:["openclaw"],inputs:["audienceType","audience"],outputs:[{id:"audience",kind:"config"}],onFailure:"skip-channel"},{id:"googlechat-service-account",phase:"enroll",handler:"googlechat.tokenPaste",outputs:[{id:"serviceAccount",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"googlechat-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowFrom",kind:"config"}]},{id:"googlechat-openclaw-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["openclaw"],outputs:[{id:"appPrincipal",kind:"config"}]},{id:"googlechat-hermes-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["hermes"],outputs:[{id:"projectId",kind:"config"},{id:"subscriptionName",kind:"config"}]}]};var slackManifest={schemaVersion:1,id:"slack",displayName:"Slack",description:"Slack bot messaging",supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"SLACK_BOT_TOKEN",formatPattern:"^xoxb-[A-Za-z0-9_-]+$",formatHint:"Slack bot tokens start with 'xoxb-' (e.g. xoxb---).",prompt:{label:"Slack Bot Token",help:"Slack API \u2192 Your Apps \u2192 OAuth & Permissions \u2192 Bot User OAuth Token (xoxb-...)."}},{id:"appToken",kind:"secret",required:true,envKey:"SLACK_APP_TOKEN",formatPattern:"^xapp-[A-Za-z0-9_-]+$",formatHint:"Slack app tokens start with 'xapp-' (e.g. xapp----).",prompt:{label:"Slack App Token (Socket Mode)",help:"Slack API \u2192 Your Apps \u2192 Basic Information \u2192 App-Level Tokens (xapp-...)."}},{id:"allowedUsers",kind:"config",required:false,envKey:"SLACK_ALLOWED_USERS",statePath:"allowedIds.slack",prompt:{label:"Slack Member IDs (comma-separated allowlist)",help:"In Slack, open each allowed human user's profile -> More -> Copy member ID. Enter one or more comma-separated member IDs, not the app or bot user ID. Member IDs look like U01ABC2DEF3.",emptyValueMessage:"bot will require manual pairing"}},{id:"allowedChannels",kind:"config",required:false,envKey:"SLACK_ALLOWED_CHANNELS",statePath:"slackConfig.allowedChannels",prompt:{label:"Slack Channel IDs (comma-separated allowlist)",help:"Optional: enter comma-separated Slack channel IDs where the bot may answer @mentions. Channel IDs look like C012AB3CD.",emptyValueMessage:"channel @mentions stay unrestricted by channel ID"}}],credentials:[{id:"slackBotToken",sourceInput:"botToken",providerName:"{sandboxName}-slack-bridge",providerEnvKey:"SLACK_BOT_TOKEN",placeholder:"xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN",primary:true},{id:"slackAppToken",sourceInput:"appToken",providerName:"{sandboxName}-slack-app",providerEnvKey:"SLACK_APP_TOKEN",placeholder:"xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN"}],policyPresets:[{name:"slack",requiredAtCreate:true}],render:[{id:"slack-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.slack",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},dmPolicy:"{{allowedIds.slack.dmPolicy}}",allowFrom:"{{allowedIds.slack.values}}",groupPolicy:"{{allowedIds.slack.groupPolicy}}",channels:"{{allowedIds.slack.channels}}"}}}}},{id:"slack-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.slack",value:{enabled:true}}},{id:"slack-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["SLACK_ALLOWED_USERS={{allowedIds.slack.csv}}","SLACK_ALLOWED_CHANNELS={{slackConfig.allowedChannels.csv}}"]},{id:"slack-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.slack",value:{enabled:true,extra:{rich_blocks:true}}}}],runtime:{openclaw:{channelName:"slack",visibility:{configKeys:["slack"],logPatterns:["slack"]},nodePreloads:[{module:"slack-channel-guard",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Slack channel guard (unhandled-rejection safety net)",installedMessage:"[channels] Slack channel guard installed (NODE_OPTIONS updated)"}],secretScans:[{path:"/sandbox/.openclaw/openclaw.json",pattern:"(?:xoxb|xapp)-(?!OPENSHELL-RESOLVE-ENV-)",message:"[SECURITY] Slack token leaked into {path} - refusing to serve",exitCode:78}]},hermes:{}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/slack@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.1":"sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg=="},tarballUrlByVersion:{"2026.9.1":"https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.1.tgz"},required:true}],hooks:[{id:"slack-socket-mode-gateway-conflict",phase:"pre-enable",handler:"slack.socketModeGatewayConflict",onFailure:"abort"},{id:"slack-openclaw-bridge-health",phase:"health-check",handler:"slack.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"slack-socket-mode-gateway-status",phase:"status",handler:"slack.socketModeGatewayStatus",outputs:[{id:"gatewayOverlaps",kind:"status"}]},{id:"slack-status-health",phase:"status",handler:"slack.statusHealth",providesReadiness:true,agents:["openclaw"],outputs:[{id:"channelHealth",kind:"status"}]},{id:"slack-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true},{id:"appToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"slack-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowedUsers",kind:"config"},{id:"allowedChannels",kind:"config"}]},{id:"slack-credential-validation",phase:"reachability-check",handler:"slack.validateCredentials",inputs:["botToken","appToken"],onFailure:"skip-channel"}]};var TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT={channelId:"teams",renderId:"teams-openclaw-channel",hookId:"teams-openclaw-channel",handlerId:"common.staticOutputs",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",configPath:"channels.msteams",webhookPath:"/api/messages"};function authorizeTeamsOpenClawWebhookField(entry){if(!isPlainDataObject(entry))return[];const contract=TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT;if(ownDataPropertyValue(entry,"channelId")!==contract.channelId||ownDataPropertyValue(entry,"renderId")!==contract.renderId||ownDataPropertyValue(entry,"hookId")!==contract.hookId||ownDataPropertyValue(entry,"handler")!==contract.handlerId||ownDataPropertyValue(entry,"kind")!==contract.kind||ownDataPropertyValue(entry,"agent")!==contract.agent||ownDataPropertyValue(entry,"target")!==contract.target||ownDataPropertyValue(entry,"path")!==contract.configPath){return[]}const value=ownDataPropertyValue(entry,"value");if(!isPlainDataObject(value))return[];const webhook=ownDataPropertyValue(value,"webhook");if(!isPlainDataObject(webhook)||!hasExactlyOwnDataProperties(webhook,["path","port"])||!isTcpPort(ownDataPropertyValue(webhook,"port"))||ownDataPropertyValue(webhook,"path")!==contract.webhookPath){return[]}return[{path:["value","webhook"],value:webhook}]}function isPlainDataObject(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function hasExactlyOwnDataProperties(value,expected){const actual=Object.getOwnPropertyNames(value).sort();return actual.length===expected.length&&actual.every((key,index)=>key===expected[index])}function isTcpPort(value){return Number.isInteger(value)&&value>=1&&value<=65535}var teamsManifest={schemaVersion:1,id:"teams",displayName:"Microsoft Teams",description:"Microsoft Teams bot messaging (experimental)",enrollmentNotes:["Microsoft Teams requires a public HTTPS webhook endpoint at /api/messages; expose the configured Teams webhook port before installing the Teams app.","Use Azure AD object IDs in TEAMS_ALLOWED_USERS so only authorized users can interact with the bot."],supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"appId",kind:"config",required:true,envKey:"MSTEAMS_APP_ID",statePath:"teamsConfig.appId",prompt:{label:"Microsoft Teams Client ID",help:"Run `teams app create --endpoint https:///api/messages`, then copy CLIENT_ID."}},{id:"clientSecret",kind:"secret",required:true,envKey:"MSTEAMS_APP_PASSWORD",prompt:{label:"Microsoft Teams Client Secret",help:"Use the CLIENT_SECRET printed by `teams app create`. It is shown once; rotate it in Entra ID if it was lost."}},{id:"tenantId",kind:"config",required:true,envKey:"MSTEAMS_TENANT_ID",statePath:"teamsConfig.tenantId",prompt:{label:"Microsoft Teams Tenant ID",help:"Use the TENANT_ID printed by `teams app create` or shown by `teams status --verbose`."}},{id:"allowedUsers",kind:"config",required:false,envKey:"TEAMS_ALLOWED_USERS",statePath:"allowedIds.teams",prompt:{label:"Microsoft Teams AAD Object IDs (comma-separated allowlist)",help:"Recommended: run `teams status --verbose` and enter the Azure AD object IDs allowed to use the bot."}},{id:"webhookPort",kind:"config",required:false,envKey:"MSTEAMS_PORT",statePath:"teamsConfig.webhookPort",defaultValue:"3978",prompt:{label:"Microsoft Teams webhook port",help:"Local bot webhook port to expose publicly. Defaults to 3978 and serves /api/messages."}},{id:"requireMention",kind:"config",required:false,envKey:"TEAMS_REQUIRE_MENTION",statePath:"teamsConfig.requireMention",validValues:["0","1"],defaultValue:"1",prompt:{label:"Microsoft Teams mention mode",help:"Controls OpenClaw group and channel behavior only. Direct messages are unaffected."}}],credentials:[{id:"teamsClientSecret",sourceInput:"clientSecret",providerName:"{sandboxName}-teams-bridge",providerEnvKey:"MSTEAMS_APP_PASSWORD",placeholder:"openshell:resolve:env:MSTEAMS_APP_PASSWORD",primary:true}],policyPresets:[{name:"teams",policyKeys:["teams"],requiredAtCreate:true}],hostForward:{port:"{{teamsConfig.webhookPort}}",label:"Microsoft Teams webhook"},render:[{id:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.renderId,kind:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.kind,agent:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.agent,target:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.target,fragment:{path:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.configPath,value:{enabled:true,appId:"{{teamsConfig.appId}}",tenantId:"{{teamsConfig.tenantId}}",webhook:{port:"{{teamsConfig.webhookPort}}",path:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.webhookPath},healthMonitor:{enabled:false},streaming:{mode:"off"},dmPolicy:"{{allowedIds.teams.dmPolicy}}",allowFrom:"{{allowedIds.teams.values}}",groupPolicy:"open",requireMention:"{{teamsConfig.requireMention}}"}}},{id:"teams-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.msteams",value:{enabled:true}}},{id:"teams-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["TEAMS_CLIENT_ID={{teamsConfig.appId}}","TEAMS_TENANT_ID={{teamsConfig.tenantId}}","TEAMS_ALLOWED_USERS={{allowedIds.teams.csv}}","TEAMS_PORT={{teamsConfig.webhookPort}}"]},{id:"teams-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.teams",value:{enabled:true}}}],runtime:{openclaw:{channelName:"msteams",visibility:{configKeys:["msteams"],logPatterns:["msteams","teams"]},nodePreloads:[{module:"msteams-message-hints",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Microsoft Teams message hint patch (native mentions)",installedMessage:"[channels] Microsoft Teams message hint patch installed (NODE_OPTIONS updated)"}]},hermes:{envAliases:[{envKey:"MSTEAMS_APP_PASSWORD",targetEnvKey:"TEAMS_CLIENT_SECRET",match:"^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_MSTEAMS_APP_PASSWORD$",value:"openshell:resolve:env:MSTEAMS_APP_PASSWORD"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/msteams@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.1":"sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong=="},tarballUrlByVersion:{"2026.9.1":"https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.1.tgz"},required:true},{id:"hermesTeamsAppsPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"microsoft-teams-apps==2.0.13.4",required:true}],hooks:[{id:"teams-host-forward-port-conflict",phase:"pre-enable",handler:"teams.hostForwardPortConflict",inputs:["webhookPort"],onFailure:"abort"},{id:"teams-host-forward-port-status",phase:"status",handler:"teams.hostForwardPortStatus",outputs:[{id:"hostForwardPortOverlaps",kind:"status"}]},{id:"teams-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"clientSecret",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"teams-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"appId",kind:"config",required:true},{id:"tenantId",kind:"config",required:true},{id:"allowedUsers",kind:"config"},{id:"webhookPort",kind:"config"},{id:"requireMention",kind:"config"}]}],state:{}};var telegramManifest={schemaVersion:1,id:"telegram",displayName:"Telegram",description:"Telegram bot messaging",diagnosticsProbe:"log-tail",enrollmentNotes:["For Telegram group chats, disable privacy mode in @BotFather (/setprivacy -> your bot -> Disable).","After changing privacy mode, remove and re-add the bot to each group before testing @mentions."],supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"TELEGRAM_BOT_TOKEN",prompt:{label:"Telegram Bot Token",help:"Create a bot via @BotFather on Telegram, then copy the token."}},{id:"allowedIds",kind:"config",required:false,envKey:"TELEGRAM_ALLOWED_IDS",statePath:"allowedIds.telegram",prompt:{label:"Telegram User ID (for DM access)",help:"Send /start to @userinfobot on Telegram to get your numeric user ID.",emptyValueMessage:"bot will require manual pairing"}},{id:"requireMention",kind:"config",required:false,envKey:"TELEGRAM_REQUIRE_MENTION",statePath:"telegramConfig.requireMention",validValues:["0","1"],defaultValue:"1",prompt:{label:"Telegram group mention mode",help:"Controls Telegram group-chat behavior only \u2014 reply only when @mentioned vs. to all group messages. Direct messages are unaffected by this setting and remain subject to pairing and TELEGRAM_ALLOWED_IDS."}},{id:"groupPolicy",kind:"config",required:false,envKey:"TELEGRAM_GROUP_POLICY",statePath:"telegramConfig.groupPolicy",validValues:["open","allowlist","disabled"],defaultValue:"open",prompt:{label:"Telegram group policy",help:"Controls OpenClaw Telegram group access. Hermes does not expose an equivalent disable-groups policy."}}],credentials:[{id:"telegramBotToken",sourceInput:"botToken",providerName:"{sandboxName}-telegram-bridge",providerEnvKey:"TELEGRAM_BOT_TOKEN",placeholder:"openshell:resolve:env:TELEGRAM_BOT_TOKEN"}],policyPresets:[{name:"telegram",requiredAtCreate:true,policyKeys:["telegram_bot"],agentPolicyKeys:{hermes:["telegram"]}}],render:[{id:"telegram-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.telegram",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},proxy:"{{proxyUrl}}",groupPolicy:"{{telegramConfig.groupPolicy}}",dmPolicy:"{{allowedIds.telegram.dmPolicy}}",allowFrom:"{{allowedIds.telegram.values}}"}}}}},{id:"telegram-openclaw-groups",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",when:"{{telegramConfig.openclawGroups}}",fragment:{path:"channels.telegram.groups",value:"{{telegramConfig.openclawGroups}}"}},{id:"telegram-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.telegram",value:{enabled:true}}},{id:"telegram-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["TELEGRAM_ALLOWED_USERS={{allowedIds.telegram.csv}}"]},{id:"telegram-hermes-config",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"telegram",value:{require_mention:"{{telegramConfig.requireMention}}"}}},{id:"telegram-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.telegram",value:{enabled:true}}}],runtime:{openclaw:{channelName:"telegram",visibility:{configKeys:["telegram"],logPatterns:["telegram"]},nodePreloads:[{module:"telegram-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Telegram diagnostics (provider readiness + inference errors)",installedMessage:"[channels] Telegram diagnostics installed (NODE_OPTIONS updated)"}]}},hooks:[{id:"telegram-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"telegram-allowlist-aliases",phase:"enroll",handler:"telegram.allowlistAliases",outputs:[{id:"allowedIds",kind:"config"}]},{id:"telegram-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"requireMention",kind:"config"},{id:"allowedIds",kind:"config"}]},{id:"telegram-openclaw-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["openclaw"],outputs:[{id:"groupPolicy",kind:"config"}]},{id:"telegram-get-me-reachability",phase:"reachability-check",handler:"telegram.getMeReachability",inputs:["botToken"],onFailure:"skip-channel"},{id:"telegram-openclaw-bridge-health",phase:"health-check",handler:"telegram.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"telegram-gateway-conflict-status",phase:"status",handler:"telegram.gatewayConflictStatus",outputs:[{id:"bridgeHealth",kind:"status"}]},{id:"telegram-status-health",phase:"status",handler:"telegram.statusHealth",agents:["openclaw"],outputs:[{id:"channelHealth",kind:"status"}]}]};var WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT={channelId:"wechat",planHookId:"wechat-seed-openclaw-account",handlerId:"wechat.seedOpenClawAccount",outputId:"openclawWeixinAccountFile",kind:"build-file",required:true,mode:"0600"};var WECHAT_SEED_OPENCLAW_ACCOUNT_HOOK_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.handlerId;var WECHAT_SEED_OPENCLAW_ACCOUNT_PLAN_HOOK_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.planHookId;var WECHAT_OPENCLAW_ACCOUNT_FILE_OUTPUT_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.outputId;var WECHAT_TOKEN_PLACEHOLDER="openshell:resolve:env:WECHAT_BOT_TOKEN";function authorizeWechatAccountFilePlaceholders(value){const content=isPlainDataObject2(value)?ownDataPropertyValue2(value,"content"):void 0;if(!isPlainDataObject2(value)||!hasExactlyOwnDataProperties2(value,["content","mode","path"])||!isWechatAccountFilePath(ownDataPropertyValue2(value,"path"))||ownDataPropertyValue2(value,"mode")!==WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.mode||!isPlainDataObject2(content)||!hasOnlyOwnDataProperties(content,["baseUrl","savedAt","token","userId"])||!hasOwnDataProperty(content,"savedAt")||!hasOwnDataProperty(content,"token")||ownDataPropertyValue2(content,"token")!==WECHAT_TOKEN_PLACEHOLDER||!isNonEmptyString(ownDataPropertyValue2(content,"savedAt"))||!isOptionalNonEmptyString(content,"baseUrl")||!isOptionalNonEmptyString(content,"userId")){return[]}return[{path:["content","token"],value:WECHAT_TOKEN_PLACEHOLDER}]}function isWechatAccountFilePath(value){if(typeof value!=="string")return false;const prefix="openclaw-weixin/accounts/";const suffix=".json";if(!value.startsWith(prefix)||!value.endsWith(suffix))return false;const accountId=value.slice(prefix.length,-suffix.length);return accountId===accountId.trim()&&isSafeWechatAccountId(accountId)}function isSafeWechatAccountId(accountId){return accountId.length>0&&accountId!=="."&&accountId!==".."&&!/[\\/\0-\x1F\x7F]/.test(accountId)&&!accountId.includes("..")}function isPlainDataObject2(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue2(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function hasOwnDataProperty(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor!==void 0&&"value"in descriptor}function hasExactlyOwnDataProperties2(value,expected){const actual=Object.getOwnPropertyNames(value).sort();return actual.length===expected.length&&actual.every((key,index)=>key===expected[index])}function hasOnlyOwnDataProperties(value,allowed){return Object.getOwnPropertyNames(value).every(key=>allowed.includes(key))}function isNonEmptyString(value){return typeof value==="string"&&value.length>0}function isOptionalNonEmptyString(value,key){return!hasOwnDataProperty(value,key)||isNonEmptyString(ownDataPropertyValue2(value,key))}var wechatManifest={schemaVersion:1,id:"wechat",displayName:"WeChat",description:"WeChat (personal) bot messaging",enrollmentHelp:"Captured automatically via a host-side QR scan during onboard \u2014 pair the bot by scanning the QR with WeChat on your phone (Discover \u2192 Scan). DM-only.",supportedAgents:["openclaw","hermes"],auth:{mode:"host-qr"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"WECHAT_BOT_TOKEN",prompt:{label:"WeChat Bot Token",help:"Captured automatically via a host-side QR scan during onboard \u2014 pair the bot by scanning the QR with WeChat on your phone (Discover \u2192 Scan). DM-only."}},{id:"accountId",kind:"config",required:true,envKey:"WECHAT_ACCOUNT_ID",statePath:"wechatConfig.accountId"},{id:"baseUrl",kind:"config",required:false,envKey:"WECHAT_BASE_URL",statePath:"wechatConfig.baseUrl"},{id:"userId",kind:"config",required:false,envKey:"WECHAT_USER_ID",statePath:"wechatConfig.userId"},{id:"allowedIds",kind:"config",required:false,envKey:"WECHAT_ALLOWED_IDS",statePath:"allowedIds.wechat",prompt:{label:"WeChat User ID(s) (DM allowlist)",help:"Optional: restrict who can DM the bot. The WeChat user id of the operator who scanned is added automatically; supply additional ids as a comma-separated list.",emptyValueMessage:"bot will require manual pairing"}}],credentials:[{id:"wechatBotToken",sourceInput:"botToken",providerName:"{sandboxName}-wechat-bridge",providerEnvKey:"WECHAT_BOT_TOKEN",placeholder:"openshell:resolve:env:WECHAT_BOT_TOKEN"}],state:{openclaw:["wechat","openclaw-weixin"]},policyPresets:[{name:"wechat",policyKeys:["wechat_bridge"],requiredAtCreate:true}],render:[{id:"wechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.openclaw-weixin",value:{enabled:true}}},{id:"wechat-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.openclaw-weixin",value:{enabled:true}}},{id:"wechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WEIXIN_ACCOUNT_ID={{wechatConfig.accountId}}","WEIXIN_BASE_URL={{wechatConfig.baseUrl}}","WEIXIN_ALLOWED_USERS={{allowedIds.wechat.csv}}"]},{id:"wechat-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.weixin",value:{enabled:true}}}],runtime:{openclaw:{channelName:"openclaw-weixin",visibility:{configKeys:["openclaw-weixin"],logPatterns:["wechat","openclaw-weixin"]},nodePreloads:[{module:"wechat-account-placeholder",injectInto:["boot"],optional:false},{module:"wechat-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing WeChat diagnostics (provider readiness + inference errors)",installedMessage:"[channels] WeChat diagnostics installed (NODE_OPTIONS updated)"}]},hermes:{envAliases:[{envKey:"WECHAT_BOT_TOKEN",targetEnvKey:"WEIXIN_TOKEN",match:"^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_WECHAT_BOT_TOKEN$",value:"openshell:resolve:env:WECHAT_BOT_TOKEN"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@tencent-weixin/openclaw-weixin@2.4.9",pin:true,integrity:"sha512-SfaYehR1Cwq2VV5HxJBp9sVilMms420VfZlMbF4YjRbWomr5+GxfXp9HkeU6y5TbnOc4Ysq0qPw1yBvJwbenBA==",tarballUrl:"https://registry.npmjs.org/@tencent-weixin/openclaw-weixin/-/openclaw-weixin-2.4.9.tgz",runtimeLock:{cachePath:"/usr/local/share/nemoclaw/wechat-npm-cache",installCacheEnvKey:"NEMOCLAW_WECHAT_NPM_INSTALL_CACHE",lockFile:"/usr/local/lib/nemoclaw/wechat-runtime/package-lock.json",projectsRoot:"/sandbox/.openclaw/npm/projects",verifierPath:"/usr/local/lib/nemoclaw/verify-wechat-runtime-lock.mts",offline:true,legacyPeerDeps:true},required:true}],hooks:[{id:"wechat-host-qr",phase:"enroll",handler:"wechat.ilinkLogin",inputs:["allowedIds"],outputs:[{id:"botToken",kind:"secret",required:true},{id:"accountId",kind:"config",required:true},{id:"baseUrl",kind:"config"},{id:"userId",kind:"config"},{id:"allowedIds",kind:"config"}],onFailure:"skip-channel"},{id:"wechat-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowedIds",kind:"config"}]},{id:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.planHookId,phase:"post-agent-install",handler:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.handlerId,agents:["openclaw"],inputs:["wechatConfig.accountId","wechatConfig.baseUrl","wechatConfig.userId","credential.wechatBotToken.placeholder"],outputs:[{id:"openclawWeixinAccountsIndex",kind:"build-file",required:true},{id:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.outputId,kind:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.kind,required:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.required},{id:"openclawConfigPatch",kind:"build-file",required:true}],onFailure:"abort"},{id:"wechat-health-check",phase:"health-check",handler:"wechat.healthCheck",inputs:["wechatConfig.accountId"],onFailure:"abort"}]};var whatsappManifest={schemaVersion:1,id:"whatsapp",displayName:"WhatsApp",description:"WhatsApp Web messaging (QR pairing)",enrollmentHelp:"WhatsApp Web pairs via QR code scanned with your phone \u2014 no host-side token. After the sandbox is running, run `openshell term` and then use `openclaw channels login --channel whatsapp` for OpenClaw or `hermes whatsapp` for Hermes to display the QR.",enrollmentNotes:["After pairing, run `nemoclaw channels status --channel whatsapp`. OpenClaw reports inbound delivery evidence; Hermes reports gateway and dashboard session-path diagnostics."],supportedAgents:["openclaw","hermes"],auth:{mode:"in-sandbox-qr"},inputs:[{id:"mode",kind:"config",required:false,envKey:"WHATSAPP_MODE",statePath:"whatsappConfig.mode",validValues:["self-chat","bot"],defaultValue:"self-chat",prompt:{label:"WhatsApp reply mode",help:"self-chat replies only to messages the paired account sends to itself. bot replies to other senders and stops replying to that self-chat: an unknown sender receives a pairing code you approve with `hermes pairing approve whatsapp `, unless you set WHATSAPP_ALLOWED_IDS to a fixed sender list before this command.",emptyValueMessage:"the sandbox replies only in your own self-chat"}},{id:"allowedIds",kind:"config",required:false,envKey:"WHATSAPP_ALLOWED_IDS",statePath:"allowedIds.whatsapp"}],credentials:[],policyPresets:["whatsapp"],render:[{id:"whatsapp-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.whatsapp",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false}}}}}},{id:"whatsapp-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.whatsapp",value:{enabled:true}}},{id:"whatsapp-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WHATSAPP_ENABLED=true","WHATSAPP_MODE={{whatsappConfig.mode}}","WHATSAPP_DM_POLICY={{whatsappConfig.dmPolicy}}","WHATSAPP_ALLOWED_USERS={{allowedIds.whatsapp.csv}}"]},{id:"whatsapp-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.whatsapp",value:{enabled:true}}}],runtime:{openclaw:{channelName:"whatsapp",visibility:{configKeys:["whatsapp"],logPatterns:["whatsapp"]},nodePreloads:[{module:"whatsapp-qr-compact",injectInto:["connect"],optional:true,installMessage:"[channels] Installing WhatsApp compact-QR renderer (scan-friendly pairing)"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/whatsapp@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.1":"sha512-llIcoMa6FM4SgYn7GG1FQIeTTA5JDdcHW5D7PT+3aGYT3/E2eLFutKwDvD/w7G0hvDwSftzZgLi3iA8dzK7a3A=="},tarballUrlByVersion:{"2026.9.1":"https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.1.tgz"},required:true}],hooks:[{id:"whatsapp-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["hermes"],outputs:[{id:"mode",kind:"config"}]},{id:"whatsapp-status-health",phase:"status",handler:"whatsapp.statusHealth",agents:["openclaw","hermes"],outputs:[{id:"channelHealth",kind:"status"}]}]};var BUILT_IN_CHANNEL_MANIFESTS=[telegramManifest,discordManifest,wechatManifest,slackManifest,whatsappManifest,teamsManifest,googlechatManifest];function createBuiltInChannelManifestRegistry(){return createChannelManifestRegistry(BUILT_IN_CHANNEL_MANIFESTS)}var EXACT_TEMPLATE_PATTERN=/^\{\{\s*([^}]+?)\s*\}\}$/;var TEMPLATE_REFERENCE_PATTERN=/\{\{\s*([^}]+?)\s*\}\}/g;function resolvedRenderTemplateReference(value){return{matched:true,value}}function resolveSandboxNameTemplate(value,sandboxName){return value.replaceAll("{sandboxName}",sandboxName)}function resolveRenderTemplatesInValue(value,context){if(typeof value==="string")return resolveRenderTemplatesInString(value,context);if(Array.isArray(value)){if(value.length===0)return value;const resolved=value.map(entry=>resolveRenderTemplatesInValue(entry,context)).filter(entry=>entry!==void 0);return resolved.length>0?resolved:void 0}if(value&&typeof value==="object"){const sourceEntries=Object.entries(value);if(sourceEntries.length===0)return value;const entries=sourceEntries.map(([key,entry])=>[key,resolveRenderTemplatesInValue(entry,context)]).filter(entry=>entry[1]!==void 0);return entries.length>0?Object.fromEntries(entries):void 0}return value}function isTruthyRenderTemplate(value,context){if(!value)return true;const resolved=resolveRenderTemplatesInString(value,context);if(resolved===void 0||resolved===null||resolved===false)return false;if(Array.isArray(resolved))return resolved.length>0;if(typeof resolved==="object")return Object.keys(resolved).length>0;if(typeof resolved==="string")return resolved.trim().length>0;return true}function resolveRenderTemplatesInString(value,context){const exact=value.match(EXACT_TEMPLATE_PATTERN);if(exact?.[1])return resolveTemplateReference(exact[1].trim(),context);let omitted=false;const resolved=value.replace(TEMPLATE_REFERENCE_PATTERN,(match,reference)=>{const replacement=resolveTemplateReference(reference.trim(),context);if(replacement===void 0||replacement===null){omitted=true;return""}if(Array.isArray(replacement))return replacement.map(String).join(",");if(typeof replacement==="object")return JSON.stringify(replacement);return String(replacement)});return omitted?void 0:resolved}function resolveTemplateReference(reference,context){const resolved=context.referenceResolver?.(reference,context);return resolved?.matched?resolved.value:"{{"+reference+"}}"}function allowedIds(context,channel){return parseList(stateValue(context,`allowedIds.${channel}`))}function stateValue(context,path4){const stateInput=context.inputs.find(input=>input.statePath===path4);if(stateInput?.value!==void 0)return stateInput.value;const inputId=path4.split(".").at(-1);return context.inputs.find(input=>input.inputId===inputId)?.value}function parseList(value){if(Array.isArray(value))return unique(value.map(String).map(cleanString).filter(Boolean));const text=cleanString(value);if(!text)return[];return unique(text.split(",").map(cleanString).filter(Boolean))}function parseBoolean(value){if(typeof value==="boolean")return value;const text=cleanString(value)?.toLowerCase();if(text==="1"||text==="true"||text==="yes"||text==="on")return true;if(text==="0"||text==="false"||text==="no"||text==="off")return false;return void 0}function nonEmptyString(value){return cleanString(value)||void 0}function cleanString(value){const text=String(value??"");if(/[\r\n]/.test(text)){throw new Error("Messaging template values must not contain line breaks.")}return text.trim()}function nonEmptyArray(values){return values.length>0?[...values]:void 0}function nonEmptyCsv(values){return values.length>0?values.join(","):void 0}function nonEmptyObject(value){return Object.keys(value).length>0?value:void 0}function unique(values){return[...new Set(values)]}var resolveDiscordTemplateReference=(reference,context)=>{if(reference==="discordProxyUrl")return resolvedRenderTemplateReference(void 0);switch(reference){case"discord.guilds":return resolvedRenderTemplateReference(nonEmptyObject(discordGuilds(context)));case"discord.hasGuilds":return resolvedRenderTemplateReference(Object.keys(discordGuilds(context)).length>0);case"discord.guildIds.csv":return resolvedRenderTemplateReference(nonEmptyCsv(Object.keys(discordGuilds(context))));case"discord.allowedUsers.values":return resolvedRenderTemplateReference(nonEmptyArray(discordAllowedUsers(context)));case"discord.allowedUsers.csv":return resolvedRenderTemplateReference(nonEmptyCsv(discordAllowedUsers(context)));case"discord.allowedUsers.dmPolicy":return resolvedRenderTemplateReference(discordAllowedUsers(context).length>0?"allowlist":void 0);case"discord.allowAllUsers":return resolvedRenderTemplateReference(Object.keys(discordGuilds(context)).length>0&&discordAllowedUsers(context).length===0?true:void 0);case"discord.requireMention":return resolvedRenderTemplateReference(discordRequireMention(context));default:return void 0}};function discordGuilds(context){const serverIds=parseList(stateValue(context,"discordGuilds.serverId"));if(serverIds.length===0)return{};const users=parseList(stateValue(context,"discordGuilds.userIds"));const requireMention=parseBoolean(stateValue(context,"discordGuilds.requireMention"))??true;return Object.fromEntries(serverIds.map(serverId=>[serverId,{requireMention,...users.length>0?{users}:{}}]))}function discordAllowedUsers(context){const users=new Set(allowedIds(context,"discord"));for(const guild of Object.values(discordGuilds(context))){for(const user of guild.users??[])users.add(String(user))}return[...users]}function discordRequireMention(context){for(const guild of Object.values(discordGuilds(context))){if(typeof guild.requireMention==="boolean")return guild.requireMention}return true}var DEFAULT_AUDIENCE_TYPE="app-url";var APP_PRINCIPAL_DISCOVERY_SENTINEL="000000000000000000000";var resolveGooglechatTemplateReference=(reference,context)=>{switch(reference){case"googlechatConfig.audienceType":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.audienceType"))??DEFAULT_AUDIENCE_TYPE);case"googlechatConfig.audience":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.audience")));case"googlechatConfig.appPrincipal":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.appPrincipal"))??APP_PRINCIPAL_DISCOVERY_SENTINEL);case"googlechatConfig.projectId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.projectId")));case"googlechatConfig.subscriptionName":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.subscriptionName")));default:break}const allowReference=reference.match(/^allowedIds[.]googlechat[.](values|dmPolicy|csv)$/);if(!allowReference?.[1])return void 0;const ids=allowedIds(context,"googlechat");switch(allowReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);case"csv":return resolvedRenderTemplateReference(ids.length>0?ids.join(","):void 0);default:return void 0}};var resolveSlackTemplateReference=(reference,context)=>{if(reference==="slackConfig.allowedChannels.csv"){return resolvedRenderTemplateReference(nonEmptyCsv(slackAllowedChannels(context)))}const allowedIdsReference=reference.match(/^allowedIds[.]slack[.](values|csv|dmPolicy|groupPolicy|channels)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"slack");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);case"groupPolicy":return resolvedRenderTemplateReference(ids.length>0||slackAllowedChannels(context).length>0?"allowlist":void 0);case"channels":return resolvedRenderTemplateReference(slackChannelConfig(context,ids));default:return void 0}};function slackChannelConfig(context,users){const allowedChannels=slackAllowedChannels(context);const entry={enabled:true,requireMention:true,...users.length>0?{users:[...users]}:{}};if(allowedChannels.length>0){return Object.fromEntries(allowedChannels.map(channelId=>[channelId,{...entry}]))}return users.length>0?{"*":entry}:void 0}function slackAllowedChannels(context){return parseList(stateValue(context,"slackConfig.allowedChannels"))}var DEFAULT_TEAMS_WEBHOOK_PORT=3978;var resolveTeamsTemplateReference=(reference,context)=>{switch(reference){case"teamsConfig.appId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"teamsConfig.appId")));case"teamsConfig.tenantId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"teamsConfig.tenantId")));case"teamsConfig.webhookPort":return resolvedRenderTemplateReference(teamsWebhookPort(context));case"teamsConfig.requireMention":return resolvedRenderTemplateReference(parseBoolean(stateValue(context,"teamsConfig.requireMention")));default:break}const allowedIdsReference=reference.match(/^allowedIds[.]teams[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"teams");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function teamsWebhookPort(context){const raw=nonEmptyString(stateValue(context,"teamsConfig.webhookPort"));if(!raw)return DEFAULT_TEAMS_WEBHOOK_PORT;const port=Number(raw);if(!Number.isInteger(port)||port<1||port>65535){throw new Error("Microsoft Teams webhook port must be an integer TCP port between 1 and 65535.")}return port}var DEFAULT_PROXY_HOST="10.200.0.1";var DEFAULT_PROXY_PORT="3128";var DEFAULT_TELEGRAM_GROUP_POLICY="open";var TELEGRAM_GROUP_POLICIES=new Set(["open","allowlist","disabled"]);var resolveTelegramTemplateReference=(reference,context)=>{if(reference==="proxyUrl")return resolvedRenderTemplateReference(proxyUrl(context.env));if(reference==="telegramConfig.groupPolicy"){return resolvedRenderTemplateReference(telegramGroupPolicy(context))}if(reference==="telegramConfig.openclawGroups"){return resolvedRenderTemplateReference(telegramOpenClawGroups(context))}if(reference==="telegramConfig.requireMention"){return resolvedRenderTemplateReference(parseBoolean(stateValue(context,"telegramConfig.requireMention")))}const allowedIdsReference=reference.match(/^allowedIds[.]telegram[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"telegram");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function proxyUrl(env){const host=nonEmptyString(env?.NEMOCLAW_PROXY_HOST)??DEFAULT_PROXY_HOST;const port=nonEmptyString(env?.NEMOCLAW_PROXY_PORT)??DEFAULT_PROXY_PORT;return`http://${host}:${port}`}function telegramGroupPolicy(context){const value=nonEmptyString(stateValue(context,"telegramConfig.groupPolicy"));return value&&TELEGRAM_GROUP_POLICIES.has(value)?value:DEFAULT_TELEGRAM_GROUP_POLICY}function telegramOpenClawGroups(context){if(telegramGroupPolicy(context)!=="open")return void 0;const requireMention=parseBoolean(stateValue(context,"telegramConfig.requireMention"));return requireMention===true?{"*":{requireMention:true}}:void 0}var WECHAT_ILINK_HOSTS=new Set(["ilinkai.weixin.qq.com","ilinkai.wechat.com"]);var WECHAT_ILINK_IDC_HOST_PATTERN=/^idc-[0-9]+[.]weixin[.]qq[.]com$/u;function normalizeWechatIlinkBaseUrl(value){const raw=String(value??"");if(/[\r\n]/.test(raw)){throw new Error("WeChat baseUrl must not contain line breaks.")}const text=raw.trim();if(!text)return void 0;let url;try{url=new URL(text)}catch{throw new Error("WeChat baseUrl must be a valid URL.")}if(url.protocol!=="https:"){throw new Error("WeChat baseUrl must use HTTPS.")}if(url.username||url.password){throw new Error("WeChat baseUrl must not include credentials.")}const authority=text.match(/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/iu)?.[1]??"";if(authority.includes(":")){throw new Error("WeChat baseUrl must not include an explicit port.")}if(!isWechatIlinkHost(url.hostname)){throw new Error("WeChat baseUrl must use an expected iLink host.")}if(url.pathname&&url.pathname!=="/"||url.search||url.hash){throw new Error("WeChat baseUrl must be an iLink origin URL.")}return url.origin}function isWechatIlinkHost(hostname){const normalized=hostname.toLowerCase();return WECHAT_ILINK_HOSTS.has(normalized)||isWechatIlinkIdcHost(normalized)}function isWechatIlinkIdcHost(hostname){return WECHAT_ILINK_IDC_HOST_PATTERN.test(hostname.toLowerCase())}var resolveWechatTemplateReference=(reference,context)=>{const wechatConfig=reference.match(/^wechatConfig[.](accountId|baseUrl|userId)$/);if(wechatConfig?.[1]){if(wechatConfig[1]==="baseUrl"){return resolvedRenderTemplateReference(normalizeWechatIlinkBaseUrl(stateValue(context,"wechatConfig.baseUrl")))}return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"wechatConfig."+wechatConfig[1])))}const allowedIdsReference=reference.match(/^allowedIds[.]wechat[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=wechatAllowedIds(context);switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function wechatAllowedIds(context){const ids=allowedIds(context,"wechat");const userId=nonEmptyString(stateValue(context,"wechatConfig.userId"));return userId&&!ids.includes(userId)?[userId,...ids]:ids}var DEFAULT_WHATSAPP_MODE="self-chat";var BOT_WHATSAPP_MODE="bot";var WHATSAPP_MODES=new Set([DEFAULT_WHATSAPP_MODE,BOT_WHATSAPP_MODE]);var resolveWhatsappTemplateReference=(reference,context)=>{if(reference==="whatsappConfig.mode"){return resolvedRenderTemplateReference(whatsappMode(context))}if(reference==="whatsappConfig.dmPolicy"){return resolvedRenderTemplateReference(whatsappDmPolicy(context))}const allowedIdsReference=reference.match(/^allowedIds[.]whatsapp[.](values|csv)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"whatsapp");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));default:return void 0}};function whatsappMode(context){const value=nonEmptyString(stateValue(context,"whatsappConfig.mode"));return value&&WHATSAPP_MODES.has(value)?value:DEFAULT_WHATSAPP_MODE}function whatsappDmPolicy(context){if(whatsappMode(context)!==BOT_WHATSAPP_MODE)return void 0;return allowedIds(context,"whatsapp").length>0?"allowlist":"pairing"}var BUILT_IN_TEMPLATE_REFERENCE_RESOLVERS=[resolveTelegramTemplateReference,resolveDiscordTemplateReference,resolveWechatTemplateReference,resolveSlackTemplateReference,resolveWhatsappTemplateReference,resolveTeamsTemplateReference,resolveGooglechatTemplateReference];function createBuiltInRenderTemplateResolver(){return(reference,context)=>{for(const resolver of BUILT_IN_TEMPLATE_REFERENCE_RESOLVERS){const resolved=resolver(reference,context);if(resolved)return resolved}return void 0}}var import_node_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 generationMatch=suffix.match(/^(?:v[0-9]{1,20}|s[a-f0-9]{64})_(.+)$/);return generationMatch?.[1]===envKey}function hasFullPersistedCredentialBindingShape(binding){return typeof binding.channelId==="string"&&typeof binding.credentialId==="string"&&typeof binding.sourceInput==="string"&&typeof binding.providerName==="string"&&typeof binding.providerEnvKey==="string"&&typeof binding.placeholder==="string"&&typeof binding.credentialAvailable==="boolean"}function normalizeFullPersistedCredentialBindings(bindings){return bindings.map(binding=>({channelId:binding.channelId,credentialId:binding.credentialId,sourceInput:binding.sourceInput,providerName:binding.providerName,providerEnvKey:binding.providerEnvKey,placeholder:normalizeProviderPlaceholderForEnvKey(binding.placeholder,binding.providerEnvKey)??binding.placeholder,credentialAvailable:binding.credentialAvailable===true,...typeof binding.credentialHash==="string"?{credentialHash:binding.credentialHash}:{}}))}function normalizePersistedAgentCredentialPlaceholders(render,credentialBindings){const credentialEnvKeys=new Set(credentialBindings.map(binding=>binding.providerEnvKey).filter(Boolean));if(credentialEnvKeys.size===0)return[...render];return render.map(entry=>{if(entry.kind!=="env-lines")return entry;return{...entry,lines:entry.lines.map(line=>normalizeCredentialEnvLine(line,credentialEnvKeys))}})}function normalizeCredentialEnvLine(line,credentialEnvKeys){const index=line.indexOf("=");if(index<=0)return line;const envKey=line.slice(0,index).trim();if(!credentialEnvKeys.has(envKey))return line;const value=line.slice(index+1);const normalized=normalizeProviderPlaceholderForEnvKey(value,envKey);return normalized?`${envKey}=${normalized}`:line}function normalizePersistedSandboxMessagingPlanShape(plan,environment=process.env){const manifestRegistry=createBuiltInChannelManifestRegistry();const disabledChannels=plan.disabledChannels.filter(channelId=>typeof channelId==="string");const disabledSet=new Set(disabledChannels);const channels=plan.channels.map(channel=>normalizePersistedChannel(channel,disabledSet,manifestRegistry.get(channel.channelId),environment));const credentialBindings=normalizePersistedCredentialBindings(plan,channels,manifestRegistry,environment);const normalizedPlan={...plan,channels,disabledChannels,credentialBindings,networkPolicy:plan.networkPolicy&&Array.isArray(plan.networkPolicy.entries)?plan.networkPolicy:{presets:[],entries:[]},agentRender:normalizePersistedAgentCredentialPlaceholders(Array.isArray(plan.agentRender)?[...plan.agentRender]:[],credentialBindings),buildSteps:Array.isArray(plan.buildSteps)?[...plan.buildSteps]:[],...plan.runtimeSetup!==void 0?{runtimeSetup:normalizeRuntimeSetup(plan.runtimeSetup)}:{},stateUpdates:Array.isArray(plan.stateUpdates)?[...plan.stateUpdates]:[],healthChecks:Array.isArray(plan.healthChecks)?[...plan.healthChecks]:[]};return normalizedPlan}function normalizePersistedChannel(channel,disabledSet,manifest,environment){const disabled=channel.disabled??disabledSet.has(channel.channelId);const configured=channel.configured??true;const hasFullShape=hasFullChannelShape(channel);const inputs=hasFullShape?normalizeFullInputs(channel.channelId,channel.inputs??[]):normalizePersistedInputs(channel,manifest);const active=channel.active??(configured&&!disabled&&requiredInputsAvailable(manifest,inputs));const hostForward=manifest?planHostForward(manifest,inputs,active&&!disabled,createBuiltInRenderTemplateResolver(),environment):void 0;return{channelId:channel.channelId,displayName:channel.displayName??manifest?.displayName??channel.channelId,authMode:channel.authMode??manifest?.auth.mode??"none",active,selected:channel.selected??configured,configured,disabled,...channel.pendingRemoval===true?{pendingRemoval:true}:{},inputs,...hostForward?{hostForward}:{},hooks:Array.isArray(channel.hooks)?[...channel.hooks]:[]}}function normalizePersistedInputs(channel,manifest){const persistedById=new Map((channel.inputs??[]).filter(input=>typeof input.inputId==="string").map(input=>[input.inputId,input]));const fromManifest=(manifest?.inputs??[]).map(input=>inputReferenceFromManifest(channel.channelId,input,persistedById.get(input.id)));const manifestInputIds=new Set((manifest?.inputs??[]).map(input=>input.id));const unknownInputs=[...persistedById.values()].flatMap(input=>{if(!input.inputId||manifestInputIds.has(input.inputId))return[];return[normalizeUnknownInput(channel.channelId,input)]});return[...fromManifest,...unknownInputs]}function normalizeFullInputs(channelId,inputs){return inputs.filter(input=>typeof input.inputId==="string").map(input=>({channelId:typeof input.channelId==="string"?input.channelId:channelId,inputId:input.inputId,kind:input.kind==="secret"||input.kind==="config"?input.kind:"config",required:typeof input.required==="boolean"?input.required:false,...typeof input.sourceEnv==="string"?{sourceEnv:input.sourceEnv}:{},...typeof input.statePath==="string"?{statePath:input.statePath}:{},...input.credentialAvailable!==void 0?{credentialAvailable:input.credentialAvailable}:{},...input.value!==void 0?{value:input.value}:{}}))}function inputReferenceFromManifest(channelId,input,persisted){return{channelId,inputId:input.id,kind:input.kind,required:input.required,...input.envKey?{sourceEnv:input.envKey}:{},...input.kind==="config"&&input.statePath?{statePath:input.statePath}:{},...persisted?.credentialAvailable!==void 0?{credentialAvailable:persisted.credentialAvailable}:{},...persisted?.value!==void 0?{value:persisted.value}:{}}}function normalizeUnknownInput(channelId,input){const kind=input.kind==="secret"||input.kind==="config"?input.kind:"config";return{channelId,inputId:input.inputId,kind,required:input.required===true,...typeof input.sourceEnv==="string"?{sourceEnv:input.sourceEnv}:{},...typeof input.statePath==="string"?{statePath:input.statePath}:{},...input.credentialAvailable!==void 0?{credentialAvailable:input.credentialAvailable}:{},...input.value!==void 0?{value:input.value}:{}}}function requiredInputsAvailable(manifest,inputs){if(!manifest)return true;return manifest.inputs.every(manifestInput=>{if(!manifestInput.required)return true;const input=inputs.find(entry=>entry.inputId===manifestInput.id);if(!input)return false;if(input.kind==="secret")return input.credentialAvailable===true;if(input.value===void 0)return false;return typeof input.value==="string"?input.value.trim().length>0:true})}function normalizePersistedCredentialBindings(plan,channels,manifestRegistry,environment){const persisted=plan.credentialBindings??[];if(Array.isArray(plan.credentialBindings)&&plan.channels.every(hasFullChannelShape)&&persisted.every(hasFullPersistedCredentialBindingShape)){return normalizeFullPersistedCredentialBindings(persisted)}const manifests=channels.flatMap(channel=>{const manifest=manifestRegistry.get(channel.channelId);return manifest?[manifest]:[]});const planForBindings={...plan,channels,credentialBindings:[],networkPolicy:{presets:[],entries:[]},agentRender:[],buildSteps:[],runtimeSetup:{nodePreloads:[],envAliases:[],secretScans:[]},stateUpdates:[],healthChecks:[]};const generated=credentialBindingsFromManifests(planForBindings,manifests,new Map(channels.map(channel=>[channel.channelId,channel.inputs])),environment);return generated.map(binding=>overlayPersistedCredentialBinding(binding,persisted))}function credentialBindingsFromManifests(plan,manifests,inputRegistry,environment){const context=compilerContext(plan);return manifests.flatMap(manifest=>planCredentialBindings(manifest,context,inputRegistry.get(manifest.id)??[],environment).map(binding=>overlayPersistedCredentialBinding(binding,plan.credentialBindings)))}function overlayPersistedCredentialBinding(binding,persisted){const match=persisted.find(candidate=>credentialBindingMatches(binding,candidate));if(!match)return binding;return{...binding,credentialAvailable:typeof match.credentialAvailable==="boolean"?match.credentialAvailable:binding.credentialAvailable,...typeof match.credentialHash==="string"&&match.credentialHash.length>0?{credentialHash:match.credentialHash}:binding.credentialHash?{credentialHash:binding.credentialHash}:{}}}function credentialBindingMatches(binding,candidate){if(candidate.channelId&&candidate.channelId!==binding.channelId)return false;if(candidate.providerEnvKey&&candidate.providerEnvKey===binding.providerEnvKey)return true;if(candidate.credentialId&&candidate.credentialId===binding.credentialId)return true;if(candidate.sourceInput&&candidate.sourceInput===binding.sourceInput)return true;return false}function hasFullChannelShape(channel){return typeof channel.displayName==="string"&&typeof channel.authMode==="string"&&typeof channel.active==="boolean"&&typeof channel.selected==="boolean"&&typeof channel.configured==="boolean"&&typeof channel.disabled==="boolean"&&Array.isArray(channel.inputs)}function normalizeRuntimeSetup(setup){return{nodePreloads:Array.isArray(setup?.nodePreloads)?[...setup.nodePreloads]:[],envAliases:Array.isArray(setup?.envAliases)?[...setup.envAliases]:[],secretScans:Array.isArray(setup?.secretScans)?[...setup.secretScans]:[]}}function compilerContext(plan){return{sandboxName:plan.sandboxName,agent:plan.agent,workflow:plan.workflow,isInteractive:false,configuredChannels:plan.channels.map(channel=>channel.channelId),disabledChannels:plan.disabledChannels,credentialAvailability:credentialAvailabilityFromPlan(plan)}}function credentialAvailabilityFromPlan(plan){const availability={};for(const channel of plan.channels){for(const input of channel.inputs){if(input.kind!=="secret"||input.credentialAvailable!==true)continue;availability[`${channel.channelId}.${input.inputId}`]=true;if(input.sourceEnv)availability[input.sourceEnv]=true}}for(const credential of plan.credentialBindings){if(!credential.credentialAvailable)continue;availability[credential.credentialId]=true;availability[`${credential.channelId}.${credential.credentialId}`]=true;availability[`${credential.channelId}.${credential.sourceInput}`]=true;availability[credential.providerEnvKey]=true}return availability}function normalizeMessagingChannelId(channelId){return channelId.trim().toLowerCase()}function enabledPlanChannels(plan){const disabled=new Set((plan.disabledChannels??[]).map(normalizeMessagingChannelId).filter(Boolean));return plan.channels.filter(channel=>{const channelId=normalizeMessagingChannelId(channel.channelId);return channelId.length>0&&channel.active&&!channel.disabled&&!disabled.has(channelId)})}function selectActiveMessagingChannelIds(plan){const seen=new Set;const channels=[];for(const item of enabledPlanChannels(plan)){const channel=normalizeMessagingChannelId(item.channelId);if(!channel||seen.has(channel))continue;seen.add(channel);channels.push(channel)}return channels}function selectEnabledMessagingAgentRender(plan){const active=new Set(selectActiveMessagingChannelIds(plan));return plan.agentRender.filter(render=>render.agent===plan.agent&&active.has(normalizeMessagingChannelId(render.channelId)))}function selectEnabledPostAgentInstallBuildFiles(plan){const active=new Set(selectActiveMessagingChannelIds(plan));const channels=enabledPlanChannels(plan);return plan.buildSteps.filter(step=>{const channelId=normalizeMessagingChannelId(step.channelId);if(!active.has(channelId)||step.kind!=="build-file")return false;if(!step.hookId)return true;const matchingChannels=channels.filter(channel=>normalizeMessagingChannelId(channel.channelId)===channelId);if(matchingChannels.length!==1)return false;const matchedHook=matchingChannels[0]?.hooks?.find(hook=>hook.id===step.hookId);return matchedHook!==void 0&&matchedHook.phase==="post-agent-install"})}function parseSandboxMessagingPlan(value,options={}){if(!isObjectRecord(value)||value.schemaVersion!==1||typeof value.sandboxName!=="string"||typeof value.agent!=="string"||typeof value.workflow!=="string"||!Array.isArray(value.channels)||!Array.isArray(value.disabledChannels)||!isOptionalObjectArray(value,"credentialBindings")||Object.hasOwn(value,"networkPolicy")&&!isObjectRecord(value.networkPolicy)||!isOptionalObjectArray(value,"agentRender")||!isOptionalObjectArray(value,"buildSteps")||!isRuntimeSetup(value.runtimeSetup)||!isOptionalObjectArray(value,"stateUpdates")||!isOptionalObjectArray(value,"healthChecks")){return null}if(options.sandboxName&&value.sandboxName!==options.sandboxName)return null;if(options.agent&&value.agent!==options.agent)return null;const supported=Array.isArray(options.supportedChannelIds)?new Set(options.supportedChannelIds):null;const normalizedChannelIds=new Set;for(const channel of value.channels){if(!isObjectRecord(channel)||typeof channel.channelId!=="string")return null;const normalizedChannelId=normalizeMessagingChannelId(channel.channelId);if(!normalizedChannelId||normalizedChannelId!==channel.channelId||normalizedChannelIds.has(normalizedChannelId)){return null}if(Object.hasOwn(channel,"configured")&&typeof channel.configured!=="boolean"){return null}if(Object.hasOwn(channel,"active")&&typeof channel.active!=="boolean")return null;if(Object.hasOwn(channel,"disabled")&&typeof channel.disabled!=="boolean")return null;if(Object.hasOwn(channel,"pendingRemoval")&&typeof channel.pendingRemoval!=="boolean"){return null}if(Object.hasOwn(channel,"inputs")&&!Array.isArray(channel.inputs))return null;if(Object.hasOwn(channel,"hostForward")&&!isHostForward(channel.hostForward))return null;if(Object.hasOwn(channel,"hooks")&&!Array.isArray(channel.hooks))return null;if(Array.isArray(channel.inputs)&&channel.inputs.some(input=>!isObjectRecord(input)||typeof input.inputId!=="string"||Object.hasOwn(input,"channelId")&&input.channelId!==normalizedChannelId)){return null}if(Array.isArray(channel.hooks)&&channel.hooks.some(hook=>!isObjectRecord(hook)||Object.hasOwn(hook,"channelId")&&hook.channelId!==normalizedChannelId)){return null}if(Object.hasOwn(channel,"hostForward")&&isObjectRecord(channel.hostForward)&&channel.hostForward.channelId!==normalizedChannelId){return null}if(supported&&!supported.has(channel.channelId))return null;normalizedChannelIds.add(normalizedChannelId)}if(!value.disabledChannels.every(isCanonicalMessagingChannelId))return null;const disabledChannelIds=new Set(value.disabledChannels);if(disabledChannelIds.size!==value.disabledChannels.length||[...disabledChannelIds].some(channelId=>!normalizedChannelIds.has(channelId))||value.channels.some(channel=>isObjectRecord(channel)&&channel.disabled===true!==disabledChannelIds.has(String(channel.channelId)))){return null}if(!hasCanonicalChannelReferences(value.credentialBindings)||!hasMatchingAgentRenderEntries(value.agentRender,value.agent)||!hasCanonicalChannelReferences(value.agentRender)||!hasCanonicalChannelReferences(value.buildSteps)||!hasCanonicalChannelReferences(value.stateUpdates)||!hasCanonicalChannelReferences(value.healthChecks)||!hasCanonicalNetworkPolicyReferences(value.networkPolicy)||!hasCanonicalRuntimeSetupReferences(value.runtimeSetup)){return null}return cloneSandboxMessagingPlan(normalizePersistedSandboxMessagingPlanShape(value,options.environment))}function hasMatchingAgentRenderEntries(value,agent){return!Array.isArray(value)||value.every(render=>isObjectRecord(render)&&render.agent===agent)}function hasCanonicalNetworkPolicyReferences(value){if(!isObjectRecord(value)||!Object.hasOwn(value,"entries"))return true;return hasCanonicalChannelReferences(value.entries)}function cloneSandboxMessagingPlan(plan){return JSON.parse(JSON.stringify(plan))}function isOptionalObjectArray(value,key){if(!Object.hasOwn(value,key))return true;const entries=value[key];return Array.isArray(entries)&&entries.every(isObjectRecord)}function isHostForward(value){return isObjectRecord(value)&&typeof value.channelId==="string"&&typeof value.port==="number"&&Number.isInteger(value.port)&&value.port>=1&&value.port<=65535&&typeof value.label==="string"}function isRuntimeSetup(value){if(value===void 0)return true;return isObjectRecord(value)&&Array.isArray(value.nodePreloads)&&Array.isArray(value.envAliases)&&Array.isArray(value.secretScans)&&value.nodePreloads.every(isObjectRecord)&&value.envAliases.every(isObjectRecord)&&value.secretScans.every(isObjectRecord)}function isCanonicalMessagingChannelId(value){return typeof value==="string"&&value.length>0&&normalizeMessagingChannelId(value)===value}function hasCanonicalChannelReferences(value){return value===void 0||Array.isArray(value)&&value.every(entry=>isObjectRecord(entry)&&isCanonicalMessagingChannelId(entry.channelId))}function hasCanonicalRuntimeSetupReferences(value){if(value===void 0)return true;if(!isObjectRecord(value))return false;return["nodePreloads","envAliases","secretScans"].every(field=>hasCanonicalChannelReferences(value[field]))}var import_node_buffer=require("node:buffer");var import_node_crypto2=require("node:crypto");var import_node_util=require("node:util");function isLoopbackHostname(hostname=""){const normalized=String(hostname||"").trim().toLowerCase().replace(/^\[|\]$/g,"");return normalized==="localhost"||normalized==="::1"||/^127(?:\.\d{1,3}){3}$/.test(normalized)}function isLoopbackDashboardUrl(value){return isLoopbackHostname(new URL(value).hostname)}function listMessagingCredentialEnvAssignments(options={}){return selectManifests(options).flatMap(manifest=>{const credentialsByTemplate=new Map(manifest.credentials.map(credential=>[`{{credential.${credential.id}.placeholder}}`,credential]));const renderedAssignments=manifest.render.flatMap(render=>{if(options.agent&&render.agent!==options.agent)return[];if(render.kind!=="env-lines")return[];return render.lines.flatMap(line=>{const separator=line.indexOf("=");if(separator<=0)return[];const credential=credentialsByTemplate.get(line.slice(separator+1));if(!credential)return[];return[{channelId:manifest.id,agent:render.agent,sourceEnvKey:credential.providerEnvKey,targetEnvKey:line.slice(0,separator),placeholder:credential.placeholder}]})});const runtimeAssignments=["openclaw","hermes"].flatMap(agent=>{if(options.agent&&agent!==options.agent)return[];if(!manifest.supportedAgents.includes(agent))return[];return(manifest.runtime?.[agent]?.envAliases??[]).flatMap(alias=>{if(!alias.targetEnvKey)return[];const credential=manifest.credentials.find(candidate=>candidate.providerEnvKey===alias.envKey);if(!credential)return[];return[{channelId:manifest.id,agent,sourceEnvKey:alias.envKey,targetEnvKey:alias.targetEnvKey,placeholder:credential.placeholder}]})});return[...renderedAssignments,...runtimeAssignments]})}function selectManifests(options){const manifests=options.manifests??BUILT_IN_CHANNEL_MANIFESTS;const agent=options.agent;const selected=agent?manifests.filter(manifest=>manifest.supportedAgents.includes(agent)):manifests;return[...selected]}function authorizeMessagingManagedStartupFields(entry,section){if(section==="agentRender")return authorizeTeamsOpenClawWebhookField(entry);if(!isPlainDataObject3(entry))return[];const contract=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT;if(ownDataPropertyValue3(entry,"channelId")!==contract.channelId||ownDataPropertyValue3(entry,"hookId")!==contract.planHookId||ownDataPropertyValue3(entry,"handler")!==contract.handlerId||ownDataPropertyValue3(entry,"outputId")!==contract.outputId||ownDataPropertyValue3(entry,"kind")!==contract.kind||ownDataPropertyValue3(entry,"required")!==contract.required){return[]}return authorizeWechatAccountFilePlaceholders(ownDataPropertyValue3(entry,"value")).map(authorization=>({...authorization,path:["value",...authorization.path]}))}function isPlainDataObject3(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue3(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}var DCODE_UPSTREAM_PROVIDER_RE=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;function isValidDcodeUpstreamProvider(value){return DCODE_UPSTREAM_PROVIDER_RE.test(value)}var LEGACY_MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=1;var MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=2;var MANAGED_STARTUP_PROFILE_MAX_BYTES=64*1024;var MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES=Math.ceil(MANAGED_STARTUP_PROFILE_MAX_BYTES/3)*4;var MAX_IDENTIFIER_BYTES=256;var MAX_MODEL_BYTES=1024;var MAX_URL_BYTES=2048;var MAX_LIST_ITEMS=128;var MAX_JSON_NODES=4096;var MAX_JSON_DEPTH=32;var MAX_TUNING_INTEGER=1e9;var MIN_HERMES_CONTEXT_WINDOW=64e3;var SHA256_RE=/^[a-f0-9]{64}$/;var CONTROL_CHARACTER_RE=/[\u0000-\u001f\u007f-\u009f]/u;var BASE64URL_RE=/^[A-Za-z0-9_-]+$/;var RAW_CA_PEM_RE=/-----BEGIN (?:TRUSTED )?CERTIFICATE-----/iu;var RAW_CA_PEM_BASE64_RE=/^LS0tLS1CRUdJTi(?:BDRVJUSUZJQ0FURS0tLS0t|BUlVTVEVEIENFUlRJRklDQVRFLS0tLS0)/u;var RAW_CA_DER_BASE64_RE=/^MII[A-Za-z0-9+/=\r\n]{253,}$/u;var RAW_CA_DATA_URI_RE=/data:application\/(?:pkix-cert|x-x509-ca-cert);base64,MII[A-Za-z0-9+/=]{253,}/iu;var URL_CANDIDATE_RE=/[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'<>]+/gu;var UTF8_DECODER=new import_node_util.TextDecoder("utf-8",{fatal:true});var CREDENTIAL_SHAPED_NAME_PATTERN=/(?:^|[_-])(?:api[_-]?key|access[_-]?key|secret[_-]?key|auth[_-]?token|refresh[_-]?token|access[_-]?token|client[_-]?secret|private[_-]?key|pass[_-]?code|personal[_-]?access[_-]?token|connection[_-]?string|webhook(?:[_-]?url)?|key|secret|token|password|passwd|passcode|auth|authorization|credential|credentials|bearer|bearer[_-]?token|cookie|cookies|pat|private|privatekey|pin|webhookurl|dsn|connectionstring)(?:$|[_-])/iu;var CREDENTIAL_COMPOUND_NAME_PATTERN=/^(?:access|refresh|client|bearer|auth|api|private|signing|session|bot|app|resolved)(?:token|key|secret|password)$/iu;var CREDENTIAL_CAMEL_SUFFIX_PATTERN=/(?:apiKey|accessKey|secretKey|authToken|refreshToken|accessToken|clientSecret|privateKey|passcode|password|passwd|passphrase|bearerToken|botToken|appToken|sessionToken|signingKey|secretPublicKey|personalAccessToken|connectionString|webhookUrl)$/iu;var CREDENTIAL_CAMEL_BOUNDARY_PATTERN=/[a-z0-9](?:Token|Key|Secret|Password|Passphrase|Pat)$/u;var CREDENTIAL_ENV_NAME_PATTERN=/^(?:[A-Z0-9]+_)*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD|PASS|PASSPHRASE|CREDENTIAL)S?$/u;var CREDENTIAL_HEADER_NAME_PATTERN=/^(?:authorization|proxy-authorization|cookie|set-cookie|.+-(?:key|token|secret|password|passphrase|credential|auth)s?)$/iu;var PUBLIC_KEY_NAME_PATTERN=/^public[-_]?keys?$/iu;var PASS_CREDENTIAL_NAME_PATTERN=/(?:^|[-_])pass(?:wd)?$/iu;var NON_SECRET_KEY_METADATA_NAMES=new Set(["envKey","installCacheEnvKey","providerEnvKey","stateKey","targetEnvKey"]);var MESSAGING_CREDENTIAL_PLACEHOLDER_RE=/^(?:openshell:resolve:env:|[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-)(?:(?:v[0-9]{1,20}|s[a-f0-9]{64})_)?[A-Z][A-Z0-9_]*$/u;var MESSAGING_CREDENTIAL_ENV_ALIASES=new Set(listMessagingCredentialEnvAssignments().filter(({sourceEnvKey,targetEnvKey})=>sourceEnvKey!==targetEnvKey).map(({agent,sourceEnvKey,targetEnvKey})=>`${agent}\0${sourceEnvKey}\0${targetEnvKey}`));var MESSAGING_CREDENTIAL_RUNTIME_ENV_ALIASES=new Set(listMessagingCredentialEnvAssignments().filter(({sourceEnvKey,targetEnvKey})=>sourceEnvKey!==targetEnvKey).map(({agent,channelId,sourceEnvKey,targetEnvKey})=>`${agent}\0${channelId}\0${sourceEnvKey}\0${targetEnvKey}`));var JSON_ARRAY_INDEX_SEGMENT_RE=/^\[(?:0|[1-9][0-9]*)\]$/u;var SECRET_VALUE_PATTERNS=[/nvapi-[A-Za-z0-9_-]{10,}/u,/nvcf-[A-Za-z0-9_-]{10,}/u,/ghp_[A-Za-z0-9_-]{10,}/u,/github_pat_[A-Za-z0-9_]{30,}/u,/sk-(?:proj-|ant-)?[A-Za-z0-9_-]{10,}/u,/(?:xox[bpas]|xapp)-[A-Za-z0-9-]{10,}/u,/A(?:K|S)IA[A-Z0-9]{16}/u,/hf_[A-Za-z0-9]{10,}/u,/glpat-[A-Za-z0-9_-]{10,}/u,/gsk_[A-Za-z0-9]{10,}/u,/pypi-[A-Za-z0-9_-]{10,}/u,/tvly-[A-Za-z0-9_-]{10,}/u,/lsv2_(?:pt|sk)_[A-Za-z0-9]{10,}(?:_[A-Za-z0-9]+)*/u,/\bbot\d{8,10}:[A-Za-z0-9_-]{35}\b/u,/\b\d{8,10}:[A-Za-z0-9_-]{35}\b/u,/\b[A-Za-z0-9]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/u,/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{10,}\b/u,/\bBearer\s+[A-Za-z0-9_.+/=-]{10,}/iu,/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----/u];var MANAGED_STARTUP_INFERENCE_APIS=["openai-completions","openai-responses","anthropic-messages"];var MANAGED_STARTUP_REASONING_EFFORTS=["default","low","medium","high"];var MANAGED_STARTUP_DCODE_AUTO_APPROVAL_MODES=["disabled","thread-opt-in"];var MANAGED_STARTUP_HERMES_TOOL_GATEWAYS=["nous-web","nous-image","nous-audio","nous-browser","nous-code"];var MANAGED_STARTUP_AGENTS=["openclaw","hermes","langchain-deepagents-code","pi"];var MANAGED_STARTUP_MESSAGING_AGENTS=["openclaw","hermes"];function freezeAgentCapabilities(capabilities){return Object.freeze({...capabilities,inferenceApis:Object.freeze([...capabilities.inferenceApis]),dashboardModes:Object.freeze([...capabilities.dashboardModes]),inputModalities:Object.freeze([...capabilities.inputModalities]),webSearchProviders:Object.freeze([...capabilities.webSearchProviders]),toolGateways:Object.freeze([...capabilities.toolGateways]),tuningFields:Object.freeze([...capabilities.tuningFields])})}var PROFILE_CAPABILITIES={openclaw:{inferenceApis:[...MANAGED_STARTUP_INFERENCE_APIS],dashboardModes:["loopback","remote"],inputModalities:["text","image"],webSearchProviders:["brave","tavily"],toolGateways:[],tuningFields:["contextWindow","maxTokens","reasoning","reasoningEffort"],supportsMessaging:true,supportsInferenceCompatibility:true,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:true,supportsAgentTimeout:true,supportsHeartbeat:true,supportsExtraAgents:true,supportsDeviceAuth:false,observability:"openclaw-otel",supportsMinimalBootstrap:true},hermes:{inferenceApis:[...MANAGED_STARTUP_INFERENCE_APIS],dashboardModes:["disabled","loopback-forwarded"],inputModalities:[],webSearchProviders:["tavily"],toolGateways:[...MANAGED_STARTUP_HERMES_TOOL_GATEWAYS],tuningFields:["contextWindow"],supportsMessaging:true,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"none",supportsMinimalBootstrap:false},"langchain-deepagents-code":{inferenceApis:["openai-completions"],dashboardModes:["disabled"],inputModalities:[],webSearchProviders:[],toolGateways:[],tuningFields:["reasoningEffort"],supportsMessaging:false,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:true,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"dcode-marker",supportsMinimalBootstrap:false},pi:{inferenceApis:["openai-completions"],dashboardModes:["disabled"],inputModalities:[],webSearchProviders:[],toolGateways:[],tuningFields:["contextWindow","maxTokens","reasoning"],supportsMessaging:false,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"none",supportsMinimalBootstrap:false}};for(const agent of MANAGED_STARTUP_AGENTS){Object.defineProperty(PROFILE_CAPABILITIES,agent,{configurable:false,enumerable:true,value:freezeAgentCapabilities(PROFILE_CAPABILITIES[agent]),writable:false})}var MANAGED_STARTUP_PROFILE_CAPABILITIES=Object.freeze(PROFILE_CAPABILITIES);function affordance(input,profilePath,source="docker-arg",representation="value"){return{input,profilePath,source,representation}}var HOST_PROXY_AFFORDANCES=[affordance("HTTP_PROXY","proxy.hostHttpUrl","runtime-env"),affordance("http_proxy","proxy.hostHttpUrl","runtime-env","derived"),affordance("HTTPS_PROXY","proxy.hostHttpsUrl","runtime-env"),affordance("https_proxy","proxy.hostHttpsUrl","runtime-env","derived"),affordance("NO_PROXY","proxy.hostNoProxy","runtime-env"),affordance("no_proxy","proxy.hostNoProxy","runtime-env","derived")];var MANAGED_STARTUP_PROFILE_AFFORDANCE_INVENTORY={openclaw:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_SERVING_PRESET","inference.servingPreset"),affordance("NEMOCLAW_PRIMARY_MODEL_REF","inference.primaryModelRef"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_INFERENCE_COMPAT_B64","inference.compatibility"),affordance("NEMOCLAW_INFERENCE_INPUTS","inference.inputModalities"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_MAX_TOKENS","tuning.maxTokens"),affordance("NEMOCLAW_REASONING","tuning.reasoning"),affordance("NEMOCLAW_REASONING_EFFORT","tuning.reasoningEffort"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_AGENT_TIMEOUT","agentConfig.agentTimeoutSeconds"),affordance("NEMOCLAW_AGENT_HEARTBEAT_EVERY","agentConfig.heartbeatEvery"),affordance("NEMOCLAW_EXTRA_AGENTS_JSON_B64","agentConfig.extraAgents"),affordance("NEMOCLAW_WEB_SEARCH_ENABLED","agentConfig.webSearch.enabled"),affordance("NEMOCLAW_WEB_SEARCH_PROVIDER","agentConfig.webSearch.provider"),affordance("NEMOCLAW_OPENCLAW_OTEL","agentConfig.otel.enabled"),affordance("NEMOCLAW_OPENCLAW_OTEL_ENDPOINT","agentConfig.otel.endpointUrl"),affordance("NEMOCLAW_OPENCLAW_OTEL_SERVICE_NAME","agentConfig.otel.serviceName"),affordance("NEMOCLAW_OPENCLAW_OTEL_SAMPLE_RATE","agentConfig.otel.sampleRate"),affordance("CHAT_UI_URL","dashboard.url"),affordance("NEMOCLAW_DASHBOARD_BIND","dashboard.bindAddress"),affordance("NEMOCLAW_WSL_DASHBOARD_EXPOSURE","dashboard.wslExposure"),affordance("NEMOCLAW_DASHBOARD_PORT","dashboard.port","runtime-env"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_MESSAGING_PLAN_B64","messaging.plan"),affordance("NEMOCLAW_MINIMAL_BOOTSTRAP","agentConfig.minimalBootstrap","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],hermes:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER","tools.enabledGateways","docker-arg","derived"),affordance("NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64","tools.enabledGateways"),affordance("NEMOCLAW_WEB_SEARCH_ENABLED","agentConfig.webSearch.enabled"),affordance("NEMOCLAW_WEB_SEARCH_PROVIDER","agentConfig.webSearch.provider"),affordance("NEMOCLAW_MESSAGING_PLAN_B64","messaging.plan"),affordance("CHAT_UI_URL","dashboard.browserUrl"),affordance("NEMOCLAW_DASHBOARD_PORT","dashboard.publicPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD","dashboard.mode","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_PORT","dashboard.publicPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_INTERNAL_PORT","dashboard.internalPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_TUI","dashboard.tuiEnabled","runtime-env"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost","runtime-env"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],"langchain-deepagents-code":[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_UPSTREAM_ENDPOINT_URL","inference.upstreamEndpointUrl"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_REASONING_EFFORT","tuning.reasoningEffort"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_DCODE_AUTO_APPROVAL","agentConfig.autoApprovalMode"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_OBSERVABILITY","agentConfig.observabilityEnabled","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],pi:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_MAX_TOKENS","tuning.maxTokens"),affordance("NEMOCLAW_REASONING","tuning.reasoning"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES]};function deferredRuntimeInput(input,owner,reason,admission="managed-launch-forwarded"){return Object.freeze({input,owner,admission,reason})}var MANAGED_STARTUP_PROFILE_DEFERRED_RUNTIME_INPUTS=Object.freeze({openclaw:Object.freeze([deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_DEADLINE_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_MCP_SHADOW_DIAGNOSTICS","application-environment","operator shadow-diagnostics tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_MCP_TOOLS_LIST_TIMEOUT_MS","application-environment","operator MCP discovery timeout tuning is applied by the application environment transaction"),deferredRuntimeInput("OPENCLAW_HOME","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("OPENCLAW_STATE_DIR","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("OPENCLAW_WORKSPACE_DIR","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),hermes:Object.freeze([deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),"langchain-deepagents-code":Object.freeze([deferredRuntimeInput("NEMOCLAW_SANDBOX_NAME","engine-identity","the lifecycle engine owns instance identity outside reusable startup intent"),deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),pi:Object.freeze([deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")])});function runtimeCleanupObligation(input,emittedFor,supportedFor,reason){return Object.freeze({input,emittedFor:Object.freeze([...emittedFor]),supportedFor:Object.freeze([...supportedFor]),owner:"application-environment",reason})}var MANAGED_STARTUP_RUNTIME_CLEANUP_OBLIGATIONS=Object.freeze([runtimeCleanupObligation("NEMOCLAW_DASHBOARD_BIND",["hermes"],["openclaw"],"generic managed-dashboard construction currently emits the OpenClaw-only bind control for Hermes"),runtimeCleanupObligation("NEMOCLAW_MINIMAL_BOOTSTRAP",["hermes","langchain-deepagents-code"],["openclaw"],"generic host-proxy construction currently emits the OpenClaw-only bootstrap control for other agents")]);var ManagedStartupProfileError=class extends Error{constructor(message){super(`Invalid managed startup profile: ${message}`);this.name="ManagedStartupProfileError"}};var PROFILE_KEYS=new Set(["schemaVersion","agent","agentConfig","inference","proxy","dashboard","tools","messaging","tuning","corporateCa"]);var INFERENCE_KEYS=new Set(["routeProvider","upstreamProvider","servingPreset","model","routedBaseUrl","upstreamEndpointUrl","api","primaryModelRef","compatibility","inputModalities"]);var PROXY_KEYS=new Set(["managedHost","managedPort","hostHttpUrl","hostHttpsUrl","hostNoProxy"]);var OPENCLAW_DASHBOARD_KEYS=new Set(["agent","mode","url","port","bindAddress","wslExposure"]);var HERMES_DASHBOARD_KEYS=new Set(["agent","mode","url","browserUrl","publicPort","internalPort","tuiEnabled"]);var DCODE_DASHBOARD_KEYS=new Set(["agent","mode"]);var TOOLS_KEYS=new Set(["disclosure","enabledGateways"]);var MESSAGING_KEYS=new Set(["plan"]);var TUNING_FIELD_ORDER=["contextWindow","maxTokens","reasoning","reasoningEffort"];var TUNING_KEYS=new Set(TUNING_FIELD_ORDER);var CORPORATE_CA_KEYS=new Set(["bundleSha256"]);var OPENCLAW_CONFIG_KEYS=new Set(["agent","webSearch","otel","agentTimeoutSeconds","heartbeatEvery","extraAgents","minimalBootstrap"]);var LEGACY_OPENCLAW_CONFIG_KEYS=new Set([...OPENCLAW_CONFIG_KEYS,"deviceAuth"]);var LEGACY_DEVICE_AUTH_KEYS=new Set(["disabled","optOutSource"]);var LEGACY_DEVICE_AUTH_OPT_OUT_SOURCES=new Set(["operator","managed-onboard"]);var HERMES_CONFIG_KEYS=new Set(["agent","webSearch"]);var DCODE_CONFIG_KEYS=new Set(["agent","autoApprovalMode","observabilityEnabled"]);var PI_CONFIG_KEYS=new Set(["agent"]);var PI_DASHBOARD_KEYS=new Set(["agent","mode"]);var WEB_SEARCH_KEYS=new Set(["enabled","provider"]);var OTEL_KEYS=new Set(["enabled","endpointUrl","serviceName","sampleRate"]);var EXTRA_AGENTS_KEYS=new Set(["agents","defaults","main"]);var MANAGED_STARTUP_AGENT_SET=new Set(MANAGED_STARTUP_AGENTS);var DCODE_AUTO_APPROVAL_MODE_SET=new Set(MANAGED_STARTUP_DCODE_AUTO_APPROVAL_MODES);var REASONING_EFFORT_SET=new Set(MANAGED_STARTUP_REASONING_EFFORTS);var HERMES_INTERNAL_API_PORT=18642;var HERMES_API_PORT_RANGE_START=8642;var HERMES_API_PORT_RANGE_END=8652;function isHermesApiPort(port){return port>=HERMES_API_PORT_RANGE_START&&port<=HERMES_API_PORT_RANGE_END}function isHermesReservedApiPort(port){return port===HERMES_INTERNAL_API_PORT||isHermesApiPort(port)}var HERMES_RESERVED_API_PORT_LABEL=`${HERMES_API_PORT_RANGE_START}-${HERMES_API_PORT_RANGE_END} or ${HERMES_INTERNAL_API_PORT}`;function isPlainObject(value){if(typeof value!=="object"||value===null||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function isCredentialShapedName(name){if(PUBLIC_KEY_NAME_PATTERN.test(name)||NON_SECRET_KEY_METADATA_NAMES.has(name))return false;return CREDENTIAL_SHAPED_NAME_PATTERN.test(name)||CREDENTIAL_COMPOUND_NAME_PATTERN.test(name)||CREDENTIAL_CAMEL_SUFFIX_PATTERN.test(name)||CREDENTIAL_CAMEL_BOUNDARY_PATTERN.test(name)||CREDENTIAL_ENV_NAME_PATTERN.test(name)||CREDENTIAL_HEADER_NAME_PATTERN.test(name)||PASS_CREDENTIAL_NAME_PATTERN.test(name)}function valueLooksLikeSecret(value){for(let index=0;index=5&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="agentRender"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")&&path4[4]==="value";const isAuthorizedBuildStepPlaceholder=allowedBuildStepPlaceholders.has(buildStepPlaceholderKey(path4,value));return isCredentialBindingPlaceholder||isAgentRenderValuePlaceholder||isAuthorizedBuildStepPlaceholder}function requiresMessagingSchemaFieldAuthorization(path4){const fieldName=path4[path4.length-1];return fieldName==="webhook"}function messagingAuthorizedFieldKey(path4){return JSON.stringify(path4)}function buildStepPlaceholderKey(path4,value){return JSON.stringify([path4,value])}function messagingCredentialPlaceholderEnvKey(value){if(!MESSAGING_CREDENTIAL_PLACEHOLDER_RE.test(value))return null;const marker=value.startsWith("openshell:resolve:env:")?"openshell:resolve:env:":"-OPENSHELL-RESOLVE-ENV-";const key=value.slice(value.indexOf(marker)+marker.length);return key.replace(/^(?:v[0-9]{1,20}|s[a-f0-9]{64})_/u,"")}function containsMessagingCredentialPlaceholder(value){return value.includes("openshell:resolve:env:")||value.includes("-OPENSHELL-RESOLVE-ENV-")}function isMessagingCredentialPlaceholderAssignment(selectedAgent,path4,value){if(path4.length!==6||path4[0]!=="messaging"||path4[1]!=="plan"||path4[2]!=="agentRender"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")||path4[4]!=="lines"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[5]??"")){return false}const separator=value.indexOf("=");if(separator<=0||value.indexOf("=",separator+1)!==-1)return false;const envKey=value.slice(0,separator);const placeholder=value.slice(separator+1);const placeholderEnvKey=messagingCredentialPlaceholderEnvKey(placeholder);return CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&placeholderEnvKey!==null&&(envKey===placeholderEnvKey||typeof selectedAgent==="string"&&MESSAGING_CREDENTIAL_ENV_ALIASES.has(`${selectedAgent}\0${placeholderEnvKey}\0${envKey}`))}function isMessagingRuntimeEnvAliasPath(path4){return path4.length===5&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="runtimeSetup"&&path4[3]==="envAliases"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[4]??"")}function ownDataPropertyValue4(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function isCanonicalMessagingRuntimeEnvAlias(selectedAgent,path4,value){if(!isMessagingRuntimeEnvAliasPath(path4))return false;const channelId=ownDataPropertyValue4(value,"channelId");const envKey=ownDataPropertyValue4(value,"envKey");const targetEnvKey=ownDataPropertyValue4(value,"targetEnvKey");const match=ownDataPropertyValue4(value,"match");const placeholder=ownDataPropertyValue4(value,"value");const expectedMatch=targetEnvKey===void 0?`^openshell:resolve:env:((?:v[0-9]{1,20}|s[a-f0-9]{64})_)?${envKey}$`:`^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_${envKey}$`;return typeof envKey==="string"&&CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&match===expectedMatch&&typeof placeholder==="string"&&messagingCredentialPlaceholderEnvKey(placeholder)===envKey&&(targetEnvKey===void 0||typeof selectedAgent==="string"&&typeof channelId==="string"&&typeof targetEnvKey==="string"&&CREDENTIAL_ENV_NAME_PATTERN.test(targetEnvKey)&&MESSAGING_CREDENTIAL_RUNTIME_ENV_ALIASES.has(`${selectedAgent}\0${channelId}\0${envKey}\0${targetEnvKey}`))}function isAllowedMessagingRuntimeAliasStringPath(path4,allowedAliasIndexes){return path4.length===6&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="runtimeSetup"&&path4[3]==="envAliases"&&allowedAliasIndexes.has(path4[4]??"")&&(path4[5]==="match"||path4[5]==="value"||path4[5]==="targetEnvKey")}function isMessagingPackagePin(path4,value){return path4.length===6&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="buildSteps"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")&&path4[4]==="value"&&path4[5]==="pin"&&typeof value==="boolean"}function containsUrlWithCredentialMaterial(value){const candidates=value.match(URL_CANDIDATE_RE)??[];for(let index=0;index{if(isCredentialShapedName(key))credentialQuery=true});const fragment=url.hash.startsWith("#")?url.hash.slice(1):url.hash;const queryStart=fragment.indexOf("?");const fragmentParameters=new URLSearchParams(queryStart>=0?fragment.slice(queryStart+1):fragment);let credentialFragment=false;fragmentParameters.forEach((_fragmentValue,key)=>{if(isCredentialShapedName(key))credentialFragment=true});if(url.username||url.password||credentialQuery||credentialFragment)return true}catch{}}return false}function invalid(reason){throw new ManagedStartupProfileError(reason)}function payloadPath(path4){return path4.reduce((result,segment)=>segment.startsWith("[")?`${result}${segment}`:`${result}${result?".":""}${segment}`,"")}function mapArrayByIndex(values,mapper){const mapped=[];for(let index=0;index0&&values[insertion-1]>selected){Object.defineProperty(values,String(insertion),{configurable:true,enumerable:true,value:values[insertion-1],writable:true});insertion-=1}Object.defineProperty(values,String(insertion),{configurable:true,enumerable:true,value:selected,writable:true})}return values}function requireRecord(value,where){if(!isPlainObject(value))invalid(`${where} must be an object`);return value}function rejectUnknownKeys(value,allowed,where){const keys=Object.keys(value);for(let index=0;indexmaxBytes||CONTROL_CHARACTER_RE.test(value)){invalid(`${where} must be a bounded, non-empty string without control characters`)}return value}function requireStringEnum(value,allowed,where){const normalized=requireBoundedString(value,where);if(!allowed.has(normalized))invalid(`${where} is not supported`);return normalized}function requireNullablePositiveInteger(value,where){if(value===null)return null;if(typeof value!=="number"||!Number.isSafeInteger(value)||value<1||value>MAX_TUNING_INTEGER){invalid(`${where} must be null or a bounded positive integer`)}return value}function requirePositiveInteger(value,where,maximum=MAX_TUNING_INTEGER){if(typeof value!=="number"||!Number.isSafeInteger(value)||value<1||value>maximum){invalid(`${where} must be a bounded positive integer`)}return value}function requirePort(value,where,minimum=1){if(typeof value!=="number"||!Number.isInteger(value)||value<1||value>65535){invalid(`${where} must be a valid TCP port`)}if(valueMAX_LIST_ITEMS){invalid(`${where} must be a bounded string list`)}const items=mapArrayByIndex(value,item=>requireBoundedString(item,`${where} item`));const unique2=new Set;for(let index=0;index{if(depth>MAX_JSON_DEPTH)invalid(`${where} exceeds the JSON depth limit`);if(current===null||typeof current==="string"||typeof current==="boolean"){return current}if(typeof current==="number"){if(!Number.isFinite(current))invalid(`${where} contains a non-finite number`);return current}if(Array.isArray(current)){return mapArrayByIndex(current,item=>clone(item,depth+1))}if(!isPlainObject(current))invalid(`${where} contains a non-JSON value`);const result=options.nullPrototypeObjects?Object.create(null):{};const keys=Object.getOwnPropertyNames(current);for(let index=0;indexMAX_IDENTIFIER_BYTES||CONTROL_CHARACTER_RE.test(key)){invalid(`${where} contains an invalid object key`)}const descriptor=Object.getOwnPropertyDescriptor(current,key);if(!descriptor||!("value"in descriptor)){invalid(`${where} contains a non-JSON value`)}Object.defineProperty(result,key,{configurable:true,enumerable:true,value:clone(descriptor.value,depth+1),writable:true})}return result};return clone(value,0)}function requireJsonObjectOrNull(value,where){if(value===null)return null;if(!isPlainObject(value))invalid(`${where} must be null or a plain JSON object`);return cloneJsonValue(value,where,{nullPrototypeObjects:true})}function requireJsonObject(value,where){const object=requireJsonObjectOrNull(value,where);if(object===null)invalid(`${where} must be a plain JSON object`);return object}function requireHttpUrl(value,where){const raw=requireBoundedString(value,where,MAX_URL_BYTES);let parsed;try{parsed=new URL(raw)}catch{invalid(`${where} must be a valid HTTP(S) URL`)}if(parsed.protocol!=="http:"&&parsed.protocol!=="https:"||parsed.username||parsed.password||parsed.search||parsed.hash){invalid(`${where} must be a credential-free HTTP(S) URL without query or fragment data`)}const pathname=parsed.pathname.replace(/\/+$/u,"");return pathname===""?parsed.origin:`${parsed.origin}${pathname}`}function requireProxyUrl(value,allowedSchemes,where){if(value===null)return null;const raw=requireBoundedString(value,where,MAX_URL_BYTES);let parsed;try{parsed=new URL(raw)}catch{invalid(`${where} must be a valid HTTP(S) proxy URL`)}if(!allowedSchemes.has(parsed.protocol)||parsed.username||parsed.password||parsed.pathname!=="/"||parsed.search||parsed.hash){invalid(`${where} must be a credential-free HTTP(S) proxy origin`)}return parsed.origin}function requireManagedProxyHost(value,where){const host=requireBoundedString(value,where);if(!/^[A-Za-z0-9._-]+$/u.test(host)){invalid(`${where} must be a hostname or IPv4 address without a scheme or separators`)}return host}function configuredDashboardPort(value){const explicit=new URL(value).port;return explicit===""?18789:Number(explicit)}function requireSampleRate(value,where){if(typeof value!=="number"||!Number.isFinite(value)||value<0||value>1){invalid(`${where} must be a number between 0 and 1`)}return value}function assertPayloadStructureAndCredentialShapes(root){const pending=[{value:root,depth:0,path:[]}];const allowedRuntimeAliasIndexes=new Set;const allowedMessagingCredentialFields=new Set;const allowedBuildStepPlaceholders=new Set;const selectedAgent=isPlainObject(root)?ownDataPropertyValue4(root,"agent"):void 0;let discoveredNodes=1;let observedBytes=0;const observeText=value=>{observedBytes+=import_node_buffer.Buffer.byteLength(value,"utf8");if(observedBytes>MANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}};const reserveNode=depth=>{discoveredNodes+=1;if(discoveredNodes>MAX_JSON_NODES||depth>MAX_JSON_DEPTH){invalid("payload structure exceeds the complexity limit")}observedBytes+=1};while(pending.length>0){const current=pending.pop();if(!current)break;if(current.depth>MAX_JSON_DEPTH){invalid("payload structure exceeds the complexity limit")}if(typeof current.value==="string"){observeText(current.value);if(!isAllowedMessagingRuntimeAliasStringPath(current.path,allowedRuntimeAliasIndexes)&&!isMessagingCredentialPlaceholder(current.path,current.value,allowedBuildStepPlaceholders,allowedMessagingCredentialFields)&&!isMessagingCredentialPlaceholderAssignment(selectedAgent,current.path,current.value)&&(valueLooksLikeSecret(current.value)||containsMessagingCredentialPlaceholder(current.value))){invalid(`payload field ${payloadPath(current.path)} contains credential-shaped string data`)}if(RAW_CA_PEM_RE.test(current.value)||RAW_CA_PEM_BASE64_RE.test(current.value)||RAW_CA_DER_BASE64_RE.test(current.value)||RAW_CA_DATA_URI_RE.test(current.value)){invalid(`payload field ${payloadPath(current.path)} contains raw certificate data; provide only the CA SHA-256 digest`)}if(containsUrlWithCredentialMaterial(current.value)){invalid(`payload field ${payloadPath(current.path)} contains a URL with embedded credentials`)}continue}if(Array.isArray(current.value)){if(Object.getPrototypeOf(current.value)!==Array.prototype){invalid("payload arrays must use the standard JSON prototype")}if("toJSON"in current.value){invalid("payload must not define a custom JSON serializer")}if(Object.getOwnPropertySymbols(current.value).length>0||Object.getOwnPropertyNames(current.value).length!==current.value.length+1){invalid("payload arrays must contain only indexed JSON values")}for(let index=0;index0||discoveredNodes+keys.length>MAX_JSON_NODES){invalid("payload structure exceeds the complexity limit")}for(let index=0;indexMANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}}function validateWebSearch(value,agent){const webSearch=requireRecord(value,"agentConfig.webSearch");rejectUnknownKeys(webSearch,WEB_SEARCH_KEYS,"agentConfig.webSearch");const provider=requireStringEnum(webSearch.provider,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].webSearchProviders),"agentConfig.webSearch.provider");return{enabled:requireBoolean(webSearch.enabled,"agentConfig.webSearch.enabled"),provider}}function validateOpenClawOtel(value){const otel=requireRecord(value,"agentConfig.otel");rejectUnknownKeys(otel,OTEL_KEYS,"agentConfig.otel");return{enabled:requireBoolean(otel.enabled,"agentConfig.otel.enabled"),endpointUrl:requireHttpUrl(otel.endpointUrl,"agentConfig.otel.endpointUrl"),serviceName:requireBoundedString(otel.serviceName,"agentConfig.otel.serviceName",MAX_IDENTIFIER_BYTES),sampleRate:requireSampleRate(otel.sampleRate,"agentConfig.otel.sampleRate")}}function validateExtraAgents(value){const extraAgents=requireRecord(value,"agentConfig.extraAgents");rejectUnknownKeys(extraAgents,EXTRA_AGENTS_KEYS,"agentConfig.extraAgents");if(!Array.isArray(extraAgents.agents)||extraAgents.agents.length>MAX_LIST_ITEMS){invalid("agentConfig.extraAgents.agents must be a bounded JSON object list")}return{agents:mapArrayByIndex(extraAgents.agents,(agent,index)=>requireJsonObject(agent,`agentConfig.extraAgents.agents[${String(index)}]`)),defaults:requireJsonObject(extraAgents.defaults,"agentConfig.extraAgents.defaults"),main:requireJsonObject(extraAgents.main,"agentConfig.extraAgents.main")}}function validateAgentConfig(value,expectedAgent){const config=requireRecord(value,"agentConfig");const agent=requireStringEnum(config.agent,MANAGED_STARTUP_AGENT_SET,"agentConfig.agent");if(agent!==expectedAgent)invalid("agentConfig.agent must match agent");if(agent==="openclaw"){rejectUnknownKeys(config,OPENCLAW_CONFIG_KEYS,"agentConfig");const heartbeatEvery=config.heartbeatEvery===null?null:requireBoundedString(config.heartbeatEvery,"agentConfig.heartbeatEvery",MAX_IDENTIFIER_BYTES);if(heartbeatEvery!==null&&!/^\d+(?:s|m|h)$/u.test(heartbeatEvery)){invalid("agentConfig.heartbeatEvery must be null or a duration ending in s, m, or h")}return{agent,webSearch:validateWebSearch(config.webSearch,agent),otel:validateOpenClawOtel(config.otel),agentTimeoutSeconds:requirePositiveInteger(config.agentTimeoutSeconds,"agentConfig.agentTimeoutSeconds"),heartbeatEvery,extraAgents:validateExtraAgents(config.extraAgents),minimalBootstrap:requireBoolean(config.minimalBootstrap,"agentConfig.minimalBootstrap")}}if(agent==="hermes"){rejectUnknownKeys(config,HERMES_CONFIG_KEYS,"agentConfig");return{agent,webSearch:validateWebSearch(config.webSearch,agent)}}if(agent==="pi"){rejectUnknownKeys(config,PI_CONFIG_KEYS,"agentConfig");return{agent}}rejectUnknownKeys(config,DCODE_CONFIG_KEYS,"agentConfig");return{agent,autoApprovalMode:requireStringEnum(config.autoApprovalMode,DCODE_AUTO_APPROVAL_MODE_SET,"agentConfig.autoApprovalMode"),observabilityEnabled:requireBoolean(config.observabilityEnabled,"agentConfig.observabilityEnabled")}}function validateDashboard(value,expectedAgent){const dashboard=requireRecord(value,"dashboard");const agent=requireStringEnum(dashboard.agent,MANAGED_STARTUP_AGENT_SET,"dashboard.agent");if(agent!==expectedAgent)invalid("dashboard.agent must match agent");if(agent==="openclaw"){rejectUnknownKeys(dashboard,OPENCLAW_DASHBOARD_KEYS,"dashboard");const mode=requireStringEnum(dashboard.mode,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].dashboardModes),"dashboard.mode");const url=requireHttpUrl(dashboard.url,"dashboard.url");const bindAddress=requireStringEnum(dashboard.bindAddress,new Set(["127.0.0.1","0.0.0.0"]),"dashboard.bindAddress");const wslExposure=requireBoolean(dashboard.wslExposure,"dashboard.wslExposure");const hasRemoteExposure=!isLoopbackDashboardUrl(url)||bindAddress==="0.0.0.0"||wslExposure;if(mode==="remote"!==hasRemoteExposure){invalid("OpenClaw dashboard.mode must reflect its URL, bind address, and WSL exposure")}const port=requirePort(dashboard.port,"dashboard.port",1024);if(isHermesApiPort(port))invalid(`OpenClaw dashboard.port must not use a reserved Hermes API port (${HERMES_API_PORT_RANGE_START}-${HERMES_API_PORT_RANGE_END})`);if(configuredDashboardPort(url)!==port){invalid("OpenClaw dashboard.port must match dashboard.url")}return{agent,mode,url,port,bindAddress,wslExposure}}if(agent==="hermes"){rejectUnknownKeys(dashboard,HERMES_DASHBOARD_KEYS,"dashboard");const mode=requireStringEnum(dashboard.mode,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].dashboardModes),"dashboard.mode");const url=requireHttpUrl(dashboard.url,"dashboard.url");if(!isLoopbackDashboardUrl(url)){invalid("Hermes dashboard.url must remain loopback; OpenShell owns the host forward")}const browserUrl=dashboard.browserUrl===void 0?void 0:requireHttpUrl(dashboard.browserUrl,"dashboard.browserUrl");if(browserUrl!==void 0&&!isLoopbackDashboardUrl(browserUrl)&&new URL(browserUrl).protocol!=="https:"){invalid("Hermes dashboard.browserUrl must use HTTPS unless it is loopback")}if(mode==="disabled"){if(dashboard.publicPort!==null||dashboard.internalPort!==null||dashboard.tuiEnabled!==false){invalid("disabled Hermes dashboard must not configure ports or TUI")}return{agent,mode,url,...browserUrl===void 0?{}:{browserUrl},publicPort:null,internalPort:null,tuiEnabled:false}}const publicPort=requirePort(dashboard.publicPort,"dashboard.publicPort",1024);const internalPort=requirePort(dashboard.internalPort,"dashboard.internalPort",1024);if(publicPort===internalPort){invalid("Hermes dashboard publicPort and internalPort must differ")}if(isHermesReservedApiPort(publicPort)||isHermesReservedApiPort(internalPort)){invalid(`Hermes dashboard ports must not use reserved API ports ${HERMES_RESERVED_API_PORT_LABEL}`)}if(configuredDashboardPort(url)!==publicPort){invalid("Hermes dashboard.publicPort must match dashboard.url")}if(browserUrl!==void 0&&isLoopbackDashboardUrl(browserUrl)&&configuredDashboardPort(browserUrl)!==publicPort){invalid("Hermes dashboard.publicPort must match dashboard.browserUrl")}return{agent,mode,url,...browserUrl===void 0?{}:{browserUrl},publicPort,internalPort,tuiEnabled:requireBoolean(dashboard.tuiEnabled,"dashboard.tuiEnabled")}}if(agent==="pi"){rejectUnknownKeys(dashboard,PI_DASHBOARD_KEYS,"dashboard");if(dashboard.mode!=="disabled")invalid("pi dashboard.mode must be disabled");return{agent,mode:"disabled"}}rejectUnknownKeys(dashboard,DCODE_DASHBOARD_KEYS,"dashboard");if(dashboard.mode!=="disabled"){invalid("langchain-deepagents-code dashboard.mode must be disabled")}return{agent,mode:"disabled"}}function validateInference(value,agent){if(value===null){if(agent!=="openclaw"&&agent!=="hermes")invalid(`${agent} requires inference configuration`);return null}const inference=requireRecord(value,"inference");rejectUnknownKeys(inference,INFERENCE_KEYS,"inference");const routeProvider=requireBoundedString(inference.routeProvider,"inference.routeProvider");const upstreamProvider=requireBoundedString(inference.upstreamProvider,"inference.upstreamProvider");const servingPreset=inference.servingPreset===void 0?void 0:inference.servingPreset===null?null:requireBoundedString(inference.servingPreset,"inference.servingPreset");const model=requireBoundedString(inference.model,"inference.model",MAX_MODEL_BYTES);const api=requireStringEnum(inference.api,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].inferenceApis),"inference.api");const upstreamEndpointUrl=inference.upstreamEndpointUrl===null?null:requireHttpUrl(inference.upstreamEndpointUrl,"inference.upstreamEndpointUrl");const primaryModelRef=inference.primaryModelRef===null?null:requireBoundedString(inference.primaryModelRef,"inference.primaryModelRef",MAX_MODEL_BYTES);const compatibility=requireJsonObjectOrNull(inference.compatibility,"inference.compatibility");const inputModalities=inference.inputModalities===null?null:requireEnumList(inference.inputModalities,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].inputModalities),"inference.inputModalities",{allowEmpty:false});if(upstreamEndpointUrl!==null&&!MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].supportsUpstreamEndpoint){invalid(`inference.upstreamEndpointUrl must be null for ${agent}`)}if(agent==="openclaw"){if(primaryModelRef===null||inputModalities===null){invalid("openclaw requires primaryModelRef and inputModalities")}if(primaryModelRef!==`${routeProvider}/${model}`){invalid("openclaw primaryModelRef must match routeProvider and model")}}else{if(primaryModelRef!==null||compatibility!==null||inputModalities!==null){invalid(`${agent} does not support primaryModelRef, compatibility, or inputModalities`)}if(agent==="langchain-deepagents-code"&&!isValidDcodeUpstreamProvider(upstreamProvider)){invalid("inference.upstreamProvider must start with an ASCII letter or digit and contain 1-64 ASCII letters, digits, dots, underscores, or hyphens for DCode")}}return{routeProvider,upstreamProvider,...servingPreset===void 0?{}:{servingPreset},model,routedBaseUrl:requireHttpUrl(inference.routedBaseUrl,"inference.routedBaseUrl"),upstreamEndpointUrl,api,primaryModelRef,compatibility,inputModalities}}function validateProxy(value,agent){const proxy=requireRecord(value,"proxy");rejectUnknownKeys(proxy,PROXY_KEYS,"proxy");const hostHttpUrl=requireProxyUrl(proxy.hostHttpUrl,new Set(["http:"]),"proxy.hostHttpUrl");const hostHttpsUrl=requireProxyUrl(proxy.hostHttpsUrl,new Set(["http:","https:"]),"proxy.hostHttpsUrl");const hostNoProxy=requireStringList(proxy.hostNoProxy,"proxy.hostNoProxy");if(!MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].supportsHostProxyIntent&&(hostHttpUrl!==null||hostHttpsUrl!==null||hostNoProxy.length>0)){invalid(`${agent} rejects host proxy intent and accepts only its root-owned managed route`)}return{managedHost:requireManagedProxyHost(proxy.managedHost,"proxy.managedHost"),managedPort:requirePort(proxy.managedPort,"proxy.managedPort"),hostHttpUrl,hostHttpsUrl,hostNoProxy}}function validateTools(value,agent){const tools=requireRecord(value,"tools");rejectUnknownKeys(tools,TOOLS_KEYS,"tools");const enabledGateways=requireEnumList(tools.enabledGateways,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].toolGateways),"tools.enabledGateways",{allowEmpty:true});return{disclosure:requireStringEnum(tools.disclosure,new Set(["progressive","direct"]),"tools.disclosure"),enabledGateways}}function validateTuning(value,agent){const tuning=requireRecord(value,"tuning");rejectUnknownKeys(tuning,TUNING_KEYS,"tuning");const result={contextWindow:requireNullablePositiveInteger(tuning.contextWindow,"tuning.contextWindow"),maxTokens:requireNullablePositiveInteger(tuning.maxTokens,"tuning.maxTokens"),reasoning:requireNullableBoolean(tuning.reasoning,"tuning.reasoning"),reasoningEffort:tuning.reasoningEffort===null?null:requireStringEnum(tuning.reasoningEffort,REASONING_EFFORT_SET,"tuning.reasoningEffort")};const advertised=new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].tuningFields);const unsupported=TUNING_FIELD_ORDER.filter(field=>result[field]!==null&&!advertised.has(field));if(unsupported.length>0){invalid(`${agent} does not support startup tuning fields: ${unsupported.join(", ")}`)}if(agent==="openclaw"){const missing=TUNING_FIELD_ORDER.filter(field=>advertised.has(field)&&result[field]===null);if(missing.length>0){invalid(`openclaw requires ${missing.join(", ")} tuning`)}}if(agent==="hermes"&&result.contextWindow!==null&&result.contextWindowcanonicalizeJson(item));if(!isPlainObject(value))return value;const result={};const keys=sortStrings(Object.keys(value));for(let index=0;indexMANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`canonical payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}return serialized}function decodeManagedStartupProfile(encoded){if(typeof encoded!=="string"||encoded.length===0||import_node_buffer.Buffer.byteLength(encoded,"ascii")>MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES||!BASE64URL_RE.test(encoded)||encoded.length%4===1){invalid("encoded payload is malformed or exceeds the size limit")}const bytes=import_node_buffer.Buffer.from(encoded,"base64url");if(bytes.length===0||bytes.length>MANAGED_STARTUP_PROFILE_MAX_BYTES||bytes.toString("base64url")!==encoded){invalid("encoded payload is malformed or exceeds the size limit")}let raw;try{raw=UTF8_DECODER.decode(bytes)}catch{invalid("payload is not valid UTF-8")}let parsed;try{parsed=JSON.parse(raw)}catch{invalid("payload is not valid JSON")}const migration=migrateDecodedManagedStartupProfile(parsed);const profile=validateManagedStartupProfile(migration.value);const canonicalPayload=migration.migratedLegacyProfile?JSON.stringify(canonicalizeJson(parsed)):serializeManagedStartupProfile(profile);if(canonicalPayload!==raw){invalid("payload is not in canonical form")}return profile}function fingerprintManagedStartupProfile(profile){return(0,import_node_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){if(profile.inference===null)return{...PROVIDERLESS_INFERENCE_ENV,NEMOCLAW_TOOL_DISCLOSURE:profile.tools.disclosure};return{NEMOCLAW_INFERENCE_API:profile.inference.api,NEMOCLAW_INFERENCE_BASE_URL:profile.inference.routedBaseUrl,NEMOCLAW_INFERENCE_PROVIDER_ID:profile.inference.routeProvider,NEMOCLAW_MODEL:profile.inference.model,NEMOCLAW_TOOL_DISCLOSURE:profile.tools.disclosure,NEMOCLAW_UPSTREAM_PROVIDER:profile.inference.upstreamProvider}}function appendHostProxyEnvironment(environment,profile,options={}){if(options.preserveAmbientWhenAbsent===true&&profile.proxy.hostHttpUrl===null&&profile.proxy.hostHttpsUrl===null&&profile.proxy.hostNoProxy.length===0){return}const httpProxy=profile.proxy.hostHttpUrl??"";const httpsProxy=profile.proxy.hostHttpsUrl??"";const noProxy=profile.proxy.hostNoProxy.join(",");environment.HTTP_PROXY=httpProxy;environment.HTTPS_PROXY=httpsProxy;environment.NO_PROXY=noProxy;environment.http_proxy=httpProxy;environment.https_proxy=httpsProxy;environment.no_proxy=noProxy}function messagingEnvironment(profile,expectedAgent){if(profile.messaging.plan===null)return{};const plan=parseSandboxMessagingPlan(profile.messaging.plan,{agent:expectedAgent});if(!plan){throw new ManagedStartupAgentEnvironmentError(`messaging.plan must contain a validated ${expectedAgent} messaging plan`)}const{workflow:_workflow,...imageBuildPlan}=plan;return{NEMOCLAW_MESSAGING_PLAN_B64:encodeCanonicalJson(imageBuildPlan)}}function corporateCaMaterial(profile){return Object.freeze({kind:"corporate-ca-handoff",legacyInput:"NEMOCLAW_CORPORATE_CA_B64",expectedSha256:profile.corporateCa.bundleSha256})}function rootOwnedFile(legacyInput,path4,value){return Object.freeze({kind:"root-owned-file",legacyInput,path:path4,contents:`${value} +except urllib.error.HTTPError as error: print(error.code)"`]}}],render:[{id:"discord-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.discord",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},proxy:"{{discordProxyUrl}}",dmPolicy:"{{discord.allowedUsers.dmPolicy}}",allowFrom:"{{discord.allowedUsers.values}}"}}}}},{id:"discord-openclaw-guilds",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",when:"{{discord.hasGuilds}}",fragment:{path:"channels.discord",value:{groupPolicy:"allowlist",guilds:"{{discord.guilds}}"}}},{id:"discord-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.discord",value:{enabled:true}}},{id:"discord-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["NEMOCLAW_DISCORD_GUILD_IDS={{discord.guildIds.csv}}","DISCORD_ALLOWED_USERS={{discord.allowedUsers.csv}}","DISCORD_ALLOW_ALL_USERS={{discord.allowAllUsers}}"]},{id:"discord-hermes-config",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"discord",value:{require_mention:"{{discord.requireMention}}",free_response_channels:"",allowed_channels:"",auto_thread:true,reactions:true,channel_prompts:{}}}},{id:"discord-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.discord",value:{enabled:true}}}],runtime:{openclaw:{channelName:"discord",visibility:{configKeys:["discord"],logPatterns:["discord"]}}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/discord@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-j+fSHxbXA+DSxwbL8SvtsDNL6tvBX4+RmH+EerVW6dCbeIwSconXj6J/+AaN/mhzZI4g0jPPj30TUhdCRRbc2w==","2026.9.1":"sha512-qNmN2a8A9dET4igPp0RML171sEn8PDMyNCYNp/DqcJ4tn3XTHpacSOTkqBmv5yXTycJRC9rfFP8FT/SdW0Rldg=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/discord/-/discord-2026.9.1.tgz"},required:true}],hooks:[{id:"discord-openclaw-bridge-health",phase:"health-check",handler:"discord.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"discord-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"discord-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"serverId",kind:"config"},{id:"requireMention",kind:"config"},{id:"userId",kind:"config"}]}]};var googlechatManifest={schemaVersion:1,id:"googlechat",displayName:"Google Chat",description:"Google Chat (Chat API) bot messaging (experimental)",supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"serviceAccount",kind:"secret",required:true,envKey:"GOOGLECHAT_SERVICE_ACCOUNT",allowLineBreaks:true,maskCap:40,formatHint:"Paste the entire service-account JSON key on one line (minified), or set GOOGLECHAT_SERVICE_ACCOUNT to the downloaded JSON, including line breaks.",maxTokenAttempts:3,prompt:{label:"Google Chat service account JSON",help:["\u2503 GOOGLE CHAT \u2014 service account key","\u2503","\u2503 Google Cloud Console \u2192 IAM & Admin \u2192 Service Accounts","\u2503 \u2192 your bot's SA \u2192 Keys \u2192 Add key \u2192 Create new key \u2192 JSON","\u2503","\u2503 A .json file downloads. Paste its contents below as ONE line (minified).","\u2503 Alternatively, GOOGLECHAT_SERVICE_ACCOUNT accepts the formatted JSON file contents.",""].join("\n")}},{id:"audienceType",kind:"config",required:false,envKey:"GOOGLECHAT_AUDIENCE_TYPE",statePath:"googlechatConfig.audienceType",validValues:["app-url","project-number"],defaultValue:"app-url"},{id:"audience",kind:"config",required:false,envKey:"GOOGLECHAT_AUDIENCE",statePath:"googlechatConfig.audience",prompt:{label:"Google Chat webhook audience",help:"Usually filled automatically from the public tunnel URL. For audienceType 'project-number', enter your GCP project number instead.",emptyValueMessage:"inbound webhook verification will be unconfigured"}},{id:"appPrincipal",kind:"config",required:false,envKey:"GOOGLECHAT_APP_PRINCIPAL",statePath:"googlechatConfig.appPrincipal",formatPattern:"^[0-9]{6,32}$",formatHint:"appPrincipal is the add-on's numeric OAuth client ID (uniqueId, ~21 digits), not an email.",prompt:{label:"Google Chat appPrincipal",help:[" Workspace account \u2192 leave blank, done."," Personal Gmail \u2192 needs the add-on's ~21-digit ID (not an email), stable across rebuilds.",""," If you already know it, paste it at the prompt and you're done."," If not, leave it blank \u2014 the first DM reveals it once the sandbox is live:",""," 1. Watch the gateway log:",' nemoclaw logs --follow | grep "unexpected add-on principal"'," 2. DM the bot once \u2014 it won't reply yet, that's expected. The log prints:"," unexpected add-on principal: "," 3. Save that and rebuild:"," GOOGLECHAT_APP_PRINCIPAL= nemoclaw channels add googlechat"," nemoclaw rebuild --yes"].join("\n"),emptyValueMessage:"Workspace accounts do not need it; personal accounts must set it later"}},{id:"allowFrom",kind:"config",required:false,envKey:"GOOGLECHAT_ALLOWED_USERS",statePath:"allowedIds.googlechat",prompt:{label:"Google Chat DM allowlist (comma-separated)",help:["Optional: restrict who can DM the bot."," OpenClaw: users/NNN (emails ignored)"," Hermes: email (users/NNN ignored)"," Blank: pairing mode (recommended) \u2014 OpenClaw's pairing reply shows your users/NNN"," Filling this switches DM policy to allowlist \u2014 a wrong-form entry is dropped silently, with no pairing code."].join("\n"),emptyValueMessage:"bot will require manual pairing"}},{id:"projectId",kind:"config",required:false,envKey:"GOOGLE_CHAT_PROJECT_ID",statePath:"googlechatConfig.projectId",prompt:{label:"Google Chat GCP project ID (Hermes Pub/Sub pull)",help:"The Google Cloud project that owns the Pub/Sub subscription Hermes pulls Chat events from. OpenClaw ignores this.",emptyValueMessage:"required for the Hermes Google Chat channel"}},{id:"subscriptionName",kind:"config",required:false,envKey:"GOOGLE_CHAT_SUBSCRIPTION_NAME",statePath:"googlechatConfig.subscriptionName",prompt:{label:"Google Chat Pub/Sub subscription (projects/

/subscriptions/)",help:["The pull subscription bound to the Chat events topic. Hermes pulls from it over the Pub/Sub REST API; the gateway-minted token is scoped to both chat.bot and pubsub."," Its topic must grant roles/pubsub.publisher to the app's push account:"," Interactive features service-@gcp-sa-gsuiteaddons.iam.gserviceaccount.com"," Classic bot chat-api-push@system.gserviceaccount.com"," Shown at Chat API \u2192 Configuration \u2192 Connection settings"," Missing it channel connects, no event arrives, Chat says the bot is not responding"].join("\n"),emptyValueMessage:"required for the Hermes Google Chat channel"}}],credentials:[],policyPresets:[{name:"googlechat",policyKeys:["googlechat"],agentPolicyKeys:{hermes:["googlechat_hermes"]}}],render:[{id:"googlechat-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.googlechat",value:{enabled:true,serviceAccount:{},audienceType:"{{googlechatConfig.audienceType}}",audience:"{{googlechatConfig.audience}}",appPrincipal:"{{googlechatConfig.appPrincipal}}",webhookPath:"/googlechat",healthMonitor:{enabled:false},dmPolicy:"{{allowedIds.googlechat.dmPolicy}}",allowFrom:"{{allowedIds.googlechat.values}}"}}},{id:"googlechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.googlechat",value:{enabled:true}}},{id:"googlechat-openclaw-gateway-reload-off",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"gateway.reload",value:{mode:"off"}}},{id:"googlechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["GOOGLE_CHAT_PROJECT_ID={{googlechatConfig.projectId}}","GOOGLE_CHAT_SUBSCRIPTION_NAME={{googlechatConfig.subscriptionName}}","GOOGLE_CHAT_ALLOWED_USERS={{allowedIds.googlechat.csv}}"]},{id:"googlechat-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.google_chat",value:{enabled:true}}}],runtime:{openclaw:{channelName:"googlechat",visibility:{configKeys:["googlechat"],logPatterns:["googlechat"]},nodePreloads:[{module:"googlechat-trusted-proxy-fetch",injectInto:["boot"],optional:false,installMessage:"[channels] Installing Google Chat trusted-proxy-fetch patch (route googleapis via trusted env proxy)",installedMessage:"[channels] Google Chat trusted-proxy-fetch patch installed (NODE_OPTIONS updated)"},{module:"googlechat-outbound-auth",injectInto:["boot"],optional:false,installMessage:"[channels] Installing Google Chat outbound-auth patch (gateway-minted bearer)",installedMessage:"[channels] Google Chat outbound-auth patch installed (NODE_OPTIONS updated)"}],secretScans:[{path:"/sandbox/.openclaw/openclaw.json",pattern:"-----BEGIN (?:RSA )?PRIVATE KEY-----",message:"[SECURITY] Google Chat service account private key leaked into {path} - refusing to serve",exitCode:78}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/googlechat@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-LUO8Lg07IhzJfEzxn+GSij8WMS/uX3hTmv0SydTy/0fKSN7iZksf74TaZg50kOoBi8FzeqeXo/zoGVuk0Mj1Ig==","2026.9.1":"sha512-Q5VTAJpfcrI7BSEw5Ugq3wf7JEg5QhTBwpi+BByGbfZsTTVjwZc7OIvNbKsVTh16I5/EWqHEnD+0WNeHqsteqw=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/googlechat/-/googlechat-2026.9.1.tgz"},required:true},{id:"hermesGooglePubsubPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-cloud-pubsub==2.39.0",required:true},{id:"hermesGoogleApiClientPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-api-python-client==2.194.0",required:true},{id:"hermesGoogleAuthPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"google-auth==2.55.1",required:true}],hooks:[{id:"googlechat-tunnel-audience-gate",phase:"enroll",handler:"googlechat.tunnelAudienceGate",agents:["openclaw"],inputs:["audienceType","audience"],outputs:[{id:"audience",kind:"config"}],onFailure:"skip-channel"},{id:"googlechat-service-account",phase:"enroll",handler:"googlechat.tokenPaste",outputs:[{id:"serviceAccount",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"googlechat-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowFrom",kind:"config"}]},{id:"googlechat-openclaw-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["openclaw"],outputs:[{id:"appPrincipal",kind:"config"}]},{id:"googlechat-hermes-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["hermes"],outputs:[{id:"projectId",kind:"config"},{id:"subscriptionName",kind:"config"}]}]};var slackManifest={schemaVersion:1,id:"slack",displayName:"Slack",description:"Slack bot messaging",supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"SLACK_BOT_TOKEN",formatPattern:"^xoxb-[A-Za-z0-9_-]+$",formatHint:"Slack bot tokens start with 'xoxb-' (e.g. xoxb---).",prompt:{label:"Slack Bot Token",help:"Slack API \u2192 Your Apps \u2192 OAuth & Permissions \u2192 Bot User OAuth Token (xoxb-...)."}},{id:"appToken",kind:"secret",required:true,envKey:"SLACK_APP_TOKEN",formatPattern:"^xapp-[A-Za-z0-9_-]+$",formatHint:"Slack app tokens start with 'xapp-' (e.g. xapp----).",prompt:{label:"Slack App Token (Socket Mode)",help:"Slack API \u2192 Your Apps \u2192 Basic Information \u2192 App-Level Tokens (xapp-...)."}},{id:"allowedUsers",kind:"config",required:false,envKey:"SLACK_ALLOWED_USERS",statePath:"allowedIds.slack",prompt:{label:"Slack Member IDs (comma-separated allowlist)",help:"In Slack, open each allowed human user's profile -> More -> Copy member ID. Enter one or more comma-separated member IDs, not the app or bot user ID. Member IDs look like U01ABC2DEF3.",emptyValueMessage:"bot will require manual pairing"}},{id:"allowedChannels",kind:"config",required:false,envKey:"SLACK_ALLOWED_CHANNELS",statePath:"slackConfig.allowedChannels",prompt:{label:"Slack Channel IDs (comma-separated allowlist)",help:"Optional: enter comma-separated Slack channel IDs where the bot may answer @mentions. Channel IDs look like C012AB3CD.",emptyValueMessage:"channel @mentions stay unrestricted by channel ID"}}],credentials:[{id:"slackBotToken",sourceInput:"botToken",providerName:"{sandboxName}-slack-bridge",providerEnvKey:"SLACK_BOT_TOKEN",placeholder:"xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN",primary:true},{id:"slackAppToken",sourceInput:"appToken",providerName:"{sandboxName}-slack-app",providerEnvKey:"SLACK_APP_TOKEN",placeholder:"xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN"}],policyPresets:[{name:"slack",requiredAtCreate:true}],render:[{id:"slack-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.slack",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},dmPolicy:"{{allowedIds.slack.dmPolicy}}",allowFrom:"{{allowedIds.slack.values}}",groupPolicy:"{{allowedIds.slack.groupPolicy}}",channels:"{{allowedIds.slack.channels}}"}}}}},{id:"slack-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.slack",value:{enabled:true}}},{id:"slack-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["SLACK_ALLOWED_USERS={{allowedIds.slack.csv}}","SLACK_ALLOWED_CHANNELS={{slackConfig.allowedChannels.csv}}"]},{id:"slack-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.slack",value:{enabled:true,extra:{rich_blocks:true}}}}],runtime:{openclaw:{channelName:"slack",visibility:{configKeys:["slack"],logPatterns:["slack"]},nodePreloads:[{module:"slack-channel-guard",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Slack channel guard (unhandled-rejection safety net)",installedMessage:"[channels] Slack channel guard installed (NODE_OPTIONS updated)"}],secretScans:[{path:"/sandbox/.openclaw/openclaw.json",pattern:"(?:xoxb|xapp)-(?!OPENSHELL-RESOLVE-ENV-)",message:"[SECURITY] Slack token leaked into {path} - refusing to serve",exitCode:78}]},hermes:{}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/slack@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-6M1M6gL3iXahpalNsYAUuA+wvnV8lbMlNNH2ToegFvaSJcIll4S9kFa5mv3GFoquhMRHQjEjnkXPHP/pXwaWcA==","2026.9.1":"sha512-tU372jE40nnPcKQ6oxmDHf2/UhGtdz8ysi4JKsRZIO1QBAEkZd2YfsOw8aucmb2r0B0vjcFD3OmIV/Qzb57COg=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/slack/-/slack-2026.9.1.tgz"},required:true}],hooks:[{id:"slack-socket-mode-gateway-conflict",phase:"pre-enable",handler:"slack.socketModeGatewayConflict",onFailure:"abort"},{id:"slack-openclaw-bridge-health",phase:"health-check",handler:"slack.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"slack-socket-mode-gateway-status",phase:"status",handler:"slack.socketModeGatewayStatus",outputs:[{id:"gatewayOverlaps",kind:"status"}]},{id:"slack-status-health",phase:"status",handler:"slack.statusHealth",providesReadiness:true,agents:["openclaw"],outputs:[{id:"channelHealth",kind:"status"}]},{id:"slack-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true},{id:"appToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"slack-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowedUsers",kind:"config"},{id:"allowedChannels",kind:"config"}]},{id:"slack-credential-validation",phase:"reachability-check",handler:"slack.validateCredentials",inputs:["botToken","appToken"],onFailure:"skip-channel"}]};var TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT={channelId:"teams",renderId:"teams-openclaw-channel",hookId:"teams-openclaw-channel",handlerId:"common.staticOutputs",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",configPath:"channels.msteams",webhookPath:"/api/messages"};function authorizeTeamsOpenClawWebhookField(entry){if(!isPlainDataObject(entry))return[];const contract=TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT;if(ownDataPropertyValue(entry,"channelId")!==contract.channelId||ownDataPropertyValue(entry,"renderId")!==contract.renderId||ownDataPropertyValue(entry,"hookId")!==contract.hookId||ownDataPropertyValue(entry,"handler")!==contract.handlerId||ownDataPropertyValue(entry,"kind")!==contract.kind||ownDataPropertyValue(entry,"agent")!==contract.agent||ownDataPropertyValue(entry,"target")!==contract.target||ownDataPropertyValue(entry,"path")!==contract.configPath){return[]}const value=ownDataPropertyValue(entry,"value");if(!isPlainDataObject(value))return[];const webhook=ownDataPropertyValue(value,"webhook");if(!isPlainDataObject(webhook)||!hasExactlyOwnDataProperties(webhook,["path","port"])||!isTcpPort(ownDataPropertyValue(webhook,"port"))||ownDataPropertyValue(webhook,"path")!==contract.webhookPath){return[]}return[{path:["value","webhook"],value:webhook}]}function isPlainDataObject(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function hasExactlyOwnDataProperties(value,expected){const actual=Object.getOwnPropertyNames(value).sort();return actual.length===expected.length&&actual.every((key,index)=>key===expected[index])}function isTcpPort(value){return Number.isInteger(value)&&value>=1&&value<=65535}var teamsManifest={schemaVersion:1,id:"teams",displayName:"Microsoft Teams",description:"Microsoft Teams bot messaging (experimental)",enrollmentNotes:["Microsoft Teams requires a public HTTPS webhook endpoint at /api/messages; expose the configured Teams webhook port before installing the Teams app.","Use Azure AD object IDs in TEAMS_ALLOWED_USERS so only authorized users can interact with the bot."],supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"appId",kind:"config",required:true,envKey:"MSTEAMS_APP_ID",statePath:"teamsConfig.appId",prompt:{label:"Microsoft Teams Client ID",help:"Run `teams app create --endpoint https:///api/messages`, then copy CLIENT_ID."}},{id:"clientSecret",kind:"secret",required:true,envKey:"MSTEAMS_APP_PASSWORD",prompt:{label:"Microsoft Teams Client Secret",help:"Use the CLIENT_SECRET printed by `teams app create`. It is shown once; rotate it in Entra ID if it was lost."}},{id:"tenantId",kind:"config",required:true,envKey:"MSTEAMS_TENANT_ID",statePath:"teamsConfig.tenantId",prompt:{label:"Microsoft Teams Tenant ID",help:"Use the TENANT_ID printed by `teams app create` or shown by `teams status --verbose`."}},{id:"allowedUsers",kind:"config",required:false,envKey:"TEAMS_ALLOWED_USERS",statePath:"allowedIds.teams",prompt:{label:"Microsoft Teams AAD Object IDs (comma-separated allowlist)",help:"Recommended: run `teams status --verbose` and enter the Azure AD object IDs allowed to use the bot."}},{id:"webhookPort",kind:"config",required:false,envKey:"MSTEAMS_PORT",statePath:"teamsConfig.webhookPort",defaultValue:"3978",prompt:{label:"Microsoft Teams webhook port",help:"Local bot webhook port to expose publicly. Defaults to 3978 and serves /api/messages."}},{id:"requireMention",kind:"config",required:false,envKey:"TEAMS_REQUIRE_MENTION",statePath:"teamsConfig.requireMention",validValues:["0","1"],defaultValue:"1",prompt:{label:"Microsoft Teams mention mode",help:"Controls OpenClaw group and channel behavior only. Direct messages are unaffected."}}],credentials:[{id:"teamsClientSecret",sourceInput:"clientSecret",providerName:"{sandboxName}-teams-bridge",providerEnvKey:"MSTEAMS_APP_PASSWORD",placeholder:"openshell:resolve:env:MSTEAMS_APP_PASSWORD",primary:true}],policyPresets:[{name:"teams",policyKeys:["teams"],requiredAtCreate:true}],hostForward:{port:"{{teamsConfig.webhookPort}}",label:"Microsoft Teams webhook"},render:[{id:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.renderId,kind:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.kind,agent:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.agent,target:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.target,fragment:{path:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.configPath,value:{enabled:true,appId:"{{teamsConfig.appId}}",tenantId:"{{teamsConfig.tenantId}}",webhook:{port:"{{teamsConfig.webhookPort}}",path:TEAMS_OPENCLAW_WEBHOOK_RENDER_CONTRACT.webhookPath},healthMonitor:{enabled:false},streaming:{mode:"off"},dmPolicy:"{{allowedIds.teams.dmPolicy}}",allowFrom:"{{allowedIds.teams.values}}",groupPolicy:"open",requireMention:"{{teamsConfig.requireMention}}"}}},{id:"teams-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.msteams",value:{enabled:true}}},{id:"teams-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["TEAMS_CLIENT_ID={{teamsConfig.appId}}","TEAMS_TENANT_ID={{teamsConfig.tenantId}}","TEAMS_ALLOWED_USERS={{allowedIds.teams.csv}}","TEAMS_PORT={{teamsConfig.webhookPort}}"]},{id:"teams-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.teams",value:{enabled:true}}}],runtime:{openclaw:{channelName:"msteams",visibility:{configKeys:["msteams"],logPatterns:["msteams","teams"]},nodePreloads:[{module:"msteams-message-hints",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Microsoft Teams message hint patch (native mentions)",installedMessage:"[channels] Microsoft Teams message hint patch installed (NODE_OPTIONS updated)"}]},hermes:{envAliases:[{envKey:"MSTEAMS_APP_PASSWORD",targetEnvKey:"TEAMS_CLIENT_SECRET",match:"^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_MSTEAMS_APP_PASSWORD$",value:"openshell:resolve:env:MSTEAMS_APP_PASSWORD"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/msteams@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-py5KvGOTcd0qGGRf3EuqbH2jO+kZtvMquDMjwGkT6x9F4XZtaCBL/lmLitb4hDb5xaIpPP8ZLIbT8GIMXQr3Og==","2026.9.1":"sha512-seRGr9/X6Vk9xU5elLVpDwq8R+TO0QFvUmxPEitqkngqDnMoXW0LEEXkriG6jgue74w2YLcNnAv/Rjf0a9jong=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/msteams/-/msteams-2026.9.1.tgz"},required:true},{id:"hermesTeamsAppsPackage",agent:"hermes",manager:"hermes-uv-pip",spec:"microsoft-teams-apps==2.0.13.4",required:true}],hooks:[{id:"teams-host-forward-port-conflict",phase:"pre-enable",handler:"teams.hostForwardPortConflict",inputs:["webhookPort"],onFailure:"abort"},{id:"teams-host-forward-port-status",phase:"status",handler:"teams.hostForwardPortStatus",outputs:[{id:"hostForwardPortOverlaps",kind:"status"}]},{id:"teams-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"clientSecret",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"teams-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"appId",kind:"config",required:true},{id:"tenantId",kind:"config",required:true},{id:"allowedUsers",kind:"config"},{id:"webhookPort",kind:"config"},{id:"requireMention",kind:"config"}]}],state:{}};var telegramManifest={schemaVersion:1,id:"telegram",displayName:"Telegram",description:"Telegram bot messaging",diagnosticsProbe:"log-tail",enrollmentNotes:["For Telegram group chats, disable privacy mode in @BotFather (/setprivacy -> your bot -> Disable).","After changing privacy mode, remove and re-add the bot to each group before testing @mentions."],supportedAgents:["openclaw","hermes"],auth:{mode:"token-paste"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"TELEGRAM_BOT_TOKEN",prompt:{label:"Telegram Bot Token",help:"Create a bot via @BotFather on Telegram, then copy the token."}},{id:"allowedIds",kind:"config",required:false,envKey:"TELEGRAM_ALLOWED_IDS",statePath:"allowedIds.telegram",prompt:{label:"Telegram User ID (for DM access)",help:"Send /start to @userinfobot on Telegram to get your numeric user ID.",emptyValueMessage:"bot will require manual pairing"}},{id:"requireMention",kind:"config",required:false,envKey:"TELEGRAM_REQUIRE_MENTION",statePath:"telegramConfig.requireMention",validValues:["0","1"],defaultValue:"1",prompt:{label:"Telegram group mention mode",help:"Controls Telegram group-chat behavior only \u2014 reply only when @mentioned vs. to all group messages. Direct messages are unaffected by this setting and remain subject to pairing and TELEGRAM_ALLOWED_IDS."}},{id:"groupPolicy",kind:"config",required:false,envKey:"TELEGRAM_GROUP_POLICY",statePath:"telegramConfig.groupPolicy",validValues:["open","allowlist","disabled"],defaultValue:"open",prompt:{label:"Telegram group policy",help:"Controls OpenClaw Telegram group access. Hermes does not expose an equivalent disable-groups policy."}}],credentials:[{id:"telegramBotToken",sourceInput:"botToken",providerName:"{sandboxName}-telegram-bridge",providerEnvKey:"TELEGRAM_BOT_TOKEN",placeholder:"openshell:resolve:env:TELEGRAM_BOT_TOKEN"}],policyPresets:[{name:"telegram",requiredAtCreate:true,policyKeys:["telegram_bot"],agentPolicyKeys:{hermes:["telegram"]}}],render:[{id:"telegram-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.telegram",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false},proxy:"{{proxyUrl}}",groupPolicy:"{{telegramConfig.groupPolicy}}",dmPolicy:"{{allowedIds.telegram.dmPolicy}}",allowFrom:"{{allowedIds.telegram.values}}"}}}}},{id:"telegram-openclaw-groups",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",when:"{{telegramConfig.openclawGroups}}",fragment:{path:"channels.telegram.groups",value:"{{telegramConfig.openclawGroups}}"}},{id:"telegram-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.telegram",value:{enabled:true}}},{id:"telegram-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["TELEGRAM_ALLOWED_USERS={{allowedIds.telegram.csv}}"]},{id:"telegram-hermes-config",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"telegram",value:{require_mention:"{{telegramConfig.requireMention}}"}}},{id:"telegram-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.telegram",value:{enabled:true}}}],runtime:{openclaw:{channelName:"telegram",visibility:{configKeys:["telegram"],logPatterns:["telegram"]},nodePreloads:[{module:"telegram-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing Telegram diagnostics (provider readiness + inference errors)",installedMessage:"[channels] Telegram diagnostics installed (NODE_OPTIONS updated)"}]}},hooks:[{id:"telegram-token-paste",phase:"enroll",handler:"common.tokenPaste",outputs:[{id:"botToken",kind:"secret",required:true}],onFailure:"skip-channel"},{id:"telegram-allowlist-aliases",phase:"enroll",handler:"telegram.allowlistAliases",outputs:[{id:"allowedIds",kind:"config"}]},{id:"telegram-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"requireMention",kind:"config"},{id:"allowedIds",kind:"config"}]},{id:"telegram-openclaw-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["openclaw"],outputs:[{id:"groupPolicy",kind:"config"}]},{id:"telegram-get-me-reachability",phase:"reachability-check",handler:"telegram.getMeReachability",inputs:["botToken"],onFailure:"skip-channel"},{id:"telegram-openclaw-bridge-health",phase:"health-check",handler:"telegram.openclawBridgeHealth",agents:["openclaw"],onFailure:"abort"},{id:"telegram-gateway-conflict-status",phase:"status",handler:"telegram.gatewayConflictStatus",outputs:[{id:"bridgeHealth",kind:"status"}]},{id:"telegram-status-health",phase:"status",handler:"telegram.statusHealth",agents:["openclaw"],outputs:[{id:"channelHealth",kind:"status"}]}]};var WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT={channelId:"wechat",planHookId:"wechat-seed-openclaw-account",handlerId:"wechat.seedOpenClawAccount",outputId:"openclawWeixinAccountFile",kind:"build-file",required:true,mode:"0600"};var WECHAT_SEED_OPENCLAW_ACCOUNT_HOOK_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.handlerId;var WECHAT_SEED_OPENCLAW_ACCOUNT_PLAN_HOOK_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.planHookId;var WECHAT_OPENCLAW_ACCOUNT_FILE_OUTPUT_ID=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.outputId;var WECHAT_TOKEN_PLACEHOLDER="openshell:resolve:env:WECHAT_BOT_TOKEN";function authorizeWechatAccountFilePlaceholders(value){const content=isPlainDataObject2(value)?ownDataPropertyValue2(value,"content"):void 0;if(!isPlainDataObject2(value)||!hasExactlyOwnDataProperties2(value,["content","mode","path"])||!isWechatAccountFilePath(ownDataPropertyValue2(value,"path"))||ownDataPropertyValue2(value,"mode")!==WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.mode||!isPlainDataObject2(content)||!hasOnlyOwnDataProperties(content,["baseUrl","savedAt","token","userId"])||!hasOwnDataProperty(content,"savedAt")||!hasOwnDataProperty(content,"token")||ownDataPropertyValue2(content,"token")!==WECHAT_TOKEN_PLACEHOLDER||!isNonEmptyString(ownDataPropertyValue2(content,"savedAt"))||!isOptionalNonEmptyString(content,"baseUrl")||!isOptionalNonEmptyString(content,"userId")){return[]}return[{path:["content","token"],value:WECHAT_TOKEN_PLACEHOLDER}]}function isWechatAccountFilePath(value){if(typeof value!=="string")return false;const prefix="openclaw-weixin/accounts/";const suffix=".json";if(!value.startsWith(prefix)||!value.endsWith(suffix))return false;const accountId=value.slice(prefix.length,-suffix.length);return accountId===accountId.trim()&&isSafeWechatAccountId(accountId)}function isSafeWechatAccountId(accountId){return accountId.length>0&&accountId!=="."&&accountId!==".."&&!/[\\/\0-\x1F\x7F]/.test(accountId)&&!accountId.includes("..")}function isPlainDataObject2(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue2(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function hasOwnDataProperty(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor!==void 0&&"value"in descriptor}function hasExactlyOwnDataProperties2(value,expected){const actual=Object.getOwnPropertyNames(value).sort();return actual.length===expected.length&&actual.every((key,index)=>key===expected[index])}function hasOnlyOwnDataProperties(value,allowed){return Object.getOwnPropertyNames(value).every(key=>allowed.includes(key))}function isNonEmptyString(value){return typeof value==="string"&&value.length>0}function isOptionalNonEmptyString(value,key){return!hasOwnDataProperty(value,key)||isNonEmptyString(ownDataPropertyValue2(value,key))}var wechatManifest={schemaVersion:1,id:"wechat",displayName:"WeChat",description:"WeChat (personal) bot messaging",enrollmentHelp:"Captured automatically via a host-side QR scan during onboard \u2014 pair the bot by scanning the QR with WeChat on your phone (Discover \u2192 Scan). DM-only.",supportedAgents:["openclaw","hermes"],auth:{mode:"host-qr"},inputs:[{id:"botToken",kind:"secret",required:true,envKey:"WECHAT_BOT_TOKEN",prompt:{label:"WeChat Bot Token",help:"Captured automatically via a host-side QR scan during onboard \u2014 pair the bot by scanning the QR with WeChat on your phone (Discover \u2192 Scan). DM-only."}},{id:"accountId",kind:"config",required:true,envKey:"WECHAT_ACCOUNT_ID",statePath:"wechatConfig.accountId"},{id:"baseUrl",kind:"config",required:false,envKey:"WECHAT_BASE_URL",statePath:"wechatConfig.baseUrl"},{id:"userId",kind:"config",required:false,envKey:"WECHAT_USER_ID",statePath:"wechatConfig.userId"},{id:"allowedIds",kind:"config",required:false,envKey:"WECHAT_ALLOWED_IDS",statePath:"allowedIds.wechat",prompt:{label:"WeChat User ID(s) (DM allowlist)",help:"Optional: restrict who can DM the bot. The WeChat user id of the operator who scanned is added automatically; supply additional ids as a comma-separated list.",emptyValueMessage:"bot will require manual pairing"}}],credentials:[{id:"wechatBotToken",sourceInput:"botToken",providerName:"{sandboxName}-wechat-bridge",providerEnvKey:"WECHAT_BOT_TOKEN",placeholder:"openshell:resolve:env:WECHAT_BOT_TOKEN"}],state:{openclaw:["wechat","openclaw-weixin"]},policyPresets:[{name:"wechat",policyKeys:["wechat_bridge"],requiredAtCreate:true}],render:[{id:"wechat-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.openclaw-weixin",value:{enabled:true}}},{id:"wechat-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.openclaw-weixin",value:{enabled:true}}},{id:"wechat-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WEIXIN_ACCOUNT_ID={{wechatConfig.accountId}}","WEIXIN_BASE_URL={{wechatConfig.baseUrl}}","WEIXIN_ALLOWED_USERS={{allowedIds.wechat.csv}}"]},{id:"wechat-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.weixin",value:{enabled:true}}}],runtime:{openclaw:{channelName:"openclaw-weixin",visibility:{configKeys:["openclaw-weixin"],logPatterns:["wechat","openclaw-weixin"]},nodePreloads:[{module:"wechat-account-placeholder",injectInto:["boot"],optional:false},{module:"wechat-diagnostics",injectInto:["boot","connect"],optional:false,installMessage:"[channels] Installing WeChat diagnostics (provider readiness + inference errors)",installedMessage:"[channels] WeChat diagnostics installed (NODE_OPTIONS updated)"}]},hermes:{envAliases:[{envKey:"WECHAT_BOT_TOKEN",targetEnvKey:"WEIXIN_TOKEN",match:"^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_WECHAT_BOT_TOKEN$",value:"openshell:resolve:env:WECHAT_BOT_TOKEN"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@tencent-weixin/openclaw-weixin@2.4.9",pin:true,integrity:"sha512-SfaYehR1Cwq2VV5HxJBp9sVilMms420VfZlMbF4YjRbWomr5+GxfXp9HkeU6y5TbnOc4Ysq0qPw1yBvJwbenBA==",tarballUrl:"https://registry.npmjs.org/@tencent-weixin/openclaw-weixin/-/openclaw-weixin-2.4.9.tgz",runtimeLock:{cachePath:"/usr/local/share/nemoclaw/wechat-npm-cache",installCacheEnvKey:"NEMOCLAW_WECHAT_NPM_INSTALL_CACHE",lockFile:"/usr/local/lib/nemoclaw/wechat-runtime/package-lock.json",projectsRoot:"/sandbox/.openclaw/npm/projects",verifierPath:"/usr/local/lib/nemoclaw/verify-wechat-runtime-lock.mts",offline:true,legacyPeerDeps:true},required:true}],hooks:[{id:"wechat-host-qr",phase:"enroll",handler:"wechat.ilinkLogin",inputs:["allowedIds"],outputs:[{id:"botToken",kind:"secret",required:true},{id:"accountId",kind:"config",required:true},{id:"baseUrl",kind:"config"},{id:"userId",kind:"config"},{id:"allowedIds",kind:"config"}],onFailure:"skip-channel"},{id:"wechat-config-prompt",phase:"enroll",handler:"common.configPrompt",outputs:[{id:"allowedIds",kind:"config"}]},{id:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.planHookId,phase:"post-agent-install",handler:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.handlerId,agents:["openclaw"],inputs:["wechatConfig.accountId","wechatConfig.baseUrl","wechatConfig.userId","credential.wechatBotToken.placeholder"],outputs:[{id:"openclawWeixinAccountsIndex",kind:"build-file",required:true},{id:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.outputId,kind:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.kind,required:WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT.required},{id:"openclawConfigPatch",kind:"build-file",required:true}],onFailure:"abort"},{id:"wechat-health-check",phase:"health-check",handler:"wechat.healthCheck",inputs:["wechatConfig.accountId"],onFailure:"abort"}]};var whatsappManifest={schemaVersion:1,id:"whatsapp",displayName:"WhatsApp",description:"WhatsApp Web messaging (QR pairing)",enrollmentHelp:"WhatsApp Web pairs via QR code scanned with your phone \u2014 no host-side token. After the sandbox is running, run `openshell term` and then use `openclaw channels login --channel whatsapp` for OpenClaw or `hermes whatsapp` for Hermes to display the QR.",enrollmentNotes:["After pairing, run `nemoclaw channels status --channel whatsapp`. OpenClaw reports inbound delivery evidence; Hermes reports gateway and dashboard session-path diagnostics."],supportedAgents:["openclaw","hermes"],auth:{mode:"in-sandbox-qr"},inputs:[{id:"mode",kind:"config",required:false,envKey:"WHATSAPP_MODE",statePath:"whatsappConfig.mode",validValues:["self-chat","bot"],defaultValue:"self-chat",prompt:{label:"WhatsApp reply mode",help:"self-chat replies only to messages the paired account sends to itself. bot replies to other senders and stops replying to that self-chat: an unknown sender receives a pairing code you approve with `hermes pairing approve whatsapp `, unless you set WHATSAPP_ALLOWED_IDS to a fixed sender list before this command.",emptyValueMessage:"the sandbox replies only in your own self-chat"}},{id:"allowedIds",kind:"config",required:false,envKey:"WHATSAPP_ALLOWED_IDS",statePath:"allowedIds.whatsapp"}],credentials:[],policyPresets:["whatsapp"],render:[{id:"whatsapp-openclaw-channel",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"channels.whatsapp",value:{enabled:true,accounts:{default:{enabled:true,healthMonitor:{enabled:false}}}}}},{id:"whatsapp-openclaw-plugin",kind:"json-fragment",agent:"openclaw",target:"openclaw.json",fragment:{path:"plugins.entries.whatsapp",value:{enabled:true}}},{id:"whatsapp-hermes-env",kind:"env-lines",agent:"hermes",target:"~/.hermes/.env",lines:["WHATSAPP_ENABLED=true","WHATSAPP_MODE={{whatsappConfig.mode}}","WHATSAPP_DM_POLICY={{whatsappConfig.dmPolicy}}","WHATSAPP_ALLOWED_USERS={{allowedIds.whatsapp.csv}}"]},{id:"whatsapp-hermes-platform",kind:"json-fragment",agent:"hermes",target:"~/.hermes/config.yaml",fragment:{path:"platforms.whatsapp",value:{enabled:true}}}],runtime:{openclaw:{channelName:"whatsapp",visibility:{configKeys:["whatsapp"],logPatterns:["whatsapp"]},nodePreloads:[{module:"whatsapp-qr-compact",injectInto:["connect"],optional:true,installMessage:"[channels] Installing WhatsApp compact-QR renderer (scan-friendly pairing)"}]}},agentPackages:[{id:"openclawPluginPackage",agent:"openclaw",manager:"openclaw-plugin",spec:"npm:@openclaw/whatsapp@{{openclaw.version}}",pin:true,integrityByVersion:{"2026.9.2":"sha512-vOWQIk7FpLHrhMmO+FaLi+pnFB82hiWNJJFJONkBuofERh2SMEz7EMut/vECFFEjFnmOZSVlYfRlxhbNkd/R6g==","2026.9.1":"sha512-llIcoMa6FM4SgYn7GG1FQIeTTA5JDdcHW5D7PT+3aGYT3/E2eLFutKwDvD/w7G0hvDwSftzZgLi3iA8dzK7a3A=="},tarballUrlByVersion:{"2026.9.2":"https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.2.tgz","2026.9.1":"https://registry.npmjs.org/@openclaw/whatsapp/-/whatsapp-2026.9.1.tgz"},required:true}],hooks:[{id:"whatsapp-config-prompt",phase:"enroll",handler:"common.configPrompt",agents:["hermes"],outputs:[{id:"mode",kind:"config"}]},{id:"whatsapp-status-health",phase:"status",handler:"whatsapp.statusHealth",agents:["openclaw","hermes"],outputs:[{id:"channelHealth",kind:"status"}]}]};var BUILT_IN_CHANNEL_MANIFESTS=[telegramManifest,discordManifest,wechatManifest,slackManifest,whatsappManifest,teamsManifest,googlechatManifest];function createBuiltInChannelManifestRegistry(){return createChannelManifestRegistry(BUILT_IN_CHANNEL_MANIFESTS)}var EXACT_TEMPLATE_PATTERN=/^\{\{\s*([^}]+?)\s*\}\}$/;var TEMPLATE_REFERENCE_PATTERN=/\{\{\s*([^}]+?)\s*\}\}/g;function resolvedRenderTemplateReference(value){return{matched:true,value}}function resolveSandboxNameTemplate(value,sandboxName){return value.replaceAll("{sandboxName}",sandboxName)}function resolveRenderTemplatesInValue(value,context){if(typeof value==="string")return resolveRenderTemplatesInString(value,context);if(Array.isArray(value)){if(value.length===0)return value;const resolved=value.map(entry=>resolveRenderTemplatesInValue(entry,context)).filter(entry=>entry!==void 0);return resolved.length>0?resolved:void 0}if(value&&typeof value==="object"){const sourceEntries=Object.entries(value);if(sourceEntries.length===0)return value;const entries=sourceEntries.map(([key,entry])=>[key,resolveRenderTemplatesInValue(entry,context)]).filter(entry=>entry[1]!==void 0);return entries.length>0?Object.fromEntries(entries):void 0}return value}function isTruthyRenderTemplate(value,context){if(!value)return true;const resolved=resolveRenderTemplatesInString(value,context);if(resolved===void 0||resolved===null||resolved===false)return false;if(Array.isArray(resolved))return resolved.length>0;if(typeof resolved==="object")return Object.keys(resolved).length>0;if(typeof resolved==="string")return resolved.trim().length>0;return true}function resolveRenderTemplatesInString(value,context){const exact=value.match(EXACT_TEMPLATE_PATTERN);if(exact?.[1])return resolveTemplateReference(exact[1].trim(),context);let omitted=false;const resolved=value.replace(TEMPLATE_REFERENCE_PATTERN,(match,reference)=>{const replacement=resolveTemplateReference(reference.trim(),context);if(replacement===void 0||replacement===null){omitted=true;return""}if(Array.isArray(replacement))return replacement.map(String).join(",");if(typeof replacement==="object")return JSON.stringify(replacement);return String(replacement)});return omitted?void 0:resolved}function resolveTemplateReference(reference,context){const resolved=context.referenceResolver?.(reference,context);return resolved?.matched?resolved.value:"{{"+reference+"}}"}function allowedIds(context,channel){return parseList(stateValue(context,`allowedIds.${channel}`))}function stateValue(context,path4){const stateInput=context.inputs.find(input=>input.statePath===path4);if(stateInput?.value!==void 0)return stateInput.value;const inputId=path4.split(".").at(-1);return context.inputs.find(input=>input.inputId===inputId)?.value}function parseList(value){if(Array.isArray(value))return unique(value.map(String).map(cleanString).filter(Boolean));const text=cleanString(value);if(!text)return[];return unique(text.split(",").map(cleanString).filter(Boolean))}function parseBoolean(value){if(typeof value==="boolean")return value;const text=cleanString(value)?.toLowerCase();if(text==="1"||text==="true"||text==="yes"||text==="on")return true;if(text==="0"||text==="false"||text==="no"||text==="off")return false;return void 0}function nonEmptyString(value){return cleanString(value)||void 0}function cleanString(value){const text=String(value??"");if(/[\r\n]/.test(text)){throw new Error("Messaging template values must not contain line breaks.")}return text.trim()}function nonEmptyArray(values){return values.length>0?[...values]:void 0}function nonEmptyCsv(values){return values.length>0?values.join(","):void 0}function nonEmptyObject(value){return Object.keys(value).length>0?value:void 0}function unique(values){return[...new Set(values)]}var resolveDiscordTemplateReference=(reference,context)=>{if(reference==="discordProxyUrl")return resolvedRenderTemplateReference(void 0);switch(reference){case"discord.guilds":return resolvedRenderTemplateReference(nonEmptyObject(discordGuilds(context)));case"discord.hasGuilds":return resolvedRenderTemplateReference(Object.keys(discordGuilds(context)).length>0);case"discord.guildIds.csv":return resolvedRenderTemplateReference(nonEmptyCsv(Object.keys(discordGuilds(context))));case"discord.allowedUsers.values":return resolvedRenderTemplateReference(nonEmptyArray(discordAllowedUsers(context)));case"discord.allowedUsers.csv":return resolvedRenderTemplateReference(nonEmptyCsv(discordAllowedUsers(context)));case"discord.allowedUsers.dmPolicy":return resolvedRenderTemplateReference(discordAllowedUsers(context).length>0?"allowlist":void 0);case"discord.allowAllUsers":return resolvedRenderTemplateReference(Object.keys(discordGuilds(context)).length>0&&discordAllowedUsers(context).length===0?true:void 0);case"discord.requireMention":return resolvedRenderTemplateReference(discordRequireMention(context));default:return void 0}};function discordGuilds(context){const serverIds=parseList(stateValue(context,"discordGuilds.serverId"));if(serverIds.length===0)return{};const users=parseList(stateValue(context,"discordGuilds.userIds"));const requireMention=parseBoolean(stateValue(context,"discordGuilds.requireMention"))??true;return Object.fromEntries(serverIds.map(serverId=>[serverId,{requireMention,...users.length>0?{users}:{}}]))}function discordAllowedUsers(context){const users=new Set(allowedIds(context,"discord"));for(const guild of Object.values(discordGuilds(context))){for(const user of guild.users??[])users.add(String(user))}return[...users]}function discordRequireMention(context){for(const guild of Object.values(discordGuilds(context))){if(typeof guild.requireMention==="boolean")return guild.requireMention}return true}var DEFAULT_AUDIENCE_TYPE="app-url";var APP_PRINCIPAL_DISCOVERY_SENTINEL="000000000000000000000";var resolveGooglechatTemplateReference=(reference,context)=>{switch(reference){case"googlechatConfig.audienceType":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.audienceType"))??DEFAULT_AUDIENCE_TYPE);case"googlechatConfig.audience":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.audience")));case"googlechatConfig.appPrincipal":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.appPrincipal"))??APP_PRINCIPAL_DISCOVERY_SENTINEL);case"googlechatConfig.projectId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.projectId")));case"googlechatConfig.subscriptionName":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"googlechatConfig.subscriptionName")));default:break}const allowReference=reference.match(/^allowedIds[.]googlechat[.](values|dmPolicy|csv)$/);if(!allowReference?.[1])return void 0;const ids=allowedIds(context,"googlechat");switch(allowReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);case"csv":return resolvedRenderTemplateReference(ids.length>0?ids.join(","):void 0);default:return void 0}};var resolveSlackTemplateReference=(reference,context)=>{if(reference==="slackConfig.allowedChannels.csv"){return resolvedRenderTemplateReference(nonEmptyCsv(slackAllowedChannels(context)))}const allowedIdsReference=reference.match(/^allowedIds[.]slack[.](values|csv|dmPolicy|groupPolicy|channels)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"slack");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);case"groupPolicy":return resolvedRenderTemplateReference(ids.length>0||slackAllowedChannels(context).length>0?"allowlist":void 0);case"channels":return resolvedRenderTemplateReference(slackChannelConfig(context,ids));default:return void 0}};function slackChannelConfig(context,users){const allowedChannels=slackAllowedChannels(context);const entry={enabled:true,requireMention:true,...users.length>0?{users:[...users]}:{}};if(allowedChannels.length>0){return Object.fromEntries(allowedChannels.map(channelId=>[channelId,{...entry}]))}return users.length>0?{"*":entry}:void 0}function slackAllowedChannels(context){return parseList(stateValue(context,"slackConfig.allowedChannels"))}var DEFAULT_TEAMS_WEBHOOK_PORT=3978;var resolveTeamsTemplateReference=(reference,context)=>{switch(reference){case"teamsConfig.appId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"teamsConfig.appId")));case"teamsConfig.tenantId":return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"teamsConfig.tenantId")));case"teamsConfig.webhookPort":return resolvedRenderTemplateReference(teamsWebhookPort(context));case"teamsConfig.requireMention":return resolvedRenderTemplateReference(parseBoolean(stateValue(context,"teamsConfig.requireMention")));default:break}const allowedIdsReference=reference.match(/^allowedIds[.]teams[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"teams");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function teamsWebhookPort(context){const raw=nonEmptyString(stateValue(context,"teamsConfig.webhookPort"));if(!raw)return DEFAULT_TEAMS_WEBHOOK_PORT;const port=Number(raw);if(!Number.isInteger(port)||port<1||port>65535){throw new Error("Microsoft Teams webhook port must be an integer TCP port between 1 and 65535.")}return port}var DEFAULT_PROXY_HOST="10.200.0.1";var DEFAULT_PROXY_PORT="3128";var DEFAULT_TELEGRAM_GROUP_POLICY="open";var TELEGRAM_GROUP_POLICIES=new Set(["open","allowlist","disabled"]);var resolveTelegramTemplateReference=(reference,context)=>{if(reference==="proxyUrl")return resolvedRenderTemplateReference(proxyUrl(context.env));if(reference==="telegramConfig.groupPolicy"){return resolvedRenderTemplateReference(telegramGroupPolicy(context))}if(reference==="telegramConfig.openclawGroups"){return resolvedRenderTemplateReference(telegramOpenClawGroups(context))}if(reference==="telegramConfig.requireMention"){return resolvedRenderTemplateReference(parseBoolean(stateValue(context,"telegramConfig.requireMention")))}const allowedIdsReference=reference.match(/^allowedIds[.]telegram[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"telegram");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function proxyUrl(env){const host=nonEmptyString(env?.NEMOCLAW_PROXY_HOST)??DEFAULT_PROXY_HOST;const port=nonEmptyString(env?.NEMOCLAW_PROXY_PORT)??DEFAULT_PROXY_PORT;return`http://${host}:${port}`}function telegramGroupPolicy(context){const value=nonEmptyString(stateValue(context,"telegramConfig.groupPolicy"));return value&&TELEGRAM_GROUP_POLICIES.has(value)?value:DEFAULT_TELEGRAM_GROUP_POLICY}function telegramOpenClawGroups(context){if(telegramGroupPolicy(context)!=="open")return void 0;const requireMention=parseBoolean(stateValue(context,"telegramConfig.requireMention"));return requireMention===true?{"*":{requireMention:true}}:void 0}var WECHAT_ILINK_HOSTS=new Set(["ilinkai.weixin.qq.com","ilinkai.wechat.com"]);var WECHAT_ILINK_IDC_HOST_PATTERN=/^idc-[0-9]+[.]weixin[.]qq[.]com$/u;function normalizeWechatIlinkBaseUrl(value){const raw=String(value??"");if(/[\r\n]/.test(raw)){throw new Error("WeChat baseUrl must not contain line breaks.")}const text=raw.trim();if(!text)return void 0;let url;try{url=new URL(text)}catch{throw new Error("WeChat baseUrl must be a valid URL.")}if(url.protocol!=="https:"){throw new Error("WeChat baseUrl must use HTTPS.")}if(url.username||url.password){throw new Error("WeChat baseUrl must not include credentials.")}const authority=text.match(/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/iu)?.[1]??"";if(authority.includes(":")){throw new Error("WeChat baseUrl must not include an explicit port.")}if(!isWechatIlinkHost(url.hostname)){throw new Error("WeChat baseUrl must use an expected iLink host.")}if(url.pathname&&url.pathname!=="/"||url.search||url.hash){throw new Error("WeChat baseUrl must be an iLink origin URL.")}return url.origin}function isWechatIlinkHost(hostname){const normalized=hostname.toLowerCase();return WECHAT_ILINK_HOSTS.has(normalized)||isWechatIlinkIdcHost(normalized)}function isWechatIlinkIdcHost(hostname){return WECHAT_ILINK_IDC_HOST_PATTERN.test(hostname.toLowerCase())}var resolveWechatTemplateReference=(reference,context)=>{const wechatConfig=reference.match(/^wechatConfig[.](accountId|baseUrl|userId)$/);if(wechatConfig?.[1]){if(wechatConfig[1]==="baseUrl"){return resolvedRenderTemplateReference(normalizeWechatIlinkBaseUrl(stateValue(context,"wechatConfig.baseUrl")))}return resolvedRenderTemplateReference(nonEmptyString(stateValue(context,"wechatConfig."+wechatConfig[1])))}const allowedIdsReference=reference.match(/^allowedIds[.]wechat[.](values|csv|dmPolicy)$/);if(!allowedIdsReference?.[1])return void 0;const ids=wechatAllowedIds(context);switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));case"dmPolicy":return resolvedRenderTemplateReference(ids.length>0?"allowlist":void 0);default:return void 0}};function wechatAllowedIds(context){const ids=allowedIds(context,"wechat");const userId=nonEmptyString(stateValue(context,"wechatConfig.userId"));return userId&&!ids.includes(userId)?[userId,...ids]:ids}var DEFAULT_WHATSAPP_MODE="self-chat";var BOT_WHATSAPP_MODE="bot";var WHATSAPP_MODES=new Set([DEFAULT_WHATSAPP_MODE,BOT_WHATSAPP_MODE]);var resolveWhatsappTemplateReference=(reference,context)=>{if(reference==="whatsappConfig.mode"){return resolvedRenderTemplateReference(whatsappMode(context))}if(reference==="whatsappConfig.dmPolicy"){return resolvedRenderTemplateReference(whatsappDmPolicy(context))}const allowedIdsReference=reference.match(/^allowedIds[.]whatsapp[.](values|csv)$/);if(!allowedIdsReference?.[1])return void 0;const ids=allowedIds(context,"whatsapp");switch(allowedIdsReference[1]){case"values":return resolvedRenderTemplateReference(nonEmptyArray(ids));case"csv":return resolvedRenderTemplateReference(nonEmptyCsv(ids));default:return void 0}};function whatsappMode(context){const value=nonEmptyString(stateValue(context,"whatsappConfig.mode"));return value&&WHATSAPP_MODES.has(value)?value:DEFAULT_WHATSAPP_MODE}function whatsappDmPolicy(context){if(whatsappMode(context)!==BOT_WHATSAPP_MODE)return void 0;return allowedIds(context,"whatsapp").length>0?"allowlist":"pairing"}var BUILT_IN_TEMPLATE_REFERENCE_RESOLVERS=[resolveTelegramTemplateReference,resolveDiscordTemplateReference,resolveWechatTemplateReference,resolveSlackTemplateReference,resolveWhatsappTemplateReference,resolveTeamsTemplateReference,resolveGooglechatTemplateReference];function createBuiltInRenderTemplateResolver(){return(reference,context)=>{for(const resolver of BUILT_IN_TEMPLATE_REFERENCE_RESOLVERS){const resolved=resolver(reference,context);if(resolved)return resolved}return void 0}}var import_node_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 generationMatch=suffix.match(/^(?:v[0-9]{1,20}|s[a-f0-9]{64})_(.+)$/);return generationMatch?.[1]===envKey}function hasFullPersistedCredentialBindingShape(binding){return typeof binding.channelId==="string"&&typeof binding.credentialId==="string"&&typeof binding.sourceInput==="string"&&typeof binding.providerName==="string"&&typeof binding.providerEnvKey==="string"&&typeof binding.placeholder==="string"&&typeof binding.credentialAvailable==="boolean"}function normalizeFullPersistedCredentialBindings(bindings){return bindings.map(binding=>({channelId:binding.channelId,credentialId:binding.credentialId,sourceInput:binding.sourceInput,providerName:binding.providerName,providerEnvKey:binding.providerEnvKey,placeholder:normalizeProviderPlaceholderForEnvKey(binding.placeholder,binding.providerEnvKey)??binding.placeholder,credentialAvailable:binding.credentialAvailable===true,...typeof binding.credentialHash==="string"?{credentialHash:binding.credentialHash}:{}}))}function normalizePersistedAgentCredentialPlaceholders(render,credentialBindings){const credentialEnvKeys=new Set(credentialBindings.map(binding=>binding.providerEnvKey).filter(Boolean));if(credentialEnvKeys.size===0)return[...render];return render.map(entry=>{if(entry.kind!=="env-lines")return entry;return{...entry,lines:entry.lines.map(line=>normalizeCredentialEnvLine(line,credentialEnvKeys))}})}function normalizeCredentialEnvLine(line,credentialEnvKeys){const index=line.indexOf("=");if(index<=0)return line;const envKey=line.slice(0,index).trim();if(!credentialEnvKeys.has(envKey))return line;const value=line.slice(index+1);const normalized=normalizeProviderPlaceholderForEnvKey(value,envKey);return normalized?`${envKey}=${normalized}`:line}function normalizePersistedSandboxMessagingPlanShape(plan,environment=process.env){const manifestRegistry=createBuiltInChannelManifestRegistry();const disabledChannels=plan.disabledChannels.filter(channelId=>typeof channelId==="string");const disabledSet=new Set(disabledChannels);const channels=plan.channels.map(channel=>normalizePersistedChannel(channel,disabledSet,manifestRegistry.get(channel.channelId),environment));const credentialBindings=normalizePersistedCredentialBindings(plan,channels,manifestRegistry,environment);const normalizedPlan={...plan,channels,disabledChannels,credentialBindings,networkPolicy:plan.networkPolicy&&Array.isArray(plan.networkPolicy.entries)?plan.networkPolicy:{presets:[],entries:[]},agentRender:normalizePersistedAgentCredentialPlaceholders(Array.isArray(plan.agentRender)?[...plan.agentRender]:[],credentialBindings),buildSteps:Array.isArray(plan.buildSteps)?[...plan.buildSteps]:[],...plan.runtimeSetup!==void 0?{runtimeSetup:normalizeRuntimeSetup(plan.runtimeSetup)}:{},stateUpdates:Array.isArray(plan.stateUpdates)?[...plan.stateUpdates]:[],healthChecks:Array.isArray(plan.healthChecks)?[...plan.healthChecks]:[]};return normalizedPlan}function normalizePersistedChannel(channel,disabledSet,manifest,environment){const disabled=channel.disabled??disabledSet.has(channel.channelId);const configured=channel.configured??true;const hasFullShape=hasFullChannelShape(channel);const inputs=hasFullShape?normalizeFullInputs(channel.channelId,channel.inputs??[]):normalizePersistedInputs(channel,manifest);const active=channel.active??(configured&&!disabled&&requiredInputsAvailable(manifest,inputs));const hostForward=manifest?planHostForward(manifest,inputs,active&&!disabled,createBuiltInRenderTemplateResolver(),environment):void 0;return{channelId:channel.channelId,displayName:channel.displayName??manifest?.displayName??channel.channelId,authMode:channel.authMode??manifest?.auth.mode??"none",active,selected:channel.selected??configured,configured,disabled,...channel.pendingRemoval===true?{pendingRemoval:true}:{},inputs,...hostForward?{hostForward}:{},hooks:Array.isArray(channel.hooks)?[...channel.hooks]:[]}}function normalizePersistedInputs(channel,manifest){const persistedById=new Map((channel.inputs??[]).filter(input=>typeof input.inputId==="string").map(input=>[input.inputId,input]));const fromManifest=(manifest?.inputs??[]).map(input=>inputReferenceFromManifest(channel.channelId,input,persistedById.get(input.id)));const manifestInputIds=new Set((manifest?.inputs??[]).map(input=>input.id));const unknownInputs=[...persistedById.values()].flatMap(input=>{if(!input.inputId||manifestInputIds.has(input.inputId))return[];return[normalizeUnknownInput(channel.channelId,input)]});return[...fromManifest,...unknownInputs]}function normalizeFullInputs(channelId,inputs){return inputs.filter(input=>typeof input.inputId==="string").map(input=>({channelId:typeof input.channelId==="string"?input.channelId:channelId,inputId:input.inputId,kind:input.kind==="secret"||input.kind==="config"?input.kind:"config",required:typeof input.required==="boolean"?input.required:false,...typeof input.sourceEnv==="string"?{sourceEnv:input.sourceEnv}:{},...typeof input.statePath==="string"?{statePath:input.statePath}:{},...input.credentialAvailable!==void 0?{credentialAvailable:input.credentialAvailable}:{},...input.value!==void 0?{value:input.value}:{}}))}function inputReferenceFromManifest(channelId,input,persisted){return{channelId,inputId:input.id,kind:input.kind,required:input.required,...input.envKey?{sourceEnv:input.envKey}:{},...input.kind==="config"&&input.statePath?{statePath:input.statePath}:{},...persisted?.credentialAvailable!==void 0?{credentialAvailable:persisted.credentialAvailable}:{},...persisted?.value!==void 0?{value:persisted.value}:{}}}function normalizeUnknownInput(channelId,input){const kind=input.kind==="secret"||input.kind==="config"?input.kind:"config";return{channelId,inputId:input.inputId,kind,required:input.required===true,...typeof input.sourceEnv==="string"?{sourceEnv:input.sourceEnv}:{},...typeof input.statePath==="string"?{statePath:input.statePath}:{},...input.credentialAvailable!==void 0?{credentialAvailable:input.credentialAvailable}:{},...input.value!==void 0?{value:input.value}:{}}}function requiredInputsAvailable(manifest,inputs){if(!manifest)return true;return manifest.inputs.every(manifestInput=>{if(!manifestInput.required)return true;const input=inputs.find(entry=>entry.inputId===manifestInput.id);if(!input)return false;if(input.kind==="secret")return input.credentialAvailable===true;if(input.value===void 0)return false;return typeof input.value==="string"?input.value.trim().length>0:true})}function normalizePersistedCredentialBindings(plan,channels,manifestRegistry,environment){const persisted=plan.credentialBindings??[];if(Array.isArray(plan.credentialBindings)&&plan.channels.every(hasFullChannelShape)&&persisted.every(hasFullPersistedCredentialBindingShape)){return normalizeFullPersistedCredentialBindings(persisted)}const manifests=channels.flatMap(channel=>{const manifest=manifestRegistry.get(channel.channelId);return manifest?[manifest]:[]});const planForBindings={...plan,channels,credentialBindings:[],networkPolicy:{presets:[],entries:[]},agentRender:[],buildSteps:[],runtimeSetup:{nodePreloads:[],envAliases:[],secretScans:[]},stateUpdates:[],healthChecks:[]};const generated=credentialBindingsFromManifests(planForBindings,manifests,new Map(channels.map(channel=>[channel.channelId,channel.inputs])),environment);return generated.map(binding=>overlayPersistedCredentialBinding(binding,persisted))}function credentialBindingsFromManifests(plan,manifests,inputRegistry,environment){const context=compilerContext(plan);return manifests.flatMap(manifest=>planCredentialBindings(manifest,context,inputRegistry.get(manifest.id)??[],environment).map(binding=>overlayPersistedCredentialBinding(binding,plan.credentialBindings)))}function overlayPersistedCredentialBinding(binding,persisted){const match=persisted.find(candidate=>credentialBindingMatches(binding,candidate));if(!match)return binding;return{...binding,credentialAvailable:typeof match.credentialAvailable==="boolean"?match.credentialAvailable:binding.credentialAvailable,...typeof match.credentialHash==="string"&&match.credentialHash.length>0?{credentialHash:match.credentialHash}:binding.credentialHash?{credentialHash:binding.credentialHash}:{}}}function credentialBindingMatches(binding,candidate){if(candidate.channelId&&candidate.channelId!==binding.channelId)return false;if(candidate.providerEnvKey&&candidate.providerEnvKey===binding.providerEnvKey)return true;if(candidate.credentialId&&candidate.credentialId===binding.credentialId)return true;if(candidate.sourceInput&&candidate.sourceInput===binding.sourceInput)return true;return false}function hasFullChannelShape(channel){return typeof channel.displayName==="string"&&typeof channel.authMode==="string"&&typeof channel.active==="boolean"&&typeof channel.selected==="boolean"&&typeof channel.configured==="boolean"&&typeof channel.disabled==="boolean"&&Array.isArray(channel.inputs)}function normalizeRuntimeSetup(setup){return{nodePreloads:Array.isArray(setup?.nodePreloads)?[...setup.nodePreloads]:[],envAliases:Array.isArray(setup?.envAliases)?[...setup.envAliases]:[],secretScans:Array.isArray(setup?.secretScans)?[...setup.secretScans]:[]}}function compilerContext(plan){return{sandboxName:plan.sandboxName,agent:plan.agent,workflow:plan.workflow,isInteractive:false,configuredChannels:plan.channels.map(channel=>channel.channelId),disabledChannels:plan.disabledChannels,credentialAvailability:credentialAvailabilityFromPlan(plan)}}function credentialAvailabilityFromPlan(plan){const availability={};for(const channel of plan.channels){for(const input of channel.inputs){if(input.kind!=="secret"||input.credentialAvailable!==true)continue;availability[`${channel.channelId}.${input.inputId}`]=true;if(input.sourceEnv)availability[input.sourceEnv]=true}}for(const credential of plan.credentialBindings){if(!credential.credentialAvailable)continue;availability[credential.credentialId]=true;availability[`${credential.channelId}.${credential.credentialId}`]=true;availability[`${credential.channelId}.${credential.sourceInput}`]=true;availability[credential.providerEnvKey]=true}return availability}function normalizeMessagingChannelId(channelId){return channelId.trim().toLowerCase()}function enabledPlanChannels(plan){const disabled=new Set((plan.disabledChannels??[]).map(normalizeMessagingChannelId).filter(Boolean));return plan.channels.filter(channel=>{const channelId=normalizeMessagingChannelId(channel.channelId);return channelId.length>0&&channel.active&&!channel.disabled&&!disabled.has(channelId)})}function selectActiveMessagingChannelIds(plan){const seen=new Set;const channels=[];for(const item of enabledPlanChannels(plan)){const channel=normalizeMessagingChannelId(item.channelId);if(!channel||seen.has(channel))continue;seen.add(channel);channels.push(channel)}return channels}function selectEnabledMessagingAgentRender(plan){const active=new Set(selectActiveMessagingChannelIds(plan));return plan.agentRender.filter(render=>render.agent===plan.agent&&active.has(normalizeMessagingChannelId(render.channelId)))}function selectEnabledPostAgentInstallBuildFiles(plan){const active=new Set(selectActiveMessagingChannelIds(plan));const channels=enabledPlanChannels(plan);return plan.buildSteps.filter(step=>{const channelId=normalizeMessagingChannelId(step.channelId);if(!active.has(channelId)||step.kind!=="build-file")return false;if(!step.hookId)return true;const matchingChannels=channels.filter(channel=>normalizeMessagingChannelId(channel.channelId)===channelId);if(matchingChannels.length!==1)return false;const matchedHook=matchingChannels[0]?.hooks?.find(hook=>hook.id===step.hookId);return matchedHook!==void 0&&matchedHook.phase==="post-agent-install"})}function parseSandboxMessagingPlan(value,options={}){if(!isObjectRecord(value)||value.schemaVersion!==1||typeof value.sandboxName!=="string"||typeof value.agent!=="string"||typeof value.workflow!=="string"||!Array.isArray(value.channels)||!Array.isArray(value.disabledChannels)||!isOptionalObjectArray(value,"credentialBindings")||Object.hasOwn(value,"networkPolicy")&&!isObjectRecord(value.networkPolicy)||!isOptionalObjectArray(value,"agentRender")||!isOptionalObjectArray(value,"buildSteps")||!isRuntimeSetup(value.runtimeSetup)||!isOptionalObjectArray(value,"stateUpdates")||!isOptionalObjectArray(value,"healthChecks")){return null}if(options.sandboxName&&value.sandboxName!==options.sandboxName)return null;if(options.agent&&value.agent!==options.agent)return null;const supported=Array.isArray(options.supportedChannelIds)?new Set(options.supportedChannelIds):null;const normalizedChannelIds=new Set;for(const channel of value.channels){if(!isObjectRecord(channel)||typeof channel.channelId!=="string")return null;const normalizedChannelId=normalizeMessagingChannelId(channel.channelId);if(!normalizedChannelId||normalizedChannelId!==channel.channelId||normalizedChannelIds.has(normalizedChannelId)){return null}if(Object.hasOwn(channel,"configured")&&typeof channel.configured!=="boolean"){return null}if(Object.hasOwn(channel,"active")&&typeof channel.active!=="boolean")return null;if(Object.hasOwn(channel,"disabled")&&typeof channel.disabled!=="boolean")return null;if(Object.hasOwn(channel,"pendingRemoval")&&typeof channel.pendingRemoval!=="boolean"){return null}if(Object.hasOwn(channel,"inputs")&&!Array.isArray(channel.inputs))return null;if(Object.hasOwn(channel,"hostForward")&&!isHostForward(channel.hostForward))return null;if(Object.hasOwn(channel,"hooks")&&!Array.isArray(channel.hooks))return null;if(Array.isArray(channel.inputs)&&channel.inputs.some(input=>!isObjectRecord(input)||typeof input.inputId!=="string"||Object.hasOwn(input,"channelId")&&input.channelId!==normalizedChannelId)){return null}if(Array.isArray(channel.hooks)&&channel.hooks.some(hook=>!isObjectRecord(hook)||Object.hasOwn(hook,"channelId")&&hook.channelId!==normalizedChannelId)){return null}if(Object.hasOwn(channel,"hostForward")&&isObjectRecord(channel.hostForward)&&channel.hostForward.channelId!==normalizedChannelId){return null}if(supported&&!supported.has(channel.channelId))return null;normalizedChannelIds.add(normalizedChannelId)}if(!value.disabledChannels.every(isCanonicalMessagingChannelId))return null;const disabledChannelIds=new Set(value.disabledChannels);if(disabledChannelIds.size!==value.disabledChannels.length||[...disabledChannelIds].some(channelId=>!normalizedChannelIds.has(channelId))||value.channels.some(channel=>isObjectRecord(channel)&&channel.disabled===true!==disabledChannelIds.has(String(channel.channelId)))){return null}if(!hasCanonicalChannelReferences(value.credentialBindings)||!hasMatchingAgentRenderEntries(value.agentRender,value.agent)||!hasCanonicalChannelReferences(value.agentRender)||!hasCanonicalChannelReferences(value.buildSteps)||!hasCanonicalChannelReferences(value.stateUpdates)||!hasCanonicalChannelReferences(value.healthChecks)||!hasCanonicalNetworkPolicyReferences(value.networkPolicy)||!hasCanonicalRuntimeSetupReferences(value.runtimeSetup)){return null}return cloneSandboxMessagingPlan(normalizePersistedSandboxMessagingPlanShape(value,options.environment))}function hasMatchingAgentRenderEntries(value,agent){return!Array.isArray(value)||value.every(render=>isObjectRecord(render)&&render.agent===agent)}function hasCanonicalNetworkPolicyReferences(value){if(!isObjectRecord(value)||!Object.hasOwn(value,"entries"))return true;return hasCanonicalChannelReferences(value.entries)}function cloneSandboxMessagingPlan(plan){return JSON.parse(JSON.stringify(plan))}function isOptionalObjectArray(value,key){if(!Object.hasOwn(value,key))return true;const entries=value[key];return Array.isArray(entries)&&entries.every(isObjectRecord)}function isHostForward(value){return isObjectRecord(value)&&typeof value.channelId==="string"&&typeof value.port==="number"&&Number.isInteger(value.port)&&value.port>=1&&value.port<=65535&&typeof value.label==="string"}function isRuntimeSetup(value){if(value===void 0)return true;return isObjectRecord(value)&&Array.isArray(value.nodePreloads)&&Array.isArray(value.envAliases)&&Array.isArray(value.secretScans)&&value.nodePreloads.every(isObjectRecord)&&value.envAliases.every(isObjectRecord)&&value.secretScans.every(isObjectRecord)}function isCanonicalMessagingChannelId(value){return typeof value==="string"&&value.length>0&&normalizeMessagingChannelId(value)===value}function hasCanonicalChannelReferences(value){return value===void 0||Array.isArray(value)&&value.every(entry=>isObjectRecord(entry)&&isCanonicalMessagingChannelId(entry.channelId))}function hasCanonicalRuntimeSetupReferences(value){if(value===void 0)return true;if(!isObjectRecord(value))return false;return["nodePreloads","envAliases","secretScans"].every(field=>hasCanonicalChannelReferences(value[field]))}var import_node_buffer=require("node:buffer");var import_node_crypto2=require("node:crypto");var import_node_util=require("node:util");function isLoopbackHostname(hostname=""){const normalized=String(hostname||"").trim().toLowerCase().replace(/^\[|\]$/g,"");return normalized==="localhost"||normalized==="::1"||/^127(?:\.\d{1,3}){3}$/.test(normalized)}function isLoopbackDashboardUrl(value){return isLoopbackHostname(new URL(value).hostname)}function listMessagingCredentialEnvAssignments(options={}){return selectManifests(options).flatMap(manifest=>{const credentialsByTemplate=new Map(manifest.credentials.map(credential=>[`{{credential.${credential.id}.placeholder}}`,credential]));const renderedAssignments=manifest.render.flatMap(render=>{if(options.agent&&render.agent!==options.agent)return[];if(render.kind!=="env-lines")return[];return render.lines.flatMap(line=>{const separator=line.indexOf("=");if(separator<=0)return[];const credential=credentialsByTemplate.get(line.slice(separator+1));if(!credential)return[];return[{channelId:manifest.id,agent:render.agent,sourceEnvKey:credential.providerEnvKey,targetEnvKey:line.slice(0,separator),placeholder:credential.placeholder}]})});const runtimeAssignments=["openclaw","hermes"].flatMap(agent=>{if(options.agent&&agent!==options.agent)return[];if(!manifest.supportedAgents.includes(agent))return[];return(manifest.runtime?.[agent]?.envAliases??[]).flatMap(alias=>{if(!alias.targetEnvKey)return[];const credential=manifest.credentials.find(candidate=>candidate.providerEnvKey===alias.envKey);if(!credential)return[];return[{channelId:manifest.id,agent,sourceEnvKey:alias.envKey,targetEnvKey:alias.targetEnvKey,placeholder:credential.placeholder}]})});return[...renderedAssignments,...runtimeAssignments]})}function selectManifests(options){const manifests=options.manifests??BUILT_IN_CHANNEL_MANIFESTS;const agent=options.agent;const selected=agent?manifests.filter(manifest=>manifest.supportedAgents.includes(agent)):manifests;return[...selected]}function authorizeMessagingManagedStartupFields(entry,section){if(section==="agentRender")return authorizeTeamsOpenClawWebhookField(entry);if(!isPlainDataObject3(entry))return[];const contract=WECHAT_OPENCLAW_ACCOUNT_FILE_CONTRACT;if(ownDataPropertyValue3(entry,"channelId")!==contract.channelId||ownDataPropertyValue3(entry,"hookId")!==contract.planHookId||ownDataPropertyValue3(entry,"handler")!==contract.handlerId||ownDataPropertyValue3(entry,"outputId")!==contract.outputId||ownDataPropertyValue3(entry,"kind")!==contract.kind||ownDataPropertyValue3(entry,"required")!==contract.required){return[]}return authorizeWechatAccountFilePlaceholders(ownDataPropertyValue3(entry,"value")).map(authorization=>({...authorization,path:["value",...authorization.path]}))}function isPlainDataObject3(value){if(value===null||typeof value!=="object"||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function ownDataPropertyValue3(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}var DCODE_UPSTREAM_PROVIDER_RE=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;function isValidDcodeUpstreamProvider(value){return DCODE_UPSTREAM_PROVIDER_RE.test(value)}var LEGACY_MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=1;var MANAGED_STARTUP_PROFILE_SCHEMA_VERSION=2;var MANAGED_STARTUP_PROFILE_MAX_BYTES=64*1024;var MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES=Math.ceil(MANAGED_STARTUP_PROFILE_MAX_BYTES/3)*4;var MAX_IDENTIFIER_BYTES=256;var MAX_MODEL_BYTES=1024;var MAX_URL_BYTES=2048;var MAX_LIST_ITEMS=128;var MAX_JSON_NODES=4096;var MAX_JSON_DEPTH=32;var MAX_TUNING_INTEGER=1e9;var MIN_HERMES_CONTEXT_WINDOW=64e3;var SHA256_RE=/^[a-f0-9]{64}$/;var CONTROL_CHARACTER_RE=/[\u0000-\u001f\u007f-\u009f]/u;var BASE64URL_RE=/^[A-Za-z0-9_-]+$/;var RAW_CA_PEM_RE=/-----BEGIN (?:TRUSTED )?CERTIFICATE-----/iu;var RAW_CA_PEM_BASE64_RE=/^LS0tLS1CRUdJTi(?:BDRVJUSUZJQ0FURS0tLS0t|BUlVTVEVEIENFUlRJRklDQVRFLS0tLS0)/u;var RAW_CA_DER_BASE64_RE=/^MII[A-Za-z0-9+/=\r\n]{253,}$/u;var RAW_CA_DATA_URI_RE=/data:application\/(?:pkix-cert|x-x509-ca-cert);base64,MII[A-Za-z0-9+/=]{253,}/iu;var URL_CANDIDATE_RE=/[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'<>]+/gu;var UTF8_DECODER=new import_node_util.TextDecoder("utf-8",{fatal:true});var CREDENTIAL_SHAPED_NAME_PATTERN=/(?:^|[_-])(?:api[_-]?key|access[_-]?key|secret[_-]?key|auth[_-]?token|refresh[_-]?token|access[_-]?token|client[_-]?secret|private[_-]?key|pass[_-]?code|personal[_-]?access[_-]?token|connection[_-]?string|webhook(?:[_-]?url)?|key|secret|token|password|passwd|passcode|auth|authorization|credential|credentials|bearer|bearer[_-]?token|cookie|cookies|pat|private|privatekey|pin|webhookurl|dsn|connectionstring)(?:$|[_-])/iu;var CREDENTIAL_COMPOUND_NAME_PATTERN=/^(?:access|refresh|client|bearer|auth|api|private|signing|session|bot|app|resolved)(?:token|key|secret|password)$/iu;var CREDENTIAL_CAMEL_SUFFIX_PATTERN=/(?:apiKey|accessKey|secretKey|authToken|refreshToken|accessToken|clientSecret|privateKey|passcode|password|passwd|passphrase|bearerToken|botToken|appToken|sessionToken|signingKey|secretPublicKey|personalAccessToken|connectionString|webhookUrl)$/iu;var CREDENTIAL_CAMEL_BOUNDARY_PATTERN=/[a-z0-9](?:Token|Key|Secret|Password|Passphrase|Pat)$/u;var CREDENTIAL_ENV_NAME_PATTERN=/^(?:[A-Z0-9]+_)*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD|PASS|PASSPHRASE|CREDENTIAL)S?$/u;var CREDENTIAL_HEADER_NAME_PATTERN=/^(?:authorization|proxy-authorization|cookie|set-cookie|.+-(?:key|token|secret|password|passphrase|credential|auth)s?)$/iu;var PUBLIC_KEY_NAME_PATTERN=/^public[-_]?keys?$/iu;var PASS_CREDENTIAL_NAME_PATTERN=/(?:^|[-_])pass(?:wd)?$/iu;var NON_SECRET_KEY_METADATA_NAMES=new Set(["envKey","installCacheEnvKey","providerEnvKey","stateKey","targetEnvKey"]);var MESSAGING_CREDENTIAL_PLACEHOLDER_RE=/^(?:openshell:resolve:env:|[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-)(?:(?:v[0-9]{1,20}|s[a-f0-9]{64})_)?[A-Z][A-Z0-9_]*$/u;var MESSAGING_CREDENTIAL_ENV_ALIASES=new Set(listMessagingCredentialEnvAssignments().filter(({sourceEnvKey,targetEnvKey})=>sourceEnvKey!==targetEnvKey).map(({agent,sourceEnvKey,targetEnvKey})=>`${agent}\0${sourceEnvKey}\0${targetEnvKey}`));var MESSAGING_CREDENTIAL_RUNTIME_ENV_ALIASES=new Set(listMessagingCredentialEnvAssignments().filter(({sourceEnvKey,targetEnvKey})=>sourceEnvKey!==targetEnvKey).map(({agent,channelId,sourceEnvKey,targetEnvKey})=>`${agent}\0${channelId}\0${sourceEnvKey}\0${targetEnvKey}`));var JSON_ARRAY_INDEX_SEGMENT_RE=/^\[(?:0|[1-9][0-9]*)\]$/u;var SECRET_VALUE_PATTERNS=[/nvapi-[A-Za-z0-9_-]{10,}/u,/nvcf-[A-Za-z0-9_-]{10,}/u,/ghp_[A-Za-z0-9_-]{10,}/u,/github_pat_[A-Za-z0-9_]{30,}/u,/sk-(?:proj-|ant-)?[A-Za-z0-9_-]{10,}/u,/(?:xox[bpas]|xapp)-[A-Za-z0-9-]{10,}/u,/A(?:K|S)IA[A-Z0-9]{16}/u,/hf_[A-Za-z0-9]{10,}/u,/glpat-[A-Za-z0-9_-]{10,}/u,/gsk_[A-Za-z0-9]{10,}/u,/pypi-[A-Za-z0-9_-]{10,}/u,/tvly-[A-Za-z0-9_-]{10,}/u,/lsv2_(?:pt|sk)_[A-Za-z0-9]{10,}(?:_[A-Za-z0-9]+)*/u,/\bbot\d{8,10}:[A-Za-z0-9_-]{35}\b/u,/\b\d{8,10}:[A-Za-z0-9_-]{35}\b/u,/\b[A-Za-z0-9]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/u,/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{10,}\b/u,/\bBearer\s+[A-Za-z0-9_.+/=-]{10,}/iu,/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----/u];var MANAGED_STARTUP_INFERENCE_APIS=["openai-completions","openai-responses","anthropic-messages"];var MANAGED_STARTUP_REASONING_EFFORTS=["default","low","medium","high"];var MANAGED_STARTUP_DCODE_AUTO_APPROVAL_MODES=["disabled","thread-opt-in"];var MANAGED_STARTUP_HERMES_TOOL_GATEWAYS=["nous-web","nous-image","nous-audio","nous-browser","nous-code"];var MANAGED_STARTUP_AGENTS=["openclaw","hermes","langchain-deepagents-code","pi"];var MANAGED_STARTUP_MESSAGING_AGENTS=["openclaw","hermes"];function freezeAgentCapabilities(capabilities){return Object.freeze({...capabilities,inferenceApis:Object.freeze([...capabilities.inferenceApis]),dashboardModes:Object.freeze([...capabilities.dashboardModes]),inputModalities:Object.freeze([...capabilities.inputModalities]),webSearchProviders:Object.freeze([...capabilities.webSearchProviders]),toolGateways:Object.freeze([...capabilities.toolGateways]),tuningFields:Object.freeze([...capabilities.tuningFields])})}var PROFILE_CAPABILITIES={openclaw:{inferenceApis:[...MANAGED_STARTUP_INFERENCE_APIS],dashboardModes:["loopback","remote"],inputModalities:["text","image"],webSearchProviders:["brave","tavily"],toolGateways:[],tuningFields:["contextWindow","maxTokens","reasoning","reasoningEffort"],supportsMessaging:true,supportsInferenceCompatibility:true,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:true,supportsAgentTimeout:true,supportsHeartbeat:true,supportsExtraAgents:true,supportsDeviceAuth:false,observability:"openclaw-otel",supportsMinimalBootstrap:true},hermes:{inferenceApis:[...MANAGED_STARTUP_INFERENCE_APIS],dashboardModes:["disabled","loopback-forwarded"],inputModalities:[],webSearchProviders:["tavily"],toolGateways:[...MANAGED_STARTUP_HERMES_TOOL_GATEWAYS],tuningFields:["contextWindow"],supportsMessaging:true,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"none",supportsMinimalBootstrap:false},"langchain-deepagents-code":{inferenceApis:["openai-completions"],dashboardModes:["disabled"],inputModalities:[],webSearchProviders:[],toolGateways:[],tuningFields:["reasoningEffort"],supportsMessaging:false,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:true,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"dcode-marker",supportsMinimalBootstrap:false},pi:{inferenceApis:["openai-completions"],dashboardModes:["disabled"],inputModalities:[],webSearchProviders:[],toolGateways:[],tuningFields:["contextWindow","maxTokens","reasoning"],supportsMessaging:false,supportsInferenceCompatibility:false,supportsUpstreamEndpoint:false,supportsHostProxyIntent:true,supportsPrimaryModelRef:false,supportsAgentTimeout:false,supportsHeartbeat:false,supportsExtraAgents:false,supportsDeviceAuth:false,observability:"none",supportsMinimalBootstrap:false}};for(const agent of MANAGED_STARTUP_AGENTS){Object.defineProperty(PROFILE_CAPABILITIES,agent,{configurable:false,enumerable:true,value:freezeAgentCapabilities(PROFILE_CAPABILITIES[agent]),writable:false})}var MANAGED_STARTUP_PROFILE_CAPABILITIES=Object.freeze(PROFILE_CAPABILITIES);function affordance(input,profilePath,source="docker-arg",representation="value"){return{input,profilePath,source,representation}}var HOST_PROXY_AFFORDANCES=[affordance("HTTP_PROXY","proxy.hostHttpUrl","runtime-env"),affordance("http_proxy","proxy.hostHttpUrl","runtime-env","derived"),affordance("HTTPS_PROXY","proxy.hostHttpsUrl","runtime-env"),affordance("https_proxy","proxy.hostHttpsUrl","runtime-env","derived"),affordance("NO_PROXY","proxy.hostNoProxy","runtime-env"),affordance("no_proxy","proxy.hostNoProxy","runtime-env","derived")];var MANAGED_STARTUP_PROFILE_AFFORDANCE_INVENTORY={openclaw:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_SERVING_PRESET","inference.servingPreset"),affordance("NEMOCLAW_PRIMARY_MODEL_REF","inference.primaryModelRef"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_INFERENCE_COMPAT_B64","inference.compatibility"),affordance("NEMOCLAW_INFERENCE_INPUTS","inference.inputModalities"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_MAX_TOKENS","tuning.maxTokens"),affordance("NEMOCLAW_REASONING","tuning.reasoning"),affordance("NEMOCLAW_REASONING_EFFORT","tuning.reasoningEffort"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_AGENT_TIMEOUT","agentConfig.agentTimeoutSeconds"),affordance("NEMOCLAW_AGENT_HEARTBEAT_EVERY","agentConfig.heartbeatEvery"),affordance("NEMOCLAW_EXTRA_AGENTS_JSON_B64","agentConfig.extraAgents"),affordance("NEMOCLAW_WEB_SEARCH_ENABLED","agentConfig.webSearch.enabled"),affordance("NEMOCLAW_WEB_SEARCH_PROVIDER","agentConfig.webSearch.provider"),affordance("NEMOCLAW_OPENCLAW_OTEL","agentConfig.otel.enabled"),affordance("NEMOCLAW_OPENCLAW_OTEL_ENDPOINT","agentConfig.otel.endpointUrl"),affordance("NEMOCLAW_OPENCLAW_OTEL_SERVICE_NAME","agentConfig.otel.serviceName"),affordance("NEMOCLAW_OPENCLAW_OTEL_SAMPLE_RATE","agentConfig.otel.sampleRate"),affordance("CHAT_UI_URL","dashboard.url"),affordance("NEMOCLAW_DASHBOARD_BIND","dashboard.bindAddress"),affordance("NEMOCLAW_WSL_DASHBOARD_EXPOSURE","dashboard.wslExposure"),affordance("NEMOCLAW_DASHBOARD_PORT","dashboard.port","runtime-env"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_MESSAGING_PLAN_B64","messaging.plan"),affordance("NEMOCLAW_MINIMAL_BOOTSTRAP","agentConfig.minimalBootstrap","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],hermes:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER","tools.enabledGateways","docker-arg","derived"),affordance("NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64","tools.enabledGateways"),affordance("NEMOCLAW_WEB_SEARCH_ENABLED","agentConfig.webSearch.enabled"),affordance("NEMOCLAW_WEB_SEARCH_PROVIDER","agentConfig.webSearch.provider"),affordance("NEMOCLAW_MESSAGING_PLAN_B64","messaging.plan"),affordance("CHAT_UI_URL","dashboard.browserUrl"),affordance("NEMOCLAW_DASHBOARD_PORT","dashboard.publicPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD","dashboard.mode","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_PORT","dashboard.publicPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_INTERNAL_PORT","dashboard.internalPort","runtime-env"),affordance("NEMOCLAW_HERMES_DASHBOARD_TUI","dashboard.tuiEnabled","runtime-env"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost","runtime-env"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],"langchain-deepagents-code":[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_UPSTREAM_ENDPOINT_URL","inference.upstreamEndpointUrl"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_REASONING_EFFORT","tuning.reasoningEffort"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_DCODE_AUTO_APPROVAL","agentConfig.autoApprovalMode"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_OBSERVABILITY","agentConfig.observabilityEnabled","runtime-env"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES],pi:[affordance("NEMOCLAW_MODEL","inference.model"),affordance("NEMOCLAW_INFERENCE_PROVIDER_ID","inference.routeProvider"),affordance("NEMOCLAW_UPSTREAM_PROVIDER","inference.upstreamProvider"),affordance("NEMOCLAW_INFERENCE_BASE_URL","inference.routedBaseUrl"),affordance("NEMOCLAW_INFERENCE_API","inference.api"),affordance("NEMOCLAW_CONTEXT_WINDOW","tuning.contextWindow"),affordance("NEMOCLAW_MAX_TOKENS","tuning.maxTokens"),affordance("NEMOCLAW_REASONING","tuning.reasoning"),affordance("NEMOCLAW_TOOL_DISCLOSURE","tools.disclosure"),affordance("NEMOCLAW_PROXY_HOST","proxy.managedHost"),affordance("NEMOCLAW_PROXY_PORT","proxy.managedPort"),affordance("NEMOCLAW_CORPORATE_CA_B64","corporateCa.bundleSha256","host-material","digest-handoff"),...HOST_PROXY_AFFORDANCES]};function deferredRuntimeInput(input,owner,reason,admission="managed-launch-forwarded"){return Object.freeze({input,owner,admission,reason})}var MANAGED_STARTUP_PROFILE_DEFERRED_RUNTIME_INPUTS=Object.freeze({openclaw:Object.freeze([deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_DEADLINE_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS","application-environment","operator scheduler tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_MCP_SHADOW_DIAGNOSTICS","application-environment","operator shadow-diagnostics tuning is applied by the application environment transaction"),deferredRuntimeInput("NEMOCLAW_MCP_TOOLS_LIST_TIMEOUT_MS","application-environment","operator MCP discovery timeout tuning is applied by the application environment transaction"),deferredRuntimeInput("OPENCLAW_HOME","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("OPENCLAW_STATE_DIR","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("OPENCLAW_WORKSPACE_DIR","fixed-image-contract","the managed image and agent definition own this fixed runtime layout path"),deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),hermes:Object.freeze([deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),"langchain-deepagents-code":Object.freeze([deferredRuntimeInput("NEMOCLAW_SANDBOX_NAME","engine-identity","the lifecycle engine owns instance identity outside reusable startup intent"),deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")]),pi:Object.freeze([deferredRuntimeInput("NEMOCLAW_EXTRA_PLACEHOLDER_KEYS","credential-plumbing","credential provider construction owns key metadata outside the secret-free profile")])});function runtimeCleanupObligation(input,emittedFor,supportedFor,reason){return Object.freeze({input,emittedFor:Object.freeze([...emittedFor]),supportedFor:Object.freeze([...supportedFor]),owner:"application-environment",reason})}var MANAGED_STARTUP_RUNTIME_CLEANUP_OBLIGATIONS=Object.freeze([runtimeCleanupObligation("NEMOCLAW_DASHBOARD_BIND",["hermes"],["openclaw"],"generic managed-dashboard construction currently emits the OpenClaw-only bind control for Hermes"),runtimeCleanupObligation("NEMOCLAW_MINIMAL_BOOTSTRAP",["hermes","langchain-deepagents-code"],["openclaw"],"generic host-proxy construction currently emits the OpenClaw-only bootstrap control for other agents")]);var ManagedStartupProfileError=class extends Error{constructor(message){super(`Invalid managed startup profile: ${message}`);this.name="ManagedStartupProfileError"}};var PROFILE_KEYS=new Set(["schemaVersion","agent","agentConfig","inference","proxy","dashboard","tools","messaging","tuning","corporateCa"]);var INFERENCE_KEYS=new Set(["routeProvider","upstreamProvider","servingPreset","model","routedBaseUrl","upstreamEndpointUrl","api","primaryModelRef","compatibility","inputModalities"]);var PROXY_KEYS=new Set(["managedHost","managedPort","hostHttpUrl","hostHttpsUrl","hostNoProxy"]);var OPENCLAW_DASHBOARD_KEYS=new Set(["agent","mode","url","port","bindAddress","wslExposure"]);var HERMES_DASHBOARD_KEYS=new Set(["agent","mode","url","browserUrl","publicPort","internalPort","tuiEnabled"]);var DCODE_DASHBOARD_KEYS=new Set(["agent","mode"]);var TOOLS_KEYS=new Set(["disclosure","enabledGateways"]);var MESSAGING_KEYS=new Set(["plan"]);var TUNING_FIELD_ORDER=["contextWindow","maxTokens","reasoning","reasoningEffort"];var TUNING_KEYS=new Set(TUNING_FIELD_ORDER);var CORPORATE_CA_KEYS=new Set(["bundleSha256"]);var OPENCLAW_CONFIG_KEYS=new Set(["agent","webSearch","otel","agentTimeoutSeconds","heartbeatEvery","extraAgents","minimalBootstrap"]);var LEGACY_OPENCLAW_CONFIG_KEYS=new Set([...OPENCLAW_CONFIG_KEYS,"deviceAuth"]);var LEGACY_DEVICE_AUTH_KEYS=new Set(["disabled","optOutSource"]);var LEGACY_DEVICE_AUTH_OPT_OUT_SOURCES=new Set(["operator","managed-onboard"]);var HERMES_CONFIG_KEYS=new Set(["agent","webSearch"]);var DCODE_CONFIG_KEYS=new Set(["agent","autoApprovalMode","observabilityEnabled"]);var PI_CONFIG_KEYS=new Set(["agent"]);var PI_DASHBOARD_KEYS=new Set(["agent","mode"]);var WEB_SEARCH_KEYS=new Set(["enabled","provider"]);var OTEL_KEYS=new Set(["enabled","endpointUrl","serviceName","sampleRate"]);var EXTRA_AGENTS_KEYS=new Set(["agents","defaults","main"]);var MANAGED_STARTUP_AGENT_SET=new Set(MANAGED_STARTUP_AGENTS);var DCODE_AUTO_APPROVAL_MODE_SET=new Set(MANAGED_STARTUP_DCODE_AUTO_APPROVAL_MODES);var REASONING_EFFORT_SET=new Set(MANAGED_STARTUP_REASONING_EFFORTS);var HERMES_INTERNAL_API_PORT=18642;var HERMES_API_PORT_RANGE_START=8642;var HERMES_API_PORT_RANGE_END=8652;function isHermesApiPort(port){return port>=HERMES_API_PORT_RANGE_START&&port<=HERMES_API_PORT_RANGE_END}function isHermesReservedApiPort(port){return port===HERMES_INTERNAL_API_PORT||isHermesApiPort(port)}var HERMES_RESERVED_API_PORT_LABEL=`${HERMES_API_PORT_RANGE_START}-${HERMES_API_PORT_RANGE_END} or ${HERMES_INTERNAL_API_PORT}`;function isPlainObject(value){if(typeof value!=="object"||value===null||Array.isArray(value))return false;const prototype=Object.getPrototypeOf(value);return prototype===Object.prototype||prototype===null}function isCredentialShapedName(name){if(PUBLIC_KEY_NAME_PATTERN.test(name)||NON_SECRET_KEY_METADATA_NAMES.has(name))return false;return CREDENTIAL_SHAPED_NAME_PATTERN.test(name)||CREDENTIAL_COMPOUND_NAME_PATTERN.test(name)||CREDENTIAL_CAMEL_SUFFIX_PATTERN.test(name)||CREDENTIAL_CAMEL_BOUNDARY_PATTERN.test(name)||CREDENTIAL_ENV_NAME_PATTERN.test(name)||CREDENTIAL_HEADER_NAME_PATTERN.test(name)||PASS_CREDENTIAL_NAME_PATTERN.test(name)}function valueLooksLikeSecret(value){for(let index=0;index=5&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="agentRender"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")&&path4[4]==="value";const isAuthorizedBuildStepPlaceholder=allowedBuildStepPlaceholders.has(buildStepPlaceholderKey(path4,value));return isCredentialBindingPlaceholder||isAgentRenderValuePlaceholder||isAuthorizedBuildStepPlaceholder}function requiresMessagingSchemaFieldAuthorization(path4){const fieldName=path4[path4.length-1];return fieldName==="webhook"}function messagingAuthorizedFieldKey(path4){return JSON.stringify(path4)}function buildStepPlaceholderKey(path4,value){return JSON.stringify([path4,value])}function messagingCredentialPlaceholderEnvKey(value){if(!MESSAGING_CREDENTIAL_PLACEHOLDER_RE.test(value))return null;const marker=value.startsWith("openshell:resolve:env:")?"openshell:resolve:env:":"-OPENSHELL-RESOLVE-ENV-";const key=value.slice(value.indexOf(marker)+marker.length);return key.replace(/^(?:v[0-9]{1,20}|s[a-f0-9]{64})_/u,"")}function containsMessagingCredentialPlaceholder(value){return value.includes("openshell:resolve:env:")||value.includes("-OPENSHELL-RESOLVE-ENV-")}function isMessagingCredentialPlaceholderAssignment(selectedAgent,path4,value){if(path4.length!==6||path4[0]!=="messaging"||path4[1]!=="plan"||path4[2]!=="agentRender"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")||path4[4]!=="lines"||!JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[5]??"")){return false}const separator=value.indexOf("=");if(separator<=0||value.indexOf("=",separator+1)!==-1)return false;const envKey=value.slice(0,separator);const placeholder=value.slice(separator+1);const placeholderEnvKey=messagingCredentialPlaceholderEnvKey(placeholder);return CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&placeholderEnvKey!==null&&(envKey===placeholderEnvKey||typeof selectedAgent==="string"&&MESSAGING_CREDENTIAL_ENV_ALIASES.has(`${selectedAgent}\0${placeholderEnvKey}\0${envKey}`))}function isMessagingRuntimeEnvAliasPath(path4){return path4.length===5&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="runtimeSetup"&&path4[3]==="envAliases"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[4]??"")}function ownDataPropertyValue4(value,key){const descriptor=Object.getOwnPropertyDescriptor(value,key);return descriptor&&"value"in descriptor?descriptor.value:void 0}function isCanonicalMessagingRuntimeEnvAlias(selectedAgent,path4,value){if(!isMessagingRuntimeEnvAliasPath(path4))return false;const channelId=ownDataPropertyValue4(value,"channelId");const envKey=ownDataPropertyValue4(value,"envKey");const targetEnvKey=ownDataPropertyValue4(value,"targetEnvKey");const match=ownDataPropertyValue4(value,"match");const placeholder=ownDataPropertyValue4(value,"value");const expectedMatch=targetEnvKey===void 0?`^openshell:resolve:env:((?:v[0-9]{1,20}|s[a-f0-9]{64})_)?${envKey}$`:`^openshell:resolve:env:(?:v[0-9]{1,20}|s[a-f0-9]{64})_${envKey}$`;return typeof envKey==="string"&&CREDENTIAL_ENV_NAME_PATTERN.test(envKey)&&match===expectedMatch&&typeof placeholder==="string"&&messagingCredentialPlaceholderEnvKey(placeholder)===envKey&&(targetEnvKey===void 0||typeof selectedAgent==="string"&&typeof channelId==="string"&&typeof targetEnvKey==="string"&&CREDENTIAL_ENV_NAME_PATTERN.test(targetEnvKey)&&MESSAGING_CREDENTIAL_RUNTIME_ENV_ALIASES.has(`${selectedAgent}\0${channelId}\0${envKey}\0${targetEnvKey}`))}function isAllowedMessagingRuntimeAliasStringPath(path4,allowedAliasIndexes){return path4.length===6&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="runtimeSetup"&&path4[3]==="envAliases"&&allowedAliasIndexes.has(path4[4]??"")&&(path4[5]==="match"||path4[5]==="value"||path4[5]==="targetEnvKey")}function isMessagingPackagePin(path4,value){return path4.length===6&&path4[0]==="messaging"&&path4[1]==="plan"&&path4[2]==="buildSteps"&&JSON_ARRAY_INDEX_SEGMENT_RE.test(path4[3]??"")&&path4[4]==="value"&&path4[5]==="pin"&&typeof value==="boolean"}function containsUrlWithCredentialMaterial(value){const candidates=value.match(URL_CANDIDATE_RE)??[];for(let index=0;index{if(isCredentialShapedName(key))credentialQuery=true});const fragment=url.hash.startsWith("#")?url.hash.slice(1):url.hash;const queryStart=fragment.indexOf("?");const fragmentParameters=new URLSearchParams(queryStart>=0?fragment.slice(queryStart+1):fragment);let credentialFragment=false;fragmentParameters.forEach((_fragmentValue,key)=>{if(isCredentialShapedName(key))credentialFragment=true});if(url.username||url.password||credentialQuery||credentialFragment)return true}catch{}}return false}function invalid(reason){throw new ManagedStartupProfileError(reason)}function payloadPath(path4){return path4.reduce((result,segment)=>segment.startsWith("[")?`${result}${segment}`:`${result}${result?".":""}${segment}`,"")}function mapArrayByIndex(values,mapper){const mapped=[];for(let index=0;index0&&values[insertion-1]>selected){Object.defineProperty(values,String(insertion),{configurable:true,enumerable:true,value:values[insertion-1],writable:true});insertion-=1}Object.defineProperty(values,String(insertion),{configurable:true,enumerable:true,value:selected,writable:true})}return values}function requireRecord(value,where){if(!isPlainObject(value))invalid(`${where} must be an object`);return value}function rejectUnknownKeys(value,allowed,where){const keys=Object.keys(value);for(let index=0;indexmaxBytes||CONTROL_CHARACTER_RE.test(value)){invalid(`${where} must be a bounded, non-empty string without control characters`)}return value}function requireStringEnum(value,allowed,where){const normalized=requireBoundedString(value,where);if(!allowed.has(normalized))invalid(`${where} is not supported`);return normalized}function requireNullablePositiveInteger(value,where){if(value===null)return null;if(typeof value!=="number"||!Number.isSafeInteger(value)||value<1||value>MAX_TUNING_INTEGER){invalid(`${where} must be null or a bounded positive integer`)}return value}function requirePositiveInteger(value,where,maximum=MAX_TUNING_INTEGER){if(typeof value!=="number"||!Number.isSafeInteger(value)||value<1||value>maximum){invalid(`${where} must be a bounded positive integer`)}return value}function requirePort(value,where,minimum=1){if(typeof value!=="number"||!Number.isInteger(value)||value<1||value>65535){invalid(`${where} must be a valid TCP port`)}if(valueMAX_LIST_ITEMS){invalid(`${where} must be a bounded string list`)}const items=mapArrayByIndex(value,item=>requireBoundedString(item,`${where} item`));const unique2=new Set;for(let index=0;index{if(depth>MAX_JSON_DEPTH)invalid(`${where} exceeds the JSON depth limit`);if(current===null||typeof current==="string"||typeof current==="boolean"){return current}if(typeof current==="number"){if(!Number.isFinite(current))invalid(`${where} contains a non-finite number`);return current}if(Array.isArray(current)){return mapArrayByIndex(current,item=>clone(item,depth+1))}if(!isPlainObject(current))invalid(`${where} contains a non-JSON value`);const result=options.nullPrototypeObjects?Object.create(null):{};const keys=Object.getOwnPropertyNames(current);for(let index=0;indexMAX_IDENTIFIER_BYTES||CONTROL_CHARACTER_RE.test(key)){invalid(`${where} contains an invalid object key`)}const descriptor=Object.getOwnPropertyDescriptor(current,key);if(!descriptor||!("value"in descriptor)){invalid(`${where} contains a non-JSON value`)}Object.defineProperty(result,key,{configurable:true,enumerable:true,value:clone(descriptor.value,depth+1),writable:true})}return result};return clone(value,0)}function requireJsonObjectOrNull(value,where){if(value===null)return null;if(!isPlainObject(value))invalid(`${where} must be null or a plain JSON object`);return cloneJsonValue(value,where,{nullPrototypeObjects:true})}function requireJsonObject(value,where){const object=requireJsonObjectOrNull(value,where);if(object===null)invalid(`${where} must be a plain JSON object`);return object}function requireHttpUrl(value,where){const raw=requireBoundedString(value,where,MAX_URL_BYTES);let parsed;try{parsed=new URL(raw)}catch{invalid(`${where} must be a valid HTTP(S) URL`)}if(parsed.protocol!=="http:"&&parsed.protocol!=="https:"||parsed.username||parsed.password||parsed.search||parsed.hash){invalid(`${where} must be a credential-free HTTP(S) URL without query or fragment data`)}const pathname=parsed.pathname.replace(/\/+$/u,"");return pathname===""?parsed.origin:`${parsed.origin}${pathname}`}function requireProxyUrl(value,allowedSchemes,where){if(value===null)return null;const raw=requireBoundedString(value,where,MAX_URL_BYTES);let parsed;try{parsed=new URL(raw)}catch{invalid(`${where} must be a valid HTTP(S) proxy URL`)}if(!allowedSchemes.has(parsed.protocol)||parsed.username||parsed.password||parsed.pathname!=="/"||parsed.search||parsed.hash){invalid(`${where} must be a credential-free HTTP(S) proxy origin`)}return parsed.origin}function requireManagedProxyHost(value,where){const host=requireBoundedString(value,where);if(!/^[A-Za-z0-9._-]+$/u.test(host)){invalid(`${where} must be a hostname or IPv4 address without a scheme or separators`)}return host}function configuredDashboardPort(value){const explicit=new URL(value).port;return explicit===""?18789:Number(explicit)}function requireSampleRate(value,where){if(typeof value!=="number"||!Number.isFinite(value)||value<0||value>1){invalid(`${where} must be a number between 0 and 1`)}return value}function assertPayloadStructureAndCredentialShapes(root){const pending=[{value:root,depth:0,path:[]}];const allowedRuntimeAliasIndexes=new Set;const allowedMessagingCredentialFields=new Set;const allowedBuildStepPlaceholders=new Set;const selectedAgent=isPlainObject(root)?ownDataPropertyValue4(root,"agent"):void 0;let discoveredNodes=1;let observedBytes=0;const observeText=value=>{observedBytes+=import_node_buffer.Buffer.byteLength(value,"utf8");if(observedBytes>MANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}};const reserveNode=depth=>{discoveredNodes+=1;if(discoveredNodes>MAX_JSON_NODES||depth>MAX_JSON_DEPTH){invalid("payload structure exceeds the complexity limit")}observedBytes+=1};while(pending.length>0){const current=pending.pop();if(!current)break;if(current.depth>MAX_JSON_DEPTH){invalid("payload structure exceeds the complexity limit")}if(typeof current.value==="string"){observeText(current.value);if(!isAllowedMessagingRuntimeAliasStringPath(current.path,allowedRuntimeAliasIndexes)&&!isMessagingCredentialPlaceholder(current.path,current.value,allowedBuildStepPlaceholders,allowedMessagingCredentialFields)&&!isMessagingCredentialPlaceholderAssignment(selectedAgent,current.path,current.value)&&(valueLooksLikeSecret(current.value)||containsMessagingCredentialPlaceholder(current.value))){invalid(`payload field ${payloadPath(current.path)} contains credential-shaped string data`)}if(RAW_CA_PEM_RE.test(current.value)||RAW_CA_PEM_BASE64_RE.test(current.value)||RAW_CA_DER_BASE64_RE.test(current.value)||RAW_CA_DATA_URI_RE.test(current.value)){invalid(`payload field ${payloadPath(current.path)} contains raw certificate data; provide only the CA SHA-256 digest`)}if(containsUrlWithCredentialMaterial(current.value)){invalid(`payload field ${payloadPath(current.path)} contains a URL with embedded credentials`)}continue}if(Array.isArray(current.value)){if(Object.getPrototypeOf(current.value)!==Array.prototype){invalid("payload arrays must use the standard JSON prototype")}if("toJSON"in current.value){invalid("payload must not define a custom JSON serializer")}if(Object.getOwnPropertySymbols(current.value).length>0||Object.getOwnPropertyNames(current.value).length!==current.value.length+1){invalid("payload arrays must contain only indexed JSON values")}for(let index=0;index0||discoveredNodes+keys.length>MAX_JSON_NODES){invalid("payload structure exceeds the complexity limit")}for(let index=0;indexMANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}}function validateWebSearch(value,agent){const webSearch=requireRecord(value,"agentConfig.webSearch");rejectUnknownKeys(webSearch,WEB_SEARCH_KEYS,"agentConfig.webSearch");const provider=requireStringEnum(webSearch.provider,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].webSearchProviders),"agentConfig.webSearch.provider");return{enabled:requireBoolean(webSearch.enabled,"agentConfig.webSearch.enabled"),provider}}function validateOpenClawOtel(value){const otel=requireRecord(value,"agentConfig.otel");rejectUnknownKeys(otel,OTEL_KEYS,"agentConfig.otel");return{enabled:requireBoolean(otel.enabled,"agentConfig.otel.enabled"),endpointUrl:requireHttpUrl(otel.endpointUrl,"agentConfig.otel.endpointUrl"),serviceName:requireBoundedString(otel.serviceName,"agentConfig.otel.serviceName",MAX_IDENTIFIER_BYTES),sampleRate:requireSampleRate(otel.sampleRate,"agentConfig.otel.sampleRate")}}function validateExtraAgents(value){const extraAgents=requireRecord(value,"agentConfig.extraAgents");rejectUnknownKeys(extraAgents,EXTRA_AGENTS_KEYS,"agentConfig.extraAgents");if(!Array.isArray(extraAgents.agents)||extraAgents.agents.length>MAX_LIST_ITEMS){invalid("agentConfig.extraAgents.agents must be a bounded JSON object list")}return{agents:mapArrayByIndex(extraAgents.agents,(agent,index)=>requireJsonObject(agent,`agentConfig.extraAgents.agents[${String(index)}]`)),defaults:requireJsonObject(extraAgents.defaults,"agentConfig.extraAgents.defaults"),main:requireJsonObject(extraAgents.main,"agentConfig.extraAgents.main")}}function validateAgentConfig(value,expectedAgent){const config=requireRecord(value,"agentConfig");const agent=requireStringEnum(config.agent,MANAGED_STARTUP_AGENT_SET,"agentConfig.agent");if(agent!==expectedAgent)invalid("agentConfig.agent must match agent");if(agent==="openclaw"){rejectUnknownKeys(config,OPENCLAW_CONFIG_KEYS,"agentConfig");const heartbeatEvery=config.heartbeatEvery===null?null:requireBoundedString(config.heartbeatEvery,"agentConfig.heartbeatEvery",MAX_IDENTIFIER_BYTES);if(heartbeatEvery!==null&&!/^\d+(?:s|m|h)$/u.test(heartbeatEvery)){invalid("agentConfig.heartbeatEvery must be null or a duration ending in s, m, or h")}return{agent,webSearch:validateWebSearch(config.webSearch,agent),otel:validateOpenClawOtel(config.otel),agentTimeoutSeconds:requirePositiveInteger(config.agentTimeoutSeconds,"agentConfig.agentTimeoutSeconds"),heartbeatEvery,extraAgents:validateExtraAgents(config.extraAgents),minimalBootstrap:requireBoolean(config.minimalBootstrap,"agentConfig.minimalBootstrap")}}if(agent==="hermes"){rejectUnknownKeys(config,HERMES_CONFIG_KEYS,"agentConfig");return{agent,webSearch:validateWebSearch(config.webSearch,agent)}}if(agent==="pi"){rejectUnknownKeys(config,PI_CONFIG_KEYS,"agentConfig");return{agent}}rejectUnknownKeys(config,DCODE_CONFIG_KEYS,"agentConfig");return{agent,autoApprovalMode:requireStringEnum(config.autoApprovalMode,DCODE_AUTO_APPROVAL_MODE_SET,"agentConfig.autoApprovalMode"),observabilityEnabled:requireBoolean(config.observabilityEnabled,"agentConfig.observabilityEnabled")}}function validateDashboard(value,expectedAgent){const dashboard=requireRecord(value,"dashboard");const agent=requireStringEnum(dashboard.agent,MANAGED_STARTUP_AGENT_SET,"dashboard.agent");if(agent!==expectedAgent)invalid("dashboard.agent must match agent");if(agent==="openclaw"){rejectUnknownKeys(dashboard,OPENCLAW_DASHBOARD_KEYS,"dashboard");const mode=requireStringEnum(dashboard.mode,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].dashboardModes),"dashboard.mode");const url=requireHttpUrl(dashboard.url,"dashboard.url");const bindAddress=requireStringEnum(dashboard.bindAddress,new Set(["127.0.0.1","0.0.0.0"]),"dashboard.bindAddress");const wslExposure=requireBoolean(dashboard.wslExposure,"dashboard.wslExposure");const hasRemoteExposure=!isLoopbackDashboardUrl(url)||bindAddress==="0.0.0.0"||wslExposure;if(mode==="remote"!==hasRemoteExposure){invalid("OpenClaw dashboard.mode must reflect its URL, bind address, and WSL exposure")}const port=requirePort(dashboard.port,"dashboard.port",1024);if(isHermesApiPort(port))invalid(`OpenClaw dashboard.port must not use a reserved Hermes API port (${HERMES_API_PORT_RANGE_START}-${HERMES_API_PORT_RANGE_END})`);if(configuredDashboardPort(url)!==port){invalid("OpenClaw dashboard.port must match dashboard.url")}return{agent,mode,url,port,bindAddress,wslExposure}}if(agent==="hermes"){rejectUnknownKeys(dashboard,HERMES_DASHBOARD_KEYS,"dashboard");const mode=requireStringEnum(dashboard.mode,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].dashboardModes),"dashboard.mode");const url=requireHttpUrl(dashboard.url,"dashboard.url");if(!isLoopbackDashboardUrl(url)){invalid("Hermes dashboard.url must remain loopback; OpenShell owns the host forward")}const browserUrl=dashboard.browserUrl===void 0?void 0:requireHttpUrl(dashboard.browserUrl,"dashboard.browserUrl");if(browserUrl!==void 0&&!isLoopbackDashboardUrl(browserUrl)&&new URL(browserUrl).protocol!=="https:"){invalid("Hermes dashboard.browserUrl must use HTTPS unless it is loopback")}if(mode==="disabled"){if(dashboard.publicPort!==null||dashboard.internalPort!==null||dashboard.tuiEnabled!==false){invalid("disabled Hermes dashboard must not configure ports or TUI")}return{agent,mode,url,...browserUrl===void 0?{}:{browserUrl},publicPort:null,internalPort:null,tuiEnabled:false}}const publicPort=requirePort(dashboard.publicPort,"dashboard.publicPort",1024);const internalPort=requirePort(dashboard.internalPort,"dashboard.internalPort",1024);if(publicPort===internalPort){invalid("Hermes dashboard publicPort and internalPort must differ")}if(isHermesReservedApiPort(publicPort)||isHermesReservedApiPort(internalPort)){invalid(`Hermes dashboard ports must not use reserved API ports ${HERMES_RESERVED_API_PORT_LABEL}`)}if(configuredDashboardPort(url)!==publicPort){invalid("Hermes dashboard.publicPort must match dashboard.url")}if(browserUrl!==void 0&&isLoopbackDashboardUrl(browserUrl)&&configuredDashboardPort(browserUrl)!==publicPort){invalid("Hermes dashboard.publicPort must match dashboard.browserUrl")}return{agent,mode,url,...browserUrl===void 0?{}:{browserUrl},publicPort,internalPort,tuiEnabled:requireBoolean(dashboard.tuiEnabled,"dashboard.tuiEnabled")}}if(agent==="pi"){rejectUnknownKeys(dashboard,PI_DASHBOARD_KEYS,"dashboard");if(dashboard.mode!=="disabled")invalid("pi dashboard.mode must be disabled");return{agent,mode:"disabled"}}rejectUnknownKeys(dashboard,DCODE_DASHBOARD_KEYS,"dashboard");if(dashboard.mode!=="disabled"){invalid("langchain-deepagents-code dashboard.mode must be disabled")}return{agent,mode:"disabled"}}function validateInference(value,agent){if(value===null){if(agent!=="openclaw"&&agent!=="hermes")invalid(`${agent} requires inference configuration`);return null}const inference=requireRecord(value,"inference");rejectUnknownKeys(inference,INFERENCE_KEYS,"inference");const routeProvider=requireBoundedString(inference.routeProvider,"inference.routeProvider");const upstreamProvider=requireBoundedString(inference.upstreamProvider,"inference.upstreamProvider");const servingPreset=inference.servingPreset===void 0?void 0:inference.servingPreset===null?null:requireBoundedString(inference.servingPreset,"inference.servingPreset");const model=requireBoundedString(inference.model,"inference.model",MAX_MODEL_BYTES);const api=requireStringEnum(inference.api,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].inferenceApis),"inference.api");const upstreamEndpointUrl=inference.upstreamEndpointUrl===null?null:requireHttpUrl(inference.upstreamEndpointUrl,"inference.upstreamEndpointUrl");const primaryModelRef=inference.primaryModelRef===null?null:requireBoundedString(inference.primaryModelRef,"inference.primaryModelRef",MAX_MODEL_BYTES);const compatibility=requireJsonObjectOrNull(inference.compatibility,"inference.compatibility");const inputModalities=inference.inputModalities===null?null:requireEnumList(inference.inputModalities,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].inputModalities),"inference.inputModalities",{allowEmpty:false});if(upstreamEndpointUrl!==null&&!MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].supportsUpstreamEndpoint){invalid(`inference.upstreamEndpointUrl must be null for ${agent}`)}if(agent==="openclaw"){if(primaryModelRef===null||inputModalities===null){invalid("openclaw requires primaryModelRef and inputModalities")}if(primaryModelRef!==`${routeProvider}/${model}`){invalid("openclaw primaryModelRef must match routeProvider and model")}}else{if(primaryModelRef!==null||compatibility!==null||inputModalities!==null){invalid(`${agent} does not support primaryModelRef, compatibility, or inputModalities`)}if(agent==="langchain-deepagents-code"&&!isValidDcodeUpstreamProvider(upstreamProvider)){invalid("inference.upstreamProvider must start with an ASCII letter or digit and contain 1-64 ASCII letters, digits, dots, underscores, or hyphens for DCode")}}return{routeProvider,upstreamProvider,...servingPreset===void 0?{}:{servingPreset},model,routedBaseUrl:requireHttpUrl(inference.routedBaseUrl,"inference.routedBaseUrl"),upstreamEndpointUrl,api,primaryModelRef,compatibility,inputModalities}}function validateProxy(value,agent){const proxy=requireRecord(value,"proxy");rejectUnknownKeys(proxy,PROXY_KEYS,"proxy");const hostHttpUrl=requireProxyUrl(proxy.hostHttpUrl,new Set(["http:"]),"proxy.hostHttpUrl");const hostHttpsUrl=requireProxyUrl(proxy.hostHttpsUrl,new Set(["http:","https:"]),"proxy.hostHttpsUrl");const hostNoProxy=requireStringList(proxy.hostNoProxy,"proxy.hostNoProxy");if(!MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].supportsHostProxyIntent&&(hostHttpUrl!==null||hostHttpsUrl!==null||hostNoProxy.length>0)){invalid(`${agent} rejects host proxy intent and accepts only its root-owned managed route`)}return{managedHost:requireManagedProxyHost(proxy.managedHost,"proxy.managedHost"),managedPort:requirePort(proxy.managedPort,"proxy.managedPort"),hostHttpUrl,hostHttpsUrl,hostNoProxy}}function validateTools(value,agent){const tools=requireRecord(value,"tools");rejectUnknownKeys(tools,TOOLS_KEYS,"tools");const enabledGateways=requireEnumList(tools.enabledGateways,new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].toolGateways),"tools.enabledGateways",{allowEmpty:true});return{disclosure:requireStringEnum(tools.disclosure,new Set(["progressive","direct"]),"tools.disclosure"),enabledGateways}}function validateTuning(value,agent){const tuning=requireRecord(value,"tuning");rejectUnknownKeys(tuning,TUNING_KEYS,"tuning");const result={contextWindow:requireNullablePositiveInteger(tuning.contextWindow,"tuning.contextWindow"),maxTokens:requireNullablePositiveInteger(tuning.maxTokens,"tuning.maxTokens"),reasoning:requireNullableBoolean(tuning.reasoning,"tuning.reasoning"),reasoningEffort:tuning.reasoningEffort===null?null:requireStringEnum(tuning.reasoningEffort,REASONING_EFFORT_SET,"tuning.reasoningEffort")};const advertised=new Set(MANAGED_STARTUP_PROFILE_CAPABILITIES[agent].tuningFields);const unsupported=TUNING_FIELD_ORDER.filter(field=>result[field]!==null&&!advertised.has(field));if(unsupported.length>0){invalid(`${agent} does not support startup tuning fields: ${unsupported.join(", ")}`)}if(agent==="openclaw"){const missing=TUNING_FIELD_ORDER.filter(field=>advertised.has(field)&&result[field]===null);if(missing.length>0){invalid(`openclaw requires ${missing.join(", ")} tuning`)}}if(agent==="hermes"&&result.contextWindow!==null&&result.contextWindowcanonicalizeJson(item));if(!isPlainObject(value))return value;const result={};const keys=sortStrings(Object.keys(value));for(let index=0;indexMANAGED_STARTUP_PROFILE_MAX_BYTES){invalid(`canonical payload exceeds ${String(MANAGED_STARTUP_PROFILE_MAX_BYTES)} bytes`)}return serialized}function decodeManagedStartupProfile(encoded){if(typeof encoded!=="string"||encoded.length===0||import_node_buffer.Buffer.byteLength(encoded,"ascii")>MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES||!BASE64URL_RE.test(encoded)||encoded.length%4===1){invalid("encoded payload is malformed or exceeds the size limit")}const bytes=import_node_buffer.Buffer.from(encoded,"base64url");if(bytes.length===0||bytes.length>MANAGED_STARTUP_PROFILE_MAX_BYTES||bytes.toString("base64url")!==encoded){invalid("encoded payload is malformed or exceeds the size limit")}let raw;try{raw=UTF8_DECODER.decode(bytes)}catch{invalid("payload is not valid UTF-8")}let parsed;try{parsed=JSON.parse(raw)}catch{invalid("payload is not valid JSON")}const migration=migrateDecodedManagedStartupProfile(parsed);const profile=validateManagedStartupProfile(migration.value);const canonicalPayload=migration.migratedLegacyProfile?JSON.stringify(canonicalizeJson(parsed)):serializeManagedStartupProfile(profile);if(canonicalPayload!==raw){invalid("payload is not in canonical form")}return profile}function fingerprintManagedStartupProfile(profile){return(0,import_node_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){if(profile.inference===null)return{...PROVIDERLESS_INFERENCE_ENV,NEMOCLAW_TOOL_DISCLOSURE:profile.tools.disclosure};return{NEMOCLAW_INFERENCE_API:profile.inference.api,NEMOCLAW_INFERENCE_BASE_URL:profile.inference.routedBaseUrl,NEMOCLAW_INFERENCE_PROVIDER_ID:profile.inference.routeProvider,NEMOCLAW_MODEL:profile.inference.model,NEMOCLAW_TOOL_DISCLOSURE:profile.tools.disclosure,NEMOCLAW_UPSTREAM_PROVIDER:profile.inference.upstreamProvider}}function appendHostProxyEnvironment(environment,profile,options={}){if(options.preserveAmbientWhenAbsent===true&&profile.proxy.hostHttpUrl===null&&profile.proxy.hostHttpsUrl===null&&profile.proxy.hostNoProxy.length===0){return}const httpProxy=profile.proxy.hostHttpUrl??"";const httpsProxy=profile.proxy.hostHttpsUrl??"";const noProxy=profile.proxy.hostNoProxy.join(",");environment.HTTP_PROXY=httpProxy;environment.HTTPS_PROXY=httpsProxy;environment.NO_PROXY=noProxy;environment.http_proxy=httpProxy;environment.https_proxy=httpsProxy;environment.no_proxy=noProxy}function messagingEnvironment(profile,expectedAgent){if(profile.messaging.plan===null)return{};const plan=parseSandboxMessagingPlan(profile.messaging.plan,{agent:expectedAgent});if(!plan){throw new ManagedStartupAgentEnvironmentError(`messaging.plan must contain a validated ${expectedAgent} messaging plan`)}const{workflow:_workflow,...imageBuildPlan}=plan;return{NEMOCLAW_MESSAGING_PLAN_B64:encodeCanonicalJson(imageBuildPlan)}}function corporateCaMaterial(profile){return Object.freeze({kind:"corporate-ca-handoff",legacyInput:"NEMOCLAW_CORPORATE_CA_B64",expectedSha256:profile.corporateCa.bundleSha256})}function rootOwnedFile(legacyInput,path4,value){return Object.freeze({kind:"root-owned-file",legacyInput,path:path4,contents:`${value} `,owner:"root",group:"root",mode:292})}function dashboardAction(dashboard){return Object.freeze({kind:"configure-dashboard",dashboard:Object.freeze(structuredClone(dashboard))})}function applicationActions(profile,messagingAgent){const actions=[];if(messagingAgent!==null){actions.push(Object.freeze({kind:"apply-messaging-plan",agent:messagingAgent,mode:profile.messaging.plan===null?"clear":"apply",phase:"runtime-setup",runAs:"root"}))}actions.push(Object.freeze({kind:"generate-agent-config",agent:profile.agent,runAs:"sandbox"}));if(messagingAgent!==null){actions.push(Object.freeze({kind:"apply-messaging-plan",agent:messagingAgent,mode:profile.messaging.plan===null?"clear":"apply",phase:"post-agent-install",runAs:"sandbox"}))}actions.push(dashboardAction(profile.dashboard));return Object.freeze(actions)}function mapOpenClawProfile(profile,environment){if(profile.agent!=="openclaw"||profile.agentConfig.agent!=="openclaw"||profile.dashboard.agent!=="openclaw"||profile.inference!==null&&(profile.inference.primaryModelRef===null||profile.inference.inputModalities===null)||profile.tuning.contextWindow===null||profile.tuning.maxTokens===null||profile.tuning.reasoning===null||profile.tuning.reasoningEffort===null){throw new ManagedStartupAgentEnvironmentError("OpenClaw profile state is inconsistent")}const configurationEnvironment={...commonConfigurationEnvironment(profile),...messagingEnvironment(profile,"openclaw"),CHAT_UI_URL:profile.dashboard.url,NEMOCLAW_AGENT_HEARTBEAT_EVERY:profile.agentConfig.heartbeatEvery??"",NEMOCLAW_AGENT_TIMEOUT:String(profile.agentConfig.agentTimeoutSeconds),NEMOCLAW_CONTEXT_WINDOW:String(profile.tuning.contextWindow),NEMOCLAW_DASHBOARD_BIND:profile.dashboard.bindAddress==="0.0.0.0"?profile.dashboard.bindAddress:"",NEMOCLAW_EXTRA_AGENTS_JSON_B64:encodeCanonicalJson(profile.agentConfig.extraAgents),NEMOCLAW_INFERENCE_COMPAT_B64:encodeCanonicalJson(profile.inference?.compatibility??{}),NEMOCLAW_INFERENCE_INPUTS:profile.inference?.inputModalities?.join(",")??"text",NEMOCLAW_MAX_TOKENS:String(profile.tuning.maxTokens),NEMOCLAW_OPENCLAW_OTEL:booleanFlag(profile.agentConfig.otel.enabled),NEMOCLAW_OPENCLAW_OTEL_ENDPOINT:profile.agentConfig.otel.endpointUrl,NEMOCLAW_OPENCLAW_OTEL_SAMPLE_RATE:String(profile.agentConfig.otel.sampleRate),NEMOCLAW_OPENCLAW_OTEL_SERVICE_NAME:profile.agentConfig.otel.serviceName,NEMOCLAW_PRIMARY_MODEL_REF:profile.inference?.primaryModelRef??"",NEMOCLAW_PROXY_HOST:profile.proxy.managedHost,NEMOCLAW_PROXY_PORT:String(profile.proxy.managedPort),NEMOCLAW_REASONING:String(profile.tuning.reasoning),NEMOCLAW_REASONING_EFFORT:profile.tuning.reasoningEffort,NEMOCLAW_SERVING_PRESET:profile.inference?.servingPreset??"",NEMOCLAW_WEB_SEARCH_ENABLED:booleanFlag(profile.agentConfig.webSearch.enabled),NEMOCLAW_WEB_SEARCH_PROVIDER:profile.agentConfig.webSearch.provider,NEMOCLAW_WSL_DASHBOARD_EXPOSURE:booleanFlag(profile.dashboard.wslExposure)};const runtimeEnvironment={...configurationEnvironment};delete runtimeEnvironment.NEMOCLAW_MESSAGING_PLAN_B64;runtimeEnvironment.NEMOCLAW_DASHBOARD_PORT=String(profile.dashboard.port);runtimeEnvironment.NEMOCLAW_MINIMAL_BOOTSTRAP=booleanFlag(profile.agentConfig.minimalBootstrap);appendHostProxyEnvironment(runtimeEnvironment,profile,{preserveAmbientWhenAbsent:true});return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials:Object.freeze([corporateCaMaterial(profile)]),actions:applicationActions(profile,"openclaw")})}function mapHermesProfile(profile,environment){if(profile.agent!=="hermes"||profile.agentConfig.agent!=="hermes"||profile.dashboard.agent!=="hermes"){throw new ManagedStartupAgentEnvironmentError("Hermes profile state is inconsistent")}let chatUiUrl=profile.dashboard.browserUrl??profile.dashboard.url;if(profile.dashboard.mode==="loopback-forwarded"){if(profile.dashboard.browserUrl===void 0){throw new ManagedStartupAgentEnvironmentError("Cannot start the Hermes dashboard because its managed startup profile has no recorded browser URL. Rerun onboarding before starting the sandbox.")}chatUiUrl=profile.dashboard.browserUrl}const configurationEnvironment={...commonConfigurationEnvironment(profile),...messagingEnvironment(profile,"hermes"),CHAT_UI_URL:chatUiUrl,NEMOCLAW_CONTEXT_WINDOW:profile.tuning.contextWindow===null?"":String(profile.tuning.contextWindow),NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER:booleanFlag(profile.tools.enabledGateways.length>0),NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64:encodeCanonicalJson(profile.tools.enabledGateways),NEMOCLAW_WEB_SEARCH_ENABLED:booleanFlag(profile.agentConfig.webSearch.enabled),NEMOCLAW_WEB_SEARCH_PROVIDER:profile.agentConfig.webSearch.provider};const runtimeEnvironment={...configurationEnvironment,HERMES_HOME:"/sandbox/.hermes",HERMES_LAZY_INSTALL_TARGET:"/sandbox/.hermes/lazy-packages"};delete runtimeEnvironment.NEMOCLAW_MESSAGING_PLAN_B64;runtimeEnvironment.NEMOCLAW_DASHBOARD_PORT=profile.dashboard.publicPort===null?"":String(profile.dashboard.publicPort);runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD=profile.dashboard.mode==="loopback-forwarded"?"1":"0";runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD_INTERNAL_PORT=profile.dashboard.internalPort===null?"":String(profile.dashboard.internalPort);runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD_PORT=profile.dashboard.publicPort===null?"":String(profile.dashboard.publicPort);runtimeEnvironment.NEMOCLAW_HERMES_DASHBOARD_TUI=booleanFlag(profile.dashboard.tuiEnabled);runtimeEnvironment.NEMOCLAW_PROXY_HOST=profile.proxy.managedHost;runtimeEnvironment.NEMOCLAW_PROXY_PORT=String(profile.proxy.managedPort);appendHostProxyEnvironment(runtimeEnvironment,profile,{preserveAmbientWhenAbsent:true});return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials:Object.freeze([corporateCaMaterial(profile)]),actions:applicationActions(profile,"hermes")})}function mapDcodeProfile(profile,environment){if(profile.agent!=="langchain-deepagents-code"||profile.agentConfig.agent!=="langchain-deepagents-code"||profile.dashboard.agent!=="langchain-deepagents-code"||profile.messaging.plan!==null||profile.inference===null){throw new ManagedStartupAgentEnvironmentError("LangChain Deep Agents Code profile state is inconsistent")}const reasoningEffort=profile.tuning.reasoningEffort===null||profile.tuning.reasoningEffort==="default"?"":profile.tuning.reasoningEffort;const configurationEnvironment={...commonConfigurationEnvironment(profile),NEMOCLAW_REASONING_EFFORT:reasoningEffort,NEMOCLAW_UPSTREAM_ENDPOINT_URL:profile.inference.upstreamEndpointUrl??""};appendHostProxyEnvironment(configurationEnvironment,profile);const runtimeEnvironment={...configurationEnvironment,NEMOCLAW_OBSERVABILITY:booleanFlag(profile.agentConfig.observabilityEnabled)};delete runtimeEnvironment.NEMOCLAW_INFERENCE_BASE_URL;delete runtimeEnvironment.NEMOCLAW_REASONING_EFFORT;delete runtimeEnvironment.NEMOCLAW_UPSTREAM_PROVIDER;for(const name of["HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy"]){delete runtimeEnvironment[name]}const materials=Object.freeze([corporateCaMaterial(profile),rootOwnedFile("NEMOCLAW_DCODE_AUTO_APPROVAL","/usr/local/share/nemoclaw/dcode-auto-approval",profile.agentConfig.autoApprovalMode),rootOwnedFile("NEMOCLAW_INFERENCE_BASE_URL","/usr/local/share/nemoclaw/dcode-inference-base-url",profile.inference.routedBaseUrl),rootOwnedFile("NEMOCLAW_UPSTREAM_PROVIDER","/usr/local/share/nemoclaw/dcode-upstream-provider",profile.inference.upstreamProvider),rootOwnedFile("NEMOCLAW_PROXY_HOST","/usr/local/share/nemoclaw/dcode-proxy-host",profile.proxy.managedHost),rootOwnedFile("NEMOCLAW_PROXY_PORT","/usr/local/share/nemoclaw/dcode-proxy-port",String(profile.proxy.managedPort)),rootOwnedFile("NEMOCLAW_REASONING_EFFORT","/usr/local/share/nemoclaw/dcode-reasoning-effort",reasoningEffort)]);return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials,actions:applicationActions(profile,null)})}function mapPiProfile(profile,environment){if(profile.agent!=="pi"||profile.agentConfig.agent!=="pi"||profile.dashboard.agent!=="pi"||profile.messaging.plan!==null){throw new ManagedStartupAgentEnvironmentError("Pi profile state is inconsistent")}const configurationEnvironment={...commonConfigurationEnvironment(profile),NEMOCLAW_CONTEXT_WINDOW:profile.tuning.contextWindow===null?"":String(profile.tuning.contextWindow),NEMOCLAW_MAX_TOKENS:profile.tuning.maxTokens===null?"":String(profile.tuning.maxTokens),NEMOCLAW_REASONING:profile.tuning.reasoning===null?"":String(profile.tuning.reasoning)};appendHostProxyEnvironment(configurationEnvironment,profile);const runtimeEnvironment={...configurationEnvironment};delete runtimeEnvironment.NEMOCLAW_INFERENCE_BASE_URL;delete runtimeEnvironment.NEMOCLAW_CONTEXT_WINDOW;delete runtimeEnvironment.NEMOCLAW_MAX_TOKENS;delete runtimeEnvironment.NEMOCLAW_REASONING;for(const name of["HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy"]){delete runtimeEnvironment[name]}const materials=Object.freeze([corporateCaMaterial(profile),rootOwnedFile("NEMOCLAW_PROXY_HOST","/usr/local/share/nemoclaw/pi-proxy-host",profile.proxy.managedHost),rootOwnedFile("NEMOCLAW_PROXY_PORT","/usr/local/share/nemoclaw/pi-proxy-port",String(profile.proxy.managedPort))]);return Object.freeze({schemaVersion:profile.schemaVersion,agent:profile.agent,configurationEnvironment:sortedEnvironment(configurationEnvironment),runtimeEnvironment:sortedEnvironment(runtimeEnvironment),applicationRuntime:applicationRuntimePlan(profile,environment),materials,actions:applicationActions(profile,null)})}function mapManagedStartupProfileToAgentEnvironment(profile,environment=EMPTY_APPLICATION_ENVIRONMENT){const validated=validateManagedStartupProfile(profile);switch(validated.agent){case"openclaw":return mapOpenClawProfile(validated,environment);case"hermes":return mapHermesProfile(validated,environment);case"langchain-deepagents-code":return mapDcodeProfile(validated,environment);case"pi":return mapPiProfile(validated,environment)}}var import_node_buffer3=require("node:buffer");var import_node_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 isManagedStartupRootApplyAgent(value){return typeof value==="string"&&MANAGED_STARTUP_AGENTS.includes(value)}function exactAgent(value){if(isManagedStartupRootApplyAgent(value))return value;return fail3("agent is unsupported")}function createManagedStartupRootApplyRequest(input){const agent=exactAgent(input.agent);if(input.encodedProfile.length===0||input.encodedProfile.length>MANAGED_STARTUP_PROFILE_MAX_ENCODED_BYTES){fail3("encoded profile exceeds its bounded transport")}const profile=decodeManagedStartupProfile(input.encodedProfile);if(profile.agent!==agent){fail3(`profile targets ${profile.agent}, expected ${agent}`)}const corporateCaB64=input.corporateCaB64??null;if(corporateCaB64!==null&&(corporateCaB64.length===0||corporateCaB64.length>MAX_CORPORATE_CA_ENCODED_BYTES||!STANDARD_BASE64_RE.test(corporateCaB64)||Buffer.from(corporateCaB64,"base64").toString("base64")!==corporateCaB64)){fail3("corporate CA is not canonical bounded base64")}if(profile.corporateCa.bundleSha256!==null!==(corporateCaB64!==null)){fail3("corporate CA transport does not match the profile")}if(corporateCaB64!==null&&(0,import_node_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 MANAGED_HERMES_STATE_ROOT="/sandbox/.hermes";var MANAGED_OPENCLAW_STATE_ROOT="/sandbox/.openclaw";var HERMES_STATE_VOLUME_NAME_PREFIX="nemoclaw-hermes-state-v1";var OPENCLAW_STATE_VOLUME_NAME_PREFIX="nemoclaw-openclaw-state-v1";var MANAGED_AGENT_STATE_ROOTS=Object.freeze({openclaw:Object.freeze([Object.freeze({mountTarget:MANAGED_OPENCLAW_STATE_ROOT,resourceIdentity:sandboxName=>`${OPENCLAW_STATE_VOLUME_NAME_PREFIX}-${sandboxName}`,ownershipLabels:(sandboxName,mountTarget)=>Object.freeze({"io.nvidia.nemoclaw.openclaw-state.managed":"true","io.nvidia.nemoclaw.openclaw-state.schema":"1","io.nvidia.nemoclaw.openclaw-state.sandbox":sandboxName,"io.nvidia.nemoclaw.openclaw-state.target":mountTarget}),uidAuthority:"agent",gidAuthority:"agent",mode:1528,readWrite:true})]),hermes:Object.freeze([Object.freeze({mountTarget:MANAGED_HERMES_STATE_ROOT,resourceIdentity:sandboxName=>`${HERMES_STATE_VOLUME_NAME_PREFIX}-${sandboxName}`,ownershipLabels:(sandboxName,mountTarget)=>Object.freeze({"io.nvidia.nemoclaw.hermes-state.managed":"true","io.nvidia.nemoclaw.hermes-state.schema":"1","io.nvidia.nemoclaw.hermes-state.sandbox":sandboxName,"io.nvidia.nemoclaw.hermes-state.target":mountTarget}),uidAuthority:"agent",gidAuthority:"agent",mode:2040,readWrite:true})]),"langchain-deepagents-code":Object.freeze([]),pi:Object.freeze([])});function managedStartupStateRootMountTargets(agent){return Object.freeze(MANAGED_AGENT_STATE_ROOTS[agent].map(({mountTarget})=>mountTarget))}var MANAGED_AGENT_WORKSPACE_ROOTS=Object.freeze({openclaw:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493}),hermes:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493}),"langchain-deepagents-code":Object.freeze({uidAuthority:"root",gidAuthority:"agent",mode:1021}),pi:Object.freeze({uidAuthority:"agent",gidAuthority:"agent",mode:493})});function exactAgentIdentity(value,label){if(!Number.isSafeInteger(value)||value<0||value>2147483647){throw new Error(`Managed startup state-root ${label} authority is invalid.`)}return value}function managedStartupWorkspaceRoot(input){const uid=exactAgentIdentity(input.agentIdentity.uid,"workspace UID");const gid=exactAgentIdentity(input.agentIdentity.gid,"workspace GID");const declaration=MANAGED_AGENT_WORKSPACE_ROOTS[input.agent];return Object.freeze({uid:declaration.uidAuthority==="root"?0:uid,gid,mode:declaration.mode})}var TRANSACTION_SCHEMA_VERSION=1;var MAX_TRANSACTION_FILES=128;var MAX_TRANSACTION_FILE_BYTES=8*1024*1024;var MAX_TRANSACTION_TOTAL_BYTES=32*1024*1024;var MAX_MANIFEST_BYTES=256*1024;var MAX_COMMIT_RECEIPT_BYTES=4096;var TRANSACTION_PARENT_DIRECTORY_MODE=493;var TRANSACTION_DIRECTORY_MODE=448;var TRANSACTION_FILE_MODE=256;var ATOMIC_TEMPORARY_FILE_MODE=384;var MANAGED_STARTUP_SHARED_TRANSACTION_DIRECTORY="/var/lib/nemoclaw/managed-startup-shared-state-transaction-v1";var MANAGED_STARTUP_SHARED_ROLLBACK_RECEIPT_DIRECTORY="/run/nemoclaw/managed-startup-shared-rollback-receipt-v1";var MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY="/var/lib/nemoclaw/managed-startup-shared-state-commit-v1";var MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE="receipt.json";function fail4(message){throw new Error(`Managed startup shared-state transaction failed: ${message}`)}function resolveOptions(options={}){const sandboxRoot=import_node_path2.default.resolve(options.sandboxRoot??"/sandbox");const transactionDirectory=import_node_path2.default.resolve(options.transactionDirectory??MANAGED_STARTUP_SHARED_TRANSACTION_DIRECTORY);const commitReceiptDirectory=import_node_path2.default.resolve(options.commitReceiptDirectory??(options.transactionDirectory?import_node_path2.default.join(import_node_path2.default.dirname(transactionDirectory),import_node_path2.default.basename(MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY)):MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_DIRECTORY));if(transactionDirectory===sandboxRoot||transactionDirectory.startsWith(`${sandboxRoot}${import_node_path2.default.sep}`)||commitReceiptDirectory===sandboxRoot||commitReceiptDirectory.startsWith(`${sandboxRoot}${import_node_path2.default.sep}`)||import_node_path2.default.dirname(commitReceiptDirectory)!==import_node_path2.default.dirname(transactionDirectory)||commitReceiptDirectory===transactionDirectory){fail4("transaction and commit receipts require distinct paths outside sandbox-shared state")}const bootstrapIdentity=options.bootstrapIdentity??null;if(bootstrapIdentity!==null&&!/^[a-f0-9]{64}$/u.test(bootstrapIdentity)){fail4("bootstrap identity must encode 32 lowercase-hex bytes")}return{sandboxRoot,transactionParentDirectory:import_node_path2.default.dirname(transactionDirectory),transactionDirectory,backupDirectory:import_node_path2.default.join(transactionDirectory,"backups"),manifestFile:import_node_path2.default.join(transactionDirectory,"manifest.json"),commitReceiptDirectory,commitReceiptFile:import_node_path2.default.join(commitReceiptDirectory,MANAGED_STARTUP_SHARED_COMMIT_RECEIPT_FILE),trustedUid:options.trustedUid??0,trustedGid:options.trustedGid??0,readOnlyReceipt:options.readOnlyReceipt??false,bootstrapIdentity}}function modeOf2(stat){if(typeof stat.mode==="bigint"){return Number(stat.mode&0o7777n)}return stat.mode&4095}function requireTransactionIdentity(options){const expectedUid=options.readOnlyReceipt?0:options.trustedUid;const expectedGid=options.readOnlyReceipt?0:options.trustedGid;if(process.geteuid?.()!==expectedUid||process.getegid?.()!==expectedGid){fail4("transaction control requires the trusted effective identity")}}function pathExistsNoFollow(target){try{import_node_fs2.default.lstatSync(target);return true}catch(error){if(error.code==="ENOENT")return false;fail4(`could not inspect ${target}`)}}function requireDirectory(target,options,expectedMode=null){let stat;try{stat=import_node_fs2.default.lstatSync(target)}catch{fail4(`required directory is missing: ${target}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`required directory is unsafe: ${target}`)}if(expectedMode!==null&&(stat.uid!==options.trustedUid||stat.gid!==options.trustedGid||modeOf2(stat)!==expectedMode)){fail4(`${target} must be ${options.trustedUid}:${options.trustedGid} mode ${expectedMode.toString(8)}`)}return stat}function requireTransactionBoundaries(options){requireDirectory(options.sandboxRoot,options);requireDirectory(options.transactionParentDirectory,options,TRANSACTION_PARENT_DIRECTORY_MODE)}function sameStableMetadata(left,right){return left.dev===right.dev&&left.ino===right.ino&&left.mode===right.mode&&left.nlink===right.nlink&&left.uid===right.uid&&left.gid===right.gid&&left.size===right.size&&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs}function readStableFile(target,maxBytes){const noFollow=import_node_fs2.default.constants.O_NOFOLLOW;if(typeof noFollow!=="number")fail4("O_NOFOLLOW is unavailable");let descriptor;try{descriptor=import_node_fs2.default.openSync(target,import_node_fs2.default.constants.O_RDONLY|noFollow)}catch{fail4(`could not safely open ${target}`)}try{const before=import_node_fs2.default.fstatSync(descriptor,{bigint:true});if(!before.isFile()||before.nlink!==1n||before.size<0n||before.size>BigInt(maxBytes)){fail4(`refusing unsafe or oversized transaction file ${target}`)}const bytes=Buffer.alloc(Number(before.size));let offset=0;while(offset!segment||segment==="."||segment==="..")){fail4(`unsafe transaction path ${JSON.stringify(value)}`)}return segments.join("/")}function absoluteTarget(relativePath,options){const safe=safeRelativePath(relativePath);const target=import_node_path2.default.resolve(options.sandboxRoot,safe);if(!target.startsWith(`${options.sandboxRoot}${import_node_path2.default.sep}`)){fail4(`transaction target escapes the sandbox root: ${relativePath}`)}return target}function relativeTarget(target,options){return safeRelativePath(import_node_path2.default.relative(options.sandboxRoot,target))}function isDeclaredAgentStateRoot(expectedAgent,outputRoot,options){const relative=import_node_path2.default.relative(options.sandboxRoot,outputRoot).split(import_node_path2.default.sep).join("/");const canonicalTarget=import_node_path2.default.posix.join("/sandbox",relative);return managedStartupStateRootMountTargets(expectedAgent).includes(canonicalTarget)}function validateExistingAncestors(target,expectedAgent,options){const relative=relativeTarget(target,options);const sandboxStat=requireDirectory(options.sandboxRoot,options);const outputRoot=agentRoot(expectedAgent,options.sandboxRoot);if(target!==outputRoot&&!target.startsWith(`${outputRoot}${import_node_path2.default.sep}`)){fail4(`transaction target escapes the ${expectedAgent} state root: ${target}`)}let current=options.sandboxRoot;let expectedDevice=sandboxStat.dev;const segments=relative.split("/").slice(0,-1);for(const segment of segments){current=import_node_path2.default.join(current,segment);let stat;try{stat=import_node_fs2.default.lstatSync(current)}catch(error){if(error.code==="ENOENT")return;fail4(`could not inspect transaction path ancestor ${current}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`transaction path ancestor is unsafe: ${current}`)}if(current===outputRoot&&isDeclaredAgentStateRoot(expectedAgent,outputRoot,options)){expectedDevice=stat.dev}else if(stat.dev!==expectedDevice){fail4(`transaction path crosses a nested filesystem mount: ${current}`)}}}function managedOutputDevice(expectedAgent,options){const sandboxStat=requireDirectory(options.sandboxRoot,options);const outputRoot=agentRoot(expectedAgent,options.sandboxRoot);let stat;try{stat=import_node_fs2.default.lstatSync(outputRoot)}catch(error){if(error.code==="ENOENT")return sandboxStat.dev;fail4(`could not inspect managed output root ${outputRoot}`)}if(stat.isSymbolicLink()||!stat.isDirectory()){fail4(`managed output root is unsafe: ${outputRoot}`)}if(!isDeclaredAgentStateRoot(expectedAgent,outputRoot,options)&&stat.dev!==sandboxStat.dev){fail4(`managed output root crosses a nested filesystem mount: ${outputRoot}`)}return stat.dev}function agentRoot(agent,sandboxRoot){switch(agent){case"openclaw":return import_node_path2.default.join(sandboxRoot,".openclaw");case"hermes":return import_node_path2.default.join(sandboxRoot,".hermes");case"langchain-deepagents-code":return import_node_path2.default.join(sandboxRoot,".deepagents");case"pi":return import_node_path2.default.join(sandboxRoot,".pi")}}function resolveUnderAgentRoot(root,relativePath){const safe=safeRelativePath(relativePath);const target=import_node_path2.default.resolve(root,safe);if(!target.startsWith(`${root}${import_node_path2.default.sep}`)){fail4(`managed output escapes the agent root: ${relativePath}`)}return target}function renderTarget(root,agent,target){if(agent==="openclaw"&&target==="openclaw.json"){return import_node_path2.default.join(root,"openclaw.json")}const prefix=agent==="openclaw"?"~/.openclaw/":agent==="hermes"?"~/.hermes/":null;if(!prefix||!target.startsWith(prefix)){fail4(`unsupported managed messaging render target ${JSON.stringify(target)}`)}return resolveUnderAgentRoot(root,target.slice(prefix.length))}function managedOutputTargets(profile,options){const root=agentRoot(profile.agent,options.sandboxRoot);const files=new Set;const directories=new Set([root]);switch(profile.agent){case"openclaw":files.add(import_node_path2.default.join(root,"openclaw.json"));break;case"hermes":files.add(import_node_path2.default.join(root,"config.yaml"));files.add(import_node_path2.default.join(root,".env"));files.add(import_node_path2.default.join(root,".config-hash"));break;case"langchain-deepagents-code":files.add(import_node_path2.default.join(root,"config.toml"));directories.add(import_node_path2.default.join(root,".state"));break;case"pi":directories.add(import_node_path2.default.join(root,"agent"));files.add(import_node_path2.default.join(root,"agent","models.json"));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)}