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

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 +826 -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 +197 -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.50';
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)
@@ -1464,12 +1487,32 @@ async function bootstrap(flags) {
1464
1487
  onboardFlags.set('policy-revision', policyRevision);
1465
1488
  onboardFlags.set('provider', provider);
1466
1489
  const onboarded = await retryBootstrapOnboarding(async () => await onboard(onboardFlags));
1490
+ if (onboarded.ok !== true || onboarded.stage === 'approve_device') {
1491
+ return {
1492
+ ok: onboarded.ok === true,
1493
+ stage: onboarded.stage,
1494
+ code: onboarded.code,
1495
+ sharedRepositoryReady: false,
1496
+ enrollment: {
1497
+ organizationId,
1498
+ deviceId: config.deviceId,
1499
+ status: 'approved',
1500
+ organizationApiTokenStored: true,
1501
+ scopes: redeemed.organizationApiTokenScopes,
1502
+ rebind,
1503
+ },
1504
+ repository: onboarded,
1505
+ };
1506
+ }
1467
1507
  const launcher = await installStableRepositoryLauncher(workspace);
1508
+ const sharedRepositoryReady = onboarded.sharedRepositoryReady === true;
1468
1509
  const completionRequested = flags.has('complete');
1469
1510
  if (!completionRequested) {
1470
1511
  return {
1471
1512
  ok: true,
1472
- stage: onboarded.stage,
1513
+ stage: sharedRepositoryReady ? onboarded.stage : 'shared_repository_pending',
1514
+ localStage: onboarded.localStage ?? onboarded.stage,
1515
+ sharedRepositoryReady,
1473
1516
  enrollment: {
1474
1517
  organizationId,
1475
1518
  deviceId: config.deviceId,
@@ -1524,7 +1567,9 @@ async function bootstrap(flags) {
1524
1567
  });
1525
1568
  return {
1526
1569
  ok: true,
1527
- stage: 'complete',
1570
+ stage: sharedRepositoryReady ? 'complete' : 'shared_repository_pending',
1571
+ localStage: 'complete',
1572
+ sharedRepositoryReady,
1528
1573
  enrollment: {
1529
1574
  organizationId,
1530
1575
  deviceId: config.deviceId,
@@ -2366,7 +2411,22 @@ async function repositoriesConnect(flags, positional, repeatedRepos, repeatedKey
2366
2411
  if (result?.stage === 'approve_device')
2367
2412
  break;
2368
2413
  }
2369
- return { ok: true, requested: unique.length, connected: results.length, repositories: results };
2414
+ const states = results.map(result => result);
2415
+ const blocked = states.filter(result => result.ok !== true).length;
2416
+ const connected = states.filter(result => result.ok === true && result.sharedRepositoryReady === true).length;
2417
+ const locallyReady = states.filter(result => result.ok === true && (['ready', 'ready_with_provider_actions', 'complete'].includes(String(result.localStage))
2418
+ || ['ready', 'ready_with_provider_actions', 'complete'].includes(String(result.stage)))).length;
2419
+ return {
2420
+ ok: blocked === 0,
2421
+ requested: unique.length,
2422
+ attempted: results.length,
2423
+ locallyReady,
2424
+ connected,
2425
+ blocked,
2426
+ pending: states.length - blocked - connected,
2427
+ sharedRepositoryReady: connected === unique.length,
2428
+ repositories: results,
2429
+ };
2370
2430
  }
2371
2431
  export function parseSelectedProviderIds(values) {
2372
2432
  const selected = [...new Set(values
@@ -2445,22 +2505,72 @@ export async function receiptAwareProviderCapabilities(providers, now = new Date
2445
2505
  }
2446
2506
  async function repositoriesList(flags) {
2447
2507
  const verbose = flags.has('verbose') || flags.has('diagnostic');
2508
+ const items = await registry();
2448
2509
  return {
2449
2510
  ok: true,
2450
- repositories: (await registry()).map((item) => ({
2511
+ repositories: await Promise.all(items.map(async (item) => ({
2451
2512
  workspaceId: item.workspaceId,
2452
2513
  name: item.name,
2453
2514
  repositoryAgentId: item.repositoryAgentId || null,
2454
2515
  repositoryAgentKey: item.repositoryAgentKey || null,
2455
2516
  controlBranch: item.controlBranch || null,
2456
2517
  connected: Boolean(item.repositoryAgentId && item.controlBranch),
2518
+ repositoryPackageState: item.repositoryPackage?.state || 'unknown',
2519
+ sharedRepositoryReady: await repositorySharedReady(item),
2457
2520
  ...(verbose ? { localPath: item.path, sourceFingerprint: item.repositoryRemoteHash } : {}),
2458
- })),
2521
+ }))),
2459
2522
  };
2460
2523
  }
2524
+ async function repositorySharedReady(item) {
2525
+ if (item.repositoryPackage?.state !== 'published' || !item.repositoryPackage.releaseId)
2526
+ return false;
2527
+ try {
2528
+ return Boolean(await installedRepositoryKnowledge(item));
2529
+ }
2530
+ catch {
2531
+ return false;
2532
+ }
2533
+ }
2534
+ export function canonicalRepositoryPackage(value) {
2535
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
2536
+ throw new Error('Dharma HQ did not return canonical repository package state.');
2537
+ }
2538
+ const row = value;
2539
+ const keys = ['state', 'candidateId', 'operationId', 'snapshotHash', 'sourceManifestHash', 'releaseId', 'generation', 'consolidationMode'];
2540
+ if (!keys.every(key => Object.hasOwn(row, key)) || Object.keys(row).some(key => !keys.includes(key))
2541
+ || !['absent', 'accepted', 'processing', 'published', 'blocked'].includes(String(row.state))
2542
+ || !Number.isSafeInteger(row.generation) || Number(row.generation) < 0) {
2543
+ throw new Error('Dharma HQ returned invalid canonical repository package state.');
2544
+ }
2545
+ const nullableUuid = (candidate) => candidate === null || typeof candidate === 'string' && UUID_PATTERN.test(candidate);
2546
+ const nullableHash = (candidate) => candidate === null || typeof candidate === 'string' && /^sha256:[a-f0-9]{64}$/.test(candidate);
2547
+ const absent = row.state === 'absent';
2548
+ if (![row.candidateId, row.releaseId].every(nullableUuid)
2549
+ || ![row.operationId, row.snapshotHash, row.sourceManifestHash].every(nullableHash)
2550
+ || ![null, 'initial_repository', 'repository_update'].includes(row.consolidationMode)
2551
+ || absent !== (row.candidateId === null && row.operationId === null && row.snapshotHash === null
2552
+ && row.sourceManifestHash === null && row.releaseId === null && row.consolidationMode === null && row.generation === 0)
2553
+ || (row.state === 'published') !== (row.releaseId !== null)
2554
+ || (!absent && (row.candidateId === null || row.operationId === null || row.snapshotHash === null
2555
+ || row.sourceManifestHash === null || row.consolidationMode === null))) {
2556
+ throw new Error('Dharma HQ returned inconsistent canonical repository package state.');
2557
+ }
2558
+ return row;
2559
+ }
2560
+ export function canonicalEndpointRole(value) {
2561
+ if (value === null)
2562
+ return null;
2563
+ if (!value || typeof value !== 'object' || Array.isArray(value))
2564
+ throw new Error('Dharma HQ returned invalid endpoint role state.');
2565
+ const row = value;
2566
+ if (Object.keys(row).sort().join(',') !== 'profileHash,revision'
2567
+ || !Number.isSafeInteger(row.revision) || Number(row.revision) < 1
2568
+ || typeof row.profileHash !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(row.profileHash)) {
2569
+ throw new Error('Dharma HQ returned invalid endpoint role state.');
2570
+ }
2571
+ return { revision: Number(row.revision), profileHash: row.profileHash };
2572
+ }
2461
2573
  async function bindRepositoryAgent(fabric, item) {
2462
- if (item.repositoryAgentId && item.repositoryBindingId && item.repositoryAgentKey && item.controlBranch)
2463
- return item;
2464
2574
  const remote = await gitValue(item.path, ['config', '--get', 'remote.origin.url']);
2465
2575
  const currentIdentity = item.repositoryIdentityVersion === 'normalized-v1'
2466
2576
  ? item.repositoryRemoteHash
@@ -2481,17 +2591,25 @@ async function bindRepositoryAgent(fabric, item) {
2481
2591
  const branch = repositoryAgent?.branch && typeof repositoryAgent.branch === 'object'
2482
2592
  ? repositoryAgent.branch
2483
2593
  : null;
2594
+ const endpoint = response.endpoint && typeof response.endpoint === 'object' && !Array.isArray(response.endpoint)
2595
+ ? response.endpoint
2596
+ : null;
2484
2597
  const updated = {
2485
2598
  ...item,
2486
2599
  repositoryRemoteHash: currentIdentity,
2487
2600
  repositoryIdentityVersion: 'normalized-v1',
2488
2601
  repositoryAgentId: String(repositoryAgent?.organization_agent_id || ''),
2489
2602
  repositoryBindingId: String(repositoryAgent?.id || ''),
2603
+ endpointId: typeof response.endpointId === 'string' ? response.endpointId
2604
+ : typeof endpoint?.id === 'string' ? endpoint.id : item.endpointId || null,
2605
+ repositoryPackage: canonicalRepositoryPackage(response.repositoryPackage),
2606
+ repositoryRole: canonicalEndpointRole(response.endpointRole),
2490
2607
  repositoryAgentKey: String(repositoryAgent?.agent_key || ''),
2491
2608
  controlBranch: String(repositoryAgent?.control_branch || branch?.branch || ''),
2492
2609
  };
2493
2610
  if (!/^[0-9a-f-]{36}$/i.test(updated.repositoryAgentId || '')
2494
2611
  || !/^[0-9a-f-]{36}$/i.test(updated.repositoryBindingId || '')
2612
+ || !UUID_PATTERN.test(updated.endpointId || '')
2495
2613
  || !/^repo:[a-f0-9]{24}$/.test(updated.repositoryAgentKey || '')
2496
2614
  || !/^agents\/[a-z0-9][a-z0-9._-]*-[a-f0-9]{8}$/.test(updated.controlBranch || '')) {
2497
2615
  throw new Error('Dharma HQ returned an invalid repository-agent binding.');
@@ -2499,6 +2617,35 @@ async function bindRepositoryAgent(fabric, item) {
2499
2617
  await saveWorkspaceRecord(updated);
2500
2618
  return updated;
2501
2619
  }
2620
+ function repositoryRoleScope(item) {
2621
+ if (!item.repositoryAgentId || !item.repositoryBindingId || !item.endpointId || !item.repositoryRemoteHash) {
2622
+ throw new Error('Repository role operations require a complete signed endpoint and repository binding.');
2623
+ }
2624
+ return { organizationId: item.organizationId, workspaceId: item.workspaceId,
2625
+ repositoryBindingId: item.repositoryBindingId, repositoryAgentId: item.repositoryAgentId,
2626
+ endpointId: item.endpointId, sourceFingerprint: item.repositoryRemoteHash };
2627
+ }
2628
+ async function repositoryRoleCommand(action, flags) {
2629
+ const workspaceId = required(flags, 'workspace-id');
2630
+ const item = (await registry()).find(candidate => candidate.workspaceId === workspaceId);
2631
+ if (!item)
2632
+ throw new Error('Repository role workspace is not registered locally.');
2633
+ const scope = repositoryRoleScope(item), transport = await client();
2634
+ if (action === 'register') {
2635
+ const categories = required(flags, 'question-categories').split(',').map(value => value.trim()).filter(Boolean);
2636
+ return registerRepositoryRoleMetadata(transport, scope, { expectedRevision: Number(flags.get('expected-revision') || 0),
2637
+ roleName: required(flags, 'role-name'), questionCategories: categories,
2638
+ description: required(flags, 'role-description') });
2639
+ }
2640
+ if (action === 'discover') {
2641
+ const category = typeof flags.get('category') === 'string' ? String(flags.get('category')) : undefined;
2642
+ return discoverRepositoryRoleMetadata(transport, scope, category);
2643
+ }
2644
+ if (action === 'ask')
2645
+ return askRepositoryRoleQuestion({ transport, scope,
2646
+ category: required(flags, 'category'), question: required(flags, 'question') });
2647
+ return readRepositoryRoleReply({ transport, scope, questionId: required(flags, 'question-id') });
2648
+ }
2502
2649
  async function workspaceSync(flags, positional) {
2503
2650
  const workspaceId = positional[0] || required(flags, 'workspace-id');
2504
2651
  const policyRevision = required(flags, 'policy-revision');
@@ -2662,23 +2809,13 @@ async function loadVerifiedWorkspacePolicy(path, workspaceId) {
2662
2809
  return verified;
2663
2810
  }
2664
2811
  export async function installRepositoryAgentFabricSkill(input) {
2812
+ if (input.sourceAuthorization !== undefined)
2813
+ input = { ...input,
2814
+ sourceAuthorization: validateRepositorySourceAuthorization(input.sourceAuthorization, input, new Date()) };
2815
+ await assertRepositoryInstallerOwnership(input.workspace, input.workspaceId);
2816
+ input = { ...input, workspace: await realpath(input.workspace) };
2817
+ await assertRepositoryInstallerOwnership(input.workspace, input.workspaceId);
2665
2818
  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
2819
  await mkdir(resolve(skillRoot, 'references'), { recursive: true, mode: 0o700 });
2683
2820
  await mkdir(resolve(input.workspace, '.dharma'), { recursive: true, mode: 0o700 });
2684
2821
  const skill = `---
@@ -2741,17 +2878,47 @@ The CLI enrolls this device through browser-confirmed Clerk organization consent
2741
2878
  controlBranch: input.controlBranch || null,
2742
2879
  workspaceId: input.workspaceId,
2743
2880
  };
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 });
2881
+ await writeRepositoryInstallerFile(input.workspace, '.agents/skills/dharma-agent-fabric/SKILL.md', skill);
2882
+ await writeRepositoryInstallerFile(input.workspace, '.agents/skills/dharma-agent-fabric/references/organization.md', reference);
2883
+ 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`);
2884
+ const knowledge = input.repositoryAgentId ? await initializeRepositoryKnowledge({
2885
+ workspace: input.workspace, organizationId: input.organizationId, repositoryAgentId: input.repositoryAgentId,
2886
+ }) : null;
2887
+ const repositoryPackage = await writeRepositoryPackageSnapshot({
2888
+ workspace: input.workspace, snapshot: await inventoryRepositoryPackage(input),
2889
+ });
2890
+ await writeRepositoryInstallerFile(input.workspace, '.dharma/agent-fabric.json', `${JSON.stringify(connection, null, 2)}\n`);
2891
+ await writeRepositoryInstallerFile(input.workspace, '.dharma/repository-agent.json', `${JSON.stringify(repositoryAgent, null, 2)}\n`);
2749
2892
  return {
2750
2893
  skillPath: '.agents/skills/dharma-agent-fabric/SKILL.md',
2751
2894
  connectionPath: '.dharma/agent-fabric.json',
2752
2895
  repositoryAgentPath: '.dharma/repository-agent.json',
2896
+ repositoryPackage,
2897
+ knowledge,
2753
2898
  };
2754
2899
  }
2900
+ async function repositorySnapshotCommand(flags, outputs) {
2901
+ if (flags.has('apply') && flags.has('dry-run'))
2902
+ throw new Error('Choose --apply or --dry-run, not both.');
2903
+ if (outputs.some(value => typeof value !== 'string'))
2904
+ throw new Error('--approved-output requires a workspace-relative file path.');
2905
+ const workspace = String(flags.get('workspace') || '.');
2906
+ const apply = flags.get('apply') === true;
2907
+ if (apply) {
2908
+ await access(resolve(workspace, '.agents/skills/dharma-agent-fabric/.dharma-agent-fabric.json')).catch(() => {
2909
+ throw new Error('Snapshot --apply requires an installed managed repository skill; use --dry-run before onboarding.');
2910
+ });
2911
+ }
2912
+ const snapshot = await inventoryRepositoryPackage({
2913
+ workspace, organizationId: required(flags, 'organization-id'), workspaceId: required(flags, 'workspace-id'),
2914
+ repositoryAgentId: (await registry()).find(record => record.path === resolve(workspace)
2915
+ && record.organizationId === flags.get('organization-id') && record.workspaceId === flags.get('workspace-id'))?.repositoryAgentId,
2916
+ approvedOutputs: outputs,
2917
+ });
2918
+ const persisted = apply ? await writeRepositoryPackageSnapshot({ workspace, snapshot }) : null;
2919
+ return { ok: true, dryRun: !apply, localMutation: apply, serverMutation: false,
2920
+ authority: 'local_inventory_not_signed', snapshot, persisted };
2921
+ }
2755
2922
  async function onboard(flags) {
2756
2923
  const workspace = await realpath(String(flags.get('workspace') || flags.get('path') || '.'));
2757
2924
  const organizationId = required(flags, 'organization-id');
@@ -2809,17 +2976,98 @@ async function onboard(flags) {
2809
2976
  const localPolicy = synced.localPolicy;
2810
2977
  const authoritativeRevision = String(localPolicy?.revision || policyRevision);
2811
2978
  const generatedPolicy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
2979
+ if (!registered.repositoryBindingId || !registered.repositoryAgentId) {
2980
+ throw new Error('Repository onboarding requires the canonical repository binding and logical agent.');
2981
+ }
2982
+ let sourceAuthorization;
2983
+ try {
2984
+ sourceAuthorization = await fetchRepositorySourceAuthorization(fabric, {
2985
+ organizationId, workspaceId: registered.workspaceId,
2986
+ repositoryBindingId: registered.repositoryBindingId, repositoryAgentId: registered.repositoryAgentId,
2987
+ });
2988
+ }
2989
+ catch {
2990
+ return { ok: false, stage: 'repository_source_authorization_required', organizationId,
2991
+ workspaceId: registered.workspaceId, code: 'repository_source_policy_unavailable',
2992
+ sharedRepositoryReady: false, localInventoryFallback: false };
2993
+ }
2812
2994
  const installed = await installRepositoryAgentFabricSkill({
2813
2995
  workspace,
2814
2996
  hqUrl,
2815
2997
  organizationId,
2816
2998
  workspaceId: registered.workspaceId,
2817
2999
  repositoryAgentId: registered.repositoryAgentId,
3000
+ repositoryBindingId: registered.repositoryBindingId,
3001
+ sourceAuthorization,
2818
3002
  repositoryAgentKey: registered.repositoryAgentKey,
2819
3003
  controlBranch: registered.controlBranch,
2820
3004
  policyRevision: authoritativeRevision,
2821
3005
  });
3006
+ const onboardingProvider = providerIds?.[0] || 'codex';
3007
+ const onboardingEvidenceFlags = new Map([
3008
+ ['workspace', workspace], ['provider', onboardingProvider],
3009
+ ['policy', resolve(workspace, '.dharma', 'approved-policy.json')], ['maximum-sessions', '20'],
3010
+ ]);
3011
+ const onboardingEvidencePreview = await evidencePreview(onboardingEvidenceFlags);
3012
+ let onboardingEvidence = { state: 'synchronized', captured: 0, synced: 0 };
3013
+ if (Number(onboardingEvidencePreview.trajectoryCount || 0) > 0
3014
+ && onboardingEvidencePreview.automaticDisclosure?.ready === true) {
3015
+ onboardingEvidenceFlags.set('sync', true);
3016
+ onboardingEvidence = await synchronizeBootstrapEvidence(async () => await retryBootstrapOnboarding(async () => await capture(onboardingEvidenceFlags, true)));
3017
+ requireCompletedBootstrapEvidence(onboardingEvidence, Number(onboardingEvidencePreview.trajectoryCount || 0));
3018
+ }
3019
+ const initialSnapshot = await inventoryRepositoryPackage({ workspace, organizationId,
3020
+ workspaceId: registered.workspaceId, repositoryAgentId: registered.repositoryAgentId,
3021
+ repositoryBindingId: registered.repositoryBindingId, sourceAuthorization });
3022
+ const candidateScope = { organizationId, workspaceId: registered.workspaceId,
3023
+ repositoryBindingId: registered.repositoryBindingId, repositoryAgentId: registered.repositoryAgentId };
3024
+ const outboxRoot = resolve(dharmaHome(), 'relay', 'repository-candidates');
3025
+ const canonicalPackage = registered.repositoryPackage;
3026
+ const candidate = canonicalPackage.state === 'absent'
3027
+ ? await synchronizeRepositoryCandidate({ transport: fabric, outboxRoot, scope: candidateScope,
3028
+ snapshot: initialSnapshot, initialRepository: true })
3029
+ : await adoptRepositoryCandidate({ outboxRoot, scope: candidateScope,
3030
+ sourceManifestHash: canonicalPackage.sourceManifestHash,
3031
+ consolidationMode: canonicalPackage.consolidationMode, candidate: {
3032
+ candidateId: canonicalPackage.candidateId, operationId: canonicalPackage.operationId,
3033
+ snapshotHash: canonicalPackage.snapshotHash, state: canonicalPackage.state,
3034
+ releaseId: canonicalPackage.releaseId,
3035
+ } });
3036
+ registered = { ...registered, repositoryPackage: {
3037
+ state: candidate.state, candidateId: candidate.candidateId, operationId: candidate.operationId,
3038
+ snapshotHash: candidate.snapshotHash,
3039
+ sourceManifestHash: canonicalPackage.state === 'absent'
3040
+ ? sha256(canonicalize(initialSnapshot.manifest)) : canonicalPackage.sourceManifestHash,
3041
+ releaseId: candidate.releaseId,
3042
+ generation: candidate.state === 'published' ? Math.max(1, canonicalPackage.generation) : canonicalPackage.generation,
3043
+ consolidationMode: canonicalPackage.state === 'absent' ? 'initial_repository' : canonicalPackage.consolidationMode,
3044
+ } };
3045
+ await saveWorkspaceRecord(registered);
2822
3046
  const providers = await receiptAwareProviderCapabilities(await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability())));
3047
+ const roleRequested = ['role-name', 'question-categories', 'role-description'].some(key => flags.has(key));
3048
+ if (roleRequested && !['role-name', 'question-categories', 'role-description'].every(key => flags.has(key))) {
3049
+ throw new Error('Repository role overrides require --role-name, --question-categories, and --role-description together.');
3050
+ }
3051
+ const derivedRole = deriveRepositoryRole({ snapshot: initialSnapshot,
3052
+ providers: providers.map(provider => provider.provider).filter(isLocalProviderId) });
3053
+ const roleInput = roleRequested ? {
3054
+ roleName: required(flags, 'role-name'),
3055
+ questionCategories: required(flags, 'question-categories').split(',').map(value => value.trim()).filter(Boolean),
3056
+ description: required(flags, 'role-description'),
3057
+ } : derivedRole;
3058
+ const profileHash = sha256(canonicalize(roleInput));
3059
+ let role = registered.repositoryRole?.profileHash === profileHash
3060
+ ? { stage: 'repository_role_observed', reused: true, role: { ...roleInput, revision: registered.repositoryRole.revision } }
3061
+ : await registerRepositoryRoleMetadata(fabric, repositoryRoleScope(registered), {
3062
+ expectedRevision: registered.repositoryRole?.revision || Number(flags.get('role-revision') || 0), ...roleInput
3063
+ });
3064
+ if (registered.repositoryRole?.profileHash !== profileHash) {
3065
+ const revision = Number(role.role?.revision);
3066
+ if (!Number.isSafeInteger(revision) || revision < 1)
3067
+ throw new Error('Repository role registration returned no durable revision.');
3068
+ registered = { ...registered, repositoryRole: { revision, profileHash } };
3069
+ await saveWorkspaceRecord(registered);
3070
+ }
2823
3071
  const nativeSkillResult = await installAvailableNativeAgentFabricBootstraps({
2824
3072
  providers,
2825
3073
  workspace,
@@ -2828,9 +3076,17 @@ async function onboard(flags) {
2828
3076
  hqUrl,
2829
3077
  });
2830
3078
  const primaryProvider = providers[0]?.provider || 'codex';
3079
+ const relay = flags.has('no-relay-daemon')
3080
+ ? { started: false, state: 'not_started' }
3081
+ : await startRelayDaemon(resolve(workspace, '.dharma', 'approved-policy.json'));
3082
+ const sharedRepositoryReady = await repositorySharedReady(registered);
2831
3083
  return {
2832
3084
  ok: true,
2833
- stage: nativeSkillResult.failures.length ? 'ready_with_provider_actions' : 'ready',
3085
+ stage: sharedRepositoryReady ? 'ready' : 'shared_repository_pending',
3086
+ localStage: nativeSkillResult.failures.length ? 'ready_with_provider_actions' : 'ready',
3087
+ sharedRepositoryReady,
3088
+ sharedRepository: { authority: sharedRepositoryReady ? 'signed_repository_release' : 'local_inventory_not_signed',
3089
+ publication: candidate.state, activation: sharedRepositoryReady },
2834
3090
  organizationId,
2835
3091
  workspaceId: registered.workspaceId,
2836
3092
  repositoryAgent: {
@@ -2849,6 +3105,14 @@ async function onboard(flags) {
2849
3105
  writePaths: generatedPolicy.tasks.writePaths,
2850
3106
  },
2851
3107
  repositorySkill: installed,
3108
+ repositoryCandidate: candidate,
3109
+ firstLearningEvidence: {
3110
+ discovered: Number(onboardingEvidencePreview.trajectoryCount || 0),
3111
+ disclosureReady: onboardingEvidencePreview.automaticDisclosure?.ready === true,
3112
+ ...onboardingEvidence,
3113
+ },
3114
+ repositoryRole: role,
3115
+ relay,
2852
3116
  nativeSkills: nativeSkillResult.installed,
2853
3117
  nativeSkillFailures: nativeSkillResult.failures,
2854
3118
  workspaceSync: synced,
@@ -4042,7 +4306,8 @@ function containedInlinePath(root, value) {
4042
4306
  }
4043
4307
  return candidate;
4044
4308
  }
4045
- export async function materializeInlineSkillFiles(bundle, sourceRoot) {
4309
+ export async function materializeInlineSkillFiles(bundle, sourceRoot, assertCurrent = () => { }) {
4310
+ assertCurrent();
4046
4311
  if (bundle.operation === 'clear')
4047
4312
  return false;
4048
4313
  const inline = bundle.skills.map((skill) => skill.files);
@@ -4073,8 +4338,11 @@ export async function materializeInlineSkillFiles(bundle, sourceRoot) {
4073
4338
  if (digest !== file.sha256)
4074
4339
  throw new Error(`Inline skill file hash mismatch: ${file.path}`);
4075
4340
  const destination = containedInlinePath(skillRoot, file.path);
4341
+ assertCurrent();
4076
4342
  await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
4343
+ assertCurrent();
4077
4344
  await writeFile(destination, content, { mode: 0o600 });
4345
+ assertCurrent();
4078
4346
  }
4079
4347
  }
4080
4348
  return true;
@@ -4110,22 +4378,68 @@ export async function recoverLegacySkillBundleIdAfterAuthorizationFailure(input)
4110
4378
  export async function installedBundleIdForSkillPollAfterAuthorizationFailure(input) {
4111
4379
  return recoverLegacySkillBundleIdAfterAuthorizationFailure(input);
4112
4380
  }
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;
4381
+ export async function loadSkillSynchronizationPolicy(policyPath, workspaceId, secureStore) {
4382
+ const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
4383
+ if (!workspace)
4384
+ throw new Error('Skill workspace is not registered locally.');
4385
+ const [providedPath, canonicalPath] = await Promise.all([
4386
+ canonicalFilesystemPath(resolve(policyPath)),
4387
+ canonicalFilesystemPath(resolve(workspace.path, '.dharma', 'approved-policy.json')),
4388
+ ]);
4389
+ if (providedPath !== canonicalPath) {
4390
+ throw new Error('Skill synchronization requires the canonical registered workspace policy path.');
4391
+ }
4392
+ const policy = await loadOrganizationPolicy(policyPath);
4393
+ const config = await readDeviceConfig();
4394
+ if (!config || config.organizationId !== workspace.organizationId || policy.organizationId !== workspace.organizationId
4395
+ || workspace.status !== 'active') {
4396
+ throw new Error('Skill policy does not match the enrolled organization and workspace.');
4397
+ }
4398
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: config.serverPublicKeyEd25519,
4399
+ organizationId: config.organizationId, workspaceId });
4400
+ await assertWorkspaceAuthorizationCurrent(workspaceId, policy.serverAuthorization);
4401
+ const enrollment = await loadDeviceEnrollmentAnchor({ config, store: secureStore });
4402
+ await assertWorkspaceAuthorizationCurrent(workspaceId, policy.serverAuthorization);
4403
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: enrollment.serverPublicKeyEd25519,
4404
+ organizationId: config.organizationId, workspaceId });
4405
+ return { workspace, policy, config, enrollment };
4406
+ }
4407
+ export function parseSkillRolloutResponse(response, organizationId) {
4408
+ if (!response || typeof response !== 'object' || Array.isArray(response))
4409
+ throw new Error('Skill rollout response is invalid.');
4410
+ const value = response;
4411
+ if (value.ok !== true || value.organizationId !== organizationId || !Object.hasOwn(value, 'rollout')) {
4412
+ throw new Error('Skill rollout response does not match the authenticated organization contract.');
4413
+ }
4414
+ const rollout = value.rollout;
4415
+ if (rollout === null)
4416
+ return null;
4417
+ if (!rollout || typeof rollout !== 'object' || Array.isArray(rollout))
4418
+ throw new Error('Skill rollout response is invalid.');
4419
+ const candidate = rollout;
4420
+ if (typeof candidate.id !== 'string' || !candidate.id.trim() || !candidate.bundle
4421
+ || typeof candidate.bundle !== 'object' || Array.isArray(candidate.bundle))
4422
+ throw new Error('Skill rollout response is invalid.');
4423
+ return rollout;
4424
+ }
4425
+ export async function prepareSkillUpdate(input) {
4426
+ const { workspaceId, provider, policyPath } = input;
4427
+ const assertRunning = input.assertRunning || (() => { });
4428
+ assertRunning();
4429
+ const context = await loadSkillSynchronizationPolicy(policyPath, workspaceId, input.store);
4430
+ const { workspace, policy, config } = context;
4431
+ assertRunning();
4432
+ const destination = nativeSkillDirectory(provider);
4433
+ const fabric = input.fabric || await client();
4434
+ assertRunning();
4435
+ let activeBundleId;
4436
+ let legacyBaselineMigrationRequested = false;
4437
+ if (input.automatic) {
4438
+ activeBundleId = (await loadActiveSkillAuthorizationAnchor({ config, workspaceId,
4439
+ organizationAgentId: String(workspace.repositoryAgentId || ''), provider, store: input.store, fresh: true }))?.bundleId ?? null;
4440
+ assertRunning();
4441
+ }
4442
+ else {
4129
4443
  try {
4130
4444
  activeBundleId = (await activeSkillAuthorization(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
4131
4445
  }
@@ -4142,152 +4456,330 @@ async function skillSync(flags) {
4142
4456
  legacyBaselineMigrationRequested = true;
4143
4457
  }
4144
4458
  }
4145
- const response = await fabric.pollSkill({
4146
- workspaceId,
4147
- provider,
4148
- installedBundleId: activeBundleId,
4149
- legacyBaselineMigrationRequested,
4459
+ }
4460
+ const pollRequest = {
4461
+ workspaceId,
4462
+ provider,
4463
+ installedBundleId: activeBundleId,
4464
+ legacyBaselineMigrationRequested,
4465
+ repositoryPackageProtocol: 'dharma.repository-package-envelope/v1',
4466
+ };
4467
+ assertRunning();
4468
+ const response = await fabric.pollSkill(pollRequest);
4469
+ assertRunning();
4470
+ const rollout = parseSkillRolloutResponse(response, config.organizationId);
4471
+ if (!rollout)
4472
+ return null;
4473
+ let bundle = rollout.bundle;
4474
+ if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
4475
+ || (bundle.operation === 'install' && bundle.skills.length === 0)
4476
+ || (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
4477
+ throw new Error('Skill bundle does not match local organization policy.');
4478
+ }
4479
+ const sources = validateSkillBundleSources(bundle);
4480
+ const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
4481
+ const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
4482
+ const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
4483
+ verifySkillBundle(bundle, serverPublicKey);
4484
+ for (const [selectors, value] of [[bundle.targetSelectors.organizationAgentIds, String(workspace.repositoryAgentId || '')],
4485
+ [bundle.targetSelectors.deviceIds, config.deviceId], [bundle.targetSelectors.workspaceIds, workspaceId],
4486
+ [bundle.targetSelectors.providers, provider]]) {
4487
+ if (selectors.length && !selectors.includes(value))
4488
+ throw new Error('Skill bundle does not target this enrolled endpoint.');
4489
+ }
4490
+ let repositoryDelivery;
4491
+ if (rollout.repositoryPackage !== undefined) {
4492
+ if (workspace.organizationId !== config.organizationId || policy.organizationId !== config.organizationId
4493
+ || !workspace.repositoryBindingId || !workspace.repositoryAgentId) {
4494
+ throw new Error('Repository delivery requires the enrolled organization and complete repository binding.');
4495
+ }
4496
+ const query = new URLSearchParams({ rolloutId: rollout.id, workspaceId, provider });
4497
+ const packageRoute = `/agent-fabric/skills/${encodeURIComponent(bundle.bundleId)}/repository-package`;
4498
+ const readPackagePart = async (route, field) => {
4499
+ assertRunning();
4500
+ const part = await fabric.signedGet(route);
4501
+ assertRunning();
4502
+ if (part.ok !== true || part.organizationId !== config.organizationId || !Object.hasOwn(part, field)) {
4503
+ throw new Error('Repository delivery response does not match the authenticated route contract.');
4504
+ }
4505
+ return part[field];
4506
+ };
4507
+ repositoryDelivery = await receiveRepositoryPackageDelivery({
4508
+ envelope: rollout.repositoryPackage, bundle, serverPublicKey,
4509
+ scope: { organizationId: config.organizationId, repositoryBindingId: workspace.repositoryBindingId,
4510
+ repositoryAgentId: workspace.repositoryAgentId, deviceId: config.deviceId, workspaceId, provider },
4511
+ fetchIndex: () => readPackagePart(`${packageRoute}/index?${query}`, 'index'),
4512
+ fetchChunk: (fileIndex, chunkIndex) => readPackagePart(`${packageRoute}/chunks?${query}&fileIndex=${fileIndex}&chunkIndex=${chunkIndex}`, 'chunk'),
4150
4513
  });
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.');
4514
+ assertRunning();
4515
+ bundle = repositoryDelivery.bundle;
4516
+ }
4517
+ const assertCurrent = () => {
4518
+ assertRunning();
4519
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: config.serverPublicKeyEd25519,
4520
+ organizationId: config.organizationId, workspaceId });
4521
+ verifySkillBundle(bundle, serverPublicKey);
4522
+ repositoryDelivery?.assertCurrent();
4523
+ };
4524
+ assertCurrent();
4525
+ const sourceParent = input.automatic ? skillPreparationScopeRoot(dharmaHome(), workspaceId, provider)
4526
+ : resolve(dharmaHome(), 'relay', 'skill-sources');
4527
+ await mkdir(sourceParent, { recursive: true, mode: 0o700 });
4528
+ assertCurrent();
4529
+ const sourceRoot = await mkdtemp(resolve(sourceParent, 'attempt-'));
4530
+ try {
4531
+ assertCurrent();
4532
+ if (repositoryDelivery) {
4533
+ for (const file of repositoryDelivery.files) {
4534
+ assertCurrent();
4535
+ const target = containedInlinePath(sourceRoot, file.path);
4536
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
4537
+ assertCurrent();
4538
+ await writeFile(target, Buffer.from(file.contentBase64, 'base64'), { mode: 0o600, flag: 'wx' });
4539
+ assertCurrent();
4540
+ }
4161
4541
  }
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) {
4542
+ const materializedInline = repositoryDelivery ? false : await materializeInlineSkillFiles(bundle, sourceRoot, assertCurrent);
4543
+ assertCurrent();
4544
+ if (bundle.operation === 'install' && !materializedInline && !repositoryDelivery) {
4172
4545
  if (!sources.singleSource)
4173
4546
  throw new Error('Multi-source skill bundles require signed inline files for every skill.');
4547
+ if (input.automatic)
4548
+ throw new Error('Automatic preparation requires bounded inline files or an authenticated complete repository package.');
4174
4549
  await rm(sourceRoot, { recursive: true, force: true });
4550
+ assertCurrent();
4175
4551
  await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
4552
+ assertCurrent();
4176
4553
  await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
4554
+ assertCurrent();
4177
4555
  await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
4556
+ assertCurrent();
4178
4557
  }
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
- };
4558
+ for (const skill of bundle.skills) {
4559
+ const actual = await contentHash(containedInlinePath(sourceRoot, skill.path));
4560
+ assertCurrent();
4561
+ if (actual !== skill.contentHash)
4562
+ throw new Error('Prepared skill tree does not match the signed content hash.');
4563
+ }
4564
+ const current = await loadSkillSynchronizationPolicy(policyPath, workspaceId, input.store);
4565
+ assertCurrent();
4566
+ if (canonicalize(current) !== canonicalize(context))
4567
+ throw new Error('Skill preparation scope or policy changed; retry with current authorization.');
4568
+ return { workspace, policy, config, destination, fabric, rollout, bundle, sourceRoot, repositoryDelivery, assertCurrent };
4569
+ }
4570
+ catch (error) {
4571
+ await rm(sourceRoot, { recursive: true, force: true });
4572
+ throw error;
4573
+ }
4574
+ }
4575
+ async function activatePreparedSkillUpdate(input) {
4576
+ const { prepared, workspaceId, provider, policyPath, flags } = input;
4577
+ const { workspace, policy, config, destination, fabric, rollout, bundle, sourceRoot, repositoryDelivery } = prepared;
4578
+ try {
4579
+ const previousAnchor = await loadActiveSkillAuthorizationAnchor({
4580
+ config,
4581
+ workspaceId,
4582
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
4583
+ provider,
4584
+ });
4585
+ const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
4586
+ repositoryDelivery?.assertCurrent();
4587
+ const current = await loadSkillSynchronizationPolicy(policyPath, workspaceId);
4588
+ if (canonicalize(current.policy) !== canonicalize(policy)
4589
+ || canonicalize(current.workspace) !== canonicalize(workspace)
4590
+ || canonicalize(current.config) !== canonicalize(config)) {
4591
+ throw new Error('Skill synchronization scope or policy changed before activation; retry with current authorization.');
4592
+ }
4593
+ repositoryDelivery?.assertCurrent();
4594
+ const receipt = await installSkillBundle({
4595
+ bundle,
4596
+ sourceDirectory: sourceRoot,
4597
+ nativeSkillDirectory: destination,
4598
+ policy,
4599
+ serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
4600
+ devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
4601
+ deviceId: config.deviceId,
4602
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
4603
+ workspaceId,
4604
+ provider,
4605
+ smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
4606
+ organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
4607
+ ...(provider === 'agy' ? {
4608
+ providerActivationCheck: () => attestAgySkillBundleActivation({
4609
+ bundle,
4610
+ nativeSkillDirectory: destination,
4611
+ workspace: workspace.path,
4612
+ }),
4613
+ } : provider === 'hermes' ? {
4614
+ providerActivationCheck: () => attestHermesSkillBundleActivation({
4615
+ bundle,
4616
+ workspace: workspace.path,
4617
+ nativeSkillDirectory: destination,
4618
+ }),
4619
+ } : {}),
4620
+ });
4621
+ const restoreAnchor = async () => {
4622
+ if (previousAnchor) {
4623
+ await saveActiveSkillAuthorizationAnchor({
4624
+ config,
4625
+ workspaceId,
4626
+ organizationAgentId: previousAnchor.organizationAgentId,
4627
+ provider,
4628
+ bundleId: previousAnchor.bundleId,
4629
+ receiptHash: previousAnchor.receiptHash,
4630
+ activatedAt: previousAnchor.activatedAt,
4631
+ expiresAt: previousAnchor.expiresAt,
4632
+ });
4633
+ }
4634
+ else {
4635
+ await deleteActiveSkillAuthorizationAnchor({ config, workspaceId, provider });
4636
+ }
4637
+ };
4638
+ const recoverLocalInstallation = async (error) => {
4639
+ const recoveryErrors = [];
4254
4640
  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
- }
4641
+ await rollbackUnconfirmedSkillBundle({
4642
+ nativeSkillDirectory: destination,
4643
+ workspaceId,
4644
+ receipt,
4645
+ });
4267
4646
  }
4268
- catch (error) {
4269
- await recoverLocalInstallation(error);
4647
+ catch (rollbackError) {
4648
+ recoveryErrors.push(rollbackError);
4270
4649
  }
4271
4650
  try {
4272
- await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
4651
+ await restoreAnchor();
4273
4652
  }
4274
- catch (error) {
4275
- if (isDefinitiveAgentFabricRejection(error))
4276
- await recoverLocalInstallation(error);
4277
- throw error;
4653
+ catch (anchorError) {
4654
+ recoveryErrors.push(anchorError);
4655
+ }
4656
+ if (recoveryErrors.length) {
4657
+ throw new AggregateError([error, ...recoveryErrors], 'Skill installation failed and local recovery was incomplete.');
4658
+ }
4659
+ throw error;
4660
+ };
4661
+ try {
4662
+ if (receipt.status === 'active') {
4663
+ await saveActiveSkillAuthorizationAnchor({
4664
+ config,
4665
+ workspaceId,
4666
+ organizationAgentId: String(workspace.repositoryAgentId || ''),
4667
+ provider,
4668
+ bundleId: receipt.bundleId,
4669
+ receiptHash: receipt.receiptHash,
4670
+ activatedAt: receipt.completedAt,
4671
+ expiresAt: bundle.expiresAt ?? null,
4672
+ });
4278
4673
  }
4279
- return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
4280
4674
  }
4281
- finally {
4282
- await rm(sourceRoot, { recursive: true, force: true });
4675
+ catch (error) {
4676
+ await recoverLocalInstallation(error);
4677
+ }
4678
+ try {
4679
+ await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
4680
+ }
4681
+ catch (error) {
4682
+ if (isDefinitiveAgentFabricRejection(error))
4683
+ await recoverLocalInstallation(error);
4684
+ throw error;
4283
4685
  }
4686
+ return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
4687
+ }
4688
+ finally {
4689
+ await rm(sourceRoot, { recursive: true, force: true });
4690
+ }
4691
+ }
4692
+ async function skillSync(flags) {
4693
+ const workspaceId = required(flags, 'workspace-id');
4694
+ const providerValue = required(flags, 'provider');
4695
+ if (!isLocalProviderId(providerValue))
4696
+ throw new Error('Skill provider must be codex, claude, agy, or hermes.');
4697
+ const provider = providerValue;
4698
+ return withWorkspaceSkillActivationLock(workspaceId, provider, async () => {
4699
+ const policyPath = required(flags, 'policy');
4700
+ const prepared = await prepareSkillUpdate({ workspaceId, provider, policyPath });
4701
+ if (!prepared)
4702
+ return { ok: true, rollout: null, changed: false };
4703
+ return activatePreparedSkillUpdate({ prepared, workspaceId, provider, policyPath, flags });
4284
4704
  });
4285
4705
  }
4706
+ async function installedRepositoryKnowledge(workspace) {
4707
+ const config = await readDeviceConfig();
4708
+ if (!config || config.organizationId !== workspace.organizationId || !workspace.repositoryBindingId || !workspace.repositoryAgentId) {
4709
+ throw new Error('Installed repository knowledge requires current enrolled workspace scope.');
4710
+ }
4711
+ const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
4712
+ const enrollment = await loadDeviceEnrollmentAnchor({ config });
4713
+ if (identity.publicKeyEd25519 !== enrollment.devicePublicKeyEd25519)
4714
+ throw new Error('Installed knowledge device identity mismatch.');
4715
+ const organizationAgentId = workspace.repositoryAgentId;
4716
+ return selectInstalledRepositoryKnowledge({ organizationId: config.organizationId,
4717
+ repositoryBindingId: workspace.repositoryBindingId, repositoryAgentId: organizationAgentId,
4718
+ loadProvider: async (provider) => {
4719
+ const root = nativeSkillDirectory(provider);
4720
+ if (!await pathExistsOrThrow(resolve(root, '.dharma-managed/workspaces', workspace.workspaceId, 'ACTIVE_BUNDLE')))
4721
+ return null;
4722
+ const anchorInput = { config, workspaceId: workspace.workspaceId, organizationAgentId, provider };
4723
+ const active = await loadActiveSkillAuthorizationAnchor(anchorInput);
4724
+ if (!active)
4725
+ throw new Error('Installed knowledge release lacks a protected authorization anchor.');
4726
+ const read = (expectedReceiptHash) => readVerifiedRepositoryKnowledge({
4727
+ nativeSkillDirectory: root, workspaceId: workspace.workspaceId, provider,
4728
+ organizationId: config.organizationId, organizationAgentId, deviceId: config.deviceId,
4729
+ serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: enrollment.serverPublicKeyEd25519 }, format: 'jwk' }),
4730
+ devicePublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: identity.publicKeyEd25519 }, format: 'jwk' }), expectedReceiptHash,
4731
+ });
4732
+ try {
4733
+ return await read(active.receiptHash);
4734
+ }
4735
+ catch (error) {
4736
+ if (!(error instanceof Error) || error.message !== 'Active bundle receipt is not the current protected authorization.')
4737
+ throw error;
4738
+ const refreshed = await loadActiveSkillAuthorizationAnchor({ ...anchorInput, fresh: true });
4739
+ if (!refreshed)
4740
+ throw error;
4741
+ return read(refreshed.receiptHash);
4742
+ }
4743
+ } });
4744
+ }
4745
+ async function takeCachedSkillUpdate(input) {
4746
+ const context = await loadSkillSynchronizationPolicy(input.policyPath, input.workspaceId);
4747
+ const { workspace, policy, config } = context;
4748
+ if (!workspace.repositoryAgentId)
4749
+ return null;
4750
+ const cached = await takeSkillPreparationCache({ home: dharmaHome(), workspaceId: input.workspaceId,
4751
+ provider: input.provider, organizationId: config.organizationId, deviceId: config.deviceId,
4752
+ repositoryAgentId: workspace.repositoryAgentId, repositoryBindingId: workspace.repositoryBindingId ?? null,
4753
+ policyHash: sha256(canonicalize(policy)), assertCurrent: () => {
4754
+ verifyServerAuthorizedPolicy({ policy, publicKeyEd25519: config.serverPublicKeyEd25519,
4755
+ organizationId: config.organizationId, workspaceId: input.workspaceId });
4756
+ } });
4757
+ if (!cached)
4758
+ return null;
4759
+ try {
4760
+ const bundle = cached.record.bundle;
4761
+ const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
4762
+ verifySkillBundle(bundle, serverPublicKey);
4763
+ for (const skill of bundle.skills) {
4764
+ if (await contentHash(containedInlinePath(cached.sourceRoot, skill.path)) !== skill.contentHash) {
4765
+ throw new Error('Cached skill tree does not match the signed content hash.');
4766
+ }
4767
+ }
4768
+ return { workspace, policy, config, destination: nativeSkillDirectory(input.provider), fabric: input.fabric,
4769
+ rollout: { id: String(cached.record.rolloutId), bundle }, bundle, sourceRoot: cached.sourceRoot,
4770
+ repositoryDelivery: undefined, assertCurrent: () => verifySkillBundle(bundle, serverPublicKey) };
4771
+ }
4772
+ catch (error) {
4773
+ await rm(cached.sourceRoot, { recursive: true, force: true });
4774
+ throw error;
4775
+ }
4776
+ }
4286
4777
  async function relayStart(flags) {
4287
4778
  const policyPath = resolve(required(flags, 'policy'));
4288
- const canonicalWorkspace = (await registry()).find((item) => resolve(item.path, '.dharma', 'approved-policy.json') === policyPath);
4289
- if (!canonicalWorkspace)
4779
+ const selectedWorkspace = (await registry()).find((item) => resolve(item.path, '.dharma', 'approved-policy.json') === policyPath);
4780
+ if (!selectedWorkspace)
4290
4781
  throw new Error('Relay policy must be the canonical policy of one registered workspace.');
4782
+ let canonicalWorkspace = selectedWorkspace;
4291
4783
  let policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
4292
4784
  const fabric = await client();
4293
4785
  const leaseSeconds = Number(flags.get('lease-seconds') || 120);
@@ -4296,7 +4788,8 @@ async function relayStart(flags) {
4296
4788
  await mkdir(resolve(dharmaHome(), 'relay'), { recursive: true, mode: 0o700 });
4297
4789
  await writeFile(pidPath, `${process.pid}\n`, { mode: 0o600 });
4298
4790
  let stopping = false;
4299
- const stop = () => { stopping = true; };
4791
+ let stopPreparation;
4792
+ const stop = () => { stopping = true; stopPreparation?.(); };
4300
4793
  process.once('SIGINT', stop);
4301
4794
  process.once('SIGTERM', stop);
4302
4795
  let tasksCompleted = 0;
@@ -4305,12 +4798,80 @@ async function relayStart(flags) {
4305
4798
  let trajectorySyncsCompleted = 0;
4306
4799
  let nextPolicyRefreshAt = 0;
4307
4800
  let evidencePolicyFresh = false;
4801
+ const repositorySourceWatcher = new RepositorySourceWatcher();
4802
+ let nextRepositorySourceScanAt = 0;
4803
+ let repositorySourceCandidates = 0;
4804
+ let repositorySourceFailures = 0;
4805
+ let repositorySourceState = 'not_scanned';
4308
4806
  const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
4309
4807
  const vault = await LocalVault.open({
4310
4808
  root: resolve(dharmaHome(), 'vault'),
4311
4809
  masterKey: await loadOrCreateVaultMasterKey(),
4312
4810
  rawLocalDays: rawLocalRetentionDays(policy),
4313
4811
  });
4812
+ let skillPreparationsCompleted = 0;
4813
+ let skillPreparationFailures = 0;
4814
+ let skillActivationsCompleted = 0;
4815
+ let skillActivationFailures = 0;
4816
+ let nextSkillActivationAt = 0;
4817
+ const skillPreparationPump = startSkillPreparationPump({
4818
+ prepare: async (assertRunning) => {
4819
+ await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
4820
+ assertRunning();
4821
+ const capability = await adapter.capability();
4822
+ assertRunning();
4823
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
4824
+ return;
4825
+ const provider = adapter.providerId;
4826
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
4827
+ provider, assertCurrent: assertRunning }, async () => {
4828
+ const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
4829
+ provider, policyPath, fabric, automatic: true, assertRunning });
4830
+ if (!prepared) {
4831
+ assertRunning();
4832
+ return;
4833
+ }
4834
+ let retained = false;
4835
+ try {
4836
+ prepared.assertCurrent();
4837
+ const repositoryAgentId = prepared.workspace.repositoryAgentId;
4838
+ if (!repositoryAgentId)
4839
+ throw new Error('Preparation cache requires an enrolled repository agent.');
4840
+ const pendingRecord = await serializeSkillPreparationRecord({
4841
+ schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
4842
+ deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
4843
+ repositoryAgentId,
4844
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
4845
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
4846
+ bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
4847
+ ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
4848
+ preparedAt: new Date().toISOString(), activationAuthorized: false,
4849
+ });
4850
+ prepared.assertCurrent();
4851
+ await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
4852
+ prepared.assertCurrent();
4853
+ await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
4854
+ provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
4855
+ expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
4856
+ repositoryAgentId,
4857
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
4858
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
4859
+ bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
4860
+ onCommitted: () => { retained = true; } });
4861
+ skillPreparationsCompleted += 1;
4862
+ }
4863
+ finally {
4864
+ if (!retained)
4865
+ await rm(prepared.sourceRoot, { recursive: true, force: true });
4866
+ }
4867
+ });
4868
+ }, () => { skillPreparationFailures += 1; });
4869
+ },
4870
+ onError: () => { skillPreparationFailures += 1; },
4871
+ });
4872
+ stopPreparation = skillPreparationPump.requestStop;
4873
+ if (stopping)
4874
+ stopPreparation();
4314
4875
  try {
4315
4876
  const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
4316
4877
  if (recoveredTaskTrajectories.state === 'completed') {
@@ -4328,6 +4889,54 @@ async function relayStart(flags) {
4328
4889
  }
4329
4890
  nextPolicyRefreshAt = Date.now() + 60_000;
4330
4891
  }
4892
+ if (canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
4893
+ try {
4894
+ const current = await pollRepositoryCandidate({ transport: fabric,
4895
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
4896
+ scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
4897
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
4898
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
4899
+ if (current && canonicalWorkspace.repositoryPackage) {
4900
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
4901
+ state: current.state, candidateId: current.candidateId, operationId: current.operationId,
4902
+ snapshotHash: current.snapshotHash, releaseId: current.releaseId,
4903
+ generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
4904
+ await saveWorkspaceRecord(canonicalWorkspace);
4905
+ }
4906
+ }
4907
+ catch {
4908
+ repositorySourceFailures += 1;
4909
+ }
4910
+ }
4911
+ if (performance.now() >= nextRepositorySourceScanAt) {
4912
+ try {
4913
+ if (!canonicalWorkspace.repositoryBindingId || !canonicalWorkspace.repositoryAgentId) {
4914
+ throw new Error('Repository source synchronization requires a complete repository binding.');
4915
+ }
4916
+ const sourceCycle = await scanRepositorySourceChanges({
4917
+ workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
4918
+ workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
4919
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
4920
+ loadRetainedKnowledge: () => installedRepositoryKnowledge(canonicalWorkspace),
4921
+ submitCandidate: snapshot => synchronizeRepositoryCandidate({ transport: fabric,
4922
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
4923
+ scope: { organizationId: canonicalWorkspace.organizationId,
4924
+ workspaceId: canonicalWorkspace.workspaceId,
4925
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
4926
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId },
4927
+ snapshot, initialRepository: false }),
4928
+ });
4929
+ repositorySourceState = sourceCycle.state;
4930
+ if (sourceCycle.localMutation)
4931
+ repositorySourceCandidates += 1;
4932
+ }
4933
+ catch {
4934
+ repositorySourceWatcher.invalidate();
4935
+ repositorySourceFailures += 1;
4936
+ repositorySourceState = 'blocked';
4937
+ }
4938
+ nextRepositorySourceScanAt = performance.now() + 60_000;
4939
+ }
4331
4940
  let evidenceRequestId;
4332
4941
  if (evidencePolicyFresh) {
4333
4942
  const evidenceCycle = await deferUnavailableRelayRetention(async () => {
@@ -4351,6 +4960,37 @@ async function relayStart(flags) {
4351
4960
  const result = await executeOneTask(fabric, leaseSeconds);
4352
4961
  if (result.taskId)
4353
4962
  tasksCompleted += 1;
4963
+ if (!result.taskId && performance.now() >= nextSkillActivationAt) {
4964
+ for (const adapter of providerAdapters) {
4965
+ try {
4966
+ const capability = await adapter.capability();
4967
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
4968
+ continue;
4969
+ const provider = adapter.providerId;
4970
+ await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
4971
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
4972
+ provider, assertCurrent: () => { if (stopping)
4973
+ throw new Error('Relay is stopping.'); } }, async () => {
4974
+ const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
4975
+ provider, policyPath, fabric });
4976
+ const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
4977
+ provider, policyPath, fabric, automatic: true });
4978
+ if (!prepared)
4979
+ return;
4980
+ if (!cached)
4981
+ skillPreparationsCompleted += 1;
4982
+ await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
4983
+ provider, policyPath, flags: new Map() });
4984
+ skillActivationsCompleted += 1;
4985
+ });
4986
+ });
4987
+ }
4988
+ catch {
4989
+ skillActivationFailures += 1;
4990
+ }
4991
+ }
4992
+ nextSkillActivationAt = performance.now() + 60_000;
4993
+ }
4354
4994
  if (flags.has('once'))
