@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,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Decoration in place"
|
|
3
|
+
description: "Why the kernel guards the platform's own service tags instead of publishing protected copies: the closed port list, what each slot's error channel becomes, and what is deliberately not on the list."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A capability kernel has to answer one question before anything else: how does
|
|
9
|
+
a caller get the guarded service rather than the raw one? The usual answer is
|
|
10
|
+
a second tag. The platform provides `FileSystem`, the kernel provides
|
|
11
|
+
`ProtectedFileSystem`, and every consumer is told to ask for the second. That
|
|
12
|
+
answer fails the moment a dependency does not know about the kernel, and it
|
|
13
|
+
leaves two tag lists to keep in sync.
|
|
14
|
+
|
|
15
|
+
The kernel does the opposite. Each guarded implementation is a middleware
|
|
16
|
+
`Layer` over the very tag the platform adapter provides: it requires
|
|
17
|
+
`FileSystem` and it provides `FileSystem`. Composed over a host bundle, the
|
|
18
|
+
guarded implementation shadows the raw one for everything downstream. A
|
|
19
|
+
library that has never heard of Smithers is guarded because there is nothing
|
|
20
|
+
else for it to resolve.
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
raw platform service
|
|
24
|
+
|
|
|
25
|
+
kernel decorator -> GrantStore
|
|
26
|
+
|
|
|
27
|
+
flow-visible service
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`HostServices.layer` is those five middleware layers merged, so one
|
|
31
|
+
composition guards the whole surface at once.
|
|
32
|
+
|
|
33
|
+
## The closed list
|
|
34
|
+
|
|
35
|
+
Five tags, and everything that touches the outside world enters through one of
|
|
36
|
+
them:
|
|
37
|
+
|
|
38
|
+
| Slot | Tag | Owner | Actions checked |
|
|
39
|
+
| ---------- | --------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| Filesystem | `FileSystem` | Effect | `fs:read`, `fs:write` |
|
|
41
|
+
| Paths | `Path` | Effect | none, by decision |
|
|
42
|
+
| Processes | `ChildProcessSpawner` | Effect | `proc:spawn` |
|
|
43
|
+
| Repository | `Jj` | [`@smthrs/jj`](/api/jj) | `jj:status`, `jj:diff`, `jj:snapshot`, `jj:restore`, `jj:workspace-add`, `jj:workspace-forget`, `jj:root`, `jj:revert`, `jj:op-restore` |
|
|
44
|
+
| Network | `HttpClient` | Effect | `net:get`, `net:post`, `model:call` |
|
|
45
|
+
|
|
46
|
+
`HostServices.HostServiceTags` is that list as values and
|
|
47
|
+
`HostServices.HostServiceIds` is the matching stable id per slot
|
|
48
|
+
(`"effect/FileSystem"`, `"effect/Path"`,
|
|
49
|
+
`"effect/process/ChildProcessSpawner"`, `"@smthrs/jj/Jj"`,
|
|
50
|
+
`"effect/HttpClient"`).
|
|
51
|
+
|
|
52
|
+
Two things depend on the list being closed. A service that is not on it cannot
|
|
53
|
+
be attenuated, denied, or audited, so it must not exist. And the list is part
|
|
54
|
+
of what a durable run records about itself: [`@smthrs/engine`](/api/engine)
|
|
55
|
+
folds the implementations that were in scope into the identity of every step it
|
|
56
|
+
caches, so a replay under a different set of them is a different step rather
|
|
57
|
+
than a silent substitution. The slot ids are durable for that reason too, and
|
|
58
|
+
change only when the service behind a slot changes.
|
|
59
|
+
|
|
60
|
+
## Four of the five tags are Effect's
|
|
61
|
+
|
|
62
|
+
Smithers supplies implementations of Effect's `FileSystem`, `Path`,
|
|
63
|
+
`ChildProcessSpawner`, and `HttpClient` rather than wrappers around them. That
|
|
64
|
+
choice decides where a refusal surfaces.
|
|
65
|
+
|
|
66
|
+
Where Effect owns the tag, the error channel stays Effect's own, and the
|
|
67
|
+
kernel projects its failure into it:
|
|
68
|
+
|
|
69
|
+
- `FileSystem` and `ChildProcessSpawner` refusals become a `PlatformError`
|
|
70
|
+
whose reason is `PermissionDenied`, carrying the structured kernel failure
|
|
71
|
+
on `cause`. `Permission.fromPlatformError` reads it back.
|
|
72
|
+
- `HttpClient` refusals become an `HttpClientError` whose reason is a
|
|
73
|
+
`TransportError`, again carrying the kernel failure.
|
|
74
|
+
`HttpClient.fromHttpClientError` reads it back.
|
|
75
|
+
|
|
76
|
+
Where Smithers owns the service, the interface names the failure directly:
|
|
77
|
+
`Jj` fails with `JjError | PermissionError`, and nothing has to be projected
|
|
78
|
+
or read back.
|
|
79
|
+
|
|
80
|
+
The asymmetry is not an accident. Widening Effect's error channels would mean
|
|
81
|
+
forking Effect's service interfaces; hiding the kernel failure inside an
|
|
82
|
+
opaque platform error would mean losing the capability, the reason, and the
|
|
83
|
+
stable code. Projection with the original on the cause keeps both.
|
|
84
|
+
|
|
85
|
+
## What is not on the list
|
|
86
|
+
|
|
87
|
+
**`Path` is on the list and checks nothing.** It is pure string manipulation:
|
|
88
|
+
`path.resolve` reads no directory and creates no file, so there is no
|
|
89
|
+
authority to guard. The slot exists as an explicit decision rather than an
|
|
90
|
+
omission. `Path.layer` resolves relative paths against `Workspace.root`, the
|
|
91
|
+
directory `ChildProcessSpawner.layer` runs a command in when it names no
|
|
92
|
+
`cwd`, so neither depends on the process's own directory.
|
|
93
|
+
|
|
94
|
+
**`Clock` and `Random` are Effect core built-ins.** They are already
|
|
95
|
+
port-shaped and already swappable with `Effect.provideService`, and neither
|
|
96
|
+
carries host authority, so they are not the kernel's to define or decorate.
|
|
97
|
+
|
|
98
|
+
**There is no Smithers transport port.** Network access is Effect's
|
|
99
|
+
`HttpClient` and the kernel decorates it. A raw Smithers transport beneath it
|
|
100
|
+
would be a second way to reach the network whose contract never mentions
|
|
101
|
+
permission, which is exactly the hole this design closes.
|
|
102
|
+
|
|
103
|
+
## What decoration cannot see
|
|
104
|
+
|
|
105
|
+
The kernel checks capabilities at adapter call sites. It does not sandbox the
|
|
106
|
+
operating system. Code that imports `node:fs` directly, or a native addon that
|
|
107
|
+
opens a socket, never passes through a decorated service and is not observed.
|
|
108
|
+
That is a real boundary, not a caveat to bury: confinement against untrusted
|
|
109
|
+
code requires an operating-system sandbox as well, and hermetic execution
|
|
110
|
+
requires a `StepBoundary` from [`@smthrs/engine`](/api/engine).
|
|
111
|
+
|
|
112
|
+
## Related
|
|
113
|
+
|
|
114
|
+
- [How a grant decision is made](./grant-decisions.md): what happens inside
|
|
115
|
+
the check each decorator performs.
|
|
116
|
+
- [Guard a host bundle](../guides/guard-a-host-bundle.md): the composition,
|
|
117
|
+
in order, with the layers a real host adds.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Bounded filesystem batches"
|
|
3
|
+
description: "Optional read batches on the guarded filesystem, with per-path grants, root identity, explicit failures, and portable fallback."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`FileSystem.batch(fs)` from `@smthrs/kernel/FileSystem` discovers an optional
|
|
7
|
+
capability on the existing Effect filesystem service. It adds no host slot and
|
|
8
|
+
changes no persisted host identity. A native adapter advertises `batchLimits`
|
|
9
|
+
on its descriptor-relative executor. The kernel exposes the guarded capability
|
|
10
|
+
after binding the workspace root; callers never receive the raw executor.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const batch = FileSystem.batch(fs)
|
|
14
|
+
if (batch !== undefined) {
|
|
15
|
+
const response = yield * batch.execute([
|
|
16
|
+
{ operation: "stat", path: "src/a.ts" },
|
|
17
|
+
{ operation: "digest", path: "src/b.ts" },
|
|
18
|
+
{ operation: "readDirectory", path: "src" }
|
|
19
|
+
])
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A batch accepts 1 through `maxSize` requests, with a hard maximum of 128.
|
|
24
|
+
`maxResponseBytes` bounds the aggregate encoded answer. Inputs are copied
|
|
25
|
+
before asynchronous root or permission checks. Every member gets the same
|
|
26
|
+
canonical `fs:read` check and post-grant re-resolution as an individual call.
|
|
27
|
+
Each grant or quota refusal remains an explicit per-path `Result` carrying its
|
|
28
|
+
original `PlatformError`. A batch with no admitted members starts no helper.
|
|
29
|
+
|
|
30
|
+
Each response carries `rootIdentity`, the root device/inode checked by the
|
|
31
|
+
executor. Members retain their original request `index` and normalized `path`,
|
|
32
|
+
and sort by path and then index. Successful values discriminate `stat`,
|
|
33
|
+
`readDirectory`, `glob`, and `digest`. A digest contains 64 lowercase SHA-256
|
|
34
|
+
hex digits, its byte count, and bytes only when requested with `content: true`.
|
|
35
|
+
Missing paths report `NotFound`; they are not silently dropped. The engine
|
|
36
|
+
decides where absence means an empty expansion or an absent digest.
|
|
37
|
+
|
|
38
|
+
Root loss or replacement refuses the entire operation. Helper failure,
|
|
39
|
+
cancellation, deadline, malformed framing, and aggregate quota exhaustion also
|
|
40
|
+
produce no partial batch. Ordinary path failures appear individually so an
|
|
41
|
+
unrelated authorized path can still be measured. A batch is not a transaction
|
|
42
|
+
or an assertion that the workspace was frozen.
|
|
43
|
+
|
|
44
|
+
When the workspace root is a symlink, its logical path must still resolve to
|
|
45
|
+
the captured root inode before and after grants. Retargeting the alias refuses
|
|
46
|
+
the batch even if the original canonical directory still exists. The helper
|
|
47
|
+
independently verifies that canonical directory against the same inode.
|
|
48
|
+
|
|
49
|
+
Hosts may omit the capability. The engine then uses the existing guarded
|
|
50
|
+
operations with at most four operations in flight. Isolated test/browser
|
|
51
|
+
volumes keep their existing attestation; optional batching does not weaken it.
|
|
52
|
+
The shared HostContract exercises each adapter that advertises batching, while
|
|
53
|
+
the Node conformance tests additionally exercise the confined helper's fault
|
|
54
|
+
and resource boundaries.
|
|
55
|
+
|
|
56
|
+
Host glob grammar remains host-specific. Declared engine globs continue to use
|
|
57
|
+
the plan's matcher, including dotfiles and its metadata-directory pruning, over
|
|
58
|
+
batched directory/stat operations.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Filesystem confinement"
|
|
3
|
+
description: "Why authorizing a pathname is not enough: canonical resources, the re-resolution after a suspended grant, descriptor identity, and the two host extensions that make confinement enforceable."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Guarding a filesystem is harder than guarding a process, because a path is not
|
|
9
|
+
a thing. It is a name that something else can rebind between the moment you
|
|
10
|
+
authorize it and the moment the host acts on it. The naive decorator, check
|
|
11
|
+
the pathname and then hand the same pathname to the host, is vulnerable to a
|
|
12
|
+
symlink swap: authorize `/workspace/notes.txt`, and by the time the host
|
|
13
|
+
resolves it the name points at `/etc/shadow`.
|
|
14
|
+
|
|
15
|
+
Everything in this page exists to close that window.
|
|
16
|
+
|
|
17
|
+
## The capability resource is canonical, not literal
|
|
18
|
+
|
|
19
|
+
`FileSystem.canonicalResource(fileSystem, path, workspaceRoot, value)` is what
|
|
20
|
+
turns a caller's string into the resource a capability names. It resolves the
|
|
21
|
+
path through every existing ancestor, so an existing symlink cannot turn an
|
|
22
|
+
inside-workspace grant into outside authority. Then it maps a canonical path
|
|
23
|
+
that lands inside the workspace back to the stable logical workspace root, so
|
|
24
|
+
capability resources stay readable and stable even when the root itself is a
|
|
25
|
+
symlink. A relative path is resolved against the workspace root first.
|
|
26
|
+
|
|
27
|
+
Every host, including a descriptor-relative executor, resolves existing
|
|
28
|
+
components to their on-disk spelling before and after a grant decision.
|
|
29
|
+
On a case-insensitive volume, `.ENV` therefore names `.env` for permission
|
|
30
|
+
checks, and `REPORTDIR/new.txt` names `ReportDir/new.txt` when `ReportDir`
|
|
31
|
+
already exists. Missing components retain their requested spelling. A
|
|
32
|
+
path-based host must implement `realPath` with canonical on-disk spelling. The
|
|
33
|
+
capability matcher remains case-sensitive so distinct files on case-sensitive
|
|
34
|
+
volumes keep distinct permissions.
|
|
35
|
+
|
|
36
|
+
Native guarded operations on outside-pointing symlinks fail with
|
|
37
|
+
`PermissionDenied`, including `readLink`, `stat`, `exists`, and `remove`.
|
|
38
|
+
Authorization for a descriptor-relative executor never resolves a pathname on
|
|
39
|
+
the host. This matters on Windows, where `realpath` opens its argument through
|
|
40
|
+
links: one call on a link to `\\.\pipe\name` or `\\host\share` connects to
|
|
41
|
+
that pipe or share. Instead the executor answers one `resolve` request from the
|
|
42
|
+
pinned root. It walks without following any link, junction, or mount point,
|
|
43
|
+
and answers the on-disk spelling of existing components, or the first link on
|
|
44
|
+
the way with its text. Link text is resolved lexically and asked again, so a
|
|
45
|
+
component swapped for a link between two checks is seen as that link. A
|
|
46
|
+
reparse point the executor cannot translate, such as a volume mount point, is
|
|
47
|
+
refused without being traversed.
|
|
48
|
+
|
|
49
|
+
Each guarded operation asks the executor to resolve its path before and after
|
|
50
|
+
the grant decision, and then runs the operation itself. A guarded batch asks
|
|
51
|
+
once for all of its members before the grants, once after them, and then runs
|
|
52
|
+
the batch.
|
|
53
|
+
|
|
54
|
+
Capability resources are native paths, and patterns match them as text. On
|
|
55
|
+
Windows, write filesystem patterns with the native separator, such as
|
|
56
|
+
`fs:read:C:\work\**`. The pattern `fs:read:C:\work/**` selects nothing.
|
|
57
|
+
|
|
58
|
+
Resolution also applies one always-on refusal: a regular file with more than
|
|
59
|
+
one link fails with `"hard-linked files cannot be confined to the workspace"`.
|
|
60
|
+
A hard link is a second name for the same inode with no ancestry to check, so
|
|
61
|
+
there is no canonical answer to give.
|
|
62
|
+
|
|
63
|
+
## The path is resolved twice
|
|
64
|
+
|
|
65
|
+
A grant decision can suspend. An attended request waits for a person; a
|
|
66
|
+
journal-backed store waits for a write. What was authorized is the resource
|
|
67
|
+
the path named at check time, so the path must still name it when the decision
|
|
68
|
+
comes back.
|
|
69
|
+
|
|
70
|
+
The guard therefore resolves the resource, checks the capability, and resolves
|
|
71
|
+
it **again**. If the second answer differs, the operation fails with
|
|
72
|
+
`"path no longer names the resource that was authorized"`. A symlink or rename
|
|
73
|
+
swapped in during the wait is refused, never followed.
|
|
74
|
+
|
|
75
|
+
## An open handle is bound to an inode
|
|
76
|
+
|
|
77
|
+
Rechecking a pathname is not enough once a file is open. The pathname could be
|
|
78
|
+
re-authorized while the descriptor you hold names something else: after a
|
|
79
|
+
rename that descriptor can address an inode outside the workspace even though
|
|
80
|
+
the replacement path is still allowed.
|
|
81
|
+
|
|
82
|
+
So an open handle binds authorization to the descriptor's `device:inode`
|
|
83
|
+
identity, captured at open time, and every guarded handle operation
|
|
84
|
+
(`read`, `write`, `stat`, `truncate`, `sync`, and the `*All` and `*Alloc`
|
|
85
|
+
forms) rechecks that identity as well as the capability. A mismatch fails with
|
|
86
|
+
`"descriptor no longer names the resource at its authorized path"`.
|
|
87
|
+
|
|
88
|
+
Open flags decide which capabilities the open itself requires. A read-only
|
|
89
|
+
flag needs `fs:read`, a write flag needs `fs:write`, and a read-write flag
|
|
90
|
+
needs both.
|
|
91
|
+
|
|
92
|
+
A host that reports no inode identity is an isolated volume. Its attestation,
|
|
93
|
+
not inode evidence, is the boundary, so there is nothing to verify and the
|
|
94
|
+
recheck passes.
|
|
95
|
+
|
|
96
|
+
## Two ways a host earns the filesystem
|
|
97
|
+
|
|
98
|
+
The kernel refuses to delegate a checked pathname to a path-based host. A
|
|
99
|
+
platform adapter has to provide one of two extensions, and a host that
|
|
100
|
+
provides neither fails **every** relevant path, directory, stream, glob, and
|
|
101
|
+
handle operation closed with
|
|
102
|
+
`"host does not provide descriptor-relative, no-follow filesystem isolation"`.
|
|
103
|
+
|
|
104
|
+
**`withAtomicFileSystem(fileSystem, atomic)`** attaches a descriptor-relative,
|
|
105
|
+
no-follow executor. Operations are expressed as an `AtomicRequest` and run
|
|
106
|
+
relative to a pinned root handle, so there is no pathname for anything to
|
|
107
|
+
rebind between the check and the act. This is what a native host uses;
|
|
108
|
+
[`@smthrs/platform-node`](/api/platform-node) attaches it.
|
|
109
|
+
|
|
110
|
+
**`withIsolatedFileSystem(fileSystem)`** attests that the whole volume is
|
|
111
|
+
already confined by an enforceable boundary, which is true of an in-memory
|
|
112
|
+
browser volume or a test double that cannot address the host filesystem at
|
|
113
|
+
all. Path delegation is safe there because there is no host filesystem to
|
|
114
|
+
reach.
|
|
115
|
+
|
|
116
|
+
Both decorate the supplied service object in place and return that same
|
|
117
|
+
identity, so a host attaches exactly once at its boundary and keeps no
|
|
118
|
+
undecorated alias.
|
|
119
|
+
|
|
120
|
+
`withIsolatedFileSystem` **throws** on a filesystem that already carries a
|
|
121
|
+
descriptor-relative executor. The executor is the stronger guarantee, and
|
|
122
|
+
replacing it with a path-delegating attestation would route `access`, `copy`,
|
|
123
|
+
`chmod`, `link`, `symlink`, `open`, `watch`, `sink`, `stream`, and every
|
|
124
|
+
`makeTemp*` call back through pathnames after the capability check: the exact
|
|
125
|
+
window the extension closes. The refusal is a throw at composition time, so a
|
|
126
|
+
host cannot be assembled that way. Layering over an executor a caller has read
|
|
127
|
+
and delegates to is still allowed, because that replacement is the caller's
|
|
128
|
+
own decision.
|
|
129
|
+
|
|
130
|
+
## Engine machinery uses a confined view
|
|
131
|
+
|
|
132
|
+
The engine's own copy-back writes files a body produced; asking a grant store
|
|
133
|
+
for them would make the engine ask permission for its own bookkeeping. It still
|
|
134
|
+
must not follow a symlink a concurrent process swapped into the workspace.
|
|
135
|
+
`confined(fileSystem, root)` gives it the same descriptor-relative requests the
|
|
136
|
+
guarded layer sends, pinned to `root`, with no grant check. A path-based host
|
|
137
|
+
gets no such view: `confined` refuses it rather than fall back to a
|
|
138
|
+
check-then-write that a swap could beat.
|
|
139
|
+
|
|
140
|
+
## Temporary directories are outside the workspace
|
|
141
|
+
|
|
142
|
+
`makeTempDirectory`, `makeTempDirectoryScoped`, `makeTempFile`, and
|
|
143
|
+
`makeTempFileScoped` name no directory of their own. An implicit one is
|
|
144
|
+
authorized as `fs:write` on
|
|
145
|
+
`path.resolve(workspace.root, "..", FileSystem.systemTemporaryDirectoryName)`,
|
|
146
|
+
where the sentinel is the literal string `"<system-temp>"`.
|
|
147
|
+
|
|
148
|
+
The sentinel is outside the workspace root by construction and is never
|
|
149
|
+
confusable with a real path, so granting an ordinary workspace write does not
|
|
150
|
+
grant system temporary-directory access. A host missing the required extension
|
|
151
|
+
reports the refusal against the logical input `../<system-temp>`.
|
|
152
|
+
|
|
153
|
+
## Mutable inputs are snapshotted
|
|
154
|
+
|
|
155
|
+
An options record a caller still holds is a second way to change an operation
|
|
156
|
+
after it was authorized. The guarded filesystem snapshots option records,
|
|
157
|
+
nested arrays, and maps before any permission suspension, freezing what it
|
|
158
|
+
copies, so the operation executed after a grant arrives is the operation that
|
|
159
|
+
was approved. An options object carrying a getter rather than plain data
|
|
160
|
+
fails with `"filesystem options must contain only data properties"`, because a
|
|
161
|
+
getter is not something a snapshot can capture.
|
|
162
|
+
|
|
163
|
+
## Related
|
|
164
|
+
|
|
165
|
+
- [Adapt a new host platform](../guides/adapt-a-new-host-platform.md):
|
|
166
|
+
attaching an extension and proving it with the contract suite.
|
|
167
|
+
- [How a grant decision is made](./grant-decisions.md): what happens between
|
|
168
|
+
the two resolutions.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "How a grant decision is made"
|
|
3
|
+
description: "The order GrantStore.check consults authority in: the fiber ceiling, four rulesets under last-match-wins, the ask default, and what an attended and an unattended store each do with ask."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Every decorated host operation ends in one call: `grants.check(capability)`.
|
|
9
|
+
That call either returns void, in which case the operation proceeds, or fails
|
|
10
|
+
with a typed permission error. Everything the kernel decides happens inside
|
|
11
|
+
it, in a fixed order.
|
|
12
|
+
|
|
13
|
+
## The order of the check
|
|
14
|
+
|
|
15
|
+
1. **Is the store open?** A closed store fails every check with
|
|
16
|
+
`store_closed`. Closing a store also rejects every request already parked
|
|
17
|
+
on it, so no waiter is stranded.
|
|
18
|
+
2. **Is the capability inside the fiber's ceiling?** `CapabilitySet` is the
|
|
19
|
+
ambient authority of the current fiber. A capability outside it fails
|
|
20
|
+
immediately with `permission_denied` and the reason
|
|
21
|
+
`"outside capability ceiling"`. No rule and no operator can override the
|
|
22
|
+
ceiling.
|
|
23
|
+
3. **What do the rules say?** `Permission.evaluate` reduces four rulesets into
|
|
24
|
+
`allow`, `deny`, or `ask`.
|
|
25
|
+
4. **`allow` proceeds. `deny` fails** with `permission_denied` and the reason
|
|
26
|
+
`"denied by permission policy"`.
|
|
27
|
+
5. **`ask` depends on who is there.** An unattended store fails with
|
|
28
|
+
`permission_required`, carrying the request id, the capability, its effect
|
|
29
|
+
tier, and the display metadata. An attended store parks the fiber on a
|
|
30
|
+
pending request and waits for a reply.
|
|
31
|
+
|
|
32
|
+
The default decision is `ask`, so a capability no rule mentions is never
|
|
33
|
+
allowed. What differs between a headless host and an operator-driven one is
|
|
34
|
+
only what `ask` becomes.
|
|
35
|
+
|
|
36
|
+
## The ceiling is monotone
|
|
37
|
+
|
|
38
|
+
`CapabilitySet` is a normalized conjunction of any-of pattern groups: a
|
|
39
|
+
capability is allowed only when every group contains a pattern matching it.
|
|
40
|
+
`CapabilitySet.attenuate(patterns)` runs an effect with the parent authority
|
|
41
|
+
**intersected** with one more group, which is the only way authority ever
|
|
42
|
+
moves. There is no public widening constructor and no unrestricted value, so a
|
|
43
|
+
scoped fiber can hand a narrower ceiling to work it delegates and can never
|
|
44
|
+
recover what it gave up.
|
|
45
|
+
|
|
46
|
+
A fiber that never passed through `attenuate` allows every capability, because
|
|
47
|
+
unrestricted authority is the identity element of intersection and a
|
|
48
|
+
permanently closed default would leave no operation able to widen a fiber
|
|
49
|
+
again. The ceiling is not what makes the kernel fail closed. The ruleset is:
|
|
50
|
+
its default verdict is `ask`, and an unattended store turns `ask` into a
|
|
51
|
+
refusal. The ceiling's job is to bound what a _scoped_ fiber may ask for.
|
|
52
|
+
|
|
53
|
+
## Four rulesets, last match wins
|
|
54
|
+
|
|
55
|
+
`GrantStore` evaluates these in order:
|
|
56
|
+
|
|
57
|
+
| Ruleset | Where it comes from |
|
|
58
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| Configured | `MakeOptions.rules`, or the first ruleset of a nested `rules` value. The operator's written policy. |
|
|
60
|
+
| Envelope | Patterns activated by `grantEnvelope`, or the construction `envelope`, with run scope. |
|
|
61
|
+
| Run | `MakeOptions.runRules` and every `"run"` reply, each remembered alongside the ceiling of the fiber that asked. |
|
|
62
|
+
| Remembered | Replayed remembered grants, and every `"remembered"` reply. |
|
|
63
|
+
|
|
64
|
+
Matching rules are last-match-wins across all four. The configured ruleset gets
|
|
65
|
+
one extra power: after it is reduced by the same last-match rule, an effective
|
|
66
|
+
denial is a hard veto that no later ruleset can lift. A configured deny that a
|
|
67
|
+
later configured allow or ask supersedes _within that ruleset_ is not a veto,
|
|
68
|
+
so an operator can still write an exception after a broad denial.
|
|
69
|
+
|
|
70
|
+
Run rules carry the ceiling of the fiber that requested them and are filtered
|
|
71
|
+
out for any capability that ceiling would not allow. A grant handed to an
|
|
72
|
+
attenuated fiber therefore cannot leak back up to a broader one, including after
|
|
73
|
+
restart. Each `flows.kernel.grant.run.v2` event persists the normalized captured
|
|
74
|
+
ceiling as a conjunction of any-of pattern groups. Replay intersects it with
|
|
75
|
+
the constructor's ceiling; reopening under a broader parent cannot widen it.
|
|
76
|
+
|
|
77
|
+
Trusted legacy `flows.kernel.grant.run.v1` events have no captured ceiling.
|
|
78
|
+
The journal store refuses construction with `invalid_resolution` when it finds
|
|
79
|
+
one. Their original boundary cannot be recovered, so obtain fresh approval in
|
|
80
|
+
a new run instead of replaying legacy authority.
|
|
81
|
+
|
|
82
|
+
A bare rule in `MakeOptions.runRules` uses the constructor's ceiling. A
|
|
83
|
+
`{ rule, ceiling }` entry carries captured pattern groups and intersects them
|
|
84
|
+
with that ceiling, as journal replay does.
|
|
85
|
+
|
|
86
|
+
## The four resolutions
|
|
87
|
+
|
|
88
|
+
`store.reply(requestId, resolution, pattern?)` takes one of four answers:
|
|
89
|
+
|
|
90
|
+
| Resolution | Effect |
|
|
91
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `once` | Authorizes this request alone. Nothing is added to the rules. |
|
|
93
|
+
| `run` | Adds an allow rule for the rest of the run. Requires a plan digest; without one the reply fails with `invalid_resolution`. |
|
|
94
|
+
| `remembered` | Adds an allow rule that a journal-backed store replays into later processes. |
|
|
95
|
+
| `deny` | Fails the parked request with `permission_denied`. |
|
|
96
|
+
|
|
97
|
+
For `run` and `remembered`, the pattern is the caller's if one is supplied and
|
|
98
|
+
otherwise derived from the exact capability. A resource containing glob
|
|
99
|
+
metacharacters has no unambiguous derived pattern, so the reply fails and says
|
|
100
|
+
to supply a pattern explicitly or resolve `once`.
|
|
101
|
+
|
|
102
|
+
A supplied pattern is checked before it is stored. `isValidGrantPattern`
|
|
103
|
+
refuses a pattern that names a different action, reaches a more dangerous
|
|
104
|
+
effect tier than the request displayed, or is a wildcard-bearing pattern
|
|
105
|
+
identical to the resource: the grammar has no escape, so that pattern's
|
|
106
|
+
wildcard reading would silently widen access. The refusal is
|
|
107
|
+
`invalid_resolution` with `"grant pattern exceeds the requested authority"`.
|
|
108
|
+
|
|
109
|
+
Adding a rule wakes every other parked request the new rule now allows, so one
|
|
110
|
+
`run` grant for `/workspace/**` resolves the queue behind it rather than
|
|
111
|
+
asking again per file.
|
|
112
|
+
|
|
113
|
+
A reply records its decision in the journal before anything activates, but the
|
|
114
|
+
write never holds the store's mutation permit. A slow or dead journal stalls
|
|
115
|
+
only the reply that issued it: checks, `list`, waiter cancellation, and store
|
|
116
|
+
closure all proceed while a write is stuck, and a write that exceeds
|
|
117
|
+
`maximumPersistMillis` (30 seconds) fails the reply with `journal_failed`,
|
|
118
|
+
leaving the request parked for a retry. A second reply to the same request
|
|
119
|
+
while its decision is mid-write fails fast with `request_not_found` rather
|
|
120
|
+
than queueing behind the journal. Envelope admissions follow the same rule,
|
|
121
|
+
and a concurrent identical envelope adopts the in-flight outcome instead of
|
|
122
|
+
writing a duplicate record.
|
|
123
|
+
|
|
124
|
+
## Envelope approvals
|
|
125
|
+
|
|
126
|
+
`grantEnvelope` approves a whole set of patterns at once, which is what a plan
|
|
127
|
+
approval is: the operator says yes to everything the plan declared, before it
|
|
128
|
+
starts. An envelope is a **set**, so `canonicalEnvelopePatterns` deduplicates
|
|
129
|
+
its predicates and sorts them by formatted identity, and `envelopeSignature`
|
|
130
|
+
computes `sha256:` followed by the 64 lowercase hexadecimal digits of the
|
|
131
|
+
SHA-256 digest of that canonical JSON encoding (plan digest, scope, sorted
|
|
132
|
+
formatted predicates). Existing v1 journal events keep their format: replay
|
|
133
|
+
recomputes the digest from their fields. Legacy full-JSON signature seeds are
|
|
134
|
+
normalized to the same digest before comparison.
|
|
135
|
+
Two approvals listing the same predicates in a different order are the same
|
|
136
|
+
approval, and the second one is a no-op rather than a second durable record.
|
|
137
|
+
|
|
138
|
+
Envelope patterns are checked by `isValidEnvelopePattern`, which preserves
|
|
139
|
+
exact action and filesystem effect-tier boundaries, so an envelope cannot turn
|
|
140
|
+
a read approval into a write one.
|
|
141
|
+
|
|
142
|
+
## Bounds are part of the contract
|
|
143
|
+
|
|
144
|
+
One store retains at most 1,024 policy rules across all four rulesets, 1,024
|
|
145
|
+
activated envelope signatures, 256 patterns per envelope, and 1,024 parked
|
|
146
|
+
requests. Metadata is limited to 16 levels of nesting, 1,024 members, and
|
|
147
|
+
64 KiB of canonical JSON, and one encoded event to 256 KiB. Run, plan,
|
|
148
|
+
and request identities are limited to 4,096 UTF-16 code units. Generated
|
|
149
|
+
envelope signatures occupy 71 ASCII characters; legacy signature seeds must
|
|
150
|
+
fit the 256 KiB event bound before normalization. Other supplied signature
|
|
151
|
+
identities retain the 4,096-unit limit. Capability resources share the
|
|
152
|
+
capability package's own 4,096-unit limit.
|
|
153
|
+
|
|
154
|
+
Construction and runtime envelopes use the same admission check. Incoming
|
|
155
|
+
canonical patterns count toward the combined rule ceiling; rules already
|
|
156
|
+
replayed for the same envelope are counted once. Journal reconstruction uses
|
|
157
|
+
this admission check before appending a construction envelope.
|
|
158
|
+
|
|
159
|
+
Every bound failure is `invalid_resolution` and happens **before** any state
|
|
160
|
+
or journal authority changes, so a refused write never half-applies. The
|
|
161
|
+
constants are exported (`GrantStore.maximumRules` and its siblings) so a host
|
|
162
|
+
can check against the same numbers rather than guessing them.
|
|
163
|
+
|
|
164
|
+
## Identity is exact text
|
|
165
|
+
|
|
166
|
+
Capability actions and resources, patterns, run ids, plan digests, request
|
|
167
|
+
ids, and grant metadata are identity-bearing values. Smithers validates
|
|
168
|
+
well-formed text but does not apply Unicode normalization: matching,
|
|
169
|
+
signatures, and journal replay all use the exact JavaScript string and its
|
|
170
|
+
UTF-16 code-unit sequence. A caller that wants NFC applies it before
|
|
171
|
+
constructing the value. Identity fields whose contract forbids it reject lone
|
|
172
|
+
surrogates and NUL.
|
|
173
|
+
|
|
174
|
+
## Related
|
|
175
|
+
|
|
176
|
+
- [Write a capability policy](../guides/write-a-capability-policy.md): rules
|
|
177
|
+
and ceilings in practice.
|
|
178
|
+
- [Answer permission requests](../guides/answer-permission-requests.md): the
|
|
179
|
+
attended surface.
|
|
180
|
+
- [The authorization model](/pkg/capability/concepts/authorization-model): the
|
|
181
|
+
vocabulary and the evaluation function this page drives.
|