@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,118 @@
1
+ ---
2
+ title: "Contain spawned processes"
3
+ description: "Compose ContainedSpawner and ProcessLedger so a cancelled run kills its children on a deadline and a crashed host leaves records its successor can reap."
4
+ sidebar:
5
+ order: 6
6
+ ---
7
+
8
+ Without containment, a spawned child that ignores `SIGTERM` hangs a
9
+ cancellation forever, and a host that crashes abandons every process it
10
+ started. This guide composes a platform lifecycle and a process ledger to own cleanup.
11
+
12
+ ## Compose the spawner over the ledger
13
+
14
+ On Node or Bun, `ProcessReaper.layerSpawner` supplies the native adapter,
15
+ prepared lifecycle, and kernel containment together. It requires
16
+ `ChildProcessSpawner` (which it also provides) and `ProcessLedger`:
17
+
18
+ ```ts
19
+ import { ProcessLedger } from "@smthrs/kernel"
20
+ import * as ProcessReaper from "@smthrs/platform-node/ProcessReaper"
21
+ import { Layer } from "effect"
22
+
23
+ const contained = ProcessReaper.layerSpawner({ graceMs: 2000 }).pipe(
24
+ Layer.provide(rawSpawner),
25
+ Layer.provideMerge(ProcessLedger.layer({ hostId: "my-host", ownerPid: process.pid }))
26
+ )
27
+ ```
28
+
29
+ `graceMs` is the interval between the `SIGTERM` that asks a child to stop and
30
+ the `SIGKILL` that makes it. It defaults to `ContainedSpawner.defaultGraceMs`
31
+ (2,000 ms).
32
+
33
+ The factory always uses the real `process.platform`. A custom host can use
34
+ `ContainedSpawner.layer(options, lifecycle)` directly; its `platform` option
35
+ must describe the real process-group behavior. A win32 record claiming
36
+ `pgid === pid` would name a group the child does not lead.
37
+
38
+ The lifecycle prepares an owner before the command starts, records its
39
+ identity, and only then activates the target. Each pipeline leg has its own
40
+ record. Node and Bun use a live supervisor to keep group ownership after the
41
+ target exits; its `pid` names that owner while `exitCode` and `isRunning`
42
+ describe the target. A raw or deadline-only wrapper has no such contract and
43
+ `ContainedSpawner.isContained` returns false for it.
44
+
45
+ Compose the kernel permission decorator **above** containment. That order
46
+ checks the caller's whole command before pipeline expansion or platform
47
+ preparation. A permission decorator below it can see the trusted supervisor
48
+ command instead of the command the caller requested.
49
+
50
+ ## Choose a ledger
51
+
52
+ ```ts
53
+ // Durable: inherits a crashed incarnation's processes. Requires a `Journal`.
54
+ ProcessLedger.layer({ hostId, ownerPid })
55
+
56
+ // In-memory: contains this incarnation and inherits nothing. No journal.
57
+ ProcessLedger.layerMemory({ hostId, ownerPid })
58
+ ```
59
+
60
+ The durable half is only as good as the journal underneath it, so the choice
61
+ belongs to the program that knows which it has. `hostId` names the run the
62
+ records live on (`flows.host:<hostId>`), and `ownerPid` is what distinguishes
63
+ this incarnation's processes from the orphans a previous one left.
64
+
65
+ On Node, `@smthrs/platform-node`'s `NodeHost.layerContained` and
66
+ `layerContainedAt` compose the spawner, bind `Jj` through it so a stray `jj`
67
+ invocation is recorded too, and add the `ProcessReaper` that sweeps inherited
68
+ records before handing the host over. You supply the ledger.
69
+
70
+ ## Handle a failed record
71
+
72
+ `record`, `release`, `reaped`, and `skipped` all carry the journal's failure to
73
+ their caller, and the spawner acts on it: a spawn whose durable record fails is
74
+ cleaned up before target activation, and the call fails. Failed startup closes
75
+ its own scope even if the caller catches the failure and keeps an outer scope
76
+ open. A cleanup failure or an unverified result fails release and retains any
77
+ record already committed; only the lifecycle's successful `settled` result
78
+ permits retirement.
79
+
80
+ The release finalizer is the exception, because it has nowhere left to report.
81
+ A missed release leaves the record inherited, and the next reaper finds the pid
82
+ already gone and retires it.
83
+
84
+ ## Read what is running and what was abandoned
85
+
86
+ ```ts
87
+ const ledger = yield * ProcessLedger.ProcessLedger
88
+
89
+ const running = yield * ledger.live // this incarnation's unreleased records
90
+ const abandoned = yield * ledger.orphans // records whose owner pid is not ours
91
+ ```
92
+
93
+ `orphans` replays the host's journal history, folds the exit, reap, and skip
94
+ events onto the spawn events, and returns what is left. A host that wants to
95
+ clean them up signals each group and then retires the record:
96
+
97
+ ```ts
98
+ yield * ledger.reaped(record) // we signalled it
99
+ yield * ledger.skipped(record, "pre-boot") // we refused to, and why
100
+ ```
101
+
102
+ Use two different calls deliberately. An operator reading the journal can then
103
+ tell "we killed it" from "we declined to", which a single retire event would
104
+ erase.
105
+
106
+ Signalling a process group you did not start is dangerous, because a pid can be
107
+ recycled. Before reaping, verify that the record names a separate process group
108
+ rather than your own, that the owner is gone, that the record belongs to the
109
+ current boot, and, where the platform can read it, that the pid's start time
110
+ still matches. `@smthrs/platform-node`'s `ProcessReaper` implements exactly
111
+ those checks; reuse it rather than writing the comparisons again.
112
+
113
+ ## Related
114
+
115
+ - [Process containment](../concepts/process-containment.md): why each of these
116
+ pieces exists.
117
+ - [Guard a host bundle](./guard-a-host-bundle.md): containment inside the full
118
+ host composition.
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: "Guard a host bundle"
3
+ description: "Compose HostServices.layer over a platform bundle so every consumer of the five host tags resolves the guarded implementation, and get the composition order right."
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ `HostServices.layer` turns a raw platform bundle into the guarded surface a
9
+ flow body runs on. This guide covers the composition, the order its pieces go
10
+ in, and the two mistakes that quietly defeat it.
11
+
12
+ ## What the layer needs
13
+
14
+ `HostServices.layer` both requires and provides all five host tags, and it
15
+ additionally requires:
16
+
17
+ - `Workspace`, the root that filesystem capability resources are resolved
18
+ against.
19
+ - `GrantStore`, the service that decides.
20
+
21
+ Every member requires the tag it provides, so the guarded implementation
22
+ shadows the raw one for everything downstream.
23
+
24
+ ## The composition
25
+
26
+ ```ts
27
+ import { GrantStore, HostServices, Workspace } from "@smthrs/kernel"
28
+ import * as NodeHost from "@smthrs/platform-node/NodeHost"
29
+ import { Layer } from "effect"
30
+
31
+ const workspaceRoot = "/absolute/path/to/workspace"
32
+
33
+ const guarded = HostServices.layer.pipe(
34
+ // 1. The decider, beneath the decorators that consult it.
35
+ Layer.provide(Layer.orDie(GrantStore.layer({ attended: false, rules: policy }))),
36
+ // 2. The raw platform, beneath the decorators that shadow it. `provideMerge`
37
+ // because the composition below still needs the tags this bundle carries
38
+ // that the kernel does not decorate.
39
+ Layer.provideMerge(NodeHost.layerAt(workspaceRoot)),
40
+ // 3. The workspace root, which both the store and the filesystem decorator
41
+ // resolve resources against. One value, provided once, so they agree.
42
+ Layer.provide(Workspace.layer(workspaceRoot))
43
+ )
44
+ ```
45
+
46
+ `GrantStore.layer` fails with `GrantStoreError` when its options are invalid,
47
+ which is a programming error rather than a runtime condition, so
48
+ `Layer.orDie` is the honest handling. Both the store and the filesystem
49
+ decorator take the workspace root from the same `Workspace` service, so
50
+ provide it once above both.
51
+
52
+ ## Contain processes at the same time
53
+
54
+ Under `NodeHost.layerAt`, the raw spawner signals its target at scope close.
55
+ That target can ignore the signal or exit while descendants keep running, and
56
+ a crashed host runs no finalizers. `NodeHost.layerContainedAt` adds a live
57
+ supervisor, a cleanup deadline, a ledger, and restart reconciliation. Keep
58
+ the permission decorator above it so the caller's complete command is
59
+ authorized before platform preparation:
60
+
61
+ ```ts
62
+ import { ProcessLedger } from "@smthrs/kernel"
63
+
64
+ const guarded = HostServices.layer.pipe(
65
+ Layer.provide(Layer.orDie(GrantStore.layer({ attended: false, rules: policy }))),
66
+ Layer.provideMerge(NodeHost.layerContainedAt(workspaceRoot)),
67
+ Layer.provideMerge(ProcessLedger.layer({ hostId: "my-host", ownerPid: process.pid })),
68
+ Layer.provide(Workspace.layer(workspaceRoot))
69
+ )
70
+ ```
71
+
72
+ `ProcessLedger.layer` needs a `Journal`. For a host that has none, or one that
73
+ should inherit nothing from a previous incarnation, use
74
+ `ProcessLedger.layerMemory` with the same options. See
75
+ [Contain spawned processes](./contain-spawned-processes.md).
76
+
77
+ ## Two mistakes to avoid
78
+
79
+ **Keeping an unguarded alias.** The guarded layer shadows the raw tag for
80
+ everything composed above it. Code that captured the raw service before the
81
+ decorator was composed, or that resolves it from a context built without the
82
+ decorator, is not guarded. If some part of the host genuinely needs unguarded
83
+ access, give it a **separate service** rather than a stale reference to the
84
+ shared one. [`@smthrs/engine`](/api/engine) does exactly that: its snapshot
85
+ bookkeeping holds a private `Jj` service built over the raw host, so the flow
86
+ bodies it runs get no repository authority from it.
87
+
88
+ **Guarding your own bookkeeping.** The guarded surface exists to check the
89
+ code you did not write. The machinery your host needs in order to run at all,
90
+ the directory its database lives in, the working copy it lays down before it
91
+ hands anything to a subject, has to exist before there is anyone to ask, so it
92
+ cannot be behind a permission check. Build that machinery over the raw bundle,
93
+ build the services you hand to the guarded code over the decorated one, and
94
+ give the guarded code no way to reach the first set.
95
+
96
+ ## The real thing
97
+
98
+ [`@smthrs/flows`](/api/flows) composes this at full size in its `NodeRuntime`:
99
+ the Node host with containment on, the kernel over an unattended store,
100
+ storage, the step boundary, the workspace sandbox, the engine, and signal
101
+ handling. [Stand up a Node runtime](/pkg/flows/guides/stand-up-a-node-runtime)
102
+ walks it when you need the whole picture rather than the shape.
103
+
104
+ ## Related
105
+
106
+ - [Write a capability policy](./write-a-capability-policy.md): what goes in
107
+ `rules`.
108
+ - [Decoration in place](../concepts/decoration-in-place.md): why the layer is
109
+ shaped this way.
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: "Persist grants across restarts"
3
+ description: "Use JournalGrantStore so a remembered decision survives the process: the two run ids, what replay accepts and refuses, and how to keep the policy journal inside its ceiling."
4
+ sidebar:
5
+ order: 4
6
+ ---
7
+
8
+ The in-memory store forgets everything when its scope closes. `remembered`
9
+ means nothing until something writes it down. `JournalGrantStore` is that
10
+ something: it persists each decision to a [`@smthrs/journal`](/api/journal)
11
+ journal **before** activating it, and replays the history when the next
12
+ process builds a store.
13
+
14
+ ## Build it
15
+
16
+ ```ts
17
+ import { JournalGrantStore } from "@smthrs/kernel"
18
+
19
+ const store = JournalGrantStore.layer({
20
+ // The operational run. Run grants and run envelopes live here.
21
+ runId: "run-2026-09-03-a",
22
+ // The dedicated policy run. Remembered grants live here, and this id stays
23
+ // stable forever, because the journal has no global grant projection.
24
+ policyRunId: "kernel-policy",
25
+ // The producer id. Replay activates events from this source and no other.
26
+ sourceId: "my-host/grants",
27
+ // Binds run grants and run envelopes to the exact active plan.
28
+ planDigest: "plan-abc",
29
+ attended: true,
30
+ rules: [configuredPolicy]
31
+ })
32
+ ```
33
+
34
+ The layer requires `Journal` and `Workspace`. `runId` and `policyRunId` must be
35
+ different, and every identity must be non-empty, well-formed text within
36
+ `GrantStore.maximumIdentityLength`.
37
+
38
+ `rules` here is the nested form: an array of rulesets whose first entry is your
39
+ configured policy. Replayed remembered rules are appended as a further ruleset,
40
+ so your configured deny keeps its hard veto over anything replay activates.
41
+
42
+ The journal is authoritative permission storage. `SqlJournal` must use the
43
+ `reject` overflow policy: a dropped grant decision cannot safely be treated as
44
+ persisted, so drop-capable policies are unsupported.
45
+
46
+ ## Persist first, activate second
47
+
48
+ Every decision commits to the journal before `GrantStore` activates it. A
49
+ journal failure becomes `journal_failed` and the decision stays **inactive**,
50
+ so a permission that could not be written down is a permission that was not
51
+ granted.
52
+
53
+ Events are routed by scope. Remembered grants and remembered envelopes go to
54
+ `policyRunId`; once, denied, run, and run envelope events go to `runId`. They
55
+ are written unfenced: the grant store is the kernel's own ledger rather than a
56
+ run's lifecycle, and admissions are first-writer-wins records that every later
57
+ process replays.
58
+
59
+ ## What replay accepts
60
+
61
+ Replay is not a general read of the journal. It activates only what it can
62
+ prove:
63
+
64
+ - **Only the configured `sourceId`.** Events from another producer are
65
+ ignored, so nothing else writing to the same journal can grant kernel
66
+ authority.
67
+ - **Only the five known event types.** Anything else in the run is ignored.
68
+ - **Only correctly scoped events.** A run-scoped event found in the policy run,
69
+ or a remembered event found in the operational run, refuses construction
70
+ rather than being skipped.
71
+ - **Only the current plan.** A run grant or run envelope whose `planDigest`
72
+ differs from the store's is skipped: it authorized a different plan.
73
+ - **Only safe patterns.** Every replayed grant pattern is rechecked with
74
+ `isValidGrantPattern`, and every envelope pattern with
75
+ `isValidEnvelopePattern`. A pattern that would exceed its request refuses
76
+ construction, so a tampered journal cannot widen authority.
77
+
78
+ Once and denied events are **audit evidence only**. They record what happened;
79
+ they activate nothing.
80
+
81
+ Two failures refuse construction outright rather than degrading: a payload that
82
+ does not decode, or whose envelope type disagrees with its own payload type;
83
+ and a journal page that does not advance past the cursor it was asked for.
84
+ Following a non-advancing page would replay the same events forever, and
85
+ accepting it would double-apply them.
86
+
87
+ Replayed rules are deduplicated by formatted pattern identity, so a capability
88
+ granted a hundred times costs one rule.
89
+
90
+ ## Start a new policy run when it fills
91
+
92
+ A store retains at most `GrantStore.maximumRules` rules and the same number of
93
+ envelope signatures. Because the policy run accumulates forever, that ceiling
94
+ is a real operational limit, and `JournalGrantStore` fails closed when replay
95
+ would exceed it. The failure names the policy run and the counts:
96
+
97
+ ```text
98
+ policy run kernel-policy replayed 1024 remembered rules; configured rules (12)
99
+ and replayed run rules (3) bring the total to 1039, which exceeds the
100
+ 1024-rule ceiling; remove configured rules or start a new policyRunId and
101
+ grant again
102
+ ```
103
+
104
+ Do not compact the policy run. Replay reads it from the first entry and never
105
+ consults a checkpoint, so a compacted policy run fails every later
106
+ construction with `journal_failed`. Point the store at a fresh `policyRunId`
107
+ instead; the old run stays intact for audit.
108
+
109
+ The envelope-signature ceiling produces the matching message. A construction
110
+ envelope is refused rather than persisted once the replayed signatures already
111
+ fill the ceiling, so the history a later process must replay cannot outgrow
112
+ what it will accept.
113
+
114
+ ## Concurrent construction on one Journal service
115
+
116
+ Two constructors sharing one `Journal` service instance could both replay the
117
+ absence of the same construction envelope and both persist it. A critical
118
+ section per service instance re-replays the target run inside the lock, so
119
+ exactly one of those constructors appends the envelope and the others replay
120
+ it instead. An envelope whose signature is already durable activates its rules
121
+ without persisting again.
122
+
123
+ The lock stops at the service object. Two processes, or two `Journal` services
124
+ over one backing journal in one process, hold separate locks, and the envelope
125
+ event carries no journal dedupe key, so both can append it. The duplicate is
126
+ harmless to authority: replay collapses identical envelopes into one signature
127
+ and the same rules. It costs one extra row per race, so build the store once
128
+ per process and share the service where construction can overlap.
129
+
130
+ ## Related
131
+
132
+ - [Answer permission requests](./answer-permission-requests.md): where
133
+ `remembered` decisions come from.
134
+ - [How a grant decision is made](../concepts/grant-decisions.md): what replay
135
+ is rebuilding.
@@ -0,0 +1,159 @@
1
+ ---
2
+ title: "Write a capability policy"
3
+ description: "Turn what a host should allow into GrantStore rules: the four rulesets, the hard veto, the resource each action names, and how to narrow authority for delegated work."
4
+ sidebar:
5
+ order: 2
6
+ ---
7
+
8
+ A policy is an ordered list of `Permission.Rule` values handed to
9
+ `GrantStore.make` or `GrantStore.layer` as `rules`. Each rule pairs a
10
+ capability pattern with `allow`, `deny`, or `ask`.
11
+
12
+ ## Write the rules
13
+
14
+ ```ts
15
+ import { Capability, GrantStore, Permission } from "@smthrs/kernel"
16
+
17
+ const policy = [
18
+ // Read anything in the workspace.
19
+ new Permission.Rule({
20
+ effect: "allow",
21
+ pattern: new Capability.CapabilityPattern({ action: "fs:read", resource: "/workspace/**" })
22
+ }),
23
+ // Write anything in the workspace, except the lockfile.
24
+ new Permission.Rule({
25
+ effect: "allow",
26
+ pattern: new Capability.CapabilityPattern({ action: "fs:write", resource: "/workspace/**" })
27
+ }),
28
+ new Permission.Rule({
29
+ effect: "deny",
30
+ pattern: new Capability.CapabilityPattern({ action: "fs:write", resource: "/workspace/pnpm-lock.yaml" })
31
+ }),
32
+ // Exactly `npm test`, nothing appended.
33
+ new Permission.Rule({
34
+ effect: "allow",
35
+ pattern: new Capability.CapabilityPattern({ action: "proc:spawn", resource: "npm test" })
36
+ })
37
+ ]
38
+
39
+ const store = GrantStore.layer({ attended: false, rules: policy })
40
+ ```
41
+
42
+ Rules are last-match-wins, so the deny above beats the broad write allow that
43
+ precedes it. Anything no rule matches defaults to `ask`, which an unattended
44
+ store reports as `permission_required`. You never need a catch-all deny; write
45
+ the allows.
46
+
47
+ ## Name the resource each action actually checks
48
+
49
+ A rule only fires if its resource glob matches the resource the decorator
50
+ built. These are the resources the kernel names:
51
+
52
+ | Action | Resource |
53
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `fs:read`, `fs:write` | The canonical absolute path, with an inside-workspace path mapped back to the logical workspace root. |
55
+ | `proc:spawn` | `CommandLine.resource(command)`: the rendered line; a chained shell line as `sh -c '<line>'`; `env <NAME>… --` for non-bootstrap environment overrides. |
56
+ | `net:get`, `net:post` | The lowercased URL host for `https:`, and `<scheme>//<lowercased host>` for anything else. |
57
+ | `model:call` | The same, with `/<model id>` appended. |
58
+ | `jj:status` | `"."` |
59
+ | `jj:diff` | `<from>:<to>` |
60
+ | `jj:snapshot` | The commit message, or `""` when none was given. |
61
+ | `jj:restore`, `jj:revert` | The change id. |
62
+ | `jj:workspace-add` | The canonicalized destination. This one also requires `fs:write` on the same resource. |
63
+ | `jj:workspace-forget` | The workspace name. |
64
+ | `jj:op-restore` | The operation id. |
65
+ | `jj:root` | The canonicalized starting directory. |
66
+
67
+ Because `https` is the implicit scheme, a grant for `api.example.com` never
68
+ authorizes `http://api.example.com`. That is the point: a host grant cannot be
69
+ downgraded into a cleartext one.
70
+
71
+ A `*` in a `proc:spawn` resource matches any text, including `;` and line
72
+ breaks. A `shell: true` line with control syntax is checked as
73
+ `sh -c '<line>'`, and every pipeline stage is checked on its own, so `npm *`
74
+ covers the `npm` stage of `npm test | sh` but not the `sh` stage. Grant exact
75
+ command lines; `npm *` still runs any npm subcommand, including
76
+ `npm exec <anything>`.
77
+
78
+ The glob grammar has no escape character, so a resource that genuinely
79
+ contains `*` or `?` cannot be named exactly. Never build a pattern by
80
+ concatenating text an agent supplied. See
81
+ [resource globs](/pkg/capability/concepts/resource-globs) for the full
82
+ grammar and its edges.
83
+
84
+ ## The configured ruleset has a veto
85
+
86
+ `MakeOptions.rules` accepts either a flat list or a nested one. The flat form
87
+ is the configured policy. In the nested form, the first ruleset is configured
88
+ policy and the rest are replayed remembered grants.
89
+
90
+ The configured ruleset carries a power the others do not: after it is reduced
91
+ by last-match-wins, an effective denial is a **hard veto** that no envelope
92
+ approval, run grant, or remembered grant can lift. A configured deny that a
93
+ later configured allow supersedes _within that same ruleset_ is not a veto, so
94
+ an operator can still write an exception after a broad denial.
95
+
96
+ Use it for the things no operator answering a prompt should be able to unlock:
97
+
98
+ ```ts
99
+ const policy = [
100
+ new Permission.Rule({
101
+ effect: "allow",
102
+ pattern: new Capability.CapabilityPattern({ action: "fs:read", resource: "/workspace/**" })
103
+ }),
104
+ // No later grant of any kind reaches these.
105
+ new Permission.Rule({
106
+ effect: "deny",
107
+ pattern: new Capability.CapabilityPattern({ action: "fs:read", resource: "/workspace/.env*" })
108
+ })
109
+ ]
110
+ ```
111
+
112
+ ## Narrow authority for delegated work
113
+
114
+ A rule says what the policy permits. `CapabilitySet` says what the current
115
+ fiber may even ask for, and it only ever narrows:
116
+
117
+ ```ts
118
+ import { Capability, CapabilitySet } from "@smthrs/kernel"
119
+
120
+ const readOnly = CapabilitySet.attenuate([
121
+ new Capability.CapabilityPattern({ action: "fs:read", resource: "/workspace/**" })
122
+ ])
123
+
124
+ // Inside `untrusted`, every other capability fails with permission_denied and
125
+ // the reason "outside capability ceiling", whatever the rules say.
126
+ const scoped = readOnly(untrusted)
127
+ ```
128
+
129
+ `attenuate` intersects the parent ceiling with one more any-of group, so the
130
+ child can narrow further and can never recover what it gave up. There is no
131
+ public widening operation. A run grant made by an attenuated fiber is
132
+ remembered with that fiber's ceiling and is filtered out for any capability the
133
+ ceiling would not allow, so authority cannot leak back up.
134
+
135
+ ## Stay inside the bounds
136
+
137
+ One store retains at most 1,024 rules across all four rulesets. The constant is
138
+ exported as `GrantStore.maximumRules`, alongside
139
+ `maximumEnvelopePatterns` (256), `maximumPendingRequests` (1,024),
140
+ `maximumMetadataDepth` (16), `maximumMetadataMembers` (1,024),
141
+ `maximumMetadataBytes` (64 KiB), `maximumEventBytes` (256 KiB),
142
+ `maximumIdentityLength` (4,096), and `maximumCapabilityResourceLength`
143
+ (4,096). Exceeding one fails with `invalid_resolution` before any state
144
+ changes. Check against the exported constants rather than hardcoding the
145
+ numbers.
146
+
147
+ Decorator resources use UTF-16 code units: 4,096 is valid; 4,097 fails with
148
+ `GrantStoreError` code `invalid_resolution` before the grant check or guarded
149
+ host operation. Filesystem and process decorators project this error into
150
+ `PlatformError`; HTTP projects it into `HttpClientError`. The structured error
151
+ is retained as the cause. Jujutsu returns the typed `GrantStoreError` directly.
152
+ Filesystem canonicalization can read metadata before rejecting a resource.
153
+
154
+ ## Related
155
+
156
+ - [How a grant decision is made](../concepts/grant-decisions.md): the order
157
+ the rulesets are consulted in.
158
+ - [Answer permission requests](./answer-permission-requests.md): what to do
159
+ with the `ask` the policy leaves behind.
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: "Installation"
3
+ description: "Install @smthrs/kernel, its runtime requirements, its public subpaths, and the platform and journal packages a working host composition adds."
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ ## Get the package
9
+
10
+ `@smthrs/kernel` is not on npm at 1.0.0-rc.0. It ships as a member of the
11
+ [smithers repository](https://github.com/smithersai/smithers) workspace, so
12
+ using it today means working from a checkout:
13
+
14
+ ```bash
15
+ git clone https://github.com/smithersai/smithers.git
16
+ cd smithers
17
+ pnpm install
18
+ ```
19
+
20
+ Code that consumes it lives in that workspace too, either an existing package
21
+ or one you add under `packages/`, and depends on it with a workspace specifier:
22
+
23
+ ```json
24
+ {
25
+ "dependencies": {
26
+ "@smthrs/kernel": "workspace:*"
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Requirements
32
+
33
+ The package requires Node.js 26.4.0 or later and ships as ESM, CommonJS, and
34
+ TypeScript declarations. Its Smithers dependencies install with it:
35
+ [`@smthrs/capability`](/api/capability), [`@smthrs/jj`](/api/jj), and
36
+ [`@smthrs/journal`](/api/journal). The host supplies the exact
37
+ `effect@4.0.0-rc.115` peer so the library never installs a second runtime.
38
+
39
+ The root entry point carries no Node built-ins. A package test bundles the
40
+ whole root dependency graph for the browser and asserts that no
41
+ `node:child_process`, `node:fs`, `node:path`, or `node:process` import
42
+ survives, so the kernel itself runs anywhere its host does.
43
+
44
+ ## Import forms
45
+
46
+ The root entry point re-exports every module as a namespace:
47
+
48
+ ```ts
49
+ import { Capability, CapabilitySet, GrantStore, HostServices, Permission, Workspace } from "@smthrs/kernel"
50
+ ```
51
+
52
+ Each module is also importable from its own subpath, which is the form the
53
+ API reference uses:
54
+
55
+ ```ts
56
+ import * as GrantStore from "@smthrs/kernel/GrantStore"
57
+ import * as HostServices from "@smthrs/kernel/HostServices"
58
+ ```
59
+
60
+ `Capability` and `Permission` are re-exports. Their modules live in
61
+ [`@smthrs/capability`](/api/capability), so their deep import paths are
62
+ `@smthrs/capability/Capability` and `@smthrs/capability/Permission`.
63
+
64
+ Two subpath shapes are blocked in the export map: `@smthrs/kernel/internal/*`
65
+ and `@smthrs/kernel/*/index`. `@smthrs/kernel/package.json` is exported.
66
+
67
+ ## Test subpaths
68
+
69
+ Two helpers ship for testing. They are published code, not dev-only files;
70
+ `test/HostContract` is an alias for `test/contract`:
71
+
72
+ | Subpath | What it gives you | Platform |
73
+ | ------------------------------------ | --------------------------------------------------------------------------------- | -------- |
74
+ | `@smthrs/kernel/test/TestGrantStore` | `layerAllow`, `layerDeny`, and `layerScripted` grant-store doubles. | any |
75
+ | `@smthrs/kernel/test/contract` | `runHostContract`, the shared behavioral contract every host bundle must satisfy. | Node.js |
76
+
77
+ `test/contract` is Node-only because it uses Node process and
78
+ temporary-directory fixtures. The deterministic host bundle is published by
79
+ `@smthrs/testing/TestHost`; keeping it with the other test utilities avoids a
80
+ kernel-to-platform dependency cycle. To use that host, add a devDependency
81
+ in the consuming workspace package:
82
+
83
+ ```json
84
+ {
85
+ "devDependencies": {
86
+ "@smthrs/testing": "workspace:*"
87
+ }
88
+ }
89
+ ```
90
+
91
+ `test/contract` registers Vitest cases, so importing it requires the declared
92
+ peers, `@effect/vitest@4.0.0-rc.115` and `vitest@5.0.0`. Both are optional: a
93
+ consumer that imports only the kernel or `test/TestGrantStore` needs neither.
94
+ See [Testing](./testing.md) for what each one covers.
95
+
96
+ ## What a working host adds
97
+
98
+ The kernel holds no platform implementations. It decorates ports that
99
+ something else provides, so a composition that actually reaches a machine adds
100
+ a platform bundle:
101
+
102
+ - [`@smthrs/platform-node`](/api/platform-node) implements the five ports on
103
+ Node, attaches the descriptor-relative filesystem executor the kernel
104
+ requires, and ships the process reaper that retires orphaned records.
105
+ - [`@smthrs/platform-bun`](/api/platform-bun) and
106
+ [`@smthrs/platform-browser`](/api/platform-browser) implement the same
107
+ ports for Bun and for the browser.
108
+ - [`@smthrs/journal`](/api/journal) is already a dependency, but a host only
109
+ needs a live `Journal` service when it uses `JournalGrantStore` or the
110
+ durable `ProcessLedger`.
111
+
112
+ ## Next step
113
+
114
+ Guard a host and watch a call get refused in the
115
+ [Quickstart](./quickstart.md).