@dharma-ai-labs/agent-fabric 0.2.118 → 0.2.120

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 (74) hide show
  1. package/AGENT_FABRIC_ONBOARDING.md +5 -0
  2. package/README.md +23 -0
  3. package/dist/demoEnrollment.d.ts.map +1 -1
  4. package/dist/demoEnrollment.js +81 -9
  5. package/dist/demoEnrollment.js.map +1 -1
  6. package/dist/demoEnrollment.test.js +185 -0
  7. package/dist/demoEnrollment.test.js.map +1 -1
  8. package/dist/demoPackage.d.ts +1 -1
  9. package/dist/demoTransportFetch.d.ts.map +1 -1
  10. package/dist/demoTransportFetch.js +3 -1
  11. package/dist/demoTransportFetch.js.map +1 -1
  12. package/dist/demoTransportFetch.test.js +62 -0
  13. package/dist/demoTransportFetch.test.js.map +1 -1
  14. package/dist/evidencePreviewDisclosureCaller.test.d.ts +2 -0
  15. package/dist/evidencePreviewDisclosureCaller.test.d.ts.map +1 -0
  16. package/dist/evidencePreviewDisclosureCaller.test.js +56 -0
  17. package/dist/evidencePreviewDisclosureCaller.test.js.map +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +683 -377
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.test.js +2 -2
  22. package/dist/index.test.js.map +1 -1
  23. package/dist/legacyInstallerRecovery.d.ts +30 -0
  24. package/dist/legacyInstallerRecovery.d.ts.map +1 -0
  25. package/dist/legacyInstallerRecovery.js +107 -0
  26. package/dist/legacyInstallerRecovery.js.map +1 -0
  27. package/dist/legacyInstallerRecovery.test.d.ts +2 -0
  28. package/dist/legacyInstallerRecovery.test.d.ts.map +1 -0
  29. package/dist/legacyInstallerRecovery.test.js +126 -0
  30. package/dist/legacyInstallerRecovery.test.js.map +1 -0
  31. package/dist/relayAutostart.d.ts +9 -1
  32. package/dist/relayAutostart.d.ts.map +1 -1
  33. package/dist/relayAutostart.js +39 -3
  34. package/dist/relayAutostart.js.map +1 -1
  35. package/dist/relayAutostart.test.js +37 -0
  36. package/dist/relayAutostart.test.js.map +1 -1
  37. package/dist/relayRuntimeUpgrade.d.ts +58 -0
  38. package/dist/relayRuntimeUpgrade.d.ts.map +1 -0
  39. package/dist/relayRuntimeUpgrade.js +193 -0
  40. package/dist/relayRuntimeUpgrade.js.map +1 -0
  41. package/dist/relayRuntimeUpgrade.test.d.ts +2 -0
  42. package/dist/relayRuntimeUpgrade.test.d.ts.map +1 -0
  43. package/dist/relayRuntimeUpgrade.test.js +173 -0
  44. package/dist/relayRuntimeUpgrade.test.js.map +1 -0
  45. package/dist/relayRuntimeUpgradeCaller.test.d.ts +2 -0
  46. package/dist/relayRuntimeUpgradeCaller.test.d.ts.map +1 -0
  47. package/dist/relayRuntimeUpgradeCaller.test.js +116 -0
  48. package/dist/relayRuntimeUpgradeCaller.test.js.map +1 -0
  49. package/dist/repositoryInstallerFiles.d.ts +2 -1
  50. package/dist/repositoryInstallerFiles.d.ts.map +1 -1
  51. package/dist/repositoryInstallerFiles.js +6 -3
  52. package/dist/repositoryInstallerFiles.js.map +1 -1
  53. package/dist/repositoryOnboardingCaller.test.js +94 -1
  54. package/dist/repositoryOnboardingCaller.test.js.map +1 -1
  55. package/dist/repositoryOnboardingFlow.test.js +4 -3
  56. package/dist/repositoryOnboardingFlow.test.js.map +1 -1
  57. package/dist/repositoryRelaySupervisor.d.ts +39 -0
  58. package/dist/repositoryRelaySupervisor.d.ts.map +1 -0
  59. package/dist/repositoryRelaySupervisor.js +145 -0
  60. package/dist/repositoryRelaySupervisor.js.map +1 -0
  61. package/dist/repositoryRelaySupervisor.test.d.ts +2 -0
  62. package/dist/repositoryRelaySupervisor.test.d.ts.map +1 -0
  63. package/dist/repositoryRelaySupervisor.test.js +189 -0
  64. package/dist/repositoryRelaySupervisor.test.js.map +1 -0
  65. package/dist/schemas/local-relay-upgrade.schema.json +32 -0
  66. package/dist/skillPreparationCache.test.js +8 -4
  67. package/dist/skillPreparationCache.test.js.map +1 -1
  68. package/dist/skillPreparationCaller.test.js +4 -2
  69. package/dist/skillPreparationCaller.test.js.map +1 -1
  70. package/dist/usage.d.ts +1 -1
  71. package/dist/usage.d.ts.map +1 -1
  72. package/dist/usage.js +1 -1
  73. package/dist/usage.js.map +1 -1
  74. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -17,13 +17,16 @@ import { contentHash, getActiveSkillBundleAuthorization, getExpiredSkillBundleAu
17
17
  import { executeTask, FileActionExecutionJournal, FileTaskReceiptStore, providerInstructionsForTask, } from '@dharma-ai-labs/agent-fabric-task-runner';
18
18
  import { CLI_USAGE } from './usage.js';
19
19
  import { superviseRelay } from './relaySupervisor.js';
20
- import { disableRelayAutostart, enableRelayAutostart, relayAutostartStatus, startRelayAutostart } from './relayAutostart.js';
20
+ import { repositoryRelayObservationReady, runRegisteredRepositoryRelays, selectRepositoryRelayRegistrations, serializeRelayWork, waitForRelayRefresh } from './repositoryRelaySupervisor.js';
21
+ import { disableRelayAutostart, enableRelayAutostart, inspectOwnedRelayAutostart, relayAutostartStatus, startRelayAutostart, stopRelayAutostart } from './relayAutostart.js';
22
+ import { relayRuntimeObservationReady, upgradeRelayRuntime } from './relayRuntimeUpgrade.js';
21
23
  import { assertStudyExactPoll, studyTaskIdFromFlags } from './studyExactTaskSelector.js';
22
24
  import { initializeRepositoryKnowledge, readRepositoryKnowledgeSource } from './repositoryKnowledge.js';
23
25
  import { inventoryRepositoryPackage, readRepositoryPackageSnapshot, readRepositorySourceBaselineSnapshot, writeRepositoryPackageSnapshot } from './repositoryPackage.js';
24
26
  import { validateRepositorySourceAuthorization } from './repositorySourceAuthorization.js';
25
27
  import { advanceRepositorySourceBaseline, BlockedRepositorySourceRetry, fetchRepositorySourceAuthorization, recoverPublishedLocalSourceBaseline, RepositorySourceWatcher, scanRepositorySourceChanges, seedRepositorySourceWatcher } from './repositorySourceSync.js';
26
28
  import { assertRepositoryInstallerOwnership, writeRepositoryInstallerFile } from './repositoryInstallerFiles.js';
29
+ import { recoverLegacyRepositoryInstaller, selectLegacyInstallerRecoveryWorkspace } from './legacyInstallerRecovery.js';
27
30
  import { onboardingResumeCommand, selectDeviceWorkspace, workspaceIdForDevice } from './onboardingWorkspace.js';
28
31
  import { receiveRepositoryPackageDelivery } from './repositoryPackageDelivery.js';
29
32
  import { selectInstalledRepositoryKnowledge } from './repositoryInstalledKnowledge.js';
@@ -55,7 +58,7 @@ import { openCodexAppServerTransport } from '@dharma-ai-labs/agent-fabric-provid
55
58
  import { createNamedSessionTrust, isNamedSessionOwnerReceipt, renewNamedSessionLifetime } from './namedSessionTrust.js';
56
59
  import { startNamedCodexThread } from './namedCodexThread.js';
57
60
  export { openCooperativeInboxSession } from './cooperativeInboxSession.js';
58
- const VERSION = '0.2.118';
61
+ const VERSION = '0.2.120';
59
62
  const USAGE = CLI_USAGE;
60
63
  const execFileAsync = promisify(execFile);
61
64
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -1302,12 +1305,24 @@ async function registry() {
1302
1305
  }
1303
1306
  async function saveRegistry(items) {
1304
1307
  await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
1305
- await writeFile(workspaceRegistryPath(), `${JSON.stringify(items, null, 2)}\n`, { mode: 0o600 });
1308
+ await writeJsonAtomic(workspaceRegistryPath(), items);
1306
1309
  }
1307
1310
  async function saveWorkspaceRecord(entry) {
1308
- const items = (await registry()).filter((item) => item.workspaceId !== entry.workspaceId);
1309
- items.push(entry);
1310
- await saveRegistry(items);
1311
+ const release = await acquirePidLock(`${workspaceRegistryPath()}.mutation.lock`, 10_000, 'Workspace registry is busy; preserve it for recovery.');
1312
+ try {
1313
+ const value = await readFile(workspaceRegistryPath(), 'utf8').catch(error => {
1314
+ if (error.code === 'ENOENT')
1315
+ return '[]';
1316
+ throw error;
1317
+ });
1318
+ const items = JSON.parse(value);
1319
+ if (!Array.isArray(items))
1320
+ throw new Error('Workspace registry is invalid; preserve it for recovery.');
1321
+ await saveRegistry([...items.filter(item => item.workspaceId !== entry.workspaceId), entry]);
1322
+ }
1323
+ finally {
1324
+ await release();
1325
+ }
1311
1326
  }
1312
1327
  async function gitValue(workspace, argv) {
1313
1328
  try {
@@ -1525,6 +1540,103 @@ async function installStableRepositoryLauncher(workspace) {
1525
1540
  await writeFile(cmdPath, contents.windows, { mode: 0o600 });
1526
1541
  return { shell: '.dharma/bin/dharma', windows: '.dharma/bin/dharma.cmd' };
1527
1542
  }
1543
+ async function relayUpgrade(flags) {
1544
+ const config = await readDeviceConfig();
1545
+ if (!config)
1546
+ throw new Error('relay_upgrade_enrollment_required');
1547
+ if (flags.has('dry-run') && flags.has('apply'))
1548
+ throw new Error('relay_upgrade_flags_conflict');
1549
+ const workspace = await realpath(String(flags.get('workspace') || '.'));
1550
+ const selected = selectDeviceWorkspace(await registry(), {
1551
+ organizationId: config.organizationId, deviceId: config.deviceId, path: workspace,
1552
+ });
1553
+ if (!selected)
1554
+ throw new Error('relay_upgrade_workspace_unregistered');
1555
+ const home = dharmaHome();
1556
+ const startup = await inspectOwnedRelayAutostart({ home });
1557
+ const expectedLauncher = resolve(workspace, '.dharma', 'bin', process.platform === 'win32' ? 'dharma.cmd' : 'dharma');
1558
+ if (startup.workspace !== workspace || startup.launcher !== expectedLauncher
1559
+ || startup.policy !== resolve(workspace, '.dharma', 'approved-policy.json')) {
1560
+ throw new Error('relay_upgrade_workspace_conflict');
1561
+ }
1562
+ const assertStopped = async () => {
1563
+ if (await relaySupervisorProcessState(home) !== 'stopped' || await relayProcessState(home) !== 'stopped') {
1564
+ throw new Error('relay_upgrade_runtime_busy: finish current work, then stop the owned relay before upgrading.');
1565
+ }
1566
+ const entries = await readdir(resolve(home, 'sessions'), { withFileTypes: true }).catch(error => {
1567
+ if (error.code === 'ENOENT')
1568
+ return [];
1569
+ throw error;
1570
+ });
1571
+ for (const entry of entries) {
1572
+ if (!entry.isDirectory() || !/^[a-z][a-z0-9-]{0,47}$/.test(entry.name))
1573
+ continue;
1574
+ if (await pidProcessState(resolve(home, 'sessions', entry.name, 'service.lock')) !== 'stopped') {
1575
+ throw new Error('relay_upgrade_session_busy: finish and stop the named session before upgrading.');
1576
+ }
1577
+ }
1578
+ };
1579
+ return withRelayStartupMutation(async () => {
1580
+ const receipt = await upgradeRelayRuntime({ home, workspace, version: VERSION,
1581
+ organizationId: config.organizationId, deviceId: config.deviceId, workspaceId: selected.workspaceId,
1582
+ dryRun: !flags.has('apply'), rollback: flags.has('rollback') }, {
1583
+ assertStopped, inspectStartup: async () => {
1584
+ const current = await inspectOwnedRelayAutostart({ home });
1585
+ if (current.launcher !== expectedLauncher || current.policy !== startup.policy
1586
+ || current.workspace !== workspace)
1587
+ throw new Error('relay_upgrade_workspace_conflict');
1588
+ return current;
1589
+ },
1590
+ launcherContents: version => stableRepositoryLauncherContents(version, { platform: process.platform, nodeDirectory: dirname(process.execPath) }),
1591
+ legacyLauncherContents: version => stableRepositoryLauncherContents(version),
1592
+ configureStartup: version => enableRelayAutostart({ home, workspace,
1593
+ launcher: startup.launcher, policy: startup.policy, version }),
1594
+ start: () => startRelayAutostart({ home }),
1595
+ stop: async () => {
1596
+ const activationLocks = resolve(home, 'registry', 'skill-activation-locks');
1597
+ const locks = await readdir(activationLocks).catch(error => {
1598
+ if (error.code === 'ENOENT')
1599
+ return [];
1600
+ throw error;
1601
+ });
1602
+ for (const lock of locks) {
1603
+ if (lock.endsWith('.lock') && await pidProcessState(resolve(activationLocks, lock)) !== 'stopped') {
1604
+ throw new Error('relay_upgrade_work_in_progress: recovery must wait for a safe task boundary.');
1605
+ }
1606
+ }
1607
+ const binding = await readFile(resolve(home, 'relay', 'supervisor-workspace.json'), 'utf8')
1608
+ .then(value => JSON.parse(value)).catch(() => null);
1609
+ if (await relaySupervisorProcessState(home) !== 'stopped'
1610
+ && binding?.workspaceId !== selected.workspaceId)
1611
+ throw new Error('relay_upgrade_process_unverified');
1612
+ await stopRelayAutostart({ home });
1613
+ const stopped = await relayStop();
1614
+ if (stopped.ok !== true)
1615
+ throw new Error('relay_upgrade_stop_unconfirmed');
1616
+ },
1617
+ verify: async (version, since) => {
1618
+ for (let attempt = 0; attempt < 90; attempt++) {
1619
+ const read = (name) => readFile(resolve(home, 'relay', name), 'utf8').catch(() => 'null');
1620
+ const supervisorPid = Number((await read('supervisor.pid')).trim());
1621
+ const relayPid = Number((await read('relay.pid')).trim());
1622
+ const supervisor = JSON.parse(await read('supervisor-workspace.json'));
1623
+ const lastPoll = JSON.parse(await read('last-successful-poll.json'));
1624
+ if (await relaySupervisorProcessState(home) === 'running' && await relayProcessState(home) === 'running'
1625
+ && relayRuntimeObservationReady({ version, workspaceId: selected.workspaceId, since,
1626
+ supervisorPid, relayPid, supervisor, lastPoll, allowLegacyPoll: version !== VERSION }))
1627
+ return;
1628
+ if (attempt < 89)
1629
+ await new Promise(resolveWait => setTimeout(resolveWait, 1000));
1630
+ }
1631
+ throw new Error('relay_upgrade_receiver_unconfirmed');
1632
+ },
1633
+ });
1634
+ const validation = await validateContract(resolve(fileURLToPath(new URL('.', import.meta.url)), 'schemas'), 'https://schemas.dharma-ai.io/local-relay-upgrade/v1', receipt);
1635
+ if (!validation.ok)
1636
+ throw new Error('relay_upgrade_receipt_invalid');
1637
+ return receipt;
1638
+ });
1639
+ }
1528
1640
  export async function waitForRelayReadiness(options = {}) {
1529
1641
  const processState = options.processState || relayProcessState;
1530
1642
  const probe = options.probe || probeRelayConnection;
@@ -1558,6 +1670,15 @@ export async function waitForRelayReadiness(options = {}) {
1558
1670
  throw new Error(`The outbound relay did not become ready after bootstrap (${reason}).`);
1559
1671
  }
1560
1672
  async function startRelayDaemon(policyPath) {
1673
+ const config = await readDeviceConfig();
1674
+ if (!config)
1675
+ throw new Error('Relay startup requires current device enrollment.');
1676
+ const policy = await loadOrganizationPolicy(policyPath);
1677
+ const workspaceId = policy.serverAuthorization?.workspaceId;
1678
+ const selected = (await registry()).find(row => row.workspaceId === workspaceId
1679
+ && row.organizationId === config.organizationId && resolve(row.path, '.dharma', 'approved-policy.json') === policyPath);
1680
+ if (!selected)
1681
+ throw new Error('Relay policy must belong to this enrolled repository.');
1561
1682
  const alreadyRunning = await relayProcessState() === 'running';
1562
1683
  const supervisorState = await relaySupervisorProcessState();
1563
1684
  if (supervisorState === 'unknown')
@@ -1579,8 +1700,11 @@ async function startRelayDaemon(policyPath) {
1579
1700
  const binding = await readFile(resolve(dharmaHome(), 'relay', 'supervisor-workspace.json'), 'utf8')
1580
1701
  .then(value => JSON.parse(value)).catch(() => null);
1581
1702
  if (binding?.pid === pid) {
1582
- if (binding.policyPath !== policyPath) {
1583
- throw new Error('relay_workspace_conflict: the active relay supervisor belongs to another workspace.');
1703
+ if (binding.version !== VERSION || !binding.standardRepositories) {
1704
+ throw new Error('relay_runtime_upgrade_required: safely stop and upgrade the existing startup-anchor relay; do not retarget a working receiver.');
1705
+ }
1706
+ if (binding.organizationId !== config.organizationId || binding.deviceId !== config.deviceId) {
1707
+ throw new Error('relay_workspace_conflict: the active supervisor belongs to another enrollment.');
1584
1708
  }
1585
1709
  supervisorReady = true;
1586
1710
  break;
@@ -1591,7 +1715,14 @@ async function startRelayDaemon(policyPath) {
1591
1715
  }
1592
1716
  if (!supervisorReady)
1593
1717
  throw new Error('The relay supervisor did not become ready after bootstrap.');
1594
- const readiness = await waitForRelayReadiness({ expectedVersion: VERSION });
1718
+ const readiness = await waitForRelayReadiness({ expectedVersion: VERSION, attempts: 120, delayMs: 1000, processState: async () => {
1719
+ if (await relayProcessState() !== 'running')
1720
+ return 'stopped';
1721
+ const pid = Number((await readFile(resolve(dharmaHome(), 'relay', 'relay.pid'), 'utf8')).trim());
1722
+ const poll = await readFile(resolve(dharmaHome(), 'relay', 'repositories', selected.workspaceId, 'last-successful-poll.json'), 'utf8').then(value => JSON.parse(value)).catch(() => null);
1723
+ return repositoryRelayObservationReady({ observation: poll, workspaceId: selected.workspaceId,
1724
+ version: VERSION, pid }) ? 'running' : 'stopped';
1725
+ } });
1595
1726
  return { started: !alreadyRunning, supervisor: 'running', ...readiness };
1596
1727
  }
1597
1728
  async function supervisedDemoCycle(registration, signal) {
@@ -1654,7 +1785,8 @@ async function relaySupervise(flags) {
1654
1785
  const health = createDemoWatchHealthRecorder({ home: dharmaHome(), pid: process.pid, version: VERSION });
1655
1786
  try {
1656
1787
  await writeJsonAtomic(bindingPath, { pid: process.pid, workspaceId: selectedWorkspace?.workspaceId ?? null,
1657
- policyPath, demoWatches: true, version: VERSION });
1788
+ policyPath, demoWatches: true, version: VERSION, standardRepositories: Boolean(policyPath),
1789
+ organizationId: config?.organizationId ?? null, deviceId: config?.deviceId ?? null });
1658
1790
  const demo = runDemoSupervisor({ signal: controller.signal,
1659
1791
  list: () => listDemoWatchRegistrations(dharmaHome()), cycle: (registration, signal) => {
1660
1792
  if (!health.available(demoWatchRegistrationKey(registration))) {
@@ -1969,6 +2101,33 @@ async function bootstrap(flags) {
1969
2101
  });
1970
2102
  scopes = redeemed.organizationApiTokenScopes;
1971
2103
  }
2104
+ let organizationApiTokenStored = false;
2105
+ let credentialFailure = null;
2106
+ try {
2107
+ organizationApiTokenStored = Boolean(await loadOrganizationApiToken({
2108
+ hqUrl, organizationId, installationId: config.installationId,
2109
+ }));
2110
+ if (!organizationApiTokenStored && !String(process.env.DHARMA_ORG_API_TOKEN || '').trim()) {
2111
+ credentialFailure = 'organization_api_credentials_required';
2112
+ }
2113
+ }
2114
+ catch {
2115
+ // Secure-store diagnostics can contain secrets. Emit only the failed stage.
2116
+ credentialFailure = 'organization_api_credential_store_unavailable';
2117
+ }
2118
+ if (credentialFailure) {
2119
+ return {
2120
+ ok: false,
2121
+ stage: 'organization_api_credentials',
2122
+ code: credentialFailure,
2123
+ message: 'The enrolled device is preserved, but its organization API credential is unavailable. Contact Dharma support for device-bound credential recovery; do not repeat enrollment or use another member\'s token.',
2124
+ sharedRepositoryReady: false,
2125
+ enrollment: {
2126
+ organizationId, deviceId: config.deviceId, status: 'approved', organizationApiTokenStored,
2127
+ scopes, recipientApproval, rebind,
2128
+ },
2129
+ };
2130
+ }
1972
2131
  const onboardFlags = new Map(flags);
1973
2132
  onboardFlags.delete('grant');
1974
2133
  onboardFlags.set('portal-url', hqUrl);
@@ -1987,7 +2146,7 @@ async function bootstrap(flags) {
1987
2146
  organizationId,
1988
2147
  deviceId: config.deviceId,
1989
2148
  status: 'approved',
1990
- organizationApiTokenStored: true,
2149
+ organizationApiTokenStored,
1991
2150
  scopes,
1992
2151
  recipientApproval,
1993
2152
  rebind,
@@ -1998,11 +2157,36 @@ async function bootstrap(flags) {
1998
2157
  const launcher = await installStableRepositoryLauncher(workspace);
1999
2158
  const autostart = flags.has('no-relay-daemon')
2000
2159
  ? { state: 'disabled', backend: null }
2001
- : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(() => enableRelayAutostart({
2002
- home: dharmaHome(), workspace, policy: resolve(workspace, '.dharma', 'approved-policy.json'),
2003
- launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell),
2004
- version: VERSION,
2005
- })));
2160
+ : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(async () => {
2161
+ const home = dharmaHome();
2162
+ const existing = await relayAutostartStatus({ home });
2163
+ if (existing.backend !== null || existing.state !== 'disabled') {
2164
+ const anchor = await inspectOwnedRelayAutostart({ home });
2165
+ if (anchor.policy !== null) {
2166
+ const current = await readDeviceConfig();
2167
+ if (!current || current.organizationId !== organizationId || current.deviceId !== config.deviceId) {
2168
+ throw new Error('relay_workspace_conflict: startup enrollment changed.');
2169
+ }
2170
+ const selected = selectDeviceWorkspace(await registry(), { organizationId,
2171
+ deviceId: config.deviceId, path: anchor.workspace });
2172
+ if (!selected)
2173
+ throw new Error('relay_workspace_conflict: startup anchor is not registered to this device.');
2174
+ if (anchor.policy !== resolve(anchor.workspace, '.dharma', 'approved-policy.json')) {
2175
+ throw new Error('relay_workspace_conflict: startup policy is not canonical.');
2176
+ }
2177
+ const authorized = await loadOrganizationPolicy(anchor.policy);
2178
+ const canonical = (await registry()).filter(row => row.workspaceId === authorized.serverAuthorization?.workspaceId
2179
+ && row.organizationId === organizationId && row.path === anchor.workspace
2180
+ && row.routeHash === selected.routeHash && row.repositoryRemoteHash === selected.repositoryRemoteHash);
2181
+ if (canonical.length !== 1)
2182
+ throw new Error('relay_workspace_conflict: startup policy route changed.');
2183
+ await loadVerifiedWorkspacePolicy(anchor.policy, authorized.serverAuthorization?.workspaceId ?? '');
2184
+ }
2185
+ }
2186
+ return enableRelayAutostart({ home, workspace, policy: resolve(workspace, '.dharma', 'approved-policy.json'),
2187
+ launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell),
2188
+ version: VERSION, preserveStandardAnchor: true });
2189
+ }));
2006
2190
  let sharedRepositoryReady = onboarded.sharedRepositoryReady === true;
2007
2191
  const completionRequested = flags.has('complete');
2008
2192
  if (!completionRequested) {
@@ -2015,7 +2199,7 @@ async function bootstrap(flags) {
2015
2199
  organizationId,
2016
2200
  deviceId: config.deviceId,
2017
2201
  status: 'approved',
2018
- organizationApiTokenStored: true,
2202
+ organizationApiTokenStored,
2019
2203
  scopes,
2020
2204
  recipientApproval,
2021
2205
  rebind,
@@ -2106,7 +2290,7 @@ async function bootstrap(flags) {
2106
2290
  organizationId,
2107
2291
  deviceId: config.deviceId,
2108
2292
  status: 'approved',
2109
- organizationApiTokenStored: true,
2293
+ organizationApiTokenStored,
2110
2294
  scopes,
2111
2295
  recipientApproval,
2112
2296
  rebind,
@@ -2791,8 +2975,18 @@ async function evidencePreview(flags) {
2791
2975
  captureProvenance: captureProvenance(session.endedAt),
2792
2976
  });
2793
2977
  });
2978
+ const disclosureFailures = capsules.map((capsule) => {
2979
+ try {
2980
+ return containsDisallowedLocalPath(capsule) ? 'local_path_disclosure_forbidden' : null;
2981
+ }
2982
+ catch {
2983
+ return 'local_path_inspection_depth_exceeded';
2984
+ }
2985
+ }).filter((reason) => reason !== null);
2794
2986
  automaticDisclosure = {
2795
- ready: true,
2987
+ ready: disclosureFailures.length === 0,
2988
+ blockedCapsuleCount: disclosureFailures.length,
2989
+ reasonCodes: [...new Set(disclosureFailures)].sort(),
2796
2990
  disclosureClass: 'automatic_capsule',
2797
2991
  disclosureMode: capsules[0]?.automaticDisclosureMode ?? policy.evidence.automaticDisclosure?.mode ?? 'local_analysis',
2798
2992
  consentReceiptId: policy.evidence.automaticDisclosure?.consentReceiptId ?? null,
@@ -3750,6 +3944,38 @@ The CLI enrolls this device through browser-confirmed Clerk organization consent
3750
3944
  knowledge,
3751
3945
  };
3752
3946
  }
3947
+ async function repositoryInstallerRecoveryCommand(flags) {
3948
+ if (flags.has('apply') === flags.has('dry-run'))
3949
+ throw new Error('Choose exactly one of --apply or --dry-run.');
3950
+ const fromWorkspaceId = required(flags, 'from-workspace-id');
3951
+ const workspaceId = required(flags, 'workspace-id');
3952
+ const workspace = await realpath(String(flags.get('workspace') || '.'));
3953
+ const organizationId = required(flags, 'organization-id');
3954
+ const config = await readDeviceConfig();
3955
+ if (!config || config.organizationId !== organizationId)
3956
+ throw new Error('Recovery requires current organization enrollment.');
3957
+ await loadDeviceEnrollmentAnchor({ config });
3958
+ const identity = await preflightBootstrapWorkspaceIdentity(workspace);
3959
+ const registered = selectLegacyInstallerRecoveryWorkspace(await registry(), { organizationId, deviceId: config.deviceId,
3960
+ path: workspace, repositoryRemoteHash: identity.fingerprint, workspaceId });
3961
+ const repositoryAgentKey = `repo:${identity.fingerprint.slice('sha256:'.length, 'sha256:'.length + 24)}`;
3962
+ if (!registered?.repositoryBindingId || !registered.repositoryAgentId
3963
+ || registered.repositoryAgentKey !== repositoryAgentKey) {
3964
+ throw new Error('Recovery requires the enrolled device canonical repository binding.');
3965
+ }
3966
+ const policy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
3967
+ if (policy.organizationId !== organizationId || policy.serverAuthorization?.workspaceId !== registered.workspaceId) {
3968
+ throw new Error('Recovery requires a current signed workspace policy.');
3969
+ }
3970
+ await assertWorkspaceAuthorizationCurrent(registered.workspaceId, policy.serverAuthorization);
3971
+ const scope = { organizationId, workspaceId: registered.workspaceId,
3972
+ repositoryBindingId: registered.repositoryBindingId, repositoryAgentId: registered.repositoryAgentId };
3973
+ validateRepositorySourceAuthorization(await fetchRepositorySourceAuthorization(await client(), scope), scope, new Date());
3974
+ const recovery = await recoverLegacyRepositoryInstaller({ workspace, fromWorkspaceId,
3975
+ workspaceId: registered.workspaceId, repositoryAgentKey, apply: flags.has('apply') });
3976
+ return { ok: true, dryRun: !recovery.applied, serverMutation: false, recovery,
3977
+ nextAction: 'Resume pinned onboarding; recovery is not signed skill installation or activation.' };
3978
+ }
3753
3979
  async function repositorySnapshotCommand(flags, outputs) {
3754
3980
  if (flags.has('apply') && flags.has('dry-run'))
3755
3981
  throw new Error('Choose --apply or --dry-run, not both.');
@@ -5837,6 +6063,52 @@ async function takeCachedSkillUpdate(input) {
5837
6063
  }
5838
6064
  }
5839
6065
  async function relayStart(flags) {
6066
+ const anchor = resolve(required(flags, 'policy'));
6067
+ const config = await readDeviceConfig();
6068
+ if (!config)
6069
+ throw new Error('Relay requires current enrollment.');
6070
+ const declared = await loadOrganizationPolicy(anchor);
6071
+ const rows = await registry();
6072
+ const selected = rows.find(row => row.workspaceId === declared.serverAuthorization?.workspaceId
6073
+ && row.organizationId === config.organizationId && resolve(row.path, '.dharma', 'approved-policy.json') === anchor);
6074
+ if (!selected)
6075
+ throw new Error('Relay policy must be the canonical policy of one registered workspace.');
6076
+ const authorized = await selectRepositoryRelayRegistrations(rows, config, loadOrganizationPolicy);
6077
+ if (!authorized.some(row => row.workspaceId === selected.workspaceId && row.policyPath === anchor)) {
6078
+ throw new Error('Relay policy is not registered to this enrolled device.');
6079
+ }
6080
+ await loadVerifiedWorkspacePolicy(anchor, selected.workspaceId);
6081
+ const release = await acquireRelayProcessLease();
6082
+ const controller = new AbortController();
6083
+ const stop = () => controller.abort();
6084
+ process.once('SIGINT', stop);
6085
+ process.once('SIGTERM', stop);
6086
+ const serialized = serializeRelayWork();
6087
+ try {
6088
+ if (flags.has('once'))
6089
+ return await relayWorkspaceLoop(flags, controller.signal, serialized);
6090
+ await runRegisteredRepositoryRelays({ signal: controller.signal, list: async () => {
6091
+ const currentConfig = await readDeviceConfig();
6092
+ if (currentConfig?.organizationId !== config.organizationId || currentConfig.deviceId !== config.deviceId) {
6093
+ throw new Error('Relay enrollment changed.');
6094
+ }
6095
+ const records = JSON.parse(await readFile(workspaceRegistryPath(), 'utf8'));
6096
+ if (!Array.isArray(records))
6097
+ throw new Error('Invalid workspace registry.');
6098
+ return selectRepositoryRelayRegistrations(records, config, loadOrganizationPolicy);
6099
+ }, run: (row, signal) => relayWorkspaceLoop(new Map([...flags, ['policy', row.policyPath]]), signal, serialized),
6100
+ observe: event => process.stderr.write(`${JSON.stringify({ event: 'repository_relay', ...event })}\n`) });
6101
+ return { ok: true, stopped: true };
6102
+ }
6103
+ finally {
6104
+ controller.abort();
6105
+ process.removeListener('SIGINT', stop);
6106
+ process.removeListener('SIGTERM', stop);
6107
+ await release();
6108
+ }
6109
+ }
6110
+ async function relayWorkspaceLoop(flags, signal, serialized) {
6111
+ signal.throwIfAborted();
5840
6112
  const policyPath = resolve(required(flags, 'policy'));
5841
6113
  const declaredPolicy = await loadOrganizationPolicy(policyPath);
5842
6114
  const declaredWorkspaceId = declaredPolicy.serverAuthorization?.workspaceId;
@@ -5851,400 +6123,411 @@ async function relayStart(flags) {
5851
6123
  }
5852
6124
  let canonicalWorkspace = selectedWorkspace;
5853
6125
  let policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6126
+ signal.throwIfAborted();
5854
6127
  const fabric = await client();
5855
6128
  const leaseSeconds = Number(flags.get('lease-seconds') || 120);
5856
6129
  const pollMs = Math.min(Math.max(Number(flags.get('poll-seconds') || 3), 1), 60) * 1_000;
5857
- const releaseRelayLease = await acquireRelayProcessLease();
5858
- try {
5859
- let stopping = false;
5860
- let stopPreparation;
5861
- const stop = () => { stopping = true; stopPreparation?.(); };
5862
- process.once('SIGINT', stop);
5863
- process.once('SIGTERM', stop);
5864
- let tasksCompleted = 0;
5865
- let lastProviderFailureCategory = null;
5866
- let taskTrajectoriesRecovered = 0;
5867
- let evidenceResponsesCompleted = 0;
5868
- let trajectorySyncsCompleted = 0;
5869
- let nextPolicyRefreshAt = 0;
5870
- let lastPollReceiptAt = 0;
5871
- let evidencePolicyFresh = false;
5872
- const repositorySourceWatcher = new RepositorySourceWatcher();
5873
- const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
5874
- let repositorySourceBaselineAvailable = true;
5875
- let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
5876
- || (canonicalWorkspace.repositoryPackage?.state === 'published'
5877
- && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
5878
- ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
5879
- if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
5880
- try {
5881
- const recovered = await recoverPublishedLocalSourceBaseline({
5882
- workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
5883
- workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5884
- repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
5885
- });
5886
- if (recovered && recovered !== publishedLocalSnapshotHash) {
5887
- publishedLocalSnapshotHash = recovered;
5888
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
5889
- ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
5890
- publishedLocalSnapshotHash: recovered,
5891
- } };
5892
- await saveWorkspaceRecord(canonicalWorkspace);
5893
- }
5894
- }
5895
- catch {
5896
- repositorySourceBaselineAvailable = false;
6130
+ let stopping = signal.aborted;
6131
+ let stopPreparation;
6132
+ const stop = () => { stopping = true; stopPreparation?.(); };
6133
+ let tasksCompleted = 0;
6134
+ let lastProviderFailureCategory = null;
6135
+ let taskTrajectoriesRecovered = 0;
6136
+ let evidenceResponsesCompleted = 0;
6137
+ let trajectorySyncsCompleted = 0;
6138
+ let nextPolicyRefreshAt = 0;
6139
+ let lastPollReceiptAt = 0;
6140
+ let evidencePolicyFresh = false;
6141
+ const repositorySourceWatcher = new RepositorySourceWatcher();
6142
+ const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
6143
+ let repositorySourceBaselineAvailable = true;
6144
+ let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
6145
+ || (canonicalWorkspace.repositoryPackage?.state === 'published'
6146
+ && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
6147
+ ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
6148
+ if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6149
+ try {
6150
+ const recovered = await recoverPublishedLocalSourceBaseline({
6151
+ workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
6152
+ workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6153
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
6154
+ });
6155
+ if (recovered && recovered !== publishedLocalSnapshotHash) {
6156
+ publishedLocalSnapshotHash = recovered;
6157
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
6158
+ ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
6159
+ publishedLocalSnapshotHash: recovered,
6160
+ } };
6161
+ await saveWorkspaceRecord(canonicalWorkspace);
5897
6162
  }
5898
6163
  }
5899
- if (publishedLocalSnapshotHash) {
5900
- try {
5901
- await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
5902
- const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
5903
- if (localHash && (localHash === publishedLocalSnapshotHash
5904
- || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
5905
- await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
5906
- organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
5907
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5908
- repositoryAgentId: canonicalWorkspace.repositoryAgentId,
5909
- publishedHash: publishedLocalSnapshotHash,
5910
- localHash,
5911
- watcher: repositorySourceWatcher });
5912
- }
5913
- }
5914
- catch {
5915
- repositorySourceBaselineAvailable = false;
6164
+ catch {
6165
+ repositorySourceBaselineAvailable = false;
6166
+ }
6167
+ }
6168
+ if (publishedLocalSnapshotHash) {
6169
+ try {
6170
+ await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
6171
+ const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
6172
+ if (localHash && (localHash === publishedLocalSnapshotHash
6173
+ || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
6174
+ await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
6175
+ organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6176
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6177
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId,
6178
+ publishedHash: publishedLocalSnapshotHash,
6179
+ localHash,
6180
+ watcher: repositorySourceWatcher });
5916
6181
  }
5917
6182
  }
5918
- let nextRepositorySourceScanAt = 0;
5919
- let repositorySourceCandidates = 0;
5920
- let repositorySourceFailures = 0;
5921
- let repositorySourceState = 'not_scanned';
5922
- let sourceScanFlight = null;
5923
- let sourceScanReady = null;
5924
- const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
5925
- const vault = await LocalVault.open({
5926
- root: resolve(dharmaHome(), 'vault'),
5927
- masterKey: await loadOrCreateVaultMasterKey(),
5928
- rawLocalDays: rawLocalRetentionDays(policy),
5929
- });
5930
- let skillPreparationsCompleted = 0;
5931
- let skillPreparationFailures = 0;
5932
- let skillActivationsCompleted = 0;
5933
- let skillActivationFailures = 0;
5934
- const skillActivationFailuresByProvider = {};
5935
- let nextSkillActivationAt = 0;
5936
- const skillPreparationPump = startSkillPreparationPump({
5937
- prepare: async (assertRunning) => {
5938
- await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
5939
- assertRunning();
5940
- const capability = await adapter.capability();
5941
- assertRunning();
5942
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6183
+ catch {
6184
+ repositorySourceBaselineAvailable = false;
6185
+ }
6186
+ }
6187
+ let nextRepositorySourceScanAt = 0;
6188
+ let repositorySourceCandidates = 0;
6189
+ let repositorySourceFailures = 0;
6190
+ let repositorySourceState = 'not_scanned';
6191
+ let sourceScanFlight = null;
6192
+ let sourceScanReady = null;
6193
+ const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
6194
+ const vault = await LocalVault.open({
6195
+ root: resolve(dharmaHome(), 'vault'),
6196
+ masterKey: await loadOrCreateVaultMasterKey(),
6197
+ rawLocalDays: rawLocalRetentionDays(policy),
6198
+ });
6199
+ if (signal.aborted)
6200
+ vault.close();
6201
+ signal.throwIfAborted();
6202
+ signal.addEventListener('abort', stop, { once: true });
6203
+ if (signal.aborted)
6204
+ stop();
6205
+ let skillPreparationsCompleted = 0;
6206
+ let skillPreparationFailures = 0;
6207
+ let skillActivationsCompleted = 0;
6208
+ let skillActivationFailures = 0;
6209
+ const skillActivationFailuresByProvider = {};
6210
+ let nextSkillActivationAt = 0;
6211
+ const skillPreparationPump = startSkillPreparationPump({
6212
+ prepare: async (assertRunning) => {
6213
+ await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
6214
+ assertRunning();
6215
+ const capability = await adapter.capability();
6216
+ assertRunning();
6217
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6218
+ return;
6219
+ const provider = adapter.providerId;
6220
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6221
+ provider, assertCurrent: assertRunning }, async () => {
6222
+ const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6223
+ provider, policyPath, fabric, automatic: true, assertRunning });
6224
+ if (!prepared) {
6225
+ assertRunning();
5943
6226
  return;
5944
- const provider = adapter.providerId;
5945
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5946
- provider, assertCurrent: assertRunning }, async () => {
5947
- const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5948
- provider, policyPath, fabric, automatic: true, assertRunning });
5949
- if (!prepared) {
5950
- assertRunning();
5951
- return;
5952
- }
5953
- let retained = false;
5954
- try {
5955
- prepared.assertCurrent();
5956
- const repositoryAgentId = prepared.workspace.repositoryAgentId;
5957
- if (!repositoryAgentId)
5958
- throw new Error('Preparation cache requires an enrolled repository agent.');
5959
- const pendingRecord = await serializeSkillPreparationRecord({
5960
- schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
5961
- deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
6227
+ }
6228
+ let retained = false;
6229
+ try {
6230
+ prepared.assertCurrent();
6231
+ const repositoryAgentId = prepared.workspace.repositoryAgentId;
6232
+ if (!repositoryAgentId)
6233
+ throw new Error('Preparation cache requires an enrolled repository agent.');
6234
+ const pendingRecord = await serializeSkillPreparationRecord({
6235
+ schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
6236
+ deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
6237
+ repositoryAgentId,
6238
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
6239
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
6240
+ bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
6241
+ ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
6242
+ preparedAt: new Date().toISOString(), activationAuthorized: false,
6243
+ });
6244
+ prepared.assertCurrent();
6245
+ await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
6246
+ prepared.assertCurrent();
6247
+ await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6248
+ provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
6249
+ expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
5962
6250
  repositoryAgentId,
5963
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
6251
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
5964
6252
  policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
5965
- bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
5966
- ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
5967
- preparedAt: new Date().toISOString(), activationAuthorized: false,
5968
- });
5969
- prepared.assertCurrent();
5970
- await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
5971
- prepared.assertCurrent();
5972
- await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5973
- provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
5974
- expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
5975
- repositoryAgentId,
5976
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
5977
- policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
5978
- bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
5979
- onCommitted: () => { retained = true; } });
5980
- skillPreparationsCompleted += 1;
5981
- }
5982
- finally {
5983
- if (!retained)
5984
- await rm(prepared.sourceRoot, { recursive: true, force: true });
5985
- }
5986
- });
5987
- }, () => { skillPreparationFailures += 1; });
5988
- },
5989
- onError: () => { skillPreparationFailures += 1; },
5990
- });
5991
- stopPreparation = skillPreparationPump.requestStop;
5992
- if (stopping)
5993
- stopPreparation();
5994
- const applyReadySourceScan = async () => {
5995
- if (!sourceScanReady)
5996
- return;
5997
- const ready = sourceScanReady;
5998
- sourceScanReady = null;
5999
- sourceScanFlight = null;
6000
- if ('error' in ready) {
6001
- repositorySourceWatcher.invalidate();
6002
- repositorySourceFailures += 1;
6003
- repositorySourceState = 'blocked';
6004
- nextRepositorySourceScanAt = performance.now() + 60_000;
6005
- return;
6006
- }
6007
- if ('state' in ready) {
6008
- repositorySourceState = ready.state;
6009
- nextRepositorySourceScanAt = performance.now() + 15_000;
6010
- return;
6011
- }
6012
- const sourceCycle = ready.cycle;
6013
- repositorySourceState = sourceCycle.state;
6014
- if (sourceCycle.localMutation) {
6015
- repositorySourceCandidates += 1;
6016
- if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
6017
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
6018
- if (sourceCycle.state === 'source_already_published') {
6253
+ bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
6254
+ onCommitted: () => { retained = true; } });
6255
+ skillPreparationsCompleted += 1;
6256
+ }
6257
+ finally {
6258
+ if (!retained)
6259
+ await rm(prepared.sourceRoot, { recursive: true, force: true });
6260
+ }
6261
+ });
6262
+ }, () => { skillPreparationFailures += 1; });
6263
+ },
6264
+ onError: () => { skillPreparationFailures += 1; },
6265
+ });
6266
+ stopPreparation = skillPreparationPump.requestStop;
6267
+ if (stopping)
6268
+ stopPreparation();
6269
+ const applyReadySourceScan = async () => {
6270
+ if (!sourceScanReady)
6271
+ return;
6272
+ const ready = sourceScanReady;
6273
+ sourceScanReady = null;
6274
+ sourceScanFlight = null;
6275
+ if ('error' in ready) {
6276
+ repositorySourceWatcher.invalidate();
6277
+ repositorySourceFailures += 1;
6278
+ repositorySourceState = 'blocked';
6279
+ nextRepositorySourceScanAt = performance.now() + 60_000;
6280
+ return;
6281
+ }
6282
+ if ('state' in ready) {
6283
+ repositorySourceState = ready.state;
6284
+ nextRepositorySourceScanAt = performance.now() + 15_000;
6285
+ return;
6286
+ }
6287
+ const sourceCycle = ready.cycle;
6288
+ repositorySourceState = sourceCycle.state;
6289
+ if (sourceCycle.localMutation) {
6290
+ repositorySourceCandidates += 1;
6291
+ if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
6292
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
6293
+ if (sourceCycle.state === 'source_already_published') {
6294
+ canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6295
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6296
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6297
+ publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6298
+ }
6299
+ else if (sourceCycle.candidate) {
6300
+ const receipt = sourceCycle.candidate;
6301
+ if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
6302
+ && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
6303
+ Object.assign(canonicalWorkspace.repositoryPackage, {
6304
+ state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
6305
+ snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
6306
+ releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
6307
+ consolidationMode: 'repository_update',
6308
+ });
6309
+ }
6310
+ if (receipt.state === 'published') {
6019
6311
  canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6020
6312
  canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6021
6313
  canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6022
6314
  publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6023
6315
  }
6024
- else if (sourceCycle.candidate) {
6025
- const receipt = sourceCycle.candidate;
6026
- if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
6027
- && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
6028
- Object.assign(canonicalWorkspace.repositoryPackage, {
6029
- state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
6030
- snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
6031
- releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
6032
- consolidationMode: 'repository_update',
6033
- });
6034
- }
6035
- if (receipt.state === 'published') {
6036
- canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6037
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6038
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6039
- publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6040
- }
6041
- else if ((receipt.state === 'accepted' || receipt.state === 'processing')
6042
- && typeof receipt.operationId === 'string') {
6043
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6044
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
6316
+ else if ((receipt.state === 'accepted' || receipt.state === 'processing')
6317
+ && typeof receipt.operationId === 'string') {
6318
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6319
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
6320
+ }
6321
+ else if (receipt.state === 'blocked') {
6322
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
6323
+ repositorySourceWatcher.invalidate();
6324
+ }
6325
+ }
6326
+ await saveWorkspaceRecord(canonicalWorkspace);
6327
+ }
6328
+ }
6329
+ nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
6330
+ };
6331
+ const consumeSourceScan = async () => {
6332
+ try {
6333
+ await applyReadySourceScan();
6334
+ }
6335
+ catch {
6336
+ repositorySourceWatcher.invalidate();
6337
+ repositorySourceFailures += 1;
6338
+ repositorySourceState = 'blocked';
6339
+ nextRepositorySourceScanAt = performance.now() + 60_000;
6340
+ }
6341
+ };
6342
+ try {
6343
+ signal.throwIfAborted();
6344
+ const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
6345
+ if (recoveredTaskTrajectories.state === 'completed') {
6346
+ taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
6347
+ }
6348
+ do {
6349
+ if (stopping)
6350
+ break;
6351
+ await consumeSourceScan();
6352
+ if (Date.now() >= nextPolicyRefreshAt) {
6353
+ try {
6354
+ await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
6355
+ policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6356
+ evidencePolicyFresh = true;
6357
+ }
6358
+ catch {
6359
+ evidencePolicyFresh = false;
6360
+ }
6361
+ nextPolicyRefreshAt = Date.now() + 60_000;
6362
+ }
6363
+ if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6364
+ try {
6365
+ const current = await pollRepositoryCandidate({ transport: fabric,
6366
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6367
+ scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6368
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6369
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
6370
+ if (current && canonicalWorkspace.repositoryPackage) {
6371
+ if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
6372
+ || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
6373
+ throw new Error('Repository candidate status changed identity while polling.');
6045
6374
  }
6046
- else if (receipt.state === 'blocked') {
6047
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
6375
+ const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
6376
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
6377
+ state: current.state, candidateId: current.candidateId, operationId: current.operationId,
6378
+ snapshotHash: current.snapshotHash, releaseId: current.releaseId,
6379
+ ...baseline,
6380
+ generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
6381
+ publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
6382
+ if (current.state === 'blocked')
6048
6383
  repositorySourceWatcher.invalidate();
6049
- }
6384
+ await saveWorkspaceRecord(canonicalWorkspace);
6050
6385
  }
6051
- await saveWorkspaceRecord(canonicalWorkspace);
6386
+ }
6387
+ catch {
6388
+ repositorySourceFailures += 1;
6052
6389
  }
6053
6390
  }
6054
- nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
6055
- };
6056
- const consumeSourceScan = async () => {
6057
- try {
6058
- await applyReadySourceScan();
6059
- }
6060
- catch {
6061
- repositorySourceWatcher.invalidate();
6062
- repositorySourceFailures += 1;
6063
- repositorySourceState = 'blocked';
6064
- nextRepositorySourceScanAt = performance.now() + 60_000;
6391
+ let evidenceRequestId;
6392
+ if (evidencePolicyFresh && !stopping) {
6393
+ const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6394
+ const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6395
+ const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
6396
+ return {
6397
+ synced,
6398
+ evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
6399
+ };
6400
+ });
6401
+ if (evidenceCycle.state === 'deferred') {
6402
+ evidencePolicyFresh = false;
6403
+ }
6404
+ else {
6405
+ trajectorySyncsCompleted += evidenceCycle.value.synced;
6406
+ evidenceRequestId = evidenceCycle.value.evidenceRequestId;
6407
+ if (evidenceRequestId)
6408
+ evidenceResponsesCompleted += 1;
6409
+ }
6065
6410
  }
6066
- };
6067
- try {
6068
- const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
6069
- if (recoveredTaskTrajectories.state === 'completed') {
6070
- taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
6411
+ const result = await serialized(async () => {
6412
+ if (stopping)
6413
+ return { taskId: null };
6414
+ return executeOneTask(fabric, leaseSeconds);
6415
+ });
6416
+ if (!stopping && Date.now() - lastPollReceiptAt >= 30_000) {
6417
+ const observation = {
6418
+ at: new Date().toISOString(), workspaceId: canonicalWorkspace.workspaceId, version: VERSION, pid: process.pid,
6419
+ };
6420
+ await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'repositories', canonicalWorkspace.workspaceId, 'last-successful-poll.json'), observation);
6421
+ const binding = await readFile(resolve(dharmaHome(), 'relay', 'supervisor-workspace.json'), 'utf8')
6422
+ .then(value => JSON.parse(value)).catch(() => null);
6423
+ if (!binding || binding.workspaceId === canonicalWorkspace.workspaceId) {
6424
+ await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'last-successful-poll.json'), observation);
6425
+ }
6426
+ lastPollReceiptAt = Date.now();
6071
6427
  }
