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

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,182 @@
1
+ ---
2
+ title: "Quickstart"
3
+ description: "Guard a host bundle end to end: refuse an ungranted read with an unattended store, then park the same read on an attended store and answer it."
4
+ sidebar:
5
+ order: 2
6
+ ---
7
+
8
+ This quickstart guards a host with the capability kernel and runs the same
9
+ filesystem read twice: once against an unattended store, which refuses it, and
10
+ once against an attended store, where you answer the request the read parks
11
+ on. Nothing touches a real machine, so the run is deterministic.
12
+
13
+ By the end you will have seen the two shapes every kernel refusal takes: the
14
+ typed `PermissionRequired` that an unattended host reports, and the parked
15
+ request an operator resolves.
16
+
17
+ ## Prerequisites
18
+
19
+ - Node.js 26.4.0 or later.
20
+ - A package that depends on `@smthrs/kernel` and has
21
+ `"@smthrs/testing": "workspace:*"` in devDependencies. See
22
+ [Installation](./installation.md) for how to get them.
23
+ - A test runner, for the second half:
24
+
25
+ ```bash
26
+ pnpm add -D @effect/vitest@4.0.0-rc.115 vitest@5.0.0
27
+ ```
28
+
29
+ ## Build the raw host
30
+
31
+ The kernel decorates ports; something else has to provide them.
32
+ `@smthrs/testing/TestHost` is the deterministic bundle: an in-memory
33
+ filesystem, a scripted interpreter, `TestClock`, and a seeded PRNG. `Workspace`
34
+ names the root that filesystem capability resources are resolved against.
35
+
36
+ Create `quickstart.test.ts`. Vitest only discovers files matching
37
+ `**/*.{test,spec}.?(c|m)[jt]s?(x)`, so the name is what makes the example
38
+ runnable:
39
+
40
+ ```ts
41
+ import { GrantStore, HostServices, Permission, Workspace } from "@smthrs/kernel"
42
+ import * as TestHost from "@smthrs/testing/TestHost"
43
+ import { Effect, Fiber, FileSystem, Layer, Option, type PlatformError } from "effect"
44
+
45
+ /** The unguarded platform bundle plus the workspace root. */
46
+ const raw = Layer.mergeAll(
47
+ TestHost.layer({ files: { "/workspace/README.md": "# hello" } }),
48
+ Workspace.layer("/workspace")
49
+ )
50
+ ```
51
+
52
+ ## Refuse a read with no policy
53
+
54
+ `GrantStore.layer({ attended: false })` builds a store with no rules and
55
+ nobody to ask. `HostServices.layer` composed over the raw bundle replaces all
56
+ five tags with their guarded implementations:
57
+
58
+ ```ts
59
+ const unattended = Effect.gen(function*() {
60
+ const fs = yield* FileSystem.FileSystem
61
+ const failure = yield* Effect.flip(fs.readFileString("/workspace/README.md"))
62
+ return Option.getOrThrow(
63
+ Permission.fromPlatformError(failure as PlatformError.PlatformError)
64
+ )
65
+ }).pipe(
66
+ Effect.provide(HostServices.layer),
67
+ Effect.provide(Layer.orDie(GrantStore.layer({ attended: false }))),
68
+ Effect.provide(raw),
69
+ Effect.scoped
70
+ )
71
+ ```
72
+
73
+ The program calls Effect's own `readFileString`. It never mentions permission,
74
+ and it is refused anyway, which is the point of decorating in place. Effect
75
+ fixes that method's error channel to `PlatformError`, so the kernel projects
76
+ its failure into one and keeps the structured original on the cause;
77
+ `Permission.fromPlatformError` reads it back:
78
+
79
+ ```text
80
+ {
81
+ _tag: '@smthrs/capability/PermissionRequired',
82
+ code: 'permission_required',
83
+ requestId: 'permission-1',
84
+ capability: { action: 'fs:read', resource: '/workspace/README.md' },
85
+ tier: 'sealed',
86
+ meta: {}
87
+ }
88
+ ```
89
+
90
+ `permission_required`, not `permission_denied`. No rule matched, so the
91
+ decision was `ask`, and an unattended store turns `ask` into a refusal that
92
+ names what it would have asked for. A `deny` would have produced
93
+ `permission_denied` instead. The `tier` is `sealed` because re-running a read
94
+ costs nothing; see [effect tiers](/pkg/capability/concepts/effect-tiers).
95
+
96
+ ## Answer the request instead
97
+
98
+ An attended store parks the asking fiber on a request and waits. Build the
99
+ store with `GrantStore.make` so you hold the service directly, fork the read,
100
+ read the pending request, and reply:
101
+
102
+ ```ts
103
+ const attended = Effect.gen(function*() {
104
+ const store = yield* GrantStore.make({ runId: "quickstart" })
105
+
106
+ /** Polls until the forked read has parked. A real host waits on its own UI. */
107
+ const pending = (): Effect.Effect<GrantStore.PendingRequest> =>
108
+ Effect.flatMap(store.list, (requests) =>
109
+ requests[0] === undefined
110
+ ? Effect.andThen(Effect.yieldNow, pending())
111
+ : Effect.succeed(requests[0]))
112
+
113
+ return yield* Effect.gen(function*() {
114
+ const fs = yield* FileSystem.FileSystem
115
+ const reading = yield* Effect.forkChild(fs.readFileString("/workspace/README.md"), {
116
+ startImmediately: true
117
+ })
118
+
119
+ const request = yield* pending()
120
+ // request.capability is { action: "fs:read", resource: "/workspace/README.md" }
121
+ yield* store.reply(request.requestId, "once")
122
+
123
+ return yield* Fiber.join(reading)
124
+ }).pipe(
125
+ Effect.provide(HostServices.layer),
126
+ Effect.provideService(GrantStore.GrantStore, store)
127
+ )
128
+ }).pipe(Effect.scoped, Effect.provide(raw))
129
+ ```
130
+
131
+ `Fiber.join(reading)` answers `"# hello"`. The read did not fail and retry: it
132
+ suspended inside the decorator, and the reply woke it up to finish the
133
+ operation it was authorized for. `"once"` authorizes this request alone. The
134
+ other resolutions, `"run"`, `"remembered"`, and `"deny"`, are covered in
135
+ [Answer permission requests](./guides/answer-permission-requests.md).
136
+
137
+ ## Run it
138
+
139
+ Append the assertions to `quickstart.test.ts`, below both programs:
140
+
141
+ ```ts
142
+ import { describe, expect, it } from "@effect/vitest"
143
+
144
+ describe("quickstart", () => {
145
+ it.effect("refuses an ungranted read", () =>
146
+ Effect.gen(function*() {
147
+ expect((yield* unattended).code).toBe("permission_required")
148
+ }))
149
+
150
+ it.effect("resumes a read the operator allows", () =>
151
+ Effect.gen(function*() {
152
+ expect(yield* attended).toBe("# hello")
153
+ }))
154
+ })
155
+ ```
156
+
157
+ ```bash
158
+ pnpm vitest run quickstart.test.ts
159
+ ```
160
+
161
+ ## What just happened
162
+
163
+ `HostServices.layer` is five middleware layers merged. Each one requires the
164
+ tag it also provides, so composing it over a platform bundle shadows the raw
165
+ implementations for everything downstream. Before delegating, the filesystem
166
+ decorator resolved `/workspace/README.md` to a canonical resource, built the
167
+ capability `fs:read` on it, and handed it to the store. The store found no
168
+ matching rule, defaulted to `ask`, and either refused or parked depending on
169
+ whether anybody was there to answer.
170
+
171
+ Nothing in either program held an unguarded `FileSystem`. That is the whole
172
+ design: there is no second, protected tag to reach for, so there is no
173
+ unguarded one to hold.
174
+
175
+ ## Next steps
176
+
177
+ - [Write a capability policy](./guides/write-a-capability-policy.md): rules
178
+ that answer `allow` before anyone is asked.
179
+ - [Guard a host bundle](./guides/guard-a-host-bundle.md): the same composition
180
+ over a real platform.
181
+ - [How a grant decision is made](./concepts/grant-decisions.md): the ceiling,
182
+ the four rulesets, and the order they are consulted in.
@@ -0,0 +1,108 @@
1
+ ---
2
+ title: "Testing"
3
+ description: "Test guarded hosts with the kernel grant-store doubles, the shared host contract suite, and the deterministic host from @smthrs/testing."
4
+ ---
5
+
6
+ The kernel ships two public test helpers: `test/TestGrantStore` and
7
+ `test/contract` (also exported as `test/HostContract`). They are published code,
8
+ so consumers can use the same doubles and host contract as the package tests.
9
+
10
+ ## Run the kernel suite
11
+
12
+ From the repository root, run `pnpm --dir packages/smithers/flows/kernel exec vitest run`.
13
+ The same command runs in the independent Windows kernel job, with the package's
14
+ coverage thresholds enforced.
15
+
16
+ ## Pick a grant-store double
17
+
18
+ `@smthrs/kernel/test/TestGrantStore` gives you three layers, and none of them
19
+ needs a `Workspace`:
20
+
21
+ ```ts
22
+ import * as TestGrantStore from "@smthrs/kernel/test/TestGrantStore"
23
+
24
+ TestGrantStore.layerAllow // every check passes
25
+ TestGrantStore.layerDeny("denied by test") // every check fails permission_denied
26
+ TestGrantStore.layerScripted(["once", "deny"]) // a deterministic sequence
27
+ ```
28
+
29
+ `layerScripted` consumes one reply per check, in order. `once`, `run`, and
30
+ `remembered` allow the check; `deny` rejects it; exhausting the script rejects
31
+ every further check with the reason
32
+ `"permission reply script exhausted"`. Use it to assert **which** operations a
33
+ subject attempts and in what order, which a blanket allow or deny cannot show.
34
+
35
+ For a test about policy rather than about the subject, build a real store with
36
+ `GrantStore.layer({ attended: false, rules })` instead. It exercises the actual
37
+ evaluation path, and it needs a `Workspace`.
38
+
39
+ ## Run against the deterministic host
40
+
41
+ Add `"@smthrs/testing": "workspace:*"` to the consuming package's
42
+ devDependencies in the checkout. See [Installation](./installation.md).
43
+
44
+ `@smthrs/testing/TestHost` is the whole host surface with every source of
45
+ nondeterminism pinned: a `Map`-backed filesystem, a scripted interpreter,
46
+ `TestClock` so time moves only when a test moves it, and a seeded PRNG.
47
+
48
+ ```ts
49
+ import * as TestHost from "@smthrs/testing/TestHost"
50
+
51
+ const host = TestHost.layer({
52
+ files: { "/workspace/README.md": "# hello" },
53
+ commands: { "npm test": { stdout: "1 passing\n", exitCode: 0 } },
54
+ seed: 42
55
+ })
56
+ ```
57
+
58
+ `TestHost.TestHost` is the zero-config bundle: empty filesystem, no scripted
59
+ commands, seed 42.
60
+
61
+ Three details are worth knowing. The filesystem is the very same
62
+ `BrowserFileSystem` adapter the browser runs, over a `Map`-backed volume, so a
63
+ bug in the adapter shows up in tests instead of hiding behind a second
64
+ implementation. The spawner is provided **over** the filesystem and path
65
+ layers, exactly as `NodeChildProcessSpawner` is, so the interpreter and the
66
+ `FileSystem` service agree about what exists. And a command that is not in
67
+ `commands` fails the way a real shell reports a missing binary, so a test
68
+ cannot accidentally depend on a host tool being installed.
69
+
70
+ `Jj` is [`@smthrs/jj`](/api/jj)'s `layerUnsupported`, whose every operation
71
+ fails with `not_installed`, so a test that reaches for the repository fails
72
+ loudly rather than touching the real machine. `HttpClient` is
73
+ `HttpClient.layerNoop`, which reports the absent host as a `TransportError`.
74
+
75
+ The module is Node-only: `effect/testing`'s `TestClock` reaches for
76
+ `node:assert`.
77
+
78
+ ## Run the shared host contract
79
+
80
+ `@smthrs/kernel/test/contract` exports `runHostContract`, the behavioral
81
+ contract every host bundle must satisfy: the complete filesystem, process, Jj,
82
+ and HTTP capability matrices, including observable process liveness and
83
+ multi-leg pipelines. The Node, Bun, browser, test, and deliberately
84
+ unsupported bundles all run it.
85
+
86
+ It registers Vitest cases, so it requires the declared peers
87
+ (`@effect/vitest@4.0.0-rc.115` and `vitest@5.0.0`) and Node process and
88
+ temporary-directory fixtures. See
89
+ [Adapt a new host platform](./guides/adapt-a-new-host-platform.md) for how to
90
+ declare your bundle's capabilities to it.
91
+
92
+ ## What the package already proves
93
+
94
+ The kernel's own suite covers capability-set intersection and equality;
95
+ bounded, immutable grant state; journal replay and its envelope limits;
96
+ canonical filesystem resources and descriptor identity; command snapshots;
97
+ process containment and ledger recovery; HTTP method, origin, and redirect
98
+ authorization; Jujutsu resources; browser-safe imports; and every supported and
99
+ refused host operation.
100
+
101
+ If you are testing a subject that reaches the host, you rarely need to re-prove
102
+ any of that. Test what your subject does with the refusal.
103
+
104
+ ## Related
105
+
106
+ - [Quickstart](./quickstart.md): the doubles in a working composition.
107
+ - [Handle a permission failure](/pkg/capability/guides/handle-a-permission-failure):
108
+ reading the typed error a refusal carries.
@@ -0,0 +1,281 @@
1
+ ---
2
+ title: "Troubleshooting"
3
+ description: "The refusals @smthrs/kernel produces, what each one means, and what to change: policy misses, ceiling denials, filesystem confinement, grant replies, journal replay, and composition errors."
4
+ ---
5
+
6
+ Every refusal this package produces is typed and carries a stable code. Find
7
+ the message or the code and read the matching section.
8
+
9
+ Where Effect owns the tag, the failure arrives projected. Read it back first:
10
+
11
+ ```ts
12
+ import { HttpClient, Permission } from "@smthrs/kernel"
13
+ import { Option } from "effect"
14
+
15
+ // FileSystem and ChildProcessSpawner failures.
16
+ Option.getOrThrow(Permission.fromPlatformError(failure))
17
+ // HttpClient failures.
18
+ Option.getOrThrow(HttpClient.fromHttpClientError(failure))
19
+ ```
20
+
21
+ `Jj` names `Permission.PermissionError` in its own channel, so there is
22
+ nothing to unwrap.
23
+
24
+ ## permission_required
25
+
26
+ **What happened.** No rule matched the capability, so the decision defaulted to
27
+ `ask`, and the store is unattended. The failure names the request id, the
28
+ capability, its effect tier, and the display metadata.
29
+
30
+ **What to change.** Add an allow rule for the capability, or run an attended
31
+ store so an operator can answer. This is the expected refusal for a headless
32
+ host meeting an operation nobody authorized; it is not a bug in the caller.
33
+
34
+ ## permission_denied: denied by permission policy
35
+
36
+ **What happened.** A rule matched and denied it. Rules are last-match-wins
37
+ across the four rulesets, and an effective denial in the **configured** ruleset
38
+ is a hard veto no envelope, run grant, or remembered grant can lift.
39
+
40
+ **What to change.** If the denial is in the configured policy and intended,
41
+ nothing: that is the veto working. If it is not intended, add a later allow in
42
+ the same configured ruleset, since a configured deny superseded within its own
43
+ ruleset is not a veto.
44
+
45
+ ## permission_denied: outside capability ceiling
46
+
47
+ **What happened.** The fiber passed through `CapabilitySet.attenuate` and the
48
+ capability is outside the resulting ceiling. This is checked before any rule,
49
+ so no policy and no operator reply can override it.
50
+
51
+ **What to change.** Widen the group you passed to `attenuate`, or do the work
52
+ outside the attenuated scope. There is no public widening operation by design:
53
+ authority only narrows.
54
+
55
+ ## permission_denied: grant store closed, and store_closed
56
+
57
+ **What happened.** The store's scope closed. A request that was parked at that
58
+ moment fails with `permission_denied` and `"grant store closed"`; a call made
59
+ afterwards fails with the `GrantStoreError` code `store_closed`.
60
+
61
+ **What to change.** Keep the store's scope open for as long as the work that
62
+ uses it. A store built inside `Effect.scoped` around a short program, while the
63
+ guarded host outlives it, produces exactly this.
64
+
65
+ ## permission_denied: host does not provide descriptor-relative, no-follow filesystem isolation
66
+
67
+ **What happened.** The filesystem in the composition carries neither
68
+ confinement extension, so every path, directory, stream, glob, and handle
69
+ operation fails closed.
70
+
71
+ **What to change.** A native adapter attaches
72
+ `FileSystem.withAtomicFileSystem` with operations rooted at a pinned
73
+ descriptor. A volume that genuinely cannot address the host filesystem attests
74
+ with `FileSystem.withIsolatedFileSystem`. Attaching neither is not a
75
+ configuration to relax; the check exists because a path-delegating filesystem
76
+ cannot be confined. See
77
+ [Filesystem confinement](./concepts/filesystem-confinement.md).
78
+
79
+ If the failure names `../<system-temp>`, the operation was
80
+ `makeTempDirectory`, `makeTempFile`, or one of their scoped forms: they name no
81
+ directory, so the kernel authorizes the system temporary directory sentinel.
82
+
83
+ ## permission_denied: hard-linked files cannot be confined to the workspace
84
+
85
+ **What happened.** The target is a regular file with more than one link. A hard
86
+ link is a second name for the same inode with no ancestry to check, so there is
87
+ no canonical resource to authorize.
88
+
89
+ **What to change.** Copy the file rather than hard-linking it, or operate on it
90
+ outside the guarded surface. This refusal is always on and there is no option
91
+ to disable it.
92
+
93
+ ## permission_denied: path no longer names the resource that was authorized
94
+
95
+ **What happened.** The grant decision suspended (an attended request, or a
96
+ journal write), and when it came back the path resolved to something else. A
97
+ symlink or a rename swapped in during the wait.
98
+
99
+ **What to change.** Usually nothing in the caller: this is the protection
100
+ working. If it fires without an adversary, something else is rewriting the tree
101
+ concurrently, most often a build or a watcher touching the same paths.
102
+
103
+ ## permission_denied: descriptor no longer names the resource at its authorized path
104
+
105
+ **What happened.** An open handle's `device:inode` identity, captured at open
106
+ time, no longer matches what the authorized path names. After a rename the
107
+ descriptor can address an inode outside the workspace even when the replacement
108
+ path is still allowed.
109
+
110
+ **What to change.** Reopen the file. Holding a handle across a rename is what
111
+ produces this.
112
+
113
+ ## Throw: filesystem already carries a descriptor-relative executor
114
+
115
+ **What happened.** `withIsolatedFileSystem` was called on a filesystem that
116
+ already carries a descriptor-relative executor. The refusal is a throw at
117
+ composition time, so a host cannot be assembled that way.
118
+
119
+ **What to change.** Do not attest whole-volume isolation over a native host.
120
+ The executor is the stronger guarantee, and replacing it with a path-delegating
121
+ attestation reopens the symlink-swap window. If you meant to layer over the
122
+ existing executor, read it first and delegate to it; that replacement is your
123
+ own decision and is allowed.
124
+
125
+ ## invalid_resolution: run grants require a plan digest
126
+
127
+ **What happened.** A `"run"` reply arrived on a store built without
128
+ `planDigest`. A run grant is bound to the exact active plan, and there is
129
+ nothing to bind it to.
130
+
131
+ **What to change.** Pass `planDigest` when building the store, or reply `once`
132
+ or `remembered` instead.
133
+
134
+ ## invalid_resolution: grant pattern exceeds the requested authority
135
+
136
+ **What happened.** The pattern supplied to `reply` names a different action,
137
+ reaches a more dangerous effect tier than the request displayed, or is a
138
+ wildcard-bearing pattern identical to the resource. The last case is refused
139
+ because the grammar has no escape, so the pattern's wildcard reading would
140
+ silently widen access.
141
+
142
+ **What to change.** Supply a pattern within the request's own action and tier,
143
+ or omit it and let the exact capability derive one.
144
+
145
+ ## invalid_resolution: the requested resource contains glob metacharacters
146
+
147
+ **What happened.** A `"run"` or `"remembered"` reply with no explicit pattern,
148
+ for a resource containing `*` or `?`. There is no unambiguous pattern to
149
+ derive.
150
+
151
+ **What to change.** Supply an explicit pattern, as the message says, or resolve
152
+ `once`.
153
+
154
+ ## request_not_found
155
+
156
+ **What happened.** No parked request has that id. It was already answered, the
157
+ asking fiber was interrupted, or the store was rebuilt.
158
+
159
+ **What to change.** Re-read `store.list` before replying. Reply ids are only
160
+ valid for the store instance that issued them.
161
+
162
+ ## invalid_resolution: rules exceed 1024 entries
163
+
164
+ **What happened.** A bound was reached. One store retains at most 1,024 rules
165
+ across all four rulesets, 1,024 envelope signatures, 256 patterns per
166
+ envelope, and 1,024 parked requests. Every bound failure happens **before**
167
+ any state or journal authority changes.
168
+
169
+ **What to change.** Narrow the policy: one broad pattern replaces many exact
170
+ ones. If the count is coming from replay, the message from
171
+ `JournalGrantStore` names the policy run and the counts, and the fix is to
172
+ start a new `policyRunId`. Do not compact the policy run: replay never reads a
173
+ checkpoint, so compaction makes every later construction fail. Check against
174
+ the exported constants (`GrantStore.maximumRules` and its siblings) rather
175
+ than hardcoding numbers.
176
+
177
+ ## journal_failed
178
+
179
+ **What happened.** `JournalGrantStore` could not persist a decision, or could
180
+ not replay history. The decision is **not** activated: a permission that could
181
+ not be written down was not granted.
182
+
183
+ **What to change.** Fix the journal (space, permissions, connectivity). Check
184
+ that a `SqlJournal` uses the `reject` overflow policy: a dropped grant decision
185
+ cannot safely be treated as persisted, so drop-capable policies are
186
+ unsupported.
187
+
188
+ ## invalid_resolution: non-advancing journal page
189
+
190
+ **What happened.** A journal page did not advance past the cursor it was asked
191
+ for. Following it would replay the same events forever; accepting it would
192
+ double-apply them, so construction is refused. The message names the run
193
+ (`policyRunId` or `runId`) whose page failed to advance, and the sequence.
194
+
195
+ **What to change.** This is corrupt journal output, not a policy problem.
196
+ Investigate the journal implementation or the store beneath it.
197
+
198
+ ## invalid_resolution: run-scoped event found in policy journal
199
+
200
+ **What happened.** Replay found an event in the wrong run: a run-scoped event
201
+ in the policy run, or a remembered event in the operational run. The sibling
202
+ failures are `"grant payload run mismatch"`, `"invalid grant payload"`,
203
+ `"grant envelope/payload type mismatch"`, and the `"unsafe ... event"`
204
+ refusals. Sequences are per run and the store replays two runs, so every one
205
+ of these names the run it was replaying and the sequence within it, for
206
+ example `invalid grant payload in run kernel-policy at journal sequence 7`.
207
+ That row is the one to inspect.
208
+
209
+ **What to change.** Check that `runId`, `policyRunId`, and `sourceId` are the
210
+ ones this store has always used. Reusing an operational run id as a policy run
211
+ id, or writing kernel event types from another producer under the same source
212
+ id, produces exactly this.
213
+
214
+ ## invalid_resolution: runId and policyRunId must be distinct
215
+
216
+ **What happened.** `JournalGrantStore` was given the same id for both. Run
217
+ grants and remembered grants must live in separate runs, because remembered
218
+ policy has to outlive any single run.
219
+
220
+ **What to change.** Keep `policyRunId` stable across every run and give each
221
+ operational run its own `runId`.
222
+
223
+ ## permission_denied: HTTP capability checks require an absolute, parseable URL
224
+
225
+ **What happened.** The request URL did not parse as an absolute URL, so there
226
+ is no host to name as a resource.
227
+
228
+ **What to change.** Build requests with an absolute URL. A relative path
229
+ resolved by a base elsewhere never reaches the decorator as one.
230
+
231
+ ## A grant for a host does not cover http:// to that host
232
+
233
+ **What happened.** This is the design, not a bug. `https` is the implicit
234
+ scheme: an `https:` URL names the bare lowercased host, and every other scheme
235
+ names `<scheme>//<host>`. A grant for `api.example.test` therefore never
236
+ authorizes `http://api.example.test`.
237
+
238
+ **What to change.** Use `https`, or write the cleartext resource explicitly in
239
+ the rule if a downgrade is genuinely intended.
240
+
241
+ ## Snapshot refusals
242
+
243
+ Three failures mean a caller handed in something that could still change after
244
+ the capability was checked:
245
+
246
+ - `"filesystem options must contain only data properties"`: an options object
247
+ carrying a getter rather than plain data.
248
+ - `"HTTP request must be an immutable supported request description"`: a
249
+ request body that is neither immutable nor copyable.
250
+ - `"command must be an immutable supported process description"`, a
251
+ `PlatformError` with reason `InvalidData`: a command that could not be
252
+ snapshotted.
253
+
254
+ **What to change.** Pass plain data. The snapshot exists so the operation
255
+ executed after a grant arrives is the operation that was authorized.
256
+
257
+ ## NotFound: no process host for `<command>`
258
+
259
+ **What happened.** The composition has `ChildProcessSpawner.layerNoop()`, the
260
+ stub for a host that cannot start processes. The matching HTTP stub reports
261
+ `"HTTP is unavailable on this host"` as a `TransportError`.
262
+
263
+ **What to change.** Provide a real platform bundle. `TestHost` composes the
264
+ HTTP stub deliberately, so a test that expected a network call will see this.
265
+
266
+ ## A spawn fails and no process is running
267
+
268
+ **What happened.** The spawn's durable `ProcessLedger` record failed to commit.
269
+ The child was signalled, the call failed, and no pid was left in
270
+ `ProcessLedger.live`.
271
+
272
+ **What to change.** Fix the journal. Do not catch this failure and proceed: a
273
+ child no incarnation of the host can discover is the one outcome containment
274
+ exists to prevent.
275
+
276
+ ## Related
277
+
278
+ - [Handle a permission failure](/pkg/capability/guides/handle-a-permission-failure):
279
+ the error shapes themselves.
280
+ - [How a grant decision is made](./concepts/grant-decisions.md): the order that
281
+ produces each of these.