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

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 (112) 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 +1 -1
  40. package/dist/cooperativeInboxSession.d.ts +1 -1
  41. package/dist/demoPackage.d.ts +1 -1
  42. package/dist/demoWatchControl.d.ts +6 -6
  43. package/dist/deviceSigningMaintenanceController.d.ts +35 -0
  44. package/dist/deviceSigningMaintenanceController.d.ts.map +1 -0
  45. package/dist/deviceSigningMaintenanceController.js +197 -0
  46. package/dist/deviceSigningMaintenanceController.js.map +1 -0
  47. package/dist/deviceSigningMaintenanceController.test.d.ts +2 -0
  48. package/dist/deviceSigningMaintenanceController.test.d.ts.map +1 -0
  49. package/dist/deviceSigningMaintenanceController.test.js +67 -0
  50. package/dist/deviceSigningMaintenanceController.test.js.map +1 -0
  51. package/dist/deviceSigningMaintenanceFlow.test.d.ts +2 -0
  52. package/dist/deviceSigningMaintenanceFlow.test.d.ts.map +1 -0
  53. package/dist/deviceSigningMaintenanceFlow.test.js +196 -0
  54. package/dist/deviceSigningMaintenanceFlow.test.js.map +1 -0
  55. package/dist/index.d.ts +9 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +609 -466
  58. package/dist/index.js.map +1 -1
  59. package/dist/legacySigningMaintenanceBoundary.d.ts +14 -0
  60. package/dist/legacySigningMaintenanceBoundary.d.ts.map +1 -0
  61. package/dist/legacySigningMaintenanceBoundary.js +128 -0
  62. package/dist/legacySigningMaintenanceBoundary.js.map +1 -0
  63. package/dist/legacySigningMaintenanceBoundary.test.d.ts +2 -0
  64. package/dist/legacySigningMaintenanceBoundary.test.d.ts.map +1 -0
  65. package/dist/legacySigningMaintenanceBoundary.test.js +109 -0
  66. package/dist/legacySigningMaintenanceBoundary.test.js.map +1 -0
  67. package/dist/namedCodexThread.d.ts.map +1 -1
  68. package/dist/namedCodexThread.js +3 -1
  69. package/dist/namedCodexThread.js.map +1 -1
  70. package/dist/namedCodexThread.test.js +17 -1
  71. package/dist/namedCodexThread.test.js.map +1 -1
  72. package/dist/namedSessionMaintenance.d.ts +1 -1
  73. package/dist/namedSessionStartupFence.test.d.ts +2 -0
  74. package/dist/namedSessionStartupFence.test.d.ts.map +1 -0
  75. package/dist/namedSessionStartupFence.test.js +81 -0
  76. package/dist/namedSessionStartupFence.test.js.map +1 -0
  77. package/dist/providerSessionReplyRecovery.d.ts +1 -1
  78. package/dist/relayRuntimeUpgrade.d.ts +1 -1
  79. package/dist/schemas/codex-signing-recovery-native-progress.schema.json +16 -0
  80. package/dist/schemas/device-enrollment-recovery-journal.schema.json +20 -0
  81. package/dist/schemas/device-signing-maintenance-challenge.schema.json +185 -0
  82. package/dist/schemas/device-signing-maintenance-completion.schema.json +98 -0
  83. package/dist/schemas/device-signing-maintenance-context-request.schema.json +68 -0
  84. package/dist/schemas/device-signing-maintenance-context.schema.json +137 -0
  85. package/dist/schemas/device-signing-maintenance-control.schema.json +12 -0
  86. package/dist/schemas/device-signing-maintenance-controller-progress.schema.json +26 -0
  87. package/dist/schemas/device-signing-maintenance-delivery-request.schema.json +45 -0
  88. package/dist/schemas/device-signing-maintenance-host-progress.schema.json +121 -0
  89. package/dist/schemas/device-signing-maintenance-plan.schema.json +136 -0
  90. package/dist/schemas/device-signing-maintenance-preparation.schema.json +56 -0
  91. package/dist/schemas/device-signing-recovery-candidate.schema.json +28 -0
  92. package/dist/schemas/device-signing-recovery-challenge.schema.json +52 -0
  93. package/dist/schemas/device-signing-recovery-completion.schema.json +23 -0
  94. package/dist/schemas/device-signing-recovery-delivery-request.schema.json +16 -0
  95. package/dist/schemas/device-signing-recovery-delivery.schema.json +22 -0
  96. package/dist/schemas/device-signing-recovery-host-progress.schema.json +28 -0
  97. package/dist/schemas/device-signing-recovery-offer-progress.schema.json +32 -0
  98. package/dist/schemas/device-signing-recovery-offer-read-request.schema.json +16 -0
  99. package/dist/schemas/device-signing-recovery-offer-registration-receipt.schema.json +16 -0
  100. package/dist/schemas/device-signing-recovery-offer-registration-request.schema.json +18 -0
  101. package/dist/schemas/device-signing-recovery-offer-response.schema.json +43 -0
  102. package/dist/schemas/device-signing-recovery-plan.schema.json +40 -0
  103. package/dist/schemas/device-signing-recovery-preparation.schema.json +23 -0
  104. package/dist/schemas/provider-session-recovery-declaration.schema.json +21 -0
  105. package/dist/signingMaintenanceCohort.test.d.ts +2 -0
  106. package/dist/signingMaintenanceCohort.test.d.ts.map +1 -0
  107. package/dist/signingMaintenanceCohort.test.js +55 -0
  108. package/dist/signingMaintenanceCohort.test.js.map +1 -0
  109. package/dist/usage.d.ts.map +1 -1
  110. package/dist/usage.js +3 -2
  111. package/dist/usage.js.map +1 -1
  112. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -12,11 +12,13 @@ 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
24
  import { assertRelayStartupOwnership, disableRelayAutostart, enableRelayAutostart, inspectOwnedRelayAutostart, relayAutostartStatus, startRelayAutostart, stopRelayAutostart } from './relayAutostart.js';
@@ -100,6 +102,8 @@ import { requestCodexSetupHostPreparation } from './codexSetupPortalPreparation.
100
102
  import { readCodexSetupPublicStdin } from './codexSetupPublicStdin.js';
101
103
  import { reserveCodexSetupLaunch } from './codexSetupLaunchReservation.js';
102
104
  export { startCodexSetupNativeHost } from './codexSetupNativeHost.js';
