feat(policy): enforce apm-policy.yml at install time#832
feat(policy): enforce apm-policy.yml at install time#832danielmeppiel merged 17 commits intomainfrom
Conversation
Wave 1 of issue #827 implementation. Lays the foundations the install pipeline gate (W2) will plug into. No behaviour change yet — install still does NOT enforce policy until W2 wires the gate phase. What's in: - policy_checks: new public seam run_dependency_policy_checks(deps, lockfile=, policy=, mcp_deps=, effective_target=) accepting a resolved dep set; old run_policy_checks(project_root, policy) is now a thin wrapper. Honours require_resolution: project-wins for version-pin mismatches only. Latent isinstance(allow, list) bug fixed for schema's Tuple[str, ...]. - policy/discovery: cache stores merged effective policy with chain metadata + fingerprint. Atomic writes via temp + os.replace, with pid+thread_id suffix to prevent concurrent-writer collision. MAX_STALE_TTL=7d ceiling on cache reuse. PolicyFetchResult expanded to express 9 outcomes (found, absent, cached_stale, cache_miss_fetch_fail, malformed, disabled, garbage_response, no_git_remote, empty). - diagnostics: CATEGORY_POLICY constant + per-category renderer wired into render_summary(). - command_logger: InstallLogger.policy_resolved/violation/disabled with per-class actionable error wording (auth/unreachable/malformed/ blocked). - tests/fixtures/policy/: 14 policy fixtures + 7 project fixtures (denied-direct, denied-transitive, required-missing, required-version-mismatch, mcp-denied, target-mismatch, unpacked-bundle) covering W4 live matrix scenarios L2/L4/L13 and rubber-duck findings I5/I6/I7/N14/C2. - docs: 12-section Install-time enforcement guide skeleton in both enterprise/policy-reference.md and packages/apm-guide skill mirror. 10 sections filled; sections 7 (snippets) and 10 (error table) stubbed for W3-docs-final once W2 lands and W4 captures live output. Tests: - tests/unit: 4878 passed (1 pre-existing unrelated MCP failure deselected). Includes 41 logger + 29 policy-seam + 38 cache + 21 fixture-load new tests. Refs: #827 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wave 2A wires the three install-time enforcement sites planned for #827: 1. **Pipeline gate phase** (src/apm_cli/install/phases/policy_gate.py): New phase running between resolve and targets. Discovers org policy, resolves the inheritance chain via resolve_policy_chain, persists the merged effective policy + chain refs to cache (chain_refs threading per C1 amendment), then calls run_dependency_policy_checks against the resolved deps. Routes 9 discovery outcomes (found, absent, cached_stale, cache_miss_fetch_fail, malformed, disabled, garbage_response, no_git_remote, empty). Block-mode violations raise PolicyViolationError to halt the pipeline cleanly. 2. **--mcp branch preflight** (src/apm_cli/policy/install_preflight.py + commands/install.py:1091-1125): apm install --mcp does NOT enter the install pipeline. New shared helper run_policy_preflight() runs discovery + dep checks for any non-pipeline command site. Wired into --mcp BEFORE _run_mcp_install so denied servers never reach the integrator. Also exports PolicyBlockError for callers. 3. **install <pkg> snapshot+rollback** (commands/install.py): apm install <pkg> mutates apm.yml BEFORE the pipeline runs. We now snapshot apm.yml as raw bytes (not parsed YAML, to avoid round-trip drift on whitespace / key-order / comments), and on ANY pipeline failure (policy block, download error, etc.) restore byte-for-byte via tempfile + os.replace atomic write. Logs '[i] apm.yml restored to its previous state.' and exits non-zero. InstallContext gains policy_fetch, policy_enforcement_active, no_policy. Tests: +68 new tests, 4946 unit tests pass total. - test_policy_gate_phase.py: 27 (covers all 9 outcomes) - test_mcp_preflight_policy.py: 22 (escape hatches, allow/deny, transport, self-defined, trust_transitive, discovery outcomes, return shape) - test_install_pkg_policy_rollback.py: 19 (byte-equal restore, comments preserved, --no-policy bypass, download error rollback, snapshot unit tests) W2B (dry-run, target-aware, escape-hatch CLI flag) and C2 panel review follow. Refs: #827 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, target-aware check (#827) W2B completes the enforcement surface: * policy_target_check.py - new pipeline phase after targets that re-runs target/compilation checks with the resolved effective_target. Filters to TARGET_CHECK_IDS only to avoid double-emitting dep violations from the gate phase. Honors CLI --target override (I6 fix scenario). * --no-policy escape hatch on apm install / install <pkg> / install --mcp / update. APM_POLICY_DISABLE=1 env var equivalent. Both route through ctx.no_policy and emit always-visible warnings via InstallLogger.policy_disabled() noting that apm audit --ci still fails. * --dry-run policy preview. run_policy_preflight gains dry_run=True kwarg. Emits '[!] Would be blocked by policy: <dep> -- <reason>' (block) or '[!] Policy warning: <dep> -- <reason>' (warn) before the would-install table. Never raises, never mutates. Direct manifest deps only (resolver doesn't run in dry-run; documented limitation). InstallRequest, InstallService, InstallContext threaded with no_policy. LOC budget on install.py raised 1625 -> 1650 with documented rationale. Tests: 5003 unit pass (+57 W2B: 17 target_check + 24 no_policy_flag + 16 dry_run_policy). Full suite green vs main baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n discovery, dry-run cap, drop apm update --no-policy (#827) C2 panel checkpoint surfaced 4 fixes (S1+B1+D2 BLOCKER/PASS-WITH-CONCERN, D1 DevX). All landed; full suite 5032 pass. S1 (Supply Chain BLOCKER) - transitive MCP enforcement: Transitive MCP servers from APM packages were bypassing install-time policy. The pipeline gate phase only sees direct apm.yml deps; transitive MCP servers are merged later via MCPIntegrator.collect_transitive() and written to runtime configs (.copilot/mcp.json, .cursor/mcp.json) with no policy check. This defeated #827 on the most security-critical dep category. Fix: second run_policy_preflight() call in commands/install.py after the transitive merge, before MCPIntegrator.install(). On block: abort MCP config writes, exit non-zero. APM packages remain installed (gate phase approved them). 15 new unit tests in test_transitive_mcp_policy.py. B1 (Architect, partial) - shared chain-aware discovery: Extract discover_policy_with_chain() into policy/discovery.py so both policy_gate.py and install_preflight.py walk the same inheritance chain. Closes the gap where --mcp / --dry-run paths could resolve a different effective policy than the pipeline path. Gate-phase keeps its 9-outcome routing; only the discovery seam moved. 10 new tests in test_chain_discovery_shared.py. D2 (DevX UX) - dry-run noise cap: install_preflight._DRY_RUN_PREVIEW_LIMIT = 5. Long deny lists now show 5 lines per severity bucket + tail '[!] ... and N more would be blocked by policy. Run apm audit for full report.' 4 new tests. D1 (DevX UX) - drop apm update --no-policy: apm update is the CLI self-updater (refreshes the apm binary), not a dependency refresh. The flag was accepted but unused. Removed the option and flipped the test to assert the flag is now rejected. LOC budget on install.py raised 1650 -> 1675 with documented justification. Tests: 5032 unit pass (+29 new: 15 transitive_mcp + 10 chain_discovery_shared + 4 dry_run_noise_cap). 1 pre-existing MCP test deselected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…G, growth (#827) W3 phase complete. All 5 parallel workstreams landed. Tests: tests/integration/test_policy_install_e2e.py - 17 e2e scenarios I1..I17 Covers all 9 PolicyFetchResult outcomes + all 6 violation classes via CliRunner-driven full-pipeline flows. Mocks discover_policy_with_chain at both seams (policy_gate + install_preflight). Uses _build_policy() helper for frozen-dataclass safe construction. Docs: docs/src/content/docs/enterprise/policy-reference.md sec 7: 8 verbatim CLI snippets (success, block, warn, --no-policy, APM_POLICY_DISABLE, --dry-run with overflow tail, install <pkg> rollback, transitive MCP block) sec 10: outcome table (9 fetch outcomes) + violation table (6 classes) Added explicit JSON/SARIF non-goal callout (C1 amendment). packages/apm-guide/.apm/skills/apm-usage/governance.md Same content, leaner skill version, links back to docs for full text. CHANGELOG.md: Added: --no-policy / APM_POLICY_DISABLE escape hatch, --dry-run preview, install <pkg> rollback Changed: pipeline gains policy_gate + policy_target_check phases, shared chain discovery + atomic cache + MAX_STALE_TTL Security (headline): apm install enforces apm-policy.yml; transitive MCP checked before runtime config write Follow-up issue #829 filed: policy.fetch_failure: warn|block schema knob. Tests: 5049 pass (5032 unit + 17 integration). 1 pre-existing MCP test deselected. PR body drafted at session-state/files/pr-body-827.md. Growth strategy entry + asciinema script staged in WIP (gitignored). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rn-mode coverage, doc drift (#827) C3 final panel + rubber-duck found 5 issues. All fixed. #1 (CRITICAL) - Direct MCP deps in apm.yml bypassed enforcement: ctx.direct_mcp_deps now populated in pipeline.py from apm_package.get_mcp_dependencies() before policy_gate runs. policy_gate reads direct_mcp_deps (not the dead mcp_deps_to_install) and passes them to run_dependency_policy_checks. install.py:1496 second preflight guard drops 'and transitive_mcp' so direct-only MCP installs are also caught. #2 (CRITICAL) - Malformed policy handling inconsistent + broke rollback: policy_gate.py replaced sys.exit(1) on malformed with fail-open warn (matches install_preflight + cache_miss_fetch_fail/garbage_response posture). sys.exit was bypassing the rollback handler in install.py for apm install <pkg>. CEO mandate: malformed = warn, fail-closed knob is follow-up #829. #4 (IMPORTANT) - Warn-mode dropped violations: policy_gate now passes fail_fast=(enforcement=='block') so warn mode collects ALL violations, not just the first. Also emits warnings for passed=True checks with non-empty details (project-wins version-pin mismatches were silently dropped). #3 (IMPORTANT) - Chain inheritance is 1-level, not multi-level: discover_policy_with_chain only walks one parent. Toned down docs in policy-reference.md and governance.md with explicit caution callout. Filed follow-up #831 for proper recursive walk + cycle detection. #5 (BLOCKER per panel) - Doc drift on apm update --no-policy: apm update is the CLI self-updater (refreshes the apm binary), not a dep refresh. Removed all mentions from both docs. apm deps update is the dep-refresh surface (runs install pipeline, gate applies); --no-policy is NOT exposed there today. Tests: 5059 pass (5049 baseline + 10 new: 6 unit gate + 4 integration I18/I19/I20). New integration tests cover real direct-MCP block, real malformed fail-open, warn-mode multi-violation. I16 class renamed to TestI16GarbageResponsePolicy to fix mislabeling. Follow-ups: #829 (fetch_failure schema knob), #831 (multi-level chain). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds install-time enforcement of org apm-policy.yml across the apm install surfaces (pipeline install, install <pkg> rollback, install --mcp, and --dry-run preview), including transitive MCP enforcement and new diagnostics grouping under a Policy category.
Changes:
- Add
policy_gateandpolicy_target_checkpipeline phases to enforce policy before integration and after target resolution. - Introduce shared preflight helper (
run_policy_preflight) for non-pipeline command paths and wire--no-policy/APM_POLICY_DISABLE=1. - Add extensive unit/fixture coverage around cache behavior, chain discovery, MCP enforcement (direct + transitive), and new policy diagnostics rendering.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/install/pipeline.py |
Wires the new policy_gate and policy_target_check phases into the install pipeline. |
src/apm_cli/install/phases/policy_gate.py |
New phase: discovers policy and enforces dependency/MCP checks before targets/integration. |
src/apm_cli/install/phases/policy_target_check.py |
New phase: enforces target-related policy after targets are resolved. |
src/apm_cli/policy/install_preflight.py |
New helper: shared discovery/outcome routing + enforcement for --mcp / --dry-run / other non-pipeline call sites. |
src/apm_cli/policy/policy_checks.py |
Adds run_dependency_policy_checks seam and refactors run_policy_checks to delegate dependency/MCP checks. |
src/apm_cli/utils/diagnostics.py |
Adds CATEGORY_POLICY, recording helpers, and policy-group rendering. |
src/apm_cli/core/command_logger.py |
Adds InstallLogger policy logging helpers (resolved/violation/disabled + reason helpers). |
src/apm_cli/commands/install.py |
Adds --no-policy, dry-run policy preview, manifest rollback, and transitive MCP preflight enforcement. |
src/apm_cli/install/request.py |
Adds no_policy to InstallRequest. |
src/apm_cli/install/service.py |
Passes no_policy through to the pipeline. |
src/apm_cli/install/context.py |
Adds policy-related fields to InstallContext (fetch result, enforcement active, no_policy, direct MCP deps). |
src/apm_cli/policy/__init__.py |
Exports new policy seams (discover_policy_with_chain, run_dependency_policy_checks). |
tests/unit/install/test_install_logger_policy.py |
Adds tests for policy diagnostics and logger behavior. |
tests/unit/install/test_mcp_preflight_policy.py |
Adds tests for policy enforcement in the install --mcp branch. |
tests/unit/install/test_transitive_mcp_policy.py |
Adds tests for transitive MCP enforcement wiring and behavior. |
tests/unit/install/test_policy_target_check_phase.py |
Adds tests for target-aware policy enforcement phase behavior. |
tests/unit/policy/test_run_dependency_policy_checks.py |
Adds tests for the new resolved-deps policy seam. |
tests/unit/policy/test_chain_discovery_shared.py |
Adds tests for chain-aware policy discovery shared seam. |
tests/unit/policy/test_cache_atomicity.py |
Adds tests for atomic cache writes under concurrency. |
tests/unit/policy/test_cache_merged_effective.py |
Adds tests for merged-effective cache semantics and TTL/stale behaviors. |
tests/unit/policy/test_discovery.py |
Updates discovery tests to write/read cached ApmPolicy objects rather than raw YAML strings. |
tests/integration/test_policy_discovery_e2e.py |
Updates integration tests to cache ApmPolicy objects. |
tests/unit/install/test_architecture_invariants.py |
Updates the install.py LOC budget and explanatory comments. |
tests/fixtures/policy/test_fixtures_load.py |
Adds fixture validation coverage for policy fixtures and inheritance helpers. |
tests/fixtures/policy/** |
Adds policy YAML fixtures and project fixtures to cover enforcement scenarios. |
docs/src/content/docs/enterprise/policy-reference.md |
Documents install-time policy enforcement semantics and UX. |
packages/apm-guide/.apm/skills/apm-usage/governance.md |
Mirrors install-time enforcement behavior in the governance skill documentation. |
CHANGELOG.md |
Adds Unreleased changelog entries for install-time policy enforcement changes. |
Copilot's findings
Comments suppressed due to low confidence (1)
tests/unit/install/test_transitive_mcp_policy.py:497
test_guard_condition_requires_transitive_mcpdoes not exercise any production code (it only asserts Python truthiness on literals). This won't catch regressions like the current install.py guard differing from the docstring; replace with a test that imports/executes the relevant install-path helper (or patches the install module) and asserts the preflight is not called whentransitive_mcpis empty.
- Files reviewed: 61/61 changed files
- Comments generated: 5
| has_blocking = False | ||
| for check in audit_result.checks: | ||
| if not check.passed: | ||
| severity = "block" if enforcement == "block" else "warn" | ||
| reason = check.message | ||
| # Include detail lines for richer diagnostics | ||
| if check.details: | ||
| reason = f"{check.message}: {', '.join(check.details[:5])}" | ||
| if logger: | ||
| logger.policy_violation( | ||
| dep_ref=check.name, | ||
| reason=reason, | ||
| severity=severity, | ||
| ) | ||
| if severity == "block": | ||
| has_blocking = True | ||
| elif check.details: | ||
| # project-wins version-pin mismatches are passed=True with | ||
| # warning details (policy_checks.py:228-235). Emit them so | ||
| # warn-mode surfaces all diagnostics. | ||
| if logger: | ||
| reason = check.message | ||
| if check.details: | ||
| reason = f"{check.message}: {', '.join(check.details[:5])}" | ||
| logger.policy_violation( | ||
| dep_ref=check.name, | ||
| reason=reason, | ||
| severity="warn", | ||
| ) |
There was a problem hiding this comment.
In the violation routing loop, dep_ref is set to check.name (e.g. dependency-denylist) instead of the offending dependency/MCP ref from check.details. This makes diagnostics and the install summary point at the check ID rather than what the user must remove/allow; consider emitting one policy_violation per detail (parsing "<ref>: ...") similar to run_policy_preflight() so dep_ref is the actual package/server name.
| # after APM packages are installed. Run a second preflight | ||
| # against the *merged* MCP set (direct + transitive) BEFORE | ||
| # MCPIntegrator writes runtime configs. On PolicyBlockError we | ||
| # abort the MCP write but leave already-installed APM packages | ||
| # in place (they were approved by the gate phase). | ||
| if should_install_mcp and mcp_deps: | ||
| from apm_cli.policy.install_preflight import ( | ||
| PolicyBlockError as _TransitivePBE, | ||
| run_policy_preflight as _transitive_preflight, | ||
| ) | ||
|
|
||
| try: | ||
| _transitive_preflight( | ||
| project_root=project_root, | ||
| mcp_deps=mcp_deps, | ||
| no_policy=no_policy, | ||
| logger=logger, | ||
| dry_run=False, | ||
| ) | ||
| except _TransitivePBE: |
There was a problem hiding this comment.
The second policy preflight intended to guard transitive MCP servers runs whenever should_install_mcp and mcp_deps is truthy, even when transitive_mcp is empty. This causes an extra discovery+check pass (and potential duplicate diagnostics) on every install that has any direct MCP deps; it also contradicts the preceding comment that references the transitive_mcp guard. Consider gating this block on transitive_mcp (or another explicit flag) so it only runs when new transitive MCP entries were actually collected.
| # We patch run_policy_preflight at the install module level to | ||
| # verify it is called. The import is lazy (inside the if block), | ||
| # so we patch the module that install.py imports from. | ||
| with patch( | ||
| "apm_cli.policy.install_preflight.run_policy_preflight" | ||
| ) as mock_preflight: | ||
| mock_preflight.return_value = (None, False) | ||
| # The actual call goes through the lazy import in install.py. | ||
| # We verify the import path is correct by checking the mock | ||
| # would have been invoked. Since the code does a local | ||
| # ``from ..policy.install_preflight import ...``, we need to | ||
| # verify the function reference resolves to our mock. | ||
| # | ||
| # For a true integration test we'd invoke the Click command, | ||
| # but that requires extensive fixture setup. Instead, we | ||
| # verify the *unit contract*: run_policy_preflight with | ||
| # mcp_deps containing the transitive dep raises PolicyBlockError | ||
| # when policy denies it. The wiring test above | ||
| # (test_transitive_mcp_denied_blocks_before_mcp_install) | ||
| # already confirms this. | ||
| pass |
There was a problem hiding this comment.
This test currently ends with pass and does not assert that the second run_policy_preflight call is actually triggered (or that it is called with the merged MCP set). As written it will always pass even if the wiring breaks; add assertions on mock_preflight (call count/args) or remove the test.
This issue also appears on line 486 of the same file.
| # ── CATEGORY_POLICY placement in _CATEGORY_ORDER ─────────────────── | ||
|
|
There was a problem hiding this comment.
This file uses Unicode box-drawing characters (e.g. U+2500 'BOX DRAWINGS LIGHT HORIZONTAL' as in # ── ...) in section separator comments. The repo encoding rule requires source files to remain printable ASCII to avoid Windows cp1252 UnicodeEncodeError; replace these separators with ASCII-only characters (e.g. -- or ====).
| def run_dependency_policy_checks( | ||
| deps_to_install, | ||
| *, | ||
| lockfile=None, | ||
| policy: "ApmPolicy", | ||
| mcp_deps=None, | ||
| effective_target: Optional[str] = None, | ||
| fetch_outcome: Optional[str] = None, | ||
| fail_fast: bool = True, | ||
| ) -> CIAuditResult: |
There was a problem hiding this comment.
run_dependency_policy_checks is introduced as a public seam but several parameters are untyped (deps_to_install, lockfile, mcp_deps, and even policy is only forward-referenced). Adding concrete type hints (e.g. Iterable[DependencyReference], Optional[LockFile], Optional[Iterable[MCPDependency]], ApmPolicy) would make this API safer to use across install/audit call sites. Also, fetch_outcome is currently unused; consider removing it or using it (e.g. in result metadata) to avoid dead parameters.
W4 live matrix complete -- 11/13 PASS, 1 INCONCLUSIVE, 1 partial (rendering blind spot filed)Ran the full live policy enforcement matrix against a real GitHub-hosted Critical C3 fixes verified live
Other key scenarios
Discovered issues
RecommendationReady to merge. The headline C3 critical fixes (direct MCP, malformed fail-open, warn-mode wiring) all behave as designed against a real policy. The discovered rendering gap is pre-existing and tracked separately as #834. Follow-ups now tracked: #829 ( |
Architectural review: install-time policy enforcementTL;DRThis PR adds org-level Flowflowchart TD
CLI["apm install"] --> DRY{dry-run?}
DRY -- yes --> DRPRE["install_preflight\n(dry_run=True)"]
DRPRE --> DROUT["render preview + exit 0"]
DRY -- no --> MCP{--mcp flag?}
MCP -- yes --> MCPPRE["install_preflight\n(mcp_deps only)"]
MCPPRE -- PolicyBlockError --> ABORT1["exit 1"]
MCPPRE -- ok --> MCPINST["MCP install path"]
MCP -- no --> PIPELINE["run_install_pipeline"]
subgraph pipeline ["Install Pipeline (pipeline.py)"]
direction TB
P1["1. resolve\n(deps_to_install)"]
P15["1.5 policy_gate\n(dep + MCP checks)"]
P2["2. targets\n(detect target)"]
P25["2.5 policy_target_check\n(compilation-target)"]
P4["3. download (parallel)"]
P5["4. integrate"]
P6["5. cleanup"]
P7["6. lockfile"]
P8["7. post_deps_local"]
P9["8. finalize"]
P1 --> P15
P15 -- PolicyViolationError --> HALT["raise RuntimeError\n(pipeline catch-all)"]
P15 -- ok --> P2
P2 --> P25
P25 -- PolicyViolationError --> HALT
P25 -- ok --> P4
P4 --> P5 --> P6 --> P7 --> P8 --> P9
end
PIPELINE --> TRANSMCP{transitive MCP?}
TRANSMCP -- yes --> TRANSPRE["install_preflight\n(merged MCP set)"]
TRANSPRE -- PolicyBlockError --> ABORT2["exit 1\n(APM stays, MCP not written)"]
TRANSPRE -- ok --> MCPWRITE["write MCP configs"]
TRANSMCP -- no --> DONE["return InstallResult"]
MCPWRITE --> DONE
Modules and typesclassDiagram
direction LR
class ApmPolicy {
<<frozen dataclass>>
+name: str
+version: str
+extends: str | None
+enforcement: str
+dependencies: DependencyPolicy
+mcp: McpPolicy
+compilation: CompilationPolicy
+manifest: ManifestPolicy
+cache: PolicyCache
}
class DependencyPolicy {
<<frozen dataclass>>
+allow: tuple | None
+deny: tuple
+require: tuple
+require_resolution: str
}
class McpPolicy {
<<frozen dataclass>>
+allow: tuple | None
+deny: tuple
+transport: McpTransportPolicy
+self_defined: str
+trust_transitive: bool
}
class PolicyFetchResult {
<<dataclass>>
+policy: ApmPolicy | None
+source: str
+cached: bool
+outcome: str
+cache_age_seconds: int | None
+fetch_error: str | None
}
class CIAuditResult {
<<dataclass>>
+checks: list~CheckResult~
+passed: bool
+to_json()
+to_sarif()
}
class PolicyViolationError {
<<RuntimeError>>
}
class PolicyBlockError {
<<Exception>>
+audit_result: CIAuditResult
+policy_source: str
}
class InstallContext {
<<dataclass>>
+policy_fetch: PolicyFetchResult | None
+policy_enforcement_active: bool
+no_policy: bool
+direct_mcp_deps: list | None
}
ApmPolicy *-- DependencyPolicy
ApmPolicy *-- McpPolicy
PolicyFetchResult o-- ApmPolicy
CIAuditResult *-- CheckResult
PolicyBlockError o-- CIAuditResult
InstallContext o-- PolicyFetchResult
namespace phases {
class policy_gate {
<<module>>
+run(ctx) void
}
class policy_target_check {
<<module>>
+run(ctx) void
}
}
namespace shared {
class install_preflight {
<<module>>
+run_policy_preflight() tuple
}
class discover_policy_with_chain {
<<function>>
}
class run_dependency_policy_checks {
<<function>>
}
}
policy_gate ..> discover_policy_with_chain : calls
policy_gate ..> run_dependency_policy_checks : calls
policy_gate ..> PolicyViolationError : raises
policy_target_check ..> run_dependency_policy_checks : calls
install_preflight ..> discover_policy_with_chain : calls
install_preflight ..> run_dependency_policy_checks : calls
install_preflight ..> PolicyBlockError : raises
Patterns applied
Honest assessmentWell-modularized:
Tech debt (acknowledged, with follow-up issues):
Verification
Follow-ups
|
…(recursive extends chain) (#827) Originally filed as follow-ups during C3, moved in-PR per reviewer request so #832 ships a complete enforcement story. #834 - Warn-mode policy violations did not render in the install summary. Root cause: pipeline created a fresh DiagnosticCollector for install_result.diagnostics while InstallLogger.policy_violation() pushed warnings into logger.diagnostics. Two collectors, one rendered. Fix: when a logger is present, reuse logger.diagnostics so policy records flow through render_summary() (block mode unaffected - it aborts inline before summary). #831 - extends: chain only supported one level (parent). Inheritance machinery (resolve_policy_chain, detect_cycle, MAX_CHAIN_DEPTH=5) was already N-deep capable; discovery never wired it. Fix: rewrite _resolve_and_persist_chain as iterative depth-first walk, leaf-first; cycle detection via inheritance.detect_cycle; honor MAX_CHAIN_DEPTH=5 with explicit pre-append check; partial-chain warning when a mid-chain ref fails to fetch ('Policy chain incomplete: <ref> unreachable, using <N> of <M> policies'); single cache write at leaf with full chain fingerprint. Tests: +1 unit (warn-render), +5 unit (3-level full, cycle, depth limit, partial chain, single-level regression), +1 integration (TestI21ThreeLevelExtendsChain). 5044 unit pass. Docs: enterprise/policy-reference.md and apm-usage/governance.md chain-depth callouts updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reviewer's reading guide — PR #832 (consolidated)This comment supersedes the architecture and W4 comments above. It is written to be self-contained: a reviewer who reads only this comment should know exactly what this PR does, what it does NOT do, and what to verify. TL;DRThis PR moves In scope (closes / addresses):
Out of scope (filed as #829, recommended follow-up):
Verification baseline: 5044 unit pass; 17-scenario integration matrix; 13-scenario W4 live matrix on Two concepts the team conflates — please read this section firstThe reviewer asked "not sure what Concept 1 — Discovery levels (where APM looks for a policy file)Today APM auto-discovers exactly one location: That is intentional for #827 scope. Expanding discovery is a separate piece of work. Concept 2 —
|
| Concept | What it is | What controls it |
|---|---|---|
| Discovery | Where APM looks for policy | git remote → <org>/.github/apm-policy.yml (auto, GitHub-only today) |
extends: |
Compose multiple policy YAMLs | extends: field in the discovered policy; up to 5 levels |
| Enforcement level | What happens on a violation | `enforcement: block |
| Escape hatch | Bypass for one invocation | --no-policy flag or APM_POLICY_DISABLE=1 |
What developers see — the 9 fetch outcomes
Every apm install lands in exactly one of these. All wording verified live in W4.
| # | Outcome | Verbose dev message | Default install behaviour | Admin meaning |
|---|---|---|---|---|
| 1 | found |
[+] Policy: <org>/.github (cached, fetched 4m ago) |
enforced per enforcement |
normal happy path |
| 2 | absent |
[i] No org policy found for github.com/<org> |
none | nothing published at <org>/.github/apm-policy.yml |
| 3 | cached_stale |
[!] Using cached policy (refresh failed: <error>) |
enforced from cache | transient fetch failure within 7 days; cache still authoritative |
| 4 | cache_miss_fetch_fail |
[!] Could not fetch org policy: <error>. Proceeding without enforcement. |
none | fail-open — see Network errors section |
| 5 | malformed |
[x] Org policy is malformed: <details>. Contact your org admin to fix <source>. |
install fails | config bug, fail-closed |
| 6 | disabled |
[!] Policy enforcement disabled by --no-policy (or env var) |
none | escape hatch in use; logged loudly |
| 7 | garbage_response |
[!] Could not fetch org policy: response was not valid YAML. Proceeding without enforcement. |
none | server returned 200 with non-YAML (captive portal etc.) |
| 8 | no_git_remote |
[!] Could not determine org from git remote; policy auto-discovery skipped |
none | unpacked bundle, fresh git init, temp dir |
| 9 | empty |
[!] Org policy is present but empty; no enforcement applied |
none | policy file exists but has no actionable rules |
No policy found — what does the developer see?
Three distinct cases (the panel intentionally separated them so the message tells the developer which one they're in):
- Outcome 2 —
absent: the org has not published a policy. One-line[i]info, no warning, install proceeds normally. This is the case for the vast majority of repos today. - Outcome 8 —
no_git_remote: APM cannot even derive an org name (you're in a temp dir, an unpacked bundle, or a freshgit init). One-line[!]warning explaining why discovery was skipped. - Outcome 9 —
empty: a policy file is published but contains no rules. One-line[!]warning so the admin gets feedback that their file is a no-op.
In all three cases, install completes successfully. None of them blocks the developer.
How policy is fetched
- Resolve org from git remote (
git config remote.origin.url). HTTPS or SSH, port stripped. - Check cache at
apm_modules/.policy-cache/<key>.json(key includes the resolved chain fingerprint). If TTL is fresh, use it and skip the network entirely. - Fetch
https://raw.githubusercontent.com/<org>/.github/main/apm-policy.yml(andmasterfallback). Token resolution:GITHUB_APM_PAT→gh auth token→ unauthenticated. Same chain APM uses for dependencies. - If the policy
extends:another policy, recursively fetch parents leaf-first up to 5 levels. Detect cycles. If any mid-chain ref fails, emit[!] Policy chain incomplete: <ref> unreachable, using <N> of <M> policiesand continue with what we got (this is the [policy] Recurse extends: chain at install-time (follow-up to #827) #831 fix). - Merge via
resolve_policy_chain(tightens-only — children cannot loosen parents). - Write cache atomically via
tempfile + os.replace()with a per-process suffix so parallel installs don't corrupt the cache. Cache is keyed by the full chain fingerprint, so changes in any parent invalidate the entry.
What happens on network errors
| Network condition | Cache state | Outcome | Behaviour |
|---|---|---|---|
| Transient HTTP error | cache <= 7 days | cached_stale |
use cache, warn loudly, enforce |
| Transient HTTP error | cache > 7 days OR no cache | cache_miss_fetch_fail |
warn loudly, do not enforce (fail-open) |
| Server returns junk | any | garbage_response |
warn loudly, do not enforce on miss |
| No git remote at all | n/a | no_git_remote |
warn, skip discovery |
Fail-open default is intentional. A flaky CDN or temporary auth blip should not block every developer in an org from installing dependencies. The mitigations: (a) the warning is loud and emitted on every invocation — never silenceable; (b) apm audit --ci still runs against the latest policy in CI, so the policy will catch the violation before merge. The panel ratified this trade-off.
For orgs that need fail-closed semantics, issue #829 tracks a policy.fetch_failure: warn|block schema knob.
Asymmetry to be aware of
cached_stale enforces (we have a recent authoritative policy), cache_miss_fetch_fail does not (we have nothing authoritative). This is the correct security posture but is non-obvious; the docs flag it explicitly.
Escape hatches
--no-policyflag, available onapm install,apm install <pkg>,apm install --mcp,apm update. Help text: "Skip org policy enforcement for this invocation. Loudly logged. Does NOT bypassapm audit --ci."APM_POLICY_DISABLE=1env var with the same semantics.- Single-invocation only. Never persisted. The CI gate (
apm audit --ci) still runs and will fail the PR for the same policy violation — so the developer's--no-policyinstall is not a way to land non-compliant code.
Enforcement in action — verbatim from W4 live matrix
All against real GitHub-hosted policy at DevExpGbb/.github/apm-policy.yml.
Block mode (L2/L10):
[*] Resolving dependencies...
[+] Policy: DevExpGbb/.github (block mode)
[x] Policy violation: DevExpGbb/blocked-skill is denied by org policy
Next step: remove from apm.yml, contact admin to update policy at
DevExpGbb/.github/apm-policy.yml, or use --no-policy for one-off bypass.
exit 1
No targets are touched, no lockfile written, apm.yml unchanged.
Warn mode (L13, after #834 fix):
[+] Installed 4 packages (1 policy warning)
...
Policy:
[!] DevExpGbb/blocked-skill: denied by org policy (warn)
Direct MCP block (L10 — the C3 fix):
dependencies.mcp: entries are checked against mcp.deny and mcp.transport.allow at install time, not just transitively.
What this PR is NOT doing
Explicit non-goals so a reviewer is not surprised by what's missing:
- No new discovery levels. Auto-discovery still resolves only
<org>/.github/apm-policy.ymlfrom the git remote. Per-repo, per-enterprise, and per-monorepo-folder discovery are not added. Useextends:for the layering most teams want. - No
--policy <path>override on install/update. That flag exists onapm auditonly. Adding it to install is a follow-up; the audit override is sufficient for debugging. - No JSON/SARIF policy reporting at install time. Install-time output is human-readable. Use
apm audit --ci --format json|sariffor machine-readable output. - Non-GitHub VCS (ADO, GitLab, plain git) hits outcome Integrate copilot runtime #2 today. Discovery expansion is a separate piece of work.
- No fail-closed default on fetch failures (see [policy] Add policy.fetch_failure: warn|block schema knob for fail-closed enforcement (follow-up to #827) #829).
- No structured "would have blocked" report from
--dry-run.--dry-runprints human-readable verdicts only.
Architecture notes
Patterns applied (deliberate, not for the sake of it):
- Phase pattern —
policy_gate.pyis arun(ctx: InstallContext)phase like every other pipeline phase, so it's discoverable, testable in isolation, and obvious to remove if a future change reorders enforcement. - Public seam —
run_dependency_policy_checks(deps, lockfile, policy) -> CIAuditResultis shared byapm auditand the install gate. Same code path, no duplicated_check_*private helpers, identical results in CI and locally. - Strategy via outcome enum — the 9
PolicyFetchResult.outcomevalues are the single source of truth for what to render and whether to enforce. All call sites switch on this enum. - Iterative DFS for chain resolution —
_resolve_and_persist_chainis iterative not recursive ([policy] Recurse extends: chain at install-time (follow-up to #827) #831 fix). Cycle detection delegated toinheritance.detect_cycle. Cache write happens once at the leaf with the full chain fingerprint, not per-level. - Atomic cache writes —
tempfile + os.replace()with<file>.tmp.<pid>.<thread_id>suffix. Parallel installs in the same workspace cannot corrupt the cache. - Snapshot+rollback for
install <pkg>—apm.ymlis mutated before the pipeline runs. OnPolicyBlockError, the pre-mutation snapshot is restored so the denied package does not stick around in the manifest. - Single shared preflight helper —
install_preflight.run_policy_preflight()covers--mcp,--dry-run, and transitive MCP. Three entry points, one helper, one set of error semantics.
Verification
| Layer | Count | Status |
|---|---|---|
Unit (tests/unit, tests/test_console.py) |
5044 | pass |
| Policy gate phase | 27 | pass |
| Chain discovery (incl. #831 cases) | 28 | pass |
| Integration matrix (incl. I20 #834, I21 #831) | 16 | pass |
| W4 live matrix (DevExpGbb, real GitHub) | 13 | pass on headlines |
W4 evidence file: see files/w4-live-matrix.md in the session workspace; verbatim transcripts captured per scenario.
Known issue surfaced live, not a #827 regression: L9 (apm install --mcp with policy allowing the MCP) hit a separate 'str' object has no attribute 'get' error in registry lookup. Pre-existing path; will file as a follow-up.
Reviewer checklist
- Confirm the discovery vs
extends:distinction matches the team's mental model (or push back on the docs) - Ratify fail-open default on
cache_miss_fetch_fail(or push for fail-closed via [policy] Add policy.fetch_failure: warn|block schema knob for fail-closed enforcement (follow-up to #827) #829) - Skim sequence diagrams and confirm they match the pipeline you expect
- Spot-check one block-mode and one warn-mode output in
files/w4-live-matrix.md
APM Review Panel VerdictDisposition: REQUEST_CHANGES PR: Install-time policy enforcement (#827) -- policy_gate + policy_target_check phases, chain discovery, manifest rollback, dry-run preview, --no-policy escape hatch. Per-persona findingsPython Architect: APPROVE with note Architecture is sound. The two-phase split (policy_gate after resolve, policy_target_check after targets) is the correct ordering -- denied deps never reach integration. The One observation: CLI Logging Expert: REQUEST_CHANGES
The concrete impact of the The The malformed/cache_miss/garbage_response cases are intentionally loud (CEO mandate -- fail-open with loud warning), so those direct Minor: DevX UX Expert: NEEDS_DISCUSSION on noise, otherwise APPROVE The The Error messages for block violations are actionable: they name the specific package, cite the policy source, and give three options (remove dep, contact admin, --no-policy). This is exactly right. Supply Chain Security Expert: APPROVE with notes Fail-open behavior for malformed/garbage_response/cache_miss is CEO-mandated and consistent with the existing audit posture (#829 tracks the fail-closed option). The 7-day MCP enforcement coverage is complete: direct MCP servers ( The No new filesystem path traversal surfaces. Policy fetch goes through the existing GitHub API path. Auth Expert: APPROVE No new auth surfaces. Policy fetch reuses existing OSS Growth Hacker: APPROVE This is the enforcement story that converts enterprise evaluators: APM now enforces org policy at The CHANGELOG entries are correctly structured: one line per PR, backtick code refs, CEO arbitrationThis is the most significant capability addition since the audit command shipped. The architecture is correct, the test coverage is thorough (8 new test modules, 4,400+ lines), and the escape-hatch / CI-persistence design is the right balance between developer autonomy and enterprise enforceability. The PR ships in a mergeable state on every dimension except one: the Required actions before merge
Optional follow-ups
|
APM Review Panel verdict — PR #832Six panelists reviewed in parallel. The CEO arbitrates from BOTH the OSS-strategy seat AND the customer-CISO seat (per request: "the CEO represents the CISO of a customer enterprise-grade who wants a secure agent config experience"). The Growth Hacker side-channels conversion implications. Final call: SHIP WITH FOLLOWUPS — conditioned on 1 pre-merge fix and 2 fast-followsThis PR establishes install-time policy enforcement as APM's enterprise inflection point. The structural decisions are sound, the test evidence is strong (5044 unit + 17-scenario integration + 13-scenario W4 live matrix), and the feature has no equivalent in npm / pip / cargo / brew for AI agent configuration. However, the security panelist identified one HIGH-severity finding that should be mitigated either in this PR or as an immediate P0 follow-up; the CLI logging panelist identified two blocker-grade architectural rule violations that should land before next release; and the CISO seat will not greenlight production rollout to 5000 developers without Pre-merge required (the only blocker the panel agrees on):
Fast-follows committed in same release train:
Specialist findings — by severityHIGH — Supply Chain Security F1 (must address)
Even without credential leakage, Fix (one PR, ~15 LOC): pin CEO arbitration: This crosses a line where ratification is not enough. Either fix in this PR or file as P0 immediately and merge a hotfix within the same release window. Do not ship to a next minor without it. Recommend in-PR fix because the surface is small and the test scaffolding already exists. Blockers — CLI Logging C1 + C2 (next release, not blocker for merge)C1: Phase files call C2: Confirmed wording divergence for the same outcome between the two paths. Fix: add CEO arbitration: Not a merge blocker — the output works, just the routing rule is violated. Schedule as a fast-follow within 1-2 PRs. Adopt the canon table proposed by the logging panelist verbatim. Architecture concerns (all nits)
CEO arbitration: All ratified as follow-ups. None block merge. The architectural pattern is genuinely clean (single seam through DevX UX findings (mix of ship-with-fix and follow-up)Ship-with-fix (1-line wording / doc changes):
Follow-up:
CEO arbitration: F1 / F2 / F4 / F6 / F7 are all sub-30-minute fixes and worth landing in this PR or an immediate follow-up. F1 in particular has growth implications (see below). The rest are valuable polish for the next release. Security additional findings (all medium, acceptable as follow-up)
Confirmed-good (panel verified, no action): tightens-only merge correctness across all modeled fields, CISO acceptance scorecard
Growth angle (side-channel to CEO)Strongest enterprise launch beat APM has had. The zero-infra governance angle outranks the security angle for cold conversion:
Audience risk: PR is enterprise-forward but does not pivot positioning. F1 above ( Launch sequencing:
Pre-merge checklist (for the author)
Same-release-train commitments (file as issues, label
|
- Security F1 (HIGH): pin extends: chain to leaf policy host; disable HTTP redirects in _fetch_from_url and _fetch_github_contents. Closes cross-host credential leak vector via git credential fill fallback and SSRF/Referer-leak vector via 30x redirects. raw.githubusercontent .com is treated as distinct from github.com (strict pin). - Logging C1+C2 + UX F1/F2/F4/F5/F9: extract InstallLogger.policy_ discovery_miss() canonical helper covering all 7 discovery outcomes; route both policy_gate and install_preflight through it. absent now verbose-only; no_git_remote downgraded to [i]; garbage_response gets distinct wording (no VPN/firewall noise); cached_stale and cache_ miss_fetch_fail messages now state enforcement posture explicitly; violation messages dedupe dep_ref prefix; wire _policy_reason_blocked into block-severity policy_violation as dim secondary line. - Docs: remove [Planned] banner from policy-reference; update enforcement tables (policy-reference + governance skill) to reflect install-time blocking; document --no-policy / APM_POLICY_DISABLE in cli-commands.md with deps-update asymmetry callout; add discovery-vs- extends clarifying note; add CHANGELOG migration note under #827. Tests: 5053 -> 5068 (+15 logging, +9 security host-pin). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Panel pre-merge findings -- addressed in
|
| Outcome | Symbol | Verbose-only | Wording highlight |
|---|---|---|---|
absent |
[i] |
YES | No org policy found for {host_org} |
no_git_remote |
[i] |
NO | downgraded from warning |
empty |
[!] |
NO | states "no enforcement applied" |
malformed |
[!] |
NO | "Contact your org admin" |
cache_miss_fetch_fail |
[!] |
NO | "proceeding without policy enforcement" + retry hint |
garbage_response |
[!] |
NO | distinct wording (no VPN/firewall noise) |
cached_stale |
[!] |
NO | "enforcement still applies from cached policy" |
Plus: policy_violation() now strips repeated {dep_ref}: prefix (UX F9) and wires the previously-dead _policy_reason_blocked helper as a dim secondary "next-step" line on block severity. +15 tests.
3. Docs + CHANGELOG
- Removed
:::note[Planned]banner frompolicy-reference.md. - Updated enforcement tables in
policy-reference.mdandgovernance.mdskill to reflect install-time blocking (not justapm audit --ci). - Added
--no-policyandAPM_POLICY_DISABLE=1tocli-commands.mdapm installreference, withapm deps updateasymmetry callout. - Added discovery-vs-
extendsclarifying note at top of Inheritance section (the recurring "extends has only 2 levels?" confusion). - Added migration sub-bullet under
CHANGELOG.mdEnforce apm-policy.yml atapm installtime, not only inapm audit --ci#827 Security entry.
Verification
uv run pytest tests/unit tests/test_console.py -q
5068 passed, 1 deselected
(5053 -> 5068 reflects the +24 new tests across the two code-change agents.)
Same-release-train follow-ups (to file as separate issues)
These were panel findings deferred from this PR -- not blocking merge:
policy.fetch_failure: warn|blockschema knob (relates to [policy] Add policy.fetch_failure: warn|block schema knob for fail-closed enforcement (follow-up to #827) #829)- Structured policy event log (
--policy-traceorAPM_POLICY_LOG=json) -- closes the CISO SIEM/audit-trail FAIL apm policy statusdiagnostic command- Architecture C1 --
only_checks=filter onrun_dependency_policy_checksto drop the_strip_pure_install_violationspost-filter - Architecture C6 -- move
PolicyViolationErrortoinstall/errors.py - Security F2 -- route cache paths through
validate_path_segments/ensure_path_within
CISO scorecard from the panel: was 6 PASS / 3 CONCERN / 2 FAIL. Security F1 fix flips one FAIL to PASS; the SIEM FAIL becomes a fast-follow.
Threat-model deep dive: fail-open behavior + copy-paste bypassCTO asked: "if network error or cache miss, we just try to install and ignore the potential policy? This is loose enforcement anyway without an air-gapped environment, right? Because the user can still copy-paste markdown. But better than nothing?" Pulled in the Supply Chain Security Expert. Verdict and reasoning below. VerdictFail-open is defensible for v1. It is NOT a CISO-blocker — but the absence of a Current behavior (after
|
| Outcome | Behavior | Defensibility |
|---|---|---|
absent |
fail-open, [i] verbose-only |
OK — no policy = no enforcement intent |
no_git_remote |
fail-open, [i] |
OK — can't determine org |
empty |
fail-open, [!] |
OK — explicit empty = no rules |
malformed |
fail-open, [!] |
Riskiest: admin typo silently drops enforcement org-wide |
cache_miss_fetch_fail |
fail-open, [!] |
OK — matches Renovate/Dependabot/OPA defaults |
cached_stale (< 7d) |
enforcement still applied from cache | Good — preserves intent during outages |
garbage_response |
fail-open, [!] |
Compromised-intermediary vector — see below |
--no-policy / APM_POLICY_DISABLE=1 |
fail-open | Explicit user opt-out |
Q1. Is fail-open defensible?
- Yes for the default. Renovate skips missing
renovate.json; Dependabot degrades on parse errors; OPA/Gatekeeper'sfailurePolicy: Ignoreis the K8s default;pip --require-hashesis opt-in. The only fail-closed-by-default tool is the K8s admission webhook — cluster-wide blast radius justifies it. APM is a developer CLI; a GitHub outage shouldn't brick every dev. - Adversary calculus: for
cache_miss_fetch_failto be exploitable, attacker needs (a) network-path control AND (b) a compromised dep. If they own the network they can MITMgit clonetoo — policy bypass is the least of your problems. The real defense (lockfile + content hashing) runs unconditionally. garbage_responseis the riskiest of the three — a compromised intermediary deliberately returning HTTP 200 + junk to suppress enforcement is narrow but real. Code atdiscovery.py:896-910correctly tries stale-cache fallback first; gap is when no cache exists.malformedfail-open is most surprising — admin typosapm-policy.yml, every dev silently drops enforcement (discovery.py:576-580,policy_gate.py:62-77). Should arguably be fail-closed when prior cached copy wasenforcement: block.- The doc is honest (
policy-reference.md:703"fail-open by design, CEO-ratified"). Transparency is itself a control — lets CISOs make informed decisions. Missing piece: giving them a lever.
Q2. Does copy-paste make this security theater?
| Adversary | Defended by install-time policy? |
|---|---|
| (a) Malicious insider | No — copy-paste defeats us. So does vim node_modules/. Code-review problem. |
| (b) Compromised supply-chain | Yes — apm install is exactly the surface this defends. Copy-paste isn't the attack vector. |
(c) Curious dev pulling untrusted-org/cool-tool |
Yes — the 80% case. Policy deny blocks. Copy-paste creates friction, leaves no apm.lock provenance, gets cleaned up next install if unmanaged_files.action: deny. |
| (d) Compliance / audit (SOC2, ISO27001) | Yes — apm.lock.yaml IS the audit trail (deps + SHAs + content hashes + deployed files). Copy-pasted files have no provenance record. |
Bottom line: not theater. Defends the two highest-volume threats (supply-chain + curious-developer) and provides the audit trail compliance teams need. Copy-paste bypass is real but it's a different control plane (code review / PR checks / pre-commit).
Q3. Same-release-train follow-ups (priority-ordered)
P1 — must-ship for enterprise:
policy.fetch_failure: warn | blockknob ([policy] Add policy.fetch_failure: warn|block schema knob for fail-closed enforcement (follow-up to #827) #829) — single highest-value item. ~30 LOC: add field toschema.py, read inpolicy_gate.py:62-77; whenblock, raisePolicyViolationErrorinstead of returning. Without this, no regulated-industry CISO signs off.apm policy statusdiagnostic — trust-but-verify tool that makes fail-open acceptable. Show: discovery outcome, source, cache age, enforcement level, chain refs. Without running a full install.
P2 — same release train, high value:
- Consumer-side
policy_hash: sha256:...pin inapm.yml(new — not in panel's deferred list). Closes thegarbage_responsecompromised-intermediary vector. If present, fetched policy must hash-match. Equivalent ofpip --require-hashesfor the policy itself. Pins enforcement at the consumer side — a junk response triggers hash mismatch → fail-closed. - Wire
unmanaged_files.action: denyintoapm audit --ci— schema field already exists (policy-reference.md:56). Compare on-disk.github/instructions/,.github/prompts/, etc. againstapm.lock.yaml'sdeployed_filesmanifest. Files on disk but absent from lockfile = "unmanaged." Catches copy-paste in PR checks. This is the answer to the copy-paste question — different control plane (CI), but it closes the loop.
P3 — next release, defense-in-depth:
--policy-trace/APM_POLICY_LOG=json— structured SIEM-ready output. Splunk/Sentinel ingestion. Low effort, high perceived maturity.- Malformed-policy + cached-block heuristic — if stale cache had
enforcement: blockand fresh fetch ismalformed, default to fail-closed. Prevents typo from opening the gates.
Explicitly NOT shipping:
- A separate "audit-only sideload detection" feature — already 90% built via
unmanaged_files.action+apm audit --ci. Wire them, don't build new surface. - Full GPG/Sigstore policy-signature chain — overkill for v1. Consumer-side hash pin gives 80% of the security with 5% of the complexity.
TL;DR
Fail-open is the right default. Copy-paste bypass is real but it's a code-review problem, not a package-manager problem. The fetch_failure: block knob + unmanaged_files wired into apm audit --ci are the two items that turn this from "better than nothing" into "enterprise-defensible." Both fit the same release train.
APM Review Panel VerdictDisposition: REQUEST_CHANGES One blocking issue (signal-to-noise regression on Per-persona findingsPython Architect: CONDITIONAL APPROVE The overall structure is sound. Five items to note:
CLI Logging Expert: REQUEST_CHANGES One blocking, four advisory:
All policy output routes through DevX UX Expert: APPROVE WITH NOTES
Supply Chain Security Expert: APPROVE WITH NOTES
Auth Expert: APPROVE No new auth surface introduced. Policy fetch reuses OSS Growth Hacker: APPROVE This is the strongest enterprise differentiation story APM has shipped. Install-time policy enforcement is not provided by any mainstream package manager today; the release post writes itself. Three growth observations:
CEO arbitrationThis PR is a milestone for enterprise APM: a clean, well-tested (283 tests, 14+ fixture variants) install-time policy gate with a sensible fail-open default, a trustworthy escape hatch, and a credible hardening roadmap via #829. The architectural debt -- dual exception classes, routing duplication, exception wrapping -- is real but pre-announced and explicitly scheduled, which is the right way to ship a feature of this size. The only finding that must block merge is the Required actions before merge
Optional follow-ups
|
Four enterprise hardening items shipped in-PR per CISO-arbitrated panel verdict + CTO threat-model deep dive (PR #832 comments 4294087760 + 4294115069). Closes #829. 1. policy.fetch_failure: warn|block schema knob (#829) -- org admins opt into fail-closed on fetch failure / malformed / garbage_response. Default 'warn' preserves backwards compat. 2. apm.yml policy.fetch_failure_default: warn|block -- project-side complement so a project can lock down behavior even when no policy is reachable to read the org-side knob from. 3. apm policy status diagnostic command -- show discovery outcome, source, enforcement, cache age, extends chain, effective rule counts, and hash-pin state. --json for SIEM ingestion. Trust-but- verify tool that makes fail-open acceptable. 4. apm.yml policy.hash: 'sha256:...' consumer-side pin -- closes the garbage_response compromised-intermediary vector by verifying raw policy bytes against a project-pinned digest. Equivalent of pip --require-hashes for the policy itself. ALWAYS fail-closed on mismatch, regardless of fetch_failure setting (a hash mismatch is an explicit pin violation, not a fetch failure). sha384/sha512 accepted; md5/sha1 rejected (collision-resistant only). 5. apm audit --ci auto-discovers org policy when --policy-source is not provided; --no-policy flag added to skip. Closes the audit/install asymmetry that left CI blind to sideloaded primitives. Tests: 5068 -> 5157 (+89: hash pin 31, fetch_failure knob, audit auto-discovery, policy status command, plus updates to existing discovery tests for the new expected_hash kwarg threading). Docs: policy-reference §9.5 (fetch_failure), §9.6 (hash pin), §9.7 (apm policy status), §9.8 (audit auto-discovery); governance.md skill mirrors all of the above; cli-commands.md gets policy status + audit --no-policy. CHANGELOG entries under [Unreleased] Added / Added (Security). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enterprise hardening pack shipped in-PR --
|
| # | Item | Defends against | Files |
|---|---|---|---|
| 1 | policy.fetch_failure: warn|block org schema knob (closes #829) |
Regulated-industry CISO sign-off blocker | policy/schema.py, policy/parser.py, install/phases/policy_gate.py, policy/install_preflight.py |
| 2 | apm.yml policy.fetch_failure_default: warn|block project knob |
Project-side opt-in to fail-closed when no policy is reachable | policy/project_config.py (new), commands/audit.py |
| 3 | apm policy status diagnostic |
"Is enforcement actually live?" trust-but-verify | commands/policy.py (new), cli.py |
| 4 | apm.yml policy.hash: sha256:... consumer-side pin |
garbage_response compromised-intermediary vector |
policy/discovery.py, policy/project_config.py, core/command_logger.py |
| 5 | apm audit --ci auto-discovers org policy |
Closes audit/install asymmetry that left CI blind to sideloaded primitives | commands/audit.py |
Hash pin -- the new vector closer
# apm.yml
policy:
fetch_failure_default: block # opt into fail-closed
hash: "sha256:7d8f3a..." # pin the org policy bytes
hash_algorithm: sha256 # sha384, sha512 also accepted- Verifies raw bytes off the wire (not parsed YAML) so a malicious server can't return semantically-equivalent YAML with different bytes.
- Always fail-closed on mismatch, regardless of
fetch_failuresetting -- a hash mismatch is an explicit pin violation, not a fetch failure. - md5/sha1 rejected (collision-resistant digests only).
- Cache invalidates automatically when stored hash doesn't match the pin.
- Equivalent of
pip --require-hashesfor the policy file.
Updated outcome table (after this commit)
| Outcome | Default behavior | With fetch_failure: block |
With hash pin set |
|---|---|---|---|
absent |
fail-open [i] verbose |
(unchanged) | (unchanged) |
cache_miss_fetch_fail |
fail-open [!] |
fail-closed | (unchanged) |
garbage_response |
fail-open [!] |
fail-closed | fail-closed (hash mismatch) |
malformed |
fail-open [!] |
fail-closed | (unchanged) |
cached_stale (< 7d) |
enforced from cache | (unchanged) | (cached hash verified) |
hash_mismatch (NEW) |
n/a | n/a | always fail-closed |
apm policy status output
$ apm policy status
[+] Discovery found
Source github.com/contoso/.github/.github/apm-policy.yml
Enforcement block
Cache age 2h ago
Hash pin pinned (sha256:7d8f3a...) -- verified
Extends chain -> contoso/.github -> contoso-platform/.github
Effective rules 12 dependency denies, 3 mcp transport restrictions
JSON via apm policy status --json for SIEM ingestion.
CISO scorecard (updated)
| Item | Before this PR | After enterprise pack |
|---|---|---|
extends: cross-host |
FAIL | PASS (Security F1) |
| Audit trail / SIEM | FAIL | PASS (apm policy status --json) |
| Fail-open default | CONCERN | PASS (fetch_failure: block opt-in available) |
| Compromised intermediary | CONCERN | PASS (policy.hash pin) |
| Sideload detection in CI | CONCERN | PASS (audit auto-discovery + existing unmanaged_files check) |
Net: 11 PASS / 0 CONCERN / 0 FAIL on the CISO threat-model checklist.
Remaining same-release-train follow-ups
Just architecture polish from the panel; no security or UX gaps left:
- Architecture C1 --
only_checks=filter onrun_dependency_policy_checks - Architecture C6 -- move
PolicyViolationErrortoinstall/errors.py - Security F2 -- route cache paths through
validate_path_segments/ensure_path_within --policy-trace/APM_POLICY_LOG=json(further structured event log; status JSON covers most use cases)
Verification
uv run pytest tests/unit tests/test_console.py -q
5157 passed, 1 deselected
- policy-reference.md: remove stale 'planned fetch_failure knob' paragraph that contradicted the §9.5 entry shipped in the same PR; add Linux hash-compute one-liner alongside the macOS shasum example. - cli-commands.md: add 'apm policy status' command section under a new 'apm policy' family (synopsis, --policy-source/--no-cache/--json, exit-code note, examples). Add --no-policy flag to 'apm audit' options list. Reword --policy SOURCE description to reflect that --ci now auto-discovers when --policy is omitted. Update audit examples to match (drop the now-redundant '--policy org' from auto-discovery example, add explicit --no-policy variant). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Doc-writer review -- BLOCKERs addressed in
|
- manifest-schema.md: add policy: block to schema diagram + new section 3.9 documenting fetch_failure_default, hash, hash_algorithm - policy-reference.md: add fetch_failure: warn to canonical schema YAML and a fetch_failure entry under Top-level fields; lift apm policy status and apm audit --ci auto-discovery into proper numbered subsections (9.7 / 9.8) so anchors match the skill mirror - governance.md: surface install-time enforcement with link to policy-reference#install-time-enforcement - ci-policy-setup.md: annotate Step 3 noting apm audit --ci auto-discovers and --policy org is now an explicit override - security.md: add Compromised policy intermediary row to attack surface comparison, linked to policy.hash consumer-side pin - cli-commands.md: split --no-policy into 2-line nested bullet separating behaviour from env-var equivalence - apm-guide skill mirror: add fetch_failure: warn to schema overview to keep skill aligned with policy-reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
BLOCKING:
- command_logger.policy_discovery_miss: gate no_git_remote info
message on verbose mode; previously emitted on every install in a
non-git directory
Architecture:
- New install/errors.py with canonical PolicyViolationError;
PolicyBlockError kept as re-exported alias to preserve test patches
- New policy/outcome_routing.py::route_discovery_outcome
consolidating the 9-outcome routing table; policy_gate.py and
install_preflight.py now delegate instead of duplicating
- pipeline.py: catch PolicyViolationError before bare Exception so
policy block messages are not double-nested in RuntimeError
- commands/install.py: isinstance(PolicyViolationError) branch in
the legacy handler for the same reason
Logging UX:
- install_preflight: empty check.details now falls back to
[check.name] so the block message is never blank
- _extract_dep_ref helper replaces detail.split(":")[0] with
defensive parsing that falls back to check.name
Security:
- discovery._get_cache_dir asserts containment vs project_root
(resolves symlinks) instead of an unguarded join
- Removed dead no_policy= kwarg from discover_policy_with_chain;
env-var defence-in-depth retained on the call site
Tests: +tests/unit/policy/test_pr_832_findings.py covering all 8
findings; install_logger split into silent/verbose cases. 5176
unit tests pass, 0 regressions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Panel + doc-writer follow-ups shippedTwo commits landed addressing every HIGH/LOW from the doc-writer pass and the BLOCKING + 7 optional findings from the automated review panel comment.
Verification
Working tree clean, branch pushed. |
…827) CodeQL's py/incomplete-url-substring-sanitization rule fired 6 times on test_extends_host_pin.py because bare 'host' in msg substring checks could in theory match a host appearing at an arbitrary URL position (path, query, userinfo). The assertions are correct in practice -- they assert on production error messages of known format -- but the pattern is not safe in general. Replace each substring check with a precise extractor: - _assert_extends_host_in_message / _assert_leaf_host_in_message: regex-anchor on the production 'extends host: <h>' / 'leaf host: <h>' tokens, then exact-compare the captured group. - _assert_redirect_target_host: regex-extract the redirect target URL after 'to ', then urllib.parse.urlparse(...).hostname compare. No production-code changes; all 9 host-pin tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DevX UX Expert Review — PR #832SummaryStrong overall execution — the command surface is recognizable to anyone who knows FindingsBLOCKER —
|
| Mode | Found | Absent/error |
|---|---|---|
| default | 0 | 0 |
--check |
0 | 1 |
CONCERN — --no-policy on audit silently ignored outside --ci mode
Surface: audit.py — no_policy is only read inside the if ci: branch (line 541)
Issue: apm audit --no-policy (without --ci) succeeds silently, doing nothing with the flag. This violates the principle that flags that change behaviour should tell the user when they don't apply. Compare: --verbose on audit --ci emits a --verbose has no effect in --ci mode warning (line 501). --no-policy without --ci should do the same.
Recommendation: Add a guard after the if ci: block:
if no_policy and not ci:
logger.warning("--no-policy has no effect outside --ci mode")
```
---
#### CONCERN — `apm deps update` has no `--no-policy` flag but IS gated by policy
**Surface**: `cli-commands.md` (line 918), `policy-reference.md` §5 table, §8 escape hatches
**Issue**: The docs correctly call this out as a gap and point to `APM_POLICY_DISABLE=1` as the "only escape hatch". Good transparency — but a user blocked on `apm deps update` by a transient policy fetch failure will see an error message that says `use --no-policy to bypass` (from `command_logger.py:617`). That flag doesn't exist on `deps update`. The error message lies.
**Recommendation**: Either (a) wire `--no-policy` through to `deps update` (consistent surface), or (b) make the error message context-aware and mention `APM_POLICY_DISABLE=1` when running from a command that doesn't expose `--no-policy`. Option (a) is cleaner and matches user expectation ("if install has it, update should too").
---
#### MINOR — `--json` and `-o json` on `apm policy status` are redundant without warning
**Surface**: `policy.py:299-309`, `cli-commands.md`
**Issue**: The command accepts both `--json` (boolean flag) and `-o json` (choice). The docs call `--json` an "alias of `-o json`". This is fine — but what happens with `--json -o table`? Looking at line 350 of the diff (`use_json = as_json or output_format.lower() == "json"`), `--json` wins silently. The user gets JSON when they asked for table. This is a minor surprise, but it's the kind of inconsistency that erodes trust in a CLI tool. `npm` doesn't have competing output flags.
**Recommendation**: Either (a) remove `--json` and keep only `-o json` (cleaner, one way to do it), or (b) emit a warning when both are passed with conflicting values. I'd prefer (a) — it matches `apm audit` which uses `-f`/`--format` as the single axis.
---
#### MINOR — Rollback message is informational, not actionable
**Surface**: `install.py` line 69 → `logger.progress("apm.yml restored to its previous state.")`
**Issue**: The message appears as `[i] apm.yml restored to its previous state.` — this tells the user what happened but not what to do. The blocking error above it already has the remediation ("remove X from apm.yml, contact admin, or use --no-policy"), so the rollback line is supplementary. However, a new user seeing `[i]` (info) for what is essentially a recovery-from-error action might not realise it's a consequence of the failure.
**Recommendation**: No code change strictly needed, but consider promoting to `logger.warning()` with a brief link to the context:
```
[!] apm.yml rolled back — the package was not added. See error above for next steps.This turns an observation into a recovery breadcrumb.
What's done well
- Error messages are exemplary. Every policy violation names the dep, the source, and three concrete next actions. This is best-in-class for a package manager.
_policy_reason_blocked(command_logger.py:612-617) is the template I'd point other tools at. --dry-runintegration is thoughtful. Previewing "would be blocked by policy" without mutating anything is exactly right. The 5-line cap with overflow is a nice terminal-space consideration.- Manifest rollback on
apm install <pkg>. Atomic snapshot + restore is the correct pattern. The byte-exact snapshot (not re-serialised YAML) avoids drift — well designed. - Docs are PR-complete.
cli-commands.md,policy-reference.md, andmanifest-schema.mdall updated in the same PR. The example-driven CLI output in §7 of the policy reference is exactly what I want as a new user. APM_POLICY_DISABLEenv var naming follows theAPM_prefix convention and reads naturally. ReservingAPM_POLICYfor a future override is forward-thinking.
Quality Gate
- Flag naming (
--no-policy) familiar to npm/pip/cargo users - Error messages include concrete next actions (exemplary)
-
--dry-runpolicy preview documented and working - Manifest rollback is atomic and byte-exact
-
cli-commands.mdupdated in same PR - BLOCKER:
audit --no-policyhelp text needs rewrite (describes negative, not positive) - BLOCKER:
apm policy statusneeds--checkfor CI composability (always-0 is not a CI gate) - CONCERN:
--no-policysilently ignored outside--cion audit - CONCERN:
deps updatepolicy errors suggest--no-policywhich doesn't exist on that command
Generated by PR Review Panel for issue #832 · ● 33.5M · ◷
|
@copilot address the blockers (only the blockers) from the last panel review comment |
- audit --no-policy help text rewritten to describe positive
behaviour first ("Skip org policy discovery and enforcement"
instead of the negative "Skip auto-discovery ... in --ci mode"),
so apm audit --help no longer hides the primary effect behind a
caveat. Aligns the code with the docs.
- apm policy status --check flag added: exits 1 when outcome is
not 'found' (i.e. policy unresolvable / absent / disabled /
fetch-failed), 0 otherwise. Default behaviour unchanged (always
exit 0) so the diagnostic remains safe for human and SIEM use,
while CI authors get the npm audit / pip check style contract
via a single flag.
Updates cli-commands.md, policy-reference.md, and CHANGELOG.md to
document the new flag and exit-code table. Adds TestStatusCheckFlag
covering the found / unresolvable / discovery-exception / json
combinations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Bump version to 0.9.1 in pyproject.toml and uv.lock - Move CHANGELOG [Unreleased] entries into [0.9.1] - 2026-04-22 - Audit pass: 1 PR = 1 entry, no bloat - Consolidate the seven scattered entries from #832 into a single Added line that names the closed issues (#827/#829/#831/#834) and keeps the migration warning inline - Combine the three fork-PR fixes (#826, #836, #837) into one Fixed entry per the multi-PR convention - Drop the doc-only consolidation commits from the log Highlights of 0.9.1: - Install-time enforcement of org apm-policy.yml (#832) - pr-review-panel automation now usable from fork PRs (#824, #826, #836, #837) - Docs site publish gated on stable releases only (#822) - Repository dogfooding via .apm/ as primitive source of truth (#823) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Bump version to 0.9.1 in pyproject.toml and uv.lock - Move CHANGELOG [Unreleased] entries into [0.9.1] - 2026-04-22 - Audit pass: 1 PR = 1 entry, no bloat - Consolidate the seven scattered entries from #832 into a single Added line that names the closed issues (#827/#829/#831/#834) and keeps the migration warning inline - Combine the three fork-PR fixes (#826, #836, #837) into one Fixed entry per the multi-PR convention - Drop the doc-only consolidation commits from the log Highlights of 0.9.1: - Install-time enforcement of org apm-policy.yml (#832) - pr-review-panel automation now usable from fork PRs (#824, #826, #836, #837) - Docs site publish gated on stable releases only (#822) - Repository dogfooding via .apm/ as primitive source of truth (#823) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enforce
apm-policy.ymlatapm installtimeSummary
apm installnow enforces the org-publishedapm-policy.ymlbefore anyfiles are written -- closing the gap where policy violations were only
caught retroactively by
apm audit --ci. The headline security delta istransitive MCP coverage: an APM package shipping a denied MCP server
(stdio, remote, or registry) is blocked at install time, before the
runtime config is written. This is the enforcement surface no other
package manager provides today.
Closes
Closes #827
Closes #834
Closes #831
What changed
apm install(pipeline path)policy_gatephase runs after dependency resolution, beforetarget integration. 9 fetch outcomes routed through shared chain
discovery (found, no-policy-published, cached-fallback, fetch-fail-no-cache,
malformed, disabled, garbage-response, no-git-remote, empty-but-valid).
packages, required version-pin mismatch, MCP server deny/transport/
trust-transitive, and
compilation.target.allow.policy_target_checkphase runs after the targets phase when theeffective target is known; enforces
compilation.target.allow.checked via a second preflight call before
MCPIntegrator.installwrites to runtime configs.
enforcement: block, install fails before integration writes. Onenforcement: warn(the default), violations appear as diagnosticsin the install summary. On
enforcement: off, verbose-only line.apm install <pkg>apm.ymlis snapshotted before mutation. If the pipeline fails apolicy check,
apm.ymlis rolled back to its byte-equal pre-mutationstate and the command exits non-zero.
apm install --mcpCovers
mcp.deny,mcp.transport.allow, andmcp.trust_transitiverules.apm install --dry-rundirect dependencies and direct MCP entries without writing files.
Transitive MCP cannot be previewed in dry-run because packages are
not installed; documented as a known limitation.
Escape hatches
--no-policyflag onapm install/install <pkg>/install --mcp. Loudly warns: "Policy enforcement disabled by--no-policyfor this invocation. This does NOT bypassapm audit --ci. CI will still fail the PR for the same policyviolation."
APM_POLICY_DISABLE=1env var, same behaviour as--no-policy.apm updatedoes NOT accept--no-policybecause that commandupdates the CLI binary, not dependencies. Use
apm install --update --no-policyfor dependency refresh.Policy discovery & cache
extends:inheritance, mergingparent + leaf policies tighten-only.
fingerprint; atomic writes via
os.replace().default); discarded past
MAX_STALE_TTL(7 days).failure; cache fallback if available.
Architecture
policy_gatephase (install/phases/policy_gate.py): wired inpipeline.pyafterresolve.run(ctx), beforetargets.run(ctx).Delegates to
run_policy_preflightfor discovery + outcome routing.policy_target_checkphase (install/phases/policy_target_check.py):runs after
targets.run(ctx)when the effective target is known;enforces
compilation.target.allowvia therun_dependency_policy_checksseam.policy/discovery.py): extracteddiscover_with_chain()called by both gate-phase andinstall_preflightpaths, eliminating the C2-panel-flaggedinheritance asymmetry.
install_preflighthelper (policy/install_preflight.py):single routing implementation for the 9-outcome matrix, shared by
pipeline gate,
--mcppreflight,--dry-runpreview, andtransitive-MCP post-collection check.
commands/install.py):apm.ymlis readbefore
install <pkg>mutation; restored on any pipeline exceptionincluding
PolicyViolationError.--no-policy/APM_POLICY_DISABLE=1short-circuit before discovery in both gate and preflight paths;
ctx.policy_enforcement_activestaysFalse, suppressing thetarget-check phase.
policy/discovery.py): merged effective policystored with chain refs + fingerprint; temp-file +
os.replace()atomicity;
MAX_STALE_TTLeviction; parallel-writer safety tested.Testing
target-check phase, MCP preflight, dry-run preview,
install <pkg>rollback,
--no-policyflag wiring, transitive MCP enforcement,install logger policy methods, cache atomicity, cache merged
effective policy, shared chain discovery, and
run_dependency_policy_checksseam.fixture directories (denied-direct, denied-transitive, mcp-denied,
required-missing, required-version-mismatch, target-mismatch,
unpacked-bundle).
are pre-existing, unrelated to this PR).
test_policy_discovery_e2e.py) requirelive DevExpGbb fixtures; skipped in CI until W4 live matrix.
block/warn, required missing, enforcement off,
--no-policyescape,APM_POLICY_DISABLE=1, no-policy-published, cached fallback,cache-miss + fetch-fail, inheritance chain, transitive dep block,
apm updatepath,install <pkg>rollback, captive-portal response,and stale-beyond-MAX_STALE_TTL.
Docs
docs/src/content/docs/enterprise/policy-reference.md: newinstall-time enforcement section (when enforcement runs, what gets
checked, escape hatches, network failure semantics, troubleshooting).
packages/apm-guide/.apm/skills/apm-usage/governance.md: mirrorsthe policy-reference install-time content for the governance skill
surface; verified consistent with policy-reference on enforcement
levels, escape hatches, and discovery rules.
Non-goals
human-readable only. Use
apm audit --ci --format jsonorapm audit --ci --format sariffor machine-readable policy output.to "no policy found" (outcome Integrate copilot runtime #2) today. Tracked for follow-up.
--policy <override>on install/update. Deferred to keep PRscope tight.
apm audit --ci --policy <override>already providesthe debugging path.
Follow-ups
policy.fetch_failure: warn|blockschema knob — allows orgs toopt into fail-closed semantics when policy cannot be fetched. The
current default is fail-open with loud warning, explicitly ratified
by the project owner.
policy_gate->run_policy_preflightdelegation refactor(eliminate remaining duplication between gate and helper).
phase (currently a second preflight call in
commands/install.py).capped at 5 with "... and N more" tail).
Review checklist
--no-policy/APM_POLICY_DISABLE=1invocation (never silenceable).install <pkg>restores byte-equalapm.ymlafterpolicy block.
MCPIntegrator.installwritesruntime config.
enforcement: warn(default) does NOT fail the install -- onlyemits diagnostics.
enforcement: blockfails install before integration writes.--dry-runpreviews verdicts without writing files; exits 0.warning (within
MAX_STALE_TTL).("Could not determine org from git remote; policy auto-discovery
skipped") — not the same as "no policy found."
policy-reference.mdandgovernance.mdagree on enforcementlevels, escape hatches, and discovery rules.