@dharma-ai-labs/agent-fabric 0.2.49 → 0.2.51

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.
Files changed (198) hide show
  1. package/dist/index.d.ts +88 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +840 -175
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.test.js +2 -2
  6. package/dist/repositoryCandidateSync.d.ts +55 -0
  7. package/dist/repositoryCandidateSync.d.ts.map +1 -0
  8. package/dist/repositoryCandidateSync.js +177 -0
  9. package/dist/repositoryCandidateSync.js.map +1 -0
  10. package/dist/repositoryCandidateSync.test.d.ts +2 -0
  11. package/dist/repositoryCandidateSync.test.d.ts.map +1 -0
  12. package/dist/repositoryCandidateSync.test.js +141 -0
  13. package/dist/repositoryCandidateSync.test.js.map +1 -0
  14. package/dist/repositoryInstalledKnowledge.d.ts +13 -0
  15. package/dist/repositoryInstalledKnowledge.d.ts.map +1 -0
  16. package/dist/repositoryInstalledKnowledge.js +25 -0
  17. package/dist/repositoryInstalledKnowledge.js.map +1 -0
  18. package/dist/repositoryInstallerFiles.d.ts +6 -0
  19. package/dist/repositoryInstallerFiles.d.ts.map +1 -0
  20. package/dist/repositoryInstallerFiles.js +113 -0
  21. package/dist/repositoryInstallerFiles.js.map +1 -0
  22. package/dist/repositoryKnowledge.d.ts +119 -0
  23. package/dist/repositoryKnowledge.d.ts.map +1 -0
  24. package/dist/repositoryKnowledge.js +462 -0
  25. package/dist/repositoryKnowledge.js.map +1 -0
  26. package/dist/repositoryKnowledge.test.d.ts +2 -0
  27. package/dist/repositoryKnowledge.test.d.ts.map +1 -0
  28. package/dist/repositoryKnowledge.test.js +245 -0
  29. package/dist/repositoryKnowledge.test.js.map +1 -0
  30. package/dist/repositoryKnowledgeRetention.test.d.ts +2 -0
  31. package/dist/repositoryKnowledgeRetention.test.d.ts.map +1 -0
  32. package/dist/repositoryKnowledgeRetention.test.js +386 -0
  33. package/dist/repositoryKnowledgeRetention.test.js.map +1 -0
  34. package/dist/repositoryOnboardingCaller.test.d.ts +2 -0
  35. package/dist/repositoryOnboardingCaller.test.d.ts.map +1 -0
  36. package/dist/repositoryOnboardingCaller.test.js +216 -0
  37. package/dist/repositoryOnboardingCaller.test.js.map +1 -0
  38. package/dist/repositoryOnboardingFlow.test.d.ts +2 -0
  39. package/dist/repositoryOnboardingFlow.test.d.ts.map +1 -0
  40. package/dist/repositoryOnboardingFlow.test.js +47 -0
  41. package/dist/repositoryOnboardingFlow.test.js.map +1 -0
  42. package/dist/repositoryPackage.d.ts +115 -0
  43. package/dist/repositoryPackage.d.ts.map +1 -0
  44. package/dist/repositoryPackage.js +1095 -0
  45. package/dist/repositoryPackage.js.map +1 -0
  46. package/dist/repositoryPackage.test.d.ts +2 -0
  47. package/dist/repositoryPackage.test.d.ts.map +1 -0
  48. package/dist/repositoryPackage.test.js +709 -0
  49. package/dist/repositoryPackage.test.js.map +1 -0
  50. package/dist/repositoryPackageDelivery.d.ts +29 -0
  51. package/dist/repositoryPackageDelivery.d.ts.map +1 -0
  52. package/dist/repositoryPackageDelivery.js +140 -0
  53. package/dist/repositoryPackageDelivery.js.map +1 -0
  54. package/dist/repositoryPackageDelivery.test.d.ts +2 -0
  55. package/dist/repositoryPackageDelivery.test.d.ts.map +1 -0
  56. package/dist/repositoryPackageDelivery.test.js +787 -0
  57. package/dist/repositoryPackageDelivery.test.js.map +1 -0
  58. package/dist/repositoryPackageDeliveryCaller.test.d.ts +2 -0
  59. package/dist/repositoryPackageDeliveryCaller.test.d.ts.map +1 -0
  60. package/dist/repositoryPackageDeliveryCaller.test.js +18 -0
  61. package/dist/repositoryPackageDeliveryCaller.test.js.map +1 -0
  62. package/dist/repositoryPackageDeliveryPreflight.test.d.ts +2 -0
  63. package/dist/repositoryPackageDeliveryPreflight.test.d.ts.map +1 -0
  64. package/dist/repositoryPackageDeliveryPreflight.test.js +29 -0
  65. package/dist/repositoryPackageDeliveryPreflight.test.js.map +1 -0
  66. package/dist/repositoryPackageTransfer.d.ts +85 -0
  67. package/dist/repositoryPackageTransfer.d.ts.map +1 -0
  68. package/dist/repositoryPackageTransfer.js +233 -0
  69. package/dist/repositoryPackageTransfer.js.map +1 -0
  70. package/dist/repositoryPackageTransfer.test.d.ts +2 -0
  71. package/dist/repositoryPackageTransfer.test.d.ts.map +1 -0
  72. package/dist/repositoryPackageTransfer.test.js +173 -0
  73. package/dist/repositoryPackageTransfer.test.js.map +1 -0
  74. package/dist/repositoryPackageTransferV2.test.d.ts +2 -0
  75. package/dist/repositoryPackageTransferV2.test.d.ts.map +1 -0
  76. package/dist/repositoryPackageTransferV2.test.js +549 -0
  77. package/dist/repositoryPackageTransferV2.test.js.map +1 -0
  78. package/dist/repositoryPackageTransferV2Schema.test.d.ts +2 -0
  79. package/dist/repositoryPackageTransferV2Schema.test.d.ts.map +1 -0
  80. package/dist/repositoryPackageTransferV2Schema.test.js +48 -0
  81. package/dist/repositoryPackageTransferV2Schema.test.js.map +1 -0
  82. package/dist/repositoryReleaseMetadata.d.ts +37 -0
  83. package/dist/repositoryReleaseMetadata.d.ts.map +1 -0
  84. package/dist/repositoryReleaseMetadata.js +251 -0
  85. package/dist/repositoryReleaseMetadata.js.map +1 -0
  86. package/dist/repositoryReleaseMetadata.test.d.ts +2 -0
  87. package/dist/repositoryReleaseMetadata.test.d.ts.map +1 -0
  88. package/dist/repositoryReleaseMetadata.test.js +297 -0
  89. package/dist/repositoryReleaseMetadata.test.js.map +1 -0
  90. package/dist/repositoryRoleDerivation.d.ts +11 -0
  91. package/dist/repositoryRoleDerivation.d.ts.map +1 -0
  92. package/dist/repositoryRoleDerivation.js +59 -0
  93. package/dist/repositoryRoleDerivation.js.map +1 -0
  94. package/dist/repositoryRoleDerivation.test.d.ts +2 -0
  95. package/dist/repositoryRoleDerivation.test.d.ts.map +1 -0
  96. package/dist/repositoryRoleDerivation.test.js +33 -0
  97. package/dist/repositoryRoleDerivation.test.js.map +1 -0
  98. package/dist/repositoryRoleMetadata.d.ts +63 -0
  99. package/dist/repositoryRoleMetadata.d.ts.map +1 -0
  100. package/dist/repositoryRoleMetadata.js +203 -0
  101. package/dist/repositoryRoleMetadata.js.map +1 -0
  102. package/dist/repositoryRoleMetadata.test.d.ts +2 -0
  103. package/dist/repositoryRoleMetadata.test.d.ts.map +1 -0
  104. package/dist/repositoryRoleMetadata.test.js +296 -0
  105. package/dist/repositoryRoleMetadata.test.js.map +1 -0
  106. package/dist/repositoryRoleQuestion.d.ts +36 -0
  107. package/dist/repositoryRoleQuestion.d.ts.map +1 -0
  108. package/dist/repositoryRoleQuestion.js +68 -0
  109. package/dist/repositoryRoleQuestion.js.map +1 -0
  110. package/dist/repositoryRoleQuestion.test.d.ts +2 -0
  111. package/dist/repositoryRoleQuestion.test.d.ts.map +1 -0
  112. package/dist/repositoryRoleQuestion.test.js +66 -0
  113. package/dist/repositoryRoleQuestion.test.js.map +1 -0
  114. package/dist/repositorySourceAuthorization.d.ts +43 -0
  115. package/dist/repositorySourceAuthorization.d.ts.map +1 -0
  116. package/dist/repositorySourceAuthorization.js +164 -0
  117. package/dist/repositorySourceAuthorization.js.map +1 -0
  118. package/dist/repositorySourceCapture.test.d.ts +2 -0
  119. package/dist/repositorySourceCapture.test.d.ts.map +1 -0
  120. package/dist/repositorySourceCapture.test.js +303 -0
  121. package/dist/repositorySourceCapture.test.js.map +1 -0
  122. package/dist/repositorySourceSync.d.ts +54 -0
  123. package/dist/repositorySourceSync.d.ts.map +1 -0
  124. package/dist/repositorySourceSync.js +88 -0
  125. package/dist/repositorySourceSync.js.map +1 -0
  126. package/dist/repositorySourceSync.test.d.ts +2 -0
  127. package/dist/repositorySourceSync.test.d.ts.map +1 -0
  128. package/dist/repositorySourceSync.test.js +218 -0
  129. package/dist/repositorySourceSync.test.js.map +1 -0
  130. package/dist/schemas/repository-knowledge-retention-v1.schema.json +26 -0
  131. package/dist/schemas/repository-knowledge-v2.schema.json +74 -0
  132. package/dist/schemas/repository-knowledge.schema.json +63 -0
  133. package/dist/schemas/repository-package-chunk-v2.schema.json +15 -0
  134. package/dist/schemas/repository-package-chunk.schema.json +18 -0
  135. package/dist/schemas/repository-package-delivery-v1.schema.json +51 -0
  136. package/dist/schemas/repository-package-envelope-v1.schema.json +16 -0
  137. package/dist/schemas/repository-package-transfer-v2.schema.json +36 -0
  138. package/dist/schemas/repository-package-transfer.schema.json +34 -0
  139. package/dist/schemas/repository-package-v2.schema.json +491 -0
  140. package/dist/schemas/repository-package-v3.schema.json +36 -0
  141. package/dist/schemas/repository-package.schema.json +94 -0
  142. package/dist/schemas/repository-release-manifest.schema.json +34 -0
  143. package/dist/schemas/repository-role-metadata.schema.json +82 -0
  144. package/dist/schemas/skill-preparation-pointer-v1.schema.json +28 -0
  145. package/dist/schemas/skill-preparation-v1.schema.json +32 -0
  146. package/dist/skillPollResponse.test.d.ts +2 -0
  147. package/dist/skillPollResponse.test.d.ts.map +1 -0
  148. package/dist/skillPollResponse.test.js +37 -0
  149. package/dist/skillPollResponse.test.js.map +1 -0
  150. package/dist/skillPreparation.test.d.ts +2 -0
  151. package/dist/skillPreparation.test.d.ts.map +1 -0
  152. package/dist/skillPreparation.test.js +163 -0
  153. package/dist/skillPreparation.test.js.map +1 -0
  154. package/dist/skillPreparationCache.d.ts +34 -0
  155. package/dist/skillPreparationCache.d.ts.map +1 -0
  156. package/dist/skillPreparationCache.js +371 -0
  157. package/dist/skillPreparationCache.js.map +1 -0
  158. package/dist/skillPreparationCache.test.d.ts +2 -0
  159. package/dist/skillPreparationCache.test.d.ts.map +1 -0
  160. package/dist/skillPreparationCache.test.js +316 -0
  161. package/dist/skillPreparationCache.test.js.map +1 -0
  162. package/dist/skillPreparationCaller.test.d.ts +2 -0
  163. package/dist/skillPreparationCaller.test.d.ts.map +1 -0
  164. package/dist/skillPreparationCaller.test.js +32 -0
  165. package/dist/skillPreparationCaller.test.js.map +1 -0
  166. package/dist/skillPreparationPump.d.ts +11 -0
  167. package/dist/skillPreparationPump.d.ts.map +1 -0
  168. package/dist/skillPreparationPump.js +41 -0
  169. package/dist/skillPreparationPump.js.map +1 -0
  170. package/dist/skillPreparationPump.test.d.ts +2 -0
  171. package/dist/skillPreparationPump.test.d.ts.map +1 -0
  172. package/dist/skillPreparationPump.test.js +87 -0
  173. package/dist/skillPreparationPump.test.js.map +1 -0
  174. package/dist/skillPreparationRecord.d.ts +2 -0
  175. package/dist/skillPreparationRecord.d.ts.map +1 -0
  176. package/dist/skillPreparationRecord.js +36 -0
  177. package/dist/skillPreparationRecord.js.map +1 -0
  178. package/dist/skillPreparationTransaction.d.ts +13 -0
  179. package/dist/skillPreparationTransaction.d.ts.map +1 -0
  180. package/dist/skillPreparationTransaction.js +236 -0
  181. package/dist/skillPreparationTransaction.js.map +1 -0
  182. package/dist/skillPreparationTransaction.test.d.ts +2 -0
  183. package/dist/skillPreparationTransaction.test.d.ts.map +1 -0
  184. package/dist/skillPreparationTransaction.test.js +181 -0
  185. package/dist/skillPreparationTransaction.test.js.map +1 -0
  186. package/dist/skillSyncPolicy.test.d.ts +2 -0
  187. package/dist/skillSyncPolicy.test.d.ts.map +1 -0
  188. package/dist/skillSyncPolicy.test.js +156 -0
  189. package/dist/skillSyncPolicy.test.js.map +1 -0
  190. package/dist/usage.d.ts +1 -1
  191. package/dist/usage.d.ts.map +1 -1
  192. package/dist/usage.js +1 -1
  193. package/dist/usage.js.map +1 -1
  194. package/dist/windowsProcessPortability.test.d.ts +2 -0
  195. package/dist/windowsProcessPortability.test.d.ts.map +1 -0
  196. package/dist/windowsProcessPortability.test.js +14 -0
  197. package/dist/windowsProcessPortability.test.js.map +1 -0
  198. package/package.json +7 -4
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { execFile, spawn } from 'node:child_process';
3
3
  import { createHash, createPrivateKey, createPublicKey, randomUUID } from 'node:crypto';
