OXYGENHUB

README quick start, a systemd user unit, and the spec §24 measurements; #13 ready for Andy 98a16830a3d3

Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key b38c03be094b
When
· 2026-11-02 23:31 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
98a16830a3d3fd27a7f8c2a9065f83a2d0b8fe7a780d4e11ec9a30ae19924ee9
Depends on
200875410937 258bb3824813 2fc88a7b3a5c 36537112fcac 8dc6df8c4d4e da84e98f104e
Depended on by
862f0db224aa 98a0fb9f0948 aa72eb904932 b576d05cf862 d17cbc7069ce
In bookmarks
main
In tags
v0.1.0
Edits
17 atoms, 3 tree operations, 5 files

Browse the tree at this change

added packaging/spectra.service +21 −0
1 +
# Spectra as a systemd user service (spec §22.3): agents reconnect to it as
2 +
# they start, so it should outlive any one terminal.
3 +
#
4 +
#   cp packaging/spectra.service ~/.config/systemd/user/
5 +
#   systemctl --user daemon-reload && systemctl --user enable --now spectra
6 +
#   journalctl --user -u spectra -f
7 +
[Unit]
8 +
Description=Spectra, the control plane for AI coding agents
9 +
After=default.target
10 +
11 +
[Service]
12 +
ExecStart=%h/.local/bin/spectra serve
13 +
Restart=on-failure
14 +
RestartSec=2
15 +
# SIGTERM writes a checkpoint and stops within five seconds.
16 +
KillSignal=SIGTERM
17 +
TimeoutStopSec=15
18 +
Environment=SPECTRA_LOG=info
19 +
20 +
[Install]
21 +
WantedBy=default.target
added tests/bench.rs +70 −0
1 +
//! The targets of spec §24, measured: `brief` and `say` with a thousand
2 +
//! messages in the ledger, and how soon `wait` wakes after a mention. Run
3 +
//! in release, by hand:
4 +
//!
5 +
//! `cargo test --release --test bench -- --ignored --nocapture`
6 +
7 +
mod common;
8 +
9 +
use common::{add_agent, setup, Serving};
10 +
use serde_json::json;
11 +
use spectra_mcp::client::{rpc_result, Client};
12 +
use std::time::{Duration, Instant};
13 +
14 +
async fn call(c: &Client, name: &str, args: serde_json::Value) -> serde_json::Value {
15 +
    let req = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": name, "arguments": args } });
16 +
    rpc_result(&c.mcp("2025-11-25", &req).await.unwrap()).unwrap()
17 +
}
18 +
19 +
fn median(mut v: Vec<Duration>) -> Duration {
20 +
    v.sort();
21 +
    v[v.len() / 2]
22 +
}
23 +
24 +
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
25 +
#[ignore]
26 +
async fn spec_24_targets() {
27 +
    let dir = setup();
28 +
    let d = dir.path();
29 +
    let a = add_agent(d, "bench-a", &[]);
30 +
    let b = add_agent(d, "bench-b", &[]);
31 +
    let srv = Serving::start(d);
32 +
    let (ca, cb) = (Client::new(&srv.socket, Some(a)), Client::new(&srv.socket, Some(b)));
33 +
    for i in 0..1_000 {
34 +
        call(&ca, "say", json!({ "body": format!("message {i} about the pin bump"), "channel": "general" })).await;
35 +
    }
36 +
    let mut brief = Vec::new();
37 +
    let mut say = Vec::new();
38 +
    for i in 0..50 {
39 +
        let t = Instant::now();
40 +
        call(&ca, "brief", json!({})).await;
41 +
        brief.push(t.elapsed());
42 +
        let t = Instant::now();
43 +
        call(&ca, "say", json!({ "body": format!("timed {i}") })).await;
44 +
        say.push(t.elapsed());
45 +
    }
46 +
    let mut wake = Vec::new();
47 +
    for i in 0..10 {
48 +
        let w = tokio::spawn({
49 +
            let cb = Client::new(&srv.socket, Some(spectra_core::keys::KeyPair::load_home(&d.join("agents/bench-b")).unwrap()));
50 +
            async move {
51 +
                call(&cb, "wait", json!({ "timeout_s": 20 })).await;
52 +
                Instant::now()
53 +
            }
54 +
        });
55 +
        tokio::time::sleep(Duration::from_millis(200)).await;
56 +
        let sent = Instant::now();
57 +
        call(&ca, "say", json!({ "body": format!("@bench-b wake {i}"), "channel": "general" })).await;
58 +
        wake.push(w.await.unwrap().saturating_duration_since(sent));
59 +
        call(&cb, "ack", json!({ "ids": [] })).await;
60 +
        let inbox = call(&cb, "inbox", json!({})).await;
61 +
        let ids: Vec<serde_json::Value> =
62 +
            inbox["result"]["structuredContent"]["items"].as_array().unwrap().iter().map(|i| i["id"].clone()).collect();
63 +
        call(&cb, "ack", json!({ "ids": ids })).await;
64 +
    }
65 +
    println!("spec §24 with 1,000 messages, round trip over the socket including signing:");
66 +
    println!("  brief  median {:?}", median(brief));
67 +
    println!("  say    median {:?}", median(say));
68 +
    println!("  wait   wakes {:?} after the mention is sent (median)", median(wake));
69 +
    srv.stop();
70 +
}
modified CHANGELOG.md +1 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
- **Added: the README's quick start, a systemd user unit (`packaging/spectra.service`) and `tests/bench.rs`** for the targets of spec §24 (brief 2.7 ms, say 3.1 ms, wait wakes 4.2 ms after a mention, with 1,000 messages, in release).
5 6
- **Added: the human at a terminal (#12).** `spectra say [-c CHANNEL] [--reply-to ID] BODY`, `spectra inbox [--follow]`, `spectra search QUERY [--channel] [--author] [--since 7d]` and `spectra api METHOD PATH [-d JSON]`, each signing with the owner's own key: through the daemon when it is serving, else straight into the ledger. Every listing takes `--json` (one document); `say` and `inbox --follow` make a terminal chat with the agents.
6 7
- **Added: the JSON API and its TypeScript types (#11).** `/api/v1` has health, version, brief, agents, projects, channels, a channel's messages (read and post), threads, the inbox and ack, search, and the event log, which follows live as SSE (`?follow=1`). Requests are signed, or a read carries the owner's Radar session; refusals are RFC 7807 problems; lists page with `?after=`; reads carry an ETag. `crates/spectra-api` holds the types, and `make ts` writes `ts/index.d.ts` for the editor extensions.
7 8
- **Fixed: a client hanging up mid-request could stop the ledger saving anything.** A request cancelled while its events were being written left a pooled database connection inside an open transaction, and every later write on that connection was lost when the daemon stopped. Every ledger write now runs to completion in a task of its own; a regression test abandons 300 appends mid-flight and finds them all.
…
11 unchanged lines
modified README.md +66 −8
1 1
# Spectra
2 2
3 -
Spectra is a control plane for AI coding agents, written in Rust. Several agents on one machine (Claude Code in one checkout, Antigravity in another) connect to one `spectra` process over the Model Context Protocol to find each other, talk in persistent channels, hand work over as typed tasks, keep to versioned house rules, search what was said and decided, and let the human watch all of it live. See [docs/spec.md](docs/spec.md) for the specification.
3 +
[![OxygenFlow](https://oxygenhub.io/andymccall/spectra/flow/badge.svg)](https://oxygenhub.io/andymccall/spectra/flow)
4 4
5 -
The hosted, multi-machine, multi-person layer is [SpectraHub](../spectrahub); the editor integrations are [spectra-vscode](../spectra-vscode) and [spectrahub-vscode](../spectrahub-vscode). The split mirrors Oxygen and OxygenHub.
5 +
Spectra is a control plane for AI coding agents, written in Rust. Several agents on one machine (Claude Code in one checkout, Antigravity in another, Kimi Code in a third) connect to one `spectra` process over the Model Context Protocol to find each other, talk in persistent channels, keep to the house rules, search what was said, and let you watch all of it live. See [docs/spec.md](docs/spec.md) for the specification.
6 6
7 +
Every agent is its owner's Oxygen delegate: its key is listed in your identity with a scope and an expiry, so "claude-oxygen · Andy McCall's agent" is a fact Spectra checks, not a label. Every request is signed, every event is chained into a tamper-evident ledger with checkpoints signed by this machine's key, and `spectra admin verify` checks all of it from the ledger alone.
8 +
9 +
The hosted, multi-machine layer is [SpectraHub](../spectrahub); the editor integrations are [spectra-vscode](../spectra-vscode) and [spectrahub-vscode](../spectrahub-vscode). The split mirrors Oxygen and OxygenHub.
10 +
7 11
## Status
12 +
13 +
0.1, Talk (Sprint 1): the ledger, owned agents with signed requests, channels, threads, the inbox and search, the MCP server over a Unix socket with the stdio shim, Radar, the JSON API, and the command line. Tasks, handoffs and the passkey are 0.2; see the [board](https://oxygenhub.io/projects/andymccall/7).
14 +
15 +
## Building
16 +
17 +
```
18 +
cargo build --release
19 +
make check            # changelog, deps-check, fmt, clippy -D warnings, tests
20 +
make install          # release build with its build identity, linked into ~/.local/bin
21 +
```
22 +
23 +
`make deps-check` keeps every crate on a version the OxygenFlow build image vendors, so CI builds with no network; after adding a dependency, `scripts/deps-pin.sh` moves new crates onto vendored versions.
24 +
25 +
**Changelog:** a commit that changes code adds a line under `## Unreleased` in CHANGELOG.md (Added, Changed, Fixed or Security, with the issue number); versions change only at release. `scripts/changelog-check.sh` checks it and runs in the gate; `NO_CHANGELOG=1` for code changes users won't notice.
26 +
27 +
## Quick start
28 +
29 +
You need an Oxygen identity of your own (`oxy identity create`). Then:
30 +
31 +
```
32 +
spectra init                                   # records your identity, makes this machine's key
33 +
oxy identity delegate add ~/.local/share/spectra/keys/instance.identity --scope changes
34 +
spectra serve                                  # or: cp packaging/spectra.service ~/.config/systemd/user/
35 +
spectra project add ~/development/oxygen
36 +
spectra agent add claude-oxygen --kind claude-code --project oxygen --home ~/.oxy-agents/claude-oxygen
37 +
```
8 38
9 -
Specification, Draft 0.1. Nothing is implemented yet.
39 +
`agent add` prints the one command that makes the agent yours and waits for it:
10 40
11 -
## Layout (planned)
41 +
```
42 +
oxy identity delegate add ~/.oxy-agents/claude-oxygen/keys/<KEY>.identity --scope changes --expires 8h
43 +
```
12 44
45 +
Connect the agent's client with one line and nothing secret in it:
46 +
47 +
```
48 +
claude mcp add spectra -- spectra mcp --agent claude-oxygen            # Claude Code
49 +
kimi mcp add spectra -- spectra mcp --agent kimi-oxygenflow            # Kimi Code
50 +
```
51 +
52 +
Antigravity, in `~/.gemini/config/mcp_config.json`:
53 +
54 +
```json
55 +
{ "mcpServers": { "spectra": { "command": "spectra", "args": ["mcp", "--agent", "antigravity-oxygenhub"] } } }
56 +
```
57 +
58 +
Open Radar with `spectra open`. From a terminal: `spectra say "…"`, `spectra inbox --follow`, `spectra search "pin bump"`, `spectra agent list`, and `spectra admin verify`.
59 +
60 +
Output uses colour and glyphs on a UTF-8 terminal, like `oxy`'s, and falls back to plain ASCII when piped, with `--plain`, or with `NO_COLOR`.
61 +
62 +
## Layout
63 +
13 64
| Path | Contents |
14 65
|---|---|
15 -
| `crates/spectra-core` | The model: event log, projections, bus, rules, tasks, claims, search, scheduler |
16 -
| `crates/spectra-mcp` | The MCP server (stateless Streamable HTTP and the stdio shim) |
17 -
| `crates/spectra-api` | JSON API types shared with SpectraHub and the extensions |
18 -
| `crates/radar` | The live web interface, linked by `spectra` and by `spectrahub` |
66 +
| `crates/spectra-core` | The model: the ledger, identities and principals, conversation, presence, rules, the daemon's state |
67 +
| `crates/spectra-mcp` | Tuner (MCP over the Unix socket and loopback), the JSON API, the signing client and the browser sessions |
68 +
| `crates/spectra-api` | The JSON API's types; `make ts` writes `ts/index.d.ts` for the extensions |
69 +
| `crates/radar` | Radar, the owner's web pages, with its templates and assets |
19 70
| `src/cli` | The `spectra` command line |
71 +
| `deps/oxygen` | Oxygen, pinned: identities, signatures, CBOR and hashing come from `oxy-core` |
20 72
| `docs/spec.md` | The specification |
73 +
| `docs/plan.md` | The sprint plan and its log |
74 +
| `packaging/spectra.service` | The systemd user unit |
75 +
76 +
## Licence
77 +
78 +
MIT or Apache-2.0, at your option (see `LICENSE`).
modified docs/plan.md +10 −1
…
25 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)
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`.
32 +
  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.
37 +
  8. Cut 0.1.0 in CHANGELOG.md and tag it.
30 38
31 39
## Log
32 40
…
1 unchanged lines
34 42
- 2026-11-02 #1 and #2 done (CI green on run #1); #3 done. Root disk was full: build trees go to /mnt/development/spectra-target (CARGO_TARGET_DIR).
35 43
- 2026-11-02 #4 done. Found and fixed while testing: a signed event's actor was not checked against its signer.
36 44
- 2026-11-02 Andy asked for an oxy-style CLI: every command now reports through oxy's ui module. #5 done.
45 +
- 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.