@cat-factory/gatekeeper-worker 0.3.1

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/dist/approvals.d.ts +90 -0
  4. package/dist/approvals.d.ts.map +1 -0
  5. package/dist/approvals.js +193 -0
  6. package/dist/approvals.js.map +1 -0
  7. package/dist/capability.d.ts +61 -0
  8. package/dist/capability.d.ts.map +1 -0
  9. package/dist/capability.js +125 -0
  10. package/dist/capability.js.map +1 -0
  11. package/dist/env.d.ts +45 -0
  12. package/dist/env.d.ts.map +1 -0
  13. package/dist/env.js +90 -0
  14. package/dist/env.js.map +1 -0
  15. package/dist/errors.d.ts +22 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +30 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/gatekeeper.d.ts +78 -0
  20. package/dist/gatekeeper.d.ts.map +1 -0
  21. package/dist/gatekeeper.js +162 -0
  22. package/dist/gatekeeper.js.map +1 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/keys.d.ts +62 -0
  28. package/dist/keys.d.ts.map +1 -0
  29. package/dist/keys.js +152 -0
  30. package/dist/keys.js.map +1 -0
  31. package/dist/masking.d.ts +9 -0
  32. package/dist/masking.d.ts.map +1 -0
  33. package/dist/masking.js +43 -0
  34. package/dist/masking.js.map +1 -0
  35. package/dist/policy/compile.d.ts +88 -0
  36. package/dist/policy/compile.d.ts.map +1 -0
  37. package/dist/policy/compile.js +170 -0
  38. package/dist/policy/compile.js.map +1 -0
  39. package/dist/policy/decisions.d.ts +93 -0
  40. package/dist/policy/decisions.d.ts.map +1 -0
  41. package/dist/policy/decisions.js +659 -0
  42. package/dist/policy/decisions.js.map +1 -0
  43. package/dist/policy/index.d.ts +6 -0
  44. package/dist/policy/index.d.ts.map +1 -0
  45. package/dist/policy/index.js +17 -0
  46. package/dist/policy/index.js.map +1 -0
  47. package/dist/state.d.ts +150 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +229 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/webhook/delivery.d.ts +102 -0
  52. package/dist/webhook/delivery.d.ts.map +1 -0
  53. package/dist/webhook/delivery.js +162 -0
  54. package/dist/webhook/delivery.js.map +1 -0
  55. package/dist/webhook/signature.d.ts +17 -0
  56. package/dist/webhook/signature.d.ts.map +1 -0
  57. package/dist/webhook/signature.js +73 -0
  58. package/dist/webhook/signature.js.map +1 -0
  59. package/dist/worker.d.ts +21 -0
  60. package/dist/worker.d.ts.map +1 -0
  61. package/dist/worker.js +155 -0
  62. package/dist/worker.js.map +1 -0
  63. package/package.json +62 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Igor Savin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # @cat-factory/gatekeeper-worker
