@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
package/docs/api.md ADDED
@@ -0,0 +1,1532 @@
1
+ ---
2
+ title: "API reference"
3
+ description: "Every public export of @smthrs/kernel: the closed host service list, the guarded decorators over each port, the grant store and its durable events, process containment, and the public test subpaths."
4
+ ---
5
+
6
+ The root entry point re-exports every module as a namespace, and each module is
7
+ also importable from `@smthrs/kernel/<Module>`:
8
+
9
+ ```ts
10
+ import { CapabilitySet, GrantStore, HostServices, Workspace } from "@smthrs/kernel"
11
+ // or
12
+ import * as GrantStore from "@smthrs/kernel/GrantStore"
13
+ ```
14
+
15
+ `@smthrs/kernel/internal/*` and `@smthrs/kernel/*/index` are not public.
16
+ `@smthrs/kernel/package.json` is exported.
17
+
18
+ :::note
19
+ Schema ids (`@smthrs/kernel/GrantEvent/RunGrant` and its siblings), journal event types (`flows.kernel.grant.*`, `flows.host.process-*`), and the `HostServiceIds` slot ids are durable identity rather than internal names. They are written to the journal and read back on replay, so code that reads grant or process history can match on them. They change only when the service behind the id changes.
20
+ :::
21
+
22
+ ## Example
23
+
24
+ ```ts
25
+ import { GrantStore, HostServices, Workspace } from "@smthrs/kernel"
26
+ import * as TestHost from "@smthrs/testing/TestHost"
27
+ import { Effect, FileSystem, Layer } from "effect"
28
+
29
+ const raw = Layer.mergeAll(TestHost.layer({ files: { "/w/a.txt": "hi" } }), Workspace.layer("/w"))
30
+ const read = Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString("/w/a.txt"))
31
+ // Refused with permission_required: no rule authorizes fs:read.
32
+ Effect.runPromise(
33
+ read.pipe(
34
+ Effect.provide(HostServices.layer),
35
+ Effect.provide(Layer.orDie(GrantStore.layer({ attended: false }))),
36
+ Effect.provide(raw),
37
+ Effect.scoped
38
+ )
39
+ )
40
+ ```
41
+
42
+ ## Entry points
43
+
44
+ | Import | Source | Platform |
45
+ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------- |
46
+ | `@smthrs/kernel` | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/src/index.ts) | any |
47
+ | `@smthrs/kernel/test/TestGrantStore` | [src/test/TestGrantStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/src/test/TestGrantStore.ts) | any |
48
+ | `@smthrs/kernel/test/contract` | [src/test/HostContract.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/src/test/HostContract.ts) | Node.js |
49
+
50
+ ## Capability and Permission
51
+
52
+ `Capability` and `Permission` are namespace re-exports from
53
+ [`@smthrs/capability`](/api/capability). That package owns the vocabulary, the
54
+ pattern grammar, policy evaluation, effect tiers, and the typed failure
55
+ contract; their deep import paths are `@smthrs/capability/Capability` and
56
+ `@smthrs/capability/Permission`. This page documents how the kernel applies
57
+ that contract at host boundaries.
58
+
59
+ Rules are ordered and last-match-wins, except that an effective configured deny
60
+ is a hard veto. The default decision is `ask`. `CapabilitySet` supplies the
61
+ ambient authority ceiling, and its public operations can only preserve or
62
+ narrow authority.
63
+
64
+ ```ts
65
+ import { Capability, Permission } from "@smthrs/kernel"
66
+
67
+ const readWorkspace = new Permission.Rule({
68
+ effect: "allow",
69
+ pattern: new Capability.CapabilityPattern({
70
+ action: "fs:read",
71
+ resource: "/workspace/**"
72
+ })
73
+ })
74
+
75
+ const decision = Permission.evaluate(
76
+ [[readWorkspace]],
77
+ Capability.make("fs:read", "/workspace/src/main.ts")
78
+ )
79
+ ```
80
+
81
+ ## CapabilitySet
82
+
83
+ The fiber's monotone authority ceiling.
84
+
85
+ ### CapabilitySet.CapabilitySet
86
+
87
+ ```ts
88
+ interface CapabilitySet {
89
+ readonly groups: ReadonlyArray<ReadonlyArray<CapabilityPattern>>
90
+ }
91
+ ```
92
+
93
+ A normalized conjunction of any-of pattern groups: a capability is allowed only
94
+ when **every** group contains a pattern matching it. Groups and patterns are
95
+ sorted, deduplicated, and frozen on construction. An empty inner group denies
96
+ everything, and any set containing one collapses to that.
97
+
98
+ ### CapabilitySet.fromPatterns
99
+
100
+ ```ts
101
+ const fromPatterns: (patterns: ReadonlyArray<CapabilityPattern>) => CapabilitySet
102
+ ```
103
+
104
+ Authority described by one any-of group.
105
+
106
+ ### CapabilitySet.none
107
+
108
+ ```ts
109
+ const none: CapabilitySet
110
+ ```
111
+
112
+ Empty authority. Its single empty any-of group rejects every capability.
113
+
114
+ ### CapabilitySet.allows
115
+
116
+ ```ts
117
+ const allows: (set: CapabilitySet, capability: Capability) => boolean
118
+ ```
119
+
120
+ Whether every group contains a pattern matching the capability.
121
+
122
+ ### CapabilitySet.intersect
123
+
124
+ ```ts
125
+ const intersect: (left: CapabilitySet, right: CapabilitySet) => CapabilitySet
126
+ ```
127
+
128
+ Concatenates the two group lists and renormalizes. Globs are never synthesized
129
+ or simplified, so intersection cannot invent authority.
130
+
131
+ ### CapabilitySet.equals
132
+
133
+ ```ts
134
+ const equals: (left: CapabilitySet, right: CapabilitySet) => boolean
135
+ ```
136
+
137
+ Structural equality between normalized sets.
138
+
139
+ ### CapabilitySet.current
140
+
141
+ ```ts
142
+ const current: Effect.Effect<CapabilitySet>
143
+ ```
144
+
145
+ The current fiber's ceiling. A fiber that never passed through `attenuate`
146
+ allows every capability, because unrestricted authority is the identity element
147
+ of `intersect`. The backing reference is module-private, so no caller can
148
+ replace the ambient set with a wider one.
149
+
150
+ ### CapabilitySet.attenuate
151
+
152
+ ```ts
153
+ const attenuate: (
154
+ patterns: ReadonlyArray<CapabilityPattern>
155
+ ) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
156
+ ```
157
+
158
+ Runs the effect with the parent ceiling **intersected** with one more any-of
159
+ group. This is the only way authority moves, and it only narrows.
160
+
161
+ ## GrantStore
162
+
163
+ The decision service every decorator consults.
164
+
165
+ ### GrantStore.Service and GrantStore.GrantStore
166
+
167
+ ```ts
168
+ interface Service {
169
+ readonly policy: Effect.Effect<Policy, GrantStoreError>
170
+ readonly check: (
171
+ capability: Capability,
172
+ meta?: Record<string, unknown>
173
+ ) => Effect.Effect<void, PermissionRequired | PermissionDenied | GrantStoreError>
174
+ readonly reply: (
175
+ requestId: string,
176
+ resolution: Resolution,
177
+ pattern?: CapabilityPattern
178
+ ) => Effect.Effect<void, GrantStoreError>
179
+ readonly list: Effect.Effect<ReadonlyArray<PendingRequest>>
180
+ readonly grantEnvelope: (options: EnvelopeGrantOptions) => Effect.Effect<void, GrantStoreError>
181
+ }
182
+
183
+ class GrantStore extends Context.Service<GrantStore, Service>()("@smthrs/kernel/GrantStore") {}
184
+ ```
185
+
186
+ `check` consults, in order: whether the store is open, the fiber's ceiling, and
187
+ the rulesets. `allow` returns void; `deny` fails `permission_denied`; `ask`
188
+ fails `permission_required` on an unattended store and parks the fiber on an
189
+ attended one. `meta` is display metadata for an attended surface and is
190
+ snapshotted, never interpreted.
191
+
192
+ `list` is a frozen snapshot whose `meta` values are the frozen snapshots taken
193
+ at check time. `reply` and `grantEnvelope` persist their decision before
194
+ activating it. The journal write never holds the store's mutation permit: a
195
+ slow or dead journal stalls only the admission that issued it, and a write
196
+ exceeding `maximumPersistMillis` fails with `journal_failed`, leaving the
197
+ request parked for a retry. A concurrent reply to a request whose decision is
198
+ mid-write fails fast with `request_not_found`; a concurrent identical envelope
199
+ admission adopts the in-flight outcome. Admissions reserve rule and envelope
200
+ signature capacity before writing, so distinct concurrent decisions cannot
201
+ exceed either ceiling. Failed or interrupted writes release their reservations;
202
+ successful writes convert them to active grants.
203
+
204
+ ### GrantStore.Policy, PolicyRule and emptyPolicy
205
+
206
+ ```ts
207
+ interface PolicyRule {
208
+ readonly rule: Rule
209
+ readonly ceiling: Pick<CapabilitySet, "groups">
210
+ }
211
+ interface Policy {
212
+ readonly groups: ReadonlyArray<ReadonlyArray<PolicyRule>>
213
+ readonly ceiling: CapabilitySet
214
+ }
215
+ const emptyPolicy: Effect.Effect<Policy>
216
+ ```
217
+
218
+ `policy` takes one immutable snapshot under the mutation permit. Groups retain
219
+ configured, envelope, captured run and remembered rule ordering. Each run rule
220
+ retains its captured ceiling; `ceiling` is the caller's current authority.
221
+ Closed stores fail with `store_closed`. Pending and once approvals provide no
222
+ persistent authority. Custom services with no provable authority can use
223
+ `emptyPolicy`, which opens nothing.
224
+
225
+ ### GrantStore.PendingRequest
226
+
227
+ ```ts
228
+ interface PendingRequest {
229
+ readonly requestId: string
230
+ readonly capability: Capability
231
+ readonly tier: EffectTier
232
+ readonly meta: Record<string, unknown>
233
+ }
234
+ ```
235
+
236
+ One parked request. `tier` is `Capability.tierOf` for the capability, resolved
237
+ against the workspace root.
238
+
239
+ ### GrantStore.Resolution
240
+
241
+ ```ts
242
+ type Resolution = "once" | "run" | "remembered" | "deny"
243
+ ```
244
+
245
+ `once` authorizes the request alone. `run` adds an allow rule for the run and
246
+ requires a `planDigest`. `remembered` adds a rule a journal-backed store
247
+ replays into later processes. `deny` fails the parked request. Adding a rule
248
+ also resolves every other parked request the new rule allows.
249
+
250
+ ### GrantStore.EnvelopeGrantOptions
251
+
252
+ ```ts
253
+ interface EnvelopeGrantOptions {
254
+ readonly planDigest: string
255
+ readonly patterns: ReadonlyArray<CapabilityPattern>
256
+ readonly scope?: "run" | "remembered" | undefined
257
+ }
258
+ ```
259
+
260
+ A bulk approval. `scope` defaults to `"run"`.
261
+
262
+ ### GrantStore.MakeOptions
263
+
264
+ ```ts
265
+ interface MakeOptions {
266
+ readonly attended?: boolean | undefined
267
+ readonly rules?: ReadonlyArray<Rule> | ReadonlyArray<ReadonlyArray<Rule>> | undefined
268
+ readonly runRules?:
269
+ | ReadonlyArray<
270
+ Rule | {
271
+ readonly rule: Rule
272
+ readonly ceiling: ReadonlyArray<ReadonlyArray<CapabilityPattern>>
273
+ }
274
+ >
275
+ | undefined
276
+ readonly envelope?: EnvelopeGrantOptions | undefined
277
+ readonly envelopeSignatures?: ReadonlyArray<string> | undefined
278
+ readonly runId?: string | undefined
279
+ readonly planDigest?: string | undefined
280
+ readonly persist?: Persist | undefined
281
+ }
282
+ ```
283
+
284
+ `attended` defaults to `true`. A flat `rules` value is the configured policy; a
285
+ nested one has configured policy first and replayed remembered grants after.
286
+ `envelopeSignatures` names envelopes that are already durable, so a matching
287
+ construction envelope activates without being persisted again.
288
+
289
+ ### GrantStore.Persist
290
+
291
+ ```ts
292
+ type Persist = (event: GrantEvent) => Effect.Effect<void, GrantStoreError>
293
+ ```
294
+
295
+ A hook that durably records a decision **before** it becomes active. A
296
+ persistence failure leaves the decision inactive. One write may take at most
297
+ `maximumPersistMillis`; a write that never returns is interrupted at the
298
+ deadline and reported as `journal_failed`.
299
+
300
+ ### GrantStore.make and GrantStore.layer
301
+
302
+ ```ts
303
+ const make: (
304
+ options?: MakeOptions
305
+ ) => Effect.Effect<Service, GrantStoreError, Scope.Scope | Workspace>
306
+
307
+ const layer: (options?: MakeOptions) => Layer.Layer<GrantStore, GrantStoreError, Workspace>
308
+ ```
309
+
310
+ Builds an in-memory store. It is scoped: closing the scope rejects every waiter
311
+ with `permission_denied` and `"grant store closed"`, clears the pending map,
312
+ and fails every later call with `store_closed`. Invalid options fail with
313
+ `invalid_resolution`.
314
+
315
+ ### GrantStore.makeNoop and GrantStore.layerNoop
316
+
317
+ ```ts
318
+ const makeNoop: Service
319
+ const layerNoop: Layer.Layer<GrantStore>
320
+ ```
321
+
322
+ An explicit allow-all seam for tests and boot paths. Not a production policy.
323
+
324
+ ### GrantStore.canonicalEnvelopePatterns
325
+
326
+ ```ts
327
+ const canonicalEnvelopePatterns: (
328
+ patterns: ReadonlyArray<CapabilityPattern>
329
+ ) => ReadonlyArray<CapabilityPattern>
330
+ ```
331
+
332
+ Deduplicates an envelope's predicates and sorts the survivors by the code-unit
333
+ order of `Capability.format(pattern)`. An envelope is a set, so canonicalizing
334
+ makes idempotency structural rather than dependent on caller discipline.
335
+
336
+ ### GrantStore.envelopeSignature
337
+
338
+ ```ts
339
+ const envelopeSignature: (
340
+ planDigest: string,
341
+ scope: "run" | "remembered",
342
+ patterns: ReadonlyArray<CapabilityPattern>
343
+ ) => string
344
+ ```
345
+
346
+ The canonical identity of an approval. Two envelopes with the same plan digest,
347
+ scope, and predicate set produce the same signature regardless of order or
348
+ repetition, which is how the store and journal replay recognise an envelope
349
+ that is already durable.
350
+
351
+ ### GrantStore.isValidGrantPattern
352
+
353
+ ```ts
354
+ const isValidGrantPattern: (
355
+ pattern: CapabilityPattern,
356
+ capability: Capability,
357
+ tier: EffectTier,
358
+ workspaceRoot: string
359
+ ) => boolean
360
+ ```
361
+
362
+ Whether a request-scoped grant pattern stays within the request. It refuses a
363
+ different action, a more dangerous effect tier than the request displayed, and
364
+ a wildcard-bearing pattern identical to the resource, which is ambiguous
365
+ because the grammar has no escape.
366
+
367
+ ### GrantStore.isValidEnvelopePattern
368
+
369
+ ```ts
370
+ const isValidEnvelopePattern: (pattern: CapabilityPattern, workspaceRoot: string) => boolean
371
+ ```
372
+
373
+ Whether a bulk envelope pattern preserves exact action and filesystem
374
+ effect-tier boundaries.
375
+
376
+ ### GrantStore limits
377
+
378
+ ```ts
379
+ const maximumRules = 1_024
380
+ const maximumEnvelopePatterns = 256
381
+ const maximumPendingRequests = 1_024
382
+ const maximumMetadataDepth = 16
383
+ const maximumMetadataMembers = 1_024
384
+ const maximumMetadataBytes = 65_536
385
+ const maximumEventBytes = 262_144
386
+ const maximumPersistMillis = 30_000
387
+ const maximumIdentityLength = 4_096
388
+ const maximumCapabilityResourceLength: number // Capability.maxResourceLength
389
+ ```
390
+
391
+ Every bound failure uses `invalid_resolution` and occurs before state or
392
+ journal authority changes. `maximumPersistMillis` instead bounds time: a
393
+ journal write that exceeds it fails the admission with `journal_failed`
394
+ without activating the decision.
395
+
396
+ ## GrantEvent
397
+
398
+ The durable wire shapes a decision is persisted as. The union has exactly five
399
+ members and no other event type is replayed.
400
+
401
+ ### GrantEvent.GrantTier and GrantEvent.GrantScope
402
+
403
+ ```ts
404
+ const GrantTier: Schema.Literals<["sealed", "compensable", "irreversible"]>
405
+ const GrantScope: Schema.Literals<["once", "run", "remembered"]>
406
+ ```
407
+
408
+ ### GrantEvent.OnceGrant
409
+
410
+ ```ts
411
+ class OnceGrant extends Schema.TaggedClass<OnceGrant>()("@smthrs/kernel/GrantEvent/OnceGrant", {
412
+ eventType: Schema.Literal("flows.kernel.grant.once.v1")
413
+ requestId: Schema.String
414
+ runId: Schema.String
415
+ planDigest: Schema.optional(Schema.String)
416
+ capability: Capability
417
+ pattern: CapabilityPattern
418
+ scope: Schema.Literal("once")
419
+ tier: GrantTier
420
+ }) {}
421
+ ```
422
+
423
+ Durable audit evidence, deliberately never replayed as active authority.
424
+
425
+ ### GrantEvent.RunGrant
426
+
427
+ ```ts
428
+ class RunGrant extends Schema.TaggedClass<RunGrant>()("@smthrs/kernel/GrantEvent/RunGrant", {
429
+ eventType: Schema.Literal("flows.kernel.grant.run.v2")
430
+ requestId: Schema.String
431
+ runId: Schema.String
432
+ planDigest: Schema.String
433
+ capability: Capability
434
+ pattern: CapabilityPattern
435
+ ceiling: Schema.Array(Schema.Array(CapabilityPattern))
436
+ scope: Schema.Literal("run")
437
+ tier: GrantTier
438
+ }) {}
439
+ ```
440
+
441
+ Replayed as active authority only for its own run and the current plan digest.
442
+ `ceiling` stores the requesting fiber's normalized conjunction of any-of pattern
443
+ groups. An empty outer array is unrestricted; an empty inner group denies all.
444
+ Replay intersects this ceiling with the constructor's ceiling. Trusted legacy
445
+ `flows.kernel.grant.run.v1` entries fail construction with `invalid_resolution`
446
+ because their captured ceiling cannot be recovered.
447
+
448
+ Both `RunGrant` and `EnvelopeGrant` require `planDigest`; other members make it optional.
449
+
450
+ ### GrantEvent.RememberedGrant
451
+
452
+ ```ts
453
+ class RememberedGrant extends Schema.TaggedClass<RememberedGrant>()(
454
+ "@smthrs/kernel/GrantEvent/RememberedGrant",
455
+ { eventType: Schema.Literal("flows.kernel.grant.remembered.v1") /* as OnceGrant, scope "remembered" */ }
456
+ ) {}
457
+ ```
458
+
459
+ Lives in the dedicated policy run and is replayed into later processes.
460
+
461
+ ### GrantEvent.DeniedGrant
462
+
463
+ ```ts
464
+ class DeniedGrant extends Schema.TaggedClass<DeniedGrant>()("@smthrs/kernel/GrantEvent/DeniedGrant", {
465
+ eventType: Schema.Literal("flows.kernel.grant.denied.v1") /* as OnceGrant */
466
+ }) {}
467
+ ```
468
+
469
+ Audit evidence for a refusal. Like `OnceGrant`, it activates nothing on replay.
470
+
471
+ ### GrantEvent.EnvelopeGrant
472
+
473
+ ```ts
474
+ class EnvelopeGrant extends Schema.TaggedClass<EnvelopeGrant>()("@smthrs/kernel/GrantEvent/EnvelopeGrant", {
475
+ eventType: Schema.Literal("flows.kernel.grant.envelope.v1")
476
+ runId: Schema.String
477
+ planDigest: Schema.String
478
+ patterns: Schema.Array(CapabilityPattern)
479
+ scope: Schema.Literals(["run", "remembered"])
480
+ }) {}
481
+ ```
482
+
483
+ Not attached to any request. `planDigest` binds the decision to the plan shown
484
+ to the approver.
485
+
486
+ ### GrantEvent.GrantEventSchema, decode, and encode
487
+
488
+ ```ts
489
+ const GrantEventSchema: Schema.Union<[OnceGrant, RememberedGrant, RunGrant, DeniedGrant, EnvelopeGrant]>
490
+ type GrantEvent = typeof GrantEventSchema.Type
491
+
492
+ const decode: (input: unknown) => Result<GrantEvent, Schema.SchemaError>
493
+ const encode: (input: unknown) => Result<unknown, Schema.SchemaError>
494
+ ```
495
+
496
+ `decode` is strict about excess properties: an unknown field is a decode
497
+ failure, not something to ignore.
498
+
499
+ ## JournalGrantStore
500
+
501
+ A `GrantStore` that persists to and replays from a
502
+ [`@smthrs/journal`](/api/journal) journal.
503
+
504
+ ### JournalGrantStore.JournalGrantStoreOptions
505
+
506
+ ```ts
507
+ interface JournalGrantStoreOptions {
508
+ readonly runId: string
509
+ readonly policyRunId: string
510
+ readonly sourceId: string
511
+ readonly planDigest: string
512
+ readonly attended?: boolean
513
+ readonly rules?: ReadonlyArray<ReadonlyArray<Rule>>
514
+ readonly envelope?: {
515
+ readonly patterns: ReadonlyArray<CapabilityPattern>
516
+ readonly scope?: "run" | "remembered" | undefined
517
+ }
518
+ }
519
+ ```
520
+
521
+ `policyRunId` is a dedicated run holding remembered-policy events; the journal
522
+ has no global grant projection, so keep the id stable to keep remembered grants
523
+ across runs. `sourceId` is checked during replay, so events from other
524
+ producers cannot activate kernel authority. `planDigest` binds run grants and
525
+ run envelopes to the active plan. `runId` and `policyRunId` must differ, and
526
+ every identity must be non-empty, well-formed, and within
527
+ `GrantStore.maximumIdentityLength`.
528
+
529
+ ### JournalGrantStore.make and JournalGrantStore.layer
530
+
531
+ ```ts
532
+ const make: (
533
+ options: JournalGrantStoreOptions
534
+ ) => Effect.Effect<GrantStore.Service, GrantStoreError, Scope.Scope | Journal | Workspace>
535
+
536
+ const layer: (
537
+ options: JournalGrantStoreOptions
538
+ ) => Layer.Layer<GrantStore, GrantStoreError, Journal | Workspace>
539
+ ```
540
+
541
+ Each decision commits before it is activated. A journal failure is
542
+ `journal_failed` and leaves the decision inactive.
543
+
544
+ Replay accepts only the configured producer and the five known event types,
545
+ rejects malformed or mis-scoped events, treats once and denied events as audit
546
+ evidence only, activates a run grant only for its run and current plan digest,
547
+ and rechecks every replayed pattern for safety. Remembered rules come from the
548
+ policy run and are deduplicated by formatted pattern identity. A policy history
549
+ past the 1,024-rule ceiling, or past the 1,024-envelope-signature ceiling,
550
+ fails closed with a message naming the policy run and the relevant counts. A
551
+ construction envelope is refused rather than persisted once the replayed
552
+ signatures already fill that ceiling, so the history a later process must
553
+ replay cannot outgrow what it will accept.
554
+
555
+ The journal is authoritative permission storage: `SqlJournal` must use the
556
+ `reject` overflow policy, because a dropped grant decision cannot safely be
557
+ treated as persisted.
558
+
559
+ ## HostServices
560
+
561
+ The closed port list and the aggregate decorator.
562
+
563
+ ### HostServices.HostService
564
+
565
+ ```ts
566
+ type HostService =
567
+ | FileSystem.FileSystem
568
+ | Path.Path
569
+ | ChildProcessSpawner
570
+ | Jj
571
+ | HttpClient
572
+ ```
573
+
574
+ Everything that touches the outside world enters Smithers through exactly one
575
+ of these tags: no ambient `node:fs`, no bare `spawn`, no global `Date.now()`.
576
+ A service not on the list cannot be attenuated, denied, or audited.
577
+
578
+ ### HostServices.HostServiceTags
579
+
580
+ ```ts
581
+ const HostServiceTags: readonly [
582
+ FileSystem.FileSystem,
583
+ Path.Path,
584
+ ChildProcessSpawner,
585
+ Jj,
586
+ HttpClient
587
+ ]
588
+ ```
589
+
590
+ The tags themselves, in slot order.
591
+
592
+ ### HostServices.HostServiceIds
593
+
594
+ ```ts
595
+ const HostServiceIds: readonly [
596
+ "effect/FileSystem",
597
+ "effect/Path",
598
+ "effect/process/ChildProcessSpawner",
599
+ "@smthrs/jj/Jj",
600
+ "effect/HttpClient"
601
+ ]
602
+ ```
603
+
604
+ Stable slot identifiers, each the tag key of its slot's service. These are the
605
+ names a slot carries in durable records, so they change only when the service
606
+ behind a slot changes.
607
+
608
+ ### HostServices.layer
609
+
610
+ ```ts
611
+ const layer: Layer.Layer<
612
+ HostService,
613
+ PlatformError,
614
+ HostService | Workspace | GrantStore
615
+ >
616
+ ```
617
+
618
+ The five decorators merged. Every member both requires and provides its own
619
+ slot's tag, so composing this over a raw platform bundle makes the guarded
620
+ implementation shadow the raw one for everything downstream:
621
+
622
+ ```text
623
+ raw platform service
624
+ |
625
+ kernel decorator -> GrantStore
626
+ |
627
+ flow-visible service
628
+ ```
629
+
630
+ `Workspace` is a requirement so the exact same root service reaches both the
631
+ grant store and the filesystem decorator.
632
+
633
+ ## FileSystem
634
+
635
+ The `fs:read` and `fs:write` decorator over Effect's own `FileSystem` tag, plus
636
+ the confinement extensions a host must attach.
637
+
638
+ ### FileSystem.systemTemporaryDirectoryName
639
+
640
+ ```ts
641
+ const systemTemporaryDirectoryName = "<system-temp>"
642
+ ```
643
+
644
+ The sentinel an implicit temporary directory is named with. An implicit
645
+ `makeTempDirectory`, `makeTempDirectoryScoped`, `makeTempFile`, or
646
+ `makeTempFileScoped` is authorized as `fs:write` on
647
+ `path.resolve(workspace.root, "..", systemTemporaryDirectoryName)`. That path
648
+ is outside the workspace root by construction, so granting an ordinary
649
+ workspace write does not grant system temporary-directory access.
650
+
651
+ ### FileSystem.AtomicFileSystemTypeId, AtomicRoot, AtomicRequest, Resolution, AtomicBatchRequest, AtomicBatchValue, AtomicBatchResponse, AtomicResults, AtomicResult, AtomicHandlers, AtomicFileSystem, AtomicHostFileSystem
652
+
653
+ ```ts
654
+ const AtomicFileSystemTypeId: unique symbol
655
+
656
+ interface AtomicRoot {
657
+ readonly boundaryRoot?: string | undefined
658
+ readonly logicalRoot?: string | undefined
659
+ readonly rootIdentity?: string | undefined
660
+ }
661
+
662
+ type AtomicRequest =
663
+ | (AtomicRoot & { readonly operation: "exists"; readonly path: string })
664
+ | (AtomicRoot & {
665
+ readonly operation: "glob"
666
+ readonly pattern: string
667
+ readonly root: string
668
+ readonly options?: { readonly exclude?: ReadonlyArray<string> | undefined } | undefined
669
+ })
670
+ | (AtomicRoot & { readonly operation: "rename"; readonly from: string; readonly to: string })
671
+ | (AtomicRoot & { readonly operation: "resolve"; readonly path: string })
672
+ | (AtomicRoot & { readonly operation: "batch"; readonly requests: ReadonlyArray<AtomicBatchRequest> })
673
+ // ...one member per operation: makeDirectory, readDirectory, readFile,
674
+ // readFileString, readLink, realPath, remove, stat, writeFile, writeFileString
675
+
676
+ interface Resolution {
677
+ readonly path: string
678
+ readonly target: string | null
679
+ }
680
+
681
+ type AtomicBatchRequest = BatchRequest | { readonly operation: "resolve"; readonly path: string }
682
+ type AtomicBatchValue = BatchValue | { readonly operation: "resolve"; readonly resolution: Resolution }
683
+ // AtomicBatchResponse: BatchResponse whose entries carry AtomicBatchValue
684
+
685
+ interface AtomicResults {
686
+ readonly exists: boolean
687
+ readonly glob: Array<string>
688
+ readonly readFile: Uint8Array
689
+ readonly stat: FileSystem.File.Info
690
+ readonly rename: void
691
+ readonly resolve: Resolution
692
+ readonly batch: AtomicBatchResponse
693
+ // ...one entry per operation
694
+ }
695
+
696
+ type AtomicResult<R extends AtomicRequest> = AtomicResults[R["operation"]]
697
+
698
+ type AtomicHandlers = {
699
+ readonly [K in keyof AtomicResults]: (
700
+ request: Extract<AtomicRequest, { readonly operation: K }>
701
+ ) => Effect.Effect<AtomicResults[K], PlatformError>
702
+ }
703
+
704
+ interface AtomicFileSystem {
705
+ readonly noFollowAuthorization?: true | undefined
706
+ readonly execute: <R extends AtomicRequest>(request: R) => Effect.Effect<AtomicResult<R>, PlatformError>
707
+ readonly isolated?: FileSystem.FileSystem | undefined
708
+ }
709
+
710
+ type AtomicHostFileSystem = FileSystem.FileSystem & {
711
+ readonly [AtomicFileSystemTypeId]: AtomicFileSystem
712
+ }
713
+ ```
714
+
715
+ The host-private extension for race-free, descriptor-relative operations. A
716
+ plain path-based filesystem cannot provide confinement, because an attacker can
717
+ replace any checked component before the delegate resolves it. `isolated` is
718
+ the escape hatch for a filesystem already confined by an enforceable boundary,
719
+ used for methods not expressible as one descriptor-relative request.
720
+
721
+ A request is discriminated by `operation` and carries exactly the operands that
722
+ operation needs, and `execute` answers the result that operation names rather
723
+ than a type its caller picked. The wire shape is unchanged: every member is the
724
+ same flat JSON object, so a journaled or serialized request still decodes. An
725
+ executor that dispatches in process can be written as an `AtomicHandlers`
726
+ record, which does not compile while an operation is unimplemented.
727
+
728
+ An executor that sets `noFollowAuthorization` answers `resolve`, alone and as
729
+ a batch member, and the guarded layer authorizes through it instead of host
730
+ pathnames. `resolve` walks from the pinned root without following any link. When
731
+ `target` is `null`, `path` is the whole request in on-disk spelling, with a
732
+ missing component and its descendants as requested. Otherwise `path` is the
733
+ first link on the way and `target` is its text.
734
+
735
+ ### FileSystem.withAtomicFileSystem
736
+
737
+ ```ts
738
+ const withAtomicFileSystem: (
739
+ fileSystem: FileSystem.FileSystem,
740
+ atomic: AtomicFileSystem
741
+ ) => AtomicHostFileSystem
742
+ ```
743
+
744
+ Attaches a trusted platform's descriptor-relative executor to its service. The
745
+ executor is attached to the supplied object and the same identity is returned,
746
+ so a host attaches exactly once at its boundary and retains no undecorated
747
+ alias.
748
+
749
+ ### FileSystem.withIsolatedFileSystem
750
+
751
+ ```ts
752
+ const withIsolatedFileSystem: (fileSystem: FileSystem.FileSystem) => AtomicHostFileSystem
753
+ ```
754
+
755
+ Attests that a filesystem is confined as a whole, for browser and test volumes
756
+ that cannot address the host filesystem at all. **Throws** on a filesystem that
757
+ already carries a descriptor-relative executor: that executor is the stronger
758
+ guarantee, and a path-delegating attestation would route `access`, `copy`,
759
+ `chmod`, `link`, `symlink`, `open`, `watch`, `sink`, `stream`, and every
760
+ `makeTemp*` call back through pathnames after the capability check.
761
+
762
+ ### FileSystem.FileSystemFault
763
+
764
+ ```ts
765
+ class FileSystemFault extends Schema.TaggedError<FileSystemFault>()("@smthrs/kernel/FileSystemFault", {
766
+ code: "executor_already_present" | "isolated_operation_unsupported" | "workspace_identity_changed"
767
+ message: string
768
+ })
769
+ ```
770
+
771
+ The tagged failure of a broken composition. `withIsolatedFileSystem` throws
772
+ `executor_already_present`. A guarded batch whose root no longer names the
773
+ authorized descriptor reports `workspace_identity_changed` as the `cause` of a
774
+ `Busy` `PlatformError`. `isolated_operation_unsupported` is a defect: an
775
+ attested volume never advertises `resolve` or `batch`.
776
+
777
+ ### FileSystem.confined
778
+
779
+ ```ts
780
+ const confined: (
781
+ fileSystem: FileSystem.FileSystem,
782
+ root: string
783
+ ) => Effect.Effect<FileSystem.FileSystem, PlatformError, Path.Path>
784
+
785
+ const isConfinable: (fileSystem: FileSystem.FileSystem) => boolean
786
+
787
+ const requireConfinable: (fileSystem: FileSystem.FileSystem, root: string) => Effect.Effect<void, PlatformError>
788
+
789
+ const confinedRoot: (fileSystem: FileSystem.FileSystem) => string | undefined
790
+ ```
791
+
792
+ A filesystem view confined to `root` without capability checks, for engine
793
+ machinery that must not ask a grant store about its own bookkeeping but must
794
+ never be redirected outside the workspace. Every path operation is one
795
+ `AtomicRequest` against `root`, pinned when the view is built by canonical path
796
+ and `device:inode` identity: the same request the guarded `layer` sends, minus
797
+ the grant check. A symlink on the path, a hard-linked file, `..` traversal, or
798
+ a replaced root is refused by the host, never followed. The root must exist.
799
+
800
+ An isolated volume, or the guarded `layer` service rooted at the same
801
+ workspace, is returned as it is. A path-based host fails with
802
+ `PermissionDenied`, as do `open`, `stream`, `sink`, `copy`, `link`, `symlink`,
803
+ `watch`, `truncate`, `utimes`, `access`, and every `makeTemp*` call.
804
+ `isConfinable` and `requireConfinable` answer the same question at composition
805
+ time, before the root is known to exist.
806
+ `confinedRoot` names the root a filesystem is already confined to (a
807
+ `confined` view's pinned root, or the guarded `layer` service's workspace
808
+ root), and is `undefined` for any other filesystem, so machinery sharing a
809
+ filesystem with its caller confines to the root the caller's paths resolve
810
+ against.
811
+
812
+ ### FileSystem.canonicalResource
813
+
814
+ ```ts
815
+ const canonicalResource: (
816
+ fileSystem: FileSystem.FileSystem,
817
+ path: Path.Path,
818
+ workspaceRoot: string,
819
+ value: string
820
+ ) => Effect.Effect<string, PlatformError>
821
+ ```
822
+
823
+ Resolves a path through every existing ancestor and maps a canonical path
824
+ inside the workspace back to the stable logical workspace root, so an existing
825
+ symlink cannot turn an inside-workspace grant into outside authority while
826
+ resources stay stable when the root itself is a symlink.
827
+
828
+ ### FileSystem.layer
829
+
830
+ ```ts
831
+ const layer: Layer.Layer<
832
+ FileSystem.FileSystem,
833
+ PlatformError,
834
+ FileSystem.FileSystem | Path.Path | Workspace | GrantStore
835
+ >
836
+ ```
837
+
838
+ The decorator. Before a grant check it resolves the canonical resource and
839
+ refuses unsafe hard-linked files. After any grant suspension it resolves the
840
+ resource again and refuses if the path now names something else. Open handles
841
+ bind authorization to the descriptor's `device:inode` identity and recheck it
842
+ on guarded handle operations. Option records, nested arrays, and maps are
843
+ snapshotted before any suspension. A host declaring neither extension fails
844
+ every relevant path, directory, stream, glob, and handle operation closed.
845
+
846
+ Refusals are `PlatformError` with reason `PermissionDenied`, carrying the
847
+ kernel failure on `cause`; `Permission.fromPlatformError` reads it back.
848
+
849
+ ## ChildProcessSpawner
850
+
851
+ The `proc:spawn` decorator over Effect's own spawner tag.
852
+
853
+ ### ChildProcessSpawner.ChildProcessSpawner and make
854
+
855
+ ```ts
856
+ export { ChildProcessSpawner, make } from "effect/unstable/process/ChildProcessSpawner"
857
+ ```
858
+
859
+ Effect's tag and constructor, re-exported unchanged so the kernel namespace
860
+ stays one-stop. `make` derives the full six-method surface from one `spawn`, so
861
+ `exitCode`, `string`, `lines`, and both `stream` helpers can never bypass what
862
+ `spawn` was given.
863
+
864
+ ### ChildProcessSpawner.makeNoop and layerNoop
865
+
866
+ ```ts
867
+ const makeNoop: (overrides?: Partial<ChildProcessSpawner["Service"]>) => ChildProcessSpawner["Service"]
868
+ const layerNoop: (overrides?: Partial<ChildProcessSpawner["Service"]>) => Layer.Layer<ChildProcessSpawner>
869
+ ```
870
+
871
+ An unavailable spawner. Every derived helper reports the missing host as a
872
+ `NotFound` `PlatformError` naming the command line, so an unconfigured
873
+ capability answers rather than vanishing.
874
+
875
+ ### ChildProcessSpawner.layer
876
+
877
+ ```ts
878
+ const layer: Layer.Layer<
879
+ ChildProcessSpawner,
880
+ never,
881
+ ChildProcessSpawner | GrantStore | Path.Path | Workspace
882
+ >
883
+ ```
884
+
885
+ The decorator. The check is suspended into the spawn itself, so building a
886
+ `Command` or a stream neither requests permission nor starts a process. The
887
+ check runs once per pipeline stage, left to right, before any stage starts.
888
+ Each stage's resource is `CommandLine.resource(stage, context)`: the rendered
889
+ line, with a `shell: true` line that holds shell control syntax marked as
890
+ `sh -c '<line>'`, environment overrides the child would not inherit named in an
891
+ `env <NAME>… --` prefix, and a working directory outside `Workspace.root`
892
+ named in a `cwd <path> --` prefix. When the host filesystem is in the layer's
893
+ context, both are compared by real path: a symlink inside the workspace that
894
+ leads out of it is named as its target, and a root reached through
895
+ `/private` on macOS still counts as inside. The spawner passes this process's
896
+ environment as `context.ambient`. Pipeline `from`/`to` routing is not part of
897
+ what a grant authorizes. `cwd` and the **names** of overridden environment
898
+ variables also reach an attended surface as display metadata; the values do
899
+ not. A
900
+ command that cannot be snapshotted fails with an `InvalidData` `PlatformError`.
901
+ A command with no `cwd`, or a relative one, runs in `Workspace.root`, never the
902
+ process's own directory, and the check sees that resolved directory.
903
+
904
+ ## ProcessConfinement
905
+
906
+ The host service required for guarded processes, imported from
907
+ `@smthrs/kernel/ProcessConfinement`.
908
+ After every process permission check succeeds, `ChildProcessSpawner.layer`
909
+ derives one profile and wraps every pipeline stage before spawning anything.
910
+ Absent this service, spawning fails with a `NotFound` `PlatformError` before
911
+ any child starts. `layerNoop` explicitly opts out of OS confinement. Wrapper
912
+ failures start no child.
913
+
914
+ ```ts
915
+ interface Profile {
916
+ readonly workspaceRoot: string
917
+ readonly reads: ReadonlyArray<string>
918
+ readonly writes: ReadonlyArray<string>
919
+ readonly readOnly: ReadonlyArray<string>
920
+ readonly network: "none" | "open"
921
+ }
922
+ interface Service {
923
+ readonly confine: (
924
+ command: ChildProcess.StandardCommand,
925
+ profile: Profile
926
+ ) => Effect.Effect<ChildProcess.StandardCommand, PlatformError, Scope.Scope>
927
+ }
928
+ class ProcessConfinement extends Context.Service<ProcessConfinement, Service>()(
929
+ "@smthrs/kernel/ProcessConfinement"
930
+ ) {}
931
+ const profile: (
932
+ grants: GrantStore.Service,
933
+ workspaceRoot: string,
934
+ path: Path.Path
935
+ ) => Effect.Effect<Profile, GrantStoreError>
936
+ const makeNoop: Service
937
+ const layerNoop: Layer.Layer<ProcessConfinement>
938
+ ```
939
+
940
+ Paths are workspace-relative, with `.` naming the root. Projection proves a
941
+ subset of effective authority using whole-tree coverage, configured deny
942
+ precedence and captured/current ceilings. Literal filesystem permissions and
943
+ complex globs open no tree unless a separate whole-tree grant covers it.
944
+ Universal `*` and `**` resources cover the workspace. Writable trees also
945
+ require read authority over the same tree and write authority for its directory
946
+ entry. Native writable mounts expose existing data and directory metadata.
947
+ Unrepresentable refusals conservatively close affected access; `readOnly` is
948
+ currently empty. Narrow tree ceilings can produce a smaller allowed tree.
949
+ Network opens only when unrestricted get, post and explicit private authority
950
+ are all proved. Restricted egress remains closed. Host confinement may also
951
+ provide system reads needed to launch programs and mask credential paths.
952
+ `makeNoop` and `layerNoop` explicitly choose unconfined execution.
953
+
954
+ ## ChildProcessEnvironment
955
+
956
+ Least-authority construction for a child process's replacement environment.
957
+
958
+ | Export | Type | Meaning |
959
+ | ----------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
960
+ | `inheritedNames` | `ReadonlyArray<string>` | `PATH`, `HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`, and `SHELL`; `LC_*` is admitted by prefix. |
961
+ | `credentialNamePattern` | `RegExp` | The credential-name rule shared with `@smthrs/model/Auth`, including complete or separator-delimited `token`, `key`, `key_id`, and `pat` suffixes. |
962
+ | `isCredentialName` | `(name: string) => boolean` | Tests one name against that rule. |
963
+ | `isInheritedName` | `(name: string) => boolean` | Tests one name against `inheritedNames` and the `LC_*` prefix, case-insensitively. |
964
+ | `make` | `(ambient, declared?) => Record<string, string>` | Selects bootstrap names, withholds sensitive ambient names, and overlays explicit declarations. |
965
+
966
+ `make` returns a null-prototype record suitable for `CommandOptions.env` with
967
+ `extendEnv: false`. An explicitly declared name is applied last, even when it
968
+ looks sensitive; an `undefined` declaration removes an inherited name.
969
+
970
+ ## ContainedSpawner
971
+
972
+ Kill deadlines, platform lifecycle preparation, and ledger recording over the same spawner tag.
973
+
974
+ ### ContainedSpawner.defaultGraceMs
975
+
976
+ ```ts
977
+ const defaultGraceMs = 2000
978
+ ```
979
+
980
+ Milliseconds between the `SIGTERM` that asks a child to stop and the `SIGKILL`
981
+ that makes it.
982
+
983
+ ### ContainedSpawner.Options
984
+
985
+ ```ts
986
+ interface Options {
987
+ readonly graceMs?: number | undefined
988
+ readonly platform?: string | undefined
989
+ }
990
+ ```
991
+
992
+ `platform` is `process.platform` spelling, and decides only whether a command
993
+ naming no `detached` option gets a process group of its own. It defaults to
994
+ `"linux"`, the detaching branch.
995
+
996
+ ### ContainedSpawner.Lifecycle
997
+
998
+ ```ts
999
+ type Lifecycle = (
1000
+ command: ChildProcess.StandardCommand,
1001
+ spawn: (command: ChildProcess.StandardCommand) => Effect.Effect<ChildProcessHandle, PlatformError, Scope>
1002
+ ) => Effect.Effect<
1003
+ {
1004
+ readonly handle: ChildProcessHandle
1005
+ readonly activate: Effect.Effect<void, PlatformError, Scope>
1006
+ readonly settled: Effect.Effect<boolean>
1007
+ },
1008
+ PlatformError,
1009
+ Scope
1010
+ >
1011
+ ```
1012
+
1013
+ Preparation owns cleanup before it can fail or be interrupted. It returns an
1014
+ identity to record and an idempotent `activate` effect. The kernel records
1015
+ that identity with the executable it prepared before activation can execute
1016
+ the target. Failed startup closes its child scope immediately; successful
1017
+ startup attaches the whole logical command to the caller's scope. Only
1018
+ `settled === true` permits ledger retirement. A target exit alone does not
1019
+ prove its children ended.
1020
+
1021
+ Node and Bun hosts install `ProcessReaper.layerSpawner` from
1022
+ `@smthrs/platform-node`, which composes the native adapter with
1023
+ `ProcessReaper.processLifecycle`. Use that factory for a smaller Node/Bun
1024
+ composition, or provide a lifecycle explicitly when implementing a custom host.
1025
+
1026
+ ### ContainedSpawner.isContained
1027
+
1028
+ ```ts
1029
+ const isContained: (spawner: ChildProcessSpawner["Service"]) => boolean
1030
+ ```
1031
+
1032
+ Checks whether the service declares a platform lifecycle. A deadline-only
1033
+ wrapper does not qualify. The declaration interoperates across ESM and CommonJS
1034
+ and is preserved by the kernel permission decorator. It is a trusted
1035
+ composition check, not proof that a caller-supplied lifecycle is honest.
1036
+
1037
+ ### ContainedSpawner.withContainment
1038
+
1039
+ ```ts
1040
+ const withContainment: (command: ChildProcess.Command, options?: Options) => ChildProcess.Command
1041
+ ```
1042
+
1043
+ Rewrites a command to carry the escalation deadline. Both legs of a pipeline
1044
+ receive it; a command that already names `killSignal` or `forceKillAfter` keeps
1045
+ the policy its caller chose.
1046
+
1047
+ ### ContainedSpawner.groupOf
1048
+
1049
+ ```ts
1050
+ const groupOf: (command: ChildProcess.Command, pid: number, platform?: string) => number | null
1051
+ ```
1052
+
1053
+ The process group to record, or `null` when the child leads none. It takes the
1054
+ platform because Effect detaches a child that names no `detached` option
1055
+ everywhere except win32, and a win32 record claiming `pgid === pid` would name
1056
+ a group the child does not lead.
1057
+
1058
+ ### ContainedSpawner.layer
1059
+
1060
+ ```ts
1061
+ const layer: (
1062
+ options?: Options,
1063
+ lifecycle?: Lifecycle
1064
+ ) => Layer.Layer<ChildProcessSpawner, never, ChildProcessSpawner | ProcessLedger>
1065
+ ```
1066
+
1067
+ Applies the policy to every spawn. Each pipeline leg is prepared and recorded
1068
+ separately; the aggregate handle exposes the rightmost leg's output and status.
1069
+ A failed record prevents target activation and closes the prepared owner's
1070
+ scope. Startup failure in a later pipeline leg also closes earlier legs.
1071
+ Cleanup failure or an unverified `settled` result retains the ledger record.
1072
+
1073
+ Compose the permission decorator **above** containment so it authorizes the
1074
+ caller's whole command before expansion. A decorator below containment sees
1075
+ platform preparation commands, which may differ from the caller's command.
1076
+ Both layers provide the spawner tag they require, but their order has this
1077
+ semantic effect.
1078
+
1079
+ ## ProcessLedger
1080
+
1081
+ The host's durable record of the processes it started.
1082
+
1083
+ ### ProcessLedger event types and identity
1084
+
1085
+ ```ts
1086
+ const SpawnedEventType = "flows.host.process-spawned.v1"
1087
+ const ExitedEventType = "flows.host.process-exited.v1"
1088
+ const ReapedEventType = "flows.host.process-reaped.v1"
1089
+ const SkippedEventType = "flows.host.process-reap-skipped.v1"
1090
+ const sourceId = "@smthrs/kernel/ProcessLedger"
1091
+ const hostRunId: (hostId: string) => JournalEvent.RunId // `flows.host:${hostId}`
1092
+ ```
1093
+
1094
+ Records are ownerless journal entries on the host run. A successful reap and a
1095
+ safety refusal retire a record with different event types, so an operator can
1096
+ tell them apart.
1097
+
1098
+ ### ProcessLedger.Spawned and ProcessRecord
1099
+
1100
+ ```ts
1101
+ interface Spawned {
1102
+ readonly pid: number
1103
+ readonly pgid: number | null
1104
+ readonly commandDigest: string
1105
+ }
1106
+
1107
+ const ProcessRecord: Schema.Struct<{
1108
+ pid: Schema.Int
1109
+ pgid: Schema.NullOr<Schema.Int>
1110
+ hostId: Schema.String
1111
+ ownerPid: Schema.Int
1112
+ startedAtMs: Schema.Int
1113
+ commandDigest: Schema.String
1114
+ }>
1115
+ type ProcessRecord = typeof ProcessRecord.Type
1116
+ ```
1117
+
1118
+ `commandDigest` is the executable alone, as `CommandLine.executable` renders
1119
+ it. Arguments carry credentials and these records are permanent, so the ledger
1120
+ never writes one down; a reader that acts on a record matches it by pid and
1121
+ process group.
1122
+
1123
+ ### ProcessLedger.Service and ProcessLedger.ProcessLedger
1124
+
1125
+ ```ts
1126
+ interface Service {
1127
+ readonly record: (spawned: Spawned) => Effect.Effect<ProcessRecord, JournalError>
1128
+ readonly release: (record: ProcessRecord) => Effect.Effect<void, JournalError>
1129
+ readonly reaped: (record: ProcessRecord) => Effect.Effect<void, JournalError>
1130
+ readonly skipped: (record: ProcessRecord, reason: string) => Effect.Effect<void, JournalError>
1131
+ readonly live: Effect.Effect<ReadonlyArray<ProcessRecord>>
1132
+ readonly orphans: Effect.Effect<ReadonlyArray<ProcessRecord>>
1133
+ }
1134
+
1135
+ class ProcessLedger extends Context.Service<ProcessLedger, Service>()("@smthrs/kernel/ProcessLedger") {}
1136
+ ```
1137
+
1138
+ Every write carries the journal's failure to its caller: a swallowed write
1139
+ leaves a child no incarnation of the host can discover. `live` is this
1140
+ incarnation's unreleased records; `orphans` replays the host run and returns
1141
+ the records whose owner pid is not this incarnation's.
1142
+
1143
+ ### ProcessLedger.Options
1144
+
1145
+ ```ts
1146
+ interface Options {
1147
+ readonly hostId: string
1148
+ readonly ownerPid: number
1149
+ }
1150
+ ```
1151
+
1152
+ ### ProcessLedger.make, layer, makeMemory, and layerMemory
1153
+
1154
+ ```ts
1155
+ const make: (options: Options) => Effect.Effect<Service, never, Journal>
1156
+ const layer: (options: Options) => Layer.Layer<ProcessLedger, never, Journal>
1157
+ const makeMemory: (options: Options) => Effect.Effect<Service>
1158
+ const layerMemory: (options: Options) => Layer.Layer<ProcessLedger>
1159
+ ```
1160
+
1161
+ The journal-backed forms inherit a crashed incarnation's processes; the memory
1162
+ forms keep only the current incarnation's bookkeeping and inherit nothing.
1163
+
1164
+ ## CommandLine
1165
+
1166
+ One renderer shared by the `proc:spawn` capability resource and the
1167
+ interpreters that execute the line, so a granted capability and the command a
1168
+ browser actually runs cannot drift apart. `resource` builds the grant identity
1169
+ from that same line. The module is pure string handling.
1170
+
1171
+ ### CommandLine.quote
1172
+
1173
+ ```ts
1174
+ const quote: (token: string) => string
1175
+ ```
1176
+
1177
+ POSIX-quotes one token, leaving tokens made only of
1178
+ `A-Za-z0-9_@%+=:,./-` alone.
1179
+
1180
+ ### CommandLine.render
1181
+
1182
+ ```ts
1183
+ const render: (command: ChildProcess.Command) => string
1184
+ ```
1185
+
1186
+ Renders a command as one shell line. A standard command with `shell: true`
1187
+ renders its tokens verbatim; a custom shell renders as an explicit
1188
+ `<shell> -c <line>` so the selected executable is part of the resource; without
1189
+ `shell`, every token is quoted to preserve literal argv semantics. A
1190
+ `PipedCommand` renders with `|` between its sides. `from` and `to` pipe options
1191
+ are not expressible this way and are ignored, so capability checks see the
1192
+ commands and never the plumbing. The rendering is POSIX-only by contract.
1193
+
1194
+ ### CommandLine.resource
1195
+
1196
+ ```ts
1197
+ interface ResourceContext {
1198
+ readonly ambient?: Readonly<Record<string, string | undefined>>
1199
+ readonly contains?: (directory: string) => boolean
1200
+ }
1201
+ const resource: (command: ChildProcess.StandardCommand, context?: ResourceContext) => string
1202
+ const stages: (command: ChildProcess.Command) => ReadonlyArray<ChildProcess.StandardCommand>
1203
+ ```
1204
+
1205
+ The `proc:spawn` capability resource for one stage: `render`, with three
1206
+ additions so a prefix grant such as `git status *` authorizes only what the
1207
+ stage names.
1208
+
1209
+ - A `shell: true` stage whose line holds `;`, `&`, `|`, a backtick, `$`, `<`,
1210
+ `>`, `(`, `)`, or a line break is marked as `sh -c '<line>'`. `git status *`
1211
+ therefore does not match `git status; curl x | sh`. A simple shell line such
1212
+ as `git status --short` keeps its verbatim resource, and so does one that
1213
+ only adds an fd duplication (`2>&1`, `>&2`) or a discard to `/dev/null`.
1214
+ - A stage that sets an environment variable the child would not otherwise
1215
+ inherit is prefixed with `env <NAME>… --`, sorted names only. That is every
1216
+ name outside the bootstrap set (`ChildProcessEnvironment.isInheritedName`)
1217
+ and a bootstrap name (`PATH`, `HOME`, and the rest) whose value differs from
1218
+ `context.ambient`. A grant for `git status` does not cover `git status` run
1219
+ with `GIT_CONFIG_PARAMETERS`, `GIT_SSH_COMMAND`, `LD_PRELOAD`,
1220
+ `NODE_OPTIONS`, or a `PATH` or `HOME` pointing somewhere new; grant
1221
+ `env GIT_SSH_COMMAND -- git fetch` to allow that override. Values never
1222
+ enter the resource. Omitted, `context.ambient` is empty and every declared
1223
+ bootstrap name is named.
1224
+ - A stage whose `cwd` `context.contains` rejects is prefixed with
1225
+ `cwd <path> --`. The spawner's `contains` accepts `Workspace.root` and
1226
+ everything below it. Omitted, every `cwd` is named. `context.resolvedCwd`,
1227
+ the `cwd` with symlinks resolved, replaces the lexical `cwd` for that check
1228
+ and in the prefix.
1229
+
1230
+ `stages` lists a command's stages from left to right. A pipeline has no single
1231
+ resource: the spawner checks every stage, so `git status | sh` needs a grant
1232
+ for `sh` too.
1233
+
1234
+ ### CommandLine.executable
1235
+
1236
+ ```ts
1237
+ const executable: (command: ChildProcess.Command) => string
1238
+ ```
1239
+
1240
+ The program a command runs, without its arguments: `command` itself when no
1241
+ shell parses it, the leading token of the line when one does, and one name per
1242
+ stage of a pipeline joined with `|`. Durable process records name their program
1243
+ this way rather than with `render`, because arguments carry credentials and a
1244
+ journal entry is permanent.
1245
+
1246
+ ### CommandLine.cwd and CommandLine.env
1247
+
1248
+ ```ts
1249
+ const cwd: (command: ChildProcess.Command) => string | undefined
1250
+ const env: (command: ChildProcess.Command) => Record<string, string | undefined> | undefined
1251
+ ```
1252
+
1253
+ The working directory and environment overrides, taking the leftmost stage of a
1254
+ pipeline, which is the stage `setCwd` and the spawners treat as the pipeline's
1255
+ own.
1256
+
1257
+ ## HttpClient
1258
+
1259
+ The `net:get`, `net:post`, and `model:call` decorator over Effect's own HTTP
1260
+ client tag. There is no Smithers transport port beneath it.
1261
+
1262
+ ### HttpClient.HttpClient and make
1263
+
1264
+ ```ts
1265
+ export { HttpClient, make } from "effect/unstable/http/HttpClient"
1266
+ ```
1267
+
1268
+ Effect's tag and constructor, unchanged.
1269
+
1270
+ ### HttpClient.authorizePreflight
1271
+
1272
+ `authorizePreflight(request, effect)` checks the ordinary HTTP capability before
1273
+ running network preflight work such as DNS resolution. The guarded client can
1274
+ consume that admission once for the same capability and grant store. Redirects,
1275
+ retries, and different stores require fresh authorization. The effect requires
1276
+ the host's `GrantStore`, including when the underlying client captures it.
1277
+
1278
+ ### HttpClient.ModelCall and withModelCall
1279
+
1280
+ ```ts
1281
+ const ModelCall: Context.Reference<string | undefined>
1282
+ const withModelCall: (modelId: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
1283
+ ```
1284
+
1285
+ A model call is not a general `net:*` effect: the same host answers many
1286
+ models, and a grant for one must not be a grant for the rest. Effect's tag has
1287
+ no room for an extra method, so the intent rides on a context reference. Inside
1288
+ `withModelCall`, the action is `model:call` and the resource gains
1289
+ `/<model id>`.
1290
+
1291
+ ### HttpClient.toHttpClientError and fromHttpClientError
1292
+
1293
+ ```ts
1294
+ const toHttpClientError: (options: {
1295
+ readonly request: HttpClientRequest
1296
+ readonly error: PermissionError
1297
+ }) => HttpClientError
1298
+
1299
+ const fromHttpClientError: (error: HttpClientError) => Option.Option<PermissionErrorPayload>
1300
+ ```
1301
+
1302
+ The projection and its inverse. The reason is always a `TransportError`,
1303
+ because the request did not leave the host; `description` carries
1304
+ `Permission.formatError` and `cause` carries the structured failure. Recovery
1305
+ returns the original cause as data-only `PermissionErrorPayload` when a
1306
+ `TransportError` cause passes `Permission.isPermissionError`. This validates
1307
+ structure, not origin; establish producer or request identity separately across
1308
+ a trust boundary. Import `decodePermissionError` from `@smthrs/capability`
1309
+ for a yieldable instance.
1310
+
1311
+ ### HttpClient.makeNoop and layerNoop
1312
+
1313
+ ```ts
1314
+ const makeNoop: () => HttpClient
1315
+ const layerNoop: () => Layer.Layer<HttpClient>
1316
+ ```
1317
+
1318
+ An unavailable client. Every request fails with a `TransportError` naming the
1319
+ request and the cause `"HTTP is unavailable on this host"`.
1320
+
1321
+ ### HttpClient.layer
1322
+
1323
+ ```ts
1324
+ const layer: Layer.Layer<HttpClient, never, HttpClient | GrantStore>
1325
+ ```
1326
+
1327
+ The decorator. GET and HEAD use `net:get` and every other method uses
1328
+ `net:post`. For an `https:` URL the resource is the lowercased URL host; for
1329
+ any other scheme it is `<scheme>//<lowercased host>`. In other words `https` is
1330
+ the implicit scheme: `https://EXAMPLE.test/x` names `example.test`, while
1331
+ `http://EXAMPLE.test/x` names `http://example.test`, so a host grant never
1332
+ authorizes a cleartext downgrade. Requests are snapshotted before any
1333
+ suspension. By default, redirects are followed **above** the guard, so every hop re-enters
1334
+ authorization independently. The client below the guard must not follow
1335
+ redirects on its own. The decorator forces `RequestInit { redirect: "manual" }`
1336
+ for each request it executes, which covers a plain `FetchHttpClient.layer`; a
1337
+ custom client that ignores `RequestInit` must meet that precondition itself.
1338
+
1339
+ A caller that provides `FetchHttpClient.RequestInit` with `redirect: "manual"`
1340
+ receives one authorized hop, including its redirect response. The caller owns
1341
+ any redirect loop and its hop limit; each subsequent request through this
1342
+ client still requires authorization. Other caller options are preserved.
1343
+ The raw host client's private manual-redirect setting does not disable the
1344
+ decorator's default redirect following.
1345
+
1346
+ ## Jj
1347
+
1348
+ ### Jj re-exports
1349
+
1350
+ ```ts
1351
+ export { Jj, layerNoop, make, makeNoop } from "@smthrs/jj"
1352
+ ```
1353
+
1354
+ The tag and its constructors, unchanged.
1355
+
1356
+ ### Jj.layer
1357
+
1358
+ ```ts
1359
+ const layer: Layer.Layer<Jj, never, Jj | FileSystem.FileSystem | Path.Path | Workspace | GrantStore>
1360
+ ```
1361
+
1362
+ Operation-specific capability checks: `jj:status` on `"."`, `jj:diff` on
1363
+ `<from>:<to>`, `jj:snapshot` on the message, `jj:restore` and `jj:revert` on
1364
+ the change id, `jj:workspace-add` on the canonicalized destination (which also
1365
+ requires `fs:write` on it), `jj:workspace-forget` on the workspace name, and
1366
+ `jj:root` on the canonicalized starting directory, and `jj:op-restore` on the
1367
+ operation id. `workspaceAdd` and `root`
1368
+ canonicalize through the **raw** filesystem first, so an existing symlink
1369
+ cannot turn an inside-workspace grant into outside authority or separate the
1370
+ authorized path from the directory jj runs in.
1371
+
1372
+ Smithers owns this service, so its interface names
1373
+ `Permission.PermissionError` directly and nothing is projected.
1374
+
1375
+ ## Path
1376
+
1377
+ ```ts
1378
+ type Path = EffectPath.Path
1379
+ const Path = EffectPath.Path
1380
+ const layer: Layer.Layer<Path, never, Path | Workspace>
1381
+ ```
1382
+
1383
+ Effect's path service with relative paths resolved against `Workspace.root`.
1384
+ Path manipulation is pure and lexical, so it requires no capability check; the
1385
+ explicit layer proves that every member of the closed list has a kernel
1386
+ decision.
1387
+
1388
+ ## Rooted
1389
+
1390
+ ```ts
1391
+ const path: (base: Path.Path, root: string) => Path.Path
1392
+ const fileSystem: (base: FileSystem.FileSystem, rooted: Path.Path) => FileSystem.FileSystem
1393
+ const command: (value: ChildProcess.Command, rooted: Path.Path) => ChildProcess.Command
1394
+ const spawner: (
1395
+ base: ChildProcessSpawner["Service"],
1396
+ rooted: Path.Path
1397
+ ) => ChildProcessSpawner["Service"]
1398
+ const layer: (
1399
+ root: string
1400
+ ) => Layer.Layer<
1401
+ FileSystem.FileSystem | Path.Path | ChildProcessSpawner,
1402
+ never,
1403
+ FileSystem.FileSystem | Path.Path | ChildProcessSpawner
1404
+ >
1405
+ ```
1406
+
1407
+ Unguarded services bound to one directory. Every relative path, and every
1408
+ command without a `cwd`, resolves against `root`; absolute paths and symlink
1409
+ targets pass through. `spawner` keeps a contained base contained. `layer`
1410
+ decorates the services it is provided in place. `Path.layer` and
1411
+ `ChildProcessSpawner.layer` root the guarded surface with the same functions.
1412
+
1413
+ ## Workspace
1414
+
1415
+ ```ts
1416
+ interface Service {
1417
+ readonly root: string
1418
+ }
1419
+
1420
+ class Workspace extends Context.Service<Workspace, Service>()("@smthrs/kernel/Workspace") {}
1421
+
1422
+ const make: (root: string) => Service
1423
+ const layer: (root: string) => Layer.Layer<Workspace>
1424
+ const makeNoop: Service // root "."
1425
+ const layerNoop: Layer.Layer<Workspace>
1426
+ ```
1427
+
1428
+ The workspace root that makes filesystem capability resources stable. It lives
1429
+ beside the closed host list rather than in a platform package: a workspace root
1430
+ is a policy decision the kernel needs before it can name a filesystem
1431
+ capability, not something a platform can answer.
1432
+
1433
+ ## HostError
1434
+
1435
+ ```ts
1436
+ type HostError = JjError
1437
+ ```
1438
+
1439
+ A type union, not a re-export: import the error itself from
1440
+ [`@smthrs/jj`](/api/jj). Process execution is absent on purpose, because it is
1441
+ Effect's `ChildProcessSpawner` and fails with `PlatformError` like the rest of
1442
+ Effect's platform surface.
1443
+
1444
+ ## Test subpaths
1445
+
1446
+ For a deterministic host, use `@smthrs/testing/TestHost`. Add
1447
+ `"@smthrs/testing": "workspace:*"` to the consuming package's devDependencies;
1448
+ see [Testing](./testing.md).
1449
+
1450
+ ### @smthrs/kernel/test/TestGrantStore
1451
+
1452
+ ```ts
1453
+ const layerAllow: Layer.Layer<GrantStore>
1454
+ const layerDeny: (reason?: string) => Layer.Layer<GrantStore>
1455
+ const layerScripted: (replies: ReadonlyArray<GrantStore.Resolution>) => Layer.Layer<GrantStore>
1456
+ ```
1457
+
1458
+ `layerDeny` defaults to the reason `"denied by test"`. `layerScripted` consumes
1459
+ one reply per check: `once`, `run`, and `remembered` allow it, `deny` rejects
1460
+ it, and exhausting the script rejects with
1461
+ `"permission reply script exhausted"`. None of the three requires a
1462
+ `Workspace`.
1463
+
1464
+ ### @smthrs/kernel/test/contract
1465
+
1466
+ ```ts
1467
+ const runHostContract: (
1468
+ name: string,
1469
+ layer: HostContractLayer,
1470
+ caps: HostContractCapabilities
1471
+ ) => void
1472
+ ```
1473
+
1474
+ Registers the shared behavioral contract for a complete host bundle:
1475
+ `FileSystemSuccess` or `FailureCapability` per slot, plus `PathSuccess`,
1476
+ `ChildProcessSuccess`, `JjSuccess`, and `HttpClientSuccess` with its three
1477
+ probes. `FileSystemOperations` and `JjOperations` are exported so an adapter can
1478
+ enumerate what it must declare. Node-only, and requires the
1479
+ `@effect/vitest` and `vitest` peers. See
1480
+ [Adapt a new host platform](./guides/adapt-a-new-host-platform.md).
1481
+
1482
+ ## Identity, failures, and bounds
1483
+
1484
+ Capability actions and resources, patterns, run ids, plan digests, request ids,
1485
+ and grant metadata are identity-bearing values. Smithers does not apply Unicode
1486
+ normalization: matching, signatures, and journal replay use the exact
1487
+ JavaScript string and UTF-16 code-unit sequence supplied. Identity fields that
1488
+ require well-formed text reject lone surrogates and NUL where their contract
1489
+ forbids it.
1490
+
1491
+ Permission failures retain stable codes: `permission_required`,
1492
+ `permission_denied`, and the `GrantStoreErrorCode` values `duplicate_request`,
1493
+ `request_not_found`, `journal_failed`, `store_closed`, and
1494
+ `invalid_resolution`. The kernel's in-memory store raises the last four;
1495
+ `duplicate_request` is part of the vocabulary for an attended surface of your
1496
+ own. Platform projections preserve the structured value as their cause.
1497
+ Validation errors identify the rejected field but never retain or print
1498
+ unbounded hostile input.
1499
+
1500
+ ## What the kernel does not do
1501
+
1502
+ :::warning
1503
+ The kernel checks capabilities at adapter call sites. Child processes require a host `ProcessConfinement` to enforce filesystem and network grants in the operating system. An explicit unconfined layer leaves child access outside the kernel boundary. Hermetic build execution additionally requires a `StepBoundary`.
1504
+ :::
1505
+
1506
+ See [Capabilities and the host kernel](/docs/concepts/kernel/) and the platform
1507
+ bundles that satisfy these ports: [`@smthrs/platform-node`](/api/platform-node),
1508
+ [`@smthrs/platform-browser`](/api/platform-browser), and
1509
+ [`@smthrs/platform-bun`](/api/platform-bun).
1510
+
1511
+ ## Web destination pinning
1512
+
1513
+ `HttpClient.Destination` carries an immutable `{ origin, addresses }` snapshot
1514
+ for one authorized web request. `HttpClient.withDestinationPinning(client)` marks only the exact trusted
1515
+ client instance that
1516
+ rejects origin mismatches, pins every socket and proxy tunnel to the approved
1517
+ addresses, preserves HTTP Host and TLS verification, and prevents reuse of
1518
+ connections from another admission. Custom clients must implement this
1519
+ contract before marking the client. Replacing the client does not inherit the assertion;
1520
+ the kernel guard preserves it when wrapping a supported client. The standard Node and Bun host
1521
+ bundles implement it. `HttpClient.supportsDestinationPinning(client)` reports
1522
+ whether that exact client is marked. Model requests leave `Destination` unset.
1523
+
1524
+ ## Unreachable
1525
+
1526
+ `Unreachable.Unreachable({ message, cause? })` is a schema-backed infrastructure
1527
+ failure. `Unreachable.classifyExit(stderr)` returns it for known DNS and transport
1528
+ signatures, including `EAI_AGAIN`, `ENOTFOUND`, HTTP 429 and HTTP 5xx; it returns
1529
+ `undefined` for other command failures. Pass stderr from a failed command only.
1530
+ The message retains the diagnostic and the optional cause retains the source.
1531
+ Import from `@smthrs/kernel` or `@smthrs/kernel/Unreachable`. The fault registry
1532
+ classifies this tag as `infra`.