Run the daemon on Earth: make install-server, a unit that waits for a Spectra (#25) aa72eb904932
make install copies the binary instead of linking into the build tree; install-server does the same over SSH with the user unit; the unit waits until a Spectra exists. Earth now serves, set up for Andy's KeyId and waiting for his claim. The README describes the two-machine setup. 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 0e66123960d9
- When
- · 2026-11-03 13:48 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
aa72eb9049325388138d098a87899003a63a605676d8b5b5620b18cbc0623310- Depends on
- 01320eefd505 200875410937 258bb3824813 98a16830a3d3 d17cbc7069ce
- Depended on by
- 862f0db224aa 98a0fb9f0948 813b6330f3ad
- In bookmarks
- main
- In tags
- v0.1.0
- Edits
- 17 atoms, 1 tree operation, 6 files
Browse the tree at this change
added scripts/install-server.sh +35 −0
| 1 | + | #!/bin/sh |
|
| 2 | + | # Install `spectra serve` on another machine as a systemd user service |
|
| 3 | + | # (spec §8.6, spectra#25): build a release here, copy a stripped binary and |
|
| 4 | + | # the unit over SSH, and enable it. The unit waits until a Spectra exists |
|
| 5 | + | # there (`spectra init --owner KEYID`), and starts from boot when the |
|
| 6 | + | # account has lingering on. |
|
| 7 | + | # |
|
| 8 | + | # scripts/install-server.sh [USER@]HOST |
|
| 9 | + | set -eu |
|
| 10 | + | [ $# -eq 1 ] || { echo "usage: $0 [USER@]HOST" >&2; exit 2; } |
|
| 11 | + | target=$1 |
|
| 12 | + | cd "$(dirname "$0")/.." |
|
| 13 | + | SPECTRA_BUILD="$(oxy --plain describe 2>/dev/null || true)" CARGO_INCREMENTAL=0 cargo build --release -p spectra |
|
| 14 | + | bin=${CARGO_TARGET_DIR:-target}/release/spectra |
|
| 15 | + | tmp=$(mktemp -d) |
|
| 16 | + | trap 'rm -rf "$tmp"' EXIT |
|
| 17 | + | strip -o "$tmp/spectra" "$bin" |
|
| 18 | + | ssh -o BatchMode=yes "$target" 'mkdir -p ~/.local/bin ~/.config/systemd/user' |
|
| 19 | + | scp -q -o BatchMode=yes "$tmp/spectra" "$target:.local/bin/spectra.new" |
|
| 20 | + | scp -q -o BatchMode=yes packaging/spectra.service "$target:.config/systemd/user/spectra.service" |
|
| 21 | + | ssh -o BatchMode=yes "$target" ' |
|
| 22 | + | set -e |
|
| 23 | + | chmod 755 ~/.local/bin/spectra.new && mv ~/.local/bin/spectra.new ~/.local/bin/spectra |
|
| 24 | + | systemctl --user daemon-reload |
|
| 25 | + | systemctl --user enable spectra >/dev/null 2>&1 |
|
| 26 | + | if systemctl --user is-active --quiet spectra; then systemctl --user restart spectra; fi |
|
| 27 | + | echo "installed: $(~/.local/bin/spectra --version)" |
|
| 28 | + | if [ -e ~/.local/share/spectra/spectra.db ]; then |
|
| 29 | + | systemctl --user start spectra |
|
| 30 | + | echo "service: $(systemctl --user is-active spectra)" |
|
| 31 | + | else |
|
| 32 | + | echo "service: enabled; waiting for a Spectra: spectra init --owner KEYID, then systemctl --user start spectra" |
|
| 33 | + | fi |
|
| 34 | + | echo "linger: $(loginctl show-user "$(id -un)" -p Linger --value) (yes means it runs from boot)" |
|
| 35 | + | ' |
modified CHANGELOG.md +1 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **Added: `make install-server HOST=user@host` (#25).** Builds a release, copies a stripped binary and the systemd user unit over SSH, and enables the service. The unit now waits until a Spectra exists instead of failing in a loop. `make install` copies the binary rather than linking into the build tree, so cleaning a build directory no longer breaks an installed service. The README describes setting up the daemon on another machine. |
|
| 5 | 6 | - **Added: Radar from another machine (#24).** With a server configured, `spectra open` serves Radar on `http://127.0.0.1:7350` (`--port` to change it) through a proxy to the pinned server, and signs you in through it; pages, forms and the live stream all pass through. The proxy answers only its own loopback host and origin, so no other site can reach the server through it. Ctrl-C stops it. |
|
| 6 | 7 | - **Changed: the command line works from another machine (#23).** With a server configured by `spectra connect`, every command goes through the JSON API and the workstation needs no ledger file: `identity show`, `agent add`, `list` and `disable`, `project add` and `list`, `say`, `inbox`, `search`, `api` and `admin verify`, which the server runs on its own ledger (`GET /api/v1/admin/verify`). `admin check` and `rebuild` say they run on the server. Locally, commands read through the daemon whenever it is serving, so the same path is exercised on one machine and on two. The API's agents now carry their delegation state and expiry, projects their agents, and inbox items and search hits their author's display name. |
|
| 7 | 8 | - **Added: `spectra connect https://HOST[:PORT]` (#21).** Uses a Spectra on another machine. It trusts the server only when your own identity delegates to its instance key and that key vouches for the certificate it presents; until you have delegated, it saves the instance's public identity and prints the `oxy identity delegate add` to run. Then it pins the certificate in `~/.config/spectra/client.toml` (or `SPECTRA_SERVER`), claims a server set up with `spectra init --owner`, and brings the server up to your current identity. A certificate that changes is refused until `connect` checks it again. The stdio shim uses the configured server (with `--home`). |
|
| … | 17 unchanged lines |
modified Makefile +7 −2
| 1 | - | .PHONY: build check test fmt clippy changelog run ts install deps-check |
|
| 1 | + | .PHONY: install-server build check test fmt clippy changelog run ts install deps-check |
|
| 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. |
|
| … | 27 unchanged lines |
||
| 32 | 32 | # Build and install spectra with its build identity (oxy describe) in --version. |
|
| 33 | 33 | install: |
|
| 34 | 34 | SPECTRA_BUILD="$$(oxy --plain describe 2>/dev/null)" CARGO_INCREMENTAL=0 cargo build --release |
|
| 35 | - | ln -sf $${CARGO_TARGET_DIR:-$(CURDIR)/target}/release/spectra $(HOME)/.local/bin/spectra |
|
| 35 | + | install -D -m 755 $${CARGO_TARGET_DIR:-$(CURDIR)/target}/release/spectra $(HOME)/.local/bin/spectra |
|
| 36 | 36 | $(HOME)/.local/bin/spectra --version |
|
| 37 | 37 | ||
| 38 | + | # The daemon on another machine over SSH (spec §8.6): make install-server HOST=user@host |
|
| 39 | + | install-server: |
|
| 40 | + | @test -n "$(HOST)" || { echo "make install-server HOST=[USER@]HOST" >&2; exit 2; } |
|
| 41 | + | scripts/install-server.sh $(HOST) |
|
| 42 | + | ||
| 38 | 43 | # Every crate in Cargo.lock must be one the OxygenFlow build image vendors |
|
| 39 | 44 | # (oxygenflow images/rust: the crates of oxygen, oxygenhub and oxygenflow), or |
|
| 40 | 45 | # CI cannot build offline (spectra#2). |
|
| … | 2 unchanged lines |
modified README.md +33 −1
| … | 16 unchanged lines |
||
| 17 | 17 | ``` |
|
| 18 | 18 | cargo build --release |
|
| 19 | 19 | make check # changelog, deps-check, fmt, clippy -D warnings, tests |
|
| 20 | - | make install # release build with its build identity, linked into ~/.local/bin |
|
| 20 | + | make install # release build with its build identity, copied into ~/.local/bin |
|
| 21 | + | make install-server HOST=user@host # the same, over SSH, with the systemd user unit |
|
| 21 | 22 | ``` |
|
| 22 | 23 | ||
| 23 | 24 | `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. |
|
| … | 33 unchanged lines |
||
| 57 | 58 | ||
| 58 | 59 | 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 | ||
| 61 | + | ## The daemon on another machine |
|
| 62 | + | ||
| 63 | + | Spectra can run on a server while you, your keys and your agents stay on your workstation (spec §8.6). Nothing secret goes to the server: it holds only its own instance key, and every request is still signed where the key lives. Traffic is TLS with a certificate the server makes itself; your workstation trusts it because your own identity delegates to the server's instance key, which vouches for the certificate. No certificate authority is involved. |
|
| 64 | + | ||
| 65 | + | On the workstation, install the server and note your KeyId: |
|
| 66 | + | ||
| 67 | + | ``` |
|
| 68 | + | make install-server HOST=claude@earth |
|
| 69 | + | oxy identity show # the 64-hex key |
|
| 70 | + | ``` |
|
| 71 | + | ||
| 72 | + | On the server: |
|
| 73 | + | ||
| 74 | + | ``` |
|
| 75 | + | spectra init --owner KEYID --name earth # no key of yours is needed there |
|
| 76 | + | # in ~/.local/share/spectra/spectra.toml, under [server]: |
|
| 77 | + | # bind = "[::]:7350" |
|
| 78 | + | # names = ["earth", "earth.example"] |
|
| 79 | + | systemctl --user start spectra |
|
| 80 | + | ``` |
|
| 81 | + | ||
| 82 | + | On the workstation: |
|
| 83 | + | ||
| 84 | + | ``` |
|
| 85 | + | spectra connect https://earth.example:7350 # prints the delegation to run, the first time |
|
| 86 | + | oxy identity delegate add ~/.config/spectra/earth-instance.identity --scope changes |
|
| 87 | + | spectra connect https://earth.example:7350 # trusted, pinned, and claimed as yours |
|
| 88 | + | ``` |
|
| 89 | + | ||
| 90 | + | 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-oxygen --home ~/.oxy-agents/claude-oxygen`. `spectra open` serves Radar on `http://127.0.0.1:7350` through a proxy to the server. |
|
| 91 | + | ||
| 60 | 92 | 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 | 93 | ||
| 62 | 94 | ## Layout |
|
| … | 16 unchanged lines |
modified docs/plan.md +5 −5
| … | 34 unchanged lines |
||
| 35 | 35 | - [x] #21 spectra connect — done 2026-11-03: trust through the owner's delegation to the instance key, pin, claim with the rotation delegating causes; tests/connect.rs, and the shim over TLS in tests/client.rs. |
|
| 36 | 36 | - [x] #23 The CLI without a ledger file — done 2026-11-03: every command through the API when a daemon serves, local or remote; admin verify via the server; tests/client.rs runs them all from a workstation with no data directory. |
|
| 37 | 37 | - [x] #24 Radar from a workstation — done 2026-11-03: spectra open serves a loopback proxy to the pinned server and signs in through it; tests/client.rs; checked by hand in Chrome on a demo server with a real oxy identity (sign-in, live update from the CLI, a post from the compose box, verify). |
|
| 38 | - | - [ ] #25 Run the daemon on Earth: install over SSH, a unit that waits for a Spectra, README. |
|
| 38 | + | - [x] #25 Run the daemon on Earth — done 2026-11-03: make install-server, a unit that waits for a Spectra, make install copies; Earth runs `spectra serve` on [::]:7350 as the claude account from boot, set up with `spectra init --owner d89664ced1fc…` (Andy's public KeyId) and waiting for his claim; /api/v1/instance answers from the workstation. |
|
| 39 | 39 | - [ ] #13 Acceptance (with Andy), across two machines. Ready when the rest is done: |
|
| 40 | - | 1. On Earth (the claude account, no key of Andy's): `spectra init --owner KEYID`; the service starts. |
|
| 41 | - | 2. On the workstation: `spectra connect https://earth.andymccall.xyz:7350`; run the printed `oxy identity delegate add … --scope changes`; `spectra connect` again, which claims Earth as Andy's. |
|
| 40 | + | 1. Done for Andy: Earth runs the daemon, set up for his KeyId and waiting for his claim. Before step 2, `spectra --version` here should be the release (`make install`). |
|
| 41 | + | 2. On the workstation: `spectra connect https://earth.andymccall.xyz:7350`; run the printed `oxy identity delegate add ~/.config/spectra/earth-instance.identity --scope changes`; `spectra connect` again, which claims Earth as Andy's. |
|
| 42 | 42 | 3. `spectra project add ~/development/oxygen` and `~/development/oxygenhub`. |
|
| 43 | 43 | 4. `spectra agent add claude-oxygen --kind claude-code --project oxygen --home ~/.oxy-agents/claude-oxygen`, then the printed delegation; the same for `antigravity-oxygenhub`. |
|
| 44 | - | 5. Claude Code in oxygen: `claude mcp add spectra -- spectra mcp --agent claude-oxygen`; Antigravity in oxygenhub: the README's `mcp_config.json` line. |
|
| 44 | + | 5. Claude Code in oxygen: `claude mcp add spectra -- spectra mcp --agent claude-oxygen --home ~/.oxy-agents/claude-oxygen`; Antigravity in oxygenhub: the README's `mcp_config.json` line with `--home` added. |
|
| 45 | 45 | 6. Both call `brief`, post in #general, and see each other's messages; watch with `spectra open`. |
|
| 46 | - | 7. `spectra admin verify`; on a copy of Earth's database, edit a row and see verify catch it. |
|
| 46 | + | 7. `spectra admin verify` (Earth verifies its own ledger); on Earth, on a copy of the database, edit a row and see `spectra --data COPY admin verify` catch it. |
|
| 47 | 47 | 8. Cut 0.1.0 in CHANGELOG.md and tag it. |
|
| 48 | 48 | ||
| 49 | 49 | ## Log |
|
| … | 7 unchanged lines |
modified packaging/spectra.service +5 −1
| … | 1 unchanged lines |
||
| 2 | 2 | # they start, so it should outlive any one terminal. |
|
| 3 | 3 | # |
|
| 4 | 4 | # cp packaging/spectra.service ~/.config/systemd/user/ |
|
| 5 | + | # (on another machine: make install-server HOST=user@host) |
|
| 5 | 6 | # systemctl --user daemon-reload && systemctl --user enable --now spectra |
|
| 6 | 7 | # journalctl --user -u spectra -f |
|
| 7 | 8 | [Unit] |
|
| 8 | 9 | Description=Spectra, the control plane for AI coding agents |
|
| 9 | - | After=default.target |
|
| 10 | + | After=default.target network-online.target |
|
| 11 | + | # Until `spectra init` (or `spectra init --owner KEYID` on a server) makes |
|
| 12 | + | # a Spectra, the enabled service waits instead of failing in a loop. |
|
| 13 | + | ConditionPathExists=%h/.local/share/spectra/spectra.db |
|
| 10 | 14 | ||
| 11 | 15 | [Service] |
|
| 12 | 16 | ExecStart=%h/.local/bin/spectra serve |
|
| … | 9 unchanged lines |