feat(governance): Executable Trust Governance v1 - deny-wins resolver, vocab unification, audit fix#1875
Conversation
Wave 1-3 of executable-trust governance v1:
- ExecutablesPolicy org block carried through merge_policies (Gap A)
- resolve_exec_decision() deny-wins precedence resolver shared by
gate + audit; enforce tier fail-safe degrades to recommend in v1
- vocabulary unification onto one noun 'executables': project
executables:{allow,deny} with allowExecutables as deprecated alias;
user consent on ~/.apm/config.json with approvals.yml migrate+delete
- lockfile exec_status field {deployed,gated_pending_approval,denied,absent}
- required-packages-deployed rewritten to assert PRESENCE not
deployed_files; new required-executable-untrusted CI signal (Gap B)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s vocab Wave 4 of #1873 v1: default approve/deny target the project apm.yml executables.{allow,deny} block (committed, admin UX); --user persists to ~/.apm/config.json (lowest authority). Add --recommended (bulk-accept org recommend set), --list (effective decision + deciding layer per package), and apm explain (effective decision + deciding/shadowed layers + one-command remedy). Register explain_cmd in cli.py. Rewrite approve/deny tests onto the project-default model and cover the new surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ns resolver (v1, #1873) Replace the legacy {**project, **user} user-wins merge with the shared deny-wins resolver across the whole install path: - effective_allow_executables() is now a back-compat shim over the resolver; filter_mcp/read_bundle route through it (dict-or-None contract guarded so an unparsed in-memory signal degrades to no project layer instead of crausing strict validation). - template._effective_allow builds and caches an ExecTrustContext on ctx.exec_trust_ctx (org policy + project apm.yml) and materialises the allow-map via the single resolver. - exec_gate records per-package exec_status (deployed / gated_pending_approval / denied) into ctx.package_exec_status; candidate keys gain bare-name + version-blind aliases so org-recommend and 'apm approve <name>' grants match any version. - lockfile phase attaches exec_status to each dependency. - integrate prompt persists personal consent to ~/.apm/config.json. Tests: fix stale MCP/CANVAS-not-enforced assertions and 2-tuple gate unpacks (now 4-tuple) on the #1865 base; move the approve/deny CLI integration tests onto the unified executables.{allow,deny} noun; rewrite the Gap B required-packages-deployed test to presence semantics; add focused unit coverage for materialize_exec_map and exec_status_for_declaration. Refs #1873 (does not close; v1.5/v2 remain). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lain + doctor drift check Per maintainer anti-sprawl guidance (#1873), drop the net-new top-level `apm explain` command. Introspection now lives as: - `apm policy explain <pkg>`: a subcommand under the existing policy group (sibling to `apm policy status`), reusing explain_decision() from approve.py. - `apm doctor`: a fleet-level executable-trust drift check (informational) that flags packages allowed locally but denied by org policy, pointing to `apm policy explain` for detail. Bulk consent view stays on `apm approve --list`. Net effect: one new subcommand + one doctor check, ZERO net-new top-level commands. Also lands Wave 6: CHANGELOG [Unreleased] v1 entry and the docs corpus update (Starlight pages + apm-guide resources), all referencing `apm policy explain`. Refs #1873 (does not close; v1.5/v2 remain). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tighten executable trust UX, docs, lockfile validation, and install caching after the advisory panel. This keeps v1 inside the scoped deny-wins resolver surface while aligning CI install behavior, audit wording, and regression coverage for exec_status persistence. Addresses panel follow-ups from the shepherd-driver review of PR #1875. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fold the final panel nits by typing the exec_status map and routing the noninteractive install fallback through CommandLogger instead of direct console helpers. Addresses final apm-review-panel follow-ups on PR #1875. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 0 | Clean single-resolver architecture with typed context caching. |
| CLI Logging Expert | 0 | 0 | 0 | CLI output is neutral, scannable, and actionable. |
| DevX UX Expert | 0 | 0 | 0 | Admin/user UX remains compact and familiar. |
| Supply Chain Security Expert | 0 | 0 | 0 | Deny-wins, inert enforce, and exec_status validation are safe for v1. |
| OSS Growth Hacker | 0 | 0 | 0 | Strong enterprise-adoption story with the docs/guide hook tightened. |
| Doc Writer | 0 | 0 | 0 | Docs now match the v1 behavior and boundaries. |
| Test Coverage Expert | 0 | 0 | 0 | Regression traps cover lockfile state, noninteractive remedy, and invalid status rejection. |
| Performance Expert | 0 | 0 | 0 | Policy/user file reads are amortized by the install-context cache. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Recommendation
Merge immediately. The final panel stance is ship_now; all active panelists returned empty findings after the fold pass. The v1 boundary is precisely held. The enforce-mandate runtime, signing, content_hash binding, and extends-chain verification remain separate v1.5/v2 design work.
Folded in this run
- (panel) Clarified version-blind grant semantics, CI install/audit behavior,
--pending/--allexamples, relative docs links, and CHANGELOG project-deny precedence -- resolved in 5f21876. - (panel) Split the apm-guide
apm approvewall of text into agent-readable chunks -- resolved in 5f21876. - (panel) Added
exec_statusdomain validation and regression coverage -- resolved in 5f21876. - (panel) Simplified the
required-executable-untrustedguard and corrected the comment -- resolved in 5f21876. - (panel) Made deny output neutral, added an
approve --listsummary, and added apolicy explainverdict headline -- resolved in 5f21876. - (panel) Cached
ExecTrustContextand the materialized allow map once per install -- resolved in 5f21876. - (panel) Routed doctor through public executable-trust helpers instead of private command imports -- resolved in 5f21876.
- (panel) Added the noninteractive install park-with-remedy path and test -- resolved in 5f21876.
- (panel) Typed
package_exec_statusand routed the fallback message throughCommandLogger-- resolved in 74a8f1a.
Copilot signals reviewed
No copilot-pull-request-reviewer[bot] review comments were present in either shepherd fetch round.
Deferred (out-of-scope follow-ups)
These items cross the stated v1 scope of this PR and remain separate design work:
- (panel) Implement the
enforcemandate runtime -- scope boundary: v1 only degradesenforcetorecommend; force-execute behavior needs a dedicated v1.5/v2 design gate. - (panel) Add cryptographic signing -- scope boundary: signing is explicitly outside Executable Trust Governance v1.
- (panel) Bind approvals to
content_hash-- scope boundary: content_hash binding is a v1.5/v2 integrity design, not a v1 resolver/audit fold. - (panel) Verify signatures across
extends:chains -- scope boundary: extends-chain signature verification belongs to the future signing design.
Regression-trap evidence (mutation-break gate)
tests/unit/test_lockfile_exec_status.py::test_invalid_exec_status_rejected-- addedforgedto the allowed status set; test FAILED as expected; guard restored.tests/integration/test_executables_gate_integration.py::TestLockfileExecStatusIntegration::test_attach_exec_status_serializes_to_lockfile_yaml-- cleared the_attach_exec_statusassignment; test FAILED as expected; guard restored.tests/integration/test_executables_gate_integration.py::TestNonInteractiveExecutablePromptIntegration::test_noninteractive_blocked_executables_warn_without_exit-- replaced the noninteractive return withSystemExit(1); test FAILED as expected; guard restored.
Lint contract
uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ both silent. Full local mirror also passed pylint R0801, auth-signal lint, and CI grep guardrails.
CI
Green on head 74a8f1a299977f11ce90bfd7c10b6204d6cf5ed7: build pass (48s), license/cla pass, deploy skipped.
Mergeability status
Captured from gh pr view 1875 --json mergeable,mergeStateStatus,statusCheckRollup immediately after the last push of this run.
| PR | head SHA | CEO stance | iters | folds | defers | Copilot rounds | CI | mergeable | mergeStateStatus | notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #1875 | 74a8f1a |
ship_now | 2 | 9 | 4 | 2 | green | MERGEABLE | CLEAN | stacked on #1865 |
Convergence
2 outer iteration(s); 2 Copilot round(s). Final panel verdict: ship_now.
Ready for maintainer review.
Full per-persona findings
All active panelists returned no remaining findings after the shepherd fold pass. Auth expert was inactive because no auth/token/credential resolution files or flows were touched.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Important
This PR is stacked on #1865 (base branch
sergio-sisternes-epam-feat-expand-executables-gate-mcp-canvas) and must merge after it. GitHub auto-retargets the base tomainwhen #1865 merges; if it does not, rebase ontomain. The diff below shows only the v1 changes layered on top of #1865's gate expansion.References #1873. Does not close it — the
enforcemandate tier, cryptographic signing, andcontent_hashbinding remain explicitly out of scope for v1.5/v2.TL;DR
v1 of Executable Trust Governance makes executable trust one concept with one resolver. It unifies the vocabulary onto a single
executablesnoun, replaces the olduser-winsallow merge with a deny-wins precedence ladder shared by both the install gate andapm audit, and rewrites the required-package audit to assert package presence (not deployed files) so install succeeds present-but-parked while CI still hard-fails on an untrusted required executable. Zero net-new control-surface files (the three new files are tests); introspection lands asapm policy explain <pkg>plus anapm doctordrift check — no new top-level command.Problem (WHY)
Before this PR, executable trust was expressed three different ways that could not agree, and the audit lied about what "deployed" meant:
executables:block onapm-policy.ymlwas not carried throughinheritance.py merge_policies, so an org couldrecommend/require/denyexecutables in policy but the install gate never saw it. Org intent silently evaporated.apm auditeach computed trust independently viaeffective_allow_executables()using a{**project, **user}user-wins merge — a personal grant could widen past a project or org decision. Trust that disagrees with itself is not trust.allowExecutables,bin_deploy, and a standalone~/.apm/approvals.ymleach named a slice of the same idea. Three nouns, three stores, no single answer to "is this allowed?".required-packages-deployedcheckedlocked.deployed_files, so a required package whose executables were correctly parked (pending approval) was reported as absent — a false "missing dependency" that blocked install for doing exactly the right thing.Grounding these four behaviors in one deterministic resolver follows the PROSE principle that "Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action." — the gate and the audit can no longer reach different verdicts because they call the same function.
Approach (WHAT)
allowExecutables,bin_deploy,approvals.ymlexecutables.{allow,deny}; old names kept as deprecated aliases for one minor cycle~/.apm/approvals.yml~/.apm/config.jsonexecutables.{allow,deny}(lowest authority, narrow-only)user-winsmergeresolve_exec_decision(package, exec_type)→{allowed, deciding_layer, trust_state}, deny-winsexecutables:dropped in mergemerge_policies(deny ratchets, never loosens)exec_status∈{deployed, gated_pending_approval, denied, absent}deployed_filesrequired-executable-untrustedsignalapm policy explain <pkg>+apm doctordrift checkImplementation (HOW)
policy/inheritance.py,policy/schema.py,policy/parser.py,policy/models.py— add theexecutables:block (deny_all/deny/require/recommend) to policy parsing and a_merge_executablesratchet inmerge_policies(deny accumulates; an org is the ceiling on deny).bin_deployfolds intoexecutables.deny[bin]as a deprecated alias.security/executables.py— the heart:resolve_exec_decision,build_exec_trust_context, theLAYER_*/TRUST_*constants, the deny-wins ladder, vocab parsing (parse_project_executables,load_user_executables), and the Wave-5 materialization helpers.effective_allow_executablesis retained only as a guarded compatibility shim.install/exec_gate.py,install/template.py,install/context.py,install/phases/*,deps/lockfile.py— the install gate now consumesresolve_exec_decisionand recordsexec_statusper locked dependency; install succeeds present-but-parked with a one-command remedy.policy/policy_checks.py—required-packages-deployedrewritten to presence semantics; emits a separaterequired-executable-untrustedsignal that hard-fails CI.commands/approve.py—--recommended(bulk-accept orgrecommend) and--list;explain_cmddemoted to a reusableexplain_decision()function.commands/policy.py— newapm policy explain <pkg>subcommand (sibling toapm policy status) callingexplain_decision.commands/marketplace/doctor.py— new informational executable-trust drift check flagging packages allowed locally but denied by org policy, pointing toapm policy explain.apm-guideskill resources updated;[Unreleased]migration note covers theallowExecutablesdeprecation and theapprovals.ymlremoval.Diagrams
Legend: the deny-wins precedence ladder — the first matching rung decides; deny always wins; default is gated pending approval, not a hard deny.
flowchart TD A["resolve_exec_decision(pkg, exec_type)"] --> G{gate enabled?} G -- no --> ALLOW["allowed (gate off)"] G -- yes --> R1{org deny_all / deny?} R1 -- yes --> DENY["denied — org ceiling"] R1 -- no --> R2{user deny?} R2 -- yes --> DENY R2 -- no --> R3{project deny?} R3 -- yes --> DENY R3 -- no --> R4{project allow?} R4 -- yes --> OK["allowed"] R4 -- no --> R5{user allow?} R5 -- yes --> OK R5 -- no --> R6{org recommend?} R6 -- yes --> OK R6 -- no --> GATED["gated_pending_approval (approvable)"]Legend: one resolver is the single source of truth for both the runtime gate and the audit, so they cannot disagree.
flowchart LR CTX["build_exec_trust_context<br/>(org policy + project apm.yml + user config)"] --> RES["resolve_exec_decision"] RES --> GATE["install gate<br/>exec_gate.py"] RES --> AUDIT["apm audit<br/>policy_checks.py"] GATE --> LOCK["lockfile exec_status"] AUDIT --> SIG["required-executable-untrusted"]Trade-offs
user-winsbehavior is gone (documented in the CHANGELOG migration note).allowExecutablesandbin_deploykeep working for one minor cycle rather than breaking immediately, trading a little parser complexity for a clean migration.approvals.ymlis deleted, not dual-read forever. Personal consent migrates into~/.apm/config.jsonon first read; the standalone file is removed to avoid a third store.enforcerung is known but inert. The resolver knows theenforcerung exists but fail-safe degrades it torecommend— no force-execute path ships in v1 (crypto/signing are v1.5/v2).apm policy explain+ a doctor check keep net-new top-level commands at zero, at the cost of slightly longer invocation.Benefits
resolve_exec_decision, eliminating the class of bugs where they disagreed.executables:now survives inheritance;require/recommend/denyactually reach the gate.required-executable-untrustedhard-fails CI when a required package's executables are untrusted.Validation
Lint contract (CI-mirror) is green, and the v1 test surfaces pass.
Lint contract + targeted test run
Scenario Evidence
denybeats a projectallow(deny-wins)tests/unit/security/test_resolve_exec_decision.pyallowExecutables/bin_deploystill parse (deprecated alias)tests/unit/security/test_exec_vocab_unification.py,tests/unit/policy/test_parser.pytests/integration/test_wave7_policy_registry_coverage.pyexecutables:survives inheritance mergetests/unit/policy/test_inheritance.pyexec_statustests/unit/test_lockfile_exec_status.pyapm policy explain+ doctor drift surface the deciding layertests/unit/commands/test_approve_deny.py,tests/unit/commands/test_marketplace_doctor.pyNote
Two EMU/ADO SSH policy-repo routing tests in
tests/integration/test_dep_url_parsing_e2e.pyfail on the #1865 base and are unrelated to executable trust — inherited, out of scope for this PR.How to test
git checkout danielmeppiel/exec-trust-governance-v1uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/uv run --extra dev python -m pytest tests/unit/security tests/unit/policy tests/integration/test_executables_gate_integration.py -qexecutables: {}block,apm policy explain owner/repo— confirm it prints the effective decision, deciding layer, and shadowed layers.executables.denyfor a locally-allowed package, runapm doctor— confirm the executable-trust drift check flags it.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com