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