@sanctuary-framework/mcp-server 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2329 -136
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2329 -136
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2009 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +689 -7
- package/dist/index.d.ts +689 -7
- package/dist/index.js +2009 -133
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4261,6 +4261,10 @@ var DEFAULT_CHANNEL = {
|
|
|
4261
4261
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4262
4262
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4263
4263
|
};
|
|
4264
|
+
var DEFAULT_APPROVAL_REDIRECT = {
|
|
4265
|
+
enabled: false,
|
|
4266
|
+
mode: "replace"
|
|
4267
|
+
};
|
|
4264
4268
|
var DEFAULT_POLICY = {
|
|
4265
4269
|
version: 1,
|
|
4266
4270
|
tier1_always_approve: [
|
|
@@ -4334,6 +4338,7 @@ var DEFAULT_POLICY = {
|
|
|
4334
4338
|
"handshake_status",
|
|
4335
4339
|
"handshake_exchange",
|
|
4336
4340
|
"handshake_verify_attestation",
|
|
4341
|
+
"handshake_abort",
|
|
4337
4342
|
"reputation_query_weighted",
|
|
4338
4343
|
"federation_peers",
|
|
4339
4344
|
"federation_trust_evaluate",
|
|
@@ -4375,7 +4380,8 @@ var DEFAULT_POLICY = {
|
|
|
4375
4380
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4376
4381
|
// Read-only; rule-based Annex III classifier
|
|
4377
4382
|
],
|
|
4378
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4383
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4384
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4379
4385
|
};
|
|
4380
4386
|
function extractOperationName(toolName) {
|
|
4381
4387
|
if (toolName.startsWith("proxy/")) {
|
|
@@ -4472,9 +4478,35 @@ function validatePolicy(raw) {
|
|
|
4472
4478
|
};
|
|
4473
4479
|
delete merged.auto_deny;
|
|
4474
4480
|
return merged;
|
|
4475
|
-
})()
|
|
4481
|
+
})(),
|
|
4482
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4476
4483
|
};
|
|
4477
4484
|
}
|
|
4485
|
+
function parseApprovalRedirect(raw) {
|
|
4486
|
+
if (raw === void 0 || raw === null) {
|
|
4487
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4488
|
+
}
|
|
4489
|
+
if (typeof raw !== "object") {
|
|
4490
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4491
|
+
}
|
|
4492
|
+
const obj = raw;
|
|
4493
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4494
|
+
const modeRaw = obj.mode;
|
|
4495
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4496
|
+
if (modeRaw !== void 0) {
|
|
4497
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4498
|
+
throw new Error(
|
|
4499
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
mode = modeRaw;
|
|
4503
|
+
}
|
|
4504
|
+
const result = { enabled, mode };
|
|
4505
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4506
|
+
result.per_agent = obj.per_agent;
|
|
4507
|
+
}
|
|
4508
|
+
return result;
|
|
4509
|
+
}
|
|
4478
4510
|
function generateDefaultPolicyYaml() {
|
|
4479
4511
|
return `# Sanctuary Principal Policy v1
|
|
4480
4512
|
# This file controls what your agent can do without asking.
|
|
@@ -4553,6 +4585,7 @@ tier3_always_allow:
|
|
|
4553
4585
|
- handshake_status
|
|
4554
4586
|
- handshake_exchange
|
|
4555
4587
|
- handshake_verify_attestation
|
|
4588
|
+
- handshake_abort
|
|
4556
4589
|
- reputation_query_weighted
|
|
4557
4590
|
- federation_peers
|
|
4558
4591
|
- federation_trust_evaluate
|
|
@@ -4587,22 +4620,69 @@ tier3_always_allow:
|
|
|
4587
4620
|
approval_channel:
|
|
4588
4621
|
type: stderr
|
|
4589
4622
|
timeout_seconds: 300
|
|
4623
|
+
|
|
4624
|
+
# \u2500\u2500\u2500 Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4625
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4626
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4627
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4628
|
+
#
|
|
4629
|
+
# mode:
|
|
4630
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4631
|
+
# decision from the inbox (default once enabled).
|
|
4632
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4633
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4634
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4635
|
+
approval_redirect:
|
|
4636
|
+
enabled: false
|
|
4637
|
+
mode: replace
|
|
4590
4638
|
`;
|
|
4591
4639
|
}
|
|
4640
|
+
var MalformedPrincipalPolicyError = class extends Error {
|
|
4641
|
+
constructor(policyPath, reason) {
|
|
4642
|
+
super(
|
|
4643
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4644
|
+
Reason: ${reason}
|
|
4645
|
+
Sanctuary refuses to substitute a default policy when an existing file is present, to avoid silently overriding operator intent. Fix the file or delete it to regenerate the default.`
|
|
4646
|
+
);
|
|
4647
|
+
this.policyPath = policyPath;
|
|
4648
|
+
this.reason = reason;
|
|
4649
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4650
|
+
}
|
|
4651
|
+
policyPath;
|
|
4652
|
+
reason;
|
|
4653
|
+
};
|
|
4592
4654
|
async function loadPrincipalPolicy(storagePath) {
|
|
4593
4655
|
const policyPath = path.join(storagePath, "principal-policy.yaml");
|
|
4656
|
+
let content;
|
|
4657
|
+
try {
|
|
4658
|
+
content = await promises.readFile(policyPath, "utf-8");
|
|
4659
|
+
} catch (err) {
|
|
4660
|
+
const code = err?.code;
|
|
4661
|
+
if (code === "ENOENT") {
|
|
4662
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4663
|
+
try {
|
|
4664
|
+
await promises.writeFile(policyPath, defaultYaml, "utf-8");
|
|
4665
|
+
await promises.chmod(policyPath, 384);
|
|
4666
|
+
} catch (writeErr) {
|
|
4667
|
+
console.warn(
|
|
4668
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4669
|
+
);
|
|
4670
|
+
}
|
|
4671
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4672
|
+
}
|
|
4673
|
+
throw new MalformedPrincipalPolicyError(
|
|
4674
|
+
policyPath,
|
|
4675
|
+
`read failed: ${err.message}`
|
|
4676
|
+
);
|
|
4677
|
+
}
|
|
4594
4678
|
try {
|
|
4595
|
-
const content = await promises.readFile(policyPath, "utf-8");
|
|
4596
4679
|
const policy = parsePolicy(content);
|
|
4597
4680
|
return Object.freeze(policy);
|
|
4598
|
-
} catch {
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
} catch {
|
|
4604
|
-
}
|
|
4605
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4681
|
+
} catch (parseErr) {
|
|
4682
|
+
throw new MalformedPrincipalPolicyError(
|
|
4683
|
+
policyPath,
|
|
4684
|
+
parseErr.message
|
|
4685
|
+
);
|
|
4606
4686
|
}
|
|
4607
4687
|
}
|
|
4608
4688
|
|
|
@@ -4829,7 +4909,7 @@ function deepSortKeys(obj) {
|
|
|
4829
4909
|
return sorted;
|
|
4830
4910
|
}
|
|
4831
4911
|
function canonicalizeForSigning(body) {
|
|
4832
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4912
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4833
4913
|
}
|
|
4834
4914
|
|
|
4835
4915
|
// src/shr/generator.ts
|
|
@@ -11651,6 +11731,16 @@ var HUB_ROUTES = {
|
|
|
11651
11731
|
*/
|
|
11652
11732
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
11653
11733
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
11734
|
+
/**
|
|
11735
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
11736
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
11737
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
11738
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
11739
|
+
* v1.3 conversational sovereignty depth.
|
|
11740
|
+
*/
|
|
11741
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
11742
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11743
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11654
11744
|
/**
|
|
11655
11745
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
11656
11746
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -11674,6 +11764,10 @@ var HUB_TIER_1_AGENT_CONTROL_ACTIONS = [
|
|
|
11674
11764
|
];
|
|
11675
11765
|
var HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
11676
11766
|
var HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
11767
|
+
var HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
11768
|
+
var HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
11769
|
+
var HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
11770
|
+
var HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
11677
11771
|
var HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
11678
11772
|
var HUB_INBOX_MAX_LIMIT = 500;
|
|
11679
11773
|
var HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -11845,6 +11939,23 @@ function checkChatMessage(value) {
|
|
|
11845
11939
|
}
|
|
11846
11940
|
return trimmed;
|
|
11847
11941
|
}
|
|
11942
|
+
function matchConciergeThreadRoute(path) {
|
|
11943
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
11944
|
+
if (!path.startsWith(prefix)) return null;
|
|
11945
|
+
const rest = path.slice(prefix.length);
|
|
11946
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
11947
|
+
const decoded = decodeURIComponent(rest);
|
|
11948
|
+
if (decoded.length === 0) return null;
|
|
11949
|
+
return { threadId: decoded };
|
|
11950
|
+
}
|
|
11951
|
+
function parseSince(raw) {
|
|
11952
|
+
if (raw === null || raw === "") return void 0;
|
|
11953
|
+
const parsed = Number.parseInt(raw, 10);
|
|
11954
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
11955
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
11956
|
+
}
|
|
11957
|
+
return parsed;
|
|
11958
|
+
}
|
|
11848
11959
|
function matchInboxRoute(path) {
|
|
11849
11960
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
11850
11961
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12028,6 +12139,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12028
12139
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12029
12140
|
return true;
|
|
12030
12141
|
}
|
|
12142
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12143
|
+
const limit = parseLimit(
|
|
12144
|
+
url.searchParams.get("limit"),
|
|
12145
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12146
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12147
|
+
);
|
|
12148
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12149
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12150
|
+
return true;
|
|
12151
|
+
}
|
|
12152
|
+
{
|
|
12153
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12154
|
+
if (threadMatch) {
|
|
12155
|
+
if (method === "GET") {
|
|
12156
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12157
|
+
const limit = parseLimit(
|
|
12158
|
+
url.searchParams.get("limit"),
|
|
12159
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12160
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12161
|
+
);
|
|
12162
|
+
const readOpts = { limit };
|
|
12163
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12164
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12165
|
+
threadMatch.threadId,
|
|
12166
|
+
readOpts
|
|
12167
|
+
);
|
|
12168
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12169
|
+
return true;
|
|
12170
|
+
}
|
|
12171
|
+
if (method === "DELETE") {
|
|
12172
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12173
|
+
threadMatch.threadId
|
|
12174
|
+
);
|
|
12175
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12176
|
+
ok: removed,
|
|
12177
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12178
|
+
});
|
|
12179
|
+
return true;
|
|
12180
|
+
}
|
|
12181
|
+
}
|
|
12182
|
+
}
|
|
12031
12183
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12032
12184
|
return true;
|
|
12033
12185
|
} catch (err) {
|
|
@@ -16077,6 +16229,162 @@ async function dispatchV11Request(inputs, req, res, url, method) {
|
|
|
16077
16229
|
return false;
|
|
16078
16230
|
}
|
|
16079
16231
|
|
|
16232
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
16233
|
+
var APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
16234
|
+
var APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
16235
|
+
var APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
16236
|
+
var APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
16237
|
+
function writeJSON4(res, status, payload) {
|
|
16238
|
+
res.writeHead(status, {
|
|
16239
|
+
"Content-Type": "application/json",
|
|
16240
|
+
"Cache-Control": "no-store"
|
|
16241
|
+
});
|
|
16242
|
+
res.end(JSON.stringify(payload));
|
|
16243
|
+
}
|
|
16244
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
16245
|
+
if (raw === null || raw === "") return defaultValue;
|
|
16246
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16247
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
16248
|
+
return defaultValue;
|
|
16249
|
+
}
|
|
16250
|
+
return Math.min(parsed, max);
|
|
16251
|
+
}
|
|
16252
|
+
function isStatusFilter(value) {
|
|
16253
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
16254
|
+
}
|
|
16255
|
+
function matchEntryRoute(path) {
|
|
16256
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
16257
|
+
if (!path.startsWith(prefix)) return null;
|
|
16258
|
+
const rest = path.slice(prefix.length);
|
|
16259
|
+
if (rest.length === 0) return null;
|
|
16260
|
+
const slash = rest.indexOf("/");
|
|
16261
|
+
if (slash === -1) {
|
|
16262
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
16263
|
+
}
|
|
16264
|
+
return {
|
|
16265
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
16266
|
+
action: rest.slice(slash + 1)
|
|
16267
|
+
};
|
|
16268
|
+
}
|
|
16269
|
+
async function handleStream2(deps, res) {
|
|
16270
|
+
res.writeHead(200, {
|
|
16271
|
+
"Content-Type": "text/event-stream",
|
|
16272
|
+
"Cache-Control": "no-cache, no-transform",
|
|
16273
|
+
Connection: "keep-alive",
|
|
16274
|
+
"X-Accel-Buffering": "no"
|
|
16275
|
+
});
|
|
16276
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
16277
|
+
res.write(
|
|
16278
|
+
`event: approval_inbox_snapshot
|
|
16279
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
16280
|
+
|
|
16281
|
+
`
|
|
16282
|
+
);
|
|
16283
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
16284
|
+
try {
|
|
16285
|
+
res.write(
|
|
16286
|
+
`event: approval_inbox_${event.type}
|
|
16287
|
+
data: ${JSON.stringify(event.entry)}
|
|
16288
|
+
|
|
16289
|
+
`
|
|
16290
|
+
);
|
|
16291
|
+
} catch {
|
|
16292
|
+
}
|
|
16293
|
+
});
|
|
16294
|
+
const keepAlive = setInterval(() => {
|
|
16295
|
+
try {
|
|
16296
|
+
res.write(": keepalive\n\n");
|
|
16297
|
+
} catch {
|
|
16298
|
+
}
|
|
16299
|
+
}, 25e3);
|
|
16300
|
+
const cleanup = () => {
|
|
16301
|
+
clearInterval(keepAlive);
|
|
16302
|
+
unsubscribe();
|
|
16303
|
+
};
|
|
16304
|
+
res.on("close", cleanup);
|
|
16305
|
+
res.on("error", cleanup);
|
|
16306
|
+
}
|
|
16307
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
16308
|
+
const host = req.headers.host || "localhost";
|
|
16309
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
16310
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
16311
|
+
const path = url.pathname;
|
|
16312
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
16313
|
+
return false;
|
|
16314
|
+
}
|
|
16315
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
16316
|
+
if (!checkAuth(req, res, url)) return true;
|
|
16317
|
+
try {
|
|
16318
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
16319
|
+
await handleStream2(deps, res);
|
|
16320
|
+
return true;
|
|
16321
|
+
}
|
|
16322
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
16323
|
+
const limit = parseLimit2(
|
|
16324
|
+
url.searchParams.get("limit"),
|
|
16325
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
16326
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
16327
|
+
);
|
|
16328
|
+
const statusRaw = url.searchParams.get("status");
|
|
16329
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
16330
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
16331
|
+
const entries = await deps.aggregator.list({
|
|
16332
|
+
status,
|
|
16333
|
+
limit,
|
|
16334
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
16335
|
+
});
|
|
16336
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
16337
|
+
return true;
|
|
16338
|
+
}
|
|
16339
|
+
const entryMatch = matchEntryRoute(path);
|
|
16340
|
+
if (entryMatch === null) {
|
|
16341
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16342
|
+
return true;
|
|
16343
|
+
}
|
|
16344
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
16345
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
16346
|
+
const entry = entries.find(
|
|
16347
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
16348
|
+
);
|
|
16349
|
+
if (!entry) {
|
|
16350
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16351
|
+
return true;
|
|
16352
|
+
}
|
|
16353
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
16354
|
+
entryMatch.aggregatorId
|
|
16355
|
+
);
|
|
16356
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
16357
|
+
return true;
|
|
16358
|
+
}
|
|
16359
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
16360
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
16361
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16362
|
+
try {
|
|
16363
|
+
const entry = await deps.aggregator.resolve(
|
|
16364
|
+
entryMatch.aggregatorId,
|
|
16365
|
+
decision,
|
|
16366
|
+
operatorId
|
|
16367
|
+
);
|
|
16368
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
16369
|
+
} catch (err) {
|
|
16370
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16371
|
+
if (msg === "approval-aggregator: not_found") {
|
|
16372
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16373
|
+
} else {
|
|
16374
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16375
|
+
}
|
|
16376
|
+
}
|
|
16377
|
+
return true;
|
|
16378
|
+
}
|
|
16379
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16380
|
+
return true;
|
|
16381
|
+
} catch (err) {
|
|
16382
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16383
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16384
|
+
return true;
|
|
16385
|
+
}
|
|
16386
|
+
}
|
|
16387
|
+
|
|
16080
16388
|
// src/principal-policy/dashboard.ts
|
|
16081
16389
|
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16082
16390
|
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -16141,6 +16449,14 @@ var DashboardApprovalChannel = class {
|
|
|
16141
16449
|
* regardless. Default route flip is deferred to v1.2.
|
|
16142
16450
|
*/
|
|
16143
16451
|
v11Bindings = null;
|
|
16452
|
+
/**
|
|
16453
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
16454
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
16455
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
16456
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
16457
|
+
* the operator-facing query / decision surface.
|
|
16458
|
+
*/
|
|
16459
|
+
approvalAggregator = null;
|
|
16144
16460
|
constructor(config) {
|
|
16145
16461
|
this.config = config;
|
|
16146
16462
|
this.authToken = config.auth_token;
|
|
@@ -16191,6 +16507,34 @@ var DashboardApprovalChannel = class {
|
|
|
16191
16507
|
setV11Bindings(bindings) {
|
|
16192
16508
|
this.v11Bindings = bindings;
|
|
16193
16509
|
}
|
|
16510
|
+
/**
|
|
16511
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
16512
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
16513
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
16514
|
+
* tests + during shutdown).
|
|
16515
|
+
*/
|
|
16516
|
+
setApprovalAggregator(aggregator) {
|
|
16517
|
+
this.approvalAggregator = aggregator;
|
|
16518
|
+
}
|
|
16519
|
+
/**
|
|
16520
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
16521
|
+
* before the legacy approval route table. Returns true when served.
|
|
16522
|
+
*/
|
|
16523
|
+
async dispatchApprovalInbox(req, res) {
|
|
16524
|
+
if (!this.approvalAggregator) return false;
|
|
16525
|
+
return handleApprovalInboxRoute(
|
|
16526
|
+
{
|
|
16527
|
+
authConfig: {
|
|
16528
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
16529
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
16530
|
+
},
|
|
16531
|
+
aggregator: this.approvalAggregator,
|
|
16532
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
16533
|
+
},
|
|
16534
|
+
req,
|
|
16535
|
+
res
|
|
16536
|
+
);
|
|
16537
|
+
}
|
|
16194
16538
|
/**
|
|
16195
16539
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
16196
16540
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -16566,6 +16910,18 @@ var DashboardApprovalChannel = class {
|
|
|
16566
16910
|
res.end();
|
|
16567
16911
|
return;
|
|
16568
16912
|
}
|
|
16913
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
16914
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
16915
|
+
if (handled) return;
|
|
16916
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
16917
|
+
}).catch(() => {
|
|
16918
|
+
if (!res.headersSent) {
|
|
16919
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
16920
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
16921
|
+
}
|
|
16922
|
+
});
|
|
16923
|
+
return;
|
|
16924
|
+
}
|
|
16569
16925
|
if (this.v11Bindings) {
|
|
16570
16926
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
16571
16927
|
if (handled) return;
|
|
@@ -18584,14 +18940,25 @@ var ApprovalGate = class {
|
|
|
18584
18940
|
auditLog;
|
|
18585
18941
|
injectionDetector;
|
|
18586
18942
|
onInjectionAlert;
|
|
18943
|
+
onApprovalEvent;
|
|
18587
18944
|
proxyTierResolver;
|
|
18588
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
18945
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
18589
18946
|
this.policy = policy;
|
|
18590
18947
|
this.baseline = baseline;
|
|
18591
18948
|
this.channel = channel;
|
|
18592
18949
|
this.auditLog = auditLog;
|
|
18593
18950
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
18594
18951
|
this.onInjectionAlert = onInjectionAlert;
|
|
18952
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
18953
|
+
}
|
|
18954
|
+
/**
|
|
18955
|
+
* Set the approval-event callback after construction. Used by the
|
|
18956
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
18957
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
18958
|
+
* constructor so existing call sites continue to work unchanged.
|
|
18959
|
+
*/
|
|
18960
|
+
setApprovalEventCallback(cb) {
|
|
18961
|
+
this.onApprovalEvent = cb;
|
|
18595
18962
|
}
|
|
18596
18963
|
/**
|
|
18597
18964
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -18825,21 +19192,105 @@ var ApprovalGate = class {
|
|
|
18825
19192
|
}
|
|
18826
19193
|
/**
|
|
18827
19194
|
* Request approval from the human principal.
|
|
19195
|
+
*
|
|
19196
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
19197
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
19198
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
19199
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
19200
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
19201
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
18828
19202
|
*/
|
|
18829
19203
|
async requestApproval(operation, tier, reason, context) {
|
|
19204
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
18830
19205
|
const request = {
|
|
18831
19206
|
operation,
|
|
18832
19207
|
tier,
|
|
18833
19208
|
reason,
|
|
18834
19209
|
context,
|
|
18835
|
-
timestamp:
|
|
19210
|
+
timestamp: requestTimestamp
|
|
18836
19211
|
};
|
|
18837
|
-
const
|
|
19212
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
19213
|
+
if (this.onApprovalEvent) {
|
|
19214
|
+
try {
|
|
19215
|
+
this.onApprovalEvent({
|
|
19216
|
+
phase: "requested",
|
|
19217
|
+
operation,
|
|
19218
|
+
tier,
|
|
19219
|
+
reason,
|
|
19220
|
+
context,
|
|
19221
|
+
request_timestamp: requestTimestamp,
|
|
19222
|
+
correlation_id: correlationId
|
|
19223
|
+
});
|
|
19224
|
+
} catch {
|
|
19225
|
+
}
|
|
19226
|
+
}
|
|
19227
|
+
let response;
|
|
19228
|
+
try {
|
|
19229
|
+
response = await this.channel.requestApproval(request);
|
|
19230
|
+
} catch (err) {
|
|
19231
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
19232
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19233
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
19234
|
+
tier,
|
|
19235
|
+
reason,
|
|
19236
|
+
decided_by: "channel_failure",
|
|
19237
|
+
channel_error: errMessage
|
|
19238
|
+
});
|
|
19239
|
+
if (this.onApprovalEvent) {
|
|
19240
|
+
try {
|
|
19241
|
+
this.onApprovalEvent({
|
|
19242
|
+
phase: "resolved",
|
|
19243
|
+
operation,
|
|
19244
|
+
tier,
|
|
19245
|
+
reason,
|
|
19246
|
+
context,
|
|
19247
|
+
request_timestamp: requestTimestamp,
|
|
19248
|
+
resolution: {
|
|
19249
|
+
decision: "deny",
|
|
19250
|
+
decided_at: decidedAt,
|
|
19251
|
+
decided_by: "channel_failure"
|
|
19252
|
+
},
|
|
19253
|
+
correlation_id: correlationId
|
|
19254
|
+
});
|
|
19255
|
+
} catch {
|
|
19256
|
+
}
|
|
19257
|
+
}
|
|
19258
|
+
return {
|
|
19259
|
+
allowed: false,
|
|
19260
|
+
tier,
|
|
19261
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
19262
|
+
approval_required: true,
|
|
19263
|
+
approval_response: {
|
|
19264
|
+
decision: "deny",
|
|
19265
|
+
decided_at: decidedAt,
|
|
19266
|
+
decided_by: "channel_failure"
|
|
19267
|
+
}
|
|
19268
|
+
};
|
|
19269
|
+
}
|
|
18838
19270
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
18839
19271
|
tier,
|
|
18840
19272
|
reason,
|
|
18841
19273
|
decided_by: response.decided_by
|
|
18842
19274
|
});
|
|
19275
|
+
if (this.onApprovalEvent) {
|
|
19276
|
+
try {
|
|
19277
|
+
this.onApprovalEvent({
|
|
19278
|
+
phase: "resolved",
|
|
19279
|
+
operation,
|
|
19280
|
+
tier,
|
|
19281
|
+
reason,
|
|
19282
|
+
context,
|
|
19283
|
+
request_timestamp: requestTimestamp,
|
|
19284
|
+
resolution: {
|
|
19285
|
+
decision: response.decision,
|
|
19286
|
+
decided_at: response.decided_at,
|
|
19287
|
+
decided_by: response.decided_by
|
|
19288
|
+
},
|
|
19289
|
+
correlation_id: correlationId
|
|
19290
|
+
});
|
|
19291
|
+
} catch {
|
|
19292
|
+
}
|
|
19293
|
+
}
|
|
18843
19294
|
return {
|
|
18844
19295
|
allowed: response.decision === "approve",
|
|
18845
19296
|
tier,
|
|
@@ -18873,50 +19324,559 @@ var ApprovalGate = class {
|
|
|
18873
19324
|
}
|
|
18874
19325
|
};
|
|
18875
19326
|
|
|
18876
|
-
// src/principal-policy/
|
|
18877
|
-
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
|
|
18885
|
-
|
|
18886
|
-
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
19327
|
+
// src/principal-policy/approval-aggregator.ts
|
|
19328
|
+
init_encryption();
|
|
19329
|
+
init_encoding();
|
|
19330
|
+
var APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
19331
|
+
var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
19332
|
+
var APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
19333
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
19334
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
19335
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
19336
|
+
};
|
|
19337
|
+
var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
19338
|
+
var DEFAULT_MAX_LIST_LIMIT = 200;
|
|
19339
|
+
var DEFAULT_LIST_PAGE_SIZE = 50;
|
|
19340
|
+
var ApprovalAggregator = class {
|
|
19341
|
+
storage;
|
|
19342
|
+
encryptionKey;
|
|
19343
|
+
auditLog;
|
|
19344
|
+
identityId;
|
|
19345
|
+
fortressId;
|
|
19346
|
+
pendingTtlMs;
|
|
19347
|
+
maxListLimit;
|
|
19348
|
+
now;
|
|
19349
|
+
resolveSourceContext;
|
|
19350
|
+
resolveHubInboxItemId;
|
|
19351
|
+
/** Cached entries by `aggregator_id`. */
|
|
19352
|
+
entries = /* @__PURE__ */ new Map();
|
|
19353
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
19354
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
19355
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
19356
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
19357
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
19358
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
19359
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
19360
|
+
hydrated = false;
|
|
19361
|
+
/** Active SSE listeners. */
|
|
19362
|
+
listeners = /* @__PURE__ */ new Set();
|
|
19363
|
+
constructor(deps) {
|
|
19364
|
+
this.storage = deps.storage;
|
|
19365
|
+
this.encryptionKey = derivePurposeKey(
|
|
19366
|
+
deps.masterKey,
|
|
19367
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
19368
|
+
);
|
|
19369
|
+
this.auditLog = deps.auditLog;
|
|
19370
|
+
this.identityId = deps.identityId;
|
|
19371
|
+
this.fortressId = deps.fortressId;
|
|
19372
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
19373
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
19374
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
19375
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
19376
|
+
source_harness: this.fortressId,
|
|
19377
|
+
source_agent_id: this.fortressId
|
|
19378
|
+
}));
|
|
19379
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
19380
|
+
}
|
|
19381
|
+
/**
|
|
19382
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
19383
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
19384
|
+
*/
|
|
19385
|
+
onEvent(listener) {
|
|
19386
|
+
this.listeners.add(listener);
|
|
19387
|
+
return () => this.listeners.delete(listener);
|
|
19388
|
+
}
|
|
19389
|
+
/**
|
|
19390
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
19391
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
19392
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
19393
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
19394
|
+
* a record for).
|
|
19395
|
+
*/
|
|
19396
|
+
async ingest(event) {
|
|
19397
|
+
await this.hydrate();
|
|
19398
|
+
if (event.phase === "requested") {
|
|
19399
|
+
return this.ingestRequested(event);
|
|
19400
|
+
}
|
|
19401
|
+
if (event.phase === "resolved") {
|
|
19402
|
+
return this.ingestResolved(event);
|
|
19403
|
+
}
|
|
19404
|
+
return null;
|
|
19405
|
+
}
|
|
19406
|
+
/**
|
|
19407
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
19408
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
19409
|
+
* snapshot is returned.
|
|
19410
|
+
*/
|
|
19411
|
+
async list(opts) {
|
|
19412
|
+
await this.hydrate();
|
|
19413
|
+
await this.expireStale();
|
|
19414
|
+
const limit = Math.min(
|
|
19415
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
19416
|
+
this.maxListLimit
|
|
19417
|
+
);
|
|
19418
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
19419
|
+
const matching = [];
|
|
19420
|
+
for (const entry of this.entries.values()) {
|
|
19421
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
19422
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
19423
|
+
matching.push(entry);
|
|
19424
|
+
}
|
|
19425
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
19426
|
+
return matching.slice(0, limit);
|
|
19427
|
+
}
|
|
19428
|
+
/**
|
|
19429
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
19430
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
19431
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
19432
|
+
*/
|
|
19433
|
+
async getFullPayload(aggregatorId) {
|
|
19434
|
+
await this.hydrate();
|
|
19435
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
19436
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
19437
|
+
}
|
|
19438
|
+
/**
|
|
19439
|
+
* Resolve an entry. Used by both:
|
|
19440
|
+
* 1. The gate wire-up on channel-decision return.
|
|
19441
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
19442
|
+
*
|
|
19443
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
19444
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
19445
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
19446
|
+
* routes return 404.
|
|
19447
|
+
*/
|
|
19448
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
19449
|
+
await this.hydrate();
|
|
19450
|
+
const entry = this.entries.get(aggregatorId);
|
|
19451
|
+
if (!entry) {
|
|
19452
|
+
throw new Error("approval-aggregator: not_found");
|
|
19453
|
+
}
|
|
19454
|
+
if (entry.status !== "pending") {
|
|
19455
|
+
return entry;
|
|
19456
|
+
}
|
|
19457
|
+
entry.status = decision;
|
|
19458
|
+
entry.resolved_at = this.now().toISOString();
|
|
19459
|
+
entry.resolved_by = operatorId;
|
|
19460
|
+
await this.persist(entry);
|
|
19461
|
+
this.auditLog.append(
|
|
19462
|
+
"l2",
|
|
19463
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19464
|
+
this.identityId,
|
|
19465
|
+
{
|
|
19466
|
+
aggregator_id: entry.aggregator_id,
|
|
19467
|
+
source_harness: entry.source_harness,
|
|
19468
|
+
source_agent_id: entry.source_agent_id,
|
|
19469
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19470
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19471
|
+
decision,
|
|
19472
|
+
decided_by: operatorId,
|
|
19473
|
+
decided_at: entry.resolved_at
|
|
19474
|
+
}
|
|
19475
|
+
);
|
|
19476
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19477
|
+
return entry;
|
|
19478
|
+
}
|
|
19479
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
19480
|
+
async ingestRequested(event) {
|
|
19481
|
+
const ctx = this.resolveSourceContext(event);
|
|
19482
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
19483
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
19484
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
19485
|
+
if (existing) {
|
|
19486
|
+
const existingEntry = this.entries.get(existing);
|
|
19487
|
+
if (existingEntry) {
|
|
19488
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
19489
|
+
this.auditLog.append(
|
|
19490
|
+
"l2",
|
|
19491
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
19492
|
+
this.identityId,
|
|
19493
|
+
{
|
|
19494
|
+
aggregator_id: existing,
|
|
19495
|
+
source_harness: ctx.source_harness,
|
|
19496
|
+
source_agent_id: ctx.source_agent_id,
|
|
19497
|
+
audit_log_entry_id: auditId,
|
|
19498
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19499
|
+
correlation_id: event.correlation_id
|
|
19500
|
+
}
|
|
19501
|
+
);
|
|
19502
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
19503
|
+
return null;
|
|
19504
|
+
}
|
|
19505
|
+
}
|
|
19506
|
+
const id = crypto.randomUUID();
|
|
19507
|
+
const now = this.now();
|
|
19508
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
19509
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
19510
|
+
const entry = {
|
|
19511
|
+
aggregator_id: id,
|
|
19512
|
+
source_harness: ctx.source_harness,
|
|
19513
|
+
source_agent_id: ctx.source_agent_id,
|
|
19514
|
+
audit_log_entry_id: auditId,
|
|
19515
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19516
|
+
action_summary: this.deriveActionSummary(event),
|
|
19517
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
19518
|
+
status: "pending",
|
|
19519
|
+
created_at: now.toISOString(),
|
|
19520
|
+
expires_at: expires.toISOString(),
|
|
19521
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19522
|
+
};
|
|
19523
|
+
this.entries.set(id, entry);
|
|
19524
|
+
this.dedupIndex.set(dedupKey, id);
|
|
19525
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
19526
|
+
this.fullPayloads.set(id, event.context);
|
|
19527
|
+
await this.persist(entry);
|
|
19528
|
+
this.auditLog.append(
|
|
19529
|
+
"l2",
|
|
19530
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
19531
|
+
this.identityId,
|
|
19532
|
+
{
|
|
19533
|
+
aggregator_id: id,
|
|
19534
|
+
source_harness: ctx.source_harness,
|
|
19535
|
+
source_agent_id: ctx.source_agent_id,
|
|
19536
|
+
audit_log_entry_id: auditId,
|
|
19537
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19538
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19539
|
+
}
|
|
19540
|
+
);
|
|
19541
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
19542
|
+
return entry;
|
|
19543
|
+
}
|
|
19544
|
+
async ingestResolved(event) {
|
|
19545
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
19546
|
+
if (!id) return null;
|
|
19547
|
+
const entry = this.entries.get(id);
|
|
19548
|
+
if (!entry) return null;
|
|
19549
|
+
if (entry.status !== "pending") return entry;
|
|
19550
|
+
if (!event.resolution) return entry;
|
|
19551
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
19552
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
19553
|
+
entry.status = status;
|
|
19554
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
19555
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
19556
|
+
await this.persist(entry);
|
|
19557
|
+
this.auditLog.append(
|
|
19558
|
+
"l2",
|
|
19559
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19560
|
+
this.identityId,
|
|
19561
|
+
{
|
|
19562
|
+
aggregator_id: id,
|
|
19563
|
+
source_harness: entry.source_harness,
|
|
19564
|
+
source_agent_id: entry.source_agent_id,
|
|
19565
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19566
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19567
|
+
decision: status,
|
|
19568
|
+
decided_by: entry.resolved_by,
|
|
19569
|
+
decided_at: entry.resolved_at,
|
|
19570
|
+
fail_closed: failClosed
|
|
19571
|
+
}
|
|
19572
|
+
);
|
|
19573
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19574
|
+
return entry;
|
|
19575
|
+
}
|
|
19576
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
19577
|
+
/**
|
|
19578
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
19579
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
19580
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
19581
|
+
* the audit entry the gate appended on the same call.
|
|
19582
|
+
*/
|
|
19583
|
+
auditEntryIdForEvent(event) {
|
|
19584
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
19585
|
+
}
|
|
19586
|
+
derivePolicyRuleId(event) {
|
|
19587
|
+
return `tier${event.tier}:${event.operation}`;
|
|
19588
|
+
}
|
|
19589
|
+
deriveActionSummary(event) {
|
|
19590
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
19591
|
+
}
|
|
19592
|
+
/**
|
|
19593
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
19594
|
+
* identical payloads always hash the same, even when key insertion order
|
|
19595
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
19596
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
19597
|
+
*/
|
|
19598
|
+
hashPayload(payload) {
|
|
19599
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
19600
|
+
return crypto.createHash("sha256").update(canonical).digest("hex");
|
|
19601
|
+
}
|
|
19602
|
+
emit(event) {
|
|
19603
|
+
for (const listener of this.listeners) {
|
|
19604
|
+
try {
|
|
19605
|
+
listener(event);
|
|
19606
|
+
} catch {
|
|
19607
|
+
}
|
|
19608
|
+
}
|
|
19609
|
+
}
|
|
19610
|
+
async expireStale() {
|
|
19611
|
+
const nowMs = this.now().getTime();
|
|
19612
|
+
for (const entry of this.entries.values()) {
|
|
19613
|
+
if (entry.status !== "pending") continue;
|
|
19614
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
19615
|
+
entry.status = "expired";
|
|
19616
|
+
entry.resolved_at = this.now().toISOString();
|
|
19617
|
+
entry.resolved_by = "system_ttl";
|
|
19618
|
+
await this.persist(entry);
|
|
19619
|
+
this.auditLog.append(
|
|
19620
|
+
"l2",
|
|
19621
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19622
|
+
this.identityId,
|
|
19623
|
+
{
|
|
19624
|
+
aggregator_id: entry.aggregator_id,
|
|
19625
|
+
source_harness: entry.source_harness,
|
|
19626
|
+
source_agent_id: entry.source_agent_id,
|
|
19627
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19628
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19629
|
+
decision: "expired",
|
|
19630
|
+
decided_by: "system_ttl",
|
|
19631
|
+
decided_at: entry.resolved_at
|
|
19632
|
+
}
|
|
19633
|
+
);
|
|
19634
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19635
|
+
}
|
|
19636
|
+
}
|
|
19637
|
+
async persist(entry) {
|
|
19638
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
19639
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
19640
|
+
await this.storage.write(
|
|
19641
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19642
|
+
entry.aggregator_id,
|
|
19643
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
19644
|
+
);
|
|
19645
|
+
}
|
|
19646
|
+
async hydrate() {
|
|
19647
|
+
if (this.hydrated) return;
|
|
19648
|
+
this.hydrated = true;
|
|
19649
|
+
try {
|
|
19650
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
19651
|
+
for (const meta of metas) {
|
|
19652
|
+
const raw = await this.storage.read(
|
|
19653
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19654
|
+
meta.key
|
|
19655
|
+
);
|
|
19656
|
+
if (!raw) continue;
|
|
19657
|
+
try {
|
|
19658
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
19659
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
19660
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
19661
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
19662
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
19663
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
19664
|
+
} catch {
|
|
19665
|
+
}
|
|
19666
|
+
}
|
|
19667
|
+
} catch {
|
|
19668
|
+
this.hydrated = false;
|
|
19669
|
+
}
|
|
19670
|
+
}
|
|
19671
|
+
};
|
|
19672
|
+
|
|
19673
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
19674
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
19675
|
+
function auditEntryIdFor(request) {
|
|
19676
|
+
return `${request.timestamp}:${request.operation}`;
|
|
19677
|
+
}
|
|
19678
|
+
function statusToDecision(entry) {
|
|
19679
|
+
switch (entry.status) {
|
|
19680
|
+
case "approved":
|
|
19681
|
+
return {
|
|
19682
|
+
decision: "approve",
|
|
19683
|
+
decided_by: "human"
|
|
19684
|
+
};
|
|
19685
|
+
case "denied":
|
|
19686
|
+
return {
|
|
19687
|
+
decision: "deny",
|
|
19688
|
+
decided_by: "human"
|
|
19689
|
+
};
|
|
19690
|
+
case "timeout":
|
|
19691
|
+
case "expired":
|
|
19692
|
+
return {
|
|
19693
|
+
decision: "deny",
|
|
19694
|
+
decided_by: "timeout"
|
|
19695
|
+
};
|
|
19696
|
+
default:
|
|
19697
|
+
return null;
|
|
19698
|
+
}
|
|
19699
|
+
}
|
|
19700
|
+
var AggregatorBackedChannel = class {
|
|
19701
|
+
underlying;
|
|
19702
|
+
aggregator;
|
|
19703
|
+
resolveRedirect;
|
|
19704
|
+
replaceModeTimeoutMs;
|
|
19705
|
+
now;
|
|
19706
|
+
constructor(opts) {
|
|
19707
|
+
this.underlying = opts.underlying;
|
|
19708
|
+
this.aggregator = opts.aggregator;
|
|
19709
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
19710
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
19711
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
19712
|
+
}
|
|
19713
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
19714
|
+
getUnderlying() {
|
|
19715
|
+
return this.underlying;
|
|
19716
|
+
}
|
|
19717
|
+
async requestApproval(request) {
|
|
19718
|
+
const cfg = this.resolveRedirect(request);
|
|
19719
|
+
if (!cfg.enabled) {
|
|
19720
|
+
return this.underlying.requestApproval(request);
|
|
19721
|
+
}
|
|
19722
|
+
if (cfg.mode === "replace") {
|
|
19723
|
+
return this.awaitAggregatorDecision(request);
|
|
19724
|
+
}
|
|
19725
|
+
return this.notifyMode(request);
|
|
19726
|
+
}
|
|
19727
|
+
/**
|
|
19728
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
19729
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
19730
|
+
* between list and subscribe). Match incoming events to this request by
|
|
19731
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
19732
|
+
*/
|
|
19733
|
+
async awaitAggregatorDecision(request) {
|
|
19734
|
+
const auditId = auditEntryIdFor(request);
|
|
19735
|
+
return new Promise((resolveOuter) => {
|
|
19736
|
+
let settled = false;
|
|
19737
|
+
let unsubscribe = null;
|
|
19738
|
+
let timeoutHandle = null;
|
|
19739
|
+
const settle = (response) => {
|
|
19740
|
+
if (settled) return;
|
|
19741
|
+
settled = true;
|
|
19742
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
19743
|
+
if (unsubscribe) {
|
|
19744
|
+
try {
|
|
19745
|
+
unsubscribe();
|
|
19746
|
+
} catch {
|
|
19747
|
+
}
|
|
19748
|
+
}
|
|
19749
|
+
resolveOuter(response);
|
|
19750
|
+
};
|
|
19751
|
+
const onEvent = (emit) => {
|
|
19752
|
+
if (emit.type !== "resolved") return;
|
|
19753
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
19754
|
+
const mapped = statusToDecision(emit.entry);
|
|
19755
|
+
if (!mapped) return;
|
|
19756
|
+
settle({
|
|
19757
|
+
decision: mapped.decision,
|
|
19758
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
19759
|
+
decided_by: mapped.decided_by
|
|
19760
|
+
});
|
|
19761
|
+
};
|
|
19762
|
+
try {
|
|
19763
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
19764
|
+
} catch (err) {
|
|
19765
|
+
settle({
|
|
19766
|
+
decision: "deny",
|
|
19767
|
+
decided_at: this.now().toISOString(),
|
|
19768
|
+
decided_by: "channel_failure"
|
|
19769
|
+
});
|
|
19770
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
19771
|
+
}
|
|
19772
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
19773
|
+
for (const entry of entries) {
|
|
19774
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
19775
|
+
const mapped = statusToDecision(entry);
|
|
19776
|
+
if (!mapped) return;
|
|
19777
|
+
settle({
|
|
19778
|
+
decision: mapped.decision,
|
|
19779
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
19780
|
+
decided_by: mapped.decided_by
|
|
19781
|
+
});
|
|
19782
|
+
return;
|
|
19783
|
+
}
|
|
19784
|
+
}).catch(() => {
|
|
19785
|
+
});
|
|
19786
|
+
timeoutHandle = setTimeout(() => {
|
|
19787
|
+
settle({
|
|
19788
|
+
decision: "deny",
|
|
19789
|
+
decided_at: this.now().toISOString(),
|
|
19790
|
+
decided_by: "timeout"
|
|
19791
|
+
});
|
|
19792
|
+
}, this.replaceModeTimeoutMs);
|
|
19793
|
+
});
|
|
19794
|
+
}
|
|
19795
|
+
/**
|
|
19796
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
19797
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
19798
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
19799
|
+
* downstream audit logging is unchanged.
|
|
19800
|
+
*
|
|
19801
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
19802
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
19803
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
19804
|
+
*/
|
|
19805
|
+
async notifyMode(request) {
|
|
19806
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
19807
|
+
let underlyingPromise;
|
|
19808
|
+
try {
|
|
19809
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
19810
|
+
} catch (err) {
|
|
19811
|
+
const response = await aggregatorPromise;
|
|
19812
|
+
return response;
|
|
19813
|
+
}
|
|
19814
|
+
return Promise.race([
|
|
19815
|
+
aggregatorPromise,
|
|
19816
|
+
underlyingPromise.catch(
|
|
19817
|
+
() => new Promise(() => {
|
|
19818
|
+
})
|
|
19819
|
+
)
|
|
19820
|
+
]);
|
|
19821
|
+
}
|
|
19822
|
+
};
|
|
19823
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
19824
|
+
return (_request) => {
|
|
19825
|
+
const cfg = supplier().approval_redirect;
|
|
19826
|
+
if (!cfg || cfg.enabled !== true) {
|
|
19827
|
+
return { enabled: false, mode: "replace" };
|
|
19828
|
+
}
|
|
19829
|
+
return {
|
|
19830
|
+
enabled: true,
|
|
19831
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
19832
|
+
};
|
|
19833
|
+
};
|
|
19834
|
+
}
|
|
19835
|
+
|
|
19836
|
+
// src/principal-policy/tools.ts
|
|
19837
|
+
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
19838
|
+
return [
|
|
19839
|
+
{
|
|
19840
|
+
name: "principal_policy_view",
|
|
19841
|
+
description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
|
|
19842
|
+
inputSchema: {
|
|
19843
|
+
type: "object",
|
|
19844
|
+
properties: {
|
|
19845
|
+
include_defaults: {
|
|
19846
|
+
type: "boolean",
|
|
19847
|
+
description: "Include tier3_always_allow list (can be long)",
|
|
19848
|
+
default: false
|
|
19849
|
+
}
|
|
19850
|
+
}
|
|
19851
|
+
},
|
|
19852
|
+
handler: async (args) => {
|
|
19853
|
+
const includeDefaults = args.include_defaults ?? false;
|
|
19854
|
+
const view = {
|
|
19855
|
+
version: policy.version,
|
|
19856
|
+
tier1_always_approve: policy.tier1_always_approve,
|
|
19857
|
+
tier2_anomaly: policy.tier2_anomaly,
|
|
19858
|
+
approval_channel: {
|
|
19859
|
+
type: policy.approval_channel.type,
|
|
19860
|
+
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
19861
|
+
auto_deny: true
|
|
19862
|
+
// SEC-002: hardcoded, not configurable
|
|
19863
|
+
}
|
|
19864
|
+
};
|
|
19865
|
+
if (includeDefaults) {
|
|
19866
|
+
view.tier3_always_allow = policy.tier3_always_allow;
|
|
19867
|
+
} else {
|
|
19868
|
+
view.tier3_always_allow_count = policy.tier3_always_allow.length;
|
|
19869
|
+
view.note = "Pass include_defaults: true to see the full tier3_always_allow list";
|
|
19870
|
+
}
|
|
19871
|
+
auditLog.append("l2", "principal_policy_view", "system", {
|
|
19872
|
+
include_defaults: includeDefaults
|
|
19873
|
+
});
|
|
19874
|
+
return toolResult(view);
|
|
19875
|
+
}
|
|
19876
|
+
},
|
|
19877
|
+
{
|
|
19878
|
+
name: "principal_baseline_view",
|
|
19879
|
+
description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
|
|
18920
19880
|
inputSchema: {
|
|
18921
19881
|
type: "object",
|
|
18922
19882
|
properties: {}
|
|
@@ -19743,6 +20703,71 @@ function verifyAttestation(attestation, now) {
|
|
|
19743
20703
|
};
|
|
19744
20704
|
}
|
|
19745
20705
|
|
|
20706
|
+
// src/handshake/audit.ts
|
|
20707
|
+
var HANDSHAKE_LIFECYCLE_OPS = {
|
|
20708
|
+
INITIATED: "handshake_initiated",
|
|
20709
|
+
COMPLETED: "handshake_completed",
|
|
20710
|
+
FAILED: "handshake_failed",
|
|
20711
|
+
ABORTED: "handshake_aborted"
|
|
20712
|
+
};
|
|
20713
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
20714
|
+
auditLog.append(
|
|
20715
|
+
"l4",
|
|
20716
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
20717
|
+
ctx.identity_id,
|
|
20718
|
+
detailsFromContext(ctx),
|
|
20719
|
+
"success"
|
|
20720
|
+
);
|
|
20721
|
+
}
|
|
20722
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
20723
|
+
const details = detailsFromContext(ctx);
|
|
20724
|
+
if (ctx.trust_tier !== void 0) {
|
|
20725
|
+
details.trust_tier = ctx.trust_tier;
|
|
20726
|
+
}
|
|
20727
|
+
auditLog.append(
|
|
20728
|
+
"l4",
|
|
20729
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
20730
|
+
ctx.identity_id,
|
|
20731
|
+
details,
|
|
20732
|
+
"success"
|
|
20733
|
+
);
|
|
20734
|
+
}
|
|
20735
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
20736
|
+
const details = detailsFromContext(ctx);
|
|
20737
|
+
details.reason = ctx.reason;
|
|
20738
|
+
if (ctx.error !== void 0) {
|
|
20739
|
+
details.error = ctx.error;
|
|
20740
|
+
}
|
|
20741
|
+
auditLog.append(
|
|
20742
|
+
"l4",
|
|
20743
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
20744
|
+
ctx.identity_id,
|
|
20745
|
+
details,
|
|
20746
|
+
"failure"
|
|
20747
|
+
);
|
|
20748
|
+
}
|
|
20749
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
20750
|
+
const details = detailsFromContext(ctx);
|
|
20751
|
+
details.reason = ctx.reason;
|
|
20752
|
+
auditLog.append(
|
|
20753
|
+
"l4",
|
|
20754
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
20755
|
+
ctx.identity_id,
|
|
20756
|
+
details,
|
|
20757
|
+
"failure"
|
|
20758
|
+
);
|
|
20759
|
+
}
|
|
20760
|
+
function detailsFromContext(ctx) {
|
|
20761
|
+
const details = {
|
|
20762
|
+
session_id: ctx.session_id,
|
|
20763
|
+
role: ctx.role
|
|
20764
|
+
};
|
|
20765
|
+
if (ctx.counterparty_id !== void 0) {
|
|
20766
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
20767
|
+
}
|
|
20768
|
+
return details;
|
|
20769
|
+
}
|
|
20770
|
+
|
|
19746
20771
|
// src/handshake/tools.ts
|
|
19747
20772
|
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
19748
20773
|
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
@@ -19776,6 +20801,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19776
20801
|
const { challenge, session } = initiateHandshake(shr);
|
|
19777
20802
|
sessions.set(session.session_id, session);
|
|
19778
20803
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
20804
|
+
auditHandshakeInitiated(auditLog, {
|
|
20805
|
+
session_id: session.session_id,
|
|
20806
|
+
role: "initiator",
|
|
20807
|
+
identity_id: shr.body.instance_id
|
|
20808
|
+
});
|
|
19779
20809
|
return toolResult({
|
|
19780
20810
|
session_id: session.session_id,
|
|
19781
20811
|
challenge,
|
|
@@ -19815,10 +20845,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19815
20845
|
);
|
|
19816
20846
|
if ("error" in result) {
|
|
19817
20847
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
20848
|
+
auditHandshakeFailed(auditLog, {
|
|
20849
|
+
session_id: "unknown",
|
|
20850
|
+
role: "responder",
|
|
20851
|
+
identity_id: shr.body.instance_id,
|
|
20852
|
+
reason: classifyRespondFailure(result.error),
|
|
20853
|
+
error: result.error
|
|
20854
|
+
});
|
|
19818
20855
|
return toolResult({ error: result.error });
|
|
19819
20856
|
}
|
|
19820
20857
|
sessions.set(result.session.session_id, result.session);
|
|
19821
20858
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
20859
|
+
auditHandshakeInitiated(auditLog, {
|
|
20860
|
+
session_id: result.session.session_id,
|
|
20861
|
+
role: "responder",
|
|
20862
|
+
identity_id: shr.body.instance_id,
|
|
20863
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
20864
|
+
});
|
|
19822
20865
|
let autoPublishResult;
|
|
19823
20866
|
if (autoPublishHandshakes) {
|
|
19824
20867
|
autoPublishResult = { attempted: true };
|
|
@@ -19926,9 +20969,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19926
20969
|
const response = args.response;
|
|
19927
20970
|
const session = sessions.get(sessionId);
|
|
19928
20971
|
if (!session) {
|
|
20972
|
+
auditHandshakeFailed(auditLog, {
|
|
20973
|
+
session_id: sessionId,
|
|
20974
|
+
role: "initiator",
|
|
20975
|
+
identity_id: "unknown",
|
|
20976
|
+
reason: "session_unknown",
|
|
20977
|
+
error: `No handshake session found: ${sessionId}`
|
|
20978
|
+
});
|
|
19929
20979
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
19930
20980
|
}
|
|
19931
20981
|
if (session.state !== "initiated") {
|
|
20982
|
+
auditHandshakeFailed(auditLog, {
|
|
20983
|
+
session_id: sessionId,
|
|
20984
|
+
role: "initiator",
|
|
20985
|
+
identity_id: session.our_shr.body.instance_id,
|
|
20986
|
+
reason: "session_state_mismatch",
|
|
20987
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
20988
|
+
});
|
|
19932
20989
|
return toolResult({
|
|
19933
20990
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
19934
20991
|
});
|
|
@@ -19942,6 +20999,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19942
20999
|
if ("error" in result) {
|
|
19943
21000
|
session.state = "failed";
|
|
19944
21001
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
21002
|
+
auditHandshakeFailed(auditLog, {
|
|
21003
|
+
session_id: sessionId,
|
|
21004
|
+
role: "initiator",
|
|
21005
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21006
|
+
reason: classifyCompleteFailure(result.error),
|
|
21007
|
+
error: result.error
|
|
21008
|
+
});
|
|
19945
21009
|
return toolResult({ error: result.error });
|
|
19946
21010
|
}
|
|
19947
21011
|
session.state = "completed";
|
|
@@ -19950,6 +21014,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19950
21014
|
session.result = result.result;
|
|
19951
21015
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
19952
21016
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
21017
|
+
auditHandshakeCompleted(auditLog, {
|
|
21018
|
+
session_id: sessionId,
|
|
21019
|
+
role: "initiator",
|
|
21020
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21021
|
+
counterparty_id: result.result.counterparty_id,
|
|
21022
|
+
trust_tier: result.result.trust_tier
|
|
21023
|
+
});
|
|
19953
21024
|
return toolResult({
|
|
19954
21025
|
completion: result.completion,
|
|
19955
21026
|
result: result.result,
|
|
@@ -19997,6 +21068,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19997
21068
|
void 0,
|
|
19998
21069
|
result.verified ? "success" : "failure"
|
|
19999
21070
|
);
|
|
21071
|
+
if (result.verified) {
|
|
21072
|
+
auditHandshakeCompleted(auditLog, {
|
|
21073
|
+
session_id: session.session_id,
|
|
21074
|
+
role: "responder",
|
|
21075
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21076
|
+
counterparty_id: result.counterparty_id,
|
|
21077
|
+
trust_tier: result.trust_tier
|
|
21078
|
+
});
|
|
21079
|
+
} else {
|
|
21080
|
+
auditHandshakeFailed(auditLog, {
|
|
21081
|
+
session_id: session.session_id,
|
|
21082
|
+
role: "responder",
|
|
21083
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21084
|
+
counterparty_id: result.counterparty_id,
|
|
21085
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
21086
|
+
error: result.errors.join("; ")
|
|
21087
|
+
});
|
|
21088
|
+
}
|
|
20000
21089
|
return toolResult({ result });
|
|
20001
21090
|
}
|
|
20002
21091
|
return toolResult({
|
|
@@ -20104,10 +21193,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20104
21193
|
_content_trust: "external"
|
|
20105
21194
|
});
|
|
20106
21195
|
}
|
|
21196
|
+
},
|
|
21197
|
+
{
|
|
21198
|
+
name: "handshake_abort",
|
|
21199
|
+
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.",
|
|
21200
|
+
inputSchema: {
|
|
21201
|
+
type: "object",
|
|
21202
|
+
properties: {
|
|
21203
|
+
session_id: {
|
|
21204
|
+
type: "string",
|
|
21205
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
21206
|
+
},
|
|
21207
|
+
reason: {
|
|
21208
|
+
type: "string",
|
|
21209
|
+
enum: [
|
|
21210
|
+
"operator_cancelled",
|
|
21211
|
+
"session_timeout",
|
|
21212
|
+
"transport_dropped",
|
|
21213
|
+
"shutdown",
|
|
21214
|
+
"other"
|
|
21215
|
+
],
|
|
21216
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
21217
|
+
}
|
|
21218
|
+
},
|
|
21219
|
+
required: ["session_id"]
|
|
21220
|
+
},
|
|
21221
|
+
handler: async (args) => {
|
|
21222
|
+
const sessionId = args.session_id;
|
|
21223
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
21224
|
+
const session = sessions.get(sessionId);
|
|
21225
|
+
if (!session) {
|
|
21226
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21227
|
+
}
|
|
21228
|
+
if (session.state === "completed") {
|
|
21229
|
+
return toolResult({
|
|
21230
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
21231
|
+
});
|
|
21232
|
+
}
|
|
21233
|
+
sessions.delete(sessionId);
|
|
21234
|
+
auditHandshakeAborted(auditLog, {
|
|
21235
|
+
session_id: sessionId,
|
|
21236
|
+
role: session.role,
|
|
21237
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21238
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
21239
|
+
reason
|
|
21240
|
+
});
|
|
21241
|
+
return toolResult({
|
|
21242
|
+
aborted: true,
|
|
21243
|
+
session_id: sessionId,
|
|
21244
|
+
reason
|
|
21245
|
+
});
|
|
21246
|
+
}
|
|
20107
21247
|
}
|
|
20108
21248
|
];
|
|
20109
21249
|
return { tools, handshakeResults };
|
|
20110
21250
|
}
|
|
21251
|
+
function classifyRespondFailure(error) {
|
|
21252
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21253
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
21254
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21255
|
+
return "other";
|
|
21256
|
+
}
|
|
21257
|
+
function classifyCompleteFailure(error) {
|
|
21258
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21259
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
21260
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
21261
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21262
|
+
return "other";
|
|
21263
|
+
}
|
|
20111
21264
|
|
|
20112
21265
|
// src/federation/registry.ts
|
|
20113
21266
|
var DEFAULT_CAPABILITIES = {
|
|
@@ -21725,6 +22878,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
21725
22878
|
}
|
|
21726
22879
|
async function consumeResetHistoryMarker(options) {
|
|
21727
22880
|
const markerPath = path.join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
22881
|
+
const consumedPath = markerPath + ".consumed";
|
|
22882
|
+
if (await fileExists3(consumedPath)) {
|
|
22883
|
+
await promises.rm(markerPath, { force: true });
|
|
22884
|
+
await promises.rm(consumedPath, { force: true });
|
|
22885
|
+
return { emitted: 0, markerPath };
|
|
22886
|
+
}
|
|
21728
22887
|
if (!await fileExists3(markerPath)) {
|
|
21729
22888
|
return { emitted: 0, markerPath };
|
|
21730
22889
|
}
|
|
@@ -21749,7 +22908,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
21749
22908
|
});
|
|
21750
22909
|
}
|
|
21751
22910
|
await options.auditLog.flush();
|
|
22911
|
+
await promises.writeFile(consumedPath, "", "utf-8");
|
|
21752
22912
|
await promises.rm(markerPath, { force: true });
|
|
22913
|
+
await promises.rm(consumedPath, { force: true });
|
|
21753
22914
|
return { emitted: markers.length, markerHash, markerPath };
|
|
21754
22915
|
}
|
|
21755
22916
|
async function fileExists3(path) {
|
|
@@ -30693,7 +31854,37 @@ var HubService = class {
|
|
|
30693
31854
|
*/
|
|
30694
31855
|
async getConciergeHistory() {
|
|
30695
31856
|
const chat = this.requireOperatorChat();
|
|
30696
|
-
return chat.getConciergeHistory();
|
|
31857
|
+
return chat.getConciergeHistory();
|
|
31858
|
+
}
|
|
31859
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
31860
|
+
/**
|
|
31861
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
31862
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
31863
|
+
* surface is unavailable on a given fortress.
|
|
31864
|
+
*/
|
|
31865
|
+
hasConciergeMemory() {
|
|
31866
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
31867
|
+
}
|
|
31868
|
+
async listConciergeMemoryThreads(opts) {
|
|
31869
|
+
const chat = this.requireOperatorChat();
|
|
31870
|
+
if (!chat.hasConciergeMemory()) {
|
|
31871
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31872
|
+
}
|
|
31873
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
31874
|
+
}
|
|
31875
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
31876
|
+
const chat = this.requireOperatorChat();
|
|
31877
|
+
if (!chat.hasConciergeMemory()) {
|
|
31878
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31879
|
+
}
|
|
31880
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
31881
|
+
}
|
|
31882
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
31883
|
+
const chat = this.requireOperatorChat();
|
|
31884
|
+
if (!chat.hasConciergeMemory()) {
|
|
31885
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31886
|
+
}
|
|
31887
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
30697
31888
|
}
|
|
30698
31889
|
/**
|
|
30699
31890
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
@@ -30782,7 +31973,28 @@ init_encoding();
|
|
|
30782
31973
|
|
|
30783
31974
|
// src/chat/operator-chat-audit-events.ts
|
|
30784
31975
|
var OPERATOR_CHAT_OPS = {
|
|
30785
|
-
CONCIERGE_CHAT: "operator_concierge_chat"
|
|
31976
|
+
CONCIERGE_CHAT: "operator_concierge_chat",
|
|
31977
|
+
/**
|
|
31978
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
31979
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
31980
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
31981
|
+
* raw turn content never crosses the audit surface.
|
|
31982
|
+
*/
|
|
31983
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
31984
|
+
/**
|
|
31985
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
31986
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
31987
|
+
* the deleted bundle.
|
|
31988
|
+
*/
|
|
31989
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
31990
|
+
/**
|
|
31991
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
31992
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
31993
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
31994
|
+
* carries thread_id + a stable failure_reason enum.
|
|
31995
|
+
*/
|
|
31996
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
|
|
31997
|
+
};
|
|
30786
31998
|
|
|
30787
31999
|
// src/chat/operator-chat-types.ts
|
|
30788
32000
|
var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
|
|
@@ -30790,6 +32002,13 @@ var CONCIERGE_THREAD_KEY = "_fortress";
|
|
|
30790
32002
|
|
|
30791
32003
|
// src/chat/operator-chat-service.ts
|
|
30792
32004
|
var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
32005
|
+
var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
32006
|
+
var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
32007
|
+
var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
32008
|
+
var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32009
|
+
function approxTokenLen(text) {
|
|
32010
|
+
return Math.ceil(text.length / 4);
|
|
32011
|
+
}
|
|
30793
32012
|
var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
30794
32013
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
30795
32014
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -30825,6 +32044,27 @@ var OperatorChatService = class {
|
|
|
30825
32044
|
contextProviders;
|
|
30826
32045
|
piiFilter;
|
|
30827
32046
|
conciergeMaxTokens;
|
|
32047
|
+
memory;
|
|
32048
|
+
historyWindowTurns;
|
|
32049
|
+
historyFreshnessMs;
|
|
32050
|
+
historyTokenBudget;
|
|
32051
|
+
sessionTtlMs;
|
|
32052
|
+
clock;
|
|
32053
|
+
/**
|
|
32054
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
32055
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
32056
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
32057
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
32058
|
+
*/
|
|
32059
|
+
activeMemoryThreadId;
|
|
32060
|
+
/**
|
|
32061
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
32062
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
32063
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
32064
|
+
* allocates a new thread_id even though the prior one is still
|
|
32065
|
+
* readable from the memory store.
|
|
32066
|
+
*/
|
|
32067
|
+
lastInteractionAt;
|
|
30828
32068
|
constructor(deps) {
|
|
30829
32069
|
this.store = deps.store;
|
|
30830
32070
|
this.auditLog = deps.auditLog;
|
|
@@ -30835,6 +32075,12 @@ var OperatorChatService = class {
|
|
|
30835
32075
|
}
|
|
30836
32076
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
30837
32077
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
32078
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32079
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
32080
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
32081
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
32082
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
32083
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
30838
32084
|
}
|
|
30839
32085
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
30840
32086
|
/**
|
|
@@ -30853,6 +32099,10 @@ var OperatorChatService = class {
|
|
|
30853
32099
|
throw new Error("concierge query must not be empty");
|
|
30854
32100
|
}
|
|
30855
32101
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
32102
|
+
const nowMs = this.clock();
|
|
32103
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
32104
|
+
this.activeMemoryThreadId = void 0;
|
|
32105
|
+
}
|
|
30856
32106
|
const operatorMessage = {
|
|
30857
32107
|
message_id: crypto.randomUUID(),
|
|
30858
32108
|
surface: "concierge",
|
|
@@ -30865,6 +32115,30 @@ var OperatorChatService = class {
|
|
|
30865
32115
|
CONCIERGE_THREAD_KEY,
|
|
30866
32116
|
operatorMessage
|
|
30867
32117
|
);
|
|
32118
|
+
let priorTurns = [];
|
|
32119
|
+
let memoryReadFailureReason = null;
|
|
32120
|
+
let activeThreadIdForRound;
|
|
32121
|
+
if (this.memory) {
|
|
32122
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
32123
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
32124
|
+
if (result.ok) {
|
|
32125
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
32126
|
+
const fresh = result.turns.filter((t) => {
|
|
32127
|
+
const ts = Date.parse(t.created_at);
|
|
32128
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
32129
|
+
});
|
|
32130
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
32131
|
+
priorTurns = recent;
|
|
32132
|
+
} else {
|
|
32133
|
+
memoryReadFailureReason = result.reason;
|
|
32134
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
32135
|
+
}
|
|
32136
|
+
}
|
|
32137
|
+
if (this.memory) {
|
|
32138
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
32139
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
32140
|
+
});
|
|
32141
|
+
}
|
|
30868
32142
|
const start = Date.now();
|
|
30869
32143
|
let conciergeBody;
|
|
30870
32144
|
let servedBy = "disabled";
|
|
@@ -30881,7 +32155,7 @@ var OperatorChatService = class {
|
|
|
30881
32155
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
30882
32156
|
outcome = "substrate_disabled";
|
|
30883
32157
|
} else {
|
|
30884
|
-
const context = await this.assembleConciergeContext();
|
|
32158
|
+
const context = await this.assembleConciergeContext(priorTurns);
|
|
30885
32159
|
const response = await this.substrateSelector.invokeSummarize(
|
|
30886
32160
|
"concierge",
|
|
30887
32161
|
{
|
|
@@ -30920,6 +32194,15 @@ var OperatorChatService = class {
|
|
|
30920
32194
|
CONCIERGE_THREAD_KEY,
|
|
30921
32195
|
responseMessage
|
|
30922
32196
|
);
|
|
32197
|
+
let assistantTurnId;
|
|
32198
|
+
if (this.memory) {
|
|
32199
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
32200
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
32201
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
32202
|
+
}
|
|
32203
|
+
if (this.memory && activeThreadIdForRound) {
|
|
32204
|
+
this.lastInteractionAt = nowMs;
|
|
32205
|
+
}
|
|
30923
32206
|
const payload = {
|
|
30924
32207
|
version: "1.2",
|
|
30925
32208
|
event_id: makeEventId("conc"),
|
|
@@ -30931,7 +32214,12 @@ var OperatorChatService = class {
|
|
|
30931
32214
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
30932
32215
|
substrate: servedBy,
|
|
30933
32216
|
latency_ms: latencyMs,
|
|
30934
|
-
outcome
|
|
32217
|
+
outcome,
|
|
32218
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
32219
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
32220
|
+
...this.memory ? {
|
|
32221
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
32222
|
+
} : {}
|
|
30935
32223
|
};
|
|
30936
32224
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
30937
32225
|
return {
|
|
@@ -30941,6 +32229,25 @@ var OperatorChatService = class {
|
|
|
30941
32229
|
outcome
|
|
30942
32230
|
};
|
|
30943
32231
|
}
|
|
32232
|
+
/**
|
|
32233
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
32234
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
32235
|
+
* with `result: "failure"` since the concierge fell back to
|
|
32236
|
+
* single-turn mode for this round-trip.
|
|
32237
|
+
*/
|
|
32238
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
32239
|
+
const payload = {
|
|
32240
|
+
version: "1.2",
|
|
32241
|
+
event_id: makeEventId("conc-memfail"),
|
|
32242
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32243
|
+
identity_id: this.identityId,
|
|
32244
|
+
kind: "operator_concierge_memory_read_failed",
|
|
32245
|
+
surface: "concierge",
|
|
32246
|
+
thread_id: threadId,
|
|
32247
|
+
failure_reason: reason
|
|
32248
|
+
};
|
|
32249
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
32250
|
+
}
|
|
30944
32251
|
/**
|
|
30945
32252
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
30946
32253
|
* an empty array when no thread exists yet.
|
|
@@ -30952,6 +32259,105 @@ var OperatorChatService = class {
|
|
|
30952
32259
|
);
|
|
30953
32260
|
return thread ? thread.messages : [];
|
|
30954
32261
|
}
|
|
32262
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
32263
|
+
/**
|
|
32264
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
32265
|
+
* 503 cleanly when called against an unwired service.
|
|
32266
|
+
*/
|
|
32267
|
+
hasConciergeMemory() {
|
|
32268
|
+
return this.memory !== void 0;
|
|
32269
|
+
}
|
|
32270
|
+
/**
|
|
32271
|
+
* List concierge memory threads, newest-first. Emits the
|
|
32272
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
32273
|
+
*/
|
|
32274
|
+
async listConciergeMemoryThreads(opts) {
|
|
32275
|
+
if (!this.memory) {
|
|
32276
|
+
throw new Error("concierge memory store not configured");
|
|
32277
|
+
}
|
|
32278
|
+
const summaries = await this.memory.listThreads(opts);
|
|
32279
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
32280
|
+
const payload = {
|
|
32281
|
+
version: "1.2",
|
|
32282
|
+
event_id: makeEventId("conc-hist"),
|
|
32283
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32284
|
+
identity_id: this.identityId,
|
|
32285
|
+
kind: "operator_concierge_history_read",
|
|
32286
|
+
surface: "concierge",
|
|
32287
|
+
thread_id: "*",
|
|
32288
|
+
turn_count: totalTurns
|
|
32289
|
+
};
|
|
32290
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
32291
|
+
return summaries;
|
|
32292
|
+
}
|
|
32293
|
+
/**
|
|
32294
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
32295
|
+
* `operator_concierge_history_read` audit event with the named
|
|
32296
|
+
* thread_id and the count of turns surfaced.
|
|
32297
|
+
*/
|
|
32298
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
32299
|
+
if (!this.memory) {
|
|
32300
|
+
throw new Error("concierge memory store not configured");
|
|
32301
|
+
}
|
|
32302
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
32303
|
+
const payload = {
|
|
32304
|
+
version: "1.2",
|
|
32305
|
+
event_id: makeEventId("conc-hist"),
|
|
32306
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32307
|
+
identity_id: this.identityId,
|
|
32308
|
+
kind: "operator_concierge_history_read",
|
|
32309
|
+
surface: "concierge",
|
|
32310
|
+
thread_id: threadId,
|
|
32311
|
+
turn_count: turns.length
|
|
32312
|
+
};
|
|
32313
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
32314
|
+
return turns;
|
|
32315
|
+
}
|
|
32316
|
+
/**
|
|
32317
|
+
* Delete a concierge memory thread. Emits
|
|
32318
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
32319
|
+
* removed; absent threads return false without an audit event.
|
|
32320
|
+
*/
|
|
32321
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
32322
|
+
if (!this.memory) {
|
|
32323
|
+
throw new Error("concierge memory store not configured");
|
|
32324
|
+
}
|
|
32325
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
32326
|
+
if (turnsBefore.length === 0) {
|
|
32327
|
+
return await this.memory.deleteThread(threadId);
|
|
32328
|
+
}
|
|
32329
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
32330
|
+
if (!removed) return false;
|
|
32331
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
32332
|
+
this.activeMemoryThreadId = void 0;
|
|
32333
|
+
}
|
|
32334
|
+
const payload = {
|
|
32335
|
+
version: "1.2",
|
|
32336
|
+
event_id: makeEventId("conc-del"),
|
|
32337
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32338
|
+
identity_id: this.identityId,
|
|
32339
|
+
kind: "operator_concierge_thread_deleted",
|
|
32340
|
+
surface: "concierge",
|
|
32341
|
+
thread_id: threadId,
|
|
32342
|
+
turn_count: turnsBefore.length
|
|
32343
|
+
};
|
|
32344
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
32345
|
+
return true;
|
|
32346
|
+
}
|
|
32347
|
+
/**
|
|
32348
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
32349
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
32350
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
32351
|
+
*/
|
|
32352
|
+
resetConciergeMemoryThread() {
|
|
32353
|
+
this.activeMemoryThreadId = void 0;
|
|
32354
|
+
}
|
|
32355
|
+
ensureActiveMemoryThread() {
|
|
32356
|
+
if (!this.activeMemoryThreadId) {
|
|
32357
|
+
this.activeMemoryThreadId = crypto.randomUUID();
|
|
32358
|
+
}
|
|
32359
|
+
return this.activeMemoryThreadId;
|
|
32360
|
+
}
|
|
30955
32361
|
/**
|
|
30956
32362
|
* Stitch fortress state into a single context blob the substrate
|
|
30957
32363
|
* folds into its summarization prompt.
|
|
@@ -30964,6 +32370,11 @@ var OperatorChatService = class {
|
|
|
30964
32370
|
* ## Sanctuary reference
|
|
30965
32371
|
* <static domain reference block>
|
|
30966
32372
|
*
|
|
32373
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
32374
|
+
* OPERATOR: ...
|
|
32375
|
+
* CONCIERGE: ...
|
|
32376
|
+
* ---
|
|
32377
|
+
*
|
|
30967
32378
|
* ## Recent activity
|
|
30968
32379
|
* <recentActivity output>
|
|
30969
32380
|
*
|
|
@@ -30973,37 +32384,69 @@ var OperatorChatService = class {
|
|
|
30973
32384
|
* ## Open inbox
|
|
30974
32385
|
* <openInbox output>
|
|
30975
32386
|
* ```
|
|
32387
|
+
*
|
|
32388
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
32389
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
32390
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
32391
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
32392
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
32393
|
+
* serialization is the canonical path for v1.3.
|
|
30976
32394
|
*/
|
|
30977
|
-
async assembleConciergeContext() {
|
|
32395
|
+
async assembleConciergeContext(priorTurns = []) {
|
|
30978
32396
|
const ref = `## Sanctuary reference
|
|
30979
32397
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
32398
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
30980
32399
|
if (!this.contextProviders) {
|
|
30981
|
-
return
|
|
30982
|
-
|
|
30983
|
-
|
|
30984
|
-
(no providers wired)
|
|
30985
|
-
|
|
30986
|
-
##
|
|
30987
|
-
(
|
|
30988
|
-
|
|
30989
|
-
## Open inbox
|
|
30990
|
-
(no providers wired)`;
|
|
32400
|
+
return [
|
|
32401
|
+
ref,
|
|
32402
|
+
...priorSection ? [priorSection] : [],
|
|
32403
|
+
"## Recent activity\n(no providers wired)",
|
|
32404
|
+
"## Wrapped agents\n(no providers wired)",
|
|
32405
|
+
"## Open inbox\n(no providers wired)"
|
|
32406
|
+
].join("\n\n");
|
|
30991
32407
|
}
|
|
30992
32408
|
const [activity, agents, inbox] = await Promise.all([
|
|
30993
32409
|
this.contextProviders.recentActivity(),
|
|
30994
32410
|
this.contextProviders.agentInventory(),
|
|
30995
32411
|
this.contextProviders.openInbox()
|
|
30996
32412
|
]);
|
|
30997
|
-
return
|
|
30998
|
-
|
|
30999
|
-
|
|
31000
|
-
|
|
31001
|
-
|
|
31002
|
-
|
|
31003
|
-
${agents}
|
|
31004
|
-
|
|
31005
|
-
|
|
31006
|
-
|
|
32413
|
+
return [
|
|
32414
|
+
ref,
|
|
32415
|
+
...priorSection ? [priorSection] : [],
|
|
32416
|
+
`## Recent activity
|
|
32417
|
+
${activity}`,
|
|
32418
|
+
`## Wrapped agents
|
|
32419
|
+
${agents}`,
|
|
32420
|
+
`## Open inbox
|
|
32421
|
+
${inbox}`
|
|
32422
|
+
].join("\n\n");
|
|
32423
|
+
}
|
|
32424
|
+
/**
|
|
32425
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
32426
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
32427
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
32428
|
+
* the input is empty or when the budget excludes every turn.
|
|
32429
|
+
*/
|
|
32430
|
+
formatPriorTurnsSection(turns) {
|
|
32431
|
+
if (turns.length === 0) return "";
|
|
32432
|
+
const HEADER = "## Prior conversation";
|
|
32433
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
32434
|
+
const headerTokens = approxTokenLen(`${HEADER}
|
|
32435
|
+
`);
|
|
32436
|
+
const sepTokens = approxTokenLen("\n");
|
|
32437
|
+
let runningTokens = headerTokens;
|
|
32438
|
+
let runningLines = [];
|
|
32439
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
32440
|
+
const line = lines[i];
|
|
32441
|
+
const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
32442
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
32443
|
+
runningTokens += tokens;
|
|
32444
|
+
runningLines.push(line);
|
|
32445
|
+
}
|
|
32446
|
+
if (runningLines.length === 0) return "";
|
|
32447
|
+
runningLines = runningLines.reverse();
|
|
32448
|
+
return `${HEADER}
|
|
32449
|
+
${runningLines.join("\n")}`;
|
|
31007
32450
|
}
|
|
31008
32451
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
31009
32452
|
emit(operation, payload, result) {
|
|
@@ -31019,6 +32462,10 @@ ${inbox}`;
|
|
|
31019
32462
|
function makeEventId(prefix) {
|
|
31020
32463
|
return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
31021
32464
|
}
|
|
32465
|
+
function formatPriorTurnLine(turn) {
|
|
32466
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
32467
|
+
return `${label}: ${turn.content}`;
|
|
32468
|
+
}
|
|
31022
32469
|
function hashOf(input) {
|
|
31023
32470
|
return hashToString(sha256.sha256(stringToBytes(input)));
|
|
31024
32471
|
}
|
|
@@ -31115,6 +32562,297 @@ var OperatorChatStore = class {
|
|
|
31115
32562
|
}
|
|
31116
32563
|
};
|
|
31117
32564
|
|
|
32565
|
+
// src/chat/concierge-memory-store.ts
|
|
32566
|
+
init_encryption();
|
|
32567
|
+
init_encoding();
|
|
32568
|
+
var CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
32569
|
+
var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
32570
|
+
var HKDF_INFO2 = "concierge-memory-store-v1";
|
|
32571
|
+
var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
32572
|
+
var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
32573
|
+
var ConciergeMemoryStore = class {
|
|
32574
|
+
storage;
|
|
32575
|
+
encryptionKey;
|
|
32576
|
+
fortressId;
|
|
32577
|
+
retentionDays;
|
|
32578
|
+
locks;
|
|
32579
|
+
constructor(opts) {
|
|
32580
|
+
this.storage = opts.storage;
|
|
32581
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
32582
|
+
this.fortressId = opts.fortressId;
|
|
32583
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
32584
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
32585
|
+
}
|
|
32586
|
+
/**
|
|
32587
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
32588
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
32589
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
32590
|
+
* monotonicity even under concurrent callers.
|
|
32591
|
+
*/
|
|
32592
|
+
async appendTurn(threadId, role, content) {
|
|
32593
|
+
return this.withLock(threadId, async () => {
|
|
32594
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
32595
|
+
const now = /* @__PURE__ */ new Date();
|
|
32596
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
32597
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
32598
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
32599
|
+
const turn = {
|
|
32600
|
+
thread_id: threadId,
|
|
32601
|
+
fortress_id: this.fortressId,
|
|
32602
|
+
turn_id: nextTurnId,
|
|
32603
|
+
role,
|
|
32604
|
+
content,
|
|
32605
|
+
created_at: now.toISOString(),
|
|
32606
|
+
retention_until: retentionUntil.toISOString()
|
|
32607
|
+
};
|
|
32608
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
32609
|
+
version: 1,
|
|
32610
|
+
thread_id: threadId,
|
|
32611
|
+
fortress_id: this.fortressId,
|
|
32612
|
+
created_at: now.toISOString(),
|
|
32613
|
+
turns: [turn]
|
|
32614
|
+
};
|
|
32615
|
+
await this.saveBundle(next);
|
|
32616
|
+
return turn;
|
|
32617
|
+
});
|
|
32618
|
+
}
|
|
32619
|
+
/**
|
|
32620
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
32621
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
32622
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
32623
|
+
*/
|
|
32624
|
+
async readThread(threadId, opts) {
|
|
32625
|
+
const bundle = await this.loadBundle(threadId);
|
|
32626
|
+
if (!bundle) return [];
|
|
32627
|
+
let turns = bundle.turns;
|
|
32628
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
32629
|
+
const cutoff = opts.sinceTurnId;
|
|
32630
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
32631
|
+
}
|
|
32632
|
+
if (opts?.limit !== void 0) {
|
|
32633
|
+
turns = turns.slice(0, opts.limit);
|
|
32634
|
+
}
|
|
32635
|
+
return turns;
|
|
32636
|
+
}
|
|
32637
|
+
/**
|
|
32638
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
32639
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
32640
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
32641
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
32642
|
+
* with a concrete cause.
|
|
32643
|
+
*
|
|
32644
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
32645
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
32646
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
32647
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
32648
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
32649
|
+
* - Storage IO error → `io_failed`.
|
|
32650
|
+
*/
|
|
32651
|
+
async readThreadStrict(threadId, opts) {
|
|
32652
|
+
const key = bundleKey(threadId);
|
|
32653
|
+
let raw;
|
|
32654
|
+
try {
|
|
32655
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32656
|
+
} catch {
|
|
32657
|
+
return { ok: false, reason: "io_failed" };
|
|
32658
|
+
}
|
|
32659
|
+
if (!raw) return { ok: true, turns: [] };
|
|
32660
|
+
if (raw.length > MAX_BUNDLE_BYTES2) {
|
|
32661
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
32662
|
+
}
|
|
32663
|
+
let envelope;
|
|
32664
|
+
try {
|
|
32665
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
32666
|
+
} catch {
|
|
32667
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
32668
|
+
}
|
|
32669
|
+
let plaintext;
|
|
32670
|
+
try {
|
|
32671
|
+
const aad = stringToBytes(threadId);
|
|
32672
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
32673
|
+
} catch {
|
|
32674
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
32675
|
+
}
|
|
32676
|
+
let parsed;
|
|
32677
|
+
try {
|
|
32678
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
32679
|
+
} catch {
|
|
32680
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
32681
|
+
}
|
|
32682
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
32683
|
+
if (parsed.thread_id !== threadId) {
|
|
32684
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
32685
|
+
}
|
|
32686
|
+
let turns = parsed.turns;
|
|
32687
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
32688
|
+
const cutoff = opts.sinceTurnId;
|
|
32689
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
32690
|
+
}
|
|
32691
|
+
if (opts?.limit !== void 0) {
|
|
32692
|
+
turns = turns.slice(0, opts.limit);
|
|
32693
|
+
}
|
|
32694
|
+
return { ok: true, turns };
|
|
32695
|
+
}
|
|
32696
|
+
/**
|
|
32697
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
32698
|
+
* Sorted newest-first by last_turn_at.
|
|
32699
|
+
*/
|
|
32700
|
+
async listThreads(opts) {
|
|
32701
|
+
const entries = await this.storage.list(
|
|
32702
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32703
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32704
|
+
);
|
|
32705
|
+
const summaries = [];
|
|
32706
|
+
for (const meta of entries) {
|
|
32707
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32708
|
+
if (threadId === null) continue;
|
|
32709
|
+
const bundle = await this.loadBundle(threadId);
|
|
32710
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
32711
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
32712
|
+
summaries.push({
|
|
32713
|
+
thread_id: bundle.thread_id,
|
|
32714
|
+
created_at: bundle.created_at,
|
|
32715
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
32716
|
+
turn_count: bundle.turns.length
|
|
32717
|
+
});
|
|
32718
|
+
}
|
|
32719
|
+
summaries.sort(
|
|
32720
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
32721
|
+
);
|
|
32722
|
+
if (opts?.limit !== void 0) {
|
|
32723
|
+
return summaries.slice(0, opts.limit);
|
|
32724
|
+
}
|
|
32725
|
+
return summaries;
|
|
32726
|
+
}
|
|
32727
|
+
/**
|
|
32728
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
32729
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
32730
|
+
* caller's responsibility.
|
|
32731
|
+
*/
|
|
32732
|
+
async deleteThread(threadId) {
|
|
32733
|
+
const key = bundleKey(threadId);
|
|
32734
|
+
return this.withLock(threadId, async () => {
|
|
32735
|
+
const existed = await this.storage.exists(
|
|
32736
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32737
|
+
key
|
|
32738
|
+
);
|
|
32739
|
+
if (!existed) return false;
|
|
32740
|
+
try {
|
|
32741
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32742
|
+
} catch {
|
|
32743
|
+
return false;
|
|
32744
|
+
}
|
|
32745
|
+
return true;
|
|
32746
|
+
});
|
|
32747
|
+
}
|
|
32748
|
+
/**
|
|
32749
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
32750
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
32751
|
+
*/
|
|
32752
|
+
async pruneExpired(now) {
|
|
32753
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
32754
|
+
const entries = await this.storage.list(
|
|
32755
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32756
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32757
|
+
);
|
|
32758
|
+
let pruned = 0;
|
|
32759
|
+
for (const meta of entries) {
|
|
32760
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32761
|
+
if (threadId === null) continue;
|
|
32762
|
+
pruned += await this.withLock(threadId, async () => {
|
|
32763
|
+
const bundle = await this.loadBundle(threadId);
|
|
32764
|
+
if (!bundle) return 0;
|
|
32765
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
32766
|
+
const dropped = bundle.turns.length - kept.length;
|
|
32767
|
+
if (dropped === 0) return 0;
|
|
32768
|
+
if (kept.length === 0) {
|
|
32769
|
+
await this.storage.delete(
|
|
32770
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32771
|
+
bundleKey(threadId)
|
|
32772
|
+
);
|
|
32773
|
+
} else {
|
|
32774
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
32775
|
+
}
|
|
32776
|
+
return dropped;
|
|
32777
|
+
});
|
|
32778
|
+
}
|
|
32779
|
+
return { pruned };
|
|
32780
|
+
}
|
|
32781
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
32782
|
+
async loadBundle(threadId) {
|
|
32783
|
+
const key = bundleKey(threadId);
|
|
32784
|
+
let raw;
|
|
32785
|
+
try {
|
|
32786
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32787
|
+
} catch {
|
|
32788
|
+
return null;
|
|
32789
|
+
}
|
|
32790
|
+
if (!raw) return null;
|
|
32791
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
32792
|
+
try {
|
|
32793
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
32794
|
+
const aad = stringToBytes(threadId);
|
|
32795
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
32796
|
+
const parsed = JSON.parse(
|
|
32797
|
+
bytesToString(plaintext)
|
|
32798
|
+
);
|
|
32799
|
+
if (parsed.version !== 1) return null;
|
|
32800
|
+
if (parsed.thread_id !== threadId) return null;
|
|
32801
|
+
return parsed;
|
|
32802
|
+
} catch {
|
|
32803
|
+
return null;
|
|
32804
|
+
}
|
|
32805
|
+
}
|
|
32806
|
+
async saveBundle(bundle) {
|
|
32807
|
+
const key = bundleKey(bundle.thread_id);
|
|
32808
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
32809
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
32810
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
32811
|
+
await this.storage.write(
|
|
32812
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32813
|
+
key,
|
|
32814
|
+
stringToBytes(JSON.stringify(envelope))
|
|
32815
|
+
);
|
|
32816
|
+
}
|
|
32817
|
+
/**
|
|
32818
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
32819
|
+
* once the task settles (success or failure). Generic helper so
|
|
32820
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
32821
|
+
*/
|
|
32822
|
+
async withLock(threadId, task) {
|
|
32823
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
32824
|
+
let release;
|
|
32825
|
+
const next = new Promise((resolve6) => {
|
|
32826
|
+
release = resolve6;
|
|
32827
|
+
});
|
|
32828
|
+
const chained = previous.then(() => next);
|
|
32829
|
+
this.locks.set(threadId, chained);
|
|
32830
|
+
try {
|
|
32831
|
+
await previous;
|
|
32832
|
+
return await task();
|
|
32833
|
+
} finally {
|
|
32834
|
+
release();
|
|
32835
|
+
if (this.locks.get(threadId) === chained) {
|
|
32836
|
+
this.locks.delete(threadId);
|
|
32837
|
+
}
|
|
32838
|
+
}
|
|
32839
|
+
}
|
|
32840
|
+
};
|
|
32841
|
+
function bundleKey(threadId) {
|
|
32842
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
32843
|
+
}
|
|
32844
|
+
function stripKeyPrefix(key) {
|
|
32845
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
32846
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
32847
|
+
}
|
|
32848
|
+
function lastTurnId(bundle) {
|
|
32849
|
+
let max = 0;
|
|
32850
|
+
for (const t of bundle.turns) {
|
|
32851
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
32852
|
+
}
|
|
32853
|
+
return max;
|
|
32854
|
+
}
|
|
32855
|
+
|
|
31118
32856
|
// src/dashboard/v1_1/wiring.ts
|
|
31119
32857
|
var CapabilityErrorAgentController = class {
|
|
31120
32858
|
fail(action) {
|
|
@@ -31153,6 +32891,14 @@ function buildV11Bindings(inputs) {
|
|
|
31153
32891
|
let operatorChatService;
|
|
31154
32892
|
if (inputs.storage && inputs.masterKey) {
|
|
31155
32893
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
32894
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
32895
|
+
storage: inputs.storage,
|
|
32896
|
+
masterKey: inputs.masterKey,
|
|
32897
|
+
fortressId: inputs.fortressId,
|
|
32898
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
32899
|
+
});
|
|
32900
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
32901
|
+
});
|
|
31156
32902
|
operatorChatService = new OperatorChatService({
|
|
31157
32903
|
store: chatStore,
|
|
31158
32904
|
auditLog: inputs.auditLog,
|
|
@@ -31163,7 +32909,8 @@ function buildV11Bindings(inputs) {
|
|
|
31163
32909
|
identityId: inputs.identityId,
|
|
31164
32910
|
registry
|
|
31165
32911
|
}),
|
|
31166
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
32912
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
32913
|
+
conciergeMemory
|
|
31167
32914
|
});
|
|
31168
32915
|
}
|
|
31169
32916
|
const hubService = new HubService({
|
|
@@ -31355,13 +33102,13 @@ init_encryption();
|
|
|
31355
33102
|
init_encoding();
|
|
31356
33103
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
31357
33104
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
31358
|
-
var
|
|
33105
|
+
var HKDF_INFO3 = "intelligence-substrate-config";
|
|
31359
33106
|
var IntelligenceConfigStore = class {
|
|
31360
33107
|
storage;
|
|
31361
33108
|
encryptionKey;
|
|
31362
33109
|
constructor(storage, masterKey) {
|
|
31363
33110
|
this.storage = storage;
|
|
31364
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33111
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
31365
33112
|
}
|
|
31366
33113
|
/**
|
|
31367
33114
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -33500,7 +35247,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33500
35247
|
);
|
|
33501
35248
|
}
|
|
33502
35249
|
}
|
|
33503
|
-
const
|
|
35250
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
35251
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
35252
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
33504
35253
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
33505
35254
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
33506
35255
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -33509,6 +35258,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33509
35258
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
33510
35259
|
);
|
|
33511
35260
|
}
|
|
35261
|
+
let detailedFailureClass;
|
|
35262
|
+
if (identityFailed) {
|
|
35263
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
35264
|
+
} else if (reputationBundleFailed) {
|
|
35265
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
35266
|
+
} else if (reputationAttestationFailed) {
|
|
35267
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
35268
|
+
} else if (unverifiableFailed) {
|
|
35269
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
35270
|
+
}
|
|
33512
35271
|
return {
|
|
33513
35272
|
version: "1.1",
|
|
33514
35273
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -33528,7 +35287,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33528
35287
|
identity,
|
|
33529
35288
|
audit,
|
|
33530
35289
|
reputation,
|
|
33531
|
-
failure_class:
|
|
35290
|
+
failure_class: detailedFailureClass
|
|
33532
35291
|
};
|
|
33533
35292
|
}
|
|
33534
35293
|
|
|
@@ -33918,7 +35677,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
33918
35677
|
}
|
|
33919
35678
|
return null;
|
|
33920
35679
|
}
|
|
33921
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
35680
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
33922
35681
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
33923
35682
|
if (!destinationSigner) {
|
|
33924
35683
|
return {
|
|
@@ -33980,8 +35739,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
33980
35739
|
}
|
|
33981
35740
|
}
|
|
33982
35741
|
}
|
|
35742
|
+
let plaintext;
|
|
33983
35743
|
try {
|
|
33984
|
-
|
|
35744
|
+
plaintext = decrypt(
|
|
33985
35745
|
item.entry.payload,
|
|
33986
35746
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
33987
35747
|
);
|
|
@@ -33990,28 +35750,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
33990
35750
|
skipped++;
|
|
33991
35751
|
continue;
|
|
33992
35752
|
}
|
|
33993
|
-
await stateStore.write(
|
|
33994
|
-
item.namespace,
|
|
33995
|
-
item.key,
|
|
33996
|
-
bytesToString(plaintext),
|
|
33997
|
-
destinationSigner.identity_id,
|
|
33998
|
-
destinationSigner.encrypted_private_key,
|
|
33999
|
-
identityEncryptionKey,
|
|
34000
|
-
{
|
|
34001
|
-
content_type: item.entry.metadata.content_type,
|
|
34002
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
34003
|
-
tags: [
|
|
34004
|
-
...item.entry.metadata.tags ?? [],
|
|
34005
|
-
"exit-import",
|
|
34006
|
-
`source:${item.entry.kid}`
|
|
34007
|
-
]
|
|
34008
|
-
}
|
|
34009
|
-
);
|
|
34010
|
-
imported++;
|
|
34011
35753
|
} catch {
|
|
34012
35754
|
skippedInvalidSig++;
|
|
34013
35755
|
skipped++;
|
|
35756
|
+
continue;
|
|
34014
35757
|
}
|
|
35758
|
+
await stateStore.write(
|
|
35759
|
+
item.namespace,
|
|
35760
|
+
item.key,
|
|
35761
|
+
bytesToString(plaintext),
|
|
35762
|
+
destinationSigner.identity_id,
|
|
35763
|
+
destinationSigner.encrypted_private_key,
|
|
35764
|
+
identityEncryptionKey,
|
|
35765
|
+
{
|
|
35766
|
+
content_type: item.entry.metadata.content_type,
|
|
35767
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
35768
|
+
tags: [
|
|
35769
|
+
...item.entry.metadata.tags ?? [],
|
|
35770
|
+
"exit-import",
|
|
35771
|
+
`source:${item.entry.kid}`
|
|
35772
|
+
]
|
|
35773
|
+
}
|
|
35774
|
+
);
|
|
35775
|
+
imported++;
|
|
35776
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
34015
35777
|
}
|
|
34016
35778
|
return {
|
|
34017
35779
|
status: "rekeyed",
|
|
@@ -34022,6 +35784,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
34022
35784
|
conflicts
|
|
34023
35785
|
};
|
|
34024
35786
|
}
|
|
35787
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
35788
|
+
let removed = 0;
|
|
35789
|
+
const failed = [];
|
|
35790
|
+
for (const loc of staged) {
|
|
35791
|
+
try {
|
|
35792
|
+
const ok = await storage.delete(loc.namespace, loc.key);
|
|
35793
|
+
if (ok) {
|
|
35794
|
+
removed++;
|
|
35795
|
+
} else {
|
|
35796
|
+
failed.push(loc);
|
|
35797
|
+
}
|
|
35798
|
+
} catch {
|
|
35799
|
+
failed.push(loc);
|
|
35800
|
+
}
|
|
35801
|
+
}
|
|
35802
|
+
return { removed, failed };
|
|
35803
|
+
}
|
|
34025
35804
|
async function stageArtifact(storage, namespace, key, value) {
|
|
34026
35805
|
await storage.write(namespace, key, jsonBytes(value));
|
|
34027
35806
|
}
|
|
@@ -34146,6 +35925,8 @@ async function importExitBundle(opts) {
|
|
|
34146
35925
|
}
|
|
34147
35926
|
const importId = importIdForManifest(manifest);
|
|
34148
35927
|
const stagedArtifacts = [];
|
|
35928
|
+
const stagedLocations = [];
|
|
35929
|
+
const importedRekeyEntries = [];
|
|
34149
35930
|
if (identityArtifact) {
|
|
34150
35931
|
await stageArtifact(
|
|
34151
35932
|
opts.storage,
|
|
@@ -34154,10 +35935,15 @@ async function importExitBundle(opts) {
|
|
|
34154
35935
|
identityArtifact.json
|
|
34155
35936
|
);
|
|
34156
35937
|
stagedArtifacts.push("public_identity");
|
|
35938
|
+
stagedLocations.push({
|
|
35939
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
35940
|
+
key: identityArtifact.json.bundle.identity_id
|
|
35941
|
+
});
|
|
34157
35942
|
}
|
|
34158
35943
|
if (policySet) {
|
|
34159
35944
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
34160
35945
|
stagedArtifacts.push("policy_set");
|
|
35946
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
34161
35947
|
}
|
|
34162
35948
|
if (auditReceipts) {
|
|
34163
35949
|
await stageArtifact(
|
|
@@ -34167,10 +35953,12 @@ async function importExitBundle(opts) {
|
|
|
34167
35953
|
auditReceipts.json
|
|
34168
35954
|
);
|
|
34169
35955
|
stagedArtifacts.push("audit_receipts");
|
|
35956
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
34170
35957
|
}
|
|
34171
35958
|
if (commitments) {
|
|
34172
35959
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
34173
35960
|
stagedArtifacts.push("commitments");
|
|
35961
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
34174
35962
|
}
|
|
34175
35963
|
if (placeholderMetadata) {
|
|
34176
35964
|
await stageArtifact(
|
|
@@ -34180,12 +35968,17 @@ async function importExitBundle(opts) {
|
|
|
34180
35968
|
placeholderMetadata.json
|
|
34181
35969
|
);
|
|
34182
35970
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
35971
|
+
stagedLocations.push({
|
|
35972
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
35973
|
+
key: importId
|
|
35974
|
+
});
|
|
34183
35975
|
}
|
|
34184
35976
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
34185
35977
|
manifest: manifest.body,
|
|
34186
35978
|
verified_at: verification.verified_at,
|
|
34187
35979
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
34188
35980
|
});
|
|
35981
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
34189
35982
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
34190
35983
|
let reputationResult = {
|
|
34191
35984
|
imported_attestations: 0,
|
|
@@ -34210,26 +36003,57 @@ async function importExitBundle(opts) {
|
|
|
34210
36003
|
encryptedState?.json ?? null,
|
|
34211
36004
|
opts
|
|
34212
36005
|
);
|
|
34213
|
-
|
|
34214
|
-
|
|
34215
|
-
|
|
34216
|
-
|
|
34217
|
-
|
|
34218
|
-
|
|
34219
|
-
|
|
34220
|
-
|
|
34221
|
-
|
|
34222
|
-
|
|
34223
|
-
|
|
34224
|
-
|
|
34225
|
-
|
|
34226
|
-
|
|
34227
|
-
|
|
34228
|
-
|
|
34229
|
-
|
|
34230
|
-
|
|
34231
|
-
|
|
34232
|
-
|
|
36006
|
+
let stateResult;
|
|
36007
|
+
try {
|
|
36008
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
36009
|
+
encryptedState.json,
|
|
36010
|
+
opts,
|
|
36011
|
+
sourceMasterKey,
|
|
36012
|
+
publicKeys.byIdentityId,
|
|
36013
|
+
importedRekeyEntries
|
|
36014
|
+
) : {
|
|
36015
|
+
status: "staged_requires_source_key",
|
|
36016
|
+
imported_keys: 0,
|
|
36017
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
36018
|
+
skipped_invalid_sig: 0,
|
|
36019
|
+
skipped_unknown_kid: 0,
|
|
36020
|
+
conflicts: conflicts.state_conflicts.length
|
|
36021
|
+
} : {
|
|
36022
|
+
status: "not_requested",
|
|
36023
|
+
imported_keys: 0,
|
|
36024
|
+
skipped_keys: 0,
|
|
36025
|
+
skipped_invalid_sig: 0,
|
|
36026
|
+
skipped_unknown_kid: 0,
|
|
36027
|
+
conflicts: 0
|
|
36028
|
+
};
|
|
36029
|
+
} catch (err) {
|
|
36030
|
+
const toCleanup = [
|
|
36031
|
+
...importedRekeyEntries,
|
|
36032
|
+
...stagedLocations
|
|
36033
|
+
];
|
|
36034
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
36035
|
+
opts.auditLog.append(
|
|
36036
|
+
"l1",
|
|
36037
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
36038
|
+
manifest.body.identity_binding.identity_id,
|
|
36039
|
+
{
|
|
36040
|
+
import_id: importId,
|
|
36041
|
+
manifest_version: manifest.body.manifest_version,
|
|
36042
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
36043
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
36044
|
+
removed_total: cleanup.removed,
|
|
36045
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
36046
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
36047
|
+
},
|
|
36048
|
+
"failure"
|
|
36049
|
+
);
|
|
36050
|
+
await opts.auditLog.flush();
|
|
36051
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
36052
|
+
throw new ExitBundleImportError(
|
|
36053
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
36054
|
+
`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).`
|
|
36055
|
+
);
|
|
36056
|
+
}
|
|
34233
36057
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
34234
36058
|
import_id: importId,
|
|
34235
36059
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -34455,7 +36279,19 @@ async function runExitCommand(args) {
|
|
|
34455
36279
|
}
|
|
34456
36280
|
const config = await loadConfig();
|
|
34457
36281
|
const ctx = await openExitContext(argv, env);
|
|
34458
|
-
|
|
36282
|
+
let policy;
|
|
36283
|
+
try {
|
|
36284
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
36285
|
+
} catch (policyErr) {
|
|
36286
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
36287
|
+
write(err, `
|
|
36288
|
+
Sanctuary cannot proceed.
|
|
36289
|
+
${policyErr.message}
|
|
36290
|
+
`);
|
|
36291
|
+
return 1;
|
|
36292
|
+
}
|
|
36293
|
+
throw policyErr;
|
|
36294
|
+
}
|
|
34459
36295
|
const result = await exportExitBundle({
|
|
34460
36296
|
bundleDir: outDir,
|
|
34461
36297
|
storage: ctx.storage,
|
|
@@ -35107,7 +36943,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35107
36943
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
35108
36944
|
await profileStore.load();
|
|
35109
36945
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
35110
|
-
|
|
36946
|
+
let policy;
|
|
36947
|
+
try {
|
|
36948
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
36949
|
+
} catch (err) {
|
|
36950
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
36951
|
+
console.error(`
|
|
36952
|
+
Sanctuary cannot start.
|
|
36953
|
+
${err.message}
|
|
36954
|
+
`);
|
|
36955
|
+
process.exit(1);
|
|
36956
|
+
}
|
|
36957
|
+
throw err;
|
|
36958
|
+
}
|
|
35111
36959
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
35112
36960
|
await baseline.load();
|
|
35113
36961
|
let approvalChannel;
|
|
@@ -35204,7 +37052,35 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35204
37052
|
timestamp: alert.timestamp
|
|
35205
37053
|
});
|
|
35206
37054
|
} : void 0;
|
|
35207
|
-
const
|
|
37055
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37056
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37057
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
37058
|
+
storage,
|
|
37059
|
+
masterKey,
|
|
37060
|
+
auditLog,
|
|
37061
|
+
identityId: aggregatorIdentityId,
|
|
37062
|
+
fortressId: fortressIdForAggregator
|
|
37063
|
+
});
|
|
37064
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
37065
|
+
underlying: approvalChannel,
|
|
37066
|
+
aggregator: approvalAggregator,
|
|
37067
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
37068
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
37069
|
+
});
|
|
37070
|
+
const gate = new ApprovalGate(
|
|
37071
|
+
policy,
|
|
37072
|
+
baseline,
|
|
37073
|
+
wrappedApprovalChannel,
|
|
37074
|
+
auditLog,
|
|
37075
|
+
injectionDetector,
|
|
37076
|
+
onInjectionAlert
|
|
37077
|
+
);
|
|
37078
|
+
gate.setApprovalEventCallback((event) => {
|
|
37079
|
+
void approvalAggregator.ingest(event);
|
|
37080
|
+
});
|
|
37081
|
+
if (dashboard) {
|
|
37082
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
37083
|
+
}
|
|
35208
37084
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
35209
37085
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
35210
37086
|
config,
|
|
@@ -35399,6 +37275,7 @@ exports.HERO_COPY = HERO_COPY;
|
|
|
35399
37275
|
exports.InMemoryModelProvenanceStore = InMemoryModelProvenanceStore;
|
|
35400
37276
|
exports.InjectionDetector = InjectionDetector;
|
|
35401
37277
|
exports.MODEL_PRESETS = MODEL_PRESETS;
|
|
37278
|
+
exports.MalformedPrincipalPolicyError = MalformedPrincipalPolicyError;
|
|
35402
37279
|
exports.MemoryStorage = MemoryStorage;
|
|
35403
37280
|
exports.PolicyStore = PolicyStore;
|
|
35404
37281
|
exports.ProxyRouter = ProxyRouter;
|