@sanctuary-framework/mcp-server 1.2.3 → 1.2.4
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 +1315 -43
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1315 -43
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1233 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +538 -7
- package/dist/index.d.ts +538 -7
- package/dist/index.js +1233 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4569,21 +4569,39 @@ approval_channel:
|
|
|
4569
4569
|
}
|
|
4570
4570
|
async function loadPrincipalPolicy(storagePath) {
|
|
4571
4571
|
const policyPath = join(storagePath, "principal-policy.yaml");
|
|
4572
|
+
let content;
|
|
4573
|
+
try {
|
|
4574
|
+
content = await readFile(policyPath, "utf-8");
|
|
4575
|
+
} catch (err) {
|
|
4576
|
+
const code = err?.code;
|
|
4577
|
+
if (code === "ENOENT") {
|
|
4578
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4579
|
+
try {
|
|
4580
|
+
await writeFile(policyPath, defaultYaml, "utf-8");
|
|
4581
|
+
await chmod(policyPath, 384);
|
|
4582
|
+
} catch (writeErr) {
|
|
4583
|
+
console.warn(
|
|
4584
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4585
|
+
);
|
|
4586
|
+
}
|
|
4587
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4588
|
+
}
|
|
4589
|
+
throw new MalformedPrincipalPolicyError(
|
|
4590
|
+
policyPath,
|
|
4591
|
+
`read failed: ${err.message}`
|
|
4592
|
+
);
|
|
4593
|
+
}
|
|
4572
4594
|
try {
|
|
4573
|
-
const content = await readFile(policyPath, "utf-8");
|
|
4574
4595
|
const policy = parsePolicy(content);
|
|
4575
4596
|
return Object.freeze(policy);
|
|
4576
|
-
} catch {
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
} catch {
|
|
4582
|
-
}
|
|
4583
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4597
|
+
} catch (parseErr) {
|
|
4598
|
+
throw new MalformedPrincipalPolicyError(
|
|
4599
|
+
policyPath,
|
|
4600
|
+
parseErr.message
|
|
4601
|
+
);
|
|
4584
4602
|
}
|
|
4585
4603
|
}
|
|
4586
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY;
|
|
4604
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4587
4605
|
var init_loader = __esm({
|
|
4588
4606
|
"src/principal-policy/loader.ts"() {
|
|
4589
4607
|
DEFAULT_TIER2 = {
|
|
@@ -4716,6 +4734,20 @@ var init_loader = __esm({
|
|
|
4716
4734
|
],
|
|
4717
4735
|
approval_channel: DEFAULT_CHANNEL
|
|
4718
4736
|
};
|
|
4737
|
+
MalformedPrincipalPolicyError = class extends Error {
|
|
4738
|
+
constructor(policyPath, reason) {
|
|
4739
|
+
super(
|
|
4740
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4741
|
+
Reason: ${reason}
|
|
4742
|
+
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.`
|
|
4743
|
+
);
|
|
4744
|
+
this.policyPath = policyPath;
|
|
4745
|
+
this.reason = reason;
|
|
4746
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4747
|
+
}
|
|
4748
|
+
policyPath;
|
|
4749
|
+
reason;
|
|
4750
|
+
};
|
|
4719
4751
|
}
|
|
4720
4752
|
});
|
|
4721
4753
|
|
|
@@ -4935,7 +4967,7 @@ function deepSortKeys(obj) {
|
|
|
4935
4967
|
return sorted;
|
|
4936
4968
|
}
|
|
4937
4969
|
function canonicalizeForSigning(body) {
|
|
4938
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4970
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4939
4971
|
}
|
|
4940
4972
|
var init_types = __esm({
|
|
4941
4973
|
"src/shr/types.ts"() {
|
|
@@ -12434,7 +12466,7 @@ var init_auth_middleware = __esm({
|
|
|
12434
12466
|
});
|
|
12435
12467
|
|
|
12436
12468
|
// src/hub/constants.ts
|
|
12437
|
-
var HUB_API_PREFIX, HUB_ROUTES, HUB_FORTRESS_AGENT_ID_SENTINEL, HUB_INBOX_ACTIONS, HUB_AGENT_CONTROL_ACTIONS, HUB_TIER_1_AGENT_CONTROL_ACTIONS, HUB_ACTIVITY_DEFAULT_LIMIT, HUB_ACTIVITY_MAX_LIMIT, HUB_INBOX_DEFAULT_LIMIT, HUB_INBOX_MAX_LIMIT, HUB_AGENTS_DEFAULT_LIMIT, HUB_AGENTS_MAX_LIMIT, HUB_MAX_REQUEST_BODY_BYTES, HUB_CHAT_MESSAGE_MAX_CHARS, HUB_INBOX_TEMPLATE_NAMESPACES, HUB_ACTIVITY_TEMPLATE_NAMESPACES;
|
|
12469
|
+
var HUB_API_PREFIX, HUB_ROUTES, HUB_FORTRESS_AGENT_ID_SENTINEL, HUB_INBOX_ACTIONS, HUB_AGENT_CONTROL_ACTIONS, HUB_TIER_1_AGENT_CONTROL_ACTIONS, HUB_ACTIVITY_DEFAULT_LIMIT, HUB_ACTIVITY_MAX_LIMIT, HUB_CHAT_THREADS_DEFAULT_LIMIT, HUB_CHAT_THREADS_MAX_LIMIT, HUB_CHAT_TURNS_DEFAULT_LIMIT, HUB_CHAT_TURNS_MAX_LIMIT, HUB_INBOX_DEFAULT_LIMIT, HUB_INBOX_MAX_LIMIT, HUB_AGENTS_DEFAULT_LIMIT, HUB_AGENTS_MAX_LIMIT, HUB_MAX_REQUEST_BODY_BYTES, HUB_CHAT_MESSAGE_MAX_CHARS, HUB_INBOX_TEMPLATE_NAMESPACES, HUB_ACTIVITY_TEMPLATE_NAMESPACES;
|
|
12438
12470
|
var init_constants3 = __esm({
|
|
12439
12471
|
"src/hub/constants.ts"() {
|
|
12440
12472
|
HUB_API_PREFIX = "/api/hub";
|
|
@@ -12462,6 +12494,16 @@ var init_constants3 = __esm({
|
|
|
12462
12494
|
*/
|
|
12463
12495
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
12464
12496
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
12497
|
+
/**
|
|
12498
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
12499
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
12500
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
12501
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
12502
|
+
* v1.3 conversational sovereignty depth.
|
|
12503
|
+
*/
|
|
12504
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
12505
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12506
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12465
12507
|
/**
|
|
12466
12508
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
12467
12509
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -12485,6 +12527,10 @@ var init_constants3 = __esm({
|
|
|
12485
12527
|
];
|
|
12486
12528
|
HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
12487
12529
|
HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
12530
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
12531
|
+
HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
12532
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
12533
|
+
HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
12488
12534
|
HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
12489
12535
|
HUB_INBOX_MAX_LIMIT = 500;
|
|
12490
12536
|
HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -12667,6 +12713,23 @@ function checkChatMessage(value) {
|
|
|
12667
12713
|
}
|
|
12668
12714
|
return trimmed;
|
|
12669
12715
|
}
|
|
12716
|
+
function matchConciergeThreadRoute(path) {
|
|
12717
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
12718
|
+
if (!path.startsWith(prefix)) return null;
|
|
12719
|
+
const rest = path.slice(prefix.length);
|
|
12720
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
12721
|
+
const decoded = decodeURIComponent(rest);
|
|
12722
|
+
if (decoded.length === 0) return null;
|
|
12723
|
+
return { threadId: decoded };
|
|
12724
|
+
}
|
|
12725
|
+
function parseSince(raw) {
|
|
12726
|
+
if (raw === null || raw === "") return void 0;
|
|
12727
|
+
const parsed = Number.parseInt(raw, 10);
|
|
12728
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
12729
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
12730
|
+
}
|
|
12731
|
+
return parsed;
|
|
12732
|
+
}
|
|
12670
12733
|
function matchInboxRoute(path) {
|
|
12671
12734
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
12672
12735
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12850,6 +12913,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12850
12913
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12851
12914
|
return true;
|
|
12852
12915
|
}
|
|
12916
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12917
|
+
const limit = parseLimit(
|
|
12918
|
+
url.searchParams.get("limit"),
|
|
12919
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12920
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12921
|
+
);
|
|
12922
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12923
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12924
|
+
return true;
|
|
12925
|
+
}
|
|
12926
|
+
{
|
|
12927
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12928
|
+
if (threadMatch) {
|
|
12929
|
+
if (method === "GET") {
|
|
12930
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12931
|
+
const limit = parseLimit(
|
|
12932
|
+
url.searchParams.get("limit"),
|
|
12933
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12934
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12935
|
+
);
|
|
12936
|
+
const readOpts = { limit };
|
|
12937
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12938
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12939
|
+
threadMatch.threadId,
|
|
12940
|
+
readOpts
|
|
12941
|
+
);
|
|
12942
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12943
|
+
return true;
|
|
12944
|
+
}
|
|
12945
|
+
if (method === "DELETE") {
|
|
12946
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12947
|
+
threadMatch.threadId
|
|
12948
|
+
);
|
|
12949
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12950
|
+
ok: removed,
|
|
12951
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12952
|
+
});
|
|
12953
|
+
return true;
|
|
12954
|
+
}
|
|
12955
|
+
}
|
|
12956
|
+
}
|
|
12853
12957
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12854
12958
|
return true;
|
|
12855
12959
|
} catch (err) {
|
|
@@ -16972,6 +17076,168 @@ var init_dispatch = __esm({
|
|
|
16972
17076
|
init_intelligence_api_router();
|
|
16973
17077
|
}
|
|
16974
17078
|
});
|
|
17079
|
+
|
|
17080
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
17081
|
+
function writeJSON4(res, status, payload) {
|
|
17082
|
+
res.writeHead(status, {
|
|
17083
|
+
"Content-Type": "application/json",
|
|
17084
|
+
"Cache-Control": "no-store"
|
|
17085
|
+
});
|
|
17086
|
+
res.end(JSON.stringify(payload));
|
|
17087
|
+
}
|
|
17088
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
17089
|
+
if (raw === null || raw === "") return defaultValue;
|
|
17090
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17091
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
17092
|
+
return defaultValue;
|
|
17093
|
+
}
|
|
17094
|
+
return Math.min(parsed, max);
|
|
17095
|
+
}
|
|
17096
|
+
function isStatusFilter(value) {
|
|
17097
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
17098
|
+
}
|
|
17099
|
+
function matchEntryRoute(path) {
|
|
17100
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
17101
|
+
if (!path.startsWith(prefix)) return null;
|
|
17102
|
+
const rest = path.slice(prefix.length);
|
|
17103
|
+
if (rest.length === 0) return null;
|
|
17104
|
+
const slash = rest.indexOf("/");
|
|
17105
|
+
if (slash === -1) {
|
|
17106
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
17107
|
+
}
|
|
17108
|
+
return {
|
|
17109
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
17110
|
+
action: rest.slice(slash + 1)
|
|
17111
|
+
};
|
|
17112
|
+
}
|
|
17113
|
+
async function handleStream2(deps, res) {
|
|
17114
|
+
res.writeHead(200, {
|
|
17115
|
+
"Content-Type": "text/event-stream",
|
|
17116
|
+
"Cache-Control": "no-cache, no-transform",
|
|
17117
|
+
Connection: "keep-alive",
|
|
17118
|
+
"X-Accel-Buffering": "no"
|
|
17119
|
+
});
|
|
17120
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
17121
|
+
res.write(
|
|
17122
|
+
`event: approval_inbox_snapshot
|
|
17123
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
17124
|
+
|
|
17125
|
+
`
|
|
17126
|
+
);
|
|
17127
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
17128
|
+
try {
|
|
17129
|
+
res.write(
|
|
17130
|
+
`event: approval_inbox_${event.type}
|
|
17131
|
+
data: ${JSON.stringify(event.entry)}
|
|
17132
|
+
|
|
17133
|
+
`
|
|
17134
|
+
);
|
|
17135
|
+
} catch {
|
|
17136
|
+
}
|
|
17137
|
+
});
|
|
17138
|
+
const keepAlive = setInterval(() => {
|
|
17139
|
+
try {
|
|
17140
|
+
res.write(": keepalive\n\n");
|
|
17141
|
+
} catch {
|
|
17142
|
+
}
|
|
17143
|
+
}, 25e3);
|
|
17144
|
+
const cleanup = () => {
|
|
17145
|
+
clearInterval(keepAlive);
|
|
17146
|
+
unsubscribe();
|
|
17147
|
+
};
|
|
17148
|
+
res.on("close", cleanup);
|
|
17149
|
+
res.on("error", cleanup);
|
|
17150
|
+
}
|
|
17151
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
17152
|
+
const host = req.headers.host || "localhost";
|
|
17153
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
17154
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
17155
|
+
const path = url.pathname;
|
|
17156
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
17157
|
+
return false;
|
|
17158
|
+
}
|
|
17159
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
17160
|
+
if (!checkAuth(req, res, url)) return true;
|
|
17161
|
+
try {
|
|
17162
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
17163
|
+
await handleStream2(deps, res);
|
|
17164
|
+
return true;
|
|
17165
|
+
}
|
|
17166
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
17167
|
+
const limit = parseLimit2(
|
|
17168
|
+
url.searchParams.get("limit"),
|
|
17169
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
17170
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
17171
|
+
);
|
|
17172
|
+
const statusRaw = url.searchParams.get("status");
|
|
17173
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
17174
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
17175
|
+
const entries = await deps.aggregator.list({
|
|
17176
|
+
status,
|
|
17177
|
+
limit,
|
|
17178
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
17179
|
+
});
|
|
17180
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
17181
|
+
return true;
|
|
17182
|
+
}
|
|
17183
|
+
const entryMatch = matchEntryRoute(path);
|
|
17184
|
+
if (entryMatch === null) {
|
|
17185
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17186
|
+
return true;
|
|
17187
|
+
}
|
|
17188
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
17189
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
17190
|
+
const entry = entries.find(
|
|
17191
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
17192
|
+
);
|
|
17193
|
+
if (!entry) {
|
|
17194
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17195
|
+
return true;
|
|
17196
|
+
}
|
|
17197
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
17198
|
+
entryMatch.aggregatorId
|
|
17199
|
+
);
|
|
17200
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
17201
|
+
return true;
|
|
17202
|
+
}
|
|
17203
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
17204
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
17205
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17206
|
+
try {
|
|
17207
|
+
const entry = await deps.aggregator.resolve(
|
|
17208
|
+
entryMatch.aggregatorId,
|
|
17209
|
+
decision,
|
|
17210
|
+
operatorId
|
|
17211
|
+
);
|
|
17212
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
17213
|
+
} catch (err) {
|
|
17214
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17215
|
+
if (msg === "approval-aggregator: not_found") {
|
|
17216
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17217
|
+
} else {
|
|
17218
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17219
|
+
}
|
|
17220
|
+
}
|
|
17221
|
+
return true;
|
|
17222
|
+
}
|
|
17223
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17224
|
+
return true;
|
|
17225
|
+
} catch (err) {
|
|
17226
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17227
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17228
|
+
return true;
|
|
17229
|
+
}
|
|
17230
|
+
}
|
|
17231
|
+
var APPROVAL_INBOX_API_PREFIX, APPROVAL_INBOX_OPERATOR_DEFAULT, APPROVAL_INBOX_DEFAULT_LIMIT, APPROVAL_INBOX_MAX_LIMIT;
|
|
17232
|
+
var init_approval_aggregator_routes = __esm({
|
|
17233
|
+
"src/principal-policy/approval-aggregator-routes.ts"() {
|
|
17234
|
+
init_auth_middleware();
|
|
17235
|
+
APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
17236
|
+
APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
17237
|
+
APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
17238
|
+
APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
17239
|
+
}
|
|
17240
|
+
});
|
|
16975
17241
|
function isDashboardViewRoute(method, path) {
|
|
16976
17242
|
if (method !== "GET") return false;
|
|
16977
17243
|
return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
|
|
@@ -16985,6 +17251,7 @@ var init_dashboard = __esm({
|
|
|
16985
17251
|
init_fortress_view();
|
|
16986
17252
|
init_system_prompt_generator();
|
|
16987
17253
|
init_dispatch();
|
|
17254
|
+
init_approval_aggregator_routes();
|
|
16988
17255
|
SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16989
17256
|
SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
16990
17257
|
MAX_SESSIONS = 1e3;
|
|
@@ -17044,6 +17311,14 @@ var init_dashboard = __esm({
|
|
|
17044
17311
|
* regardless. Default route flip is deferred to v1.2.
|
|
17045
17312
|
*/
|
|
17046
17313
|
v11Bindings = null;
|
|
17314
|
+
/**
|
|
17315
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
17316
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
17317
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
17318
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
17319
|
+
* the operator-facing query / decision surface.
|
|
17320
|
+
*/
|
|
17321
|
+
approvalAggregator = null;
|
|
17047
17322
|
constructor(config) {
|
|
17048
17323
|
this.config = config;
|
|
17049
17324
|
this.authToken = config.auth_token;
|
|
@@ -17094,6 +17369,34 @@ var init_dashboard = __esm({
|
|
|
17094
17369
|
setV11Bindings(bindings) {
|
|
17095
17370
|
this.v11Bindings = bindings;
|
|
17096
17371
|
}
|
|
17372
|
+
/**
|
|
17373
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
17374
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
17375
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
17376
|
+
* tests + during shutdown).
|
|
17377
|
+
*/
|
|
17378
|
+
setApprovalAggregator(aggregator) {
|
|
17379
|
+
this.approvalAggregator = aggregator;
|
|
17380
|
+
}
|
|
17381
|
+
/**
|
|
17382
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
17383
|
+
* before the legacy approval route table. Returns true when served.
|
|
17384
|
+
*/
|
|
17385
|
+
async dispatchApprovalInbox(req, res) {
|
|
17386
|
+
if (!this.approvalAggregator) return false;
|
|
17387
|
+
return handleApprovalInboxRoute(
|
|
17388
|
+
{
|
|
17389
|
+
authConfig: {
|
|
17390
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
17391
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
17392
|
+
},
|
|
17393
|
+
aggregator: this.approvalAggregator,
|
|
17394
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
17395
|
+
},
|
|
17396
|
+
req,
|
|
17397
|
+
res
|
|
17398
|
+
);
|
|
17399
|
+
}
|
|
17097
17400
|
/**
|
|
17098
17401
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
17099
17402
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -17469,6 +17772,18 @@ var init_dashboard = __esm({
|
|
|
17469
17772
|
res.end();
|
|
17470
17773
|
return;
|
|
17471
17774
|
}
|
|
17775
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
17776
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
17777
|
+
if (handled) return;
|
|
17778
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
17779
|
+
}).catch(() => {
|
|
17780
|
+
if (!res.headersSent) {
|
|
17781
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
17782
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
17783
|
+
}
|
|
17784
|
+
});
|
|
17785
|
+
return;
|
|
17786
|
+
}
|
|
17472
17787
|
if (this.v11Bindings) {
|
|
17473
17788
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
17474
17789
|
if (handled) return;
|
|
@@ -19512,14 +19827,25 @@ var init_gate = __esm({
|
|
|
19512
19827
|
auditLog;
|
|
19513
19828
|
injectionDetector;
|
|
19514
19829
|
onInjectionAlert;
|
|
19830
|
+
onApprovalEvent;
|
|
19515
19831
|
proxyTierResolver;
|
|
19516
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
19832
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
19517
19833
|
this.policy = policy;
|
|
19518
19834
|
this.baseline = baseline;
|
|
19519
19835
|
this.channel = channel;
|
|
19520
19836
|
this.auditLog = auditLog;
|
|
19521
19837
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
19522
19838
|
this.onInjectionAlert = onInjectionAlert;
|
|
19839
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
19840
|
+
}
|
|
19841
|
+
/**
|
|
19842
|
+
* Set the approval-event callback after construction. Used by the
|
|
19843
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
19844
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
19845
|
+
* constructor so existing call sites continue to work unchanged.
|
|
19846
|
+
*/
|
|
19847
|
+
setApprovalEventCallback(cb) {
|
|
19848
|
+
this.onApprovalEvent = cb;
|
|
19523
19849
|
}
|
|
19524
19850
|
/**
|
|
19525
19851
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -19753,21 +20079,105 @@ var init_gate = __esm({
|
|
|
19753
20079
|
}
|
|
19754
20080
|
/**
|
|
19755
20081
|
* Request approval from the human principal.
|
|
20082
|
+
*
|
|
20083
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
20084
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
20085
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
20086
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
20087
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
20088
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
19756
20089
|
*/
|
|
19757
20090
|
async requestApproval(operation, tier, reason, context) {
|
|
20091
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19758
20092
|
const request = {
|
|
19759
20093
|
operation,
|
|
19760
20094
|
tier,
|
|
19761
20095
|
reason,
|
|
19762
20096
|
context,
|
|
19763
|
-
timestamp:
|
|
20097
|
+
timestamp: requestTimestamp
|
|
19764
20098
|
};
|
|
19765
|
-
const
|
|
20099
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
20100
|
+
if (this.onApprovalEvent) {
|
|
20101
|
+
try {
|
|
20102
|
+
this.onApprovalEvent({
|
|
20103
|
+
phase: "requested",
|
|
20104
|
+
operation,
|
|
20105
|
+
tier,
|
|
20106
|
+
reason,
|
|
20107
|
+
context,
|
|
20108
|
+
request_timestamp: requestTimestamp,
|
|
20109
|
+
correlation_id: correlationId
|
|
20110
|
+
});
|
|
20111
|
+
} catch {
|
|
20112
|
+
}
|
|
20113
|
+
}
|
|
20114
|
+
let response;
|
|
20115
|
+
try {
|
|
20116
|
+
response = await this.channel.requestApproval(request);
|
|
20117
|
+
} catch (err) {
|
|
20118
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
20119
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20120
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
20121
|
+
tier,
|
|
20122
|
+
reason,
|
|
20123
|
+
decided_by: "channel_failure",
|
|
20124
|
+
channel_error: errMessage
|
|
20125
|
+
});
|
|
20126
|
+
if (this.onApprovalEvent) {
|
|
20127
|
+
try {
|
|
20128
|
+
this.onApprovalEvent({
|
|
20129
|
+
phase: "resolved",
|
|
20130
|
+
operation,
|
|
20131
|
+
tier,
|
|
20132
|
+
reason,
|
|
20133
|
+
context,
|
|
20134
|
+
request_timestamp: requestTimestamp,
|
|
20135
|
+
resolution: {
|
|
20136
|
+
decision: "deny",
|
|
20137
|
+
decided_at: decidedAt,
|
|
20138
|
+
decided_by: "channel_failure"
|
|
20139
|
+
},
|
|
20140
|
+
correlation_id: correlationId
|
|
20141
|
+
});
|
|
20142
|
+
} catch {
|
|
20143
|
+
}
|
|
20144
|
+
}
|
|
20145
|
+
return {
|
|
20146
|
+
allowed: false,
|
|
20147
|
+
tier,
|
|
20148
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
20149
|
+
approval_required: true,
|
|
20150
|
+
approval_response: {
|
|
20151
|
+
decision: "deny",
|
|
20152
|
+
decided_at: decidedAt,
|
|
20153
|
+
decided_by: "channel_failure"
|
|
20154
|
+
}
|
|
20155
|
+
};
|
|
20156
|
+
}
|
|
19766
20157
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
19767
20158
|
tier,
|
|
19768
20159
|
reason,
|
|
19769
20160
|
decided_by: response.decided_by
|
|
19770
20161
|
});
|
|
20162
|
+
if (this.onApprovalEvent) {
|
|
20163
|
+
try {
|
|
20164
|
+
this.onApprovalEvent({
|
|
20165
|
+
phase: "resolved",
|
|
20166
|
+
operation,
|
|
20167
|
+
tier,
|
|
20168
|
+
reason,
|
|
20169
|
+
context,
|
|
20170
|
+
request_timestamp: requestTimestamp,
|
|
20171
|
+
resolution: {
|
|
20172
|
+
decision: response.decision,
|
|
20173
|
+
decided_at: response.decided_at,
|
|
20174
|
+
decided_by: response.decided_by
|
|
20175
|
+
},
|
|
20176
|
+
correlation_id: correlationId
|
|
20177
|
+
});
|
|
20178
|
+
} catch {
|
|
20179
|
+
}
|
|
20180
|
+
}
|
|
19771
20181
|
return {
|
|
19772
20182
|
allowed: response.decision === "approve",
|
|
19773
20183
|
tier,
|
|
@@ -19802,6 +20212,356 @@ var init_gate = __esm({
|
|
|
19802
20212
|
};
|
|
19803
20213
|
}
|
|
19804
20214
|
});
|
|
20215
|
+
var APPROVAL_AGGREGATOR_NAMESPACE, APPROVAL_AGGREGATOR_HKDF_INFO, APPROVAL_AGGREGATOR_AUDIT_OPS, DEFAULT_PENDING_TTL_MS, DEFAULT_MAX_LIST_LIMIT, DEFAULT_LIST_PAGE_SIZE, ApprovalAggregator;
|
|
20216
|
+
var init_approval_aggregator = __esm({
|
|
20217
|
+
"src/principal-policy/approval-aggregator.ts"() {
|
|
20218
|
+
init_encryption();
|
|
20219
|
+
init_key_derivation();
|
|
20220
|
+
init_encoding();
|
|
20221
|
+
APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
20222
|
+
APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
20223
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
20224
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
20225
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
20226
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
20227
|
+
};
|
|
20228
|
+
DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
20229
|
+
DEFAULT_MAX_LIST_LIMIT = 200;
|
|
20230
|
+
DEFAULT_LIST_PAGE_SIZE = 50;
|
|
20231
|
+
ApprovalAggregator = class {
|
|
20232
|
+
storage;
|
|
20233
|
+
encryptionKey;
|
|
20234
|
+
auditLog;
|
|
20235
|
+
identityId;
|
|
20236
|
+
fortressId;
|
|
20237
|
+
pendingTtlMs;
|
|
20238
|
+
maxListLimit;
|
|
20239
|
+
now;
|
|
20240
|
+
resolveSourceContext;
|
|
20241
|
+
resolveHubInboxItemId;
|
|
20242
|
+
/** Cached entries by `aggregator_id`. */
|
|
20243
|
+
entries = /* @__PURE__ */ new Map();
|
|
20244
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
20245
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
20246
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
20247
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
20248
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
20249
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
20250
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
20251
|
+
hydrated = false;
|
|
20252
|
+
/** Active SSE listeners. */
|
|
20253
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20254
|
+
constructor(deps) {
|
|
20255
|
+
this.storage = deps.storage;
|
|
20256
|
+
this.encryptionKey = derivePurposeKey(
|
|
20257
|
+
deps.masterKey,
|
|
20258
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
20259
|
+
);
|
|
20260
|
+
this.auditLog = deps.auditLog;
|
|
20261
|
+
this.identityId = deps.identityId;
|
|
20262
|
+
this.fortressId = deps.fortressId;
|
|
20263
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
20264
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
20265
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
20266
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
20267
|
+
source_harness: this.fortressId,
|
|
20268
|
+
source_agent_id: this.fortressId
|
|
20269
|
+
}));
|
|
20270
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
20271
|
+
}
|
|
20272
|
+
/**
|
|
20273
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
20274
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
20275
|
+
*/
|
|
20276
|
+
onEvent(listener) {
|
|
20277
|
+
this.listeners.add(listener);
|
|
20278
|
+
return () => this.listeners.delete(listener);
|
|
20279
|
+
}
|
|
20280
|
+
/**
|
|
20281
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
20282
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
20283
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
20284
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
20285
|
+
* a record for).
|
|
20286
|
+
*/
|
|
20287
|
+
async ingest(event) {
|
|
20288
|
+
await this.hydrate();
|
|
20289
|
+
if (event.phase === "requested") {
|
|
20290
|
+
return this.ingestRequested(event);
|
|
20291
|
+
}
|
|
20292
|
+
if (event.phase === "resolved") {
|
|
20293
|
+
return this.ingestResolved(event);
|
|
20294
|
+
}
|
|
20295
|
+
return null;
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
20299
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
20300
|
+
* snapshot is returned.
|
|
20301
|
+
*/
|
|
20302
|
+
async list(opts) {
|
|
20303
|
+
await this.hydrate();
|
|
20304
|
+
await this.expireStale();
|
|
20305
|
+
const limit = Math.min(
|
|
20306
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
20307
|
+
this.maxListLimit
|
|
20308
|
+
);
|
|
20309
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
20310
|
+
const matching = [];
|
|
20311
|
+
for (const entry of this.entries.values()) {
|
|
20312
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
20313
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
20314
|
+
matching.push(entry);
|
|
20315
|
+
}
|
|
20316
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
20317
|
+
return matching.slice(0, limit);
|
|
20318
|
+
}
|
|
20319
|
+
/**
|
|
20320
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
20321
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
20322
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
20323
|
+
*/
|
|
20324
|
+
async getFullPayload(aggregatorId) {
|
|
20325
|
+
await this.hydrate();
|
|
20326
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
20327
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
20328
|
+
}
|
|
20329
|
+
/**
|
|
20330
|
+
* Resolve an entry. Used by both:
|
|
20331
|
+
* 1. The gate wire-up on channel-decision return.
|
|
20332
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
20333
|
+
*
|
|
20334
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
20335
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
20336
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
20337
|
+
* routes return 404.
|
|
20338
|
+
*/
|
|
20339
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
20340
|
+
await this.hydrate();
|
|
20341
|
+
const entry = this.entries.get(aggregatorId);
|
|
20342
|
+
if (!entry) {
|
|
20343
|
+
throw new Error("approval-aggregator: not_found");
|
|
20344
|
+
}
|
|
20345
|
+
if (entry.status !== "pending") {
|
|
20346
|
+
return entry;
|
|
20347
|
+
}
|
|
20348
|
+
entry.status = decision;
|
|
20349
|
+
entry.resolved_at = this.now().toISOString();
|
|
20350
|
+
entry.resolved_by = operatorId;
|
|
20351
|
+
await this.persist(entry);
|
|
20352
|
+
this.auditLog.append(
|
|
20353
|
+
"l2",
|
|
20354
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20355
|
+
this.identityId,
|
|
20356
|
+
{
|
|
20357
|
+
aggregator_id: entry.aggregator_id,
|
|
20358
|
+
source_harness: entry.source_harness,
|
|
20359
|
+
source_agent_id: entry.source_agent_id,
|
|
20360
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20361
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20362
|
+
decision,
|
|
20363
|
+
decided_by: operatorId,
|
|
20364
|
+
decided_at: entry.resolved_at
|
|
20365
|
+
}
|
|
20366
|
+
);
|
|
20367
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20368
|
+
return entry;
|
|
20369
|
+
}
|
|
20370
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
20371
|
+
async ingestRequested(event) {
|
|
20372
|
+
const ctx = this.resolveSourceContext(event);
|
|
20373
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
20374
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
20375
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
20376
|
+
if (existing) {
|
|
20377
|
+
const existingEntry = this.entries.get(existing);
|
|
20378
|
+
if (existingEntry) {
|
|
20379
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
20380
|
+
this.auditLog.append(
|
|
20381
|
+
"l2",
|
|
20382
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
20383
|
+
this.identityId,
|
|
20384
|
+
{
|
|
20385
|
+
aggregator_id: existing,
|
|
20386
|
+
source_harness: ctx.source_harness,
|
|
20387
|
+
source_agent_id: ctx.source_agent_id,
|
|
20388
|
+
audit_log_entry_id: auditId,
|
|
20389
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20390
|
+
correlation_id: event.correlation_id
|
|
20391
|
+
}
|
|
20392
|
+
);
|
|
20393
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
20394
|
+
return null;
|
|
20395
|
+
}
|
|
20396
|
+
}
|
|
20397
|
+
const id = randomUUID();
|
|
20398
|
+
const now = this.now();
|
|
20399
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
20400
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
20401
|
+
const entry = {
|
|
20402
|
+
aggregator_id: id,
|
|
20403
|
+
source_harness: ctx.source_harness,
|
|
20404
|
+
source_agent_id: ctx.source_agent_id,
|
|
20405
|
+
audit_log_entry_id: auditId,
|
|
20406
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20407
|
+
action_summary: this.deriveActionSummary(event),
|
|
20408
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
20409
|
+
status: "pending",
|
|
20410
|
+
created_at: now.toISOString(),
|
|
20411
|
+
expires_at: expires.toISOString(),
|
|
20412
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20413
|
+
};
|
|
20414
|
+
this.entries.set(id, entry);
|
|
20415
|
+
this.dedupIndex.set(dedupKey, id);
|
|
20416
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
20417
|
+
this.fullPayloads.set(id, event.context);
|
|
20418
|
+
await this.persist(entry);
|
|
20419
|
+
this.auditLog.append(
|
|
20420
|
+
"l2",
|
|
20421
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
20422
|
+
this.identityId,
|
|
20423
|
+
{
|
|
20424
|
+
aggregator_id: id,
|
|
20425
|
+
source_harness: ctx.source_harness,
|
|
20426
|
+
source_agent_id: ctx.source_agent_id,
|
|
20427
|
+
audit_log_entry_id: auditId,
|
|
20428
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20429
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20430
|
+
}
|
|
20431
|
+
);
|
|
20432
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
20433
|
+
return entry;
|
|
20434
|
+
}
|
|
20435
|
+
async ingestResolved(event) {
|
|
20436
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
20437
|
+
if (!id) return null;
|
|
20438
|
+
const entry = this.entries.get(id);
|
|
20439
|
+
if (!entry) return null;
|
|
20440
|
+
if (entry.status !== "pending") return entry;
|
|
20441
|
+
if (!event.resolution) return entry;
|
|
20442
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
20443
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
20444
|
+
entry.status = status;
|
|
20445
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
20446
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
20447
|
+
await this.persist(entry);
|
|
20448
|
+
this.auditLog.append(
|
|
20449
|
+
"l2",
|
|
20450
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20451
|
+
this.identityId,
|
|
20452
|
+
{
|
|
20453
|
+
aggregator_id: id,
|
|
20454
|
+
source_harness: entry.source_harness,
|
|
20455
|
+
source_agent_id: entry.source_agent_id,
|
|
20456
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20457
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20458
|
+
decision: status,
|
|
20459
|
+
decided_by: entry.resolved_by,
|
|
20460
|
+
decided_at: entry.resolved_at,
|
|
20461
|
+
fail_closed: failClosed
|
|
20462
|
+
}
|
|
20463
|
+
);
|
|
20464
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20465
|
+
return entry;
|
|
20466
|
+
}
|
|
20467
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
20468
|
+
/**
|
|
20469
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
20470
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
20471
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
20472
|
+
* the audit entry the gate appended on the same call.
|
|
20473
|
+
*/
|
|
20474
|
+
auditEntryIdForEvent(event) {
|
|
20475
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
20476
|
+
}
|
|
20477
|
+
derivePolicyRuleId(event) {
|
|
20478
|
+
return `tier${event.tier}:${event.operation}`;
|
|
20479
|
+
}
|
|
20480
|
+
deriveActionSummary(event) {
|
|
20481
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
20482
|
+
}
|
|
20483
|
+
/**
|
|
20484
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
20485
|
+
* identical payloads always hash the same, even when key insertion order
|
|
20486
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
20487
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
20488
|
+
*/
|
|
20489
|
+
hashPayload(payload) {
|
|
20490
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
20491
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
20492
|
+
}
|
|
20493
|
+
emit(event) {
|
|
20494
|
+
for (const listener of this.listeners) {
|
|
20495
|
+
try {
|
|
20496
|
+
listener(event);
|
|
20497
|
+
} catch {
|
|
20498
|
+
}
|
|
20499
|
+
}
|
|
20500
|
+
}
|
|
20501
|
+
async expireStale() {
|
|
20502
|
+
const nowMs = this.now().getTime();
|
|
20503
|
+
for (const entry of this.entries.values()) {
|
|
20504
|
+
if (entry.status !== "pending") continue;
|
|
20505
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
20506
|
+
entry.status = "expired";
|
|
20507
|
+
entry.resolved_at = this.now().toISOString();
|
|
20508
|
+
entry.resolved_by = "system_ttl";
|
|
20509
|
+
await this.persist(entry);
|
|
20510
|
+
this.auditLog.append(
|
|
20511
|
+
"l2",
|
|
20512
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20513
|
+
this.identityId,
|
|
20514
|
+
{
|
|
20515
|
+
aggregator_id: entry.aggregator_id,
|
|
20516
|
+
source_harness: entry.source_harness,
|
|
20517
|
+
source_agent_id: entry.source_agent_id,
|
|
20518
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20519
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20520
|
+
decision: "expired",
|
|
20521
|
+
decided_by: "system_ttl",
|
|
20522
|
+
decided_at: entry.resolved_at
|
|
20523
|
+
}
|
|
20524
|
+
);
|
|
20525
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20526
|
+
}
|
|
20527
|
+
}
|
|
20528
|
+
async persist(entry) {
|
|
20529
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
20530
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
20531
|
+
await this.storage.write(
|
|
20532
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20533
|
+
entry.aggregator_id,
|
|
20534
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
20535
|
+
);
|
|
20536
|
+
}
|
|
20537
|
+
async hydrate() {
|
|
20538
|
+
if (this.hydrated) return;
|
|
20539
|
+
this.hydrated = true;
|
|
20540
|
+
try {
|
|
20541
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
20542
|
+
for (const meta of metas) {
|
|
20543
|
+
const raw = await this.storage.read(
|
|
20544
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20545
|
+
meta.key
|
|
20546
|
+
);
|
|
20547
|
+
if (!raw) continue;
|
|
20548
|
+
try {
|
|
20549
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
20550
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
20551
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
20552
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
20553
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
20554
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
20555
|
+
} catch {
|
|
20556
|
+
}
|
|
20557
|
+
}
|
|
20558
|
+
} catch {
|
|
20559
|
+
this.hydrated = false;
|
|
20560
|
+
}
|
|
20561
|
+
}
|
|
20562
|
+
};
|
|
20563
|
+
}
|
|
20564
|
+
});
|
|
19805
20565
|
|
|
19806
20566
|
// src/principal-policy/tools.ts
|
|
19807
20567
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
@@ -22719,6 +23479,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
22719
23479
|
}
|
|
22720
23480
|
async function consumeResetHistoryMarker(options) {
|
|
22721
23481
|
const markerPath = join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
23482
|
+
const consumedPath = markerPath + ".consumed";
|
|
23483
|
+
if (await fileExists3(consumedPath)) {
|
|
23484
|
+
await rm(markerPath, { force: true });
|
|
23485
|
+
await rm(consumedPath, { force: true });
|
|
23486
|
+
return { emitted: 0, markerPath };
|
|
23487
|
+
}
|
|
22722
23488
|
if (!await fileExists3(markerPath)) {
|
|
22723
23489
|
return { emitted: 0, markerPath };
|
|
22724
23490
|
}
|
|
@@ -22743,7 +23509,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
22743
23509
|
});
|
|
22744
23510
|
}
|
|
22745
23511
|
await options.auditLog.flush();
|
|
23512
|
+
await writeFile(consumedPath, "", "utf-8");
|
|
22746
23513
|
await rm(markerPath, { force: true });
|
|
23514
|
+
await rm(consumedPath, { force: true });
|
|
22747
23515
|
return { emitted: markers.length, markerHash, markerPath };
|
|
22748
23516
|
}
|
|
22749
23517
|
async function fileExists3(path) {
|
|
@@ -32026,6 +32794,36 @@ var init_hub_service = __esm({
|
|
|
32026
32794
|
const chat = this.requireOperatorChat();
|
|
32027
32795
|
return chat.getConciergeHistory();
|
|
32028
32796
|
}
|
|
32797
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
32798
|
+
/**
|
|
32799
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
32800
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
32801
|
+
* surface is unavailable on a given fortress.
|
|
32802
|
+
*/
|
|
32803
|
+
hasConciergeMemory() {
|
|
32804
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
32805
|
+
}
|
|
32806
|
+
async listConciergeMemoryThreads(opts) {
|
|
32807
|
+
const chat = this.requireOperatorChat();
|
|
32808
|
+
if (!chat.hasConciergeMemory()) {
|
|
32809
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32810
|
+
}
|
|
32811
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
32812
|
+
}
|
|
32813
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
32814
|
+
const chat = this.requireOperatorChat();
|
|
32815
|
+
if (!chat.hasConciergeMemory()) {
|
|
32816
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32817
|
+
}
|
|
32818
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
32819
|
+
}
|
|
32820
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
32821
|
+
const chat = this.requireOperatorChat();
|
|
32822
|
+
if (!chat.hasConciergeMemory()) {
|
|
32823
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32824
|
+
}
|
|
32825
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
32826
|
+
}
|
|
32029
32827
|
/**
|
|
32030
32828
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
32031
32829
|
* panel surfaces recent activity routed through this agent, pending
|
|
@@ -32164,7 +32962,20 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32164
32962
|
* affordance now opens an inspect/approve panel (recent activity +
|
|
32165
32963
|
* pending approvals + policy summary) instead of a chat session.
|
|
32166
32964
|
*/
|
|
32167
|
-
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened"
|
|
32965
|
+
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
|
|
32966
|
+
/**
|
|
32967
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
32968
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
32969
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
32970
|
+
* raw turn content never crosses the audit surface.
|
|
32971
|
+
*/
|
|
32972
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
32973
|
+
/**
|
|
32974
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
32975
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
32976
|
+
* the deleted bundle.
|
|
32977
|
+
*/
|
|
32978
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
32168
32979
|
};
|
|
32169
32980
|
}
|
|
32170
32981
|
});
|
|
@@ -32226,6 +33037,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32226
33037
|
contextProviders;
|
|
32227
33038
|
piiFilter;
|
|
32228
33039
|
conciergeMaxTokens;
|
|
33040
|
+
memory;
|
|
33041
|
+
/**
|
|
33042
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
33043
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
33044
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
33045
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33046
|
+
*/
|
|
33047
|
+
activeMemoryThreadId;
|
|
32229
33048
|
constructor(deps) {
|
|
32230
33049
|
this.store = deps.store;
|
|
32231
33050
|
this.auditLog = deps.auditLog;
|
|
@@ -32236,6 +33055,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32236
33055
|
}
|
|
32237
33056
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
32238
33057
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33058
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32239
33059
|
}
|
|
32240
33060
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
32241
33061
|
/**
|
|
@@ -32266,6 +33086,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32266
33086
|
CONCIERGE_THREAD_KEY,
|
|
32267
33087
|
operatorMessage
|
|
32268
33088
|
);
|
|
33089
|
+
if (this.memory) {
|
|
33090
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33091
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
33092
|
+
});
|
|
33093
|
+
}
|
|
32269
33094
|
const start = Date.now();
|
|
32270
33095
|
let conciergeBody;
|
|
32271
33096
|
let servedBy = "disabled";
|
|
@@ -32321,6 +33146,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32321
33146
|
CONCIERGE_THREAD_KEY,
|
|
32322
33147
|
responseMessage
|
|
32323
33148
|
);
|
|
33149
|
+
if (this.memory) {
|
|
33150
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33151
|
+
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
|
|
33152
|
+
});
|
|
33153
|
+
}
|
|
32324
33154
|
const payload = {
|
|
32325
33155
|
version: "1.2",
|
|
32326
33156
|
event_id: makeEventId("conc"),
|
|
@@ -32353,6 +33183,105 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32353
33183
|
);
|
|
32354
33184
|
return thread ? thread.messages : [];
|
|
32355
33185
|
}
|
|
33186
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
33187
|
+
/**
|
|
33188
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
33189
|
+
* 503 cleanly when called against an unwired service.
|
|
33190
|
+
*/
|
|
33191
|
+
hasConciergeMemory() {
|
|
33192
|
+
return this.memory !== void 0;
|
|
33193
|
+
}
|
|
33194
|
+
/**
|
|
33195
|
+
* List concierge memory threads, newest-first. Emits the
|
|
33196
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
33197
|
+
*/
|
|
33198
|
+
async listConciergeMemoryThreads(opts) {
|
|
33199
|
+
if (!this.memory) {
|
|
33200
|
+
throw new Error("concierge memory store not configured");
|
|
33201
|
+
}
|
|
33202
|
+
const summaries = await this.memory.listThreads(opts);
|
|
33203
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
33204
|
+
const payload = {
|
|
33205
|
+
version: "1.2",
|
|
33206
|
+
event_id: makeEventId("conc-hist"),
|
|
33207
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33208
|
+
identity_id: this.identityId,
|
|
33209
|
+
kind: "operator_concierge_history_read",
|
|
33210
|
+
surface: "concierge",
|
|
33211
|
+
thread_id: "*",
|
|
33212
|
+
turn_count: totalTurns
|
|
33213
|
+
};
|
|
33214
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33215
|
+
return summaries;
|
|
33216
|
+
}
|
|
33217
|
+
/**
|
|
33218
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
33219
|
+
* `operator_concierge_history_read` audit event with the named
|
|
33220
|
+
* thread_id and the count of turns surfaced.
|
|
33221
|
+
*/
|
|
33222
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
33223
|
+
if (!this.memory) {
|
|
33224
|
+
throw new Error("concierge memory store not configured");
|
|
33225
|
+
}
|
|
33226
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
33227
|
+
const payload = {
|
|
33228
|
+
version: "1.2",
|
|
33229
|
+
event_id: makeEventId("conc-hist"),
|
|
33230
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33231
|
+
identity_id: this.identityId,
|
|
33232
|
+
kind: "operator_concierge_history_read",
|
|
33233
|
+
surface: "concierge",
|
|
33234
|
+
thread_id: threadId,
|
|
33235
|
+
turn_count: turns.length
|
|
33236
|
+
};
|
|
33237
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33238
|
+
return turns;
|
|
33239
|
+
}
|
|
33240
|
+
/**
|
|
33241
|
+
* Delete a concierge memory thread. Emits
|
|
33242
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
33243
|
+
* removed; absent threads return false without an audit event.
|
|
33244
|
+
*/
|
|
33245
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
33246
|
+
if (!this.memory) {
|
|
33247
|
+
throw new Error("concierge memory store not configured");
|
|
33248
|
+
}
|
|
33249
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
33250
|
+
if (turnsBefore.length === 0) {
|
|
33251
|
+
return await this.memory.deleteThread(threadId);
|
|
33252
|
+
}
|
|
33253
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
33254
|
+
if (!removed) return false;
|
|
33255
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
33256
|
+
this.activeMemoryThreadId = void 0;
|
|
33257
|
+
}
|
|
33258
|
+
const payload = {
|
|
33259
|
+
version: "1.2",
|
|
33260
|
+
event_id: makeEventId("conc-del"),
|
|
33261
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33262
|
+
identity_id: this.identityId,
|
|
33263
|
+
kind: "operator_concierge_thread_deleted",
|
|
33264
|
+
surface: "concierge",
|
|
33265
|
+
thread_id: threadId,
|
|
33266
|
+
turn_count: turnsBefore.length
|
|
33267
|
+
};
|
|
33268
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
33269
|
+
return true;
|
|
33270
|
+
}
|
|
33271
|
+
/**
|
|
33272
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
33273
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
33274
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
33275
|
+
*/
|
|
33276
|
+
resetConciergeMemoryThread() {
|
|
33277
|
+
this.activeMemoryThreadId = void 0;
|
|
33278
|
+
}
|
|
33279
|
+
ensureActiveMemoryThread() {
|
|
33280
|
+
if (!this.activeMemoryThreadId) {
|
|
33281
|
+
this.activeMemoryThreadId = randomUUID();
|
|
33282
|
+
}
|
|
33283
|
+
return this.activeMemoryThreadId;
|
|
33284
|
+
}
|
|
32356
33285
|
/**
|
|
32357
33286
|
* Stitch fortress state into a single context blob the substrate
|
|
32358
33287
|
* folds into its summarization prompt.
|
|
@@ -32519,11 +33448,250 @@ var init_operator_chat_store = __esm({
|
|
|
32519
33448
|
}
|
|
32520
33449
|
});
|
|
32521
33450
|
|
|
33451
|
+
// src/chat/concierge-memory-store.ts
|
|
33452
|
+
function bundleKey(threadId) {
|
|
33453
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
33454
|
+
}
|
|
33455
|
+
function stripKeyPrefix(key) {
|
|
33456
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
33457
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
33458
|
+
}
|
|
33459
|
+
function lastTurnId(bundle) {
|
|
33460
|
+
let max = 0;
|
|
33461
|
+
for (const t of bundle.turns) {
|
|
33462
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
33463
|
+
}
|
|
33464
|
+
return max;
|
|
33465
|
+
}
|
|
33466
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO2, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES2, ConciergeMemoryStore;
|
|
33467
|
+
var init_concierge_memory_store = __esm({
|
|
33468
|
+
"src/chat/concierge-memory-store.ts"() {
|
|
33469
|
+
init_encryption();
|
|
33470
|
+
init_key_derivation();
|
|
33471
|
+
init_encoding();
|
|
33472
|
+
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
33473
|
+
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
33474
|
+
HKDF_INFO2 = "concierge-memory-store-v1";
|
|
33475
|
+
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
33476
|
+
MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
33477
|
+
ConciergeMemoryStore = class {
|
|
33478
|
+
storage;
|
|
33479
|
+
encryptionKey;
|
|
33480
|
+
fortressId;
|
|
33481
|
+
retentionDays;
|
|
33482
|
+
locks;
|
|
33483
|
+
constructor(opts) {
|
|
33484
|
+
this.storage = opts.storage;
|
|
33485
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
33486
|
+
this.fortressId = opts.fortressId;
|
|
33487
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
33488
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
33489
|
+
}
|
|
33490
|
+
/**
|
|
33491
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
33492
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
33493
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
33494
|
+
* monotonicity even under concurrent callers.
|
|
33495
|
+
*/
|
|
33496
|
+
async appendTurn(threadId, role, content) {
|
|
33497
|
+
return this.withLock(threadId, async () => {
|
|
33498
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
33499
|
+
const now = /* @__PURE__ */ new Date();
|
|
33500
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
33501
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
33502
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
33503
|
+
const turn = {
|
|
33504
|
+
thread_id: threadId,
|
|
33505
|
+
fortress_id: this.fortressId,
|
|
33506
|
+
turn_id: nextTurnId,
|
|
33507
|
+
role,
|
|
33508
|
+
content,
|
|
33509
|
+
created_at: now.toISOString(),
|
|
33510
|
+
retention_until: retentionUntil.toISOString()
|
|
33511
|
+
};
|
|
33512
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
33513
|
+
version: 1,
|
|
33514
|
+
thread_id: threadId,
|
|
33515
|
+
fortress_id: this.fortressId,
|
|
33516
|
+
created_at: now.toISOString(),
|
|
33517
|
+
turns: [turn]
|
|
33518
|
+
};
|
|
33519
|
+
await this.saveBundle(next);
|
|
33520
|
+
return turn;
|
|
33521
|
+
});
|
|
33522
|
+
}
|
|
33523
|
+
/**
|
|
33524
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
33525
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
33526
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
33527
|
+
*/
|
|
33528
|
+
async readThread(threadId, opts) {
|
|
33529
|
+
const bundle = await this.loadBundle(threadId);
|
|
33530
|
+
if (!bundle) return [];
|
|
33531
|
+
let turns = bundle.turns;
|
|
33532
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
33533
|
+
const cutoff = opts.sinceTurnId;
|
|
33534
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
33535
|
+
}
|
|
33536
|
+
if (opts?.limit !== void 0) {
|
|
33537
|
+
turns = turns.slice(0, opts.limit);
|
|
33538
|
+
}
|
|
33539
|
+
return turns;
|
|
33540
|
+
}
|
|
33541
|
+
/**
|
|
33542
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33543
|
+
* Sorted newest-first by last_turn_at.
|
|
33544
|
+
*/
|
|
33545
|
+
async listThreads(opts) {
|
|
33546
|
+
const entries = await this.storage.list(
|
|
33547
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33548
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33549
|
+
);
|
|
33550
|
+
const summaries = [];
|
|
33551
|
+
for (const meta of entries) {
|
|
33552
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33553
|
+
if (threadId === null) continue;
|
|
33554
|
+
const bundle = await this.loadBundle(threadId);
|
|
33555
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
33556
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
33557
|
+
summaries.push({
|
|
33558
|
+
thread_id: bundle.thread_id,
|
|
33559
|
+
created_at: bundle.created_at,
|
|
33560
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
33561
|
+
turn_count: bundle.turns.length
|
|
33562
|
+
});
|
|
33563
|
+
}
|
|
33564
|
+
summaries.sort(
|
|
33565
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
33566
|
+
);
|
|
33567
|
+
if (opts?.limit !== void 0) {
|
|
33568
|
+
return summaries.slice(0, opts.limit);
|
|
33569
|
+
}
|
|
33570
|
+
return summaries;
|
|
33571
|
+
}
|
|
33572
|
+
/**
|
|
33573
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
33574
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
33575
|
+
* caller's responsibility.
|
|
33576
|
+
*/
|
|
33577
|
+
async deleteThread(threadId) {
|
|
33578
|
+
const key = bundleKey(threadId);
|
|
33579
|
+
return this.withLock(threadId, async () => {
|
|
33580
|
+
const existed = await this.storage.exists(
|
|
33581
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33582
|
+
key
|
|
33583
|
+
);
|
|
33584
|
+
if (!existed) return false;
|
|
33585
|
+
try {
|
|
33586
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33587
|
+
} catch {
|
|
33588
|
+
return false;
|
|
33589
|
+
}
|
|
33590
|
+
return true;
|
|
33591
|
+
});
|
|
33592
|
+
}
|
|
33593
|
+
/**
|
|
33594
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
33595
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
33596
|
+
*/
|
|
33597
|
+
async pruneExpired(now) {
|
|
33598
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
33599
|
+
const entries = await this.storage.list(
|
|
33600
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33601
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33602
|
+
);
|
|
33603
|
+
let pruned = 0;
|
|
33604
|
+
for (const meta of entries) {
|
|
33605
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33606
|
+
if (threadId === null) continue;
|
|
33607
|
+
pruned += await this.withLock(threadId, async () => {
|
|
33608
|
+
const bundle = await this.loadBundle(threadId);
|
|
33609
|
+
if (!bundle) return 0;
|
|
33610
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
33611
|
+
const dropped = bundle.turns.length - kept.length;
|
|
33612
|
+
if (dropped === 0) return 0;
|
|
33613
|
+
if (kept.length === 0) {
|
|
33614
|
+
await this.storage.delete(
|
|
33615
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33616
|
+
bundleKey(threadId)
|
|
33617
|
+
);
|
|
33618
|
+
} else {
|
|
33619
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
33620
|
+
}
|
|
33621
|
+
return dropped;
|
|
33622
|
+
});
|
|
33623
|
+
}
|
|
33624
|
+
return { pruned };
|
|
33625
|
+
}
|
|
33626
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
33627
|
+
async loadBundle(threadId) {
|
|
33628
|
+
const key = bundleKey(threadId);
|
|
33629
|
+
let raw;
|
|
33630
|
+
try {
|
|
33631
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33632
|
+
} catch {
|
|
33633
|
+
return null;
|
|
33634
|
+
}
|
|
33635
|
+
if (!raw) return null;
|
|
33636
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
33637
|
+
try {
|
|
33638
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
33639
|
+
const aad = stringToBytes(threadId);
|
|
33640
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
33641
|
+
const parsed = JSON.parse(
|
|
33642
|
+
bytesToString(plaintext)
|
|
33643
|
+
);
|
|
33644
|
+
if (parsed.version !== 1) return null;
|
|
33645
|
+
if (parsed.thread_id !== threadId) return null;
|
|
33646
|
+
return parsed;
|
|
33647
|
+
} catch {
|
|
33648
|
+
return null;
|
|
33649
|
+
}
|
|
33650
|
+
}
|
|
33651
|
+
async saveBundle(bundle) {
|
|
33652
|
+
const key = bundleKey(bundle.thread_id);
|
|
33653
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
33654
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
33655
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
33656
|
+
await this.storage.write(
|
|
33657
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33658
|
+
key,
|
|
33659
|
+
stringToBytes(JSON.stringify(envelope))
|
|
33660
|
+
);
|
|
33661
|
+
}
|
|
33662
|
+
/**
|
|
33663
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
33664
|
+
* once the task settles (success or failure). Generic helper so
|
|
33665
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
33666
|
+
*/
|
|
33667
|
+
async withLock(threadId, task) {
|
|
33668
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
33669
|
+
let release;
|
|
33670
|
+
const next = new Promise((resolve8) => {
|
|
33671
|
+
release = resolve8;
|
|
33672
|
+
});
|
|
33673
|
+
const chained = previous.then(() => next);
|
|
33674
|
+
this.locks.set(threadId, chained);
|
|
33675
|
+
try {
|
|
33676
|
+
await previous;
|
|
33677
|
+
return await task();
|
|
33678
|
+
} finally {
|
|
33679
|
+
release();
|
|
33680
|
+
if (this.locks.get(threadId) === chained) {
|
|
33681
|
+
this.locks.delete(threadId);
|
|
33682
|
+
}
|
|
33683
|
+
}
|
|
33684
|
+
}
|
|
33685
|
+
};
|
|
33686
|
+
}
|
|
33687
|
+
});
|
|
33688
|
+
|
|
32522
33689
|
// src/chat/operator-chat-index.ts
|
|
32523
33690
|
var init_operator_chat_index = __esm({
|
|
32524
33691
|
"src/chat/operator-chat-index.ts"() {
|
|
32525
33692
|
init_operator_chat_service();
|
|
32526
33693
|
init_operator_chat_store();
|
|
33694
|
+
init_concierge_memory_store();
|
|
32527
33695
|
init_operator_chat_audit_events();
|
|
32528
33696
|
init_operator_chat_types();
|
|
32529
33697
|
}
|
|
@@ -32537,6 +33705,14 @@ function buildV11Bindings(inputs) {
|
|
|
32537
33705
|
let operatorChatService;
|
|
32538
33706
|
if (inputs.storage && inputs.masterKey) {
|
|
32539
33707
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
33708
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
33709
|
+
storage: inputs.storage,
|
|
33710
|
+
masterKey: inputs.masterKey,
|
|
33711
|
+
fortressId: inputs.fortressId,
|
|
33712
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
33713
|
+
});
|
|
33714
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
33715
|
+
});
|
|
32540
33716
|
operatorChatService = new OperatorChatService({
|
|
32541
33717
|
store: chatStore,
|
|
32542
33718
|
auditLog: inputs.auditLog,
|
|
@@ -32547,7 +33723,8 @@ function buildV11Bindings(inputs) {
|
|
|
32547
33723
|
identityId: inputs.identityId,
|
|
32548
33724
|
registry
|
|
32549
33725
|
}),
|
|
32550
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
33726
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
33727
|
+
conciergeMemory
|
|
32551
33728
|
});
|
|
32552
33729
|
}
|
|
32553
33730
|
const hubService = new HubService({
|
|
@@ -32782,7 +33959,7 @@ var init_defaults = __esm({
|
|
|
32782
33959
|
});
|
|
32783
33960
|
|
|
32784
33961
|
// src/intelligence/policy-store.ts
|
|
32785
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
33962
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO3, IntelligenceConfigStore;
|
|
32786
33963
|
var init_policy_store = __esm({
|
|
32787
33964
|
"src/intelligence/policy-store.ts"() {
|
|
32788
33965
|
init_encryption();
|
|
@@ -32791,13 +33968,13 @@ var init_policy_store = __esm({
|
|
|
32791
33968
|
init_defaults();
|
|
32792
33969
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
32793
33970
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
32794
|
-
|
|
33971
|
+
HKDF_INFO3 = "intelligence-substrate-config";
|
|
32795
33972
|
IntelligenceConfigStore = class {
|
|
32796
33973
|
storage;
|
|
32797
33974
|
encryptionKey;
|
|
32798
33975
|
constructor(storage, masterKey) {
|
|
32799
33976
|
this.storage = storage;
|
|
32800
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33977
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
32801
33978
|
}
|
|
32802
33979
|
/**
|
|
32803
33980
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -34819,7 +35996,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34819
35996
|
);
|
|
34820
35997
|
}
|
|
34821
35998
|
}
|
|
34822
|
-
const
|
|
35999
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
36000
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
36001
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
34823
36002
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
34824
36003
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
34825
36004
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -34828,6 +36007,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34828
36007
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
34829
36008
|
);
|
|
34830
36009
|
}
|
|
36010
|
+
let detailedFailureClass;
|
|
36011
|
+
if (identityFailed) {
|
|
36012
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
36013
|
+
} else if (reputationBundleFailed) {
|
|
36014
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
36015
|
+
} else if (reputationAttestationFailed) {
|
|
36016
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
36017
|
+
} else if (unverifiableFailed) {
|
|
36018
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
36019
|
+
}
|
|
34831
36020
|
return {
|
|
34832
36021
|
version: "1.1",
|
|
34833
36022
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -34847,7 +36036,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34847
36036
|
identity,
|
|
34848
36037
|
audit,
|
|
34849
36038
|
reputation,
|
|
34850
|
-
failure_class:
|
|
36039
|
+
failure_class: detailedFailureClass
|
|
34851
36040
|
};
|
|
34852
36041
|
}
|
|
34853
36042
|
var InvalidExitBundleError, PRIVATE_MATERIAL_KEYS;
|
|
@@ -35815,7 +37004,19 @@ async function runExitCommand(args) {
|
|
|
35815
37004
|
}
|
|
35816
37005
|
const config = await loadConfig();
|
|
35817
37006
|
const ctx = await openExitContext(argv, env);
|
|
35818
|
-
|
|
37007
|
+
let policy;
|
|
37008
|
+
try {
|
|
37009
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
37010
|
+
} catch (policyErr) {
|
|
37011
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
37012
|
+
write(err, `
|
|
37013
|
+
Sanctuary cannot proceed.
|
|
37014
|
+
${policyErr.message}
|
|
37015
|
+
`);
|
|
37016
|
+
return 1;
|
|
37017
|
+
}
|
|
37018
|
+
throw policyErr;
|
|
37019
|
+
}
|
|
35819
37020
|
const result = await exportExitBundle({
|
|
35820
37021
|
bundleDir: outDir,
|
|
35821
37022
|
storage: ctx.storage,
|
|
@@ -36515,7 +37716,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36515
37716
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
36516
37717
|
await profileStore.load();
|
|
36517
37718
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
36518
|
-
|
|
37719
|
+
let policy;
|
|
37720
|
+
try {
|
|
37721
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
37722
|
+
} catch (err) {
|
|
37723
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
37724
|
+
console.error(`
|
|
37725
|
+
Sanctuary cannot start.
|
|
37726
|
+
${err.message}
|
|
37727
|
+
`);
|
|
37728
|
+
process.exit(1);
|
|
37729
|
+
}
|
|
37730
|
+
throw err;
|
|
37731
|
+
}
|
|
36519
37732
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
36520
37733
|
await baseline.load();
|
|
36521
37734
|
let approvalChannel;
|
|
@@ -36613,6 +37826,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36613
37826
|
});
|
|
36614
37827
|
} : void 0;
|
|
36615
37828
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37829
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37830
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37831
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
37832
|
+
storage,
|
|
37833
|
+
masterKey,
|
|
37834
|
+
auditLog,
|
|
37835
|
+
identityId: aggregatorIdentityId,
|
|
37836
|
+
fortressId: fortressIdForAggregator
|
|
37837
|
+
});
|
|
37838
|
+
gate.setApprovalEventCallback((event) => {
|
|
37839
|
+
void approvalAggregator.ingest(event);
|
|
37840
|
+
});
|
|
37841
|
+
if (dashboard) {
|
|
37842
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
37843
|
+
}
|
|
36616
37844
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
36617
37845
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
36618
37846
|
config,
|
|
@@ -36803,6 +38031,7 @@ var init_src = __esm({
|
|
|
36803
38031
|
init_dashboard();
|
|
36804
38032
|
init_webhook();
|
|
36805
38033
|
init_gate();
|
|
38034
|
+
init_approval_aggregator();
|
|
36806
38035
|
init_tools4();
|
|
36807
38036
|
init_router();
|
|
36808
38037
|
init_router();
|
|
@@ -41166,7 +42395,8 @@ var init_agents = __esm({
|
|
|
41166
42395
|
// src/cli/reset-passphrase.ts
|
|
41167
42396
|
var reset_passphrase_exports = {};
|
|
41168
42397
|
__export(reset_passphrase_exports, {
|
|
41169
|
-
runResetPassphraseCommand: () => runResetPassphraseCommand
|
|
42398
|
+
runResetPassphraseCommand: () => runResetPassphraseCommand,
|
|
42399
|
+
zeroizeBuffers: () => zeroizeBuffers
|
|
41170
42400
|
});
|
|
41171
42401
|
async function runResetPassphraseCommand(args) {
|
|
41172
42402
|
const out = args.out ?? process.stdout;
|
|
@@ -41194,34 +42424,42 @@ Then re-run this command.
|
|
|
41194
42424
|
return 1;
|
|
41195
42425
|
}
|
|
41196
42426
|
const lines = new LineReader(stdin);
|
|
42427
|
+
let code = 1;
|
|
42428
|
+
let nukeSucceeded = false;
|
|
41197
42429
|
try {
|
|
41198
42430
|
const availability = await surveyAvailableModes(storagePath);
|
|
41199
42431
|
const mode = parsed.mode ?? await selectMode(lines, out, err, availability);
|
|
41200
42432
|
if (!mode) {
|
|
41201
42433
|
err.write("Aborted: no recovery mode selected.\n");
|
|
41202
|
-
|
|
41203
|
-
}
|
|
41204
|
-
|
|
41205
|
-
|
|
41206
|
-
|
|
41207
|
-
|
|
41208
|
-
|
|
42434
|
+
code = 1;
|
|
42435
|
+
} else if (mode === "shares") {
|
|
42436
|
+
code = await runSharesPath(out, err, availability);
|
|
42437
|
+
} else if (mode === "guardian") {
|
|
42438
|
+
code = await runGuardianPath(out, err, availability);
|
|
42439
|
+
} else {
|
|
42440
|
+
code = await runNukePath({
|
|
42441
|
+
out,
|
|
42442
|
+
err,
|
|
42443
|
+
lines,
|
|
42444
|
+
storagePath,
|
|
42445
|
+
home,
|
|
42446
|
+
plat,
|
|
42447
|
+
exec: args.exec ?? defaultExec2
|
|
42448
|
+
});
|
|
42449
|
+
nukeSucceeded = mode === "nuke" && code === 0;
|
|
41209
42450
|
}
|
|
41210
|
-
return await runNukePath({
|
|
41211
|
-
out,
|
|
41212
|
-
err,
|
|
41213
|
-
lines,
|
|
41214
|
-
storagePath,
|
|
41215
|
-
home,
|
|
41216
|
-
plat,
|
|
41217
|
-
exec: args.exec ?? defaultExec2
|
|
41218
|
-
});
|
|
41219
42451
|
} finally {
|
|
42452
|
+
zeroizeBuffers(args.keyMaterialToZeroize);
|
|
41220
42453
|
lines.close();
|
|
41221
42454
|
}
|
|
42455
|
+
if (parsed.exitOnCompletion && nukeSucceeded) {
|
|
42456
|
+
const doExit = args.exitProcess ?? ((c) => process.exit(c));
|
|
42457
|
+
doExit(0);
|
|
42458
|
+
}
|
|
42459
|
+
return code;
|
|
41222
42460
|
}
|
|
41223
42461
|
function parseArgs2(argv) {
|
|
41224
|
-
const out = { help: false };
|
|
42462
|
+
const out = { exitOnCompletion: false, help: false };
|
|
41225
42463
|
for (let i = 0; i < argv.length; i++) {
|
|
41226
42464
|
const a = argv[i];
|
|
41227
42465
|
if (a === "--help" || a === "-h") {
|
|
@@ -41238,6 +42476,8 @@ function parseArgs2(argv) {
|
|
|
41238
42476
|
out.storage = argv[++i];
|
|
41239
42477
|
} else if (a === "--fortress" && argv[i + 1]) {
|
|
41240
42478
|
out.fortress = argv[++i];
|
|
42479
|
+
} else if (a === "--exit-on-completion") {
|
|
42480
|
+
out.exitOnCompletion = true;
|
|
41241
42481
|
} else if (a && a.startsWith("--")) {
|
|
41242
42482
|
throw new Error(`Unknown flag: ${a}`);
|
|
41243
42483
|
}
|
|
@@ -41270,6 +42510,16 @@ Options:
|
|
|
41270
42510
|
--fortress <path> Override the fortress storage path.
|
|
41271
42511
|
Consistent with "sanctuary wrap --fortress".
|
|
41272
42512
|
--storage <path> Alias for --fortress.
|
|
42513
|
+
--exit-on-completion After a successful nuke, call process.exit(0)
|
|
42514
|
+
immediately so the post-wipe heap is reaped
|
|
42515
|
+
by the OS without re-entering the shell. Use
|
|
42516
|
+
on extreme-threat-model deployments where an
|
|
42517
|
+
attacker-on-host with heap-dump access could
|
|
42518
|
+
recover residual passphrase or key bytes
|
|
42519
|
+
between the wipe and the next operator
|
|
42520
|
+
command. JS strings cannot be explicitly
|
|
42521
|
+
zeroed; this flag is the supported way to
|
|
42522
|
+
bound the heap-dump window.
|
|
41273
42523
|
--help, -h Show this help.
|
|
41274
42524
|
|
|
41275
42525
|
Without --mode, the command surveys which paths are operationally available
|
|
@@ -41539,6 +42789,16 @@ async function prompt(lines, err, question) {
|
|
|
41539
42789
|
err.write(question);
|
|
41540
42790
|
return await lines.next();
|
|
41541
42791
|
}
|
|
42792
|
+
function zeroizeBuffers(buffers) {
|
|
42793
|
+
if (!buffers) return;
|
|
42794
|
+
for (const b of buffers) {
|
|
42795
|
+
if (!b) continue;
|
|
42796
|
+
try {
|
|
42797
|
+
b.fill(0);
|
|
42798
|
+
} catch {
|
|
42799
|
+
}
|
|
42800
|
+
}
|
|
42801
|
+
}
|
|
41542
42802
|
async function defaultExec2(cmd, args) {
|
|
41543
42803
|
return await new Promise((resolve8, reject) => {
|
|
41544
42804
|
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -42219,7 +43479,19 @@ Refusing to start the dashboard while the reset-history marker is unreadable.`
|
|
|
42219
43479
|
}
|
|
42220
43480
|
throw err;
|
|
42221
43481
|
}
|
|
42222
|
-
|
|
43482
|
+
let policy;
|
|
43483
|
+
try {
|
|
43484
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
43485
|
+
} catch (err) {
|
|
43486
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
43487
|
+
console.error(`
|
|
43488
|
+
Sanctuary cannot start.
|
|
43489
|
+
${err.message}
|
|
43490
|
+
`);
|
|
43491
|
+
process.exit(1);
|
|
43492
|
+
}
|
|
43493
|
+
throw err;
|
|
43494
|
+
}
|
|
42223
43495
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
42224
43496
|
await baseline.load();
|
|
42225
43497
|
const dashboardPort = options.port ?? config.dashboard.port;
|