diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bb26580f..c73974ee 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,62 +1,64 @@
# Contributing to peerd
-Thanks for helping out. peerd is a browser-native AI agent that runs **entirely
-in your browser** — no backend, no account, no telemetry, bring-your-own-key.
-That promise is load-bearing (it's in the README, `CLAUDE.md`, and the manifest
-itself), so the one hard rule for any change: **don't add a backend call,
-telemetry, analytics, or anything that phones home.**
+Thanks for helping out. peerd is a browser-native AI agent with no hosted agent
+backend, account, or telemetry. It supports bring-your-own-key providers.
+That requirement applies to every change. It is documented in the README,
+`CLAUDE.md`, and the manifest. **Do not add a backend call,
+telemetry, analytics, or an undeclared network service.**
## The one thing to know: there is no build step
peerd is vanilla JavaScript + ES modules. The browser runs the code exactly as
-written — no bundler, no transpiler, no watch process. The dev loop is just
-**edit → reload the extension.**
+written. There is no bundler, transpiler, or watch process. The dev loop is
+edit, then reload the extension.
## Setup
1. Clone, then `bun install`. This installs the **dev tooling only** (the test
runner, ESLint, the type checker). The extension itself needs no install and
no build to run.
-2. Load it unpacked — follow the **"Load unpacked"** steps in the
- [README](README.md): `chrome://extensions` → enable Developer mode → **Load
- unpacked** → pick the `extension/` directory.
+2. Load it unpacked. Follow the **"Load unpacked"** steps in the
+ [README](README.md). Open `chrome://extensions`, enable Developer mode,
+ choose **Load unpacked**, and select the `extension/` directory.
3. After an edit, click the reload icon on the extension's card (or reload the
page you're testing). That's the whole loop.
`extension/manifest.json` and `extension/shared/channel-config.js` are
**generated** by `bun run gen:dev` (from `manifests/*.json` +
-`packaging/default-settings.mjs`) — don't hand-edit them; edit the source and
+`packaging/default-settings.mjs`). Do not hand-edit them. Edit the source and
regenerate. CI fails on drift.
## Read this first
[`CLAUDE.md`](CLAUDE.md) is the architecture orientation: the codebase is five
`peerd-*` modules, one per letter of the wordmark. Skim it before anything
-non-trivial. The code is the spec — there's no separate design-doc corpus.
+non-trivial. The code is the spec. There is no separate current design-doc corpus.
-## Tests — three surfaces, different jobs
+## Tests: three surfaces, different jobs
-- **Bun** — `bun test ./tests`. Pure logic, no browser, runs in the terminal in
- under a second. *If your test is values-in, values-out, it goes here.*
-- **In-browser** — `bun scripts/cdp/run-inbrowser-tests.mjs` (or open
+- **Bun:** `bun test ./tests`. Use this for pure logic with no browser.
+- **In-browser:** `bun scripts/cdp/run-inbrowser-tests.mjs` (or open
`extension/tests/runner.html`). Anything that needs a real browser: the DOM,
`chrome.*`, IndexedDB, the side-panel components.
-- **Live end-to-end** — `bun run e2e:verify`. Drives the real extension through
+- **Live end-to-end:** `bun run e2e:verify`. Drives the real extension through
the side panel via Chrome DevTools Protocol.
Rule of thumb: *if a test would have to mock half the world to run, it wants the
browser; if it's values in and values out, it wants Bun.*
-The in-browser and e2e runners need Chrome for Testing — `bun run e2e:chrome`
+The in-browser and e2e runners need Chrome for Testing. `bun run e2e:chrome`
fetches it.
+For UI work, inspect `scripts/cdp/artifacts/result.json` and every generated
+screenshot. A passing assertion does not replace visual inspection.
+
## Before you push
-Run **`bun run preflight`**. It mirrors CI (generated-file drift, ESLint, the
-type check, the dweb boundary, the packaged-import check, and the Bun suite), so
-a green preflight is a green CI in miniature. `bun run preflight -- --matrix`
-additionally builds and boots the packaged artifacts (slower; needs Chrome for
-Testing).
+Run **`bun run preflight`**. It covers local generation, lint, type, unit,
+security-invariant, and package-boundary checks. Use the in-browser suite for
+browser behavior. For UI or complete flow changes, also run `bun run e2e:verify`
+and inspect its result file and screenshots. CI runs additional security,
+network, browser, package, and visual lanes.
## House conventions
@@ -64,10 +66,13 @@ Most are enforced by `bun run lint` (ESLint autofixes much of it with
`eslint extension --fix`). The essentials:
- Vanilla JS, ES modules, **no new build step and no npm runtime dependency** in
- the extension — third-party code lives in `vendor/` with a `SOURCE.txt`.
+ the extension. Third-party code lives in `vendor/` with a `SOURCE.txt`.
- A module's `index.js` is its public API; import across modules only through it.
+- `peerd-distributed` is stricter. Nothing outside that module imports it,
+ including its `index.js`. Use `shared/dweb-interface.js` and
+ `shared/dweb-loader.js`.
- Comments explain **why**, not what.
-- Modern, functional JS — `const`/`let` not `var`, arrow callbacks, template
+- Modern, functional JS: `const`/`let` not `var`, arrow callbacks, template
literals, array methods.
- Filenames are `lower-hyphenated.js`.
@@ -75,12 +80,13 @@ The full list lives in `CLAUDE.md` and `eslint.config.js`.
## Opening a pull request
-- Keep it focused — one concern per PR.
-- Title it like `fix(area): …`, `feat(area): …`, or `test(area): …`.
+- Keep it focused. Use one concern per PR.
+- Use a title such as `fix(area): description`, `feat(area): description`, or
+ `test(area): description`.
- Make sure `bun run preflight` is green first.
- The pull-request template will prompt for the rest.
-**New here?** Look for issues labelled **`good first issue`** — they're scoped to
+**New here?** Look for issues labelled **`good first issue`**. They are scoped to
be a clean first contribution.
Found a security issue? Please follow [`SECURITY.md`](SECURITY.md) rather than
diff --git a/README.md b/README.md
index 83670b22..a8b6b73c 100644
--- a/README.md
+++ b/README.md
@@ -7,553 +7,162 @@
[](https://github.com/NotASithLord/peerd/actions/workflows/package-and-release.yml)
[](LICENSE)
-[](#install)
+[](#status)
[](#install)
-[-success.svg)](#getting-started)
-
-[](packaging/check-tscheck.ts)
[](SECURITY.md)
-
+peerd is a browser extension that runs an AI agent in the browser. It can work
+with tabs, create isolated compute environments, and build local browser apps.
+You choose the model provider and supply any required key. peerd has no account,
+hosted agent backend, or telemetry. Model requests go directly from the
+extension to the provider you choose.
-**peerd is the first AI agent harness native to the browser.** It's a
-Chrome/Firefox extension that runs a full agent loop *inside* the
-browser you already use, with your existing tabs and sessions.
-It reads and drives your pages, spins up sandboxed compute (JS
-Notebooks, compiled WebAssembly tools, full Linux VMs, personal
-client-side apps), and (on the preview channel) shares what it builds over a
-peer-to-peer WebRTC network built for agent-to-agent communication. BYOK
-to the model provider of your choice. **No backend, no telemetry, no
-cloud component in the data path.**
+## Status
-
+peerd is an experimental 0.x beta. Breaking changes are likely. Storage formats
+and product behavior may change. It can drive browser pages and use API keys, so
+review the security model before using it with sensitive data.
+The code is the source of truth for current behavior. Start with
+[`CLAUDE.md`](CLAUDE.md), then read the relevant module under `extension/`.
+## What it does
+- Runs an agent loop in a Chrome or Firefox extension.
+- Reads and drives browser tabs through per-environment actors.
+- Runs Linux WebVMs, JavaScript Notebooks, browser Apps, and headless scripts.
+- Supports cloud and local model providers. The live list is in
+ [`extension/peerd-provider/registry.js`](extension/peerd-provider/registry.js).
+- Stores provider secrets in a local encrypted vault.
+- Includes an optional peer-to-peer dweb in preview builds.
-https://github.com/user-attachments/assets/d2e4c285-6952-4c95-bf5a-d06087de084d
+Apps and WebVMs currently run on Chrome only. Apps have no ambient network access. Remote
+resources, fetches, WebRTC, forms, and external document navigation are blocked.
+External HTTP and HTTPS links require user confirmation.
+## Security model
+peerd uses browser isolation, narrow tool exposure, service-worker policy gates,
+and explicit egress controls. The main agent delegates environment work to
+keyless actors. On Chrome, non-orchestrator agent loops run in separate worker
+heaps. Firefox uses a more limited fallback where browser APIs do not provide the
+same isolation features.
+Network behavior depends on the operation. Model calls, web reads, runtime asset
+loads, sandbox traffic, and preview dweb traffic use different scoped paths and
+policies. See [`SECURITY.md`](SECURITY.md) and the
+[`threat model`](docs/security/THREAT-MODEL.md) for the current boundaries and
+known limitations.
-
+## Install
-peerd uses *the browser* as its runtime and its security model. It builds
-on decades of hardened browser platform work (V8 isolates for sandboxing,
-WebCrypto for the vault, WebAuthn passkeys to unlock it, opaque-origin
-iframes, Subresource Integrity) and writes none of its own cryptographic
-or process-isolation code. The agent that holds your keys never operates
-an environment itself. Each browser tab, VM, notebook, and app is driven
-by its own actor: a separate agent loop that holds no key and holds only
-that one environment's tools. On Chrome, each actor runs in its own
-worker heap, a separate block of memory, so the untrusted content it
-reads (page text, command output, file contents) stays inside that actor.
-The actor reaches the model, the network, or the page only by asking the
-service worker, which holds the key and re-checks and gates every request
-before running it. The main agent acts as an orchestrator. It delegates a
-goal to an actor and gets back a summary fenced as untrusted, so raw page
-text and command output never reach the context that holds your keys, and
-a confused or prompt-injected main agent has no tool to touch an
-environment in the first place. Every page action reports back what it
-actually changed on the live page (a navigation or a mutation summary),
-so success is judged from observed effect, not from the model's
-assumption. This isolation is the core of peerd's security model, not an
-add-on. (More at [peerd.ai](https://peerd.ai).) The security model is
-documented and testable: see the formal threat model
-([`docs/security/THREAT-MODEL.md`](docs/security/THREAT-MODEL.md)) and the
-CI-gated red-team suite ([`tests/red-team/`](tests/red-team/), results in
-[`docs/security/RED-TEAM-RESULTS.md`](docs/security/RED-TEAM-RESULTS.md)).
-
-**Status: 0.x, experimental beta.** The initial feature buildout is
-complete and integrated, but the surface is still
-moving: **breaking changes are likely**, storage formats may shift, and
-it drives your browser and holds your API keys, so use it with care.
-There is no "V1" commitment; versions stay in the 0.x range until the
-surface stabilizes.
-
-For what's shipped, read the module code under `extension/peerd-*/`:
-the code is the spec.
+### Chrome from source
-## Install
+1. Clone the repository.
+2. Open `chrome://extensions`.
+3. Enable Developer mode.
+4. Choose **Load unpacked** and select the `extension/` directory.
-**Developer preview:**
-Load the source tree unpacked using the steps below. This is the current
-source-of-truth install path for contributors and early testers.
-
-**Store packages:**
-Chrome Web Store / Firefox Add-ons listings will be linked here once they
-are approved. Store packages omit preview-only dweb pieces and the
-preview/dev advanced automation path.
-
-**Dweb preview (research package):**
-GitHub Releases may include signed preview artifacts. If there is no
-release attached yet, use the source install path below.
-
-The preview package includes the decentralized web (dweb) layer:
-peer-to-peer dwapps between peerd instances. It's intended for
-contributors and early testers, since the dweb protocol is research-grade
-and subject to change. Most users want one of the two store packages
-above. The preview installs alongside the store package as a separate
-extension ("peerd preview") with its own isolated storage; move state
-between them explicitly via **Settings → Export & import**.
-
-Preview package install paths (Firefox is the smoother of the two):
-
-- **Firefox:** click `peerd-preview-firefox.xpi` on the release page.
- It's AMO-signed, installs like any extension, and auto-updates.
-- **Chrome on macOS / Windows (recommended): load the zip unpacked.**
- Chrome hard-disables off-store CRX installs on these platforms
- ("may have been added without your knowledge", enable toggle locked),
- and field testing showed even an `ExtensionInstallAllowlist`
- policy visible in `chrome://policy` does NOT unlock it on an
- unmanaged machine (Chrome wants MDM-grade management). So don't
- fight it: download `peerd-preview-chrome.zip`, unzip it, enable
- Developer mode at `chrome://extensions`, **Load unpacked**, and pick
- the unzipped folder. Caveats: no auto-update (download the new zip
- per release) and the extension ID is machine-specific, not the
- table's CRX ID. This is a Chrome platform restriction on all
- self-hosted extensions, not a peerd choice.
-- **Chrome on Linux (or any policy-managed Chrome):** download
- `peerd-preview-chrome.crx`, enable Developer mode at
- `chrome://extensions`, and drag the file onto the page. Auto-update
- then follows the feed at `peerd.ai/updates/`.
-
-**Extension IDs** (verify which package you're running):
-
-| package | id |
-|---|---|
-| peerd (Chrome store) | verify from the store listing or `chrome://extensions` after install |
-| peerd (Firefox store) | `peerd@peerd.ai` |
-| peerd preview (Chrome) | `lpdkhfeldihoejbbfonnbekpjclkknoc` *(CRX installs only — an unpacked load gets a machine-specific ID)* |
-| peerd preview (Firefox) | `peerd-preview@peerd.ai` |
-
-## Getting started
-
-peerd has **no build step**: you load the `extension/` folder straight
-into Chrome as it is on disk. You need a Chromium-based browser (Chrome,
-Edge, Brave, Arc, …) and a model to talk to: a key from
-[Anthropic](https://console.anthropic.com/),
-[OpenRouter](https://openrouter.ai/keys),
-[OpenAI](https://platform.openai.com/api-keys), or
-[Z.ai](https://z.ai/) (GLM) — or a local
-[Ollama](https://ollama.com/) (keyless, no bill, nothing leaves your
-machine). BYOK: any key lives encrypted in a local vault and is only
-ever sent to that provider.
-
-**1. Get the code**
+Reload the extension from `chrome://extensions` after source changes.
-```
-git clone https://github.com/NotASithLord/peerd.git
-cd peerd
+### Firefox from source
+
+Firefox needs a Firefox-specific package. Do not load the checked-in Chrome
+development manifest.
+
+```sh
+bun run package -- --channel=preview --browser=firefox --no-sign
```
-**2. Load the extension in Chrome**
-
-1. Open `chrome://extensions`.
-2. Turn on **Developer mode** (toggle, top-right).
-3. Click **Load unpacked**.
-4. Select the **`extension/`** folder inside the repo, *not* the repo
- root. (The folder with `manifest.json` in it.)
-
-peerd now appears in your extensions list. Click the puzzle-piece icon
-in the toolbar and **pin** peerd so its icon is always visible.
-
-**3. Open peerd and set up the vault**
-
-Click the peerd toolbar icon and the side panel opens. On first run you
-create a local vault: unlock with **Touch ID / a passkey** (recommended)
-or a recovery passphrase. Keys, chat history, and the audit log are all
-encrypted on this device; nothing leaves your machine except the calls
-to your model provider.
-
-**4. Add your API key(s)**
-
-Open **Settings** (gear icon) → **API keys**. Paste a key for
-**Anthropic** (`sk-ant-…`), **OpenRouter** (`sk-or-…`), **OpenAI**
-(`sk-…`), or **Z.ai** (GLM) — set as many as you like, each stored
-independently. Choose a default under
-*Default model for new chats*, and switch the model per chat from the
-picker above the message box.
-
-**5. Chat**
-
-Back in the chat, type a message. peerd can read and drive your open
-tabs, run shell commands in a sandboxed in-browser Linux VM, build small
-apps, search the web, and more. Turn on **Confirm before actions** in
-Settings if you want to approve each tab/automation step first (off by
-default).
-
-**Updating after a code change.** Hit the **reload icon** on the peerd
-card in `chrome://extensions`. The side panel, offscreen document, and
-any open VM/JS/App tabs reload with it.
-
-**Firefox (temporary).** `about:debugging#/runtime/this-firefox` →
-**Load Temporary Add-on** → pick `extension/manifest.json`. Re-load on
-each edit. Firefox parity is still being polished; Chrome is the
-primary target for now.
-
-**Generated files.** `extension/manifest.json` and
-`extension/shared/channel-config.js` are GENERATED (the checked-in copies
-are the dev defaults: preview channel, dweb on). Don't hand-edit
-them; change `manifests/*.json` or `packaging/default-settings.mjs` and run
-`bun run gen:dev`. CI fails if they drift.
-
-**Why the permissions?** peerd asks for broad host access (``,
-and `debugger` on the preview/dev channels) because driving arbitrary
-tabs and reading the page the agent is acting on is the whole point. Each
-permission, why it's needed, and what the store build strips is spelled
-out in
-[`docs/store/PERMISSION-JUSTIFICATIONS.md`](docs/store/PERMISSION-JUSTIFICATIONS.md),
-and the trust boundaries (BYOK vault, egress allowlist,
-untrusted-content handling, no telemetry) in [`SECURITY.md`](SECURITY.md).
-
-## Project conventions (the short version)
-
-- Plain vanilla JS, ES2024+. No TypeScript, no JSX, no bundler, no `npm`
- inside `extension/`.
-- ES modules only. Strict mode by default.
-- Pure functions and reducers over classes. Classes only where lifecycle is
- real (vault, VM, ports).
-- `safeFetch` / `webFetch` for all outbound HTTP; bare `fetch` is forbidden.
-- Comments explain *why*, not *what*. The codebase is security-sensitive
- and is meant to be read carefully.
-
-The full version of these conventions and the architectural rationale
-lives in `CLAUDE.md` (orientation) and in the module code under
-`extension/peerd-*/`: the code is the spec (vault crypto, dispatcher
-gates, prompt-injection defenses, and the MV3 keepalive trick all live
-in the modules that own them).
-
-## The five modules
-
-The five-letter wordmark *is* the architecture: each colored letter is
-one top-level module, each owning its public API through `index.js`:
-
-| | Module | Role |
-|---|---|---|
-| **`p`** · cyan | [`peerd-provider`](extension/peerd-provider/) | Model adapters — Anthropic, OpenRouter, OpenAI, Z.ai GLM, Ollama (streaming, caching, cost, retries) |
-| **`e`** · red | [`peerd-egress`](extension/peerd-egress/) | Security — the vault, the egress chokepoint, the denylist, the audit log |
-| **`e`** · amber | [`peerd-engine`](extension/peerd-engine/) | Sandboxes — WebVMs, Notebooks, Apps, and the headless worker |
-| **`r`** · green | [`peerd-runtime`](extension/peerd-runtime/) | The orchestrator — agent loop, tools, the `message_actor` delegation channel, actors, sessions, memory, skills, review, goal mode, voice |
-| **`d`** · magenta | [`peerd-distributed`](extension/peerd-distributed/) | The dweb — the peer-to-peer network (preview channel only) |
-
-The brand IS the architecture: cross-module imports go through each
-module's `index.js`, never deep paths; nothing outside
-`peerd-distributed/` imports it at all. Each module's `index.js` is its
-public API and the dependency graph.
-
-## Trust boundaries
-
-peerd's safety is *who is allowed to do what*: small boundaries
-enforced by the browser platform, not by peerd's own crypto. Two
-principles run through all of it: **the agent that holds your keys never
-touches a raw page or runs untrusted code** (the environment-operating
-tools are not even attached to it; they belong to per-environment actor
-actors), and **the agent never gets the final word on correctness:
-every page action reports what it actually changed on the live page, and
-success is judged from that observed effect.**
-
-The orchestrator delegates; an actor does the work. Each tab, VM,
-notebook, and app is owned by one actor that holds only that
-environment's tools, runs without keys, and hands back a fenced summary.
-So isolation between environments is structural, not a convention: even a
-fully prompt-injected main agent cannot reach an environment it was not
-asked to, because it never held the tool.
-
-| Actor | Trusted with | Never |
-|---|---|---|
-| **The vault** (`peerd-egress/vault`) | your API keys + secrets, decrypted only after Touch ID / passkey / passphrase unlock; idle auto-lock | leaving the device — keys go only to the provider you chose |
-| **The orchestrator** (`peerd-runtime/loop`) | the conversation, planning, delegating a goal to an actor via `message_actor` | holding any environment's tools, reading raw page bytes, or running untrusted code directly |
-| **A bound actor** (`peerd-runtime/actor`) | driving ONE tab / VM / notebook / app — it exclusively holds that environment's tools, keyless, in its own worker heap (Chrome) | touching another environment, holding keys, or returning anything to the orchestrator except a `wrapUntrusted`-fenced summary |
-| **A actor** (`peerd-runtime/actor`) | a disposable ephemeral actor the orchestrator spawns to decompose a task — keyless, in its own worker heap (Chrome), holding only a narrowed subset of the orchestrator's tools | escalating past its grant, holding keys, or reaching another agent's heap; every tool call is re-checked service-worker-side and its result returns fenced |
-| **The egress chokepoint** (`safeFetch` / `webFetch`) | every outbound byte — provider allowlist + denylist + SSRF guard | being bypassed; a bare `fetch` is lint-forbidden |
-| **The sandboxes** (WebVM · Notebook · App) | running code — V8 isolates + opaque-origin iframes | extension access; their HTTP routes back through egress |
-| **Web content** | nothing by default | being trusted — all of it is fenced as untrusted input |
-
-The AI proposes and drives; the browser platform (WebCrypto vault,
-WebAuthn unlock, V8 isolates, SRI) and the live DOM decide what actually
-happens. Full detail in [`SECURITY.md`](SECURITY.md) and the
-`peerd-egress` / `peerd-runtime` code.
-
-## Threat model and red-team suite
-
-peerd's security model is documented and testable, not only asserted. The
-formal **threat model**
-([`docs/security/THREAT-MODEL.md`](docs/security/THREAT-MODEL.md)) defines the
-actors, trust boundaries, assets, adversaries, numbered invariants, and known
-residual risks. A **red-team suite** ([`tests/red-team/`](tests/red-team/))
-turns those invariants into runnable probes: each drives a real defense function
-with hostile input and records whether it held. It runs in CI and covers API-key
-exfiltration, induced cross-origin fetches, summarizing secrets into model
-context, SSRF, sandbox escape, hostile peer bundles, and A2A / tool-poisoning
-analogs. The live pass/fail matrix is regenerated into
-[`docs/security/RED-TEAM-RESULTS.md`](docs/security/RED-TEAM-RESULTS.md) by
-`bun run red-team:report`.
-
-Read this honestly: these are runnable security probes for peerd's core
-invariants, not a complete adversarial audit. Most probes run at the unit level
-against the real defense functions; the real Worker and iframe realm escapes are
-verified in the in-browser suite. The threat model is explicit about what is out
-of scope and about the residual risks that remain (for example, the Chrome-only
-heap split, memory poisoning, trusted skill bodies, and origin-blind confirm
-grants). See [`tests/red-team/README.md`](tests/red-team/README.md) for how to
-run and extend the suite.
+Open `about:debugging#/runtime/this-firefox`, choose **Load Temporary Add-on**,
+and select `artifacts/peerd-preview-firefox.xpi`. Temporary add-ons must be
+loaded again after Firefox restarts. Browser and channel transforms are defined
+by the packaging scripts.
-## Documentation
+### Release packages
-The code is the spec. Read `CLAUDE.md` for orientation, each module's
-`index.js` for its public API, and the code itself for the rest.
-`SECURITY.md` and [`docs/security/`](docs/security/) cover the trust boundaries,
-the formal threat model, and the red-team results; `docs/store/` holds the
-store-listing and compliance material.
+See [GitHub Releases](https://github.com/NotASithLord/peerd/releases) for current
+artifacts. Store and preview builds differ. Store builds omit the dweb. Preview
+builds include it and may enable additional automation features. The packaging
+code is the authority for each browser and channel.
-## Repo layout
+## First run
-The five-letter wordmark *is* the architecture (the module code is the
-detail). Each colored letter maps to a top-level module:
+1. Open peerd from the browser toolbar.
+2. Create and unlock the local vault. Passphrase unlock is always available.
+ Passkey unlock depends on WebAuthn PRF support in the browser and device.
+3. Open Settings, then add a provider key or choose a supported local provider.
+4. Select a model and start a chat.
-```
-peerd/
-├── extension/ # the extension itself — load this dir unpacked
-│ ├── manifest.json
-│ ├── peerd-provider/ # p · cyan — model adapters (Anthropic, OpenRouter, OpenAI, Z.ai GLM, Ollama)
-│ ├── peerd-egress/ # e · red — vault, allowlist, denylist, confirm, audit
-│ ├── peerd-engine/ # e · amber — execution-instance registries (WebVM, Notebook, App). Tab runtimes in engine-tabs/-tab/; the headless script worker in offscreen/.
-│ ├── peerd-runtime/ # r · green — agent loop, tools + message_actor delegation, actors + actors, sessions, permissions, composer, skills, memory, review, goal mode, cost, transfer, voice, clock, dom, edit
-│ ├── peerd-distributed/ # d · magenta — the dweb layer between peerd instances (ships ONLY in preview packages)
-│ ├── background/ # chassis: service worker + per-kind tab trackers + clients
-│ ├── offscreen/ # chassis: the actor/actor worker heaps, headless script runs, voice, SW keepalive
-│ ├── sidepanel/ # chassis: chat UI (Mithril)
-│ ├── engine-tabs/ # chassis: the three peerd-engine tab-host pages, grouped
-│ │ ├── vm-tab/ # WebVM tab page (CheerpX + bash + xterm)
-│ │ ├── notebook-tab/ # Notebook tab page (Web Worker + OPFS)
-│ │ └── app-tab/ # App tab page (stored HTML in sandboxed iframe)
-│ ├── eval/ # live end-to-end eval harness (runner.html)
-│ ├── shared/ # base types and utilities (importable everywhere)
-│ ├── tests/ # in-browser test runner — open runner.html
-│ ├── vendor/ # third-party deps, committed as-is (CheerpX, xterm, mithril, Moonshine)
-│ └── permissions/ # permission-grant pages (mic, etc.)
-├── manifests/ # base manifest + per-channel patch documents
-├── packaging/ # Bun packaging scripts: manifest gen, channel artifacts, signing, feeds
-├── tests/ # Bun test suite (bun test ./tests)
-├── update-feeds/ # generated auto-update feeds served at peerd.ai/updates/ (copied to peerd-site to deploy)
-├── docs/ # store/ — store-listing + compliance material
-├── signaling-node/ # dweb rendezvous server shells (share the pure signaling reducer)
-├── v1-deliverables/ # V1 buildout record: INTEGRATION-LOG.md, TEST-PLAN.md
-└── scripts/ # dev helpers (cdp/ headless harness, dev-server.sh, vendor-*)
-```
+Only vault secrets and protected security records are covered by the vault
+encryption boundary. Other local extension state follows the storage rules in
+the security documentation.
-peerd ships from this one tree in **two channels**: `peerd` (Chrome Web
-Store / Firefox Add-ons, no dweb code in the artifact) and
-`peerd preview` (GitHub Releases, dweb enabled, signed,
-auto-updating). Same source, same version, same release; the channel
-only decides whether the dweb module ships. The `packaging/` scripts
-have the whole story.
-
-Cross-module imports go through each module's `index.js`, never deep
-paths. ESLint enforces. Within a module, deep imports are fine.
-
-## Execution instances
-
-`peerd-engine` hosts Sandboxes: four execution kinds (taxonomy in the
-`peerd-engine/` code). Three are
-discrete, persistent browser tabs the user can
-see, focus, and close, grouped under "peerd" in the tab strip and
-surviving browser restarts: the WebVM, the Notebook, and the App. The
-fourth, the headless worker (`script`), runs the Notebook's sealed worker
-offscreen with no tab: ephemeral, for the agent's own quick compute. The
-orchestrator picks the lightest kind that fits the task, bootstraps the
-instance, and then delegates the work to that instance's actor; the
-tool lists below are the surface an actor drives, not the main agent. One
-main-agent tool spans all of them: **`actor_list`** enumerates every
-addressable actor (WebVMs, Notebooks, Apps, open tabs, and API
-integrations), each tagged with its `type` and the handle to pass to
-`message_actor`, so discovery is one call instead of five.
-
-**WebVM**: CheerpX-emulated Debian (sandboxed Linux). Own disk (IDB
-overlay), own bash, own POSIX. ~10s first boot. Use it when you need
-real binaries, a shell, or multi-language stacks.
+## Architecture
-```
-vm_create vm_boot vm_import vm_write_file vm_delete
-```
+The extension has five main modules. Each module exposes its public API through
+its `index.js`.
-HTTP egress from the VM (curl / wget / git clone) is intercepted by
-bash function wrappers that route every request through `peerd-egress`
-before it leaves the browser.
-
-**Notebook**: a sealed Web Worker with its own JS realm and an OPFS file
-tree, in a visible tab. ~hundreds of ms boot. `peerd.egress.fetch` is the
-worker's only network, routed through `peerd-egress` so it's honest. Each
-`js_notebook` run spawns a fresh worker, so in-memory state (`globalThis`,
-`let`/`const`) does NOT carry between runs; persist via
-`peerd.self.writeFile`/`readFile` to the OPFS file tree. The sealed worker
-also runs **compiled wasm32-wasi binaries** via the `peerd:wasi` builtin —
-SQLite over a user's `.sqlite` file, codecs, language runtimes — against an
-in-memory filesystem, with zero ambient capabilities (a wasm module has no
-network path even in principle; it sees only the stdin/files the call
-passes it).
+| Module | Role |
+|---|---|
+| [`peerd-provider`](extension/peerd-provider/) | Model adapters and response formatting |
+| [`peerd-egress`](extension/peerd-egress/) | Vault, network policy, denylist, and audit |
+| [`peerd-engine`](extension/peerd-engine/) | WebVM, Notebook, App, and headless execution |
+| [`peerd-runtime`](extension/peerd-runtime/) | Agent loop, actors, tools, sessions, memory, and permissions |
+| [`peerd-distributed`](extension/peerd-distributed/) | Preview-only peer-to-peer network and dwapps |
-```
-sandbox_create js_notebook script js_write_file js_read_file js_delete
-```
+The extension chassis lives in `background/`, `offscreen/`, `sidepanel/`,
+`engine-tabs/`, `permissions/`, `shared/`, and related support directories.
-**Headless worker** is the same sealed worker as a Notebook, but headless:
-`script` runs it in the offscreen document with no tab, ephemeral scratch
-discarded after. It's the agent's own quick compute and peerd's code mode
-(one script instead of a chain of tool/MCP calls), not a workspace you
-watch. A distinct kind from the Notebook, same substrate.
+## Development
-**App**: a stored HTML document the agent built for the user, rendered
-in a sandboxed iframe (own opaque origin, no extension access).
-Metadata in `chrome.storage.local`; body in IndexedDB; substring
-search across name, tags, and body. `app_update` auto-reloads the open
-tab so iterations show live.
+The shipped extension is vanilla JavaScript with ES modules and no bundling or
+transpilation. Bun is used for tests, generation, packaging, and release checks.
-```
-app_create app_update app_open app_search app_delete
+```sh
+bun install
+bun run gen:dev
+bun test ./tests
+bun scripts/cdp/run-inbrowser-tests.mjs
+bun run typecheck
+bun run lint
+bun run e2e:verify
+bun run preflight
```
-## Tests
+There are three test surfaces:
-Two surfaces, different jobs (see `CLAUDE.md`):
+- Bun tests for pure logic.
+- In-browser tests for extension and browser integration.
+- Live Chrome E2E and visual verification for complete flows.
-**In-browser**: things that need a real browser (DOM, `chrome.*`, IDB,
-side-panel components, the SW). Open
-`chrome-extension:///tests/runner.html` in a tab and refresh to
-re-run. Tiny custom framework covering the vault, the tool dispatcher,
-introspection tools, provider streaming + tool_use, the
-session store, agent loop, denylist matcher, egress, and more. The same
-suite runs headless in CI via the CDP harness
-(`scripts/cdp/run-inbrowser-tests.mjs`, headless Chrome over the
-DevTools Protocol, no MCP).
+For UI changes, run `bun run e2e:verify`, inspect
+`scripts/cdp/artifacts/result.json`, and inspect the generated screenshots.
-**Bun**: pure logic that runs without a browser (registries, the module
-resolver, the Markdown renderer, the OpenAI/OpenRouter format layer).
-Fast and runnable from the terminal:
+Generated files must not be edited by hand. In particular,
+`extension/manifest.json` and `extension/shared/channel-config.js` come from the
+manifest and packaging sources. CI checks them for drift.
-```
-bun install # once — pulls the dev-only test deps (e.g. fake-indexeddb)
-bun test ./tests
-```
+Read [`CONTRIBUTING.md`](CONTRIBUTING.md) before changing code.
+
+## Documentation
+
+- [`CLAUDE.md`](CLAUDE.md): project structure, conventions, and current posture
+- [`SECURITY.md`](SECURITY.md): security policy and reporting
+- [`docs/security/THREAT-MODEL.md`](docs/security/THREAT-MODEL.md): trust boundaries and residual risks
+- [`docs/security/RED-TEAM-RESULTS.md`](docs/security/RED-TEAM-RESULTS.md): red-team coverage
+- [`docs/store/`](docs/store/): store packaging, permissions, privacy, and reviewer notes
+- [`scripts/cdp/GALLERY.md`](scripts/cdp/GALLERY.md): E2E and visual states
+
+Design records under `docs/design/` are point-in-time proposals. They explain
+past decisions and planned work. They do not override current code.
+
+## Dependencies and license
+
+Runtime dependencies are vendored under `extension/vendor/`. Their source,
+version, license, and integrity records live in the adjacent `SOURCE.txt` files
+and [`extension/vendor/vendor.lock.json`](extension/vendor/vendor.lock.json).
-(Bun is only needed for these terminal tests and for re-vendoring
-third-party deps; running the extension itself needs no toolchain at
-all.)
-
-**Types: JSDoc + `// @ts-check`, mandatory for browser files.** The
-extension is no-build vanilla JS, so types come from JSDoc checked by a
-`// @ts-check` directive, not a `.ts` toolchain. `bun run typecheck`
-(strict `tsc`) checks every annotated file; `bun run check:tscheck` is a
-CI gate on coverage. **Every browser file (`extension/**/*.js`) now
-carries `// @ts-check` (100%), and it is required on new ones:** add the
-directive and make the file type-clean (`bun run typecheck`), or CI
-fails. (The Bun tests under `tests/` are real TypeScript, since Bun runs
-`.ts` directly; only code the browser loads is JSDoc-on-JS.)
-
-## Open-source components
-
-peerd stands on a lot of excellent open-source work. The MV3 CSP
-forbids remote script execution (`script-src 'self' 'wasm-unsafe-eval'`),
-so every third-party runtime dependency is **vendored**: committed
-pre-built under `extension/vendor/`, pinned to a version, and SHA-verified
-by a `scripts/vendor-*.sh` (or `.ts`) re-vendor step. Each directory
-carries a `SOURCE.txt` recording the upstream, the pinned version, the
-hash, and the update procedure. A fresh clone runs with **no build and no
-network fetch** for code. You only touch the vendor scripts when *updating*
-a dependency, and the regenerated bytes are checked in; peerd's own code is
-plain ES modules loaded directly, never bundled.
-
-Thank you to the maintainers of all of these projects.
-
-### Vendored runtime dependencies
-
-| Component | Version | License | Used for |
-|---|---|---|---|
-| [CheerpX](https://leaningtech.com/cheerpx/) ([docs](https://cheerpx.io/docs)) | 1.2.8 | Proprietary — license your responsibility¹ | x86 Linux in WebAssembly — the WebVM sandbox runtime (`peerd-engine`, `engine-tabs/vm-tab/`) |
-| [xterm.js](https://xtermjs.org/) (`@xterm/xterm` + `@xterm/addon-fit`) | 5.5.0 / 0.10.0 | MIT | In-browser terminal emulator rendering the WebVM's PTY (`engine-tabs/vm-tab/`) |
-| [Mithril.js](https://mithril.js.org/) | 2.3.8 | MIT | UI framework for the side panel and Apps |
-| [CodeMirror 6](https://codemirror.net/) (`@codemirror/*`) | 6.x | MIT | Code editor in the App tab (`peerd-engine/editor.js`) |
-| [Moonshine](https://github.com/moonshine-ai/moonshine) (`@moonshine-ai/moonshine-js`) | 0.1.29 | MIT | Local, in-browser speech-to-text for voice input (`peerd-runtime/voice/`) |
-| [ONNX Runtime Web](https://github.com/microsoft/onnxruntime) (`onnxruntime-web`) | 1.22.0 | MIT | WASM/WebGPU inference backend Moonshine runs on (`vendor/onnxruntime-web/`) |
-| [Silero VAD](https://github.com/snakers4/silero-vad) (`@ricky0123/vad-web`) | 0.0.24 | MIT | Voice-activity detection / speech endpointing for Moonshine (`vendor/vad-web/`) |
-| [hash-wasm](https://github.com/Daninet/hash-wasm) (Argon2 bundle) | 4.12.0 | MIT | Argon2id KDF deriving the vault's key-encryption key (`peerd-egress/vault/`) |
-| [browser_wasi_shim](https://github.com/bjorn3/browser_wasi_shim) (`@bjorn3/browser_wasi_shim`) | 0.4.2 | MIT OR Apache-2.0 | WASI preview1 syscall layer behind the `peerd:wasi` builtin — runs wasm32-wasi binaries in the sealed worker (`engine-tabs/notebook-tab/notebook-wasi.js`) |
-| [webextension-polyfill](https://github.com/mozilla/webextension-polyfill) | 0.12.0 | MPL-2.0 | One promise-based `browser.*` API across Chrome and Firefox |
-| [Transformers.js](https://github.com/huggingface/transformers.js) (`@huggingface/transformers`) | 4.2.0 | Apache-2.0 | WebGPU runtime for the on-device local-inference runner (`offscreen/local-model.js`)² |
-
-¹ **CheerpX is proprietary, closed-source software** — the one vendored
-dependency here that is *not* under an open-source license, and the only
-one with a paid tier. Per Leaning Technologies'
-[EULA](https://github.com/leaningtech/cheerpx-meta/blob/main/LICENSE.txt)
-and [licensing terms](https://cheerpx.io/docs/licensing), the free
-*Community* tier covers **individuals and one-person companies for any
-purpose** (including revenue-generating, public-facing products);
-**organizations of more than one person** may use it for free only for
-evaluation and testing — production use requires a paid **Commercial
-License** (contact-sales; no public price list). Separately, **bundling
-and redistributing the CheerpX runtime — which peerd does by vendoring it
-into `extension/vendor/cheerpx/` — and self-hosting it off Leaning's CDN
-is gated**: their terms state that downloading a CheerpX build to host it
-elsewhere is not permitted without a commercial license. **peerd ships
-the runtime as a convenience and makes no licensing grant. If you run,
-fork, distribute, or build a commercial offering on peerd, obtaining
-whatever CheerpX license your use requires is your responsibility, not
-peerd's** — contact Leaning Technologies before any commercial launch.
-² Local in-browser WebGPU inference is **early but proven**: one model
-(Gemma-4-E2B) ships behind an opt-in download, WebGPU-only; broader model
-support is staged. The runner lives in `offscreen/local-model.js`.
-
-### Models and data fetched at runtime
-
-These are **data, not script**, so they're fetched lazily on first use
-and cached locally (IndexedDB / OPFS) rather than shipped in-package, but
-they're open assets worth crediting:
-
-- **CheerpX Debian image**: CheerpX's stock Debian `ext2` disk,
- streamed lazily over WebSocket from `disks.webvm.io` (the only relaxed
- `connect-src` origin). The disk *content* is unmodified Debian under
- Debian's own (free) licensing, a separate concern from the proprietary
- CheerpX runtime that streams it (note ¹ above).
-- **Moonshine STT models**: [`UsefulSensors/moonshine`](https://huggingface.co/UsefulSensors/moonshine)
- ONNX weights (the `base` variant, ~250 MB), SRI-pinned to specific
- Hugging Face commits (`peerd-runtime/voice/model-store.js`).
-- **Silero VAD model**: `silero_vad` ONNX weights, served same-origin
- from the vendored `vad-web` package.
-- **Gemma on-device model**: [`onnx-community/gemma-4-E2B-it-ONNX`](https://huggingface.co/onnx-community/gemma-4-E2B-it-ONNX)
- weights (~1.3 GB), the model behind the early on-device WebGPU runner.
- It's Google's **Gemma** converted to ONNX by the onnx-community /
- **Xenova** ([Transformers.js](https://github.com/huggingface/transformers.js))
- ecosystem, downloaded opt-in and run in the offscreen doc
- (`offscreen/local-model.js`). The Gemma weights are under Google's
- [Gemma Terms of Use](https://ai.google.dev/gemma/terms), a custom
- license with use restrictions (**not** a standard OSI-approved one), so
- they're a credited runtime download, never bundled.
-
-The brand mark on monochrome, the spinner cadence, and the rest of peerd's
-own design are first-party. Everything above is third-party and credited to
-its upstream.
-
-## License
-
-Apache 2.0. See [`LICENSE`](LICENSE).
-
-## Warranty
-
-peerd is provided **"as is", without warranty of any kind**, express or
-implied — including, without limitation, the implied warranties of
-merchantability, fitness for a particular purpose, title, and
-non-infringement. The entire risk as to the quality and performance of
-the software is with you.
-
-In no event shall the authors or copyright holders be liable for any
-claim, damages, or other liability — whether in contract, tort, or
-otherwise — arising from, out of, or in connection with the software or
-its use.
-
-This is early, actively-developed software that drives your browser,
-executes code, and handles your API keys and other secrets on your
-behalf. **Use it at your own risk.** The controlling terms are the
-Disclaimer of Warranty and Limitation of Liability in
-[`LICENSE`](LICENSE) (Apache 2.0, sections 7 and 8).
+peerd is licensed under the [Apache License 2.0](LICENSE). Vendored components
+retain their own licenses. CheerpX is a proprietary runtime provided by Leaning
+Technologies and is not covered by peerd's Apache license.
diff --git a/SECURITY.md b/SECURITY.md
index eace70bb..ff09d929 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -13,7 +13,7 @@ We take security seriously and welcome good-faith research.
**Do not open a public issue for security problems.**
Report privately via **GitHub Private Vulnerability Reporting**:
-the repository's **Security** tab → **Report a vulnerability**
+open the repository's **Security** tab and choose **Report a vulnerability**
(`https://github.com/NotASithLord/peerd/security/advisories/new`). This
opens a private advisory only the maintainers can see.
@@ -45,53 +45,51 @@ security claims can be re-checked against the code rather than taken on faith.
Understanding the boundaries helps you scope a report:
-- **BYOK, no backend, no telemetry.** Your API key is held locally in an
- encrypted **vault** (`peerd-egress/vault/`, Argon2id / WebAuthn-PRF).
- Nothing is sent anywhere except your chosen model endpoint.
-- **Egress chokepoint.** All network calls route through
- `peerd-egress/fetch/`: `safeFetch` (a hardcoded provider allowlist for
- model calls) and `webFetch` (SSRF guard + a denylist of sensitive
- origins, no redirects). There is no other egress path. The denylist also
- has a network-level backstop (`peerd-egress/denylist/dnr-rules.js` +
- `background/denylist-net-guard.js`): a `declarativeNetRequest` rule that
- blocks denylisted domains inside the tabs peerd is currently driving —
- and only those, never your own browsing — so a page that navigates
- ITSELF onto a sensitive site is refused below the page, where no
- decision-time gate can see it.
+- **BYOK, no peerd backend, no telemetry.** Provider secrets are held locally
+ in an encrypted vault (`peerd-egress/vault/`). Model requests go directly to
+ the selected provider.
+- **Scoped network paths.** Provider calls use a provider allowlist. Open-web
+ reads use SSRF and denylist checks. WebVM and Notebook network operations use
+ host-mediated routes. Browser navigation is checked separately. App tabs
+ have no ambient network and use a tab-scoped network rule as a backstop.
+ Preview dweb builds also use signaling and peer-to-peer WebRTC. The current
+ implementations live in `peerd-egress/`, `peerd-engine/`,
+ `peerd-distributed/`, and the service-worker wiring.
- **Untrusted-content boundary (the heap split).** The main agent never
sees raw page content: page/DOM work is delegated to a per-tab **web
- actor** — a separate agent loop that, on Chrome, runs in its OWN Worker
+ actor**, a separate agent loop that, on Chrome, runs in its own Worker
heap, holds no key, no `chrome.*`, and no egress, and reaches the model,
the network, or the page only by asking the service worker, which holds
the key and re-checks every request. Untrusted content (page text,
command output, file contents) stays inside that heap and returns to the
orchestrator only as a `wrapUntrusted`-fenced summary. Actors run the
- same way — keyless, in their own heap, with a narrowed toolset. This is
- the core prompt-injection / "lethal trifecta" defense: a memory boundary,
- not a prompt one. (Firefox lacks the offscreen API, so there the actor
- runs keyless in the shared loop until it lands.)
+ same way: keyless, in their own heap, with a narrowed toolset. This is
+ the main prompt-injection defense. It is a memory boundary, not a prompt
+ convention. Firefox lacks the offscreen API and has no equivalent heap
+ separation. Spawned children remain keyless there, but bound actors run in
+ the service worker with live model credentials. This is a known residual risk.
- **Policy-gated tool dispatch** with a local, append-only audit log. The
current policy checks and hooks live in `peerd-runtime/tools/`.
- **What the model reads is what you could have seen.** Bytes that are
- invisible to a person but legible to a model — zero-width runs, bidi
- overrides, Unicode tag characters, HTML comments — are stripped before
+ invisible to a person but legible to a model, including zero-width runs, bidi
+ overrides, Unicode tag characters, and HTML comments, are stripped before
page text reaches the model, at both read boundaries and inside the
untrusted-content fence itself. Text in every script survives, including
the zero-width non-joiner Persian, Urdu and the Indic scripts need
(`peerd-runtime/dom/cdr.js`).
-- **Acting as you, on a page strangers wrote, takes you.** On sites where
- third parties author the content — issue trackers, shared docs, social
- feeds — an authenticated write asks you first, **even if you turned
+- **Authenticated writes to user-generated content require confirmation.** On sites where
+ third parties author the content, such as issue trackers, shared docs, and social
+ feeds, an authenticated write asks you first, **even if you turned
confirmations off**. Reading is exempt; so is navigating away
(`peerd-runtime/actor/ugc-registry.js`).
-- **An exfil-shaped navigation is blocked.** A tab tool sending a long,
+- **Suspicious cross-origin navigation is blocked.** A tab tool sending a long,
scraped-looking blob to another origin in the URL is refused. Best
effort: it catches the obvious shape, not everything, and it deliberately
does not scan query strings, because that is where legitimate login
tokens live (`peerd-runtime/tools/egress-heuristics.js`).
-- **A helper that browses the web can't walk into your accounts.** Every web
- helper is either *roaming* — it browses freely and holds no authority — or
+- **Web helpers do not enter known account sites without authority.** Every web
+ helper is either *roaming*, so it browses freely and holds no authority, or
*bound* to exactly one site it may not leave. A roaming helper that reaches a
site you have an account on stops instead of continuing, and peerd checks
where the tab actually ENDED UP rather than where something asked it to go, so
@@ -100,20 +98,21 @@ Understanding the boundaries helps you scope a report:
(`peerd-runtime/actor/landing-rule.js`).
Two things this list does **not** claim. Knowing which sites you have an account
-on is a list, and lists are incomplete — the first visit to a site peerd has
+on is a list, and lists are incomplete. The first visit to a site peerd has
never seen a login page for is unprotected (R15). And the strict structural
reply format for web actors ships **off** by default (R14). The threat model
states both plainly rather than counting them as defenses.
-- **Sandboxed execution.** WebVM (CheerpX, network only via the egress
- wrappers), JS Sandbox (realm-sealed Web Worker), App (opaque-origin
- sandboxed iframe).
+
+- **Sandboxed execution.** WebVM uses CheerpX. Notebook and headless script
+ execution use sealed workers. Apps use opaque-origin sandboxed iframes and
+ currently run only on Chrome.
## In scope
- Exfiltration of the vault / API key / conversation off-device.
- Prompt injection that bypasses the actor boundary (the keyless
per-environment heap) and reaches the orchestrator's tools or memory.
-- Sandbox escape (WebVM / JS Sandbox / App iframe) reaching the host,
+- Sandbox escape (WebVM, Notebook, headless script, or App iframe) reaching the host,
other origins, or the extension's privileged contexts.
- Denylist / egress-chokepoint / SSRF-guard bypass.
- Vault / crypto weaknesses; auth-bypass of the lock.
diff --git a/docs/store/OPEN-DECISIONS.md b/docs/store/OPEN-DECISIONS.md
index 78b0a918..a56402c8 100644
--- a/docs/store/OPEN-DECISIONS.md
+++ b/docs/store/OPEN-DECISIONS.md
@@ -1,228 +1,41 @@
-# Store submission — open decisions (blockers on purpose)
+# Store decisions
-Everything mechanical is done (manifest, icons, package script, privacy
-policy, justifications, reviewer notes). These four are judgment calls
-that change review risk materially; decide them before submitting.
+This file records the current store posture. Packaging code and CI are the
+authority for shipped behavior.
----
+## Current decisions
-## STATUS — decisions taken (2026-06-11)
+### Advanced automation
-- **#1 `debugger` → resolved: option C ADOPTED (2026-06-13).** The
- initial store Chrome package ships WITHOUT `debugger`: the
- chrome.scripting / DOM-walk path is its default automation surface
- (same posture as Firefox), so initial Chrome Web Store approval isn't
- gated on the highest-risk permission. CDP stays the default in the
- preview/dev channels (where `debugger` ships, required at install) and
- is re-added to a store UPDATE after the first approval — a one-line
- flip (`STORE_STRIPPED_PERMISSIONS` in `packaging/gen-manifest.ts`). This
- SUPERSEDES the earlier "option B, optional_permissions" note (option B
- is impossible — Chrome forbids `debugger` as optional; see §1) and the
- interim option-A "ship required + in-app switch" posture. History kept
- below for the record.
-- **#2 comment honesty pass → DONE** (commit `c398703`): "bypass CSP /
- bypass detection / banking" framing replaced with accurate
- Trusted-Types / trusted-input wording across debugger-pool.js,
- page-exec.js, type.js, click.js, CLAUDE.md.
-- **#3 skills remote install → resolved: option B, IMPLEMENTED.**
- `REMOTE_SKILL_INSTALL = false` in `extension/shared/flags.js`; SW
- refuses git/manifest install, UI hides the URL tabs, local paste
- stays. Commit `b9539ab`.
-- **#4 single-purpose framing → NOTED, owner-directed.** Listing copy
- already uses the one-assistant framing. The "peer daemon" P2P story is
- intentional roadmap, not in this package — owner is building on the
- distribution shim toward release; keep it out of the dashboard copy
- until it ships.
+The initial Chrome store package omits the `debugger` permission. Store Chrome
+and Firefox use the scripting-based page path. Preview and development Chrome
+builds may include the debugger-based path. The browser and channel transforms
+live in `packaging/gen-manifest.ts` and are checked by the store posture tests.
-The original analysis for each is preserved below.
+Chrome does not allow `debugger` as an optional permission. Adding it to a
+future store update requires a separate review decision.
----
+### Remote skill installation
-## 1. `debugger`: keep, gate, or drop for V1 — RESOLVED (B impossible → A interim → C ADOPTED: store scripting-first, CDP in preview/dev)
+The store package keeps remote skill installation disabled. Local skill text is
+supported. The live feature gate and service-worker checks are the authority.
-**Correction (2026-06-12).** Option B is IMPOSSIBLE in Chrome: the
-`debugger` permission cannot be listed under `optional_permissions` —
-Chrome warns "Permission 'debugger' cannot be listed as optional. This
-permission will be omitted." and strips it, which silently killed the
-whole CDP path (snapshot refs, page_exec, runner pre-seeding) on every
-install. Implemented outcome: `debugger` is a REQUIRED install-time
-permission (option A's posture) and the user-facing control became the
-`advancedAutomationEnabled` SETTING (default on; Settings → Advanced;
-the SW only wires the CDP pool into tool contexts while it's on). The
-submission defense is option A's: denylist default-ON + audit log +
-Chrome's banner, plus the in-product off switch. The original analysis
-below is preserved for the record.
+### Dweb
-**ADOPTED as the initial-submission plan (owner directive,
-2026-06-13).** Option C is no longer a contingency — it's how the store
-build ships from the first submission. It's a manifest knob, not a
-rewrite:
+The store package omits `peerd-distributed`. The preview package includes it.
+Packaging and boundary checks verify that store artifacts contain no dweb code.
-- The DOM-walk pseudo-snapshot is the UNIVERSAL no-CDP fallback, keyed
- on **CDP-pool availability, never browser detection**. Same path
- serves all three triggers: Firefox (no API), Chrome with
- `advancedAutomationEnabled` off (user choice), and the store package
- with the permission stripped. Code tolerates `chrome.debugger` being
- *absent*, not just the setting being off — different failure modes,
- both handled (`debuggerApiAvailable()` gates on the namespace).
-- The build strips `debugger` per-channel for Firefox AND, now, for the
- store channel — `STORE_STRIPPED_PERMISSIONS` in `packaging/gen-manifest.ts`
- (asserted by `tests/store/store-posture.test.ts` and
- `packaging/verify-store-artifact.ts`). Re-adding CDP to a store update
- post-approval = delete `'debugger'` from that one list.
-- Capabilities that honestly die with the permission in store/chrome —
- `page_exec` on Trusted-Types pages, `page_keys` (trusted/`isTrusted`
- input) — stay available in the preview channel, exactly like the
- Firefox posture. `read_state` does NOT die: it gained a
- chrome.scripting `world:'MAIN'` selector fallback
- (`peerd-runtime/dom/framework-state.js`), so framework introspection
- works without CDP given a CSS selector.
-- Cost: hardened/bot-protected SPAs degrade in the store package;
- ordinary sites keep working through `read_page` + selector tools +
- the pseudo-snapshot (runner pre-seeding included), and the runner is
- told its channel up front so it doesn't reach for CDP-only tools.
+### Anti-bot behavior
-The single highest-risk item. Options, in increasing concession order:
+Challenge handling and site automation limits remain open product questions.
+See [`ANTI-BOT-POSTURE.md`](ANTI-BOT-POSTURE.md). peerd does not use fingerprint
+spoofing, proxies, CAPTCHA solvers, or other challenge bypasses.
-**A. Keep, defend (current state).**
-Submit with the justification in PERMISSION-JUSTIFICATIONS.md, lean on
-default-ON denylist + audit log + Chrome's banner. Risk: reviewers
-treat `debugger` as reserved for dev-tools extensions; expect at least
-one rejection round and a human re-review. Strongest version of the
-defense requires the demo video to *show* the banner and the denylist
-refusing a bank.
+## Submission checks
-**B. Make it an optional permission.**
-Move `debugger` to `optional_permissions`; `page_exec` requests it on
-first use with an explainer ("this site blocks normal automation —
-grant advanced automation?"). Core install reviews as a normal-risk
-extension; the heavy permission becomes opt-in. Cost: one extra prompt
-the first time a user hits Gmail/Notion; some review friction remains
-but materially less. ~Half a day of work (request flow + degraded
-fallback when denied).
+Before a store submission:
-**C. Drop for V1.0, ship as V1.0.1.**
-Remove `debugger` + `page_exec`; `page_eval` still covers non-hardened
-sites. Fastest possible approval, weakest product (fails on Gmail,
-Notion, Slack). Re-adding later triggers a new review with a
-permission-increase warning to existing users — you pay the review cost
-eventually anyway.
-
-### Note: the grant is global, not per-origin (deferred enhancement)
-
-Worth knowing how the capability actually scopes: `debugger` is an
-**API** permission, which Chrome treats as all-or-nothing — it cannot
-be origin-scoped (only *host* permissions can carry `origins`). peerd
-holds it browser-wide from install, bounded by the `` host
-permission + the default-ON denylist + the advancedAutomationEnabled
-setting.
-
-What IS per-page is the **attachment**: the pool attaches `chrome.debugger`
-to a tab lazily, only when a CDP tool runs on it (`debugger-pool.js`
-`attach(tabId)`), so the "DevTools is debugging this tab" banner and the
-actual debugging only ever touch the specific tab being driven. Idle tabs
-are never attached. So the *behavior* is effectively page-scoped even
-though the *grant* is global.
-
-**Deferred — do NOT build unless a store reviewer challenges the global
-grant:** a true per-origin gate (an app-level allowlist of origins where
-CDP automation is permitted, checked in the pool before `attach()`, with
-a per-site "allow advanced automation here?" prompt). Chrome doesn't give
-this for free; it's a policy layer on top. The natural hook is the top of
-`debugger-pool.js` `attach(tabId)`. Holding off — the global-grant +
-lazy-per-tab-attach + denylist posture is defensible as-is, and the
-prompt is already cited in PERMISSION-JUSTIFICATIONS / REVIEWER-NOTES.
-
-Recommendation: **B**. It converts "why does an AI chat extension need
-the most dangerous permission" into "the user explicitly grants an
-advanced capability," which is the framing reviewers accept.
-
-## 2. Comment honesty pass (do alongside whichever #1 you pick)
-
-Not hiding anything — replacing adversarial framing with accurate
-framing. The shipped files a reviewer will open:
-
-- `background/debugger-pool.js:21` — "bypass page CSP including
- Trusted Types". Proposed: "Trusted-Types pages reject injected
- script elements, so agent automation runs via CDP's sanctioned
- `allowUnsafeEvalBlockedByCSP` evaluation path instead. Page CSP
- governs page-injected script; CDP evaluation is the
- user-privileged channel for user-directed automation."
-- `peerd-runtime/tools/defs/page-exec.js:19-20` — "Mature peerd use
- will run in inactive/offscreen tabs where the banner is invisible
- anyway" reads as *wanting the debugger banner hidden from the user*.
- This sentence is worse than the bypass language. Delete it, or
- replace with: "The banner is a feature: it's the user-visible signal
- that automation is active."
-- `CLAUDE.md` "What's shipped" — drop the word "banking" from the
- Trusted-Types example list (it's an orientation doc, not shipped,
- but reviewers can and do read public repos).
-
-## 3. Skills remote install: ship, gate, or hide for V1
-
-Reality check: skills are NOT a stub (CLAUDE.md is stale). Full
-implementation: parse/store/registry/install + a sidepanel management
-UI with local-paste, git-URL, and manifest-URL install paths.
-
-**A. Ship as-is** with the RHC defense already drafted in
-REVIEWER-NOTES.md §4 (markdown instructions ≠ code; user-initiated;
-egress-gated; audited). Defensible, but it's the textbook RHC probe and
-invites a slow review.
-
-**B. V1 = local paste only.** Hide the git/manifest tabs in
-`sidepanel/components/skills-view.js` behind a flag; keep install.js
-shipped but unreachable from UI. Small change, kills the sharpest RHC
-question, feature returns in V1.x with its own review. Reviewer notes
-then truthfully say "skills are user-pasted text".
-
-**C. Exclude skills entirely from the package.** Requires cutting the
-re-exports in `peerd-runtime/index.js` + the sidepanel view + SW
-handlers. Most invasive, least review risk. Probably overkill given B
-exists.
-
-Recommendation: **B**.
-
-## 4. Single-purpose listing framing
-
-LISTING.md is already written to the safe framing: one assistant,
-capabilities not modules. Decide whether marketing copy elsewhere
-(peerd.ai, README) stays "five modules / harness / sovereign" — that's
-fine for the site, but never paste it into the dashboard. The one
-remaining naming risk: the store name "peerd" + site tagline "peer
-daemon" suggests P2P networking, which V1 does not ship
-(peerd-distributed is excluded from the package). If a reviewer asks
-"where's the peer part," the answer is "future roadmap, not in this
-package."
-
----
-
-## 5. Anti-bot detection posture — OPEN (see `ANTI-BOT-POSTURE.md`)
-
-A later-arising judgment call, not one of the original four. A field
-report surfaced that the web actor tripped a site's velocity/CAPTCHA
-wall by acting faster than a human. We shipped a first mitigation
-(human-cadence action pacing) and researched the rest. The full option
-set — challenge hand-back, co-pilot/assist-only on hostile sites,
-CDP-footprint minimization, API-first routing, site-posture tiers, and
-the techniques we deliberately REJECT (fingerprint spoofing, proxies,
-CAPTCHA-solvers) — plus the load-bearing reframe (detection ≠ permission;
-some sites' ToS forbid extension automation outright, enforceably) lives
-in `ANTI-BOT-POSTURE.md` as a spec with per-option `DECISION:` lines to
-fill in. Decide alongside the store-submission posture (§1): it bears on
-the store-vs-preview automation-surface trade-off.
-
----
-
-## Already decided (for the record)
-
-- CSP `connect-src` keeps `https:`: the agent fetches user-chosen pages
- from the SW (extension_pages CSP governs the SW), and the voice-model
- download follows HF's redirect to rotating CDN hosts — a fixed host
- list would break both. Blanket `wss:` and `http://localhost:11434`
- removed; only `wss://disks.webvm.io` remains.
-- `tests/runner.html` WAR exposure removed; dev loop unaffected (direct
- chrome-extension:// navigation doesn't need WAR).
-- `peerd-distributed/`, `tests/`, `eval/`, in-tree dev notes excluded
- from the uploaded package by the store channel of `packaging/package.ts`
- (verified by `bun run verify:store`; the older `scripts/package.sh`
- was superseded by the dual-channel build and removed).
+- confirm the current package contents with the packaging and verification commands
+- review `PERMISSION-JUSTIFICATIONS.md`, `PRIVACY.md`, and `REVIEWER-NOTES.md`
+- replace any submission placeholders, including the reviewer demo URL
+- verify the public privacy policy URL in the store dashboards
diff --git a/docs/store/PRIVACY.md b/docs/store/PRIVACY.md
index 0c90d886..102a9b76 100644
--- a/docs/store/PRIVACY.md
+++ b/docs/store/PRIVACY.md
@@ -1,90 +1,91 @@
# peerd Privacy Policy
**Effective date:** July 12, 2026
-**Applies to:** the peerd browser extension ("peerd" on the Chrome Web
-Store and Firefox Add-ons)
-
-> **TODO before publishing:** publish this page at
-> `https://peerd.ai/privacy` and paste that URL into the store
-> dashboards.
-
-## The short version
-
-peerd collects nothing. There are no servers, no accounts, no
-analytics, no telemetry, and no crash reporting. The developer never
-receives, stores, or has access to any of your data. Everything peerd
-knows lives in your browser on your device, and the only network
-requests it makes are the ones your own use of it causes: calls to the
-AI provider you configured with your own API key, requests to the
-websites you ask the assistant to work on, and one-time downloads of
-public runtime assets.
-
-## What stays on your device
-
-All of peerd's data is stored in the extension's local browser storage
-and never reaches the developer:
-
-- **Your API key(s)** — kept in an encrypted vault, unlocked with a
- passphrase or your device's biometrics (Touch ID / Windows Hello).
-- **Your conversations, settings, and history.**
-- **The audit log** — a local record of every network request the
- assistant made, allowed or denied, so you can see exactly what it
- did. It is never transmitted.
-- **Sandbox files and caches** — Linux VM disks, notebook files, and
- the downloaded voice model.
-
-Voice input is transcribed locally on your device by default; audio is
-never stored.
-
-## What leaves your device
-
-1. **Calls to your AI provider.** Your messages — including page
- content the assistant reads for a task you gave it — are sent to
- the provider you configured, authenticated with your own key. peerd
- only ever sends your key to the provider you chose. That provider's
- privacy policy governs its handling of those requests.
-2. **Websites the assistant works on.** When you give the assistant a
- task, it requests the pages needed to carry it out, the same way
- your browser does when you visit them. A denylist (on by default)
- blocks sensitive sites such as banks, health portals, and government
- services; a private-network block prevents access to localhost and
- LAN addresses; and every request, allowed or denied, is written to
- the local audit log.
-3. **One-time public asset downloads.** Enabling local voice downloads
- a public speech-recognition model from huggingface.co
- (integrity-verified, then cached). Booting the Linux VM streams a
- public Debian disk image from disks.webvm.io. These are static
- public files; the requests carry no personal data or credentials.
-4. **Optional browser speech fallback.** If local transcription is
- unavailable and you use the browser's built-in speech recognition
- instead, your browser (not peerd) may send audio to its vendor's
- speech service. peerd's default is the local transcriber.
-
-Nothing else leaves your device. peerd has no backend and phones home
-to no one. The usage costs shown in the UI are computed locally from
-your own API responses.
-
-## What we never do
-
-- Collect, receive, or store your data — there is nowhere for it to go.
-- Analytics, tracking, telemetry, crash reporting, or ads.
-- Sell, share, or monetize data in any way.
-
-## Deleting your data
-
-Everything is retained locally until you delete it. You can clear
-conversations, caches, VM disks, and the vault from peerd's settings.
-Uninstalling the extension removes all of its stored data.
+
+**Applies to:** all peerd browser extension distribution channels
+
+The public policy URL is `https://peerd.ai/privacy`. Confirm that the published
+copy matches this file before each store submission.
+
+## Summary
+
+peerd has no account system, analytics, telemetry, or crash reporting. peerd
+does not operate a hosted agent backend. Store builds do not send extension data
+to a peerd service. Preview signaling servers process the rendezvous metadata
+needed to connect peers and may retain normal service logs.
+
+Model requests go directly from the extension to the provider selected by the
+user. Other network requests occur only when needed for a user task, a runtime
+asset, an optional browser service, or an enabled preview feature.
+
+## Data stored on the device
+
+peerd stores local extension data such as:
+
+- provider secrets in an encrypted vault
+- conversations, settings, and history
+- the local audit log
+- sandbox files, caches, and downloaded runtime assets
+
+Vault encryption does not apply to every item in extension storage. The
+security documentation defines the current storage boundaries.
+
+Voice input is transcribed locally by default. peerd does not retain recorded
+audio.
+
+## Data sent from the device
+
+### Model providers
+
+Messages and task context are sent to the model provider selected by the user.
+Requests use the user's provider key when required. The provider's privacy
+policy applies to that data.
+
+### Websites and user-directed network requests
+
+peerd can request websites and APIs needed for a task. Network policy blocks
+private network targets and configured sensitive sites on supported paths. The
+local audit log records allowed and denied requests.
+
+### Runtime assets
+
+Some features download public runtime assets, such as a speech model or a
+WebVM disk image. Integrity and source controls are defined in the code and
+vendored dependency records.
+
+### Browser speech services
+
+If local transcription is unavailable and the user chooses the browser speech
+fallback, the browser may send audio to its own speech service. The browser
+vendor controls that service.
+
+### Preview dweb
+
+Preview builds can use signaling servers for peer discovery and WebRTC for
+peer-to-peer traffic when the dweb is enabled. Store builds omit the dweb.
+
+## What peerd does not do
+
+- sell user data
+- use extension data for advertising
+- run analytics or telemetry
+- send crash reports
+- provide extension data to a peerd-hosted agent service
+
+## Data deletion
+
+Local data remains until the user deletes it. peerd settings provide controls
+for local records and caches. Uninstalling the extension asks the browser to
+remove extension storage, subject to browser behavior.
## Children
peerd is not directed at children under 13.
-## Changes to this policy
+## Changes
-Changes will be published at this URL with an updated effective date.
-Because peerd has no server, a change to this policy can never
-retroactively grant access to your data — there is nothing to access.
+Policy changes will be published at the public policy URL with an updated
+effective date.
## Contact
diff --git a/docs/store/REVIEWER-NOTES.md b/docs/store/REVIEWER-NOTES.md
index c5344dfa..5bf53881 100644
--- a/docs/store/REVIEWER-NOTES.md
+++ b/docs/store/REVIEWER-NOTES.md
@@ -1,7 +1,7 @@
-# Chrome Web Store — reviewer notes (paste into "Notes for reviewer")
+# Chrome Web Store reviewer notes
-One placeholder to fill before submitting, marked `«…»` below: the
-demo video URL. No test API key is provided — the demo video covers
+One placeholder must be filled before submitting: the demo video URL below.
+No test API key is provided. The demo video covers
the full flow instead.
---
@@ -12,9 +12,10 @@ peerd is an AI assistant in the browser side panel. The user types or
speaks a task; the assistant performs it by reading and interacting
with web pages, and by running computations in sandboxes (a WebAssembly
Linux VM and a JavaScript sandbox that can also run WebAssembly (WASI)
-programs) that exist entirely inside the browser. It is local-first: bring-your-own-API-key, no accounts, no
-backend, no analytics or telemetry of any kind. The developer operates
-no servers and receives no data.
+programs) that exist entirely inside the browser. It is local-first, uses
+bring-your-own-key providers, and has no account, hosted agent backend,
+analytics, or telemetry. The developer does not receive or store extension
+data through the extension.
## How to test
@@ -33,7 +34,9 @@ no servers and receives no data.
**Demo video** (full agent flow, VM boot, automation, audit log):
«VIDEO URL»
-## Remotely hosted code — none. Pre-answering the five places a scan
+## Remotely hosted code
+
+There is no remotely hosted code. These are the five places a scan
will flag:
1. **CheerpX (x86-in-WASM runtime) is fully vendored** in
@@ -43,7 +46,7 @@ will flag:
treatment. No CDN script loading anywhere; the package is vanilla,
unobfuscated ES modules.
2. **`disks.webvm.io` (vm-tab)** streams a stock Debian *filesystem
- image* — bytes interpreted as an ext2 disk by the sandboxed WASM VM.
+ image*. These bytes are interpreted as an ext2 disk by the sandboxed WASM VM.
It is data, not extension code, equivalent to a game loading an
asset file. It is the public image published by Leaning Technologies
(CheerpX's authors), fetched read-only when the user boots a VM.
@@ -55,29 +58,29 @@ will flag:
refuses to download in production. ONNX model weights are data
consumed by the bundled inference runtime, not executable code.
4. **Skills (`peerd-runtime/skills/`)** let the user import a SKILL.md
- instruction file — markdown *instructions for the AI model* (the same
- category as a user typing a long prompt), parsed and stored locally,
- never evaluated as code. In this V1 build the ONLY install path is
+ instruction file. It contains markdown instructions for the model, in the
+ same category as a user typing a long prompt. It is parsed and stored locally,
+ never evaluated as code. In the store build the only install path is
pasting text: remote install (fetch a SKILL.md from a git/manifest
URL) is gated OFF via `extension/shared/flags.js`
(`REMOTE_SKILL_INSTALL = false`). The side panel hides the URL tabs
and, more importantly, the service worker refuses the
- `skills/installGit` / `skills/installManifest` messages outright — so
+ `skills/installGit` / `skills/installManifest` messages outright, so
no remote fetch of agent-actioned files can happen, even from a
crafted message. The installer code ships but is unreachable; the
remote paths return in a later version with their own review.
-5. **WASI modules (`engine-tabs/notebook-tab/notebook-wasi.js`)** — the JavaScript
+5. **WASI modules (`engine-tabs/notebook-tab/notebook-wasi.js`)**. The JavaScript
sandbox can run wasm32-wasi programs (e.g. query a SQLite file the
user provides, decode an archive) via `WebAssembly.compile`, under
the same `wasm-unsafe-eval` CSP allowance the bundled WASM above
already uses. The runtime that hosts them is fully vendored and
audited (`vendor/browser-wasi-shim/SOURCE.txt`); the module bytes
- are user-directed data on the same footing as item 2's disk image —
- and confined strictly tighter than the JS around them: a module's
+ are user-directed data on the same footing as item 2's disk image and are
+ confined more tightly than the JavaScript around them. A module's
only imports are the bundled shim's WASI syscalls, every descriptor
behind those syscalls is constructed by our wrapper (stdin bytes,
size-capped stdout/stderr, an in-memory file table built from the
- call), and it has **no network, DOM, storage, or `chrome.*` reach —
+ call), and it has **no network, DOM, storage, or `chrome.*` reach because
no such import exists to link against**. It executes inside the
already-sealed Notebook/worker realm described below, bounded by
that run's timeout.
@@ -87,7 +90,7 @@ will flag:
This store package does **not** request the `debugger` permission. The
assistant operates pages entirely through `chrome.scripting`: it reads
content, builds an accessibility-style snapshot by walking the DOM, and
-performs selector/element click & type — all with bundled, in-package
+performs selector and element click or type actions with bundled, in-package
code (nothing fetched or generated remotely). There is no Chrome
DevTools Protocol use in this package.
@@ -104,8 +107,8 @@ and three things keep that honest regardless of channel:
- Every action goes to the local audit log, including denied attempts.
Maintainer note (not for the dashboard): an optional Chrome DevTools
-Protocol path — for sites that ship Trusted Types / strict CSP (Gmail,
-Notion, Slack), which reject injected scripts — ships in the separate
+Protocol path for sites that ship Trusted Types or strict CSP (Gmail,
+Notion, Slack), which reject injected scripts, ships in the separate
GitHub-distributed *preview* channel, gated by the in-app "Advanced
automation" switch and Chrome's visible "is debugging this browser"
banner. It is intentionally held out of the initial store submission so
@@ -123,43 +126,43 @@ task and is constrained by the same denylist + SSRF block + audit log.
We separate two things on purpose:
- **Credentialed provider path** (`safeFetch`): a hardcoded allowlist.
- Your API key can only reach a provider you configured — exfil of the
+ Your API key can only reach a provider you configured. Sending the
key/conversation to an arbitrary host is closed as a class.
- **Open-web path** (`webFetch`: the agent's web-read tools, the VM HTTP
- egress, and the Notebook's `peerd.egress.fetch` bridge): deliberately
- allowlist-FREE — the whole web is the point. It enforces a scheme
+ egress, and the Notebook's `peerd.egress.fetch` bridge): this path has no host
+ allowlist because the target is user-selected. It enforces a scheme
check, an SSRF/private-network block (IPv4 + structural IPv6, incl.
the cloud-metadata IP and IPv4-mapped forms), a sensitive-site
- denylist, fail-closed redirect handling, and a full audit log — but
- **not** a per-host allowlist. So exfil to an arbitrary *public* domain
- over this path is not categorically prevented; the architectural
- mitigations are (a) the do/get/check runner has no web tools, and (b)
- the audit log records every request. We do not claim otherwise.
+ denylist, fail-closed redirect handling, and a full audit log, but
+ **not** a per-host allowlist. Traffic to an arbitrary *public* domain
+ over this path is not categorically prevented. The web actor is keyless, its tool access is
+ narrowed, and the audit log records every request. We do not claim otherwise.
The Notebook specifically: the `js_notebook` Web Worker runs
agent-authored code, so its raw network primitives (XHR / WebSocket /
EventSource / WebTransport, plus native `fetch` recovered off the
prototype, and any nested `Worker`) are neutralized at the boundary by
-the host page's CSP `connect-src 'self'` (extension/engine-tabs/notebook-tab/index.html),
-which the worker and its descendants inherit — verified empirically. The
+the host page's CSP `connect-src 'none'` (extension/engine-tabs/notebook-tab/index.html),
+which the worker and its descendants inherit and which tests verify. The
only egress that leaves the Notebook is the audited `peerd.egress.fetch` bridge,
which is governed by the open-web `webFetch` gates above.
## CSP note
`connect-src` includes `https:` deliberately: the assistant fetches
-pages the user asks it to read, from the extension's service worker —
-the target set is user-chosen and cannot be enumerated in a manifest.
+pages the user asks it to read from the extension's service worker.
+The target set is user-chosen and cannot be enumerated in a manifest.
The egress layer enforces what the manifest cannot express: a hardcoded
allowlist for credentialed provider calls, the denylist + SSRF block
-for everything else, and the audit log for all of it. The only
-non-HTTPS entry is `wss://disks.webvm.io`, the disk-image stream's
-websocket fallback.
+for everything else, and the audit log for all of it. The generated manifest
+and store posture tests are the authority for the narrow non-HTTPS sources used
+by local providers and runtime assets.
## Privacy posture (for the data form)
-No backend, no analytics, no telemetry — the only "metering" in the
+No hosted agent backend, analytics, or telemetry. The only metering in the
code computes local cost estimates from the user's own API responses.
-User data goes exactly one place: the AI provider the user configured
-with their own key. API keys are stored in an encrypted vault
+Messages and task context go to the configured AI provider. User-directed web
+requests go to their requested destinations. Runtime assets are downloaded from
+the sources listed above. API keys are stored in an encrypted vault
(passphrase or WebAuthn PRF / platform biometrics).