@abide/abide 0.125.2 → 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 +10 -0
- package/docs/REGISTRY.md +9 -8
- package/docs/RULEBOOK.md +55 -17
- package/docs/abide.md +26 -16
- package/package.json +1 -1
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/bridgeRelay.ts +110 -33
- package/src/server/claudeCode.ts +26 -22
- package/src/server/faultPolicy.ts +5 -2
- package/src/server/hooks.ts +4 -1
- package/src/server/principal.ts +12 -0
- package/src/shared/isAbort.ts +4 -0
- package/src/shared/onError.ts +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.125.2
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/docs/REGISTRY.md
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
|
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>]` |
|
|
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
|
|
3498
|
-
|
|
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
|
|
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
|
|
3540
|
-
|
|
3541
|
-
|
|
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
|
|
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
|
|
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,
|
|
3587
|
-
the site, the
|
|
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
|
|
3591
|
-
Support
|
|
3592
|
-
install
|
|
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
|
|
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">
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
6640
|
-
| `BridgeStatus` | One status in that stream, `{ paired, command, link, install }`. `install` is the command that installs
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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;
|
|
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>]` |
|
|
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,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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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, {
|
|
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:
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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<
|
|
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
|
-
// `
|
|
197
|
-
let said: string | undefined
|
|
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
|
|
235
|
+
const bridge = BY_KEY.get(key)
|
|
211
236
|
if (
|
|
212
|
-
|
|
213
|
-
|
|
237
|
+
pairing === undefined ||
|
|
238
|
+
pairing.expires <= Date.now() ||
|
|
239
|
+
!CODES.has(pairing.code)
|
|
214
240
|
)
|
|
215
|
-
pairing = mintCode(caller, signedIn)
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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 =
|
|
379
|
+
const pairing = standingCode(code)
|
|
344
380
|
// 44.14 — a code pairs once.
|
|
345
381
|
CODES.delete(code)
|
|
346
|
-
if (pairing === undefined
|
|
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({
|
|
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
|
}
|
package/src/server/claudeCode.ts
CHANGED
|
@@ -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
|
|
9
|
-
// grants or does not: the bridge decides
|
|
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
|
-
/**
|
|
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
|
|
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,
|
|
46
|
-
*
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
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
|
|
154
|
-
*
|
|
155
|
-
* the
|
|
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
|
|
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)
|
package/src/server/hooks.ts
CHANGED
|
@@ -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
|
package/src/server/principal.ts
CHANGED
|
@@ -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
|
*
|
package/src/shared/onError.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|