OXYGENHUB

Remove Radar: Spectra serves only signed requests (spec Draft 0.7) 862f0db224aa

Spectra has no web pages any more: the command line and the VS Code
extension are how people use it, and SpectraHub will be the website.
Radar, the browser sign-in sessions, the loopback proxy, spectra open
and serve --open are gone. Radar's last code (with the Constellation)
is in 6b914945 for SpectraHub.

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 701b9fa70303
When
· 2026-11-03 20:12 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
862f0db224aa804b21e54af40cadff0e1aea2f21ed5572f45c7d476410f1fdce
Depends on
01320eefd505 200875410937 258bb3824813 2fc88a7b3a5c 36537112fcac 39f5c913f617 571572f5fb2c 58e1ce30ae58 67bfa06a894f 77df591d5a2f 8cfb9269bb14 8d803d858c8c 8dc6df8c4d4e 98a0fb9f0948 98a16830a3d3 aa72eb904932 ad258dc8cf5e b044d90930d9 b576d05cf862 c7161ec1a607 d17cbc7069ce e071e91a14ef eb675dfa5e18 f7986cb07b0e fe6b29c66cd4
Depended on by
1876f2ce71e4
In bookmarks
main
Edits
64 atoms, 22 tree operations, 36 files

Browse the tree at this change

removed crates/radar +0 −0

No line changes.

removed crates/radar/Cargo.toml +0 −0
…
24 unchanged lines
removed crates/radar/assets +0 −0

No line changes.

removed crates/radar/assets/constellation.js +0 −0
…
179 unchanged lines
removed crates/radar/assets/radar.js +0 −0
…
17 unchanged lines
removed crates/radar/assets/spectra.svg +0 −0
…
13 unchanged lines
removed crates/radar/assets/style.css +0 −0
…
98 unchanged lines
removed crates/radar/src +0 −0

No line changes.

removed crates/radar/src/lib.rs +0 −0
…
896 unchanged lines
removed crates/radar/templates +0 −0

No line changes.

removed crates/radar/templates/base.html +0 −0
…
31 unchanged lines
removed crates/radar/templates/briefs.html +0 −0
…
23 unchanged lines
removed crates/radar/templates/channel.html +0 −0
…
36 unchanged lines
removed crates/radar/templates/channels.html +0 −0
…
10 unchanged lines
removed crates/radar/templates/constellation.html +0 −0
…
24 unchanged lines
removed crates/radar/templates/live.html +0 −0
…
28 unchanged lines
removed crates/radar/templates/log.html +0 −0
…
31 unchanged lines
removed crates/radar/templates/signin.html +0 −0
…
12 unchanged lines
removed crates/spectra-mcp/src/proxy.rs +0 −0
…
90 unchanged lines
removed crates/spectra-mcp/src/session.rs +0 −0
…
86 unchanged lines
removed src/cli/open.rs +0 −0
…
83 unchanged lines
removed tests/radar.rs +0 −0
…
118 unchanged lines
modified .flow/pipeline.kdl +3 −3
…
37 unchanged lines
38 38
// formatting, lints and tests (as in oxygenhub).
39 39
job "fmt" {
40 40
    sandbox "rust-small"
41 -
    exec "cargo" "fmt" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "-p" "radar" "--check"
41 +
    exec "cargo" "fmt" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "--check"
42 42
}
43 43
44 44
job "clippy" {
…
13 unchanged lines
58 58
    env {
59 59
        SPECTRA_BUILD "ci"
60 60
    }
61 -
    exec "cargo" "clippy" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "-p" "radar" "--all-targets" "--locked" "--" "-D" "warnings"
61 +
    exec "cargo" "clippy" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "--all-targets" "--locked" "--" "-D" "warnings"
62 62
}
63 63
64 64
// The tests, run under cargo-nextest and cargo-llvm-cov: a test report
…
12 unchanged lines
77 77
    env {
78 78
        SPECTRA_BUILD "ci"
79 79
    }
80 -
    exec "cargo" "llvm-cov" "nextest" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "-p" "radar" "--locked" "--profile" "ci" "--lcov" "--output-path" "/src/target/lcov.info"
80 +
    exec "cargo" "llvm-cov" "nextest" "-p" "spectra" "-p" "spectra-core" "-p" "spectra-mcp" "-p" "spectra-api" "--locked" "--profile" "ci" "--lcov" "--output-path" "/src/target/lcov.info"