6072
- do {
6073
- await consumeSourceScan();
6074
- if (Date.now() >= nextPolicyRefreshAt) {
6428
+ if (result.taskId)
6429
+ tasksCompleted += 1;
6430
+ if (typeof result.failureCategory === 'string')
6431
+ lastProviderFailureCategory = result.failureCategory;
6432
+ if (!stopping && !result.taskId && performance.now() >= nextSkillActivationAt) {
6433
+ for (const adapter of providerAdapters) {
6075
6434
  try {
6076
- await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
6077
- policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6078
- evidencePolicyFresh = true;
6435
+ const capability = await adapter.capability();
6436
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6437
+ continue;
6438
+ const provider = adapter.providerId;
6439
+ await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
6440
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6441
+ provider, assertCurrent: () => { if (stopping)
6442
+ throw new Error('Relay is stopping.'); } }, async () => {
6443
+ const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6444
+ provider, policyPath, fabric });
6445
+ const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6446
+ provider, policyPath, fabric, automatic: true });
6447
+ if (!prepared)
6448
+ return;
6449
+ if (!cached)
6450
+ skillPreparationsCompleted += 1;
6451
+ await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
6452
+ provider, policyPath, flags: new Map() });
6453
+ skillActivationsCompleted += 1;
6454
+ });
6455
+ });
6079
6456
  }