105
+ export { attachCodexSigningRecoveryNativeHost } from './codexSigningRecoveryNativeHost.js';
106
+ export { openCodexSigningRecoveryOwner } from './codexBoundSession.js';
103
107
  export { openCodexSetupVaultJournal } from './codexSetupVaultJournal.js';
104
108
  import { createCodexSetupReadinessOwner } from './codexSetupReadiness.js';
105
109
  export { createCodexSetupReadinessOwner } from './codexSetupReadiness.js';
@@ -107,7 +111,8 @@ import { assertCodexSetupExecutionLease } from './codexSetupAdmission.js';
107
111
  import { parseLocalCodexSetupReadiness } from '@dharma-ai-labs/agent-fabric-local-vault/setup-readiness';
108
112
  import { writeBootstrapHostJson, writeBootstrapHostText } from './bootstrapHostFiles.js';
109
113
  export { openCooperativeInboxSession } from './cooperativeInboxSession.js';
110
- const VERSION = '0.2.209';
114
+ import { inspectNamedCodexRecoveryToolDeclaration } from './codexSigningRecoveryTool.js';
115
+ const VERSION = '0.2.210';
111
116
  const USAGE = CLI_USAGE;
112
117
  const execFileAsync = promisify(execFile);
113
118
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -161,10 +166,25 @@ export function portalUrl(flags, fallback = 'https://www.dharma-ai.io') {
161
166
  return String(flags.get('portal-url') || flags.get('hq-url') || fallback);
162
167
  }
163
168
  function print(value) {
164
- process.stdout.write(typeof value === 'string' ? `${value}\n` : `${JSON.stringify(value, null, 2)}\n`);
169
+ process.stdout.write(formatCommandOutput(value));
170
+ }
171
+ export function formatCommandOutput(value) {
172
+ if (typeof value === 'string')
173
+ return value + '\n';
174
+ const streaming = value && typeof value === 'object' && !Array.isArray(value)
175
+ && value.schema === 'dharma.device-signing-maintenance-controller-progress/v1';
176
+ return JSON.stringify(value, null, streaming ? undefined : 2) + '\n';
165
177
  }
