Skip to content

fix(runtime): preserve disableOAuth across headless paths#198

Merged
steipete merged 15 commits into
openclaw:mainfrom
feniix:feat/disable-oauth-connect-option
Jun 8, 2026
Merged

fix(runtime): preserve disableOAuth across headless paths#198
steipete merged 15 commits into
openclaw:mainfrom
feniix:feat/disable-oauth-connect-option

Conversation

@feniix

@feniix feniix commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Headless MCPorter callers can now suppress interactive OAuth without losing the connection pooling they need for daemon and repeated-tool workflows. disableOAuth now stays intact across direct runtime helpers, keep-alive daemon paths, and CLI commands instead of only working at the low-level connect() entry point.

This closes #197.

What changed

  • Preserves cached no-OAuth connections through callTool, listTools, listResources, and readResource, including the case where a caller pre-connects with disableOAuth: true and then uses higher-level helpers.
  • Forwards the option through keep-alive runtime wrappers, daemon protocol/host handling, and daemon fast-path calls so daemon-managed servers cannot accidentally re-enable OAuth.
  • Exposes --disable-oauth / --no-oauth for mcporter call, mcporter list, and mcporter resource.
  • Normalizes disableOAuth: false to the same cache identity as omitting the option.
  • Adds regression coverage for OAuth server suppression, 401 promotion suppression, helper reuse, daemon forwarding, CLI parsing, and tool metadata cache keys.

Demo

A local terminal evidence capture was recorded at:

/var/folders/l7/gpl83h991dbfbmbr1v8860f00000gn/T/mcporter-disable-oauth-demo-XXXXXX.KvYUH5Y2tf/disable-oauth-cli-evidence.svg

It shows the actual built CLI help for call, list, and resource exposing --disable-oauth.

Test plan

  • ./runner pnpm run check
  • ./runner pnpm test
  • ./runner pnpm exec tsx scripts/docs-list.ts
  • ./git diff --check

Compound Engineering
Pi

@clawsweeper

clawsweeper Bot commented Jun 6, 2026

Copy link
Copy Markdown

Codex review: needs changes before merge. Reviewed June 8, 2026, 4:40 PM ET / 20:40 UTC.

Summary
The PR adds a cache-friendly disableOAuth Runtime/CLI/daemon option, forwards it through helper, proxy, resource, and callOnce paths, and adds docs, a demo, and regression coverage.

Reproducibility: yes. from source inspection and the contributor’s live-output proof: current main couples maxOAuthAttempts: 0 to uncached connections, while the PR demonstrates pooled disableOAuth calls against a local MCP server. I did not run the test suite because this review is read-only.

Review metrics: 2 noteworthy metrics.

  • Changed surface: 31 files, +1050/-76. The branch crosses runtime, transport, daemon, CLI, docs, examples, and tests, so maintainer review should focus on API and auth behavior, not only unit coverage.
  • Public auth surfaces: Runtime types, CLI flags, daemon protocol changed. These are compatibility-sensitive surfaces that users and generated clients may depend on after release.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Remove the direct CHANGELOG.md entry and keep that release-note context in the PR body.
  • Confirm maintainer acceptance of disableOAuth as the public Runtime, CLI, daemon, proxy, and callOnce contract.

Risk before merge

  • [P1] This changes public Runtime types, daemon protocol params, CLI flags, and OAuth cache identity, so maintainers should intentionally accept disableOAuth as the durable API/flag shape before merge.
  • [P1] The PR’s direct CHANGELOG.md edit should be removed because release notes are release-owned even though the release-note context itself is useful.

Maintainer options:

  1. Accept the additive auth surface
    Maintainers can accept disableOAuth as the public Runtime, CLI, daemon, and proxy contract and merge after the release-owned changelog line is removed.
  2. Narrow the public API before merge
    If maintainers are not ready to expose the full type and CLI surface, keep the internal runtime fix but remove or defer the public exports and command flags.
  3. Pause for API-shape review
    If the related type/API follow-ups should be decided first, pause this PR until the disableOAuth, ConnectOptions, and callOnce surface is explicitly approved.

Next step before merge

  • [P2] A narrow automated repair can remove the release-owned CHANGELOG.md line without changing the runtime, CLI, daemon, docs, or tests.

Security
Cleared: No concrete security or supply-chain regression was found; the auth change is opt-in and narrows browser/OAuth behavior when enabled.

