Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/security-audit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ jobs:
timeout-minutes: 40
# The AUDIT_PAT (read-only Administration + Secrets + Environments)
# lives in this environment, whose deployment-branch-policy admits
# only `main` and `v*` tags — refs that the §3 rulesets reserve to
# admins. A bot-pushed feature branch cannot reach this job at all
# only `main` and `v*` tags — refs that the `Tag operations` and
# `Merge access` rulesets reserve to admins. A bot-pushed feature branch cannot reach this job at all
# (GitHub rejects the run before any step starts), so the PAT
# cannot be exfiltrated through a hand-authored workflow on a
# non-admin-gated ref.
Expand Down
6 changes: 3 additions & 3 deletions docs/specs/dor-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Source of truth: `resolveBrowserViewport` in `dor-lib-common/src/browser-viewpor

## Automated Browser

Dormouse is a viewer and client for the user's installed provider CLI; it neither bundles nor forks a browser. `dor agent-browser` / `dor playwright` extract identity flags, handle `dor-embed-size` and prepare initial sizing ([Viewport presets](#viewport-presets)); native commands then run against the resolved session, and flags Dormouse does not model pass through (`docs/specs/dor-cli.md` → Browser Surface Addressing). The webview's two channels:
Dormouse is a viewer and client for the user's installed provider CLI; it neither bundles nor forks a browser. `dor agent-browser` / `dor playwright` extract identity flags, handle `dor-embed-size` and `dor-diagnostics`, and prepare initial sizing ([Viewport presets](#viewport-presets)); native commands then run against the resolved session, and flags Dormouse does not model pass through (`docs/specs/dor-cli.md` → Browser Surface Addressing). The webview's two channels:

```mermaid
flowchart LR
Expand Down Expand Up @@ -172,7 +172,7 @@ A pane out of sight ([Resource Policy](#resource-policy)) or unmounted parks aft

**Must send a local paste as bounded `input_text` messages** (`viewerTextInputs`), never a key pair per character (rationale). Select-all, copy, cut, and tab select or close go through host operations (`edit`, `tab`), since editing chords do not survive CDP input; undo/redo is not emulated.

Source of truth: `lib/src/components/wall/agent-browser-surface-controller.ts` (`Phase`, `driver`), `lib/src/components/wall/AgentBrowserPanel.tsx`, `lib/src/components/wall/agent-browser-input.ts`, `onBrowserLaunchFailed` in `lib/src/components/Wall.tsx`.
Source of truth: `lib/src/components/wall/agent-browser-surface-controller.ts` (`Phase`, `driver`), `lib/src/components/wall/AgentBrowserPanel.tsx`, `lib/src/components/wall/agent-browser-input.ts`, `viewerTextInputs` in `lib/src/lib/platform/browser-automation.ts`, `onBrowserLaunchFailed` in `lib/src/components/Wall.tsx`.

### Viewer Socket

Expand Down Expand Up @@ -248,7 +248,7 @@ Source of truth: `createPlaywrightProvider` in `lib/src/host/playwright-host.ts`

## Iframe Renderer

`dor iframe <url>` frames the page's own DOM — zero-lag for human inspection, but agents cannot drive or read it. On hosts with `createIframeProxyUrl`, `IframePanel` frames a per-grant loopback proxy URL; without it, a raw uninstrumented iframe. The desktop playground's fronts only its own viewers (`docs/specs/tutorial.md` → Playground filesystem).
`dor iframe <url>` frames the page's own DOM — zero-lag for human inspection, but agents cannot drive or read it. On hosts with `createIframeProxyUrl`, `IframePanel` frames a per-grant loopback proxy URL; without it, a raw uninstrumented iframe. The desktop playground's `createIframeProxyUrl` fronts only its own viewers (`docs/specs/tutorial.md` → Playground filesystem).

The proxy instruments any `http://` upstream, loopback and remote alike: headers are rewritten per the table below and the shim injected into HTML; HTTP and WebSocket traffic passes through. A site's "do not embed" is overridden, not obeyed (rationale); the sandbox neutralizes JS framebusting. **Must refuse link-local / cloud-metadata address literals (`scheme`) after canonicalizing equivalent spellings** — decimal/octal/hex, short forms, IPv4-mapped IPv6; this checks the hostname literal, not DNS answers, and named targets remain the user's command authority.

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ Source of truth: `runBrowserCli` in `dor/src/commands/browser-cli.ts`; `BrowserV

**Must intercept `dor agent-browser` and `dor playwright` before stricli parses provider arguments.**

- **Exactly one identity flag**: `--key` (default `default`), `--session`, or `--surface`, plus `--workspace`; any two fail. Except the Dormouse-owned `dor-embed-size`, arguments are forwarded to the provider; stdout, stderr and exit status pass through.
- **Exactly one identity flag**: `--key` (default `default`), `--session`, or `--surface`, plus `--workspace`; any two fail. Except the Dormouse-owned `dor-embed-size` and `dor-diagnostics`, arguments are forwarded to the provider; stdout, stderr and exit status pass through.
- **Resolution is host-side**: a `--key` or `--surface` takes one `surface.resolveBrowser { provider, key | surface, proposed? }` round trip before the binary runs, answered with a binding `{ session, cwd?, binaryPath? }` (`docs/specs/dor-browser.md` → Managed identity). `proposed` — the caller's cwd and executable — rides only a command that may bind. `--session` and an informational command ask nothing. Outside Dormouse a `--surface` fails.
- **After a command that may bind succeeds, `surface.browser { provider, key?, session, cwd, binaryPath, wsPort? }` opens or reuses its Surface**: `dor agent-browser` first reads the stream port itself (`docs/specs/dor-browser.md` → agent-browser), and the host reports Playwright's stream. **The call must wait past `BROWSER_REQUEST_TIMEOUT_MS`**, since the host's answer can queue behind a launch or close of the browser (rationale). A failure there adds a stderr warning without changing the command's success. **Exception: a fixed-DPR Playwright `open` binds before navigating**, and on a DPR mismatch kills a Surface it created and fails unopened.

Expand Down
6 changes: 3 additions & 3 deletions dor/src/commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ type SendInput =
| { kind: 'key'; key: string };

// stricli parses each flag independently, so it can't enforce "no duplicate input
// flags" or "--text before --key". `cli.ts` runs this before stricli parses, next
// to the flag definitions below — keep the recognized-flag list in `sendFlagName`
// in sync with `parameters.flags`.
// flags" or "--text before --key". `cli-core.ts` runs this as `preParse` before
// stricli parses — keep `sendFlagName`'s input and output flags in sync with
// `parameters.flags`.
export function validateSendFlags(args: string[]): ParseResult<void> {
const flagPositions = new Map<string, number>();
const positionalStart = args.indexOf('--');
Expand Down
2 changes: 1 addition & 1 deletion hosted/server/entitlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ if (!/^[^'\\]+$/.test(ADMIN_EMAIL)) throw new Error("ADMIN_EMAIL cannot be inlin

/**
* The entitlement (docs/specs/pricing.md -> "Checkout and entitlement"): a
* SQL boolean over the `"user"` row aliased `user`, so a query that resolves
* SQL boolean over the `"user"` row aliased by `user` (default `u`), so a query that resolves
* a bearer resolves its owner's entitlement in the same statement. Admin-only
* until billing ships: the verified `ADMIN_EMAIL`.
*/
Expand Down
2 changes: 1 addition & 1 deletion lib/src/host/managed-voice-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export function createManagedVoiceHost(options: {
const speakUrl = voice === null ? null : voice + MANAGED_VOICE_SPEAK_PATH;
const tokenUrl = voice === null ? null : voice + MANAGED_VOICE_TOKEN_PATH;
let loaded: Promise<StoredManagedVoice> | null = null;
// Each edit reads, then rewrites the whole file: two at once would drop a field.
// Each edit derives its record from the loaded one: two at once would drop one's change.
const serialize = createSerialQueue();
const clips = createClipCache();
let notEntitled = false;
Expand Down
29 changes: 15 additions & 14 deletions lib/src/host/remote/native-direct-peer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
* **The addon is resolved here rather than through the shipped factory.** It is
* installed under `standalone/sidecar`, which is where the shipped bundle finds
* it with a bare `require`; `lib` must not depend on it, because `lib` is a
* browser bundle root. So these cases inject a plain factory over the polyfill
* this file resolves, and the shipped factory is driven by the last case alone.
* browser bundle root. So the negotiation cases inject a plain factory over the
* polyfill this file resolves; the Local networks, Anywhere, and teardown cases
* build through the shipped factory, and only the last tears it down.
*/

import { createSocket, type SocketType } from 'node:dgram';
Expand Down Expand Up @@ -99,7 +100,7 @@ const CASE_BUDGET_MS = 45_000;
interface Negotiation {
/** Whether the channel this attempt describes has come up. */
open(): boolean;
/** Drop this attempt's peers, so a retry does not run beside them. */
/** Close this run's peers; each case calls it once it is done with them. */
abandon(): void;
}

Expand Down Expand Up @@ -559,17 +560,6 @@ describe('the direct path over the native addon', () => {
}
});

/**
* The shipped factory, last because its teardown is the process's.
*
* **A teardown is terminal**: the native threads are gone, so a peer built on
* them is not one this process can use and every later offer declines,
* leaving that session relayed. The other half of the contract — a load that
* fails warns once and declines from then on — is not drivable here: vitest's
* `require` resolves from inside the store, where every workspace package is
* reachable, so the addon cannot be made to not load without replacing the
* module system this case exists to exercise.
*/
/**
* **What the reliability check does *not* reach on this stack.** `DirectPeer`
* refuses a channel that is unordered or partially reliable, and the answerer
Expand Down Expand Up @@ -601,6 +591,17 @@ describe('the direct path over the native addon', () => {
CASE_BUDGET_MS,
);

/**
* The shipped factory, last because its teardown is the process's.
*
* **A teardown is terminal**: the native threads are gone, so a peer built on
* them is not one this process can use and every later offer declines,
* leaving that session relayed. The other half of the contract — a load that
* fails warns once and declines from then on — is not drivable here: vitest's
* `require` resolves from inside the store, where every workspace package is
* reachable, so the addon cannot be made to not load without replacing the
* module system this case exists to exercise.
*/
it('builds a peer through a bare require, and declines once torn down', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/lib/managed-voice-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface ManagedVoicePlayback {
create(url: string): ManagedVoiceAudio;
}

/** Blob URL + `HTMLAudioElement`; the standalone CSP grants `media-src blob:`. */
/** Blob URL + `HTMLAudioElement`; both hosts' CSPs grant `media-src blob:`. */
export const browserPlayback: ManagedVoicePlayback = {
url: (audio) => URL.createObjectURL(new Blob([audio as BlobPart], { type: 'audio/mpeg' })),
revoke: (url) => URL.revokeObjectURL(url),
Expand Down
4 changes: 2 additions & 2 deletions relay/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ export interface AppConfig {
readonly vapidPublicKey?: string;
/**
* Web Push delivery. Injectable for the same reason as `now` — the send route
* is testable without a real push service. `index.ts` supplies the `web-push`
* implementation.
* is testable without a real push service. `start.ts` supplies
* `createWebPushSender`.
*/
readonly pushSender?: PushSender;
/**
Expand Down
3 changes: 2 additions & 1 deletion relay/src/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ function decodeVapidKey(value: string, name: 'public' | 'private', length: numbe
}

/**
* Real delivery through `web-push`, at the shared `PUSH_TTL_SECONDS`. The
* Real delivery: `web-push` builds the encrypted body and VAPID headers, and
* `postToPushService` sends them, at the shared `PUSH_TTL_SECONDS`. The
* request timeout is a separate bound on socket inactivity while talking to
* the push service.
*/
Expand Down
2 changes: 1 addition & 1 deletion relay/src/setup-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class SetupTokenIssuer {
}
}

/** Keep only `limit` live tokens for one Burrow, oldest expiry first. */
/** Keep only `limit` outstanding tokens for one Burrow, oldest expiry first. */
#trimBurrow(burrowId: string, limit: number): void {
// Only this Burrow's rows are candidates, so a loop of mints cannot cost
// another Burrow the token it is currently displaying.
Expand Down
2 changes: 1 addition & 1 deletion remote-lib-common/src/security/acl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface BurrowAclRecord {
/**
* The opaque bearer capability this Client registers, queries, and deletes
* push subscriptions with (`docs/specs/remote-security-model.md` → Push
* sealing). Minted by the Burrow at approval, base64url of 32 random bytes, and
* sealing). Minted by the Burrow at approval with `mintPushDeliveryId`, and
* known only to this record and the Client's own `KnownBurrowV1`.
*/
readonly deliveryId: string;
Expand Down
2 changes: 1 addition & 1 deletion scripts/pairing-walkthrough/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Nothing in step 8 is driven around the product. The digits go into the modal's o

## Artifacts

Everything lands in the run directory, whose path is printed at the start and at the end. **Nothing is written into the repo.**
Everything lands in the run directory, whose path is printed at the start and at the end. **No artifact is written into the repo**; Burrow staging still is (Known limitations).

```
relay.log the Relay's whole stdout/stderr
Expand Down
2 changes: 1 addition & 1 deletion website/src/lib/tut-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const FLAPPY_TITLE = "🐭 FlappyTerm 🐭";
const FLAPPY_DESKTOP_GAME_OVER_PROMPT = "Read about Dormouse Pocket [p]";
const FLAPPY_POCKET_GAME_OVER_PROMPT = "Dormouse Hosted updates [n]";

// --- Flappy Term game constants (ported from flappy-term.html) ---
// --- Flappy Term game constants ---
const FLAPPY_TICK_MS = 60;
const FLAPPY_GRAVITY = 0.18;
const FLAPPY_FLAP_V = -1.1;
Expand Down
Loading