6080
6457
  catch {
6081
- evidencePolicyFresh = false;
6458
+ skillActivationFailures += 1;
6459
+ skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
6082
6460
  }
6083
- nextPolicyRefreshAt = Date.now() + 60_000;
6084
6461
  }
6085
- if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6462
+ nextSkillActivationAt = performance.now() + 60_000;
6463
+ }
6464
+ if (!stopping && !sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6465
+ const sourceWorkspace = canonicalWorkspace;
6466
+ const sourceBaselineHash = publishedLocalSnapshotHash;
6467
+ sourceScanFlight = (async () => {
6086
6468
  try {
6087
- const current = await pollRepositoryCandidate({ transport: fabric,
6088
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6089
- scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6090
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6091
- repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
6092
- if (current && canonicalWorkspace.repositoryPackage) {
6093
- if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
6094
- || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
6095
- throw new Error('Repository candidate status changed identity while polling.');
6096
- }
6097
- const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
6098
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
6099
- state: current.state, candidateId: current.candidateId, operationId: current.operationId,
6100
- snapshotHash: current.snapshotHash, releaseId: current.releaseId,
6101
- ...baseline,
6102
- generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
6103
- publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
6104
- if (current.state === 'blocked')
6105
- repositorySourceWatcher.invalidate();
6106
- await saveWorkspaceRecord(canonicalWorkspace);
6469
+ if (!repositorySourceBaselineAvailable) {
6470
+ throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
6107
6471
  }
6108
- }
6109
- catch {
6110
- repositorySourceFailures += 1;
6111
- }
6112
- }
6113
- let evidenceRequestId;
6114
- if (evidencePolicyFresh) {
6115
- const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6116
- const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6117
- const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
6118
- return {
6119
- synced,
6120
- evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
6121
- };
6122
- });
6123
- if (evidenceCycle.state === 'deferred') {
6124
- evidencePolicyFresh = false;
6125
- }
6126
- else {
6127
- trajectorySyncsCompleted += evidenceCycle.value.synced;
6128
- evidenceRequestId = evidenceCycle.value.evidenceRequestId;
6129
- if (evidenceRequestId)
6130
- evidenceResponsesCompleted += 1;
6131
- }
6132
- }
6133
- const result = await executeOneTask(fabric, leaseSeconds);
6134
- if (Date.now() - lastPollReceiptAt >= 30_000) {
6135
- await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'last-successful-poll.json'), {
6136
- at: new Date().toISOString(), workspaceId: canonicalWorkspace.workspaceId, version: VERSION,
6137
- });
6138
- lastPollReceiptAt = Date.now();
6139
- }
6140
- if (result.taskId)
6141
- tasksCompleted += 1;
6142
- if (typeof result.failureCategory === 'string')
6143
- lastProviderFailureCategory = result.failureCategory;
6144
- if (!result.taskId && performance.now() >= nextSkillActivationAt) {
6145
- for (const adapter of providerAdapters) {
6146
- try {
6147
- const capability = await adapter.capability();
6148
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6149
- continue;
6150
- const provider = adapter.providerId;
6151
- await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
6152
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6153
- provider, assertCurrent: () => { if (stopping)
6154
- throw new Error('Relay is stopping.'); } }, async () => {
6155
- const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6156
- provider, policyPath, fabric });
6157
- const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6158
- provider, policyPath, fabric, automatic: true });
6159
- if (!prepared)
6160
- return;
6161
- if (!cached)
6162
- skillPreparationsCompleted += 1;
6163
- await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
6164
- provider, policyPath, flags: new Map() });
6165
- skillActivationsCompleted += 1;
6166
- });
6167
- });
6472
+ if (!sourceWorkspace.repositoryBindingId || !sourceWorkspace.repositoryAgentId) {
6473
+ throw new Error('Repository source synchronization requires a complete repository binding.');
6168
6474
  }
