@sanctuary-framework/mcp-server 1.2.4 → 1.2.6
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/dist/cli.cjs +1873 -123
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1873 -123
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1582 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +391 -4
- package/dist/index.d.ts +391 -4
- package/dist/index.js +1582 -102
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4261,6 +4261,10 @@ var DEFAULT_CHANNEL = {
|
|
|
4261
4261
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4262
4262
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4263
4263
|
};
|
|
4264
|
+
var DEFAULT_APPROVAL_REDIRECT = {
|
|
4265
|
+
enabled: false,
|
|
4266
|
+
mode: "replace"
|
|
4267
|
+
};
|
|
4264
4268
|
var DEFAULT_POLICY = {
|
|
4265
4269
|
version: 1,
|
|
4266
4270
|
tier1_always_approve: [
|
|
@@ -4334,6 +4338,7 @@ var DEFAULT_POLICY = {
|
|
|
4334
4338
|
"handshake_status",
|
|
4335
4339
|
"handshake_exchange",
|
|
4336
4340
|
"handshake_verify_attestation",
|
|
4341
|
+
"handshake_abort",
|
|
4337
4342
|
"reputation_query_weighted",
|
|
4338
4343
|
"federation_peers",
|
|
4339
4344
|
"federation_trust_evaluate",
|
|
@@ -4375,7 +4380,8 @@ var DEFAULT_POLICY = {
|
|
|
4375
4380
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4376
4381
|
// Read-only; rule-based Annex III classifier
|
|
4377
4382
|
],
|
|
4378
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4383
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4384
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4379
4385
|
};
|
|
4380
4386
|
function extractOperationName(toolName) {
|
|
4381
4387
|
if (toolName.startsWith("proxy/")) {
|
|
@@ -4472,9 +4478,35 @@ function validatePolicy(raw) {
|
|
|
4472
4478
|
};
|
|
4473
4479
|
delete merged.auto_deny;
|
|
4474
4480
|
return merged;
|
|
4475
|
-
})()
|
|
4481
|
+
})(),
|
|
4482
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4476
4483
|
};
|
|
4477
4484
|
}
|
|
4485
|
+
function parseApprovalRedirect(raw) {
|
|
4486
|
+
if (raw === void 0 || raw === null) {
|
|
4487
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4488
|
+
}
|
|
4489
|
+
if (typeof raw !== "object") {
|
|
4490
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4491
|
+
}
|
|
4492
|
+
const obj = raw;
|
|
4493
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4494
|
+
const modeRaw = obj.mode;
|
|
4495
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4496
|
+
if (modeRaw !== void 0) {
|
|
4497
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4498
|
+
throw new Error(
|
|
4499
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
mode = modeRaw;
|
|
4503
|
+
}
|
|
4504
|
+
const result = { enabled, mode };
|
|
4505
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4506
|
+
result.per_agent = obj.per_agent;
|
|
4507
|
+
}
|
|
4508
|
+
return result;
|
|
4509
|
+
}
|
|
4478
4510
|
function generateDefaultPolicyYaml() {
|
|
4479
4511
|
return `# Sanctuary Principal Policy v1
|
|
4480
4512
|
# This file controls what your agent can do without asking.
|
|
@@ -4553,6 +4585,7 @@ tier3_always_allow:
|
|
|
4553
4585
|
- handshake_status
|
|
4554
4586
|
- handshake_exchange
|
|
4555
4587
|
- handshake_verify_attestation
|
|
4588
|
+
- handshake_abort
|
|
4556
4589
|
- reputation_query_weighted
|
|
4557
4590
|
- federation_peers
|
|
4558
4591
|
- federation_trust_evaluate
|
|
@@ -4587,6 +4620,21 @@ tier3_always_allow:
|
|
|
4587
4620
|
approval_channel:
|
|
4588
4621
|
type: stderr
|
|
4589
4622
|
timeout_seconds: 300
|
|
4623
|
+
|
|
4624
|
+
# \u2500\u2500\u2500 Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4625
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4626
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4627
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4628
|
+
#
|
|
4629
|
+
# mode:
|
|
4630
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4631
|
+
# decision from the inbox (default once enabled).
|
|
4632
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4633
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4634
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4635
|
+
approval_redirect:
|
|
4636
|
+
enabled: false
|
|
4637
|
+
mode: replace
|
|
4590
4638
|
`;
|
|
4591
4639
|
}
|
|
4592
4640
|
var MalformedPrincipalPolicyError = class extends Error {
|
|
@@ -16271,6 +16319,27 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
16271
16319
|
await handleStream2(deps, res);
|
|
16272
16320
|
return true;
|
|
16273
16321
|
}
|
|
16322
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
|
|
16323
|
+
const limit = parseLimit2(
|
|
16324
|
+
url.searchParams.get("limit"),
|
|
16325
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
16326
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
16327
|
+
);
|
|
16328
|
+
const statusRaw = url.searchParams.get("status");
|
|
16329
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
16330
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16331
|
+
const filterStatus = statusRaw && isStatusFilter(statusRaw) && statusRaw !== "pending" ? statusRaw : void 0;
|
|
16332
|
+
const entries = await deps.aggregator.getHistory(
|
|
16333
|
+
{
|
|
16334
|
+
limit,
|
|
16335
|
+
...filterStatus !== void 0 ? { status: filterStatus } : {},
|
|
16336
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
16337
|
+
},
|
|
16338
|
+
operatorId
|
|
16339
|
+
);
|
|
16340
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
16341
|
+
return true;
|
|
16342
|
+
}
|
|
16274
16343
|
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
16275
16344
|
const limit = parseLimit2(
|
|
16276
16345
|
url.searchParams.get("limit"),
|
|
@@ -16293,11 +16362,39 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
16293
16362
|
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16294
16363
|
return true;
|
|
16295
16364
|
}
|
|
16296
|
-
if (method === "GET" && entryMatch.action ===
|
|
16297
|
-
const
|
|
16298
|
-
|
|
16299
|
-
(
|
|
16365
|
+
if (method === "GET" && entryMatch.action === "audit-trail") {
|
|
16366
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
16367
|
+
if (!entry) {
|
|
16368
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16369
|
+
return true;
|
|
16370
|
+
}
|
|
16371
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16372
|
+
const trail = await deps.aggregator.getAuditTrail(
|
|
16373
|
+
entryMatch.aggregatorId,
|
|
16374
|
+
operatorId
|
|
16375
|
+
);
|
|
16376
|
+
writeJSON4(res, 200, { ok: true, data: { entry, audit_trail: trail } });
|
|
16377
|
+
return true;
|
|
16378
|
+
}
|
|
16379
|
+
if (method === "GET" && entryMatch.action === "payload") {
|
|
16380
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
16381
|
+
if (!entry) {
|
|
16382
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16383
|
+
return true;
|
|
16384
|
+
}
|
|
16385
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16386
|
+
const payload = await deps.aggregator.getFullPayloadWithAudit(
|
|
16387
|
+
entryMatch.aggregatorId,
|
|
16388
|
+
operatorId
|
|
16300
16389
|
);
|
|
16390
|
+
writeJSON4(res, 200, {
|
|
16391
|
+
ok: true,
|
|
16392
|
+
data: { entry, request_payload: payload }
|
|
16393
|
+
});
|
|
16394
|
+
return true;
|
|
16395
|
+
}
|
|
16396
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
16397
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
16301
16398
|
if (!entry) {
|
|
16302
16399
|
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16303
16400
|
return true;
|
|
@@ -19284,7 +19381,10 @@ var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
|
19284
19381
|
var APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
19285
19382
|
AGGREGATED: "cross_harness_approval_aggregated",
|
|
19286
19383
|
RESOLVED: "cross_harness_approval_resolved",
|
|
19287
|
-
DEDUPED: "cross_harness_approval_deduped"
|
|
19384
|
+
DEDUPED: "cross_harness_approval_deduped",
|
|
19385
|
+
PAYLOAD_DECRYPTED: "cross_harness_approval_payload_decrypted",
|
|
19386
|
+
AUDIT_TRAIL_VIEWED: "cross_harness_approval_audit_trail_viewed",
|
|
19387
|
+
REPLAYED: "cross_harness_approval_replayed"
|
|
19288
19388
|
};
|
|
19289
19389
|
var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
19290
19390
|
var DEFAULT_MAX_LIST_LIMIT = 200;
|
|
@@ -19300,6 +19400,8 @@ var ApprovalAggregator = class {
|
|
|
19300
19400
|
now;
|
|
19301
19401
|
resolveSourceContext;
|
|
19302
19402
|
resolveHubInboxItemId;
|
|
19403
|
+
payloadStore;
|
|
19404
|
+
resolveEnforcementChain;
|
|
19303
19405
|
/** Cached entries by `aggregator_id`. */
|
|
19304
19406
|
entries = /* @__PURE__ */ new Map();
|
|
19305
19407
|
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
@@ -19329,6 +19431,14 @@ var ApprovalAggregator = class {
|
|
|
19329
19431
|
source_agent_id: this.fortressId
|
|
19330
19432
|
}));
|
|
19331
19433
|
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
19434
|
+
this.payloadStore = deps.payloadStore ?? null;
|
|
19435
|
+
this.resolveEnforcementChain = deps.resolveEnforcementChain ?? ((event) => [
|
|
19436
|
+
{
|
|
19437
|
+
layer: "l2",
|
|
19438
|
+
event: `approval_required:${event.operation}`,
|
|
19439
|
+
timestamp: event.request_timestamp
|
|
19440
|
+
}
|
|
19441
|
+
]);
|
|
19332
19442
|
}
|
|
19333
19443
|
/**
|
|
19334
19444
|
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
@@ -19379,13 +19489,152 @@ var ApprovalAggregator = class {
|
|
|
19379
19489
|
}
|
|
19380
19490
|
/**
|
|
19381
19491
|
* Return the original (unhashed) request payload for the entry. Returns
|
|
19382
|
-
* `null` when the entry is unknown
|
|
19383
|
-
*
|
|
19492
|
+
* `null` when the entry is unknown. When the in-memory payload map has
|
|
19493
|
+
* been evicted (e.g. after a server restart) and a `payloadStore` was
|
|
19494
|
+
* provided, the at-rest bundle is decrypted and the in-memory map is
|
|
19495
|
+
* refilled. Audit emission lives on the `*WithAudit` variant; this base
|
|
19496
|
+
* accessor is silent so internal callers can read without polluting the
|
|
19497
|
+
* audit trail.
|
|
19384
19498
|
*/
|
|
19385
19499
|
async getFullPayload(aggregatorId) {
|
|
19386
19500
|
await this.hydrate();
|
|
19387
19501
|
if (!this.entries.has(aggregatorId)) return null;
|
|
19388
|
-
|
|
19502
|
+
const cached = this.fullPayloads.get(aggregatorId);
|
|
19503
|
+
if (cached !== void 0) return cached;
|
|
19504
|
+
if (this.payloadStore) {
|
|
19505
|
+
try {
|
|
19506
|
+
const restored = await this.payloadStore.loadPayload(aggregatorId);
|
|
19507
|
+
if (restored !== null) {
|
|
19508
|
+
this.fullPayloads.set(aggregatorId, restored);
|
|
19509
|
+
return restored;
|
|
19510
|
+
}
|
|
19511
|
+
} catch {
|
|
19512
|
+
}
|
|
19513
|
+
}
|
|
19514
|
+
return null;
|
|
19515
|
+
}
|
|
19516
|
+
/**
|
|
19517
|
+
* Return the entry record for the given id, or null when unknown.
|
|
19518
|
+
* Idempotent. v1.3 Upsilon-3.
|
|
19519
|
+
*/
|
|
19520
|
+
async getEntry(aggregatorId) {
|
|
19521
|
+
await this.hydrate();
|
|
19522
|
+
return this.entries.get(aggregatorId) ?? null;
|
|
19523
|
+
}
|
|
19524
|
+
/**
|
|
19525
|
+
* Audited variant of `getFullPayload`. Emits the
|
|
19526
|
+
* `cross_harness_approval_payload_decrypted` audit event before
|
|
19527
|
+
* returning. Used by the operator-facing /payload replay route.
|
|
19528
|
+
* v1.3 Upsilon-3.
|
|
19529
|
+
*/
|
|
19530
|
+
async getFullPayloadWithAudit(aggregatorId, operatorId) {
|
|
19531
|
+
const payload = await this.getFullPayload(aggregatorId);
|
|
19532
|
+
if (payload === null) return null;
|
|
19533
|
+
const entry = this.entries.get(aggregatorId);
|
|
19534
|
+
this.auditLog.append(
|
|
19535
|
+
"l2",
|
|
19536
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.PAYLOAD_DECRYPTED,
|
|
19537
|
+
operatorId,
|
|
19538
|
+
{
|
|
19539
|
+
aggregator_id: aggregatorId,
|
|
19540
|
+
...entry ? {
|
|
19541
|
+
source_harness: entry.source_harness,
|
|
19542
|
+
source_agent_id: entry.source_agent_id,
|
|
19543
|
+
entry_status: entry.status
|
|
19544
|
+
} : {}
|
|
19545
|
+
}
|
|
19546
|
+
);
|
|
19547
|
+
return payload;
|
|
19548
|
+
}
|
|
19549
|
+
/**
|
|
19550
|
+
* Return the audit-log entries that led to and surround this approval.
|
|
19551
|
+
* Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
|
|
19552
|
+
* DEDUPED, replay events) all carry `details.aggregator_id` and link
|
|
19553
|
+
* directly. Gate-side emissions (`gate_*:operation`) do not carry the
|
|
19554
|
+
* aggregator id at v1.3, so they are matched via timestamp window
|
|
19555
|
+
* (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
|
|
19556
|
+
* pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
|
|
19557
|
+
* v1.3 Upsilon-3.
|
|
19558
|
+
*/
|
|
19559
|
+
async getAuditTrail(aggregatorId, operatorId) {
|
|
19560
|
+
await this.hydrate();
|
|
19561
|
+
const entry = this.entries.get(aggregatorId);
|
|
19562
|
+
if (!entry) {
|
|
19563
|
+
return [];
|
|
19564
|
+
}
|
|
19565
|
+
const sinceMs = Date.parse(entry.created_at) - 1e3;
|
|
19566
|
+
const sinceIso = new Date(sinceMs).toISOString();
|
|
19567
|
+
const queried = await this.auditLog.query({ since: sinceIso, limit: 1e3 });
|
|
19568
|
+
const operationPart = entry.policy_rule_id.includes(":") ? entry.policy_rule_id.slice(entry.policy_rule_id.indexOf(":") + 1) : entry.policy_rule_id;
|
|
19569
|
+
const lifetimeStart = sinceMs;
|
|
19570
|
+
const lifetimeEnd = entry.resolved_at ? Date.parse(entry.resolved_at) + 1e3 : Date.parse(entry.expires_at) + 1e3;
|
|
19571
|
+
const matches = [];
|
|
19572
|
+
for (const audit of queried.entries) {
|
|
19573
|
+
const detailsId = audit.details !== void 0 ? audit.details["aggregator_id"] : void 0;
|
|
19574
|
+
if (detailsId === aggregatorId) {
|
|
19575
|
+
matches.push(audit);
|
|
19576
|
+
continue;
|
|
19577
|
+
}
|
|
19578
|
+
const auditMs = Date.parse(audit.timestamp);
|
|
19579
|
+
if (auditMs < lifetimeStart || auditMs > lifetimeEnd) continue;
|
|
19580
|
+
if (audit.operation.endsWith(`:${operationPart}`)) {
|
|
19581
|
+
matches.push(audit);
|
|
19582
|
+
}
|
|
19583
|
+
}
|
|
19584
|
+
matches.sort(
|
|
19585
|
+
(a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
|
|
19586
|
+
);
|
|
19587
|
+
this.auditLog.append(
|
|
19588
|
+
"l2",
|
|
19589
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AUDIT_TRAIL_VIEWED,
|
|
19590
|
+
operatorId,
|
|
19591
|
+
{
|
|
19592
|
+
aggregator_id: aggregatorId,
|
|
19593
|
+
entry_status: entry.status,
|
|
19594
|
+
match_count: matches.length
|
|
19595
|
+
}
|
|
19596
|
+
);
|
|
19597
|
+
return matches;
|
|
19598
|
+
}
|
|
19599
|
+
/**
|
|
19600
|
+
* List historical (resolved) approvals. Excludes pending entries by
|
|
19601
|
+
* design: `list()` is the pending-inbox surface and `getHistory()` is
|
|
19602
|
+
* the resolved-replay surface. Emits REPLAYED on each call. v1.3
|
|
19603
|
+
* Upsilon-3.
|
|
19604
|
+
*/
|
|
19605
|
+
async getHistory(opts, operatorId) {
|
|
19606
|
+
await this.hydrate();
|
|
19607
|
+
await this.expireStale();
|
|
19608
|
+
const limit = Math.min(
|
|
19609
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
19610
|
+
this.maxListLimit
|
|
19611
|
+
);
|
|
19612
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
19613
|
+
const matching = [];
|
|
19614
|
+
for (const entry of this.entries.values()) {
|
|
19615
|
+
if (entry.status === "pending") continue;
|
|
19616
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
19617
|
+
const stamp = Date.parse(entry.resolved_at ?? entry.created_at);
|
|
19618
|
+
if (stamp < sinceMs) continue;
|
|
19619
|
+
matching.push(entry);
|
|
19620
|
+
}
|
|
19621
|
+
matching.sort((a, b) => {
|
|
19622
|
+
const aStamp = a.resolved_at ?? a.created_at;
|
|
19623
|
+
const bStamp = b.resolved_at ?? b.created_at;
|
|
19624
|
+
return bStamp.localeCompare(aStamp);
|
|
19625
|
+
});
|
|
19626
|
+
const sliced = matching.slice(0, limit);
|
|
19627
|
+
this.auditLog.append(
|
|
19628
|
+
"l2",
|
|
19629
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.REPLAYED,
|
|
19630
|
+
operatorId,
|
|
19631
|
+
{
|
|
19632
|
+
result_count: sliced.length,
|
|
19633
|
+
...opts?.status !== void 0 ? { status_filter: opts.status } : {},
|
|
19634
|
+
...opts?.sinceTs !== void 0 ? { since: opts.sinceTs } : {}
|
|
19635
|
+
}
|
|
19636
|
+
);
|
|
19637
|
+
return sliced;
|
|
19389
19638
|
}
|
|
19390
19639
|
/**
|
|
19391
19640
|
* Resolve an entry. Used by both:
|
|
@@ -19459,6 +19708,7 @@ var ApprovalAggregator = class {
|
|
|
19459
19708
|
const now = this.now();
|
|
19460
19709
|
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
19461
19710
|
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
19711
|
+
const enforcementChain = this.resolveEnforcementChain(event);
|
|
19462
19712
|
const entry = {
|
|
19463
19713
|
aggregator_id: id,
|
|
19464
19714
|
source_harness: ctx.source_harness,
|
|
@@ -19470,13 +19720,20 @@ var ApprovalAggregator = class {
|
|
|
19470
19720
|
status: "pending",
|
|
19471
19721
|
created_at: now.toISOString(),
|
|
19472
19722
|
expires_at: expires.toISOString(),
|
|
19473
|
-
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19723
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
|
|
19724
|
+
...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
|
|
19474
19725
|
};
|
|
19475
19726
|
this.entries.set(id, entry);
|
|
19476
19727
|
this.dedupIndex.set(dedupKey, id);
|
|
19477
19728
|
this.correlationIndex.set(event.correlation_id, id);
|
|
19478
19729
|
this.fullPayloads.set(id, event.context);
|
|
19479
19730
|
await this.persist(entry);
|
|
19731
|
+
if (this.payloadStore) {
|
|
19732
|
+
try {
|
|
19733
|
+
await this.payloadStore.savePayload(id, event.context);
|
|
19734
|
+
} catch {
|
|
19735
|
+
}
|
|
19736
|
+
}
|
|
19480
19737
|
this.auditLog.append(
|
|
19481
19738
|
"l2",
|
|
19482
19739
|
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
@@ -19622,6 +19879,306 @@ var ApprovalAggregator = class {
|
|
|
19622
19879
|
}
|
|
19623
19880
|
};
|
|
19624
19881
|
|
|
19882
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
19883
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
19884
|
+
function auditEntryIdFor(request) {
|
|
19885
|
+
return `${request.timestamp}:${request.operation}`;
|
|
19886
|
+
}
|
|
19887
|
+
function statusToDecision(entry) {
|
|
19888
|
+
switch (entry.status) {
|
|
19889
|
+
case "approved":
|
|
19890
|
+
return {
|
|
19891
|
+
decision: "approve",
|
|
19892
|
+
decided_by: "human"
|
|
19893
|
+
};
|
|
19894
|
+
case "denied":
|
|
19895
|
+
return {
|
|
19896
|
+
decision: "deny",
|
|
19897
|
+
decided_by: "human"
|
|
19898
|
+
};
|
|
19899
|
+
case "timeout":
|
|
19900
|
+
case "expired":
|
|
19901
|
+
return {
|
|
19902
|
+
decision: "deny",
|
|
19903
|
+
decided_by: "timeout"
|
|
19904
|
+
};
|
|
19905
|
+
default:
|
|
19906
|
+
return null;
|
|
19907
|
+
}
|
|
19908
|
+
}
|
|
19909
|
+
var AggregatorBackedChannel = class {
|
|
19910
|
+
underlying;
|
|
19911
|
+
aggregator;
|
|
19912
|
+
resolveRedirect;
|
|
19913
|
+
replaceModeTimeoutMs;
|
|
19914
|
+
now;
|
|
19915
|
+
constructor(opts) {
|
|
19916
|
+
this.underlying = opts.underlying;
|
|
19917
|
+
this.aggregator = opts.aggregator;
|
|
19918
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
19919
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
19920
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
19921
|
+
}
|
|
19922
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
19923
|
+
getUnderlying() {
|
|
19924
|
+
return this.underlying;
|
|
19925
|
+
}
|
|
19926
|
+
async requestApproval(request) {
|
|
19927
|
+
const cfg = this.resolveRedirect(request);
|
|
19928
|
+
if (!cfg.enabled) {
|
|
19929
|
+
return this.underlying.requestApproval(request);
|
|
19930
|
+
}
|
|
19931
|
+
if (cfg.mode === "replace") {
|
|
19932
|
+
return this.awaitAggregatorDecision(request);
|
|
19933
|
+
}
|
|
19934
|
+
return this.notifyMode(request);
|
|
19935
|
+
}
|
|
19936
|
+
/**
|
|
19937
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
19938
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
19939
|
+
* between list and subscribe). Match incoming events to this request by
|
|
19940
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
19941
|
+
*/
|
|
19942
|
+
async awaitAggregatorDecision(request) {
|
|
19943
|
+
const auditId = auditEntryIdFor(request);
|
|
19944
|
+
return new Promise((resolveOuter) => {
|
|
19945
|
+
let settled = false;
|
|
19946
|
+
let unsubscribe = null;
|
|
19947
|
+
let timeoutHandle = null;
|
|
19948
|
+
const settle = (response) => {
|
|
19949
|
+
if (settled) return;
|
|
19950
|
+
settled = true;
|
|
19951
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
19952
|
+
if (unsubscribe) {
|
|
19953
|
+
try {
|
|
19954
|
+
unsubscribe();
|
|
19955
|
+
} catch {
|
|
19956
|
+
}
|
|
19957
|
+
}
|
|
19958
|
+
resolveOuter(response);
|
|
19959
|
+
};
|
|
19960
|
+
const onEvent = (emit) => {
|
|
19961
|
+
if (emit.type !== "resolved") return;
|
|
19962
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
19963
|
+
const mapped = statusToDecision(emit.entry);
|
|
19964
|
+
if (!mapped) return;
|
|
19965
|
+
settle({
|
|
19966
|
+
decision: mapped.decision,
|
|
19967
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
19968
|
+
decided_by: mapped.decided_by
|
|
19969
|
+
});
|
|
19970
|
+
};
|
|
19971
|
+
try {
|
|
19972
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
19973
|
+
} catch (err) {
|
|
19974
|
+
settle({
|
|
19975
|
+
decision: "deny",
|
|
19976
|
+
decided_at: this.now().toISOString(),
|
|
19977
|
+
decided_by: "channel_failure"
|
|
19978
|
+
});
|
|
19979
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
19980
|
+
}
|
|
19981
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
19982
|
+
for (const entry of entries) {
|
|
19983
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
19984
|
+
const mapped = statusToDecision(entry);
|
|
19985
|
+
if (!mapped) return;
|
|
19986
|
+
settle({
|
|
19987
|
+
decision: mapped.decision,
|
|
19988
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
19989
|
+
decided_by: mapped.decided_by
|
|
19990
|
+
});
|
|
19991
|
+
return;
|
|
19992
|
+
}
|
|
19993
|
+
}).catch(() => {
|
|
19994
|
+
});
|
|
19995
|
+
timeoutHandle = setTimeout(() => {
|
|
19996
|
+
settle({
|
|
19997
|
+
decision: "deny",
|
|
19998
|
+
decided_at: this.now().toISOString(),
|
|
19999
|
+
decided_by: "timeout"
|
|
20000
|
+
});
|
|
20001
|
+
}, this.replaceModeTimeoutMs);
|
|
20002
|
+
});
|
|
20003
|
+
}
|
|
20004
|
+
/**
|
|
20005
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
20006
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
20007
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
20008
|
+
* downstream audit logging is unchanged.
|
|
20009
|
+
*
|
|
20010
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
20011
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
20012
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
20013
|
+
*/
|
|
20014
|
+
async notifyMode(request) {
|
|
20015
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
20016
|
+
let underlyingPromise;
|
|
20017
|
+
try {
|
|
20018
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
20019
|
+
} catch (err) {
|
|
20020
|
+
const response = await aggregatorPromise;
|
|
20021
|
+
return response;
|
|
20022
|
+
}
|
|
20023
|
+
return Promise.race([
|
|
20024
|
+
aggregatorPromise,
|
|
20025
|
+
underlyingPromise.catch(
|
|
20026
|
+
() => new Promise(() => {
|
|
20027
|
+
})
|
|
20028
|
+
)
|
|
20029
|
+
]);
|
|
20030
|
+
}
|
|
20031
|
+
};
|
|
20032
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
20033
|
+
return (_request) => {
|
|
20034
|
+
const cfg = supplier().approval_redirect;
|
|
20035
|
+
if (!cfg || cfg.enabled !== true) {
|
|
20036
|
+
return { enabled: false, mode: "replace" };
|
|
20037
|
+
}
|
|
20038
|
+
return {
|
|
20039
|
+
enabled: true,
|
|
20040
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
20041
|
+
};
|
|
20042
|
+
};
|
|
20043
|
+
}
|
|
20044
|
+
|
|
20045
|
+
// src/principal-policy/aggregator-store.ts
|
|
20046
|
+
init_encryption();
|
|
20047
|
+
init_encoding();
|
|
20048
|
+
var AGGREGATOR_PAYLOAD_NAMESPACE = "_approval_aggregator_payloads";
|
|
20049
|
+
var AGGREGATOR_PAYLOAD_KEY_PREFIX = "payload.";
|
|
20050
|
+
var HKDF_INFO = "l2-approval-aggregator-payload-v1";
|
|
20051
|
+
var DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS = 30;
|
|
20052
|
+
var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
20053
|
+
var AggregatorPayloadStore = class {
|
|
20054
|
+
storage;
|
|
20055
|
+
encryptionKey;
|
|
20056
|
+
fortressId;
|
|
20057
|
+
retentionDays;
|
|
20058
|
+
constructor(opts) {
|
|
20059
|
+
this.storage = opts.storage;
|
|
20060
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO);
|
|
20061
|
+
this.fortressId = opts.fortressId;
|
|
20062
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS;
|
|
20063
|
+
}
|
|
20064
|
+
/**
|
|
20065
|
+
* Persist `payload` under the given aggregator_id. Idempotent; calling
|
|
20066
|
+
* twice with the same id rewrites the bundle (retention_until is
|
|
20067
|
+
* recomputed). Returns the bundle's retention_until ISO-8601 timestamp
|
|
20068
|
+
* so callers can log it.
|
|
20069
|
+
*/
|
|
20070
|
+
async savePayload(aggregatorId, payload) {
|
|
20071
|
+
const now = /* @__PURE__ */ new Date();
|
|
20072
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
20073
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
20074
|
+
const bundle = {
|
|
20075
|
+
version: 1,
|
|
20076
|
+
aggregator_id: aggregatorId,
|
|
20077
|
+
fortress_id: this.fortressId,
|
|
20078
|
+
created_at: now.toISOString(),
|
|
20079
|
+
retention_until: retentionUntil.toISOString(),
|
|
20080
|
+
payload
|
|
20081
|
+
};
|
|
20082
|
+
const aad = stringToBytes(aggregatorId);
|
|
20083
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
20084
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
20085
|
+
await this.storage.write(
|
|
20086
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20087
|
+
payloadKey(aggregatorId),
|
|
20088
|
+
stringToBytes(JSON.stringify(envelope))
|
|
20089
|
+
);
|
|
20090
|
+
return bundle.retention_until;
|
|
20091
|
+
}
|
|
20092
|
+
/**
|
|
20093
|
+
* Read the persisted payload for the aggregator_id. Returns null if no
|
|
20094
|
+
* bundle exists, the bundle is corrupted, or AAD binding fails.
|
|
20095
|
+
*/
|
|
20096
|
+
async loadPayload(aggregatorId) {
|
|
20097
|
+
const key = payloadKey(aggregatorId);
|
|
20098
|
+
let raw;
|
|
20099
|
+
try {
|
|
20100
|
+
raw = await this.storage.read(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
20101
|
+
} catch {
|
|
20102
|
+
return null;
|
|
20103
|
+
}
|
|
20104
|
+
if (!raw) return null;
|
|
20105
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
20106
|
+
try {
|
|
20107
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
20108
|
+
const aad = stringToBytes(aggregatorId);
|
|
20109
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
20110
|
+
const parsed = JSON.parse(
|
|
20111
|
+
bytesToString(plaintext)
|
|
20112
|
+
);
|
|
20113
|
+
if (parsed.version !== 1) return null;
|
|
20114
|
+
if (parsed.aggregator_id !== aggregatorId) return null;
|
|
20115
|
+
return parsed.payload;
|
|
20116
|
+
} catch {
|
|
20117
|
+
return null;
|
|
20118
|
+
}
|
|
20119
|
+
}
|
|
20120
|
+
/**
|
|
20121
|
+
* Delete the persisted payload. Returns true when a bundle was removed,
|
|
20122
|
+
* false when none existed.
|
|
20123
|
+
*/
|
|
20124
|
+
async deletePayload(aggregatorId) {
|
|
20125
|
+
const key = payloadKey(aggregatorId);
|
|
20126
|
+
const existed = await this.storage.exists(
|
|
20127
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20128
|
+
key
|
|
20129
|
+
);
|
|
20130
|
+
if (!existed) return false;
|
|
20131
|
+
try {
|
|
20132
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
20133
|
+
} catch {
|
|
20134
|
+
return false;
|
|
20135
|
+
}
|
|
20136
|
+
return true;
|
|
20137
|
+
}
|
|
20138
|
+
/**
|
|
20139
|
+
* Drop expired payload bundles. Returns the count of bundles pruned.
|
|
20140
|
+
* Caller wires this into the cocoon-unlock initialization path.
|
|
20141
|
+
*/
|
|
20142
|
+
async pruneExpired(now) {
|
|
20143
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
20144
|
+
const entries = await this.storage.list(
|
|
20145
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20146
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX
|
|
20147
|
+
);
|
|
20148
|
+
let pruned = 0;
|
|
20149
|
+
for (const meta of entries) {
|
|
20150
|
+
const aggregatorId = stripKeyPrefix(meta.key);
|
|
20151
|
+
if (aggregatorId === null) continue;
|
|
20152
|
+
const raw = await this.storage.read(
|
|
20153
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20154
|
+
meta.key
|
|
20155
|
+
);
|
|
20156
|
+
if (!raw) continue;
|
|
20157
|
+
try {
|
|
20158
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
20159
|
+
const aad = stringToBytes(aggregatorId);
|
|
20160
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
20161
|
+
const parsed = JSON.parse(
|
|
20162
|
+
bytesToString(plaintext)
|
|
20163
|
+
);
|
|
20164
|
+
if (parsed.retention_until <= cutoff) {
|
|
20165
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
|
|
20166
|
+
pruned += 1;
|
|
20167
|
+
}
|
|
20168
|
+
} catch {
|
|
20169
|
+
}
|
|
20170
|
+
}
|
|
20171
|
+
return { pruned };
|
|
20172
|
+
}
|
|
20173
|
+
};
|
|
20174
|
+
function payloadKey(aggregatorId) {
|
|
20175
|
+
return `${AGGREGATOR_PAYLOAD_KEY_PREFIX}${aggregatorId}`;
|
|
20176
|
+
}
|
|
20177
|
+
function stripKeyPrefix(key) {
|
|
20178
|
+
if (!key.startsWith(AGGREGATOR_PAYLOAD_KEY_PREFIX)) return null;
|
|
20179
|
+
return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
|
|
20180
|
+
}
|
|
20181
|
+
|
|
19625
20182
|
// src/principal-policy/tools.ts
|
|
19626
20183
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
19627
20184
|
return [
|
|
@@ -20492,16 +21049,81 @@ function verifyAttestation(attestation, now) {
|
|
|
20492
21049
|
};
|
|
20493
21050
|
}
|
|
20494
21051
|
|
|
20495
|
-
// src/handshake/
|
|
20496
|
-
|
|
20497
|
-
|
|
20498
|
-
|
|
20499
|
-
|
|
20500
|
-
|
|
20501
|
-
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
|
|
21052
|
+
// src/handshake/audit.ts
|
|
21053
|
+
var HANDSHAKE_LIFECYCLE_OPS = {
|
|
21054
|
+
INITIATED: "handshake_initiated",
|
|
21055
|
+
COMPLETED: "handshake_completed",
|
|
21056
|
+
FAILED: "handshake_failed",
|
|
21057
|
+
ABORTED: "handshake_aborted"
|
|
21058
|
+
};
|
|
21059
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
21060
|
+
auditLog.append(
|
|
21061
|
+
"l4",
|
|
21062
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
21063
|
+
ctx.identity_id,
|
|
21064
|
+
detailsFromContext(ctx),
|
|
21065
|
+
"success"
|
|
21066
|
+
);
|
|
21067
|
+
}
|
|
21068
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
21069
|
+
const details = detailsFromContext(ctx);
|
|
21070
|
+
if (ctx.trust_tier !== void 0) {
|
|
21071
|
+
details.trust_tier = ctx.trust_tier;
|
|
21072
|
+
}
|
|
21073
|
+
auditLog.append(
|
|
21074
|
+
"l4",
|
|
21075
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
21076
|
+
ctx.identity_id,
|
|
21077
|
+
details,
|
|
21078
|
+
"success"
|
|
21079
|
+
);
|
|
21080
|
+
}
|
|
21081
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
21082
|
+
const details = detailsFromContext(ctx);
|
|
21083
|
+
details.reason = ctx.reason;
|
|
21084
|
+
if (ctx.error !== void 0) {
|
|
21085
|
+
details.error = ctx.error;
|
|
21086
|
+
}
|
|
21087
|
+
auditLog.append(
|
|
21088
|
+
"l4",
|
|
21089
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
21090
|
+
ctx.identity_id,
|
|
21091
|
+
details,
|
|
21092
|
+
"failure"
|
|
21093
|
+
);
|
|
21094
|
+
}
|
|
21095
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
21096
|
+
const details = detailsFromContext(ctx);
|
|
21097
|
+
details.reason = ctx.reason;
|
|
21098
|
+
auditLog.append(
|
|
21099
|
+
"l4",
|
|
21100
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
21101
|
+
ctx.identity_id,
|
|
21102
|
+
details,
|
|
21103
|
+
"failure"
|
|
21104
|
+
);
|
|
21105
|
+
}
|
|
21106
|
+
function detailsFromContext(ctx) {
|
|
21107
|
+
const details = {
|
|
21108
|
+
session_id: ctx.session_id,
|
|
21109
|
+
role: ctx.role
|
|
21110
|
+
};
|
|
21111
|
+
if (ctx.counterparty_id !== void 0) {
|
|
21112
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
21113
|
+
}
|
|
21114
|
+
return details;
|
|
21115
|
+
}
|
|
21116
|
+
|
|
21117
|
+
// src/handshake/tools.ts
|
|
21118
|
+
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
21119
|
+
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
21120
|
+
const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
|
|
21121
|
+
const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
21122
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
21123
|
+
const handshakeResults = /* @__PURE__ */ new Map();
|
|
21124
|
+
const shrOpts = {
|
|
21125
|
+
config,
|
|
21126
|
+
identityManager,
|
|
20505
21127
|
masterKey
|
|
20506
21128
|
};
|
|
20507
21129
|
const tools = [
|
|
@@ -20525,6 +21147,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20525
21147
|
const { challenge, session } = initiateHandshake(shr);
|
|
20526
21148
|
sessions.set(session.session_id, session);
|
|
20527
21149
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
21150
|
+
auditHandshakeInitiated(auditLog, {
|
|
21151
|
+
session_id: session.session_id,
|
|
21152
|
+
role: "initiator",
|
|
21153
|
+
identity_id: shr.body.instance_id
|
|
21154
|
+
});
|
|
20528
21155
|
return toolResult({
|
|
20529
21156
|
session_id: session.session_id,
|
|
20530
21157
|
challenge,
|
|
@@ -20564,10 +21191,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20564
21191
|
);
|
|
20565
21192
|
if ("error" in result) {
|
|
20566
21193
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
21194
|
+
auditHandshakeFailed(auditLog, {
|
|
21195
|
+
session_id: "unknown",
|
|
21196
|
+
role: "responder",
|
|
21197
|
+
identity_id: shr.body.instance_id,
|
|
21198
|
+
reason: classifyRespondFailure(result.error),
|
|
21199
|
+
error: result.error
|
|
21200
|
+
});
|
|
20567
21201
|
return toolResult({ error: result.error });
|
|
20568
21202
|
}
|
|
20569
21203
|
sessions.set(result.session.session_id, result.session);
|
|
20570
21204
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
21205
|
+
auditHandshakeInitiated(auditLog, {
|
|
21206
|
+
session_id: result.session.session_id,
|
|
21207
|
+
role: "responder",
|
|
21208
|
+
identity_id: shr.body.instance_id,
|
|
21209
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
21210
|
+
});
|
|
20571
21211
|
let autoPublishResult;
|
|
20572
21212
|
if (autoPublishHandshakes) {
|
|
20573
21213
|
autoPublishResult = { attempted: true };
|
|
@@ -20675,9 +21315,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20675
21315
|
const response = args.response;
|
|
20676
21316
|
const session = sessions.get(sessionId);
|
|
20677
21317
|
if (!session) {
|
|
21318
|
+
auditHandshakeFailed(auditLog, {
|
|
21319
|
+
session_id: sessionId,
|
|
21320
|
+
role: "initiator",
|
|
21321
|
+
identity_id: "unknown",
|
|
21322
|
+
reason: "session_unknown",
|
|
21323
|
+
error: `No handshake session found: ${sessionId}`
|
|
21324
|
+
});
|
|
20678
21325
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
20679
21326
|
}
|
|
20680
21327
|
if (session.state !== "initiated") {
|
|
21328
|
+
auditHandshakeFailed(auditLog, {
|
|
21329
|
+
session_id: sessionId,
|
|
21330
|
+
role: "initiator",
|
|
21331
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21332
|
+
reason: "session_state_mismatch",
|
|
21333
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21334
|
+
});
|
|
20681
21335
|
return toolResult({
|
|
20682
21336
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
20683
21337
|
});
|
|
@@ -20691,6 +21345,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20691
21345
|
if ("error" in result) {
|
|
20692
21346
|
session.state = "failed";
|
|
20693
21347
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
21348
|
+
auditHandshakeFailed(auditLog, {
|
|
21349
|
+
session_id: sessionId,
|
|
21350
|
+
role: "initiator",
|
|
21351
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21352
|
+
reason: classifyCompleteFailure(result.error),
|
|
21353
|
+
error: result.error
|
|
21354
|
+
});
|
|
20694
21355
|
return toolResult({ error: result.error });
|
|
20695
21356
|
}
|
|
20696
21357
|
session.state = "completed";
|
|
@@ -20699,6 +21360,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20699
21360
|
session.result = result.result;
|
|
20700
21361
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
20701
21362
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
21363
|
+
auditHandshakeCompleted(auditLog, {
|
|
21364
|
+
session_id: sessionId,
|
|
21365
|
+
role: "initiator",
|
|
21366
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21367
|
+
counterparty_id: result.result.counterparty_id,
|
|
21368
|
+
trust_tier: result.result.trust_tier
|
|
21369
|
+
});
|
|
20702
21370
|
return toolResult({
|
|
20703
21371
|
completion: result.completion,
|
|
20704
21372
|
result: result.result,
|
|
@@ -20746,6 +21414,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20746
21414
|
void 0,
|
|
20747
21415
|
result.verified ? "success" : "failure"
|
|
20748
21416
|
);
|
|
21417
|
+
if (result.verified) {
|
|
21418
|
+
auditHandshakeCompleted(auditLog, {
|
|
21419
|
+
session_id: session.session_id,
|
|
21420
|
+
role: "responder",
|
|
21421
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21422
|
+
counterparty_id: result.counterparty_id,
|
|
21423
|
+
trust_tier: result.trust_tier
|
|
21424
|
+
});
|
|
21425
|
+
} else {
|
|
21426
|
+
auditHandshakeFailed(auditLog, {
|
|
21427
|
+
session_id: session.session_id,
|
|
21428
|
+
role: "responder",
|
|
21429
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21430
|
+
counterparty_id: result.counterparty_id,
|
|
21431
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
21432
|
+
error: result.errors.join("; ")
|
|
21433
|
+
});
|
|
21434
|
+
}
|
|
20749
21435
|
return toolResult({ result });
|
|
20750
21436
|
}
|
|
20751
21437
|
return toolResult({
|
|
@@ -20853,10 +21539,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20853
21539
|
_content_trust: "external"
|
|
20854
21540
|
});
|
|
20855
21541
|
}
|
|
21542
|
+
},
|
|
21543
|
+
{
|
|
21544
|
+
name: "handshake_abort",
|
|
21545
|
+
description: "Abort an in-flight handshake session. Drops the session record and appends a session-lifecycle audit entry (handshake_aborted) so the operator can distinguish operator-cancelled, timed-out, and dropped sessions from sessions that simply fell off the protocol path.",
|
|
21546
|
+
inputSchema: {
|
|
21547
|
+
type: "object",
|
|
21548
|
+
properties: {
|
|
21549
|
+
session_id: {
|
|
21550
|
+
type: "string",
|
|
21551
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
21552
|
+
},
|
|
21553
|
+
reason: {
|
|
21554
|
+
type: "string",
|
|
21555
|
+
enum: [
|
|
21556
|
+
"operator_cancelled",
|
|
21557
|
+
"session_timeout",
|
|
21558
|
+
"transport_dropped",
|
|
21559
|
+
"shutdown",
|
|
21560
|
+
"other"
|
|
21561
|
+
],
|
|
21562
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
21563
|
+
}
|
|
21564
|
+
},
|
|
21565
|
+
required: ["session_id"]
|
|
21566
|
+
},
|
|
21567
|
+
handler: async (args) => {
|
|
21568
|
+
const sessionId = args.session_id;
|
|
21569
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
21570
|
+
const session = sessions.get(sessionId);
|
|
21571
|
+
if (!session) {
|
|
21572
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21573
|
+
}
|
|
21574
|
+
if (session.state === "completed") {
|
|
21575
|
+
return toolResult({
|
|
21576
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
21577
|
+
});
|
|
21578
|
+
}
|
|
21579
|
+
sessions.delete(sessionId);
|
|
21580
|
+
auditHandshakeAborted(auditLog, {
|
|
21581
|
+
session_id: sessionId,
|
|
21582
|
+
role: session.role,
|
|
21583
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21584
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
21585
|
+
reason
|
|
21586
|
+
});
|
|
21587
|
+
return toolResult({
|
|
21588
|
+
aborted: true,
|
|
21589
|
+
session_id: sessionId,
|
|
21590
|
+
reason
|
|
21591
|
+
});
|
|
21592
|
+
}
|
|
20856
21593
|
}
|
|
20857
21594
|
];
|
|
20858
21595
|
return { tools, handshakeResults };
|
|
20859
21596
|
}
|
|
21597
|
+
function classifyRespondFailure(error) {
|
|
21598
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21599
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
21600
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21601
|
+
return "other";
|
|
21602
|
+
}
|
|
21603
|
+
function classifyCompleteFailure(error) {
|
|
21604
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21605
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
21606
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
21607
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21608
|
+
return "other";
|
|
21609
|
+
}
|
|
20860
21610
|
|
|
20861
21611
|
// src/federation/registry.ts
|
|
20862
21612
|
var DEFAULT_CAPABILITIES = {
|
|
@@ -31582,15 +32332,300 @@ var OPERATOR_CHAT_OPS = {
|
|
|
31582
32332
|
* successful thread removal. Body carries thread_id + turn_count of
|
|
31583
32333
|
* the deleted bundle.
|
|
31584
32334
|
*/
|
|
31585
|
-
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
32335
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
32336
|
+
/**
|
|
32337
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
32338
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
32339
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
32340
|
+
* carries thread_id + a stable failure_reason enum.
|
|
32341
|
+
*/
|
|
32342
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed",
|
|
32343
|
+
/**
|
|
32344
|
+
* Concierge dynamic-context fetcher failed (WP-V1.3-9 Tau-3). Emitted
|
|
32345
|
+
* when a category fetcher throws while assembling the dynamic context
|
|
32346
|
+
* fold. The concierge omits that category and continues; the user-
|
|
32347
|
+
* facing query is never broken. Body carries category + failure_reason.
|
|
32348
|
+
*/
|
|
32349
|
+
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
|
|
31586
32350
|
};
|
|
31587
32351
|
|
|
31588
32352
|
// src/chat/operator-chat-types.ts
|
|
31589
32353
|
var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
|
|
31590
32354
|
var CONCIERGE_THREAD_KEY = "_fortress";
|
|
31591
32355
|
|
|
32356
|
+
// src/chat/concierge-context-router.ts
|
|
32357
|
+
var APPROX_CHARS_PER_TOKEN = 4;
|
|
32358
|
+
var DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET = 2e3;
|
|
32359
|
+
var DYNAMIC_CONTEXT_SECTION_HEADER = "## Live fortress context";
|
|
32360
|
+
var CONTEXT_CATEGORIES = [
|
|
32361
|
+
"templates",
|
|
32362
|
+
"agent_state",
|
|
32363
|
+
"agent_activity",
|
|
32364
|
+
"audit_log",
|
|
32365
|
+
"sentinel_findings",
|
|
32366
|
+
"anomaly_alerts",
|
|
32367
|
+
"recent_receipts",
|
|
32368
|
+
"verascore_deltas"
|
|
32369
|
+
];
|
|
32370
|
+
function phrasePattern(phrase) {
|
|
32371
|
+
const escaped = phrase.toLowerCase().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
32372
|
+
return { source: `\\b${escaped}\\b`, phrase };
|
|
32373
|
+
}
|
|
32374
|
+
var CATEGORY_KEYWORDS = [
|
|
32375
|
+
{
|
|
32376
|
+
category: "templates",
|
|
32377
|
+
patterns: [
|
|
32378
|
+
"templates",
|
|
32379
|
+
"template",
|
|
32380
|
+
"channel templates",
|
|
32381
|
+
"channel template",
|
|
32382
|
+
"list templates",
|
|
32383
|
+
"available templates",
|
|
32384
|
+
"what templates"
|
|
32385
|
+
].map(phrasePattern)
|
|
32386
|
+
},
|
|
32387
|
+
{
|
|
32388
|
+
category: "agent_state",
|
|
32389
|
+
patterns: [
|
|
32390
|
+
"state",
|
|
32391
|
+
"status",
|
|
32392
|
+
"agent state",
|
|
32393
|
+
"agent status",
|
|
32394
|
+
"status of agent",
|
|
32395
|
+
"status of agents",
|
|
32396
|
+
"state of",
|
|
32397
|
+
"doing"
|
|
32398
|
+
].map(phrasePattern)
|
|
32399
|
+
},
|
|
32400
|
+
{
|
|
32401
|
+
category: "agent_activity",
|
|
32402
|
+
patterns: [
|
|
32403
|
+
"activity",
|
|
32404
|
+
"agent activity",
|
|
32405
|
+
"what did",
|
|
32406
|
+
"recent activity"
|
|
32407
|
+
].map(phrasePattern)
|
|
32408
|
+
},
|
|
32409
|
+
{
|
|
32410
|
+
category: "audit_log",
|
|
32411
|
+
patterns: [
|
|
32412
|
+
"audit log",
|
|
32413
|
+
"audit",
|
|
32414
|
+
"log entry",
|
|
32415
|
+
"log entries",
|
|
32416
|
+
"what happened",
|
|
32417
|
+
"show me events",
|
|
32418
|
+
"event class"
|
|
32419
|
+
].map(phrasePattern)
|
|
32420
|
+
},
|
|
32421
|
+
{
|
|
32422
|
+
category: "sentinel_findings",
|
|
32423
|
+
patterns: [
|
|
32424
|
+
"sentinel",
|
|
32425
|
+
"sentinels",
|
|
32426
|
+
"warning",
|
|
32427
|
+
"warnings",
|
|
32428
|
+
"alert",
|
|
32429
|
+
"alerts",
|
|
32430
|
+
"whats wrong",
|
|
32431
|
+
"what's wrong",
|
|
32432
|
+
"findings"
|
|
32433
|
+
].map(phrasePattern)
|
|
32434
|
+
},
|
|
32435
|
+
{
|
|
32436
|
+
category: "anomaly_alerts",
|
|
32437
|
+
patterns: [
|
|
32438
|
+
"anomaly",
|
|
32439
|
+
"anomalies",
|
|
32440
|
+
"spike",
|
|
32441
|
+
"unusual",
|
|
32442
|
+
"outlier"
|
|
32443
|
+
].map(phrasePattern)
|
|
32444
|
+
},
|
|
32445
|
+
{
|
|
32446
|
+
category: "recent_receipts",
|
|
32447
|
+
patterns: [
|
|
32448
|
+
"receipt",
|
|
32449
|
+
"receipts",
|
|
32450
|
+
"concordia",
|
|
32451
|
+
"commitment",
|
|
32452
|
+
"commitments",
|
|
32453
|
+
"chain",
|
|
32454
|
+
"chains"
|
|
32455
|
+
].map(phrasePattern)
|
|
32456
|
+
},
|
|
32457
|
+
{
|
|
32458
|
+
category: "verascore_deltas",
|
|
32459
|
+
patterns: [
|
|
32460
|
+
"verascore",
|
|
32461
|
+
"vera score",
|
|
32462
|
+
"trust score",
|
|
32463
|
+
"reputation"
|
|
32464
|
+
].map(phrasePattern)
|
|
32465
|
+
}
|
|
32466
|
+
];
|
|
32467
|
+
function extractAgentNameHint(query) {
|
|
32468
|
+
const agentPattern = /\bagent\s+["']?([A-Za-z][\w-]{0,40})["']?/i;
|
|
32469
|
+
const m = query.match(agentPattern);
|
|
32470
|
+
if (m && m[1]) return m[1];
|
|
32471
|
+
const quoted = query.match(/["']([A-Za-z][\w-]{0,40})["']/);
|
|
32472
|
+
if (quoted && quoted[1]) return quoted[1];
|
|
32473
|
+
return null;
|
|
32474
|
+
}
|
|
32475
|
+
var TRIVIAL_GREETINGS = /* @__PURE__ */ new Set([
|
|
32476
|
+
"hi",
|
|
32477
|
+
"hello",
|
|
32478
|
+
"hey",
|
|
32479
|
+
"yo",
|
|
32480
|
+
"ok",
|
|
32481
|
+
"thanks",
|
|
32482
|
+
"thx",
|
|
32483
|
+
"thank you"
|
|
32484
|
+
]);
|
|
32485
|
+
function isTrivialQuery(query) {
|
|
32486
|
+
const norm = query.trim().toLowerCase();
|
|
32487
|
+
if (norm.length === 0) return true;
|
|
32488
|
+
if (norm.length < 8) return true;
|
|
32489
|
+
return TRIVIAL_GREETINGS.has(norm);
|
|
32490
|
+
}
|
|
32491
|
+
function classifyQuery(query) {
|
|
32492
|
+
const normalized = query.toLowerCase();
|
|
32493
|
+
const matches = [];
|
|
32494
|
+
for (const spec of CATEGORY_KEYWORDS) {
|
|
32495
|
+
const matchedPhrases = [];
|
|
32496
|
+
for (const pattern of spec.patterns) {
|
|
32497
|
+
if (matchedPhrases.includes(pattern.phrase)) continue;
|
|
32498
|
+
const re = new RegExp(pattern.source, "i");
|
|
32499
|
+
if (re.test(normalized)) {
|
|
32500
|
+
matchedPhrases.push(pattern.phrase);
|
|
32501
|
+
}
|
|
32502
|
+
}
|
|
32503
|
+
if (matchedPhrases.length === 0) continue;
|
|
32504
|
+
const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
|
|
32505
|
+
matches.push({
|
|
32506
|
+
category: spec.category,
|
|
32507
|
+
confidence,
|
|
32508
|
+
matched_keywords: matchedPhrases,
|
|
32509
|
+
agent_name_hint: spec.category === "agent_state" || spec.category === "agent_activity" ? extractAgentNameHint(query) : null
|
|
32510
|
+
});
|
|
32511
|
+
}
|
|
32512
|
+
matches.sort((a, b) => {
|
|
32513
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
32514
|
+
return CONTEXT_CATEGORIES.indexOf(a.category) - CONTEXT_CATEGORIES.indexOf(b.category);
|
|
32515
|
+
});
|
|
32516
|
+
return matches;
|
|
32517
|
+
}
|
|
32518
|
+
function approxTokenLen(text) {
|
|
32519
|
+
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
32520
|
+
}
|
|
32521
|
+
var CATEGORY_LABELS = {
|
|
32522
|
+
templates: "Templates",
|
|
32523
|
+
agent_state: "Agent state",
|
|
32524
|
+
agent_activity: "Agent activity",
|
|
32525
|
+
audit_log: "Audit log",
|
|
32526
|
+
sentinel_findings: "Sentinel findings",
|
|
32527
|
+
anomaly_alerts: "Anomaly alerts",
|
|
32528
|
+
recent_receipts: "Recent receipts",
|
|
32529
|
+
verascore_deltas: "Verascore deltas"
|
|
32530
|
+
};
|
|
32531
|
+
async function runFetcher(match, fetchers) {
|
|
32532
|
+
switch (match.category) {
|
|
32533
|
+
case "templates":
|
|
32534
|
+
return fetchers.templates();
|
|
32535
|
+
case "agent_state":
|
|
32536
|
+
return fetchers.agent_state(match.agent_name_hint);
|
|
32537
|
+
case "agent_activity":
|
|
32538
|
+
return fetchers.agent_activity(match.agent_name_hint);
|
|
32539
|
+
case "audit_log":
|
|
32540
|
+
return fetchers.audit_log();
|
|
32541
|
+
case "sentinel_findings":
|
|
32542
|
+
return fetchers.sentinel_findings();
|
|
32543
|
+
case "anomaly_alerts":
|
|
32544
|
+
return fetchers.anomaly_alerts();
|
|
32545
|
+
case "recent_receipts":
|
|
32546
|
+
return fetchers.recent_receipts();
|
|
32547
|
+
case "verascore_deltas":
|
|
32548
|
+
return fetchers.verascore_deltas();
|
|
32549
|
+
}
|
|
32550
|
+
}
|
|
32551
|
+
function trivialMatch(category) {
|
|
32552
|
+
return {
|
|
32553
|
+
category,
|
|
32554
|
+
confidence: 0.5,
|
|
32555
|
+
matched_keywords: ["llm-assist"],
|
|
32556
|
+
agent_name_hint: null
|
|
32557
|
+
};
|
|
32558
|
+
}
|
|
32559
|
+
async function foldContext(query, fetchers, opts) {
|
|
32560
|
+
const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
|
|
32561
|
+
let matches = classifyQuery(query);
|
|
32562
|
+
if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
|
|
32563
|
+
try {
|
|
32564
|
+
const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
|
|
32565
|
+
if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
|
|
32566
|
+
matches = [trivialMatch(picked)];
|
|
32567
|
+
}
|
|
32568
|
+
} catch {
|
|
32569
|
+
}
|
|
32570
|
+
}
|
|
32571
|
+
if (matches.length === 0) {
|
|
32572
|
+
return { section: "", categoriesIncluded: [] };
|
|
32573
|
+
}
|
|
32574
|
+
const attempts = [];
|
|
32575
|
+
for (const match of matches) {
|
|
32576
|
+
try {
|
|
32577
|
+
const text = await runFetcher(match, fetchers);
|
|
32578
|
+
const trimmed = text.trim();
|
|
32579
|
+
if (trimmed.length > 0) {
|
|
32580
|
+
attempts.push({ category: match.category, text: trimmed });
|
|
32581
|
+
}
|
|
32582
|
+
} catch (err) {
|
|
32583
|
+
opts?.onFetcherFailure?.(match.category, err);
|
|
32584
|
+
}
|
|
32585
|
+
}
|
|
32586
|
+
if (attempts.length === 0) {
|
|
32587
|
+
return { section: "", categoriesIncluded: [] };
|
|
32588
|
+
}
|
|
32589
|
+
const headerTokens = approxTokenLen(`${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
32590
|
+
`);
|
|
32591
|
+
const sepTokens = approxTokenLen("\n\n");
|
|
32592
|
+
let runningTokens = headerTokens;
|
|
32593
|
+
const kept = [];
|
|
32594
|
+
for (const attempt of attempts) {
|
|
32595
|
+
const block = `### ${CATEGORY_LABELS[attempt.category]}
|
|
32596
|
+
${attempt.text}`;
|
|
32597
|
+
const tokens = approxTokenLen(block) + (kept.length > 0 ? sepTokens : 0);
|
|
32598
|
+
if (kept.length === 0) {
|
|
32599
|
+
kept.push(attempt);
|
|
32600
|
+
runningTokens += tokens;
|
|
32601
|
+
continue;
|
|
32602
|
+
}
|
|
32603
|
+
if (runningTokens + tokens > budget) break;
|
|
32604
|
+
kept.push(attempt);
|
|
32605
|
+
runningTokens += tokens;
|
|
32606
|
+
}
|
|
32607
|
+
const blocks = kept.map(
|
|
32608
|
+
(k) => `### ${CATEGORY_LABELS[k.category]}
|
|
32609
|
+
${k.text}`
|
|
32610
|
+
);
|
|
32611
|
+
const section = `${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
32612
|
+
${blocks.join("\n\n")}`;
|
|
32613
|
+
return {
|
|
32614
|
+
section,
|
|
32615
|
+
categoriesIncluded: kept.map((k) => k.category)
|
|
32616
|
+
};
|
|
32617
|
+
}
|
|
32618
|
+
|
|
31592
32619
|
// src/chat/operator-chat-service.ts
|
|
31593
32620
|
var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
32621
|
+
var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
32622
|
+
var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
32623
|
+
var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
32624
|
+
var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32625
|
+
var DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
|
|
32626
|
+
function approxTokenLen2(text) {
|
|
32627
|
+
return Math.ceil(text.length / 4);
|
|
32628
|
+
}
|
|
31594
32629
|
var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
31595
32630
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
31596
32631
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -31627,6 +32662,14 @@ var OperatorChatService = class {
|
|
|
31627
32662
|
piiFilter;
|
|
31628
32663
|
conciergeMaxTokens;
|
|
31629
32664
|
memory;
|
|
32665
|
+
historyWindowTurns;
|
|
32666
|
+
historyFreshnessMs;
|
|
32667
|
+
historyTokenBudget;
|
|
32668
|
+
sessionTtlMs;
|
|
32669
|
+
clock;
|
|
32670
|
+
contextFetchers;
|
|
32671
|
+
contextLlmAssist;
|
|
32672
|
+
dynamicContextBudget;
|
|
31630
32673
|
/**
|
|
31631
32674
|
* In-memory thread_id assigned to the active concierge session.
|
|
31632
32675
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -31634,6 +32677,14 @@ var OperatorChatService = class {
|
|
|
31634
32677
|
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
31635
32678
|
*/
|
|
31636
32679
|
activeMemoryThreadId;
|
|
32680
|
+
/**
|
|
32681
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
32682
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
32683
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
32684
|
+
* allocates a new thread_id even though the prior one is still
|
|
32685
|
+
* readable from the memory store.
|
|
32686
|
+
*/
|
|
32687
|
+
lastInteractionAt;
|
|
31637
32688
|
constructor(deps) {
|
|
31638
32689
|
this.store = deps.store;
|
|
31639
32690
|
this.auditLog = deps.auditLog;
|
|
@@ -31645,6 +32696,18 @@ var OperatorChatService = class {
|
|
|
31645
32696
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
31646
32697
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
31647
32698
|
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32699
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
32700
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
32701
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
32702
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
32703
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
32704
|
+
if (deps.conciergeContextFetchers) {
|
|
32705
|
+
this.contextFetchers = deps.conciergeContextFetchers;
|
|
32706
|
+
}
|
|
32707
|
+
if (deps.conciergeContextLlmAssist) {
|
|
32708
|
+
this.contextLlmAssist = deps.conciergeContextLlmAssist;
|
|
32709
|
+
}
|
|
32710
|
+
this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
|
|
31648
32711
|
}
|
|
31649
32712
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
31650
32713
|
/**
|
|
@@ -31663,6 +32726,10 @@ var OperatorChatService = class {
|
|
|
31663
32726
|
throw new Error("concierge query must not be empty");
|
|
31664
32727
|
}
|
|
31665
32728
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
32729
|
+
const nowMs = this.clock();
|
|
32730
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
32731
|
+
this.activeMemoryThreadId = void 0;
|
|
32732
|
+
}
|
|
31666
32733
|
const operatorMessage = {
|
|
31667
32734
|
message_id: crypto.randomUUID(),
|
|
31668
32735
|
surface: "concierge",
|
|
@@ -31675,6 +32742,25 @@ var OperatorChatService = class {
|
|
|
31675
32742
|
CONCIERGE_THREAD_KEY,
|
|
31676
32743
|
operatorMessage
|
|
31677
32744
|
);
|
|
32745
|
+
let priorTurns = [];
|
|
32746
|
+
let memoryReadFailureReason = null;
|
|
32747
|
+
let activeThreadIdForRound;
|
|
32748
|
+
if (this.memory) {
|
|
32749
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
32750
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
32751
|
+
if (result.ok) {
|
|
32752
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
32753
|
+
const fresh = result.turns.filter((t) => {
|
|
32754
|
+
const ts = Date.parse(t.created_at);
|
|
32755
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
32756
|
+
});
|
|
32757
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
32758
|
+
priorTurns = recent;
|
|
32759
|
+
} else {
|
|
32760
|
+
memoryReadFailureReason = result.reason;
|
|
32761
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
32762
|
+
}
|
|
32763
|
+
}
|
|
31678
32764
|
if (this.memory) {
|
|
31679
32765
|
const threadId = this.ensureActiveMemoryThread();
|
|
31680
32766
|
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
@@ -31685,6 +32771,7 @@ var OperatorChatService = class {
|
|
|
31685
32771
|
let servedBy = "disabled";
|
|
31686
32772
|
let displayLabel = "Concierge: substrate not configured";
|
|
31687
32773
|
let outcome = "substrate_disabled";
|
|
32774
|
+
let dynamicCategoriesIncluded = [];
|
|
31688
32775
|
if (!this.substrateSelector) {
|
|
31689
32776
|
conciergeBody = "Concierge unavailable. The substrate selector is not configured for this fortress. Pick a substrate in the Policy center to enable concierge replies.";
|
|
31690
32777
|
} else {
|
|
@@ -31696,7 +32783,14 @@ var OperatorChatService = class {
|
|
|
31696
32783
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
31697
32784
|
outcome = "substrate_disabled";
|
|
31698
32785
|
} else {
|
|
31699
|
-
const
|
|
32786
|
+
const dynamicResult = await this.runDynamicContextFold(
|
|
32787
|
+
filterResult.filtered
|
|
32788
|
+
);
|
|
32789
|
+
dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
|
|
32790
|
+
const context = await this.assembleConciergeContext(
|
|
32791
|
+
priorTurns,
|
|
32792
|
+
dynamicResult.section
|
|
32793
|
+
);
|
|
31700
32794
|
const response = await this.substrateSelector.invokeSummarize(
|
|
31701
32795
|
"concierge",
|
|
31702
32796
|
{
|
|
@@ -31735,10 +32829,14 @@ var OperatorChatService = class {
|
|
|
31735
32829
|
CONCIERGE_THREAD_KEY,
|
|
31736
32830
|
responseMessage
|
|
31737
32831
|
);
|
|
32832
|
+
let assistantTurnId;
|
|
31738
32833
|
if (this.memory) {
|
|
31739
32834
|
const threadId = this.ensureActiveMemoryThread();
|
|
31740
|
-
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() =>
|
|
31741
|
-
|
|
32835
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
32836
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
32837
|
+
}
|
|
32838
|
+
if (this.memory && activeThreadIdForRound) {
|
|
32839
|
+
this.lastInteractionAt = nowMs;
|
|
31742
32840
|
}
|
|
31743
32841
|
const payload = {
|
|
31744
32842
|
version: "1.2",
|
|
@@ -31751,7 +32849,13 @@ var OperatorChatService = class {
|
|
|
31751
32849
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
31752
32850
|
substrate: servedBy,
|
|
31753
32851
|
latency_ms: latencyMs,
|
|
31754
|
-
outcome
|
|
32852
|
+
outcome,
|
|
32853
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
32854
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
32855
|
+
...this.memory ? {
|
|
32856
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
32857
|
+
} : {},
|
|
32858
|
+
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
|
|
31755
32859
|
};
|
|
31756
32860
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
31757
32861
|
return {
|
|
@@ -31761,6 +32865,25 @@ var OperatorChatService = class {
|
|
|
31761
32865
|
outcome
|
|
31762
32866
|
};
|
|
31763
32867
|
}
|
|
32868
|
+
/**
|
|
32869
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
32870
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
32871
|
+
* with `result: "failure"` since the concierge fell back to
|
|
32872
|
+
* single-turn mode for this round-trip.
|
|
32873
|
+
*/
|
|
32874
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
32875
|
+
const payload = {
|
|
32876
|
+
version: "1.2",
|
|
32877
|
+
event_id: makeEventId("conc-memfail"),
|
|
32878
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32879
|
+
identity_id: this.identityId,
|
|
32880
|
+
kind: "operator_concierge_memory_read_failed",
|
|
32881
|
+
surface: "concierge",
|
|
32882
|
+
thread_id: threadId,
|
|
32883
|
+
failure_reason: reason
|
|
32884
|
+
};
|
|
32885
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
32886
|
+
}
|
|
31764
32887
|
/**
|
|
31765
32888
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
31766
32889
|
* an empty array when no thread exists yet.
|
|
@@ -31883,6 +33006,14 @@ var OperatorChatService = class {
|
|
|
31883
33006
|
* ## Sanctuary reference
|
|
31884
33007
|
* <static domain reference block>
|
|
31885
33008
|
*
|
|
33009
|
+
* ## Live fortress context ← WP-V1.3-9 Tau-3, when present
|
|
33010
|
+
* ### <Category>
|
|
33011
|
+
* <fetcher payload>
|
|
33012
|
+
*
|
|
33013
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
33014
|
+
* OPERATOR: ...
|
|
33015
|
+
* CONCIERGE: ...
|
|
33016
|
+
*
|
|
31886
33017
|
* ## Recent activity
|
|
31887
33018
|
* <recentActivity output>
|
|
31888
33019
|
*
|
|
@@ -31892,37 +33023,116 @@ var OperatorChatService = class {
|
|
|
31892
33023
|
* ## Open inbox
|
|
31893
33024
|
* <openInbox output>
|
|
31894
33025
|
* ```
|
|
31895
|
-
|
|
31896
|
-
|
|
33026
|
+
*
|
|
33027
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
33028
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
33029
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
33030
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
33031
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
33032
|
+
* serialization is the canonical path for v1.3.
|
|
33033
|
+
*/
|
|
33034
|
+
async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
|
|
31897
33035
|
const ref = `## Sanctuary reference
|
|
31898
33036
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
33037
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
31899
33038
|
if (!this.contextProviders) {
|
|
31900
|
-
return
|
|
31901
|
-
|
|
31902
|
-
|
|
31903
|
-
|
|
31904
|
-
|
|
31905
|
-
## Wrapped agents
|
|
31906
|
-
(no providers wired)
|
|
31907
|
-
|
|
31908
|
-
## Open inbox
|
|
31909
|
-
(no providers wired)`;
|
|
33039
|
+
return [
|
|
33040
|
+
ref,
|
|
33041
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
33042
|
+
...priorSection ? [priorSection] : [],
|
|
33043
|
+
"## Recent activity\n(no providers wired)",
|
|
33044
|
+
"## Wrapped agents\n(no providers wired)",
|
|
33045
|
+
"## Open inbox\n(no providers wired)"
|
|
33046
|
+
].join("\n\n");
|
|
31910
33047
|
}
|
|
31911
33048
|
const [activity, agents, inbox] = await Promise.all([
|
|
31912
33049
|
this.contextProviders.recentActivity(),
|
|
31913
33050
|
this.contextProviders.agentInventory(),
|
|
31914
33051
|
this.contextProviders.openInbox()
|
|
31915
33052
|
]);
|
|
31916
|
-
return
|
|
31917
|
-
|
|
31918
|
-
|
|
31919
|
-
|
|
31920
|
-
|
|
31921
|
-
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
${inbox}
|
|
33053
|
+
return [
|
|
33054
|
+
ref,
|
|
33055
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
33056
|
+
...priorSection ? [priorSection] : [],
|
|
33057
|
+
`## Recent activity
|
|
33058
|
+
${activity}`,
|
|
33059
|
+
`## Wrapped agents
|
|
33060
|
+
${agents}`,
|
|
33061
|
+
`## Open inbox
|
|
33062
|
+
${inbox}`
|
|
33063
|
+
].join("\n\n");
|
|
33064
|
+
}
|
|
33065
|
+
/**
|
|
33066
|
+
* Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
|
|
33067
|
+
* trip. Fail-soft on every axis: missing fetchers short-circuit to
|
|
33068
|
+
* an empty fold, fetcher failures emit a per-category audit event
|
|
33069
|
+
* and are omitted from the rendered section, an LLM-assist failure
|
|
33070
|
+
* proceeds with no fold. Returns the rendered section + the list of
|
|
33071
|
+
* categories whose data made it into the section (used for the
|
|
33072
|
+
* round-trip audit emission).
|
|
33073
|
+
*/
|
|
33074
|
+
async runDynamicContextFold(query) {
|
|
33075
|
+
if (!this.contextFetchers) {
|
|
33076
|
+
return { section: "", categoriesIncluded: [] };
|
|
33077
|
+
}
|
|
33078
|
+
const result = await foldContext(query, this.contextFetchers, {
|
|
33079
|
+
maxTokens: this.dynamicContextBudget,
|
|
33080
|
+
...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
|
|
33081
|
+
onFetcherFailure: (category, error) => {
|
|
33082
|
+
this.emitContextFetcherFailed(category, classifyFetcherError(error));
|
|
33083
|
+
}
|
|
33084
|
+
});
|
|
33085
|
+
return result;
|
|
33086
|
+
}
|
|
33087
|
+
/**
|
|
33088
|
+
* Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
|
|
33089
|
+
* of the fold path so the dynamic-context handler stays readable.
|
|
33090
|
+
* Emits with `result: "failure"` since the named category dropped
|
|
33091
|
+
* from the rendered section for this round-trip.
|
|
33092
|
+
*/
|
|
33093
|
+
emitContextFetcherFailed(category, failureReason) {
|
|
33094
|
+
const payload = {
|
|
33095
|
+
version: "1.2",
|
|
33096
|
+
event_id: makeEventId("conc-ctxfail"),
|
|
33097
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33098
|
+
identity_id: this.identityId,
|
|
33099
|
+
kind: "operator_concierge_context_fetcher_failed",
|
|
33100
|
+
surface: "concierge",
|
|
33101
|
+
category,
|
|
33102
|
+
failure_reason: failureReason
|
|
33103
|
+
};
|
|
33104
|
+
this.emit(
|
|
33105
|
+
OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
|
|
33106
|
+
payload,
|
|
33107
|
+
"failure"
|
|
33108
|
+
);
|
|
33109
|
+
}
|
|
33110
|
+
/**
|
|
33111
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
33112
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
33113
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
33114
|
+
* the input is empty or when the budget excludes every turn.
|
|
33115
|
+
*/
|
|
33116
|
+
formatPriorTurnsSection(turns) {
|
|
33117
|
+
if (turns.length === 0) return "";
|
|
33118
|
+
const HEADER = "## Prior conversation";
|
|
33119
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
33120
|
+
const headerTokens = approxTokenLen2(`${HEADER}
|
|
33121
|
+
`);
|
|
33122
|
+
const sepTokens = approxTokenLen2("\n");
|
|
33123
|
+
let runningTokens = headerTokens;
|
|
33124
|
+
let runningLines = [];
|
|
33125
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
33126
|
+
const line = lines[i];
|
|
33127
|
+
const tokens = approxTokenLen2(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
33128
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
33129
|
+
runningTokens += tokens;
|
|
33130
|
+
runningLines.push(line);
|
|
33131
|
+
}
|
|
33132
|
+
if (runningLines.length === 0) return "";
|
|
33133
|
+
runningLines = runningLines.reverse();
|
|
33134
|
+
return `${HEADER}
|
|
33135
|
+
${runningLines.join("\n")}`;
|
|
31926
33136
|
}
|
|
31927
33137
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
31928
33138
|
emit(operation, payload, result) {
|
|
@@ -31938,6 +33148,21 @@ ${inbox}`;
|
|
|
31938
33148
|
function makeEventId(prefix) {
|
|
31939
33149
|
return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
31940
33150
|
}
|
|
33151
|
+
function classifyFetcherError(error) {
|
|
33152
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
33153
|
+
if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
|
|
33154
|
+
if (msg.includes("schema") || msg.includes("invalid shape")) {
|
|
33155
|
+
return "schema_mismatch";
|
|
33156
|
+
}
|
|
33157
|
+
if (msg.includes("io") || msg.includes("read") || msg.includes("enoent") || msg.includes("eacces")) {
|
|
33158
|
+
return "io_failed";
|
|
33159
|
+
}
|
|
33160
|
+
return "unknown";
|
|
33161
|
+
}
|
|
33162
|
+
function formatPriorTurnLine(turn) {
|
|
33163
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
33164
|
+
return `${label}: ${turn.content}`;
|
|
33165
|
+
}
|
|
31941
33166
|
function hashOf(input) {
|
|
31942
33167
|
return hashToString(sha256.sha256(stringToBytes(input)));
|
|
31943
33168
|
}
|
|
@@ -31946,7 +33171,7 @@ function hashOf(input) {
|
|
|
31946
33171
|
init_encryption();
|
|
31947
33172
|
init_encoding();
|
|
31948
33173
|
var OPERATOR_CHAT_NAMESPACE = "_chat";
|
|
31949
|
-
var
|
|
33174
|
+
var HKDF_INFO2 = "operator-chat-store-v1";
|
|
31950
33175
|
function chatStorageKey(surface, threadKey) {
|
|
31951
33176
|
return `${surface}.${threadKey}`;
|
|
31952
33177
|
}
|
|
@@ -31955,7 +33180,7 @@ var OperatorChatStore = class {
|
|
|
31955
33180
|
encryptionKey;
|
|
31956
33181
|
constructor(storage, masterKey) {
|
|
31957
33182
|
this.storage = storage;
|
|
31958
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33183
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
|
|
31959
33184
|
}
|
|
31960
33185
|
/**
|
|
31961
33186
|
* Load a thread. Returns null if no record exists or if the on-disk
|
|
@@ -32039,9 +33264,9 @@ init_encryption();
|
|
|
32039
33264
|
init_encoding();
|
|
32040
33265
|
var CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
32041
33266
|
var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
32042
|
-
var
|
|
33267
|
+
var HKDF_INFO3 = "concierge-memory-store-v1";
|
|
32043
33268
|
var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
32044
|
-
var
|
|
33269
|
+
var MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
|
|
32045
33270
|
var ConciergeMemoryStore = class {
|
|
32046
33271
|
storage;
|
|
32047
33272
|
encryptionKey;
|
|
@@ -32050,7 +33275,7 @@ var ConciergeMemoryStore = class {
|
|
|
32050
33275
|
locks;
|
|
32051
33276
|
constructor(opts) {
|
|
32052
33277
|
this.storage = opts.storage;
|
|
32053
|
-
this.encryptionKey = derivePurposeKey(opts.masterKey,
|
|
33278
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
|
|
32054
33279
|
this.fortressId = opts.fortressId;
|
|
32055
33280
|
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
32056
33281
|
this.locks = /* @__PURE__ */ new Map();
|
|
@@ -32106,6 +33331,65 @@ var ConciergeMemoryStore = class {
|
|
|
32106
33331
|
}
|
|
32107
33332
|
return turns;
|
|
32108
33333
|
}
|
|
33334
|
+
/**
|
|
33335
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
33336
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
33337
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
33338
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
33339
|
+
* with a concrete cause.
|
|
33340
|
+
*
|
|
33341
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
33342
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
33343
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
33344
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
33345
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
33346
|
+
* - Storage IO error → `io_failed`.
|
|
33347
|
+
*/
|
|
33348
|
+
async readThreadStrict(threadId, opts) {
|
|
33349
|
+
const key = bundleKey(threadId);
|
|
33350
|
+
let raw;
|
|
33351
|
+
try {
|
|
33352
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33353
|
+
} catch {
|
|
33354
|
+
return { ok: false, reason: "io_failed" };
|
|
33355
|
+
}
|
|
33356
|
+
if (!raw) return { ok: true, turns: [] };
|
|
33357
|
+
if (raw.length > MAX_BUNDLE_BYTES3) {
|
|
33358
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
33359
|
+
}
|
|
33360
|
+
let envelope;
|
|
33361
|
+
try {
|
|
33362
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
33363
|
+
} catch {
|
|
33364
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
33365
|
+
}
|
|
33366
|
+
let plaintext;
|
|
33367
|
+
try {
|
|
33368
|
+
const aad = stringToBytes(threadId);
|
|
33369
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
33370
|
+
} catch {
|
|
33371
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
33372
|
+
}
|
|
33373
|
+
let parsed;
|
|
33374
|
+
try {
|
|
33375
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
33376
|
+
} catch {
|
|
33377
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
33378
|
+
}
|
|
33379
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
33380
|
+
if (parsed.thread_id !== threadId) {
|
|
33381
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
33382
|
+
}
|
|
33383
|
+
let turns = parsed.turns;
|
|
33384
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
33385
|
+
const cutoff = opts.sinceTurnId;
|
|
33386
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
33387
|
+
}
|
|
33388
|
+
if (opts?.limit !== void 0) {
|
|
33389
|
+
turns = turns.slice(0, opts.limit);
|
|
33390
|
+
}
|
|
33391
|
+
return { ok: true, turns };
|
|
33392
|
+
}
|
|
32109
33393
|
/**
|
|
32110
33394
|
* Enumerate concierge threads in this fortress with summary metadata.
|
|
32111
33395
|
* Sorted newest-first by last_turn_at.
|
|
@@ -32117,7 +33401,7 @@ var ConciergeMemoryStore = class {
|
|
|
32117
33401
|
);
|
|
32118
33402
|
const summaries = [];
|
|
32119
33403
|
for (const meta of entries) {
|
|
32120
|
-
const threadId =
|
|
33404
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
32121
33405
|
if (threadId === null) continue;
|
|
32122
33406
|
const bundle = await this.loadBundle(threadId);
|
|
32123
33407
|
if (!bundle || bundle.turns.length === 0) continue;
|
|
@@ -32170,7 +33454,7 @@ var ConciergeMemoryStore = class {
|
|
|
32170
33454
|
);
|
|
32171
33455
|
let pruned = 0;
|
|
32172
33456
|
for (const meta of entries) {
|
|
32173
|
-
const threadId =
|
|
33457
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
32174
33458
|
if (threadId === null) continue;
|
|
32175
33459
|
pruned += await this.withLock(threadId, async () => {
|
|
32176
33460
|
const bundle = await this.loadBundle(threadId);
|
|
@@ -32201,7 +33485,7 @@ var ConciergeMemoryStore = class {
|
|
|
32201
33485
|
return null;
|
|
32202
33486
|
}
|
|
32203
33487
|
if (!raw) return null;
|
|
32204
|
-
if (raw.length >
|
|
33488
|
+
if (raw.length > MAX_BUNDLE_BYTES3) return null;
|
|
32205
33489
|
try {
|
|
32206
33490
|
const envelope = JSON.parse(bytesToString(raw));
|
|
32207
33491
|
const aad = stringToBytes(threadId);
|
|
@@ -32254,7 +33538,7 @@ var ConciergeMemoryStore = class {
|
|
|
32254
33538
|
function bundleKey(threadId) {
|
|
32255
33539
|
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
32256
33540
|
}
|
|
32257
|
-
function
|
|
33541
|
+
function stripKeyPrefix2(key) {
|
|
32258
33542
|
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
32259
33543
|
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
32260
33544
|
}
|
|
@@ -32323,7 +33607,18 @@ function buildV11Bindings(inputs) {
|
|
|
32323
33607
|
registry
|
|
32324
33608
|
}),
|
|
32325
33609
|
conciergePiiFilter: buildConciergePiiFilter(),
|
|
32326
|
-
conciergeMemory
|
|
33610
|
+
conciergeMemory,
|
|
33611
|
+
conciergeContextFetchers: buildConciergeContextFetchers({
|
|
33612
|
+
auditLog: inputs.auditLog,
|
|
33613
|
+
identityId: inputs.identityId,
|
|
33614
|
+
registry
|
|
33615
|
+
}),
|
|
33616
|
+
...inputs.intelligenceSelector ? {
|
|
33617
|
+
conciergeContextLlmAssist: buildConciergeContextLlmAssist({
|
|
33618
|
+
selector: inputs.intelligenceSelector,
|
|
33619
|
+
identityId: inputs.identityId
|
|
33620
|
+
})
|
|
33621
|
+
} : {}
|
|
32327
33622
|
});
|
|
32328
33623
|
}
|
|
32329
33624
|
const hubService = new HubService({
|
|
@@ -32384,6 +33679,107 @@ function buildConciergeContextProviders(args) {
|
|
|
32384
33679
|
}
|
|
32385
33680
|
};
|
|
32386
33681
|
}
|
|
33682
|
+
function buildConciergeContextFetchers(args) {
|
|
33683
|
+
const empty = async () => "";
|
|
33684
|
+
return {
|
|
33685
|
+
templates: async () => {
|
|
33686
|
+
const entries = listTemplates();
|
|
33687
|
+
if (entries.length === 0) return "(no templates installed)";
|
|
33688
|
+
const lines = entries.map((e) => {
|
|
33689
|
+
const m = e.metadata;
|
|
33690
|
+
return `${m.name} (tier ${m.tier}, channel ${m.channel}, target ${m.target_archetype})`;
|
|
33691
|
+
});
|
|
33692
|
+
return lines.join("\n");
|
|
33693
|
+
},
|
|
33694
|
+
agent_state: async (agentNameHint) => {
|
|
33695
|
+
const records = args.registry.list({ identity_id: args.identityId });
|
|
33696
|
+
if (records.length === 0) return "(no wrapped agents)";
|
|
33697
|
+
const filtered = agentNameHint ? records.filter(
|
|
33698
|
+
(r) => r.agent_id.toLowerCase().includes(agentNameHint.toLowerCase()) || r.harness.toLowerCase().includes(agentNameHint.toLowerCase())
|
|
33699
|
+
) : records;
|
|
33700
|
+
const target = filtered.length > 0 ? filtered : records;
|
|
33701
|
+
const lines = target.slice(0, 20).map((r) => {
|
|
33702
|
+
const tmpl = typeof r.channel_template_id === "string" ? r.channel_template_id : "no_template";
|
|
33703
|
+
return `${r.agent_id} harness=${r.harness} status=${r.status} template=${tmpl}`;
|
|
33704
|
+
});
|
|
33705
|
+
return lines.join("\n");
|
|
33706
|
+
},
|
|
33707
|
+
agent_activity: async (agentNameHint) => {
|
|
33708
|
+
const result = await args.auditLog.query({ limit: 50 });
|
|
33709
|
+
const owned = result.entries.filter(
|
|
33710
|
+
(e) => e.identity_id === args.identityId
|
|
33711
|
+
);
|
|
33712
|
+
const filtered = agentNameHint ? owned.filter((e) => {
|
|
33713
|
+
const agentId = e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : "";
|
|
33714
|
+
return agentId.toLowerCase().includes(agentNameHint.toLowerCase());
|
|
33715
|
+
}) : owned;
|
|
33716
|
+
const tail = (filtered.length > 0 ? filtered : owned).slice(-20);
|
|
33717
|
+
if (tail.length === 0) return "(no activity)";
|
|
33718
|
+
return tail.map((e) => {
|
|
33719
|
+
const agentId = (e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : null) ?? "_fortress";
|
|
33720
|
+
return `${e.timestamp} ${e.layer}.${e.operation} agent=${agentId} result=${e.result}`;
|
|
33721
|
+
}).join("\n");
|
|
33722
|
+
},
|
|
33723
|
+
audit_log: async () => {
|
|
33724
|
+
const result = await args.auditLog.query({ limit: 30 });
|
|
33725
|
+
const owned = result.entries.filter(
|
|
33726
|
+
(e) => e.identity_id === args.identityId
|
|
33727
|
+
);
|
|
33728
|
+
if (owned.length === 0) return "(no audit log entries)";
|
|
33729
|
+
return owned.slice(-30).map(
|
|
33730
|
+
(e) => `${e.timestamp} ${e.layer}.${e.operation} result=${e.result}`
|
|
33731
|
+
).join("\n");
|
|
33732
|
+
},
|
|
33733
|
+
sentinel_findings: empty,
|
|
33734
|
+
anomaly_alerts: empty,
|
|
33735
|
+
recent_receipts: async () => {
|
|
33736
|
+
const result = await args.auditLog.query({ limit: 100 });
|
|
33737
|
+
const owned = result.entries.filter(
|
|
33738
|
+
(e) => e.identity_id === args.identityId && e.operation.startsWith("composition_")
|
|
33739
|
+
);
|
|
33740
|
+
if (owned.length === 0) return "(no recent composition events)";
|
|
33741
|
+
return owned.slice(-15).map((e) => `${e.timestamp} ${e.operation} result=${e.result}`).join("\n");
|
|
33742
|
+
},
|
|
33743
|
+
verascore_deltas: empty
|
|
33744
|
+
};
|
|
33745
|
+
}
|
|
33746
|
+
function buildConciergeContextLlmAssist(args) {
|
|
33747
|
+
return async (query, categories) => {
|
|
33748
|
+
const labelList = categories.map((c) => `- ${c}`).join("\n");
|
|
33749
|
+
const prompt = `You are a router. Classify the operator's query into one of the categories below or "none".
|
|
33750
|
+
Reply with exactly one token: one category name or "none".
|
|
33751
|
+
|
|
33752
|
+
Categories:
|
|
33753
|
+
${labelList}
|
|
33754
|
+
|
|
33755
|
+
Query: ${query}
|
|
33756
|
+
|
|
33757
|
+
Category:`;
|
|
33758
|
+
try {
|
|
33759
|
+
const handle = await args.selector.getSubstrate("concierge");
|
|
33760
|
+
if (!handle.capability.summarize) return "none";
|
|
33761
|
+
const response = await args.selector.invokeSummarize("concierge", {
|
|
33762
|
+
kind: "summarize",
|
|
33763
|
+
context: prompt,
|
|
33764
|
+
query: "Output the single category token.",
|
|
33765
|
+
maxTokens: 16
|
|
33766
|
+
});
|
|
33767
|
+
if (response.failureClass || response.body.kind !== "summarize") {
|
|
33768
|
+
return "none";
|
|
33769
|
+
}
|
|
33770
|
+
const raw = response.body.text.trim().toLowerCase();
|
|
33771
|
+
const head = raw.split(/\s|[.,!?:;]/)[0] ?? "";
|
|
33772
|
+
const normalized = head.replace(/[^a-z_]/g, "");
|
|
33773
|
+
const known = categories;
|
|
33774
|
+
if (known.includes(normalized)) {
|
|
33775
|
+
return normalized;
|
|
33776
|
+
}
|
|
33777
|
+
return "none";
|
|
33778
|
+
} catch {
|
|
33779
|
+
return "none";
|
|
33780
|
+
}
|
|
33781
|
+
};
|
|
33782
|
+
}
|
|
32387
33783
|
function buildConciergePiiFilter() {
|
|
32388
33784
|
return {
|
|
32389
33785
|
filter(input) {
|
|
@@ -32515,13 +33911,13 @@ init_encryption();
|
|
|
32515
33911
|
init_encoding();
|
|
32516
33912
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
32517
33913
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
32518
|
-
var
|
|
33914
|
+
var HKDF_INFO4 = "intelligence-substrate-config";
|
|
32519
33915
|
var IntelligenceConfigStore = class {
|
|
32520
33916
|
storage;
|
|
32521
33917
|
encryptionKey;
|
|
32522
33918
|
constructor(storage, masterKey) {
|
|
32523
33919
|
this.storage = storage;
|
|
32524
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33920
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
|
|
32525
33921
|
}
|
|
32526
33922
|
/**
|
|
32527
33923
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -35090,7 +36486,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
35090
36486
|
}
|
|
35091
36487
|
return null;
|
|
35092
36488
|
}
|
|
35093
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
36489
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
35094
36490
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
35095
36491
|
if (!destinationSigner) {
|
|
35096
36492
|
return {
|
|
@@ -35152,8 +36548,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
35152
36548
|
}
|
|
35153
36549
|
}
|
|
35154
36550
|
}
|
|
36551
|
+
let plaintext;
|
|
35155
36552
|
try {
|
|
35156
|
-
|
|
36553
|
+
plaintext = decrypt(
|
|
35157
36554
|
item.entry.payload,
|
|
35158
36555
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
35159
36556
|
);
|
|
@@ -35162,28 +36559,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
35162
36559
|
skipped++;
|
|
35163
36560
|
continue;
|
|
35164
36561
|
}
|
|
35165
|
-
await stateStore.write(
|
|
35166
|
-
item.namespace,
|
|
35167
|
-
item.key,
|
|
35168
|
-
bytesToString(plaintext),
|
|
35169
|
-
destinationSigner.identity_id,
|
|
35170
|
-
destinationSigner.encrypted_private_key,
|
|
35171
|
-
identityEncryptionKey,
|
|
35172
|
-
{
|
|
35173
|
-
content_type: item.entry.metadata.content_type,
|
|
35174
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
35175
|
-
tags: [
|
|
35176
|
-
...item.entry.metadata.tags ?? [],
|
|
35177
|
-
"exit-import",
|
|
35178
|
-
`source:${item.entry.kid}`
|
|
35179
|
-
]
|
|
35180
|
-
}
|
|
35181
|
-
);
|
|
35182
|
-
imported++;
|
|
35183
36562
|
} catch {
|
|
35184
36563
|
skippedInvalidSig++;
|
|
35185
36564
|
skipped++;
|
|
36565
|
+
continue;
|
|
35186
36566
|
}
|
|
36567
|
+
await stateStore.write(
|
|
36568
|
+
item.namespace,
|
|
36569
|
+
item.key,
|
|
36570
|
+
bytesToString(plaintext),
|
|
36571
|
+
destinationSigner.identity_id,
|
|
36572
|
+
destinationSigner.encrypted_private_key,
|
|
36573
|
+
identityEncryptionKey,
|
|
36574
|
+
{
|
|
36575
|
+
content_type: item.entry.metadata.content_type,
|
|
36576
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
36577
|
+
tags: [
|
|
36578
|
+
...item.entry.metadata.tags ?? [],
|
|
36579
|
+
"exit-import",
|
|
36580
|
+
`source:${item.entry.kid}`
|
|
36581
|
+
]
|
|
36582
|
+
}
|
|
36583
|
+
);
|
|
36584
|
+
imported++;
|
|
36585
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
35187
36586
|
}
|
|
35188
36587
|
return {
|
|
35189
36588
|
status: "rekeyed",
|
|
@@ -35194,6 +36593,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
35194
36593
|
conflicts
|
|
35195
36594
|
};
|
|
35196
36595
|
}
|
|
36596
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
36597
|
+
let removed = 0;
|
|
36598
|
+
const failed = [];
|
|
36599
|
+
for (const loc of staged) {
|
|
36600
|
+
try {
|
|
36601
|
+
const ok = await storage.delete(loc.namespace, loc.key);
|
|
36602
|
+
if (ok) {
|
|
36603
|
+
removed++;
|
|
36604
|
+
} else {
|
|
36605
|
+
failed.push(loc);
|
|
36606
|
+
}
|
|
36607
|
+
} catch {
|
|
36608
|
+
failed.push(loc);
|
|
36609
|
+
}
|
|
36610
|
+
}
|
|
36611
|
+
return { removed, failed };
|
|
36612
|
+
}
|
|
35197
36613
|
async function stageArtifact(storage, namespace, key, value) {
|
|
35198
36614
|
await storage.write(namespace, key, jsonBytes(value));
|
|
35199
36615
|
}
|
|
@@ -35318,6 +36734,8 @@ async function importExitBundle(opts) {
|
|
|
35318
36734
|
}
|
|
35319
36735
|
const importId = importIdForManifest(manifest);
|
|
35320
36736
|
const stagedArtifacts = [];
|
|
36737
|
+
const stagedLocations = [];
|
|
36738
|
+
const importedRekeyEntries = [];
|
|
35321
36739
|
if (identityArtifact) {
|
|
35322
36740
|
await stageArtifact(
|
|
35323
36741
|
opts.storage,
|
|
@@ -35326,10 +36744,15 @@ async function importExitBundle(opts) {
|
|
|
35326
36744
|
identityArtifact.json
|
|
35327
36745
|
);
|
|
35328
36746
|
stagedArtifacts.push("public_identity");
|
|
36747
|
+
stagedLocations.push({
|
|
36748
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
36749
|
+
key: identityArtifact.json.bundle.identity_id
|
|
36750
|
+
});
|
|
35329
36751
|
}
|
|
35330
36752
|
if (policySet) {
|
|
35331
36753
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
35332
36754
|
stagedArtifacts.push("policy_set");
|
|
36755
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
35333
36756
|
}
|
|
35334
36757
|
if (auditReceipts) {
|
|
35335
36758
|
await stageArtifact(
|
|
@@ -35339,10 +36762,12 @@ async function importExitBundle(opts) {
|
|
|
35339
36762
|
auditReceipts.json
|
|
35340
36763
|
);
|
|
35341
36764
|
stagedArtifacts.push("audit_receipts");
|
|
36765
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
35342
36766
|
}
|
|
35343
36767
|
if (commitments) {
|
|
35344
36768
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
35345
36769
|
stagedArtifacts.push("commitments");
|
|
36770
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
35346
36771
|
}
|
|
35347
36772
|
if (placeholderMetadata) {
|
|
35348
36773
|
await stageArtifact(
|
|
@@ -35352,12 +36777,17 @@ async function importExitBundle(opts) {
|
|
|
35352
36777
|
placeholderMetadata.json
|
|
35353
36778
|
);
|
|
35354
36779
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
36780
|
+
stagedLocations.push({
|
|
36781
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
36782
|
+
key: importId
|
|
36783
|
+
});
|
|
35355
36784
|
}
|
|
35356
36785
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
35357
36786
|
manifest: manifest.body,
|
|
35358
36787
|
verified_at: verification.verified_at,
|
|
35359
36788
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
35360
36789
|
});
|
|
36790
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
35361
36791
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
35362
36792
|
let reputationResult = {
|
|
35363
36793
|
imported_attestations: 0,
|
|
@@ -35382,26 +36812,57 @@ async function importExitBundle(opts) {
|
|
|
35382
36812
|
encryptedState?.json ?? null,
|
|
35383
36813
|
opts
|
|
35384
36814
|
);
|
|
35385
|
-
|
|
35386
|
-
|
|
35387
|
-
|
|
35388
|
-
|
|
35389
|
-
|
|
35390
|
-
|
|
35391
|
-
|
|
35392
|
-
|
|
35393
|
-
|
|
35394
|
-
|
|
35395
|
-
|
|
35396
|
-
|
|
35397
|
-
|
|
35398
|
-
|
|
35399
|
-
|
|
35400
|
-
|
|
35401
|
-
|
|
35402
|
-
|
|
35403
|
-
|
|
35404
|
-
|
|
36815
|
+
let stateResult;
|
|
36816
|
+
try {
|
|
36817
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
36818
|
+
encryptedState.json,
|
|
36819
|
+
opts,
|
|
36820
|
+
sourceMasterKey,
|
|
36821
|
+
publicKeys.byIdentityId,
|
|
36822
|
+
importedRekeyEntries
|
|
36823
|
+
) : {
|
|
36824
|
+
status: "staged_requires_source_key",
|
|
36825
|
+
imported_keys: 0,
|
|
36826
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
36827
|
+
skipped_invalid_sig: 0,
|
|
36828
|
+
skipped_unknown_kid: 0,
|
|
36829
|
+
conflicts: conflicts.state_conflicts.length
|
|
36830
|
+
} : {
|
|
36831
|
+
status: "not_requested",
|
|
36832
|
+
imported_keys: 0,
|
|
36833
|
+
skipped_keys: 0,
|
|
36834
|
+
skipped_invalid_sig: 0,
|
|
36835
|
+
skipped_unknown_kid: 0,
|
|
36836
|
+
conflicts: 0
|
|
36837
|
+
};
|
|
36838
|
+
} catch (err) {
|
|
36839
|
+
const toCleanup = [
|
|
36840
|
+
...importedRekeyEntries,
|
|
36841
|
+
...stagedLocations
|
|
36842
|
+
];
|
|
36843
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
36844
|
+
opts.auditLog.append(
|
|
36845
|
+
"l1",
|
|
36846
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
36847
|
+
manifest.body.identity_binding.identity_id,
|
|
36848
|
+
{
|
|
36849
|
+
import_id: importId,
|
|
36850
|
+
manifest_version: manifest.body.manifest_version,
|
|
36851
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
36852
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
36853
|
+
removed_total: cleanup.removed,
|
|
36854
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
36855
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
36856
|
+
},
|
|
36857
|
+
"failure"
|
|
36858
|
+
);
|
|
36859
|
+
await opts.auditLog.flush();
|
|
36860
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
36861
|
+
throw new ExitBundleImportError(
|
|
36862
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
36863
|
+
`Exit-bundle re-key failed: ${originalMessage}. Cleanup removed ${cleanup.removed} of ${toCleanup.length} staged paths (${importedRekeyEntries.length} re-keyed entries plus ${stagedLocations.length} staged artifacts; ${cleanup.failed.length} cleanup deletes failed).`
|
|
36864
|
+
);
|
|
36865
|
+
}
|
|
35405
36866
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
35406
36867
|
import_id: importId,
|
|
35407
36868
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -36400,16 +37861,35 @@ ${err.message}
|
|
|
36400
37861
|
timestamp: alert.timestamp
|
|
36401
37862
|
});
|
|
36402
37863
|
} : void 0;
|
|
36403
|
-
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
36404
37864
|
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
36405
37865
|
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37866
|
+
const aggregatorPayloadStore = new AggregatorPayloadStore({
|
|
37867
|
+
storage,
|
|
37868
|
+
masterKey,
|
|
37869
|
+
fortressId: fortressIdForAggregator
|
|
37870
|
+
});
|
|
36406
37871
|
const approvalAggregator = new ApprovalAggregator({
|
|
36407
37872
|
storage,
|
|
36408
37873
|
masterKey,
|
|
36409
37874
|
auditLog,
|
|
36410
37875
|
identityId: aggregatorIdentityId,
|
|
36411
|
-
fortressId: fortressIdForAggregator
|
|
37876
|
+
fortressId: fortressIdForAggregator,
|
|
37877
|
+
payloadStore: aggregatorPayloadStore
|
|
36412
37878
|
});
|
|
37879
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
37880
|
+
underlying: approvalChannel,
|
|
37881
|
+
aggregator: approvalAggregator,
|
|
37882
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
37883
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
37884
|
+
});
|
|
37885
|
+
const gate = new ApprovalGate(
|
|
37886
|
+
policy,
|
|
37887
|
+
baseline,
|
|
37888
|
+
wrappedApprovalChannel,
|
|
37889
|
+
auditLog,
|
|
37890
|
+
injectionDetector,
|
|
37891
|
+
onInjectionAlert
|
|
37892
|
+
);
|
|
36413
37893
|
gate.setApprovalEventCallback((event) => {
|
|
36414
37894
|
void approvalAggregator.ingest(event);
|
|
36415
37895
|
});
|