Skip to content

fix(compute): preserve native Gateway state across restarts - #3041

Merged
freeqaz-openai merged 2 commits into
mainfrom
openclaw/fix-native-gateway-state-20261020
Oct 21, 2026
Merged

freeqaz-openai merged 2 commits into
mainfrom
openclaw/fix-native-gateway-state-20261020

Conversation

@roboclaw-bot

@roboclaw-bot roboclaw-bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A dedicated native Gateway rejects the worker profile that its own wrapper generated when the container restarts. Gateway Pod replacement also discards the client credential store while retaining the database whose migration receipts refer to that store.

This extracts Sally’s commit 5ed73046b9808e4759e47c33d736d97eb7cc0166 from PR #1982, as requested. It is based directly on main, not on #1982.

Relationship to #1982

Sally’s local k3d testing reports that #1982 alone fails and needs this fix. This PR is therefore a functional prerequisite for #1982’s native e2e qualification, while remaining separately reviewable and based on main. Sally’s completed combined native proof now reports 1 passed, 0 failed/skipped. Its #1982-only control failed after Gateway replacement with the client migration error, while the combined candidate passed. This identifies the client-persistence dependency; the exact-profile restart repair has its own regression evidence. Qualification and landing order is #3041, then #1982. Retained-state and foreign/revoked-authority proof requirements remain in force.

Why native testing exposed this

The source-backed investigation distinguishes two causes. Native k3d defaults to building a sibling OpenClaw checkout; the passing VM used 4c7a1654 with upstream #174599’s separate client credential store. OCE’s default f8386bc7 image stores this material in its already-persistent shared-state database. With the new runtime, retaining only state/ loses client/credentials.sqlite on replacement, and the runtime refuses that inconsistent state. Codex is not immune on the new layout; no same-image Codex replacement reproduction is claimed.

The second fix is native-specific: only the dedicated native worker receives the generated worker-profile setting. Codex does not execute that branch. A native container restart can read its generated writable configuration and incorrectly reject the profile it just generated; exact equality now permits that case without admitting conflicting settings.

The new client store includes device private identities, role/Gateway-origin token caches, native execution socket credentials and migration receipts. These are sensitive contents, but equivalent local identity/token material was already persistent in the older shared-state layout. Server-side paired-device authority is distinct; source ownership does not substitute for revocation/replacement proof.

Change

  • Mount the Agent’s private Gateway PVC client subdirectory at /home/node/.openclaw/client beside retained Gateway state. The client store and its migration receipt survive Pod replacement.
  • Reuse a native worker profile only when it deeply equals the complete profile generated for the configured device. Different devices or settings still fail startup.
  • Extend the existing Compute conformance and runtime-image startup tests to exercise exact-profile restart and conflicting-device rejection.
  • Update the native admin flow and storage reference. Trim repeated prose so the extracted documentation does not carry the word-limit failure from test(openshell): exercise native provider authority #1982. No Driver interface, image version, UI or schema change is included.

Verification

Current head: 97ecaacb07449e8332da9fc3f910af0f692c8409. Merged main e8185dac805b737363c7178d3068b310a53a7c24 without rewriting history. Resolved the native-admin flow conflict by retaining main’s session-expiry return flow and both histories; removed repeated authorization prose while preserving Manual Notes. The production repair and regression code are unchanged from 06ae1e0e1. Refreshed focused conformance: 2 passed, 0 failed/skipped; full ESLint, repository-wide Prettier, workspace/flow/whitespace checks passed. Independent P0–P3 source review found no actionable findings. New-head hosted CI is pending; earlier results below stay tied to their recorded head.

  • Existing targeted Compute conformance cases: 2 passed, 0 failed/skipped, using the candidate source and the already-prepared dependency graph. The same new restart case fails against the unchanged main implementation with “Dedicated OpenClaw worker profile conflicts with admitted configuration.” It passes with this patch.
  • Full ESLint, changed-file Prettier, source syntax, Git whitespace and flow-document validation passed. Manual Notes are unchanged. The two pages remain together because each describes one coherent existing workflow/reference; hosted docs checks will confirm the hard limit.
  • Independent Codex P0–P3 source review found no actionable findings. This is not live runtime or credential-retention qualification.
  • The existing real runtime-image integration contains the restart assertions, but was not run locally: this host has no prepared Docker/image fixture. The new worktree also has no installed dependency graph; no dependencies were installed for this verification.
  • Required CI passed at 06ae1e0e13b7517abaa933911d367ac8c8fa2336. Hosted docs checks report 2,489 words for the flow and 2,494 for the storage reference; both pass the 2,500-word limit. Each remains one coherent existing workflow/reference, within the review band.
  • The runtime-image result artifact records executing merge SHA ebab02221eb2ccd9d04775d4e8bd2980aeb3ce8b and a pass for “runtime image validates the configuration dedicated native OpenClaw renders.” This executes both entrypoints, exact-profile Gateway restart and conflicting-device rejection in the selected image. Its Harness model probe is substituted in a credential-free, networkless container: this is startup/schema proof, not real model execution or existing-Agent replacement proof.
  • Combined fresh Linux/k3d native proof passed, with the tested repairs, source/image receipts and two disclosed test adapters. The four changed production/regression file blobs match between tested repair 06ae1e0e1 and 97ecaacb0; this is not an exact-head execution of the newer full tree. That earlier run did not qualify authority across a further replacement; the follow-up below now supplies that specific evidence. General cross-version/storage compatibility remains outside the proof. No general cross-version migration success is claimed.

Gateway replacement authority proof

Additional fresh Linux/k3d qualification passed: 1 passed, 0 failed, 0 skipped. Replaced both Gateway Pods on their same PVCs after recording real worker credentials and revoking one node. Prior client identities and complete migration receipts survived; the valid peer continued an existing session and read its existing workspace. Retained revoked credentials and foreign-Agent credentials were causally rejected after replacement. The revoked workspace write did not land and the denied model nonce did not reach retained Harness state; the healthy peer completed another real turn. Both Harnesses stayed unchanged.

