@dharma-ai-labs/agent-fabric 0.1.26 → 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.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 { getActiveSkillBundleId, installSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
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.26';
19
+ const VERSION = '0.2.0';
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
- if (resolve(policyPath) !== canonicalPolicyPath) {
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
- const { capsuleHash: _capsuleHash, ...unsigned } = capsule;
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
- || capsule.taskId !== null
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 !== null || eventRecord.providerModel !== null) {
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;
@@ -919,6 +970,8 @@ function responseTextFromEvent(value) {
919
970
  return item.text;
920
971
  if (event.type === 'result' && typeof event.result === 'string')
921
972
  return event.result;
973
+ if (event.status === 'SUCCESS' && typeof event.response === 'string')
974
+ return event.response;
922
975
  const message = event.message && typeof event.message === 'object' && !Array.isArray(event.message)
923
976
  ? event.message
924
977
  : {};
@@ -1095,16 +1148,36 @@ async function runOrganizationCommand(command, subcommand, flags) {
1095
1148
  if (command === 'remediations' && subcommand === 'list')
1096
1149
  return api.listRemediations();
1097
1150
  if (command === 'remediations' && subcommand === 'act') {
1098
- requireExplicitConfirmation(flags, 'Changing a repository remediation release');
1099
1151
  const targetId = required(flags, 'target-id');
1100
1152
  const body = await commandJsonBody(flags);
1101
1153
  const action = String(flags.get('action') || body.action || '');
1102
- if (!['run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
1103
- 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.');
1104
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');
1105
1174
  return api.transitionRemediationTarget(targetId, {
1106
- ...body,
1107
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
+ : {}),
1108
1181
  });
1109
1182
  }
1110
1183
  if (command === 'skills' && subcommand === 'list')
@@ -1196,6 +1269,7 @@ async function login(flags) {
1196
1269
  serverPublicKeyEd25519: result.serverPublicKeyEd25519, relayUrl: result.relayUrl, enrolledAt: new Date().toISOString(),
1197
1270
  };
1198
1271
  await saveDeviceConfig(configPath(), config);
1272
+ await saveDeviceEnrollmentAnchor({ config });
1199
1273
  await rm(pendingEnrollmentPath(), { force: true });
1200
1274
  return { ok: true, status: 'approved', deviceId: config.deviceId, organizationId: pending.organizationId, relayUrl: config.relayUrl };
1201
1275
  }
@@ -1242,6 +1316,11 @@ async function capture(flags, batch = false) {
1242
1316
  const registered = (await registry()).find((item) => item.path === workspace);
1243
1317
  if (!registered)
1244
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
+ });
1245
1324
  let policy = await loadVerifiedWorkspacePolicy(policyPath, registered.workspaceId);
1246
1325
  const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
1247
1326
  const fabric = flags.has('sync') ? await client() : null;
@@ -1263,6 +1342,7 @@ async function capture(flags, batch = false) {
1263
1342
  const firstRevision = buildTrajectoryCapsule({
1264
1343
  organizationId: device.organizationId, deviceId: device.deviceId, workspaceId: registered.workspaceId,
1265
1344
  session: selected, policy, rawContentId, rawBytes: rawTurn.byteLength, rawKind: 'raw-provider-turn',
1345
+ captureProvenance: captureProvenance(selected.endedAt),
1266
1346
  });
1267
1347
  const latestMetadata = vault.getLatestCapsuleMetadata(firstRevision.trajectoryId);
1268
1348
  const latestCapsule = latestMetadata
@@ -1279,9 +1359,10 @@ async function capture(flags, batch = false) {
1279
1359
  organizationId: device.organizationId, deviceId: device.deviceId, workspaceId: registered.workspaceId,
1280
1360
  session: selected, policy, rawContentId, rawBytes: rawTurn.byteLength, rawKind: 'raw-provider-turn',
1281
1361
  revision: latestMetadata.revision + 1, previousRevisionHash: latestMetadata.capsuleHash,
1362
+ captureProvenance: captureProvenance(selected.endedAt),
1282
1363
  })
1283
1364
  : firstRevision;
1284
- const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), 'https://schemas.dharma-ai.io/trajectory-capsule/v1', capsule);
1365
+ const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
1285
1366
  if (!validation.ok)
1286
1367
  throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
1287
1368
  if (!latestCapsule || latestCapsule.capsuleHash !== capsule.capsuleHash) {
@@ -1369,6 +1450,11 @@ async function evidencePreview(flags) {
1369
1450
  if (!registered)
1370
1451
  throw new Error('Workspace is not registered locally. Run dharma workspace add.');
1371
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
+ });
1372
1458
  const capsules = sessions.map((session) => {
1373
1459
  const rawTurn = Buffer.from(`${session.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
1374
1460
  return buildTrajectoryCapsule({
@@ -1380,6 +1466,7 @@ async function evidencePreview(flags) {
1380
1466
  rawContentId: sha256(rawTurn),
1381
1467
  rawBytes: rawTurn.byteLength,
1382
1468
  rawKind: 'raw-provider-turn',
1469
+ captureProvenance: captureProvenance(session.endedAt),
1383
1470
  });
1384
1471
  });
1385
1472
  automaticDisclosure = {
@@ -1690,6 +1777,73 @@ async function readDeviceConfig() {
1690
1777
  return null;
1691
1778
  }
1692
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
+ }
1693
1847
  async function loadVerifiedWorkspacePolicy(path, workspaceId) {
1694
1848
  const policy = await loadOrganizationPolicy(path);
1695
1849
  if (policy.evidence.automaticDisclosure?.mode !== 'customer_authorized_content')
@@ -1908,7 +2062,7 @@ async function onboard(flags) {
1908
2062
  async function evidenceSync(flags) {
1909
2063
  const capsule = JSON.parse(await readFile(resolve(required(flags, 'file')), 'utf8'));
1910
2064
  const workspaceId = typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : String(capsule.workspaceId || '');
1911
- const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), 'https://schemas.dharma-ai.io/trajectory-capsule/v1', capsule);
2065
+ const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
1912
2066
  if (!validation.ok)
1913
2067
  throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
1914
2068
  assertCapsuleIntegrity(capsule);
@@ -2123,7 +2277,279 @@ async function runOneEvidenceRequest(flags) {
2123
2277
  const policy = await refreshVerifiedWorkspacePolicyForTransmission(required(flags, 'policy'), workspaceId, fabric);
2124
2278
  return processEvidenceRequest(fabric, policy, workspaceId);
2125
2279
  }
2126
- async function executeOneTask(fabric, policy, leaseSeconds) {
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) {
2127
2553
  const polled = await fabric.pollTask(leaseSeconds);
2128
2554
  const taskRow = polled.task;
2129
2555
  if (!taskRow?.envelope)
@@ -2132,54 +2558,113 @@ async function executeOneTask(fabric, policy, leaseSeconds) {
2132
2558
  const workspace = (await registry()).find((item) => item.workspaceId === task.workspaceId);
2133
2559
  if (!workspace)
2134
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 });
2135
2563
  const config = JSON.parse(await readFile(configPath(), 'utf8'));
2136
- if (task.target.deviceId !== config.deviceId)
2137
- throw new Error('Task target does not match this enrolled device.');
2138
- const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
2139
- const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider), task.workspaceId);
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)));
2140
2571
  try {
2141
- assertTaskSkillPin(task.skillBundle, activeBundleId);
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
+ });
2142
2641
  }
2143
2642
  catch (error) {
2144
- await fabric.postTaskEvent(task.taskId, 'failed', {
2145
- phase: 'preflight',
2146
- code: taskSkillPinFailureCode(error),
2147
- taskBundleId: task.skillBundle?.bundleId || null,
2148
- localBundleId: activeBundleId,
2149
- }).catch(() => undefined);
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
+ }
2150
2649
  throw error;
2151
2650
  }
2152
- await fabric.postTaskEvent(task.taskId, 'started', {
2153
- bundleId: task.skillBundle?.bundleId || null,
2154
- bundleHash: task.skillBundle?.bundleHash || null,
2155
- });
2156
- const heartbeats = [];
2157
- const heartbeat = setInterval(() => {
2158
- heartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', { taskId: task.taskId }).catch(() => undefined));
2159
- }, Math.max(15_000, Math.floor(leaseSeconds * 500)));
2160
- let receipt;
2161
- try {
2162
- receipt = await executeTask({
2163
- task, policy, workspace: workspace.path, relayStateDirectory: resolve(dharmaHome(), 'relay'), serverPublicKey,
2164
- receiptStore: new FileTaskReceiptStore(resolve(dharmaHome(), 'relay', 'receipts')),
2165
- });
2166
- }
2167
2651
  finally {
2168
- clearInterval(heartbeat);
2169
- await Promise.allSettled(heartbeats);
2170
- }
2171
- const summary = {
2172
- status: receipt.status, branch: receipt.branch,
2173
- response: taskResponsePreview(receipt),
2174
- commandResults: receipt.commandResults.map(({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 }) => ({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 })),
2175
- startedAt: receipt.startedAt, completedAt: receipt.completedAt,
2176
- };
2177
- await fabric.postTaskEvent(task.taskId, receipt.status, summary);
2178
- return { ok: true, taskId: task.taskId, receipt: summary };
2652
+ clearInterval(waitingHeartbeat);
2653
+ await Promise.allSettled(waitingHeartbeats);
2654
+ }
2179
2655
  }
2180
2656
  async function runOneTask(flags) {
2181
- const policy = await loadVerifiedWorkspacePolicy(required(flags, 'policy'), typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : undefined);
2182
- return executeOneTask(await client(), policy, Number(flags.get('lease-seconds') || 120));
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;
2183
2668
  }
2184
2669
  export function nativeSkillDirectory(provider, env = process.env, home = homedir()) {
2185
2670
  if (provider === 'codex')
@@ -2270,12 +2755,19 @@ export async function verifyAgentFabricSkillInstallation(input) {
2270
2755
  const repositoryInstalled = await pathExists(repositorySkillPath) && await pathExists(connectionPath);
2271
2756
  const nativeInstalled = await pathExists(nativeSkillPath) && await pathExists(nativeMarkerPath);
2272
2757
  let workspaceId;
2758
+ let organizationAgentId;
2273
2759
  try {
2274
2760
  const connection = JSON.parse(await readFile(connectionPath, 'utf8'));
2275
- if (typeof connection.workspaceId === 'string')
2761
+ if (typeof connection.workspaceId === 'string') {
2276
2762
  workspaceId = connection.workspaceId;
2763
+ organizationAgentId = (await registry()).find((item) => item.workspaceId === workspaceId)?.repositoryAgentId || undefined;
2764
+ }
2277
2765
  }
2278
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;
2279
2771
  return {
2280
2772
  provider: input.provider,
2281
2773
  ready: repositoryInstalled && nativeInstalled,
@@ -2285,7 +2777,7 @@ export async function verifyAgentFabricSkillInstallation(input) {
2285
2777
  connectionPath,
2286
2778
  nativeSkillPath,
2287
2779
  workspaceId: workspaceId || null,
2288
- activeBundleId: workspaceId ? await getActiveSkillBundleId(nativeRoot, workspaceId) : null,
2780
+ activeBundleId,
2289
2781
  activation: 'next_session',
2290
2782
  nextAction: repositoryInstalled && nativeInstalled
2291
2783
  ? `Start a new ${input.provider} session from ${workspace} and invoke the dharma-agent-fabric skill.`
@@ -2357,79 +2849,177 @@ export async function materializeInlineSkillFiles(bundle, sourceRoot) {
2357
2849
  }
2358
2850
  return true;
2359
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
+ }
2360
2861
  async function skillSync(flags) {
2361
2862
  const workspaceId = required(flags, 'workspace-id');
2362
2863
  const providerValue = required(flags, 'provider');
2363
2864
  if (!['codex', 'claude', 'agy'].includes(providerValue))
2364
2865
  throw new Error('Skill provider must be codex, claude, or agy.');
2365
2866
  const provider = providerValue;
2366
- const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
2367
- if (!workspace)
2368
- throw new Error('Skill workspace is not registered locally.');
2369
- const policy = await loadOrganizationPolicy(required(flags, 'policy'));
2370
- const destination = nativeSkillDirectory(provider);
2371
- const fabric = await client();
2372
- const response = await fabric.pollSkill({
2373
- workspaceId,
2374
- provider,
2375
- installedBundleId: await getActiveSkillBundleId(destination, workspaceId),
2376
- });
2377
- const rollout = response.rollout;
2378
- if (!rollout)
2379
- return { ok: true, rollout: null, changed: false };
2380
- if (typeof rollout.id !== 'string' || !rollout.bundle || typeof rollout.bundle !== 'object')
2381
- throw new Error('Skill rollout response is invalid.');
2382
- const bundle = rollout.bundle;
2383
- if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
2384
- || (bundle.operation === 'install' && bundle.skills.length === 0)
2385
- || (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
2386
- throw new Error('Skill bundle does not match local organization policy.');
2387
- }
2388
- const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
2389
- const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
2390
- if (bundle.operation === 'install') {
2391
- if (commits.length !== 1 || !/^[a-f0-9]{40,64}$/i.test(commits[0]))
2392
- throw new Error('Skill bundle must pin one full Git commit.');
2393
- if (repositories.length !== 1 || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(repositories[0])) {
2394
- throw new Error('Skill bundle must pin one credential-free GitHub repository.');
2395
- }
2396
- }
2397
- const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
2398
- const config = JSON.parse(await readFile(configPath(), 'utf8'));
2399
- verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
2400
- await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
2401
- await rm(sourceRoot, { recursive: true, force: true });
2402
- await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
2403
- const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
2404
- if (bundle.operation === 'install' && !materializedInline) {
2405
- await rm(sourceRoot, { recursive: true, force: true });
2406
- await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
2407
- await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
2408
- await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
2409
- }
2410
- try {
2411
- const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
2412
- const receipt = await installSkillBundle({
2413
- bundle,
2414
- sourceDirectory: sourceRoot,
2415
- nativeSkillDirectory: destination,
2416
- policy,
2417
- serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
2418
- devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
2419
- deviceId: config.deviceId,
2867
+ return withWorkspaceSkillActivationLock(workspaceId, provider, async () => {
2868
+ const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
2869
+ if (!workspace)
2870
+ throw new Error('Skill workspace is not registered locally.');
2871
+ const policy = await loadOrganizationPolicy(required(flags, 'policy'));
2872
+ const destination = nativeSkillDirectory(provider);
2873
+ const fabric = await client();
2874
+ const config = JSON.parse(await readFile(configPath(), 'utf8'));
2875
+ let activeBundleId;
2876
+ try {
2877
+ activeBundleId = (await activeSkillAuthorization(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
2878
+ }
2879
+ catch (error) {
2880
+ if (error instanceof Error && error.message === 'Skill bundle has expired.') {
2881
+ activeBundleId = (await expiredSkillAuthorizationForReplacement(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
2882
+ }
2883
+ else {
2884
+ activeBundleId = await recoverLegacySkillBundleIdAfterAuthorizationFailure({
2885
+ nativeSkillDirectory: destination,
2886
+ workspaceId,
2887
+ authorizationError: error,
2888
+ });
2889
+ }
2890
+ }
2891
+ const response = await fabric.pollSkill({
2420
2892
  workspaceId,
2421
2893
  provider,
2422
- smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
2423
- organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
2894
+ installedBundleId: activeBundleId,
2424
2895
  });
2425
- if (provider === 'agy' && receipt.status === 'active')
2426
- await activateAgyPlugin();
2427
- await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
2428
- return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
2429
- }
2430
- finally {
2896
+ const rollout = response.rollout;
2897
+ if (!rollout)
2898
+ return { ok: true, rollout: null, changed: false };
2899
+ if (typeof rollout.id !== 'string' || !rollout.bundle || typeof rollout.bundle !== 'object')
2900
+ throw new Error('Skill rollout response is invalid.');
2901
+ const bundle = rollout.bundle;
2902
+ if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
2903
+ || (bundle.operation === 'install' && bundle.skills.length === 0)
2904
+ || (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
2905
+ throw new Error('Skill bundle does not match local organization policy.');
2906
+ }
2907
+ const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
2908
+ const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
2909
+ if (bundle.operation === 'install') {
2910
+ if (commits.length !== 1 || !/^[a-f0-9]{40,64}$/i.test(commits[0]))
2911
+ throw new Error('Skill bundle must pin one full Git commit.');
2912
+ if (repositories.length !== 1 || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(repositories[0])) {
2913
+ throw new Error('Skill bundle must pin one credential-free GitHub repository.');
2914
+ }
2915
+ }
2916
+ const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
2917
+ verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
2918
+ await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
2431
2919
  await rm(sourceRoot, { recursive: true, force: true });
2432
- }
2920
+ await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
2921
+ const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
2922
+ if (bundle.operation === 'install' && !materializedInline) {
2923
+ await rm(sourceRoot, { recursive: true, force: true });
2924
+ await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
2925
+ await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
2926
+ await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
2927
+ }
2928
+ try {
2929
+ const previousAnchor = await loadActiveSkillAuthorizationAnchor({
2930
+ config,
2931
+ workspaceId,
2932
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
2933
+ provider,
2934
+ });
2935
+ const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
2936
+ const receipt = await installSkillBundle({
2937
+ bundle,
2938
+ sourceDirectory: sourceRoot,
2939
+ nativeSkillDirectory: destination,
2940
+ policy,
2941
+ serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
2942
+ devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
2943
+ deviceId: config.deviceId,
2944
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
2945
+ workspaceId,
2946
+ provider,
2947
+ smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
2948
+ organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
2949
+ });
2950
+ const restoreAnchor = async () => {
2951
+ if (previousAnchor) {
2952
+ await saveActiveSkillAuthorizationAnchor({
2953
+ config,
2954
+ workspaceId,
2955
+ organizationAgentId: previousAnchor.organizationAgentId,
2956
+ provider,
2957
+ bundleId: previousAnchor.bundleId,
2958
+ receiptHash: previousAnchor.receiptHash,
2959
+ activatedAt: previousAnchor.activatedAt,
2960
+ expiresAt: previousAnchor.expiresAt,
2961
+ });
2962
+ }
2963
+ else {
2964
+ await deleteActiveSkillAuthorizationAnchor({ config, workspaceId, provider });
2965
+ }
2966
+ };
2967
+ const recoverLocalInstallation = async (error) => {
2968
+ const recoveryErrors = [];
2969
+ try {
2970
+ await rollbackUnconfirmedSkillBundle({
2971
+ nativeSkillDirectory: destination,
2972
+ workspaceId,
2973
+ receipt,
2974
+ });
2975
+ }
2976
+ catch (rollbackError) {
2977
+ recoveryErrors.push(rollbackError);
2978
+ }
2979
+ try {
2980
+ await restoreAnchor();
2981
+ }
2982
+ catch (anchorError) {
2983
+ recoveryErrors.push(anchorError);
2984
+ }
2985
+ if (recoveryErrors.length) {
2986
+ throw new AggregateError([error, ...recoveryErrors], 'Skill installation failed and local recovery was incomplete.');
2987
+ }
2988
+ throw error;
2989
+ };
2990
+ try {
2991
+ if (receipt.status === 'active') {
2992
+ await saveActiveSkillAuthorizationAnchor({
2993
+ config,
2994
+ workspaceId,
2995
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
2996
+ provider,
2997
+ bundleId: receipt.bundleId,
2998
+ receiptHash: receipt.receiptHash,
2999
+ activatedAt: receipt.completedAt,
3000
+ expiresAt: bundle.expiresAt ?? null,
3001
+ });
3002
+ if (provider === 'agy')
3003
+ await activateAgyPlugin();
3004
+ }
3005
+ }
3006
+ catch (error) {
3007
+ await recoverLocalInstallation(error);
3008
+ }
3009
+ try {
3010
+ await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
3011
+ }
3012
+ catch (error) {
3013
+ if (isDefinitiveAgentFabricRejection(error))
3014
+ await recoverLocalInstallation(error);
3015
+ throw error;
3016
+ }
3017
+ return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
3018
+ }
3019
+ finally {
3020
+ await rm(sourceRoot, { recursive: true, force: true });
3021
+ }
3022
+ });
2433
3023
  }
2434
3024
  async function relayStart(flags) {
2435
3025
  const policyPath = resolve(required(flags, 'policy'));
@@ -2448,10 +3038,19 @@ async function relayStart(flags) {
2448
3038
  process.once('SIGINT', stop);
2449
3039
  process.once('SIGTERM', stop);
2450
3040
  let tasksCompleted = 0;
3041
+ let taskTrajectoriesRecovered = 0;
2451
3042
  let evidenceResponsesCompleted = 0;
3043
+ let trajectorySyncsCompleted = 0;
2452
3044
  let nextPolicyRefreshAt = 0;
2453
3045
  let evidencePolicyFresh = false;
3046
+ const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
3047
+ const vault = await LocalVault.open({
3048
+ root: resolve(dharmaHome(), 'vault'),
3049
+ masterKey: await loadOrCreateVaultMasterKey(),
3050
+ rawLocalDays: rawLocalRetentionDays(policy),
3051
+ });
2454
3052
  try {
3053
+ taskTrajectoriesRecovered += (await finalizeRecoveredSignedTaskTrajectories(fabric, policy)).length;
2455
3054
  do {
2456
3055
  if (Date.now() >= nextPolicyRefreshAt) {
2457
3056
  try {
@@ -2466,12 +3065,13 @@ async function relayStart(flags) {
2466
3065
  }
2467
3066
  let evidenceRequestId;
2468
3067
  if (evidencePolicyFresh) {
3068
+ trajectorySyncsCompleted += await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
2469
3069
  const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
2470
3070
  evidenceRequestId = typeof evidence.requestId === 'string' ? evidence.requestId : undefined;
2471
3071
  if (evidenceRequestId)
2472
3072
  evidenceResponsesCompleted += 1;
2473
3073
  }
2474
- const result = await executeOneTask(fabric, policy, leaseSeconds);
3074
+ const result = await executeOneTask(fabric, leaseSeconds);
2475
3075
  if (result.taskId)
2476
3076
  tasksCompleted += 1;
2477
3077
  if (flags.has('once'))
@@ -2481,11 +3081,15 @@ async function relayStart(flags) {
2481
3081
  } while (!stopping);
2482
3082
  }
2483
3083
  finally {
3084
+ vault.close();
2484
3085
  process.removeListener('SIGINT', stop);
2485
3086
  process.removeListener('SIGTERM', stop);
2486
3087
  await rm(pidPath, { force: true });
2487
3088
  }
2488
- return { ok: true, stopped: true, tasksCompleted, evidenceResponsesCompleted };
3089
+ return {
3090
+ ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
3091
+ evidenceResponsesCompleted, trajectorySyncsCompleted,
3092
+ };
2489
3093
  }
2490
3094
  export async function run(argv) {
2491
3095
  const { positional, flags, repeated } = parseCliOptions(argv);
@@ -2567,10 +3171,14 @@ export async function run(argv) {
2567
3171
  throw new Error('Skill provider must be codex, claude, or agy.');
2568
3172
  const root = nativeSkillDirectory(providerValue);
2569
3173
  const workspaceId = required(flags, 'workspace-id');
3174
+ const config = JSON.parse(await readFile(configPath(), 'utf8'));
3175
+ const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
3176
+ if (!workspace)
3177
+ throw new Error('Skill workspace is not registered locally.');
2570
3178
  return {
2571
3179
  provider: providerValue,
2572
3180
  workspaceId,
2573
- activeBundleId: await getActiveSkillBundleId(root, workspaceId),
3181
+ activeBundleId: (await activeSkillAuthorization(providerValue, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null,
2574
3182
  nativeSkillDirectory: root,
2575
3183
  };
2576
3184
  }