Skip to content

Remote control ("Pocket"): self-hosted phone terminal#203

Open
nedtwigg wants to merge 33 commits into
mainfrom
security-model
Open

Remote control ("Pocket"): self-hosted phone terminal#203
nedtwigg wants to merge 33 commits into
mainfrom
security-model

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Jul 3, 2026

Copy link
Copy Markdown
Member

Remote control ("Pocket"): self-hosted phone terminal

Adds the first end-to-end path for driving a Dormouse terminal from a phone: a self-hosted relay server, a shared security model, and a Pocket web app that renders a real remote Host over a terminal-only remote-api v1. This is a POC (one owner account, terminal surfaces only), built as five vertical slices behind the design specs in docs/specs/.

What's here

server-lib-common/ — runtime-agnostic contracts shared by server, host, and client so the three sides can't drift:

  • Security primitives (docs/specs/remote-security-model.md): device keys, host challenges, passkey-assertion verification, the Host ACL, the pairing ceremony, and connection authorization — with end-to-end tests covering every spec guarantee.
  • The wire contract (remote/wire.ts): HTTP routes, relay frames, and the terminal-only remote-api v1 messages.

server/ — a Hono webserver acting as the coordinating relay:

  • Slice 1: selfhost accounts + "WebAuthn without a WebAuthn library" passkey auth.
  • Slice 2: host enrollment + a transport-dumb WebSocket relay.
  • Slice 3: the security handshake layered on the relay (pair/connect/connect2 verification).
  • Slice 5: serves the built Pocket app statically with SPA fallback.

lib/src/remote/ — the host and client halves plus the Pocket app:

  • Host: enrollment, pairing approval, and the terminal bridge (RemoteApiSession) that translates the wire protocol into the existing xterm/PTY plumbing.
  • Client + Pocket app: a RemotePtyAdapter that implements the existing PlatformAdapter seam, so the same MobileTerminalUi the site proves out with a fake adapter renders a live remote Host (phase 1a/1b).

Notes

  • The remote session is modeled as a platform adapter, not a special case bolted onto shared components (docs/specs/pocket-app.md).
  • The final commit (consolidate duplicated helpers behind shared contracts) is a cleanup pass: a shared clampTerminalDimension and RemoteWebSocket type, a JsonFileStore base behind the two server stores, a readPasswordGated helper and #resolveSurface guard for the repeated route boilerplate, and use of the shared loadHostAcl.

Testing

  • server-lib-common, server: tsc + node test suites green.
  • lib: 776 vitest tests pass, tsc -b clean.

🤖 Generated with Claude Code

nedtwigg and others added 20 commits July 2, 2026 10:23
One protocol, two consumption depths: the phone watches a pane
directory and attaches to one surface; VR replicates the whole wall
(layout tree + per-surface streams + the existing surface.* mutation
vocabulary). Terminals replicate as PTY data + semantic events; browser
surfaces as adaptive screencasts; iframes are placeholder-only in v1.
Rides on the authorized session from the remote security model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t, no iframes

- transport is WebSocket relay only in v1; WebRTC and app-layer
  encryption noted as API-neutral future upgrades
- new Server deployment modes section: selfhost (env-var setup password,
  single user, passkey sign-in, local files, no database) ships in v1
  and remains supported forever; SaaS multitenant (email + passkey)
  comes later
- iframe surfaces are simply unsupported: omitted from the directory,
  refuse attachment, inert placeholder in wall snapshots
- DirectoryEntry: attention -> ringing + hasTODO

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tic scrollback as v2

v1 terminal attach carries the client's dimensions and the resize IS
the repaint: TUIs redraw on SIGWINCH (with a SIGWINCH-then-nudge
fallback when sizes happen to match), shells redraw their prompt, and
no snapshot transfers. The in-flight command's output (captured from
its OSC 133/633 commandStart boundary, tail-capped) replays on attach
so 'is my build done?' works. History is explicitly v2: semantic
command scrollback — per-command blocks rendered at the client's own
width — additive to the v1 protocol.

