@brightbeamai/chap-coordinator 0.2.12 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -7
- package/dist/{api-DiiL0sil.d.cts → api-BkvwN42r.d.cts} +40 -0
- package/dist/{api-BOGEBNmX.d.ts → api-Ddy9fqkY.d.ts} +40 -0
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.cjs +667 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -7
- package/dist/index.d.ts +24 -7
- package/dist/index.js +663 -143
- package/dist/index.js.map +1 -1
- package/dist/methods.cjs.map +1 -1
- package/dist/methods.d.cts +1 -1
- package/dist/methods.d.ts +1 -1
- package/dist/patch.cjs +22 -6
- package/dist/patch.cjs.map +1 -1
- package/dist/patch.js +22 -6
- package/dist/patch.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +28 -9
- package/dist/types.d.ts +28 -9
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,21 @@ import { createHash } from "crypto";
|
|
|
3
3
|
var ZERO_HASH = "sha256:" + "0".repeat(64);
|
|
4
4
|
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.';
|
|
5
5
|
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.";
|
|
6
|
+
var LONE_SURROGATE_ERROR = "CHAP canonical strings must be valid Unicode (I-JSON / RFC 7493); unpaired UTF-16 surrogates are not permitted.";
|
|
7
|
+
function assertNoLoneSurrogate(s) {
|
|
8
|
+
for (let i = 0; i < s.length; i++) {
|
|
9
|
+
const c = s.charCodeAt(i);
|
|
10
|
+
if (c >= 55296 && c <= 56319) {
|
|
11
|
+
const next = i + 1 < s.length ? s.charCodeAt(i + 1) : 0;
|
|
12
|
+
if (next < 56320 || next > 57343) throw new Error(LONE_SURROGATE_ERROR);
|
|
13
|
+
i++;
|
|
14
|
+
} else if (c >= 56320 && c <= 57343) {
|
|
15
|
+
throw new Error(LONE_SURROGATE_ERROR);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
function canonicalString(s) {
|
|
20
|
+
assertNoLoneSurrogate(s);
|
|
7
21
|
return JSON.stringify(s);
|
|
8
22
|
}
|
|
9
23
|
function canonicalNumber(n) {
|
|
@@ -52,6 +66,55 @@ function contentHash(content) {
|
|
|
52
66
|
return sha256Hex(canonicalize(content));
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
// src/catalogue.ts
|
|
70
|
+
var OWNING_PROFILE = Object.freeze({
|
|
71
|
+
"abstain.declare": "review/1.0",
|
|
72
|
+
"audit.read": "core/1.0",
|
|
73
|
+
"audit.submit_to_scitt": "audit-scitt/1.0",
|
|
74
|
+
"audit.verify_chain": "audit-scitt/1.0",
|
|
75
|
+
"audit.verify_receipt": "audit-scitt/1.0",
|
|
76
|
+
"control.cancel": "control/1.0",
|
|
77
|
+
"control.pause": "control/1.0",
|
|
78
|
+
"control.resume": "control/1.0",
|
|
79
|
+
"control.rollback": "control/1.0",
|
|
80
|
+
"control.set_mode_ceiling": "control/1.0",
|
|
81
|
+
"control.snapshot": "control/1.0",
|
|
82
|
+
"control.supersede": "control/1.0",
|
|
83
|
+
"decide.approve": "review/1.0",
|
|
84
|
+
"decide.override": "review/1.0",
|
|
85
|
+
"decide.reject": "review/1.0",
|
|
86
|
+
"deliberate.close": "deliberation/1.0",
|
|
87
|
+
"deliberate.comment": "deliberation/1.0",
|
|
88
|
+
"deliberate.open": "deliberation/1.0",
|
|
89
|
+
"deliberate.vote": "deliberation/1.0",
|
|
90
|
+
"escalate.auto": "routing/1.0",
|
|
91
|
+
"escalate.raise": "review/1.0",
|
|
92
|
+
"handoff.accept": "handoff/1.0",
|
|
93
|
+
"handoff.decline": "handoff/1.0",
|
|
94
|
+
"handoff.propose": "handoff/1.0",
|
|
95
|
+
"participant.join": "core/1.0",
|
|
96
|
+
"participant.leave": "core/1.0",
|
|
97
|
+
"participant.revoke_key": "core/1.0",
|
|
98
|
+
"participant.rotate_key": "core/1.0",
|
|
99
|
+
"review.depth": "routing/1.0",
|
|
100
|
+
"review.request": "review/1.0",
|
|
101
|
+
"task.complete": "core/1.0",
|
|
102
|
+
"task.create": "core/1.0",
|
|
103
|
+
"task.route": "routing/1.0",
|
|
104
|
+
"task.update": "core/1.0",
|
|
105
|
+
"whisper.answer": "whisper/1.0",
|
|
106
|
+
"whisper.ask": "whisper/1.0",
|
|
107
|
+
"workspace.create": "core/1.0",
|
|
108
|
+
"workspace.describe": "core/1.0",
|
|
109
|
+
"workspace.set_profiles": "core/1.0"
|
|
110
|
+
});
|
|
111
|
+
var ALWAYS_AVAILABLE = /* @__PURE__ */ new Set([
|
|
112
|
+
"workspace.describe",
|
|
113
|
+
"audit.read",
|
|
114
|
+
"audit.verify_chain",
|
|
115
|
+
"audit.verify_receipt"
|
|
116
|
+
]);
|
|
117
|
+
|
|
55
118
|
// src/crypto.ts
|
|
56
119
|
import {
|
|
57
120
|
createPrivateKey,
|
|
@@ -271,11 +334,84 @@ function isValidEnvelope(env) {
|
|
|
271
334
|
return e.jsonrpc === "2.0";
|
|
272
335
|
}
|
|
273
336
|
|
|
337
|
+
// src/audit.ts
|
|
338
|
+
function entryRecord(entry) {
|
|
339
|
+
if (entry.outcome != null) return { outcome: entry.outcome, request: entry.request };
|
|
340
|
+
return entry.envelope;
|
|
341
|
+
}
|
|
342
|
+
function entryCall(entry) {
|
|
343
|
+
return entry.envelope ?? entry.request ?? void 0;
|
|
344
|
+
}
|
|
345
|
+
function isRefusal(entry) {
|
|
346
|
+
return entry.outcome != null;
|
|
347
|
+
}
|
|
348
|
+
function isCall(value) {
|
|
349
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
350
|
+
const v = value;
|
|
351
|
+
return v.jsonrpc === "2.0" && typeof v.method === "string";
|
|
352
|
+
}
|
|
353
|
+
function entryIsWellFormed(entry) {
|
|
354
|
+
const accepted = entry.envelope != null;
|
|
355
|
+
const refused = entry.request != null;
|
|
356
|
+
if (accepted === refused) return false;
|
|
357
|
+
if (accepted) return isCall(entry.envelope) && entry.outcome == null;
|
|
358
|
+
const o = entry.outcome;
|
|
359
|
+
return isCall(entry.request) && o != null && typeof o === "object" && o.status === "refused" && Number.isInteger(o.code);
|
|
360
|
+
}
|
|
361
|
+
function isSigned(call2) {
|
|
362
|
+
return call2 !== null && typeof call2 === "object" && typeof call2.sig === "string";
|
|
363
|
+
}
|
|
364
|
+
function signedDigest(call2) {
|
|
365
|
+
const signed = { ...call2 };
|
|
366
|
+
delete signed.sig;
|
|
367
|
+
return sha256Hex(canonicalize(signed));
|
|
368
|
+
}
|
|
369
|
+
function linkHash(record, prev) {
|
|
370
|
+
return sha256Hex(Buffer.concat([canonicalize(record), Buffer.from(prev, "utf-8")]));
|
|
371
|
+
}
|
|
372
|
+
var UNRECORDED_CODES = /* @__PURE__ */ new Set([
|
|
373
|
+
E.PARSE,
|
|
374
|
+
E.REQUEST,
|
|
375
|
+
E.PARAMS,
|
|
376
|
+
E.INTERNAL,
|
|
377
|
+
E.SIG_VERIFY_FAILED,
|
|
378
|
+
E.SIG_KEY_NOT_FOUND,
|
|
379
|
+
E.SIG_KEY_REVOKED,
|
|
380
|
+
E.SIG_ROTATION_KEY_MISMATCH
|
|
381
|
+
]);
|
|
382
|
+
var UNRECORDED_METHODS = /* @__PURE__ */ new Set(["workspace.create", "participant.join"]);
|
|
383
|
+
function refusalIsRecorded(method, error, privileged) {
|
|
384
|
+
if (!Number.isInteger(error.code)) return false;
|
|
385
|
+
if (UNRECORDED_METHODS.has(method)) return false;
|
|
386
|
+
if (UNRECORDED_CODES.has(error.code)) return false;
|
|
387
|
+
if (error.code === E.METHOD) {
|
|
388
|
+
const data = error.data;
|
|
389
|
+
return typeof data?.profile === "string" && privileged.has(method);
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
274
394
|
// src/patch.ts
|
|
275
395
|
var PatchError = class extends Error {
|
|
276
396
|
};
|
|
277
397
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
278
398
|
var ARRAY_INDEX_RE = /^(0|[1-9][0-9]*)$/;
|
|
399
|
+
function jsonType(value) {
|
|
400
|
+
if (value === null) return "null";
|
|
401
|
+
if (Array.isArray(value)) return "array";
|
|
402
|
+
switch (typeof value) {
|
|
403
|
+
case "object":
|
|
404
|
+
return "object";
|
|
405
|
+
case "string":
|
|
406
|
+
return "string";
|
|
407
|
+
case "number":
|
|
408
|
+
return "number";
|
|
409
|
+
case "boolean":
|
|
410
|
+
return "boolean";
|
|
411
|
+
default:
|
|
412
|
+
return "null";
|
|
413
|
+
}
|
|
414
|
+
}
|
|
279
415
|
function arrayIndex(seg) {
|
|
280
416
|
if (!ARRAY_INDEX_RE.test(seg)) throw new PatchError(`Array index expected at ${JSON.stringify(seg)}`);
|
|
281
417
|
return Number(seg);
|
|
@@ -315,7 +451,7 @@ function navigate(doc, parts) {
|
|
|
315
451
|
}
|
|
316
452
|
parent = parent[part];
|
|
317
453
|
} else {
|
|
318
|
-
throw new PatchError(`Cannot traverse into ${
|
|
454
|
+
throw new PatchError(`Cannot traverse into ${jsonType(parent)} at ${JSON.stringify(part)}`);
|
|
319
455
|
}
|
|
320
456
|
}
|
|
321
457
|
const last = parts[parts.length - 1];
|
|
@@ -385,7 +521,7 @@ function applyOne(doc, op) {
|
|
|
385
521
|
} else if (parent !== null && typeof parent === "object") {
|
|
386
522
|
parent[key] = op.value;
|
|
387
523
|
} else {
|
|
388
|
-
throw new PatchError(`Cannot add into ${
|
|
524
|
+
throw new PatchError(`Cannot add into ${jsonType(parent)}`);
|
|
389
525
|
}
|
|
390
526
|
return doc;
|
|
391
527
|
}
|
|
@@ -395,7 +531,7 @@ function applyOne(doc, op) {
|
|
|
395
531
|
if (parts.length === 0) return op.value;
|
|
396
532
|
const [parent, key] = navigate(doc, parts);
|
|
397
533
|
if (Array.isArray(parent)) {
|
|
398
|
-
const idx = key;
|
|
534
|
+
const idx = key === "-" ? -1 : key;
|
|
399
535
|
if (idx < 0 || idx >= parent.length) {
|
|
400
536
|
throw new PatchError(`Path not found for replace: ${path}`);
|
|
401
537
|
}
|
|
@@ -406,7 +542,7 @@ function applyOne(doc, op) {
|
|
|
406
542
|
}
|
|
407
543
|
parent[key] = op.value;
|
|
408
544
|
} else {
|
|
409
|
-
throw new PatchError(`Cannot replace in ${
|
|
545
|
+
throw new PatchError(`Cannot replace in ${jsonType(parent)}`);
|
|
410
546
|
}
|
|
411
547
|
return doc;
|
|
412
548
|
}
|
|
@@ -415,7 +551,7 @@ function applyOne(doc, op) {
|
|
|
415
551
|
if (parts.length === 0) throw new PatchError("Cannot remove root.");
|
|
416
552
|
const [parent, key] = navigate(doc, parts);
|
|
417
553
|
if (Array.isArray(parent)) {
|
|
418
|
-
const idx = key;
|
|
554
|
+
const idx = key === "-" ? -1 : key;
|
|
419
555
|
if (idx < 0 || idx >= parent.length) {
|
|
420
556
|
throw new PatchError(`Index out of range for remove: ${path}`);
|
|
421
557
|
}
|
|
@@ -426,7 +562,7 @@ function applyOne(doc, op) {
|
|
|
426
562
|
}
|
|
427
563
|
delete parent[key];
|
|
428
564
|
} else {
|
|
429
|
-
throw new PatchError(`Cannot remove from ${
|
|
565
|
+
throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
|
|
430
566
|
}
|
|
431
567
|
return doc;
|
|
432
568
|
}
|
|
@@ -634,7 +770,8 @@ function modeLE(a, ceiling) {
|
|
|
634
770
|
}
|
|
635
771
|
|
|
636
772
|
// src/profiles/audit_scitt.ts
|
|
637
|
-
|
|
773
|
+
var MALFORMED = "sha256:malformed";
|
|
774
|
+
function buildStatement(workspaceId, record, issuer) {
|
|
638
775
|
return {
|
|
639
776
|
protected: {
|
|
640
777
|
alg: -8,
|
|
@@ -642,9 +779,9 @@ function buildStatement(workspaceId, envelope, issuer) {
|
|
|
642
779
|
iss: issuer,
|
|
643
780
|
kid: "scitt-issuer",
|
|
644
781
|
cwt_claims: { sub: workspaceId, iat: null },
|
|
645
|
-
"content-type": "application/chap+json;version=0.
|
|
782
|
+
"content-type": "application/chap+json;version=0.3"
|
|
646
783
|
},
|
|
647
|
-
payload: canonicalize(
|
|
784
|
+
payload: canonicalize(record).toString("utf-8"),
|
|
648
785
|
signature: "<deployment-supplied>"
|
|
649
786
|
};
|
|
650
787
|
}
|
|
@@ -657,7 +794,7 @@ function registerAuditScitt(coord) {
|
|
|
657
794
|
const toSeq = range.to_seq ?? ws.audit.length;
|
|
658
795
|
const issuer = p.issuer ?? "service:coordinator";
|
|
659
796
|
if (!coord.options.scittSubmitter) {
|
|
660
|
-
const statements = ws.audit.slice(fromSeq, toSeq).map((e) => buildStatement(ws.id, e
|
|
797
|
+
const statements = ws.audit.slice(fromSeq, toSeq).map((e) => buildStatement(ws.id, entryRecord(e), issuer));
|
|
661
798
|
return { result: {
|
|
662
799
|
statements,
|
|
663
800
|
note: "No scittSubmitter configured; submit these out-of-band"
|
|
@@ -665,7 +802,7 @@ function registerAuditScitt(coord) {
|
|
|
665
802
|
}
|
|
666
803
|
const receipts = [];
|
|
667
804
|
for (const entry of ws.audit.slice(fromSeq, toSeq)) {
|
|
668
|
-
const statement = buildStatement(ws.id, entry
|
|
805
|
+
const statement = buildStatement(ws.id, entryRecord(entry), issuer);
|
|
669
806
|
let receipt;
|
|
670
807
|
try {
|
|
671
808
|
receipt = coord.options.scittSubmitter(statement);
|
|
@@ -717,12 +854,20 @@ function registerAuditScitt(coord) {
|
|
|
717
854
|
if (start < 0) start = ws.audit.length;
|
|
718
855
|
const errors = [];
|
|
719
856
|
let prev = ZERO_HASH;
|
|
857
|
+
let resync = false;
|
|
720
858
|
for (const e of ws.audit.slice(start)) {
|
|
721
|
-
const expectedPrev = prev;
|
|
859
|
+
const expectedPrev = resync && typeof e.prev_hash === "string" ? e.prev_hash : prev;
|
|
860
|
+
resync = false;
|
|
722
861
|
if (e.prev_hash !== expectedPrev) {
|
|
723
862
|
errors.push(`seq ${e.seq}: prev_hash mismatch`);
|
|
724
863
|
}
|
|
725
|
-
|
|
864
|
+
if (!entryIsWellFormed(e)) {
|
|
865
|
+
errors.push(`seq ${e.seq}: malformed entry`);
|
|
866
|
+
prev = MALFORMED;
|
|
867
|
+
resync = true;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
prev = linkHash(entryRecord(e), expectedPrev);
|
|
726
871
|
}
|
|
727
872
|
const storedHead = ws.chain_head === void 0 || ws.chain_head === null ? ZERO_HASH : ws.chain_head;
|
|
728
873
|
if (prev !== storedHead) {
|
|
@@ -777,6 +922,7 @@ function registerControl(coord) {
|
|
|
777
922
|
}
|
|
778
923
|
task.paused = true;
|
|
779
924
|
const prior = task.state;
|
|
925
|
+
if (prior !== "paused") task.paused_from = prior;
|
|
780
926
|
task.state = "paused";
|
|
781
927
|
task.updated_at = coord.now();
|
|
782
928
|
task.history.push({ ts: task.updated_at, from: p.from, state: "paused", note: `was ${prior}` });
|
|
@@ -810,10 +956,12 @@ function registerControl(coord) {
|
|
|
810
956
|
return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot resume ${task.state} task`) };
|
|
811
957
|
}
|
|
812
958
|
task.paused = false;
|
|
813
|
-
task.
|
|
959
|
+
const restored = task.paused_from ?? "in_progress";
|
|
960
|
+
task.paused_from = void 0;
|
|
961
|
+
task.state = restored;
|
|
814
962
|
task.updated_at = coord.now();
|
|
815
|
-
task.history.push({ ts: task.updated_at, from: p.from, state:
|
|
816
|
-
return { result: { scope: "task", task_id: task.id, state:
|
|
963
|
+
task.history.push({ ts: task.updated_at, from: p.from, state: restored });
|
|
964
|
+
return { result: { scope: "task", task_id: task.id, state: restored } };
|
|
817
965
|
}
|
|
818
966
|
if (scope === "participant") {
|
|
819
967
|
const uri = p.participant_uri || p.uri;
|
|
@@ -840,36 +988,56 @@ function registerControl(coord) {
|
|
|
840
988
|
coord.handlers.set("control.snapshot", (p) => {
|
|
841
989
|
const ws = coord.workspaces.get(p.workspace);
|
|
842
990
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
843
|
-
|
|
991
|
+
if (Array.isArray(p.include) && p.include.length === 0) {
|
|
992
|
+
return { error: rpcError(E.PARAMS, "include must not be empty") };
|
|
993
|
+
}
|
|
994
|
+
const include = Array.isArray(p.include) ? [...p.include] : ["members", "open_tasks", "mode_ceiling"];
|
|
844
995
|
const state = {};
|
|
845
996
|
if (include.includes("members")) {
|
|
846
|
-
|
|
997
|
+
const members = Array.from(ws.members.values()).map((m) => ({
|
|
847
998
|
uri: m.uri,
|
|
848
999
|
type: m.type,
|
|
849
|
-
role: m.role,
|
|
850
|
-
scopes: m.scopes
|
|
1000
|
+
...m.role ? { role: m.role } : {},
|
|
1001
|
+
...m.scopes?.length ? { scopes: m.scopes } : {}
|
|
851
1002
|
}));
|
|
1003
|
+
if (members.length) state.members = members;
|
|
852
1004
|
}
|
|
853
1005
|
if (include.includes("open_tasks")) {
|
|
854
|
-
|
|
1006
|
+
const tasks = Array.from(ws.tasks.values()).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled" && t.state !== "superseded").map((t) => ({
|
|
1007
|
+
id: t.id,
|
|
1008
|
+
kind: t.kind,
|
|
1009
|
+
state: t.state,
|
|
1010
|
+
...t.assignee ? { assignee: t.assignee } : {}
|
|
1011
|
+
}));
|
|
1012
|
+
if (tasks.length) state.open_tasks = tasks;
|
|
855
1013
|
}
|
|
856
1014
|
if (include.includes("mode_ceiling")) state.mode_ceiling = ws.mode_ceiling;
|
|
857
|
-
if (include.includes("policy")) state.routing_policy_uri = ws.routing_policy_uri;
|
|
1015
|
+
if (include.includes("policy") && ws.routing_policy_uri) state.routing_policy_uri = ws.routing_policy_uri;
|
|
858
1016
|
if (include.includes("audit") || include.includes("all")) state.audit_seq = ws.audit.length;
|
|
1017
|
+
const content = structuredClone({
|
|
1018
|
+
workspace: ws.id,
|
|
1019
|
+
audit_seq: ws.audit.length,
|
|
1020
|
+
include,
|
|
1021
|
+
state,
|
|
1022
|
+
...p.label ? { label: p.label } : {}
|
|
1023
|
+
});
|
|
859
1024
|
const snapId = coord.ids.artefactId();
|
|
860
1025
|
const snap = {
|
|
861
1026
|
id: snapId,
|
|
862
1027
|
kind: "snapshot",
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
label: p.label,
|
|
868
|
-
include,
|
|
869
|
-
state
|
|
1028
|
+
produced_at: coord.now(),
|
|
1029
|
+
produced_by: p.from,
|
|
1030
|
+
content_hash: contentHash(content),
|
|
1031
|
+
content
|
|
870
1032
|
};
|
|
871
1033
|
ws.snapshots.set(snapId, snap);
|
|
872
|
-
return {
|
|
1034
|
+
return {
|
|
1035
|
+
result: {
|
|
1036
|
+
snapshot_artefact_id: snapId,
|
|
1037
|
+
audit_seq: snap.content.audit_seq,
|
|
1038
|
+
artefact: structuredClone(snap)
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
873
1041
|
});
|
|
874
1042
|
coord.handlers.set("control.rollback", (p) => {
|
|
875
1043
|
const ws = coord.workspaces.get(p.workspace);
|
|
@@ -878,31 +1046,34 @@ function registerControl(coord) {
|
|
|
878
1046
|
if (!snapId) return { error: rpcError(E.PARAMS, "to_snapshot_artefact_id is required") };
|
|
879
1047
|
const snap = ws.snapshots.get(snapId);
|
|
880
1048
|
if (!snap) return { error: rpcError(E.CONTROL_SNAPSHOT_NOT_FOUND, `Unknown snapshot: ${snapId}`) };
|
|
881
|
-
|
|
1049
|
+
if (Array.isArray(p.what_to_restore) && p.what_to_restore.length === 0) {
|
|
1050
|
+
return { error: rpcError(E.PARAMS, "what_to_restore must not be empty") };
|
|
1051
|
+
}
|
|
1052
|
+
const what = p.what_to_restore ?? snap.content.include;
|
|
882
1053
|
const restored = [];
|
|
883
|
-
if (what.includes("mode_ceiling") && snap.state.mode_ceiling) {
|
|
884
|
-
ws.mode_ceiling = snap.state.mode_ceiling;
|
|
1054
|
+
if (what.includes("mode_ceiling") && snap.content.state.mode_ceiling) {
|
|
1055
|
+
ws.mode_ceiling = snap.content.state.mode_ceiling;
|
|
885
1056
|
restored.push("mode_ceiling");
|
|
886
1057
|
}
|
|
887
|
-
if (what.includes("members") && Array.isArray(snap.state.members)) {
|
|
1058
|
+
if (what.includes("members") && Array.isArray(snap.content.state.members)) {
|
|
888
1059
|
const snapByUri = /* @__PURE__ */ new Map();
|
|
889
|
-
for (const m of snap.state.members) {
|
|
1060
|
+
for (const m of snap.content.state.members) {
|
|
890
1061
|
snapByUri.set(m.uri, m);
|
|
891
1062
|
}
|
|
892
1063
|
for (const [uri, snapM] of snapByUri) {
|
|
893
1064
|
if (ws.members.has(uri)) {
|
|
894
1065
|
const cur = ws.members.get(uri);
|
|
895
1066
|
cur.role = snapM.role ?? cur.role;
|
|
896
|
-
cur.scopes = snapM.scopes;
|
|
1067
|
+
cur.scopes = Array.isArray(snapM.scopes) ? [...snapM.scopes] : void 0;
|
|
897
1068
|
}
|
|
898
1069
|
}
|
|
899
1070
|
restored.push("members");
|
|
900
1071
|
}
|
|
901
1072
|
return { result: {
|
|
902
1073
|
rolled_back_to: snapId,
|
|
903
|
-
audit_seq: snap.audit_seq,
|
|
1074
|
+
audit_seq: snap.content.audit_seq,
|
|
904
1075
|
restored,
|
|
905
|
-
reason: p.reason
|
|
1076
|
+
...p.reason ? { reason: p.reason } : {}
|
|
906
1077
|
} };
|
|
907
1078
|
});
|
|
908
1079
|
coord.handlers.set("control.supersede", (p) => {
|
|
@@ -916,16 +1087,25 @@ function registerControl(coord) {
|
|
|
916
1087
|
return { error: rpcError(E.PARAMS, "successor_task must include kind") };
|
|
917
1088
|
}
|
|
918
1089
|
const assignee = newSpec.assignee || old.assignee;
|
|
919
|
-
if (!ws.members.has(assignee)) {
|
|
1090
|
+
if (typeof assignee !== "string" || !ws.members.has(assignee)) {
|
|
920
1091
|
return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
|
|
921
1092
|
}
|
|
922
|
-
|
|
1093
|
+
if (ws.members.get(assignee).paused) {
|
|
1094
|
+
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
|
|
1095
|
+
}
|
|
1096
|
+
if (newSpec.mode !== void 0 && newSpec.mode !== null && typeof newSpec.mode !== "string") {
|
|
1097
|
+
return { error: rpcError(E.PARAMS, "successor_task.mode must be a string") };
|
|
1098
|
+
}
|
|
1099
|
+
const requestedMode = newSpec.mode || old.mode;
|
|
923
1100
|
if (!modeLE(requestedMode, ws.mode_ceiling)) {
|
|
924
1101
|
return { error: rpcError(
|
|
925
1102
|
E.MODE_CEILING_EXCEEDED,
|
|
926
1103
|
`Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
|
|
927
1104
|
) };
|
|
928
1105
|
}
|
|
1106
|
+
if (newSpec.review_required !== void 0 && newSpec.review_required !== null && typeof newSpec.review_required !== "boolean") {
|
|
1107
|
+
return { error: rpcError(E.PARAMS, "successor_task.review_required must be a boolean") };
|
|
1108
|
+
}
|
|
929
1109
|
const now = coord.now();
|
|
930
1110
|
const newId = coord.ids.taskId();
|
|
931
1111
|
const newTask = {
|
|
@@ -947,8 +1127,11 @@ function registerControl(coord) {
|
|
|
947
1127
|
}],
|
|
948
1128
|
paused: false
|
|
949
1129
|
};
|
|
950
|
-
if (ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial")
|
|
951
|
-
|
|
1130
|
+
if (old.review_required || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
|
|
1131
|
+
newTask.review_required = true;
|
|
1132
|
+
} else if ("review_required" in newSpec) {
|
|
1133
|
+
newTask.review_required = !!newSpec.review_required;
|
|
1134
|
+
}
|
|
952
1135
|
ws.tasks.set(newId, newTask);
|
|
953
1136
|
old.state = "superseded";
|
|
954
1137
|
old.superseded_by = newId;
|
|
@@ -1245,8 +1428,14 @@ function registerHandoff(coord) {
|
|
|
1245
1428
|
) };
|
|
1246
1429
|
}
|
|
1247
1430
|
}
|
|
1431
|
+
if (ws.members.get(acceptor).paused) {
|
|
1432
|
+
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Acceptor ${acceptor} is paused`) };
|
|
1433
|
+
}
|
|
1434
|
+
if (Array.isArray(p.accepted_task_ids) && p.accepted_task_ids.length === 0) {
|
|
1435
|
+
return { error: rpcError(E.PARAMS, "accepted_task_ids must not be empty") };
|
|
1436
|
+
}
|
|
1248
1437
|
const validIds = new Set(ho.tasks.map((t) => t.task_id));
|
|
1249
|
-
const acceptedIds = Array.isArray(p.accepted_task_ids) ? p.accepted_task_ids : ho.tasks.map((t) => t.task_id);
|
|
1438
|
+
const acceptedIds = Array.isArray(p.accepted_task_ids) ? [...p.accepted_task_ids] : ho.tasks.map((t) => t.task_id);
|
|
1250
1439
|
for (const tid of acceptedIds) {
|
|
1251
1440
|
if (!validIds.has(tid)) {
|
|
1252
1441
|
return { error: rpcError(E.PARAMS, `Task ${tid} not in this handoff`) };
|
|
@@ -1317,6 +1506,9 @@ function registerRouting(coord) {
|
|
|
1317
1506
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1318
1507
|
const task = ws.tasks.get(p.task_id);
|
|
1319
1508
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
1509
|
+
if (p.candidates !== void 0 && p.candidates !== null && !Array.isArray(p.candidates)) {
|
|
1510
|
+
return { error: rpcError(E.PARAMS, "candidates must be a list") };
|
|
1511
|
+
}
|
|
1320
1512
|
const candidates = p.candidates ?? [];
|
|
1321
1513
|
if (!candidates.length) {
|
|
1322
1514
|
return { error: rpcError(E.ROUTING_CANDIDATES_EMPTY, "candidates array was empty") };
|
|
@@ -1333,19 +1525,29 @@ function registerRouting(coord) {
|
|
|
1333
1525
|
return { error: rpcError(E.ROUTING_POLICY_UNREACHABLE, `routing policy error: ${msg}`) };
|
|
1334
1526
|
}
|
|
1335
1527
|
} else {
|
|
1336
|
-
const
|
|
1528
|
+
const memberOf = (c) => typeof c === "string" ? ws.members.get(c) : void 0;
|
|
1529
|
+
const eligible = candidates.filter((c) => {
|
|
1530
|
+
const m = memberOf(c);
|
|
1531
|
+
return !!m && !m.paused;
|
|
1532
|
+
});
|
|
1337
1533
|
if (!eligible.length) {
|
|
1338
|
-
return { error: rpcError(E.ROUTING_NO_ELIGIBLE_ASSIGNEE, "No candidate is a workspace member") };
|
|
1534
|
+
return { error: rpcError(E.ROUTING_NO_ELIGIBLE_ASSIGNEE, "No candidate is a workspace member who is not paused") };
|
|
1339
1535
|
}
|
|
1340
1536
|
selected = eligible[0];
|
|
1537
|
+
const alternatives = [];
|
|
1538
|
+
let chosen = false;
|
|
1539
|
+
for (const c of candidates) {
|
|
1540
|
+
const m = memberOf(c);
|
|
1541
|
+
if (!m) continue;
|
|
1542
|
+
if (m.paused) alternatives.push({ candidate: c, reason_excluded: "paused" });
|
|
1543
|
+
else if (!chosen) chosen = true;
|
|
1544
|
+
else alternatives.push({ candidate: c, reason_excluded: "not first eligible" });
|
|
1545
|
+
}
|
|
1341
1546
|
rationale = {
|
|
1342
1547
|
policy_id: "default",
|
|
1343
1548
|
hints_used: Object.keys(task.routing_hints ?? {}),
|
|
1344
1549
|
summary: "default policy: first eligible candidate",
|
|
1345
|
-
alternatives_considered:
|
|
1346
|
-
candidate: c,
|
|
1347
|
-
reason_excluded: "not first eligible"
|
|
1348
|
-
}))
|
|
1550
|
+
alternatives_considered: alternatives
|
|
1349
1551
|
};
|
|
1350
1552
|
}
|
|
1351
1553
|
if (!ws.members.has(selected)) {
|
|
@@ -1354,6 +1556,9 @@ function registerRouting(coord) {
|
|
|
1354
1556
|
`Selected ${JSON.stringify(selected)} is not a member`
|
|
1355
1557
|
) };
|
|
1356
1558
|
}
|
|
1559
|
+
if (ws.members.get(selected).paused) {
|
|
1560
|
+
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${selected} is paused`) };
|
|
1561
|
+
}
|
|
1357
1562
|
task.assignee = selected;
|
|
1358
1563
|
task.updated_at = coord.now();
|
|
1359
1564
|
task.history.push({
|
|
@@ -1471,6 +1676,18 @@ function registerRouting(coord) {
|
|
|
1471
1676
|
hints_used: ["criticality", "confidence"]
|
|
1472
1677
|
};
|
|
1473
1678
|
}
|
|
1679
|
+
if (escalate) {
|
|
1680
|
+
if (!to) return { error: rpcError(
|
|
1681
|
+
E.ROUTING_ESC_TARGET_UNAVAILABLE,
|
|
1682
|
+
"Escalation triggered but no target available"
|
|
1683
|
+
) };
|
|
1684
|
+
if (!to.startsWith("group:") && !ws.members.has(to)) {
|
|
1685
|
+
return { error: rpcError(
|
|
1686
|
+
E.ROUTING_ESC_TARGET_UNAVAILABLE,
|
|
1687
|
+
`Escalation target ${to} is not a member or group`
|
|
1688
|
+
) };
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1474
1691
|
const artId = coord.ids.artefactId();
|
|
1475
1692
|
const artefact = {
|
|
1476
1693
|
id: artId,
|
|
@@ -1487,17 +1704,7 @@ function registerRouting(coord) {
|
|
|
1487
1704
|
};
|
|
1488
1705
|
ws.route_decisions.set(artId, artefact);
|
|
1489
1706
|
if (escalate) {
|
|
1490
|
-
if (
|
|
1491
|
-
E.ROUTING_ESC_TARGET_UNAVAILABLE,
|
|
1492
|
-
"Escalation triggered but no target available"
|
|
1493
|
-
) };
|
|
1494
|
-
if (!to.startsWith("group:") && !ws.members.has(to)) {
|
|
1495
|
-
return { error: rpcError(
|
|
1496
|
-
E.ROUTING_ESC_TARGET_UNAVAILABLE,
|
|
1497
|
-
`Escalation target ${to} is not a member or group`
|
|
1498
|
-
) };
|
|
1499
|
-
}
|
|
1500
|
-
if (coord.options.onAutoEscalate) {
|
|
1707
|
+
if (coord.options.onAutoEscalate && to) {
|
|
1501
1708
|
try {
|
|
1502
1709
|
coord.options.onAutoEscalate(task, to);
|
|
1503
1710
|
} catch {
|
|
@@ -1567,6 +1774,12 @@ function registerSecuritySigned(coord) {
|
|
|
1567
1774
|
function parseIso(ts) {
|
|
1568
1775
|
return new Date(ts).getTime();
|
|
1569
1776
|
}
|
|
1777
|
+
function present(v) {
|
|
1778
|
+
if (v === void 0 || v === null || v === false || v === 0 || v === "") return false;
|
|
1779
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
1780
|
+
if (typeof v === "object") return Object.keys(v).length > 0;
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1570
1783
|
function registerWhisper(coord) {
|
|
1571
1784
|
coord.handlers.set("whisper.ask", (p) => {
|
|
1572
1785
|
const ws = coord.workspaces.get(p.workspace);
|
|
@@ -1577,6 +1790,9 @@ function registerWhisper(coord) {
|
|
|
1577
1790
|
if (!ws.tasks.has(p.task_id)) {
|
|
1578
1791
|
return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
1579
1792
|
}
|
|
1793
|
+
if (p.options !== void 0 && p.options !== null && !Array.isArray(p.options)) {
|
|
1794
|
+
return { error: rpcError(E.PARAMS, "options must be a list") };
|
|
1795
|
+
}
|
|
1580
1796
|
const to = p.to;
|
|
1581
1797
|
const askee = Array.isArray(to) ? to : [to];
|
|
1582
1798
|
const id = p.whisper_id || coord.ids.artefactId();
|
|
@@ -1620,22 +1836,21 @@ function registerWhisper(coord) {
|
|
|
1620
1836
|
return { error: rpcError(E.WHISPER_LAPSED, "Whisper already lapsed") };
|
|
1621
1837
|
}
|
|
1622
1838
|
const answerOption = p.answer_option;
|
|
1623
|
-
const
|
|
1839
|
+
const hasOption = answerOption !== void 0 && answerOption !== null;
|
|
1840
|
+
const answerText = present(p.answer) ? p.answer : p.answer_text;
|
|
1624
1841
|
if (prompt.options && prompt.options.length) {
|
|
1625
|
-
if (
|
|
1842
|
+
if (!hasOption) {
|
|
1626
1843
|
return { error: rpcError(E.PARAMS, "answer_option is required when options are defined") };
|
|
1627
1844
|
}
|
|
1628
1845
|
const valid = new Set(prompt.options.map((o) => o.id));
|
|
1629
1846
|
if (!valid.has(answerOption)) {
|
|
1630
1847
|
return { error: rpcError(
|
|
1631
1848
|
E.WHISPER_OPTION_NOT_IN_SET,
|
|
1632
|
-
`Answer option ${
|
|
1849
|
+
`Answer option ${canonicalize(answerOption).toString("utf-8")} not in option set`
|
|
1633
1850
|
) };
|
|
1634
1851
|
}
|
|
1635
|
-
} else {
|
|
1636
|
-
|
|
1637
|
-
return { error: rpcError(E.PARAMS, "answer or answer_option required") };
|
|
1638
|
-
}
|
|
1852
|
+
} else if (!present(answerText) && !hasOption) {
|
|
1853
|
+
return { error: rpcError(E.PARAMS, "answer or answer_option required") };
|
|
1639
1854
|
}
|
|
1640
1855
|
prompt.state = "answered";
|
|
1641
1856
|
prompt.answered_at = coord.now();
|
|
@@ -1643,7 +1858,6 @@ function registerWhisper(coord) {
|
|
|
1643
1858
|
prompt.answer_option = answerOption;
|
|
1644
1859
|
prompt.answer_text = answerText;
|
|
1645
1860
|
prompt.comment = p.comment;
|
|
1646
|
-
p.task_id = prompt.task_id;
|
|
1647
1861
|
return { result: { answered: true, whisper_id: prompt.id, task_id: prompt.task_id } };
|
|
1648
1862
|
});
|
|
1649
1863
|
coord.checkWhisperLapses = (workspaceId, now) => {
|
|
@@ -1696,14 +1910,38 @@ var PRIVILEGED_METHODS = /* @__PURE__ */ new Set([
|
|
|
1696
1910
|
"participant.rotate_key",
|
|
1697
1911
|
"participant.revoke_key"
|
|
1698
1912
|
]);
|
|
1913
|
+
var MEMBER_ONLY_METHODS = /* @__PURE__ */ new Set([
|
|
1914
|
+
"task.route",
|
|
1915
|
+
"review.depth",
|
|
1916
|
+
"escalate.auto",
|
|
1917
|
+
"participant.leave"
|
|
1918
|
+
]);
|
|
1699
1919
|
var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
|
|
1700
1920
|
"workspace.describe",
|
|
1701
1921
|
"audit.read",
|
|
1702
1922
|
"audit.verify_chain",
|
|
1703
|
-
"audit.verify_receipt"
|
|
1923
|
+
"audit.verify_receipt",
|
|
1924
|
+
// Submitting the chain to a transparency service reads it and sends it
|
|
1925
|
+
// onward. Recording the submission would append to the very log being
|
|
1926
|
+
// submitted, so the receipt would attest a chain one entry shorter.
|
|
1927
|
+
"audit.submit_to_scitt"
|
|
1704
1928
|
]);
|
|
1705
1929
|
var MAX_IDEMPOTENCY_KEYS = 1e4;
|
|
1706
1930
|
var DEFAULT_MAX_ENVELOPE_BYTES = 1048576;
|
|
1931
|
+
var MAX_NESTING = 64;
|
|
1932
|
+
function withinNesting(value, limit) {
|
|
1933
|
+
const stack = [[value, 1]];
|
|
1934
|
+
while (stack.length) {
|
|
1935
|
+
const [node, depth] = stack.pop();
|
|
1936
|
+
if (depth > limit) return false;
|
|
1937
|
+
if (Array.isArray(node)) {
|
|
1938
|
+
for (const v of node) stack.push([v, depth + 1]);
|
|
1939
|
+
} else if (node !== null && typeof node === "object") {
|
|
1940
|
+
for (const v of Object.values(node)) stack.push([v, depth + 1]);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return true;
|
|
1944
|
+
}
|
|
1707
1945
|
function nowIso(clockMs) {
|
|
1708
1946
|
const d = clockMs === void 0 ? /* @__PURE__ */ new Date() : new Date(clockMs);
|
|
1709
1947
|
return d.toISOString().replace(/\.(\d{3})Z$/, ".$1Z");
|
|
@@ -1719,9 +1957,6 @@ function tagsError(p) {
|
|
|
1719
1957
|
}
|
|
1720
1958
|
return null;
|
|
1721
1959
|
}
|
|
1722
|
-
function linkHash(envelope, prev) {
|
|
1723
|
-
return sha256Hex(Buffer.concat([canonicalize(envelope), Buffer.from(prev, "utf-8")]));
|
|
1724
|
-
}
|
|
1725
1960
|
function reviewRuleSupported(rule) {
|
|
1726
1961
|
if (rule === "any_one_approves" || rule === "all_approve") return true;
|
|
1727
1962
|
if (rule.startsWith("quorum:")) {
|
|
@@ -1747,14 +1982,35 @@ function reply(env, body) {
|
|
|
1747
1982
|
else out.result = body.result;
|
|
1748
1983
|
return out;
|
|
1749
1984
|
}
|
|
1985
|
+
function indexSigned(index, entry) {
|
|
1986
|
+
const call2 = entryCall(entry);
|
|
1987
|
+
if (!isSigned(call2)) return;
|
|
1988
|
+
let digest;
|
|
1989
|
+
try {
|
|
1990
|
+
digest = signedDigest(call2);
|
|
1991
|
+
} catch {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (entry.outcome != null) index.refused.set(digest, { seq: entry.seq, code: entry.outcome.code });
|
|
1995
|
+
else index.accepted.add(digest);
|
|
1996
|
+
}
|
|
1997
|
+
var REVIEW_REQUEST_STOPPED = ["cancelled", "superseded", "paused"];
|
|
1750
1998
|
var Coordinator = class {
|
|
1751
1999
|
workspaces = /* @__PURE__ */ new Map();
|
|
1752
2000
|
options;
|
|
1753
2001
|
ids;
|
|
1754
2002
|
store;
|
|
1755
2003
|
wsVersions = /* @__PURE__ */ new Map();
|
|
2004
|
+
saveChains = /* @__PURE__ */ new Map();
|
|
1756
2005
|
clockMs;
|
|
2006
|
+
frozenNow;
|
|
1757
2007
|
auditListeners = [];
|
|
2008
|
+
/**
|
|
2009
|
+
* The signed calls on each workspace's log, by what their senders signed:
|
|
2010
|
+
* recorded refusals with their seq and code, and accepted calls. Rebuilt
|
|
2011
|
+
* from the log when missing, so a snapshot never carries it.
|
|
2012
|
+
*/
|
|
2013
|
+
signedIndexes = /* @__PURE__ */ new WeakMap();
|
|
1758
2014
|
/** Method handler registry; profiles plug into this. */
|
|
1759
2015
|
handlers = /* @__PURE__ */ new Map();
|
|
1760
2016
|
/** Custom lapse-check function (whisper/1.0). Set by the whisper profile. */
|
|
@@ -1796,42 +2052,72 @@ var Coordinator = class {
|
|
|
1796
2052
|
this.applyRecords(records);
|
|
1797
2053
|
}
|
|
1798
2054
|
applyRecords(records) {
|
|
1799
|
-
this.workspaces.clear();
|
|
1800
2055
|
this.wsVersions.clear();
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
this.wsVersions.set(r.id, r.version);
|
|
1804
|
-
}
|
|
2056
|
+
this.restore(records.map((r) => r.data));
|
|
2057
|
+
for (const r of records) this.wsVersions.set(r.id, r.version);
|
|
1805
2058
|
}
|
|
1806
2059
|
/** Persist the current snapshot of one workspace to the store. */
|
|
1807
2060
|
persist(ws) {
|
|
1808
2061
|
const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
|
|
1809
2062
|
this.wsVersions.set(ws.id, next);
|
|
2063
|
+
let record;
|
|
1810
2064
|
try {
|
|
1811
2065
|
const all = this.snapshot();
|
|
1812
2066
|
const data = all.find((w) => w.id === ws.id);
|
|
1813
2067
|
if (!data) return;
|
|
1814
|
-
|
|
1815
|
-
id: ws.id,
|
|
1816
|
-
data,
|
|
1817
|
-
version: next,
|
|
1818
|
-
updated_at: this.now()
|
|
1819
|
-
});
|
|
1820
|
-
if (result instanceof Promise) {
|
|
1821
|
-
result.catch(() => {
|
|
1822
|
-
});
|
|
1823
|
-
}
|
|
2068
|
+
record = { id: ws.id, data, version: next, updated_at: this.now() };
|
|
1824
2069
|
} catch {
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
const pending = this.saveChains.get(ws.id);
|
|
2073
|
+
if (pending) {
|
|
2074
|
+
this.trackSave(pending.then(() => this.store.save(record)), record);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
let result;
|
|
2078
|
+
try {
|
|
2079
|
+
result = this.store.save(record);
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
this.reportStoreError(err, record);
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
if (result instanceof Promise) {
|
|
2085
|
+
this.trackSave(result, record);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
trackSave(save, record) {
|
|
2089
|
+
const guarded = save.catch((err) => this.reportStoreError(err, record));
|
|
2090
|
+
this.saveChains.set(record.id, guarded);
|
|
2091
|
+
void guarded.then(() => {
|
|
2092
|
+
if (this.saveChains.get(record.id) === guarded) {
|
|
2093
|
+
this.saveChains.delete(record.id);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
reportStoreError(err, record) {
|
|
2098
|
+
if (this.options.onStoreError) {
|
|
2099
|
+
try {
|
|
2100
|
+
this.options.onStoreError(err, { id: record.id, version: record.version });
|
|
2101
|
+
} catch {
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async drainSaves() {
|
|
2106
|
+
while (this.saveChains.size) {
|
|
2107
|
+
await Promise.allSettled([...this.saveChains.values()]);
|
|
1825
2108
|
}
|
|
1826
2109
|
}
|
|
1827
2110
|
// -- lifecycle -----------------------------------------------------
|
|
1828
|
-
|
|
2111
|
+
advanceClock() {
|
|
1829
2112
|
if (this.clockMs !== void 0) {
|
|
1830
2113
|
this.clockMs += 1e3;
|
|
1831
2114
|
return nowIso(this.clockMs);
|
|
1832
2115
|
}
|
|
1833
2116
|
return nowIso();
|
|
1834
2117
|
}
|
|
2118
|
+
now() {
|
|
2119
|
+
return this.frozenNow ?? this.advanceClock();
|
|
2120
|
+
}
|
|
1835
2121
|
onAudit(listener) {
|
|
1836
2122
|
this.auditListeners.push(listener);
|
|
1837
2123
|
return () => {
|
|
@@ -1856,8 +2142,11 @@ var Coordinator = class {
|
|
|
1856
2142
|
* error-table divergence is tracked separately.)
|
|
1857
2143
|
*/
|
|
1858
2144
|
requireMember(ws, sender) {
|
|
1859
|
-
if (typeof sender !== "string"
|
|
1860
|
-
return { error: rpcError(E.NOT_AUTHORISED,
|
|
2145
|
+
if (typeof sender !== "string") {
|
|
2146
|
+
return { error: rpcError(E.NOT_AUTHORISED, "Not a workspace member: from is not a participant URI") };
|
|
2147
|
+
}
|
|
2148
|
+
if (!ws.members.has(sender)) {
|
|
2149
|
+
return { error: rpcError(E.NOT_AUTHORISED, `Not a workspace member: ${sender}`) };
|
|
1861
2150
|
}
|
|
1862
2151
|
return null;
|
|
1863
2152
|
}
|
|
@@ -1954,8 +2243,40 @@ var Coordinator = class {
|
|
|
1954
2243
|
ws.deliberations.set(x.id, x);
|
|
1955
2244
|
for (const x of w.handoffs ?? [])
|
|
1956
2245
|
ws.handoffs.set(x.id, x);
|
|
1957
|
-
for (const x of w.snapshots ?? [])
|
|
1958
|
-
|
|
2246
|
+
for (const x of w.snapshots ?? []) {
|
|
2247
|
+
const content = structuredClone(x.content ?? {
|
|
2248
|
+
workspace: x.workspace,
|
|
2249
|
+
audit_seq: x.audit_seq,
|
|
2250
|
+
include: x.include,
|
|
2251
|
+
state: x.state,
|
|
2252
|
+
...x.label ? { label: x.label } : {}
|
|
2253
|
+
});
|
|
2254
|
+
if (!x.content) {
|
|
2255
|
+
const oldState = content.state;
|
|
2256
|
+
const state = {};
|
|
2257
|
+
for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
|
|
2258
|
+
if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
|
|
2259
|
+
}
|
|
2260
|
+
for (const [key, fields] of [
|
|
2261
|
+
["members", ["uri", "type", "role", "scopes"]],
|
|
2262
|
+
["open_tasks", ["id", "kind", "state", "assignee"]]
|
|
2263
|
+
]) {
|
|
2264
|
+
const items = oldState[key];
|
|
2265
|
+
if (Array.isArray(items) && items.length) {
|
|
2266
|
+
state[key] = items.map((item) => Object.fromEntries(fields.filter((field) => item[field] != null && item[field] !== "" && (!Array.isArray(item[field]) || item[field].length > 0)).map((field) => [field, item[field]])));
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
content.state = state;
|
|
2270
|
+
}
|
|
2271
|
+
ws.snapshots.set(x.id, {
|
|
2272
|
+
id: x.id,
|
|
2273
|
+
kind: "snapshot",
|
|
2274
|
+
produced_by: x.produced_by ?? x.by,
|
|
2275
|
+
produced_at: x.produced_at ?? x.ts,
|
|
2276
|
+
content_hash: x.content_hash ?? contentHash(content),
|
|
2277
|
+
content
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
1959
2280
|
for (const x of w.route_decisions ?? [])
|
|
1960
2281
|
ws.route_decisions.set(x.id, x);
|
|
1961
2282
|
this.workspaces.set(ws.id, ws);
|
|
@@ -1963,14 +2284,29 @@ var Coordinator = class {
|
|
|
1963
2284
|
}
|
|
1964
2285
|
// -- dispatch ------------------------------------------------------
|
|
1965
2286
|
dispatch(envelope) {
|
|
1966
|
-
|
|
2287
|
+
this.frozenNow = this.advanceClock();
|
|
2288
|
+
try {
|
|
2289
|
+
const response = this._dispatch(envelope);
|
|
2290
|
+
if (response.error) this.recordRefusal(envelope, response.error);
|
|
2291
|
+
return response;
|
|
2292
|
+
} finally {
|
|
2293
|
+
this.frozenNow = void 0;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
_dispatch(envelope) {
|
|
2297
|
+
if (!isValidEnvelope(envelope) || typeof envelope.method !== "string" || !envelope.method) {
|
|
1967
2298
|
return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
|
|
1968
2299
|
}
|
|
2300
|
+
if (!withinNesting(envelope, MAX_NESTING)) {
|
|
2301
|
+
return reply(envelope, { error: rpcError(E.REQUEST, `Envelope nests deeper than ${MAX_NESTING} levels`) });
|
|
2302
|
+
}
|
|
1969
2303
|
const maxBytes = this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES;
|
|
1970
2304
|
let size = 0;
|
|
2305
|
+
let canonicalError;
|
|
1971
2306
|
try {
|
|
1972
2307
|
size = canonicalize(envelope).length;
|
|
1973
|
-
} catch {
|
|
2308
|
+
} catch (e) {
|
|
2309
|
+
canonicalError = e;
|
|
1974
2310
|
}
|
|
1975
2311
|
if (size > maxBytes) {
|
|
1976
2312
|
return reply(envelope, { error: rpcError(
|
|
@@ -1984,6 +2320,28 @@ var Coordinator = class {
|
|
|
1984
2320
|
return reply(envelope, { error: rpcError(E.PARAMS, "Invalid params: expected an object") });
|
|
1985
2321
|
}
|
|
1986
2322
|
const params = rawParams ?? {};
|
|
2323
|
+
const handler = this.handlers.get(method);
|
|
2324
|
+
if (!handler) {
|
|
2325
|
+
return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
|
|
2326
|
+
}
|
|
2327
|
+
if (canonicalError !== void 0) {
|
|
2328
|
+
const msg = canonicalError instanceof Error ? canonicalError.message : String(canonicalError);
|
|
2329
|
+
return reply(envelope, { error: rpcError(E.PARAMS, msg) });
|
|
2330
|
+
}
|
|
2331
|
+
if (isSigned(envelope)) {
|
|
2332
|
+
const wsId2 = params.workspace;
|
|
2333
|
+
const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
|
|
2334
|
+
if (ws) {
|
|
2335
|
+
const prior = this.signedIndex(ws).refused.get(signedDigest(envelope));
|
|
2336
|
+
if (prior) {
|
|
2337
|
+
return reply(envelope, { error: rpcError(
|
|
2338
|
+
prior.code,
|
|
2339
|
+
`Refused at seq ${prior.seq}; a refused request is not evaluated again`,
|
|
2340
|
+
{ refused_at_seq: prior.seq }
|
|
2341
|
+
) });
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
1987
2345
|
if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
|
|
1988
2346
|
const sigErr = this.verifySignature(envelope);
|
|
1989
2347
|
if (sigErr) return reply(envelope, { error: sigErr });
|
|
@@ -2002,7 +2360,11 @@ var Coordinator = class {
|
|
|
2002
2360
|
const stale = this.checkStepUp(params);
|
|
2003
2361
|
if (stale) return reply(envelope, { error: stale });
|
|
2004
2362
|
}
|
|
2005
|
-
|
|
2363
|
+
{
|
|
2364
|
+
const gate = this.profileGate(method, params);
|
|
2365
|
+
if (gate) return reply(envelope, { error: gate });
|
|
2366
|
+
}
|
|
2367
|
+
if (method.startsWith("control.") || method.startsWith("deliberate.") || method.startsWith("handoff.") || method.startsWith("whisper.") || MEMBER_ONLY_METHODS.has(method)) {
|
|
2006
2368
|
const wsId2 = params.workspace;
|
|
2007
2369
|
const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
|
|
2008
2370
|
if (ws) {
|
|
@@ -2037,16 +2399,6 @@ var Coordinator = class {
|
|
|
2037
2399
|
if (task) return reply(envelope, { result: { task_id: existingId, state: task.state } });
|
|
2038
2400
|
}
|
|
2039
2401
|
}
|
|
2040
|
-
const handler = this.handlers.get(method);
|
|
2041
|
-
if (!handler) {
|
|
2042
|
-
return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
|
|
2043
|
-
}
|
|
2044
|
-
try {
|
|
2045
|
-
canonicalize(envelope);
|
|
2046
|
-
} catch (e) {
|
|
2047
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2048
|
-
return reply(envelope, { error: rpcError(E.PARAMS, msg) });
|
|
2049
|
-
}
|
|
2050
2402
|
let out;
|
|
2051
2403
|
try {
|
|
2052
2404
|
out = handler(params);
|
|
@@ -2064,17 +2416,56 @@ var Coordinator = class {
|
|
|
2064
2416
|
}
|
|
2065
2417
|
// -- audit ---------------------------------------------------------
|
|
2066
2418
|
recordAudit(ws, envelope) {
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2419
|
+
this.appendEntry(ws, { envelope: JSON.parse(JSON.stringify(envelope)) });
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* SPECIFICATION 10.1: record a refused call when it is a governed attempt.
|
|
2423
|
+
* The caller must be a member of an existing workspace, the call must be a
|
|
2424
|
+
* JSON-RPC call for a method that is not a read, and the refusal must be one
|
|
2425
|
+
* `refusalIsRecorded` names. The request is kept as it arrived, so a
|
|
2426
|
+
* signature on it still verifies. Every call that reaches a recorded refusal
|
|
2427
|
+
* has passed the canonical check, so it can be hashed into the chain.
|
|
2428
|
+
*/
|
|
2429
|
+
recordRefusal(envelope, error) {
|
|
2430
|
+
if (!isValidEnvelope(envelope) || typeof envelope.method !== "string") return;
|
|
2431
|
+
const method = envelope.method;
|
|
2432
|
+
if (READ_ONLY_METHODS.has(method)) return;
|
|
2433
|
+
if (!refusalIsRecorded(method, error, PRIVILEGED_METHODS)) return;
|
|
2434
|
+
const raw = envelope.params;
|
|
2435
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
2436
|
+
const params = raw;
|
|
2437
|
+
const ws = typeof params.workspace === "string" ? this.workspaces.get(params.workspace) : void 0;
|
|
2438
|
+
if (!ws || typeof params.from !== "string" || !ws.members.has(params.from)) return;
|
|
2439
|
+
if (isSigned(envelope)) {
|
|
2440
|
+
const index = this.signedIndex(ws);
|
|
2441
|
+
const digest = signedDigest(envelope);
|
|
2442
|
+
if (index.refused.has(digest) || index.accepted.has(digest)) return;
|
|
2443
|
+
}
|
|
2444
|
+
this.appendEntry(ws, {
|
|
2445
|
+
request: JSON.parse(JSON.stringify(envelope)),
|
|
2446
|
+
outcome: { status: "refused", code: error.code }
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
/** The signed calls on this workspace's log, built from the log when missing. */
|
|
2450
|
+
signedIndex(ws) {
|
|
2451
|
+
let index = this.signedIndexes.get(ws);
|
|
2452
|
+
if (!index) {
|
|
2453
|
+
index = { refused: /* @__PURE__ */ new Map(), accepted: /* @__PURE__ */ new Set() };
|
|
2454
|
+
for (const entry of ws.audit) indexSigned(index, entry);
|
|
2455
|
+
this.signedIndexes.set(ws, index);
|
|
2456
|
+
}
|
|
2457
|
+
return index;
|
|
2458
|
+
}
|
|
2459
|
+
appendEntry(ws, record) {
|
|
2460
|
+
const entry = { seq: ws.audit.length, arrived: this.now(), ...record };
|
|
2072
2461
|
if (ws.chain_enabled || this.options.enableChain) {
|
|
2073
2462
|
const prev = ws.chain_head ?? ZERO_HASH;
|
|
2074
2463
|
entry.prev_hash = prev;
|
|
2075
|
-
ws.chain_head = linkHash(entry
|
|
2464
|
+
ws.chain_head = linkHash(entryRecord(entry), prev);
|
|
2076
2465
|
}
|
|
2077
2466
|
ws.audit.push(entry);
|
|
2467
|
+
const index = this.signedIndexes.get(ws);
|
|
2468
|
+
if (index) indexSigned(index, entry);
|
|
2078
2469
|
for (const l of this.auditListeners) {
|
|
2079
2470
|
try {
|
|
2080
2471
|
l(ws, entry);
|
|
@@ -2097,10 +2488,14 @@ var Coordinator = class {
|
|
|
2097
2488
|
const params = envelope.params ?? {};
|
|
2098
2489
|
const sender = params.from;
|
|
2099
2490
|
const wsId = params.workspace;
|
|
2100
|
-
const
|
|
2101
|
-
if (!sender || !wsId) {
|
|
2491
|
+
const rawTs = params.ts;
|
|
2492
|
+
if (typeof sender !== "string" || !sender || typeof wsId !== "string" || !wsId) {
|
|
2102
2493
|
return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: missing from/workspace");
|
|
2103
2494
|
}
|
|
2495
|
+
if (rawTs !== void 0 && rawTs !== null && typeof rawTs !== "string") {
|
|
2496
|
+
return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: ts must be a string");
|
|
2497
|
+
}
|
|
2498
|
+
const ts = rawTs ?? this.now();
|
|
2104
2499
|
const ws = this.workspaces.get(wsId);
|
|
2105
2500
|
if (!ws) {
|
|
2106
2501
|
return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: unknown workspace");
|
|
@@ -2145,10 +2540,34 @@ var Coordinator = class {
|
|
|
2145
2540
|
return null;
|
|
2146
2541
|
}
|
|
2147
2542
|
// -- step-up (identity-oidc/1.0) -----------------------------------
|
|
2543
|
+
/**
|
|
2544
|
+
* The SPECIFICATION 15.4 check, per method rather than per namespace.
|
|
2545
|
+
*
|
|
2546
|
+
* Core is always present, so a core method is never gated. The reads in
|
|
2547
|
+
* ALWAYS_AVAILABLE are never gated either: a workspace must be able to ask
|
|
2548
|
+
* what it is and to check its own chain, and `audit.verify_chain` belongs to
|
|
2549
|
+
* `audit-scitt/1.0` while chaining also turns on through an option, so
|
|
2550
|
+
* gating it would let a workspace write a hash-linked chain it is refused
|
|
2551
|
+
* permission to verify.
|
|
2552
|
+
*/
|
|
2553
|
+
profileGate(method, params) {
|
|
2554
|
+
const owner = OWNING_PROFILE[method];
|
|
2555
|
+
if (owner === void 0 || owner === "core/1.0" || ALWAYS_AVAILABLE.has(method)) return void 0;
|
|
2556
|
+
const wsId = params.workspace;
|
|
2557
|
+
const ws = typeof wsId === "string" ? this.workspaces.get(wsId) : void 0;
|
|
2558
|
+
if (!ws) return void 0;
|
|
2559
|
+
const name = owner.split("/")[0];
|
|
2560
|
+
if (ws.profiles.some((p) => p === name || p.startsWith(name + "/"))) return void 0;
|
|
2561
|
+
return rpcError(
|
|
2562
|
+
E.METHOD,
|
|
2563
|
+
`Unknown method: ${method}`,
|
|
2564
|
+
{ profile: owner, advertised: [...ws.profiles] }
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2148
2567
|
checkStepUp(params) {
|
|
2149
2568
|
const wsId = params.workspace;
|
|
2150
2569
|
const sender = params.from;
|
|
2151
|
-
if (!wsId || !sender) return null;
|
|
2570
|
+
if (typeof wsId !== "string" || typeof sender !== "string" || !wsId || !sender) return null;
|
|
2152
2571
|
const ws = this.workspaces.get(wsId);
|
|
2153
2572
|
if (!ws) return null;
|
|
2154
2573
|
const member = ws.members.get(sender);
|
|
@@ -2212,6 +2631,20 @@ var Coordinator = class {
|
|
|
2212
2631
|
if (typeof id !== "string") return { error: rpcError(E.PARAMS, "workspace must be a string id") };
|
|
2213
2632
|
if (this.workspaces.has(id)) return { error: rpcError(E.PARAMS, `workspace already exists: ${id}`) };
|
|
2214
2633
|
const profiles = Array.isArray(p.profiles) ? [...p.profiles] : [...this.options.defaultProfiles ?? ["core/1.0", "review/1.0"]];
|
|
2634
|
+
for (const [profile, enforced, option] of [
|
|
2635
|
+
["security-signed/1.0", !!this.options.requireSignatures, "requireSignatures"],
|
|
2636
|
+
["identity-oidc/1.0", this.options.verifyOidcToken !== void 0, "verifyOidcToken"]
|
|
2637
|
+
]) {
|
|
2638
|
+
const name = profile.split("/")[0];
|
|
2639
|
+
const advertised = profiles.some((x) => x === name || x.startsWith(name + "/"));
|
|
2640
|
+
if (enforced && !advertised) profiles.push(profile);
|
|
2641
|
+
else if (advertised && !enforced) {
|
|
2642
|
+
return { error: rpcError(
|
|
2643
|
+
E.PARAMS,
|
|
2644
|
+
`${profile} is advertised but ${option} is not configured. A workspace that advertises it and does not enforce it describes a guarantee it does not give.`
|
|
2645
|
+
) };
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2215
2648
|
const chainEnabled = profiles.includes("audit-scitt/1.0") || !!this.options.enableChain;
|
|
2216
2649
|
const ws = {
|
|
2217
2650
|
id,
|
|
@@ -2261,7 +2694,10 @@ var Coordinator = class {
|
|
|
2261
2694
|
audit_count: ws.audit.length,
|
|
2262
2695
|
task_count: ws.tasks.size,
|
|
2263
2696
|
override_count: ws.overrides.size,
|
|
2264
|
-
|
|
2697
|
+
// Omitted where the workspace has no chain, which is what the Python
|
|
2698
|
+
// reference now sends too: JSON.stringify drops an undefined value, so
|
|
2699
|
+
// the two answered the same call differently on the wire.
|
|
2700
|
+
...ws.chain_head ? { evidence_head: ws.chain_head } : {},
|
|
2265
2701
|
...ws.routing_policy_uri ? { routing_policy_uri: ws.routing_policy_uri } : {}
|
|
2266
2702
|
} };
|
|
2267
2703
|
}
|
|
@@ -2295,6 +2731,8 @@ var Coordinator = class {
|
|
|
2295
2731
|
}
|
|
2296
2732
|
const uri = p.from;
|
|
2297
2733
|
const now = this.now();
|
|
2734
|
+
const existing = ws.members.get(uri);
|
|
2735
|
+
let tokenVerified = false;
|
|
2298
2736
|
const member = {
|
|
2299
2737
|
uri,
|
|
2300
2738
|
type: p.type,
|
|
@@ -2309,20 +2747,41 @@ var Coordinator = class {
|
|
|
2309
2747
|
if (this.options.verifyOidcToken && typeof p.oidc_token === "string") {
|
|
2310
2748
|
const claims = this.options.verifyOidcToken(p.oidc_token);
|
|
2311
2749
|
if (claims === null) return { error: rpcError(E.OIDC_TOKEN_INVALID, "OIDC token invalid") };
|
|
2312
|
-
|
|
2750
|
+
const boundUri = claims.chap_participant_uri;
|
|
2751
|
+
const sub = typeof claims.sub === "string" && claims.sub !== "" ? claims.sub : void 0;
|
|
2752
|
+
if (boundUri != null && boundUri !== uri) {
|
|
2753
|
+
return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token is bound to another participant") };
|
|
2754
|
+
}
|
|
2755
|
+
if (existing) {
|
|
2756
|
+
if (existing.oidc_sub != null) {
|
|
2757
|
+
if (sub !== existing.oidc_sub) {
|
|
2758
|
+
return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token subject does not match the member") };
|
|
2759
|
+
}
|
|
2760
|
+
} else if (boundUri !== uri) {
|
|
2761
|
+
return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token does not name this member") };
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
member.oidc_sub = sub;
|
|
2765
|
+
tokenVerified = true;
|
|
2313
2766
|
const at = claims.auth_time;
|
|
2314
2767
|
if (typeof at === "number") member.oidc_auth_time = at;
|
|
2315
|
-
member.oidc_acr = claims.acr;
|
|
2768
|
+
member.oidc_acr = typeof claims.acr === "string" ? claims.acr : void 0;
|
|
2316
2769
|
const cnf = claims.cnf;
|
|
2317
2770
|
const cnfJwk = cnf?.jwk;
|
|
2318
2771
|
if (cnfJwk && typeof cnfJwk.kid === "string") {
|
|
2319
2772
|
member.keys.push({ jwk: cnfJwk, kid: cnfJwk.kid, valid_from: now });
|
|
2320
2773
|
}
|
|
2321
2774
|
}
|
|
2322
|
-
if (this.options.verifyVc && typeof p.vc_presentation === "object" && p.vc_presentation !== null) {
|
|
2775
|
+
if (this.options.verifyVc && typeof p.vc_presentation === "object" && p.vc_presentation !== null && !Array.isArray(p.vc_presentation)) {
|
|
2323
2776
|
const subject = this.options.verifyVc(p.vc_presentation);
|
|
2324
2777
|
if (subject === null) return { error: rpcError(E.VC_VP_INVALID, "VC presentation invalid") };
|
|
2325
|
-
|
|
2778
|
+
const holder = [subject.holder, subject.id].find(
|
|
2779
|
+
(h) => typeof h === "string" && h !== ""
|
|
2780
|
+
);
|
|
2781
|
+
if (existing && (existing.vc_holder == null || holder !== existing.vc_holder)) {
|
|
2782
|
+
return { error: rpcError(E.VC_HOLDER_BINDING_INVALID, "Presentation holder does not match the member") };
|
|
2783
|
+
}
|
|
2784
|
+
member.vc_holder = holder;
|
|
2326
2785
|
const vpJwk = subject.cnf_jwk;
|
|
2327
2786
|
if (vpJwk && typeof vpJwk.kid === "string") {
|
|
2328
2787
|
member.keys.push({ jwk: vpJwk, kid: vpJwk.kid, valid_from: now });
|
|
@@ -2337,11 +2796,11 @@ var Coordinator = class {
|
|
|
2337
2796
|
}
|
|
2338
2797
|
}
|
|
2339
2798
|
}
|
|
2340
|
-
const existing = ws.members.get(uri);
|
|
2341
2799
|
if (existing) {
|
|
2342
2800
|
if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
|
|
2343
2801
|
if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
|
|
2344
2802
|
if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
|
|
2803
|
+
if (tokenVerified) existing.oidc_acr = member.oidc_acr;
|
|
2345
2804
|
if (member.vc_holder !== void 0) existing.vc_holder = member.vc_holder;
|
|
2346
2805
|
for (const k of attested) {
|
|
2347
2806
|
if (!existing.keys.some((x) => x.kid === k.kid)) existing.keys.push(k);
|
|
@@ -2365,12 +2824,15 @@ var Coordinator = class {
|
|
|
2365
2824
|
const notMember = this.requireMember(ws, p.from);
|
|
2366
2825
|
if (notMember) return notMember;
|
|
2367
2826
|
const assignee = p.assignee || p.to;
|
|
2368
|
-
if (
|
|
2827
|
+
if (typeof assignee !== "string" || !ws.members.has(assignee)) {
|
|
2369
2828
|
return { error: rpcError(E.PARAMS, "Assignee not in workspace") };
|
|
2370
2829
|
}
|
|
2371
2830
|
if (ws.members.get(assignee).paused) {
|
|
2372
2831
|
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
|
|
2373
2832
|
}
|
|
2833
|
+
if (p.mode !== void 0 && p.mode !== null && typeof p.mode !== "string") {
|
|
2834
|
+
return { error: rpcError(E.PARAMS, "mode must be a string") };
|
|
2835
|
+
}
|
|
2374
2836
|
const requestedMode = p.mode || ws.mode;
|
|
2375
2837
|
if (!modeLE(requestedMode, ws.mode_ceiling)) {
|
|
2376
2838
|
return { error: rpcError(
|
|
@@ -2378,6 +2840,9 @@ var Coordinator = class {
|
|
|
2378
2840
|
`Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
|
|
2379
2841
|
) };
|
|
2380
2842
|
}
|
|
2843
|
+
if (p.review_required !== void 0 && p.review_required !== null && typeof p.review_required !== "boolean") {
|
|
2844
|
+
return { error: rpcError(E.PARAMS, "review_required must be a boolean") };
|
|
2845
|
+
}
|
|
2381
2846
|
const taskId = this.ids.taskId();
|
|
2382
2847
|
const now = this.now();
|
|
2383
2848
|
const task = {
|
|
@@ -2414,14 +2879,20 @@ var Coordinator = class {
|
|
|
2414
2879
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
2415
2880
|
const newState = p.state;
|
|
2416
2881
|
const legal = {
|
|
2417
|
-
created: ["in_progress", "declined"
|
|
2418
|
-
in_progress: ["in_progress", "completed", "declined"
|
|
2882
|
+
created: ["in_progress", "declined"],
|
|
2883
|
+
in_progress: ["in_progress", "completed", "declined"],
|
|
2419
2884
|
review_requested: ["in_progress"],
|
|
2420
|
-
paused: ["
|
|
2885
|
+
paused: ["cancelled"]
|
|
2421
2886
|
};
|
|
2422
2887
|
if (!legal[task.state]?.includes(newState)) {
|
|
2423
2888
|
return { error: rpcError(E.PARAMS, `Illegal transition ${task.state} -> ${newState}`) };
|
|
2424
2889
|
}
|
|
2890
|
+
if (newState === "completed" && task.review_required) {
|
|
2891
|
+
return { error: rpcError(
|
|
2892
|
+
E.PARAMS,
|
|
2893
|
+
"This task requires review. Submit the output with task.complete, which opens the review; a reviewer decision completes it."
|
|
2894
|
+
) };
|
|
2895
|
+
}
|
|
2425
2896
|
task.state = newState;
|
|
2426
2897
|
task.updated_at = this.now();
|
|
2427
2898
|
task.history.push({
|
|
@@ -2444,18 +2915,18 @@ var Coordinator = class {
|
|
|
2444
2915
|
}
|
|
2445
2916
|
if (task.review_required) {
|
|
2446
2917
|
const now = this.now();
|
|
2447
|
-
task.pending_artefact = p.output;
|
|
2448
2918
|
if (!task.review) {
|
|
2449
2919
|
const producer = p.from;
|
|
2450
|
-
const eligible = [...ws.members.
|
|
2920
|
+
const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
|
|
2451
2921
|
if (eligible.length === 0) {
|
|
2452
2922
|
return { error: rpcError(
|
|
2453
2923
|
E.NOT_AUTHORISED,
|
|
2454
|
-
"Task requires review but has no eligible reviewer (needs a member other than its assignee and completer)"
|
|
2924
|
+
"Task requires review but has no eligible human reviewer (needs a human member other than its assignee and completer)"
|
|
2455
2925
|
) };
|
|
2456
2926
|
}
|
|
2457
2927
|
task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
|
|
2458
2928
|
}
|
|
2929
|
+
task.pending_artefact = p.output;
|
|
2459
2930
|
task.state = "review_requested";
|
|
2460
2931
|
task.updated_at = now;
|
|
2461
2932
|
task.history.push({
|
|
@@ -2482,23 +2953,39 @@ var Coordinator = class {
|
|
|
2482
2953
|
}
|
|
2483
2954
|
const range = p.range ?? {};
|
|
2484
2955
|
const filter = p.filter ?? {};
|
|
2956
|
+
if (filter.outcome != null && filter.outcome !== "accepted" && filter.outcome !== "refused") {
|
|
2957
|
+
return { error: rpcError(E.PARAMS, "filter.outcome must be 'accepted' or 'refused'") };
|
|
2958
|
+
}
|
|
2485
2959
|
const fromSeq = range.from_seq ?? 0;
|
|
2486
2960
|
const toSeq = range.to_seq ?? ws.audit.length;
|
|
2487
2961
|
const out = [];
|
|
2488
2962
|
for (const entry of ws.audit.slice(fromSeq, toSeq)) {
|
|
2489
|
-
const
|
|
2490
|
-
if (filter.
|
|
2963
|
+
const refused = entry.outcome != null;
|
|
2964
|
+
if (filter.outcome === "accepted" && refused) continue;
|
|
2965
|
+
if (filter.outcome === "refused" && !refused) continue;
|
|
2966
|
+
const call2 = entryCall(entry) ?? {};
|
|
2967
|
+
const ep = call2.params ?? {};
|
|
2968
|
+
if (filter.method && call2.method !== filter.method) continue;
|
|
2491
2969
|
if (filter.from && ep.from !== filter.from) continue;
|
|
2492
|
-
if (filter.task_id
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2970
|
+
if (filter.task_id) {
|
|
2971
|
+
let taskId = ep.task_id;
|
|
2972
|
+
if (call2.method === "whisper.answer") {
|
|
2973
|
+
const prompt = ws.whispers.get(ep.whisper_id);
|
|
2974
|
+
taskId = prompt ? prompt.task_id : void 0;
|
|
2975
|
+
}
|
|
2976
|
+
if (taskId !== filter.task_id) continue;
|
|
2977
|
+
}
|
|
2978
|
+
const item = { seq: entry.seq, arrived: entry.arrived };
|
|
2979
|
+
if (refused) {
|
|
2980
|
+
item.request = entry.request;
|
|
2981
|
+
item.outcome = entry.outcome;
|
|
2982
|
+
} else {
|
|
2983
|
+
item.envelope = entry.envelope;
|
|
2984
|
+
}
|
|
2498
2985
|
if (entry.prev_hash) item.prev_hash = entry.prev_hash;
|
|
2499
2986
|
out.push(item);
|
|
2500
2987
|
}
|
|
2501
|
-
return { result: { entries: out, next_seq: toSeq } };
|
|
2988
|
+
return { result: { entries: out, next_seq: Math.min(toSeq, ws.audit.length) } };
|
|
2502
2989
|
}
|
|
2503
2990
|
// ==========================================================
|
|
2504
2991
|
// review/1.0 handlers
|
|
@@ -2512,11 +2999,15 @@ var Coordinator = class {
|
|
|
2512
2999
|
if (notMember) return notMember;
|
|
2513
3000
|
const task = ws.tasks.get(p.task_id);
|
|
2514
3001
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
3002
|
+
if (REVIEW_REQUEST_STOPPED.includes(task.state)) {
|
|
3003
|
+
return { error: rpcError(E.NOT_REVIEWABLE, `Cannot request review for task in state: ${task.state}`) };
|
|
3004
|
+
}
|
|
2515
3005
|
const to = p.to;
|
|
2516
3006
|
const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
|
|
2517
3007
|
if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
|
|
2518
3008
|
const now = this.now();
|
|
2519
|
-
const
|
|
3009
|
+
const declaredRule = p.rule;
|
|
3010
|
+
const rule = declaredRule || "any_one_approves";
|
|
2520
3011
|
if (!reviewRuleSupported(rule)) {
|
|
2521
3012
|
return { error: rpcError(E.PARAMS, `Unsupported review rule ${JSON.stringify(rule)}: review/1.0 supports any_one_approves, all_approve, quorum:<n>`) };
|
|
2522
3013
|
}
|
|
@@ -2527,7 +3018,7 @@ var Coordinator = class {
|
|
|
2527
3018
|
"A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
|
|
2528
3019
|
) };
|
|
2529
3020
|
}
|
|
2530
|
-
if (
|
|
3021
|
+
if (declaredRule !== void 0 && declaredRule !== task.review.rule) {
|
|
2531
3022
|
return { error: rpcError(
|
|
2532
3023
|
E.REVIEW_ALREADY_OPEN,
|
|
2533
3024
|
`Cannot change the decision rule of an open review (currently ${task.review.rule})`
|
|
@@ -2719,16 +3210,34 @@ var Coordinator = class {
|
|
|
2719
3210
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
2720
3211
|
const notMember = this.requireMember(ws, p.from);
|
|
2721
3212
|
if (notMember) return notMember;
|
|
2722
|
-
const
|
|
3213
|
+
const origId = p.original_task_id;
|
|
3214
|
+
const orig = typeof origId === "string" ? ws.tasks.get(origId) : void 0;
|
|
2723
3215
|
if (!orig) return { error: rpcError(E.PARAMS, "Unknown original task") };
|
|
2724
3216
|
if (orig.state === "completed" || orig.state === "cancelled" || orig.state === "superseded") {
|
|
2725
3217
|
return { error: rpcError(E.PARAMS, `Cannot escalate a terminal task: ${orig.state}`) };
|
|
2726
3218
|
}
|
|
3219
|
+
if (p.new_task === null || typeof p.new_task !== "object" || Array.isArray(p.new_task)) {
|
|
3220
|
+
return { error: rpcError(E.PARAMS, "new_task must be an object") };
|
|
3221
|
+
}
|
|
2727
3222
|
const nt = p.new_task;
|
|
2728
3223
|
const assignee = nt.assignee;
|
|
2729
|
-
if (
|
|
3224
|
+
if (typeof assignee !== "string" || !ws.members.has(assignee)) {
|
|
2730
3225
|
return { error: rpcError(E.PARAMS, "Escalation assignee not in workspace") };
|
|
2731
3226
|
}
|
|
3227
|
+
const named = nt.mode;
|
|
3228
|
+
if (named !== void 0 && named !== null && typeof named !== "string") {
|
|
3229
|
+
return { error: rpcError(E.PARAMS, "new_task.mode must be a string") };
|
|
3230
|
+
}
|
|
3231
|
+
if (ws.members.get(assignee).paused) {
|
|
3232
|
+
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
|
|
3233
|
+
}
|
|
3234
|
+
const mode = named || orig.mode;
|
|
3235
|
+
if (!modeLE(mode, ws.mode_ceiling)) {
|
|
3236
|
+
return { error: rpcError(
|
|
3237
|
+
E.MODE_CEILING_EXCEEDED,
|
|
3238
|
+
`Requested mode ${mode} exceeds ceiling ${ws.mode_ceiling}`
|
|
3239
|
+
) };
|
|
3240
|
+
}
|
|
2732
3241
|
const now = this.now();
|
|
2733
3242
|
const newId = this.ids.taskId();
|
|
2734
3243
|
const newTask = {
|
|
@@ -2740,11 +3249,14 @@ var Coordinator = class {
|
|
|
2740
3249
|
input: nt.input ?? {},
|
|
2741
3250
|
created_at: now,
|
|
2742
3251
|
updated_at: now,
|
|
2743
|
-
mode
|
|
3252
|
+
mode,
|
|
2744
3253
|
supersedes: orig.id,
|
|
2745
3254
|
history: [{ ts: now, from: p.from, state: "created", note: `escalated from ${orig.id}` }],
|
|
2746
3255
|
paused: false
|
|
2747
3256
|
};
|
|
3257
|
+
if (orig.review_required || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
|
|
3258
|
+
newTask.review_required = true;
|
|
3259
|
+
}
|
|
2748
3260
|
ws.tasks.set(newId, newTask);
|
|
2749
3261
|
orig.state = "escalated";
|
|
2750
3262
|
orig.updated_at = now;
|
|
@@ -2885,11 +3397,13 @@ function wrapMcpToolCall(coord, workspace, options) {
|
|
|
2885
3397
|
method: "task.update",
|
|
2886
3398
|
params: { workspace, from: options.caller, task_id: taskId, state: "in_progress" }
|
|
2887
3399
|
});
|
|
3400
|
+
const output = { result: options.result, citations };
|
|
3401
|
+
if (options.fulfils) output.fulfils = options.fulfils;
|
|
2888
3402
|
const completeParams = {
|
|
2889
3403
|
workspace,
|
|
2890
3404
|
from: options.caller,
|
|
2891
3405
|
task_id: taskId,
|
|
2892
|
-
output
|
|
3406
|
+
output
|
|
2893
3407
|
};
|
|
2894
3408
|
if (options.confidence !== void 0) completeParams.confidence = options.confidence;
|
|
2895
3409
|
const completeResp = coord.dispatch({
|
|
@@ -2941,11 +3455,13 @@ function wrapA2aMessageExchange(coord, workspace, options) {
|
|
|
2941
3455
|
state: "in_progress"
|
|
2942
3456
|
}
|
|
2943
3457
|
});
|
|
3458
|
+
const output = { received: options.received, citations: [citation] };
|
|
3459
|
+
if (options.fulfils) output.fulfils = options.fulfils;
|
|
2944
3460
|
const completeParams = {
|
|
2945
3461
|
workspace,
|
|
2946
3462
|
from: options.bridgeUri,
|
|
2947
3463
|
task_id: taskId,
|
|
2948
|
-
output
|
|
3464
|
+
output
|
|
2949
3465
|
};
|
|
2950
3466
|
if (options.confidence !== void 0) completeParams.confidence = options.confidence;
|
|
2951
3467
|
const completeResp = coord.dispatch({
|
|
@@ -2973,8 +3489,12 @@ export {
|
|
|
2973
3489
|
deriveKeypair,
|
|
2974
3490
|
deriveSeed,
|
|
2975
3491
|
diffJsonPatch,
|
|
3492
|
+
entryCall,
|
|
3493
|
+
entryRecord,
|
|
3494
|
+
isRefusal,
|
|
2976
3495
|
isValidEnvelope,
|
|
2977
3496
|
jwkFromPrivateKey,
|
|
3497
|
+
linkHash,
|
|
2978
3498
|
makeApi,
|
|
2979
3499
|
makeDefaultPolicy,
|
|
2980
3500
|
modeLE,
|