@brightbeamai/chap-coordinator 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +271 -0
  3. package/dist/api-C3nkEk-B.d.cts +260 -0
  4. package/dist/api-CmSvlIqO.d.ts +260 -0
  5. package/dist/api.cjs +125 -0
  6. package/dist/api.cjs.map +1 -0
  7. package/dist/api.d.cts +4 -0
  8. package/dist/api.d.ts +4 -0
  9. package/dist/api.js +98 -0
  10. package/dist/api.js.map +1 -0
  11. package/dist/index.cjs +2804 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +175 -0
  14. package/dist/index.d.ts +175 -0
  15. package/dist/index.js +2762 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/jsonrpc.cjs +103 -0
  18. package/dist/jsonrpc.cjs.map +1 -0
  19. package/dist/jsonrpc.d.cts +65 -0
  20. package/dist/jsonrpc.d.ts +65 -0
  21. package/dist/jsonrpc.js +76 -0
  22. package/dist/jsonrpc.js.map +1 -0
  23. package/dist/methods.cjs +19 -0
  24. package/dist/methods.cjs.map +1 -0
  25. package/dist/methods.d.cts +455 -0
  26. package/dist/methods.d.ts +455 -0
  27. package/dist/methods.js +1 -0
  28. package/dist/methods.js.map +1 -0
  29. package/dist/patch.cjs +277 -0
  30. package/dist/patch.cjs.map +1 -0
  31. package/dist/patch.d.cts +29 -0
  32. package/dist/patch.d.ts +29 -0
  33. package/dist/patch.js +250 -0
  34. package/dist/patch.js.map +1 -0
  35. package/dist/storage/sqlite.cjs +94 -0
  36. package/dist/storage/sqlite.cjs.map +1 -0
  37. package/dist/storage/sqlite.d.cts +67 -0
  38. package/dist/storage/sqlite.d.ts +67 -0
  39. package/dist/storage/sqlite.js +76 -0
  40. package/dist/storage/sqlite.js.map +1 -0
  41. package/dist/storage/store.cjs +45 -0
  42. package/dist/storage/store.cjs.map +1 -0
  43. package/dist/storage/store.d.cts +53 -0
  44. package/dist/storage/store.d.ts +53 -0
  45. package/dist/storage/store.js +20 -0
  46. package/dist/storage/store.js.map +1 -0
  47. package/dist/types.cjs +34 -0
  48. package/dist/types.cjs.map +1 -0
  49. package/dist/types.d.cts +287 -0
  50. package/dist/types.d.ts +287 -0
  51. package/dist/types.js +9 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +52 -0
