Skip to content

test(openshell): prove model, tool and gateway outages fail closed - #2393

Merged
mrunalp merged 11 commits into
openclaw:mainfrom
mrunalp:test/credential-gateway-outages
Oct 16, 2026
Merged

mrunalp merged 11 commits into
openclaw:mainfrom
mrunalp:test/credential-gateway-outages

Conversation

@mrunalp

@mrunalp mrunalp commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

#118 criterion 4 also needs proof that the OpenShell credential route fails closed when its dependencies go away, and recovers when they return. #2374 covers foreign destinations, proxy bypass and untrusted upstreams; no test took the model provider, a tool upstream or the OpenShell gateway down.

#2374 merged as a squash, so main is merged into this branch (ca0988105, 5122f1a25 after #2048, then 4c46e5f8e after #2515 and #2600) rather than rebased; the branch holds only this PR's changes.

Change

tests/integration/sandbox-driver-openshell-k3d-real.test.mjs (Codex scenario) adds three phases after the untrusted-model-upstream check, in this order. Each takes one dependency away and restores it before asserting:

  • Tool upstream. The token-echo Service selects no Pod. OpenShell must end the request without a response (curl exit 52) and deliver no token. Once the Service is restored, the upstream must receive the source's own token again: curl exits 0 and the echo's digest equals the source token's.
  • Model provider. Cluster DNS sends the model host to a Service with no endpoints, so OpenShell's connection is refused. The turn must fail without exposing the model credential, and the same revision must serve the next turn once DNS is restored.
  • OpenShell gateway. The Namespace's gateway StatefulSet is scaled to zero and back. While it is down, the running Agent cannot serve a turn (the Agent Gateway reaches Codex through OpenShell's service exposure), a source read reports failed, registration returns 503 DEPENDENCY_UNAVAILABLE, and a redeploy fails at least one attempt and stays queued without activating; the test waits for that attempt while the gateway is still stopped. Once it is back, the redeploy activates and serves a turn. A registration attempted during the outage is left deleting; the test deletes it, and then its Secret, after recovery.

Supporting changes:

  • A shared Sandbox probe helper. It matches the command by its unique script name and checks every attempt for the selected model credential and the tool tokens. When the model answers without calling the exec tool, it asks up to twice more, 20 seconds apart, and logs Codex's own error lines if all three are skipped. Skips happened intermittently on the first exec turn after cluster DNS for the model host was restored, and say nothing about OpenShell. Probe results are never retried.
  • A hook that restarts the test's gateway port-forward after the Pod is replaced, and a 40-minute test timeout (runs take about 25 minutes).
  • Foreign-host probe fix that missed test(openshell): prove the credential route and refuse untrusted upstreams #2374. test(openshell): prove the credential route and refuse untrusted upstreams #2374 merged without its last review fix: a 200 from the foreign-host probe now counts only when curl exits 0, and an empty header only when the echo reports length 0, so a body that never arrives fails the check.

Docs:

  • docs/testing/openshell-coverage.md replaces "Not covered: outages" with an Outages section.
  • docs/reference/drivers/openshell-credential-gateway.md adds the gateway-outage limit and a troubleshooting row for the refresh race below.
  • docs/reference/drivers/credential-gateway.md fixes a troubleshooting row that said a status read returns 503; it reports failed, as credential-sources.md documents.

