- 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.
181 lines
5.9 KiB
Python
181 lines
5.9 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)
|
|
)
|
|
# MANDATORY for approval-gated work: the operator x-only pubkey and the
|
|
# deployment identifier are part of every signed approval message.
|
|
# Without BOTH, daemons refuse to execute/send/publish — no degraded mode.
|
|
operator_pubkey: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_OPERATOR_PUBKEY", "")
|
|
)
|
|
deployment_id: str = field(
|
|
default_factory=lambda: os.environ.get("CAYCE_OPS_DEPLOYMENT_ID", "")
|
|
)
|
|
|
|
# --- 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"
|
|
)
|
|
)
|
|
|
|
|
|
class ConfigError(RuntimeError):
|
|
"""Raised when mandatory configuration is missing."""
|
|
|
|
|
|
def load() -> Config:
|
|
"""Build the process Config snapshot."""
|
|
return Config()
|