package/dist/index.js ADDED
@@ -0,0 +1,2762 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/canonical.ts
9
+ import { createHash } from "crypto";
10
+ var ZERO_HASH = "sha256:" + "0".repeat(64);
11
+ var NON_INTEGER_ERROR = 'CHAP canonical numbers must be integers; represent decimals as strings (e.g. "8.2") so the hash is deterministic across implementations.';
12
+ var NUMBER_RANGE_ERROR = "CHAP canonical integers must be within the safe-integer range (abs value <= Number.MAX_SAFE_INTEGER); represent larger numbers as strings.";
13
+ function canonicalString(s) {
14
+ return JSON.stringify(s);
15
+ }
16
+ function canonicalNumber(n) {
17
+ if (!Number.isFinite(n)) {
18
+ throw new Error(
19
+ "Non-finite numbers are not permitted in a CHAP canonical value"
20
+ );
21
+ }
22
+ if (!Number.isInteger(n)) {
23
+ throw new Error(NON_INTEGER_ERROR);
24
+ }
25
+ if (Math.abs(n) > Number.MAX_SAFE_INTEGER) {
26
+ throw new Error(NUMBER_RANGE_ERROR);
27
+ }
28
+ return n.toString();
29
+ }
30
+ function canon(value) {
31
+ if (value === null) return "null";
32
+ if (value === true) return "true";
33
+ if (value === false) return "false";
34
+ if (typeof value === "string") return canonicalString(value);
35
+ if (typeof value === "number") return canonicalNumber(value);
36
+ if (Array.isArray(value)) {
37
+ return "[" + value.map(canon).join(",") + "]";
38
+ }
39
+ if (typeof value === "object") {
40
+ const obj = value;
41
+ const keys = Object.keys(obj).sort();
42
+ const parts = [];
43
+ for (const key of keys) {
44
+ parts.push(canonicalString(key) + ":" + canon(obj[key]));
45
+ }
46
+ return "{" + parts.join(",") + "}";
47
+ }
48
+ throw new TypeError(`Cannot canonicalise value of type ${typeof value}`);
49
+ }
50
+ function canonicalize(value) {
51
+ return Buffer.from(canon(value), "utf-8");
52
+ }
53
+ function sha256Hex(data) {
54
+ const h = createHash("sha256");
55
+ h.update(typeof data === "string" ? Buffer.from(data, "utf-8") : data);
56
+ return "sha256:" + h.digest("hex");
57
+ }
58
+ function contentHash(content) {
59
+ return sha256Hex(canonicalize(content));
60
+ }
61
+
62
+ // src/crypto.ts
63
+ import {
64
+ createPrivateKey,
65
+ createPublicKey,
66
+ sign as nodeSign,
67
+ verify as nodeVerify,
68
+ createHash as createHash2
69
+ } from "crypto";
70
+ var ED25519_OID = Buffer.from("302a300506032b6570032100", "hex");
71
+ function b64urlNoPad(buf) {
72
+ return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
73
+ }
74
+ function b64urlDecode(s) {
75
+ const pad = "=".repeat((4 - s.length % 4) % 4);
76
+ return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
77
+ }
78
+ function deriveSeed(uri) {
79
+ return createHash2("sha256").update("chap:" + uri).digest();
80
+ }
81
+ function privateKeyFromSeed(seed) {
82
+ if (seed.length !== 32) {
83
+ throw new Error("Ed25519 seed must be 32 bytes");
84
+ }
85
+ const pkcs8 = Buffer.concat([
86
+ Buffer.from("302e020100300506032b657004220420", "hex"),
87
+ seed
88
+ ]);
89
+ return createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" });
90
+ }
91
+ function publicKeyBytes(privateKey) {
92
+ const pub = createPublicKey(privateKey);
93
+ const der = pub.export({ format: "der", type: "spki" });
94
+ return der.subarray(der.length - 32);
95
+ }
96
+ function publicKeyFromRaw(raw) {
97
+ if (raw.length !== 32) {
98
+ throw new Error("Ed25519 public key must be 32 bytes");
99
+ }
100
+ const der = Buffer.concat([ED25519_OID, raw]);
101
+ return createPublicKey({ key: der, format: "der", type: "spki" });
102
+ }
103
+ function jwkFromPrivateKey(uri, key) {
104
+ const pub = publicKeyBytes(key);
105
+ return {
106
+ kty: "OKP",
107
+ crv: "Ed25519",
108
+ kid: createHash2("sha256").update(uri).digest("hex").slice(0, 16),
109
+ use: "sig",
110
+ alg: "EdDSA",
111
+ x: b64urlNoPad(pub)
112
+ };
113
+ }
114
+ function signEnvelope(canonical, privateKey, kid) {
115
+ const sig = nodeSign(null, canonical, privateKey);
116
+ return `ed25519:${kid}:${sig.toString("base64")}`;
117
+ }
118
+ function verifyEnvelope(canonical, sigTag, publicKey) {
119
+ if (!sigTag.startsWith("ed25519:")) return false;
120
+ const parts = sigTag.split(":");
121
+ if (parts.length < 2) return false;
122
+ const sigB64 = parts[parts.length - 1];
123
+ try {
124
+ const sig = Buffer.from(sigB64, "base64");
125
+ return nodeVerify(null, canonical, publicKey, sig);
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ function publicKeyFromJwk(jwk) {
131
+ return publicKeyFromRaw(b64urlDecode(jwk.x));
132
+ }
133
+ function deriveKeypair(uri) {
134
+ const seed = deriveSeed(uri);
135
+ const privateKey = privateKeyFromSeed(seed);
136
+ return { privateKey, jwk: jwkFromPrivateKey(uri, privateKey) };
137
+ }
138
+
139
+ // src/ids.ts
140
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
141
+ function encode(value, length) {
142
+ let out = "";
143
+ for (let i = 0; i < length; i++) {
144
+ const idx = Number(value & 31n);
145
+ out = ALPHABET[idx] + out;
146
+ value >>= 5n;
147
+ }
148
+ return out;
149
+ }
150
+ var IdFactory = class {
151
+ constructor(deterministic = false, seed = 0n, startMs = 17e11) {
152
+ this.deterministic = deterministic;
153
+ this.counter = seed;
154
+ this.clockMs = startMs;
155
+ }
156
+ deterministic;
157
+ counter;
158
+ clockMs;
159
+ nowMs() {
160
+ if (this.deterministic) {
161
+ this.clockMs += 1;
162
+ return this.clockMs;
163
+ }
164
+ return Date.now();
165
+ }
166
+ randomness() {
167
+ if (this.deterministic) {
168
+ this.counter += 1n;
169
+ return this.counter * 0x9E3779B97F4A7C15n & (1n << 80n) - 1n;
170
+ }
171
+ const buf = new Uint8Array(10);
172
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
173
+ crypto.getRandomValues(buf);
174
+ } else {
175
+ const nodeCrypto = __require("crypto");
176
+ buf.set(nodeCrypto.randomBytes(10));
177
+ }
178
+ let n = 0n;
179
+ for (const b of buf) n = n << 8n | BigInt(b);
180
+ return n;
181
+ }
182
+ ulid() {
183
+ const ts = BigInt(this.nowMs()) & (1n << 48n) - 1n;
184
+ const rand = this.randomness();
185
+ return encode(ts, 10) + encode(rand, 16);
186
+ }
187
+ envelopeId() {
188
+ return this.ulid();
189
+ }
190
+ taskId() {
191
+ return "tsk_" + this.ulid();
192
+ }
193
+ artefactId() {
194
+ return "art_" + this.ulid();
195
+ }
196
+ logicalId() {
197
+ return "lgl_" + this.ulid();
198
+ }
199
+ workspaceId() {
200
+ return "wsp_" + this.ulid();
201
+ }
202
+ deliberationId() {
203
+ return "dlb_" + this.ulid();
204
+ }
205
+ handoffId() {
206
+ return "hnd_" + this.ulid();
207
+ }
208
+ snapshotId() {
209
+ return "snp_" + this.ulid();
210
+ }
211
+ };
212
+
213
+ // src/jsonrpc.ts
214
+ var E = {
215
+ // JSON-RPC 2.0 standard
216
+ PARSE: -32700,
217
+ REQUEST: -32600,
218
+ METHOD: -32601,
219
+ PARAMS: -32602,
220
+ INTERNAL: -32603,
221
+ // review/1.0 profile
222
+ NOT_REVIEWABLE: -32010,
223
+ NOT_AUTHORISED: -32011,
224
+ PATCH_FAILED: -32012,
225
+ REVIEW_LAPSED: -32013,
226
+ // whisper/1.0 profile (profiles/whisper.md S6)
227
+ WHISPER_ALREADY_ANSWERED: -32020,
228
+ WHISPER_LAPSED: -32021,
229
+ WHISPER_OPTION_NOT_IN_SET: -32022,
230
+ // deliberation/1.0 profile (profiles/deliberation.md S5)
231
+ DELIB_VOTER_NOT_IN_LIST: -32030,
232
+ DELIB_ALREADY_VOTED: -32031,
233
+ DELIB_CLOSED_OR_LAPSED: -32032,
234
+ DELIB_UNKNOWN_RULE: -32033,
235
+ // modes/1.0 profile (profiles/modes.md S6)
236
+ MODE_CEILING_EXCEEDED: -32040,
237
+ MODE_STEP_UP_REQUIRED: -32041,
238
+ // handoff/1.0 profile (profiles/handoff.md S6)
239
+ HANDOFF_TASKS_NOT_ASSIGNED_TO_PROPOSER: -32050,
240
+ HANDOFF_ALREADY_RESOLVED: -32051,
241
+ HANDOFF_RECIPIENT_NOT_MEMBER: -32052,
242
+ // control/1.0 profile (profiles/control.md S6)
243
+ CONTROL_STEP_UP_REQUIRED: -32060,
244
+ CONTROL_NOT_AUTHORISED: -32061,
245
+ CONTROL_SNAPSHOT_NOT_FOUND: -32062,
246
+ CONTROL_WORKSPACE_PAUSED: -32063,
247
+ // security-signed/1.0 profile (profiles/security-signed.md S7)
248
+ SIG_VERIFY_FAILED: -32070,
249
+ SIG_KEY_NOT_FOUND: -32071,
250
+ SIG_KEY_REVOKED: -32072,
251
+ SIG_ROTATION_KEY_MISMATCH: -32073,
252
+ // audit-scitt/1.0 profile (profiles/audit-scitt.md S8)
253
+ SCITT_UNREACHABLE: -32080,
254
+ SCITT_STATEMENT_REJECTED: -32081,
255
+ SCITT_RECEIPT_INVALID: -32082,
256
+ // identity-oidc/1.0 profile (profiles/identity-oidc.md S8)
257
+ OIDC_STEP_UP_REQUIRED: -32402,
258
+ OIDC_TOKEN_INVALID: -32403,
259
+ OIDC_CNF_MISMATCH: -32404,
260
+ OIDC_SCOPE_MISSING: -32405,
261
+ // identity-vc/1.0 profile (profiles/identity-vc.md S8)
262
+ VC_VP_INVALID: -32410,
263
+ VC_HOLDER_BINDING_INVALID: -32411,
264
+ VC_CREDENTIAL_REVOKED: -32412,
265
+ VC_SCHEMA_UNKNOWN: -32413,
266
+ // routing/1.0 profile (profiles/routing.md S3-S5)
267
+ ROUTING_NO_ELIGIBLE_ASSIGNEE: -32510,
268
+ ROUTING_POLICY_VIOLATION: -32511,
269
+ ROUTING_AUTO_ESCALATION_TRIGGERED: -32512,
270
+ ROUTING_CANDIDATES_EMPTY: -32513,
271
+ ROUTING_DEPTH_NOT_APPLICABLE: -32514,
272
+ ROUTING_POLICY_UNREACHABLE: -32515,
273
+ ROUTING_ESC_TARGET_UNAVAILABLE: -32516
274
+ };
275
+ function rpcError(code, message, data) {
276
+ return { code, message, ...data !== void 0 ? { data } : {} };
277
+ }
278
+ function isValidEnvelope(env) {
279
+ if (typeof env !== "object" || env === null) return false;
280
+ const e = env;
281
+ return e.jsonrpc === "2.0";
282
+ }
283
+
284
+ // src/patch.ts
285
+ var PatchError = class extends Error {
286
+ };
287
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
288
+ function unescape(token) {
289
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
290
+ }
291
+ function splitPath(path) {
292
+ if (path === "") return [];
293
+ if (!path.startsWith("/")) {
294
+ throw new PatchError(`JSON Pointer must start with '/': ${JSON.stringify(path)}`);
295
+ }
296
+ const segments = path.slice(1).split("/").map(unescape);
297
+ for (const seg of segments) {
298
+ if (DANGEROUS_KEYS.has(seg)) {
299
+ throw new PatchError(`Refusing unsafe path segment ${JSON.stringify(seg)}`);
300
+ }
301
+ }
302
+ return segments;
303
+ }
304
+ function navigate(doc, parts) {
305
+ if (parts.length === 0) {
306
+ throw new PatchError("Cannot operate on root with this helper.");
307
+ }
308
+ let parent = doc;
309
+ for (let i = 0; i < parts.length - 1; i++) {
310
+ const part = parts[i];
311
+ if (Array.isArray(parent)) {
312
+ const idx = Number(part);
313
+ if (!Number.isInteger(idx) || idx < 0 || idx >= parent.length) {
314
+ throw new PatchError(`Index out of range at /${parts.slice(0, i + 1).join("/")}`);
315
+ }
316
+ parent = parent[idx];
317
+ } else if (parent !== null && typeof parent === "object") {
318
+ if (!Object.prototype.hasOwnProperty.call(parent, part)) {
319
+ throw new PatchError(`Path not found: /${parts.slice(0, i + 1).join("/")}`);
320
+ }
321
+ parent = parent[part];
322
+ } else {
323
+ throw new PatchError(`Cannot traverse into ${typeof parent} at ${JSON.stringify(part)}`);
324
+ }
325
+ }
326
+ const last = parts[parts.length - 1];
327
+ if (Array.isArray(parent)) {
328
+ if (last === "-") return [parent, "-"];
329
+ const idx = Number(last);
330
+ if (!Number.isInteger(idx)) {
331
+ throw new PatchError(`Array index expected at ${JSON.stringify(last)}`);
332
+ }
333
+ return [parent, idx];
334
+ }
335
+ return [parent, last];
336
+ }
337
+ function getAt(doc, path) {
338
+ const parts = splitPath(path);
339
+ if (parts.length === 0) return doc;
340
+ const [parent, key] = navigate(doc, parts);
341
+ if (Array.isArray(parent)) {
342
+ if (key === "-") throw new PatchError("Cannot read '-' position.");
343
+ const idx = key;
344
+ if (idx < 0 || idx >= parent.length) throw new PatchError(`Index out of range: ${path}`);
345
+ return parent[idx];
346
+ }
347
+ if (parent === null || typeof parent !== "object" || !Object.prototype.hasOwnProperty.call(parent, key)) {
348
+ throw new PatchError(`Path not found: ${path}`);
349
+ }
350
+ return parent[key];
351
+ }
352
+ function clone(v) {
353
+ return v === void 0 ? v : JSON.parse(JSON.stringify(v));
354
+ }
355
+ function deepEqual(a, b) {
356
+ if (a === b) return true;
357
+ if (a === null || b === null) return a === b;
358
+ if (typeof a !== typeof b) return false;
359
+ if (typeof a !== "object") return a === b;
360
+ const aArr = Array.isArray(a);
361
+ if (aArr !== Array.isArray(b)) return false;
362
+ if (aArr) {
363
+ if (a.length !== b.length) return false;
364
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
365
+ return true;
366
+ }
367
+ const ak = Object.keys(a);
368
+ const bk = Object.keys(b);
369
+ if (ak.length !== bk.length) return false;
370
+ for (const k of ak) {
371
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
372
+ if (!deepEqual(a[k], b[k])) return false;
373
+ }
374
+ return true;
375
+ }
376
+ function applyOne(doc, op) {
377
+ const kind = op.op;
378
+ const path = op.path ?? "";
379
+ if (kind === "add") {
380
+ if (!("value" in op)) throw new PatchError("'add' requires 'value'");
381
+ const parts = splitPath(path);
382
+ if (parts.length === 0) return op.value;
383
+ const [parent, key] = navigate(doc, parts);
384
+ if (Array.isArray(parent)) {
385
+ if (key === "-") {
386
+ parent.push(op.value);
387
+ } else {
388
+ const idx = key;
389
+ if (idx < 0 || idx > parent.length) {
390
+ throw new PatchError(`Index out of range for add: ${path}`);
391
+ }
392
+ parent.splice(idx, 0, op.value);
393
+ }
394
+ } else if (parent !== null && typeof parent === "object") {
395
+ parent[key] = op.value;
396
+ } else {
397
+ throw new PatchError(`Cannot add into ${typeof parent}`);
398
+ }
399
+ return doc;
400
+ }
401
+ if (kind === "replace") {
402
+ if (!("value" in op)) throw new PatchError("'replace' requires 'value'");
403
+ const parts = splitPath(path);
404
+ if (parts.length === 0) return op.value;
405
+ const [parent, key] = navigate(doc, parts);
406
+ if (Array.isArray(parent)) {
407
+ const idx = key;
408
+ if (idx < 0 || idx >= parent.length) {
409
+ throw new PatchError(`Path not found for replace: ${path}`);
410
+ }
411
+ parent[idx] = op.value;
412
+ } else if (parent !== null && typeof parent === "object") {
413
+ if (!Object.prototype.hasOwnProperty.call(parent, key)) {
414
+ throw new PatchError(`Path not found for replace: ${path}`);
415
+ }
416
+ parent[key] = op.value;
417
+ } else {
418
+ throw new PatchError(`Cannot replace in ${typeof parent}`);
419
+ }
420
+ return doc;
421
+ }
422
+ if (kind === "remove") {
423
+ const parts = splitPath(path);
424
+ if (parts.length === 0) throw new PatchError("Cannot remove root.");
425
+ const [parent, key] = navigate(doc, parts);
426
+ if (Array.isArray(parent)) {
427
+ const idx = key;
428
+ if (idx < 0 || idx >= parent.length) {
429
+ throw new PatchError(`Index out of range for remove: ${path}`);
430
+ }
431
+ parent.splice(idx, 1);
432
+ } else if (parent !== null && typeof parent === "object") {
433
+ if (!Object.prototype.hasOwnProperty.call(parent, key)) {
434
+ throw new PatchError(`Path not found for remove: ${path}`);
435
+ }
436
+ delete parent[key];
437
+ } else {
438
+ throw new PatchError(`Cannot remove from ${typeof parent}`);
439
+ }
440
+ return doc;
441
+ }
442
+ if (kind === "copy") {
443
+ const src = op.from;
444
+ if (src === void 0) throw new PatchError("'copy' requires 'from'");
445
+ const value = clone(getAt(doc, src));
446
+ return applyOne(doc, { op: "add", path, value });
447
+ }
448
+ if (kind === "move") {
449
+ const src = op.from;
450
+ if (src === void 0) throw new PatchError("'move' requires 'from'");
451
+ if (path.startsWith(src + "/")) {
452
+ throw new PatchError("'move' cannot move a location into its own child.");
453
+ }
454
+ const value = clone(getAt(doc, src));
455
+ doc = applyOne(doc, { op: "remove", path: src });
456
+ return applyOne(doc, { op: "add", path, value });
457
+ }
458
+ if (kind === "test") {
459
+ if (!("value" in op)) throw new PatchError("'test' requires 'value'");
460
+ const actual = getAt(doc, path);
461
+ if (!deepEqual(actual, op.value)) throw new PatchError(`'test' failed at ${path}`);
462
+ return doc;
463
+ }
464
+ throw new PatchError(`Unsupported op: ${JSON.stringify(kind)}`);
465
+ }
466
+ var MAX_PATCH_OPS = 1e3;
467
+ var MAX_DOCUMENT_NODES = 1e5;
468
+ function nodeCount(value) {
469
+ if (Array.isArray(value)) {
470
+ let n = 1;
471
+ for (const v of value) n += nodeCount(v);
472
+ return n;
473
+ }
474
+ if (value !== null && typeof value === "object") {
475
+ let n = 1;
476
+ for (const v of Object.values(value)) n += nodeCount(v);
477
+ return n;
478
+ }
479
+ return 1;
480
+ }
481
+ function applyJsonPatch(doc, ops) {
482
+ if (ops.length > MAX_PATCH_OPS) {
483
+ throw new PatchError(`Patch has too many operations (${ops.length} > ${MAX_PATCH_OPS})`);
484
+ }
485
+ let out = JSON.parse(JSON.stringify(doc));
486
+ for (const op of ops) {
487
+ out = applyOne(out, op);
488
+ if (nodeCount(out) > MAX_DOCUMENT_NODES) {
489
+ throw new PatchError(`Patched document exceeds the node limit (${MAX_DOCUMENT_NODES})`);
490
+ }
491
+ }
492
+ return out;
493
+ }
494
+ function diffJsonPatch(from, to, basePath = "") {
495
+ const ops = [];
496
+ if (typeof from !== typeof to || Array.isArray(from) !== Array.isArray(to)) {
497
+ ops.push({ op: "replace", path: basePath || "/", value: to });
498
+ return ops;
499
+ }
500
+ if (from === null || to === null || typeof from !== "object") {
501
+ if (from !== to) {
502
+ ops.push({ op: "replace", path: basePath || "/", value: to });
503
+ }
504
+ return ops;
505
+ }
506
+ if (Array.isArray(from) && Array.isArray(to)) {
507
+ if (JSON.stringify(from) !== JSON.stringify(to)) {
508
+ ops.push({ op: "replace", path: basePath || "/", value: to });
509
+ }
510
+ return ops;
511
+ }
512
+ const fromObj = from;
513
+ const toObj = to;
514
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(fromObj), ...Object.keys(toObj)]);
515
+ for (const key of allKeys) {
516
+ const escaped = key.replace(/~/g, "~0").replace(/\//g, "~1");
517
+ const path = `${basePath}/${escaped}`;
518
+ if (!(key in fromObj)) {
519
+ ops.push({ op: "add", path, value: toObj[key] });
520
+ } else if (!(key in toObj)) {
521
+ ops.push({ op: "remove", path });
522
+ } else {
523
+ ops.push(...diffJsonPatch(fromObj[key], toObj[key], path));
524
+ }
525
+ }
526
+ return ops;
527
+ }
528
+
529
+ // src/storage/store.ts
530
+ var MemoryStore = class {
531
+ records = /* @__PURE__ */ new Map();
532
+ load() {
533
+ return Array.from(this.records.values());
534
+ }
535
+ save(record) {
536
+ this.records.set(record.id, record);
537
+ }
538
+ delete(id) {
539
+ this.records.delete(id);
540
+ }
541
+ close() {
542
+ this.records.clear();
543
+ }
544
+ };
545
+
546
+ // src/api.ts
547
+ var CoordinatorError = class extends Error {
548
+ code;
549
+ data;
550
+ constructor(code, message, data) {
551
+ super(message);
552
+ this.name = "CoordinatorError";
553
+ this.code = code;
554
+ this.data = data;
555
+ }
556
+ };
557
+ function call(coord, method, params) {
558
+ const envelope = {
559
+ jsonrpc: "2.0",
560
+ id: coord.ids.envelopeId(),
561
+ method,
562
+ params
563
+ };
564
+ const out = coord.dispatch(envelope);
565
+ if (out.error) {
566
+ throw new CoordinatorError(out.error.code, out.error.message, out.error.data);
567
+ }
568
+ return out.result;
569
+ }
570
+ function makeApi(coord) {
571
+ const c = (m, p) => call(coord, m, p);
572
+ return {
573
+ workspace: {
574
+ create: (p) => c("workspace.create", p),
575
+ describe: (p) => c("workspace.describe", p),
576
+ set_profiles: (p) => c("workspace.set_profiles", p)
577
+ },
578
+ participant: {
579
+ join: (p) => c("participant.join", p),
580
+ leave: (p) => c("participant.leave", p),
581
+ rotate_key: (p) => c("participant.rotate_key", p),
582
+ revoke_key: (p) => c("participant.revoke_key", p)
583
+ },
584
+ task: {
585
+ create: (p) => c("task.create", p),
586
+ update: (p) => c("task.update", p),
587
+ complete: (p) => c("task.complete", p),
588
+ route: (p) => c("task.route", p)
589
+ },
590
+ review: {
591
+ request: (p) => c("review.request", p),
592
+ depth: (p) => c("review.depth", p)
593
+ },
594
+ decide: {
595
+ approve: (p) => c("decide.approve", p),
596
+ reject: (p) => c("decide.reject", p),
597
+ override: (p) => c("decide.override", p)
598
+ },
599
+ abstain: {
600
+ declare: (p) => c("abstain.declare", p)
601
+ },
602
+ escalate: {
603
+ raise: (p) => c("escalate.raise", p),
604
+ auto: (p) => c("escalate.auto", p)
605
+ },
606
+ whisper: {
607
+ ask: (p) => c("whisper.ask", p),
608
+ answer: (p) => c("whisper.answer", p)
609
+ },
610
+ deliberate: {
611
+ open: (p) => c("deliberate.open", p),
612
+ comment: (p) => c("deliberate.comment", p),
613
+ vote: (p) => c("deliberate.vote", p),
614
+ close: (p) => c("deliberate.close", p)
615
+ },
616
+ handoff: {
617
+ propose: (p) => c("handoff.propose", p),
618
+ accept: (p) => c("handoff.accept", p),
619
+ decline: (p) => c("handoff.decline", p)
620
+ },
621
+ control: {
622
+ pause: (p) => c("control.pause", p),
623
+ resume: (p) => c("control.resume", p),
624
+ cancel: (p) => c("control.cancel", p),
625
+ supersede: (p) => c("control.supersede", p),
626
+ snapshot: (p) => c("control.snapshot", p),
627
+ rollback: (p) => c("control.rollback", p),
628
+ set_mode_ceiling: (p) => c("control.set_mode_ceiling", p)
629
+ },
630
+ audit: {
631
+ read: (p) => c("audit.read", p),
632
+ submit_to_scitt: (p) => c("audit.submit_to_scitt", p),
633
+ verify_receipt: (p) => c("audit.verify_receipt", p),
634
+ verify_chain: (p) => c("audit.verify_chain", p)
635
+ }
636
+ };
637
+ }
638
+
639
+ // src/types.ts
640
+ function modeLE(a, ceiling) {
641
+ const order = { shadow: 0, trial: 1, production: 2 };
642
+ return order[a] <= order[ceiling];
643
+ }
644
+
645
+ // src/profiles/audit_scitt.ts
646
+ function buildStatement(workspaceId, envelope, issuer) {
647
+ return {
648
+ protected: {
649
+ alg: -8,
650
+ // Ed25519 per COSE
651
+ iss: issuer,
652
+ kid: "scitt-issuer",
653
+ cwt_claims: { sub: workspaceId, iat: null },
654
+ "content-type": "application/chap+json;version=0.2"
655
+ },
656
+ payload: canonicalize(envelope).toString("utf-8"),
657
+ signature: "<deployment-supplied>"
658
+ };
659
+ }
660
+ function registerAuditScitt(coord) {
661
+ coord.handlers.set("audit.submit_to_scitt", (p) => {
662
+ const ws = coord.workspaces.get(p.workspace);
663
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
664
+ const range = p.range ?? {};
665
+ const fromSeq = range.from_seq ?? 0;
666
+ const toSeq = range.to_seq ?? ws.audit.length;
667
+ const issuer = p.issuer ?? "service:coordinator";
668
+ if (!coord.options.scittSubmitter) {
669
+ const statements = ws.audit.slice(fromSeq, toSeq).map((e) => buildStatement(ws.id, e.envelope, issuer));
670
+ return { result: {
671
+ statements,
672
+ note: "No scittSubmitter configured; submit these out-of-band"
673
+ } };
674
+ }
675
+ const receipts = [];
676
+ for (const entry of ws.audit.slice(fromSeq, toSeq)) {
677
+ const statement = buildStatement(ws.id, entry.envelope, issuer);
678
+ let receipt;
679
+ try {
680
+ receipt = coord.options.scittSubmitter(statement);
681
+ } catch (e) {
682
+ const msg = e instanceof Error ? e.message : String(e);
683
+ return { error: rpcError(E.SCITT_UNREACHABLE, `SCITT submission error: ${msg}`) };
684
+ }
685
+ if (receipt === null) {
686
+ return { error: rpcError(
687
+ E.SCITT_STATEMENT_REJECTED,
688
+ `Statement rejected at seq ${entry.seq}`
689
+ ) };
690
+ }
691
+ receipts.push({ seq: entry.seq, receipt });
692
+ }
693
+ return { result: { receipts } };
694
+ });
695
+ coord.handlers.set("audit.verify_receipt", (p) => {
696
+ const receipt = p.receipt;
697
+ if (typeof receipt !== "object" || receipt === null) {
698
+ return { error: rpcError(E.PARAMS, "receipt must be an object") };
699
+ }
700
+ if (coord.options.verifyScittReceipt) {
701
+ let ok;
702
+ try {
703
+ ok = !!coord.options.verifyScittReceipt(receipt);
704
+ } catch (e) {
705
+ const msg = e instanceof Error ? e.message : String(e);
706
+ return { error: rpcError(E.SCITT_RECEIPT_INVALID, `verify error: ${msg}`) };
707
+ }
708
+ if (!ok) return { error: rpcError(E.SCITT_RECEIPT_INVALID, "Receipt did not verify") };
709
+ return { result: { verified: true } };
710
+ }
711
+ return { error: rpcError(E.SCITT_RECEIPT_INVALID, "No verifyScittReceipt hook configured; receipt not verified") };
712
+ });
713
+ coord.handlers.set("audit.verify_chain", (p) => {
714
+ const ws = coord.workspaces.get(p.workspace);
715
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
716
+ if (!(ws.chain_enabled || coord.options.enableChain)) {
717
+ return { error: rpcError(E.PARAMS, "Chain not enabled for this workspace") };
718
+ }
719
+ let start = ws.audit.findIndex((e) => e.prev_hash != null);
720
+ if (start < 0) start = ws.audit.length;
721
+ const errors = [];
722
+ let prev = ZERO_HASH;
723
+ for (const e of ws.audit.slice(start)) {
724
+ const expectedPrev = prev;
725
+ if (e.prev_hash !== expectedPrev) {
726
+ errors.push(`seq ${e.seq}: prev_hash mismatch`);
727
+ }
728
+ prev = sha256Hex(Buffer.concat([canonicalize(e.envelope), Buffer.from(expectedPrev, "utf-8")]));
729
+ }
730
+ const storedHead = ws.chain_head ?? ZERO_HASH;
731
+ if (prev !== storedHead) {
732
+ errors.push("chain_head mismatch: replay does not match stored head");
733
+ }
734
+ if (errors.length) {
735
+ return { error: rpcError(E.PARAMS, errors.join("; ")) };
736
+ }
737
+ return { result: {
738
+ ok: true,
739
+ entries_checked: ws.audit.length - start,
740
+ chain_head: storedHead
741
+ } };
742
+ });
743
+ }
744
+
745
+ // src/profiles/control.ts
746
+ var VALID_MODES = /* @__PURE__ */ new Set(["shadow", "trial", "production"]);
747
+ var VALID_SCOPES = /* @__PURE__ */ new Set(["task", "participant", "workspace"]);
748
+ function registerControl(coord) {
749
+ coord.handlers.set("control.pause", (p) => {
750
+ const ws = coord.workspaces.get(p.workspace);
751
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
752
+ const scope = p.scope || "task";
753
+ if (!VALID_SCOPES.has(scope)) {
754
+ return { error: rpcError(E.PARAMS, `scope must be task/participant/workspace`) };
755
+ }
756
+ if (scope === "task") {
757
+ const task = ws.tasks.get(p.task_id);
758
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
759
+ if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
760
+ return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot pause ${task.state} task`) };
761
+ }
762
+ task.paused = true;
763
+ const prior = task.state;
764
+ task.state = "paused";
765
+ task.updated_at = coord.now();
766
+ task.history.push({ ts: task.updated_at, from: p.from, state: "paused", note: `was ${prior}` });
767
+ return { result: { scope: "task", task_id: task.id, state: "paused" } };
768
+ }
769
+ if (scope === "participant") {
770
+ const uri = p.participant_uri || p.uri;
771
+ if (!uri || !ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
772
+ ws.members.get(uri).paused = true;
773
+ return { result: {
774
+ scope: "participant",
775
+ participant_uri: uri,
776
+ paused: true,
777
+ in_flight_policy: p.in_flight_policy || "allow_to_complete"
778
+ } };
779
+ }
780
+ ws.state = "paused";
781
+ return { result: { scope: "workspace", workspace: ws.id, state: "paused" } };
782
+ });
783
+ coord.handlers.set("control.resume", (p) => {
784
+ const ws = coord.workspaces.get(p.workspace);
785
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
786
+ const scope = p.scope || "task";
787
+ if (!VALID_SCOPES.has(scope)) {
788
+ return { error: rpcError(E.PARAMS, `scope must be task/participant/workspace`) };
789
+ }
790
+ if (scope === "task") {
791
+ const task = ws.tasks.get(p.task_id);
792
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
793
+ if (task.state !== "paused") {
794
+ return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot resume ${task.state} task`) };
795
+ }
796
+ task.paused = false;
797
+ task.state = "in_progress";
798
+ task.updated_at = coord.now();
799
+ task.history.push({ ts: task.updated_at, from: p.from, state: "in_progress" });
800
+ return { result: { scope: "task", task_id: task.id, state: "in_progress" } };
801
+ }
802
+ if (scope === "participant") {
803
+ const uri = p.participant_uri || p.uri;
804
+ if (!uri || !ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
805
+ ws.members.get(uri).paused = false;
806
+ return { result: { scope: "participant", participant_uri: uri, paused: false } };
807
+ }
808
+ ws.state = "active";
809
+ return { result: { scope: "workspace", workspace: ws.id, state: "active" } };
810
+ });
811
+ coord.handlers.set("control.cancel", (p) => {
812
+ const ws = coord.workspaces.get(p.workspace);
813
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
814
+ const task = ws.tasks.get(p.task_id);
815
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
816
+ if (task.state === "completed" || task.state === "declined" || task.state === "cancelled") {
817
+ return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot cancel ${task.state} task`) };
818
+ }
819
+ task.state = "cancelled";
820
+ task.updated_at = coord.now();
821
+ task.history.push({ ts: task.updated_at, from: p.from, state: "cancelled", note: p.reason });
822
+ return { result: { state: "cancelled" } };
823
+ });
824
+ coord.handlers.set("control.snapshot", (p) => {
825
+ const ws = coord.workspaces.get(p.workspace);
826
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
827
+ const include = p.include ?? ["members", "open_tasks", "mode_ceiling"];
828
+ const state = {};
829
+ if (include.includes("members")) {
830
+ state.members = Array.from(ws.members.values()).map((m) => ({
831
+ uri: m.uri,
832
+ type: m.type,
833
+ role: m.role,
834
+ scopes: m.scopes
835
+ }));
836
+ }
837
+ if (include.includes("open_tasks")) {
838
+ state.open_tasks = Array.from(ws.tasks.values()).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled").map((t) => ({ id: t.id, kind: t.kind, state: t.state, assignee: t.assignee }));
839
+ }
840
+ if (include.includes("mode_ceiling")) state.mode_ceiling = ws.mode_ceiling;
841
+ if (include.includes("policy")) state.routing_policy_uri = ws.routing_policy_uri;
842
+ if (include.includes("audit") || include.includes("all")) state.audit_seq = ws.audit.length;
843
+ const snapId = coord.ids.artefactId();
844
+ const snap = {
845
+ id: snapId,
846
+ kind: "snapshot",
847
+ ts: coord.now(),
848
+ by: p.from,
849
+ workspace: ws.id,
850
+ audit_seq: ws.audit.length,
851
+ label: p.label,
852
+ include,
853
+ state
854
+ };
855
+ ws.snapshots.set(snapId, snap);
856
+ return { result: { snapshot_artefact_id: snapId, audit_seq: snap.audit_seq, artefact: snap } };
857
+ });
858
+ coord.handlers.set("control.rollback", (p) => {
859
+ const ws = coord.workspaces.get(p.workspace);
860
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
861
+ const snapId = p.to_snapshot_artefact_id || p.snapshot_artefact_id || p.snapshot_id;
862
+ if (!snapId) return { error: rpcError(E.PARAMS, "to_snapshot_artefact_id is required") };
863
+ const snap = ws.snapshots.get(snapId);
864
+ if (!snap) return { error: rpcError(E.CONTROL_SNAPSHOT_NOT_FOUND, `Unknown snapshot: ${snapId}`) };
865
+ const what = p.what_to_restore ?? snap.include;
866
+ const restored = [];
867
+ if (what.includes("mode_ceiling") && snap.state.mode_ceiling) {
868
+ ws.mode_ceiling = snap.state.mode_ceiling;
869
+ restored.push("mode_ceiling");
870
+ }
871
+ if (what.includes("members") && Array.isArray(snap.state.members)) {
872
+ const snapByUri = /* @__PURE__ */ new Map();
873
+ for (const m of snap.state.members) {
874
+ snapByUri.set(m.uri, m);
875
+ }
876
+ for (const [uri, snapM] of snapByUri) {
877
+ if (ws.members.has(uri)) {
878
+ const cur = ws.members.get(uri);
879
+ cur.role = snapM.role ?? cur.role;
880
+ cur.scopes = snapM.scopes;
881
+ }
882
+ }
883
+ restored.push("members");
884
+ }
885
+ return { result: {
886
+ rolled_back_to: snapId,
887
+ audit_seq: snap.audit_seq,
888
+ restored,
889
+ reason: p.reason
890
+ } };
891
+ });
892
+ coord.handlers.set("control.supersede", (p) => {
893
+ const ws = coord.workspaces.get(p.workspace);
894
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
895
+ const oldId = p.task_id;
896
+ const old = ws.tasks.get(oldId);
897
+ if (!old) return { error: rpcError(E.PARAMS, "Unknown task") };
898
+ const newSpec = p.successor_task ?? {};
899
+ if (!newSpec.kind) {
900
+ return { error: rpcError(E.PARAMS, "successor_task must include kind") };
901
+ }
902
+ const assignee = newSpec.assignee || old.assignee;
903
+ if (!ws.members.has(assignee)) {
904
+ return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
905
+ }
906
+ const now = coord.now();
907
+ const newId = coord.ids.taskId();
908
+ const newTask = {
909
+ id: newId,
910
+ kind: newSpec.kind,
911
+ state: "created",
912
+ assignee,
913
+ delegator: p.from,
914
+ input: newSpec.input ?? {},
915
+ created_at: now,
916
+ updated_at: now,
917
+ mode: newSpec.mode ?? old.mode,
918
+ supersedes: old.id,
919
+ history: [{
920
+ ts: now,
921
+ from: p.from,
922
+ state: "created",
923
+ note: `supersedes ${old.id}: ${p.reason || ""}`
924
+ }],
925
+ paused: false
926
+ };
927
+ ws.tasks.set(newId, newTask);
928
+ old.state = "superseded";
929
+ old.superseded_by = newId;
930
+ old.updated_at = now;
931
+ old.history.push({
932
+ ts: now,
933
+ from: p.from,
934
+ state: "superseded",
935
+ note: `-> ${newId}: ${p.reason || ""}`
936
+ });
937
+ return { result: { superseded_task_id: old.id, new_task_id: newId, reason: p.reason } };
938
+ });
939
+ coord.handlers.set("control.set_mode_ceiling", (p) => {
940
+ const ws = coord.workspaces.get(p.workspace);
941
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
942
+ const m = p.new_ceiling ?? p.mode_ceiling;
943
+ if (!VALID_MODES.has(m)) {
944
+ return { error: rpcError(E.PARAMS, `new_ceiling must be shadow/trial/production`) };
945
+ }
946
+ ws.mode_ceiling = m;
947
+ return { result: { mode_ceiling: ws.mode_ceiling, reason: p.reason } };
948
+ });
949
+ }
950
+
951
+ // src/profiles/deliberation.ts
952
+ function parseRule(rule) {
953
+ if (rule.includes(":")) {
954
+ const [kind, arg] = rule.split(":", 2);
955
+ if (kind === "quorum") return { kind, params: { n: parseInt(arg, 10) } };
956
+ if (kind === "weighted_vote" || kind === "weighted_vote_with_veto") {
957
+ return { kind, params: { threshold: parseFloat(arg) } };
958
+ }
959
+ }
960
+ if (rule === "any_one_approves" || rule === "all_approve") return { kind: rule, params: {} };
961
+ throw new Error(`Unknown rule: ${rule}`);
962
+ }
963
+ function computeOutcome(delib) {
964
+ const parsed = parseRule(delib.rule);
965
+ const yea = delib.votes.filter((v) => v.vote === "yea");
966
+ const nay = delib.votes.filter((v) => v.vote === "nay");
967
+ const vetoes = delib.votes.filter((v) => v.veto_invoked && (delib.veto ?? {})[v.voter]);
968
+ const weights = delib.weights ?? {};
969
+ const weighted = (vs) => vs.reduce((s, v) => s + (weights[v.voter] ?? 1), 0);
970
+ if (parsed.kind === "any_one_approves") {
971
+ return {
972
+ outcome: yea.length >= 1 ? "approved" : "rejected",
973
+ rule: delib.rule,
974
+ tally: { yea: yea.length, nay: nay.length },
975
+ vetoes: vetoes.map((v) => v.voter)
976
+ };
977
+ }
978
+ if (parsed.kind === "all_approve") {
979
+ const passed = yea.length === delib.participants.length && nay.length === 0;
980
+ return {
981
+ outcome: passed ? "approved" : "rejected",
982
+ rule: delib.rule,
983
+ tally: { yea: yea.length, nay: nay.length },
984
+ vetoes: vetoes.map((v) => v.voter)
985
+ };
986
+ }
987
+ if (parsed.kind === "quorum") {
988
+ const n = parsed.params.n;
989
+ const cast = yea.length + nay.length;
990
+ if (cast < n) {
991
+ return {
992
+ outcome: "rejected",
993
+ reason: "quorum not met",
994
+ rule: delib.rule,
995
+ tally: { yea: yea.length, nay: nay.length },
996
+ vetoes: []
997
+ };
998
+ }
999
+ return {
1000
+ outcome: yea.length > nay.length ? "approved" : "rejected",
1001
+ rule: delib.rule,
1002
+ tally: { yea: yea.length, nay: nay.length },
1003
+ vetoes: []
1004
+ };
1005
+ }
1006
+ if (parsed.kind === "weighted_vote") {
1007
+ const yeaW = weighted(yea), nayW = weighted(nay);
1008
+ return {
1009
+ outcome: yeaW >= parsed.params.threshold ? "approved" : "rejected",
1010
+ rule: delib.rule,
1011
+ tally: { yea: yeaW, nay: nayW },
1012
+ vetoes: []
1013
+ };
1014
+ }
1015
+ if (parsed.kind === "weighted_vote_with_veto") {
1016
+ if (vetoes.length) {
1017
+ return {
1018
+ outcome: "rejected",
1019
+ rule: delib.rule,
1020
+ tally: { yea: 0, nay: 0 },
1021
+ vetoes: vetoes.map((v) => v.voter)
1022
+ };
1023
+ }
1024
+ const yeaW = weighted(yea), nayW = weighted(nay);
1025
+ return {
1026
+ outcome: yeaW >= parsed.params.threshold ? "approved" : "rejected",
1027
+ rule: delib.rule,
1028
+ tally: { yea: yeaW, nay: nayW },
1029
+ vetoes: []
1030
+ };
1031
+ }
1032
+ return { outcome: "error", rule: delib.rule, reason: `unsupported rule ${parsed.kind}` };
1033
+ }
1034
+ function registerDeliberation(coord) {
1035
+ coord.handlers.set("deliberate.open", (p) => {
1036
+ const ws = coord.workspaces.get(p.workspace);
1037
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1038
+ for (const f of ["from", "rule"]) {
1039
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1040
+ }
1041
+ const to = p.to ?? p.participants;
1042
+ const participants = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
1043
+ if (!participants.length) {
1044
+ return { error: rpcError(E.PARAMS, "deliberate.open needs 'to' (participants list)") };
1045
+ }
1046
+ try {
1047
+ parseRule(p.rule);
1048
+ } catch (e) {
1049
+ const msg = e instanceof Error ? e.message : String(e);
1050
+ return { error: rpcError(E.DELIB_UNKNOWN_RULE, msg) };
1051
+ }
1052
+ const id = p.deliberation_id || coord.ids.deliberationId();
1053
+ const delib = {
1054
+ id,
1055
+ task_id: p.task_id,
1056
+ opener: p.from,
1057
+ participants,
1058
+ rule: p.rule,
1059
+ question: p.question,
1060
+ weights: p.weights,
1061
+ veto: p.veto,
1062
+ deadline: p.deadline,
1063
+ state: "open",
1064
+ comments: [],
1065
+ votes: [],
1066
+ opened_at: coord.now()
1067
+ };
1068
+ ws.deliberations.set(id, delib);
1069
+ return { result: { deliberation_id: id, state: "open" } };
1070
+ });
1071
+ coord.handlers.set("deliberate.comment", (p) => {
1072
+ const ws = coord.workspaces.get(p.workspace);
1073
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1074
+ const delib = ws.deliberations.get(p.deliberation_id);
1075
+ if (!delib) return { error: rpcError(E.PARAMS, "Unknown deliberation") };
1076
+ if (delib.state !== "open") {
1077
+ return { error: rpcError(E.DELIB_CLOSED_OR_LAPSED, "Deliberation not open") };
1078
+ }
1079
+ delib.comments.push({
1080
+ ts: coord.now(),
1081
+ from: p.from,
1082
+ text: p.comment || p.text || ""
1083
+ });
1084
+ return { result: { accepted: true } };
1085
+ });
1086
+ coord.handlers.set("deliberate.vote", (p) => {
1087
+ const ws = coord.workspaces.get(p.workspace);
1088
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1089
+ const delib = ws.deliberations.get(p.deliberation_id);
1090
+ if (!delib) return { error: rpcError(E.PARAMS, "Unknown deliberation") };
1091
+ if (delib.state !== "open") {
1092
+ return { error: rpcError(E.DELIB_CLOSED_OR_LAPSED, "Deliberation not open") };
1093
+ }
1094
+ for (const f of ["from", "vote"]) {
1095
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1096
+ }
1097
+ const v = p.vote;
1098
+ if (v !== "yea" && v !== "nay" && v !== "abstain") {
1099
+ return { error: rpcError(E.PARAMS, "vote must be yea/nay/abstain") };
1100
+ }
1101
+ const voter = p.from;
1102
+ if (!delib.participants.includes(voter)) {
1103
+ return { error: rpcError(
1104
+ E.DELIB_VOTER_NOT_IN_LIST,
1105
+ `Voter ${voter} not in participant list`
1106
+ ) };
1107
+ }
1108
+ if (delib.votes.some((x) => x.voter === voter)) {
1109
+ return { error: rpcError(E.DELIB_ALREADY_VOTED, `Voter ${voter} has already voted`) };
1110
+ }
1111
+ delib.votes.push({
1112
+ ts: coord.now(),
1113
+ voter,
1114
+ vote: v,
1115
+ weight: p.weight,
1116
+ comment: p.comment,
1117
+ veto_invoked: !!p.veto_invoked
1118
+ });
1119
+ return { result: { recorded: true } };
1120
+ });
1121
+ coord.handlers.set("deliberate.close", (p) => {
1122
+ const ws = coord.workspaces.get(p.workspace);
1123
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1124
+ const delib = ws.deliberations.get(p.deliberation_id);
1125
+ if (!delib) return { error: rpcError(E.PARAMS, "Unknown deliberation") };
1126
+ if (delib.state === "closed") return { result: delib.outcome };
1127
+ delib.state = "closed";
1128
+ delib.outcome = computeOutcome(delib);
1129
+ return { result: delib.outcome };
1130
+ });
1131
+ }
1132
+
1133
+ // src/profiles/handoff.ts
1134
+ function registerHandoff(coord) {
1135
+ coord.handlers.set("handoff.propose", (p) => {
1136
+ const ws = coord.workspaces.get(p.workspace);
1137
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1138
+ for (const f of ["from", "to", "tasks"]) {
1139
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1140
+ }
1141
+ const proposer = p.from;
1142
+ const recipient = p.to;
1143
+ if (typeof recipient === "string" && !recipient.startsWith("group:")) {
1144
+ if (!ws.members.has(recipient)) {
1145
+ return { error: rpcError(
1146
+ E.HANDOFF_RECIPIENT_NOT_MEMBER,
1147
+ `Recipient ${recipient} is not a member`
1148
+ ) };
1149
+ }
1150
+ }
1151
+ const tasksIn = p.tasks;
1152
+ if (!Array.isArray(tasksIn) || !tasksIn.length) {
1153
+ return { error: rpcError(E.PARAMS, "tasks must be a non-empty list") };
1154
+ }
1155
+ const proposed = [];
1156
+ for (const t of tasksIn) {
1157
+ const tid = t.task_id;
1158
+ if (!tid || !ws.tasks.has(tid)) {
1159
+ return { error: rpcError(E.PARAMS, `Unknown task in handoff: ${tid}`) };
1160
+ }
1161
+ const task = ws.tasks.get(tid);
1162
+ if (task.assignee !== proposer) {
1163
+ return { error: rpcError(
1164
+ E.HANDOFF_TASKS_NOT_ASSIGNED_TO_PROPOSER,
1165
+ `Task ${tid} is not currently assigned to ${proposer}`
1166
+ ) };
1167
+ }
1168
+ proposed.push({
1169
+ task_id: tid,
1170
+ title: t.title,
1171
+ status_summary: t.status_summary,
1172
+ next_action: t.next_action,
1173
+ blockers: t.blockers ?? []
1174
+ });
1175
+ }
1176
+ const id = p.handoff_id || coord.ids.handoffId();
1177
+ const ho = {
1178
+ id,
1179
+ proposer,
1180
+ recipient,
1181
+ proposed_at: coord.now(),
1182
+ tasks: proposed,
1183
+ summary: p.summary,
1184
+ context_links: p.context_links ?? [],
1185
+ state: "proposed"
1186
+ };
1187
+ ws.handoffs.set(id, ho);
1188
+ return { result: { handoff_id: id, state: "proposed", task_ids: proposed.map((t) => t.task_id) } };
1189
+ });
1190
+ coord.handlers.set("handoff.accept", (p) => {
1191
+ const ws = coord.workspaces.get(p.workspace);
1192
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1193
+ for (const f of ["from", "handoff_id"]) {
1194
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1195
+ }
1196
+ const ho = ws.handoffs.get(p.handoff_id);
1197
+ if (!ho) return { error: rpcError(E.PARAMS, "Unknown handoff") };
1198
+ if (ho.state !== "proposed") {
1199
+ return { error: rpcError(E.HANDOFF_ALREADY_RESOLVED, `Handoff already ${ho.state}`) };
1200
+ }
1201
+ const acceptor = p.from;
1202
+ if (ho.recipient.startsWith("group:")) {
1203
+ if (!ws.members.has(acceptor)) {
1204
+ return { error: rpcError(
1205
+ E.HANDOFF_RECIPIENT_NOT_MEMBER,
1206
+ "Acceptor is not a workspace member"
1207
+ ) };
1208
+ }
1209
+ } else {
1210
+ if (acceptor !== ho.recipient) {
1211
+ return { error: rpcError(
1212
+ E.HANDOFF_RECIPIENT_NOT_MEMBER,
1213
+ "Acceptor is not the named recipient"
1214
+ ) };
1215
+ }
1216
+ if (!ws.members.has(acceptor)) {
1217
+ return { error: rpcError(
1218
+ E.HANDOFF_RECIPIENT_NOT_MEMBER,
1219
+ "Acceptor is not a workspace member"
1220
+ ) };
1221
+ }
1222
+ }
1223
+ const validIds = new Set(ho.tasks.map((t) => t.task_id));
1224
+ const acceptedIds = Array.isArray(p.accepted_task_ids) ? p.accepted_task_ids : ho.tasks.map((t) => t.task_id);
1225
+ for (const tid of acceptedIds) {
1226
+ if (!validIds.has(tid)) {
1227
+ return { error: rpcError(E.PARAMS, `Task ${tid} not in this handoff`) };
1228
+ }
1229
+ }
1230
+ const now = coord.now();
1231
+ for (const tid of acceptedIds) {
1232
+ const task = ws.tasks.get(tid);
1233
+ task.assignee = acceptor;
1234
+ task.updated_at = now;
1235
+ task.history.push({
1236
+ ts: now,
1237
+ from: acceptor,
1238
+ state: task.state,
1239
+ note: `handoff accepted from ${ho.proposer}`
1240
+ });
1241
+ }
1242
+ ho.state = "accepted";
1243
+ ho.accepted_by = acceptor;
1244
+ ho.accepted_task_ids = [...acceptedIds];
1245
+ ho.accept_comment = p.comment;
1246
+ ho.resolved_at = now;
1247
+ return { result: { accepted: true, task_ids: [...acceptedIds], assignee: acceptor } };
1248
+ });
1249
+ coord.handlers.set("handoff.decline", (p) => {
1250
+ const ws = coord.workspaces.get(p.workspace);
1251
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1252
+ for (const f of ["from", "handoff_id"]) {
1253
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1254
+ }
1255
+ const ho = ws.handoffs.get(p.handoff_id);
1256
+ if (!ho) return { error: rpcError(E.PARAMS, "Unknown handoff") };
1257
+ if (ho.state !== "proposed") {
1258
+ return { error: rpcError(E.HANDOFF_ALREADY_RESOLVED, `Handoff already ${ho.state}`) };
1259
+ }
1260
+ const decliner = p.from;
1261
+ ho.decline_reason = p.reason;
1262
+ ho.decline_suggested_target = p.suggested_target;
1263
+ if (!ho.recipient.startsWith("group:") && decliner === ho.recipient) {
1264
+ ho.state = "declined";
1265
+ ho.resolved_at = coord.now();
1266
+ }
1267
+ return { result: { recorded: true, state: ho.state } };
1268
+ });
1269
+ }
1270
+
1271
+ // src/profiles/routing.ts
1272
+ var CRIT = { low: 0, medium: 1, high: 2, critical: 3 };
1273
+ function defaultDepth(hints) {
1274
+ const crit = hints.criticality ?? "medium";
1275
+ let conf = 1;
1276
+ try {
1277
+ conf = Number(hints.confidence ?? 1);
1278
+ } catch {
1279
+ conf = 1;
1280
+ }
1281
+ if (isNaN(conf)) conf = 1;
1282
+ const r = CRIT[crit] ?? 1;
1283
+ if (r >= 3) return { depth: "full", summary: "criticality=critical: full review" };
1284
+ if (r === 2 && conf < 0.7) return { depth: "full", summary: "criticality=high and confidence<0.7: full review" };
1285
+ if (r === 0 && conf >= 0.95) return { depth: "skip", summary: "very low criticality + very high confidence: skip" };
1286
+ if (r <= 1 && conf >= 0.85) return { depth: "spot_check", sampling: 0.1, summary: "low criticality + high confidence: 10% sample" };
1287
+ return { depth: "spot_check", sampling: 0.25, summary: "default mid-confidence: 25% sample" };
1288
+ }
1289
+ function registerRouting(coord) {
1290
+ coord.handlers.set("task.route", (p) => {
1291
+ const ws = coord.workspaces.get(p.workspace);
1292
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1293
+ const task = ws.tasks.get(p.task_id);
1294
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
1295
+ const candidates = p.candidates ?? [];
1296
+ if (!candidates.length) {
1297
+ return { error: rpcError(E.ROUTING_CANDIDATES_EMPTY, "candidates array was empty") };
1298
+ }
1299
+ let selected;
1300
+ let rationale;
1301
+ if (coord.options.routingPolicy) {
1302
+ try {
1303
+ const d = coord.options.routingPolicy(task, [...candidates]);
1304
+ selected = d.selected;
1305
+ rationale = d.rationale;
1306
+ } catch (e) {
1307
+ const msg = e instanceof Error ? e.message : String(e);
1308
+ return { error: rpcError(E.ROUTING_POLICY_UNREACHABLE, `routing policy error: ${msg}`) };
1309
+ }
1310
+ } else {
1311
+ const eligible = candidates.filter((c) => ws.members.has(c));
1312
+ if (!eligible.length) {
1313
+ return { error: rpcError(E.ROUTING_NO_ELIGIBLE_ASSIGNEE, "No candidate is a workspace member") };
1314
+ }
1315
+ selected = eligible[0];
1316
+ rationale = {
1317
+ policy_id: "default",
1318
+ hints_used: Object.keys(task.routing_hints ?? {}),
1319
+ summary: "default policy: first eligible candidate",
1320
+ alternatives_considered: eligible.slice(1).map((c) => ({
1321
+ candidate: c,
1322
+ reason_excluded: "not first eligible"
1323
+ }))
1324
+ };
1325
+ }
1326
+ if (!ws.members.has(selected)) {
1327
+ return { error: rpcError(
1328
+ E.ROUTING_NO_ELIGIBLE_ASSIGNEE,
1329
+ `Selected ${JSON.stringify(selected)} is not a member`
1330
+ ) };
1331
+ }
1332
+ task.assignee = selected;
1333
+ task.updated_at = coord.now();
1334
+ task.history.push({
1335
+ ts: task.updated_at,
1336
+ from: "service:coordinator",
1337
+ state: task.state,
1338
+ note: `routed to ${selected}`
1339
+ });
1340
+ const artId = coord.ids.artefactId();
1341
+ const artefact = {
1342
+ id: artId,
1343
+ kind: "route_decision",
1344
+ decision_type: "task.route",
1345
+ outcome: selected,
1346
+ produced_by: "service:coordinator",
1347
+ produced_at: task.updated_at,
1348
+ task: task.id,
1349
+ policy_id: rationale.policy_id,
1350
+ hints_observed: { ...task.routing_hints ?? {} },
1351
+ rationale: rationale.summary ?? "",
1352
+ alternatives_considered: rationale.alternatives_considered
1353
+ };
1354
+ ws.route_decisions.set(artId, artefact);
1355
+ return { result: { selected, decision_artefact: artId, rationale } };
1356
+ });
1357
+ coord.handlers.set("review.depth", (p) => {
1358
+ const ws = coord.workspaces.get(p.workspace);
1359
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1360
+ const task = ws.tasks.get(p.task_id);
1361
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
1362
+ const hints = {
1363
+ ...task.routing_hints ?? {},
1364
+ ...p.artefact_routing_hints ?? {}
1365
+ };
1366
+ if (!Object.keys(hints).length) {
1367
+ return { error: rpcError(E.ROUTING_DEPTH_NOT_APPLICABLE, "No routing_hints to consult") };
1368
+ }
1369
+ let depth;
1370
+ let sampling;
1371
+ let summary;
1372
+ let policyId;
1373
+ if (coord.options.reviewDepthPolicy) {
1374
+ try {
1375
+ const d = coord.options.reviewDepthPolicy(task, hints);
1376
+ depth = d.depth;
1377
+ sampling = d.sampling_probability;
1378
+ summary = d.rationale.summary ?? "operator policy";
1379
+ policyId = d.rationale.policy_id ?? "operator";
1380
+ } catch (e) {
1381
+ const msg = e instanceof Error ? e.message : String(e);
1382
+ return { error: rpcError(E.ROUTING_POLICY_UNREACHABLE, `depth policy error: ${msg}`) };
1383
+ }
1384
+ } else {
1385
+ const d = defaultDepth(hints);
1386
+ depth = d.depth;
1387
+ sampling = d.sampling;
1388
+ summary = d.summary;
1389
+ policyId = "default";
1390
+ }
1391
+ const artId = coord.ids.artefactId();
1392
+ const artefact = {
1393
+ id: artId,
1394
+ kind: "route_decision",
1395
+ decision_type: "review.depth",
1396
+ outcome: depth,
1397
+ produced_by: "service:coordinator",
1398
+ produced_at: coord.now(),
1399
+ task: task.id,
1400
+ policy_id: policyId,
1401
+ hints_observed: hints,
1402
+ rationale: summary,
1403
+ ...sampling !== void 0 ? { sampling_probability: sampling } : {}
1404
+ };
1405
+ ws.route_decisions.set(artId, artefact);
1406
+ const result = {
1407
+ depth,
1408
+ decision_artefact: artId,
1409
+ rationale: { policy_id: policyId, hints_used: Object.keys(hints), summary }
1410
+ };
1411
+ if (sampling !== void 0) result.sampling_probability = sampling;
1412
+ return { result };
1413
+ });
1414
+ coord.handlers.set("escalate.auto", (p) => {
1415
+ const ws = coord.workspaces.get(p.workspace);
1416
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1417
+ const task = ws.tasks.get(p.task_id);
1418
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
1419
+ const hints = { ...task.routing_hints ?? {} };
1420
+ let escalate, to, rule;
1421
+ if (coord.options.escalationPolicy) {
1422
+ try {
1423
+ const d = coord.options.escalationPolicy(task, hints);
1424
+ escalate = !!d.escalate;
1425
+ to = d.to;
1426
+ rule = d.triggered_rule ?? {};
1427
+ } catch (e) {
1428
+ const msg = e instanceof Error ? e.message : String(e);
1429
+ return { error: rpcError(E.ROUTING_POLICY_UNREACHABLE, `escalation policy error: ${msg}`) };
1430
+ }
1431
+ } else {
1432
+ const crit = hints.criticality ?? "medium";
1433
+ let conf = 1;
1434
+ try {
1435
+ conf = Number(hints.confidence ?? 1);
1436
+ } catch {
1437
+ conf = 1;
1438
+ }
1439
+ if (isNaN(conf)) conf = 1;
1440
+ const r = CRIT[crit] ?? 1;
1441
+ escalate = r >= 3 || r >= 2 && conf < 0.6;
1442
+ to = p.default_escalation_target;
1443
+ rule = {
1444
+ rule_id: "default-auto-esc",
1445
+ summary: "criticality=critical OR (criticality>=high AND confidence<0.6)",
1446
+ hints_used: ["criticality", "confidence"]
1447
+ };
1448
+ }
1449
+ const artId = coord.ids.artefactId();
1450
+ const artefact = {
1451
+ id: artId,
1452
+ kind: "route_decision",
1453
+ decision_type: "escalate.auto",
1454
+ outcome: escalate ? { escalate: true, to } : { escalate: false },
1455
+ produced_by: "service:coordinator",
1456
+ produced_at: coord.now(),
1457
+ task: task.id,
1458
+ policy_id: rule.rule_id,
1459
+ hints_observed: hints,
1460
+ rationale: rule.summary ?? "",
1461
+ ...escalate ? { escalation_target: to } : {}
1462
+ };
1463
+ ws.route_decisions.set(artId, artefact);
1464
+ if (escalate) {
1465
+ if (!to) return { error: rpcError(
1466
+ E.ROUTING_ESC_TARGET_UNAVAILABLE,
1467
+ "Escalation triggered but no target available"
1468
+ ) };
1469
+ if (!to.startsWith("group:") && !ws.members.has(to)) {
1470
+ return { error: rpcError(
1471
+ E.ROUTING_ESC_TARGET_UNAVAILABLE,
1472
+ `Escalation target ${to} is not a member or group`
1473
+ ) };
1474
+ }
1475
+ if (coord.options.onAutoEscalate) {
1476
+ try {
1477
+ coord.options.onAutoEscalate(task, to);
1478
+ } catch {
1479
+ }
1480
+ }
1481
+ return { result: { escalate: true, to, decision_artefact: artId, triggered_rule: rule } };
1482
+ }
1483
+ return { result: { escalate: false, decision_artefact: artId } };
1484
+ });
1485
+ }
1486
+
1487
+ // src/profiles/security_signed.ts
1488
+ function registerSecuritySigned(coord) {
1489
+ coord.handlers.set("participant.rotate_key", (p) => {
1490
+ const ws = coord.workspaces.get(p.workspace);
1491
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1492
+ for (const f of ["from", "old_kid", "new_jwk"]) {
1493
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1494
+ }
1495
+ const member = ws.members.get(p.from);
1496
+ if (!member) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `Unknown participant: ${p.from}`) };
1497
+ const oldKid = p.old_kid;
1498
+ const oldKey = member.keys.find((k) => k.kid === oldKid);
1499
+ if (!oldKey) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `No key ${oldKid} for ${p.from}`) };
1500
+ if (oldKey.revoked_at) return { error: rpcError(E.SIG_KEY_REVOKED, `Key ${oldKid} is revoked`) };
1501
+ const newJwk = p.new_jwk;
1502
+ if (!newJwk || typeof newJwk.kid !== "string") {
1503
+ return { error: rpcError(E.PARAMS, "new_jwk must include kid") };
1504
+ }
1505
+ const now = coord.now();
1506
+ oldKey.valid_until = now;
1507
+ member.keys.push({
1508
+ jwk: newJwk,
1509
+ kid: newJwk.kid,
1510
+ valid_from: now
1511
+ });
1512
+ return { result: { rotated: true, old_kid: oldKid, new_kid: newJwk.kid, valid_from: now } };
1513
+ });
1514
+ coord.handlers.set("participant.revoke_key", (p) => {
1515
+ const ws = coord.workspaces.get(p.workspace);
1516
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1517
+ for (const f of ["target_uri", "kid"]) {
1518
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1519
+ }
1520
+ const member = ws.members.get(p.target_uri);
1521
+ if (!member) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `Unknown target: ${p.target_uri}`) };
1522
+ const key = member.keys.find((k) => k.kid === p.kid);
1523
+ if (!key) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `No key ${p.kid} for target`) };
1524
+ const now = coord.now();
1525
+ key.revoked_at = now;
1526
+ key.revoked_reason = p.reason || "unspecified";
1527
+ return { result: { revoked: true, kid: key.kid, revoked_at: now, reason: key.revoked_reason } };
1528
+ });
1529
+ }
1530
+
1531
+ // src/profiles/whisper.ts
1532
+ function parseIso(ts) {
1533
+ return new Date(ts).getTime();
1534
+ }
1535
+ function registerWhisper(coord) {
1536
+ coord.handlers.set("whisper.ask", (p) => {
1537
+ const ws = coord.workspaces.get(p.workspace);
1538
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1539
+ for (const f of ["from", "to", "task_id", "question", "deadline_ms", "default_if_lapsed"]) {
1540
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1541
+ }
1542
+ if (!ws.tasks.has(p.task_id)) {
1543
+ return { error: rpcError(E.PARAMS, "Unknown task") };
1544
+ }
1545
+ const to = p.to;
1546
+ const askee = Array.isArray(to) ? to : [to];
1547
+ const id = p.whisper_id || coord.ids.artefactId();
1548
+ const prompt = {
1549
+ id,
1550
+ task_id: p.task_id,
1551
+ asker: p.from,
1552
+ askee,
1553
+ question: p.question,
1554
+ options: p.options,
1555
+ asked_at: coord.now(),
1556
+ deadline_ms: Number(p.deadline_ms),
1557
+ default_if_lapsed: p.default_if_lapsed,
1558
+ urgency: p.urgency || "low",
1559
+ state: "pending"
1560
+ };
1561
+ ws.whispers.set(id, prompt);
1562
+ return { result: { whisper_id: id, deadline_ms: prompt.deadline_ms } };
1563
+ });
1564
+ coord.handlers.set("whisper.answer", (p) => {
1565
+ const ws = coord.workspaces.get(p.workspace);
1566
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
1567
+ for (const f of ["from", "whisper_id"]) {
1568
+ if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
1569
+ }
1570
+ const prompt = ws.whispers.get(p.whisper_id);
1571
+ if (!prompt) return { error: rpcError(E.PARAMS, "Unknown whisper id") };
1572
+ const answerer = p.from;
1573
+ const broadcast = prompt.askee.some((a) => typeof a === "string" && (a.startsWith("workspace:") || a.startsWith("group:")));
1574
+ if (broadcast) {
1575
+ if (!ws.members.has(answerer)) {
1576
+ return { error: rpcError(E.NOT_AUTHORISED, `Not a workspace member: ${answerer}`) };
1577
+ }
1578
+ } else if (!prompt.askee.includes(answerer)) {
1579
+ return { error: rpcError(E.NOT_AUTHORISED, `Whisper was not addressed to ${answerer}`) };
1580
+ }
1581
+ if (prompt.state === "answered") {
1582
+ return { error: rpcError(E.WHISPER_ALREADY_ANSWERED, "Whisper already answered") };
1583
+ }
1584
+ if (prompt.state === "lapsed") {
1585
+ return { error: rpcError(E.WHISPER_LAPSED, "Whisper already lapsed") };
1586
+ }
1587
+ const answerOption = p.answer_option;
1588
+ const answerText = p.answer ?? p.answer_text;
1589
+ if (prompt.options && prompt.options.length) {
1590
+ if (answerOption === void 0) {
1591
+ return { error: rpcError(E.PARAMS, "answer_option is required when options are defined") };
1592
+ }
1593
+ const valid = new Set(prompt.options.map((o) => o.id));
1594
+ if (!valid.has(answerOption)) {
1595
+ return { error: rpcError(
1596
+ E.WHISPER_OPTION_NOT_IN_SET,
1597
+ `Answer option ${JSON.stringify(answerOption)} not in option set`
1598
+ ) };
1599
+ }
1600
+ } else {
1601
+ if (answerText === void 0 && answerOption === void 0) {
1602
+ return { error: rpcError(E.PARAMS, "answer or answer_option required") };
1603
+ }
1604
+ }
1605
+ prompt.state = "answered";
1606
+ prompt.answered_at = coord.now();
1607
+ prompt.answered_by = p.from;
1608
+ prompt.answer_option = answerOption;
1609
+ prompt.answer_text = answerText;
1610
+ prompt.comment = p.comment;
1611
+ return { result: { answered: true, whisper_id: prompt.id } };
1612
+ });
1613
+ coord.checkWhisperLapses = (workspaceId, now) => {
1614
+ const ws = coord.workspaces.get(workspaceId);
1615
+ if (!ws) return [];
1616
+ const cutoff = parseIso(now ?? coord.now());
1617
+ const emitted = [];
1618
+ for (const prompt of ws.whispers.values()) {
1619
+ if (prompt.state !== "pending") continue;
1620
+ const asked = parseIso(prompt.asked_at);
1621
+ const deadline = asked + prompt.deadline_ms;
1622
+ if (cutoff < deadline) continue;
1623
+ prompt.state = "lapsed";
1624
+ prompt.default_applied = prompt.default_if_lapsed;
1625
+ const notify = {
1626
+ jsonrpc: "2.0",
1627
+ method: "notify.message",
1628
+ params: {
1629
+ workspace: ws.id,
1630
+ from: "service:coordinator",
1631
+ to: [prompt.asker, ...prompt.askee],
1632
+ ts: coord.now(),
1633
+ kind: "whisper_lapsed",
1634
+ whisper_id: prompt.id,
1635
+ default_applied: prompt.default_if_lapsed
1636
+ }
1637
+ };
1638
+ coord.recordAudit(ws, notify);
1639
+ emitted.push(notify);
1640
+ }
1641
+ return emitted;
1642
+ };
1643
+ }
1644
+
1645
+ // src/coordinator.ts
1646
+ var PRIVILEGED_METHODS = /* @__PURE__ */ new Set([
1647
+ "control.pause",
1648
+ "control.resume",
1649
+ "control.cancel",
1650
+ "control.supersede",
1651
+ "control.snapshot",
1652
+ "control.rollback",
1653
+ "control.set_mode_ceiling",
1654
+ "workspace.set_profiles",
1655
+ "participant.rotate_key",
1656
+ "participant.revoke_key"
1657
+ ]);
1658
+ var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
1659
+ "workspace.describe",
1660
+ "audit.read",
1661
+ "audit.verify_chain",
1662
+ "audit.verify_receipt"
1663
+ ]);
1664
+ function nowIso(clockMs) {
1665
+ const d = clockMs === void 0 ? /* @__PURE__ */ new Date() : new Date(clockMs);
1666
+ return d.toISOString().replace(/\.(\d{3})Z$/, ".$1Z");
1667
+ }
1668
+ function missing(params, fields) {
1669
+ for (const f of fields) if (!(f in params)) return f;
1670
+ return null;
1671
+ }
1672
+ function tagsError(p) {
1673
+ const tags = p.tags;
1674
+ if (tags !== void 0 && (!Array.isArray(tags) || !tags.every((t) => typeof t === "string"))) {
1675
+ return { error: rpcError(E.PARAMS, "tags must be a list of strings") };
1676
+ }
1677
+ return null;
1678
+ }
1679
+ function linkHash(envelope, prev) {
1680
+ return sha256Hex(Buffer.concat([canonicalize(envelope), Buffer.from(prev, "utf-8")]));
1681
+ }
1682
+ function reply(env, body) {
1683
+ const out = { jsonrpc: "2.0", id: env.id };
1684
+ if (body.error !== void 0) out.error = body.error;
1685
+ else out.result = body.result;
1686
+ return out;
1687
+ }
1688
+ var Coordinator = class {
1689
+ workspaces = /* @__PURE__ */ new Map();
1690
+ options;
1691
+ ids;
1692
+ store;
1693
+ wsVersions = /* @__PURE__ */ new Map();
1694
+ clockMs;
1695
+ auditListeners = [];
1696
+ /** Method handler registry; profiles plug into this. */
1697
+ handlers = /* @__PURE__ */ new Map();
1698
+ /** Custom lapse-check function (whisper/1.0). Set by the whisper profile. */
1699
+ checkWhisperLapses;
1700
+ /** Typed method facade. Defined via getter in the constructor. */
1701
+ api;
1702
+ /** @internal cached facade instance */
1703
+ _api;
1704
+ constructor(options = {}) {
1705
+ this.options = options;
1706
+ this.store = options.store ?? new MemoryStore();
1707
+ this.ids = new IdFactory(!!options.deterministicIds, 0n, 17e11);
1708
+ if (options.deterministicClock) this.clockMs = 17e11;
1709
+ if (options.onAudit) this.auditListeners.push(options.onAudit);
1710
+ this.registerCoreHandlers();
1711
+ this.registerProfileHandlers();
1712
+ Object.defineProperty(this, "api", {
1713
+ get: () => {
1714
+ if (!this._api) this._api = makeApi(this);
1715
+ return this._api;
1716
+ },
1717
+ enumerable: false,
1718
+ configurable: true
1719
+ });
1720
+ try {
1721
+ const loaded = this.store.load();
1722
+ if (!(loaded instanceof Promise)) {
1723
+ this.applyRecords(loaded);
1724
+ }
1725
+ } catch {
1726
+ }
1727
+ }
1728
+ /**
1729
+ * Asynchronously hydrate workspaces from the store. Required only for
1730
+ * stores whose `load()` returns a Promise. Idempotent.
1731
+ */
1732
+ async start() {
1733
+ const records = await this.store.load();
1734
+ this.applyRecords(records);
1735
+ }
1736
+ applyRecords(records) {
1737
+ this.workspaces.clear();
1738
+ this.wsVersions.clear();
1739
+ for (const r of records) {
1740
+ this.restore([r.data]);
1741
+ this.wsVersions.set(r.id, r.version);
1742
+ }
1743
+ }
1744
+ /** Persist the current snapshot of one workspace to the store. */
1745
+ persist(ws) {
1746
+ const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
1747
+ this.wsVersions.set(ws.id, next);
1748
+ try {
1749
+ const all = this.snapshot();
1750
+ const data = all.find((w) => w.id === ws.id);
1751
+ if (!data) return;
1752
+ const result = this.store.save({
1753
+ id: ws.id,
1754
+ data,
1755
+ version: next,
1756
+ updated_at: this.now()
1757
+ });
1758
+ if (result instanceof Promise) {
1759
+ result.catch(() => {
1760
+ });
1761
+ }
1762
+ } catch {
1763
+ }
1764
+ }
1765
+ // -- lifecycle -----------------------------------------------------
1766
+ now() {
1767
+ if (this.clockMs !== void 0) {
1768
+ this.clockMs += 1e3;
1769
+ return nowIso(this.clockMs);
1770
+ }
1771
+ return nowIso();
1772
+ }
1773
+ onAudit(listener) {
1774
+ this.auditListeners.push(listener);
1775
+ return () => {
1776
+ const i = this.auditListeners.indexOf(listener);
1777
+ if (i >= 0) this.auditListeners.splice(i, 1);
1778
+ };
1779
+ }
1780
+ /** Convenience: get a workspace by id, or undefined. */
1781
+ getWorkspace(id) {
1782
+ return this.workspaces.get(id);
1783
+ }
1784
+ // ----- authorisation preconditions -----
1785
+ /**
1786
+ * Assert that `sender` is a joined member of `ws`. Returns an error
1787
+ * fragment if not, else null. Every actor-action method runs this on
1788
+ * its `from` so a decision, completion, or review request can never
1789
+ * be attributed to a participant who never joined. See
1790
+ * SPECIFICATION.md S6.3 and the `unknown_participant` condition in
1791
+ * S13.3. (The published spec assigns -32403; the reference
1792
+ * implementations use NOT_AUTHORISED -32011 because -32403 already
1793
+ * denotes OIDC_TOKEN_INVALID in their private range. The spec/impl
1794
+ * error-table divergence is tracked separately.)
1795
+ */
1796
+ requireMember(ws, sender) {
1797
+ if (typeof sender !== "string" || !ws.members.has(sender)) {
1798
+ return { error: rpcError(E.NOT_AUTHORISED, `Not a workspace member: ${String(sender)}`) };
1799
+ }
1800
+ return null;
1801
+ }
1802
+ /**
1803
+ * Assert that `sender` was addressed in the task's review. Applied to
1804
+ * review-decision methods (decide.* and abstain.declare): to act on a
1805
+ * review you must be one of the reviewers it was addressed to (the
1806
+ * `to` set on review.request). Membership is the floor; this is the
1807
+ * eligibility ceiling for decisions. Returns an error fragment if not
1808
+ * eligible, else null.
1809
+ */
1810
+ requireReviewer(task, sender) {
1811
+ const review = task.review;
1812
+ if (!review || !review.requested_to || review.requested_to.length === 0) {
1813
+ return null;
1814
+ }
1815
+ if (review.requested_to.some((r) => typeof r === "string" && (r.startsWith("workspace:") || r.startsWith("group:")))) {
1816
+ return null;
1817
+ }
1818
+ if (typeof sender !== "string" || !review.requested_to.includes(sender)) {
1819
+ return { error: rpcError(E.NOT_AUTHORISED, `Not an addressed reviewer for this task: ${String(sender)}`) };
1820
+ }
1821
+ return null;
1822
+ }
1823
+ /** Serialise all workspaces to a JSON-safe structure for persistence. */
1824
+ snapshot() {
1825
+ const out = [];
1826
+ for (const ws of this.workspaces.values()) {
1827
+ out.push({
1828
+ id: ws.id,
1829
+ created: ws.created,
1830
+ state: ws.state,
1831
+ profiles: ws.profiles,
1832
+ mode: ws.mode,
1833
+ mode_ceiling: ws.mode_ceiling,
1834
+ routing_policy_uri: ws.routing_policy_uri,
1835
+ step_up_window_sec: ws.step_up_window_sec,
1836
+ members: Array.from(ws.members.values()),
1837
+ tasks: Array.from(ws.tasks.values()),
1838
+ overrides: Array.from(ws.overrides.values()),
1839
+ whispers: Array.from(ws.whispers.values()),
1840
+ deliberations: Array.from(ws.deliberations.values()),
1841
+ handoffs: Array.from(ws.handoffs.values()),
1842
+ snapshots: Array.from(ws.snapshots.values()),
1843
+ route_decisions: Array.from(ws.route_decisions.values()),
1844
+ audit: ws.audit,
1845
+ chain_head: ws.chain_head,
1846
+ chain_enabled: ws.chain_enabled
1847
+ });
1848
+ }
1849
+ return out;
1850
+ }
1851
+ /** Restore from a snapshot produced by ``snapshot()``. Replaces
1852
+ * any existing in-memory workspaces. */
1853
+ restore(data) {
1854
+ this.workspaces.clear();
1855
+ if (!Array.isArray(data)) return;
1856
+ for (const w of data) {
1857
+ const ws = {
1858
+ id: w.id,
1859
+ created: w.created,
1860
+ state: w.state ?? "active",
1861
+ profiles: w.profiles ?? [],
1862
+ mode: w.mode ?? "trial",
1863
+ mode_ceiling: w.mode_ceiling ?? "production",
1864
+ routing_policy_uri: w.routing_policy_uri,
1865
+ step_up_window_sec: w.step_up_window_sec ?? 300,
1866
+ members: /* @__PURE__ */ new Map(),
1867
+ tasks: /* @__PURE__ */ new Map(),
1868
+ overrides: /* @__PURE__ */ new Map(),
1869
+ whispers: /* @__PURE__ */ new Map(),
1870
+ deliberations: /* @__PURE__ */ new Map(),
1871
+ handoffs: /* @__PURE__ */ new Map(),
1872
+ snapshots: /* @__PURE__ */ new Map(),
1873
+ route_decisions: /* @__PURE__ */ new Map(),
1874
+ audit: w.audit ?? [],
1875
+ chain_head: w.chain_head,
1876
+ chain_enabled: !!w.chain_enabled
1877
+ };
1878
+ for (const m of w.members ?? []) {
1879
+ if (!Array.isArray(m.keys)) m.keys = [];
1880
+ if (m.paused === void 0) m.paused = false;
1881
+ ws.members.set(m.uri, m);
1882
+ }
1883
+ for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
1884
+ for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
1885
+ for (const x of w.whispers ?? [])
1886
+ ws.whispers.set(x.id, x);
1887
+ for (const x of w.deliberations ?? [])
1888
+ ws.deliberations.set(x.id, x);
1889
+ for (const x of w.handoffs ?? [])
1890
+ ws.handoffs.set(x.id, x);
1891
+ for (const x of w.snapshots ?? [])
1892
+ ws.snapshots.set(x.id, x);
1893
+ for (const x of w.route_decisions ?? [])
1894
+ ws.route_decisions.set(x.id, x);
1895
+ this.workspaces.set(ws.id, ws);
1896
+ }
1897
+ }
1898
+ // -- dispatch ------------------------------------------------------
1899
+ dispatch(envelope) {
1900
+ if (!isValidEnvelope(envelope) || !envelope.method) {
1901
+ return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
1902
+ }
1903
+ const method = envelope.method;
1904
+ const rawParams = envelope.params;
1905
+ if (rawParams !== void 0 && rawParams !== null && (typeof rawParams !== "object" || Array.isArray(rawParams))) {
1906
+ return reply(envelope, { error: rpcError(E.PARAMS, "Invalid params: expected an object") });
1907
+ }
1908
+ const params = rawParams ?? {};
1909
+ if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
1910
+ const sigErr = this.verifySignature(envelope);
1911
+ if (sigErr) return reply(envelope, { error: sigErr });
1912
+ }
1913
+ if (this.options.enforceStepUp && PRIVILEGED_METHODS.has(method)) {
1914
+ const stale = this.checkStepUp(params);
1915
+ if (stale) return reply(envelope, { error: stale });
1916
+ }
1917
+ if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.")) {
1918
+ const wsId2 = params.workspace;
1919
+ const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
1920
+ if (ws) {
1921
+ const notMember = this.requireMember(ws, params.from);
1922
+ if (notMember) return reply(envelope, { error: notMember.error });
1923
+ }
1924
+ }
1925
+ const exempt = /* @__PURE__ */ new Set([
1926
+ "workspace.create",
1927
+ "workspace.describe",
1928
+ "control.resume",
1929
+ "audit.read",
1930
+ "participant.join",
1931
+ "participant.leave"
1932
+ ]);
1933
+ if (!exempt.has(method)) {
1934
+ const wsId2 = params.workspace;
1935
+ if (typeof wsId2 === "string") {
1936
+ const ws = this.workspaces.get(wsId2);
1937
+ if (ws && ws.state === "paused") {
1938
+ return reply(envelope, { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Workspace ${wsId2} is paused`) });
1939
+ }
1940
+ }
1941
+ }
1942
+ const handler = this.handlers.get(method);
1943
+ if (!handler) {
1944
+ return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
1945
+ }
1946
+ try {
1947
+ canonicalize(envelope);
1948
+ } catch (e) {
1949
+ const msg = e instanceof Error ? e.message : String(e);
1950
+ return reply(envelope, { error: rpcError(E.PARAMS, msg) });
1951
+ }
1952
+ let out;
1953
+ try {
1954
+ out = handler(params);
1955
+ } catch (e) {
1956
+ const msg = e instanceof Error ? e.message : String(e);
1957
+ return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
1958
+ }
1959
+ if (out.error) return reply(envelope, { error: out.error });
1960
+ const wsId = params.workspace;
1961
+ if (typeof wsId === "string" && !READ_ONLY_METHODS.has(method)) {
1962
+ const ws = this.workspaces.get(wsId);
1963
+ if (ws) this.recordAudit(ws, envelope);
1964
+ }
1965
+ return reply(envelope, { result: out.result });
1966
+ }
1967
+ // -- audit ---------------------------------------------------------
1968
+ recordAudit(ws, envelope) {
1969
+ const entry = {
1970
+ seq: ws.audit.length,
1971
+ arrived: this.now(),
1972
+ envelope: JSON.parse(JSON.stringify(envelope))
1973
+ };
1974
+ if (ws.chain_enabled || this.options.enableChain) {
1975
+ const prev = ws.chain_head ?? ZERO_HASH;
1976
+ entry.prev_hash = prev;
1977
+ ws.chain_head = linkHash(entry.envelope, prev);
1978
+ }
1979
+ ws.audit.push(entry);
1980
+ for (const l of this.auditListeners) {
1981
+ try {
1982
+ l(ws, entry);
1983
+ } catch {
1984
+ }
1985
+ }
1986
+ this.persist(ws);
1987
+ }
1988
+ // -- signature verification (security-signed/1.0) -----------------
1989
+ verifySignature(envelope) {
1990
+ const sig = envelope.sig;
1991
+ if (!sig || typeof sig !== "string") {
1992
+ return rpcError(E.SIG_VERIFY_FAILED, "Missing top-level `sig` field");
1993
+ }
1994
+ const parts = sig.split(":");
1995
+ if (parts.length !== 3 || parts[0] !== "ed25519") {
1996
+ return rpcError(E.SIG_VERIFY_FAILED, "Malformed signature tag");
1997
+ }
1998
+ const kid = parts[1];
1999
+ const params = envelope.params ?? {};
2000
+ const sender = params.from;
2001
+ const wsId = params.workspace;
2002
+ const ts = params.ts ?? this.now();
2003
+ if (!sender || !wsId) {
2004
+ return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: missing from/workspace");
2005
+ }
2006
+ const ws = this.workspaces.get(wsId);
2007
+ if (!ws) {
2008
+ return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: unknown workspace");
2009
+ }
2010
+ const member = ws.members.get(sender);
2011
+ if (!member) return rpcError(E.SIG_KEY_NOT_FOUND, `No member: ${sender}`);
2012
+ const now = this.now();
2013
+ if (member.keys.some((k) => k.kid === kid && k.revoked_at !== void 0 && now >= k.revoked_at)) {
2014
+ return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
2015
+ }
2016
+ const key = this.lookupKey(member, kid, ts);
2017
+ if (!key) {
2018
+ const revoked = member.keys.find((k) => k.kid === kid && k.revoked_at);
2019
+ if (revoked) return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
2020
+ return rpcError(E.SIG_KEY_NOT_FOUND, `No key ${kid} valid at ${ts} for ${sender}`);
2021
+ }
2022
+ try {
2023
+ const stripped = JSON.parse(JSON.stringify(envelope));
2024
+ delete stripped.sig;
2025
+ const canonical = canonicalize(stripped);
2026
+ const pub = publicKeyFromJwk(key.jwk);
2027
+ if (!verifyEnvelope(canonical, sig, pub)) {
2028
+ return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
2029
+ }
2030
+ return null;
2031
+ } catch (e) {
2032
+ const msg = e instanceof Error ? e.message : String(e);
2033
+ return rpcError(E.INTERNAL, `Signature check error: ${msg}`);
2034
+ }
2035
+ }
2036
+ /** Lookup the key valid for a given participant at a given time. */
2037
+ lookupKey(member, kid, ts) {
2038
+ for (const k of member.keys) {
2039
+ if (k.kid !== kid) continue;
2040
+ if (k.revoked_at !== void 0 && ts >= k.revoked_at) continue;
2041
+ if (ts < k.valid_from) continue;
2042
+ if (k.valid_until !== void 0 && ts >= k.valid_until) continue;
2043
+ return k;
2044
+ }
2045
+ return null;
2046
+ }
2047
+ // -- step-up (identity-oidc/1.0) -----------------------------------
2048
+ checkStepUp(params) {
2049
+ const wsId = params.workspace;
2050
+ const sender = params.from;
2051
+ if (!wsId || !sender) return null;
2052
+ const ws = this.workspaces.get(wsId);
2053
+ if (!ws) return null;
2054
+ const member = ws.members.get(sender);
2055
+ if (!member || member.oidc_auth_time === void 0) return null;
2056
+ const nowSec = Math.floor(Date.now() / 1e3);
2057
+ const age = nowSec - member.oidc_auth_time;
2058
+ if (age > ws.step_up_window_sec) {
2059
+ return rpcError(E.OIDC_STEP_UP_REQUIRED, "Step-up authentication required", {
2060
+ window_sec: ws.step_up_window_sec,
2061
+ age_sec: age
2062
+ });
2063
+ }
2064
+ return null;
2065
+ }
2066
+ // ==========================================================
2067
+ // Registration
2068
+ // ==========================================================
2069
+ registerCoreHandlers() {
2070
+ this.handlers.set("workspace.create", (p) => this.opWorkspaceCreate(p));
2071
+ this.handlers.set("workspace.describe", (p) => this.opWorkspaceDescribe(p));
2072
+ this.handlers.set("workspace.set_profiles", (p) => this.opWorkspaceSetProfiles(p));
2073
+ this.handlers.set("participant.join", (p) => this.opParticipantJoin(p));
2074
+ this.handlers.set("participant.leave", (p) => this.opParticipantLeave(p));
2075
+ this.handlers.set("task.create", (p) => this.opTaskCreate(p));
2076
+ this.handlers.set("task.update", (p) => this.opTaskUpdate(p));
2077
+ this.handlers.set("task.complete", (p) => this.opTaskComplete(p));
2078
+ this.handlers.set("audit.read", (p) => this.opAuditRead(p));
2079
+ this.handlers.set("review.request", (p) => this.opReviewRequest(p));
2080
+ this.handlers.set("decide.approve", (p) => this.opDecide(p, "approve"));
2081
+ this.handlers.set("decide.reject", (p) => this.opDecide(p, "reject"));
2082
+ this.handlers.set("decide.override", (p) => this.opDecideOverride(p));
2083
+ this.handlers.set("abstain.declare", (p) => this.opAbstainDeclare(p));
2084
+ this.handlers.set("escalate.raise", (p) => this.opEscalateRaise(p));
2085
+ }
2086
+ registerProfileHandlers() {
2087
+ registerWhisper(this);
2088
+ registerDeliberation(this);
2089
+ registerHandoff(this);
2090
+ registerControl(this);
2091
+ registerRouting(this);
2092
+ registerSecuritySigned(this);
2093
+ registerAuditScitt(this);
2094
+ }
2095
+ // ==========================================================
2096
+ // Core method handlers
2097
+ // ==========================================================
2098
+ opWorkspaceCreate(p) {
2099
+ const id = p.workspace || this.ids.workspaceId();
2100
+ if (typeof id !== "string") return { error: rpcError(E.PARAMS, "workspace must be a string id") };
2101
+ if (this.workspaces.has(id)) return { error: rpcError(E.PARAMS, `workspace already exists: ${id}`) };
2102
+ const profiles = Array.isArray(p.profiles) ? [...p.profiles] : [...this.options.defaultProfiles ?? ["core/1.0", "review/1.0"]];
2103
+ const chainEnabled = profiles.includes("audit-scitt/1.0") || !!this.options.enableChain;
2104
+ const ws = {
2105
+ id,
2106
+ created: this.now(),
2107
+ state: "active",
2108
+ profiles,
2109
+ mode: p.mode ?? "trial",
2110
+ mode_ceiling: p.mode_ceiling ?? "production",
2111
+ routing_policy_uri: p.routing_policy_uri,
2112
+ step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
2113
+ members: /* @__PURE__ */ new Map(),
2114
+ tasks: /* @__PURE__ */ new Map(),
2115
+ overrides: /* @__PURE__ */ new Map(),
2116
+ whispers: /* @__PURE__ */ new Map(),
2117
+ deliberations: /* @__PURE__ */ new Map(),
2118
+ handoffs: /* @__PURE__ */ new Map(),
2119
+ snapshots: /* @__PURE__ */ new Map(),
2120
+ route_decisions: /* @__PURE__ */ new Map(),
2121
+ audit: [],
2122
+ chain_enabled: chainEnabled,
2123
+ chain_head: chainEnabled ? ZERO_HASH : void 0
2124
+ };
2125
+ this.workspaces.set(id, ws);
2126
+ return { result: { workspace: id, created: ws.created } };
2127
+ }
2128
+ opWorkspaceDescribe(p) {
2129
+ const miss = missing(p, ["workspace"]);
2130
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2131
+ const ws = this.workspaces.get(p.workspace);
2132
+ if (!ws) return { error: rpcError(E.PARAMS, `Unknown workspace: ${p.workspace}`) };
2133
+ return { result: {
2134
+ id: ws.id,
2135
+ created: ws.created,
2136
+ state: ws.state,
2137
+ mode: ws.mode,
2138
+ mode_ceiling: ws.mode_ceiling,
2139
+ step_up_window_sec: ws.step_up_window_sec,
2140
+ profiles: ws.profiles,
2141
+ members: Array.from(ws.members.values()).map(memberToDict),
2142
+ audit_count: ws.audit.length,
2143
+ task_count: ws.tasks.size,
2144
+ override_count: ws.overrides.size,
2145
+ evidence_head: ws.chain_head,
2146
+ ...ws.routing_policy_uri ? { routing_policy_uri: ws.routing_policy_uri } : {}
2147
+ } };
2148
+ }
2149
+ opWorkspaceSetProfiles(p) {
2150
+ const miss = missing(p, ["workspace", "profiles"]);
2151
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2152
+ const ws = this.workspaces.get(p.workspace);
2153
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2154
+ const newProfiles = [...p.profiles];
2155
+ if (!newProfiles.some((pr) => pr.startsWith("core/"))) newProfiles.push("core/1.0");
2156
+ ws.profiles = newProfiles;
2157
+ if (newProfiles.includes("audit-scitt/1.0") && !ws.chain_enabled) {
2158
+ ws.chain_enabled = true;
2159
+ ws.chain_head = ZERO_HASH;
2160
+ }
2161
+ return { result: { profiles: ws.profiles } };
2162
+ }
2163
+ opParticipantJoin(p) {
2164
+ const miss = missing(p, ["workspace", "from", "type"]);
2165
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2166
+ let ws = this.workspaces.get(p.workspace);
2167
+ if (!ws) {
2168
+ const create = this.opWorkspaceCreate({ workspace: p.workspace });
2169
+ if (create.error) return create;
2170
+ ws = this.workspaces.get(p.workspace);
2171
+ }
2172
+ const uri = p.from;
2173
+ const now = this.now();
2174
+ const member = {
2175
+ uri,
2176
+ type: p.type,
2177
+ role: p.role || "participant",
2178
+ joined: now,
2179
+ display_name: p.display_name,
2180
+ capabilities: p.capabilities,
2181
+ scopes: p.scopes,
2182
+ keys: [],
2183
+ paused: false
2184
+ };
2185
+ if (this.options.verifyOidcToken && typeof p.oidc_token === "string") {
2186
+ const claims = this.options.verifyOidcToken(p.oidc_token);
2187
+ if (claims === null) return { error: rpcError(E.OIDC_TOKEN_INVALID, "OIDC token invalid") };
2188
+ member.oidc_sub = claims.sub;
2189
+ const at = claims.auth_time;
2190
+ if (typeof at === "number") member.oidc_auth_time = at;
2191
+ const cnf = claims.cnf;
2192
+ const cnfJwk = cnf?.jwk;
2193
+ if (cnfJwk && typeof cnfJwk.kid === "string") {
2194
+ member.keys.push({ jwk: cnfJwk, kid: cnfJwk.kid, valid_from: now });
2195
+ }
2196
+ }
2197
+ if (this.options.verifyVc && typeof p.vc_presentation === "object" && p.vc_presentation !== null) {
2198
+ const subject = this.options.verifyVc(p.vc_presentation);
2199
+ if (subject === null) return { error: rpcError(E.VC_VP_INVALID, "VC presentation invalid") };
2200
+ member.vc_holder = subject.holder ?? subject.id;
2201
+ const vpJwk = subject.cnf_jwk;
2202
+ if (vpJwk && typeof vpJwk.kid === "string") {
2203
+ member.keys.push({ jwk: vpJwk, kid: vpJwk.kid, valid_from: now });
2204
+ }
2205
+ }
2206
+ const attested = [...member.keys];
2207
+ const jwks = p.jwks;
2208
+ if (jwks?.keys && attested.length === 0) {
2209
+ for (const j of jwks.keys) {
2210
+ if (j.kid && !member.keys.some((k) => k.kid === j.kid)) {
2211
+ member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
2212
+ }
2213
+ }
2214
+ }
2215
+ const existing = ws.members.get(uri);
2216
+ if (existing) {
2217
+ if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
2218
+ if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
2219
+ if (member.vc_holder !== void 0) existing.vc_holder = member.vc_holder;
2220
+ for (const k of attested) {
2221
+ if (!existing.keys.some((x) => x.kid === k.kid)) existing.keys.push(k);
2222
+ }
2223
+ return { result: { joined: true, as: uri } };
2224
+ }
2225
+ ws.members.set(uri, member);
2226
+ return { result: { joined: true, as: uri } };
2227
+ }
2228
+ opParticipantLeave(p) {
2229
+ const ws = this.workspaces.get(p.workspace);
2230
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2231
+ ws.members.delete(p.from);
2232
+ return { result: { left: true } };
2233
+ }
2234
+ opTaskCreate(p) {
2235
+ const miss = missing(p, ["workspace", "from", "kind", "input"]);
2236
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2237
+ const ws = this.workspaces.get(p.workspace);
2238
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2239
+ const assignee = p.assignee || p.to;
2240
+ if (!assignee || !ws.members.has(assignee)) {
2241
+ return { error: rpcError(E.PARAMS, "Assignee not in workspace") };
2242
+ }
2243
+ if (ws.members.get(assignee).paused) {
2244
+ return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
2245
+ }
2246
+ const requestedMode = p.mode || ws.mode;
2247
+ if (!modeLE(requestedMode, ws.mode_ceiling)) {
2248
+ return { error: rpcError(
2249
+ E.MODE_CEILING_EXCEEDED,
2250
+ `Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
2251
+ ) };
2252
+ }
2253
+ const taskId = this.ids.taskId();
2254
+ const now = this.now();
2255
+ const task = {
2256
+ id: taskId,
2257
+ kind: p.kind,
2258
+ state: "created",
2259
+ assignee,
2260
+ delegator: p.from,
2261
+ input: p.input,
2262
+ created_at: now,
2263
+ updated_at: now,
2264
+ deadline: p.deadline,
2265
+ mode: requestedMode,
2266
+ routing_hints: p.routing_hints,
2267
+ history: [{ ts: now, from: p.from, state: "created" }],
2268
+ paused: false
2269
+ };
2270
+ if (task.mode === "trial") task.review_required = true;
2271
+ else if ("review_required" in p) task.review_required = !!p.review_required;
2272
+ ws.tasks.set(taskId, task);
2273
+ return { result: { task_id: taskId, state: "created" } };
2274
+ }
2275
+ opTaskUpdate(p) {
2276
+ const ws = this.workspaces.get(p.workspace);
2277
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2278
+ const task = ws.tasks.get(p.task_id);
2279
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2280
+ const newState = p.state;
2281
+ const legal = {
2282
+ created: ["in_progress", "declined", "paused"],
2283
+ in_progress: ["in_progress", "completed", "declined", "review_requested", "paused"],
2284
+ review_requested: ["in_progress", "completed", "declined"],
2285
+ paused: ["in_progress", "cancelled"]
2286
+ };
2287
+ if (!legal[task.state]?.includes(newState)) {
2288
+ return { error: rpcError(E.PARAMS, `Illegal transition ${task.state} -> ${newState}`) };
2289
+ }
2290
+ task.state = newState;
2291
+ task.updated_at = this.now();
2292
+ task.history.push({
2293
+ ts: task.updated_at,
2294
+ from: p.from,
2295
+ state: newState,
2296
+ note: p.progress_note
2297
+ });
2298
+ return { result: { state: newState } };
2299
+ }
2300
+ opTaskComplete(p) {
2301
+ const ws = this.workspaces.get(p.workspace);
2302
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2303
+ const notMember = this.requireMember(ws, p.from);
2304
+ if (notMember) return notMember;
2305
+ const task = ws.tasks.get(p.task_id);
2306
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2307
+ if (task.state !== "created" && task.state !== "in_progress") {
2308
+ return { error: rpcError(E.PARAMS, `Cannot complete task in state: ${task.state}`) };
2309
+ }
2310
+ task.output = p.output;
2311
+ if (typeof p.confidence === "number" || typeof p.confidence === "string") task.confidence = p.confidence;
2312
+ task.state = "completed";
2313
+ task.updated_at = this.now();
2314
+ task.history.push({ ts: task.updated_at, from: p.from, state: "completed" });
2315
+ return { result: { state: "completed" } };
2316
+ }
2317
+ opAuditRead(p) {
2318
+ const ws = this.workspaces.get(p.workspace);
2319
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2320
+ const range = p.range ?? {};
2321
+ const filter = p.filter ?? {};
2322
+ const fromSeq = range.from_seq ?? 0;
2323
+ const toSeq = range.to_seq ?? ws.audit.length;
2324
+ const out = [];
2325
+ for (const entry of ws.audit.slice(fromSeq, toSeq)) {
2326
+ const ep = entry.envelope.params ?? {};
2327
+ if (filter.method && entry.envelope.method !== filter.method) continue;
2328
+ if (filter.from && ep.from !== filter.from) continue;
2329
+ if (filter.task_id && ep.task_id !== filter.task_id) continue;
2330
+ const item = {
2331
+ seq: entry.seq,
2332
+ arrived: entry.arrived,
2333
+ envelope: entry.envelope
2334
+ };
2335
+ if (entry.prev_hash) item.prev_hash = entry.prev_hash;
2336
+ out.push(item);
2337
+ }
2338
+ return { result: { entries: out, next_seq: toSeq } };
2339
+ }
2340
+ // ==========================================================
2341
+ // review/1.0 handlers
2342
+ // ==========================================================
2343
+ opReviewRequest(p) {
2344
+ const miss = missing(p, ["workspace", "from", "task_id", "artefact"]);
2345
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2346
+ const ws = this.workspaces.get(p.workspace);
2347
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2348
+ const notMember = this.requireMember(ws, p.from);
2349
+ if (notMember) return notMember;
2350
+ const task = ws.tasks.get(p.task_id);
2351
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2352
+ const to = p.to;
2353
+ const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
2354
+ if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
2355
+ const now = this.now();
2356
+ task.state = "review_requested";
2357
+ task.updated_at = now;
2358
+ task.review = {
2359
+ requested_at: now,
2360
+ requested_to: reviewers,
2361
+ rule: p.rule || "any_one_approves",
2362
+ deadline: p.deadline,
2363
+ decisions: []
2364
+ };
2365
+ task.history.push({ ts: now, from: p.from, state: "review_requested" });
2366
+ task.pending_artefact = p.artefact;
2367
+ return { result: { state: "review_requested", review_id: task.id } };
2368
+ }
2369
+ opDecide(p, kind) {
2370
+ const miss = missing(p, ["workspace", "from", "task_id"]);
2371
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2372
+ const badTags = tagsError(p);
2373
+ if (badTags) return badTags;
2374
+ const ws = this.workspaces.get(p.workspace);
2375
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2376
+ const notMember = this.requireMember(ws, p.from);
2377
+ if (notMember) return notMember;
2378
+ const task = ws.tasks.get(p.task_id);
2379
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2380
+ if (task.state !== "review_requested") {
2381
+ return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
2382
+ }
2383
+ const notReviewer = this.requireReviewer(task, p.from);
2384
+ if (notReviewer) return notReviewer;
2385
+ const now = this.now();
2386
+ task.review.decisions.push({
2387
+ reviewer: p.from,
2388
+ kind,
2389
+ ts: now,
2390
+ comment: p.comment,
2391
+ tags: p.tags
2392
+ });
2393
+ if (kind === "approve") {
2394
+ task.output = task.pending_artefact;
2395
+ task.state = "completed";
2396
+ } else {
2397
+ task.state = p.request_revision ? "in_progress" : "declined";
2398
+ }
2399
+ task.updated_at = now;
2400
+ task.history.push({ ts: now, from: p.from, state: task.state });
2401
+ return { result: { state: task.state } };
2402
+ }
2403
+ opDecideOverride(p) {
2404
+ const miss = missing(p, ["workspace", "from", "task_id", "diff", "rationale"]);
2405
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2406
+ const badTags = tagsError(p);
2407
+ if (badTags) return badTags;
2408
+ const ws = this.workspaces.get(p.workspace);
2409
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2410
+ const notMember = this.requireMember(ws, p.from);
2411
+ if (notMember) return notMember;
2412
+ const task = ws.tasks.get(p.task_id);
2413
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2414
+ if (task.state !== "review_requested") {
2415
+ return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
2416
+ }
2417
+ const notReviewer = this.requireReviewer(task, p.from);
2418
+ if (notReviewer) return notReviewer;
2419
+ const base = task.pending_artefact;
2420
+ if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
2421
+ let applied;
2422
+ try {
2423
+ applied = applyJsonPatch(base, p.diff);
2424
+ } catch (e) {
2425
+ const msg = e instanceof Error ? e.message : String(e);
2426
+ return { error: rpcError(E.PATCH_FAILED, msg) };
2427
+ }
2428
+ const now = this.now();
2429
+ const id = this.ids.artefactId();
2430
+ const override = {
2431
+ id,
2432
+ task_id: task.id,
2433
+ reviewer: p.from,
2434
+ based_on_artefact: base,
2435
+ diff: p.diff,
2436
+ result: applied,
2437
+ rationale: p.rationale,
2438
+ tags: p.tags ?? [],
2439
+ policy_refs: p.policy_refs ?? [],
2440
+ ts: now,
2441
+ logical_id: p.logical_id,
2442
+ instance_id: p.instance_id,
2443
+ intent_preserved: p.intent_preserved
2444
+ };
2445
+ ws.overrides.set(id, override);
2446
+ task.review.decisions.push({
2447
+ reviewer: p.from,
2448
+ kind: "override",
2449
+ ts: now,
2450
+ tags: override.tags,
2451
+ override_artefact_id: id
2452
+ });
2453
+ task.output = applied;
2454
+ task.state = "completed";
2455
+ task.updated_at = now;
2456
+ task.history.push({ ts: now, from: p.from, state: "completed", note: "override applied" });
2457
+ return { result: { state: "completed", override_artefact_id: id, applied } };
2458
+ }
2459
+ opAbstainDeclare(p) {
2460
+ const miss = missing(p, ["workspace", "from", "task_id", "reason"]);
2461
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2462
+ const ws = this.workspaces.get(p.workspace);
2463
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2464
+ const notMember = this.requireMember(ws, p.from);
2465
+ if (notMember) return notMember;
2466
+ const task = ws.tasks.get(p.task_id);
2467
+ if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
2468
+ if (task.state !== "review_requested") {
2469
+ return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
2470
+ }
2471
+ const notReviewer = this.requireReviewer(task, p.from);
2472
+ if (notReviewer) return notReviewer;
2473
+ const now = this.now();
2474
+ task.review.decisions.push({
2475
+ reviewer: p.from,
2476
+ kind: "abstain",
2477
+ ts: now,
2478
+ comment: p.reason,
2479
+ abstain_category: p.category || "other"
2480
+ });
2481
+ task.state = "abstained";
2482
+ task.updated_at = now;
2483
+ task.history.push({ ts: now, from: p.from, state: "abstained", note: p.reason });
2484
+ return { result: { state: "abstained" } };
2485
+ }
2486
+ opEscalateRaise(p) {
2487
+ const miss = missing(p, ["workspace", "from", "original_task_id", "new_task"]);
2488
+ if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
2489
+ const ws = this.workspaces.get(p.workspace);
2490
+ if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
2491
+ const notMember = this.requireMember(ws, p.from);
2492
+ if (notMember) return notMember;
2493
+ const orig = ws.tasks.get(p.original_task_id);
2494
+ if (!orig) return { error: rpcError(E.PARAMS, "Unknown original task") };
2495
+ if (orig.state === "completed" || orig.state === "cancelled" || orig.state === "superseded") {
2496
+ return { error: rpcError(E.PARAMS, `Cannot escalate a terminal task: ${orig.state}`) };
2497
+ }
2498
+ const nt = p.new_task;
2499
+ const assignee = nt.assignee;
2500
+ if (!assignee || !ws.members.has(assignee)) {
2501
+ return { error: rpcError(E.PARAMS, "Escalation assignee not in workspace") };
2502
+ }
2503
+ const now = this.now();
2504
+ const newId = this.ids.taskId();
2505
+ const newTask = {
2506
+ id: newId,
2507
+ kind: nt.kind || orig.kind,
2508
+ state: "created",
2509
+ assignee,
2510
+ delegator: p.from,
2511
+ input: nt.input ?? {},
2512
+ created_at: now,
2513
+ updated_at: now,
2514
+ mode: orig.mode,
2515
+ supersedes: orig.id,
2516
+ history: [{ ts: now, from: p.from, state: "created", note: `escalated from ${orig.id}` }],
2517
+ paused: false
2518
+ };
2519
+ ws.tasks.set(newId, newTask);
2520
+ orig.state = "escalated";
2521
+ orig.updated_at = now;
2522
+ orig.superseded_by = newId;
2523
+ orig.history.push({ ts: now, from: p.from, state: "escalated", note: `-> ${newId}` });
2524
+ return { result: { new_task_id: newId, escalated_from: orig.id } };
2525
+ }
2526
+ };
2527
+ function memberToDict(m) {
2528
+ const out = {
2529
+ uri: m.uri,
2530
+ type: m.type,
2531
+ role: m.role,
2532
+ joined: m.joined
2533
+ };
2534
+ if (m.display_name) out.display_name = m.display_name;
2535
+ if (m.capabilities) out.capabilities = m.capabilities;
2536
+ if (m.scopes) out.scopes = m.scopes;
2537
+ if (m.keys.length) {
2538
+ out.jwks = { keys: m.keys.filter((k) => !k.revoked_at && !k.valid_until).map((k) => k.jwk) };
2539
+ out.key_history = m.keys;
2540
+ }
2541
+ if (m.paused) out.paused = true;
2542
+ if (m.oidc_sub) out.oidc_sub = m.oidc_sub;
2543
+ if (m.vc_holder) out.vc_holder = m.vc_holder;
2544
+ return out;
2545
+ }
2546
+
2547
+ // src/policy.ts
2548
+ var CRIT2 = { low: 0, medium: 1, high: 2, critical: 3 };
2549
+ function makeDefaultPolicy(seniorTarget) {
2550
+ const routingPolicy = (task, candidates) => {
2551
+ const eligible = candidates;
2552
+ return {
2553
+ selected: eligible[0],
2554
+ rationale: {
2555
+ policy_id: "default",
2556
+ hints_used: Object.keys(task.routing_hints ?? {}),
2557
+ summary: "default policy: first eligible candidate",
2558
+ alternatives_considered: eligible.slice(1).map((c) => ({
2559
+ candidate: c,
2560
+ reason_excluded: "not first eligible"
2561
+ }))
2562
+ }
2563
+ };
2564
+ };
2565
+ const reviewDepthPolicy = (_task, hints) => {
2566
+ const crit = hints.criticality ?? "medium";
2567
+ let conf = 1;
2568
+ try {
2569
+ conf = Number(hints.confidence ?? 1);
2570
+ } catch {
2571
+ conf = 1;
2572
+ }
2573
+ if (isNaN(conf)) conf = 1;
2574
+ const r = CRIT2[crit] ?? 1;
2575
+ if (r >= 3) return {
2576
+ depth: "full",
2577
+ rationale: { policy_id: "default", summary: "criticality=critical: full review" }
2578
+ };
2579
+ if (r === 2 && conf < 0.7) return {
2580
+ depth: "full",
2581
+ rationale: { policy_id: "default", summary: "criticality=high and confidence<0.7: full review" }
2582
+ };
2583
+ if (r === 0 && conf >= 0.95) return {
2584
+ depth: "skip",
2585
+ rationale: { policy_id: "default", summary: "very low criticality + very high confidence: skip" }
2586
+ };
2587
+ if (r <= 1 && conf >= 0.85) return {
2588
+ depth: "spot_check",
2589
+ sampling_probability: 0.1,
2590
+ rationale: { policy_id: "default", summary: "low criticality + high confidence: 10% sample" }
2591
+ };
2592
+ return {
2593
+ depth: "spot_check",
2594
+ sampling_probability: 0.25,
2595
+ rationale: { policy_id: "default", summary: "default mid-confidence: 25% sample" }
2596
+ };
2597
+ };
2598
+ const escalationPolicy = (_task, hints) => {
2599
+ const crit = hints.criticality ?? "medium";
2600
+ let conf = 1;
2601
+ try {
2602
+ conf = Number(hints.confidence ?? 1);
2603
+ } catch {
2604
+ conf = 1;
2605
+ }
2606
+ if (isNaN(conf)) conf = 1;
2607
+ const r = CRIT2[crit] ?? 1;
2608
+ const escalate = r >= 3 || r >= 2 && conf < 0.6;
2609
+ return {
2610
+ escalate,
2611
+ to: escalate ? seniorTarget : void 0,
2612
+ triggered_rule: {
2613
+ rule_id: "default-auto-esc",
2614
+ summary: "criticality=critical OR (criticality>=high AND confidence<0.6)",
2615
+ hints_used: ["criticality", "confidence"]
2616
+ }
2617
+ };
2618
+ };
2619
+ return { routingPolicy, reviewDepthPolicy, escalationPolicy };
2620
+ }
2621
+
2622
+ // src/transports/wrap.ts
2623
+ function wrapMcpToolCall(coord, workspace, options) {
2624
+ if (!workspace) throw new Error("workspace is required");
2625
+ if (!options.caller) throw new Error("caller is required");
2626
+ if (!options.tool) throw new Error("tool is required");
2627
+ const inputHash = contentHash(options.args);
2628
+ const outputHash = contentHash(options.result);
2629
+ const kind = options.taskKind ?? `mcp_call:${options.tool}`;
2630
+ const citations = [{
2631
+ kind: "mcp_tool_call",
2632
+ server: options.server ?? "unknown",
2633
+ tool: options.tool,
2634
+ input_hash: inputHash,
2635
+ output_hash: outputHash
2636
+ }];
2637
+ const createParams = {
2638
+ workspace,
2639
+ from: options.caller,
2640
+ kind,
2641
+ assignee: options.caller,
2642
+ input: options.args
2643
+ };
2644
+ if (options.routingHints) createParams.routing_hints = { ...options.routingHints };
2645
+ const createResp = coord.dispatch({
2646
+ jsonrpc: "2.0",
2647
+ id: `wrap-${options.tool}-create`,
2648
+ method: "task.create",
2649
+ params: createParams
2650
+ });
2651
+ if (createResp.error) throw new Error(`task.create failed: ${JSON.stringify(createResp.error)}`);
2652
+ const taskId = createResp.result.task_id;
2653
+ coord.dispatch({
2654
+ jsonrpc: "2.0",
2655
+ id: `wrap-${options.tool}-update`,
2656
+ method: "task.update",
2657
+ params: { workspace, from: options.caller, task_id: taskId, state: "in_progress" }
2658
+ });
2659
+ const completeParams = {
2660
+ workspace,
2661
+ from: options.caller,
2662
+ task_id: taskId,
2663
+ output: { result: options.result, citations }
2664
+ };
2665
+ if (options.confidence !== void 0) completeParams.confidence = options.confidence;
2666
+ const completeResp = coord.dispatch({
2667
+ jsonrpc: "2.0",
2668
+ id: `wrap-${options.tool}-complete`,
2669
+ method: "task.complete",
2670
+ params: completeParams
2671
+ });
2672
+ if (completeResp.error) throw new Error(`task.complete failed: ${JSON.stringify(completeResp.error)}`);
2673
+ return { task_id: taskId, input_hash: inputHash, output_hash: outputHash };
2674
+ }
2675
+ function wrapA2aMessageExchange(coord, workspace, options) {
2676
+ if (!workspace) throw new Error("workspace is required");
2677
+ if (!options.bridgeUri) throw new Error("bridgeUri is required");
2678
+ if (!options.remoteAgent) throw new Error("remoteAgent is required");
2679
+ const sentHash = contentHash(options.sent);
2680
+ const receivedHash = contentHash(options.received);
2681
+ const kind = options.taskKind ?? "a2a_exchange";
2682
+ const citation = {
2683
+ kind: "a2a_exchange",
2684
+ remote_agent: options.remoteAgent,
2685
+ sent_hash: sentHash,
2686
+ received_hash: receivedHash
2687
+ };
2688
+ const createParams = {
2689
+ workspace,
2690
+ from: options.bridgeUri,
2691
+ kind,
2692
+ assignee: options.bridgeUri,
2693
+ input: { remote_agent: options.remoteAgent, sent: options.sent }
2694
+ };
2695
+ if (options.routingHints) createParams.routing_hints = { ...options.routingHints };
2696
+ const createResp = coord.dispatch({
2697
+ jsonrpc: "2.0",
2698
+ id: "wrap-a2a-create",
2699
+ method: "task.create",
2700
+ params: createParams
2701
+ });
2702
+ if (createResp.error) throw new Error(`task.create failed: ${JSON.stringify(createResp.error)}`);
2703
+ const taskId = createResp.result.task_id;
2704
+ coord.dispatch({
2705
+ jsonrpc: "2.0",
2706
+ id: "wrap-a2a-update",
2707
+ method: "task.update",
2708
+ params: {
2709
+ workspace,
2710
+ from: options.bridgeUri,
2711
+ task_id: taskId,
2712
+ state: "in_progress"
2713
+ }
2714
+ });
2715
+ const completeParams = {
2716
+ workspace,
2717
+ from: options.bridgeUri,
2718
+ task_id: taskId,
2719
+ output: { received: options.received, citations: [citation] }
2720
+ };
2721
+ if (options.confidence !== void 0) completeParams.confidence = options.confidence;
2722
+ const completeResp = coord.dispatch({
2723
+ jsonrpc: "2.0",
2724
+ id: "wrap-a2a-complete",
2725
+ method: "task.complete",
2726
+ params: completeParams
2727
+ });
2728
+ if (completeResp.error) throw new Error(`task.complete failed: ${JSON.stringify(completeResp.error)}`);
2729
+ return { task_id: taskId, sent_hash: sentHash, received_hash: receivedHash };
2730
+ }
2731
+ export {
2732
+ Coordinator,
2733
+ CoordinatorError,
2734
+ E,
2735
+ IdFactory,
2736
+ MemoryStore,
2737
+ PRIVILEGED_METHODS,
2738
+ PatchError,
2739
+ ZERO_HASH,
2740
+ applyJsonPatch,
2741
+ call,
2742
+ canonicalize,
2743
+ contentHash,
2744
+ deriveKeypair,
2745
+ deriveSeed,
2746
+ diffJsonPatch,
2747
+ isValidEnvelope,
2748
+ jwkFromPrivateKey,
2749
+ makeApi,
2750
+ makeDefaultPolicy,
2751
+ modeLE,
2752
+ publicKeyBytes,
2753
+ publicKeyFromJwk,
2754
+ publicKeyFromRaw,
2755
+ rpcError,
2756
+ sha256Hex,
2757
+ signEnvelope,
2758
+ verifyEnvelope,
2759
+ wrapA2aMessageExchange,
2760
+ wrapMcpToolCall
2761
+ };
2762
+ //# sourceMappingURL=index.js.map