6169
- catch {
6170
- skillActivationFailures += 1;
6171
- skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
6475
+ const installedKnowledge = await installedRepositoryKnowledge(sourceWorkspace);
6476
+ if (!installedKnowledge && (sourceWorkspace.repositoryPackage?.generation ?? 0) > 0) {
6477
+ sourceScanReady = { state: 'awaiting_signed_baseline' };
6172
6478
  }
6173
- }
6174
- nextSkillActivationAt = performance.now() + 60_000;
6175
- }
6176
- if (!sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6177
- const sourceWorkspace = canonicalWorkspace;
6178
- const sourceBaselineHash = publishedLocalSnapshotHash;
6179
- sourceScanFlight = (async () => {
6180
- try {
6181
- if (!repositorySourceBaselineAvailable) {
6182
- throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
6183
- }
6184
- if (!sourceWorkspace.repositoryBindingId || !sourceWorkspace.repositoryAgentId) {
6185
- throw new Error('Repository source synchronization requires a complete repository binding.');
6186
- }
6187
- const installedKnowledge = await installedRepositoryKnowledge(sourceWorkspace);
6188
- if (!installedKnowledge && (sourceWorkspace.repositoryPackage?.generation ?? 0) > 0) {
6189
- sourceScanReady = { state: 'awaiting_signed_baseline' };
6190
- }
6191
- else {
6192
- if (sourceWorkspace.repositoryPackage?.state === 'blocked' && sourceWorkspace.repositoryPackage.snapshotHash) {
6193
- const blocked = await readRepositorySourceBaselineSnapshot(sourceWorkspace.path, sourceWorkspace.repositoryPackage.snapshotHash);
6194
- if (blockedRepositorySourceRetry.consider(sourceWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
6195
- repositorySourceWatcher.invalidate();
6196
- }
6197
- const sourceCycle = await scanRepositorySourceChanges({
6198
- workspace: sourceWorkspace.path, organizationId: sourceWorkspace.organizationId,
6199
- workspaceId: sourceWorkspace.workspaceId, repositoryBindingId: sourceWorkspace.repositoryBindingId,
6200
- repositoryAgentId: sourceWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
6201
- loadRetainedKnowledge: () => installedRepositoryKnowledge(sourceWorkspace),
6202
- loadPublishedLocalBaseline: () => sourceBaselineHash
6203
- ? readRepositoryPackageSnapshot(sourceWorkspace.path, sourceBaselineHash) : Promise.resolve(null),
6204
- submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
6205
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6206
- scope: { organizationId: sourceWorkspace.organizationId,
6207
- workspaceId: sourceWorkspace.workspaceId,
6208
- repositoryBindingId: sourceWorkspace.repositoryBindingId,
6209
- repositoryAgentId: sourceWorkspace.repositoryAgentId },
6210
- snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
6211
- });
6212
- sourceScanReady = { cycle: sourceCycle };
6479
+ else {
6480
+ if (sourceWorkspace.repositoryPackage?.state === 'blocked' && sourceWorkspace.repositoryPackage.snapshotHash) {
6481
+ const blocked = await readRepositorySourceBaselineSnapshot(sourceWorkspace.path, sourceWorkspace.repositoryPackage.snapshotHash);
6482
+ if (blockedRepositorySourceRetry.consider(sourceWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
6483
+ repositorySourceWatcher.invalidate();
6213
6484
  }
6485
+ const sourceCycle = await scanRepositorySourceChanges({
6486
+ workspace: sourceWorkspace.path, organizationId: sourceWorkspace.organizationId,
6487
+ workspaceId: sourceWorkspace.workspaceId, repositoryBindingId: sourceWorkspace.repositoryBindingId,
6488
+ repositoryAgentId: sourceWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
6489
+ loadRetainedKnowledge: () => installedRepositoryKnowledge(sourceWorkspace),
6490
+ loadPublishedLocalBaseline: () => sourceBaselineHash
6491
+ ? readRepositoryPackageSnapshot(sourceWorkspace.path, sourceBaselineHash) : Promise.resolve(null),
6492
+ submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
6493
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6494
+ scope: { organizationId: sourceWorkspace.organizationId,
6495
+ workspaceId: sourceWorkspace.workspaceId,
6496
+ repositoryBindingId: sourceWorkspace.repositoryBindingId,
6497
+ repositoryAgentId: sourceWorkspace.repositoryAgentId },
6498
+ snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
6499
+ });
6500
+ sourceScanReady = { cycle: sourceCycle };
6214
6501
  }
6215
- catch (error) {
6216
- sourceScanReady = { error };
6217
- }
6218
- })();
6219
- }
6220
- if (flags.has('once'))
6221
- break;
6222
- if (!result.taskId && !evidenceRequestId)
6223
- await new Promise((accept) => setTimeout(accept, pollMs));
6224
- } while (!stopping);
6225
- }
6226
- finally {
6227
- if (sourceScanFlight)
6228
- await sourceScanFlight;
6229
- await consumeSourceScan();
6230
- await skillPreparationPump.stop();
6231
- vault.close();
6232
- process.removeListener('SIGINT', stop);
6233
- process.removeListener('SIGTERM', stop);
6234
- }
6235
- return {
6236
- ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
6237
- lastProviderFailureCategory,
6238
- evidenceResponsesCompleted, trajectorySyncsCompleted,
6239
- repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
6240
- skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
6241
- skillActivationFailuresByProvider,
6242
- sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
6243
- };
6502
+ }
6503
+ catch (error) {
6504
+ sourceScanReady = { error };
6505
+ }
6506
+ })();
6507
+ }
6508
+ if (flags.has('once'))
6509
+ break;
6510
+ if (!result.taskId && !evidenceRequestId)
6511
+ await waitForRelayRefresh(pollMs, signal);
6512
+ } while (!stopping);
6244
6513
  }
