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