Review findings

  • [P3] Remove the release-owned changelog entry — CHANGELOG.md:7
Review details

Best possible solution:

Land the additive disableOAuth path after removing the direct changelog edit and confirming the public Runtime/CLI/daemon contract is the intended long-term shape.

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

Yes, from source inspection and the contributor’s live-output proof: current main couples maxOAuthAttempts: 0 to uncached connections, while the PR demonstrates pooled disableOAuth calls against a local MCP server. I did not run the test suite because this review is read-only.

Is this the best way to solve the issue?

Mostly yes: the additive option preserves the legacy maxOAuthAttempts: 0 behavior while giving headless callers a cache-friendly path. The safer pre-merge cleanup is to remove the release-owned changelog edit and have maintainers accept the new public API shape.

Full review comments:

  • [P3] Remove the release-owned changelog entry — CHANGELOG.md:7
    The PR adds a release note directly to CHANGELOG.md, but OpenClaw release notes are release-owned. Please keep this useful release-note context in the PR body or commit message and leave the changelog entry for the release owner.
    Confidence: 0.93

Overall correctness: patch is correct
Overall confidence: 0.86

AGENTS.md: found, but no applicable review policy affected this item.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 8f74252a4d89.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR comments include copied live output from a local MCP mock-server demo showing pooled disableOAuth behavior plus targeted regression-test output.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR comments include copied live output from a local MCP mock-server demo showing pooled disableOAuth behavior plus targeted regression-test output.
  • remove rating: 🌊 off-meta tidepool: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P2: This fixes a normal-priority headless OAuth/runtime pooling problem with limited but real impact on daemon and scripted callers.
  • merge-risk: 🚨 compatibility: The PR adds public options and daemon protocol fields and changes cache identity for disabled-OAuth connections.
  • merge-risk: 🚨 auth-provider: The PR changes OAuth session creation, cached-token use, and 401 promotion behavior when the new suppression option is active.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR comments include copied live output from a local MCP mock-server demo showing pooled disableOAuth behavior plus targeted regression-test output.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR comments include copied live output from a local MCP mock-server demo showing pooled disableOAuth behavior plus targeted regression-test output.
Evidence reviewed

Acceptance criteria:

  • [P1] ./git diff --check.

What I checked:

  • Repository policy read: AGENTS.md was read in full; .agents/maintainer-notes was not present, and the only .agents file found was the crabbox skill. (AGENTS.md:1, 8f74252a4d89)
  • Current main lacks the cache-friendly OAuth-suppression path: On current main, connect() only caches when maxOAuthAttempts is undefined, while helper calls only pass allowCachedAuth; there is no disableOAuth option in the Runtime surface. (src/runtime.ts:295, 8f74252a4d89)
  • PR implements the runtime inheritance and cache split: The PR head adds public disableOAuth options, inherits the cached no-OAuth posture for helper calls, and makes cache identity distinguish disabled-OAuth connections from normal OAuth-capable connections. (src/runtime.ts:353, 221de0ab08c1)
  • PR suppresses OAuth session creation and 401 promotion when requested: The transport layer short-circuits both OAuth session creation and unauthorized fallback promotion when disableOAuth is true. (src/runtime/transport.ts:194, 221de0ab08c1)
  • Regression coverage added: The PR adds integration tests for cache reuse, legacy maxOAuthAttempts: 0 behavior, cache eviction when the OAuth posture changes, and pre-connect plus helper reuse. (tests/runtime-integration.test.ts:137, 221de0ab08c1)
  • Diff hygiene check: The branch diff has no whitespace errors according to git diff --check. (221de0ab08c1)

