@smthrs/kernel 0.0.0-stage → 1.0.0-rc.4

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 (267) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -2
  4. package/dist/cjs/CapabilitySet.d.ts +6 -0
  5. package/dist/cjs/CapabilitySet.d.ts.map +1 -0
  6. package/dist/cjs/CapabilitySet.js +19 -0
  7. package/dist/cjs/CapabilitySet.js.map +7 -0
  8. package/dist/cjs/ChildProcessEnvironment.d.ts +60 -0
  9. package/dist/cjs/ChildProcessEnvironment.d.ts.map +1 -0
  10. package/dist/cjs/ChildProcessEnvironment.js +61 -0
  11. package/dist/cjs/ChildProcessEnvironment.js.map +7 -0
  12. package/dist/cjs/ChildProcessSpawner.d.ts +100 -0
  13. package/dist/cjs/ChildProcessSpawner.d.ts.map +1 -0
  14. package/dist/cjs/ChildProcessSpawner.js +291 -0
  15. package/dist/cjs/ChildProcessSpawner.js.map +7 -0
  16. package/dist/cjs/CommandLine.d.ts +173 -0
  17. package/dist/cjs/CommandLine.d.ts.map +1 -0
  18. package/dist/cjs/CommandLine.js +54 -0
  19. package/dist/cjs/CommandLine.js.map +7 -0
  20. package/dist/cjs/ContainedSpawner.d.ts +135 -0
  21. package/dist/cjs/ContainedSpawner.d.ts.map +1 -0
  22. package/dist/cjs/ContainedSpawner.js +141 -0
  23. package/dist/cjs/ContainedSpawner.js.map +7 -0
  24. package/dist/cjs/FileSystem.d.ts +452 -0
  25. package/dist/cjs/FileSystem.d.ts.map +1 -0
  26. package/dist/cjs/FileSystem.js +890 -0
  27. package/dist/cjs/FileSystem.js.map +7 -0
  28. package/dist/cjs/FileSystemBatch.d.ts +94 -0
  29. package/dist/cjs/FileSystemBatch.d.ts.map +1 -0
  30. package/dist/cjs/FileSystemBatch.js +31 -0
  31. package/dist/cjs/FileSystemBatch.js.map +7 -0
  32. package/dist/cjs/GrantEvent.d.ts +240 -0
  33. package/dist/cjs/GrantEvent.d.ts.map +1 -0
  34. package/dist/cjs/GrantEvent.js +115 -0
  35. package/dist/cjs/GrantEvent.js.map +7 -0
  36. package/dist/cjs/GrantStore.d.ts +270 -0
  37. package/dist/cjs/GrantStore.d.ts.map +1 -0
  38. package/dist/cjs/GrantStore.js +857 -0
  39. package/dist/cjs/GrantStore.js.map +7 -0
  40. package/dist/cjs/HostError.d.ts +36 -0
  41. package/dist/cjs/HostError.d.ts.map +1 -0
  42. package/dist/cjs/HostError.js +17 -0
  43. package/dist/cjs/HostError.js.map +7 -0
  44. package/dist/cjs/HostServices.d.ts +101 -0
  45. package/dist/cjs/HostServices.d.ts.map +1 -0
  46. package/dist/cjs/HostServices.js +66 -0
  47. package/dist/cjs/HostServices.js.map +7 -0
  48. package/dist/cjs/HttpClient.d.ts +177 -0
  49. package/dist/cjs/HttpClient.d.ts.map +1 -0
  50. package/dist/cjs/HttpClient.js +217 -0
  51. package/dist/cjs/HttpClient.js.map +7 -0
  52. package/dist/cjs/Jj.d.ts +67 -0
  53. package/dist/cjs/Jj.d.ts.map +1 -0
  54. package/dist/cjs/Jj.js +118 -0
  55. package/dist/cjs/Jj.js.map +7 -0
  56. package/dist/cjs/JournalGrantStore.d.ts +65 -0
  57. package/dist/cjs/JournalGrantStore.d.ts.map +1 -0
  58. package/dist/cjs/JournalGrantStore.js +354 -0
  59. package/dist/cjs/JournalGrantStore.js.map +7 -0
  60. package/dist/cjs/Path.d.ts +37 -0
  61. package/dist/cjs/Path.d.ts.map +1 -0
  62. package/dist/cjs/Path.js +46 -0
  63. package/dist/cjs/Path.js.map +7 -0
  64. package/dist/cjs/ProcessConfinement.d.ts +55 -0
  65. package/dist/cjs/ProcessConfinement.d.ts.map +1 -0
  66. package/dist/cjs/ProcessConfinement.js +94 -0
  67. package/dist/cjs/ProcessConfinement.js.map +7 -0
  68. package/dist/cjs/ProcessLedger.d.ts +260 -0
  69. package/dist/cjs/ProcessLedger.d.ts.map +1 -0
  70. package/dist/cjs/ProcessLedger.js +190 -0
  71. package/dist/cjs/ProcessLedger.js.map +7 -0
  72. package/dist/cjs/Rooted.d.ts +53 -0
  73. package/dist/cjs/Rooted.d.ts.map +1 -0
  74. package/dist/cjs/Rooted.js +101 -0
  75. package/dist/cjs/Rooted.js.map +7 -0
  76. package/dist/cjs/Unreachable.d.ts +27 -0
  77. package/dist/cjs/Unreachable.d.ts.map +1 -0
  78. package/dist/cjs/Unreachable.js +43 -0
  79. package/dist/cjs/Unreachable.js.map +7 -0
  80. package/dist/cjs/Workspace.d.ts +62 -0
  81. package/dist/cjs/Workspace.d.ts.map +1 -0
  82. package/dist/cjs/Workspace.js +35 -0
  83. package/dist/cjs/Workspace.js.map +7 -0
  84. package/dist/cjs/index.d.ts +161 -0
  85. package/dist/cjs/index.d.ts.map +1 -0
  86. package/dist/cjs/index.js +73 -0
  87. package/dist/cjs/index.js.map +7 -0
  88. package/dist/cjs/internal/Containment.d.ts +24 -0
  89. package/dist/cjs/internal/Containment.d.ts.map +1 -0
  90. package/dist/cjs/internal/Containment.js +30 -0
  91. package/dist/cjs/internal/Containment.js.map +7 -0
  92. package/dist/cjs/internal/Pipeline.d.ts +18 -0
  93. package/dist/cjs/internal/Pipeline.d.ts.map +1 -0
  94. package/dist/cjs/internal/Pipeline.js +90 -0
  95. package/dist/cjs/internal/Pipeline.js.map +7 -0
  96. package/dist/cjs/internal/makeCapability.d.ts +16 -0
  97. package/dist/cjs/internal/makeCapability.d.ts.map +1 -0
  98. package/dist/cjs/internal/makeCapability.js +44 -0
  99. package/dist/cjs/internal/makeCapability.js.map +7 -0
  100. package/dist/cjs/package.json +1 -0
  101. package/dist/cjs/test/FileSystemBatchContract.d.ts +15 -0
  102. package/dist/cjs/test/FileSystemBatchContract.d.ts.map +1 -0
  103. package/dist/cjs/test/FileSystemBatchContract.js +76 -0
  104. package/dist/cjs/test/FileSystemBatchContract.js.map +7 -0
  105. package/dist/cjs/test/HostContract.d.ts +225 -0
  106. package/dist/cjs/test/HostContract.d.ts.map +1 -0
  107. package/dist/cjs/test/HostContract.js +489 -0
  108. package/dist/cjs/test/HostContract.js.map +7 -0
  109. package/dist/cjs/test/TestGrantStore.d.ts +31 -0
  110. package/dist/cjs/test/TestGrantStore.d.ts.map +1 -0
  111. package/dist/cjs/test/TestGrantStore.js +65 -0
  112. package/dist/cjs/test/TestGrantStore.js.map +7 -0
  113. package/dist/esm/CapabilitySet.d.ts +6 -0
  114. package/dist/esm/CapabilitySet.d.ts.map +1 -0
  115. package/dist/esm/CapabilitySet.js +6 -0
  116. package/dist/esm/CapabilitySet.js.map +1 -0
  117. package/dist/esm/ChildProcessEnvironment.d.ts +60 -0
  118. package/dist/esm/ChildProcessEnvironment.d.ts.map +1 -0
  119. package/dist/esm/ChildProcessEnvironment.js +91 -0
  120. package/dist/esm/ChildProcessEnvironment.js.map +1 -0
  121. package/dist/esm/ChildProcessSpawner.d.ts +100 -0
  122. package/dist/esm/ChildProcessSpawner.d.ts.map +1 -0
  123. package/dist/esm/ChildProcessSpawner.js +342 -0
  124. package/dist/esm/ChildProcessSpawner.js.map +1 -0
  125. package/dist/esm/CommandLine.d.ts +173 -0
  126. package/dist/esm/CommandLine.d.ts.map +1 -0
  127. package/dist/esm/CommandLine.js +195 -0
  128. package/dist/esm/CommandLine.js.map +1 -0
  129. package/dist/esm/ContainedSpawner.d.ts +135 -0
  130. package/dist/esm/ContainedSpawner.d.ts.map +1 -0
  131. package/dist/esm/ContainedSpawner.js +180 -0
  132. package/dist/esm/ContainedSpawner.js.map +1 -0
  133. package/dist/esm/FileSystem.d.ts +452 -0
  134. package/dist/esm/FileSystem.d.ts.map +1 -0
  135. package/dist/esm/FileSystem.js +989 -0
  136. package/dist/esm/FileSystem.js.map +1 -0
  137. package/dist/esm/FileSystemBatch.d.ts +94 -0
  138. package/dist/esm/FileSystemBatch.d.ts.map +1 -0
  139. package/dist/esm/FileSystemBatch.js +26 -0
  140. package/dist/esm/FileSystemBatch.js.map +1 -0
  141. package/dist/esm/GrantEvent.d.ts +240 -0
  142. package/dist/esm/GrantEvent.d.ts.map +1 -0
  143. package/dist/esm/GrantEvent.js +155 -0
  144. package/dist/esm/GrantEvent.js.map +1 -0
  145. package/dist/esm/GrantStore.d.ts +270 -0
  146. package/dist/esm/GrantStore.d.ts.map +1 -0
  147. package/dist/esm/GrantStore.js +951 -0
  148. package/dist/esm/GrantStore.js.map +1 -0
  149. package/dist/esm/HostError.d.ts +36 -0
  150. package/dist/esm/HostError.d.ts.map +1 -0
  151. package/dist/esm/HostError.js +20 -0
  152. package/dist/esm/HostError.js.map +1 -0
  153. package/dist/esm/HostServices.d.ts +101 -0
  154. package/dist/esm/HostServices.d.ts.map +1 -0
  155. package/dist/esm/HostServices.js +76 -0
  156. package/dist/esm/HostServices.js.map +1 -0
  157. package/dist/esm/HttpClient.d.ts +177 -0
  158. package/dist/esm/HttpClient.d.ts.map +1 -0
  159. package/dist/esm/HttpClient.js +299 -0
  160. package/dist/esm/HttpClient.js.map +1 -0
  161. package/dist/esm/Jj.d.ts +67 -0
  162. package/dist/esm/Jj.d.ts.map +1 -0
  163. package/dist/esm/Jj.js +111 -0
  164. package/dist/esm/Jj.js.map +1 -0
  165. package/dist/esm/JournalGrantStore.d.ts +65 -0
  166. package/dist/esm/JournalGrantStore.d.ts.map +1 -0
  167. package/dist/esm/JournalGrantStore.js +350 -0
  168. package/dist/esm/JournalGrantStore.js.map +1 -0
  169. package/dist/esm/Path.d.ts +37 -0
  170. package/dist/esm/Path.d.ts.map +1 -0
  171. package/dist/esm/Path.js +34 -0
  172. package/dist/esm/Path.js.map +1 -0
  173. package/dist/esm/ProcessConfinement.d.ts +55 -0
  174. package/dist/esm/ProcessConfinement.d.ts.map +1 -0
  175. package/dist/esm/ProcessConfinement.js +106 -0
  176. package/dist/esm/ProcessConfinement.js.map +1 -0
  177. package/dist/esm/ProcessLedger.d.ts +260 -0
  178. package/dist/esm/ProcessLedger.d.ts.map +1 -0
  179. package/dist/esm/ProcessLedger.js +283 -0
  180. package/dist/esm/ProcessLedger.js.map +1 -0
  181. package/dist/esm/Rooted.d.ts +53 -0
  182. package/dist/esm/Rooted.d.ts.map +1 -0
  183. package/dist/esm/Rooted.js +104 -0
  184. package/dist/esm/Rooted.js.map +1 -0
  185. package/dist/esm/Unreachable.d.ts +27 -0
  186. package/dist/esm/Unreachable.d.ts.map +1 -0
  187. package/dist/esm/Unreachable.js +26 -0
  188. package/dist/esm/Unreachable.js.map +1 -0
  189. package/dist/esm/Workspace.d.ts +62 -0
  190. package/dist/esm/Workspace.d.ts.map +1 -0
  191. package/dist/esm/Workspace.js +51 -0
  192. package/dist/esm/Workspace.js.map +1 -0
  193. package/dist/esm/index.d.ts +161 -0
  194. package/dist/esm/index.d.ts.map +1 -0
  195. package/dist/esm/index.js +151 -0
  196. package/dist/esm/index.js.map +1 -0
  197. package/dist/esm/internal/Containment.d.ts +24 -0
  198. package/dist/esm/internal/Containment.d.ts.map +1 -0
  199. package/dist/esm/internal/Containment.js +27 -0
  200. package/dist/esm/internal/Containment.js.map +1 -0
  201. package/dist/esm/internal/Pipeline.d.ts +18 -0
  202. package/dist/esm/internal/Pipeline.d.ts.map +1 -0
  203. package/dist/esm/internal/Pipeline.js +68 -0
  204. package/dist/esm/internal/Pipeline.js.map +1 -0
  205. package/dist/esm/internal/makeCapability.d.ts +16 -0
  206. package/dist/esm/internal/makeCapability.d.ts.map +1 -0
  207. package/dist/esm/internal/makeCapability.js +22 -0
  208. package/dist/esm/internal/makeCapability.js.map +1 -0
  209. package/dist/esm/test/FileSystemBatchContract.d.ts +15 -0
  210. package/dist/esm/test/FileSystemBatchContract.d.ts.map +1 -0
  211. package/dist/esm/test/FileSystemBatchContract.js +57 -0
  212. package/dist/esm/test/FileSystemBatchContract.js.map +1 -0
  213. package/dist/esm/test/HostContract.d.ts +225 -0
  214. package/dist/esm/test/HostContract.d.ts.map +1 -0
  215. package/dist/esm/test/HostContract.js +460 -0
  216. package/dist/esm/test/HostContract.js.map +1 -0
  217. package/dist/esm/test/TestGrantStore.d.ts +31 -0
  218. package/dist/esm/test/TestGrantStore.d.ts.map +1 -0
  219. package/dist/esm/test/TestGrantStore.js +57 -0
  220. package/dist/esm/test/TestGrantStore.js.map +1 -0
  221. package/docs/README.md +162 -0
  222. package/docs/api.md +1532 -0
  223. package/docs/concepts/decoration-in-place.md +117 -0
  224. package/docs/concepts/filesystem-batches.md +58 -0
  225. package/docs/concepts/filesystem-confinement.md +168 -0
  226. package/docs/concepts/grant-decisions.md +181 -0
  227. package/docs/concepts/process-containment.md +155 -0
  228. package/docs/contract.md +14 -0
  229. package/docs/guides/adapt-a-new-host-platform.md +148 -0
  230. package/docs/guides/answer-permission-requests.md +151 -0
  231. package/docs/guides/authorize-network-and-model-calls.md +118 -0
  232. package/docs/guides/contain-spawned-processes.md +118 -0
  233. package/docs/guides/guard-a-host-bundle.md +109 -0
  234. package/docs/guides/persist-grants-across-restarts.md +135 -0
  235. package/docs/guides/write-a-capability-policy.md +159 -0
  236. package/docs/installation.md +115 -0
  237. package/docs/quickstart.md +182 -0
  238. package/docs/testing.md +108 -0
  239. package/docs/troubleshooting.md +281 -0
  240. package/package.json +344 -3
  241. package/src/CapabilitySet.ts +6 -0
  242. package/src/ChildProcessEnvironment.ts +98 -0
  243. package/src/ChildProcessSpawner.ts +414 -0
  244. package/src/CommandLine.ts +250 -0
  245. package/src/ContainedSpawner.ts +284 -0
  246. package/src/FileSystem.ts +1655 -0
  247. package/src/FileSystemBatch.ts +90 -0
  248. package/src/GrantEvent.ts +174 -0
  249. package/src/GrantStore.ts +1345 -0
  250. package/src/HostError.ts +37 -0
  251. package/src/HostServices.ts +133 -0
  252. package/src/HttpClient.ts +406 -0
  253. package/src/Jj.ts +159 -0
  254. package/src/JournalGrantStore.ts +461 -0
  255. package/src/Path.ts +47 -0
  256. package/src/ProcessConfinement.ts +156 -0
  257. package/src/ProcessLedger.ts +423 -0
  258. package/src/Rooted.ts +128 -0
  259. package/src/Unreachable.ts +30 -0
  260. package/src/Workspace.ts +65 -0
  261. package/src/index.ts +181 -0
  262. package/src/internal/Containment.ts +37 -0
  263. package/src/internal/Pipeline.ts +85 -0
  264. package/src/internal/makeCapability.ts +26 -0
  265. package/src/test/FileSystemBatchContract.ts +59 -0
  266. package/src/test/HostContract.ts +785 -0
  267. package/src/test/TestGrantStore.ts +73 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Process containment: prepared ownership, bounded cleanup and durable records.
3
+ *
4
+ * A host supplies a {@link Lifecycle} that owns launch and cleanup independently
5
+ * of the command's exit status. The lifecycle first prepares a process identity;
6
+ * this decorator commits that identity to {@link ProcessLedger.ProcessLedger}
7
+ * before activating the caller's command. Failed or interrupted startup closes
8
+ * its child scope immediately, including any earlier legs of a partial pipeline.
9
+ *
10
+ * Each command receives default termination and escalation options. The host
11
+ * lifecycle validates that policy and verifies cleanup before the decorator
12
+ * retires its record. Unverifiable cleanup retains the record for recovery;
13
+ * a host that dies also leaves its committed records discoverable by the next
14
+ * incarnation's reaper (`ProcessReaper` in `@smthrs/platform-node`).
15
+ *
16
+ * Every pipeline leg is prepared and recorded separately, including legs whose
17
+ * pid the aggregate handle does not expose. The module remains platform-neutral.
18
+ * A layer without a lifecycle provides only deadline defaults and bookkeeping;
19
+ * it does not declare the verified-cleanup contract read by {@link isContained}.
20
+ *
21
+ * Governing design:
22
+ * `docs/specs/Concepts/Host Adapters.md`.
23
+ *
24
+ * @since 1.0.0-rc.0
25
+ */
26
+
27
+ import * as Effect from "effect/Effect"
28
+ import * as Exit from "effect/Exit"
29
+ import * as Layer from "effect/Layer"
30
+ import * as PlatformError from "effect/PlatformError"
31
+ import * as Schedule from "effect/Schedule"
32
+ import * as Scope from "effect/Scope"
33
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
34
+ import {
35
+ type ChildProcessHandle,
36
+ ChildProcessSpawner,
37
+ make as makeSpawner
38
+ } from "effect/unstable/process/ChildProcessSpawner"
39
+ import * as CommandLine from "./CommandLine.ts"
40
+ import * as Containment from "./internal/Containment.ts"
41
+ import * as Pipeline from "./internal/Pipeline.ts"
42
+ import * as ProcessLedger from "./ProcessLedger.ts"
43
+
44
+ /**
45
+ * How long a contained child may take to honour `SIGTERM`.
46
+ *
47
+ * @category models
48
+ * @since 1.0.0-rc.0
49
+ */
50
+ export const defaultGraceMs = 2000
51
+
52
+ /**
53
+ * Containment configuration.
54
+ *
55
+ * @category models
56
+ * @since 1.0.0-rc.0
57
+ */
58
+ export interface Options {
59
+ /**
60
+ * Milliseconds between the `SIGTERM` that asks a child to stop and the
61
+ * `SIGKILL` that makes it. Default {@link defaultGraceMs}.
62
+ */
63
+ readonly graceMs?: number | undefined
64
+ /**
65
+ * The platform the host spawner runs on, as `process.platform` spells it.
66
+ * Windows records no POSIX process group, including for detached children.
67
+ * On POSIX, detachment determines whether the child leads its own group.
68
+ * Default `"linux"`; host bundles pass their actual platform.
69
+ */
70
+ readonly platform?: string | undefined
71
+ }
72
+
73
+ /**
74
+ * A platform's preparation, activation, and cleanup of a contained process.
75
+ *
76
+ * Preparation receives the underlying spawner and must own cleanup before it
77
+ * can fail or be interrupted. It returns the process identity to record and an
78
+ * idempotent `activate` effect. The decorator runs activation only after that
79
+ * identity commits to the ledger. A platform can therefore prepare an owner
80
+ * without executing the caller's command before it is durably discoverable.
81
+ *
82
+ * Failed startup closes its own scope immediately. A successful startup keeps
83
+ * that scope attached to the caller's scope. `settled` must stay false on failed
84
+ * or unverifiable cleanup; only verified cleanup permits ledger retirement.
85
+ * The wrapped handle applies the same policy to an explicit `kill`.
86
+ *
87
+ * @category models
88
+ * @since 1.0.0
89
+ */
90
+ export type Lifecycle = (
91
+ command: ChildProcess.StandardCommand,
92
+ spawn: (
93
+ command: ChildProcess.StandardCommand
94
+ ) => Effect.Effect<ChildProcessHandle, PlatformError.PlatformError, Scope.Scope>
95
+ ) => Effect.Effect<
96
+ {
97
+ readonly handle: ChildProcessHandle
98
+ readonly activate: Effect.Effect<void, PlatformError.PlatformError, Scope.Scope>
99
+ readonly settled: Effect.Effect<boolean>
100
+ },
101
+ PlatformError.PlatformError,
102
+ Scope.Scope
103
+ >
104
+
105
+ /**
106
+ * Whether a spawner declares the platform lifecycle required for containment.
107
+ *
108
+ * Only a layer with a {@link Lifecycle} declares this contract. A kill deadline
109
+ * alone does not verify cleanup. The permission decorator preserves the
110
+ * declaration when it wraps a contained service. The marker interoperates
111
+ * across the package's ESM and CommonJS projections.
112
+ *
113
+ * This is a trusted composition check: a caller-supplied lifecycle remains
114
+ * responsible for implementing its contract.
115
+ *
116
+ * @category predicates
117
+ * @since 1.0.0
118
+ */
119
+ export const isContained = (spawner: ChildProcessSpawner["Service"]): boolean => Containment.isContained(spawner)
120
+
121
+ /**
122
+ * Supplies a command's default termination signal and escalation deadline.
123
+ *
124
+ * Both legs of a pipeline get the same policy — a pipeline is one action's
125
+ * process tree and a surviving leg is as much of a leak as a surviving
126
+ * child. Explicit `killSignal` and `forceKillAfter` values are preserved for
127
+ * the host lifecycle to validate.
128
+ *
129
+ * @category constructors
130
+ * @since 1.0.0-rc.0
131
+ */
132
+ export const withContainment = (
133
+ command: ChildProcess.Command,
134
+ options?: Options
135
+ ): ChildProcess.Command => {
136
+ const graceMs = options?.graceMs ?? defaultGraceMs
137
+ if (command._tag === "PipedCommand") {
138
+ return ChildProcess.pipeTo(
139
+ withContainment(command.left, options),
140
+ withContainment(command.right, options),
141
+ command.options
142
+ )
143
+ }
144
+ return ChildProcess.make(command.command, [...command.args], {
145
+ ...command.options,
146
+ killSignal: command.options.killSignal ?? "SIGTERM",
147
+ forceKillAfter: command.options.forceKillAfter ?? graceMs
148
+ })
149
+ }
150
+
151
+ /**
152
+ * The process group a started child leads, or `null` when it shares the
153
+ * host's or the platform has no POSIX process groups.
154
+ *
155
+ * A detached POSIX child leads a group whose id is its own pid. A child in
156
+ * the host's group must not record that group as its own cleanup target.
157
+ * Windows detachment creates an independent console, not a POSIX group;
158
+ * Windows records therefore always carry `null`.
159
+ *
160
+ * @category constructors
161
+ * @since 1.0.0-rc.0
162
+ */
163
+ export const groupOf = (command: ChildProcess.Command, pid: number, platform?: string): number | null => {
164
+ if (platform === "win32") return null
165
+ const options = command._tag === "PipedCommand" ? rightmost(command).options : command.options
166
+ return (options.detached ?? true) ? pid : null
167
+ }
168
+
169
+ const rightmost = (command: ChildProcess.Command): ChildProcess.StandardCommand =>
170
+ command._tag === "StandardCommand" ? command : rightmost(command.right)
171
+
172
+ /**
173
+ * Decorates the process spawner in place with containment and bookkeeping.
174
+ *
175
+ * Compose it over a host spawner layer with `Layer.provide`. It provides the
176
+ * tag it also requires. Compose the permission decorator over this layer to
177
+ * authorize the caller's whole pipeline before its per-process expansion. A
178
+ * lifecycle may use different commands during preparation; a decorator below
179
+ * containment sees those platform commands instead of the caller's command.
180
+ *
181
+ * @category layers
182
+ * @since 1.0.0-rc.0
183
+ */
184
+ export const layer = (
185
+ options?: Options,
186
+ lifecycle?: Lifecycle
187
+ ): Layer.Layer<ChildProcessSpawner, never, ChildProcessSpawner | ProcessLedger.ProcessLedger> =>
188
+ Layer.effect(
189
+ ChildProcessSpawner,
190
+ Effect.gen(function*() {
191
+ const spawner = yield* ChildProcessSpawner
192
+ const ledger = yield* ProcessLedger.ProcessLedger
193
+ const service = makeSpawner(
194
+ (command) =>
195
+ Effect.uninterruptibleMask((restore) =>
196
+ Effect.gen(function*() {
197
+ // A failed startup must clean up even when its caller catches the
198
+ // error and keeps the outer scope open. One child scope contains
199
+ // the entire pipeline, so failure of a later leg also closes all
200
+ // earlier legs. Successful spawns retain it until their caller closes.
201
+ const processScope = yield* Scope.fork(yield* Scope.Scope)
202
+ return yield* restore(
203
+ Pipeline.spawn(
204
+ command,
205
+ Effect.fn("ContainedSpawner.spawn")(function*(command) {
206
+ // The release finalizer is registered BEFORE the spawn, so scope
207
+ // closure runs it AFTER the spawn's own kill finalizer: finalizers
208
+ // run last-registered-first, and a ledger that announced an exit
209
+ // while the process was still being signalled would be telling the
210
+ // next incarnation to stop looking for something still alive. The
211
+ // slot is empty until the record exists, which is also what makes a
212
+ // spawn that never got recorded a no-op to release.
213
+ const slot: { current?: ProcessLedger.ProcessRecord; settled: Effect.Effect<boolean> } = {
214
+ settled: Effect.succeed(true)
215
+ }
216
+ yield* Effect.addFinalizer(() =>
217
+ Effect.gen(function*() {
218
+ if (slot.current === undefined) return
219
+ if (yield* slot.settled) return yield* releaseQuietly(ledger, slot.current)
220
+ yield* Effect.logWarning(
221
+ `process cleanup could not verify ${slot.current.pid}; retaining its ledger record`
222
+ )
223
+ })
224
+ )
225
+ const contained = withContainment(command, options) as ChildProcess.StandardCommand
226
+ const watched = lifecycle === undefined ? undefined : yield* lifecycle(contained, spawner.spawn)
227
+ const handle = watched?.handle ?? (yield* spawner.spawn(contained))
228
+ slot.settled = watched?.settled ?? slot.settled
229
+ const pid = handle.pid as number
230
+ slot.current = yield* ledger.record({
231
+ pid,
232
+ pgid: groupOf(command, pid, options?.platform),
233
+ commandDigest: CommandLine.executable(command)
234
+ }).pipe(
235
+ // A spawn whose record did not commit is precisely the child this
236
+ // module exists to make discoverable, so it is not started: the
237
+ // process is signalled and the caller is told, rather than left
238
+ // running with nothing durable naming it.
239
+ Effect.tapCause(() => Effect.ignore(handle.kill())),
240
+ Effect.mapError(unrecorded)
241
+ )
242
+ if (watched !== undefined) yield* watched.activate
243
+ return handle
244
+ })
245
+ ).pipe(Scope.provide(processScope))
246
+ ).pipe(
247
+ Effect.onError((cause) => Scope.close(processScope, Exit.failCause(cause)))
248
+ )
249
+ })
250
+ )
251
+ )
252
+ return lifecycle === undefined ? service : Containment.mark(service)
253
+ })
254
+ )
255
+
256
+ /** The spawn failure a caller sees when the ledger could not record a child. */
257
+ const unrecorded = (cause: unknown): PlatformError.PlatformError =>
258
+ PlatformError.systemError({
259
+ _tag: "Unknown",
260
+ module: "ChildProcess",
261
+ method: "spawn",
262
+ description: "the process ledger could not record this spawn durably",
263
+ cause
264
+ })
265
+
266
+ /**
267
+ * Retires a record on scope close, retrying the durable write.
268
+ *
269
+ * A finalizer has nowhere to report a failure, so the retry is the report: an
270
+ * exit that never commits leaves the record inherited, and the next
271
+ * incarnation's reaper finds a pid that is already gone and retires it then.
272
+ * That is the safe direction, and the reason this is the ONE place a ledger
273
+ * failure is logged rather than propagated.
274
+ */
275
+ const releaseQuietly = (
276
+ ledger: ProcessLedger.Service,
277
+ record: ProcessLedger.ProcessRecord
278
+ ): Effect.Effect<void> =>
279
+ ledger.release(record).pipe(
280
+ Effect.retry(Schedule.recurs(2)),
281
+ Effect.catch((error) =>
282
+ Effect.logWarning(`process ledger could not retire ${record.pid}; leaving it for the reaper`, error)
283
+ )
284
+ )