166
178
  export function commandExitCode(argv, value) {
167
179
  const { positional, flags } = parseCliOptions(argv);
180
+ if (positional[0] === 'signing-recovery') {
181
+ if (!value || typeof value !== 'object' || Array.isArray(value))
182
+ return 2;
183
+ const receipt = value, progress = receipt.progress;
184
+ return receipt.schema === 'dharma.device-signing-maintenance-controller-progress/v1' && receipt.ok === true
185
+ && receipt.nativeContinuationAuthorized === false && receipt.ready === false
186
+ && (flags.get('apply') === true ? progress?.state === 'trust_installed' && progress.clientTrustInstalled === true : receipt.planned === true) ? 0 : 2;
187
+ }
168
188
  if (positional[0] === 'sessions' && positional[1] === 'readiness' && flags.get('diagnostic') === true) {
169
189
  const diagnosticResult = value && typeof value === 'object' && !Array.isArray(value)
170
190
  ? value : null;
@@ -1044,7 +1064,7 @@ async function pidProcessState(pidPath) {
1044
1064
  const code = error.code;
1045
1065
  if (code === 'ESRCH')
1046
1066
  return 'stopped';
1047
- return code === 'EPERM' ? 'unknown' : 'stopped';
1067
+ return 'unknown';
1048
1068
  }
1049
1069
  }
1050
1070
  export async function relayProcessState(home = dharmaHome()) {
@@ -1053,6 +1073,42 @@ export async function relayProcessState(home = dharmaHome()) {
1053
1073
  export async function relaySupervisorProcessState(home = dharmaHome()) {
1054
1074
  return pidProcessState(resolve(home, 'relay', 'supervisor.pid'));
1055
1075
  }
1076
+ /** A legacy runtime may not participate in the new signing mutex. Require all
1077
+ * registrations belonging to this device disabled and their known owned PID
1078
+ * locks stopped, plus its relay/supervisor, before any maintenance store read.
1079
+ * This check never stops a process or changes startup/registration state. */
1080
+ export async function deviceTrustMaintenanceProcessState(home, name) {
1081
+ const selected = await readNamedSession(home, name);
1082
+ if (!selected)
1083
+ return 'unknown';
1084
+ const states = await Promise.all([relayProcessState(home), relaySupervisorProcessState(home)]);
1085
+ let entries;
1086
+ try {
1087
+ entries = await readdir(resolve(home, 'sessions'));
1088
+ }
1089
+ catch (error) {
1090
+ if (error.code !== 'ENOENT')
1091
+ return 'unknown';
1092
+ entries = [];
1093
+ }
1094
+ for (const entry of entries) {
1095
+ if (!/^[a-z][a-z0-9-]{0,47}$/.test(String(entry)))
1096
+ continue;
1097
+ let registration;
1098
+ try {
1099
+ registration = await readNamedSession(home, String(entry));
1100
+ }
1101
+ catch {
1102
+ return 'unknown';
1103
+ }
1104
+ if (!registration)
1105
+ return 'unknown';
1106
+ if (registration.identity.deviceId !== selected.identity.deviceId || registration.identity.organizationId !== selected.identity.organizationId)
1107
+ continue;
1108
+ states.push(registration.enabled ? 'unknown' : await pidProcessState(resolve(namedSessionPaths(home, String(entry)).root, 'service.lock')));
1109
+ }
1110
+ return states.includes('running') ? 'running' : states.includes('unknown') ? 'unknown' : 'stopped';
1111
+ }
1056
1112
  export async function acquireRelayProcessLease(home = dharmaHome()) {
1057
1113
  const pidPath = resolve(home, 'relay', 'relay.pid');
1058
1114
  const releaseLock = await acquirePidLock(`${pidPath}.lock`, 250, 'Another relay process is already running for this device.');
@@ -1228,6 +1284,35 @@ export async function withRelayStartupMutation(operation, home = dharmaHome()) {
1228
1284
  await release?.();
1229
1285
  }
1230
1286
  }
1287
+ export async function acquireNamedSessionStartupAdmission(home = dharmaHome()) {
1288
+ return acquirePidLock(resolve(home, 'autostart-mutation.lock'), 250, 'named_session_startup_busy');
1289
+ }
1290
+ async function assertSigningMaintenanceManagedCohort(home, workspace) {
1291
+ const startup = await inspectOwnedRelayAutostart({ home });
1292
+ const bin = resolve(workspace, '.dharma', 'bin');
1293
+ if (startup.version !== VERSION || startup.workspace !== workspace
1294
+ || startup.launcher !== resolve(bin, process.platform === 'win32' ? 'dharma.cmd' : 'dharma')
1295
+ || startup.policy !== resolve(workspace, '.dharma', 'approved-policy.json')) {
1296
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1297
+ }
1298
+ const readLauncher = async (name) => {
1299
+ const path = resolve(bin, name), before = await lstat(path);
1300
+ if (!before.isFile() || before.isSymbolicLink() || before.size > 65536 || await realpath(path) !== path) {
1301
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1302
+ }
1303
+ const contents = await readFile(path, 'utf8'), after = await lstat(path);
1304
+ if (after.ino !== before.ino || after.dev !== before.dev || after.size !== before.size
1305
+ || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || after.isSymbolicLink()) {
1306
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1307
+ }
1308
+ return contents;
1309
+ };
1310
+ const launchers = { shell: await readLauncher('dharma'), windows: await readLauncher('dharma.cmd') };
1311
+ if (!await verifyRecordedRepositoryLaunchers(VERSION, launchers)) {
1312
+ throw Error('device_signing_maintenance_managed_cohort_upgrade_required');
1313
+ }
1314
+ return sha256(JSON.stringify({ schema: 'dharma.managed-signing-maintenance-cohort/v1', startup, launchers }));
1315
+ }
1231
1316
  export async function withWorkspacePolicyRefreshLock(workspaceId, operation) {
1232
1317
  if (!workspaceId)
1233
1318
  throw new Error('Content transmission requires a registered workspace ID.');
@@ -5246,498 +5331,522 @@ async function namedSessionCommand(action, flags) {
5246
5331
  if (process.platform !== 'linux')
5247
5332
  throw new Error('codex_session_sandbox_unqualified');
5248
5333
  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 });
5287
- }
5288
- catch (error) {
5289
- if (diagnostic && error instanceof NamedSessionTransportFailure)
5290
- return namedSessionReadinessDiagnostic(existing, error);
5291
- throw error;
5292
- }
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');
5334
+ let startupAdmission;
5335
+ const releaseStartupAdmission = async () => {
5336
+ const release = startupAdmission;
5337
+ startupAdmission = undefined;
5338
+ await release?.();
5339
+ };
5340
+ try {
5341
+ if (['start', 'serve'].includes(action) && flags.has('apply')) {
5342
+ startupAdmission = await step(() => acquireNamedSessionStartupAdmission());
5298
5343
  }
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
- }
5335
- });
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');
5344
+ let childMessage;
5345
+ if (flags.has('setup-handoff')) {
5346
+ if (action !== 'serve' || flags.get('setup-handoff') !== true
5347
+ || [...flags.keys()].some(key => !['name', 'workspace-id', 'apply', 'setup-handoff'].includes(key))) {
5348
+ throw new Error('setup_child_unavailable');
5349
+ }
5350
+ childMessage = await receiveCodexSetupChildStart({ process, signal: new AbortController().signal });
5351
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' });
5352
+ const item = (await step(() => registry())).find(row => row.workspaceId === workspaceId);
5353
+ const config = await step(() => readDeviceConfig());
5354
+ if (!item || !config || item.organizationId !== config.organizationId)
5355
+ throw new Error('named_session_scope_mismatch');
5356
+ await assertBootstrapHostSource(item.path, String(item.repositoryRemoteHash || ''));
5357
+ const scope = repositoryRoleScope(item);
5358
+ const existing = await step(() => readNamedSession(dharmaHome(), name));
5359
+ if (setupSessionScope && (name !== setupSessionScope.request.name || workspaceId !== setupSessionScope.request.workspaceId
5360
+ || existing && existing.identity.membershipId !== setupSessionScope.request.membershipId))
5361
+ throw new Error('setup_session_scope_changed');
5362
+ if (existing && (existing.identity.workspaceId !== item.workspaceId
5363
+ || existing.identity.organizationId !== config.organizationId || existing.identity.deviceId !== config.deviceId
5364
+ || existing.identity.endpointId !== scope.endpointId || existing.identity.repositoryBindingId !== scope.repositoryBindingId)) {
5365
+ throw new Error('named_session_scope_mismatch');
5370
5366
  }
5371
- catch {
5372
- return { ok: true, name, state: 'stopped', enabled: false };
5367
+ if (action === 'status') {
5368
+ try {
5369
+ return await namedSessionRequest(dharmaHome(), name, { action: 'status' });
5370
+ }
5371
+ catch {
5372
+ return { ok: true, name, state: 'stopped', registered: Boolean(existing), enabled: existing?.enabled ?? false,
5373
+ bindingId: existing?.bindingId ?? null };
5374
+ }
5373
5375
  }
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) }));
5376
+ if (action === 'readiness') {
5377
+ if (!existing?.enabled)
5378
+ throw new Error('named_session_readiness_unavailable');
5379
+ let result;
5380
+ try {
5381
+ result = await namedSessionRequest(dharmaHome(), name, { action: 'readiness' }, { diagnostic });
5391
5382
  }
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 }));
5383
+ catch (error) {
5384
+ if (diagnostic && error instanceof NamedSessionTransportFailure)
5385
+ return namedSessionReadinessDiagnostic(existing, error);
5386
+ throw error;
5387
+ }
5388
+ if (Object.keys(result).sort().join(',') !== 'bindingId,deviceId,endpointId,membershipId,name,nativeSkill,ok,organizationId,provider,repositoryBindingId,schema,sessionId,workspaceId'
5389
+ || result.schema !== 'dharma.named-session-readiness/v1' || result.name !== name || result.bindingId !== existing.bindingId
5390
+ || typeof result.sessionId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$(?![\s\S])/.test(result.sessionId)
5391
+ || Object.entries(existing.identity).some(([key, value]) => result[key] !== value)) {
5392
+ throw new Error('named_session_readiness_scope_mismatch');
5393
+ }
5394
+ return { ...result, nativeSkill: parseNamedCodexSkillObservation(result.nativeSkill) };
5395
+ }
5396
+ if (action === 'work')
5397
+ return namedSessionRequest(dharmaHome(), name, { action: 'work',
5398
+ workId: String(flags.get('work-id') || randomUUID()), prompt: required(flags, 'prompt') });
5399
+ if (['quiesce', 'resume'].includes(action) || action === 'stop' && flags.has('drained')) {
5400
+ const allowed = ['name', 'workspace-id', 'fence-id', 'apply', 'dry-run', 'json',
5401
+ ...(action === 'stop' ? ['drained'] : action === 'resume' ? ['recover-stopped'] : [])];
5402
+ if (hostScope || supervisorOwner || setupSessionScope || !existing
5403
+ || [...flags.keys()].some(key => !allowed.includes(key))
5404
+ || ['apply', 'dry-run', 'drained', 'recover-stopped', 'json'].some(key => flags.has(key) && flags.get(key) !== true)
5405
+ || flags.has('apply') && flags.has('dry-run'))
5406
+ throw new Error('named_session_maintenance_unavailable');
5407
+ const fenceId = required(flags, 'fence-id');
5408
+ if (!UUID_PATTERN.test(fenceId))
5409
+ throw new Error('named_session_fence_invalid');
5410
+ const requestAction = action === 'stop' ? 'stop-drained' : action;
5411
+ if (!flags.has('apply'))
5412
+ return { ok: true, planned: true, name, workspaceId, action: requestAction, fenceId,
5413
+ mode: flags.has('recover-stopped') ? 'stopped_recovery' : 'live' };
5414
+ if (action === 'resume' && flags.has('recover-stopped'))
5415
+ return withRelayStartupMutation(async () => {
5416
+ let vault;
5417
+ try {
5418
+ return await recoverStoppedNamedSession({ registration: existing, fenceId, workspace: item.path,
5419
+ processState: () => pidProcessState(resolve(paths.root, 'service.lock')),
5420
+ readRegistration: () => readNamedSession(dharmaHome(), name),
5421
+ readBinding: async () => {
5422
+ vault = await openBootstrapVault({ root: resolve(dharmaHome(), 'vault') });
5423
+ return vault.getProviderSessionBinding(existing.bindingId, existing.identity);
5424
+ },
5425
+ saveRegistration: value => saveNamedSession(dharmaHome(), value) });
5426
+ }
5427
+ finally {
5428
+ await vault?.close();
5429
+ }
5430
+ });
5431
+ return maintainNamedSession({ action: requestAction, fenceId,
5432
+ registration: existing, request: body => namedSessionRequest(dharmaHome(), name, body) });
5433
+ }
5434
+ if (action === 'audit') {
5435
+ if (!existing?.enabled || [...flags.keys()].some(key => !['name', 'workspace-id', 'work-id'].includes(key))) {
5436
+ throw new Error('named_session_audit_unavailable');
5437
+ }
5438
+ const workId = required(flags, 'work-id');
5439
+ if (!/^[A-Za-z0-9_-]{1,128}$(?![\s\S])/.test(workId))
5440
+ throw new Error('named_session_work_invalid');
5441
+ const result = await namedSessionRequest(dharmaHome(), name, { action: 'audit', workId });
5442
+ if (!(await validateContract(resolve(dirname(fileURLToPath(import.meta.url)), 'schemas'), 'https://schemas.dharma-ai.io/named-session-native-audit/v1', result)).ok
5443
+ || result.bindingId !== existing.bindingId || result.workId !== workId
5444
+ || Object.entries(existing.identity).some(([key, value]) => result[key] !== value)) {
5445
+ throw new Error('named_session_audit_scope_mismatch');
5400
5446
  }
5447
+ return result;
5401
5448
  }
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();
5449
+ if (action === 'stop') {
5450
+ if (!flags.has('apply'))
5451
+ return { ok: true, planned: true, name, action: 'stop' };
5452
+ const current = await namedSessionRequest(dharmaHome(), name, { action: 'status' }).catch(() => null);
5453
+ if (current?.admission) {
5454
+ if (!existing || current.bindingId !== existing.bindingId
5455
+ || Object.entries(existing.identity).some(([key, value]) => current[key] !== value)) {
5456
+ throw new Error('named_session_scope_mismatch');
5435
5457
  }
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';
5458
+ // The capable service rejects an unfenced stop without changing its registration.
5459
+ return namedSessionRequest(dharmaHome(), name, { action: 'stop' });
5460
+ }
5461
+ if (existing)
5462
+ await saveNamedSession(dharmaHome(), { ...existing, enabled: false });
5465
5463
  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');
5464
+ return await namedSessionRequest(dharmaHome(), name, { action: 'stop' });
5465
+ }
5466
+ catch {
5467
+ return { ok: true, name, state: 'stopped', enabled: false };
5468
+ }
5469
+ }
5470
+ if (!['start', 'serve'].includes(action))
5471
+ throw new Error('named_session_action_invalid');
5472
+ if (!flags.has('apply'))
5473
+ return { ok: true, planned: true, name, workspaceId,
5474
+ provider: 'codex', localWork: 'workspace_write', peerQuestions: 'read_only', network: 'deny' };
5475
+ await step(async () => verifyNamedSessionVisibleSkill(await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path }), await repositorySharedReady(item)));
5476
+ if (action === 'start') {
5477
+ if (existing && !existing.enabled) {
5478
+ if (existing.maintenanceFence) {
5479
+ if (hostScope || supervisorOwner || setupSessionScope || !flags.has('fence-id') || flags.get('apply') !== true
5480
+ || [...flags.keys()].some(key => !['name', 'workspace-id', 'fence-id', 'apply', 'json'].includes(key))) {
5481
+ throw new Error('named_session_quiesced');
5499
5482
  }
5500
- ownerReceipt = response;
5483
+ await activateMaintenanceRestart({ registration: existing, fenceId: required(flags, 'fence-id'),
5484
+ processState: () => pidProcessState(resolve(paths.root, 'service.lock')),
5485
+ readRegistration: () => readNamedSession(dharmaHome(), name), saveRegistration: value => saveNamedSession(dharmaHome(), value) });
5501
5486
  }
5502
5487
  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');
5488
+ if (supervisorOwner)
5489
+ throw new Error('named_session_disabled');
5490
+ const live = await step(() => namedSessionRequest(dharmaHome(), name, { action: 'status' })).catch(() => null);
5491
+ if (live?.admission && live.admission.state !== 'open') {
5492
+ throw new Error('named_session_quiesced');
5515
5493
  }
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 };
5494
+ await step(() => saveNamedSession(dharmaHome(), { ...existing, enabled: true }));
5524
5495
  }
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');
5496
+ }
5497
+ try {
5498
+ return await step(() => namedSessionRequest(dharmaHome(), name, { action: 'status' }));
5499
+ }
5500
+ catch { /* Start an owned worker below. */ }
5501
+ const args = [fileURLToPath(import.meta.url), 'sessions', 'serve', '--name', name,
5502
+ '--workspace-id', workspaceId, '--apply'];
5503
+ if (setupSessionScope)
5504
+ args.push('--setup-handoff');
5505
+ for (const key of ['session-budget-cents', 'turn-budget-cents']) {
5506
+ if (flags.has(key))
5507
+ args.push(`--${key}`, required(flags, key));
5508
+ }
5509
+ let startupChild;
5510
+ try {
5511
+ // The child takes this same admission lock before private startup. Never
5512
+ // wait for it while holding its parent-side registration/enablement lock.
5513
+ await releaseStartupAdmission();
5514
+ await step(async () => {
5515
+ const start = () => {
5516
+ const child = spawn(process.execPath, args, { cwd: item.path, detached: !hostScope && !supervisorOwner,
5517
+ stdio: setupSessionScope ? ['ignore', 'ignore', 'pipe', 'ipc'] : ['ignore', 'ignore', 'pipe'], env: process.env });
5518
+ startupChild = observeNamedSessionStartupChild(child);
5519
+ return child;
5520
+ };
5521
+ if (supervisorOwner) {
5522
+ const child = await supervisorOwner.spawn(name, start);
5523
+ if (setupSessionScope)
5524
+ await sendCodexSetupChildStart({ owner: supervisorOwner, child, scope: setupSessionScope });
5525
+ return;
5526
+ }
5527
+ const child = start();
5528
+ if (hostScope)
5529
+ captureBootstrapHostChild(hostScope, child);
5530
+ else {
5531
+ child.on('error', () => { });
5532
+ child.unref();
5542
5533
  }
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
5534
  });
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);
5535
+ return await waitForNamedSessionStartup({ step,
5536
+ assertActive: async () => { await hostScope?.assert(); },
5537
+ observe: () => namedSessionRequest(dharmaHome(), name, { action: 'status' }),
5538
+ readFailure: () => startupChild?.readFailure(),
5539
+ maximumWaitMs: setupSessionScope
5540
+ ? Math.min(120_000, Date.parse(setupSessionScope.request.expiresAt) - Date.now()) : 120_000 });
5571
5541
  }
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' } };
5542
+ finally {
5543
+ startupChild?.dispose();
5544
+ }
5545
+ }
5546
+ await mkdir(paths.root, { recursive: true, mode: 0o700 });
5547
+ const vault = await openBootstrapVault({ root: resolve(dharmaHome(), 'vault') })
5548
+ .catch(error => { throw namedSessionStartupPhaseError('vault', error); });
5549
+ let releaseLock;
5550
+ const controller = new AbortController(), stop = () => controller.abort();
5551
+ process.once('SIGTERM', stop);
5552
+ process.once('SIGINT', stop);
5553
+ if (childMessage)
5554
+ process.once('disconnect', stop);
5555
+ let transport;
5556
+ try {
5557
+ releaseLock = await acquirePidLock(resolve(paths.root, 'service.lock'), 250, 'named_session_already_running')
5558
+ .catch(error => { throw namedSessionStartupPhaseError('lock', error); });
5559
+ // A startup poll may predate quiescence. Recheck durable admission after winning the owner lock.
5560
+ assertNamedSessionStartupRegistration(existing, await step(() => readNamedSession(dharmaHome(), name)));
5561
+ // Once the per-service lock is held, maintenance cannot acquire its stopped
5562
+ // topology. Release home-wide admission before the long-lived provider loop.
5563
+ await releaseStartupAdmission();
5564
+ const startProvider = async (childScope) => {
5565
+ let startupPhase = 'client';
5566
+ try {
5567
+ const startupStep = (operation) => childScope ? childScope.step(operation) : operation();
5568
+ if (childScope && existing && existing.identity.membershipId !== childScope.request.membershipId) {
5569
+ throw new Error('setup_child_unavailable');
5570
+ }
5571
+ const fabric = await startupStep(() => AgentFabricClient.open({ configPath: configPath(), statePath: resolve(paths.root, 'protocol-state.json'),
5572
+ hostScope: currentBootstrapHostScope() }));
5573
+ if (fabric.config.organizationId !== config.organizationId || fabric.config.deviceId !== config.deviceId
5574
+ || fabric.config.publicKeyEd25519 !== config.publicKeyEd25519)
5575
+ throw new Error('named_session_trust_scope_mismatch');
5576
+ startupPhase = 'trust';
5577
+ const trust = createNamedSessionTrust({ configPath: configPath(), identity: fabric.config });
5578
+ await startupStep(() => trust.refresh());
5579
+ startupPhase = 'session';
5580
+ await startupStep(() => fabric.openSession(VERSION));
5581
+ startupPhase = 'policy';
5582
+ const policyPath = resolve(item.path, '.dharma', 'approved-policy.json');
5583
+ const policy = await startupStep(() => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric));
5584
+ const writeRoots = policy.tasks.writePaths.filter(path => /^[a-zA-Z0-9_-]+\/\*\*$/.test(path))
5585
+ .map(path => path.slice(0, -3));
5586
+ if (!writeRoots.length)
5587
+ throw new Error('named_session_workspace_write_not_authorized');
5588
+ startupPhase = 'registration';
5589
+ let registration = existing, pending;
5590
+ let ownerReceipt;
5591
+ // Resolve authenticated ownership before opening any provider transport.
5592
+ if (registration) {
5593
+ const response = await startupStep(() => fabric.signedPost('/agent-fabric/provider-sessions', {
5594
+ schema: 'dharma.provider-session-registration/v1', action: 'inspect', provider: 'codex', mode: 'bridge_owned',
5595
+ workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5596
+ bindingId: registration.bindingId, expectedRevision: 0, leaseSeconds: 60,
5597
+ }));
5598
+ if (!isNamedSessionReconnectReceipt(response, registration.bindingId, registration.identity)) {
5599
+ throw new Error('named_session_registration_invalid');
5669
5600
  }
5670
- catch { /* Source capture failure must not replay or rewrite a coding task. */ }
5601
+ ownerReceipt = response;
5671
5602
  }
5603
+ else {
5604
+ const maximumCostCents = boundedInteger(flags.get('session-budget-cents'), 1000, 1, 10000, '--session-budget-cents');
5605
+ const maximumTurnCostCents = boundedInteger(flags.get('turn-budget-cents'), 25, 1, maximumCostCents, '--turn-budget-cents');
5606
+ const bindingId = randomUUID();
5607
+ const response = await startupStep(() => fabric.signedPost('/agent-fabric/provider-sessions', {
5608
+ schema: 'dharma.provider-session-registration/v1', action: 'attach', provider: 'codex', mode: 'bridge_owned',
5609
+ workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5610
+ bindingId, expectedRevision: 0, leaseSeconds: 60,
5611
+ }));
5612
+ const remote = response.registration;
5613
+ if (!remote || !UUID_PATTERN.test(String(remote.membershipId))
5614
+ || childScope && remote.membershipId !== childScope.request.membershipId || remote.revision !== 1) {
5615
+ throw new Error('named_session_registration_invalid');
5616
+ }
5617
+ const identity = { organizationId: config.organizationId, repositoryBindingId: scope.repositoryBindingId,
5618
+ workspaceId, endpointId: scope.endpointId, membershipId: String(remote.membershipId), deviceId: config.deviceId,
5619
+ provider: 'codex' };
5620
+ if (!isNamedSessionOwnerReceipt(response, bindingId, identity))
5621
+ throw new Error('named_session_registration_invalid');
5622
+ ownerReceipt = response;
5623
+ pending = { schema: 'dharma.named-session/v1', name, bindingId, identity,
5624
+ maximumCostCents, maximumTurnCostCents, enabled: true };
5625
+ }
5626
+ startupPhase = 'environment';
5627
+ const environment = namedCodexEnvironment(process.env);
5628
+ const privateRoots = [dharmaHome(), resolve(environment.CODEX_HOME || resolve(homedir(), '.codex'))];
5629
+ if (environment.XDG_RUNTIME_DIR)
5630
+ privateRoots.push(environment.XDG_RUNTIME_DIR);
5631
+ if (environment.DBUS_SESSION_BUS_ADDRESS)
5632
+ privateRoots.push(environment.DBUS_SESSION_BUS_ADDRESS.slice('unix:path='.length));
5633
+ startupPhase = 'filesystem';
5634
+ const filesystem = await startupStep(() => namedCodexFilesystem({ environment, workspace: item.path, privateRoots, writeRoots }));
5635
+ startupPhase = 'transport';
5636
+ await startupStep(async () => {
5637
+ const expectedOwner = registration ?? pending;
5638
+ const ownerConfirmed = registration
5639
+ ? isNamedSessionReconnectReceipt(ownerReceipt, expectedOwner.bindingId, expectedOwner.identity)
5640
+ : isNamedSessionOwnerReceipt(ownerReceipt, expectedOwner.bindingId, expectedOwner.identity);
5641
+ if (!ownerConfirmed) {
5642
+ throw new Error('named_session_registration_invalid');
5643
+ }
5644
+ transport = await openCodexAppServerTransport({ command: filesystem.providerExecutable, cwd: item.path,
5645
+ environment: filesystem.environment, experimentalApi: true, toolCallTimeoutMs: 60_000,
5646
+ argv: [...namedCodexNodeOutputArguments(), '-c', 'allow_login_shell=false', '-c', 'default_permissions="dharma_bridge"',
5647
+ '-c', `permissions.dharma_bridge.filesystem=${filesystem.peer}`,
5648
+ '-c', 'permissions.dharma_bridge.network={enabled=false}',
5649
+ '-c', `permissions.dharma_work.filesystem=${filesystem.work}`,
5650
+ '-c', 'permissions.dharma_work.network={enabled=false}', 'app-server'] });
5651
+ return transport;
5652
+ });
5653
+ startupPhase = 'account';
5654
+ const account = await startupStep(() => transport.request('account/read', { refreshToken: false }));
5655
+ if (!account.account)
5656
+ throw new Error('named_session_provider_authentication_required');
5657
+ if (pending) {
5658
+ startupPhase = 'thread';
5659
+ const threadId = await startupStep(() => startNamedCodexThread(transport, item.path, name));
5660
+ const declaredRecoveryToolHash = inspectNamedCodexRecoveryToolDeclaration(transport, threadId);
5661
+ if (!declaredRecoveryToolHash)
5662
+ throw new Error('named_session_thread_invalid');
5663
+ startupPhase = 'binding';
5664
+ await startupStep(async () => vault.saveProviderSessionBinding({ schema: 'dharma.local-provider-session-binding/v1', ...pending.identity,
5665
+ bindingId: pending.bindingId, owner: 'dharma_bridge', sessionId: threadId, workspaceRoot: item.path,
5666
+ createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(),
5667
+ maximumProviderCostCents: pending.maximumTurnCostCents }));
5668
+ await startupStep(async () => vault.saveProviderSessionRecoveryDeclaration(pending.bindingId, pending.identity, declaredRecoveryToolHash));
5669
+ registration = pending;
5670
+ await startupStep(async () => saveNamedSession(dharmaHome(), registration));
5671
+ }
5672
+ return { registration: registration, filesystem, fabric, trust, policyPath, writeRoots };
5672
5673
  }
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');
5674
+ catch (error) {
5675
+ throw namedSessionStartupPhaseError(startupPhase, error);
5691
5676
  }
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 () => {
5677
+ };
5678
+ const { registration, filesystem, fabric, trust, policyPath, writeRoots } = childMessage
5679
+ ? await runCodexSetupChildStartup({ vault: vault, message: childMessage, name, workspaceId,
5680
+ signal: controller.signal, authorize: qualifyCodexSetupChildRequest }, startProvider)
5681
+ : await startProvider();
5682
+ const authorizeLifetime = async () => {
5704
5683
  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();
5684
+ if (!writeRoots.every(root => current.tasks.writePaths.includes(`${root}/**`)))
5685
+ return false;
5686
+ const response = await fabric.signedPost('/agent-fabric/provider-sessions', {
5687
+ schema: 'dharma.provider-session-registration/v1', action: 'inspect', provider: 'codex', mode: 'bridge_owned',
5688
+ workspaceId, endpointId: scope.endpointId, repositoryBindingId: scope.repositoryBindingId,
5689
+ bindingId: registration.bindingId, expectedRevision: 0, leaseSeconds: 60,
5690
+ });
5691
+ return isNamedSessionOwnerReceipt(response, registration.bindingId, registration.identity);
5692
+ };
5693
+ const refreshLifetime = () => renewNamedSessionLifetime({ vault, bindingId: registration.bindingId,
5694
+ identity: registration.identity, trust, authorize: authorizeLifetime });
5695
+ await refreshLifetime().catch(error => { throw namedSessionStartupPhaseError('lifetime', error); });
5696
+ const channelTransport = { async signedPost(route, body) {
5697
+ await trust.refresh();
5698
+ const response = await fabric.signedPost(route, body);
5699
+ await trust.refresh();
5700
+ return response;
5701
+ } };
5702
+ // Fresh server acceptance and this live-owner claim fence peer execution;
5703
+ // durable reservations retain cost even when delivery or execution is uncertain.
5704
+ const consumed = new Set();
5705
+ let taskState = null;
5706
+ let taskStateFailure = 'source_state_unavailable';
5707
+ const taskSnapshot = async () => {
5708
+ const sourceAuthorization = await fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item));
5709
+ return inventoryRepositoryPackage({ workspace: item.path, organizationId: item.organizationId,
5710
+ workspaceId, repositoryBindingId: item.repositoryBindingId, repositoryAgentId: item.repositoryAgentId, sourceAuthorization });
5711
+ };
5712
+ const readCurrentRepositoryContext = async (contentMode = 'references', referenceText) => readNamedSessionRepositoryContext({
5713
+ installation: await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path }),
5714
+ sharedRepositoryReady: await repositorySharedReady(item), scope: repositoryRoleScope(item), workspaceRoot: item.path, contentMode, referenceText,
5715
+ loadAuthority: async () => ({ policy: await refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5716
+ source: await fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item)) })
5717
+ });
5718
+ return await runNamedSessionService({ home: dharmaHome(), registration, vault, signal: controller.signal,
5719
+ localWriteRoots: writeRoots, additionalFilesystemRules: filesystem.additionalFilesystemRules,
5720
+ readPeerRepositoryContext: question => readCurrentRepositoryContext('verified_inline', question.question),
5721
+ observeNativeSkill: async () => {
5722
+ const installation = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5723
+ const native = await observeNamedCodexSkill(transport, { workspace: item.path, installation,
5724
+ sharedRepositoryReady: await repositorySharedReady(item) });
5725
+ const current = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5726
+ if (!current.signedLifecycleReady || current.activeBundleId !== native.bundleId
5727
+ || current.activeBundleHash !== native.bundleHash || current.nativeSkillPath !== installation.nativeSkillPath
5728
+ || !await repositorySharedReady(item))
5729
+ throw new Error('named_session_native_skill_unavailable');
5730
+ return native;
5731
+ },
5732
+ syncTaskExports: () => syncNamedSessionTaskExports({ vault, bindingId: registration.bindingId,
5733
+ identity: registration.identity,
5734
+ loadPolicy: () => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5735
+ send: body => fabric.signedPost('/agent-fabric/codex-task-exports', body) }),
5736
+ withActivationBoundary: (operation, work) => withDeviceSigningControl({ config: fabric.config }, () => withWorkspaceSkillActivationLock(workspaceId, 'codex', async () => {
5737
+ await refreshLifetime();
5738
+ const skill = await verifyAgentFabricSkillInstallation({ provider: 'codex', workspace: item.path });
5739
+ const sharedReady = await repositorySharedReady(item);
5740
+ const packageContent = await readNamedSessionPackageContent(skill, sharedReady);
5741
+ const bundleId = packageContent.bundleId;
5742
+ taskState = null;
5743
+ taskStateFailure = 'source_state_unavailable';
5744
+ if (work) {
5745
+ const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5746
+ if (current.evidence.automaticDisclosure?.mode === 'customer_authorized_content' && skill.activeBundleHash) {
5747
+ try {
5748
+ taskStateFailure = 'public_context_unavailable';
5749
+ const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5750
+ if (!binding)
5751
+ throw new Error('named_session_binding_unavailable');
5752
+ const retained = await readCodexPublicContext(transport, { threadId: binding.sessionId, workspaceRoot: binding.workspaceRoot });
5753
+ taskStateFailure = 'runtime_version_unavailable';
5754
+ const version = (await execFileAsync('codex', ['--version'], { timeout: 10_000, maxBuffer: 4096,
5755
+ env: providerProcessEnvironment(process.env) })).stdout.trim();
5756
+ if (!/^codex-cli [0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$/.test(version)) {
5757
+ throw new Error('named_session_runtime_version_unavailable');
5758
+ }
5759
+ const contextContentHash = await vault.putBlob(Buffer.from(retained.bytes), 'named-session-public-context');
5760
+ if (contextContentHash !== retained.contextHash)
5761
+ throw new Error('named_session_context_integrity_failed');
5762
+ taskStateFailure = 'source_state_unavailable';
5763
+ const before = await taskSnapshot();
5764
+ const sourceContentHash = await vault.putBlob(Buffer.from(serializeRepositoryPackageSnapshot(before)), 'named-session-source-state');
5765
+ await vault.putBlob(Buffer.from(JSON.stringify({ workId: work.workId, bindingId: registration.bindingId,
5766
+ sourceContentHash, sourceSnapshotHash: before.manifest.snapshotHash, bundleId, bundleHash: skill.activeBundleHash,
5767
+ packageContent, contextContentHash, evidenceRevision: current.revision,
5768
+ consentReceiptId: current.evidence.automaticDisclosure.consentReceiptId })), 'named-session-source-boundary');
5769
+ taskState = { workId: work.workId, before, bundleId, bundleHash: skill.activeBundleHash, packageContent,
5770
+ evidenceRevision: current.revision, consentReceiptId: current.evidence.automaticDisclosure.consentReceiptId,
5771
+ providerContext: { retainedContextHash: retained.contextHash, contextContentHash,
5772
+ runtimeVersion: version.slice('codex-cli '.length), requestedModel: retained.context.configuredModel,
5773
+ executedModel: null, replayMode: 'task_level' } };
5774
+ }
5775
+ catch { /* Source capture failure must not replay or rewrite a coding task. */ }
5776
+ }
5777
+ }
5778
+ try {
5779
+ const context = work ? await readCurrentRepositoryContext() : undefined;
5780
+ return await operation(context);
5781
+ }
5782
+ finally {
5783
+ taskState = null;
5784
+ }
5785
+ })),
5786
+ retainRepositoryState: async (capture) => {
5787
+ const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5788
+ if (current.evidence.automaticDisclosure?.mode !== 'customer_authorized_content') {
5789
+ return { state: 'not_authorized', acceptedLearningObservation: false };
5790
+ }
5791
+ if (!taskState)
5792
+ return { state: 'blocked', code: taskStateFailure, acceptedLearningObservation: false };
5793
+ if (current.revision !== taskState.evidenceRevision
5794
+ || current.evidence.automaticDisclosure.consentReceiptId !== taskState.consentReceiptId) {
5795
+ throw new Error('named_session_repository_state_consent_changed');
5796
+ }
5797
+ const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5798
+ if (!binding)
5799
+ throw new Error('named_session_binding_unavailable');
5800
+ const receipt = await retainNamedSessionRepositoryState({ vault, binding, capture, workId: taskState.workId,
5801
+ before: taskState.before, after: await taskSnapshot(), activeBundleId: taskState.bundleId, activeBundleHash: taskState.bundleHash,
5802
+ taskExport: { policy: current, contextBytes: await vault.getBlob(taskState.providerContext.contextContentHash),
5803
+ contextHash: taskState.providerContext.retainedContextHash } });
5804
+ return { ...receipt, providerContext: taskState.providerContext,
5805
+ packageContent: { manifestHash: taskState.packageContent.manifestHash,
5806
+ catalogHash: taskState.packageContent.catalogHash, skillsHash: taskState.packageContent.skillsHash } };
5807
+ },
5808
+ authorizeLocalWork: async () => {
5809
+ const current = await refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric);
5810
+ return writeRoots.every(root => current.tasks.writePaths.includes(`${root}/**`));
5811
+ },
5812
+ queueEvidence: async (capture) => {
5813
+ const current = await loadVerifiedWorkspacePolicy(policyPath, workspaceId);
5814
+ const binding = await vault.getProviderSessionBinding(registration.bindingId, registration.identity);
5815
+ if (!binding)
5816
+ throw new Error('named_session_binding_unavailable');
5817
+ // The existing relay rechecks current authority, disclosure limits and device binding before upload.
5818
+ return queueNamedSessionEvidence({ vault, capture, binding, policy: current });
5819
+ },
5820
+ openTransport: async () => transport, channelTransport,
5821
+ verifier: { resolvePublicKey: trust.resolvePublicKey, consume: async (id) => { if (consumed.has(id))
5822
+ return false; consumed.add(id); return true; } },
5823
+ authorizeContent: createNamedPeerContentAuthorization({ scope: repositoryRoleScope(item), diagnostics: true,
5824
+ loadCurrentPolicy: () => refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric),
5825
+ loadCurrentSourceAuthorization: () => fetchRepositorySourceAuthorization(fabric, repositoryRoleScope(item)) }) })
5826
+ .catch(error => { throw namedSessionStartupPhaseError('service', error); });
5727
5827
  }