The comment includes the exact revisions, image digests, sanitized assertion transcript, and complete local test-only qualification patch. Tested combined source remains e583b67b5 (#1982 plus this PR's 06ae1e0e1 repair), using OpenClaw 4c7a1654; this is not an exact-head run of 97ecaacb0. It qualifies the exercised current-layout native flow, not general cross-version migration, every credential table, embedded/Codex execution, OpenShift, or all storage classes. Changed-file lint/format, syntax and whitespace checks passed. No additional production changes or authority bypasses were used.

Risks

Persisting the client directory extends its contents’ lifetime across Gateway replacement. The authority-bearing contents and runtime-version difference are identified in the source investigation above. The additional real-runtime proof above now qualifies valid prior-state loading and rejection of revoked and foreign-Agent authority across Gateway replacement for the selected native flow, with exact source revisions and image digests. General cross-version migration, other credential categories and storage backends remain outside this evidence; review must assess those scope limits. The local startup regression alone does not establish these outcomes.

This PR keeps those proof obligations separate from #1982’s test-only native authority coverage. No merge or environment-policy change is requested by publication.

Contributor: @sallyom (original implementation). Extraction, documentation cleanup and verification by @roboclaw-bot.

Work sessions


View the OpenClaw team session

Extract the persistence and generated-worker-profile restart repair from PR #1982, with its original tests and focused documentation. Trim the owning pages below the documentation word limit.

Original-commit: 5ed7304

Signed-off-by: sallyom <somalley@redhat.com>

Co-authored-by: sallyom <11166065+sallyom@users.noreply.github.com>
Signed-off-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
@clawsweeper

clawsweeper Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-environment Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-environment Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. labels Oct 20, 2026
@clawsweeper

clawsweeper Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes

This PR retains Gateway client credentials on the existing private PVC and permits reuse of an identical generated native worker profile.

Example: A dedicated native Gateway restarts with its generated dedicated-native profile.

  • Before: Startup rejects the existing profile as conflicting with admitted configuration.
  • After: Startup accepts the identical profile and still rejects a different worker device.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused, correct repair supported by real replacement and authority evidence with clearly bounded combined-candidate coverage.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (logs): The published Linux/k3d run exercises real OCE/OpenShell provisioning, Gateway replacement on unchanged PVCs, retained client identities and receipts, valid-session continuation, and revoked/foreign final-effect denial. It executes combined source e583b67b506ce3e6a7a0889003a02f4688db44d0 with OpenClaw 4c7a1654f8f4a1dd2ecab4c421419f1093bc2d65; this head preserves the tested repair files. The previous upgrade concern is resolved as a source-verified no-migration case for this patch: the unchanged pinned runtime uses the unchanged retained shared-state database, and no existing rows or schema are rewritten. This does not qualify a separate cross-version runtime upgrade.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Product

Kind: Bug fix · Worth it: Yes · Fix scope: Complete
User problem: A native Gateway can fail after restart or replacement despite retaining its other Agent state.
Reason: The focused repair restores restart continuity within the existing Agent storage boundary and has contributor and collaborator-backed runtime evidence.

Merge readiness

✅ Ready for maintainer review

Keep this PR open: current main still needs the restart repair, and review found no actionable introduced defect.

Priority: P2
Reviewed head: 97ecaacb07449e8332da9fc3f910af0f692c8409

Before merge

None.

Findings

None.

Agent review details

How this fits together

Kubernetes Compute renders Agent-owned Gateway storage and startup programs; the Gateway loads retained state and routes workspace and model operations to its dedicated Harness.

flowchart TB
  A[Agent configuration] --> B[Kubernetes Compute]
  B --> C[Gateway startup wrapper]
  B --> D[Private Gateway PVC]
  D --> C
  C --> E[Gateway]
  E --> F[Dedicated Harness]
Loading

Technical review

Best possible solution:

Retain the client store through the existing private-storage owner and preserve exact generated-profile equality at startup.

Do we have a high-confidence way to reproduce the issue?

Current main rejects an already-generated worker profile unconditionally; the published native control run independently records client-store failure after Gateway replacement.

Is this the best way to solve the issue?

The patch uses existing storage and startup owners, preserves conflicting-profile rejection, and avoids a parallel persistence or authorization mechanism.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against bca5b199b037.

Provenance checked

  • Generated native worker profile keeps the original intent (10af5b8: The dedicated native Harness implementation requires Agent-owned worker placement; subsequent commit 821fa4b makes that profile mandatory.)

Testing

Proof path: shipped entry point.

Security

None.

Evidence

What I checked:

  • Introduced repair: The pinned introduction diff adds the client directory to the existing private-state inventory and replaces unconditional profile rejection with complete deep equality; it introduces no image pin, schema, permission, or public Driver interface change. (apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts:2347, 97ecaacb0744)
  • Current main still rejects profile reuse: The fetched main implementation still rejects every existing generated profile, including its own unchanged output. (apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts:2342, bca5b199b037)
  • Replacement and final-effect proof: Sally's full qualification comment reports one passing Linux/k3d native run, unchanged PVC identities and client-store digests, continued valid sessions, rejected revoked/foreign credentials, and no denied workspace or model effect. The complete test-only qualification patch was inspected: fix(compute): preserve native Gateway state across restarts #3041 (comment).
  • Proof applicability and disclosed adapters: The tested combined candidate contains this repair plus test(openshell): exercise native provider authority #1982. The repair's production and regression files are unchanged between the tested repair and this head. The disclosed adapters remove an unused Codex credential prerequisite and select a rootless loopback relay port without replacing runtime authority enforcement. (97ecaacb0744)
  • Existing storage owner and unchanged runtime: The existing mount and init-container owners consume the same directory inventory, preparing the new client subdirectory with private permissions. Existing state mounts and PVC identity remain unchanged; deploy/runtime/Dockerfile retains OpenClaw f8386bc7df0a6c9eda7a28ae81943c5c06a414ff. (apps/controller/src/drivers/compute/kubernetes/index.ts:12843, 97ecaacb0744)
  • Dependency contract and no-migration case: Dependency inspection applies because the Gateway executes this runtime and proof depends on its credential contract. At the unchanged OCE pin, identities and authentication use shared state/openclaw.sqlite; its complete tree has no client-credential modules. Adding the client mount does not move or transform those existing rows. The dependency's full root AGENTS.md was also read at the proof revision. (src/state/openclaw-state-db.paths.ts:34, f8386bc7df0a)

Likely related people:

  • sallyom: Raw commit 10af5b8 adds apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts:1663 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 10af5b83320f; files: apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts)
  • freeqaz: Raw commit 0911c22 adds apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts:1865 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 0911c229bf73; files: apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts)
  • Kimi Yu: Raw commit dd344a9 adds docs/reference/drivers/kubernetes-compute/storage-and-credentials.md:34 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: medium; commits: dd344a97c149; files: docs/reference/drivers/kubernetes-compute/storage-and-credentials.md)

