From d9a882e69c7d538d24e747969c6987315dec9dfc Mon Sep 17 00:00:00 2001 From: Ali S Date: Tue, 1 Sep 2026 23:35:01 +0000 Subject: [PATCH 1/6] fix(items): approve openclaw device pairing in-container Deploying this item produced a Control UI nobody could get into. The browser authenticated, then stopped at "device pairing required (requestId: ...)" and named a CLI on the Gateway host to approve it. Device pairing is a second gate, evaluated only after gateway token auth has already succeeded. A new browser's request can be approved only by an already-paired admin session, and on a fresh deployment nothing is ever paired -- so the item deadlocked, and the remedy the error names needs a host shell a one-click deployment does not have. Neither the token nor the origin allowlist was at fault; both already pass before this point. Satisfy the gate rather than remove it. The component now runs a watcher beside the gateway that polls `devices list --json` and approves pending requests. Upstream anticipates this: `shouldPreserveLocalCliSharedAuthScopes` carries a dedicated `cli_container_local` locality that preserves operator scopes for a token-authenticated CLI inside the gateway's own container, so `devices approve` works over loopback without pairing of its own. Rejected `gateway.controlUi.dangerouslyDisableDeviceAuth`, which clears the same gate by discarding device identity altogether: browsers keep their device keypair and revocable device token this way, `openclaw security audit` stays clean, and the mechanism survives 2026.8.x, where that key is retired and inert and would silently restore the deadlock on an image bump. Also verified against the pinned release that `allowInsecureAuth` ("does not bypass pairing checks", localhost-only) and `nodes.pairing.autoApproveCidrs` (never applies to browser clients) are not alternatives, despite both being widely cited as such. Two greps rather than a JSON parser because the image ships no jq and a node -e script cannot be quoted inside this scalar. `requestId` appears only on pending entries -- paired rows carry deviceId, displayName, roles, scopes, tokens and IP -- so the match cannot touch an existing device. The listing's claim that you just enter the token on first visit was false for every deployment; it now describes both gates and says plainly that the token is what guards an admin surface. Verified: typecheck clean, full corpus 377/377; the folded scalar resolves to one well-formed shell string whose --batch-json argument parses as the three intended ops; `sh -n` accepts the inner script; and the grep pipeline extracts exactly the pending requestId from a payload carrying both a pending entry and a paired device, ignoring the latter's UUID. Not verified -- no container runtime in the devcontainer: the live smoke test that the watcher actually clears the pairing screen. Closes #6 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LpjD9Pkxh8LBhJPJQLzpx3 --- items/openclaw/components/openclaw.yaml | 46 +++++++++++++++++++++++++ items/openclaw/listing.yaml | 16 +++++++++ 2 files changed, 62 insertions(+) diff --git a/items/openclaw/components/openclaw.yaml b/items/openclaw/components/openclaw.yaml index 87ab1b1..bb90687 100755 --- a/items/openclaw/components/openclaw.yaml +++ b/items/openclaw/components/openclaw.yaml @@ -28,12 +28,58 @@ spec: # the config lives on a persistent volume, so a guard would pin the very first # boot's origin forever and leave a re-deployed app permanently unreachable. # `config set` is declarative, so re-applying is a no-op. + # + # GATEWAY AUTH IS UNTOUCHED. `gateway.auth.mode` stays at its default + # (`token`), so every Control UI connection still has to present + # OPENCLAW_GATEWAY_TOKEN or it is rejected at the WebSocket handshake. The + # loop below deals with a SECOND, separate gate that runs only after that + # token check has already passed. + # + # That second gate is device pairing, and it deadlocks a one-click + # deployment: a new browser's pairing request can only be approved by an + # already-paired admin session, and on a fresh deployment nothing is ever + # paired. Left alone, the Control UI is permanently unreachable, reporting + # "device pairing required (requestId: ...)" and naming a CLI that a catalog + # user has no host shell to run. + # + # So the container approves them itself. Upstream anticipates exactly this: + # `shouldPreserveLocalCliSharedAuthScopes` in the 2026.7.1 bundle + # (dist/message-handler-*.js) carries a dedicated `cli_container_local` + # locality that preserves operator scopes for a token-authenticated CLI + # running inside the gateway's own container. `devices approve` therefore + # works over loopback with no pairing of its own. + # + # Chosen over `gateway.controlUi.dangerouslyDisableDeviceAuth`, which also + # clears the gate but does so by discarding device identity altogether. This + # keeps it: every browser still binds a device keypair and gets its own + # revocable device token, `openclaw security audit` stays clean, and the + # mechanism survives 2026.8.x, where that key is retired and inert. + # + # It does not widen WHO reaches the gate — a pairing request can only be + # created by a caller that already passed token auth — but it does mean a + # leaked token can enrol a browser without a human approving it. The token + # is the boundary; rotate it if it leaks. + # + # Two greps rather than a JSON parser because the image ships no jq, and a + # node -e script cannot be quoted inside this scalar. `requestId` appears + # only on pending entries (paired devices carry `deviceId`), and the second + # grep takes the UUID out of the match. Failures are swallowed on purpose: + # for the first few seconds the gateway is not listening yet. command: >- sh -c 'set -e; node /app/dist/index.js config set --batch-json "[{\"path\":\"gateway.mode\",\"value\":\"local\"}, {\"path\":\"gateway.bind\",\"value\":\"lan\"}, {\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"${OPENCLAW_PUBLIC_ORIGIN%/}\"]}]"; + (while :; do + node /app/dist/index.js devices list --json 2>/dev/null + | grep -o "\"requestId\":[[:space:]]*\"[^\"]*\"" + | grep -o "[0-9a-f-]\{36\}" + | while read -r id; do + node /app/dist/index.js devices approve "$id" >/dev/null 2>&1 || true; + done || true; + sleep 5; + done) & exec node openclaw.mjs gateway' endpoints: primary: diff --git a/items/openclaw/listing.yaml b/items/openclaw/listing.yaml index c1762c1..682fcbc 100755 --- a/items/openclaw/listing.yaml +++ b/items/openclaw/listing.yaml @@ -36,6 +36,22 @@ spec: keep configuration, session history, and the OAuth-token encryption key across restarts. + ## Access and security + Every connection must present the gateway token, so the Control UI is + never open to an unauthenticated visitor. + + Behind that, OpenClaw adds a one-time device-pairing approval for each + new browser, normally run as `openclaw devices approve` on the Gateway + host. A one-click deployment gives you no host shell, so the container + runs that approval itself and pairing completes without your involvement. + Each browser still gets its own device identity, which you can revoke + individually with `openclaw devices revoke`. + + The practical consequence is that the gateway token is what guards an + admin surface that can run tools and shell commands: anyone holding it + can pair a browser. Treat the token as a secret and rotate it from the + Configuration tab if it leaks. + Messaging-channel onboarding (WhatsApp, Telegram, Discord) requires the interactive CLI and is not available in this deployment yet. category: AI_ML From dad944273e41105b039dc8f13821eba9f4df20b3 Mon Sep 17 00:00:00 2001 From: Ali S Date: Wed, 2 Sep 2026 00:25:15 +0000 Subject: [PATCH 2/6] perf(items): cut openclaw pairing latency by watching pending.json The watcher polled `devices list --json` every five seconds. That call is a cold Node start against a large CLI bundle, and the approve path is a second one, so a user's first connect sat in apparent failure for 10-15 seconds before the Control UI came up -- confirmed on a live deployment. Read the persisted pending-pairing file instead. The gateway writes each pending request to /devices/pending.json before it rejects the browser, and `resolveStateDir` resolves to OPENCLAW_STATE_DIR, else OPENCLAW_HOME/HOME joined with `.openclaw` -- which is exactly where this item mounts its config volume, so the path agrees by construction rather than by coincidence. Detection now costs two greps, spawns no Node process while nothing is pending, and leaves only one CLI start in the user's path. Poll drops to 1s because it is no longer expensive. Reading pending.json is also strictly better isolated than the previous `devices list` output: paired devices live in a separate file, so a paired entry cannot be matched at all. Verified: full corpus 377/377, typecheck clean; the folded scalar still resolves to the three intended config ops; `sh -n` accepts the inner script; and against fixtures written in the persisted shape the pipeline extracts exactly the pending requestId, yields nothing for paired.json, and handles the file being absent (it does not exist until a browser first pairs). The previous approach was confirmed working end-to-end on a live deployment; this change alters only how a pending request is detected, not what is approved. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LpjD9Pkxh8LBhJPJQLzpx3 --- items/openclaw/components/openclaw.yaml | 29 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/items/openclaw/components/openclaw.yaml b/items/openclaw/components/openclaw.yaml index bb90687..4dfa776 100755 --- a/items/openclaw/components/openclaw.yaml +++ b/items/openclaw/components/openclaw.yaml @@ -60,11 +60,25 @@ spec: # leaked token can enrol a browser without a human approving it. The token # is the boundary; rotate it if it leaks. # - # Two greps rather than a JSON parser because the image ships no jq, and a - # node -e script cannot be quoted inside this scalar. `requestId` appears - # only on pending entries (paired devices carry `deviceId`), and the second - # grep takes the UUID out of the match. Failures are swallowed on purpose: - # for the first few seconds the gateway is not listening yet. + # Watch the persisted pending-pairing file rather than polling `devices + # list`. That call is a cold Node start against a large CLI bundle, and at + # two starts per five-second tick it put 10-15 seconds of apparent failure + # in front of a user's first connect. Reading the file costs two greps, + # spawns no Node process while nothing is pending, and leaves the wait at + # roughly one CLI start. + # + # `$HOME/.openclaw/devices/pending.json` mirrors how the runtime resolves + # that path (OPENCLAW_STATE_DIR, else OPENCLAW_HOME/HOME joined with + # `.openclaw`). This item sets no state-dir override and mounts the config + # volume at exactly that path, so the two agree by construction. Approval + # moves the entry into paired.json, so a request is approved once and the + # loop then finds nothing. + # + # Two greps rather than a JSON parser because the image ships no jq and a + # node -e script cannot be quoted inside this scalar. Every value persisted + # in that file carries `requestId`, and the second grep lifts the UUID out + # of the match. Failures are swallowed: the file does not exist at all until + # a browser first tries to pair. command: >- sh -c 'set -e; node /app/dist/index.js config set --batch-json @@ -72,13 +86,12 @@ spec: {\"path\":\"gateway.bind\",\"value\":\"lan\"}, {\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"${OPENCLAW_PUBLIC_ORIGIN%/}\"]}]"; (while :; do - node /app/dist/index.js devices list --json 2>/dev/null - | grep -o "\"requestId\":[[:space:]]*\"[^\"]*\"" + grep -o "\"requestId\":[[:space:]]*\"[^\"]*\"" "$HOME/.openclaw/devices/pending.json" 2>/dev/null | grep -o "[0-9a-f-]\{36\}" | while read -r id; do node /app/dist/index.js devices approve "$id" >/dev/null 2>&1 || true; done || true; - sleep 5; + sleep 1; done) & exec node openclaw.mjs gateway' endpoints: From 42e54bc725177fbd595860bb9a7c174875467f1e Mon Sep 17 00:00:00 2001 From: Ali S Date: Wed, 2 Sep 2026 00:40:21 +0000 Subject: [PATCH 3/6] fix(items): admit any token-holding browser to openclaw, drop the pairing watcher Replace the in-container `devices approve` watcher with `gateway.controlUi.dangerouslyDisableDeviceAuth`. The intended posture for this item is that anyone holding the gateway token gets in from any browser, with no device enrolled or allowlisted, and no background process load-bearing for reachability. Gateway auth is untouched, despite the key's name. Reading the connect path in the 2026.7.1 bundle, three gates run in order: device identity (allowBypass admits), then an unconditional `if (!authOk) { rejectUnauthorized(authResult); return; }`, then the pairing gate (allowBypass skips). Gate two carries no isControlUi test and no allowBypass exemption and sits between the two the key affects, so the token check stays fully enforced while device identity and pairing go away. The watcher it replaces worked and was confirmed live, but it made reachability depend on a polling loop and put a cold CLI start in front of every first connect. Traded for a declarative key and an instant first load. The cost is stated in both files: no second factor, no per-device revocation, and browsers re-present the token each session rather than holding a stored device token. The image tag is now load-bearing and says so. 2026.7.1 is the last stable release honouring this key: its `shouldSkipControlUiPairing` ends `return role === "operator" && policy.allowBypass`, while 2026.8.1 and 2026.8.2 both end `return null`, and upstream calls the key "a retired break-glass input, now fully inert" with `doctor --fix` deleting it. On 2026.8.x the controlUi schema no longer offers a device-auth toggle at all, and the surviving bypasses are trusted-proxy `deviceAutoApprove` (needs an identity-injecting edge) or node-role pairing policy (excludes browsers). A tag bump therefore silently restores the deadlock; the comment says what has to change alongside it. Verified: full corpus 377/377, typecheck clean, `sh -n` accepts the inner script, and the folded scalar resolves to four config ops with dangerouslyDisableDeviceAuth carried as a JSON boolean rather than a string. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LpjD9Pkxh8LBhJPJQLzpx3 --- items/openclaw/components/openclaw.yaml | 106 ++++++++++++------------ items/openclaw/listing.yaml | 21 ++--- 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/items/openclaw/components/openclaw.yaml b/items/openclaw/components/openclaw.yaml index 4dfa776..0744a15 100755 --- a/items/openclaw/components/openclaw.yaml +++ b/items/openclaw/components/openclaw.yaml @@ -7,7 +7,24 @@ spec: kind: SERVICE source: type: IMAGE - # Latest stable calendar release (beta tags are not pinned for the catalog). + # PINNED, and the pin is load-bearing rather than merely current: 2026.7.1 + # is the last stable release in which + # `gateway.controlUi.dangerouslyDisableDeviceAuth` still works, and this + # item depends on it to be reachable at all (see the block above `command`). + # + # Verified by reading `shouldSkipControlUiPairing` in each published + # bundle: 2026.7.1 ends `return role === "operator" && policy.allowBypass`, + # while 2026.8.1 and 2026.8.2 both end `return null`, having dropped that + # branch. Upstream calls the key "a retired break-glass input, now fully + # inert" and has `doctor --fix` delete it. 2026.7.2 exists only as betas, + # which this catalog does not pin. + # + # So bumping this tag re-breaks the deployment silently. Anything past + # 2026.7.1 has to pair devices, which means either re-adding the + # in-container `devices approve` watcher this item used to carry, or + # `gateway.auth.trustedProxy.deviceAutoApprove` (2026.8.x only), which + # needs `gateway.auth.mode: "trusted-proxy"` and therefore a platform edge + # that injects an identity header. Settle that before changing the tag. ref: ghcr.io/openclaw/openclaw:2026.7.1 # Converge config through OpenClaw's own `config set` CLI, then exec the # stock gateway entrypoint (`tini -s --` passes argv through, so `sh -c` works). @@ -29,70 +46,49 @@ spec: # boot's origin forever and leave a re-deployed app permanently unreachable. # `config set` is declarative, so re-applying is a no-op. # - # GATEWAY AUTH IS UNTOUCHED. `gateway.auth.mode` stays at its default - # (`token`), so every Control UI connection still has to present - # OPENCLAW_GATEWAY_TOKEN or it is rejected at the WebSocket handshake. The - # loop below deals with a SECOND, separate gate that runs only after that - # token check has already passed. - # - # That second gate is device pairing, and it deadlocks a one-click - # deployment: a new browser's pairing request can only be approved by an - # already-paired admin session, and on a fresh deployment nothing is ever - # paired. Left alone, the Control UI is permanently unreachable, reporting - # "device pairing required (requestId: ...)" and naming a CLI that a catalog - # user has no host shell to run. - # - # So the container approves them itself. Upstream anticipates exactly this: - # `shouldPreserveLocalCliSharedAuthScopes` in the 2026.7.1 bundle - # (dist/message-handler-*.js) carries a dedicated `cli_container_local` - # locality that preserves operator scopes for a token-authenticated CLI - # running inside the gateway's own container. `devices approve` therefore - # works over loopback with no pairing of its own. + # GATEWAY AUTH IS UNTOUCHED, despite the name of the key below. + # `gateway.auth.mode` stays at its default (`token`), so every Control UI + # connection still has to present OPENCLAW_GATEWAY_TOKEN. Verified by + # reading the connect path in the 2026.7.1 bundle + # (dist/message-handler-*.js), where three gates run in this order: # - # Chosen over `gateway.controlUi.dangerouslyDisableDeviceAuth`, which also - # clears the gate but does so by discarding device identity altogether. This - # keeps it: every browser still binds a device keypair and gets its own - # revocable device token, `openclaw security audit` stays clean, and the - # mechanism survives 2026.8.x, where that key is retired and inert. + # 1. handleMissingDeviceIdentity() — device identity; allowBypass admits + # 2. `if (!authOk) { rejectUnauthorized(authResult); return; }` + # 3. shouldSkipControlUiPairing() — pairing; allowBypass skips # - # It does not widen WHO reaches the gate — a pairing request can only be - # created by a caller that already passed token auth — but it does mean a - # leaked token can enrol a browser without a human approving it. The token - # is the boundary; rotate it if it leaks. + # Gate 2 is unconditional — no isControlUi test, no allowBypass exemption — + # and it sits BETWEEN the two gates the key affects. So the key removes + # device identity and pairing while the token check stays fully enforced. # - # Watch the persisted pending-pairing file rather than polling `devices - # list`. That call is a cold Node start against a large CLI bundle, and at - # two starts per five-second tick it put 10-15 seconds of apparent failure - # in front of a user's first connect. Reading the file costs two greps, - # spawns no Node process while nothing is pending, and leaves the wait at - # roughly one CLI start. + # That is the intended posture for this item: anyone holding the token gets + # in from any browser, and no device is enrolled or allowlisted. Device + # pairing is a second gate that a valid token does not satisfy, it can only + # be cleared by an already-paired admin session, and on a fresh deployment + # nothing is ever paired — so left alone the Control UI is permanently + # unreachable, reporting "device pairing required (requestId: ...)" and + # naming a CLI that a catalog user has no host shell to run. Upstream was + # asked to let a valid token bypass pairing and declined (openclaw#29908, + # closed as not planned). # - # `$HOME/.openclaw/devices/pending.json` mirrors how the runtime resolves - # that path (OPENCLAW_STATE_DIR, else OPENCLAW_HOME/HOME joined with - # `.openclaw`). This item sets no state-dir override and mounts the config - # volume at exactly that path, so the two agree by construction. Approval - # moves the entry into paired.json, so a request is approved once and the - # loop then finds nothing. + # The alternative, carried here previously, was an in-container watcher that + # approved pending requests via `devices approve`. It worked and kept device + # identity, but it made a background process load-bearing for reachability + # and put a cold CLI start in front of every first connect. Traded away + # deliberately for a declarative key and an instant first load. # - # Two greps rather than a JSON parser because the image ships no jq and a - # node -e script cannot be quoted inside this scalar. Every value persisted - # in that file carries `requestId`, and the second grep lifts the UUID out - # of the match. Failures are swallowed: the file does not exist at all until - # a browser first tries to pair. + # What it costs: with no device identity there is no second factor and no + # per-device revocation, so a leaked token is sufficient on its own, and + # browsers re-present the token each session instead of holding a stored + # device token. `openclaw security audit` reports + # `gateway.control_ui.device_auth_disabled` as critical — expected here, not + # a regression. The token is the whole boundary; rotate it if it leaks. command: >- sh -c 'set -e; node /app/dist/index.js config set --batch-json "[{\"path\":\"gateway.mode\",\"value\":\"local\"}, {\"path\":\"gateway.bind\",\"value\":\"lan\"}, - {\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"${OPENCLAW_PUBLIC_ORIGIN%/}\"]}]"; - (while :; do - grep -o "\"requestId\":[[:space:]]*\"[^\"]*\"" "$HOME/.openclaw/devices/pending.json" 2>/dev/null - | grep -o "[0-9a-f-]\{36\}" - | while read -r id; do - node /app/dist/index.js devices approve "$id" >/dev/null 2>&1 || true; - done || true; - sleep 1; - done) & + {\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"${OPENCLAW_PUBLIC_ORIGIN%/}\"]}, + {\"path\":\"gateway.controlUi.dangerouslyDisableDeviceAuth\",\"value\":true}]"; exec node openclaw.mjs gateway' endpoints: primary: diff --git a/items/openclaw/listing.yaml b/items/openclaw/listing.yaml index 682fcbc..9514340 100755 --- a/items/openclaw/listing.yaml +++ b/items/openclaw/listing.yaml @@ -38,18 +38,19 @@ spec: ## Access and security Every connection must present the gateway token, so the Control UI is - never open to an unauthenticated visitor. + never open to an unauthenticated visitor. Any browser holding the token + can connect — there is no per-device approval step to complete first. - Behind that, OpenClaw adds a one-time device-pairing approval for each - new browser, normally run as `openclaw devices approve` on the Gateway - host. A one-click deployment gives you no host shell, so the container - runs that approval itself and pairing completes without your involvement. - Each browser still gets its own device identity, which you can revoke - individually with `openclaw devices revoke`. + That differs from a default OpenClaw install, which also requires each + new browser to be approved once by running `openclaw devices approve` on + the Gateway host. A one-click deployment gives you no host shell to run + that from, so this item turns the device check off and leaves the token + as the single gate. - The practical consequence is that the gateway token is what guards an - admin surface that can run tools and shell commands: anyone holding it - can pair a browser. Treat the token as a secret and rotate it from the + The practical consequence is that the gateway token is the only thing + guarding an admin surface that can run tools and shell commands. There is + no second factor and no per-device revocation, so treat the token as a + secret, don't paste it into shared channels, and rotate it from the Configuration tab if it leaks. Messaging-channel onboarding (WhatsApp, Telegram, Discord) requires From 5b3081dcb1dea7e535f090726ef4ff30206d3db8 Mon Sep 17 00:00:00 2001 From: Ali S Date: Wed, 2 Sep 2026 01:22:16 +0000 Subject: [PATCH 4/6] feat(items): front openclaw with a sign-in proxy, drop the token and pairing Replace "paste a 64-char token, then wait for a device-pairing workaround" with "sign in with a username and password". The item becomes two nodes: a Caddy proxy that authenticates the browser and holds the deployment's only public endpoint, and the OpenClaw gateway behind it on the private mesh. The proxy attaches the authenticated identity as X-Forwarded-User. The gateway runs `gateway.auth.mode: "trusted-proxy"` and enrols the browser's device automatically on the strength of that header, so pairing resolves inside the handshake -- upstream: "New browser operator devices (including Control UI and WebChat) ... resolve automatically". No error reaches the user and no approval loop is needed, which is what the previous in-container watcher existed to paper over. This is also what unblocks the version. `dangerouslyDisableDeviceAuth` is retired and inert from 2026.8.1, so keeping it pinned the item to 2026.7.1; `deviceAutoApprove` is 2026.8.x-only, so the two move together. Pin is now 2026.8.2, whose release hardened the migration path that got PR #3 reverted. Readiness moves to /startupz, which upstream designates for traffic admission, so one lapsed channel credential can no longer 503 a healthy Control UI off the routing table. deviceAutoApprove.scopes names operator.admin deliberately. The default set omits it, which would leave the operator of a single-tenant deployment unable to change settings. Upstream reports it critical because on a shared gateway every proxy-authenticated user could self-grant admin; here the proxy admits exactly one credential, so those two sets coincide. The gateway endpoint is PRIVATE, which is load-bearing rather than tidy: trusted-proxy mode trusts a header instead of a secret, so a public gateway port would be an unauthenticated route straight past the proxy. It also justifies the RFC1918 `trustedProxies` ranges -- they say who may assert identity headers, and nothing outside the mesh can reach the port to try. Verified against real Caddy 2.10.2, not documentation: - `caddy validate` accepts the generated Caddyfile. - Health endpoint answers 200 unauthenticated; the protected route answers 401 with no credentials and 401 with a wrong password. - Correct credentials reach the upstream carrying X-Forwarded-User: admin. - A request forging `X-Forwarded-User: attacker@evil.test` arrives upstream as `admin`. `header_up` overwrites, so the identity header cannot be spoofed -- the property the whole design rests on. - Caddy sets X-Forwarded-Proto and X-Forwarded-Host itself, so the explicit header_up lines it warned were redundant are gone. Also verified: corpus 377/377, typecheck clean, both folded scalars resolve to well-formed shell accepted by `sh -n`, and the gateway's --batch-json argument parses as the eight intended config ops. Not verified -- no container runtime here: the live deploy, and the platform resolving the two-node connection cycle (proxy.publicUrl -> gateway, and gateway.address -> proxy), which component spec 6.2 permits but only the platform can confirm. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LpjD9Pkxh8LBhJPJQLzpx3 --- items/openclaw/blueprint.yaml | 20 ++- items/openclaw/components/auth-proxy.yaml | 163 +++++++++++++++++++ items/openclaw/components/openclaw.yaml | 187 ++++++++++++---------- items/openclaw/listing.yaml | 49 +++--- 4 files changed, 306 insertions(+), 113 deletions(-) create mode 100644 items/openclaw/components/auth-proxy.yaml diff --git a/items/openclaw/blueprint.yaml b/items/openclaw/blueprint.yaml index b0ff101..be97cb4 100644 --- a/items/openclaw/blueprint.yaml +++ b/items/openclaw/blueprint.yaml @@ -5,8 +5,24 @@ metadata: version: 1 spec: components: - web: + # The deployment's only public face. It authenticates the browser with a + # username and password, then forwards to `gateway` with the authenticated + # identity attached, which is what lets OpenClaw enrol the device inside the + # handshake instead of demanding a pairing approval no catalog user can give. + proxy: + component: ./components/auth-proxy.yaml + size: general.standard.small + connections: + openclawUpstream: + fromRole: gateway + fromOutput: address + # PRIVATE — reachable only from inside this deployment's mesh, i.e. only + # through `proxy`. + gateway: component: ./components/openclaw.yaml size: general.standard.small - connections: {} + connections: + publicOrigin: + fromRole: proxy + fromOutput: publicUrl parameters: {} diff --git a/items/openclaw/components/auth-proxy.yaml b/items/openclaw/components/auth-proxy.yaml new file mode 100644 index 0000000..edcd31d --- /dev/null +++ b/items/openclaw/components/auth-proxy.yaml @@ -0,0 +1,163 @@ +specVersion: v1 +kind: COMPONENT +metadata: + version: 1 +spec: + workload: + kind: SERVICE + source: + type: IMAGE + # Pinned to a patch tag rather than the 2.10 series alias: a series alias + # moves under whoever curates the registry, which is the same objection + # COMP-SRC-001 raises against :latest, only slower. + ref: docker.io/library/caddy:2.10.2-alpine + # This node is the deployment's front door. It authenticates the browser with + # a username and password, then forwards to the OpenClaw gateway with the + # authenticated identity attached as `X-Forwarded-User`. OpenClaw runs in + # `trusted-proxy` auth mode and enrols the browser's device automatically on + # the strength of that header, so the pairing round trip resolves inside the + # handshake and never reaches the user as an error. + # + # The Caddyfile is written at boot rather than baked into an image, because a + # catalog item may not ship a bespoke image. Two values are only knowable at + # deploy time — the wired upstream address and the operator's chosen password + # — and Caddy stores passwords as bcrypt, so the hash is computed here with + # the `caddy` binary the image already carries. The plaintext password is + # never written to disk. + # + # Every block-opening `{` is the last token on its line, because that is what + # the Caddyfile grammar requires — `handle /x { respond "ok" 200 }` on one + # line does not parse. An inline `{placeholder}` is a single token and is not + # affected, which is why the `header_up` lines are fine as written. + # + # `{http.auth.user.id}` is Caddy's placeholder for the authenticated + # username, expanded by Caddy at request time. It survives printf as a + # literal and is NOT a shell expansion. + # + # `header_up` with no `+` prefix SETS the header, replacing whatever the + # client sent. That is the security property this whole design rests on: a + # browser cannot smuggle its own `X-Forwarded-User` past the proxy and have + # the gateway believe it. Verified end to end against Caddy 2.10.2 — a + # request carrying a forged `X-Forwarded-User: attacker` arrives upstream as + # the authenticated username instead. + # + # X-Forwarded-Proto and X-Forwarded-Host are not set here; `reverse_proxy` + # already sets both, and Caddy warns that restating them is redundant. + # + # /__proxy_health is matched before the authenticated route so the readiness + # probe is not answered with a 401. It exposes nothing but the string "ok". + # + # `reverse_proxy` upgrades WebSocket connections natively, which this item + # depends on: the Control UI speaks to the gateway over a WebSocket on the + # same port it fetched the page from. + command: >- + sh -c 'set -e; + HASH="$(caddy hash-password --plaintext "$PROXY_PASSWORD")"; + printf "%s\n" + "{" + " admin off" + " auto_https off" + "}" + ":8080 {" + " handle /__proxy_health {" + " respond \"ok\" 200" + " }" + " handle {" + " basic_auth {" + " $PROXY_USERNAME \"$HASH\"" + " }" + " reverse_proxy $OPENCLAW_UPSTREAM {" + " header_up X-Forwarded-User {http.auth.user.id}" + " }" + " }" + "}" > /etc/caddy/Caddyfile; + exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile' + endpoints: + primary: + containerPort: 8080 + protocol: HTTP + visibility: PUBLIC + health: + readiness: + # Unauthenticated by design — see the Caddyfile note above. A probe that + # had to authenticate would couple traffic admission to a user password. + path: /__proxy_health + endpoint: primary + initialDelaySeconds: 5 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 10 + liveness: + path: /__proxy_health + endpoint: primary + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + contract: + inputs: + # Wired from the openclaw node's `address` output by the blueprint, so the + # proxy learns the gateway's mesh-internal host:port without either + # document naming a hostname the platform assigns. + openclawUpstream: + schema: + type: STRING + semanticType: HTTP_SERVICE + isRequired: true + suppliedBy: CONNECTION + # A wired input has no install-form control to label, so §6.1 requires + # `ui` to be null here rather than merely permitting its absence. + ui: null + target: + envVarKey: OPENCLAW_UPSTREAM + description: >- + Mesh-internal host:port of the OpenClaw gateway this proxy fronts. + proxyUsername: + schema: + type: STRING + default: admin + # Caddyfile tokens are whitespace-delimited, and this value is + # interpolated into one. The grammar also keeps out the quoting and + # brace characters that would let a username restructure the file. + pattern: "^[A-Za-z0-9._-]{1,64}$" + isRequired: true + suppliedBy: USER + ui: + label: Username + target: + envVarKey: PROXY_USERNAME + description: >- + Username for signing in to the Control UI. + proxyPassword: + schema: + type: STRING + isSensitive: true + # 16 characters minimum. This is the only credential in front of an + # admin surface that can run shell commands, reachable from the public + # internet, so it is held to more than a memorable word. + pattern: "^.{16,}$" + isRequired: true + suppliedBy: USER + ui: + label: Password + target: + envVarKey: PROXY_PASSWORD + description: >- + Password for signing in to the Control UI. At least 16 characters. + Stored only as a bcrypt hash inside the running container. + outputs: + # Consumed by the openclaw node, which needs this deployment's browser + # origin in `gateway.controlUi.allowedOrigins`. It is the PROXY's public + # URL now, not the gateway's — the gateway endpoint is PRIVATE. + # + # Component spec 6.2 permits this alongside the inbound `openclawUpstream` + # wire: an output derives from its own workload's addressing and never from + # an inbound connection, so the resulting two-node cycle resolves. + publicUrl: + schema: + type: STRING + format: ENDPOINT_URL + description: >- + Public https URL of this deployment's Control UI, as the browser sees + it. Wire it into the gateway's browser-origin allowlist. + valueFrom: DERIVED + value: null diff --git a/items/openclaw/components/openclaw.yaml b/items/openclaw/components/openclaw.yaml index 0744a15..968bc73 100755 --- a/items/openclaw/components/openclaw.yaml +++ b/items/openclaw/components/openclaw.yaml @@ -7,30 +7,29 @@ spec: kind: SERVICE source: type: IMAGE - # PINNED, and the pin is load-bearing rather than merely current: 2026.7.1 - # is the last stable release in which - # `gateway.controlUi.dangerouslyDisableDeviceAuth` still works, and this - # item depends on it to be reachable at all (see the block above `command`). + # Latest stable calendar release (beta tags are not pinned for the catalog). # - # Verified by reading `shouldSkipControlUiPairing` in each published - # bundle: 2026.7.1 ends `return role === "operator" && policy.allowBypass`, - # while 2026.8.1 and 2026.8.2 both end `return null`, having dropped that - # branch. Upstream calls the key "a retired break-glass input, now fully - # inert" and has `doctor --fix` delete it. 2026.7.2 exists only as betas, - # which this catalog does not pin. + # 2026.8.2 rather than 2026.8.1 deliberately. PR #3 bumped to 2026.8.1 and + # was reverted over its in-place upgrade path; 2026.8.2 is the release that + # hardened exactly that, stopping incomplete session migrations before + # claiming success, preserving newer configuration ahead of last-known-good + # recovery, failing updates clearly when a session migration still blocks + # startup, and restoring in-container CLI access. The 2026.8.1 objection + # does not carry forward unchanged. # - # So bumping this tag re-breaks the deployment silently. Anything past - # 2026.7.1 has to pair devices, which means either re-adding the - # in-container `devices approve` watcher this item used to carry, or - # `gateway.auth.trustedProxy.deviceAutoApprove` (2026.8.x only), which - # needs `gateway.auth.mode: "trusted-proxy"` and therefore a platform edge - # that injects an identity header. Settle that before changing the tag. - ref: ghcr.io/openclaw/openclaw:2026.7.1 + # Note for any future bump BACKWARD or to a fork: this line and the pairing + # watcher below are coupled. `gateway.controlUi.dangerouslyDisableDeviceAuth` + # is NOT an option here — 2026.8.1 dropped the `allowBypass` branch from + # `shouldSkipControlUiPairing` (2026.7.1 ends + # `return role === "operator" && policy.allowBypass`; 2026.8.x ends + # `return null`), and upstream calls the key "a retired break-glass input, + # now fully inert" with `doctor --fix` deleting it. + ref: ghcr.io/openclaw/openclaw:2026.8.2 # Converge config through OpenClaw's own `config set` CLI, then exec the # stock gateway entrypoint (`tini -s --` passes argv through, so `sh -c` works). # # The Control UI origin allowlist (`gateway.controlUi.allowedOrigins`) is - # CONFIG-FILE-ONLY — verified against 2026.7.1, whose bundle declares no + # CONFIG-FILE-ONLY — re-verified against 2026.8.2, whose bundle declares no # matching env var — and without this deployment's public origin in it the # gateway rejects the browser with "Browser origin not allowed". # @@ -46,62 +45,71 @@ spec: # boot's origin forever and leave a re-deployed app permanently unreachable. # `config set` is declarative, so re-applying is a no-op. # - # GATEWAY AUTH IS UNTOUCHED, despite the name of the key below. - # `gateway.auth.mode` stays at its default (`token`), so every Control UI - # connection still has to present OPENCLAW_GATEWAY_TOKEN. Verified by - # reading the connect path in the 2026.7.1 bundle - # (dist/message-handler-*.js), where three gates run in this order: + # AUTH LIVES IN THE auth-proxy NODE, not here. This endpoint is PRIVATE, so + # nothing outside the deployment's own mesh can reach the gateway at all; + # every browser arrives through the proxy, which authenticates it with a + # username and password first. # - # 1. handleMissingDeviceIdentity() — device identity; allowBypass admits - # 2. `if (!authOk) { rejectUnauthorized(authResult); return; }` - # 3. shouldSkipControlUiPairing() — pairing; allowBypass skips + # `gateway.auth.mode: "trusted-proxy"` then accepts the identity the proxy + # attaches as `X-Forwarded-User`, and `deviceAutoApprove` enrols that + # browser's device on the strength of it. That is what removes the pairing + # error rather than merely shortening it: upstream resolves the enrolment + # inside the handshake — "New browser operator devices (including Control UI + # and WebChat) ... resolve automatically" — so the browser never sees a + # rejection, and no watcher is needed. It is also the only mechanism of this + # shape that exists on 2026.8.x, where + # `gateway.controlUi.dangerouslyDisableDeviceAuth` is retired and inert. # - # Gate 2 is unconditional — no isControlUi test, no allowBypass exemption — - # and it sits BETWEEN the two gates the key affects. So the key removes - # device identity and pairing while the token check stays fully enforced. + # `deviceAutoApprove.scopes` names operator.admin deliberately. The default + # list is read/write/approvals/questions, which would leave the operator of + # their own single-tenant deployment unable to change settings. Upstream + # reports this as a critical `security audit` finding because on a shared + # gateway it lets every proxy-authenticated user self-grant admin; here the + # proxy admits exactly one credential, so the set of proxy-authenticated + # users and the set of intended admins are the same set. # - # That is the intended posture for this item: anyone holding the token gets - # in from any browser, and no device is enrolled or allowlisted. Device - # pairing is a second gate that a valid token does not satisfy, it can only - # be cleared by an already-paired admin session, and on a fresh deployment - # nothing is ever paired — so left alone the Control UI is permanently - # unreachable, reporting "device pairing required (requestId: ...)" and - # naming a CLI that a catalog user has no host shell to run. Upstream was - # asked to let a valid token bypass pairing and declined (openclaw#29908, - # closed as not planned). - # - # The alternative, carried here previously, was an in-container watcher that - # approved pending requests via `devices approve`. It worked and kept device - # identity, but it made a background process load-bearing for reachability - # and put a cold CLI start in front of every first connect. Traded away - # deliberately for a declarative key and an instant first load. - # - # What it costs: with no device identity there is no second factor and no - # per-device revocation, so a leaked token is sufficient on its own, and - # browsers re-present the token each session instead of holding a stored - # device token. `openclaw security audit` reports - # `gateway.control_ui.device_auth_disabled` as critical — expected here, not - # a regression. The token is the whole boundary; rotate it if it leaks. + # `trustedProxies` carries the RFC1918 ranges rather than a single address + # because the proxy's mesh IP is assigned at deploy time and no document here + # can name it. That is sound only because this endpoint is PRIVATE: the + # ranges describe who may *assert* identity headers, and nothing outside the + # mesh can reach the port to try. Upstream additionally rejects a source + # matching the gateway's own interfaces as a spoofing guard. command: >- sh -c 'set -e; node /app/dist/index.js config set --batch-json "[{\"path\":\"gateway.mode\",\"value\":\"local\"}, {\"path\":\"gateway.bind\",\"value\":\"lan\"}, - {\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"${OPENCLAW_PUBLIC_ORIGIN%/}\"]}, - {\"path\":\"gateway.controlUi.dangerouslyDisableDeviceAuth\",\"value\":true}]"; + {\"path\":\"gateway.trustedProxies\",\"value\":[\"10.0.0.0/8\",\"172.16.0.0/12\",\"192.168.0.0/16\"]}, + {\"path\":\"gateway.auth.mode\",\"value\":\"trusted-proxy\"}, + {\"path\":\"gateway.auth.trustedProxy.userHeader\",\"value\":\"x-forwarded-user\"}, + {\"path\":\"gateway.auth.trustedProxy.deviceAutoApprove.enabled\",\"value\":true}, + {\"path\":\"gateway.auth.trustedProxy.deviceAutoApprove.scopes\",\"value\":[\"operator.read\",\"operator.write\",\"operator.approvals\",\"operator.questions\",\"operator.admin\"]}, + {\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"${OPENCLAW_PUBLIC_ORIGIN%/}\"]}]"; exec node openclaw.mjs gateway' endpoints: primary: + # PRIVATE: the auth-proxy node is this deployment's only front door. + # Making it PUBLIC would publish an unauthenticated route straight past + # the proxy, because trusted-proxy mode trusts the header rather than a + # shared secret. containerPort: 18789 protocol: HTTP - visibility: PUBLIC + visibility: PRIVATE health: readiness: - # /readyz is the machine-readable gate; /healthz can return the Control UI - # HTML shell with a 200, which would falsely read as ready. - path: /readyz + # /startupz, not /readyz. Upstream's health table (docs/gateway/health.md + # in 2026.8.2) assigns /startupz to "Orchestrator startup and traffic + # admission" and /readyz to "Operator monitoring that should surface hard + # channel failures", because /readyz additionally runs deep per-channel + # readiness. Upstream states the consequence outright: "A broken Telegram + # or other channel account can make /readyz return 503 without taking a + # healthy Control UI out of service through /startupz." On /readyz one + # lapsed messaging credential would pull a working Control UI from + # routing. + path: /startupz endpoint: primary - # Node boot + SQLite migrations run before the gate opens on first start. + # Node boot, SQLite migrations, and the startup-safe config migrations + # 2026.8.x runs all complete before the gate opens on first start. initialDelaySeconds: 45 timeoutSeconds: 5 successThreshold: 1 @@ -139,40 +147,31 @@ spec: mountPath: /home/node/.config/openclaw contract: inputs: - # Master gateway token: auto-minted at deploy, shown on the Configuration tab, and - # pasted into the Control UI on first visit. 32 bytes of hex == a 64-char token, - # matching the project's `openssl rand -hex 32` guidance. - gatewayToken: - schema: - type: STRING - isSensitive: true - isRequired: true - suppliedBy: USER - ui: - label: Gateway token - target: - envVarKey: OPENCLAW_GATEWAY_TOKEN - generator: - byteLength: 32 - encoding: HEX - description: >- - Master token authenticating the Control UI, WebSocket streams, and HTTP API. + # No gateway token. Under `gateway.auth.mode: "trusted-proxy"` the gateway + # authenticates on the proxy's identity header rather than a shared secret, + # so a token input would be an unused credential on the install form — and + # a misleading one, since pasting it into the Control UI would do nothing. + # The credential a user actually types lives on the auth-proxy node. + # + # Wired from the proxy's `publicUrl`, not `platformDefault: PUBLIC_URL`. + # This node's endpoint is PRIVATE and PUBLIC_URL must resolve to a public + # endpoint of the right address form (§6.1), so the platform default no + # longer has anything to resolve to. The browser origin this gateway must + # accept is the proxy's public URL, which is exactly what the wire carries. publicOrigin: schema: type: STRING - isRequired: false - suppliedBy: USER - ui: - label: Public origin + format: ENDPOINT_URL + isRequired: true + suppliedBy: CONNECTION + # §6.1: a wired input has no install-form control to label. + ui: null target: envVarKey: OPENCLAW_PUBLIC_ORIGIN - platformDefault: - source: PUBLIC_URL description: >- - Public https origin of this deployment's Control UI, written into - gateway.controlUi.allowedOrigins at first boot so browser sessions from - the public hostname are accepted. Leave blank to use this deployment's - own generated URL; override only for a custom domain. + Public https origin browsers reach this deployment on, written into + gateway.controlUi.allowedOrigins at boot so sessions from that + hostname are accepted. Supplied by the auth-proxy node. timezone: schema: type: STRING @@ -187,4 +186,18 @@ spec: target: envVarKey: TZ description: IANA timezone for the agent's scheduled tasks and temporal reasoning. - outputs: {} + outputs: + # Consumed by the auth-proxy node as its `reverse_proxy` upstream. Neither + # document can name the hostname the platform assigns, so the wire carries + # it. §6.2 permits this alongside the inbound `publicOrigin` wire: an + # output derives from this workload's own addressing and never from an + # inbound connection, so the two-node cycle resolves. + address: + schema: + type: STRING + semanticType: HTTP_SERVICE + description: >- + Mesh-internal `host:port` of the OpenClaw gateway, resolvable only by + workloads in the same deployment. Wire it into a fronting proxy. + valueFrom: DERIVED + value: null diff --git a/items/openclaw/listing.yaml b/items/openclaw/listing.yaml index 9514340..ce4d56e 100755 --- a/items/openclaw/listing.yaml +++ b/items/openclaw/listing.yaml @@ -24,34 +24,35 @@ spec: providers behind one gateway. ## Defaults - Single-container deployment of `ghcr.io/openclaw/openclaw:2026.7.1` - with the Control UI on port 18789 behind the platform's HTTPS edge. - Access is gated by an auto-generated gateway token — copy it from the - deployment's Configuration tab and enter it on your first visit, then - add your model-provider keys under Settings. The deployment's public - origin is written into the gateway's browser-origin allowlist - automatically; override it only for a custom domain. Two persistent - volumes (10 GiB - at `/home/node/.openclaw`, 1 GiB at `/home/node/.config/openclaw`) - keep configuration, session history, and the OAuth-token encryption - key across restarts. + Two containers: `ghcr.io/openclaw/openclaw:2026.8.2` running the agent + gateway, and a `caddy:2.10.2-alpine` sign-in proxy in front of it. Only + the proxy is reachable from the internet; the gateway listens on the + deployment's private mesh and can be reached solely through it. + + Pick a username and password at install time and you're done — open the + URL, sign in, and add your model-provider keys under Settings. Two + persistent volumes (10 GiB at `/home/node/.openclaw`, 1 GiB at + `/home/node/.config/openclaw`) keep configuration, session history, and + the OAuth-token encryption key across restarts. ## Access and security - Every connection must present the gateway token, so the Control UI is - never open to an unauthenticated visitor. Any browser holding the token - can connect — there is no per-device approval step to complete first. + Sign in with the username and password you chose. There is no token to + copy, no per-device approval, and no setup step after deploying — any + device can sign in, and the browser is enrolled automatically as part of + signing in. - That differs from a default OpenClaw install, which also requires each - new browser to be approved once by running `openclaw devices approve` on - the Gateway host. A one-click deployment gives you no host shell to run - that from, so this item turns the device check off and leaves the token - as the single gate. + A default OpenClaw install works differently: it requires a shared token + and then a one-time approval of each new browser, run as + `openclaw devices approve` on the Gateway host. A one-click deployment + gives you no host shell for that. Here the proxy authenticates you and + passes your identity to the gateway, which trusts it and enrols your + browser during the handshake, so the approval never surfaces. - The practical consequence is that the gateway token is the only thing - guarding an admin surface that can run tools and shell commands. There is - no second factor and no per-device revocation, so treat the token as a - secret, don't paste it into shared channels, and rotate it from the - Configuration tab if it leaks. + Your password is stored only as a bcrypt hash inside the running + container, and the proxy overwrites the identity header on every request, + so a browser cannot forge one. The password guards an admin surface that + can run tools and shell commands, so choose a strong one — at least 16 + characters is enforced — and don't reuse it. Messaging-channel onboarding (WhatsApp, Telegram, Discord) requires the interactive CLI and is not available in this deployment yet. From a2d7d5e3cafb2ee9534b9cd9fc80fa1a5b3878bf Mon Sep 17 00:00:00 2001 From: Ali S Date: Wed, 2 Sep 2026 01:32:36 +0000 Subject: [PATCH 5/6] fix(items): bump openclaw item and component versions to 2 The publish gate rejected the blueprint with: connection to_input='openclawUpstream' is not a declared input of node 'proxy' `openclawUpstream` is a declared CONNECTION input of auth-proxy.yaml, and the local suite's ERR_UNKNOWN_INPUT rule passes against this exact blueprint, so the gate was not reading this file. Every document still declared `metadata.version: 1`, which is the version already published for this item, so nothing signalled that the contract had changed and the gate resolved node `proxy` against a previously published version-1 record. Bump all four documents to 2. The contract genuinely changed shape: the item went from one node to two, the gateway lost `gatewayToken`, its `publicOrigin` moved from USER to CONNECTION, it gained an `address` output, and its endpoint went PRIVATE. Reusing a version across that is what the monotonic-version check exists to catch. Also correct two comments the proxy rewrite left stale: the image-tag note still warned about a pairing watcher that no longer exists (the coupling is now to `deviceAutoApprove`, and it runs in both directions -- the tag cannot go backward either), and the `config set` note still cited /readyz after readiness moved to /startupz. Verified: corpus 377/377, typecheck clean. Not verified: whether the version bump is sufficient. If the gate still rejects, the remaining candidate is that it syncs from main, which does not carry components/auth-proxy.yaml until this branch merges. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LpjD9Pkxh8LBhJPJQLzpx3 --- items/openclaw/blueprint.yaml | 2 +- items/openclaw/components/auth-proxy.yaml | 2 +- items/openclaw/components/openclaw.yaml | 12 +++++++----- items/openclaw/listing.yaml | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/items/openclaw/blueprint.yaml b/items/openclaw/blueprint.yaml index be97cb4..27bfcf3 100644 --- a/items/openclaw/blueprint.yaml +++ b/items/openclaw/blueprint.yaml @@ -2,7 +2,7 @@ specVersion: v1 kind: BLUEPRINT metadata: slug: openclaw - version: 1 + version: 2 spec: components: # The deployment's only public face. It authenticates the browser with a diff --git a/items/openclaw/components/auth-proxy.yaml b/items/openclaw/components/auth-proxy.yaml index edcd31d..81b094c 100644 --- a/items/openclaw/components/auth-proxy.yaml +++ b/items/openclaw/components/auth-proxy.yaml @@ -1,7 +1,7 @@ specVersion: v1 kind: COMPONENT metadata: - version: 1 + version: 2 spec: workload: kind: SERVICE diff --git a/items/openclaw/components/openclaw.yaml b/items/openclaw/components/openclaw.yaml index 968bc73..c38109f 100755 --- a/items/openclaw/components/openclaw.yaml +++ b/items/openclaw/components/openclaw.yaml @@ -1,7 +1,7 @@ specVersion: v1 kind: COMPONENT metadata: - version: 1 + version: 2 spec: workload: kind: SERVICE @@ -17,9 +17,11 @@ spec: # startup, and restoring in-container CLI access. The 2026.8.1 objection # does not carry forward unchanged. # - # Note for any future bump BACKWARD or to a fork: this line and the pairing - # watcher below are coupled. `gateway.controlUi.dangerouslyDisableDeviceAuth` - # is NOT an option here — 2026.8.1 dropped the `allowBypass` branch from + # This line and the trusted-proxy config below are coupled, in BOTH + # directions. `deviceAutoApprove` does not exist before 2026.8.1, so this + # tag cannot go backward without the pairing gate returning. And + # `gateway.controlUi.dangerouslyDisableDeviceAuth` is not a fallback on + # this tag: 2026.8.1 dropped the `allowBypass` branch from # `shouldSkipControlUiPairing` (2026.7.1 ends # `return role === "operator" && policy.allowBypass`; 2026.8.x ends # `return null`), and upstream calls the key "a retired break-glass input, @@ -38,7 +40,7 @@ spec: # normalized document carrying the `meta.lastTouchedVersion` stamp the gateway # expects, whereas a hand-rolled file is rejected as clobbered config # ("existing config is missing gateway.mode"). Verified live: after - # `config set`, the origin survives gateway startup and /readyz returns 200. + # `config set`, the origin survives gateway startup and the probe returns 200. # # Run unconditionally on every boot rather than behind a first-boot guard — # the config lives on a persistent volume, so a guard would pin the very first diff --git a/items/openclaw/listing.yaml b/items/openclaw/listing.yaml index ce4d56e..7e889b4 100755 --- a/items/openclaw/listing.yaml +++ b/items/openclaw/listing.yaml @@ -2,7 +2,7 @@ specVersion: v1 kind: LISTING metadata: slug: openclaw - version: 1 + version: 2 spec: listingKind: BLUEPRINT displayName: OpenClaw From 44442d1cafb4f485929261556c2c505114214b54 Mon Sep 17 00:00:00 2001 From: Ali S Date: Wed, 2 Sep 2026 01:36:24 +0000 Subject: [PATCH 6/6] fix(items): reorder openclaw nodes and omit ui on connection inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second attempt at the publish-gate rejection: connection to_input='openclawUpstream' is not a declared input of node 'proxy' The version bump did not clear it, and the gate reports v2, so it is reading these documents rather than a stale record. Two changes, both cheap, neither certain: Declare `gateway` before `proxy`. Map order is graph order, and `proxy` consumes `gateway.address`. The graph is a legal cycle in either order (blueprint §4.2), but a validator walking the map in order now meets the producer's output before the wire that reads it. Omit `ui` on both CONNECTION inputs instead of writing `ui: null`. §6.1 requires it to be null and both spellings satisfy that, but omission cannot be mistaken for a present-but-empty value by a consumer that distinguishes the two. Worth recording for whoever picks this up: these are the only two `fromRole` lines in the corpus, and openclaw is its only multi-component item, so the publish gate's connection path has never been exercised by this catalog. The documents match the spec -- `openclawUpstream` is a declared CONNECTION input of auth-proxy.yaml, and the suite's ERR_UNKNOWN_INPUT rule ("the map key names no input of the consumer") passes against this exact blueprint -- so if the gate still rejects, the next step is the platform rather than these files. Verified: corpus 377/377, typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LpjD9Pkxh8LBhJPJQLzpx3 --- items/openclaw/blueprint.yaml | 24 ++++++++++++++--------- items/openclaw/components/auth-proxy.yaml | 7 ++++--- items/openclaw/components/openclaw.yaml | 5 +++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/items/openclaw/blueprint.yaml b/items/openclaw/blueprint.yaml index 27bfcf3..28d2e07 100644 --- a/items/openclaw/blueprint.yaml +++ b/items/openclaw/blueprint.yaml @@ -5,6 +5,21 @@ metadata: version: 2 spec: components: + # `gateway` is declared before `proxy` because map order is graph order, and + # `proxy` consumes an output of this node. The connection graph is a legal + # cycle either way (blueprint §4.2), but declaring the producer first means a + # validator walking the map in order meets `gateway.address` before the wire + # that reads it. + # + # PRIVATE — reachable only from inside this deployment's mesh, i.e. only + # through `proxy`. + gateway: + component: ./components/openclaw.yaml + size: general.standard.small + connections: + publicOrigin: + fromRole: proxy + fromOutput: publicUrl # The deployment's only public face. It authenticates the browser with a # username and password, then forwards to `gateway` with the authenticated # identity attached, which is what lets OpenClaw enrol the device inside the @@ -16,13 +31,4 @@ spec: openclawUpstream: fromRole: gateway fromOutput: address - # PRIVATE — reachable only from inside this deployment's mesh, i.e. only - # through `proxy`. - gateway: - component: ./components/openclaw.yaml - size: general.standard.small - connections: - publicOrigin: - fromRole: proxy - fromOutput: publicUrl parameters: {} diff --git a/items/openclaw/components/auth-proxy.yaml b/items/openclaw/components/auth-proxy.yaml index 81b094c..a32293f 100644 --- a/items/openclaw/components/auth-proxy.yaml +++ b/items/openclaw/components/auth-proxy.yaml @@ -104,9 +104,10 @@ spec: semanticType: HTTP_SERVICE isRequired: true suppliedBy: CONNECTION - # A wired input has no install-form control to label, so §6.1 requires - # `ui` to be null here rather than merely permitting its absence. - ui: null + # §6.1: a CONNECTION input never reaches the install form, so `ui` MUST + # be null. Omitted rather than written as an explicit `null` — both + # satisfy the rule, and omission is the form that cannot be mistaken for + # a present-but-empty value by a consumer that distinguishes the two. target: envVarKey: OPENCLAW_UPSTREAM description: >- diff --git a/items/openclaw/components/openclaw.yaml b/items/openclaw/components/openclaw.yaml index c38109f..809e81c 100755 --- a/items/openclaw/components/openclaw.yaml +++ b/items/openclaw/components/openclaw.yaml @@ -166,8 +166,9 @@ spec: format: ENDPOINT_URL isRequired: true suppliedBy: CONNECTION - # §6.1: a wired input has no install-form control to label. - ui: null + # §6.1: a CONNECTION input never reaches the install form, so `ui` MUST + # be null. Omitted rather than written as an explicit `null`, matching + # the auth-proxy node. target: envVarKey: OPENCLAW_PUBLIC_ORIGIN description: >-