"""coreci/executor-config/app/main.py Allowlist classifier (v4.1) — see coreci/executor-config/tools/coreci-executor/SPEC.md §11. """ import re # v4 — pre-hoc classifier gate. Any pack containing a write verb against # read-only targets is NEEDS_APPROVAL. WRITE_VERB_RE = re.compile( r"(?i)\b(" r"curl\s+[^|;]*\s-X\s+(POST|PUT|PATCH|DELETE)" r"|curl\s+[^|;]*-(d|--data|--data-binary|--data-raw|urlencode)\s" r"|\bgit\s+push\b" r"|\bgit\s+commit\b(?![^|;\n]*--amend\b)" r"|\bgh\s+(pr|issue|release|repo)\s+(create|edit|close|merge|delete)\b" r"|\b(rm|mv|cp)\s+-[^\n]*\s+/\w" r")" ) # v4.1 — mailbox-path exemption. Recognises the per-home mailbox envelope # from coreci/company/standards/communications.md §"Mailbox" (decisions 1–9). # Path shape: /mailbox/(outbox|inbox)/ # where = coreci-/home or coreci//home. # Does NOT exempt paths in scripts/ or anywhere outside mailbox/. MAILBOX_PATH_RE = re.compile( r"^(?:coreci[-/][\w-]+/home|" r"coreci[-/]\w+)/mailbox/(?:outbox|inbox)(?:/[^\s]*)?$" ) def current_allowlist(): import os, yaml with open(os.environ.get( "ALLOWLIST_PATH", "allowlist.yaml"), "r") as f: return yaml.safe_load(f) def _is_own_home(agent, target): """Return True if `target` is 's own home repo. Naming convention v2: agent field = Gitea username = allowlist key. Format: coreci-. Two accepted shapes for the home repo path: coreci-/home (standard) coreci//home (forward-compatible) """ role = agent.replace("coreci-", "", 1) return target in {f"coreci-{role}/home", f"coreci/{role}/home"} def _mailbox_only_targets(targets): """Return True iff every target is an own-home mailbox path.""" for t in targets: if not MAILBOX_PATH_RE.match(t): return False return True def classify(agent, targets, content, kind): al = current_allowlist() defaults = al["defaults"] agent_cfg = al["agents"].get(agent) if agent_cfg is None: return defaults["decision"] agent_kind_cfg = ( agent_cfg.get("kinds", {}).get(kind, {}) if isinstance(agent_cfg, dict) else {} ) auto_run_targets = ( set(defaults.get("auto_run_targets", [])) | set(agent_kind_cfg.get( "auto_run_targets", agent_cfg.get("auto_run_targets", []))) ) read_targets = ( set(defaults.get("read_targets", [])) | set(agent_kind_cfg.get( "read_targets", agent_cfg.get("read_targets", []))) ) # v4.1 — Decision 6 mailbox-path exemption [1]. # Mailbox writes are ALWAYS AUTO_RUN when: # (a) every target is the sender's own home mailbox path, AND # (b) every target is on the sender's own home (not a cross-agent mirror # to someone else's /home/mailbox/), AND # (c) the content does not contain any non-mailbox write verb. # Cross-agent mailbox INBOX mirror (CAIRO writes to coreci-coo/home/mailbox/inbox/) # is NOT covered by this exemption — that needs explicit cross-agent grant. # That's the gap Decision 6 left; COO needs to add mailbox mirror grants in v4.2. if (_mailbox_only_targets(targets) and all(_is_own_home(agent, _home_part(t)) for t in targets) and not WRITE_VERB_RE.search(content)): return "AUTO_RUN" # v4 — pre-existing classifier, unchanged. if all(t in auto_run_targets for t in targets): return "AUTO_RUN" allowed_for_read = auto_run_targets | read_targets if all(t in allowed_for_read for t in targets): if not WRITE_VERB_RE.search(content): return "AUTO_RUN" return defaults["decision"] def _home_part(target): """Return the portion of a mailbox target. Example: 'coreci-coo/home/mailbox/outbox/x.md' -> 'coreci-coo/home' Used to gate the Decision 6 exemption to own-home only. """ parts = target.split("/mailbox/", 1) return parts[0] if parts else target