fix: address external audit — criticals 1-5 + mediums
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.
This commit is contained in:
@@ -32,6 +32,60 @@ def test_reject_records_actor(cfg, sess):
|
||||
assert p.status == ProposalStatus.REJECTED
|
||||
|
||||
|
||||
def test_signed_approval_required_when_operator_key_configured(cfg, sess):
|
||||
"""Critical finding 1: --actor was free text; now a key can be required."""
|
||||
from coincurve import PrivateKey, PublicKey
|
||||
|
||||
from cayceops.components.executor import executor
|
||||
|
||||
secret = bytes.fromhex("22" * 32)
|
||||
pubkey = PublicKey.from_secret(secret).format().hex()[2:]
|
||||
p = _proposal()
|
||||
sess.add(p)
|
||||
sess.flush()
|
||||
|
||||
config = cfg(CAYCE_OPS_OPERATOR_PUBKEY=pubkey)
|
||||
|
||||
# no signature -> refused
|
||||
assert executor.approve(p.id, "gustavo", config, sess, signature=None) == 2
|
||||
# wrong key -> refused
|
||||
bad = (
|
||||
PrivateKey(bytes.fromhex("33" * 32))
|
||||
.sign_schnorr(executor.approval_message(p.id), aux_randomness=b"\x00" * 32)
|
||||
.hex()
|
||||
)
|
||||
assert executor.approve(p.id, "gustavo", config, sess, signature=bad) == 2
|
||||
# correct key -> approved
|
||||
good = (
|
||||
PrivateKey(secret)
|
||||
.sign_schnorr(executor.approval_message(p.id), aux_randomness=b"\x00" * 32)
|
||||
.hex()
|
||||
)
|
||||
assert executor.approve(p.id, "gustavo", config, sess, signature=good) == 0
|
||||
assert p.status is ProposalStatus.APPROVED
|
||||
entry = sess.query(AuditEntry).filter_by(action="approve", result="ok").one()
|
||||
assert "signature" in entry.detail
|
||||
|
||||
|
||||
def test_auto_approve_orders_by_relevance_desc(cfg, sess):
|
||||
"""Medium finding: ascending 'risk_score' approved noise first."""
|
||||
low = _proposal(kind="content_gap", title="barely relevant")
|
||||
low.match_score = 0.1
|
||||
high = _proposal(kind="content_gap", title="highly relevant")
|
||||
high.match_score = 0.9
|
||||
sess.add_all([low, high])
|
||||
sess.flush()
|
||||
|
||||
config = cfg(
|
||||
CAYCE_OPS_AUTONOMOUS_MODE="true",
|
||||
CAYCE_OPS_AUTONOMOUS_KINDS="content_gap",
|
||||
CAYCE_OPS_AUTONOMOUS_DAILY_CAP="1",
|
||||
)
|
||||
executor._auto_approve(config, sess)
|
||||
assert high.status is ProposalStatus.APPROVED
|
||||
assert low.status is ProposalStatus.PROPOSED
|
||||
|
||||
|
||||
def test_run_executes_only_approved(cfg, sess):
|
||||
approved = _proposal(title="Approved item")
|
||||
proposed = _proposal(title="Proposed item")
|
||||
|
||||
Reference in New Issue
Block a user