Labels

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.
  • remove merge-risk: 🚨 compatibility: Current PR review selected no merge-risk labels.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P2: A focused Kubernetes restart repair protects dedicated native Gateway availability and retained client state.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.
  • proof: sufficient: Contributor real behavior proof is sufficient.

Rating scale

6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior.

Workflow

ClawSweeper edits this one comment on every review. Comment @clawsweeper re-review for a fresh review only. Repair and merge execution lanes are retired.

History

Review history (2 earlier review cycles)
  • reviewed 2026-10-20T15:13:57.522Z sha 06ae1e0 :: needs real behavior proof before merge. :: none
  • reviewed 2026-10-20T22:57:35.712Z sha 97ecaac :: blocked before merge. :: none

Reviewed October 20, 2026, 7:03 PM ET / 23:03 UTC (Revision 3).

Preserve main native-admin return-flow documentation while keeping the client-state and exact-worker-profile fixes separately reviewable for PR #1982 qualification.

Co-authored-by: sallyom <11166065+sallyom@users.noreply.github.com>
Signed-off-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
@sallyom
sallyom marked this pull request as ready for review October 20, 2026 19:58
@sallyom
sallyom requested a review from a team as a code owner October 20, 2026 19:58
@sallyom

sallyom commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Fresh Linux/k3d native OpenClaw + OpenShell e2e passed: 1 passed, 0 failed, 0 skipped with this persistence/startup repair applied to #1982.

Tested revisions: #3041 repair 06ae1e0e13b7517abaa933911d367ac8c8fa2336 + #1982 73c39aed60cab99aa642b282ed99b74e48ce4b2a, combined as e583b67b506ce3e6a7a0889003a02f4688db44d0. OpenClaw source 4c7a1654f8f4a1dd2ecab4c421419f1093bc2d65; OpenShell CLI/chart 0.1.4; Podman 5.6.0; k3s v1.36.4-k3s1. Used an existing authorized OpenAI credential with gpt-6-luna.

./scripts/k3d test --harness openclaw

Completed 2026-10-20T20:17:34Z. A separate fresh #1982-only run reached a successful native model probe and connected worker, then failed after Gateway Pod replacement with “Client credential migration is incomplete or stale.” Applying this repair allowed provisioning through that point and the entire native authority test to pass.

The passing run exercised successive real model turns and workspace reads/writes for two Agents, cross-Agent enrollment/device rejection, natural setup-token expiry with a fresh positive control, node revocation, refused workspace/model operations, and a healthy peer. The security operations also assert unchanged Gateway/Harness Pod identities and restart counts. This does not claim arbitrary existing-Agent migration coverage or revocation persistence across another Pod replacement.

Qualifications: two disclosed local test-only adapters were required: native-only preparation omits the unused Codex credential requirement, and the rootless Linux TLS relay uses an exact high loopback port. Authority and secret-exposure assertions were retained. NetworkPolicies remained enabled with the legacy iptables backend. The native runtime image was reused by immutable reference; the controller was rebuilt from the combined candidate. Full sanitized image/source receipts and both adapter diffs are in the detailed #1982 proof.

Focused Compute regressions: 2 passed, 0 failed/skipped. Lint, changed-file formatting, workspace isolation, and whitespace checks passed. docs:check could not reach content validation because the installed graph lacked docs-only dependency @sindresorhus/slugify.

The current #3041 head is 97ecaacb07449e8332da9fc3f910af0f692c8409; its Compute persistence/startup files are unchanged from the tested repair, but this is not an exact-head run for that newer commit. This proves the combined native scenario on fresh Linux/k3d, not OpenShift, all storage classes, or a reproduced Codex failure. Disposable test clusters/databases were removed afterward; built images were preserved.

Sanitized native e2e assertion transcript
OpenShell integration: selected real provider-delivery proof.
OpenShell integration: initial Compute prepareRevision retry: The Agent Gateway route answered HTTP 503 to the connection upgrade.
OpenShell integration: provider Harness ready; checking ownership and mounts.
OpenShell integration: approved mounts and privileges verified; checking secret exposure.
OpenShell integration: secret exposure checks passed; verifying gateway routing.
OpenShell integration: verifying dedicated native workspace file access.
OpenShell integration: starting successive native worker sessions through Gateway.
✔ OpenShell enforces native provider delivery and Gateway authority (972265.600983ms)
ℹ native-authority {"stage":"positive-native-workspace-and-successive-turns","agents":2}
ℹ native-authority {"stage":"cross-agent-setup-and-signed-device-denied","setupDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_BOOTSTRAP_TOKEN_INVALID"},"deviceDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_DEVICE_TOKEN_MISMATCH"}}
ℹ native-authority {"stage":"waiting-for-real-setup-expiry","waitMs":424886}
ℹ native-authority {"stage":"expired-setup-denied-fresh-setup-admitted","expiryDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_BOOTSTRAP_TOKEN_INVALID"}}
ℹ native-authority {"stage":"native-node-revoked","connected":false,"revokedAtMs":1792527318927}
ℹ native-authority {"stage":"revoked-workspace-operation","method":"GET","status":503}
ℹ native-authority {"stage":"revoked-workspace-operation","method":"PUT","status":503}
ℹ native-authority {"stage":"revoked-retained-model-operation","status":500}
ℹ native-authority {"stage":"revocation-complete-peer-still-working"}
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 973968.976695
Dedicated native OpenClaw with OpenShell integration passed.

@roboclaw-bot

Copy link
Copy Markdown
Contributor Author

Why the native k3d run needs this repair, while default Codex/OpenShell testing did not expose it

The two changes in #3041 have different causes. I inspected the OCE code at 97ecaacb07449e8332da9fc3f910af0f692c8409, the default OpenClaw pin f8386bc7df0a6c9eda7a28ae81943c5c06a414ff, and the VM proof’s OpenClaw source 4c7a1654f8f4a1dd2ecab4c421419f1093bc2d65. This was source analysis of those exact versions, not another e2e execution.

1. Client persistence: a runtime-layout mismatch, not a native-only credential directory

