@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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/approvals.d.ts +90 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +193 -0
- package/dist/approvals.js.map +1 -0
- package/dist/capability.d.ts +61 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +125 -0
- package/dist/capability.js.map +1 -0
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +90 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -0
- package/dist/gatekeeper.d.ts +78 -0
- package/dist/gatekeeper.d.ts.map +1 -0
- package/dist/gatekeeper.js +162 -0
- package/dist/gatekeeper.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +62 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +152 -0
- package/dist/keys.js.map +1 -0
- package/dist/masking.d.ts +9 -0
- package/dist/masking.d.ts.map +1 -0
- package/dist/masking.js +43 -0
- package/dist/masking.js.map +1 -0
- package/dist/policy/compile.d.ts +88 -0
- package/dist/policy/compile.d.ts.map +1 -0
- package/dist/policy/compile.js +170 -0
- package/dist/policy/compile.js.map +1 -0
- package/dist/policy/decisions.d.ts +93 -0
- package/dist/policy/decisions.d.ts.map +1 -0
- package/dist/policy/decisions.js +659 -0
- package/dist/policy/decisions.js.map +1 -0
- package/dist/policy/index.d.ts +6 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +17 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/state.d.ts +150 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +229 -0
- package/dist/state.js.map +1 -0
- package/dist/webhook/delivery.d.ts +102 -0
- package/dist/webhook/delivery.d.ts.map +1 -0
- package/dist/webhook/delivery.js +162 -0
- package/dist/webhook/delivery.js.map +1 -0
- package/dist/webhook/signature.d.ts +17 -0
- package/dist/webhook/signature.d.ts.map +1 -0
- package/dist/webhook/signature.js +73 -0
- package/dist/webhook/signature.js.map +1 -0
- package/dist/worker.d.ts +21 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +155 -0
- package/dist/worker.js.map +1 -0
- 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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"}
|