@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.
Files changed (267) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -2
  4. package/dist/cjs/CapabilitySet.d.ts +6 -0
  5. package/dist/cjs/CapabilitySet.d.ts.map +1 -0
  6. package/dist/cjs/CapabilitySet.js +19 -0
  7. package/dist/cjs/CapabilitySet.js.map +7 -0
  8. package/dist/cjs/ChildProcessEnvironment.d.ts +60 -0
  9. package/dist/cjs/ChildProcessEnvironment.d.ts.map +1 -0
  10. package/dist/cjs/ChildProcessEnvironment.js +61 -0
  11. package/dist/cjs/ChildProcessEnvironment.js.map +7 -0
  12. package/dist/cjs/ChildProcessSpawner.d.ts +100 -0
  13. package/dist/cjs/ChildProcessSpawner.d.ts.map +1 -0
  14. package/dist/cjs/ChildProcessSpawner.js +291 -0
  15. package/dist/cjs/ChildProcessSpawner.js.map +7 -0
  16. package/dist/cjs/CommandLine.d.ts +173 -0
  17. package/dist/cjs/CommandLine.d.ts.map +1 -0
  18. package/dist/cjs/CommandLine.js +54 -0
  19. package/dist/cjs/CommandLine.js.map +7 -0
  20. package/dist/cjs/ContainedSpawner.d.ts +135 -0
  21. package/dist/cjs/ContainedSpawner.d.ts.map +1 -0
  22. package/dist/cjs/ContainedSpawner.js +141 -0
  23. package/dist/cjs/ContainedSpawner.js.map +7 -0
  24. package/dist/cjs/FileSystem.d.ts +452 -0
  25. package/dist/cjs/FileSystem.d.ts.map +1 -0
  26. package/dist/cjs/FileSystem.js +890 -0
  27. package/dist/cjs/FileSystem.js.map +7 -0
  28. package/dist/cjs/FileSystemBatch.d.ts +94 -0
  29. package/dist/cjs/FileSystemBatch.d.ts.map +1 -0
  30. package/dist/cjs/FileSystemBatch.js +31 -0
  31. package/dist/cjs/FileSystemBatch.js.map +7 -0
  32. package/dist/cjs/GrantEvent.d.ts +240 -0
  33. package/dist/cjs/GrantEvent.d.ts.map +1 -0
  34. package/dist/cjs/GrantEvent.js +115 -0
  35. package/dist/cjs/GrantEvent.js.map +7 -0
  36. package/dist/cjs/GrantStore.d.ts +270 -0
  37. package/dist/cjs/GrantStore.d.ts.map +1 -0
  38. package/dist/cjs/GrantStore.js +857 -0
  39. package/dist/cjs/GrantStore.js.map +7 -0
  40. package/dist/cjs/HostError.d.ts +36 -0
  41. package/dist/cjs/HostError.d.ts.map +1 -0
  42. package/dist/cjs/HostError.js +17 -0
  43. package/dist/cjs/HostError.js.map +7 -0
  44. package/dist/cjs/HostServices.d.ts +101 -0
  45. package/dist/cjs/HostServices.d.ts.map +1 -0
  46. package/dist/cjs/HostServices.js +66 -0
  47. package/dist/cjs/HostServices.js.map +7 -0
  48. package/dist/cjs/HttpClient.d.ts +177 -0
  49. package/dist/cjs/HttpClient.d.ts.map +1 -0
  50. package/dist/cjs/HttpClient.js +217 -0
  51. package/dist/cjs/HttpClient.js.map +7 -0
  52. package/dist/cjs/Jj.d.ts +67 -0
  53. package/dist/cjs/Jj.d.ts.map +1 -0
  54. package/dist/cjs/Jj.js +118 -0
  55. package/dist/cjs/Jj.js.map +7 -0
  56. package/dist/cjs/JournalGrantStore.d.ts +65 -0
  57. package/dist/cjs/JournalGrantStore.d.ts.map +1 -0
  58. package/dist/cjs/JournalGrantStore.js +354 -0
  59. package/dist/cjs/JournalGrantStore.js.map +7 -0
  60. package/dist/cjs/Path.d.ts +37 -0
  61. package/dist/cjs/Path.d.ts.map +1 -0
  62. package/dist/cjs/Path.js +46 -0
  63. package/dist/cjs/Path.js.map +7 -0
  64. package/dist/cjs/ProcessConfinement.d.ts +55 -0
  65. package/dist/cjs/ProcessConfinement.d.ts.map +1 -0
  66. package/dist/cjs/ProcessConfinement.js +94 -0
  67. package/dist/cjs/ProcessConfinement.js.map +7 -0
  68. package/dist/cjs/ProcessLedger.d.ts +260 -0
  69. package/dist/cjs/ProcessLedger.d.ts.map +1 -0
  70. package/dist/cjs/ProcessLedger.js +190 -0
  71. package/dist/cjs/ProcessLedger.js.map +7 -0
  72. package/dist/cjs/Rooted.d.ts +53 -0
  73. package/dist/cjs/Rooted.d.ts.map +1 -0
  74. package/dist/cjs/Rooted.js +101 -0
  75. package/dist/cjs/Rooted.js.map +7 -0
  76. package/dist/cjs/Unreachable.d.ts +27 -0
  77. package/dist/cjs/Unreachable.d.ts.map +1 -0
  78. package/dist/cjs/Unreachable.js +43 -0
  79. package/dist/cjs/Unreachable.js.map +7 -0
  80. package/dist/cjs/Workspace.d.ts +62 -0
  81. package/dist/cjs/Workspace.d.ts.map +1 -0
  82. package/dist/cjs/Workspace.js +35 -0
  83. package/dist/cjs/Workspace.js.map +7 -0
  84. package/dist/cjs/index.d.ts +161 -0
  85. package/dist/cjs/index.d.ts.map +1 -0
  86. package/dist/cjs/index.js +73 -0
  87. package/dist/cjs/index.js.map +7 -0
  88. package/dist/cjs/internal/Containment.d.ts +24 -0
  89. package/dist/cjs/internal/Containment.d.ts.map +1 -0
  90. package/dist/cjs/internal/Containment.js +30 -0
  91. package/dist/cjs/internal/Containment.js.map +7 -0
  92. package/dist/cjs/internal/Pipeline.d.ts +18 -0
  93. package/dist/cjs/internal/Pipeline.d.ts.map +1 -0
  94. package/dist/cjs/internal/Pipeline.js +90 -0
  95. package/dist/cjs/internal/Pipeline.js.map +7 -0
  96. package/dist/cjs/internal/makeCapability.d.ts +16 -0
  97. package/dist/cjs/internal/makeCapability.d.ts.map +1 -0
  98. package/dist/cjs/internal/makeCapability.js +44 -0
  99. package/dist/cjs/internal/makeCapability.js.map +7 -0
  100. package/dist/cjs/package.json +1 -0
  101. package/dist/cjs/test/FileSystemBatchContract.d.ts +15 -0
  102. package/dist/cjs/test/FileSystemBatchContract.d.ts.map +1 -0
  103. package/dist/cjs/test/FileSystemBatchContract.js +76 -0
  104. package/dist/cjs/test/FileSystemBatchContract.js.map +7 -0
  105. package/dist/cjs/test/HostContract.d.ts +225 -0
  106. package/dist/cjs/test/HostContract.d.ts.map +1 -0
  107. package/dist/cjs/test/HostContract.js +489 -0
  108. package/dist/cjs/test/HostContract.js.map +7 -0
  109. package/dist/cjs/test/TestGrantStore.d.ts +31 -0
  110. package/dist/cjs/test/TestGrantStore.d.ts.map +1 -0
  111. package/dist/cjs/test/TestGrantStore.js +65 -0
  112. package/dist/cjs/test/TestGrantStore.js.map +7 -0
  113. package/dist/esm/CapabilitySet.d.ts +6 -0
  114. package/dist/esm/CapabilitySet.d.ts.map +1 -0
  115. package/dist/esm/CapabilitySet.js +6 -0
  116. package/dist/esm/CapabilitySet.js.map +1 -0
  117. package/dist/esm/ChildProcessEnvironment.d.ts +60 -0
  118. package/dist/esm/ChildProcessEnvironment.d.ts.map +1 -0
  119. package/dist/esm/ChildProcessEnvironment.js +91 -0
  120. package/dist/esm/ChildProcessEnvironment.js.map +1 -0
  121. package/dist/esm/ChildProcessSpawner.d.ts +100 -0
  122. package/dist/esm/ChildProcessSpawner.d.ts.map +1 -0
  123. package/dist/esm/ChildProcessSpawner.js +342 -0
  124. package/dist/esm/ChildProcessSpawner.js.map +1 -0
  125. package/dist/esm/CommandLine.d.ts +173 -0
  126. package/dist/esm/CommandLine.d.ts.map +1 -0
  127. package/dist/esm/CommandLine.js +195 -0
  128. package/dist/esm/CommandLine.js.map +1 -0
  129. package/dist/esm/ContainedSpawner.d.ts +135 -0
  130. package/dist/esm/ContainedSpawner.d.ts.map +1 -0
  131. package/dist/esm/ContainedSpawner.js +180 -0
  132. package/dist/esm/ContainedSpawner.js.map +1 -0
  133. package/dist/esm/FileSystem.d.ts +452 -0
  134. package/dist/esm/FileSystem.d.ts.map +1 -0
  135. package/dist/esm/FileSystem.js +989 -0
  136. package/dist/esm/FileSystem.js.map +1 -0
  137. package/dist/esm/FileSystemBatch.d.ts +94 -0
  138. package/dist/esm/FileSystemBatch.d.ts.map +1 -0
  139. package/dist/esm/FileSystemBatch.js +26 -0
  140. package/dist/esm/FileSystemBatch.js.map +1 -0
  141. package/dist/esm/GrantEvent.d.ts +240 -0
  142. package/dist/esm/GrantEvent.d.ts.map +1 -0
  143. package/dist/esm/GrantEvent.js +155 -0
  144. package/dist/esm/GrantEvent.js.map +1 -0
  145. package/dist/esm/GrantStore.d.ts +270 -0
  146. package/dist/esm/GrantStore.d.ts.map +1 -0
  147. package/dist/esm/GrantStore.js +951 -0
  148. package/dist/esm/GrantStore.js.map +1 -0
  149. package/dist/esm/HostError.d.ts +36 -0
  150. package/dist/esm/HostError.d.ts.map +1 -0
  151. package/dist/esm/HostError.js +20 -0
  152. package/dist/esm/HostError.js.map +1 -0
  153. package/dist/esm/HostServices.d.ts +101 -0
  154. package/dist/esm/HostServices.d.ts.map +1 -0
  155. package/dist/esm/HostServices.js +76 -0
  156. package/dist/esm/HostServices.js.map +1 -0
  157. package/dist/esm/HttpClient.d.ts +177 -0
  158. package/dist/esm/HttpClient.d.ts.map +1 -0
  159. package/dist/esm/HttpClient.js +299 -0
  160. package/dist/esm/HttpClient.js.map +1 -0
  161. package/dist/esm/Jj.d.ts +67 -0
  162. package/dist/esm/Jj.d.ts.map +1 -0
  163. package/dist/esm/Jj.js +111 -0
  164. package/dist/esm/Jj.js.map +1 -0
  165. package/dist/esm/JournalGrantStore.d.ts +65 -0
  166. package/dist/esm/JournalGrantStore.d.ts.map +1 -0
  167. package/dist/esm/JournalGrantStore.js +350 -0
  168. package/dist/esm/JournalGrantStore.js.map +1 -0
  169. package/dist/esm/Path.d.ts +37 -0
  170. package/dist/esm/Path.d.ts.map +1 -0
  171. package/dist/esm/Path.js +34 -0
  172. package/dist/esm/Path.js.map +1 -0
  173. package/dist/esm/ProcessConfinement.d.ts +55 -0
  174. package/dist/esm/ProcessConfinement.d.ts.map +1 -0
  175. package/dist/esm/ProcessConfinement.js +106 -0
  176. package/dist/esm/ProcessConfinement.js.map +1 -0
  177. package/dist/esm/ProcessLedger.d.ts +260 -0
  178. package/dist/esm/ProcessLedger.d.ts.map +1 -0
  179. package/dist/esm/ProcessLedger.js +283 -0
  180. package/dist/esm/ProcessLedger.js.map +1 -0
  181. package/dist/esm/Rooted.d.ts +53 -0
  182. package/dist/esm/Rooted.d.ts.map +1 -0
  183. package/dist/esm/Rooted.js +104 -0
  184. package/dist/esm/Rooted.js.map +1 -0
  185. package/dist/esm/Unreachable.d.ts +27 -0
  186. package/dist/esm/Unreachable.d.ts.map +1 -0
  187. package/dist/esm/Unreachable.js +26 -0
  188. package/dist/esm/Unreachable.js.map +1 -0
  189. package/dist/esm/Workspace.d.ts +62 -0
  190. package/dist/esm/Workspace.d.ts.map +1 -0
  191. package/dist/esm/Workspace.js +51 -0
  192. package/dist/esm/Workspace.js.map +1 -0
  193. package/dist/esm/index.d.ts +161 -0
  194. package/dist/esm/index.d.ts.map +1 -0
  195. package/dist/esm/index.js +151 -0
  196. package/dist/esm/index.js.map +1 -0
  197. package/dist/esm/internal/Containment.d.ts +24 -0
  198. package/dist/esm/internal/Containment.d.ts.map +1 -0
  199. package/dist/esm/internal/Containment.js +27 -0
  200. package/dist/esm/internal/Containment.js.map +1 -0
  201. package/dist/esm/internal/Pipeline.d.ts +18 -0
  202. package/dist/esm/internal/Pipeline.d.ts.map +1 -0
  203. package/dist/esm/internal/Pipeline.js +68 -0
  204. package/dist/esm/internal/Pipeline.js.map +1 -0
  205. package/dist/esm/internal/makeCapability.d.ts +16 -0
  206. package/dist/esm/internal/makeCapability.d.ts.map +1 -0
  207. package/dist/esm/internal/makeCapability.js +22 -0
  208. package/dist/esm/internal/makeCapability.js.map +1 -0
  209. package/dist/esm/test/FileSystemBatchContract.d.ts +15 -0
  210. package/dist/esm/test/FileSystemBatchContract.d.ts.map +1 -0
  211. package/dist/esm/test/FileSystemBatchContract.js +57 -0
  212. package/dist/esm/test/FileSystemBatchContract.js.map +1 -0
  213. package/dist/esm/test/HostContract.d.ts +225 -0
  214. package/dist/esm/test/HostContract.d.ts.map +1 -0
  215. package/dist/esm/test/HostContract.js +460 -0
  216. package/dist/esm/test/HostContract.js.map +1 -0
  217. package/dist/esm/test/TestGrantStore.d.ts +31 -0
  218. package/dist/esm/test/TestGrantStore.d.ts.map +1 -0
  219. package/dist/esm/test/TestGrantStore.js +57 -0
  220. package/dist/esm/test/TestGrantStore.js.map +1 -0
  221. package/docs/README.md +162 -0
  222. package/docs/api.md +1532 -0
  223. package/docs/concepts/decoration-in-place.md +117 -0
  224. package/docs/concepts/filesystem-batches.md +58 -0
  225. package/docs/concepts/filesystem-confinement.md +168 -0
  226. package/docs/concepts/grant-decisions.md +181 -0
  227. package/docs/concepts/process-containment.md +155 -0
  228. package/docs/contract.md +14 -0
  229. package/docs/guides/adapt-a-new-host-platform.md +148 -0
  230. package/docs/guides/answer-permission-requests.md +151 -0
  231. package/docs/guides/authorize-network-and-model-calls.md +118 -0
  232. package/docs/guides/contain-spawned-processes.md +118 -0
  233. package/docs/guides/guard-a-host-bundle.md +109 -0
  234. package/docs/guides/persist-grants-across-restarts.md +135 -0
  235. package/docs/guides/write-a-capability-policy.md +159 -0
  236. package/docs/installation.md +115 -0
  237. package/docs/quickstart.md +182 -0
  238. package/docs/testing.md +108 -0
  239. package/docs/troubleshooting.md +281 -0
  240. package/package.json +344 -3
  241. package/src/CapabilitySet.ts +6 -0
  242. package/src/ChildProcessEnvironment.ts +98 -0
  243. package/src/ChildProcessSpawner.ts +414 -0
  244. package/src/CommandLine.ts +250 -0
  245. package/src/ContainedSpawner.ts +284 -0
  246. package/src/FileSystem.ts +1655 -0
  247. package/src/FileSystemBatch.ts +90 -0
  248. package/src/GrantEvent.ts +174 -0
  249. package/src/GrantStore.ts +1345 -0
  250. package/src/HostError.ts +37 -0
  251. package/src/HostServices.ts +133 -0
  252. package/src/HttpClient.ts +406 -0
  253. package/src/Jj.ts +159 -0
  254. package/src/JournalGrantStore.ts +461 -0
  255. package/src/Path.ts +47 -0
  256. package/src/ProcessConfinement.ts +156 -0
  257. package/src/ProcessLedger.ts +423 -0
  258. package/src/Rooted.ts +128 -0
  259. package/src/Unreachable.ts +30 -0
  260. package/src/Workspace.ts +65 -0
  261. package/src/index.ts +181 -0
  262. package/src/internal/Containment.ts +37 -0
  263. package/src/internal/Pipeline.ts +85 -0
  264. package/src/internal/makeCapability.ts +26 -0
  265. package/src/test/FileSystemBatchContract.ts +59 -0
  266. package/src/test/HostContract.ts +785 -0
  267. package/src/test/TestGrantStore.ts +73 -0