81 81
    output {
82 82
        artifact "junit" {
83 83
            path "/src/target/nextest/ci/junit.xml"
…
27 unchanged lines
modified CHANGELOG.md +2 −4
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 -
- **Changed: replies show under their message in Radar's channel view,** the latest three with a link to any earlier ones and to reply in the thread, instead of behind a "1 reply" link.
5 +
- **Removed: Radar, `spectra open` and `spectra serve --open`.** Spectra has no web interface any more: you use it through the command line and the VS Code extension, and SpectraHub will be the website. The daemon now serves only signed requests, with no browser sessions, cookies or proxy. The Constellation moves to SpectraHub.
6 6
- **Changed: a mention can be part of an agent's name.** `@kimi` reaches `kimi-bubblebobble` and `@metalgear2` reaches `claude-metalgear2`, when exactly one agent has that part; a part several agents share reaches nobody, so a mention never goes to the wrong agent.
7 7
- **Added: `spectra agent allow NAME --channel CHANNEL [--remove]`.** The owner lets one agent post in another project's channel, and can take it back; it is a signed `agent.scoped` event that no agent can make for itself. An agent refused a channel is told this command.
8 8
- **Added: `spectra mcp --auto [--kind KIND]`, one MCP entry for every folder.** The shim speaks for the agent whose project folder the client started in, at any depth inside it, so a client needs Spectra added once (`claude mcp add --scope user spectra -- spectra mcp --auto`) rather than once per folder. Each folder still signs as its own agent. `spectra agent add` labels the agent's home with an `agent.toml` for this, and older homes are labelled on first use. A folder with no agent gets a quiet server whose instructions say how to add one, instead of an error.
9 -
- **Added: `spectra open --page constellation`** (or `briefs`, `channels`, `log`) lands on that page once signed in; only Radar's own pages are accepted.
10 9
- **Added: `spectra status`, `spectra probe` and `spectra bridge` (#31).** `status [--json]` says what is set up here: your identity, and the Spectra you use and whether it answers. `probe URL [--json]` says what a server is (its name, instance key and owner, or the identity it waits for) without trusting it. `bridge` serves a loopback address that signs requests as you, for a program that cannot sign itself: it prints its address and a one-run token, refuses anything without the token or from a web page, passes streams through, and ends when its input closes. `identity create --json` prints the new key. This is how the VS Code extension sets Spectra up and talks to it.
11 -
- **Added: briefs (#29).** An overall brief that every agent gets and a brief for each project that its agents get too, set with `spectra brief set [--project P] TEXT` (or `--file`, or `spectra brief edit` in your editor), on Radar's new Briefs page, or with `PUT /api/v1/briefs`. Each change is signed into the ledger; an agent is told on its next call that its brief changed, and only the agents a change concerns are told. A project's channel shows its brief. Briefs replace the house-rules file (`rules/site.md`), which is no longer read.
12 -
- **Added: Radar's Constellation (#30).** A live picture: projects as prisms linked by what depends on what, agents orbiting their project in the colour of their presence, you at the centre. Every message is a beam of light from its author to its channel, tool calls pulse, guard trips flare red. Plain SVG and script under Radar's security policy, a still picture without JavaScript, and calm when the system asks for reduced motion.
10 +
- **Added: briefs (#29).** An overall brief that every agent gets and a brief for each project that its agents get too, set with `spectra brief set [--project P] TEXT` (or `--file`, or `spectra brief edit` in your editor) or or with `PUT /api/v1/briefs`. Each change is signed into the ledger; an agent is told on its next call that its brief changed, and only the agents a change concerns are told. Briefs replace the house-rules file (`rules/site.md`), which is no longer read.
13 11
## 0.1.0 (2026-11-03): Talk
14 12
15 13
The first release. Accepted on two machines: the daemon on Earth, the owner, Claude Code in `oxygen` and Antigravity in `oxygenhub` on the workstation, each agent its owner's delegate, talking in #general; the ledger verified, and an edited copy caught.
…
26 unchanged lines
modified Cargo.lock +0 −272
…
17 unchanged lines
18 18
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
19 19
20 20
[[package]]
21 -
name = "ammonia"
22 -
version = "4.2.0"
23 -
source = "registry+https://github.com/rust-lang/crates.io-index"
24 -
checksum = "3f7ed3ac4252be4d3e36fb589ed5659356842587b21a31596a9dc89d3b2f5e2a"
25 -
dependencies = [
26 -
 "cssparser",
27 -
 "html5ever",
28 -
 "maplit",
29 -
 "url",
30 -
]
31 -
32 -
[[package]]
33 21
name = "anstream"
34 22
version = "1.0.0"
35 23
source = "registry+https://github.com/rust-lang/crates.io-index"
…
54 unchanged lines
90 78
version = "0.7.8"
91 79
source = "registry+https://github.com/rust-lang/crates.io-index"
92 80
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
93 -
94 -
[[package]]
95 -
name = "askama"
96 -
version = "0.14.0"
97 -
source = "registry+https://github.com/rust-lang/crates.io-index"
98 -
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
99 -
dependencies = [
100 -
 "askama_derive",
101 -
 "itoa",
102 -
 "percent-encoding",
103 -
 "serde",
104 -
 "serde_json",
105 -
]
106 -
107 -
[[package]]
108 -
name = "askama_derive"
109 -
version = "0.14.0"
110 -
source = "registry+https://github.com/rust-lang/crates.io-index"
111 -
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
112 -
dependencies = [
113 -
 "askama_parser",
114 -
 "basic-toml",
115 -
 "memchr",
116 -
 "proc-macro2",
117 -
 "quote",
118 -
 "rustc-hash",
119 -
 "serde",
120 -
 "serde_derive",
121 -
 "syn 2.0.119",
122 -
]
123 -
124 -
[[package]]
125 -
name = "askama_parser"
126 -
version = "0.14.0"
127 -
source = "registry+https://github.com/rust-lang/crates.io-index"
128 -
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
129 -
dependencies = [
130 -
 "memchr",
131 -
 "serde",
132 -
 "serde_derive",
133 -
 "winnow",
134 -
]
135 81
136 82
[[package]]
137 83
name = "asn1-rs"
…
136 unchanged lines
274 220
version = "1.8.3"
275 221
source = "registry+https://github.com/rust-lang/crates.io-index"
276 222
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
277 -
278 -
[[package]]
279 -
name = "basic-toml"
280 -
version = "0.1.10"
281 -
source = "registry+https://github.com/rust-lang/crates.io-index"
282 -
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
283 -
dependencies = [
284 -
 "serde",
285 -
]
286 223
287 224
[[package]]
288 225
name = "bit-vec"
…
231 unchanged lines
520 457
]
521 458
522 459
[[package]]
523 -
name = "cssparser"
524 -
version = "0.38.0"
525 -
source = "registry+https://github.com/rust-lang/crates.io-index"
526 -
checksum = "11119743ad110e8c1bdccd930d7f5c30c99e5fc76a7b63ec9807e84eef0c5f59"
527 -
dependencies = [
528 -
 "dtoa-short",
529 -
 "itoa",
530 -
 "smallvec",
531 -
]
532 -
533 -
[[package]]
534 460
name = "ctutils"
535 461
version = "0.4.2"
536 462
source = "registry+https://github.com/rust-lang/crates.io-index"
…
114 unchanged lines
651 577
version = "0.15.7"
652 578
source = "registry+https://github.com/rust-lang/crates.io-index"
653 579
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
654 -
655 -
[[package]]
656 -
name = "dtoa"
657 -
version = "1.0.11"
658 -
source = "registry+https://github.com/rust-lang/crates.io-index"
659 -
checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
660 -
661 -
[[package]]
662 -
name = "dtoa-short"
663 -
version = "0.3.5"
664 -
source = "registry+https://github.com/rust-lang/crates.io-index"
665 -
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
666 -
dependencies = [
667 -
 "dtoa",
668 -
]
669 580
670 581
[[package]]
671 582
name = "dyn-clone"
…
326 unchanged lines
998 909
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
999 910
dependencies = [
1000 911
 "windows-sys 0.61.2",
1001 -
]
1002 -
1003 -
[[package]]
1004 -
name = "html5ever"
1005 -
version = "0.40.1"
1006 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1007 -
checksum = "456a1a377e608e555d22ddab27ac0114bc7a7b4199078108e34c2aeae6c9b130"
1008 -
dependencies = [
1009 -
 "log",
1010 -
 "markup5ever",
1011 -
 "memchr",
1012 912
]
1013 913
1014 914
[[package]]
…
328 unchanged lines
1343 1243
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
1344 1244
1345 1245
[[package]]
1346 -
name = "maplit"
1347 -
version = "1.0.2"
1348 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1349 -
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
1350 -
1351 -
[[package]]
1352 -
name = "markup5ever"
1353 -
version = "0.40.0"
1354 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1355 -
checksum = "0ab3dc68ac4a0f5719e560136778c1ee716e296030d75dbd4484e37e39e3a842"
1356 -
dependencies = [
1357 -
 "log",
1358 -
 "tendril",
1359 -
 "web_atoms",
1360 -
]
1361 -
1362 -
[[package]]
1363 1246
name = "matchers"
1364 1247
version = "0.2.0"
1365 1248
source = "registry+https://github.com/rust-lang/crates.io-index"
…
84 unchanged lines
1450 1333
]
1451 1334
1452 1335
[[package]]
1453 -
name = "new_debug_unreachable"
1454 -
version = "1.0.6"
1455 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1456 -
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
1457 -
1458 -
[[package]]
1459 1336
name = "nom"
1460 1337
version = "7.1.3"
1461 1338
source = "registry+https://github.com/rust-lang/crates.io-index"
…
153 unchanged lines
1615 1492
version = "2.3.2"
1616 1493
source = "registry+https://github.com/rust-lang/crates.io-index"
1617 1494
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1618 -
1619 -
[[package]]
1620 -
name = "phf"
1621 -
version = "0.14.0"
1622 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1623 -
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
1624 -
dependencies = [
1625 -
 "phf_shared",
1626 -
 "serde",
1627 -
]
1628 -
1629 -
[[package]]
1630 -
name = "phf_codegen"
1631 -
version = "0.14.0"
1632 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1633 -
checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe"
1634 -
dependencies = [
1635 -
 "phf_generator",
1636 -
 "phf_shared",
1637 -
]
1638 -
1639 -
[[package]]
1640 -
name = "phf_generator"
1641 -
version = "0.14.0"
1642 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1643 -
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
1644 -
dependencies = [
1645 -
 "fastrand",
1646 -
 "phf_shared",
1647 -
]
1648 1495
1649 1496
[[package]]
1650 -
name = "phf_shared"
1651 -
version = "0.14.0"
1652 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1653 -
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
1654 -
dependencies = [
1655 -
 "siphasher",
1656 -
]
1657 -
1658 -
[[package]]
1659 1497
name = "pin-project-lite"
1660 1498
version = "0.2.17"
1661 1499
source = "registry+https://github.com/rust-lang/crates.io-index"
…
71 unchanged lines
1733 1571
dependencies = [
1734 1572
 "zerocopy",
1735 1573
]
1736 -
1737 -
[[package]]
1738 -
name = "precomputed-hash"
1739 -
version = "0.1.1"
1740 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1741 -
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
1742 1574
1743 1575
[[package]]
1744 1576
name = "proc-macro2"
…
5 unchanged lines
1750 1582
]
1751 1583
1752 1584
[[package]]
1753 -
name = "pulldown-cmark"
1754 -
version = "0.13.4"
1755 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1756 -
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
1757 -
dependencies = [
1758 -
 "bitflags",
1759 -
 "memchr",
1760 -
 "pulldown-cmark-escape",
1761 -
 "unicase",
1762 -
]
1763 -
1764 -
[[package]]
1765 -
name = "pulldown-cmark-escape"
1766 -
version = "0.11.0"
1767 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1768 -
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
1769 -
1770 -
[[package]]
1771 1585
name = "quote"
1772 1586
version = "1.0.47"
1773 1587
source = "registry+https://github.com/rust-lang/crates.io-index"
…
15 unchanged lines
1789 1603
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1790 1604
1791 1605
[[package]]
1792 -
name = "radar"
1793 -
version = "0.1.0"
1794 -
dependencies = [
1795 -
 "ammonia",
1796 -
 "anyhow",
1797 -
 "askama",
1798 -
 "axum",
1799 -
 "futures",
1800 -
 "hex",
1801 -
 "pulldown-cmark",
1802 -
 "rand",
1803 -
 "serde",
1804 -
 "serde_json",
1805 -
 "spectra-core",
1806 -
 "spectra-mcp",
1807 -
 "sqlx",
1808 -
 "tokio",
1809 -
 "tokio-stream",
1810 -
 "tracing",
1811 -
]
1812 -
1813 -
[[package]]
1814 1606
name = "rand"
1815 1607
version = "0.8.8"
1816 1608
source = "registry+https://github.com/rust-lang/crates.io-index"
…
133 unchanged lines
1950 1742
]
1951 1743
1952 1744
[[package]]
1953 -
name = "rustc-hash"
1954 -
version = "2.1.3"
1955 -
source = "registry+https://github.com/rust-lang/crates.io-index"
1956 -
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
1957 -
1958 -
[[package]]
1959 1745
name = "rustc_version"
1960 1746
version = "0.4.1"
1961 1747
source = "registry+https://github.com/rust-lang/crates.io-index"
…
290 unchanged lines
2252 2038
 "digest 0.11.3",
2253 2039
 "rand_core 0.10.1",
2254 2040
]
2255 -
2256 -
[[package]]
2257 -
name = "siphasher"
2258 -
version = "1.0.4"
2259 -
source = "registry+https://github.com/rust-lang/crates.io-index"
2260 -
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
2261 2041
2262 2042
[[package]]
2263 2043
name = "slab"
…
32 unchanged lines
2296 2076
 "indicatif",
