Skip to content
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Required MCP runtime defaults are now overrideable prompts, while secret
defaults remain hidden and VS Code OCI launchers resolve every runtime
placeholder without writing secret values to `mcp.json`. (#2455)
- `apm marketplace audit --strict` now fails when a local marketplace source
is skipped, so CI cannot report success after an incomplete local audit.
(#2460)
Expand Down
15 changes: 13 additions & 2 deletions CONFORMANCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,17 @@
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_diagnoses_empty_skill_subset_match"
]
},
{
"conformance_class": "consumer",
"id": "req-mf-023",
"keyword": "MUST",
"section": "4.5",
"status": "active",
"test_count": 1,
"tests": [
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_resolves_runtime_argument_templates_without_secret_leakage"
]
},
{
"conformance_class": "governance",
"id": "req-pl-001",
Expand Down Expand Up @@ -1246,7 +1257,7 @@
"spec_version": "v0.1.1",
"summary_by_class": {
"consumer": {
"active": 79,
"active": 80,
"skipped": 1,
"unbound": 0,
"xfail": 0
Expand All @@ -1270,5 +1281,5 @@
"xfail": 0
}
},
"total_requirements": 109
"total_requirements": 110
}
3 changes: 2 additions & 1 deletion CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ All four conformance classes (Producer, Consumer, Registry, Governance) carry ac
| Class | Active | Skipped | Xfail | Unbound |
|-------|-------:|--------:|------:|--------:|
| Producer | 12 | 0 | 0 | 0 |
| Consumer | 79 | 1 | 0 | 0 |
| Consumer | 80 | 1 | 0 | 0 |
| Registry | 1 | 0 | 0 | 0 |
| Governance | 16 | 0 | 0 | 0 |

Expand Down Expand Up @@ -75,6 +75,7 @@ All four conformance classes (Producer, Consumer, Registry, Governance) carry ac
| [req-mf-020](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-020) | MUST | 4.1 | consumer | active | 1 |
| [req-mf-021](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-021) | MUST | 4.8 | producer | active | 1 |
| [req-mf-022](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-022) | MUST | 4.3.2 | consumer | active | 1 |
| [req-mf-023](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-023) | MUST | 4.5 | consumer | active | 1 |
| [req-pl-001](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-001) | MUST | 6.1 | governance | active | 1 |
| [req-pl-002](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-002) | MUST | 6.2 | governance | active | 1 |
| [req-pl-003](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-003) | MUST | 6.4 | governance | active | 1 |
Expand Down
4 changes: 4 additions & 0 deletions docs/public/specs/manifests/openapm-v0.1.requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ requirements:
keyword: MUST
section: "4.3.2"
conformance_class: consumer
- id: req-mf-023
keyword: MUST
section: "4.5"
conformance_class: consumer
- id: req-ext-001
keyword: MUST
section: "4.1"
Expand Down
11 changes: 11 additions & 0 deletions docs/src/content/docs/consumer/install-mcp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ VS Code prefers `npm`, PyPI, then OCI. OCI packages require Docker to be
available when the harness starts the server; no per-target launcher
configuration is needed.

When a required registry runtime variable has a default, APM prompts once
per variable and displays that default as the suggested answer. Press Enter
to accept it or provide an override. Secret defaults remain accepted on
Enter but are never displayed. For OCI/Docker launchers, non-secret selected
values replace every `{variable}` reference across the package's runtime and
package arguments before the native config is written. VS Code renders secret
variables as target-native secret-input references instead, so secret bytes
never enter `mcp.json`. A required variable without a collected value or
default declines that target configuration; VS Code treats `workspaceFolder`
as its built-in `${workspaceFolder}` token.

For VS Code and Copilot-family adapters, non-container `npm`, `pypi`,
and generic packages preserve typed v0.1 `runtimeArguments` and
`packageArguments` in authored order, with exactly one semantic package
Expand Down
29 changes: 27 additions & 2 deletions docs/src/content/docs/specs/openapm-v0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ The OPTIONAL `devDependencies` block has the same structure as
installed locally but excluded from packed plugin bundles produced
by the producer toolchain.

### 4.5 Variable references in MCP env/headers
### 4.5 Variable references in MCP env/headers and runtime arguments

Values inside `mcp[].env` and `mcp[].headers` MAY contain three
placeholder syntaxes:
Expand All @@ -656,6 +656,27 @@ literal text. When an unsupported placeholder is encountered for the
active target, the consumer MUST emit a diagnostic and MAY refuse to
write the generated config.

Registry OCI/Docker package `runtime_arguments` and `package_arguments`
entries MAY contain bare `{name}` templates in their `value` or `default`
fields. An entry's `variables` map declares metadata for variable names
across the package; `isSecret: true` marks a name secret. This syntax is
distinct from the `${...}` env/header forms above.

<a id="req-mf-023"></a>
**[req-mf-023]** A conforming **consumer** implementation that renders
a registry OCI/Docker MCP package to VS Code configuration MUST apply a
resolved non-secret variable value to every `{name}` occurrence across
the package's runtime and package arguments, including an occurrence
whose argument does not repeat the variable metadata. Secret
classification is package-scoped: once any entry declares a name with
`isSecret: true`, the consumer MUST use the VS Code secret input
reference for every occurrence of that name rather than write the
resolved secret value into generated configuration bytes. The consumer
MUST NOT write a literal unresolved `{name}` template to generated VS
Code configuration; when a required runtime-argument variable cannot
be resolved, it MUST emit a diagnostic and MAY decline that package's
target configuration.

### 4.6 Manifest extension surfaces

#### 4.6.1 `policy` (consumer-side controls)
Expand Down Expand Up @@ -752,6 +773,7 @@ This section's normative statements are:
[req-mf-016](#req-mf-016), [req-mf-018](#req-mf-018),
[req-mf-019](#req-mf-019), [req-mf-020](#req-mf-020),
[req-mf-021](#req-mf-021), [req-mf-022](#req-mf-022),
[req-mf-023](#req-mf-023),
[req-ext-001](#req-ext-001),
[req-ext-002](#req-ext-002),
[req-tg-004](#req-tg-004), [req-sc-006](#req-sc-006).
Expand Down Expand Up @@ -2957,6 +2979,7 @@ conformance statement identifying:
[req-mf-016](#req-mf-016), [req-mf-018](#req-mf-018),
[req-mf-019](#req-mf-019), [req-mf-020](#req-mf-020),
[req-mf-021](#req-mf-021), [req-mf-022](#req-mf-022),
[req-mf-023](#req-mf-023),
[req-ext-001](#req-ext-001),
[req-lk-001](#req-lk-001), [req-lk-002](#req-lk-002),
[req-lk-003](#req-lk-003), [req-lk-004](#req-lk-004),
Expand Down Expand Up @@ -3342,6 +3365,7 @@ renumbering of conformance classes.
| [req-mf-020](#req-mf-020) | MUST | 4.1 | consumer |
| [req-mf-021](#req-mf-021) | MUST | 4.8 | producer |
| [req-mf-022](#req-mf-022) | MUST | 4.3.2 | consumer |
| [req-mf-023](#req-mf-023) | MUST | 4.5 | consumer |
| [req-ext-001](#req-ext-001) | MUST | 4.1 | consumer |
| [req-ext-002](#req-ext-002) | MUST | 4.1 | producer |
| [req-lk-001](#req-lk-001) | MUST | 5.1 | consumer |
Expand Down Expand Up @@ -3430,7 +3454,7 @@ renumbering of conformance classes.
| [req-cf-001](#req-cf-001) | MUST | 12.5 | consumer |
| [req-cf-002](#req-cf-002) | MUST | 12.3 | consumer |

**Total normative statements: 109** (104 MUST, 5 SHOULD).
**Total normative statements: 110** (105 MUST, 5 SHOULD).

---

Expand Down Expand Up @@ -3465,6 +3489,7 @@ renumbering of conformance classes.
| 0.1.23 | 2026-07-31 | Spec-citation fold for case-preserving dependency materialization (closes #2347). Added [req-lk-022] (Section 5.2, consumer MUST): a consumer that case-folds repository identity but retains different source spelling records `materialization_repo_url`, validates it maps to the same canonical identity, excludes it from identity/cache/sort/trust decisions, preserves exact virtual-path casing, and either transactionally migrates one stale case variant or fails closed without deleting colliding paths. Defined rollback semantics for case-only rename and preserved interrupted recovery state. Added the field to the lockfile schema and conformance fixture, plus migration and collision conformance oracles. Hardened lockfile schema: `repo_url` now carries `minLength: 1` to match the prose requirement that git-sourced entries provide a non-empty canonical identifier ([req-lk-003](#req-lk-003)). Section 5.7, Section 10.11, Section 11.3.2, and Appendix C updated. Statement count: 106 -> 107 (102 MUST, 5 SHOULD). |
| 0.1.24 | 2026-08-03 | Spec-citation fold for fail-closed Kiro agent vocabulary gate (closes #2089 Mode-B silent-extension gate). Added [req-tg-009] (Section 8.5.1, consumer MUST): a consumer deploying an agent primitive into a target with a fixed, enumerable capability vocabulary MUST fail closed -- writing zero bytes and emitting an actionable diagnostic -- if any source-declared tool falls outside the approved set; the gate fires per agent independently and does not block vocabulary-conformant sibling agents; the gate applies only to targets included in the effective intersection under [req-tg-008]; content-identity fast-paths are not exempt. Added editorial note naming the Target Registry companion as the vocabulary authority and mandating version-pinning for conformance testing. Section 8.7, Section 11.3.2 Consumer enumeration, and Appendix C updated. Statement count: 107 -> 108 (103 MUST, 5 SHOULD). |
| 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). |
| 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). |

Errata (none at publication).

Expand Down
8 changes: 8 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ A normal project install creates or updates `apm.lock.yaml` when the manifest de

`apm install --frozen` validates package and MCP lock state before lockfile, target config, deployment, or cache mutation. A missing or stale MCP-only lock exits nonzero without writing; run normal `apm install` to repair it. `--only=mcp` follows the same guard. Add-style `--mcp NAME` is incompatible with `--frozen` because it mutates `apm.yml`.

### Registry MCP runtime variables

For registry MCP runtime variables, `apm install` prompts once for a required
non-secret default and accepts an override; secret defaults remain hidden.
Non-secret values resolve every matching `{variable}` launcher reference,
while VS Code uses secret-input references so secret bytes stay out of
`mcp.json`. A missing required value declines that target configuration.

### Target resolution chain

`apm install` resolves harness targets in strict priority order:
Expand Down
12 changes: 12 additions & 0 deletions scripts/lint-architecture-boundaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,18 @@ if [ "$apply_to_normalizer_defs" -ne 1 ] \
violations=$((violations + 1))
fi

echo "[*] AC32: MCP runtime argument variable authority"
mcp_runtime_variable_owner_defs=$(grep -rEc \
'^[[:space:]]*def _substitute_runtime_variables\(' \
src/apm_cli/adapters/client --include='*.py' \
| awk -F: '{sum += $2} END {print sum + 0}')
if [ "$mcp_runtime_variable_owner_defs" -ne 1 ] \
|| ! grep -q '^ def _substitute_runtime_variables(' "$mcp_container_owner" \
|| ! grep -q 'cls\._substitute_runtime_variables(' src/apm_cli/adapters/client/vscode.py; then
echo "[x] MCP runtime argument variables must route through MCPClientAdapter"
violations=$((violations + 1))
fi

if [ "$violations" -gt 0 ]; then
echo "[x] $violations architecture boundary rule(s) failed"
exit 1
Expand Down
89 changes: 89 additions & 0 deletions src/apm_cli/adapters/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# Detects the legacy ``<VAR>`` placeholder syntax only. Used to aggregate
# deprecation warnings across all servers in a single install run.
_LEGACY_ANGLE_VAR_RE = re.compile(r"<([A-Z_][A-Z0-9_]*)>")
_RUNTIME_TEMPLATE_VARIABLE_RE = re.compile(r"(?<!\$)\{([a-zA-Z_][a-zA-Z0-9_]*)\}")

# MCP Registry v0.1 names the container registry type ``oci``; the adapters
# below key their launcher dispatch on ``docker`` (the vocabulary the legacy
Expand Down Expand Up @@ -656,6 +657,94 @@ def _resolve_non_container_argument_value(
None,
)

@staticmethod
def _package_runtime_variable_metadata(
package: dict[str, Any] | None,
) -> dict[str, dict] | None:
"""Return one validated runtime-variable declaration map for a package.

Registry metadata may declare a variable on one argument and reference it
later without repeating the declaration. Keeping this map package-scoped
makes those references resolve consistently while rejecting malformed
secret metadata before an adapter can render a collected value.
"""
metadata_by_name: dict[str, dict] = {}
for field_name in ("runtime_arguments", "package_arguments"):
for argument in (package or {}).get(field_name) or []:
if not isinstance(argument, dict):
continue
variables = argument.get("variables")
if variables is None:
continue
if not isinstance(variables, dict):
return None
for name, metadata in variables.items():
if not isinstance(name, str) or not isinstance(metadata, dict):
return None
secret_marker = metadata.get("isSecret", metadata.get("is_secret", False))
if not isinstance(secret_marker, bool):
return None
existing = metadata_by_name.get(name)
if existing is not None and existing != metadata:
return None
metadata_by_name[name] = metadata
return metadata_by_name

@staticmethod
def _substitute_runtime_variables(
template: str,
variables: dict[object, object] | None,
runtime_vars: dict[str, object] | None,
runtime_variable_fallbacks: dict[str, str] | None = None,
secret_variable_fallbacks: dict[str, str] | None = None,
) -> str | None:
"""Resolve every APM runtime variable in one registry argument template.

Variable metadata may appear on an earlier argument while later
arguments reference the same value. Collected values therefore apply to
every template, not only the metadata-bearing entry. Target-specific
fallbacks are used only for declared variables with no collected value.
"""
secret_fallbacks = secret_variable_fallbacks or {}
values: dict[str, str] = dict(secret_fallbacks)
for name, value in (runtime_vars or {}).items():
if (
isinstance(name, str)
and name not in secret_fallbacks
and value is not None
and str(value) != ""
):
values[name] = str(value)

fallbacks = runtime_variable_fallbacks or {}
for name, metadata in (variables or {}).items():
if not isinstance(name, str) or not isinstance(metadata, dict):
return None
placeholder = f"{{{name}}}"
if placeholder not in template or name in values:
continue
secret_marker = metadata.get("isSecret", metadata.get("is_secret", False))
if not isinstance(secret_marker, bool):
return None
if secret_marker is True:
return None
fallback = fallbacks.get(name)
if fallback is not None:
values[name] = fallback
continue
configured = metadata.get("value", metadata.get("default"))
if isinstance(configured, str) and configured:
values[name] = configured
elif configured is not None:
return None

for name, value in values.items():
template = template.replace(f"{{{name}}}", value)

if _RUNTIME_TEMPLATE_VARIABLE_RE.search(template):
return None
return template

@staticmethod
def _processed_non_container_argument_groups(
arguments: list[object],
Expand Down
Loading
Loading