@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 +50 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +441 -36
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +350 -20
- package/dist/index.test.js.map +1 -1
- package/package.json +1 -1
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/
|
|
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:
|
|
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
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAGrE,OAAO,EAAoE,KAAK,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAC3I,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAExG,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,wBAAwB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC;IAC/B,MAAM,EAAE,KAAK,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,aAAa,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACxE,CAAC,CAAC;IACH,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC5C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,
|
|
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.
|
|
40
|
-
throw new Error('Skill bundle
|
|
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
|
|
92
|
+
async function readActiveBundlePointer(root) {
|
|
93
|
+
let bundleId;
|
|
64
94
|
try {
|
|
65
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|