@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 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, 39 method handlers in total. Spec-aligned
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 20+ required. No required runtime dependencies; uses Node's built-in
17
- `crypto` for Ed25519 and JCS. `better-sqlite3` is an optional dependency,
18
- needed only for `SqliteStore`.
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 when `requireSignatures` is true |
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
- **39 method handlers in total**, matching the Python reference exactly.
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. The call itself has already succeeded in memory; the next
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. Rebuilt
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
- /** The change since the last write, taking the recorded keys with it. */
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` was addressed in the task's review. Applied to
205
- * review-decision methods (decide.* and abstain.declare): to act on a
206
- * review you must be one of the reviewers it was addressed to (the
207
- * `to` set on review.request). Membership is the floor; this is the
208
- * eligibility ceiling for decisions. Returns an error fragment if not
209
- * eligible, else null.
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
- /** One workspace as `snapshot()` holds it. Reads nothing as a change. */
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. The call itself has already succeeded in memory; the next
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. Rebuilt
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
- /** The change since the last write, taking the recorded keys with it. */
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` was addressed in the task's review. Applied to
205
- * review-decision methods (decide.* and abstain.declare): to act on a
206
- * review you must be one of the reviewers it was addressed to (the
207
- * `to` set on review.request). Membership is the floor; this is the
208
- * eligibility ceiling for decisions. Returns an error fragment if not
209
- * eligible, else null.
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
- /** One workspace as `snapshot()` holds it. Reads nothing as a change. */
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: {