Materializing wrote a second plaintext file per secret, which is the problem the store
was meant to solve: two files drift, and the copy is what ends up committed or grepped.
- tangled_pr / tangled_pr_edit / tangled_repo now read tangled.cookie from the store.
engine/.tangled-session is deleted; --cookie-file remains as a legacy escape hatch.
- materialize() is replaced by run-time injection that leaves nothing at rest:
exec-env <group> -- cmd group as env vars (use this instead of a systemd
EnvironmentFile — same effect, no plaintext on disk)
with-file <key> -- cmd {} 0600 file in a private tmpdir, removed when cmd exits,
for consumers that insist on a path (ssh -i, a TLS key)
Co-Authored-By: Claude <noreply@anthropic.com>
/srv is the projects tree — agents grep, find and `ls -R` it constantly, so a store
under it turns up in ordinary searches and risks being read (or pasted) by accident.
/secrets sits outside that blast radius: nothing routinely walks it, and it is still
0700 loops, still not a repo, still ciphertext at rest.
Override with AO_SECRETS_STORE if a host puts it elsewhere.
Co-Authored-By: Claude <noreply@anthropic.com>
Credentials were scattered in plaintext: a gitea password baked into six git remote
URLs (`git remote -v` prints those), API keys in .env files, an incus client key at
0644. Anything living in a repo is one `git add -A` from being pushed.
So: ONE encrypted file outside every git tree, and a helper each project uses.
/srv/secrets/store.yaml sops+age ciphertext, 0600, not a repo, no remote
~/.config/sops/age/keys.txt the only plaintext secret on disk, 0600
secrets.py is stdlib + the sops binary: get("group.key"), get_group("group"), and
materialize() for consumers that must read a fixed path (systemd EnvironmentFile,
ssh IdentityFile, nix authKeyFile) — those keep their file, but the store is the
source of truth, so a materialized file is never hand-edited.
`list` prints names only, never values, so it is safe in a transcript.
Co-Authored-By: Claude <noreply@anthropic.com>
Multi-reviewer setups (e.g. a correctness + a readability adversary) can now have
the watchdog ping ALL reviewers on a claim, each in its own session with its own
submit key. A bare string still works (single agent). _ping_agents() helper.
When [watchdog].log_tokens (or [loop].log_tokens) is true, the watchdog records
for each phase how many tokens each agent used (and the total) and how long the
phase took, appended to <log_dir>/token-log.jsonl. Tokens are summed from each
agent's session transcript, attributed by working dir. View with `agents.py
tokens`. Baseline snapshot at phase start + delta at phase advance/complete;
robust across watchdog restarts. Validated: the transcript sum matches an
independent external collector exactly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unit tests (no agents/tmux): config load + defaults merge, kickoff-template
assembly, phase machine (advance/idempotent-complete/append-resumes), limit
reset-banner parsing, WAITING-UNTIL/stall parsing, claude+opencode activity
detectors. Live smokes bring a throwaway project up THROUGH agents.py on each
real backend in an isolated sandbox (unique prefix, opencode on a non-4096
port), verify attach + status + down, and clean up. tests/run.sh runs unit
always + smokes when backends present; README documents it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>