6245
6514
  finally {
6246
- await releaseRelayLease();
6515
+ if (sourceScanFlight)
6516
+ await sourceScanFlight;
6517
+ await consumeSourceScan();
6518
+ await skillPreparationPump.stop();
6519
+ vault.close();
6520
+ signal.removeEventListener('abort', stop);
6247
6521
  }
6522
+ return {
6523
+ ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
6524
+ lastProviderFailureCategory,
6525
+ evidenceResponsesCompleted, trajectorySyncsCompleted,
6526
+ repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
6527
+ skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
6528
+ skillActivationFailuresByProvider,
6529
+ sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
6530
+ };
6248
6531
  }
6249
6532
  export async function run(argv) {
6250
6533
  const { positional, flags, repeated } = parseCliOptions(argv);
@@ -6436,6 +6719,8 @@ export async function run(argv) {
6436
6719
  }
6437
6720
  if (command === 'repositories' && subcommand === 'list')
6438
6721
  return repositoriesList(flags);
6722
+ if (command === 'repositories' && subcommand === 'recover-installer')
6723
+ return repositoryInstallerRecoveryCommand(flags);
6439
6724
  if (command === 'repositories' && subcommand === 'snapshot') {
6440
6725
  return repositorySnapshotCommand(flags, repeated.get('approved-output') || []);
6441
6726
  }
@@ -6489,6 +6774,25 @@ export async function run(argv) {
6489
6774
  try {
6490
6775
  const config = JSON.parse(await readFile(configPath(), 'utf8'));
6491
6776
  const status = { version: VERSION, enrolled: true, relay, supervisor, autostart, reconnect };
6777
+ if (flags.has('verbose') || flags.has('diagnostic')) {
6778
+ const relayPid = await readFile(resolve(dharmaHome(), 'relay', 'relay.pid'), 'utf8')
6779
+ .then(value => Number(value.trim())).catch(() => 0);
6780
+ try {
6781
+ const registered = await selectRepositoryRelayRegistrations(await registry(), config, loadOrganizationPolicy);
6782
+ const observations = [];
6783
+ for (const row of registered) {
6784
+ const observation = await readFile(resolve(dharmaHome(), 'relay', 'repositories', row.workspaceId, 'last-successful-poll.json'), 'utf8').then(value => JSON.parse(value)).catch(() => null);
6785
+ observations.push({ workspaceId: row.workspaceId,
6786
+ state: relay === 'running' && repositoryRelayObservationReady({ observation,
6787
+ workspaceId: row.workspaceId, version: VERSION, pid: relayPid }) ? 'acknowledged_recently' : 'pending',
6788
+ lastSuccessfulPollAt: typeof observation?.at === 'string' ? observation.at : null });
6789
+ }
6790
+ status.repositoryRelays = { state: 'observed', repositories: observations };
6791
+ }
6792
+ catch {
6793
+ status.repositoryRelays = { state: 'unavailable', repositories: [] };
6794
+ }
6795
+ }
6492
6796
  if (flags.has('verbose') || flags.has('diagnostic')) {
6493
6797
  Object.assign(status, { home: dharmaHome(), organizationId: config.organizationId, deviceId: config.deviceId });
6494
6798
  }
@@ -6522,6 +6826,8 @@ export async function run(argv) {
6522
6826
  return probeRelayConnection();
6523
6827
  if (command === 'relay' && subcommand === 'stop')
6524
6828
  return relayStop();
6829
+ if (command === 'relay' && subcommand === 'upgrade')
6830
+ return relayUpgrade(flags);
6525
6831
  if (command === 'relay' && subcommand === 'autostart') {
6526
6832
  if (positional[2] === 'status')
6527
6833
  return relayAutostartStatus({ home: dharmaHome() });