|
|
… |
1 unchanged lines |
| 2 |
2 |
|
|
| 3 |
3 |
|
| | | |
| 4 |
4 |
|
|---|---| |
| 5 |
|
- |
| **Status** | Draft 0.3c | |
| 6 |
|
- |
| **Date** | 2026-11-02 | |
| 7 |
|
- |
| **Scope** | One Rust binary, `spectra`, that AI coding agents on one machine connect to over the Model Context Protocol (MCP) 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`). | |
|
5 |
+ |
| **Status** | Draft 0.4 | |
|
6 |
+ |
| **Date** | 2026-11-03 | |
|
7 |
+ |
| **Scope** | One Rust binary, `spectra`, that AI coding agents on one machine 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` | |
| 9 |
9 |
|
| **Audience** | Implementers, security reviewers, and the person who runs it | |
| 10 |
10 |
|
|
|
|
… |
323 unchanged lines |
| 334 |
334 |
|
|
| 335 |
335 |
|
**One writer.** Only one process appends to a data directory's ledger at a time: it holds an exclusive lock on `spectra.lock`, so the chain can never fork. While `spectra serve` runs, the CLI's writes go through it; reading and `spectra admin verify` need no lock. |
| 336 |
336 |
|
|
|
337 |
+ |
### 8.6 Client and server |
|
338 |
+ |
|
|
339 |
+ |
Spectra may run on one machine and be used from others: the daemon on a server, and the owner, the CLI, the shims and the agents on a workstation. Nothing about identity changes. Every request is still signed where the key lives (§8.5); keys never leave the machine they were made on; the server holds only its instance key. |
|
340 |
+ |
|
|
341 |
+ |
**The server.** `[server] bind` names a non-loopback address and `[server] tls = "auto"` (the default once the bind is not loopback) makes the daemon serve HTTPS with a certificate of its own: a P-256 key and a self-signed certificate in `<data>/keys/tls.key` and `tls.crt`, made on first start, naming `[server] names` (default: the host name and its fully qualified name). The instance key signs the certificate's binding, `"spectra1 tls " || SHA-256(SubjectPublicKeyInfo)`, through the challenge signing of §8.5, and the daemon records it as an `instance.tls` event. `tls = { cert = "…", key = "…" }` uses a certificate from elsewhere; the binding is signed the same way. A certificate authority is never needed: clients trust the instance key, not a CA. `GET /api/v1/instance` answers without authentication with the instance's name and identity, the owner's key, the certificate's SubjectPublicKeyInfo hash and the binding signature. |
|
342 |
+ |
|
|
343 |
+ |
**Connecting.** `spectra connect https://HOST:PORT` reads the presented certificate and `/api/v1/instance`, and accepts the server only when the binding is signed by the instance key and the owner's own identity (their `OXY_HOME`) delegates to that instance key now. Until the owner has delegated, it saves the instance's public identity and prints the `oxy identity delegate add` that would; it never asks the owner to trust a fingerprint by eye. On success it writes the client configuration, `$XDG_CONFIG_HOME/spectra/client.toml`: |
|
344 |
+ |
|
|
345 |
+ |
```toml |
|
346 |
+ |
server = "https://earth.example:7350" |
|
347 |
+ |
instance = "KEYID" # the instance key, delegated by the owner |
|
348 |
+ |
tls_spki = "SHA-256 hex" # the certificate the instance key signed |
|
349 |
+ |
``` |
|
350 |
+ |
|
|
351 |
+ |
Every later connection, from the CLI or a shim, accepts exactly that certificate. A changed certificate is refused until `spectra connect` runs again and the new one is signed by the same delegated instance key. `SPECTRA_SERVER` names a client configuration file for one command. |
|
352 |
+ |
|
|
353 |
+ |
**Remote setup.** `spectra init --owner KEYID` on the server makes the instance key and the ledger without the owner's secret key: the ledger starts with `identity.instance`, and the daemon serves `/api/v1/instance` and nothing else until the owner claims it. The first `spectra connect` from the owner sends `identity.owner`, signed over its payload as every CLI event is (§9.5), and the daemon accepts it only from the key `--owner` named. Until then every other request is refused with the reason "this Spectra has no owner yet". |
|
354 |
+ |
|
|
355 |
+ |
**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. |
|
356 |
+ |
|
|
357 |
+ |
**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. |
|
358 |
+ |
|
|
359 |
+ |
**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. |
|
360 |
+ |
|
| 337 |
361 |
|
**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. |
| 338 |
362 |
|
|
| 339 |
363 |
|
|
|
|
… |
497 unchanged lines |
| 837 |
861 |
|
|
| 838 |
862 |
|
### 21.1 Binding and origin |
| 839 |
863 |
|
|
| 840 |
|
- |
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 `[server] tls` 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. |
|
864 |
+ |
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. |
| 841 |
865 |
|
|
| 842 |
866 |
|
### 21.2 Web |
| 843 |
867 |
|
|
|
|
… |
43 unchanged lines |
| 887 |
911 |
|
# url = "https://oxygenhub.io" # where checkpoints are witnessed (§9.5) |
| 888 |
912 |
|
|
| 889 |
913 |
|
[server] |
| 890 |
|
- |
bind = "127.0.0.1:7350" |
|
914 |
+ |
bind = "127.0.0.1:7350" # "[::]:7350" to serve other machines (§8.6) |
| 891 |
915 |
|
base_url = "http://127.0.0.1:7350" |
| 892 |
916 |
|
behind_proxy = false |
| 893 |
|
- |
# tls = { cert = "…", key = "…" } |
|
917 |
+ |
tls = "auto" # beyond loopback: a certificate of its own, signed by the instance key; or { cert = "…", key = "…" } |
|
918 |
+ |
# names = ["earth", "earth.example"] # the certificate's names (default: the host name and its FQDN) |
| 894 |
919 |
|
|
| 895 |
920 |
|
[tasks] |
| 896 |
921 |
|
default_ttl = "24h" |
|
|
… |
73 unchanged lines |
| 970 |
995 |
|
|
| 971 |
996 |
|
| | Delivers | Done when | |
| 972 |
997 |
|
|---|---|---| |
| 973 |
|
- |
| **0.1 Talk** | 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` | 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 | |
|
998 |
+ |
| **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 | |
| 974 |
999 |
|
| **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 | |
| 975 |
1000 |
|
| **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 | |
| 976 |
1001 |
|
| **0.4 Push and automations** | The Claude Code channel shim with permission relay, schedules, webhook routes with the OxygenHub signature, the relay, Settings page, `.oxymodules` relations, `to: "downstream"` | A push to `oxygen` on OxygenHub appears in `#oxygen` and creates a pin-bump task for the `oxygenhub` agent, who is told by channel event without polling | |
|
|
… |
2 unchanged lines |
| 979 |
1004 |
|
|
| 980 |
1005 |
|
## 26. Open questions |
| 981 |
1006 |
|
|
| 982 |
|
- |
1. **Hub or no hub.** Everything in this document is one machine, one human. The Hub exists for other machines and other people. If the second machine never comes, the Hub is the Android app's backend and a backup, and could be a `spectra serve --public` mode instead. Decide after 0.3, when the local experience has been lived with. |
|
1007 |
+ |
1. **Hub or no hub.** Everything in this document is one human, and since Draft 0.4 one daemon that other machines may reach (§8.6). The Hub exists for other machines and other people. If the second machine never comes, the Hub is the Android app's backend and a backup, and could be a `spectra serve --public` mode instead. Decide after 0.3, when the local experience has been lived with. |
| 983 |
1008 |
|
2. **Semantic search.** FTS5 first (§13.4). Revisit when a question `search` cannot answer comes up twice. |
| 984 |
1009 |
|
3. **Channel shim lifetime.** The Claude Code channel feature is a research preview with an allowlist. If it ships generally, `spectra channel` becomes a plugin in an official marketplace; if it is withdrawn, Tier 3 hooks are the push path. Keep both until one wins. |
| 985 |
1010 |
|
4. **Tool count.** Twenty after Draft 0.2 folded the transitions. Measure which are called; `claim` and `schedule` could fold into `task_update`'s shape further, or `ask` into `delegate`, if the list still costs too much for small models. |
|
|
… |
7 unchanged lines |
| 993 |
1018 |
|
|
| 994 |
1019 |
|
## 27. Changelog |
| 995 |
1020 |
|
|
|
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). |
| 996 |
1022 |
|
- Draft 0.3c (2026-11-02), while building #10: a Radar session's events carry the method `session` (assurance 2) and, as proof, the owner's signed request for the sign-in link, which `verify` checks against the owner (§8.3); every ledger write runs to completion in a task of its own, so a caller that goes away cannot leave a transaction open (§9.5); Radar's templates and assets live in the `radar` crate, so SpectraHub mounts it whole (§7.3). |
| 997 |
1023 |
|
- Draft 0.3b (2026-11-02), while building #7: a refused request names its reason (§15.1); requests to the daemon are signed with the owner identity the ledger already records, and a rotation travels in the same batch as the event that needs it (§8.5). |
| 998 |
1024 |
|
- Draft 0.3a (2026-11-02), while building #3 and #4: request and checkpoint signatures go through `oxy-core`'s challenge signing with Spectra's prefix inside the message (§8.5, §9.5); CLI-written events are signed over their payload, and a signed event's actor must be its signer (§9.5); one writer per data directory under a lock (§8.5); `identity refresh` proves possession at every successor and refuses a fresh key pair in 0.1 (§8.1); `agent add` waits for the delegation and resumes (§8.1). |
|
|
… |
3 unchanged lines |