After this chapter you can write and rehearse an apm-policy.yml, inspect its effective
inherited rules, and pilot Meridian's warn → measure → remediate → block
rollout behind a protected CI gate without mistaking install-time
Governance for runtime security.
Edition 1.2 targets exactly APM 0.31.0, against the
reviewed book 1.1 / APM 0.23.1 baseline. Read
Chapter 8 first for integrity and trust boundaries, and
Chapter 7 for the baseline CI audit. Current examples below use recorded
0.31.0 explorer observations; the original Chapter 9 policies retain their historical 0.23.1 stamps.
A lockfile tells you which dependency will arrive, not whether your organization permits it.
An npm lockfile is not an approved-suppliers list; agent context needs the same distinction.
Chapter 8 introduced content scanning, hashes, and scoped
trust decisions. Those checks help detect suspicious content or changed bytes; a clean result does
not prove benign intent or authenticate a publisher. A second question has
no universal answer: may our developers install
some-org/some-skill? is the GitHub MCP server approved? which compilation targets do we
support? That depends entirely on your organization's risk posture, contracts, and
compliance regime.
That second question is Governance — APM's third
promise, “governed by policy”
(the three
promises). Where the integrity checks are built into the tool,
governance is declared: an org authors an allow/deny contract in
apm-policy.yml. Applicable policy and security checks gate agent-readable
deployment; acquisition, staging, and cache writes can happen earlier. Policy checks
declarations against organizational rules, not the meaning or future behavior of a prompt
(0.31.0 policy guide,
install boundary).
Integrity and governance answer different questions about the same installation.
Security (Chapter 8)
Governance (this chapter)
Question
Does this artifact pass the applicable integrity and trust checks?
Is this artifact allowed here?
Source of authority
Built-in checks and explicitly scoped trust decisions
Declared — the org authors apm-policy.yml
Varies by org?
Integrity tests do not depend on an approved-suppliers list
Yes — each org's own contract
Where enforced
Deployment checks and later integrity audit
Install gate andapm audit --ci
The two verdicts are independent. A package can pass its scan and hash checks yet
be denied because it comes from an un-vetted source; an
allowed source still runs through the security scan. So beware the twin misconception
— “governance is just more security,” and its inverse, “if it's allowed by policy
it must be safe.” Neither holds. Policy is not an antivirus, and an allow-list is not a safety
certificate.
Policy is deliberately visible to developers yet authoritative for the org. It fires on
a developer's own local apm install and prints an inline violation with a remediation hint
— but an authoritative org baseline must not depend on an unreviewed checkout override. As
Chapter 1 foreshadowed, apm-policy.yml is an
org-remote authority: the policy repository is protected by CODEOWNERS and branch
protection, and discovery uses your project's git remote. Local files are useful for
inheritance rehearsals and explicit audits; merely naming a checkout
file apm-policy.yml does not make install discover it.
The required CI job must protect its source selection and environment as well as its command.
Keep the install/runtime distinction from the
APM README:
apm-policy.yml governs what gets installed; your agent harness governs what
the live agent may do. This is not a runtime sandbox or a permission system for
agent behavior. APM's separate script and executable-consent surfaces remain
Chapter 8's concern.
The allow/deny contract is a YAML declaration. Dependency
rules restrict sources and require packages or bounded direct constraints; MCP rules restrict
connection declarations and transports; compilation rules restrict destinations. Manifest and
unmanaged-file rules make reviewable intent and file ownership auditable. Cache settings govern
availability of that contract, not package trust.
This is a trimmed excerpt of the current schema fixture, not an exhaustive policy
or a recommended organization-wide default. In the PowerShell examples, $Apm means the
absolute path to the prepared 0.31.0 executable, never a PATH-selected CLI.
Local commands use an isolated scratch copy of the current policy/ fixture; the
worked example explains the complete input and baseline.
backend/examples/updates/1.2/operations/policy/apm-policy.schema.yml, selected sections.
APM 0.31.0 — offline parse and rule registration observed
in policy-status-schema, exit 0; not proof every field is enforced.
For an allow list, null means no opinion, [] means
allow nothing, and a populated list means only matches.
Deny wins over allow. A require list adds mandatory packages; the
worked policy includes a version-pinned requirement.
A * glob stays within one path segment; ** can span segments
(matching rules).
Matching uses canonical GitHub/registry owner-repository casing, not a blanket
lowercase conversion of every identifier. In the registry control, OPERATIONS/**
matched operations/parent: the allow policy audited 0, the deny policy
1. GitHub normalization is source-grounded at this target; refs, subpaths,
local paths, and MCP identities are not all case-insensitive. Preserve their actual spelling
(source-aware policy matching).
Inspect what loaded, not just what you wrote
apm policy status reports the source, resolution outcome, warnings, cache state, and
effective rule counts. Its --policy-source accepts a local path for rehearsal, or an
organization/repository/HTTPS source. --check tests policy resolution,
not whether the project complies. Both the warn and block files in this chapter resolve
successfully; only their audits distinguish compliance outcomes.
Inspect policy/apm-policy.schema.yml in the scratch copy.
APM 0.31.0; policy-status-schema, offline, exit
0. Comments summarize selected observed fields.
The correct policy key is compilation.target.allow. A top-level
targets in a policy is unknown: 0.31.0 warns and registers no target rule.
That is different from the valid targets list in the consumer's
apm.yml.
Observed 0.31.0 schema negative controls; diagnostic exit is not parser acceptance.
dependencies.allow is the string microsoft/**, not a list
outcome: malformed; dependencies.allow must be a YAML list of package patterns
policy-status-wrong-list: 0; with --check: 1
require_pinned_constraint is quoted "true", not a boolean
outcome: malformed; boolean type required
policy-status-wrong-bool: 0; with --check: 1
cache.ttl is boolean true, not an integer
outcome: malformed; positive integer required
policy-status-wrong-integer: 0; with --check: 1
Known wrong native types fail parsing; plain status still exits 0
because it is a diagnostic command. An explicit malformed-policy audit can also exit
0 under the default fetch-failure posture, or 1 under the
consumer fail-closed setting. Do not equate a green command with
loaded rules
(0.31.0 parser).
The other apm policy subcommand, explain, explains executable trust
decisions from Chapter 8; it is not a policy-compliance test.
Install-time preflight.apm install discovers policy from the git
remote and evaluates applicable install rules before agent-readable deployment. Do not turn this
into a promise that acquisition and staging write nothing, or that a failed multi-package command
rolls back every earlier success. There is no --policy override on
apm install; the attempted override returned exit 2 in
policy-install-no-override-flag. A local policy rehearsal is not a live
organization-discovery test.
CI audit.apm audit --ci --policy adds policy checks to the
lockfile, ownership, integrity, and replay checks from Chapter 7.
This includes audit-only concerns such as required manifest fields, scripts, and unmanaged files.
Use it as a required merge check with protected inputs.
Passing --policy to bare audit without --ci only produced an
ignored-option warning in policy-source-without-ci; it did not enforce the policy.
Audit can produce the JSON report used below or a selected SARIF report for Code Scanning; SARIF is
not an automatic upload. A cold replay can fetch lock-pinned content into scratch, so audit is
not universally offline and there is no universal fixed check count.
This chapter's local-policy fixture needs no external package fetch
(audit modes and formats).
The enforcement dial: warn and block
The top-level enforcement field has three modes — off, warn,
block. It gives you a measurement phase before a rule becomes a merge gate.
There is no general per-rule enforcement switch. For the ordinary
required-packages violation in the worked example, with the baseline clean:
Same missing package, observed APM 0.31.0 audits: policy-audit-warn and policy-audit-block.
Mode
JSON check result
Exit code
warn
passed: true, but the missing-package message includes (enforcement: warn)
0 — measures, does not gate
block
passed: false, with the same missing package in details
1 — fails this rule
Do not generalize this into “warn makes every failure green.” Baseline failures are
independent, and security.integrity.require_hashes: true is explicitly fail-closed
even in warn mode. A deliberately hash-stripped Git lock failed
dependency-content-hashes, exit 1, in
policy-require-hash-even-warn
(check-specific fail-closed boundary).
Tighten-only inheritance: enterprise → org → repo
A shared baseline needs room for stricter product teams, not a way for each repo to undo it.
Policies compose through extends, canonically an
enterprise hub → org policy → repo child.
The merge is tighten-only: a child's relaxation attempt leaves the parent's
restriction effective. It need not make parsing fail
(0.31.0 inheritance implementation).
Tighten-only composition: target source plus local and HTTPS inheritance observations at APM 0.31.0.
Deny and require lists (dependencies.deny, dependencies.require, mcp.deny)
union
Add restrictions; an empty or null child list does not erase the parent's entries
enforcement and fetch_failure
stricter value (off < warn < block for enforcement; warn < block for fetch failure)
Escalate; never turn inherited block into warn
require_pinned_constraint
logical OR
Turn it on — never off if a parent set it
dependencies.max_depth, cache.ttl
min
Lower the cap — never raise it
The target's
companion schema page
has a conflicting inheritance table: its suggestion that child
[] can clear inherited deny/require lists, or that a child freely overrides
fetch_failure, does not describe the observed implementation.
Exceptions therefore flow upward to the owners of the governing parent. Adding an allow entry does
not cancel a deny; review and narrow the restriction itself. A child file is not a waiver.
Local-file inheritance works in 0.31.0. These two committed rehearsal files make
it observable without publishing any policy:
policy/apm-policy.parent.yml — a restrictive local parent.
APM 0.31.0; merged in
policy-local-inheritance-status, exit 0, offline.
policy/apm-policy.child.yml — a deliberate relaxation attempt, not a
recommended child policy. APM 0.31.0;
policy-local-inheritance-audit still exits 1, offline.
Here, extends_chain: [] did not mean “no merge”:
the effective block mode, retained counts, and failing audit prove inheritance. Inspect effective
rules and a known violation, not the chain array alone. This tests local composition, not automatic
organization discovery.
The separate loopback-HTTPS parent/child test retained block enforcement, block fetch posture,
TTL 120, depth 3, mandatory pins, and inherited restrictions on both
cold and warm reads. The warm read made no additional requests; its audit still failed the missing
requirement. The source caps chains at five levels; cross-host and incomplete-chain controls
returned 1 with status --check. These were anonymous local tests,
not private-host authorization tests.
Discovery follows the host, not one universal repository list
The org-remote model depends on locating the right authority.
The frozen discovery implementation gives each host its own convention for the repository
containing apm-policy.yml:
APM 0.31.0 source contracts, not live organization-discovery results.
Host
Policy location / precedence
GitHub / GitHub-class
.github-private before .github, then .apm, then
_apm. Meridian's existing .github location remains valid.
GitLab
The top-level group'sapm-policy project, not a cascade
through GitHub-style names or the nearest subgroup.
Azure DevOps
Project apm, repository apm-policy. The legacy
_apm/_apm coordinate is tried only after a 404, not after
an arbitrary authentication or network failure.
These locations come from the
actual discovery source
and GitLab adapter,
not an inferred release-note migration. Live private GitHub/GHES, GitLab, and ADO authorization and
policy publication are SKIPPED-needs-network: no organization
repository or permissions were supplied.
Policy cache now lives beneath the APM cache root, in
policy_v1/<project-key>/<source-key>.{yml,meta.json}, not
apm_modules/.policy-cache/. The default TTL is one hour; the test parent deliberately
tightens it to two minutes. A warm cache must preserve the same effective restrictions, not a
weaker child-only policy. Fleet distribution belongs in
Chapter 11.
A new policy rule is retroactive: the next apm install or CI audit in every
consuming repo runs it against dependencies that were legal yesterday. Flip straight to
enforcement: block and every repo that already has a violation fails on its next run —
you spend the day rolling back instead of rolling out
(Policy pilot).
So warn is not a lesser mode you skip — it is the measurement phase
that makes block safe. The recommended sequence is four steps:
Author in warn, rehearse locally, then review it into the
protected org policy repository. Meridian keeps meridian-finance/.github/apm-policy.yml;
the new preferred GitHub location does not require relocating the story.
Measure the warnings and structured audit findings, using JSON or a deliberately
configured SARIF/Code Scanning integration. A green warn-mode check is not a zero-violation count.
Remediate by reviewing, pinning, upgrading, or replacing offending direct
dependencies, or satisfying required-package rules. Exceptions require review at the governing
parent, not an empty child deny/require list.
Block after the pilot's findings are resolved or its parent rules deliberately
amended. Stage a small rule set or pilot a stricter child in a narrower scope:
block applies to that entire effective policy, not just one inherited rule.
Pitfall: the pin rule checks direct declarations
dependencies.require_pinned_constraint implements a narrower concept than
“every edge of the graph has a bounded range.” In 0.31.0 the check is
direct-scoped. A resolved lockfile commit does not make an unbounded direct
declaration bounded; conversely, an unbounded transitive declaration alone is not this check's
failing case
(direct pin check).
Deliberate direct violation: the registry-graph/apm.yml variant's
dependency excerpt, not a standalone manifest. The fixture retains its explicit registry routing.
APM 0.31.0; policy-pin-direct-unbounded-control,
exit 1. Loopback network only, experimental
registry fixture; no public or private registry service is implied.
Observed direct-scope control under a blocking pin policy; the registry graph has a real depth-two lock.
Root's direct constraint
Transitive constraint
Audit result / probe
^1.0.0 (bounded)
Unbounded
0 — policy-pin-direct-only-control
>=1.0.0 (no upper bound)
Unbounded
1 — policy-pin-direct-unbounded-control;
operations/parent: unbounded upper; pair with '<X.Y' or use a caret range
Both controls use require_pinned_constraint: true with enforcement: block.
Use a bounded direct constraint to remediate this finding, then install and review the
resulting lock as Chapter 6 taught.
The policy's definition of pinned includes bounded ranges; it is not limited to immutable SHAs.
Do not substitute the sample package's transitive #main edge as Ch9's failing pin demo.
Protect CI's environment as well as its command
A required job is authoritative because you control its inputs, not because its command contains
--ci. The following isolated negative controls all used the same
missing-required-package block policy. They diagnose the enforcement boundary; they are
not CI configurations to copy.
Observed APM 0.31.0 bypass controls. Exit 0 with missing policy checks is an authority failure, not compliance.
Control condition
Actual audit result
Probe
Explicit block policy, no bypass
1; missing package fails
policy-explicit-control-no-bypass
Explicit policy plus --no-policy
1; explicit source beats the flag
policy-explicit-flag-only
Explicit policy with environment APM_POLICY_DISABLE=1
0; only baseline checks, explicit policy suppressed
policy-explicit-env-only
Explicit policy, environment disable, and --no-policy
0; still suppressed
policy-explicit-overrides-disable
Environment disable plus explicit policy and consumer fail-closed configuration
0; fail-closed fetch posture does not defeat the disable switch
policy-disable-even-failclosed
Thus explicit --policy beats --no-policy, but not
APM_POLICY_DISABLE=1 in this release, despite broader precedence wording
in the help/docs. Protect the required workflow, its selected policy source, consumer settings,
and environment from unreviewed changes. Ensure the disable switch cannot arrive enabled in the
required job. Run status and audit under the same controlled inputs, inspect the
actual loaded source, outcome, enforcement, warnings, and effective rules, and
retain the rule findings. Require the resolution preflight to succeed; do not discard its failure
when a later command runs. A baseline-only exit 0 cannot establish organization compliance.
Fail closed from the consumer, before the first fetch
An unread remote policy cannot tell a consumer what to do when fetching it fails.
Put the failure default in the consumer's reviewed apm.yml when policy availability is
mandatory. This is distinct from fetch_failure inside the policy, and from the
violation-mode enforcement dial:
Consumer apm.yml fragment used by the recorded
policy_followup.py controls, based on the current policy/ fixture.
APM 0.31.0;
policy-cold-fetch-consumer-block returned 1 on a synthetic HTTPS
503. Loopback network only.
policy:
fetch_failure_default: block
A consumer policy.hash pins the fetched policy's raw bytes; it is not a publisher
signature. In the nonempty-policy test, adding only a harmless comment broke that pin.
Status with --check rejected the mismatch, but audit with the default consumer
posture still exited 0 and skipped policy. Adding the setting above made the same
audit visibly fail with Policy fetch failed: Policy hash mismatch, exit 1.
Actual APM 0.31.0 audit exits with the disable switch absent; all baselines were clean.
Freshness is another separate check. After a real one-second cache TTL expired and refresh returned
503, a cached policy with fetch_failure: block gave status
--check exit 1 (outcome: cached_stale), while audit evaluated
the cached rules and exited 0 on that compliant fixture. If freshness is mandatory,
require the resolution preflight as well as rule audit. These anonymous loopback results
narrow the broad fail-closed wording in the
target policy reference;
they do not certify a live organization's policy service.
Keep the rehearsal smaller than the rollout. A missing required package cleanly isolates the
warn/block dial, without relying on plural-target audit coverage,
a transitive pin violation, or access to Meridian's private sources.
A minimal current warn/block rehearsal
Use backend/examples/updates/1.2/operations/policy/, not the similarly
named historical Chapter 9 files. Copy the whole fixture, including its local
.apm/ instruction and genuine apm.lock.yaml, into a fresh, short scratch
directory with isolated HOME/config/cache and no git remote. Do not execute it in the long book
checkout or alter the book's dependencies. The
fixture runner instructions
describe this isolation; the runner's policy suite also reproduces the labelled bypass
limitation, rather than hiding it.
policy/apm.yml — the current local consumer, with Meridian's three targets.
APM 0.31.0; policy-frozen and
policy-audit-ci observed 0 / 0, offline. The full fixture is required.
# Local policy rehearsals use explicit --policy; this is not a published authority.
name: operations-policy
version: 1.0.0
description: Safe local policy rehearsal.
targets: [copilot, claude, cursor]
dependencies:
apm: []
mcp: []
includes: auto
First materialize the unchanged locked local content and establish that the baseline is clean.
This fixture deliberately has no org remote; its successful baseline audit is not
organization-policy compliance. Do not add the production consumer fail-closed setting to this
no-authority baseline and expect it to remain green.
Baseline-only control in the complete scratch fixture.
APM 0.31.0; policy-frozen /
policy-audit-ci, observed 0 / 0, offline.
& $Apm install --frozen
# exit 0: local deployment restored; lock unchanged
& $Apm audit --ci --no-fail-fast -f json
# exit 0: baseline clean
# Warning: Could not determine org from git remote; enforcement skipped
Now use one policy-only finding: the manifest does not declare
example-org/required-review-package#v1.0.0. This is a deliberately absent fixture name,
not a package you should try to fetch. Audit checks the declaration; it does not
install that missing requirement.
policy/apm-policy.warn.yml — measure one missing requirement.
APM 0.31.0; policy-audit-warn,
observed exit 0with a finding, offline.
# An absent required package is a policy-only violation; nothing is fetched.
name: Operations warning rehearsal
enforcement: warn
dependencies:
require: [example-org/required-review-package#v1.0.0]
policy/apm-policy.block.yml — the identical rule becomes blocking.
APM 0.31.0; policy-audit-block,
observed exit 1, offline. The different display name does not change the rule.
# Same violation as warn; only the outcome dial changes.
name: Operations blocking rehearsal
enforcement: block
dependencies:
require: [example-org/required-review-package#v1.0.0]
Inspect each actual policy source, then audit that same file in the same controlled environment.
Both status checks succeed because the policies load; the audits tell you what enforcement does
to the same violation:
Load and evaluate the two policies. APM 0.31.0, offline;
policy-status-warn / policy-audit-warn: 0 / 0;
policy-status-block / policy-audit-block: 0 / 1.
& $Apm policy status --policy-source ./apm-policy.warn.yml --json --check
# exit 0: source file:apm-policy.warn.yml; outcome found; enforcement warn
# rule_counts.dependencies_require: 1
& $Apm audit --ci --policy ./apm-policy.warn.yml --no-fail-fast -f json
# exit 0: required-packages finding is reported as passed (enforcement: warn)
& $Apm policy status --policy-source ./apm-policy.block.yml --json --check
# exit 0: source file:apm-policy.block.yml; outcome found; enforcement block
# rule_counts.dependencies_require: 1
& $Apm audit --ci --policy ./apm-policy.block.yml --no-fail-fast -f json
# exit 1: required-packages failed; the same package is still missing
Selected required-packages fields from the actual warn/block JSON reports.
Policy
passed
message
details entry
warn
true
1 required package(s) missing from manifest (enforcement: warn)
example-org/required-review-package
block
false
1 required package(s) missing from manifest
example-org/required-review-package
This deliberately holds the violation constant so the final failure is visible. In Meridian's
rollout, remediation happens before the switch: satisfy the reviewed requirement
or amend it at the owning parent, then re-audit before enforcing. Both JSON modes identify the
offending package; block is not the first opportunity to find its name. The standards package
they will produce in Chapter 10 gives that future requirement
something real to name.
Security vs. governance are two questions at one gate.Security checks integrity and scoped trust;
Governance asks “is it allowed
here?” Neither an allow list nor a clean scan proves benign intent.
Policy gates installation, not the live agent's capabilities.
Write and inspect the real schema. Target rules use
compilation.target.allow. Unknown top-level keys warn; known invalid types fail
parsing. allow: null = no opinion, [] = allow nothing;
deny wins. Status resolution is not rule compliance.
Know each check's scope. Install discovers policy automatically; explicit
sources belong to status and CI audit. The pin rule checks direct declarations, plural-only
target audit has a coverage gap, and mcp.trust_transitive is parsed but not enforced.
Keep Chapter 8's actual MCP trust boundary separate.
The rollout is warn → measure → remediate → block.
The same missing requirement audited 0 in warn and 1 in block.
Baseline and mandatory-hash failures are not made harmless by the warn dial.
Inheritance is tighten-only. Enterprise → org → repo:
allows intersect, deny/require lists union, severity escalates. Local extends
works; empty child lists cannot erase parent restrictions. Warm cache must retain them too.
Exceptions require parent-owner review.
Protect the enforcement point. Host-specific discovery, consumer
policy.fetch_failure_default: block, the actual loaded policy, and controlled CI
inputs matter. Explicit policy beats the bypass flag, not the observed environment disable.
A hash mismatch alone did not guarantee audit failure.
The engine remains early preview at APM 0.31.0.
Pin the executable and retain both successful and deliberately failing controls; historical
0.23.1 examples are not fresh verification.
Next
You have now consumed, locked, maintained, secured, and governed agent context — the full
four-property arc for a consumer. Chapter 10 — Becoming
a Producer flips the perspective: packaging and publishing your own reusable skills, prompts, and
plugins so other teams install them through the same loop — and shipping them shaped to pass the
very Governance gate you just wrote.