@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.js
CHANGED
|
@@ -4254,6 +4254,10 @@ var DEFAULT_CHANNEL = {
|
|
|
4254
4254
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4255
4255
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4256
4256
|
};
|
|
4257
|
+
var DEFAULT_APPROVAL_REDIRECT = {
|
|
4258
|
+
enabled: false,
|
|
4259
|
+
mode: "replace"
|
|
4260
|
+
};
|
|
4257
4261
|
var DEFAULT_POLICY = {
|
|
4258
4262
|
version: 1,
|
|
4259
4263
|
tier1_always_approve: [
|
|
@@ -4327,6 +4331,7 @@ var DEFAULT_POLICY = {
|
|
|
4327
4331
|
"handshake_status",
|
|
4328
4332
|
"handshake_exchange",
|
|
4329
4333
|
"handshake_verify_attestation",
|
|
4334
|
+
"handshake_abort",
|
|
4330
4335
|
"reputation_query_weighted",
|
|
4331
4336
|
"federation_peers",
|
|
4332
4337
|
"federation_trust_evaluate",
|
|
@@ -4368,7 +4373,8 @@ var DEFAULT_POLICY = {
|
|
|
4368
4373
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4369
4374
|
// Read-only; rule-based Annex III classifier
|
|
4370
4375
|
],
|
|
4371
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4376
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4377
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4372
4378
|
};
|
|
4373
4379
|
function extractOperationName(toolName) {
|
|
4374
4380
|
if (toolName.startsWith("proxy/")) {
|
|
@@ -4465,9 +4471,35 @@ function validatePolicy(raw) {
|
|
|
4465
4471
|
};
|
|
4466
4472
|
delete merged.auto_deny;
|
|
4467
4473
|
return merged;
|
|
4468
|
-
})()
|
|
4474
|
+
})(),
|
|
4475
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4469
4476
|
};
|
|
4470
4477
|
}
|
|
4478
|
+
function parseApprovalRedirect(raw) {
|
|
4479
|
+
if (raw === void 0 || raw === null) {
|
|
4480
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4481
|
+
}
|
|
4482
|
+
if (typeof raw !== "object") {
|
|
4483
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4484
|
+
}
|
|
4485
|
+
const obj = raw;
|
|
4486
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4487
|
+
const modeRaw = obj.mode;
|
|
4488
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4489
|
+
if (modeRaw !== void 0) {
|
|
4490
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4491
|
+
throw new Error(
|
|
4492
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4493
|
+
);
|
|
4494
|
+
}
|
|
4495
|
+
mode = modeRaw;
|
|
4496
|
+
}
|
|
4497
|
+
const result = { enabled, mode };
|
|
4498
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4499
|
+
result.per_agent = obj.per_agent;
|
|
4500
|
+
}
|
|
4501
|
+
return result;
|
|
4502
|
+
}
|
|
4471
4503
|
function generateDefaultPolicyYaml() {
|
|
4472
4504
|
return `# Sanctuary Principal Policy v1
|
|
4473
4505
|
# This file controls what your agent can do without asking.
|
|
@@ -4546,6 +4578,7 @@ tier3_always_allow:
|
|
|
4546
4578
|
- handshake_status
|
|
4547
4579
|
- handshake_exchange
|
|
4548
4580
|
- handshake_verify_attestation
|
|
4581
|
+
- handshake_abort
|
|
4549
4582
|
- reputation_query_weighted
|
|
4550
4583
|
- federation_peers
|
|
4551
4584
|
- federation_trust_evaluate
|
|
@@ -4580,6 +4613,21 @@ tier3_always_allow:
|
|
|
4580
4613
|
approval_channel:
|
|
4581
4614
|
type: stderr
|
|
4582
4615
|
timeout_seconds: 300
|
|
4616
|
+
|
|
4617
|
+
# \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
|
|
4618
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4619
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4620
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4621
|
+
#
|
|
4622
|
+
# mode:
|
|
4623
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4624
|
+
# decision from the inbox (default once enabled).
|
|
4625
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4626
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4627
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4628
|
+
approval_redirect:
|
|
4629
|
+
enabled: false
|
|
4630
|
+
mode: replace
|
|
4583
4631
|
`;
|
|
4584
4632
|
}
|
|
4585
4633
|
var MalformedPrincipalPolicyError = class extends Error {
|
|
@@ -16264,6 +16312,27 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
16264
16312
|
await handleStream2(deps, res);
|
|
16265
16313
|
return true;
|
|
16266
16314
|
}
|
|
16315
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
|
|
16316
|
+
const limit = parseLimit2(
|
|
16317
|
+
url.searchParams.get("limit"),
|
|
16318
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
16319
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
16320
|
+
);
|
|
16321
|
+
const statusRaw = url.searchParams.get("status");
|
|
16322
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
16323
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16324
|
+
const filterStatus = statusRaw && isStatusFilter(statusRaw) && statusRaw !== "pending" ? statusRaw : void 0;
|
|
16325
|
+
const entries = await deps.aggregator.getHistory(
|
|
16326
|
+
{
|
|
16327
|
+
limit,
|
|
16328
|
+
...filterStatus !== void 0 ? { status: filterStatus } : {},
|
|
16329
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
16330
|
+
},
|
|
16331
|
+
operatorId
|
|
16332
|
+
);
|
|
16333
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
16334
|
+
return true;
|
|
16335
|
+
}
|
|
16267
16336
|
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
16268
16337
|
const limit = parseLimit2(
|
|
16269
16338
|
url.searchParams.get("limit"),
|
|
@@ -16286,11 +16355,39 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
16286
16355
|
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16287
16356
|
return true;
|
|
16288
16357
|
}
|
|
16289
|
-
if (method === "GET" && entryMatch.action ===
|
|
16290
|
-
const
|
|
16291
|
-
|
|
16292
|
-
(
|
|
16358
|
+
if (method === "GET" && entryMatch.action === "audit-trail") {
|
|
16359
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
16360
|
+
if (!entry) {
|
|
16361
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16362
|
+
return true;
|
|
16363
|
+
}
|
|
16364
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16365
|
+
const trail = await deps.aggregator.getAuditTrail(
|
|
16366
|
+
entryMatch.aggregatorId,
|
|
16367
|
+
operatorId
|
|
16368
|
+
);
|
|
16369
|
+
writeJSON4(res, 200, { ok: true, data: { entry, audit_trail: trail } });
|
|
16370
|
+
return true;
|
|
16371
|
+
}
|
|
16372
|
+
if (method === "GET" && entryMatch.action === "payload") {
|
|
16373
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
16374
|
+
if (!entry) {
|
|
16375
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16376
|
+
return true;
|
|
16377
|
+
}
|
|
16378
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16379
|
+
const payload = await deps.aggregator.getFullPayloadWithAudit(
|
|
16380
|
+
entryMatch.aggregatorId,
|
|
16381
|
+
operatorId
|
|
16293
16382
|
);
|
|
16383
|
+
writeJSON4(res, 200, {
|
|
16384
|
+
ok: true,
|
|
16385
|
+
data: { entry, request_payload: payload }
|
|
16386
|
+
});
|
|
16387
|
+
return true;
|
|
16388
|
+
}
|
|
16389
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
16390
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
16294
16391
|
if (!entry) {
|
|
16295
16392
|
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16296
16393
|
return true;
|
|
@@ -19277,7 +19374,10 @@ var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
|
19277
19374
|
var APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
19278
19375
|
AGGREGATED: "cross_harness_approval_aggregated",
|
|
19279
19376
|
RESOLVED: "cross_harness_approval_resolved",
|
|
19280
|
-
DEDUPED: "cross_harness_approval_deduped"
|
|
19377
|
+
DEDUPED: "cross_harness_approval_deduped",
|
|
19378
|
+
PAYLOAD_DECRYPTED: "cross_harness_approval_payload_decrypted",
|
|
19379
|
+
AUDIT_TRAIL_VIEWED: "cross_harness_approval_audit_trail_viewed",
|
|
19380
|
+
REPLAYED: "cross_harness_approval_replayed"
|
|
19281
19381
|
};
|
|
19282
19382
|
var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
19283
19383
|
var DEFAULT_MAX_LIST_LIMIT = 200;
|
|
@@ -19293,6 +19393,8 @@ var ApprovalAggregator = class {
|
|
|
19293
19393
|
now;
|
|
19294
19394
|
resolveSourceContext;
|
|
19295
19395
|
resolveHubInboxItemId;
|
|
19396
|
+
payloadStore;
|
|
19397
|
+
resolveEnforcementChain;
|
|
19296
19398
|
/** Cached entries by `aggregator_id`. */
|
|
19297
19399
|
entries = /* @__PURE__ */ new Map();
|
|
19298
19400
|
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
@@ -19322,6 +19424,14 @@ var ApprovalAggregator = class {
|
|
|
19322
19424
|
source_agent_id: this.fortressId
|
|
19323
19425
|
}));
|
|
19324
19426
|
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
19427
|
+
this.payloadStore = deps.payloadStore ?? null;
|
|
19428
|
+
this.resolveEnforcementChain = deps.resolveEnforcementChain ?? ((event) => [
|
|
19429
|
+
{
|
|
19430
|
+
layer: "l2",
|
|
19431
|
+
event: `approval_required:${event.operation}`,
|
|
19432
|
+
timestamp: event.request_timestamp
|
|
19433
|
+
}
|
|
19434
|
+
]);
|
|
19325
19435
|
}
|
|
19326
19436
|
/**
|
|
19327
19437
|
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
@@ -19372,13 +19482,152 @@ var ApprovalAggregator = class {
|
|
|
19372
19482
|
}
|
|
19373
19483
|
/**
|
|
19374
19484
|
* Return the original (unhashed) request payload for the entry. Returns
|
|
19375
|
-
* `null` when the entry is unknown
|
|
19376
|
-
*
|
|
19485
|
+
* `null` when the entry is unknown. When the in-memory payload map has
|
|
19486
|
+
* been evicted (e.g. after a server restart) and a `payloadStore` was
|
|
19487
|
+
* provided, the at-rest bundle is decrypted and the in-memory map is
|
|
19488
|
+
* refilled. Audit emission lives on the `*WithAudit` variant; this base
|
|
19489
|
+
* accessor is silent so internal callers can read without polluting the
|
|
19490
|
+
* audit trail.
|
|
19377
19491
|
*/
|
|
19378
19492
|
async getFullPayload(aggregatorId) {
|
|
19379
19493
|
await this.hydrate();
|
|
19380
19494
|
if (!this.entries.has(aggregatorId)) return null;
|
|
19381
|
-
|
|
19495
|
+
const cached = this.fullPayloads.get(aggregatorId);
|
|
19496
|
+
if (cached !== void 0) return cached;
|
|
19497
|
+
if (this.payloadStore) {
|
|
19498
|
+
try {
|
|
19499
|
+
const restored = await this.payloadStore.loadPayload(aggregatorId);
|
|
19500
|
+
if (restored !== null) {
|
|
19501
|
+
this.fullPayloads.set(aggregatorId, restored);
|
|
19502
|
+
return restored;
|
|
19503
|
+
}
|
|
19504
|
+
} catch {
|
|
19505
|
+
}
|
|
19506
|
+
}
|
|
19507
|
+
return null;
|
|
19508
|
+
}
|
|
19509
|
+
/**
|
|
19510
|
+
* Return the entry record for the given id, or null when unknown.
|
|
19511
|
+
* Idempotent. v1.3 Upsilon-3.
|
|
19512
|
+
*/
|
|
19513
|
+
async getEntry(aggregatorId) {
|
|
19514
|
+
await this.hydrate();
|
|
19515
|
+
return this.entries.get(aggregatorId) ?? null;
|
|
19516
|
+
}
|
|
19517
|
+
/**
|
|
19518
|
+
* Audited variant of `getFullPayload`. Emits the
|
|
19519
|
+
* `cross_harness_approval_payload_decrypted` audit event before
|
|
19520
|
+
* returning. Used by the operator-facing /payload replay route.
|
|
19521
|
+
* v1.3 Upsilon-3.
|
|
19522
|
+
*/
|
|
19523
|
+
async getFullPayloadWithAudit(aggregatorId, operatorId) {
|
|
19524
|
+
const payload = await this.getFullPayload(aggregatorId);
|
|
19525
|
+
if (payload === null) return null;
|
|
19526
|
+
const entry = this.entries.get(aggregatorId);
|
|
19527
|
+
this.auditLog.append(
|
|
19528
|
+
"l2",
|
|
19529
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.PAYLOAD_DECRYPTED,
|
|
19530
|
+
operatorId,
|
|
19531
|
+
{
|
|
19532
|
+
aggregator_id: aggregatorId,
|
|
19533
|
+
...entry ? {
|
|
19534
|
+
source_harness: entry.source_harness,
|
|
19535
|
+
source_agent_id: entry.source_agent_id,
|
|
19536
|
+
entry_status: entry.status
|
|
19537
|
+
} : {}
|
|
19538
|
+
}
|
|
19539
|
+
);
|
|
19540
|
+
return payload;
|
|
19541
|
+
}
|
|
19542
|
+
/**
|
|
19543
|
+
* Return the audit-log entries that led to and surround this approval.
|
|
19544
|
+
* Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
|
|
19545
|
+
* DEDUPED, replay events) all carry `details.aggregator_id` and link
|
|
19546
|
+
* directly. Gate-side emissions (`gate_*:operation`) do not carry the
|
|
19547
|
+
* aggregator id at v1.3, so they are matched via timestamp window
|
|
19548
|
+
* (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
|
|
19549
|
+
* pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
|
|
19550
|
+
* v1.3 Upsilon-3.
|
|
19551
|
+
*/
|
|
19552
|
+
async getAuditTrail(aggregatorId, operatorId) {
|
|
19553
|
+
await this.hydrate();
|
|
19554
|
+
const entry = this.entries.get(aggregatorId);
|
|
19555
|
+
if (!entry) {
|
|
19556
|
+
return [];
|
|
19557
|
+
}
|
|
19558
|
+
const sinceMs = Date.parse(entry.created_at) - 1e3;
|
|
19559
|
+
const sinceIso = new Date(sinceMs).toISOString();
|
|
19560
|
+
const queried = await this.auditLog.query({ since: sinceIso, limit: 1e3 });
|
|
19561
|
+
const operationPart = entry.policy_rule_id.includes(":") ? entry.policy_rule_id.slice(entry.policy_rule_id.indexOf(":") + 1) : entry.policy_rule_id;
|
|
19562
|
+
const lifetimeStart = sinceMs;
|
|
19563
|
+
const lifetimeEnd = entry.resolved_at ? Date.parse(entry.resolved_at) + 1e3 : Date.parse(entry.expires_at) + 1e3;
|
|
19564
|
+
const matches = [];
|
|
19565
|
+
for (const audit of queried.entries) {
|
|
19566
|
+
const detailsId = audit.details !== void 0 ? audit.details["aggregator_id"] : void 0;
|
|
19567
|
+
if (detailsId === aggregatorId) {
|
|
19568
|
+
matches.push(audit);
|
|
19569
|
+
continue;
|
|
19570
|
+
}
|
|
19571
|
+
const auditMs = Date.parse(audit.timestamp);
|
|
19572
|
+
if (auditMs < lifetimeStart || auditMs > lifetimeEnd) continue;
|
|
19573
|
+
if (audit.operation.endsWith(`:${operationPart}`)) {
|
|
19574
|
+
matches.push(audit);
|
|
19575
|
+
}
|
|
19576
|
+
}
|
|
19577
|
+
matches.sort(
|
|
19578
|
+
(a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
|
|
19579
|
+
);
|
|
19580
|
+
this.auditLog.append(
|
|
19581
|
+
"l2",
|
|
19582
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AUDIT_TRAIL_VIEWED,
|
|
19583
|
+
operatorId,
|
|
19584
|
+
{
|
|
19585
|
+
aggregator_id: aggregatorId,
|
|
19586
|
+
entry_status: entry.status,
|
|
19587
|
+
match_count: matches.length
|
|
19588
|
+
}
|
|
19589
|
+
);
|
|
19590
|
+
return matches;
|
|
19591
|
+
}
|
|
19592
|
+
/**
|
|
19593
|
+
* List historical (resolved) approvals. Excludes pending entries by
|
|
19594
|
+
* design: `list()` is the pending-inbox surface and `getHistory()` is
|
|
19595
|
+
* the resolved-replay surface. Emits REPLAYED on each call. v1.3
|
|
19596
|
+
* Upsilon-3.
|
|
19597
|
+
*/
|
|
19598
|
+
async getHistory(opts, operatorId) {
|
|
19599
|
+
await this.hydrate();
|
|
19600
|
+
await this.expireStale();
|
|
19601
|
+
const limit = Math.min(
|
|
19602
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
19603
|
+
this.maxListLimit
|
|
19604
|
+
);
|
|
19605
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
19606
|
+
const matching = [];
|
|
19607
|
+
for (const entry of this.entries.values()) {
|
|
19608
|
+
if (entry.status === "pending") continue;
|
|
19609
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
19610
|
+
const stamp = Date.parse(entry.resolved_at ?? entry.created_at);
|
|
19611
|
+
if (stamp < sinceMs) continue;
|
|
19612
|
+
matching.push(entry);
|
|
19613
|
+
}
|
|
19614
|
+
matching.sort((a, b) => {
|
|
19615
|
+
const aStamp = a.resolved_at ?? a.created_at;
|
|
19616
|
+
const bStamp = b.resolved_at ?? b.created_at;
|
|
19617
|
+
return bStamp.localeCompare(aStamp);
|
|
19618
|
+
});
|
|
19619
|
+
const sliced = matching.slice(0, limit);
|
|
19620
|
+
this.auditLog.append(
|
|
19621
|
+
"l2",
|
|
19622
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.REPLAYED,
|
|
19623
|
+
operatorId,
|
|
19624
|
+
{
|
|
19625
|
+
result_count: sliced.length,
|
|
19626
|
+
...opts?.status !== void 0 ? { status_filter: opts.status } : {},
|
|
19627
|
+
...opts?.sinceTs !== void 0 ? { since: opts.sinceTs } : {}
|
|
19628
|
+
}
|
|
19629
|
+
);
|
|
19630
|
+
return sliced;
|
|
19382
19631
|
}
|
|
19383
19632
|
/**
|
|
19384
19633
|
* Resolve an entry. Used by both:
|
|
@@ -19452,6 +19701,7 @@ var ApprovalAggregator = class {
|
|
|
19452
19701
|
const now = this.now();
|
|
19453
19702
|
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
19454
19703
|
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
19704
|
+
const enforcementChain = this.resolveEnforcementChain(event);
|
|
19455
19705
|
const entry = {
|
|
19456
19706
|
aggregator_id: id,
|
|
19457
19707
|
source_harness: ctx.source_harness,
|
|
@@ -19463,13 +19713,20 @@ var ApprovalAggregator = class {
|
|
|
19463
19713
|
status: "pending",
|
|
19464
19714
|
created_at: now.toISOString(),
|
|
19465
19715
|
expires_at: expires.toISOString(),
|
|
19466
|
-
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19716
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
|
|
19717
|
+
...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
|
|
19467
19718
|
};
|
|
19468
19719
|
this.entries.set(id, entry);
|
|
19469
19720
|
this.dedupIndex.set(dedupKey, id);
|
|
19470
19721
|
this.correlationIndex.set(event.correlation_id, id);
|
|
19471
19722
|
this.fullPayloads.set(id, event.context);
|
|
19472
19723
|
await this.persist(entry);
|
|
19724
|
+
if (this.payloadStore) {
|
|
19725
|
+
try {
|
|
19726
|
+
await this.payloadStore.savePayload(id, event.context);
|
|
19727
|
+
} catch {
|
|
19728
|
+
}
|
|
19729
|
+
}
|
|
19473
19730
|
this.auditLog.append(
|
|
19474
19731
|
"l2",
|
|
19475
19732
|
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
@@ -19615,6 +19872,306 @@ var ApprovalAggregator = class {
|
|
|
19615
19872
|
}
|
|
19616
19873
|
};
|
|
19617
19874
|
|
|
19875
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
19876
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
19877
|
+
function auditEntryIdFor(request) {
|
|
19878
|
+
return `${request.timestamp}:${request.operation}`;
|
|
19879
|
+
}
|
|
19880
|
+
function statusToDecision(entry) {
|
|
19881
|
+
switch (entry.status) {
|
|
19882
|
+
case "approved":
|
|
19883
|
+
return {
|
|
19884
|
+
decision: "approve",
|
|
19885
|
+
decided_by: "human"
|
|
19886
|
+
};
|
|
19887
|
+
case "denied":
|
|
19888
|
+
return {
|
|
19889
|
+
decision: "deny",
|
|
19890
|
+
decided_by: "human"
|
|
19891
|
+
};
|
|
19892
|
+
case "timeout":
|
|
19893
|
+
case "expired":
|
|
19894
|
+
return {
|
|
19895
|
+
decision: "deny",
|
|
19896
|
+
decided_by: "timeout"
|
|
19897
|
+
};
|
|
19898
|
+
default:
|
|
19899
|
+
return null;
|
|
19900
|
+
}
|
|
19901
|
+
}
|
|
19902
|
+
var AggregatorBackedChannel = class {
|
|
19903
|
+
underlying;
|
|
19904
|
+
aggregator;
|
|
19905
|
+
resolveRedirect;
|
|
19906
|
+
replaceModeTimeoutMs;
|
|
19907
|
+
now;
|
|
19908
|
+
constructor(opts) {
|
|
19909
|
+
this.underlying = opts.underlying;
|
|
19910
|
+
this.aggregator = opts.aggregator;
|
|
19911
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
19912
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
19913
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
19914
|
+
}
|
|
19915
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
19916
|
+
getUnderlying() {
|
|
19917
|
+
return this.underlying;
|
|
19918
|
+
}
|
|
19919
|
+
async requestApproval(request) {
|
|
19920
|
+
const cfg = this.resolveRedirect(request);
|
|
19921
|
+
if (!cfg.enabled) {
|
|
19922
|
+
return this.underlying.requestApproval(request);
|
|
19923
|
+
}
|
|
19924
|
+
if (cfg.mode === "replace") {
|
|
19925
|
+
return this.awaitAggregatorDecision(request);
|
|
19926
|
+
}
|
|
19927
|
+
return this.notifyMode(request);
|
|
19928
|
+
}
|
|
19929
|
+
/**
|
|
19930
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
19931
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
19932
|
+
* between list and subscribe). Match incoming events to this request by
|
|
19933
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
19934
|
+
*/
|
|
19935
|
+
async awaitAggregatorDecision(request) {
|
|
19936
|
+
const auditId = auditEntryIdFor(request);
|
|
19937
|
+
return new Promise((resolveOuter) => {
|
|
19938
|
+
let settled = false;
|
|
19939
|
+
let unsubscribe = null;
|
|
19940
|
+
let timeoutHandle = null;
|
|
19941
|
+
const settle = (response) => {
|
|
19942
|
+
if (settled) return;
|
|
19943
|
+
settled = true;
|
|
19944
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
19945
|
+
if (unsubscribe) {
|
|
19946
|
+
try {
|
|
19947
|
+
unsubscribe();
|
|
19948
|
+
} catch {
|
|
19949
|
+
}
|
|
19950
|
+
}
|
|
19951
|
+
resolveOuter(response);
|
|
19952
|
+
};
|
|
19953
|
+
const onEvent = (emit) => {
|
|
19954
|
+
if (emit.type !== "resolved") return;
|
|
19955
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
19956
|
+
const mapped = statusToDecision(emit.entry);
|
|
19957
|
+
if (!mapped) return;
|
|
19958
|
+
settle({
|
|
19959
|
+
decision: mapped.decision,
|
|
19960
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
19961
|
+
decided_by: mapped.decided_by
|
|
19962
|
+
});
|
|
19963
|
+
};
|
|
19964
|
+
try {
|
|
19965
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
19966
|
+
} catch (err) {
|
|
19967
|
+
settle({
|
|
19968
|
+
decision: "deny",
|
|
19969
|
+
decided_at: this.now().toISOString(),
|
|
19970
|
+
decided_by: "channel_failure"
|
|
19971
|
+
});
|
|
19972
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
19973
|
+
}
|
|
19974
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
19975
|
+
for (const entry of entries) {
|
|
19976
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
19977
|
+
const mapped = statusToDecision(entry);
|
|
19978
|
+
if (!mapped) return;
|
|
19979
|
+
settle({
|
|
19980
|
+
decision: mapped.decision,
|
|
19981
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
19982
|
+
decided_by: mapped.decided_by
|
|
19983
|
+
});
|
|
19984
|
+
return;
|
|
19985
|
+
}
|
|
19986
|
+
}).catch(() => {
|
|
19987
|
+
});
|
|
19988
|
+
timeoutHandle = setTimeout(() => {
|
|
19989
|
+
settle({
|
|
19990
|
+
decision: "deny",
|
|
19991
|
+
decided_at: this.now().toISOString(),
|
|
19992
|
+
decided_by: "timeout"
|
|
19993
|
+
});
|
|
19994
|
+
}, this.replaceModeTimeoutMs);
|
|
19995
|
+
});
|
|
19996
|
+
}
|
|
19997
|
+
/**
|
|
19998
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
19999
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
20000
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
20001
|
+
* downstream audit logging is unchanged.
|
|
20002
|
+
*
|
|
20003
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
20004
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
20005
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
20006
|
+
*/
|
|
20007
|
+
async notifyMode(request) {
|
|
20008
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
20009
|
+
let underlyingPromise;
|
|
20010
|
+
try {
|
|
20011
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
20012
|
+
} catch (err) {
|
|
20013
|
+
const response = await aggregatorPromise;
|
|
20014
|
+
return response;
|
|
20015
|
+
}
|
|
20016
|
+
return Promise.race([
|
|
20017
|
+
aggregatorPromise,
|
|
20018
|
+
underlyingPromise.catch(
|
|
20019
|
+
() => new Promise(() => {
|
|
20020
|
+
})
|
|
20021
|
+
)
|
|
20022
|
+
]);
|
|
20023
|
+
}
|
|
20024
|
+
};
|
|
20025
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
20026
|
+
return (_request) => {
|
|
20027
|
+
const cfg = supplier().approval_redirect;
|
|
20028
|
+
if (!cfg || cfg.enabled !== true) {
|
|
20029
|
+
return { enabled: false, mode: "replace" };
|
|
20030
|
+
}
|
|
20031
|
+
return {
|
|
20032
|
+
enabled: true,
|
|
20033
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
20034
|
+
};
|
|
20035
|
+
};
|
|
20036
|
+
}
|
|
20037
|
+
|
|
20038
|
+
// src/principal-policy/aggregator-store.ts
|
|
20039
|
+
init_encryption();
|
|
20040
|
+
init_encoding();
|
|
20041
|
+
var AGGREGATOR_PAYLOAD_NAMESPACE = "_approval_aggregator_payloads";
|
|
20042
|
+
var AGGREGATOR_PAYLOAD_KEY_PREFIX = "payload.";
|
|
20043
|
+
var HKDF_INFO = "l2-approval-aggregator-payload-v1";
|
|
20044
|
+
var DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS = 30;
|
|
20045
|
+
var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
20046
|
+
var AggregatorPayloadStore = class {
|
|
20047
|
+
storage;
|
|
20048
|
+
encryptionKey;
|
|
20049
|
+
fortressId;
|
|
20050
|
+
retentionDays;
|
|
20051
|
+
constructor(opts) {
|
|
20052
|
+
this.storage = opts.storage;
|
|
20053
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO);
|
|
20054
|
+
this.fortressId = opts.fortressId;
|
|
20055
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS;
|
|
20056
|
+
}
|
|
20057
|
+
/**
|
|
20058
|
+
* Persist `payload` under the given aggregator_id. Idempotent; calling
|
|
20059
|
+
* twice with the same id rewrites the bundle (retention_until is
|
|
20060
|
+
* recomputed). Returns the bundle's retention_until ISO-8601 timestamp
|
|
20061
|
+
* so callers can log it.
|
|
20062
|
+
*/
|
|
20063
|
+
async savePayload(aggregatorId, payload) {
|
|
20064
|
+
const now = /* @__PURE__ */ new Date();
|
|
20065
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
20066
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
20067
|
+
const bundle = {
|
|
20068
|
+
version: 1,
|
|
20069
|
+
aggregator_id: aggregatorId,
|
|
20070
|
+
fortress_id: this.fortressId,
|
|
20071
|
+
created_at: now.toISOString(),
|
|
20072
|
+
retention_until: retentionUntil.toISOString(),
|
|
20073
|
+
payload
|
|
20074
|
+
};
|
|
20075
|
+
const aad = stringToBytes(aggregatorId);
|
|
20076
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
20077
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
20078
|
+
await this.storage.write(
|
|
20079
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20080
|
+
payloadKey(aggregatorId),
|
|
20081
|
+
stringToBytes(JSON.stringify(envelope))
|
|
20082
|
+
);
|
|
20083
|
+
return bundle.retention_until;
|
|
20084
|
+
}
|
|
20085
|
+
/**
|
|
20086
|
+
* Read the persisted payload for the aggregator_id. Returns null if no
|
|
20087
|
+
* bundle exists, the bundle is corrupted, or AAD binding fails.
|
|
20088
|
+
*/
|
|
20089
|
+
async loadPayload(aggregatorId) {
|
|
20090
|
+
const key = payloadKey(aggregatorId);
|
|
20091
|
+
let raw;
|
|
20092
|
+
try {
|
|
20093
|
+
raw = await this.storage.read(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
20094
|
+
} catch {
|
|
20095
|
+
return null;
|
|
20096
|
+
}
|
|
20097
|
+
if (!raw) return null;
|
|
20098
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
20099
|
+
try {
|
|
20100
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
20101
|
+
const aad = stringToBytes(aggregatorId);
|
|
20102
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
20103
|
+
const parsed = JSON.parse(
|
|
20104
|
+
bytesToString(plaintext)
|
|
20105
|
+
);
|
|
20106
|
+
if (parsed.version !== 1) return null;
|
|
20107
|
+
if (parsed.aggregator_id !== aggregatorId) return null;
|
|
20108
|
+
return parsed.payload;
|
|
20109
|
+
} catch {
|
|
20110
|
+
return null;
|
|
20111
|
+
}
|
|
20112
|
+
}
|
|
20113
|
+
/**
|
|
20114
|
+
* Delete the persisted payload. Returns true when a bundle was removed,
|
|
20115
|
+
* false when none existed.
|
|
20116
|
+
*/
|
|
20117
|
+
async deletePayload(aggregatorId) {
|
|
20118
|
+
const key = payloadKey(aggregatorId);
|
|
20119
|
+
const existed = await this.storage.exists(
|
|
20120
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20121
|
+
key
|
|
20122
|
+
);
|
|
20123
|
+
if (!existed) return false;
|
|
20124
|
+
try {
|
|
20125
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
20126
|
+
} catch {
|
|
20127
|
+
return false;
|
|
20128
|
+
}
|
|
20129
|
+
return true;
|
|
20130
|
+
}
|
|
20131
|
+
/**
|
|
20132
|
+
* Drop expired payload bundles. Returns the count of bundles pruned.
|
|
20133
|
+
* Caller wires this into the cocoon-unlock initialization path.
|
|
20134
|
+
*/
|
|
20135
|
+
async pruneExpired(now) {
|
|
20136
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
20137
|
+
const entries = await this.storage.list(
|
|
20138
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20139
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX
|
|
20140
|
+
);
|
|
20141
|
+
let pruned = 0;
|
|
20142
|
+
for (const meta of entries) {
|
|
20143
|
+
const aggregatorId = stripKeyPrefix(meta.key);
|
|
20144
|
+
if (aggregatorId === null) continue;
|
|
20145
|
+
const raw = await this.storage.read(
|
|
20146
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
20147
|
+
meta.key
|
|
20148
|
+
);
|
|
20149
|
+
if (!raw) continue;
|
|
20150
|
+
try {
|
|
20151
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
20152
|
+
const aad = stringToBytes(aggregatorId);
|
|
20153
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
20154
|
+
const parsed = JSON.parse(
|
|
20155
|
+
bytesToString(plaintext)
|
|
20156
|
+
);
|
|
20157
|
+
if (parsed.retention_until <= cutoff) {
|
|
20158
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
|
|
20159
|
+
pruned += 1;
|
|
20160
|
+
}
|
|
20161
|
+
} catch {
|
|
20162
|
+
}
|
|
20163
|
+
}
|
|
20164
|
+
return { pruned };
|
|
20165
|
+
}
|
|
20166
|
+
};
|
|
20167
|
+
function payloadKey(aggregatorId) {
|
|
20168
|
+
return `${AGGREGATOR_PAYLOAD_KEY_PREFIX}${aggregatorId}`;
|
|
20169
|
+
}
|
|
20170
|
+
function stripKeyPrefix(key) {
|
|
20171
|
+
if (!key.startsWith(AGGREGATOR_PAYLOAD_KEY_PREFIX)) return null;
|
|
20172
|
+
return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
|
|
20173
|
+
}
|
|
20174
|
+
|
|
19618
20175
|
// src/principal-policy/tools.ts
|
|
19619
20176
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
19620
20177
|
return [
|
|
@@ -20485,16 +21042,81 @@ function verifyAttestation(attestation, now) {
|
|
|
20485
21042
|
};
|
|
20486
21043
|
}
|
|
20487
21044
|
|
|
20488
|
-
// src/handshake/
|
|
20489
|
-
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
|
|
20495
|
-
|
|
20496
|
-
|
|
20497
|
-
|
|
21045
|
+
// src/handshake/audit.ts
|
|
21046
|
+
var HANDSHAKE_LIFECYCLE_OPS = {
|
|
21047
|
+
INITIATED: "handshake_initiated",
|
|
21048
|
+
COMPLETED: "handshake_completed",
|
|
21049
|
+
FAILED: "handshake_failed",
|
|
21050
|
+
ABORTED: "handshake_aborted"
|
|
21051
|
+
};
|
|
21052
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
21053
|
+
auditLog.append(
|
|
21054
|
+
"l4",
|
|
21055
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
21056
|
+
ctx.identity_id,
|
|
21057
|
+
detailsFromContext(ctx),
|
|
21058
|
+
"success"
|
|
21059
|
+
);
|
|
21060
|
+
}
|
|
21061
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
21062
|
+
const details = detailsFromContext(ctx);
|
|
21063
|
+
if (ctx.trust_tier !== void 0) {
|
|
21064
|
+
details.trust_tier = ctx.trust_tier;
|
|
21065
|
+
}
|
|
21066
|
+
auditLog.append(
|
|
21067
|
+
"l4",
|
|
21068
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
21069
|
+
ctx.identity_id,
|
|
21070
|
+
details,
|
|
21071
|
+
"success"
|
|
21072
|
+
);
|
|
21073
|
+
}
|
|
21074
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
21075
|
+
const details = detailsFromContext(ctx);
|
|
21076
|
+
details.reason = ctx.reason;
|
|
21077
|
+
if (ctx.error !== void 0) {
|
|
21078
|
+
details.error = ctx.error;
|
|
21079
|
+
}
|
|
21080
|
+
auditLog.append(
|
|
21081
|
+
"l4",
|
|
21082
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
21083
|
+
ctx.identity_id,
|
|
21084
|
+
details,
|
|
21085
|
+
"failure"
|
|
21086
|
+
);
|
|
21087
|
+
}
|
|
21088
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
21089
|
+
const details = detailsFromContext(ctx);
|
|
21090
|
+
details.reason = ctx.reason;
|
|
21091
|
+
auditLog.append(
|
|
21092
|
+
"l4",
|
|
21093
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
21094
|
+
ctx.identity_id,
|
|
21095
|
+
details,
|
|
21096
|
+
"failure"
|
|
21097
|
+
);
|
|
21098
|
+
}
|
|
21099
|
+
function detailsFromContext(ctx) {
|
|
21100
|
+
const details = {
|
|
21101
|
+
session_id: ctx.session_id,
|
|
21102
|
+
role: ctx.role
|
|
21103
|
+
};
|
|
21104
|
+
if (ctx.counterparty_id !== void 0) {
|
|
21105
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
21106
|
+
}
|
|
21107
|
+
return details;
|
|
21108
|
+
}
|
|
21109
|
+
|
|
21110
|
+
// src/handshake/tools.ts
|
|
21111
|
+
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
21112
|
+
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
21113
|
+
const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
|
|
21114
|
+
const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
21115
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
21116
|
+
const handshakeResults = /* @__PURE__ */ new Map();
|
|
21117
|
+
const shrOpts = {
|
|
21118
|
+
config,
|
|
21119
|
+
identityManager,
|
|
20498
21120
|
masterKey
|
|
20499
21121
|
};
|
|
20500
21122
|
const tools = [
|
|
@@ -20518,6 +21140,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20518
21140
|
const { challenge, session } = initiateHandshake(shr);
|
|
20519
21141
|
sessions.set(session.session_id, session);
|
|
20520
21142
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
21143
|
+
auditHandshakeInitiated(auditLog, {
|
|
21144
|
+
session_id: session.session_id,
|
|
21145
|
+
role: "initiator",
|
|
21146
|
+
identity_id: shr.body.instance_id
|
|
21147
|
+
});
|
|
20521
21148
|
return toolResult({
|
|
20522
21149
|
session_id: session.session_id,
|
|
20523
21150
|
challenge,
|
|
@@ -20557,10 +21184,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20557
21184
|
);
|
|
20558
21185
|
if ("error" in result) {
|
|
20559
21186
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
21187
|
+
auditHandshakeFailed(auditLog, {
|
|
21188
|
+
session_id: "unknown",
|
|
21189
|
+
role: "responder",
|
|
21190
|
+
identity_id: shr.body.instance_id,
|
|
21191
|
+
reason: classifyRespondFailure(result.error),
|
|
21192
|
+
error: result.error
|
|
21193
|
+
});
|
|
20560
21194
|
return toolResult({ error: result.error });
|
|
20561
21195
|
}
|
|
20562
21196
|
sessions.set(result.session.session_id, result.session);
|
|
20563
21197
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
21198
|
+
auditHandshakeInitiated(auditLog, {
|
|
21199
|
+
session_id: result.session.session_id,
|
|
21200
|
+
role: "responder",
|
|
21201
|
+
identity_id: shr.body.instance_id,
|
|
21202
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
21203
|
+
});
|
|
20564
21204
|
let autoPublishResult;
|
|
20565
21205
|
if (autoPublishHandshakes) {
|
|
20566
21206
|
autoPublishResult = { attempted: true };
|
|
@@ -20668,9 +21308,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20668
21308
|
const response = args.response;
|
|
20669
21309
|
const session = sessions.get(sessionId);
|
|
20670
21310
|
if (!session) {
|
|
21311
|
+
auditHandshakeFailed(auditLog, {
|
|
21312
|
+
session_id: sessionId,
|
|
21313
|
+
role: "initiator",
|
|
21314
|
+
identity_id: "unknown",
|
|
21315
|
+
reason: "session_unknown",
|
|
21316
|
+
error: `No handshake session found: ${sessionId}`
|
|
21317
|
+
});
|
|
20671
21318
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
20672
21319
|
}
|
|
20673
21320
|
if (session.state !== "initiated") {
|
|
21321
|
+
auditHandshakeFailed(auditLog, {
|
|
21322
|
+
session_id: sessionId,
|
|
21323
|
+
role: "initiator",
|
|
21324
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21325
|
+
reason: "session_state_mismatch",
|
|
21326
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21327
|
+
});
|
|
20674
21328
|
return toolResult({
|
|
20675
21329
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
20676
21330
|
});
|
|
@@ -20684,6 +21338,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20684
21338
|
if ("error" in result) {
|
|
20685
21339
|
session.state = "failed";
|
|
20686
21340
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
21341
|
+
auditHandshakeFailed(auditLog, {
|
|
21342
|
+
session_id: sessionId,
|
|
21343
|
+
role: "initiator",
|
|
21344
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21345
|
+
reason: classifyCompleteFailure(result.error),
|
|
21346
|
+
error: result.error
|
|
21347
|
+
});
|
|
20687
21348
|
return toolResult({ error: result.error });
|
|
20688
21349
|
}
|
|
20689
21350
|
session.state = "completed";
|
|
@@ -20692,6 +21353,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20692
21353
|
session.result = result.result;
|
|
20693
21354
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
20694
21355
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
21356
|
+
auditHandshakeCompleted(auditLog, {
|
|
21357
|
+
session_id: sessionId,
|
|
21358
|
+
role: "initiator",
|
|
21359
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21360
|
+
counterparty_id: result.result.counterparty_id,
|
|
21361
|
+
trust_tier: result.result.trust_tier
|
|
21362
|
+
});
|
|
20695
21363
|
return toolResult({
|
|
20696
21364
|
completion: result.completion,
|
|
20697
21365
|
result: result.result,
|
|
@@ -20739,6 +21407,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20739
21407
|
void 0,
|
|
20740
21408
|
result.verified ? "success" : "failure"
|
|
20741
21409
|
);
|
|
21410
|
+
if (result.verified) {
|
|
21411
|
+
auditHandshakeCompleted(auditLog, {
|
|
21412
|
+
session_id: session.session_id,
|
|
21413
|
+
role: "responder",
|
|
21414
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21415
|
+
counterparty_id: result.counterparty_id,
|
|
21416
|
+
trust_tier: result.trust_tier
|
|
21417
|
+
});
|
|
21418
|
+
} else {
|
|
21419
|
+
auditHandshakeFailed(auditLog, {
|
|
21420
|
+
session_id: session.session_id,
|
|
21421
|
+
role: "responder",
|
|
21422
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21423
|
+
counterparty_id: result.counterparty_id,
|
|
21424
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
21425
|
+
error: result.errors.join("; ")
|
|
21426
|
+
});
|
|
21427
|
+
}
|
|
20742
21428
|
return toolResult({ result });
|
|
20743
21429
|
}
|
|
20744
21430
|
return toolResult({
|
|
@@ -20846,10 +21532,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20846
21532
|
_content_trust: "external"
|
|
20847
21533
|
});
|
|
20848
21534
|
}
|
|
21535
|
+
},
|
|
21536
|
+
{
|
|
21537
|
+
name: "handshake_abort",
|
|
21538
|
+
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.",
|
|
21539
|
+
inputSchema: {
|
|
21540
|
+
type: "object",
|
|
21541
|
+
properties: {
|
|
21542
|
+
session_id: {
|
|
21543
|
+
type: "string",
|
|
21544
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
21545
|
+
},
|
|
21546
|
+
reason: {
|
|
21547
|
+
type: "string",
|
|
21548
|
+
enum: [
|
|
21549
|
+
"operator_cancelled",
|
|
21550
|
+
"session_timeout",
|
|
21551
|
+
"transport_dropped",
|
|
21552
|
+
"shutdown",
|
|
21553
|
+
"other"
|
|
21554
|
+
],
|
|
21555
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
21556
|
+
}
|
|
21557
|
+
},
|
|
21558
|
+
required: ["session_id"]
|
|
21559
|
+
},
|
|
21560
|
+
handler: async (args) => {
|
|
21561
|
+
const sessionId = args.session_id;
|
|
21562
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
21563
|
+
const session = sessions.get(sessionId);
|
|
21564
|
+
if (!session) {
|
|
21565
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21566
|
+
}
|
|
21567
|
+
if (session.state === "completed") {
|
|
21568
|
+
return toolResult({
|
|
21569
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
21570
|
+
});
|
|
21571
|
+
}
|
|
21572
|
+
sessions.delete(sessionId);
|
|
21573
|
+
auditHandshakeAborted(auditLog, {
|
|
21574
|
+
session_id: sessionId,
|
|
21575
|
+
role: session.role,
|
|
21576
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21577
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
21578
|
+
reason
|
|
21579
|
+
});
|
|
21580
|
+
return toolResult({
|
|
21581
|
+
aborted: true,
|
|
21582
|
+
session_id: sessionId,
|
|
21583
|
+
reason
|
|
21584
|
+
});
|
|
21585
|
+
}
|
|
20849
21586
|
}
|
|
20850
21587
|
];
|
|
20851
21588
|
return { tools, handshakeResults };
|
|
20852
21589
|
}
|
|
21590
|
+
function classifyRespondFailure(error) {
|
|
21591
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21592
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
21593
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21594
|
+
return "other";
|
|
21595
|
+
}
|
|
21596
|
+
function classifyCompleteFailure(error) {
|
|
21597
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21598
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
21599
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
21600
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21601
|
+
return "other";
|
|
21602
|
+
}
|
|
20853
21603
|
|
|
20854
21604
|
// src/federation/registry.ts
|
|
20855
21605
|
var DEFAULT_CAPABILITIES = {
|
|
@@ -31575,15 +32325,300 @@ var OPERATOR_CHAT_OPS = {
|
|
|
31575
32325
|
* successful thread removal. Body carries thread_id + turn_count of
|
|
31576
32326
|
* the deleted bundle.
|
|
31577
32327
|
*/
|
|
31578
|
-
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
32328
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
32329
|
+
/**
|
|
32330
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
32331
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
32332
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
32333
|
+
* carries thread_id + a stable failure_reason enum.
|
|
32334
|
+
*/
|
|
32335
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed",
|
|
32336
|
+
/**
|
|
32337
|
+
* Concierge dynamic-context fetcher failed (WP-V1.3-9 Tau-3). Emitted
|
|
32338
|
+
* when a category fetcher throws while assembling the dynamic context
|
|
32339
|
+
* fold. The concierge omits that category and continues; the user-
|
|
32340
|
+
* facing query is never broken. Body carries category + failure_reason.
|
|
32341
|
+
*/
|
|
32342
|
+
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
|
|
31579
32343
|
};
|
|
31580
32344
|
|
|
31581
32345
|
// src/chat/operator-chat-types.ts
|
|
31582
32346
|
var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
|
|
31583
32347
|
var CONCIERGE_THREAD_KEY = "_fortress";
|
|
31584
32348
|
|
|
32349
|
+
// src/chat/concierge-context-router.ts
|
|
32350
|
+
var APPROX_CHARS_PER_TOKEN = 4;
|
|
32351
|
+
var DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET = 2e3;
|
|
32352
|
+
var DYNAMIC_CONTEXT_SECTION_HEADER = "## Live fortress context";
|
|
32353
|
+
var CONTEXT_CATEGORIES = [
|
|
32354
|
+
"templates",
|
|
32355
|
+
"agent_state",
|
|
32356
|
+
"agent_activity",
|
|
32357
|
+
"audit_log",
|
|
32358
|
+
"sentinel_findings",
|
|
32359
|
+
"anomaly_alerts",
|
|
32360
|
+
"recent_receipts",
|
|
32361
|
+
"verascore_deltas"
|
|
32362
|
+
];
|
|
32363
|
+
function phrasePattern(phrase) {
|
|
32364
|
+
const escaped = phrase.toLowerCase().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
32365
|
+
return { source: `\\b${escaped}\\b`, phrase };
|
|
32366
|
+
}
|
|
32367
|
+
var CATEGORY_KEYWORDS = [
|
|
32368
|
+
{
|
|
32369
|
+
category: "templates",
|
|
32370
|
+
patterns: [
|
|
32371
|
+
"templates",
|
|
32372
|
+
"template",
|
|
32373
|
+
"channel templates",
|
|
32374
|
+
"channel template",
|
|
32375
|
+
"list templates",
|
|
32376
|
+
"available templates",
|
|
32377
|
+
"what templates"
|
|
32378
|
+
].map(phrasePattern)
|
|
32379
|
+
},
|
|
32380
|
+
{
|
|
32381
|
+
category: "agent_state",
|
|
32382
|
+
patterns: [
|
|
32383
|
+
"state",
|
|
32384
|
+
"status",
|
|
32385
|
+
"agent state",
|
|
32386
|
+
"agent status",
|
|
32387
|
+
"status of agent",
|
|
32388
|
+
"status of agents",
|
|
32389
|
+
"state of",
|
|
32390
|
+
"doing"
|
|
32391
|
+
].map(phrasePattern)
|
|
32392
|
+
},
|
|
32393
|
+
{
|
|
32394
|
+
category: "agent_activity",
|
|
32395
|
+
patterns: [
|
|
32396
|
+
"activity",
|
|
32397
|
+
"agent activity",
|
|
32398
|
+
"what did",
|
|
32399
|
+
"recent activity"
|
|
32400
|
+
].map(phrasePattern)
|
|
32401
|
+
},
|
|
32402
|
+
{
|
|
32403
|
+
category: "audit_log",
|
|
32404
|
+
patterns: [
|
|
32405
|
+
"audit log",
|
|
32406
|
+
"audit",
|
|
32407
|
+
"log entry",
|
|
32408
|
+
"log entries",
|
|
32409
|
+
"what happened",
|
|
32410
|
+
"show me events",
|
|
32411
|
+
"event class"
|
|
32412
|
+
].map(phrasePattern)
|
|
32413
|
+
},
|
|
32414
|
+
{
|
|
32415
|
+
category: "sentinel_findings",
|
|
32416
|
+
patterns: [
|
|
32417
|
+
"sentinel",
|
|
32418
|
+
"sentinels",
|
|
32419
|
+
"warning",
|
|
32420
|
+
"warnings",
|
|
32421
|
+
"alert",
|
|
32422
|
+
"alerts",
|
|
32423
|
+
"whats wrong",
|
|
32424
|
+
"what's wrong",
|
|
32425
|
+
"findings"
|
|
32426
|
+
].map(phrasePattern)
|
|
32427
|
+
},
|
|
32428
|
+
{
|
|
32429
|
+
category: "anomaly_alerts",
|
|
32430
|
+
patterns: [
|
|
32431
|
+
"anomaly",
|
|
32432
|
+
"anomalies",
|
|
32433
|
+
"spike",
|
|
32434
|
+
"unusual",
|
|
32435
|
+
"outlier"
|
|
32436
|
+
].map(phrasePattern)
|
|
32437
|
+
},
|
|
32438
|
+
{
|
|
32439
|
+
category: "recent_receipts",
|
|
32440
|
+
patterns: [
|
|
32441
|
+
"receipt",
|
|
32442
|
+
"receipts",
|
|
32443
|
+
"concordia",
|
|
32444
|
+
"commitment",
|
|
32445
|
+
"commitments",
|
|
32446
|
+
"chain",
|
|
32447
|
+
"chains"
|
|
32448
|
+
].map(phrasePattern)
|
|
32449
|
+
},
|
|
32450
|
+
{
|
|
32451
|
+
category: "verascore_deltas",
|
|
32452
|
+
patterns: [
|
|
32453
|
+
"verascore",
|
|
32454
|
+
"vera score",
|
|
32455
|
+
"trust score",
|
|
32456
|
+
"reputation"
|
|
32457
|
+
].map(phrasePattern)
|
|
32458
|
+
}
|
|
32459
|
+
];
|
|
32460
|
+
function extractAgentNameHint(query) {
|
|
32461
|
+
const agentPattern = /\bagent\s+["']?([A-Za-z][\w-]{0,40})["']?/i;
|
|
32462
|
+
const m = query.match(agentPattern);
|
|
32463
|
+
if (m && m[1]) return m[1];
|
|
32464
|
+
const quoted = query.match(/["']([A-Za-z][\w-]{0,40})["']/);
|
|
32465
|
+
if (quoted && quoted[1]) return quoted[1];
|
|
32466
|
+
return null;
|
|
32467
|
+
}
|
|
32468
|
+
var TRIVIAL_GREETINGS = /* @__PURE__ */ new Set([
|
|
32469
|
+
"hi",
|
|
32470
|
+
"hello",
|
|
32471
|
+
"hey",
|
|
32472
|
+
"yo",
|
|
32473
|
+
"ok",
|
|
32474
|
+
"thanks",
|
|
32475
|
+
"thx",
|
|
32476
|
+
"thank you"
|
|
32477
|
+
]);
|
|
32478
|
+
function isTrivialQuery(query) {
|
|
32479
|
+
const norm = query.trim().toLowerCase();
|
|
32480
|
+
if (norm.length === 0) return true;
|
|
32481
|
+
if (norm.length < 8) return true;
|
|
32482
|
+
return TRIVIAL_GREETINGS.has(norm);
|
|
32483
|
+
}
|
|
32484
|
+
function classifyQuery(query) {
|
|
32485
|
+
const normalized = query.toLowerCase();
|
|
32486
|
+
const matches = [];
|
|
32487
|
+
for (const spec of CATEGORY_KEYWORDS) {
|
|
32488
|
+
const matchedPhrases = [];
|
|
32489
|
+
for (const pattern of spec.patterns) {
|
|
32490
|
+
if (matchedPhrases.includes(pattern.phrase)) continue;
|
|
32491
|
+
const re = new RegExp(pattern.source, "i");
|
|
32492
|
+
if (re.test(normalized)) {
|
|
32493
|
+
matchedPhrases.push(pattern.phrase);
|
|
32494
|
+
}
|
|
32495
|
+
}
|
|
32496
|
+
if (matchedPhrases.length === 0) continue;
|
|
32497
|
+
const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
|
|
32498
|
+
matches.push({
|
|
32499
|
+
category: spec.category,
|
|
32500
|
+
confidence,
|
|
32501
|
+
matched_keywords: matchedPhrases,
|
|
32502
|
+
agent_name_hint: spec.category === "agent_state" || spec.category === "agent_activity" ? extractAgentNameHint(query) : null
|
|
32503
|
+
});
|
|
32504
|
+
}
|
|
32505
|
+
matches.sort((a, b) => {
|
|
32506
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
32507
|
+
return CONTEXT_CATEGORIES.indexOf(a.category) - CONTEXT_CATEGORIES.indexOf(b.category);
|
|
32508
|
+
});
|
|
32509
|
+
return matches;
|
|
32510
|
+
}
|
|
32511
|
+
function approxTokenLen(text) {
|
|
32512
|
+
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
32513
|
+
}
|
|
32514
|
+
var CATEGORY_LABELS = {
|
|
32515
|
+
templates: "Templates",
|
|
32516
|
+
agent_state: "Agent state",
|
|
32517
|
+
agent_activity: "Agent activity",
|
|
32518
|
+
audit_log: "Audit log",
|
|
32519
|
+
sentinel_findings: "Sentinel findings",
|
|
32520
|
+
anomaly_alerts: "Anomaly alerts",
|
|
32521
|
+
recent_receipts: "Recent receipts",
|
|
32522
|
+
verascore_deltas: "Verascore deltas"
|
|
32523
|
+
};
|
|
32524
|
+
async function runFetcher(match, fetchers) {
|
|
32525
|
+
switch (match.category) {
|
|
32526
|
+
case "templates":
|
|
32527
|
+
return fetchers.templates();
|
|
32528
|
+
case "agent_state":
|
|
32529
|
+
return fetchers.agent_state(match.agent_name_hint);
|
|
32530
|
+
case "agent_activity":
|
|
32531
|
+
return fetchers.agent_activity(match.agent_name_hint);
|
|
32532
|
+
case "audit_log":
|
|
32533
|
+
return fetchers.audit_log();
|
|
32534
|
+
case "sentinel_findings":
|
|
32535
|
+
return fetchers.sentinel_findings();
|
|
32536
|
+
case "anomaly_alerts":
|
|
32537
|
+
return fetchers.anomaly_alerts();
|
|
32538
|
+
case "recent_receipts":
|
|
32539
|
+
return fetchers.recent_receipts();
|
|
32540
|
+
case "verascore_deltas":
|
|
32541
|
+
return fetchers.verascore_deltas();
|
|
32542
|
+
}
|
|
32543
|
+
}
|
|
32544
|
+
function trivialMatch(category) {
|
|
32545
|
+
return {
|
|
32546
|
+
category,
|
|
32547
|
+
confidence: 0.5,
|
|
32548
|
+
matched_keywords: ["llm-assist"],
|
|
32549
|
+
agent_name_hint: null
|
|
32550
|
+
};
|
|
32551
|
+
}
|
|
32552
|
+
async function foldContext(query, fetchers, opts) {
|
|
32553
|
+
const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
|
|
32554
|
+
let matches = classifyQuery(query);
|
|
32555
|
+
if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
|
|
32556
|
+
try {
|
|
32557
|
+
const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
|
|
32558
|
+
if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
|
|
32559
|
+
matches = [trivialMatch(picked)];
|
|
32560
|
+
}
|
|
32561
|
+
} catch {
|
|
32562
|
+
}
|
|
32563
|
+
}
|
|
32564
|
+
if (matches.length === 0) {
|
|
32565
|
+
return { section: "", categoriesIncluded: [] };
|
|
32566
|
+
}
|
|
32567
|
+
const attempts = [];
|
|
32568
|
+
for (const match of matches) {
|
|
32569
|
+
try {
|
|
32570
|
+
const text = await runFetcher(match, fetchers);
|
|
32571
|
+
const trimmed = text.trim();
|
|
32572
|
+
if (trimmed.length > 0) {
|
|
32573
|
+
attempts.push({ category: match.category, text: trimmed });
|
|
32574
|
+
}
|
|
32575
|
+
} catch (err) {
|
|
32576
|
+
opts?.onFetcherFailure?.(match.category, err);
|
|
32577
|
+
}
|
|
32578
|
+
}
|
|
32579
|
+
if (attempts.length === 0) {
|
|
32580
|
+
return { section: "", categoriesIncluded: [] };
|
|
32581
|
+
}
|
|
32582
|
+
const headerTokens = approxTokenLen(`${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
32583
|
+
`);
|
|
32584
|
+
const sepTokens = approxTokenLen("\n\n");
|
|
32585
|
+
let runningTokens = headerTokens;
|
|
32586
|
+
const kept = [];
|
|
32587
|
+
for (const attempt of attempts) {
|
|
32588
|
+
const block = `### ${CATEGORY_LABELS[attempt.category]}
|
|
32589
|
+
${attempt.text}`;
|
|
32590
|
+
const tokens = approxTokenLen(block) + (kept.length > 0 ? sepTokens : 0);
|
|
32591
|
+
if (kept.length === 0) {
|
|
32592
|
+
kept.push(attempt);
|
|
32593
|
+
runningTokens += tokens;
|
|
32594
|
+
continue;
|
|
32595
|
+
}
|
|
32596
|
+
if (runningTokens + tokens > budget) break;
|
|
32597
|
+
kept.push(attempt);
|
|
32598
|
+
runningTokens += tokens;
|
|
32599
|
+
}
|
|
32600
|
+
const blocks = kept.map(
|
|
32601
|
+
(k) => `### ${CATEGORY_LABELS[k.category]}
|
|
32602
|
+
${k.text}`
|
|
32603
|
+
);
|
|
32604
|
+
const section = `${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
32605
|
+
${blocks.join("\n\n")}`;
|
|
32606
|
+
return {
|
|
32607
|
+
section,
|
|
32608
|
+
categoriesIncluded: kept.map((k) => k.category)
|
|
32609
|
+
};
|
|
32610
|
+
}
|
|
32611
|
+
|
|
31585
32612
|
// src/chat/operator-chat-service.ts
|
|
31586
32613
|
var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
32614
|
+
var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
32615
|
+
var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
32616
|
+
var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
32617
|
+
var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32618
|
+
var DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
|
|
32619
|
+
function approxTokenLen2(text) {
|
|
32620
|
+
return Math.ceil(text.length / 4);
|
|
32621
|
+
}
|
|
31587
32622
|
var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
31588
32623
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
31589
32624
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -31620,6 +32655,14 @@ var OperatorChatService = class {
|
|
|
31620
32655
|
piiFilter;
|
|
31621
32656
|
conciergeMaxTokens;
|
|
31622
32657
|
memory;
|
|
32658
|
+
historyWindowTurns;
|
|
32659
|
+
historyFreshnessMs;
|
|
32660
|
+
historyTokenBudget;
|
|
32661
|
+
sessionTtlMs;
|
|
32662
|
+
clock;
|
|
32663
|
+
contextFetchers;
|
|
32664
|
+
contextLlmAssist;
|
|
32665
|
+
dynamicContextBudget;
|
|
31623
32666
|
/**
|
|
31624
32667
|
* In-memory thread_id assigned to the active concierge session.
|
|
31625
32668
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -31627,6 +32670,14 @@ var OperatorChatService = class {
|
|
|
31627
32670
|
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
31628
32671
|
*/
|
|
31629
32672
|
activeMemoryThreadId;
|
|
32673
|
+
/**
|
|
32674
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
32675
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
32676
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
32677
|
+
* allocates a new thread_id even though the prior one is still
|
|
32678
|
+
* readable from the memory store.
|
|
32679
|
+
*/
|
|
32680
|
+
lastInteractionAt;
|
|
31630
32681
|
constructor(deps) {
|
|
31631
32682
|
this.store = deps.store;
|
|
31632
32683
|
this.auditLog = deps.auditLog;
|
|
@@ -31638,6 +32689,18 @@ var OperatorChatService = class {
|
|
|
31638
32689
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
31639
32690
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
31640
32691
|
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32692
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
32693
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
32694
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
32695
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
32696
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
32697
|
+
if (deps.conciergeContextFetchers) {
|
|
32698
|
+
this.contextFetchers = deps.conciergeContextFetchers;
|
|
32699
|
+
}
|
|
32700
|
+
if (deps.conciergeContextLlmAssist) {
|
|
32701
|
+
this.contextLlmAssist = deps.conciergeContextLlmAssist;
|
|
32702
|
+
}
|
|
32703
|
+
this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
|
|
31641
32704
|
}
|
|
31642
32705
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
31643
32706
|
/**
|
|
@@ -31656,6 +32719,10 @@ var OperatorChatService = class {
|
|
|
31656
32719
|
throw new Error("concierge query must not be empty");
|
|
31657
32720
|
}
|
|
31658
32721
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
32722
|
+
const nowMs = this.clock();
|
|
32723
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
32724
|
+
this.activeMemoryThreadId = void 0;
|
|
32725
|
+
}
|
|
31659
32726
|
const operatorMessage = {
|
|
31660
32727
|
message_id: randomUUID(),
|
|
31661
32728
|
surface: "concierge",
|
|
@@ -31668,6 +32735,25 @@ var OperatorChatService = class {
|
|
|
31668
32735
|
CONCIERGE_THREAD_KEY,
|
|
31669
32736
|
operatorMessage
|
|
31670
32737
|
);
|
|
32738
|
+
let priorTurns = [];
|
|
32739
|
+
let memoryReadFailureReason = null;
|
|
32740
|
+
let activeThreadIdForRound;
|
|
32741
|
+
if (this.memory) {
|
|
32742
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
32743
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
32744
|
+
if (result.ok) {
|
|
32745
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
32746
|
+
const fresh = result.turns.filter((t) => {
|
|
32747
|
+
const ts = Date.parse(t.created_at);
|
|
32748
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
32749
|
+
});
|
|
32750
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
32751
|
+
priorTurns = recent;
|
|
32752
|
+
} else {
|
|
32753
|
+
memoryReadFailureReason = result.reason;
|
|
32754
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
32755
|
+
}
|
|
32756
|
+
}
|
|
31671
32757
|
if (this.memory) {
|
|
31672
32758
|
const threadId = this.ensureActiveMemoryThread();
|
|
31673
32759
|
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
@@ -31678,6 +32764,7 @@ var OperatorChatService = class {
|
|
|
31678
32764
|
let servedBy = "disabled";
|
|
31679
32765
|
let displayLabel = "Concierge: substrate not configured";
|
|
31680
32766
|
let outcome = "substrate_disabled";
|
|
32767
|
+
let dynamicCategoriesIncluded = [];
|
|
31681
32768
|
if (!this.substrateSelector) {
|
|
31682
32769
|
conciergeBody = "Concierge unavailable. The substrate selector is not configured for this fortress. Pick a substrate in the Policy center to enable concierge replies.";
|
|
31683
32770
|
} else {
|
|
@@ -31689,7 +32776,14 @@ var OperatorChatService = class {
|
|
|
31689
32776
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
31690
32777
|
outcome = "substrate_disabled";
|
|
31691
32778
|
} else {
|
|
31692
|
-
const
|
|
32779
|
+
const dynamicResult = await this.runDynamicContextFold(
|
|
32780
|
+
filterResult.filtered
|
|
32781
|
+
);
|
|
32782
|
+
dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
|
|
32783
|
+
const context = await this.assembleConciergeContext(
|
|
32784
|
+
priorTurns,
|
|
32785
|
+
dynamicResult.section
|
|
32786
|
+
);
|
|
31693
32787
|
const response = await this.substrateSelector.invokeSummarize(
|
|
31694
32788
|
"concierge",
|
|
31695
32789
|
{
|
|
@@ -31728,10 +32822,14 @@ var OperatorChatService = class {
|
|
|
31728
32822
|
CONCIERGE_THREAD_KEY,
|
|
31729
32823
|
responseMessage
|
|
31730
32824
|
);
|
|
32825
|
+
let assistantTurnId;
|
|
31731
32826
|
if (this.memory) {
|
|
31732
32827
|
const threadId = this.ensureActiveMemoryThread();
|
|
31733
|
-
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() =>
|
|
31734
|
-
|
|
32828
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
32829
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
32830
|
+
}
|
|
32831
|
+
if (this.memory && activeThreadIdForRound) {
|
|
32832
|
+
this.lastInteractionAt = nowMs;
|
|
31735
32833
|
}
|
|
31736
32834
|
const payload = {
|
|
31737
32835
|
version: "1.2",
|
|
@@ -31744,7 +32842,13 @@ var OperatorChatService = class {
|
|
|
31744
32842
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
31745
32843
|
substrate: servedBy,
|
|
31746
32844
|
latency_ms: latencyMs,
|
|
31747
|
-
outcome
|
|
32845
|
+
outcome,
|
|
32846
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
32847
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
32848
|
+
...this.memory ? {
|
|
32849
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
32850
|
+
} : {},
|
|
32851
|
+
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
|
|
31748
32852
|
};
|
|
31749
32853
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
31750
32854
|
return {
|
|
@@ -31754,6 +32858,25 @@ var OperatorChatService = class {
|
|
|
31754
32858
|
outcome
|
|
31755
32859
|
};
|
|
31756
32860
|
}
|
|
32861
|
+
/**
|
|
32862
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
32863
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
32864
|
+
* with `result: "failure"` since the concierge fell back to
|
|
32865
|
+
* single-turn mode for this round-trip.
|
|
32866
|
+
*/
|
|
32867
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
32868
|
+
const payload = {
|
|
32869
|
+
version: "1.2",
|
|
32870
|
+
event_id: makeEventId("conc-memfail"),
|
|
32871
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32872
|
+
identity_id: this.identityId,
|
|
32873
|
+
kind: "operator_concierge_memory_read_failed",
|
|
32874
|
+
surface: "concierge",
|
|
32875
|
+
thread_id: threadId,
|
|
32876
|
+
failure_reason: reason
|
|
32877
|
+
};
|
|
32878
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
32879
|
+
}
|
|
31757
32880
|
/**
|
|
31758
32881
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
31759
32882
|
* an empty array when no thread exists yet.
|
|
@@ -31876,6 +32999,14 @@ var OperatorChatService = class {
|
|
|
31876
32999
|
* ## Sanctuary reference
|
|
31877
33000
|
* <static domain reference block>
|
|
31878
33001
|
*
|
|
33002
|
+
* ## Live fortress context ← WP-V1.3-9 Tau-3, when present
|
|
33003
|
+
* ### <Category>
|
|
33004
|
+
* <fetcher payload>
|
|
33005
|
+
*
|
|
33006
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
33007
|
+
* OPERATOR: ...
|
|
33008
|
+
* CONCIERGE: ...
|
|
33009
|
+
*
|
|
31879
33010
|
* ## Recent activity
|
|
31880
33011
|
* <recentActivity output>
|
|
31881
33012
|
*
|
|
@@ -31885,37 +33016,116 @@ var OperatorChatService = class {
|
|
|
31885
33016
|
* ## Open inbox
|
|
31886
33017
|
* <openInbox output>
|
|
31887
33018
|
* ```
|
|
31888
|
-
|
|
31889
|
-
|
|
33019
|
+
*
|
|
33020
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
33021
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
33022
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
33023
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
33024
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
33025
|
+
* serialization is the canonical path for v1.3.
|
|
33026
|
+
*/
|
|
33027
|
+
async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
|
|
31890
33028
|
const ref = `## Sanctuary reference
|
|
31891
33029
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
33030
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
31892
33031
|
if (!this.contextProviders) {
|
|
31893
|
-
return
|
|
31894
|
-
|
|
31895
|
-
|
|
31896
|
-
|
|
31897
|
-
|
|
31898
|
-
## Wrapped agents
|
|
31899
|
-
(no providers wired)
|
|
31900
|
-
|
|
31901
|
-
## Open inbox
|
|
31902
|
-
(no providers wired)`;
|
|
33032
|
+
return [
|
|
33033
|
+
ref,
|
|
33034
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
33035
|
+
...priorSection ? [priorSection] : [],
|
|
33036
|
+
"## Recent activity\n(no providers wired)",
|
|
33037
|
+
"## Wrapped agents\n(no providers wired)",
|
|
33038
|
+
"## Open inbox\n(no providers wired)"
|
|
33039
|
+
].join("\n\n");
|
|
31903
33040
|
}
|
|
31904
33041
|
const [activity, agents, inbox] = await Promise.all([
|
|
31905
33042
|
this.contextProviders.recentActivity(),
|
|
31906
33043
|
this.contextProviders.agentInventory(),
|
|
31907
33044
|
this.contextProviders.openInbox()
|
|
31908
33045
|
]);
|
|
31909
|
-
return
|
|
31910
|
-
|
|
31911
|
-
|
|
31912
|
-
|
|
31913
|
-
|
|
31914
|
-
|
|
31915
|
-
|
|
31916
|
-
|
|
31917
|
-
|
|
31918
|
-
${inbox}
|
|
33046
|
+
return [
|
|
33047
|
+
ref,
|
|
33048
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
33049
|
+
...priorSection ? [priorSection] : [],
|
|
33050
|
+
`## Recent activity
|
|
33051
|
+
${activity}`,
|
|
33052
|
+
`## Wrapped agents
|
|
33053
|
+
${agents}`,
|
|
33054
|
+
`## Open inbox
|
|
33055
|
+
${inbox}`
|
|
33056
|
+
].join("\n\n");
|
|
33057
|
+
}
|
|
33058
|
+
/**
|
|
33059
|
+
* Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
|
|
33060
|
+
* trip. Fail-soft on every axis: missing fetchers short-circuit to
|
|
33061
|
+
* an empty fold, fetcher failures emit a per-category audit event
|
|
33062
|
+
* and are omitted from the rendered section, an LLM-assist failure
|
|
33063
|
+
* proceeds with no fold. Returns the rendered section + the list of
|
|
33064
|
+
* categories whose data made it into the section (used for the
|
|
33065
|
+
* round-trip audit emission).
|
|
33066
|
+
*/
|
|
33067
|
+
async runDynamicContextFold(query) {
|
|
33068
|
+
if (!this.contextFetchers) {
|
|
33069
|
+
return { section: "", categoriesIncluded: [] };
|
|
33070
|
+
}
|
|
33071
|
+
const result = await foldContext(query, this.contextFetchers, {
|
|
33072
|
+
maxTokens: this.dynamicContextBudget,
|
|
33073
|
+
...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
|
|
33074
|
+
onFetcherFailure: (category, error) => {
|
|
33075
|
+
this.emitContextFetcherFailed(category, classifyFetcherError(error));
|
|
33076
|
+
}
|
|
33077
|
+
});
|
|
33078
|
+
return result;
|
|
33079
|
+
}
|
|
33080
|
+
/**
|
|
33081
|
+
* Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
|
|
33082
|
+
* of the fold path so the dynamic-context handler stays readable.
|
|
33083
|
+
* Emits with `result: "failure"` since the named category dropped
|
|
33084
|
+
* from the rendered section for this round-trip.
|
|
33085
|
+
*/
|
|
33086
|
+
emitContextFetcherFailed(category, failureReason) {
|
|
33087
|
+
const payload = {
|
|
33088
|
+
version: "1.2",
|
|
33089
|
+
event_id: makeEventId("conc-ctxfail"),
|
|
33090
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33091
|
+
identity_id: this.identityId,
|
|
33092
|
+
kind: "operator_concierge_context_fetcher_failed",
|
|
33093
|
+
surface: "concierge",
|
|
33094
|
+
category,
|
|
33095
|
+
failure_reason: failureReason
|
|
33096
|
+
};
|
|
33097
|
+
this.emit(
|
|
33098
|
+
OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
|
|
33099
|
+
payload,
|
|
33100
|
+
"failure"
|
|
33101
|
+
);
|
|
33102
|
+
}
|
|
33103
|
+
/**
|
|
33104
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
33105
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
33106
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
33107
|
+
* the input is empty or when the budget excludes every turn.
|
|
33108
|
+
*/
|
|
33109
|
+
formatPriorTurnsSection(turns) {
|
|
33110
|
+
if (turns.length === 0) return "";
|
|
33111
|
+
const HEADER = "## Prior conversation";
|
|
33112
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
33113
|
+
const headerTokens = approxTokenLen2(`${HEADER}
|
|
33114
|
+
`);
|
|
33115
|
+
const sepTokens = approxTokenLen2("\n");
|
|
33116
|
+
let runningTokens = headerTokens;
|
|
33117
|
+
let runningLines = [];
|
|
33118
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
33119
|
+
const line = lines[i];
|
|
33120
|
+
const tokens = approxTokenLen2(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
33121
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
33122
|
+
runningTokens += tokens;
|
|
33123
|
+
runningLines.push(line);
|
|
33124
|
+
}
|
|
33125
|
+
if (runningLines.length === 0) return "";
|
|
33126
|
+
runningLines = runningLines.reverse();
|
|
33127
|
+
return `${HEADER}
|
|
33128
|
+
${runningLines.join("\n")}`;
|
|
31919
33129
|
}
|
|
31920
33130
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
31921
33131
|
emit(operation, payload, result) {
|
|
@@ -31931,6 +33141,21 @@ ${inbox}`;
|
|
|
31931
33141
|
function makeEventId(prefix) {
|
|
31932
33142
|
return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
31933
33143
|
}
|
|
33144
|
+
function classifyFetcherError(error) {
|
|
33145
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
33146
|
+
if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
|
|
33147
|
+
if (msg.includes("schema") || msg.includes("invalid shape")) {
|
|
33148
|
+
return "schema_mismatch";
|
|
33149
|
+
}
|
|
33150
|
+
if (msg.includes("io") || msg.includes("read") || msg.includes("enoent") || msg.includes("eacces")) {
|
|
33151
|
+
return "io_failed";
|
|
33152
|
+
}
|
|
33153
|
+
return "unknown";
|
|
33154
|
+
}
|
|
33155
|
+
function formatPriorTurnLine(turn) {
|
|
33156
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
33157
|
+
return `${label}: ${turn.content}`;
|
|
33158
|
+
}
|
|
31934
33159
|
function hashOf(input) {
|
|
31935
33160
|
return hashToString(sha256(stringToBytes(input)));
|
|
31936
33161
|
}
|
|
@@ -31939,7 +33164,7 @@ function hashOf(input) {
|
|
|
31939
33164
|
init_encryption();
|
|
31940
33165
|
init_encoding();
|
|
31941
33166
|
var OPERATOR_CHAT_NAMESPACE = "_chat";
|
|
31942
|
-
var
|
|
33167
|
+
var HKDF_INFO2 = "operator-chat-store-v1";
|
|
31943
33168
|
function chatStorageKey(surface, threadKey) {
|
|
31944
33169
|
return `${surface}.${threadKey}`;
|
|
31945
33170
|
}
|
|
@@ -31948,7 +33173,7 @@ var OperatorChatStore = class {
|
|
|
31948
33173
|
encryptionKey;
|
|
31949
33174
|
constructor(storage, masterKey) {
|
|
31950
33175
|
this.storage = storage;
|
|
31951
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33176
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
|
|
31952
33177
|
}
|
|
31953
33178
|
/**
|
|
31954
33179
|
* Load a thread. Returns null if no record exists or if the on-disk
|
|
@@ -32032,9 +33257,9 @@ init_encryption();
|
|
|
32032
33257
|
init_encoding();
|
|
32033
33258
|
var CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
32034
33259
|
var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
32035
|
-
var
|
|
33260
|
+
var HKDF_INFO3 = "concierge-memory-store-v1";
|
|
32036
33261
|
var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
32037
|
-
var
|
|
33262
|
+
var MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
|
|
32038
33263
|
var ConciergeMemoryStore = class {
|
|
32039
33264
|
storage;
|
|
32040
33265
|
encryptionKey;
|
|
@@ -32043,7 +33268,7 @@ var ConciergeMemoryStore = class {
|
|
|
32043
33268
|
locks;
|
|
32044
33269
|
constructor(opts) {
|
|
32045
33270
|
this.storage = opts.storage;
|
|
32046
|
-
this.encryptionKey = derivePurposeKey(opts.masterKey,
|
|
33271
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
|
|
32047
33272
|
this.fortressId = opts.fortressId;
|
|
32048
33273
|
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
32049
33274
|
this.locks = /* @__PURE__ */ new Map();
|
|
@@ -32099,6 +33324,65 @@ var ConciergeMemoryStore = class {
|
|
|
32099
33324
|
}
|
|
32100
33325
|
return turns;
|
|
32101
33326
|
}
|
|
33327
|
+
/**
|
|
33328
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
33329
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
33330
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
33331
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
33332
|
+
* with a concrete cause.
|
|
33333
|
+
*
|
|
33334
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
33335
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
33336
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
33337
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
33338
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
33339
|
+
* - Storage IO error → `io_failed`.
|
|
33340
|
+
*/
|
|
33341
|
+
async readThreadStrict(threadId, opts) {
|
|
33342
|
+
const key = bundleKey(threadId);
|
|
33343
|
+
let raw;
|
|
33344
|
+
try {
|
|
33345
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33346
|
+
} catch {
|
|
33347
|
+
return { ok: false, reason: "io_failed" };
|
|
33348
|
+
}
|
|
33349
|
+
if (!raw) return { ok: true, turns: [] };
|
|
33350
|
+
if (raw.length > MAX_BUNDLE_BYTES3) {
|
|
33351
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
33352
|
+
}
|
|
33353
|
+
let envelope;
|
|
33354
|
+
try {
|
|
33355
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
33356
|
+
} catch {
|
|
33357
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
33358
|
+
}
|
|
33359
|
+
let plaintext;
|
|
33360
|
+
try {
|
|
33361
|
+
const aad = stringToBytes(threadId);
|
|
33362
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
33363
|
+
} catch {
|
|
33364
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
33365
|
+
}
|
|
33366
|
+
let parsed;
|
|
33367
|
+
try {
|
|
33368
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
33369
|
+
} catch {
|
|
33370
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
33371
|
+
}
|
|
33372
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
33373
|
+
if (parsed.thread_id !== threadId) {
|
|
33374
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
33375
|
+
}
|
|
33376
|
+
let turns = parsed.turns;
|
|
33377
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
33378
|
+
const cutoff = opts.sinceTurnId;
|
|
33379
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
33380
|
+
}
|
|
33381
|
+
if (opts?.limit !== void 0) {
|
|
33382
|
+
turns = turns.slice(0, opts.limit);
|
|
33383
|
+
}
|
|
33384
|
+
return { ok: true, turns };
|
|
33385
|
+
}
|
|
32102
33386
|
/**
|
|
32103
33387
|
* Enumerate concierge threads in this fortress with summary metadata.
|
|
32104
33388
|
* Sorted newest-first by last_turn_at.
|
|
@@ -32110,7 +33394,7 @@ var ConciergeMemoryStore = class {
|
|
|
32110
33394
|
);
|
|
32111
33395
|
const summaries = [];
|
|
32112
33396
|
for (const meta of entries) {
|
|
32113
|
-
const threadId =
|
|
33397
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
32114
33398
|
if (threadId === null) continue;
|
|
32115
33399
|
const bundle = await this.loadBundle(threadId);
|
|
32116
33400
|
if (!bundle || bundle.turns.length === 0) continue;
|
|
@@ -32163,7 +33447,7 @@ var ConciergeMemoryStore = class {
|
|
|
32163
33447
|
);
|
|
32164
33448
|
let pruned = 0;
|
|
32165
33449
|
for (const meta of entries) {
|
|
32166
|
-
const threadId =
|
|
33450
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
32167
33451
|
if (threadId === null) continue;
|
|
32168
33452
|
pruned += await this.withLock(threadId, async () => {
|
|
32169
33453
|
const bundle = await this.loadBundle(threadId);
|
|
@@ -32194,7 +33478,7 @@ var ConciergeMemoryStore = class {
|
|
|
32194
33478
|
return null;
|
|
32195
33479
|
}
|
|
32196
33480
|
if (!raw) return null;
|
|
32197
|
-
if (raw.length >
|
|
33481
|
+
if (raw.length > MAX_BUNDLE_BYTES3) return null;
|
|
32198
33482
|
try {
|
|
32199
33483
|
const envelope = JSON.parse(bytesToString(raw));
|
|
32200
33484
|
const aad = stringToBytes(threadId);
|
|
@@ -32247,7 +33531,7 @@ var ConciergeMemoryStore = class {
|
|
|
32247
33531
|
function bundleKey(threadId) {
|
|
32248
33532
|
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
32249
33533
|
}
|
|
32250
|
-
function
|
|
33534
|
+
function stripKeyPrefix2(key) {
|
|
32251
33535
|
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
32252
33536
|
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
32253
33537
|
}
|
|
@@ -32316,7 +33600,18 @@ function buildV11Bindings(inputs) {
|
|
|
32316
33600
|
registry
|
|
32317
33601
|
}),
|
|
32318
33602
|
conciergePiiFilter: buildConciergePiiFilter(),
|
|
32319
|
-
conciergeMemory
|
|
33603
|
+
conciergeMemory,
|
|
33604
|
+
conciergeContextFetchers: buildConciergeContextFetchers({
|
|
33605
|
+
auditLog: inputs.auditLog,
|
|
33606
|
+
identityId: inputs.identityId,
|
|
33607
|
+
registry
|
|
33608
|
+
}),
|
|
33609
|
+
...inputs.intelligenceSelector ? {
|
|
33610
|
+
conciergeContextLlmAssist: buildConciergeContextLlmAssist({
|
|
33611
|
+
selector: inputs.intelligenceSelector,
|
|
33612
|
+
identityId: inputs.identityId
|
|
33613
|
+
})
|
|
33614
|
+
} : {}
|
|
32320
33615
|
});
|
|
32321
33616
|
}
|
|
32322
33617
|
const hubService = new HubService({
|
|
@@ -32377,6 +33672,107 @@ function buildConciergeContextProviders(args) {
|
|
|
32377
33672
|
}
|
|
32378
33673
|
};
|
|
32379
33674
|
}
|
|
33675
|
+
function buildConciergeContextFetchers(args) {
|
|
33676
|
+
const empty = async () => "";
|
|
33677
|
+
return {
|
|
33678
|
+
templates: async () => {
|
|
33679
|
+
const entries = listTemplates();
|
|
33680
|
+
if (entries.length === 0) return "(no templates installed)";
|
|
33681
|
+
const lines = entries.map((e) => {
|
|
33682
|
+
const m = e.metadata;
|
|
33683
|
+
return `${m.name} (tier ${m.tier}, channel ${m.channel}, target ${m.target_archetype})`;
|
|
33684
|
+
});
|
|
33685
|
+
return lines.join("\n");
|
|
33686
|
+
},
|
|
33687
|
+
agent_state: async (agentNameHint) => {
|
|
33688
|
+
const records = args.registry.list({ identity_id: args.identityId });
|
|
33689
|
+
if (records.length === 0) return "(no wrapped agents)";
|
|
33690
|
+
const filtered = agentNameHint ? records.filter(
|
|
33691
|
+
(r) => r.agent_id.toLowerCase().includes(agentNameHint.toLowerCase()) || r.harness.toLowerCase().includes(agentNameHint.toLowerCase())
|
|
33692
|
+
) : records;
|
|
33693
|
+
const target = filtered.length > 0 ? filtered : records;
|
|
33694
|
+
const lines = target.slice(0, 20).map((r) => {
|
|
33695
|
+
const tmpl = typeof r.channel_template_id === "string" ? r.channel_template_id : "no_template";
|
|
33696
|
+
return `${r.agent_id} harness=${r.harness} status=${r.status} template=${tmpl}`;
|
|
33697
|
+
});
|
|
33698
|
+
return lines.join("\n");
|
|
33699
|
+
},
|
|
33700
|
+
agent_activity: async (agentNameHint) => {
|
|
33701
|
+
const result = await args.auditLog.query({ limit: 50 });
|
|
33702
|
+
const owned = result.entries.filter(
|
|
33703
|
+
(e) => e.identity_id === args.identityId
|
|
33704
|
+
);
|
|
33705
|
+
const filtered = agentNameHint ? owned.filter((e) => {
|
|
33706
|
+
const agentId = e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : "";
|
|
33707
|
+
return agentId.toLowerCase().includes(agentNameHint.toLowerCase());
|
|
33708
|
+
}) : owned;
|
|
33709
|
+
const tail = (filtered.length > 0 ? filtered : owned).slice(-20);
|
|
33710
|
+
if (tail.length === 0) return "(no activity)";
|
|
33711
|
+
return tail.map((e) => {
|
|
33712
|
+
const agentId = (e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : null) ?? "_fortress";
|
|
33713
|
+
return `${e.timestamp} ${e.layer}.${e.operation} agent=${agentId} result=${e.result}`;
|
|
33714
|
+
}).join("\n");
|
|
33715
|
+
},
|
|
33716
|
+
audit_log: async () => {
|
|
33717
|
+
const result = await args.auditLog.query({ limit: 30 });
|
|
33718
|
+
const owned = result.entries.filter(
|
|
33719
|
+
(e) => e.identity_id === args.identityId
|
|
33720
|
+
);
|
|
33721
|
+
if (owned.length === 0) return "(no audit log entries)";
|
|
33722
|
+
return owned.slice(-30).map(
|
|
33723
|
+
(e) => `${e.timestamp} ${e.layer}.${e.operation} result=${e.result}`
|
|
33724
|
+
).join("\n");
|
|
33725
|
+
},
|
|
33726
|
+
sentinel_findings: empty,
|
|
33727
|
+
anomaly_alerts: empty,
|
|
33728
|
+
recent_receipts: async () => {
|
|
33729
|
+
const result = await args.auditLog.query({ limit: 100 });
|
|
33730
|
+
const owned = result.entries.filter(
|
|
33731
|
+
(e) => e.identity_id === args.identityId && e.operation.startsWith("composition_")
|
|
33732
|
+
);
|
|
33733
|
+
if (owned.length === 0) return "(no recent composition events)";
|
|
33734
|
+
return owned.slice(-15).map((e) => `${e.timestamp} ${e.operation} result=${e.result}`).join("\n");
|
|
33735
|
+
},
|
|
33736
|
+
verascore_deltas: empty
|
|
33737
|
+
};
|
|
33738
|
+
}
|
|
33739
|
+
function buildConciergeContextLlmAssist(args) {
|
|
33740
|
+
return async (query, categories) => {
|
|
33741
|
+
const labelList = categories.map((c) => `- ${c}`).join("\n");
|
|
33742
|
+
const prompt = `You are a router. Classify the operator's query into one of the categories below or "none".
|
|
33743
|
+
Reply with exactly one token: one category name or "none".
|
|
33744
|
+
|
|
33745
|
+
Categories:
|
|
33746
|
+
${labelList}
|
|
33747
|
+
|
|
33748
|
+
Query: ${query}
|
|
33749
|
+
|
|
33750
|
+
Category:`;
|
|
33751
|
+
try {
|
|
33752
|
+
const handle = await args.selector.getSubstrate("concierge");
|
|
33753
|
+
if (!handle.capability.summarize) return "none";
|
|
33754
|
+
const response = await args.selector.invokeSummarize("concierge", {
|
|
33755
|
+
kind: "summarize",
|
|
33756
|
+
context: prompt,
|
|
33757
|
+
query: "Output the single category token.",
|
|
33758
|
+
maxTokens: 16
|
|
33759
|
+
});
|
|
33760
|
+
if (response.failureClass || response.body.kind !== "summarize") {
|
|
33761
|
+
return "none";
|
|
33762
|
+
}
|
|
33763
|
+
const raw = response.body.text.trim().toLowerCase();
|
|
33764
|
+
const head = raw.split(/\s|[.,!?:;]/)[0] ?? "";
|
|
33765
|
+
const normalized = head.replace(/[^a-z_]/g, "");
|
|
33766
|
+
const known = categories;
|
|
33767
|
+
if (known.includes(normalized)) {
|
|
33768
|
+
return normalized;
|
|
33769
|
+
}
|
|
33770
|
+
return "none";
|
|
33771
|
+
} catch {
|
|
33772
|
+
return "none";
|
|
33773
|
+
}
|
|
33774
|
+
};
|
|
33775
|
+
}
|
|
32380
33776
|
function buildConciergePiiFilter() {
|
|
32381
33777
|
return {
|
|
32382
33778
|
filter(input) {
|
|
@@ -32508,13 +33904,13 @@ init_encryption();
|
|
|
32508
33904
|
init_encoding();
|
|
32509
33905
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
32510
33906
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
32511
|
-
var
|
|
33907
|
+
var HKDF_INFO4 = "intelligence-substrate-config";
|
|
32512
33908
|
var IntelligenceConfigStore = class {
|
|
32513
33909
|
storage;
|
|
32514
33910
|
encryptionKey;
|
|
32515
33911
|
constructor(storage, masterKey) {
|
|
32516
33912
|
this.storage = storage;
|
|
32517
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33913
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
|
|
32518
33914
|
}
|
|
32519
33915
|
/**
|
|
32520
33916
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -35083,7 +36479,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
35083
36479
|
}
|
|
35084
36480
|
return null;
|
|
35085
36481
|
}
|
|
35086
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
36482
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
35087
36483
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
35088
36484
|
if (!destinationSigner) {
|
|
35089
36485
|
return {
|
|
@@ -35145,8 +36541,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
35145
36541
|
}
|
|
35146
36542
|
}
|
|
35147
36543
|
}
|
|
36544
|
+
let plaintext;
|
|
35148
36545
|
try {
|
|
35149
|
-
|
|
36546
|
+
plaintext = decrypt(
|
|
35150
36547
|
item.entry.payload,
|
|
35151
36548
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
35152
36549
|
);
|
|
@@ -35155,28 +36552,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
35155
36552
|
skipped++;
|
|
35156
36553
|
continue;
|
|
35157
36554
|
}
|
|
35158
|
-
await stateStore.write(
|
|
35159
|
-
item.namespace,
|
|
35160
|
-
item.key,
|
|
35161
|
-
bytesToString(plaintext),
|
|
35162
|
-
destinationSigner.identity_id,
|
|
35163
|
-
destinationSigner.encrypted_private_key,
|
|
35164
|
-
identityEncryptionKey,
|
|
35165
|
-
{
|
|
35166
|
-
content_type: item.entry.metadata.content_type,
|
|
35167
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
35168
|
-
tags: [
|
|
35169
|
-
...item.entry.metadata.tags ?? [],
|
|
35170
|
-
"exit-import",
|
|
35171
|
-
`source:${item.entry.kid}`
|
|
35172
|
-
]
|
|
35173
|
-
}
|
|
35174
|
-
);
|
|
35175
|
-
imported++;
|
|
35176
36555
|
} catch {
|
|
35177
36556
|
skippedInvalidSig++;
|
|
35178
36557
|
skipped++;
|
|
36558
|
+
continue;
|
|
35179
36559
|
}
|
|
36560
|
+
await stateStore.write(
|
|
36561
|
+
item.namespace,
|
|
36562
|
+
item.key,
|
|
36563
|
+
bytesToString(plaintext),
|
|
36564
|
+
destinationSigner.identity_id,
|
|
36565
|
+
destinationSigner.encrypted_private_key,
|
|
36566
|
+
identityEncryptionKey,
|
|
36567
|
+
{
|
|
36568
|
+
content_type: item.entry.metadata.content_type,
|
|
36569
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
36570
|
+
tags: [
|
|
36571
|
+
...item.entry.metadata.tags ?? [],
|
|
36572
|
+
"exit-import",
|
|
36573
|
+
`source:${item.entry.kid}`
|
|
36574
|
+
]
|
|
36575
|
+
}
|
|
36576
|
+
);
|
|
36577
|
+
imported++;
|
|
36578
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
35180
36579
|
}
|
|
35181
36580
|
return {
|
|
35182
36581
|
status: "rekeyed",
|
|
@@ -35187,6 +36586,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
35187
36586
|
conflicts
|
|
35188
36587
|
};
|
|
35189
36588
|
}
|
|
36589
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
36590
|
+
let removed = 0;
|
|
36591
|
+
const failed = [];
|
|
36592
|
+
for (const loc of staged) {
|
|
36593
|
+
try {
|
|
36594
|
+
const ok = await storage.delete(loc.namespace, loc.key);
|
|
36595
|
+
if (ok) {
|
|
36596
|
+
removed++;
|
|
36597
|
+
} else {
|
|
36598
|
+
failed.push(loc);
|
|
36599
|
+
}
|
|
36600
|
+
} catch {
|
|
36601
|
+
failed.push(loc);
|
|
36602
|
+
}
|
|
36603
|
+
}
|
|
36604
|
+
return { removed, failed };
|
|
36605
|
+
}
|
|
35190
36606
|
async function stageArtifact(storage, namespace, key, value) {
|
|
35191
36607
|
await storage.write(namespace, key, jsonBytes(value));
|
|
35192
36608
|
}
|
|
@@ -35311,6 +36727,8 @@ async function importExitBundle(opts) {
|
|
|
35311
36727
|
}
|
|
35312
36728
|
const importId = importIdForManifest(manifest);
|
|
35313
36729
|
const stagedArtifacts = [];
|
|
36730
|
+
const stagedLocations = [];
|
|
36731
|
+
const importedRekeyEntries = [];
|
|
35314
36732
|
if (identityArtifact) {
|
|
35315
36733
|
await stageArtifact(
|
|
35316
36734
|
opts.storage,
|
|
@@ -35319,10 +36737,15 @@ async function importExitBundle(opts) {
|
|
|
35319
36737
|
identityArtifact.json
|
|
35320
36738
|
);
|
|
35321
36739
|
stagedArtifacts.push("public_identity");
|
|
36740
|
+
stagedLocations.push({
|
|
36741
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
36742
|
+
key: identityArtifact.json.bundle.identity_id
|
|
36743
|
+
});
|
|
35322
36744
|
}
|
|
35323
36745
|
if (policySet) {
|
|
35324
36746
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
35325
36747
|
stagedArtifacts.push("policy_set");
|
|
36748
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
35326
36749
|
}
|
|
35327
36750
|
if (auditReceipts) {
|
|
35328
36751
|
await stageArtifact(
|
|
@@ -35332,10 +36755,12 @@ async function importExitBundle(opts) {
|
|
|
35332
36755
|
auditReceipts.json
|
|
35333
36756
|
);
|
|
35334
36757
|
stagedArtifacts.push("audit_receipts");
|
|
36758
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
35335
36759
|
}
|
|
35336
36760
|
if (commitments) {
|
|
35337
36761
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
35338
36762
|
stagedArtifacts.push("commitments");
|
|
36763
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
35339
36764
|
}
|
|
35340
36765
|
if (placeholderMetadata) {
|
|
35341
36766
|
await stageArtifact(
|
|
@@ -35345,12 +36770,17 @@ async function importExitBundle(opts) {
|
|
|
35345
36770
|
placeholderMetadata.json
|
|
35346
36771
|
);
|
|
35347
36772
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
36773
|
+
stagedLocations.push({
|
|
36774
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
36775
|
+
key: importId
|
|
36776
|
+
});
|
|
35348
36777
|
}
|
|
35349
36778
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
35350
36779
|
manifest: manifest.body,
|
|
35351
36780
|
verified_at: verification.verified_at,
|
|
35352
36781
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
35353
36782
|
});
|
|
36783
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
35354
36784
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
35355
36785
|
let reputationResult = {
|
|
35356
36786
|
imported_attestations: 0,
|
|
@@ -35375,26 +36805,57 @@ async function importExitBundle(opts) {
|
|
|
35375
36805
|
encryptedState?.json ?? null,
|
|
35376
36806
|
opts
|
|
35377
36807
|
);
|
|
35378
|
-
|
|
35379
|
-
|
|
35380
|
-
|
|
35381
|
-
|
|
35382
|
-
|
|
35383
|
-
|
|
35384
|
-
|
|
35385
|
-
|
|
35386
|
-
|
|
35387
|
-
|
|
35388
|
-
|
|
35389
|
-
|
|
35390
|
-
|
|
35391
|
-
|
|
35392
|
-
|
|
35393
|
-
|
|
35394
|
-
|
|
35395
|
-
|
|
35396
|
-
|
|
35397
|
-
|
|
36808
|
+
let stateResult;
|
|
36809
|
+
try {
|
|
36810
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
36811
|
+
encryptedState.json,
|
|
36812
|
+
opts,
|
|
36813
|
+
sourceMasterKey,
|
|
36814
|
+
publicKeys.byIdentityId,
|
|
36815
|
+
importedRekeyEntries
|
|
36816
|
+
) : {
|
|
36817
|
+
status: "staged_requires_source_key",
|
|
36818
|
+
imported_keys: 0,
|
|
36819
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
36820
|
+
skipped_invalid_sig: 0,
|
|
36821
|
+
skipped_unknown_kid: 0,
|
|
36822
|
+
conflicts: conflicts.state_conflicts.length
|
|
36823
|
+
} : {
|
|
36824
|
+
status: "not_requested",
|
|
36825
|
+
imported_keys: 0,
|
|
36826
|
+
skipped_keys: 0,
|
|
36827
|
+
skipped_invalid_sig: 0,
|
|
36828
|
+
skipped_unknown_kid: 0,
|
|
36829
|
+
conflicts: 0
|
|
36830
|
+
};
|
|
36831
|
+
} catch (err) {
|
|
36832
|
+
const toCleanup = [
|
|
36833
|
+
...importedRekeyEntries,
|
|
36834
|
+
...stagedLocations
|
|
36835
|
+
];
|
|
36836
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
36837
|
+
opts.auditLog.append(
|
|
36838
|
+
"l1",
|
|
36839
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
36840
|
+
manifest.body.identity_binding.identity_id,
|
|
36841
|
+
{
|
|
36842
|
+
import_id: importId,
|
|
36843
|
+
manifest_version: manifest.body.manifest_version,
|
|
36844
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
36845
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
36846
|
+
removed_total: cleanup.removed,
|
|
36847
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
36848
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
36849
|
+
},
|
|
36850
|
+
"failure"
|
|
36851
|
+
);
|
|
36852
|
+
await opts.auditLog.flush();
|
|
36853
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
36854
|
+
throw new ExitBundleImportError(
|
|
36855
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
36856
|
+
`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).`
|
|
36857
|
+
);
|
|
36858
|
+
}
|
|
35398
36859
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
35399
36860
|
import_id: importId,
|
|
35400
36861
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -36393,16 +37854,35 @@ ${err.message}
|
|
|
36393
37854
|
timestamp: alert.timestamp
|
|
36394
37855
|
});
|
|
36395
37856
|
} : void 0;
|
|
36396
|
-
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
36397
37857
|
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
36398
37858
|
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37859
|
+
const aggregatorPayloadStore = new AggregatorPayloadStore({
|
|
37860
|
+
storage,
|
|
37861
|
+
masterKey,
|
|
37862
|
+
fortressId: fortressIdForAggregator
|
|
37863
|
+
});
|
|
36399
37864
|
const approvalAggregator = new ApprovalAggregator({
|
|
36400
37865
|
storage,
|
|
36401
37866
|
masterKey,
|
|
36402
37867
|
auditLog,
|
|
36403
37868
|
identityId: aggregatorIdentityId,
|
|
36404
|
-
fortressId: fortressIdForAggregator
|
|
37869
|
+
fortressId: fortressIdForAggregator,
|
|
37870
|
+
payloadStore: aggregatorPayloadStore
|
|
36405
37871
|
});
|
|
37872
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
37873
|
+
underlying: approvalChannel,
|
|
37874
|
+
aggregator: approvalAggregator,
|
|
37875
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
37876
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
37877
|
+
});
|
|
37878
|
+
const gate = new ApprovalGate(
|
|
37879
|
+
policy,
|
|
37880
|
+
baseline,
|
|
37881
|
+
wrappedApprovalChannel,
|
|
37882
|
+
auditLog,
|
|
37883
|
+
injectionDetector,
|
|
37884
|
+
onInjectionAlert
|
|
37885
|
+
);
|
|
36406
37886
|
gate.setApprovalEventCallback((event) => {
|
|
36407
37887
|
void approvalAggregator.ingest(event);
|
|
36408
37888
|
});
|