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.
173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
"""Cayce Ops — configuration.
|
|
|
|
All configuration comes from environment variables prefixed with ``CAYCE_OPS_``.
|
|
No config files, no defaults that touch the network. See .env.example for the
|
|
full list with placeholder values.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
def _bool(name: str, default: bool = False) -> bool:
|
|
raw = os.environ.get(name)
|
|
if raw is None:
|
|
return default
|
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _int(name: str, default: int) -> int:
|
|
raw = os.environ.get(name)
|
|
if raw is None or not raw.strip():
|
|
return default
|
|
return int(raw)
|
|
|
|
|
|
def _list(name: str, default: list[str] | None = None) -> list[str]:
|
|
raw = os.environ.get(name)
|
|
if raw is None or not raw.strip():
|
|
return list(default or [])
|
|
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Config:
|
|
"""Snapshot of the process environment. Constructed once per CLI invocation."""
|
|
|
|
# --- core ---
|
|
database_url: str = field(
|
|
default_factory=lambda: os.environ.get(
|
|
"CAYCE_OPS_DATABASE_URL", "sqlite:///cayce-ops.db"
|
|
)
|
|
)
|
|
autonomous_mode: bool = field(
|
|
default_factory=lambda: _bool("CAYCE_OPS_AUTONOMOUS_MODE", False)
|
|
)
|
|
log_level: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_LOG_LEVEL", "INFO")
|
|
)
|
|
state_dir: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_STATE_DIR", ".")
|
|
)
|
|
media_dir: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_MEDIA_DIR", "./media")
|
|
)
|
|
|
|
# --- scanner ---
|
|
scanner_sources: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_SCANNER_SOURCES", "[]")
|
|
)
|
|
scanner_keywords: list[str] = field(
|
|
default_factory=lambda: _list(
|
|
"CAYCE_OPS_SCANNER_KEYWORDS",
|
|
["affiliate", "partnership", "bitcoin", "lightning", "travel"],
|
|
)
|
|
)
|
|
scanner_user_agent: str = field(
|
|
default_factory=lambda: os.environ.get(
|
|
"CAYCE_OPS_SCANNER_USER_AGENT", "cayce-ops-scanner/0.1"
|
|
)
|
|
)
|
|
|
|
# --- executor ---
|
|
autonomous_kinds: list[str] = field(
|
|
default_factory=lambda: _list("CAYCE_OPS_AUTONOMOUS_KINDS", [])
|
|
)
|
|
autonomous_daily_cap: int = field(
|
|
default_factory=lambda: _int("CAYCE_OPS_AUTONOMOUS_DAILY_CAP", 0)
|
|
)
|
|
max_attempts: int = field(
|
|
default_factory=lambda: _int("CAYCE_OPS_MAX_ATTEMPTS", 2)
|
|
)
|
|
enable_http_actions: bool = field(
|
|
default_factory=lambda: _bool("CAYCE_OPS_ENABLE_HTTP_ACTIONS", False)
|
|
)
|
|
# when set (64-char hex x-only pubkey), human approvals MUST carry a valid
|
|
# BIP-340 signature over "cayce-ops:approve:<id>" made by the operator key.
|
|
operator_pubkey: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_OPERATOR_PUBKEY", "")
|
|
)
|
|
|
|
# --- mail ---
|
|
imap_host: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_IMAP_HOST", "")
|
|
)
|
|
imap_port: int = field(default_factory=lambda: _int("CAYCE_OPS_IMAP_PORT", 993))
|
|
imap_user: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_IMAP_USER", "")
|
|
)
|
|
imap_password: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_IMAP_PASSWORD", "")
|
|
)
|
|
imap_mailbox: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_IMAP_MAILBOX", "INBOX")
|
|
)
|
|
smtp_host: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_SMTP_HOST", "")
|
|
)
|
|
smtp_port: int = field(default_factory=lambda: _int("CAYCE_OPS_SMTP_PORT", 587))
|
|
smtp_user: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_SMTP_USER", "")
|
|
)
|
|
smtp_password: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_SMTP_PASSWORD", "")
|
|
)
|
|
mail_from: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_MAIL_FROM", "")
|
|
)
|
|
|
|
# --- llm ---
|
|
llm_provider: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_LLM_PROVIDER", "null")
|
|
)
|
|
llm_model: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_LLM_MODEL", "")
|
|
)
|
|
llm_base_url: str = field(
|
|
default_factory=lambda: os.environ.get(
|
|
"CAYCE_OPS_LLM_BASE_URL", "https://api.openai.com/v1"
|
|
)
|
|
)
|
|
llm_api_key: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_LLM_API_KEY", "")
|
|
)
|
|
llm_review_threshold: float = field(
|
|
default_factory=lambda: float(
|
|
os.environ.get("CAYCE_OPS_LLM_REVIEW_THRESHOLD", "0.7")
|
|
)
|
|
)
|
|
|
|
# --- publisher / nostr ---
|
|
nostr_private_key: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_NOSTR_PRIVATE_KEY", "")
|
|
)
|
|
nostr_relays: list[str] = field(
|
|
default_factory=lambda: _list("CAYCE_OPS_NOSTR_RELAYS", [])
|
|
)
|
|
enable_facebook: bool = field(
|
|
default_factory=lambda: _bool("CAYCE_OPS_ENABLE_FACEBOOK", False)
|
|
)
|
|
facebook_page_token: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_FACEBOOK_PAGE_TOKEN", "")
|
|
)
|
|
enable_instagram: bool = field(
|
|
default_factory=lambda: _bool("CAYCE_OPS_ENABLE_INSTAGRAM", False)
|
|
)
|
|
instagram_user_id: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_INSTAGRAM_USER_ID", "")
|
|
)
|
|
|
|
# --- monitor ---
|
|
monitor_output: str = field(
|
|
default_factory=lambda: os.environ.get(
|
|
"CAYCE_OPS_MONITOR_OUTPUT", "./status.html"
|
|
)
|
|
)
|
|
|
|
|
|
def load() -> Config:
|
|
"""Build the process Config snapshot."""
|
|
return Config()
|