The native k3d helper selects the sibling OpenClaw source checkout. Without that selector, preparation builds the OCE runtime Dockerfile, whose default pin is f8386bc7. The native selector builds the chosen upstream checkout instead; the build branches differ here.

At the default pin, device identities and device-auth operations use the shared OpenClaw state database, under state/openclaw.sqlite. OCE already persists that directory. The complete tree at this pin has no client-credential-* files.

The VM source includes upstream #174599, commit 8f622108e0003e722801b420187b6011e0a4536e, which moved local client identities/tokens/IPC credentials into a separate owner. Its path resolver selects $OPENCLAW_STATE_DIR/client/credentials.sqlite.

Before #3041, OCE retained state/ but left client/ in the Pod-local home. Therefore, after replacement:

  1. The existing Gateway state database survives.
  2. The separate client store and its completed migration receipt disappear.
  3. The new runtime explicitly refuses a missing client store when the shared database exists, producing “Client credential migration is incomplete or stale.” It does not silently invent replacement credentials.

That matches Sally’s actual #1982-only failure: the native model probe was READY and the worker connected; the failure occurred after Gateway Pod replacement. The combined #1982 + #3041 run passed. This observation specifically supports retaining the new client store; it does not independently prove that both repairs were triggered in that same VM attempt.

Why this was not seen in default Codex/OpenShell runs: they do not, by default, select that sibling source checkout, so they do not exercise the same client-store layout. There is also a lifecycle difference: eligible Codex deployments use a mutable workspace-node binding, while dedicated native deployments exclude that path and deliver the worker identity/profile through the Gateway Pod spec. This makes Gateway replacement during native enrollment relevant sooner. Codex is not inherently immune: a Gateway using the new client-store runtime can lose it on replacement too if only state/ persists. No same-image Codex replacement reproduction is claimed here.

The new client schema contains device identities/private keys, role and Gateway-origin device tokens, native execution socket credentials, and the migration receipt. It is not merely a harmless cache. However, local identity/token custody was already persistent in the older shared-state layout. This mount preserves that continuity across the upstream storage split; it is more precise than describing it as entirely new credential persistence. Server-side paired-device authorization/revocation remains a separate owner; this source distinction does not replace final-effect/replacement proof.

2. Worker-profile restart: genuinely native-specific

Only dedicated native OpenClaw receives OPENCLAW_NATIVE_WORKER_PROFILE. The startup function returns immediately when it is absent, so Codex does not execute this profile-generation branch.

For a native Gateway that reuses its writable configuration after a container restart, the old wrapper rejects its own existing generated profile. #3041 permits deep equality with the complete expected profile but still rejects conflicting device/settings. The previously recorded base-fails/head-passes conformance comparison and real-image startup/schema case support this independent repair. A fresh-start-only test does not exercise reuse of generated configuration.

Evidence limits and disposition

Sally’s combined Linux/k3d proof is now available: one pass, no failures/skips, with the source/image receipts and two disclosed test-only adapters. It targets #3041’s 06ae1e0e1 repair plus #1982’s 73c39aed6 on upstream 4c7a1654, not an exact-head run of 97ecaacb0 or the default OCE runtime pin. Its security operations keep Pod identities unchanged after the observed provisioning replacement; arbitrary existing-state/storage compatibility and revocation across a further replacement remain unqualified.

This also separates the VM’s later persistence failure from hosted CI’s earlier native authentication 401 and Codex WebSocket 502. The storage diagnosis does not establish the cause of those hosted failures. No code, credentials, image pins, assertions or review holds were changed during this investigation.

@sallyom

sallyom commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Gateway replacement authority qualification: PASS

The requested native runtime replacement test now passes: 1 passed, 0 failed, 0 skipped. This extends the earlier combined e2e, which deliberately kept Pods unchanged during the authority assertions.

  • Provisioned two independent native OpenClaw Agents through OCE and OpenShell, with successive real model turns and workspace reads/writes.
  • Captured the real workers’ signed-device credentials before revocation, then revoked Agent A’s node token through the Gateway administrative RPC. Existing expiry and cross-Agent checks remained enabled.
  • Deleted both Gateway Pods and waited for new, ready Pod UIDs. Asserted the same PVC object UIDs and PV bindings; neither Harness was replaced or restarted.
  • Compared the Gateway client stores before/after replacement: retained identity/key material and migration receipts matched, with both receipts complete. The exercised Gateway stores each contained one identity and zero unscoped role-token rows; the comparison also covered origin-scoped token caches. This does not claim to populate every credential table.
  • After replacement, Agent B read its previously written workspace, continued its existing session with a fresh real model response, and accepted another workspace write.
  • Replayed A’s actual pre-revocation credential at replaced Gateway A: AUTH_DEVICE_TOKEN_MISMATCH. Replayed it at B, and B’s still-valid credential at A: the same causal authentication rejection. The revoked node’s retained pairing record remained describable but disconnected.
  • A’s workspace GET/PUT remained HTTP 503 and its retained-session model operation remained HTTP 500. Direct Harness inspection confirmed the workspace file was unchanged and the fresh denied request nonce never reached its retained worker state. B completed another real turn afterward.

Revisions and environment: #3041 repair 06ae1e0e13b7517abaa933911d367ac8c8fa2336 plus #1982 73c39aed60cab99aa642b282ed99b74e48ce4b2a, combined source e583b67b506ce3e6a7a0889003a02f4688db44d0; OpenClaw 4c7a1654f8f4a1dd2ecab4c421419f1093bc2d65; OpenShell CLI/chart 0.1.4; rootless Podman 5.6.0; k3s v1.36.4-k3s1; authorized OpenAI credential and gpt-6-luna. This is the combined native proof, not #3041 alone or an exact-head run of current 97ecaacb07449e8332da9fc3f910af0f692c8409.

./scripts/k3d test --harness openclaw

Started 2026-10-20T22:23:56Z, finished 2026-10-20T22:46:17Z (22m21s including preparation; test approximately 18m04s). Syntax, changed-file ESLint/Prettier, and whitespace checks passed. Exact executed test SHA256: 358cf6779a5a2e0d91d39ceeb4a281658911dc167369dce1729a6855fca5a2da.

Scope and retained authority: the selected runtime’s client schema supports private device identities, role/origin-scoped device token caches, native execution socket credentials, and migration receipts. Server-side revocation is separately persisted with the paired device, and token verification rejects a revoked entry. This run qualifies fresh provisioning and replacement with already-written current-layout state; it does not claim general cross-version migration, recovery of already-lost credentials, embedded/Codex execution, OpenShift, or all storage classes.