2297 2077
 "libc",
2298 2078
 "mimalloc",
2299 -
 "radar",
2300 2079
 "serde_json",
2301 2080
 "spectra-api",
2302 2081
 "spectra-core",
…
291 unchanged lines
2594 2373
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
2595 2374
2596 2375
[[package]]
2597 -
name = "string_cache"
2598 -
version = "0.11.0"
2599 -
source = "registry+https://github.com/rust-lang/crates.io-index"
2600 -
checksum = "ffa8a5dbe8b3f0bbe29d4c3225daafaeead63afdc1b65fc4c01a1384166038e6"
2601 -
dependencies = [
2602 -
 "new_debug_unreachable",
2603 -
 "parking_lot",
2604 -
 "phf_shared",
2605 -
 "precomputed-hash",
2606 -
]
2607 -
2608 -
[[package]]
2609 -
name = "string_cache_codegen"
2610 -
version = "0.11.2"
2611 -
source = "registry+https://github.com/rust-lang/crates.io-index"
2612 -
checksum = "928dcdf75e47626b3617a976ec205d9f057584c371c1f23b782129268d0e6edc"
2613 -
dependencies = [
2614 -
 "phf_generator",
2615 -
 "phf_shared",
2616 -
 "proc-macro2",
2617 -
 "quote",
2618 -
]
2619 -
2620 -
[[package]]
2621 2376
name = "stringprep"
2622 2377
version = "0.1.5"
2623 2378
source = "registry+https://github.com/rust-lang/crates.io-index"
…
80 unchanged lines
2704 2459
]
2705 2460
2706 2461
[[package]]
2707 -
name = "tendril"
2708 -
version = "0.5.1"
2709 -
source = "registry+https://github.com/rust-lang/crates.io-index"
2710 -
checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
2711 -
dependencies = [
2712 -
 "new_debug_unreachable",
2713 -
]
2714 -
2715 -
[[package]]
2716 2462
name = "thiserror"
2717 2463
version = "2.0.21"
2718 2464
source = "registry+https://github.com/rust-lang/crates.io-index"
…
274 unchanged lines
2993 2739
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
2994 2740
2995 2741
[[package]]
2996 -
name = "unicase"
2997 -
version = "2.9.0"
2998 -
source = "registry+https://github.com/rust-lang/crates.io-index"
2999 -
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
3000 -
3001 -
[[package]]
3002 2742
name = "unicode-bidi"
3003 2743
version = "0.3.18"
3004 2744
source = "registry+https://github.com/rust-lang/crates.io-index"
…
157 unchanged lines
3162 2902
dependencies = [
3163 2903
 "js-sys",
3164 2904
 "wasm-bindgen",
3165 -
]
3166 -
3167 -
[[package]]
3168 -
name = "web_atoms"
3169 -
version = "0.3.0"
3170 -
source = "registry+https://github.com/rust-lang/crates.io-index"
3171 -
checksum = "7572660c8890448ba236b7376f27e389c6a7e1c70195622faced601f855c0ada"
3172 -
dependencies = [
3173 -
 "phf",
3174 -
 "phf_codegen",
3175 -
 "string_cache",
3176 -
 "string_cache_codegen",
3177 2905
]
3178 2906
3179 2907
[[package]]
…
329 unchanged lines
modified Cargo.toml +1 −3
1 1
[workspace]
2 -
members = ["crates/spectra-core", "crates/spectra-mcp", "crates/spectra-api", "crates/radar", "."]
2 +
members = ["crates/spectra-core", "crates/spectra-mcp", "crates/spectra-api", "."]
3 3
resolver = "2"
4 4
5 5
[workspace.package]
…
5 unchanged lines
11 11
spectra-core = { path = "crates/spectra-core" }
12 12
spectra-mcp = { path = "crates/spectra-mcp" }
13 13
spectra-api = { path = "crates/spectra-api" }
14 -
radar = { path = "crates/radar" }
15 14
16 15
# Versions as oxygenhub's workspace has them, so the vendored OxygenFlow
17 16
# image builds Spectra too (spectra#2; `make deps-check`).
…
37 unchanged lines
55 54
spectra-core.workspace = true
56 55
spectra-mcp.workspace = true
57 56
spectra-api.workspace = true
58 -
radar.workspace = true
59 57
anyhow.workspace = true
60 58
clap.workspace = true
61 59
serde_json.workspace = true
…
21 unchanged lines
modified Makefile +1 −1
…
1 unchanged lines
2 2
3 3
# Spectra's own packages. `--workspace` and `--all` also reach the pinned
4 4
# Oxygen in deps/ (a path dependency), whose tests and lints are its own.
5 -
PKGS = -p spectra -p spectra-core -p spectra-mcp -p spectra-api -p radar
5 +
PKGS = -p spectra -p spectra-core -p spectra-mcp -p spectra-api
6 6
7 7
build:
8 8
	cargo build --release