Size authority flips from host-owned to last-attach-wins; displays
that lose authority grey out as 'tethering to <device>', and the wall
lease becomes purely presentational (wholesale tethering).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New 'v1 scope' section pins v1 to: hello, snapshot-only directory (no
deltas, no thumbnails), one attachment per session, terminal
attach-is-the-resize with last-attach-wins tethering, browser frames at
fixed quality with pointer/key input, and a single flat grant (selfhost
is single-user, so every paired session is the owner).

Deferred, each additive to the v1 protocol: in-flight replay (first
follow-up; agent TUIs repaint on resize, which is what makes it
deferrable), semantic scrollback (v2), thumbnails, graded grants +
layout mutations, the whole VR/wall section, WebRTC, audio.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nal in five slices

One Hono process, no database (account.json + hosts.json), terminal-only.
WebAuthn needs no library: registration uses the browser's
getPublicKey() SPKI (no CBOR/attestation), assertions verify through the
same server-lib-common function the host uses, and server challenges
reuse HostChallengeIssuer. One host challenge feeds both the passkey
assertion and the device-key signature, so connecting costs one
biometric prompt. Build order is five independently testable slices;
the first three are pure Node, driven end to end by the SimAuthenticator
harness with no browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HTTP routes/payloads, relay frames (client/server/host), and the
terminal-only remote-api v1 messages, so server, host module, and
Pocket UI build against one contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements slice 1 of the selfhost server POC (docs/specs/server.md):
accounts and passkeys, pure Node with full automated coverage.

- createApp(config) factory ({ setupPassword, origin, stateDir, now? }),
  replacing the module-level app singleton; index.ts builds config from
  env (DORMOUSE_SETUP_PASSWORD required, DORMOUSE_ORIGIN/STATE_DIR/PORT
  defaults) and derives rpId from the origin hostname.
- state.ts: account.json persistence (atomic tmp+rename, mutation mutex,
  duplicate-credential guard). No hosts.json yet (slice 2).
- Endpoints: /api/setup/begin+finish (constant-time password gate with a
  250ms failure delay, clientDataJSON sanity checks, SPKI import check),
  /api/signin/begin+finish (single-use challenge consume then
  verifyPasskeyAssertion, in-memory 12h sessions). Setup and sign-in use
  separate HostChallengeIssuer instances so challenges cannot cross flows.
- Exposed SessionStore.validate (raw token, for the slice-2 WS path) and a
  requireSession Bearer middleware.
- 19 node --test cases via SimAuthenticator: register happy path (on-disk
  account.json), wrong password (begin/finish), challenge replay, wrong
  origin, wrong clientData type, duplicate credential, unimportable key,
  sign-in happy path, unknown credential, replay, tampered signature,
  foreign origin, expired session, requireSession gating.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds slice 2 of the selfhost POC (docs/specs/server.md): hosts.json, host
enrollment, the client/host WebSocket relay, and presence.

- HostStore: atomic `hosts.json` of { hostId, hostToken, label, enrolledAt },
  random base64url ids/tokens, append-under-mutex — mirrors AccountStore.
- POST /api/host/enroll (password-gated) → { hostId, hostToken, origin, rpId }.
- GET /api/hosts (session Bearer) → per-host { hostId, label, online }.
- RelayHub + @hono/node-ws: one live socket per hostId (reconnect replaces),
  server-assigned secret clientId, handshake allowlist (pair/connect/connect2
  up, pair-result/challenge/decision down), msg gated on an allowed host
  decision, client-gone/host-gone teardown. Bad WS tokens 401 at upgrade.
- index.ts injects the WS handler after serve().

Tests: 37 server tests green (enroll, presence flip, token rejection, frame
round-trips, msg gating both directions, disconnect fan-out, vanished-peer
safety); server-lib-common still 111 green.

