@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/src/Jj.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission-aware Jujutsu version-control operations.
|
|
3
|
+
*
|
|
4
|
+
* There is no kernel `Jj` interface and no kernel `Jj` tag. `@smthrs/jj` owns
|
|
5
|
+
* both, and its error channel already names the failures the kernel adds, so
|
|
6
|
+
* this module is only the middleware: a `Layer` over the same tag that reads
|
|
7
|
+
* the raw service out of context and returns a guarded one in its place.
|
|
8
|
+
*
|
|
9
|
+
* Governing design:
|
|
10
|
+
* `docs/specs/Concepts/Permission Kernel.md`,
|
|
11
|
+
* `docs/specs/Concepts/Effect Taxonomy.md`, and
|
|
12
|
+
* `docs/specs/Concepts/Host Adapters.md`.
|
|
13
|
+
*
|
|
14
|
+
* @since 1.0.0-rc.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Action } from "@smthrs/capability/Capability"
|
|
18
|
+
import { permissionDenied } from "@smthrs/capability/Permission"
|
|
19
|
+
import { Jj, type OperationId, type Revision } from "@smthrs/jj"
|
|
20
|
+
import { Effect, FileSystem as EffectFileSystem, Layer, Path as EffectPath } from "effect"
|
|
21
|
+
import { canonicalResource } from "./FileSystem.ts"
|
|
22
|
+
import { GrantStore } from "./GrantStore.ts"
|
|
23
|
+
import { makeCapability } from "./internal/makeCapability.ts"
|
|
24
|
+
import { Workspace } from "./Workspace.ts"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `@smthrs/jj`'s own Jujutsu tag, unchanged. Re-exported so the kernel
|
|
28
|
+
* namespace stays one-stop; it is the *same* tag, never a second one.
|
|
29
|
+
*
|
|
30
|
+
* @category services
|
|
31
|
+
* @since 1.0.0-rc.0
|
|
32
|
+
*/
|
|
33
|
+
export { Jj } from "@smthrs/jj"
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Provides an unavailable Jujutsu stub.
|
|
37
|
+
*
|
|
38
|
+
* @category layers
|
|
39
|
+
* @since 1.0.0-rc.0
|
|
40
|
+
*/
|
|
41
|
+
export { layerNoop } from "@smthrs/jj"
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Constructs a Jujutsu service from an implementation.
|
|
45
|
+
*
|
|
46
|
+
* @category constructors
|
|
47
|
+
* @since 1.0.0-rc.0
|
|
48
|
+
*/
|
|
49
|
+
export { make } from "@smthrs/jj"
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Constructs an unavailable Jujutsu stub.
|
|
53
|
+
*
|
|
54
|
+
* @category constructors
|
|
55
|
+
* @since 1.0.0-rc.0
|
|
56
|
+
*/
|
|
57
|
+
export { makeNoop } from "@smthrs/jj"
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Decorates the Jujutsu service in place with operation-specific capability
|
|
61
|
+
* checks.
|
|
62
|
+
*
|
|
63
|
+
* The layer provides the tag it also requires: compose it over a host jj layer
|
|
64
|
+
* with `Layer.provide` and every consumer of `Jj` — including one that never
|
|
65
|
+
* heard of the kernel — resolves the guarded implementation.
|
|
66
|
+
*
|
|
67
|
+
* `workspaceAdd` canonicalizes its destination and `root` canonicalizes its
|
|
68
|
+
* starting directory through the *raw* filesystem before asking for
|
|
69
|
+
* `jj:workspace-add` and `fs:write`, or for `jj:root`, so an existing symlink
|
|
70
|
+
* cannot turn an inside-workspace grant into outside authority or separate the
|
|
71
|
+
* authorized path from the directory jj is run in.
|
|
72
|
+
*
|
|
73
|
+
* @category layers
|
|
74
|
+
* @since 1.0.0-rc.0
|
|
75
|
+
*/
|
|
76
|
+
export const layer: Layer.Layer<
|
|
77
|
+
Jj,
|
|
78
|
+
never,
|
|
79
|
+
Jj | EffectFileSystem.FileSystem | EffectPath.Path | Workspace | GrantStore
|
|
80
|
+
> = Layer.effect(
|
|
81
|
+
Jj,
|
|
82
|
+
Effect.gen(function*() {
|
|
83
|
+
const jj = yield* Jj
|
|
84
|
+
const jjRoot = jj.root
|
|
85
|
+
const jjRevert = jj.revert
|
|
86
|
+
const jjOpRestore = jj.opRestore
|
|
87
|
+
const fileSystem = yield* EffectFileSystem.FileSystem
|
|
88
|
+
const path = yield* EffectPath.Path
|
|
89
|
+
const workspace = yield* Workspace
|
|
90
|
+
const grants = yield* GrantStore
|
|
91
|
+
const check = (action: Action, resource: string) =>
|
|
92
|
+
makeCapability(action, resource).pipe(Effect.flatMap((capability) => grants.check(capability)))
|
|
93
|
+
return Jj.of({
|
|
94
|
+
status: Effect.fn("Jj.status")(() => check("jj:status", ".").pipe(Effect.andThen(jj.status()))),
|
|
95
|
+
diff: Effect.fn("Jj.diff")((from, to) =>
|
|
96
|
+
check("jj:diff", `${from}:${to}`).pipe(Effect.andThen(jj.diff(from, to)))
|
|
97
|
+
),
|
|
98
|
+
snapshot: Effect.fn("Jj.snapshot")((message) =>
|
|
99
|
+
check("jj:snapshot", message ?? "").pipe(Effect.andThen(jj.snapshot(message)))
|
|
100
|
+
),
|
|
101
|
+
restore: Effect.fn("Jj.restore")((revision) =>
|
|
102
|
+
check("jj:restore", revision).pipe(Effect.andThen(jj.restore(revision)))
|
|
103
|
+
),
|
|
104
|
+
workspaceAdd: Effect.fn("Jj.workspaceAdd")((name, destination, revision) =>
|
|
105
|
+
canonicalResource(fileSystem, path, workspace.root, destination).pipe(
|
|
106
|
+
Effect.flatMap((resource) =>
|
|
107
|
+
check("jj:workspace-add", resource).pipe(
|
|
108
|
+
Effect.andThen(check("fs:write", resource)),
|
|
109
|
+
Effect.andThen(canonicalResource(fileSystem, path, workspace.root, destination)),
|
|
110
|
+
Effect.flatMap((settled) =>
|
|
111
|
+
settled === resource
|
|
112
|
+
? jj.workspaceAdd(name, resource, revision)
|
|
113
|
+
: makeCapability("jj:workspace-add", resource).pipe(
|
|
114
|
+
Effect.flatMap((capability) =>
|
|
115
|
+
Effect.fail(permissionDenied(
|
|
116
|
+
capability,
|
|
117
|
+
"workspace destination no longer names the resource that was authorized"
|
|
118
|
+
))
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
),
|
|
126
|
+
workspaceForget: Effect.fn("Jj.workspaceForget")((name) =>
|
|
127
|
+
check("jj:workspace-forget", name).pipe(Effect.andThen(jj.workspaceForget(name)))
|
|
128
|
+
),
|
|
129
|
+
// `root`, `revert`, and `opRestore` are optional on the service, so the decorator
|
|
130
|
+
// forwards the ABSENCE too. A backend that cannot revert must keep
|
|
131
|
+
// reading as a backend that cannot revert: replacing it with a guarded
|
|
132
|
+
// method that fails on call would turn "this host has no revert" into
|
|
133
|
+
// "your revert was refused", which is a different answer to a caller
|
|
134
|
+
// deciding what it can offer.
|
|
135
|
+
...jjRoot === undefined ? {} : {
|
|
136
|
+
root: Effect.fn("Jj.root")((from: string) =>
|
|
137
|
+
// Canonicalized BEFORE the check, and the canonical path is what jj
|
|
138
|
+
// is then run in. Checking the caller's spelling and inspecting a
|
|
139
|
+
// different directory is the whole symlink-alias problem: an
|
|
140
|
+
// authorized name would decide the answer for a repository the grant
|
|
141
|
+
// never mentioned.
|
|
142
|
+
canonicalResource(fileSystem, path, workspace.root, from).pipe(
|
|
143
|
+
Effect.flatMap((resource) => check("jj:root", resource).pipe(Effect.andThen(jjRoot(resource))))
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
...jjRevert === undefined ? {} : {
|
|
148
|
+
revert: Effect.fn("Jj.revert")((revision: Revision) =>
|
|
149
|
+
check("jj:revert", revision).pipe(Effect.andThen(jjRevert(revision)))
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
...jjOpRestore === undefined ? {} : {
|
|
153
|
+
opRestore: Effect.fn("Jj.opRestore")((operationId: OperationId) =>
|
|
154
|
+
check("jj:op-restore", operationId).pipe(Effect.andThen(jjOpRestore(operationId)))
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
)
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal-backed persistence and replay for capability decisions.
|
|
3
|
+
*
|
|
4
|
+
* Governing design:
|
|
5
|
+
* `docs/specs/Concepts/Permission Kernel.md` and
|
|
6
|
+
* `docs/specs/Concepts/Journal Queue.md`.
|
|
7
|
+
*
|
|
8
|
+
* @since 1.0.0-rc.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as Capability from "@smthrs/capability/Capability"
|
|
12
|
+
import { GrantStoreError, Rule } from "@smthrs/capability/Permission"
|
|
13
|
+
import * as JournalModule from "@smthrs/journal/Journal"
|
|
14
|
+
import * as JournalEvent from "@smthrs/journal/JournalEvent"
|
|
15
|
+
import * as Effect from "effect/Effect"
|
|
16
|
+
import * as Layer from "effect/Layer"
|
|
17
|
+
import * as Schema from "effect/Schema"
|
|
18
|
+
import * as Semaphore from "effect/Semaphore"
|
|
19
|
+
import { decode, type GrantEvent, GrantEventSchema } from "./GrantEvent.ts"
|
|
20
|
+
import * as GrantStore from "./GrantStore.ts"
|
|
21
|
+
import { Workspace } from "./Workspace.ts"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for a journal-backed grant store.
|
|
25
|
+
*
|
|
26
|
+
* `policyRunId` is a deliberately dedicated run containing remembered-policy
|
|
27
|
+
* events. The journal has no global grant projection, so callers that want
|
|
28
|
+
* remembered grants to span operational runs must keep this id stable.
|
|
29
|
+
* `planDigest` binds run grants and envelopes to the exact active plan.
|
|
30
|
+
* `sourceId` is also checked during replay; events from other producers cannot
|
|
31
|
+
* activate kernel authority.
|
|
32
|
+
*
|
|
33
|
+
* The journal is authoritative permission storage: SqlJournal must use the
|
|
34
|
+
* `reject` overflow policy. Drop-capable overflow policies are unsupported,
|
|
35
|
+
* because a dropped grant decision cannot safely be treated as persisted.
|
|
36
|
+
*
|
|
37
|
+
* @category models
|
|
38
|
+
* @since 1.0.0-rc.0
|
|
39
|
+
*/
|
|
40
|
+
export interface JournalGrantStoreOptions {
|
|
41
|
+
readonly runId: string
|
|
42
|
+
readonly policyRunId: string
|
|
43
|
+
readonly sourceId: string
|
|
44
|
+
readonly planDigest: string
|
|
45
|
+
readonly attended?: boolean
|
|
46
|
+
readonly rules?: ReadonlyArray<ReadonlyArray<Rule>>
|
|
47
|
+
readonly envelope?: {
|
|
48
|
+
readonly patterns: ReadonlyArray<Capability.CapabilityPattern>
|
|
49
|
+
readonly scope?: "run" | "remembered" | undefined
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const journalFailed = (message: string, cause: unknown): GrantStoreError =>
|
|
54
|
+
new GrantStoreError({
|
|
55
|
+
code: "journal_failed",
|
|
56
|
+
message,
|
|
57
|
+
cause
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const knownEventTypes: ReadonlySet<string> = new Set([
|
|
61
|
+
"flows.kernel.grant.once.v1",
|
|
62
|
+
"flows.kernel.grant.run.v1",
|
|
63
|
+
"flows.kernel.grant.run.v2",
|
|
64
|
+
"flows.kernel.grant.remembered.v1",
|
|
65
|
+
"flows.kernel.grant.denied.v1",
|
|
66
|
+
"flows.kernel.grant.envelope.v1"
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
const invalidReplay = (message: string): GrantStoreError => new GrantStoreError({ code: "invalid_resolution", message })
|
|
70
|
+
|
|
71
|
+
const appendReplayedRule = (
|
|
72
|
+
rules: Array<NonNullable<GrantStore.MakeOptions["runRules"]>[number]>,
|
|
73
|
+
seen: Set<string>,
|
|
74
|
+
pattern: Capability.CapabilityPattern
|
|
75
|
+
): void => {
|
|
76
|
+
const rule = new Rule({ effect: "allow", pattern })
|
|
77
|
+
const key = Capability.format(rule.pattern)
|
|
78
|
+
if (seen.has(key)) return
|
|
79
|
+
seen.add(key)
|
|
80
|
+
rules.push(rule)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const validId = (value: unknown): value is string => {
|
|
84
|
+
if (typeof value !== "string" || value.length === 0 || value.length > GrantStore.maximumIdentityLength) return false
|
|
85
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
86
|
+
const unit = value.charCodeAt(index)
|
|
87
|
+
if (unit === 0) return false
|
|
88
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
89
|
+
const next = value.charCodeAt(index + 1)
|
|
90
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false
|
|
91
|
+
index += 1
|
|
92
|
+
} else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const encodeGrantEvent = Schema.encodeSync(GrantEventSchema)
|
|
100
|
+
|
|
101
|
+
// Replay reads two runs (`policyRunId` and `runId`) and journal sequences are
|
|
102
|
+
// per run, so every refusal names the run it was replaying: the operator then
|
|
103
|
+
// knows which journal run to inspect. Replay always reads a run from its start
|
|
104
|
+
// and never consults a checkpoint, so compacting a run it replays makes every
|
|
105
|
+
// later construction fail `compacted`.
|
|
106
|
+
const decodeTrustedEntry = (
|
|
107
|
+
entry: JournalEvent.Entry,
|
|
108
|
+
sourceId: string,
|
|
109
|
+
runId: string
|
|
110
|
+
): Effect.Effect<GrantEvent | undefined, GrantStoreError> => {
|
|
111
|
+
if (entry.sourceId !== sourceId || !knownEventTypes.has(entry.eventType)) {
|
|
112
|
+
return Effect.succeed(undefined)
|
|
113
|
+
}
|
|
114
|
+
if (entry.eventType === "flows.kernel.grant.run.v1") {
|
|
115
|
+
return Effect.fail(
|
|
116
|
+
invalidReplay(`legacy run grant has no captured ceiling in run ${runId} at journal sequence ${entry.seq}`)
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
const event = decode(entry.payload)
|
|
120
|
+
if (event._tag === "Failure") {
|
|
121
|
+
return Effect.fail(invalidReplay(`invalid grant payload in run ${runId} at journal sequence ${entry.seq}`))
|
|
122
|
+
}
|
|
123
|
+
if (event.success.eventType !== entry.eventType) {
|
|
124
|
+
return Effect.fail(
|
|
125
|
+
invalidReplay(`grant envelope/payload type mismatch in run ${runId} at journal sequence ${entry.seq}`)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return Effect.succeed(event.success)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// One construction-envelope critical section per Journal service instance.
|
|
132
|
+
// Two concurrent constructors sharing one service object can both replay the
|
|
133
|
+
// envelope's absence before either persists it; the lock serializes the
|
|
134
|
+
// re-check-then-append so exactly one construction envelope lands.
|
|
135
|
+
//
|
|
136
|
+
// The guarantee stops at the service object. The lock is a WeakMap keyed by
|
|
137
|
+
// the in-memory `Journal.Service`, and the persisted Input carries no
|
|
138
|
+
// `sourceSeq` or `dedupe` key, so two service objects over one backing journal
|
|
139
|
+
// (two processes, or two connections in one process) each hold their own
|
|
140
|
+
// permit and can both append the envelope. Replay tolerates the duplicate:
|
|
141
|
+
// the signature set collapses it and the rules are identical.
|
|
142
|
+
const constructionLocks = new WeakMap<JournalModule.Service, Semaphore.Semaphore>()
|
|
143
|
+
|
|
144
|
+
const constructionLock = (journal: JournalModule.Service): Semaphore.Semaphore => {
|
|
145
|
+
const existing = constructionLocks.get(journal)
|
|
146
|
+
if (existing !== undefined) {
|
|
147
|
+
return existing
|
|
148
|
+
}
|
|
149
|
+
const created = Semaphore.makeUnsafe(1)
|
|
150
|
+
constructionLocks.set(journal, created)
|
|
151
|
+
return created
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const replayRememberedRules = (
|
|
155
|
+
policyRunId: string,
|
|
156
|
+
sourceId: string,
|
|
157
|
+
workspaceRoot: string
|
|
158
|
+
) =>
|
|
159
|
+
Effect.gen(function*() {
|
|
160
|
+
const journal = yield* JournalModule.Journal
|
|
161
|
+
const rules: Array<Rule> = []
|
|
162
|
+
const seenRules = new Set<string>()
|
|
163
|
+
const envelopeSignatures = new Set<string>()
|
|
164
|
+
let after: JournalEvent.Seq | undefined
|
|
165
|
+
|
|
166
|
+
do {
|
|
167
|
+
const page = yield* journal.entries({
|
|
168
|
+
runId: policyRunId as JournalEvent.RunId,
|
|
169
|
+
...(after === undefined ? {} : { after }),
|
|
170
|
+
limit: 256
|
|
171
|
+
})
|
|
172
|
+
const last = page.entries.at(-1)
|
|
173
|
+
if (last !== undefined && after !== undefined && last.seq <= after) {
|
|
174
|
+
// A page that does not advance past the cursor it was asked for is
|
|
175
|
+
// corrupt journal output. Following it would replay the same events
|
|
176
|
+
// forever; accepting it would double-apply them. Refuse construction.
|
|
177
|
+
return yield* Effect.fail(
|
|
178
|
+
invalidReplay(`non-advancing journal page in run ${policyRunId} at sequence ${last.seq}`)
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
for (const entry of page.entries) {
|
|
182
|
+
const event = yield* decodeTrustedEntry(entry, sourceId, policyRunId)
|
|
183
|
+
if (event === undefined) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
if (event.eventType === "flows.kernel.grant.remembered.v1") {
|
|
187
|
+
if (!GrantStore.isValidGrantPattern(event.pattern, event.capability, event.tier, workspaceRoot)) {
|
|
188
|
+
return yield* Effect.fail(
|
|
189
|
+
invalidReplay(`unsafe remembered grant event in run ${policyRunId} at sequence ${entry.seq}`)
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
appendReplayedRule(rules, seenRules, event.pattern)
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if (event.eventType === "flows.kernel.grant.envelope.v1" && event.scope === "remembered") {
|
|
196
|
+
if (event.patterns.some((pattern) => !GrantStore.isValidEnvelopePattern(pattern, workspaceRoot))) {
|
|
197
|
+
return yield* Effect.fail(
|
|
198
|
+
invalidReplay(`unsafe remembered envelope event in run ${policyRunId} at sequence ${entry.seq}`)
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
for (const pattern of event.patterns) appendReplayedRule(rules, seenRules, pattern)
|
|
202
|
+
envelopeSignatures.add(GrantStore.envelopeSignature(event.planDigest, event.scope, event.patterns))
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
return yield* Effect.fail(
|
|
206
|
+
invalidReplay(`run-scoped event found in policy journal run ${policyRunId} at sequence ${entry.seq}`)
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
after = last?.seq
|
|
210
|
+
if (!page.hasMore) {
|
|
211
|
+
return { rules, envelopeSignatures }
|
|
212
|
+
}
|
|
213
|
+
} while (after !== undefined)
|
|
214
|
+
|
|
215
|
+
return { rules, envelopeSignatures }
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const replayRunRules = (
|
|
219
|
+
runId: string,
|
|
220
|
+
sourceId: string,
|
|
221
|
+
planDigest: string,
|
|
222
|
+
workspaceRoot: string
|
|
223
|
+
) =>
|
|
224
|
+
Effect.gen(function*() {
|
|
225
|
+
const journal = yield* JournalModule.Journal
|
|
226
|
+
const rules: Array<NonNullable<GrantStore.MakeOptions["runRules"]>[number]> = []
|
|
227
|
+
const seenRules = new Set<string>()
|
|
228
|
+
const envelopeSignatures = new Set<string>()
|
|
229
|
+
let after: JournalEvent.Seq | undefined
|
|
230
|
+
|
|
231
|
+
do {
|
|
232
|
+
const page = yield* journal.entries({
|
|
233
|
+
runId: runId as JournalEvent.RunId,
|
|
234
|
+
...(after === undefined ? {} : { after }),
|
|
235
|
+
limit: 256
|
|
236
|
+
})
|
|
237
|
+
const last = page.entries.at(-1)
|
|
238
|
+
if (last !== undefined && after !== undefined && last.seq <= after) {
|
|
239
|
+
// See the identical guard in `replayRememberedRules`.
|
|
240
|
+
return yield* Effect.fail(invalidReplay(`non-advancing journal page in run ${runId} at sequence ${last.seq}`))
|
|
241
|
+
}
|
|
242
|
+
for (const entry of page.entries) {
|
|
243
|
+
const event = yield* decodeTrustedEntry(entry, sourceId, runId)
|
|
244
|
+
if (event === undefined) {
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
if (event.runId !== runId) {
|
|
248
|
+
return yield* Effect.fail(
|
|
249
|
+
invalidReplay(`grant payload run mismatch in run ${runId} at journal sequence ${entry.seq}`)
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
if (event.eventType === "flows.kernel.grant.once.v1" || event.eventType === "flows.kernel.grant.denied.v1") {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
if (event.eventType === "flows.kernel.grant.run.v2") {
|
|
256
|
+
if (event.planDigest !== planDigest) {
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
if (!GrantStore.isValidGrantPattern(event.pattern, event.capability, event.tier, workspaceRoot)) {
|
|
260
|
+
return yield* Effect.fail(invalidReplay(`unsafe run grant event in run ${runId} at sequence ${entry.seq}`))
|
|
261
|
+
}
|
|
262
|
+
const key = `run:${JSON.stringify([event.pattern, event.ceiling])}`
|
|
263
|
+
if (!seenRules.has(key)) {
|
|
264
|
+
seenRules.add(key)
|
|
265
|
+
rules.push({ rule: new Rule({ effect: "allow", pattern: event.pattern }), ceiling: event.ceiling })
|
|
266
|
+
}
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
if (event.eventType === "flows.kernel.grant.envelope.v1" && event.scope === "run") {
|
|
270
|
+
if (event.planDigest !== planDigest) {
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (event.patterns.some((pattern) => !GrantStore.isValidEnvelopePattern(pattern, workspaceRoot))) {
|
|
274
|
+
return yield* Effect.fail(
|
|
275
|
+
invalidReplay(`unsafe run envelope event in run ${runId} at sequence ${entry.seq}`)
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
for (const pattern of event.patterns) appendReplayedRule(rules, seenRules, pattern)
|
|
279
|
+
envelopeSignatures.add(GrantStore.envelopeSignature(event.planDigest, event.scope, event.patterns))
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
return yield* Effect.fail(
|
|
283
|
+
invalidReplay(`remembered event found in run journal ${runId} at sequence ${entry.seq}`)
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
after = last?.seq
|
|
287
|
+
if (!page.hasMore) {
|
|
288
|
+
return { rules, envelopeSignatures }
|
|
289
|
+
}
|
|
290
|
+
} while (after !== undefined)
|
|
291
|
+
|
|
292
|
+
return { rules, envelopeSignatures }
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Makes a grant store that replays remembered policy from, and writes decisions
|
|
297
|
+
* to, the supplied journal.
|
|
298
|
+
*
|
|
299
|
+
* `emitDurable` commits before `GrantStore` activates a remembered or
|
|
300
|
+
* run-scoped rule (or resolves a denial). Any journal failure is mapped to
|
|
301
|
+
* `journal_failed`, so permission decisions fail closed.
|
|
302
|
+
*
|
|
303
|
+
* @category constructors
|
|
304
|
+
* @since 1.0.0-rc.0
|
|
305
|
+
*/
|
|
306
|
+
export const make = (options: JournalGrantStoreOptions) =>
|
|
307
|
+
Effect.gen(function*() {
|
|
308
|
+
const runId = options.runId
|
|
309
|
+
const policyRunId = options.policyRunId
|
|
310
|
+
const sourceId = options.sourceId
|
|
311
|
+
const planDigest = options.planDigest
|
|
312
|
+
if (!validId(runId)) return yield* Effect.fail(invalidReplay("runId must be non-empty, bounded, well-formed text"))
|
|
313
|
+
if (!validId(policyRunId)) {
|
|
314
|
+
return yield* Effect.fail(invalidReplay("policyRunId must be non-empty, bounded, well-formed text"))
|
|
315
|
+
}
|
|
316
|
+
if (!validId(sourceId)) {
|
|
317
|
+
return yield* Effect.fail(invalidReplay("sourceId must be non-empty, bounded, well-formed text"))
|
|
318
|
+
}
|
|
319
|
+
if (!validId(planDigest)) {
|
|
320
|
+
return yield* Effect.fail(invalidReplay("planDigest must be non-empty, bounded, well-formed text"))
|
|
321
|
+
}
|
|
322
|
+
if (runId === policyRunId) {
|
|
323
|
+
return yield* Effect.fail(invalidReplay("runId and policyRunId must be distinct"))
|
|
324
|
+
}
|
|
325
|
+
const journal = yield* JournalModule.Journal
|
|
326
|
+
const workspace = yield* Workspace
|
|
327
|
+
const replayPolicy = replayRememberedRules(
|
|
328
|
+
policyRunId,
|
|
329
|
+
sourceId,
|
|
330
|
+
workspace.root
|
|
331
|
+
).pipe(
|
|
332
|
+
Effect.mapError((cause) =>
|
|
333
|
+
cause instanceof GrantStoreError ? cause : journalFailed("could not replay remembered grants", cause)
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
const replayRun = replayRunRules(
|
|
337
|
+
runId,
|
|
338
|
+
sourceId,
|
|
339
|
+
planDigest,
|
|
340
|
+
workspace.root
|
|
341
|
+
).pipe(
|
|
342
|
+
Effect.mapError((cause) =>
|
|
343
|
+
cause instanceof GrantStoreError ? cause : journalFailed("could not replay run grants", cause)
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
let replayedPolicy = yield* replayPolicy
|
|
347
|
+
let replayedRun = yield* replayRun
|
|
348
|
+
const persist = (event: GrantEvent): Effect.Effect<void, GrantStoreError> => {
|
|
349
|
+
const payload = encodeGrantEvent(event)
|
|
350
|
+
return Effect.gen(function*() {
|
|
351
|
+
// Unfenced: the grant store is the kernel's own ledger, not a run's
|
|
352
|
+
// lifecycle — it owns no run, and grant admissions are
|
|
353
|
+
// first-writer-wins records replayed by every later process.
|
|
354
|
+
yield* journal.emitDurableUnfenced(
|
|
355
|
+
new JournalEvent.Input({
|
|
356
|
+
runId: (
|
|
357
|
+
event.eventType === "flows.kernel.grant.remembered.v1"
|
|
358
|
+
|| (event.eventType === "flows.kernel.grant.envelope.v1" && event.scope === "remembered")
|
|
359
|
+
? policyRunId
|
|
360
|
+
: runId
|
|
361
|
+
) as JournalEvent.RunId,
|
|
362
|
+
sourceId: sourceId as JournalEvent.SourceId,
|
|
363
|
+
eventType: event.eventType,
|
|
364
|
+
payload
|
|
365
|
+
})
|
|
366
|
+
)
|
|
367
|
+
}).pipe(
|
|
368
|
+
Effect.mapError((cause) => journalFailed("could not persist grant event", cause)),
|
|
369
|
+
Effect.asVoid
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
const envelope = options.envelope
|
|
373
|
+
const build = () => {
|
|
374
|
+
const configuredCount = options.rules?.reduce((count, rules) => count + rules.length, 0) ?? 0
|
|
375
|
+
const rememberedCount = replayedPolicy.rules.length
|
|
376
|
+
const runCount = replayedRun.rules.length
|
|
377
|
+
const total = configuredCount + rememberedCount + runCount
|
|
378
|
+
if (total > GrantStore.maximumRules) {
|
|
379
|
+
return Effect.fail(
|
|
380
|
+
invalidReplay(
|
|
381
|
+
`policy run ${policyRunId} replayed ${rememberedCount} remembered rules; configured rules (${configuredCount}) and replayed run rules (${runCount}) bring the total to ${total}, which exceeds the ${GrantStore.maximumRules}-rule ceiling; remove configured rules or start a new policyRunId and grant again`
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
// Envelope signatures accumulate in the policy journal exactly the way
|
|
386
|
+
// remembered rules do, and `GrantStore.make` refuses more than
|
|
387
|
+
// `maximumRules` of them with a message that names neither the journal
|
|
388
|
+
// nor the counts. Diagnose it here instead, so an operator reading the
|
|
389
|
+
// failure knows which policy run filled rather than which field overflowed.
|
|
390
|
+
const policySignatureCount = replayedPolicy.envelopeSignatures.size
|
|
391
|
+
const runSignatureCount = replayedRun.envelopeSignatures.size
|
|
392
|
+
const signatureTotal = policySignatureCount + runSignatureCount
|
|
393
|
+
if (signatureTotal > GrantStore.maximumRules) {
|
|
394
|
+
return Effect.fail(
|
|
395
|
+
invalidReplay(
|
|
396
|
+
`policy run ${policyRunId} replayed ${policySignatureCount} remembered envelope signatures; replayed run envelope signatures (${runSignatureCount}) bring the total to ${signatureTotal}, which exceeds the ${GrantStore.maximumRules}-envelope-signature ceiling; start a new policyRunId and grant again`
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
// GrantStore's shared envelope admission adds only patterns not already
|
|
401
|
+
// replayed for a seeded envelope, and checks the combined rule and
|
|
402
|
+
// signature counts before any construction event is persisted.
|
|
403
|
+
return GrantStore.make({
|
|
404
|
+
runId,
|
|
405
|
+
planDigest,
|
|
406
|
+
...(options.attended === undefined ? {} : { attended: options.attended }),
|
|
407
|
+
rules: options.rules === undefined || options.rules.length === 0
|
|
408
|
+
? [[], replayedPolicy.rules]
|
|
409
|
+
: [...options.rules, replayedPolicy.rules],
|
|
410
|
+
runRules: replayedRun.rules,
|
|
411
|
+
envelopeSignatures: [
|
|
412
|
+
...replayedPolicy.envelopeSignatures,
|
|
413
|
+
...replayedRun.envelopeSignatures
|
|
414
|
+
],
|
|
415
|
+
...(envelope === undefined ? {} : {
|
|
416
|
+
envelope: {
|
|
417
|
+
planDigest,
|
|
418
|
+
patterns: envelope.patterns,
|
|
419
|
+
...(envelope.scope === undefined ? {} : { scope: envelope.scope })
|
|
420
|
+
}
|
|
421
|
+
}),
|
|
422
|
+
persist
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
if (envelope === undefined || envelope.patterns.length === 0) {
|
|
426
|
+
return yield* build()
|
|
427
|
+
}
|
|
428
|
+
const scope = envelope.scope ?? "run"
|
|
429
|
+
const signature = GrantStore.envelopeSignature(planDigest, scope, envelope.patterns)
|
|
430
|
+
const replayedSignatures = scope === "remembered"
|
|
431
|
+
? replayedPolicy.envelopeSignatures
|
|
432
|
+
: replayedRun.envelopeSignatures
|
|
433
|
+
if (replayedSignatures.has(signature)) {
|
|
434
|
+
// Already durable: the seeded signature makes the construction envelope
|
|
435
|
+
// activate without persisting again.
|
|
436
|
+
return yield* build()
|
|
437
|
+
}
|
|
438
|
+
// The envelope was absent when this constructor replayed, but a concurrent
|
|
439
|
+
// constructor sharing this Journal service may persist it before we do.
|
|
440
|
+
// Re-replay the target run inside the per-service critical section, so
|
|
441
|
+
// exactly one such constructor appends the envelope and every other one
|
|
442
|
+
// replays it instead.
|
|
443
|
+
return yield* constructionLock(journal).withPermit(
|
|
444
|
+
Effect.gen(function*() {
|
|
445
|
+
if (scope === "remembered") {
|
|
446
|
+
replayedPolicy = yield* replayPolicy
|
|
447
|
+
} else {
|
|
448
|
+
replayedRun = yield* replayRun
|
|
449
|
+
}
|
|
450
|
+
return yield* build()
|
|
451
|
+
})
|
|
452
|
+
)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Provides a journal-backed `GrantStore`.
|
|
457
|
+
*
|
|
458
|
+
* @category layers
|
|
459
|
+
* @since 1.0.0-rc.0
|
|
460
|
+
*/
|
|
461
|
+
export const layer = (options: JournalGrantStoreOptions) => Layer.effect(GrantStore.GrantStore)(make(options))
|
package/src/Path.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit kernel decision for the Effect path service.
|
|
3
|
+
*
|
|
4
|
+
* Governing design:
|
|
5
|
+
* `docs/specs/Concepts/Permission Kernel.md` and
|
|
6
|
+
* `docs/specs/Concepts/Host Adapters.md`.
|
|
7
|
+
*
|
|
8
|
+
* @since 1.0.0-rc.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Effect, Layer, Path as EffectPath } from "effect"
|
|
12
|
+
import * as Rooted from "./Rooted.ts"
|
|
13
|
+
import { Workspace } from "./Workspace.ts"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Effect's lexical path-service shape.
|
|
17
|
+
*
|
|
18
|
+
* @category models
|
|
19
|
+
* @since 1.0.0-rc.0
|
|
20
|
+
*/
|
|
21
|
+
export type Path = EffectPath.Path
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The unchanged underlying path-service tag.
|
|
25
|
+
*
|
|
26
|
+
* @category services
|
|
27
|
+
* @since 1.0.0-rc.0
|
|
28
|
+
*/
|
|
29
|
+
export const Path = EffectPath.Path
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Re-provides Effect's path service with relative paths resolved against
|
|
33
|
+
* `Workspace.root`, the same root the kernel spawner runs commands in. Path
|
|
34
|
+
* manipulation is pure and lexical, so it requires no capability check. This
|
|
35
|
+
* explicit layer proves that every member of the Host services closed list has
|
|
36
|
+
* a kernel decision.
|
|
37
|
+
*
|
|
38
|
+
* @category layers
|
|
39
|
+
* @since 1.0.0-rc.0
|
|
40
|
+
*/
|
|
41
|
+
export const layer: Layer.Layer<Path, never, Path | Workspace> = Layer.effect(
|
|
42
|
+
Path,
|
|
43
|
+
Effect.gen(function*() {
|
|
44
|
+
const path = yield* Path
|
|
45
|
+
return Rooted.path(path, path.resolve((yield* Workspace).root))
|
|
46
|
+
})
|
|
47
|
+
)
|