@dharma-ai-labs/agent-fabric 0.1.27 → 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.27';
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;
@@ -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'), 'https://schemas.dharma-ai.io/trajectory-capsule/v1', capsule);
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'), 'https://schemas.dharma-ai.io/trajectory-capsule/v1', capsule);
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
- 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) {
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
- if (task.target.deviceId !== config.deviceId)
2139
- throw new Error('Task target does not match this enrolled device.');
2140
- const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
2141
- 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)));
2142
2571
  try {
2143
- 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
+ });
2144
2641
  }
2145
2642
  catch (error) {
2146
- await fabric.postTaskEvent(task.taskId, 'failed', {
2147
- phase: 'preflight',
2148
- code: taskSkillPinFailureCode(error),
2149
- taskBundleId: task.skillBundle?.bundleId || null,
2150
- localBundleId: activeBundleId,
2151
- }).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
+ }
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(heartbeat);
2171
- await Promise.allSettled(heartbeats);
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 policy = await loadVerifiedWorkspacePolicy(required(flags, 'policy'), typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : undefined);
2184
- 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;
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: workspaceId ? await getActiveSkillBundleId(nativeRoot, workspaceId) : null,
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,177 @@ 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
+ }
2362
2861
  async function skillSync(flags) {
2363
2862
  const workspaceId = required(flags, 'workspace-id');
2364
2863
  const providerValue = required(flags, 'provider');
2365
2864
  if (!['codex', 'claude', 'agy'].includes(providerValue))
2366
2865
  throw new Error('Skill provider must be codex, claude, or agy.');
2367
2866
  const provider = providerValue;
2368
- const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
2369
- if (!workspace)
2370
- throw new Error('Skill workspace is not registered locally.');
2371
- const policy = await loadOrganizationPolicy(required(flags, 'policy'));
2372
- const destination = nativeSkillDirectory(provider);
2373
- const fabric = await client();
2374
- const response = await fabric.pollSkill({
2375
- workspaceId,
2376
- provider,
2377
- installedBundleId: await getActiveSkillBundleId(destination, workspaceId),
2378
- });
2379
- const rollout = response.rollout;
2380
- if (!rollout)
2381
- return { ok: true, rollout: null, changed: false };
2382
- if (typeof rollout.id !== 'string' || !rollout.bundle || typeof rollout.bundle !== 'object')
2383
- throw new Error('Skill rollout response is invalid.');
2384
- const bundle = rollout.bundle;
2385
- if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
2386
- || (bundle.operation === 'install' && bundle.skills.length === 0)
2387
- || (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
2388
- throw new Error('Skill bundle does not match local organization policy.');
2389
- }
2390
- const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
2391
- const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
2392
- if (bundle.operation === 'install') {
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,
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({
2422
2892
  workspaceId,
2423
2893
  provider,
2424
- smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
2425
- organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
2894
+ installedBundleId: activeBundleId,
2426
2895
  });
2427
- if (provider === 'agy' && receipt.status === 'active')
2428
- await activateAgyPlugin();
2429
- await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
2430
- return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
2431
- }
2432
- 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 });
2433
2919
  await rm(sourceRoot, { recursive: true, force: true });
2434
- }
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
+ });
2435
3023
  }
2436
3024
  async function relayStart(flags) {
2437
3025
  const policyPath = resolve(required(flags, 'policy'));
@@ -2450,10 +3038,19 @@ async function relayStart(flags) {
2450
3038
  process.once('SIGINT', stop);
2451
3039
  process.once('SIGTERM', stop);
2452
3040
  let tasksCompleted = 0;
3041
+ let taskTrajectoriesRecovered = 0;
2453
3042
  let evidenceResponsesCompleted = 0;
3043
+ let trajectorySyncsCompleted = 0;
2454
3044
  let nextPolicyRefreshAt = 0;
2455
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
+ });
2456
3052
  try {
3053
+ taskTrajectoriesRecovered += (await finalizeRecoveredSignedTaskTrajectories(fabric, policy)).length;
2457
3054
  do {
2458
3055
  if (Date.now() >= nextPolicyRefreshAt) {
2459
3056
  try {
@@ -2468,12 +3065,13 @@ async function relayStart(flags) {
2468
3065
  }
2469
3066
  let evidenceRequestId;
2470
3067
  if (evidencePolicyFresh) {
3068
+ trajectorySyncsCompleted += await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
2471
3069
  const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
2472
3070
  evidenceRequestId = typeof evidence.requestId === 'string' ? evidence.requestId : undefined;
2473
3071
  if (evidenceRequestId)
2474
3072
  evidenceResponsesCompleted += 1;
2475
3073
  }
2476
- const result = await executeOneTask(fabric, policy, leaseSeconds);
3074
+ const result = await executeOneTask(fabric, leaseSeconds);
2477
3075
  if (result.taskId)
2478
3076
  tasksCompleted += 1;
2479
3077
  if (flags.has('once'))
@@ -2483,11 +3081,15 @@ async function relayStart(flags) {
2483
3081
  } while (!stopping);
2484
3082
  }
2485
3083
  finally {
3084
+ vault.close();
2486
3085
  process.removeListener('SIGINT', stop);
2487
3086
  process.removeListener('SIGTERM', stop);
2488
3087
  await rm(pidPath, { force: true });
2489
3088
  }
2490
- return { ok: true, stopped: true, tasksCompleted, evidenceResponsesCompleted };
3089
+ return {
3090
+ ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
3091
+ evidenceResponsesCompleted, trajectorySyncsCompleted,
3092
+ };
2491
3093
  }
2492
3094
  export async function run(argv) {
2493
3095
  const { positional, flags, repeated } = parseCliOptions(argv);
@@ -2569,10 +3171,14 @@ export async function run(argv) {
2569
3171
  throw new Error('Skill provider must be codex, claude, or agy.');
2570
3172
  const root = nativeSkillDirectory(providerValue);
2571
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.');
2572
3178
  return {
2573
3179
  provider: providerValue,
2574
3180
  workspaceId,
2575
- activeBundleId: await getActiveSkillBundleId(root, workspaceId),
3181
+ activeBundleId: (await activeSkillAuthorization(providerValue, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null,
2576
3182
  nativeSkillDirectory: root,
2577
3183
  };
2578
3184
  }