@jini-ai/agent-plugins 0.4.6 → 0.4.8

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 (152) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +48 -0
  3. package/dist/admin-surfaces/action-dispatcher.d.ts +37 -0
  4. package/dist/admin-surfaces/action-dispatcher.js +69 -0
  5. package/dist/admin-surfaces/action-schema.d.ts +56 -0
  6. package/dist/admin-surfaces/action-schema.js +167 -0
  7. package/dist/admin-surfaces/action-tools.d.ts +40 -0
  8. package/dist/admin-surfaces/action-tools.js +52 -0
  9. package/dist/admin-surfaces/callback-field-inbox.d.ts +35 -0
  10. package/dist/admin-surfaces/callback-field-inbox.js +53 -0
  11. package/dist/admin-surfaces/capability-declarations.d.ts +41 -0
  12. package/dist/admin-surfaces/capability-declarations.js +102 -0
  13. package/dist/admin-surfaces/contribution.d.ts +49 -0
  14. package/dist/admin-surfaces/contribution.js +30 -0
  15. package/dist/admin-surfaces/delivery-bindings.d.ts +76 -0
  16. package/dist/admin-surfaces/delivery-bindings.js +99 -0
  17. package/dist/admin-surfaces/descriptor.d.ts +69 -0
  18. package/dist/admin-surfaces/descriptor.js +196 -0
  19. package/dist/admin-surfaces/external-links.d.ts +45 -0
  20. package/dist/admin-surfaces/external-links.js +70 -0
  21. package/dist/admin-surfaces/host-validator.d.ts +62 -0
  22. package/dist/admin-surfaces/host-validator.js +87 -0
  23. package/dist/admin-surfaces/index.d.ts +23 -0
  24. package/dist/admin-surfaces/index.js +21 -0
  25. package/dist/admin-surfaces/protocol.d.ts +103 -0
  26. package/dist/admin-surfaces/protocol.js +25 -0
  27. package/dist/admin-surfaces/sandbox.d.ts +78 -0
  28. package/dist/admin-surfaces/sandbox.js +92 -0
  29. package/dist/admin-surfaces/settings-value.d.ts +28 -0
  30. package/dist/admin-surfaces/settings-value.js +45 -0
  31. package/dist/admin-surfaces/surface-capabilities.d.ts +167 -0
  32. package/dist/admin-surfaces/surface-capabilities.js +215 -0
  33. package/dist/admin-surfaces/surface-client.d.ts +116 -0
  34. package/dist/admin-surfaces/surface-client.js +219 -0
  35. package/dist/admin-surfaces/surface-handlers.d.ts +111 -0
  36. package/dist/admin-surfaces/surface-handlers.js +172 -0
  37. package/dist/admin-surfaces/theme.d.ts +59 -0
  38. package/dist/admin-surfaces/theme.js +57 -0
  39. package/dist/folder-zip-node.d.ts +11 -0
  40. package/dist/folder-zip-node.js +84 -0
  41. package/dist/folder-zip.d.ts +38 -0
  42. package/dist/folder-zip.js +134 -0
  43. package/dist/lifecycle/automatic-use.d.ts +79 -0
  44. package/dist/lifecycle/automatic-use.js +165 -0
  45. package/dist/lifecycle/compatibility-metadata.d.ts +19 -0
  46. package/dist/lifecycle/compatibility-metadata.js +45 -0
  47. package/dist/lifecycle/compatibility.d.ts +52 -0
  48. package/dist/lifecycle/compatibility.js +60 -0
  49. package/dist/lifecycle/fetch-archive.js +26 -24
  50. package/dist/lifecycle/index.d.ts +14 -2
  51. package/dist/lifecycle/index.js +8 -1
  52. package/dist/lifecycle/install-errors.d.ts +14 -0
  53. package/dist/lifecycle/install-errors.js +9 -0
  54. package/dist/lifecycle/install.d.ts +2 -13
  55. package/dist/lifecycle/install.js +12 -123
  56. package/dist/lifecycle/installed-package-index.d.ts +15 -0
  57. package/dist/lifecycle/installed-package-index.js +128 -0
  58. package/dist/lifecycle/installed-package-roots.js +28 -20
  59. package/dist/lifecycle/manifest.d.ts +14 -0
  60. package/dist/lifecycle/manifest.js +115 -100
  61. package/dist/lifecycle/package-paths.js +3 -3
  62. package/dist/lifecycle/ports.d.ts +1 -1
  63. package/dist/lifecycle/provider-contribution.d.ts +65 -0
  64. package/dist/lifecycle/provider-contribution.js +93 -0
  65. package/dist/lifecycle/resolve-agent-plugin-refs.js +19 -6
  66. package/dist/lifecycle/retire-bundled.js +20 -18
  67. package/dist/lifecycle/search.d.ts +6 -1
  68. package/dist/lifecycle/search.js +45 -7
  69. package/dist/lifecycle/set-enabled.d.ts +2 -1
  70. package/dist/lifecycle/set-enabled.js +52 -8
  71. package/dist/lifecycle/skill-file-reader.js +85 -71
  72. package/dist/lifecycle/tools/capabilities.d.ts +199 -0
  73. package/dist/lifecycle/tools/capabilities.js +213 -0
  74. package/dist/lifecycle/tools/definition.d.ts +32 -0
  75. package/dist/lifecycle/tools/definition.js +33 -0
  76. package/dist/lifecycle/tools/index.d.ts +10 -0
  77. package/dist/lifecycle/tools/index.js +10 -0
  78. package/dist/lifecycle/tools/manifest.d.ts +33 -0
  79. package/dist/lifecycle/tools/manifest.js +89 -0
  80. package/dist/lifecycle/tools/projection.d.ts +90 -0
  81. package/dist/lifecycle/tools/projection.js +283 -0
  82. package/dist/lifecycle/types.d.ts +2 -0
  83. package/dist/lifecycle/uninstall.d.ts +3 -1
  84. package/dist/lifecycle/uninstall.js +51 -27
  85. package/dist/lifecycle/update-plan.d.ts +18 -0
  86. package/dist/lifecycle/update-plan.js +22 -0
  87. package/dist/lifecycle/update-store.d.ts +71 -0
  88. package/dist/lifecycle/update-store.js +150 -0
  89. package/dist/lifecycle/update.d.ts +55 -0
  90. package/dist/lifecycle/update.js +321 -0
  91. package/dist/manifest.d.ts +2 -1
  92. package/dist/manifest.js +2 -1
  93. package/package.json +50 -6
  94. package/ui-ux-design/plugin.json +8 -2
  95. package/dist/index.d.ts.map +0 -1
  96. package/dist/index.js.map +0 -1
  97. package/dist/lifecycle/activation.d.ts.map +0 -1
  98. package/dist/lifecycle/activation.js.map +0 -1
  99. package/dist/lifecycle/bundled-digests.d.ts.map +0 -1
  100. package/dist/lifecycle/bundled-digests.js.map +0 -1
  101. package/dist/lifecycle/bundled-source-archive.d.ts.map +0 -1
  102. package/dist/lifecycle/bundled-source-archive.js.map +0 -1
  103. package/dist/lifecycle/capability-projection.d.ts.map +0 -1
  104. package/dist/lifecycle/capability-projection.js.map +0 -1
  105. package/dist/lifecycle/contribution-loader.d.ts.map +0 -1
  106. package/dist/lifecycle/contribution-loader.js.map +0 -1
  107. package/dist/lifecycle/fetch-archive.d.ts.map +0 -1
  108. package/dist/lifecycle/fetch-archive.js.map +0 -1
  109. package/dist/lifecycle/index.d.ts.map +0 -1
  110. package/dist/lifecycle/index.js.map +0 -1
  111. package/dist/lifecycle/install-from-url.d.ts.map +0 -1
  112. package/dist/lifecycle/install-from-url.js.map +0 -1
  113. package/dist/lifecycle/install.d.ts.map +0 -1
  114. package/dist/lifecycle/install.js.map +0 -1
  115. package/dist/lifecycle/installed-package-roots.d.ts.map +0 -1
  116. package/dist/lifecycle/installed-package-roots.js.map +0 -1
  117. package/dist/lifecycle/layout.d.ts.map +0 -1
  118. package/dist/lifecycle/layout.js.map +0 -1
  119. package/dist/lifecycle/manifest.d.ts.map +0 -1
  120. package/dist/lifecycle/manifest.js.map +0 -1
  121. package/dist/lifecycle/mcp-provisioning.d.ts.map +0 -1
  122. package/dist/lifecycle/mcp-provisioning.js.map +0 -1
  123. package/dist/lifecycle/node.d.ts.map +0 -1
  124. package/dist/lifecycle/node.js.map +0 -1
  125. package/dist/lifecycle/package-paths.d.ts.map +0 -1
  126. package/dist/lifecycle/package-paths.js.map +0 -1
  127. package/dist/lifecycle/persistent-state.d.ts.map +0 -1
  128. package/dist/lifecycle/persistent-state.js.map +0 -1
  129. package/dist/lifecycle/ports.d.ts.map +0 -1
  130. package/dist/lifecycle/ports.js.map +0 -1
  131. package/dist/lifecycle/resolve-agent-plugin-refs.d.ts.map +0 -1
  132. package/dist/lifecycle/resolve-agent-plugin-refs.js.map +0 -1
  133. package/dist/lifecycle/retire-bundled.d.ts.map +0 -1
  134. package/dist/lifecycle/retire-bundled.js.map +0 -1
  135. package/dist/lifecycle/search.d.ts.map +0 -1
  136. package/dist/lifecycle/search.js.map +0 -1
  137. package/dist/lifecycle/seed-bundled.d.ts.map +0 -1
  138. package/dist/lifecycle/seed-bundled.js.map +0 -1
  139. package/dist/lifecycle/set-enabled.d.ts.map +0 -1
  140. package/dist/lifecycle/set-enabled.js.map +0 -1
  141. package/dist/lifecycle/skill-file-reader.d.ts.map +0 -1
  142. package/dist/lifecycle/skill-file-reader.js.map +0 -1
  143. package/dist/lifecycle/trusted-plugin-files.d.ts.map +0 -1
  144. package/dist/lifecycle/trusted-plugin-files.js.map +0 -1
  145. package/dist/lifecycle/types.d.ts.map +0 -1
  146. package/dist/lifecycle/types.js.map +0 -1
  147. package/dist/lifecycle/uninstall.d.ts.map +0 -1
  148. package/dist/lifecycle/uninstall.js.map +0 -1
  149. package/dist/lifecycle/yauzl-archive-reader.d.ts.map +0 -1
  150. package/dist/lifecycle/yauzl-archive-reader.js.map +0 -1
  151. package/dist/manifest.d.ts.map +0 -1
  152. package/dist/manifest.js.map +0 -1
@@ -112,6 +112,7 @@ import path from "node:path";
112
112
  import { createPackagePathsModule } from "./package-paths.js";
113
113
  import { createActivationModule } from "./activation.js";
114
114
  import { createResolveAgentPluginRefsModule } from "./resolve-agent-plugin-refs.js";
115
+ import { createAgentPluginUpdateStore, settleAgentPluginUpdateJournal } from "./update-store.js";
115
116
  /** The plugin id has no installed package on disk for this workspace. */
116
117
  export class AgentPluginNotFoundError extends Error {
117
118
  constructor({ message }, optional = {}) { super(message, optional); }
@@ -174,50 +175,71 @@ function buildModule(ports) {
174
175
  return createPersistentStateModule(ports).withPluginLock({ workspaceRoot, pluginId: required.pluginId,
175
176
  run: () => uninstallAgentPluginUnlocked(required, optional) });
176
177
  }
177
- // Reversible work first — see this file's header, "Why removal stages first". Nothing on this
178
- // side of the try is destructive: every step up to and including the activation-record delete can
179
- // be undone by renaming the staged trees back where they came from.
180
- // Past this point the live state is already settled: the packages are no longer reachable under
181
- // any installed-digest name and the activation record is gone. These removes are the only
182
- // irreversible step, and nothing observable depends on them finishing.
183
- // Remove only empty package ancestors: a concurrently-installed digest must never be deleted.
184
- // No package may have appeared since this uninstall's preview. Preserve it on a race.
185
178
  async function uninstallAgentPluginUnlocked(required, optional = {}) {
186
179
  const { workspaceRoot, packagesDir, matches } = await resolveUninstallTargets(required, optional.retiredBundled === true);
187
180
  assertUnchangedSincePreview(required.pluginId, optional.confirmedPreview, matches);
188
- if (optional.deleteMemory && (!optional.confirmedPreview || optional.confirmedPreview.pluginId !== required.pluginId)) {
181
+ assertMemoryDeletionConfirmed(required, optional);
182
+ // An unfinished update journal would make boot recovery resurrect the old revision over this
183
+ // uninstall, so it refuses (PLUGIN_UPDATE_PENDING) until a boot recovers it. The store resolves
184
+ // paths from this call's own layout, like every other path here.
185
+ const updates = createAgentPluginUpdateStore({ ports: { ...ports, layout: required.layout }, workspaceId: required.workspaceId, pluginId: required.pluginId });
186
+ await settleAgentPluginUpdateJournal({ store: updates });
187
+ const staged = await stageAndDeleteActivation({ workspaceRoot, packagesDir, pluginId: required.pluginId, matches });
188
+ // Past this point the live state is already settled: the packages are no longer reachable under
189
+ // any installed-digest name and the activation record is gone. These removes are the only
190
+ // irreversible step, and nothing observable depends on them finishing.
191
+ for (const tree of staged) {
192
+ await removeFrozenPackageTree(tree.quarantined);
193
+ }
194
+ await removeEmptyPackageAncestors({ packagesDir });
195
+ if (optional.deleteMemory)
196
+ await deletePersistentMemory({ ...required, workspaceRoot });
197
+ return { pluginId: required.pluginId, removedDigests: matches.map((plugin) => plugin.archiveDigest) };
198
+ }
199
+ function assertMemoryDeletionConfirmed(required, optional) {
200
+ if (optional.deleteMemory && optional.confirmedPreview?.pluginId !== required.pluginId) {
189
201
  throw new Error('Deleting plugin memory requires a confirmed preview for this plugin');
190
202
  }
203
+ }
204
+ /** Reversible work first — see this file's header, "Why removal stages first". Nothing in here is
205
+ * destructive: every step up to and including the activation-record delete can be undone by
206
+ * renaming the staged trees back where they came from. */
207
+ async function stageAndDeleteActivation(required) {
191
208
  const staged = [];
192
209
  try {
193
- for (const plugin of matches) {
194
- staged.push(await stageForRemoval(packagesDir, plugin.packageRoot));
210
+ for (const plugin of required.matches) {
211
+ staged.push(await stageForRemoval(required.packagesDir, plugin.packageRoot));
195
212
  }
196
- await deleteAgentPluginActivation({ workspaceRoot, pluginId: required.pluginId });
213
+ await deleteAgentPluginActivation({ workspaceRoot: required.workspaceRoot, pluginId: required.pluginId });
197
214
  }
198
215
  catch (error) {
199
216
  throw await restoreStagedTrees(staged, error);
200
217
  }
201
- for (const tree of staged) {
202
- await removeFrozenPackageTree(tree.quarantined);
203
- }
204
- for (const directory of [packagesDir, path.dirname(packagesDir)]) {
218
+ return staged;
219
+ }
220
+ /** Remove only empty package ancestors: a concurrently-installed digest must never be deleted. */
221
+ async function removeEmptyPackageAncestors(required) {
222
+ for (const directory of [required.packagesDir, path.dirname(required.packagesDir)]) {
205
223
  try {
206
224
  await rmdir(directory);
207
225
  }
208
226
  catch (error) {
209
- if (!error || typeof error !== 'object' || !('code' in error) || !['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(String(error.code)))
227
+ if (!isAncestorStillInUse(error))
210
228
  throw error;
211
229
  }
212
230
  }
213
- if (optional.deleteMemory) {
214
- const pluginRoot = required.layout.forWorkspace({ workspaceId: required.workspaceId }).pluginRootDir({ pluginId: required.pluginId });
215
- await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, pluginRoot));
216
- if ((await listInstalledPlugins(workspaceRoot)).some(plugin => plugin.pluginId === required.pluginId))
217
- throw new Error('Plugin was reinstalled during uninstall; persistent memory was kept');
218
- await rm(pluginRoot, { recursive: true, force: true });
219
- }
220
- return { pluginId: required.pluginId, removedDigests: matches.map((plugin) => plugin.archiveDigest) };
231
+ }
232
+ function isAncestorStillInUse(error) {
233
+ return typeof error === 'object' && error !== null && 'code' in error && ['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(String(error.code));
234
+ }
235
+ /** No package may have appeared since this uninstall's preview. Preserve it on a race. */
236
+ async function deletePersistentMemory(required) {
237
+ const { workspaceRoot, pluginId } = required;
238
+ const pluginRoot = required.layout.forWorkspace({ workspaceId: required.workspaceId }).pluginRootDir({ pluginId });
239
+ await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, pluginRoot));
240
+ if ((await listInstalledPlugins(workspaceRoot)).some(plugin => plugin.pluginId === pluginId))
241
+ throw new Error('Plugin was reinstalled during uninstall; persistent memory was kept');
242
+ await rm(pluginRoot, { recursive: true, force: true });
221
243
  }
222
244
  /**
223
245
  * Refuses when the digests resolved now are not exactly the set a confirmed preview listed — an archive added
@@ -245,8 +267,10 @@ function buildModule(ports) {
245
267
  *
246
268
  * @complexity One `stat`, one or two `chmod`s, one `rename`.
247
269
  */
248
- async function stageForRemoval(packagesDir, packageRoot) {
249
- const quarantined = path.join(packagesDir, `${STAGED_DIRNAME_PREFIX}${randomUUID()}`);
270
+ async function stageForRemoval(packagesDir, packageRoot, optional = {}) {
271
+ // Updates choose a journaled destination outside packages/sha256; uninstall keeps its sibling
272
+ // destination and same-filesystem guarantee documented above. Both retain the same move owner.
273
+ const quarantined = optional.quarantined ?? path.join(packagesDir, `${STAGED_DIRNAME_PREFIX}${randomUUID()}`);
250
274
  const originalMode = (await stat(packageRoot)).mode & 0o777;
251
275
  await chmod(packageRoot, originalMode | 0o700);
252
276
  try {
@@ -0,0 +1,18 @@
1
+ import type { InstalledAgentPlugin } from './types.js';
2
+ type Revision = Pick<InstalledAgentPlugin, 'pluginId' | 'archiveDigest' | 'version'> & {
3
+ readonly enabled?: boolean;
4
+ };
5
+ export interface AgentPluginUpdatePlan {
6
+ readonly action: 'none' | 'update' | 'downgrade-refused' | 'id-mismatch';
7
+ readonly enabled: boolean;
8
+ readonly warnings: readonly string[];
9
+ }
10
+ /** Manifest semver orders releases; digest identifies bytes. Unknown or equal versions require
11
+ * an explicit replacement; automatic rollback bypasses planning. @complexity O(v) version length.
12
+ */
13
+ export declare function planAgentPluginUpdate({ installed, candidate }: {
14
+ readonly installed: Revision;
15
+ readonly candidate: Revision;
16
+ }, _optional?: Record<string, never>): AgentPluginUpdatePlan;
17
+ export {};
18
+ //# sourceMappingURL=update-plan.d.ts.map
@@ -0,0 +1,22 @@
1
+ import * as semver from 'semver';
2
+ /** Manifest semver orders releases; digest identifies bytes. Unknown or equal versions require
3
+ * an explicit replacement; automatic rollback bypasses planning. @complexity O(v) version length.
4
+ */
5
+ export function planAgentPluginUpdate({ installed, candidate }, _optional = {}) {
6
+ const enabled = installed.enabled === true;
7
+ if (installed.pluginId !== candidate.pluginId)
8
+ return { action: 'id-mismatch', enabled, warnings: [] };
9
+ if (installed.archiveDigest === candidate.archiveDigest)
10
+ return { action: 'none', enabled, warnings: [] };
11
+ // `semver.valid` returns null for an absent version as well as a malformed one.
12
+ const from = semver.valid(installed.version);
13
+ const to = semver.valid(candidate.version);
14
+ if (!from || !to) {
15
+ return { action: 'update', enabled, warnings: ['Unknown release version; explicit replacement required.'] };
16
+ }
17
+ const order = semver.compare(to, from);
18
+ if (order < 0)
19
+ return { action: 'downgrade-refused', enabled, warnings: [] };
20
+ return { action: 'update', enabled, warnings: order === 0 ? ['Same version with different bytes; explicit replacement required.'] : [] };
21
+ }
22
+ //# sourceMappingURL=update-plan.js.map
@@ -0,0 +1,71 @@
1
+ import type { AgentPluginLifecyclePorts } from './ports.js';
2
+ /** One update transaction's durable intent and, once finished, its audit record (`result`).
3
+ * `settingsSnapshot` is the host's NON-SECRET settings summary; hosts must never put secret values
4
+ * in it, because it is retained in the update history after the transaction ends. */
5
+ export interface AgentPluginUpdateJournal {
6
+ readonly schemaVersion: 1;
7
+ readonly state: 'pending' | 'published' | 'committed';
8
+ readonly transactionId: string;
9
+ readonly pluginId: string;
10
+ readonly actor: string;
11
+ readonly enabled: boolean;
12
+ readonly fromDigest: string;
13
+ readonly toDigest: string;
14
+ readonly fromVersion?: string | undefined;
15
+ readonly toVersion?: string | undefined;
16
+ readonly settingsSnapshot: unknown;
17
+ readonly startedAt: string;
18
+ readonly result?: 'updated' | 'rolled-back';
19
+ }
20
+ export declare class AgentPluginUpdateError extends Error {
21
+ readonly code: string;
22
+ constructor(required: {
23
+ code: string;
24
+ message: string;
25
+ }, optional?: ErrorOptions);
26
+ }
27
+ /** Durable per-plugin journal, audit history and frozen rollback area, all under the plugin's own
28
+ * state root (never under `package/sha256`, so a rollback copy is never a live digest). Files use
29
+ * the existing platform atomic writer. Settings snapshots are private (0600) and never returned to
30
+ * tools or HTTP callers.
31
+ * @complexity O(s) serialization of the host's settings slice.
32
+ */
33
+ export declare function createAgentPluginUpdateStore({ ports, workspaceId, pluginId }: {
34
+ ports: AgentPluginLifecyclePorts;
35
+ workspaceId: string;
36
+ pluginId: string;
37
+ }, _optional?: Record<string, never>): {
38
+ read: (_required: Record<string, never>, _optional?: {}) => Promise<AgentPluginUpdateJournal | undefined>;
39
+ write: ({ journal, history }: {
40
+ journal: AgentPluginUpdateJournal;
41
+ history?: boolean;
42
+ }, _optional?: {}) => Promise<void>;
43
+ clear: (_required: Record<string, never>, _optional?: {}) => Promise<void>;
44
+ finish: ({ journal }: {
45
+ journal: AgentPluginUpdateJournal;
46
+ }, _optional?: {}) => Promise<void>;
47
+ prepare: (_required: Record<string, never>, _optional?: {}) => Promise<void>;
48
+ listRollbackRoots: (_required: Record<string, never>, _optional?: {}) => Promise<string[]>;
49
+ realDirectoryExists: ({ root: target }: {
50
+ root: string;
51
+ }, _optional?: {}) => Promise<boolean>;
52
+ rollbackDir: string;
53
+ rollbackRoot: ({ digest }: {
54
+ digest: string;
55
+ }, _optional?: {}) => string;
56
+ liveRoot: ({ digest }: {
57
+ digest: string;
58
+ }, _optional?: {}) => string;
59
+ };
60
+ /** Every mutation of a plugin (update, enable/disable, uninstall) runs this under the plugin lock
61
+ * first. An unfinished journal refuses it: boot recovery would otherwise resurrect the old revision
62
+ * over an uninstall or overwrite an operator's newer decision. A committed journal whose audit filing
63
+ * was interrupted is filed now.
64
+ * @throws AgentPluginUpdateError PLUGIN_UPDATE_PENDING, UPDATE_JOURNAL_INVALID. */
65
+ export declare function settleAgentPluginUpdateJournal({ store }: {
66
+ store: Pick<ReturnType<typeof createAgentPluginUpdateStore>, 'read' | 'finish'>;
67
+ }, _optional?: {}): Promise<void>;
68
+ export declare function isMissing({ error }: {
69
+ error: unknown;
70
+ }, _optional?: {}): boolean;
71
+ //# sourceMappingURL=update-store.d.ts.map
@@ -0,0 +1,150 @@
1
+ import path from 'node:path';
2
+ import { writeJsonFileAtomicAsync } from '@jini-ai/platform/fs';
3
+ import { createPersistentStateModule } from './persistent-state.js';
4
+ export class AgentPluginUpdateError extends Error {
5
+ code;
6
+ constructor(required, optional = {}) {
7
+ super(required.message, optional);
8
+ this.code = required.code;
9
+ this.name = 'AgentPluginUpdateError';
10
+ }
11
+ }
12
+ const DIGEST_PATTERN = /^[a-f0-9]{64}$/;
13
+ const TRANSACTION_PATTERN = /^[a-zA-Z0-9-]{1,100}$/;
14
+ const JOURNAL_STATES = ['pending', 'published', 'committed'];
15
+ function invalidJournal(optional = {}) {
16
+ return new AgentPluginUpdateError({ code: 'UPDATE_JOURNAL_INVALID', message: 'Update recovery record is invalid.' }, optional);
17
+ }
18
+ function hasValidIdentity({ raw, pluginId }) {
19
+ return raw.schemaVersion === 1 && raw.pluginId === pluginId && typeof raw.actor === 'string' && typeof raw.enabled === 'boolean';
20
+ }
21
+ /** Digests become path segments in recovery; the grammar check is what keeps them segments. */
22
+ function hasValidDigests({ raw }) {
23
+ const digests = [raw.fromDigest, raw.toDigest];
24
+ return raw.fromDigest !== raw.toDigest && digests.every(digest => typeof digest === 'string' && DIGEST_PATTERN.test(digest));
25
+ }
26
+ /** The transaction id names the history file, so it gets the same segment discipline. */
27
+ function hasValidTransaction({ raw }) {
28
+ return JOURNAL_STATES.includes(raw.state) && typeof raw.transactionId === 'string'
29
+ && TRANSACTION_PATTERN.test(raw.transactionId) && typeof raw.startedAt === 'string';
30
+ }
31
+ /** Validates durable intent before using it; no journal field is ever accepted as a path.
32
+ * A damaged journal blocks mutations instead of discarding the only recovery record.
33
+ */
34
+ function parseJournal({ value, pluginId }, _optional = {}) {
35
+ if (typeof value !== 'object' || value === null)
36
+ throw invalidJournal();
37
+ const raw = value;
38
+ if (!hasValidIdentity({ raw, pluginId }) || !hasValidDigests({ raw }) || !hasValidTransaction({ raw }))
39
+ throw invalidJournal();
40
+ return raw;
41
+ }
42
+ /** Durable per-plugin journal, audit history and frozen rollback area, all under the plugin's own
43
+ * state root (never under `package/sha256`, so a rollback copy is never a live digest). Files use
44
+ * the existing platform atomic writer. Settings snapshots are private (0600) and never returned to
45
+ * tools or HTTP callers.
46
+ * @complexity O(s) serialization of the host's settings slice.
47
+ */
48
+ export function createAgentPluginUpdateStore({ ports, workspaceId, pluginId }, _optional = {}) {
49
+ const workspace = ports.layout.forWorkspace({ workspaceId });
50
+ const root = workspace.pluginRootDir({ pluginId });
51
+ const journalPath = path.join(root, 'update-journal.json');
52
+ const rollbackDir = path.join(root, 'rollback');
53
+ const persistent = createPersistentStateModule(ports);
54
+ async function contain({ entryPath }, _optional = {}) {
55
+ await persistent.assertOwnedPath({ workspaceRoot: workspace.root, entryPath: path.relative(workspace.root, entryPath) });
56
+ }
57
+ async function read(_required, _optional = {}) {
58
+ await contain({ entryPath: journalPath });
59
+ let text;
60
+ try {
61
+ text = await ports.filesystem.readFile(journalPath, 'utf8');
62
+ }
63
+ catch (error) {
64
+ if (!isMissing({ error }))
65
+ throw error;
66
+ return undefined;
67
+ }
68
+ let value;
69
+ try {
70
+ value = JSON.parse(text);
71
+ }
72
+ catch (cause) {
73
+ throw invalidJournal({ cause });
74
+ }
75
+ return parseJournal({ value, pluginId });
76
+ }
77
+ async function write({ journal, history = false }, _optional = {}) {
78
+ const filePath = history ? path.join(root, 'update-history', `${journal.transactionId}.json`) : journalPath;
79
+ await contain({ entryPath: filePath });
80
+ await writeJsonFileAtomicAsync({ filePath, data: journal }, { fs: ports.filesystem,
81
+ createParent: true, mode: 0o600, platform: ports.process.platform, tempName: () => ports.ids.newId() });
82
+ }
83
+ async function clear(_required, _optional = {}) {
84
+ await contain({ entryPath: journalPath });
85
+ await ports.filesystem.rm(journalPath, { force: true });
86
+ }
87
+ /** Audit first, then drop the active journal: a crash between the two leaves a finished journal
88
+ * that the next update or boot re-files (same transaction id, so the history write is idempotent). */
89
+ async function finish({ journal }, _optional = {}) {
90
+ await write({ journal, history: true });
91
+ await clear({});
92
+ }
93
+ async function prepare(_required, _optional = {}) {
94
+ await contain({ entryPath: rollbackDir });
95
+ await ports.filesystem.mkdir(rollbackDir, { recursive: true, mode: 0o700 });
96
+ }
97
+ /** Recovery chmods, renames and removes rollback and live roots, so each one must be a real
98
+ * directory inside this workspace's plugin state, never a symlink planted to redirect those calls
99
+ * (a symlink leaf fails `isDirectory()` under lstat; `contain` rejects symlinked ancestors).
100
+ * @returns false when the root is absent. */
101
+ async function realDirectoryExists({ root: target }, _optional = {}) {
102
+ let stats;
103
+ try {
104
+ stats = await ports.filesystem.lstat(target);
105
+ }
106
+ catch (error) {
107
+ if (!isMissing({ error }))
108
+ throw error;
109
+ return false;
110
+ }
111
+ if (!stats.isDirectory())
112
+ throw new AgentPluginUpdateError({ code: 'PLUGIN_UPDATE_PATH_UNSAFE', message: 'An update recovery path is not a real directory; repair it by hand.' });
113
+ await contain({ entryPath: target });
114
+ return true;
115
+ }
116
+ /** Retained rollback copies (digest-named directories only); other entries are never touched. */
117
+ async function listRollbackRoots(_required, _optional = {}) {
118
+ await contain({ entryPath: rollbackDir });
119
+ let entries;
120
+ try {
121
+ entries = await ports.filesystem.readdir(rollbackDir, { withFileTypes: true });
122
+ }
123
+ catch (error) {
124
+ if (!isMissing({ error }))
125
+ throw error;
126
+ return [];
127
+ }
128
+ return entries.filter(entry => entry.isDirectory() && DIGEST_PATTERN.test(entry.name)).map(entry => path.join(rollbackDir, entry.name));
129
+ }
130
+ return { read, write, clear, finish, prepare, listRollbackRoots, realDirectoryExists, rollbackDir,
131
+ rollbackRoot: ({ digest }, _optional = {}) => path.join(rollbackDir, digest),
132
+ liveRoot: ({ digest }, _optional = {}) => path.join(workspace.pluginPackagesDir({ pluginId }), digest) };
133
+ }
134
+ /** Every mutation of a plugin (update, enable/disable, uninstall) runs this under the plugin lock
135
+ * first. An unfinished journal refuses it: boot recovery would otherwise resurrect the old revision
136
+ * over an uninstall or overwrite an operator's newer decision. A committed journal whose audit filing
137
+ * was interrupted is filed now.
138
+ * @throws AgentPluginUpdateError PLUGIN_UPDATE_PENDING, UPDATE_JOURNAL_INVALID. */
139
+ export async function settleAgentPluginUpdateJournal({ store }, _optional = {}) {
140
+ const outstanding = await store.read({});
141
+ if (!outstanding)
142
+ return;
143
+ if (outstanding.state !== 'committed')
144
+ throw new AgentPluginUpdateError({ code: 'PLUGIN_UPDATE_PENDING', message: 'Restart the host to recover the interrupted update.' });
145
+ await store.finish({ journal: outstanding });
146
+ }
147
+ export function isMissing({ error }, _optional = {}) {
148
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
149
+ }
150
+ //# sourceMappingURL=update-store.js.map
@@ -0,0 +1,55 @@
1
+ import { type InstallAgentPluginOptional, type InstallAgentPluginRequired } from './install.js';
2
+ import type { AgentPluginLifecyclePorts } from './ports.js';
3
+ import type { InstalledAgentPlugin } from './types.js';
4
+ export interface UpdateAgentPluginRequired extends InstallAgentPluginRequired {
5
+ readonly pluginId: string;
6
+ /** The digest the operator previewed; any other live revision refuses with PLUGIN_CHANGED. */
7
+ readonly expectedOldDigest: string;
8
+ /** The authenticated principal, recorded in the update audit. */
9
+ readonly actor: string;
10
+ /** Host owns settings schemas; archives never supply executable migrations or snapshot code.
11
+ * Return a NON-SECRET summary only: it is persisted (0600) in the journal and update history. */
12
+ readonly snapshotSettings: (required: {
13
+ workspaceId: string;
14
+ pluginId: string;
15
+ }, optional: Record<string, never>) => Promise<unknown>;
16
+ }
17
+ export interface UpdateAgentPluginResult {
18
+ readonly plugin: InstalledAgentPlugin;
19
+ readonly enabled: boolean;
20
+ readonly alreadyInstalled: boolean;
21
+ readonly warnings: readonly string[];
22
+ }
23
+ type InstallAgentPluginPort = (required: InstallAgentPluginRequired, optional: InstallAgentPluginOptional) => Promise<InstalledAgentPlugin>;
24
+ /** Update transaction owner for operator-installed plugins (bundled ones stay seed-managed).
25
+ *
26
+ * Runs inside the installer's identity lock and per-plugin file lock (the publish guard), so it is
27
+ * serialized against enable/disable/uninstall, which take the same plugin lock. Nothing called in
28
+ * the guard re-takes that lock: activation writes use their own activations.json lock.
29
+ *
30
+ * Invariant: never two live digests. The old revision moves to a frozen rollback area outside
31
+ * `package/sha256` before the candidate is published; a per-plugin journal (pending -> published ->
32
+ * committed) lets boot reconciliation restore it if the process dies before commit.
33
+ *
34
+ * Documented behavior:
35
+ * - In-flight MCP calls against the old revision may fail once the roster is re-provisioned.
36
+ * - Running chats keep the skill text they already captured; new runs resolve the new revision.
37
+ * - Rollback bytes are retained after commit (a run may still read them) and collected by the next
38
+ * boot's reconciliation, when no run can hold those paths.
39
+ * - Plugin data and memory directories carry over untouched. A rollback restores package bytes and
40
+ * activation only; it does NOT revert data, memory or settings written while the candidate ran.
41
+ * The settings snapshot is an audit record, not a restore source.
42
+ * - Caches: this package keeps no cache of skill prompt indexes or search ranking keyed by plugin
43
+ * (search ranking is computed per call from the installed list; per-run automatic-use state is
44
+ * per run; module imports are keyed by the digest path), so a switch has nothing to invalidate.
45
+ */
46
+ export declare function createUpdateModule(ports: AgentPluginLifecyclePorts, optional?: {
47
+ installAgentPlugin?: InstallAgentPluginPort;
48
+ }): {
49
+ updateAgentPlugin: (required: UpdateAgentPluginRequired, _optional?: Record<string, never>) => Promise<UpdateAgentPluginResult>;
50
+ reconcileAgentPluginUpdates: ({ workspaceId }: {
51
+ workspaceId: string;
52
+ }, _optional?: Record<string, never>) => Promise<void>;
53
+ };
54
+ export {};
55
+ //# sourceMappingURL=update.d.ts.map