…
39 unchanged lines
modified README.md +3 −4
…
9 unchanged lines
10 10
11 11
## Status
12 12
13 -
0.1, Talk: the ledger, owned agents with signed requests, channels, threads, the inbox and search, the MCP server with the stdio shim, Radar, the JSON API, the command line, and the daemon on another machine over TLS. Tasks, handoffs and the passkey are 0.2; see the [board](https://oxygenhub.io/projects/andymccall/7).
13 +
0.1, Talk: the ledger, owned agents with signed requests, channels, threads, the inbox and search, the MCP server with the stdio shim, the JSON API, the command line, the VS Code extension, and the daemon on another machine over TLS. Tasks, handoffs and the passkey are 0.2; see the [board](https://oxygenhub.io/projects/andymccall/7).
14 14
15 15
## Building
16 16
…
35 unchanged lines
52 52
{ "mcpServers": { "spectra": { "command": "spectra", "args": ["mcp", "--agent", "antigravity-index"] } } }
53 53
```
54 54
55 -
Open Radar with `spectra open`. From a terminal: `spectra say "…"`, `spectra inbox --follow`, `spectra search "chapter 3"`, `spectra agent list`, and `spectra admin verify`.
55 +
Watch and chat from the [VS Code extension](../spectra-vscode), or from a terminal: `spectra say "…"`, `spectra inbox --follow`, `spectra search "chapter 3"`, `spectra agent list`, and `spectra admin verify`.
56 56
57 57
## The daemon on another machine
58 58
…
22 unchanged lines
81 81
spectra connect https://earth.example:7350     # shows the instance key; confirm it matches
82 82
```
83 83
84 -
From then on every command on the workstation works against the server, with no ledger file locally. Agents name their home, since it is on the workstation: `spectra mcp --agent claude-book --home ~/.spectra-agents/claude-book`. `spectra open` serves Radar on `http://127.0.0.1:7350` through a proxy to the server.
84 +
From then on every command on the workstation works against the server, with no ledger file locally. Agents name their home, since it is on the workstation: `spectra mcp --agent claude-book --home ~/.spectra-agents/claude-book`.
85 85
86 86
Output uses colour and glyphs on a UTF-8 terminal and falls back to plain ASCII when piped, with `--plain`, or with `NO_COLOR`.
87 87
…
4 unchanged lines
92 92
| `crates/spectra-core` | The model: the ledger, Spectra's own identities, signatures and CBOR, principals, conversation, presence, rules, the daemon's state |
93 93
| `crates/spectra-mcp` | Tuner (MCP over the Unix socket and loopback), the JSON API, the signing client and the browser sessions |
94 94
| `crates/spectra-api` | The JSON API's types; `make ts` writes `ts/index.d.ts` for the extensions |
95 -
| `crates/radar` | Radar, the owner's web pages, with its templates and assets |
96 95
| `src/cli` | The `spectra` command line |
97 96
| `docs/spec.md` | The specification |
98 97
| `docs/plan.md` | The sprint plan and its log |
…
5 unchanged lines
modified crates/spectra-mcp/src/api.rs +3 −25
…
16 unchanged lines
17 17
use spectra_api as api;
18 18
use spectra_core::authn::Caller;
19 19
use spectra_core::conversation::{self as conv, Author, Post, Posted, Signer};
20 -
use spectra_core::event::{Auth, Method};
21 20
use spectra_core::ids::now_ms;
22 21
use spectra_core::principals::Principal;
23 22
use std::time::Duration;
…
43 unchanged lines
67 66
    hex::encode(*spectra_core::event::hash_body_plain(b))
68 67
}
69 68
70 -
/// Who is asking: a signed request, or for a read the owner's session.
69 +
/// Who is asking: every request is signed (spec §8.5).
71 70
async fn caller(
72 71
    st: &AppState,
73 72
    peer: PeerInfo,
…
2 unchanged lines
76 75
    body: &[u8],
77 76
    read: bool,
78 77
) -> Result<Caller, Response> {
79 -
    if read && headers.get("spectra-key").is_none() {
80 -
        let cookie = headers.get(header::COOKIE).and_then(|c| c.to_str().ok()).unwrap_or("");
81 -
        let id = cookie.split(';').map(str::trim).find_map(|kv| kv.strip_prefix("spectra_session=")).unwrap_or("");
82 -
        if let Some(s) = st.sessions.get(id, now_ms()) {
83 -
            if !crate::http::origin_ok(headers, &st.origins) {
84 -
                return Err(problem(StatusCode::FORBIDDEN, "this origin may not read Spectra", None));
85 -
            }
86 -
            let key = spectra_core::identity::KeyId(s.key);
87 -
            let principal = st
88 -
                .daemon
89 -
                .principals
90 -
                .read()
91 -
                .await
92 -
                .principal(&key, now_ms())
93 -
                .map_err(|why| problem(StatusCode::UNAUTHORIZED, "not authenticated", Some(why)))?;
94 -
            return Ok(Caller {
95 -
                principal,
96 -
                auth: Auth { key: Some(s.key), method: Method::Session, assurance: 2, proof: Some(s.proof) },
97 -
                learn_uid: None,
98 -
            });
99 -
        }
100 -
    }
78 +
    let _ = read;
101 79
    let signed = headers.get("spectra-key").is_some();
102 80
    crate::http::authenticate_why(st, peer, headers, method_path, body).await.map_err(|(status, why)| {
103 81
        if status == StatusCode::FORBIDDEN {
…
5 unchanged lines
109 87
            problem(
110 88
                StatusCode::UNAUTHORIZED,
111 89
                "not authenticated",
112 -
                Some("sign the request with your key (spectra's client does), or open Radar with `spectra open`".into()),
90 +
                Some("sign the request with your key: spectra's own client, its shims and `spectra bridge` do".into()),
113 91
            )
114 92
        }
115 93
    })
…
487 unchanged lines
modified crates/spectra-mcp/src/http.rs +0 −2
…
50 unchanged lines
51 51
pub struct AppState {
52 52
    pub daemon: Arc<Daemon>,
53 53
    pub origins: Arc<Vec<String>>,
54 -
    /// The owner's browser sessions (Radar's sign-in link).
55 -
    pub sessions: Arc<crate::session::Sessions>,
56 54
}
57 55
58 56
/// Tuner's routes; the binary adds the API and Radar beside them.
…
207 unchanged lines
modified crates/spectra-mcp/src/lib.rs +0 −2
…
7 unchanged lines
8 8
pub mod http;
9 9
pub mod instance;
10 10
pub mod protocol;
11 -
pub mod proxy;
12 -
pub mod session;
13 11
pub mod tls;
14 12
pub mod tools;
15 13
…
10 unchanged lines
modified docs/plan.md +2 −1
…
57 unchanged lines
58 58
## Sprint 4: it just works
59 59
60 60
- [x] #29 Briefs — done 2026-11-03: brief.set events (overall and per project), compiled per agent with the change line, spectra brief show|set|edit, GET/PUT /api/v1/briefs, Radar's Briefs page and the brief on a project's channel; tests/briefs.rs.
61 -
- [x] #30 Constellation — done 2026-11-03: /constellation with a still SVG and constellation.js animating the stream (beams, pulses, flares, orbits by presence); checked in Chrome on a demo with five agents chattering.
61 +
- [x] #30 Constellation (moved to SpectraHub on 2026-11-03, when Radar was removed) — done 2026-11-03: /constellation with a still SVG and constellation.js animating the stream (beams, pulses, flares, orbits by presence); checked in Chrome on a demo with five agents chattering.
62 62
- [x] #31 bridge, probe, status — done 2026-11-03: spectra-mcp::bridge (token, loopback Host, no Origin, ends with stdin), spectra status --json, spectra probe --json, identity create --json; tests/bridge.rs and a probe check in tests/connect.rs.
63 63
- [ ] spectra-vscode#6 0.1 Just works: the walkthrough, the bridge client, chat, briefs.
64 64
- [ ] #32 Acceptance: a new project set up and chatting from VS Code alone.
…
8 unchanged lines
73 73
- 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).
74 74
- 2026-11-03 Andy: Spectra must be its own program, not tied to Oxygen or to programming work. Spec Draft 0.5; Sprint 3 opened with #26–#28; #13 moved again. Earth's first ledger (Oxygen identities) is set aside, not deleted.
75 75
- 2026-11-03 #13 accepted across two machines; 0.1.0 cut and tagged. Sprint 3 done.
76 +
- 2026-11-03 Radar removed (spec Draft 0.7): Spectra serves only signed requests; the editor is the local view and SpectraHub the website. Radar's last code is in commit 6b914945 (crates/radar), for SpectraHub's Constellation.
modified docs/spec.md +33 −59
…
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
modified src/cli/mod.rs +2 −28
…
11 unchanged lines
12 12
pub mod identity;
13 13
pub mod marks;
14 14
pub mod mcp;
15 -
pub mod open;
16 15
pub mod project;
17 16
pub mod serve;
18 17
pub mod setup;
…
98 unchanged lines
117 116
        /// Allow a non-loopback address in plain HTTP
118 117
        #[arg(long)]
119 118
        insecure: bool,
120 -
        /// Open Radar in the browser once serving (signs a link as the owner)
121 -
        #[arg(long)]
122 -
        open: bool,
123 119
    },
