Skip to content

Commit 6a4f285

Browse files
danielmeppielSergio Sisternessergio-sisternes-epamCopilot
authored
feat(install): add --trust-bin / --no-trust-bin for plugin bin/ consent (supersedes #2045, closes #1620) (#2508)
* feat(install): add --trust-bin / --no-trust-bin CLI flag for plugin bin/ consent Adds per-invocation consent control for deploying marketplace-plugin bin/ executables during apm install. The tri-state flag layers user consent on top of the existing allowExecutables policy gate. - commands/install.py: Added --trust-bin/--no-trust-bin Click option and trust_bin field on InstallContext - install/services.py: Extracted _resolve_bin_skip() helper; fixed docstring to accurately describe gate-order (allowExecutables checked first, then trust_bin=False yields not_trusted) - integration/skill_integrator.py: Added trust_bin + bin_skip_reason_override params; removed duplicate [!] prefix from trust-posture warning (the logger already renders the symbol via symbol='warning') - install/exec_gate.py: Added not_trusted case in log_bin_status - install/template.py: Passes trust_bin from context to integrate_package_primitives - Resolved conflicts with main: kept install_result/target_decision fields on InstallContext, kept _warn_target_reconcile_failure helper, uses HEAD's transaction-guarded install_ctx block with trust_bin added Closes #1620, supersedes #2045 addresses Copilot inline on src/apm_cli/integration/skill_integrator.py:1557 addresses Copilot inline on src/apm_cli/install/services.py:248 Co-authored-by: sergio-sisternes-epam <sergio-sisternes-epam@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fold: address apm-review-panel follow-ups on --trust-bin surface Folds 6 items surfaced by the inline apm-review-panel run: FU-1 (cli-logging-expert, blocking): Use logger.warning() instead of logger.progress(symbol='warning') for the trust-posture advisory. progress() renders in blue and may be suppressed in --quiet mode; a security-relevant message must survive quiet mode and use the yellow warning affordance. FU-2 (supply-chain-security-expert, blocking): Add non-TTY/CI safety guard: when stdout is not a terminal and trust_bin is None (no explicit flag), _deploy_plugin_bin returns not_trusted before deploying. Prevents executables landing on PATH in headless CI runs without visible consent. Added test_trust_bin_none_non_tty_skips to cover this path. FU-3 (doc-writer, blocking): Add --trust-bin/--no-trust-bin to the canonical docs/reference/cli/install.md flag table under Policy and trust. Add CHANGELOG [Unreleased] Added and Changed entries for the user-visible behavioral change. FU-4 (supply-chain-security-expert): Update enterprise/security.md Executable trust gate section with a Per-invocation consent subsection documenting the flags and clarifying they cannot override policy-layer denies. FU-5 (devx-ux-expert): Rewrite --trust-bin help text in plain language; remove 'consent posture'/'trust-posture' jargon; add cross-reference to 'apm approve' for persistent trust. FU-6 (test-coverage-expert): Update _RecordingLogger to capture warning() messages; update test assertions to match new warning text and logger method. DEFER: deny-by-default redesign for interactive contexts (devx-ux-expert); lockfile drift-replay inconsistency (python-architect). addresses CEO follow-up FU-1 through FU-6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove shepherd working artifact Removes pr-body.md (shepherd-driver working file, not repo content). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spec(0.1.28): add req-sc-014 per-invocation bin/ consent in non-TTY Spec-citation fold for per-invocation executable consent in non-interactive contexts (closes #1620 Mode-B silent-extension gate). Added [req-sc-014] (Section 10.15, consumer MUST): a consumer that supports a per-invocation consent flag for bin/ executable deployment MUST deny deployment by default when stdout is not a TTY, unless the operator has explicitly opted in for that invocation. An explicit opt-in overrides the non-interactive default and permits deployment. An explicit opt-out overrides the default and denies deployment even in a terminal. The allowExecutables policy gate [req-sc-009] is evaluated first and always takes precedence. Updated: - docs/src/content/docs/specs/openapm-v0.1.md: new Section 10.15, row 19 in the Section 10.11 summary table, Section 11.3.2 Consumer enumeration, Appendix C row, statement count 111 -> 112, rev 0.1.28 - docs/public/specs/manifests/openapm-v0.1.requirements.yml: req-sc-014 - tests/spec_conformance/test_manifest_reqs.py: @pytest.mark.req Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(spec-test): use verbatim phrase from req-sc-014 spec body The assert_spec_contains test for req-sc-014 used 'stdout is not connected to a terminal' but the spec body says 'standard output is not connected to a terminal'. Match the verbatim phrase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(spec-test): match req-sc-014 assertion to actual line-split in spec body The spec text wraps 'standard output is not connected to a terminal' across two lines; fix assertion to match the verbatim line boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(spec-artifacts): regenerate CONFORMANCE.{json,md} for req-sc-014 Add req-sc-014 entry to generated CONFORMANCE artifacts. Updated Consumer count from 81 to 82 active requirements. These files are normally generated by: uv run python -m tests.spec_conformance.gen_statement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(conformance): fix CONFORMANCE.json total count after adding req-sc-014 Update total_requirements from 111 to 112 and consumer.active from 81 to 82 to reflect the newly added req-sc-014. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(tests): repair failing unit tests after trust-bin gate integration Four test breakages addressed: 1. TTY check moved from _deploy_plugin_bin to _resolve_bin_skip() (as non_interactive kwarg). This way existing unit tests that call _deploy_plugin_bin directly are not affected by the TTY check; the CI-safe behaviour flows through integrate_package_primitives which passes non_interactive=not sys.stdout.isatty(). 2. test_install_context.py: add trust_bin to EXPECTED_FIELDS and update expected field count from 35 to 36. 3. test_architecture_invariants.py: update services.py LOC budget from 1110 to 1125 to accommodate _resolve_bin_skip and call site. 4. test_plugin_bin_trust_posture.py: simplify non-TTY test to test _resolve_bin_skip directly (no longer needs TTY mock); remove unneeded patch import; add test_trust_bin_none_tty_allows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct services.py LOC budget to 1140 (actual 1132) The services.py file has grown to 1132 lines after the _resolve_bin_skip helper and non_interactive TTY check integration. Set budget to 1140 with a small margin. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Sergio Sisternes <sergio.sisternes@epam.com> Co-authored-by: sergio-sisternes-epam <sergio-sisternes-epam@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 46f8ca4 commit 6a4f285

20 files changed

Lines changed: 385 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `apm install` now accepts `--trust-bin` / `--no-trust-bin` for per-invocation
13+
consent over marketplace-plugin `bin/` executable deployment. `--trust-bin`
14+
approves deployment silently; `--no-trust-bin` skips `bin/` even when policy
15+
permits it. The `allowExecutables` policy gate still takes precedence. (closes #1620)
16+
1017
### Fixed
1118

1219
- Multi-target `apm compile` now avoids repeating expensive project analysis
1320
for each target, making multi-target runs scale like single-target runs
1421
without changing generated output. (closes #2482)
1522

23+
### Changed
24+
25+
- `apm install` now emits a trust-posture warning (via `[!]`) when a marketplace
26+
plugin deploys executables to Claude Code's PATH without an explicit `--trust-bin`
27+
flag. In non-interactive (non-TTY) contexts the default is `--no-trust-bin`.
28+
Pass `--trust-bin` to suppress the warning and deploy, or `apm approve` for
29+
persistent per-package approval.
30+
1631
## [0.28.0] - 2026-08-04
1732

1833
### Added

‎CONFORMANCE.json‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,17 @@
11541154
"tests/spec_conformance/test_manifest_reqs.py::test_configured_host_class_precedence_is_credential_isolated"
11551155
]
11561156
},
1157+
{
1158+
"conformance_class": "consumer",
1159+
"id": "req-sc-014",
1160+
"keyword": "MUST",
1161+
"section": "10.15",
1162+
"status": "active",
1163+
"test_count": 1,
1164+
"tests": [
1165+
"tests/spec_conformance/test_manifest_reqs.py::test_bin_deployment_defaults_to_deny_in_non_interactive_context"
1166+
]
1167+
},
11571168
{
11581169
"conformance_class": "consumer",
11591170
"id": "req-tg-001",
@@ -1268,7 +1279,7 @@
12681279
"spec_version": "v0.1.1",
12691280
"summary_by_class": {
12701281
"consumer": {
1271-
"active": 81,
1282+
"active": 82,
12721283
"skipped": 1,
12731284
"unbound": 0,
12741285
"xfail": 0
@@ -1292,5 +1303,5 @@
12921303
"xfail": 0
12931304
}
12941305
},
1295-
"total_requirements": 111
1306+
"total_requirements": 112
12961307
}

‎CONFORMANCE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ All four conformance classes (Producer, Consumer, Registry, Governance) carry ac
1919
| Class | Active | Skipped | Xfail | Unbound |
2020
|-------|-------:|--------:|------:|--------:|
2121
| Producer | 12 | 0 | 0 | 0 |
22-
| Consumer | 81 | 1 | 0 | 0 |
22+
| Consumer | 82 | 1 | 0 | 0 |
2323
| Registry | 1 | 0 | 0 | 0 |
2424
| Governance | 16 | 0 | 0 | 0 |
2525

@@ -128,6 +128,7 @@ All four conformance classes (Producer, Consumer, Registry, Governance) carry ac
128128
| [req-sc-011](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-011) | MUST | 10.14 | consumer | active | 1 |
129129
| [req-sc-012](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-012) | MUST | 10.14 | consumer | active | 1 |
130130
| [req-sc-013](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-013) | MUST | 10.3 | consumer | active | 1 |
131+
| [req-sc-014](docs/src/content/docs/specs/openapm-v0.1.md#req-sc-014) | MUST | 10.15 | consumer | active | 1 |
131132
| [req-tg-001](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-001) | MUST | 8.4 | consumer | active | 1 |
132133
| [req-tg-002](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-002) | MUST | 8.5 | consumer | active | 1 |
133134
| [req-tg-003](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-003) | MUST | 8.5 | consumer | active | 1 |

‎docs/public/specs/manifests/openapm-v0.1.requirements.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,11 @@ requirements:
465465
section: "10.3"
466466
conformance_class: consumer
467467
notes: "configured authority overlap selects one documented host class before credential resolution; only that class's credentials reach requests and child processes; explicit non-default ports remain scoped"
468+
- id: req-sc-014
469+
keyword: MUST
470+
section: "10.15"
471+
conformance_class: consumer
472+
notes: "per-invocation consent for bin/ deployment defaults to deny when stdout is not a TTY; explicit opt-in overrides; allowExecutables policy gate evaluated first"
468473
- id: req-rg-001
469474
keyword: MUST
470475
section: "11.3.3"

‎docs/src/content/docs/enterprise/security.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@ executables are untrusted. Manage trust with
442442
with `apm policy explain <pkg>`, and surface fleet-wide layer conflicts with
443443
`apm doctor`.
444444

445+
### Per-invocation consent (--trust-bin / --no-trust-bin)
446+
447+
For one-shot installs without changing persisted policy, `apm install` accepts
448+
`--trust-bin` (deploy bin/ executables silently) and `--no-trust-bin` (skip bin/
449+
deployment for this invocation). These flags only apply when the policy ladder
450+
would otherwise permit deployment -- they cannot override an org or project deny.
451+
In non-interactive contexts (piped output, `--frozen`) the default is
452+
`--no-trust-bin`. For persistent trust decisions that apply to the whole team,
453+
use `apm approve` or `apm deny`.
454+
445455
## MCP server trust model
446456

447457
APM integrates MCP (Model Context Protocol) server configurations from packages. Trust is explicit and scoped by dependency depth.

‎docs/src/content/docs/producer/repo-shapes.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ Claude Code skills target. Authoring rules:
218218
`executables.deny` policy (the legacy `bin_deploy` rule remains a
219219
deprecated alias) -- see the
220220
[policy schema](../../reference/policy-schema/#executables).
221+
- Consumers can pass `--trust-bin` on `apm install` to explicitly
222+
consent (suppresses the trust-posture warning) or `--no-trust-bin`
223+
to skip bin/ deployment for a single invocation. Without either
224+
flag, bin/ deploys with a prominent warning.
221225
222226
## What to read next
223227

‎docs/src/content/docs/reference/cli/install.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ auto-detection only when `apm.yml` declares no targets.
6666
| `--audit <off\|warn\|block>` | (config/policy) | Run a content audit over the files this install deploys. `warn` records findings in the summary; `block` halts the install on critical findings. Overrides your `audit-on-install` config but cannot relax an org policy floor. Requires the `external-scanners` experimental flag. |
6767
| `--no-audit` | off | Disable the install-time audit for this invocation (equivalent to `--audit off`). Cannot relax an org policy `block` floor. |
6868
| `--trust-transitive-mcp` | off | Trust self-defined MCP servers shipped by transitive packages without re-declaring them in your `apm.yml`. |
69+
| `--trust-bin` / `--no-trust-bin` | (warn) | Per-invocation consent for marketplace-plugin `bin/` executable deployment. `--trust-bin` explicitly approves deployment and suppresses the trust-posture warning. `--no-trust-bin` skips `bin/` deployment even when policy permits it. Default (neither flag): deploys `bin/` and emits a warning. In non-interactive contexts (piped output or `--frozen`), the default is equivalent to `--no-trust-bin`. The `allowExecutables` policy gate always takes precedence; `--trust-bin` cannot override a policy-level deny. For persistent per-package trust, use [`apm approve`](../approve/). |
6970
| `--allow-insecure` | off | Permit direct `http://` (non-TLS) dependencies. |
7071
| `--allow-insecure-host HOSTNAME` | unset | Permit transitive `http://` dependencies from `HOSTNAME`. Repeatable. |
7172

‎docs/src/content/docs/reference/policy-schema.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ deny).
465465

466466
This realizes Claude Code's "skills-directory plugin" contract: a folder under a skills directory that contains `.claude-plugin/plugin.json` loads as `<name>@skills-dir`, and its root `bin/` is added to the Bash tool's `PATH`. The package's `.claude-plugin/plugin.json` is required for Claude to load the folder as a plugin; APM copies it alongside `bin/` when the package ships one. The contract is Claude-specific, so deployment only targets Claude. Restart Claude Code (or run `/reload-plugins`) after install for new executables to be picked up.
467467

468-
**Security note:** deployed executables are tightened to `0o700` on POSIX systems and placed on Claude Code's `PATH`, so Claude can invoke them without further confirmation. By default, APM mirrors npm's trust model: installing a package implies trusting its declared artifacts, including executables. Use this field to opt out globally or per-package in enterprise environments.
468+
**Security note:** deployed executables are tightened to `0o700` on POSIX systems and placed on Claude Code's `PATH`, so Claude can invoke them without further confirmation. By default, APM mirrors npm's trust model: installing a package implies trusting its declared artifacts, including executables. Use this field to opt out globally or per-package in enterprise environments. Additionally, the `--trust-bin` / `--no-trust-bin` flags on `apm install` provide per-invocation consent: `--trust-bin` suppresses the trust-posture warning, `--no-trust-bin` skips bin/ deployment even if policy allows it, and the default (no flag) deploys with a warning.
469469

470470
**Scope:** bin/ deployment only activates for global (`-g`, user-scope) installs. Project-scope installs do not deploy executables.
471471

‎docs/src/content/docs/specs/openapm-v0.1.md‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2842,6 +2842,7 @@ every stored hash, foreclosing algorithm-ambiguity attacks.
28422842
| 16| Silent capability-scope widening via lossy target conversion | [req-tg-006](#req-tg-006); default-visible conversion diagnostic | Consumer-default |
28432843
| 17| Cross-target primitive deployment | [req-tg-008](#req-tg-008), [req-lk-021](#req-lk-021) | Consumer-default |
28442844
| 18| Case-collision materialization confusion | [req-lk-022](#req-lk-022), [req-rs-016](#req-rs-016) | Consumer-default |
2845+
| 19| Executable deployment in non-interactive contexts | [req-sc-014](#req-sc-014) | Consumer-default |
28452846

28462847
### 10.12 Publisher provenance and attestations (reserved for v0.2)
28472848

@@ -2929,6 +2930,30 @@ from deployment by the trust resolution of
29292930
requirement as satisfied and MUST surface each withheld executable as
29302931
a diagnostic signal distinct from any missing-package violation.
29312932

2933+
### 10.15 Per-invocation executable consent
2934+
2935+
**Threat.** An operator runs `apm install` in a non-interactive context
2936+
(piped output, CI pipeline, `--frozen` mode) and a marketplace plugin
2937+
deploys `bin/` executables to the developer tool's PATH without any
2938+
visible consent signal, because the per-invocation warning is swallowed
2939+
by log redirection.
2940+
2941+
**Mitigation.**
2942+
2943+
<a id="req-sc-014"></a>
2944+
**[req-sc-014]** A conforming **consumer** implementation that supports
2945+
a per-invocation consent flag for `bin/` executable deployment MUST deny
2946+
that deployment by default when its standard output is not connected to a
2947+
terminal (i.e., the output stream is not a TTY), unless the operator has
2948+
explicitly opted in for that invocation. An explicit per-invocation opt-in
2949+
(for example `--trust-bin`) overrides the non-interactive default and
2950+
permits deployment. An explicit per-invocation opt-out (for example
2951+
`--no-trust-bin`) overrides the non-interactive default and denies
2952+
deployment even when the output IS a terminal. The `allowExecutables`
2953+
policy gate [req-sc-009](#req-sc-009) is evaluated before per-invocation
2954+
consent and always takes precedence: a policy-level denial cannot be
2955+
overridden by a per-invocation opt-in.
2956+
29322957
---
29332958

29342959
## 11. Conformance
@@ -3027,6 +3052,7 @@ conformance statement identifying:
30273052
[req-sc-008](#req-sc-008) (SHOULD), [req-sc-009](#req-sc-009),
30283053
[req-sc-010](#req-sc-010), [req-sc-011](#req-sc-011),
30293054
[req-sc-012](#req-sc-012), [req-sc-013](#req-sc-013),
3055+
[req-sc-014](#req-sc-014),
30303056
[req-cf-001](#req-cf-001),
30313057
[req-cf-002](#req-cf-002).
30323058

@@ -3465,11 +3491,12 @@ renumbering of conformance classes.
34653491
| [req-sc-011](#req-sc-011) | MUST | 10.14 | consumer |
34663492
| [req-sc-012](#req-sc-012) | MUST | 10.14 | consumer |
34673493
| [req-sc-013](#req-sc-013) | MUST | 10.3 | consumer |
3494+
| [req-sc-014](#req-sc-014) | MUST | 10.15 | consumer |
34683495
| [req-rg-001](#req-rg-001) | MUST | 11.3.3 | registry |
34693496
| [req-cf-001](#req-cf-001) | MUST | 12.5 | consumer |
34703497
| [req-cf-002](#req-cf-002) | MUST | 12.3 | consumer |
34713498

3472-
**Total normative statements: 111** (106 MUST, 5 SHOULD).
3499+
**Total normative statements: 112** (107 MUST, 5 SHOULD).
34733500

34743501
---
34753502

@@ -3506,6 +3533,7 @@ renumbering of conformance classes.
35063533
| 0.1.25 | 2026-08-03 | Spec-citation fold for portable project-scoped Claude hooks (closes #2408 Mode-B silent-extension gate). Added [req-tg-010] (Section 8.5.4, consumer MUST): a project-scoped native hook that may launch outside the consumer project anchors its generated command through the target portable project-directory environment variable, preserves the relative hook path, executes successfully when the variable identifies the consumer project, and never embeds an absolute checkout path; shell-expansion path syntax is rejected. Claude uses `CLAUDE_PROJECT_DIR` in POSIX and `$env:CLAUDE_PROJECT_DIR` in PowerShell. Section 8.7, Section 11.3.2 Consumer enumeration, and Appendix C updated. Statement count: 108 -> 109 (104 MUST, 5 SHOULD). |
35073534
| 0.1.26 | 2026-08-03 | Spec-citation fold for VS Code OCI/Docker MCP runtime argument resolution (closes #2438). Added [req-mf-023] (Section 4.5, consumer MUST): a non-secret runtime variable resolves every `{name}` occurrence across package runtime and package arguments, an unresolved template is never written literally, and package-scoped secret metadata uses VS Code secret-input references instead of generated config bytes. Section 4.9, Section 11.3.2, and Appendix C updated. Statement count: 109 -> 110 (105 MUST, 5 SHOULD). |
35083535
| 0.1.27 | 2026-08-03 | Spec-citation fold for object-form registry identity preservation on CLI-driven manifest updates (closes the PR #2166 Mode-B silent-extension gate). Added [req-mf-024] (Section 4.3.2, consumer MUST): a consumer MUST NOT silently rewrite an existing `id:`-form (registry-sourced) manifest entry into a `git:`-form entry when persisting a subsequent CLI-driven update (e.g. an additive `--skill` pin) for the same dependency identity; when a CLI-parsed reference is ambiguous about its source but an existing manifest entry for the same identity already resolves to the `registry` source, the existing entry's source MUST be honored, and an update that would otherwise replace a registry-sourced entry with a non-registry-shaped entry MUST be rejected with a diagnostic naming the identity. Section 4.9 and Section 11.3.2 Consumer enumerations and Appendix C updated. Statement count: 110 -> 111 (106 MUST, 5 SHOULD). |
3536+
| 0.1.28 | 2026-08-06 | Spec-citation fold for per-invocation executable consent in non-interactive contexts (closes #1620 Mode-B silent-extension gate). Added [req-sc-014] (Section 10.15, consumer MUST): a consumer that supports a per-invocation consent flag for bin/ executable deployment MUST deny deployment by default when stdout is not a TTY, unless the operator has explicitly opted in for that invocation; an explicit opt-in overrides the non-interactive default and permits deployment; an explicit opt-out overrides the default and denies deployment even in a terminal; the allowExecutables policy gate [req-sc-009] is evaluated first and always takes precedence. Added row 19 to the Section 10.11 summary table. Section 11.3.2 Consumer enumeration and Appendix C updated. Statement count: 111 -> 112 (107 MUST, 5 SHOULD). |
35093537

35103538
Errata (none at publication).
35113539

0 commit comments

Comments
 (0)