Co-Authored-By: Claude <noreply@anthropic.com>
Layer server-side handshake verification on the transport-dumb RelayHub via a
new handshake policy module the hub consults:

- pair: verify the request is consistent with the authenticated session (owner
  account, a registered passkey credential, matching stored public-key hash)
  before relaying; reject locally otherwise, never forwarding.
- connect2: verify accountId, that the asserted credential is registered and the
  presented publicKey matches the STORED key, that the assertion verifies against
  the stored key over request.challenge, and that the challenge equals the one
  the server relayed to this client (single-use). Reject before forwarding on any
  failure; the Host stays the final authority via its own decision.
- Remember each relayed Host challenge (clientId -> challenge) for the freshness
  half of connect2 validation.
- Fix the slice-2 stale-session gap: invalidate established sessions (host-gone +
  clear) on Host replacement, not only on disconnect.

Add a reusable headless FakeHost harness (HostAcl/HostChallengeIssuer/
PairingCeremony/authorizeConnection + minimal remote-api hello) and a runnable
scripts/fake-host.mjs for manual testing. E2E tests drive the full flow (real
authenticator + device key as the phone, FakeHost as the laptop) through a real
listening server, plus deny cases: unpaired device (Host denies), server-side
rejections before forwarding (assertion/challenge mismatch, unknown credential,
substituted publicKey, unregistered pair credential), replayed connect2, msg
blocked after denial, and host-restart session invalidation.

Co-Authored-By: Claude <noreply@anthropic.com>
…l bridge (slice 4)

Adds lib/src/remote/host: a standalone-active Host that enrolls with the
selfhost Server, holds the /ws/host relay socket, shows a real pairing
approval modal, persists its HostAcl to localStorage, is final authority
for connections (authorizeConnection), and bridges real terminal panes over
the remote-api v1 terminal-only protocol.

Mirrors server/test/harness/fake-host.mjs exactly: pair-result carries the
HostAclRecord, an allowed decision omits failures, msg is gated on an allowed
decision, and client-gone drops per-client state.

- enrollment.ts / acl.ts: localStorage persistence (records()/fromRecords)
- remote-host.ts: DOM-free controller (injected socket + session factory)
- remote-api.ts: hello / directory.watch (coalesced snapshots) /
  surface.attach (attach-is-the-resize via xterm.resize) / terminal.write /
  terminal.resize / surface.detach, forwarding PTY data as base64url
- RemotePairingModal(Host): approval UI wired next to the other Wall modals
- activation.ts: window.dormouseRemoteHost console hook (enroll/status/clear)
- tests: pure directory builder, ACL + enrollment localStorage round-trips,
  frame handling with a fake socket and real WebCrypto

Co-Authored-By: Claude <noreply@anthropic.com>
The phone-side Pocket app (built from `lib`, served statically by `server`),
completing the selfhost remote-control POC: sign in / first-time passkey setup,
a persisted IndexedDB device key, pair-then-connect against a host, pick a live
terminal pane, and view + type into it over the remote-api v1 relay.

- lib/src/pocket: `webauthn.ts` (navigator.credentials wrappers), `device-key.ts`
  (non-extractable CryptoKeys in IndexedDB, injectable store), `pocket-client.ts`
  (UI-free protocol client driving the exact register→signin→pair→connect→msg
  flow with injected fetch/webauthn/WebSocket), plus `App.tsx`/`main.tsx` and a
  self-contained dark mobile UI (xterm + fit addon terminal view).
- Pocket build: `vite.pocket.config.ts` + `pocket/index.html` → `dist-pocket/`,
  script `build:pocket`; the main build is untouched.
- server: serves `pocketDir` statically at `/*` (SPA fallback to index.html),
  new optional `pocketDir` config + `DORMOUSE_POCKET_DIR` env defaulting to
  `lib/dist-pocket`; API and `/ws` routes keep precedence, missing build → a
  stub naming the build command.
