You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
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 keyapprovals disappears.
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).
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/configexecutables.
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).
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.
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
allowExecutables(projectapm.yml),bin_deploy(orgapm-policy.yml), a proposedexecutables(policy), andapprovals(new user file in harden(executables): expand allowExecutables gate to mcp and canvas; store approvals user-local #1865). This is unlearnable at scale.apm-policy.ymlcan centrally DENY executables (bin_deploy.deny_all/deny) but has no field to GRANT or mandate them.effective_allow_executables()reads only projectapm.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.required package whose only payload is executable is gate-stripped to emptydeployed_files;required-packages-deployed(policy_checks.py:~198,if not locked.deployed_files) then reports a false non-compliance.~/.apm/approvals.ymlbecomes a 3rd policy file; we want net-new files = 0.Core principle
~/.apm/config(net-new files = 0).Vocabulary unification (do this in v1)
One noun everywhere:
executables. The CLI verbapm approvestays; the keyapprovalsdisappears.apm-policy.ymlbin_deploy.{deny_all,deny}(+ no grant)executables: {deny_all, deny, require, recommend, enforce}apm.ymlallowExecutablesexecutables: {allow, deny}~/.apm/configapprovals.yml(#1865)executables: {allow, deny}(delete the standalone file)allowExecutablesis kept as a deprecated alias that maps to projectexecutables.allowfor one minor cycle, with a CLI deprecation warning and a CHANGELOG migration note.bin_deployfolds intoexecutables.denyscoped to thebintype (deprecated alias kept one cycle).Recommended end-state
Surviving control surfaces
apm-policy.yml->executables:block (inheritable viaextends:, carried throughinheritance.py:merge_policies).apm.yml->executables: {allow, deny}(committed team consent;apm approvewrites here by default).~/.apm/config->executables: {allow, deny}namespace (personal, lowest authority; onlyapm approve --userpersists here).Precedence / resolution table (deny-wins total order; first match wins)
deny_all/denydenyenforceAND provenance verified (v2)enforcebut provenance NOT verifiedrecommend(fail-safe)allowallowrecommendContested 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 toapm-policy.yml, carried through the existing tighten-onlymerge_policiesratchet.deny/deny_all/requiretighten (already ratchet-safe);recommendis user-overridable;enforceis the only relax-from-above and is provenance-gated (v2). One new key, not a new file.Gap B fix
resolve_exec_decision(package, exec_type) -> ExecDecision{allowed, deciding_layer, trust_state}in the security module.policy/policy_checks.py(kills the split-brain where gate and audit each guess independently).exec_status/trust_statein {deployed,gated_pending_approval,denied,absent} to each lockfile entry.required-packages-deployed: assert package presence (trust_state != absent), notdeployed_files. Emit a separaterequired-executable-untrustedsignal for present-but-gated.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 doctoris a no-argument health-check aggregator (wrong shape for per-package introspection) andapm policy statusalready does policy-chain introspection. Therefore:apm policy explain <pkg>-- a sibling to the existingapm policy statusunder thepolicycommand group. It prints the effective decision, the deciding layer (org/project/user), and the shadowed layers (thegit config --show-origin/kubectl auth can-ianalog). One new SUBcommand, zero new top-level commands.apm doctor(no package arg -- fits doctor's existing pass/fail model) that flags conflicts and points the user toapm policy explainfor per-package detail.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 onapm-policy.ymlthroughmerge_policies(Gap A):deny_all,deny,require,recommend.executables(deprecateallowExecutablesalias), foldbin_deploy->executables.deny[bin]alias, delete standaloneapprovals.yml, personal consent under~/.apm/configexecutables.resolve_exec_decision()shared by install gate + audit.exec_status/trust_statelockfile field.required-packages-deployedpresence rewrite +required-executable-untrustedsignal (Gap B).apm approve --recommended(bulk-accept org-vetted set),apm approve --list.apm policy explain <pkg>(subcommand under existingpolicygroup) + an executable-trust-drift check inapm doctor. NO net-new top-level command.packages/apm-guideresources).enforceruntime behavior in v1.v1.5 (separate issue): hash-bind approvals --
is_package_approved(security/executables.py:~100) consultscontent_hash(already in the lockfile) and forces re-approval on content change for pinned refs.v2 (separate issue, gated):
enforcemandate 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):enforcehonored only undersecurity.integrity.require_hasheson theextends:chain + signature-verified policy source + hash/identity-pinned grants, with fail-safe degradation torecommend, the committed lockfile+policy git history as a free transparency log, andapm policy explainmaking every forced decision attributable. This is whyenforceis v2, not v1.Acceptance criteria (UX is gating)
apm-policy.ymlentry that inherits;apm policy explain <pkg>shows it as the deciding layer.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.Tracks follow-ups from #1865.