Skip to content

feat(governance): Executable Trust Governance v1 - deny-wins resolver, vocab unification, audit fix#1875

Open
danielmeppiel wants to merge 6 commits into
sergio-sisternes-epam-feat-expand-executables-gate-mcp-canvasfrom
danielmeppiel/exec-trust-governance-v1
Open

feat(governance): Executable Trust Governance v1 - deny-wins resolver, vocab unification, audit fix#1875
danielmeppiel wants to merge 6 commits into
sergio-sisternes-epam-feat-expand-executables-gate-mcp-canvasfrom
danielmeppiel/exec-trust-governance-v1

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

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 to main when #1865 merges; if it does not, rebase onto main. The diff below shows only the v1 changes layered on top of #1865's gate expansion.

References #1873. Does not close it — the enforce mandate tier, cryptographic signing, and content_hash binding 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 executables noun, replaces the old user-wins allow merge with a deny-wins precedence ladder shared by both the install gate and apm 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 as apm policy explain <pkg> plus an apm doctor drift 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:

  • GRANT/MANDATE gap (Gap A). The executables: block on apm-policy.yml was not carried through inheritance.py merge_policies, so an org could recommend/require/deny executables in policy but the install gate never saw it. Org intent silently evaporated.
  • Two resolvers, one truth. The install gate and apm audit each computed trust independently via effective_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.
  • Vocabulary sprawl. allowExecutables, bin_deploy, and a standalone ~/.apm/approvals.yml each named a slice of the same idea. Three nouns, three stores, no single answer to "is this allowed?".
  • The audit asserted the wrong thing (Gap B). required-packages-deployed checked locked.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)

Concern Before After (v1)
Vocabulary allowExecutables, bin_deploy, approvals.yml One noun executables.{allow,deny}; old names kept as deprecated aliases for one minor cycle
Personal consent standalone ~/.apm/approvals.yml folded into ~/.apm/config.json executables.{allow,deny} (lowest authority, narrow-only)
Resolution two paths, user-wins merge one resolve_exec_decision(package, exec_type){allowed, deciding_layer, trust_state}, deny-wins
Org policy executables: dropped in merge carried through merge_policies (deny ratchets, never loosens)
Lockfile none exec_status{deployed, gated_pending_approval, denied, absent}
Required audit asserts deployed_files asserts presence; new required-executable-untrusted signal
Introspection apm policy explain <pkg> + apm doctor drift check

Implementation (HOW)

  • policy/inheritance.py, policy/schema.py, policy/parser.py, policy/models.py — add the executables: block (deny_all/deny/require/recommend) to policy parsing and a _merge_executables ratchet in merge_policies (deny accumulates; an org is the ceiling on deny). bin_deploy folds into executables.deny[bin] as a deprecated alias.
  • security/executables.py — the heart: resolve_exec_decision, build_exec_trust_context, the LAYER_*/TRUST_* constants, the deny-wins ladder, vocab parsing (parse_project_executables, load_user_executables), and the Wave-5 materialization helpers. effective_allow_executables is 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 consumes resolve_exec_decision and records exec_status per locked dependency; install succeeds present-but-parked with a one-command remedy.
  • policy/policy_checks.pyrequired-packages-deployed rewritten to presence semantics; emits a separate required-executable-untrusted signal that hard-fails CI.
  • commands/approve.py--recommended (bulk-accept org recommend) and --list; explain_cmd demoted to a reusable explain_decision() function.
  • commands/policy.py — new apm policy explain <pkg> subcommand (sibling to apm policy status) calling explain_decision.
  • commands/marketplace/doctor.py — new informational executable-trust drift check flagging packages allowed locally but denied by org policy, pointing to apm policy explain.
  • Docs + CHANGELOG — Starlight pages and apm-guide skill resources updated; [Unreleased] migration note covers the allowExecutables deprecation and the approvals.yml removal.

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)"]
Loading

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"]
Loading

Trade-offs

  • Deny-wins replaces user-wins. A personal grant can no longer widen trust past a project/org decision — a deliberate authority inversion. The old user-wins behavior is gone (documented in the CHANGELOG migration note).
  • Aliases, not a hard break. allowExecutables and bin_deploy keep working for one minor cycle rather than breaking immediately, trading a little parser complexity for a clean migration.
  • approvals.yml is deleted, not dual-read forever. Personal consent migrates into ~/.apm/config.json on first read; the standalone file is removed to avoid a third store.
  • enforce rung is known but inert. The resolver knows the enforce rung exists but fail-safe degrades it to recommend — no force-execute path ships in v1 (crypto/signing are v1.5/v2).
  • Introspection as a subcommand, not a top-level command. apm policy explain + a doctor check keep net-new top-level commands at zero, at the cost of slightly longer invocation.

