Background: CAYCE_OPS_OPERATOR_PUBKEY was set to Cayce nostr pubkey —
Cayce was approving itself. Signature was technically valid but semantically
null: human approval signed by the agent is not human approval.
- signing.py: validate_no_self_approval() rejects the operator pubkey when
it matches (a) the pubkey derived from any private key in the config,
or (b) any pubkey in CAYCE_OPS_FORBIDDEN_OPERATOR_PUBKEYS deny-list.
- executor/maild/publisher _signing() now calls the validation, so every
daemon and every CLI approve-* path refuses to operate (D16).
- tests: 6 new (agent-key refusal, human key passes, empty passes,
explicit denylist, end-to-end daemon refusal, derivation roundtrip).
- docs/DECISIONS.md: D16.
The operator signature is now produced OUTSIDE the VPS. CAYCE_OPS_OPERATOR_PUBKEY
must stay empty until the external signer exists — daemons refuse, which is
correct. The Cayce nsec itself is NOT touched (still the agent identity for
publishing and anchoring).
- README: the stack belongs to Cayce, an autonomous entity operating under
the Próspera ZEDE agentic-companies jurisdiction pilot; the experiment is
managed by Cayce itself
- tests: approver actor renamed to the generic label operator
- zero occurrences of personal names in the tree (verified with grep)
Critical 1 (approval forgery): audit_log is now hash-chained
(prev_hash/entry_hash, verify_audit_chain); executor approve REQUIRES a
BIP-340 signature over sha256(cayce-ops:approve:<id>) when
CAYCE_OPS_OPERATOR_PUBKEY is set; deploy/roles.sql ships least-privilege
per-daemon DB roles.
Critical 2 (threading dead): sends create real OUTBOUND rows (own
Message-ID, In-Reply-To -> inbound); inbound message_id never overwritten;
maild compose adds the first-contact path; _match_proposal falls back to
inbound chains.
Critical 3 (duplicate sends): per-message transactions in send path —
SMTP failure on message N no longer resurrects 1..N-1; failures audited
(send_failed) and the row stays queued.
Critical 4 (classifier): negated approvals (cannot accept / not approved /
not accepting) classify REJECTION at 0.95 before the bare-keyword rule;
LLM prompt braces escaped (.format no longer KeyError into silent fallback).
Critical 5 (systemd): every daemon has a run subcommand
(maild run = poll+send, publisher run = publish-due, monitor run = build);
deploy README documents the separate daily publisher plan timer.
Mediums: approved_hash binds drafts/captions to the approved text (edited
text refuses to send/publish until re-approved); risk_score renamed
match_score (migration 0002) and auto-approve orders HIGHEST relevance
first; compose binds postgres to 127.0.0.1; IMAP marks \\Seen only after
commit (fetch returns (num, raw), mark post-commit); disabled channels are
terminal (ContentStatus.SKIPPED) and publish failures burn a bounded
attempt budget before abandonment; make hooks installs the pre-push gate.
tests: 12 new — 56 total, all green; ruff 0 findings; gitleaks clean.
All 32 findings resolved: regex literals kept single-line with noqa
(pattern IS the reviewed artifact), HTML rows string-concatenated for
line length, walrus tuple in BIP-340 verify split into named generator,
unused pytest import dropped. No behavior changes; 44/44 tests pass.
Full-gate discipline restored: ruff exit 0.