124 -
    /// Sign in to Radar: a one-time link signed with the owner's key
125 -
    Open {
126 -
        #[arg(long)]
127 -
        owner_home: Option<PathBuf>,
128 -
        /// Print the link without opening a browser
129 -
        #[arg(long)]
130 -
        no_browser: bool,
131 -
        /// With Spectra on another machine: the loopback port of the proxy
132 -
        /// Radar is served through (spec §8.6)
133 -
        #[arg(long, default_value_t = 7350)]
134 -
        port: u16,
135 -
        /// The page to land on: live, constellation, briefs, channels or log
136 -
        #[arg(long)]
137 -
        page: Option<String>,
138 -
    },
139 120
    /// The stdio MCP server for one agent: signs with its key and forwards to the daemon
140 121
    Mcp {
141 122
        /// The agent it speaks for
…
495 unchanged lines
637 618
                IdentityCmd::Refresh { owner_home } => identity::refresh(&ctx, owner_home).await,
638 619
            }
639 620
        }),
640 -
        Command::Serve { bind, tls, insecure, open } => {
621 +
        Command::Serve { bind, tls, insecure } => {
641 622
            init_logging();
642 -
            runtime()?.block_on(serve::serve(&ctx, bind, tls, insecure, open))
623 +
            runtime()?.block_on(serve::serve(&ctx, bind, tls, insecure))
643 624
        }
644 -
        Command::Open { owner_home, no_browser, port, page } => runtime()?.block_on(async {
645 -
            if ctx.remote()?.is_some() {
646 -
                open::open_remote(&ctx, owner_home, !no_browser, port, page.as_deref()).await
647 -
            } else {
648 -
                open::open(&ctx, owner_home, !no_browser, page.as_deref()).await
649 -
            }
650 -
        }),
651 625
        Command::Mcp { agent, auto, kind, home } => runtime()?.block_on(async {
652 626
            match (agent, auto) {
653 627
                (Some(a), _) => mcp::run(&ctx, &a, home).await,
…
62 unchanged lines
modified src/cli/serve.rs +3 −14
…
66 unchanged lines
67 67
68 68
type Served = std::pin::Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + Send>>;
69 69
70 -
pub async fn serve(ctx: &Ctx, bind: Option<String>, tls: Option<String>, insecure: bool, open: bool) -> Result<i32> {
70 +
pub async fn serve(ctx: &Ctx, bind: Option<String>, tls: Option<String>, insecure: bool) -> Result<i32> {
71 71
    let cfg = server_cfg(ctx, bind, tls)?;
72 72
    let addr: SocketAddr = cfg.bind.parse().with_context(|| format!("{} is not an address and port", cfg.bind))?;
73 73
    let use_tls = match cfg.tls.as_str() {
…
37 unchanged lines
111 111
    } else {
112 112
        None
113 113
    };
114 -
    let state = AppState { daemon: daemon.clone(), origins: Arc::new(origins), sessions: Default::default() };
115 -
    let app = spectra_mcp::http::router(state.clone()).merge(spectra_mcp::api::router(state.clone())).merge(radar::router(state));
114 +
    let state = AppState { daemon: daemon.clone(), origins: Arc::new(origins) };
115 +
    let app = spectra_mcp::http::router(state.clone()).merge(spectra_mcp::api::router(state));
116 116
117 117
    let (head, _) = daemon.ledger.head().await;
118 118
    let agents = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM agents WHERE disabled_ms IS NULL")
…
22 unchanged lines
141 141
    }
142 142
    println!("{}", ui::kv("ledger", format!("{head} events{}{agents} agent(s)", ui::sep())));
143 143
    println!("  {} {}", ui::note(), ui::dim("agents connect with: spectra mcp --agent NAME"));
144 -
    println!("  {} {}", ui::note(), ui::dim("open Radar with: spectra open"));
145 -
    if open {
146 -
        let ctx2 = Ctx { data: ctx.data.clone() };
147 -
        tokio::spawn(async move {
148 -
            // Once the socket answers, sign the link as the owner and open it.
149 -
            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
150 -
            if let Err(e) = super::open::open(&ctx2, None, true, None).await {
151 -
                eprintln!("{} {e:#}", ui::caution());
152 -
            }
153 -
        });
154 -
    }
155 144
156 145
    let (stop_tx, stop_rx) = tokio::sync::watch::channel(false);
157 146
    let stop = |mut rx: tokio::sync::watch::Receiver<bool>| async move {
…
57 unchanged lines
modified tests/client.rs +0 −87
…
101 unchanged lines
102 102
    let o = run(s, &["admin", "verify"]);
103 103
    assert!(o.status.success(), "{}", text(&o));
104 104
}
105 -
106 -
/// One HTTP/1.1 exchange with the proxy: status, head, body.
107 -
fn http(port: u16, method: &str, path: &str, headers: &[(&str, &str)], body: &str) -> (u16, String, String) {
108 -
    use std::io::{Read, Write};
109 -
    let mut s = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
110 -
    s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
111 -
    let mut req = format!("{method} {path} HTTP/1.1\r\nconnection: close\r\ncontent-length: {}\r\n", body.len());
112 -
    if !headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("host")) {
113 -
        req.push_str(&format!("host: 127.0.0.1:{port}\r\n"));
114 -
    }
115 -
    for (k, v) in headers {
116 -
        req.push_str(&format!("{k}: {v}\r\n"));
117 -
    }
118 -
    req.push_str("\r\n");
119 -
    req.push_str(body);
120 -
    s.write_all(req.as_bytes()).unwrap();
121 -
    let mut out = Vec::new();
122 -
    let _ = s.read_to_end(&mut out);
123 -
    let text = String::from_utf8_lossy(&out).into_owned();
124 -
    let (head, body) = text.split_once("\r\n\r\n").unwrap_or((&text, ""));
125 -
    let status = head.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0);
126 -
    (status, head.to_string(), body.to_string())
127 -
}
128 -
129 -
#[tokio::test]
130 -
async fn radar_through_the_loopback_proxy() {
131 -
    use std::io::BufRead;
132 -
    let (sd, cd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
133 -
    let (s, c) = (sd.path(), cd.path());
134 -
    let (srv, _port) = connected(s, c);
135 -
136 -
    let mut open = hermetic(c).args(["open", "--no-browser", "--port", "0"]).stdout(Stdio::piped()).spawn().unwrap();
137 -
    let out = open.stdout.take().unwrap();
138 -
    let (tx, rx) = std::sync::mpsc::channel();
139 -
    std::thread::spawn(move || {
140 -
        for line in std::io::BufReader::new(out).lines().map_while(Result::ok) {
141 -
            if let Some(u) = line.split_whitespace().find(|w| w.contains("/signin?code=")) {
142 -
                let _ = tx.send(u.to_string());
143 -
            }
144 -
        }
145 -
    });
146 -
    let link = rx.recv_timeout(Duration::from_secs(10)).expect("spectra open printed a link");
147 -
    let rest = link.strip_prefix("http://127.0.0.1:").expect("a loopback link");
148 -
    let (port, path) = rest.split_once('/').unwrap();
149 -
    let port: u16 = port.parse().unwrap();
150 -
151 -
    // Signing in through the proxy sets the session cookie.
152 -
    let (status, head, _) = http(port, "GET", &format!("/{path}"), &[], "");
153 -
    assert_eq!(status, 303, "{head}");
154 -
    let cookie = head
155 -
        .lines()
156 -
        .find_map(|l| {
157 -
            l.to_ascii_lowercase()
158 -
                .starts_with("set-cookie:")
159 -
                .then(|| l.split_once(':').unwrap().1.trim().split(';').next().unwrap().to_string())
160 -
        })
161 -
        .expect("a session cookie");
162 -
163 -
    // Live and a channel load; the channel's form carries the CSRF token.
164 -
    let (status, _, live) = http(port, "GET", "/", &[("cookie", &cookie)], "");
165 -
    assert_eq!(status, 200);
166 -
    assert!(live.contains("Spectra"), "{live}");
167 -
    let (status, _, page) = http(port, "GET", "/c/general", &[("cookie", &cookie)], "");
168 -
    assert_eq!(status, 200);
169 -
    let csrf = page.split("name=\"_csrf\" value=\"").nth(1).and_then(|r| r.split('"').next()).expect("a CSRF token").to_string();
170 -
171 -
    // A post from the proxy's own page goes through, as the owner.
172 -
    let origin = format!("http://127.0.0.1:{port}");
173 -
    let form = format!("_csrf={csrf}&body=hello+from+radar+over+the+proxy");
174 -
    let hdrs = [("cookie", cookie.as_str()), ("origin", origin.as_str()), ("content-type", "application/x-www-form-urlencoded")];
175 -
    let (status, head, _) = http(port, "POST", "/c/general", &hdrs, &form);
176 -
    assert_eq!(status, 303, "{head}");
177 -
    let hits = json_of(c, &["search", "--json", "proxy"]);
178 -
    assert_eq!(hits.as_array().unwrap().len(), 1, "{hits}");
179 -
    assert_eq!(hits[0]["author"], "human");
180 -
181 -
    // Another site cannot use the proxy: not by Origin, nor by Host.
182 -
    let evil = [("cookie", cookie.as_str()), ("origin", "http://evil.example"), ("content-type", "application/x-www-form-urlencoded")];
183 -
    assert_eq!(http(port, "POST", "/c/general", &evil, &form).0, 403);
184 -
    assert_eq!(http(port, "GET", "/", &[("host", "evil.example"), ("cookie", &cookie)], "").0, 403);
185 -
186 -
    open.kill().ok();
187 -
    open.wait().ok();
188 -
    srv.stop();
189 -
    let o = run(s, &["admin", "verify"]);
190 -
    assert!(o.status.success(), "{}", text(&o));
191 -
}