@abide/abide 0.125.1 → 0.126.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.126.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5eb1a76: `claudeCode`'s `tools` asks for the visitor's own MCP tools as well as built-in ones — `mcp__calendar__meetings`, or `mcp__calendar` for all of a server's — granted the same way, through the command or the link (44.29). A `BridgeStatus` now always carries a command and a link, paired or not, and pairing with one replaces the bridge already paired, so a page can offer a reconnect (44.59). It also carries `missing`: the tools the pairing asked for that the visitor's bridge cannot give (44.60). `/__abide/bridge` names who a code was minted for, without spending it, for a bridge that always allows a site (44.63).
8
+
9
+ ### Patch Changes
10
+
11
+ - d61b26b: An abort (a `DOMException` named `AbortError`: a superseded call's, a stopped stream's) reaches no `onError`, the browser's or the server's — 37.28, as 37.25 keeps an aborted call from `onRefusal`. The server still writes it to its log, and under `abide start` an abort nothing handled no longer stops the app (37.10).
12
+
13
+ ## 0.125.2
14
+
15
+ ### Patch Changes
16
+
17
+ - d1029ab: A `Middleware` whose `Extra` asks for `args` fits a keyed handler again — 16.86. 0.125.1's plain rung array inferred the handler's `Args` off the rung's `args: () => unknown` as well as off the `Keyed`, and refused the handler.
18
+
3
19
  ## 0.125.1
4
20
 
5
21
  ### Patch Changes
package/docs/REGISTRY.md CHANGED
@@ -268,7 +268,7 @@ type Reached = { readonly [REACHED]: true }
268
268
  | Name | Signature | Meaning | Rules |
269
269
  | --- | --- | --- | --- |
270
270
  | `description` | `string` | The human description carried onto every generated surface. | 16.28 |
271
- | `middleware` | `{ [At in keyof Answers]: (next: (ctx?: { request: Request; args: () => Args }) => Promise<Reached>, ctx: { request: Request; args: () => Args }) => Answers[At] \| Promise<Answers[At]> } \| readonly ((next: (ctx?: { request: Request; args: () => Args }) => Promise<Reached>, ctx: { request: Request; args: () => Args }) => unknown)[]` | The rungs run per call. | 16.8, 16.11, 16.12, 16.16, 16.17, 16.18, 16.19, 16.20, 16.86 |
271
+ | `middleware` | `{ [At in keyof Answers]: (next: (ctx?: { request: Request; args: () => Args }) => Promise<Reached>, ctx: { request: Request; args: () => Args }) => Answers[At] \| Promise<Answers[At]> } \| readonly ((next: (ctx?: { request: Request; args: () => NoInfer<Args> }) => Promise<Reached>, ctx: { request: Request; args: () => NoInfer<Args> }) => unknown)[]` | The rungs run per call. | 16.8, 16.11, 16.12, 16.16, 16.17, 16.18, 16.19, 16.20, 16.86 |
272
272
  | `timeout` | `number` | How long the call may go without progress. | 16.22, 16.58, 16.59, 16.60, 16.61, 16.77 |
273
273
  | `clients` | `Clients` | Which surfaces carry this handler. | 16.37, 20.3 |
274
274
  | `crossOrigin` | `boolean \| string[]` | Whether other origins may call it, or follow it over the mux. | 16.24, 18.2, 21.19 |
@@ -382,11 +382,11 @@ type Reached = { readonly [REACHED]: true }
382
382
  | Name | Signature | Meaning | Rules |
383
383
  | --- | --- | --- | --- |
384
384
  | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | 44.5, 44.13, 44.17, 44.18, 44.19, 44.33 |
385
- | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | 44.16, 44.29 |
386
- | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; install: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given, and the command that installs the app the link opens. | 44.14, 44.32, 44.33, 44.40, 44.56 |
387
- | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22, 44.32, 44.33, 44.34, 44.40 |
388
- | `BridgeStatus` | `{ paired: boolean; command: string \| undefined; link: string \| undefined; install: string \| undefined }` | Whether a bridge is paired, and the command and the link that pair one where none is, with the command that installs the app the link opens. | 44.20, 44.40, 44.56 |
389
- | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | 36.7, 44.14, 44.15, 44.19, 44.23, 44.26, 16.57, 16.78 |
385
+ | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the tools of the visitor's machine it asks for. | 44.15, 44.16, 44.29, 44.58 |
386
+ | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; install: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the tools it is given, and the command that installs what opens the link. | 44.14, 44.32, 44.33, 44.40, 44.56 |
387
+ | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22, 44.32, 44.33, 44.34, 44.40, 44.59, 44.60 |
388
+ | `BridgeStatus` | `{ paired: boolean; missing: string[]; command: string; link: string; install: string }` | Whether a bridge is paired, the tools it cannot make available, and the command and the link that pair one, with the command that installs what opens the link. | 44.20, 44.40, 44.56, 44.59, 44.60 |
389
+ | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | 36.7, 44.14, 44.15, 44.19, 44.23, 44.26, 44.61, 44.63, 16.57, 16.78 |
390
390
 
391
391
  # Ambient values
392
392
 
@@ -753,7 +753,7 @@ type Reached = { readonly [REACHED]: true }
753
753
  | `Lane` | `'page' \| 'handler' \| 'file' \| 'openapi' \| 'mcp' \| 'bridge' \| 'socket' \| 'abide'` | What answers a request a `middleware` rung is handed. | 37.19 |
754
754
  | `onStart` | `(fn: (start: () => Promise<void>) => void \| Promise<void>) => () => void` | Wraps the boot. | 37.4, 37.6, 37.13 |
755
755
  | `onStop` | `(fn: (stop: () => Promise<void>) => void \| Promise<void>) => () => void` | Mirrors it for teardown. | 37.4, 37.6, 37.13 |
756
- | `onError` | `(fn: (error: unknown) => unknown) => () => void` | Runs on an unexpected error at the calling file's route, or in the browser below the calling component. | 37.5, 37.8, 37.13, 37.16, 37.17, 37.18, 37.21, 12.6 |
756
+ | `onError` | `(fn: (error: unknown) => unknown) => () => void` | Runs on an unexpected error at the calling file's route, or in the browser below the calling component. | 37.5, 37.8, 37.13, 37.16, 37.17, 37.18, 37.21, 37.28, 12.6 |
757
757
  | `onRefusal` | `(fn: (refusal: Failed<string, unknown>, call: { method: 'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'; url: string }) => unknown) => () => void` | What the browser hands each refusal a call over http is answered with, below the calling component. | 37.13, 37.22, 37.23, 37.24, 37.25, 37.26, 37.27 |
758
758
  | `onConfig` | `(fn: ConfigDefaults \| null, options?: ConfigOptions) => () => void` | The app's config defaults. | 27.6, 37.7, 37.13 |
759
759
  | `onHealth` | `(report: () => unknown \| Promise<unknown>) => () => void` | The app's health reporter. | 25.2, 37.7, 37.13 |
@@ -933,6 +933,7 @@ site's `claudeCode`.
933
933
 
934
934
  | Command | Meaning | Rules |
935
935
  | --- | --- | --- |
936
- | `abide-bridge <site> <code> [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | This machine's Claude Code, paired with the browser a site minted the code for. | 44.15, 44.16, 44.24, 44.25, 44.27, 44.30, 44.44, 44.45, 44.46, 44.47 |
937
- | `abide-bridge install [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | The app that opens a site's `abide-bridge` links on this machine, registered. | 44.41, 44.42, 44.43, 44.44 |
936
+ | `abide-bridge <site> <code> [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | This machine's Claude Code, paired with the browser a site minted the code for. | 44.15, 44.16, 44.24, 44.25, 44.27, 44.29, 44.30, 44.44, 44.45, 44.46, 44.47, 44.58, 44.61 |
937
+ | `abide-bridge install [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | What opens a site's `abide-bridge` links on this machine, registered. | 44.41, 44.42, 44.43, 44.44, 44.62, 44.64, 44.65 |
938
938
  | `abide-bridge uninstall` | `abide-bridge install`, undone. | 44.43 |
939
+ | `abide-bridge forget [<site>]` | The sites a link pairs with unasked, made to ask again. | 44.62, 44.66 |
package/docs/RULEBOOK.md CHANGED
@@ -2539,7 +2539,8 @@ reach `onError` as 37.5's error does.
2539
2539
  process.
2540
2540
 
2541
2541
  37.10 Under `abide start` and the executable's `start`, a rejection nothing handled and a throw
2542
- nothing caught MUST stop the app through `App.stop`, and the process MUST then exit with 1.
2542
+ nothing caught MUST stop the app through `App.stop`, and the process MUST then exit with 1 — save
2543
+ 37.28's abort, which MUST NOT stop it.
2543
2544
 
2544
2545
  37.11 Under `abide start` and the executable's `start`, `SIGTERM` and `SIGINT` MUST stop the app
2545
2546
  through `App.stop`, and the process MUST then exit with 0.
@@ -2597,6 +2598,10 @@ and once, handed the call's method and URL beside it.
2597
2598
  37.27 An `onRefusal` MUST NOT change the `Failed` the call refuses with, nor keep the call from
2598
2599
  refusing with it.
2599
2600
 
2601
+ 37.28 A `DOMException` named `AbortError` that 37.5, 37.8, 37.17 or 12.6 would hand `onError` MUST
2602
+ NOT reach it, on a server as in a browser: an abort is asked for, as 37.25's aborted call is, and is
2603
+ no failure.
2604
+
2600
2605
  # 38. Commands
2601
2606
 
2602
2607
  ## The commands
@@ -3494,9 +3499,9 @@ and MUST refuse 409 where none is paired.
3494
3499
  44.14 A code `claudeCode.bridgeCommand` mints MUST pair one bridge with the browser that asked for
3495
3500
  it, once, and MUST NOT pair once it has expired.
3496
3501
 
3497
- 44.15 A bridge MUST run Claude Code with no MCP server but the bridge's own, in an empty directory
3498
- removed when the run ends, and with no built-in tool and no setting source the visitor did not pass
3499
- it after `--`.
3502
+ 44.15 A bridge MUST run Claude Code with no MCP server but the bridge's own and the visitor's servers
3503
+ of the MCP tools 44.29 makes available to the turn, in an empty directory removed when the run ends,
3504
+ and with no built-in tool and no setting source the visitor did not pass it after `--`.
3500
3505
 
3501
3506
  44.16 A bridge MUST lay each option a site sets but `tools` (44.29) on as Claude Code's flag of that
3502
3507
  name, MUST fail a turn whose config sets any option `ClaudeCodeConfig` does not name, and MUST lay
@@ -3510,7 +3515,7 @@ was made in.
3510
3515
  44.19 `/__abide/bridge` MUST be closed until `claudeCode` has minted a pairing code in that process.
3511
3516
 
3512
3517
  44.20 `claudeCode.bridge` MUST yield a `BridgeStatus` whenever the asking browser's pairing changes,
3513
- and MUST yield a fresh command where the standing one's code expires unpaired.
3518
+ and MUST yield a fresh command where the standing one's code expires or pairs.
3514
3519
 
3515
3520
  44.21 A failure the Messages API reports to `claudeApi`, or a bridge reports to `claudeCode`, MUST
3516
3521
  refuse 502.
@@ -3536,9 +3541,11 @@ redial it as 44.25 says.
3536
3541
  44.28 A `Provider` MUST yield a tool call's `tool` event when the model starts writing the call,
3537
3542
  and its `input` event, with the same `id`, once the call is written.
3538
3543
 
3539
- 44.29 A built-in tool MUST be available to a turn only where the site's `tools` names it and the
3540
- visitor's `--tools` does, MUST be approved without asking unless the visitor passed `--allowedTools`,
3541
- and the bridge MUST say once each tool the site asked for that the visitor did not grant.
3544
+ 44.29 A tool of the visitor's machine — built-in by its name, or an MCP tool as `mcp__<server>__<tool>`
3545
+ — MUST be available to a turn only where the site's `tools` names it and the visitor's `--tools`
3546
+ names it or, for an MCP tool, `mcp__<server>`, and an MCP tool only where 44.58 finds its server; it
3547
+ MUST be approved without asking unless the visitor passed `--allowedTools`, and the bridge MUST say
3548
+ once each tool the site asked for that it cannot make available.
3542
3549
 
3543
3550
  44.30 A bridge stopped by `SIGTERM`, `SIGINT` or `SIGHUP` MUST end every Claude Code run it started
3544
3551
  before it exits.
@@ -3550,7 +3557,7 @@ signal 44.57 hands its provider aborted, else `failed` where its provider threw,
3550
3557
  line, else `stopped` where its caller stopped reading.
3551
3558
 
3552
3559
  44.32 A command `claudeCode.bridgeCommand` or `claudeCode.bridge` mints with `tools` MUST grant
3553
- those built-in tools after its `--` as `--tools`, and one minted without `tools` MUST carry no `--`.
3560
+ those tools after its `--` as `--tools`, and one minted without `tools` MUST carry no `--`.
3554
3561
 
3555
3562
  44.33 A pairing MUST be to the claims the principal was sealed with (26.11), or to none, where its
3556
3563
  code was minted, and `claudeCode` and `claudeCode.bridge` MUST read it as absent to a request from
@@ -3578,22 +3585,24 @@ its type, in its place in its turn.
3578
3585
  as one text piece.
3579
3586
 
3580
3587
  44.40 A pairing `claudeCode.bridgeCommand` or `claudeCode.bridge` mints MUST carry, beside its command,
3581
- a link in the `abide-bridge` scheme naming the same site, code, bridge release and built-in tools.
3588
+ a link in the `abide-bridge` scheme naming the same site, code, bridge release and tools.
3582
3589
 
3583
3590
  44.41 A bridge MUST open only a link of the `abide-bridge` scheme whose site is https or loopback and
3584
3591
  whose release is a version alone, and MUST run the release it names.
3585
3592
 
3586
- 44.42 A bridge opening an `abide-bridge` link MUST ask the visitor before it pairs, every time, naming
3587
- the site, the built-in tools and the bridge release the link asks for, and the options and the names
3588
- of the variables `abide-bridge install` kept (44.44).
3593
+ 44.42 A bridge opening an `abide-bridge` link MUST ask the visitor before it pairs, save where 44.62
3594
+ pairs it unasked, naming the site, the tools and the bridge release the link asks for, and the
3595
+ options and the names of the variables `abide-bridge install` kept (44.44), and MUST offer to always
3596
+ allow the site only where `/__abide/bridge` named a holder for the link's code (44.63).
3589
3597
 
3590
- 44.43 `abide-bridge install` MUST write the app that opens links under the visitor's own Application
3591
- Support and register it there, and `abide-bridge uninstall` MUST unregister it and remove all
3592
- install wrote.
3598
+ 44.43 `abide-bridge install` MUST write what opens links under the visitor's own home — Application
3599
+ Support on macOS, `XDG_DATA_HOME` on Linux, `LOCALAPPDATA` on Windows — and register it with that
3600
+ desktop for the visitor alone, and `abide-bridge uninstall` MUST unregister it and remove all install
3601
+ wrote.
3593
3602
 
3594
3603
  44.44 `abide-bridge install` MUST keep the Claude Code options after its `--` and each `--env
3595
3604
  NAME=VALUE` it is given, and a bridge opening an `abide-bridge` link MUST run Claude Code with those
3596
- options ahead of the link's built-in tools and with those variables set, and a link MUST NOT set either.
3605
+ options ahead of the link's tools and with those variables set, and a link MUST NOT set either.
3597
3606
 
3598
3607
  44.45 A bridge MUST say that `/__abide/bridge` did not take a report once, and not again until a
3599
3608
  report is taken, and MUST NOT say it while it redials per 44.25.
@@ -3634,6 +3643,35 @@ names, reading its `items` where its `files` hold none.
3634
3643
  aborts or once the `signal` its caller passed does, and once that signal aborts MUST end the
3635
3644
  conversation without waiting for the provider to, refused with `aborted` at status 499.
3636
3645
 
3646
+ 44.58 A bridge MUST find the visitor's MCP servers, at each turn, in each `--mcp-config` the visitor
3647
+ passed after `--`, or where they passed none, in the user scope of their Claude Code configuration,
3648
+ and MUST NOT find the bridge's own server's name there.
3649
+
3650
+ 44.59 A `BridgeStatus` MUST carry a command and a link whether or not a bridge is paired, and a bridge
3651
+ pairing by its code MUST take the place of the one paired.
3652
+
3653
+ 44.60 A `BridgeStatus` MUST carry as `missing` each tool its pairing's code asked for that the paired
3654
+ bridge last reported it cannot make available (44.29), and none while no bridge is paired.
3655
+
3656
+ 44.61 A bridge MUST report to `/__abide/bridge` the tools its pairing's code asked for that it cannot
3657
+ make available once it pairs, and again before a turn wherever they changed.
3658
+
3659
+ 44.62 A bridge opening an `abide-bridge` link MUST pair without asking only where the visitor chose to
3660
+ always allow its site for the holder `/__abide/bridge` names for its code (44.63), and the link asks
3661
+ for no tool beyond those allowed when they chose.
3662
+
3663
+ 44.63 `/__abide/bridge` MUST name, for a code minted and unspent, a holder without spending the code —
3664
+ the same for every code minted to one sign-in, or signed out to one browser, and different for any
3665
+ other — and MUST refuse 403 any other code.
3666
+
3667
+ 44.64 A bridge opening an `abide-bridge` link on a desktop it cannot ask on MUST NOT pair.
3668
+
3669
+ 44.65 A bridge opening an `abide-bridge` link MUST name, in the question 44.42 asks, each MCP tool the
3670
+ link asks for whose server 44.58 does not find.
3671
+
3672
+ 44.66 `abide-bridge forget` MUST stop always allowing the site it names, or every site where it names
3673
+ none.
3674
+
3637
3675
  # 45. Offline
3638
3676
 
3639
3677
  45.1 Where `ABIDE_OFFLINE` is on, a page `abide build` built MUST register the worker
package/docs/abide.md CHANGED
@@ -6493,12 +6493,15 @@ const answer = reactive(() => {
6493
6493
  <ul class="fields">
6494
6494
  <li><span>your claude code</span><strong>paired</strong></li>
6495
6495
  </ul>
6496
+ <div class="row">
6497
+ <a href={pairing.at(-1)?.link}>Reconnect</a>
6498
+ </div>
6496
6499
  {:else if pairing.at(-1)}
6497
6500
  <pre class="rounded-md border border-rule bg-raised p-snug
6498
6501
  text-chrome">{pairing.at(-1)?.command}</pre>
6499
6502
  <div class="row">
6500
6503
  <a href={pairing.at(-1)?.link}>Open in abide bridge</a>
6501
- <span class="status">on macOS, after
6504
+ <span class="status">after
6502
6505
  <code>bunx @abide/bridge install</code></span>
6503
6506
  </div>
6504
6507
  {:else}
@@ -6610,25 +6613,31 @@ test('unpaired: refused, with a command and a link', async () => {
6610
6613
 
6611
6614
  `claudeCode` sends the conversation to Claude Code on the asker's machine, on their own sign-in and plan. Its config, `ClaudeCodeConfig`, is Claude Code's own option names, each passed as its flag: `model`, `fallbackModel`, `systemPrompt`, `appendSystemPrompt`, `effort`, `maxBudgetUsd`, `betas` and `disallowedTools`. Permissions, directories and settings are not the site's to set.
6612
6615
 
6613
- `tools` asks for Claude Code's built-in tools, and the visitor decides. A tool runs only where they granted it after the command's `--`, and the bridge tells them about any it was asked for and not given. Anything else after `--` goes to Claude Code as written, and overrides what the site set.
6616
+ `tools` asks for tools of the visitor's machine, and the visitor decides. Name a built-in tool, such as `WebSearch`, or a tool of an MCP server the visitor configured, as Claude Code names it: `mcp__calendar__meetings`, or `mcp__calendar` for all of that server's. A tool runs only where they granted it after the command's `--`, and the bridge tells them about any it was asked for and not given. Anything else after `--` goes to Claude Code as written, and overrides what the site set.
6614
6617
 
6615
6618
  ```
6616
- bunx @abide/bridge <site> <code> -- --tools WebSearch,WebFetch
6619
+ bunx @abide/bridge <site> <code> -- --tools WebSearch,mcp__calendar__meetings
6617
6620
  ```
6618
6621
 
6622
+ The site names tools, never servers: a server is a command that runs on the visitor's machine. The bridge finds it in their own Claude Code configuration, the user scope `claude mcp add --scope user` writes, or in each `--mcp-config` they pass instead. It reads them again at every turn, so a server added mid-visit is used at the next question, and a turn gets only the servers of the tools it was given.
6623
+
6619
6624
  `bunx @abide/bridge` runs `abide-bridge <site> <code>`. `--claude <path>` names the Claude Code to run where `claude` is not on the `PATH`, and each `--env NAME=VALUE` sets a variable for it, such as a proxy or an API key of the visitor's own.
6620
6625
 
6621
6626
  Pass the same `tools` to `claudeCode.bridge` or `claudeCode.bridgeCommand` and the command it mints carries that grant, so a visitor who runs it as shown gives the tools. One minted without them grants none.
6622
6627
 
6623
- The visitor pairs once per visit by running a command, which dials out to the site. Asking with nothing paired is refused 409.
6628
+ The visitor pairs once per visit by running a command, which dials out to the site. Asking with nothing paired is refused 409. Every status carries a command, paired or not, and a bridge paired with it takes the place of the one paired, so a page can offer a reconnect without waiting for the old bridge to go.
6624
6629
 
6625
- On a Mac the visitor can pair with a click instead. Each status carries a `link` beside its command, the same pairing in the `abide-bridge` scheme, and a visitor who has run this once opens it from the page:
6630
+ Each status also carries `missing`: the tools the pairing asked for that the paired bridge reports it cannot give, because the visitor did not grant one or has no server for it. Show the visitor what to add from it.
6631
+
6632
+ On macOS, Linux and Windows the visitor can pair with a click instead. Each status carries a `link` beside its command, the same pairing in the `abide-bridge` scheme, and a visitor who has run this once opens it from the page:
6626
6633
 
6627
6634
  ```
6628
6635
  bunx @abide/bridge install
6629
6636
  ```
6630
6637
 
6631
- That builds a small app under their own Application Support folder, nowhere in Applications, and registers it for the scheme. `abide-bridge install` takes the same `--claude`, `--env` and options after `--` as the command, and keeps them: every link it opens runs Claude Code with them, and no link can set either. Opening a link asks them every time, naming the site, the tools and the bridge release it asks for, and the options and variable names install kept, and then runs that release, as the command would. A link only proves the site minted its code, never that this visitor asked for it, so no answer is kept for a site. `abide-bridge uninstall` takes away everything install wrote.
6638
+ That registers a handler for the scheme under the visitor's own home: a small app in Application Support on macOS, a desktop entry on Linux, the visitor's own registry classes on Windows. `abide-bridge install` takes the same `--claude`, `--env` and options after `--` as the command, and keeps them: every link it opens runs Claude Code with them, and no link can set either. Opening a link asks first, naming the site, the tools and the bridge release it asks for, any tool whose server they lack, and the options and variable names install kept, and then runs that release, as the command would. A Linux desktop with neither `zenity` nor `kdialog` cannot ask, so it pairs nothing from a link.
6639
+
6640
+ The visitor can answer Always Allow. A link only proves the site minted its code, never that this visitor asked for it, so the bridge keeps the answer for the site and for whom the code was minted: the signed-in member, or the browser when nobody is. A later link pairs unasked only where its code was minted for the same one and asks for no new tool. A code someone else minted and put in front of them is asked about. `abide-bridge forget` asks again for one site, or every one, and `abide-bridge uninstall` takes away everything install wrote.
6632
6641
 
6633
6642
  A bridge that loses the site says so once, and not again until the site takes a report. A turn whose report the site did not take is ended, and reported failed.
6634
6643
 
@@ -6636,8 +6645,8 @@ The tools the conversation calls run as the visitor, on the sign-in their ask ca
6636
6645
 
6637
6646
  | Name | What it does for the CRM |
6638
6647
  | --- | --- |
6639
- | `claudeCode.bridge` | Streams this browser's pairing: unpaired with the command to run and the link to open, then paired. |
6640
- | `BridgeStatus` | One status in that stream, `{ paired, command, link, install }`. `install` is the command that installs the app the link opens, at the release the command runs. |
6648
+ | `claudeCode.bridge` | Streams this browser's pairing: unpaired, then paired, each with the command to run and the link to open. |
6649
+ | `BridgeStatus` | One status in that stream, `{ paired, missing, command, link, install }`. `install` is the command that installs what opens the link, at the release the command runs. |
6641
6650
  | `claudeCode.bridgeCommand` | Mints the command and the link alone, for a page that wants them without the stream. Its code expires unused after ten minutes. |
6642
6651
  | `/__abide/bridge` | Where the visitor's bridge connects. It opens the first time a command is minted. |
6643
6652
 
@@ -8528,7 +8537,7 @@ Reactive primitives: what is written in place rather than imported.
8528
8537
  | Name | Signature | Meaning | Rules |
8529
8538
  | --- | --- | --- | --- |
8530
8539
  | `description` | `string` | The human description carried onto every generated surface. | [16.28](/rulebook#16.28) |
8531
- | `middleware` | `{ [At in keyof Answers]: (next: (ctx?: { request: Request; args: () => Args }) => Promise<Reached>, ctx: { request: Request; args: () => Args }) => Answers[At] \| Promise<Answers[At]> } \| readonly ((next: (ctx?: { request: Request; args: () => Args }) => Promise<Reached>, ctx: { request: Request; args: () => Args }) => unknown)[]` | The rungs run per call. | [16.8](/rulebook#16.8), [16.11](/rulebook#16.11), [16.12](/rulebook#16.12), [16.16](/rulebook#16.16), [16.17](/rulebook#16.17), [16.18](/rulebook#16.18), [16.19](/rulebook#16.19), [16.20](/rulebook#16.20), [16.86](/rulebook#16.86) |
8540
+ | `middleware` | `{ [At in keyof Answers]: (next: (ctx?: { request: Request; args: () => Args }) => Promise<Reached>, ctx: { request: Request; args: () => Args }) => Answers[At] \| Promise<Answers[At]> } \| readonly ((next: (ctx?: { request: Request; args: () => NoInfer<Args> }) => Promise<Reached>, ctx: { request: Request; args: () => NoInfer<Args> }) => unknown)[]` | The rungs run per call. | [16.8](/rulebook#16.8), [16.11](/rulebook#16.11), [16.12](/rulebook#16.12), [16.16](/rulebook#16.16), [16.17](/rulebook#16.17), [16.18](/rulebook#16.18), [16.19](/rulebook#16.19), [16.20](/rulebook#16.20), [16.86](/rulebook#16.86) |
8532
8541
  | `timeout` | `number` | How long the call may go without progress. | [16.22](/rulebook#16.22), [16.58](/rulebook#16.58), [16.59](/rulebook#16.59), [16.60](/rulebook#16.60), [16.61](/rulebook#16.61), [16.77](/rulebook#16.77) |
8533
8542
  | `clients` | `Clients` | Which surfaces carry this handler. | [16.37](/rulebook#16.37), [20.3](/rulebook#20.3) |
8534
8543
  | `crossOrigin` | `boolean \| string[]` | Whether other origins may call it, or follow it over the mux. | [16.24](/rulebook#16.24), [18.2](/rulebook#18.2), [21.19](/rulebook#21.19) |
@@ -9168,9 +9177,9 @@ Generated surfaces: what is written in place rather than imported.
9168
9177
  | Name | Signature | Meaning | Rules |
9169
9178
  | --- | --- | --- | --- |
9170
9179
  | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.33](/rulebook#44.33) |
9171
- | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; install: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given, and the command that installs the app the link opens. | [44.14](/rulebook#44.14), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.40](/rulebook#44.40), [44.56](/rulebook#44.56) |
9172
- | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.34](/rulebook#44.34), [44.40](/rulebook#44.40) |
9173
- | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | [36.7](/rulebook#36.7), [44.14](/rulebook#44.14), [44.15](/rulebook#44.15), [44.19](/rulebook#44.19), [44.23](/rulebook#44.23), [44.26](/rulebook#44.26), [16.57](/rulebook#16.57), [16.78](/rulebook#16.78) |
9180
+ | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; install: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the tools it is given, and the command that installs what opens the link. | [44.14](/rulebook#44.14), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.40](/rulebook#44.40), [44.56](/rulebook#44.56) |
9181
+ | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.34](/rulebook#44.34), [44.40](/rulebook#44.40), [44.59](/rulebook#44.59), [44.60](/rulebook#44.60) |
9182
+ | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | [36.7](/rulebook#36.7), [44.14](/rulebook#44.14), [44.15](/rulebook#44.15), [44.19](/rulebook#44.19), [44.23](/rulebook#44.23), [44.26](/rulebook#44.26), [44.61](/rulebook#44.61), [44.63](/rulebook#44.63), [16.57](/rulebook#16.57), [16.78](/rulebook#16.78) |
9174
9183
 
9175
9184
  ## Taught in
9176
9185
 
@@ -9188,7 +9197,7 @@ Generated surfaces: what is written in place rather than imported.
9188
9197
 
9189
9198
  | Name | Signature | Meaning | Rules |
9190
9199
  | --- | --- | --- | --- |
9191
- | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | [44.16](/rulebook#44.16), [44.29](/rulebook#44.29) |
9200
+ | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the tools of the visitor's machine it asks for. | [44.15](/rulebook#44.15), [44.16](/rulebook#44.16), [44.29](/rulebook#44.29), [44.58](/rulebook#44.58) |
9192
9201
 
9193
9202
  ## Taught in
9194
9203
 
@@ -9206,7 +9215,7 @@ Generated surfaces: what is written in place rather than imported.
9206
9215
 
9207
9216
  | Name | Signature | Meaning | Rules |
9208
9217
  | --- | --- | --- | --- |
9209
- | `BridgeStatus` | `{ paired: boolean; command: string \| undefined; link: string \| undefined; install: string \| undefined }` | Whether a bridge is paired, and the command and the link that pair one where none is, with the command that installs the app the link opens. | [44.20](/rulebook#44.20), [44.40](/rulebook#44.40), [44.56](/rulebook#44.56) |
9218
+ | `BridgeStatus` | `{ paired: boolean; missing: string[]; command: string; link: string; install: string }` | Whether a bridge is paired, the tools it cannot make available, and the command and the link that pair one, with the command that installs what opens the link. | [44.20](/rulebook#44.20), [44.40](/rulebook#44.40), [44.56](/rulebook#44.56), [44.59](/rulebook#44.59), [44.60](/rulebook#44.60) |
9210
9219
 
9211
9220
  ## Taught in
9212
9221
 
@@ -10405,7 +10414,7 @@ Configuration: what is written in place rather than imported.
10405
10414
 
10406
10415
  | Name | Signature | Meaning | Rules |
10407
10416
  | --- | --- | --- | --- |
10408
- | `onError` | `(fn: (error: unknown) => unknown) => () => void` | Runs on an unexpected error at the calling file's route, or in the browser below the calling component. | [37.5](/rulebook#37.5), [37.8](/rulebook#37.8), [37.13](/rulebook#37.13), [37.16](/rulebook#37.16), [37.17](/rulebook#37.17), [37.18](/rulebook#37.18), [37.21](/rulebook#37.21), [12.6](/rulebook#12.6) |
10417
+ | `onError` | `(fn: (error: unknown) => unknown) => () => void` | Runs on an unexpected error at the calling file's route, or in the browser below the calling component. | [37.5](/rulebook#37.5), [37.8](/rulebook#37.8), [37.13](/rulebook#37.13), [37.16](/rulebook#37.16), [37.17](/rulebook#37.17), [37.18](/rulebook#37.18), [37.21](/rulebook#37.21), [37.28](/rulebook#37.28), [12.6](/rulebook#12.6) |
10409
10418
 
10410
10419
  ## Taught in
10411
10420
 
@@ -11617,9 +11626,10 @@ CLI: what is written in place rather than imported.
11617
11626
 
11618
11627
  | Command | Meaning | Rules |
11619
11628
  | --- | --- | --- |
11620
- | `abide-bridge <site> <code> [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | This machine's Claude Code, paired with the browser a site minted the code for. | [44.15](/rulebook#44.15), [44.16](/rulebook#44.16), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.27](/rulebook#44.27), [44.30](/rulebook#44.30), [44.44](/rulebook#44.44), [44.45](/rulebook#44.45), [44.46](/rulebook#44.46), [44.47](/rulebook#44.47) |
11621
- | `abide-bridge install [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | The app that opens a site's `abide-bridge` links on this machine, registered. | [44.41](/rulebook#44.41), [44.42](/rulebook#44.42), [44.43](/rulebook#44.43), [44.44](/rulebook#44.44) |
11629
+ | `abide-bridge <site> <code> [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | This machine's Claude Code, paired with the browser a site minted the code for. | [44.15](/rulebook#44.15), [44.16](/rulebook#44.16), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.27](/rulebook#44.27), [44.29](/rulebook#44.29), [44.30](/rulebook#44.30), [44.44](/rulebook#44.44), [44.45](/rulebook#44.45), [44.46](/rulebook#44.46), [44.47](/rulebook#44.47), [44.58](/rulebook#44.58), [44.61](/rulebook#44.61) |
11630
+ | `abide-bridge install [--claude <path>] [--env NAME=VALUE]… [-- <options>]` | What opens a site's `abide-bridge` links on this machine, registered. | [44.41](/rulebook#44.41), [44.42](/rulebook#44.42), [44.43](/rulebook#44.43), [44.44](/rulebook#44.44), [44.62](/rulebook#44.62), [44.64](/rulebook#44.64), [44.65](/rulebook#44.65) |
11622
11631
  | `abide-bridge uninstall` | `abide-bridge install`, undone. | [44.43](/rulebook#44.43) |
11632
+ | `abide-bridge forget [<site>]` | The sites a link pairs with unasked, made to ask again. | [44.62](/rulebook#44.62), [44.66](/rulebook#44.66) |
11623
11633
 
11624
11634
  ## Taught in
11625
11635
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.125.1",
3
+ "version": "0.126.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -1,3 +1,3 @@
1
1
  // The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
2
2
  // server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
3
- export const BRIDGE_VERSION = '0.5.2'
3
+ export const BRIDGE_VERSION = '0.6.0'
@@ -4,9 +4,12 @@
4
4
  //
5
5
  // GET /__abide/bridge?code=… the pairing, answered by a server-sent stream of what the bridge is
6
6
  // to do: the token it posts with, each turn, each tool reply, a cancel.
7
+ // GET /__abide/bridge?holder=… who a code was minted for, as an opaque string, without spending it
8
+ // (44.63): what a bridge always allowing a site checks a link against.
7
9
  // GET /__abide/bridge the same stream, resumed by the token after it dropped (44.26).
8
10
  // POST /__abide/bridge what the bridge reports, one frame per line: a turn's events, the
9
- // MCP requests its Claude Code made, the turn's end or failure.
11
+ // MCP requests its Claude Code made, the turn's end or failure, and
12
+ // the tools the pairing asked for that it cannot give (44.61).
10
13
  //
11
14
  // A PAIRING IS TO A BROWSER — the `abide-caller` its page carries (26.5) — and to whoever was signed
12
15
  // in there when its code was minted (44.33), for as long as the bridge's stream is open, or
@@ -24,6 +27,7 @@ import { bodyWithin } from './answerRpc.ts'
24
27
  import { framed } from './answerValue.ts'
25
28
  import type { ProviderEvent } from './assistant.ts'
26
29
  import { Inbox } from './Inbox.ts'
30
+ import { holderOf } from './principal.ts'
27
31
  import { randomToken } from './randomToken.ts'
28
32
  import { RESUME_MILLISECONDS } from './RESUME_MILLISECONDS.ts'
29
33
  import { onSignInChanged } from './signInChanged.ts'
@@ -53,14 +57,24 @@ type Bridge = {
53
57
  idle: ReturnType<typeof setTimeout> | undefined
54
58
  // Set while the stream is dropped and the pairing waits to be resumed (44.26).
55
59
  held: ReturnType<typeof setTimeout> | undefined
60
+ // What its code asked for of the visitor's machine, and of that what it reported it cannot give.
61
+ asked: string[]
62
+ missing: string[]
56
63
  }
57
64
 
65
+ /** A pairing as a status stream says it: whether a bridge is paired, the code that pairs one, and what it cannot give. */
66
+ export type PairingState = { paired: boolean; code: string; missing: string[] }
67
+
58
68
  /** How long a pairing code stands unused. */
59
69
  const CODE_LIFETIME = 10 * 60_000
60
70
  // How long a bridge outlives the last page watching it, so a reload does not end it.
61
71
  const IDLE_GRACE = 30_000
62
72
 
63
- const CODES = new Map<string, { key: string; expires: number }>()
73
+ // Each code minted and unspent: the pairing it is for, who it was minted for, and what it asks.
74
+ const CODES = new Map<
75
+ string,
76
+ { key: string; expires: number; holder: string; tools: string[] }
77
+ >()
64
78
  const BY_KEY = new Map<string, Bridge>()
65
79
  const BY_TOKEN = new Map<string, Bridge>()
66
80
  const WATCHERS = new Map<string, Set<() => void>>()
@@ -89,17 +103,26 @@ function pairingKey(caller: string, signedIn: string): string {
89
103
  // 44.19 — the relay answers once the app has handed out a pairing code, and never before.
90
104
  let minted = false
91
105
 
92
- /** A single-use pairing code for `caller` signed in as `signedIn`, and when it stops standing. 44.14. */
106
+ /**
107
+ * A single-use pairing code for `caller` signed in as `signedIn`, asking for `tools` of the visitor's
108
+ * machine, and when it stops standing. 44.14.
109
+ */
93
110
  export function mintCode(
94
111
  caller: string,
95
112
  signedIn: string,
113
+ tools: readonly string[] = [],
96
114
  ): { code: string; expires: number } {
97
115
  const now = Date.now()
98
116
  for (const [code, pairing] of CODES)
99
117
  if (pairing.expires <= now) CODES.delete(code)
100
118
  const code = randomToken(12, 'base64url')
101
119
  const expires = now + CODE_LIFETIME
102
- CODES.set(code, { key: pairingKey(caller, signedIn), expires })
120
+ CODES.set(code, {
121
+ key: pairingKey(caller, signedIn),
122
+ expires,
123
+ holder: holderOf(caller, signedIn),
124
+ tools: [...tools],
125
+ })
103
126
  minted = true
104
127
  return { code, expires }
105
128
  }
@@ -167,9 +190,10 @@ export async function* runTurn({
167
190
  }
168
191
 
169
192
  /**
170
- * The pairing of `caller` signed in as `signedIn` as it changes, until `signal` aborts: the code that
171
- * pairs it while unpaired, then `undefined` the moment a bridge connects, and a code again if it goes. A
172
- * code that expires while nothing is paired is replaced with a fresh one. 44.20.
193
+ * The pairing of `caller` signed in as `signedIn` as it changes, until `signal` aborts: whether a bridge
194
+ * is paired, what it reported it cannot give of `tools`, and always a code asking for `tools` that pairs
195
+ * one — in place of the one paired, where one is (44.59). A code that expires or is spent is replaced
196
+ * with a fresh one. 44.20.
173
197
  *
174
198
  * 44.34 — IT FOLLOWS WHOEVER SIGNS IN AT THE BROWSER: a sign-in or a sign-out made since is the
175
199
  * pairing it watches from then on, and the code it offers is that sign-in's.
@@ -177,8 +201,9 @@ export async function* runTurn({
177
201
  export async function* pairingStatus(
178
202
  caller: string,
179
203
  signedIn: string,
204
+ tools: readonly string[],
180
205
  signal: AbortSignal,
181
- ): AsyncGenerator<string | undefined> {
206
+ ): AsyncGenerator<PairingState> {
182
207
  // A change landing while the loop was not waiting — paused at its `yield` — stays queued, so it
183
208
  // looks again; a look that finds nothing new yields nothing.
184
209
  const changes = new Inbox<undefined>()
@@ -193,8 +218,8 @@ export async function* pairingStatus(
193
218
  let unwatch = (): void => {}
194
219
  let unfollow = (): void => {}
195
220
  let pairing: { code: string; expires: number } | undefined
196
- // `null` until the first is said; `undefined` says paired.
197
- let said: string | undefined | null = null
221
+ // `undefined` until the first is said.
222
+ let said: string | undefined
198
223
  signal.addEventListener('abort', rouse)
199
224
  try {
200
225
  while (!signal.aborted) {
@@ -207,20 +232,28 @@ export async function* pairingStatus(
207
232
  watchingKey = key
208
233
  pairing = undefined
209
234
  }
210
- const paired = BY_KEY.has(key)
235
+ const bridge = BY_KEY.get(key)
211
236
  if (
212
- !paired &&
213
- (pairing === undefined || pairing.expires <= Date.now())
237
+ pairing === undefined ||
238
+ pairing.expires <= Date.now() ||
239
+ !CODES.has(pairing.code)
214
240
  )
215
- pairing = mintCode(caller, signedIn)
216
- const code = paired ? undefined : pairing?.code
217
- if (code !== said) {
218
- said = code
219
- yield code
241
+ pairing = mintCode(caller, signedIn, tools)
242
+ const state: PairingState = {
243
+ paired: bridge !== undefined,
244
+ code: pairing.code,
245
+ // 44.60 — of what this stream's code asks for: two on one page may ask for different tools.
246
+ missing:
247
+ bridge === undefined
248
+ ? []
249
+ : missingOf(tools, bridge.missing),
220
250
  }
221
- const renew = paired
222
- ? undefined
223
- : setTimeout(rouse, (pairing?.expires ?? 0) - Date.now())
251
+ const saying = `${state.paired} ${state.code} ${state.missing.join(',')}`
252
+ if (saying !== said) {
253
+ said = saying
254
+ yield state
255
+ }
256
+ const renew = setTimeout(rouse, pairing.expires - Date.now())
224
257
  await waiting.next()
225
258
  clearTimeout(renew)
226
259
  }
@@ -338,18 +371,15 @@ export async function answerBridge(request: Request): Promise<unknown> {
338
371
  }
339
372
 
340
373
  function pair(request: Request): Response {
341
- const code = new URL(request.url).searchParams.get('code')
374
+ const query = new URL(request.url).searchParams
375
+ const holder = query.get('holder')
376
+ if (holder !== null) return holderAnswer(holder)
377
+ const code = query.get('code')
342
378
  if (code === null) return resume(request)
343
- const pairing = CODES.get(code)
379
+ const pairing = standingCode(code)
344
380
  // 44.14 — a code pairs once.
345
381
  CODES.delete(code)
346
- if (pairing === undefined || pairing.expires <= Date.now())
347
- return answered(
348
- refuse(
349
- 403,
350
- 'This pairing code was not issued here, or it has been used, or it has expired.',
351
- ),
352
- )
382
+ if (pairing === undefined) return unknownCode()
353
383
  const standing = BY_KEY.get(pairing.key)
354
384
  if (standing !== undefined) {
355
385
  standing.frames.push({ type: 'bye' })
@@ -363,14 +393,45 @@ function pair(request: Request): Response {
363
393
  watched: WATCHERS.has(pairing.key),
364
394
  idle: undefined,
365
395
  held: undefined,
396
+ asked: pairing.tools,
397
+ missing: [],
366
398
  }
367
399
  BY_KEY.set(bridge.key, bridge)
368
400
  BY_TOKEN.set(bridge.token, bridge)
369
- bridge.frames.push({ type: 'paired', token: bridge.token })
401
+ bridge.frames.push({
402
+ type: 'paired',
403
+ token: bridge.token,
404
+ tools: pairing.tools,
405
+ })
370
406
  wakeWatchers(bridge.key)
371
407
  return attach(bridge, request)
372
408
  }
373
409
 
410
+ // 44.63 — WHO A CODE WAS MINTED FOR, read without spending it: a code minted for someone else names
411
+ // someone else, which is what a bridge always allowing a site refuses to pair unasked.
412
+ function holderAnswer(code: string): Response {
413
+ const pairing = standingCode(code)
414
+ if (pairing === undefined) return unknownCode()
415
+ return Response.json({ holder: pairing.holder })
416
+ }
417
+
418
+ /** What `code` was minted with, while it is unspent and unexpired. */
419
+ function standingCode(code: string) {
420
+ const pairing = CODES.get(code)
421
+ return pairing === undefined || pairing.expires <= Date.now()
422
+ ? undefined
423
+ : pairing
424
+ }
425
+
426
+ function unknownCode(): Response {
427
+ return answered(
428
+ refuse(
429
+ 403,
430
+ 'This pairing code was not issued here, or it has been used, or it has expired.',
431
+ ),
432
+ )
433
+ }
434
+
374
435
  /** The bridge the request's bearer token was handed when it paired. */
375
436
  function pairedBy(request: Request): Bridge | undefined {
376
437
  return BY_TOKEN.get(
@@ -404,8 +465,9 @@ function attach(bridge: Bridge, request: Request): Response {
404
465
  }
405
466
 
406
467
  type BridgeReport = {
407
- type: 'event' | 'mcp' | 'end' | 'fail'
468
+ type: 'event' | 'mcp' | 'end' | 'fail' | 'missing'
408
469
  turn: string
470
+ tools?: string[]
409
471
  event?: unknown
410
472
  id?: number
411
473
  method?: string
@@ -413,6 +475,15 @@ type BridgeReport = {
413
475
  message?: string
414
476
  }
415
477
 
478
+ /** The tools of `asked` that `reported` names: a bridge says what it cannot give of no other. */
479
+ function missingOf(asked: readonly string[], reported: unknown): string[] {
480
+ const missing: string[] = []
481
+ if (Array.isArray(reported))
482
+ for (const tool of asked)
483
+ if (reported.includes(tool)) missing.push(tool)
484
+ return missing
485
+ }
486
+
416
487
  async function relay(request: Request): Promise<Response> {
417
488
  const bridge = pairedBy(request)
418
489
  if (bridge === undefined)
@@ -432,9 +503,15 @@ async function relay(request: Request): Promise<Response> {
432
503
  }
433
504
  }
434
505
  for (const report of reports) {
506
+ // 44.60 — what the pairing asked for and the bridge cannot give, as the page's status says it.
507
+ if (report.type === 'missing') {
508
+ bridge.missing = missingOf(bridge.asked, report.tools)
509
+ wakeWatchers(bridge.key)
510
+ continue
511
+ }
435
512
  const turn = bridge.turns.get(report.turn)
436
513
  if (turn === undefined) continue
437
- turn.push(item(report.type, report))
514
+ turn.push(item(report.type as TurnItem['kind'], report))
438
515
  }
439
516
  return new Response(null, { status: 204 })
440
517
  }
@@ -5,8 +5,9 @@
5
5
  //
6
6
  // CONFIGURED BY CLAUDE CODE'S OWN OPTION NAMES, which the bridge lays on as their flags, and only the ones
7
7
  // that shape the answer. What would give a site power over the visitor's machine — permissions,
8
- // directory, settings — is not an option here at all, and its built-in `tools` are a request the visitor
9
- // grants or does not: the bridge decides (44.15, 44.16, 44.29).
8
+ // directory, settings, an MCP server — is not an option here at all, and its `tools`, built-in or of
9
+ // the visitor's own MCP servers, are a request the visitor grants or does not: the bridge decides
10
+ // (44.15, 44.16, 44.29).
10
11
 
11
12
  import type {
12
13
  AssistantContent,
@@ -25,7 +26,10 @@ import { mcpFailure } from './generatedSurfaces.ts'
25
26
  import { signedInAs } from './principal.ts'
26
27
  import { request } from './requestScope.ts'
27
28
 
28
- /** The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. */
29
+ /**
30
+ * The Claude Code options a site may set for a turn, and the tools of the visitor's machine it asks
31
+ * for: built-in by name, or of the visitor's own MCP servers as `mcp__<server>__<tool>`.
32
+ */
29
33
  export type ClaudeCodeConfig = {
30
34
  model?: string
31
35
  fallbackModel?: string
@@ -38,18 +42,20 @@ export type ClaudeCodeConfig = {
38
42
  tools?: string[]
39
43
  }
40
44
 
41
- /** The built-in tools a pairing command grants, as the visitor would after its `--`. 44.32. */
45
+ /** The tools a pairing command grants, as the visitor would after its `--`. 44.32. */
42
46
  type PairingOptions = Pick<ClaudeCodeConfig, 'tools'>
43
47
 
44
48
  /**
45
- * Whether this browser's bridge is connected, and the command and the link that pair one where it is
46
- * not, with the command that installs the app the link opens.
49
+ * Whether this browser's bridge is connected, the tools it asked for that the bridge cannot give, and
50
+ * the command and the link that pair one — in place of the one connected, where one is — with the
51
+ * command that installs what opens the link.
47
52
  */
48
53
  export type BridgeStatus = {
49
54
  paired: boolean
50
- command: string | undefined
51
- link: string | undefined
52
- install: string | undefined
55
+ missing: string[]
56
+ command: string
57
+ link: string
58
+ install: string
53
59
  }
54
60
 
55
61
  /** The visitor's own Claude Code, reached through the bridge paired to their browser. */
@@ -125,7 +131,11 @@ function bridgeCommand(options: PairingOptions = {}): {
125
131
  install: string
126
132
  expires: number
127
133
  } {
128
- const { code, expires } = mintCode(askingCaller(), signedInAs())
134
+ const { code, expires } = mintCode(
135
+ askingCaller(),
136
+ signedInAs(),
137
+ options.tools,
138
+ )
129
139
  return { ...commandFor(code, options), expires }
130
140
  }
131
141
 
@@ -150,26 +160,20 @@ function commandFor(
150
160
  }
151
161
 
152
162
  /**
153
- * This browser's pairing as it changes: unpaired with the command to run and the link to open, then
154
- * paired the moment a bridge connects, and unpaired again if it goes — following whoever signs in at
155
- * the browser since. 44.20, 44.34.
163
+ * This browser's pairing as it changes: unpaired, then paired the moment a bridge connects, and
164
+ * unpaired again if it goes — following whoever signs in at the browser since — each status with the
165
+ * command to run and the link to open. 44.20, 44.34, 44.59, 44.60.
156
166
  */
157
167
  async function* bridge(
158
168
  options: PairingOptions = {},
159
169
  ): AsyncGenerator<BridgeStatus> {
160
- for await (const code of pairingStatus(
170
+ for await (const { paired, code, missing } of pairingStatus(
161
171
  askingCaller(),
162
172
  signedInAs(),
173
+ options.tools ?? [],
163
174
  request().signal,
164
175
  ))
165
- yield code === undefined
166
- ? {
167
- paired: true,
168
- command: undefined,
169
- link: undefined,
170
- install: undefined,
171
- }
172
- : { paired: false, ...commandFor(code, options) }
176
+ yield { paired, missing, ...commandFor(code, options) }
173
177
  }
174
178
 
175
179
  /** The visitor's own Claude Code as a provider, with the pairing that reaches it. */
@@ -2,6 +2,8 @@
2
2
  // per command that serves. `createApp` installs neither, so a test or a script keeps bun's default of
3
3
  // failing loudly.
4
4
 
5
+ import { isAbort } from '#shared/isAbort.ts'
6
+
5
7
  import type { App } from './createApp.ts'
6
8
  import { reportUnexpected } from './hooks.ts'
7
9
 
@@ -27,7 +29,8 @@ export function keepServing(): void {
27
29
  *
28
30
  * A FAULT NOBODY AWAITED stops it too, after `onError` has it: the app's state is no longer one
29
31
  * anything vouched for, and a supervisor restarting a clean process is the recovery. The exit code
30
- * is what tells the supervisor which of the two it was.
32
+ * is what tells the supervisor which of the two it was. An abort was asked for and vouches for the
33
+ * state as it found it, so it is written and the app serves on (37.28).
31
34
  *
32
35
  * 37.12 — A SECOND SIGNAL WHILE STOPPING ENDS THE PROCESS AT ONCE, so a drain that never finishes
33
36
  * is still one keystroke from over.
@@ -52,7 +55,7 @@ export function stopOnSignalOrFault(app: App): void {
52
55
  })
53
56
  const fault = (error: unknown): void => {
54
57
  reportUnexpected(error)
55
- if (!stopping) stopThenExit(1)
58
+ if (!stopping && !isAbort(error)) stopThenExit(1)
56
59
  }
57
60
  process.on('unhandledRejection', fault)
58
61
  process.on('uncaughtException', fault)
@@ -12,6 +12,7 @@
12
12
  // `App` in it boots under the same hooks (42.12's sharing, from the other side).
13
13
 
14
14
  import { type Failed, isFailed } from '#shared/Failed.ts'
15
+ import { isAbort } from '#shared/isAbort.ts'
15
16
  import { namedLogger } from '#shared/log.ts'
16
17
  import type { Rung } from '#shared/Middleware.ts'
17
18
  import { aborted, refuse } from '#shared/refuse.ts'
@@ -313,9 +314,11 @@ export function reportUnexpected(error: unknown): void {
313
314
  /**
314
315
  * Hands an error to every `onError` hook, writing nothing of its own.
315
316
  *
316
- * 12.6 — the half of 37.5 a failure already warned on its own namespace is owed.
317
+ * 12.6 — the half of 37.5 a failure already warned on its own namespace is owed. An abort is
318
+ * written and handed to none (37.28).
317
319
  */
318
320
  export function handOnError(error: unknown): void {
321
+ if (isAbort(error)) return
319
322
  // 37.5 — NEAREST ROUTE FIRST, over the route the request matched; outside every request, or
320
323
  // before one matched, the root's alone.
321
324
  const route = currentRequestScope()?.route
@@ -241,6 +241,18 @@ export function signedInAs(): string {
241
241
  return identityOf(openedSeal(within('claudeCode')))
242
242
  }
243
243
 
244
+ /**
245
+ * Who a pairing code minted for `caller` signed in as `signedIn` is minted for, as a bridge is told it
246
+ * (44.63): one string per sign-in — signed by the principal secret, so no claim is read off it — or,
247
+ * signed out, one per browser, and never the same for two.
248
+ */
249
+ export function holderOf(caller: string, signedIn: string): string {
250
+ const [secret] = secrets()
251
+ return signedIn === 'null' || secret === undefined
252
+ ? `browser.${new Bun.CryptoHasher('sha256').update(caller).digest('base64url')}`
253
+ : `account.${signature(secret, signedIn)}`
254
+ }
255
+
244
256
  /**
245
257
  * Signs this caller in with `claims`, sealing them into the cookie.
246
258
  *
@@ -0,0 +1,4 @@
1
+ /** Whether `error` is an abort, which was asked for — a superseded call's, a stopped stream's. 37.28. */
2
+ export function isAbort(error: unknown): boolean {
3
+ return error instanceof DOMException && error.name === 'AbortError'
4
+ }
@@ -7,9 +7,10 @@
7
7
  // WHAT NOTHING CAUGHT IS THE BROWSER'S OWN ACCOUNT: the window's `error` event and its
8
8
  // `unhandledrejection`, which a call's refusal no handler awaited or caught arrives as. 12.6's
9
9
  // throws are handed through `UNEXPECTED` as they are on a server, and the one of them that then
10
- // surfaces as uncaught (12.7) is not handed a second time.
10
+ // surfaces as uncaught (12.7) is not handed a second time. An abort is handed to none (37.28).
11
11
 
12
12
  import { BrowserHooks } from './BrowserHooks.ts'
13
+ import { isAbort } from './isAbort.ts'
13
14
  import { UNEXPECTED } from './UNEXPECTED.ts'
14
15
 
15
16
  const HOOKS = new BrowserHooks<[error: unknown]>(
@@ -38,7 +39,7 @@ function listen(): void {
38
39
  function handedUnexpected(error: unknown): void {
39
40
  if (SURFACING.length === 0) setTimeout(() => (SURFACING.length = 0))
40
41
  SURFACING.push(error)
41
- HOOKS.hand(error)
42
+ hand(error)
42
43
  }
43
44
 
44
45
  function surfaced(error: unknown): void {
@@ -47,5 +48,10 @@ function surfaced(error: unknown): void {
47
48
  SURFACING.splice(at, 1)
48
49
  return
49
50
  }
50
- HOOKS.hand(error)
51
+ hand(error)
52
+ }
53
+
54
+ // 37.28 — every browser route to the hooks passes here, so an abort reaches none of them.
55
+ function hand(error: unknown): void {
56
+ if (!isAbort(error)) HOOKS.hand(error)
51
57
  }
package/src/shared/rpc.ts CHANGED
@@ -223,11 +223,13 @@ export type RpcOptions<
223
223
  *
224
224
  * The plain array arm is the context a list built by a GENERIC helper is typed in: the mapped
225
225
  * arm is over `Answers`, still being inferred, so the helper's own `Args` read nothing there and
226
- * are `unknown`. It adds no refusal of its own — `Answers` is still read off the list.
226
+ * are `unknown`. It adds no refusal of its own — `Answers` is still read off the list — and
227
+ * no `Args` either: a `Middleware` asking `args: () => unknown` of its context would otherwise
228
+ * widen a keyed handler's to `unknown`, which its `Keyed` then refuses.
227
229
  */
228
230
  middleware?:
229
231
  | { [At in keyof Answers]: HandlerRung<Args, Answers[At]> }
230
- | readonly HandlerRung<Args, unknown>[]
232
+ | readonly HandlerRung<NoInfer<Args>, unknown>[]
231
233
  timeout?: number
232
234
  clients?: Clients
233
235
  crossOrigin?: boolean | string[]