OXYGENHUB

Sprint 2: client and server (spec Draft 0.4, §8.6) and its plan d17cbc7069ce

The daemon on one machine, the owner and agents on another, over TLS
trusted through the instance key and the owner's delegation; remote
setup without the owner's secret key; #13 moves to Sprint 2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 0559f022ed0b
When
· 2026-11-03 12:57 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
d17cbc7069cee474801dcaf0dd22c52a4d8f3f6dc9259f44d9fe50fa62299d72
Depends on
09752034b5a8 258bb3824813 39f5c913f617 8dc6df8c4d4e 98a16830a3d3 da84e98f104e
Depended on by
862f0db224aa f7986cb07b0e 8f87df88babf 98a0fb9f0948 aa72eb904932 01320eefd505 67bfa06a894f eca5af92279b 77df591d5a2f b576d05cf862
In bookmarks
main
In tags
v0.1.0
Edits
16 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified docs/plan.md +21 −10
1 -
# Sprint 1 plan: Spectra 0.1, Talk
1 +
# Sprint plan: Spectra 0.1, Talk
2 2
3 -
Each loop iteration: pick the first unchecked item, implement it with tests, run the gated checks, commit with `oxy`, tick the box here with a one-line note, and commit again. Update `docs/spec.md` (and its changelog) whenever behaviour the spec describes changes. If an item cannot be finished in two iterations, note why under it and move on. The issues are on https://oxygenhub.io/projects/andymccall/7.
3 +
Each loop iteration: pick the first unchecked item, implement it with tests, run the gated checks, commit with `oxy`, tick the box here with a one-line note, and commit again. Update `docs/spec.md` (and its changelog) whenever behaviour the spec describes changes. If an item cannot be finished in two iterations, note why under it and move on. The issues are on https://oxygenhub.io/projects/andymccall/7. Sprint 1 built 0.1 on one machine; Sprint 2 makes it client and server (spec §8.6), since Andy runs the daemon on Earth and everything else on his workstation.
4 4
5 5
## Rules
6 6
…
5 unchanged lines
12 12
- Never commit or push anything under `docs/prompts/` (Andy's prompts; ignored in `.oxyignore`). Name the paths in every commit.
13 13
- Never act as Andy: owner-only steps (delegating to an agent key, the acceptance run) wait for him.
14 14
15 -
## Items
15 +
## Sprint 1 (done)
16 16
17 17
- [x] #1 Project skeleton in Oxygen's shape — done 2026-11-02: workspace, deps/oxygen pinned, make check, deps-check (212 crates, all vendored).
18 18
- [x] #2 CI on OxygenFlow — done 2026-11-02: run #1 passed fmt, clippy, test, build with no image rebuild.
…
7 unchanged lines
26 26
- [x] #10 Radar — done 2026-11-02: Live, Channels, channel/thread with compose, Log with Verify now; spectra open; checked by hand in Chrome on a demo Spectra. That demo found the ledger cancellation bug (fixed, regression test fails on the old code) and a referrer policy that blanked the Origin of form posts.
27 27
- [x] #11 JSON API and spectra-api types — done 2026-11-02: every 0.1 endpoint, problems, paging, ETags, the live log; types and ts/index.d.ts. How spectra-vscode will sign is spec open question 11.
28 28
- [x] #12 Human CLI — done 2026-11-02: say, inbox --follow, search, api (the signing proxy for spectra-vscode); tests/cli_json.rs parses every --json output with and without the daemon.
29 -
- [ ] #13 Acceptance (with Andy). Ready for him; what is left needs his key and his MCP configuration:
30 -
  1. `make install`; `spectra init` (reads his identity from ~/.config/oxy); `oxy identity delegate add <data>/keys/instance.identity --scope changes`.
31 -
  2. `cp packaging/spectra.service ~/.config/systemd/user/ && systemctl --user enable --now spectra`.
29 +
- #13 Acceptance moved to Sprint 2, to run across two machines.
30 +
31 +
## Sprint 2: client and server
32 +
33 +
- [ ] #20 Server over TLS: a certificate signed by the instance key, /api/v1/instance, no uid over TCP.
34 +
- [ ] #22 Remote setup: spectra init --owner KEYID and the owner's claim from the client.
35 +
- [ ] #21 spectra connect and the TLS client: trust through the owner's delegation, pinned certificate, shims over the network.
36 +
- [ ] #23 The CLI without a ledger file: every command through the JSON API.
37 +
- [ ] #24 Radar from a workstation: spectra open's loopback proxy to the pinned server.
38 +
- [ ] #25 Run the daemon on Earth: install over SSH, a unit that waits for a Spectra, README.
39 +
- [ ] #13 Acceptance (with Andy), across two machines. Ready when the rest is done:
40 +
  1. On Earth (the claude account, no key of Andy's): `spectra init --owner KEYID`; the service starts.
41 +
  2. On the workstation: `spectra connect https://earth.andymccall.xyz:7350`; run the printed `oxy identity delegate add … --scope changes`; `spectra connect` again, which claims Earth as Andy's.
32 42
  3. `spectra project add ~/development/oxygen` and `~/development/oxygenhub`.
33 -
  4. `spectra agent add claude-oxygen --kind claude-code --project oxygen --home ~/.oxy-agents/claude-oxygen`, then the printed `oxy identity delegate add …`; the same for `antigravity-oxygenhub`.
34 -
  5. Claude Code in oxygen: `claude mcp add spectra -- spectra mcp --agent claude-oxygen`; Antigravity in oxygenhub: the `mcp_config.json` line in the README.
35 -
  6. Both call `brief`, post in #general, and see each other's messages; watch on `spectra open`.
36 -
  7. `spectra admin verify`; then on a copy of the database, edit a row and see verify catch it.
43 +
  4. `spectra agent add claude-oxygen --kind claude-code --project oxygen --home ~/.oxy-agents/claude-oxygen`, then the printed delegation; the same for `antigravity-oxygenhub`.
44 +
  5. Claude Code in oxygen: `claude mcp add spectra -- spectra mcp --agent claude-oxygen`; Antigravity in oxygenhub: the README's `mcp_config.json` line.
45 +
  6. Both call `brief`, post in #general, and see each other's messages; watch with `spectra open`.
46 +
  7. `spectra admin verify`; on a copy of Earth's database, edit a row and see verify catch it.
37 47
  8. Cut 0.1.0 in CHANGELOG.md and tag it.
38 48
39 49
## Log
…
3 unchanged lines
43 53
- 2026-11-02 #4 done. Found and fixed while testing: a signed event's actor was not checked against its signer.
44 54
- 2026-11-02 Andy asked for an oxy-style CLI: every command now reports through oxy's ui module. #5 done.
45 55
- 2026-11-02 #11 and #12 done. Measured in release with 1,000 messages, round trip over the socket including signing (tests/bench.rs): brief 2.7 ms (target 5), say 3.1 ms (target 10), wait wakes 4.2 ms after a mention (target 100). A single ledger append 0.15 ms. 58 tests. #13 is ready for Andy.
56 +
- 2026-11-03 Sprint 2 set up: Andy wants the daemon on Earth and the rest on his workstation. Spec Draft 0.4 adds client and server (§8.6); #20–#25 opened, #13 moved and rewritten for two machines. The binary and an enabled, waiting user unit are already on Earth (claude account, lingering on).
modified docs/spec.md +34 −8
…
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