Skip to content

Commit 2a129e9

Browse files
refactor(install): delete legacy receipt and state readers (#841)
* refactor(install): delete legacy receipt and state readers * chore: link changeset to #841 * chore: drop the pre-receipt uninstall sentence from the #818 changeset
1 parent ccccd79 commit 2a129e9

28 files changed

Lines changed: 811 additions & 1366 deletions

‎.changeset/foreign-destination-inventory.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"agent-bundle": patch
33
---
44

5-
Refuse a foreign destination and a same-version marketplace restage from `install.mjs` when the destination lacks paths listed in `agent-bundle.manifest.json`, instead of crashing with `ENOENT`. `uninstall --force` on a pre-receipt copy removes the files present in that copy, matching the framework CLI. (#818)
5+
Refuse a foreign destination and a same-version marketplace restage from `install.mjs` when the destination lacks paths listed in `agent-bundle.manifest.json`, instead of crashing with `ENOENT`. (#818)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"agent-bundle": minor
3+
---
4+
5+
Remove the legacy install readers from `install`, `uninstall`, `doctor`, and the emitted `install.mjs`: format-1 receipts (`agent-bundle-install-receipt/1`), receipt-less "legacy" adoption of a pre-receipt Cursor copy, the in-tree `<plugin root>/state` handling and its `--purge-data` removal, the compatibility receipt `stateRoot` field, and the `AB7317` unsupported-runtime report. A Cursor directory without a format-2 receipt naming the plugin is foreign: `install` refuses it with `AB7005` (with or without `--replace`), `uninstall` refuses it with `AB7007` (with or without `--force`), and Doctor reports it as `AB7321`; remove such a directory by hand and reinstall. An in-tree `state/` is an ordinary unowned entry that `uninstall` retains and lists, never purges. A runtime that rejects the status probe is a failed probe (`AB7318`). `AB7317`, `AB7329`, and `AB7332` are retired. (#841)

‎docs/diagnostics.md‎

Lines changed: 68 additions & 67 deletions
Large diffs are not rendered by default.

‎docs/framework-mode.md‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -622,11 +622,11 @@ manifest. A root whose selection includes `cursor` or `portable` also
622622
includes a standalone `install.mjs`. Its staged copy is idempotent for identical
623623
content, records an install receipt (`.agent-bundle-install.json`: plugin,
624624
version, content hash, owned files and directories), replaces a same-version stale copy of its
625-
own plugin in place (owned files only; legacy `state/` survives, while current builds keep
626-
framework state outside the plugin root), and accepts
627-
`--replace` to replace a different installed version or adopt
628-
a pre-receipt copy. Foreign directories are refused with a content-hash
629-
comparison. It never invokes sudo or changes PATH. `agent-bundle install <host>
625+
own plugin in place (owned files only; unowned entries survive, and framework
626+
state lives outside the plugin root), and accepts
627+
`--replace` to replace a different installed version. A directory without a
628+
receipt naming this plugin, a copy placed before receipts existed included, is
629+
foreign and refused with a content-hash comparison; remove it by hand. It never invokes sudo or changes PATH. `agent-bundle install <host>
630630
[--replace]` applies the same policy for every host, and `agent-bundle doctor
631631
--from` reports the installed copy versus the artifact as `current`, `stale`,
632632
`version-mismatch`, `foreign`, or `not-installed` (see the package README's
@@ -644,8 +644,7 @@ host root, the host `registrations` it performed in order, and `installedAt` /
644644
`updatedAt`. Cursor local copies carry it in-tree; Claude, Codex, and Cursor
645645
marketplace-mode installs keep theirs in `<host root>/agent-bundle/receipts/`.
646646
`install --replace`, `uninstall`, and `doctor` all consume the same document;
647-
a receipt written before #101 is read with its lifecycle fields synthesized and
648-
diagnosed (`AB7329`), never rejected.
647+
only format 2 is read, and a copy carrying an older receipt is foreign.
649648

650649
```sh
651650
agent-bundle uninstall claude --from artifact --plan # exact paths and host verbs, no writer
@@ -657,13 +656,15 @@ node artifact/install.mjs --uninstall [--plan] [--mode marketplace]
657656

658657
Uninstall removes exactly what the receipt owns and reverses exactly the
659658
registrations it recorded; anything else stays and is listed as retained.
660-
Legacy durable runtime state (`state/`) is kept unless `--purge-data --confirm-purge`
661-
(current builds keep framework state outside the plugin root);
659+
Durable runtime state (the framework state roots the receipt records with
660+
ownership evidence) is kept unless `--purge-data --confirm-purge`; an unowned
661+
`state/` directory beside the plugin is retained and listed, never purged;
662662
the typed `data.outcome` says what the host itself decided where Agent Bundle
663663
cannot (`retained-by-host` for Claude's ~14-day orphaned copy,
664-
`removed-by-host` / `unavailable` for Codex, which has no keep-data option). A
665-
missing receipt or an owned-content mismatch is refused (`AB7009`, `AB7007`)
666-
unless `--force`; a receipt or manifest naming another plugin is refused
664+
`removed-by-host` for Codex, which has no keep-data option). A missing store
665+
receipt for a host-registered install or an owned-content mismatch is refused
666+
(`AB7009`, `AB7007`) unless `--force`; a Cursor local directory without a
667+
receipt, or a receipt or manifest naming another plugin, is foreign and refused
667668
regardless; `--purge-data` without `--confirm-purge` is `AB7008`; a second run is
668669
a `not-installed` no-op. `doctor --from` adds the lifecycle stage per host,
669670
placed → registered → enabled → active, each observed or typed `unavailable`

‎packages/agent-bundle/README.md‎

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ manifests at files inside those payloads without compiling them. Payload files c
136136
| `agent-bundle build` | Build a validated artifact from source, plus the declared `dist/` package build. |
137137
| `agent-bundle prepack` | Run the release build, dry-run npm packing without scripts, and verify packaged outputs, artifact hashes, bins, and versions (`--output` and `--json` supported). |
138138
| `agent-bundle install <host>` | Install a built bundle into Claude, Codex, or Cursor (`--from`, `--scope`, `--replace`, `--mode local\|marketplace` for Cursor, and `--json` supported). Same-version content drift of an agent-bundle-managed install is replaced automatically; identical reruns are a no-op. |
139-
| `agent-bundle uninstall <host>` | Remove a receipt-owned install and nothing else: the receipt's files and directories, the host registrations it recorded (`claude plugin uninstall --keep-data` + `marketplace remove`, `codex plugin remove` + `marketplace remove`, the Cursor local directory or staged marketplace). `--plan` prints the exact paths without changing anything; the effective framework state root, web-data, and legacy `state/` are kept unless `--purge-data --confirm-purge`; a missing receipt or content mismatch is refused unless `--force`; a rerun is `not-installed`. |
140-
| `agent-bundle doctor` | Read-only host inspection: host probes, installed inventory, effective and legacy state roots with existence and writability, store receipts cross-checked against the host, and, with `--from`, the installed copy compared against the built artifact by version and content hash (`current`, `stale`, `version-mismatch`, `foreign`, `not-installed`) plus the lifecycle stage (placed → registered → enabled → active, unobservable stages typed `unavailable`). |
139+
| `agent-bundle uninstall <host>` | Remove a receipt-owned install and nothing else: the receipt's files and directories, the host registrations it recorded (`claude plugin uninstall --keep-data` + `marketplace remove`, `codex plugin remove` + `marketplace remove`, the Cursor local directory or staged marketplace). `--plan` prints the exact paths without changing anything; the receipt-recorded framework state roots and web-data are kept unless `--purge-data --confirm-purge`; a missing store receipt or content mismatch is refused unless `--force`, and a Cursor directory without a receipt is foreign and refused regardless; a rerun is `not-installed`. |
140+
| `agent-bundle doctor` | Read-only host inspection: host probes, installed inventory, effective state roots with existence and writability, store receipts cross-checked against the host, and, with `--from`, the installed copy compared against the built artifact by version and content hash (`current`, `stale`, `version-mismatch`, `foreign`, `not-installed`) plus the lifecycle stage (placed → registered → enabled → active, unobservable stages typed `unavailable`). |
141141
| `agent-bundle validate` | Validate project source, or an artifact with `--artifact`. |
142142
| `agent-bundle inspect` | Inspect the normalized model and each selected host projection's plan from source, with per-host component accounting: which skills, commands, rules, hooks, MCP surfaces, and scripts each host emits and, for every omission, whether the author excluded it or the host's pinned capability judgment (`degraded`/`unavailable`/`prohibited`, with reason) ruled it out. |
143143
| `agent-bundle inspect --bundler` | Dump the lowered Rspack config (post-`tools`-hatch merge, as Rslib/Rsbuild hand it to the compiler) for every generated output. |
@@ -262,7 +262,7 @@ every host treats it differently. `agent-bundle install` and the emitted
262262
hash differs (a stale copy), install replaces it without a flag. Cursor
263263
replacement is in place and touches owned files only: stale owned files are
264264
removed, new files are renamed over their predecessors, and unowned entries
265-
such as workspace-durable `state/` stores survive; if a rebuilt artifact
265+
such as a `state/` directory beside the plugin survive; if a rebuilt artifact
266266
introduces a path an existing unowned file already occupies, replacement
267267
aborts before any change and names it. Claude replacement runs
268268
`claude plugin uninstall <plugin>@<marketplace> --scope <scope> --keep-data`
@@ -271,14 +271,9 @@ every host treats it differently. `agent-bundle install` and the emitted
271271
cache stays stale. Codex replacement runs `codex plugin remove` before
272272
`marketplace add` + `add`, so files a rebuild removed do not linger.
273273
- **`--replace`.** Also replaces an agent-bundle install of
274-
the same plugin at a *different* version, and adopts a Cursor copy that was
275-
installed before receipts existed (recognised by its emitted `INSTALL.md` +
276-
`install.mjs` and matching manifest name). A legacy copy has no owned-file
277-
inventory, so adoption rewrites the files the new artifact ships and leaves
278-
every other file in place (operator files, files an earlier rebuild dropped,
279-
`state/`); those leftovers stay unowned under the new receipt, and later
280-
same-version rebuilds replace automatically. A byte-identical legacy copy
281-
under `--replace` reports `adopted` and changes no plugin file.
274+
the same plugin at a *different* version. It does not apply to a directory
275+
without a receipt naming this plugin: a Cursor copy placed before receipts
276+
existed is foreign, byte-identical or not, and is removed by hand.
282277
- **Foreign installs are always refused.** A directory under the plugin name
283278
that is not an agent-bundle install of this plugin fails with `AB7005` and a
284279
content-hash comparison (`installed <name>@<version> content <hash> vs
@@ -324,8 +319,8 @@ receipt and remove exactly what it owns:
324319
install itself created. Unowned entries are retained and listed; when the
325320
root survives, a remnant receipt (owning no files) keeps the created host
326321
directories accountable for a later purge and lets Doctor explain the
327-
directory. Reinstalling around preserved state is an `installed`, not a
328-
foreign refusal.
322+
directory. Reinstalling into a root that still carries that remnant receipt
323+
is an `installed`, not a foreign refusal; without it the root is foreign.
329324
- Cursor marketplace: the staged repository after its `HEAD` matches the
330325
recorded commit, plus the receipt; a copy Cursor imported is Cursor-owned and
331326
reported `manual` with the Customize step.
@@ -338,27 +333,29 @@ receipt and remove exactly what it owns:
338333
host no longer holds is `already-absent`, so an orphaned receipt is consumed
339334
without running a host verb.
340335

341-
Durable runtime state (`state/`: state kernel, notices journal; for a Cursor
342-
copy of an Agent Plugins pack, also the `PLUGIN_DATA` directory the receipt
343-
records), effective framework state, and web-data are kept by default;
344-
`--purge-data --confirm-purge` removes them (`AB7008` without the confirmation).
345-
The typed `data.outcome` is honest per host: Cursor `kept` / `purged` / `absent`;
346-
Claude `retained-by-host` (Claude 2.1.257 orphans the cached copy for its ~14-day
347-
grace period; a purge also removes external framework state, web-data, `state/`,
348-
and `plugins/data/<id>/`); Codex reports external state as `kept` / `purged`,
349-
while in-tree `state/` is removed by the host and cannot be kept (codex-cli
350-
0.147.0 has no keep-data option).
351-
An older receipt that records no state location never makes a root derived
352-
from the current environment or home purgeable; it is reported unproven and
353-
retained, including after a keep-data cycle.
336+
Durable runtime state (the framework state roots the receipt's `state` block
337+
records with ownership evidence, web-data, and for a Cursor copy of an Agent
338+
Plugins pack the `PLUGIN_DATA` directory the receipt records) is kept by
339+
default; `--purge-data --confirm-purge` removes the roots whose ownership is
340+
provable (`AB7008` without the confirmation). A `state/` directory beside the
341+
plugin is an unowned entry: retained and listed, never purged. The typed
342+
`data.outcome` is honest per host: Cursor `kept` / `purged` / `absent`; Claude
343+
`retained-by-host` (Claude 2.1.257 orphans the cached copy for its ~14-day
344+
grace period; a purge also removes the owned framework state, web-data, and
345+
`plugins/data/<id>/`); Codex reports external state as `kept` / `purged`, while
346+
the cached tree is removed by the host (codex-cli 0.147.0 has no keep-data
347+
option). A receipt with no `state` block never makes a root derived from the
348+
current environment or home purgeable; it is reported unproven and retained.
354349
`--plan` reports the same exact paths and host verbs without opening a writer.
355-
A missing receipt (`AB7009`) or an owned-content, version, or `HEAD` mismatch
356-
(`AB7007`) is refused unless `--force`; a receipt or manifest naming another
357-
plugin is refused regardless; a second run is a `not-installed` no-op.
350+
A missing store receipt for a host-registered install (`AB7009`) or an
351+
owned-content, version, or `HEAD` mismatch (`AB7007`) is refused unless
352+
`--force`; a Cursor directory without a receipt, or a receipt or manifest
353+
naming another plugin, is foreign and refused regardless; a second run is a
354+
`not-installed` no-op.
358355

359356
`agent-bundle doctor` inventories the receipt store per host and flags receipts
360-
the host no longer honours (`AB7328`), reports receipts that predate format 2 as
361-
migrated (`AB7329`; an identical `install` rerun rewrites them), and with
357+
the host no longer honours (`AB7328`), treats a receipt that is not format 2 as
358+
absent (the copy is then foreign, `AB7321`), and with
362359
`--from` reports the lifecycle stage per host (`AB7330`): placed → registered →
363360
enabled → active, each observed from `plugin list --json` (Claude/Codex
364361
`enabled` flags), the Cursor local directory, or the Cursor marketplace import

‎packages/agent-bundle/src/contracts/discovery.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export type DiscoveryRuntimeStatus =
6464
readonly startedAt?: string;
6565
readonly status: 'available';
6666
}>
67-
| Readonly<{ readonly status: 'failed' | 'unavailable' | 'unsupported' }>;
67+
| Readonly<{ readonly status: 'failed' | 'unavailable' }>;
6868

6969
export interface DiscoveryMcpServer {
7070
readonly name: string;

‎packages/agent-bundle/src/events/ipc.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export type RequestEventRuntimeStatusOptions = Readonly<{
208208

209209
export type EventRuntimeStatusResult =
210210
| Readonly<EventRuntimeStatus & { readonly status: 'available' }>
211-
| Readonly<{ readonly status: 'unavailable' | 'unsupported' }>;
211+
| Readonly<{ readonly status: 'unavailable' }>;
212212

213213
export const eventRuntimeEndpoint = (endpointId: string): string => {
214214
const hash = createHash('sha256').update(endpointId, 'utf8').digest('hex').slice(0, 32);
@@ -1195,7 +1195,9 @@ const statusProgram = (
11951195
'Event runtime status response does not match the wire schema.',
11961196
));
11971197
}
1198-
if (response.data.status === 'error') return Object.freeze({ status: 'unsupported' as const });
1198+
if (response.data.status === 'error') {
1199+
return yield* Effect.fail(transportError('runtime-failed', response.data.message));
1200+
}
11991201
return Object.freeze({
12001202
...response.data.runtime,
12011203
status: 'available' as const,

‎packages/agent-bundle/src/install/commands.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,13 @@ export const registerLifecycleCommands = (program: Command, options: LifecycleCo
129129
)
130130
.option('--scope <scope>', 'Host install scope', installScope, 'user')
131131
.option('--mode <mode>', 'Cursor delivery mode to uninstall: local (default) or marketplace', installMode)
132-
.option('--keep-data', 'Keep the plugin\'s durable runtime state (state/) in place; this is the default')
133-
.option('--purge-data', 'Also remove the plugin\'s durable runtime state; requires --confirm-purge')
132+
.option('--keep-data', 'Keep the plugin\'s durable runtime state in place; this is the default')
133+
.option('--purge-data', 'Also remove the receipt-owned durable runtime state; requires --confirm-purge')
134134
.option('--confirm-purge', 'Confirm that --purge-data may delete durable state')
135135
.option(
136136
'--force',
137-
'Proceed without an install receipt (legacy or host-only install) or when owned content no longer matches the receipt; ' +
138-
'foreign directories are still refused',
137+
'Proceed when a host-only install has no store receipt or when owned content no longer matches the receipt; ' +
138+
'directories without a receipt naming this plugin are still refused',
139139
)
140140
.option('--plan', 'Print the exact paths and host registrations that would be removed without changing anything')
141141
.option('--json', 'Write one machine-readable JSON document');

0 commit comments

Comments
 (0)