@dharma-ai-labs/agent-fabric 0.2.209 → 0.2.211

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 (158) hide show
  1. package/dist/bin.js +2 -2
  2. package/dist/bin.js.map +1 -1
  3. package/dist/bootstrapHostScope.test.js +1 -1
  4. package/dist/bootstrapHostScope.test.js.map +1 -1
  5. package/dist/bootstrapProcessCaller.test.js +1 -0
  6. package/dist/bootstrapProcessCaller.test.js.map +1 -1
  7. package/dist/bootstrapVaultScope.test.js +25 -1
  8. package/dist/bootstrapVaultScope.test.js.map +1 -1
  9. package/dist/codexBoundSession.d.ts +59 -0
  10. package/dist/codexBoundSession.d.ts.map +1 -1
  11. package/dist/codexBoundSession.js +140 -1
  12. package/dist/codexBoundSession.js.map +1 -1
  13. package/dist/codexInboxSession.d.ts +1 -1
  14. package/dist/codexInboxSession.d.ts.map +1 -1
  15. package/dist/codexRecoveryController.test.d.ts +2 -0
  16. package/dist/codexRecoveryController.test.d.ts.map +1 -0
  17. package/dist/codexRecoveryController.test.js +295 -0
  18. package/dist/codexRecoveryController.test.js.map +1 -0
  19. package/dist/codexScopedDispatch.test.js +2 -1
  20. package/dist/codexScopedDispatch.test.js.map +1 -1
  21. package/dist/codexSetupChildProvider.test.js +20 -0
  22. package/dist/codexSetupChildProvider.test.js.map +1 -1
  23. package/dist/codexSetupNativeHost.test.js +2 -1
  24. package/dist/codexSetupNativeHost.test.js.map +1 -1
  25. package/dist/codexSetupSessionCaller.test.js +1 -0
  26. package/dist/codexSetupSessionCaller.test.js.map +1 -1
  27. package/dist/codexSigningRecoveryNativeHost.d.ts +50 -0
  28. package/dist/codexSigningRecoveryNativeHost.d.ts.map +1 -0
  29. package/dist/codexSigningRecoveryNativeHost.js +186 -0
  30. package/dist/codexSigningRecoveryNativeHost.js.map +1 -0
  31. package/dist/codexSigningRecoveryNativeHost.test.d.ts +2 -0
  32. package/dist/codexSigningRecoveryNativeHost.test.d.ts.map +1 -0
  33. package/dist/codexSigningRecoveryNativeHost.test.js +475 -0
  34. package/dist/codexSigningRecoveryNativeHost.test.js.map +1 -0
  35. package/dist/codexSigningRecoveryTool.d.ts +25 -0
  36. package/dist/codexSigningRecoveryTool.d.ts.map +1 -0
  37. package/dist/codexSigningRecoveryTool.js +24 -0
  38. package/dist/codexSigningRecoveryTool.js.map +1 -0
  39. package/dist/containerRelayLifecycle.d.ts +2 -1
  40. package/dist/containerRelayLifecycle.d.ts.map +1 -1
  41. package/dist/containerRelayLifecycle.js +12 -0
  42. package/dist/containerRelayLifecycle.js.map +1 -1
  43. package/dist/containerRelayLifecycle.test.js +38 -6
  44. package/dist/containerRelayLifecycle.test.js.map +1 -1
  45. package/dist/containerRelayRuntimeTrust.test.js +10 -0
  46. package/dist/containerRelayRuntimeTrust.test.js.map +1 -1
  47. package/dist/cooperativeInboxSession.d.ts +1 -1
  48. package/dist/demoPackage.d.ts +1 -1
  49. package/dist/demoWatchControl.d.ts +6 -6
  50. package/dist/deviceSigningMaintenanceController.d.ts +35 -0
  51. package/dist/deviceSigningMaintenanceController.d.ts.map +1 -0
  52. package/dist/deviceSigningMaintenanceController.js +197 -0
  53. package/dist/deviceSigningMaintenanceController.js.map +1 -0
  54. package/dist/deviceSigningMaintenanceController.test.d.ts +2 -0
  55. package/dist/deviceSigningMaintenanceController.test.d.ts.map +1 -0
  56. package/dist/deviceSigningMaintenanceController.test.js +67 -0
  57. package/dist/deviceSigningMaintenanceController.test.js.map +1 -0
  58. package/dist/deviceSigningMaintenanceFlow.test.d.ts +2 -0
  59. package/dist/deviceSigningMaintenanceFlow.test.d.ts.map +1 -0
  60. package/dist/deviceSigningMaintenanceFlow.test.js +196 -0
  61. package/dist/deviceSigningMaintenanceFlow.test.js.map +1 -0
  62. package/dist/index.d.ts +9 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +723 -470
  65. package/dist/index.js.map +1 -1
  66. package/dist/legacySigningMaintenanceBoundary.d.ts +14 -0
  67. package/dist/legacySigningMaintenanceBoundary.d.ts.map +1 -0
  68. package/dist/legacySigningMaintenanceBoundary.js +128 -0
  69. package/dist/legacySigningMaintenanceBoundary.js.map +1 -0
  70. package/dist/legacySigningMaintenanceBoundary.test.d.ts +2 -0
  71. package/dist/legacySigningMaintenanceBoundary.test.d.ts.map +1 -0
  72. package/dist/legacySigningMaintenanceBoundary.test.js +109 -0
  73. package/dist/legacySigningMaintenanceBoundary.test.js.map +1 -0
  74. package/dist/maintenanceControlCaller.test.d.ts +2 -0
  75. package/dist/maintenanceControlCaller.test.d.ts.map +1 -0
  76. package/dist/maintenanceControlCaller.test.js +119 -0
  77. package/dist/maintenanceControlCaller.test.js.map +1 -0
  78. package/dist/namedCodexThread.d.ts.map +1 -1
  79. package/dist/namedCodexThread.js +3 -1
  80. package/dist/namedCodexThread.js.map +1 -1
  81. package/dist/namedCodexThread.test.js +17 -1
  82. package/dist/namedCodexThread.test.js.map +1 -1
  83. package/dist/namedSessionMaintenance.d.ts +1 -1
  84. package/dist/namedSessionStartupFence.test.d.ts +2 -0
  85. package/dist/namedSessionStartupFence.test.d.ts.map +1 -0
  86. package/dist/namedSessionStartupFence.test.js +81 -0
  87. package/dist/namedSessionStartupFence.test.js.map +1 -0
  88. package/dist/namedSessionStoppedLock.d.ts +34 -0
  89. package/dist/namedSessionStoppedLock.d.ts.map +1 -0
  90. package/dist/namedSessionStoppedLock.js +108 -0
  91. package/dist/namedSessionStoppedLock.js.map +1 -0
  92. package/dist/namedSessionStoppedLock.test.d.ts +2 -0
  93. package/dist/namedSessionStoppedLock.test.d.ts.map +1 -0
  94. package/dist/namedSessionStoppedLock.test.js +104 -0
  95. package/dist/namedSessionStoppedLock.test.js.map +1 -0
  96. package/dist/providerSessionReplyRecovery.d.ts +1 -1
  97. package/dist/relayAutostart.d.ts +3 -0
  98. package/dist/relayAutostart.d.ts.map +1 -1
  99. package/dist/relayAutostart.js +45 -6
  100. package/dist/relayAutostart.js.map +1 -1
  101. package/dist/relayAutostart.test.js +49 -1
  102. package/dist/relayAutostart.test.js.map +1 -1
  103. package/dist/relayRuntimeUpgrade.d.ts +2 -1
  104. package/dist/relayRuntimeUpgrade.d.ts.map +1 -1
  105. package/dist/relayRuntimeUpgrade.js +3 -3
  106. package/dist/relayRuntimeUpgrade.js.map +1 -1
  107. package/dist/relayRuntimeUpgradeCaller.test.js +76 -0
  108. package/dist/relayRuntimeUpgradeCaller.test.js.map +1 -1
  109. package/dist/relaySigningMaintenanceUpgrade.d.ts +44 -0
  110. package/dist/relaySigningMaintenanceUpgrade.d.ts.map +1 -0
  111. package/dist/relaySigningMaintenanceUpgrade.js +249 -0
  112. package/dist/relaySigningMaintenanceUpgrade.js.map +1 -0
  113. package/dist/relaySigningMaintenanceUpgrade.test.d.ts +2 -0
  114. package/dist/relaySigningMaintenanceUpgrade.test.d.ts.map +1 -0
  115. package/dist/relaySigningMaintenanceUpgrade.test.js +230 -0
  116. package/dist/relaySigningMaintenanceUpgrade.test.js.map +1 -0
  117. package/dist/schemas/codex-signing-recovery-native-progress.schema.json +16 -0
  118. package/dist/schemas/device-enrollment-recovery-journal.schema.json +20 -0
  119. package/dist/schemas/device-signing-maintenance-challenge.schema.json +185 -0
  120. package/dist/schemas/device-signing-maintenance-completion.schema.json +98 -0
  121. package/dist/schemas/device-signing-maintenance-context-request.schema.json +68 -0
  122. package/dist/schemas/device-signing-maintenance-context.schema.json +137 -0
  123. package/dist/schemas/device-signing-maintenance-control.schema.json +12 -0
  124. package/dist/schemas/device-signing-maintenance-controller-progress.schema.json +26 -0
  125. package/dist/schemas/device-signing-maintenance-delivery-request.schema.json +45 -0
  126. package/dist/schemas/device-signing-maintenance-host-progress.schema.json +121 -0
  127. package/dist/schemas/device-signing-maintenance-plan.schema.json +136 -0
  128. package/dist/schemas/device-signing-maintenance-preparation.schema.json +56 -0
  129. package/dist/schemas/device-signing-recovery-candidate.schema.json +28 -0
  130. package/dist/schemas/device-signing-recovery-challenge.schema.json +52 -0
  131. package/dist/schemas/device-signing-recovery-completion.schema.json +23 -0
  132. package/dist/schemas/device-signing-recovery-delivery-request.schema.json +16 -0
  133. package/dist/schemas/device-signing-recovery-delivery.schema.json +22 -0
  134. package/dist/schemas/device-signing-recovery-host-progress.schema.json +28 -0
  135. package/dist/schemas/device-signing-recovery-offer-progress.schema.json +32 -0
  136. package/dist/schemas/device-signing-recovery-offer-read-request.schema.json +16 -0
  137. package/dist/schemas/device-signing-recovery-offer-registration-receipt.schema.json +16 -0
  138. package/dist/schemas/device-signing-recovery-offer-registration-request.schema.json +18 -0
  139. package/dist/schemas/device-signing-recovery-offer-response.schema.json +43 -0
  140. package/dist/schemas/device-signing-recovery-plan.schema.json +40 -0
  141. package/dist/schemas/device-signing-recovery-preparation.schema.json +23 -0
  142. package/dist/schemas/local-signing-maintenance-upgrade-journal.schema.json +34 -0
  143. package/dist/schemas/local-signing-maintenance-upgrade.schema.json +33 -0
  144. package/dist/schemas/provider-session-recovery-declaration.schema.json +21 -0
  145. package/dist/signingMaintenanceCohort.test.d.ts +2 -0
  146. package/dist/signingMaintenanceCohort.test.d.ts.map +1 -0
  147. package/dist/signingMaintenanceCohort.test.js +55 -0
  148. package/dist/signingMaintenanceCohort.test.js.map +1 -0
  149. package/dist/usage.d.ts.map +1 -1
  150. package/dist/usage.js +4 -2
  151. package/dist/usage.js.map +1 -1
  152. package/dist/wslRelayHost.d.ts +2 -0
  153. package/dist/wslRelayHost.d.ts.map +1 -1
  154. package/dist/wslRelayHost.js +35 -0
  155. package/dist/wslRelayHost.js.map +1 -1
  156. package/dist/wslRelayHost.test.js +81 -4
  157. package/dist/wslRelayHost.test.js.map +1 -1
  158. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -12,18 +12,22 @@ import { canonicalize, createActionDecisionPublicKeyResolver, sha256, validateCo
12
12
  import { buildTrajectoryCapsule, containsDisallowedLocalPath, redactValue, referencesExcludedPath, trajectoryCapsuleHash } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
13
13
  import { assertPolicy, loadOrganizationPolicy, verifyServerAuthorizedPolicy } from '@dharma-ai-labs/agent-fabric-policy';
14
14
  import { agyAdapter, claudeAdapter, codexAdapter, hermesAdapter, providerAdapters, providerExecutionRecords, providerProcessEnvironment } from '@dharma-ai-labs/agent-fabric-provider-adapters';
15
- import { AgentFabricClient, beginEnrollment, loadDeviceIdentity, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, assertDeviceEnrollmentAnchorAbsent, deleteActiveSkillAuthorizationAnchor, loadActiveSkillAuthorizationAnchor, loadDeviceEnrollmentAnchor, saveActiveSkillAuthorizationAnchor, isDefinitiveAgentFabricRejection, recoverDeviceEnrollmentConsistency, loadOrganizationApiToken, redeemBootstrapGrant, claimSetupReference, setupClaimSourceRegistration, saveDeviceConfig, saveDeviceEnrollmentAnchor, saveOrganizationApiToken, saveDeviceConnectionPreference, } from '@dharma-ai-labs/agent-fabric-relay-client';
15
+ import { AgentFabricClient, beginEnrollment, loadDeviceIdentity, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, assertDeviceEnrollmentAnchorAbsent, deleteActiveSkillAuthorizationAnchor, loadActiveSkillAuthorizationAnchor, loadDeviceEnrollmentAnchor, saveActiveSkillAuthorizationAnchor, isDefinitiveAgentFabricRejection, recoverDeviceEnrollmentConsistency, withDeviceSigningControl, loadOrganizationApiToken, redeemBootstrapGrant, claimSetupReference, setupClaimSourceRegistration, saveDeviceConfig, saveDeviceEnrollmentAnchor, saveOrganizationApiToken, saveDeviceConnectionPreference, } from '@dharma-ai-labs/agent-fabric-relay-client';
16
16
  import { AgentFabricClient as AgentFabricApiClient, } from '@dharma-ai-labs/agent-fabric-sdk';
17
17
  import { contentHash, getActiveSkillBundleAuthorization, getExpiredSkillBundleAuthorizationForReplacement, getLegacySkillBundleIdForUpgrade, installSkillBundle, readVerifiedRepositoryKnowledge, rollbackUnconfirmedSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
18
18
  import { executeTask, FileActionExecutionJournal, FileTaskReceiptStore, providerInstructionsForTask, } from '@dharma-ai-labs/agent-fabric-task-runner';
19
19
  import { CLI_USAGE } from './usage.js';
20
+ import { runDeviceSigningMaintenanceController } from './deviceSigningMaintenanceController.js';
21
+ import { withLegacySigningMaintenanceBoundary } from './legacySigningMaintenanceBoundary.js';
20
22
  import { superviseRelay } from './relaySupervisor.js';
21
23
  import { currentRepositoryRelayFailure, repositoryRelayObservationReady, runRegisteredRepositoryRelays, selectRepositoryRelayRegistrations, serializeRelayWork, waitForRelayRefresh, withRepositoryRelayStage } from './repositoryRelaySupervisor.js';
22
- import { assertRelayStartupOwnership, disableRelayAutostart, enableRelayAutostart, inspectOwnedRelayAutostart, relayAutostartStatus, startRelayAutostart, stopRelayAutostart } from './relayAutostart.js';
24
+ import { assertRelayStartupOwnership, assertRelayAutostartPaused, disableRelayAutostart, enableRelayAutostart, inspectOwnedRelayAutostart, relayAutostartStatus, startRelayAutostart, stopRelayAutostart } from './relayAutostart.js';
23
25
  import { runOwnedContainerEntrypoint, readContainerProcessIdentity } from './containerRelayLifecycle.js';
24
26
  import { readWorkspaceRegistry } from './workspaceRegistry.js';
25
27
  import { appendRecoveredWorkspace, applyRegistryRecoveryFile, inspectRegistryRecoveryFile, resolveRegistryRecoveryProjection } from './workspaceRegistryRecovery.js';
26
28
  import { relayRuntimeObservationReady, upgradeRelayRuntime } from './relayRuntimeUpgrade.js';
29
+ import { reconcileStoppedNamedSessionLock } from './namedSessionStoppedLock.js';
30
+ import { upgradeRelayForSigningMaintenance } from './relaySigningMaintenanceUpgrade.js';
27
31
  import { activateMaintenanceRestart, maintainNamedSession, recoverStoppedNamedSession } from './namedSessionMaintenance.js';
28
32
  import { assertNamedSessionStartupRegistration } from './namedSessionAdmission.js';
29
33
  import { recoverableContainerJournal } from './containerRelayRecovery.js';
@@ -100,6 +104,8 @@ import { requestCodexSetupHostPreparation } from './codexSetupPortalPreparation.
100
104
  import { readCodexSetupPublicStdin } from './codexSetupPublicStdin.js';
101
105
  import { reserveCodexSetupLaunch } from './codexSetupLaunchReservation.js';
102
106
  export { startCodexSetupNativeHost } from './codexSetupNativeHost.js';
107
+ export { attachCodexSigningRecoveryNativeHost } from './codexSigningRecoveryNativeHost.js';
108
+ export { openCodexSigningRecoveryOwner } from './codexBoundSession.js';
103
109
  export { openCodexSetupVaultJournal } from './codexSetupVaultJournal.js';
104
110
  import { createCodexSetupReadinessOwner } from './codexSetupReadiness.js';
105
111
  export { createCodexSetupReadinessOwner } from './codexSetupReadiness.js';
@@ -107,7 +113,8 @@ import { assertCodexSetupExecutionLease } from './codexSetupAdmission.js';
107
113
  import { parseLocalCodexSetupReadiness } from '@dharma-ai-labs/agent-fabric-local-vault/setup-readiness';
108
114
  import { writeBootstrapHostJson, writeBootstrapHostText } from './bootstrapHostFiles.js';
109
115
  export { openCooperativeInboxSession } from './cooperativeInboxSession.js';
110
- const VERSION = '0.2.209';
116
+ import { inspectNamedCodexRecoveryToolDeclaration } from './codexSigningRecoveryTool.js';
117
+ const VERSION = '0.2.211';
111
118
  const USAGE = CLI_USAGE;
112
119
  const execFileAsync = promisify(execFile);
113
120
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -161,10 +168,37 @@ export function portalUrl(flags, fallback = 'https://www.dharma-ai.io') {
161
168
  return String(flags.get('portal-url') || flags.get('hq-url') || fallback);
162
169
  }
163
170
  function print(value) {
164
- process.stdout.write(typeof value === 'string' ? `${value}\n` : `${JSON.stringify(value, null, 2)}\n`);
171
+ process.stdout.write(formatCommandOutput(value));
172
+ }
173
+ export function formatCommandOutput(value) {
174
+ if (typeof value === 'string')
175
+ return value + '\n';
176
+ const streaming = value && typeof value === 'object' && !Array.isArray(value)
177
+ && value.schema === 'dharma.device-signing-maintenance-controller-progress/v1';
178
+ return JSON.stringify(value, null, streaming ? undefined : 2) + '\n';
165
179
  }
166
180
  export function commandExitCode(argv, value) {
167
181
  const { positional, flags } = parseCliOptions(argv);
182
+ if (positional[0] === 'relay' && positional[1] === 'upgrade'
183
+ && (flags.has('stage-signing-maintenance') || flags.has('finish-signing-maintenance'))) {
184
+ if (!value || typeof value !== 'object' || Array.isArray(value))
185
+ return 2;
186
+ const receipt = value;
187
+ if (receipt.schema !== 'dharma.local-signing-maintenance-upgrade/v1' || receipt.ready !== false
188
+ || receipt.nativeContinuationAuthorized !== false)
189
+ return 2;
190
+ return flags.get('apply') !== true ? receipt.state === 'planned' ? 0 : 2
191
+ : flags.has('finish-signing-maintenance') ? receipt.state === 'completed' && receipt.upgradeComplete === true ? 0 : 2
192
+ : receipt.state === 'staged' && receipt.upgradeComplete === false ? 0 : 2;
193
+ }
194
+ if (positional[0] === 'signing-recovery') {
195
+ if (!value || typeof value !== 'object' || Array.isArray(value))
196
+ return 2;
197
+ const receipt = value, progress = receipt.progress;
198
+ return receipt.schema === 'dharma.device-signing-maintenance-controller-progress/v1' && receipt.ok === true
199
+ && receipt.nativeContinuationAuthorized === false && receipt.ready === false
200
+ && (flags.get('apply') === true ? progress?.state === 'trust_installed' && progress.clientTrustInstalled === true : receipt.planned === true) ? 0 : 2;
201
+ }
168
202
  if (positional[0] === 'sessions' && positional[1] === 'readiness' && flags.get('diagnostic') === true) {
169
203
  const diagnosticResult = value && typeof value === 'object' && !Array.isArray(value)
170
204
  ? value : null;
@@ -1044,7 +1078,7 @@ async function pidProcessState(pidPath) {
1044
1078
  const code = error.code;
1045
1079
  if (code === 'ESRCH')
1046
1080
  return 'stopped';
1047
- return code === 'EPERM' ? 'unknown' : 'stopped';
1081
+ return 'unknown';
1048
1082
  }
1049
1083
  }
1050
1084
  export async function relayProcessState(home = dharmaHome()) {
@@ -1053,6 +1087,42 @@ export async function relayProcessState(home = dharmaHome()) {
1053
1087
  export async function relaySupervisorProcessState(home = dharmaHome()) {
1054
1088
  return pidProcessState(resolve(home, 'relay', 'supervisor.pid'));
1055
1089
  }
1090
+ /** A legacy runtime may not participate in the new signing mutex. Require all
1091
+ * registrations belonging to this device disabled and their known owned PID
1092
+ * locks stopped, plus its relay/supervisor, before any maintenance store read.
1093
+ * This check never stops a process or changes startup/registration state. */
1094
+ export async function deviceTrustMaintenanceProcessState(home, name) {
1095
+ const selected = await readNamedSession(home, name);
1096
+ if (!selected)
1097
+ return 'unknown';
1098
+ const states = await Promise.all([relayProcessState(home), relaySupervisorProcessState(home)]);
1099
+ let entries;
1100
+ try {
1101
+ entries = await readdir(resolve(home, 'sessions'));
1102
+ }
1103
+ catch (error) {
1104
+ if (error.code !== 'ENOENT')
1105
+ return 'unknown';
1106
+ entries = [];
1107
+ }
1108
+ for (const entry of entries) {
1109
+ if (!/^[a-z][a-z0-9-]{0,47}$/.test(String(entry)))
1110
+ continue;
1111
+ let registration;
1112
+ try {
1113
+ registration = await readNamedSession(home, String(entry));
1114
+ }
1115
+ catch {
1116
+ return 'unknown';
1117
+ }
1118
+ if (!registration)
1119
+ return 'unknown';
1120
+ if (registration.identity.deviceId !== selected.identity.deviceId || registration.identity.organizationId !== selected.identity.organizationId)
1121
+ continue;
1122
+ states.push(registration.enabled ? 'unknown' : await pidProcessState(resolve(namedSessionPaths(home, String(entry)).root, 'service.lock')));
1123
+ }
1124
+ return states.includes('running') ? 'running' : states.includes('unknown') ? 'unknown' : 'stopped';
1125
+ }
1056
1126
  export async function acquireRelayProcessLease(home = dharmaHome()) {
1057
1127
  const pidPath = resolve(home, 'relay', 'relay.pid');
1058
1128
  const releaseLock = await acquirePidLock(`${pidPath}.lock`, 250, 'Another relay process is already running for this device.');
@@ -1228,6 +1298,35 @@ export async function withRelayStartupMutation(operation, home = dharmaHome()) {
1228
1298
  await release?.();
1229
1299
  }
1230
1300
  }
1301
+ export async function acquireNamedSessionStartupAdmission(home = dharmaHome()) {
1302
+ return acquirePidLock(resolve(home, 'autostart-mutation.lock'), 250, 'named_session_startup_busy');
1303
+ }
1304
+ async function assertSigningMaintenanceManagedCohort(home, workspace) {
1305
+ const startup = await inspectOwnedRelayAutostart({ home });
1306
+ const bin = resolve(workspace, '.dharma', 'bin');
1307
+ if (startup.version !== VERSION || startup.workspace !== workspace
1308
+ || startup.launcher !== resolve(bin, process.platform === 'win32' ? 'dharma.cmd' : 'dharma')
1309
+ || startup.policy !== resolve(workspace, '.dharma', 'approved-policy.json')) {
1310
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1311
+ }
1312
+ const readLauncher = async (name) => {
1313
+ const path = resolve(bin, name), before = await lstat(path);
1314
+ if (!before.isFile() || before.isSymbolicLink() || before.size > 65536 || await realpath(path) !== path) {
1315
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1316
+ }
1317
+ const contents = await readFile(path, 'utf8'), after = await lstat(path);
1318
+ if (after.ino !== before.ino || after.dev !== before.dev || after.size !== before.size
1319
+ || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || after.isSymbolicLink()) {
1320
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1321
+ }
1322
+ return contents;
1323
+ };
1324
+ const launchers = { shell: await readLauncher('dharma'), windows: await readLauncher('dharma.cmd') };
1325
+ if (!await verifyRecordedRepositoryLaunchers(VERSION, launchers)) {
1326
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1327
+ }
1328
+ return sha256(JSON.stringify({ schema: 'dharma.managed-signing-maintenance-cohort/v1', startup, launchers }));
1329
+ }
1231
1330
  export async function withWorkspacePolicyRefreshLock(workspaceId, operation) {
1232
1331
  if (!workspaceId)
1233
1332
  throw new Error('Content transmission requires a registered workspace ID.');
@@ -1929,6 +2028,15 @@ async function installStableRepositoryLauncher(workspace) {
1929
2028
  return { shell: '.dharma/bin/dharma', windows: '.dharma/bin/dharma.cmd' };
1930
2029
  }
1931
2030
  async function relayUpgrade(flags) {
2031
+ const stageMaintenance = flags.has('stage-signing-maintenance'), finishMaintenance = flags.has('finish-signing-maintenance');
2032
+ const maintenance = stageMaintenance || finishMaintenance;
2033
+ if (maintenance && process.platform !== 'linux')
2034
+ throw new Error('relay_signing_maintenance_linux_required');
2035
+ if (stageMaintenance && finishMaintenance || maintenance && flags.has('rollback')
2036
+ || ['stage-signing-maintenance', 'finish-signing-maintenance'].some(key => flags.has(key) && flags.get(key) !== true)
2037
+ || maintenance && ['apply', 'dry-run'].some(key => flags.has(key) && flags.get(key) !== true)
2038
+ || flags.has('previous-upgrade-sha256') && !stageMaintenance)
2039
+ throw new Error('relay_signing_maintenance_options_invalid');
1932
2040
  const config = await readDeviceConfig();
1933
2041
  if (!config)
1934
2042
  throw new Error('relay_upgrade_enrollment_required');
@@ -1972,10 +2080,46 @@ async function relayUpgrade(flags) {
1972
2080
  const npmCliPath = process.platform === 'linux' && !flags.has('rollback') ? await resolveRepositoryNpmCli() : undefined;
1973
2081
  const codexDirectory = !flags.has('rollback')
1974
2082
  ? await resolveRepositoryCodexDirectory({ searchPath: providerProcessEnvironment(process.env).PATH }) : undefined;
1975
- const receipt = await upgradeRelayRuntime({ home, workspace, version: VERSION,
2083
+ const upgrade = maintenance ? upgradeRelayForSigningMaintenance : upgradeRelayRuntime;
2084
+ const receipt = await upgrade({ home, workspace, version: VERSION,
1976
2085
  organizationId: config.organizationId, deviceId: config.deviceId, workspaceId: selected.workspaceId,
1977
- dryRun: !flags.has('apply'), rollback: flags.has('rollback') }, {
2086
+ dryRun: !flags.has('apply'), rollback: flags.has('rollback'),
2087
+ trustGeneration: config.serverSigningKeyset?.generation ?? 0, finish: finishMaintenance,
2088
+ ...(flags.has('previous-upgrade-sha256') ? { previousUpgradeSha256: String(flags.get('previous-upgrade-sha256')) } : {}) }, {
1978
2089
  assertStopped,
2090
+ assertStartupPaused: () => assertRelayAutostartPaused(startupOptions),
2091
+ stageStartup: (version, restore) => enableRelayAutostart({ ...startupOptions, workspace,
2092
+ launcher: startup.launcher, policy: startup.policy, version, restoreCodexHome: restore.codexHome, pauseForSigningMaintenance: true }),
2093
+ assertNamedDisabled: async () => {
2094
+ const entries = await readdir(resolve(home, 'sessions'), { withFileTypes: true });
2095
+ const names = entries.filter(entry => entry.isDirectory() && /^[a-z][a-z0-9-]{0,47}$/.test(entry.name));
2096
+ if (names.length !== 1)
2097
+ throw Error('relay_signing_maintenance_session_scope_unavailable');
2098
+ const registration = await readNamedSession(home, names[0].name);
2099
+ if (!registration || registration.enabled || registration.identity.organizationId !== config.organizationId
2100
+ || registration.identity.deviceId !== config.deviceId || registration.identity.workspaceId !== selected.workspaceId)
2101
+ throw Error('relay_signing_maintenance_session_scope_unavailable');
2102
+ try {
2103
+ await lstat(resolve(home, 'sessions', names[0].name, 'service.lock'));
2104
+ throw Error('relay_signing_maintenance_session_lock_requires_reconciliation');
2105
+ }
2106
+ catch (error) {
2107
+ if (error.code !== 'ENOENT')
2108
+ throw error;
2109
+ }
2110
+ },
2111
+ assertRecoveredTrust: async (previousGeneration) => {
2112
+ const current = await readDeviceConfig(), keyset = current?.serverSigningKeyset;
2113
+ if (!current || current.organizationId !== config.organizationId || current.deviceId !== config.deviceId
2114
+ || current.hqUrl !== config.hqUrl || current.relayUrl !== config.relayUrl || current.enrolledAt !== config.enrolledAt
2115
+ || current.connectionMode !== config.connectionMode
2116
+ || current.installationId !== config.installationId || current.publicKeyEd25519 !== config.publicKeyEd25519
2117
+ || current.setupClaimReference !== config.setupClaimReference || current.setupClaimRepositoryFingerprint !== config.setupClaimRepositoryFingerprint
2118
+ || !keyset || keyset.generation <= previousGeneration || !validateTrustedServerSigningKeysetContract(keyset).ok
2119
+ || !verifyInitialServerSigningKeyset(keyset, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: current.serverPublicKeyEd25519 }, format: 'jwk' }), current.organizationId).ok)
2120
+ throw Error('relay_signing_maintenance_trust_not_recovered');
2121
+ await loadDeviceEnrollmentAnchor({ config: current, readOnly: true });
2122
+ },
1979
2123
  recoverableJournal: startup.backend === 'container-entrypoint' ? recoverableContainerJournal : undefined,
1980
2124
  inspectStartup: async () => {
1981
2125
  const current = await inspectOwnedRelayAutostart(startupOptions);
@@ -2035,7 +2179,8 @@ async function relayUpgrade(flags) {
2035
2179
  throw new Error('relay_upgrade_receiver_unconfirmed');
2036
2180
  },
2037
2181
  });
2038
- const validation = await validateContract(resolve(fileURLToPath(new URL('.', import.meta.url)), 'schemas'), 'https://schemas.dharma-ai.io/local-relay-upgrade/v1', receipt);
2182
+ const validation = await validateContract(resolve(fileURLToPath(new URL('.', import.meta.url)), 'schemas'), maintenance ? 'https://schemas.dharma-ai.io/local-signing-maintenance-upgrade/v1'
2183
+ : 'https://schemas.dharma-ai.io/local-relay-upgrade/v1', receipt);
2039
2184
  if (!validation.ok)
2040
2185
  throw new Error('relay_upgrade_receipt_invalid');
2041
2186
  return receipt;
@@ -5231,6 +5376,11 @@ async function namedSessionCommand(action, flags) {
5231
5376
  const hostScope = currentBootstrapHostScope();
5232
5377
  const setupSessionScope = currentAcceptedSetupSessionScope();
5233
5378
  const supervisorOwner = currentNamedSessionChildOwner();
5379
+ if (action === 'reconcile-stopped-lock' && (hostScope || setupSessionScope || supervisorOwner
5380
+ || [...flags.keys()].some(k => !['name', 'workspace-id', 'registration-sha256', 'lock-sha256', 'apply', 'dry-run', 'json'].includes(k))
5381
+ || ['apply', 'dry-run', 'json'].some(k => flags.has(k) && flags.get(k) !== true)
5382
+ || flags.has('apply') && flags.has('dry-run')))
5383
+ throw Error('named_session_stopped_lock_options_invalid');
5234
5384
  if (hostScope && (supervisorOwner || setupSessionScope))
5235
5385
  throw new Error('named_session_child_owner_conflict');
5236
5386
  if (setupSessionScope && !['start', 'status', 'readiness'].includes(action))
@@ -5246,498 +5396,552 @@ async function namedSessionCommand(action, flags) {
5246
5396
  if (process.platform !== 'linux')
5247
5397
  throw new Error('codex_session_sandbox_unqualified');
5248
5398
  const workspaceId = required(flags, 'workspace-id');
5249
- let childMessage;
5250
- if (flags.has('setup-handoff')) {
5251
- if (action !== 'serve' || flags.get('setup-handoff') !== true
5252
- || [...flags.keys()].some(key => !['name', 'workspace-id', 'apply', 'setup-handoff'].includes(key))) {
5253
- throw new Error('setup_child_unavailable');
5254
- }
5255
- childMessage = await receiveCodexSetupChildStart({ process, signal: new AbortController().signal });
5256
- }
5257
- const item = (await step(() => registry())).find(row => row.workspaceId === workspaceId);
5258
- const config = await step(() => readDeviceConfig());
5259
- if (!item || !config || item.organizationId !== config.organizationId)
5260
- throw new Error('named_session_scope_mismatch');
5261
- await assertBootstrapHostSource(item.path, String(item.repositoryRemoteHash || ''));
5262
- const scope = repositoryRoleScope(item);
5263
- const existing = await step(() => readNamedSession(dharmaHome(), name));
5264
- if (setupSessionScope && (name !== setupSessionScope.request.name || workspaceId !== setupSessionScope.request.workspaceId
5265
- || existing && existing.identity.membershipId !== setupSessionScope.request.membershipId))
5266
- throw new Error('setup_session_scope_changed');
5267
- if (existing && (existing.identity.workspaceId !== item.workspaceId
5268
- || existing.identity.organizationId !== config.organizationId || existing.identity.deviceId !== config.deviceId
5269
- || existing.identity.endpointId !== scope.endpointId || existing.identity.repositoryBindingId !== scope.repositoryBindingId)) {
5270
- throw new Error('named_session_scope_mismatch');
5271
- }
5272
- if (action === 'status') {
5273
- try {
5274
- return await namedSessionRequest(dharmaHome(), name, { action: 'status' });
5275
- }
5276
- catch {
5277
- return { ok: true, name, state: 'stopped', registered: Boolean(existing), enabled: existing?.enabled ?? false,
5278
- bindingId: existing?.bindingId ?? null };
5279
- }
5280
- }
5281
- if (action === 'readiness') {
5282
- if (!existing?.enabled)
5283
- throw new Error('named_session_readiness_unavailable');
5284
- let result;
5285
- try {
5286
- result = await namedSessionRequest(dharmaHome(), name, { action: 'readiness' }, { diagnostic });
5399
+ let startupAdmission;
5400
+ const releaseStartupAdmission = async () => {
5401
+ const release = startupAdmission;
5402
+ startupAdmission = undefined;
5403
+ await release?.();
5404
+ };
5405
+ try {
5406
+ if (['start', 'serve'].includes(action) && flags.has('apply') || action === 'reconcile-stopped-lock') {
5407
+ startupAdmission = await step(() => acquireNamedSessionStartupAdmission());
5287
5408
  }
5288
- catch (error) {
5289
- if (diagnostic && error instanceof NamedSessionTransportFailure)
5290
- return namedSessionReadinessDiagnostic(existing, error);
5291
- throw error;
5409
+ let childMessage;
5410
+ if (flags.has('setup-handoff')) {
5411
+ if (action !== 'serve' || flags.get('setup-handoff') !== true
5412
+ || [...flags.keys()].some(key => !['name', 'workspace-id', 'apply', 'setup-handoff'].includes(key))) {
5413
+ throw new Error('setup_child_unavailable');
5414
+ }
5415
+ childMessage = await receiveCodexSetupChildStart({ process, signal: new AbortController().signal });
5292
5416
  }
5293
- if (Object.keys(result).sort().join(',') !== 'bindingId,deviceId,endpointId,membershipId,name,nativeSkill,ok,organizationId,provider,repositoryBindingId,schema,sessionId,workspaceId'
5294
- || result.schema !== 'dharma.named-session-readiness/v1' || result.name !== name || result.bindingId !== existing.bindingId
5295
- || typeof result.sessionId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$(?![\s\S])/.test(result.sessionId)
5296
- || Object.entries(existing.identity).some(([key, value]) => result[key] !== value)) {
5297
- throw new Error('named_session_readiness_scope_mismatch');
5417
+ const item = (await step(() => registry())).find(row => row.workspaceId === workspaceId);
5418
+ const config = await step(() => readDeviceConfig());
5419
+ if (!item || !config || item.organizationId !== config.organizationId)
5420
+ throw new Error('named_session_scope_mismatch');
5421
+ await assertBootstrapHostSource(item.path, String(item.repositoryRemoteHash || ''));
5422
+ const scope = repositoryRoleScope(item);
5423
+ const existing = await step(() => readNamedSession(dharmaHome(), name));
5424
+ if (setupSessionScope && (name !== setupSessionScope.request.name || workspaceId !== setupSessionScope.request.workspaceId
5425
+ || existing && existing.identity.membershipId !== setupSessionScope.request.membershipId))
5426
+ throw new Error('setup_session_scope_changed');
5427
+ if (existing && (existing.identity.workspaceId !== item.workspaceId
5428
+ || existing.identity.organizationId !== config.organizationId || existing.identity.deviceId !== config.deviceId
5429
+ || existing.identity.endpointId !== scope.endpointId || existing.identity.repositoryBindingId !== scope.repositoryBindingId)) {
5430
+ throw new Error('named_session_scope_mismatch');
5298
5431
  }
5299
- return { ...result, nativeSkill: parseNamedCodexSkillObservation(result.nativeSkill) };
5300
- }
5301
- if (action === 'work')
5302
- return namedSessionRequest(dharmaHome(), name, { action: 'work',
5303
- workId: String(flags.get('work-id') || randomUUID()), prompt: required(flags, 'prompt') });
5304
- if (['quiesce', 'resume'].includes(action) || action === 'stop' && flags.has('drained')) {
5305
- const allowed = ['name', 'workspace-id', 'fence-id', 'apply', 'dry-run', 'json',
5306
- ...(action === 'stop' ? ['drained'] : action === 'resume' ? ['recover-stopped'] : [])];
5307
- if (hostScope || supervisorOwner || setupSessionScope || !existing
5308
- || [...flags.keys()].some(key => !allowed.includes(key))
5309
- || ['apply', 'dry-run', 'drained', 'recover-stopped', 'json'].some(key => flags.has(key) && flags.get(key) !== true)
5310
- || flags.has('apply') && flags.has('dry-run'))
5311
- throw new Error('named_session_maintenance_unavailable');
5312
- const fenceId = required(flags, 'fence-id');
5313
- if (!UUID_PATTERN.test(fenceId))
5314
- throw new Error('named_session_fence_invalid');
5315
- const requestAction = action === 'stop' ? 'stop-drained' : action;
5316
- if (!flags.has('apply'))
5317
- return { ok: true, planned: true, name, workspaceId, action: requestAction, fenceId,
5318
- mode: flags.has('recover-stopped') ? 'stopped_recovery' : 'live' };
5319
- if (action === 'resume' && flags.has('recover-stopped'))
5320
- return withRelayStartupMutation(async () => {
5321
- let vault;
5322
- try {
5323
- return await recoverStoppedNamedSession({ registration: existing, fenceId, workspace: item.path,
5324
- processState: () => pidProcessState(resolve(paths.root, 'service.lock')),
5325
- readRegistration: () => readNamedSession(dharmaHome(), name),
5326
- readBinding: async () => {
5327
- vault = await openBootstrapVault({ root: resolve(dharmaHome(), 'vault') });
5328
- return vault.getProviderSessionBinding(existing.bindingId, existing.identity);
5329
- },
5330
- saveRegistration: value => saveNamedSession(dharmaHome(), value) });
5331
- }
5332
- finally {
5333
- await vault?.close();
5334
- }
5432
+ if (action === 'status') {
5433
+ try {
5434
+ return await namedSessionRequest(dharmaHome(), name, { action: 'status' });
5435
+ }
5436
+ catch {
5437
+ return { ok: true, name, state: 'stopped', registered: Boolean(existing), enabled: existing?.enabled ?? false,
5438
+ bindingId: existing?.bindingId ?? null };
5439
+ }
5440
+ }
5441
+ if (action === 'reconcile-stopped-lock') {
5442
+ if (!existing)
5443
+ throw Error('named_session_stopped_lock_unconfirmed');
5444
+ return await reconcileStoppedNamedSessionLock({ home: dharmaHome(), name, registration: existing,
5445
+ registrationSha256: required(flags, 'registration-sha256'), lockSha256: required(flags, 'lock-sha256'), apply: flags.has('apply') }, {
5446
+ assertTopology: async () => {
5447
+ await assertRelayAutostartPaused({ home: dharmaHome() });
5448
+ if (await relayProcessState(dharmaHome()) !== 'stopped' || await relaySupervisorProcessState(dharmaHome()) !== 'stopped')
5449
+ throw Error('named_session_stopped_lock_unconfirmed');
5450
+ try {
5451
+ await namedSessionRequest(dharmaHome(), name, { action: 'status' }, { diagnostic: true });
5452
+ throw Error('named_session_stopped_lock_unconfirmed');
5453
+ }
5454
+ catch (error) {
5455
+ if (!(error instanceof NamedSessionTransportFailure) || error.diagnostic.phase !== 'connect'
5456
+ || !['ENOENT', 'ECONNREFUSED'].includes(String(error.diagnostic.osCode)))
5457
+ throw Error('named_session_stopped_lock_unconfirmed');
5458
+ }
5459
+ },
5460
+ processState: async (pid) => {
5461
+ try {
5462
+ process.kill(pid, 0);
5463
+ return 'running';
5464
+ }
5465
+ catch (error) {
5466
+ return error.code === 'ESRCH' ? 'stopped' : 'unknown';
5467
+ }
5468
+ },
5335
5469
  });
5336
- return maintainNamedSession({ action: requestAction, fenceId,
5337
- registration: existing, request: body => namedSessionRequest(dharmaHome(), name, body) });
5338
- }
5339
- if (action === 'audit') {
5340
- if (!existing?.enabled || [...flags.keys()].some(key => !['name', 'workspace-id', 'work-id'].includes(key))) {
5341
- throw new Error('named_session_audit_unavailable');
5342
- }
5343
- const workId = required(flags, 'work-id');
5344
- if (!/^[A-Za-z0-9_-]{1,128}$(?![\s\S])/.test(workId))
5345
- throw new Error('named_session_work_invalid');
5346
- const result = await namedSessionRequest(dharmaHome(), name, { action: 'audit', workId });
5347
- if (!(await validateContract(resolve(dirname(fileURLToPath(import.meta.url)), 'schemas'), 'https://schemas.dharma-ai.io/named-session-native-audit/v1', result)).ok
5348
- || result.bindingId !== existing.bindingId || result.workId !== workId
5349
- || Object.entries(existing.identity).some(([key, value]) => result[key] !== value)) {
5350
- throw new Error('named_session_audit_scope_mismatch');
5351
- }
5352
- return result;
5353
- }
5354
- if (action === 'stop') {
5355
- if (!flags.has('apply'))
5356
- return { ok: true, planned: true, name, action: 'stop' };
5357
- const current = await namedSessionRequest(dharmaHome(), name, { action: 'status' }).catch(() => null);
5358
- if (current?.admission) {
5359
- if (!existing || current.bindingId !== existing.bindingId
5360
- || Object.entries(existing.identity).some(([key, value]) => current[key] !== value)) {
5361
- throw new Error('named_session_scope_mismatch');
5362
- }
5363
- // The capable service rejects an unfenced stop without changing its registration.
5364
- return namedSessionRequest(dharmaHome(), name, { action: 'stop' });
5365
- }
5366
- if (existing)
5367
- await saveNamedSession(dharmaHome(), { ...existing, enabled: false });
5368
- try {
5369
- return await namedSessionRequest(dharmaHome(), name, { action: 'stop' });
5370
- }
5371
- catch {
5372
- return { ok: true, name, state: 'stopped', enabled: false };
5373
5470
  }
5374
- }
5375
- if (!['start', 'serve'].includes(action))
5376
- throw new Error('named_session_action_invalid');
5377
- if (!flags.has('apply'))
5378
- return { ok: true, planned: true, name, workspaceId,
5379
- provider: 'codex', localWork: 'workspace_write', peerQuestions: 'read_only', network: 'deny' };
5380
- await step(async () => verifyNamedSessionVisibleSkill(await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path }), await repositorySharedReady(item)));
5381
- if (action === 'start') {
5382
- if (existing && !existing.enabled) {
5383
- if (existing.maintenanceFence) {
5384
- if (hostScope || supervisorOwner || setupSessionScope || !flags.has('fence-id') || flags.get('apply') !== true
5385
- || [...flags.keys()].some(key => !['name', 'workspace-id', 'fence-id', 'apply', 'json'].includes(key))) {
5386
- throw new Error('named_session_quiesced');
5387
- }
5388
- await withRelayStartupMutation(() => activateMaintenanceRestart({ registration: existing, fenceId: required(flags, 'fence-id'),
5389
- processState: () => pidProcessState(resolve(paths.root, 'service.lock')),
5390
- readRegistration: () => readNamedSession(dharmaHome(), name), saveRegistration: value => saveNamedSession(dharmaHome(), value) }));
5471
+ if (action === 'readiness') {
5472
+ if (!existing?.enabled)
5473
+ throw new Error('named_session_readiness_unavailable');
5474
+ let result;
5475
+ try {
5476
+ result = await namedSessionRequest(dharmaHome(), name, { action: 'readiness' }, { diagnostic });
5391
5477
  }
5392
- else {
5393
- if (supervisorOwner)
5394
- throw new Error('named_session_disabled');
5395
- const live = await step(() => namedSessionRequest(dharmaHome(), name, { action: 'status' })).catch(() => null);
5396
- if (live?.admission && live.admission.state !== 'open') {
5397
- throw new Error('named_session_quiesced');
5398
- }
5399
- await step(() => saveNamedSession(dharmaHome(), { ...existing, enabled: true }));
5478
+ catch (error) {
5479
+ if (diagnostic && error instanceof NamedSessionTransportFailure)
5480
+ return namedSessionReadinessDiagnostic(existing, error);
5481
+ throw error;
5400
5482
  }
5483
+ if (Object.keys(result).sort().join(',') !== 'bindingId,deviceId,endpointId,membershipId,name,nativeSkill,ok,organizationId,provider,repositoryBindingId,schema,sessionId,workspaceId'
5484
+ || result.schema !== 'dharma.named-session-readiness/v1' || result.name !== name || result.bindingId !== existing.bindingId
5485
+ || typeof result.sessionId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$(?![\s\S])/.test(result.sessionId)
5486
+ || Object.entries(existing.identity).some(([key, value]) => result[key] !== value)) {
5487
+ throw new Error('named_session_readiness_scope_mismatch');
5488
+ }
5489
+ return { ...result, nativeSkill: parseNamedCodexSkillObservation(result.nativeSkill) };
5490
+ }
5491
+ if (action === 'work')
5492
+ return namedSessionRequest(dharmaHome(), name, { action: 'work',
5493
+ workId: String(flags.get('work-id') || randomUUID()), prompt: required(flags, 'prompt') });
5494
+ if (['quiesce', 'resume'].includes(action) || action === 'stop' && flags.has('drained')) {
5495
+ const allowed = ['name', 'workspace-id', 'fence-id', 'apply', 'dry-run', 'json',
5496
+ ...(action === 'stop' ? ['drained'] : action === 'resume' ? ['recover-stopped'] : [])];
5497
+ if (hostScope || supervisorOwner || setupSessionScope || !existing
5498
+ || [...flags.keys()].some(key => !allowed.includes(key))
5499
+ || ['apply', 'dry-run', 'drained', 'recover-stopped', 'json'].some(key => flags.has(key) && flags.get(key) !== true)
5500
+ || flags.has('apply') && flags.has('dry-run'))
5501
+ throw new Error('named_session_maintenance_unavailable');
5502
+ const fenceId = required(flags, 'fence-id');
5503
+ if (!UUID_PATTERN.test(fenceId))
5504
+ throw new Error('named_session_fence_invalid');
5505
+ const requestAction = action === 'stop' ? 'stop-drained' : action;
5506
+ if (!flags.has('apply'))
5507
+ return { ok: true, planned: true, name, workspaceId, action: requestAction, fenceId,
5508
+ mode: flags.has('recover-stopped') ? 'stopped_recovery' : 'live' };
5509
+ if (action === 'resume' && flags.has('recover-stopped'))
5510
+ return withRelayStartupMutation(async () => {
5511
+ let vault;
5512
+ try {
5513
+ return await recoverStoppedNamedSession({ registration: existing, fenceId, workspace: item.path,
5514
+ processState: () => pidProcessState(resolve(paths.root, 'service.lock')),
5515
+ readRegistration: () => readNamedSession(dharmaHome(), name),
5516
+ readBinding: async () => {
5517
+ vault = await openBootstrapVault({ root: resolve(dharmaHome(), 'vault') });
5518
+ return vault.getProviderSessionBinding(existing.bindingId, existing.identity);
5519
+ },
5520
+ saveRegistration: value => saveNamedSession(dharmaHome(), value) });
5521
+ }
5522
+ finally {
5523
+ await vault?.close();
5524
+ }
5525
+ });
5526
+ return maintainNamedSession({ action: requestAction, fenceId,
5527
+ registration: existing, request: body => namedSessionRequest(dharmaHome(), name, body) });
5528
+ }
5529
+ if (action === 'audit') {
5530
+ if (!existing?.enabled || [...flags.keys()].some(key => !['name', 'workspace-id', 'work-id'].includes(key))) {
5531
+ throw new Error('named_session_audit_unavailable');
5532
+ }
5533
+ const workId = required(flags, 'work-id');
5534
+ if (!/^[A-Za-z0-9_-]{1,128}$(?![\s\S])/.test(workId))
5535
+ throw new Error('named_session_work_invalid');
5536
+ const result = await namedSessionRequest(dharmaHome(), name, { action: 'audit', workId });
5537
+ if (!(await validateContract(resolve(dirname(fileURLToPath(import.meta.url)), 'schemas'), 'https://schemas.dharma-ai.io/named-session-native-audit/v1', result)).ok
5538
+ || result.bindingId !== existing.bindingId || result.workId !== workId
5539
+ || Object.entries(existing.identity).some(([key, value]) => result[key] !== value)) {
5540
+ throw new Error('named_session_audit_scope_mismatch');
5541
+ }
5542
+ return result;
5401
5543
  }
5402
- try {
5403
- return await step(() => namedSessionRequest(dharmaHome(), name, { action: 'status' }));
5404
- }
5405
- catch { /* Start an owned worker below. */ }
5406
- const args = [fileURLToPath(import.meta.url), 'sessions', 'serve', '--name', name,
5407
- '--workspace-id', workspaceId, '--apply'];
5408
- if (setupSessionScope)
5409
- args.push('--setup-handoff');
5410
- for (const key of ['session-budget-cents', 'turn-budget-cents']) {
5411
- if (flags.has(key))
5412
- args.push(`--${key}`, required(flags, key));
5413
- }
5414
- let startupChild;
5415
- try {
5416
- await step(async () => {
5417
- const start = () => {
5418
- const child = spawn(process.execPath, args, { cwd: item.path, detached: !hostScope && !supervisorOwner,
5419
- stdio: setupSessionScope ? ['ignore', 'ignore', 'pipe', 'ipc'] : ['ignore', 'ignore', 'pipe'], env: process.env });
5420
- startupChild = observeNamedSessionStartupChild(child);
5421
- return child;
5422
- };
5423
- if (supervisorOwner) {
5424
- const child = await supervisorOwner.spawn(name, start);
5425
- if (setupSessionScope)
5426
- await sendCodexSetupChildStart({ owner: supervisorOwner, child, scope: setupSessionScope });
5427
- return;
5428
- }
5429
- const child = start();
5430
- if (hostScope)
5431
- captureBootstrapHostChild(hostScope, child);
5432
- else {
5433
- child.on('error', () => { });
5434
- child.unref();
5544
+ if (action === 'stop') {
5545
+ if (!flags.has('apply'))
5546
+ return { ok: true, planned: true, name, action: 'stop' };
5547
+ const current = await namedSessionRequest(dharmaHome(), name, { action: 'status' }).catch(() => null);
5548
+ if (current?.admission) {
5549
+ if (!existing || current.bindingId !== existing.bindingId
5550
+ || Object.entries(existing.identity).some(([key, value]) => current[key] !== value)) {
5551
+ throw new Error('named_session_scope_mismatch');
5435
5552
  }
5436
- });
5437
- return await waitForNamedSessionStartup({ step,
5438
- assertActive: async () => { await hostScope?.assert(); },
5439
- observe: () => namedSessionRequest(dharmaHome(), name, { action: 'status' }),
5440
- readFailure: () => startupChild?.readFailure(),
5441
- maximumWaitMs: setupSessionScope
5442
- ? Math.min(120_000, Date.parse(setupSessionScope.request.expiresAt) - Date.now()) : 120_000 });
5443
- }
5444
- finally {
5445
- startupChild?.dispose();
5446
- }
5447
- }
5448
- await mkdir(paths.root, { recursive: true, mode: 0o700 });
5449
- const vault = await openBootstrapVault({ root: resolve(dharmaHome(), 'vault') })
5450
- .catch(error => { throw namedSessionStartupPhaseError('vault', error); });
5451
- let releaseLock;
5452
- const controller = new AbortController(), stop = () => controller.abort();
5453
- process.once('SIGTERM', stop);
5454
- process.once('SIGINT', stop);
5455
- if (childMessage)
5456
- process.once('disconnect', stop);
5457
- let transport;
5458
- try {
5459
- releaseLock = await acquirePidLock(resolve(paths.root, 'service.lock'), 250, 'named_session_already_running')
5460
- .catch(error => { throw namedSessionStartupPhaseError('lock', error); });
5461
- // A startup poll may predate quiescence. Recheck durable admission after winning the owner lock.
5462
- assertNamedSessionStartupRegistration(existing, await step(() => readNamedSession(dharmaHome(), name)));
5463
- const startProvider = async (childScope) => {
5464
- let startupPhase = 'client';
5553
+ // The capable service rejects an unfenced stop without changing its registration.
5554
+ return namedSessionRequest(dharmaHome(), name, { action: 'stop' });
5555
+ }
5556
+ if (existing)
5557
+ await saveNamedSession(dharmaHome(), { ...existing, enabled: false });
5465
5558
  try {
5466
- const startupStep = (operation) => childScope ? childScope.step(operation) : operation();
5467
- if (childScope && existing && existing.identity.membershipId !== childScope.request.membershipId) {
5468
- throw new Error('setup_child_unavailable');
5469
- }
5470
- const fabric = await startupStep(() => AgentFabricClient.open({ configPath: configPath(), statePath: resolve(paths.root, 'protocol-state.json'),
5471
- hostScope: currentBootstrapHostScope() }));
5472
- if (fabric.config.organizationId !== config.organizationId || fabric.config.deviceId !== config.deviceId
5473
- || fabric.config.publicKeyEd25519 !== config.publicKeyEd25519)
5474
- throw new Error('named_session_trust_scope_mismatch');
5475
- startupPhase = 'trust';
5476
- const trust = createNamedSessionTrust({ configPath: configPath(), identity: fabric.config });
5477
- await startupStep(() => trust.refresh());
5478
- startupPhase = 'session';
5479
- await startupStep(() => fabric.openSession(VERSION));
5480
- startupPhase = 'policy';
5481
- const policyPath = resolve(item.path, '.dharma', 'approved-policy.json');
5482
- const policy = await startupStep(() => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric));
5483
- const writeRoots = policy.tasks.writePaths.filter(path => /^[a-zA-Z0-9_-]+\/\*\*$/.test(path))
5484
- .map(path => path.slice(0, -3));
5485
- if (!writeRoots.length)
5486
- throw new Error('named_session_workspace_write_not_authorized');
5487
- startupPhase = 'registration';
5488
- let registration = existing, pending;
5489
- let ownerReceipt;
5490
- // Resolve authenticated ownership before opening any provider transport.
5491
- if (registration) {
5492
- const response = await startupStep(() => fabric.signedPost('/agent-fabric/provider-sessions', {
5493
- schema: 'dharma.provider-session-registration/v1', action: 'inspect', provider: 'codex', mode: 'bridge_owned',
5494
- workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5495
- bindingId: registration.bindingId, expectedRevision: 0, leaseSeconds: 60,
5496
- }));
5497
- if (!isNamedSessionReconnectReceipt(response, registration.bindingId, registration.identity)) {
5498
- throw new Error('named_session_registration_invalid');
5559
+ return await namedSessionRequest(dharmaHome(), name, { action: 'stop' });
5560
+ }
5561
+ catch {
5562
+ return { ok: true, name, state: 'stopped', enabled: false };
5563
+ }
5564
+ }
5565
+ if (!['start', 'serve'].includes(action))
5566
+ throw new Error('named_session_action_invalid');
5567
+ if (!flags.has('apply'))
5568
+ return { ok: true, planned: true, name, workspaceId,
5569
+ provider: 'codex', localWork: 'workspace_write', peerQuestions: 'read_only', network: 'deny' };
5570
+ await step(async () => verifyNamedSessionVisibleSkill(await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path }), await repositorySharedReady(item)));
5571
+ if (action === 'start') {
5572
+ if (existing && !existing.enabled) {
5573
+ if (existing.maintenanceFence) {
5574
+ if (hostScope || supervisorOwner || setupSessionScope || !flags.has('fence-id') || flags.get('apply') !== true
5575
+ || [...flags.keys()].some(key => !['name', 'workspace-id', 'fence-id', 'apply', 'json'].includes(key))) {
5576
+ throw new Error('named_session_quiesced');
5499
5577
  }
5500
- ownerReceipt = response;
5578
+ await activateMaintenanceRestart({ registration: existing, fenceId: required(flags, 'fence-id'),
5579
+ processState: () => pidProcessState(resolve(paths.root, 'service.lock')),
5580
+ readRegistration: () => readNamedSession(dharmaHome(), name), saveRegistration: value => saveNamedSession(dharmaHome(), value) });
5501
5581
  }
5502
5582
  else {
5503
- const maximumCostCents = boundedInteger(flags.get('session-budget-cents'), 1000, 1, 10000, '--session-budget-cents');
5504
- const maximumTurnCostCents = boundedInteger(flags.get('turn-budget-cents'), 25, 1, maximumCostCents, '--turn-budget-cents');
5505
- const bindingId = randomUUID();
5506
- const response = await startupStep(() => fabric.signedPost('/agent-fabric/provider-sessions', {
5507
- schema: 'dharma.provider-session-registration/v1', action: 'attach', provider: 'codex', mode: 'bridge_owned',
5508
- workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5509
- bindingId, expectedRevision: 0, leaseSeconds: 60,
5510
- }));
5511
- const remote = response.registration;
5512
- if (!remote || !UUID_PATTERN.test(String(remote.membershipId))
5513
- || childScope && remote.membershipId !== childScope.request.membershipId || remote.revision !== 1) {
5514
- throw new Error('named_session_registration_invalid');
5583
+ if (supervisorOwner)
5584
+ throw new Error('named_session_disabled');
5585
+ const live = await step(() => namedSessionRequest(dharmaHome(), name, { action: 'status' })).catch(() => null);
5586
+ if (live?.admission && live.admission.state !== 'open') {
5587
+ throw new Error('named_session_quiesced');
5515
5588
  }
5516
- const identity = { organizationId: config.organizationId, repositoryBindingId: scope.repositoryBindingId,
5517
- workspaceId, endpointId: scope.endpointId, membershipId: String(remote.membershipId), deviceId: config.deviceId,
5518
- provider: 'codex' };
5519
- if (!isNamedSessionOwnerReceipt(response, bindingId, identity))
5520
- throw new Error('named_session_registration_invalid');
5521
- ownerReceipt = response;
5522
- pending = { schema: 'dharma.named-session/v1', name, bindingId, identity,
5523
- maximumCostCents, maximumTurnCostCents, enabled: true };
5589
+ await step(() => saveNamedSession(dharmaHome(), { ...existing, enabled: true }));
5524
5590
  }
5525
- startupPhase = 'environment';
5526
- const environment = namedCodexEnvironment(process.env);
5527
- const privateRoots = [dharmaHome(), resolve(environment.CODEX_HOME || resolve(homedir(), '.codex'))];
5528
- if (environment.XDG_RUNTIME_DIR)
5529
- privateRoots.push(environment.XDG_RUNTIME_DIR);
5530
- if (environment.DBUS_SESSION_BUS_ADDRESS)
5531
- privateRoots.push(environment.DBUS_SESSION_BUS_ADDRESS.slice('unix:path='.length));
5532
- startupPhase = 'filesystem';
5533
- const filesystem = await startupStep(() => namedCodexFilesystem({ environment, workspace: item.path, privateRoots, writeRoots }));
5534
- startupPhase = 'transport';
5535
- await startupStep(async () => {
5536
- const expectedOwner = registration ?? pending;
5537
- const ownerConfirmed = registration
5538
- ? isNamedSessionReconnectReceipt(ownerReceipt, expectedOwner.bindingId, expectedOwner.identity)
5539
- : isNamedSessionOwnerReceipt(ownerReceipt, expectedOwner.bindingId, expectedOwner.identity);
5540
- if (!ownerConfirmed) {
5541
- throw new Error('named_session_registration_invalid');
5591
+ }
5592
+ try {
5593
+ return await step(() => namedSessionRequest(dharmaHome(), name, { action: 'status' }));
5594
+ }
5595
+ catch { /* Start an owned worker below. */ }
5596
+ const args = [fileURLToPath(import.meta.url), 'sessions', 'serve', '--name', name,
5597
+ '--workspace-id', workspaceId, '--apply'];
5598
+ if (setupSessionScope)
5599
+ args.push('--setup-handoff');
5600
+ for (const key of ['session-budget-cents', 'turn-budget-cents']) {
5601
+ if (flags.has(key))
5602
+ args.push(`--${key}`, required(flags, key));
5603
+ }
5604
+ let startupChild;
5605
+ try {
5606
+ // The child takes this same admission lock before private startup. Never
5607
+ // wait for it while holding its parent-side registration/enablement lock.
5608
+ await releaseStartupAdmission();
5609
+ await step(async () => {
5610
+ const start = () => {
5611
+ const child = spawn(process.execPath, args, { cwd: item.path, detached: !hostScope && !supervisorOwner,
5612
+ stdio: setupSessionScope ? ['ignore', 'ignore', 'pipe', 'ipc'] : ['ignore', 'ignore', 'pipe'], env: process.env });
5613
+ startupChild = observeNamedSessionStartupChild(child);
5614
+ return child;
5615
+ };
5616
+ if (supervisorOwner) {
5617
+ const child = await supervisorOwner.spawn(name, start);
5618
+ if (setupSessionScope)
5619
+ await sendCodexSetupChildStart({ owner: supervisorOwner, child, scope: setupSessionScope });
5620
+ return;
5621
+ }
5622
+ const child = start();
5623
+ if (hostScope)
5624
+ captureBootstrapHostChild(hostScope, child);
5625
+ else {
5626
+ child.on('error', () => { });
5627
+ child.unref();
5542
5628
  }
5543
- transport = await openCodexAppServerTransport({ command: filesystem.providerExecutable, cwd: item.path,
5544
- environment: filesystem.environment, experimentalApi: true, toolCallTimeoutMs: 60_000,
5545
- argv: [...namedCodexNodeOutputArguments(), '-c', 'allow_login_shell=false', '-c', 'default_permissions="dharma_bridge"',
5546
- '-c', `permissions.dharma_bridge.filesystem=${filesystem.peer}`,
5547
- '-c', 'permissions.dharma_bridge.network={enabled=false}',
5548
- '-c', `permissions.dharma_work.filesystem=${filesystem.work}`,
5549
- '-c', 'permissions.dharma_work.network={enabled=false}', 'app-server'] });
5550
- return transport;
5551
5629
  });
5552
- startupPhase = 'account';
5553
- const account = await startupStep(() => transport.request('account/read', { refreshToken: false }));
5554
- if (!account.account)
5555
- throw new Error('named_session_provider_authentication_required');
5556
- if (pending) {
5557
- startupPhase = 'thread';
5558
- const threadId = await startupStep(() => startNamedCodexThread(transport, item.path, name));
5559
- startupPhase = 'binding';
5560
- await startupStep(async () => vault.saveProviderSessionBinding({ schema: 'dharma.local-provider-session-binding/v1', ...pending.identity,
5561
- bindingId: pending.bindingId, owner: 'dharma_bridge', sessionId: threadId, workspaceRoot: item.path,
5562
- createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(),
5563
- maximumProviderCostCents: pending.maximumTurnCostCents }));
5564
- registration = pending;
5565
- await startupStep(async () => saveNamedSession(dharmaHome(), registration));
5566
- }
5567
- return { registration: registration, filesystem, fabric, trust, policyPath, writeRoots };
5568
- }
5569
- catch (error) {
5570
- throw namedSessionStartupPhaseError(startupPhase, error);
5630
+ return await waitForNamedSessionStartup({ step,
5631
+ assertActive: async () => { await hostScope?.assert(); },
5632
+ observe: () => namedSessionRequest(dharmaHome(), name, { action: 'status' }),
5633
+ readFailure: () => startupChild?.readFailure(),
5634
+ maximumWaitMs: setupSessionScope
5635
+ ? Math.min(120_000, Date.parse(setupSessionScope.request.expiresAt) - Date.now()) : 120_000 });
5571
5636
  }
5572
- };
5573
- const { registration, filesystem, fabric, trust, policyPath, writeRoots } = childMessage
5574
- ? await runCodexSetupChildStartup({ vault: vault, message: childMessage, name, workspaceId,
5575
- signal: controller.signal, authorize: qualifyCodexSetupChildRequest }, startProvider)
5576
- : await startProvider();
5577
- const authorizeLifetime = async () => {
5578
- const current = await refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric);
5579
- if (!writeRoots.every(root => current.tasks.writePaths.includes(`${root}/**`)))
5580
- return false;
5581
- const response = await fabric.signedPost('/agent-fabric/provider-sessions', {
5582
- schema: 'dharma.provider-session-registration/v1', action: 'inspect', provider: 'codex', mode: 'bridge_owned',
5583
- workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5584
- bindingId: registration.bindingId, expectedRevision: 0, leaseSeconds: 60,
5585
- });
5586
- return isNamedSessionOwnerReceipt(response, registration.bindingId, registration.identity);
5587
- };
5588
- const refreshLifetime = () => renewNamedSessionLifetime({ vault, bindingId: registration.bindingId,
5589
- identity: registration.identity, trust, authorize: authorizeLifetime });
5590
- await refreshLifetime().catch(error => { throw namedSessionStartupPhaseError('lifetime', error); });
5591
- const channelTransport = { async signedPost(route, body) {
5592
- await trust.refresh();
5593
- const response = await fabric.signedPost(route, body);
5594
- await trust.refresh();
5595
- return response;
5596
- } };
5597
- // Fresh server acceptance and this live-owner claim fence peer execution;
5598
- // durable reservations retain cost even when delivery or execution is uncertain.
5599
- const consumed = new Set();
5600
- let taskState = null;
5601
- let taskStateFailure = 'source_state_unavailable';
5602
- const taskSnapshot = async () => {
5603
- const sourceAuthorization = await fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item));
5604
- return inventoryRepositoryPackage({ workspace: item.path, organizationId: item.organizationId,
5605
- workspaceId, repositoryBindingId: item.repositoryBindingId, repositoryAgentId: item.repositoryAgentId, sourceAuthorization });
5606
- };
5607
- const readCurrentRepositoryContext = async (contentMode = 'references', referenceText) => readNamedSessionRepositoryContext({
5608
- installation: await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path }),
5609
- sharedRepositoryReady: await repositorySharedReady(item), scope: repositoryRoleScope(item), workspaceRoot: item.path, contentMode, referenceText,
5610
- loadAuthority: async () => ({ policy: await refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5611
- source: await fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item)) })
5612
- });
5613
- return await runNamedSessionService({ home: dharmaHome(), registration, vault, signal: controller.signal,
5614
- localWriteRoots: writeRoots, additionalFilesystemRules: filesystem.additionalFilesystemRules,
5615
- readPeerRepositoryContext: question => readCurrentRepositoryContext('verified_inline', question.question),
5616
- observeNativeSkill: async () => {
5617
- const installation = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5618
- const native = await observeNamedCodexSkill(transport, { workspace: item.path, installation,
5619
- sharedRepositoryReady: await repositorySharedReady(item) });
5620
- const current = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5621
- if (!current.signedLifecycleReady || current.activeBundleId !== native.bundleId
5622
- || current.activeBundleHash !== native.bundleHash || current.nativeSkillPath !== installation.nativeSkillPath
5623
- || !await repositorySharedReady(item))
5624
- throw new Error('named_session_native_skill_unavailable');
5625
- return native;
5626
- },
5627
- syncTaskExports: () => syncNamedSessionTaskExports({ vault, bindingId: registration.bindingId,
5628
- identity: registration.identity,
5629
- loadPolicy: () => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5630
- send: body => fabric.signedPost('/agent-fabric/codex-task-exports', body) }),
5631
- withActivationBoundary: (operation, work) => withWorkspaceSkillActivationLock(workspaceId, 'codex', async () => {
5632
- await refreshLifetime();
5633
- const skill = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5634
- const sharedReady = await repositorySharedReady(item);
5635
- const packageContent = await readNamedSessionPackageContent(skill, sharedReady);
5636
- const bundleId = packageContent.bundleId;
5637
- taskState = null;
5638
- taskStateFailure = 'source_state_unavailable';
5639
- if (work) {
5640
- const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5641
- if (current.evidence.automaticDisclosure?.mode === 'customer_authorized_content' && skill.activeBundleHash) {
5642
- try {
5643
- taskStateFailure = 'public_context_unavailable';
5644
- const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5645
- if (!binding)
5646
- throw new Error('named_session_binding_unavailable');
5647
- const retained = await readCodexPublicContext(transport, { threadId: binding.sessionId, workspaceRoot: binding.workspaceRoot });
5648
- taskStateFailure = 'runtime_version_unavailable';
5649
- const version = (await execFileAsync('codex', ['--version'], { timeout: 10_000, maxBuffer: 4096,
5650
- env: providerProcessEnvironment(process.env) })).stdout.trim();
5651
- if (!/^codex-cli [0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$/.test(version)) {
5652
- throw new Error('named_session_runtime_version_unavailable');
5653
- }
5654
- const contextContentHash = await vault.putBlob(Buffer.from(retained.bytes), 'named-session-public-context');
5655
- if (contextContentHash !== retained.contextHash)
5656
- throw new Error('named_session_context_integrity_failed');
5657
- taskStateFailure = 'source_state_unavailable';
5658
- const before = await taskSnapshot();
5659
- const sourceContentHash = await vault.putBlob(Buffer.from(serializeRepositoryPackageSnapshot(before)), 'named-session-source-state');
5660
- await vault.putBlob(Buffer.from(JSON.stringify({ workId: work.workId, bindingId: registration.bindingId,
5661
- sourceContentHash, sourceSnapshotHash: before.manifest.snapshotHash, bundleId, bundleHash: skill.activeBundleHash,
5662
- packageContent, contextContentHash, evidenceRevision: current.revision,
5663
- consentReceiptId: current.evidence.automaticDisclosure.consentReceiptId })), 'named-session-source-boundary');
5664
- taskState = { workId: work.workId, before, bundleId, bundleHash: skill.activeBundleHash, packageContent,
5665
- evidenceRevision: current.revision, consentReceiptId: current.evidence.automaticDisclosure.consentReceiptId,
5666
- providerContext: { retainedContextHash: retained.contextHash, contextContentHash,
5667
- runtimeVersion: version.slice('codex-cli '.length), requestedModel: retained.context.configuredModel,
5668
- executedModel: null, replayMode: 'task_level' } };
5637
+ finally {
5638
+ startupChild?.dispose();
5639
+ }
5640
+ }
5641
+ await mkdir(paths.root, { recursive: true, mode: 0o700 });
5642
+ const vault = await openBootstrapVault({ root: resolve(dharmaHome(), 'vault') })
5643
+ .catch(error => { throw namedSessionStartupPhaseError('vault', error); });
5644
+ let releaseLock;
5645
+ const controller = new AbortController(), stop = () => controller.abort();
5646
+ process.once('SIGTERM', stop);
5647
+ process.once('SIGINT', stop);
5648
+ if (childMessage)
5649
+ process.once('disconnect', stop);
5650
+ let transport;
5651
+ try {
5652
+ releaseLock = await acquirePidLock(resolve(paths.root, 'service.lock'), 250, 'named_session_already_running')
5653
+ .catch(error => { throw namedSessionStartupPhaseError('lock', error); });
5654
+ // A startup poll may predate quiescence. Recheck durable admission after winning the owner lock.
5655
+ assertNamedSessionStartupRegistration(existing, await step(() => readNamedSession(dharmaHome(), name)));
5656
+ // Once the per-service lock is held, maintenance cannot acquire its stopped
5657
+ // topology. Release home-wide admission before the long-lived provider loop.
5658
+ await releaseStartupAdmission();
5659
+ const startProvider = async (childScope) => {
5660
+ let startupPhase = 'client';
5661
+ try {
5662
+ const startupStep = (operation) => childScope ? childScope.step(operation) : operation();
5663
+ if (childScope && existing && existing.identity.membershipId !== childScope.request.membershipId) {
5664
+ throw new Error('setup_child_unavailable');
5665
+ }
5666
+ const fabric = await startupStep(() => AgentFabricClient.open({ configPath: configPath(), statePath: resolve(paths.root, 'protocol-state.json'),
5667
+ hostScope: currentBootstrapHostScope() }));
5668
+ if (fabric.config.organizationId !== config.organizationId || fabric.config.deviceId !== config.deviceId
5669
+ || fabric.config.publicKeyEd25519 !== config.publicKeyEd25519)
5670
+ throw new Error('named_session_trust_scope_mismatch');
5671
+ startupPhase = 'trust';
5672
+ const trust = createNamedSessionTrust({ configPath: configPath(), identity: fabric.config });
5673
+ await startupStep(() => trust.refresh());
5674
+ startupPhase = 'session';
5675
+ await startupStep(() => fabric.openSession(VERSION));
5676
+ startupPhase = 'policy';
5677
+ const policyPath = resolve(item.path, '.dharma', 'approved-policy.json');
5678
+ const policy = await startupStep(() => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric));
5679
+ const writeRoots = policy.tasks.writePaths.filter(path => /^[a-zA-Z0-9_-]+\/\*\*$/.test(path))
5680
+ .map(path => path.slice(0, -3));
5681
+ if (!writeRoots.length)
5682
+ throw new Error('named_session_workspace_write_not_authorized');
5683
+ startupPhase = 'registration';
5684
+ let registration = existing, pending;
5685
+ let ownerReceipt;
5686
+ // Resolve authenticated ownership before opening any provider transport.
5687
+ if (registration) {
5688
+ const response = await startupStep(() => fabric.signedPost('/agent-fabric/provider-sessions', {
5689
+ schema: 'dharma.provider-session-registration/v1', action: 'inspect', provider: 'codex', mode: 'bridge_owned',
5690
+ workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5691
+ bindingId: registration.bindingId, expectedRevision: 0, leaseSeconds: 60,
5692
+ }));
5693
+ if (!isNamedSessionReconnectReceipt(response, registration.bindingId, registration.identity)) {
5694
+ throw new Error('named_session_registration_invalid');
5669
5695
  }
5670
- catch { /* Source capture failure must not replay or rewrite a coding task. */ }
5696
+ ownerReceipt = response;
5671
5697
  }
5698
+ else {
5699
+ const maximumCostCents = boundedInteger(flags.get('session-budget-cents'), 1000, 1, 10000, '--session-budget-cents');
5700
+ const maximumTurnCostCents = boundedInteger(flags.get('turn-budget-cents'), 25, 1, maximumCostCents, '--turn-budget-cents');
5701
+ const bindingId = randomUUID();
5702
+ const response = await startupStep(() => fabric.signedPost('/agent-fabric/provider-sessions', {
5703
+ schema: 'dharma.provider-session-registration/v1', action: 'attach', provider: 'codex', mode: 'bridge_owned',
5704
+ workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5705
+ bindingId, expectedRevision: 0, leaseSeconds: 60,
5706
+ }));
5707
+ const remote = response.registration;
5708
+ if (!remote || !UUID_PATTERN.test(String(remote.membershipId))
5709
+ || childScope && remote.membershipId !== childScope.request.membershipId || remote.revision !== 1) {
5710
+ throw new Error('named_session_registration_invalid');
5711
+ }
5712
+ const identity = { organizationId: config.organizationId, repositoryBindingId: scope.repositoryBindingId,
5713
+ workspaceId, endpointId: scope.endpointId, membershipId: String(remote.membershipId), deviceId: config.deviceId,
5714
+ provider: 'codex' };
5715
+ if (!isNamedSessionOwnerReceipt(response, bindingId, identity))
5716
+ throw new Error('named_session_registration_invalid');
5717
+ ownerReceipt = response;
5718
+ pending = { schema: 'dharma.named-session/v1', name, bindingId, identity,
5719
+ maximumCostCents, maximumTurnCostCents, enabled: true };
5720
+ }
5721
+ startupPhase = 'environment';
5722
+ const environment = namedCodexEnvironment(process.env);
5723
+ const privateRoots = [dharmaHome(), resolve(environment.CODEX_HOME || resolve(homedir(), '.codex'))];
5724
+ if (environment.XDG_RUNTIME_DIR)
5725
+ privateRoots.push(environment.XDG_RUNTIME_DIR);
5726
+ if (environment.DBUS_SESSION_BUS_ADDRESS)
5727
+ privateRoots.push(environment.DBUS_SESSION_BUS_ADDRESS.slice('unix:path='.length));
5728
+ startupPhase = 'filesystem';
5729
+ const filesystem = await startupStep(() => namedCodexFilesystem({ environment, workspace: item.path, privateRoots, writeRoots }));
5730
+ startupPhase = 'transport';
5731
+ await startupStep(async () => {
5732
+ const expectedOwner = registration ?? pending;
5733
+ const ownerConfirmed = registration
5734
+ ? isNamedSessionReconnectReceipt(ownerReceipt, expectedOwner.bindingId, expectedOwner.identity)
5735
+ : isNamedSessionOwnerReceipt(ownerReceipt, expectedOwner.bindingId, expectedOwner.identity);
5736
+ if (!ownerConfirmed) {
5737
+ throw new Error('named_session_registration_invalid');
5738
+ }
5739
+ transport = await openCodexAppServerTransport({ command: filesystem.providerExecutable, cwd: item.path,
5740
+ environment: filesystem.environment, experimentalApi: true, toolCallTimeoutMs: 60_000,
5741
+ argv: [...namedCodexNodeOutputArguments(), '-c', 'allow_login_shell=false', '-c', 'default_permissions="dharma_bridge"',
5742
+ '-c', `permissions.dharma_bridge.filesystem=${filesystem.peer}`,
5743
+ '-c', 'permissions.dharma_bridge.network={enabled=false}',
5744
+ '-c', `permissions.dharma_work.filesystem=${filesystem.work}`,
5745
+ '-c', 'permissions.dharma_work.network={enabled=false}', 'app-server'] });
5746
+ return transport;
5747
+ });
5748
+ startupPhase = 'account';
5749
+ const account = await startupStep(() => transport.request('account/read', { refreshToken: false }));
5750
+ if (!account.account)
5751
+ throw new Error('named_session_provider_authentication_required');
5752
+ if (pending) {
5753
+ startupPhase = 'thread';
5754
+ const threadId = await startupStep(() => startNamedCodexThread(transport, item.path, name));
5755
+ const declaredRecoveryToolHash = inspectNamedCodexRecoveryToolDeclaration(transport, threadId);
5756
+ if (!declaredRecoveryToolHash)
5757
+ throw new Error('named_session_thread_invalid');
5758
+ startupPhase = 'binding';
5759
+ await startupStep(async () => vault.saveProviderSessionBinding({ schema: 'dharma.local-provider-session-binding/v1', ...pending.identity,
5760
+ bindingId: pending.bindingId, owner: 'dharma_bridge', sessionId: threadId, workspaceRoot: item.path,
5761
+ createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(),
5762
+ maximumProviderCostCents: pending.maximumTurnCostCents }));
5763
+ await startupStep(async () => vault.saveProviderSessionRecoveryDeclaration(pending.bindingId, pending.identity, declaredRecoveryToolHash));
5764
+ registration = pending;
5765
+ await startupStep(async () => saveNamedSession(dharmaHome(), registration));
5766
+ }
5767
+ return { registration: registration, filesystem, fabric, trust, policyPath, writeRoots };
5672
5768
  }
5673
- try {
5674
- const context = work ? await readCurrentRepositoryContext() : undefined;
5675
- return await operation(context);
5676
- }
5677
- finally {
5678
- taskState = null;
5679
- }
5680
- }),
5681
- retainRepositoryState: async (capture) => {
5682
- const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5683
- if (current.evidence.automaticDisclosure?.mode !== 'customer_authorized_content') {
5684
- return { state: 'not_authorized', acceptedLearningObservation: false };
5685
- }
5686
- if (!taskState)
5687
- return { state: 'blocked', code: taskStateFailure, acceptedLearningObservation: false };
5688
- if (current.revision !== taskState.evidenceRevision
5689
- || current.evidence.automaticDisclosure.consentReceiptId !== taskState.consentReceiptId) {
5690
- throw new Error('named_session_repository_state_consent_changed');
5769
+ catch (error) {
5770
+ throw namedSessionStartupPhaseError(startupPhase, error);
5691
5771
  }
5692
- const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5693
- if (!binding)
5694
- throw new Error('named_session_binding_unavailable');
5695
- const receipt = await retainNamedSessionRepositoryState({ vault, binding, capture, workId: taskState.workId,
5696
- before: taskState.before, after: await taskSnapshot(), activeBundleId: taskState.bundleId, activeBundleHash: taskState.bundleHash,
5697
- taskExport: { policy: current, contextBytes: await vault.getBlob(taskState.providerContext.contextContentHash),
5698
- contextHash: taskState.providerContext.retainedContextHash } });
5699
- return { ...receipt, providerContext: taskState.providerContext,
5700
- packageContent: { manifestHash: taskState.packageContent.manifestHash,
5701
- catalogHash: taskState.packageContent.catalogHash, skillsHash: taskState.packageContent.skillsHash } };
5702
- },
5703
- authorizeLocalWork: async () => {
5772
+ };
5773
+ const { registration, filesystem, fabric, trust, policyPath, writeRoots } = childMessage
5774
+ ? await runCodexSetupChildStartup({ vault: vault, message: childMessage, name, workspaceId,
5775
+ signal: controller.signal, authorize: qualifyCodexSetupChildRequest }, startProvider)
5776
+ : await startProvider();
5777
+ const authorizeLifetime = async () => {
5704
5778
  const current = await refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric);
5705
- return writeRoots.every(root => current.tasks.writePaths.includes(`${root}/**`));
5706
- },
5707
- queueEvidence: async (capture) => {
5708
- const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5709
- const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5710
- if (!binding)
5711
- throw new Error('named_session_binding_unavailable');
5712
- // The existing relay rechecks current authority, disclosure limits and device binding before upload.
5713
- return queueNamedSessionEvidence({ vault, capture, binding, policy: current });
5714
- },
5715
- openTransport: async () => transport, channelTransport,
5716
- verifier: { resolvePublicKey: trust.resolvePublicKey, consume: async (id) => { if (consumed.has(id))
5717
- return false; consumed.add(id); return true; } },
5718
- authorizeContent: createNamedPeerContentAuthorization({ scope: repositoryRoleScope(item), diagnostics: true,
5719
- loadCurrentPolicy: () => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5720
- loadCurrentSourceAuthorization: () => fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item)) }) })
5721
- .catch(error => { throw namedSessionStartupPhaseError('service', error); });
5722
- }
5723
- finally {
5724
- try {
5725
- if (transport)
5726
- await transport.close();
5779
+ if (!writeRoots.every(root => current.tasks.writePaths.includes(`${root}/**`)))
5780
+ return false;
5781
+ const response = await fabric.signedPost('/agent-fabric/provider-sessions', {
5782
+ schema: 'dharma.provider-session-registration/v1', action: 'inspect', provider: 'codex', mode: 'bridge_owned',
5783
+ workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5784
+ bindingId: registration.bindingId, expectedRevision: 0, leaseSeconds: 60,
5785
+ });
5786
+ return isNamedSessionOwnerReceipt(response, registration.bindingId, registration.identity);
5787
+ };
5788
+ const refreshLifetime = () => renewNamedSessionLifetime({ vault, bindingId: registration.bindingId,
5789
+ identity: registration.identity, trust, authorize: authorizeLifetime });
5790
+ await refreshLifetime().catch(error => { throw namedSessionStartupPhaseError('lifetime', error); });
5791
+ const channelTransport = { async signedPost(route, body) {
5792
+ await trust.refresh();
5793
+ const response = await fabric.signedPost(route, body);
5794
+ await trust.refresh();
5795
+ return response;
5796
+ } };
5797
+ // Fresh server acceptance and this live-owner claim fence peer execution;
5798
+ // durable reservations retain cost even when delivery or execution is uncertain.
5799
+ const consumed = new Set();
5800
+ let taskState = null;
5801
+ let taskStateFailure = 'source_state_unavailable';
5802
+ const taskSnapshot = async () => {
5803
+ const sourceAuthorization = await fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item));
5804
+ return inventoryRepositoryPackage({ workspace: item.path, organizationId: item.organizationId,
5805
+ workspaceId, repositoryBindingId: item.repositoryBindingId, repositoryAgentId: item.repositoryAgentId, sourceAuthorization });
5806
+ };
5807
+ const readCurrentRepositoryContext = async (contentMode = 'references', referenceText) => readNamedSessionRepositoryContext({
5808
+ installation: await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path }),
5809
+ sharedRepositoryReady: await repositorySharedReady(item), scope: repositoryRoleScope(item), workspaceRoot: item.path, contentMode, referenceText,
5810
+ loadAuthority: async () => ({ policy: await refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5811
+ source: await fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item)) })
5812
+ });
5813
+ return await runNamedSessionService({ home: dharmaHome(), registration, vault, signal: controller.signal,
5814
+ localWriteRoots: writeRoots, additionalFilesystemRules: filesystem.additionalFilesystemRules,
5815
+ readPeerRepositoryContext: question => readCurrentRepositoryContext('verified_inline', question.question),
5816
+ observeNativeSkill: async () => {
5817
+ const installation = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5818
+ const native = await observeNamedCodexSkill(transport, { workspace: item.path, installation,
5819
+ sharedRepositoryReady: await repositorySharedReady(item) });
5820
+ const current = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5821
+ if (!current.signedLifecycleReady || current.activeBundleId !== native.bundleId
5822
+ || current.activeBundleHash !== native.bundleHash || current.nativeSkillPath !== installation.nativeSkillPath
5823
+ || !await repositorySharedReady(item))
5824
+ throw new Error('named_session_native_skill_unavailable');
5825
+ return native;
5826
+ },
5827
+ syncTaskExports: () => syncNamedSessionTaskExports({ vault, bindingId: registration.bindingId,
5828
+ identity: registration.identity,
5829
+ loadPolicy: () => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5830
+ send: body => fabric.signedPost('/agent-fabric/codex-task-exports', body) }),
5831
+ withActivationBoundary: (operation, work) => withDeviceSigningControl({ config: fabric.config }, () => withWorkspaceSkillActivationLock(workspaceId, 'codex', async () => {
5832
+ await refreshLifetime();
5833
+ const skill = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5834
+ const sharedReady = await repositorySharedReady(item);
5835
+ const packageContent = await readNamedSessionPackageContent(skill, sharedReady);
5836
+ const bundleId = packageContent.bundleId;
5837
+ taskState = null;
5838
+ taskStateFailure = 'source_state_unavailable';
5839
+ if (work) {
5840
+ const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5841
+ if (current.evidence.automaticDisclosure?.mode === 'customer_authorized_content' && skill.activeBundleHash) {
5842
+ try {
5843
+ taskStateFailure = 'public_context_unavailable';
5844
+ const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5845
+ if (!binding)
5846
+ throw new Error('named_session_binding_unavailable');
5847
+ const retained = await readCodexPublicContext(transport, { threadId: binding.sessionId, workspaceRoot: binding.workspaceRoot });
5848
+ taskStateFailure = 'runtime_version_unavailable';
5849
+ const version = (await execFileAsync('codex', ['--version'], { timeout: 10_000, maxBuffer: 4096,
5850
+ env: providerProcessEnvironment(process.env) })).stdout.trim();
5851
+ if (!/^codex-cli [0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$/.test(version)) {
5852
+ throw new Error('named_session_runtime_version_unavailable');
5853
+ }
5854
+ const contextContentHash = await vault.putBlob(Buffer.from(retained.bytes), 'named-session-public-context');
5855
+ if (contextContentHash !== retained.contextHash)
5856
+ throw new Error('named_session_context_integrity_failed');
5857
+ taskStateFailure = 'source_state_unavailable';
5858
+ const before = await taskSnapshot();
5859
+ const sourceContentHash = await vault.putBlob(Buffer.from(serializeRepositoryPackageSnapshot(before)), 'named-session-source-state');
5860
+ await vault.putBlob(Buffer.from(JSON.stringify({ workId: work.workId, bindingId: registration.bindingId,
5861
+ sourceContentHash, sourceSnapshotHash: before.manifest.snapshotHash, bundleId, bundleHash: skill.activeBundleHash,
5862
+ packageContent, contextContentHash, evidenceRevision: current.revision,
5863
+ consentReceiptId: current.evidence.automaticDisclosure.consentReceiptId })), 'named-session-source-boundary');
5864
+ taskState = { workId: work.workId, before, bundleId, bundleHash: skill.activeBundleHash, packageContent,
5865
+ evidenceRevision: current.revision, consentReceiptId: current.evidence.automaticDisclosure.consentReceiptId,
5866
+ providerContext: { retainedContextHash: retained.contextHash, contextContentHash,
5867
+ runtimeVersion: version.slice('codex-cli '.length), requestedModel: retained.context.configuredModel,
5868
+ executedModel: null, replayMode: 'task_level' } };
5869
+ }
5870
+ catch { /* Source capture failure must not replay or rewrite a coding task. */ }
5871
+ }
5872
+ }
5873
+ try {
5874
+ const context = work ? await readCurrentRepositoryContext() : undefined;
5875
+ return await operation(context);
5876
+ }
5877
+ finally {
5878
+ taskState = null;
5879
+ }
5880
+ })),
5881
+ retainRepositoryState: async (capture) => {
5882
+ const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5883
+ if (current.evidence.automaticDisclosure?.mode !== 'customer_authorized_content') {
5884
+ return { state: 'not_authorized', acceptedLearningObservation: false };
5885
+ }
5886
+ if (!taskState)
5887
+ return { state: 'blocked', code: taskStateFailure, acceptedLearningObservation: false };
5888
+ if (current.revision !== taskState.evidenceRevision
5889
+ || current.evidence.automaticDisclosure.consentReceiptId !== taskState.consentReceiptId) {
5890
+ throw new Error('named_session_repository_state_consent_changed');
5891
+ }
5892
+ const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5893
+ if (!binding)
5894
+ throw new Error('named_session_binding_unavailable');
5895
+ const receipt = await retainNamedSessionRepositoryState({ vault, binding, capture, workId: taskState.workId,
5896
+ before: taskState.before, after: await taskSnapshot(), activeBundleId: taskState.bundleId, activeBundleHash: taskState.bundleHash,
5897
+ taskExport: { policy: current, contextBytes: await vault.getBlob(taskState.providerContext.contextContentHash),
5898
+ contextHash: taskState.providerContext.retainedContextHash } });
5899
+ return { ...receipt, providerContext: taskState.providerContext,
5900
+ packageContent: { manifestHash: taskState.packageContent.manifestHash,
5901
+ catalogHash: taskState.packageContent.catalogHash, skillsHash: taskState.packageContent.skillsHash } };
5902
+ },
5903
+ authorizeLocalWork: async () => {
5904
+ const current = await refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric);
5905
+ return writeRoots.every(root => current.tasks.writePaths.includes(`${root}/**`));
5906
+ },
5907
+ queueEvidence: async (capture) => {
5908
+ const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5909
+ const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5910
+ if (!binding)
5911
+ throw new Error('named_session_binding_unavailable');
5912
+ // The existing relay rechecks current authority, disclosure limits and device binding before upload.
5913
+ return queueNamedSessionEvidence({ vault, capture, binding, policy: current });
5914
+ },
5915
+ openTransport: async () => transport, channelTransport,
5916
+ verifier: { resolvePublicKey: trust.resolvePublicKey, consume: async (id) => { if (consumed.has(id))
5917
+ return false; consumed.add(id); return true; } },
5918
+ authorizeContent: createNamedPeerContentAuthorization({ scope: repositoryRoleScope(item), diagnostics: true,
5919
+ loadCurrentPolicy: () => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5920
+ loadCurrentSourceAuthorization: () => fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item)) }) })
5921
+ .catch(error => { throw namedSessionStartupPhaseError('service', error); });
5727
5922
  }
5728
5923
  finally {
5729
5924
  try {
5730
- await vault.close();
5925
+ if (transport)
5926
+ await transport.close();
5731
5927
  }
5732
5928
  finally {
5733
- await releaseLock?.();
5734
- process.removeListener('SIGTERM', stop);
5735
- process.removeListener('SIGINT', stop);
5736
- if (childMessage)
5737
- process.removeListener('disconnect', stop);
5929
+ try {
5930
+ await vault.close();
5931
+ }
5932
+ finally {
5933
+ await releaseLock?.();
5934
+ process.removeListener('SIGTERM', stop);
5935
+ process.removeListener('SIGINT', stop);
5936
+ if (childMessage)
5937
+ process.removeListener('disconnect', stop);
5938
+ }
5738
5939
  }
5739
5940
  }
5740
5941
  }
5942
+ finally {
5943
+ await releaseStartupAdmission();
5944
+ }
5741
5945
  }
5742
5946
  async function workspaceSync(flags, positional) {
5743
5947
  const workspaceId = positional[0] || required(flags, 'workspace-id');
@@ -9162,6 +9366,40 @@ export async function run(argv) {
9162
9366
  return USAGE;
9163
9367
  if (flags.has('version') || command === 'version')
9164
9368
  return { version: VERSION };
9369
+ if (command === 'signing-recovery') {
9370
+ if (positional.length !== 1 || [...repeated.values()].some(values => values.length !== 1)
9371
+ || [...flags.keys()].some(key => !['name', 'workspace-id', 'operation-id', 'apply', 'json'].includes(key))
9372
+ || flags.has('apply') && flags.get('apply') !== true)
9373
+ throw Error('device_signing_maintenance_options_invalid');
9374
+ const home = dharmaHome(), name = required(flags, 'name'), workspaceId = required(flags, 'workspace-id');
9375
+ const controller = new AbortController(), stop = () => controller.abort();
9376
+ process.once('SIGTERM', stop);
9377
+ process.once('SIGINT', stop);
9378
+ const execute = (legacyBoundary) => runDeviceSigningMaintenanceController({ home, configPath: configPath(), name, workspaceId,
9379
+ operationId: typeof flags.get('operation-id') === 'string' ? String(flags.get('operation-id')) : undefined, apply: flags.get('apply') === true,
9380
+ readWorkspace: async () => {
9381
+ const selected = (await registry()).find(value => value.workspaceId === workspaceId);
9382
+ return selected && selected.repositoryBindingId && selected.repositoryRemoteHash ? { organizationId: selected.organizationId, workspaceId: selected.workspaceId,
9383
+ repositoryBindingId: selected.repositoryBindingId, repositoryRemoteHash: selected.repositoryRemoteHash, path: selected.path } : null;
9384
+ }, processState: legacyBoundary ? async () => { await legacyBoundary.assertHeld(); return 'stopped'; } : () => deviceTrustMaintenanceProcessState(home, name),
9385
+ legacyBoundary,
9386
+ openVault: () => openBootstrapVault({ root: resolve(home, 'vault') }), signal: controller.signal, frames: process.stdin,
9387
+ emit: value => process.stdout.write(JSON.stringify(value) + '\n') });
9388
+ try {
9389
+ return await (flags.get('apply') === true
9390
+ ? withLegacySigningMaintenanceBoundary({ home, name, signal: controller.signal, assertManagedCohort: async () => {
9391
+ const selected = (await registry()).find(value => value.workspaceId === workspaceId);
9392
+ if (!selected)
9393
+ throw Error('device_signing_maintenance_scope_unavailable');
9394
+ return assertSigningMaintenanceManagedCohort(home, selected.path);
9395
+ } }, execute) : execute());
9396
+ }
9397
+ finally {
9398
+ controller.abort();
9399
+ process.removeListener('SIGTERM', stop);
9400
+ process.removeListener('SIGINT', stop);
9401
+ }
9402
+ }
9165
9403
  if (command === 'demo' && ['connect', 'status', 'role', 'peers', 'ask', 'reply', 'inbox', 'ack', 'resume',
9166
9404
  'package', 'package-status', 'watch', 'watch-enable', 'watch-status', 'watch-disable',
9167
9405
  'signing-client-proof', 'signing-owner-proof', 'transport-connect']
@@ -9506,9 +9744,24 @@ export async function run(argv) {
9506
9744
  if (command === 'relay' && subcommand === 'autostart') {
9507
9745
  if (positional[2] === 'status')
9508
9746
  return relayAutostartStatus({ home: dharmaHome() });
9747
+ if (positional[2] === 'pause') {
9748
+ if ([...flags.keys()].some(k => !['apply', 'dry-run', 'json'].includes(k))
9749
+ || [...flags.values()].some(v => v !== true) || flags.has('apply') && flags.has('dry-run'))
9750
+ throw Error('relay_pause_options_invalid');
9751
+ return withRelayStartupMutation(async () => {
9752
+ const registration = await inspectOwnedRelayAutostart({ home: dharmaHome() });
9753
+ if (registration.backend !== 'container-entrypoint')
9754
+ throw Error('relay_pause_container_required');
9755
+ if (!flags.has('apply'))
9756
+ return { ok: true, planned: true, state: 'pause_planned', backend: registration.backend };
9757
+ await stopRelayAutostart({ home: dharmaHome() });
9758
+ await assertRelayAutostartPaused({ home: dharmaHome() });
9759
+ return { ok: true, planned: false, state: 'paused', backend: registration.backend, registrationPreserved: true };
9760
+ });
9761
+ }
9509
9762
  if (positional[2] === 'disable')
9510
9763
  return withRelayStartupMutation(() => disableRelayAutostart({ home: dharmaHome() }));
9511
- throw new Error('Use dharma relay autostart status or dharma relay autostart disable.');
9764
+ throw new Error('Use dharma relay autostart status, pause or disable.');
9512
9765
  }
9513
9766
  if (command === 'relay' && subcommand === 'supervise')
9514
9767
  return relaySupervise(flags);