- Tests: vitest for the protocol client (setup/signin, pair, connect
  allowed/denied, request/response + event routing by subId, injected-store
  device key); node:test for static serving + API precedence.

Co-Authored-By: Claude <noreply@anthropic.com>
Verified end to end against the production entrypoint: server boots
from env vars, serves the Pocket build, and a scripted phone (real
WebAuthn assertions via the test authenticator) registered, signed in,
paired with the fake host, passed the connect handshake, and exchanged
remote-api messages over the bridged relay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Host enrolls from the webview's own origin (the vite dev port), so
the JSON POST triggers an OPTIONS preflight that previously 404'd and
blocked the fetch. Permissive CORS is safe here: every endpoint is
gated by the setup password or a bearer token and nothing rides on
cookies. Verified with a live preflight against the production
entrypoint; three regression tests added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rm adapter

One lib-owned Pocket: auth screens + MobileTerminalUi/MobileWall on a
RemotePtyAdapter, the same composition the website playground proves
with FakePtyAdapter. Always served same-origin with its API (WebAuthn
origin binding + Chrome PNA both demand it): selfhost Node server now,
CloudFlare static + routed /api,/ws for SaaS later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…RemotePtyAdapter (phase 1a)

Move the UI-free Pocket protocol modules (pocket-client, device-key, webauthn
+ test) from lib/src/pocket to lib/src/remote/client with git mv, and fix the
POC UI's imports (App.tsx, PocketTerminal.tsx) — phase 1b replaces that shell.

Add RemotePtyAdapter: a PlatformAdapter backed by a connected PocketClient so
the mobile terminal UI can render a real remote Host over remote-api v1.
directory.snapshot → onPtyList + getDirectoryEntries/subscribeDirectory;
setActivePane drives the one-attachment-per-session detach→attach; terminal.data
→ onPtyData (base64url utf8 → string); writePty/resizePty reach only the
attached pane; terminal.closed → onPtyExit. Everything outside the PTY core
no-ops or is absent.

Extend the fake-host harness with a synthetic directory + two echo shells so the
adapter is testable end to end without a real Host, and add coverage: a lib
vitest suite against a network-free fake client, and a server node --test that
drives the real wire (SimAuthenticator + device key through the relay).

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the bespoke POC terminal UI with the real mobile experience. The
Pocket shell now lives in lib/src/remote/pocket-app/: the proven auth flow
(setup/signin -> hosts -> pair/connect) followed by MobileTerminalUi +
MobileWall driven by a RemotePtyAdapter over the connected PocketClient —
the same composition the website playground proves out with FakePtyAdapter.

On connect: construct the adapter, setPlatform, dispose stale sessions +
initAlertStateReceiver, then watch the directory. Sessions come from the
directory snapshot (id = surfaceId); ringing/hasTODO/cwd map onto the
existing session-item badge affordances. Active-pane changes funnel through
adapter.setActivePane (one attachment per session); writes/paste target the
active pane. Socket drop disposes the adapter and returns to Hosts.

Delete PocketTerminal.tsx and the bespoke xterm handling; retarget the pocket
vite entry (build:pocket / dist-pocket unchanged) and add Tailwind + theme
plumbing so the shared terminal UI is styled.

Co-Authored-By: Claude <noreply@anthropic.com>
server/data holds the live account.json and hosts.json (passkey public
keys and bearer host tokens) whenever the server runs with the default
DORMOUSE_STATE_DIR; it must never be committable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… contracts

- clampTerminalDimension in wire.ts replaces three per-file clamps
  (host remote-api, client adapter, fake-host harness)
- RemoteWebSocket in lib/src/remote/ws.ts unifies the identical
  PocketSocket and WebSocketLike interfaces
- JsonFileStore base class extracts the atomic-write + mutex machinery
  shared by AccountStore and HostStore
- readPasswordGated dedupes the three password-gated routes; relay.ts
  hoists the per-case client guard; RemoteApiSession gains
  #resolveSurface; directory-collect stops double-fetching pane state;
  loadHostAcl is injectable and owns the fallback-to-empty path

