@jini-ai/agent-plugins 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/core/persistent-state.d.ts +25 -0
  3. package/core/persistent-state.js +260 -0
  4. package/dist/lifecycle/activation.js +38 -166
  5. package/dist/lifecycle/bundled-digests.d.ts +6 -0
  6. package/dist/lifecycle/bundled-digests.js +160 -0
  7. package/dist/lifecycle/bundled-source-archive.d.ts +3 -0
  8. package/dist/lifecycle/bundled-source-archive.js +74 -0
  9. package/dist/lifecycle/contribution-loader.d.ts +102 -0
  10. package/dist/lifecycle/contribution-loader.js +92 -0
  11. package/dist/lifecycle/fetch-archive.d.ts +1 -2
  12. package/dist/lifecycle/index.d.ts +24 -4
  13. package/dist/lifecycle/index.js +11 -0
  14. package/dist/lifecycle/install-from-url.d.ts +2 -5
  15. package/dist/lifecycle/install-from-url.js +2 -5
  16. package/dist/lifecycle/install.d.ts +58 -3
  17. package/dist/lifecycle/install.js +301 -23
  18. package/dist/lifecycle/installed-package-roots.d.ts +16 -0
  19. package/dist/lifecycle/installed-package-roots.js +55 -0
  20. package/dist/lifecycle/layout.js +1 -1
  21. package/dist/lifecycle/manifest.d.ts +5 -60
  22. package/dist/lifecycle/manifest.js +12 -20
  23. package/dist/lifecycle/package-paths.d.ts +2 -0
  24. package/dist/lifecycle/package-paths.js +62 -0
  25. package/dist/lifecycle/persistent-state.d.ts +5 -1
  26. package/dist/lifecycle/persistent-state.js +3 -1
  27. package/dist/lifecycle/ports.d.ts +20 -2
  28. package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +13 -1
  29. package/dist/lifecycle/resolve-agent-plugin-refs.js +150 -40
  30. package/dist/lifecycle/retire-bundled.d.ts +50 -1
  31. package/dist/lifecycle/retire-bundled.js +9 -2
  32. package/dist/lifecycle/search.d.ts +65 -0
  33. package/dist/lifecycle/search.js +31 -2
  34. package/dist/lifecycle/seed-bundled.d.ts +18 -1
  35. package/dist/lifecycle/seed-bundled.js +131 -3
  36. package/dist/lifecycle/set-enabled.d.ts +7 -0
  37. package/dist/lifecycle/set-enabled.js +5 -0
  38. package/dist/lifecycle/trusted-plugin-files.d.ts +38 -0
  39. package/dist/lifecycle/trusted-plugin-files.js +18 -0
  40. package/dist/lifecycle/types.d.ts +14 -0
  41. package/dist/lifecycle/uninstall.d.ts +29 -0
  42. package/dist/lifecycle/uninstall.js +204 -4
  43. package/dist/lifecycle/yauzl-archive-reader.d.ts +0 -1
  44. package/dist/manifest.d.ts +3 -0
  45. package/dist/manifest.js +3 -2
  46. package/package.json +22 -8
  47. package/persistent-state.js +2 -260
  48. package/ui-ux-design/plugin.json +15 -1
@@ -13,12 +13,14 @@ import { createResolveAgentPluginRefsModule } from './resolve-agent-plugin-refs.
13
13
  import { createSearchModule } from './search.js';
14
14
  import { createSetEnabledModule } from './set-enabled.js';
15
15
  import { createTrustedPluginFilesModule } from './trusted-plugin-files.js';
16
+ import { createPluginContributionLoader } from './contribution-loader.js';
16
17
  import { createUninstallModule } from './uninstall.js';
17
18
  export * from './ports.js';
18
19
  export * from './layout.js';
19
20
  export * from './manifest.js';
20
21
  export * from './fetch-archive.js';
21
22
  export * from './mcp-provisioning.js';
23
+ export * from './contribution-loader.js';
22
24
  export { createAgentPluginActivations, ACTIVATIONS_FILENAME, ACTIVATIONS_LOCK_FILENAME, AgentPluginActivationsBusyError, AgentPluginActivationsUnreadableError } from './activation.js';
23
25
  export { BUNDLED_DIGESTS_FILENAME } from './bundled-digests.js';
