15 Commits
Author SHA1 Message Date
cayce 6ddf76983b regla permanente de secretos: test anti-patrones + hook de journald
ci / lint-test-leaks (pull_request) Canceled after 0s
Nueva regla del operador (2026-10-31): los ficheros de secretos solo se
inspeccionan con herramientas que no muestran valores (jq keys, grep -c,
stat). Prohibido cat/grep -r sobre .env y volcar errores con DSN.

- tests/test_no_credential_patterns.py: 3 tests — DSN con credencial
  literal fuera de placeholders, nsec bech32 versionado, tokens hex de
  64 chars fuera de tests/.
- scripts/check-credential-leaks.sh: hook que falla si journald
  reciente contiene DSN con contraseña o nsec (redactando el valor en
  la salida). Sin timer propio (no autorizado); ejecutar manual o desde
  cron del operador.

Contexto: la rotación del rol hardcap y el DSN filtrado a journald por
el test del gate (corregido; la credencial filtrada ya está rotada).
2026-10-31 18:00:26 +00:00
cayce f5b026070b audit-4: classifier understands Spanish negations (veto unchanged)
ci / lint-test-leaks (pull_request) Canceled after 0s
Merchants write in Spanish; negations like nunca, lamentablemente or
no podemos must veto approval exactly like their English counterparts.
Previously an all-Spanish rejection mail could fall through to OTHER,
and a mixed mail with the approval word in English approved despite a
Spanish negation.

- negation list adds: nunca, jamas, tampoco, ningun/ninguna, nadie,
  nada, no podemos, no es posible, no aceptamos, rechaz*, descartad*,
  lamentablemente, no ha sido aprobad*, no procede
- new Spanish REJECTION rule (rechaz*, solicitud denegada, no cumple
  los requisitos) and Spanish APPROVAL rule (aprob*, aceptad*,
  bienvenid*, encantados de trabajar)
- veto rule unchanged: any negation anywhere -> needs_review
- tests: 15 new (12 veto cases incl. 2 mixed-language, pure-Spanish
  approval still approves, Spanish rejection classifies rejection)

Gate: ruff 0 · 96 unit + 1 PG integration · gitleaks clean.
2026-10-28 19:59:44 +00:00
cayce 09fe9199b3 audit-3: append-only audit log + strict legacy-prefix rule + anchor cross-check
ci / lint-test-leaks (pull_request) Canceled after 0s
- verify_audit_chain(): rows with entry_hash IS NULL are only tolerated
  BEFORE the first hashed row (documented legacy prefix). A NULL hash
  AFTER the chain started is tampering and fails verification at that
  row id — previously any NULL row anywhere was silently skipped, so an
  attacker with UPDATE could erase intermediate evidence undetected.
- migration 0004: audit_log becomes append-only. SQLite: BEFORE UPDATE/
  DELETE triggers RAISE(ABORT). Postgres: BEFORE triggers RAISE EXCEPTION.
  Closes the hole for the table owner and direct SQL connections.
- monitor.verify_chain_against_anchors(): compares the chain tail against
  anchors published to Nostr. Explicit statuses: no_anchors (never
  anchored / notes missing on relays), local_only (no fetcher), ok
  (relay note attests the tail or the last local anchor), mismatch.
- tests: NULL-before-genesis ok, NULL-in-middle fails at that id, NULL
  late fails, UPDATE rejected, DELETE rejected, INSERT still allowed,
  no-anchors explicit, local-only explicit, mismatch detected, match ok.

Gate: ruff 0 · 81 unit + 1 PG integration · alembic 0001→0004 on PG ·
gitleaks clean · make check exit 0.
2026-10-28 19:56:50 +00:00
cayce 0d24309433 audit-D16: operator key must never be an agent identity
ci / lint-test-leaks (pull_request) Canceled after 0s
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
cayce a2de419467 identity: Próspera agentic-companies pilot framing — no personal names
- 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)
2026-10-20 02:29:54 +00:00
cayce ffe425320d audit-4: serialized audit chain + legacy genesis + daily Nostr anchoring
- audit() writes are serialized: pg_advisory_xact_lock on Postgres;
  in-process RLock held until session commit/rollback on SQLite
  (test: 4 concurrent writers x 25 entries -> chain verifies, 100 rows)
