@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.js
CHANGED
|
@@ -4450,9 +4450,35 @@ function validatePolicy(raw) {
|
|
|
4450
4450
|
};
|
|
4451
4451
|
delete merged.auto_deny;
|
|
4452
4452
|
return merged;
|
|
4453
|
-
})()
|
|
4453
|
+
})(),
|
|
4454
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4454
4455
|
};
|
|
4455
4456
|
}
|
|
4457
|
+
function parseApprovalRedirect(raw) {
|
|
4458
|
+
if (raw === void 0 || raw === null) {
|
|
4459
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4460
|
+
}
|
|
4461
|
+
if (typeof raw !== "object") {
|
|
4462
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4463
|
+
}
|
|
4464
|
+
const obj = raw;
|
|
4465
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4466
|
+
const modeRaw = obj.mode;
|
|
4467
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4468
|
+
if (modeRaw !== void 0) {
|
|
4469
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4470
|
+
throw new Error(
|
|
4471
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
mode = modeRaw;
|
|
4475
|
+
}
|
|
4476
|
+
const result = { enabled, mode };
|
|
4477
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4478
|
+
result.per_agent = obj.per_agent;
|
|
4479
|
+
}
|
|
4480
|
+
return result;
|
|
4481
|
+
}
|
|
4456
4482
|
function generateDefaultPolicyYaml() {
|
|
4457
4483
|
return `# Sanctuary Principal Policy v1
|
|
4458
4484
|
# This file controls what your agent can do without asking.
|
|
@@ -4531,6 +4557,7 @@ tier3_always_allow:
|
|
|
4531
4557
|
- handshake_status
|
|
4532
4558
|
- handshake_exchange
|
|
4533
4559
|
- handshake_verify_attestation
|
|
4560
|
+
- handshake_abort
|
|
4534
4561
|
- reputation_query_weighted
|
|
4535
4562
|
- federation_peers
|
|
4536
4563
|
- federation_trust_evaluate
|
|
@@ -4565,6 +4592,21 @@ tier3_always_allow:
|
|
|
4565
4592
|
approval_channel:
|
|
4566
4593
|
type: stderr
|
|
4567
4594
|
timeout_seconds: 300
|
|
4595
|
+
|
|
4596
|
+
# \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
|
|
4597
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4598
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4599
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4600
|
+
#
|
|
4601
|
+
# mode:
|
|
4602
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4603
|
+
# decision from the inbox (default once enabled).
|
|
4604
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4605
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4606
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4607
|
+
approval_redirect:
|
|
4608
|
+
enabled: false
|
|
4609
|
+
mode: replace
|
|
4568
4610
|
`;
|
|
4569
4611
|
}
|
|
4570
4612
|
async function loadPrincipalPolicy(storagePath) {
|
|
@@ -4601,7 +4643,7 @@ async function loadPrincipalPolicy(storagePath) {
|
|
|
4601
4643
|
);
|
|
4602
4644
|
}
|
|
4603
4645
|
}
|
|
4604
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4646
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_APPROVAL_REDIRECT, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4605
4647
|
var init_loader = __esm({
|
|
4606
4648
|
"src/principal-policy/loader.ts"() {
|
|
4607
4649
|
DEFAULT_TIER2 = {
|
|
@@ -4618,6 +4660,10 @@ var init_loader = __esm({
|
|
|
4618
4660
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4619
4661
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4620
4662
|
};
|
|
4663
|
+
DEFAULT_APPROVAL_REDIRECT = {
|
|
4664
|
+
enabled: false,
|
|
4665
|
+
mode: "replace"
|
|
4666
|
+
};
|
|
4621
4667
|
DEFAULT_POLICY = {
|
|
4622
4668
|
version: 1,
|
|
4623
4669
|
tier1_always_approve: [
|
|
@@ -4691,6 +4737,7 @@ var init_loader = __esm({
|
|
|
4691
4737
|
"handshake_status",
|
|
4692
4738
|
"handshake_exchange",
|
|
4693
4739
|
"handshake_verify_attestation",
|
|
4740
|
+
"handshake_abort",
|
|
4694
4741
|
"reputation_query_weighted",
|
|
4695
4742
|
"federation_peers",
|
|
4696
4743
|
"federation_trust_evaluate",
|
|
@@ -4732,7 +4779,8 @@ var init_loader = __esm({
|
|
|
4732
4779
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4733
4780
|
// Read-only; rule-based Annex III classifier
|
|
4734
4781
|
],
|
|
4735
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4782
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4783
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4736
4784
|
};
|
|
4737
4785
|
MalformedPrincipalPolicyError = class extends Error {
|
|
4738
4786
|
constructor(policyPath, reason) {
|
|
@@ -17163,6 +17211,27 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
17163
17211
|
await handleStream2(deps, res);
|
|
17164
17212
|
return true;
|
|
17165
17213
|
}
|
|
17214
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
|
|
17215
|
+
const limit = parseLimit2(
|
|
17216
|
+
url.searchParams.get("limit"),
|
|
17217
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
17218
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
17219
|
+
);
|
|
17220
|
+
const statusRaw = url.searchParams.get("status");
|
|
17221
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
17222
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17223
|
+
const filterStatus = statusRaw && isStatusFilter(statusRaw) && statusRaw !== "pending" ? statusRaw : void 0;
|
|
17224
|
+
const entries = await deps.aggregator.getHistory(
|
|
17225
|
+
{
|
|
17226
|
+
limit,
|
|
17227
|
+
...filterStatus !== void 0 ? { status: filterStatus } : {},
|
|
17228
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
17229
|
+
},
|
|
17230
|
+
operatorId
|
|
17231
|
+
);
|
|
17232
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
17233
|
+
return true;
|
|
17234
|
+
}
|
|
17166
17235
|
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
17167
17236
|
const limit = parseLimit2(
|
|
17168
17237
|
url.searchParams.get("limit"),
|
|
@@ -17185,11 +17254,39 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
17185
17254
|
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17186
17255
|
return true;
|
|
17187
17256
|
}
|
|
17188
|
-
if (method === "GET" && entryMatch.action ===
|
|
17189
|
-
const
|
|
17190
|
-
|
|
17191
|
-
(
|
|
17257
|
+
if (method === "GET" && entryMatch.action === "audit-trail") {
|
|
17258
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
17259
|
+
if (!entry) {
|
|
17260
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17261
|
+
return true;
|
|
17262
|
+
}
|
|
17263
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17264
|
+
const trail = await deps.aggregator.getAuditTrail(
|
|
17265
|
+
entryMatch.aggregatorId,
|
|
17266
|
+
operatorId
|
|
17267
|
+
);
|
|
17268
|
+
writeJSON4(res, 200, { ok: true, data: { entry, audit_trail: trail } });
|
|
17269
|
+
return true;
|
|
17270
|
+
}
|
|
17271
|
+
if (method === "GET" && entryMatch.action === "payload") {
|
|
17272
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
17273
|
+
if (!entry) {
|
|
17274
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17275
|
+
return true;
|
|
17276
|
+
}
|
|
17277
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17278
|
+
const payload = await deps.aggregator.getFullPayloadWithAudit(
|
|
17279
|
+
entryMatch.aggregatorId,
|
|
17280
|
+
operatorId
|
|
17192
17281
|
);
|
|
17282
|
+
writeJSON4(res, 200, {
|
|
17283
|
+
ok: true,
|
|
17284
|
+
data: { entry, request_payload: payload }
|
|
17285
|
+
});
|
|
17286
|
+
return true;
|
|
17287
|
+
}
|
|
17288
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
17289
|
+
const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
|
|
17193
17290
|
if (!entry) {
|
|
17194
17291
|
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17195
17292
|
return true;
|
|
@@ -20223,7 +20320,10 @@ var init_approval_aggregator = __esm({
|
|
|
20223
20320
|
APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
20224
20321
|
AGGREGATED: "cross_harness_approval_aggregated",
|
|
20225
20322
|
RESOLVED: "cross_harness_approval_resolved",
|
|
20226
|
-
DEDUPED: "cross_harness_approval_deduped"
|
|
20323
|
+
DEDUPED: "cross_harness_approval_deduped",
|
|
20324
|
+
PAYLOAD_DECRYPTED: "cross_harness_approval_payload_decrypted",
|
|
20325
|
+
AUDIT_TRAIL_VIEWED: "cross_harness_approval_audit_trail_viewed",
|
|
20326
|
+
REPLAYED: "cross_harness_approval_replayed"
|
|
20227
20327
|
};
|
|
20228
20328
|
DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
20229
20329
|
DEFAULT_MAX_LIST_LIMIT = 200;
|
|
@@ -20239,6 +20339,8 @@ var init_approval_aggregator = __esm({
|
|
|
20239
20339
|
now;
|
|
20240
20340
|
resolveSourceContext;
|
|
20241
20341
|
resolveHubInboxItemId;
|
|
20342
|
+
payloadStore;
|
|
20343
|
+
resolveEnforcementChain;
|
|
20242
20344
|
/** Cached entries by `aggregator_id`. */
|
|
20243
20345
|
entries = /* @__PURE__ */ new Map();
|
|
20244
20346
|
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
@@ -20268,6 +20370,14 @@ var init_approval_aggregator = __esm({
|
|
|
20268
20370
|
source_agent_id: this.fortressId
|
|
20269
20371
|
}));
|
|
20270
20372
|
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
20373
|
+
this.payloadStore = deps.payloadStore ?? null;
|
|
20374
|
+
this.resolveEnforcementChain = deps.resolveEnforcementChain ?? ((event) => [
|
|
20375
|
+
{
|
|
20376
|
+
layer: "l2",
|
|
20377
|
+
event: `approval_required:${event.operation}`,
|
|
20378
|
+
timestamp: event.request_timestamp
|
|
20379
|
+
}
|
|
20380
|
+
]);
|
|
20271
20381
|
}
|
|
20272
20382
|
/**
|
|
20273
20383
|
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
@@ -20318,13 +20428,152 @@ var init_approval_aggregator = __esm({
|
|
|
20318
20428
|
}
|
|
20319
20429
|
/**
|
|
20320
20430
|
* Return the original (unhashed) request payload for the entry. Returns
|
|
20321
|
-
* `null` when the entry is unknown
|
|
20322
|
-
*
|
|
20431
|
+
* `null` when the entry is unknown. When the in-memory payload map has
|
|
20432
|
+
* been evicted (e.g. after a server restart) and a `payloadStore` was
|
|
20433
|
+
* provided, the at-rest bundle is decrypted and the in-memory map is
|
|
20434
|
+
* refilled. Audit emission lives on the `*WithAudit` variant; this base
|
|
20435
|
+
* accessor is silent so internal callers can read without polluting the
|
|
20436
|
+
* audit trail.
|
|
20323
20437
|
*/
|
|
20324
20438
|
async getFullPayload(aggregatorId) {
|
|
20325
20439
|
await this.hydrate();
|
|
20326
20440
|
if (!this.entries.has(aggregatorId)) return null;
|
|
20327
|
-
|
|
20441
|
+
const cached = this.fullPayloads.get(aggregatorId);
|
|
20442
|
+
if (cached !== void 0) return cached;
|
|
20443
|
+
if (this.payloadStore) {
|
|
20444
|
+
try {
|
|
20445
|
+
const restored = await this.payloadStore.loadPayload(aggregatorId);
|
|
20446
|
+
if (restored !== null) {
|
|
20447
|
+
this.fullPayloads.set(aggregatorId, restored);
|
|
20448
|
+
return restored;
|
|
20449
|
+
}
|
|
20450
|
+
} catch {
|
|
20451
|
+
}
|
|
20452
|
+
}
|
|
20453
|
+
return null;
|
|
20454
|
+
}
|
|
20455
|
+
/**
|
|
20456
|
+
* Return the entry record for the given id, or null when unknown.
|
|
20457
|
+
* Idempotent. v1.3 Upsilon-3.
|
|
20458
|
+
*/
|
|
20459
|
+
async getEntry(aggregatorId) {
|
|
20460
|
+
await this.hydrate();
|
|
20461
|
+
return this.entries.get(aggregatorId) ?? null;
|
|
20462
|
+
}
|
|
20463
|
+
/**
|
|
20464
|
+
* Audited variant of `getFullPayload`. Emits the
|
|
20465
|
+
* `cross_harness_approval_payload_decrypted` audit event before
|
|
20466
|
+
* returning. Used by the operator-facing /payload replay route.
|
|
20467
|
+
* v1.3 Upsilon-3.
|
|
20468
|
+
*/
|
|
20469
|
+
async getFullPayloadWithAudit(aggregatorId, operatorId) {
|
|
20470
|
+
const payload = await this.getFullPayload(aggregatorId);
|
|
20471
|
+
if (payload === null) return null;
|
|
20472
|
+
const entry = this.entries.get(aggregatorId);
|
|
20473
|
+
this.auditLog.append(
|
|
20474
|
+
"l2",
|
|
20475
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.PAYLOAD_DECRYPTED,
|
|
20476
|
+
operatorId,
|
|
20477
|
+
{
|
|
20478
|
+
aggregator_id: aggregatorId,
|
|
20479
|
+
...entry ? {
|
|
20480
|
+
source_harness: entry.source_harness,
|
|
20481
|
+
source_agent_id: entry.source_agent_id,
|
|
20482
|
+
entry_status: entry.status
|
|
20483
|
+
} : {}
|
|
20484
|
+
}
|
|
20485
|
+
);
|
|
20486
|
+
return payload;
|
|
20487
|
+
}
|
|
20488
|
+
/**
|
|
20489
|
+
* Return the audit-log entries that led to and surround this approval.
|
|
20490
|
+
* Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
|
|
20491
|
+
* DEDUPED, replay events) all carry `details.aggregator_id` and link
|
|
20492
|
+
* directly. Gate-side emissions (`gate_*:operation`) do not carry the
|
|
20493
|
+
* aggregator id at v1.3, so they are matched via timestamp window
|
|
20494
|
+
* (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
|
|
20495
|
+
* pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
|
|
20496
|
+
* v1.3 Upsilon-3.
|
|
20497
|
+
*/
|
|
20498
|
+
async getAuditTrail(aggregatorId, operatorId) {
|
|
20499
|
+
await this.hydrate();
|
|
20500
|
+
const entry = this.entries.get(aggregatorId);
|
|
20501
|
+
if (!entry) {
|
|
20502
|
+
return [];
|
|
20503
|
+
}
|
|
20504
|
+
const sinceMs = Date.parse(entry.created_at) - 1e3;
|
|
20505
|
+
const sinceIso = new Date(sinceMs).toISOString();
|
|
20506
|
+
const queried = await this.auditLog.query({ since: sinceIso, limit: 1e3 });
|
|
20507
|
+
const operationPart = entry.policy_rule_id.includes(":") ? entry.policy_rule_id.slice(entry.policy_rule_id.indexOf(":") + 1) : entry.policy_rule_id;
|
|
20508
|
+
const lifetimeStart = sinceMs;
|
|
20509
|
+
const lifetimeEnd = entry.resolved_at ? Date.parse(entry.resolved_at) + 1e3 : Date.parse(entry.expires_at) + 1e3;
|
|
20510
|
+
const matches = [];
|
|
20511
|
+
for (const audit of queried.entries) {
|
|
20512
|
+
const detailsId = audit.details !== void 0 ? audit.details["aggregator_id"] : void 0;
|
|
20513
|
+
if (detailsId === aggregatorId) {
|
|
20514
|
+
matches.push(audit);
|
|
20515
|
+
continue;
|
|
20516
|
+
}
|
|
20517
|
+
const auditMs = Date.parse(audit.timestamp);
|
|
20518
|
+
if (auditMs < lifetimeStart || auditMs > lifetimeEnd) continue;
|
|
20519
|
+
if (audit.operation.endsWith(`:${operationPart}`)) {
|
|
20520
|
+
matches.push(audit);
|
|
20521
|
+
}
|
|
20522
|
+
}
|
|
20523
|
+
matches.sort(
|
|
20524
|
+
(a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
|
|
20525
|
+
);
|
|
20526
|
+
this.auditLog.append(
|
|
20527
|
+
"l2",
|
|
20528
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AUDIT_TRAIL_VIEWED,
|
|
20529
|
+
operatorId,
|
|
20530
|
+
{
|
|
20531
|
+
aggregator_id: aggregatorId,
|
|
20532
|
+
entry_status: entry.status,
|
|
20533
|
+
match_count: matches.length
|
|
20534
|
+
}
|
|
20535
|
+
);
|
|
20536
|
+
return matches;
|
|
20537
|
+
}
|
|
20538
|
+
/**
|
|
20539
|
+
* List historical (resolved) approvals. Excludes pending entries by
|
|
20540
|
+
* design: `list()` is the pending-inbox surface and `getHistory()` is
|
|
20541
|
+
* the resolved-replay surface. Emits REPLAYED on each call. v1.3
|
|
20542
|
+
* Upsilon-3.
|
|
20543
|
+
*/
|
|
20544
|
+
async getHistory(opts, operatorId) {
|
|
20545
|
+
await this.hydrate();
|
|
20546
|
+
await this.expireStale();
|
|
20547
|
+
const limit = Math.min(
|
|
20548
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
20549
|
+
this.maxListLimit
|
|
20550
|
+
);
|
|
20551
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
20552
|
+
const matching = [];
|
|
20553
|
+
for (const entry of this.entries.values()) {
|
|
20554
|
+
if (entry.status === "pending") continue;
|
|
20555
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
20556
|
+
const stamp = Date.parse(entry.resolved_at ?? entry.created_at);
|
|
20557
|
+
if (stamp < sinceMs) continue;
|
|
20558
|
+
matching.push(entry);
|
|
20559
|
+
}
|
|
20560
|
+
matching.sort((a, b) => {
|
|
20561
|
+
const aStamp = a.resolved_at ?? a.created_at;
|
|
20562
|
+
const bStamp = b.resolved_at ?? b.created_at;
|
|
20563
|
+
return bStamp.localeCompare(aStamp);
|
|
20564
|
+
});
|
|
20565
|
+
const sliced = matching.slice(0, limit);
|
|
20566
|
+
this.auditLog.append(
|
|
20567
|
+
"l2",
|
|
20568
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.REPLAYED,
|
|
20569
|
+
operatorId,
|
|
20570
|
+
{
|
|
20571
|
+
result_count: sliced.length,
|
|
20572
|
+
...opts?.status !== void 0 ? { status_filter: opts.status } : {},
|
|
20573
|
+
...opts?.sinceTs !== void 0 ? { since: opts.sinceTs } : {}
|
|
20574
|
+
}
|
|
20575
|
+
);
|
|
20576
|
+
return sliced;
|
|
20328
20577
|
}
|
|
20329
20578
|
/**
|
|
20330
20579
|
* Resolve an entry. Used by both:
|
|
@@ -20398,6 +20647,7 @@ var init_approval_aggregator = __esm({
|
|
|
20398
20647
|
const now = this.now();
|
|
20399
20648
|
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
20400
20649
|
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
20650
|
+
const enforcementChain = this.resolveEnforcementChain(event);
|
|
20401
20651
|
const entry = {
|
|
20402
20652
|
aggregator_id: id,
|
|
20403
20653
|
source_harness: ctx.source_harness,
|
|
@@ -20409,13 +20659,20 @@ var init_approval_aggregator = __esm({
|
|
|
20409
20659
|
status: "pending",
|
|
20410
20660
|
created_at: now.toISOString(),
|
|
20411
20661
|
expires_at: expires.toISOString(),
|
|
20412
|
-
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20662
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
|
|
20663
|
+
...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
|
|
20413
20664
|
};
|
|
20414
20665
|
this.entries.set(id, entry);
|
|
20415
20666
|
this.dedupIndex.set(dedupKey, id);
|
|
20416
20667
|
this.correlationIndex.set(event.correlation_id, id);
|
|
20417
20668
|
this.fullPayloads.set(id, event.context);
|
|
20418
20669
|
await this.persist(entry);
|
|
20670
|
+
if (this.payloadStore) {
|
|
20671
|
+
try {
|
|
20672
|
+
await this.payloadStore.savePayload(id, event.context);
|
|
20673
|
+
} catch {
|
|
20674
|
+
}
|
|
20675
|
+
}
|
|
20419
20676
|
this.auditLog.append(
|
|
20420
20677
|
"l2",
|
|
20421
20678
|
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
@@ -20563,6 +20820,317 @@ var init_approval_aggregator = __esm({
|
|
|
20563
20820
|
}
|
|
20564
20821
|
});
|
|
20565
20822
|
|
|
20823
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
20824
|
+
function auditEntryIdFor(request) {
|
|
20825
|
+
return `${request.timestamp}:${request.operation}`;
|
|
20826
|
+
}
|
|
20827
|
+
function statusToDecision(entry) {
|
|
20828
|
+
switch (entry.status) {
|
|
20829
|
+
case "approved":
|
|
20830
|
+
return {
|
|
20831
|
+
decision: "approve",
|
|
20832
|
+
decided_by: "human"
|
|
20833
|
+
};
|
|
20834
|
+
case "denied":
|
|
20835
|
+
return {
|
|
20836
|
+
decision: "deny",
|
|
20837
|
+
decided_by: "human"
|
|
20838
|
+
};
|
|
20839
|
+
case "timeout":
|
|
20840
|
+
case "expired":
|
|
20841
|
+
return {
|
|
20842
|
+
decision: "deny",
|
|
20843
|
+
decided_by: "timeout"
|
|
20844
|
+
};
|
|
20845
|
+
default:
|
|
20846
|
+
return null;
|
|
20847
|
+
}
|
|
20848
|
+
}
|
|
20849
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
20850
|
+
return (_request) => {
|
|
20851
|
+
const cfg = supplier().approval_redirect;
|
|
20852
|
+
if (!cfg || cfg.enabled !== true) {
|
|
20853
|
+
return { enabled: false, mode: "replace" };
|
|
20854
|
+
}
|
|
20855
|
+
return {
|
|
20856
|
+
enabled: true,
|
|
20857
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
20858
|
+
};
|
|
20859
|
+
};
|
|
20860
|
+
}
|
|
20861
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS, AggregatorBackedChannel;
|
|
20862
|
+
var init_aggregator_backed_channel = __esm({
|
|
20863
|
+
"src/principal-policy/channels/aggregator-backed-channel.ts"() {
|
|
20864
|
+
DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
20865
|
+
AggregatorBackedChannel = class {
|
|
20866
|
+
underlying;
|
|
20867
|
+
aggregator;
|
|
20868
|
+
resolveRedirect;
|
|
20869
|
+
replaceModeTimeoutMs;
|
|
20870
|
+
now;
|
|
20871
|
+
constructor(opts) {
|
|
20872
|
+
this.underlying = opts.underlying;
|
|
20873
|
+
this.aggregator = opts.aggregator;
|
|
20874
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
20875
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
20876
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
20877
|
+
}
|
|
20878
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
20879
|
+
getUnderlying() {
|
|
20880
|
+
return this.underlying;
|
|
20881
|
+
}
|
|
20882
|
+
async requestApproval(request) {
|
|
20883
|
+
const cfg = this.resolveRedirect(request);
|
|
20884
|
+
if (!cfg.enabled) {
|
|
20885
|
+
return this.underlying.requestApproval(request);
|
|
20886
|
+
}
|
|
20887
|
+
if (cfg.mode === "replace") {
|
|
20888
|
+
return this.awaitAggregatorDecision(request);
|
|
20889
|
+
}
|
|
20890
|
+
return this.notifyMode(request);
|
|
20891
|
+
}
|
|
20892
|
+
/**
|
|
20893
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
20894
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
20895
|
+
* between list and subscribe). Match incoming events to this request by
|
|
20896
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
20897
|
+
*/
|
|
20898
|
+
async awaitAggregatorDecision(request) {
|
|
20899
|
+
const auditId = auditEntryIdFor(request);
|
|
20900
|
+
return new Promise((resolveOuter) => {
|
|
20901
|
+
let settled = false;
|
|
20902
|
+
let unsubscribe = null;
|
|
20903
|
+
let timeoutHandle = null;
|
|
20904
|
+
const settle = (response) => {
|
|
20905
|
+
if (settled) return;
|
|
20906
|
+
settled = true;
|
|
20907
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
20908
|
+
if (unsubscribe) {
|
|
20909
|
+
try {
|
|
20910
|
+
unsubscribe();
|
|
20911
|
+
} catch {
|
|
20912
|
+
}
|
|
20913
|
+
}
|
|
20914
|
+
resolveOuter(response);
|
|
20915
|
+
};
|
|
20916
|
+
const onEvent = (emit) => {
|
|
20917
|
+
if (emit.type !== "resolved") return;
|
|
20918
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
20919
|
+
const mapped = statusToDecision(emit.entry);
|
|
20920
|
+
if (!mapped) return;
|
|
20921
|
+
settle({
|
|
20922
|
+
decision: mapped.decision,
|
|
20923
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
20924
|
+
decided_by: mapped.decided_by
|
|
20925
|
+
});
|
|
20926
|
+
};
|
|
20927
|
+
try {
|
|
20928
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
20929
|
+
} catch (err) {
|
|
20930
|
+
settle({
|
|
20931
|
+
decision: "deny",
|
|
20932
|
+
decided_at: this.now().toISOString(),
|
|
20933
|
+
decided_by: "channel_failure"
|
|
20934
|
+
});
|
|
20935
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
20936
|
+
}
|
|
20937
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
20938
|
+
for (const entry of entries) {
|
|
20939
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
20940
|
+
const mapped = statusToDecision(entry);
|
|
20941
|
+
if (!mapped) return;
|
|
20942
|
+
settle({
|
|
20943
|
+
decision: mapped.decision,
|
|
20944
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
20945
|
+
decided_by: mapped.decided_by
|
|
20946
|
+
});
|
|
20947
|
+
return;
|
|
20948
|
+
}
|
|
20949
|
+
}).catch(() => {
|
|
20950
|
+
});
|
|
20951
|
+
timeoutHandle = setTimeout(() => {
|
|
20952
|
+
settle({
|
|
20953
|
+
decision: "deny",
|
|
20954
|
+
decided_at: this.now().toISOString(),
|
|
20955
|
+
decided_by: "timeout"
|
|
20956
|
+
});
|
|
20957
|
+
}, this.replaceModeTimeoutMs);
|
|
20958
|
+
});
|
|
20959
|
+
}
|
|
20960
|
+
/**
|
|
20961
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
20962
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
20963
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
20964
|
+
* downstream audit logging is unchanged.
|
|
20965
|
+
*
|
|
20966
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
20967
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
20968
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
20969
|
+
*/
|
|
20970
|
+
async notifyMode(request) {
|
|
20971
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
20972
|
+
let underlyingPromise;
|
|
20973
|
+
try {
|
|
20974
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
20975
|
+
} catch (err) {
|
|
20976
|
+
const response = await aggregatorPromise;
|
|
20977
|
+
return response;
|
|
20978
|
+
}
|
|
20979
|
+
return Promise.race([
|
|
20980
|
+
aggregatorPromise,
|
|
20981
|
+
underlyingPromise.catch(
|
|
20982
|
+
() => new Promise(() => {
|
|
20983
|
+
})
|
|
20984
|
+
)
|
|
20985
|
+
]);
|
|
20986
|
+
}
|
|
20987
|
+
};
|
|
20988
|
+
}
|
|
20989
|
+
});
|
|
20990
|
+
|
|
20991
|
+
// src/principal-policy/aggregator-store.ts
|
|
20992
|
+
function payloadKey(aggregatorId) {
|
|
20993
|
+
return `${AGGREGATOR_PAYLOAD_KEY_PREFIX}${aggregatorId}`;
|
|
20994
|
+
}
|
|
20995
|
+
function stripKeyPrefix(key) {
|
|
20996
|
+
if (!key.startsWith(AGGREGATOR_PAYLOAD_KEY_PREFIX)) return null;
|
|
20997
|
+
return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
|
|
20998
|
+
}
|
|
20999
|
+
var AGGREGATOR_PAYLOAD_NAMESPACE, AGGREGATOR_PAYLOAD_KEY_PREFIX, HKDF_INFO, DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS, MAX_BUNDLE_BYTES2, AggregatorPayloadStore;
|
|
21000
|
+
var init_aggregator_store = __esm({
|
|
21001
|
+
"src/principal-policy/aggregator-store.ts"() {
|
|
21002
|
+
init_encryption();
|
|
21003
|
+
init_key_derivation();
|
|
21004
|
+
init_encoding();
|
|
21005
|
+
AGGREGATOR_PAYLOAD_NAMESPACE = "_approval_aggregator_payloads";
|
|
21006
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX = "payload.";
|
|
21007
|
+
HKDF_INFO = "l2-approval-aggregator-payload-v1";
|
|
21008
|
+
DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS = 30;
|
|
21009
|
+
MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
21010
|
+
AggregatorPayloadStore = class {
|
|
21011
|
+
storage;
|
|
21012
|
+
encryptionKey;
|
|
21013
|
+
fortressId;
|
|
21014
|
+
retentionDays;
|
|
21015
|
+
constructor(opts) {
|
|
21016
|
+
this.storage = opts.storage;
|
|
21017
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO);
|
|
21018
|
+
this.fortressId = opts.fortressId;
|
|
21019
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS;
|
|
21020
|
+
}
|
|
21021
|
+
/**
|
|
21022
|
+
* Persist `payload` under the given aggregator_id. Idempotent; calling
|
|
21023
|
+
* twice with the same id rewrites the bundle (retention_until is
|
|
21024
|
+
* recomputed). Returns the bundle's retention_until ISO-8601 timestamp
|
|
21025
|
+
* so callers can log it.
|
|
21026
|
+
*/
|
|
21027
|
+
async savePayload(aggregatorId, payload) {
|
|
21028
|
+
const now = /* @__PURE__ */ new Date();
|
|
21029
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
21030
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
21031
|
+
const bundle = {
|
|
21032
|
+
version: 1,
|
|
21033
|
+
aggregator_id: aggregatorId,
|
|
21034
|
+
fortress_id: this.fortressId,
|
|
21035
|
+
created_at: now.toISOString(),
|
|
21036
|
+
retention_until: retentionUntil.toISOString(),
|
|
21037
|
+
payload
|
|
21038
|
+
};
|
|
21039
|
+
const aad = stringToBytes(aggregatorId);
|
|
21040
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
21041
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
21042
|
+
await this.storage.write(
|
|
21043
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21044
|
+
payloadKey(aggregatorId),
|
|
21045
|
+
stringToBytes(JSON.stringify(envelope))
|
|
21046
|
+
);
|
|
21047
|
+
return bundle.retention_until;
|
|
21048
|
+
}
|
|
21049
|
+
/**
|
|
21050
|
+
* Read the persisted payload for the aggregator_id. Returns null if no
|
|
21051
|
+
* bundle exists, the bundle is corrupted, or AAD binding fails.
|
|
21052
|
+
*/
|
|
21053
|
+
async loadPayload(aggregatorId) {
|
|
21054
|
+
const key = payloadKey(aggregatorId);
|
|
21055
|
+
let raw;
|
|
21056
|
+
try {
|
|
21057
|
+
raw = await this.storage.read(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
21058
|
+
} catch {
|
|
21059
|
+
return null;
|
|
21060
|
+
}
|
|
21061
|
+
if (!raw) return null;
|
|
21062
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
21063
|
+
try {
|
|
21064
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21065
|
+
const aad = stringToBytes(aggregatorId);
|
|
21066
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21067
|
+
const parsed = JSON.parse(
|
|
21068
|
+
bytesToString(plaintext)
|
|
21069
|
+
);
|
|
21070
|
+
if (parsed.version !== 1) return null;
|
|
21071
|
+
if (parsed.aggregator_id !== aggregatorId) return null;
|
|
21072
|
+
return parsed.payload;
|
|
21073
|
+
} catch {
|
|
21074
|
+
return null;
|
|
21075
|
+
}
|
|
21076
|
+
}
|
|
21077
|
+
/**
|
|
21078
|
+
* Delete the persisted payload. Returns true when a bundle was removed,
|
|
21079
|
+
* false when none existed.
|
|
21080
|
+
*/
|
|
21081
|
+
async deletePayload(aggregatorId) {
|
|
21082
|
+
const key = payloadKey(aggregatorId);
|
|
21083
|
+
const existed = await this.storage.exists(
|
|
21084
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21085
|
+
key
|
|
21086
|
+
);
|
|
21087
|
+
if (!existed) return false;
|
|
21088
|
+
try {
|
|
21089
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, key);
|
|
21090
|
+
} catch {
|
|
21091
|
+
return false;
|
|
21092
|
+
}
|
|
21093
|
+
return true;
|
|
21094
|
+
}
|
|
21095
|
+
/**
|
|
21096
|
+
* Drop expired payload bundles. Returns the count of bundles pruned.
|
|
21097
|
+
* Caller wires this into the cocoon-unlock initialization path.
|
|
21098
|
+
*/
|
|
21099
|
+
async pruneExpired(now) {
|
|
21100
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
21101
|
+
const entries = await this.storage.list(
|
|
21102
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21103
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX
|
|
21104
|
+
);
|
|
21105
|
+
let pruned = 0;
|
|
21106
|
+
for (const meta of entries) {
|
|
21107
|
+
const aggregatorId = stripKeyPrefix(meta.key);
|
|
21108
|
+
if (aggregatorId === null) continue;
|
|
21109
|
+
const raw = await this.storage.read(
|
|
21110
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21111
|
+
meta.key
|
|
21112
|
+
);
|
|
21113
|
+
if (!raw) continue;
|
|
21114
|
+
try {
|
|
21115
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21116
|
+
const aad = stringToBytes(aggregatorId);
|
|
21117
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21118
|
+
const parsed = JSON.parse(
|
|
21119
|
+
bytesToString(plaintext)
|
|
21120
|
+
);
|
|
21121
|
+
if (parsed.retention_until <= cutoff) {
|
|
21122
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
|
|
21123
|
+
pruned += 1;
|
|
21124
|
+
}
|
|
21125
|
+
} catch {
|
|
21126
|
+
}
|
|
21127
|
+
}
|
|
21128
|
+
return { pruned };
|
|
21129
|
+
}
|
|
21130
|
+
};
|
|
21131
|
+
}
|
|
21132
|
+
});
|
|
21133
|
+
|
|
20566
21134
|
// src/principal-policy/tools.ts
|
|
20567
21135
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
20568
21136
|
return [
|
|
@@ -21466,6 +22034,76 @@ var init_attestation = __esm({
|
|
|
21466
22034
|
}
|
|
21467
22035
|
});
|
|
21468
22036
|
|
|
22037
|
+
// src/handshake/audit.ts
|
|
22038
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
22039
|
+
auditLog.append(
|
|
22040
|
+
"l4",
|
|
22041
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
22042
|
+
ctx.identity_id,
|
|
22043
|
+
detailsFromContext(ctx),
|
|
22044
|
+
"success"
|
|
22045
|
+
);
|
|
22046
|
+
}
|
|
22047
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
22048
|
+
const details = detailsFromContext(ctx);
|
|
22049
|
+
if (ctx.trust_tier !== void 0) {
|
|
22050
|
+
details.trust_tier = ctx.trust_tier;
|
|
22051
|
+
}
|
|
22052
|
+
auditLog.append(
|
|
22053
|
+
"l4",
|
|
22054
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
22055
|
+
ctx.identity_id,
|
|
22056
|
+
details,
|
|
22057
|
+
"success"
|
|
22058
|
+
);
|
|
22059
|
+
}
|
|
22060
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
22061
|
+
const details = detailsFromContext(ctx);
|
|
22062
|
+
details.reason = ctx.reason;
|
|
22063
|
+
if (ctx.error !== void 0) {
|
|
22064
|
+
details.error = ctx.error;
|
|
22065
|
+
}
|
|
22066
|
+
auditLog.append(
|
|
22067
|
+
"l4",
|
|
22068
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
22069
|
+
ctx.identity_id,
|
|
22070
|
+
details,
|
|
22071
|
+
"failure"
|
|
22072
|
+
);
|
|
22073
|
+
}
|
|
22074
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
22075
|
+
const details = detailsFromContext(ctx);
|
|
22076
|
+
details.reason = ctx.reason;
|
|
22077
|
+
auditLog.append(
|
|
22078
|
+
"l4",
|
|
22079
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
22080
|
+
ctx.identity_id,
|
|
22081
|
+
details,
|
|
22082
|
+
"failure"
|
|
22083
|
+
);
|
|
22084
|
+
}
|
|
22085
|
+
function detailsFromContext(ctx) {
|
|
22086
|
+
const details = {
|
|
22087
|
+
session_id: ctx.session_id,
|
|
22088
|
+
role: ctx.role
|
|
22089
|
+
};
|
|
22090
|
+
if (ctx.counterparty_id !== void 0) {
|
|
22091
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
22092
|
+
}
|
|
22093
|
+
return details;
|
|
22094
|
+
}
|
|
22095
|
+
var HANDSHAKE_LIFECYCLE_OPS;
|
|
22096
|
+
var init_audit = __esm({
|
|
22097
|
+
"src/handshake/audit.ts"() {
|
|
22098
|
+
HANDSHAKE_LIFECYCLE_OPS = {
|
|
22099
|
+
INITIATED: "handshake_initiated",
|
|
22100
|
+
COMPLETED: "handshake_completed",
|
|
22101
|
+
FAILED: "handshake_failed",
|
|
22102
|
+
ABORTED: "handshake_aborted"
|
|
22103
|
+
};
|
|
22104
|
+
}
|
|
22105
|
+
});
|
|
22106
|
+
|
|
21469
22107
|
// src/handshake/tools.ts
|
|
21470
22108
|
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
21471
22109
|
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
@@ -21499,6 +22137,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21499
22137
|
const { challenge, session } = initiateHandshake(shr);
|
|
21500
22138
|
sessions.set(session.session_id, session);
|
|
21501
22139
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
22140
|
+
auditHandshakeInitiated(auditLog, {
|
|
22141
|
+
session_id: session.session_id,
|
|
22142
|
+
role: "initiator",
|
|
22143
|
+
identity_id: shr.body.instance_id
|
|
22144
|
+
});
|
|
21502
22145
|
return toolResult({
|
|
21503
22146
|
session_id: session.session_id,
|
|
21504
22147
|
challenge,
|
|
@@ -21538,10 +22181,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21538
22181
|
);
|
|
21539
22182
|
if ("error" in result) {
|
|
21540
22183
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
22184
|
+
auditHandshakeFailed(auditLog, {
|
|
22185
|
+
session_id: "unknown",
|
|
22186
|
+
role: "responder",
|
|
22187
|
+
identity_id: shr.body.instance_id,
|
|
22188
|
+
reason: classifyRespondFailure(result.error),
|
|
22189
|
+
error: result.error
|
|
22190
|
+
});
|
|
21541
22191
|
return toolResult({ error: result.error });
|
|
21542
22192
|
}
|
|
21543
22193
|
sessions.set(result.session.session_id, result.session);
|
|
21544
22194
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
22195
|
+
auditHandshakeInitiated(auditLog, {
|
|
22196
|
+
session_id: result.session.session_id,
|
|
22197
|
+
role: "responder",
|
|
22198
|
+
identity_id: shr.body.instance_id,
|
|
22199
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
22200
|
+
});
|
|
21545
22201
|
let autoPublishResult;
|
|
21546
22202
|
if (autoPublishHandshakes) {
|
|
21547
22203
|
autoPublishResult = { attempted: true };
|
|
@@ -21649,9 +22305,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21649
22305
|
const response = args.response;
|
|
21650
22306
|
const session = sessions.get(sessionId);
|
|
21651
22307
|
if (!session) {
|
|
22308
|
+
auditHandshakeFailed(auditLog, {
|
|
22309
|
+
session_id: sessionId,
|
|
22310
|
+
role: "initiator",
|
|
22311
|
+
identity_id: "unknown",
|
|
22312
|
+
reason: "session_unknown",
|
|
22313
|
+
error: `No handshake session found: ${sessionId}`
|
|
22314
|
+
});
|
|
21652
22315
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21653
22316
|
}
|
|
21654
22317
|
if (session.state !== "initiated") {
|
|
22318
|
+
auditHandshakeFailed(auditLog, {
|
|
22319
|
+
session_id: sessionId,
|
|
22320
|
+
role: "initiator",
|
|
22321
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22322
|
+
reason: "session_state_mismatch",
|
|
22323
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
22324
|
+
});
|
|
21655
22325
|
return toolResult({
|
|
21656
22326
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21657
22327
|
});
|
|
@@ -21665,6 +22335,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21665
22335
|
if ("error" in result) {
|
|
21666
22336
|
session.state = "failed";
|
|
21667
22337
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
22338
|
+
auditHandshakeFailed(auditLog, {
|
|
22339
|
+
session_id: sessionId,
|
|
22340
|
+
role: "initiator",
|
|
22341
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22342
|
+
reason: classifyCompleteFailure(result.error),
|
|
22343
|
+
error: result.error
|
|
22344
|
+
});
|
|
21668
22345
|
return toolResult({ error: result.error });
|
|
21669
22346
|
}
|
|
21670
22347
|
session.state = "completed";
|
|
@@ -21673,6 +22350,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21673
22350
|
session.result = result.result;
|
|
21674
22351
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
21675
22352
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
22353
|
+
auditHandshakeCompleted(auditLog, {
|
|
22354
|
+
session_id: sessionId,
|
|
22355
|
+
role: "initiator",
|
|
22356
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22357
|
+
counterparty_id: result.result.counterparty_id,
|
|
22358
|
+
trust_tier: result.result.trust_tier
|
|
22359
|
+
});
|
|
21676
22360
|
return toolResult({
|
|
21677
22361
|
completion: result.completion,
|
|
21678
22362
|
result: result.result,
|
|
@@ -21720,6 +22404,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21720
22404
|
void 0,
|
|
21721
22405
|
result.verified ? "success" : "failure"
|
|
21722
22406
|
);
|
|
22407
|
+
if (result.verified) {
|
|
22408
|
+
auditHandshakeCompleted(auditLog, {
|
|
22409
|
+
session_id: session.session_id,
|
|
22410
|
+
role: "responder",
|
|
22411
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22412
|
+
counterparty_id: result.counterparty_id,
|
|
22413
|
+
trust_tier: result.trust_tier
|
|
22414
|
+
});
|
|
22415
|
+
} else {
|
|
22416
|
+
auditHandshakeFailed(auditLog, {
|
|
22417
|
+
session_id: session.session_id,
|
|
22418
|
+
role: "responder",
|
|
22419
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22420
|
+
counterparty_id: result.counterparty_id,
|
|
22421
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
22422
|
+
error: result.errors.join("; ")
|
|
22423
|
+
});
|
|
22424
|
+
}
|
|
21723
22425
|
return toolResult({ result });
|
|
21724
22426
|
}
|
|
21725
22427
|
return toolResult({
|
|
@@ -21827,10 +22529,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21827
22529
|
_content_trust: "external"
|
|
21828
22530
|
});
|
|
21829
22531
|
}
|
|
22532
|
+
},
|
|
22533
|
+
{
|
|
22534
|
+
name: "handshake_abort",
|
|
22535
|
+
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.",
|
|
22536
|
+
inputSchema: {
|
|
22537
|
+
type: "object",
|
|
22538
|
+
properties: {
|
|
22539
|
+
session_id: {
|
|
22540
|
+
type: "string",
|
|
22541
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
22542
|
+
},
|
|
22543
|
+
reason: {
|
|
22544
|
+
type: "string",
|
|
22545
|
+
enum: [
|
|
22546
|
+
"operator_cancelled",
|
|
22547
|
+
"session_timeout",
|
|
22548
|
+
"transport_dropped",
|
|
22549
|
+
"shutdown",
|
|
22550
|
+
"other"
|
|
22551
|
+
],
|
|
22552
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
22553
|
+
}
|
|
22554
|
+
},
|
|
22555
|
+
required: ["session_id"]
|
|
22556
|
+
},
|
|
22557
|
+
handler: async (args) => {
|
|
22558
|
+
const sessionId = args.session_id;
|
|
22559
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
22560
|
+
const session = sessions.get(sessionId);
|
|
22561
|
+
if (!session) {
|
|
22562
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
22563
|
+
}
|
|
22564
|
+
if (session.state === "completed") {
|
|
22565
|
+
return toolResult({
|
|
22566
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
22567
|
+
});
|
|
22568
|
+
}
|
|
22569
|
+
sessions.delete(sessionId);
|
|
22570
|
+
auditHandshakeAborted(auditLog, {
|
|
22571
|
+
session_id: sessionId,
|
|
22572
|
+
role: session.role,
|
|
22573
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22574
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
22575
|
+
reason
|
|
22576
|
+
});
|
|
22577
|
+
return toolResult({
|
|
22578
|
+
aborted: true,
|
|
22579
|
+
session_id: sessionId,
|
|
22580
|
+
reason
|
|
22581
|
+
});
|
|
22582
|
+
}
|
|
21830
22583
|
}
|
|
21831
22584
|
];
|
|
21832
22585
|
return { tools, handshakeResults };
|
|
21833
22586
|
}
|
|
22587
|
+
function classifyRespondFailure(error) {
|
|
22588
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22589
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
22590
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22591
|
+
return "other";
|
|
22592
|
+
}
|
|
22593
|
+
function classifyCompleteFailure(error) {
|
|
22594
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22595
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
22596
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
22597
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22598
|
+
return "other";
|
|
22599
|
+
}
|
|
21834
22600
|
var init_tools6 = __esm({
|
|
21835
22601
|
"src/handshake/tools.ts"() {
|
|
21836
22602
|
init_router();
|
|
@@ -21840,6 +22606,7 @@ var init_tools6 = __esm({
|
|
|
21840
22606
|
init_encoding();
|
|
21841
22607
|
init_protocol();
|
|
21842
22608
|
init_attestation();
|
|
22609
|
+
init_audit();
|
|
21843
22610
|
init_verifier();
|
|
21844
22611
|
}
|
|
21845
22612
|
});
|
|
@@ -32975,7 +33742,21 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32975
33742
|
* successful thread removal. Body carries thread_id + turn_count of
|
|
32976
33743
|
* the deleted bundle.
|
|
32977
33744
|
*/
|
|
32978
|
-
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
33745
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
33746
|
+
/**
|
|
33747
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
33748
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
33749
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
33750
|
+
* carries thread_id + a stable failure_reason enum.
|
|
33751
|
+
*/
|
|
33752
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed",
|
|
33753
|
+
/**
|
|
33754
|
+
* Concierge dynamic-context fetcher failed (WP-V1.3-9 Tau-3). Emitted
|
|
33755
|
+
* when a category fetcher throws while assembling the dynamic context
|
|
33756
|
+
* fold. The concierge omits that category and continues; the user-
|
|
33757
|
+
* facing query is never broken. Body carries category + failure_reason.
|
|
33758
|
+
*/
|
|
33759
|
+
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
|
|
32979
33760
|
};
|
|
32980
33761
|
}
|
|
32981
33762
|
});
|
|
@@ -32988,20 +33769,312 @@ var init_operator_chat_types = __esm({
|
|
|
32988
33769
|
CONCIERGE_THREAD_KEY = "_fortress";
|
|
32989
33770
|
}
|
|
32990
33771
|
});
|
|
33772
|
+
|
|
33773
|
+
// src/chat/concierge-context-router.ts
|
|
33774
|
+
function phrasePattern(phrase) {
|
|
33775
|
+
const escaped = phrase.toLowerCase().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
33776
|
+
return { source: `\\b${escaped}\\b`, phrase };
|
|
33777
|
+
}
|
|
33778
|
+
function extractAgentNameHint(query) {
|
|
33779
|
+
const agentPattern = /\bagent\s+["']?([A-Za-z][\w-]{0,40})["']?/i;
|
|
33780
|
+
const m = query.match(agentPattern);
|
|
33781
|
+
if (m && m[1]) return m[1];
|
|
33782
|
+
const quoted = query.match(/["']([A-Za-z][\w-]{0,40})["']/);
|
|
33783
|
+
if (quoted && quoted[1]) return quoted[1];
|
|
33784
|
+
return null;
|
|
33785
|
+
}
|
|
33786
|
+
function isTrivialQuery(query) {
|
|
33787
|
+
const norm = query.trim().toLowerCase();
|
|
33788
|
+
if (norm.length === 0) return true;
|
|
33789
|
+
if (norm.length < 8) return true;
|
|
33790
|
+
return TRIVIAL_GREETINGS.has(norm);
|
|
33791
|
+
}
|
|
33792
|
+
function classifyQuery(query) {
|
|
33793
|
+
const normalized = query.toLowerCase();
|
|
33794
|
+
const matches = [];
|
|
33795
|
+
for (const spec of CATEGORY_KEYWORDS) {
|
|
33796
|
+
const matchedPhrases = [];
|
|
33797
|
+
for (const pattern of spec.patterns) {
|
|
33798
|
+
if (matchedPhrases.includes(pattern.phrase)) continue;
|
|
33799
|
+
const re = new RegExp(pattern.source, "i");
|
|
33800
|
+
if (re.test(normalized)) {
|
|
33801
|
+
matchedPhrases.push(pattern.phrase);
|
|
33802
|
+
}
|
|
33803
|
+
}
|
|
33804
|
+
if (matchedPhrases.length === 0) continue;
|
|
33805
|
+
const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
|
|
33806
|
+
matches.push({
|
|
33807
|
+
category: spec.category,
|
|
33808
|
+
confidence,
|
|
33809
|
+
matched_keywords: matchedPhrases,
|
|
33810
|
+
agent_name_hint: spec.category === "agent_state" || spec.category === "agent_activity" ? extractAgentNameHint(query) : null
|
|
33811
|
+
});
|
|
33812
|
+
}
|
|
33813
|
+
matches.sort((a, b) => {
|
|
33814
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
33815
|
+
return CONTEXT_CATEGORIES.indexOf(a.category) - CONTEXT_CATEGORIES.indexOf(b.category);
|
|
33816
|
+
});
|
|
33817
|
+
return matches;
|
|
33818
|
+
}
|
|
33819
|
+
function approxTokenLen(text) {
|
|
33820
|
+
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
33821
|
+
}
|
|
33822
|
+
async function runFetcher(match, fetchers) {
|
|
33823
|
+
switch (match.category) {
|
|
33824
|
+
case "templates":
|
|
33825
|
+
return fetchers.templates();
|
|
33826
|
+
case "agent_state":
|
|
33827
|
+
return fetchers.agent_state(match.agent_name_hint);
|
|
33828
|
+
case "agent_activity":
|
|
33829
|
+
return fetchers.agent_activity(match.agent_name_hint);
|
|
33830
|
+
case "audit_log":
|
|
33831
|
+
return fetchers.audit_log();
|
|
33832
|
+
case "sentinel_findings":
|
|
33833
|
+
return fetchers.sentinel_findings();
|
|
33834
|
+
case "anomaly_alerts":
|
|
33835
|
+
return fetchers.anomaly_alerts();
|
|
33836
|
+
case "recent_receipts":
|
|
33837
|
+
return fetchers.recent_receipts();
|
|
33838
|
+
case "verascore_deltas":
|
|
33839
|
+
return fetchers.verascore_deltas();
|
|
33840
|
+
}
|
|
33841
|
+
}
|
|
33842
|
+
function trivialMatch(category) {
|
|
33843
|
+
return {
|
|
33844
|
+
category,
|
|
33845
|
+
confidence: 0.5,
|
|
33846
|
+
matched_keywords: ["llm-assist"],
|
|
33847
|
+
agent_name_hint: null
|
|
33848
|
+
};
|
|
33849
|
+
}
|
|
33850
|
+
async function foldContext(query, fetchers, opts) {
|
|
33851
|
+
const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
|
|
33852
|
+
let matches = classifyQuery(query);
|
|
33853
|
+
if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
|
|
33854
|
+
try {
|
|
33855
|
+
const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
|
|
33856
|
+
if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
|
|
33857
|
+
matches = [trivialMatch(picked)];
|
|
33858
|
+
}
|
|
33859
|
+
} catch {
|
|
33860
|
+
}
|
|
33861
|
+
}
|
|
33862
|
+
if (matches.length === 0) {
|
|
33863
|
+
return { section: "", categoriesIncluded: [] };
|
|
33864
|
+
}
|
|
33865
|
+
const attempts = [];
|
|
33866
|
+
for (const match of matches) {
|
|
33867
|
+
try {
|
|
33868
|
+
const text = await runFetcher(match, fetchers);
|
|
33869
|
+
const trimmed = text.trim();
|
|
33870
|
+
if (trimmed.length > 0) {
|
|
33871
|
+
attempts.push({ category: match.category, text: trimmed });
|
|
33872
|
+
}
|
|
33873
|
+
} catch (err) {
|
|
33874
|
+
opts?.onFetcherFailure?.(match.category, err);
|
|
33875
|
+
}
|
|
33876
|
+
}
|
|
33877
|
+
if (attempts.length === 0) {
|
|
33878
|
+
return { section: "", categoriesIncluded: [] };
|
|
33879
|
+
}
|
|
33880
|
+
const headerTokens = approxTokenLen(`${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
33881
|
+
`);
|
|
33882
|
+
const sepTokens = approxTokenLen("\n\n");
|
|
33883
|
+
let runningTokens = headerTokens;
|
|
33884
|
+
const kept = [];
|
|
33885
|
+
for (const attempt of attempts) {
|
|
33886
|
+
const block = `### ${CATEGORY_LABELS[attempt.category]}
|
|
33887
|
+
${attempt.text}`;
|
|
33888
|
+
const tokens = approxTokenLen(block) + (kept.length > 0 ? sepTokens : 0);
|
|
33889
|
+
if (kept.length === 0) {
|
|
33890
|
+
kept.push(attempt);
|
|
33891
|
+
runningTokens += tokens;
|
|
33892
|
+
continue;
|
|
33893
|
+
}
|
|
33894
|
+
if (runningTokens + tokens > budget) break;
|
|
33895
|
+
kept.push(attempt);
|
|
33896
|
+
runningTokens += tokens;
|
|
33897
|
+
}
|
|
33898
|
+
const blocks = kept.map(
|
|
33899
|
+
(k) => `### ${CATEGORY_LABELS[k.category]}
|
|
33900
|
+
${k.text}`
|
|
33901
|
+
);
|
|
33902
|
+
const section = `${DYNAMIC_CONTEXT_SECTION_HEADER}
|
|
33903
|
+
${blocks.join("\n\n")}`;
|
|
33904
|
+
return {
|
|
33905
|
+
section,
|
|
33906
|
+
categoriesIncluded: kept.map((k) => k.category)
|
|
33907
|
+
};
|
|
33908
|
+
}
|
|
33909
|
+
var APPROX_CHARS_PER_TOKEN, DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET, DYNAMIC_CONTEXT_SECTION_HEADER, CONTEXT_CATEGORIES, CATEGORY_KEYWORDS, TRIVIAL_GREETINGS, CATEGORY_LABELS;
|
|
33910
|
+
var init_concierge_context_router = __esm({
|
|
33911
|
+
"src/chat/concierge-context-router.ts"() {
|
|
33912
|
+
APPROX_CHARS_PER_TOKEN = 4;
|
|
33913
|
+
DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET = 2e3;
|
|
33914
|
+
DYNAMIC_CONTEXT_SECTION_HEADER = "## Live fortress context";
|
|
33915
|
+
CONTEXT_CATEGORIES = [
|
|
33916
|
+
"templates",
|
|
33917
|
+
"agent_state",
|
|
33918
|
+
"agent_activity",
|
|
33919
|
+
"audit_log",
|
|
33920
|
+
"sentinel_findings",
|
|
33921
|
+
"anomaly_alerts",
|
|
33922
|
+
"recent_receipts",
|
|
33923
|
+
"verascore_deltas"
|
|
33924
|
+
];
|
|
33925
|
+
CATEGORY_KEYWORDS = [
|
|
33926
|
+
{
|
|
33927
|
+
category: "templates",
|
|
33928
|
+
patterns: [
|
|
33929
|
+
"templates",
|
|
33930
|
+
"template",
|
|
33931
|
+
"channel templates",
|
|
33932
|
+
"channel template",
|
|
33933
|
+
"list templates",
|
|
33934
|
+
"available templates",
|
|
33935
|
+
"what templates"
|
|
33936
|
+
].map(phrasePattern)
|
|
33937
|
+
},
|
|
33938
|
+
{
|
|
33939
|
+
category: "agent_state",
|
|
33940
|
+
patterns: [
|
|
33941
|
+
"state",
|
|
33942
|
+
"status",
|
|
33943
|
+
"agent state",
|
|
33944
|
+
"agent status",
|
|
33945
|
+
"status of agent",
|
|
33946
|
+
"status of agents",
|
|
33947
|
+
"state of",
|
|
33948
|
+
"doing"
|
|
33949
|
+
].map(phrasePattern)
|
|
33950
|
+
},
|
|
33951
|
+
{
|
|
33952
|
+
category: "agent_activity",
|
|
33953
|
+
patterns: [
|
|
33954
|
+
"activity",
|
|
33955
|
+
"agent activity",
|
|
33956
|
+
"what did",
|
|
33957
|
+
"recent activity"
|
|
33958
|
+
].map(phrasePattern)
|
|
33959
|
+
},
|
|
33960
|
+
{
|
|
33961
|
+
category: "audit_log",
|
|
33962
|
+
patterns: [
|
|
33963
|
+
"audit log",
|
|
33964
|
+
"audit",
|
|
33965
|
+
"log entry",
|
|
33966
|
+
"log entries",
|
|
33967
|
+
"what happened",
|
|
33968
|
+
"show me events",
|
|
33969
|
+
"event class"
|
|
33970
|
+
].map(phrasePattern)
|
|
33971
|
+
},
|
|
33972
|
+
{
|
|
33973
|
+
category: "sentinel_findings",
|
|
33974
|
+
patterns: [
|
|
33975
|
+
"sentinel",
|
|
33976
|
+
"sentinels",
|
|
33977
|
+
"warning",
|
|
33978
|
+
"warnings",
|
|
33979
|
+
"alert",
|
|
33980
|
+
"alerts",
|
|
33981
|
+
"whats wrong",
|
|
33982
|
+
"what's wrong",
|
|
33983
|
+
"findings"
|
|
33984
|
+
].map(phrasePattern)
|
|
33985
|
+
},
|
|
33986
|
+
{
|
|
33987
|
+
category: "anomaly_alerts",
|
|
33988
|
+
patterns: [
|
|
33989
|
+
"anomaly",
|
|
33990
|
+
"anomalies",
|
|
33991
|
+
"spike",
|
|
33992
|
+
"unusual",
|
|
33993
|
+
"outlier"
|
|
33994
|
+
].map(phrasePattern)
|
|
33995
|
+
},
|
|
33996
|
+
{
|
|
33997
|
+
category: "recent_receipts",
|
|
33998
|
+
patterns: [
|
|
33999
|
+
"receipt",
|
|
34000
|
+
"receipts",
|
|
34001
|
+
"concordia",
|
|
34002
|
+
"commitment",
|
|
34003
|
+
"commitments",
|
|
34004
|
+
"chain",
|
|
34005
|
+
"chains"
|
|
34006
|
+
].map(phrasePattern)
|
|
34007
|
+
},
|
|
34008
|
+
{
|
|
34009
|
+
category: "verascore_deltas",
|
|
34010
|
+
patterns: [
|
|
34011
|
+
"verascore",
|
|
34012
|
+
"vera score",
|
|
34013
|
+
"trust score",
|
|
34014
|
+
"reputation"
|
|
34015
|
+
].map(phrasePattern)
|
|
34016
|
+
}
|
|
34017
|
+
];
|
|
34018
|
+
TRIVIAL_GREETINGS = /* @__PURE__ */ new Set([
|
|
34019
|
+
"hi",
|
|
34020
|
+
"hello",
|
|
34021
|
+
"hey",
|
|
34022
|
+
"yo",
|
|
34023
|
+
"ok",
|
|
34024
|
+
"thanks",
|
|
34025
|
+
"thx",
|
|
34026
|
+
"thank you"
|
|
34027
|
+
]);
|
|
34028
|
+
CATEGORY_LABELS = {
|
|
34029
|
+
templates: "Templates",
|
|
34030
|
+
agent_state: "Agent state",
|
|
34031
|
+
agent_activity: "Agent activity",
|
|
34032
|
+
audit_log: "Audit log",
|
|
34033
|
+
sentinel_findings: "Sentinel findings",
|
|
34034
|
+
anomaly_alerts: "Anomaly alerts",
|
|
34035
|
+
recent_receipts: "Recent receipts",
|
|
34036
|
+
verascore_deltas: "Verascore deltas"
|
|
34037
|
+
};
|
|
34038
|
+
}
|
|
34039
|
+
});
|
|
34040
|
+
function approxTokenLen2(text) {
|
|
34041
|
+
return Math.ceil(text.length / 4);
|
|
34042
|
+
}
|
|
32991
34043
|
function makeEventId(prefix) {
|
|
32992
34044
|
return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
32993
34045
|
}
|
|
34046
|
+
function classifyFetcherError(error) {
|
|
34047
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
34048
|
+
if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
|
|
34049
|
+
if (msg.includes("schema") || msg.includes("invalid shape")) {
|
|
34050
|
+
return "schema_mismatch";
|
|
34051
|
+
}
|
|
34052
|
+
if (msg.includes("io") || msg.includes("read") || msg.includes("enoent") || msg.includes("eacces")) {
|
|
34053
|
+
return "io_failed";
|
|
34054
|
+
}
|
|
34055
|
+
return "unknown";
|
|
34056
|
+
}
|
|
34057
|
+
function formatPriorTurnLine(turn) {
|
|
34058
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
34059
|
+
return `${label}: ${turn.content}`;
|
|
34060
|
+
}
|
|
32994
34061
|
function hashOf(input) {
|
|
32995
34062
|
return hashToString(sha256(stringToBytes(input)));
|
|
32996
34063
|
}
|
|
32997
|
-
var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
34064
|
+
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;
|
|
32998
34065
|
var init_operator_chat_service = __esm({
|
|
32999
34066
|
"src/chat/operator-chat-service.ts"() {
|
|
33000
34067
|
init_hashing();
|
|
33001
34068
|
init_encoding();
|
|
33002
34069
|
init_operator_chat_audit_events();
|
|
33003
34070
|
init_operator_chat_types();
|
|
34071
|
+
init_concierge_context_router();
|
|
33004
34072
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
34073
|
+
DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
34074
|
+
DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
34075
|
+
DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
34076
|
+
DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
34077
|
+
DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
|
|
33005
34078
|
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
33006
34079
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
33007
34080
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -33038,6 +34111,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33038
34111
|
piiFilter;
|
|
33039
34112
|
conciergeMaxTokens;
|
|
33040
34113
|
memory;
|
|
34114
|
+
historyWindowTurns;
|
|
34115
|
+
historyFreshnessMs;
|
|
34116
|
+
historyTokenBudget;
|
|
34117
|
+
sessionTtlMs;
|
|
34118
|
+
clock;
|
|
34119
|
+
contextFetchers;
|
|
34120
|
+
contextLlmAssist;
|
|
34121
|
+
dynamicContextBudget;
|
|
33041
34122
|
/**
|
|
33042
34123
|
* In-memory thread_id assigned to the active concierge session.
|
|
33043
34124
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -33045,6 +34126,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33045
34126
|
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33046
34127
|
*/
|
|
33047
34128
|
activeMemoryThreadId;
|
|
34129
|
+
/**
|
|
34130
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
34131
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
34132
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
34133
|
+
* allocates a new thread_id even though the prior one is still
|
|
34134
|
+
* readable from the memory store.
|
|
34135
|
+
*/
|
|
34136
|
+
lastInteractionAt;
|
|
33048
34137
|
constructor(deps) {
|
|
33049
34138
|
this.store = deps.store;
|
|
33050
34139
|
this.auditLog = deps.auditLog;
|
|
@@ -33056,6 +34145,18 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33056
34145
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
33057
34146
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33058
34147
|
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
34148
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
34149
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
34150
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
34151
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
34152
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
34153
|
+
if (deps.conciergeContextFetchers) {
|
|
34154
|
+
this.contextFetchers = deps.conciergeContextFetchers;
|
|
34155
|
+
}
|
|
34156
|
+
if (deps.conciergeContextLlmAssist) {
|
|
34157
|
+
this.contextLlmAssist = deps.conciergeContextLlmAssist;
|
|
34158
|
+
}
|
|
34159
|
+
this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
|
|
33059
34160
|
}
|
|
33060
34161
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
33061
34162
|
/**
|
|
@@ -33074,6 +34175,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33074
34175
|
throw new Error("concierge query must not be empty");
|
|
33075
34176
|
}
|
|
33076
34177
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
34178
|
+
const nowMs = this.clock();
|
|
34179
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
34180
|
+
this.activeMemoryThreadId = void 0;
|
|
34181
|
+
}
|
|
33077
34182
|
const operatorMessage = {
|
|
33078
34183
|
message_id: randomUUID(),
|
|
33079
34184
|
surface: "concierge",
|
|
@@ -33086,6 +34191,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33086
34191
|
CONCIERGE_THREAD_KEY,
|
|
33087
34192
|
operatorMessage
|
|
33088
34193
|
);
|
|
34194
|
+
let priorTurns = [];
|
|
34195
|
+
let memoryReadFailureReason = null;
|
|
34196
|
+
let activeThreadIdForRound;
|
|
34197
|
+
if (this.memory) {
|
|
34198
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
34199
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
34200
|
+
if (result.ok) {
|
|
34201
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
34202
|
+
const fresh = result.turns.filter((t) => {
|
|
34203
|
+
const ts = Date.parse(t.created_at);
|
|
34204
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
34205
|
+
});
|
|
34206
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
34207
|
+
priorTurns = recent;
|
|
34208
|
+
} else {
|
|
34209
|
+
memoryReadFailureReason = result.reason;
|
|
34210
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
34211
|
+
}
|
|
34212
|
+
}
|
|
33089
34213
|
if (this.memory) {
|
|
33090
34214
|
const threadId = this.ensureActiveMemoryThread();
|
|
33091
34215
|
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
@@ -33096,6 +34220,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33096
34220
|
let servedBy = "disabled";
|
|
33097
34221
|
let displayLabel = "Concierge: substrate not configured";
|
|
33098
34222
|
let outcome = "substrate_disabled";
|
|
34223
|
+
let dynamicCategoriesIncluded = [];
|
|
33099
34224
|
if (!this.substrateSelector) {
|
|
33100
34225
|
conciergeBody = "Concierge unavailable. The substrate selector is not configured for this fortress. Pick a substrate in the Policy center to enable concierge replies.";
|
|
33101
34226
|
} else {
|
|
@@ -33107,7 +34232,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33107
34232
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
33108
34233
|
outcome = "substrate_disabled";
|
|
33109
34234
|
} else {
|
|
33110
|
-
const
|
|
34235
|
+
const dynamicResult = await this.runDynamicContextFold(
|
|
34236
|
+
filterResult.filtered
|
|
34237
|
+
);
|
|
34238
|
+
dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
|
|
34239
|
+
const context = await this.assembleConciergeContext(
|
|
34240
|
+
priorTurns,
|
|
34241
|
+
dynamicResult.section
|
|
34242
|
+
);
|
|
33111
34243
|
const response = await this.substrateSelector.invokeSummarize(
|
|
33112
34244
|
"concierge",
|
|
33113
34245
|
{
|
|
@@ -33146,10 +34278,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33146
34278
|
CONCIERGE_THREAD_KEY,
|
|
33147
34279
|
responseMessage
|
|
33148
34280
|
);
|
|
34281
|
+
let assistantTurnId;
|
|
33149
34282
|
if (this.memory) {
|
|
33150
34283
|
const threadId = this.ensureActiveMemoryThread();
|
|
33151
|
-
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() =>
|
|
33152
|
-
|
|
34284
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
34285
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
34286
|
+
}
|
|
34287
|
+
if (this.memory && activeThreadIdForRound) {
|
|
34288
|
+
this.lastInteractionAt = nowMs;
|
|
33153
34289
|
}
|
|
33154
34290
|
const payload = {
|
|
33155
34291
|
version: "1.2",
|
|
@@ -33162,7 +34298,13 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33162
34298
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
33163
34299
|
substrate: servedBy,
|
|
33164
34300
|
latency_ms: latencyMs,
|
|
33165
|
-
outcome
|
|
34301
|
+
outcome,
|
|
34302
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
34303
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
34304
|
+
...this.memory ? {
|
|
34305
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
34306
|
+
} : {},
|
|
34307
|
+
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
|
|
33166
34308
|
};
|
|
33167
34309
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
33168
34310
|
return {
|
|
@@ -33172,6 +34314,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33172
34314
|
outcome
|
|
33173
34315
|
};
|
|
33174
34316
|
}
|
|
34317
|
+
/**
|
|
34318
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
34319
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
34320
|
+
* with `result: "failure"` since the concierge fell back to
|
|
34321
|
+
* single-turn mode for this round-trip.
|
|
34322
|
+
*/
|
|
34323
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
34324
|
+
const payload = {
|
|
34325
|
+
version: "1.2",
|
|
34326
|
+
event_id: makeEventId("conc-memfail"),
|
|
34327
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34328
|
+
identity_id: this.identityId,
|
|
34329
|
+
kind: "operator_concierge_memory_read_failed",
|
|
34330
|
+
surface: "concierge",
|
|
34331
|
+
thread_id: threadId,
|
|
34332
|
+
failure_reason: reason
|
|
34333
|
+
};
|
|
34334
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
34335
|
+
}
|
|
33175
34336
|
/**
|
|
33176
34337
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
33177
34338
|
* an empty array when no thread exists yet.
|
|
@@ -33294,6 +34455,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33294
34455
|
* ## Sanctuary reference
|
|
33295
34456
|
* <static domain reference block>
|
|
33296
34457
|
*
|
|
34458
|
+
* ## Live fortress context ← WP-V1.3-9 Tau-3, when present
|
|
34459
|
+
* ### <Category>
|
|
34460
|
+
* <fetcher payload>
|
|
34461
|
+
*
|
|
34462
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
34463
|
+
* OPERATOR: ...
|
|
34464
|
+
* CONCIERGE: ...
|
|
34465
|
+
*
|
|
33297
34466
|
* ## Recent activity
|
|
33298
34467
|
* <recentActivity output>
|
|
33299
34468
|
*
|
|
@@ -33303,37 +34472,116 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33303
34472
|
* ## Open inbox
|
|
33304
34473
|
* <openInbox output>
|
|
33305
34474
|
* ```
|
|
33306
|
-
|
|
33307
|
-
|
|
34475
|
+
*
|
|
34476
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
34477
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
34478
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
34479
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
34480
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
34481
|
+
* serialization is the canonical path for v1.3.
|
|
34482
|
+
*/
|
|
34483
|
+
async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
|
|
33308
34484
|
const ref = `## Sanctuary reference
|
|
33309
34485
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
34486
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
33310
34487
|
if (!this.contextProviders) {
|
|
33311
|
-
return
|
|
33312
|
-
|
|
33313
|
-
|
|
33314
|
-
|
|
33315
|
-
|
|
33316
|
-
## Wrapped agents
|
|
33317
|
-
(no providers wired)
|
|
33318
|
-
|
|
33319
|
-
## Open inbox
|
|
33320
|
-
(no providers wired)`;
|
|
34488
|
+
return [
|
|
34489
|
+
ref,
|
|
34490
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
34491
|
+
...priorSection ? [priorSection] : [],
|
|
34492
|
+
"## Recent activity\n(no providers wired)",
|
|
34493
|
+
"## Wrapped agents\n(no providers wired)",
|
|
34494
|
+
"## Open inbox\n(no providers wired)"
|
|
34495
|
+
].join("\n\n");
|
|
33321
34496
|
}
|
|
33322
34497
|
const [activity, agents, inbox] = await Promise.all([
|
|
33323
34498
|
this.contextProviders.recentActivity(),
|
|
33324
34499
|
this.contextProviders.agentInventory(),
|
|
33325
34500
|
this.contextProviders.openInbox()
|
|
33326
34501
|
]);
|
|
33327
|
-
return
|
|
33328
|
-
|
|
33329
|
-
|
|
33330
|
-
|
|
33331
|
-
|
|
33332
|
-
|
|
33333
|
-
|
|
33334
|
-
|
|
33335
|
-
|
|
33336
|
-
${inbox}
|
|
34502
|
+
return [
|
|
34503
|
+
ref,
|
|
34504
|
+
...dynamicSection ? [dynamicSection] : [],
|
|
34505
|
+
...priorSection ? [priorSection] : [],
|
|
34506
|
+
`## Recent activity
|
|
34507
|
+
${activity}`,
|
|
34508
|
+
`## Wrapped agents
|
|
34509
|
+
${agents}`,
|
|
34510
|
+
`## Open inbox
|
|
34511
|
+
${inbox}`
|
|
34512
|
+
].join("\n\n");
|
|
34513
|
+
}
|
|
34514
|
+
/**
|
|
34515
|
+
* Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
|
|
34516
|
+
* trip. Fail-soft on every axis: missing fetchers short-circuit to
|
|
34517
|
+
* an empty fold, fetcher failures emit a per-category audit event
|
|
34518
|
+
* and are omitted from the rendered section, an LLM-assist failure
|
|
34519
|
+
* proceeds with no fold. Returns the rendered section + the list of
|
|
34520
|
+
* categories whose data made it into the section (used for the
|
|
34521
|
+
* round-trip audit emission).
|
|
34522
|
+
*/
|
|
34523
|
+
async runDynamicContextFold(query) {
|
|
34524
|
+
if (!this.contextFetchers) {
|
|
34525
|
+
return { section: "", categoriesIncluded: [] };
|
|
34526
|
+
}
|
|
34527
|
+
const result = await foldContext(query, this.contextFetchers, {
|
|
34528
|
+
maxTokens: this.dynamicContextBudget,
|
|
34529
|
+
...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
|
|
34530
|
+
onFetcherFailure: (category, error) => {
|
|
34531
|
+
this.emitContextFetcherFailed(category, classifyFetcherError(error));
|
|
34532
|
+
}
|
|
34533
|
+
});
|
|
34534
|
+
return result;
|
|
34535
|
+
}
|
|
34536
|
+
/**
|
|
34537
|
+
* Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
|
|
34538
|
+
* of the fold path so the dynamic-context handler stays readable.
|
|
34539
|
+
* Emits with `result: "failure"` since the named category dropped
|
|
34540
|
+
* from the rendered section for this round-trip.
|
|
34541
|
+
*/
|
|
34542
|
+
emitContextFetcherFailed(category, failureReason) {
|
|
34543
|
+
const payload = {
|
|
34544
|
+
version: "1.2",
|
|
34545
|
+
event_id: makeEventId("conc-ctxfail"),
|
|
34546
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34547
|
+
identity_id: this.identityId,
|
|
34548
|
+
kind: "operator_concierge_context_fetcher_failed",
|
|
34549
|
+
surface: "concierge",
|
|
34550
|
+
category,
|
|
34551
|
+
failure_reason: failureReason
|
|
34552
|
+
};
|
|
34553
|
+
this.emit(
|
|
34554
|
+
OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
|
|
34555
|
+
payload,
|
|
34556
|
+
"failure"
|
|
34557
|
+
);
|
|
34558
|
+
}
|
|
34559
|
+
/**
|
|
34560
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
34561
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
34562
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
34563
|
+
* the input is empty or when the budget excludes every turn.
|
|
34564
|
+
*/
|
|
34565
|
+
formatPriorTurnsSection(turns) {
|
|
34566
|
+
if (turns.length === 0) return "";
|
|
34567
|
+
const HEADER = "## Prior conversation";
|
|
34568
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
34569
|
+
const headerTokens = approxTokenLen2(`${HEADER}
|
|
34570
|
+
`);
|
|
34571
|
+
const sepTokens = approxTokenLen2("\n");
|
|
34572
|
+
let runningTokens = headerTokens;
|
|
34573
|
+
let runningLines = [];
|
|
34574
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
34575
|
+
const line = lines[i];
|
|
34576
|
+
const tokens = approxTokenLen2(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
34577
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
34578
|
+
runningTokens += tokens;
|
|
34579
|
+
runningLines.push(line);
|
|
34580
|
+
}
|
|
34581
|
+
if (runningLines.length === 0) return "";
|
|
34582
|
+
runningLines = runningLines.reverse();
|
|
34583
|
+
return `${HEADER}
|
|
34584
|
+
${runningLines.join("\n")}`;
|
|
33337
34585
|
}
|
|
33338
34586
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
33339
34587
|
emit(operation, payload, result) {
|
|
@@ -33353,7 +34601,7 @@ ${inbox}`;
|
|
|
33353
34601
|
function chatStorageKey(surface, threadKey) {
|
|
33354
34602
|
return `${surface}.${threadKey}`;
|
|
33355
34603
|
}
|
|
33356
|
-
var OPERATOR_CHAT_NAMESPACE,
|
|
34604
|
+
var OPERATOR_CHAT_NAMESPACE, HKDF_INFO2, OperatorChatStore;
|
|
33357
34605
|
var init_operator_chat_store = __esm({
|
|
33358
34606
|
"src/chat/operator-chat-store.ts"() {
|
|
33359
34607
|
init_encryption();
|
|
@@ -33361,13 +34609,13 @@ var init_operator_chat_store = __esm({
|
|
|
33361
34609
|
init_encoding();
|
|
33362
34610
|
init_operator_chat_types();
|
|
33363
34611
|
OPERATOR_CHAT_NAMESPACE = "_chat";
|
|
33364
|
-
|
|
34612
|
+
HKDF_INFO2 = "operator-chat-store-v1";
|
|
33365
34613
|
OperatorChatStore = class {
|
|
33366
34614
|
storage;
|
|
33367
34615
|
encryptionKey;
|
|
33368
34616
|
constructor(storage, masterKey) {
|
|
33369
34617
|
this.storage = storage;
|
|
33370
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
34618
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
|
|
33371
34619
|
}
|
|
33372
34620
|
/**
|
|
33373
34621
|
* Load a thread. Returns null if no record exists or if the on-disk
|
|
@@ -33452,7 +34700,7 @@ var init_operator_chat_store = __esm({
|
|
|
33452
34700
|
function bundleKey(threadId) {
|
|
33453
34701
|
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
33454
34702
|
}
|
|
33455
|
-
function
|
|
34703
|
+
function stripKeyPrefix2(key) {
|
|
33456
34704
|
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
33457
34705
|
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
33458
34706
|
}
|
|
@@ -33463,7 +34711,7 @@ function lastTurnId(bundle) {
|
|
|
33463
34711
|
}
|
|
33464
34712
|
return max;
|
|
33465
34713
|
}
|
|
33466
|
-
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX,
|
|
34714
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO3, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
|
|
33467
34715
|
var init_concierge_memory_store = __esm({
|
|
33468
34716
|
"src/chat/concierge-memory-store.ts"() {
|
|
33469
34717
|
init_encryption();
|
|
@@ -33471,9 +34719,9 @@ var init_concierge_memory_store = __esm({
|
|
|
33471
34719
|
init_encoding();
|
|
33472
34720
|
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
33473
34721
|
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
33474
|
-
|
|
34722
|
+
HKDF_INFO3 = "concierge-memory-store-v1";
|
|
33475
34723
|
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
33476
|
-
|
|
34724
|
+
MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
|
|
33477
34725
|
ConciergeMemoryStore = class {
|
|
33478
34726
|
storage;
|
|
33479
34727
|
encryptionKey;
|
|
@@ -33482,7 +34730,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33482
34730
|
locks;
|
|
33483
34731
|
constructor(opts) {
|
|
33484
34732
|
this.storage = opts.storage;
|
|
33485
|
-
this.encryptionKey = derivePurposeKey(opts.masterKey,
|
|
34733
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
|
|
33486
34734
|
this.fortressId = opts.fortressId;
|
|
33487
34735
|
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
33488
34736
|
this.locks = /* @__PURE__ */ new Map();
|
|
@@ -33538,6 +34786,65 @@ var init_concierge_memory_store = __esm({
|
|
|
33538
34786
|
}
|
|
33539
34787
|
return turns;
|
|
33540
34788
|
}
|
|
34789
|
+
/**
|
|
34790
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
34791
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
34792
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
34793
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
34794
|
+
* with a concrete cause.
|
|
34795
|
+
*
|
|
34796
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
34797
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
34798
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
34799
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
34800
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
34801
|
+
* - Storage IO error → `io_failed`.
|
|
34802
|
+
*/
|
|
34803
|
+
async readThreadStrict(threadId, opts) {
|
|
34804
|
+
const key = bundleKey(threadId);
|
|
34805
|
+
let raw;
|
|
34806
|
+
try {
|
|
34807
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
34808
|
+
} catch {
|
|
34809
|
+
return { ok: false, reason: "io_failed" };
|
|
34810
|
+
}
|
|
34811
|
+
if (!raw) return { ok: true, turns: [] };
|
|
34812
|
+
if (raw.length > MAX_BUNDLE_BYTES3) {
|
|
34813
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
34814
|
+
}
|
|
34815
|
+
let envelope;
|
|
34816
|
+
try {
|
|
34817
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
34818
|
+
} catch {
|
|
34819
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34820
|
+
}
|
|
34821
|
+
let plaintext;
|
|
34822
|
+
try {
|
|
34823
|
+
const aad = stringToBytes(threadId);
|
|
34824
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
34825
|
+
} catch {
|
|
34826
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
34827
|
+
}
|
|
34828
|
+
let parsed;
|
|
34829
|
+
try {
|
|
34830
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
34831
|
+
} catch {
|
|
34832
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34833
|
+
}
|
|
34834
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
34835
|
+
if (parsed.thread_id !== threadId) {
|
|
34836
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34837
|
+
}
|
|
34838
|
+
let turns = parsed.turns;
|
|
34839
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
34840
|
+
const cutoff = opts.sinceTurnId;
|
|
34841
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
34842
|
+
}
|
|
34843
|
+
if (opts?.limit !== void 0) {
|
|
34844
|
+
turns = turns.slice(0, opts.limit);
|
|
34845
|
+
}
|
|
34846
|
+
return { ok: true, turns };
|
|
34847
|
+
}
|
|
33541
34848
|
/**
|
|
33542
34849
|
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33543
34850
|
* Sorted newest-first by last_turn_at.
|
|
@@ -33549,7 +34856,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33549
34856
|
);
|
|
33550
34857
|
const summaries = [];
|
|
33551
34858
|
for (const meta of entries) {
|
|
33552
|
-
const threadId =
|
|
34859
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
33553
34860
|
if (threadId === null) continue;
|
|
33554
34861
|
const bundle = await this.loadBundle(threadId);
|
|
33555
34862
|
if (!bundle || bundle.turns.length === 0) continue;
|
|
@@ -33602,7 +34909,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33602
34909
|
);
|
|
33603
34910
|
let pruned = 0;
|
|
33604
34911
|
for (const meta of entries) {
|
|
33605
|
-
const threadId =
|
|
34912
|
+
const threadId = stripKeyPrefix2(meta.key);
|
|
33606
34913
|
if (threadId === null) continue;
|
|
33607
34914
|
pruned += await this.withLock(threadId, async () => {
|
|
33608
34915
|
const bundle = await this.loadBundle(threadId);
|
|
@@ -33633,7 +34940,7 @@ var init_concierge_memory_store = __esm({
|
|
|
33633
34940
|
return null;
|
|
33634
34941
|
}
|
|
33635
34942
|
if (!raw) return null;
|
|
33636
|
-
if (raw.length >
|
|
34943
|
+
if (raw.length > MAX_BUNDLE_BYTES3) return null;
|
|
33637
34944
|
try {
|
|
33638
34945
|
const envelope = JSON.parse(bytesToString(raw));
|
|
33639
34946
|
const aad = stringToBytes(threadId);
|
|
@@ -33724,7 +35031,18 @@ function buildV11Bindings(inputs) {
|
|
|
33724
35031
|
registry
|
|
33725
35032
|
}),
|
|
33726
35033
|
conciergePiiFilter: buildConciergePiiFilter(),
|
|
33727
|
-
conciergeMemory
|
|
35034
|
+
conciergeMemory,
|
|
35035
|
+
conciergeContextFetchers: buildConciergeContextFetchers({
|
|
35036
|
+
auditLog: inputs.auditLog,
|
|
35037
|
+
identityId: inputs.identityId,
|
|
35038
|
+
registry
|
|
35039
|
+
}),
|
|
35040
|
+
...inputs.intelligenceSelector ? {
|
|
35041
|
+
conciergeContextLlmAssist: buildConciergeContextLlmAssist({
|
|
35042
|
+
selector: inputs.intelligenceSelector,
|
|
35043
|
+
identityId: inputs.identityId
|
|
35044
|
+
})
|
|
35045
|
+
} : {}
|
|
33728
35046
|
});
|
|
33729
35047
|
}
|
|
33730
35048
|
const hubService = new HubService({
|
|
@@ -33785,6 +35103,107 @@ function buildConciergeContextProviders(args) {
|
|
|
33785
35103
|
}
|
|
33786
35104
|
};
|
|
33787
35105
|
}
|
|
35106
|
+
function buildConciergeContextFetchers(args) {
|
|
35107
|
+
const empty = async () => "";
|
|
35108
|
+
return {
|
|
35109
|
+
templates: async () => {
|
|
35110
|
+
const entries = listTemplates();
|
|
35111
|
+
if (entries.length === 0) return "(no templates installed)";
|
|
35112
|
+
const lines = entries.map((e) => {
|
|
35113
|
+
const m = e.metadata;
|
|
35114
|
+
return `${m.name} (tier ${m.tier}, channel ${m.channel}, target ${m.target_archetype})`;
|
|
35115
|
+
});
|
|
35116
|
+
return lines.join("\n");
|
|
35117
|
+
},
|
|
35118
|
+
agent_state: async (agentNameHint) => {
|
|
35119
|
+
const records = args.registry.list({ identity_id: args.identityId });
|
|
35120
|
+
if (records.length === 0) return "(no wrapped agents)";
|
|
35121
|
+
const filtered = agentNameHint ? records.filter(
|
|
35122
|
+
(r) => r.agent_id.toLowerCase().includes(agentNameHint.toLowerCase()) || r.harness.toLowerCase().includes(agentNameHint.toLowerCase())
|
|
35123
|
+
) : records;
|
|
35124
|
+
const target = filtered.length > 0 ? filtered : records;
|
|
35125
|
+
const lines = target.slice(0, 20).map((r) => {
|
|
35126
|
+
const tmpl = typeof r.channel_template_id === "string" ? r.channel_template_id : "no_template";
|
|
35127
|
+
return `${r.agent_id} harness=${r.harness} status=${r.status} template=${tmpl}`;
|
|
35128
|
+
});
|
|
35129
|
+
return lines.join("\n");
|
|
35130
|
+
},
|
|
35131
|
+
agent_activity: async (agentNameHint) => {
|
|
35132
|
+
const result = await args.auditLog.query({ limit: 50 });
|
|
35133
|
+
const owned = result.entries.filter(
|
|
35134
|
+
(e) => e.identity_id === args.identityId
|
|
35135
|
+
);
|
|
35136
|
+
const filtered = agentNameHint ? owned.filter((e) => {
|
|
35137
|
+
const agentId = e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : "";
|
|
35138
|
+
return agentId.toLowerCase().includes(agentNameHint.toLowerCase());
|
|
35139
|
+
}) : owned;
|
|
35140
|
+
const tail = (filtered.length > 0 ? filtered : owned).slice(-20);
|
|
35141
|
+
if (tail.length === 0) return "(no activity)";
|
|
35142
|
+
return tail.map((e) => {
|
|
35143
|
+
const agentId = (e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : null) ?? "_fortress";
|
|
35144
|
+
return `${e.timestamp} ${e.layer}.${e.operation} agent=${agentId} result=${e.result}`;
|
|
35145
|
+
}).join("\n");
|
|
35146
|
+
},
|
|
35147
|
+
audit_log: async () => {
|
|
35148
|
+
const result = await args.auditLog.query({ limit: 30 });
|
|
35149
|
+
const owned = result.entries.filter(
|
|
35150
|
+
(e) => e.identity_id === args.identityId
|
|
35151
|
+
);
|
|
35152
|
+
if (owned.length === 0) return "(no audit log entries)";
|
|
35153
|
+
return owned.slice(-30).map(
|
|
35154
|
+
(e) => `${e.timestamp} ${e.layer}.${e.operation} result=${e.result}`
|
|
35155
|
+
).join("\n");
|
|
35156
|
+
},
|
|
35157
|
+
sentinel_findings: empty,
|
|
35158
|
+
anomaly_alerts: empty,
|
|
35159
|
+
recent_receipts: async () => {
|
|
35160
|
+
const result = await args.auditLog.query({ limit: 100 });
|
|
35161
|
+
const owned = result.entries.filter(
|
|
35162
|
+
(e) => e.identity_id === args.identityId && e.operation.startsWith("composition_")
|
|
35163
|
+
);
|
|
35164
|
+
if (owned.length === 0) return "(no recent composition events)";
|
|
35165
|
+
return owned.slice(-15).map((e) => `${e.timestamp} ${e.operation} result=${e.result}`).join("\n");
|
|
35166
|
+
},
|
|
35167
|
+
verascore_deltas: empty
|
|
35168
|
+
};
|
|
35169
|
+
}
|
|
35170
|
+
function buildConciergeContextLlmAssist(args) {
|
|
35171
|
+
return async (query, categories) => {
|
|
35172
|
+
const labelList = categories.map((c) => `- ${c}`).join("\n");
|
|
35173
|
+
const prompt2 = `You are a router. Classify the operator's query into one of the categories below or "none".
|
|
35174
|
+
Reply with exactly one token: one category name or "none".
|
|
35175
|
+
|
|
35176
|
+
Categories:
|
|
35177
|
+
${labelList}
|
|
35178
|
+
|
|
35179
|
+
Query: ${query}
|
|
35180
|
+
|
|
35181
|
+
Category:`;
|
|
35182
|
+
try {
|
|
35183
|
+
const handle = await args.selector.getSubstrate("concierge");
|
|
35184
|
+
if (!handle.capability.summarize) return "none";
|
|
35185
|
+
const response = await args.selector.invokeSummarize("concierge", {
|
|
35186
|
+
kind: "summarize",
|
|
35187
|
+
context: prompt2,
|
|
35188
|
+
query: "Output the single category token.",
|
|
35189
|
+
maxTokens: 16
|
|
35190
|
+
});
|
|
35191
|
+
if (response.failureClass || response.body.kind !== "summarize") {
|
|
35192
|
+
return "none";
|
|
35193
|
+
}
|
|
35194
|
+
const raw = response.body.text.trim().toLowerCase();
|
|
35195
|
+
const head = raw.split(/\s|[.,!?:;]/)[0] ?? "";
|
|
35196
|
+
const normalized = head.replace(/[^a-z_]/g, "");
|
|
35197
|
+
const known = categories;
|
|
35198
|
+
if (known.includes(normalized)) {
|
|
35199
|
+
return normalized;
|
|
35200
|
+
}
|
|
35201
|
+
return "none";
|
|
35202
|
+
} catch {
|
|
35203
|
+
return "none";
|
|
35204
|
+
}
|
|
35205
|
+
};
|
|
35206
|
+
}
|
|
33788
35207
|
function buildConciergePiiFilter() {
|
|
33789
35208
|
return {
|
|
33790
35209
|
filter(input) {
|
|
@@ -33812,6 +35231,7 @@ var init_wiring = __esm({
|
|
|
33812
35231
|
init_agent_registry_persistence();
|
|
33813
35232
|
init_operator_chat_index();
|
|
33814
35233
|
init_privacy_filter();
|
|
35234
|
+
init_registry();
|
|
33815
35235
|
CapabilityErrorAgentController = class {
|
|
33816
35236
|
fail(action) {
|
|
33817
35237
|
throw new HubCapabilityError(
|
|
@@ -33959,7 +35379,7 @@ var init_defaults = __esm({
|
|
|
33959
35379
|
});
|
|
33960
35380
|
|
|
33961
35381
|
// src/intelligence/policy-store.ts
|
|
33962
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
35382
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO4, IntelligenceConfigStore;
|
|
33963
35383
|
var init_policy_store = __esm({
|
|
33964
35384
|
"src/intelligence/policy-store.ts"() {
|
|
33965
35385
|
init_encryption();
|
|
@@ -33968,13 +35388,13 @@ var init_policy_store = __esm({
|
|
|
33968
35388
|
init_defaults();
|
|
33969
35389
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
33970
35390
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
33971
|
-
|
|
35391
|
+
HKDF_INFO4 = "intelligence-substrate-config";
|
|
33972
35392
|
IntelligenceConfigStore = class {
|
|
33973
35393
|
storage;
|
|
33974
35394
|
encryptionKey;
|
|
33975
35395
|
constructor(storage, masterKey) {
|
|
33976
35396
|
this.storage = storage;
|
|
33977
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
35397
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
|
|
33978
35398
|
}
|
|
33979
35399
|
/**
|
|
33980
35400
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -36435,7 +37855,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
36435
37855
|
}
|
|
36436
37856
|
return null;
|
|
36437
37857
|
}
|
|
36438
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
37858
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
36439
37859
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
36440
37860
|
if (!destinationSigner) {
|
|
36441
37861
|
return {
|
|
@@ -36497,8 +37917,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36497
37917
|
}
|
|
36498
37918
|
}
|
|
36499
37919
|
}
|
|
37920
|
+
let plaintext;
|
|
36500
37921
|
try {
|
|
36501
|
-
|
|
37922
|
+
plaintext = decrypt(
|
|
36502
37923
|
item.entry.payload,
|
|
36503
37924
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
36504
37925
|
);
|
|
@@ -36507,28 +37928,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36507
37928
|
skipped++;
|
|
36508
37929
|
continue;
|
|
36509
37930
|
}
|
|
36510
|
-
await stateStore.write(
|
|
36511
|
-
item.namespace,
|
|
36512
|
-
item.key,
|
|
36513
|
-
bytesToString(plaintext),
|
|
36514
|
-
destinationSigner.identity_id,
|
|
36515
|
-
destinationSigner.encrypted_private_key,
|
|
36516
|
-
identityEncryptionKey,
|
|
36517
|
-
{
|
|
36518
|
-
content_type: item.entry.metadata.content_type,
|
|
36519
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
36520
|
-
tags: [
|
|
36521
|
-
...item.entry.metadata.tags ?? [],
|
|
36522
|
-
"exit-import",
|
|
36523
|
-
`source:${item.entry.kid}`
|
|
36524
|
-
]
|
|
36525
|
-
}
|
|
36526
|
-
);
|
|
36527
|
-
imported++;
|
|
36528
37931
|
} catch {
|
|
36529
37932
|
skippedInvalidSig++;
|
|
36530
37933
|
skipped++;
|
|
37934
|
+
continue;
|
|
36531
37935
|
}
|
|
37936
|
+
await stateStore.write(
|
|
37937
|
+
item.namespace,
|
|
37938
|
+
item.key,
|
|
37939
|
+
bytesToString(plaintext),
|
|
37940
|
+
destinationSigner.identity_id,
|
|
37941
|
+
destinationSigner.encrypted_private_key,
|
|
37942
|
+
identityEncryptionKey,
|
|
37943
|
+
{
|
|
37944
|
+
content_type: item.entry.metadata.content_type,
|
|
37945
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
37946
|
+
tags: [
|
|
37947
|
+
...item.entry.metadata.tags ?? [],
|
|
37948
|
+
"exit-import",
|
|
37949
|
+
`source:${item.entry.kid}`
|
|
37950
|
+
]
|
|
37951
|
+
}
|
|
37952
|
+
);
|
|
37953
|
+
imported++;
|
|
37954
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
36532
37955
|
}
|
|
36533
37956
|
return {
|
|
36534
37957
|
status: "rekeyed",
|
|
@@ -36539,6 +37962,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36539
37962
|
conflicts
|
|
36540
37963
|
};
|
|
36541
37964
|
}
|
|
37965
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
37966
|
+
let removed = 0;
|
|
37967
|
+
const failed = [];
|
|
37968
|
+
for (const loc of staged) {
|
|
37969
|
+
try {
|
|
37970
|
+
const ok2 = await storage.delete(loc.namespace, loc.key);
|
|
37971
|
+
if (ok2) {
|
|
37972
|
+
removed++;
|
|
37973
|
+
} else {
|
|
37974
|
+
failed.push(loc);
|
|
37975
|
+
}
|
|
37976
|
+
} catch {
|
|
37977
|
+
failed.push(loc);
|
|
37978
|
+
}
|
|
37979
|
+
}
|
|
37980
|
+
return { removed, failed };
|
|
37981
|
+
}
|
|
36542
37982
|
async function stageArtifact(storage, namespace, key, value) {
|
|
36543
37983
|
await storage.write(namespace, key, jsonBytes(value));
|
|
36544
37984
|
}
|
|
@@ -36663,6 +38103,8 @@ async function importExitBundle(opts) {
|
|
|
36663
38103
|
}
|
|
36664
38104
|
const importId = importIdForManifest(manifest);
|
|
36665
38105
|
const stagedArtifacts = [];
|
|
38106
|
+
const stagedLocations = [];
|
|
38107
|
+
const importedRekeyEntries = [];
|
|
36666
38108
|
if (identityArtifact) {
|
|
36667
38109
|
await stageArtifact(
|
|
36668
38110
|
opts.storage,
|
|
@@ -36671,10 +38113,15 @@ async function importExitBundle(opts) {
|
|
|
36671
38113
|
identityArtifact.json
|
|
36672
38114
|
);
|
|
36673
38115
|
stagedArtifacts.push("public_identity");
|
|
38116
|
+
stagedLocations.push({
|
|
38117
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
38118
|
+
key: identityArtifact.json.bundle.identity_id
|
|
38119
|
+
});
|
|
36674
38120
|
}
|
|
36675
38121
|
if (policySet) {
|
|
36676
38122
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
36677
38123
|
stagedArtifacts.push("policy_set");
|
|
38124
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
36678
38125
|
}
|
|
36679
38126
|
if (auditReceipts) {
|
|
36680
38127
|
await stageArtifact(
|
|
@@ -36684,10 +38131,12 @@ async function importExitBundle(opts) {
|
|
|
36684
38131
|
auditReceipts.json
|
|
36685
38132
|
);
|
|
36686
38133
|
stagedArtifacts.push("audit_receipts");
|
|
38134
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
36687
38135
|
}
|
|
36688
38136
|
if (commitments) {
|
|
36689
38137
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
36690
38138
|
stagedArtifacts.push("commitments");
|
|
38139
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
36691
38140
|
}
|
|
36692
38141
|
if (placeholderMetadata) {
|
|
36693
38142
|
await stageArtifact(
|
|
@@ -36697,12 +38146,17 @@ async function importExitBundle(opts) {
|
|
|
36697
38146
|
placeholderMetadata.json
|
|
36698
38147
|
);
|
|
36699
38148
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
38149
|
+
stagedLocations.push({
|
|
38150
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
38151
|
+
key: importId
|
|
38152
|
+
});
|
|
36700
38153
|
}
|
|
36701
38154
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
36702
38155
|
manifest: manifest.body,
|
|
36703
38156
|
verified_at: verification.verified_at,
|
|
36704
38157
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
36705
38158
|
});
|
|
38159
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
36706
38160
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
36707
38161
|
let reputationResult = {
|
|
36708
38162
|
imported_attestations: 0,
|
|
@@ -36727,26 +38181,57 @@ async function importExitBundle(opts) {
|
|
|
36727
38181
|
encryptedState?.json ?? null,
|
|
36728
38182
|
opts
|
|
36729
38183
|
);
|
|
36730
|
-
|
|
36731
|
-
|
|
36732
|
-
|
|
36733
|
-
|
|
36734
|
-
|
|
36735
|
-
|
|
36736
|
-
|
|
36737
|
-
|
|
36738
|
-
|
|
36739
|
-
|
|
36740
|
-
|
|
36741
|
-
|
|
36742
|
-
|
|
36743
|
-
|
|
36744
|
-
|
|
36745
|
-
|
|
36746
|
-
|
|
36747
|
-
|
|
36748
|
-
|
|
36749
|
-
|
|
38184
|
+
let stateResult;
|
|
38185
|
+
try {
|
|
38186
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
38187
|
+
encryptedState.json,
|
|
38188
|
+
opts,
|
|
38189
|
+
sourceMasterKey,
|
|
38190
|
+
publicKeys.byIdentityId,
|
|
38191
|
+
importedRekeyEntries
|
|
38192
|
+
) : {
|
|
38193
|
+
status: "staged_requires_source_key",
|
|
38194
|
+
imported_keys: 0,
|
|
38195
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
38196
|
+
skipped_invalid_sig: 0,
|
|
38197
|
+
skipped_unknown_kid: 0,
|
|
38198
|
+
conflicts: conflicts.state_conflicts.length
|
|
38199
|
+
} : {
|
|
38200
|
+
status: "not_requested",
|
|
38201
|
+
imported_keys: 0,
|
|
38202
|
+
skipped_keys: 0,
|
|
38203
|
+
skipped_invalid_sig: 0,
|
|
38204
|
+
skipped_unknown_kid: 0,
|
|
38205
|
+
conflicts: 0
|
|
38206
|
+
};
|
|
38207
|
+
} catch (err) {
|
|
38208
|
+
const toCleanup = [
|
|
38209
|
+
...importedRekeyEntries,
|
|
38210
|
+
...stagedLocations
|
|
38211
|
+
];
|
|
38212
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
38213
|
+
opts.auditLog.append(
|
|
38214
|
+
"l1",
|
|
38215
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
38216
|
+
manifest.body.identity_binding.identity_id,
|
|
38217
|
+
{
|
|
38218
|
+
import_id: importId,
|
|
38219
|
+
manifest_version: manifest.body.manifest_version,
|
|
38220
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
38221
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
38222
|
+
removed_total: cleanup.removed,
|
|
38223
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
38224
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
38225
|
+
},
|
|
38226
|
+
"failure"
|
|
38227
|
+
);
|
|
38228
|
+
await opts.auditLog.flush();
|
|
38229
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
38230
|
+
throw new ExitBundleImportError(
|
|
38231
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
38232
|
+
`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).`
|
|
38233
|
+
);
|
|
38234
|
+
}
|
|
36750
38235
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
36751
38236
|
import_id: importId,
|
|
36752
38237
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -37825,16 +39310,35 @@ ${err.message}
|
|
|
37825
39310
|
timestamp: alert.timestamp
|
|
37826
39311
|
});
|
|
37827
39312
|
} : void 0;
|
|
37828
|
-
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37829
39313
|
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37830
39314
|
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
39315
|
+
const aggregatorPayloadStore = new AggregatorPayloadStore({
|
|
39316
|
+
storage,
|
|
39317
|
+
masterKey,
|
|
39318
|
+
fortressId: fortressIdForAggregator
|
|
39319
|
+
});
|
|
37831
39320
|
const approvalAggregator = new ApprovalAggregator({
|
|
37832
39321
|
storage,
|
|
37833
39322
|
masterKey,
|
|
37834
39323
|
auditLog,
|
|
37835
39324
|
identityId: aggregatorIdentityId,
|
|
37836
|
-
fortressId: fortressIdForAggregator
|
|
39325
|
+
fortressId: fortressIdForAggregator,
|
|
39326
|
+
payloadStore: aggregatorPayloadStore
|
|
39327
|
+
});
|
|
39328
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
39329
|
+
underlying: approvalChannel,
|
|
39330
|
+
aggregator: approvalAggregator,
|
|
39331
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
39332
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
37837
39333
|
});
|
|
39334
|
+
const gate = new ApprovalGate(
|
|
39335
|
+
policy,
|
|
39336
|
+
baseline,
|
|
39337
|
+
wrappedApprovalChannel,
|
|
39338
|
+
auditLog,
|
|
39339
|
+
injectionDetector,
|
|
39340
|
+
onInjectionAlert
|
|
39341
|
+
);
|
|
37838
39342
|
gate.setApprovalEventCallback((event) => {
|
|
37839
39343
|
void approvalAggregator.ingest(event);
|
|
37840
39344
|
});
|
|
@@ -38032,6 +39536,8 @@ var init_src = __esm({
|
|
|
38032
39536
|
init_webhook();
|
|
38033
39537
|
init_gate();
|
|
38034
39538
|
init_approval_aggregator();
|
|
39539
|
+
init_aggregator_backed_channel();
|
|
39540
|
+
init_aggregator_store();
|
|
38035
39541
|
init_tools4();
|
|
38036
39542
|
init_router();
|
|
38037
39543
|
init_router();
|
|
@@ -40996,6 +42502,22 @@ var init_broker = __esm({
|
|
|
40996
42502
|
auditLog;
|
|
40997
42503
|
issuer;
|
|
40998
42504
|
principalIdentityId;
|
|
42505
|
+
/**
|
|
42506
|
+
* Per-secret-name mutex. Hardening wave 6 finding #64: two concurrent
|
|
42507
|
+
* addSecret() / rotateSecret() / deleteSecret() calls on the same name
|
|
42508
|
+
* MUST serialize cleanly. The keychain backend's `find-then-add` and
|
|
42509
|
+
* `find-then-delete-then-add` shapes (KeychainBackend.addSecret /
|
|
42510
|
+
* .rotateSecret) are not atomic against another caller racing the same
|
|
42511
|
+
* service-name; without serialization the second caller can observe a
|
|
42512
|
+
* stale "exists" check and either drop the new value or leave a
|
|
42513
|
+
* duplicate keychain entry.
|
|
42514
|
+
*
|
|
42515
|
+
* Implementation: an in-memory promise chain per name. Subsequent
|
|
42516
|
+
* callers `await` the chain tail and append their own work; failures
|
|
42517
|
+
* propagate to the failing caller without poisoning the chain for
|
|
42518
|
+
* later callers.
|
|
42519
|
+
*/
|
|
42520
|
+
nameLocks = /* @__PURE__ */ new Map();
|
|
40999
42521
|
constructor(opts) {
|
|
41000
42522
|
this.backend = opts.backend;
|
|
41001
42523
|
this.auditLog = opts.auditLog;
|
|
@@ -41006,6 +42528,40 @@ var init_broker = __esm({
|
|
|
41006
42528
|
grants: opts.grants
|
|
41007
42529
|
});
|
|
41008
42530
|
}
|
|
42531
|
+
/**
|
|
42532
|
+
* Serialize `op` against any other in-flight write to the same secret
|
|
42533
|
+
* `name`. Per-name fairness only, distinct names run in parallel.
|
|
42534
|
+
* The current chain tail is used as the acceptance gate; we then
|
|
42535
|
+
* publish a new tail that swallows the operation's outcome so a
|
|
42536
|
+
* thrown error does not poison the next caller's wait.
|
|
42537
|
+
*/
|
|
42538
|
+
async withNameLock(name, op) {
|
|
42539
|
+
const previous = this.nameLocks.get(name) ?? Promise.resolve();
|
|
42540
|
+
let release = () => {
|
|
42541
|
+
};
|
|
42542
|
+
const next = new Promise((resolve8) => {
|
|
42543
|
+
release = resolve8;
|
|
42544
|
+
});
|
|
42545
|
+
this.nameLocks.set(name, next);
|
|
42546
|
+
try {
|
|
42547
|
+
await previous.catch(() => {
|
|
42548
|
+
});
|
|
42549
|
+
return await op();
|
|
42550
|
+
} finally {
|
|
42551
|
+
release();
|
|
42552
|
+
if (this.nameLocks.get(name) === next) {
|
|
42553
|
+
this.nameLocks.delete(name);
|
|
42554
|
+
}
|
|
42555
|
+
}
|
|
42556
|
+
}
|
|
42557
|
+
/**
|
|
42558
|
+
* Diagnostic-only: visible for tests so they can assert that distinct
|
|
42559
|
+
* names do not contend on a shared lock. Not part of the public broker
|
|
42560
|
+
* contract; do not consume from production code.
|
|
42561
|
+
*/
|
|
42562
|
+
__nameLockCountForTests() {
|
|
42563
|
+
return this.nameLocks.size;
|
|
42564
|
+
}
|
|
41009
42565
|
/** Ensure backend is initialized and unlocked. Audits the unlock. */
|
|
41010
42566
|
async ensureUnlocked(passphrase) {
|
|
41011
42567
|
await this.backend.ensureInitialized(passphrase);
|
|
@@ -41018,31 +42574,37 @@ var init_broker = __esm({
|
|
|
41018
42574
|
);
|
|
41019
42575
|
}
|
|
41020
42576
|
async addSecret(name, value) {
|
|
41021
|
-
await this.
|
|
41022
|
-
|
|
41023
|
-
|
|
41024
|
-
|
|
41025
|
-
|
|
41026
|
-
|
|
41027
|
-
|
|
42577
|
+
await this.withNameLock(name, async () => {
|
|
42578
|
+
await this.backend.addSecret(name, value);
|
|
42579
|
+
this.auditLog.append(
|
|
42580
|
+
"l3",
|
|
42581
|
+
BROKER_OPS.SECRET_ADDED,
|
|
42582
|
+
this.principalIdentityId,
|
|
42583
|
+
{ secret: name }
|
|
42584
|
+
);
|
|
42585
|
+
});
|
|
41028
42586
|
}
|
|
41029
42587
|
async rotateSecret(name, newValue) {
|
|
41030
|
-
await this.
|
|
41031
|
-
|
|
41032
|
-
|
|
41033
|
-
|
|
41034
|
-
|
|
41035
|
-
|
|
41036
|
-
|
|
42588
|
+
await this.withNameLock(name, async () => {
|
|
42589
|
+
await this.backend.rotateSecret(name, newValue);
|
|
42590
|
+
this.auditLog.append(
|
|
42591
|
+
"l3",
|
|
42592
|
+
BROKER_OPS.SECRET_ROTATED,
|
|
42593
|
+
this.principalIdentityId,
|
|
42594
|
+
{ secret: name }
|
|
42595
|
+
);
|
|
42596
|
+
});
|
|
41037
42597
|
}
|
|
41038
42598
|
async deleteSecret(name) {
|
|
41039
|
-
await this.
|
|
41040
|
-
|
|
41041
|
-
|
|
41042
|
-
|
|
41043
|
-
|
|
41044
|
-
|
|
41045
|
-
|
|
42599
|
+
await this.withNameLock(name, async () => {
|
|
42600
|
+
await this.backend.deleteSecret(name);
|
|
42601
|
+
this.auditLog.append(
|
|
42602
|
+
"l3",
|
|
42603
|
+
BROKER_OPS.SECRET_DELETED,
|
|
42604
|
+
this.principalIdentityId,
|
|
42605
|
+
{ secret: name }
|
|
42606
|
+
);
|
|
42607
|
+
});
|
|
41046
42608
|
}
|
|
41047
42609
|
async listSecretNames() {
|
|
41048
42610
|
return this.backend.listSecretNames();
|
|
@@ -41082,6 +42644,19 @@ var init_broker = __esm({
|
|
|
41082
42644
|
liveTokenCount() {
|
|
41083
42645
|
return this.issuer.liveTokenCount();
|
|
41084
42646
|
}
|
|
42647
|
+
/**
|
|
42648
|
+
* Drop expired tokens from the in-memory issuer map. Hardening wave 6
|
|
42649
|
+
* finding #86: previously expiry pruning depended on opportunistic
|
|
42650
|
+
* `pruneExpired()` calls; now the cocoon-unlock initialization path
|
|
42651
|
+
* (openBroker -> after backend.ensureInitialized -> after Broker
|
|
42652
|
+
* construction) fires this once so each cocoon-unlock cycle drops
|
|
42653
|
+
* stale bindings before any operator interaction.
|
|
42654
|
+
*
|
|
42655
|
+
* Returns the number of tokens removed. Safe to call repeatedly; idempotent.
|
|
42656
|
+
*/
|
|
42657
|
+
pruneExpiredTokens() {
|
|
42658
|
+
return this.issuer.pruneExpired();
|
|
42659
|
+
}
|
|
41085
42660
|
/**
|
|
41086
42661
|
* Audit query restricted to broker-scoped operations. Returns entries
|
|
41087
42662
|
* with their timestamps, op, and result (never the secret value).
|
|
@@ -41240,6 +42815,7 @@ async function openBroker(opts = {}) {
|
|
|
41240
42815
|
grants,
|
|
41241
42816
|
principalIdentityId: opts.principalIdentityId ?? "sanctuary-broker"
|
|
41242
42817
|
});
|
|
42818
|
+
broker.pruneExpiredTokens();
|
|
41243
42819
|
return {
|
|
41244
42820
|
broker,
|
|
41245
42821
|
close: async () => {
|
|
@@ -42108,8 +43684,6 @@ var init_health = __esm({
|
|
|
42108
43684
|
DEFAULT_TIMEOUT_MS4 = 500;
|
|
42109
43685
|
}
|
|
42110
43686
|
});
|
|
42111
|
-
|
|
42112
|
-
// src/cli/agents/cli.ts
|
|
42113
43687
|
function resolveCtx(args) {
|
|
42114
43688
|
const env = args.env ?? process.env;
|
|
42115
43689
|
const discoverOpts = {
|
|
@@ -42147,6 +43721,8 @@ async function runAgentsCommand(args) {
|
|
|
42147
43721
|
return await cmdShow2(rest, ctx);
|
|
42148
43722
|
case "status":
|
|
42149
43723
|
return await cmdStatus(rest, ctx);
|
|
43724
|
+
case "config":
|
|
43725
|
+
return await cmdConfig(rest, ctx);
|
|
42150
43726
|
default:
|
|
42151
43727
|
ctx.err.write(`Unknown subcommand: ${sub}
|
|
42152
43728
|
`);
|
|
@@ -42162,10 +43738,18 @@ async function runAgentsCommand(args) {
|
|
|
42162
43738
|
}
|
|
42163
43739
|
function printUsage4(s) {
|
|
42164
43740
|
s.write(`Usage: sanctuary agents <command> [flags]
|
|
43741
|
+
sanctuary agent <command> [flags] (alias)
|
|
42165
43742
|
|
|
42166
43743
|
list [--json] List every tenant visible on this host.
|
|
42167
|
-
show <tenant> [--json] Show details for one tenant
|
|
43744
|
+
show <tenant> [--json] Show details for one tenant (includes
|
|
43745
|
+
approval-redirect state).
|
|
42168
43746
|
status [--json] One-line-per-tenant running/stopped summary.
|
|
43747
|
+
config <tenant> [opts] Write tenant principal-policy.yaml fields.
|
|
43748
|
+
--approval-redirect=<bool> Toggle cross-harness inbox redirect.
|
|
43749
|
+
--approval-redirect-mode=<replace|notify>
|
|
43750
|
+
Pick replace (bypass underlying channel)
|
|
43751
|
+
or notify (race both paths). Default
|
|
43752
|
+
replace when toggled on.
|
|
42169
43753
|
|
|
42170
43754
|
Options:
|
|
42171
43755
|
--fortress <path> Scope discovery to a specific storage path
|
|
@@ -42267,6 +43851,7 @@ async function cmdShow2(argv, ctx) {
|
|
|
42267
43851
|
return 1;
|
|
42268
43852
|
}
|
|
42269
43853
|
const probe = await ctx.probe(tenant);
|
|
43854
|
+
const approvalRedirect = await readApprovalRedirectState(tenant);
|
|
42270
43855
|
const payload = {
|
|
42271
43856
|
name: tenant.name,
|
|
42272
43857
|
storage_path: tenant.storage_path,
|
|
@@ -42280,7 +43865,8 @@ async function cmdShow2(argv, ctx) {
|
|
|
42280
43865
|
running: probe.running,
|
|
42281
43866
|
status: probe.status,
|
|
42282
43867
|
reason: probe.reason
|
|
42283
|
-
}
|
|
43868
|
+
},
|
|
43869
|
+
approval_redirect: approvalRedirect
|
|
42284
43870
|
};
|
|
42285
43871
|
if (hasJsonFlag(argv)) {
|
|
42286
43872
|
ctx.out.write(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -42324,10 +43910,173 @@ async function cmdShow2(argv, ctx) {
|
|
|
42324
43910
|
}
|
|
42325
43911
|
ctx.out.write(
|
|
42326
43912
|
`probe: ${probe.running ? "running" : "not-running"}${probe.reason ? ` (${probe.reason})` : ""}
|
|
43913
|
+
`
|
|
43914
|
+
);
|
|
43915
|
+
ctx.out.write(
|
|
43916
|
+
`approval_redirect: ${approvalRedirect.enabled ? `on (${approvalRedirect.mode})` : "off"}
|
|
42327
43917
|
`
|
|
42328
43918
|
);
|
|
42329
43919
|
return 0;
|
|
42330
43920
|
}
|
|
43921
|
+
async function readApprovalRedirectState(tenant) {
|
|
43922
|
+
const policyPath = join(tenant.storage_path, "principal-policy.yaml");
|
|
43923
|
+
try {
|
|
43924
|
+
const content = await readFile(policyPath, "utf-8");
|
|
43925
|
+
const parsed = parsePolicy(content);
|
|
43926
|
+
const cfg = parsed.approval_redirect;
|
|
43927
|
+
if (!cfg) return { enabled: false, mode: "replace" };
|
|
43928
|
+
return {
|
|
43929
|
+
enabled: !!cfg.enabled,
|
|
43930
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
43931
|
+
};
|
|
43932
|
+
} catch {
|
|
43933
|
+
return { enabled: false, mode: "replace" };
|
|
43934
|
+
}
|
|
43935
|
+
}
|
|
43936
|
+
function parseBoolFlag(raw) {
|
|
43937
|
+
if (raw === void 0) return null;
|
|
43938
|
+
const v = raw.toLowerCase();
|
|
43939
|
+
if (v === "true" || v === "yes" || v === "on" || v === "1") return true;
|
|
43940
|
+
if (v === "false" || v === "no" || v === "off" || v === "0") return false;
|
|
43941
|
+
return null;
|
|
43942
|
+
}
|
|
43943
|
+
function findFlagValue(argv, name) {
|
|
43944
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43945
|
+
const a = argv[i];
|
|
43946
|
+
if (a === name) {
|
|
43947
|
+
return argv[i + 1];
|
|
43948
|
+
}
|
|
43949
|
+
const eq = `${name}=`;
|
|
43950
|
+
if (a.startsWith(eq)) {
|
|
43951
|
+
return a.slice(eq.length);
|
|
43952
|
+
}
|
|
43953
|
+
}
|
|
43954
|
+
return void 0;
|
|
43955
|
+
}
|
|
43956
|
+
async function cmdConfig(argv, ctx) {
|
|
43957
|
+
const positional = argv.find((a) => !a.startsWith("--"));
|
|
43958
|
+
if (!positional) {
|
|
43959
|
+
ctx.err.write(
|
|
43960
|
+
"Missing tenant. Usage: sanctuary agents config <tenant> --approval-redirect=<bool>\n"
|
|
43961
|
+
);
|
|
43962
|
+
return 2;
|
|
43963
|
+
}
|
|
43964
|
+
const tenant = await findTenant(positional, ctx.discoverOpts);
|
|
43965
|
+
if (!tenant) {
|
|
43966
|
+
ctx.err.write(`sanctuary agents: unknown tenant "${positional}"
|
|
43967
|
+
`);
|
|
43968
|
+
return 1;
|
|
43969
|
+
}
|
|
43970
|
+
const redirectFlag = parseBoolFlag(
|
|
43971
|
+
findFlagValue(argv, "--approval-redirect")
|
|
43972
|
+
);
|
|
43973
|
+
const modeFlag = findFlagValue(argv, "--approval-redirect-mode");
|
|
43974
|
+
if (redirectFlag === null && modeFlag === void 0) {
|
|
43975
|
+
ctx.err.write(
|
|
43976
|
+
"sanctuary agents config: nothing to do. Pass --approval-redirect=<bool> or --approval-redirect-mode=<replace|notify>.\n"
|
|
43977
|
+
);
|
|
43978
|
+
return 2;
|
|
43979
|
+
}
|
|
43980
|
+
if (modeFlag !== void 0 && modeFlag !== "replace" && modeFlag !== "notify") {
|
|
43981
|
+
ctx.err.write(
|
|
43982
|
+
`sanctuary agents config: --approval-redirect-mode must be "replace" or "notify" (got "${modeFlag}")
|
|
43983
|
+
`
|
|
43984
|
+
);
|
|
43985
|
+
return 2;
|
|
43986
|
+
}
|
|
43987
|
+
const current = await readApprovalRedirectState(tenant);
|
|
43988
|
+
const next = {
|
|
43989
|
+
enabled: redirectFlag !== null ? redirectFlag : current.enabled,
|
|
43990
|
+
mode: modeFlag === "notify" || modeFlag === "replace" ? modeFlag : current.mode
|
|
43991
|
+
};
|
|
43992
|
+
await writeApprovalRedirectToPolicyFile(tenant.storage_path, next);
|
|
43993
|
+
if (hasJsonFlag(argv)) {
|
|
43994
|
+
ctx.out.write(
|
|
43995
|
+
JSON.stringify(
|
|
43996
|
+
{
|
|
43997
|
+
tenant: tenant.name,
|
|
43998
|
+
approval_redirect: next
|
|
43999
|
+
},
|
|
44000
|
+
null,
|
|
44001
|
+
2
|
|
44002
|
+
) + "\n"
|
|
44003
|
+
);
|
|
44004
|
+
} else {
|
|
44005
|
+
ctx.out.write(
|
|
44006
|
+
`sanctuary agents config: tenant "${tenant.name}" approval_redirect=${next.enabled ? `on (${next.mode})` : "off"}
|
|
44007
|
+
`
|
|
44008
|
+
);
|
|
44009
|
+
ctx.out.write(
|
|
44010
|
+
` Takes effect on the next gate request for the running server.
|
|
44011
|
+
`
|
|
44012
|
+
);
|
|
44013
|
+
}
|
|
44014
|
+
return 0;
|
|
44015
|
+
}
|
|
44016
|
+
async function writeApprovalRedirectToPolicyFile(storagePath, state) {
|
|
44017
|
+
const policyPath = join(storagePath, "principal-policy.yaml");
|
|
44018
|
+
let content;
|
|
44019
|
+
try {
|
|
44020
|
+
content = await readFile(policyPath, "utf-8");
|
|
44021
|
+
} catch (err) {
|
|
44022
|
+
const code = err?.code;
|
|
44023
|
+
if (code !== "ENOENT") throw err;
|
|
44024
|
+
content = await defaultPolicyTextForBootstrap();
|
|
44025
|
+
}
|
|
44026
|
+
const block = renderApprovalRedirectBlock(state);
|
|
44027
|
+
const updated = upsertApprovalRedirectBlock(content, block);
|
|
44028
|
+
await writeFile(policyPath, updated, "utf-8");
|
|
44029
|
+
await chmod(policyPath, 384);
|
|
44030
|
+
}
|
|
44031
|
+
function renderApprovalRedirectBlock(state) {
|
|
44032
|
+
return [
|
|
44033
|
+
"# Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2)",
|
|
44034
|
+
"approval_redirect:",
|
|
44035
|
+
` enabled: ${state.enabled ? "true" : "false"}`,
|
|
44036
|
+
` mode: ${state.mode}`
|
|
44037
|
+
].join("\n");
|
|
44038
|
+
}
|
|
44039
|
+
function upsertApprovalRedirectBlock(content, block) {
|
|
44040
|
+
const lines = content.split("\n");
|
|
44041
|
+
const startIdx = lines.findIndex((l) => l.startsWith("approval_redirect:"));
|
|
44042
|
+
if (startIdx === -1) {
|
|
44043
|
+
const trimmed = content.endsWith("\n") ? content : content + "\n";
|
|
44044
|
+
return trimmed + "\n" + block + "\n";
|
|
44045
|
+
}
|
|
44046
|
+
let blockStart = startIdx;
|
|
44047
|
+
if (blockStart > 0 && lines[blockStart - 1] !== void 0 && lines[blockStart - 1].startsWith("# Approval Redirect")) {
|
|
44048
|
+
blockStart = blockStart - 1;
|
|
44049
|
+
}
|
|
44050
|
+
let blockEnd = startIdx + 1;
|
|
44051
|
+
while (blockEnd < lines.length) {
|
|
44052
|
+
const l = lines[blockEnd];
|
|
44053
|
+
if (l === "") {
|
|
44054
|
+
blockEnd++;
|
|
44055
|
+
continue;
|
|
44056
|
+
}
|
|
44057
|
+
if (/^[A-Za-z0-9#]/.test(l)) {
|
|
44058
|
+
break;
|
|
44059
|
+
}
|
|
44060
|
+
blockEnd++;
|
|
44061
|
+
}
|
|
44062
|
+
const before = lines.slice(0, blockStart);
|
|
44063
|
+
const after = lines.slice(blockEnd);
|
|
44064
|
+
const replaced = [...before, ...block.split("\n"), ...after].join("\n");
|
|
44065
|
+
return replaced.endsWith("\n") ? replaced : replaced + "\n";
|
|
44066
|
+
}
|
|
44067
|
+
async function defaultPolicyTextForBootstrap() {
|
|
44068
|
+
return [
|
|
44069
|
+
"version: 1",
|
|
44070
|
+
"tier1_always_approve:",
|
|
44071
|
+
" - state_export",
|
|
44072
|
+
" - state_import",
|
|
44073
|
+
" - state_delete",
|
|
44074
|
+
"approval_channel:",
|
|
44075
|
+
" type: stderr",
|
|
44076
|
+
" timeout_seconds: 300",
|
|
44077
|
+
""
|
|
44078
|
+
].join("\n");
|
|
44079
|
+
}
|
|
42331
44080
|
async function cmdStatus(argv, ctx) {
|
|
42332
44081
|
const tenants = await discoverTenants(ctx.discoverOpts);
|
|
42333
44082
|
const probes = await Promise.all(tenants.map((t) => ctx.probe(t)));
|
|
@@ -42368,6 +44117,7 @@ var init_cli5 = __esm({
|
|
|
42368
44117
|
"src/cli/agents/cli.ts"() {
|
|
42369
44118
|
init_discovery();
|
|
42370
44119
|
init_health();
|
|
44120
|
+
init_loader();
|
|
42371
44121
|
}
|
|
42372
44122
|
});
|
|
42373
44123
|
|
|
@@ -43796,7 +45546,7 @@ async function main() {
|
|
|
43796
45546
|
const code = await runIdentityCommand2({ argv: args.slice(1) });
|
|
43797
45547
|
process.exit(code);
|
|
43798
45548
|
}
|
|
43799
|
-
if (args[0] === "agents") {
|
|
45549
|
+
if (args[0] === "agents" || args[0] === "agent") {
|
|
43800
45550
|
const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
43801
45551
|
const code = await runAgentsCommand2({ argv: args.slice(1) });
|
|
43802
45552
|
process.exit(code);
|