@mono-agent/agent-app 0.19.1 → 0.20.0

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 (303) hide show
  1. package/README.md +304 -32
  2. package/dist/acp-bridge.d.ts.map +1 -1
  3. package/dist/acp-bridge.js +113 -54
  4. package/dist/acp-bridge.js.map +1 -1
  5. package/dist/acp-session-store.d.ts +12 -0
  6. package/dist/acp-session-store.d.ts.map +1 -0
  7. package/dist/acp-session-store.js +74 -0
  8. package/dist/acp-session-store.js.map +1 -0
  9. package/dist/agent-artifact-paths.d.ts +8 -0
  10. package/dist/agent-artifact-paths.d.ts.map +1 -0
  11. package/dist/agent-artifact-paths.js +11 -0
  12. package/dist/agent-artifact-paths.js.map +1 -0
  13. package/dist/agent-root-coordinator.d.ts +101 -0
  14. package/dist/agent-root-coordinator.d.ts.map +1 -0
  15. package/dist/agent-root-coordinator.js +470 -0
  16. package/dist/agent-root-coordinator.js.map +1 -0
  17. package/dist/app-controller-channels.d.ts +11 -1
  18. package/dist/app-controller-channels.d.ts.map +1 -1
  19. package/dist/app-controller-channels.js +145 -20
  20. package/dist/app-controller-channels.js.map +1 -1
  21. package/dist/app-controller-continuation.d.ts +2 -0
  22. package/dist/app-controller-continuation.d.ts.map +1 -1
  23. package/dist/app-controller-continuation.js.map +1 -1
  24. package/dist/app-controller-lifecycle.d.ts +5 -0
  25. package/dist/app-controller-lifecycle.d.ts.map +1 -1
  26. package/dist/app-controller-lifecycle.js +24 -5
  27. package/dist/app-controller-lifecycle.js.map +1 -1
  28. package/dist/app-controller-maintenance.d.ts +2 -0
  29. package/dist/app-controller-maintenance.d.ts.map +1 -1
  30. package/dist/app-controller-maintenance.js +32 -2
  31. package/dist/app-controller-maintenance.js.map +1 -1
  32. package/dist/app-controller-process-jobs.d.ts +46 -0
  33. package/dist/app-controller-process-jobs.d.ts.map +1 -0
  34. package/dist/app-controller-process-jobs.js +281 -0
  35. package/dist/app-controller-process-jobs.js.map +1 -0
  36. package/dist/app-controller-responder.d.ts +15 -0
  37. package/dist/app-controller-responder.d.ts.map +1 -1
  38. package/dist/app-controller-responder.js +146 -12
  39. package/dist/app-controller-responder.js.map +1 -1
  40. package/dist/app-controller-traceability.d.ts +6 -0
  41. package/dist/app-controller-traceability.d.ts.map +1 -1
  42. package/dist/app-controller-traceability.js +10 -0
  43. package/dist/app-controller-traceability.js.map +1 -1
  44. package/dist/app-controller-utils.d.ts +8 -0
  45. package/dist/app-controller-utils.d.ts.map +1 -1
  46. package/dist/app-controller-utils.js +24 -1
  47. package/dist/app-controller-utils.js.map +1 -1
  48. package/dist/app-controller.d.ts +40 -1
  49. package/dist/app-controller.d.ts.map +1 -1
  50. package/dist/app-controller.js +124 -39
  51. package/dist/app-controller.js.map +1 -1
  52. package/dist/background-log-maintenance.d.ts +85 -2
  53. package/dist/background-log-maintenance.d.ts.map +1 -1
  54. package/dist/background-log-maintenance.js +407 -136
  55. package/dist/background-log-maintenance.js.map +1 -1
  56. package/dist/background-runtime.d.ts.map +1 -1
  57. package/dist/background-runtime.js +22 -41
  58. package/dist/background-runtime.js.map +1 -1
  59. package/dist/background.d.ts +18 -26
  60. package/dist/background.d.ts.map +1 -1
  61. package/dist/background.js +83 -138
  62. package/dist/background.js.map +1 -1
  63. package/dist/channel-drivers/cron.d.ts +12 -1
  64. package/dist/channel-drivers/cron.d.ts.map +1 -1
  65. package/dist/channel-drivers/cron.js +121 -18
  66. package/dist/channel-drivers/cron.js.map +1 -1
  67. package/dist/channel-drivers/native-notify.d.ts +3 -0
  68. package/dist/channel-drivers/native-notify.d.ts.map +1 -1
  69. package/dist/channel-drivers/native-notify.js +2 -1
  70. package/dist/channel-drivers/native-notify.js.map +1 -1
  71. package/dist/channel-drivers/slack.d.ts.map +1 -1
  72. package/dist/channel-drivers/slack.js +41 -4
  73. package/dist/channel-drivers/slack.js.map +1 -1
  74. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  75. package/dist/channel-drivers/telegram.js +46 -3
  76. package/dist/channel-drivers/telegram.js.map +1 -1
  77. package/dist/channel-drivers/tui.d.ts +13 -2
  78. package/dist/channel-drivers/tui.d.ts.map +1 -1
  79. package/dist/channel-drivers/tui.js +112 -3
  80. package/dist/channel-drivers/tui.js.map +1 -1
  81. package/dist/channels.d.ts +1 -1
  82. package/dist/channels.d.ts.map +1 -1
  83. package/dist/channels.js +4 -2
  84. package/dist/channels.js.map +1 -1
  85. package/dist/cli-args.d.ts +15 -1
  86. package/dist/cli-args.d.ts.map +1 -1
  87. package/dist/cli-args.js +82 -12
  88. package/dist/cli-args.js.map +1 -1
  89. package/dist/cli-background-command.d.ts +9 -2
  90. package/dist/cli-background-command.d.ts.map +1 -1
  91. package/dist/cli-background-command.js +174 -66
  92. package/dist/cli-background-command.js.map +1 -1
  93. package/dist/cli-commands.d.ts.map +1 -1
  94. package/dist/cli-commands.js +50 -12
  95. package/dist/cli-commands.js.map +1 -1
  96. package/dist/cli-help.d.ts.map +1 -1
  97. package/dist/cli-help.js +21 -1
  98. package/dist/cli-help.js.map +1 -1
  99. package/dist/config-reference.d.ts.map +1 -1
  100. package/dist/config-reference.js +99 -2
  101. package/dist/config-reference.js.map +1 -1
  102. package/dist/configured-agent.d.ts +44 -3
  103. package/dist/configured-agent.d.ts.map +1 -1
  104. package/dist/configured-agent.js +805 -156
  105. package/dist/configured-agent.js.map +1 -1
  106. package/dist/conversation-state-roots.d.ts +59 -0
  107. package/dist/conversation-state-roots.d.ts.map +1 -0
  108. package/dist/conversation-state-roots.js +248 -0
  109. package/dist/conversation-state-roots.js.map +1 -0
  110. package/dist/cron-control-store.d.ts +91 -0
  111. package/dist/cron-control-store.d.ts.map +1 -0
  112. package/dist/cron-control-store.js +1251 -0
  113. package/dist/cron-control-store.js.map +1 -0
  114. package/dist/cron-operator-service.d.ts +54 -0
  115. package/dist/cron-operator-service.d.ts.map +1 -0
  116. package/dist/cron-operator-service.js +352 -0
  117. package/dist/cron-operator-service.js.map +1 -0
  118. package/dist/doctor-session-history.d.ts +9 -0
  119. package/dist/doctor-session-history.d.ts.map +1 -0
  120. package/dist/doctor-session-history.js +313 -0
  121. package/dist/doctor-session-history.js.map +1 -0
  122. package/dist/doctor.d.ts +2 -1
  123. package/dist/doctor.d.ts.map +1 -1
  124. package/dist/doctor.js +320 -9
  125. package/dist/doctor.js.map +1 -1
  126. package/dist/index.d.ts +4 -0
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +2 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/interaction-bridge.d.ts +1 -0
  131. package/dist/interaction-bridge.d.ts.map +1 -1
  132. package/dist/interaction-bridge.js +54 -6
  133. package/dist/interaction-bridge.js.map +1 -1
  134. package/dist/jobs-command.d.ts +17 -0
  135. package/dist/jobs-command.d.ts.map +1 -0
  136. package/dist/jobs-command.js +207 -0
  137. package/dist/jobs-command.js.map +1 -0
  138. package/dist/launchd-lifecycle-lock.d.ts +36 -0
  139. package/dist/launchd-lifecycle-lock.d.ts.map +1 -0
  140. package/dist/launchd-lifecycle-lock.js +124 -0
  141. package/dist/launchd-lifecycle-lock.js.map +1 -0
  142. package/dist/launchd-log-monitor-status.d.ts +7 -0
  143. package/dist/launchd-log-monitor-status.d.ts.map +1 -0
  144. package/dist/launchd-log-monitor-status.js +224 -0
  145. package/dist/launchd-log-monitor-status.js.map +1 -0
  146. package/dist/launchd-logs.d.ts +9 -1
  147. package/dist/launchd-logs.d.ts.map +1 -1
  148. package/dist/launchd-logs.js +69 -16
  149. package/dist/launchd-logs.js.map +1 -1
  150. package/dist/launchd-maintenance-command.d.ts +10 -0
  151. package/dist/launchd-maintenance-command.d.ts.map +1 -0
  152. package/dist/launchd-maintenance-command.js +2 -0
  153. package/dist/launchd-maintenance-command.js.map +1 -0
  154. package/dist/launchd-maintenance-entry.d.ts +30 -0
  155. package/dist/launchd-maintenance-entry.d.ts.map +1 -0
  156. package/dist/launchd-maintenance-entry.js +206 -0
  157. package/dist/launchd-maintenance-entry.js.map +1 -0
  158. package/dist/launchd-maintenance-gate.d.ts +28 -0
  159. package/dist/launchd-maintenance-gate.d.ts.map +1 -0
  160. package/dist/launchd-maintenance-gate.js +80 -0
  161. package/dist/launchd-maintenance-gate.js.map +1 -0
  162. package/dist/launchd-private-files.d.ts +3 -0
  163. package/dist/launchd-private-files.d.ts.map +1 -0
  164. package/dist/launchd-private-files.js +58 -0
  165. package/dist/launchd-private-files.js.map +1 -0
  166. package/dist/launchd.d.ts +56 -2
  167. package/dist/launchd.d.ts.map +1 -1
  168. package/dist/launchd.js +210 -5
  169. package/dist/launchd.js.map +1 -1
  170. package/dist/local-configuration.d.ts.map +1 -1
  171. package/dist/local-configuration.js +33 -7
  172. package/dist/local-configuration.js.map +1 -1
  173. package/dist/managed-launchd-maintenance-environment.d.ts +3 -0
  174. package/dist/managed-launchd-maintenance-environment.d.ts.map +1 -0
  175. package/dist/managed-launchd-maintenance-environment.js +12 -0
  176. package/dist/managed-launchd-maintenance-environment.js.map +1 -0
  177. package/dist/managed-runtime-launch-proof.d.ts +11 -0
  178. package/dist/managed-runtime-launch-proof.d.ts.map +1 -0
  179. package/dist/managed-runtime-launch-proof.js +59 -0
  180. package/dist/managed-runtime-launch-proof.js.map +1 -0
  181. package/dist/managed-runtime-maintenance-entry.d.ts +10 -0
  182. package/dist/managed-runtime-maintenance-entry.d.ts.map +1 -0
  183. package/dist/managed-runtime-maintenance-entry.js +244 -0
  184. package/dist/managed-runtime-maintenance-entry.js.map +1 -0
  185. package/dist/managed-web-logs.d.ts +29 -11
  186. package/dist/managed-web-logs.d.ts.map +1 -1
  187. package/dist/managed-web-logs.js +244 -121
  188. package/dist/managed-web-logs.js.map +1 -1
  189. package/dist/managed-web-maintenance-environment.d.ts +6 -0
  190. package/dist/managed-web-maintenance-environment.d.ts.map +1 -0
  191. package/dist/managed-web-maintenance-environment.js +19 -0
  192. package/dist/managed-web-maintenance-environment.js.map +1 -0
  193. package/dist/mcp-apps.d.ts +37 -0
  194. package/dist/mcp-apps.d.ts.map +1 -0
  195. package/dist/mcp-apps.js +1753 -0
  196. package/dist/mcp-apps.js.map +1 -0
  197. package/dist/memory-command.d.ts +12 -0
  198. package/dist/memory-command.d.ts.map +1 -1
  199. package/dist/memory-command.js +510 -1
  200. package/dist/memory-command.js.map +1 -1
  201. package/dist/modules/known-tools.d.ts +1 -1
  202. package/dist/modules/known-tools.d.ts.map +1 -1
  203. package/dist/modules/known-tools.js +2 -0
  204. package/dist/modules/known-tools.js.map +1 -1
  205. package/dist/posted-reply-history.d.ts.map +1 -1
  206. package/dist/posted-reply-history.js +12 -0
  207. package/dist/posted-reply-history.js.map +1 -1
  208. package/dist/proactive-notify.d.ts +4 -1
  209. package/dist/proactive-notify.d.ts.map +1 -1
  210. package/dist/proactive-notify.js +43 -5
  211. package/dist/proactive-notify.js.map +1 -1
  212. package/dist/process-jobs-config.d.ts +82 -0
  213. package/dist/process-jobs-config.d.ts.map +1 -0
  214. package/dist/process-jobs-config.js +423 -0
  215. package/dist/process-jobs-config.js.map +1 -0
  216. package/dist/process-jobs-context.d.ts +24 -0
  217. package/dist/process-jobs-context.d.ts.map +1 -0
  218. package/dist/process-jobs-context.js +135 -0
  219. package/dist/process-jobs-context.js.map +1 -0
  220. package/dist/process-jobs-root-registry.d.ts +115 -0
  221. package/dist/process-jobs-root-registry.d.ts.map +1 -0
  222. package/dist/process-jobs-root-registry.js +978 -0
  223. package/dist/process-jobs-root-registry.js.map +1 -0
  224. package/dist/process-jobs-runtime.d.ts +35 -0
  225. package/dist/process-jobs-runtime.d.ts.map +1 -0
  226. package/dist/process-jobs-runtime.js +177 -0
  227. package/dist/process-jobs-runtime.js.map +1 -0
  228. package/dist/process-jobs-service.d.ts +70 -0
  229. package/dist/process-jobs-service.d.ts.map +1 -0
  230. package/dist/process-jobs-service.js +1989 -0
  231. package/dist/process-jobs-service.js.map +1 -0
  232. package/dist/process-jobs-store.d.ts +151 -0
  233. package/dist/process-jobs-store.d.ts.map +1 -0
  234. package/dist/process-jobs-store.js +1290 -0
  235. package/dist/process-jobs-store.js.map +1 -0
  236. package/dist/reply-artifacts.d.ts +65 -0
  237. package/dist/reply-artifacts.d.ts.map +1 -0
  238. package/dist/reply-artifacts.js +1147 -0
  239. package/dist/reply-artifacts.js.map +1 -0
  240. package/dist/reply-part-budget.d.ts +16 -0
  241. package/dist/reply-part-budget.d.ts.map +1 -0
  242. package/dist/reply-part-budget.js +57 -0
  243. package/dist/reply-part-budget.js.map +1 -0
  244. package/dist/request-model-override.d.ts +19 -0
  245. package/dist/request-model-override.d.ts.map +1 -1
  246. package/dist/request-model-override.js +54 -1
  247. package/dist/request-model-override.js.map +1 -1
  248. package/dist/request-scoped-mcp.d.ts +31 -0
  249. package/dist/request-scoped-mcp.d.ts.map +1 -0
  250. package/dist/request-scoped-mcp.js +197 -0
  251. package/dist/request-scoped-mcp.js.map +1 -0
  252. package/dist/run-history.d.ts.map +1 -1
  253. package/dist/run-history.js +50 -254
  254. package/dist/run-history.js.map +1 -1
  255. package/dist/runtime-option-extensions.d.ts +21 -0
  256. package/dist/runtime-option-extensions.d.ts.map +1 -1
  257. package/dist/runtime-option-extensions.js +53 -0
  258. package/dist/runtime-option-extensions.js.map +1 -1
  259. package/dist/secure-file-replace.d.ts +3 -0
  260. package/dist/secure-file-replace.d.ts.map +1 -1
  261. package/dist/secure-file-replace.js +31 -7
  262. package/dist/secure-file-replace.js.map +1 -1
  263. package/dist/session-history.d.ts +146 -0
  264. package/dist/session-history.d.ts.map +1 -0
  265. package/dist/session-history.js +347 -0
  266. package/dist/session-history.js.map +1 -0
  267. package/dist/sessions.d.ts +45 -2
  268. package/dist/sessions.d.ts.map +1 -1
  269. package/dist/sessions.js +849 -30
  270. package/dist/sessions.js.map +1 -1
  271. package/dist/web-command.d.ts +12 -11
  272. package/dist/web-command.d.ts.map +1 -1
  273. package/dist/web-command.js +554 -80
  274. package/dist/web-command.js.map +1 -1
  275. package/dist/web-log-maintenance-command.d.ts +7 -0
  276. package/dist/web-log-maintenance-command.d.ts.map +1 -0
  277. package/dist/web-log-maintenance-command.js +21 -0
  278. package/dist/web-log-maintenance-command.js.map +1 -0
  279. package/dist/web-log-maintenance-status.d.ts +26 -0
  280. package/dist/web-log-maintenance-status.d.ts.map +1 -0
  281. package/dist/web-log-maintenance-status.js +171 -0
  282. package/dist/web-log-maintenance-status.js.map +1 -0
  283. package/dist/web-log-maintenance.d.ts +32 -0
  284. package/dist/web-log-maintenance.d.ts.map +1 -0
  285. package/dist/web-log-maintenance.js +163 -0
  286. package/dist/web-log-maintenance.js.map +1 -0
  287. package/dist/web-maintenance-paths.d.ts +12 -0
  288. package/dist/web-maintenance-paths.d.ts.map +1 -0
  289. package/dist/web-maintenance-paths.js +24 -0
  290. package/dist/web-maintenance-paths.js.map +1 -0
  291. package/dist/wizard/prompts.d.ts.map +1 -1
  292. package/dist/wizard/prompts.js +1 -0
  293. package/dist/wizard/prompts.js.map +1 -1
  294. package/dist/wizard/run.js +1 -1
  295. package/dist/wizard/run.js.map +1 -1
  296. package/package.json +15 -15
  297. package/schema/mono-agent.config.schema.json +102 -1
  298. package/skills/mono-agent-composer/references/config-blueprint.md +26 -2
  299. package/skills/mono-agent-composer/references/discovery-questions.md +2 -2
  300. package/skills/mono-agent-composer/references/feature-coverage.md +6 -4
  301. package/skills/mono-agent-composer/references/package-map.md +19 -3
  302. package/skills/mono-agent-composer/references/playbooks.md +3 -3
  303. package/skills/mono-agent-composer/references/validation.md +4 -2
