331809f014
Per CAIRO task brief 2026-09-26 [1] (D1-D6), Decision 6 mailbox
writes are ALWAYS AUTO_RUN against the sender's own home.
v4 did not implement this; v4.1 adds:
* MAILBOX_PATH_RE recognises <home-repo>/mailbox/(outbox|inbox)/<path>
for both coreci-<role>/home (standard) and coreci/<role>/home
(forward-compat).
* classify() extended with own-home gate: if every target is an
own-home mailbox path and no non-mailbox write verb is present,
classify AUTO_RUN.
* T18-T25 added; T8/T17 xfail preserved.
* deny_read_targets NOT implemented (per brief).
[1] CAIRO task brief - consolidated D1-D6
(per-home mailbox + read_targets parser)
114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
"""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: <home-repo>/mailbox/(outbox|inbox)/<file-or-dir>
|
||
# where <home-repo> = coreci-<agent>/home or coreci/<agent>/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 <agent>'s own home repo.
|
||
|
||
Naming convention v2: agent field = Gitea username = allowlist key.
|
||
Format: coreci-<role>. Two accepted shapes for the home repo path:
|
||
coreci-<role>/home (standard)
|
||
coreci/<role>/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 <n>/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 <home-repo> 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
|