@brightbeamai/chap-coordinator 0.3.4 → 0.3.5
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/README.md +54 -8
- package/dist/{api-59aU01Bs.d.cts → api-CjZQnj63.d.cts} +128 -14
- package/dist/{api-CxciGFHA.d.ts → api-DG0VFGVr.d.ts} +128 -14
- package/dist/api.cjs +3 -1
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +3 -1
- package/dist/api.js.map +1 -1
- package/dist/index.cjs +1336 -219
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +1336 -219
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc.cjs +1 -0
- package/dist/jsonrpc.cjs.map +1 -1
- package/dist/jsonrpc.d.cts +1 -0
- package/dist/jsonrpc.d.ts +1 -0
- package/dist/jsonrpc.js +1 -0
- package/dist/jsonrpc.js.map +1 -1
- package/dist/methods.cjs.map +1 -1
- package/dist/methods.d.cts +36 -3
- package/dist/methods.d.ts +36 -3
- package/dist/storage/sqlite.cjs +81 -19
- package/dist/storage/sqlite.cjs.map +1 -1
- package/dist/storage/sqlite.d.cts +19 -9
- package/dist/storage/sqlite.d.ts +19 -9
- package/dist/storage/sqlite.js +81 -19
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.cjs +20 -15
- package/dist/storage/store.cjs.map +1 -1
- package/dist/storage/store.d.cts +24 -6
- package/dist/storage/store.d.ts +24 -6
- package/dist/storage/store.js +20 -15
- package/dist/storage/store.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +56 -1
- package/dist/types.d.ts +56 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript reference implementation of the Collaborative Human-Agent Protocol (CHAP).
|
|
4
4
|
|
|
5
|
-
Covers Core plus every profile,
|
|
5
|
+
Covers Core plus every profile, 40 method handlers in total. Spec-aligned
|
|
6
6
|
field names, error codes, and response shapes against `profiles/*.md`.
|
|
7
7
|
The Python reference at `packages/coordinator-py/` implements the same
|
|
8
8
|
surface, so the two interoperate on the same JSON-RPC 2.0 wire.
|
|
@@ -13,9 +13,10 @@ surface, so the two interoperate on the same JSON-RPC 2.0 wire.
|
|
|
13
13
|
npm install @brightbeamai/chap-coordinator
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Node
|
|
17
|
-
`crypto` for Ed25519 and JCS. `better-sqlite3
|
|
18
|
-
|
|
16
|
+
Node 22+ required. No required runtime dependencies; uses Node's built-in
|
|
17
|
+
`crypto` for Ed25519 and JCS. `SqliteStore` uses `better-sqlite3`, an
|
|
18
|
+
optional dependency, where it is installed, and Node's built-in
|
|
19
|
+
`node:sqlite` (Node 22.13 and later) where it is not.
|
|
19
20
|
|
|
20
21
|
## Companion packages
|
|
21
22
|
|
|
@@ -87,19 +88,19 @@ console.log(resp.result); // { task_id: "tsk_...", state: "created" }
|
|
|
87
88
|
| Profile | Methods (per profile spec) |
|
|
88
89
|
|-----------------------|-------------------------------------------------------------------------------------------|
|
|
89
90
|
| `core/1.0` | `workspace.create`, `workspace.describe`, `workspace.set_profiles`, `participant.join`, `participant.leave`, `task.create`, `task.update`, `task.complete`, `audit.read`; a task created with `review_required` completes only on a reviewer decision |
|
|
90
|
-
| `review/1.0` | `review.request`, `decide.approve`, `decide.reject`, `decide.override`, `abstain.declare`, `escalate.raise` |
|
|
91
|
+
| `review/1.0` | `review.request`, `review.acknowledge`, `decide.approve`, `decide.reject`, `decide.override`, `abstain.declare`, `escalate.raise` |
|
|
91
92
|
| `whisper/1.0` | `whisper.ask`, `whisper.answer`; lapse hook via `coord.checkWhisperLapses(workspaceId, now?)` |
|
|
92
93
|
| `deliberation/1.0` | `deliberate.open`, `deliberate.comment`, `deliberate.vote`, `deliberate.close` |
|
|
93
94
|
| `handoff/1.0` | `handoff.propose`, `handoff.accept`, `handoff.decline`; multi-task; recipient may be a URI or `group:` |
|
|
94
95
|
| `control/1.0` | `control.pause`, `control.resume`, `control.cancel`, `control.snapshot`, `control.rollback`, `control.supersede`, `control.set_mode_ceiling`; pause/resume take `scope: task/participant/workspace` |
|
|
95
96
|
| `routing/1.0` | `task.route`, `review.depth`, `escalate.auto`; each emits a `route_decision` artefact |
|
|
96
97
|
| `modes/1.0` | Mode handling at `task.create`; `trial` mode forces `review_required`; `control.set_mode_ceiling` shared with control/1.0 |
|
|
97
|
-
| `security-signed/1.0` | `participant.rotate_key`, `participant.revoke_key`; top-level `sig` verified at dispatch
|
|
98
|
+
| `security-signed/1.0` | `participant.rotate_key`, `participant.revoke_key`; top-level `sig` verified at dispatch where the workspace advertises the profile or `requireSignatures` is true, under the workspace's `signature_policy` |
|
|
98
99
|
| `audit-scitt/1.0` | `audit.submit_to_scitt`, `audit.verify_receipt`, `audit.verify_chain`; SCITT statements assembled and passed to a deployment-supplied submitter; local prev-hash chain linkage retained |
|
|
99
100
|
| `identity-oidc/1.0` | `participant.join` binding via `verifyOidcToken`; `cnf.jwk` pinning, step-up auth via `enforceStepUp` |
|
|
100
101
|
| `identity-vc/1.0` | `participant.join` binding via `verifyVc`; holder-key pinning |
|
|
101
102
|
|
|
102
|
-
**
|
|
103
|
+
**40 method handlers in total**, matching the Python reference exactly.
|
|
103
104
|
|
|
104
105
|
**The review gate.** A task carrying `review_required` does not complete on
|
|
105
106
|
`task.complete`. The call opens a review, holding the submitted output as the
|
|
@@ -110,6 +111,13 @@ the completer and the assignee; a workspace with no eligible human refuses the
|
|
|
110
111
|
completion rather than opening a review nobody can decide. Under `modes/1.0` a
|
|
111
112
|
trial-mode task has `review_required` set for it.
|
|
112
113
|
|
|
114
|
+
A review records its `authors`, the task's assignee and the member that
|
|
115
|
+
completed the work, and refuses a decision from either with `-32011`, however
|
|
116
|
+
the review is addressed. On a task that requires review only a human member
|
|
117
|
+
decides, and a `review.request` naming an author or an agent is refused. A task
|
|
118
|
+
that holds a review, taken back to `in_progress`, completes only through a
|
|
119
|
+
decision: its next `task.complete` opens a new round.
|
|
120
|
+
|
|
113
121
|
## Architecture
|
|
114
122
|
|
|
115
123
|
The coordinator is transport-agnostic, persistence-agnostic, and
|
|
@@ -123,6 +131,20 @@ const response = coord.dispatch(envelope);
|
|
|
123
131
|
response. State is held in memory; persist by subscribing with
|
|
124
132
|
`coord.onAudit(listener)`.
|
|
125
133
|
|
|
134
|
+
With a `store` option (`SqliteStore`, or a store of your own), each call's
|
|
135
|
+
changes are written before `dispatch` returns, and the listeners hear of an
|
|
136
|
+
entry once it is written. A call whose write fails is answered with
|
|
137
|
+
`-32603` and `data: { reason: "store" }`, and its workspace goes back to
|
|
138
|
+
what the store holds. Before each call on a workspace the coordinator asks
|
|
139
|
+
the store how many entries it holds, and reloads a workspace another
|
|
140
|
+
instance has written, so instances that share a store take turns
|
|
141
|
+
([SPECIFICATION.md §10.3](../../SPECIFICATION.md#103-writers)).
|
|
142
|
+
|
|
143
|
+
A handler that throws is answered with `-32603` and no detail, since an
|
|
144
|
+
exception's text can name a path or another workspace's data. The error
|
|
145
|
+
goes to `onInternalError(err, { method, workspace })`, or to stderr when
|
|
146
|
+
the option is unset.
|
|
147
|
+
|
|
126
148
|
Reference HTTP bindings:
|
|
127
149
|
- `reference/core/` and `reference/core-plus-review/` (standalone TS
|
|
128
150
|
servers used by the conformance harness and the demo playground)
|
|
@@ -160,7 +182,12 @@ const coord = new Coordinator({
|
|
|
160
182
|
|
|
161
183
|
If a `participant.join` envelope includes `oidc_token` or
|
|
162
184
|
`vc_presentation`, the relevant hook is called and the bound key is
|
|
163
|
-
added to the participant's key history.
|
|
185
|
+
added to the participant's key history. A token with no `cnf.jwk` binds
|
|
186
|
+
the key in the join's `jwks` whose RFC 7638 thumbprint is the token's
|
|
187
|
+
`nonce`. Where a hook is configured and the workspace advertises its
|
|
188
|
+
profile, a `human` joins with the credential it checks or is refused
|
|
189
|
+
(`-32403`, `-32410`), and the log records the credential as its SHA-256
|
|
190
|
+
digest, never the credential itself.
|
|
164
191
|
|
|
165
192
|
## Signed envelopes (security-signed/1.0)
|
|
166
193
|
|
|
@@ -168,6 +195,14 @@ added to the participant's key history.
|
|
|
168
195
|
const coord = new Coordinator({ requireSignatures: true });
|
|
169
196
|
```
|
|
170
197
|
|
|
198
|
+
A workspace also requires signatures when it advertises
|
|
199
|
+
`security-signed/1.0`, and cannot stop advertising it once it does. Its
|
|
200
|
+
`signature_policy`, a `workspace.create` param that falls back to the
|
|
201
|
+
`signaturePolicy` option, is `"all"` by default; under `"humans"` an
|
|
202
|
+
unsigned call is taken from a member that is not human, so an agent on the
|
|
203
|
+
server's own transport works unsigned, and every person's decision is
|
|
204
|
+
still signed.
|
|
205
|
+
|
|
171
206
|
Each participant registers one or more JWKs at `participant.join` via
|
|
172
207
|
`jwks: { keys: [...] }`. Senders sign the JCS canonicalisation of the
|
|
173
208
|
envelope **with the top-level `sig` field removed**, then attach the
|
|
@@ -175,6 +210,17 @@ signature as a top-level `sig: "ed25519:<kid>:<base64>"` field. The
|
|
|
175
210
|
Coordinator looks up the verifying key by `(from, kid, ts)`, so
|
|
176
211
|
historical envelopes verify across key rotation.
|
|
177
212
|
|
|
213
|
+
A key held by a browser's authenticator, a P-256 JWK with a `webauthn`
|
|
214
|
+
member naming its `rp_id`, signs with a WebAuthn assertion whose challenge
|
|
215
|
+
is the SHA-256 of those same bytes, tagged
|
|
216
|
+
`webauthn:<kid>:<authenticatorData>.<clientDataJSON>.<signature>`
|
|
217
|
+
([profiles/security-signed.md](../../profiles/security-signed.md) §3).
|
|
218
|
+
|
|
219
|
+
A signed request takes effect once. The same signed call sent again, its
|
|
220
|
+
`sig` re-encoded or not, is refused with `-32075` and
|
|
221
|
+
`data.accepted_at_seq`, and a copy of a recorded refusal is answered with
|
|
222
|
+
that refusal; neither is evaluated again.
|
|
223
|
+
|
|
178
224
|
Key lifecycle:
|
|
179
225
|
- `participant.rotate_key` is signed with the old key; the Coordinator
|
|
180
226
|
marks `valid_until` on the old key and `valid_from` on the new one.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { MethodParams, WorkspaceCreateResult, WorkspaceDescribeResult, OkResult, TaskCreateResult, TaskRouteResult, ReviewRequestResult, ReviewDepthResult, DecideOverrideResult, EscalateRaiseResult, EscalateAutoResult, WhisperAskResult, DeliberateOpenResult, DeliberateCloseResult, HandoffProposeResult, ControlSupersedeResult, ControlSnapshotResult, AuditReadResult, AuditSubmitToScittResult, AuditVerifyReceiptResult, AuditVerifyChainResult, MethodName, MethodResult } from './methods.cjs';
|
|
1
|
+
import { MethodParams, WorkspaceCreateResult, WorkspaceDescribeResult, OkResult, WorkspaceInviteResult, TaskCreateResult, TaskRouteResult, ReviewRequestResult, ReviewAcknowledgeResult, ReviewDepthResult, DecideOverrideResult, EscalateRaiseResult, EscalateAutoResult, WhisperAskResult, DeliberateOpenResult, DeliberateCloseResult, HandoffProposeResult, ControlSupersedeResult, ControlSnapshotResult, AuditReadResult, AuditSubmitToScittResult, AuditVerifyReceiptResult, AuditVerifyChainResult, MethodName, MethodResult } from './methods.cjs';
|
|
2
2
|
import { Store } from './storage/store.cjs';
|
|
3
|
-
import { Workspace, AuditListener, Task, ParticipantUri, Envelope, WorkspaceId, Member, KeyRecord } from './types.cjs';
|
|
3
|
+
import { Workspace, SignaturePolicy, JoinPolicy, AuditListener, Task, ParticipantUri, Envelope, WorkspaceId, Member, KeyRecord } from './types.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @brightbeamai/chap-coordinator/ids
|
|
@@ -72,23 +72,59 @@ interface CoordinatorOptions {
|
|
|
72
72
|
deterministicIds?: boolean;
|
|
73
73
|
deterministicClock?: boolean;
|
|
74
74
|
enableChain?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Require signatures on every workspace, as advertising
|
|
77
|
+
* `security-signed/1.0` does on one.
|
|
78
|
+
*/
|
|
75
79
|
requireSignatures?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The signature policy of a workspace whose `workspace.create` names none:
|
|
82
|
+
* "all" (the default) verifies every call, and "humans" takes an unsigned
|
|
83
|
+
* call from an agent member.
|
|
84
|
+
*/
|
|
85
|
+
signaturePolicy?: SignaturePolicy;
|
|
86
|
+
/**
|
|
87
|
+
* The join policy of a workspace whose `workspace.create` names none:
|
|
88
|
+
* "open" (the default) admits anyone, and "invite" admits only a
|
|
89
|
+
* participant an admin invited, and the first member.
|
|
90
|
+
*/
|
|
91
|
+
joinPolicy?: JoinPolicy;
|
|
76
92
|
enforceStepUp?: boolean;
|
|
77
93
|
requireReadMembership?: boolean;
|
|
78
94
|
/** Largest envelope accepted (default 1 MiB), published in the descriptor. */
|
|
79
95
|
maxEnvelopeBytes?: number;
|
|
96
|
+
/** Told of each audit entry once the store has taken it. */
|
|
80
97
|
onAudit?: AuditListener;
|
|
81
98
|
onAutoEscalate?: (task: Task, to: ParticipantUri) => void;
|
|
82
99
|
/**
|
|
83
100
|
* Called when a write to the store fails, with the workspace and the
|
|
84
101
|
* version the write would have reached. When unset, the failure is written
|
|
85
|
-
* to stderr.
|
|
86
|
-
* write to the workspace carries what this one did not.
|
|
102
|
+
* to stderr. What happens to the call is `storeErrors`'s to say.
|
|
87
103
|
*/
|
|
88
104
|
onStoreError?: (err: unknown, record: {
|
|
89
105
|
id: string;
|
|
90
106
|
version: number;
|
|
91
107
|
}) => void;
|
|
108
|
+
/**
|
|
109
|
+
* What a failed synchronous write does to the call that made it. "raise"
|
|
110
|
+
* (the default): the call is answered with -32603 and
|
|
111
|
+
* `data: { reason: "store" }`, and the workspace goes back to what the
|
|
112
|
+
* store holds. "log": the call is answered as it was, and the next write
|
|
113
|
+
* carries its change again. A write another writer got to first is
|
|
114
|
+
* answered as "raise" says under either, and the call may be sent again.
|
|
115
|
+
* An asynchronous write's call was answered already, and the next write
|
|
116
|
+
* to the workspace carries what this one did not.
|
|
117
|
+
*/
|
|
118
|
+
storeErrors?: "raise" | "log";
|
|
119
|
+
/**
|
|
120
|
+
* Called when a handler throws, with the error and the call's method and
|
|
121
|
+
* workspace. The caller is answered with -32603 and no detail. When unset,
|
|
122
|
+
* the error is written to stderr.
|
|
123
|
+
*/
|
|
124
|
+
onInternalError?: (err: unknown, call: {
|
|
125
|
+
method: string;
|
|
126
|
+
workspace?: string;
|
|
127
|
+
}) => void;
|
|
92
128
|
verifyOidcToken?: TokenVerifier;
|
|
93
129
|
verifyVc?: CredentialVerifier;
|
|
94
130
|
scittSubmitter?: ScittSubmitter;
|
|
@@ -130,13 +166,21 @@ declare class Coordinator {
|
|
|
130
166
|
private unflushed;
|
|
131
167
|
/** Workspaces with a write queued behind an async write in flight. */
|
|
132
168
|
private flushQueued;
|
|
169
|
+
/**
|
|
170
|
+
* Workspaces whose store copy may differ from memory after a synchronous
|
|
171
|
+
* write failed or conflicted. Each is reloaded from the store before the
|
|
172
|
+
* next call on it, which is refused while the reload fails.
|
|
173
|
+
*/
|
|
174
|
+
private held;
|
|
175
|
+
/** Entries appended and not yet written, which the audit listeners hear of once they are. */
|
|
176
|
+
private unannounced;
|
|
133
177
|
private clockMs?;
|
|
134
178
|
private frozenNow?;
|
|
135
179
|
private auditListeners;
|
|
136
180
|
/**
|
|
137
181
|
* The signed calls on each workspace's log, by what their senders signed:
|
|
138
|
-
* recorded refusals with their seq and code, and accepted calls
|
|
139
|
-
* from the log when missing, so a snapshot never carries it.
|
|
182
|
+
* recorded refusals with their seq and code, and accepted calls with their
|
|
183
|
+
* seq. Rebuilt from the log when missing, so a snapshot never carries it.
|
|
140
184
|
*/
|
|
141
185
|
private signedIndexes;
|
|
142
186
|
/** Method handler registry; profiles plug into this. */
|
|
@@ -174,13 +218,21 @@ declare class Coordinator {
|
|
|
174
218
|
private flush;
|
|
175
219
|
private chainWrite;
|
|
176
220
|
private writeChange;
|
|
177
|
-
/**
|
|
221
|
+
/** Tell the audit listeners of the entries now written, up to `upTo`. */
|
|
222
|
+
private announce;
|
|
223
|
+
/**
|
|
224
|
+
* The change since the last write, taking the recorded keys with it. Each
|
|
225
|
+
* object is written as a copy in the shape a restore gives back, so a
|
|
226
|
+
* coordinator restarted on the store holds what this one holds, and the
|
|
227
|
+
* workspace's own objects are left as they are.
|
|
228
|
+
*/
|
|
178
229
|
private takeChange;
|
|
179
230
|
/** A failed write's keys go back, so the next write carries them again. */
|
|
180
231
|
private giveBack;
|
|
181
232
|
/** Persist the whole workspace, for a store that implements only `save`. */
|
|
182
233
|
private persistSnapshot;
|
|
183
234
|
private trackSave;
|
|
235
|
+
private reportInternalError;
|
|
184
236
|
private reportStoreError;
|
|
185
237
|
drainSaves(): Promise<void>;
|
|
186
238
|
private advanceClock;
|
|
@@ -201,19 +253,24 @@ declare class Coordinator {
|
|
|
201
253
|
*/
|
|
202
254
|
private requireMember;
|
|
203
255
|
/**
|
|
204
|
-
* Assert that `sender`
|
|
205
|
-
* review-decision methods (decide.* and abstain.declare)
|
|
206
|
-
* review
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
256
|
+
* Assert that `sender` may decide the task's open review. Applied to
|
|
257
|
+
* review-decision methods (decide.* and abstain.declare) and to
|
|
258
|
+
* review.acknowledge: to act on a review you must not be an author of the
|
|
259
|
+
* work under review, must be a human member where the task requires
|
|
260
|
+
* review, and must be one of the reviewers it was addressed to (the `to`
|
|
261
|
+
* set on review.request).
|
|
262
|
+
* Membership is the floor; this is the eligibility ceiling for decisions.
|
|
263
|
+
* Returns an error fragment if not eligible, else null.
|
|
210
264
|
*/
|
|
211
265
|
private requireReviewer;
|
|
212
266
|
/** Serialise all workspaces to a JSON-safe structure for persistence. */
|
|
213
267
|
snapshot(): unknown;
|
|
214
268
|
/** The workspace's own fields: everything except its collections and log. */
|
|
215
269
|
private metaOf;
|
|
216
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* One workspace as `snapshot()` holds it: each object of its collections a
|
|
272
|
+
* copy in the shape the store keeps. Reads nothing as a change.
|
|
273
|
+
*/
|
|
217
274
|
private snapshotOf;
|
|
218
275
|
/** Restore from a snapshot produced by ``snapshot()``. Replaces
|
|
219
276
|
* any existing in-memory workspaces. */
|
|
@@ -221,6 +278,24 @@ declare class Coordinator {
|
|
|
221
278
|
/** One workspace from a snapshot entry, its maps tracked. */
|
|
222
279
|
private rebuild;
|
|
223
280
|
dispatch(envelope: Envelope): Envelope;
|
|
281
|
+
/**
|
|
282
|
+
* Work that changes a workspace outside any call, a whisper lapse check
|
|
283
|
+
* for one, kept to the rules a call keeps (SPECIFICATION 10.3): the
|
|
284
|
+
* workspace is first brought up to date with the store, and left alone
|
|
285
|
+
* as long as it is held; what the work changed is written once, when it ends;
|
|
286
|
+
* and where that write fails, the workspace goes back to what the store
|
|
287
|
+
* holds and the work's results are dropped, since none took effect.
|
|
288
|
+
*/
|
|
289
|
+
outsideCall<T>(id: WorkspaceId, work: () => T[]): T[];
|
|
290
|
+
/**
|
|
291
|
+
* Before a call on a workspace: where the store holds a different number of
|
|
292
|
+
* audit entries for it than this instance last wrote, another writer has
|
|
293
|
+
* written it, and it is reloaded. A workspace held after a failed write is
|
|
294
|
+
* reloaded as well, and the call refused while that fails.
|
|
295
|
+
*/
|
|
296
|
+
private keepUp;
|
|
297
|
+
/** Replace a workspace with what the store holds of it. False when it cannot be read. */
|
|
298
|
+
private reload;
|
|
224
299
|
private _dispatch;
|
|
225
300
|
recordAudit(ws: Workspace, envelope: Envelope): void;
|
|
226
301
|
/**
|
|
@@ -236,6 +311,20 @@ declare class Coordinator {
|
|
|
236
311
|
private signedIndex;
|
|
237
312
|
private appendEntry;
|
|
238
313
|
private verifySignature;
|
|
314
|
+
/**
|
|
315
|
+
* SPECIFICATION 6.5: an identity profile states that a verifier checks
|
|
316
|
+
* people's credentials, so advertising one this coordinator has no
|
|
317
|
+
* verifier for describes a guarantee it does not give.
|
|
318
|
+
*/
|
|
319
|
+
private unverifiedIdentity;
|
|
320
|
+
/** Whether a workspace requires signatures: it advertises security-signed/1.0, or this coordinator is configured to. */
|
|
321
|
+
private requiresSignatures;
|
|
322
|
+
/**
|
|
323
|
+
* Whether a participant of this type signs its calls in a workspace that
|
|
324
|
+
* requires signatures: every participant under "all", and every one but an
|
|
325
|
+
* agent under "humans". A non-member signs.
|
|
326
|
+
*/
|
|
327
|
+
private mustSign;
|
|
239
328
|
/** Lookup the key valid for a given participant at a given time. */
|
|
240
329
|
lookupKey(member: Member, kid: string, ts: string): KeyRecord | null;
|
|
241
330
|
/**
|
|
@@ -253,15 +342,38 @@ declare class Coordinator {
|
|
|
253
342
|
private registerCoreHandlers;
|
|
254
343
|
private registerProfileHandlers;
|
|
255
344
|
private opWorkspaceCreate;
|
|
345
|
+
/**
|
|
346
|
+
* The workspace a workspace.create with these params makes, or why it
|
|
347
|
+
* cannot be made. Nothing is registered, so participant.join can judge a
|
|
348
|
+
* join by the workspace it would make and make it only once the join
|
|
349
|
+
* passes.
|
|
350
|
+
*/
|
|
351
|
+
private buildWorkspace;
|
|
352
|
+
private addWorkspace;
|
|
256
353
|
private opWorkspaceDescribe;
|
|
257
354
|
private opWorkspaceSetProfiles;
|
|
258
355
|
private opParticipantJoin;
|
|
356
|
+
/**
|
|
357
|
+
* SPECIFICATION 6.3.3: an admin invites a participant. For a URI that is
|
|
358
|
+
* not a member it records an invitation, which the invitee's join takes
|
|
359
|
+
* up. For a member that holds no key in force it registers the
|
|
360
|
+
* invitation's keys: the way back for a member whose keys were all revoked,
|
|
361
|
+
* or who joined a workspace that requires signatures with none.
|
|
362
|
+
*/
|
|
363
|
+
private opWorkspaceInvite;
|
|
259
364
|
private opParticipantLeave;
|
|
260
365
|
private opTaskCreate;
|
|
261
366
|
private opTaskUpdate;
|
|
262
367
|
private opTaskComplete;
|
|
263
368
|
private opAuditRead;
|
|
264
369
|
private opReviewRequest;
|
|
370
|
+
/**
|
|
371
|
+
* review/1.0 S3.6: a reviewer says they have begun the open review. Only
|
|
372
|
+
* someone who may decide it can, so the log's record of who took a review
|
|
373
|
+
* up, and when, names reviewers alone. It changes nothing else: the review
|
|
374
|
+
* stays open and the acknowledgement counts towards no rule.
|
|
375
|
+
*/
|
|
376
|
+
private opReviewAcknowledge;
|
|
265
377
|
/**
|
|
266
378
|
* Verify an optional `approved_artefact_digest` against the artefact under
|
|
267
379
|
* review (review/1.0 S3.2).
|
|
@@ -296,6 +408,7 @@ declare function makeApi(coord: Coordinator): {
|
|
|
296
408
|
create: (p: MethodParams<"workspace.create">) => WorkspaceCreateResult;
|
|
297
409
|
describe: (p: MethodParams<"workspace.describe">) => WorkspaceDescribeResult;
|
|
298
410
|
set_profiles: (p: MethodParams<"workspace.set_profiles">) => OkResult;
|
|
411
|
+
invite: (p: MethodParams<"workspace.invite">) => WorkspaceInviteResult;
|
|
299
412
|
};
|
|
300
413
|
participant: {
|
|
301
414
|
join: (p: MethodParams<"participant.join">) => OkResult;
|
|
@@ -311,6 +424,7 @@ declare function makeApi(coord: Coordinator): {
|
|
|
311
424
|
};
|
|
312
425
|
review: {
|
|
313
426
|
request: (p: MethodParams<"review.request">) => ReviewRequestResult;
|
|
427
|
+
acknowledge: (p: MethodParams<"review.acknowledge">) => ReviewAcknowledgeResult;
|
|
314
428
|
depth: (p: MethodParams<"review.depth">) => ReviewDepthResult;
|
|
315
429
|
};
|
|
316
430
|
decide: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { MethodParams, WorkspaceCreateResult, WorkspaceDescribeResult, OkResult, TaskCreateResult, TaskRouteResult, ReviewRequestResult, ReviewDepthResult, DecideOverrideResult, EscalateRaiseResult, EscalateAutoResult, WhisperAskResult, DeliberateOpenResult, DeliberateCloseResult, HandoffProposeResult, ControlSupersedeResult, ControlSnapshotResult, AuditReadResult, AuditSubmitToScittResult, AuditVerifyReceiptResult, AuditVerifyChainResult, MethodName, MethodResult } from './methods.js';
|
|
1
|
+
import { MethodParams, WorkspaceCreateResult, WorkspaceDescribeResult, OkResult, WorkspaceInviteResult, TaskCreateResult, TaskRouteResult, ReviewRequestResult, ReviewAcknowledgeResult, ReviewDepthResult, DecideOverrideResult, EscalateRaiseResult, EscalateAutoResult, WhisperAskResult, DeliberateOpenResult, DeliberateCloseResult, HandoffProposeResult, ControlSupersedeResult, ControlSnapshotResult, AuditReadResult, AuditSubmitToScittResult, AuditVerifyReceiptResult, AuditVerifyChainResult, MethodName, MethodResult } from './methods.js';
|
|
2
2
|
import { Store } from './storage/store.js';
|
|
3
|
-
import { Workspace, AuditListener, Task, ParticipantUri, Envelope, WorkspaceId, Member, KeyRecord } from './types.js';
|
|
3
|
+
import { Workspace, SignaturePolicy, JoinPolicy, AuditListener, Task, ParticipantUri, Envelope, WorkspaceId, Member, KeyRecord } from './types.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @brightbeamai/chap-coordinator/ids
|
|
@@ -72,23 +72,59 @@ interface CoordinatorOptions {
|
|
|
72
72
|
deterministicIds?: boolean;
|
|
73
73
|
deterministicClock?: boolean;
|
|
74
74
|
enableChain?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Require signatures on every workspace, as advertising
|
|
77
|
+
* `security-signed/1.0` does on one.
|
|
78
|
+
*/
|
|
75
79
|
requireSignatures?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The signature policy of a workspace whose `workspace.create` names none:
|
|
82
|
+
* "all" (the default) verifies every call, and "humans" takes an unsigned
|
|
83
|
+
* call from an agent member.
|
|
84
|
+
*/
|
|
85
|
+
signaturePolicy?: SignaturePolicy;
|
|
86
|
+
/**
|
|
87
|
+
* The join policy of a workspace whose `workspace.create` names none:
|
|
88
|
+
* "open" (the default) admits anyone, and "invite" admits only a
|
|
89
|
+
* participant an admin invited, and the first member.
|
|
90
|
+
*/
|
|
91
|
+
joinPolicy?: JoinPolicy;
|
|
76
92
|
enforceStepUp?: boolean;
|
|
77
93
|
requireReadMembership?: boolean;
|
|
78
94
|
/** Largest envelope accepted (default 1 MiB), published in the descriptor. */
|
|
79
95
|
maxEnvelopeBytes?: number;
|
|
96
|
+
/** Told of each audit entry once the store has taken it. */
|
|
80
97
|
onAudit?: AuditListener;
|
|
81
98
|
onAutoEscalate?: (task: Task, to: ParticipantUri) => void;
|
|
82
99
|
/**
|
|
83
100
|
* Called when a write to the store fails, with the workspace and the
|
|
84
101
|
* version the write would have reached. When unset, the failure is written
|
|
85
|
-
* to stderr.
|
|
86
|
-
* write to the workspace carries what this one did not.
|
|
102
|
+
* to stderr. What happens to the call is `storeErrors`'s to say.
|
|
87
103
|
*/
|
|
88
104
|
onStoreError?: (err: unknown, record: {
|
|
89
105
|
id: string;
|
|
90
106
|
version: number;
|
|
91
107
|
}) => void;
|
|
108
|
+
/**
|
|
109
|
+
* What a failed synchronous write does to the call that made it. "raise"
|
|
110
|
+
* (the default): the call is answered with -32603 and
|
|
111
|
+
* `data: { reason: "store" }`, and the workspace goes back to what the
|
|
112
|
+
* store holds. "log": the call is answered as it was, and the next write
|
|
113
|
+
* carries its change again. A write another writer got to first is
|
|
114
|
+
* answered as "raise" says under either, and the call may be sent again.
|
|
115
|
+
* An asynchronous write's call was answered already, and the next write
|
|
116
|
+
* to the workspace carries what this one did not.
|
|
117
|
+
*/
|
|
118
|
+
storeErrors?: "raise" | "log";
|
|
119
|
+
/**
|
|
120
|
+
* Called when a handler throws, with the error and the call's method and
|
|
121
|
+
* workspace. The caller is answered with -32603 and no detail. When unset,
|
|
122
|
+
* the error is written to stderr.
|
|
123
|
+
*/
|
|
124
|
+
onInternalError?: (err: unknown, call: {
|
|
125
|
+
method: string;
|
|
126
|
+
workspace?: string;
|
|
127
|
+
}) => void;
|
|
92
128
|
verifyOidcToken?: TokenVerifier;
|
|
93
129
|
verifyVc?: CredentialVerifier;
|
|
94
130
|
scittSubmitter?: ScittSubmitter;
|
|
@@ -130,13 +166,21 @@ declare class Coordinator {
|
|
|
130
166
|
private unflushed;
|
|
131
167
|
/** Workspaces with a write queued behind an async write in flight. */
|
|
132
168
|
private flushQueued;
|
|
169
|
+
/**
|
|
170
|
+
* Workspaces whose store copy may differ from memory after a synchronous
|
|
171
|
+
* write failed or conflicted. Each is reloaded from the store before the
|
|
172
|
+
* next call on it, which is refused while the reload fails.
|
|
173
|
+
*/
|
|
174
|
+
private held;
|
|
175
|
+
/** Entries appended and not yet written, which the audit listeners hear of once they are. */
|
|
176
|
+
private unannounced;
|
|
133
177
|
private clockMs?;
|
|
134
178
|
private frozenNow?;
|
|
135
179
|
private auditListeners;
|
|
136
180
|
/**
|
|
137
181
|
* The signed calls on each workspace's log, by what their senders signed:
|
|
138
|
-
* recorded refusals with their seq and code, and accepted calls
|
|
139
|
-
* from the log when missing, so a snapshot never carries it.
|
|
182
|
+
* recorded refusals with their seq and code, and accepted calls with their
|
|
183
|
+
* seq. Rebuilt from the log when missing, so a snapshot never carries it.
|
|
140
184
|
*/
|
|
141
185
|
private signedIndexes;
|
|
142
186
|
/** Method handler registry; profiles plug into this. */
|
|
@@ -174,13 +218,21 @@ declare class Coordinator {
|
|
|
174
218
|
private flush;
|
|
175
219
|
private chainWrite;
|
|
176
220
|
private writeChange;
|
|
177
|
-
/**
|
|
221
|
+
/** Tell the audit listeners of the entries now written, up to `upTo`. */
|
|
222
|
+
private announce;
|
|
223
|
+
/**
|
|
224
|
+
* The change since the last write, taking the recorded keys with it. Each
|
|
225
|
+
* object is written as a copy in the shape a restore gives back, so a
|
|
226
|
+
* coordinator restarted on the store holds what this one holds, and the
|
|
227
|
+
* workspace's own objects are left as they are.
|
|
228
|
+
*/
|
|
178
229
|
private takeChange;
|
|
179
230
|
/** A failed write's keys go back, so the next write carries them again. */
|
|
180
231
|
private giveBack;
|
|
181
232
|
/** Persist the whole workspace, for a store that implements only `save`. */
|
|
182
233
|
private persistSnapshot;
|
|
183
234
|
private trackSave;
|
|
235
|
+
private reportInternalError;
|
|
184
236
|
private reportStoreError;
|
|
185
237
|
drainSaves(): Promise<void>;
|
|
186
238
|
private advanceClock;
|
|
@@ -201,19 +253,24 @@ declare class Coordinator {
|
|
|
201
253
|
*/
|
|
202
254
|
private requireMember;
|
|
203
255
|
/**
|
|
204
|
-
* Assert that `sender`
|
|
205
|
-
* review-decision methods (decide.* and abstain.declare)
|
|
206
|
-
* review
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
256
|
+
* Assert that `sender` may decide the task's open review. Applied to
|
|
257
|
+
* review-decision methods (decide.* and abstain.declare) and to
|
|
258
|
+
* review.acknowledge: to act on a review you must not be an author of the
|
|
259
|
+
* work under review, must be a human member where the task requires
|
|
260
|
+
* review, and must be one of the reviewers it was addressed to (the `to`
|
|
261
|
+
* set on review.request).
|
|
262
|
+
* Membership is the floor; this is the eligibility ceiling for decisions.
|
|
263
|
+
* Returns an error fragment if not eligible, else null.
|
|
210
264
|
*/
|
|
211
265
|
private requireReviewer;
|
|
212
266
|
/** Serialise all workspaces to a JSON-safe structure for persistence. */
|
|
213
267
|
snapshot(): unknown;
|
|
214
268
|
/** The workspace's own fields: everything except its collections and log. */
|
|
215
269
|
private metaOf;
|
|
216
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* One workspace as `snapshot()` holds it: each object of its collections a
|
|
272
|
+
* copy in the shape the store keeps. Reads nothing as a change.
|
|
273
|
+
*/
|
|
217
274
|
private snapshotOf;
|
|
218
275
|
/** Restore from a snapshot produced by ``snapshot()``. Replaces
|
|
219
276
|
* any existing in-memory workspaces. */
|
|
@@ -221,6 +278,24 @@ declare class Coordinator {
|
|
|
221
278
|
/** One workspace from a snapshot entry, its maps tracked. */
|
|
222
279
|
private rebuild;
|
|
223
280
|
dispatch(envelope: Envelope): Envelope;
|
|
281
|
+
/**
|
|
282
|
+
* Work that changes a workspace outside any call, a whisper lapse check
|
|
283
|
+
* for one, kept to the rules a call keeps (SPECIFICATION 10.3): the
|
|
284
|
+
* workspace is first brought up to date with the store, and left alone
|
|
285
|
+
* as long as it is held; what the work changed is written once, when it ends;
|
|
286
|
+
* and where that write fails, the workspace goes back to what the store
|
|
287
|
+
* holds and the work's results are dropped, since none took effect.
|
|
288
|
+
*/
|
|
289
|
+
outsideCall<T>(id: WorkspaceId, work: () => T[]): T[];
|
|
290
|
+
/**
|
|
291
|
+
* Before a call on a workspace: where the store holds a different number of
|
|
292
|
+
* audit entries for it than this instance last wrote, another writer has
|
|
293
|
+
* written it, and it is reloaded. A workspace held after a failed write is
|
|
294
|
+
* reloaded as well, and the call refused while that fails.
|
|
295
|
+
*/
|
|
296
|
+
private keepUp;
|
|
297
|
+
/** Replace a workspace with what the store holds of it. False when it cannot be read. */
|
|
298
|
+
private reload;
|
|
224
299
|
private _dispatch;
|
|
225
300
|
recordAudit(ws: Workspace, envelope: Envelope): void;
|
|
226
301
|
/**
|
|
@@ -236,6 +311,20 @@ declare class Coordinator {
|
|
|
236
311
|
private signedIndex;
|
|
237
312
|
private appendEntry;
|
|
238
313
|
private verifySignature;
|
|
314
|
+
/**
|
|
315
|
+
* SPECIFICATION 6.5: an identity profile states that a verifier checks
|
|
316
|
+
* people's credentials, so advertising one this coordinator has no
|
|
317
|
+
* verifier for describes a guarantee it does not give.
|
|
318
|
+
*/
|
|
319
|
+
private unverifiedIdentity;
|
|
320
|
+
/** Whether a workspace requires signatures: it advertises security-signed/1.0, or this coordinator is configured to. */
|
|
321
|
+
private requiresSignatures;
|
|
322
|
+
/**
|
|
323
|
+
* Whether a participant of this type signs its calls in a workspace that
|
|
324
|
+
* requires signatures: every participant under "all", and every one but an
|
|
325
|
+
* agent under "humans". A non-member signs.
|
|
326
|
+
*/
|
|
327
|
+
private mustSign;
|
|
239
328
|
/** Lookup the key valid for a given participant at a given time. */
|
|
240
329
|
lookupKey(member: Member, kid: string, ts: string): KeyRecord | null;
|
|
241
330
|
/**
|
|
@@ -253,15 +342,38 @@ declare class Coordinator {
|
|
|
253
342
|
private registerCoreHandlers;
|
|
254
343
|
private registerProfileHandlers;
|
|
255
344
|
private opWorkspaceCreate;
|
|
345
|
+
/**
|
|
346
|
+
* The workspace a workspace.create with these params makes, or why it
|
|
347
|
+
* cannot be made. Nothing is registered, so participant.join can judge a
|
|
348
|
+
* join by the workspace it would make and make it only once the join
|
|
349
|
+
* passes.
|
|
350
|
+
*/
|
|
351
|
+
private buildWorkspace;
|
|
352
|
+
private addWorkspace;
|
|
256
353
|
private opWorkspaceDescribe;
|
|
257
354
|
private opWorkspaceSetProfiles;
|
|
258
355
|
private opParticipantJoin;
|
|
356
|
+
/**
|
|
357
|
+
* SPECIFICATION 6.3.3: an admin invites a participant. For a URI that is
|
|
358
|
+
* not a member it records an invitation, which the invitee's join takes
|
|
359
|
+
* up. For a member that holds no key in force it registers the
|
|
360
|
+
* invitation's keys: the way back for a member whose keys were all revoked,
|
|
361
|
+
* or who joined a workspace that requires signatures with none.
|
|
362
|
+
*/
|
|
363
|
+
private opWorkspaceInvite;
|
|
259
364
|
private opParticipantLeave;
|
|
260
365
|
private opTaskCreate;
|
|
261
366
|
private opTaskUpdate;
|
|
262
367
|
private opTaskComplete;
|
|
263
368
|
private opAuditRead;
|
|
264
369
|
private opReviewRequest;
|
|
370
|
+
/**
|
|
371
|
+
* review/1.0 S3.6: a reviewer says they have begun the open review. Only
|
|
372
|
+
* someone who may decide it can, so the log's record of who took a review
|
|
373
|
+
* up, and when, names reviewers alone. It changes nothing else: the review
|
|
374
|
+
* stays open and the acknowledgement counts towards no rule.
|
|
375
|
+
*/
|
|
376
|
+
private opReviewAcknowledge;
|
|
265
377
|
/**
|
|
266
378
|
* Verify an optional `approved_artefact_digest` against the artefact under
|
|
267
379
|
* review (review/1.0 S3.2).
|
|
@@ -296,6 +408,7 @@ declare function makeApi(coord: Coordinator): {
|
|
|
296
408
|
create: (p: MethodParams<"workspace.create">) => WorkspaceCreateResult;
|
|
297
409
|
describe: (p: MethodParams<"workspace.describe">) => WorkspaceDescribeResult;
|
|
298
410
|
set_profiles: (p: MethodParams<"workspace.set_profiles">) => OkResult;
|
|
411
|
+
invite: (p: MethodParams<"workspace.invite">) => WorkspaceInviteResult;
|
|
299
412
|
};
|
|
300
413
|
participant: {
|
|
301
414
|
join: (p: MethodParams<"participant.join">) => OkResult;
|
|
@@ -311,6 +424,7 @@ declare function makeApi(coord: Coordinator): {
|
|
|
311
424
|
};
|
|
312
425
|
review: {
|
|
313
426
|
request: (p: MethodParams<"review.request">) => ReviewRequestResult;
|
|
427
|
+
acknowledge: (p: MethodParams<"review.acknowledge">) => ReviewAcknowledgeResult;
|
|
314
428
|
depth: (p: MethodParams<"review.depth">) => ReviewDepthResult;
|
|
315
429
|
};
|
|
316
430
|
decide: {
|
package/dist/api.cjs
CHANGED
|
@@ -54,7 +54,8 @@ function makeApi(coord) {
|
|
|
54
54
|
workspace: {
|
|
55
55
|
create: (p) => c("workspace.create", p),
|
|
56
56
|
describe: (p) => c("workspace.describe", p),
|
|
57
|
-
set_profiles: (p) => c("workspace.set_profiles", p)
|
|
57
|
+
set_profiles: (p) => c("workspace.set_profiles", p),
|
|
58
|
+
invite: (p) => c("workspace.invite", p)
|
|
58
59
|
},
|
|
59
60
|
participant: {
|
|
60
61
|
join: (p) => c("participant.join", p),
|
|
@@ -70,6 +71,7 @@ function makeApi(coord) {
|
|
|
70
71
|
},
|
|
71
72
|
review: {
|
|
72
73
|
request: (p) => c("review.request", p),
|
|
74
|
+
acknowledge: (p) => c("review.acknowledge", p),
|
|
73
75
|
depth: (p) => c("review.depth", p)
|
|
74
76
|
},
|
|
75
77
|
decide: {
|