5728
5828
  finally {
5729
5829
  try {
5730
- await vault.close();
5830
+ if (transport)
5831
+ await transport.close();
5731
5832
  }
5732
5833
  finally {
5733
- await releaseLock?.();
5734
- process.removeListener('SIGTERM', stop);
5735
- process.removeListener('SIGINT', stop);
5736
- if (childMessage)
5737
- process.removeListener('disconnect', stop);
5834
+ try {
5835
+ await vault.close();
5836
+ }
5837
+ finally {
5838
+ await releaseLock?.();
5839
+ process.removeListener('SIGTERM', stop);
5840
+ process.removeListener('SIGINT', stop);
5841
+ if (childMessage)
5842
+ process.removeListener('disconnect', stop);
5843
+ }
5738
5844
  }
5739
5845
  }
5740
5846
  }
5847
+ finally {
5848
+ await releaseStartupAdmission();
5849
+ }
5741
5850
  }
5742
5851
  async function workspaceSync(flags, positional) {
5743
5852
  const workspaceId = positional[0] || required(flags, 'workspace-id');
@@ -9162,6 +9271,40 @@ export async function run(argv) {
9162
9271
  return USAGE;
9163
9272
  if (flags.has('version') || command === 'version')
9164
9273
  return { version: VERSION };
9274
+ if (command === 'signing-recovery') {
9275
+ if (positional.length !== 1 || [...repeated.values()].some(values => values.length !== 1)
9276
+ || [...flags.keys()].some(key => !['name', 'workspace-id', 'operation-id', 'apply', 'json'].includes(key))
9277
+ || flags.has('apply') && flags.get('apply') !== true)
9278
+ throw Error('device_signing_maintenance_options_invalid');
9279
+ const home = dharmaHome(), name = required(flags, 'name'), workspaceId = required(flags, 'workspace-id');
9280
+ const controller = new AbortController(), stop = () => controller.abort();
9281
+ process.once('SIGTERM', stop);
9282
+ process.once('SIGINT', stop);
9283
+ const execute = (legacyBoundary) => runDeviceSigningMaintenanceController({ home, configPath: configPath(), name, workspaceId,
9284
+ operationId: typeof flags.get('operation-id') === 'string' ? String(flags.get('operation-id')) : undefined, apply: flags.get('apply') === true,
9285
+ readWorkspace: async () => {
9286
+ const selected = (await registry()).find(value => value.workspaceId === workspaceId);
9287
+ return selected && selected.repositoryBindingId && selected.repositoryRemoteHash ? { organizationId: selected.organizationId, workspaceId: selected.workspaceId,
9288
+ repositoryBindingId: selected.repositoryBindingId, repositoryRemoteHash: selected.repositoryRemoteHash, path: selected.path } : null;
9289
+ }, processState: legacyBoundary ? async () => { await legacyBoundary.assertHeld(); return 'stopped'; } : () => deviceTrustMaintenanceProcessState(home, name),
9290
+ legacyBoundary,
9291
+ openVault: () => openBootstrapVault({ root: resolve(home, 'vault') }), signal: controller.signal, frames: process.stdin,
9292
+ emit: value => process.stdout.write(JSON.stringify(value) + '\n') });
9293
+ try {
9294
+ return await (flags.get('apply') === true
9295
+ ? withLegacySigningMaintenanceBoundary({ home, name, signal: controller.signal, assertManagedCohort: async () => {
9296
+ const selected = (await registry()).find(value => value.workspaceId === workspaceId);
9297
+ if (!selected)
9298
+ throw Error('device_signing_maintenance_scope_unavailable');
9299
+ return assertSigningMaintenanceManagedCohort(home, selected.path);
9300
+ } }, execute) : execute());
9301
+ }
9302
+ finally {
9303
+ controller.abort();
9304
+ process.removeListener('SIGTERM', stop);
9305
+ process.removeListener('SIGINT', stop);
9306
+ }
9307
+ }
9165
9308
  if (command === 'demo' && ['connect', 'status', 'role', 'peers', 'ask', 'reply', 'inbox', 'ack', 'resume',
9166
9309
  'package', 'package-status', 'watch', 'watch-enable', 'watch-status', 'watch-disable',
9167
9310
  'signing-client-proof', 'signing-owner-proof', 'transport-connect']