From 1050cee5df6c8bcd6e748db5cbd1282e42872da7 Mon Sep 17 00:00:00 2001 From: NotASithLord <48842926+NotASithLord@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:13:31 +0200 Subject: [PATCH] docs: refresh project guidance --- CONTRIBUTING.md | 62 ++-- README.md | 633 +++++++---------------------------- SECURITY.md | 63 ++-- docs/store/OPEN-DECISIONS.md | 241 ++----------- docs/store/PRIVACY.md | 157 ++++----- docs/store/REVIEWER-NOTES.md | 77 +++-- 6 files changed, 332 insertions(+), 901 deletions(-) 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 @@ [![CI](https://github.com/NotASithLord/peerd/actions/workflows/package-and-release.yml/badge.svg)](https://github.com/NotASithLord/peerd/actions/workflows/package-and-release.yml) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) -[![Status: 0.x experimental](https://img.shields.io/badge/status-0.x%20experimental-orange.svg)](#install) +[![Status: 0.x experimental](https://img.shields.io/badge/status-0.x%20experimental-orange.svg)](#status) [![Manifest V3](https://img.shields.io/badge/Manifest%20V3-Chrome%20%26%20Firefox-informational.svg)](#install) -[![No build step](https://img.shields.io/badge/build-none%20(vanilla%20JS)-success.svg)](#getting-started) - -[![types: 100% ts-check](https://img.shields.io/badge/types-100%25%20%2F%2F%20%40ts--check-brightgreen.svg)](packaging/check-tscheck.ts) [![Security policy](https://img.shields.io/badge/security-policy-blue.svg)](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).