The only new changes were local test-only qualification assertions, included below; no additional production repair was used. The same two disclosed environment adapters from the earlier proof remain: native-only preparation does not require an unused Codex token, and the rootless TLS relay uses an exact high loopback port. NetworkPolicies remained enabled with legacy iptables. Immutable runtime/controller images were reused. An initial qualification attempt failed in the new helper because it tried to capture the worker token after revocation had cleared the cache; moving capture before revocation produced this pass. No production code or authority assertions were weakened.

Sanitized assertion transcript
✔ OpenShell enforces native provider delivery and Gateway authority (1082729.614933ms)
ℹ native-authority {"stage":"positive-native-workspace-and-successive-turns","agents":2}
ℹ native-authority {"stage":"cross-agent-setup-and-signed-device-denied","setupDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_BOOTSTRAP_TOKEN_INVALID"},"deviceDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_DEVICE_TOKEN_MISMATCH"}}
ℹ native-authority {"stage":"waiting-for-real-setup-expiry","waitMs":426069}
ℹ native-authority {"stage":"expired-setup-denied-fresh-setup-admitted","expiryDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_BOOTSTRAP_TOKEN_INVALID"}}
ℹ native-authority {"stage":"native-node-revoked","connected":false,"revokedAtMs":1792536127597}
ℹ native-authority {"stage":"revoked-workspace-operation","method":"GET","status":503}
ℹ native-authority {"stage":"revoked-workspace-operation","method":"PUT","status":503}
ℹ native-authority {"stage":"revoked-retained-model-operation","status":500}
ℹ native-authority {"stage":"revocation-complete-peer-still-working"}
ℹ native-authority {"stage":"gateway-replaced-same-pvc","previousPodUid":"10cc9c0e-ec9d-4300-be5d-c8822a7cb634","replacementPodUid":"8ebac603-c90c-4ec4-8e61-622548d4da97","claimUids":["6d9772c0-da11-45c1-936d-8dc12886e1f4"]}
ℹ native-authority {"stage":"gateway-replaced-same-pvc","previousPodUid":"f416b46c-800b-461c-b42f-7ab7302619e9","replacementPodUid":"839f09c0-ae8e-41e6-9b8e-dc8333974290","claimUids":["a8e8cfe1-0f7a-47a3-b203-1a83f7c2f82e"]}
ℹ native-authority {"stage":"prior-client-state-loaded","agents":2,"migrationReceiptsComplete":true,"identities":[1,1],"tokens":[0,0]}
ℹ native-authority {"stage":"valid-prior-session-and-workspace-after-replacement"}
ℹ native-authority {"stage":"retained-revoked-and-foreign-authority-denied-after-replacement","revokedDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_DEVICE_TOKEN_MISMATCH"},"foreignDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_DEVICE_TOKEN_MISMATCH"},"validForeignDenial":{"code":"INVALID_REQUEST","detailCode":"AUTH_DEVICE_TOKEN_MISMATCH"}}
ℹ native-authority {"stage":"replacement-authority-final-effects-verified","revokedWorkspaceWriteLanded":false,"revokedModelReachedWorker":false,"revokedModelStatus":500,"healthyPeerCompleted":true}
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1084453.007921
Dedicated native OpenClaw with OpenShell integration passed.
Source, immutable images, and result receipt
{
  "runReceipt": {
    "oceCommit": "e583b67b506ce3e6a7a0889003a02f4688db44d0",
    "openclawCommit": "4c7a1654f8f4a1dd2ecab4c421419f1093bc2d65",
    "openShellVersion": "0.1.4",
    "model": "gpt-6-luna",
    "engine": "podman",
    "command": [
      "./scripts/k3d",
      "test",
      "--harness",
      "openclaw"
    ],
    "startedAt": "2026-10-20T22:23:56Z",
    "nativeBuildHeapMb": 6144,
    "qualification": "Gateway replacement with retained valid, foreign, and revoked authority",
    "testFileSha256": "358cf6779a5a2e0d91d39ceeb4a281658911dc167369dce1729a6855fca5a2da",
    "iptablesBackend": "legacy",
    "freshClusterAndDatabase": true,
    "productionPatchesBeyondPR": [
      "06ae1e0e13b7517abaa933911d367ac8c8fa2336"
    ],
    "basePr1982Commit": "73c39aed60cab99aa642b282ed99b74e48ce4b2a",
    "repairPr3041Commit": "06ae1e0e13b7517abaa933911d367ac8c8fa2336",
    "controllerSourceCommit": "e583b67b506ce3e6a7a0889003a02f4688db44d0",
    "imageReuse": "Existing exact-source immutable runtime/controller builds; imported into the new cluster by standard preparation",
    "imageSelectorWrapperSha256": "0e0dbcb88b7b14757e4bee9fe22b4c67fac152ac6c66a2da4934ec9079c1f6b1",
    "rootlessRelayPatchSha256": "74fdf329a4e0dfe9f98c7497372f8b0b18613b83fc7c045a543d0c993e16ac17",
    "preparationPatchSha256": "dca5b2a998a8d25a7560cb4d48650058576a90ae700622a38ba464e7847e3f3f",
    "exitCode": 0,
    "elapsedSeconds": 1341.11,
    "finishedAt": "2026-10-20T22:46:17Z"
  },
  "counts": {
    "tests": 1,
    "pass": 1,
    "fail": 0,
    "skipped": 0
  },
  "checks": {
    "syntax": 0,
    "format": 0,
    "lint": 0,
    "whitespace": 0
  },
  "images": [
    {
      "kind": "k3d-image",
      "name": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-kubernetes-gateway-image-56a0a6394c97:local",
      "sourceImage": "localhost/openclaw-ci-image-openclaw-ci-local-e72c0affcd5f/runtime@sha256:8950cea722cd6a6afdad7bec803b3db9b754a2efc43f5c87f3667773798de98e",
      "reference": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-kubernetes-gateway-image-56a0a6394c97@sha256:7d930c9455551e0570b102510093587fe5130e55d6383150f696911aea774b6a"
    },
    {
      "kind": "k3d-image",
      "name": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-production-controller-image-1bcbd9ebc1af:local",
      "sourceImage": "localhost/pr1982-3041/controller@sha256:5beaa58447142dd58825a3e99eb99b966af8c035f02c0f95bee76f76e5c6d2a8",
      "reference": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-production-controller-image-1bcbd9ebc1af@sha256:37a5dba34735e2a7e6133842946e273dbd834fd90a553f24611ad8e9cb9c4f6e"
    },
    {
      "kind": "k3d-image",
      "name": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-openshell-gateway-image-47dd84e28776:local",
      "sourceImage": "ghcr.io/nvidia/openshell/gateway:5a7728c304b5528e954e9ae900cf21f84e1a12b1@sha256:bdb4fe248070032c88be78f029dad9325cb698480e90486e1413a674a5d8edc8",
      "reference": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-openshell-gateway-image-47dd84e28776@sha256:f8b547a91cf53642c0fbad5691b1739f17d545283fcfc6844bf922cec4c18017"
    },
    {
      "kind": "k3d-image",
      "name": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-openshell-supervisor-image-79a0e9d2d34e:local",
      "sourceImage": "ghcr.io/nvidia/openshell/supervisor:5a7728c304b5528e954e9ae900cf21f84e1a12b1@sha256:aa681d8264af6ee860e38349ed3c76609a51778eb6d0f02743df03787b80a290",
      "reference": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-openshell-supervisor-image-79a0e9d2d34e@sha256:4a80d5151a87906a86c0a06c330b17f0cef558d51b9a52bb58161dd95fd1ecac"
    },
    {
      "kind": "k3d-image",
      "name": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-openshell-sandbox-image-516bb527b5d2:local",
      "sourceImage": "ghcr.io/nvidia/openshell/sandbox:5a7728c304b5528e954e9ae900cf21f84e1a12b1@sha256:ca00c094aeaa60681b8196cda18ab3c5dd5f7d782d4c36798256427b67599070",
      "reference": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-openshell-sandbox-image-516bb527b5d2@sha256:6e5696d10b0df98d44cda8e789d815fda97e95ec44c224c750d278c3509b6c39"
    },
    {
      "kind": "k3d-image",
      "name": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-keycloak-image-917c9812706b:local",
      "sourceImage": "quay.io/keycloak/keycloak:26.7.5@sha256:37dbaf6f0722c9ec246335f36e1ef8b2e6cb960f7c27e0d8c615121a3d475a85",
      "reference": "localhost/openclaw-k8s-opencl-eeed62ce09b9/occ-test-keycloak-image-917c9812706b@sha256:fcbdc620064b5115738e24a1f57b0f54a4fc16a85ff6879a680225cddfa36fc4"
    }
  ],
  "testFileSha256": "358cf6779a5a2e0d91d39ceeb4a281658911dc167369dce1729a6855fca5a2da"
}
Complete additional test-only replacement qualification patch

Apply on top of the previously disclosed native-only preparation/rootless relay adapters. This patch is retained for handoff; it has not been pushed to either PR branch.

--- a/tests/integration/sandbox-driver-openshell-k3d-real.test.mjs
+++ b/tests/integration/sandbox-driver-openshell-k3d-real.test.mjs
@@ -4237,6 +4237,150 @@
   context.diagnostic("native-authority " + JSON.stringify({ stage, ...observations }));
 }
 
+// This qualification reads the real client database without exporting its contents.
+// Only the worker handshake probe below retains credentials in test-process memory.
+async function nativeClientState(topology, worker = false) {
+  const pod = worker ? topology.harnessPod : topology.gatewayPod;
+  const script = `
+const { DatabaseSync } = require("node:sqlite");
+const { createHash } = require("node:crypto");
+const { join } = require("node:path");
+const worker = process.argv[1] === "worker";
+const root = worker ? process.env.OPENCLAW_NODE_STATE_DIR : process.env.OPENCLAW_STATE_DIR;
+const db = new DatabaseSync(join(root, "client", "credentials.sqlite"), { readOnly: true });
+const select = (sql) => db.prepare(sql).all();
+const identities = select("SELECT identity_key, device_id, public_key_pem, private_key_pem FROM device_identities ORDER BY identity_key");
+const tokens = select("SELECT device_id, role, token, scopes_json FROM device_auth_tokens ORDER BY device_id, role");
+const origins = select("SELECT gateway_scope, device_id, role, token, scopes_json FROM gateway_origin_device_tokens ORDER BY gateway_scope, device_id, role");
+const receipt = select("SELECT version, phase, source_store FROM client_credential_migration ORDER BY version");
+const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
+const result = {
+  identityCount: identities.length, tokenCount: tokens.length, originTokenCount: origins.length,
+  identitiesDigest: digest(identities), tokensDigest: digest(tokens), originsDigest: digest(origins),
+  receiptDigest: digest(receipt), receiptComplete: receipt.length === 1 && receipt[0].phase === "complete",
+};
+if (worker) {
+  const token = [...tokens, ...origins].find((row) => row.role === "node");
+  const identity = identities.find((row) => row.device_id === token?.device_id);
+  if (!identity || !token) throw new Error("Enrolled worker client authority missing");
+  result.deviceIdentity = { deviceId: identity.device_id, publicKeyPem: identity.public_key_pem, privateKeyPem: identity.private_key_pem };
+  result.deviceToken = token.token;
+}
+db.close();
+process.stdout.write(JSON.stringify(result));
+`;
+  const output = await kubectl(
+    "exec",
+    pod.metadata.name,
+    "--namespace",
+    worker ? topology.placement : topology.gatewayPlacement,
+    "--container",
+    worker ? "agent" : "gateway",
+    "--",
+    "node",
+    "-e",
+    script,
+    worker ? "worker" : "gateway",
+  );
+  return JSON.parse(output);
+}
+
+async function replaceNativeGateway(context, topology) {
+  const old = await resource("pod", topology.gatewayPod.metadata.name, topology.gatewayPlacement);
+  const claims = (pod) =>
+    pod.spec.volumes
+      .filter((v) => v.persistentVolumeClaim)
+      .map((v) => v.persistentVolumeClaim.claimName)
+      .sort();
+  const claimNames = claims(old);
+  assert.ok(claimNames.length > 0, "Gateway replacement must retain its actual PVC");
+  const claimsBefore = await Promise.all(
+    claimNames.map((name) => resource("pvc", name, topology.gatewayPlacement)),
+  );
+  await kubectl(
+    "delete",
+    "pod",
+    old.metadata.name,
+    "--namespace",
+    topology.gatewayPlacement,
+    "--wait=true",
+    "--timeout=90s",
+  );
+  const replacement = await waitFor(
+    "replacement Gateway ready with a new Pod UID",
+    async () => {
+      const pods = (await resources("pods", topology.gatewayPlacement)).filter(
+        (pod) =>
+          pod.metadata.labels?.["openclaw.dev/workload-role"] === "gateway" &&
+          pod.metadata.labels?.["openclaw.dev/agent"] === topology.agent.id &&
+          pod.metadata.deletionTimestamp === undefined &&
+          pod.metadata.uid !== old.metadata.uid &&
+          pod.status?.conditions?.some(
+            (condition) => condition.type === "Ready" && condition.status === "True",
+          ),
+      );
+      return pods.length === 1 ? pods[0] : undefined;
+    },
+    180_000,
+  );
+  assert.deepEqual(claims(replacement), claimNames);
+  for (const previous of claimsBefore) {
+    const current = await resource("pvc", previous.metadata.name, topology.gatewayPlacement);
+    assert.equal(
+      current.metadata.uid,
+      previous.metadata.uid,
+      "replacement must reuse the same PVC object",
+    );
+    assert.equal(
+      current.spec.volumeName,
+      previous.spec.volumeName,
+      "replacement must reuse the same PV",
+    );
+  }
+  nativeAuthorityTrace(context, "gateway-replaced-same-pvc", {
+    previousPodUid: old.metadata.uid,
+    replacementPodUid: replacement.metadata.uid,
+    claimUids: claimsBefore.map((claim) => claim.metadata.uid),
+  });
+  return { ...topology, gatewayPod: replacement };
+}
+
+async function nativeWorkerEffects(topology, nonce) {
+  const script = `
+const fs = require("node:fs");
+const path = require("node:path");
+const crypto = require("node:crypto");
+const nonce = process.argv[1];
+const workspace = process.env.OPENCLAW_WORKSPACE_DIR;
+const content = fs.readFileSync(path.join(workspace, "AGENTS.md"));
+let nonceFound = false;
+const walk = (root) => {
+  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
+    const file = path.join(root, entry.name);
+    if (entry.isDirectory()) walk(file);
+    else if (entry.isFile() && fs.readFileSync(file).includes(nonce)) nonceFound = true;
+  }
+};
+walk(process.env.OPENCLAW_NODE_STATE_DIR);
+process.stdout.write(JSON.stringify({ workspaceDigest: crypto.createHash("sha256").update(content).digest("hex"), nonceFound }));
+`;
+  return JSON.parse(
+    await kubectl(
+      "exec",
+      topology.harnessPod.metadata.name,
+      "--namespace",
+      topology.placement,
+      "--container",
+      "agent",
+      "--",
+      "node",
+      "-e",
+      script,
+      nonce,
+    ),
+  );
+}
+
 async function assertNativeAuthorityChain(context, topology, positive) {
   const adminA = await connectNativeAdministrator(context, topology);
   const issueSetup = async () => {
@@ -4371,6 +4515,11 @@
     topology.gatewayPod.metadata.name,
     topology.gatewayPlacement,
   );
+  // Capture real credentials while still valid: the native client clears them on revocation.
+  const revokedClient = await nativeClientState(topology, true);
+  assert.equal(revokedClient.deviceIdentity.deviceId, deviceId);
+  const validClient = await nativeClientState(peer, true);
+  assert.equal(validClient.deviceIdentity.deviceId, peerNodeId);
   const revoked = await adminA("device.token.revoke", { deviceId, role: "node" });
   assert.equal(revoked.deviceId, deviceId);
   assert.equal(revoked.role, "node");
@@ -4438,6 +4587,140 @@
   );
   assert.equal((await adminA("node.describe", { nodeId: deviceId })).connected, false);
   nativeAuthorityTrace(context, "revocation-complete-peer-still-working");
+
+  // Persist revocation BEFORE replacing either Gateway. A surviving healthy peer
+  // distinguishes authority rejection from a broken replacement or model provider.
+  const stateA = await nativeClientState(topology);
+  const stateB = await nativeClientState(peer);
+  assert.equal(stateA.receiptComplete, true);
+  assert.equal(stateB.receiptComplete, true);
+  const workspacePath = (item) =>
+    `/namespaces/${item.namespaceId}/agents/${item.agent.id}/workspace/files/AGENTS.md`;
+  const peerFile = await peer.request("GET", workspacePath(peer));
+  assert.equal(peerFile.status, 200);
+  const retained = await nativeWorkerEffects(topology, "unissued-nonce-" + randomUUID());
+  const replacementA = await replaceNativeGateway(context, topology);
+  const replacementB = await replaceNativeGateway(context, peer);
+  assert.deepEqual(
+    await nativeClientState(replacementA),
+    stateA,
+    "A must load its unchanged prior client store and receipt",
+  );
+  assert.deepEqual(
+    await nativeClientState(replacementB),
+    stateB,
+    "B must load its unchanged prior client store and receipt",
+  );
+  nativeAuthorityTrace(context, "prior-client-state-loaded", {
+    agents: 2,
+    migrationReceiptsComplete: true,
+    identities: [stateA.identityCount, stateB.identityCount],
+    tokens: [stateA.tokenCount, stateB.tokenCount],
+  });
+  const replacementAdminA = await connectNativeAdministrator(context, replacementA);
+  const replacementAdminB = await connectNativeAdministrator(context, replacementB);
+  const requesterA = await createNativeTurnRequester(context, replacementA);
+  const requesterB = await createNativeTurnRequester(context, replacementB);
+  await waitFor("valid prior worker reconnects after replacement", async () =>
+    (await replacementAdminB("node.describe", { nodeId: peerNodeId })).connected === true
+      ? true
+      : undefined,
+  );
+  assert.equal((await replacementAdminA("node.describe", { nodeId: deviceId })).connected, false);
+  assert.deepEqual(
+    (await peer.request("GET", workspacePath(peer))).data,
+    peerFile.data,
+    "the peer's previously written workspace must remain readable",
+  );
+  await assertNativeTurn(requesterB, peerTurns.sessions[0]);
+  await assertNativeOpenClawWorkspaceFiles(replacementB);
+  nativeAuthorityTrace(context, "valid-prior-session-and-workspace-after-replacement");
+
+  const nodeProbe = { clientName: "node-host", mode: "node", role: "node", scopes: [] };
+  const revokedDenial = await expectNativeAuthorityDenied(
+    {
+      ...nodeProbe,
+      url: requesterA.nodeUrl,
+      deviceIdentity: revokedClient.deviceIdentity,
+      deviceToken: revokedClient.deviceToken,
+    },
+    "AUTH_DEVICE_TOKEN_MISMATCH",
+  );
+  const foreignDenial = await expectNativeAuthorityDenied(
+    {
+      ...nodeProbe,
+      url: requesterB.nodeUrl,
+      deviceIdentity: revokedClient.deviceIdentity,
+      deviceToken: revokedClient.deviceToken,
+    },
+    "AUTH_DEVICE_TOKEN_MISMATCH",
+  );
+  // A's credentials must not be rejected everywhere merely because they are revoked:
+  // B's still-valid real worker credential must also fail at the foreign Gateway A.
+  const validForeignDenial = await expectNativeAuthorityDenied(
+    {
+      ...nodeProbe,
+      url: requesterA.nodeUrl,
+      deviceIdentity: validClient.deviceIdentity,
+      deviceToken: validClient.deviceToken,
+    },
+    "AUTH_DEVICE_TOKEN_MISMATCH",
+  );
+  nativeAuthorityTrace(context, "retained-revoked-and-foreign-authority-denied-after-replacement", {
+    revokedDenial,
+    foreignDenial,
+    validForeignDenial,
+  });
+  const writeNonce = "replacement-revoked-write-" + randomUUID();
+  for (const method of ["GET", "PUT"]) {
+    const result = await topology.request(
+      method,
+      workspacePath(topology),
+      method === "PUT" ? { content: writeNonce } : undefined,
+    );
+    assert.equal(result.status, 503, "replacement must not restore revoked workspace access");
+  }
+  const rejectedTurn = await requesterA.turn(positive.sessions[0]);
+  assert.ok(rejectedTurn.status >= 500 && rejectedTurn.status <= 599);
+  assert.ok(rejectedTurn.payload.error);
+  assert.equal(
+    /capacity|billing|quota|rate.limit/iu.test(rejectedTurn.payload.error.message ?? ""),
+    false,
+  );
+  assert.equal(JSON.stringify(rejectedTurn.payload).includes(rejectedTurn.nonce), false);
+  const effects = await nativeWorkerEffects(topology, rejectedTurn.nonce);
+  assert.equal(
+    effects.workspaceDigest,
+    retained.workspaceDigest,
+    "revoked write must not reach the Harness",
+  );
+  assert.equal(
+    effects.nonceFound,
+    false,
+    "revoked model request must not reach retained worker state",
+  );
+  const writeEffects = await nativeWorkerEffects(topology, writeNonce);
+  assert.equal(writeEffects.nonceFound, false);
+  await assertNativeTurn(requesterB, peerTurns.sessions[0]);
+  assert.equal((await replacementAdminA("node.describe", { nodeId: deviceId })).connected, false);
+  for (const item of [topology, peer]) {
+    const observed = await resource("pod", item.harnessPod.metadata.name, item.placement);
+    assert.equal(observed.metadata.uid, item.harnessPod.metadata.uid);
+    assert.deepEqual(
+      observed.status.containerStatuses.map(({ name, restartCount }) => ({ name, restartCount })),
+      item.harnessPod.status.containerStatuses.map(({ name, restartCount }) => ({
+        name,
+        restartCount,
+      })),
+      "Gateway replacement must not restart or replace the retained Harness",
+    );
+  }
+  nativeAuthorityTrace(context, "replacement-authority-final-effects-verified", {
+    revokedWorkspaceWriteLanded: false,
+    revokedModelReachedWorker: false,
+    revokedModelStatus: rejectedTurn.status,
+    healthyPeerCompleted: true,
+  });
 }
 
 async function holdNativeOpenClawDemo(context, topology) {

@sallyom

sallyom commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

@clawsweeper re-review

The requested Gateway replacement authority proof now passes: 1 passed, 0 failed, 0 skipped. Sanitized transcript, exact revisions/image digests, and complete qualification patch. The PR body now records the retained-state, valid-session, revoked/foreign-authority and final-effect results, plus the combined-candidate and current-layout scope limits. No review holds were manually cleared.

@clawsweeper

clawsweeper Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
Exact review queued.

Re-review progress:

@clawsweeper clawsweeper Bot added clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. clawsweeper:intake:bug Preliminary submission classification; full source review pending. labels Oct 20, 2026
@clawsweeper clawsweeper Bot added clawsweeper:intake:bug Preliminary submission classification; full source review pending. clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed clawsweeper:intake:needs-repro Preliminary submission classification; full source review pending. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. clawsweeper:intake:bug Preliminary submission classification; full source review pending. labels Oct 20, 2026
@freeqaz

freeqaz commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Free's clanker here (an AI agent working on Free's behalf).

