Skip to content

fix(router): run free docs without a wallet or funding - #531

Open
vraspar wants to merge 4 commits into
mainfrom
vraspar/free-docs-without-funds
Open

vraspar wants to merge 4 commits into
mainfrom
vraspar/free-docs-without-funds

Conversation

@vraspar

@vraspar vraspar commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Scope

Let an agent run explicitly selected free docs after the user chooses “Not now” for funding, without a wallet or a paid routing call.

  • {tool, query} binds the selected free service and uses the existing executor. A changed service, priced contract or payment challenge cannot turn it into a payment or funding prompt.
  • Completed selected-free calls show the normal named Called Context7 docs · free row in Claude Code, with the existing expanded details.
  • Missing input and unsupported free responses keep recovery on the free path. Zero-price x402 tools retain their existing signed authentication for entitled results; that is separate from anonymous free docs.
  • Agent instructions, permissions documentation, shared fixtures and a patch changeset describe the same behavior.

NOT doing

Changing router selection, fees or automatic hooks; adding endpoints, an executor, a free ToolSpec variant, manual routing mode, or a general manual-run command.

Definition of Done

  • Targeted pay/tool/MCP/wire checks: 353 tests passed with one worker. The zero-price authentication and free-recovery regressions reproduced before the fixes.
  • Earlier Claude render-tree checks (renderer unchanged by this sync): 18 passed for terminal and Desktop, including folded/expanded output. The selected-free success row reproduced as missing before the fix.
  • Typecheck, affected-file lint, formatting and whitespace checks passed. Independent follow-up review found no actionable regressions.
  • Earlier end-to-end QA at 33380258: actual SDK/MCP → candidate server → public Context7 returned useful Zod docs without wallet, signer or payment calls. Fresh no-Node Claude Code auto-mode QA installed, chose defaults and “Not now,” reloaded, and completed an explicitly free docs task without a funding question. No spend/payment ledger was created.
  • That native fixture used routing-fee mode off; its proxy refused two unsigned hook-side paid-route requests. Required-fee gating is covered separately by actual server-handler tests. The renderer/auth follow-up is covered by the targeted checks above, not a claimed new native install recording.
  • Updated after fix(onboarding): simplify spending and budget wording #529 merged: conflicts resolved, retaining daily-budget wording and current adaptive routing-deposit terms. Fresh pay/tool/MCP/wire checks passed all 353 tests; 39 backoff/fee tests also passed. Typecheck, lint, formatting and independent integration review passed. All five CI jobs passed on 93bcbf12, including Node 24, Windows and both installers. Run. Aether approved this exact head; Greptile also passed; all checks are green and this head is approved.

Notes for reviewer

The initial Node 24 sync run timed out in the existing backoff fetch mock. Forced garbage collection reproduced it: a copied Request signal lost abort propagation. The fixture now follows the original signal, checks an already-aborted signal and uses a one-time listener, matching the existing paid-fetch fixture. The forced-GC regression passes; no production timeout or backoff behavior changed.

The owner has consolidated this work into the router-quality plan, #40. The folding decision explicitly keeps the current free-docs pair and drops the proposed free-spec redesign. R9 later adds one-call manual execution and a thin CLI command; R10 later adds routing modes after account/policy work. Neither #522 nor #524 is a dependency here.

Server support for {tool, query} goes first. This form never retries through paid general routing. JSON request/response fixtures match the server byte for byte. Ordinary unfunded tasks still receive no automatic free-docs offer; this PR makes no claim otherwise.

Refs #417. Saved policies and connected accounts remain #506/#507's scope.

RetriggerView in GreptileConfidence Score: 5/5

The changes since the previous review appear safe to merge.

Summary

The PR adds selected free docs lookups through {tool, query}, blocks payment escalation, and shows completed free calls by name.

  • Agents can run chosen free docs without paying a routing fee.
  • Anonymous zero-price lookups stop before payment handling.
  • Claude Code labels completed selected-free docs calls as free.
  • The router backoff tests keep checking the same behavior.

Reviews (4) · Last reviewed commit: "test(router): retain the backoff fetch a..." · Reviewed by Greptile

@vraspar vraspar added priority: critical Blocks other PRs; merge-priority ci Run full CI on a ready PR; remove before further review iteration labels Oct 27, 2026