Review fixes folded in: the SPA deep-link fallback re-reads index.html
per request (a cached copy would point at deleted content-hashed assets
after an in-place Pocket rebuild, and a read failure now degrades to a
404 instead of crashing createApp at startup), and loadHostAcl's
fallback logs a console.warn so a dropped ACL is explicable rather than
silently de-pairing every client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds server/ and server-lib-common/ to the architecture and structure
sections, lib/src/remote/ to the structure list, and spec entries for
remote-security-model.md (read first for anything remote),
remote-api.md, server.md, and pocket-app.md with read-this-when-touching
file lists matching the existing format.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per the AGENTS.md convention, specs using Pane/Surface vocabulary lead
with a glossary blockquote; the Terminology section now defers to
glossary.md as canonical and keeps only remote-specific usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 32dedb7
Status: ✅  Deploy successful!
Preview URL: https://20f52e8c.mouseterm.pages.dev
Branch Preview URL: https://security-model.mouseterm.pages.dev

View logs

The standalone `tsc --noEmit` typechecks lib SOURCE through the
`dormouse-lib/* → ../lib/src/*` path map. Phase 1b wired
`RemotePairingModalHost` into the shared `Wall`, so that graph now
reaches `lib/src/remote/host/*`, which import `server-lib-common`.

As a bare specifier that resolves to `server-lib-common`'s package
`exports` (`./dist/index.d.ts`), which the standalone smoketest job never
builds — so tsc reports TS2307 "Cannot find module 'server-lib-common'".
Map it to source like `dormouse-lib` and `dor` so the typecheck needs no
prior build. Verified with both sibling `dist/` dirs removed (the CI
state): `npx tsc --noEmit` in `standalone` exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the new server relay/handshake and the Host bridge closely — the trust model reads carefully: the server verifies connect2 assertions against the stored passkey key (never the request-carried one), consumes the relayed challenge single-use before forwarding, invalidates sessions on Host replacement, and leaves authorizeConnection as the Host's final authority. Nice. The one thing blocking a green run is a build-graph gap, not the code itself.

Visual Regression + Cloudflare Pages fail: server-lib-common isn't built for the browser bundles

Both red checks share one root cause. This PR makes Wall.tsx statically import RemotePairingModalHostenrollment.tsimport … from 'server-lib-common'. Wall has Storybook stories and is bundled into the website, so both browser bundles now pull in server-lib-common, which resolves via its exports to ./dist/index.js. That dist/ is gitignored and only gets built in pipelines that build it explicitly:

  • Build & Test is green because pnpm -r run test runs server-lib-common's own test script (pnpm run build && node --test …), which produces dist/ as a side effect before lib's vitest runs.
  • Chromatic (build-storybook) and the website build never build it, so rolldown fails: Rolldown failed to resolve import "server-lib-common" from "./src/remote/host/enrollment.ts". Chromatic was green on main (f13ea08), so this is new to the PR.

Two fixes, both defensible:

  1. Alias to source in the two bundler configs, matching the pattern already there. lib/.storybook/main.ts's viteFinal already aliases dor and dormouse-lib to source precisely for this class of failure (its comment: "Storybook's Vite doesn't read tsconfig paths, so … any Wall-importing story fails"). Adding 'server-lib-common': path.resolve(here, '..', '..', 'server-lib-common', 'src') there, and the same alias in website/vite.config.ts's resolve.alias, fixes both without a build step.
  2. Build it first in each pipeline (mirrors what Build & Test gets for free) — e.g. build server-lib-common before build-storybook in chromatic.yml and before the website build.

I'd lean toward option 1 since it's the established convention in this repo and needs no CI-ordering. Happy to push a commit with the aliases (Storybook + website) and confirm both builds go green locally — say the word.