Review of head 97ecaacb0 against main and against the Credential Proxy PR #2487 (head 9aa269774), which is expected to land first:

  • Code: no defects found. configureNativeWorkerProfile now reuses only a profile that is isDeepStrictEqual to the one it would generate, so a different device still fails. The new client category goes through the same init-container path as the other private-state subpaths (mkdir + chmod 0700 as uid 1000), so an existing PVC gets a correctly owned empty client/ directory on the next Pod start rather than a kubelet-created root-owned one.
  • Credential Proxy Implementation #2487 compatibility: git merge-tree against Credential Proxy Implementation #2487's head is clean (it also edits runtime-entrypoints.ts, kubernetes/index.ts and kubernetes-compute.test.mjs, in separate hunks). On a local merge of Credential Proxy Implementation #2487 + this PR, tsc --build passes, and the two changed conformance cases ("dedicated OpenClaw renders an enrolled Harness…" and "Gateway and Harness storage are separate…") pass.
  • CI: CI Required and First Agent Smoke are green on this head.

Still open before a merge: the ClawSweeper item on existing-state compatibility (an existing Agent's pre-change PVC through the controller cutover). A re-review of this head is running. This PR persists the client credential store across Gateway replacement, and its scope includes the Compute startup wrapper, so @freeqaz-openai should make the landing call; I am not merging it.

@freeqaz-openai
freeqaz-openai merged commit c7f95ca into main Oct 21, 2026
112 checks passed
@freeqaz-openai
freeqaz-openai deleted the openclaw/fix-native-gateway-state-20261020 branch October 21, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants