@dharma-ai-labs/agent-fabric-skill-manager 0.1.3 → 0.2.0

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.
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type KeyObject } from 'node:crypto';
2
2
  import { type ProviderId } from '@dharma-ai-labs/agent-fabric-contracts';
3
3
  import { type OrganizationPolicy } from '@dharma-ai-labs/agent-fabric-policy';
4
4
  export interface SkillBundle {
5
- schema: 'dharma.skill-bundle/v1';
5
+ schema: 'dharma.skill-bundle/v2';
6
6
  bundleId: string;
7
7
  organizationId: string;
8
8
  version: string;
@@ -21,7 +21,12 @@ export interface SkillBundle {
21
21
  }>;
22
22
  }>;
23
23
  riskClass: 'R0' | 'R1' | 'R2' | 'R3' | 'R4';
24
- targetSelectors: Record<string, unknown>;
24
+ targetSelectors: {
25
+ organizationAgentIds: string[];
26
+ deviceIds: string[];
27
+ workspaceIds: string[];
28
+ providers: ProviderId[];
29
+ };
25
30
  activationPolicy: 'next_task' | 'next_session' | 'host_restart' | 'immediate_safe_reload';
26
31
  rollbackBundleId: string | null;
27
32
  evaluationReceiptId: string;
@@ -52,10 +57,46 @@ export interface InstallReceipt {
52
57
  receiptHash: string;
53
58
  signature: string;
54
59
  }
60
+ export interface ActiveSkillBundleAuthorization {
61
+ bundleId: string;
62
+ activatedAt: string;
63
+ expiresAt: string | null;
64
+ }
55
65
  declare function contentHash(path: string): Promise<string>;
56
66
  export declare function calculateBundleHash(bundle: Omit<SkillBundle, 'signature' | 'bundleHash'>): string;
57
67
  export declare function verifySkillBundle(bundle: SkillBundle, serverPublicKey: KeyObject, now?: Date): void;
58
- export declare function getActiveSkillBundleId(nativeSkillDirectory: string, workspaceId?: string): Promise<string | null>;
68
+ export declare function getLegacySkillBundleIdForUpgrade(input: {
69
+ nativeSkillDirectory: string;
70
+ workspaceId: string;
71
+ }): Promise<string | null>;
72
+ export declare function getInstalledSkillBundleIdForRecovery(input: {
73
+ nativeSkillDirectory: string;
74
+ workspaceId: string;
75
+ }): Promise<string | null>;
76
+ export declare function getExpiredSkillBundleAuthorizationForReplacement(input: {
77
+ nativeSkillDirectory: string;
78
+ workspaceId: string;
79
+ provider: ProviderId;
80
+ organizationId: string;
81
+ organizationAgentId: string;
82
+ deviceId: string;
83
+ serverPublicKey: KeyObject;
84
+ devicePublicKey: KeyObject;
85
+ expectedReceiptHash: string;
86
+ now?: Date;
87
+ }): Promise<ActiveSkillBundleAuthorization | null>;
88
+ export declare function getActiveSkillBundleAuthorization(input: {
89
+ nativeSkillDirectory: string;
90
+ workspaceId: string;
91
+ provider: ProviderId;
92
+ organizationId: string;
93
+ organizationAgentId: string;
94
+ deviceId: string;
95
+ serverPublicKey: KeyObject;
96
+ devicePublicKey: KeyObject;
97
+ expectedReceiptHash: string;
98
+ now?: Date;
99
+ }): Promise<ActiveSkillBundleAuthorization | null>;
59
100
  export declare function installSkillBundle(input: {
60
101
  bundle: SkillBundle;
61
102
  sourceDirectory: string;
@@ -64,10 +105,16 @@ export declare function installSkillBundle(input: {
64
105
  serverPublicKey: KeyObject;
65
106
  devicePrivateKey: KeyObject;
66
107
  deviceId: string;
108
+ organizationAgentId: string;
67
109
  workspaceId: string;
68
110
  provider: ProviderId;
69
111
  smokeCommandId?: string;
70
112
  organizationApprovalId?: string;
71
113
  }): Promise<InstallReceipt>;
114
+ export declare function rollbackUnconfirmedSkillBundle(input: {
115
+ nativeSkillDirectory: string;
116
+ workspaceId: string;
117
+ receipt: InstallReceipt;
118
+ }): Promise<void>;
72
119
  export { contentHash };
73
120
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -36,14 +36,43 @@ export function calculateBundleHash(bundle) {
36
36
  return sha256(canonicalize(bundle));
37
37
  }
38
38
  export function verifySkillBundle(bundle, serverPublicKey, now = new Date()) {
39
- if (bundle.expiresAt && Date.parse(bundle.expiresAt) <= now.getTime())
40
- throw new Error('Skill bundle has expired.');
39
+ if (bundle.schema !== 'dharma.skill-bundle/v2') {
40
+ throw new Error('Skill bundle schema is not authorized for installation or task execution.');
41
+ }
42
+ if (bundle.expiresAt) {
43
+ const expiresAt = Date.parse(bundle.expiresAt);
44
+ if (!Number.isFinite(expiresAt))
45
+ throw new Error('Skill bundle expiry is invalid.');
46
+ if (expiresAt <= now.getTime())
47
+ throw new Error('Skill bundle has expired.');
48
+ }
41
49
  const { signature, ...unsigned } = bundle;
42
50
  if (!verifyCanonicalObject(unsigned, signature, serverPublicKey))
43
51
  throw new Error('Skill bundle signature is invalid.');
44
52
  const { bundleHash, ...hashInput } = unsigned;
45
53
  if (bundleHash !== calculateBundleHash(hashInput))
46
54
  throw new Error('Skill bundle hash is invalid.');
55
+ const selectors = bundle.targetSelectors;
56
+ if (!selectors || typeof selectors !== 'object'
57
+ || !Array.isArray(selectors.organizationAgentIds) || !Array.isArray(selectors.deviceIds)
58
+ || !Array.isArray(selectors.workspaceIds) || !Array.isArray(selectors.providers)
59
+ || selectors.organizationAgentIds.some((value) => typeof value !== 'string' || !value)
60
+ || selectors.deviceIds.some((value) => typeof value !== 'string' || !value)
61
+ || selectors.workspaceIds.some((value) => typeof value !== 'string' || !value)
62
+ || selectors.providers.some((value) => !['codex', 'claude', 'agy'].includes(value))) {
63
+ throw new Error('Skill bundle target selectors are invalid.');
64
+ }
65
+ }
66
+ function assertTargetSelector(selector, value, label) {
67
+ if (selector.length > 0 && !selector.includes(value)) {
68
+ throw new Error(`Skill bundle is not authorized for this ${label}.`);
69
+ }
70
+ }
71
+ function assertBundleTargetsEndpoint(bundle, endpoint) {
72
+ assertTargetSelector(bundle.targetSelectors.organizationAgentIds, endpoint.organizationAgentId, 'repository agent');
73
+ assertTargetSelector(bundle.targetSelectors.deviceIds, endpoint.deviceId, 'device');
74
+ assertTargetSelector(bundle.targetSelectors.workspaceIds, endpoint.workspaceId, 'workspace');
75
+ assertTargetSelector(bundle.targetSelectors.providers, endpoint.provider, 'provider');
47
76
  }
48
77
  async function runSmoke(commandId, policy, cwd) {
49
78
  const command = resolveRegisteredCommand(policy, commandId);
@@ -60,15 +89,51 @@ async function runSmoke(commandId, policy, cwd) {
60
89
  });
61
90
  });
62
91
  }
63
- async function readActiveBundleId(root) {
92
+ async function readActiveBundlePointer(root) {
93
+ let bundleId;
64
94
  try {
65
- return (await readFile(resolve(root, 'ACTIVE_BUNDLE'), 'utf8')).trim() || null;
95
+ bundleId = (await readFile(resolve(root, 'ACTIVE_BUNDLE'), 'utf8')).trim();
66
96
  }
67
97
  catch (error) {
68
98
  if (error.code === 'ENOENT')
69
99
  return null;
70
100
  throw error;
71
101
  }
102
+ if (!bundleId)
103
+ return null;
104
+ let manifest;
105
+ try {
106
+ manifest = JSON.parse(await readFile(resolve(root, 'active', 'BUNDLE.json'), 'utf8'));
107
+ }
108
+ catch (error) {
109
+ if (error.code === 'ENOENT') {
110
+ throw new Error('Active bundle manifest is missing for the installed bundle pointer.');
111
+ }
112
+ throw error;
113
+ }
114
+ if (manifest.bundleId !== bundleId) {
115
+ throw new Error('Active bundle pointer does not match the installed release manifest.');
116
+ }
117
+ return bundleId;
118
+ }
119
+ function verifyInstallReceipt(receipt, devicePublicKey, expected, now) {
120
+ const { signature, ...signed } = receipt;
121
+ if (!verifyCanonicalObject(signed, signature, devicePublicKey))
122
+ throw new Error('Active bundle install receipt signature is invalid.');
123
+ const { receiptHash, ...hashInput } = signed;
124
+ if (receiptHash !== sha256(canonicalize(hashInput)))
125
+ throw new Error('Active bundle install receipt hash is invalid.');
126
+ if (receipt.status !== 'active')
127
+ throw new Error('Active bundle install receipt is not active.');
128
+ if (receipt.bundleId !== expected.bundleId || receipt.organizationId !== expected.organizationId
129
+ || receipt.deviceId !== expected.deviceId || receipt.workspaceId !== expected.workspaceId || receipt.provider !== expected.provider) {
130
+ throw new Error('Active bundle install receipt does not match the selected endpoint.');
131
+ }
132
+ const startedAt = Date.parse(receipt.startedAt);
133
+ const completedAt = Date.parse(receipt.completedAt);
134
+ if (!Number.isFinite(startedAt) || !Number.isFinite(completedAt) || completedAt < startedAt || completedAt > now.getTime() + 5_000) {
135
+ throw new Error('Active bundle install receipt has an invalid activation window.');
136
+ }
72
137
  }
73
138
  function workspaceManagedRoot(nativeSkillDirectory, workspaceId) {
74
139
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(workspaceId)) {
@@ -76,6 +141,101 @@ function workspaceManagedRoot(nativeSkillDirectory, workspaceId) {
76
141
  }
77
142
  return resolve(nativeSkillDirectory, '.dharma-managed', 'workspaces', workspaceId);
78
143
  }
144
+ export async function getLegacySkillBundleIdForUpgrade(input) {
145
+ const root = workspaceManagedRoot(input.nativeSkillDirectory, input.workspaceId);
146
+ await recoverInterruptedSkillRollbacks(input.nativeSkillDirectory, input.workspaceId);
147
+ const bundleId = await readActiveBundlePointer(root);
148
+ if (!bundleId)
149
+ return null;
150
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(bundleId)) {
151
+ throw new Error('Legacy skill bundle identifier is invalid.');
152
+ }
153
+ let authorization;
154
+ let source = 'authorization';
155
+ try {
156
+ authorization = JSON.parse(await readFile(resolve(root, 'active', 'AUTHORIZATION.json'), 'utf8'));
157
+ }
158
+ catch (error) {
159
+ if (error.code !== 'ENOENT')
160
+ throw error;
161
+ source = 'legacy_manifest';
162
+ authorization = JSON.parse(await readFile(resolve(root, 'active', 'BUNDLE.json'), 'utf8'));
163
+ }
164
+ if (authorization.bundleId !== bundleId)
165
+ return null;
166
+ if (source === 'authorization') {
167
+ if (authorization.schema !== 'dharma.skill-bundle/v1')
168
+ return null;
169
+ }
170
+ else if (authorization.schema !== undefined
171
+ || authorization.workspaceId !== input.workspaceId
172
+ || !Array.isArray(authorization.skillIds)
173
+ || !authorization.skillIds.every((skillId) => typeof skillId === 'string' && skillId.length > 0)) {
174
+ return null;
175
+ }
176
+ return bundleId;
177
+ }
178
+ export async function getInstalledSkillBundleIdForRecovery(input) {
179
+ const root = workspaceManagedRoot(input.nativeSkillDirectory, input.workspaceId);
180
+ await recoverInterruptedSkillRollbacks(input.nativeSkillDirectory, input.workspaceId);
181
+ const bundleId = await readActiveBundlePointer(root);
182
+ if (!bundleId)
183
+ return null;
184
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(bundleId)) {
185
+ throw new Error('Installed skill bundle identifier is invalid.');
186
+ }
187
+ let manifest;
188
+ let source = 'authorization';
189
+ try {
190
+ manifest = JSON.parse(await readFile(resolve(root, 'active', 'AUTHORIZATION.json'), 'utf8'));
191
+ }
192
+ catch (error) {
193
+ if (error.code !== 'ENOENT')
194
+ throw error;
195
+ source = 'legacy-manifest';
196
+ manifest = JSON.parse(await readFile(resolve(root, 'active', 'BUNDLE.json'), 'utf8'));
197
+ }
198
+ const authorizedManifest = source === 'authorization'
199
+ && ['dharma.skill-bundle/v1', 'dharma.skill-bundle/v2'].includes(String(manifest.schema));
200
+ const legacyInstallManifest = source === 'legacy-manifest'
201
+ && manifest.schema === undefined
202
+ && manifest.workspaceId === input.workspaceId
203
+ && Array.isArray(manifest.skillIds)
204
+ && manifest.skillIds.every((skillId) => typeof skillId === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(skillId));
205
+ if ((!authorizedManifest && !legacyInstallManifest) || manifest.bundleId !== bundleId) {
206
+ throw new Error('Installed skill recovery metadata is invalid.');
207
+ }
208
+ return bundleId;
209
+ }
210
+ export async function getExpiredSkillBundleAuthorizationForReplacement(input) {
211
+ const root = workspaceManagedRoot(input.nativeSkillDirectory, input.workspaceId);
212
+ await recoverInterruptedSkillRollbacks(input.nativeSkillDirectory, input.workspaceId);
213
+ const bundleId = await readActiveBundlePointer(root);
214
+ if (!bundleId)
215
+ return null;
216
+ const bundle = JSON.parse(await readFile(resolve(root, 'active', 'AUTHORIZATION.json'), 'utf8'));
217
+ const receipt = JSON.parse(await readFile(resolve(root, 'active', 'INSTALL_RECEIPT.json'), 'utf8'));
218
+ const now = input.now ?? new Date();
219
+ const expiresAt = Date.parse(String(bundle.expiresAt || ''));
220
+ if (!Number.isFinite(expiresAt) || expiresAt > now.getTime()) {
221
+ throw new Error('Installed skill bundle is not an expired replacement candidate.');
222
+ }
223
+ if (bundle.bundleId !== bundleId)
224
+ throw new Error('Active bundle pointer does not match the signed release authorization.');
225
+ verifySkillBundle(bundle, input.serverPublicKey, new Date(expiresAt - 1));
226
+ assertBundleTargetsEndpoint(bundle, input);
227
+ verifyInstallReceipt(receipt, input.devicePublicKey, {
228
+ bundleId,
229
+ organizationId: input.organizationId,
230
+ deviceId: input.deviceId,
231
+ workspaceId: input.workspaceId,
232
+ provider: input.provider,
233
+ }, now);
234
+ if (receipt.receiptHash !== input.expectedReceiptHash) {
235
+ throw new Error('Active bundle receipt is not the current protected authorization.');
236
+ }
237
+ return { bundleId, activatedAt: receipt.completedAt, expiresAt: bundle.expiresAt ?? null };
238
+ }
79
239
  async function pathExists(path) {
80
240
  try {
81
241
  await lstat(path);
@@ -156,16 +316,110 @@ async function activateNativeSkills(input) {
156
316
  await rm(transactionRoot, { recursive: true, force: true });
157
317
  }
158
318
  }
159
- export async function getActiveSkillBundleId(nativeSkillDirectory, workspaceId) {
160
- return readActiveBundleId(workspaceId
161
- ? workspaceManagedRoot(nativeSkillDirectory, workspaceId)
162
- : resolve(nativeSkillDirectory, '.dharma-managed'));
319
+ async function nativeSkillIdsOwnedByWorkspace(nativeSkillDirectory, workspaceId) {
320
+ let entries;
321
+ try {
322
+ entries = await readdir(nativeSkillDirectory, { withFileTypes: true });
323
+ }
324
+ catch (error) {
325
+ if (error.code === 'ENOENT')
326
+ return [];
327
+ throw error;
328
+ }
329
+ const owned = [];
330
+ for (const entry of entries) {
331
+ if (!entry.isDirectory() || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(entry.name))
332
+ continue;
333
+ try {
334
+ const marker = JSON.parse(await readFile(resolve(nativeSkillDirectory, entry.name, '.dharma-agent-fabric.json'), 'utf8'));
335
+ if (marker.workspaceId === workspaceId)
336
+ owned.push(entry.name);
337
+ }
338
+ catch (error) {
339
+ if (error.code !== 'ENOENT')
340
+ throw error;
341
+ }
342
+ }
343
+ return owned;
344
+ }
345
+ async function recoverInterruptedSkillRollbacks(nativeSkillDirectory, workspaceId) {
346
+ const managedRoot = workspaceManagedRoot(nativeSkillDirectory, workspaceId);
347
+ let entries;
348
+ try {
349
+ entries = await readdir(managedRoot, { withFileTypes: true });
350
+ }
351
+ catch (error) {
352
+ if (error.code === 'ENOENT')
353
+ return;
354
+ throw error;
355
+ }
356
+ for (const entry of entries.filter((candidate) => candidate.isDirectory() && candidate.name.startsWith('.rollback-'))) {
357
+ const transactionRoot = assertContained(managedRoot, resolve(managedRoot, entry.name));
358
+ const manifest = JSON.parse(await readFile(resolve(transactionRoot, 'ROLLBACK_RECOVERY.json'), 'utf8'));
359
+ if (manifest.schema !== 'dharma.skill-rollback-recovery/v1'
360
+ || manifest.workspaceId !== workspaceId
361
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(manifest.currentBundleId)
362
+ || !Array.isArray(manifest.currentSkillIds)
363
+ || manifest.currentSkillIds.some((skillId) => typeof skillId !== 'string'
364
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(skillId))) {
365
+ throw new Error('Interrupted skill rollback recovery metadata is invalid.');
366
+ }
367
+ const releases = resolve(managedRoot, 'releases');
368
+ const currentRelease = assertContained(releases, resolve(releases, manifest.currentBundleId));
369
+ const active = resolve(managedRoot, 'active');
370
+ const backupActive = resolve(transactionRoot, 'backup-active');
371
+ if (await pathExists(backupActive)) {
372
+ await rm(active, { recursive: true, force: true });
373
+ await rename(backupActive, active);
374
+ }
375
+ else if (!await pathExists(active)) {
376
+ await cp(currentRelease, active, { recursive: true, errorOnExist: true, force: false });
377
+ }
378
+ await activateNativeSkills({
379
+ nativeSkillDirectory,
380
+ release: currentRelease,
381
+ bundleId: manifest.currentBundleId,
382
+ workspaceId,
383
+ skillIds: manifest.currentSkillIds,
384
+ removeSkillIds: await nativeSkillIdsOwnedByWorkspace(nativeSkillDirectory, workspaceId),
385
+ });
386
+ const pointerTemp = resolve(transactionRoot, 'ACTIVE_BUNDLE.recovery');
387
+ await writeFile(pointerTemp, `${manifest.currentBundleId}\n`, { mode: 0o600 });
388
+ await rename(pointerTemp, resolve(managedRoot, 'ACTIVE_BUNDLE'));
389
+ await rm(transactionRoot, { recursive: true, force: true });
390
+ }
391
+ }
392
+ export async function getActiveSkillBundleAuthorization(input) {
393
+ const root = workspaceManagedRoot(input.nativeSkillDirectory, input.workspaceId);
394
+ await recoverInterruptedSkillRollbacks(input.nativeSkillDirectory, input.workspaceId);
395
+ const bundleId = await readActiveBundlePointer(root);
396
+ if (!bundleId)
397
+ return null;
398
+ const bundle = JSON.parse(await readFile(resolve(root, 'active', 'AUTHORIZATION.json'), 'utf8'));
399
+ const receipt = JSON.parse(await readFile(resolve(root, 'active', 'INSTALL_RECEIPT.json'), 'utf8'));
400
+ const now = input.now ?? new Date();
401
+ if (bundle.bundleId !== bundleId)
402
+ throw new Error('Active bundle pointer does not match the signed release authorization.');
403
+ verifySkillBundle(bundle, input.serverPublicKey, now);
404
+ assertBundleTargetsEndpoint(bundle, input);
405
+ verifyInstallReceipt(receipt, input.devicePublicKey, {
406
+ bundleId,
407
+ organizationId: input.organizationId,
408
+ deviceId: input.deviceId,
409
+ workspaceId: input.workspaceId,
410
+ provider: input.provider,
411
+ }, now);
412
+ if (receipt.receiptHash !== input.expectedReceiptHash) {
413
+ throw new Error('Active bundle receipt is not the current protected authorization.');
414
+ }
415
+ return { bundleId, activatedAt: receipt.completedAt, expiresAt: bundle.expiresAt ?? null };
163
416
  }
164
417
  export async function installSkillBundle(input) {
165
418
  const startedAt = new Date().toISOString();
166
419
  verifySkillBundle(input.bundle, input.serverPublicKey);
167
420
  if (input.bundle.organizationId !== input.policy.organizationId)
168
421
  throw new Error('Bundle organization does not match policy.');
422
+ assertBundleTargetsEndpoint(input.bundle, input);
169
423
  if (input.bundle.operation === 'clear' && input.bundle.skills.length !== 0)
170
424
  throw new Error('Clear bundles cannot contain skills.');
171
425
  if (input.bundle.operation === 'install' && input.bundle.skills.length === 0)
@@ -176,11 +430,12 @@ export async function installSkillBundle(input) {
176
430
  if (!input.policy.skills.automaticInstall)
177
431
  throw new Error('Automatic skill installation is disabled by policy.');
178
432
  const managedRoot = workspaceManagedRoot(input.nativeSkillDirectory, input.workspaceId);
433
+ await recoverInterruptedSkillRollbacks(input.nativeSkillDirectory, input.workspaceId);
179
434
  const releases = resolve(managedRoot, 'releases');
180
435
  const release = assertContained(releases, resolve(releases, input.bundle.bundleId));
181
436
  const active = resolve(managedRoot, 'active');
182
437
  const rollback = resolve(managedRoot, 'rollback');
183
- const previousBundleId = await readActiveBundleId(managedRoot);
438
+ const previousBundleId = await readActiveBundlePointer(managedRoot);
184
439
  await mkdir(releases, { recursive: true, mode: 0o700 });
185
440
  await rm(release, { recursive: true, force: true });
186
441
  await mkdir(release, { recursive: true, mode: 0o700 });
@@ -197,6 +452,7 @@ export async function installSkillBundle(input) {
197
452
  }
198
453
  const skillIds = input.bundle.skills.map((skill) => skill.skillId);
199
454
  await writeFile(resolve(release, 'BUNDLE.json'), `${JSON.stringify({ bundleId: input.bundle.bundleId, workspaceId: input.workspaceId, skillIds })}\n`, { mode: 0o600 });
455
+ await writeFile(resolve(release, 'AUTHORIZATION.json'), `${JSON.stringify(input.bundle)}\n`, { mode: 0o600 });
200
456
  const previousRelease = previousBundleId ? resolve(releases, previousBundleId) : null;
201
457
  const previousSkillIds = previousRelease ? await releaseSkillIds(previousRelease) : [];
202
458
  await rm(rollback, { recursive: true, force: true });
@@ -217,39 +473,47 @@ export async function installSkillBundle(input) {
217
473
  removeSkillIds: previousSkillIds,
218
474
  });
219
475
  let status = 'active';
476
+ const restorePreviousBundle = async () => {
477
+ await rm(active, { recursive: true, force: true });
478
+ let restored = 'failed';
479
+ try {
480
+ await rename(rollback, active);
481
+ restored = 'rolled_back';
482
+ }
483
+ catch (error) {
484
+ if (error.code !== 'ENOENT')
485
+ throw error;
486
+ }
487
+ await activateNativeSkills({
488
+ nativeSkillDirectory: input.nativeSkillDirectory,
489
+ release: previousRelease || release,
490
+ bundleId: previousBundleId || input.bundle.bundleId,
491
+ workspaceId: input.workspaceId,
492
+ skillIds: previousBundleId ? previousSkillIds : [],
493
+ removeSkillIds: skillIds,
494
+ });
495
+ return restored;
496
+ };
220
497
  if (input.smokeCommandId) {
221
498
  const check = await runSmoke(input.smokeCommandId, input.policy, active);
222
499
  checks.push({ name: `smoke:${input.smokeCommandId}`, ...check });
223
500
  if (check.status === 'fail') {
224
- await rm(active, { recursive: true, force: true });
225
- try {
226
- await rename(rollback, active);
227
- status = 'rolled_back';
228
- }
229
- catch (error) {
230
- if (error.code !== 'ENOENT')
231
- throw error;
232
- status = 'failed';
233
- }
234
- await activateNativeSkills({
235
- nativeSkillDirectory: input.nativeSkillDirectory,
236
- release: previousRelease || release,
237
- bundleId: previousBundleId || input.bundle.bundleId,
238
- workspaceId: input.workspaceId,
239
- skillIds: previousBundleId ? previousSkillIds : [],
240
- removeSkillIds: skillIds,
241
- });
501
+ status = await restorePreviousBundle();
242
502
  }
243
503
  }
244
504
  if (status === 'active') {
245
- await rm(rollback, { recursive: true, force: true });
246
- await writeFile(resolve(managedRoot, 'ACTIVE_BUNDLE'), `${input.bundle.bundleId}\n`, { mode: 0o600 });
247
- }
248
- else if (previousBundleId) {
249
- await writeFile(resolve(managedRoot, 'ACTIVE_BUNDLE'), `${previousBundleId}\n`, { mode: 0o600 });
250
- }
251
- else {
252
- await rm(resolve(managedRoot, 'ACTIVE_BUNDLE'), { force: true });
505
+ try {
506
+ verifySkillBundle(input.bundle, input.serverPublicKey);
507
+ checks.push({ name: 'authorization:activation-window', status: 'pass', details: input.bundle.expiresAt || 'no-expiry' });
508
+ }
509
+ catch (error) {
510
+ checks.push({
511
+ name: 'authorization:activation-window',
512
+ status: 'fail',
513
+ details: error instanceof Error ? error.message : String(error),
514
+ });
515
+ status = await restorePreviousBundle();
516
+ }
253
517
  }
254
518
  const receiptBase = {
255
519
  schema: 'dharma.install-receipt/v1',
@@ -269,7 +533,148 @@ export async function installSkillBundle(input) {
269
533
  };
270
534
  const receiptHash = sha256(canonicalize(receiptBase));
271
535
  const signature = signCanonicalObject({ ...receiptBase, receiptHash }, input.devicePrivateKey);
272
- return { ...receiptBase, receiptHash, signature };
536
+ const receipt = { ...receiptBase, receiptHash, signature };
537
+ if (status === 'active') {
538
+ try {
539
+ await writeFile(resolve(release, 'INSTALL_RECEIPT.json'), `${JSON.stringify(receipt)}\n`, { mode: 0o600 });
540
+ await writeFile(resolve(active, 'INSTALL_RECEIPT.json'), `${JSON.stringify(receipt)}\n`, { mode: 0o600 });
541
+ await writeFile(resolve(managedRoot, 'ACTIVE_BUNDLE'), `${input.bundle.bundleId}\n`, { mode: 0o600 });
542
+ }
543
+ catch (error) {
544
+ const recoveryErrors = [];
545
+ try {
546
+ await restorePreviousBundle();
547
+ }
548
+ catch (recoveryError) {
549
+ recoveryErrors.push(recoveryError);
550
+ }
551
+ try {
552
+ if (previousBundleId) {
553
+ await writeFile(resolve(managedRoot, 'ACTIVE_BUNDLE'), `${previousBundleId}\n`, { mode: 0o600 });
554
+ }
555
+ else {
556
+ await rm(resolve(managedRoot, 'ACTIVE_BUNDLE'), { force: true });
557
+ }
558
+ }
559
+ catch (recoveryError) {
560
+ recoveryErrors.push(recoveryError);
561
+ }
562
+ if (recoveryErrors.length) {
563
+ throw new AggregateError([error, ...recoveryErrors], 'Skill receipt persistence failed and local recovery was incomplete.');
564
+ }
565
+ throw error;
566
+ }
567
+ }
568
+ else if (previousBundleId) {
569
+ await writeFile(resolve(managedRoot, 'ACTIVE_BUNDLE'), `${previousBundleId}\n`, { mode: 0o600 });
570
+ }
571
+ else {
572
+ await rm(resolve(managedRoot, 'ACTIVE_BUNDLE'), { force: true });
573
+ }
574
+ return receipt;
575
+ }
576
+ export async function rollbackUnconfirmedSkillBundle(input) {
577
+ if (input.receipt.status !== 'active')
578
+ return;
579
+ const managedRoot = workspaceManagedRoot(input.nativeSkillDirectory, input.workspaceId);
580
+ await recoverInterruptedSkillRollbacks(input.nativeSkillDirectory, input.workspaceId);
581
+ const activeBundleId = await readActiveBundlePointer(managedRoot);
582
+ if (activeBundleId !== input.receipt.bundleId) {
583
+ throw new Error('Refusing to roll back an installation that is no longer the active local bundle.');
584
+ }
585
+ const releases = resolve(managedRoot, 'releases');
586
+ const active = resolve(managedRoot, 'active');
587
+ const currentRelease = assertContained(releases, resolve(releases, input.receipt.bundleId));
588
+ const currentSkillIds = await releaseSkillIds(currentRelease);
589
+ const previousBundleId = input.receipt.previousBundleId;
590
+ const previousRelease = previousBundleId
591
+ ? assertContained(releases, resolve(releases, previousBundleId))
592
+ : null;
593
+ const previousSkillIds = previousRelease ? await releaseSkillIds(previousRelease) : [];
594
+ const transactionRoot = assertContained(managedRoot, resolve(managedRoot, `.rollback-${randomUUID()}`));
595
+ const stagedActive = resolve(transactionRoot, 'staged-active');
596
+ const backupActive = resolve(transactionRoot, 'backup-active');
597
+ const pointer = resolve(managedRoot, 'ACTIVE_BUNDLE');
598
+ const pointerTemp = resolve(transactionRoot, 'ACTIVE_BUNDLE.next');
599
+ let nativeChanged = false;
600
+ let activeMoved = false;
601
+ let stagedMoved = false;
602
+ let preserveRecovery = false;
603
+ await mkdir(transactionRoot, { recursive: true, mode: 0o700 });
604
+ await writeFile(resolve(transactionRoot, 'ROLLBACK_RECOVERY.json'), `${JSON.stringify({
605
+ schema: 'dharma.skill-rollback-recovery/v1',
606
+ workspaceId: input.workspaceId,
607
+ currentBundleId: input.receipt.bundleId,
608
+ currentSkillIds,
609
+ })}\n`, { mode: 0o600 });
610
+ if (previousRelease) {
611
+ await cp(previousRelease, stagedActive, { recursive: true, errorOnExist: true, force: false });
612
+ }
613
+ try {
614
+ await activateNativeSkills({
615
+ nativeSkillDirectory: input.nativeSkillDirectory,
616
+ release: previousRelease || currentRelease,
617
+ bundleId: previousBundleId || input.receipt.bundleId,
618
+ workspaceId: input.workspaceId,
619
+ skillIds: previousSkillIds,
620
+ removeSkillIds: currentSkillIds,
621
+ });
622
+ nativeChanged = true;
623
+ await rename(active, backupActive);
624
+ activeMoved = true;
625
+ if (previousRelease) {
626
+ await rename(stagedActive, active);
627
+ stagedMoved = true;
628
+ await writeFile(pointerTemp, `${previousBundleId}\n`, { mode: 0o600 });
629
+ await rename(pointerTemp, pointer);
630
+ }
631
+ else {
632
+ await rm(pointer, { force: true });
633
+ }
634
+ }
635
+ catch (error) {
636
+ const recoveryErrors = [];
637
+ if (activeMoved) {
638
+ try {
639
+ if (stagedMoved)
640
+ await rm(active, { recursive: true, force: true });
641
+ await rename(backupActive, active);
642
+ }
643
+ catch (recoveryError) {
644
+ recoveryErrors.push(recoveryError);
645
+ }
646
+ }
647
+ if (nativeChanged) {
648
+ try {
649
+ await activateNativeSkills({
650
+ nativeSkillDirectory: input.nativeSkillDirectory,
651
+ release: currentRelease,
652
+ bundleId: input.receipt.bundleId,
653
+ workspaceId: input.workspaceId,
654
+ skillIds: currentSkillIds,
655
+ removeSkillIds: previousSkillIds,
656
+ });
657
+ }
658
+ catch (recoveryError) {
659
+ recoveryErrors.push(recoveryError);
660
+ }
661
+ }
662
+ try {
663
+ await writeFile(pointer, `${input.receipt.bundleId}\n`, { mode: 0o600 });
664
+ }
665
+ catch (recoveryError) {
666
+ recoveryErrors.push(recoveryError);
667
+ }
668
+ if (recoveryErrors.length) {
669
+ preserveRecovery = true;
670
+ throw new AggregateError([error, ...recoveryErrors], 'Skill rollback failed and the active installation could not be fully restored.');
671
+ }
672
+ throw error;
673
+ }
674
+ finally {
675
+ if (!preserveRecovery)
676
+ await rm(transactionRoot, { recursive: true, force: true });
677
+ }
273
678
  }
274
679
  export { contentHash };
275
680
  //# sourceMappingURL=index.js.map