4
4
  import { realpathSync } from 'node:fs';
5
- import { access, chmod, link, mkdir, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
5
+ import { access, chmod, link, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
6
6
  import { homedir } from 'node:os';
7
7
  import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
@@ -13,10 +13,25 @@ import { assertPolicy, loadOrganizationPolicy, verifyServerAuthorizedPolicy } fr
13
13
  import { agyAdapter, claudeAdapter, codexAdapter, hermesAdapter, providerAdapters, providerExecutionRecords, providerProcessEnvironment } from '@dharma-ai-labs/agent-fabric-provider-adapters';
14
14
  import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, deleteActiveSkillAuthorizationAnchor, loadActiveSkillAuthorizationAnchor, loadDeviceEnrollmentAnchor, saveActiveSkillAuthorizationAnchor, isDefinitiveAgentFabricRejection, recoverDeviceEnrollmentConsistency, loadOrganizationApiToken, redeemBootstrapGrant, saveDeviceConfig, saveDeviceEnrollmentAnchor, saveOrganizationApiToken, } from '@dharma-ai-labs/agent-fabric-relay-client';
15
15
  import { AgentFabricClient as AgentFabricApiClient, } from '@dharma-ai-labs/agent-fabric-sdk';
16
- import { getActiveSkillBundleAuthorization, getExpiredSkillBundleAuthorizationForReplacement, getLegacySkillBundleIdForUpgrade, installSkillBundle, rollbackUnconfirmedSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
16
+ import { contentHash, getActiveSkillBundleAuthorization, getExpiredSkillBundleAuthorizationForReplacement, getLegacySkillBundleIdForUpgrade, installSkillBundle, readVerifiedRepositoryKnowledge, rollbackUnconfirmedSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
17
17
  import { executeTask, FileActionExecutionJournal, FileTaskReceiptStore, providerInstructionsForTask, } from '@dharma-ai-labs/agent-fabric-task-runner';
18
18
  import { CLI_USAGE } from './usage.js';
19
- const VERSION = '0.2.49';
19
+ import { initializeRepositoryKnowledge } from './repositoryKnowledge.js';
20
+ import { inventoryRepositoryPackage, writeRepositoryPackageSnapshot } from './repositoryPackage.js';
21
+ import { validateRepositorySourceAuthorization } from './repositorySourceAuthorization.js';
22
+ import { fetchRepositorySourceAuthorization, RepositorySourceWatcher, scanRepositorySourceChanges } from './repositorySourceSync.js';
23
+ import { assertRepositoryInstallerOwnership, writeRepositoryInstallerFile } from './repositoryInstallerFiles.js';
24
+ import { receiveRepositoryPackageDelivery } from './repositoryPackageDelivery.js';
25
+ import { selectInstalledRepositoryKnowledge } from './repositoryInstalledKnowledge.js';
26
+ import { prepareProvidersIndependently, startSkillPreparationPump } from './skillPreparationPump.js';
27
+ import { serializeSkillPreparationRecord } from './skillPreparationRecord.js';
28
+ import { skillPreparationScopeRoot, withSkillPreparationTransaction } from './skillPreparationTransaction.js';
29
+ import { publishSkillPreparationCache, takeSkillPreparationCache } from './skillPreparationCache.js';
30
+ import { adoptRepositoryCandidate, pollRepositoryCandidate, synchronizeRepositoryCandidate } from './repositoryCandidateSync.js';
31
+ import { registerRepositoryRoleMetadata, discoverRepositoryRoleMetadata } from './repositoryRoleMetadata.js';
32
+ import { askRepositoryRoleQuestion, readRepositoryRoleReply } from './repositoryRoleQuestion.js';
33
+ import { deriveRepositoryRole } from './repositoryRoleDerivation.js';
34
+ const VERSION = '0.2.51';
20
35
  const USAGE = CLI_USAGE;
21
36
  const execFileAsync = promisify(execFile);
22
37
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -754,7 +769,15 @@ async function assertWorkspaceAuthorizationCurrent(workspaceId, authorization, r
754
769
  const statePath = workspaceAuthorizationStatePath(workspaceId);
755
770
  let previous = null;
756
771
  try {
757
- previous = JSON.parse(await readFile(statePath, 'utf8'));
772
+ const parsed = JSON.parse(await readFile(statePath, 'utf8'));
773
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
774
+ || typeof parsed.issuedAt !== 'string'
775
+ || !Number.isFinite(Date.parse(parsed.issuedAt))
776
+ || typeof parsed.signature !== 'string'
777
+ || !parsed.signature) {
778
+ throw new Error('Workspace authorization replay state is missing or invalid; apply a fresh server policy.');
779
+ }
780
+ previous = parsed;
758
781
  }
759
782
  catch (error) {
760
783
  if (error.code === 'ENOENT' && !requireExisting)
@@ -1417,6 +1440,10 @@ async function bootstrap(flags) {
1417
1440
  const name = String(flags.get('device-name') || `${process.env.USER || process.env.USERNAME || 'developer'} device`);
1418
1441
  const devicePlatform = await platform();
1419
1442
  const identity = await loadOrCreateDeviceIdentity({ hqUrl, organizationId });
1443
+ let recipientApproval = {
1444
+ required: false,
1445
+ browserOpened: false,
1446
+ };
1420
1447
  const redeemed = await redeemBootstrapGrant({
1421
1448
  hqUrl,
1422
1449
  organizationId,
@@ -1424,6 +1451,13 @@ async function bootstrap(flags) {
1424
1451
  name,
1425
1452
  platform: devicePlatform,
1426
1453
  publicKeyEd25519: identity.publicKeyEd25519,
1454
+ onRecipientApprovalRequired: async (approval) => {
1455
+ recipientApproval.required = true;
1456
+ recipientApproval.browserOpened = flags.has('no-browser')
1457
+ ? false
1458
+ : await openVerificationUri(approval.url);
1459
+ process.stderr.write(`Confirm this exact device in the authenticated Dharma portal before ${approval.expiresAt}: ${approval.url}\n`);
1460
+ },
1427
1461
  });
1428
1462
  const rebind = enrollmentMismatch && existing
1429
1463
  ? await archiveEnrollmentForAuthorizedRebind(existing)
@@ -1464,18 +1498,40 @@ async function bootstrap(flags) {
1464
1498
  onboardFlags.set('policy-revision', policyRevision);
1465
1499
  onboardFlags.set('provider', provider);
1466
1500
  const onboarded = await retryBootstrapOnboarding(async () => await onboard(onboardFlags));
1501
+ if (onboarded.ok !== true || onboarded.stage === 'approve_device') {
1502
+ return {
1503
+ ok: onboarded.ok === true,
1504
+ stage: onboarded.stage,
1505
+ code: onboarded.code,
1506
+ sharedRepositoryReady: false,
1507
+ enrollment: {
1508
+ organizationId,
1509
+ deviceId: config.deviceId,
1510
+ status: 'approved',
1511
+ organizationApiTokenStored: true,
1512
+ scopes: redeemed.organizationApiTokenScopes,
1513
+ recipientApproval,
1514
+ rebind,
1515
+ },
1516
+ repository: onboarded,
1517
+ };
1518
+ }
1467
1519
  const launcher = await installStableRepositoryLauncher(workspace);
1520
+ const sharedRepositoryReady = onboarded.sharedRepositoryReady === true;
1468
1521
  const completionRequested = flags.has('complete');
1469
1522
  if (!completionRequested) {
1470
1523
  return {
1471
1524
  ok: true,
1472
- stage: onboarded.stage,
1525
+ stage: sharedRepositoryReady ? onboarded.stage : 'shared_repository_pending',
1526
+ localStage: onboarded.localStage ?? onboarded.stage,
1527
+ sharedRepositoryReady,
1473
1528
  enrollment: {
1474
1529
  organizationId,
1475
1530
  deviceId: config.deviceId,
1476
1531
  status: 'approved',
1477
1532
  organizationApiTokenStored: true,
1478
1533
  scopes: redeemed.organizationApiTokenScopes,
1534
+ recipientApproval,
1479
1535
  rebind,
1480
1536
  },
1481
1537
  repository: onboarded,
@@ -1524,13 +1580,16 @@ async function bootstrap(flags) {
1524
1580
  });
1525
1581
  return {
1526
1582
  ok: true,
1527
- stage: 'complete',
1583
+ stage: sharedRepositoryReady ? 'complete' : 'shared_repository_pending',
1584
+ localStage: 'complete',
1585
+ sharedRepositoryReady,
1528
1586
  enrollment: {
1529
1587
  organizationId,
1530
1588
  deviceId: config.deviceId,
1531
1589
  status: 'approved',
1532
1590
  organizationApiTokenStored: true,
1533
1591
  scopes: redeemed.organizationApiTokenScopes,
1592
+ recipientApproval,
1534
1593
  rebind,
1535
1594
  },
1536
1595
  repository: onboarded,
@@ -2366,7 +2425,22 @@ async function repositoriesConnect(flags, positional, repeatedRepos, repeatedKey
2366
2425
  if (result?.stage === 'approve_device')
2367
2426
  break;
2368
2427
  }
2369
- return { ok: true, requested: unique.length, connected: results.length, repositories: results };
2428
+ const states = results.map(result => result);
2429
+ const blocked = states.filter(result => result.ok !== true).length;
2430
+ const connected = states.filter(result => result.ok === true && result.sharedRepositoryReady === true).length;
2431
+ const locallyReady = states.filter(result => result.ok === true && (['ready', 'ready_with_provider_actions', 'complete'].includes(String(result.localStage))
2432
+ || ['ready', 'ready_with_provider_actions', 'complete'].includes(String(result.stage)))).length;
2433
+ return {
2434
+ ok: blocked === 0,
2435
+ requested: unique.length,
2436
+ attempted: results.length,
2437
+ locallyReady,
2438
+ connected,
2439
+ blocked,
2440
+ pending: states.length - blocked - connected,
2441
+ sharedRepositoryReady: connected === unique.length,
2442
+ repositories: results,
2443
+ };
2370
2444
  }
2371
2445
  export function parseSelectedProviderIds(values) {
2372
2446
  const selected = [...new Set(values
@@ -2445,22 +2519,72 @@ export async function receiptAwareProviderCapabilities(providers, now = new Date
2445
2519
  }
2446
2520
  async function repositoriesList(flags) {
2447
2521
  const verbose = flags.has('verbose') || flags.has('diagnostic');
2522
+ const items = await registry();
2448
2523
  return {
2449
2524
  ok: true,
2450
- repositories: (await registry()).map((item) => ({
2525
+ repositories: await Promise.all(items.map(async (item) => ({
2451
2526
  workspaceId: item.workspaceId,
2452
2527
  name: item.name,
2453
2528
  repositoryAgentId: item.repositoryAgentId || null,
2454
2529
  repositoryAgentKey: item.repositoryAgentKey || null,
2455
2530
  controlBranch: item.controlBranch || null,
2456
2531
  connected: Boolean(item.repositoryAgentId && item.controlBranch),
2532
+ repositoryPackageState: item.repositoryPackage?.state || 'unknown',
2533
+ sharedRepositoryReady: await repositorySharedReady(item),
2457
2534
  ...(verbose ? { localPath: item.path, sourceFingerprint: item.repositoryRemoteHash } : {}),
2458
- })),
2535
+ }))),
2459
2536
  };
2460
2537
  }
2538
+ async function repositorySharedReady(item) {
2539
+ if (item.repositoryPackage?.state !== 'published' || !item.repositoryPackage.releaseId)
2540
+ return false;
2541
+ try {
2542
+ return Boolean(await installedRepositoryKnowledge(item));
2543
+ }
2544
+ catch {
2545
+ return false;
2546
+ }
2547
+ }
2548
+ export function canonicalRepositoryPackage(value) {
2549
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
2550
+ throw new Error('Dharma HQ did not return canonical repository package state.');
2551
+ }
2552
+ const row = value;
2553
+ const keys = ['state', 'candidateId', 'operationId', 'snapshotHash', 'sourceManifestHash', 'releaseId', 'generation', 'consolidationMode'];
2554
+ if (!keys.every(key => Object.hasOwn(row, key)) || Object.keys(row).some(key => !keys.includes(key))
2555
+ || !['absent', 'accepted', 'processing', 'published', 'blocked'].includes(String(row.state))
2556
+ || !Number.isSafeInteger(row.generation) || Number(row.generation) < 0) {
2557
+ throw new Error('Dharma HQ returned invalid canonical repository package state.');
2558
+ }
2559
+ const nullableUuid = (candidate) => candidate === null || typeof candidate === 'string' && UUID_PATTERN.test(candidate);
2560
+ const nullableHash = (candidate) => candidate === null || typeof candidate === 'string' && /^sha256:[a-f0-9]{64}$/.test(candidate);
2561
+ const absent = row.state === 'absent';
2562
+ if (![row.candidateId, row.releaseId].every(nullableUuid)
2563
+ || ![row.operationId, row.snapshotHash, row.sourceManifestHash].every(nullableHash)
2564
+ || ![null, 'initial_repository', 'repository_update'].includes(row.consolidationMode)
2565
+ || absent !== (row.candidateId === null && row.operationId === null && row.snapshotHash === null
2566
+ && row.sourceManifestHash === null && row.releaseId === null && row.consolidationMode === null && row.generation === 0)
2567
+ || (row.state === 'published') !== (row.releaseId !== null)
2568
+ || (!absent && (row.candidateId === null || row.operationId === null || row.snapshotHash === null
2569
+ || row.sourceManifestHash === null || row.consolidationMode === null))) {
2570
+ throw new Error('Dharma HQ returned inconsistent canonical repository package state.');
2571
+ }
2572
+ return row;
2573
+ }
2574
+ export function canonicalEndpointRole(value) {
2575
+ if (value === null)
2576
+ return null;
2577
+ if (!value || typeof value !== 'object' || Array.isArray(value))
2578
+ throw new Error('Dharma HQ returned invalid endpoint role state.');
2579
+ const row = value;
2580
+ if (Object.keys(row).sort().join(',') !== 'profileHash,revision'
2581
+ || !Number.isSafeInteger(row.revision) || Number(row.revision) < 1
2582
+ || typeof row.profileHash !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(row.profileHash)) {
2583
+ throw new Error('Dharma HQ returned invalid endpoint role state.');
2584
+ }
2585
+ return { revision: Number(row.revision), profileHash: row.profileHash };
2586
+ }
2461
2587
  async function bindRepositoryAgent(fabric, item) {
2462
- if (item.repositoryAgentId && item.repositoryBindingId && item.repositoryAgentKey && item.controlBranch)
2463
- return item;
2464
2588
  const remote = await gitValue(item.path, ['config', '--get', 'remote.origin.url']);
2465
2589
  const currentIdentity = item.repositoryIdentityVersion === 'normalized-v1'
2466
2590
  ? item.repositoryRemoteHash
@@ -2481,17 +2605,25 @@ async function bindRepositoryAgent(fabric, item) {
2481
2605
  const branch = repositoryAgent?.branch && typeof repositoryAgent.branch === 'object'
2482
2606
  ? repositoryAgent.branch
2483
2607
  : null;
2608
+ const endpoint = response.endpoint && typeof response.endpoint === 'object' && !Array.isArray(response.endpoint)
2609
+ ? response.endpoint
2610
+ : null;
2484
2611
  const updated = {
2485
2612
  ...item,
2486
2613
  repositoryRemoteHash: currentIdentity,
2487
2614
  repositoryIdentityVersion: 'normalized-v1',
2488
2615
  repositoryAgentId: String(repositoryAgent?.organization_agent_id || ''),
2489
2616
  repositoryBindingId: String(repositoryAgent?.id || ''),
2617
+ endpointId: typeof response.endpointId === 'string' ? response.endpointId
2618
+ : typeof endpoint?.id === 'string' ? endpoint.id : item.endpointId || null,
2619
+ repositoryPackage: canonicalRepositoryPackage(response.repositoryPackage),
2620
+ repositoryRole: canonicalEndpointRole(response.endpointRole),
2490
2621
  repositoryAgentKey: String(repositoryAgent?.agent_key || ''),
2491
2622
  controlBranch: String(repositoryAgent?.control_branch || branch?.branch || ''),
2492
2623
  };
2493
2624
  if (!/^[0-9a-f-]{36}$/i.test(updated.repositoryAgentId || '')
2494
2625
  || !/^[0-9a-f-]{36}$/i.test(updated.repositoryBindingId || '')
2626
+ || !UUID_PATTERN.test(updated.endpointId || '')
2495
2627
  || !/^repo:[a-f0-9]{24}$/.test(updated.repositoryAgentKey || '')
2496
2628
  || !/^agents\/[a-z0-9][a-z0-9._-]*-[a-f0-9]{8}$/.test(updated.controlBranch || '')) {
2497
2629
  throw new Error('Dharma HQ returned an invalid repository-agent binding.');
@@ -2499,6 +2631,35 @@ async function bindRepositoryAgent(fabric, item) {
2499
2631
  await saveWorkspaceRecord(updated);
2500
2632
  return updated;
2501
2633
  }
2634
+ function repositoryRoleScope(item) {
2635
+ if (!item.repositoryAgentId || !item.repositoryBindingId || !item.endpointId || !item.repositoryRemoteHash) {
2636
+ throw new Error('Repository role operations require a complete signed endpoint and repository binding.');
2637
+ }
2638
+ return { organizationId: item.organizationId, workspaceId: item.workspaceId,
2639
+ repositoryBindingId: item.repositoryBindingId, repositoryAgentId: item.repositoryAgentId,
2640
+ endpointId: item.endpointId, sourceFingerprint: item.repositoryRemoteHash };
2641
+ }
2642
+ async function repositoryRoleCommand(action, flags) {
2643
+ const workspaceId = required(flags, 'workspace-id');
2644
+ const item = (await registry()).find(candidate => candidate.workspaceId === workspaceId);
2645
+ if (!item)
2646
+ throw new Error('Repository role workspace is not registered locally.');
2647
+ const scope = repositoryRoleScope(item), transport = await client();
2648
+ if (action === 'register') {
2649
+ const categories = required(flags, 'question-categories').split(',').map(value => value.trim()).filter(Boolean);
2650
+ return registerRepositoryRoleMetadata(transport, scope, { expectedRevision: Number(flags.get('expected-revision') || 0),
2651
+ roleName: required(flags, 'role-name'), questionCategories: categories,
2652
+ description: required(flags, 'role-description') });
2653
+ }
2654
+ if (action === 'discover') {
2655
+ const category = typeof flags.get('category') === 'string' ? String(flags.get('category')) : undefined;
2656
+ return discoverRepositoryRoleMetadata(transport, scope, category);
2657
+ }
2658
+ if (action === 'ask')
2659
+ return askRepositoryRoleQuestion({ transport, scope,
2660
+ category: required(flags, 'category'), question: required(flags, 'question') });
2661
+ return readRepositoryRoleReply({ transport, scope, questionId: required(flags, 'question-id') });
2662
+ }
2502
2663
  async function workspaceSync(flags, positional) {
2503
2664
  const workspaceId = positional[0] || required(flags, 'workspace-id');
2504
2665
  const policyRevision = required(flags, 'policy-revision');
@@ -2662,23 +2823,13 @@ async function loadVerifiedWorkspacePolicy(path, workspaceId) {
2662
2823
  return verified;
2663
2824
  }
2664
2825
  export async function installRepositoryAgentFabricSkill(input) {
2826
+ if (input.sourceAuthorization !== undefined)
2827
+ input = { ...input,
2828
+ sourceAuthorization: validateRepositorySourceAuthorization(input.sourceAuthorization, input, new Date()) };
2829
+ await assertRepositoryInstallerOwnership(input.workspace, input.workspaceId);
2830
+ input = { ...input, workspace: await realpath(input.workspace) };
2831
+ await assertRepositoryInstallerOwnership(input.workspace, input.workspaceId);
2665
2832
  const skillRoot = resolve(input.workspace, '.agents', 'skills', 'dharma-agent-fabric');
2666
- const marker = resolve(skillRoot, '.dharma-agent-fabric.json');
2667
- let skillRootExists = true;
2668
- try {
2669
- await access(skillRoot);
2670
- }
2671
- catch {
2672
- skillRootExists = false;
2673
- }
2674
- if (skillRootExists) {
2675
- try {
2676
- await access(marker);
2677
- }
2678
- catch {
2679
- throw new Error('Refusing to replace an unmanaged repository skill at .agents/skills/dharma-agent-fabric.');
2680
- }
2681
- }
2682
2833
  await mkdir(resolve(skillRoot, 'references'), { recursive: true, mode: 0o700 });
2683
2834
  await mkdir(resolve(input.workspace, '.dharma'), { recursive: true, mode: 0o700 });
2684
2835
  const skill = `---
@@ -2741,17 +2892,47 @@ The CLI enrolls this device through browser-confirmed Clerk organization consent
2741
2892
  controlBranch: input.controlBranch || null,
2742
2893
  workspaceId: input.workspaceId,
2743
2894
  };
2744
- await writeFile(resolve(skillRoot, 'SKILL.md'), skill, { mode: 0o600 });
2745
- await writeFile(resolve(skillRoot, 'references', 'organization.md'), reference, { mode: 0o600 });
2746
- await writeFile(marker, `${JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: input.workspaceId }, null, 2)}\n`, { mode: 0o600 });
2747
- await writeFile(resolve(input.workspace, '.dharma', 'agent-fabric.json'), `${JSON.stringify(connection, null, 2)}\n`, { mode: 0o600 });
2748
- await writeFile(resolve(input.workspace, '.dharma', 'repository-agent.json'), `${JSON.stringify(repositoryAgent, null, 2)}\n`, { mode: 0o600 });
2895
+ await writeRepositoryInstallerFile(input.workspace, '.agents/skills/dharma-agent-fabric/SKILL.md', skill);
2896
+ await writeRepositoryInstallerFile(input.workspace, '.agents/skills/dharma-agent-fabric/references/organization.md', reference);
2897
+ await writeRepositoryInstallerFile(input.workspace, '.agents/skills/dharma-agent-fabric/.dharma-agent-fabric.json', `${JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: input.workspaceId }, null, 2)}\n`);
2898
+ const knowledge = input.repositoryAgentId ? await initializeRepositoryKnowledge({
2899
+ workspace: input.workspace, organizationId: input.organizationId, repositoryAgentId: input.repositoryAgentId,
2900
+ }) : null;
2901
+ const repositoryPackage = await writeRepositoryPackageSnapshot({
2902
+ workspace: input.workspace, snapshot: await inventoryRepositoryPackage(input),
2903
+ });
2904
+ await writeRepositoryInstallerFile(input.workspace, '.dharma/agent-fabric.json', `${JSON.stringify(connection, null, 2)}\n`);
2905
+ await writeRepositoryInstallerFile(input.workspace, '.dharma/repository-agent.json', `${JSON.stringify(repositoryAgent, null, 2)}\n`);
2749
2906
  return {
2750
2907
  skillPath: '.agents/skills/dharma-agent-fabric/SKILL.md',
2751
2908
  connectionPath: '.dharma/agent-fabric.json',
2752
2909
  repositoryAgentPath: '.dharma/repository-agent.json',
2910
+ repositoryPackage,
2911
+ knowledge,
2753
2912
  };
2754
2913
  }
2914
+ async function repositorySnapshotCommand(flags, outputs) {
2915
+ if (flags.has('apply') && flags.has('dry-run'))
2916
+ throw new Error('Choose --apply or --dry-run, not both.');
2917
+ if (outputs.some(value => typeof value !== 'string'))
2918
+ throw new Error('--approved-output requires a workspace-relative file path.');
2919
+ const workspace = String(flags.get('workspace') || '.');
2920
+ const apply = flags.get('apply') === true;
2921
+ if (apply) {
2922
+ await access(resolve(workspace, '.agents/skills/dharma-agent-fabric/.dharma-agent-fabric.json')).catch(() => {
2923
+ throw new Error('Snapshot --apply requires an installed managed repository skill; use --dry-run before onboarding.');
2924
+ });
2925
+ }
2926
+ const snapshot = await inventoryRepositoryPackage({
2927
+ workspace, organizationId: required(flags, 'organization-id'), workspaceId: required(flags, 'workspace-id'),
2928
+ repositoryAgentId: (await registry()).find(record => record.path === resolve(workspace)
2929
+ && record.organizationId === flags.get('organization-id') && record.workspaceId === flags.get('workspace-id'))?.repositoryAgentId,
2930
+ approvedOutputs: outputs,
2931
+ });
2932
+ const persisted = apply ? await writeRepositoryPackageSnapshot({ workspace, snapshot }) : null;
2933
+ return { ok: true, dryRun: !apply, localMutation: apply, serverMutation: false,
2934
+ authority: 'local_inventory_not_signed', snapshot, persisted };
2935
+ }
2755
2936
  async function onboard(flags) {
2756
2937
  const workspace = await realpath(String(flags.get('workspace') || flags.get('path') || '.'));
2757
2938
  const organizationId = required(flags, 'organization-id');
@@ -2809,17 +2990,98 @@ async function onboard(flags) {
2809
2990
  const localPolicy = synced.localPolicy;
2810
2991
  const authoritativeRevision = String(localPolicy?.revision || policyRevision);
2811
2992
  const generatedPolicy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
2993
+ if (!registered.repositoryBindingId || !registered.repositoryAgentId) {
2994
+ throw new Error('Repository onboarding requires the canonical repository binding and logical agent.');
2995
+ }
2996
+ let sourceAuthorization;
2997
+ try {
2998
+ sourceAuthorization = await fetchRepositorySourceAuthorization(fabric, {
2999
+ organizationId, workspaceId: registered.workspaceId,
3000
+ repositoryBindingId: registered.repositoryBindingId, repositoryAgentId: registered.repositoryAgentId,
3001
+ });
3002
+ }
3003
+ catch {
3004
+ return { ok: false, stage: 'repository_source_authorization_required', organizationId,
3005
+ workspaceId: registered.workspaceId, code: 'repository_source_policy_unavailable',
3006
+ sharedRepositoryReady: false, localInventoryFallback: false };
3007
+ }
2812
3008
  const installed = await installRepositoryAgentFabricSkill({
2813
3009
  workspace,
2814
3010
  hqUrl,
2815
3011
  organizationId,
2816
3012
  workspaceId: registered.workspaceId,
2817
3013
  repositoryAgentId: registered.repositoryAgentId,
3014
+ repositoryBindingId: registered.repositoryBindingId,
3015
+ sourceAuthorization,
2818
3016
  repositoryAgentKey: registered.repositoryAgentKey,
2819
3017
  controlBranch: registered.controlBranch,
2820
3018
  policyRevision: authoritativeRevision,
2821
3019
  });
3020
+ const onboardingProvider = providerIds?.[0] || 'codex';
3021
+ const onboardingEvidenceFlags = new Map([
3022
+ ['workspace', workspace], ['provider', onboardingProvider],
3023
+ ['policy', resolve(workspace, '.dharma', 'approved-policy.json')], ['maximum-sessions', '20'],
3024
+ ]);
3025
+ const onboardingEvidencePreview = await evidencePreview(onboardingEvidenceFlags);
3026
+ let onboardingEvidence = { state: 'synchronized', captured: 0, synced: 0 };
3027
+ if (Number(onboardingEvidencePreview.trajectoryCount || 0) > 0
3028
+ && onboardingEvidencePreview.automaticDisclosure?.ready === true) {
3029
+ onboardingEvidenceFlags.set('sync', true);
3030
+ onboardingEvidence = await synchronizeBootstrapEvidence(async () => await retryBootstrapOnboarding(async () => await capture(onboardingEvidenceFlags, true)));
3031
+ requireCompletedBootstrapEvidence(onboardingEvidence, Number(onboardingEvidencePreview.trajectoryCount || 0));
3032
+ }
3033
+ const initialSnapshot = await inventoryRepositoryPackage({ workspace, organizationId,
3034
+ workspaceId: registered.workspaceId, repositoryAgentId: registered.repositoryAgentId,
3035
+ repositoryBindingId: registered.repositoryBindingId, sourceAuthorization });
3036
+ const candidateScope = { organizationId, workspaceId: registered.workspaceId,
3037
+ repositoryBindingId: registered.repositoryBindingId, repositoryAgentId: registered.repositoryAgentId };
3038
+ const outboxRoot = resolve(dharmaHome(), 'relay', 'repository-candidates');
3039
+ const canonicalPackage = registered.repositoryPackage;
3040
+ const candidate = canonicalPackage.state === 'absent'
3041
+ ? await synchronizeRepositoryCandidate({ transport: fabric, outboxRoot, scope: candidateScope,
3042
+ snapshot: initialSnapshot, initialRepository: true })
3043
+ : await adoptRepositoryCandidate({ outboxRoot, scope: candidateScope,
3044
+ sourceManifestHash: canonicalPackage.sourceManifestHash,
3045
+ consolidationMode: canonicalPackage.consolidationMode, candidate: {
3046
+ candidateId: canonicalPackage.candidateId, operationId: canonicalPackage.operationId,
3047
+ snapshotHash: canonicalPackage.snapshotHash, state: canonicalPackage.state,
3048
+ releaseId: canonicalPackage.releaseId,
3049
+ } });
3050
+ registered = { ...registered, repositoryPackage: {
3051
+ state: candidate.state, candidateId: candidate.candidateId, operationId: candidate.operationId,
3052
+ snapshotHash: candidate.snapshotHash,
3053
+ sourceManifestHash: canonicalPackage.state === 'absent'
3054
+ ? sha256(canonicalize(initialSnapshot.manifest)) : canonicalPackage.sourceManifestHash,
3055
+ releaseId: candidate.releaseId,
3056
+ generation: candidate.state === 'published' ? Math.max(1, canonicalPackage.generation) : canonicalPackage.generation,
3057
+ consolidationMode: canonicalPackage.state === 'absent' ? 'initial_repository' : canonicalPackage.consolidationMode,
3058
+ } };
3059
+ await saveWorkspaceRecord(registered);
2822
3060
  const providers = await receiptAwareProviderCapabilities(await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability())));
3061
+ const roleRequested = ['role-name', 'question-categories', 'role-description'].some(key => flags.has(key));
3062
+ if (roleRequested && !['role-name', 'question-categories', 'role-description'].every(key => flags.has(key))) {
3063
+ throw new Error('Repository role overrides require --role-name, --question-categories, and --role-description together.');
3064
+ }
3065
+ const derivedRole = deriveRepositoryRole({ snapshot: initialSnapshot,
3066
+ providers: providers.map(provider => provider.provider).filter(isLocalProviderId) });
3067
+ const roleInput = roleRequested ? {
3068
+ roleName: required(flags, 'role-name'),
3069
+ questionCategories: required(flags, 'question-categories').split(',').map(value => value.trim()).filter(Boolean),
3070
+ description: required(flags, 'role-description'),
3071
+ } : derivedRole;
3072
+ const profileHash = sha256(canonicalize(roleInput));
3073
+ let role = registered.repositoryRole?.profileHash === profileHash
3074
+ ? { stage: 'repository_role_observed', reused: true, role: { ...roleInput, revision: registered.repositoryRole.revision } }
3075
+ : await registerRepositoryRoleMetadata(fabric, repositoryRoleScope(registered), {
3076
+ expectedRevision: registered.repositoryRole?.revision || Number(flags.get('role-revision') || 0), ...roleInput
3077
+ });
3078
+ if (registered.repositoryRole?.profileHash !== profileHash) {
3079
+ const revision = Number(role.role?.revision);
3080
+ if (!Number.isSafeInteger(revision) || revision < 1)
3081
+ throw new Error('Repository role registration returned no durable revision.');
3082
+ registered = { ...registered, repositoryRole: { revision, profileHash } };
3083
+ await saveWorkspaceRecord(registered);
3084
+ }
2823
3085
  const nativeSkillResult = await installAvailableNativeAgentFabricBootstraps({
2824
3086
  providers,
2825
3087
  workspace,
@@ -2828,9 +3090,17 @@ async function onboard(flags) {
2828
3090
  hqUrl,
2829
3091
  });
2830
3092
  const primaryProvider = providers[0]?.provider || 'codex';
3093
+ const relay = flags.has('no-relay-daemon')
3094
+ ? { started: false, state: 'not_started' }
3095
+ : await startRelayDaemon(resolve(workspace, '.dharma', 'approved-policy.json'));
3096
+ const sharedRepositoryReady = await repositorySharedReady(registered);
2831
3097
  return {
2832
3098
  ok: true,
2833
- stage: nativeSkillResult.failures.length ? 'ready_with_provider_actions' : 'ready',
3099
+ stage: sharedRepositoryReady ? 'ready' : 'shared_repository_pending',
3100
+ localStage: nativeSkillResult.failures.length ? 'ready_with_provider_actions' : 'ready',
3101
+ sharedRepositoryReady,
3102
+ sharedRepository: { authority: sharedRepositoryReady ? 'signed_repository_release' : 'local_inventory_not_signed',
3103
+ publication: candidate.state, activation: sharedRepositoryReady },
2834
3104
  organizationId,
2835
3105
  workspaceId: registered.workspaceId,
2836
3106
  repositoryAgent: {
@@ -2849,6 +3119,14 @@ async function onboard(flags) {
2849
3119
  writePaths: generatedPolicy.tasks.writePaths,
2850
3120
  },
2851
3121
  repositorySkill: installed,
3122
+ repositoryCandidate: candidate,
3123
+ firstLearningEvidence: {
3124
+ discovered: Number(onboardingEvidencePreview.trajectoryCount || 0),
3125
+ disclosureReady: onboardingEvidencePreview.automaticDisclosure?.ready === true,
3126
+ ...onboardingEvidence,
3127
+ },
3128
+ repositoryRole: role,
3129
+ relay,
2852
3130
  nativeSkills: nativeSkillResult.installed,
2853
3131
  nativeSkillFailures: nativeSkillResult.failures,
2854
3132
  workspaceSync: synced,
@@ -4042,7 +4320,8 @@ function containedInlinePath(root, value) {
4042
4320
  }
4043
4321
  return candidate;
4044
4322
  }
4045
- export async function materializeInlineSkillFiles(bundle, sourceRoot) {
4323
+ export async function materializeInlineSkillFiles(bundle, sourceRoot, assertCurrent = () => { }) {
4324
+ assertCurrent();
4046
4325
  if (bundle.operation === 'clear')
4047
4326
  return false;
4048
4327
  const inline = bundle.skills.map((skill) => skill.files);
@@ -4073,8 +4352,11 @@ export async function materializeInlineSkillFiles(bundle, sourceRoot) {
4073
4352
  if (digest !== file.sha256)
4074
4353
  throw new Error(`Inline skill file hash mismatch: ${file.path}`);
4075
4354
  const destination = containedInlinePath(skillRoot, file.path);
4355
+ assertCurrent();
4076
4356
  await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
4357
+ assertCurrent();
4077
4358
  await writeFile(destination, content, { mode: 0o600 });
4359
+ assertCurrent();
4078
4360
  }
4079
4361
  }
4080
4362
  return true;
@@ -4110,22 +4392,68 @@ export async function recoverLegacySkillBundleIdAfterAuthorizationFailure(input)
4110
4392
  export async function installedBundleIdForSkillPollAfterAuthorizationFailure(input) {
4111
4393
  return recoverLegacySkillBundleIdAfterAuthorizationFailure(input);
4112
4394
  }
4113
- async function skillSync(flags) {
4114
- const workspaceId = required(flags, 'workspace-id');
4115
- const providerValue = required(flags, 'provider');
4116
- if (!isLocalProviderId(providerValue))
4117
- throw new Error('Skill provider must be codex, claude, agy, or hermes.');
4118
- const provider = providerValue;
4119
- return withWorkspaceSkillActivationLock(workspaceId, provider, async () => {
4120
- const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
4121
- if (!workspace)
4122
- throw new Error('Skill workspace is not registered locally.');
4123
- const policy = await loadOrganizationPolicy(required(flags, 'policy'));
4124
- const destination = nativeSkillDirectory(provider);
4125
- const fabric = await client();
4126
- const config = JSON.parse(await readFile(configPath(), 'utf8'));
4127
- let activeBundleId;
4128
- let legacyBaselineMigrationRequested = false;
4395
+ export async function loadSkillSynchronizationPolicy(policyPath, workspaceId, secureStore) {
4396
+ const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
4397
+ if (!workspace)
4398
+ throw new Error('Skill workspace is not registered locally.');
4399
+ const [providedPath, canonicalPath] = await Promise.all([
4400
+ canonicalFilesystemPath(resolve(policyPath)),
4401
+ canonicalFilesystemPath(resolve(workspace.path, '.dharma', 'approved-policy.json')),
4402
+ ]);
4403
+ if (providedPath !== canonicalPath) {
4404
+ throw new Error('Skill synchronization requires the canonical registered workspace policy path.');
4405
+ }
4406
+ const policy = await loadOrganizationPolicy(policyPath);
4407
+ const config = await readDeviceConfig();
4408
+ if (!config || config.organizationId !== workspace.organizationId || policy.organizationId !== workspace.organizationId
4409
+ || workspace.status !== 'active') {
4410
+ throw new Error('Skill policy does not match the enrolled organization and workspace.');
4411
+ }
4412
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: config.serverPublicKeyEd25519,
4413
+ organizationId: config.organizationId, workspaceId });
4414
+ await assertWorkspaceAuthorizationCurrent(workspaceId, policy.serverAuthorization);
4415
+ const enrollment = await loadDeviceEnrollmentAnchor({ config, store: secureStore });
4416
+ await assertWorkspaceAuthorizationCurrent(workspaceId, policy.serverAuthorization);
4417
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: enrollment.serverPublicKeyEd25519,
4418
+ organizationId: config.organizationId, workspaceId });
4419
+ return { workspace, policy, config, enrollment };
4420
+ }
4421
+ export function parseSkillRolloutResponse(response, organizationId) {
4422
+ if (!response || typeof response !== 'object' || Array.isArray(response))
4423
+ throw new Error('Skill rollout response is invalid.');
4424
+ const value = response;
4425
+ if (value.ok !== true || value.organizationId !== organizationId || !Object.hasOwn(value, 'rollout')) {
4426
+ throw new Error('Skill rollout response does not match the authenticated organization contract.');
4427
+ }
4428
+ const rollout = value.rollout;
4429
+ if (rollout === null)
4430
+ return null;
4431
+ if (!rollout || typeof rollout !== 'object' || Array.isArray(rollout))
4432
+ throw new Error('Skill rollout response is invalid.');
4433
+ const candidate = rollout;
4434
+ if (typeof candidate.id !== 'string' || !candidate.id.trim() || !candidate.bundle
4435
+ || typeof candidate.bundle !== 'object' || Array.isArray(candidate.bundle))
4436
+ throw new Error('Skill rollout response is invalid.');
4437
+ return rollout;
4438
+ }
4439
+ export async function prepareSkillUpdate(input) {
4440
+ const { workspaceId, provider, policyPath } = input;
4441
+ const assertRunning = input.assertRunning || (() => { });
4442
+ assertRunning();
4443
+ const context = await loadSkillSynchronizationPolicy(policyPath, workspaceId, input.store);
4444
+ const { workspace, policy, config } = context;
4445
+ assertRunning();
4446
+ const destination = nativeSkillDirectory(provider);
4447
+ const fabric = input.fabric || await client();
4448
+ assertRunning();
4449
+ let activeBundleId;
4450
+ let legacyBaselineMigrationRequested = false;
4451
+ if (input.automatic) {
4452
+ activeBundleId = (await loadActiveSkillAuthorizationAnchor({ config, workspaceId,
4453
+ organizationAgentId: String(workspace.repositoryAgentId || ''), provider, store: input.store, fresh: true }))?.bundleId ?? null;
4454
+ assertRunning();
4455
+ }
4456
+ else {
4129
4457
  try {
4130
4458
  activeBundleId = (await activeSkillAuthorization(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
4131
4459
  }
@@ -4142,152 +4470,330 @@ async function skillSync(flags) {
4142
4470
  legacyBaselineMigrationRequested = true;
4143
4471
  }
4144
4472
  }
4145
- const response = await fabric.pollSkill({
4146
- workspaceId,
4147
- provider,
4148
- installedBundleId: activeBundleId,
4149
- legacyBaselineMigrationRequested,
4473
+ }
4474
+ const pollRequest = {
4475
+ workspaceId,
4476
+ provider,
4477
+ installedBundleId: activeBundleId,
4478
+ legacyBaselineMigrationRequested,
4479
+ repositoryPackageProtocol: 'dharma.repository-package-envelope/v1',
4480
+ };
4481
+ assertRunning();
4482
+ const response = await fabric.pollSkill(pollRequest);
4483
+ assertRunning();
4484
+ const rollout = parseSkillRolloutResponse(response, config.organizationId);
4485
+ if (!rollout)
4486
+ return null;
4487
+ let bundle = rollout.bundle;
4488
+ if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
4489
+ || (bundle.operation === 'install' && bundle.skills.length === 0)
4490
+ || (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
4491
+ throw new Error('Skill bundle does not match local organization policy.');
4492
+ }
4493
+ const sources = validateSkillBundleSources(bundle);
4494
+ const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
4495
+ const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
4496
+ const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
4497
+ verifySkillBundle(bundle, serverPublicKey);
4498
+ for (const [selectors, value] of [[bundle.targetSelectors.organizationAgentIds, String(workspace.repositoryAgentId || '')],
4499
+ [bundle.targetSelectors.deviceIds, config.deviceId], [bundle.targetSelectors.workspaceIds, workspaceId],
4500
+ [bundle.targetSelectors.providers, provider]]) {
4501
+ if (selectors.length && !selectors.includes(value))
4502
+ throw new Error('Skill bundle does not target this enrolled endpoint.');
4503
+ }
4504
+ let repositoryDelivery;
4505
+ if (rollout.repositoryPackage !== undefined) {
4506
+ if (workspace.organizationId !== config.organizationId || policy.organizationId !== config.organizationId
4507
+ || !workspace.repositoryBindingId || !workspace.repositoryAgentId) {
4508
+ throw new Error('Repository delivery requires the enrolled organization and complete repository binding.');
4509
+ }
4510
+ const query = new URLSearchParams({ rolloutId: rollout.id, workspaceId, provider });
4511
+ const packageRoute = `/agent-fabric/skills/${encodeURIComponent(bundle.bundleId)}/repository-package`;
4512
+ const readPackagePart = async (route, field) => {
4513
+ assertRunning();
4514
+ const part = await fabric.signedGet(route);
4515
+ assertRunning();
4516
+ if (part.ok !== true || part.organizationId !== config.organizationId || !Object.hasOwn(part, field)) {
4517
+ throw new Error('Repository delivery response does not match the authenticated route contract.');
4518
+ }
4519
+ return part[field];
4520
+ };
4521
+ repositoryDelivery = await receiveRepositoryPackageDelivery({
4522
+ envelope: rollout.repositoryPackage, bundle, serverPublicKey,
4523
+ scope: { organizationId: config.organizationId, repositoryBindingId: workspace.repositoryBindingId,
4524
+ repositoryAgentId: workspace.repositoryAgentId, deviceId: config.deviceId, workspaceId, provider },
4525
+ fetchIndex: () => readPackagePart(`${packageRoute}/index?${query}`, 'index'),
4526
+ fetchChunk: (fileIndex, chunkIndex) => readPackagePart(`${packageRoute}/chunks?${query}&fileIndex=${fileIndex}&chunkIndex=${chunkIndex}`, 'chunk'),
4150
4527
  });
4151
- const rollout = response.rollout;
4152
- if (!rollout)
4153
- return { ok: true, rollout: null, changed: false };
4154
- if (typeof rollout.id !== 'string' || !rollout.bundle || typeof rollout.bundle !== 'object')
4155
- throw new Error('Skill rollout response is invalid.');
4156
- const bundle = rollout.bundle;
4157
- if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
4158
- || (bundle.operation === 'install' && bundle.skills.length === 0)
4159
- || (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
4160
- throw new Error('Skill bundle does not match local organization policy.');
4528
+ assertRunning();
4529
+ bundle = repositoryDelivery.bundle;
4530
+ }
4531
+ const assertCurrent = () => {
4532
+ assertRunning();
4533
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: config.serverPublicKeyEd25519,
4534
+ organizationId: config.organizationId, workspaceId });
4535
+ verifySkillBundle(bundle, serverPublicKey);
4536
+ repositoryDelivery?.assertCurrent();
4537
+ };
4538
+ assertCurrent();
4539
+ const sourceParent = input.automatic ? skillPreparationScopeRoot(dharmaHome(), workspaceId, provider)
4540
+ : resolve(dharmaHome(), 'relay', 'skill-sources');
4541
+ await mkdir(sourceParent, { recursive: true, mode: 0o700 });
4542
+ assertCurrent();
4543
+ const sourceRoot = await mkdtemp(resolve(sourceParent, 'attempt-'));
4544
+ try {
4545
+ assertCurrent();
4546
+ if (repositoryDelivery) {
4547
+ for (const file of repositoryDelivery.files) {
4548
+ assertCurrent();
4549
+ const target = containedInlinePath(sourceRoot, file.path);
4550
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
4551
+ assertCurrent();
4552
+ await writeFile(target, Buffer.from(file.contentBase64, 'base64'), { mode: 0o600, flag: 'wx' });
4553
+ assertCurrent();
4554
+ }
4161
4555
  }
4162
- const sources = validateSkillBundleSources(bundle);
4163
- const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
4164
- const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
4165
- const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
4166
- verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
4167
- await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
4168
- await rm(sourceRoot, { recursive: true, force: true });
4169
- await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
4170
- const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
4171
- if (bundle.operation === 'install' && !materializedInline) {
4556
+ const materializedInline = repositoryDelivery ? false : await materializeInlineSkillFiles(bundle, sourceRoot, assertCurrent);
4557
+ assertCurrent();
4558
+ if (bundle.operation === 'install' && !materializedInline && !repositoryDelivery) {
4172
4559
  if (!sources.singleSource)
4173
4560
  throw new Error('Multi-source skill bundles require signed inline files for every skill.');
4561
+ if (input.automatic)
4562
+ throw new Error('Automatic preparation requires bounded inline files or an authenticated complete repository package.');
4174
4563
  await rm(sourceRoot, { recursive: true, force: true });
4564
+ assertCurrent();
4175
4565
  await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
4566
+ assertCurrent();
4176
4567
  await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
4568
+ assertCurrent();
4177
4569
  await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
4570
+ assertCurrent();
4178
4571
  }
4179
- try {
4180
- const previousAnchor = await loadActiveSkillAuthorizationAnchor({
4181
- config,
4182
- workspaceId,
4183
- organizationAgentId: String(workspace.repositoryAgentId || ''),
4184
- provider,
4185
- });
4186
- const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
4187
- const receipt = await installSkillBundle({
4188
- bundle,
4189
- sourceDirectory: sourceRoot,
4190
- nativeSkillDirectory: destination,
4191
- policy,
4192
- serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
4193
- devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
4194
- deviceId: config.deviceId,
4195
- organizationAgentId: String(workspace.repositoryAgentId || ''),
4196
- workspaceId,
4197
- provider,
4198
- smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
4199
- organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
4200
- ...(provider === 'agy' ? {
4201
- providerActivationCheck: () => attestAgySkillBundleActivation({
4202
- bundle,
4203
- nativeSkillDirectory: destination,
4204
- workspace: workspace.path,
4205
- }),
4206
- } : provider === 'hermes' ? {
4207
- providerActivationCheck: () => attestHermesSkillBundleActivation({
4208
- bundle,
4209
- workspace: workspace.path,
4210
- nativeSkillDirectory: destination,
4211
- }),
4212
- } : {}),
4213
- });
4214
- const restoreAnchor = async () => {
4215
- if (previousAnchor) {
4216
- await saveActiveSkillAuthorizationAnchor({
4217
- config,
4218
- workspaceId,
4219
- organizationAgentId: previousAnchor.organizationAgentId,
4220
- provider,
4221
- bundleId: previousAnchor.bundleId,
4222
- receiptHash: previousAnchor.receiptHash,
4223
- activatedAt: previousAnchor.activatedAt,
4224
- expiresAt: previousAnchor.expiresAt,
4225
- });
4226
- }
4227
- else {
4228
- await deleteActiveSkillAuthorizationAnchor({ config, workspaceId, provider });
4229
- }
4230
- };
4231
- const recoverLocalInstallation = async (error) => {
4232
- const recoveryErrors = [];
4233
- try {
4234
- await rollbackUnconfirmedSkillBundle({
4235
- nativeSkillDirectory: destination,
4236
- workspaceId,
4237
- receipt,
4238
- });
4239
- }
4240
- catch (rollbackError) {
4241
- recoveryErrors.push(rollbackError);
4242
- }
4243
- try {
4244
- await restoreAnchor();
4245
- }
4246
- catch (anchorError) {
4247
- recoveryErrors.push(anchorError);
4248
- }
4249
- if (recoveryErrors.length) {
4250
- throw new AggregateError([error, ...recoveryErrors], 'Skill installation failed and local recovery was incomplete.');
4251
- }
4252
- throw error;
4253
- };
4572
+ for (const skill of bundle.skills) {
4573
+ const actual = await contentHash(containedInlinePath(sourceRoot, skill.path));
4574
+ assertCurrent();
4575
+ if (actual !== skill.contentHash)
4576
+ throw new Error('Prepared skill tree does not match the signed content hash.');
4577
+ }
4578
+ const current = await loadSkillSynchronizationPolicy(policyPath, workspaceId, input.store);
4579
+ assertCurrent();
4580
+ if (canonicalize(current) !== canonicalize(context))
4581
+ throw new Error('Skill preparation scope or policy changed; retry with current authorization.');
4582
+ return { workspace, policy, config, destination, fabric, rollout, bundle, sourceRoot, repositoryDelivery, assertCurrent };
4583
+ }
4584
+ catch (error) {
4585
+ await rm(sourceRoot, { recursive: true, force: true });
4586
+ throw error;
4587
+ }
4588
+ }
4589
+ async function activatePreparedSkillUpdate(input) {
4590
+ const { prepared, workspaceId, provider, policyPath, flags } = input;
4591
+ const { workspace, policy, config, destination, fabric, rollout, bundle, sourceRoot, repositoryDelivery } = prepared;
4592
+ try {
4593
+ const previousAnchor = await loadActiveSkillAuthorizationAnchor({
4594
+ config,
4595
+ workspaceId,
4596
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
4597
+ provider,
4598
+ });
4599
+ const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
4600
+ repositoryDelivery?.assertCurrent();
4601
+ const current = await loadSkillSynchronizationPolicy(policyPath, workspaceId);
4602
+ if (canonicalize(current.policy) !== canonicalize(policy)
4603
+ || canonicalize(current.workspace) !== canonicalize(workspace)
4604
+ || canonicalize(current.config) !== canonicalize(config)) {
4605
+ throw new Error('Skill synchronization scope or policy changed before activation; retry with current authorization.');
4606
+ }
4607
+ repositoryDelivery?.assertCurrent();
4608
+ const receipt = await installSkillBundle({
4609
+ bundle,
4610
+ sourceDirectory: sourceRoot,
4611
+ nativeSkillDirectory: destination,
4612
+ policy,
4613
+ serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
4614
+ devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
4615
+ deviceId: config.deviceId,
4616
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
4617
+ workspaceId,
4618
+ provider,
4619
+ smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
4620
+ organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
4621
+ ...(provider === 'agy' ? {
4622
+ providerActivationCheck: () => attestAgySkillBundleActivation({
4623
+ bundle,
4624
+ nativeSkillDirectory: destination,
4625
+ workspace: workspace.path,
4626
+ }),
4627
+ } : provider === 'hermes' ? {
4628
+ providerActivationCheck: () => attestHermesSkillBundleActivation({
4629
+ bundle,
4630
+ workspace: workspace.path,
4631
+ nativeSkillDirectory: destination,
4632
+ }),
4633
+ } : {}),
4634
+ });
4635
+ const restoreAnchor = async () => {
4636
+ if (previousAnchor) {
4637
+ await saveActiveSkillAuthorizationAnchor({
4638
+ config,
4639
+ workspaceId,
4640
+ organizationAgentId: previousAnchor.organizationAgentId,
4641
+ provider,
4642
+ bundleId: previousAnchor.bundleId,
4643
+ receiptHash: previousAnchor.receiptHash,
4644
+ activatedAt: previousAnchor.activatedAt,
4645
+ expiresAt: previousAnchor.expiresAt,
4646
+ });
4647
+ }
4648
+ else {
4649
+ await deleteActiveSkillAuthorizationAnchor({ config, workspaceId, provider });
4650
+ }
4651
+ };
4652
+ const recoverLocalInstallation = async (error) => {
4653
+ const recoveryErrors = [];
4254
4654
  try {
4255
- if (receipt.status === 'active') {
4256
- await saveActiveSkillAuthorizationAnchor({
4257
- config,
4258
- workspaceId,
4259
- organizationAgentId: String(workspace.repositoryAgentId || ''),
4260
- provider,
4261
- bundleId: receipt.bundleId,
4262
- receiptHash: receipt.receiptHash,
4263
- activatedAt: receipt.completedAt,
4264
- expiresAt: bundle.expiresAt ?? null,
4265
- });
4266
- }
4655
+ await rollbackUnconfirmedSkillBundle({
4656
+ nativeSkillDirectory: destination,
4657
+ workspaceId,
4658
+ receipt,
4659
+ });
4267
4660
  }
4268
- catch (error) {
4269
- await recoverLocalInstallation(error);
4661
+ catch (rollbackError) {
4662
+ recoveryErrors.push(rollbackError);
4270
4663
  }
4271
4664
  try {
4272
- await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
4665
+ await restoreAnchor();
4273
4666
  }
4274
- catch (error) {
4275
- if (isDefinitiveAgentFabricRejection(error))
4276
- await recoverLocalInstallation(error);
4277
- throw error;
4667
+ catch (anchorError) {
4668
+ recoveryErrors.push(anchorError);
4669
+ }
4670
+ if (recoveryErrors.length) {
4671
+ throw new AggregateError([error, ...recoveryErrors], 'Skill installation failed and local recovery was incomplete.');
4672
+ }
4673
+ throw error;
4674
+ };
4675
+ try {
4676
+ if (receipt.status === 'active') {
4677
+ await saveActiveSkillAuthorizationAnchor({
4678
+ config,
4679
+ workspaceId,
4680
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
4681
+ provider,
4682
+ bundleId: receipt.bundleId,
4683
+ receiptHash: receipt.receiptHash,
4684
+ activatedAt: receipt.completedAt,
4685
+ expiresAt: bundle.expiresAt ?? null,
4686
+ });
4278
4687
  }
4279
- return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
4280
4688
  }
4281
- finally {
4282
- await rm(sourceRoot, { recursive: true, force: true });
4689
+ catch (error) {
4690
+ await recoverLocalInstallation(error);
4691
+ }
4692
+ try {
4693
+ await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
4283
4694
  }
4695
+ catch (error) {
4696
+ if (isDefinitiveAgentFabricRejection(error))
4697
+ await recoverLocalInstallation(error);
4698
+ throw error;
4699
+ }
4700
+ return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
4701
+ }
4702
+ finally {
4703
+ await rm(sourceRoot, { recursive: true, force: true });
4704
+ }
4705
+ }
4706
+ async function skillSync(flags) {
4707
+ const workspaceId = required(flags, 'workspace-id');
4708
+ const providerValue = required(flags, 'provider');
4709
+ if (!isLocalProviderId(providerValue))
4710
+ throw new Error('Skill provider must be codex, claude, agy, or hermes.');
4711
+ const provider = providerValue;
4712
+ return withWorkspaceSkillActivationLock(workspaceId, provider, async () => {
4713
+ const policyPath = required(flags, 'policy');
4714
+ const prepared = await prepareSkillUpdate({ workspaceId, provider, policyPath });
4715
+ if (!prepared)
4716
+ return { ok: true, rollout: null, changed: false };
4717
+ return activatePreparedSkillUpdate({ prepared, workspaceId, provider, policyPath, flags });
4284
4718
  });
4285
4719
  }
4720
+ async function installedRepositoryKnowledge(workspace) {
4721
+ const config = await readDeviceConfig();
4722
+ if (!config || config.organizationId !== workspace.organizationId || !workspace.repositoryBindingId || !workspace.repositoryAgentId) {
4723
+ throw new Error('Installed repository knowledge requires current enrolled workspace scope.');
4724
+ }
4725
+ const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
4726
+ const enrollment = await loadDeviceEnrollmentAnchor({ config });
4727
+ if (identity.publicKeyEd25519 !== enrollment.devicePublicKeyEd25519)
4728
+ throw new Error('Installed knowledge device identity mismatch.');
4729
+ const organizationAgentId = workspace.repositoryAgentId;
4730
+ return selectInstalledRepositoryKnowledge({ organizationId: config.organizationId,
4731
+ repositoryBindingId: workspace.repositoryBindingId, repositoryAgentId: organizationAgentId,
4732
+ loadProvider: async (provider) => {
4733
+ const root = nativeSkillDirectory(provider);
4734
+ if (!await pathExistsOrThrow(resolve(root, '.dharma-managed/workspaces', workspace.workspaceId, 'ACTIVE_BUNDLE')))
4735
+ return null;
4736
+ const anchorInput = { config, workspaceId: workspace.workspaceId, organizationAgentId, provider };
4737
+ const active = await loadActiveSkillAuthorizationAnchor(anchorInput);
4738
+ if (!active)
4739
+ throw new Error('Installed knowledge release lacks a protected authorization anchor.');
4740
+ const read = (expectedReceiptHash) => readVerifiedRepositoryKnowledge({
4741
+ nativeSkillDirectory: root, workspaceId: workspace.workspaceId, provider,
4742
+ organizationId: config.organizationId, organizationAgentId, deviceId: config.deviceId,
4743
+ serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: enrollment.serverPublicKeyEd25519 }, format: 'jwk' }),
4744
+ devicePublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: identity.publicKeyEd25519 }, format: 'jwk' }), expectedReceiptHash,
4745
+ });
4746
+ try {
4747
+ return await read(active.receiptHash);
4748
+ }
4749
+ catch (error) {
4750
+ if (!(error instanceof Error) || error.message !== 'Active bundle receipt is not the current protected authorization.')
4751
+ throw error;
4752
+ const refreshed = await loadActiveSkillAuthorizationAnchor({ ...anchorInput, fresh: true });
4753
+ if (!refreshed)
4754
+ throw error;
4755
+ return read(refreshed.receiptHash);
4756
+ }
4757
+ } });
4758
+ }
4759
+ async function takeCachedSkillUpdate(input) {
4760
+ const context = await loadSkillSynchronizationPolicy(input.policyPath, input.workspaceId);
4761
+ const { workspace, policy, config } = context;
4762
+ if (!workspace.repositoryAgentId)
4763
+ return null;
4764
+ const cached = await takeSkillPreparationCache({ home: dharmaHome(), workspaceId: input.workspaceId,
4765
+ provider: input.provider, organizationId: config.organizationId, deviceId: config.deviceId,
4766
+ repositoryAgentId: workspace.repositoryAgentId, repositoryBindingId: workspace.repositoryBindingId ?? null,
4767
+ policyHash: sha256(canonicalize(policy)), assertCurrent: () => {
4768
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: config.serverPublicKeyEd25519,
4769
+ organizationId: config.organizationId, workspaceId: input.workspaceId });
4770
+ } });
4771
+ if (!cached)
4772
+ return null;
4773
+ try {
4774
+ const bundle = cached.record.bundle;
4775
+ const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
4776
+ verifySkillBundle(bundle, serverPublicKey);
4777
+ for (const skill of bundle.skills) {
4778
+ if (await contentHash(containedInlinePath(cached.sourceRoot, skill.path)) !== skill.contentHash) {
4779
+ throw new Error('Cached skill tree does not match the signed content hash.');
4780
+ }
4781
+ }
4782
+ return { workspace, policy, config, destination: nativeSkillDirectory(input.provider), fabric: input.fabric,
4783
+ rollout: { id: String(cached.record.rolloutId), bundle }, bundle, sourceRoot: cached.sourceRoot,
4784
+ repositoryDelivery: undefined, assertCurrent: () => verifySkillBundle(bundle, serverPublicKey) };
4785
+ }
4786
+ catch (error) {
4787
+ await rm(cached.sourceRoot, { recursive: true, force: true });
4788
+ throw error;
4789
+ }
4790
+ }
4286
4791
  async function relayStart(flags) {
4287
4792
  const policyPath = resolve(required(flags, 'policy'));
4288
- const canonicalWorkspace = (await registry()).find((item) => resolve(item.path, '.dharma', 'approved-policy.json') === policyPath);
4289
- if (!canonicalWorkspace)
4793
+ const selectedWorkspace = (await registry()).find((item) => resolve(item.path, '.dharma', 'approved-policy.json') === policyPath);
4794
+ if (!selectedWorkspace)
4290
4795
  throw new Error('Relay policy must be the canonical policy of one registered workspace.');
4796
+ let canonicalWorkspace = selectedWorkspace;
4291
4797
  let policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
4292
4798
  const fabric = await client();
4293
4799
  const leaseSeconds = Number(flags.get('lease-seconds') || 120);
@@ -4296,7 +4802,8 @@ async function relayStart(flags) {
4296
4802
  await mkdir(resolve(dharmaHome(), 'relay'), { recursive: true, mode: 0o700 });
4297
4803
  await writeFile(pidPath, `${process.pid}\n`, { mode: 0o600 });
4298
4804
  let stopping = false;
4299
- const stop = () => { stopping = true; };
4805
+ let stopPreparation;
4806
+ const stop = () => { stopping = true; stopPreparation?.(); };
4300
4807
  process.once('SIGINT', stop);
4301
4808
  process.once('SIGTERM', stop);
4302
4809
  let tasksCompleted = 0;
@@ -4305,12 +4812,80 @@ async function relayStart(flags) {
4305
4812
  let trajectorySyncsCompleted = 0;
4306
4813
  let nextPolicyRefreshAt = 0;
4307
4814
  let evidencePolicyFresh = false;
4815
+ const repositorySourceWatcher = new RepositorySourceWatcher();
4816
+ let nextRepositorySourceScanAt = 0;
4817
+ let repositorySourceCandidates = 0;
4818
+ let repositorySourceFailures = 0;
4819
+ let repositorySourceState = 'not_scanned';
4308
4820
  const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
4309
4821
  const vault = await LocalVault.open({
4310
4822
  root: resolve(dharmaHome(), 'vault'),
4311
4823
  masterKey: await loadOrCreateVaultMasterKey(),
4312
4824
  rawLocalDays: rawLocalRetentionDays(policy),
4313
4825
  });
4826
+ let skillPreparationsCompleted = 0;
4827
+ let skillPreparationFailures = 0;
4828
+ let skillActivationsCompleted = 0;
4829
+ let skillActivationFailures = 0;
4830
+ let nextSkillActivationAt = 0;
4831
+ const skillPreparationPump = startSkillPreparationPump({
4832
+ prepare: async (assertRunning) => {
4833
+ await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
4834
+ assertRunning();
4835
+ const capability = await adapter.capability();
4836
+ assertRunning();
4837
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
4838
+ return;
4839
+ const provider = adapter.providerId;
4840
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
4841
+ provider, assertCurrent: assertRunning }, async () => {
4842
+ const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
4843
+ provider, policyPath, fabric, automatic: true, assertRunning });
4844
+ if (!prepared) {
4845
+ assertRunning();
4846
+ return;
4847
+ }
4848
+ let retained = false;
4849
+ try {
4850
+ prepared.assertCurrent();
4851
+ const repositoryAgentId = prepared.workspace.repositoryAgentId;
4852
+ if (!repositoryAgentId)
4853
+ throw new Error('Preparation cache requires an enrolled repository agent.');
4854
+ const pendingRecord = await serializeSkillPreparationRecord({
4855
+ schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
4856
+ deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
4857
+ repositoryAgentId,
4858
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
4859
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
4860
+ bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
4861
+ ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
4862
+ preparedAt: new Date().toISOString(), activationAuthorized: false,
4863
+ });
4864
+ prepared.assertCurrent();
4865
+ await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
4866
+ prepared.assertCurrent();
4867
+ await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
4868
+ provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
4869
+ expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
4870
+ repositoryAgentId,
4871
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
4872
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
4873
+ bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
4874
+ onCommitted: () => { retained = true; } });
4875
+ skillPreparationsCompleted += 1;
4876
+ }
4877
+ finally {
4878
+ if (!retained)
4879
+ await rm(prepared.sourceRoot, { recursive: true, force: true });
4880
+ }
4881
+ });
4882
+ }, () => { skillPreparationFailures += 1; });
4883
+ },
4884
+ onError: () => { skillPreparationFailures += 1; },
4885
+ });
4886
+ stopPreparation = skillPreparationPump.requestStop;
4887
+ if (stopping)
4888
+ stopPreparation();
4314
4889
  try {
4315
4890
  const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
4316
4891
  if (recoveredTaskTrajectories.state === 'completed') {
@@ -4328,6 +4903,54 @@ async function relayStart(flags) {
4328
4903
  }
4329
4904
  nextPolicyRefreshAt = Date.now() + 60_000;
4330
4905
  }
4906
+ if (canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
4907
+ try {
4908
+ const current = await pollRepositoryCandidate({ transport: fabric,
4909
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
4910
+ scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
4911
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
4912
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
4913
+ if (current && canonicalWorkspace.repositoryPackage) {
4914
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
4915
+ state: current.state, candidateId: current.candidateId, operationId: current.operationId,
4916
+ snapshotHash: current.snapshotHash, releaseId: current.releaseId,
4917
+ generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
4918
+ await saveWorkspaceRecord(canonicalWorkspace);
4919
+ }
4920
+ }
4921
+ catch {
4922
+ repositorySourceFailures += 1;
4923
+ }
4924
+ }
4925
+ if (performance.now() >= nextRepositorySourceScanAt) {
4926
+ try {
4927
+ if (!canonicalWorkspace.repositoryBindingId || !canonicalWorkspace.repositoryAgentId) {
4928
+ throw new Error('Repository source synchronization requires a complete repository binding.');
4929
+ }
4930
+ const sourceCycle = await scanRepositorySourceChanges({
4931
+ workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
4932
+ workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
4933
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
4934
+ loadRetainedKnowledge: () => installedRepositoryKnowledge(canonicalWorkspace),
4935
+ submitCandidate: snapshot => synchronizeRepositoryCandidate({ transport: fabric,
4936
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
4937
+ scope: { organizationId: canonicalWorkspace.organizationId,
4938
+ workspaceId: canonicalWorkspace.workspaceId,
4939
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
4940
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId },
4941
+ snapshot, initialRepository: false }),
4942
+ });
4943
+ repositorySourceState = sourceCycle.state;
4944
+ if (sourceCycle.localMutation)
4945
+ repositorySourceCandidates += 1;
4946
+ }
4947
+ catch {
4948
+ repositorySourceWatcher.invalidate();
4949
+ repositorySourceFailures += 1;
4950
+ repositorySourceState = 'blocked';
4951
+ }
4952
+ nextRepositorySourceScanAt = performance.now() + 60_000;
4953
+ }
4331
4954
  let evidenceRequestId;
4332
4955
  if (evidencePolicyFresh) {
4333
4956
  const evidenceCycle = await deferUnavailableRelayRetention(async () => {
@@ -4351,6 +4974,37 @@ async function relayStart(flags) {
4351
4974
  const result = await executeOneTask(fabric, leaseSeconds);
4352
4975
  if (result.taskId)
4353
4976
  tasksCompleted += 1;
4977
+ if (!result.taskId && performance.now() >= nextSkillActivationAt) {
4978
+ for (const adapter of providerAdapters) {
4979
+ try {
4980
+ const capability = await adapter.capability();
4981
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
4982
+ continue;
4983
+ const provider = adapter.providerId;
4984
+ await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
4985
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
4986
+ provider, assertCurrent: () => { if (stopping)
4987
+ throw new Error('Relay is stopping.'); } }, async () => {
4988
+ const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
4989
+ provider, policyPath, fabric });
4990
+ const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
4991
+ provider, policyPath, fabric, automatic: true });
4992
+ if (!prepared)
4993
+ return;
4994
+ if (!cached)
4995
+ skillPreparationsCompleted += 1;
4996
+ await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
4997
+ provider, policyPath, flags: new Map() });
4998
+ skillActivationsCompleted += 1;
4999
+ });
5000
+ });
5001
+ }
5002
+ catch {
5003
+ skillActivationFailures += 1;
5004
+ }
5005
+ }
5006
+ nextSkillActivationAt = performance.now() + 60_000;
5007
+ }
4354
5008
  if (flags.has('once'))
4355
5009
  break;
4356
5010
  if (!result.taskId && !evidenceRequestId)
@@ -4358,6 +5012,7 @@ async function relayStart(flags) {
4358
5012
  } while (!stopping);
4359
5013
  }
4360
5014
  finally {
5015
+ await skillPreparationPump.stop();
4361
5016
  vault.close();
4362
5017
  process.removeListener('SIGINT', stop);
4363
5018
  process.removeListener('SIGTERM', stop);
@@ -4366,6 +5021,9 @@ async function relayStart(flags) {
4366
5021
  return {
4367
5022
  ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
4368
5023
  evidenceResponsesCompleted, trajectorySyncsCompleted,
5024
+ repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
5025
+ skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
5026
+ sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
4369
5027
  };
4370
5028
  }
4371
5029
  export async function run(argv) {
@@ -4393,6 +5051,13 @@ export async function run(argv) {
4393
5051
  }
4394
5052
  if (command === 'repositories' && subcommand === 'list')
4395
5053
  return repositoriesList(flags);
5054
+ if (command === 'repositories' && subcommand === 'snapshot') {
5055
+ return repositorySnapshotCommand(flags, repeated.get('approved-output') || []);
5056
+ }
5057
+ if (command === 'repositories' && ['role-register', 'role-discover', 'ask', 'reply'].includes(String(subcommand))) {
5058
+ const action = subcommand === 'role-register' ? 'register' : subcommand === 'role-discover' ? 'discover' : subcommand;
5059
+ return repositoryRoleCommand(action, flags);
5060
+ }
4396
5061
  if (command === 'repositories' && subcommand === 'status') {
4397
5062
  const listed = await repositoriesList(flags);
4398
5063
  return {