Skip to content

Commit 747ed0b

Browse files
authored
feat: web-actor architecture, plus actor_list and message_actor oneShot
The main agent becomes an orchestrator that delegates each environment (WebVM, notebook, app, open web) to a per-environment sub-agent (an actor) holding only that environment's tools. One delegation channel, message_actor; the web actor is the single entry point for all web work. Three things get better: - Less context on the main agent: per-environment detail loads only on delegation, and the redundant web tools collapse into the web actor. - Fewer tool calls: actor_list replaces five list tools, and message_actor oneShot drops a model turn on single-round delegations. - Real isolation: the environment tools are off the main agent entirely, and untrusted page/VM/fetch output reaches the orchestrator only as a fenced reply. Delegations run async and in parallel, show as inline cards you can stop, report their cost, and survive a service-worker restart via a durable inbox. Also lands the staged open-PR backlog (PDF reading, on-device OCR, browser-native VM networking, session auto-resume, whole-extension type coverage) and bumps the version to 0.2.0. See CHANGELOG.md [0.2.0].
1 parent 29ddf68 commit 747ed0b

131 files changed

Lines changed: 7938 additions & 2762 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,57 @@ storage formats may move until the surface stabilizes.
1010

1111
## [Unreleased]
1212

13-
The staged integration of the open-PR backlog onto one verified branch
14-
(the next milestone). Highlights:
13+
_Nothing yet._
14+
15+
---
16+
17+
## [0.2.0] — 2026-06-29
18+
19+
The big change is how peerd's agent is structured, landing with the
20+
staged backlog of open PRs on one verified branch. Three things get
21+
better:
22+
23+
- **Less context on the main agent.** Each environment's operating
24+
details (VM shell quirks, notebook isolation rules, app iframe gotchas,
25+
whether to fetch or render a page) used to sit in the main prompt on
26+
every turn, mostly unused. Now they live with the sub-agent that uses
27+
them and load only when work is handed off. The main agent also drops
28+
the web tools it no longer needs: web_search, read_article, call_api,
29+
and submit_form all fold into the web actor.
30+
- **Fewer tool calls.** actor_list replaces five separate list tools with
31+
one, so the agent makes one call and carries one list instead of five.
32+
message_actor's oneShot skips a whole model turn when one round of work
33+
is enough.
34+
- **Real isolation, not just convention.** The tools that operate
35+
environments are no longer on the main agent at all, so even a confused
36+
or prompt-injected agent can't reach them; it has to send a
37+
permission-gated message. Page text, fetch bodies, and command output
38+
stay inside the sub-agent and come back as a quoted, untrusted reply,
39+
never as raw text the agent could be steered by.
1540

1641
### Added
42+
- **The actor architecture** (DESIGN-17 / DESIGN-18). The main agent now
43+
acts as an orchestrator. It opens an environment (a WebVM, a notebook, a
44+
built app, or the open web) and hands the work to that environment's own
45+
sub-agent, called an actor, which holds only that environment's tools.
46+
There is one way to delegate, message_actor. The web actor is the single
47+
entry point for all web work and picks per task between a sessionless
48+
fetch (fetch_url) and driving a real tab. Delegations run in the
49+
background and in parallel, show up as cards in the chat you can watch
50+
and stop, report their own cost, and survive a service-worker restart
51+
because pending work is written to storage.
52+
- **API integrations (origin actors).** Send a message to a bare origin
53+
like api.github.com and peerd forms a fetch-only, keyless,
54+
origin-locked actor for it that remembers what it learns about that API
55+
across messages.
56+
- **actor_list.** One tool that lists everything you can message: every
57+
WebVM, notebook, app, open tab, and API integration, each with its type
58+
and the handle to pass to message_actor. Replaces five separate list
59+
tools.
60+
- **message_actor oneShot.** Set it when one round of work settles the
61+
request, like a specific command or a read. The actor does the action
62+
and hands back the raw result instead of spending an extra model turn to
63+
restate it.
1764
- **PDF reading** (`read_pdf`) — pdf.js text-layer extraction in the
1865
offscreen document for born-digital PDFs; runner-only, output wrapped
1966
as untrusted web content.
@@ -32,6 +79,17 @@ The staged integration of the open-PR backlog onto one verified branch
3279
into the WebVM bridge.
3380

3481
### Changed
82+
- The main agent's browser tools are now just actor_list, open_tab, and
83+
message_actor (plus capture). The low-level page tools and the tools
84+
that write to an environment moved to the actors.
85+
- WebVM self-heal. When the browser freezes a backgrounded VM tab, peerd
86+
now checks it and reloads it before a command lands on a dead shell. The
87+
terminal output stripping was also fixed so output is not eaten when it
88+
splits across a chunk.
89+
- The thinking and boot spinner is now the brand orb ring, one rainbow
90+
sweep masked to a hollow ring.
91+
- The prose docs were removed. The code is the spec, and CLAUDE.md is the
92+
short orientation map.
3593
- Service worker restructured into per-route modules with injected
3694
per-module state stores; handlers stay thin.
3795
- README reordered to lead with install + project conventions; Tesseract

README.md

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ peerd uses *the browser* as its runtime and its security model. It builds
3939
on decades of hardened browser platform work (V8 isolates for sandboxing,
4040
WebCrypto for the vault, WebAuthn passkeys to unlock it, opaque-origin
4141
iframes, Subresource Integrity) and writes none of its own cryptographic
42-
or process-isolation code. The agent that holds your keys never reads a
43-
raw page; a disposable runner with no keys and no network does, and its
44-
output comes back fenced as untrusted. Every action the agent drives is
42+
or process-isolation code. The agent that holds your keys never operates
43+
an environment itself: each browser tab, VM, notebook, and app is driven
44+
by its own keyless actor sub-agent that exclusively holds that
45+
environment's tools. The main agent acts as an orchestrator. It delegates
46+
a goal to an actor and gets back a summary fenced as untrusted, so raw
47+
page text and command output never reach the context that holds your
48+
keys, and a confused or prompt-injected main agent has no tool to touch
49+
an environment with in the first place. Every action an actor drives is
4550
verified against the live page before it counts as done. (More at
4651
[peerd.ai](https://peerd.ai).)
4752

@@ -214,7 +219,7 @@ works today, its public API, known limitations, and TODOs:
214219
| **`p`** · cyan | [`peerd-provider`](extension/peerd-provider/README.md) | Model adapters — Anthropic, OpenRouter, Ollama (streaming, caching, cost, retries) |
215220
| **`e`** · red | [`peerd-egress`](extension/peerd-egress/README.md) | Security — the vault, the egress chokepoint, the denylist, the audit log |
216221
| **`e`** · amber | [`peerd-engine`](extension/peerd-engine/README.md) | Sandboxes — WebVMs, Notebooks, Apps, and the headless worker |
217-
| **`r`** · green | [`peerd-runtime`](extension/peerd-runtime/README.md) | The agent — loop, tools, do/get/check, memory, skills, review, goal mode, voice |
222+
| **`r`** · green | [`peerd-runtime`](extension/peerd-runtime/README.md) | The orchestratoragent loop, tools, the `message_actor` delegation channel, actors, sessions, memory, skills, review, goal mode, voice |
218223
| **`d`** · magenta | [`peerd-distributed`](extension/peerd-distributed/README.md) | The dweb — the peer-to-peer network (preview channel only) |
219224

220225
The brand IS the architecture: cross-module imports go through each
@@ -227,15 +232,24 @@ module's `index.js`, never deep paths; nothing outside
227232
peerd's safety is *who is allowed to do what*: small boundaries
228233
enforced by the browser platform, not by peerd's own crypto. Two
229234
principles run through all of it: **the agent that holds your keys never
230-
touches a raw page or runs untrusted code**, and **the agent never gets the
231-
final word on correctness; every action is verified against the live page
232-
before it counts as done.**
235+
touches a raw page or runs untrusted code** — the environment-operating
236+
tools are not even attached to it, they belong to per-environment actor
237+
sub-agents — and **the agent never gets the final word on correctness;
238+
every action is verified against the live page before it counts as done.**
239+
240+
The orchestrator delegates; an actor does the work. Each tab, VM,
241+
notebook, and app is owned by one actor that holds only that
242+
environment's tools, runs without keys, and hands back a fenced summary.
243+
So isolation between environments is structural, not a convention: even a
244+
fully prompt-injected main agent cannot reach an environment it was not
245+
asked to, because it never held the tool.
233246

234247
| Actor | Trusted with | Never |
235248
|---|---|---|
236249
| **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 |
237-
| **The main agent** (`peerd-runtime/loop`) | the conversation, planning, tool dispatch | reading raw page bytes or running untrusted code directly |
238-
| **The disposable runner** (`peerd-runtime/runner`) | driving + reading the page via do/get/check | holding keys or its own network; its output returns `wrapUntrusted`-fenced |
250+
| **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 |
251+
| **An actor** (`peerd-runtime/subagent`) | driving ONE tab / VM / notebook / app — it exclusively holds that environment's tools, keyless | touching another environment, holding keys, or returning anything to the orchestrator except a `wrapUntrusted`-fenced summary |
252+
| **The disposable runner** (`peerd-runtime/runner`) | driving + reading a page keyless via do/get/check — the lineage a web actor and subagents use | holding keys or its own network; its output returns `wrapUntrusted`-fenced |
239253
| **The egress chokepoint** (`safeFetch` / `webFetch`) | every outbound byte — provider allowlist + denylist + SSRF guard | being bypassed; a bare `fetch` is lint-forbidden |
240254
| **The sandboxes** (WebVM · Notebook · App) | running code — V8 isolates + opaque-origin iframes | extension access; their HTTP routes back through egress |
241255
| **Web content** | nothing by default | being trusted — all of it is fenced as untrusted input |
@@ -306,14 +320,20 @@ see, focus, and close, grouped under "peerd" in the tab strip and
306320
surviving browser restarts: the WebVM, the Notebook, and the App. The
307321
fourth, the headless worker (`js_run`), runs the Notebook's sealed worker
308322
offscreen with no tab: ephemeral, for the agent's own quick compute. The
309-
agent picks the lightest kind that fits the task.
323+
orchestrator picks the lightest kind that fits the task, bootstraps the
324+
instance, and then delegates the work to that instance's actor; the
325+
tool lists below are the surface an actor drives, not the main agent. One
326+
main-agent tool spans all of them: **`actor_list`** enumerates every
327+
addressable actor — WebVMs, Notebooks, Apps, open tabs, and API
328+
integrations — each tagged with its `type` and the handle to pass to
329+
`message_actor`, so discovery is one call instead of five.
310330

311331
**WebVM**: CheerpX-emulated Debian (sandboxed Linux). Own disk (IDB
312332
overlay), own bash, own POSIX. ~10s first boot. Use it when you need
313333
real binaries, a shell, or multi-language stacks.
314334

315335
```
316-
vm_list vm_create vm_boot vm_import vm_write_file vm_delete
336+
vm_create vm_boot vm_import vm_write_file vm_delete
317337
```
318338

319339
HTTP egress from the VM (curl / wget / git clone) is intercepted by
@@ -328,7 +348,7 @@ worker's only network, routed through `peerd-egress` so it's honest. Each
328348
`peerd.self.writeFile`/`readFile` to the OPFS file tree.
329349

330350
```
331-
js_list js_create js_notebook js_run js_write_file js_read_file js_delete
351+
js_create js_notebook js_run js_write_file js_read_file js_delete
332352
```
333353

334354
**Headless worker** is the same sealed worker as a Notebook, but headless:
@@ -344,7 +364,7 @@ search across name, tags, and body. `app_update` auto-reloads the open
344364
tab so iterations show live.
345365

346366
```
347-
app_list app_create app_update app_open app_search app_delete
367+
app_create app_update app_open app_search app_delete
348368
```
349369

350370
## Tests

docs/DECISIONS.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,38 @@ This is the **first** non-`self` `peerd.*` capability wired for Apps;
624624
`egress` goes first because its blast radius is the most contained of the
625625
five modules, and the grant/quota machinery built here is the template the
626626
others inherit (#21).
627+
628+
## 28. An actor is a per-instance agent; the binding is a routing pointer, not an owner gate
629+
630+
DESIGN-17 P0 (`docs/specs/DESIGN-17-actor-agents.md` + `DESIGN-17-DEV-NOTES.md`).
631+
Three structural choices worth recording:
632+
633+
**A third `SessionKind`, `actor`.** Each tab-hosted instance (WebVM /
634+
Notebook / App) is owned by one agent — an actor — that exclusively holds
635+
that environment's MUTATING tools and is addressed only by `message_actor`.
636+
The per-environment tooling LEAVES the main agent (context optimized, and
637+
non-eroding because it never returns), and "who may touch this instance" becomes
638+
STRUCTURAL instead of a convention. An actor is "a session with parentage"
639+
(the subagent precedent, #SUBAGENTS) — no new shape, just `kind:'actor'` +
640+
two binding fields. Hidden from `/chats` (reached via its instance, not the
641+
chat list), like a subagent.
642+
643+
**The capability tier is a dispatch-gate refusal, not descriptor hygiene.** The
644+
exposure axis is binary `main`/not-`main` and cannot keep instance tools off a
645+
*subagent*, so a deny-set alone is bypassable by a one-line
646+
`spawn_subagent({tools:['app_delete']})`. The wall is `exposureGate`: the
647+
mutating tier is refused for every ctx whose marker isn't `actor`, and an
648+
actor is positively scoped to its own kind + pinned to its own instance.
649+
Only MUTATION is tiered — reads stay global and id-addressable.
650+
651+
**The instance→actor binding is a ROUTING pointer (`actorSessionId`), not
652+
a per-call owner gate.** A `session === record.owner` check on every mutation
653+
carries transfer-on-settle and brick-the-instance failure modes; the routing
654+
pointer is one-directional addressing — mutation is gated by the capability
655+
tier, not the pointer — so if the actor session is gone, mint a fresh one and
656+
the instance is never bricked. Minting is lazy (the first `message_actor`).
657+
658+
The actor structure is the foundation for later per-tab JS-safety isolation
659+
(relocate only the inner loop into a per-tab worker — the wrapper stays SW-side),
660+
purpose-tuned per-actor model tiers, an in-browser actor mesh, and A2A across
661+
peers — but P0 is the structure + its security, behind a flag, attended-only.

0 commit comments

Comments
 (0)