@dharma-ai-labs/agent-fabric 0.1.27 → 0.2.1
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/README.md +22 -3
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +733 -123
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +204 -1
- package/dist/index.test.js.map +1 -1
- package/dist/schemas/skill-bundle.schema.json +67 -4
- package/dist/schemas/trajectory-capsule-v2.schema.json +427 -0
- package/dist/schemas/trajectory-capsule-v3.schema.json +568 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -8,15 +8,15 @@ import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
10
|
import { canonicalize, sha256, validateContract, verifyCanonicalObject } from '@dharma-ai-labs/agent-fabric-contracts';
|
|
11
|
-
import { buildTrajectoryCapsule, redactValue, referencesExcludedPath } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
|
|
11
|
+
import { buildTrajectoryCapsule, redactValue, referencesExcludedPath, trajectoryCapsuleHash } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
|
|
12
12
|
import { assertPolicy, loadOrganizationPolicy, verifyServerAuthorizedPolicy } from '@dharma-ai-labs/agent-fabric-policy';
|
|
13
13
|
import { agyAdapter, claudeAdapter, codexAdapter, providerAdapters } from '@dharma-ai-labs/agent-fabric-provider-adapters';
|
|
14
|
-
import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, saveDeviceConfig, } from '@dharma-ai-labs/agent-fabric-relay-client';
|
|
14
|
+
import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, deleteActiveSkillAuthorizationAnchor, loadActiveSkillAuthorizationAnchor, loadDeviceEnrollmentAnchor, saveActiveSkillAuthorizationAnchor, isDefinitiveAgentFabricRejection, saveDeviceConfig, saveDeviceEnrollmentAnchor, } from '@dharma-ai-labs/agent-fabric-relay-client';
|
|
15
15
|
import { AgentFabricClient as AgentFabricApiClient } from '@dharma-ai-labs/agent-fabric-sdk';
|
|
16
|
-
import {
|
|
16
|
+
import { getActiveSkillBundleAuthorization, getExpiredSkillBundleAuthorizationForReplacement, getLegacySkillBundleIdForUpgrade, installSkillBundle, rollbackUnconfirmedSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
|
|
17
17
|
import { executeTask, FileTaskReceiptStore } from '@dharma-ai-labs/agent-fabric-task-runner';
|
|
18
18
|
import { CLI_USAGE } from './usage.js';
|
|
19
|
-
const VERSION = '0.1
|
|
19
|
+
const VERSION = '0.2.1';
|
|
20
20
|
const USAGE = CLI_USAGE;
|
|
21
21
|
const execFileAsync = promisify(execFile);
|
|
22
22
|
export function parseCliOptions(args) {
|
|
@@ -133,6 +133,21 @@ function evidenceLedgerForPolicyActivation(value, day) {
|
|
|
133
133
|
throw new Error('Evidence upload ledger is invalid.');
|
|
134
134
|
return newEvidenceUploadLedger(day);
|
|
135
135
|
}
|
|
136
|
+
export async function pathExistsOrThrow(path, check = access) {
|
|
137
|
+
try {
|
|
138
|
+
await check(path);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error.code === 'ENOENT')
|
|
143
|
+
return false;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export async function canonicalFilesystemPath(path) {
|
|
148
|
+
return realpath(path);
|
|
149
|
+
}
|
|
150
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
136
151
|
async function pathExists(path) {
|
|
137
152
|
try {
|
|
138
153
|
await access(path);
|
|
@@ -223,7 +238,11 @@ async function refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspa
|
|
|
223
238
|
if (!item)
|
|
224
239
|
throw new Error('Content transmission workspace is not registered locally.');
|
|
225
240
|
const canonicalPolicyPath = resolve(item.path, '.dharma', 'approved-policy.json');
|
|
226
|
-
|
|
241
|
+
const [providedIdentity, registeredIdentity] = await Promise.all([
|
|
242
|
+
canonicalFilesystemPath(resolve(policyPath)),
|
|
243
|
+
canonicalFilesystemPath(canonicalPolicyPath),
|
|
244
|
+
]);
|
|
245
|
+
if (providedIdentity !== registeredIdentity) {
|
|
227
246
|
throw new Error('Content transmission requires the canonical registered workspace policy path.');
|
|
228
247
|
}
|
|
229
248
|
const current = await loadOrganizationPolicy(policyPath);
|
|
@@ -236,11 +255,21 @@ async function refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspa
|
|
|
236
255
|
}
|
|
237
256
|
export function assertCapsuleIntegrity(capsule) {
|
|
238
257
|
const capsuleHash = String(capsule.capsuleHash || '');
|
|
239
|
-
|
|
240
|
-
if (!/^sha256:[a-f0-9]{64}$/.test(capsuleHash) || sha256(canonicalize(unsigned)) !== capsuleHash) {
|
|
258
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(capsuleHash) || trajectoryCapsuleHash(capsule) !== capsuleHash) {
|
|
241
259
|
throw new Error('Trajectory capsule integrity check failed.');
|
|
242
260
|
}
|
|
243
261
|
}
|
|
262
|
+
function trajectoryCapsuleSchemaId(capsule) {
|
|
263
|
+
const schema = capsule && typeof capsule === 'object' && !Array.isArray(capsule)
|
|
264
|
+
? capsule.schema : null;
|
|
265
|
+
if (schema === 'dharma.trajectory-capsule/v1')
|
|
266
|
+
return 'https://schemas.dharma-ai.io/trajectory-capsule/v1';
|
|
267
|
+
if (schema === 'dharma.trajectory-capsule/v2')
|
|
268
|
+
return 'https://schemas.dharma-ai.io/trajectory-capsule/v2';
|
|
269
|
+
if (schema === 'dharma.trajectory-capsule/v3')
|
|
270
|
+
return 'https://schemas.dharma-ai.io/trajectory-capsule/v3';
|
|
271
|
+
throw new Error('Trajectory capsule schema is unsupported.');
|
|
272
|
+
}
|
|
244
273
|
export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
245
274
|
assertPolicy(policy);
|
|
246
275
|
const mode = capsule.automaticDisclosureMode;
|
|
@@ -256,6 +285,10 @@ export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
|
256
285
|
const events = Array.isArray(capsule.events) ? capsule.events : [];
|
|
257
286
|
if (mode !== 'customer_authorized_content') {
|
|
258
287
|
const provider = String(capsule.provider || '');
|
|
288
|
+
const capsuleSchema = String(capsule.schema || '');
|
|
289
|
+
const captureProvenance = capsule.captureProvenance && typeof capsule.captureProvenance === 'object'
|
|
290
|
+
&& !Array.isArray(capsule.captureProvenance)
|
|
291
|
+
? capsule.captureProvenance : {};
|
|
259
292
|
const fixedEventKinds = new Set([
|
|
260
293
|
'user_message', 'agent_message', 'tool_call', 'tool_result', 'command', 'file_read', 'file_write',
|
|
261
294
|
'git', 'validation', 'permission', 'subagent', 'error', 'retry', 'session_state', 'metadata', 'collapsed_output',
|
|
@@ -282,9 +315,19 @@ export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
|
282
315
|
const coverage = capsule.coverage && typeof capsule.coverage === 'object' && !Array.isArray(capsule.coverage)
|
|
283
316
|
? capsule.coverage : {};
|
|
284
317
|
const missingFields = Array.isArray(coverage.missingFields) ? coverage.missingFields : null;
|
|
318
|
+
const signedTaskCapture = capsuleSchema === 'dharma.trajectory-capsule/v3'
|
|
319
|
+
&& captureProvenance.sourceClass === 'signed_task_execution';
|
|
320
|
+
const signedTaskBundleIds = [...new Set(events.map((event) => (event && typeof event === 'object' && !Array.isArray(event)
|
|
321
|
+
? String(event.skillBundleId || '') : '')).filter(Boolean))];
|
|
285
322
|
if (!['codex', 'claude', 'agy'].includes(provider)
|
|
286
323
|
|| !digest.test(String(capsule.sessionId || ''))
|
|
287
|
-
||
|
|
324
|
+
|| (signedTaskCapture
|
|
325
|
+
? !uuid.test(String(capsule.taskId || ''))
|
|
326
|
+
|| !digest.test(String(captureProvenance.taskReceiptHash || ''))
|
|
327
|
+
|| !Number.isFinite(Date.parse(String(captureProvenance.collectedAt || '')))
|
|
328
|
+
|| signedTaskBundleIds.length !== 1
|
|
329
|
+
|| !uuid.test(signedTaskBundleIds[0] || '')
|
|
330
|
+
: capsule.taskId !== null || capsuleSchema === 'dharma.trajectory-capsule/v3')
|
|
288
331
|
|| !uuid.test(String(capsule.deviceId || ''))
|
|
289
332
|
|| capsule.redactionReceipt === null
|
|
290
333
|
|| receipt.policyRevision !== policy.revision
|
|
@@ -358,7 +401,8 @@ export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
|
358
401
|
|| !Array.isArray(eventRecord.contentRefs) || eventRecord.contentRefs.length !== 0
|
|
359
402
|
|| source.nativeEventId !== null || source.localLocatorId !== null || source.sourceKind !== eventKind
|
|
360
403
|
|| record.nativeKind !== eventKind
|
|
361
|
-
|| eventRecord.skillBundleId !==
|
|
404
|
+
|| eventRecord.skillBundleId !== (signedTaskCapture ? signedTaskBundleIds[0] : null)
|
|
405
|
+
|| eventRecord.providerModel !== null) {
|
|
362
406
|
throw new Error('Reduced trajectory capsule contains unauthorized event descriptors.');
|
|
363
407
|
}
|
|
364
408
|
}
|
|
@@ -694,6 +738,13 @@ export async function withWorkspacePolicyRefreshLock(workspaceId, operation) {
|
|
|
694
738
|
throw new Error('Content transmission requires a registered workspace ID.');
|
|
695
739
|
return withFileLock(`${workspaceAuthorizationStatePath(workspaceId)}.refresh.lock`, operation, 'Timed out waiting for the workspace policy refresh lock.');
|
|
696
740
|
}
|
|
741
|
+
export async function withWorkspaceSkillActivationLock(workspaceId, provider, operation) {
|
|
742
|
+
if (!workspaceId || !['codex', 'claude', 'agy'].includes(provider)) {
|
|
743
|
+
throw new Error('Skill activation lock requires a registered workspace and supported provider.');
|
|
744
|
+
}
|
|
745
|
+
const key = createHash('sha256').update(`${workspaceId}:${provider}`).digest('hex');
|
|
746
|
+
return withFileLock(resolve(dharmaHome(), 'registry', 'skill-activation-locks', `${key}.lock`), operation, 'Timed out waiting for the workspace skill activation lock.');
|
|
747
|
+
}
|
|
697
748
|
async function assertWorkspaceAuthorizationCurrent(workspaceId, authorization, requireExisting = true) {
|
|
698
749
|
const statePath = workspaceAuthorizationStatePath(workspaceId);
|
|
699
750
|
let previous = null;
|
|
@@ -1097,16 +1148,36 @@ async function runOrganizationCommand(command, subcommand, flags) {
|
|
|
1097
1148
|
if (command === 'remediations' && subcommand === 'list')
|
|
1098
1149
|
return api.listRemediations();
|
|
1099
1150
|
if (command === 'remediations' && subcommand === 'act') {
|
|
1100
|
-
requireExplicitConfirmation(flags, 'Changing a repository remediation release');
|
|
1101
1151
|
const targetId = required(flags, 'target-id');
|
|
1102
1152
|
const body = await commandJsonBody(flags);
|
|
1103
1153
|
const action = String(flags.get('action') || body.action || '');
|
|
1104
|
-
if (!['run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
|
|
1105
|
-
throw new Error('Remediation action must be run_backtest, link_backtest, approve, merge_pr, release, expand, or rollback.');
|
|
1154
|
+
if (!['stage_evaluation', 'run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
|
|
1155
|
+
throw new Error('Remediation action must be stage_evaluation, run_backtest, link_backtest, approve, merge_pr, release, expand, or rollback.');
|
|
1106
1156
|
}
|
|
1157
|
+
if (action === 'stage_evaluation') {
|
|
1158
|
+
const endpointId = String(flags.get('endpoint-id') || body.endpointId || '');
|
|
1159
|
+
if (!UUID_PATTERN.test(endpointId))
|
|
1160
|
+
throw new Error('stage_evaluation requires --endpoint-id with an exact endpoint UUID.');
|
|
1161
|
+
if (flags.get('dry-run') === true) {
|
|
1162
|
+
return {
|
|
1163
|
+
ok: true,
|
|
1164
|
+
planned: true,
|
|
1165
|
+
serverMutation: false,
|
|
1166
|
+
targetId,
|
|
1167
|
+
transition: { action, endpointId },
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
requireExplicitConfirmation(flags, 'Staging a repository remediation evaluation');
|
|
1171
|
+
return api.transitionRemediationTarget(targetId, { action, endpointId });
|
|
1172
|
+
}
|
|
1173
|
+
requireExplicitConfirmation(flags, 'Changing a repository remediation release');
|
|
1107
1174
|
return api.transitionRemediationTarget(targetId, {
|
|
1108
|
-
...body,
|
|
1109
1175
|
action: action,
|
|
1176
|
+
...(Array.isArray(body.trajectoryIds) ? { trajectoryIds: body.trajectoryIds.map(String) } : {}),
|
|
1177
|
+
...(typeof body.campaignId === 'string' ? { campaignId: body.campaignId } : {}),
|
|
1178
|
+
...(typeof body.establishAutoUpdatePolicy === 'boolean'
|
|
1179
|
+
? { establishAutoUpdatePolicy: body.establishAutoUpdatePolicy }
|
|
1180
|
+
: {}),
|
|
1110
1181
|
});
|
|
1111
1182
|
}
|
|
1112
1183
|
if (command === 'skills' && subcommand === 'list')
|
|
@@ -1198,6 +1269,7 @@ async function login(flags) {
|
|
|
1198
1269
|
serverPublicKeyEd25519: result.serverPublicKeyEd25519, relayUrl: result.relayUrl, enrolledAt: new Date().toISOString(),
|
|
1199
1270
|
};
|
|
1200
1271
|
await saveDeviceConfig(configPath(), config);
|
|
1272
|
+
await saveDeviceEnrollmentAnchor({ config });
|
|
1201
1273
|
await rm(pendingEnrollmentPath(), { force: true });
|
|
1202
1274
|
return { ok: true, status: 'approved', deviceId: config.deviceId, organizationId: pending.organizationId, relayUrl: config.relayUrl };
|
|
1203
1275
|
}
|
|
@@ -1244,6 +1316,11 @@ async function capture(flags, batch = false) {
|
|
|
1244
1316
|
const registered = (await registry()).find((item) => item.path === workspace);
|
|
1245
1317
|
if (!registered)
|
|
1246
1318
|
throw new Error('Workspace is not registered locally. Run dharma workspace add.');
|
|
1319
|
+
const captureProvenance = (collectedAt) => ({
|
|
1320
|
+
sourceClass: (typeof root === 'string' ? 'explicit_import' : 'provider_discovery'),
|
|
1321
|
+
collectedAt,
|
|
1322
|
+
taskReceiptHash: null,
|
|
1323
|
+
});
|
|
1247
1324
|
let policy = await loadVerifiedWorkspacePolicy(policyPath, registered.workspaceId);
|
|
1248
1325
|
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
1249
1326
|
const fabric = flags.has('sync') ? await client() : null;
|
|
@@ -1265,6 +1342,7 @@ async function capture(flags, batch = false) {
|
|
|
1265
1342
|
const firstRevision = buildTrajectoryCapsule({
|
|
1266
1343
|
organizationId: device.organizationId, deviceId: device.deviceId, workspaceId: registered.workspaceId,
|
|
1267
1344
|
session: selected, policy, rawContentId, rawBytes: rawTurn.byteLength, rawKind: 'raw-provider-turn',
|
|
1345
|
+
captureProvenance: captureProvenance(selected.endedAt),
|
|
1268
1346
|
});
|
|
1269
1347
|
const latestMetadata = vault.getLatestCapsuleMetadata(firstRevision.trajectoryId);
|
|
1270
1348
|
const latestCapsule = latestMetadata
|
|
@@ -1281,9 +1359,10 @@ async function capture(flags, batch = false) {
|
|
|
1281
1359
|
organizationId: device.organizationId, deviceId: device.deviceId, workspaceId: registered.workspaceId,
|
|
1282
1360
|
session: selected, policy, rawContentId, rawBytes: rawTurn.byteLength, rawKind: 'raw-provider-turn',
|
|
1283
1361
|
revision: latestMetadata.revision + 1, previousRevisionHash: latestMetadata.capsuleHash,
|
|
1362
|
+
captureProvenance: captureProvenance(selected.endedAt),
|
|
1284
1363
|
})
|
|
1285
1364
|
: firstRevision;
|
|
1286
|
-
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'),
|
|
1365
|
+
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
|
|
1287
1366
|
if (!validation.ok)
|
|
1288
1367
|
throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
|
|
1289
1368
|
if (!latestCapsule || latestCapsule.capsuleHash !== capsule.capsuleHash) {
|
|
@@ -1371,6 +1450,11 @@ async function evidencePreview(flags) {
|
|
|
1371
1450
|
if (!registered)
|
|
1372
1451
|
throw new Error('Workspace is not registered locally. Run dharma workspace add.');
|
|
1373
1452
|
const policy = await loadVerifiedWorkspacePolicy(policyPath, registered?.workspaceId);
|
|
1453
|
+
const captureProvenance = (collectedAt) => ({
|
|
1454
|
+
sourceClass: (typeof root === 'string' ? 'explicit_import' : 'provider_discovery'),
|
|
1455
|
+
collectedAt,
|
|
1456
|
+
taskReceiptHash: null,
|
|
1457
|
+
});
|
|
1374
1458
|
const capsules = sessions.map((session) => {
|
|
1375
1459
|
const rawTurn = Buffer.from(`${session.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
|
|
1376
1460
|
return buildTrajectoryCapsule({
|
|
@@ -1382,6 +1466,7 @@ async function evidencePreview(flags) {
|
|
|
1382
1466
|
rawContentId: sha256(rawTurn),
|
|
1383
1467
|
rawBytes: rawTurn.byteLength,
|
|
1384
1468
|
rawKind: 'raw-provider-turn',
|
|
1469
|
+
captureProvenance: captureProvenance(session.endedAt),
|
|
1385
1470
|
});
|
|
1386
1471
|
});
|
|
1387
1472
|
automaticDisclosure = {
|
|
@@ -1692,6 +1777,73 @@ async function readDeviceConfig() {
|
|
|
1692
1777
|
return null;
|
|
1693
1778
|
}
|
|
1694
1779
|
}
|
|
1780
|
+
async function activeSkillAuthorization(provider, workspaceId, organizationAgentId, config) {
|
|
1781
|
+
const root = nativeSkillDirectory(provider);
|
|
1782
|
+
const pointer = resolve(root, '.dharma-managed', 'workspaces', workspaceId, 'ACTIVE_BUNDLE');
|
|
1783
|
+
if (!await pathExistsOrThrow(pointer))
|
|
1784
|
+
return null;
|
|
1785
|
+
if (!organizationAgentId)
|
|
1786
|
+
throw new Error('Workspace is not bound to a repository agent. Run dharma workspace sync.');
|
|
1787
|
+
const [identity, enrollment, active] = await Promise.all([
|
|
1788
|
+
loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId }),
|
|
1789
|
+
loadDeviceEnrollmentAnchor({ config }),
|
|
1790
|
+
loadActiveSkillAuthorizationAnchor({ config, workspaceId, organizationAgentId, provider }),
|
|
1791
|
+
]);
|
|
1792
|
+
if (!active)
|
|
1793
|
+
throw new Error('Active skill state is not anchored in secure storage. Run dharma skill sync again.');
|
|
1794
|
+
if (identity.publicKeyEd25519 !== enrollment.devicePublicKeyEd25519) {
|
|
1795
|
+
throw new Error('Active skill device identity does not match secure enrollment state.');
|
|
1796
|
+
}
|
|
1797
|
+
return getActiveSkillBundleAuthorization({
|
|
1798
|
+
nativeSkillDirectory: root,
|
|
1799
|
+
workspaceId,
|
|
1800
|
+
provider,
|
|
1801
|
+
organizationId: config.organizationId,
|
|
1802
|
+
organizationAgentId,
|
|
1803
|
+
deviceId: config.deviceId,
|
|
1804
|
+
serverPublicKey: createPublicKey({
|
|
1805
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: enrollment.serverPublicKeyEd25519 },
|
|
1806
|
+
format: 'jwk',
|
|
1807
|
+
}),
|
|
1808
|
+
devicePublicKey: createPublicKey({
|
|
1809
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: identity.publicKeyEd25519 },
|
|
1810
|
+
format: 'jwk',
|
|
1811
|
+
}),
|
|
1812
|
+
expectedReceiptHash: active.receiptHash,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
async function expiredSkillAuthorizationForReplacement(provider, workspaceId, organizationAgentId, config) {
|
|
1816
|
+
const root = nativeSkillDirectory(provider);
|
|
1817
|
+
if (!organizationAgentId)
|
|
1818
|
+
throw new Error('Workspace is not bound to a repository agent. Run dharma workspace sync.');
|
|
1819
|
+
const [identity, enrollment, active] = await Promise.all([
|
|
1820
|
+
loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId }),
|
|
1821
|
+
loadDeviceEnrollmentAnchor({ config }),
|
|
1822
|
+
loadActiveSkillAuthorizationAnchor({ config, workspaceId, organizationAgentId, provider }),
|
|
1823
|
+
]);
|
|
1824
|
+
if (!active)
|
|
1825
|
+
throw new Error('Active skill state is not anchored in secure storage. Run dharma skill sync again.');
|
|
1826
|
+
if (identity.publicKeyEd25519 !== enrollment.devicePublicKeyEd25519) {
|
|
1827
|
+
throw new Error('Active skill device identity does not match secure enrollment state.');
|
|
1828
|
+
}
|
|
1829
|
+
return getExpiredSkillBundleAuthorizationForReplacement({
|
|
1830
|
+
nativeSkillDirectory: root,
|
|
1831
|
+
workspaceId,
|
|
1832
|
+
provider,
|
|
1833
|
+
organizationId: config.organizationId,
|
|
1834
|
+
organizationAgentId,
|
|
1835
|
+
deviceId: config.deviceId,
|
|
1836
|
+
serverPublicKey: createPublicKey({
|
|
1837
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: enrollment.serverPublicKeyEd25519 },
|
|
1838
|
+
format: 'jwk',
|
|
1839
|
+
}),
|
|
1840
|
+
devicePublicKey: createPublicKey({
|
|
1841
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: identity.publicKeyEd25519 },
|
|
1842
|
+
format: 'jwk',
|
|
1843
|
+
}),
|
|
1844
|
+
expectedReceiptHash: active.receiptHash,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1695
1847
|
async function loadVerifiedWorkspacePolicy(path, workspaceId) {
|
|
1696
1848
|
const policy = await loadOrganizationPolicy(path);
|
|
1697
1849
|
if (policy.evidence.automaticDisclosure?.mode !== 'customer_authorized_content')
|
|
@@ -1910,7 +2062,7 @@ async function onboard(flags) {
|
|
|
1910
2062
|
async function evidenceSync(flags) {
|
|
1911
2063
|
const capsule = JSON.parse(await readFile(resolve(required(flags, 'file')), 'utf8'));
|
|
1912
2064
|
const workspaceId = typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : String(capsule.workspaceId || '');
|
|
1913
|
-
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'),
|
|
2065
|
+
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
|
|
1914
2066
|
if (!validation.ok)
|
|
1915
2067
|
throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
|
|
1916
2068
|
assertCapsuleIntegrity(capsule);
|
|
@@ -2125,7 +2277,279 @@ async function runOneEvidenceRequest(flags) {
|
|
|
2125
2277
|
const policy = await refreshVerifiedWorkspacePolicyForTransmission(required(flags, 'policy'), workspaceId, fabric);
|
|
2126
2278
|
return processEvidenceRequest(fabric, policy, workspaceId);
|
|
2127
2279
|
}
|
|
2128
|
-
|
|
2280
|
+
export function taskReceiptSession(task, receipt, workspace) {
|
|
2281
|
+
return {
|
|
2282
|
+
provider: task.target.provider,
|
|
2283
|
+
sessionId: `dharma-task-${task.taskId}`,
|
|
2284
|
+
sourcePath: 'dharma-task-receipt',
|
|
2285
|
+
workspace,
|
|
2286
|
+
coverage: 'observed',
|
|
2287
|
+
startedAt: receipt.startedAt,
|
|
2288
|
+
endedAt: receipt.completedAt,
|
|
2289
|
+
records: receipt.commandResults.map((result, index) => ({
|
|
2290
|
+
native: {
|
|
2291
|
+
taskId: task.taskId,
|
|
2292
|
+
commandId: result.commandId,
|
|
2293
|
+
exitCode: result.exitCode,
|
|
2294
|
+
signal: result.signal,
|
|
2295
|
+
timedOut: result.timedOut,
|
|
2296
|
+
stdout: result.stdout,
|
|
2297
|
+
stderr: result.stderr,
|
|
2298
|
+
stdoutSha256: result.stdoutSha256,
|
|
2299
|
+
stderrSha256: result.stderrSha256,
|
|
2300
|
+
},
|
|
2301
|
+
sourcePath: 'dharma-task-receipt',
|
|
2302
|
+
line: index + 1,
|
|
2303
|
+
workspace,
|
|
2304
|
+
timestamp: receipt.completedAt,
|
|
2305
|
+
kind: result.commandId.startsWith('provider.') ? 'agent_message' : 'validation',
|
|
2306
|
+
coverage: 'observed',
|
|
2307
|
+
})),
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
function prepareSignedTaskTrajectory(input) {
|
|
2311
|
+
if (!input.activeSkill || input.task.skillBundle?.bundleId !== input.activeSkill.bundleId) {
|
|
2312
|
+
throw new Error('Signed task trajectory requires the active task-pinned skill bundle.');
|
|
2313
|
+
}
|
|
2314
|
+
const session = taskReceiptSession(input.task, input.receipt, input.workspace.path);
|
|
2315
|
+
const rawTurn = Buffer.from(`${session.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
|
|
2316
|
+
const rawContentId = sha256(rawTurn);
|
|
2317
|
+
const capsule = buildTrajectoryCapsule({
|
|
2318
|
+
organizationId: input.device.organizationId,
|
|
2319
|
+
deviceId: input.device.deviceId,
|
|
2320
|
+
workspaceId: input.workspace.workspaceId,
|
|
2321
|
+
session,
|
|
2322
|
+
policy: input.policy,
|
|
2323
|
+
rawContentId,
|
|
2324
|
+
rawBytes: rawTurn.byteLength,
|
|
2325
|
+
rawKind: 'raw-provider-turn',
|
|
2326
|
+
taskId: input.task.taskId,
|
|
2327
|
+
captureProvenance: {
|
|
2328
|
+
sourceClass: 'signed_task_execution',
|
|
2329
|
+
collectedAt: input.collectedAt,
|
|
2330
|
+
taskReceiptHash: input.taskReceiptHash,
|
|
2331
|
+
},
|
|
2332
|
+
activeSkillBundleId: input.activeSkill.bundleId,
|
|
2333
|
+
activeSkillBundleActivatedAt: input.activeSkill.activatedAt,
|
|
2334
|
+
activeSkillBundleExpiresAt: input.activeSkill.expiresAt,
|
|
2335
|
+
activeSkillBundleVerifiedAt: input.activeSkillVerifiedAt,
|
|
2336
|
+
});
|
|
2337
|
+
return { capsule, rawTurn, rawContentId, session };
|
|
2338
|
+
}
|
|
2339
|
+
async function syncSignedTaskTrajectory(input) {
|
|
2340
|
+
const { capsule, rawTurn, rawContentId, session } = input.prepared;
|
|
2341
|
+
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
|
|
2342
|
+
if (!validation.ok)
|
|
2343
|
+
throw new Error(`Signed task capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
|
|
2344
|
+
assertCapsuleAuthorizedByCurrentPolicy(capsule, input.policy);
|
|
2345
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
2346
|
+
const vault = await LocalVault.open({
|
|
2347
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2348
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2349
|
+
rawLocalDays: rawLocalRetentionDays(input.policy),
|
|
2350
|
+
});
|
|
2351
|
+
try {
|
|
2352
|
+
const existing = vault.getCapsuleMetadata(capsule.trajectoryId, capsule.revision);
|
|
2353
|
+
if (!existing) {
|
|
2354
|
+
await vault.commitCapture({
|
|
2355
|
+
raw: { plaintext: rawTurn, kind: 'raw-provider-turn', expectedContentId: rawContentId },
|
|
2356
|
+
capsule: {
|
|
2357
|
+
plaintext: Buffer.from(JSON.stringify(capsule)), trajectoryId: capsule.trajectoryId,
|
|
2358
|
+
revision: capsule.revision, capsuleHash: capsule.capsuleHash,
|
|
2359
|
+
},
|
|
2360
|
+
session: {
|
|
2361
|
+
sessionId: session.sessionId, provider: session.provider, workspaceId: input.workspace.workspaceId,
|
|
2362
|
+
sourceLocator: session.sourcePath, status: session.coverage, observedAt: session.endedAt,
|
|
2363
|
+
},
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
else if (existing.capsuleHash !== capsule.capsuleHash) {
|
|
2367
|
+
throw new Error('Signed task evidence already exists with different immutable content.');
|
|
2368
|
+
}
|
|
2369
|
+
vault.queueCapsuleSync(capsule.trajectoryId, capsule.revision);
|
|
2370
|
+
await reserveDailyContentUpload(capsule, input.policy);
|
|
2371
|
+
const synced = await input.fabric.syncTrajectory(capsule);
|
|
2372
|
+
vault.markCapsuleSynced(capsule.trajectoryId, capsule.revision);
|
|
2373
|
+
return synced;
|
|
2374
|
+
}
|
|
2375
|
+
finally {
|
|
2376
|
+
vault.close();
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
export function assertRecoveredTaskWorkspacePolicy(input) {
|
|
2380
|
+
if (input.workspace.workspaceId !== input.recoveryWorkspaceId) {
|
|
2381
|
+
throw new Error('Recovered task completion does not match its registered workspace.');
|
|
2382
|
+
}
|
|
2383
|
+
if (input.policy.organizationId !== input.workspace.organizationId
|
|
2384
|
+
|| (input.policy.serverAuthorization
|
|
2385
|
+
&& input.policy.serverAuthorization.workspaceId !== input.workspace.workspaceId)) {
|
|
2386
|
+
throw new Error('Recovered task completion policy does not authorize its workspace.');
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
export function recoveredTaskPolicyWasSuperseded(capsule, currentPolicy) {
|
|
2390
|
+
return capsule.redactionReceipt?.policyRevision !== currentPolicy.revision;
|
|
2391
|
+
}
|
|
2392
|
+
export function assertTaskWorkspacePolicy(input) {
|
|
2393
|
+
if (input.task.workspaceId !== input.workspace.workspaceId
|
|
2394
|
+
|| input.task.organizationId !== input.workspace.organizationId
|
|
2395
|
+
|| input.policy.organizationId !== input.workspace.organizationId
|
|
2396
|
+
|| (input.policy.serverAuthorization
|
|
2397
|
+
&& input.policy.serverAuthorization.workspaceId !== input.workspace.workspaceId)) {
|
|
2398
|
+
throw new Error('Task workspace policy does not match the signed task and local registration.');
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
async function stageSignedTaskTrajectoryRecovery(taskId, workspaceId, policy, prepared) {
|
|
2402
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
2403
|
+
const vault = await LocalVault.open({
|
|
2404
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2405
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2406
|
+
rawLocalDays: rawLocalRetentionDays(policy),
|
|
2407
|
+
});
|
|
2408
|
+
try {
|
|
2409
|
+
const recovery = {
|
|
2410
|
+
schema: 'dharma.signed-task-trajectory-recovery/v1', taskId, workspaceId,
|
|
2411
|
+
prepared: {
|
|
2412
|
+
capsule: prepared.capsule,
|
|
2413
|
+
rawTurnBase64: prepared.rawTurn.toString('base64'),
|
|
2414
|
+
rawContentId: prepared.rawContentId,
|
|
2415
|
+
session: prepared.session,
|
|
2416
|
+
},
|
|
2417
|
+
};
|
|
2418
|
+
await vault.stageTaskCompletionRecovery(taskId, Buffer.from(JSON.stringify(recovery)));
|
|
2419
|
+
}
|
|
2420
|
+
finally {
|
|
2421
|
+
vault.close();
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
async function finalizeRecoveredSignedTaskTrajectories(fabric, vaultPolicy, onlyTaskId) {
|
|
2425
|
+
const completions = fabric.listRecoveredTaskCompletions()
|
|
2426
|
+
.filter((item) => !onlyTaskId || item.taskId === onlyTaskId);
|
|
2427
|
+
const finalized = [];
|
|
2428
|
+
for (const completion of completions) {
|
|
2429
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
2430
|
+
const vault = await LocalVault.open({
|
|
2431
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2432
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2433
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2434
|
+
});
|
|
2435
|
+
let recovery;
|
|
2436
|
+
try {
|
|
2437
|
+
recovery = await vault.getTaskCompletionRecovery(completion.taskId);
|
|
2438
|
+
}
|
|
2439
|
+
finally {
|
|
2440
|
+
vault.close();
|
|
2441
|
+
}
|
|
2442
|
+
if (!recovery || recovery.schema !== 'dharma.signed-task-trajectory-recovery/v1'
|
|
2443
|
+
|| recovery.taskId !== completion.taskId) {
|
|
2444
|
+
throw new Error(`Recovered task completion ${completion.taskId} is missing its encrypted local evidence.`);
|
|
2445
|
+
}
|
|
2446
|
+
const workspace = (await registry()).find((item) => item.workspaceId === recovery.workspaceId);
|
|
2447
|
+
if (!workspace)
|
|
2448
|
+
throw new Error(`Recovered task completion ${completion.taskId} has no registered workspace.`);
|
|
2449
|
+
const recoveryPolicy = await refreshVerifiedWorkspacePolicyForTransmission(resolve(workspace.path, '.dharma', 'approved-policy.json'), recovery.workspaceId, fabric);
|
|
2450
|
+
assertRecoveredTaskWorkspacePolicy({
|
|
2451
|
+
recoveryWorkspaceId: recovery.workspaceId,
|
|
2452
|
+
workspace,
|
|
2453
|
+
policy: recoveryPolicy,
|
|
2454
|
+
});
|
|
2455
|
+
const rawTurn = Buffer.from(recovery.prepared.rawTurnBase64, 'base64');
|
|
2456
|
+
if (recovery.prepared.capsule.taskId !== completion.taskId
|
|
2457
|
+
|| recovery.prepared.capsule.workspaceId !== recovery.workspaceId
|
|
2458
|
+
|| recovery.prepared.capsule.capsuleHash !== completion.trajectoryCapsuleHash
|
|
2459
|
+
|| sha256(rawTurn) !== recovery.prepared.rawContentId) {
|
|
2460
|
+
throw new Error(`Recovered task completion ${completion.taskId} does not match its trajectory capsule.`);
|
|
2461
|
+
}
|
|
2462
|
+
const capsule = {
|
|
2463
|
+
...recovery.prepared.capsule,
|
|
2464
|
+
captureProvenance: {
|
|
2465
|
+
...recovery.prepared.capsule.captureProvenance,
|
|
2466
|
+
taskReceiptHash: completion.receiptHash,
|
|
2467
|
+
},
|
|
2468
|
+
};
|
|
2469
|
+
assertCapsuleIntegrity(capsule);
|
|
2470
|
+
if (recoveredTaskPolicyWasSuperseded(capsule, recoveryPolicy)) {
|
|
2471
|
+
const supersededVault = await LocalVault.open({
|
|
2472
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2473
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2474
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2475
|
+
});
|
|
2476
|
+
try {
|
|
2477
|
+
const existing = supersededVault.getCapsuleMetadata(capsule.trajectoryId, capsule.revision);
|
|
2478
|
+
if (!existing) {
|
|
2479
|
+
await supersededVault.commitCapture({
|
|
2480
|
+
raw: { plaintext: rawTurn, kind: 'raw-provider-turn', expectedContentId: recovery.prepared.rawContentId },
|
|
2481
|
+
capsule: {
|
|
2482
|
+
plaintext: Buffer.from(JSON.stringify(capsule)), trajectoryId: capsule.trajectoryId,
|
|
2483
|
+
revision: capsule.revision, capsuleHash: capsule.capsuleHash,
|
|
2484
|
+
},
|
|
2485
|
+
session: {
|
|
2486
|
+
sessionId: recovery.prepared.session.sessionId,
|
|
2487
|
+
provider: recovery.prepared.session.provider,
|
|
2488
|
+
workspaceId: recovery.workspaceId,
|
|
2489
|
+
sourceLocator: recovery.prepared.session.sourcePath,
|
|
2490
|
+
status: recovery.prepared.session.coverage,
|
|
2491
|
+
observedAt: recovery.prepared.session.endedAt,
|
|
2492
|
+
},
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
else if (existing.capsuleHash !== capsule.capsuleHash) {
|
|
2496
|
+
throw new Error('Recovered task evidence already exists with different immutable content.');
|
|
2497
|
+
}
|
|
2498
|
+
supersededVault.discardPendingCapsuleSync(capsule.trajectoryId, capsule.revision, 'policy_revision_superseded');
|
|
2499
|
+
}
|
|
2500
|
+
finally {
|
|
2501
|
+
supersededVault.close();
|
|
2502
|
+
}
|
|
2503
|
+
await fabric.acknowledgeRecoveredTaskCompletion(completion.taskId, completion.receiptHash);
|
|
2504
|
+
const acknowledgedVault = await LocalVault.open({
|
|
2505
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2506
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2507
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2508
|
+
});
|
|
2509
|
+
try {
|
|
2510
|
+
await acknowledgedVault.clearTaskCompletionRecovery(completion.taskId);
|
|
2511
|
+
}
|
|
2512
|
+
finally {
|
|
2513
|
+
acknowledgedVault.close();
|
|
2514
|
+
}
|
|
2515
|
+
finalized.push({
|
|
2516
|
+
taskId: completion.taskId,
|
|
2517
|
+
trajectory: {
|
|
2518
|
+
ok: false,
|
|
2519
|
+
status: 'withheld',
|
|
2520
|
+
reason: 'policy_revision_superseded',
|
|
2521
|
+
trajectoryId: capsule.trajectoryId,
|
|
2522
|
+
revision: capsule.revision,
|
|
2523
|
+
},
|
|
2524
|
+
});
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
const trajectory = await syncSignedTaskTrajectory({
|
|
2528
|
+
fabric, policy: recoveryPolicy, workspace,
|
|
2529
|
+
prepared: {
|
|
2530
|
+
capsule,
|
|
2531
|
+
rawTurn,
|
|
2532
|
+
rawContentId: recovery.prepared.rawContentId,
|
|
2533
|
+
session: recovery.prepared.session,
|
|
2534
|
+
},
|
|
2535
|
+
});
|
|
2536
|
+
await fabric.acknowledgeRecoveredTaskCompletion(completion.taskId, completion.receiptHash);
|
|
2537
|
+
const cleanupVault = await LocalVault.open({
|
|
2538
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2539
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2540
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2541
|
+
});
|
|
2542
|
+
try {
|
|
2543
|
+
await cleanupVault.clearTaskCompletionRecovery(completion.taskId);
|
|
2544
|
+
}
|
|
2545
|
+
finally {
|
|
2546
|
+
cleanupVault.close();
|
|
2547
|
+
}
|
|
2548
|
+
finalized.push({ taskId: completion.taskId, trajectory });
|
|
2549
|
+
}
|
|
2550
|
+
return finalized;
|
|
2551
|
+
}
|
|
2552
|
+
async function executeOneTask(fabric, leaseSeconds) {
|
|
2129
2553
|
const polled = await fabric.pollTask(leaseSeconds);
|
|
2130
2554
|
const taskRow = polled.task;
|
|
2131
2555
|
if (!taskRow?.envelope)
|
|
@@ -2134,54 +2558,113 @@ async function executeOneTask(fabric, policy, leaseSeconds) {
|
|
|
2134
2558
|
const workspace = (await registry()).find((item) => item.workspaceId === task.workspaceId);
|
|
2135
2559
|
if (!workspace)
|
|
2136
2560
|
throw new Error('Task workspace is not registered on this device.');
|
|
2561
|
+
const taskPolicy = await refreshVerifiedWorkspacePolicyForTransmission(resolve(workspace.path, '.dharma', 'approved-policy.json'), workspace.workspaceId, fabric);
|
|
2562
|
+
assertTaskWorkspacePolicy({ task, workspace, policy: taskPolicy });
|
|
2137
2563
|
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2564
|
+
const waitingHeartbeats = [];
|
|
2565
|
+
const waitingHeartbeat = setInterval(() => {
|
|
2566
|
+
waitingHeartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', {
|
|
2567
|
+
taskId: task.taskId,
|
|
2568
|
+
phase: 'waiting_for_skill_activation_lock',
|
|
2569
|
+
}).catch(() => undefined));
|
|
2570
|
+
}, Math.max(5_000, Math.floor(leaseSeconds * 500)));
|
|
2142
2571
|
try {
|
|
2143
|
-
|
|
2572
|
+
return await withWorkspaceSkillActivationLock(task.workspaceId, task.target.provider, async () => {
|
|
2573
|
+
if (task.target.deviceId !== config.deviceId)
|
|
2574
|
+
throw new Error('Task target does not match this enrolled device.');
|
|
2575
|
+
const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
|
|
2576
|
+
const activeSkill = await activeSkillAuthorization(task.target.provider, task.workspaceId, String(workspace.repositoryAgentId || ''), config);
|
|
2577
|
+
const activeBundleId = activeSkill?.bundleId ?? null;
|
|
2578
|
+
try {
|
|
2579
|
+
assertTaskSkillPin(task.skillBundle, activeBundleId);
|
|
2580
|
+
}
|
|
2581
|
+
catch (error) {
|
|
2582
|
+
await fabric.postTaskEvent(task.taskId, 'failed', {
|
|
2583
|
+
phase: 'preflight',
|
|
2584
|
+
code: taskSkillPinFailureCode(error),
|
|
2585
|
+
taskBundleId: task.skillBundle?.bundleId || null,
|
|
2586
|
+
localBundleId: activeBundleId,
|
|
2587
|
+
}).catch(() => undefined);
|
|
2588
|
+
throw error;
|
|
2589
|
+
}
|
|
2590
|
+
await fabric.postTaskEvent(task.taskId, 'started', {
|
|
2591
|
+
bundleId: task.skillBundle?.bundleId || null,
|
|
2592
|
+
bundleHash: task.skillBundle?.bundleHash || null,
|
|
2593
|
+
});
|
|
2594
|
+
const heartbeats = [];
|
|
2595
|
+
const heartbeat = setInterval(() => {
|
|
2596
|
+
heartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', { taskId: task.taskId }).catch(() => undefined));
|
|
2597
|
+
}, Math.max(15_000, Math.floor(leaseSeconds * 500)));
|
|
2598
|
+
let receipt;
|
|
2599
|
+
try {
|
|
2600
|
+
receipt = await executeTask({
|
|
2601
|
+
task, policy: taskPolicy, workspace: workspace.path, relayStateDirectory: resolve(dharmaHome(), 'relay'), serverPublicKey,
|
|
2602
|
+
receiptStore: new FileTaskReceiptStore(resolve(dharmaHome(), 'relay', 'receipts')),
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
finally {
|
|
2606
|
+
clearInterval(heartbeat);
|
|
2607
|
+
await Promise.allSettled(heartbeats);
|
|
2608
|
+
}
|
|
2609
|
+
const summary = {
|
|
2610
|
+
status: receipt.status, branch: receipt.branch,
|
|
2611
|
+
response: taskResponsePreview(receipt),
|
|
2612
|
+
commandResults: receipt.commandResults.map(({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 }) => ({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 })),
|
|
2613
|
+
startedAt: receipt.startedAt, completedAt: receipt.completedAt,
|
|
2614
|
+
};
|
|
2615
|
+
const collectedAt = receipt.completedAt;
|
|
2616
|
+
const prepared = receipt.status === 'completed' && task.skillBundle
|
|
2617
|
+
? prepareSignedTaskTrajectory({
|
|
2618
|
+
policy: taskPolicy, task, receipt, taskReceiptHash: `sha256:${'0'.repeat(64)}`, collectedAt,
|
|
2619
|
+
workspace, device: config, activeSkill,
|
|
2620
|
+
// The receipt is durable and retry-idempotent. Its start time is after
|
|
2621
|
+
// the first successful local bundle verification, so it is also the
|
|
2622
|
+
// stable verification timestamp for a recovered trajectory.
|
|
2623
|
+
activeSkillVerifiedAt: receipt.startedAt,
|
|
2624
|
+
})
|
|
2625
|
+
: null;
|
|
2626
|
+
if (prepared)
|
|
2627
|
+
await stageSignedTaskTrajectoryRecovery(task.taskId, workspace.workspaceId, taskPolicy, prepared);
|
|
2628
|
+
await fabric.postTaskEvent(task.taskId, receipt.status, {
|
|
2629
|
+
...summary,
|
|
2630
|
+
...(prepared ? { trajectoryCapsuleHash: prepared.capsule.capsuleHash } : {}),
|
|
2631
|
+
});
|
|
2632
|
+
let trajectory = null;
|
|
2633
|
+
if (receipt.status === 'completed' && task.skillBundle) {
|
|
2634
|
+
const finalized = await finalizeRecoveredSignedTaskTrajectories(fabric, taskPolicy, task.taskId);
|
|
2635
|
+
trajectory = finalized[0]?.trajectory || null;
|
|
2636
|
+
if (!trajectory)
|
|
2637
|
+
throw new Error('Task completion did not return a recoverable server receipt.');
|
|
2638
|
+
}
|
|
2639
|
+
return { ok: true, taskId: task.taskId, receipt: summary, trajectory };
|
|
2640
|
+
});
|
|
2144
2641
|
}
|
|
2145
2642
|
catch (error) {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
}
|
|
2643
|
+
if (error instanceof Error && error.message === 'Timed out waiting for the workspace skill activation lock.') {
|
|
2644
|
+
await fabric.postTaskEvent(task.taskId, 'failed', {
|
|
2645
|
+
phase: 'coordination',
|
|
2646
|
+
code: 'skill_activation_lock_timeout',
|
|
2647
|
+
}).catch(() => undefined);
|
|
2648
|
+
}
|
|
2152
2649
|
throw error;
|
|
2153
2650
|
}
|
|
2154
|
-
await fabric.postTaskEvent(task.taskId, 'started', {
|
|
2155
|
-
bundleId: task.skillBundle?.bundleId || null,
|
|
2156
|
-
bundleHash: task.skillBundle?.bundleHash || null,
|
|
2157
|
-
});
|
|
2158
|
-
const heartbeats = [];
|
|
2159
|
-
const heartbeat = setInterval(() => {
|
|
2160
|
-
heartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', { taskId: task.taskId }).catch(() => undefined));
|
|
2161
|
-
}, Math.max(15_000, Math.floor(leaseSeconds * 500)));
|
|
2162
|
-
let receipt;
|
|
2163
|
-
try {
|
|
2164
|
-
receipt = await executeTask({
|
|
2165
|
-
task, policy, workspace: workspace.path, relayStateDirectory: resolve(dharmaHome(), 'relay'), serverPublicKey,
|
|
2166
|
-
receiptStore: new FileTaskReceiptStore(resolve(dharmaHome(), 'relay', 'receipts')),
|
|
2167
|
-
});
|
|
2168
|
-
}
|
|
2169
2651
|
finally {
|
|
2170
|
-
clearInterval(
|
|
2171
|
-
await Promise.allSettled(
|
|
2172
|
-
}
|
|
2173
|
-
const summary = {
|
|
2174
|
-
status: receipt.status, branch: receipt.branch,
|
|
2175
|
-
response: taskResponsePreview(receipt),
|
|
2176
|
-
commandResults: receipt.commandResults.map(({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 }) => ({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 })),
|
|
2177
|
-
startedAt: receipt.startedAt, completedAt: receipt.completedAt,
|
|
2178
|
-
};
|
|
2179
|
-
await fabric.postTaskEvent(task.taskId, receipt.status, summary);
|
|
2180
|
-
return { ok: true, taskId: task.taskId, receipt: summary };
|
|
2652
|
+
clearInterval(waitingHeartbeat);
|
|
2653
|
+
await Promise.allSettled(waitingHeartbeats);
|
|
2654
|
+
}
|
|
2181
2655
|
}
|
|
2182
2656
|
async function runOneTask(flags) {
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2657
|
+
const policyPath = resolve(required(flags, 'policy'));
|
|
2658
|
+
const requestedWorkspaceId = typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : undefined;
|
|
2659
|
+
const selectedWorkspace = (await registry()).find((item) => (resolve(item.path, '.dharma', 'approved-policy.json') === policyPath
|
|
2660
|
+
&& (!requestedWorkspaceId || item.workspaceId === requestedWorkspaceId)));
|
|
2661
|
+
if (!selectedWorkspace)
|
|
2662
|
+
throw new Error('Task policy must be the canonical policy of one registered workspace.');
|
|
2663
|
+
const policy = await loadVerifiedWorkspacePolicy(policyPath, selectedWorkspace.workspaceId);
|
|
2664
|
+
const fabric = await client();
|
|
2665
|
+
const recoveredTaskTrajectories = await finalizeRecoveredSignedTaskTrajectories(fabric, policy);
|
|
2666
|
+
const result = await executeOneTask(fabric, Number(flags.get('lease-seconds') || 120));
|
|
2667
|
+
return recoveredTaskTrajectories.length ? { ...result, recoveredTaskTrajectories } : result;
|
|
2185
2668
|
}
|
|
2186
2669
|
export function nativeSkillDirectory(provider, env = process.env, home = homedir()) {
|
|
2187
2670
|
if (provider === 'codex')
|
|
@@ -2272,12 +2755,19 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
2272
2755
|
const repositoryInstalled = await pathExists(repositorySkillPath) && await pathExists(connectionPath);
|
|
2273
2756
|
const nativeInstalled = await pathExists(nativeSkillPath) && await pathExists(nativeMarkerPath);
|
|
2274
2757
|
let workspaceId;
|
|
2758
|
+
let organizationAgentId;
|
|
2275
2759
|
try {
|
|
2276
2760
|
const connection = JSON.parse(await readFile(connectionPath, 'utf8'));
|
|
2277
|
-
if (typeof connection.workspaceId === 'string')
|
|
2761
|
+
if (typeof connection.workspaceId === 'string') {
|
|
2278
2762
|
workspaceId = connection.workspaceId;
|
|
2763
|
+
organizationAgentId = (await registry()).find((item) => item.workspaceId === workspaceId)?.repositoryAgentId || undefined;
|
|
2764
|
+
}
|
|
2279
2765
|
}
|
|
2280
2766
|
catch { }
|
|
2767
|
+
const config = await readDeviceConfig();
|
|
2768
|
+
const activeBundleId = workspaceId && organizationAgentId && config
|
|
2769
|
+
? (await activeSkillAuthorization(input.provider, workspaceId, organizationAgentId, config))?.bundleId ?? null
|
|
2770
|
+
: null;
|
|
2281
2771
|
return {
|
|
2282
2772
|
provider: input.provider,
|
|
2283
2773
|
ready: repositoryInstalled && nativeInstalled,
|
|
@@ -2287,7 +2777,7 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
2287
2777
|
connectionPath,
|
|
2288
2778
|
nativeSkillPath,
|
|
2289
2779
|
workspaceId: workspaceId || null,
|
|
2290
|
-
activeBundleId
|
|
2780
|
+
activeBundleId,
|
|
2291
2781
|
activation: 'next_session',
|
|
2292
2782
|
nextAction: repositoryInstalled && nativeInstalled
|
|
2293
2783
|
? `Start a new ${input.provider} session from ${workspace} and invoke the dharma-agent-fabric skill.`
|
|
@@ -2359,79 +2849,181 @@ export async function materializeInlineSkillFiles(bundle, sourceRoot) {
|
|
|
2359
2849
|
}
|
|
2360
2850
|
return true;
|
|
2361
2851
|
}
|
|
2852
|
+
export async function recoverLegacySkillBundleIdAfterAuthorizationFailure(input) {
|
|
2853
|
+
const legacyBundleId = await getLegacySkillBundleIdForUpgrade({
|
|
2854
|
+
nativeSkillDirectory: input.nativeSkillDirectory,
|
|
2855
|
+
workspaceId: input.workspaceId,
|
|
2856
|
+
});
|
|
2857
|
+
if (!legacyBundleId)
|
|
2858
|
+
throw input.authorizationError;
|
|
2859
|
+
return legacyBundleId;
|
|
2860
|
+
}
|
|
2861
|
+
export async function installedBundleIdForSkillPollAfterAuthorizationFailure(input) {
|
|
2862
|
+
await recoverLegacySkillBundleIdAfterAuthorizationFailure(input);
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2362
2865
|
async function skillSync(flags) {
|
|
2363
2866
|
const workspaceId = required(flags, 'workspace-id');
|
|
2364
2867
|
const providerValue = required(flags, 'provider');
|
|
2365
2868
|
if (!['codex', 'claude', 'agy'].includes(providerValue))
|
|
2366
2869
|
throw new Error('Skill provider must be codex, claude, or agy.');
|
|
2367
2870
|
const provider = providerValue;
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
if (commits.length !== 1 || !/^[a-f0-9]{40,64}$/i.test(commits[0]))
|
|
2394
|
-
throw new Error('Skill bundle must pin one full Git commit.');
|
|
2395
|
-
if (repositories.length !== 1 || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(repositories[0])) {
|
|
2396
|
-
throw new Error('Skill bundle must pin one credential-free GitHub repository.');
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
|
|
2400
|
-
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
2401
|
-
verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
|
|
2402
|
-
await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
|
|
2403
|
-
await rm(sourceRoot, { recursive: true, force: true });
|
|
2404
|
-
await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
|
|
2405
|
-
const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
|
|
2406
|
-
if (bundle.operation === 'install' && !materializedInline) {
|
|
2407
|
-
await rm(sourceRoot, { recursive: true, force: true });
|
|
2408
|
-
await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
|
|
2409
|
-
await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
|
|
2410
|
-
await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
|
|
2411
|
-
}
|
|
2412
|
-
try {
|
|
2413
|
-
const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
|
|
2414
|
-
const receipt = await installSkillBundle({
|
|
2415
|
-
bundle,
|
|
2416
|
-
sourceDirectory: sourceRoot,
|
|
2417
|
-
nativeSkillDirectory: destination,
|
|
2418
|
-
policy,
|
|
2419
|
-
serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
|
|
2420
|
-
devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
|
|
2421
|
-
deviceId: config.deviceId,
|
|
2871
|
+
return withWorkspaceSkillActivationLock(workspaceId, provider, async () => {
|
|
2872
|
+
const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
|
|
2873
|
+
if (!workspace)
|
|
2874
|
+
throw new Error('Skill workspace is not registered locally.');
|
|
2875
|
+
const policy = await loadOrganizationPolicy(required(flags, 'policy'));
|
|
2876
|
+
const destination = nativeSkillDirectory(provider);
|
|
2877
|
+
const fabric = await client();
|
|
2878
|
+
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
2879
|
+
let activeBundleId;
|
|
2880
|
+
try {
|
|
2881
|
+
activeBundleId = (await activeSkillAuthorization(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
|
|
2882
|
+
}
|
|
2883
|
+
catch (error) {
|
|
2884
|
+
if (error instanceof Error && error.message === 'Skill bundle has expired.') {
|
|
2885
|
+
activeBundleId = (await expiredSkillAuthorizationForReplacement(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
activeBundleId = await installedBundleIdForSkillPollAfterAuthorizationFailure({
|
|
2889
|
+
nativeSkillDirectory: destination,
|
|
2890
|
+
workspaceId,
|
|
2891
|
+
authorizationError: error,
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
const response = await fabric.pollSkill({
|
|
2422
2896
|
workspaceId,
|
|
2423
2897
|
provider,
|
|
2424
|
-
|
|
2425
|
-
organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
|
|
2898
|
+
installedBundleId: activeBundleId,
|
|
2426
2899
|
});
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2900
|
+
const rollout = response.rollout;
|
|
2901
|
+
if (!rollout)
|
|
2902
|
+
return { ok: true, rollout: null, changed: false };
|
|
2903
|
+
if (typeof rollout.id !== 'string' || !rollout.bundle || typeof rollout.bundle !== 'object')
|
|
2904
|
+
throw new Error('Skill rollout response is invalid.');
|
|
2905
|
+
const bundle = rollout.bundle;
|
|
2906
|
+
if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
|
|
2907
|
+
|| (bundle.operation === 'install' && bundle.skills.length === 0)
|
|
2908
|
+
|| (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
|
|
2909
|
+
throw new Error('Skill bundle does not match local organization policy.');
|
|
2910
|
+
}
|
|
2911
|
+
const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
|
|
2912
|
+
const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
|
|
2913
|
+
if (bundle.operation === 'install') {
|
|
2914
|
+
if (commits.length !== 1 || !/^[a-f0-9]{40,64}$/i.test(commits[0]))
|
|
2915
|
+
throw new Error('Skill bundle must pin one full Git commit.');
|
|
2916
|
+
if (repositories.length !== 1 || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(repositories[0])) {
|
|
2917
|
+
throw new Error('Skill bundle must pin one credential-free GitHub repository.');
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
|
|
2921
|
+
verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
|
|
2922
|
+
await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
|
|
2433
2923
|
await rm(sourceRoot, { recursive: true, force: true });
|
|
2434
|
-
|
|
2924
|
+
await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
|
|
2925
|
+
const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
|
|
2926
|
+
if (bundle.operation === 'install' && !materializedInline) {
|
|
2927
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
2928
|
+
await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
|
|
2929
|
+
await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
|
|
2930
|
+
await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
|
|
2931
|
+
}
|
|
2932
|
+
try {
|
|
2933
|
+
const previousAnchor = await loadActiveSkillAuthorizationAnchor({
|
|
2934
|
+
config,
|
|
2935
|
+
workspaceId,
|
|
2936
|
+
organizationAgentId: String(workspace.repositoryAgentId || ''),
|
|
2937
|
+
provider,
|
|
2938
|
+
});
|
|
2939
|
+
const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
|
|
2940
|
+
const receipt = await installSkillBundle({
|
|
2941
|
+
bundle,
|
|
2942
|
+
sourceDirectory: sourceRoot,
|
|
2943
|
+
nativeSkillDirectory: destination,
|
|
2944
|
+
policy,
|
|
2945
|
+
serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
|
|
2946
|
+
devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
|
|
2947
|
+
deviceId: config.deviceId,
|
|
2948
|
+
organizationAgentId: String(workspace.repositoryAgentId || ''),
|
|
2949
|
+
workspaceId,
|
|
2950
|
+
provider,
|
|
2951
|
+
smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
|
|
2952
|
+
organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
|
|
2953
|
+
});
|
|
2954
|
+
const restoreAnchor = async () => {
|
|
2955
|
+
if (previousAnchor) {
|
|
2956
|
+
await saveActiveSkillAuthorizationAnchor({
|
|
2957
|
+
config,
|
|
2958
|
+
workspaceId,
|
|
2959
|
+
organizationAgentId: previousAnchor.organizationAgentId,
|
|
2960
|
+
provider,
|
|
2961
|
+
bundleId: previousAnchor.bundleId,
|
|
2962
|
+
receiptHash: previousAnchor.receiptHash,
|
|
2963
|
+
activatedAt: previousAnchor.activatedAt,
|
|
2964
|
+
expiresAt: previousAnchor.expiresAt,
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
else {
|
|
2968
|
+
await deleteActiveSkillAuthorizationAnchor({ config, workspaceId, provider });
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
const recoverLocalInstallation = async (error) => {
|
|
2972
|
+
const recoveryErrors = [];
|
|
2973
|
+
try {
|
|
2974
|
+
await rollbackUnconfirmedSkillBundle({
|
|
2975
|
+
nativeSkillDirectory: destination,
|
|
2976
|
+
workspaceId,
|
|
2977
|
+
receipt,
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
catch (rollbackError) {
|
|
2981
|
+
recoveryErrors.push(rollbackError);
|
|
2982
|
+
}
|
|
2983
|
+
try {
|
|
2984
|
+
await restoreAnchor();
|
|
2985
|
+
}
|
|
2986
|
+
catch (anchorError) {
|
|
2987
|
+
recoveryErrors.push(anchorError);
|
|
2988
|
+
}
|
|
2989
|
+
if (recoveryErrors.length) {
|
|
2990
|
+
throw new AggregateError([error, ...recoveryErrors], 'Skill installation failed and local recovery was incomplete.');
|
|
2991
|
+
}
|
|
2992
|
+
throw error;
|
|
2993
|
+
};
|
|
2994
|
+
try {
|
|
2995
|
+
if (receipt.status === 'active') {
|
|
2996
|
+
await saveActiveSkillAuthorizationAnchor({
|
|
2997
|
+
config,
|
|
2998
|
+
workspaceId,
|
|
2999
|
+
organizationAgentId: String(workspace.repositoryAgentId || ''),
|
|
3000
|
+
provider,
|
|
3001
|
+
bundleId: receipt.bundleId,
|
|
3002
|
+
receiptHash: receipt.receiptHash,
|
|
3003
|
+
activatedAt: receipt.completedAt,
|
|
3004
|
+
expiresAt: bundle.expiresAt ?? null,
|
|
3005
|
+
});
|
|
3006
|
+
if (provider === 'agy')
|
|
3007
|
+
await activateAgyPlugin();
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
catch (error) {
|
|
3011
|
+
await recoverLocalInstallation(error);
|
|
3012
|
+
}
|
|
3013
|
+
try {
|
|
3014
|
+
await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
|
|
3015
|
+
}
|
|
3016
|
+
catch (error) {
|
|
3017
|
+
if (isDefinitiveAgentFabricRejection(error))
|
|
3018
|
+
await recoverLocalInstallation(error);
|
|
3019
|
+
throw error;
|
|
3020
|
+
}
|
|
3021
|
+
return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
|
|
3022
|
+
}
|
|
3023
|
+
finally {
|
|
3024
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
2435
3027
|
}
|
|
2436
3028
|
async function relayStart(flags) {
|
|
2437
3029
|
const policyPath = resolve(required(flags, 'policy'));
|
|
@@ -2450,10 +3042,19 @@ async function relayStart(flags) {
|
|
|
2450
3042
|
process.once('SIGINT', stop);
|
|
2451
3043
|
process.once('SIGTERM', stop);
|
|
2452
3044
|
let tasksCompleted = 0;
|
|
3045
|
+
let taskTrajectoriesRecovered = 0;
|
|
2453
3046
|
let evidenceResponsesCompleted = 0;
|
|
3047
|
+
let trajectorySyncsCompleted = 0;
|
|
2454
3048
|
let nextPolicyRefreshAt = 0;
|
|
2455
3049
|
let evidencePolicyFresh = false;
|
|
3050
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
3051
|
+
const vault = await LocalVault.open({
|
|
3052
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
3053
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
3054
|
+
rawLocalDays: rawLocalRetentionDays(policy),
|
|
3055
|
+
});
|
|
2456
3056
|
try {
|
|
3057
|
+
taskTrajectoriesRecovered += (await finalizeRecoveredSignedTaskTrajectories(fabric, policy)).length;
|
|
2457
3058
|
do {
|
|
2458
3059
|
if (Date.now() >= nextPolicyRefreshAt) {
|
|
2459
3060
|
try {
|
|
@@ -2468,12 +3069,13 @@ async function relayStart(flags) {
|
|
|
2468
3069
|
}
|
|
2469
3070
|
let evidenceRequestId;
|
|
2470
3071
|
if (evidencePolicyFresh) {
|
|
3072
|
+
trajectorySyncsCompleted += await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
|
|
2471
3073
|
const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
|
|
2472
3074
|
evidenceRequestId = typeof evidence.requestId === 'string' ? evidence.requestId : undefined;
|
|
2473
3075
|
if (evidenceRequestId)
|
|
2474
3076
|
evidenceResponsesCompleted += 1;
|
|
2475
3077
|
}
|
|
2476
|
-
const result = await executeOneTask(fabric,
|
|
3078
|
+
const result = await executeOneTask(fabric, leaseSeconds);
|
|
2477
3079
|
if (result.taskId)
|
|
2478
3080
|
tasksCompleted += 1;
|
|
2479
3081
|
if (flags.has('once'))
|
|
@@ -2483,11 +3085,15 @@ async function relayStart(flags) {
|
|
|
2483
3085
|
} while (!stopping);
|
|
2484
3086
|
}
|
|
2485
3087
|
finally {
|
|
3088
|
+
vault.close();
|
|
2486
3089
|
process.removeListener('SIGINT', stop);
|
|
2487
3090
|
process.removeListener('SIGTERM', stop);
|
|
2488
3091
|
await rm(pidPath, { force: true });
|
|
2489
3092
|
}
|
|
2490
|
-
return {
|
|
3093
|
+
return {
|
|
3094
|
+
ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
|
|
3095
|
+
evidenceResponsesCompleted, trajectorySyncsCompleted,
|
|
3096
|
+
};
|
|
2491
3097
|
}
|
|
2492
3098
|
export async function run(argv) {
|
|
2493
3099
|
const { positional, flags, repeated } = parseCliOptions(argv);
|
|
@@ -2569,10 +3175,14 @@ export async function run(argv) {
|
|
|
2569
3175
|
throw new Error('Skill provider must be codex, claude, or agy.');
|
|
2570
3176
|
const root = nativeSkillDirectory(providerValue);
|
|
2571
3177
|
const workspaceId = required(flags, 'workspace-id');
|
|
3178
|
+
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
3179
|
+
const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
|
|
3180
|
+
if (!workspace)
|
|
3181
|
+
throw new Error('Skill workspace is not registered locally.');
|
|
2572
3182
|
return {
|
|
2573
3183
|
provider: providerValue,
|
|
2574
3184
|
workspaceId,
|
|
2575
|
-
activeBundleId: await
|
|
3185
|
+
activeBundleId: (await activeSkillAuthorization(providerValue, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null,
|
|
2576
3186
|
nativeSkillDirectory: root,
|
|
2577
3187
|
};
|
|
2578
3188
|
}
|