Files
cayce-ops/tests/test_no_self_approval.py
T
cayce 0d24309433
ci / lint-test-leaks (pull_request) Canceled after 0s
audit-D16: operator key must never be an agent identity
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).
2026-10-28 19:51:04 +00:00

83 lines
2.9 KiB
Python

"""Tests for D16: the operator key may never be an agent identity.
Background: the operator key was, at some point, Cayce's nostr key. The
daemon must refuse to operate in that configuration, because "human
approval" signed by the agent is not human approval.
"""
from __future__ import annotations
import pytest
from cayceops.signing import (
SelfApprovalError,
pubkey_from_private_hex,
validate_no_self_approval,
)
# keys invented for tests only (allowlisted in .gitleaks.toml)
CAYCE_SECRET = "11" * 32 # simulates Cayce nostr identity
CAYCE_PUBKEY = pubkey_from_private_hex(CAYCE_SECRET)
HUMAN_SECRET = "33" * 32 # a proper human operator key
HUMAN_PUBKEY = pubkey_from_private_hex(HUMAN_SECRET)
def _cfg(monkeypatch, operator_pubkey: str, nostr_private_key: str = CAYCE_SECRET):
from cayceops.config import Config
monkeypatch.setenv("CAYCE_OPS_DEPLOYMENT_ID", "itest")
monkeypatch.setenv("CAYCE_OPS_OPERATOR_PUBKEY", operator_pubkey)
monkeypatch.setenv("CAYCE_OPS_NOSTR_PRIVATE_KEY", nostr_private_key)
return Config()
def test_agent_pubkey_as_operator_is_refused(monkeypatch):
"""The exact bug: operator pubkey = Cayce pubkey must fail."""
cfg = _cfg(monkeypatch, operator_pubkey=CAYCE_PUBKEY)
with pytest.raises(SelfApprovalError):
validate_no_self_approval(cfg)
def test_human_pubkey_passes(monkeypatch):
cfg = _cfg(monkeypatch, operator_pubkey=HUMAN_PUBKEY)
validate_no_self_approval(cfg) # does not raise
def test_empty_operator_pubkey_passes_validation(monkeypatch):
"""Empty is the 'not configured' state — rejected elsewhere, not here."""
cfg = _cfg(monkeypatch, operator_pubkey="")
validate_no_self_approval(cfg)
def test_explicit_denylist_is_honoured(monkeypatch):
"""A key held outside this process can be listed explicitly."""
outside_pubkey = pubkey_from_private_hex("44" * 32)
monkeypatch.setenv(
"CAYCE_OPS_FORBIDDEN_OPERATOR_PUBKEYS", f"{outside_pubkey}, {HUMAN_PUBKEY}"
)
cfg = _cfg(monkeypatch, operator_pubkey=HUMAN_PUBKEY, nostr_private_key="")
with pytest.raises(SelfApprovalError):
validate_no_self_approval(cfg)
def test_daemon_refuses_to_start_with_agent_key(monkeypatch, sess, db_engine):
"""End-to-end: executor.run must refuse (exit 2) when the operator
pubkey is Cayce's identity."""
from cayceops.components.executor import executor
from cayceops.models import DaemonRun
cfg = _cfg(monkeypatch, operator_pubkey=CAYCE_PUBKEY)
code = executor.run(cfg, sess)
assert code == 2
run_row = sess.query(DaemonRun).filter_by(daemon="executor").one()
assert run_row.status == "error"
assert "agent identity" in run_row.detail["refused"]
def test_pubkey_derivation_roundtrip():
assert pubkey_from_private_hex(HUMAN_SECRET) == HUMAN_PUBKEY
with pytest.raises(ValueError):
pubkey_from_private_hex("")
with pytest.raises(ValueError):
pubkey_from_private_hex("nothex")