2
+
3
+ ## What it is
4
+
5
+ The Cloudflare Worker machinery behind a cat-factory **Gatekeeper**: a credential-holding front
6
+ end that lets a [Cloudflare OS](https://github.com/cloudflare/cloudflare-os) workspace (or any
7
+ agent runtime speaking Cap'n Web) drive cat-factory without an agent ever seeing a credential.
8
+ Agents hold an object-capability whose methods are exactly what policy granted; the keys stay in
9
+ Worker secrets and Durable Object storage.
10
+
11
+ It is the machinery half of the Gatekeeper family:
12
+
13
+ | Piece | What it is | How you take it |
14
+ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------- |
15
+ | [`@cat-factory/gatekeeper-bindings`](https://www.npmjs.com/package/@cat-factory/gatekeeper-bindings) | the generated operation table this package compiles policy against | comes in as a dependency |
16
+ | `@cat-factory/gatekeeper-worker` (this package) | the capability surface, key broker, webhook receiver, approval inbox, state DO | install, and write only a policy |
17
+ | [`deploy/gatekeeper`](https://github.com/kibertoad/cat-factory/tree/main/deploy/gatekeeper) | the deployment template: policy, wrangler bindings, three lines of wiring | copy, and edit `src/policy.config.ts` |
18
+
19
+ You install this package and write one file. Everything a deployment differs by is the
20
+ **policy**; everything else, from the capability surface down to the Durable Object the minted
21
+ keys live in, comes from here. That split is the goal: upgrading the machinery is a version bump
22
+ rather than a merge against files you have edited, and "did you get the security-relevant fix" is
23
+ answerable from a version number.
24
+
25
+ ## Purpose and goal
26
+
27
+ Its purpose is to put cat-factory behind an organization's own governance pane: per-actor
28
+ credentials so every run traces back to a person, per-tier operation grants so an agent can reach
29
+ only what its operator decided, field masking, and the platform's parked decisions surfaced as an
30
+ approval inbox instead of a polling loop.
31
+
32
+ It is a **consumer of the stable public surface**: it rides `/api/v1` and the outbound webhook
33
+ delivery contract through [`@cat-factory/sdk`](https://www.npmjs.com/package/@cat-factory/sdk) and
34
+ reaches nothing else. A cat-factory deployment that has never heard of it is byte-for-byte
35
+ unchanged.
36
+
37
+ ## How to use it
38
+
39
+ The starting point is the template at
40
+ [`deploy/gatekeeper`](https://github.com/kibertoad/cat-factory/tree/main/deploy/gatekeeper): copy
41
+ it, point it at your workspace, and edit its `src/policy.config.ts`. A deployment's whole Worker
42
+ is:
43
+
44
+ ```ts
45
+ import { createGatekeeperWorker } from '@cat-factory/gatekeeper-worker'
46
+ import { POLICY } from './policy.config'
47
+
48
+ // wrangler resolves `class_name` against the Worker's OWN exports, so the Durable Object class has
49
+ // to be named here even though it is implemented in this package.
50
+ export { GatekeeperState } from '@cat-factory/gatekeeper-worker'
51
+
52
+ export default createGatekeeperWorker({ policy: POLICY })
53
+ ```
54
+
55
+ ```ts
56
+ // policy.config.ts: the `/policy` entry point carries the vocabulary without the Worker runtime,
57
+ // so this file and its tests load anywhere.
58
+ import { DECISION_BINDINGS, type GatekeeperPolicy } from '@cat-factory/gatekeeper-worker/policy'
59
+
60
+ export const POLICY: GatekeeperPolicy = {
61
+ defaultTier: null, // no implicit access: an ungranted actor gets `unknown_actor`, not a capability
62
+ tiers: {
63
+ observer: { description: 'Read the board and runs.', keyScope: 'read', allow: '*' },
64
+ approver: {
65
+ description: 'Answer a run’s parked decisions.',
66
+ keyScope: 'decide',
67
+ // Derived from the answerer table, never transcribed: a run parks on thirteen different
68
+ // things and the surface carries more than forty operations for answering them.
69
+ allow: ['tasks_get_run', ...DECISION_BINDINGS],
70
+ mask: ['run.pullRequestUrl'],
71
+ },
72
+ },
73
+ grants: { 'someone@your-org.example': 'approver' },
74
+ }
75
+ ```
76
+
77
+ `@cloudflare/workers-types` is a **required** peer, not an optional one. Every type this package
78
+ publishes is stated in terms of the Worker globals (`ExportedHandler`, `DurableObjectNamespace`,
79
+ `Request`) and the Durable Object base class comes from `cloudflare:workers`, so a consumer without
80
+ those types cannot compile the three lines above, let alone anything else. It is a peer rather than
81
+ a dependency because the globals are ambient: two copies in one tree redeclare each other, so the
82
+ version has to be the consumer's.
83
+
84
+ ## What it does
85
+
86
+ - **Object-capability bindings over Cap'n Web.** An agent holds an object whose METHODS are the
87
+ operations policy granted it. There is no allow-list consulted per call, because there is nothing
88
+ to consult: an operation the tier does not carry is not a method that refuses, it is absent.
89
+ - **Per-actor credentials.** Each caller gets their own cat-factory key, minted through
90
+ `POST /api/v1/keys` at the tier's scope and stamped with your identity for that person
91
+ (`externalIdentity`), so a run traces back to a human and role-scoped merge policy stays real.
92
+ Minting is claimed before it runs and re-mints once on a 401, so concurrent first calls mint
93
+ once and rotating the provisioning key heals instead of wedging.
94
+ - **Approvals as an inbox, for every park.** The platform's outbound webhook delivers
95
+ parked-decision cards; the Worker verifies the HMAC over the raw bytes, dedupes on `deliveryId`,
96
+ and raises a card. A run can stop on thirteen different things and each has an answerer keyed on
97
+ the SDK's own kind union, so a park the platform adds fails this package's build rather than
98
+ reporting `stale` forever. Answering re-reads the run's live decisions and posts through the
99
+ caller's own key.
100
+ - **Run lifecycle without polling.** `run.started` / `run.completed` / `run.failed` land as a
101
+ `runs_watched()` projection, and a terminal event settles that run's open cards.
102
+ - **Self-enrolment and offboarding.** The endpoint registers itself under a caller-chosen webhook
103
+ id, hourly and idempotently. `POST /admin/retire?actorId=…` revokes every key minted for one
104
+ person, upstream first and then here.
105
+
106
+ One thing to tell whoever writes the agent on the other side: **a task filed with only a title
107
+ parks immediately**, before any agent runs. cat-factory reduces a task's own authored fields before
108
+ the first dispatch, and a missing description is a blocking finding, so `tasks_create` +
109
+ `tasks_start` with a bare title yields a run stopped on an `input-gate` decision rather than one
110
+ that is working. That park is answerable from here like any other, but the cheaper fix is filing
111
+ work that says what it wants.
112
+
113
+ ## What to configure
114
+
115
+ Everything operational comes from the Worker's environment, through two mechanisms that are not
116
+ interchangeable: the vars and the Durable Object binding are written in the template's
117
+ `wrangler.toml`, and the three credentials are secrets, put with `wrangler secret put` into the
118
+ platform's secret store. A credential in a config file is a credential in a repository, so a
119
+ refusal names the mechanism its binding actually takes rather than offering both.
120
+
121
+ | Binding | Kind | What it is |
122
+ | ---------------------- | -------------- | ----------------------------------------------------------------------------- |
123
+ | `CAT_FACTORY_BASE_URL` | var | The cat-factory deployment this Gatekeeper is paired with. |
124
+ | `PUBLIC_URL` | var | This Worker's own public origin; deliveries arrive at `<PUBLIC_URL>/webhook`. |
125
+ | `WEBHOOK_ID` | var | The outbound-webhook id to enrol under. Caller-chosen; keep it stable. |
126
+ | `PROVISIONING_KEY` | secret | An `admin` cat-factory API key. Mints per-actor keys; nothing else. |
127
+ | `WEBHOOK_SECRET` | secret | 16-200 chars. Registered with the endpoint and verified on every delivery. |
128
+ | `OS_SHARED_TOKEN` | secret | The bearer the paired OS deployment presents on every RPC call. |
129
+ | `STATE` | Durable Object | A namespace bound to `GatekeeperState`: cards, dedupe log, minted keys. |
130
+
131
+ A missing binding is answered as a 503 naming it and how it is set, never defaulted: there is no
132
+ safe stand-in for a credential or for the identity of the deployment it talks to. `GET /health`
133
+ asks the whole table at once rather than the bindings a given request path happens to read, so a
134
+ deployment that is wired for liveness and unwired for traffic reads as what it is.
135
+
136
+ The Worker serves five routes:
137
+
138
+ | Route | Auth | What it is |
139
+ | ------------------------------ | ----------------- | ------------------------------------------------------------- |
140
+ | `POST /webhook` | delivery HMAC | The platform's outbound deliveries. Verified over raw bytes. |
141
+ | `ALL /rpc` | `OS_SHARED_TOKEN` | The Cap'n Web endpoint the OS deployment talks to. |
142
+ | `POST /admin/enroll` | `OS_SHARED_TOKEN` | Re-assert the webhook registration. Also runs hourly on cron. |
143
+ | `POST /admin/retire?actorId=…` | `OS_SHARED_TOKEN` | Offboarding: revoke every key minted for one OS user. |
144
+ | `GET /health` | none | Green only when every binding is set and the policy compiles. |
145
+
146
+ `/rpc` is bearer-gated even though the intended path is a Worker service binding, which never
147
+ traverses the internet: a Worker with a route attached is reachable by anyone who finds it, and a
148
+ capability surface whose only defence is obscurity is not one.
149
+
150
+ ## What to customize: the policy
151
+
152
+ The policy is the ONE thing a deployment writes, and it is an argument
153
+ (`createGatekeeperWorker({ policy })`), never a file this package reads. A `GatekeeperPolicy` has
154
+ three fields:
155
+
156
+ - `defaultTier`: the tier an actor with no explicit grant receives, or `null` to refuse unknown
157
+ actors (`unknown_actor`). `null` is the shipped default and the safe one: adding a person is
158
+ then a deliberate edit.
159
+ - `tiers`: named `TierPolicy` entries, each carrying:
160
+ - `description`: prose the OS shows beside the tier.
161
+ - `keyScope`: the scope of the per-actor key minted for this tier (`read` / `write` /
162
+ `decide`). It is also the ceiling on the grants; `admin` is refused outright, because
163
+ `POST /api/v1/keys` cannot mint it and a tier asking for it is asking for the Gatekeeper's
164
+ own provisioning secret.
165
+ - `allow`: binding names to grant, or `'*'` for everything within `keyScope`.
166
+ - `deny`: binding names to subtract from `allow`. Applied last, so a deny always wins; the
167
+ template uses it to keep the debug surface (model prompts, captured output) away from a
168
+ `'*'` read tier.
169
+ - `mask`: dotted paths redacted from every result before it reaches the caller (see below).
170
+ - `grants`: OS actor identity (whatever the OS authenticates and passes to `connect()`) to tier
171
+ name.
172
+
173
+ A policy is compiled against the LIVE operation table and only ever SUBTRACTS from
174
+ `bindingsWithinScope(tier.keyScope)`, so a tier cannot grant above the key backing it, and a
175
+ retired or misspelled operation is a refusal to serve (`PolicyError`) rather than a method that
176
+ 403s on every call. Two rules are worth keeping whatever else you change:
177
+
178
+ - **Grant by name above `read`.** `'*'` is honest for a read-only tier and dangerous above it: a
179
+ deployment that adds an operation ships it to every `'*'` tier on upgrade with nobody deciding to.
180
+ - **Keep `keyScope` as low as the grants allow.** It is the scope of the key minted for each actor,
181
+ so it is the blast radius of that actor's credential.
182
+
183
+ **Masking replaces, never deletes.** A masked leaf becomes the exported `MASKED` sentinel
184
+ (`[masked by gatekeeper policy]`) rather than disappearing, because a removed key and a key the
185
+ platform had no value for read identically to the consuming agent, and they are different facts.
186
+ Paths are dotted and traverse arrays element-wise (`steps.status` masks every step's status); a
187
+ path that matches nothing is not an error, because result shapes legitimately vary by operation.
188
+
189
+ ## What a caller holds
190
+
191
+ `connect({ actorId, label? })` on the `/rpc` session resolves the actor's tier and returns the
192
+ capability. `actorId` is the OS's own authenticated identity for the person, and it is the ONLY
193
+ claim the Gatekeeper trusts: nothing the caller sends picks a tier. Beyond the granted operation
194
+ methods, every capability carries seven reserved methods:
195
+
196
+ - `tier()`: who the caller is acting as (actor, tier name, description, key scope).
197
+ - `bindings()`: the granted operations, each with its scope floor, consequence (cautious default
198
+ applied) and argument shape, so the OS can run its own approval governance per call.
199
+ - `withheld()`: every binding the deployment serves that this capability does NOT carry, with the
200
+ reason. The four reasons are kept apart on purpose: `not_in_policy` and `denied_by_policy` are
201
+ questions for the policy's author, `above_key_scope` is a different one (raise the tier's key,
202
+ or accept the ceiling), and `not_relayable` is neither: an SSE stream or a binary blob cannot
203
+ cross a Cap'n Web call, so the fix is to ask another way (poll `tasks_get_run` instead of the
204
+ event stream).
205
+ - `approvals_list()`, `approvals_inspect(cardId)`, `approvals_answer(cardId, input)`: the
206
+ approval inbox; see the template README for the flow and the three answer outcomes.
207
+ - `runs_watched()`: the run-lifecycle projection built from the `run.*` webhook events.
208
+
209
+ Refusals from a live Gatekeeper are `GatekeeperError`s carrying a machine-readable `reason`
210
+ (`unknown_actor`, `card_not_found`, `ambiguous_park`, …), the same role the platform's own
211
+ `details.reason` plays: an OS Gadget maps it to copy and a remedy. Operator mistakes are
212
+ `PolicyError`s raised at compile time, before any capability exists, so a misconfigured Gatekeeper
213
+ serves nothing rather than serving methods that fail.
214
+
215
+ ## Custody, and what it does not promise
216
+
217
+ The provisioning key is a Worker secret and never leaves the platform's secret store. The per-actor
218
+ keys it mints live in the Durable Object's storage: outside every agent's reach, but at rest in
219
+ your account. If that is not acceptable, mint per call and revoke after, at the cost of a key row
220
+ per operation.
221
+
222
+ What this enforces is which operations an actor may reach and on whose credential. What a run then
223
+ does inside cat-factory is governed by cat-factory's own merge policy and approvals.
224
+
225
+ ## Upgrading
226
+
227
+ Upgrade this package. `@cat-factory/gatekeeper-bindings` arrives as its dependency, pinned to an
228
+ exact version, so bumping this package IS how the operation table moves and there is nothing to
229
+ keep in step by hand. Do not add a direct dependency on the bindings to keep them "together": an
230
+ exact pin plus a second range installs a SECOND copy of the table, while policy still compiles
231
+ against the one resolved here. Everything a policy names is re-exported from
232
+ `@cat-factory/gatekeeper-worker/policy`, so a deployment never needs that dependency.
233
+
234
+ Version skew is reported rather than absorbed. A policy naming an operation newer than the
235
+ installed table fails with a `PolicyError` telling you to upgrade, and the ladder helpers throw on
236
+ a scope rung they do not carry, so a deployment ahead of your packages reads as skew, never as a
237
+ key with no permissions.
238
+
239
+ ## Tests
240
+
241
+ The suite runs inside real `workerd` under `@cloudflare/vitest-pool-workers`, against a Worker
242
+ built from this package's own factory with a real Durable Object, real WebCrypto and a real Cap'n
243
+ Web client, talking to a scripted cat-factory origin bound as the pool's outbound service. The
244
+ credential-custody story IS "the key is a Worker secret", so a Node mock of a Worker would prove
245
+ nothing about it.
246
+
247
+ ```sh
248
+ pnpm --filter @cat-factory/gatekeeper-worker test:run
249
+ ```
250
+
251
+ `test/live/` is the same Worker with the scripted origin taken away. A fixture agrees with this
252
+ package by construction, so a request shape the bindings and the SDK both consider correct can only
253
+ be wrong against a real deployment: the live specs enrol on the real webhook collection, mint a real
254
+ per-actor key (and recover from its revocation), forward the everyday loop, and answer a run that
255
+ really parked, off the card the platform's own notification raises. They are run by
256
+ `@cat-factory/sdk-smoketest`, which owns the deployment they need, so this package carries no
257
+ Postgres-shaped devDependency:
258
+
259
+ ```sh
260
+ DATABASE_URL=... pnpm --filter @cat-factory/sdk-smoketest run smoketest -- --only=gatekeeper
261
+ ```
262
+
263
+ What that deliberately does not cover is a delivery that TRAVELLED: the platform refuses to register
264
+ a loopback endpoint, so the receiver is driven with an envelope the suite signs around the
265
+ platform's own notification object.
266
+
267
+ ## References
268
+
269
+ - [`deploy/gatekeeper`](https://github.com/kibertoad/cat-factory/tree/main/deploy/gatekeeper): the
270
+ template you copy, its configuration walkthrough and the OS-side usage example.
271
+ - [The initiative tracker](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/cloudflare-os-gatekeeper.md):
272
+ design notes, the decisions behind each half, and what the suite deliberately does not cover.
273
+ - [`backend/docs/public-api.md`](https://github.com/kibertoad/cat-factory/blob/main/backend/docs/public-api.md):
274
+ the API this rides (keys, scopes, webhooks, endpoint semantics).
275
+ - [`@cat-factory/gatekeeper-bindings`](https://www.npmjs.com/package/@cat-factory/gatekeeper-bindings):
276
+ the generated operation table policy is compiled against.
@@ -0,0 +1,90 @@
1
+ import { type AnswerFields, type DecisionVerb, type LiveDecision } from './policy/decisions.js';
2
+ import type { ApprovalCard } from './state.js';
3
+ export interface AnswerInput extends AnswerFields {
4
+ /** The verb, as the kind's answerer names it (`approve`, `reply`, `resolve`, …). */
5
+ action: string;
6
+ /**
7
+ * Which park to answer, when the run holds more than one.
8
+ *
9
+ * Optional because the common case is a run parked on exactly one thing. Where two are pending
10
+ * (a follow-up triage accrues while a later step's gate is open), answering without naming the
11
+ * kind is REFUSED rather than resolved by order: the platform lists them in a shape order, not a
12
+ * priority order, so picking the first would settle whichever the projection happened to build
13
+ * first.
14
+ */
15
+ kind?: string;
16
+ }
17
+ /**
18
+ * What answering did.
19
+ *
20
+ * `stale` and `recorded` are the two outcomes an integration gets wrong when they are collapsed
21
+ * into "it worked": the first means the run no longer holds a park this surface can answer, and
22
+ * the second means the answer was taken but the park is still holding the run (an approval short
23
+ * of quorum, a reply recorded before the incorporation that folds it in). Reporting either as
24
+ * "answered" leaves a person believing a run is moving when it is not.
25
+ */
26
+ export type AnswerOutcome = {
27
+ status: 'answered';
28
+ runId: string;
29
+ kind: string;
30
+ action: string;
31
+ decisions: unknown;
32
+ } | {
33
+ status: 'recorded';
34
+ runId: string;
35
+ kind: string;
36
+ action: string;
37
+ /** Why the park still holds, in the run's own numbers where it has them. */
38
+ detail: string;
39
+ decisions: unknown;
40
+ } | {
41
+ status: 'stale';
42
+ runId: string;
43
+ detail: string;
44
+ decisions: unknown;
45
+ };
46
+ /** The minimal shape this module reads off a decision list. */
47
+ export interface DecisionListShape {
48
+ parked?: boolean;
49
+ status?: string;
50
+ decisions?: unknown[];
51
+ unanswerable?: {
52
+ reason?: string;
53
+ detail?: string;
54
+ }[];
55
+ }
56
+ /** Invoke a granted binding. Supplied by the capability, so policy is enforced on every hop. */
57
+ export type BindingInvoker = (name: string, args: Record<string, unknown>) => Promise<unknown>;
58
+ /** One park holding a run, paired with the answerer that knows how to settle it. */
59
+ export interface PendingPark {
60
+ kind: string;
61
+ decision: LiveDecision;
62
+ verbs: readonly DecisionVerb[];
63
+ summary: string;
64
+ }
65
+ /**
66
+ * Every park in the list this Gatekeeper could answer, in the platform's own order.
67
+ *
68
+ * A settled entry is skipped by its kind's own `pending` predicate, and a kind this package does
69
+ * not model is skipped entirely: both are cases where posting an answer would be a guess.
70
+ */
71
+ export declare function pendingParks(list: DecisionListShape): PendingPark[];
72
+ /**
73
+ * Why nothing here is answerable, in the run's own words where it has them.
74
+ *
75
+ * Four different facts, and an integration acts on each differently: the run finished, a person on
76
+ * the VCS host is the gate, the run is parked on a kind this package does not model (a deployment
77
+ * newer than this Gatekeeper), or it is parked on one that is modelled but already settled.
78
+ * Flattening those into "not parked" would send an operator looking in the wrong place for each.
79
+ */
80
+ export declare function describeStale(list: DecisionListShape): string;
81
+ /**
82
+ * Answer one card against the live run.
83
+ *
84
+ * `invoke` carries the policy check, so this function never consults the tier itself: a binding the
85
+ * caller was not granted rejects on the way through, with the capability's own refusal.
86
+ */
87
+ export declare function answerCard(card: ApprovalCard, input: AnswerInput, invoke: BindingInvoker): Promise<AnswerOutcome>;
88
+ /** Refuse a card that cannot be answered, with the reason the OS maps to copy. */
89
+ export declare function assertAnswerable(card: ApprovalCard | null, cardId: string): ApprovalCard;
90
+ //# sourceMappingURL=approvals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"AAqBA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,MAAM,WAAW,WAAY,SAAQ,YAAY;IAC/C,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GACvF;IACE,MAAM,EAAE,UAAU,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;CACnB,GACD;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAA;AAE1E,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,EAAE,CAAA;IACrB,YAAY,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACtD;AAED,gGAAgG;AAChG,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAE9F,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,YAAY,CAAA;IACtB,KAAK,EAAE,SAAS,YAAY,EAAE,CAAA;IAC9B,OAAO,EAAE,MAAM,CAAA;CAChB;AAQD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,iBAAiB,GAAG,WAAW,EAAE,CAenE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA0B7D;AAwED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,aAAa,CAAC,CAqCxB;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,CAgBxF"}
@@ -0,0 +1,193 @@
1
+ // Answering a card: the half of the pattern where the OS's workspace inbox becomes a real decision
2
+ // on a real run.
3
+ //
4
+ // The card is a POINTER, never the decision. Between the delivery that raised it and the answer,
5
+ // the run may have advanced, been stopped, or had the same park answered by a person in the
6
+ // cat-factory app; and the ids every action addresses (`approvalId`, `decisionId`, the brainstorm
7
+ // `stage`) are not on the card at all, because the notification does not carry them. So answering
8
+ // re-reads `/runs/:runId/decisions` first, every time. That is the platform's own instruction
9
+ // ("the webhook is a trigger; the API is the truth") and it is what makes a stale card a REPORT
10
+ // rather than a 404 someone has to interpret.
11
+ //
12
+ // What this module does NOT do is decide how any particular park is answered. It finds the entry
13
+ // holding the run and hands it to that kind's answerer in `decisions.ts`. The run can be parked on
14
+ // any of thirteen things and the card's notification type is at best a hint about which: a
15
+ // `decision_required` card can be an approval gate or an agent question, and a run that parked
16
+ // twice is answering the SECOND park by the time anyone reads the first card.
17
+ //
18
+ // Every upstream call goes through the caller's own granted bindings, so a tier that was not
19
+ // granted `decisions_approve_step` cannot approve through this route either. The answer flow gets
20
+ // no privilege of its own.
21
+ import { answererFor, } from './policy/decisions.js';
22
+ import { GatekeeperError } from './errors.js';
23
+ function asDecision(entry) {
24
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
25
+ return null;
26
+ const kind = entry.kind;
27
+ return typeof kind === 'string' ? entry : null;
28
+ }
29
+ /**
30
+ * Every park in the list this Gatekeeper could answer, in the platform's own order.
31
+ *
32
+ * A settled entry is skipped by its kind's own `pending` predicate, and a kind this package does
33
+ * not model is skipped entirely: both are cases where posting an answer would be a guess.
34
+ */
35
+ export function pendingParks(list) {
36
+ const parks = [];
37
+ for (const entry of list.decisions ?? []) {
38
+ const decision = asDecision(entry);
39
+ if (decision === null)
40
+ continue;
41
+ const answerer = answererFor(decision.kind);
42
+ if (answerer === undefined || !answerer.pending(decision))
43
+ continue;
44
+ parks.push({
45
+ kind: decision.kind,
46
+ decision,
47
+ verbs: answerer.verbs,
48
+ summary: answerer.summary,
49
+ });
50
+ }
51
+ return parks;
52
+ }
53
+ /**
54
+ * Why nothing here is answerable, in the run's own words where it has them.
55
+ *
56
+ * Four different facts, and an integration acts on each differently: the run finished, a person on
57
+ * the VCS host is the gate, the run is parked on a kind this package does not model (a deployment
58
+ * newer than this Gatekeeper), or it is parked on one that is modelled but already settled.
59
+ * Flattening those into "not parked" would send an operator looking in the wrong place for each.
60
+ */
61
+ export function describeStale(list) {
62
+ const waits = list.unanswerable ?? [];
63
+ if (waits.length > 0) {
64
+ return waits
65
+ .map((wait) => `${wait.reason ?? 'unknown'}: ${wait.detail ?? 'no detail supplied'}`)
66
+ .join('; ');
67
+ }
68
+ const kinds = (list.decisions ?? [])
69
+ .map(asDecision)
70
+ .filter((decision) => decision !== null)
71
+ .map((decision) => decision.kind);
72
+ if (kinds.length > 0) {
73
+ const unmodelled = kinds.filter((kind) => answererFor(kind) === undefined);
74
+ if (unmodelled.length > 0) {
75
+ return (`The run is parked on '${[...new Set(unmodelled)].join("', '")}', which this Gatekeeper ` +
76
+ 'does not know how to answer. Upgrade it if the deployment is newer, or answer in the ' +
77
+ 'cat-factory app.');
78
+ }
79
+ return `The run carries '${[...new Set(kinds)].join("', '")}', all already settled.`;
80
+ }
81
+ if (list.parked === false) {
82
+ return `The run is '${list.status ?? 'unknown'}' and holds no parked decision; it was answered elsewhere, or it has moved on.`;
83
+ }
84
+ return 'The run is parked, and the platform named no decision and no unanswerable wait for it.';
85
+ }
86
+ /**
87
+ * Whether the run has left the park entirely.
88
+ *
89
+ * This is what decides a card's fate, and it is deliberately a question about the RUN rather than
90
+ * about the answer: an approve that met its quorum settles the card, an approve short of it does
91
+ * not, and neither is visible in the action's own status code. A `stale` answer settles nothing at
92
+ * all: the run may be parked on a wait a person has to clear, and destroying its inbox entry
93
+ * would hide the one pointer anybody had to it.
94
+ */
95
+ function stillParked(after, kind) {
96
+ return pendingParks(after).find((park) => park.kind === kind) ?? null;
97
+ }
98
+ function pickPark(parks, requested) {
99
+ if (requested !== undefined) {
100
+ const park = parks.find((candidate) => candidate.kind === requested);
101
+ if (park === undefined) {
102
+ throw new GatekeeperError('no_such_park', `The run holds no pending '${requested}' decision. It is parked on ` +
103
+ `'${parks.map((candidate) => candidate.kind).join("', '")}'.`);
104
+ }
105
+ return park;
106
+ }
107
+ if (parks.length > 1) {
108
+ throw new GatekeeperError('ambiguous_park', `The run is parked on ${parks.length} decisions at once ` +
109
+ `('${parks.map((park) => park.kind).join("', '")}'). Name which one with \`kind\`: the ` +
110
+ 'platform lists them in a shape order, not a priority order, so answering the first would ' +
111
+ 'settle whichever the projection happened to build first.');
112
+ }
113
+ return parks[0];
114
+ }
115
+ function pickVerb(park, action) {
116
+ const verb = park.verbs.find((candidate) => candidate.action === action);
117
+ if (verb === undefined) {
118
+ throw new GatekeeperError('unsupported_action', `A '${park.kind}' decision does not take '${action}'. It takes ` +
119
+ `'${park.verbs.map((candidate) => candidate.action).join("', '")}'.`);
120
+ }
121
+ return verb;
122
+ }
123
+ /**
124
+ * Why a park that survived the answer is still holding the run.
125
+ *
126
+ * Derived from the platform's own numbers, never from prose: a quorum reports its tally, an
127
+ * iterative review reports the pass it is on. Where the park states neither, the honest answer is
128
+ * that it is still there, which is itself the fact the caller needs.
129
+ */
130
+ function describeProgress(park) {
131
+ const { decision } = park;
132
+ if (typeof decision.recordedApprovals === 'number' &&
133
+ typeof decision.requiredApprovals === 'number') {
134
+ return `${decision.recordedApprovals} of ${decision.requiredApprovals} approvals recorded; the gate still needs the rest.`;
135
+ }
136
+ if (typeof decision.iteration === 'number' && typeof decision.maxIterations === 'number') {
137
+ return `The loop is on pass ${decision.iteration} of ${decision.maxIterations} and has not converged.`;
138
+ }
139
+ return `The '${park.kind}' decision is still holding the run; there is more to answer on it.`;
140
+ }
141
+ /**
142
+ * Answer one card against the live run.
143
+ *
144
+ * `invoke` carries the policy check, so this function never consults the tier itself: a binding the
145
+ * caller was not granted rejects on the way through, with the capability's own refusal.
146
+ */
147
+ export async function answerCard(card, input, invoke) {
148
+ const before = (await invoke('decisions_list', { runId: card.runId }));
149
+ const parks = pendingParks(before);
150
+ if (parks.length === 0) {
151
+ return { status: 'stale', runId: card.runId, detail: describeStale(before), decisions: before };
152
+ }
153
+ const park = pickPark(parks, input.kind);
154
+ const verb = pickVerb(park, input.action);
155
+ const call = verb.call(park.decision, input);
156
+ const after = (await invoke(call.binding, {
157
+ runId: card.runId,
158
+ ...call.args,
159
+ }));
160
+ // Every decision action returns the run's WHOLE list, re-read after the action. So whether the
161
+ // answer moved the run is in hand already, with no second call.
162
+ const survived = stillParked(after, park.kind);
163
+ if (survived !== null) {
164
+ return {
165
+ status: 'recorded',
166
+ runId: card.runId,
167
+ kind: park.kind,
168
+ action: verb.action,
169
+ detail: describeProgress(survived),
170
+ decisions: after,
171
+ };
172
+ }
173
+ return {
174
+ status: 'answered',
175
+ runId: card.runId,
176
+ kind: park.kind,
177
+ action: verb.action,
178
+ decisions: after,
179
+ };
180
+ }
181
+ /** Refuse a card that cannot be answered, with the reason the OS maps to copy. */
182
+ export function assertAnswerable(card, cardId) {
183
+ if (card === null) {
184
+ throw new GatekeeperError('card_not_found', `No approval card '${cardId}'. It may have been raised against a different paired workspace, ` +
185
+ 'or predate this Gatekeeper.');
186
+ }
187
+ if (card.resolvedAt !== null) {
188
+ throw new GatekeeperError('card_already_resolved', `Approval card '${cardId}' was settled as '${card.resolution}'. Read the run's decisions if ` +
189
+ 'it has parked again since.');
190
+ }
191
+ return card;
192
+ }
193
+ //# sourceMappingURL=approvals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}