Benefits

  1. One verdict, always. Gate and audit share resolve_exec_decision, eliminating the class of bugs where they disagreed.
  2. Org intent is honored. Policy executables: now survives inheritance; require/recommend/deny actually reach the gate.
  3. Install stops lying. A required-but-parked package no longer reports as absent (Gap B); install succeeds with a one-command remedy.
  4. CI still safe. required-executable-untrusted hard-fails CI when a required package's executables are untrusted.
  5. Zero net-new control-surface files — the three added files are tests; introspection reuses existing command groups.

Validation

Lint contract (CI-mirror) is green, and the v1 test surfaces pass.

Lint contract + targeted test run
$ uv run --extra dev ruff check src/ tests/
All checks passed!
$ uv run --extra dev ruff format --check src/ tests/
1299 files already formatted
$ uv run --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/
Your code has been rated at 10.00/10
$ bash scripts/lint-auth-signals.sh
[+] auth-signal lint clean

$ uv run --extra dev python -m pytest tests/unit/security/test_resolve_exec_decision.py \
    tests/unit/security/test_exec_vocab_unification.py tests/unit/test_lockfile_exec_status.py \
    tests/unit/policy/test_inheritance.py tests/unit/policy/test_policy_checks.py \
    tests/integration/test_executables_gate_integration.py \
    tests/integration/test_wave7_policy_registry_coverage.py \
    tests/unit/commands/test_approve_deny.py tests/unit/commands/test_marketplace_doctor.py -q
635 passed in 2.04s

Scenario Evidence

User-promise scenario Proven by Principle
Org deny beats a project allow (deny-wins) tests/unit/security/test_resolve_exec_decision.py Governance is enforceable
allowExecutables/bin_deploy still parse (deprecated alias) tests/unit/security/test_exec_vocab_unification.py, tests/unit/policy/test_parser.py Migration without a hard break
Required-but-parked package PASSES install, ABSENT fails tests/integration/test_wave7_policy_registry_coverage.py Install tells the truth (Gap B)
Org executables: survives inheritance merge tests/unit/policy/test_inheritance.py Org intent is honored (Gap A)
Lockfile records exec_status tests/unit/test_lockfile_exec_status.py Auditable trust state
apm policy explain + doctor drift surface the deciding layer tests/unit/commands/test_approve_deny.py, tests/unit/commands/test_marketplace_doctor.py Clean admin/user UX

Note

Two EMU/ADO SSH policy-repo routing tests in tests/integration/test_dep_url_parsing_e2e.py fail 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-v1
  • uv 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 -q
  • In a project with an executables: {} block, apm policy explain owner/repo — confirm it prints the effective decision, deciding layer, and shadowed layers.
  • Add an org policy executables.deny for a locally-allowed package, run apm doctor — confirm the executable-trust drift check flags it.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

danielmeppiel and others added 4 commits June 21, 2026 18:09
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>
danielmeppiel and others added 2 commits June 21, 2026 23:08
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>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_now

Executable Trust Governance v1: one resolver, deny-wins, org policy is the ceiling -- the install gate and audit now share a single source of truth with 638 tests standing guard.

cc @danielmeppiel @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

All active panel lenses converged to empty findings after the shepherd fold pass. The architecture is clean: one ExecTrustContext value object, one resolve_exec_decision resolver, deny-wins precedence, and a bounded _effective_allow cache populated once per install. The UX remains inside the v1 acceptance gates: no top-level command, no net-new control-surface files, and introspection through apm policy explain <pkg> plus the informational apm doctor drift check.

Supply-chain security confirms the highest-risk surfaces are closed for v1: invalid exec_status values are rejected, the inert enforce rung degrades safely to recommend, and lockfile exec_status serialization is covered by a regression test. CLI logging and DevX follow-ups were folded: deny output is neutral, --list leads with a scan summary, policy explain prints a verdict headline, and noninteractive installs park executables with a one-command remedy instead of prompting.

Aligned with: portable by manifest: trust state is declarative in policy/manifest stores; secure by default: deny-wins and default gated pending approval; governed by policy: org policy is the ceiling; pragmatic as npm: approve/list/explain flows are one-command and explainable.

Growth signal. Release hook: One resolver. Deny-wins. Your org policy is the ceiling - full stop.

Panel summary

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/--all examples, relative docs links, and CHANGELOG project-deny precedence -- resolved in 5f21876.
  • (panel) Split the apm-guide apm approve wall of text into agent-readable chunks -- resolved in 5f21876.
  • (panel) Added exec_status domain validation and regression coverage -- resolved in 5f21876.
  • (panel) Simplified the required-executable-untrusted guard and corrected the comment -- resolved in 5f21876.
  • (panel) Made deny output neutral, added an approve --list summary, and added a policy explain verdict headline -- resolved in 5f21876.
  • (panel) Cached ExecTrustContext and 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_status and routed the fallback message through CommandLogger -- 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 enforce mandate runtime -- scope boundary: v1 only degrades enforce to recommend; 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 -- added forged to 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_status assignment; 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 with SystemExit(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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant