Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .apm/architecture/owners/contracts-tooling.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@
},
{
"id": "deterministic-atomic-text-writes",
"decision": "Deterministic UTF-8 text writes and atomic replacement",
"owner": "utils/atomic_io.py (write_text_lf, atomic_write_text)",
"decision": "Deterministic UTF-8 text writes, atomic replacement, and exclusive directory publication",
"owner": "utils/atomic_io.py (write_text_lf, atomic_write_text, publish_directory_noreplace)",
"selectors": ["src/apm_cli/utils/atomic_io.py"],
"guards": ["contracts-tooling-project-yaml-write-delegation"]
"guards": ["contracts-tooling-project-yaml-write-delegation", "marketplace-integrations-exclusive-directory-publication"]
},
{
"id": "read-only-lockfile-path",
Expand Down
10 changes: 10 additions & 0 deletions .apm/architecture/owners/marketplace-plugins.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{
"version": 1,
"owners": [
{
"id": "source-package-resources",
"decision": "Working-draft resource selection and non-activating APM source-envelope admission",
"owner": "models/package_resources.py (parse_resource_roots, collect_package_resources) + bundle/source_package.py (require_resource_pack_mode, reject_source_deployment, restore_source_package)",
"selectors": [
"src/apm_cli/models/package_resources.py",
"src/apm_cli/bundle/source_package.py"
],
"guards": ["marketplace-integrations-source-resources"]
},
{
"id": "marketplace-tag-pattern",
"decision": "Marketplace tag-pattern validation and expansion",
Expand Down
71 changes: 71 additions & 0 deletions docs/src/content/docs/producer/pack-a-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,77 @@ apm pack --archive -o ./dist
# -> ./dist/my-pkg-<version>.zip
```

## Independent resources (experimental)

For package-owned contracts, checks, or other data that must not become
automatically activated primitives, use the working-draft `resources` field:

```yaml
name: software-factory
version: "1.0.0"
resources:
- contracts
- checks
```

Keep an existing, supported `apm.lock.yaml` beside `apm.yml`, then run:

```bash
apm pack --format apm --source --archive -o ./dist
apm unpack --source ./dist/software-factory-1.0.0.zip -o ./acquired
```

`acquired` must not exist, even as an empty directory. Source mode copies
the original manifest, lockfile, and selected resource files byte-for-byte.
It never runs scripts, checks, hooks, dependency installation, or compilation;
it also suppresses marketplace and plugin-manifest output. Choose an isolated
acquisition directory, not a consumer workspace. Configuration in the restored
manifest remains inert until a separate command explicitly uses it.

Each resource entry is a nonempty, package-relative directory, recursively
selected without globs. Use POSIX paths without a leading `./` or trailing
slash; nested roots such as `factory/contracts` work. Roots cannot overlap
or alias each other by case. Files and directories must be regular, contained,
and unambiguous across platforms; symlinks and special files fail.
Hidden paths, `apm.yml`, `apm.lock`, `apm.lock.yaml`, `plugin.json`, `mcp.json`,
`apm_modules`, `node_modules`, `__pycache__`, `build`, and `dist` are rejected
anywhere in selected content. Do not select `.` or a broad parent containing
metadata, secrets, or caches. APM does not perform secret detection on arbitrary
resource bytes; the author is responsible for the explicitly selected content.

The existing APM envelope contains an integrity lockfile with `pack.source: true`
and `pack.bundle_files` SHA-256 entries. Its `package/` directory holds the exact
author files, including the original lockfile; restoration removes only this
envelope prefix. Hashes detect corruption, not publisher authenticity: obtain
the archive and any external checksum from a trusted source. Missing, changed,
extra, ambiguous, or unsupported-version metadata fails before restoration.
Final staged bytes are checked against the original envelope hashes before
publication, not against hashes recomputed from potentially changed input.
Metadata documents are limited to 4 MiB each; the shared asset inventory allows
at most 10,000 budgeted entries and 512 MiB in aggregate.
Portable paths are limited to 4,096 UTF-8 bytes and 128 segments.
Ordinary file permissions are copied where the host supports them; set-ID and
sticky bits are rejected. SHA-256 entries attest file bytes, not permissions,
ownership, or timestamps. Use an externally trusted archive checksum when those
archive metadata values are part of your verification policy.

Directory output and `--archive-format tar.gz` are also supported. `--dry-run`
performs the same admission and integrity checks without writing. `--force` and
`--skip-verify` cannot bypass source verification, and occupied outputs are
never replaced.
Directory publication requires native atomic no-replace support; unavailable
platform or filesystem capabilities fail closed. A destination created while
staging is also left untouched.

This route packages only the author's declared resources, not dependency
resources or installed primitives. It adds no remote acquisition operation.
`apm install` and ordinary `apm unpack` reject marked source envelopes.
Renaming the marker lockfile to legacy `apm.lock` does not enable deployment.
Ordinary pack and plugin formats reject manifests declaring resources rather
than silently dropping them; `includes` remains primitive selection.
This extension is not normative OpenAPM: omit `$schema` to select the working
draft. Existing releases without `--source` support cannot use this route.

## The plugin.json contract

`plugin.json` is the bundle's identity card. Only `name` is required. APM
Expand Down
8 changes: 8 additions & 0 deletions docs/src/content/docs/reference/cli/pack.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,16 @@ Bundles are target-agnostic. The consumer's project decides where files land at

## Options

**Experimental source mode:** `--format apm --source` packages only declared
`resources` plus the original `apm.yml` and `apm.lock.yaml`, without deployment
or marketplace output. Restore with `apm unpack --source`, not `apm install`.
See [independent resources](../../../producer/pack-a-bundle/#independent-resources-experimental)
for selection, integrity, and safety rules. Declared resources cause ordinary
pack and plugin formats to fail rather than silently omit them.

| Flag | Default | Description |
|---|---|---|
| `--source` | off | Experimental non-activating source package. Requires `--format apm`, working-draft `resources`, and an existing supported `apm.lock.yaml`. Only archive/output/dry-run/JSON/verbose options combine with this mode; outputs must not exist. |
| `--claude-plugin` | on (no-flag default) | Select the Claude Code plugin bundle: `plugin.json` plus plugin-native subdirs (`agents/`, `skills/`, `commands/`, `instructions/`, `hooks/`). This is what `apm pack` produces with zero flags. |
| `--format plugin\|agent-plugin\|claude\|claude-plugin\|apm` | `claude-plugin` | Bundle format selector. `agent-plugin` is the sole opt-in for the portable Agent Plugins v1 bundle. `plugin` is a compatibility alias for the Claude Code plugin bundle, not for `agent-plugin`. `claude` and `claude-plugin` also select the Claude Code plugin bundle (the no-flag default). `apm` emits the legacy APM bundle layout, kept for tooling that still consumes it (e.g. `microsoft/apm-action@v1` restore mode). Passing more than one selector (`--claude-plugin`, `--format`) is a usage error. |
| `--archive` | off | Produce a `.zip` archive instead of a directory (previous default: `.tar.gz`; use `--archive-format tar.gz` for legacy CI pipelines). Bundle only. |
Expand Down
27 changes: 25 additions & 2 deletions docs/src/content/docs/reference/cli/unpack.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
title: apm unpack
description: Extract an APM bundle into a project directory with verification and security scanning.
description: Restore inert source packages, or extract legacy deployment bundles.
sidebar:
order: 18
---

:::caution[Deprecated]
`apm unpack` is deprecated and will be removed in a future release. For plugin-format bundles, prefer [`apm install <bundle-path>`](../install/) -- it shares the same air-gapped path, integrates with target resolution, and records deployed files in the project lockfile. `apm unpack` remains the only deploy path for legacy `--format apm` tarballs (see [Behavior](#behavior)).
Deployment extraction without `--source` is deprecated. For plugin-format bundles, prefer [`apm install <bundle-path>`](../install/) -- it shares the same air-gapped path, integrates with target resolution, and records deployed files in the project lockfile. The experimental `--source` mode below is a separate, non-activating restoration route.
:::

## Synopsis
Expand All @@ -17,6 +17,28 @@ apm unpack BUNDLE_PATH [OPTIONS]

## Description

### Source restoration (experimental)

```bash
apm unpack --source ./software-factory-1.0.0.zip -o ./acquired
```

Accepts a directory, ZIP, or tar.gz produced by `apm pack --format apm --source`.
The output directory must not exist, even if empty. Restores the exact author
`apm.yml`, `apm.lock.yaml`, and resource paths and bytes after mandatory SHA-256
verification. Does not install dependencies, activate primitives, run checks or
hooks, compile, or modify another workspace. No network acquisition is added.
Missing, corrupt, ambiguous, or unsafe content fails before any output files
are published. `--dry-run` validates without writing; `--skip-verify` and
`--force` are rejected. Hashes verify integrity, not publisher authenticity.
See [source-package rules](../../../producer/pack-a-bundle/#independent-resources-experimental).

Marked source packages cannot be passed to `apm install` or unpacked without
`--source`. The remaining deployment behavior below applies only without
`--source`.

### Legacy deployment extraction

`apm unpack` extracts an APM bundle (a `.zip` or legacy `.tar.gz` archive, or an already-unpacked bundle directory) into a target project. It runs the built-in security scan against the bundle contents before writing any files, and -- unless `--skip-verify` is set -- checks that every entry in the bundle's `apm.lock.yaml` `deployed_files` list is actually present in the archive.

Extraction is **additive-only**: only files listed in the bundle's lockfile are written. Existing project files at colliding paths are overwritten by the bundle version. Files outside the bundle's manifest are never touched, and the bundle's `apm.lock.yaml` is treated as metadata -- it is not copied into the output directory.
Expand All @@ -38,6 +60,7 @@ governs dependency installs.

| Flag | Default | Description |
|---|---|---|
| `--source` | off | Experimental, verified, non-activating restoration into a new directory. Cannot combine with `--force` or `--skip-verify`. |
| `-o`, `--output PATH` | `.` | Target project directory. Created if it does not exist. |
| `--skip-verify` | off | Skip the bundle completeness check against the bundle's `apm.lock.yaml`. Useful for partial bundles. |
| `--dry-run` | off | List files that would be unpacked without writing anything. |
Expand Down
15 changes: 15 additions & 0 deletions docs/src/content/docs/reference/manifest-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ target: <enum | list<enum>>
type: <enum>
scripts: <map<string, string>>
includes: <enum | list<string>>
resources: <list<string>> # experimental; working draft only
registries: <map<string, RegistryEntry> & {default?: <string>}>
dependencies:
apm: <list<ApmDependency>>
Expand Down Expand Up @@ -346,6 +347,20 @@ For full client semantics - auth, lockfile fields, and routing rules - see the [

---

### 3.12. `resources` (experimental)

An optional, non-empty list of dedicated package-relative **directory** roots
for independent data, such as `resources: [contracts, checks]`. It is available
only in the working draft (omit `$schema`), not normative OpenAPM.
Unlike `includes`, it does not select deployable primitives or authorize
execution. Roots must be nonoverlapping, portable POSIX paths with no globs,
traversal, `.` root, hidden paths, or metadata/cache directories.

Only `apm pack --format apm --source` represents these resources; ordinary
pack and both plugin formats reject them. `apm unpack --source` restores their
exact bytes and author metadata into a new directory, without activation.
See [selection and restoration rules](../../producer/pack-a-bundle/#independent-resources-experimental).

## 4. Dependencies

| | |
Expand Down
11 changes: 11 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,17 @@ Lifecycle scripts fire on six events: `pre-install`, `post-install`, `pre-update

## Distribution

Experimental source packages use `apm pack --format apm --source [--archive]`
with working-draft `resources` directory roots and an existing `apm.lock.yaml`.
Only output/archive/dry-run/JSON/verbose options combine with source pack.
Restore with `apm unpack --source BUNDLE -o NEW_DIRECTORY`; the directory must
not exist. Both commands preserve exact manifest/lock/resource bytes without
installation, compilation, hook/check/script execution, or marketplace output.
`--dry-run` still validates. `--force` and `--skip-verify` cannot bypass source
integrity checks. This local route adds no remote acquisition. Plugin/ordinary
pack rejects declared resources; install and legacy unpack reject source
envelopes. See [resource selection rules](package-authoring.md#independent-resources-experimental-working-draft).

| Command | Purpose | Key flags |
|---------|---------|-----------|
| `apm pack` | Build distributable artifacts (bundle and/or marketplace.json -- driven by `apm.yml`). A `dependencies:` mapping, including `dependencies: {}`, produces a bundle of local package content; omitted or null `dependencies:` does not. Default output (no format flag) is a Claude Code plugin directory. Pass `--format agent-plugin` to opt into a portable Agent Plugins v1 bundle instead -- strict portable core only (root `plugin.json`, `skills/`, root `mcp.json` written even when empty; no `agents/`, `commands/`, `instructions/`, `extensions/`, `hooks/`, or LSP payload). That bundle build fails before any output is written if the source project has non-portable agents/commands/instructions/extensions/hooks/LSP, naming the surfaces and pointing to `--format claude-plugin` (and to configuring LSP in the target directly, since neither pack format carries it). A packed Agent Plugin installs through the declarative route: declare it as a dependency in `apm.yml` and run `apm install --target copilot`, which keeps the unit whole under `apm_modules/` and registers it without locating or executing Copilot; stable Copilot CLI 1.0.81 or newer is required when loading the projection. The imperative local-bundle route still fails closed for Agent Plugin bundles. Bundles are **target-agnostic**: `pack.target` is recorded in every bundle for diagnostic purposes (typically `"all"` for target-agnostic packs, or the project's detected target) and is not authoritative at install time; `pack.bundle_files` (path -> sha256) drives integrity verification. The consumer's project decides where files land. Dependency content is packed **exclusively** from lockfile-attested `deployed_files` (in every bundle format); the `apm_modules` cache is never packed. Each file is verified against its `deployed_file_hashes` SHA-256 before inclusion, so a file tampered after `apm install` (hash mismatch) or deleted (missing on disk) fails the pack with a message pointing at `apm install`; files with no recorded hash (older lockfiles) pack unverified. Dependency hooks-config / MCP-config is not attested, so it is not packed -- `apm pack` warns (`[!]`) and names the dependency (first-party root hooks/MCP are still packed). Marketplace-publishing projects (`marketplace:` block, no `dependencies:`) no longer emit the misleading "No plugin.json found" warning; after a successful build, a vendor-neutral catalog of artifact paths is appended together with a single docs pointer (`producer/publish-to-a-marketplace/#consume-from-any-assistant`) listing per-assistant install paths. Release-time gates `--check-versions` and `--check-clean` are opt-in and exit non-zero on misalignment / drift (codes 3 and 4 respectively) so release pipelines can fail fast; `--check-clean` is always read-only and never writes pack outputs. The version gate reads a local package's `apm.yml` first; Plugin collections without `apm.yml` use `plugin.json`'s `version`. Invalid or versionless `apm.yml` fails closed, and the fallback likewise rejects malformed or non-object JSON and a missing or blank version. When `apm.yml` declares `target: claude` or `target: copilot` (or the plural `targets:` equivalent), `apm pack` also generates an ecosystem-specific `plugin.json`: `.claude-plugin/plugin.json` for Claude (includes `mcpServers` from `.mcp.json` if present) and `.github/plugin/plugin.json` for Copilot (omits `mcpServers`). An existing file at the target path is preserved (a warning is emitted and the write is skipped) unless `--force` is passed; `--dry-run` prevents writes. Credential-bearing keys and secret-shaped values in `.mcp.json` are stripped recursively at any depth from the Claude manifest before writing, so a committed manifest never leaks secrets (see the apm pack reference, `reference/cli/pack/#credential-stripping-claude-mcpservers`). | `-o PATH`, `--archive` (produce a `.zip` archive instead of a directory; changed from `.tar.gz`), `--archive-format [zip\|tar.gz]` (default `zip`; use `tar.gz` for smaller legacy CI artifacts; only active with `--archive`), `--dry-run`, `--format [plugin\|agent-plugin\|claude\|claude-plugin\|apm]` (`agent-plugin` is the sole selector for the Agent Plugin bundle; `plugin` is a compatibility alias for the Claude plugin bundle, not for `agent-plugin`; `claude`/`claude-plugin` also select the Claude plugin bundle; `apm` selects the legacy APM layout; default `claude-plugin`), `--claude-plugin` (shortcut for `--format claude-plugin`; passing more than one of `--claude-plugin`/`--format` is a usage error), `--force`, `--offline`, `--include-prerelease`, `--marketplace=FORMATS`, `--marketplace-path FORMAT=PATH`, `--json`, `--check-versions` (release gate: per-package versions match `marketplace.versioning.strategy`; exit 3 on failure), `--check-clean` (read-only release gate: regenerate-and-diff against the effective marketplace path, including `--marketplace-path` overrides; never writes pack outputs; exit 4 on drift). `-t/--target` is **deprecated** (warn only). Exit codes: `0` success, `1` build/runtime error, `2` schema validation error, `3` `--check-versions` misalignment, `4` `--check-clean` drift. |
Expand Down
Loading
Loading