Repository navigation
feat(roe): generate the enforced engagement RoE from creation data (phase 1) - #1057
Conversation
…hase 1) Engagement scope collapses to one machine-enforceable RoE generated mechanically from the creation inputs: MCP start_engagement materializes plan/roe.json from targets + optional out_of_scope and refuses to start without a derivable in-scope target (fail-closed, replacing the audit-only fallback). URL targets reduce to their host, CIDRs normalize to the canonical network, subdomain globs add their apex. Plain-hostname and glob targets are passively enriched with crt.sh CT subdomains (bounded, root-filtered, best-effort). Design: docs/design/2026-10-22-roe-unification.md
Embedded target lists split, scheme-relative //host URLs collapse to the hostname, hostnames lowercase / lose the DNS trailing dot / IDNA-encode, bracketed bare IPv6 drops its brackets, host:port authorizes the host on all ports (documented), and patterns no real DNS/IP target can match (empty labels, a bare *.) are dropped with a warning instead of sitting as dead rules. Passive discovery splits the same way before querying.
…surface The CLI scan path (decepticon-cli scan) now materializes the same enforced plan/roe.json the MCP bridge does, from a shared module (decepticon.roe_materialization, free of the mcp extra) that also owns engagement_workspace. Filesystem-path targets carry no network scope: an all-paths engagement gets an empty in-scope allowlist (denylist and forbidden destinations still enforce), while network-shaped inputs that all die as unmatched patterns are refused even alongside valid paths. Passive CT discovery now covers URL and git-remote hosts too (git@host:repo reduces to the host). The Go launcher's direct-engagement flow already wrote its own enforce-mode RoE before the TUI enters; its dual-form document unifies in phase 2.
…stem targets The GitHub Actions composite scans its checkout root with --target ./, but the path classification excluded dot-prefixed fragments, so the default Actions configuration was refused instead of materializing a path-scoped RoE. Classify by disk existence with path markers (separator, leading . or ~); bare existing names stay paths with a warning pointing at the URL form for unambiguous host scoping.
Consume packages/decepticon-core/tests/testdata/roe-target-vectors.json — the same vectors the companion launcher PR's Go tests read — so the Python builder and the Go launcher cannot drift. Also record the companion launcher PR (machine-form generation, marker retirement) in the design doc.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Policy now matches the operator decision: - In-scope observation runs without an approval gate. - Out-of-scope calls are always rejected with ROE_REFUSED and audited. - Egress enforcement is always active when an in-scope allowlist exists. - Missing or malformed machine RoE is fail-closed for gated tools with ROE_CONFIG_MISSING. - Legacy EnforcementMode and the mode field are removed from the core schema and generated engagement RoE. - Legacy mode-specific tests and security documentation are removed or updated; audit/warn-only behaviors are no longer tested. ATT&CK remains metadata/reporting; impact-based HITL is separate follow-up. All remaining RoE mode references in runtime code are removed.
Follow-up: legacy RoE modes removed and fail-closed policy verifiedThis follow-up commit ( Changes
Direct runtime verification (not tests-only)Executed against the real Python runtime in the uv workspace:
Test verification: focused RoE/egress/technology tests 156 passed. No mode references remain in runtime RoE/egress code or security docs. |
Policy decision represented by this follow-upThe legacy |
Resolve CHAGELOG.md conflict (keep both #1057 and #1052 Unreleased entries) and repair the tests the legacy-mode removal (a047e71) left stale: - test_roe_materialization: drop the two `machine_enforcement["mode"] == "enforce"` assertions (the key no longer exists; always enforce) - test_public_api_stability: CORE_PUBLIC_API 75 -> 74 after EnforcementMode left the public API - CHAGELOG: record the EnforcementMode removal under the #1057 entry Verified: decepticon-core 136 passed; decepticon unit suite 5638 passed, 5 skipped (mcp_server excluded — optional `mcp` extra not installed locally); ruff check/format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- drop unused StrEnum import left by the EnforcementMode removal - pin mode absence in the builder test instead of the empty tautology - give from_dict(None) real assertions and a post-removal name
The lint job never reached the format stage before (check failed first), so four files from the mode-removal commits carried formatting drift.
Post-unification (#1057) enforcement is unconditional and the runtime ignores the mode key, so the generated roe.json no longer writes "mode":"enforce" and the picker's readiness gate keys on the presence of a machine block with in-scope entries — matching the runtime for legacy documents too (a legacy audit-mode block is read as ready, since that is what the runtime does with it).
Summary
Phase 1 of the RoE unification design (
docs/design/2026-10-22-roe-unification.md, included in this PR): engagement scope collapses to one machine-enforceable RoE generated mechanically from the creation data — always enforced, fail-closed. Today an engagement started without a provisionedplan/roe.jsonsilently runs audit-only — an authorization gate that defaults to off. Creation now is the scope declaration.Changes
build_engagement_roe(engagement_name, targets, out_of_scope=(), *, require_scope=True)(decepticon_core, pure): each target becomes an in-scopeScopeRule(host / CIDR / glob auto-detected). Targets are normalized mechanically — embedded lists split, URLs and scheme-relative//hostcollapse to the hostname, CIDRs canonicalize,host:portauthorizes the host on all ports (documented), hostnames lowercase / lose the DNS trailing dot / IDNA-encode, bracketed bare IPv6 drops the brackets, git-scp remotes reduce to the host, a*.xglob adds its apex, and patterns no real DNS/IP target can match are dropped with a warning. No derivable in-scope target raises.start_engagementand thedecepticon-cli scanpath share one materializer (decepticon.roe_materialization, free of themcpextra): writes<workspace>/plan/roe.jsonbefore dispatch and refuses to start an engagement with no scope; new optionalout_of_scopecreation parameter (CLI flag added); never overwrites an existing (reviewed)plan/roe.json.decepticon.passive_discovery): plain-hostname, URL, git-remote and glob-base targets are enriched with Certificate-Transparency subdomains (crt.sh, injectable fetcher, bounded at 500, filtered to the authorized root, best-effort — discovery failure degrades to the operator targets and never blocks creation).Intent
docs/design/2026-10-22-roe-unification.mdphase 1 (this PR). Supersedes fix(mcp): restrict start_engagement to orchestration graphs (RoE enforcement) #1055 (closed) — generating the RoE at creation covers every graph, so restrictingstart_engagementto orchestration graphs is unnecessary.moderemoval, authorization retirement, or HITL scope expansion here — phases 2–5 are separate PRs per the design doc (the launcher companion PR already pulls parts of 2 and 4 forward).Blast radius
Diff budget
docs/**,tests/**excluded per QUALITY_BAR.End-to-end verification
Ran on my machine (worktree
feat/roe-unification, uv venv, real MCP server build path):uv run python /tmp/roe-e2e.py, productionbuild_serverwith a realEngagementClient):start_engagementwith empty targets → tool errorcannot start engagement 'mcp-…': engagement RoE requires at least one in-scope target…engagements run only against an authorized scope, no thread/run created.materialize_engagement_roe("roe-e2e-check", ["https://elice.io/path", "10.0.0.5/24", "*.elice.io"], out_of_scope=["admin.elice.io"])wroteplan/roe.jsonwithmode: enforceand 179 in-scope entries — the operator targets (normalized:elice.io,10.0.0.0/24,*.elice.io) plus real CT-discovered subdomains, denylist preserved.uv run decepticon-cli scan --target … --langgraph-url http://127.0.0.1:1):--target /abs/path→ path-scopedin_scope: [];--target ./(the GitHub Actions composite default) → path-scoped RoE written, then dispatch fails on the dead URL as designed;--target https://elice.io→ 176-entry enforced scope.Testing
uv run --extra mcp pytest -n auto -q -m "not slow"→ 5988 passed, 43 skipped, 0 failed (includes the 12 shared golden normalization vectors inpackages/decepticon-core/tests/testdata/roe-target-vectors.json, also consumed by the companion launcher PR's Go tests)ruffclean on all changed filesmake quality/make smoke— not run locally (CI covers)Quality Bar self-check
except, no bare# noqa, noprint(outside the CLI's stderr contract, no mutable defaults, no TODO without issue, no mocked system-under-test)Related Issues
Supersedes #1055. Companion launcher PR: mechanical engagement creation, scope review preview, marker retirement.