@brightbeamai/chap-coordinator 0.2.13 → 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 +6 -3
- 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 +652 -139
- 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 +648 -139
- 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,6 +1982,18 @@ 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
|
+
}
|
|
1750
1997
|
var REVIEW_REQUEST_STOPPED = ["cancelled", "superseded", "paused"];
|
|
1751
1998
|
var Coordinator = class {
|
|
1752
1999
|
workspaces = /* @__PURE__ */ new Map();
|
|
@@ -1754,8 +2001,16 @@ var Coordinator = class {
|
|
|
1754
2001
|
ids;
|
|
1755
2002
|
store;
|
|
1756
2003
|
wsVersions = /* @__PURE__ */ new Map();
|
|
2004
|
+
saveChains = /* @__PURE__ */ new Map();
|
|
1757
2005
|
clockMs;
|
|
2006
|
+
frozenNow;
|
|
1758
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();
|
|
1759
2014
|
/** Method handler registry; profiles plug into this. */
|
|
1760
2015
|
handlers = /* @__PURE__ */ new Map();
|
|
1761
2016
|
/** Custom lapse-check function (whisper/1.0). Set by the whisper profile. */
|
|
@@ -1797,42 +2052,72 @@ var Coordinator = class {
|
|
|
1797
2052
|
this.applyRecords(records);
|
|
1798
2053
|
}
|
|
1799
2054
|
applyRecords(records) {
|
|
1800
|
-
this.workspaces.clear();
|
|
1801
2055
|
this.wsVersions.clear();
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
this.wsVersions.set(r.id, r.version);
|
|
1805
|
-
}
|
|
2056
|
+
this.restore(records.map((r) => r.data));
|
|
2057
|
+
for (const r of records) this.wsVersions.set(r.id, r.version);
|
|
1806
2058
|
}
|
|
1807
2059
|
/** Persist the current snapshot of one workspace to the store. */
|
|
1808
2060
|
persist(ws) {
|
|
1809
2061
|
const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
|
|
1810
2062
|
this.wsVersions.set(ws.id, next);
|
|
2063
|
+
let record;
|
|
1811
2064
|
try {
|
|
1812
2065
|
const all = this.snapshot();
|
|
1813
2066
|
const data = all.find((w) => w.id === ws.id);
|
|
1814
2067
|
if (!data) return;
|
|
1815
|
-
|
|
1816
|
-
id: ws.id,
|
|
1817
|
-
data,
|
|
1818
|
-
version: next,
|
|
1819
|
-
updated_at: this.now()
|
|
1820
|
-
});
|
|
1821
|
-
if (result instanceof Promise) {
|
|
1822
|
-
result.catch(() => {
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
2068
|
+
record = { id: ws.id, data, version: next, updated_at: this.now() };
|
|
1825
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()]);
|
|
1826
2108
|
}
|
|
1827
2109
|
}
|
|
1828
2110
|
// -- lifecycle -----------------------------------------------------
|
|
1829
|
-
|
|
2111
|
+
advanceClock() {
|
|
1830
2112
|
if (this.clockMs !== void 0) {
|
|
1831
2113
|
this.clockMs += 1e3;
|
|
1832
2114
|
return nowIso(this.clockMs);
|
|
1833
2115
|
}
|
|
1834
2116
|
return nowIso();
|
|
1835
2117
|
}
|
|
2118
|
+
now() {
|
|
2119
|
+
return this.frozenNow ?? this.advanceClock();
|
|
2120
|
+
}
|
|
1836
2121
|
onAudit(listener) {
|
|
1837
2122
|
this.auditListeners.push(listener);
|
|
1838
2123
|
return () => {
|
|
@@ -1857,8 +2142,11 @@ var Coordinator = class {
|
|
|
1857
2142
|
* error-table divergence is tracked separately.)
|
|
1858
2143
|
*/
|
|
1859
2144
|
requireMember(ws, sender) {
|
|
1860
|
-
if (typeof sender !== "string"
|
|
1861
|
-
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}`) };
|
|
1862
2150
|
}
|
|
1863
2151
|
return null;
|
|
1864
2152
|
}
|
|
@@ -1955,8 +2243,40 @@ var Coordinator = class {
|
|
|
1955
2243
|
ws.deliberations.set(x.id, x);
|
|
1956
2244
|
for (const x of w.handoffs ?? [])
|
|
1957
2245
|
ws.handoffs.set(x.id, x);
|
|
1958
|
-
for (const x of w.snapshots ?? [])
|
|
1959
|
-
|
|
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
|
+
}
|
|
1960
2280
|
for (const x of w.route_decisions ?? [])
|
|
1961
2281
|
ws.route_decisions.set(x.id, x);
|
|
1962
2282
|
this.workspaces.set(ws.id, ws);
|
|
@@ -1964,14 +2284,29 @@ var Coordinator = class {
|
|
|
1964
2284
|
}
|
|
1965
2285
|
// -- dispatch ------------------------------------------------------
|
|
1966
2286
|
dispatch(envelope) {
|
|
1967
|
-
|
|
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) {
|
|
1968
2298
|
return reply(envelope, { error: rpcError(E.REQUEST, "Invalid JSON-RPC 2.0 request") });
|
|
1969
2299
|
}
|
|
2300
|
+
if (!withinNesting(envelope, MAX_NESTING)) {
|
|
2301
|
+
return reply(envelope, { error: rpcError(E.REQUEST, `Envelope nests deeper than ${MAX_NESTING} levels`) });
|
|
2302
|
+
}
|
|
1970
2303
|
const maxBytes = this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES;
|
|
1971
2304
|
let size = 0;
|
|
2305
|
+
let canonicalError;
|
|
1972
2306
|
try {
|
|
1973
2307
|
size = canonicalize(envelope).length;
|
|
1974
|
-
} catch {
|
|
2308
|
+
} catch (e) {
|
|
2309
|
+
canonicalError = e;
|
|
1975
2310
|
}
|
|
1976
2311
|
if (size > maxBytes) {
|
|
1977
2312
|
return reply(envelope, { error: rpcError(
|
|
@@ -1985,6 +2320,28 @@ var Coordinator = class {
|
|
|
1985
2320
|
return reply(envelope, { error: rpcError(E.PARAMS, "Invalid params: expected an object") });
|
|
1986
2321
|
}
|
|
1987
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
|
+
}
|
|
1988
2345
|
if (this.options.requireSignatures && method !== "participant.join" && method !== "workspace.create") {
|
|
1989
2346
|
const sigErr = this.verifySignature(envelope);
|
|
1990
2347
|
if (sigErr) return reply(envelope, { error: sigErr });
|
|
@@ -2003,7 +2360,11 @@ var Coordinator = class {
|
|
|
2003
2360
|
const stale = this.checkStepUp(params);
|
|
2004
2361
|
if (stale) return reply(envelope, { error: stale });
|
|
2005
2362
|
}
|
|
2006
|
-
|
|
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)) {
|
|
2007
2368
|
const wsId2 = params.workspace;
|
|
2008
2369
|
const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
|
|
2009
2370
|
if (ws) {
|
|
@@ -2038,16 +2399,6 @@ var Coordinator = class {
|
|
|
2038
2399
|
if (task) return reply(envelope, { result: { task_id: existingId, state: task.state } });
|
|
2039
2400
|
}
|
|
2040
2401
|
}
|
|
2041
|
-
const handler = this.handlers.get(method);
|
|
2042
|
-
if (!handler) {
|
|
2043
|
-
return reply(envelope, { error: rpcError(E.METHOD, `Unknown method: ${method}`) });
|
|
2044
|
-
}
|
|
2045
|
-
try {
|
|
2046
|
-
canonicalize(envelope);
|
|
2047
|
-
} catch (e) {
|
|
2048
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2049
|
-
return reply(envelope, { error: rpcError(E.PARAMS, msg) });
|
|
2050
|
-
}
|
|
2051
2402
|
let out;
|
|
2052
2403
|
try {
|
|
2053
2404
|
out = handler(params);
|
|
@@ -2065,17 +2416,56 @@ var Coordinator = class {
|
|
|
2065
2416
|
}
|
|
2066
2417
|
// -- audit ---------------------------------------------------------
|
|
2067
2418
|
recordAudit(ws, envelope) {
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
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 };
|
|
2073
2461
|
if (ws.chain_enabled || this.options.enableChain) {
|
|
2074
2462
|
const prev = ws.chain_head ?? ZERO_HASH;
|
|
2075
2463
|
entry.prev_hash = prev;
|
|
2076
|
-
ws.chain_head = linkHash(entry
|
|
2464
|
+
ws.chain_head = linkHash(entryRecord(entry), prev);
|
|
2077
2465
|
}
|
|
2078
2466
|
ws.audit.push(entry);
|
|
2467
|
+
const index = this.signedIndexes.get(ws);
|
|
2468
|
+
if (index) indexSigned(index, entry);
|
|
2079
2469
|
for (const l of this.auditListeners) {
|
|
2080
2470
|
try {
|
|
2081
2471
|
l(ws, entry);
|
|
@@ -2098,10 +2488,14 @@ var Coordinator = class {
|
|
|
2098
2488
|
const params = envelope.params ?? {};
|
|
2099
2489
|
const sender = params.from;
|
|
2100
2490
|
const wsId = params.workspace;
|
|
2101
|
-
const
|
|
2102
|
-
if (!sender || !wsId) {
|
|
2491
|
+
const rawTs = params.ts;
|
|
2492
|
+
if (typeof sender !== "string" || !sender || typeof wsId !== "string" || !wsId) {
|
|
2103
2493
|
return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: missing from/workspace");
|
|
2104
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();
|
|
2105
2499
|
const ws = this.workspaces.get(wsId);
|
|
2106
2500
|
if (!ws) {
|
|
2107
2501
|
return rpcError(E.SIG_VERIFY_FAILED, "Cannot verify signature: unknown workspace");
|
|
@@ -2146,10 +2540,34 @@ var Coordinator = class {
|
|
|
2146
2540
|
return null;
|
|
2147
2541
|
}
|
|
2148
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
|
+
}
|
|
2149
2567
|
checkStepUp(params) {
|
|
2150
2568
|
const wsId = params.workspace;
|
|
2151
2569
|
const sender = params.from;
|
|
2152
|
-
if (!wsId || !sender) return null;
|
|
2570
|
+
if (typeof wsId !== "string" || typeof sender !== "string" || !wsId || !sender) return null;
|
|
2153
2571
|
const ws = this.workspaces.get(wsId);
|
|
2154
2572
|
if (!ws) return null;
|
|
2155
2573
|
const member = ws.members.get(sender);
|
|
@@ -2213,6 +2631,20 @@ var Coordinator = class {
|
|
|
2213
2631
|
if (typeof id !== "string") return { error: rpcError(E.PARAMS, "workspace must be a string id") };
|
|
2214
2632
|
if (this.workspaces.has(id)) return { error: rpcError(E.PARAMS, `workspace already exists: ${id}`) };
|
|
2215
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
|
+
}
|
|
2216
2648
|
const chainEnabled = profiles.includes("audit-scitt/1.0") || !!this.options.enableChain;
|
|
2217
2649
|
const ws = {
|
|
2218
2650
|
id,
|
|
@@ -2262,7 +2694,10 @@ var Coordinator = class {
|
|
|
2262
2694
|
audit_count: ws.audit.length,
|
|
2263
2695
|
task_count: ws.tasks.size,
|
|
2264
2696
|
override_count: ws.overrides.size,
|
|
2265
|
-
|
|
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 } : {},
|
|
2266
2701
|
...ws.routing_policy_uri ? { routing_policy_uri: ws.routing_policy_uri } : {}
|
|
2267
2702
|
} };
|
|
2268
2703
|
}
|
|
@@ -2296,6 +2731,8 @@ var Coordinator = class {
|
|
|
2296
2731
|
}
|
|
2297
2732
|
const uri = p.from;
|
|
2298
2733
|
const now = this.now();
|
|
2734
|
+
const existing = ws.members.get(uri);
|
|
2735
|
+
let tokenVerified = false;
|
|
2299
2736
|
const member = {
|
|
2300
2737
|
uri,
|
|
2301
2738
|
type: p.type,
|
|
@@ -2310,20 +2747,41 @@ var Coordinator = class {
|
|
|
2310
2747
|
if (this.options.verifyOidcToken && typeof p.oidc_token === "string") {
|
|
2311
2748
|
const claims = this.options.verifyOidcToken(p.oidc_token);
|
|
2312
2749
|
if (claims === null) return { error: rpcError(E.OIDC_TOKEN_INVALID, "OIDC token invalid") };
|
|
2313
|
-
|
|
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;
|
|
2314
2766
|
const at = claims.auth_time;
|
|
2315
2767
|
if (typeof at === "number") member.oidc_auth_time = at;
|
|
2316
|
-
member.oidc_acr = claims.acr;
|
|
2768
|
+
member.oidc_acr = typeof claims.acr === "string" ? claims.acr : void 0;
|
|
2317
2769
|
const cnf = claims.cnf;
|
|
2318
2770
|
const cnfJwk = cnf?.jwk;
|
|
2319
2771
|
if (cnfJwk && typeof cnfJwk.kid === "string") {
|
|
2320
2772
|
member.keys.push({ jwk: cnfJwk, kid: cnfJwk.kid, valid_from: now });
|
|
2321
2773
|
}
|
|
2322
2774
|
}
|
|
2323
|
-
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)) {
|
|
2324
2776
|
const subject = this.options.verifyVc(p.vc_presentation);
|
|
2325
2777
|
if (subject === null) return { error: rpcError(E.VC_VP_INVALID, "VC presentation invalid") };
|
|
2326
|
-
|
|
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;
|
|
2327
2785
|
const vpJwk = subject.cnf_jwk;
|
|
2328
2786
|
if (vpJwk && typeof vpJwk.kid === "string") {
|
|
2329
2787
|
member.keys.push({ jwk: vpJwk, kid: vpJwk.kid, valid_from: now });
|
|
@@ -2338,11 +2796,11 @@ var Coordinator = class {
|
|
|
2338
2796
|
}
|
|
2339
2797
|
}
|
|
2340
2798
|
}
|
|
2341
|
-
const existing = ws.members.get(uri);
|
|
2342
2799
|
if (existing) {
|
|
2343
2800
|
if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
|
|
2344
2801
|
if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
|
|
2345
2802
|
if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
|
|
2803
|
+
if (tokenVerified) existing.oidc_acr = member.oidc_acr;
|
|
2346
2804
|
if (member.vc_holder !== void 0) existing.vc_holder = member.vc_holder;
|
|
2347
2805
|
for (const k of attested) {
|
|
2348
2806
|
if (!existing.keys.some((x) => x.kid === k.kid)) existing.keys.push(k);
|
|
@@ -2366,12 +2824,15 @@ var Coordinator = class {
|
|
|
2366
2824
|
const notMember = this.requireMember(ws, p.from);
|
|
2367
2825
|
if (notMember) return notMember;
|
|
2368
2826
|
const assignee = p.assignee || p.to;
|
|
2369
|
-
if (
|
|
2827
|
+
if (typeof assignee !== "string" || !ws.members.has(assignee)) {
|
|
2370
2828
|
return { error: rpcError(E.PARAMS, "Assignee not in workspace") };
|
|
2371
2829
|
}
|
|
2372
2830
|
if (ws.members.get(assignee).paused) {
|
|
2373
2831
|
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
|
|
2374
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
|
+
}
|
|
2375
2836
|
const requestedMode = p.mode || ws.mode;
|
|
2376
2837
|
if (!modeLE(requestedMode, ws.mode_ceiling)) {
|
|
2377
2838
|
return { error: rpcError(
|
|
@@ -2379,6 +2840,9 @@ var Coordinator = class {
|
|
|
2379
2840
|
`Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
|
|
2380
2841
|
) };
|
|
2381
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
|
+
}
|
|
2382
2846
|
const taskId = this.ids.taskId();
|
|
2383
2847
|
const now = this.now();
|
|
2384
2848
|
const task = {
|
|
@@ -2415,10 +2879,10 @@ var Coordinator = class {
|
|
|
2415
2879
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
2416
2880
|
const newState = p.state;
|
|
2417
2881
|
const legal = {
|
|
2418
|
-
created: ["in_progress", "declined"
|
|
2419
|
-
in_progress: ["in_progress", "completed", "declined"
|
|
2882
|
+
created: ["in_progress", "declined"],
|
|
2883
|
+
in_progress: ["in_progress", "completed", "declined"],
|
|
2420
2884
|
review_requested: ["in_progress"],
|
|
2421
|
-
paused: ["
|
|
2885
|
+
paused: ["cancelled"]
|
|
2422
2886
|
};
|
|
2423
2887
|
if (!legal[task.state]?.includes(newState)) {
|
|
2424
2888
|
return { error: rpcError(E.PARAMS, `Illegal transition ${task.state} -> ${newState}`) };
|
|
@@ -2451,7 +2915,6 @@ var Coordinator = class {
|
|
|
2451
2915
|
}
|
|
2452
2916
|
if (task.review_required) {
|
|
2453
2917
|
const now = this.now();
|
|
2454
|
-
task.pending_artefact = p.output;
|
|
2455
2918
|
if (!task.review) {
|
|
2456
2919
|
const producer = p.from;
|
|
2457
2920
|
const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
|
|
@@ -2463,6 +2926,7 @@ var Coordinator = class {
|
|
|
2463
2926
|
}
|
|
2464
2927
|
task.review = { requested_at: now, requested_to: eligible, rule: "any_one_approves", decisions: [] };
|
|
2465
2928
|
}
|
|
2929
|
+
task.pending_artefact = p.output;
|
|
2466
2930
|
task.state = "review_requested";
|
|
2467
2931
|
task.updated_at = now;
|
|
2468
2932
|
task.history.push({
|
|
@@ -2489,23 +2953,39 @@ var Coordinator = class {
|
|
|
2489
2953
|
}
|
|
2490
2954
|
const range = p.range ?? {};
|
|
2491
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
|
+
}
|
|
2492
2959
|
const fromSeq = range.from_seq ?? 0;
|
|
2493
2960
|
const toSeq = range.to_seq ?? ws.audit.length;
|
|
2494
2961
|
const out = [];
|
|
2495
2962
|
for (const entry of ws.audit.slice(fromSeq, toSeq)) {
|
|
2496
|
-
const
|
|
2497
|
-
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;
|
|
2498
2969
|
if (filter.from && ep.from !== filter.from) continue;
|
|
2499
|
-
if (filter.task_id
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
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
|
+
}
|
|
2505
2985
|
if (entry.prev_hash) item.prev_hash = entry.prev_hash;
|
|
2506
2986
|
out.push(item);
|
|
2507
2987
|
}
|
|
2508
|
-
return { result: { entries: out, next_seq: toSeq } };
|
|
2988
|
+
return { result: { entries: out, next_seq: Math.min(toSeq, ws.audit.length) } };
|
|
2509
2989
|
}
|
|
2510
2990
|
// ==========================================================
|
|
2511
2991
|
// review/1.0 handlers
|
|
@@ -2730,16 +3210,34 @@ var Coordinator = class {
|
|
|
2730
3210
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
2731
3211
|
const notMember = this.requireMember(ws, p.from);
|
|
2732
3212
|
if (notMember) return notMember;
|
|
2733
|
-
const
|
|
3213
|
+
const origId = p.original_task_id;
|
|
3214
|
+
const orig = typeof origId === "string" ? ws.tasks.get(origId) : void 0;
|
|
2734
3215
|
if (!orig) return { error: rpcError(E.PARAMS, "Unknown original task") };
|
|
2735
3216
|
if (orig.state === "completed" || orig.state === "cancelled" || orig.state === "superseded") {
|
|
2736
3217
|
return { error: rpcError(E.PARAMS, `Cannot escalate a terminal task: ${orig.state}`) };
|
|
2737
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
|
+
}
|
|
2738
3222
|
const nt = p.new_task;
|
|
2739
3223
|
const assignee = nt.assignee;
|
|
2740
|
-
if (
|
|
3224
|
+
if (typeof assignee !== "string" || !ws.members.has(assignee)) {
|
|
2741
3225
|
return { error: rpcError(E.PARAMS, "Escalation assignee not in workspace") };
|
|
2742
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
|
+
}
|
|
2743
3241
|
const now = this.now();
|
|
2744
3242
|
const newId = this.ids.taskId();
|
|
2745
3243
|
const newTask = {
|
|
@@ -2751,11 +3249,14 @@ var Coordinator = class {
|
|
|
2751
3249
|
input: nt.input ?? {},
|
|
2752
3250
|
created_at: now,
|
|
2753
3251
|
updated_at: now,
|
|
2754
|
-
mode
|
|
3252
|
+
mode,
|
|
2755
3253
|
supersedes: orig.id,
|
|
2756
3254
|
history: [{ ts: now, from: p.from, state: "created", note: `escalated from ${orig.id}` }],
|
|
2757
3255
|
paused: false
|
|
2758
3256
|
};
|
|
3257
|
+
if (orig.review_required || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
|
|
3258
|
+
newTask.review_required = true;
|
|
3259
|
+
}
|
|
2759
3260
|
ws.tasks.set(newId, newTask);
|
|
2760
3261
|
orig.state = "escalated";
|
|
2761
3262
|
orig.updated_at = now;
|
|
@@ -2896,11 +3397,13 @@ function wrapMcpToolCall(coord, workspace, options) {
|
|
|
2896
3397
|
method: "task.update",
|
|
2897
3398
|
params: { workspace, from: options.caller, task_id: taskId, state: "in_progress" }
|
|
2898
3399
|
});
|
|
3400
|
+
const output = { result: options.result, citations };
|
|
3401
|
+
if (options.fulfils) output.fulfils = options.fulfils;
|
|
2899
3402
|
const completeParams = {
|
|
2900
3403
|
workspace,
|
|
2901
3404
|
from: options.caller,
|
|
2902
3405
|
task_id: taskId,
|
|
2903
|
-
output
|
|
3406
|
+
output
|
|
2904
3407
|
};
|
|
2905
3408
|
if (options.confidence !== void 0) completeParams.confidence = options.confidence;
|
|
2906
3409
|
const completeResp = coord.dispatch({
|
|
@@ -2952,11 +3455,13 @@ function wrapA2aMessageExchange(coord, workspace, options) {
|
|
|
2952
3455
|
state: "in_progress"
|
|
2953
3456
|
}
|
|
2954
3457
|
});
|
|
3458
|
+
const output = { received: options.received, citations: [citation] };
|
|
3459
|
+
if (options.fulfils) output.fulfils = options.fulfils;
|
|
2955
3460
|
const completeParams = {
|
|
2956
3461
|
workspace,
|
|
2957
3462
|
from: options.bridgeUri,
|
|
2958
3463
|
task_id: taskId,
|
|
2959
|
-
output
|
|
3464
|
+
output
|
|
2960
3465
|
};
|
|
2961
3466
|
if (options.confidence !== void 0) completeParams.confidence = options.confidence;
|
|
2962
3467
|
const completeResp = coord.dispatch({
|
|
@@ -2984,8 +3489,12 @@ export {
|
|
|
2984
3489
|
deriveKeypair,
|
|
2985
3490
|
deriveSeed,
|
|
2986
3491
|
diffJsonPatch,
|
|
3492
|
+
entryCall,
|
|
3493
|
+
entryRecord,
|
|
3494
|
+
isRefusal,
|
|
2987
3495
|
isValidEnvelope,
|
|
2988
3496
|
jwkFromPrivateKey,
|
|
3497
|
+
linkHash,
|
|
2989
3498
|
makeApi,
|
|
2990
3499
|
makeDefaultPolicy,
|
|
2991
3500
|
modeLE,
|