- legacy rows (entry_hash NULL, pre-0002) are skipped: the chain begins
  GENESIS at the first hashed row; migrated DBs verify clean by design
- monitor anchor: publishes the last chain hash as a signed NIP-01 note
  once per day, logs it in the audit log (self-verified before publish)

tests: concurrent writers; legacy-genesis verification; anchor signs,
logs once per day.
2026-10-20 02:20:56 +00:00
cayce d77409148c audit-3 (critical 1 deploy half): roles.sql rewritten + PG integration test
- one least-privilege role per daemon; cayce_approver is the ONLY writer of
  status=APPROVED / operator_signature / approved columns
- per-table BEFORE triggers reject approval writes from daemon roles
- no literal passwords: refuses to run without the six -v password vars
  (psql exits 3 via missing-table SELECT guard)
- idempotent: revokes + drops trigger/function before recreating
- tests/integration/test_pg_roles.py: applies roles.sql via psql -v, runs a
  full cycle per daemon role (propose -> approve -> execute; compose ->
  approve -> send; plan -> approve -> publish; anchor) and asserts the
  denials; runs the alembic upgrade->downgrade->upgrade round-trip on
  Postgres (point 6). Skips cleanly without a reachable PG.
2026-10-20 02:20:26 +00:00
cayce 40453aadb4 audit-1 (critical 1): mandatory operator signatures bound to content+deployment
- signing module: canonical content hashes + approval_message
  sha256(cayce-ops:<type>:<deployment_id>:<id>:<content_hash>)
- approve/approve-draft/publisher-approve REQUIRE signature; without
  CAYCE_OPS_OPERATOR_PUBKEY+DEPLOYMENT_ID the daemons refuse to
  execute/send/publish (no degraded mode)
- run-time re-verification at execute/send/publish: unsigned rows (raw SQL
  approval) or edited content revert to pre-approval state, audited as error
- FULL signatures stored on rows (operator_signature, migration 0003) and
  in audit detail — never truncated
- signature failures audited from CLI wrappers too (1f)
- AUTONOMOUS_MODE approval path removed (D13): no non-human approvals

tests: SQL-approved row refused at execute/send/publish; to_addr change
blocks send; foreign deployment/content signatures refused; full-signature
persistence. 65 tests green.
2026-10-20 02:20:09 +00:00
cayce 90e6cfcc19 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.
2026-10-19 23:06:29 +00:00
cayce a282aedb94 lint: wrap long lines (E501), fix F841/F821/E731 — make check fully green
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.
2026-10-19 22:03:53 +00:00
cayce f61372e28e monitor: static status page — health, queue, gates, audit tail
ci / lint-test-leaks (pull_request) Canceled after 0s
- self-contained HTML (no external assets/JS), everything html-escaped
- daemon health from daemon_runs (last status + last run per daemon)
- proposal queue counts by state; human-gate counters (mail drafts awaiting
  approval, content draft/approved)
- audit tail: actor/action/result only — no detail payloads, no addresses,
  no subjects, no content bodies (tests assert the negatives)
- DaemonRun bookkeeping like every other daemon

tests: 3 new (sections render, leak-negatives, XSS escape) — 44 total
2026-10-19 21:57:31 +00:00
cayce aabd6bafc7 publisher: themed calendar, NIP-01 nostr adapter, gated brand images
ci / lint-test-leaks (pull_request) Canceled after 0s
- calendar: one draft per theme/channel (idempotent), rotated schedule
- human gate: approve() audited; AUTONOMOUS_MODE has no effect here by design
- nostr (primary): in-repo NIP-01 client (D4) — canonical serialization,
  sha256 id, BIP-340 schnorr sign via coincurve with zero aux randomness
  (deterministic), pure-python reference verification before publish,
  per-relay publish with isolated errors, event id stored as post_ref