24
26
  export { AgentPluginInstallError } from './install.js';
@@ -46,8 +48,14 @@ export function createAgentPluginLifecycle(required, optional = {}) {
46
48
  const search = createSearchModule(context);
47
49
  const enabled = createSetEnabledModule(context);
48
50
  const trusted = createTrustedPluginFilesModule(context);
51
+ const contributions = createPluginContributionLoader({
52
+ findPackages: input => trusted.findTrustedPluginPackages(input),
53
+ readFile: ({ plugin, filename }) => trusted.readTrustedPluginFile(plugin, filename),
54
+ importModule: ({ plugin, modulePath }) => trusted.importContainedModule(plugin, modulePath),
55
+ });
49
56
  const uninstall = createUninstallModule(context);
50
57
  return {
58
+ ...contributions,
51
59
  fetchAgentPluginArchive: (input, optional = {}) => fetchAgentPluginArchive({ ...input, fetch: context.fetch, outboundGuard: context.outboundGuard }, optional),
52
60
  maxAgentPluginArchiveBytes,
53
61
  seedBundledAgentPlugins: createSeedBundledModule(context).seedBundledAgentPlugins,
@@ -64,6 +72,9 @@ export function createAgentPluginLifecycle(required, optional = {}) {
64
72
  findTrustedPluginPackages: (input, optional = {}) => trusted.findTrustedPluginPackages({ ...input, ...optional }),
65
73
  previewAgentPluginUninstall: uninstall.previewAgentPluginUninstall,
66
74
  uninstallAgentPlugin: uninstall.uninstallAgentPlugin,
75
+ stageForRemoval: (input, _optional = {}) => uninstall.stageForRemoval(input.packagesDir, input.packageRoot),
76
+ restoreStagedTrees: (input, _optional = {}) => uninstall.restoreStagedTrees(input.staged, input.cause),
77
+ removeFrozenPackageTree: (input, _optional = {}) => uninstall.removeFrozenPackageTree(input.root),
67
78
  isAgentPluginActive: (input) => activation.isAgentPluginActive(input.activations, input.pluginId),
68
79
  filterActiveAgentPlugins: (input) => activation.filterActiveAgentPlugins(input.activations, input.items, item => input.pluginIdOf({ item })),
69
80
  readAgentPluginActivations: (input) => activation.readAgentPluginActivations(input.workspaceRoot),
@@ -1,20 +1,17 @@
1
1
  /**
2
- * Historical lifecycle rationale, carried with the implementation during consumer adoption.
3
2
  *
4
- * Host command-line rationale retained when the unused URL-install CLI was removed:
3
+ * Caller integrity constraints:
5
4
  * - A digest-only preview downloads and reports SHA-256 without installing. Hashing those bytes
6
5
  * does not establish provenance; the host must make that distinction visible to the operator.
7
6
  * - A real install requires exactly one explicit integrity decision: an independent pinned digest
8
7
  * or trust-on-first-use. Missing or conflicting choices must be refused before execution, with
9
8
  * diagnostics naming the choice the operator needs to supply.
10
- * - Keep pure token classification separate from argument validation and exit/reporting effects,
11
- * so both parsing and legal combinations are directly testable without a network or process exit.
12
9
  * - The CLI composes shared download/extraction operations; digest verification, expansion bounds,
13
10
  * path containment and tenant isolation belong in those operations rather than a CLI-only guard.
14
11
  * @file The one composition step that turns "here is an address" into "the plugin is on this
15
12
  * machine": {@link fetchAgentPluginArchive} -> `installAgentPlugin`.
16
13
  *
17
- * This is the caller `install.ts` was written for and never had. It adds no extraction, path, or
14
+ * It adds no extraction, path, or
18
15
  * manifest logic of its own — every one of those guarantees still comes from `install.ts`, reached
19
16
  * with bytes instead of a URL.
20
17
  *
@@ -1,20 +1,17 @@
1
1
  /**
2
- * Historical lifecycle rationale, carried with the implementation during consumer adoption.
3
2
  *
4
- * Host command-line rationale retained when the unused URL-install CLI was removed:
3
+ * Caller integrity constraints:
5
4
  * - A digest-only preview downloads and reports SHA-256 without installing. Hashing those bytes
6
5
  * does not establish provenance; the host must make that distinction visible to the operator.
7
6
  * - A real install requires exactly one explicit integrity decision: an independent pinned digest
8
7
  * or trust-on-first-use. Missing or conflicting choices must be refused before execution, with
9
8
  * diagnostics naming the choice the operator needs to supply.
10
- * - Keep pure token classification separate from argument validation and exit/reporting effects,
11
- * so both parsing and legal combinations are directly testable without a network or process exit.
12
9
  * - The CLI composes shared download/extraction operations; digest verification, expansion bounds,
13
10
  * path containment and tenant isolation belong in those operations rather than a CLI-only guard.
14
11
  * @file The one composition step that turns "here is an address" into "the plugin is on this
15
12
  * machine": {@link fetchAgentPluginArchive} -> `installAgentPlugin`.
16
13
  *
17
- * This is the caller `install.ts` was written for and never had. It adds no extraction, path, or
14
+ * It adds no extraction, path, or
18
15
  * manifest logic of its own — every one of those guarantees still comes from `install.ts`, reached
19
16
  * with bytes instead of a URL.
20
17
  *
@@ -2,7 +2,10 @@ import type { AgentPluginLayout } from "./layout.js";
2
2
  import type { AgentPluginLifecyclePorts } from "./ports.js";
3
3
  import type { InstalledAgentPlugin } from "./types.js";
4
4
  export type { InstalledAgentPlugin, InstalledAgentPluginSkill } from "./types.js";
5
- export type AgentPluginInstallErrorCode = "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
5
+ /** One extraction/install failure reason. A caller (an admin route, a future marketplace installer)
6
+ * branches on `code` rather than parsing `message` — this codebase's own convention
7
+ * (`PluginLoadError.reason` in `plugin-runtime/loader.ts` is the direct precedent). */
8
+ export type AgentPluginInstallErrorCode = "PLUGIN_ID_CONFLICT" | "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
6
9
  export declare class AgentPluginInstallError extends Error {
7
10
  readonly code: AgentPluginInstallErrorCode;
8
11
  constructor({ code, message }: {
@@ -12,9 +15,14 @@ export declare class AgentPluginInstallError extends Error {
12
15
  cause?: unknown;
13
16
  });
14
17
  }
18
+ /** One archive entry. A symlink is modeled as its OWN kind rather than folded into "file" — the
19
+ * zip-slip research this module's header cites is explicit that the symlink-entry vector is
20
+ * distinct from the lexical-traversal one, and the two need independent tests and independent
21
+ * handling (see the "symlink entries are rejected outright" rule below). */
15
22
  export type AgentPluginArchiveEntry = {
16
23
  readonly kind: "file";
17
24
  readonly entryPath: string;
25
+ /** The archive format's own claimed size. Never trusted alone — see `DECOMPRESSION_BOMB`. */
18
26
  readonly declaredSize?: (number) | undefined;
19
27
  readonly executable?: (boolean) | undefined;
20
28
  readonly openReadStream: (required: Record<string, never>) => AsyncIterable<Uint8Array>;
@@ -30,6 +38,7 @@ export type AgentPluginArchiveEntry = {
30
38
  * `entries` must be a pure function of the archive bytes: an install iterates it twice, once to
31
39
  * check entry-count limits from metadata alone and once to extract.
32
40
  */
41
+ /** `entries` is a pure function of archive bytes: metadata preflight and extraction iterate it twice. */
33
42
  export interface AgentPluginArchiveReaderPort {
34
43
  entries(required: {
35
44
  readonly archive: Uint8Array;
@@ -37,16 +46,62 @@ export interface AgentPluginArchiveReaderPort {
37
46
  }
38
47
  export interface InstallAgentPluginRequired {
39
48
  readonly archive: Uint8Array;
49
+ /** Lowercase hex SHA-256 the caller expects `archive` to hash to — a real marketplace flow would
50
+ * supply this from the server's own metadata; verified here, never trusted from the archive
51
+ * itself. */
40
52
  readonly expectedSha256: string;
41
53
  readonly archiveReader: AgentPluginArchiveReaderPort;
54
+ /**
55
+ * SECURITY (2026-08-13, security pass Finding 2 — tenant isolation, `ADS-memory/reports/security/
56
+ * 2026-08-13-post-session-security-pass.md`): this field used to be an already-resolved
57
+ * `AgentPluginWorkspaceLayout` (`{ root, packages, staging, pluginDataDir }`) — a plain interface
58
+ * of four independently-typed string/function fields with no tag identifying which workspace it
59
+ * came from. Nothing stopped a caller from hand-assembling one from two DIFFERENT real
60
+ * `forWorkspace()` results (wrong variable capture, a stale cached layout, a future code path that
61
+ * builds one by hand instead of calling `forWorkspace`) — such a value type-checked identically to
62
+ * a correctly-resolved one, and `installAgentPlugin` published into whatever `packages`/`staging`
63
+ * it was handed with no consistency check. Proven at the unit level in `install.unit.test.ts`
64
+ * (a layout stitching workspace A's `root`/`pluginDataDir` onto workspace B's real `packages`/
65
+ * `staging` published into workspace B's real, on-disk store with no error).
66
+ *
67
+ * The fix: `installAgentPlugin` no longer accepts a pre-resolved workspace layout as input AT ALL.
68
+ * It takes the RAW MATERIALS instead — the INSTANCE-level layout (`resolveAgentPluginLayout()`,
69
+ * carries no workspace-scoped path of its own) plus one `workspaceId` — and calls
70
+ * `layout.forWorkspace(workspaceId)` itself, exactly once, internally. There is no longer any
71
+ * `AgentPluginWorkspaceLayout`-shaped parameter here for a caller to stitch fields into: the one
72
+ * and only path from these two inputs to `packages`/`staging`/`pluginDataDir` is the real
73
+ * `forWorkspace()` closure, called atomically, so the four resulting paths can never disagree with
74
+ * each other about which workspace they belong to. Passing a hand-built `{ packages, staging, ... }`
75
+ * literal where this field is expected is now a compile-time type error, not a runtime hazard.
76
+ *
77
+ * `workspaceId` must still come from the authenticated principal's own request context, never
78
+ * derived from `layout` itself (a hand-built or malicious `AgentPluginLayout` could fake a
79
+ * `forWorkspace` implementation identically) — that half of the guarantee is a caller obligation
80
+ * this type cannot enforce, the same way `archiveReader` above is a fully caller-trusted port. What
81
+ * IS closed is the specific, demonstrated bug class: stitching or hand-assembling an
82
+ * already-resolved workspace layout from parts.
83
+ */
42
84
  readonly layout: AgentPluginLayout;
85
+ /** The workspace this install is for. Combined with {@link layout} via `layout.forWorkspace(workspaceId)`
86
+ * — see the SECURITY note on {@link layout} above. */
43
87
  readonly workspaceId: string;
44
88
  }
45
- export type InstallAgentPluginOptional = {};
89
+ export interface InstallAgentPluginOptional {
90
+ /** Full bounded extraction/validation in staging, with no package or memory publication. */
91
+ readonly previewOnly?: boolean;
92
+ /** Host policy transaction inside the per-plugin lock after validation. Call publish exactly
93
+ * once to install; returning an existing package without calling it is a deduplicated no-op. */
94
+ /** Host policy transaction, called inside the existing per-plugin lock after validation.
95
+ * It must invoke publish exactly once to install. Shared lifecycle counterpart is in chat-tools.jini.patch. */
96
+ readonly publishGuard?: (required: {
97
+ readonly plugin: InstalledAgentPlugin;
98
+ readonly publish: () => Promise<InstalledAgentPlugin>;
99
+ }, optional: Record<string, never>) => Promise<InstalledAgentPlugin>;
100
+ }
46
101
  /** Internal composition; one state/queue instance per injected host context. */
47
102
  export declare function createInstallModule(ports: AgentPluginLifecyclePorts): {
48
103
  maxAgentPluginInstallArchiveBytes: () => number;
49
- installAgentPlugin: (required: InstallAgentPluginRequired, _optional?: InstallAgentPluginOptional) => Promise<InstalledAgentPlugin>;
104
+ installAgentPlugin: (required: InstallAgentPluginRequired, optional?: InstallAgentPluginOptional) => Promise<InstalledAgentPlugin>;
50
105
  indexInstalledRoot: (packageRoot: string, archiveDigest: string) => Promise<InstalledAgentPlugin>;
51
106
  };
52
107
  //# sourceMappingURL=install.d.ts.map