4355
4995
  break;
4356
4996
  if (!result.taskId && !evidenceRequestId)
@@ -4358,6 +4998,7 @@ async function relayStart(flags) {
4358
4998
  } while (!stopping);
4359
4999
  }
4360
5000
  finally {
5001
+ await skillPreparationPump.stop();
4361
5002
  vault.close();
4362
5003
  process.removeListener('SIGINT', stop);
4363
5004
  process.removeListener('SIGTERM', stop);
@@ -4366,6 +5007,9 @@ async function relayStart(flags) {
4366
5007
  return {
4367
5008
  ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
4368
5009
  evidenceResponsesCompleted, trajectorySyncsCompleted,
5010
+ repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
5011
+ skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
5012
+ sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
4369
5013
  };
4370
5014
  }
4371
5015
  export async function run(argv) {
@@ -4393,6 +5037,13 @@ export async function run(argv) {
4393
5037
  }
4394
5038
  if (command === 'repositories' && subcommand === 'list')
4395
5039
  return repositoriesList(flags);
5040
+ if (command === 'repositories' && subcommand === 'snapshot') {
5041
+ return repositorySnapshotCommand(flags, repeated.get('approved-output') || []);
5042
+ }
5043
+ if (command === 'repositories' && ['role-register', 'role-discover', 'ask', 'reply'].includes(String(subcommand))) {
5044
+ const action = subcommand === 'role-register' ? 'register' : subcommand === 'role-discover' ? 'discover' : subcommand;
5045
+ return repositoryRoleCommand(action, flags);
5046
+ }
4396
5047
  if (command === 'repositories' && subcommand === 'status') {
4397
5048
  const listed = await repositoriesList(flags);
4398
5049
  return {