Likely related people:

  • Peter Steinberger: Current main blame attributes the core runtime cache/OAuth helper paths, daemon forwarding shape, server proxy schema fetch, and OAuth transport decisions to release commit 94e65ba, and later daemon list handling to 56be50f. (role: introduced behavior and recent area contributor; confidence: high; commits: 94e65ba0572e, 56be50f76354, 8f74252a4d89; files: src/runtime.ts, src/runtime/transport.ts, src/daemon/runtime-wrapper.ts)
  • Lil Z: Recent current-main daemon work in 68b2289 touched daemon process metadata near the forwarding path this PR extends. (role: recent daemon adjacent contributor; confidence: medium; commits: 68b228943c38; files: src/daemon/host.ts)
  • LDMB123: Recent current-main runtime work in 2bf7a5e touched the same runtime file around replay/record plumbing adjacent to the connection cache code. (role: recent runtime adjacent contributor; confidence: medium; commits: 2bf7a5eab23f; files: src/runtime.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d22606a6dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/runtime.ts Outdated
try {
const { client } = await this.connect(server, {
allowCachedAuth: true,
disableOAuth: this.effectiveDisableOAuthForOperation(server, options.disableOAuth),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the preconnected no-OAuth cache slot

When a caller does the documented headless setup await runtime.connect(server, { disableOAuth: true }) and then invokes callTool() without restating every option, this added inherited disableOAuth still gets paired with allowCachedAuth: true here. The cached client from connect() was stored with allowCachedAuth: undefined, so connect() treats the flags as different and closes/reopens the transport instead of reusing the disabled-OAuth connection; the same mismatch affects listTools()'s default. This defeats the pooling guarantee for the common pre-connect path unless callers know to also pass allowCachedAuth: true to connect().

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 6, 2026
feniix added a commit to feniix/mcporter that referenced this pull request Jun 6, 2026
…lTool/listTools

Addresses PR openclaw#198 review comment r3366238654.

The documented headless setup is:

    await runtime.connect(server, { disableOAuth: true });
    await runtime.callTool(server, 'foo', { ... });

The first call stored the cache slot with `allowCachedAuth: undefined`,
but `callTool()` internally calls `this.connect(server, {
allowCachedAuth: true, disableOAuth: <effective>: true })` and the
cache-match check treated the two options shapes as structurally
different:

    existing.allowCachedAuth (undefined)
       !== options.allowCachedAuth (true)
       && options.allowCachedAuth !== undefined
    => MISMATCH => evict + reopen transport

Every first callTool / listTools after a pre-connect spawned a fresh
transport, defeating the pooling guarantee that motivated the
disableOAuth option in the first place. Same shape affected `listTools`
(which defaults `allowCachedAuth: options.allowCachedAuth ?? true`).

Fix: normalize at the connect() entrypoint. A `disableOAuth: true`
caller has no path to interactive OAuth, so cached-token application
is the only auth they can ever use — default `allowCachedAuth: true`
when the caller didn't pick a side. Explicit `false` is honored
(header-only / anonymous callers). The normalized value flows through
both the cache lookup and the cache write so subsequent internal
callers compose without eviction.

Two regression tests added to `tests/runtime-integration.test.ts`:

  - `preserves the cached client across connect(disableOAuth:true) →
    callTool() (no implicit eviction)`
  - `preserves the cached client across connect(disableOAuth:true) →
    listTools() (no implicit eviction)`

Both call `runtime.connect(disableOAuth:true)`, then invoke the
internal-cached path (callTool or listTools), then re-call
`runtime.connect(disableOAuth:true)` and assert the resulting
ClientContext is `=== ` the first one. Both tests fail without this
fix (the second connect returns a new ClientContext because the first
was evicted).

`pnpm test` 723 pass / 3 skip / 0 fail. `pnpm lint` + `pnpm
typecheck` clean. No push.
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. 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: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jun 6, 2026
feniix added a commit to feniix/mcporter that referenced this pull request Jun 6, 2026
Demonstrates the three patterns under the new `disableOAuth` option
against a local mock MCP server (no real auth). Reproducible artifact
for PR openclaw#198 review proof.

Patterns demonstrated:

* Legacy `maxOAuthAttempts: 0` (uncached): 5 connect() calls produce
  5 distinct ClientContexts. Existing contract preserved.
* `disableOAuth: true` on every connect: 5 calls produce 1
  ClientContext. Cache reuse under cache-friendly suppression.
* Documented headless setup — pre-connect(disableOAuth: true) +
  5 callTool() — proves the pre-connected slot survives the implicit
  internal connect path. Directly demonstrates the fix from b0e3e2e.

Run: `pnpm tsx examples/headless-pooling-demo.ts`

Sample output is intentionally redacted to no PII / no secrets: a local
http://127.0.0.1:<random-port>/mcp server with a public `add` tool.
@feniix

feniix commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Real-behavior proof — PR #198

Two artifacts: vitest output showing the regression tests pass, and a
runnable demo (examples/headless-pooling-demo.ts) that demonstrates the
three patterns against a local mock MCP server.

Reproducible from this branch:

$ pnpm test -- tests/runtime-integration.test.ts --reporter=verbose
$ pnpm tsx examples/headless-pooling-demo.ts

1. Regression tests (vitest)

The integration test suite carries 8 cases that lock in the new
disableOAuth contract. All pass.

 RUN  v4.1.5 /Users/feniix/src/spantree/mcporter

 ✓ tests/runtime-integration.test.ts > runtime integration > lists tools and calls a tool over HTTP 27ms
 ✓ tests/runtime-integration.test.ts > runtime integration > lists and reads resources over HTTP 4ms
 ✓ tests/runtime-integration.test.ts > runtime integration > reuses cached connection when disableOAuth: true is passed 6ms
 ✓ tests/runtime-integration.test.ts > runtime integration > treats disableOAuth: false like omitted for cache identity 2ms
 ✓ tests/runtime-integration.test.ts > runtime integration > maxOAuthAttempts: 0 still bypasses the cache (existing contract preserved) 2ms
 ✓ tests/runtime-integration.test.ts > runtime integration > evicts and re-establishes the cached client when disableOAuth flag changes 3ms
 ✓ tests/runtime-integration.test.ts > runtime integration > preserves the cached client across connect(disableOAuth:true) → callTool() (no implicit eviction) 3ms
 ✓ tests/runtime-integration.test.ts > runtime integration > preserves the cached client across connect(disableOAuth:true) → listTools() (no implicit eviction) 4ms

 Test Files  1 passed (1)
      Tests  8 passed (8)

The last two cases are the targeted regression for review comment
r3366238654
— they prove that connect(disableOAuth: true) followed by callTool()
or listTools() does NOT evict the pre-connected slot.

Full suite (all 124 test files): 723 pass / 3 skipped / 0 fail.
pnpm lint + pnpm typecheck clean.

2. Demonstrating the three patterns end-to-end

examples/headless-pooling-demo.ts starts a local express + MCP mock
server with a public add tool, then drives the runtime through three
patterns side-by-side, counting distinct ClientContext objects via
identity.

$ pnpm tsx examples/headless-pooling-demo.ts
[demo] Mock MCP server listening at http://127.0.0.1:55082/mcp

[demo] Pattern A — legacy maxOAuthAttempts: 0
[demo]   5 connect() calls → 5 distinct ClientContexts
[demo]   Expected: 5 (legacy contract: cache disabled when maxOAuthAttempts is set)
[demo]   Result:   OK

[demo] Pattern B — disableOAuth: true on every connect
[demo]   5 connect() calls → 1 distinct ClientContexts
[demo]   Expected: 1 (cache reuse under cache-friendly suppression)
[demo]   Result:   PASS

[demo] Pattern C — pre-connect(disableOAuth:true) + 5 callTool()
[demo]   Sum of 5 add() results: 25
[demo]   Post-callTool connect() === pre-connect ClientContext: true
[demo]   Expected: true (no implicit eviction from callTool internals)
[demo]   Result:   PASS

What each pattern proves:

  • Pattern A (existing contract preserved): callers that opt out via
    the legacy maxOAuthAttempts: 0 knob still bypass the cache. No
    behavior change for existing code.
  • Pattern B (the new contract works): repeated connect() calls
    with disableOAuth: true return the same cached ClientContext, so
    the daemon use case — wrapRuntimeForDaemon forcing this option on
    every internal connect — actually pools transports instead of
    spawning one per RPC.
  • Pattern C (the review-comment regression): the documented setup
    await runtime.connect(server, { disableOAuth: true }) followed by
    callTool() keeps the pre-connected slot. The 5 add() invocations
    succeed (sum = 0+1 + 1+2 + 2+3 + 3+4 + 4+5 = 1+3+5+7+9 = 25) and the
    post-callTool connect() returns === the original ClientContext.

OAuth-suppression semantics under disableOAuth: true (the
shouldEstablishOAuth / maybePromoteHttpDefinition short-circuits)
are exercised by the existing unit tests in
tests/runtime-transport.test.ts. The demo's mock server has no
auth: 'oauth' definition so it doesn't exercise the OAuth code path —
proving the cache fix end-to-end on a real transport was the goal here.

@feniix

feniix commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 6, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added 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. and removed 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. labels Jun 6, 2026
feniix added a commit to feniix/mcporter that referenced this pull request Jun 6, 2026
…ools

Addresses PR openclaw#198 review comment r3366307210 (clawsweeper proxy gap).

The Proxy returned by `createServerProxy` calls `ensureMetadata()` on
every tool invocation, which fires `runtime.listTools(server, {
includeSchema: true })` for schema discovery. That call ran BEFORE the
proxy parsed the caller's options bag, so a `proxy.tool({ ... }, {
disableOAuth: true })` invocation on an OAuth server with no cached
schema could still trigger an interactive OAuth flow during metadata
fetch — defeating the no-browser guarantee the option was meant to
provide.

Fix:

* Pre-scan callArgs once for `disableOAuth: true` before invoking
  `ensureMetadata`. The scan is a single linear pass over the
  already-present argument list and short-circuits on the first match.
* Extend `ensureMetadata(toolName, { disableOAuth? })` and forward the
  flag to the underlying `runtime.listTools(serverName, { includeSchema:
  true, disableOAuth: true })` call.
* The schema-fetch path that was vulnerable now inherits the same
  no-OAuth posture as the eventual `runtime.callTool` invocation. End-
  to-end no-browser guarantee is preserved across the proxy interface.

Regression test in `tests/server-proxy.test.ts`:

  > threads disableOAuth through schema discovery so
  > proxy.tool({disableOAuth:true}) cannot trigger OAuth during
  > metadata fetch

Asserts BOTH:
- `runtime.listTools` called with `{ includeSchema: true, disableOAuth:
  true }`
- `runtime.callTool` called with the eventual tool args and
  `disableOAuth: true`

Locks the contract on both halves so a future refactor that re-introduces
the gap on either side will fail loudly.

Full suite: 724 pass / 3 skipped / 0 fail. `pnpm check` (format + lint
+ typecheck) clean.
@feniix

feniix commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the proxy schema-discovery gap from the last review.

dfd1b6d (fix(server-proxy): thread disableOAuth through schema-discovery listTools) threads disableOAuth from the caller's options into the proxy's pre-call runtime.listTools(server, { includeSchema: true }) so the schema-fetch and the eventual callTool share the same no-OAuth posture. End-to-end no-browser guarantee now holds across the proxy interface.

Regression test in tests/server-proxy.test.ts asserts both:

  • runtime.listTools('mock', { includeSchema: true, disableOAuth: true })
  • runtime.callTool('mock', 'some-tool', { args: { foo: 'bar' }, disableOAuth: true })

pnpm test: 724 pass / 3 skipped / 0 fail. pnpm check (format + lint + typecheck) clean.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 6, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added 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. rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. 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. 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. labels Jun 6, 2026
feniix and others added 10 commits June 8, 2026 21:30
…h suppression)

Closes openclaw#197.

Long-running headless callers (daemons, scheduled jobs, CI workers) need
to suppress the interactive OAuth flow without losing connection caching.
The only existing knob — `maxOAuthAttempts: 0` — couples those two concerns
because `useCache` is gated on `options.maxOAuthAttempts === undefined`.
Daemons that wrap `connect` to force `maxOAuthAttempts: 0` end up spawning
a fresh transport per `callTool`/`listTools` and `runtime.close()` cannot
reap any of them.

Add an additive `disableOAuth: boolean` option that suppresses OAuth at
the transport layer (short-circuits `shouldEstablishOAuth` and
`maybePromoteHttpDefinition`) but preserves caching. The cache entry
metadata gains a `disableOAuth` field so connections established with
the flag don't share a slot with connections that could refresh into an
OAuth flow — switching the flag between calls evicts and re-establishes,
mirroring the existing `allowCachedAuth` mismatch path.

Backward compatibility:

* `maxOAuthAttempts: 0` keeps its legacy escape-the-cache contract
  unchanged. Existing callers see no behavior change.
* `skipCache: true` keeps its behavior unchanged.
* `disableOAuth` defaults to undefined; only opt-in changes behavior.

Also export `ConnectOptions` from `runtime.ts` and add the parameter to
the `Runtime.connect` interface signature — the implementation already
accepted options at runtime but the interface only exposed
`connect(server)`, so callers couldn't pass options through the type
system. (Pre-existing gap surfaced by adding the new test coverage.)

Tests added to `tests/runtime-integration.test.ts`:

* `reuses cached connection when disableOAuth: true is passed` — two
  calls return the same ClientContext, `close()` reaps it.
* `maxOAuthAttempts: 0 still bypasses the cache (existing contract
  preserved)` — regression guard.
* `evicts and re-establishes the cached client when disableOAuth flag
  changes` — the core eviction semantic.

`pnpm test` (709 pass / 3 skip), `pnpm lint`, `pnpm typecheck` all
green.
…lTool/listTools

Addresses PR openclaw#198 review comment r3366238654.

The documented headless setup is:

    await runtime.connect(server, { disableOAuth: true });
    await runtime.callTool(server, 'foo', { ... });

The first call stored the cache slot with `allowCachedAuth: undefined`,
but `callTool()` internally calls `this.connect(server, {
allowCachedAuth: true, disableOAuth: <effective>: true })` and the
cache-match check treated the two options shapes as structurally
different:

    existing.allowCachedAuth (undefined)
       !== options.allowCachedAuth (true)
       && options.allowCachedAuth !== undefined
    => MISMATCH => evict + reopen transport

Every first callTool / listTools after a pre-connect spawned a fresh
transport, defeating the pooling guarantee that motivated the
disableOAuth option in the first place. Same shape affected `listTools`
(which defaults `allowCachedAuth: options.allowCachedAuth ?? true`).

Fix: normalize at the connect() entrypoint. A `disableOAuth: true`
caller has no path to interactive OAuth, so cached-token application
is the only auth they can ever use — default `allowCachedAuth: true`
when the caller didn't pick a side. Explicit `false` is honored
(header-only / anonymous callers). The normalized value flows through
both the cache lookup and the cache write so subsequent internal
callers compose without eviction.

Two regression tests added to `tests/runtime-integration.test.ts`:

  - `preserves the cached client across connect(disableOAuth:true) →
    callTool() (no implicit eviction)`
  - `preserves the cached client across connect(disableOAuth:true) →
    listTools() (no implicit eviction)`

Both call `runtime.connect(disableOAuth:true)`, then invoke the
internal-cached path (callTool or listTools), then re-call
`runtime.connect(disableOAuth:true)` and assert the resulting
ClientContext is `=== ` the first one. Both tests fail without this
fix (the second connect returns a new ClientContext because the first
was evicted).

`pnpm test` 723 pass / 3 skip / 0 fail. `pnpm lint` + `pnpm
typecheck` clean. No push.
Demonstrates the three patterns under the new `disableOAuth` option
against a local mock MCP server (no real auth). Reproducible artifact
for PR openclaw#198 review proof.

Patterns demonstrated:

* Legacy `maxOAuthAttempts: 0` (uncached): 5 connect() calls produce
  5 distinct ClientContexts. Existing contract preserved.
* `disableOAuth: true` on every connect: 5 calls produce 1
  ClientContext. Cache reuse under cache-friendly suppression.
* Documented headless setup — pre-connect(disableOAuth: true) +
  5 callTool() — proves the pre-connected slot survives the implicit
  internal connect path. Directly demonstrates the fix from b0e3e2e.

Run: `pnpm tsx examples/headless-pooling-demo.ts`

Sample output is intentionally redacted to no PII / no secrets: a local
http://127.0.0.1:<random-port>/mcp server with a public `add` tool.
…ools

Addresses PR openclaw#198 review comment r3366307210 (clawsweeper proxy gap).

The Proxy returned by `createServerProxy` calls `ensureMetadata()` on
every tool invocation, which fires `runtime.listTools(server, {
includeSchema: true })` for schema discovery. That call ran BEFORE the
proxy parsed the caller's options bag, so a `proxy.tool({ ... }, {
disableOAuth: true })` invocation on an OAuth server with no cached
schema could still trigger an interactive OAuth flow during metadata
fetch — defeating the no-browser guarantee the option was meant to
provide.

Fix:

* Pre-scan callArgs once for `disableOAuth: true` before invoking
  `ensureMetadata`. The scan is a single linear pass over the
  already-present argument list and short-circuits on the first match.
* Extend `ensureMetadata(toolName, { disableOAuth? })` and forward the
  flag to the underlying `runtime.listTools(serverName, { includeSchema:
  true, disableOAuth: true })` call.
* The schema-fetch path that was vulnerable now inherits the same
  no-OAuth posture as the eventual `runtime.callTool` invocation. End-
  to-end no-browser guarantee is preserved across the proxy interface.

Regression test in `tests/server-proxy.test.ts`:

  > threads disableOAuth through schema discovery so
  > proxy.tool({disableOAuth:true}) cannot trigger OAuth during
  > metadata fetch

Asserts BOTH:
- `runtime.listTools` called with `{ includeSchema: true, disableOAuth:
  true }`
- `runtime.callTool` called with the eventual tool args and
  `disableOAuth: true`

Locks the contract on both halves so a future refactor that re-introduces
the gap on either side will fail loudly.

Full suite: 724 pass / 3 skipped / 0 fail. `pnpm check` (format + lint
+ typecheck) clean.
The PR originally exposed two CLI names for the same intent:
--disable-oauth (mirroring the JS option `disableOAuth: true`) and
--no-oauth (the GNU-style boolean opt-out). Two names for one
behavior is noise — documentation has to mention both, users have to
learn both, and they invite drift.

--no-oauth is the right shape for a per-invocation boolean opt-out:
- Matches the dominant unix convention (git --no-verify, npm --no-save,
  bun --no-cache, curl --no-progress-meter).
- Shorter to type.
- Composes naturally with other flags in scripts.

The JS option name stays `disableOAuth: boolean` — that's the right
shape for a JS option (verb+noun, no Boolean-negation prefix
ambiguity), and the JS and CLI naming conventions are genuinely
different domains.

Removed CLI registrations + help text + internal forwarding for
--disable-oauth across:
- src/cli/call-arguments.ts (FLAG_HANDLERS registration)
- src/cli/call-command.ts (internal listArgs forwarding, 2 sites)
- src/cli/call-help.ts (help text)
- src/cli/list-command.ts (help text)
- src/cli/list-flags.ts (token check)
- src/cli/resource-command.ts (token check + help text)
- docs/cli-reference.md (3 references)

Renamed test cases that exclusively exercised --disable-oauth to
exercise --no-oauth instead, preserving regression coverage:
- tests/call-arguments.test.ts
- tests/cli-list-flags.test.ts
- tests/cli-resource-command.test.ts

The internal cache-key fragment `disable-oauth:` in
src/cli/tool-cache.ts is kept — it mirrors the JS option name (which
stays `disableOAuth`), not the CLI flag.

Tests: 724 passed, 3 skipped, 0 failed.
Lint: 0 warnings, 0 errors.
Typecheck: clean.
@steipete steipete force-pushed the feat/disable-oauth-connect-option branch from 625463c to 221de0a Compare June 8, 2026 20:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 221de0ab08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server-proxy.ts

const KNOWN_OPTION_KEYS = new Set(['tailLog', 'timeout', 'stream', 'streamLog', 'mimeType', 'metadata', 'log']);
const KNOWN_OPTION_KEYS = new Set([
'disableOAuth',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve proxy args when using disableOAuth inline

When a schema-backed proxy call passes the new flag in the same object as its tool arguments, e.g. proxy.someTool({ foo: 'bar', disableOAuth: true }), adding disableOAuth to KNOWN_OPTION_KEYS makes the later treatAsArgs predicate classify the whole object as call options rather than arguments. The proxy then forwards disableOAuth but never places foo under finalOptions.args, so required tool inputs are dropped unless callers know to split this into two objects or use { args: ... }.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added 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 rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 8, 2026
@steipete

steipete commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification on f837500:

  • Focused proxy/runtime suite: 9 files, 81 tests passed.
  • pnpm check: format, lint, and typecheck passed.
  • pnpm test: 125 files passed, 1 skipped; 773 tests passed, 3 skipped.
  • pnpm tsx examples/headless-pooling-demo.ts: legacy uncached, cache-friendly suppression, and pre-connect reuse patterns all passed.
  • Local patch autoreview: clean.
  • Full branch autoreview against origin/main: clean.
  • GitHub CI: Ubuntu, macOS, Windows, and both Socket checks passed.

The final proxy handling preserves schema-owned option names, uses uncached no-authorize discovery for ambiguous argument shapes, and isolates concurrent schema discovery by OAuth posture.

@steipete steipete merged commit 3e27b64 into openclaw:main Jun 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. 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.

Add disableOAuth connect option — maxOAuthAttempts: 0 defeats the connection cache

2 participants