Finding: OAuth2 refresh after a gateway restart (NVIDIA/OpenShell#4526)

In operator workspace mode, OpenShell's refresh worker sweeps immediately at startup, before its namespace watcher has filled the operator allowlist. Every OAuth2 refresh due in that sweep fails with PermissionDenied ("workspace … is not in the operator namespace allowlist"), which OpenShell classifies as refresh_configuration_invalid and retries an hour later. When the current token expires, the Agent's tool calls to that source fail until then. The namespace is labeled throughout; Sandboxes and static providers are unaffected.

Filed upstream as NVIDIA/OpenShell#4526, with a fix in NVIDIA/OpenShell#4527. The test accepts that state, with TODO(NVIDIA/OpenShell#4526), and requires a forced rotation to return each source to ready; the reference documents that recovery.

Verification

Real OpenShell suite on a disposable k3d cluster, commit 4c46e5f8e (main at b75d3e371 plus this PR), Codex scenario, API-key mode (OCC_TEST_OPENSHELL_MODEL_BASE_URL=https://api.openai.com/v1), gpt-6-astra:

Run on 2026-10-16, 23.3 minutes, 1 test passed, 0 failed. Proof lines, which contain no credential values:

credential route {"modelPlaceholder":{"inheritedByCommand":false,"readableFromHarness":true},"foreignDestination":{"httpStatus":"403","exitCode":0,"received":"refused"},"modelHostFromCurl":{"httpStatus":"200","connectStatus":"000","exitCode":0},"proxyBypass":{"allowedHostExitCode":0,"deniedHostExitCode":7,"addressExitCode":7,"sandboxDns":"resolved"},"untrustedUpstream":{"httpStatus":null,"exitCode":52,"harnessSawProxyIssuer":true,"tlsFailures":1,"handshakes":0,"requests":0},"podNetwork":{"address":"ECONNREFUSED"}}
untrusted model upstream {"turnRejected":true,"tlsFailures":30,"handshakes":0,"requests":0}
tool upstream outage {"outage":{"httpStatus":"000","exitCode":52,"placeholder":null,"tokenDelivered":false},"recovered":{"httpStatus":"200","exitCode":0,"placeholder":"false","tokenDelivered":true}}
model provider outage {"turnRejected":true,"recoveredTurn":true,"sameRevision":true}
gateway outage {"runningAgentTurn":"failed","sourceRead":{"httpStatus":200,"state":"failed"},"registration":{"httpStatus":503,"code":"DEPENDENCY_UNAVAILABLE"},"redeployDuringOutage":{"status":"queued","work":{"state":"queued","reason_code":null,"attempt_count":1},"activeRevisionUnchanged":true,"gatewayStopped":true},"recovery":{"redeployActivated":true,"work":{"state":"claimed","reason_code":null,"attempt_count":4},"sourceState":"ready","registrationRecord":"deleting","turn":true,"refresh":[{"type":"oauth2-client-credentials","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}},{"type":"oauth2-refresh-token","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}},{"type":"oauth2-client-credentials","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}}]}}
  • During the gateway outage the redeploy failed its first attempt while the gateway was stopped and stayed queued; after recovery it activated.
  • The model-provider outage turn failed after Codex's retries (about 5 minutes); the recovery turn used the same revision.
  • All three OAuth2 sources hit the OpenShell race after the restart (failed:refresh_configuration_invalid), and each returned to ready on a forced rotation. The rest of the suite, including the OAuth2 refresh phase, then passed on the restored tokens.
  • No model key or OpenShell placeholder in the prepare, run or cleanup logs; the cluster was removed.
  • Prettier, ESLint, pnpm docs:check and pnpm docs:check-length pass.

Not verified live: the default Codex token mode. No CODEX_ACCESS_TOKEN was available. The lane preparation's presence check for it was satisfied with the same API key, which the suite does not read in API-key mode.

CI on this PR does not run this suite: QA advisory skips fork PRs, and the Full Integration openshell lane runs only from main. The phases add about 8 minutes to the suite.

Page length. docs/reference/drivers/openshell-credential-gateway.md is 2,491 words, under the 2,500 limit. To add the outage limit and troubleshooting row I removed five repeated statements (the shared Backend credential, two restatements of OAuth2 token pickup and redeploys, a repeated absent mapping, and a Limits bullet that repeated where the placeholder goes). It stays one page as the single reference for the Driver's configuration, trust boundary and troubleshooting.

Risks

Test and docs only. The phases change cluster-wide CoreDNS and scale the Namespace's OpenShell gateway on the disposable cluster, and restore each before asserting.

Refs #118

…reams

Run the route probes as Sandbox processes behind OpenShell's proxy: the
model placeholder is refused at a tool source's host, direct connections
reach neither a denied host nor a bare address, and a tool source whose
upstream presents an untrusted certificate never receives a request.
After replacement, point cluster DNS for the model provider at that
upstream and require the model turn to fail without a request reaching it.

Record that commands Codex starts can use the model route, because
OpenShell matches policy binaries against ancestors, and move the suite's
coverage notes to their own testing page.
Custom model endpoints must serve Responses, not model listing, so probe
the inserted key with a minimal Responses request.
Count the foreign-host probe as refused only on 403 with OpenShell's
credential_endpoint_mismatch code; a transport failure or another error
now fails the check. Require the allowed host to answer without the
proxy settings, so the denied-host checks run with an outbound path, and
count untrusted-upstream TLS failures only for the expected server name.
Take each dependency of the credentialed route away and restore it before
asserting:

- the model provider: cluster DNS sends the model host to a Service with
  no endpoints; the turn fails without the key, and the same revision
  serves the next turn once DNS is restored;
- a tool upstream: OpenShell ends the request without a response, and
  the token is substituted again once the upstream returns;
- the Namespace's OpenShell gateway: while it is scaled to zero the
  running Agent cannot serve a turn, a source read reports failed,
  registration returns 503, and a redeploy stays queued; once it is
  back, the redeploy activates and serves a turn.

After the gateway restarts, an OAuth2 source due in OpenShell's first
refresh sweep fails with refresh_configuration_invalid and is retried an
hour later (NVIDIA/OpenShell#4526). The test accepts that state and
requires a forced rotation to restore the token, and the reference
documents that recovery.

Share the Sandbox probe helper between phases, match the probe command
by its unique name, and ask again when the model skips the exec tool.
@clawsweeper

clawsweeper Bot commented Oct 14, 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 P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Oct 14, 2026
@clawsweeper

clawsweeper Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes

This PR adds real OpenShell tool, model-provider, and gateway outage tests, recovery assertions, and matching documentation.

Example: The token-echo Service temporarily has no endpoints.

  • Before: The suite did not verify credentialed tool requests during an upstream outage.
  • After: It requires curl exit 52 during the outage, then HTTP 200, exit 0, and delivery of the exact source token after recovery.

Review scores

Measure Result What it means
Overall readiness 🦞 diamond lobster (5/6) Focused real-runtime coverage with current-head live results, preserved upstream intent, and no remaining actionable findings.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (logs): Current-head live output exercises production OCC APIs, the durable worker, real Codex transport, and OpenShell on disposable k3d in API-key mode, showing all three outages rejected and recovery succeeding; the earlier run also demonstrates forced OAuth2 rotation recovery.
Patch quality 🦞 diamond lobster (5/6) No actionable review findings were identified.

Product

Kind: Test only · Worth it: Yes · Fix scope: Partial
User problem: Maintainers lacked real-runtime evidence that credentialed Agent workflows reject dependency outages and recover afterward.
Reason: The scenarios protect distinct supported behaviors, extend the established integration owner, and correctly use a related milestone reference rather than claiming complete delivery.

Merge readiness

✅ Ready for maintainer review

This PR adds useful outage coverage absent from current main; the conflict resolution and selected-credential repair address the recorded concerns, with no actionable findings remaining.

Priority: P2
Reviewed head: 80805c826a1e05f78f72eb860e383b94509ad2af

Before merge

None.

Findings

None.

Tests

  • Missing end-to-end proof: The supplied live run covers API-key mode; default Codex token mode remains unverified.
Agent review details

How this fits together

OCC registers credential sources and deploys dedicated Codex Harnesses through selected Drivers; OpenShell mediates their model and tool requests, while the integration suite disrupts dependencies and observes API, worker, and runtime outcomes.

flowchart TB
  Suite[Test suite] --> OCC[OCC API and worker]
  OCC --> Codex[Dedicated Codex Harness]
  Suite --> Gateway[Agent Gateway]
  Gateway --> OpenShell[OpenShell gateway and proxy]
  OpenShell --> Codex
  Codex --> OpenShell
  OpenShell --> Model[Model provider]
  OpenShell --> Tool[Tool upstream]
Loading

Technical review

Best possible solution:

Retain the focused real-runtime outage coverage and its explicit separation from broader milestone and release qualification.

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

This is primarily a coverage change; source inspection confirms the missing outage phases on main and the login-mode forwarding mismatch repaired by this PR.

Is this the best way to solve the issue?

Extending the existing production-path suite is appropriate because it observes real dependency failures, exact credential recovery, and durable reconciliation without adding a competing implementation.

AGENTS.md: found and applied where relevant.

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

Provenance checked

  • Credential-route probe extraction and response classification keeps the original intent (4e396e7: The prior route-proof PR established destination, proxy, and TLS enforcement through real Sandbox commands.)
  • Selected model credential, placeholder inheritance, and Backend placement keeps the original intent (3414ccb: PAT support selected the credential and Codex isolation mode while retaining API-key behavior and production Backend placement semantics.)
  • Sandbox configuration delegation keeps the original intent (10af5b8: The wrapper delegates configuration admission to the real Sandbox Driver; forwarding its new third argument preserves that purpose.)
  • Credential reference consolidation keeps the original intent (d4f544c: The reference documents gateway custody, source lifecycle, and operator recovery; edits remove repetition while retaining those boundaries.)

Testing

Proof path: shipped entry point.

Security

None.

Evidence

What I checked:

Likely related people:

  • Mrunal Patel: Raw commit c91b9c6 adds tests/integration/sandbox-driver-openshell-k3d-real.test.mjs:1474 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: c91b9c6ceebe; files: tests/integration/sandbox-driver-openshell-k3d-real.test.mjs)
  • Kevin: Raw commit 3414ccb adds tests/integration/sandbox-driver-openshell-k3d-real.test.mjs:4452 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 3414ccb0d27a; files: tests/integration/sandbox-driver-openshell-k3d-real.test.mjs)
  • freeqaz: Raw commit b03ffcb adds docs/reference/drivers/openshell-credential-gateway.md:114 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: b03ffcb792c6; files: docs/reference/drivers/openshell-credential-gateway.md)

Review metrics

Metric Value Why it matters
Change distribution Production +0/-0; integration tests +614/-51; documentation +63/-43 Growth adds three distinct real-dependency outage scenarios and shared probe handling without changing production behavior.

Labels

Label changes:

No label changes.

Label justifications:

  • P2: Useful credential-route reliability coverage with no demonstrated urgent production regression introduced by this PR.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • 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 (3 earlier review cycles)
  • reviewed 2026-10-14T05:51:30.517Z sha 8b02964 :: needs changes before merge. :: [P2] Reject incomplete foreign-host echo responses | [P2] Verify the exact recovered tool credential | [P2] Require reconciliation to run while the gateway is down
  • reviewed 2026-10-14T14:08:13.902Z sha 8b02964 :: needs changes before merge. :: [P2] Reject incomplete foreign-host echo responses | [P2] Verify the exact recovered tool credential | [P2] Require reconciliation to run while the gateway is down
  • reviewed 2026-10-14T22:49:05.965Z sha 82a9679 :: needs maintainer review before merge. :: none

Reviewed October 15, 2026, 7:58 PM ET / 23:58 UTC (Revision 4).

@mrunalp
mrunalp marked this pull request as ready for review October 14, 2026 14:04
@mrunalp
mrunalp requested a review from a team as a code owner October 14, 2026 14:04
@freeqaz

freeqaz commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Free's clanker here (an AI agent working on Free's behalf). I reviewed only the last commit, 8b029645a (the three outage phases, the shared probe helper and the docs), as the PR body asks.

Verdict: the approach holds up. Each outage is real: a ClusterIP Service with no endpoints refuses connections, and a StatefulSet scaled to 0 has no Pod left. Each dependency comes back before any assertion runs, and every wait has a bound. The run log in the body matches what the code asserts. I found three low-severity gaps where an assertion proves less than the docs claim. None of them blocks the stack, but all three are cheap to tighten.

  1. Tool-upstream recovery doesn't prove the token arrived (sandbox-driver-openshell-k3d-real.test.mjs:5031-5032). token-echo answers 200 with placeholder:false when Authorization is missing or wrong, so the test would pass if OpenShell dropped the header after the upstream came back. The probe already saves the body. Pull "digest" out of it too, assert it equals toolA.digest (as :3522 does), and require exitCode("TOOL") === 0. (The automated review flagged this as well.)
  2. The redeploy during the gateway outage isn't shown to be attempted (:5121). delay(60_000) is followed by only activeRevisionUnchanged. The deployment status and attempt_count are logged, but nothing asserts them. On a slow worker the first attempt could land after scale(1), and the phase would still pass. The coverage page and the PR body both say the redeploy "stays queued". A bounded waitFor(attempt_count >= 1) while the gateway is still at 0 replicas, followed by deployment.status === "queued", would prove that and replace the fixed sleep. (The automated review flagged this too.)
  3. A swallowed key-leak assertion on the gateway-down turn (:5080-5092). The assert.equal(JSON.stringify(turn).includes(key), false) sits inside the try. If a turn completed and leaked the key, the AssertionError ("true == false") lands in the catch, whose own key check passes, and runningAgentTurn becomes "failed", which is the expected value. Set "completed" and keep the turn, then run the leak check outside the try, or rethrow AssertionError from the catch.

Nits (optional):

  • runSandboxProbe checks each attempt only for the model key. The tool-token and untrusted-TLS-token checks run on the final result, after the "must run the probe script" message (which includes agent text) is built. Passing the extra secrets into the helper would check every attempt before anything is printed.
  • In the model-provider outage, rejected accepts any error, including the helper's 300 s turn timeout. The body says the turn failed after about 5 minutes, which is close to that bound. Either outcome is fail-closed. Matching Codex's final error would tell "provider refused" apart from "timed out", in case a later change starts relying on that distinction.
  • The model-outage Service and the outage Secret are never deleted. Both live in disposable namespaces, so this is just for tidiness.
  • openshell-credential-gateway.md lines 160 and 279 are now long unwrapped lines, unlike the rest of the page.

What I checked:

  • Credentials: no key or token reaches a log line, an assertion message or the observed blobs. The extra Harness-log dump drops bearer/eyJ/placeholder lines and the known secrets.
  • Probe retry: it re-asks only when no commandExecution names the nonce, so a probe never runs twice and the TLS event counts stay valid.
  • DNS restore: the modelDnsOverride bookkeeping waits correctly after a failed set.
  • Test timeout: the 40-minute timeout fits the lane's 120-minute job.
  • Keycloak helper: tests/helpers/keycloak-real.mjs, docs.json and docs/testing/openshell.md belong to test(openshell): prove the credential route and refuse untrusted upstreams #2374's commit, not this one. The helper's new options default to the old SAN and CA name, so its only other caller (keycloak-real.mjs:165) is unchanged.
  • Docs: Static Checks are green on this head (the word cap is the CI counter's call). The removed sentences are repeated elsewhere on the page. Outside the "stays queued" wording above, the pages describe what the test proves.
  • Local runs: node --check passes, and the file skips cleanly without the cluster env. The k3d-real suite itself needs a cluster, so I didn't run it. As the body notes, PR CI doesn't run it either, so the live proof rests on your run.

…sifying it

curl prints 200 even when the response body then fails to arrive, for
example on a timeout (exit 28). The classifier then read the missing
length as 0 and accepted the result as an empty header. Count a 200 only
when curl exits 0, accept an empty header only when the echo reported
length 0, and log the probe's exit code.
Tool upstream: the echo answers 200 without a credential, so recovery
now requires curl to exit 0 and the echo's digest to match tool A's
token, and the outage request must not deliver it.

Gateway: replace the fixed 60-second wait with a bounded wait for a
failed reconcile attempt while the gateway is still at zero replicas,
and require the deployment to stay queued. Check the gateway-down
turn for the model key outside the try that classifies it, so a turn
that completed and leaked the key can no longer read as failed.

Check every suite credential on each probe turn before anything is
printed, delete the model-outage Service and the outage Secret, and
rewrap two reference lines.
openclaw#2374 was squash-merged, so its original commits stay on this branch. Resolve the conflicts by applying only this branch's changes after 954910a on top of main, and rewrap three reference lines.
@freeqaz

freeqaz commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Free's clanker here (an AI agent working on Free's behalf). I re-reviewed the two commits after 8b029645a: 155962577 (foreign-host classifier) and 8a4167950 (outage phases). I checked them against the three gaps from my earlier comment.

All three are addressed:

  1. Tool-upstream recovery. The probe now extracts the echo's digest and compares it with toolA.digest (the echo hashes the raw Authorization value and toolA.digest is sha256("Bearer <token>"), so the two match). Recovery also requires exitCode: 0, and the outage request must not deliver the token. A dropped or mangled header now fails the test, and the digest is compared in the test process and never printed.
  2. Redeploy during the gateway outage. The fixed 60 s sleep is replaced by a bounded waitFor (180 s, 500 ms polls) for state = 'queued' with attempt_count >= 1. A claim moves the row to claimed and counts an attempt, so seeing queued again with a count means at least one attempt failed. The test then checks that the gateway still has no Pod, so every attempt ran against the stopped gateway, and it asserts deployment status queued. The coverage page now claims only what's checked: "fails at least one attempt and stays queued".
  3. Swallowed key-leak assertion. The leak check now runs outside the inner try. Here's the case that used to slip through: a turn completes and leaks the key, so downTurn is set, runningAgentTurn becomes "completed", and the leak assert throws. That throw happens inside the outer try, so it lands in failure. The gateway is restored, observed is logged (it holds no turn content), and failure is rethrown. Even without the leak assert, "completed" would fail the final runningAgentTurn === "failed" check. If the error message itself leaked the key, the inner catch assert throws to the outer catch and the test also fails. None of the assertion messages contain a credential.

155962577 also fixes the earlier automated finding about a foreign host returning 200 and then timing out. A 200 counts only when curl exits 0, and an empty header counts only when the echo reported 0. An empty length with exit 0 now falls through to other and fails. main doesn't have this check yet, because #2374 landed without it, so please keep this branch's classifier when you resolve the conflict. The other changes look good: credential checks run on every probe attempt before anything is printed, the untrusted-TLS token is included, and the cleanup of the model-outage Service and the outage Secret is bounded. No new fixed sleeps.

Local checks on 8a4167950: node --check passes, the suite skips cleanly without OCC_TEST_OPENSHELL_K3D_REAL, and Prettier and ESLint pass on the touched files. I didn't run the k3d suite. The live evidence in the PR body is still from 8b029645a.

What's left:

  • The PR is CONFLICTING with main. A trial merge conflicts in tests/integration/sandbox-driver-openshell-k3d-real.test.mjs, docs/testing/openshell-coverage.md and docs/reference/drivers/openshell-credential-gateway.md, now that test(openshell): prove the credential route and refuse untrusted upstreams #2374 has landed. Could you merge main into the branch? After the merge, please re-check the word count on openshell-credential-gateway.md: it was at 2,492 of 2,500, so the merged text could push it over.
  • A live run on the merged head would help, since the new assertions (digest match, the queued-after-attempt wait, gatewayStopped) haven't run yet. Please also update the PR body: the "Stacked on test(openshell): prove the credential route and refuse untrusted upstreams #2374 / review only the last commit" note and the Verification section, which cites 8b029645a.

I'm leaving the merge to a maintainer.

openclaw#2454 made the Credential Gateway Driver ask the Backend client where
the gateway runs, so it can refuse token_url hosts in that namespace.
The real suite's Backend client, which reaches each Namespace's gateway
through a port-forward, had no placement(), so registering the first
source failed with 500 INTERNAL_ERROR. Return the Namespace's gateway
Service, and no placement when called without a namespace, as the
production Backend does for a bare serviceName.
@mrunalp

mrunalp commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

#2374 merged as a squash of 954910a3d, so I merged main into this branch (ca0988105) rather than rebasing. The branch is now this PR's own commits on top of main.

ClawSweeper's review (8b029645a):

  • Incomplete foreign-host echo (P2). Fixed in 155962577. test(openshell): prove the credential route and refuse untrusted upstreams #2374 merged without this fix, so this PR carries it. A 200 now counts only when curl exits 0, empty only when the echo reports length "0", and the probe's exit code is logged. A body that never arrives (for example a timeout, exit 28) fails the check.
  • Recovered tool credential (P2). Fixed in 8a4167950. Recovery requires 200 with curl exit 0, placeholder: "false", and the echo's digest equal to toolA.digest (tokenDelivered). During the outage tokenDelivered must be false.
  • Reconciliation during the gateway outage (P2). Fixed in 8a4167950. The fixed 60 s sleep is replaced by a bounded wait (180 s) for the redeploy's controller_work row to be queued with attempt_count >= 1 while the gateway StatefulSet still has 0 replicas. The phase then asserts gatewayStopped and deployment status queued.

@freeqaz's review:

  1. Tool-upstream recovery. The same fix as above, in 8a4167950.
  2. Redeploy attempted during the outage. The same fix as above, in 8a4167950. The coverage page now says "a redeploy fails at least one attempt and stays queued", which is what the test asserts.
  3. Key-leak check on the gateway-down turn. Fixed in 8a4167950. The turn's result is kept, and the leak check runs outside the try, so an AssertionError can no longer be caught and counted as a failed turn.

Nits:

  • runSandboxProbe. Done in 8a4167950. It checks every attempt for the model key, every tool source token and the untrusted-TLS source token before any message is built.
  • Model-outage error text. Not changed. Codex's final error text isn't a stable interface, and both outcomes (refused, or the 300 s turn timeout) fail closed, which is what the phase proves. Matching the text would make the test break on Codex wording changes without protecting additional OCC behavior.
  • Cleanup. Done in 8a4167950. The model-outage Service is deleted after DNS is restored. The outage Secret is deleted through DELETE /namespaces/:id/secrets/:secretId once its registration is gone (bounded wait; 204 or 404).
  • Long lines. Rewrapped in 8a4167950, along with three more lines from the merge.

One more change: 82a96793f repairs this suite on main. #2454 made the Credential Gateway Driver call backend.client.placement(), which the suite's port-forward Backend client lacked, so the first source registration on main returns 500 INTERNAL_ERROR. The client now returns the Namespace's gateway Service, and no placement without a namespace, as the production Backend does for a bare serviceName.

The real OpenShell suite passed on 82a96793f (base 8642bef0e) in 26.7 minutes:

credential route {"modelPlaceholder":{"inheritedByCommand":false,"readableFromAncestor":true},"foreignDestination":{"httpStatus":"403","exitCode":0,"received":"refused"},"modelHostFromCurl":{"httpStatus":"200","connectStatus":"000","exitCode":0},"proxyBypass":{"allowedHostExitCode":0,"deniedHostExitCode":7,"addressExitCode":7,"sandboxDns":"resolved"},"untrustedUpstream":{"httpStatus":null,"exitCode":52,"harnessSawProxyIssuer":true,"tlsFailures":1,"handshakes":0,"requests":0},"podNetwork":{"address":"ECONNREFUSED"}}
untrusted model upstream {"turnRejected":true,"tlsFailures":37,"handshakes":0,"requests":0}
tool upstream outage {"outage":{"httpStatus":"000","exitCode":52,"placeholder":null,"tokenDelivered":false},"recovered":{"httpStatus":"200","exitCode":0,"placeholder":"false","tokenDelivered":true}}
model provider outage {"turnRejected":true,"recoveredTurn":true,"sameRevision":true}
gateway outage {"runningAgentTurn":"failed","sourceRead":{"httpStatus":200,"state":"failed"},"registration":{"httpStatus":503,"code":"DEPENDENCY_UNAVAILABLE"},"redeployDuringOutage":{"status":"queued","work":{"state":"queued","reason_code":null,"attempt_count":1},"activeRevisionUnchanged":true,"gatewayStopped":true},"recovery":{"redeployActivated":true,"work":{"state":"claimed","reason_code":null,"attempt_count":5},"sourceState":"ready","registrationRecord":"deleting","turn":true,"refresh":[{"type":"oauth2-client-credentials","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}},{"type":"oauth2-refresh-token","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}},{"type":"oauth2-client-credentials","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}}]}}

The redeploy failed its first attempt while the gateway was stopped and stayed queued. All three OAuth2 sources hit NVIDIA/OpenShell#4526 after the restart and returned to ready on a forced rotation. No model key or placeholder appeared in the prepare, run or cleanup logs.

@clawsweeper re-review re-review

@clawsweeper

clawsweeper Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
Exact review queued.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Oct 14, 2026

@freeqaz-openai freeqaz-openai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2, in the conflict resolution please check the selected model credential. The exact-head Claw result resolves the prior findings; its real outage/recovery evidence is useful on the tuple tested. This is a new integration-oracle issue: main now defaults to a Codex access token, while this PR’s new absence checks name only the API-key slot. This is not evidence of a credential in actual output.

I reproduced three conflict blocks in two files against landed 04514330. In sandbox-driver-openshell-k3d-real.test.mjs, preserve main’s modelCredentialVariable placeholder check and this PR’s shared sandboxProbeFunction and runSandboxProbe. Also update the six non-conflicting oracle references: use main’s already-selected modelCredential in the probe’s credential list and the five model/gateway outage observation checks. Otherwise the list can omit the selected test value, while .includes(undefined) searches for the text “undefined,” not that value.

With only invented canaries in an isolated JS context, I exercised the actual selector and the six assertion operands. The mechanical resolution detected a planted selected value in 0/6 default-token checks (whether the unrelated API slot was absent or set); the corrected source detects it in 6/6. Native/custom-endpoint API mode remains 6/6, clean observations zero false positives. This discriminates the oracle; it is not six new live outage passes or a claim arbitrary output is safe.

In openshell-coverage.md keep main’s manual-full vs trusted-PR-advisory distinction and remove the obsolete “outages not covered.” I verified a complete four-file resolution applies to the exact main beforeimages. If #2440 lands first, retain its chart-version change; its proposed OpenShell 5a7728c3 is 16 commits behind the upstream #4527 fix by Git ancestry, so don’t remove the recovery workaround just because the version changes. Please bind ordinary CI and applicable outage qualification to the actual resulting source and selected credential mode. No new credential, value logging, bot rerun, cluster mutation or merge requested.

Resolve the Sandbox suite's conflicts with Codex PAT support: keep
main's selected-credential inheritance check and this branch's shared
probe helper. The probe helper and the outage phases now check main's
selected modelCredential rather than OPENAI_API_KEY, which is unset in
the default Codex token mode. Keep main's Backend client placement(),
which supersedes this branch's stub. The coverage page keeps main's
advisory-run sentence and drops the obsolete "Not covered" section.
…dbox Driver

openclaw#2048 made the OpenShell Sandbox Driver choose Codex isolation from the
admitted credential source's login mode, the third configureAgent
argument. The suite's wrapper dropped it, so OCC always selected
workspace-write. That matches the expectation in the default Codex token
mode, but API-key mode (OCC_TEST_OPENSHELL_MODEL_BASE_URL) failed its
danger-full-access check at the first deploy.
@mrunalp

mrunalp commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@freeqaz thanks. Addressed in 5122f1a25, a merge of main at 91ac04d69 (no rebase), plus 80805c826.

  • Conflicts. The route probe keeps main's modelCredentialVariable inheritance check and this branch's shared sandboxProbeFunction and runSandboxProbe.

  • Selected credential. All six references now use main's modelCredential: the probe helper's secret list and the five model-provider and gateway outage checks. No process.env.OPENAI_API_KEY remains in the suite.

  • Coverage page. It keeps main's manual-full vs trusted-PR-advisory sentence and drops "Not covered". The model-provider bullet now says "the model credential" rather than "the key".

  • Backend placement(). feat(openshell): support Codex PAT credentials #2048 added its own placement() to the suite's Backend client, built on OpenShellGateway. It supersedes this branch's stub (82a96793f), so the merge keeps main's version and this PR no longer carries that repair.

  • chore(openshell): bump pinned release to 0.1.4 #2440. It hasn't landed, so nothing changes here. When it does, the TODO(NVIDIA/OpenShell#4526) recovery stays until a pinned release includes fix(driver-kubernetes): wait for operator namespace listing before workspace checks NVIDIA/OpenShell#4527.

  • 80805c826, a main suite fix found by the API-key run. feat(openshell): support Codex PAT credentials #2048 made the Sandbox Driver choose Codex isolation from the admitted login mode (configureAgent's third argument), but the suite's configureAgent wrapper dropped it. OCC therefore always selected workspace-write, which matches the default token mode but fails API-key mode's danger-full-access check at the first deploy. The wrapper now passes the argument through.

Verification. The real OpenShell suite passed on 80805c826 in API-key mode (OCC_TEST_OPENSHELL_MODEL_BASE_URL=https://api.openai.com/v1, gpt-6-astra) in 22.8 minutes, so modelCredential was the API key:

credential route {"modelPlaceholder":{"inheritedByCommand":false,"readableFromHarness":true},"foreignDestination":{"httpStatus":"403","exitCode":0,"received":"refused"},"modelHostFromCurl":{"httpStatus":"200","connectStatus":"000","exitCode":0},"proxyBypass":{"allowedHostExitCode":0,"deniedHostExitCode":7,"addressExitCode":7,"sandboxDns":"resolved"},"untrustedUpstream":{"httpStatus":null,"exitCode":52,"harnessSawProxyIssuer":true,"tlsFailures":1,"handshakes":0,"requests":0},"podNetwork":{"address":"ECONNREFUSED"}}
untrusted model upstream {"turnRejected":true,"tlsFailures":30,"handshakes":0,"requests":0}
tool upstream outage {"outage":{"httpStatus":"000","exitCode":52,"placeholder":null,"tokenDelivered":false},"recovered":{"httpStatus":"200","exitCode":0,"placeholder":"false","tokenDelivered":true}}
model provider outage {"turnRejected":true,"recoveredTurn":true,"sameRevision":true}
gateway outage {"runningAgentTurn":"failed","sourceRead":{"httpStatus":200,"state":"failed"},"registration":{"httpStatus":503,"code":"DEPENDENCY_UNAVAILABLE"},"redeployDuringOutage":{"status":"queued","work":{"state":"queued","reason_code":null,"attempt_count":1},"activeRevisionUnchanged":true,"gatewayStopped":true},"recovery":{"redeployActivated":true,"work":{"state":"claimed","reason_code":null,"attempt_count":4},"sourceState":"ready","registrationRecord":"deleting","turn":true,"refresh":[{"type":"oauth2-client-credentials","afterRestart":"ready:-","rotated":null},{"type":"oauth2-refresh-token","afterRestart":"ready:-","rotated":null},{"type":"oauth2-client-credentials","afterRestart":"ready:-","rotated":null}]}}

No model key or placeholder appeared in the prepare, run or cleanup logs, and the cluster was removed. Two earlier runs on the same commit failed on the provider's "Selected model is at capacity" response. Run 45 had already passed every phase above before hitting it.

Not verified live: the default Codex token mode. I don't have a CODEX_ACCESS_TOKEN. QA advisory skips fork PRs, and the hosted openshell lane runs only from main, so that mode needs a maintainer dispatch or the post-merge run. In this run, the lane preparation's presence check for CODEX_ACCESS_TOKEN was satisfied with the same API key; the suite doesn't read it in API-key mode. This run didn't hit the NVIDIA/OpenShell#4526 race (all three OAuth2 sources stayed ready); the run on 82a96793f did, and recovered.

@freeqaz-openai freeqaz-openai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Free’s clanker here. My requested change is fixed in 80805c826; thank you. The probe and all five outage observations now check the selected model credential. I exercised those six checks with invented canaries in four token/API-key configurations: each detected the planted value, and none flagged the clean observations. The test also forwards the admitted login mode and uses the existing Backend placement implementation.

I read your real outage and recovery results as API-key mode only, not a live default-token run. Required CI and First Agent Smoke passed on this head; neither runs that full outage scenario. This approval clears my outdated changes request, not those evidence limits or the need to receive the current-head Claw review.

@freeqaz-openai

Copy link
Copy Markdown
Collaborator

Please review the conflict resolution and selected-credential fix on 80805c826. The completed review above covered 82a96793; no CI rerun or merge is requested.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

🦞👀
Exact review queued.

Re-review progress:

@freeqaz

freeqaz commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Free's clanker here (an AI agent working on Free's behalf). I reviewed 5122f1a25 (the main merge) and 80805c826, which is still the head.

  • The conflict resolution keeps the outage phases and swaps every leak assertion to main's modelCredential; the coverage page keeps main's advisory sentence. No new sleeps or fixed timeouts, so no added flake surface.
  • 80805c826 forwards the admitted login mode through the suite's configureAgent wrapper, which is the right fix for the API-key check against today's main.

One heads-up: #2515 removes that third configureAgent argument from the contract and makes OpenShellSandboxDriver select danger-full-access for every login mode. If it lands first, the forwarded argument becomes a no-op (harmless in the .mjs wrapper) and the API-key-mode expectation holds without it. Nothing to do here now.

Not landing this; it is yours to merge.

@freeqaz

freeqaz commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

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

Heads-up: #2515 landed on main at 01:20 UTC as a rebase merge (26d00dd..0fbd964), and this branch now conflicts with main in tests/integration/sandbox-driver-openshell-k3d-real.test.mjs. That's the only conflicting file.

Something to watch when you merge main: #2515 removed the third credentialSourceLoginMode argument from SandboxDriver.configureAgent. This branch's head commit 80805c8 ("pass the admitted login mode through the suite's Sandbox Driver") forwards exactly that argument, so it is now a no-op. It's probably best to drop it in the merge rather than keep it alongside main's version. Ten commits on main also touch this file (boundary-probe and paired-supervisor changes), so the merge isn't purely mechanical.

I haven't pushed anything to this branch.

@freeqaz-openai freeqaz-openai left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Free’s clanker here. #2515 landed after the last review, and GitHub now reports a merge conflict. I reproduced it with PR head 80805c82, live main at a79ddca3, and merge base 91ac04d6: all three docs merge cleanly; the integration test has one conflicting hunk.

In createIntegrationSandboxDriverFactory, keep main’s side of that hunk only: the shared configurationDriver, its harnessHome, and the two-argument configureAgent(configuration, harness). Please don’t take main’s entire file. The current Driver no longer reads the third login-mode argument; this suite selects the actual PAT or API-key credential independently. This resolution preserves both the landed Driver behavior and this PR’s six credential-refusal checks, without choosing a new isolation policy.

I checked syntax and exercised those six expressions with synthetic PAT and API-key canaries, including the negative control that an API-key-only check misses the PAT. I didn’t run the live outage suite; the resulting head still needs its own required CI and review. No branch write or rerun from me.

Landing order with #2600: I tested both orders against actual main 80705670a, this head 80805c826, and that head c856ccb4b. If #2600 lands first, the resolution above remains the only conflict for this PR. In the reverse order, Git also puts #2600’s replacement diagnostic opposite this PR’s three outage tests; retain both blocks. Please don’t choose an entire file side.

Both private integrations produced the identical complete tree (b5231a3fc8) and test blob (0182f3906). I verified the three outage functions, six changed diagnostic/reconciliation functions, and their call sites in the combined file; syntax and the six secret-refusal oracles in five synthetic PAT/API-key configurations passed. This is source and merge evidence, not a new live outage run or an instruction to force an order. The published branch and original author’s runtime qualification remain unchanged.

openclaw#2515 removed SandboxDriver.configureAgent's login-mode argument and
selects Codex full access for every login mode, so keep main's side of
the suite's Sandbox Driver wrapper: the shared configurationDriver, its
harnessHome and the two-argument configureAgent. That drops this
branch's login-mode forwarding (80805c8), which no longer has a
reader. The outage phases and the selected-credential checks are kept.
@mrunalp

mrunalp commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

@freeqaz thanks for the resolution notes. 4c46e5f8e merges main at b75d3e371 (after #2515 and #2600; no rebase).

The real OpenShell suite passed on 4c46e5f8e in API-key mode (OCC_TEST_OPENSHELL_MODEL_BASE_URL=https://api.openai.com/v1, gpt-6-astra) in 23.3 minutes:

credential route {"modelPlaceholder":{"inheritedByCommand":false,"readableFromHarness":true},"foreignDestination":{"httpStatus":"403","exitCode":0,"received":"refused"},"modelHostFromCurl":{"httpStatus":"200","connectStatus":"000","exitCode":0},"proxyBypass":{"allowedHostExitCode":0,"deniedHostExitCode":7,"addressExitCode":7,"sandboxDns":"resolved"},"untrustedUpstream":{"httpStatus":null,"exitCode":52,"harnessSawProxyIssuer":true,"tlsFailures":1,"handshakes":0,"requests":0},"podNetwork":{"address":"ECONNREFUSED"}}
untrusted model upstream {"turnRejected":true,"tlsFailures":30,"handshakes":0,"requests":0}
tool upstream outage {"outage":{"httpStatus":"000","exitCode":52,"placeholder":null,"tokenDelivered":false},"recovered":{"httpStatus":"200","exitCode":0,"placeholder":"false","tokenDelivered":true}}
model provider outage {"turnRejected":true,"recoveredTurn":true,"sameRevision":true}
gateway outage {"runningAgentTurn":"failed","sourceRead":{"httpStatus":200,"state":"failed"},"registration":{"httpStatus":503,"code":"DEPENDENCY_UNAVAILABLE"},"redeployDuringOutage":{"status":"queued","work":{"state":"queued","reason_code":null,"attempt_count":1},"activeRevisionUnchanged":true,"gatewayStopped":true},"recovery":{"redeployActivated":true,"work":{"state":"claimed","reason_code":null,"attempt_count":4},"sourceState":"ready","registrationRecord":"deleting","turn":true,"refresh":[{"type":"oauth2-client-credentials","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}},{"type":"oauth2-refresh-token","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}},{"type":"oauth2-client-credentials","afterRestart":"failed:refresh_configuration_invalid","rotated":{"httpStatus":200,"state":"ready"}}]}}

The redeploy failed its first attempt while the gateway was stopped and stayed queued. All three OAuth2 sources hit NVIDIA/OpenShell#4526 after the restart and returned to ready on a forced rotation. No model key or placeholder appeared in the prepare, run or cleanup logs, and the cluster was removed. As before, this is API-key mode only; the default Codex token mode has no live run.

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: 🦞 diamond lobster Very strong PR readiness with only minor 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