@@ -0,0 +1,1345 @@
1
+ /**
2
+ * In-memory capability grants with an attended Deferred request lifecycle.
3
+ *
4
+ * Governing design:
5
+ * `docs/specs/Concepts/Permission Kernel.md` and
6
+ * `docs/specs/Concepts/Trust Granularity.md`.
7
+ *
8
+ * @since 1.0.0-rc.0
9
+ */
10
+
11
+ import {
12
+ type Capability,
13
+ Capability as CapabilityValue,
14
+ CapabilityPattern,
15
+ type EffectTier,
16
+ format,
17
+ matches,
18
+ maxResourceLength,
19
+ parsePattern,
20
+ patternFromCapability,
21
+ subsumes,
22
+ tierOf
23
+ } from "@smthrs/capability/Capability"
24
+ import {
25
+ evaluate,
26
+ GrantStoreError,
27
+ type PermissionDenied,
28
+ permissionDenied,
29
+ type PermissionRequired,
30
+ permissionRequired,
31
+ Rule
32
+ } from "@smthrs/capability/Permission"
33
+ import { digestSync } from "@smthrs/crypto/Sha256"
34
+ import { Cause, Context, Deferred, Effect, Layer, Option, Schema, type Scope, Semaphore } from "effect"
35
+ import { allows, type CapabilitySet, current, fromPatterns, intersect } from "./CapabilitySet.ts"
36
+ import { DeniedGrant, EnvelopeGrant, type GrantEvent, OnceGrant, RememberedGrant, RunGrant } from "./GrantEvent.ts"
37
+ import { Workspace } from "./Workspace.ts"
38
+
39
+ /**
40
+ * A capability request waiting for an attended reply.
41
+ *
42
+ * @category models
43
+ * @since 1.0.0-rc.0
44
+ */
45
+ export interface PendingRequest {
46
+ readonly requestId: string
47
+ readonly capability: Capability
48
+ readonly tier: EffectTier
49
+ readonly meta: Record<string, unknown>
50
+ }
51
+
52
+ /**
53
+ * A resolution supplied by an attended permission surface.
54
+ *
55
+ * @category models
56
+ * @since 1.0.0-rc.0
57
+ */
58
+ export type Resolution = "once" | "run" | "remembered" | "deny"
59
+
60
+ /**
61
+ * Scope and plan identity for a bulk envelope approval.
62
+ *
63
+ * @category models
64
+ * @since 1.0.0-rc.0
65
+ */
66
+ export interface EnvelopeGrantOptions {
67
+ readonly planDigest: string
68
+ readonly patterns: ReadonlyArray<CapabilityPattern>
69
+ readonly scope?: "run" | "remembered" | undefined
70
+ }
71
+
72
+ /** A policy rule and the authority captured when it was granted.
73
+ * @category models
74
+ * @since 1.0.0-rc.1
75
+ */
76
+ export interface PolicyRule {
77
+ readonly rule: Rule
78
+ readonly ceiling: Pick<CapabilitySet, "groups">
79
+ }
80
+
81
+ /** Atomic ordered policy snapshot, bounded by the calling fiber.
82
+ * @category models
83
+ * @since 1.0.0-rc.1
84
+ */
85
+ export interface Policy {
86
+ readonly groups: ReadonlyArray<ReadonlyArray<PolicyRule>>
87
+ readonly ceiling: CapabilitySet
88
+ }
89
+
90
+ /** A conservative snapshot for services that expose no durable authority.
91
+ * @category constructors
92
+ * @since 1.0.0-rc.1
93
+ */
94
+ export const emptyPolicy: Effect.Effect<Policy> = current.pipe(
95
+ Effect.map((ceiling) => Object.freeze({ groups: Object.freeze([]), ceiling }))
96
+ )
97
+
98
+ /** Operations exposed by the grant store.
99
+ * @category models
100
+ * @since 1.0.0-rc.0
101
+ */
102
+ export interface Service {
103
+ readonly policy: Effect.Effect<Policy, GrantStoreError>
104
+ readonly check: (
105
+ capability: Capability,
106
+ meta?: Record<string, unknown>
107
+ ) => Effect.Effect<void, PermissionRequired | PermissionDenied | GrantStoreError>
108
+ readonly reply: (
109
+ requestId: string,
110
+ resolution: Resolution,
111
+ pattern?: CapabilityPattern
112
+ ) => Effect.Effect<void, GrantStoreError>
113
+ readonly list: Effect.Effect<ReadonlyArray<PendingRequest>>
114
+ readonly grantEnvelope: (options: EnvelopeGrantOptions) => Effect.Effect<void, GrantStoreError>
115
+ }
116
+
117
+ /**
118
+ * Service key for permission decisions and attended grant requests.
119
+ *
120
+ * @category services
121
+ * @since 1.0.0-rc.0
122
+ */
123
+ export class GrantStore extends Context.Service<GrantStore, Service>()("@smthrs/kernel/GrantStore") {}
124
+
125
+ /**
126
+ * A hook that durably records a grant decision before it becomes active.
127
+ *
128
+ * @category models
129
+ * @since 1.0.0-rc.0
130
+ */
131
+ export type Persist = (event: GrantEvent) => Effect.Effect<void, GrantStoreError>
132
+
133
+ /**
134
+ * Configuration for an in-memory grant store.
135
+ *
136
+ * A nested `rules` value is also accepted by the journal adapter: its first
137
+ * ruleset is configured policy and subsequent rulesets are replayed remembered
138
+ * grants.
139
+ *
140
+ * @category models
141
+ * @since 1.0.0-rc.0
142
+ */
143
+ export interface MakeOptions {
144
+ readonly attended?: boolean | undefined
145
+ readonly rules?: ReadonlyArray<Rule> | ReadonlyArray<ReadonlyArray<Rule>> | undefined
146
+ /** Bare rules use the constructor ceiling; captured groups intersect it. */
147
+ readonly runRules?:
148
+ | ReadonlyArray<
149
+ Rule | {
150
+ readonly rule: Rule
151
+ readonly ceiling: ReadonlyArray<ReadonlyArray<CapabilityPattern>>
152
+ }
153
+ >
154
+ | undefined
155
+ readonly envelope?: EnvelopeGrantOptions | undefined
156
+ /**
157
+ * {@link envelopeSignature} values of envelopes that are already durable —
158
+ * typically replayed from a journal. A construction or runtime envelope
159
+ * matching a seeded signature still activates its rules but is not
160
+ * persisted again.
161
+ */
162
+ readonly envelopeSignatures?: ReadonlyArray<string> | undefined
163
+ readonly runId?: string | undefined
164
+ readonly planDigest?: string | undefined
165
+ readonly persist?: Persist | undefined
166
+ }
167
+
168
+ interface PendingEntry extends PendingRequest {
169
+ readonly ceiling: CapabilitySet
170
+ readonly deferred: Deferred.Deferred<void, PermissionDenied>
171
+ }
172
+
173
+ /**
174
+ * Maximum policy rules retained by one store.
175
+ * @category limits
176
+ * @since 1.0.0-rc.0
177
+ */
178
+ export const maximumRules = 1_024
179
+ /**
180
+ * Maximum predicates in one approval envelope.
181
+ * @category limits
182
+ * @since 1.0.0-rc.0
183
+ */
184
+ export const maximumEnvelopePatterns = 256
185
+ /**
186
+ * Maximum permission requests parked at once.
187
+ * @category limits
188
+ * @since 1.0.0-rc.0
189
+ */
190
+ export const maximumPendingRequests = 1_024
191
+ /**
192
+ * Maximum nesting depth of permission metadata.
193
+ * @category limits
194
+ * @since 1.0.0-rc.0
195
+ */
196
+ export const maximumMetadataDepth = 16
197
+ /**
198
+ * Maximum members across one permission metadata graph.
199
+ * @category limits
200
+ * @since 1.0.0-rc.0
201
+ */
202
+ export const maximumMetadataMembers = 1_024
203
+ /**
204
+ * Maximum encoded bytes retained for one metadata value.
205
+ * @category limits
206
+ * @since 1.0.0-rc.0
207
+ */
208
+ export const maximumMetadataBytes = 64 * 1024
209
+ /**
210
+ * Maximum encoded bytes persisted for one grant event.
211
+ * @category limits
212
+ * @since 1.0.0-rc.0
213
+ */
214
+ export const maximumEventBytes = 256 * 1024
215
+ /**
216
+ * Maximum milliseconds one grant journal write may take before its reply or
217
+ * envelope admission fails with `journal_failed`. Journal writes never hold
218
+ * the store's mutation permit, so a slow write stalls only the admission that
219
+ * issued it.
220
+ * @category limits
221
+ * @since 1.0.0-rc.0
222
+ */
223
+ export const maximumPersistMillis = 30_000
224
+ /**
225
+ * Maximum length of a run, plan, request, or signature identity.
226
+ * @category limits
227
+ * @since 1.0.0-rc.0
228
+ */
229
+ export const maximumIdentityLength = 4_096
230
+ /**
231
+ * Maximum capability resource length enforced by the capability vocabulary.
232
+ * @category limits
233
+ * @since 1.0.0-rc.0
234
+ */
235
+ export const maximumCapabilityResourceLength = maxResourceLength
236
+
237
+ const encoder = new TextEncoder()
238
+
239
+ const isWellFormed = (value: string): boolean => {
240
+ for (let index = 0; index < value.length; index += 1) {
241
+ const unit = value.charCodeAt(index)
242
+ if (unit >= 0xd800 && unit <= 0xdbff) {
243
+ const next = value.charCodeAt(index + 1)
244
+ if (!(next >= 0xdc00 && next <= 0xdfff)) return false
245
+ index += 1
246
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) {
247
+ return false
248
+ }
249
+ }
250
+ return true
251
+ }
252
+
253
+ const invalid = (message: string): GrantStoreError => new GrantStoreError({ code: "invalid_resolution", message })
254
+
255
+ const immutableFields = <A extends object, K extends keyof A>(
256
+ value: A,
257
+ fields: ReadonlyArray<K>
258
+ ): A => {
259
+ for (const field of fields) {
260
+ Object.defineProperty(value, field, {
261
+ value: value[field],
262
+ enumerable: true,
263
+ writable: false,
264
+ configurable: false
265
+ })
266
+ }
267
+ return value
268
+ }
269
+
270
+ const snapshotCapability = (capability: Capability): Capability =>
271
+ immutableFields(
272
+ new CapabilityValue({ action: capability.action, resource: capability.resource }),
273
+ ["action", "resource"]
274
+ )
275
+
276
+ const snapshotPattern = (pattern: CapabilityPattern): CapabilityPattern =>
277
+ immutableFields(
278
+ new CapabilityPattern({ action: pattern.action, resource: pattern.resource }),
279
+ ["action", "resource"]
280
+ )
281
+
282
+ const snapshotRule = (rule: Rule): Rule =>
283
+ immutableFields(
284
+ new Rule({ effect: rule.effect, pattern: snapshotPattern(rule.pattern) }),
285
+ ["effect", "pattern"]
286
+ )
287
+
288
+ const snapshotFailure = (what: string, cause: unknown): GrantStoreError =>
289
+ cause instanceof GrantStoreError ? cause : invalid(`${what} is invalid`)
290
+
291
+ const attemptSnapshot = <A>(what: string, evaluate: () => A): Effect.Effect<A, GrantStoreError> =>
292
+ Effect.try({
293
+ try: evaluate,
294
+ catch: (cause) => snapshotFailure(what, cause)
295
+ })
296
+
297
+ type Json = null | boolean | number | string | ReadonlyArray<Json> | { readonly [key: string]: Json }
298
+
299
+ const metadataSnapshot = (input: Readonly<Record<string, unknown>>): Readonly<Record<string, Json>> => {
300
+ let members = 0
301
+ const active = new WeakSet<object>()
302
+
303
+ const visit = (value: unknown, depth: number): Json => {
304
+ if (depth > maximumMetadataDepth) throw invalid(`metadata exceeds depth ${maximumMetadataDepth}`)
305
+ if (value === null || typeof value === "boolean") return value
306
+ if (typeof value === "number") {
307
+ if (!Number.isFinite(value)) throw invalid("metadata contains a non-finite number")
308
+ return value
309
+ }
310
+ if (typeof value === "string") {
311
+ if (!isWellFormed(value)) throw invalid("metadata contains ill-formed text")
312
+ if (value.length > maximumMetadataBytes) throw invalid(`metadata exceeds ${maximumMetadataBytes} bytes`)
313
+ return value
314
+ }
315
+ if (typeof value !== "object" || value === null) throw invalid("metadata must be JSON data")
316
+ if (active.has(value)) throw invalid("metadata must not contain cycles")
317
+ active.add(value)
318
+ try {
319
+ if (Array.isArray(value)) {
320
+ const descriptors = Object.getOwnPropertyDescriptors(value)
321
+ const snapshot: Array<Json> = []
322
+ for (let index = 0; index < value.length; index += 1) {
323
+ members += 1
324
+ if (members > maximumMetadataMembers) {
325
+ throw invalid(`metadata exceeds ${maximumMetadataMembers} members`)
326
+ }
327
+ const descriptor = descriptors[String(index)]
328
+ if (descriptor === undefined || !("value" in descriptor)) {
329
+ throw invalid("metadata arrays must be dense data")
330
+ }
331
+ snapshot.push(visit(descriptor.value, depth + 1))
332
+ }
333
+ return Object.freeze(snapshot)
334
+ }
335
+
336
+ const prototype = Object.getPrototypeOf(value)
337
+ if (prototype !== Object.prototype && prototype !== null) {
338
+ throw invalid("metadata objects must be plain records")
339
+ }
340
+ const snapshot: Record<string, Json> = Object.create(null)
341
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
342
+ if (!descriptor.enumerable) continue
343
+ members += 1
344
+ if (members > maximumMetadataMembers) {
345
+ throw invalid(`metadata exceeds ${maximumMetadataMembers} members`)
346
+ }
347
+ if (!isWellFormed(key) || !("value" in descriptor)) {
348
+ throw invalid("metadata members must be well-formed data properties")
349
+ }
350
+ if (descriptor.value !== undefined) snapshot[key] = visit(descriptor.value, depth + 1)
351
+ }
352
+ return Object.freeze(snapshot)
353
+ } finally {
354
+ active.delete(value)
355
+ }
356
+ }
357
+
358
+ const snapshot = visit(input, 0)
359
+ if (Array.isArray(snapshot) || snapshot === null || typeof snapshot !== "object") {
360
+ throw invalid("metadata must be a record")
361
+ }
362
+ const encoded = JSON.stringify(snapshot)
363
+ if (encoder.encode(encoded).byteLength > maximumMetadataBytes) {
364
+ throw invalid(`metadata exceeds ${maximumMetadataBytes} bytes`)
365
+ }
366
+ return snapshot as Readonly<Record<string, Json>>
367
+ }
368
+
369
+ const validIdentity = (value: unknown): value is string | undefined =>
370
+ value === undefined ||
371
+ (typeof value === "string" &&
372
+ value.length > 0 &&
373
+ value.length <= maximumIdentityLength &&
374
+ isWellFormed(value) &&
375
+ !value.includes("\0"))
376
+
377
+ const prepareEnvelope = (
378
+ input: EnvelopeGrantOptions,
379
+ expectedPlanDigest: string | undefined,
380
+ workspaceRoot: string
381
+ ): Effect.Effect<{
382
+ readonly planDigest: string
383
+ readonly scope: "run" | "remembered"
384
+ readonly patterns: ReadonlyArray<CapabilityPattern>
385
+ }, GrantStoreError> =>
386
+ attemptSnapshot("grant envelope", () => {
387
+ if (!Array.isArray(input.patterns)) throw invalid("patterns must be an array")
388
+ if (input.patterns.length > maximumEnvelopePatterns) {
389
+ throw invalid(`patterns exceed ${maximumEnvelopePatterns} entries`)
390
+ }
391
+ const patterns = canonicalEnvelopePatterns(input.patterns)
392
+ // An empty envelope carries no authority. It is deliberately a no-op even
393
+ // when replayed without the plan identity that authorized a non-empty one.
394
+ if (patterns.length === 0) {
395
+ return { planDigest: expectedPlanDigest ?? "", scope: "run" as const, patterns }
396
+ }
397
+ const scope = input.scope ?? "run"
398
+ if (!isEnvelopeScope(scope)) throw invalid("scope must be run or remembered")
399
+ if (expectedPlanDigest === undefined) throw invalid("grant envelope requires a configured planDigest")
400
+ if (!validIdentity(input.planDigest)) throw invalid("planDigest is empty, malformed, or too long")
401
+ if (input.planDigest !== expectedPlanDigest) throw invalid("planDigest mismatch")
402
+ for (let index = 0; index < patterns.length; index += 1) {
403
+ if (!isValidEnvelopePattern(patterns[index]!, workspaceRoot)) {
404
+ throw invalid(`patterns[${index}] is outside the workspace envelope`)
405
+ }
406
+ }
407
+ return { planDigest: input.planDigest, scope, patterns }
408
+ })
409
+
410
+ /**
411
+ * Records the displayed capability for once/deny audit events, which are never
412
+ * installed or replayed as authority.
413
+ */
414
+ const exactPattern = (capability: Capability): CapabilityPattern =>
415
+ new CapabilityPattern({
416
+ action: capability.action,
417
+ resource: capability.resource
418
+ })
419
+
420
+ const hasResourceWildcard = (resource: string): boolean => resource.includes("*") || resource.includes("?")
421
+
422
+ const isResolution = (value: string): value is Resolution =>
423
+ value === "once" || value === "run" || value === "remembered" || value === "deny"
424
+
425
+ const isEnvelopeScope = (value: string): value is "run" | "remembered" => value === "run" || value === "remembered"
426
+
427
+ /**
428
+ * Canonicalizes an envelope pattern list: duplicate predicates collapse to
429
+ * their first occurrence and the survivors sort by their formatted identity.
430
+ *
431
+ * An envelope is a *set* of predicates, so two envelopes listing the same
432
+ * predicates in a different order or multiplicity are the same approval. Every
433
+ * envelope is canonicalized before it is persisted or compared, which makes
434
+ * envelope idempotency structural rather than dependent on caller discipline.
435
+ *
436
+ * The ordering is the code-unit sort of `format(pattern)` — the one renderer
437
+ * for capability identity. RFC 8785 canonical JSON (`@smthrs/canonical`)
438
+ * was considered and does not fit: it canonicalizes object keys but preserves
439
+ * array order as semantic, and the ordering that matters here is exactly the
440
+ * pattern-array order.
441
+ *
442
+ * @category validation
443
+ * @since 1.0.0-rc.0
444
+ */
445
+ export const canonicalEnvelopePatterns = (
446
+ patterns: ReadonlyArray<CapabilityPattern>
447
+ ): ReadonlyArray<CapabilityPattern> => {
448
+ const byIdentity = new Map<string, CapabilityPattern>()
449
+ for (const input of patterns) {
450
+ const pattern = snapshotPattern(input)
451
+ const identity = format(pattern)
452
+ if (!byIdentity.has(identity)) {
453
+ byIdentity.set(identity, pattern)
454
+ }
455
+ }
456
+ return Object.freeze([...byIdentity.keys()].sort().map((identity) => byIdentity.get(identity)!))
457
+ }
458
+
459
+ const envelopeEncoding = (
460
+ planDigest: string,
461
+ scope: "run" | "remembered",
462
+ patterns: ReadonlyArray<CapabilityPattern>
463
+ ): string => JSON.stringify({ planDigest, scope, patterns: canonicalEnvelopePatterns(patterns).map(format) })
464
+
465
+ /**
466
+ * Computes the canonical identity of an envelope approval.
467
+ *
468
+ * Two envelopes with the same plan digest, scope, and predicate set produce
469
+ * the same signature regardless of pattern order or repetition. The signature
470
+ * is how the store, and journal replay above it, recognise an envelope that
471
+ * is already durable. It is `sha256:` followed by 64 lowercase hex digits.
472
+ *
473
+ * @category validation
474
+ * @since 1.0.0-rc.0
475
+ */
476
+ export const envelopeSignature = (
477
+ planDigest: string,
478
+ scope: "run" | "remembered",
479
+ patterns: ReadonlyArray<CapabilityPattern>
480
+ ): string => `sha256:${digestSync(envelopeEncoding(planDigest, scope, patterns))}`
481
+
482
+ const decodeLegacySignature = Schema.decodeUnknownResult(
483
+ Schema.fromJsonString(Schema.Struct({
484
+ planDigest: Schema.String,
485
+ scope: Schema.Literals(["run", "remembered"]),
486
+ patterns: Schema.Array(Schema.String).check(Schema.isMaxLength(maximumEnvelopePatterns))
487
+ })),
488
+ { onExcessProperty: "error" }
489
+ )
490
+
491
+ // v1 events contain predicates, not signatures, and replay hashes them afresh.
492
+ // Also accept canonical full-JSON identities supplied by older seed callers.
493
+ const normalizeEnvelopeSignature = (value: string): string => {
494
+ if (!value.startsWith("{")) return value
495
+ if (value.length > maximumEventBytes || encoder.encode(value).byteLength > maximumEventBytes) return value
496
+ const decoded = decodeLegacySignature(value)
497
+ if (decoded._tag === "Failure" || !validIdentity(decoded.success.planDigest)) return value
498
+ const { planDigest, scope, patterns } = decoded.success
499
+ const parsed = Option.all(patterns.map(parsePattern))
500
+ if (Option.isNone(parsed) || envelopeEncoding(planDigest, scope, parsed.value) !== value) return value
501
+ return envelopeSignature(planDigest, scope, parsed.value)
502
+ }
503
+
504
+ /**
505
+ * Checks that a request-scoped grant cannot authorize a different action or a
506
+ * more dangerous effect tier than the request displayed to the user. A
507
+ * wildcard-bearing pattern identical to the resource is ambiguous because the
508
+ * grammar has no escape, so its wildcard reading would silently widen access.
509
+ *
510
+ * @category validation
511
+ * @since 1.0.0-rc.0
512
+ */
513
+ export const isValidGrantPattern = (
514
+ pattern: CapabilityPattern,
515
+ capability: Capability,
516
+ tier: EffectTier,
517
+ workspaceRoot: string
518
+ ): boolean => {
519
+ if (pattern.action !== capability.action || !matches(pattern, capability)) {
520
+ return false
521
+ }
522
+ if (hasResourceWildcard(pattern.resource) && pattern.resource === capability.resource) {
523
+ return false
524
+ }
525
+ if (tierOf(capability, { workspaceRoot }) !== tier) {
526
+ return false
527
+ }
528
+ if (capability.action !== "fs:write") {
529
+ return true
530
+ }
531
+ if (tier === "irreversible") {
532
+ return !hasResourceWildcard(pattern.resource)
533
+ }
534
+ const workspacePattern = new CapabilityPattern({
535
+ action: "fs:write",
536
+ resource: `${workspaceRoot.replace(/[\\/]+$/, "")}/**`
537
+ })
538
+ return pattern.resource === workspaceRoot || subsumes(workspacePattern, pattern)
539
+ }
540
+
541
+ /**
542
+ * Checks that a bulk envelope pattern preserves exact action and filesystem
543
+ * effect-tier boundaries.
544
+ *
545
+ * @category validation
546
+ * @since 1.0.0-rc.0
547
+ */
548
+ export const isValidEnvelopePattern = (
549
+ pattern: CapabilityPattern,
550
+ workspaceRoot: string
551
+ ): boolean => {
552
+ if (pattern.action.includes("*")) {
553
+ return false
554
+ }
555
+ if (pattern.action !== "fs:write" || !hasResourceWildcard(pattern.resource)) {
556
+ return true
557
+ }
558
+ const workspacePattern = new CapabilityPattern({
559
+ action: "fs:write",
560
+ resource: `${workspaceRoot.replace(/[\\/]+$/, "")}/**`
561
+ })
562
+ return subsumes(workspacePattern, pattern)
563
+ }
564
+
565
+ const normalizeRules = (
566
+ rules: MakeOptions["rules"]
567
+ ): {
568
+ readonly configured: ReadonlyArray<Rule>
569
+ readonly remembered: ReadonlyArray<Rule>
570
+ } => {
571
+ if (rules === undefined || rules.length === 0 || rules[0] === undefined) {
572
+ return { configured: [], remembered: [] }
573
+ }
574
+ if (rules.length > maximumRules) throw invalid(`rules exceed ${maximumRules} entries`)
575
+ if (Array.isArray(rules[0])) {
576
+ const rulesets = rules as ReadonlyArray<ReadonlyArray<Rule>>
577
+ let count = 0
578
+ for (const ruleset of rulesets) {
579
+ count += ruleset.length
580
+ if (count > maximumRules) throw invalid(`rules exceed ${maximumRules} entries`)
581
+ }
582
+ return {
583
+ configured: Object.freeze(rulesets[0]!.map(snapshotRule)),
584
+ remembered: Object.freeze(rulesets.slice(1).flatMap((ruleset) => ruleset.map(snapshotRule)))
585
+ }
586
+ }
587
+ return {
588
+ configured: Object.freeze((rules as ReadonlyArray<Rule>).map(snapshotRule)),
589
+ remembered: []
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Builds an in-memory grant store.
595
+ *
596
+ * Attended stores suspend an asking fiber on a request-local `Deferred`.
597
+ * Unattended stores fail immediately with `PermissionRequired`. The returned
598
+ * service is scoped: closing its scope rejects every waiter and clears the
599
+ * pending map.
600
+ *
601
+ * @category constructors
602
+ * @since 1.0.0-rc.0
603
+ */
604
+ export const make = (
605
+ options: MakeOptions = {}
606
+ ): Effect.Effect<Service, GrantStoreError, Scope.Scope | Workspace> =>
607
+ Effect.gen(function*() {
608
+ const attended = options.attended ?? true
609
+ const runId = options.runId
610
+ const planDigest = options.planDigest
611
+ if (!validIdentity(runId)) {
612
+ return yield* Effect.fail(invalid("runId is empty, malformed, or too long"))
613
+ }
614
+ if (!validIdentity(planDigest)) {
615
+ return yield* Effect.fail(invalid("planDigest is empty, malformed, or too long"))
616
+ }
617
+ const workspace = yield* Workspace
618
+ const initialCeiling = yield* current
619
+ const workspaceRoot = workspace.root
620
+ const initial = yield* attemptSnapshot("rules", () => normalizeRules(options.rules))
621
+ const initialRunRules = yield* attemptSnapshot("runRules", () => {
622
+ const values = options.runRules ?? []
623
+ if (values.length > maximumRules) throw invalid(`runRules exceed ${maximumRules} entries`)
624
+ return values.map((value) =>
625
+ "rule" in value
626
+ ? {
627
+ rule: snapshotRule(value.rule),
628
+ ceiling: value.ceiling.reduce(
629
+ (ceiling, group) => intersect(ceiling, fromPatterns(group)),
630
+ initialCeiling
631
+ )
632
+ }
633
+ : { rule: snapshotRule(value), ceiling: initialCeiling }
634
+ )
635
+ })
636
+ if (initial.configured.length + initial.remembered.length + initialRunRules.length > maximumRules) {
637
+ return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`))
638
+ }
639
+ const configuredRules = initial.configured
640
+ const envelopeRules: Array<Rule> = []
641
+ const runRules = initialRunRules
642
+ const rememberedRules = [...initial.remembered]
643
+ const pending = new Map<string, PendingEntry>()
644
+ const persist = options.persist ?? (() => Effect.void)
645
+ const signatures = yield* attemptSnapshot("envelopeSignatures", () => {
646
+ const values = options.envelopeSignatures ?? []
647
+ if (!Array.isArray(values) || values.length > maximumRules) {
648
+ throw invalid(`envelopeSignatures exceed ${maximumRules} entries`)
649
+ }
650
+ return values.map((value, index) => {
651
+ const signature = typeof value === "string" ? normalizeEnvelopeSignature(value) : value
652
+ if (typeof signature !== "string" || !validIdentity(signature)) {
653
+ throw invalid(`envelopeSignatures[${index}] is malformed or too long`)
654
+ }
655
+ return signature
656
+ })
657
+ })
658
+ const grantedEnvelopes = new Set<string>(signatures)
659
+ let nextRequestId = 1
660
+ let closed = false
661
+ const mutation = yield* Semaphore.make(1)
662
+
663
+ const persistEvent = (event: GrantEvent): Effect.Effect<void, GrantStoreError> => {
664
+ // Every event is constructed below exclusively from validated primitive
665
+ // identities and immutable capability snapshots, so encoding cannot
666
+ // observe caller objects or invoke a caller-defined `toJSON` hook.
667
+ const bytes = encoder.encode(JSON.stringify(event)).byteLength
668
+ return bytes > maximumEventBytes
669
+ ? Effect.fail(invalid(`grant event exceeds ${maximumEventBytes} bytes`))
670
+ : Effect.timeoutOrElse(persist(event), {
671
+ duration: maximumPersistMillis,
672
+ orElse: () =>
673
+ Effect.fail(
674
+ new GrantStoreError({
675
+ code: "journal_failed",
676
+ message: `grant journal write exceeded ${maximumPersistMillis}ms`
677
+ })
678
+ )
679
+ })
680
+ }
681
+
682
+ const invalidRemembered = rememberedRules.findIndex((rule) =>
683
+ rule.effect === "allow" && !isValidEnvelopePattern(rule.pattern, workspaceRoot)
684
+ )
685
+ if (invalidRemembered !== -1) {
686
+ return yield* Effect.fail(invalid(`rememberedRules[${invalidRemembered}] is outside the workspace envelope`))
687
+ }
688
+ const invalidRun = runRules.findIndex(({ rule }) =>
689
+ rule.effect === "allow" && !isValidEnvelopePattern(rule.pattern, workspaceRoot)
690
+ )
691
+ if (invalidRun !== -1) {
692
+ return yield* Effect.fail(invalid(`runRules[${invalidRun}] is outside the workspace envelope`))
693
+ }
694
+
695
+ // Construction envelopes validate capacity before persistence or
696
+ // activation. A seeded construction envelope may already have rules
697
+ // installed by replay. Runtime admission is two-phase in grantEnvelope.
698
+ const admitEnvelope = (
699
+ envelope: Effect.Success<ReturnType<typeof prepareEnvelope>>
700
+ ): Effect.Effect<boolean, GrantStoreError> =>
701
+ Effect.gen(function*() {
702
+ const { patterns, scope } = envelope
703
+ if (patterns.length === 0) return false
704
+ const signature = envelopeSignature(envelope.planDigest, scope, patterns)
705
+ const durable = grantedEnvelopes.has(signature)
706
+ const destination = scope === "remembered" ? rememberedRules : envelopeRules
707
+ const replayed = scope === "remembered"
708
+ ? rememberedRules
709
+ : runRules.filter(({ ceiling }) => ceiling === initialCeiling).map(({ rule }) => rule)
710
+ const existing = new Set<string>()
711
+ for (const rule of replayed) {
712
+ // Supplied rules can mask earlier grants. Only the trailing allow
713
+ // rules (including journal replay) can replace a fresh activation.
714
+ if (rule.effect === "allow") existing.add(format(rule.pattern))
715
+ else existing.clear()
716
+ }
717
+ const incoming = durable ? patterns.filter((pattern) => !existing.has(format(pattern))) : patterns
718
+ if (
719
+ configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
720
+ incoming.length > maximumRules
721
+ ) {
722
+ return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`))
723
+ }
724
+ if (!durable && grantedEnvelopes.size >= maximumRules) {
725
+ return yield* Effect.fail(invalid(`grant envelopes exceed ${maximumRules} entries`))
726
+ }
727
+ if (!durable) {
728
+ yield* persistEvent(
729
+ new EnvelopeGrant({
730
+ eventType: "flows.kernel.grant.envelope.v1",
731
+ runId: runId ?? "",
732
+ planDigest: envelope.planDigest,
733
+ patterns,
734
+ scope
735
+ })
736
+ )
737
+ grantedEnvelopes.add(signature)
738
+ }
739
+ for (const pattern of incoming) {
740
+ destination.push(snapshotRule(new Rule({ effect: "allow", pattern })))
741
+ }
742
+ return true
743
+ })
744
+
745
+ if (options.envelope !== undefined) {
746
+ const envelope = yield* prepareEnvelope(options.envelope, planDigest, workspaceRoot)
747
+ yield* admitEnvelope(envelope)
748
+ }
749
+
750
+ const rulesets = (capability: Capability): ReadonlyArray<ReadonlyArray<Rule>> => [
751
+ configuredRules,
752
+ envelopeRules,
753
+ runRules
754
+ .filter(({ ceiling }) => allows(ceiling, capability))
755
+ .map(({ rule }) => rule),
756
+ rememberedRules
757
+ ]
758
+
759
+ yield* Effect.addFinalizer(() =>
760
+ mutation.withPermit(
761
+ Effect.gen(function*() {
762
+ closed = true
763
+ const entries = [...pending.values()]
764
+ pending.clear()
765
+ yield* Effect.forEach(
766
+ entries,
767
+ (entry) =>
768
+ Deferred.fail(
769
+ entry.deferred,
770
+ permissionDenied(entry.capability, "grant store closed")
771
+ ),
772
+ { discard: true }
773
+ )
774
+ })
775
+ )
776
+ )
777
+
778
+ const allocateRequest = (
779
+ capability: Capability,
780
+ tier: EffectTier,
781
+ meta: Record<string, unknown>,
782
+ ceiling: CapabilitySet
783
+ ): Effect.Effect<PendingEntry, GrantStoreError> =>
784
+ Effect.gen(function*() {
785
+ if (pending.size >= maximumPendingRequests) {
786
+ return yield* Effect.fail(invalid(`pending requests exceed ${maximumPendingRequests} entries`))
787
+ }
788
+ const requestId = `permission-${nextRequestId++}`
789
+ const deferred = yield* Deferred.make<void, PermissionDenied>()
790
+ const entry: PendingEntry = {
791
+ requestId,
792
+ capability,
793
+ tier,
794
+ meta,
795
+ ceiling,
796
+ deferred
797
+ }
798
+ pending.set(requestId, entry)
799
+ return entry
800
+ })
801
+
802
+ const nextUnattendedRequestId = Effect.sync(() => `permission-${nextRequestId++}`)
803
+
804
+ const check: Service["check"] = Effect.fn("GrantStore.check")((capability, meta = {}) =>
805
+ Effect.uninterruptibleMask((restore) =>
806
+ Effect.gen(function*() {
807
+ const request = yield* attemptSnapshot("permission request", () => ({
808
+ capability: snapshotCapability(capability),
809
+ meta: metadataSnapshot(meta)
810
+ }))
811
+ // The permit guards only in-memory state, so acquiring it is
812
+ // interruptible: a cancelled caller is released instead of queueing
813
+ // uninterruptibly. The critical section itself stays uninterruptible.
814
+ const entry = yield* restore(mutation.take(1)).pipe(
815
+ Effect.andThen(
816
+ Effect.gen(function*() {
817
+ if (closed) {
818
+ return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }))
819
+ }
820
+ const ceiling = yield* current
821
+ if (!allows(ceiling, request.capability)) {
822
+ return yield* Effect.fail(permissionDenied(request.capability, "outside capability ceiling"))
823
+ }
824
+
825
+ const decision = evaluate(rulesets(request.capability), request.capability)
826
+ if (decision === "allow") {
827
+ return undefined
828
+ }
829
+ if (decision === "deny") {
830
+ return yield* Effect.fail(permissionDenied(request.capability, "denied by permission policy"))
831
+ }
832
+
833
+ const tier = tierOf(request.capability, { workspaceRoot })
834
+ if (!attended) {
835
+ const requestId = yield* nextUnattendedRequestId
836
+ return yield* Effect.fail(
837
+ permissionRequired({
838
+ requestId,
839
+ runId,
840
+ capability: request.capability,
841
+ tier,
842
+ meta: request.meta
843
+ })
844
+ )
845
+ }
846
+
847
+ return yield* allocateRequest(
848
+ request.capability,
849
+ tier,
850
+ request.meta,
851
+ ceiling
852
+ )
853
+ }).pipe(Effect.ensuring(mutation.release(1)))
854
+ )
855
+ )
856
+ if (entry === undefined) {
857
+ return
858
+ }
859
+ return yield* restore(Deferred.await(entry.deferred)).pipe(
860
+ Effect.ensuring(
861
+ mutation.withPermit(
862
+ Effect.sync(() => {
863
+ pending.delete(entry.requestId)
864
+ })
865
+ )
866
+ )
867
+ )
868
+ })
869
+ )
870
+ )
871
+
872
+ const resolveCovered: Effect.Effect<void> = Effect.suspend(() =>
873
+ Effect.forEach(
874
+ [...pending.entries()],
875
+ ([requestId, entry]) => {
876
+ if (
877
+ !allows(entry.ceiling, entry.capability) ||
878
+ evaluate(rulesets(entry.capability), entry.capability) !== "allow"
879
+ ) {
880
+ return Effect.void
881
+ }
882
+ return Deferred.succeed(entry.deferred, undefined).pipe(
883
+ Effect.andThen(
884
+ Effect.sync(() => {
885
+ pending.delete(requestId)
886
+ })
887
+ )
888
+ )
889
+ },
890
+ { discard: true }
891
+ ).pipe(Effect.asVoid)
892
+ )
893
+
894
+ // Requests and envelopes with a journal write in flight. The mutation
895
+ // permit guards only in-memory state: a decision is planned under the
896
+ // permit, persisted without it, then activated under it again.
897
+ const resolving = new Set<string>()
898
+ const granting = new Map<string, Deferred.Deferred<void, GrantStoreError>>()
899
+ // Planned rules occupy capacity until their write fails or they activate.
900
+ // Replies and envelopes share this count, guarded by the mutation permit.
901
+ let reservedRules = 0
902
+
903
+ type PlannedReply =
904
+ | {
905
+ readonly resolution: "once" | "deny"
906
+ readonly entry: PendingEntry
907
+ readonly event: GrantEvent
908
+ }
909
+ | {
910
+ readonly resolution: "run" | "remembered"
911
+ readonly entry: PendingEntry
912
+ readonly rule: Rule
913
+ readonly event: GrantEvent
914
+ }
915
+
916
+ const planReply = (
917
+ requestId: string,
918
+ resolution: Resolution,
919
+ suppliedPattern: CapabilityPattern | undefined
920
+ ): Effect.Effect<PlannedReply, GrantStoreError> =>
921
+ Effect.gen(function*() {
922
+ if (closed) {
923
+ return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }))
924
+ }
925
+ if (!isResolution(resolution)) {
926
+ // A runtime-invalid resolution must fail the reply, not fall
927
+ // through the switch below: silently succeeding would strand
928
+ // the request's waiter on its Deferred forever. The request
929
+ // stays pending so the caller can still answer it.
930
+ return yield* Effect.fail(
931
+ new GrantStoreError({
932
+ code: "invalid_resolution",
933
+ message: "unknown grant resolution"
934
+ })
935
+ )
936
+ }
937
+ const entry = pending.get(requestId)
938
+ // A request whose decision is already being persisted is decided:
939
+ // the losing reply fails fast instead of queueing behind the journal.
940
+ if (entry === undefined || resolving.has(requestId)) {
941
+ return yield* Effect.fail(new GrantStoreError({ code: "request_not_found" }))
942
+ }
943
+ const pattern = yield* attemptSnapshot("grant pattern", () => {
944
+ if (resolution === "run" || resolution === "remembered") {
945
+ if (suppliedPattern !== undefined) return snapshotPattern(suppliedPattern)
946
+ const derived = patternFromCapability(entry.capability)
947
+ if (Option.isNone(derived)) {
948
+ throw invalid(
949
+ "the requested resource contains glob metacharacters; supply an explicit grant pattern or resolve once"
950
+ )
951
+ }
952
+ return snapshotPattern(derived.value)
953
+ }
954
+ return snapshotPattern(exactPattern(entry.capability))
955
+ })
956
+
957
+ switch (resolution) {
958
+ case "once": {
959
+ resolving.add(requestId)
960
+ return {
961
+ resolution,
962
+ entry,
963
+ event: new OnceGrant({
964
+ eventType: "flows.kernel.grant.once.v1",
965
+ requestId,
966
+ runId: runId ?? "",
967
+ ...(planDigest === undefined ? {} : { planDigest }),
968
+ capability: entry.capability,
969
+ pattern,
970
+ scope: "once",
971
+ tier: entry.tier
972
+ })
973
+ }
974
+ }
975
+ case "run": {
976
+ if (planDigest === undefined) {
977
+ return yield* Effect.fail(
978
+ new GrantStoreError({
979
+ code: "invalid_resolution",
980
+ message: "run grants require a plan digest"
981
+ })
982
+ )
983
+ }
984
+ if (!isValidGrantPattern(pattern, entry.capability, entry.tier, workspaceRoot)) {
985
+ return yield* Effect.fail(invalid("grant pattern exceeds the requested authority"))
986
+ }
987
+ if (
988
+ configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
989
+ reservedRules >=
990
+ maximumRules
991
+ ) {
992
+ return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`))
993
+ }
994
+ resolving.add(requestId)
995
+ reservedRules += 1
996
+ return {
997
+ resolution,
998
+ entry,
999
+ rule: snapshotRule(new Rule({ effect: "allow", pattern })),
1000
+ event: new RunGrant({
1001
+ eventType: "flows.kernel.grant.run.v2",
1002
+ requestId,
1003
+ runId: runId ?? "",
1004
+ planDigest,
1005
+ capability: entry.capability,
1006
+ pattern,
1007
+ ceiling: entry.ceiling.groups,
1008
+ scope: "run",
1009
+ tier: entry.tier
1010
+ })
1011
+ }
1012
+ }
1013
+ case "remembered": {
1014
+ if (!isValidGrantPattern(pattern, entry.capability, entry.tier, workspaceRoot)) {
1015
+ return yield* Effect.fail(invalid("grant pattern exceeds the requested authority"))
1016
+ }
1017
+ if (
1018
+ configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
1019
+ reservedRules >=
1020
+ maximumRules
1021
+ ) {
1022
+ return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`))
1023
+ }
1024
+ resolving.add(requestId)
1025
+ reservedRules += 1
1026
+ return {
1027
+ resolution,
1028
+ entry,
1029
+ rule: snapshotRule(new Rule({ effect: "allow", pattern })),
1030
+ event: new RememberedGrant({
1031
+ eventType: "flows.kernel.grant.remembered.v1",
1032
+ requestId,
1033
+ runId: runId ?? "",
1034
+ ...(planDigest === undefined ? {} : { planDigest }),
1035
+ capability: entry.capability,
1036
+ pattern,
1037
+ scope: "remembered",
1038
+ tier: entry.tier
1039
+ })
1040
+ }
1041
+ }
1042
+ case "deny": {
1043
+ resolving.add(requestId)
1044
+ return {
1045
+ resolution,
1046
+ entry,
1047
+ event: new DeniedGrant({
1048
+ eventType: "flows.kernel.grant.denied.v1",
1049
+ requestId,
1050
+ runId: runId ?? "",
1051
+ ...(planDigest === undefined ? {} : { planDigest }),
1052
+ capability: entry.capability,
1053
+ pattern,
1054
+ scope: "once",
1055
+ tier: entry.tier
1056
+ })
1057
+ }
1058
+ }
1059
+ }
1060
+ })
1061
+
1062
+ const activateReply = (requestId: string, planned: PlannedReply): Effect.Effect<void, GrantStoreError> =>
1063
+ Effect.gen(function*() {
1064
+ resolving.delete(requestId)
1065
+ if (planned.resolution === "run" || planned.resolution === "remembered") reservedRules -= 1
1066
+ if (closed) {
1067
+ return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }))
1068
+ }
1069
+ const entry = pending.get(requestId)
1070
+ switch (planned.resolution) {
1071
+ case "once": {
1072
+ if (entry === undefined) {
1073
+ return yield* Effect.fail(new GrantStoreError({ code: "request_not_found" }))
1074
+ }
1075
+ yield* Deferred.succeed(entry.deferred, undefined)
1076
+ pending.delete(requestId)
1077
+ return
1078
+ }
1079
+ case "deny": {
1080
+ if (entry === undefined) {
1081
+ return yield* Effect.fail(new GrantStoreError({ code: "request_not_found" }))
1082
+ }
1083
+ yield* Deferred.fail(
1084
+ entry.deferred,
1085
+ permissionDenied(entry.capability, "permission request denied")
1086
+ )
1087
+ pending.delete(requestId)
1088
+ return
1089
+ }
1090
+ case "run": {
1091
+ // The decision is already durable, so the rule activates even
1092
+ // when the request's own waiter left while the journal wrote:
1093
+ // replay installs it on the next open and the live store must
1094
+ // not diverge from the journal. Capacity was validated before
1095
+ // persistence.
1096
+ runRules.push({ rule: planned.rule, ceiling: planned.entry.ceiling })
1097
+ yield* resolveCovered
1098
+ return
1099
+ }
1100
+ case "remembered": {
1101
+ rememberedRules.push(planned.rule)
1102
+ yield* resolveCovered
1103
+ return
1104
+ }
1105
+ }
1106
+ })
1107
+
1108
+ const reply: Service["reply"] = Effect.fn("GrantStore.reply")((
1109
+ requestId,
1110
+ resolution,
1111
+ suppliedPattern
1112
+ ) =>
1113
+ Effect.uninterruptibleMask((restore) =>
1114
+ Effect.gen(function*() {
1115
+ const planned = yield* mutation.withPermit(planReply(requestId, resolution, suppliedPattern))
1116
+ // Journal IO never holds the permit: only this reply waits on it,
1117
+ // interruptibly, so the write deadline can fire and a cancelled
1118
+ // reply is released. A failed write clears the in-flight mark so
1119
+ // the request stays retryable.
1120
+ yield* restore(persistEvent(planned.event)).pipe(
1121
+ Effect.onError(() =>
1122
+ mutation.withPermit(
1123
+ Effect.sync(() => {
1124
+ resolving.delete(requestId)
1125
+ if (planned.resolution === "run" || planned.resolution === "remembered") reservedRules -= 1
1126
+ })
1127
+ )
1128
+ )
1129
+ )
1130
+ yield* mutation.withPermit(activateReply(requestId, planned))
1131
+ })
1132
+ )
1133
+ )
1134
+
1135
+ const list: Service["list"] = Effect.fn("GrantStore.list")(() =>
1136
+ mutation.withPermit(
1137
+ Effect.sync(() =>
1138
+ // Every entry's meta is already the frozen snapshot taken at check
1139
+ // time; list copies the array and re-wraps identity without
1140
+ // re-walking or re-serializing the metadata graph.
1141
+ Object.freeze(Array.from(
1142
+ pending.values(),
1143
+ ({ requestId, capability, tier, meta }): PendingRequest =>
1144
+ Object.freeze({
1145
+ requestId,
1146
+ capability: snapshotCapability(capability),
1147
+ tier,
1148
+ meta
1149
+ })
1150
+ ))
1151
+ )
1152
+ )
1153
+ )()
1154
+
1155
+ type EnvelopeAdmission =
1156
+ | { readonly _tag: "Durable" }
1157
+ | { readonly _tag: "Wait"; readonly completion: Deferred.Deferred<void, GrantStoreError> }
1158
+ | {
1159
+ readonly _tag: "Fresh"
1160
+ readonly signature: string
1161
+ readonly scope: "run" | "remembered"
1162
+ readonly patterns: ReadonlyArray<CapabilityPattern>
1163
+ readonly completion: Deferred.Deferred<void, GrantStoreError>
1164
+ readonly event: GrantEvent
1165
+ }
1166
+
1167
+ const planEnvelope = (
1168
+ envelope: Effect.Success<ReturnType<typeof prepareEnvelope>>
1169
+ ): Effect.Effect<EnvelopeAdmission, GrantStoreError> =>
1170
+ Effect.gen(function*() {
1171
+ if (closed) {
1172
+ return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }))
1173
+ }
1174
+ const { patterns, scope } = envelope
1175
+ const signature = envelopeSignature(envelope.planDigest, scope, patterns)
1176
+ if (grantedEnvelopes.has(signature)) return { _tag: "Durable" as const }
1177
+ // A concurrent admission of the same envelope is already persisting;
1178
+ // wait for its outcome rather than writing a duplicate record.
1179
+ const inFlight = granting.get(signature)
1180
+ if (inFlight !== undefined) return { _tag: "Wait" as const, completion: inFlight }
1181
+ if (
1182
+ configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
1183
+ reservedRules + patterns.length > maximumRules
1184
+ ) {
1185
+ return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`))
1186
+ }
1187
+ if (grantedEnvelopes.size + granting.size >= maximumRules) {
1188
+ return yield* Effect.fail(invalid(`grant envelopes exceed ${maximumRules} entries`))
1189
+ }
1190
+ const completion = yield* Deferred.make<void, GrantStoreError>()
1191
+ granting.set(signature, completion)
1192
+ reservedRules += patterns.length
1193
+ return {
1194
+ _tag: "Fresh" as const,
1195
+ signature,
1196
+ scope,
1197
+ patterns,
1198
+ completion,
1199
+ event: new EnvelopeGrant({
1200
+ eventType: "flows.kernel.grant.envelope.v1",
1201
+ runId: runId ?? "",
1202
+ planDigest: envelope.planDigest,
1203
+ patterns,
1204
+ scope
1205
+ })
1206
+ }
1207
+ })
1208
+
1209
+ const activateEnvelope = (
1210
+ planned: Extract<EnvelopeAdmission, { readonly _tag: "Fresh" }>
1211
+ ): Effect.Effect<void, GrantStoreError> =>
1212
+ Effect.gen(function*() {
1213
+ granting.delete(planned.signature)
1214
+ reservedRules -= planned.patterns.length
1215
+ if (closed) {
1216
+ return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }))
1217
+ }
1218
+ // The decision is already durable, so its rules activate without
1219
+ // re-checking capacity: replay installs them on the next open and the
1220
+ // live store must not diverge from the journal.
1221
+ grantedEnvelopes.add(planned.signature)
1222
+ const destination = planned.scope === "remembered" ? rememberedRules : envelopeRules
1223
+ for (const pattern of planned.patterns) {
1224
+ destination.push(snapshotRule(new Rule({ effect: "allow", pattern })))
1225
+ }
1226
+ yield* resolveCovered
1227
+ })
1228
+
1229
+ const grantEnvelope: Service["grantEnvelope"] = Effect.fn("GrantStore.grantEnvelope")((input) =>
1230
+ Effect.uninterruptibleMask((restore) => {
1231
+ const admit = (
1232
+ prepared: Effect.Success<ReturnType<typeof prepareEnvelope>>
1233
+ ): Effect.Effect<void, GrantStoreError> =>
1234
+ Effect.gen(function*() {
1235
+ const planned = yield* mutation.withPermit(planEnvelope(prepared))
1236
+ switch (planned._tag) {
1237
+ case "Durable":
1238
+ return
1239
+ case "Wait": {
1240
+ // The identical envelope is mid-flight elsewhere; adopt its
1241
+ // outcome. A failed write fails here too: re-planning would
1242
+ // turn one journal outage into an unbounded loop of writes.
1243
+ // An interrupted first admission decided nothing and can be
1244
+ // retried. Check this caller's cancellation before re-planning
1245
+ // so an interrupted duplicate cannot loop under the mask.
1246
+ return yield* restore(Deferred.await(planned.completion)).pipe(
1247
+ Effect.catchCause((cause) =>
1248
+ Cause.hasInterruptsOnly(cause)
1249
+ ? restore(Effect.void).pipe(Effect.andThen(admit(prepared)))
1250
+ : Effect.failCause(cause)
1251
+ )
1252
+ )
1253
+ }
1254
+ case "Fresh": {
1255
+ // Journal IO never holds the permit: only this admission
1256
+ // waits on it, interruptibly, so the write deadline can fire.
1257
+ // Concurrent duplicates are released with the same outcome.
1258
+ yield* restore(persistEvent(planned.event)).pipe(
1259
+ Effect.onError((cause) =>
1260
+ mutation.withPermit(
1261
+ Effect.gen(function*() {
1262
+ granting.delete(planned.signature)
1263
+ reservedRules -= planned.patterns.length
1264
+ yield* Deferred.failCause(planned.completion, cause)
1265
+ })
1266
+ )
1267
+ )
1268
+ )
1269
+ yield* mutation.withPermit(activateEnvelope(planned)).pipe(
1270
+ Effect.tap(() => Deferred.succeed(planned.completion, undefined)),
1271
+ Effect.onError((cause) => Deferred.failCause(planned.completion, cause))
1272
+ )
1273
+ return
1274
+ }
1275
+ }
1276
+ })
1277
+ return Effect.gen(function*() {
1278
+ const prepared = yield* prepareEnvelope(input, planDigest, workspaceRoot)
1279
+ if (prepared.patterns.length === 0) return
1280
+ yield* admit(prepared)
1281
+ })
1282
+ })
1283
+ )
1284
+
1285
+ const policy: Service["policy"] = mutation.withPermit(Effect.gen(function*() {
1286
+ if (closed) return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }))
1287
+ const ceiling = yield* current
1288
+ const unbounded = (rules: ReadonlyArray<Rule>) =>
1289
+ rules.map((rule) =>
1290
+ Object.freeze({ rule: snapshotRule(rule), ceiling: Object.freeze({ groups: Object.freeze([]) }) })
1291
+ )
1292
+ return Object.freeze({
1293
+ ceiling,
1294
+ groups: Object.freeze([
1295
+ unbounded(configuredRules),
1296
+ unbounded(envelopeRules),
1297
+ runRules.map(({ rule, ceiling }) => Object.freeze({ rule: snapshotRule(rule), ceiling })),
1298
+ unbounded(rememberedRules)
1299
+ ].map((group) => Object.freeze(group)))
1300
+ })
1301
+ }))
1302
+ return GrantStore.of({ check, reply, list, grantEnvelope, policy })
1303
+ })
1304
+
1305
+ /**
1306
+ * Provides a scoped in-memory grant store.
1307
+ *
1308
+ * @category layers
1309
+ * @since 1.0.0-rc.0
1310
+ */
1311
+ export const layer = (
1312
+ options: MakeOptions = {}
1313
+ ): Layer.Layer<GrantStore, GrantStoreError, Workspace> => Layer.effect(GrantStore, make(options))
1314
+
1315
+ /**
1316
+ * An allow-all grant store.
1317
+ *
1318
+ * @category constructors
1319
+ * @since 1.0.0-rc.0
1320
+ */
1321
+ export const makeNoop: Service = GrantStore.of({
1322
+ policy: current.pipe(Effect.map((ceiling) =>
1323
+ Object.freeze({
1324
+ ceiling,
1325
+ groups: Object.freeze([Object.freeze((["*", "net:private"] as const).map((action) =>
1326
+ Object.freeze({
1327
+ rule: snapshotRule(new Rule({ effect: "allow", pattern: new CapabilityPattern({ action, resource: "**" }) })),
1328
+ ceiling: Object.freeze({ groups: Object.freeze([]) })
1329
+ })
1330
+ ))])
1331
+ })
1332
+ )),
1333
+ check: Effect.fn("GrantStore.check")(() => Effect.void),
1334
+ reply: Effect.fn("GrantStore.reply")(() => Effect.void),
1335
+ list: Effect.fn("GrantStore.list")(() => Effect.succeed([]))(),
1336
+ grantEnvelope: Effect.fn("GrantStore.grantEnvelope")(() => Effect.void)
1337
+ })
1338
+
1339
+ /**
1340
+ * Provides the allow-all grant store.
1341
+ *
1342
+ * @category layers
1343
+ * @since 1.0.0-rc.0
1344
+ */
1345
+ export const layerNoop: Layer.Layer<GrantStore> = Layer.succeed(GrantStore)(makeNoop)