Conversation
🦋 Changeset detectedLatest commit: 3841183 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughThe adapter API advances from Sequence Diagram(s)sequenceDiagram
participant CLI
participant materialize
participant Adapter
participant SDK
participant Filesystem
CLI->>materialize: install asset
materialize->>Adapter: readAsset(read request)
Adapter->>SDK: read skill bundle or single file
SDK->>Filesystem: read asset files
Filesystem-->>SDK: asset bytes or structured failure
SDK-->>Adapter: tagged read result
Adapter-->>materialize: asset or failure
materialize->>Adapter: installAsset(install request)
Adapter->>SDK: install bundle or single file
SDK->>Filesystem: write and reconcile asset files
Filesystem-->>SDK: write result
SDK-->>Adapter: tagged install result
Adapter-->>materialize: primary path or failure
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6bf5b4501
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Defense-in-depth: the textual validation above already guarantees | ||
| // containment; this re-checks the resolved form so a validator bug | ||
| // can't silently become a traversal. | ||
| if (abs !== root && !abs.startsWith(`${root}/`)) { |
There was a problem hiding this comment.
Use platform-safe containment for companion paths
On Windows, node:path produces backslash-separated root and abs values, so a valid companion such as references/api.md resolves to C:\...\review\references\api.md but can never start with ${root}/. As a result, every non-empty companion set is rejected as invalid-companion-path by install, read, and delete operations on Windows. Use relative/isAbsolute (as the pruning helper does) or the platform separator for this defense-in-depth containment check.
Useful? React with 👍 / 👎.
| const newSet = new Set(newPaths) | ||
| const staleOwned = options.ownedCompanionPaths.filter((p) => !newSet.has(p)) |
There was a problem hiding this comment.
Reject SKILL.md from companion ownership
A direct SDK caller can supply SKILL.md as either a companion key or an owned companion path because the containment validation accepts it. In the former case the companion-write loop overwrites the assembled primary with opaque bytes; in the latter it is treated as stale and deleted after being written. This violates the bundle contract and can leave a successful install with no valid primary, so reserve the primary filename when validating both supplied companion sets.
Useful? React with 👍 / 👎.
|
7e815ef to
2b854bf
Compare
c6bf5b4 to
332e90d
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 332e90d12d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| undo: async () => { | ||
| if (previous) { | ||
| await adapter.installAsset(asset.scope, asset.type, asset.name, previous.content, previous.metadata ?? {}) | ||
| await adapter.installAsset(installRequestFor(asset, previous.content, previous.metadata ?? {})) |
There was a problem hiding this comment.
Propagate failed rollback results
When a later materialization operation fails and the journal rolls this write back, first-party adapters now report an undo failure as { ok: false } rather than throwing. This callback ignores that result, so InstallJournal.rollback() counts the undo as successful and the install can report a clean rollback even though the original asset was not restored (the delete undo at line 304 has the same issue). Check each result and throw or otherwise surface its failure to the journal.
AGENTS.md reference: AGENTS.md:L172-L183
Useful? React with 👍 / 👎.
| if (!check.ok) { | ||
| return { ok: false, failure: { code: 'invalid-companion-path', path: relPath, reason: check.reason } } | ||
| } | ||
| const abs = resolve(join(root, relPath)) |
There was a problem hiding this comment.
Reject symlinked companion parents
If a user-controlled directory inside an existing skill root is a symlink (for example, references -> /tmp/outside), this lexical resolve() check still accepts references/api.md; the subsequent mkdir/writeFile follows the symlink and writes the companion outside the skill root. Validate canonical existing parents with lstat/realpath (and reject symlink components) before mutating bundle paths so the promised containment holds against filesystem links.
Useful? React with 👍 / 👎.
332e90d to
b24cc08
Compare
2b854bf to
3526a29
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b24cc08b5e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const snapshot: Snapshot = new Map() | ||
| try { | ||
| // Snapshot everything we will touch before mutating anything. | ||
| await snapshotFile(snapshot, paths.primaryFile) |
There was a problem hiding this comment.
Validate primary paths before mutating skill bundles
SkillBundlePaths is a public SDK input, but only companion paths are validated here. A direct adapter-SDK caller can provide a legitimate root with primaryFile outside it (for example, /tmp/victim); install will snapshot and overwrite that file, while delete will remove it. Validate that the resolved primary is a strict descendant of root before any snapshot, read, or mutation so the documented containment guarantee holds.
AGENTS.md reference: packages/adapter/AGENTS.md:L10-L20
Useful? React with 👍 / 👎.
b24cc08 to
947602e
Compare
3526a29 to
93a9c9b
Compare
947602e to
842359d
Compare
93a9c9b to
009140b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 842359d53c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const companions: Record<string, Uint8Array> = {} | ||
| for (const [relPath, abs] of validated.resolved) { | ||
| try { | ||
| companions[relPath] = new Uint8Array(await readFile(abs)) |
There was a problem hiding this comment.
Use a prototype-free map for returned companions
When an owned companion is legitimately named __proto__, this assignment invokes the inherited prototype setter rather than creating a map entry. The file is read successfully but is omitted from asset.companions (and the returned object's prototype is mutated), so a subsequent integrity/reconciliation pass treats that companion as missing. Use Object.create(null) or a Map-backed conversion for companion results.
Useful? React with 👍 / 👎.
| for (const [relPath, bytes] of Object.entries(options.companions)) { | ||
| // biome-ignore lint/style/noNonNullAssertion: resolved contains every validated path | ||
| const abs = resolved.get(relPath)! | ||
| currentPath = abs | ||
| await mkdir(dirname(abs), { recursive: true }) | ||
| await writeFile(abs, bytes) |
There was a problem hiding this comment.
Reject companion paths that collide by filesystem case
On case-insensitive filesystems (including standard Windows and many macOS volumes), a bundle containing both References/api.md and references/api.md passes validation but both writes target the same file. The second write silently overwrites the first and the install reports success; later reads return the second bytes under both keys. Detect filesystem-equivalent duplicate companion paths before snapshotting/writing and return a structured failure.
Useful? React with 👍 / 👎.
009140b to
e2b15b8
Compare
842359d to
99729d4
Compare
e2b15b8 to
54323fe
Compare
99729d4 to
98ab0d2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98ab0d260f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // stale-owned sweep deletes a primary that was just written. A companion | ||
| // named SKILL.md in a sub-directory (references/SKILL.md) is fine — it | ||
| // resolves to a different path than the primary. | ||
| if (abs === primaryAbs) { |
There was a problem hiding this comment.
Reject case-equivalent primary companion paths
On case-insensitive filesystems, a companion named skill.md (or another case-only variant of SKILL.md) resolves to the primary file even though these absolute-path strings differ. It passes this exact comparison and the companion write then overwrites the assembled primary with opaque bytes; if supplied only as an owned path it can likewise be removed as stale. Compare the companion against the primary using the same portable case-fold/NFC key used for companion collisions.
Useful? React with 👍 / 👎.
| await mkdir(dirname(abs), { recursive: true }) | ||
| await writeFile(abs, bytes) |
There was a problem hiding this comment.
Reject symlinked companion targets before writing
When an existing companion file itself is a symlink (for example, references/api.md -> /tmp/victim), rejectSymlinkedParents permits it because only its parent directories are checked. writeFile follows that final symlink, so an otherwise contained bundle request writes outside the skill root despite the helper's containment guarantee. Lstat and reject the target itself (and similarly protect the primary) before snapshotting or mutating.
Useful? React with 👍 / 👎.
54323fe to
e4de25e
Compare
daba02d to
f9cf6ea
Compare
f9cf6ea to
3feeead
Compare
3feeead to
b499951
Compare
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b499951e0e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| while (current !== root && isStrictlyBelow(current, root)) { | ||
| chain.push(current) | ||
| current = dirname(current) |
There was a problem hiding this comment.
Reject a symlinked skill root before materializing
When an existing skill root itself is a symlink (for example, skills/review -> /tmp/outside), this walk deliberately stops before checking root; the later mkdir/writeFile calls therefore follow that link and install the primary and companions outside the adapter-controlled tree. This is distinct from the intermediate-parent check: validate the root directory itself with lstat (while still allowing symlinked ancestors such as /var) before any snapshot or mutation.
Useful? React with 👍 / 👎.
| failure: { code: 'io-failed', operation: 'delete', path: path.file, message: errorMessage(err) }, | ||
| } | ||
| } | ||
| return { ok: true, existed, deletedPaths: existed ? [path.file] : [] } |
There was a problem hiding this comment.
Sidecar cleanup skews delete result
Medium Severity
The new result-shaped delete helpers set existed from the primary (and owned companions) only, then still remove legacy ${primary}.meta.json sidecars via deleteAssetFile or an extra rm. When the primary is absent but a sidecar remains, delete succeeds with existed: false and empty deletedPaths even though a file was removed, breaking the tagged contract’s idempotent bookkeeping.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b499951. Configure here.
| return { ok: false, failure: { code: 'invalid-companion-path', path: relPath, reason: parentIssue } } | ||
| } | ||
|
|
||
| resolvedMap.set(relPath, abs) |
There was a problem hiding this comment.
Companion symlink files bypass containment
Medium Severity
resolveCompanionPaths rejects symlinked parent directories but never inspects the resolved companion path itself. If that leaf already exists as a symbolic link, subsequent writeFile / readFile calls follow the link, so bundle install or read can touch bytes outside the skill root despite the stated pre-mutation containment checks.
Reviewed by Cursor Bugbot for commit b499951. Configure here.
…unions and atomic skill-bundle helpers
b499951 to
8840e4d
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8840e4d. Configure here.
| // the sidecar path. | ||
| await snapshotFile(snapshot, sidecarPath) | ||
| currentPath = sidecarPath | ||
| await rm(sidecarPath, { force: true }) |
There was a problem hiding this comment.
Sidecar cleanup removes unowned files
Medium Severity
deleteSkillBundle always removes ${primaryFile}.meta.json after the owned-target loop, even when that path was never listed in ownedCompanionPaths. A valid companion at the relative path SKILL.md.meta.json resolves to the same absolute path as the legacy sidecar, so a delete with an empty owned set can remove that file while still claiming to preserve unowned skill files.
Reviewed by Cursor Bugbot for commit 8840e4d. Configure here.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/guides/custom-adapters.mdx (1)
267-267: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winStale
assetPathreference after example rewrite.The scaffolded example was rewritten to use
baseDir+skillPaths(name)(noassetPathhelper exists anymore), but this sentence still tells readers to check files against "yourassetPath". This now points to a symbol that doesn't exist in the example.📝 Proposed fix
-Check that files landed where your `assetPath` puts them. From here, harden `buildAssetMetadata` with a real schema and flesh out the on-disk layout your tool expects. +Check that files landed where your `baseDir`/`skillPaths()` layout puts them. From here, harden `buildAssetMetadata` with a real schema and flesh out the on-disk layout your tool expects.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 36b52b7f-0fa6-49c5-8802-1de6b68b9eaa
📒 Files selected for processing (38)
.changeset/tagged-adapter-asset-contract.mddocs/cli/adapters/install.mdxdocs/cli/adapters/list.mdxdocs/guides/custom-adapters.mdxdocs/guides/troubleshooting.mdxpackages/adapter/src/__tests__/index.test.tspackages/adapter/src/__tests__/skill-bundle.test.tspackages/adapter/src/api-version.tspackages/adapter/src/asset-fs.tspackages/adapter/src/define-adapter.tspackages/adapter/src/index.tspackages/adapter/src/skill-bundle.tspackages/adapter/src/types.tspackages/adapters/claude-code/src/__tests__/adapter.test.tspackages/adapters/claude-code/src/index.tspackages/adapters/codex/src/__tests__/adapter.test.tspackages/adapters/codex/src/index.tspackages/adapters/opencode/src/__tests__/adapter.test.tspackages/adapters/opencode/src/index.tspackages/cli/src/__tests__/adapter-install-cli.e2e.test.tspackages/cli/src/commands/add/__tests__/add.test.tspackages/cli/src/commands/install/__tests__/install-cli.test.tspackages/cli/src/util/__tests__/adapter-install-errors.test.tspackages/engine/src/__tests__/build-pipeline.test.tspackages/engine/src/__tests__/materialize.test.tspackages/engine/src/__tests__/run-install.test.tspackages/engine/src/adapters/__tests__/api-compatibility.test.tspackages/engine/src/adapters/__tests__/inspect.test.tspackages/engine/src/adapters/__tests__/placement-managed.test.tspackages/engine/src/adapters/__tests__/verify.test.tspackages/engine/src/adapters/api-compatibility.tspackages/engine/src/adapters/verify.tspackages/engine/src/install/__tests__/run-add.test.tspackages/engine/src/install/__tests__/run-install.chain.test.tspackages/engine/src/install/__tests__/run-install.receipt.test.tspackages/engine/src/install/__tests__/run-install.test.tspackages/engine/src/install/__tests__/run-remove.test.tspackages/engine/src/install/materialize.ts
| for (const [relPath, bytes] of Object.entries(options.companions)) { | ||
| // biome-ignore lint/style/noNonNullAssertion: resolved contains every validated path | ||
| const abs = resolved.get(relPath)! | ||
| currentPath = abs | ||
| await mkdir(dirname(abs), { recursive: true }) | ||
| await writeFile(abs, bytes) |
There was a problem hiding this comment.
Root-level companion bypasses symlink guard, enabling arbitrary file write
rejectSymlinkedParents(abs, root) starts its walk at dirname(abs). For a root-level companion like api.md, dirname(abs) equals root, so the while condition current !== root is immediately false — zero iterations run and no symlink check occurs. Additionally, abs itself (the final path component) is never lstat-checked, so a pre-placed symlink at <root>/api.md passes all validation and the subsequent writeFile(abs, bytes) follows the link to an external target. An attacker who can create a file at the skill root level (or a path whose final component is a symlink, with real parent dirs) redirects companion bytes outside the root on any install that names that path.
Fix: call lstat(abs) after the parent walk and reject if it is a symbolic link.
How this was verified: The security-review subagent created a symlink at <skill-root>/api.md → /outside/api.md, called installSkillBundle with companion key 'api.md', and confirmed writeFile followed the link and wrote the companion bytes to the external target. The root-level bypass (while loop never executes) was confirmed by tracing installSkillBundle → resolveCompanionPaths → rejectSymlinkedParents; the external file contained the injected content.
| try { | ||
| await mkdir(dirname(paths.primaryFile), { recursive: true }) | ||
| await writeFile(paths.primaryFile, assembleAssetContent(options.content, options.metadata), 'utf8') | ||
| for (const [relPath, bytes] of Object.entries(options.companions)) { | ||
| // biome-ignore lint/style/noNonNullAssertion: resolved contains every validated path | ||
| const abs = resolved.get(relPath)! | ||
| currentPath = abs | ||
| await mkdir(dirname(abs), { recursive: true }) | ||
| await writeFile(abs, bytes) |
There was a problem hiding this comment.
TOCTOU race: symlink inserted between validation and mutation can redirect companion write
resolveCompanionPaths lstat-checks parent directories during the validation phase, but mkdir/writeFile execute in a separate block after validation completes. An actor who can write to the skill directory during that window can atomically rename a validated real directory to a symlink pointing outside the root. The subsequent mkdir(dirname(abs), { recursive: true }) follows the link, and writeFile(abs, bytes) writes companion bytes to the external location. The race window spans the entire snapshot + mutation phase.
Practical mitigations include: re-lstat the resolved path immediately before each write (with the understanding that races still exist at a narrow level), writing to a temp path inside the root then using rename, or using platform-specific no-follow open semantics.
How this was verified: The security-review subagent validated a companion path with a real parent directory, then replaced the parent with a symlink targeting an external dir before the mutation phase ran. The companion write landed outside the skill root; exit code 0; external file contained the injected content.
Merge activity
|
This PR was auto-generated by the release workflow. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @agent-facets/adapter@0.28.0 ### Minor Changes - [#438](#438) [`d20cdae`](d20cdae) Thanks [@eXamadeus](https://github.com/eXamadeus)! - **BREAKING (pre-1.0 minor):** the adapter asset contract is now tagged request/result unions instead of positional parameters, and the adapter API identifier advances from `0.0` to `0.1`. `installAsset`, `readAsset`, and `deleteAsset` each take a single request object tagged by `assetType` and return a discriminated result — expected failures (`not-found`, `invalid-companion-path`, `unsupported-scope`, `not-implemented`, `io-failed`) are structured values, never thrown errors. Skill requests carry a companion byte map plus the caller-verified owned companion path set for atomic multi-file skill bundles; agent and command requests structurally cannot carry companions. `defineAdapter` stubs for omitted methods now return `not-implemented` failures instead of throwing. The SDK's canonical `ADAPTER_API_VERSION` is now `0.1`, identifying this tagged contract; `defineAdapter()` stamps it and first-party packages publish `"facetAdapterApiVersion": "0.1"`. `0.0` named the earlier positional contract: a CLI that supports only `0.1` classifies a `0.0` adapter as well-formed but unsupported and fails closed (before any contract method or project write) with reinstall guidance. There is no positional/tagged compatibility bridge — an adapter built against `0.0` must be rebuilt against a `0.1` SDK release and reinstalled. New SDK helpers: `installSkillBundle` / `readSkillBundle` / `deleteSkillBundle` (staged all-or-nothing bundle replacement with rollback, ownership-set-based deletion, and empty-directory pruning), `installSingleFileAsset` / `readSingleFileAsset` / `deleteSingleFileAsset` (result-shaped single-file operations), and `validateContainedRelativePath` (pre-filesystem containment validation applied to every supplied companion path). Every adapter implementing the previous positional contract must migrate. The first-party claude-code, opencode, and codex adapters are migrated in their matching minor releases; codex delete operations now prune emptied directories consistently with the other adapters. Release ordering: this SDK release and the three first-party adapter releases publish `0.1` to npm **before** any `agent-facets` CLI release requires `0.1`. Until that CLI ships, existing `0.0` CLIs keep selecting the highest compatible `0.0` adapter release, so this changeset intentionally carries **no** `agent-facets` bump — the CLI change that makes `0.1` the supported set lands in a later release cycle gated on all three first-party adapters having published `facetAdapterApiVersion: 0.1`. ## @agent-facets/adapter-claude-code@0.9.0 ### Minor Changes - [#438](#438) [`d20cdae`](d20cdae) Thanks [@eXamadeus](https://github.com/eXamadeus)! - **BREAKING (pre-1.0 minor):** the adapter asset contract is now tagged request/result unions instead of positional parameters, and the adapter API identifier advances from `0.0` to `0.1`. `installAsset`, `readAsset`, and `deleteAsset` each take a single request object tagged by `assetType` and return a discriminated result — expected failures (`not-found`, `invalid-companion-path`, `unsupported-scope`, `not-implemented`, `io-failed`) are structured values, never thrown errors. Skill requests carry a companion byte map plus the caller-verified owned companion path set for atomic multi-file skill bundles; agent and command requests structurally cannot carry companions. `defineAdapter` stubs for omitted methods now return `not-implemented` failures instead of throwing. The SDK's canonical `ADAPTER_API_VERSION` is now `0.1`, identifying this tagged contract; `defineAdapter()` stamps it and first-party packages publish `"facetAdapterApiVersion": "0.1"`. `0.0` named the earlier positional contract: a CLI that supports only `0.1` classifies a `0.0` adapter as well-formed but unsupported and fails closed (before any contract method or project write) with reinstall guidance. There is no positional/tagged compatibility bridge — an adapter built against `0.0` must be rebuilt against a `0.1` SDK release and reinstalled. New SDK helpers: `installSkillBundle` / `readSkillBundle` / `deleteSkillBundle` (staged all-or-nothing bundle replacement with rollback, ownership-set-based deletion, and empty-directory pruning), `installSingleFileAsset` / `readSingleFileAsset` / `deleteSingleFileAsset` (result-shaped single-file operations), and `validateContainedRelativePath` (pre-filesystem containment validation applied to every supplied companion path). Every adapter implementing the previous positional contract must migrate. The first-party claude-code, opencode, and codex adapters are migrated in their matching minor releases; codex delete operations now prune emptied directories consistently with the other adapters. Release ordering: this SDK release and the three first-party adapter releases publish `0.1` to npm **before** any `agent-facets` CLI release requires `0.1`. Until that CLI ships, existing `0.0` CLIs keep selecting the highest compatible `0.0` adapter release, so this changeset intentionally carries **no** `agent-facets` bump — the CLI change that makes `0.1` the supported set lands in a later release cycle gated on all three first-party adapters having published `facetAdapterApiVersion: 0.1`. ## @agent-facets/adapter-codex@0.7.0 ### Minor Changes - [#438](#438) [`d20cdae`](d20cdae) Thanks [@eXamadeus](https://github.com/eXamadeus)! - **BREAKING (pre-1.0 minor):** the adapter asset contract is now tagged request/result unions instead of positional parameters, and the adapter API identifier advances from `0.0` to `0.1`. `installAsset`, `readAsset`, and `deleteAsset` each take a single request object tagged by `assetType` and return a discriminated result — expected failures (`not-found`, `invalid-companion-path`, `unsupported-scope`, `not-implemented`, `io-failed`) are structured values, never thrown errors. Skill requests carry a companion byte map plus the caller-verified owned companion path set for atomic multi-file skill bundles; agent and command requests structurally cannot carry companions. `defineAdapter` stubs for omitted methods now return `not-implemented` failures instead of throwing. The SDK's canonical `ADAPTER_API_VERSION` is now `0.1`, identifying this tagged contract; `defineAdapter()` stamps it and first-party packages publish `"facetAdapterApiVersion": "0.1"`. `0.0` named the earlier positional contract: a CLI that supports only `0.1` classifies a `0.0` adapter as well-formed but unsupported and fails closed (before any contract method or project write) with reinstall guidance. There is no positional/tagged compatibility bridge — an adapter built against `0.0` must be rebuilt against a `0.1` SDK release and reinstalled. New SDK helpers: `installSkillBundle` / `readSkillBundle` / `deleteSkillBundle` (staged all-or-nothing bundle replacement with rollback, ownership-set-based deletion, and empty-directory pruning), `installSingleFileAsset` / `readSingleFileAsset` / `deleteSingleFileAsset` (result-shaped single-file operations), and `validateContainedRelativePath` (pre-filesystem containment validation applied to every supplied companion path). Every adapter implementing the previous positional contract must migrate. The first-party claude-code, opencode, and codex adapters are migrated in their matching minor releases; codex delete operations now prune emptied directories consistently with the other adapters. Release ordering: this SDK release and the three first-party adapter releases publish `0.1` to npm **before** any `agent-facets` CLI release requires `0.1`. Until that CLI ships, existing `0.0` CLIs keep selecting the highest compatible `0.0` adapter release, so this changeset intentionally carries **no** `agent-facets` bump — the CLI change that makes `0.1` the supported set lands in a later release cycle gated on all three first-party adapters having published `facetAdapterApiVersion: 0.1`. ## @agent-facets/adapter-opencode@0.10.0 ### Minor Changes - [#438](#438) [`d20cdae`](d20cdae) Thanks [@eXamadeus](https://github.com/eXamadeus)! - **BREAKING (pre-1.0 minor):** the adapter asset contract is now tagged request/result unions instead of positional parameters, and the adapter API identifier advances from `0.0` to `0.1`. `installAsset`, `readAsset`, and `deleteAsset` each take a single request object tagged by `assetType` and return a discriminated result — expected failures (`not-found`, `invalid-companion-path`, `unsupported-scope`, `not-implemented`, `io-failed`) are structured values, never thrown errors. Skill requests carry a companion byte map plus the caller-verified owned companion path set for atomic multi-file skill bundles; agent and command requests structurally cannot carry companions. `defineAdapter` stubs for omitted methods now return `not-implemented` failures instead of throwing. The SDK's canonical `ADAPTER_API_VERSION` is now `0.1`, identifying this tagged contract; `defineAdapter()` stamps it and first-party packages publish `"facetAdapterApiVersion": "0.1"`. `0.0` named the earlier positional contract: a CLI that supports only `0.1` classifies a `0.0` adapter as well-formed but unsupported and fails closed (before any contract method or project write) with reinstall guidance. There is no positional/tagged compatibility bridge — an adapter built against `0.0` must be rebuilt against a `0.1` SDK release and reinstalled. New SDK helpers: `installSkillBundle` / `readSkillBundle` / `deleteSkillBundle` (staged all-or-nothing bundle replacement with rollback, ownership-set-based deletion, and empty-directory pruning), `installSingleFileAsset` / `readSingleFileAsset` / `deleteSingleFileAsset` (result-shaped single-file operations), and `validateContainedRelativePath` (pre-filesystem containment validation applied to every supplied companion path). Every adapter implementing the previous positional contract must migrate. The first-party claude-code, opencode, and codex adapters are migrated in their matching minor releases; codex delete operations now prune emptied directories consistently with the other adapters. Release ordering: this SDK release and the three first-party adapter releases publish `0.1` to npm **before** any `agent-facets` CLI release requires `0.1`. Until that CLI ships, existing `0.0` CLIs keep selecting the highest compatible `0.0` adapter release, so this changeset intentionally carries **no** `agent-facets` bump — the CLI change that makes `0.1` the supported set lands in a later release cycle gated on all three first-party adapters having published `facetAdapterApiVersion: 0.1`. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Publishes a breaking pre-1.0 adapter contract (`0.1`) that forces third-party adapter rebuilds; lockfile schema change may affect tooling that reads facets.lock. > > **Overview** > Automated **release** PR that bumps **`@agent-facets/adapter`** to **0.28.0** and the claude-code, opencode, and codex adapter packages to matching minors, with changelog entries for the **adapter API `0.0` → `0.1`** breaking change from [#438](#438). The consumed changeset is removed and **`bun.lock`** version pins are updated accordingly; there is **no `agent-facets` CLI bump** in this cycle. > > **`facets.lock`** moves to **`lockfileVersion` 0.2**, adding per-asset **`files`** entries (relative path + `sha256` integrity) for every locked facet asset instead of name-only asset records. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 95e8cc6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->



Why
The previous adapter asset contract used positional parameters (
scope,assetType,name,content,metadata) and communicated failures by throwing errors. This made it impossible to represent multi-file skill bundles, forced callers to catch and classify thrown errors, and left no structural distinction between skill, agent, and command requests.Details
installAsset,readAsset, anddeleteAssetnow each accept a single request object tagged byassetTypeand return a discriminated result union. Expected failures (not-found,invalid-companion-path,unsupported-scope,not-implemented,io-failed) are structured values on the result — nothing is thrown. Skill requests carry a companion byte map (CompanionMap) and a caller-supplied owned companion path set; agent and command request types structurally cannot carry companions, so the distinction is enforced at the type level rather than by convention.defineAdapterstubs for omitted methods now return{ ok: false, failure: { code: 'not-implemented', method } }instead of throwing.Three new SDK helpers cover the skill bundle lifecycle:
installSkillBundle/readSkillBundle/deleteSkillBundle— staged all-or-nothing bundle replacement with snapshot/rollback, ownership-set-based deletion, and empty-directory pruning. Ownership is caller-supplied per operation; adapters never enumerate the skill directory or infer ownership from disk.installSingleFileAsset/readSingleFileAsset/deleteSingleFileAsset— result-shaped wrappers for single-file agent and command assets.validateContainedRelativePath— purely textual containment validation applied to every supplied companion path before any filesystem access. One failing path rejects the whole request without touching the filesystem.The engine's
materialize.tsis updated to drive the new contract:readPreviousnarrows on the structurednot-foundresult instead of catching ENOENT, and install/delete calls branch onresult.okbefore recording journal entries. Skill install requests from the engine currently pass empty companion maps and owned-path sets; real companion bytes and lockfile-derived ownership are deferred to the per-file integrity work.The claude-code, opencode, and codex adapters are fully migrated. Codex delete operations now prune emptied directories consistently with the other adapters, which they previously did not.
Verification
CI — adapter SDK unit tests cover companion-less and multi-file skills, escaping paths in bundles and ownership sets, malformed ownership rejection before filesystem access, idempotence, canonical reads limited to requested owned paths, unowned content preservation, and injected failures at every write/delete/commit boundary. All three first-party adapter test suites are updated and pass.
Note
High Risk
Breaking wire contract and filesystem materialization across SDK, engine, and all adapters; skill-bundle path validation and rollback are security- and data-integrity sensitive, though heavily tested.
Overview
Breaking adapter contract (
0.0→0.1):installAsset,readAsset, anddeleteAssetnow take a single request tagged byassetTypeand return{ ok: true, … } | { ok: false, failure }. Expected outcomes use failure codes (not-found,invalid-companion-path,unsupported-scope,not-implemented,io-failed) instead of throws; omitted methods indefineAdapterreturnnot-implementedresults.The SDK adds atomic skill-bundle helpers (
installSkillBundle/readSkillBundle/deleteSkillBundle) with snapshot rollback, caller-supplied owned companion paths, and pruning, plus result-shaped single-file helpers andvalidateContainedRelativePathfor pre-IO path checks. Skill installs can carry companion byte maps; agent/command types cannot.Engine and CLI call sites, mocks, and compatibility checks are updated for
ADAPTER_API_VERSION0.1;0.0adapters fail closed as unsupported. Materialize/journal paths branch on structured results (including rollback when inverse ops fail).First-party claude-code, opencode, and codex adapters switch to the new API (skills as bundles; unsupported
systemscope returnsunsupported-scope). Codex agent deletes now prune empty dirs like the others. Docs and changeset describe migration and release ordering (SDK/adapters before a0.1-only CLI).Reviewed by Cursor Bugbot for commit 3841183. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit