@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/cli.cjs
CHANGED
|
@@ -4457,9 +4457,35 @@ function validatePolicy(raw) {
|
|
|
4457
4457
|
};
|
|
4458
4458
|
delete merged.auto_deny;
|
|
4459
4459
|
return merged;
|
|
4460
|
-
})()
|
|
4460
|
+
})(),
|
|
4461
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4461
4462
|
};
|
|
4462
4463
|
}
|
|
4464
|
+
function parseApprovalRedirect(raw) {
|
|
4465
|
+
if (raw === void 0 || raw === null) {
|
|
4466
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4467
|
+
}
|
|
4468
|
+
if (typeof raw !== "object") {
|
|
4469
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4470
|
+
}
|
|
4471
|
+
const obj = raw;
|
|
4472
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4473
|
+
const modeRaw = obj.mode;
|
|
4474
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4475
|
+
if (modeRaw !== void 0) {
|
|
4476
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4477
|
+
throw new Error(
|
|
4478
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4479
|
+
);
|
|
4480
|
+
}
|
|
4481
|
+
mode = modeRaw;
|
|
4482
|
+
}
|
|
4483
|
+
const result = { enabled, mode };
|
|
4484
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4485
|
+
result.per_agent = obj.per_agent;
|
|
4486
|
+
}
|
|
4487
|
+
return result;
|
|
4488
|
+
}
|
|
4463
4489
|
function generateDefaultPolicyYaml() {
|
|
4464
4490
|
return `# Sanctuary Principal Policy v1
|
|
4465
4491
|
# This file controls what your agent can do without asking.
|
|
@@ -4538,6 +4564,7 @@ tier3_always_allow:
|
|
|
4538
4564
|
- handshake_status
|
|
4539
4565
|
- handshake_exchange
|
|
4540
4566
|
- handshake_verify_attestation
|
|
4567
|
+
- handshake_abort
|
|
4541
4568
|
- reputation_query_weighted
|
|
4542
4569
|
- federation_peers
|
|
4543
4570
|
- federation_trust_evaluate
|
|
@@ -4572,6 +4599,21 @@ tier3_always_allow:
|
|
|
4572
4599
|
approval_channel:
|
|
4573
4600
|
type: stderr
|
|
4574
4601
|
timeout_seconds: 300
|
|
4602
|
+
|
|
4603
|
+
# \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
|
|
4604
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4605
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4606
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4607
|
+
#
|
|
4608
|
+
# mode:
|
|
4609
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4610
|
+
# decision from the inbox (default once enabled).
|
|
4611
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4612
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4613
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4614
|
+
approval_redirect:
|
|
4615
|
+
enabled: false
|
|
4616
|
+
mode: replace
|
|
4575
4617
|
`;
|
|
4576
4618
|
}
|
|
4577
4619
|
async function loadPrincipalPolicy(storagePath) {
|
|
@@ -4608,7 +4650,7 @@ async function loadPrincipalPolicy(storagePath) {
|
|
|
4608
4650
|
);
|
|
4609
4651
|
}
|
|
4610
4652
|
}
|
|
4611
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4653
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_APPROVAL_REDIRECT, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4612
4654
|
var init_loader = __esm({
|
|
4613
4655
|
"src/principal-policy/loader.ts"() {
|
|
4614
4656
|
DEFAULT_TIER2 = {
|
|
@@ -4625,6 +4667,10 @@ var init_loader = __esm({
|
|
|
4625
4667
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4626
4668
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4627
4669
|
};
|
|
4670
|
+
DEFAULT_APPROVAL_REDIRECT = {
|
|
4671
|
+
enabled: false,
|
|
4672
|
+
mode: "replace"
|
|
4673
|
+
};
|
|
4628
4674
|
DEFAULT_POLICY = {
|
|
4629
4675
|
version: 1,
|
|
4630
4676
|
tier1_always_approve: [
|
|
@@ -4698,6 +4744,7 @@ var init_loader = __esm({
|
|
|
4698
4744
|
"handshake_status",
|
|
4699
4745
|
"handshake_exchange",
|
|
4700
4746
|
"handshake_verify_attestation",
|
|
4747
|
+
"handshake_abort",
|
|
4701
4748
|
"reputation_query_weighted",
|
|
4702
4749
|
"federation_peers",
|
|
4703
4750
|
"federation_trust_evaluate",
|
|
@@ -4739,7 +4786,8 @@ var init_loader = __esm({
|
|
|
4739
4786
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4740
4787
|
// Read-only; rule-based Annex III classifier
|
|
4741
4788
|
],
|
|
4742
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4789
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4790
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4743
4791
|
};
|
|
4744
4792
|
MalformedPrincipalPolicyError = class extends Error {
|
|
4745
4793
|
constructor(policyPath, reason) {
|
|
@@ -17170,6 +17218,27 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
17170
17218
|
await handleStream2(deps, res);
|
|
17171
17219
|
return true;
|
|
17172
17220
|
}
|
|
17221
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
|
|
17222
|
+
const limit = parseLimit2(
|
|
17223
|
+
url.searchParams.get("limit"),
|
|
17224
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
17225
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
17226
|
+
);
|
|
17227
|
+
const statusRaw = url.searchParams.get("status");
|
|
17228
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
17229
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17230
|
+
const filterStatus = statusRaw && isStatusFilter(statusRaw) && statusRaw !== "pending" ? statusRaw : void 0;
|
|
17231
|
+
const entries = await deps.aggregator.getHistory(
|
|
17232
|
+
{
|
|
17233
|
+
limit,
|
|
17234
|
+
...filterStatus !== void 0 ? { status: filterStatus } : {},
|
|
17235
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
17236
|
+
},
|
|
17237
|
+
operatorId
|
|
17238
|
+
);
|
|
17239
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
17240
|
+
return true;
|
|
17241
|
+
}
|
|
17173
17242
|
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
17174
17243
|
const limit = parseLimit2(
|
|
17175
17244
|
url.searchParams.get("limit"),
|
|
@@ -17192,11 +17261,39 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
17192
17261
|
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17193
17262
|
return true;
|
|
17194
17263
|
}
|
|
17195
|
-
if (method === "GET" && entryMatch.action ===
|
|
17196
|
-
const
|
|
17197
|
-
|
|
17198
|
-
(
|
|
17264
|
+
if (method === "GET" && entryMatch.action === "audit-trail") {
|
|
17265
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
17266
|
+
if (!entry) {
|
|
17267
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17268
|
+
return true;
|
|
17269
|
+
}
|
|
17270
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17271
|
+
const trail = await deps.aggregator.getAuditTrail(
|
|
17272
|
+
entryMatch.aggregatorId,
|
|
17273
|
+
operatorId
|
|
17274
|
+
);
|
|
17275
|
+
writeJSON4(res, 200, { ok: true, data: { entry, audit_trail: trail } });
|
|
17276
|
+
return true;
|
|
17277
|
+
}
|
|
17278
|
+
if (method === "GET" && entryMatch.action === "payload") {
|
|
17279
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
17280
|
+
if (!entry) {
|
|
17281
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17282
|
+
return true;
|
|
17283
|
+
}
|
|
17284
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17285
|
+
const payload = await deps.aggregator.getFullPayloadWithAudit(
|
|
17286
|
+
entryMatch.aggregatorId,
|
|
17287
|
+
operatorId
|
|
17199
17288
|
);
|
|
17289
|
+
writeJSON4(res, 200, {
|
|
17290
|
+
ok: true,
|
|
17291
|
+
data: { entry, request_payload: payload }
|
|
17292
|
+
});
|
|
17293
|
+
return true;
|
|
17294
|
+
}
|
|
17295
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
17296
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
17200
17297
|
if (!entry) {
|
|
17201
17298
|
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17202
17299
|
return true;
|
|
@@ -20230,7 +20327,10 @@ var init_approval_aggregator = __esm({
|
|
|
20230
20327
|
APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
20231
20328
|
AGGREGATED: "cross_harness_approval_aggregated",
|
|
20232
20329
|
RESOLVED: "cross_harness_approval_resolved",
|
|
20233
|
-
DEDUPED: "cross_harness_approval_deduped"
|
|
20330
|
+
DEDUPED: "cross_harness_approval_deduped",
|
|
20331
|
+
PAYLOAD_DECRYPTED: "cross_harness_approval_payload_decrypted",
|
|
20332
|
+
AUDIT_TRAIL_VIEWED: "cross_harness_approval_audit_trail_viewed",
|
|
20333
|
+
REPLAYED: "cross_harness_approval_replayed"
|
|
20234
20334
|
};
|
|
20235
20335
|
DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
20236
20336
|
DEFAULT_MAX_LIST_LIMIT = 200;
|
|
@@ -20246,6 +20346,8 @@ var init_approval_aggregator = __esm({
|
|
|
20246
20346
|
now;
|
|
20247
20347
|
resolveSourceContext;
|
|
20248
20348
|
resolveHubInboxItemId;
|
|
20349
|
+
payloadStore;
|
|
20350
|
+
resolveEnforcementChain;
|
|
20249
20351
|
/** Cached entries by `aggregator_id`. */
|
|
20250
20352
|
entries = /* @__PURE__ */ new Map();
|
|
20251
20353
|
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
@@ -20275,6 +20377,14 @@ var init_approval_aggregator = __esm({
|
|
|
20275
20377
|
source_agent_id: this.fortressId
|
|
20276
20378
|
}));
|
|
20277
20379
|
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
20380
|
+
this.payloadStore = deps.payloadStore ?? null;
|
|
20381
|
+
this.resolveEnforcementChain = deps.resolveEnforcementChain ?? ((event) => [
|
|
20382
|
+
{
|
|
20383
|
+
layer: "l2",
|
|
20384
|
+
event: `approval_required:${event.operation}`,
|
|
20385
|
+
timestamp: event.request_timestamp
|
|
20386
|
+
}
|
|
20387
|
+
]);
|
|
20278
20388
|
}
|
|
20279
20389
|
/**
|
|
20280
20390
|
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
@@ -20325,13 +20435,152 @@ var init_approval_aggregator = __esm({
|
|
|
20325
20435
|
}
|
|
20326
20436
|
/**
|
|
20327
20437
|
* Return the original (unhashed) request payload for the entry. Returns
|
|
20328
|
-
* `null` when the entry is unknown
|
|
20329
|
-
*
|
|
20438
|
+
* `null` when the entry is unknown. When the in-memory payload map has
|
|
20439
|
+
* been evicted (e.g. after a server restart) and a `payloadStore` was
|
|
20440
|
+
* provided, the at-rest bundle is decrypted and the in-memory map is
|
|
20441
|
+
* refilled. Audit emission lives on the `*WithAudit` variant; this base
|
|
20442
|
+
* accessor is silent so internal callers can read without polluting the
|
|
20443
|
+
* audit trail.
|
|
20330
20444
|
*/
|
|
20331
20445
|
async getFullPayload(aggregatorId) {
|
|
20332
20446
|
await this.hydrate();
|
|
20333
20447
|
if (!this.entries.has(aggregatorId)) return null;
|
|
20334
|
-
|
|
20448
|
+
const cached = this.fullPayloads.get(aggregatorId);
|
|
20449
|
+
if (cached !== void 0) return cached;
|
|
20450
|
+
if (this.payloadStore) {
|
|
20451
|
+
try {
|
|
20452
|
+
const restored = await this.payloadStore.loadPayload(aggregatorId);
|
|
20453
|
+
if (restored !== null) {
|
|
20454
|
+
this.fullPayloads.set(aggregatorId, restored);
|
|
20455
|
+
return restored;
|
|
20456
|
+
}
|
|
20457
|
+
} catch {
|
|
20458
|
+
}
|
|
20459
|
+
}
|
|
20460
|
+
return null;
|
|
20461
|
+
}
|
|
20462
|
+
/**
|
|
20463
|
+
* Return the entry record for the given id, or null when unknown.
|
|
20464
|
+
* Idempotent. v1.3 Upsilon-3.
|
|
20465
|
+
*/
|
|
20466
|
+
async getEntry(aggregatorId) {
|
|
20467
|
+
await this.hydrate();
|
|
20468
|
+
return this.entries.get(aggregatorId) ?? null;
|
|
20469
|
+
}
|
|
20470
|
+
/**
|
|
20471
|
+
* Audited variant of `getFullPayload`. Emits the
|
|
20472
|
+
* `cross_harness_approval_payload_decrypted` audit event before
|
|
20473
|
+
* returning. Used by the operator-facing /payload replay route.
|
|
20474
|
+
* v1.3 Upsilon-3.
|
|
20475
|
+
*/
|
|
20476
|
+
async getFullPayloadWithAudit(aggregatorId, operatorId) {
|
|
20477
|
+
const payload = await this.getFullPayload(aggregatorId);
|
|
20478
|
+
if (payload === null) return null;
|
|
20479
|
+
const entry = this.entries.get(aggregatorId);
|
|
20480
|
+
this.auditLog.append(
|
|
20481
|
+
"l2",
|
|
20482
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.PAYLOAD_DECRYPTED,
|
|
20483
|
+
operatorId,
|
|
20484
|
+
{
|
|
20485
|
+
aggregator_id: aggregatorId,
|
|
20486
|
+
...entry ? {
|
|
20487
|
+
source_harness: entry.source_harness,
|
|
20488
|
+
source_agent_id: entry.source_agent_id,
|
|
20489
|
+
entry_status: entry.status
|
|
20490
|
+
} : {}
|
|
20491
|
+
}
|
|
20492
|
+
);
|
|
20493
|
+
return payload;
|
|
20494
|
+
}
|
|
20495
|
+
/**
|
|
20496
|
+
* Return the audit-log entries that led to and surround this approval.
|
|
20497
|
+
* Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
|
|
20498
|
+
* DEDUPED, replay events) all carry `details.aggregator_id` and link
|
|
20499
|
+
* directly. Gate-side emissions (`gate_*:operation`) do not carry the
|
|
20500
|
+
* aggregator id at v1.3, so they are matched via timestamp window
|
|
20501
|
+
* (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
|
|
20502
|
+
* pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
|
|
20503
|
+
* v1.3 Upsilon-3.
|
|
20504
|
+
*/
|
|
20505
|
+
async getAuditTrail(aggregatorId, operatorId) {
|
|
20506
|
+
await this.hydrate();
|
|
20507
|
+
const entry = this.entries.get(aggregatorId);
|
|
20508
|
+
if (!entry) {
|
|
20509
|
+
return [];
|
|
20510
|
+
}
|
|
20511
|
+
const sinceMs = Date.parse(entry.created_at) - 1e3;
|
|
20512
|
+
const sinceIso = new Date(sinceMs).toISOString();
|
|
20513
|
+
const queried = await this.auditLog.query({ since: sinceIso, limit: 1e3 });
|
|
20514
|
+
const operationPart = entry.policy_rule_id.includes(":") ? entry.policy_rule_id.slice(entry.policy_rule_id.indexOf(":") + 1) : entry.policy_rule_id;
|
|
20515
|
+
const lifetimeStart = sinceMs;
|
|
20516
|
+
const lifetimeEnd = entry.resolved_at ? Date.parse(entry.resolved_at) + 1e3 : Date.parse(entry.expires_at) + 1e3;
|
|
20517
|
+
const matches = [];
|
|
20518
|
+
for (const audit of queried.entries) {
|
|
20519
|
+
const detailsId = audit.details !== void 0 ? audit.details["aggregator_id"] : void 0;
|
|
20520
|
+
if (detailsId === aggregatorId) {
|
|
20521
|
+
matches.push(audit);
|
|
20522
|
+
continue;
|
|
20523
|
+
}
|
|
20524
|
+
const auditMs = Date.parse(audit.timestamp);
|
|
20525
|
+
if (auditMs < lifetimeStart || auditMs > lifetimeEnd) continue;
|
|
20526
|
+
if (audit.operation.endsWith(`:${operationPart}`)) {
|
|
20527
|
+
matches.push(audit);
|
|
20528
|
+
}
|
|
20529
|
+
}
|
|
20530
|
+
matches.sort(
|
|
20531
|
+
(a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
|
|
20532
|
+
);
|
|
20533
|
+
this.auditLog.append(
|
|
20534
|
+
"l2",
|
|
20535
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AUDIT_TRAIL_VIEWED,
|
|
20536
|
+
operatorId,
|
|
20537
|
+
{
|
|
20538
|
+
aggregator_id: aggregatorId,
|
|
20539
|
+
entry_status: entry.status,
|
|
20540
|
+
match_count: matches.length
|
|
20541
|
+
}
|
|
20542
|
+
);
|
|
20543
|
+
return matches;
|
|
20544
|
+
}
|
|
20545
|
+
/**
|
|
20546
|
+
* List historical (resolved) approvals. Excludes pending entries by
|
|
20547
|
+
* design: `list()` is the pending-inbox surface and `getHistory()` is
|
|
20548
|
+
* the resolved-replay surface. Emits REPLAYED on each call. v1.3
|
|
20549
|
+
* Upsilon-3.
|
|
20550
|
+
*/
|
|
20551
|
+
async getHistory(opts, operatorId) {
|
|
20552
|
+
await this.hydrate();
|
|
20553
|
+
await this.expireStale();
|
|
20554
|
+
const limit = Math.min(
|
|
20555
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
20556
|
+
this.maxListLimit
|
|
20557
|
+
);
|
|
20558
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
20559
|
+
const matching = [];
|
|
20560
|
+
for (const entry of this.entries.values()) {
|
|
20561
|
+
if (entry.status === "pending") continue;
|
|
20562
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
20563
|
+
const stamp = Date.parse(entry.resolved_at ?? entry.created_at);
|
|
20564
|
+
if (stamp < sinceMs) continue;
|
|
20565
|
+
matching.push(entry);
|
|
20566
|
+
}
|
|
20567
|
+
matching.sort((a, b) => {
|
|
20568
|
+
const aStamp = a.resolved_at ?? a.created_at;
|
|
20569
|
+
const bStamp = b.resolved_at ?? b.created_at;
|
|
20570
|
+
return bStamp.localeCompare(aStamp);
|
|
20571
|
+
});
|
|
20572
|
+
const sliced = matching.slice(0, limit);
|
|
20573
|
+
this.auditLog.append(
|
|
20574
|
+
"l2",
|
|
20575
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.REPLAYED,
|
|
20576
|
+
operatorId,
|
|
20577
|
+
{
|
|
20578
|
+
result_count: sliced.length,
|
|
20579
|
+
...opts?.status !== void 0 ? { status_filter: opts.status } : {},
|
|
20580
|
+
...opts?.sinceTs !== void 0 ? { since: opts.sinceTs } : {}
|
|
20581
|
+
}
|
|
20582
|
+
);
|
|
20583
|
+
return sliced;
|
|
20335
20584
|
}
|
|
20336
20585
|
/**
|
|
20337
20586
|
* Resolve an entry. Used by both:
|
|
@@ -20405,6 +20654,7 @@ var init_approval_aggregator = __esm({
|
|
|
20405
20654
|
const now = this.now();
|
|
20406
20655
|
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
20407
20656
|
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
20657
|
+
const enforcementChain = this.resolveEnforcementChain(event);
|
|
20408
20658
|
const entry = {
|
|
20409
20659
|
aggregator_id: id,
|
|
20410
20660
|
source_harness: ctx.source_harness,
|
|
@@ -20416,13 +20666,20 @@ var init_approval_aggregator = __esm({
|
|
|
20416
20666
|
status: "pending",
|
|
20417
20667
|
created_at: now.toISOString(),
|
|
20418
20668
|
expires_at: expires.toISOString(),
|
|
20419
|
-
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20669
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
|
|
20670
|
+
...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
|
|
20420
20671
|
};
|
|
20421
20672
|
this.entries.set(id, entry);
|
|
20422
20673
|
this.dedupIndex.set(dedupKey, id);
|
|
20423
20674
|
this.correlationIndex.set(event.correlation_id, id);
|
|
20424
20675
|
this.fullPayloads.set(id, event.context);
|
|
20425
20676
|
await this.persist(entry);
|
|
20677
|
+
if (this.payloadStore) {
|
|
20678
|
+
try {
|
|
20679
|
+
await this.payloadStore.savePayload(id, event.context);
|
|
20680
|
+
} catch {
|
|
20681
|
+
}
|
|
20682
|
+
}
|
|
20426
20683
|
this.auditLog.append(
|
|
20427
20684
|
"l2",
|
|
20428
20685
|
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
@@ -20570,6 +20827,317 @@ var init_approval_aggregator = __esm({
|
|
|
20570
20827
|
}
|
|
20571
20828
|
});
|
|
20572
20829
|
|
|
20830
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
20831
|
+
function auditEntryIdFor(request) {
|
|
20832
|
+
return `${request.timestamp}:${request.operation}`;
|
|
20833
|
+
}
|
|
20834
|
+
function statusToDecision(entry) {
|
|
20835
|
+
switch (entry.status) {
|
|
20836
|
+
case "approved":
|
|
20837
|
+
return {
|
|
20838
|
+
decision: "approve",
|
|
20839
|
+
decided_by: "human"
|
|
20840
|
+
};
|
|
20841
|
+
case "denied":
|
|
20842
|
+
return {
|
|
20843
|
+
decision: "deny",
|
|
20844
|
+
decided_by: "human"
|
|
20845
|
+
};
|
|
20846
|
+
case "timeout":
|
|
20847
|
+
case "expired":
|
|
20848
|
+
return {
|
|
20849
|
+
decision: "deny",
|
|
20850
|
+
decided_by: "timeout"
|
|
20851
|
+
};
|
|
20852
|
+
default:
|
|
20853
|
+
return null;
|
|
20854
|
+
}
|
|
20855
|
+
}
|
|
20856
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
20857
|
+
return (_request) => {
|
|
20858
|
+
const cfg = supplier().approval_redirect;
|
|
20859
|
+
if (!cfg || cfg.enabled !== true) {
|
|
20860
|
+
return { enabled: false, mode: "replace" };
|
|
20861
|
+
}
|
|
20862
|
+
return {
|
|
20863
|
+
enabled: true,
|
|
20864
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
20865
|
+
};
|
|
20866
|
+
};
|
|
20867
|
+
}
|
|
20868
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS, AggregatorBackedChannel;
|
|
20869
|
+
var init_aggregator_backed_channel = __esm({
|
|
20870
|
+
"src/principal-policy/channels/aggregator-backed-channel.ts"() {
|
|
20871
|
+
DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
20872
|
+
AggregatorBackedChannel = class {
|
|
20873
|
+
underlying;
|
|
20874
|
+
aggregator;
|
|
20875
|
+
resolveRedirect;
|
|
20876
|
+
replaceModeTimeoutMs;
|
|
20877
|
+
now;
|
|
20878
|
+
constructor(opts) {
|
|
20879
|
+
this.underlying = opts.underlying;
|
|
20880
|
+
this.aggregator = opts.aggregator;
|
|
20881
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
20882
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
20883
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
20884
|
+
}
|
|
20885
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
20886
|
+
getUnderlying() {
|
|
20887
|
+
return this.underlying;
|
|
20888
|
+
}
|
|
20889
|
+
async requestApproval(request) {
|
|
20890
|
+
const cfg = this.resolveRedirect(request);
|
|
20891
|
+
if (!cfg.enabled) {
|
|
20892
|
+
return this.underlying.requestApproval(request);
|
|
20893
|
+
}
|
|
20894
|
+
if (cfg.mode === "replace") {
|
|
20895
|
+
return this.awaitAggregatorDecision(request);
|
|
20896
|
+
}
|
|
20897
|
+
return this.notifyMode(request);
|
|
20898
|
+
}
|
|
20899
|
+
/**
|
|
20900
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
20901
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
20902
|
+
* between list and subscribe). Match incoming events to this request by
|
|
20903
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
20904
|
+
*/
|
|
20905
|
+
async awaitAggregatorDecision(request) {
|
|
20906
|
+
const auditId = auditEntryIdFor(request);
|
|
20907
|
+
return new Promise((resolveOuter) => {
|
|
20908
|
+
let settled = false;
|
|
20909
|
+
let unsubscribe = null;
|
|
20910
|
+
let timeoutHandle = null;
|
|
20911
|
+
const settle = (response) => {
|
|
20912
|
+
if (settled) return;
|
|
20913
|
+
settled = true;
|
|
20914
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
20915
|
+
if (unsubscribe) {
|
|
20916
|
+
try {
|
|
20917
|
+
unsubscribe();
|
|
20918
|
+
} catch {
|
|
20919
|
+
}
|
|
20920
|
+
}
|
|
20921
|
+
resolveOuter(response);
|
|
20922
|
+
};
|
|
20923
|
+
const onEvent = (emit) => {
|
|
20924
|
+
if (emit.type !== "resolved") return;
|
|
20925
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
20926
|
+
const mapped = statusToDecision(emit.entry);
|
|
20927
|
+
if (!mapped) return;
|
|
20928
|
+
settle({
|
|
20929
|
+
decision: mapped.decision,
|
|
20930
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
20931
|
+
decided_by: mapped.decided_by
|
|
20932
|
+
});
|
|
20933
|
+
};
|
|
20934
|
+
try {
|
|
20935
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
20936
|
+
} catch (err) {
|
|
20937
|
+
settle({
|
|
20938
|
+
decision: "deny",
|
|
20939
|
+
decided_at: this.now().toISOString(),
|
|
20940
|
+
decided_by: "channel_failure"
|
|
20941
|
+
});
|
|
20942
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
20943
|
+
}
|
|
20944
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
20945
|
+
for (const entry of entries) {
|
|
20946
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
20947
|
+
const mapped = statusToDecision(entry);
|
|
20948
|
+
if (!mapped) return;
|
|
20949
|
+
settle({
|
|
20950
|
+
decision: mapped.decision,
|
|
20951
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
20952
|
+
decided_by: mapped.decided_by
|
|
20953
|
+
});
|
|
20954
|
+
return;
|
|
20955
|
+
}
|
|
20956
|
+
}).catch(() => {
|
|
20957
|
+
});
|
|
20958
|
+
timeoutHandle = setTimeout(() => {
|
|
20959
|
+
settle({
|
|
20960
|
+
decision: "deny",
|
|
20961
|
+
decided_at: this.now().toISOString(),
|
|
20962
|
+
decided_by: "timeout"
|
|
20963
|
+
});
|
|
20964
|
+
}, this.replaceModeTimeoutMs);
|
|
20965
|
+
});
|
|
20966
|
+
}
|
|
20967
|
+
/**
|
|
20968
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
20969
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
20970
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
20971
|
+
* downstream audit logging is unchanged.
|
|
20972
|
+
*
|
|
20973
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
20974
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
20975
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
20976
|
+
*/
|
|
20977
|
+
async notifyMode(request) {
|
|
20978
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
20979
|
+
let underlyingPromise;
|
|
20980
|
+
try {
|
|
20981
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
20982
|
+
} catch (err) {
|
|
20983
|
+
const response = await aggregatorPromise;
|
|
20984
|
+
return response;
|
|
20985
|
+
}
|
|
20986
|
+
return Promise.race([
|
|
20987
|
+
aggregatorPromise,
|
|
20988
|
+
underlyingPromise.catch(
|
|
20989
|
+
() => new Promise(() => {
|
|
20990
|
+
})
|
|
20991
|
+
)
|
|
20992
|
+
]);
|
|
20993
|
+
}
|
|
20994
|
+
};
|
|
20995
|
+
}
|
|
20996
|
+
});
|
|
20997
|
+
|
|
20998
|
+
// src/principal-policy/aggregator-store.ts
|
|
20999
|
+
function payloadKey(aggregatorId) {
|
|
21000
|
+
return `${AGGREGATOR_PAYLOAD_KEY_PREFIX}${aggregatorId}`;
|
|
21001
|
+
}
|
|
21002
|
+
function stripKeyPrefix(key) {
|
|
21003
|
+
if (!key.startsWith(AGGREGATOR_PAYLOAD_KEY_PREFIX)) return null;
|
|
21004
|
+
return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
|
|
21005
|
+
}
|
|
21006
|
+
var AGGREGATOR_PAYLOAD_NAMESPACE, AGGREGATOR_PAYLOAD_KEY_PREFIX, HKDF_INFO, DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS, MAX_BUNDLE_BYTES2, AggregatorPayloadStore;
|
|
21007
|
+
var init_aggregator_store = __esm({
|
|
21008
|
+
"src/principal-policy/aggregator-store.ts"() {
|
|
21009
|
+
init_encryption();
|
|
21010
|
+
init_key_derivation();
|
|
21011
|
+
init_encoding();
|
|
21012
|
+
AGGREGATOR_PAYLOAD_NAMESPACE = "_approval_aggregator_payloads";
|
|
21013
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX = "payload.";
|
|
21014
|
+
HKDF_INFO = "l2-approval-aggregator-payload-v1";
|
|
21015
|
+
DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS = 30;
|
|
21016
|
+
MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
21017
|
+
AggregatorPayloadStore = class {
|
|
21018
|
+
storage;
|
|
21019
|
+
encryptionKey;
|
|
21020
|
+
fortressId;
|
|
21021
|
+
retentionDays;
|
|
21022
|
+
constructor(opts) {
|
|
21023
|
+
this.storage = opts.storage;
|
|
21024
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO);
|
|
21025
|
+
this.fortressId = opts.fortressId;
|
|
21026
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS;
|
|
21027
|
+
}
|
|
21028
|
+
/**
|
|
21029
|
+
* Persist `payload` under the given aggregator_id. Idempotent; calling
|
|
21030
|
+
* twice with the same id rewrites the bundle (retention_until is
|
|
21031
|
+
* recomputed). Returns the bundle's retention_until ISO-8601 timestamp
|
|
21032
|
+
* so callers can log it.
|
|
21033
|
+
*/
|
|
21034
|
+
async savePayload(aggregatorId, payload) {
|
|
21035
|
+
const now = /* @__PURE__ */ new Date();
|
|
21036
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
21037
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
21038
|
+
const bundle = {
|
|
21039
|
+
version: 1,
|
|
21040
|
+
aggregator_id: aggregatorId,
|
|
21041
|
+
fortress_id: this.fortressId,
|
|
21042
|
+
created_at: now.toISOString(),
|
|
21043
|
+
retention_until: retentionUntil.toISOString(),
|
|
21044
|
+
payload
|
|
21045
|
+
};
|
|
21046
|
+
const aad = stringToBytes(aggregatorId);
|
|
21047
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
21048
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
21049
|
+
await this.storage.write(
|
|
21050
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21051
|
+
payloadKey(aggregatorId),
|
|
21052
|
+
stringToBytes(JSON.stringify(envelope))
|
|
21053
|
+
);
|
|
21054
|
+
return bundle.retention_until;
|
|
21055
|
+
}
|
|
21056
|
+
/**
|
|
21057
|
+
* Read the persisted payload for the aggregator_id. Returns null if no
|
|
21058
|
+
* bundle exists, the bundle is corrupted, or AAD binding fails.
|
|
21059
|
+
*/
|
|
21060
|
+
async loadPayload(aggregatorId) {
|
|
21061
|
+
const key = payloadKey(aggregatorId);
|
|
21062
|
+
let raw;
|
|
21063
|
+
try {
|
|
21064
|
+
raw = await this.storage.read(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
21065
|
+
} catch {
|
|
21066
|
+
return null;
|
|
21067
|
+
}
|
|
21068
|
+
if (!raw) return null;
|
|
21069
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
21070
|
+
try {
|
|
21071
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21072
|
+
const aad = stringToBytes(aggregatorId);
|
|
21073
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21074
|
+
const parsed = JSON.parse(
|
|
21075
|
+
bytesToString(plaintext)
|
|
21076
|
+
);
|
|
21077
|
+
if (parsed.version !== 1) return null;
|
|
21078
|
+
if (parsed.aggregator_id !== aggregatorId) return null;
|
|
21079
|
+
return parsed.payload;
|
|
21080
|
+
} catch {
|
|
21081
|
+
return null;
|
|
21082
|
+
}
|
|
21083
|
+
}
|
|
21084
|
+
/**
|
|
21085
|
+
* Delete the persisted payload. Returns true when a bundle was removed,
|
|
21086
|
+
* false when none existed.
|
|
21087
|
+
*/
|
|
21088
|
+
async deletePayload(aggregatorId) {
|
|
21089
|
+
const key = payloadKey(aggregatorId);
|
|
21090
|
+
const existed = await this.storage.exists(
|
|
21091
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21092
|
+
key
|
|
21093
|
+
);
|
|
21094
|
+
if (!existed) return false;
|
|
21095
|
+
try {
|
|
21096
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
21097
|
+
} catch {
|
|
21098
|
+
return false;
|
|
21099
|
+
}
|
|
21100
|
+
return true;
|
|
21101
|
+
}
|
|
21102
|
+
/**
|
|
21103
|
+
* Drop expired payload bundles. Returns the count of bundles pruned.
|
|
21104
|
+
* Caller wires this into the cocoon-unlock initialization path.
|
|
21105
|
+
*/
|
|
21106
|
+
async pruneExpired(now) {
|
|
21107
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
21108
|
+
const entries = await this.storage.list(
|
|
21109
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21110
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX
|
|
21111
|
+
);
|
|
21112
|
+
let pruned = 0;
|
|
21113
|
+
for (const meta of entries) {
|
|
21114
|
+
const aggregatorId = stripKeyPrefix(meta.key);
|
|
21115
|
+
if (aggregatorId === null) continue;
|
|
21116
|
+
const raw = await this.storage.read(
|
|
21117
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21118
|
+
meta.key
|
|
21119
|
+
);
|
|
21120
|
+
if (!raw) continue;
|
|
21121
|
+
try {
|
|
21122
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21123
|
+
const aad = stringToBytes(aggregatorId);
|
|
21124
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21125
|
+
const parsed = JSON.parse(
|
|
21126
|
+
bytesToString(plaintext)
|
|
21127
|
+
);
|
|
21128
|
+
if (parsed.retention_until <= cutoff) {
|
|
21129
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
|
|
21130
|
+
pruned += 1;
|
|
21131
|
+
}
|
|
21132
|
+
} catch {
|
|
21133
|
+
}
|
|
21134
|
+
}
|
|
21135
|
+
return { pruned };
|
|
21136
|
+
}
|
|
21137
|
+
};
|
|
21138
|
+
}
|
|
21139
|
+
});
|
|
21140
|
+
|
|
20573
21141
|
// src/principal-policy/tools.ts
|
|
20574
21142
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
20575
21143
|
return [
|
|
@@ -21473,6 +22041,76 @@ var init_attestation = __esm({
|
|
|
21473
22041
|
}
|
|
21474
22042
|
});
|
|
21475
22043
|
|
|
22044
|
+
// src/handshake/audit.ts
|
|
22045
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
22046
|
+
auditLog.append(
|
|
22047
|
+
"l4",
|
|
22048
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
22049
|
+
ctx.identity_id,
|
|
22050
|
+
detailsFromContext(ctx),
|
|
22051
|
+
"success"
|
|
22052
|
+
);
|
|
22053
|
+
}
|
|
22054
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
22055
|
+
const details = detailsFromContext(ctx);
|
|
22056
|
+
if (ctx.trust_tier !== void 0) {
|
|
22057
|
+
details.trust_tier = ctx.trust_tier;
|
|
22058
|
+
}
|
|
22059
|
+
auditLog.append(
|
|
22060
|
+
"l4",
|
|
22061
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
22062
|
+
ctx.identity_id,
|
|
22063
|
+
details,
|
|
22064
|
+
"success"
|
|
22065
|
+
);
|
|
22066
|
+
}
|
|
22067
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
22068
|
+
const details = detailsFromContext(ctx);
|
|
22069
|
+
details.reason = ctx.reason;
|
|
22070
|
+
if (ctx.error !== void 0) {
|
|
22071
|
+
details.error = ctx.error;
|
|
22072
|
+
}
|
|
22073
|
+
auditLog.append(
|
|
22074
|
+
"l4",
|
|
22075
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
22076
|
+
ctx.identity_id,
|
|
22077
|
+
details,
|
|
22078
|
+
"failure"
|
|
22079
|
+
);
|
|
22080
|
+
}
|
|
22081
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
22082
|
+
const details = detailsFromContext(ctx);
|
|
22083
|
+
details.reason = ctx.reason;
|
|
22084
|
+
auditLog.append(
|
|
22085
|
+
"l4",
|
|
22086
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
22087
|
+
ctx.identity_id,
|
|
22088
|
+
details,
|
|
22089
|
+
"failure"
|
|
22090
|
+
);
|
|
22091
|
+
}
|
|
22092
|
+
function detailsFromContext(ctx) {
|
|
22093
|
+
const details = {
|
|
22094
|
+
session_id: ctx.session_id,
|
|
22095
|
+
role: ctx.role
|
|
22096
|
+
};
|
|
22097
|
+
if (ctx.counterparty_id !== void 0) {
|
|
22098
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
22099
|
+
}
|
|
22100
|
+
return details;
|
|
22101
|
+
}
|
|
22102
|
+
var HANDSHAKE_LIFECYCLE_OPS;
|
|
22103
|
+
var init_audit = __esm({
|
|
22104
|
+
"src/handshake/audit.ts"() {
|
|
22105
|
+
HANDSHAKE_LIFECYCLE_OPS = {
|
|
22106
|
+
INITIATED: "handshake_initiated",
|
|
22107
|
+
COMPLETED: "handshake_completed",
|
|
22108
|
+
FAILED: "handshake_failed",
|
|
22109
|
+
ABORTED: "handshake_aborted"
|
|
22110
|
+
};
|
|
22111
|
+
}
|
|
22112
|
+
});
|
|
22113
|
+
|
|
21476
22114
|
// src/handshake/tools.ts
|
|
21477
22115
|
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
21478
22116
|
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
@@ -21506,6 +22144,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21506
22144
|
const { challenge, session } = initiateHandshake(shr);
|
|
21507
22145
|
sessions.set(session.session_id, session);
|
|
21508
22146
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
22147
|
+
auditHandshakeInitiated(auditLog, {
|
|
22148
|
+
session_id: session.session_id,
|
|
22149
|
+
role: "initiator",
|
|
22150
|
+
identity_id: shr.body.instance_id
|
|
22151
|
+
});
|
|
21509
22152
|
return toolResult({
|
|
21510
22153
|
session_id: session.session_id,
|
|
21511
22154
|
challenge,
|
|
@@ -21545,10 +22188,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21545
22188
|
);
|
|
21546
22189
|
if ("error" in result) {
|
|
21547
22190
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
22191
|
+
auditHandshakeFailed(auditLog, {
|
|
22192
|
+
session_id: "unknown",
|
|
22193
|
+
role: "responder",
|
|
22194
|
+
identity_id: shr.body.instance_id,
|
|
22195
|
+
reason: classifyRespondFailure(result.error),
|
|
22196
|
+
error: result.error
|
|
22197
|
+
});
|
|
21548
22198
|
return toolResult({ error: result.error });
|
|
21549
22199
|
}
|
|
21550
22200
|
sessions.set(result.session.session_id, result.session);
|
|
21551
22201
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
22202
|
+
auditHandshakeInitiated(auditLog, {
|
|
22203
|
+
session_id: result.session.session_id,
|
|
22204
|
+
role: "responder",
|
|
22205
|
+
identity_id: shr.body.instance_id,
|
|
22206
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
22207
|
+
});
|
|
21552
22208
|
let autoPublishResult;
|
|
21553
22209
|
if (autoPublishHandshakes) {
|
|
21554
22210
|
autoPublishResult = { attempted: true };
|
|
@@ -21656,9 +22312,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21656
22312
|
const response = args.response;
|
|
21657
22313
|
const session = sessions.get(sessionId);
|
|
21658
22314
|
if (!session) {
|
|
22315
|
+
auditHandshakeFailed(auditLog, {
|
|
22316
|
+
session_id: sessionId,
|
|
22317
|
+
role: "initiator",
|
|
22318
|
+
identity_id: "unknown",
|
|
22319
|
+
reason: "session_unknown",
|
|
22320
|
+
error: `No handshake session found: ${sessionId}`
|
|
22321
|
+
});
|
|
21659
22322
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21660
22323
|
}
|
|
21661
22324
|
if (session.state !== "initiated") {
|
|
22325
|
+
auditHandshakeFailed(auditLog, {
|
|
22326
|
+
session_id: sessionId,
|
|
22327
|
+
role: "initiator",
|
|
22328
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22329
|
+
reason: "session_state_mismatch",
|
|
22330
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
22331
|
+
});
|
|
21662
22332
|
return toolResult({
|
|
21663
22333
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21664
22334
|
});
|
|
@@ -21672,6 +22342,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21672
22342
|
if ("error" in result) {
|
|
21673
22343
|
session.state = "failed";
|
|
21674
22344
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
22345
|
+
auditHandshakeFailed(auditLog, {
|
|
22346
|
+
session_id: sessionId,
|
|
22347
|
+
role: "initiator",
|
|
22348
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22349
|
+
reason: classifyCompleteFailure(result.error),
|
|
22350
|
+
error: result.error
|
|
22351
|
+
});
|
|
21675
22352
|
return toolResult({ error: result.error });
|
|
21676
22353
|
}
|
|
21677
22354
|
session.state = "completed";
|
|
@@ -21680,6 +22357,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21680
22357
|
session.result = result.result;
|
|
21681
22358
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
21682
22359
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
22360
|
+
auditHandshakeCompleted(auditLog, {
|
|
22361
|
+
session_id: sessionId,
|
|
22362
|
+
role: "initiator",
|
|
22363
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22364
|
+
counterparty_id: result.result.counterparty_id,
|
|
22365
|
+
trust_tier: result.result.trust_tier
|
|
22366
|
+
});
|
|
21683
22367
|
return toolResult({
|
|
21684
22368
|
completion: result.completion,
|
|
21685
22369
|
result: result.result,
|
|
@@ -21727,6 +22411,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21727
22411
|
void 0,
|
|
21728
22412
|
result.verified ? "success" : "failure"
|
|
21729
22413
|
);
|
|
22414
|
+
if (result.verified) {
|
|
22415
|
+
auditHandshakeCompleted(auditLog, {
|
|
22416
|
+
session_id: session.session_id,
|
|
22417
|
+
role: "responder",
|
|
22418
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22419
|
+
counterparty_id: result.counterparty_id,
|
|
22420
|
+
trust_tier: result.trust_tier
|
|
22421
|
+
});
|
|
22422
|
+
} else {
|
|
22423
|
+
auditHandshakeFailed(auditLog, {
|
|
22424
|
+
session_id: session.session_id,
|
|
22425
|
+
role: "responder",
|
|
22426
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22427
|
+
counterparty_id: result.counterparty_id,
|
|
22428
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
22429
|
+
error: result.errors.join("; ")
|
|
22430
|
+
});
|
|
22431
|
+
}
|
|
21730
22432
|
return toolResult({ result });
|
|
21731
22433
|
}
|
|
21732
22434
|
return toolResult({
|
|
@@ -21834,10 +22536,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21834
22536
|
_content_trust: "external"
|
|
21835
22537
|
});
|
|
21836
22538
|
}
|
|
22539
|
+
},
|
|
22540
|
+
{
|
|
22541
|
+
name: "handshake_abort",
|
|
22542
|
+
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.",
|
|
22543
|
+
inputSchema: {
|
|
22544
|
+
type: "object",
|
|
22545
|
+
properties: {
|
|
22546
|
+
session_id: {
|
|
22547
|
+
type: "string",
|
|
22548
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
22549
|
+
},
|
|
22550
|
+
reason: {
|
|
22551
|
+
type: "string",
|
|
22552
|
+
enum: [
|
|
22553
|
+
"operator_cancelled",
|
|
22554
|
+
"session_timeout",
|
|
22555
|
+
"transport_dropped",
|
|
22556
|
+
"shutdown",
|
|
22557
|
+
"other"
|
|
22558
|
+
],
|
|
22559
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
22560
|
+
}
|
|
22561
|
+
},
|
|
22562
|
+
required: ["session_id"]
|
|
22563
|
+
},
|
|
22564
|
+
handler: async (args) => {
|
|
22565
|
+
const sessionId = args.session_id;
|
|
22566
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
22567
|
+
const session = sessions.get(sessionId);
|
|
22568
|
+
if (!session) {
|
|
22569
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
22570
|
+
}
|
|
22571
|
+
if (session.state === "completed") {
|
|
22572
|
+
return toolResult({
|
|
22573
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
22574
|
+
});
|
|
22575
|
+
}
|
|
22576
|
+
sessions.delete(sessionId);
|
|
22577
|
+
auditHandshakeAborted(auditLog, {
|
|
22578
|
+
session_id: sessionId,
|
|
22579
|
+
role: session.role,
|
|
22580
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22581
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
22582
|
+
reason
|
|
22583
|
+
});
|
|
22584
|
+
return toolResult({
|
|
22585
|
+
aborted: true,
|
|
22586
|
+
session_id: sessionId,
|
|
22587
|
+
reason
|
|
22588
|
+
});
|
|
22589
|
+
}
|
|
21837
22590
|
}
|
|
21838
22591
|
];
|
|
21839
22592
|
return { tools, handshakeResults };
|
|
21840
22593
|
}
|
|
22594
|
+
function classifyRespondFailure(error) {
|
|
22595
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22596
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
22597
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22598
|
+
return "other";
|
|
22599
|
+
}
|
|
22600
|
+
function classifyCompleteFailure(error) {
|
|
22601
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22602
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
22603
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
22604
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22605
|
+
return "other";
|
|
22606
|
+
}
|
|
21841
22607
|
var init_tools6 = __esm({
|
|
21842
22608
|
"src/handshake/tools.ts"() {
|
|
21843
22609
|
init_router();
|
|
@@ -21847,6 +22613,7 @@ var init_tools6 = __esm({
|
|
|
21847
22613
|
init_encoding();
|
|
21848
22614
|
init_protocol();
|
|
21849
22615
|
init_attestation();
|
|
22616
|
+
init_audit();
|
|
21850
22617
|
init_verifier();
|
|
21851
22618
|
}
|
|
21852
22619
|
});
|
|
@@ -32982,7 +33749,21 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32982
33749
|
* successful thread removal. Body carries thread_id + turn_count of
|
|
32983
33750
|
* the deleted bundle.
|
|
32984
33751
|
*/
|
|
32985
|
-
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
33752
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
33753
|
+
/**
|
|
33754
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
33755
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
33756
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
33757
|
+
* carries thread_id + a stable failure_reason enum.
|
|
33758
|
+
*/
|
|
33759
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed",
|
|
33760
|
+
/**
|
|
33761
|
+
* Concierge dynamic-context fetcher failed (WP-V1.3-9 Tau-3). Emitted
|
|
33762
|
+
* when a category fetcher throws while assembling the dynamic context
|
|
33763
|
+
* fold. The concierge omits that category and continues; the user-
|
|
33764
|
+
* facing query is never broken. Body carries category + failure_reason.
|
|
33765
|
+
*/
|
|
33766
|
+
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
|
|
32986
33767
|
};
|
|
32987
33768
|
}
|
|
32988
33769
|
});
|
|
@@ -32995,20 +33776,312 @@ var init_operator_chat_types = __esm({
|
|
|
32995
33776
|
CONCIERGE_THREAD_KEY = "_fortress";
|
|
32996
33777
|
}
|
|
32997
33778
|
});
|
|
33779
|
+
|
|
33780
|
+
// src/chat/concierge-context-router.ts
|
|
33781
|
+
function phrasePattern(phrase) {
|
|
33782
|
+
const escaped = phrase.toLowerCase().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
33783
|
+
return { source: `\\b${escaped}\\b`, phrase };
|
|
33784
|
+
}
|
|
33785
|
+
function extractAgentNameHint(query) {
|
|
33786
|
+
const agentPattern = /\bagent\s+["']?([A-Za-z][\w-]{0,40})["']?/i;
|
|
33787
|
+
const m = query.match(agentPattern);
|
|
33788
|
+
if (m && m[1]) return m[1];
|
|
33789
|
+
const quoted = query.match(/["']([A-Za-z][\w-]{0,40})["']/);
|
|
33790
|
+
if (quoted && quoted[1]) return quoted[1];
|
|
33791
|
+
return null;
|
|
33792
|
+
}
|
|
33793
|
+
function isTrivialQuery(query) {
|
|
33794
|
+
const norm = query.trim().toLowerCase();
|
|
33795
|
+
if (norm.length === 0) return true;
|
|
33796
|
+
if (norm.length < 8) return true;
|
|
33797
|
+
return TRIVIAL_GREETINGS.has(norm);
|
|
33798
|
+
}
|
|
33799
|
+
function classifyQuery(query) {
|
|
33800
|
+
const normalized = query.toLowerCase();
|
|
33801
|
+
const matches = [];
|
|
33802
|
+
for (const spec of CATEGORY_KEYWORDS) {
|
|
33803
|
+
const matchedPhrases = [];
|
|
33804
|
+
for (const pattern of spec.patterns) {
|
|
33805
|
+
if (matchedPhrases.includes(pattern.phrase)) continue;
|
|
33806
|
+
const re = new RegExp(pattern.source, "i");
|
|
33807
|
+
if (re.test(normalized)) {
|
|
33808
|
+
matchedPhrases.push(pattern.phrase);
|
|
33809
|
+
}
|
|
33810
|
+
}
|
|
33811
|
+
if (matchedPhrases.length === 0) continue;
|
|
33812
|
+
const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
|
|
33813
|
+
matches.push({
|
|
33814
|
+
category: spec.category,
|
|
33815
|
+
confidence,
|
|
33816
|
+
matched_keywords: matchedPhrases,
|
|
33817
|
+
agent_name_hint: spec.category === "agent_state" || spec.category === "agent_activity" ? extractAgentNameHint(query) : null
|
|
33818
|
+
});
|
|
33819
|
+
}
|
|
33820
|
+
matches.sort((a, b) => {
|
|
33821
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
33822
|
+
return CONTEXT_CATEGORIES.indexOf(a.category) - CONTEXT_CATEGORIES.indexOf(b.category);
|
|
33823
|
+
});
|
|
33824
|
+
return matches;
|
|
33825
|
+
}
|
|
33826
|
+
function approxTokenLen(text) {
|
|
33827
|
+
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
33828
|
+
}
|
|
33829
|
+
async function runFetcher(match, fetchers) {
|
|
33830
|
+
switch (match.category) {
|
|
33831
|
+
case "templates":
|
|
33832
|
+
return fetchers.templates();
|
|
33833
|
+
case "agent_state":
|
|
33834
|
+
return fetchers.agent_state(match.agent_name_hint);
|
|
33835
|
+
case "agent_activity":
|
|
33836
|
+
return fetchers.agent_activity(match.agent_name_hint);
|
|
33837
|
+
case "audit_log":
|
|
33838
|
+
return fetchers.audit_log();
|
|
33839
|
+
case "sentinel_findings":
|
|
33840
|
+
return fetchers.sentinel_findings();
|
|
33841
|
+
case "anomaly_alerts":
|
|
33842
|
+
return fetchers.anomaly_alerts();
|
|
33843
|
+
case "recent_receipts":
|
|
33844
|
+
return fetchers.recent_receipts();
|
|
33845
|
+
case "verascore_deltas":
|
|
33846
|
+
return fetchers.verascore_deltas();
|
|
33847
|
+
}
|
|
33848
|
+
}
|
|
33849
|
+
function trivialMatch(category) {
|
|
33850
|
+
return {
|
|
33851
|
+
category,
|
|
33852
|
+
confidence: 0.5,
|
|
33853
|
+
matched_keywords: ["llm-assist"],
|
|
33854
|
+
agent_name_hint: null
|
|
33855
|
+
};
|
|
33856
|
+
}
|
|
33857
|
+
async function foldContext(query, fetchers, opts) {
|
|
33858
|
+
const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
|
|
33859
|
+
let matches = classifyQuery(query);
|
|
33860
|
+
if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
|
|
33861
|
+
try {
|
|
33862
|
+
const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
|
|
33863
|
+
if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
|
|
33864
|
+
matches = [trivialMatch(picked)];
|
|
33865
|
+
}
|
|
33866
|
+
} catch {
|
|
33867
|
+
}
|
|
33868
|
+
}
|
|
33869
|
+
if (matches.length === 0) {
|
|
33870
|
+
return { section: "", categoriesIncluded: [] };
|
|
33871
|
+
}
|
|
33872
|
+
const attempts = [];
|
|
33873
|
+
for (const match of matches) {
|
|
33874
|
+
try {
|
|
33875
|
+
const text = await runFetcher(match, fetchers);
|
|
33876
|
+
const trimmed = text.trim();
|
|
33877
|
+
if (trimmed.length > 0) {
|
|
33878
|
+
attempts.push({ category: match.category, text: trimmed });
|
|
33879
|
+
}
|
|
33880
|
+
} catch (err) {
|
|
33881
|
+
opts?.onFetcherFailure?.(match.category, err);
|
|
33882
|
+
}
|
|
33883
|
+
}
|
|
33884
|
+
if (attempts.length === 0) {
|
|
33885
|
+
return { section: "", categoriesIncluded: [] };
|
|
33886
|
+
}
|
|
33887
|
+
const headerTokens = approxTokenLen(`${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
33888
|
+
`);
|
|
33889
|
+
const sepTokens = approxTokenLen("\n\n");
|
|
33890
|
+
let runningTokens = headerTokens;
|
|
33891
|
+
const kept = [];
|
|
33892
|
+
for (const attempt of attempts) {
|
|
33893
|
+
const block = `### ${CATEGORY_LABELS[attempt.category]}
|
|
33894
|
+
${attempt.text}`;
|
|
33895
|
+
const tokens = approxTokenLen(block) + (kept.length > 0 ? sepTokens : 0);
|
|
33896
|
+
if (kept.length === 0) {
|
|
33897
|
+
kept.push(attempt);
|
|
33898
|
+
runningTokens += tokens;
|
|
33899
|
+
continue;
|
|
33900
|
+
}
|
|
33901
|
+
if (runningTokens + tokens > budget) break;
|
|
33902
|
+
kept.push(attempt);
|
|
33903
|
+
runningTokens += tokens;
|
|
33904
|
+
}
|
|
33905
|
+
const blocks = kept.map(
|
|
33906
|
+
(k) => `### ${CATEGORY_LABELS[k.category]}
|
|
33907
|
+
${k.text}`
|
|
33908
|
+
);
|
|
33909
|
+
const section = `${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
33910
|
+
${blocks.join("\n\n")}`;
|
|
33911
|
+
return {
|
|
33912
|
+
section,
|
|
33913
|
+
categoriesIncluded: kept.map((k) => k.category)
|
|
33914
|
+
};
|
|
33915
|
+
}
|
|
33916
|
+
var APPROX_CHARS_PER_TOKEN, DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET, DYNAMIC_CONTEXT_SECTION_HEADER, CONTEXT_CATEGORIES, CATEGORY_KEYWORDS, TRIVIAL_GREETINGS, CATEGORY_LABELS;
|
|
33917
|
+
var init_concierge_context_router = __esm({
|
|
33918
|
+
"src/chat/concierge-context-router.ts"() {
|
|
33919
|
+
APPROX_CHARS_PER_TOKEN = 4;
|
|
33920
|
+
DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET = 2e3;
|
|
33921
|
+
DYNAMIC_CONTEXT_SECTION_HEADER = "## Live fortress context";
|
|
33922
|
+
CONTEXT_CATEGORIES = [
|
|
33923
|
+
"templates",
|
|
33924
|
+
"agent_state",
|
|
33925
|
+
"agent_activity",
|
|
33926
|
+
"audit_log",
|
|
33927
|
+
"sentinel_findings",
|
|
33928
|
+
"anomaly_alerts",
|
|
33929
|
+
"recent_receipts",
|
|
33930
|
+
"verascore_deltas"
|
|
33931
|
+
];
|
|
33932
|
+
CATEGORY_KEYWORDS = [
|
|
33933
|
+
{
|
|
33934
|
+
category: "templates",
|
|
33935
|
+
patterns: [
|
|
33936
|
+
"templates",
|
|
33937
|
+
"template",
|
|
33938
|
+
"channel templates",
|
|
33939
|
+
"channel template",
|
|
33940
|
+
"list templates",
|
|
33941
|
+
"available templates",
|
|
33942
|
+
"what templates"
|
|
33943
|
+
].map(phrasePattern)
|
|
33944
|
+
},
|
|
33945
|
+
{
|
|
33946
|
+
category: "agent_state",
|
|
33947
|
+
patterns: [
|
|
33948
|
+
"state",
|
|
33949
|
+
"status",
|
|
33950
|
+
"agent state",
|
|
33951
|
+
"agent status",
|
|
33952
|
+
"status of agent",
|
|
33953
|
+
"status of agents",
|
|
33954
|
+
"state of",
|
|
33955
|
+
"doing"
|
|
33956
|
+
].map(phrasePattern)
|
|
33957
|
+
},
|
|
33958
|
+
{
|
|
33959
|
+
category: "agent_activity",
|
|
33960
|
+
patterns: [
|
|
33961
|
+
"activity",
|
|
33962
|
+
"agent activity",
|
|
33963
|
+
"what did",
|
|
33964
|
+
"recent activity"
|
|
33965
|
+
].map(phrasePattern)
|
|
33966
|
+
},
|
|
33967
|
+
{
|
|
33968
|
+
category: "audit_log",
|
|
33969
|
+
patterns: [
|
|
33970
|
+
"audit log",
|
|
33971
|
+
"audit",
|
|
33972
|
+
"log entry",
|
|
33973
|
+
"log entries",
|
|
33974
|
+
"what happened",
|
|
33975
|
+
"show me events",
|
|
33976
|
+
"event class"
|
|
33977
|
+
].map(phrasePattern)
|
|
33978
|
+
},
|
|
33979
|
+
{
|
|
33980
|
+
category: "sentinel_findings",
|
|
33981
|
+
patterns: [
|
|
33982
|
+
"sentinel",
|
|
33983
|
+
"sentinels",
|
|
33984
|
+
"warning",
|
|
33985
|
+
"warnings",
|
|
33986
|
+
"alert",
|
|
33987
|
+
"alerts",
|
|
33988
|
+
"whats wrong",
|
|
33989
|
+
"what's wrong",
|
|
33990
|
+
"findings"
|
|
33991
|
+
].map(phrasePattern)
|
|
33992
|
+
},
|
|
33993
|
+
{
|
|
33994
|
+
category: "anomaly_alerts",
|
|
33995
|
+
patterns: [
|
|
33996
|
+
"anomaly",
|
|
33997
|
+
"anomalies",
|
|
33998
|
+
"spike",
|
|
33999
|
+
"unusual",
|
|
34000
|
+
"outlier"
|
|
34001
|
+
].map(phrasePattern)
|
|
34002
|
+
},
|
|
34003
|
+
{
|
|
34004
|
+
category: "recent_receipts",
|
|
34005
|
+
patterns: [
|
|
34006
|
+
"receipt",
|
|
34007
|
+
"receipts",
|
|
34008
|
+
"concordia",
|
|
34009
|
+
"commitment",
|
|
34010
|
+
"commitments",
|
|
34011
|
+
"chain",
|
|
34012
|
+
"chains"
|
|
34013
|
+
].map(phrasePattern)
|
|
34014
|
+
},
|
|
34015
|
+
{
|
|
34016
|
+
category: "verascore_deltas",
|
|
34017
|
+
patterns: [
|
|
34018
|
+
"verascore",
|
|
34019
|
+
"vera score",
|
|
34020
|
+
"trust score",
|
|
34021
|
+
"reputation"
|
|
34022
|
+
].map(phrasePattern)
|
|
34023
|
+
}
|
|
34024
|
+
];
|
|
34025
|
+
TRIVIAL_GREETINGS = /* @__PURE__ */ new Set([
|
|
34026
|
+
"hi",
|
|
34027
|
+
"hello",
|
|
34028
|
+
"hey",
|
|
34029
|
+
"yo",
|
|
34030
|
+
"ok",
|
|
34031
|
+
"thanks",
|
|
34032
|
+
"thx",
|
|
34033
|
+
"thank you"
|
|
34034
|
+
]);
|
|
34035
|
+
CATEGORY_LABELS = {
|
|
34036
|
+
templates: "Templates",
|
|
34037
|
+
agent_state: "Agent state",
|
|
34038
|
+
agent_activity: "Agent activity",
|
|
34039
|
+
audit_log: "Audit log",
|
|
34040
|
+
sentinel_findings: "Sentinel findings",
|
|
34041
|
+
anomaly_alerts: "Anomaly alerts",
|
|
34042
|
+
recent_receipts: "Recent receipts",
|
|
34043
|
+
verascore_deltas: "Verascore deltas"
|
|
34044
|
+
};
|
|
34045
|
+
}
|
|
34046
|
+
});
|
|
34047
|
+
function approxTokenLen2(text) {
|
|
34048
|
+
return Math.ceil(text.length / 4);
|
|
34049
|
+
}
|
|
32998
34050
|
function makeEventId(prefix) {
|
|
32999
34051
|
return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
33000
34052
|
}
|
|
34053
|
+
function classifyFetcherError(error) {
|
|
34054
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
34055
|
+
if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
|
|
34056
|
+
if (msg.includes("schema") || msg.includes("invalid shape")) {
|
|
34057
|
+
return "schema_mismatch";
|
|
34058
|
+
}
|
|
34059
|
+
if (msg.includes("io") || msg.includes("read") || msg.includes("enoent") || msg.includes("eacces")) {
|
|
34060
|
+
return "io_failed";
|
|
34061
|
+
}
|
|
34062
|
+
return "unknown";
|
|
34063
|
+
}
|
|
34064
|
+
function formatPriorTurnLine(turn) {
|
|
34065
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
34066
|
+
return `${label}: ${turn.content}`;
|
|
34067
|
+
}
|
|
33001
34068
|
function hashOf(input) {
|
|
33002
34069
|
return hashToString(sha256.sha256(stringToBytes(input)));
|
|
33003
34070
|
}
|
|
33004
|
-
var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
34071
|
+
var DEFAULT_CONCIERGE_MAX_TOKENS, DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS, DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS, DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET, DEFAULT_CONCIERGE_SESSION_TTL_MS, DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
33005
34072
|
var init_operator_chat_service = __esm({
|
|
33006
34073
|
"src/chat/operator-chat-service.ts"() {
|
|
33007
34074
|
init_hashing();
|
|
33008
34075
|
init_encoding();
|
|
33009
34076
|
init_operator_chat_audit_events();
|
|
33010
34077
|
init_operator_chat_types();
|
|
34078
|
+
init_concierge_context_router();
|
|
33011
34079
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
34080
|
+
DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
34081
|
+
DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
34082
|
+
DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
34083
|
+
DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
34084
|
+
DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
|
|
33012
34085
|
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
33013
34086
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
33014
34087
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -33045,6 +34118,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33045
34118
|
piiFilter;
|
|
33046
34119
|
conciergeMaxTokens;
|
|
33047
34120
|
memory;
|
|
34121
|
+
historyWindowTurns;
|
|
34122
|
+
historyFreshnessMs;
|
|
34123
|
+
historyTokenBudget;
|
|
34124
|
+
sessionTtlMs;
|
|
34125
|
+
clock;
|
|
34126
|
+
contextFetchers;
|
|
34127
|
+
contextLlmAssist;
|
|
34128
|
+
dynamicContextBudget;
|
|
33048
34129
|
/**
|
|
33049
34130
|
* In-memory thread_id assigned to the active concierge session.
|
|
33050
34131
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -33052,6 +34133,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33052
34133
|
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33053
34134
|
*/
|
|
33054
34135
|
activeMemoryThreadId;
|
|
34136
|
+
/**
|
|
34137
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
34138
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
34139
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
34140
|
+
* allocates a new thread_id even though the prior one is still
|
|
34141
|
+
* readable from the memory store.
|
|
34142
|
+
*/
|
|
34143
|
+
lastInteractionAt;
|
|
33055
34144
|
constructor(deps) {
|
|
33056
34145
|
this.store = deps.store;
|
|
33057
34146
|
this.auditLog = deps.auditLog;
|
|
@@ -33063,6 +34152,18 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33063
34152
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
33064
34153
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33065
34154
|
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
34155
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
34156
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
34157
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
34158
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
34159
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
34160
|
+
if (deps.conciergeContextFetchers) {
|
|
34161
|
+
this.contextFetchers = deps.conciergeContextFetchers;
|
|
34162
|
+
}
|
|
34163
|
+
if (deps.conciergeContextLlmAssist) {
|
|
34164
|
+
this.contextLlmAssist = deps.conciergeContextLlmAssist;
|
|
34165
|
+
}
|
|
34166
|
+
this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
|
|
33066
34167
|
}
|
|
33067
34168
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
33068
34169
|
/**
|
|
@@ -33081,6 +34182,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33081
34182
|
throw new Error("concierge query must not be empty");
|
|
33082
34183
|
}
|
|
33083
34184
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
34185
|
+
const nowMs = this.clock();
|
|
34186
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
34187
|
+
this.activeMemoryThreadId = void 0;
|
|
34188
|
+
}
|
|
33084
34189
|
const operatorMessage = {
|
|
33085
34190
|
message_id: crypto.randomUUID(),
|
|
33086
34191
|
surface: "concierge",
|
|
@@ -33093,6 +34198,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33093
34198
|
CONCIERGE_THREAD_KEY,
|
|
33094
34199
|
operatorMessage
|
|
33095
34200
|
);
|
|
34201
|
+
let priorTurns = [];
|
|
34202
|
+
let memoryReadFailureReason = null;
|
|
34203
|
+
let activeThreadIdForRound;
|
|
34204
|
+
if (this.memory) {
|
|
34205
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
34206
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
34207
|
+
if (result.ok) {
|
|
34208
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
34209
|
+
const fresh = result.turns.filter((t) => {
|
|
34210
|
+
const ts = Date.parse(t.created_at);
|
|
34211
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
34212
|
+
});
|
|
34213
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
34214
|
+
priorTurns = recent;
|
|
34215
|
+
} else {
|
|
34216
|
+
memoryReadFailureReason = result.reason;
|
|
34217
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
34218
|
+
}
|
|
34219
|
+
}
|
|
33096
34220
|
if (this.memory) {
|
|
33097
34221
|
const threadId = this.ensureActiveMemoryThread();
|
|
33098
34222
|
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
@@ -33103,6 +34227,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33103
34227
|
let servedBy = "disabled";
|
|
33104
34228
|
let displayLabel = "Concierge: substrate not configured";
|
|
33105
34229
|
let outcome = "substrate_disabled";
|
|
34230
|
+
let dynamicCategoriesIncluded = [];
|
|
33106
34231
|
if (!this.substrateSelector) {
|
|
33107
34232
|
conciergeBody = "Concierge unavailable. The substrate selector is not configured for this fortress. Pick a substrate in the Policy center to enable concierge replies.";
|
|
33108
34233
|
} else {
|
|
@@ -33114,7 +34239,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33114
34239
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
33115
34240
|
outcome = "substrate_disabled";
|
|
33116
34241
|
} else {
|
|
33117
|
-
const
|
|
34242
|
+
const dynamicResult = await this.runDynamicContextFold(
|
|
34243
|
+
filterResult.filtered
|
|
34244
|
+
);
|
|
34245
|
+
dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
|
|
34246
|
+
const context = await this.assembleConciergeContext(
|
|
34247
|
+
priorTurns,
|
|
34248
|
+
dynamicResult.section
|
|
34249
|
+
);
|
|
33118
34250
|
const response = await this.substrateSelector.invokeSummarize(
|
|
33119
34251
|
"concierge",
|
|
33120
34252
|
{
|
|
@@ -33153,10 +34285,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33153
34285
|
CONCIERGE_THREAD_KEY,
|
|
33154
34286
|
responseMessage
|
|
33155
34287
|
);
|
|
34288
|
+
let assistantTurnId;
|
|
33156
34289
|
if (this.memory) {
|
|
33157
34290
|
const threadId = this.ensureActiveMemoryThread();
|
|
33158
|
-
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() =>
|
|
33159
|
-
|
|
34291
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
34292
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
34293
|
+
}
|
|
34294
|
+
if (this.memory && activeThreadIdForRound) {
|
|
34295
|
+
this.lastInteractionAt = nowMs;
|
|
33160
34296
|
}
|
|
33161
34297
|
const payload = {
|
|
33162
34298
|
version: "1.2",
|
|
@@ -33169,7 +34305,13 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33169
34305
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
33170
34306
|
substrate: servedBy,
|
|
33171
34307
|
latency_ms: latencyMs,
|
|
33172
|
-
outcome
|
|
34308
|
+
outcome,
|
|
34309
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
34310
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
34311
|
+
...this.memory ? {
|
|
34312
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
34313
|
+
} : {},
|
|
34314
|
+
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
|
|
33173
34315
|
};
|
|
33174
34316
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
33175
34317
|
return {
|
|
@@ -33179,6 +34321,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33179
34321
|
outcome
|
|
33180
34322
|
};
|
|
33181
34323
|
}
|
|
34324
|
+
/**
|
|
34325
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
34326
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
34327
|
+
* with `result: "failure"` since the concierge fell back to
|
|
34328
|
+
* single-turn mode for this round-trip.
|
|
34329
|
+
*/
|
|
34330
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
34331
|
+
const payload = {
|
|
34332
|
+
version: "1.2",
|
|
34333
|
+
event_id: makeEventId("conc-memfail"),
|
|
34334
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34335
|
+
identity_id: this.identityId,
|
|
34336
|
+
kind: "operator_concierge_memory_read_failed",
|
|
34337
|
+
surface: "concierge",
|
|
34338
|
+
thread_id: threadId,
|
|
34339
|
+
failure_reason: reason
|
|
34340
|
+
};
|
|
34341
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
34342
|
+
}
|
|
33182
34343
|
/**
|
|
33183
34344
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
33184
34345
|
* an empty array when no thread exists yet.
|
|
@@ -33301,6 +34462,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33301
34462
|
* ## Sanctuary reference
|
|
33302
34463
|
* <static domain reference block>
|
|
33303
34464
|
*
|
|
34465
|
+
* ## Live fortress context ← WP-V1.3-9 Tau-3, when present
|
|
34466
|
+
* ### <Category>
|
|
34467
|
+
* <fetcher payload>
|
|
34468
|
+
*
|
|
34469
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
34470
|
+
* OPERATOR: ...
|
|
34471
|
+
* CONCIERGE: ...
|
|
34472
|
+
*
|
|
33304
34473
|
* ## Recent activity
|
|
33305
34474
|
* <recentActivity output>
|
|
33306
34475
|
*
|
|
@@ -33310,37 +34479,116 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33310
34479
|
* ## Open inbox
|
|
33311
34480
|
* <openInbox output>
|
|
33312
34481
|
* ```
|
|
33313
|
-
|
|
33314
|
-
|
|
34482
|
+
*
|
|
34483
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
34484
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
34485
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
34486
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
34487
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
34488
|
+
* serialization is the canonical path for v1.3.
|
|
34489
|
+
*/
|
|
34490
|
+
async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
|
|
33315
34491
|
const ref = `## Sanctuary reference
|
|
33316
34492
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
34493
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
33317
34494
|
if (!this.contextProviders) {
|
|
33318
|
-
return
|
|
33319
|
-
|
|
33320
|
-
|
|
33321
|
-
|
|
33322
|
-
|
|
33323
|
-
## Wrapped agents
|
|
33324
|
-
(no providers wired)
|
|
33325
|
-
|
|
33326
|
-
## Open inbox
|
|
33327
|
-
(no providers wired)`;
|
|
34495
|
+
return [
|
|
34496
|
+
ref,
|
|
34497
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
34498
|
+
...priorSection ? [priorSection] : [],
|
|
34499
|
+
"## Recent activity\n(no providers wired)",
|
|
34500
|
+
"## Wrapped agents\n(no providers wired)",
|
|
34501
|
+
"## Open inbox\n(no providers wired)"
|
|
34502
|
+
].join("\n\n");
|
|
33328
34503
|
}
|
|
33329
34504
|
const [activity, agents, inbox] = await Promise.all([
|
|
33330
34505
|
this.contextProviders.recentActivity(),
|
|
33331
34506
|
this.contextProviders.agentInventory(),
|
|
33332
34507
|
this.contextProviders.openInbox()
|
|
33333
34508
|
]);
|
|
33334
|
-
return
|
|
33335
|
-
|
|
33336
|
-
|
|
33337
|
-
|
|
33338
|
-
|
|
33339
|
-
|
|
33340
|
-
|
|
33341
|
-
|
|
33342
|
-
|
|
33343
|
-
${inbox}
|
|
34509
|
+
return [
|
|
34510
|
+
ref,
|
|
34511
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
34512
|
+
...priorSection ? [priorSection] : [],
|
|
34513
|
+
`## Recent activity
|
|
34514
|
+
${activity}`,
|
|
34515
|
+
`## Wrapped agents
|
|
34516
|
+
${agents}`,
|
|
34517
|
+
`## Open inbox
|
|
34518
|
+
${inbox}`
|
|
34519
|
+
].join("\n\n");
|
|
34520
|
+
}
|
|
34521
|
+
/**
|
|
34522
|
+
* Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
|
|
34523
|
+
* trip. Fail-soft on every axis: missing fetchers short-circuit to
|
|
34524
|
+
* an empty fold, fetcher failures emit a per-category audit event
|
|
34525
|
+
* and are omitted from the rendered section, an LLM-assist failure
|
|
34526
|
+
* proceeds with no fold. Returns the rendered section + the list of
|
|
34527
|
+
* categories whose data made it into the section (used for the
|
|
34528
|
+
* round-trip audit emission).
|
|
34529
|
+
*/
|
|
34530
|
+
async runDynamicContextFold(query) {
|
|
34531
|
+
if (!this.contextFetchers) {
|
|
34532
|
+
return { section: "", categoriesIncluded: [] };
|
|
34533
|
+
}
|
|
34534
|
+
const result = await foldContext(query, this.contextFetchers, {
|
|
34535
|
+
maxTokens: this.dynamicContextBudget,
|
|
34536
|
+
...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
|
|
34537
|
+
onFetcherFailure: (category, error) => {
|
|
34538
|
+
this.emitContextFetcherFailed(category, classifyFetcherError(error));
|
|
34539
|
+
}
|
|
34540
|
+
});
|
|
34541
|
+
return result;
|
|
34542
|
+
}
|
|
34543
|
+
/**
|
|
34544
|
+
* Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
|
|
34545
|
+
* of the fold path so the dynamic-context handler stays readable.
|
|
34546
|
+
* Emits with `result: "failure"` since the named category dropped
|
|
34547
|
+
* from the rendered section for this round-trip.
|
|
34548
|
+
*/
|
|
34549
|
+
emitContextFetcherFailed(category, failureReason) {
|
|
34550
|
+
const payload = {
|
|
34551
|
+
version: "1.2",
|
|
34552
|
+
event_id: makeEventId("conc-ctxfail"),
|
|
34553
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34554
|
+
identity_id: this.identityId,
|
|
34555
|
+
kind: "operator_concierge_context_fetcher_failed",
|
|
34556
|
+
surface: "concierge",
|
|
34557
|
+
category,
|
|
34558
|
+
failure_reason: failureReason
|
|
34559
|
+
};
|
|
34560
|
+
this.emit(
|
|
34561
|
+
OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
|
|
34562
|
+
payload,
|
|
34563
|
+
"failure"
|
|
34564
|
+
);
|
|
34565
|
+
}
|
|
34566
|
+
/**
|
|
34567
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
34568
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
34569
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
34570
|
+
* the input is empty or when the budget excludes every turn.
|
|
34571
|
+
*/
|
|
34572
|
+
formatPriorTurnsSection(turns) {
|
|
34573
|
+
if (turns.length === 0) return "";
|
|
34574
|
+
const HEADER = "## Prior conversation";
|
|
34575
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
34576
|
+
const headerTokens = approxTokenLen2(`${HEADER}
|
|
34577
|
+
`);
|
|
34578
|
+
const sepTokens = approxTokenLen2("\n");
|
|
34579
|
+
let runningTokens = headerTokens;
|
|
34580
|
+
let runningLines = [];
|
|
34581
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
34582
|
+
const line = lines[i];
|
|
34583
|
+
const tokens = approxTokenLen2(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
34584
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
34585
|
+
runningTokens += tokens;
|
|
34586
|
+
runningLines.push(line);
|
|
34587
|
+
}
|
|
34588
|
+
if (runningLines.length === 0) return "";
|
|
34589
|
+
runningLines = runningLines.reverse();
|
|
34590
|
+
return `${HEADER}
|
|
34591
|
+
${runningLines.join("\n")}`;
|
|
33344
34592
|
}
|
|
33345
34593
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
33346
34594
|
emit(operation, payload, result) {
|
|
@@ -33360,7 +34608,7 @@ ${inbox}`;
|
|
|
33360
34608
|
function chatStorageKey(surface, threadKey) {
|
|
33361
34609
|
return `${surface}.${threadKey}`;
|
|
33362
34610
|
}
|
|
33363
|
-
var OPERATOR_CHAT_NAMESPACE,
|
|
34611
|
+
var OPERATOR_CHAT_NAMESPACE, HKDF_INFO2, OperatorChatStore;
|
|
33364
34612
|
var init_operator_chat_store = __esm({
|
|
33365
34613
|
"src/chat/operator-chat-store.ts"() {
|
|
33366
34614
|
init_encryption();
|
|
@@ -33368,13 +34616,13 @@ var init_operator_chat_store = __esm({
|
|
|
33368
34616
|
init_encoding();
|
|
33369
34617
|
init_operator_chat_types();
|
|
33370
34618
|
OPERATOR_CHAT_NAMESPACE = "_chat";
|
|
33371
|
-
|
|
34619
|
+
HKDF_INFO2 = "operator-chat-store-v1";
|
|
33372
34620
|
OperatorChatStore = class {
|
|
33373
34621
|
storage;
|
|
33374
34622
|
encryptionKey;
|
|
33375
34623
|
constructor(storage, masterKey) {
|
|
33376
34624
|
this.storage = storage;
|
|
33377
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
34625
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
|
|
33378
34626
|
}
|
|
33379
34627
|
/**
|
|
33380
34628
|
* Load a thread. Returns null if no record exists or if the on-disk
|
|
@@ -33459,7 +34707,7 @@ var init_operator_chat_store = __esm({
|
|
|
33459
34707
|
function bundleKey(threadId) {
|
|
33460
34708
|
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
33461
34709
|
}
|
|
33462
|
-
function
|
|
34710
|
+
function stripKeyPrefix2(key) {
|
|
33463
34711
|
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
33464
34712
|
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
33465
34713
|
}
|
|
@@ -33470,7 +34718,7 @@ function lastTurnId(bundle) {
|
|
|
33470
34718
|
}
|
|
33471
34719
|
return max;
|
|
33472
34720
|
}
|
|
33473
|
-
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX,
|
|
34721
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO3, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
|
|
33474
34722
|
var init_concierge_memory_store = __esm({
|
|
33475
34723
|
"src/chat/concierge-memory-store.ts"() {
|
|
33476
34724
|
init_encryption();
|
|
@@ -33478,9 +34726,9 @@ var init_concierge_memory_store = __esm({
|
|
|
33478
34726
|
init_encoding();
|
|
33479
34727
|
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
33480
34728
|
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
33481
|
-
|
|
34729
|
+
HKDF_INFO3 = "concierge-memory-store-v1";
|
|
33482
34730
|
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
33483
|
-
|
|
34731
|
+
MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
|
|
33484
34732
|
ConciergeMemoryStore = class {
|
|
33485
34733
|
storage;
|
|
33486
34734
|
encryptionKey;
|
|
@@ -33489,7 +34737,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33489
34737
|
locks;
|
|
33490
34738
|
constructor(opts) {
|
|
33491
34739
|
this.storage = opts.storage;
|
|
33492
|
-
this.encryptionKey = derivePurposeKey(opts.masterKey,
|
|
34740
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
|
|
33493
34741
|
this.fortressId = opts.fortressId;
|
|
33494
34742
|
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
33495
34743
|
this.locks = /* @__PURE__ */ new Map();
|
|
@@ -33545,6 +34793,65 @@ var init_concierge_memory_store = __esm({
|
|
|
33545
34793
|
}
|
|
33546
34794
|
return turns;
|
|
33547
34795
|
}
|
|
34796
|
+
/**
|
|
34797
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
34798
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
34799
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
34800
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
34801
|
+
* with a concrete cause.
|
|
34802
|
+
*
|
|
34803
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
34804
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
34805
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
34806
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
34807
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
34808
|
+
* - Storage IO error → `io_failed`.
|
|
34809
|
+
*/
|
|
34810
|
+
async readThreadStrict(threadId, opts) {
|
|
34811
|
+
const key = bundleKey(threadId);
|
|
34812
|
+
let raw;
|
|
34813
|
+
try {
|
|
34814
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
34815
|
+
} catch {
|
|
34816
|
+
return { ok: false, reason: "io_failed" };
|
|
34817
|
+
}
|
|
34818
|
+
if (!raw) return { ok: true, turns: [] };
|
|
34819
|
+
if (raw.length > MAX_BUNDLE_BYTES3) {
|
|
34820
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
34821
|
+
}
|
|
34822
|
+
let envelope;
|
|
34823
|
+
try {
|
|
34824
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
34825
|
+
} catch {
|
|
34826
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34827
|
+
}
|
|
34828
|
+
let plaintext;
|
|
34829
|
+
try {
|
|
34830
|
+
const aad = stringToBytes(threadId);
|
|
34831
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
34832
|
+
} catch {
|
|
34833
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
34834
|
+
}
|
|
34835
|
+
let parsed;
|
|
34836
|
+
try {
|
|
34837
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
34838
|
+
} catch {
|
|
34839
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34840
|
+
}
|
|
34841
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
34842
|
+
if (parsed.thread_id !== threadId) {
|
|
34843
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34844
|
+
}
|
|
34845
|
+
let turns = parsed.turns;
|
|
34846
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
34847
|
+
const cutoff = opts.sinceTurnId;
|
|
34848
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
34849
|
+
}
|
|
34850
|
+
if (opts?.limit !== void 0) {
|
|
34851
|
+
turns = turns.slice(0, opts.limit);
|
|
34852
|
+
}
|
|
34853
|
+
return { ok: true, turns };
|
|
34854
|
+
}
|
|
33548
34855
|
/**
|
|
33549
34856
|
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33550
34857
|
* Sorted newest-first by last_turn_at.
|
|
@@ -33556,7 +34863,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33556
34863
|
);
|
|
33557
34864
|
const summaries = [];
|
|
33558
34865
|
for (const meta of entries) {
|
|
33559
|
-
const threadId =
|
|
34866
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
33560
34867
|
if (threadId === null) continue;
|
|
33561
34868
|
const bundle = await this.loadBundle(threadId);
|
|
33562
34869
|
if (!bundle || bundle.turns.length === 0) continue;
|
|
@@ -33609,7 +34916,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33609
34916
|
);
|
|
33610
34917
|
let pruned = 0;
|
|
33611
34918
|
for (const meta of entries) {
|
|
33612
|
-
const threadId =
|
|
34919
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
33613
34920
|
if (threadId === null) continue;
|
|
33614
34921
|
pruned += await this.withLock(threadId, async () => {
|
|
33615
34922
|
const bundle = await this.loadBundle(threadId);
|
|
@@ -33640,7 +34947,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33640
34947
|
return null;
|
|
33641
34948
|
}
|
|
33642
34949
|
if (!raw) return null;
|
|
33643
|
-
if (raw.length >
|
|
34950
|
+
if (raw.length > MAX_BUNDLE_BYTES3) return null;
|
|
33644
34951
|
try {
|
|
33645
34952
|
const envelope = JSON.parse(bytesToString(raw));
|
|
33646
34953
|
const aad = stringToBytes(threadId);
|
|
@@ -33731,7 +35038,18 @@ function buildV11Bindings(inputs) {
|
|
|
33731
35038
|
registry
|
|
33732
35039
|
}),
|
|
33733
35040
|
conciergePiiFilter: buildConciergePiiFilter(),
|
|
33734
|
-
conciergeMemory
|
|
35041
|
+
conciergeMemory,
|
|
35042
|
+
conciergeContextFetchers: buildConciergeContextFetchers({
|
|
35043
|
+
auditLog: inputs.auditLog,
|
|
35044
|
+
identityId: inputs.identityId,
|
|
35045
|
+
registry
|
|
35046
|
+
}),
|
|
35047
|
+
...inputs.intelligenceSelector ? {
|
|
35048
|
+
conciergeContextLlmAssist: buildConciergeContextLlmAssist({
|
|
35049
|
+
selector: inputs.intelligenceSelector,
|
|
35050
|
+
identityId: inputs.identityId
|
|
35051
|
+
})
|
|
35052
|
+
} : {}
|
|
33735
35053
|
});
|
|
33736
35054
|
}
|
|
33737
35055
|
const hubService = new HubService({
|
|
@@ -33792,6 +35110,107 @@ function buildConciergeContextProviders(args) {
|
|
|
33792
35110
|
}
|
|
33793
35111
|
};
|
|
33794
35112
|
}
|
|
35113
|
+
function buildConciergeContextFetchers(args) {
|
|
35114
|
+
const empty = async () => "";
|
|
35115
|
+
return {
|
|
35116
|
+
templates: async () => {
|
|
35117
|
+
const entries = listTemplates();
|
|
35118
|
+
if (entries.length === 0) return "(no templates installed)";
|
|
35119
|
+
const lines = entries.map((e) => {
|
|
35120
|
+
const m = e.metadata;
|
|
35121
|
+
return `${m.name} (tier ${m.tier}, channel ${m.channel}, target ${m.target_archetype})`;
|
|
35122
|
+
});
|
|
35123
|
+
return lines.join("\n");
|
|
35124
|
+
},
|
|
35125
|
+
agent_state: async (agentNameHint) => {
|
|
35126
|
+
const records = args.registry.list({ identity_id: args.identityId });
|
|
35127
|
+
if (records.length === 0) return "(no wrapped agents)";
|
|
35128
|
+
const filtered = agentNameHint ? records.filter(
|
|
35129
|
+
(r) => r.agent_id.toLowerCase().includes(agentNameHint.toLowerCase()) || r.harness.toLowerCase().includes(agentNameHint.toLowerCase())
|
|
35130
|
+
) : records;
|
|
35131
|
+
const target = filtered.length > 0 ? filtered : records;
|
|
35132
|
+
const lines = target.slice(0, 20).map((r) => {
|
|
35133
|
+
const tmpl = typeof r.channel_template_id === "string" ? r.channel_template_id : "no_template";
|
|
35134
|
+
return `${r.agent_id} harness=${r.harness} status=${r.status} template=${tmpl}`;
|
|
35135
|
+
});
|
|
35136
|
+
return lines.join("\n");
|
|
35137
|
+
},
|
|
35138
|
+
agent_activity: async (agentNameHint) => {
|
|
35139
|
+
const result = await args.auditLog.query({ limit: 50 });
|
|
35140
|
+
const owned = result.entries.filter(
|
|
35141
|
+
(e) => e.identity_id === args.identityId
|
|
35142
|
+
);
|
|
35143
|
+
const filtered = agentNameHint ? owned.filter((e) => {
|
|
35144
|
+
const agentId = e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : "";
|
|
35145
|
+
return agentId.toLowerCase().includes(agentNameHint.toLowerCase());
|
|
35146
|
+
}) : owned;
|
|
35147
|
+
const tail = (filtered.length > 0 ? filtered : owned).slice(-20);
|
|
35148
|
+
if (tail.length === 0) return "(no activity)";
|
|
35149
|
+
return tail.map((e) => {
|
|
35150
|
+
const agentId = (e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : null) ?? "_fortress";
|
|
35151
|
+
return `${e.timestamp} ${e.layer}.${e.operation} agent=${agentId} result=${e.result}`;
|
|
35152
|
+
}).join("\n");
|
|
35153
|
+
},
|
|
35154
|
+
audit_log: async () => {
|
|
35155
|
+
const result = await args.auditLog.query({ limit: 30 });
|
|
35156
|
+
const owned = result.entries.filter(
|
|
35157
|
+
(e) => e.identity_id === args.identityId
|
|
35158
|
+
);
|
|
35159
|
+
if (owned.length === 0) return "(no audit log entries)";
|
|
35160
|
+
return owned.slice(-30).map(
|
|
35161
|
+
(e) => `${e.timestamp} ${e.layer}.${e.operation} result=${e.result}`
|
|
35162
|
+
).join("\n");
|
|
35163
|
+
},
|
|
35164
|
+
sentinel_findings: empty,
|
|
35165
|
+
anomaly_alerts: empty,
|
|
35166
|
+
recent_receipts: async () => {
|
|
35167
|
+
const result = await args.auditLog.query({ limit: 100 });
|
|
35168
|
+
const owned = result.entries.filter(
|
|
35169
|
+
(e) => e.identity_id === args.identityId && e.operation.startsWith("composition_")
|
|
35170
|
+
);
|
|
35171
|
+
if (owned.length === 0) return "(no recent composition events)";
|
|
35172
|
+
return owned.slice(-15).map((e) => `${e.timestamp} ${e.operation} result=${e.result}`).join("\n");
|
|
35173
|
+
},
|
|
35174
|
+
verascore_deltas: empty
|
|
35175
|
+
};
|
|
35176
|
+
}
|
|
35177
|
+
function buildConciergeContextLlmAssist(args) {
|
|
35178
|
+
return async (query, categories) => {
|
|
35179
|
+
const labelList = categories.map((c) => `- ${c}`).join("\n");
|
|
35180
|
+
const prompt2 = `You are a router. Classify the operator's query into one of the categories below or "none".
|
|
35181
|
+
Reply with exactly one token: one category name or "none".
|
|
35182
|
+
|
|
35183
|
+
Categories:
|
|
35184
|
+
${labelList}
|
|
35185
|
+
|
|
35186
|
+
Query: ${query}
|
|
35187
|
+
|
|
35188
|
+
Category:`;
|
|
35189
|
+
try {
|
|
35190
|
+
const handle = await args.selector.getSubstrate("concierge");
|
|
35191
|
+
if (!handle.capability.summarize) return "none";
|
|
35192
|
+
const response = await args.selector.invokeSummarize("concierge", {
|
|
35193
|
+
kind: "summarize",
|
|
35194
|
+
context: prompt2,
|
|
35195
|
+
query: "Output the single category token.",
|
|
35196
|
+
maxTokens: 16
|
|
35197
|
+
});
|
|
35198
|
+
if (response.failureClass || response.body.kind !== "summarize") {
|
|
35199
|
+
return "none";
|
|
35200
|
+
}
|
|
35201
|
+
const raw = response.body.text.trim().toLowerCase();
|
|
35202
|
+
const head = raw.split(/\s|[.,!?:;]/)[0] ?? "";
|
|
35203
|
+
const normalized = head.replace(/[^a-z_]/g, "");
|
|
35204
|
+
const known = categories;
|
|
35205
|
+
if (known.includes(normalized)) {
|
|
35206
|
+
return normalized;
|
|
35207
|
+
}
|
|
35208
|
+
return "none";
|
|
35209
|
+
} catch {
|
|
35210
|
+
return "none";
|
|
35211
|
+
}
|
|
35212
|
+
};
|
|
35213
|
+
}
|
|
33795
35214
|
function buildConciergePiiFilter() {
|
|
33796
35215
|
return {
|
|
33797
35216
|
filter(input) {
|
|
@@ -33819,6 +35238,7 @@ var init_wiring = __esm({
|
|
|
33819
35238
|
init_agent_registry_persistence();
|
|
33820
35239
|
init_operator_chat_index();
|
|
33821
35240
|
init_privacy_filter();
|
|
35241
|
+
init_registry();
|
|
33822
35242
|
CapabilityErrorAgentController = class {
|
|
33823
35243
|
fail(action) {
|
|
33824
35244
|
throw new HubCapabilityError(
|
|
@@ -33966,7 +35386,7 @@ var init_defaults = __esm({
|
|
|
33966
35386
|
});
|
|
33967
35387
|
|
|
33968
35388
|
// src/intelligence/policy-store.ts
|
|
33969
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
35389
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO4, IntelligenceConfigStore;
|
|
33970
35390
|
var init_policy_store = __esm({
|
|
33971
35391
|
"src/intelligence/policy-store.ts"() {
|
|
33972
35392
|
init_encryption();
|
|
@@ -33975,13 +35395,13 @@ var init_policy_store = __esm({
|
|
|
33975
35395
|
init_defaults();
|
|
33976
35396
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
33977
35397
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
33978
|
-
|
|
35398
|
+
HKDF_INFO4 = "intelligence-substrate-config";
|
|
33979
35399
|
IntelligenceConfigStore = class {
|
|
33980
35400
|
storage;
|
|
33981
35401
|
encryptionKey;
|
|
33982
35402
|
constructor(storage, masterKey) {
|
|
33983
35403
|
this.storage = storage;
|
|
33984
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
35404
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
|
|
33985
35405
|
}
|
|
33986
35406
|
/**
|
|
33987
35407
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -36442,7 +37862,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
36442
37862
|
}
|
|
36443
37863
|
return null;
|
|
36444
37864
|
}
|
|
36445
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
37865
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
36446
37866
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
36447
37867
|
if (!destinationSigner) {
|
|
36448
37868
|
return {
|
|
@@ -36504,8 +37924,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36504
37924
|
}
|
|
36505
37925
|
}
|
|
36506
37926
|
}
|
|
37927
|
+
let plaintext;
|
|
36507
37928
|
try {
|
|
36508
|
-
|
|
37929
|
+
plaintext = decrypt(
|
|
36509
37930
|
item.entry.payload,
|
|
36510
37931
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
36511
37932
|
);
|
|
@@ -36514,28 +37935,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36514
37935
|
skipped++;
|
|
36515
37936
|
continue;
|
|
36516
37937
|
}
|
|
36517
|
-
await stateStore.write(
|
|
36518
|
-
item.namespace,
|
|
36519
|
-
item.key,
|
|
36520
|
-
bytesToString(plaintext),
|
|
36521
|
-
destinationSigner.identity_id,
|
|
36522
|
-
destinationSigner.encrypted_private_key,
|
|
36523
|
-
identityEncryptionKey,
|
|
36524
|
-
{
|
|
36525
|
-
content_type: item.entry.metadata.content_type,
|
|
36526
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
36527
|
-
tags: [
|
|
36528
|
-
...item.entry.metadata.tags ?? [],
|
|
36529
|
-
"exit-import",
|
|
36530
|
-
`source:${item.entry.kid}`
|
|
36531
|
-
]
|
|
36532
|
-
}
|
|
36533
|
-
);
|
|
36534
|
-
imported++;
|
|
36535
37938
|
} catch {
|
|
36536
37939
|
skippedInvalidSig++;
|
|
36537
37940
|
skipped++;
|
|
37941
|
+
continue;
|
|
36538
37942
|
}
|
|
37943
|
+
await stateStore.write(
|
|
37944
|
+
item.namespace,
|
|
37945
|
+
item.key,
|
|
37946
|
+
bytesToString(plaintext),
|
|
37947
|
+
destinationSigner.identity_id,
|
|
37948
|
+
destinationSigner.encrypted_private_key,
|
|
37949
|
+
identityEncryptionKey,
|
|
37950
|
+
{
|
|
37951
|
+
content_type: item.entry.metadata.content_type,
|
|
37952
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
37953
|
+
tags: [
|
|
37954
|
+
...item.entry.metadata.tags ?? [],
|
|
37955
|
+
"exit-import",
|
|
37956
|
+
`source:${item.entry.kid}`
|
|
37957
|
+
]
|
|
37958
|
+
}
|
|
37959
|
+
);
|
|
37960
|
+
imported++;
|
|
37961
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
36539
37962
|
}
|
|
36540
37963
|
return {
|
|
36541
37964
|
status: "rekeyed",
|
|
@@ -36546,6 +37969,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36546
37969
|
conflicts
|
|
36547
37970
|
};
|
|
36548
37971
|
}
|
|
37972
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
37973
|
+
let removed = 0;
|
|
37974
|
+
const failed = [];
|
|
37975
|
+
for (const loc of staged) {
|
|
37976
|
+
try {
|
|
37977
|
+
const ok2 = await storage.delete(loc.namespace, loc.key);
|
|
37978
|
+
if (ok2) {
|
|
37979
|
+
removed++;
|
|
37980
|
+
} else {
|
|
37981
|
+
failed.push(loc);
|
|
37982
|
+
}
|
|
37983
|
+
} catch {
|
|
37984
|
+
failed.push(loc);
|
|
37985
|
+
}
|
|
37986
|
+
}
|
|
37987
|
+
return { removed, failed };
|
|
37988
|
+
}
|
|
36549
37989
|
async function stageArtifact(storage, namespace, key, value) {
|
|
36550
37990
|
await storage.write(namespace, key, jsonBytes(value));
|
|
36551
37991
|
}
|
|
@@ -36670,6 +38110,8 @@ async function importExitBundle(opts) {
|
|
|
36670
38110
|
}
|
|
36671
38111
|
const importId = importIdForManifest(manifest);
|
|
36672
38112
|
const stagedArtifacts = [];
|
|
38113
|
+
const stagedLocations = [];
|
|
38114
|
+
const importedRekeyEntries = [];
|
|
36673
38115
|
if (identityArtifact) {
|
|
36674
38116
|
await stageArtifact(
|
|
36675
38117
|
opts.storage,
|
|
@@ -36678,10 +38120,15 @@ async function importExitBundle(opts) {
|
|
|
36678
38120
|
identityArtifact.json
|
|
36679
38121
|
);
|
|
36680
38122
|
stagedArtifacts.push("public_identity");
|
|
38123
|
+
stagedLocations.push({
|
|
38124
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
38125
|
+
key: identityArtifact.json.bundle.identity_id
|
|
38126
|
+
});
|
|
36681
38127
|
}
|
|
36682
38128
|
if (policySet) {
|
|
36683
38129
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
36684
38130
|
stagedArtifacts.push("policy_set");
|
|
38131
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
36685
38132
|
}
|
|
36686
38133
|
if (auditReceipts) {
|
|
36687
38134
|
await stageArtifact(
|
|
@@ -36691,10 +38138,12 @@ async function importExitBundle(opts) {
|
|
|
36691
38138
|
auditReceipts.json
|
|
36692
38139
|
);
|
|
36693
38140
|
stagedArtifacts.push("audit_receipts");
|
|
38141
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
36694
38142
|
}
|
|
36695
38143
|
if (commitments) {
|
|
36696
38144
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
36697
38145
|
stagedArtifacts.push("commitments");
|
|
38146
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
36698
38147
|
}
|
|
36699
38148
|
if (placeholderMetadata) {
|
|
36700
38149
|
await stageArtifact(
|
|
@@ -36704,12 +38153,17 @@ async function importExitBundle(opts) {
|
|
|
36704
38153
|
placeholderMetadata.json
|
|
36705
38154
|
);
|
|
36706
38155
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
38156
|
+
stagedLocations.push({
|
|
38157
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
38158
|
+
key: importId
|
|
38159
|
+
});
|
|
36707
38160
|
}
|
|
36708
38161
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
36709
38162
|
manifest: manifest.body,
|
|
36710
38163
|
verified_at: verification.verified_at,
|
|
36711
38164
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
36712
38165
|
});
|
|
38166
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
36713
38167
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
36714
38168
|
let reputationResult = {
|
|
36715
38169
|
imported_attestations: 0,
|
|
@@ -36734,26 +38188,57 @@ async function importExitBundle(opts) {
|
|
|
36734
38188
|
encryptedState?.json ?? null,
|
|
36735
38189
|
opts
|
|
36736
38190
|
);
|
|
36737
|
-
|
|
36738
|
-
|
|
36739
|
-
|
|
36740
|
-
|
|
36741
|
-
|
|
36742
|
-
|
|
36743
|
-
|
|
36744
|
-
|
|
36745
|
-
|
|
36746
|
-
|
|
36747
|
-
|
|
36748
|
-
|
|
36749
|
-
|
|
36750
|
-
|
|
36751
|
-
|
|
36752
|
-
|
|
36753
|
-
|
|
36754
|
-
|
|
36755
|
-
|
|
36756
|
-
|
|
38191
|
+
let stateResult;
|
|
38192
|
+
try {
|
|
38193
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
38194
|
+
encryptedState.json,
|
|
38195
|
+
opts,
|
|
38196
|
+
sourceMasterKey,
|
|
38197
|
+
publicKeys.byIdentityId,
|
|
38198
|
+
importedRekeyEntries
|
|
38199
|
+
) : {
|
|
38200
|
+
status: "staged_requires_source_key",
|
|
38201
|
+
imported_keys: 0,
|
|
38202
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
38203
|
+
skipped_invalid_sig: 0,
|
|
38204
|
+
skipped_unknown_kid: 0,
|
|
38205
|
+
conflicts: conflicts.state_conflicts.length
|
|
38206
|
+
} : {
|
|
38207
|
+
status: "not_requested",
|
|
38208
|
+
imported_keys: 0,
|
|
38209
|
+
skipped_keys: 0,
|
|
38210
|
+
skipped_invalid_sig: 0,
|
|
38211
|
+
skipped_unknown_kid: 0,
|
|
38212
|
+
conflicts: 0
|
|
38213
|
+
};
|
|
38214
|
+
} catch (err) {
|
|
38215
|
+
const toCleanup = [
|
|
38216
|
+
...importedRekeyEntries,
|
|
38217
|
+
...stagedLocations
|
|
38218
|
+
];
|
|
38219
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
38220
|
+
opts.auditLog.append(
|
|
38221
|
+
"l1",
|
|
38222
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
38223
|
+
manifest.body.identity_binding.identity_id,
|
|
38224
|
+
{
|
|
38225
|
+
import_id: importId,
|
|
38226
|
+
manifest_version: manifest.body.manifest_version,
|
|
38227
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
38228
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
38229
|
+
removed_total: cleanup.removed,
|
|
38230
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
38231
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
38232
|
+
},
|
|
38233
|
+
"failure"
|
|
38234
|
+
);
|
|
38235
|
+
await opts.auditLog.flush();
|
|
38236
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
38237
|
+
throw new ExitBundleImportError(
|
|
38238
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
38239
|
+
`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).`
|
|
38240
|
+
);
|
|
38241
|
+
}
|
|
36757
38242
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
36758
38243
|
import_id: importId,
|
|
36759
38244
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -37832,16 +39317,35 @@ ${err.message}
|
|
|
37832
39317
|
timestamp: alert.timestamp
|
|
37833
39318
|
});
|
|
37834
39319
|
} : void 0;
|
|
37835
|
-
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37836
39320
|
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37837
39321
|
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
39322
|
+
const aggregatorPayloadStore = new AggregatorPayloadStore({
|
|
39323
|
+
storage,
|
|
39324
|
+
masterKey,
|
|
39325
|
+
fortressId: fortressIdForAggregator
|
|
39326
|
+
});
|
|
37838
39327
|
const approvalAggregator = new ApprovalAggregator({
|
|
37839
39328
|
storage,
|
|
37840
39329
|
masterKey,
|
|
37841
39330
|
auditLog,
|
|
37842
39331
|
identityId: aggregatorIdentityId,
|
|
37843
|
-
fortressId: fortressIdForAggregator
|
|
39332
|
+
fortressId: fortressIdForAggregator,
|
|
39333
|
+
payloadStore: aggregatorPayloadStore
|
|
39334
|
+
});
|
|
39335
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
39336
|
+
underlying: approvalChannel,
|
|
39337
|
+
aggregator: approvalAggregator,
|
|
39338
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
39339
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
37844
39340
|
});
|
|
39341
|
+
const gate = new ApprovalGate(
|
|
39342
|
+
policy,
|
|
39343
|
+
baseline,
|
|
39344
|
+
wrappedApprovalChannel,
|
|
39345
|
+
auditLog,
|
|
39346
|
+
injectionDetector,
|
|
39347
|
+
onInjectionAlert
|
|
39348
|
+
);
|
|
37845
39349
|
gate.setApprovalEventCallback((event) => {
|
|
37846
39350
|
void approvalAggregator.ingest(event);
|
|
37847
39351
|
});
|
|
@@ -38039,6 +39543,8 @@ var init_src = __esm({
|
|
|
38039
39543
|
init_webhook();
|
|
38040
39544
|
init_gate();
|
|
38041
39545
|
init_approval_aggregator();
|
|
39546
|
+
init_aggregator_backed_channel();
|
|
39547
|
+
init_aggregator_store();
|
|
38042
39548
|
init_tools4();
|
|
38043
39549
|
init_router();
|
|
38044
39550
|
init_router();
|
|
@@ -41003,6 +42509,22 @@ var init_broker = __esm({
|
|
|
41003
42509
|
auditLog;
|
|
41004
42510
|
issuer;
|
|
41005
42511
|
principalIdentityId;
|
|
42512
|
+
/**
|
|
42513
|
+
* Per-secret-name mutex. Hardening wave 6 finding #64: two concurrent
|
|
42514
|
+
* addSecret() / rotateSecret() / deleteSecret() calls on the same name
|
|
42515
|
+
* MUST serialize cleanly. The keychain backend's `find-then-add` and
|
|
42516
|
+
* `find-then-delete-then-add` shapes (KeychainBackend.addSecret /
|
|
42517
|
+
* .rotateSecret) are not atomic against another caller racing the same
|
|
42518
|
+
* service-name; without serialization the second caller can observe a
|
|
42519
|
+
* stale "exists" check and either drop the new value or leave a
|
|
42520
|
+
* duplicate keychain entry.
|
|
42521
|
+
*
|
|
42522
|
+
* Implementation: an in-memory promise chain per name. Subsequent
|
|
42523
|
+
* callers `await` the chain tail and append their own work; failures
|
|
42524
|
+
* propagate to the failing caller without poisoning the chain for
|
|
42525
|
+
* later callers.
|
|
42526
|
+
*/
|
|
42527
|
+
nameLocks = /* @__PURE__ */ new Map();
|
|
41006
42528
|
constructor(opts) {
|
|
41007
42529
|
this.backend = opts.backend;
|
|
41008
42530
|
this.auditLog = opts.auditLog;
|
|
@@ -41013,6 +42535,40 @@ var init_broker = __esm({
|
|
|
41013
42535
|
grants: opts.grants
|
|
41014
42536
|
});
|
|
41015
42537
|
}
|
|
42538
|
+
/**
|
|
42539
|
+
* Serialize `op` against any other in-flight write to the same secret
|
|
42540
|
+
* `name`. Per-name fairness only, distinct names run in parallel.
|
|
42541
|
+
* The current chain tail is used as the acceptance gate; we then
|
|
42542
|
+
* publish a new tail that swallows the operation's outcome so a
|
|
42543
|
+
* thrown error does not poison the next caller's wait.
|
|
42544
|
+
*/
|
|
42545
|
+
async withNameLock(name, op) {
|
|
42546
|
+
const previous = this.nameLocks.get(name) ?? Promise.resolve();
|
|
42547
|
+
let release = () => {
|
|
42548
|
+
};
|
|
42549
|
+
const next = new Promise((resolve8) => {
|
|
42550
|
+
release = resolve8;
|
|
42551
|
+
});
|
|
42552
|
+
this.nameLocks.set(name, next);
|
|
42553
|
+
try {
|
|
42554
|
+
await previous.catch(() => {
|
|
42555
|
+
});
|
|
42556
|
+
return await op();
|
|
42557
|
+
} finally {
|
|
42558
|
+
release();
|
|
42559
|
+
if (this.nameLocks.get(name) === next) {
|
|
42560
|
+
this.nameLocks.delete(name);
|
|
42561
|
+
}
|
|
42562
|
+
}
|
|
42563
|
+
}
|
|
42564
|
+
/**
|
|
42565
|
+
* Diagnostic-only: visible for tests so they can assert that distinct
|
|
42566
|
+
* names do not contend on a shared lock. Not part of the public broker
|
|
42567
|
+
* contract; do not consume from production code.
|
|
42568
|
+
*/
|
|
42569
|
+
__nameLockCountForTests() {
|
|
42570
|
+
return this.nameLocks.size;
|
|
42571
|
+
}
|
|
41016
42572
|
/** Ensure backend is initialized and unlocked. Audits the unlock. */
|
|
41017
42573
|
async ensureUnlocked(passphrase) {
|
|
41018
42574
|
await this.backend.ensureInitialized(passphrase);
|
|
@@ -41025,31 +42581,37 @@ var init_broker = __esm({
|
|
|
41025
42581
|
);
|
|
41026
42582
|
}
|
|
41027
42583
|
async addSecret(name, value) {
|
|
41028
|
-
await this.
|
|
41029
|
-
|
|
41030
|
-
|
|
41031
|
-
|
|
41032
|
-
|
|
41033
|
-
|
|
41034
|
-
|
|
42584
|
+
await this.withNameLock(name, async () => {
|
|
42585
|
+
await this.backend.addSecret(name, value);
|
|
42586
|
+
this.auditLog.append(
|
|
42587
|
+
"l3",
|
|
42588
|
+
BROKER_OPS.SECRET_ADDED,
|
|
42589
|
+
this.principalIdentityId,
|
|
42590
|
+
{ secret: name }
|
|
42591
|
+
);
|
|
42592
|
+
});
|
|
41035
42593
|
}
|
|
41036
42594
|
async rotateSecret(name, newValue) {
|
|
41037
|
-
await this.
|
|
41038
|
-
|
|
41039
|
-
|
|
41040
|
-
|
|
41041
|
-
|
|
41042
|
-
|
|
41043
|
-
|
|
42595
|
+
await this.withNameLock(name, async () => {
|
|
42596
|
+
await this.backend.rotateSecret(name, newValue);
|
|
42597
|
+
this.auditLog.append(
|
|
42598
|
+
"l3",
|
|
42599
|
+
BROKER_OPS.SECRET_ROTATED,
|
|
42600
|
+
this.principalIdentityId,
|
|
42601
|
+
{ secret: name }
|
|
42602
|
+
);
|
|
42603
|
+
});
|
|
41044
42604
|
}
|
|
41045
42605
|
async deleteSecret(name) {
|
|
41046
|
-
await this.
|
|
41047
|
-
|
|
41048
|
-
|
|
41049
|
-
|
|
41050
|
-
|
|
41051
|
-
|
|
41052
|
-
|
|
42606
|
+
await this.withNameLock(name, async () => {
|
|
42607
|
+
await this.backend.deleteSecret(name);
|
|
42608
|
+
this.auditLog.append(
|
|
42609
|
+
"l3",
|
|
42610
|
+
BROKER_OPS.SECRET_DELETED,
|
|
42611
|
+
this.principalIdentityId,
|
|
42612
|
+
{ secret: name }
|
|
42613
|
+
);
|
|
42614
|
+
});
|
|
41053
42615
|
}
|
|
41054
42616
|
async listSecretNames() {
|
|
41055
42617
|
return this.backend.listSecretNames();
|
|
@@ -41089,6 +42651,19 @@ var init_broker = __esm({
|
|
|
41089
42651
|
liveTokenCount() {
|
|
41090
42652
|
return this.issuer.liveTokenCount();
|
|
41091
42653
|
}
|
|
42654
|
+
/**
|
|
42655
|
+
* Drop expired tokens from the in-memory issuer map. Hardening wave 6
|
|
42656
|
+
* finding #86: previously expiry pruning depended on opportunistic
|
|
42657
|
+
* `pruneExpired()` calls; now the cocoon-unlock initialization path
|
|
42658
|
+
* (openBroker -> after backend.ensureInitialized -> after Broker
|
|
42659
|
+
* construction) fires this once so each cocoon-unlock cycle drops
|
|
42660
|
+
* stale bindings before any operator interaction.
|
|
42661
|
+
*
|
|
42662
|
+
* Returns the number of tokens removed. Safe to call repeatedly; idempotent.
|
|
42663
|
+
*/
|
|
42664
|
+
pruneExpiredTokens() {
|
|
42665
|
+
return this.issuer.pruneExpired();
|
|
42666
|
+
}
|
|
41092
42667
|
/**
|
|
41093
42668
|
* Audit query restricted to broker-scoped operations. Returns entries
|
|
41094
42669
|
* with their timestamps, op, and result (never the secret value).
|
|
@@ -41247,6 +42822,7 @@ async function openBroker(opts = {}) {
|
|
|
41247
42822
|
grants,
|
|
41248
42823
|
principalIdentityId: opts.principalIdentityId ?? "sanctuary-broker"
|
|
41249
42824
|
});
|
|
42825
|
+
broker.pruneExpiredTokens();
|
|
41250
42826
|
return {
|
|
41251
42827
|
broker,
|
|
41252
42828
|
close: async () => {
|
|
@@ -42115,8 +43691,6 @@ var init_health = __esm({
|
|
|
42115
43691
|
DEFAULT_TIMEOUT_MS4 = 500;
|
|
42116
43692
|
}
|
|
42117
43693
|
});
|
|
42118
|
-
|
|
42119
|
-
// src/cli/agents/cli.ts
|
|
42120
43694
|
function resolveCtx(args) {
|
|
42121
43695
|
const env = args.env ?? process.env;
|
|
42122
43696
|
const discoverOpts = {
|
|
@@ -42154,6 +43728,8 @@ async function runAgentsCommand(args) {
|
|
|
42154
43728
|
return await cmdShow2(rest, ctx);
|
|
42155
43729
|
case "status":
|
|
42156
43730
|
return await cmdStatus(rest, ctx);
|
|
43731
|
+
case "config":
|
|
43732
|
+
return await cmdConfig(rest, ctx);
|
|
42157
43733
|
default:
|
|
42158
43734
|
ctx.err.write(`Unknown subcommand: ${sub}
|
|
42159
43735
|
`);
|
|
@@ -42169,10 +43745,18 @@ async function runAgentsCommand(args) {
|
|
|
42169
43745
|
}
|
|
42170
43746
|
function printUsage4(s) {
|
|
42171
43747
|
s.write(`Usage: sanctuary agents <command> [flags]
|
|
43748
|
+
sanctuary agent <command> [flags] (alias)
|
|
42172
43749
|
|
|
42173
43750
|
list [--json] List every tenant visible on this host.
|
|
42174
|
-
show <tenant> [--json] Show details for one tenant
|
|
43751
|
+
show <tenant> [--json] Show details for one tenant (includes
|
|
43752
|
+
approval-redirect state).
|
|
42175
43753
|
status [--json] One-line-per-tenant running/stopped summary.
|
|
43754
|
+
config <tenant> [opts] Write tenant principal-policy.yaml fields.
|
|
43755
|
+
--approval-redirect=<bool> Toggle cross-harness inbox redirect.
|
|
43756
|
+
--approval-redirect-mode=<replace|notify>
|
|
43757
|
+
Pick replace (bypass underlying channel)
|
|
43758
|
+
or notify (race both paths). Default
|
|
43759
|
+
replace when toggled on.
|
|
42176
43760
|
|
|
42177
43761
|
Options:
|
|
42178
43762
|
--fortress <path> Scope discovery to a specific storage path
|
|
@@ -42274,6 +43858,7 @@ async function cmdShow2(argv, ctx) {
|
|
|
42274
43858
|
return 1;
|
|
42275
43859
|
}
|
|
42276
43860
|
const probe = await ctx.probe(tenant);
|
|
43861
|
+
const approvalRedirect = await readApprovalRedirectState(tenant);
|
|
42277
43862
|
const payload = {
|
|
42278
43863
|
name: tenant.name,
|
|
42279
43864
|
storage_path: tenant.storage_path,
|
|
@@ -42287,7 +43872,8 @@ async function cmdShow2(argv, ctx) {
|
|
|
42287
43872
|
running: probe.running,
|
|
42288
43873
|
status: probe.status,
|
|
42289
43874
|
reason: probe.reason
|
|
42290
|
-
}
|
|
43875
|
+
},
|
|
43876
|
+
approval_redirect: approvalRedirect
|
|
42291
43877
|
};
|
|
42292
43878
|
if (hasJsonFlag(argv)) {
|
|
42293
43879
|
ctx.out.write(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -42331,10 +43917,173 @@ async function cmdShow2(argv, ctx) {
|
|
|
42331
43917
|
}
|
|
42332
43918
|
ctx.out.write(
|
|
42333
43919
|
`probe: ${probe.running ? "running" : "not-running"}${probe.reason ? ` (${probe.reason})` : ""}
|
|
43920
|
+
`
|
|
43921
|
+
);
|
|
43922
|
+
ctx.out.write(
|
|
43923
|
+
`approval_redirect: ${approvalRedirect.enabled ? `on (${approvalRedirect.mode})` : "off"}
|
|
42334
43924
|
`
|
|
42335
43925
|
);
|
|
42336
43926
|
return 0;
|
|
42337
43927
|
}
|
|
43928
|
+
async function readApprovalRedirectState(tenant) {
|
|
43929
|
+
const policyPath = path.join(tenant.storage_path, "principal-policy.yaml");
|
|
43930
|
+
try {
|
|
43931
|
+
const content = await promises.readFile(policyPath, "utf-8");
|
|
43932
|
+
const parsed = parsePolicy(content);
|
|
43933
|
+
const cfg = parsed.approval_redirect;
|
|
43934
|
+
if (!cfg) return { enabled: false, mode: "replace" };
|
|
43935
|
+
return {
|
|
43936
|
+
enabled: !!cfg.enabled,
|
|
43937
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
43938
|
+
};
|
|
43939
|
+
} catch {
|
|
43940
|
+
return { enabled: false, mode: "replace" };
|
|
43941
|
+
}
|
|
43942
|
+
}
|
|
43943
|
+
function parseBoolFlag(raw) {
|
|
43944
|
+
if (raw === void 0) return null;
|
|
43945
|
+
const v = raw.toLowerCase();
|
|
43946
|
+
if (v === "true" || v === "yes" || v === "on" || v === "1") return true;
|
|
43947
|
+
if (v === "false" || v === "no" || v === "off" || v === "0") return false;
|
|
43948
|
+
return null;
|
|
43949
|
+
}
|
|
43950
|
+
function findFlagValue(argv, name) {
|
|
43951
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43952
|
+
const a = argv[i];
|
|
43953
|
+
if (a === name) {
|
|
43954
|
+
return argv[i + 1];
|
|
43955
|
+
}
|
|
43956
|
+
const eq = `${name}=`;
|
|
43957
|
+
if (a.startsWith(eq)) {
|
|
43958
|
+
return a.slice(eq.length);
|
|
43959
|
+
}
|
|
43960
|
+
}
|
|
43961
|
+
return void 0;
|
|
43962
|
+
}
|
|
43963
|
+
async function cmdConfig(argv, ctx) {
|
|
43964
|
+
const positional = argv.find((a) => !a.startsWith("--"));
|
|
43965
|
+
if (!positional) {
|
|
43966
|
+
ctx.err.write(
|
|
43967
|
+
"Missing tenant. Usage: sanctuary agents config <tenant> --approval-redirect=<bool>\n"
|
|
43968
|
+
);
|
|
43969
|
+
return 2;
|
|
43970
|
+
}
|
|
43971
|
+
const tenant = await findTenant(positional, ctx.discoverOpts);
|
|
43972
|
+
if (!tenant) {
|
|
43973
|
+
ctx.err.write(`sanctuary agents: unknown tenant "${positional}"
|
|
43974
|
+
`);
|
|
43975
|
+
return 1;
|
|
43976
|
+
}
|
|
43977
|
+
const redirectFlag = parseBoolFlag(
|
|
43978
|
+
findFlagValue(argv, "--approval-redirect")
|
|
43979
|
+
);
|
|
43980
|
+
const modeFlag = findFlagValue(argv, "--approval-redirect-mode");
|
|
43981
|
+
if (redirectFlag === null && modeFlag === void 0) {
|
|
43982
|
+
ctx.err.write(
|
|
43983
|
+
"sanctuary agents config: nothing to do. Pass --approval-redirect=<bool> or --approval-redirect-mode=<replace|notify>.\n"
|
|
43984
|
+
);
|
|
43985
|
+
return 2;
|
|
43986
|
+
}
|
|
43987
|
+
if (modeFlag !== void 0 && modeFlag !== "replace" && modeFlag !== "notify") {
|
|
43988
|
+
ctx.err.write(
|
|
43989
|
+
`sanctuary agents config: --approval-redirect-mode must be "replace" or "notify" (got "${modeFlag}")
|
|
43990
|
+
`
|
|
43991
|
+
);
|
|
43992
|
+
return 2;
|
|
43993
|
+
}
|
|
43994
|
+
const current = await readApprovalRedirectState(tenant);
|
|
43995
|
+
const next = {
|
|
43996
|
+
enabled: redirectFlag !== null ? redirectFlag : current.enabled,
|
|
43997
|
+
mode: modeFlag === "notify" || modeFlag === "replace" ? modeFlag : current.mode
|
|
43998
|
+
};
|
|
43999
|
+
await writeApprovalRedirectToPolicyFile(tenant.storage_path, next);
|
|
44000
|
+
if (hasJsonFlag(argv)) {
|
|
44001
|
+
ctx.out.write(
|
|
44002
|
+
JSON.stringify(
|
|
44003
|
+
{
|
|
44004
|
+
tenant: tenant.name,
|
|
44005
|
+
approval_redirect: next
|
|
44006
|
+
},
|
|
44007
|
+
null,
|
|
44008
|
+
2
|
|
44009
|
+
) + "\n"
|
|
44010
|
+
);
|
|
44011
|
+
} else {
|
|
44012
|
+
ctx.out.write(
|
|
44013
|
+
`sanctuary agents config: tenant "${tenant.name}" approval_redirect=${next.enabled ? `on (${next.mode})` : "off"}
|
|
44014
|
+
`
|
|
44015
|
+
);
|
|
44016
|
+
ctx.out.write(
|
|
44017
|
+
` Takes effect on the next gate request for the running server.
|
|
44018
|
+
`
|
|
44019
|
+
);
|
|
44020
|
+
}
|
|
44021
|
+
return 0;
|
|
44022
|
+
}
|
|
44023
|
+
async function writeApprovalRedirectToPolicyFile(storagePath, state) {
|
|
44024
|
+
const policyPath = path.join(storagePath, "principal-policy.yaml");
|
|
44025
|
+
let content;
|
|
44026
|
+
try {
|
|
44027
|
+
content = await promises.readFile(policyPath, "utf-8");
|
|
44028
|
+
} catch (err) {
|
|
44029
|
+
const code = err?.code;
|
|
44030
|
+
if (code !== "ENOENT") throw err;
|
|
44031
|
+
content = await defaultPolicyTextForBootstrap();
|
|
44032
|
+
}
|
|
44033
|
+
const block = renderApprovalRedirectBlock(state);
|
|
44034
|
+
const updated = upsertApprovalRedirectBlock(content, block);
|
|
44035
|
+
await promises.writeFile(policyPath, updated, "utf-8");
|
|
44036
|
+
await promises.chmod(policyPath, 384);
|
|
44037
|
+
}
|
|
44038
|
+
function renderApprovalRedirectBlock(state) {
|
|
44039
|
+
return [
|
|
44040
|
+
"# Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2)",
|
|
44041
|
+
"approval_redirect:",
|
|
44042
|
+
` enabled: ${state.enabled ? "true" : "false"}`,
|
|
44043
|
+
` mode: ${state.mode}`
|
|
44044
|
+
].join("\n");
|
|
44045
|
+
}
|
|
44046
|
+
function upsertApprovalRedirectBlock(content, block) {
|
|
44047
|
+
const lines = content.split("\n");
|
|
44048
|
+
const startIdx = lines.findIndex((l) => l.startsWith("approval_redirect:"));
|
|
44049
|
+
if (startIdx === -1) {
|
|
44050
|
+
const trimmed = content.endsWith("\n") ? content : content + "\n";
|
|
44051
|
+
return trimmed + "\n" + block + "\n";
|
|
44052
|
+
}
|
|
44053
|
+
let blockStart = startIdx;
|
|
44054
|
+
if (blockStart > 0 && lines[blockStart - 1] !== void 0 && lines[blockStart - 1].startsWith("# Approval Redirect")) {
|
|
44055
|
+
blockStart = blockStart - 1;
|
|
44056
|
+
}
|
|
44057
|
+
let blockEnd = startIdx + 1;
|
|
44058
|
+
while (blockEnd < lines.length) {
|
|
44059
|
+
const l = lines[blockEnd];
|
|
44060
|
+
if (l === "") {
|
|
44061
|
+
blockEnd++;
|
|
44062
|
+
continue;
|
|
44063
|
+
}
|
|
44064
|
+
if (/^[A-Za-z0-9#]/.test(l)) {
|
|
44065
|
+
break;
|
|
44066
|
+
}
|
|
44067
|
+
blockEnd++;
|
|
44068
|
+
}
|
|
44069
|
+
const before = lines.slice(0, blockStart);
|
|
44070
|
+
const after = lines.slice(blockEnd);
|
|
44071
|
+
const replaced = [...before, ...block.split("\n"), ...after].join("\n");
|
|
44072
|
+
return replaced.endsWith("\n") ? replaced : replaced + "\n";
|
|
44073
|
+
}
|
|
44074
|
+
async function defaultPolicyTextForBootstrap() {
|
|
44075
|
+
return [
|
|
44076
|
+
"version: 1",
|
|
44077
|
+
"tier1_always_approve:",
|
|
44078
|
+
" - state_export",
|
|
44079
|
+
" - state_import",
|
|
44080
|
+
" - state_delete",
|
|
44081
|
+
"approval_channel:",
|
|
44082
|
+
" type: stderr",
|
|
44083
|
+
" timeout_seconds: 300",
|
|
44084
|
+
""
|
|
44085
|
+
].join("\n");
|
|
44086
|
+
}
|
|
42338
44087
|
async function cmdStatus(argv, ctx) {
|
|
42339
44088
|
const tenants = await discoverTenants(ctx.discoverOpts);
|
|
42340
44089
|
const probes = await Promise.all(tenants.map((t) => ctx.probe(t)));
|
|
@@ -42375,6 +44124,7 @@ var init_cli5 = __esm({
|
|
|
42375
44124
|
"src/cli/agents/cli.ts"() {
|
|
42376
44125
|
init_discovery();
|
|
42377
44126
|
init_health();
|
|
44127
|
+
init_loader();
|
|
42378
44128
|
}
|
|
42379
44129
|
});
|
|
42380
44130
|
|
|
@@ -43803,7 +45553,7 @@ async function main() {
|
|
|
43803
45553
|
const code = await runIdentityCommand2({ argv: args.slice(1) });
|
|
43804
45554
|
process.exit(code);
|
|
43805
45555
|
}
|
|
43806
|
-
if (args[0] === "agents") {
|
|
45556
|
+
if (args[0] === "agents" || args[0] === "agent") {
|
|
43807
45557
|
const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
43808
45558
|
const code = await runAgentsCommand2({ argv: args.slice(1) });
|
|
43809
45559
|
process.exit(code);
|