@brightbeamai/chap-coordinator 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -8
- package/dist/{api-59aU01Bs.d.cts → api-CjZQnj63.d.cts} +128 -14
- package/dist/{api-CxciGFHA.d.ts → api-DG0VFGVr.d.ts} +128 -14
- package/dist/api.cjs +3 -1
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +3 -1
- package/dist/api.js.map +1 -1
- package/dist/index.cjs +1578 -276
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -6
- package/dist/index.d.ts +20 -6
- package/dist/index.js +1577 -276
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc.cjs +1 -0
- package/dist/jsonrpc.cjs.map +1 -1
- package/dist/jsonrpc.d.cts +1 -0
- package/dist/jsonrpc.d.ts +1 -0
- package/dist/jsonrpc.js +1 -0
- package/dist/jsonrpc.js.map +1 -1
- package/dist/methods.cjs.map +1 -1
- package/dist/methods.d.cts +36 -3
- package/dist/methods.d.ts +36 -3
- package/dist/patch.cjs +195 -61
- package/dist/patch.cjs.map +1 -1
- package/dist/patch.d.cts +16 -2
- package/dist/patch.d.ts +16 -2
- package/dist/patch.js +194 -61
- package/dist/patch.js.map +1 -1
- package/dist/storage/sqlite.cjs +81 -19
- package/dist/storage/sqlite.cjs.map +1 -1
- package/dist/storage/sqlite.d.cts +19 -9
- package/dist/storage/sqlite.d.ts +19 -9
- package/dist/storage/sqlite.js +81 -19
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.cjs +20 -15
- package/dist/storage/store.cjs.map +1 -1
- package/dist/storage/store.d.cts +24 -6
- package/dist/storage/store.d.ts +24 -6
- package/dist/storage/store.js +20 -15
- package/dist/storage/store.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +56 -1
- package/dist/types.d.ts +56 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var src_exports = {};
|
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
Coordinator: () => Coordinator,
|
|
24
24
|
CoordinatorError: () => CoordinatorError,
|
|
25
|
+
DEFAULT_MAX_PATCHED_BYTES: () => DEFAULT_MAX_PATCHED_BYTES,
|
|
25
26
|
E: () => E,
|
|
26
27
|
IdFactory: () => IdFactory,
|
|
27
28
|
MemoryStore: () => MemoryStore,
|
|
@@ -61,7 +62,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
61
62
|
var import_node_crypto = require("crypto");
|
|
62
63
|
var ZERO_HASH = "sha256:" + "0".repeat(64);
|
|
63
64
|
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.';
|
|
64
|
-
var NUMBER_RANGE_ERROR = "CHAP canonical integers must be within the safe-integer range (abs value <=
|
|
65
|
+
var NUMBER_RANGE_ERROR = "CHAP canonical integers must be within the safe-integer range (abs value <= 2^53 - 1); represent larger numbers as strings.";
|
|
65
66
|
var LONE_SURROGATE_ERROR = "CHAP canonical strings must be valid Unicode (I-JSON / RFC 7493); unpaired UTF-16 surrogates are not permitted.";
|
|
66
67
|
function assertNoLoneSurrogate(s) {
|
|
67
68
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -82,7 +83,7 @@ function canonicalString(s) {
|
|
|
82
83
|
function canonicalNumber(n) {
|
|
83
84
|
if (!Number.isFinite(n)) {
|
|
84
85
|
throw new Error(
|
|
85
|
-
"Non-finite numbers are not permitted in a CHAP canonical value"
|
|
86
|
+
"Non-finite numbers are not permitted in a CHAP canonical value."
|
|
86
87
|
);
|
|
87
88
|
}
|
|
88
89
|
if (!Number.isInteger(n)) {
|
|
@@ -155,6 +156,7 @@ var OWNING_PROFILE = Object.freeze({
|
|
|
155
156
|
"participant.leave": "core/1.0",
|
|
156
157
|
"participant.revoke_key": "core/1.0",
|
|
157
158
|
"participant.rotate_key": "core/1.0",
|
|
159
|
+
"review.acknowledge": "review/1.0",
|
|
158
160
|
"review.depth": "routing/1.0",
|
|
159
161
|
"review.request": "review/1.0",
|
|
160
162
|
"task.complete": "core/1.0",
|
|
@@ -165,6 +167,7 @@ var OWNING_PROFILE = Object.freeze({
|
|
|
165
167
|
"whisper.ask": "whisper/1.0",
|
|
166
168
|
"workspace.create": "core/1.0",
|
|
167
169
|
"workspace.describe": "core/1.0",
|
|
170
|
+
"workspace.invite": "core/1.0",
|
|
168
171
|
"workspace.set_profiles": "core/1.0"
|
|
169
172
|
});
|
|
170
173
|
var ALWAYS_AVAILABLE = /* @__PURE__ */ new Set([
|
|
@@ -173,6 +176,20 @@ var ALWAYS_AVAILABLE = /* @__PURE__ */ new Set([
|
|
|
173
176
|
"audit.verify_chain",
|
|
174
177
|
"audit.verify_receipt"
|
|
175
178
|
]);
|
|
179
|
+
var PROFILE_VERSIONS = Object.freeze({
|
|
180
|
+
"audit-scitt": "1.0",
|
|
181
|
+
"control": "1.0",
|
|
182
|
+
"core": "1.0",
|
|
183
|
+
"deliberation": "1.0",
|
|
184
|
+
"handoff": "1.0",
|
|
185
|
+
"identity-oidc": "1.0",
|
|
186
|
+
"identity-vc": "1.0",
|
|
187
|
+
"modes": "1.0",
|
|
188
|
+
"review": "1.0",
|
|
189
|
+
"routing": "1.0",
|
|
190
|
+
"security-signed": "1.0",
|
|
191
|
+
"whisper": "1.0"
|
|
192
|
+
});
|
|
176
193
|
|
|
177
194
|
// src/crypto.ts
|
|
178
195
|
var import_node_crypto2 = require("crypto");
|
|
@@ -224,21 +241,132 @@ function signEnvelope(canonical, privateKey, kid) {
|
|
|
224
241
|
const sig = (0, import_node_crypto2.sign)(null, canonical, privateKey);
|
|
225
242
|
return `ed25519:${kid}:${sig.toString("base64")}`;
|
|
226
243
|
}
|
|
244
|
+
function strictBase64(s) {
|
|
245
|
+
const bytes = Buffer.from(s, "base64");
|
|
246
|
+
return bytes.toString("base64") === s ? bytes : void 0;
|
|
247
|
+
}
|
|
227
248
|
function verifyEnvelope(canonical, sigTag, publicKey) {
|
|
228
249
|
if (!sigTag.startsWith("ed25519:")) return false;
|
|
229
250
|
const parts = sigTag.split(":");
|
|
230
251
|
if (parts.length < 2) return false;
|
|
231
|
-
const
|
|
252
|
+
const sig = strictBase64(parts[parts.length - 1]);
|
|
253
|
+
if (sig === void 0) return false;
|
|
232
254
|
try {
|
|
233
|
-
const sig = Buffer.from(sigB64, "base64");
|
|
234
255
|
return (0, import_node_crypto2.verify)(null, canonical, publicKey, sig);
|
|
235
256
|
} catch {
|
|
236
257
|
return false;
|
|
237
258
|
}
|
|
238
259
|
}
|
|
260
|
+
function isEd25519Key(jwk) {
|
|
261
|
+
if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) return false;
|
|
262
|
+
const key = jwk;
|
|
263
|
+
return key.kty === "OKP" && key.crv === "Ed25519" && typeof key.x === "string" && COORDINATE.test(key.x);
|
|
264
|
+
}
|
|
239
265
|
function publicKeyFromJwk(jwk) {
|
|
240
266
|
return publicKeyFromRaw(b64urlDecode(jwk.x));
|
|
241
267
|
}
|
|
268
|
+
function jwkThumbprint(jwk) {
|
|
269
|
+
if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) return void 0;
|
|
270
|
+
const key = jwk;
|
|
271
|
+
const names = key.kty === "OKP" ? ["crv", "kty", "x"] : key.kty === "EC" ? ["crv", "kty", "x", "y"] : void 0;
|
|
272
|
+
if (!names || names.some((n) => typeof key[n] !== "string")) return void 0;
|
|
273
|
+
const members = Object.fromEntries(names.map((n) => [n, key[n]]));
|
|
274
|
+
return b64urlNoPad((0, import_node_crypto2.createHash)("sha256").update(canonicalize(members)).digest());
|
|
275
|
+
}
|
|
276
|
+
var B64URL = /^[A-Za-z0-9_-]+$/;
|
|
277
|
+
function parseWebAuthnAssertion(part) {
|
|
278
|
+
const pieces = part.split(".");
|
|
279
|
+
if (pieces.length !== 3 || pieces.some((p) => !B64URL.test(p) || p.length % 4 === 1)) return void 0;
|
|
280
|
+
const [authenticatorData, clientDataJSON, signature] = pieces.map((p) => Buffer.from(p, "base64url"));
|
|
281
|
+
return { authenticatorData, clientDataJSON, signature };
|
|
282
|
+
}
|
|
283
|
+
var COORDINATE = /^[A-Za-z0-9_-]{43}$/;
|
|
284
|
+
function isWebAuthnKey(jwk) {
|
|
285
|
+
if (jwk === null || typeof jwk !== "object" || Array.isArray(jwk)) return false;
|
|
286
|
+
const key = jwk;
|
|
287
|
+
const w = key.webauthn;
|
|
288
|
+
return key.kty === "EC" && key.crv === "P-256" && typeof key.x === "string" && COORDINATE.test(key.x) && typeof key.y === "string" && COORDINATE.test(key.y) && (key.alg === void 0 || key.alg === "ES256") && w !== null && typeof w === "object" && !Array.isArray(w) && typeof w.rp_id === "string" && w.rp_id !== "" && (w.origins === void 0 || Array.isArray(w.origins) && w.origins.every((o) => typeof o === "string"));
|
|
289
|
+
}
|
|
290
|
+
var UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
291
|
+
var MAX_CLIENT_DATA_NESTING = 64;
|
|
292
|
+
function jsonDepth(text) {
|
|
293
|
+
let depth = 0;
|
|
294
|
+
let deepest = 0;
|
|
295
|
+
let inString = false;
|
|
296
|
+
for (let i = 0; i < text.length; i++) {
|
|
297
|
+
const ch = text[i];
|
|
298
|
+
if (inString) {
|
|
299
|
+
if (ch === "\\") i++;
|
|
300
|
+
else if (ch === '"') inString = false;
|
|
301
|
+
} else if (ch === '"') {
|
|
302
|
+
inString = true;
|
|
303
|
+
} else if (ch === "{" || ch === "[") {
|
|
304
|
+
deepest = Math.max(deepest, ++depth);
|
|
305
|
+
} else if (ch === "}" || ch === "]") {
|
|
306
|
+
depth--;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return deepest;
|
|
310
|
+
}
|
|
311
|
+
var UP = 1;
|
|
312
|
+
var UV = 4;
|
|
313
|
+
var BE = 8;
|
|
314
|
+
var BS = 16;
|
|
315
|
+
var AT = 64;
|
|
316
|
+
var ED = 128;
|
|
317
|
+
function webAuthnFailure(canonical, assertion, jwk) {
|
|
318
|
+
const w = jwk.webauthn;
|
|
319
|
+
if (!Array.isArray(w.origins) || w.origins.length === 0) return "WebAuthn key lists no origin";
|
|
320
|
+
let text;
|
|
321
|
+
try {
|
|
322
|
+
text = UTF8.decode(assertion.clientDataJSON);
|
|
323
|
+
} catch {
|
|
324
|
+
text = void 0;
|
|
325
|
+
}
|
|
326
|
+
if (text !== void 0 && jsonDepth(text) > MAX_CLIENT_DATA_NESTING) {
|
|
327
|
+
return `WebAuthn clientDataJSON nests deeper than ${MAX_CLIENT_DATA_NESTING} levels`;
|
|
328
|
+
}
|
|
329
|
+
let client;
|
|
330
|
+
try {
|
|
331
|
+
client = text === void 0 ? void 0 : JSON.parse(text);
|
|
332
|
+
} catch {
|
|
333
|
+
client = void 0;
|
|
334
|
+
}
|
|
335
|
+
if (client === null || typeof client !== "object" || Array.isArray(client)) {
|
|
336
|
+
return "WebAuthn clientDataJSON is not a JSON object";
|
|
337
|
+
}
|
|
338
|
+
const data = client;
|
|
339
|
+
if (data.type !== "webauthn.get") return "WebAuthn clientDataJSON type is not webauthn.get";
|
|
340
|
+
if (data.challenge !== b64urlNoPad((0, import_node_crypto2.createHash)("sha256").update(canonical).digest())) {
|
|
341
|
+
return "WebAuthn challenge is not the digest of this request";
|
|
342
|
+
}
|
|
343
|
+
if (!w.origins.includes(data.origin)) return "WebAuthn origin is not one the key allows";
|
|
344
|
+
if (data.crossOrigin === true || data.topOrigin !== void 0) {
|
|
345
|
+
return "WebAuthn assertion was made in a cross-origin frame";
|
|
346
|
+
}
|
|
347
|
+
const auth = assertion.authenticatorData;
|
|
348
|
+
if (auth.length < 37) return "WebAuthn authenticatorData is too short";
|
|
349
|
+
if (!auth.subarray(0, 32).equals((0, import_node_crypto2.createHash)("sha256").update(w.rp_id, "utf-8").digest())) {
|
|
350
|
+
return "WebAuthn rpIdHash does not match the key's rp_id";
|
|
351
|
+
}
|
|
352
|
+
const flags = auth[32];
|
|
353
|
+
if ((flags & (UP | UV)) !== (UP | UV)) return "WebAuthn assertion lacks user presence or verification";
|
|
354
|
+
if (flags & BS && !(flags & BE)) return "WebAuthn assertion is backed up but not backup eligible";
|
|
355
|
+
if (flags & AT) return "WebAuthn assertion carries attested credential data";
|
|
356
|
+
if (flags & ED ? auth.length === 37 : auth.length !== 37) {
|
|
357
|
+
return "WebAuthn authenticatorData does not end where its flags say";
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const key = (0, import_node_crypto2.createPublicKey)({
|
|
361
|
+
key: { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
|
|
362
|
+
format: "jwk"
|
|
363
|
+
});
|
|
364
|
+
const signed = Buffer.concat([auth, (0, import_node_crypto2.createHash)("sha256").update(assertion.clientDataJSON).digest()]);
|
|
365
|
+
return (0, import_node_crypto2.verify)("sha256", signed, { key, dsaEncoding: "der" }, assertion.signature) ? null : "Signature failed verification";
|
|
366
|
+
} catch {
|
|
367
|
+
return "Signature failed verification";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
242
370
|
function deriveKeypair(uri) {
|
|
243
371
|
const seed = deriveSeed(uri);
|
|
244
372
|
const privateKey = privateKeyFromSeed(seed);
|
|
@@ -355,6 +483,7 @@ var E = {
|
|
|
355
483
|
SIG_KEY_REVOKED: -32072,
|
|
356
484
|
SIG_ROTATION_KEY_MISMATCH: -32073,
|
|
357
485
|
SIG_ARTEFACT_DIGEST_MISMATCH: -32074,
|
|
486
|
+
SIG_ALREADY_ACCEPTED: -32075,
|
|
358
487
|
// audit-scitt/1.0 profile (profiles/audit-scitt.md S8)
|
|
359
488
|
SCITT_UNREACHABLE: -32080,
|
|
360
489
|
SCITT_STATEMENT_REJECTED: -32081,
|
|
@@ -387,6 +516,44 @@ function isValidEnvelope(env) {
|
|
|
387
516
|
return e.jsonrpc === "2.0";
|
|
388
517
|
}
|
|
389
518
|
|
|
519
|
+
// src/params.ts
|
|
520
|
+
var NAMES = {
|
|
521
|
+
string: "a string",
|
|
522
|
+
strings: "a list of strings",
|
|
523
|
+
boolean: "a boolean",
|
|
524
|
+
object: "an object",
|
|
525
|
+
list: "a list",
|
|
526
|
+
integer: "an integer"
|
|
527
|
+
};
|
|
528
|
+
function isKind(value, kind) {
|
|
529
|
+
switch (kind) {
|
|
530
|
+
case "string":
|
|
531
|
+
return typeof value === "string";
|
|
532
|
+
case "strings":
|
|
533
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
534
|
+
case "boolean":
|
|
535
|
+
return typeof value === "boolean";
|
|
536
|
+
case "object":
|
|
537
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
538
|
+
case "list":
|
|
539
|
+
return Array.isArray(value);
|
|
540
|
+
case "integer":
|
|
541
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function paramsError(params, checks) {
|
|
545
|
+
for (const [name, kind] of checks) {
|
|
546
|
+
let value = params;
|
|
547
|
+
for (const part of name.split(".")) {
|
|
548
|
+
value = typeof value === "object" && value !== null && !Array.isArray(value) ? value[part] : void 0;
|
|
549
|
+
}
|
|
550
|
+
if (value !== void 0 && value !== null && !isKind(value, kind)) {
|
|
551
|
+
return { error: rpcError(E.PARAMS, `${name} must be ${NAMES[kind]}`) };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
390
557
|
// src/audit.ts
|
|
391
558
|
function entryRecord(entry) {
|
|
392
559
|
if (entry.outcome != null) return { outcome: entry.outcome, request: entry.request };
|
|
@@ -430,7 +597,8 @@ var UNRECORDED_CODES = /* @__PURE__ */ new Set([
|
|
|
430
597
|
E.SIG_VERIFY_FAILED,
|
|
431
598
|
E.SIG_KEY_NOT_FOUND,
|
|
432
599
|
E.SIG_KEY_REVOKED,
|
|
433
|
-
E.SIG_ROTATION_KEY_MISMATCH
|
|
600
|
+
E.SIG_ROTATION_KEY_MISMATCH,
|
|
601
|
+
E.SIG_ALREADY_ACCEPTED
|
|
434
602
|
]);
|
|
435
603
|
var UNRECORDED_METHODS = /* @__PURE__ */ new Set(["workspace.create", "participant.join"]);
|
|
436
604
|
function refusalIsRecorded(method, error, privileged) {
|
|
@@ -553,77 +721,116 @@ function deepEqual(a, b) {
|
|
|
553
721
|
}
|
|
554
722
|
return true;
|
|
555
723
|
}
|
|
556
|
-
function
|
|
724
|
+
function addAt(doc, path, value, size, copied = false, movedFrom) {
|
|
725
|
+
const parts = splitPath(path);
|
|
726
|
+
if (parts.length === 0) {
|
|
727
|
+
if (size) {
|
|
728
|
+
size.reset(value);
|
|
729
|
+
if (copied || movedFrom !== void 0) size.carry(size);
|
|
730
|
+
}
|
|
731
|
+
return value;
|
|
732
|
+
}
|
|
733
|
+
const [parent, key] = navigate(doc, parts);
|
|
734
|
+
let newKey;
|
|
735
|
+
if (Array.isArray(parent)) {
|
|
736
|
+
if (key === "-") {
|
|
737
|
+
parent.push(value);
|
|
738
|
+
} else {
|
|
739
|
+
const idx = key;
|
|
740
|
+
if (idx < 0 || idx > parent.length) {
|
|
741
|
+
throw new PatchError(`Index out of range for add: ${path}`);
|
|
742
|
+
}
|
|
743
|
+
parent.splice(idx, 0, value);
|
|
744
|
+
}
|
|
745
|
+
} else if (parent !== null && typeof parent === "object") {
|
|
746
|
+
if (Object.prototype.hasOwnProperty.call(parent, key)) size?.take(parent[key]);
|
|
747
|
+
else newKey = key;
|
|
748
|
+
parent[key] = value;
|
|
749
|
+
} else {
|
|
750
|
+
throw new PatchError(`Cannot add into ${jsonType(parent)}`);
|
|
751
|
+
}
|
|
752
|
+
if (size) {
|
|
753
|
+
if (movedFrom === void 0) {
|
|
754
|
+
const inserted = size.insert(value, parts.length, newKey);
|
|
755
|
+
if (copied) size.carry(inserted);
|
|
756
|
+
} else {
|
|
757
|
+
if (newKey !== void 0) size.bytes += utf8Length(newKey);
|
|
758
|
+
if (parts.length > movedFrom) {
|
|
759
|
+
const moved = sizeOf(value);
|
|
760
|
+
size.depth = Math.max(size.depth, parts.length + moved.depth);
|
|
761
|
+
size.carry(moved);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return doc;
|
|
766
|
+
}
|
|
767
|
+
function removeAt(doc, path, size, moved = false) {
|
|
768
|
+
const parts = splitPath(path);
|
|
769
|
+
if (parts.length === 0) throw new PatchError("Cannot remove root.");
|
|
770
|
+
const [parent, key] = navigate(doc, parts);
|
|
771
|
+
if (Array.isArray(parent)) {
|
|
772
|
+
const idx = key === "-" ? -1 : key;
|
|
773
|
+
if (idx < 0 || idx >= parent.length) {
|
|
774
|
+
throw new PatchError(`Index out of range for remove: ${path}`);
|
|
775
|
+
}
|
|
776
|
+
if (size && !moved) size.take(parent[idx]);
|
|
777
|
+
parent.splice(idx, 1);
|
|
778
|
+
} else if (parent !== null && typeof parent === "object") {
|
|
779
|
+
if (!Object.prototype.hasOwnProperty.call(parent, key)) {
|
|
780
|
+
throw new PatchError(`Path not found for remove: ${path}`);
|
|
781
|
+
}
|
|
782
|
+
if (size) {
|
|
783
|
+
if (moved) size.bytes -= utf8Length(key);
|
|
784
|
+
else size.take(parent[key], key);
|
|
785
|
+
}
|
|
786
|
+
delete parent[key];
|
|
787
|
+
} else {
|
|
788
|
+
throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
|
|
789
|
+
}
|
|
790
|
+
return doc;
|
|
791
|
+
}
|
|
792
|
+
function applyOne(doc, op, size) {
|
|
557
793
|
const kind = op.op;
|
|
558
794
|
const path = op.path ?? "";
|
|
559
795
|
if (kind === "add") {
|
|
560
796
|
if (!("value" in op)) throw new PatchError("'add' requires 'value'");
|
|
561
|
-
|
|
562
|
-
if (parts.length === 0) return op.value;
|
|
563
|
-
const [parent, key] = navigate(doc, parts);
|
|
564
|
-
if (Array.isArray(parent)) {
|
|
565
|
-
if (key === "-") {
|
|
566
|
-
parent.push(op.value);
|
|
567
|
-
} else {
|
|
568
|
-
const idx = key;
|
|
569
|
-
if (idx < 0 || idx > parent.length) {
|
|
570
|
-
throw new PatchError(`Index out of range for add: ${path}`);
|
|
571
|
-
}
|
|
572
|
-
parent.splice(idx, 0, op.value);
|
|
573
|
-
}
|
|
574
|
-
} else if (parent !== null && typeof parent === "object") {
|
|
575
|
-
parent[key] = op.value;
|
|
576
|
-
} else {
|
|
577
|
-
throw new PatchError(`Cannot add into ${jsonType(parent)}`);
|
|
578
|
-
}
|
|
579
|
-
return doc;
|
|
797
|
+
return addAt(doc, path, clone(op.value), size);
|
|
580
798
|
}
|
|
581
799
|
if (kind === "replace") {
|
|
582
800
|
if (!("value" in op)) throw new PatchError("'replace' requires 'value'");
|
|
583
801
|
const parts = splitPath(path);
|
|
584
|
-
|
|
802
|
+
const value = clone(op.value);
|
|
803
|
+
if (parts.length === 0) {
|
|
804
|
+
size?.reset(value);
|
|
805
|
+
return value;
|
|
806
|
+
}
|
|
585
807
|
const [parent, key] = navigate(doc, parts);
|
|
586
808
|
if (Array.isArray(parent)) {
|
|
587
809
|
const idx = key === "-" ? -1 : key;
|
|
588
810
|
if (idx < 0 || idx >= parent.length) {
|
|
589
811
|
throw new PatchError(`Path not found for replace: ${path}`);
|
|
590
812
|
}
|
|
591
|
-
parent[idx]
|
|
813
|
+
size?.take(parent[idx]);
|
|
814
|
+
parent[idx] = value;
|
|
592
815
|
} else if (parent !== null && typeof parent === "object") {
|
|
593
816
|
if (!Object.prototype.hasOwnProperty.call(parent, key)) {
|
|
594
817
|
throw new PatchError(`Path not found for replace: ${path}`);
|
|
595
818
|
}
|
|
596
|
-
parent[key]
|
|
819
|
+
size?.take(parent[key]);
|
|
820
|
+
parent[key] = value;
|
|
597
821
|
} else {
|
|
598
822
|
throw new PatchError(`Cannot replace in ${jsonType(parent)}`);
|
|
599
823
|
}
|
|
824
|
+
size?.insert(value, parts.length);
|
|
600
825
|
return doc;
|
|
601
826
|
}
|
|
602
827
|
if (kind === "remove") {
|
|
603
|
-
|
|
604
|
-
if (parts.length === 0) throw new PatchError("Cannot remove root.");
|
|
605
|
-
const [parent, key] = navigate(doc, parts);
|
|
606
|
-
if (Array.isArray(parent)) {
|
|
607
|
-
const idx = key === "-" ? -1 : key;
|
|
608
|
-
if (idx < 0 || idx >= parent.length) {
|
|
609
|
-
throw new PatchError(`Index out of range for remove: ${path}`);
|
|
610
|
-
}
|
|
611
|
-
parent.splice(idx, 1);
|
|
612
|
-
} else if (parent !== null && typeof parent === "object") {
|
|
613
|
-
if (!Object.prototype.hasOwnProperty.call(parent, key)) {
|
|
614
|
-
throw new PatchError(`Path not found for remove: ${path}`);
|
|
615
|
-
}
|
|
616
|
-
delete parent[key];
|
|
617
|
-
} else {
|
|
618
|
-
throw new PatchError(`Cannot remove from ${jsonType(parent)}`);
|
|
619
|
-
}
|
|
620
|
-
return doc;
|
|
828
|
+
return removeAt(doc, path, size);
|
|
621
829
|
}
|
|
622
830
|
if (kind === "copy") {
|
|
623
831
|
const src = op.from;
|
|
624
832
|
if (src === void 0) throw new PatchError("'copy' requires 'from'");
|
|
625
|
-
|
|
626
|
-
return applyOne(doc, { op: "add", path, value });
|
|
833
|
+
return addAt(doc, path, clone(getAt(doc, src)), size, true);
|
|
627
834
|
}
|
|
628
835
|
if (kind === "move") {
|
|
629
836
|
const src = op.from;
|
|
@@ -631,9 +838,9 @@ function applyOne(doc, op) {
|
|
|
631
838
|
if (path.startsWith(src + "/")) {
|
|
632
839
|
throw new PatchError("'move' cannot move a location into its own child.");
|
|
633
840
|
}
|
|
634
|
-
const value =
|
|
635
|
-
doc =
|
|
636
|
-
return
|
|
841
|
+
const value = getAt(doc, src);
|
|
842
|
+
doc = removeAt(doc, src, size, true);
|
|
843
|
+
return addAt(doc, path, value, size, false, splitPath(src).length);
|
|
637
844
|
}
|
|
638
845
|
if (kind === "test") {
|
|
639
846
|
if (!("value" in op)) throw new PatchError("'test' requires 'value'");
|
|
@@ -645,29 +852,122 @@ function applyOne(doc, op) {
|
|
|
645
852
|
}
|
|
646
853
|
var MAX_PATCH_OPS = 1e3;
|
|
647
854
|
var MAX_DOCUMENT_NODES = 1e5;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
855
|
+
var MAX_DOCUMENT_DEPTH = 64;
|
|
856
|
+
var MAX_CARRIED_TIMES = 3;
|
|
857
|
+
var DEFAULT_MAX_PATCHED_BYTES = 1048576;
|
|
858
|
+
function utf8Length(s) {
|
|
859
|
+
const buffer = globalThis.Buffer;
|
|
860
|
+
if (typeof buffer === "function") {
|
|
861
|
+
return buffer.byteLength(s, "utf8");
|
|
862
|
+
}
|
|
863
|
+
let n = 0;
|
|
864
|
+
for (let i = 0; i < s.length; i++) {
|
|
865
|
+
const c = s.charCodeAt(i);
|
|
866
|
+
if (c < 128) n += 1;
|
|
867
|
+
else if (c < 2048) n += 2;
|
|
868
|
+
else if (c >= 55296 && c <= 56319 && i + 1 < s.length) {
|
|
869
|
+
const d = s.charCodeAt(i + 1);
|
|
870
|
+
if (d >= 56320 && d <= 57343) {
|
|
871
|
+
n += 4;
|
|
872
|
+
i++;
|
|
873
|
+
} else n += 3;
|
|
874
|
+
} else n += 3;
|
|
875
|
+
}
|
|
876
|
+
return n;
|
|
877
|
+
}
|
|
878
|
+
function sizeOf(value) {
|
|
879
|
+
if (value === null || typeof value !== "object") {
|
|
880
|
+
return { nodes: 1, bytes: typeof value === "string" ? utf8Length(value) : 0, depth: 1 };
|
|
881
|
+
}
|
|
882
|
+
let nodes = 0;
|
|
883
|
+
let bytes = 0;
|
|
884
|
+
let depth = 0;
|
|
885
|
+
const values = [value];
|
|
886
|
+
const levels = [1];
|
|
887
|
+
const visit = (x, level) => {
|
|
888
|
+
if (x !== null && typeof x === "object") {
|
|
889
|
+
values.push(x);
|
|
890
|
+
levels.push(level);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
nodes += 1;
|
|
894
|
+
if (level > depth) depth = level;
|
|
895
|
+
if (typeof x === "string") bytes += utf8Length(x);
|
|
896
|
+
};
|
|
897
|
+
while (values.length > 0) {
|
|
898
|
+
const v = values.pop();
|
|
899
|
+
const level = levels.pop();
|
|
900
|
+
nodes += 1;
|
|
901
|
+
if (level > depth) depth = level;
|
|
902
|
+
if (Array.isArray(v)) {
|
|
903
|
+
for (const x of v) visit(x, level + 1);
|
|
904
|
+
} else {
|
|
905
|
+
const o = v;
|
|
906
|
+
for (const k of Object.keys(o)) {
|
|
907
|
+
bytes += utf8Length(k);
|
|
908
|
+
visit(o[k], level + 1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
658
911
|
}
|
|
659
|
-
return
|
|
912
|
+
return { nodes, bytes, depth };
|
|
660
913
|
}
|
|
661
|
-
|
|
914
|
+
var Size = class {
|
|
915
|
+
nodes = 0;
|
|
916
|
+
bytes = 0;
|
|
917
|
+
depth = 0;
|
|
918
|
+
/** What copies, and moves to a deeper place or onto the whole document, have carried. */
|
|
919
|
+
carriedNodes = 0;
|
|
920
|
+
carriedBytes = 0;
|
|
921
|
+
constructor(doc) {
|
|
922
|
+
this.reset(doc);
|
|
923
|
+
}
|
|
924
|
+
reset(doc) {
|
|
925
|
+
const s = sizeOf(doc);
|
|
926
|
+
this.nodes = s.nodes;
|
|
927
|
+
this.bytes = s.bytes;
|
|
928
|
+
this.depth = s.depth;
|
|
929
|
+
}
|
|
930
|
+
/** `value` goes in `segments` levels below the top, under a new `key`. */
|
|
931
|
+
insert(value, segments, key) {
|
|
932
|
+
const s = sizeOf(value);
|
|
933
|
+
this.nodes += s.nodes;
|
|
934
|
+
this.bytes += s.bytes + (key === void 0 ? 0 : utf8Length(key));
|
|
935
|
+
this.depth = Math.max(this.depth, segments + s.depth);
|
|
936
|
+
return s;
|
|
937
|
+
}
|
|
938
|
+
carry(s) {
|
|
939
|
+
this.carriedNodes += s.nodes;
|
|
940
|
+
this.carriedBytes += s.bytes;
|
|
941
|
+
}
|
|
942
|
+
/** `value` comes out, with its `key`. */
|
|
943
|
+
take(value, key) {
|
|
944
|
+
const s = sizeOf(value);
|
|
945
|
+
this.nodes -= s.nodes;
|
|
946
|
+
this.bytes -= s.bytes + (key === void 0 ? 0 : utf8Length(key));
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
function applyJsonPatch(doc, ops, limits = {}) {
|
|
950
|
+
const maxBytes = limits.maxStringBytes ?? DEFAULT_MAX_PATCHED_BYTES;
|
|
662
951
|
if (ops.length > MAX_PATCH_OPS) {
|
|
663
952
|
throw new PatchError(`Patch has too many operations (${ops.length} > ${MAX_PATCH_OPS})`);
|
|
664
953
|
}
|
|
665
954
|
let out = JSON.parse(JSON.stringify(doc));
|
|
955
|
+
const size = new Size(out);
|
|
666
956
|
for (const op of ops) {
|
|
667
|
-
out = applyOne(out, op);
|
|
668
|
-
if (
|
|
957
|
+
out = applyOne(out, op, size);
|
|
958
|
+
if (size.depth > MAX_DOCUMENT_DEPTH) size.depth = sizeOf(out).depth;
|
|
959
|
+
if (size.nodes > MAX_DOCUMENT_NODES) {
|
|
669
960
|
throw new PatchError(`Patched document exceeds the node limit (${MAX_DOCUMENT_NODES})`);
|
|
670
961
|
}
|
|
962
|
+
if (size.bytes > maxBytes) {
|
|
963
|
+
throw new PatchError(`Patched document exceeds the size limit (${maxBytes} bytes of strings and keys)`);
|
|
964
|
+
}
|
|
965
|
+
if (size.depth > MAX_DOCUMENT_DEPTH) {
|
|
966
|
+
throw new PatchError(`Patched document nests deeper than ${MAX_DOCUMENT_DEPTH} levels`);
|
|
967
|
+
}
|
|
968
|
+
if (size.carriedNodes > MAX_CARRIED_TIMES * MAX_DOCUMENT_NODES || size.carriedBytes > MAX_CARRIED_TIMES * maxBytes) {
|
|
969
|
+
throw new PatchError(`Patch copies or moves more than its limit (${MAX_CARRIED_TIMES * MAX_DOCUMENT_NODES} values or ${MAX_CARRIED_TIMES * maxBytes} bytes of strings and keys)`);
|
|
970
|
+
}
|
|
671
971
|
}
|
|
672
972
|
return out;
|
|
673
973
|
}
|
|
@@ -715,7 +1015,8 @@ var COLLECTIONS = [
|
|
|
715
1015
|
"deliberations",
|
|
716
1016
|
"handoffs",
|
|
717
1017
|
"snapshots",
|
|
718
|
-
"route_decisions"
|
|
1018
|
+
"route_decisions",
|
|
1019
|
+
"invitations"
|
|
719
1020
|
];
|
|
720
1021
|
var IDEMPOTENCY_KEYS = "idempotency_keys";
|
|
721
1022
|
var StoreConflictError = class extends Error {
|
|
@@ -726,7 +1027,7 @@ var StoreConflictError = class extends Error {
|
|
|
726
1027
|
base;
|
|
727
1028
|
constructor(workspace, held, base) {
|
|
728
1029
|
super(
|
|
729
|
-
`The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, the store may have been changed outside this coordinator
|
|
1030
|
+
`The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, or the store may have been changed outside this coordinator. A coordinator reloads the workspace and answers the call as failed, and the call may be sent again.`
|
|
730
1031
|
);
|
|
731
1032
|
this.name = "StoreConflictError";
|
|
732
1033
|
this.workspace = workspace;
|
|
@@ -803,20 +1104,24 @@ function assembleSnapshot(meta, entries, objects) {
|
|
|
803
1104
|
var MemoryStore = class {
|
|
804
1105
|
held = /* @__PURE__ */ new Map();
|
|
805
1106
|
load() {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
|
|
815
|
-
version: h.version,
|
|
816
|
-
updated_at: h.updated_at
|
|
817
|
-
});
|
|
1107
|
+
return [...this.held.keys()].map((id) => this.loadOne(id));
|
|
1108
|
+
}
|
|
1109
|
+
loadOne(id) {
|
|
1110
|
+
const h = this.held.get(id);
|
|
1111
|
+
if (!h) return void 0;
|
|
1112
|
+
const objects = [];
|
|
1113
|
+
for (const [collection, items] of h.objects) {
|
|
1114
|
+
for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);
|
|
818
1115
|
}
|
|
819
|
-
return
|
|
1116
|
+
return {
|
|
1117
|
+
id,
|
|
1118
|
+
data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
|
|
1119
|
+
version: h.version,
|
|
1120
|
+
updated_at: h.updated_at
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
version(id) {
|
|
1124
|
+
return this.held.get(id)?.entries.length;
|
|
820
1125
|
}
|
|
821
1126
|
save(record) {
|
|
822
1127
|
this.apply(changeFromRecord(record));
|
|
@@ -943,7 +1248,8 @@ function makeApi(coord) {
|
|
|
943
1248
|
workspace: {
|
|
944
1249
|
create: (p) => c("workspace.create", p),
|
|
945
1250
|
describe: (p) => c("workspace.describe", p),
|
|
946
|
-
set_profiles: (p) => c("workspace.set_profiles", p)
|
|
1251
|
+
set_profiles: (p) => c("workspace.set_profiles", p),
|
|
1252
|
+
invite: (p) => c("workspace.invite", p)
|
|
947
1253
|
},
|
|
948
1254
|
participant: {
|
|
949
1255
|
join: (p) => c("participant.join", p),
|
|
@@ -959,6 +1265,7 @@ function makeApi(coord) {
|
|
|
959
1265
|
},
|
|
960
1266
|
review: {
|
|
961
1267
|
request: (p) => c("review.request", p),
|
|
1268
|
+
acknowledge: (p) => c("review.acknowledge", p),
|
|
962
1269
|
depth: (p) => c("review.depth", p)
|
|
963
1270
|
},
|
|
964
1271
|
decide: {
|
|
@@ -1032,10 +1339,17 @@ function registerAuditScitt(coord) {
|
|
|
1032
1339
|
coord.handlers.set("audit.submit_to_scitt", (p) => {
|
|
1033
1340
|
const ws = coord.workspaces.get(p.workspace);
|
|
1034
1341
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1342
|
+
const bad = paramsError(p, [
|
|
1343
|
+
["range", "object"],
|
|
1344
|
+
["range.from_seq", "integer"],
|
|
1345
|
+
["range.to_seq", "integer"],
|
|
1346
|
+
["issuer", "string"]
|
|
1347
|
+
]);
|
|
1348
|
+
if (bad) return bad;
|
|
1035
1349
|
const range = p.range ?? {};
|
|
1036
1350
|
const fromSeq = range.from_seq ?? 0;
|
|
1037
1351
|
const toSeq = range.to_seq ?? ws.audit.length;
|
|
1038
|
-
const issuer = p.issuer
|
|
1352
|
+
const issuer = p.issuer || "service:coordinator";
|
|
1039
1353
|
if (!coord.options.scittSubmitter) {
|
|
1040
1354
|
const statements = ws.audit.slice(fromSeq, toSeq).map((e) => buildStatement(ws.id, entryRecord(e), issuer));
|
|
1041
1355
|
return { result: {
|
|
@@ -1065,7 +1379,7 @@ function registerAuditScitt(coord) {
|
|
|
1065
1379
|
});
|
|
1066
1380
|
coord.handlers.set("audit.verify_receipt", (p) => {
|
|
1067
1381
|
const receipt = p.receipt;
|
|
1068
|
-
if (typeof receipt !== "object" || receipt === null) {
|
|
1382
|
+
if (typeof receipt !== "object" || receipt === null || Array.isArray(receipt)) {
|
|
1069
1383
|
return { error: rpcError(E.PARAMS, "receipt must be an object") };
|
|
1070
1384
|
}
|
|
1071
1385
|
if (coord.options.verifyScittReceipt) {
|
|
@@ -1079,7 +1393,7 @@ function registerAuditScitt(coord) {
|
|
|
1079
1393
|
if (!ok) return { error: rpcError(E.SCITT_RECEIPT_INVALID, "Receipt did not verify") };
|
|
1080
1394
|
return { result: { verified: true } };
|
|
1081
1395
|
}
|
|
1082
|
-
return { error: rpcError(E.SCITT_RECEIPT_INVALID, "No
|
|
1396
|
+
return { error: rpcError(E.SCITT_RECEIPT_INVALID, "No receipt verifier is configured; receipt not verified") };
|
|
1083
1397
|
});
|
|
1084
1398
|
coord.handlers.set("audit.verify_chain", (p) => {
|
|
1085
1399
|
const ws = coord.workspaces.get(p.workspace);
|
|
@@ -1087,7 +1401,7 @@ function registerAuditScitt(coord) {
|
|
|
1087
1401
|
if (!(ws.chain_enabled || coord.options.enableChain)) {
|
|
1088
1402
|
return { error: rpcError(E.PARAMS, "Chain not enabled for this workspace") };
|
|
1089
1403
|
}
|
|
1090
|
-
if (p.from_seq
|
|
1404
|
+
if (p.from_seq != null || p.to_seq != null) {
|
|
1091
1405
|
return { error: rpcError(
|
|
1092
1406
|
E.PARAMS,
|
|
1093
1407
|
"Range verification is not implemented; omit from_seq and to_seq to verify the whole log."
|
|
@@ -1149,13 +1463,21 @@ function registerAuditScitt(coord) {
|
|
|
1149
1463
|
// src/profiles/control.ts
|
|
1150
1464
|
var VALID_MODES = /* @__PURE__ */ new Set(["shadow", "trial", "production"]);
|
|
1151
1465
|
var VALID_SCOPES = /* @__PURE__ */ new Set(["task", "participant", "workspace"]);
|
|
1466
|
+
var SCOPE_ERROR = "scope must be task, participant or workspace";
|
|
1152
1467
|
function registerControl(coord) {
|
|
1153
1468
|
coord.handlers.set("control.pause", (p) => {
|
|
1154
1469
|
const ws = coord.workspaces.get(p.workspace);
|
|
1155
1470
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1471
|
+
const bad = paramsError(p, [
|
|
1472
|
+
["scope", "string"],
|
|
1473
|
+
["participant_uri", "string"],
|
|
1474
|
+
["uri", "string"],
|
|
1475
|
+
["in_flight_policy", "string"]
|
|
1476
|
+
]);
|
|
1477
|
+
if (bad) return bad;
|
|
1156
1478
|
const scope = p.scope || "task";
|
|
1157
1479
|
if (!VALID_SCOPES.has(scope)) {
|
|
1158
|
-
return { error: rpcError(E.PARAMS,
|
|
1480
|
+
return { error: rpcError(E.PARAMS, SCOPE_ERROR) };
|
|
1159
1481
|
}
|
|
1160
1482
|
if (scope === "task") {
|
|
1161
1483
|
const task = ws.tasks.get(p.task_id);
|
|
@@ -1173,7 +1495,8 @@ function registerControl(coord) {
|
|
|
1173
1495
|
}
|
|
1174
1496
|
if (scope === "participant") {
|
|
1175
1497
|
const uri = p.participant_uri || p.uri;
|
|
1176
|
-
if (!uri
|
|
1498
|
+
if (!uri) return { error: rpcError(E.PARAMS, "participant_uri is required") };
|
|
1499
|
+
if (!ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
|
|
1177
1500
|
ws.members.get(uri).paused = true;
|
|
1178
1501
|
return { result: {
|
|
1179
1502
|
scope: "participant",
|
|
@@ -1188,9 +1511,11 @@ function registerControl(coord) {
|
|
|
1188
1511
|
coord.handlers.set("control.resume", (p) => {
|
|
1189
1512
|
const ws = coord.workspaces.get(p.workspace);
|
|
1190
1513
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1514
|
+
const bad = paramsError(p, [["scope", "string"], ["participant_uri", "string"], ["uri", "string"]]);
|
|
1515
|
+
if (bad) return bad;
|
|
1191
1516
|
const scope = p.scope || "task";
|
|
1192
1517
|
if (!VALID_SCOPES.has(scope)) {
|
|
1193
|
-
return { error: rpcError(E.PARAMS,
|
|
1518
|
+
return { error: rpcError(E.PARAMS, SCOPE_ERROR) };
|
|
1194
1519
|
}
|
|
1195
1520
|
if (scope === "task") {
|
|
1196
1521
|
const task = ws.tasks.get(p.task_id);
|
|
@@ -1208,7 +1533,9 @@ function registerControl(coord) {
|
|
|
1208
1533
|
}
|
|
1209
1534
|
if (scope === "participant") {
|
|
1210
1535
|
const uri = p.participant_uri || p.uri;
|
|
1211
|
-
if (!uri
|
|
1536
|
+
if (!uri) return { error: rpcError(E.PARAMS, "participant_uri is required") };
|
|
1537
|
+
if (!ws.members.has(uri)) return { error: rpcError(E.PARAMS, `Unknown participant: ${uri}`) };
|
|
1538
|
+
if (uri === p.from) return { error: rpcError(E.NOT_AUTHORISED, "A paused participant does not lift its own pause") };
|
|
1212
1539
|
ws.members.get(uri).paused = false;
|
|
1213
1540
|
return { result: { scope: "participant", participant_uri: uri, paused: false } };
|
|
1214
1541
|
}
|
|
@@ -1223,6 +1550,12 @@ function registerControl(coord) {
|
|
|
1223
1550
|
if (task.state === "completed" || task.state === "declined" || task.state === "cancelled" || task.state === "superseded") {
|
|
1224
1551
|
return { error: rpcError(E.CONTROL_NOT_AUTHORISED, `Cannot cancel ${task.state} task`) };
|
|
1225
1552
|
}
|
|
1553
|
+
if (openRequiredReviewAuthors(ws, task).includes(p.from)) {
|
|
1554
|
+
return { error: rpcError(
|
|
1555
|
+
E.NOT_AUTHORISED,
|
|
1556
|
+
"The authors of the work under review cannot end its review without a decision"
|
|
1557
|
+
) };
|
|
1558
|
+
}
|
|
1226
1559
|
task.state = "cancelled";
|
|
1227
1560
|
task.updated_at = coord.now();
|
|
1228
1561
|
task.history.push({ ts: task.updated_at, from: p.from, state: "cancelled", note: p.reason });
|
|
@@ -1231,6 +1564,8 @@ function registerControl(coord) {
|
|
|
1231
1564
|
coord.handlers.set("control.snapshot", (p) => {
|
|
1232
1565
|
const ws = coord.workspaces.get(p.workspace);
|
|
1233
1566
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1567
|
+
const bad = paramsError(p, [["include", "strings"], ["label", "string"]]);
|
|
1568
|
+
if (bad) return bad;
|
|
1234
1569
|
if (Array.isArray(p.include) && p.include.length === 0) {
|
|
1235
1570
|
return { error: rpcError(E.PARAMS, "include must not be empty") };
|
|
1236
1571
|
}
|
|
@@ -1285,6 +1620,14 @@ function registerControl(coord) {
|
|
|
1285
1620
|
coord.handlers.set("control.rollback", (p) => {
|
|
1286
1621
|
const ws = coord.workspaces.get(p.workspace);
|
|
1287
1622
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1623
|
+
const bad = paramsError(p, [
|
|
1624
|
+
["to_snapshot_artefact_id", "string"],
|
|
1625
|
+
["snapshot_artefact_id", "string"],
|
|
1626
|
+
["snapshot_id", "string"],
|
|
1627
|
+
["what_to_restore", "strings"],
|
|
1628
|
+
["reason", "string"]
|
|
1629
|
+
]);
|
|
1630
|
+
if (bad) return bad;
|
|
1288
1631
|
const snapId = p.to_snapshot_artefact_id || p.snapshot_artefact_id || p.snapshot_id;
|
|
1289
1632
|
if (!snapId) return { error: rpcError(E.PARAMS, "to_snapshot_artefact_id is required") };
|
|
1290
1633
|
const snap = ws.snapshots.get(snapId);
|
|
@@ -1306,7 +1649,8 @@ function registerControl(coord) {
|
|
|
1306
1649
|
for (const [uri, snapM] of snapByUri) {
|
|
1307
1650
|
if (ws.members.has(uri)) {
|
|
1308
1651
|
const cur = ws.members.get(uri);
|
|
1309
|
-
|
|
1652
|
+
const role = snapM.role ?? cur.role;
|
|
1653
|
+
if (role !== "admin" && cur.role !== "admin") cur.role = role;
|
|
1310
1654
|
cur.scopes = Array.isArray(snapM.scopes) ? [...snapM.scopes] : void 0;
|
|
1311
1655
|
}
|
|
1312
1656
|
}
|
|
@@ -1325,10 +1669,18 @@ function registerControl(coord) {
|
|
|
1325
1669
|
const oldId = p.task_id;
|
|
1326
1670
|
const old = ws.tasks.get(oldId);
|
|
1327
1671
|
if (!old) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
1672
|
+
if (openRequiredReviewAuthors(ws, old).includes(p.from)) {
|
|
1673
|
+
return { error: rpcError(
|
|
1674
|
+
E.NOT_AUTHORISED,
|
|
1675
|
+
"The authors of the work under review cannot end its review without a decision"
|
|
1676
|
+
) };
|
|
1677
|
+
}
|
|
1328
1678
|
const newSpec = p.successor_task ?? {};
|
|
1329
|
-
if (!newSpec.kind) {
|
|
1679
|
+
if (typeof newSpec.kind !== "string" || !newSpec.kind) {
|
|
1330
1680
|
return { error: rpcError(E.PARAMS, "successor_task must include kind") };
|
|
1331
1681
|
}
|
|
1682
|
+
const bad = paramsError(p, [["successor_task.assignee", "string"], ["reason", "string"]]);
|
|
1683
|
+
if (bad) return bad;
|
|
1332
1684
|
const assignee = newSpec.assignee || old.assignee;
|
|
1333
1685
|
if (typeof assignee !== "string" || !ws.members.has(assignee)) {
|
|
1334
1686
|
return { error: rpcError(E.PARAMS, "successor assignee not in workspace") };
|
|
@@ -1375,6 +1727,7 @@ function registerControl(coord) {
|
|
|
1375
1727
|
} else if ("review_required" in newSpec) {
|
|
1376
1728
|
newTask.review_required = !!newSpec.review_required;
|
|
1377
1729
|
}
|
|
1730
|
+
newTask.prior_authors = successorAuthors(old, p.from);
|
|
1378
1731
|
ws.tasks.set(newId, newTask);
|
|
1379
1732
|
old.state = "superseded";
|
|
1380
1733
|
old.superseded_by = newId;
|
|
@@ -1385,26 +1738,37 @@ function registerControl(coord) {
|
|
|
1385
1738
|
state: "superseded",
|
|
1386
1739
|
note: `-> ${newId}: ${p.reason || ""}`
|
|
1387
1740
|
});
|
|
1388
|
-
return { result: {
|
|
1741
|
+
return { result: {
|
|
1742
|
+
superseded_task_id: old.id,
|
|
1743
|
+
new_task_id: newId,
|
|
1744
|
+
...p.reason != null ? { reason: p.reason } : {}
|
|
1745
|
+
} };
|
|
1389
1746
|
});
|
|
1390
1747
|
coord.handlers.set("control.set_mode_ceiling", (p) => {
|
|
1391
1748
|
const ws = coord.workspaces.get(p.workspace);
|
|
1392
1749
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1750
|
+
const bad = paramsError(p, [["reason", "string"]]);
|
|
1751
|
+
if (bad) return bad;
|
|
1393
1752
|
const m = p.new_ceiling ?? p.mode_ceiling;
|
|
1394
1753
|
if (!VALID_MODES.has(m)) {
|
|
1395
|
-
return { error: rpcError(E.PARAMS,
|
|
1754
|
+
return { error: rpcError(E.PARAMS, "new_ceiling must be shadow, trial or production") };
|
|
1396
1755
|
}
|
|
1397
1756
|
ws.mode_ceiling = m;
|
|
1398
|
-
return { result: { mode_ceiling: ws.mode_ceiling, reason: p.reason } };
|
|
1757
|
+
return { result: { mode_ceiling: ws.mode_ceiling, ...p.reason != null ? { reason: p.reason } : {} } };
|
|
1399
1758
|
});
|
|
1400
1759
|
}
|
|
1401
1760
|
|
|
1402
1761
|
// src/profiles/deliberation.ts
|
|
1762
|
+
var RESERVED_IDS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1763
|
+
var QUORUM_ARG = /^[0-9]+$/;
|
|
1764
|
+
var THRESHOLD_ARG = /^[0-9]+(\.[0-9]+)?$/;
|
|
1403
1765
|
function parseRule(rule) {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1766
|
+
const colon = rule.indexOf(":");
|
|
1767
|
+
if (colon >= 0) {
|
|
1768
|
+
const kind = rule.slice(0, colon);
|
|
1769
|
+
const arg = rule.slice(colon + 1);
|
|
1770
|
+
if (kind === "quorum" && QUORUM_ARG.test(arg)) return { kind, params: { n: parseInt(arg, 10) } };
|
|
1771
|
+
if ((kind === "weighted_vote" || kind === "weighted_vote_with_veto") && THRESHOLD_ARG.test(arg)) {
|
|
1408
1772
|
return { kind, params: { threshold: parseFloat(arg) } };
|
|
1409
1773
|
}
|
|
1410
1774
|
}
|
|
@@ -1489,6 +1853,23 @@ function registerDeliberation(coord) {
|
|
|
1489
1853
|
for (const f of ["from", "rule"]) {
|
|
1490
1854
|
if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
|
|
1491
1855
|
}
|
|
1856
|
+
for (const name of ["to", "participants"]) {
|
|
1857
|
+
const v = p[name];
|
|
1858
|
+
if (v != null && typeof v !== "string" && !(Array.isArray(v) && v.every((x) => typeof x === "string"))) {
|
|
1859
|
+
return { error: rpcError(E.PARAMS, `${name} must be a string or a list of strings`) };
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (typeof p.rule !== "string") return { error: rpcError(E.PARAMS, "rule must be a string") };
|
|
1863
|
+
const bad = paramsError(p, [["weights", "object"], ["veto", "object"]]);
|
|
1864
|
+
if (bad) return bad;
|
|
1865
|
+
const weights = p.weights ?? {};
|
|
1866
|
+
if (!Object.values(weights).every((w) => typeof w === "number" && Number.isInteger(w))) {
|
|
1867
|
+
return { error: rpcError(E.PARAMS, "weights must map participants to integers") };
|
|
1868
|
+
}
|
|
1869
|
+
const veto = p.veto ?? {};
|
|
1870
|
+
if (!Object.values(veto).every((v) => typeof v === "boolean")) {
|
|
1871
|
+
return { error: rpcError(E.PARAMS, "veto must map participants to booleans") };
|
|
1872
|
+
}
|
|
1492
1873
|
const to = p.to ?? p.participants;
|
|
1493
1874
|
const participants = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
|
|
1494
1875
|
if (!participants.length) {
|
|
@@ -1500,6 +1881,18 @@ function registerDeliberation(coord) {
|
|
|
1500
1881
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1501
1882
|
return { error: rpcError(E.DELIB_UNKNOWN_RULE, msg) };
|
|
1502
1883
|
}
|
|
1884
|
+
const suppliedDeliberationId = p.deliberation_id;
|
|
1885
|
+
if (suppliedDeliberationId !== void 0 && suppliedDeliberationId !== null && suppliedDeliberationId !== "") {
|
|
1886
|
+
if (typeof suppliedDeliberationId !== "string") {
|
|
1887
|
+
return { error: rpcError(E.PARAMS, "deliberation_id must be a string") };
|
|
1888
|
+
}
|
|
1889
|
+
if (RESERVED_IDS.has(suppliedDeliberationId)) {
|
|
1890
|
+
return { error: rpcError(E.PARAMS, `deliberation_id cannot be ${JSON.stringify(suppliedDeliberationId)}`) };
|
|
1891
|
+
}
|
|
1892
|
+
if (ws.deliberations.has(suppliedDeliberationId)) {
|
|
1893
|
+
return { error: rpcError(E.PARAMS, `deliberation_id already in use: ${suppliedDeliberationId}`) };
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1503
1896
|
const id = p.deliberation_id || coord.ids.deliberationId();
|
|
1504
1897
|
const delib = {
|
|
1505
1898
|
id,
|
|
@@ -1527,6 +1920,8 @@ function registerDeliberation(coord) {
|
|
|
1527
1920
|
if (delib.state !== "open") {
|
|
1528
1921
|
return { error: rpcError(E.DELIB_CLOSED_OR_LAPSED, "Deliberation not open") };
|
|
1529
1922
|
}
|
|
1923
|
+
const bad = paramsError(p, [["comment", "string"], ["text", "string"]]);
|
|
1924
|
+
if (bad) return bad;
|
|
1530
1925
|
delib.comments.push({
|
|
1531
1926
|
ts: coord.now(),
|
|
1532
1927
|
from: p.from,
|
|
@@ -1549,6 +1944,8 @@ function registerDeliberation(coord) {
|
|
|
1549
1944
|
if (v !== "yea" && v !== "nay" && v !== "abstain") {
|
|
1550
1945
|
return { error: rpcError(E.PARAMS, "vote must be yea/nay/abstain") };
|
|
1551
1946
|
}
|
|
1947
|
+
const bad = paramsError(p, [["veto_invoked", "boolean"]]);
|
|
1948
|
+
if (bad) return bad;
|
|
1552
1949
|
const voter = p.from;
|
|
1553
1950
|
if (!delib.participants.includes(voter)) {
|
|
1554
1951
|
return { error: rpcError(
|
|
@@ -1582,6 +1979,7 @@ function registerDeliberation(coord) {
|
|
|
1582
1979
|
}
|
|
1583
1980
|
|
|
1584
1981
|
// src/profiles/handoff.ts
|
|
1982
|
+
var RESERVED_IDS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1585
1983
|
function registerHandoff(coord) {
|
|
1586
1984
|
coord.handlers.set("handoff.propose", (p) => {
|
|
1587
1985
|
const ws = coord.workspaces.get(p.workspace);
|
|
@@ -1589,6 +1987,9 @@ function registerHandoff(coord) {
|
|
|
1589
1987
|
for (const f of ["from", "to", "tasks"]) {
|
|
1590
1988
|
if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
|
|
1591
1989
|
}
|
|
1990
|
+
if (typeof p.to !== "string") return { error: rpcError(E.PARAMS, "to must be a string") };
|
|
1991
|
+
const bad = paramsError(p, [["context_links", "strings"]]);
|
|
1992
|
+
if (bad) return bad;
|
|
1592
1993
|
const proposer = p.from;
|
|
1593
1994
|
const recipient = p.to;
|
|
1594
1995
|
if (typeof recipient === "string" && !recipient.startsWith("group:")) {
|
|
@@ -1605,6 +2006,11 @@ function registerHandoff(coord) {
|
|
|
1605
2006
|
}
|
|
1606
2007
|
const proposed = [];
|
|
1607
2008
|
for (const t of tasksIn) {
|
|
2009
|
+
if (t === null || typeof t !== "object" || Array.isArray(t) || typeof t.task_id !== "string") {
|
|
2010
|
+
return { error: rpcError(E.PARAMS, "each entry in tasks must be an object with a string task_id") };
|
|
2011
|
+
}
|
|
2012
|
+
const badItem = paramsError(t, [["blockers", "strings"]]);
|
|
2013
|
+
if (badItem) return badItem;
|
|
1608
2014
|
const tid = t.task_id;
|
|
1609
2015
|
if (!tid || !ws.tasks.has(tid)) {
|
|
1610
2016
|
return { error: rpcError(E.PARAMS, `Unknown task in handoff: ${tid}`) };
|
|
@@ -1624,6 +2030,18 @@ function registerHandoff(coord) {
|
|
|
1624
2030
|
blockers: t.blockers ?? []
|
|
1625
2031
|
});
|
|
1626
2032
|
}
|
|
2033
|
+
const suppliedHandoffId = p.handoff_id;
|
|
2034
|
+
if (suppliedHandoffId !== void 0 && suppliedHandoffId !== null && suppliedHandoffId !== "") {
|
|
2035
|
+
if (typeof suppliedHandoffId !== "string") {
|
|
2036
|
+
return { error: rpcError(E.PARAMS, "handoff_id must be a string") };
|
|
2037
|
+
}
|
|
2038
|
+
if (RESERVED_IDS2.has(suppliedHandoffId)) {
|
|
2039
|
+
return { error: rpcError(E.PARAMS, `handoff_id cannot be ${JSON.stringify(suppliedHandoffId)}`) };
|
|
2040
|
+
}
|
|
2041
|
+
if (ws.handoffs.has(suppliedHandoffId)) {
|
|
2042
|
+
return { error: rpcError(E.PARAMS, `handoff_id already in use: ${suppliedHandoffId}`) };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
1627
2045
|
const id = p.handoff_id || coord.ids.handoffId();
|
|
1628
2046
|
const ho = {
|
|
1629
2047
|
id,
|
|
@@ -1674,6 +2092,8 @@ function registerHandoff(coord) {
|
|
|
1674
2092
|
if (ws.members.get(acceptor).paused) {
|
|
1675
2093
|
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Acceptor ${acceptor} is paused`) };
|
|
1676
2094
|
}
|
|
2095
|
+
const bad = paramsError(p, [["accepted_task_ids", "strings"]]);
|
|
2096
|
+
if (bad) return bad;
|
|
1677
2097
|
if (Array.isArray(p.accepted_task_ids) && p.accepted_task_ids.length === 0) {
|
|
1678
2098
|
return { error: rpcError(E.PARAMS, "accepted_task_ids must not be empty") };
|
|
1679
2099
|
}
|
|
@@ -1684,9 +2104,18 @@ function registerHandoff(coord) {
|
|
|
1684
2104
|
return { error: rpcError(E.PARAMS, `Task ${tid} not in this handoff`) };
|
|
1685
2105
|
}
|
|
1686
2106
|
}
|
|
2107
|
+
for (const tid of acceptedIds) {
|
|
2108
|
+
if (ws.tasks.get(tid)?.assignee !== ho.proposer) {
|
|
2109
|
+
return { error: rpcError(
|
|
2110
|
+
E.HANDOFF_TASKS_NOT_ASSIGNED_TO_PROPOSER,
|
|
2111
|
+
`Task ${tid} is not currently assigned to ${ho.proposer}`
|
|
2112
|
+
) };
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
1687
2115
|
const now = coord.now();
|
|
1688
2116
|
for (const tid of acceptedIds) {
|
|
1689
2117
|
const task = ws.tasks.get(tid);
|
|
2118
|
+
if (acceptor !== task.assignee) task.prior_authors = unionOf(task.prior_authors ?? [], heldBy(task, ho.proposer));
|
|
1690
2119
|
task.assignee = acceptor;
|
|
1691
2120
|
task.updated_at = now;
|
|
1692
2121
|
task.history.push({
|
|
@@ -1727,16 +2156,20 @@ function registerHandoff(coord) {
|
|
|
1727
2156
|
|
|
1728
2157
|
// src/profiles/routing.ts
|
|
1729
2158
|
var CRIT = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
1730
|
-
|
|
2159
|
+
var DECIMAL = /^-?[0-9]+(\.[0-9]+)?$/;
|
|
2160
|
+
function confidenceOf(hints) {
|
|
2161
|
+
const v = hints.confidence;
|
|
2162
|
+
if (typeof v === "number" && Number.isInteger(v)) return v;
|
|
2163
|
+
if (typeof v === "string" && DECIMAL.test(v)) return Number(v);
|
|
2164
|
+
return 1;
|
|
2165
|
+
}
|
|
2166
|
+
function criticalityOf(hints) {
|
|
1731
2167
|
const crit = hints.criticality ?? "medium";
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1738
|
-
if (isNaN(conf)) conf = 1;
|
|
1739
|
-
const r = CRIT[crit] ?? 1;
|
|
2168
|
+
return typeof crit === "string" && Object.prototype.hasOwnProperty.call(CRIT, crit) ? CRIT[crit] : 1;
|
|
2169
|
+
}
|
|
2170
|
+
function defaultDepth(hints) {
|
|
2171
|
+
const conf = confidenceOf(hints);
|
|
2172
|
+
const r = criticalityOf(hints);
|
|
1740
2173
|
if (r >= 3) return { depth: "full", summary: "criticality=critical: full review" };
|
|
1741
2174
|
if (r === 2 && conf < 0.7) return { depth: "full", summary: "criticality=high and confidence<0.7: full review" };
|
|
1742
2175
|
if (r === 0 && conf >= 0.95) return { depth: "skip", summary: "very low criticality + very high confidence: skip" };
|
|
@@ -1802,6 +2235,8 @@ function registerRouting(coord) {
|
|
|
1802
2235
|
if (ws.members.get(selected).paused) {
|
|
1803
2236
|
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${selected} is paused`) };
|
|
1804
2237
|
}
|
|
2238
|
+
const leaving = heldBy(task, p.from);
|
|
2239
|
+
if (selected !== task.assignee && leaving.length) task.prior_authors = unionOf(task.prior_authors ?? [], leaving);
|
|
1805
2240
|
task.assignee = selected;
|
|
1806
2241
|
task.updated_at = coord.now();
|
|
1807
2242
|
task.history.push({
|
|
@@ -1832,6 +2267,8 @@ function registerRouting(coord) {
|
|
|
1832
2267
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1833
2268
|
const task = ws.tasks.get(p.task_id);
|
|
1834
2269
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
2270
|
+
const bad = paramsError(p, [["artefact_routing_hints", "object"]]);
|
|
2271
|
+
if (bad) return bad;
|
|
1835
2272
|
const hints = {
|
|
1836
2273
|
...task.routing_hints ?? {},
|
|
1837
2274
|
...p.artefact_routing_hints ?? {}
|
|
@@ -1889,6 +2326,8 @@ function registerRouting(coord) {
|
|
|
1889
2326
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
1890
2327
|
const task = ws.tasks.get(p.task_id);
|
|
1891
2328
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
2329
|
+
const bad = paramsError(p, [["default_escalation_target", "string"]]);
|
|
2330
|
+
if (bad) return bad;
|
|
1892
2331
|
const hints = { ...task.routing_hints ?? {} };
|
|
1893
2332
|
let escalate, to, rule;
|
|
1894
2333
|
if (coord.options.escalationPolicy) {
|
|
@@ -1902,15 +2341,8 @@ function registerRouting(coord) {
|
|
|
1902
2341
|
return { error: rpcError(E.ROUTING_POLICY_UNREACHABLE, `escalation policy error: ${msg}`) };
|
|
1903
2342
|
}
|
|
1904
2343
|
} else {
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
try {
|
|
1908
|
-
conf = Number(hints.confidence ?? 1);
|
|
1909
|
-
} catch {
|
|
1910
|
-
conf = 1;
|
|
1911
|
-
}
|
|
1912
|
-
if (isNaN(conf)) conf = 1;
|
|
1913
|
-
const r = CRIT[crit] ?? 1;
|
|
2344
|
+
const conf = confidenceOf(hints);
|
|
2345
|
+
const r = criticalityOf(hints);
|
|
1914
2346
|
escalate = r >= 3 || r >= 2 && conf < 0.6;
|
|
1915
2347
|
to = p.default_escalation_target;
|
|
1916
2348
|
rule = {
|
|
@@ -1967,6 +2399,9 @@ function registerSecuritySigned(coord) {
|
|
|
1967
2399
|
for (const f of ["from", "old_kid", "new_jwk"]) {
|
|
1968
2400
|
if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
|
|
1969
2401
|
}
|
|
2402
|
+
for (const f of ["from", "old_kid"]) {
|
|
2403
|
+
if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
|
|
2404
|
+
}
|
|
1970
2405
|
const member = ws.members.get(p.from);
|
|
1971
2406
|
if (!member) return { error: rpcError(E.SIG_KEY_NOT_FOUND, `Unknown participant: ${p.from}`) };
|
|
1972
2407
|
const oldKid = p.old_kid;
|
|
@@ -1977,6 +2412,9 @@ function registerSecuritySigned(coord) {
|
|
|
1977
2412
|
if (!newJwk || typeof newJwk.kid !== "string") {
|
|
1978
2413
|
return { error: rpcError(E.PARAMS, "new_jwk must include kid") };
|
|
1979
2414
|
}
|
|
2415
|
+
if (member.keys.some((k) => k.kid === newJwk.kid)) {
|
|
2416
|
+
return { error: rpcError(E.PARAMS, `${p.from} already holds a key ${newJwk.kid}; a new key takes a kid of its own`) };
|
|
2417
|
+
}
|
|
1980
2418
|
const now = coord.now();
|
|
1981
2419
|
oldKey.valid_until = now;
|
|
1982
2420
|
member.keys.push({
|
|
@@ -1992,6 +2430,11 @@ function registerSecuritySigned(coord) {
|
|
|
1992
2430
|
for (const f of ["target_uri", "kid"]) {
|
|
1993
2431
|
if (!(f in p)) return { error: rpcError(E.PARAMS, `Missing field: ${f}`) };
|
|
1994
2432
|
}
|
|
2433
|
+
for (const f of ["target_uri", "kid"]) {
|
|
2434
|
+
if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
|
|
2435
|
+
}
|
|
2436
|
+
const bad = paramsError(p, [["from", "string"], ["reason", "string"]]);
|
|
2437
|
+
if (bad) return bad;
|
|
1995
2438
|
const sender = p.from;
|
|
1996
2439
|
if (sender !== p.target_uri) {
|
|
1997
2440
|
const caller = sender ? ws.members.get(sender) : void 0;
|
|
@@ -2014,6 +2457,7 @@ function registerSecuritySigned(coord) {
|
|
|
2014
2457
|
}
|
|
2015
2458
|
|
|
2016
2459
|
// src/profiles/whisper.ts
|
|
2460
|
+
var RESERVED_IDS3 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2017
2461
|
function parseIso(ts) {
|
|
2018
2462
|
return new Date(ts).getTime();
|
|
2019
2463
|
}
|
|
@@ -2033,11 +2477,25 @@ function registerWhisper(coord) {
|
|
|
2033
2477
|
if (!ws.tasks.has(p.task_id)) {
|
|
2034
2478
|
return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
2035
2479
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2480
|
+
const bad = paramsError(p, [["options", "list"], ["urgency", "string"]]);
|
|
2481
|
+
if (bad) return bad;
|
|
2482
|
+
if (typeof p.deadline_ms !== "number" || !Number.isInteger(p.deadline_ms)) {
|
|
2483
|
+
return { error: rpcError(E.PARAMS, "deadline_ms must be an integer") };
|
|
2038
2484
|
}
|
|
2039
2485
|
const to = p.to;
|
|
2040
2486
|
const askee = Array.isArray(to) ? to : [to];
|
|
2487
|
+
const suppliedWhisperId = p.whisper_id;
|
|
2488
|
+
if (suppliedWhisperId !== void 0 && suppliedWhisperId !== null && suppliedWhisperId !== "") {
|
|
2489
|
+
if (typeof suppliedWhisperId !== "string") {
|
|
2490
|
+
return { error: rpcError(E.PARAMS, "whisper_id must be a string") };
|
|
2491
|
+
}
|
|
2492
|
+
if (RESERVED_IDS3.has(suppliedWhisperId)) {
|
|
2493
|
+
return { error: rpcError(E.PARAMS, `whisper_id cannot be ${JSON.stringify(suppliedWhisperId)}`) };
|
|
2494
|
+
}
|
|
2495
|
+
if (ws.whispers.has(suppliedWhisperId)) {
|
|
2496
|
+
return { error: rpcError(E.PARAMS, `whisper_id already in use: ${suppliedWhisperId}`) };
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2041
2499
|
const id = p.whisper_id || coord.ids.artefactId();
|
|
2042
2500
|
const prompt = {
|
|
2043
2501
|
id,
|
|
@@ -2085,7 +2543,7 @@ function registerWhisper(coord) {
|
|
|
2085
2543
|
if (!hasOption) {
|
|
2086
2544
|
return { error: rpcError(E.PARAMS, "answer_option is required when options are defined") };
|
|
2087
2545
|
}
|
|
2088
|
-
const valid = new Set(prompt.options.map((o) => o.id));
|
|
2546
|
+
const valid = new Set(prompt.options.filter((o) => o !== null && typeof o === "object" && !Array.isArray(o)).map((o) => o.id));
|
|
2089
2547
|
if (!valid.has(answerOption)) {
|
|
2090
2548
|
return { error: rpcError(
|
|
2091
2549
|
E.WHISPER_OPTION_NOT_IN_SET,
|
|
@@ -2103,12 +2561,13 @@ function registerWhisper(coord) {
|
|
|
2103
2561
|
prompt.comment = p.comment;
|
|
2104
2562
|
return { result: { answered: true, whisper_id: prompt.id, task_id: prompt.task_id } };
|
|
2105
2563
|
});
|
|
2106
|
-
coord.checkWhisperLapses = (workspaceId, now) =>
|
|
2564
|
+
coord.checkWhisperLapses = (workspaceId, now) => coord.outsideCall(workspaceId, () => lapse(workspaceId, now));
|
|
2565
|
+
function lapse(workspaceId, now) {
|
|
2107
2566
|
const ws = coord.workspaces.get(workspaceId);
|
|
2108
2567
|
if (!ws) return [];
|
|
2109
2568
|
const cutoff = parseIso(now ?? coord.now());
|
|
2110
2569
|
const emitted = [];
|
|
2111
|
-
for (const prompt of ws.whispers
|
|
2570
|
+
for (const prompt of rawValues(ws.whispers)) {
|
|
2112
2571
|
if (prompt.state !== "pending") continue;
|
|
2113
2572
|
const asked = parseIso(prompt.asked_at);
|
|
2114
2573
|
const deadline = asked + prompt.deadline_ms;
|
|
@@ -2138,7 +2597,7 @@ function registerWhisper(coord) {
|
|
|
2138
2597
|
emitted.push(notify);
|
|
2139
2598
|
}
|
|
2140
2599
|
return emitted;
|
|
2141
|
-
}
|
|
2600
|
+
}
|
|
2142
2601
|
}
|
|
2143
2602
|
|
|
2144
2603
|
// src/coordinator.ts
|
|
@@ -2151,6 +2610,7 @@ var PRIVILEGED_METHODS = /* @__PURE__ */ new Set([
|
|
|
2151
2610
|
"control.rollback",
|
|
2152
2611
|
"control.set_mode_ceiling",
|
|
2153
2612
|
"workspace.set_profiles",
|
|
2613
|
+
"workspace.invite",
|
|
2154
2614
|
"participant.rotate_key",
|
|
2155
2615
|
"participant.revoke_key"
|
|
2156
2616
|
]);
|
|
@@ -2194,12 +2654,9 @@ function missing(params, fields) {
|
|
|
2194
2654
|
for (const f of fields) if (!(f in params)) return f;
|
|
2195
2655
|
return null;
|
|
2196
2656
|
}
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
return { error: rpcError(E.PARAMS, "tags must be a list of strings") };
|
|
2201
|
-
}
|
|
2202
|
-
return null;
|
|
2657
|
+
var MODES = /* @__PURE__ */ new Set(["shadow", "trial", "production"]);
|
|
2658
|
+
function isObject(value) {
|
|
2659
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2203
2660
|
}
|
|
2204
2661
|
function reviewRuleSupported(rule) {
|
|
2205
2662
|
if (rule === "any_one_approves" || rule === "all_approve") return true;
|
|
@@ -2220,8 +2677,214 @@ function reviewSatisfied(review) {
|
|
|
2220
2677
|
if (rule.startsWith("quorum:")) return approvers.size >= parseInt(rule.slice("quorum:".length), 10);
|
|
2221
2678
|
return approvers.size >= 1;
|
|
2222
2679
|
}
|
|
2680
|
+
function advertises(ws, name) {
|
|
2681
|
+
return ws.profiles.some((p) => p === name || p.startsWith(name + "/"));
|
|
2682
|
+
}
|
|
2683
|
+
function knownProfile(id) {
|
|
2684
|
+
const m = /^([a-z][a-z0-9-]*)\/(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(id);
|
|
2685
|
+
if (!m || !Object.prototype.hasOwnProperty.call(PROFILE_VERSIONS, m[1])) return false;
|
|
2686
|
+
return PROFILE_VERSIONS[m[1]].split(".")[0] === m[2];
|
|
2687
|
+
}
|
|
2688
|
+
var LASTING_PROFILES = ["security-signed", "identity-oidc", "identity-vc"];
|
|
2689
|
+
function profilesError(profiles) {
|
|
2690
|
+
const unknown = profiles.find((x) => !knownProfile(x));
|
|
2691
|
+
if (unknown === void 0) return null;
|
|
2692
|
+
return { error: rpcError(
|
|
2693
|
+
E.PARAMS,
|
|
2694
|
+
`Unknown profile ${JSON.stringify(unknown)}: a profile id is <name>/<major>.<minor> for a profile the catalogue lists`
|
|
2695
|
+
) };
|
|
2696
|
+
}
|
|
2697
|
+
var RESERVED_KEYS = ["__proto__", "constructor", "prototype"];
|
|
2698
|
+
var PARTICIPANT_TYPES = ["human", "agent", "service", "group", "workspace"];
|
|
2699
|
+
var ADMIN_TYPES = ["human", "service"];
|
|
2700
|
+
function participantError(type, uri, field) {
|
|
2701
|
+
if (!PARTICIPANT_TYPES.includes(type)) {
|
|
2702
|
+
return { error: rpcError(E.PARAMS, "type must be human, agent, service, group or workspace") };
|
|
2703
|
+
}
|
|
2704
|
+
if (!uri.startsWith(`${type}:`)) {
|
|
2705
|
+
return { error: rpcError(E.PARAMS, `${field} must start with "${type}:" for type ${type}`) };
|
|
2706
|
+
}
|
|
2707
|
+
if (type === "group" || type === "workspace") {
|
|
2708
|
+
return { error: rpcError(E.PARAMS, "group: and workspace: URIs name address scopes, which cannot join") };
|
|
2709
|
+
}
|
|
2710
|
+
return null;
|
|
2711
|
+
}
|
|
2712
|
+
function inForce(key, now) {
|
|
2713
|
+
return !(key.revoked_at !== void 0 && now >= key.revoked_at) && !(key.valid_until !== void 0 && now >= key.valid_until);
|
|
2714
|
+
}
|
|
2715
|
+
function sameKey(a, b) {
|
|
2716
|
+
const material = (k) => jwkThumbprint(k) ?? canonicalize(k).toString("utf-8");
|
|
2717
|
+
return a.kid === b.kid && material(a) === material(b);
|
|
2718
|
+
}
|
|
2719
|
+
function credentialKeys(cnf, nonce, joinKeys) {
|
|
2720
|
+
if (isObject(cnf)) {
|
|
2721
|
+
const kid = typeof cnf.kid === "string" && cnf.kid !== "" ? cnf.kid : jwkThumbprint(cnf);
|
|
2722
|
+
if (kid === void 0) return null;
|
|
2723
|
+
const key = { ...cnf, kid };
|
|
2724
|
+
return joinKeys.every((j) => sameKey(key, j)) ? [key] : null;
|
|
2725
|
+
}
|
|
2726
|
+
if (typeof nonce === "string") return joinKeys.every((j) => jwkThumbprint(j) === nonce) ? joinKeys : null;
|
|
2727
|
+
return void 0;
|
|
2728
|
+
}
|
|
2729
|
+
function rawInvitation(ws, uri) {
|
|
2730
|
+
return Map.prototype.get.call(ws.invitations, uri);
|
|
2731
|
+
}
|
|
2732
|
+
function awaitsFirstMember(ws) {
|
|
2733
|
+
if (ws.members.size > 0) return false;
|
|
2734
|
+
return !ws.audit.some((e) => e.outcome == null && entryCall(e)?.method === "participant.join");
|
|
2735
|
+
}
|
|
2736
|
+
function signingKeys(jwks) {
|
|
2737
|
+
const keys = isObject(jwks) && Array.isArray(jwks.keys) ? jwks.keys : [];
|
|
2738
|
+
return keys.filter((j) => isObject(j) && typeof j.kid === "string" && j.kid !== "");
|
|
2739
|
+
}
|
|
2740
|
+
function recordedForm(envelope) {
|
|
2741
|
+
if (envelope.method !== "participant.join" || !isObject(envelope.params)) return envelope;
|
|
2742
|
+
const params = { ...envelope.params };
|
|
2743
|
+
if (typeof params.oidc_token === "string") params.oidc_token = sha256Hex(params.oidc_token);
|
|
2744
|
+
if (isObject(params.vc_presentation)) params.vc_presentation = contentHash(params.vc_presentation);
|
|
2745
|
+
return { ...envelope, params };
|
|
2746
|
+
}
|
|
2747
|
+
function storeFailed() {
|
|
2748
|
+
return rpcError(E.INTERNAL, "Internal error", { reason: "store" });
|
|
2749
|
+
}
|
|
2750
|
+
function targetOf(envelope) {
|
|
2751
|
+
const params = isObject(envelope) ? envelope.params : void 0;
|
|
2752
|
+
const id = isObject(params) ? params.workspace : void 0;
|
|
2753
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
2754
|
+
}
|
|
2755
|
+
var KEPT_NULLS = { whispers: ["default_if_lapsed"] };
|
|
2756
|
+
function dropNulls(o, keep = []) {
|
|
2757
|
+
for (const key of Object.keys(o)) if (o[key] === null && !keep.includes(key)) delete o[key];
|
|
2758
|
+
}
|
|
2759
|
+
function shapeObject(name, item) {
|
|
2760
|
+
if (name === "route_decisions") {
|
|
2761
|
+
const extra = item.extra;
|
|
2762
|
+
delete item.extra;
|
|
2763
|
+
if (isObject(extra)) Object.assign(item, extra);
|
|
2764
|
+
item.kind = "route_decision";
|
|
2765
|
+
}
|
|
2766
|
+
dropNulls(item, KEPT_NULLS[name]);
|
|
2767
|
+
const each = (o, field, f) => {
|
|
2768
|
+
if (!Array.isArray(o[field])) return;
|
|
2769
|
+
for (const x of o[field]) {
|
|
2770
|
+
if (isObject(x)) {
|
|
2771
|
+
dropNulls(x);
|
|
2772
|
+
f?.(x);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
if (name === "members") each(item, "keys");
|
|
2777
|
+
if (name === "tasks") {
|
|
2778
|
+
each(item, "history", (h) => {
|
|
2779
|
+
if (h.from === void 0 && typeof h.from_ === "string") h.from = h.from_;
|
|
2780
|
+
delete h.from_;
|
|
2781
|
+
});
|
|
2782
|
+
if (isObject(item.review)) {
|
|
2783
|
+
dropNulls(item.review);
|
|
2784
|
+
each(item.review, "decisions");
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
if (name === "deliberations") each(item, "votes");
|
|
2788
|
+
if (name === "handoffs") {
|
|
2789
|
+
each(item, "tasks");
|
|
2790
|
+
if (Array.isArray(item.accepted_task_ids) && item.accepted_task_ids.length === 0) delete item.accepted_task_ids;
|
|
2791
|
+
}
|
|
2792
|
+
return item;
|
|
2793
|
+
}
|
|
2794
|
+
function storedCopy(name, item) {
|
|
2795
|
+
return shapeObject(name, structuredClone(item));
|
|
2796
|
+
}
|
|
2797
|
+
function shapeSnapshot(w) {
|
|
2798
|
+
const out = { ...w };
|
|
2799
|
+
dropNulls(out);
|
|
2800
|
+
for (const name of COLLECTIONS) {
|
|
2801
|
+
if (Array.isArray(out[name])) {
|
|
2802
|
+
out[name] = out[name].map((x) => isObject(x) ? storedCopy(name, x) : x);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
return out;
|
|
2806
|
+
}
|
|
2807
|
+
function reviewRequired(ws, task) {
|
|
2808
|
+
return task.review_required === true || task.mode === "trial" && advertises(ws, "modes");
|
|
2809
|
+
}
|
|
2810
|
+
function rawMember(ws, uri) {
|
|
2811
|
+
return typeof uri === "string" ? Map.prototype.get.call(ws.members, uri) : void 0;
|
|
2812
|
+
}
|
|
2813
|
+
function unionOf(...lists) {
|
|
2814
|
+
const out = [];
|
|
2815
|
+
for (const list of lists) for (const uri of list) if (!out.includes(uri)) out.push(uri);
|
|
2816
|
+
return out;
|
|
2817
|
+
}
|
|
2818
|
+
function authorsOf(task, completer) {
|
|
2819
|
+
const authors = [task.assignee];
|
|
2820
|
+
if (typeof completer === "string" && completer !== task.assignee) authors.push(completer);
|
|
2821
|
+
return unionOf(authors, Array.isArray(task.prior_authors) ? task.prior_authors : []);
|
|
2822
|
+
}
|
|
2823
|
+
function stateBefore(history, i) {
|
|
2824
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2825
|
+
if (history[j].state !== "paused") return history[j].state;
|
|
2826
|
+
}
|
|
2827
|
+
return void 0;
|
|
2828
|
+
}
|
|
2829
|
+
function workCompletion(history, i) {
|
|
2830
|
+
if (history[i].state !== "completed") return false;
|
|
2831
|
+
const before = stateBefore(history, i);
|
|
2832
|
+
return before !== "review_requested" && before !== "completed";
|
|
2833
|
+
}
|
|
2834
|
+
function lastCompleter(task) {
|
|
2835
|
+
for (let i = task.history.length - 1; i >= 0; i--) {
|
|
2836
|
+
if (workCompletion(task.history, i)) return task.history[i].from;
|
|
2837
|
+
}
|
|
2838
|
+
return void 0;
|
|
2839
|
+
}
|
|
2840
|
+
var OPENED_ON_COMPLETE = "review required; opened on task.complete";
|
|
2841
|
+
var NEW_ROUND_ON_COMPLETE = "a new round of the review; opened on task.complete";
|
|
2842
|
+
function storedAuthors(task) {
|
|
2843
|
+
let requester;
|
|
2844
|
+
const withRequester = (authors) => unionOf(authors, requester !== void 0 ? [requester] : []);
|
|
2845
|
+
for (let i = task.history.length - 1; i >= 0; i--) {
|
|
2846
|
+
const h = task.history[i];
|
|
2847
|
+
const opens = h.state === "review_requested" && stateBefore(task.history, i) !== "review_requested";
|
|
2848
|
+
if (opens && (h.note === OPENED_ON_COMPLETE || h.note === NEW_ROUND_ON_COMPLETE)) return withRequester(authorsOf(task, h.from));
|
|
2849
|
+
if (opens && requester === void 0) requester = h.from;
|
|
2850
|
+
if (workCompletion(task.history, i)) return withRequester(authorsOf(task, h.from));
|
|
2851
|
+
}
|
|
2852
|
+
return withRequester(authorsOf(task, void 0));
|
|
2853
|
+
}
|
|
2854
|
+
var ENDED = ["completed", "declined", "cancelled", "superseded", "escalated"];
|
|
2855
|
+
function reviewHolds(ws, task) {
|
|
2856
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2857
|
+
for (let t = task; t && !seen.has(t.id); t = typeof t.supersedes === "string" ? ws.tasks.get(t.supersedes) : void 0) {
|
|
2858
|
+
seen.add(t.id);
|
|
2859
|
+
if (!reviewRequired(ws, t)) return [];
|
|
2860
|
+
if (t.review) {
|
|
2861
|
+
const decided = t.review.decisions.some((d) => d.kind === "reject" || d.kind === "override") || reviewSatisfied(t.review);
|
|
2862
|
+
return decided ? [] : t.review.authors;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return [];
|
|
2866
|
+
}
|
|
2867
|
+
function openRequiredReviewAuthors(ws, task) {
|
|
2868
|
+
return ENDED.includes(task.state) ? [] : reviewHolds(ws, task);
|
|
2869
|
+
}
|
|
2870
|
+
function heldBy(task, mover) {
|
|
2871
|
+
return mover === task.assignee ? [task.assignee] : [];
|
|
2872
|
+
}
|
|
2873
|
+
function successorAuthors(old, mover) {
|
|
2874
|
+
const completer = lastCompleter(old);
|
|
2875
|
+
return unionOf(
|
|
2876
|
+
old.review ? old.review.authors : [],
|
|
2877
|
+
heldBy(old, mover),
|
|
2878
|
+
completer !== void 0 ? [completer] : [],
|
|
2879
|
+
Array.isArray(old.prior_authors) ? old.prior_authors : []
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
function isScope(uri) {
|
|
2883
|
+
return typeof uri === "string" && (uri.startsWith("workspace:") || uri.startsWith("group:"));
|
|
2884
|
+
}
|
|
2223
2885
|
function reply(env, body) {
|
|
2224
|
-
const
|
|
2886
|
+
const sent = env !== null && typeof env === "object" && !Array.isArray(env) ? env.id : void 0;
|
|
2887
|
+
const out = { jsonrpc: "2.0", id: sent ?? null };
|
|
2225
2888
|
if (body.error !== void 0) out.error = body.error;
|
|
2226
2889
|
else out.result = body.result;
|
|
2227
2890
|
return out;
|
|
@@ -2236,7 +2899,7 @@ function indexSigned(index, entry) {
|
|
|
2236
2899
|
return;
|
|
2237
2900
|
}
|
|
2238
2901
|
if (entry.outcome != null) index.refused.set(digest, { seq: entry.seq, code: entry.outcome.code });
|
|
2239
|
-
else index.accepted.
|
|
2902
|
+
else if (!index.accepted.has(digest)) index.accepted.set(digest, entry.seq);
|
|
2240
2903
|
}
|
|
2241
2904
|
function isThenable(value) {
|
|
2242
2905
|
return typeof value?.then === "function";
|
|
@@ -2264,13 +2927,21 @@ var Coordinator = class {
|
|
|
2264
2927
|
unflushed = /* @__PURE__ */ new Set();
|
|
2265
2928
|
/** Workspaces with a write queued behind an async write in flight. */
|
|
2266
2929
|
flushQueued = /* @__PURE__ */ new Set();
|
|
2930
|
+
/**
|
|
2931
|
+
* Workspaces whose store copy may differ from memory after a synchronous
|
|
2932
|
+
* write failed or conflicted. Each is reloaded from the store before the
|
|
2933
|
+
* next call on it, which is refused while the reload fails.
|
|
2934
|
+
*/
|
|
2935
|
+
held = /* @__PURE__ */ new Set();
|
|
2936
|
+
/** Entries appended and not yet written, which the audit listeners hear of once they are. */
|
|
2937
|
+
unannounced = /* @__PURE__ */ new WeakMap();
|
|
2267
2938
|
clockMs;
|
|
2268
2939
|
frozenNow;
|
|
2269
2940
|
auditListeners = [];
|
|
2270
2941
|
/**
|
|
2271
2942
|
* The signed calls on each workspace's log, by what their senders signed:
|
|
2272
|
-
* recorded refusals with their seq and code, and accepted calls
|
|
2273
|
-
* from the log when missing, so a snapshot never carries it.
|
|
2943
|
+
* recorded refusals with their seq and code, and accepted calls with their
|
|
2944
|
+
* seq. Rebuilt from the log when missing, so a snapshot never carries it.
|
|
2274
2945
|
*/
|
|
2275
2946
|
signedIndexes = /* @__PURE__ */ new WeakMap();
|
|
2276
2947
|
/** Method handler registry; profiles plug into this. */
|
|
@@ -2406,6 +3077,7 @@ var Coordinator = class {
|
|
|
2406
3077
|
change = this.takeChange(ws, p);
|
|
2407
3078
|
} catch (err) {
|
|
2408
3079
|
p.replace = true;
|
|
3080
|
+
if (this.options.storeErrors !== "log") this.held.add(ws.id);
|
|
2409
3081
|
this.reportStoreError(err, { id: ws.id, version: ws.audit.length });
|
|
2410
3082
|
return;
|
|
2411
3083
|
}
|
|
@@ -2414,6 +3086,7 @@ var Coordinator = class {
|
|
|
2414
3086
|
const done = () => {
|
|
2415
3087
|
p.seq = taken.version;
|
|
2416
3088
|
if (taken.replace) p.replace = false;
|
|
3089
|
+
this.announce(ws, taken.version);
|
|
2417
3090
|
};
|
|
2418
3091
|
const failed = (err) => {
|
|
2419
3092
|
this.giveBack(ws, p, taken);
|
|
@@ -2423,13 +3096,34 @@ var Coordinator = class {
|
|
|
2423
3096
|
try {
|
|
2424
3097
|
result = this.store.apply(taken);
|
|
2425
3098
|
} catch (err) {
|
|
3099
|
+
if (err instanceof StoreConflictError || this.options.storeErrors !== "log") this.held.add(ws.id);
|
|
2426
3100
|
failed(err);
|
|
2427
3101
|
return;
|
|
2428
3102
|
}
|
|
2429
3103
|
if (isThenable(result)) return Promise.resolve(result).then(done, failed);
|
|
2430
3104
|
done();
|
|
2431
3105
|
}
|
|
2432
|
-
/**
|
|
3106
|
+
/** Tell the audit listeners of the entries now written, up to `upTo`. */
|
|
3107
|
+
announce(ws, upTo) {
|
|
3108
|
+
const queued = this.unannounced.get(ws);
|
|
3109
|
+
if (!queued?.length) return;
|
|
3110
|
+
const now = queued.filter((e) => e.seq < upTo);
|
|
3111
|
+
this.unannounced.set(ws, queued.filter((e) => e.seq >= upTo));
|
|
3112
|
+
for (const entry of now) {
|
|
3113
|
+
for (const l of this.auditListeners) {
|
|
3114
|
+
try {
|
|
3115
|
+
l(ws, entry);
|
|
3116
|
+
} catch {
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* The change since the last write, taking the recorded keys with it. Each
|
|
3123
|
+
* object is written as a copy in the shape a restore gives back, so a
|
|
3124
|
+
* coordinator restarted on the store holds what this one holds, and the
|
|
3125
|
+
* workspace's own objects are left as they are.
|
|
3126
|
+
*/
|
|
2433
3127
|
takeChange(ws, p) {
|
|
2434
3128
|
const replace = p.replace;
|
|
2435
3129
|
const objects = {};
|
|
@@ -2438,12 +3132,15 @@ var Coordinator = class {
|
|
|
2438
3132
|
if (replace) {
|
|
2439
3133
|
const all = {};
|
|
2440
3134
|
for (const [key, value] of Map.prototype.entries.call(map)) {
|
|
2441
|
-
all[key] = value;
|
|
3135
|
+
all[key] = storedCopy(name, value);
|
|
2442
3136
|
}
|
|
2443
3137
|
objects[name] = all;
|
|
2444
3138
|
} else if (map.dirty.size) {
|
|
2445
3139
|
const changed = {};
|
|
2446
|
-
for (const key of map.dirty)
|
|
3140
|
+
for (const key of map.dirty) {
|
|
3141
|
+
const value = map.peek(key);
|
|
3142
|
+
changed[key] = value === void 0 ? null : storedCopy(name, value);
|
|
3143
|
+
}
|
|
2447
3144
|
objects[name] = changed;
|
|
2448
3145
|
}
|
|
2449
3146
|
map.dirty.clear();
|
|
@@ -2495,24 +3192,28 @@ var Coordinator = class {
|
|
|
2495
3192
|
this.reportStoreError(err, { id: ws.id, version: next });
|
|
2496
3193
|
return;
|
|
2497
3194
|
}
|
|
3195
|
+
const upTo = ws.audit.length;
|
|
2498
3196
|
const pending = this.saveChains.get(ws.id);
|
|
2499
3197
|
if (pending) {
|
|
2500
|
-
this.trackSave(pending.then(() => this.store.save(record)), record);
|
|
3198
|
+
this.trackSave(pending.then(() => this.store.save(record)), record, () => this.announce(ws, upTo));
|
|
2501
3199
|
return;
|
|
2502
3200
|
}
|
|
2503
3201
|
let result;
|
|
2504
3202
|
try {
|
|
2505
3203
|
result = this.store.save(record);
|
|
2506
3204
|
} catch (err) {
|
|
3205
|
+
if (this.options.storeErrors !== "log") this.held.add(ws.id);
|
|
2507
3206
|
this.reportStoreError(err, record);
|
|
2508
3207
|
return;
|
|
2509
3208
|
}
|
|
2510
3209
|
if (isThenable(result)) {
|
|
2511
|
-
this.trackSave(Promise.resolve(result), record);
|
|
3210
|
+
this.trackSave(Promise.resolve(result), record, () => this.announce(ws, upTo));
|
|
3211
|
+
return;
|
|
2512
3212
|
}
|
|
3213
|
+
this.announce(ws, upTo);
|
|
2513
3214
|
}
|
|
2514
|
-
trackSave(save, record) {
|
|
2515
|
-
const guarded = save.
|
|
3215
|
+
trackSave(save, record, saved) {
|
|
3216
|
+
const guarded = save.then(saved, (err) => this.reportStoreError(err, record));
|
|
2516
3217
|
this.saveChains.set(record.id, guarded);
|
|
2517
3218
|
void guarded.then(() => {
|
|
2518
3219
|
if (this.saveChains.get(record.id) === guarded) {
|
|
@@ -2520,6 +3221,17 @@ var Coordinator = class {
|
|
|
2520
3221
|
}
|
|
2521
3222
|
});
|
|
2522
3223
|
}
|
|
3224
|
+
reportInternalError(err, method, workspace) {
|
|
3225
|
+
const call2 = { method, ...workspace !== void 0 ? { workspace } : {} };
|
|
3226
|
+
if (this.options.onInternalError) {
|
|
3227
|
+
try {
|
|
3228
|
+
this.options.onInternalError(err, call2);
|
|
3229
|
+
} catch {
|
|
3230
|
+
}
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
console.error(`chap coordinator: ${method} failed with an internal error:`, err);
|
|
3234
|
+
}
|
|
2523
3235
|
reportStoreError(err, record) {
|
|
2524
3236
|
if (this.options.onStoreError) {
|
|
2525
3237
|
try {
|
|
@@ -2529,7 +3241,9 @@ var Coordinator = class {
|
|
|
2529
3241
|
return;
|
|
2530
3242
|
}
|
|
2531
3243
|
const message = err instanceof Error ? err.message : String(err);
|
|
2532
|
-
|
|
3244
|
+
const line = `chap coordinator: writing workspace ${record.id} to the store failed: ${message}`;
|
|
3245
|
+
if (err instanceof StoreConflictError) console.warn(line);
|
|
3246
|
+
else console.error(line);
|
|
2533
3247
|
}
|
|
2534
3248
|
async drainSaves() {
|
|
2535
3249
|
while (this.saveChains.size) {
|
|
@@ -2580,16 +3294,25 @@ var Coordinator = class {
|
|
|
2580
3294
|
return null;
|
|
2581
3295
|
}
|
|
2582
3296
|
/**
|
|
2583
|
-
* Assert that `sender`
|
|
2584
|
-
* review-decision methods (decide.* and abstain.declare)
|
|
2585
|
-
* review
|
|
2586
|
-
*
|
|
2587
|
-
*
|
|
2588
|
-
*
|
|
3297
|
+
* Assert that `sender` may decide the task's open review. Applied to
|
|
3298
|
+
* review-decision methods (decide.* and abstain.declare) and to
|
|
3299
|
+
* review.acknowledge: to act on a review you must not be an author of the
|
|
3300
|
+
* work under review, must be a human member where the task requires
|
|
3301
|
+
* review, and must be one of the reviewers it was addressed to (the `to`
|
|
3302
|
+
* set on review.request).
|
|
3303
|
+
* Membership is the floor; this is the eligibility ceiling for decisions.
|
|
3304
|
+
* Returns an error fragment if not eligible, else null.
|
|
2589
3305
|
*/
|
|
2590
|
-
requireReviewer(task, sender) {
|
|
3306
|
+
requireReviewer(ws, task, sender) {
|
|
2591
3307
|
const review = task.review;
|
|
2592
|
-
if (!review
|
|
3308
|
+
if (!review) return null;
|
|
3309
|
+
if (typeof sender === "string" && review.authors.includes(sender)) {
|
|
3310
|
+
return { error: rpcError(E.NOT_AUTHORISED, "The author of the work under review cannot decide it") };
|
|
3311
|
+
}
|
|
3312
|
+
if (reviewRequired(ws, task) && rawMember(ws, sender)?.type !== "human") {
|
|
3313
|
+
return { error: rpcError(E.NOT_AUTHORISED, `This task requires a person's review: ${String(sender)} is not a human member`) };
|
|
3314
|
+
}
|
|
3315
|
+
if (!review.requested_to || review.requested_to.length === 0) {
|
|
2593
3316
|
return null;
|
|
2594
3317
|
}
|
|
2595
3318
|
if (review.requested_to.some((r) => typeof r === "string" && (r.startsWith("workspace:") || r.startsWith("group:")))) {
|
|
@@ -2618,23 +3341,31 @@ var Coordinator = class {
|
|
|
2618
3341
|
routing_policy_uri: ws.routing_policy_uri,
|
|
2619
3342
|
step_up_window_sec: ws.step_up_window_sec,
|
|
2620
3343
|
min_acr: ws.min_acr,
|
|
3344
|
+
signature_policy: ws.signature_policy,
|
|
3345
|
+
join_policy: ws.join_policy,
|
|
3346
|
+
review_rule: ws.review_rule,
|
|
2621
3347
|
chain_head: ws.chain_head,
|
|
2622
3348
|
chain_enabled: ws.chain_enabled
|
|
2623
3349
|
};
|
|
2624
3350
|
}
|
|
2625
|
-
/**
|
|
3351
|
+
/**
|
|
3352
|
+
* One workspace as `snapshot()` holds it: each object of its collections a
|
|
3353
|
+
* copy in the shape the store keeps. Reads nothing as a change.
|
|
3354
|
+
*/
|
|
2626
3355
|
snapshotOf(ws) {
|
|
2627
3356
|
const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
|
|
3357
|
+
const stored = (name) => Array.from(rawValues(ws[name])).map((v) => storedCopy(name, v));
|
|
2628
3358
|
return {
|
|
2629
3359
|
...own,
|
|
2630
|
-
members:
|
|
2631
|
-
tasks:
|
|
2632
|
-
overrides:
|
|
2633
|
-
whispers:
|
|
2634
|
-
deliberations:
|
|
2635
|
-
handoffs:
|
|
2636
|
-
snapshots:
|
|
2637
|
-
route_decisions:
|
|
3360
|
+
members: stored("members"),
|
|
3361
|
+
tasks: stored("tasks"),
|
|
3362
|
+
overrides: stored("overrides"),
|
|
3363
|
+
whispers: stored("whispers"),
|
|
3364
|
+
deliberations: stored("deliberations"),
|
|
3365
|
+
handoffs: stored("handoffs"),
|
|
3366
|
+
snapshots: stored("snapshots"),
|
|
3367
|
+
route_decisions: stored("route_decisions"),
|
|
3368
|
+
invitations: stored("invitations"),
|
|
2638
3369
|
idempotency_keys: { ...ws.idempotency_keys },
|
|
2639
3370
|
audit: ws.audit,
|
|
2640
3371
|
chain_head,
|
|
@@ -2662,7 +3393,8 @@ var Coordinator = class {
|
|
|
2662
3393
|
}
|
|
2663
3394
|
}
|
|
2664
3395
|
/** One workspace from a snapshot entry, its maps tracked. */
|
|
2665
|
-
rebuild(
|
|
3396
|
+
rebuild(snapshot) {
|
|
3397
|
+
const w = shapeSnapshot(snapshot);
|
|
2666
3398
|
const ws = {
|
|
2667
3399
|
id: w.id,
|
|
2668
3400
|
created: w.created,
|
|
@@ -2673,6 +3405,11 @@ var Coordinator = class {
|
|
|
2673
3405
|
routing_policy_uri: w.routing_policy_uri,
|
|
2674
3406
|
step_up_window_sec: w.step_up_window_sec ?? 300,
|
|
2675
3407
|
min_acr: w.min_acr,
|
|
3408
|
+
// A workspace written before the policy existed verified every call,
|
|
3409
|
+
// and one written before the join policy existed admitted anyone.
|
|
3410
|
+
signature_policy: w.signature_policy === "humans" ? "humans" : "all",
|
|
3411
|
+
join_policy: w.join_policy === "invite" ? "invite" : "open",
|
|
3412
|
+
review_rule: typeof w.review_rule === "string" && reviewRuleSupported(w.review_rule) ? w.review_rule : void 0,
|
|
2676
3413
|
members: this.newMap(),
|
|
2677
3414
|
tasks: this.newMap(),
|
|
2678
3415
|
overrides: this.newMap(),
|
|
@@ -2681,6 +3418,7 @@ var Coordinator = class {
|
|
|
2681
3418
|
handoffs: this.newMap(),
|
|
2682
3419
|
snapshots: this.newMap(),
|
|
2683
3420
|
route_decisions: this.newMap(),
|
|
3421
|
+
invitations: this.newMap(),
|
|
2684
3422
|
idempotency_keys: w.idempotency_keys ?? {},
|
|
2685
3423
|
audit: w.audit ?? [],
|
|
2686
3424
|
chain_head: w.chain_head,
|
|
@@ -2689,9 +3427,16 @@ var Coordinator = class {
|
|
|
2689
3427
|
for (const m of w.members ?? []) {
|
|
2690
3428
|
if (!Array.isArray(m.keys)) m.keys = [];
|
|
2691
3429
|
if (m.paused === void 0) m.paused = false;
|
|
3430
|
+
const scheme = typeof m.uri === "string" ? m.uri.split(":")[0] : "";
|
|
3431
|
+
if (PARTICIPANT_TYPES.includes(scheme)) m.type = scheme;
|
|
3432
|
+
if (m.role === "admin" && !ADMIN_TYPES.includes(m.type)) m.role = "participant";
|
|
2692
3433
|
ws.members.set(m.uri, m);
|
|
2693
3434
|
}
|
|
2694
|
-
for (const t of w.tasks ?? [])
|
|
3435
|
+
for (const t of w.tasks ?? []) {
|
|
3436
|
+
if (t.review && !Array.isArray(t.review.authors)) t.review.authors = storedAuthors(t);
|
|
3437
|
+
if (t.review && !Array.isArray(t.review.acknowledgements)) t.review.acknowledgements = [];
|
|
3438
|
+
ws.tasks.set(t.id, t);
|
|
3439
|
+
}
|
|
2695
3440
|
for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
|
|
2696
3441
|
for (const x of w.whispers ?? [])
|
|
2697
3442
|
ws.whispers.set(x.id, x);
|
|
@@ -2735,20 +3480,119 @@ var Coordinator = class {
|
|
|
2735
3480
|
}
|
|
2736
3481
|
for (const x of w.route_decisions ?? [])
|
|
2737
3482
|
ws.route_decisions.set(x.id, x);
|
|
3483
|
+
for (const x of w.invitations ?? []) ws.invitations.set(x.uri, x);
|
|
2738
3484
|
return ws;
|
|
2739
3485
|
}
|
|
2740
3486
|
// -- dispatch ------------------------------------------------------
|
|
2741
3487
|
dispatch(envelope) {
|
|
2742
3488
|
this.frozenNow = this.advanceClock();
|
|
2743
3489
|
this.tracker.depth++;
|
|
3490
|
+
let response;
|
|
3491
|
+
let target;
|
|
3492
|
+
let ran = false;
|
|
2744
3493
|
try {
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
3494
|
+
target = targetOf(envelope);
|
|
3495
|
+
const refused = target === void 0 ? null : this.keepUp(target);
|
|
3496
|
+
if (refused) {
|
|
3497
|
+
response = reply(envelope, { error: refused });
|
|
3498
|
+
} else {
|
|
3499
|
+
ran = true;
|
|
3500
|
+
response = this._dispatch(envelope);
|
|
3501
|
+
if (response.error) this.recordRefusal(envelope, response.error);
|
|
3502
|
+
else if (target === void 0 && envelope.method === "workspace.create") {
|
|
3503
|
+
target = response.result?.workspace;
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
2748
3506
|
} finally {
|
|
2749
3507
|
if (--this.tracker.depth === 0) this.flushUnflushed();
|
|
2750
3508
|
this.frozenNow = void 0;
|
|
2751
3509
|
}
|
|
3510
|
+
if (ran && target !== void 0 && this.tracker.depth === 0 && this.held.has(target)) {
|
|
3511
|
+
this.reload(target);
|
|
3512
|
+
return reply(envelope, { error: storeFailed() });
|
|
3513
|
+
}
|
|
3514
|
+
return response;
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Work that changes a workspace outside any call, a whisper lapse check
|
|
3518
|
+
* for one, kept to the rules a call keeps (SPECIFICATION 10.3): the
|
|
3519
|
+
* workspace is first brought up to date with the store, and left alone
|
|
3520
|
+
* as long as it is held; what the work changed is written once, when it ends;
|
|
3521
|
+
* and where that write fails, the workspace goes back to what the store
|
|
3522
|
+
* holds and the work's results are dropped, since none took effect.
|
|
3523
|
+
*/
|
|
3524
|
+
outsideCall(id, work) {
|
|
3525
|
+
if (this.tracker.depth > 0) return work();
|
|
3526
|
+
if (this.keepUp(id)) return [];
|
|
3527
|
+
this.tracker.depth++;
|
|
3528
|
+
let out;
|
|
3529
|
+
try {
|
|
3530
|
+
out = work();
|
|
3531
|
+
} finally {
|
|
3532
|
+
if (--this.tracker.depth === 0) this.flushUnflushed();
|
|
3533
|
+
}
|
|
3534
|
+
if (this.held.has(id)) {
|
|
3535
|
+
this.reload(id);
|
|
3536
|
+
return [];
|
|
3537
|
+
}
|
|
3538
|
+
return out;
|
|
3539
|
+
}
|
|
3540
|
+
/**
|
|
3541
|
+
* Before a call on a workspace: where the store holds a different number of
|
|
3542
|
+
* audit entries for it than this instance last wrote, another writer has
|
|
3543
|
+
* written it, and it is reloaded. A workspace held after a failed write is
|
|
3544
|
+
* reloaded as well, and the call refused while that fails.
|
|
3545
|
+
*/
|
|
3546
|
+
keepUp(id) {
|
|
3547
|
+
if (this.held.has(id)) return this.reload(id) ? null : storeFailed();
|
|
3548
|
+
if (typeof this.store.version !== "function" || this.saveChains.has(id)) return null;
|
|
3549
|
+
let stored;
|
|
3550
|
+
try {
|
|
3551
|
+
stored = this.store.version(id);
|
|
3552
|
+
} catch {
|
|
3553
|
+
return null;
|
|
3554
|
+
}
|
|
3555
|
+
if (isThenable(stored)) return null;
|
|
3556
|
+
const ws = this.workspaces.get(id);
|
|
3557
|
+
const p = ws ? this.persisted.get(ws) : void 0;
|
|
3558
|
+
if (ws && (!p || p.replace)) return null;
|
|
3559
|
+
if (stored === p?.seq) return null;
|
|
3560
|
+
return this.reload(id) ? null : storeFailed();
|
|
3561
|
+
}
|
|
3562
|
+
/** Replace a workspace with what the store holds of it. False when it cannot be read. */
|
|
3563
|
+
reload(id) {
|
|
3564
|
+
let record;
|
|
3565
|
+
try {
|
|
3566
|
+
if (typeof this.store.loadOne === "function") {
|
|
3567
|
+
record = this.store.loadOne(id);
|
|
3568
|
+
} else {
|
|
3569
|
+
const all = this.store.load();
|
|
3570
|
+
if (isThenable(all)) return false;
|
|
3571
|
+
record = all.find((r) => r.id === id);
|
|
3572
|
+
}
|
|
3573
|
+
if (isThenable(record)) return false;
|
|
3574
|
+
} catch (err) {
|
|
3575
|
+
this.reportStoreError(err, { id, version: this.workspaces.get(id)?.audit.length ?? 0 });
|
|
3576
|
+
return false;
|
|
3577
|
+
}
|
|
3578
|
+
const old = this.workspaces.get(id);
|
|
3579
|
+
if (old) this.unflushed.delete(old);
|
|
3580
|
+
if (record) {
|
|
3581
|
+
let ws;
|
|
3582
|
+
try {
|
|
3583
|
+
ws = this.rebuild(record.data);
|
|
3584
|
+
} catch (err) {
|
|
3585
|
+
this.reportStoreError(err, { id, version: record.version });
|
|
3586
|
+
return false;
|
|
3587
|
+
}
|
|
3588
|
+
this.workspaces.set(id, ws);
|
|
3589
|
+
this.markWritten(ws);
|
|
3590
|
+
this.wsVersions.set(id, record.version);
|
|
3591
|
+
} else {
|
|
3592
|
+
this.workspaces.delete(id);
|
|
3593
|
+
}
|
|
3594
|
+
this.held.delete(id);
|
|
3595
|
+
return true;
|
|
2752
3596
|
}
|
|
2753
3597
|
_dispatch(envelope) {
|
|
2754
3598
|
if (!isValidEnvelope(envelope) || typeof envelope.method !== "string" || !envelope.method) {
|
|
@@ -2785,11 +3629,16 @@ var Coordinator = class {
|
|
|
2785
3629
|
const msg = canonicalError instanceof Error ? canonicalError.message : String(canonicalError);
|
|
2786
3630
|
return reply(envelope, { error: rpcError(E.PARAMS, msg) });
|
|
2787
3631
|
}
|
|
3632
|
+
if (params.workspace !== void 0 && (typeof params.workspace !== "string" || !params.workspace)) {
|
|
3633
|
+
return reply(envelope, { error: rpcError(E.PARAMS, "workspace must be a non-empty string") });
|
|
3634
|
+
}
|
|
2788
3635
|
if (isSigned(envelope)) {
|
|
2789
3636
|
const wsId2 = params.workspace;
|
|
2790
3637
|
const ws = typeof wsId2 === "string" ? this.workspaces.get(wsId2) : void 0;
|
|
2791
3638
|
if (ws) {
|
|
2792
|
-
const
|
|
3639
|
+
const index = this.signedIndex(ws);
|
|
3640
|
+
const digest = signedDigest(recordedForm(envelope));
|
|
3641
|
+
const prior = index.refused.get(digest);
|
|
2793
3642
|
if (prior) {
|
|
2794
3643
|
return reply(envelope, { error: rpcError(
|
|
2795
3644
|
prior.code,
|
|
@@ -2797,15 +3646,24 @@ var Coordinator = class {
|
|
|
2797
3646
|
{ refused_at_seq: prior.seq }
|
|
2798
3647
|
) });
|
|
2799
3648
|
}
|
|
3649
|
+
const accepted = index.accepted.get(digest);
|
|
3650
|
+
if (accepted !== void 0) {
|
|
3651
|
+
return reply(envelope, { error: rpcError(
|
|
3652
|
+
E.SIG_ALREADY_ACCEPTED,
|
|
3653
|
+
`Accepted at seq ${accepted}; a signed request takes effect once`,
|
|
3654
|
+
{ accepted_at_seq: accepted }
|
|
3655
|
+
) });
|
|
3656
|
+
}
|
|
2800
3657
|
}
|
|
2801
3658
|
}
|
|
2802
|
-
|
|
3659
|
+
const target = typeof params.workspace === "string" ? this.workspaces.get(params.workspace) : void 0;
|
|
3660
|
+
if (this.requiresSignatures(target) && method !== "participant.join" && method !== "workspace.create" && !(!isSigned(envelope) && target !== void 0 && !this.mustSign(target, rawMember(target, params.from)?.type))) {
|
|
2803
3661
|
const sigErr = this.verifySignature(envelope);
|
|
2804
3662
|
if (sigErr) return reply(envelope, { error: sigErr });
|
|
2805
3663
|
if (method === "participant.rotate_key") {
|
|
2806
3664
|
const signingKid = envelope.sig.split(":")[1];
|
|
2807
3665
|
const oldKid = params.old_kid;
|
|
2808
|
-
if (oldKid
|
|
3666
|
+
if (oldKid != null && signingKid !== oldKid) {
|
|
2809
3667
|
return reply(envelope, { error: rpcError(
|
|
2810
3668
|
E.SIG_ROTATION_KEY_MISMATCH,
|
|
2811
3669
|
"Rotation must be signed with the old key"
|
|
@@ -2860,14 +3718,14 @@ var Coordinator = class {
|
|
|
2860
3718
|
try {
|
|
2861
3719
|
out = handler(ownCopy(params));
|
|
2862
3720
|
} catch (e) {
|
|
2863
|
-
|
|
2864
|
-
return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error"
|
|
3721
|
+
this.reportInternalError(e, method, params.workspace);
|
|
3722
|
+
return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error") });
|
|
2865
3723
|
}
|
|
2866
3724
|
if (out.error) return reply(envelope, { error: out.error });
|
|
2867
|
-
const wsId = params.workspace;
|
|
3725
|
+
const wsId = method === "workspace.create" ? out.result.workspace : params.workspace;
|
|
2868
3726
|
if (typeof wsId === "string" && !READ_ONLY_METHODS.has(method)) {
|
|
2869
3727
|
const ws = this.workspaces.get(wsId);
|
|
2870
|
-
if (ws) this.recordAudit(ws, envelope);
|
|
3728
|
+
if (ws) this.recordAudit(ws, recordedForm(envelope));
|
|
2871
3729
|
}
|
|
2872
3730
|
return reply(envelope, { result: ownCopy(out.result) });
|
|
2873
3731
|
}
|
|
@@ -2907,7 +3765,7 @@ var Coordinator = class {
|
|
|
2907
3765
|
signedIndex(ws) {
|
|
2908
3766
|
let index = this.signedIndexes.get(ws);
|
|
2909
3767
|
if (!index) {
|
|
2910
|
-
index = { refused: /* @__PURE__ */ new Map(), accepted: /* @__PURE__ */ new
|
|
3768
|
+
index = { refused: /* @__PURE__ */ new Map(), accepted: /* @__PURE__ */ new Map() };
|
|
2911
3769
|
for (const entry of ws.audit) indexSigned(index, entry);
|
|
2912
3770
|
this.signedIndexes.set(ws, index);
|
|
2913
3771
|
}
|
|
@@ -2923,12 +3781,9 @@ var Coordinator = class {
|
|
|
2923
3781
|
ws.audit.push(entry);
|
|
2924
3782
|
const index = this.signedIndexes.get(ws);
|
|
2925
3783
|
if (index) indexSigned(index, entry);
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
} catch {
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
3784
|
+
const queued = this.unannounced.get(ws);
|
|
3785
|
+
if (queued) queued.push(entry);
|
|
3786
|
+
else this.unannounced.set(ws, [entry]);
|
|
2932
3787
|
this.persist(ws);
|
|
2933
3788
|
}
|
|
2934
3789
|
// -- signature verification (security-signed/1.0) -----------------
|
|
@@ -2938,7 +3793,12 @@ var Coordinator = class {
|
|
|
2938
3793
|
return rpcError(E.SIG_VERIFY_FAILED, "Missing top-level `sig` field");
|
|
2939
3794
|
}
|
|
2940
3795
|
const parts = sig.split(":");
|
|
2941
|
-
|
|
3796
|
+
const scheme = parts[0];
|
|
3797
|
+
if (parts.length !== 3 || scheme !== "ed25519" && scheme !== "webauthn") {
|
|
3798
|
+
return rpcError(E.SIG_VERIFY_FAILED, "Malformed signature tag");
|
|
3799
|
+
}
|
|
3800
|
+
const assertion = scheme === "webauthn" ? parseWebAuthnAssertion(parts[2]) : void 0;
|
|
3801
|
+
if (scheme === "webauthn" && !assertion) {
|
|
2942
3802
|
return rpcError(E.SIG_VERIFY_FAILED, "Malformed signature tag");
|
|
2943
3803
|
}
|
|
2944
3804
|
const kid = parts[1];
|
|
@@ -2972,6 +3832,14 @@ var Coordinator = class {
|
|
|
2972
3832
|
if (revoked) return rpcError(E.SIG_KEY_REVOKED, `Key ${kid} is revoked`);
|
|
2973
3833
|
return rpcError(E.SIG_KEY_NOT_FOUND, `No key ${kid} valid at ${ts} for ${sender}`);
|
|
2974
3834
|
}
|
|
3835
|
+
if (assertion) {
|
|
3836
|
+
if (!isWebAuthnKey(key.jwk)) return rpcError(E.SIG_VERIFY_FAILED, `Key ${kid} is not a WebAuthn key`);
|
|
3837
|
+
const stripped = JSON.parse(JSON.stringify(envelope));
|
|
3838
|
+
delete stripped.sig;
|
|
3839
|
+
const failure = webAuthnFailure(canonicalize(stripped), assertion, key.jwk);
|
|
3840
|
+
return failure === null ? null : rpcError(E.SIG_VERIFY_FAILED, failure);
|
|
3841
|
+
}
|
|
3842
|
+
if (!isEd25519Key(key.jwk)) return rpcError(E.SIG_VERIFY_FAILED, `Key ${kid} is not an Ed25519 key`);
|
|
2975
3843
|
try {
|
|
2976
3844
|
const stripped = JSON.parse(JSON.stringify(envelope));
|
|
2977
3845
|
delete stripped.sig;
|
|
@@ -2985,6 +3853,39 @@ var Coordinator = class {
|
|
|
2985
3853
|
return rpcError(E.SIG_VERIFY_FAILED, "Signature failed verification");
|
|
2986
3854
|
}
|
|
2987
3855
|
}
|
|
3856
|
+
/**
|
|
3857
|
+
* SPECIFICATION 6.5: an identity profile states that a verifier checks
|
|
3858
|
+
* people's credentials, so advertising one this coordinator has no
|
|
3859
|
+
* verifier for describes a guarantee it does not give.
|
|
3860
|
+
*/
|
|
3861
|
+
unverifiedIdentity(profiles) {
|
|
3862
|
+
const named = (name) => profiles.some((x) => x === name || x.startsWith(name + "/"));
|
|
3863
|
+
if (named("identity-oidc") && this.options.verifyOidcToken === void 0) {
|
|
3864
|
+
return rpcError(
|
|
3865
|
+
E.PARAMS,
|
|
3866
|
+
"identity-oidc/1.0 is advertised but no token verifier is configured. A workspace that advertises it and does not enforce it describes a guarantee it does not give."
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
if (named("identity-vc") && this.options.verifyVc === void 0) {
|
|
3870
|
+
return rpcError(
|
|
3871
|
+
E.PARAMS,
|
|
3872
|
+
"identity-vc/1.0 is advertised but no presentation verifier is configured. A workspace that advertises it and does not enforce it describes a guarantee it does not give."
|
|
3873
|
+
);
|
|
3874
|
+
}
|
|
3875
|
+
return null;
|
|
3876
|
+
}
|
|
3877
|
+
/** Whether a workspace requires signatures: it advertises security-signed/1.0, or this coordinator is configured to. */
|
|
3878
|
+
requiresSignatures(ws) {
|
|
3879
|
+
return !!this.options.requireSignatures || ws !== void 0 && advertises(ws, "security-signed");
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Whether a participant of this type signs its calls in a workspace that
|
|
3883
|
+
* requires signatures: every participant under "all", and every one but an
|
|
3884
|
+
* agent under "humans". A non-member signs.
|
|
3885
|
+
*/
|
|
3886
|
+
mustSign(ws, type) {
|
|
3887
|
+
return ws.signature_policy === "all" || type !== "agent";
|
|
3888
|
+
}
|
|
2988
3889
|
/** Lookup the key valid for a given participant at a given time. */
|
|
2989
3890
|
lookupKey(member, kid, ts) {
|
|
2990
3891
|
for (const k of member.keys) {
|
|
@@ -3058,6 +3959,7 @@ var Coordinator = class {
|
|
|
3058
3959
|
this.handlers.set("workspace.create", (p) => this.opWorkspaceCreate(p));
|
|
3059
3960
|
this.handlers.set("workspace.describe", (p) => this.opWorkspaceDescribe(p));
|
|
3060
3961
|
this.handlers.set("workspace.set_profiles", (p) => this.opWorkspaceSetProfiles(p));
|
|
3962
|
+
this.handlers.set("workspace.invite", (p) => this.opWorkspaceInvite(p));
|
|
3061
3963
|
this.handlers.set("participant.join", (p) => this.opParticipantJoin(p));
|
|
3062
3964
|
this.handlers.set("participant.leave", (p) => this.opParticipantLeave(p));
|
|
3063
3965
|
this.handlers.set("task.create", (p) => this.opTaskCreate(p));
|
|
@@ -3065,6 +3967,7 @@ var Coordinator = class {
|
|
|
3065
3967
|
this.handlers.set("task.complete", (p) => this.opTaskComplete(p));
|
|
3066
3968
|
this.handlers.set("audit.read", (p) => this.opAuditRead(p));
|
|
3067
3969
|
this.handlers.set("review.request", (p) => this.opReviewRequest(p));
|
|
3970
|
+
this.handlers.set("review.acknowledge", (p) => this.opReviewAcknowledge(p));
|
|
3068
3971
|
this.handlers.set("decide.approve", (p) => this.opDecide(p, "approve"));
|
|
3069
3972
|
this.handlers.set("decide.reject", (p) => this.opDecide(p, "reject"));
|
|
3070
3973
|
this.handlers.set("decide.override", (p) => this.opDecideOverride(p));
|
|
@@ -3084,24 +3987,59 @@ var Coordinator = class {
|
|
|
3084
3987
|
// Core method handlers
|
|
3085
3988
|
// ==========================================================
|
|
3086
3989
|
opWorkspaceCreate(p) {
|
|
3990
|
+
const built = this.buildWorkspace(p);
|
|
3991
|
+
if (built.error) return { error: built.error };
|
|
3992
|
+
this.addWorkspace(built.ws);
|
|
3993
|
+
return { result: { workspace: built.ws.id, created: built.ws.created } };
|
|
3994
|
+
}
|
|
3995
|
+
/**
|
|
3996
|
+
* The workspace a workspace.create with these params makes, or why it
|
|
3997
|
+
* cannot be made. Nothing is registered, so participant.join can judge a
|
|
3998
|
+
* join by the workspace it would make and make it only once the join
|
|
3999
|
+
* passes.
|
|
4000
|
+
*/
|
|
4001
|
+
buildWorkspace(p) {
|
|
4002
|
+
const bad = paramsError(p, [
|
|
4003
|
+
["profiles", "strings"],
|
|
4004
|
+
["mode", "string"],
|
|
4005
|
+
["mode_ceiling", "string"],
|
|
4006
|
+
["routing_policy_uri", "string"],
|
|
4007
|
+
["step_up_window_sec", "integer"],
|
|
4008
|
+
["min_acr", "string"]
|
|
4009
|
+
]);
|
|
4010
|
+
if (bad) return bad;
|
|
4011
|
+
for (const name of ["mode", "mode_ceiling"]) {
|
|
4012
|
+
if (p[name] != null && !MODES.has(p[name])) {
|
|
4013
|
+
return { error: rpcError(E.PARAMS, `${name} must be shadow, trial or production`) };
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
const policy = p.signature_policy ?? this.options.signaturePolicy ?? "all";
|
|
4017
|
+
if (policy !== "all" && policy !== "humans") {
|
|
4018
|
+
return { error: rpcError(E.PARAMS, "signature_policy must be all or humans") };
|
|
4019
|
+
}
|
|
4020
|
+
const joinPolicy = p.join_policy ?? this.options.joinPolicy ?? "open";
|
|
4021
|
+
if (joinPolicy !== "open" && joinPolicy !== "invite") {
|
|
4022
|
+
return { error: rpcError(E.PARAMS, "join_policy must be open or invite") };
|
|
4023
|
+
}
|
|
4024
|
+
const reviewRule = p.review_rule ?? void 0;
|
|
4025
|
+
if (reviewRule !== void 0 && (typeof reviewRule !== "string" || !reviewRuleSupported(reviewRule))) {
|
|
4026
|
+
return { error: rpcError(E.PARAMS, "review_rule must be any_one_approves, all_approve or quorum:<n>") };
|
|
4027
|
+
}
|
|
3087
4028
|
const id = p.workspace || this.ids.workspaceId();
|
|
3088
|
-
if (typeof id !== "string") return { error: rpcError(E.PARAMS, "workspace must be a string id") };
|
|
3089
4029
|
if (this.workspaces.has(id)) return { error: rpcError(E.PARAMS, `workspace already exists: ${id}`) };
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
["identity-oidc/1.0", this.options.verifyOidcToken !== void 0, "verifyOidcToken"]
|
|
3094
|
-
]) {
|
|
3095
|
-
const name = profile.split("/")[0];
|
|
3096
|
-
const advertised = profiles.some((x) => x === name || x.startsWith(name + "/"));
|
|
3097
|
-
if (enforced && !advertised) profiles.push(profile);
|
|
3098
|
-
else if (advertised && !enforced) {
|
|
3099
|
-
return { error: rpcError(
|
|
3100
|
-
E.PARAMS,
|
|
3101
|
-
`${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.`
|
|
3102
|
-
) };
|
|
3103
|
-
}
|
|
4030
|
+
if (Array.isArray(p.profiles)) {
|
|
4031
|
+
const unknown = profilesError(p.profiles);
|
|
4032
|
+
if (unknown) return unknown;
|
|
3104
4033
|
}
|
|
4034
|
+
const profiles = Array.isArray(p.profiles) ? [...p.profiles] : [...this.options.defaultProfiles ?? ["core/1.0", "review/1.0"]];
|
|
4035
|
+
const signed = profiles.some((x) => x === "security-signed" || x.startsWith("security-signed/"));
|
|
4036
|
+
if (this.options.requireSignatures && !signed) profiles.push("security-signed/1.0");
|
|
4037
|
+
const oidc = profiles.some((x) => x === "identity-oidc" || x.startsWith("identity-oidc/"));
|
|
4038
|
+
if (this.options.verifyOidcToken !== void 0 && !oidc) profiles.push("identity-oidc/1.0");
|
|
4039
|
+
const vc = profiles.some((x) => x === "identity-vc" || x.startsWith("identity-vc/"));
|
|
4040
|
+
if (this.options.verifyVc !== void 0 && !vc) profiles.push("identity-vc/1.0");
|
|
4041
|
+
const unverified = this.unverifiedIdentity(profiles);
|
|
4042
|
+
if (unverified) return { error: unverified };
|
|
3105
4043
|
const chainEnabled = profiles.includes("audit-scitt/1.0") || !!this.options.enableChain;
|
|
3106
4044
|
const ws = {
|
|
3107
4045
|
id,
|
|
@@ -3110,9 +4048,14 @@ var Coordinator = class {
|
|
|
3110
4048
|
profiles,
|
|
3111
4049
|
mode: p.mode ?? "trial",
|
|
3112
4050
|
mode_ceiling: p.mode_ceiling ?? "production",
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
4051
|
+
// A param given as null gives no value, as in the Python reference: a
|
|
4052
|
+
// null min_acr sets no minimum.
|
|
4053
|
+
routing_policy_uri: p.routing_policy_uri ?? void 0,
|
|
4054
|
+
step_up_window_sec: p.step_up_window_sec ?? 300,
|
|
4055
|
+
min_acr: p.min_acr ?? void 0,
|
|
4056
|
+
signature_policy: policy,
|
|
4057
|
+
join_policy: joinPolicy,
|
|
4058
|
+
...reviewRule !== void 0 ? { review_rule: reviewRule } : {},
|
|
3116
4059
|
members: this.newMap(),
|
|
3117
4060
|
tasks: this.newMap(),
|
|
3118
4061
|
overrides: this.newMap(),
|
|
@@ -3121,14 +4064,17 @@ var Coordinator = class {
|
|
|
3121
4064
|
handoffs: this.newMap(),
|
|
3122
4065
|
snapshots: this.newMap(),
|
|
3123
4066
|
route_decisions: this.newMap(),
|
|
4067
|
+
invitations: this.newMap(),
|
|
3124
4068
|
idempotency_keys: {},
|
|
3125
4069
|
audit: [],
|
|
3126
4070
|
chain_enabled: chainEnabled,
|
|
3127
4071
|
chain_head: chainEnabled ? ZERO_HASH : void 0
|
|
3128
4072
|
};
|
|
3129
|
-
|
|
4073
|
+
return { ws };
|
|
4074
|
+
}
|
|
4075
|
+
addWorkspace(ws) {
|
|
4076
|
+
this.workspaces.set(ws.id, ws);
|
|
3130
4077
|
this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
|
|
3131
|
-
return { result: { workspace: id, created: ws.created } };
|
|
3132
4078
|
}
|
|
3133
4079
|
opWorkspaceDescribe(p) {
|
|
3134
4080
|
const miss = missing(p, ["workspace"]);
|
|
@@ -3147,6 +4093,9 @@ var Coordinator = class {
|
|
|
3147
4093
|
mode_ceiling: ws.mode_ceiling,
|
|
3148
4094
|
max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
|
|
3149
4095
|
step_up_window_sec: ws.step_up_window_sec,
|
|
4096
|
+
signature_policy: ws.signature_policy,
|
|
4097
|
+
join_policy: ws.join_policy,
|
|
4098
|
+
review_rule: ws.review_rule ?? "any_one_approves",
|
|
3150
4099
|
profiles: ws.profiles,
|
|
3151
4100
|
members: Array.from(rawValues(ws.members)).map(memberToDict),
|
|
3152
4101
|
audit_count: ws.audit.length,
|
|
@@ -3162,6 +4111,11 @@ var Coordinator = class {
|
|
|
3162
4111
|
opWorkspaceSetProfiles(p) {
|
|
3163
4112
|
const miss = missing(p, ["workspace", "from", "profiles"]);
|
|
3164
4113
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
4114
|
+
if (!Array.isArray(p.profiles) || !p.profiles.every((x) => typeof x === "string")) {
|
|
4115
|
+
return { error: rpcError(E.PARAMS, "profiles must be a list of strings") };
|
|
4116
|
+
}
|
|
4117
|
+
const unknown = profilesError(p.profiles);
|
|
4118
|
+
if (unknown) return unknown;
|
|
3165
4119
|
const ws = this.workspaces.get(p.workspace);
|
|
3166
4120
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
3167
4121
|
const notMember = this.requireMember(ws, p.from);
|
|
@@ -3170,6 +4124,14 @@ var Coordinator = class {
|
|
|
3170
4124
|
return { error: rpcError(E.NOT_AUTHORISED, "workspace.set_profiles requires the admin role") };
|
|
3171
4125
|
}
|
|
3172
4126
|
const newProfiles = [...p.profiles];
|
|
4127
|
+
for (const name of LASTING_PROFILES) {
|
|
4128
|
+
const held = ws.profiles.find((x) => x === name || x.startsWith(name + "/"));
|
|
4129
|
+
if (held !== void 0 && !newProfiles.some((x) => x.startsWith(name + "/"))) {
|
|
4130
|
+
return { error: rpcError(E.NOT_AUTHORISED, `${held} cannot be removed once advertised`) };
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
const unverified = this.unverifiedIdentity(newProfiles);
|
|
4134
|
+
if (unverified) return { error: unverified };
|
|
3173
4135
|
if (!newProfiles.some((pr) => pr.startsWith("core/"))) newProfiles.push("core/1.0");
|
|
3174
4136
|
ws.profiles = newProfiles;
|
|
3175
4137
|
if (newProfiles.includes("audit-scitt/1.0") && !ws.chain_enabled) {
|
|
@@ -3181,20 +4143,76 @@ var Coordinator = class {
|
|
|
3181
4143
|
opParticipantJoin(p) {
|
|
3182
4144
|
const miss = missing(p, ["workspace", "from", "type"]);
|
|
3183
4145
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
4146
|
+
for (const f of ["from", "type"]) {
|
|
4147
|
+
if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
|
|
4148
|
+
}
|
|
4149
|
+
const type = p.type;
|
|
4150
|
+
const named = participantError(type, p.from, "from");
|
|
4151
|
+
if (named) return named;
|
|
4152
|
+
const bad = paramsError(p, [
|
|
4153
|
+
["role", "string"],
|
|
4154
|
+
["display_name", "string"],
|
|
4155
|
+
["scopes", "strings"],
|
|
4156
|
+
["capabilities", "object"],
|
|
4157
|
+
["jwks", "object"],
|
|
4158
|
+
["jwks.keys", "list"],
|
|
4159
|
+
["oidc_token", "string"],
|
|
4160
|
+
["vc_presentation", "object"]
|
|
4161
|
+
]);
|
|
4162
|
+
if (bad) return bad;
|
|
4163
|
+
if (p.role === "admin" && !ADMIN_TYPES.includes(type)) {
|
|
4164
|
+
return { error: rpcError(E.PARAMS, "The admin role is held by a human or a service") };
|
|
4165
|
+
}
|
|
3184
4166
|
let ws = this.workspaces.get(p.workspace);
|
|
4167
|
+
const made = ws === void 0;
|
|
3185
4168
|
if (!ws) {
|
|
3186
|
-
const
|
|
3187
|
-
if (
|
|
3188
|
-
ws =
|
|
4169
|
+
const built = this.buildWorkspace({ workspace: p.workspace });
|
|
4170
|
+
if (built.error) return { error: built.error };
|
|
4171
|
+
ws = built.ws;
|
|
3189
4172
|
}
|
|
3190
4173
|
const uri = p.from;
|
|
3191
4174
|
const now = this.now();
|
|
3192
4175
|
const existing = ws.members.get(uri);
|
|
3193
4176
|
let tokenVerified = false;
|
|
4177
|
+
let holderVerified = false;
|
|
4178
|
+
let credentialBound = false;
|
|
4179
|
+
const invitation = existing ? void 0 : rawInvitation(ws, uri);
|
|
4180
|
+
const asked = typeof p.role === "string" && p.role !== "" ? p.role : void 0;
|
|
4181
|
+
let role = asked ?? "participant";
|
|
4182
|
+
if (!existing) {
|
|
4183
|
+
const first = awaitsFirstMember(ws);
|
|
4184
|
+
if (ws.join_policy === "invite" && !invitation && !(first && ADMIN_TYPES.includes(type))) {
|
|
4185
|
+
return { error: rpcError(E.NOT_AUTHORISED, `${uri} has no invitation to this workspace`) };
|
|
4186
|
+
}
|
|
4187
|
+
if (invitation?.role !== void 0) {
|
|
4188
|
+
if (asked === "admin" && invitation.role !== "admin") {
|
|
4189
|
+
return { error: rpcError(E.NOT_AUTHORISED, "The admin role is granted by an admin") };
|
|
4190
|
+
}
|
|
4191
|
+
role = invitation.role;
|
|
4192
|
+
} else if (first && ws.join_policy === "invite") {
|
|
4193
|
+
if (asked !== void 0 && asked !== "admin") {
|
|
4194
|
+
return { error: rpcError(
|
|
4195
|
+
E.PARAMS,
|
|
4196
|
+
"The first member of a workspace that admits by invitation is its admin"
|
|
4197
|
+
) };
|
|
4198
|
+
}
|
|
4199
|
+
role = "admin";
|
|
4200
|
+
} else if (asked === "admin" && !first) {
|
|
4201
|
+
return { error: rpcError(E.NOT_AUTHORISED, "The admin role is granted by an admin") };
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
if (type === "human") {
|
|
4205
|
+
const oidc = this.options.verifyOidcToken !== void 0 && advertises(ws, "identity-oidc");
|
|
4206
|
+
const vc = this.options.verifyVc !== void 0 && advertises(ws, "identity-vc");
|
|
4207
|
+
const presented = oidc && typeof p.oidc_token === "string" || vc && isObject(p.vc_presentation);
|
|
4208
|
+
if ((oidc || vc) && !presented) {
|
|
4209
|
+
return { error: oidc ? rpcError(E.OIDC_TOKEN_INVALID, vc ? "An oidc_token or a vc_presentation is required to join as a human participant" : "An oidc_token is required to join as a human participant") : rpcError(E.VC_VP_INVALID, "A vc_presentation is required to join as a human participant") };
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
3194
4212
|
const member = {
|
|
3195
4213
|
uri,
|
|
3196
|
-
type
|
|
3197
|
-
role
|
|
4214
|
+
type,
|
|
4215
|
+
role,
|
|
3198
4216
|
joined: now,
|
|
3199
4217
|
display_name: p.display_name,
|
|
3200
4218
|
capabilities: p.capabilities,
|
|
@@ -3202,6 +4220,9 @@ var Coordinator = class {
|
|
|
3202
4220
|
keys: [],
|
|
3203
4221
|
paused: false
|
|
3204
4222
|
};
|
|
4223
|
+
if (invitation?.oidc_sub !== void 0 && (this.options.verifyOidcToken === void 0 || typeof p.oidc_token !== "string")) {
|
|
4224
|
+
return { error: rpcError(E.OIDC_TOKEN_INVALID, "An oidc_token is required: the invitation names a token subject") };
|
|
4225
|
+
}
|
|
3205
4226
|
if (this.options.verifyOidcToken && typeof p.oidc_token === "string") {
|
|
3206
4227
|
const claims = this.options.verifyOidcToken(p.oidc_token);
|
|
3207
4228
|
if (claims === null) return { error: rpcError(E.OIDC_TOKEN_INVALID, "OIDC token invalid") };
|
|
@@ -3210,6 +4231,9 @@ var Coordinator = class {
|
|
|
3210
4231
|
if (boundUri != null && boundUri !== uri) {
|
|
3211
4232
|
return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token is bound to another participant") };
|
|
3212
4233
|
}
|
|
4234
|
+
if (invitation?.oidc_sub !== void 0 && sub !== invitation.oidc_sub) {
|
|
4235
|
+
return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token subject does not match the invitation") };
|
|
4236
|
+
}
|
|
3213
4237
|
if (existing) {
|
|
3214
4238
|
if (existing.oidc_sub != null) {
|
|
3215
4239
|
if (sub !== existing.oidc_sub) {
|
|
@@ -3218,19 +4242,23 @@ var Coordinator = class {
|
|
|
3218
4242
|
} else if (boundUri !== uri) {
|
|
3219
4243
|
return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token does not name this member") };
|
|
3220
4244
|
}
|
|
4245
|
+
} else if (type === "human" && boundUri !== uri && invitation?.oidc_sub === void 0) {
|
|
4246
|
+
return { error: rpcError(E.OIDC_CNF_MISMATCH, "OIDC token does not name this participant") };
|
|
3221
4247
|
}
|
|
3222
4248
|
member.oidc_sub = sub;
|
|
3223
4249
|
tokenVerified = true;
|
|
3224
4250
|
const at = claims.auth_time;
|
|
3225
|
-
if (typeof at === "number") member.oidc_auth_time = at;
|
|
4251
|
+
if (typeof at === "number" && Number.isFinite(at) && Math.abs(at) <= Number.MAX_SAFE_INTEGER) member.oidc_auth_time = Math.floor(at);
|
|
3226
4252
|
member.oidc_acr = typeof claims.acr === "string" ? claims.acr : void 0;
|
|
3227
|
-
const cnf = claims.cnf;
|
|
3228
|
-
const
|
|
3229
|
-
if (
|
|
3230
|
-
|
|
4253
|
+
const cnf = isObject(claims.cnf) ? claims.cnf.jwk : void 0;
|
|
4254
|
+
const bound = credentialKeys(cnf, cnf == null ? claims.nonce : void 0, signingKeys(p.jwks));
|
|
4255
|
+
if (bound === null) return { error: rpcError(E.OIDC_CNF_MISMATCH, "The join's keys are not the key the token binds") };
|
|
4256
|
+
if (bound !== void 0) {
|
|
4257
|
+
credentialBound = true;
|
|
4258
|
+
for (const j of bound) member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
|
|
3231
4259
|
}
|
|
3232
4260
|
}
|
|
3233
|
-
if (this.options.verifyVc &&
|
|
4261
|
+
if (this.options.verifyVc && isObject(p.vc_presentation)) {
|
|
3234
4262
|
const subject = this.options.verifyVc(p.vc_presentation);
|
|
3235
4263
|
if (subject === null) return { error: rpcError(E.VC_VP_INVALID, "VC presentation invalid") };
|
|
3236
4264
|
const holder = [subject.holder, subject.id].find(
|
|
@@ -3240,21 +4268,54 @@ var Coordinator = class {
|
|
|
3240
4268
|
return { error: rpcError(E.VC_HOLDER_BINDING_INVALID, "Presentation holder does not match the member") };
|
|
3241
4269
|
}
|
|
3242
4270
|
member.vc_holder = holder;
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
4271
|
+
holderVerified = true;
|
|
4272
|
+
const bound = credentialKeys(subject.cnf_jwk, void 0, signingKeys(p.jwks));
|
|
4273
|
+
if (bound === null) {
|
|
4274
|
+
return { error: rpcError(E.VC_HOLDER_BINDING_INVALID, "The join's keys are not the key the presentation binds") };
|
|
4275
|
+
}
|
|
4276
|
+
if (bound !== void 0) {
|
|
4277
|
+
credentialBound = true;
|
|
4278
|
+
for (const j of bound) member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
if (!existing && type === "human" && (tokenVerified || holderVerified) && !credentialBound && !invitation?.jwks && signingKeys(p.jwks).length > 0) {
|
|
4282
|
+
return { error: tokenVerified ? rpcError(E.OIDC_CNF_MISMATCH, "The token binds no key: a person it verifies signs with a key it binds") : rpcError(
|
|
4283
|
+
E.VC_HOLDER_BINDING_INVALID,
|
|
4284
|
+
"The presentation binds no key: a person it verifies signs with a key it binds"
|
|
4285
|
+
) };
|
|
4286
|
+
}
|
|
4287
|
+
const pinned = invitation?.jwks?.keys;
|
|
4288
|
+
if (pinned) {
|
|
4289
|
+
const strays = [...signingKeys(p.jwks), ...member.keys.map((k) => k.jwk)].filter((j) => !pinned.some((k) => sameKey(k, j)));
|
|
4290
|
+
if (strays.length) {
|
|
4291
|
+
return { error: rpcError(
|
|
4292
|
+
E.NOT_AUTHORISED,
|
|
4293
|
+
`${uri} joins with keys its invitation does not name`,
|
|
4294
|
+
{ kids: [...new Set(strays.map((j) => j.kid))] }
|
|
4295
|
+
) };
|
|
3246
4296
|
}
|
|
4297
|
+
member.keys = pinned.map((j) => ({ jwk: j, kid: j.kid, valid_from: now }));
|
|
3247
4298
|
}
|
|
3248
4299
|
const attested = [...member.keys];
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
if (j.kid && !member.keys.some((k) => k.kid === j.kid)) {
|
|
4300
|
+
if (attested.length === 0) {
|
|
4301
|
+
for (const j of signingKeys(p.jwks)) {
|
|
4302
|
+
if (!member.keys.some((k) => k.kid === j.kid)) {
|
|
3253
4303
|
member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
|
|
3254
4304
|
}
|
|
3255
4305
|
}
|
|
3256
4306
|
}
|
|
3257
4307
|
if (existing) {
|
|
4308
|
+
if (attested.length === 0) {
|
|
4309
|
+
const keys = signingKeys(p.jwks);
|
|
4310
|
+
const held = keys.some((j) => existing.keys.some((k) => sameKey(k.jwk, j)));
|
|
4311
|
+
if (keys.length > 0 && !held) {
|
|
4312
|
+
return { error: rpcError(
|
|
4313
|
+
E.NOT_AUTHORISED,
|
|
4314
|
+
`${uri} is already a member, with other keys; a new key arrives through participant.rotate_key`,
|
|
4315
|
+
{ kids: [...new Set(keys.map((j) => j.kid))] }
|
|
4316
|
+
) };
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
3258
4319
|
if (member.oidc_sub !== void 0) existing.oidc_sub = member.oidc_sub;
|
|
3259
4320
|
if (member.oidc_auth_time !== void 0) existing.oidc_auth_time = member.oidc_auth_time;
|
|
3260
4321
|
if (member.oidc_acr !== void 0) existing.oidc_acr = member.oidc_acr;
|
|
@@ -3265,18 +4326,126 @@ var Coordinator = class {
|
|
|
3265
4326
|
}
|
|
3266
4327
|
return { result: { joined: true, as: uri } };
|
|
3267
4328
|
}
|
|
4329
|
+
if (this.requiresSignatures(ws) && this.mustSign(ws, type) && member.keys.length === 0) {
|
|
4330
|
+
return { error: rpcError(E.PARAMS, `${uri} must sign in this workspace, and joins with no key`) };
|
|
4331
|
+
}
|
|
4332
|
+
if (made) this.addWorkspace(ws);
|
|
3268
4333
|
ws.members.set(uri, member);
|
|
4334
|
+
if (invitation) ws.invitations.delete(uri);
|
|
3269
4335
|
return { result: { joined: true, as: uri } };
|
|
3270
4336
|
}
|
|
4337
|
+
/**
|
|
4338
|
+
* SPECIFICATION 6.3.3: an admin invites a participant. For a URI that is
|
|
4339
|
+
* not a member it records an invitation, which the invitee's join takes
|
|
4340
|
+
* up. For a member that holds no key in force it registers the
|
|
4341
|
+
* invitation's keys: the way back for a member whose keys were all revoked,
|
|
4342
|
+
* or who joined a workspace that requires signatures with none.
|
|
4343
|
+
*/
|
|
4344
|
+
opWorkspaceInvite(p) {
|
|
4345
|
+
const miss = missing(p, ["workspace", "from", "uri", "type"]);
|
|
4346
|
+
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
4347
|
+
for (const f of ["uri", "type"]) {
|
|
4348
|
+
if (typeof p[f] !== "string") return { error: rpcError(E.PARAMS, `${f} must be a string`) };
|
|
4349
|
+
}
|
|
4350
|
+
const bad = paramsError(p, [
|
|
4351
|
+
["role", "string"],
|
|
4352
|
+
["jwks", "object"],
|
|
4353
|
+
["jwks.keys", "list"],
|
|
4354
|
+
["oidc_sub", "string"]
|
|
4355
|
+
]);
|
|
4356
|
+
if (bad) return bad;
|
|
4357
|
+
const ws = this.workspaces.get(p.workspace);
|
|
4358
|
+
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
4359
|
+
const notMember = this.requireMember(ws, p.from);
|
|
4360
|
+
if (notMember) return notMember;
|
|
4361
|
+
if (rawMember(ws, p.from).role !== "admin") {
|
|
4362
|
+
return { error: rpcError(E.NOT_AUTHORISED, "workspace.invite requires the admin role") };
|
|
4363
|
+
}
|
|
4364
|
+
const uri = p.uri;
|
|
4365
|
+
const type = p.type;
|
|
4366
|
+
const named = participantError(type, uri, "uri");
|
|
4367
|
+
if (named) return named;
|
|
4368
|
+
const role = typeof p.role === "string" && p.role !== "" ? p.role : void 0;
|
|
4369
|
+
if (role === "admin" && !ADMIN_TYPES.includes(type)) {
|
|
4370
|
+
return { error: rpcError(E.PARAMS, "The admin role is held by a human or a service") };
|
|
4371
|
+
}
|
|
4372
|
+
const sub = typeof p.oidc_sub === "string" ? p.oidc_sub : void 0;
|
|
4373
|
+
if (sub !== void 0 && this.options.verifyOidcToken === void 0) {
|
|
4374
|
+
return { error: rpcError(E.PARAMS, "oidc_sub names a token subject, and this Coordinator has no token verifier") };
|
|
4375
|
+
}
|
|
4376
|
+
let pinned;
|
|
4377
|
+
if (p.jwks != null) {
|
|
4378
|
+
pinned = signingKeys(p.jwks);
|
|
4379
|
+
if (pinned.length === 0) return { error: rpcError(E.PARAMS, "jwks names no key: each key needs a kid") };
|
|
4380
|
+
}
|
|
4381
|
+
const now = this.now();
|
|
4382
|
+
const member = ws.members.get(uri);
|
|
4383
|
+
if (member) {
|
|
4384
|
+
if (member.keys.some((k) => inForce(k, now))) {
|
|
4385
|
+
return { error: rpcError(
|
|
4386
|
+
E.PARAMS,
|
|
4387
|
+
`${uri} is a member and holds a key in force; its keys change through participant.rotate_key`
|
|
4388
|
+
) };
|
|
4389
|
+
}
|
|
4390
|
+
if (!pinned) {
|
|
4391
|
+
return { error: rpcError(E.PARAMS, `${uri} is a member; an invitation for a member registers the keys its jwks names`) };
|
|
4392
|
+
}
|
|
4393
|
+
if (this.options.verifyOidcToken !== void 0 && (member.type === "human" || member.oidc_sub !== void 0) || this.options.verifyVc !== void 0 && (member.type === "human" || member.vc_holder !== void 0)) {
|
|
4394
|
+
return { error: rpcError(
|
|
4395
|
+
E.PARAMS,
|
|
4396
|
+
`${uri} registers a new key by joining again with a token that binds it; an invitation does not register a person's keys here`
|
|
4397
|
+
) };
|
|
4398
|
+
}
|
|
4399
|
+
if (role !== void 0 && role !== member.role || sub !== void 0 && sub !== member.oidc_sub) {
|
|
4400
|
+
return { error: rpcError(E.PARAMS, `${uri} is a member; an invitation for a member registers keys and changes nothing else`) };
|
|
4401
|
+
}
|
|
4402
|
+
const reused = pinned.find((j) => member.keys.some((k) => k.kid === j.kid));
|
|
4403
|
+
if (reused) return { error: rpcError(E.PARAMS, `Key ${reused.kid} was ${uri}'s; a new key takes a new kid`) };
|
|
4404
|
+
for (const j of pinned) member.keys.push({ jwk: j, kid: j.kid, valid_from: now });
|
|
4405
|
+
return { result: { invited: true, uri, registered: pinned.map((j) => j.kid) } };
|
|
4406
|
+
}
|
|
4407
|
+
if (pinned && (type === "human" || sub !== void 0) && (this.options.verifyOidcToken !== void 0 || this.options.verifyVc !== void 0)) {
|
|
4408
|
+
return { error: rpcError(
|
|
4409
|
+
E.PARAMS,
|
|
4410
|
+
`${uri} joins with a token that binds its key; an invitation does not register a person's keys here`
|
|
4411
|
+
) };
|
|
4412
|
+
}
|
|
4413
|
+
const invitation = {
|
|
4414
|
+
uri,
|
|
4415
|
+
type,
|
|
4416
|
+
invited_by: p.from,
|
|
4417
|
+
invited_at: now
|
|
4418
|
+
};
|
|
4419
|
+
if (role !== void 0) invitation.role = role;
|
|
4420
|
+
if (pinned) invitation.jwks = { keys: pinned };
|
|
4421
|
+
if (sub !== void 0) invitation.oidc_sub = sub;
|
|
4422
|
+
ws.invitations.set(uri, invitation);
|
|
4423
|
+
return { result: { invited: true, uri } };
|
|
4424
|
+
}
|
|
3271
4425
|
opParticipantLeave(p) {
|
|
3272
4426
|
const ws = this.workspaces.get(p.workspace);
|
|
3273
4427
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
4428
|
+
if (rawMember(ws, p.from)?.paused) {
|
|
4429
|
+
return { error: rpcError(E.NOT_AUTHORISED, "A paused participant does not leave until another member lifts its pause") };
|
|
4430
|
+
}
|
|
3274
4431
|
ws.members.delete(p.from);
|
|
3275
4432
|
return { result: { left: true } };
|
|
3276
4433
|
}
|
|
3277
4434
|
opTaskCreate(p) {
|
|
3278
4435
|
const miss = missing(p, ["workspace", "from", "kind", "input"]);
|
|
3279
4436
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
4437
|
+
const bad = paramsError(p, [
|
|
4438
|
+
["assignee", "string"],
|
|
4439
|
+
["to", "string"],
|
|
4440
|
+
["mode", "string"],
|
|
4441
|
+
["review_required", "boolean"],
|
|
4442
|
+
["routing_hints", "object"],
|
|
4443
|
+
["idempotency_key", "string"]
|
|
4444
|
+
]);
|
|
4445
|
+
if (bad) return bad;
|
|
4446
|
+
if (RESERVED_KEYS.includes(p.idempotency_key)) {
|
|
4447
|
+
return { error: rpcError(E.PARAMS, "idempotency_key must not be __proto__, constructor or prototype") };
|
|
4448
|
+
}
|
|
3280
4449
|
const ws = this.workspaces.get(p.workspace);
|
|
3281
4450
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
3282
4451
|
const notMember = this.requireMember(ws, p.from);
|
|
@@ -3288,9 +4457,6 @@ var Coordinator = class {
|
|
|
3288
4457
|
if (ws.members.get(assignee).paused) {
|
|
3289
4458
|
return { error: rpcError(E.CONTROL_WORKSPACE_PAUSED, `Assignee ${assignee} is paused`) };
|
|
3290
4459
|
}
|
|
3291
|
-
if (p.mode !== void 0 && p.mode !== null && typeof p.mode !== "string") {
|
|
3292
|
-
return { error: rpcError(E.PARAMS, "mode must be a string") };
|
|
3293
|
-
}
|
|
3294
4460
|
const requestedMode = p.mode || ws.mode;
|
|
3295
4461
|
if (!modeLE(requestedMode, ws.mode_ceiling)) {
|
|
3296
4462
|
return { error: rpcError(
|
|
@@ -3298,9 +4464,6 @@ var Coordinator = class {
|
|
|
3298
4464
|
`Requested mode ${requestedMode} exceeds ceiling ${ws.mode_ceiling}`
|
|
3299
4465
|
) };
|
|
3300
4466
|
}
|
|
3301
|
-
if (p.review_required !== void 0 && p.review_required !== null && typeof p.review_required !== "boolean") {
|
|
3302
|
-
return { error: rpcError(E.PARAMS, "review_required must be a boolean") };
|
|
3303
|
-
}
|
|
3304
4467
|
const taskId = this.ids.taskId();
|
|
3305
4468
|
const now = this.now();
|
|
3306
4469
|
const task = {
|
|
@@ -3339,6 +4502,7 @@ var Coordinator = class {
|
|
|
3339
4502
|
if (notMember) return notMember;
|
|
3340
4503
|
const task = ws.tasks.get(p.task_id);
|
|
3341
4504
|
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
4505
|
+
if (typeof p.state !== "string") return { error: rpcError(E.PARAMS, "state must be a string") };
|
|
3342
4506
|
const newState = p.state;
|
|
3343
4507
|
const legal = {
|
|
3344
4508
|
created: ["in_progress", "declined"],
|
|
@@ -3349,12 +4513,42 @@ var Coordinator = class {
|
|
|
3349
4513
|
if (!legal[task.state]?.includes(newState)) {
|
|
3350
4514
|
return { error: rpcError(E.PARAMS, `Illegal transition ${task.state} -> ${newState}`) };
|
|
3351
4515
|
}
|
|
3352
|
-
if (newState === "completed" && task
|
|
4516
|
+
if (newState === "completed" && reviewRequired(ws, task)) {
|
|
3353
4517
|
return { error: rpcError(
|
|
3354
4518
|
E.PARAMS,
|
|
3355
4519
|
"This task requires review. Submit the output with task.complete, which opens the review; a reviewer decision completes it."
|
|
3356
4520
|
) };
|
|
3357
4521
|
}
|
|
4522
|
+
if (newState === "completed" && task.review) {
|
|
4523
|
+
return { error: rpcError(
|
|
4524
|
+
E.PARAMS,
|
|
4525
|
+
"This task holds a review. Submit the output with task.complete, which opens a new round; a reviewer decision completes it."
|
|
4526
|
+
) };
|
|
4527
|
+
}
|
|
4528
|
+
if (task.state === "review_requested" && !task.review.authors.includes(p.from)) {
|
|
4529
|
+
return { error: rpcError(
|
|
4530
|
+
E.NOT_AUTHORISED,
|
|
4531
|
+
"Only the authors of the work under review take it back from review"
|
|
4532
|
+
) };
|
|
4533
|
+
}
|
|
4534
|
+
if (newState === "cancelled" && openRequiredReviewAuthors(ws, task).includes(p.from)) {
|
|
4535
|
+
return { error: rpcError(
|
|
4536
|
+
E.NOT_AUTHORISED,
|
|
4537
|
+
"The authors of the work under review cannot end its review without a decision"
|
|
4538
|
+
) };
|
|
4539
|
+
}
|
|
4540
|
+
if (newState === "declined" && task.review && !task.review.decisions.some((d) => d.kind === "reject")) {
|
|
4541
|
+
return { error: rpcError(
|
|
4542
|
+
E.PARAMS,
|
|
4543
|
+
"This task holds a review no decision has closed; a reviewer's decision ends it"
|
|
4544
|
+
) };
|
|
4545
|
+
}
|
|
4546
|
+
if (newState === "declined" && !task.review && openRequiredReviewAuthors(ws, task).length > 0) {
|
|
4547
|
+
return { error: rpcError(
|
|
4548
|
+
E.PARAMS,
|
|
4549
|
+
"This task carries on a review no decision has closed; completing it opens the review a reviewer decides"
|
|
4550
|
+
) };
|
|
4551
|
+
}
|
|
3358
4552
|
task.state = newState;
|
|
3359
4553
|
task.updated_at = this.now();
|
|
3360
4554
|
task.history.push({
|
|
@@ -3375,32 +4569,60 @@ var Coordinator = class {
|
|
|
3375
4569
|
if (task.state !== "created" && task.state !== "in_progress") {
|
|
3376
4570
|
return { error: rpcError(E.PARAMS, `Cannot complete task in state: ${task.state}`) };
|
|
3377
4571
|
}
|
|
3378
|
-
if (
|
|
4572
|
+
if (p.confidence != null && typeof p.confidence !== "number" && typeof p.confidence !== "string") {
|
|
4573
|
+
return { error: rpcError(E.PARAMS, "confidence must be a number or a string") };
|
|
4574
|
+
}
|
|
4575
|
+
const required = reviewRequired(ws, task);
|
|
4576
|
+
if (required && p.from !== task.assignee) {
|
|
4577
|
+
return { error: rpcError(E.NOT_AUTHORISED, "Only the assignee completes a task that requires review") };
|
|
4578
|
+
}
|
|
4579
|
+
if (required || task.review) {
|
|
3379
4580
|
const now = this.now();
|
|
4581
|
+
const authors = authorsOf(task, p.from);
|
|
4582
|
+
let note = OPENED_ON_COMPLETE;
|
|
3380
4583
|
if (!task.review) {
|
|
3381
|
-
const
|
|
3382
|
-
const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
|
|
4584
|
+
const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => !authors.includes(uri) && member.type === "human").map(([uri]) => uri);
|
|
3383
4585
|
if (eligible.length === 0) {
|
|
3384
4586
|
return { error: rpcError(
|
|
3385
4587
|
E.NOT_AUTHORISED,
|
|
3386
4588
|
"Task requires review but has no eligible human reviewer (needs a human member other than its assignee and completer)"
|
|
3387
4589
|
) };
|
|
3388
4590
|
}
|
|
3389
|
-
task.review = {
|
|
4591
|
+
task.review = {
|
|
4592
|
+
requested_at: now,
|
|
4593
|
+
requested_to: eligible,
|
|
4594
|
+
rule: ws.review_rule ?? "any_one_approves",
|
|
4595
|
+
decisions: [],
|
|
4596
|
+
authors,
|
|
4597
|
+
acknowledgements: []
|
|
4598
|
+
};
|
|
4599
|
+
} else {
|
|
4600
|
+
const roundAuthors = unionOf(task.review.authors, authors);
|
|
4601
|
+
const reviewers = task.review.requested_to.filter((r) => !roundAuthors.includes(r) && !(required && rawMember(ws, r) !== void 0 && rawMember(ws, r).type !== "human"));
|
|
4602
|
+
if (reviewers.length === 0) {
|
|
4603
|
+
return { error: rpcError(
|
|
4604
|
+
E.NOT_AUTHORISED,
|
|
4605
|
+
"No reviewer of this task remains once the authors of the work are left out"
|
|
4606
|
+
) };
|
|
4607
|
+
}
|
|
4608
|
+
task.review = {
|
|
4609
|
+
...task.review,
|
|
4610
|
+
requested_at: now,
|
|
4611
|
+
requested_to: reviewers,
|
|
4612
|
+
decisions: [],
|
|
4613
|
+
authors: roundAuthors,
|
|
4614
|
+
acknowledgements: []
|
|
4615
|
+
};
|
|
4616
|
+
note = NEW_ROUND_ON_COMPLETE;
|
|
3390
4617
|
}
|
|
3391
4618
|
task.pending_artefact = p.output;
|
|
3392
4619
|
task.state = "review_requested";
|
|
3393
4620
|
task.updated_at = now;
|
|
3394
|
-
task.history.push({
|
|
3395
|
-
ts: now,
|
|
3396
|
-
from: p.from,
|
|
3397
|
-
state: "review_requested",
|
|
3398
|
-
note: "review required; opened on task.complete"
|
|
3399
|
-
});
|
|
4621
|
+
task.history.push({ ts: now, from: p.from, state: "review_requested", note });
|
|
3400
4622
|
return { result: { state: "review_requested", review_id: task.id } };
|
|
3401
4623
|
}
|
|
3402
4624
|
task.output = p.output;
|
|
3403
|
-
if (
|
|
4625
|
+
if (p.confidence != null) task.confidence = p.confidence;
|
|
3404
4626
|
task.state = "completed";
|
|
3405
4627
|
task.updated_at = this.now();
|
|
3406
4628
|
task.history.push({ ts: task.updated_at, from: p.from, state: "completed" });
|
|
@@ -3413,8 +4635,16 @@ var Coordinator = class {
|
|
|
3413
4635
|
const notMember = this.requireMember(ws, p.from);
|
|
3414
4636
|
if (notMember) return notMember;
|
|
3415
4637
|
}
|
|
3416
|
-
const
|
|
3417
|
-
|
|
4638
|
+
const bad = paramsError(p, [
|
|
4639
|
+
["range.from_seq", "integer"],
|
|
4640
|
+
["range.to_seq", "integer"],
|
|
4641
|
+
["filter.method", "string"],
|
|
4642
|
+
["filter.from", "string"],
|
|
4643
|
+
["filter.task_id", "string"]
|
|
4644
|
+
]);
|
|
4645
|
+
if (bad) return bad;
|
|
4646
|
+
const range = isObject(p.range) ? p.range : {};
|
|
4647
|
+
const filter = isObject(p.filter) ? p.filter : {};
|
|
3418
4648
|
if (filter.outcome != null && filter.outcome !== "accepted" && filter.outcome !== "refused") {
|
|
3419
4649
|
return { error: rpcError(E.PARAMS, "filter.outcome must be 'accepted' or 'refused'") };
|
|
3420
4650
|
}
|
|
@@ -3465,37 +4695,73 @@ var Coordinator = class {
|
|
|
3465
4695
|
return { error: rpcError(E.NOT_REVIEWABLE, `Cannot request review for task in state: ${task.state}`) };
|
|
3466
4696
|
}
|
|
3467
4697
|
const to = p.to;
|
|
4698
|
+
if (to != null && typeof to !== "string" && !(Array.isArray(to) && to.every((r) => typeof r === "string"))) {
|
|
4699
|
+
return { error: rpcError(E.PARAMS, "to must be a string or a list of strings") };
|
|
4700
|
+
}
|
|
3468
4701
|
const reviewers = Array.isArray(to) ? to : typeof to === "string" ? [to] : [];
|
|
3469
4702
|
if (!reviewers.length) return { error: rpcError(E.PARAMS, "review.request needs 'to'") };
|
|
4703
|
+
const bad = paramsError(p, [["rule", "string"]]);
|
|
4704
|
+
if (bad) return bad;
|
|
3470
4705
|
const now = this.now();
|
|
3471
4706
|
const declaredRule = p.rule;
|
|
3472
4707
|
const rule = declaredRule || "any_one_approves";
|
|
3473
4708
|
if (!reviewRuleSupported(rule)) {
|
|
3474
4709
|
return { error: rpcError(E.PARAMS, `Unsupported review rule ${JSON.stringify(rule)}: review/1.0 supports any_one_approves, all_approve, quorum:<n>`) };
|
|
3475
4710
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
4711
|
+
const open = task.state === "review_requested" ? task.review : void 0;
|
|
4712
|
+
if (open) {
|
|
4713
|
+
if (contentHash(task.pending_artefact ?? null) !== contentHash(p.artefact ?? null)) {
|
|
3478
4714
|
return { error: rpcError(
|
|
3479
4715
|
E.REVIEW_ALREADY_OPEN,
|
|
3480
4716
|
"A review is already open on this task with different content. Decide, abstain, escalate or cancel it before requesting review of a new artefact."
|
|
3481
4717
|
) };
|
|
3482
4718
|
}
|
|
3483
|
-
if (declaredRule
|
|
4719
|
+
if (declaredRule != null && declaredRule !== open.rule) {
|
|
3484
4720
|
return { error: rpcError(
|
|
3485
4721
|
E.REVIEW_ALREADY_OPEN,
|
|
3486
|
-
`Cannot change the decision rule of an open review (currently ${
|
|
4722
|
+
`Cannot change the decision rule of an open review (currently ${open.rule})`
|
|
4723
|
+
) };
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
const authors = open ? open.authors : unionOf(task.review ? task.review.authors : [], authorsOf(task, lastCompleter(task)));
|
|
4727
|
+
if (reviewRequired(ws, task)) {
|
|
4728
|
+
if (!open) {
|
|
4729
|
+
return { error: rpcError(
|
|
4730
|
+
E.NOT_AUTHORISED,
|
|
4731
|
+
"This task requires a person's review, which opens on task.complete; review.request only adds reviewers to it once it is open"
|
|
4732
|
+
) };
|
|
4733
|
+
}
|
|
4734
|
+
if (authors.includes(p.from)) {
|
|
4735
|
+
return { error: rpcError(E.NOT_AUTHORISED, "The author of the work under review cannot request its review") };
|
|
4736
|
+
}
|
|
4737
|
+
const scope = reviewers.find(isScope);
|
|
4738
|
+
if (scope !== void 0) {
|
|
4739
|
+
return { error: rpcError(
|
|
4740
|
+
E.PARAMS,
|
|
4741
|
+
`A review that requires a person names its reviewers, and ${scope} is an address scope`
|
|
3487
4742
|
) };
|
|
3488
4743
|
}
|
|
3489
4744
|
for (const r of reviewers) {
|
|
3490
|
-
if (
|
|
4745
|
+
if (authors.includes(r)) {
|
|
4746
|
+
return { error: rpcError(E.NOT_AUTHORISED, `The author of the work under review cannot review it: ${r}`) };
|
|
4747
|
+
}
|
|
4748
|
+
const member = rawMember(ws, r);
|
|
4749
|
+
if (member && member.type !== "human") {
|
|
4750
|
+
return { error: rpcError(E.NOT_AUTHORISED, `This task requires a person's review: ${r} is not a human member`) };
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
if (open) {
|
|
4755
|
+
for (const r of reviewers) {
|
|
4756
|
+
if (!open.requested_to.includes(r)) open.requested_to.push(r);
|
|
3491
4757
|
}
|
|
3492
|
-
if (p.deadline
|
|
4758
|
+
if (p.deadline != null) open.deadline = p.deadline;
|
|
3493
4759
|
task.updated_at = now;
|
|
3494
4760
|
return { result: {
|
|
3495
4761
|
state: "review_requested",
|
|
3496
4762
|
review_id: task.id,
|
|
3497
4763
|
amended: true,
|
|
3498
|
-
requested_to: [...
|
|
4764
|
+
requested_to: [...open.requested_to]
|
|
3499
4765
|
} };
|
|
3500
4766
|
}
|
|
3501
4767
|
task.state = "review_requested";
|
|
@@ -3505,12 +4771,41 @@ var Coordinator = class {
|
|
|
3505
4771
|
requested_to: reviewers,
|
|
3506
4772
|
rule,
|
|
3507
4773
|
deadline: p.deadline,
|
|
3508
|
-
decisions: []
|
|
4774
|
+
decisions: [],
|
|
4775
|
+
authors,
|
|
4776
|
+
acknowledgements: []
|
|
3509
4777
|
};
|
|
3510
4778
|
task.history.push({ ts: now, from: p.from, state: "review_requested" });
|
|
3511
4779
|
task.pending_artefact = p.artefact;
|
|
3512
4780
|
return { result: { state: "review_requested", review_id: task.id } };
|
|
3513
4781
|
}
|
|
4782
|
+
/**
|
|
4783
|
+
* review/1.0 S3.6: a reviewer says they have begun the open review. Only
|
|
4784
|
+
* someone who may decide it can, so the log's record of who took a review
|
|
4785
|
+
* up, and when, names reviewers alone. It changes nothing else: the review
|
|
4786
|
+
* stays open and the acknowledgement counts towards no rule.
|
|
4787
|
+
*/
|
|
4788
|
+
opReviewAcknowledge(p) {
|
|
4789
|
+
const miss = missing(p, ["workspace", "from", "task_id"]);
|
|
4790
|
+
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
4791
|
+
const ws = this.workspaces.get(p.workspace);
|
|
4792
|
+
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
4793
|
+
const notMember = this.requireMember(ws, p.from);
|
|
4794
|
+
if (notMember) return notMember;
|
|
4795
|
+
const task = ws.tasks.get(p.task_id);
|
|
4796
|
+
if (!task) return { error: rpcError(E.PARAMS, "Unknown task") };
|
|
4797
|
+
if (task.state !== "review_requested") {
|
|
4798
|
+
return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
|
|
4799
|
+
}
|
|
4800
|
+
const notReviewer = this.requireReviewer(ws, task, p.from);
|
|
4801
|
+
if (notReviewer) return notReviewer;
|
|
4802
|
+
const review = task.review;
|
|
4803
|
+
if (review.acknowledgements.some((a) => a.reviewer === p.from)) {
|
|
4804
|
+
return { error: rpcError(E.PARAMS, "Already acknowledged in this round") };
|
|
4805
|
+
}
|
|
4806
|
+
review.acknowledgements.push({ reviewer: p.from, ts: this.now() });
|
|
4807
|
+
return { result: { acknowledged: true, requested_at: review.requested_at } };
|
|
4808
|
+
}
|
|
3514
4809
|
/**
|
|
3515
4810
|
* Verify an optional `approved_artefact_digest` against the artefact under
|
|
3516
4811
|
* review (review/1.0 S3.2).
|
|
@@ -3529,7 +4824,7 @@ var Coordinator = class {
|
|
|
3529
4824
|
if (typeof declared !== "string") {
|
|
3530
4825
|
return { error: rpcError(E.PARAMS, "approved_artefact_digest must be a string") };
|
|
3531
4826
|
}
|
|
3532
|
-
if (task.pending_artefact === void 0) {
|
|
4827
|
+
if (task.pending_artefact === void 0 || task.pending_artefact === null) {
|
|
3533
4828
|
return { error: rpcError(E.PARAMS, "No artefact under review to bind a digest to") };
|
|
3534
4829
|
}
|
|
3535
4830
|
const actual = contentHash(task.pending_artefact);
|
|
@@ -3545,8 +4840,8 @@ var Coordinator = class {
|
|
|
3545
4840
|
opDecide(p, kind) {
|
|
3546
4841
|
const miss = missing(p, ["workspace", "from", "task_id"]);
|
|
3547
4842
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
3548
|
-
const
|
|
3549
|
-
if (
|
|
4843
|
+
const bad = paramsError(p, [["tags", "strings"], ["request_revision", "boolean"]]);
|
|
4844
|
+
if (bad) return bad;
|
|
3550
4845
|
const ws = this.workspaces.get(p.workspace);
|
|
3551
4846
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
3552
4847
|
const notMember = this.requireMember(ws, p.from);
|
|
@@ -3556,7 +4851,7 @@ var Coordinator = class {
|
|
|
3556
4851
|
if (task.state !== "review_requested") {
|
|
3557
4852
|
return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
|
|
3558
4853
|
}
|
|
3559
|
-
const notReviewer = this.requireReviewer(task, p.from);
|
|
4854
|
+
const notReviewer = this.requireReviewer(ws, task, p.from);
|
|
3560
4855
|
if (notReviewer) return notReviewer;
|
|
3561
4856
|
const digestError = this.checkArtefactDigest(p, task);
|
|
3562
4857
|
if (digestError) return digestError;
|
|
@@ -3583,8 +4878,8 @@ var Coordinator = class {
|
|
|
3583
4878
|
opDecideOverride(p) {
|
|
3584
4879
|
const miss = missing(p, ["workspace", "from", "task_id", "diff", "rationale"]);
|
|
3585
4880
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
3586
|
-
const
|
|
3587
|
-
if (
|
|
4881
|
+
const bad = paramsError(p, [["tags", "strings"], ["policy_refs", "strings"]]);
|
|
4882
|
+
if (bad) return bad;
|
|
3588
4883
|
const ws = this.workspaces.get(p.workspace);
|
|
3589
4884
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
3590
4885
|
const notMember = this.requireMember(ws, p.from);
|
|
@@ -3594,15 +4889,17 @@ var Coordinator = class {
|
|
|
3594
4889
|
if (task.state !== "review_requested") {
|
|
3595
4890
|
return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
|
|
3596
4891
|
}
|
|
3597
|
-
const notReviewer = this.requireReviewer(task, p.from);
|
|
4892
|
+
const notReviewer = this.requireReviewer(ws, task, p.from);
|
|
3598
4893
|
if (notReviewer) return notReviewer;
|
|
3599
4894
|
const digestError = this.checkArtefactDigest(p, task);
|
|
3600
4895
|
if (digestError) return digestError;
|
|
3601
4896
|
const base = task.pending_artefact;
|
|
3602
|
-
if (base === void 0) return { error: rpcError(E.PARAMS, "No base artefact for override") };
|
|
4897
|
+
if (base === void 0 || base === null) return { error: rpcError(E.PARAMS, "No base artefact for override") };
|
|
3603
4898
|
let applied;
|
|
3604
4899
|
try {
|
|
3605
|
-
applied = applyJsonPatch(base, p.diff
|
|
4900
|
+
applied = applyJsonPatch(base, p.diff, {
|
|
4901
|
+
maxStringBytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES
|
|
4902
|
+
});
|
|
3606
4903
|
} catch (e) {
|
|
3607
4904
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3608
4905
|
return { error: rpcError(E.PATCH_FAILED, msg) };
|
|
@@ -3641,6 +4938,8 @@ var Coordinator = class {
|
|
|
3641
4938
|
opAbstainDeclare(p) {
|
|
3642
4939
|
const miss = missing(p, ["workspace", "from", "task_id", "reason"]);
|
|
3643
4940
|
if (miss) return { error: rpcError(E.PARAMS, `Missing field: ${miss}`) };
|
|
4941
|
+
const bad = paramsError(p, [["category", "string"]]);
|
|
4942
|
+
if (bad) return bad;
|
|
3644
4943
|
const ws = this.workspaces.get(p.workspace);
|
|
3645
4944
|
if (!ws) return { error: rpcError(E.PARAMS, "Unknown workspace") };
|
|
3646
4945
|
const notMember = this.requireMember(ws, p.from);
|
|
@@ -3650,7 +4949,7 @@ var Coordinator = class {
|
|
|
3650
4949
|
if (task.state !== "review_requested") {
|
|
3651
4950
|
return { error: rpcError(E.NOT_REVIEWABLE, `Task not awaiting review: ${task.state}`) };
|
|
3652
4951
|
}
|
|
3653
|
-
const notReviewer = this.requireReviewer(task, p.from);
|
|
4952
|
+
const notReviewer = this.requireReviewer(ws, task, p.from);
|
|
3654
4953
|
if (notReviewer) return notReviewer;
|
|
3655
4954
|
const now = this.now();
|
|
3656
4955
|
task.review.decisions.push({
|
|
@@ -3682,6 +4981,8 @@ var Coordinator = class {
|
|
|
3682
4981
|
return { error: rpcError(E.PARAMS, "new_task must be an object") };
|
|
3683
4982
|
}
|
|
3684
4983
|
const nt = p.new_task;
|
|
4984
|
+
const bad = paramsError(p, [["new_task.kind", "string"]]);
|
|
4985
|
+
if (bad) return bad;
|
|
3685
4986
|
const assignee = nt.assignee;
|
|
3686
4987
|
if (typeof assignee !== "string" || !ws.members.has(assignee)) {
|
|
3687
4988
|
return { error: rpcError(E.PARAMS, "Escalation assignee not in workspace") };
|
|
@@ -3716,9 +5017,10 @@ var Coordinator = class {
|
|
|
3716
5017
|
history: [{ ts: now, from: p.from, state: "created", note: `escalated from ${orig.id}` }],
|
|
3717
5018
|
paused: false
|
|
3718
5019
|
};
|
|
3719
|
-
if (orig.review_required || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
|
|
5020
|
+
if (orig.review_required || orig.review !== void 0 || ws.profiles.some((pr) => pr.startsWith("modes/")) && newTask.mode === "trial") {
|
|
3720
5021
|
newTask.review_required = true;
|
|
3721
5022
|
}
|
|
5023
|
+
newTask.prior_authors = successorAuthors(orig, p.from);
|
|
3722
5024
|
ws.tasks.set(newId, newTask);
|
|
3723
5025
|
orig.state = "escalated";
|
|
3724
5026
|
orig.updated_at = now;
|
|
@@ -3735,8 +5037,8 @@ function memberToDict(m) {
|
|
|
3735
5037
|
joined: m.joined
|
|
3736
5038
|
};
|
|
3737
5039
|
if (m.display_name) out.display_name = m.display_name;
|
|
3738
|
-
if (m.capabilities) out.capabilities = m.capabilities;
|
|
3739
|
-
if (m.scopes) out.scopes = m.scopes;
|
|
5040
|
+
if (m.capabilities && Object.keys(m.capabilities).length) out.capabilities = m.capabilities;
|
|
5041
|
+
if (m.scopes?.length) out.scopes = m.scopes;
|
|
3740
5042
|
if (m.keys.length) {
|
|
3741
5043
|
out.jwks = { keys: m.keys.filter((k) => !k.revoked_at && !k.valid_until).map((k) => k.jwk) };
|
|
3742
5044
|
out.key_history = m.keys;
|
|
@@ -3748,7 +5050,6 @@ function memberToDict(m) {
|
|
|
3748
5050
|
}
|
|
3749
5051
|
|
|
3750
5052
|
// src/policy.ts
|
|
3751
|
-
var CRIT2 = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
3752
5053
|
function makeDefaultPolicy(seniorTarget) {
|
|
3753
5054
|
const routingPolicy = (task, candidates) => {
|
|
3754
5055
|
const eligible = candidates;
|
|
@@ -3766,15 +5067,8 @@ function makeDefaultPolicy(seniorTarget) {
|
|
|
3766
5067
|
};
|
|
3767
5068
|
};
|
|
3768
5069
|
const reviewDepthPolicy = (_task, hints) => {
|
|
3769
|
-
const
|
|
3770
|
-
|
|
3771
|
-
try {
|
|
3772
|
-
conf = Number(hints.confidence ?? 1);
|
|
3773
|
-
} catch {
|
|
3774
|
-
conf = 1;
|
|
3775
|
-
}
|
|
3776
|
-
if (isNaN(conf)) conf = 1;
|
|
3777
|
-
const r = CRIT2[crit] ?? 1;
|
|
5070
|
+
const conf = confidenceOf(hints);
|
|
5071
|
+
const r = criticalityOf(hints);
|
|
3778
5072
|
if (r >= 3) return {
|
|
3779
5073
|
depth: "full",
|
|
3780
5074
|
rationale: { policy_id: "default", summary: "criticality=critical: full review" }
|
|
@@ -3799,15 +5093,8 @@ function makeDefaultPolicy(seniorTarget) {
|
|
|
3799
5093
|
};
|
|
3800
5094
|
};
|
|
3801
5095
|
const escalationPolicy = (_task, hints) => {
|
|
3802
|
-
const
|
|
3803
|
-
|
|
3804
|
-
try {
|
|
3805
|
-
conf = Number(hints.confidence ?? 1);
|
|
3806
|
-
} catch {
|
|
3807
|
-
conf = 1;
|
|
3808
|
-
}
|
|
3809
|
-
if (isNaN(conf)) conf = 1;
|
|
3810
|
-
const r = CRIT2[crit] ?? 1;
|
|
5096
|
+
const conf = confidenceOf(hints);
|
|
5097
|
+
const r = criticalityOf(hints);
|
|
3811
5098
|
const escalate = r >= 3 || r >= 2 && conf < 0.6;
|
|
3812
5099
|
return {
|
|
3813
5100
|
escalate,
|
|
@@ -3827,6 +5114,19 @@ function wrapMcpToolCall(coord, workspace, options) {
|
|
|
3827
5114
|
if (!workspace) throw new Error("workspace is required");
|
|
3828
5115
|
if (!options.caller) throw new Error("caller is required");
|
|
3829
5116
|
if (!options.tool) throw new Error("tool is required");
|
|
5117
|
+
if (options.reviewRequired) {
|
|
5118
|
+
const described = coord.dispatch({
|
|
5119
|
+
jsonrpc: "2.0",
|
|
5120
|
+
id: `wrap-${options.tool}-describe`,
|
|
5121
|
+
method: "workspace.describe",
|
|
5122
|
+
params: { workspace, from: options.caller }
|
|
5123
|
+
});
|
|
5124
|
+
if (described.error) throw new Error(`workspace.describe failed: ${JSON.stringify(described.error)}`);
|
|
5125
|
+
const members = described.result.members;
|
|
5126
|
+
if (!members.some((m) => m.type === "human" && m.uri !== options.caller)) {
|
|
5127
|
+
throw new Error("review_required needs a human member other than the caller to review the result; nothing was recorded");
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
3830
5130
|
const inputHash = contentHash(options.args);
|
|
3831
5131
|
const outputHash = contentHash(options.result);
|
|
3832
5132
|
const kind = options.taskKind ?? `mcp_call:${options.tool}`;
|
|
@@ -3845,6 +5145,7 @@ function wrapMcpToolCall(coord, workspace, options) {
|
|
|
3845
5145
|
input: options.args
|
|
3846
5146
|
};
|
|
3847
5147
|
if (options.routingHints) createParams.routing_hints = { ...options.routingHints };
|
|
5148
|
+
if (options.reviewRequired) createParams.review_required = true;
|
|
3848
5149
|
const createResp = coord.dispatch({
|
|
3849
5150
|
jsonrpc: "2.0",
|
|
3850
5151
|
id: `wrap-${options.tool}-create`,
|
|
@@ -3874,7 +5175,7 @@ function wrapMcpToolCall(coord, workspace, options) {
|
|
|
3874
5175
|
method: "task.complete",
|
|
3875
5176
|
params: completeParams
|
|
3876
5177
|
});
|
|
3877
|
-
if (completeResp.error) throw new Error(`task.complete failed: ${JSON.stringify(completeResp.error)}`);
|
|
5178
|
+
if (completeResp.error) throw new Error(`task.complete failed for ${taskId}: ${JSON.stringify(completeResp.error)}`);
|
|
3878
5179
|
return { task_id: taskId, input_hash: inputHash, output_hash: outputHash };
|
|
3879
5180
|
}
|
|
3880
5181
|
function wrapA2aMessageExchange(coord, workspace, options) {
|
|
@@ -3939,6 +5240,7 @@ function wrapA2aMessageExchange(coord, workspace, options) {
|
|
|
3939
5240
|
0 && (module.exports = {
|
|
3940
5241
|
Coordinator,
|
|
3941
5242
|
CoordinatorError,
|
|
5243
|
+
DEFAULT_MAX_PATCHED_BYTES,
|
|
3942
5244
|
E,
|
|
3943
5245
|
IdFactory,
|
|
3944
5246
|
MemoryStore,
|