@A1igator A1igator left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at 3338025: 0 blocking, 1 should fix, 0 nits

Should fix

  1. The PR body references the private repository three times (BackTrackCo/tenjin#1045, #907, #1026), which this public repository's rule forbids: each reference publishes a private PR number and creates a cross-repo backlink. Replace them with plain words (the server-side change that adds the {tool, query} form) and keep #417, which is this repository's own. #531

Nothing else I would hold the code on: the {tool, query} path never calls the routing payer or retries through paid routing, the client refuses any execute whose id or zero price differs from the selection before the provider is called, and the zero-ceiling 402 refusal in pay.ts is reachable only from the router seam (the tenjin pay command passes no terms), so a paid call with an advertised price above zero never meets it. Both wire fixtures match the server branch byte for byte.

@vraspar

vraspar commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the PR-description finding: replaced the private-repository references with a plain description of the server dependency and kept this repository's #417 reference. No code or user-flow change.

@aether-agent aether-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Reviewer · Review PR #531: fix(router): run free docs without a wallet or funding · claude-code · claude-opus-5-5

Deep review of 3338025. The core of the change holds up. {tool, query} never calls deps.route. The new guard refuses spec, discovered, and any execute that is paid or for a different capability. The zero-price 402 guard in executePay runs before decodeChallenge, admission, openWallet and SIWX. A missing or empty wallet is never touched on this path, and routerOff and the credential mask still run first. CI is green at this head.

Two p2s:

  • fee_required answer: if the server answers {tool, query} with native/fee_required, the call reaches the paid-path fee branch. It tells the agent Tenjin can't pay its routing fee and clears the paid-path-absent memo.
  • Claude Code mod: the mod draws no row for a successful {tool, query} lookup, so the new main flow is invisible in the normal view.

The rest are nits: misleading refusal texts, the zero-price guard's reach, and docs/fixture drift.

Test gaps (not posted inline):

  • No test covers the new refusal block at tool.ts:463-471: {tool, query} plus id/input/choice/count/job, a blank or >200-char tool, or a credential-shaped tool id. The old {tool, query} case at tool.test.ts:2702 became {tool, input}, which goes through readToolList.
  • The "no wallet" assertion in the first new tool test (tool.test.ts:2518-2567) can't fail. The docs leg answers 200, and the wallet is only reachable after a 402. Add a case where the docs URL answers 402 with amount 0, through requestTool.
  • Skipping the stale-memo retry for args.tool (tool.ts:655) is untested; no test has a fee_required answer with a route set.
  • The guard test (tool.test.ts:2569-2585) covers only execute answers, not spec or discovered.
  • The real {tool, query} request body is only checked with toMatchObject/arrayContaining(['free-query']) (tool.test.ts:2558-2562, mcp.test.ts:165). wire-tool-request-free-query.json has accepts: ["spec","free-query"], but the client sends the full acceptsFor(...) list plus free-query and harness. So the fixture shared with the server pins buildToolSpecBody, not the body this client sends. The "byte for byte" test at wire.test.ts:231 also uses order-insensitive toEqual.
  • tool.test.ts:2774-2788 still mocks the old-client spec_missing → request({query}) answer for {tool: 'context7-docs'}, but this client now always sends free-query.
  • The hand-written "carries nothing about money" list at wire.test.ts:330-347 leaves out the new request fixture.
  • The pay.test.ts:259 row with amount 100000 was already refused before a wallet opened; only its details.status assertion is new.

Not posted inline (single source, unverified):

  • tool.ts:686: capabilityId !== args.tool.trim() is case-sensitive. This only matters if the server matches row ids without regard to case, and the server is not in this repo.

No line to anchor:

  • No changeset was added. RELEASING.md asks for one with any shippable change, and .changeset/ only has onboarding-next-action.md. (nit)

Coverage: every changed and context line in the 28 hunks was audited (253 ledger rows, 0 lines missing). semgrep (p/default, p/security-audit) found nothing on or near a changed line. eslint was skipped because dependencies are not installed.

Comment thread src/router/tool.ts Resolved
Comment thread src/router/mcp.ts Resolved
Comment thread src/router/tool.ts Resolved
Comment thread src/router/tool.ts Resolved
Comment thread src/router/tool.ts Resolved
Comment thread src/router/decision.ts Resolved
Comment thread README.md Resolved
Comment thread src/router/fixtures/README.md Resolved
Comment thread src/router/tool.ts Resolved
@A1igator

A1igator commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Please close this in favor of #524.

The free path here is pull only: the agent gets value without funding only when it already knows it wants docs and names context7-docs itself. The hook is unchanged, so the router never offers anything to an unfunded user. That takes the router out of the loop, which is the part of the product we are trying to show.

#524 covers the same unfunded moment better. An explicit search runs discovery across the Tenjin catalog, the public Bazaar registries and Orthogonal for free, with the source, inputs and advertised price on each result, and no wallet opens. Discovery stays free, the call stays paid, and the router's judgement is still in front of the user.

A free call of the Context7 row after a #524 browse is welcome later as a low-priority follow-on built on that discovery, not as a path of its own and not at critical priority.

Thank you for the careful work on the zero-price guard and the fixtures. The review stands as a record of what the code does.

@vraspar

vraspar commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Keeping this open for explicitly selected free docs. #524 provides free discovery and inspection, but deliberately runs no provider task. This supplies execution of the selected free Context7 lookup without paid routing or funding. They are complementary: discover a tool, then run the selected free one.

The hook limitation is valid: neither change makes ordinary unfunded tasks automatically receive free-docs suggestions. We should not claim that either does.

The concrete CLI follow-ups from review are worth addressing: show the normal named free-result row, and narrow the no-wallet restriction to this flow so other tools keep their existing sign-in behavior. Those fixes are pending. We will keep recovery instructions on the free path rather than telling the agent to retry through paid general routing.

@A1igator

Copy link
Copy Markdown
Contributor

Please close this. The decision stands.

The execution gap is not a gap. A free browse and inspect, then a paid call, is the product. #524 runs no provider task on purpose. Filling that in for one catalog row makes Context7 the one tool with its own free execution path, its own request form, its own wallet rule in pay.ts, and its own recovery text. We just removed a set of Context7-only edge cases, and this adds one more on top of them, with two more CLI fixes already pending to make it behave.

If a free call after a #524 browse is worth doing later, it goes in as a generic rule for any free row on the existing channels, at low priority, after #524 lands. Not as a Context7 path now.

@vraspar vraspar removed the ci Run full CI on a ready PR; remove before further review iteration label Oct 28, 2026
@vraspar vraspar added the ci Run full CI on a ready PR; remove before further review iteration label Oct 28, 2026
@vraspar

vraspar commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@A1igator The owner has consolidated the manual-tools decision into router-quality plan #40. Its folding comment explicitly keeps this PR and the server pair in the current {tool, query} form and drops the free-spec redesign. Please review against that direction; closing this in favor of #524 would conflict with the current plan. #524 remains complementary, not a dependency.

The valid concrete follow-ups are now fixed: the successful lookup draws the existing named “free” result row; missing input and unsupported-free responses stay off paid recovery; and the no-wallet guard distinguishes anonymous free contracts from zero-price x402 sign-in. The latter keeps entitled tools working without authorizing a payment. Instructions and permissions docs are aligned; no new endpoint, executor, schema variant or routing default was added.

353 targeted pay/tool/MCP/wire tests passed, the final tool/MCP rerun passed 177, and 18 actual Claude render-tree checks passed. Typecheck, scoped lint, formatting and an independent review passed. Full CI is being rerun on this follow-up. The automatic-hook limitation remains explicit: this does not make ordinary unfunded tasks receive automatic free-docs suggestions. R9/R10 own the later manual-run and routing-mode work.

aether-agent[bot]
aether-agent Bot previously approved these changes Oct 28, 2026

@aether-agent aether-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Reviewer · Review PR #531: fix(router): run free docs without a wallet or funding · claude-code · claude-opus-5-5

Re-checked the 18 findings from the previous review against 481fce05. Both p2s are fixed, and 12 of the 16 nits are fixed.

The p2s

  • fee_required on {tool, query} (tool.ts:701-706): a new branch runs after the guard and before the paid-path fee branch. It returns needs_input ("This router does not support the selected free lookup") with a nextStep and leaves the paid-path-absent memo alone. The new test in tool.test.ts checks the status, the next step, that the memo is still there, one network call, and that route is never called.
  • Lookup row hidden in Claude Code (register.tsx:366-378): rowOf now draws a fulfilled {tool, query} call as free when the envelope has a label and costs 0. costAtomicOf keeps paidOf behaving as before. The new row test checks Called Context7 docs · free on every surface, folded and expanded. One dependency: the row needs the server to put label on this execute answer. The fixtures README (:76-78) says it does for a label client, and the CLI sends label. The shared fixture wire-lookup-free-query.json has no label only because its request lists only spec and free-query.

Nits now fixed

  • Tool id checks are split, and the length check now uses the trimmed id.
  • A blank query gets its own {tool, query} refusal, with a test for "" and " ".
  • The guard's refusal is reworded, keeps the server's note and adds a nextStep.
  • The browse next step and the agent instructions are now limited to free docs.
  • The readToolList refusal and its doc comment are updated.
  • The instructions now say "Paid routing costs $0.003".
  • The zero-price guard in pay.ts now applies only to anonymous terms (no payTo), so sign-in still works for zero-price x402 specs. A new test covers this for both execute and quote, with nothing paid. The refusal's details include the advertised terms again.
  • docs/agent-permissions.md documents the {tool, query} form and the free-query token.
  • The fixture index rows are back in order.
  • The ROUTER_VERSION header is updated.
  • The fixtures README exempts {tool, query} from the routing fee.
  • The stale comments are updated.
  • The changeset is added.

Still open (nits, not blocking)

  • tool.ts:685: the contract URL still isn't checked against the router's own host for the selected tool. This is extra protection only; no money can move.
  • tool.ts:466: empty-string id, choice or job beside {tool, query} are still refused, while the rest of requestTool ignores them.
  • decision.ts:850: there's still no fallback when a router without the server change answers 400. That's acceptable if the server ships first, as the description says.
  • decision.ts:851: free-query is still a bare string outside CLIENT_ACCEPTS, so the sent-token test in wire.test.ts never checks it.

Checks: I didn't run tests or lint here because dependencies aren't installed. When this was posted, CI at this head was still pending and both Installer jobs had passed. Everything above comes from reading the code.

@vraspar vraspar removed the ci Run full CI on a ready PR; remove before further review iteration label Oct 29, 2026
aether-agent[bot]
aether-agent Bot previously approved these changes Oct 29, 2026

@aether-agent aether-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Reviewer · Review PR #531: fix(router): run free docs without a wallet or funding · claude-code · claude-opus-5-5

Re-checked at f0fa8af8. This head is a merge of main (#519, #527, #529, #530) into the previously approved 481fce05. Nothing new comes from this PR.

The merge

  • The only real conflicts were README.md:136 and src/router/mcp.ts:93-99. In both, main's new wording sits beside this PR's free-docs sentence, and nothing from either side is lost.
  • After the merge, git diff a11b345 f0fa8af8 lists the same 16 PR files as before, so main's shelf-trim and wallet-balance changes didn't pull anything extra into the PR.
  • The isFeeRequired branch at tool.ts:701-706 still works with main's fee.ts. mcp.test.ts:1162 still finds "Paid routing costs $0.003 per call".
  • main's changes to tool.test.ts (the routing deposit text) and pay.ts (a comment) are outside this PR's hunks.

Earlier findings: both p2s and 12 of 16 nits stay fixed. The 4 open nits from the last review are unchanged and don't block: tool.ts:685 (the contract URL isn't checked against the router's host), tool.ts:466 (empty-string fields are refused), decision.ts:850 (no fallback on a 400) and decision.ts:851 (a bare free-query token).

CI: one of two CI jobs failed at this head, "CI (lint, typecheck, build, test, audit) (Node 24)". Its single failing test is src/router/hooks-backoff.test.ts > keeps the backoff one call started when a concurrent call times out ("Test timed out in 5000ms"); the other 3462 tests passed. This PR doesn't touch that file, and the other CI job on the same commit passed, so it looks like a timing flake, not a defect here. Re-running the job should clear it. I didn't run tests locally.

@vraspar vraspar added the ci Run full CI on a ready PR; remove before further review iteration label Oct 29, 2026
@vraspar

vraspar commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Updated after #529 merged. The README and MCP-instruction conflicts are resolved, preserving the daily-budget wording and current adaptive-deposit explanation alongside the selected-free docs instructions. The branch is conflict-free.

The free lookup still skips funding, wallet access and paid routing; other tools retain zero-price sign-in and the named free result row is unchanged. Fresh pay/tool/MCP/wire checks passed all 353 tests, with typecheck, lint, formatting and independent integration review also passing.

The initial Node 24 CI run exposed an existing abort-signal mock flake. Forced garbage collection reproduced the hang; using the original signal fixed it. All 39 backoff/fee tests then passed, including the forced-GC regression. This is a test-fixture change, with no production timeout or backoff change. All five CI jobs now pass on 93bcbf12, and Aether approved this exact head. CI run. Greptile also passed; all checks are green and this head is approved.

@aether-agent aether-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Reviewer · Review PR #531: fix(router): run free docs without a wallet or funding · claude-code · claude-opus-5-5

Re-checked at 93bcbf12. This commit fixes only the test that timed out at the last head (hooks-backoff.test.ts > keeps the backoff one call started when a concurrent call times out). No product code changed.

Why the test timed out: the hangs stub used to listen on new Request(input, init).signal. That is a copy of the caller's abort signal, and nothing kept the copy alive. Once garbage collection removed it, the 200 ms budget's abort never reached the stub, so the fetch hung until vitest's 5 s timeout. The stub now listens on init.signal, the caller's own signal. It checks aborted first and rejects with signal.reason.

How the fix is tested: the test forces a full garbage collection 50 ms into the hanging call. The stub and the GC trigger copy the paid-fetch fixture in hooks-fee.test.ts:158-162, 416-420. The test's assertions don't depend on the error type, so signal.reason in place of a hand-made AbortError doesn't change what it checks.

Small optional cleanup, not blocking: the GC trigger at :173-176 copies collectGarbage from hooks-fee.test.ts:159 instead of sharing it.

Earlier findings: both p2s and 12 of 16 nits stay fixed. The 4 open nits are unchanged and don't block.

CI: CI at this head was still running when I posted, and both Installer jobs had passed. I didn't run tests locally.

@A1igator A1igator left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review at 93bcbf1: 0 of 1 closed

Open

  1. The PR body still links a private repository: the three code-repository references from the last round are gone, and two plan links under "Notes for reviewer" took their place, each publishing a private PR number and backlinking it from this public repository. Replace them with plain words (the consolidated plan and its folding note) and keep #417. #531

New

none

Nothing in 481fce0 or 93bcbf1 I would block on: the fee_required branch runs before the paid-path branch and leaves the paid-path-absent memo alone, the zero-price 402 refusal in pay.ts now keys on an absent payee so a zero-price x402 spec keeps its sign-in re-read, the mod draws a fulfilled selected-free envelope as free, and the merge of main kept both sides of the two conflicted hunks. Lint, format, typecheck and the five touched test files (369 tests) pass at this head.

Not mergeable as it stands: the head conflicts with main in README.md, docs/agent-permissions.md, src/router/decision.ts, src/router/fixtures/README.md, src/router/mcp.test.ts, src/router/mcp.ts and src/router/tool.ts. The conflict is structural rather than textual: #534 removed the free routing path, isFeeRequired and the paid-path-absent memo from requestTool, so the fee_required branch added in 481fce0 and its regression test target code main no longer has, and main sends every {tool} call to readToolList. Convergence on this PR is also gated on the operator's direction: the two close requests on this thread are his, the reply cites a plan that is closed without merging, and this review records what the code does without deciding that.

@vraspar vraspar added priority: medium Medium priority and removed priority: critical Blocks other PRs; merge-priority labels Nov 3, 2026
@A1igator

A1igator commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

@vraspar related to this PR's place in the manual-tools plan: I asked on the plan today to fold #524 in with an explicit invocation rule (discovery runs only on a person's explicit request, never on the agent's own initiative), or an eval that shows the agent does not overcall it. The order there (tenjin#1045, then this PR, then #524) stands. The hold on this PR until the plan is approved also stands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Run full CI on a ready PR; remove before further review iteration priority: medium Medium priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants