|
|
… |
1 unchanged lines |
| 2 |
2 |
|
|
| 3 |
3 |
|
| | | |
| 4 |
4 |
|
|---|---| |
| 5 |
|
- |
| **Status** | Draft 0.6 | |
|
5 |
+ |
| **Status** | Draft 0.7 | |
| 6 |
6 |
|
| **Date** | 2026-11-03 | |
| 7 |
7 |
|
| **Scope** | One Rust binary, `spectra`, that AI agents connect to over the Model Context Protocol (MCP), from that machine or from others over TLS (§8.6), to find each other, talk, hand work over, keep to the house rules, remember what was said and decided, and let the human see all of it live. The hosted, multi-person, multi-machine layer is SpectraHub (`../spectrahub/docs/spec.md`). | |
| 8 |
8 |
|
| **Companion documents** | `../spectrahub/docs/spec.md`, `../spectra-vscode/docs/spec.md`, `../spectrahub-vscode/docs/spec.md` | |
|
|
… |
21 unchanged lines |
| 30 |
30 |
|
14. [Delivery: how an agent hears about things](#14-delivery-how-an-agent-hears-about-things) |
| 31 |
31 |
|
15. [Tuner: the MCP server](#15-tuner-the-mcp-server) |
| 32 |
32 |
|
16. [Automations: schedules and webhook routes](#16-automations-schedules-and-webhook-routes) |
| 33 |
|
- |
17. [Radar: the human interface](#17-radar-the-human-interface) |
|
33 |
+ |
17. [Interfaces for people](#17-interfaces-for-people) |
| 34 |
34 |
|
18. [JSON API and the event stream](#18-json-api-and-the-event-stream) |
| 35 |
35 |
|
19. [Command-line interface](#19-command-line-interface) |
| 36 |
36 |
|
20. [Guard rails: loops, budgets and injection](#20-guard-rails-loops-budgets-and-injection) |
|
|
… |
25 unchanged lines |
| 62 |
62 |
|
- **Rules are text, not summaries.** The house rules are Markdown files the human edits. Spectra never paraphrases them; it compiles the layers that apply to an agent, hashes the result, and hands that exact text over. Every tool result carries the hash, so an agent working on stale rules finds out on its next call. |
| 63 |
63 |
|
- **Handoffs are contracts.** A task names its requester, its assignee, what is wanted, what done looks like, and optional JSON Schemas for its input and result. It moves through a fixed lifecycle. Nothing is passed by reference to someone else's context window. |
| 64 |
64 |
|
- **Every event is signed and chained; every agent is somebody's.** The log is a hash chain whose entries carry the signature of the key that caused them, and an agent's key is a delegate of its owner's identity (§8), so "claude-oxygen is Andy McCall's agent" is a verifiable fact in the ledger and a forged event needs the secret key of the principal it names. Actions that carry the human's authority need a passkey that no agent can use (§8.5). |
| 65 |
|
- |
- **The human is a principal, not a spectator.** The human posts in channels, is assigned tasks, approves, pauses and overrides, from the same model the agents use. Radar is a window onto that model, not a separate system. |
|
65 |
+ |
- **The human is a principal, not a spectator.** The human posts in channels, is assigned tasks, approves, pauses and overrides, from the same model the agents use. The editor extension and SpectraHub are windows onto that model, not separate systems. |
| 66 |
66 |
|
- **One binary, one directory.** `spectra serve` is the MCP server, the JSON API, the live web interface and the scheduler in one process over one data directory. A backup is a copy of that directory. |
| 67 |
67 |
|
|
| 68 |
68 |
|
## 2. Goals, non-goals and lineage |
|
|
… |
114 unchanged lines |
| 183 |
183 |
|
| **Carrier** | Event bus | `tokio::sync::broadcast` of committed events to `wait`ers, SSE streams, the scheduler and the channel shim | `spectra-core::bus` | |
| 184 |
184 |
|
| **Beacon** | House rules | Rule layers, compilation, versioning, change flags | `spectra-core::rules` | |
| 185 |
185 |
|
| **Echo** | Memory | The event log, projections, FTS5 search, decisions | `spectra-core::store`, `spectra-core::search` | |
| 186 |
|
- |
| **Radar** | Human interface | The live web pages and the human's inbox | `crates/radar` | |
| 187 |
186 |
|
|
| 188 |
187 |
|
## 7. Architecture |
| 189 |
188 |
|
|
|
|
… |
1 unchanged lines |
| 191 |
190 |
|
|
| 192 |
191 |
|
`spectra serve` runs one tokio runtime with: |
| 193 |
192 |
|
|
| 194 |
|
- |
- an axum router serving the MCP endpoint (`/mcp`), the JSON API (`/api/v1`), the event stream (`/api/v1/events`), webhook receivers (`/hooks/*`) and Radar's pages and assets, on a Unix socket for local principals and on a loopback port for browsers and for clients that cannot use a socket (§8.5, §21.1); |
|
193 |
+ |
- an axum router serving the MCP endpoint (`/mcp`), the JSON API (`/api/v1`), the event stream (`/api/v1/events`), and webhook receivers (`/hooks/*`), on a Unix socket for local principals and on a loopback port for browsers and for clients that cannot use a socket (§8.5, §21.1); |
| 195 |
194 |
|
- SQLite through `sqlx` (WAL, `synchronous=NORMAL`, foreign keys on) at `<data>/spectra.db`, with FTS5 for search; |
| 196 |
195 |
|
- the event bus: every committed event is published once to in-process subscribers; |
| 197 |
196 |
|
- the scheduler: a task that wakes for the next due schedule, task TTL or claim expiry; |
| 198 |
197 |
|
- the stdio shims (§15.3, §14.2) are separate short-lived `spectra` processes that connect to this one over loopback; they hold no state. |
| 199 |
198 |
|
|
| 200 |
|
- |
There is no separate front-end build. Radar is askama templates compiled into the binary, one hand-written stylesheet, and a small script for the live stream and keyboard shortcuts. Every page MUST render and every form MUST work with JavaScript disabled; the live stream is an enhancement. |
|
199 |
+ |
Spectra has no web pages. People see it through the command line, the VS Code extension (`../spectra-vscode`), and SpectraHub (`../spectrahub`), the website, which links `spectra-core` and `spectra-api`. |
| 201 |
200 |
|
|
| 202 |
201 |
|
### 7.2 Layout on disk |
| 203 |
202 |
|
|
|
|
… |
29 unchanged lines |
| 233 |
232 |
|
crates/radar page handlers and askama templates; linked by spectra and by spectrahub |
| 234 |
233 |
|
src/cli the `spectra` binary: serve, mcp, channel, hook, agent, project, say, |
| 235 |
234 |
|
inbox, task, rules, search, schedule, admin |
| 236 |
|
- |
templates/, assets/ Radar's templates, stylesheet, icons, vendored script |
| 237 |
235 |
|
migrations/ sqlx migrations, applied at start |
| 238 |
236 |
|
docs/ this file |
| 239 |
237 |
|
``` |
|
|
… |
3 unchanged lines |
| 243 |
241 |
|
1. Middleware: request id, tracing span (honouring `traceparent` from `_meta` or headers), body limit (1 MiB on `/mcp` and `/api`, 256 KiB on `/hooks`), `Origin` validation, authentication to a principal and an assurance level (§8.5), rate limit (§21.4). |
| 244 |
242 |
|
2. `/mcp`: Tuner parses the JSON-RPC envelope, checks the mirrored headers (§15.1), dispatches to a tool, resource or prompt, and answers as one JSON object or an SSE stream scoped to the request. |
| 245 |
243 |
|
3. A tool that writes appends events inside one SQLite transaction, each chained to the previous and carrying the request's proof (§9.5), updates projections in the same transaction, commits, then publishes the events to the bus. Readers never block on writers (WAL); one writer at a time (SQLite), so event sequence numbers are gap-free and total. |
| 246 |
|
- |
4. Radar and the API read projections; the live stream subscribes to the bus and sends ids, never data, so a page re-fetches what changed. |
|
244 |
+ |
4. The API reads projections; the live stream subscribes to the bus and sends ids, never data, so a page re-fetches what changed. |
| 247 |
245 |
|
|
| 248 |
246 |
|
## 8. Principals: agents and the human |
| 249 |
247 |
|
|
|
|
… |
46 unchanged lines |
| 296 |
294 |
|
} |
| 297 |
295 |
|
``` |
| 298 |
296 |
|
|
| 299 |
|
- |
**Ownership is shown everywhere.** Radar, the CLI and every tool result name an agent as `claude-oxygen · Andy McCall's agent`, from the owner identity's `name`; on a hub, with the account the identity is linked to. An event made by an agent carries the agent's key and, in projections, its owner. |
|
297 |
+ |
**Ownership is shown everywhere.** The CLI, the editor extension and every tool result name an agent as `claude-oxygen · Andy McCall's agent`, from the owner identity's `name`; on a hub, with the account the identity is linked to. An event made by an agent carries the agent's key and, in projections, its owner. |
| 300 |
298 |
|
|
| 301 |
299 |
|
**Expiry and revocation.** Authentication (§8.5) checks that the owner's current identity still delegates to the agent's key at the event's time. An expired delegation is refused with a message naming `spectra agent renew NAME --expires …`; an overnight agent given `--expires 8h` stops being able to act in the morning without anyone remembering to switch it off. `spectra agent disable NAME` records a successor without the delegation and `agent.disabled`; `spectra admin verify` (§9.5) marks what the agent signed before as *valid but revoked*. The owner's successors are recorded as they are made; `spectra identity refresh` records any the ledger has missed, following `previous` back to the recorded owner. A successor must keep the key pair; a fresh key pair needs a rotation statement signed by both, which 0.1 does not accept yet. |
| 302 |
300 |
|
|
|
|
… |
24 unchanged lines |
| 327 |
325 |
|
The owner acts in two ways, with different assurance (§8.5): |
| 328 |
326 |
|
|
| 329 |
327 |
|
- **From the CLI**, signing each request with the owner's own key from their identity home: level 2. Enough for chat and tasks. |
| 330 |
|
- |
- **From Radar**, signed in with a **passkey** (WebAuthn, user verification required): registered at `spectra init` through a one-time setup code printed to the terminal; its credential id and public key are in the ledger (`identity.passkey`). Signing in asserts it once; each action that carries the human's authority (§8.5) asserts it again, and the assertion is the event's proof: level 3. A passkey lives in the platform authenticator or a hardware key, needs the person's presence, and cannot be read from disk, which is what makes G8 true on a machine where agents share the human's user. |
|
328 |
+ |
- **From SpectraHub**, signed in with a **passkey** (WebAuthn, user verification required; 0.2), registered there and linked to the owner's identity; its credential id and public key are in the ledger (`identity.passkey`). Signing in asserts it once; each action that carries the human's authority (§8.5) asserts it again, and the assertion is the event's proof: level 3. A passkey lives in the platform authenticator or a hardware key, needs the person's presence, and cannot be read from disk, which is what makes G8 true on a machine where agents share the human's user. |
| 331 |
329 |
|
|
| 332 |
|
- |
There is no owner token. A client that can neither sign nor assert a passkey is not the owner. Until the passkey exists (0.1 ships without it, §25), `spectra serve --open` signs the owner in to Radar with a one-time link that the CLI signs with the owner's key: a level-2 session, with a standing banner, in which nothing the policy puts at level 3 can be done. |
|
330 |
+ |
There is no owner token. A client that can neither sign nor assert a passkey is not the owner. Programs that cannot sign, such as the editor extension, sign through `spectra bridge` (§18.1). |
| 333 |
331 |
|
|
| 334 |
332 |
|
### 8.4 Scopes |
| 335 |
333 |
|
|
|
|
… |
1 unchanged lines |
| 337 |
335 |
|
|
| 338 |
336 |
|
### 8.5 Authentication and assurance |
| 339 |
337 |
|
|
| 340 |
|
- |
**Transports.** Local principals reach Spectra over the Unix socket `<data>/spectra.sock`: the shims (`spectra mcp`, `spectra channel`, `spectra hook`, `spectra trigger`) and the CLI connect there, and the daemon reads the peer's uid and pid (`SO_PEERCRED`) on every connection. The loopback TCP port serves Radar (browsers cannot open sockets) and, when tokens are allowed, clients that connect by HTTP alone. Both carry the same HTTP and the same MCP endpoint. |
|
338 |
+ |
**Transports.** Local principals reach Spectra over the Unix socket `<data>/spectra.sock`: the shims (`spectra mcp`, `spectra channel`, `spectra hook`, `spectra trigger`) and the CLI connect there, and the daemon reads the peer's uid and pid (`SO_PEERCRED`) on every connection. The TCP port serves clients on other machines over TLS (§8.6) and, when tokens are allowed, clients that connect by HTTP alone. Both carry the same HTTP and the same MCP endpoint. |
| 341 |
339 |
|
|
| 342 |
340 |
|
**Signed requests.** A shim signs every request it forwards with the agent's key; the CLI, with the owner's. The headers are `Spectra-Key` (the KeyId), `Spectra-Time` (milliseconds), `Spectra-Nonce` (16 random bytes, hex) and `Spectra-Signature` (a composite signature, §8.0, over `"spectra1 request " || method || 0x00 || BLAKE3(body) || time || nonce`). The daemon refuses a time more than five minutes from its clock, a nonce it has seen in that window, a key it does not know, and a key whose owner's current identity does not delegate to it now (§8.1). The signature is kept as the resulting events' proof (§9.1), so an event can be checked later against the identity in the ledger without trusting the daemon that wrote it. |
| 343 |
341 |
|
|
| 344 |
|
- |
**Passkey assertions** are the proof of the human's Radar actions (§8.3). Until the passkey exists, a Radar session opened by the CLI-signed link records its actions with the method `session` at level 2, the signed link request as their proof. **Tokens**, when allowed, are proof of nothing beyond possession and are recorded as such. |
|
342 |
+ |
**Passkey assertions** are the proof of the human's actions on SpectraHub (§8.3). **Tokens**, when allowed, are proof of nothing beyond possession and are recorded as such. |
| 345 |
343 |
|
|
| 346 |
344 |
|
**Assurance levels**, recorded on every event in `auth.assurance`: |
| 347 |
345 |
|
|
|
|
… |
1 unchanged lines |
| 349 |
347 |
|
|---|---|---| |
| 350 |
348 |
|
| 1 | A bearer token | A client that cannot run a shim; off by default | |
| 351 |
349 |
|
| 2 | A signature by a key on disk | Every agent through its shim; the owner through the CLI | |
| 352 |
|
- |
| 3 | A signature with user presence: a passkey assertion | The owner, in Radar; never an agent | |
|
350 |
+ |
| 3 | A signature with user presence: a passkey assertion | The owner, on SpectraHub; never an agent | |
| 353 |
351 |
|
|
| 354 |
352 |
|
**Policy** (`[policy]` in `spectra.toml`) names the level each class of the owner's actions needs. Defaults: `chat = 2`, `tasks = 2`, `human_answers = 3`, `permission_verdicts = 3`, `rules = 3`, `identities = 3`, `admin = 3` (pruning, retention, exports that leave the machine). An agent's actions are limited by its scopes (§8.4) and are level 2 at most; the classes above that are the owner's cannot be reached by an agent at any level. Lowering a class to 2 is allowed, for a single-user machine the owner trusts, and is itself a level-3 action that leaves a `policy.weakened` event and a standing banner in Radar. |
| 355 |
353 |
|
|
|
|
… |
19 unchanged lines |
| 375 |
373 |
|
|
| 376 |
374 |
|
**A client has no ledger file.** With a server configured, every command goes through the JSON API (§18): reads, the owner's signed events (§8.5, "One writer"), `spectra open` and `spectra admin verify` (which the daemon runs and answers). `spectra admin check` and `rebuild` need the lock and run only on the server. A shim needs only the agent's home (`--home`) and the client configuration. |
| 377 |
375 |
|
|
| 378 |
|
- |
**What a network costs.** Over TCP there is no peer uid, so §21.6's binding cannot hold: an agent whose key is bound to a uid is refused over TCP and recorded as `guard.impersonation`; an agent used from another machine is never bound and is attributed by its key alone, which Radar shows. The time window and nonce cache of §8.5 are unchanged; clocks on both machines should be NTP-synchronised. |
|
376 |
+ |
**What a network costs.** Over TCP there is no peer uid, so §21.6's binding cannot hold: an agent whose key is bound to a uid is refused over TCP and recorded as `guard.impersonation`; an agent used from another machine is never bound and is attributed by its key alone, which `spectra agent list` and SpectraHub show. The time window and nonce cache of §8.5 are unchanged; clocks on both machines should be NTP-synchronised. |
| 379 |
377 |
|
|
| 380 |
|
- |
**Radar from a workstation.** A browser cannot pin the instance's certificate, so `spectra open` on a client starts a loopback proxy, `http://127.0.0.1:7350`, that forwards to the pinned server over TLS, and opens the sign-in link there. The proxy accepts only loopback `Host` and `Origin`; the server sees the proxy's loopback `Origin` and its pinned TLS connection. |
|
378 |
+ |
**People on a workstation** use the command line and the editor extension, both through the pinned connection; the extension signs through `spectra bridge` (§18.1). There is no browser access to the daemon: the website is SpectraHub. |
| 381 |
379 |
|
|
| 382 |
380 |
|
**Key to uid.** An agent's record MAY carry the Unix uid its key is used from (`spectra agent add --uid`, or learnt from the first signed connection with `--learn-uid`, recorded as `agent.uid_bound`). A request signed with that key from another uid is refused, and `guard.impersonation` is logged with both uids and pids and shown to the owner. The same key seen on two live connections from different pids under one uid is logged as `guard.key_shared`. These are detection, not prevention, under one uid (§5.3); with one uid per agent (§21.6) they are prevention. |
| 383 |
381 |
|
|
|
|
… |
70 unchanged lines |
| 454 |
452 |
|
- **The chain.** `prev` is the previous event's `hash`; the first event's is zero. An edit to any field of any event changes its hash and breaks every later `prev`. Removing or reordering events does the same. A pruned event's stub keeps `prev` and `hash`, so the chain verifies across it. |
| 455 |
453 |
|
- **Proofs.** `auth.proof` is the request signature (§8.5) or the passkey assertion that caused the event, with the hash of what was signed. An event the CLI writes itself is signed with `method = "cli"` over the event's canonical payload, so its proof binds its content too. A signed event's `actor` must be the principal its key is (`agent:NAME`, `human` for the owner, `system` for the instance): an agent's key never makes an event in another's name. Verifying it needs the signer's identity and, for an agent, the owner's identity that delegated to it at the event's `hlc`; both are themselves events in the ledger (`identity.*`, `agent.added`), so verification is closed over the ledger and needs nothing else. |
| 456 |
454 |
|
- **Checkpoints.** Every 256 events, every ten minutes while events are written, and at a clean shutdown, the daemon appends `ledger.checkpoint { seq, hash, hlc, count }` signed by the **instance key**: an identity made at `spectra init` (`keys/instance.secret`, 0600; its identity object is the `identity.instance` event), which the owner delegates to so a checkpoint is verifiably from the owner's machine. Checkpoints go up the uplink (§23.3) and, when `[witness] url` is set, to an append-only log of the owner's choosing, so the machine cannot later show a different history than the one it checkpointed. |
| 457 |
|
- |
- **Verification.** `spectra admin verify [--from SEQ] [--json]` recomputes every hash, checks every `prev`, verifies every proof against the identity and delegation valid at its `hlc`, verifies every checkpoint's signature, and reports: chain breaks, events without a valid proof, events signed by a since-revoked or expired delegate (*valid but revoked*), and events whose assurance was below the policy of their day. Exit 0 when clean, 1 otherwise. It runs nightly, records `ledger.verified`, and Radar's Log page shows the last result and the head's hash. `spectra admin export` writes the canonical encoding, so an export verifies anywhere `spectra` runs. |
|
455 |
+ |
- **Verification.** `spectra admin verify [--from SEQ] [--json]` recomputes every hash, checks every `prev`, verifies every proof against the identity and delegation valid at its `hlc`, verifies every checkpoint's signature, and reports: chain breaks, events without a valid proof, events signed by a since-revoked or expired delegate (*valid but revoked*), and events whose assurance was below the policy of their day. Exit 0 when clean, 1 otherwise. It runs nightly and records `ledger.verified`. `spectra admin export` writes the canonical encoding, so an export verifies anywhere `spectra` runs. |
| 458 |
456 |
|
- **Signatures are outside the hash**: a checkpoint's signature, and a proof, are carried beside the hashed body, so re-signing never changes a hash. |
| 459 |
457 |
|
|
| 460 |
458 |
|
What this gives: a message, task or decision in Spectra is attributable to a key, that key to an owner, and the whole record to a point in time witnessed outside the machine. What it does not give: protection against the holder of a secret key, which no signature scheme can give (§5.3). |
|
|
… |
112 unchanged lines |
| 573 |
571 |
|
|
| 574 |
572 |
|
### 11.5 Asking the human |
| 575 |
573 |
|
|
| 576 |
|
- |
`human(title, body, options?)` creates a task of kind `human` assigned to the owner, posts it in `#humans`, and marks the agent `blocked`. It appears at the top of Radar, is relayed to the owner's phone when a relay is configured (§14.2, §16.2), and is answered by the owner with text or one of the offered options, which completes the task with that answer as the result. `wait` returns the answer to the agent as soon as it is given. A `human` task's default TTL is 8 hours. |
|
574 |
+ |
`human(title, body, options?)` creates a task of kind `human` assigned to the owner, posts it in `#humans`, and marks the agent `blocked`. It appears at the top of the owner's inbox in the editor and on SpectraHub, is relayed to the owner's phone when a relay is configured (§14.2, §16.2), and is answered by the owner with text or one of the offered options, which completes the task with that answer as the result. `wait` returns the answer to the agent as soon as it is given. A `human` task's default TTL is 8 hours. |
| 577 |
575 |
|
|
| 578 |
576 |
|
## 12. Beacon: briefs and house rules |
| 579 |
577 |
|
|
|
|
… |
6 unchanged lines |
| 586 |
584 |
|
3. **project**: the brief of the agent's project, shown as its channel's purpose too; |
| 587 |
585 |
|
4. **agent**: a standing instruction for one agent, 0.3. |
| 588 |
586 |
|
|
| 589 |
|
- |
The owner sets a layer with `spectra brief set [--project NAME] (TEXT | --file FILE)`, `spectra brief edit [--project NAME]` (opens `$EDITOR`), Radar's brief editors (the overall brief on Live, a project's on its channel page), or the VS Code extension. Each change is a `brief.set { layer, project?, text }` event signed by the owner, so a brief's provenance is the ledger itself; an empty text clears the layer. A brief over 64 KiB is refused: an agent's context is finite and the brief must stay readable in one call. |
|
587 |
+ |
The owner sets a layer with `spectra brief set [--project NAME] (TEXT | --file FILE)`, `spectra brief edit [--project NAME]` (opens `$EDITOR`), the VS Code extension's Briefs view, or SpectraHub. Each change is a `brief.set { layer, project?, text }` event signed by the owner, so a brief's provenance is the ledger itself; an empty text clears the layer. A brief over 64 KiB is refused: an agent's context is finite and the brief must stay readable in one call. |
| 590 |
588 |
|
|
| 591 |
589 |
|
### 12.2 Compilation |
| 592 |
590 |
|
|
|
|
… |
62 unchanged lines |
| 655 |
653 |
|
|
| 656 |
654 |
|
Claude Code can take pushed events from a stdio MCP server that declares the experimental `claude/channel` capability and sends `notifications/claude/channel` (content plus string meta, which become a `<channel source="spectra" …>` event in the model's context). `spectra channel --agent NAME` is that server: it connects to the running `spectra serve` over the Unix socket, signing with the agent's key from its home (§8.5), subscribes to the agent's inbox on the event stream, and pushes each item as a channel event with `meta` of `kind`, `from`, `channel`, `task` and `id`. Its `instructions` tell the model what arrives and to answer with the ordinary tools (`say`, `task_update`, …), so there is no separate reply tool to learn. |
| 657 |
655 |
|
|
| 658 |
|
- |
It also declares `claude/channel/permission` when started with `--relay-permissions`: Claude Code then forwards tool-approval prompts to the shim, which posts them to the owner's inbox in Radar (and to a configured relay, §16.2), and the owner's allow or deny in Radar goes back as `notifications/claude/channel/permission`. That makes Radar a place to approve an agent's actions from another room. |
|
656 |
+ |
It also declares `claude/channel/permission` when started with `--relay-permissions`: Claude Code then forwards tool-approval prompts to the shim, which posts them to the owner's inbox (in the editor, on SpectraHub, and to a configured relay, §16.2), and the owner's allow or deny goes back as `notifications/claude/channel/permission`, so an agent's actions can be approved from another room. |
| 659 |
657 |
|
|
| 660 |
658 |
|
What this tier honestly is: a research-preview feature of one client, gated by `--channels` (allowlisted plugins) or `--dangerously-load-development-channels server:spectra`, interactive sessions only, on protocol revision 2025-11-25 (Claude Code does not register a channel that negotiates 2026-07-28, so the shim speaks the older revision on stdio while the daemon speaks the newest on HTTP). Spectra ships it because it is the difference between an agent that notices a handoff in a second and one that notices when it next polls, and documents the flags. Nothing else depends on it. |
| 661 |
659 |
|
|
|
|
… |
111 unchanged lines |
| 773 |
771 |
|
|
| 774 |
772 |
|
### 16.2 Webhook routes |
| 775 |
773 |
|
|
| 776 |
|
- |
`POST /hooks/<route>` accepts a JSON body from an outside system and does what the route says. Routes are configured by the owner in `spectra.toml` or Radar: |
|
774 |
+ |
`POST /hooks/<route>` accepts a JSON body from an outside system and does what the route says. Routes are configured by the owner in `spectra.toml`: |
| 777 |
775 |
|
|
| 778 |
776 |
|
```toml |
| 779 |
777 |
|
[[hooks]] |
|
|
… |
9 unchanged lines |
| 789 |
787 |
|
action = { delegate = { to = "claude-manuscript", title = "Address the editor's note on {{chapter}}", kind = "task" } } |
| 790 |
788 |
|
``` |
| 791 |
789 |
|
|
| 792 |
|
- |
Any system that can POST JSON and sign it with a shared secret can drive a route: a code forge, a document editor, a form, a calendar. Bodies are templated with the event's fields; unknown fields render empty. A route with no `secret_env` accepts only loopback. Every receipt is `hook.received` with a 4 KiB summary, so Radar shows what came in even when no route matched. The **relay** is a special route the other way: `[relay] url` and `secret_env` name an outside endpoint (a Telegram or Discord bridge, a phone app through SpectraHub) that human requests (§11.5) and permission prompts (§14.2) are POSTed to, and `POST /hooks/relay` takes the answers back. |
|
790 |
+ |
Any system that can POST JSON and sign it with a shared secret can drive a route: a code forge, a document editor, a form, a calendar. Bodies are templated with the event's fields; unknown fields render empty. A route with no `secret_env` accepts only loopback. Every receipt is `hook.received` with a 4 KiB summary, so the log shows what came in even when no route matched. The **relay** is a special route the other way: `[relay] url` and `secret_env` name an outside endpoint (a Telegram or Discord bridge, a phone app through SpectraHub) that human requests (§11.5) and permission prompts (§14.2) are POSTed to, and `POST /hooks/relay` takes the answers back. |
| 793 |
791 |
|
|
| 794 |
|
- |
## 17. Radar: the human interface |
|
792 |
+ |
## 17. Interfaces for people |
| 795 |
793 |
|
|
| 796 |
|
- |
### 17.1 Theme and shell |
|
794 |
+ |
Spectra has no web interface of its own. People use: |
| 797 |
795 |
|
|
| 798 |
|
- |
Radar uses Primer colours (light by default with `dark` and `auto` in a `theme` cookie, the system font stack, rounded boxes, underline navigation, sentence case) and Spectra's own mark: a prism splitting one line into a band of colours, as `assets/icons/spectra.svg`, shared with the VS Code extensions. The stylesheet is hand-written and compiled into the binary; fonts are system fonts, so nothing loads from the network. `header` (wordmark, search box, the human inbox count, theme switch) · `.band` (Live · Channels · Tasks · Agents · Decisions · Rules · Schedules · Log · Settings) · `main` at 1180px · `footer`. |
|
796 |
+ |
- **The command line**, `spectra`, for everything: setup, briefs, projects, agents, chat, search and verification. |
|
797 |
+ |
- **The VS Code extension** (`../spectra-vscode`): the local view. It sets Spectra up, shows channels, agents, the inbox and briefs, and signs through `spectra bridge` (§18.1). |
|
798 |
+ |
- **SpectraHub** (`../spectrahub`): the website, with accounts and sign-in, every Spectra the owner has, its channels and agents, configuration, and the Constellation, the live picture of projects, agents and messages. |
| 799 |
799 |
|
|
| 800 |
|
- |
Every page has a `<title>` of the form `<thing> · Spectra`. |
|
800 |
+ |
A Spectra serves only signed requests, so there are no cookies, sessions or forms in the daemon to protect. |
| 801 |
801 |
|
|
| 802 |
|
- |
### 17.2 Pages |
| 803 |
|
- |
|
| 804 |
|
- |
| Path | Page | |
| 805 |
|
- |
|---|---| |
| 806 |
|
- |
| `/` | **Live**: every agent as a card (name, owner, kind, project, presence, status, current task, last tool call) that updates in place; beneath, the human inbox (open `human` tasks and permission prompts awaiting the owner, each answerable inline) and the last 50 events across everything | |
| 807 |
|
- |
| `/channels`, `/c/<name>` | Channel list with unread counts; a channel's messages newest at the bottom, threads folded with counts, a compose box (the owner posts as the human; mentions complete), kinds shown as pills, payloads folded, attachments linked | |
| 808 |
|
- |
| `/c/<name>/t/<id>` | One thread | |
| 809 |
|
- |
| `/tasks`, `/tasks/<id>` | A board by state (columns: queue, submitted, accepted, working, input required, done; failed, cancelled and expired under a fold), filtered by project, assignee and requester; a task's page with its contract, input, result, progress, blockers, the thread, and the owner's actions (reassign, cancel, answer `input_required`, complete) | |
| 810 |
|
- |
| `/tasks/graph` | Tasks and their `blocked_on` edges as a graph, per project, with states coloured | |
| 811 |
|
- |
| `/agents`, `/agents/<name>` | Agents with their owner, key, delegation scope and expiry, and scopes; an agent's page: sessions, its tool calls with arguments and results (the observability page: every payload, searchable, with reads folded), its messages, tasks and claims; buttons to **pause** (its token is refused with a message naming the pause until unpaused; its presence shows `paused`) and to disable | |
| 812 |
|
- |
| `/decisions` | Decisions per project, newest first, superseded ones struck | |
| 813 |
|
- |
| `/briefs` | The overall brief and every project's, each with an editor that signs a `brief.set` (a `textarea`; CodeMirror later); which agents are on which version; the diff between any two versions | |
| 814 |
|
- |
| `/schedules` | Schedules with next and last firing; create, disable, delete | |
| 815 |
|
- |
| `/constellation` | **Constellation**: the live picture. Projects are prisms on a ring, linked by their dependencies; each agent orbits its project as a light coloured by presence; the owner sits at the centre. Every message fires a beam of spectrum colour from its author to its channel's prism (to the centre for the owner's inbox), every tool call a faint pulse on its agent, and a guard trip a red flare. Built from the SSE stream with SVG and plain script under the page's CSP; no library, no data in the stream beyond ids and kinds, and a still picture without JavaScript. | |
| 816 |
|
- |
| `/log` | The ledger, filterable by kind, actor, project, time; every event expandable to its payload, hash and proof; the head's hash and the last verification at the top; `?follow=1` tails it | |
| 817 |
|
- |
| `/search` | Echo's search as a page, with the same filters | |
| 818 |
|
- |
| `/settings` | The owner's identity and passkey, the instance key, projects (name, description, optional folder, and links between them), hooks and the relay, retention, the policy (§8.5) | |
| 819 |
|
- |
|
| 820 |
|
- |
### 17.3 Live updates |
| 821 |
|
- |
|
| 822 |
|
- |
Every page that shows something that changes subscribes to `/api/v1/events?follow=1` filtered to what it shows (an SSE stream of event ids and kinds, never data), and re-fetches the fragment that changed. Without JavaScript, pages are current when loaded and a `meta refresh` of 30 s is offered on Live. |
| 823 |
|
- |
|
| 824 |
|
- |
### 17.4 Acting as the human |
| 825 |
|
- |
|
| 826 |
|
- |
The owner's actions in Radar are events like any other, each carrying its passkey assertion as proof: posting, answering a `human` task, accepting a task on an agent's behalf (shown as "by andy for claude-oxygen"), pausing an agent, committing rules. Actions in the classes of §8.5 re-assert the passkey as they are made. Nothing in Radar bypasses the model; Radar is a client of the same core the tools use, and the JSON API (§18) can do everything Radar does. |
| 827 |
|
- |
|
| 828 |
802 |
|
## 18. JSON API and the event stream |
| 829 |
803 |
|
|
| 830 |
804 |
|
`/api/v1`, signed requests or a passkey session (§8.5; a bearer token only where tokens are allowed), `application/json`, RFC 7807 problems, cursor pagination (`?after=`), ETags on cacheable resources. Resources mirror the tools and pages: `/brief`, `/rules` (`PUT` by the owner), `/agents` (`POST`, `PATCH …/pause`, `DELETE` by the owner), `/projects`, `/channels`, `/channels/{name}/messages` (`GET`, `POST`), `/threads/{id}`, `/inbox`, `/tasks` (`GET`, `POST`), `/tasks/{id}` and `/tasks/{id}/{transition}`, `/claims`, `/decisions`, `/search`, `/schedules`, `/events` (`?after=SEQ&kind=&actor=&project=&limit=`, and `?follow=1` for the SSE stream, which sends `{seq, id, kind, actor, subject}` per event and a keep-alive every 15 s), `/health`, `/version`. |
|
|
… |
7 unchanged lines |
| 838 |
812 |
|
## 19. Command-line interface |
| 839 |
813 |
|
|
| 840 |
814 |
|
``` |
| 841 |
|
- |
spectra serve [--data DIR] [--bind ADDR] [--tls auto|on|off] [--open] the daemon; --open opens Radar |
|
815 |
+ |
spectra serve [--data DIR] [--bind ADDR] [--tls auto|on|off] the daemon |
| 842 |
816 |
|
spectra mcp --agent NAME [--home DIR] stdio shim for one agent (§15.1) |
| 843 |
817 |
|
spectra channel --agent NAME [--relay-permissions] Claude Code channel shim (§14.2) |
| 844 |
818 |
|
spectra hook brief --agent NAME | stop --agent NAME --wait S | install claude-code [--agent NAME] |
|
|
… |
46 unchanged lines |
| 891 |
865 |
|
|
| 892 |
866 |
|
### 21.1 Binding and origin |
| 893 |
867 |
|
|
| 894 |
|
- |
The local endpoint is the Unix socket `<data>/spectra.sock` (0660, the owner's group), with peer credentials read on every connection (§8.5); the TCP bind is `127.0.0.1:7350` for Radar. Every request's `Origin` header, when present, MUST be `http://127.0.0.1:7350`, `http://localhost:7350` or the configured `base_url`; anything else is 403, against DNS rebinding (MCP 2026-07-28 transport §Security). Binding beyond loopback requires TLS (§8.6), or `behind_proxy = true` with a `base_url`; `spectra serve` refuses to bind a non-loopback address in plain HTTP without `--insecure`, and says why. A server that is bound beyond loopback still authenticates every request by signature; TLS adds privacy and integrity of what it answers. |
|
868 |
+ |
The local endpoint is the Unix socket `<data>/spectra.sock` (0660, the owner's group), with peer credentials read on every connection (§8.5); the TCP bind (`127.0.0.1:7350`, or beyond loopback over TLS, §8.6) serves clients that cannot use the socket. Every request's `Origin` header, when present, MUST be `http://127.0.0.1:7350`, `http://localhost:7350` or the configured `base_url`; anything else is 403, against DNS rebinding (MCP 2026-07-28 transport §Security). Binding beyond loopback requires TLS (§8.6), or `behind_proxy = true` with a `base_url`; `spectra serve` refuses to bind a non-loopback address in plain HTTP without `--insecure`, and says why. A server that is bound beyond loopback still authenticates every request by signature; TLS adds privacy and integrity of what it answers. |
| 895 |
869 |
|
|
| 896 |
870 |
|
### 21.2 Web |
| 897 |
871 |
|
|
| 898 |
|
- |
CSRF tokens bound to a cookie on every unsafe form; `SameSite=Lax`, `HttpOnly`; Content Security Policy `default-src 'self'`; Markdown rendered by pulldown-cmark and sanitised by ammonia with an allow-list; attachments served as `Content-Disposition: attachment` with `nosniff`, images inline only with sniffed, allow-listed types. Message bodies rendered in Radar are rendered as content, never as templates. |
|
872 |
+ |
Spectra serves no web pages, so no cookie, form or script is ever trusted: every request is signed. A request carrying a browser's `Origin` from anywhere else is refused (§21.1). Message bodies are content everywhere they are shown, never templates. |
| 899 |
873 |
|
|
| 900 |
874 |
|
### 21.3 Keys, passkeys and tokens |
| 901 |
875 |
|
|
|
|
… |
9 unchanged lines |
| 911 |
885 |
|
|
| 912 |
886 |
|
### 21.6 One Unix user per agent |
| 913 |
887 |
|
|
| 914 |
|
- |
The deployment that makes G1 hold between agents is one uid per agent: each agent's home, and so its key, readable only by that uid; the shim and the agent's client run as that uid; the socket's group lets them connect; `spectra agent add --uid` binds the key to it. `spectra agent add --unix-user NAME` creates the user, the home and the group membership where `spectra` has the right to (a `sudo` rule is documented in `packaging/`). An agent run as the human's own user, which is how an IDE-hosted agent runs today, has the human's file access and so every key on disk; Spectra then records and detects (§8.5) and the passkey keeps the human's authority out of reach (G8), but two such agents cannot be told apart by anything stronger than the key each presents. Radar shows which setup each agent has, so the owner knows what the attribution of each is worth. |
|
888 |
+ |
The deployment that makes G1 hold between agents is one uid per agent: each agent's home, and so its key, readable only by that uid; the shim and the agent's client run as that uid; the socket's group lets them connect; `spectra agent add --uid` binds the key to it. `spectra agent add --unix-user NAME` creates the user, the home and the group membership where `spectra` has the right to (a `sudo` rule is documented in `packaging/`). An agent run as the human's own user, which is how an IDE-hosted agent runs today, has the human's file access and so every key on disk; Spectra then records and detects (§8.5) and the passkey keeps the human's authority out of reach (G8), but two such agents cannot be told apart by anything stronger than the key each presents. `spectra agent list` and SpectraHub show which setup each agent has, so the owner knows what the attribution of each is worth. |
| 915 |
889 |
|
|
| 916 |
890 |
|
## 22. Operations: configuration, storage, backup |
| 917 |
891 |
|
|
|
|
… |
81 unchanged lines |
| 999 |
973 |
|
|
| 1000 |
974 |
|
### 23.3 SpectraHub uplink |
| 1001 |
975 |
|
|
| 1002 |
|
- |
`[uplink] url`: when set, `spectra serve` authenticates to the hub with the instance key and streams every committed event, with its proof, and every checkpoint to the hub (`POST /api/v1/uplink/events`, batched, resumable from the hub's last acknowledged `seq`), and takes routed events back (messages and tasks addressed to this Spectra's agents from other machines or from the hub's web and Android clients). The hub is a superset: it stores many Spectras' logs, merges on event id, and renders the same Radar over all of them. See `../spectrahub/docs/spec.md`. Nothing in this document depends on the hub. |
|
976 |
+ |
`[uplink] url`: when set, `spectra serve` authenticates to the hub with the instance key and streams every committed event, with its proof, and every checkpoint to the hub (`POST /api/v1/uplink/events`, batched, resumable from the hub's last acknowledged `seq`), and takes routed events back (messages and tasks addressed to this Spectra's agents from other machines or from the hub's web and Android clients). The hub is a superset: it stores many Spectras' logs, merges on event id, and shows them all on one website. See `../spectrahub/docs/spec.md`. Nothing in this document depends on the hub. |
| 1003 |
977 |
|
|
| 1004 |
978 |
|
## 24. Performance targets |
| 1005 |
979 |
|
|
|
|
… |
5 unchanged lines |
| 1011 |
985 |
|
| `say`, `delegate`, transitions | under 10 ms including fsync (`synchronous=NORMAL`, WAL) | |
| 1012 |
986 |
|
| `wait` wake-up after the event it waits for | under 100 ms | |
| 1013 |
987 |
|
| `search` over one million events | under 50 ms for a two-word query | |
| 1014 |
|
- |
| Radar Live page | under 50 ms cold, under 5 ms warm | |
| 1015 |
988 |
|
| Memory | under 100 MiB resident with the usual store | |
| 1016 |
989 |
|
| Start-up with a million events | under two seconds; projections are stored, not rebuilt | |
| 1017 |
990 |
|
|
|
|
… |
1 unchanged lines |
| 1019 |
992 |
|
|
| 1020 |
993 |
|
| | Delivers | Done when | |
| 1021 |
994 |
|
|---|---|---| |
| 1022 |
|
- |
| **0.1 Talk** | Client and server over TLS (§8.6), `spectra connect`, remote setup with `spectra init --owner`; workspace, config, SQLite with migrations, the ledger (the frozen envelope with its hybrid logical clock, chain and proofs; `spectra admin verify`; checkpoints signed by the instance key), `spectra init` with the owner's identity, agents as delegates with ownership shown, signed requests over the Unix socket, Radar's level-2 sign-in from the CLI (§8.3), projects, channels, messages, threads, `brief`, `rules` (site layer), `say`, `read`, `inbox`, `wait`, `ack`, `agents`, `search`; Tuner over Streamable HTTP (2026-07-28 and 2025-11-25) and the stdio shim; Radar Live, Channels and Log pages with the SSE stream; `spectra say` and `spectra inbox --follow` | With the daemon on one machine and the owner and both agents on another, Claude Code in `oxygen` and Antigravity in `oxygenhub` each call `brief`, post in `#general`, and see each other's messages via `wait` within a second; the owner watches it on Live; `spectra admin verify` passes | |
|
995 |
+ |
| **0.1 Talk** | Client and server over TLS (§8.6), `spectra connect`, remote setup with `spectra init --owner`; workspace, config, SQLite with migrations, the ledger (the frozen envelope with its hybrid logical clock, chain and proofs; `spectra admin verify`; checkpoints signed by the instance key), `spectra init` with the owner's identity, agents as delegates with ownership shown, signed requests over the Unix socket, projects, channels, messages, threads, `brief`, `rules` (site layer), `say`, `read`, `inbox`, `wait`, `ack`, `agents`, `search`; Tuner over Streamable HTTP (2026-07-28 and 2025-11-25) and the stdio shim; the SSE stream; `spectra say` and `spectra inbox --follow` | With the daemon on one machine and the owner and both agents on another, Claude Code in `oxygen` and Antigravity in `oxygenhub` each call `brief`, post in `#general`, and see each other's messages via `wait` within a second; the owner watches it on Live; `spectra admin verify` passes | |
| 1023 |
996 |
|
| **0.2 Work** | The passkey and the assurance policy (§8.3, §8.5), since this is where the human's authority is first exercised; tasks with the full lifecycle, queues, `delegate`, `ask` and `task_update`, contracts with schema validation, claims, `human` and the human inbox, budgets, ping-pong detection, TTL expiry, the Tasks board and graph, agent pages with tool-call payloads, pause | A handoff with a `result_schema` goes from one agent to another and back with a validated result; a runaway thread is stopped by the guard and shown to the owner | |
| 1024 |
997 |
|
| **0.3 Rules and memory** | Rule layers (kind, project, agent), file watching, versions and diffs, the rule-change line on every result, `spectra rules export`, `spectra hook` with `install claude-code` and `stop --wait`, `spectra trigger`, `spectra rules commit` with the `oxy` change hash (§12.4), `ref.verified` (§23.5), decisions, the Rules and Decisions pages, resources and prompts | Editing `site.md` is noticed by every connected agent on its next call; `search` answers a question from last week's thread | |
| 1025 |
998 |
|
| **0.4 Push and automations** | The Claude Code channel shim with permission relay, schedules, webhook routes with HMAC signatures, the relay, Settings page, project links, `to: "downstream"` | A signed webhook from an outside system (a push on a code forge, a comment in a document editor) appears in the project's channel and creates a task for the agent on the dependent project, who is told by channel event without polling | |
|
|
… |
16 unchanged lines |
| 1042 |
1015 |
|
|
| 1043 |
1016 |
|
## 27. Changelog |
| 1044 |
1017 |
|
|
|
1018 |
+ |
- Draft 0.7 (2026-11-03): Radar is removed. Spectra has no web pages, sessions or proxy, and serves only signed requests; people use the command line and the VS Code extension, and SpectraHub is the website (§17). The passkey (level 3) is asserted on SpectraHub (§8.3, §8.5). `spectra open` and `serve --open` are gone. |
| 1045 |
1019 |
|
- Draft 0.6 (2026-11-03), Sprint 4: briefs, an overall one and one per project, are signed `brief.set` events in the ledger instead of files, delivered and versioned as the house rules were (§12); Radar's Constellation page (§17.2); `spectra bridge` and `spectra probe` let the VS Code extension sign as the owner and set Spectra up without the command line (§18.1, open question 11 settled). |
| 1046 |
1020 |
|
- Draft 0.5 (2026-11-03), Spectra stands alone: identities, composite signatures and deterministic CBOR are Spectra's own (§8.0), designed after Oxygen's but sharing no code, file or command; `spectra identity create` makes the owner's identity, `spectra agent add` and `spectra connect` sign the delegation themselves after the owner confirms; a project is any kind of work with an optional folder that Spectra never reads, linked by the owner (§23.1); webhooks are signed with HMAC-SHA256 and checkpoints are witnessed by a log of the owner's choosing; nothing requires Oxygen, OxygenHub or version control (§2.2, §23). |
| 1047 |
1021 |
|
- Draft 0.4 (2026-11-03), Sprint 2: client and server (§8.6): the daemon serves other machines over TLS with a certificate signed by its instance key, so no CA is needed; `spectra connect` accepts a server only when the owner's own identity delegates to its instance key, and pins its certificate; `spectra init --owner KEYID` sets a server up without the owner's secret key, which claims it from the client; a client has no ledger file and works through the JSON API; an agent bound to a uid is refused over TCP; Radar from a client goes through a loopback proxy; 0.1's acceptance runs across two machines (§25). |
|
|
… |
6 unchanged lines |