nedtwigg and others added 3 commits July 2, 2026 20:05
The Chromatic "Visual Regression Tests" job runs `build-storybook` in
`lib` after `pnpm install`, without building `server-lib-common`. A Wall
story pulls `Wall -> RemotePairingModalHost -> remote/host/* ->
server-lib-common`, whose package `exports` resolve to an unbuilt
`dist/index.js`, so Rolldown fails: "failed to resolve import
'server-lib-common' from './src/remote/host/remote-api.ts'".

Alias the bare specifier to source in `.storybook/main.ts`, exactly as
`dor` and `dormouse-lib` already are (Storybook's Vite ignores tsconfig
paths). Verified by removing `server-lib-common/dist` (the CI state):
`build-storybook` then completes successfully.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Cloudflare Pages build runs `pnpm build:website` (`react-router
build`) after a fresh `pnpm install`, without building `server-lib-common`.
The desktop playground (`PlaygroundDesktop.tsx`) bundles `Wall`, which
now pulls in `RemotePairingModalHost -> remote/host/* ->
server-lib-common`. That package's `exports` resolve to an unbuilt
`dist/index.js`, so Rolldown fails: "failed to resolve import
'server-lib-common' from '.../lib/src/remote/host/remote-host.ts'".

Alias the bare specifier to source in `vite.config.ts`, exactly like the
existing `dormouse-lib` alias. Verified by removing `server-lib-common/dist`
(the Cloudflare state): `pnpm build:website` then completes successfully
(client + SSR bundles).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nedtwigg and others added 3 commits July 2, 2026 22:40
registerHost displaces the old socket and invalidates its clients'
sessions, but the displaced socket's handler could still deliver queued
frames — a late 'decision allowed' would re-establish a session the
replacement just dropped, and 'msg' data from the dead host process
would route as if current. onHostFrame now only routes frames from the
socket the hub's map points at.

Unit tests drive RelayHub directly with fake sockets: displaced
decision/msg/challenge/pair-result frames are all ignored, the
replacement socket works end to end, and a stale socket cannot speak
for a host that has since gone offline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Pocket entry imports the remote modules, which import
server-lib-common; its package exports point at a dist that a clean
checkout has not built (this vite-only build has no tsc -b step), so
build:pocket — the FIRST step of dev:pocket-server — failed before the
server build could generate it. Alias to source, the same idiom the
website, Storybook, and standalone configs already use. Verified by
deleting server-lib-common/dist and building.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sskeys, socket loss

Three lifecycle fixes from PROBLEMS.md:

- surface.detach now names its surface: a stale detach for a pane the
  client already switched away from is an idempotent no-op instead of
  tearing down the newer attachment (host module + fake-host parity,
  covered by a wire-level rapid-switch test)
- passkey registration requires a resident/discoverable credential
  (residentKey 'required' + requireResidentKey): sign-in discovers with
  an empty allowCredentials, so a non-resident credential — which
  'preferred' can silently produce — would register fine and then never
  be able to sign in; failing at setup is the recoverable place
- an unexpected relay socket close is now host loss: pocket-client
  fires onHostGone for an established session (matching its own doc
  comment), nulls the socket so it can never be reused, and clears
  connectedHostId on host-gone frames; PocketClient.close() nulls the
  socket first so an intentional close never reads as a drop; the app
  reopens via the new socketOpen getter instead of a stale ref, so a
  server restart or network drop self-heals without a page reload

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nedtwigg and others added 6 commits July 3, 2026 08:36
…app UI

Cover every new UI surface this branch adds:
- Modals/RemotePairingModal — Default, UnnamedDevice, LongValues
- Pocket/SetupOrSignin — Welcome, SetupExpanded, SigningIn, CreatingAccount, Error
- Pocket/HostsView — Empty, MixedList, Pairing, Connecting, Refreshing, Error
- Pocket/PocketWall — SingleSession smoke over a fake RemoteAdapterClient

Export SetupOrSignin, HostsView, and HostView from the Pocket App so the
prop-driven views can be storied directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants