Skip to content

Executable Trust Governance: unify vocabulary, close the policy GRANT/MANDATE gap, and fix the required-package audit #1873

Description

@danielmeppiel

Summary

APM governs executable trust (hooks, bin, MCP servers, canvas Node bundles) across two unlinked planes today, with sprawling vocabulary and a structural asymmetry. This issue specifies a unified, phased design. It is the design follow-up surfaced by #1865 (whose narrow security wins can merge independently).

Decision arbitrated by the apm-review-panel + a 4-expert enterprise-governance panel (admin control-plane, developer-consent DX, supply-chain/SBOM, minimal-surface architect) + CEO synthesis.


Problem

  1. Vocabulary sprawl. The same concept wears four different key names across files: allowExecutables (project apm.yml), bin_deploy (org apm-policy.yml), a proposed executables (policy), and approvals (new user file in harden(executables): expand allowExecutables gate to mcp and canvas; store approvals user-local #1865). This is unlearnable at scale.
  2. Gap A -- asymmetry. apm-policy.yml can centrally DENY executables (bin_deploy.deny_all/deny) but has no field to GRANT or mandate them. effective_allow_executables() reads only project apm.yml + the user file, never policy, and user wins on conflict. An admin cannot roll a required hook/MCP out to a 1-5K+ fleet.
  3. Gap B -- audit false-positive. A required package whose only payload is executable is gate-stripped to empty deployed_files; required-packages-deployed (policy_checks.py:~198, if not locked.deployed_files) then reports a false non-compliance.
  4. Control-surface proliferation risk. A standalone ~/.apm/approvals.yml becomes a 3rd policy file; we want net-new files = 0.

Core principle

Denial delegates upward; execution does not -- unless the artifact is cryptographically pinned.

  • Org is always the ceiling on DENY (deny-wins, absolute).
  • Granting execution of unseen/unsigned third-party code stays a developer consent decision.
  • Org may force execution (mandate) only when the policy source is signature/hash-verified AND the grant is hash/identity-pinned (deferred to v2).
  • Personal consent folds into the existing ~/.apm/config (net-new files = 0).

Vocabulary unification (do this in v1)

One noun everywhere: executables. The CLI verb apm approve stays; the key approvals disappears.

Plane File Today Target key
Org apm-policy.yml bin_deploy.{deny_all,deny} (+ no grant) executables: {deny_all, deny, require, recommend, enforce}
Project apm.yml allowExecutables executables: {allow, deny}
User ~/.apm/config new approvals.yml (#1865) executables: {allow, deny} (delete the standalone file)
  • allowExecutables is kept as a deprecated alias that maps to project executables.allow for one minor cycle, with a CLI deprecation warning and a CHANGELOG migration note.
  • bin_deploy folds into executables.deny scoped to the bin type (deprecated alias kept one cycle).

Recommended end-state

Surviving control surfaces

  • apm-policy.yml -> executables: block (inheritable via extends:, carried through inheritance.py:merge_policies).
  • apm.yml -> executables: {allow, deny} (committed team consent; apm approve writes here by default).
  • ~/.apm/config -> executables: {allow, deny} namespace (personal, lowest authority; only apm approve --user persists here).

Precedence / resolution table (deny-wins total order; first match wins)

# Rule Outcome
1 ORG deny_all / deny DENIED (absolute)
2 USER deny DENIED (narrowing always allowed)
3 ORG enforce AND provenance verified (v2) FORCED-ALLOW
4 ORG enforce but provenance NOT verified degrades to recommend (fail-safe)
5 PROJECT allow ALLOWED (overridable only by USER deny)
6 USER allow ALLOWED
7 ORG recommend default-allow, user-overridable
8 (no match) DENIED (secure-by-default)

Contested cells: org-vs-user on DENY -> org wins (user may narrow further); org-vs-user on GRANT of unsigned third-party -> user wins (org lever is only recommend); org-vs-user on GRANT of pinned/signed -> org wins silently (v2 only).


Gap A fix

Add the executables: block to apm-policy.yml, carried through the existing tighten-only merge_policies ratchet. deny/deny_all/require tighten (already ratchet-safe); recommend is user-overridable; enforce is the only relax-from-above and is provenance-gated (v2). One new key, not a new file.

Gap B fix

  • Extract one resolver: resolve_exec_decision(package, exec_type) -> ExecDecision{allowed, deciding_layer, trust_state} in the security module.
  • Consume it from both the install gate and policy/policy_checks.py (kills the split-brain where gate and audit each guess independently).
  • Add exec_status / trust_state in {deployed, gated_pending_approval, denied, absent} to each lockfile entry.
  • Rewrite required-packages-deployed: assert package presence (trust_state != absent), not deployed_files. Emit a separate required-executable-untrusted signal for present-but-gated.
  • UX: install succeeds present-but-parked with a one-command remedy; CI hard-fails on untrusted-required.

Scaling tool: apm policy explain <pkg> (reuse an existing namespace -- NO net-new top-level command)

The "why is this decision what it is" introspection does not exist today, but it must NOT become a net-new top-level verb. apm doctor is a no-argument health-check aggregator (wrong shape for per-package introspection) and apm policy status already does policy-chain introspection. Therefore:

  • Add apm policy explain <pkg> -- a sibling to the existing apm policy status under the policy command group. It prints the effective decision, the deciding layer (org/project/user), and the shadowed layers (the git config --show-origin / kubectl auth can-i analog). One new SUBcommand, zero new top-level commands.
  • Add a fleet-level executable-trust drift check to apm doctor (no package arg -- fits doctor's existing pass/fail model) that flags conflicts and points the user to apm policy explain for per-package detail.
  • Bulk consent view stays on apm approve --list.

This keeps the CLI control surface from sprawling the same way the file vocabulary was unified onto one noun.


Phasing

v1 (this issue's implementation scope):

  • executables: block on apm-policy.yml through merge_policies (Gap A): deny_all, deny, require, recommend.
  • Vocabulary unification: project executables (deprecate allowExecutables alias), fold bin_deploy -> executables.deny[bin] alias, delete standalone approvals.yml, personal consent under ~/.apm/config executables.
  • resolve_exec_decision() shared by install gate + audit.
  • exec_status/trust_state lockfile field.
  • required-packages-deployed presence rewrite + required-executable-untrusted signal (Gap B).
  • apm approve --recommended (bulk-accept org-vetted set), apm approve --list.
  • apm policy explain <pkg> (subcommand under existing policy group) + an executable-trust-drift check in apm doctor. NO net-new top-level command.
  • CHANGELOG migration note + docs (Starlight pages + packages/apm-guide resources).
  • No crypto, no silent execution, no enforce runtime behavior in v1.

v1.5 (separate issue): hash-bind approvals -- is_package_approved (security/executables.py:~100) consults content_hash (already in the lockfile) and forces re-approval on content change for pinned refs.

v2 (separate issue, gated): enforce mandate tier + extends:-chain signature/hash verification + publisher-identity trust mode + break-glass. Mandatory force-execute does not ship before this.


The one accepted risk + mitigation

Keeping the door open to org-forced execution (v2) reintroduces relax-from-above into a tighten-only system -- a potential RCE push channel over extends:. Mitigation (the firebreak): enforce honored only under security.integrity.require_hashes on the extends: chain + signature-verified policy source + hash/identity-pinned grants, with fail-safe degradation to recommend, the committed lockfile+policy git history as a free transparency log, and apm policy explain making every forced decision attributable. This is why enforce is v2, not v1.


Acceptance criteria (UX is gating)

  • Admin verdict: an admin enables a fleet-wide recommended executable with a single apm-policy.yml entry that inherits; apm policy explain <pkg> shows it as the deciding layer.
  • User verdict: a developer accepts the org-vetted set with one apm approve --recommended; a required-but-unapproved package installs present-but-parked with an actionable one-line remedy; apm policy explain <pkg> is legible in plain English.
  • Net-new control-surface files = 0.
  • Gap A and Gap B both closed with the shared resolver; v1 ships no crypto/enforce behavior.

Tracks follow-ups from #1865.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/content-securityUnicode scanning, Glassworm, apm audit content checks, SARIF output.breaking-changepriority/highShips in current or next milestonestatus/needs-designDirection approved, design discussion required before code.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/governanceGoverned by policy. apm-policy, audit, enforcement, enterprise rollout.theme/securitySecure by default. Content scanning, lockfile integrity, MCP trust boundaries.type/architectureDesign-impacting change (new module, pattern, contract).

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions