@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.
- 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,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.
|
package/docs/testing.md
ADDED
|
@@ -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.
|