- facebook/instagram adapters: exist but refuse to run unless explicitly
  enabled via env (default deployment never touches Meta)
- images: Pillow brand templates (bh gold / hc orange), 1080x1350,
  template-driven typeset design (not AI-generated media)
- failure isolation: config errors audited, item stays approved

tests: 12 new (event roundtrip+tamper, determinism, key validation,
calendar idempotence, approve gate, nostr happy path, future schedule,
clean config failure, disabled meta adapters, image render) — 41 total
2026-10-19 21:54:39 +00:00
cayce 5b259bc663 mail-negotiator: classify merchant replies, draft gated, audited sends
ci / lint-test-leaks (pull_request) Canceled after 0s
- IMAP poll (UNSEEN, stdlib imaplib) -> parse (bare address via email.utils)
- threading: In-Reply-To -> outbound sent-draft message-id -> proposal
- classify: deterministic rules; optional LLM behind provider interface;
  below-threshold confidence -> needs_review (never guessed)
- unthreadable approvals flagged needs_review (suspicious by definition)
- merchant_status recorded on proposal detail; proposal lifecycle untouched
- reply drafts generated from templates (drafted), NEVER auto-sent
- SMTP sends only draft_status=approved (human approve_draft, audited);
  sent drafts store their Message-ID for future threading
- idempotent on Message-ID; DaemonRun wrapper for monitor

tests: 9 new (classification rules, threshold->review, threading, idempotence,
blocked send without approval, approve->send->audit chain, double approve)
— 29 total passing
2026-10-19 21:47:18 +00:00
cayce 73ccdf7bdf scanner + executor: discovery files proposals; execution gated on approval
ci / lint-test-leaks (pull_request) Canceled after 0s
scanner (read-only):
- sources from CAYCE_OPS_SCANNER_SOURCES JSON (rss + page types), stdlib only
- keyword scoring -> risk_score; kind via heuristics (affiliate/partnership/content_gap)
- idempotent: sha1 dedupe_key; repeated scans skip existing
- every proposal audited (actor daemon:scanner); source errors recorded, never fatal

executor (human gate):
- approve/reject audited with operator identity; only proposed -> approved
- AUTONOMOUS_MODE=true adds exactly one path: policy auto-approval
  (kinds allowlist + daily cap, actor auto:policy) — DECISIONS D5
- plugin registry by kind; unknown kinds fall back to log_action (D9)
- retries with max_attempts, then failed; every transition audited
- http_post plugin inert unless CAYCE_OPS_ENABLE_HTTP_ACTIONS=true

tests: 13 new (dedupe, keyword filter, autonomy bounds, retry-then-fail,
blocked http_post, fallback plugin) — 20 total passing
2026-10-19 21:42:34 +00:00
cayce 34a5f331ac skeleton: package, schema, config, CLI, alembic, CI, deploy, docs
ci / lint-test-leaks (pull_request) Canceled after 0s
- cayceops core: env-only config (CAYCE_OPS_*), engine/session, ORM models
  with state machines (proposal/mail/content) and audited writes
- provider-agnostic LLM interface (null provider default, stdlib openai-compat)
- Typer CLI with lazy component loading (scanner/executor/maild/publisher/monitor)
- alembic 0001 initial schema; sqlite-portable JSON columns
- tests: 7 passing on throwaway sqlite; fixtures invented, never real data
- ci: ruff + alembic smoke + pytest + gitleaks(full history) in
  .gitea/workflows + .github/workflows; local gate = make check
- deploy: docker-compose (postgres 16) + hardened systemd units/timers
- docs: DECISIONS D1-D9; README project (grant text retired)
2026-10-19 21:35:50 +00:00