@@ -0,0 +1,978 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstatSync, realpathSync } from "node:fs";
3
+ import { chmod, link, lstat, mkdir, opendir, realpath, rmdir, unlink } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import process from "node:process";
6
+ import {} from "./agent-root-coordinator.js";
7
+ import { clearSessionsRegistryRoot } from "./conversation-state-roots.js";
8
+ import { acquireOwnerPrivateLock } from "./owner-private-lock.js";
9
+ import { readVerifiedFile, secureFileReplace } from "./secure-file-replace.js";
10
+ import { syncDirectory } from "./continuation-store-fs.js";
11
+ export const PROCESS_JOBS_ROOT_REGISTRY_SCHEMA = "mono-agent.process-jobs-roots.v1";
12
+ export const PROCESS_JOBS_ROOT_REGISTRY_DIRECTORY = "process-jobs-roots-v1";
13
+ export const PROCESS_JOBS_ROOT_REGISTRY_FILE = "registry.json";
14
+ export const PROCESS_JOBS_ROOT_REGISTRY_RECOVERY_DIRECTORY = "process-jobs-roots-v1.recovery";
15
+ export const PROCESS_JOBS_ROOT_REGISTRY_STAGING_FILE = "registry.staging.json";
16
+ export const PROCESS_JOBS_ROOT_REGISTRY_PREVIOUS_FILE = "registry.previous.json";
17
+ export const PROCESS_JOBS_ROOT_REGISTRY_FAILED_FILE = "registry.failed.json";
18
+ export const PROCESS_JOBS_ROOT_REGISTRY_MAX_RECOVERY_ENTRIES = 3;
19
+ export const PROCESS_JOBS_ROOT_REGISTRY_MAX_ROOTS = 64;
20
+ export const PROCESS_JOBS_ROOT_REGISTRY_MAX_SEGMENTS = 64;
21
+ export const PROCESS_JOBS_ROOT_REGISTRY_MAX_SEGMENT_BYTES = 255;
22
+ export const PROCESS_JOBS_ROOT_REGISTRY_MAX_ROOT_BYTES = 2 * 1024;
23
+ export const PROCESS_JOBS_ROOT_REGISTRY_MAX_BYTES = 256 * 1024;
24
+ export const PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR = "Process-job private-state protection is unavailable.";
25
+ const EMPTY_GENERATION_ID = "mono-agent.process-jobs-roots.absent";
26
+ const REGISTRY_LOCK_SCHEMA = "mono-agent.process-jobs-roots-lock.v1";
27
+ const REGISTRY_LOCK_OWNERLESS_GRACE_MS = 1_000;
28
+ const REGISTRATION_PROOF = Symbol("process-jobs-root-registration-proof");
29
+ export function processJobsRootRegistryPaths(agentRoot) {
30
+ const managedRoot = resolve(agentRoot, ".mono-agent");
31
+ const registryDir = join(managedRoot, PROCESS_JOBS_ROOT_REGISTRY_DIRECTORY);
32
+ const recoveryDir = join(managedRoot, PROCESS_JOBS_ROOT_REGISTRY_RECOVERY_DIRECTORY);
33
+ return {
34
+ registryDir,
35
+ recoveryDir,
36
+ manifestPath: join(registryDir, PROCESS_JOBS_ROOT_REGISTRY_FILE),
37
+ stagingPath: join(recoveryDir, PROCESS_JOBS_ROOT_REGISTRY_STAGING_FILE),
38
+ previousPath: join(recoveryDir, PROCESS_JOBS_ROOT_REGISTRY_PREVIOUS_FILE),
39
+ failedPath: join(recoveryDir, PROCESS_JOBS_ROOT_REGISTRY_FAILED_FILE),
40
+ mutationLockPath: join(managedRoot, `.${PROCESS_JOBS_ROOT_REGISTRY_DIRECTORY}.lock`),
41
+ };
42
+ }
43
+ /** Load a strict snapshot; malformed or unsafe durable state becomes a provider-zero state. */
44
+ export async function loadProcessJobsRootRegistryProtection(agentRoot, workspace) {
45
+ const canonicalRoot = await canonicalAgentRoot(agentRoot);
46
+ try {
47
+ return await loadStrict(canonicalRoot, workspace);
48
+ }
49
+ catch {
50
+ return failedProcessJobsRootRegistryProtection(canonicalRoot);
51
+ }
52
+ }
53
+ export function failedProcessJobsRootRegistryProtection(canonicalAgentRoot) {
54
+ const agentRoot = resolve(canonicalAgentRoot);
55
+ const paths = processJobsRootRegistryPaths(agentRoot);
56
+ return {
57
+ kind: "failed",
58
+ agentRoot,
59
+ ...paths,
60
+ generation: Object.freeze({
61
+ id: "mono-agent.process-jobs-roots.failed",
62
+ rootKeys: Object.freeze([]),
63
+ }),
64
+ roots: [],
65
+ protectedRoots: Object.freeze([paths.registryDir, paths.recoveryDir, paths.mutationLockPath]),
66
+ error: PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR,
67
+ };
68
+ }
69
+ /** Re-read and identity-prove the captured manifest at a request/destructive boundary. */
70
+ export async function attestProcessJobsRootRegistrySnapshot(snapshot, workspace) {
71
+ if (snapshot.kind === "failed")
72
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
73
+ const current = await loadStrict(snapshot.agentRoot, workspace);
74
+ if (snapshot.kind === "empty") {
75
+ if (current.kind !== "empty")
76
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
77
+ return current;
78
+ }
79
+ if (current.kind !== "ready"
80
+ || current.generation.id !== snapshot.generation.id
81
+ || !sameStringArrays(current.generation.rootKeys, snapshot.generation.rootKeys)
82
+ || current.manifestIdentity.dev !== snapshot.manifestIdentity.dev
83
+ || current.manifestIdentity.ino !== snapshot.manifestIdentity.ino
84
+ || !current.manifestContents.equals(snapshot.manifestContents)) {
85
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
86
+ }
87
+ return current;
88
+ }
89
+ /** Fsync a monotonic registration before any ProcessJobs root creation/open. */
90
+ export async function registerProcessJobsRoot(options) {
91
+ const agentRoot = await canonicalAgentRoot(options.agentRoot);
92
+ const rootSegments = relativeSegments(agentRoot, options.stateDir);
93
+ const rootKey = rootSegments.join("/");
94
+ const paths = processJobsRootRegistryPaths(agentRoot);
95
+ // Reject an unsafe candidate before publishing it into the monotonic
96
+ // manifest; a bad config must fail closed without permanently poisoning an
97
+ // otherwise valid registry generation.
98
+ validateRegisteredRoot({
99
+ agentRoot,
100
+ workspace: options.workspace,
101
+ registryDir: paths.registryDir,
102
+ recoveryDir: paths.recoveryDir,
103
+ mutationLockPath: paths.mutationLockPath,
104
+ segments: rootSegments,
105
+ });
106
+ await ensurePrivateDirectory(dirname(paths.registryDir), "Process-job registry parent");
107
+ const lock = await acquireRegistryLock(paths.mutationLockPath);
108
+ if (lock === undefined)
109
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
110
+ try {
111
+ await ensureRecoveryDirectory(paths);
112
+ await recoverRegistryArtifacts(paths, options.hooks);
113
+ // A first-registration crash may leave only the durable directory. Strict
114
+ // request readers remain provider-zero; the mutation-locked writer alone
115
+ // may remove that exact empty artifact before retrying publication.
116
+ await removeExactEmptyRegistryDirectory(paths.registryDir);
117
+ let current = await loadStrict(agentRoot, options.workspace);
118
+ if (current.kind === "ready" && current.generation.rootKeys.includes(rootKey)) {
119
+ options.coordinator.publishGeneration(current.generation);
120
+ return proof(current, rootKey);
121
+ }
122
+ const roots = [
123
+ ...(current.kind === "ready" ? current.roots.map((root) => root.segments) : []),
124
+ rootSegments,
125
+ ].sort(compareSegments);
126
+ if (roots.length > PROCESS_JOBS_ROOT_REGISTRY_MAX_ROOTS) {
127
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
128
+ }
129
+ const manifest = {
130
+ schema: PROCESS_JOBS_ROOT_REGISTRY_SCHEMA,
131
+ generation: randomUUID(),
132
+ roots,
133
+ };
134
+ const contents = encodeManifest(manifest);
135
+ if (current.kind === "empty") {
136
+ await ensurePrivateDirectory(paths.registryDir, "Process-job registry");
137
+ await options.hooks?.afterRegistryDirectoryCreated?.();
138
+ await replaceManifest(paths, contents, undefined, options.hooks);
139
+ }
140
+ else {
141
+ await replaceManifest(paths, contents, current, options.hooks);
142
+ }
143
+ await syncDirectory(paths.registryDir);
144
+ await syncDirectory(dirname(paths.registryDir));
145
+ current = await loadStrict(agentRoot, options.workspace);
146
+ if (current.kind !== "ready"
147
+ || current.generation.id !== manifest.generation
148
+ || !current.generation.rootKeys.includes(rootKey)) {
149
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
150
+ }
151
+ options.coordinator.publishGeneration(current.generation);
152
+ return proof(current, rootKey);
153
+ }
154
+ catch (error) {
155
+ try {
156
+ await recoverRegistryArtifacts(paths, options.hooks);
157
+ await removeExactEmptyRegistryDirectory(paths.registryDir);
158
+ }
159
+ catch (cleanupError) {
160
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR, {
161
+ cause: new AggregateError([error, cleanupError], "Registry publication and empty-directory cleanup failed."),
162
+ });
163
+ }
164
+ if (error instanceof Error && error.message === PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR)
165
+ throw error;
166
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR, { cause: error });
167
+ }
168
+ finally {
169
+ await lock.release().catch(() => undefined);
170
+ }
171
+ }
172
+ /** Hold the registry mutation lock across clear-sessions preflight and first rename. */
173
+ export async function freezeProcessJobsRootRegistry(agentRoot, workspace) {
174
+ const canonicalRoot = await canonicalAgentRoot(agentRoot);
175
+ const paths = processJobsRootRegistryPaths(canonicalRoot);
176
+ await ensurePrivateDirectory(dirname(paths.registryDir), "Process-job registry parent");
177
+ const lock = await acquireRegistryLock(paths.mutationLockPath);
178
+ if (lock === undefined)
179
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
180
+ try {
181
+ await recoverRegistryArtifacts(paths);
182
+ await removeExactEmptyRegistryDirectory(paths.registryDir);
183
+ const snapshot = await loadStrict(canonicalRoot, workspace);
184
+ return {
185
+ snapshot,
186
+ reattest: async (nextWorkspace) => await attestProcessJobsRootRegistrySnapshot(snapshot, nextWorkspace),
187
+ release: async () => await lock.release(),
188
+ };
189
+ }
190
+ catch (error) {
191
+ await lock.release().catch(() => undefined);
192
+ throw error;
193
+ }
194
+ }
195
+ /** Prove a supplied state directory remains in the exact registered generation. */
196
+ export async function attestProcessJobsRootRegistration(proofValue, workspace, stateDir) {
197
+ if (proofValue?.[REGISTRATION_PROOF] !== true) {
198
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
199
+ }
200
+ const current = await attestProcessJobsRootRegistrySnapshot(proofValue.snapshot, workspace);
201
+ if (current.kind !== "ready")
202
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
203
+ const key = relativeSegments(current.agentRoot, stateDir).join("/");
204
+ if (key !== proofValue.rootKey || !current.generation.rootKeys.includes(key)) {
205
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
206
+ }
207
+ return current;
208
+ }
209
+ /** All state roots must remain disjoint from every destructive/control path. */
210
+ export function assertProcessJobsRegistryDisjointFromPaths(snapshot, paths) {
211
+ if (snapshot.kind === "empty")
212
+ return;
213
+ const privatePaths = [snapshot.registryDir, snapshot.recoveryDir, snapshot.mutationLockPath,
214
+ ...snapshot.roots.flatMap((root) => root.protectedPaths)];
215
+ for (const privatePath of privatePaths) {
216
+ for (const candidate of paths) {
217
+ if (pathsOverlap(resolve(privatePath), resolve(candidate))) {
218
+ throw new Error("restart --clear-sessions overlaps retained process-job private state; nothing was deleted.");
219
+ }
220
+ }
221
+ }
222
+ }
223
+ export function processJobsProtectionPolicyRoots(snapshot) {
224
+ return snapshot.kind === "empty" ? [] : snapshot.protectedRoots;
225
+ }
226
+ export function processJobsRootForKey(snapshot, key) {
227
+ return snapshot.kind === "ready" ? snapshot.roots.find((root) => root.key === key) : undefined;
228
+ }
229
+ export function processJobsRegistryGeneration(snapshot) {
230
+ return snapshot.generation;
231
+ }
232
+ async function loadStrict(agentRoot, workspace) {
233
+ const paths = processJobsRootRegistryPaths(agentRoot);
234
+ const target = await inspectRegistryTarget(paths);
235
+ const recovery = await scanRecoveryArtifacts(paths, target.directoryDetails);
236
+ if (hasRecoveryArtifacts(recovery))
237
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
238
+ if (target.directoryDetails === undefined) {
239
+ return {
240
+ kind: "empty",
241
+ agentRoot,
242
+ ...paths,
243
+ generation: Object.freeze({ id: EMPTY_GENERATION_ID, rootKeys: Object.freeze([]) }),
244
+ roots: [],
245
+ protectedRoots: [],
246
+ };
247
+ }
248
+ const read = target.current;
249
+ if (read === undefined)
250
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
251
+ const manifest = read.manifest;
252
+ const roots = manifest.roots.map((segments) => validateRegisteredRoot({
253
+ agentRoot,
254
+ workspace,
255
+ registryDir: paths.registryDir,
256
+ recoveryDir: paths.recoveryDir,
257
+ mutationLockPath: paths.mutationLockPath,
258
+ segments,
259
+ }));
260
+ const rootKeys = roots.map((root) => root.key);
261
+ const protectedRoots = uniquePaths([
262
+ paths.registryDir,
263
+ paths.recoveryDir,
264
+ paths.mutationLockPath,
265
+ ...roots.flatMap((root) => root.protectedPaths),
266
+ ]);
267
+ return {
268
+ kind: "ready",
269
+ agentRoot,
270
+ ...paths,
271
+ generation: Object.freeze({ id: manifest.generation, rootKeys: Object.freeze(rootKeys) }),
272
+ roots: Object.freeze(roots),
273
+ protectedRoots: Object.freeze(protectedRoots),
274
+ manifestIdentity: { dev: read.details.dev, ino: read.details.ino },
275
+ manifestContents: Buffer.from(read.contents),
276
+ };
277
+ }
278
+ async function inspectRegistryTarget(paths) {
279
+ return await inspectRegistryTargetWithReader(paths, readProvenManifest);
280
+ }
281
+ async function inspectRegistryTargetForRecovery(paths) {
282
+ return await inspectRegistryTargetWithReader(paths, readRecoveryPairCandidateManifest);
283
+ }
284
+ async function inspectRegistryTargetWithReader(paths, readCurrent) {
285
+ let before;
286
+ try {
287
+ before = await lstat(paths.registryDir, { bigint: true });
288
+ }
289
+ catch (error) {
290
+ if (!isErrno(error, "ENOENT"))
291
+ throw error;
292
+ if (await readManifest(paths.manifestPath) !== undefined) {
293
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
294
+ }
295
+ return {};
296
+ }
297
+ assertPrivateDirectory(before, "Process-job registry");
298
+ if (await realpath(paths.registryDir) !== paths.registryDir) {
299
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
300
+ }
301
+ const directory = await opendir(paths.registryDir);
302
+ let entry;
303
+ try {
304
+ entry = await directory.read();
305
+ if (entry !== null && (entry.name !== PROCESS_JOBS_ROOT_REGISTRY_FILE
306
+ || !entry.isFile() || entry.isSymbolicLink())) {
307
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
308
+ }
309
+ if (await directory.read() !== null)
310
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
311
+ }
312
+ finally {
313
+ await directory.close();
314
+ }
315
+ const current = entry === null ? undefined : await readCurrent(paths.manifestPath);
316
+ const after = await lstat(paths.registryDir, { bigint: true });
317
+ assertPrivateDirectory(after, "Process-job registry");
318
+ if (!sameFileIdentity(before, after))
319
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
320
+ return { directoryDetails: after, ...(current === undefined ? {} : { current }) };
321
+ }
322
+ async function scanRecoveryArtifacts(paths, registryDetails) {
323
+ return await scanRecoveryArtifactsWithPreviousReader(paths, registryDetails, readProvenManifest);
324
+ }
325
+ async function scanRecoveryArtifactsForRecovery(paths, registryDetails) {
326
+ return await scanRecoveryArtifactsWithPreviousReader(paths, registryDetails, readRecoveryPairCandidateManifest);
327
+ }
328
+ async function scanRecoveryArtifactsWithPreviousReader(paths, registryDetails, readPrevious) {
329
+ let before;
330
+ try {
331
+ before = await lstat(paths.recoveryDir, { bigint: true });
332
+ }
333
+ catch (error) {
334
+ if (isErrno(error, "ENOENT"))
335
+ return {};
336
+ throw error;
337
+ }
338
+ assertRecoveryDirectory(before);
339
+ if (await realpath(paths.recoveryDir) !== paths.recoveryDir) {
340
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
341
+ }
342
+ const filesystemPeer = registryDetails ?? await lstat(dirname(paths.recoveryDir), { bigint: true });
343
+ assertPrivateDirectory(filesystemPeer, "Process-job registry filesystem peer");
344
+ if (before.dev !== filesystemPeer.dev)
345
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
346
+ let staging;
347
+ let previous;
348
+ let failed;
349
+ let count = 0;
350
+ const directory = await opendir(paths.recoveryDir);
351
+ try {
352
+ for (;;) {
353
+ const entry = await directory.read();
354
+ if (entry === null)
355
+ break;
356
+ count += 1;
357
+ if (count > PROCESS_JOBS_ROOT_REGISTRY_MAX_RECOVERY_ENTRIES) {
358
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
359
+ }
360
+ if (!entry.isFile() || entry.isSymbolicLink()) {
361
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
362
+ }
363
+ if (entry.name === PROCESS_JOBS_ROOT_REGISTRY_STAGING_FILE) {
364
+ staging = await readProvenManifest(paths.stagingPath);
365
+ }
366
+ else if (entry.name === PROCESS_JOBS_ROOT_REGISTRY_PREVIOUS_FILE) {
367
+ previous = await readPrevious(paths.previousPath);
368
+ }
369
+ else if (entry.name === PROCESS_JOBS_ROOT_REGISTRY_FAILED_FILE) {
370
+ failed = await readProvenManifest(paths.failedPath);
371
+ }
372
+ else {
373
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
374
+ }
375
+ }
376
+ }
377
+ finally {
378
+ await directory.close();
379
+ }
380
+ const after = await lstat(paths.recoveryDir, { bigint: true });
381
+ assertRecoveryDirectory(after);
382
+ if (!sameFileIdentity(before, after))
383
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
384
+ return {
385
+ ...(staging === undefined ? {} : { staging }),
386
+ ...(previous === undefined ? {} : { previous }),
387
+ ...(failed === undefined ? {} : { failed }),
388
+ };
389
+ }
390
+ async function readProvenManifest(path) {
391
+ const read = await readManifest(path);
392
+ if (read === undefined)
393
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
394
+ return { path, manifest: parseManifest(read.contents), contents: read.contents, details: read.details };
395
+ }
396
+ async function readRecoveryPairCandidateManifest(path) {
397
+ const read = await readVerifiedFile(path, {
398
+ validate: (details) => {
399
+ if (!details.isFile() || details.isSymbolicLink()
400
+ || (details.nlink !== 1n && details.nlink !== 2n)
401
+ || details.size > BigInt(PROCESS_JOBS_ROOT_REGISTRY_MAX_BYTES)) {
402
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
403
+ }
404
+ if (typeof process.getuid === "function" && details.uid !== BigInt(process.getuid())) {
405
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
406
+ }
407
+ if ((details.mode & 511n) !== 384n)
408
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
409
+ },
410
+ changedError: () => new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR),
411
+ });
412
+ if (read === undefined)
413
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
414
+ return { path, manifest: parseManifest(read.contents), contents: read.contents, details: read.details };
415
+ }
416
+ function hasRecoveryArtifacts(artifacts) {
417
+ return artifacts.staging !== undefined || artifacts.previous !== undefined || artifacts.failed !== undefined;
418
+ }
419
+ async function readManifest(path) {
420
+ return await readVerifiedFile(path, {
421
+ validate: (details) => {
422
+ if (!details.isFile() || details.isSymbolicLink() || details.nlink !== 1n
423
+ || details.size > BigInt(PROCESS_JOBS_ROOT_REGISTRY_MAX_BYTES)) {
424
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
425
+ }
426
+ if (typeof process.getuid === "function" && details.uid !== BigInt(process.getuid())) {
427
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
428
+ }
429
+ if ((details.mode & 511n) !== 384n)
430
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
431
+ },
432
+ changedError: () => new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR),
433
+ });
434
+ }
435
+ function parseManifest(contents) {
436
+ if (contents.byteLength < 1 || contents.byteLength > PROCESS_JOBS_ROOT_REGISTRY_MAX_BYTES) {
437
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
438
+ }
439
+ let value;
440
+ try {
441
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(contents));
442
+ }
443
+ catch {
444
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
445
+ }
446
+ if (!isRecord(value)
447
+ || !hasExactKeys(value, ["schema", "generation", "roots"])
448
+ || value.schema !== PROCESS_JOBS_ROOT_REGISTRY_SCHEMA
449
+ || typeof value.generation !== "string"
450
+ || !isUuid(value.generation)
451
+ || !Array.isArray(value.roots)
452
+ || value.roots.length < 1
453
+ || value.roots.length > PROCESS_JOBS_ROOT_REGISTRY_MAX_ROOTS) {
454
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
455
+ }
456
+ const roots = value.roots.map(validateSegments);
457
+ const sorted = [...roots].sort(compareSegments);
458
+ if (!sameSegmentRoots(roots, sorted)
459
+ || new Set(roots.map((root) => root.join("/"))).size !== roots.length) {
460
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
461
+ }
462
+ return { schema: PROCESS_JOBS_ROOT_REGISTRY_SCHEMA, generation: value.generation, roots };
463
+ }
464
+ function validateSegments(value) {
465
+ if (!Array.isArray(value) || value.length < 1 || value.length > PROCESS_JOBS_ROOT_REGISTRY_MAX_SEGMENTS) {
466
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
467
+ }
468
+ const segments = value.map((segment) => {
469
+ if (typeof segment !== "string"
470
+ || segment.length === 0
471
+ || segment === "."
472
+ || segment === ".."
473
+ || segment.includes("/")
474
+ || segment.includes("\\")
475
+ || segment.includes("\0")
476
+ || Buffer.byteLength(segment, "utf8") > PROCESS_JOBS_ROOT_REGISTRY_MAX_SEGMENT_BYTES) {
477
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
478
+ }
479
+ return segment;
480
+ });
481
+ if (Buffer.byteLength(segments.join("/"), "utf8") > PROCESS_JOBS_ROOT_REGISTRY_MAX_ROOT_BYTES) {
482
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
483
+ }
484
+ return Object.freeze(segments);
485
+ }
486
+ function validateRegisteredRoot(input) {
487
+ const lexicalPath = resolve(input.agentRoot, ...input.segments);
488
+ const lexicalWorkspace = resolve(input.workspace);
489
+ const canonicalWorkspace = realpathSync(lexicalWorkspace);
490
+ const key = input.segments.join("/");
491
+ // Reject the configured spelling before touching any candidate component.
492
+ // Keep this recoveryDir branch distinct from the canonical check below:
493
+ // case-folding and other non-symlink filesystem aliases need both proofs.
494
+ if (!strictDescendant(input.agentRoot, lexicalPath)
495
+ || pathsOverlap(lexicalPath, input.registryDir)
496
+ || pathsOverlap(lexicalPath, input.recoveryDir)
497
+ || pathsOverlap(lexicalPath, input.mutationLockPath)
498
+ || pathsOverlap(lexicalPath, clearSessionsRegistryRoot(input.agentRoot))
499
+ || stateRootContainsWorkspace(lexicalPath, lexicalWorkspace, canonicalWorkspace)) {
500
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
501
+ }
502
+ let current = input.agentRoot;
503
+ let canonicalPath = lexicalPath;
504
+ for (const [index, segment] of input.segments.entries()) {
505
+ current = join(current, segment);
506
+ let details;
507
+ try {
508
+ details = lstatSync(current, { bigint: true });
509
+ }
510
+ catch (error) {
511
+ if (isErrno(error, "ENOENT"))
512
+ break;
513
+ throw error;
514
+ }
515
+ if (!details.isDirectory() || details.isSymbolicLink()) {
516
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
517
+ }
518
+ assertOwned(details);
519
+ const final = index === input.segments.length - 1;
520
+ if (final && (details.mode & 63n) !== 0n) {
521
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
522
+ }
523
+ if (!final && (details.mode & 18n) !== 0n) {
524
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
525
+ }
526
+ if (final)
527
+ canonicalPath = realpathSync(current);
528
+ }
529
+ // Symlink components are rejected above rather than followed. This second
530
+ // recoveryDir branch covers a canonical alias exposed without a symlink and
531
+ // must not be collapsed into the lexical-only check.
532
+ if (!strictDescendant(input.agentRoot, canonicalPath)
533
+ || pathsOverlap(canonicalPath, input.registryDir)
534
+ || pathsOverlap(canonicalPath, input.recoveryDir)
535
+ || pathsOverlap(canonicalPath, input.mutationLockPath)
536
+ || pathsOverlap(canonicalPath, clearSessionsRegistryRoot(input.agentRoot))
537
+ || stateRootContainsWorkspace(canonicalPath, lexicalWorkspace, canonicalWorkspace)) {
538
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
539
+ }
540
+ return Object.freeze({
541
+ segments: Object.freeze([...input.segments]),
542
+ key,
543
+ lexicalPath,
544
+ canonicalPath,
545
+ protectedPaths: Object.freeze(uniquePaths([lexicalPath, canonicalPath])),
546
+ });
547
+ }
548
+ async function replaceManifest(paths, contents, previous, hooks) {
549
+ // Fixed recovery names are safe only while every official publisher holds
550
+ // the mutation lock and proves the recovery namespace drained immediately
551
+ // before entering secureFileReplace, whose cross-directory rename paths are
552
+ // intentionally fixed for this registry.
553
+ await assertRecoveryArtifactsDrained(paths);
554
+ await secureFileReplace({
555
+ path: paths.manifestPath,
556
+ temporaryPath: paths.stagingPath,
557
+ contents,
558
+ mode: 0o600,
559
+ target: previous === undefined
560
+ ? {
561
+ expected: { kind: "missing" },
562
+ failedPath: paths.failedPath,
563
+ recovery: "preserve-current",
564
+ ...(hooks?.afterManifestPublish === undefined
565
+ ? {} : { afterPublish: hooks.afterManifestPublish }),
566
+ }
567
+ : {
568
+ expected: {
569
+ kind: "present",
570
+ claimPath: paths.previousPath,
571
+ validate: async (candidate) => {
572
+ const current = await readManifest(candidate);
573
+ return current !== undefined
574
+ && current.details.dev === previous.manifestIdentity.dev
575
+ && current.details.ino === previous.manifestIdentity.ino
576
+ && current.contents.equals(previous.manifestContents);
577
+ },
578
+ invalidError: () => new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR),
579
+ },
580
+ failedPath: paths.failedPath,
581
+ recovery: "restore-previous",
582
+ ...(hooks?.afterManifestPublish === undefined
583
+ ? {} : { afterPublish: hooks.afterManifestPublish }),
584
+ ...(hooks?.beforeManifestRestore === undefined
585
+ ? {} : { beforeRestore: hooks.beforeManifestRestore }),
586
+ },
587
+ });
588
+ }
589
+ async function ensureRecoveryDirectory(paths) {
590
+ let created = false;
591
+ try {
592
+ await mkdir(paths.recoveryDir, { mode: 0o700 });
593
+ created = true;
594
+ }
595
+ catch (error) {
596
+ if (!isErrno(error, "EEXIST"))
597
+ throw error;
598
+ }
599
+ if (created)
600
+ await chmod(paths.recoveryDir, 0o700);
601
+ const details = await lstat(paths.recoveryDir, { bigint: true });
602
+ assertRecoveryDirectory(details);
603
+ if (await realpath(paths.recoveryDir) !== paths.recoveryDir) {
604
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
605
+ }
606
+ let filesystemPeer;
607
+ try {
608
+ filesystemPeer = await lstat(paths.registryDir, { bigint: true });
609
+ assertPrivateDirectory(filesystemPeer, "Process-job registry");
610
+ }
611
+ catch (error) {
612
+ if (!isErrno(error, "ENOENT"))
613
+ throw error;
614
+ filesystemPeer = await lstat(dirname(paths.recoveryDir), { bigint: true });
615
+ assertPrivateDirectory(filesystemPeer, "Process-job registry parent");
616
+ }
617
+ if (details.dev !== filesystemPeer.dev)
618
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
619
+ if (created) {
620
+ await syncDirectory(dirname(paths.recoveryDir));
621
+ const after = await lstat(paths.recoveryDir, { bigint: true });
622
+ assertRecoveryDirectory(after);
623
+ if (!sameFileIdentity(details, after))
624
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
625
+ }
626
+ }
627
+ async function assertRecoveryArtifactsDrained(paths) {
628
+ const target = await inspectRegistryTarget(paths);
629
+ const artifacts = await scanRecoveryArtifacts(paths, target.directoryDetails);
630
+ if (hasRecoveryArtifacts(artifacts)) {
631
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
632
+ }
633
+ }
634
+ async function createMissingRegistryDirectoryForRestore(paths) {
635
+ try {
636
+ await mkdir(paths.registryDir, { mode: 0o700 });
637
+ }
638
+ catch (error) {
639
+ // A pathname appearing after the absent-target proof is ambiguous. Never
640
+ // adopt, chmod, remove, or overwrite it as part of recovery.
641
+ if (isErrno(error, "EEXIST"))
642
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
643
+ throw error;
644
+ }
645
+ const before = await lstat(paths.registryDir, { bigint: true });
646
+ assertPrivateDirectory(before, "Process-job registry");
647
+ await chmod(paths.registryDir, 0o700);
648
+ const after = await lstat(paths.registryDir, { bigint: true });
649
+ assertPrivateDirectory(after, "Process-job registry");
650
+ if (!sameFileIdentity(before, after) || await realpath(paths.registryDir) !== paths.registryDir) {
651
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
652
+ }
653
+ const recovery = await lstat(paths.recoveryDir, { bigint: true });
654
+ assertRecoveryDirectory(recovery);
655
+ if (after.dev !== recovery.dev)
656
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
657
+ const parentPath = dirname(paths.registryDir);
658
+ await syncDirectory(parentPath);
659
+ const proven = await lstat(paths.registryDir, { bigint: true });
660
+ assertPrivateDirectory(proven, "Process-job registry");
661
+ if (!sameFileIdentity(after, proven))
662
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
663
+ }
664
+ async function recoverRegistryArtifacts(paths, hooks) {
665
+ await hooks?.beforeRegistryRecovery?.();
666
+ let target = await inspectRegistryTargetForRecovery(paths);
667
+ let artifacts = await scanRecoveryArtifactsForRecovery(paths, target.directoryDetails);
668
+ if (target.current?.details.nlink === 2n || artifacts.previous?.details.nlink === 2n) {
669
+ const interruptedTarget = target.current;
670
+ const interruptedPrevious = artifacts.previous;
671
+ if (interruptedTarget === undefined
672
+ || interruptedPrevious === undefined
673
+ || interruptedTarget.details.nlink !== 2n
674
+ || interruptedPrevious.details.nlink !== 2n
675
+ || !sameFileIdentity(interruptedTarget.details, interruptedPrevious.details)
676
+ || !interruptedTarget.contents.equals(interruptedPrevious.contents)) {
677
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
678
+ }
679
+ await completeInterruptedPreviousRestore(paths, interruptedTarget, interruptedPrevious);
680
+ // Return to the ordinary single-link readers before considering any other
681
+ // artifact cleanup. No general two-link state is admitted beyond the exact
682
+ // target+previous recovery pair completed above.
683
+ target = await inspectRegistryTarget(paths);
684
+ artifacts = await scanRecoveryArtifacts(paths, target.directoryDetails);
685
+ }
686
+ if (!hasRecoveryArtifacts(artifacts))
687
+ return;
688
+ if (target.current !== undefined) {
689
+ for (const artifact of [artifacts.previous, artifacts.staging, artifacts.failed]) {
690
+ if (artifact !== undefined)
691
+ await removeProvenRecoveryArtifact(paths.recoveryDir, artifact);
692
+ }
693
+ await proveManifestUnchanged(target.current);
694
+ return;
695
+ }
696
+ if (artifacts.previous !== undefined) {
697
+ if (target.directoryDetails === undefined) {
698
+ await createMissingRegistryDirectoryForRestore(paths);
699
+ }
700
+ await restorePreviousManifest(paths, artifacts.previous);
701
+ for (const artifact of [artifacts.staging, artifacts.failed]) {
702
+ if (artifact !== undefined)
703
+ await removeProvenRecoveryArtifact(paths.recoveryDir, artifact);
704
+ }
705
+ return;
706
+ }
707
+ if (artifacts.failed !== undefined) {
708
+ // A failed publication without a previous generation cannot prove which
709
+ // generation should be authoritative.
710
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
711
+ }
712
+ if (artifacts.staging !== undefined) {
713
+ await removeProvenRecoveryArtifact(paths.recoveryDir, artifacts.staging);
714
+ }
715
+ }
716
+ async function restorePreviousManifest(paths, previous) {
717
+ await proveManifestUnchanged(previous);
718
+ await link(previous.path, paths.manifestPath);
719
+ const target = await readRecoveryPairCandidateManifest(paths.manifestPath);
720
+ const linkedPrevious = await readRecoveryPairCandidateManifest(previous.path);
721
+ if (target.details.nlink !== 2n
722
+ || linkedPrevious.details.nlink !== 2n
723
+ || !sameFileIdentity(target.details, linkedPrevious.details)
724
+ || !target.contents.equals(linkedPrevious.contents)) {
725
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
726
+ }
727
+ await completeInterruptedPreviousRestore(paths, target, linkedPrevious);
728
+ }
729
+ async function completeInterruptedPreviousRestore(paths, expectedTarget, expectedPrevious) {
730
+ let target = await proveRecoveryPairCandidateUnchanged(expectedTarget);
731
+ let previous = await proveRecoveryPairCandidateUnchanged(expectedPrevious);
732
+ assertExactInterruptedPreviousRestorePair(target, previous);
733
+ // The target link may have been created immediately before process death.
734
+ // Make it durable before consuming the only recovery pathname, then reprove
735
+ // both names after the fsync yield and immediately before unlinking.
736
+ await syncDirectory(paths.registryDir);
737
+ target = await proveRecoveryPairCandidateUnchanged(target);
738
+ previous = await proveRecoveryPairCandidateUnchanged(previous);
739
+ assertExactInterruptedPreviousRestorePair(target, previous);
740
+ await unlink(previous.path);
741
+ await syncDirectory(paths.recoveryDir);
742
+ const restored = await readProvenManifest(paths.manifestPath);
743
+ if (!sameFileIdentity(target.details, restored.details)
744
+ || !target.contents.equals(restored.contents)
745
+ || restored.details.nlink !== 1n) {
746
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
747
+ }
748
+ try {
749
+ await lstat(previous.path);
750
+ }
751
+ catch (error) {
752
+ if (isErrno(error, "ENOENT"))
753
+ return;
754
+ throw error;
755
+ }
756
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
757
+ }
758
+ function assertExactInterruptedPreviousRestorePair(target, previous) {
759
+ if (target.path === previous.path
760
+ || target.details.nlink !== 2n
761
+ || previous.details.nlink !== 2n
762
+ || !sameFileIdentity(target.details, previous.details)
763
+ || !target.contents.equals(previous.contents)) {
764
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
765
+ }
766
+ }
767
+ async function proveRecoveryPairCandidateUnchanged(expected) {
768
+ const current = await readRecoveryPairCandidateManifest(expected.path);
769
+ if (!sameFileIdentity(expected.details, current.details)
770
+ || !expected.contents.equals(current.contents)) {
771
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
772
+ }
773
+ return current;
774
+ }
775
+ async function removeProvenRecoveryArtifact(recoveryDir, artifact) {
776
+ await proveManifestUnchanged(artifact);
777
+ await unlink(artifact.path);
778
+ await syncDirectory(recoveryDir);
779
+ try {
780
+ await lstat(artifact.path);
781
+ }
782
+ catch (error) {
783
+ if (isErrno(error, "ENOENT"))
784
+ return;
785
+ throw error;
786
+ }
787
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
788
+ }
789
+ async function proveManifestUnchanged(expected) {
790
+ const current = await readProvenManifest(expected.path);
791
+ if (!sameFileIdentity(expected.details, current.details)
792
+ || !expected.contents.equals(current.contents)) {
793
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
794
+ }
795
+ }
796
+ function encodeManifest(manifest) {
797
+ const contents = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8");
798
+ if (contents.byteLength > PROCESS_JOBS_ROOT_REGISTRY_MAX_BYTES) {
799
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
800
+ }
801
+ return contents;
802
+ }
803
+ async function acquireRegistryLock(path) {
804
+ return await acquireOwnerPrivateLock({
805
+ path,
806
+ label: "Process-job root registry",
807
+ schemaTag: REGISTRY_LOCK_SCHEMA,
808
+ ownerlessGraceMs: REGISTRY_LOCK_OWNERLESS_GRACE_MS,
809
+ invalidOwner: "error",
810
+ });
811
+ }
812
+ /** Remove only the exact owner-private empty artifact left by first registration. */
813
+ async function removeExactEmptyRegistryDirectory(path) {
814
+ let before;
815
+ try {
816
+ before = await lstat(path, { bigint: true });
817
+ }
818
+ catch (error) {
819
+ if (isErrno(error, "ENOENT"))
820
+ return false;
821
+ throw error;
822
+ }
823
+ assertPrivateDirectory(before, "Process-job registry");
824
+ if (await directoryHasEntry(path))
825
+ return false;
826
+ if (await realpath(path) !== path)
827
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
828
+ const parentPath = dirname(path);
829
+ const parentBefore = await lstat(parentPath, { bigint: true });
830
+ assertPrivateDirectory(parentBefore, "Process-job registry parent");
831
+ const current = await lstat(path, { bigint: true });
832
+ assertPrivateDirectory(current, "Process-job registry");
833
+ if (!sameFileIdentity(before, current))
834
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
835
+ const parentCurrent = await lstat(parentPath, { bigint: true });
836
+ assertPrivateDirectory(parentCurrent, "Process-job registry parent");
837
+ if (!sameFileIdentity(parentBefore, parentCurrent)) {
838
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
839
+ }
840
+ try {
841
+ await rmdir(path);
842
+ }
843
+ catch (error) {
844
+ // An entry appeared after the bounded empty check. Preserve it so the
845
+ // strict loader can fail closed; never recursively clean unknown state.
846
+ if (isErrno(error, "ENOTEMPTY") || isErrno(error, "EEXIST"))
847
+ return false;
848
+ throw error;
849
+ }
850
+ await syncDirectory(parentPath);
851
+ const parentAfter = await lstat(parentPath, { bigint: true });
852
+ assertPrivateDirectory(parentAfter, "Process-job registry parent");
853
+ if (!sameFileIdentity(parentBefore, parentAfter)) {
854
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
855
+ }
856
+ try {
857
+ await lstat(path);
858
+ }
859
+ catch (error) {
860
+ if (isErrno(error, "ENOENT"))
861
+ return true;
862
+ throw error;
863
+ }
864
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
865
+ }
866
+ async function directoryHasEntry(path) {
867
+ const directory = await opendir(path);
868
+ try {
869
+ return await directory.read() !== null;
870
+ }
871
+ finally {
872
+ await directory.close();
873
+ }
874
+ }
875
+ async function ensurePrivateDirectory(path, _label) {
876
+ try {
877
+ await mkdir(path, { mode: 0o700 });
878
+ }
879
+ catch (error) {
880
+ if (!isErrno(error, "EEXIST"))
881
+ throw error;
882
+ }
883
+ const before = await lstat(path, { bigint: true });
884
+ if (!before.isDirectory() || before.isSymbolicLink())
885
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
886
+ assertOwned(before);
887
+ await chmod(path, 0o700);
888
+ const after = await lstat(path, { bigint: true });
889
+ if (after.dev !== before.dev || after.ino !== before.ino)
890
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
891
+ assertPrivateDirectory(after, "Process-job registry");
892
+ }
893
+ function assertPrivateDirectory(details, _label) {
894
+ if (!details.isDirectory() || details.isSymbolicLink() || (details.mode & 63n) !== 0n) {
895
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
896
+ }
897
+ assertOwned(details);
898
+ }
899
+ function assertRecoveryDirectory(details) {
900
+ if (!details.isDirectory() || details.isSymbolicLink()
901
+ || (details.mode & 511n) !== 448n) {
902
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
903
+ }
904
+ assertOwned(details);
905
+ }
906
+ function assertOwned(details) {
907
+ if (typeof process.getuid === "function" && details.uid !== BigInt(process.getuid())) {
908
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
909
+ }
910
+ }
911
+ function sameFileIdentity(left, right) {
912
+ return left.dev === right.dev && left.ino === right.ino;
913
+ }
914
+ async function canonicalAgentRoot(path) {
915
+ const canonical = await realpath(resolve(path));
916
+ const details = await lstat(canonical, { bigint: true });
917
+ assertPrivateDirectoryOwner(details);
918
+ return canonical;
919
+ }
920
+ function assertPrivateDirectoryOwner(details) {
921
+ if (!details.isDirectory() || details.isSymbolicLink())
922
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
923
+ assertOwned(details);
924
+ }
925
+ function relativeSegments(agentRoot, stateDir) {
926
+ const candidate = resolve(stateDir);
927
+ const relativePath = relative(agentRoot, candidate);
928
+ if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
929
+ throw new Error(PROCESS_JOBS_REGISTRY_UNAVAILABLE_ERROR);
930
+ }
931
+ return validateSegments(relativePath.split(sep));
932
+ }
933
+ function proof(snapshot, rootKey) {
934
+ return Object.freeze({ snapshot, rootKey, [REGISTRATION_PROOF]: true });
935
+ }
936
+ function compareSegments(left, right) {
937
+ return Buffer.compare(Buffer.from(left.join("/"), "utf8"), Buffer.from(right.join("/"), "utf8"));
938
+ }
939
+ function sameSegmentRoots(left, right) {
940
+ return left.length === right.length && left.every((root, index) => sameStringArrays(root, right[index] ?? []));
941
+ }
942
+ function sameStringArrays(left, right) {
943
+ return left.length === right.length && left.every((value, index) => value === right[index]);
944
+ }
945
+ function uniquePaths(paths) {
946
+ return [...new Set(paths.map((path) => resolve(path)))].sort();
947
+ }
948
+ function strictDescendant(parent, child) {
949
+ const candidate = relative(parent, child);
950
+ return candidate.length > 0 && candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute(candidate);
951
+ }
952
+ function pathsOverlap(left, right) {
953
+ return containsPath(left, right) || containsPath(right, left);
954
+ }
955
+ /** A private leaf may sit inside the workspace; it must never own the workspace. */
956
+ function stateRootContainsWorkspace(stateRoot, lexicalWorkspace, canonicalWorkspace) {
957
+ return containsPath(stateRoot, lexicalWorkspace)
958
+ || containsPath(stateRoot, canonicalWorkspace);
959
+ }
960
+ function containsPath(parent, child) {
961
+ const candidate = relative(parent, child);
962
+ return candidate.length === 0 || (candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute(candidate));
963
+ }
964
+ function hasExactKeys(value, keys) {
965
+ const actual = Object.keys(value).sort();
966
+ const expected = [...keys].sort();
967
+ return sameStringArrays(actual, expected);
968
+ }
969
+ function isRecord(value) {
970
+ return typeof value === "object" && value !== null && !Array.isArray(value);
971
+ }
972
+ function isUuid(value) {
973
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value);
974
+ }
975
+ function isErrno(error, code) {
976
+ return typeof error === "object" && error !== null && error.code === code;
977
+ }
978
+ //# sourceMappingURL=process-jobs-root-registry.js.map