@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.cjs
CHANGED
|
@@ -4576,21 +4576,39 @@ approval_channel:
|
|
|
4576
4576
|
}
|
|
4577
4577
|
async function loadPrincipalPolicy(storagePath) {
|
|
4578
4578
|
const policyPath = path.join(storagePath, "principal-policy.yaml");
|
|
4579
|
+
let content;
|
|
4580
|
+
try {
|
|
4581
|
+
content = await promises.readFile(policyPath, "utf-8");
|
|
4582
|
+
} catch (err) {
|
|
4583
|
+
const code = err?.code;
|
|
4584
|
+
if (code === "ENOENT") {
|
|
4585
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4586
|
+
try {
|
|
4587
|
+
await promises.writeFile(policyPath, defaultYaml, "utf-8");
|
|
4588
|
+
await promises.chmod(policyPath, 384);
|
|
4589
|
+
} catch (writeErr) {
|
|
4590
|
+
console.warn(
|
|
4591
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4592
|
+
);
|
|
4593
|
+
}
|
|
4594
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4595
|
+
}
|
|
4596
|
+
throw new MalformedPrincipalPolicyError(
|
|
4597
|
+
policyPath,
|
|
4598
|
+
`read failed: ${err.message}`
|
|
4599
|
+
);
|
|
4600
|
+
}
|
|
4579
4601
|
try {
|
|
4580
|
-
const content = await promises.readFile(policyPath, "utf-8");
|
|
4581
4602
|
const policy = parsePolicy(content);
|
|
4582
4603
|
return Object.freeze(policy);
|
|
4583
|
-
} catch {
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
} catch {
|
|
4589
|
-
}
|
|
4590
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4604
|
+
} catch (parseErr) {
|
|
4605
|
+
throw new MalformedPrincipalPolicyError(
|
|
4606
|
+
policyPath,
|
|
4607
|
+
parseErr.message
|
|
4608
|
+
);
|
|
4591
4609
|
}
|
|
4592
4610
|
}
|
|
4593
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY;
|
|
4611
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4594
4612
|
var init_loader = __esm({
|
|
4595
4613
|
"src/principal-policy/loader.ts"() {
|
|
4596
4614
|
DEFAULT_TIER2 = {
|
|
@@ -4723,6 +4741,20 @@ var init_loader = __esm({
|
|
|
4723
4741
|
],
|
|
4724
4742
|
approval_channel: DEFAULT_CHANNEL
|
|
4725
4743
|
};
|
|
4744
|
+
MalformedPrincipalPolicyError = class extends Error {
|
|
4745
|
+
constructor(policyPath, reason) {
|
|
4746
|
+
super(
|
|
4747
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4748
|
+
Reason: ${reason}
|
|
4749
|
+
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.`
|
|
4750
|
+
);
|
|
4751
|
+
this.policyPath = policyPath;
|
|
4752
|
+
this.reason = reason;
|
|
4753
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4754
|
+
}
|
|
4755
|
+
policyPath;
|
|
4756
|
+
reason;
|
|
4757
|
+
};
|
|
4726
4758
|
}
|
|
4727
4759
|
});
|
|
4728
4760
|
|
|
@@ -4942,7 +4974,7 @@ function deepSortKeys(obj) {
|
|
|
4942
4974
|
return sorted;
|
|
4943
4975
|
}
|
|
4944
4976
|
function canonicalizeForSigning(body) {
|
|
4945
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4977
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4946
4978
|
}
|
|
4947
4979
|
var init_types = __esm({
|
|
4948
4980
|
"src/shr/types.ts"() {
|
|
@@ -12441,7 +12473,7 @@ var init_auth_middleware = __esm({
|
|
|
12441
12473
|
});
|
|
12442
12474
|
|
|
12443
12475
|
// src/hub/constants.ts
|
|
12444
|
-
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;
|
|
12476
|
+
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;
|
|
12445
12477
|
var init_constants3 = __esm({
|
|
12446
12478
|
"src/hub/constants.ts"() {
|
|
12447
12479
|
HUB_API_PREFIX = "/api/hub";
|
|
@@ -12469,6 +12501,16 @@ var init_constants3 = __esm({
|
|
|
12469
12501
|
*/
|
|
12470
12502
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
12471
12503
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
12504
|
+
/**
|
|
12505
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
12506
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
12507
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
12508
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
12509
|
+
* v1.3 conversational sovereignty depth.
|
|
12510
|
+
*/
|
|
12511
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
12512
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12513
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12472
12514
|
/**
|
|
12473
12515
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
12474
12516
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -12492,6 +12534,10 @@ var init_constants3 = __esm({
|
|
|
12492
12534
|
];
|
|
12493
12535
|
HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
12494
12536
|
HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
12537
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
12538
|
+
HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
12539
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
12540
|
+
HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
12495
12541
|
HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
12496
12542
|
HUB_INBOX_MAX_LIMIT = 500;
|
|
12497
12543
|
HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -12674,6 +12720,23 @@ function checkChatMessage(value) {
|
|
|
12674
12720
|
}
|
|
12675
12721
|
return trimmed;
|
|
12676
12722
|
}
|
|
12723
|
+
function matchConciergeThreadRoute(path) {
|
|
12724
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
12725
|
+
if (!path.startsWith(prefix)) return null;
|
|
12726
|
+
const rest = path.slice(prefix.length);
|
|
12727
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
12728
|
+
const decoded = decodeURIComponent(rest);
|
|
12729
|
+
if (decoded.length === 0) return null;
|
|
12730
|
+
return { threadId: decoded };
|
|
12731
|
+
}
|
|
12732
|
+
function parseSince(raw) {
|
|
12733
|
+
if (raw === null || raw === "") return void 0;
|
|
12734
|
+
const parsed = Number.parseInt(raw, 10);
|
|
12735
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
12736
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
12737
|
+
}
|
|
12738
|
+
return parsed;
|
|
12739
|
+
}
|
|
12677
12740
|
function matchInboxRoute(path) {
|
|
12678
12741
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
12679
12742
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12857,6 +12920,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12857
12920
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12858
12921
|
return true;
|
|
12859
12922
|
}
|
|
12923
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12924
|
+
const limit = parseLimit(
|
|
12925
|
+
url.searchParams.get("limit"),
|
|
12926
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12927
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12928
|
+
);
|
|
12929
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12930
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12931
|
+
return true;
|
|
12932
|
+
}
|
|
12933
|
+
{
|
|
12934
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12935
|
+
if (threadMatch) {
|
|
12936
|
+
if (method === "GET") {
|
|
12937
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12938
|
+
const limit = parseLimit(
|
|
12939
|
+
url.searchParams.get("limit"),
|
|
12940
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12941
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12942
|
+
);
|
|
12943
|
+
const readOpts = { limit };
|
|
12944
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12945
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12946
|
+
threadMatch.threadId,
|
|
12947
|
+
readOpts
|
|
12948
|
+
);
|
|
12949
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12950
|
+
return true;
|
|
12951
|
+
}
|
|
12952
|
+
if (method === "DELETE") {
|
|
12953
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12954
|
+
threadMatch.threadId
|
|
12955
|
+
);
|
|
12956
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12957
|
+
ok: removed,
|
|
12958
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12959
|
+
});
|
|
12960
|
+
return true;
|
|
12961
|
+
}
|
|
12962
|
+
}
|
|
12963
|
+
}
|
|
12860
12964
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12861
12965
|
return true;
|
|
12862
12966
|
} catch (err) {
|
|
@@ -16979,6 +17083,168 @@ var init_dispatch = __esm({
|
|
|
16979
17083
|
init_intelligence_api_router();
|
|
16980
17084
|
}
|
|
16981
17085
|
});
|
|
17086
|
+
|
|
17087
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
17088
|
+
function writeJSON4(res, status, payload) {
|
|
17089
|
+
res.writeHead(status, {
|
|
17090
|
+
"Content-Type": "application/json",
|
|
17091
|
+
"Cache-Control": "no-store"
|
|
17092
|
+
});
|
|
17093
|
+
res.end(JSON.stringify(payload));
|
|
17094
|
+
}
|
|
17095
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
17096
|
+
if (raw === null || raw === "") return defaultValue;
|
|
17097
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17098
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
17099
|
+
return defaultValue;
|
|
17100
|
+
}
|
|
17101
|
+
return Math.min(parsed, max);
|
|
17102
|
+
}
|
|
17103
|
+
function isStatusFilter(value) {
|
|
17104
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
17105
|
+
}
|
|
17106
|
+
function matchEntryRoute(path) {
|
|
17107
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
17108
|
+
if (!path.startsWith(prefix)) return null;
|
|
17109
|
+
const rest = path.slice(prefix.length);
|
|
17110
|
+
if (rest.length === 0) return null;
|
|
17111
|
+
const slash = rest.indexOf("/");
|
|
17112
|
+
if (slash === -1) {
|
|
17113
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
17114
|
+
}
|
|
17115
|
+
return {
|
|
17116
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
17117
|
+
action: rest.slice(slash + 1)
|
|
17118
|
+
};
|
|
17119
|
+
}
|
|
17120
|
+
async function handleStream2(deps, res) {
|
|
17121
|
+
res.writeHead(200, {
|
|
17122
|
+
"Content-Type": "text/event-stream",
|
|
17123
|
+
"Cache-Control": "no-cache, no-transform",
|
|
17124
|
+
Connection: "keep-alive",
|
|
17125
|
+
"X-Accel-Buffering": "no"
|
|
17126
|
+
});
|
|
17127
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
17128
|
+
res.write(
|
|
17129
|
+
`event: approval_inbox_snapshot
|
|
17130
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
17131
|
+
|
|
17132
|
+
`
|
|
17133
|
+
);
|
|
17134
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
17135
|
+
try {
|
|
17136
|
+
res.write(
|
|
17137
|
+
`event: approval_inbox_${event.type}
|
|
17138
|
+
data: ${JSON.stringify(event.entry)}
|
|
17139
|
+
|
|
17140
|
+
`
|
|
17141
|
+
);
|
|
17142
|
+
} catch {
|
|
17143
|
+
}
|
|
17144
|
+
});
|
|
17145
|
+
const keepAlive = setInterval(() => {
|
|
17146
|
+
try {
|
|
17147
|
+
res.write(": keepalive\n\n");
|
|
17148
|
+
} catch {
|
|
17149
|
+
}
|
|
17150
|
+
}, 25e3);
|
|
17151
|
+
const cleanup = () => {
|
|
17152
|
+
clearInterval(keepAlive);
|
|
17153
|
+
unsubscribe();
|
|
17154
|
+
};
|
|
17155
|
+
res.on("close", cleanup);
|
|
17156
|
+
res.on("error", cleanup);
|
|
17157
|
+
}
|
|
17158
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
17159
|
+
const host = req.headers.host || "localhost";
|
|
17160
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
17161
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
17162
|
+
const path = url.pathname;
|
|
17163
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
17164
|
+
return false;
|
|
17165
|
+
}
|
|
17166
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
17167
|
+
if (!checkAuth(req, res, url)) return true;
|
|
17168
|
+
try {
|
|
17169
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
17170
|
+
await handleStream2(deps, res);
|
|
17171
|
+
return true;
|
|
17172
|
+
}
|
|
17173
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
17174
|
+
const limit = parseLimit2(
|
|
17175
|
+
url.searchParams.get("limit"),
|
|
17176
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
17177
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
17178
|
+
);
|
|
17179
|
+
const statusRaw = url.searchParams.get("status");
|
|
17180
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
17181
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
17182
|
+
const entries = await deps.aggregator.list({
|
|
17183
|
+
status,
|
|
17184
|
+
limit,
|
|
17185
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
17186
|
+
});
|
|
17187
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
17188
|
+
return true;
|
|
17189
|
+
}
|
|
17190
|
+
const entryMatch = matchEntryRoute(path);
|
|
17191
|
+
if (entryMatch === null) {
|
|
17192
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17193
|
+
return true;
|
|
17194
|
+
}
|
|
17195
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
17196
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
17197
|
+
const entry = entries.find(
|
|
17198
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
17199
|
+
);
|
|
17200
|
+
if (!entry) {
|
|
17201
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17202
|
+
return true;
|
|
17203
|
+
}
|
|
17204
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
17205
|
+
entryMatch.aggregatorId
|
|
17206
|
+
);
|
|
17207
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
17208
|
+
return true;
|
|
17209
|
+
}
|
|
17210
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
17211
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
17212
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17213
|
+
try {
|
|
17214
|
+
const entry = await deps.aggregator.resolve(
|
|
17215
|
+
entryMatch.aggregatorId,
|
|
17216
|
+
decision,
|
|
17217
|
+
operatorId
|
|
17218
|
+
);
|
|
17219
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
17220
|
+
} catch (err) {
|
|
17221
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17222
|
+
if (msg === "approval-aggregator: not_found") {
|
|
17223
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17224
|
+
} else {
|
|
17225
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17226
|
+
}
|
|
17227
|
+
}
|
|
17228
|
+
return true;
|
|
17229
|
+
}
|
|
17230
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17231
|
+
return true;
|
|
17232
|
+
} catch (err) {
|
|
17233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17234
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17235
|
+
return true;
|
|
17236
|
+
}
|
|
17237
|
+
}
|
|
17238
|
+
var APPROVAL_INBOX_API_PREFIX, APPROVAL_INBOX_OPERATOR_DEFAULT, APPROVAL_INBOX_DEFAULT_LIMIT, APPROVAL_INBOX_MAX_LIMIT;
|
|
17239
|
+
var init_approval_aggregator_routes = __esm({
|
|
17240
|
+
"src/principal-policy/approval-aggregator-routes.ts"() {
|
|
17241
|
+
init_auth_middleware();
|
|
17242
|
+
APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
17243
|
+
APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
17244
|
+
APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
17245
|
+
APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
17246
|
+
}
|
|
17247
|
+
});
|
|
16982
17248
|
function isDashboardViewRoute(method, path) {
|
|
16983
17249
|
if (method !== "GET") return false;
|
|
16984
17250
|
return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
|
|
@@ -16992,6 +17258,7 @@ var init_dashboard = __esm({
|
|
|
16992
17258
|
init_fortress_view();
|
|
16993
17259
|
init_system_prompt_generator();
|
|
16994
17260
|
init_dispatch();
|
|
17261
|
+
init_approval_aggregator_routes();
|
|
16995
17262
|
SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16996
17263
|
SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
16997
17264
|
MAX_SESSIONS = 1e3;
|
|
@@ -17051,6 +17318,14 @@ var init_dashboard = __esm({
|
|
|
17051
17318
|
* regardless. Default route flip is deferred to v1.2.
|
|
17052
17319
|
*/
|
|
17053
17320
|
v11Bindings = null;
|
|
17321
|
+
/**
|
|
17322
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
17323
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
17324
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
17325
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
17326
|
+
* the operator-facing query / decision surface.
|
|
17327
|
+
*/
|
|
17328
|
+
approvalAggregator = null;
|
|
17054
17329
|
constructor(config) {
|
|
17055
17330
|
this.config = config;
|
|
17056
17331
|
this.authToken = config.auth_token;
|
|
@@ -17101,6 +17376,34 @@ var init_dashboard = __esm({
|
|
|
17101
17376
|
setV11Bindings(bindings) {
|
|
17102
17377
|
this.v11Bindings = bindings;
|
|
17103
17378
|
}
|
|
17379
|
+
/**
|
|
17380
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
17381
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
17382
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
17383
|
+
* tests + during shutdown).
|
|
17384
|
+
*/
|
|
17385
|
+
setApprovalAggregator(aggregator) {
|
|
17386
|
+
this.approvalAggregator = aggregator;
|
|
17387
|
+
}
|
|
17388
|
+
/**
|
|
17389
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
17390
|
+
* before the legacy approval route table. Returns true when served.
|
|
17391
|
+
*/
|
|
17392
|
+
async dispatchApprovalInbox(req, res) {
|
|
17393
|
+
if (!this.approvalAggregator) return false;
|
|
17394
|
+
return handleApprovalInboxRoute(
|
|
17395
|
+
{
|
|
17396
|
+
authConfig: {
|
|
17397
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
17398
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
17399
|
+
},
|
|
17400
|
+
aggregator: this.approvalAggregator,
|
|
17401
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
17402
|
+
},
|
|
17403
|
+
req,
|
|
17404
|
+
res
|
|
17405
|
+
);
|
|
17406
|
+
}
|
|
17104
17407
|
/**
|
|
17105
17408
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
17106
17409
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -17476,6 +17779,18 @@ var init_dashboard = __esm({
|
|
|
17476
17779
|
res.end();
|
|
17477
17780
|
return;
|
|
17478
17781
|
}
|
|
17782
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
17783
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
17784
|
+
if (handled) return;
|
|
17785
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
17786
|
+
}).catch(() => {
|
|
17787
|
+
if (!res.headersSent) {
|
|
17788
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
17789
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
17790
|
+
}
|
|
17791
|
+
});
|
|
17792
|
+
return;
|
|
17793
|
+
}
|
|
17479
17794
|
if (this.v11Bindings) {
|
|
17480
17795
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
17481
17796
|
if (handled) return;
|
|
@@ -19519,14 +19834,25 @@ var init_gate = __esm({
|
|
|
19519
19834
|
auditLog;
|
|
19520
19835
|
injectionDetector;
|
|
19521
19836
|
onInjectionAlert;
|
|
19837
|
+
onApprovalEvent;
|
|
19522
19838
|
proxyTierResolver;
|
|
19523
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
19839
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
19524
19840
|
this.policy = policy;
|
|
19525
19841
|
this.baseline = baseline;
|
|
19526
19842
|
this.channel = channel;
|
|
19527
19843
|
this.auditLog = auditLog;
|
|
19528
19844
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
19529
19845
|
this.onInjectionAlert = onInjectionAlert;
|
|
19846
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
19847
|
+
}
|
|
19848
|
+
/**
|
|
19849
|
+
* Set the approval-event callback after construction. Used by the
|
|
19850
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
19851
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
19852
|
+
* constructor so existing call sites continue to work unchanged.
|
|
19853
|
+
*/
|
|
19854
|
+
setApprovalEventCallback(cb) {
|
|
19855
|
+
this.onApprovalEvent = cb;
|
|
19530
19856
|
}
|
|
19531
19857
|
/**
|
|
19532
19858
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -19760,21 +20086,105 @@ var init_gate = __esm({
|
|
|
19760
20086
|
}
|
|
19761
20087
|
/**
|
|
19762
20088
|
* Request approval from the human principal.
|
|
20089
|
+
*
|
|
20090
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
20091
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
20092
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
20093
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
20094
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
20095
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
19763
20096
|
*/
|
|
19764
20097
|
async requestApproval(operation, tier, reason, context) {
|
|
20098
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19765
20099
|
const request = {
|
|
19766
20100
|
operation,
|
|
19767
20101
|
tier,
|
|
19768
20102
|
reason,
|
|
19769
20103
|
context,
|
|
19770
|
-
timestamp:
|
|
20104
|
+
timestamp: requestTimestamp
|
|
19771
20105
|
};
|
|
19772
|
-
const
|
|
20106
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
20107
|
+
if (this.onApprovalEvent) {
|
|
20108
|
+
try {
|
|
20109
|
+
this.onApprovalEvent({
|
|
20110
|
+
phase: "requested",
|
|
20111
|
+
operation,
|
|
20112
|
+
tier,
|
|
20113
|
+
reason,
|
|
20114
|
+
context,
|
|
20115
|
+
request_timestamp: requestTimestamp,
|
|
20116
|
+
correlation_id: correlationId
|
|
20117
|
+
});
|
|
20118
|
+
} catch {
|
|
20119
|
+
}
|
|
20120
|
+
}
|
|
20121
|
+
let response;
|
|
20122
|
+
try {
|
|
20123
|
+
response = await this.channel.requestApproval(request);
|
|
20124
|
+
} catch (err) {
|
|
20125
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
20126
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20127
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
20128
|
+
tier,
|
|
20129
|
+
reason,
|
|
20130
|
+
decided_by: "channel_failure",
|
|
20131
|
+
channel_error: errMessage
|
|
20132
|
+
});
|
|
20133
|
+
if (this.onApprovalEvent) {
|
|
20134
|
+
try {
|
|
20135
|
+
this.onApprovalEvent({
|
|
20136
|
+
phase: "resolved",
|
|
20137
|
+
operation,
|
|
20138
|
+
tier,
|
|
20139
|
+
reason,
|
|
20140
|
+
context,
|
|
20141
|
+
request_timestamp: requestTimestamp,
|
|
20142
|
+
resolution: {
|
|
20143
|
+
decision: "deny",
|
|
20144
|
+
decided_at: decidedAt,
|
|
20145
|
+
decided_by: "channel_failure"
|
|
20146
|
+
},
|
|
20147
|
+
correlation_id: correlationId
|
|
20148
|
+
});
|
|
20149
|
+
} catch {
|
|
20150
|
+
}
|
|
20151
|
+
}
|
|
20152
|
+
return {
|
|
20153
|
+
allowed: false,
|
|
20154
|
+
tier,
|
|
20155
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
20156
|
+
approval_required: true,
|
|
20157
|
+
approval_response: {
|
|
20158
|
+
decision: "deny",
|
|
20159
|
+
decided_at: decidedAt,
|
|
20160
|
+
decided_by: "channel_failure"
|
|
20161
|
+
}
|
|
20162
|
+
};
|
|
20163
|
+
}
|
|
19773
20164
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
19774
20165
|
tier,
|
|
19775
20166
|
reason,
|
|
19776
20167
|
decided_by: response.decided_by
|
|
19777
20168
|
});
|
|
20169
|
+
if (this.onApprovalEvent) {
|
|
20170
|
+
try {
|
|
20171
|
+
this.onApprovalEvent({
|
|
20172
|
+
phase: "resolved",
|
|
20173
|
+
operation,
|
|
20174
|
+
tier,
|
|
20175
|
+
reason,
|
|
20176
|
+
context,
|
|
20177
|
+
request_timestamp: requestTimestamp,
|
|
20178
|
+
resolution: {
|
|
20179
|
+
decision: response.decision,
|
|
20180
|
+
decided_at: response.decided_at,
|
|
20181
|
+
decided_by: response.decided_by
|
|
20182
|
+
},
|
|
20183
|
+
correlation_id: correlationId
|
|
20184
|
+
});
|
|
20185
|
+
} catch {
|
|
20186
|
+
}
|
|
20187
|
+
}
|
|
19778
20188
|
return {
|
|
19779
20189
|
allowed: response.decision === "approve",
|
|
19780
20190
|
tier,
|
|
@@ -19809,6 +20219,356 @@ var init_gate = __esm({
|
|
|
19809
20219
|
};
|
|
19810
20220
|
}
|
|
19811
20221
|
});
|
|
20222
|
+
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;
|
|
20223
|
+
var init_approval_aggregator = __esm({
|
|
20224
|
+
"src/principal-policy/approval-aggregator.ts"() {
|
|
20225
|
+
init_encryption();
|
|
20226
|
+
init_key_derivation();
|
|
20227
|
+
init_encoding();
|
|
20228
|
+
APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
20229
|
+
APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
20230
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
20231
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
20232
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
20233
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
20234
|
+
};
|
|
20235
|
+
DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
20236
|
+
DEFAULT_MAX_LIST_LIMIT = 200;
|
|
20237
|
+
DEFAULT_LIST_PAGE_SIZE = 50;
|
|
20238
|
+
ApprovalAggregator = class {
|
|
20239
|
+
storage;
|
|
20240
|
+
encryptionKey;
|
|
20241
|
+
auditLog;
|
|
20242
|
+
identityId;
|
|
20243
|
+
fortressId;
|
|
20244
|
+
pendingTtlMs;
|
|
20245
|
+
maxListLimit;
|
|
20246
|
+
now;
|
|
20247
|
+
resolveSourceContext;
|
|
20248
|
+
resolveHubInboxItemId;
|
|
20249
|
+
/** Cached entries by `aggregator_id`. */
|
|
20250
|
+
entries = /* @__PURE__ */ new Map();
|
|
20251
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
20252
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
20253
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
20254
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
20255
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
20256
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
20257
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
20258
|
+
hydrated = false;
|
|
20259
|
+
/** Active SSE listeners. */
|
|
20260
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20261
|
+
constructor(deps) {
|
|
20262
|
+
this.storage = deps.storage;
|
|
20263
|
+
this.encryptionKey = derivePurposeKey(
|
|
20264
|
+
deps.masterKey,
|
|
20265
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
20266
|
+
);
|
|
20267
|
+
this.auditLog = deps.auditLog;
|
|
20268
|
+
this.identityId = deps.identityId;
|
|
20269
|
+
this.fortressId = deps.fortressId;
|
|
20270
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
20271
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
20272
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
20273
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
20274
|
+
source_harness: this.fortressId,
|
|
20275
|
+
source_agent_id: this.fortressId
|
|
20276
|
+
}));
|
|
20277
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
20278
|
+
}
|
|
20279
|
+
/**
|
|
20280
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
20281
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
20282
|
+
*/
|
|
20283
|
+
onEvent(listener) {
|
|
20284
|
+
this.listeners.add(listener);
|
|
20285
|
+
return () => this.listeners.delete(listener);
|
|
20286
|
+
}
|
|
20287
|
+
/**
|
|
20288
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
20289
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
20290
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
20291
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
20292
|
+
* a record for).
|
|
20293
|
+
*/
|
|
20294
|
+
async ingest(event) {
|
|
20295
|
+
await this.hydrate();
|
|
20296
|
+
if (event.phase === "requested") {
|
|
20297
|
+
return this.ingestRequested(event);
|
|
20298
|
+
}
|
|
20299
|
+
if (event.phase === "resolved") {
|
|
20300
|
+
return this.ingestResolved(event);
|
|
20301
|
+
}
|
|
20302
|
+
return null;
|
|
20303
|
+
}
|
|
20304
|
+
/**
|
|
20305
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
20306
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
20307
|
+
* snapshot is returned.
|
|
20308
|
+
*/
|
|
20309
|
+
async list(opts) {
|
|
20310
|
+
await this.hydrate();
|
|
20311
|
+
await this.expireStale();
|
|
20312
|
+
const limit = Math.min(
|
|
20313
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
20314
|
+
this.maxListLimit
|
|
20315
|
+
);
|
|
20316
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
20317
|
+
const matching = [];
|
|
20318
|
+
for (const entry of this.entries.values()) {
|
|
20319
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
20320
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
20321
|
+
matching.push(entry);
|
|
20322
|
+
}
|
|
20323
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
20324
|
+
return matching.slice(0, limit);
|
|
20325
|
+
}
|
|
20326
|
+
/**
|
|
20327
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
20328
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
20329
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
20330
|
+
*/
|
|
20331
|
+
async getFullPayload(aggregatorId) {
|
|
20332
|
+
await this.hydrate();
|
|
20333
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
20334
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
20335
|
+
}
|
|
20336
|
+
/**
|
|
20337
|
+
* Resolve an entry. Used by both:
|
|
20338
|
+
* 1. The gate wire-up on channel-decision return.
|
|
20339
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
20340
|
+
*
|
|
20341
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
20342
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
20343
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
20344
|
+
* routes return 404.
|
|
20345
|
+
*/
|
|
20346
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
20347
|
+
await this.hydrate();
|
|
20348
|
+
const entry = this.entries.get(aggregatorId);
|
|
20349
|
+
if (!entry) {
|
|
20350
|
+
throw new Error("approval-aggregator: not_found");
|
|
20351
|
+
}
|
|
20352
|
+
if (entry.status !== "pending") {
|
|
20353
|
+
return entry;
|
|
20354
|
+
}
|
|
20355
|
+
entry.status = decision;
|
|
20356
|
+
entry.resolved_at = this.now().toISOString();
|
|
20357
|
+
entry.resolved_by = operatorId;
|
|
20358
|
+
await this.persist(entry);
|
|
20359
|
+
this.auditLog.append(
|
|
20360
|
+
"l2",
|
|
20361
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20362
|
+
this.identityId,
|
|
20363
|
+
{
|
|
20364
|
+
aggregator_id: entry.aggregator_id,
|
|
20365
|
+
source_harness: entry.source_harness,
|
|
20366
|
+
source_agent_id: entry.source_agent_id,
|
|
20367
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20368
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20369
|
+
decision,
|
|
20370
|
+
decided_by: operatorId,
|
|
20371
|
+
decided_at: entry.resolved_at
|
|
20372
|
+
}
|
|
20373
|
+
);
|
|
20374
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20375
|
+
return entry;
|
|
20376
|
+
}
|
|
20377
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
20378
|
+
async ingestRequested(event) {
|
|
20379
|
+
const ctx = this.resolveSourceContext(event);
|
|
20380
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
20381
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
20382
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
20383
|
+
if (existing) {
|
|
20384
|
+
const existingEntry = this.entries.get(existing);
|
|
20385
|
+
if (existingEntry) {
|
|
20386
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
20387
|
+
this.auditLog.append(
|
|
20388
|
+
"l2",
|
|
20389
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
20390
|
+
this.identityId,
|
|
20391
|
+
{
|
|
20392
|
+
aggregator_id: existing,
|
|
20393
|
+
source_harness: ctx.source_harness,
|
|
20394
|
+
source_agent_id: ctx.source_agent_id,
|
|
20395
|
+
audit_log_entry_id: auditId,
|
|
20396
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20397
|
+
correlation_id: event.correlation_id
|
|
20398
|
+
}
|
|
20399
|
+
);
|
|
20400
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
20401
|
+
return null;
|
|
20402
|
+
}
|
|
20403
|
+
}
|
|
20404
|
+
const id = crypto.randomUUID();
|
|
20405
|
+
const now = this.now();
|
|
20406
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
20407
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
20408
|
+
const entry = {
|
|
20409
|
+
aggregator_id: id,
|
|
20410
|
+
source_harness: ctx.source_harness,
|
|
20411
|
+
source_agent_id: ctx.source_agent_id,
|
|
20412
|
+
audit_log_entry_id: auditId,
|
|
20413
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20414
|
+
action_summary: this.deriveActionSummary(event),
|
|
20415
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
20416
|
+
status: "pending",
|
|
20417
|
+
created_at: now.toISOString(),
|
|
20418
|
+
expires_at: expires.toISOString(),
|
|
20419
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20420
|
+
};
|
|
20421
|
+
this.entries.set(id, entry);
|
|
20422
|
+
this.dedupIndex.set(dedupKey, id);
|
|
20423
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
20424
|
+
this.fullPayloads.set(id, event.context);
|
|
20425
|
+
await this.persist(entry);
|
|
20426
|
+
this.auditLog.append(
|
|
20427
|
+
"l2",
|
|
20428
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
20429
|
+
this.identityId,
|
|
20430
|
+
{
|
|
20431
|
+
aggregator_id: id,
|
|
20432
|
+
source_harness: ctx.source_harness,
|
|
20433
|
+
source_agent_id: ctx.source_agent_id,
|
|
20434
|
+
audit_log_entry_id: auditId,
|
|
20435
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20436
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20437
|
+
}
|
|
20438
|
+
);
|
|
20439
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
20440
|
+
return entry;
|
|
20441
|
+
}
|
|
20442
|
+
async ingestResolved(event) {
|
|
20443
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
20444
|
+
if (!id) return null;
|
|
20445
|
+
const entry = this.entries.get(id);
|
|
20446
|
+
if (!entry) return null;
|
|
20447
|
+
if (entry.status !== "pending") return entry;
|
|
20448
|
+
if (!event.resolution) return entry;
|
|
20449
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
20450
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
20451
|
+
entry.status = status;
|
|
20452
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
20453
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
20454
|
+
await this.persist(entry);
|
|
20455
|
+
this.auditLog.append(
|
|
20456
|
+
"l2",
|
|
20457
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20458
|
+
this.identityId,
|
|
20459
|
+
{
|
|
20460
|
+
aggregator_id: id,
|
|
20461
|
+
source_harness: entry.source_harness,
|
|
20462
|
+
source_agent_id: entry.source_agent_id,
|
|
20463
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20464
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20465
|
+
decision: status,
|
|
20466
|
+
decided_by: entry.resolved_by,
|
|
20467
|
+
decided_at: entry.resolved_at,
|
|
20468
|
+
fail_closed: failClosed
|
|
20469
|
+
}
|
|
20470
|
+
);
|
|
20471
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20472
|
+
return entry;
|
|
20473
|
+
}
|
|
20474
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
20475
|
+
/**
|
|
20476
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
20477
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
20478
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
20479
|
+
* the audit entry the gate appended on the same call.
|
|
20480
|
+
*/
|
|
20481
|
+
auditEntryIdForEvent(event) {
|
|
20482
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
20483
|
+
}
|
|
20484
|
+
derivePolicyRuleId(event) {
|
|
20485
|
+
return `tier${event.tier}:${event.operation}`;
|
|
20486
|
+
}
|
|
20487
|
+
deriveActionSummary(event) {
|
|
20488
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
20489
|
+
}
|
|
20490
|
+
/**
|
|
20491
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
20492
|
+
* identical payloads always hash the same, even when key insertion order
|
|
20493
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
20494
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
20495
|
+
*/
|
|
20496
|
+
hashPayload(payload) {
|
|
20497
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
20498
|
+
return crypto.createHash("sha256").update(canonical).digest("hex");
|
|
20499
|
+
}
|
|
20500
|
+
emit(event) {
|
|
20501
|
+
for (const listener of this.listeners) {
|
|
20502
|
+
try {
|
|
20503
|
+
listener(event);
|
|
20504
|
+
} catch {
|
|
20505
|
+
}
|
|
20506
|
+
}
|
|
20507
|
+
}
|
|
20508
|
+
async expireStale() {
|
|
20509
|
+
const nowMs = this.now().getTime();
|
|
20510
|
+
for (const entry of this.entries.values()) {
|
|
20511
|
+
if (entry.status !== "pending") continue;
|
|
20512
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
20513
|
+
entry.status = "expired";
|
|
20514
|
+
entry.resolved_at = this.now().toISOString();
|
|
20515
|
+
entry.resolved_by = "system_ttl";
|
|
20516
|
+
await this.persist(entry);
|
|
20517
|
+
this.auditLog.append(
|
|
20518
|
+
"l2",
|
|
20519
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20520
|
+
this.identityId,
|
|
20521
|
+
{
|
|
20522
|
+
aggregator_id: entry.aggregator_id,
|
|
20523
|
+
source_harness: entry.source_harness,
|
|
20524
|
+
source_agent_id: entry.source_agent_id,
|
|
20525
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20526
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20527
|
+
decision: "expired",
|
|
20528
|
+
decided_by: "system_ttl",
|
|
20529
|
+
decided_at: entry.resolved_at
|
|
20530
|
+
}
|
|
20531
|
+
);
|
|
20532
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20533
|
+
}
|
|
20534
|
+
}
|
|
20535
|
+
async persist(entry) {
|
|
20536
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
20537
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
20538
|
+
await this.storage.write(
|
|
20539
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20540
|
+
entry.aggregator_id,
|
|
20541
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
20542
|
+
);
|
|
20543
|
+
}
|
|
20544
|
+
async hydrate() {
|
|
20545
|
+
if (this.hydrated) return;
|
|
20546
|
+
this.hydrated = true;
|
|
20547
|
+
try {
|
|
20548
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
20549
|
+
for (const meta of metas) {
|
|
20550
|
+
const raw = await this.storage.read(
|
|
20551
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20552
|
+
meta.key
|
|
20553
|
+
);
|
|
20554
|
+
if (!raw) continue;
|
|
20555
|
+
try {
|
|
20556
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
20557
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
20558
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
20559
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
20560
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
20561
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
20562
|
+
} catch {
|
|
20563
|
+
}
|
|
20564
|
+
}
|
|
20565
|
+
} catch {
|
|
20566
|
+
this.hydrated = false;
|
|
20567
|
+
}
|
|
20568
|
+
}
|
|
20569
|
+
};
|
|
20570
|
+
}
|
|
20571
|
+
});
|
|
19812
20572
|
|
|
19813
20573
|
// src/principal-policy/tools.ts
|
|
19814
20574
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
@@ -22726,6 +23486,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
22726
23486
|
}
|
|
22727
23487
|
async function consumeResetHistoryMarker(options) {
|
|
22728
23488
|
const markerPath = path.join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
23489
|
+
const consumedPath = markerPath + ".consumed";
|
|
23490
|
+
if (await fileExists3(consumedPath)) {
|
|
23491
|
+
await promises.rm(markerPath, { force: true });
|
|
23492
|
+
await promises.rm(consumedPath, { force: true });
|
|
23493
|
+
return { emitted: 0, markerPath };
|
|
23494
|
+
}
|
|
22729
23495
|
if (!await fileExists3(markerPath)) {
|
|
22730
23496
|
return { emitted: 0, markerPath };
|
|
22731
23497
|
}
|
|
@@ -22750,7 +23516,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
22750
23516
|
});
|
|
22751
23517
|
}
|
|
22752
23518
|
await options.auditLog.flush();
|
|
23519
|
+
await promises.writeFile(consumedPath, "", "utf-8");
|
|
22753
23520
|
await promises.rm(markerPath, { force: true });
|
|
23521
|
+
await promises.rm(consumedPath, { force: true });
|
|
22754
23522
|
return { emitted: markers.length, markerHash, markerPath };
|
|
22755
23523
|
}
|
|
22756
23524
|
async function fileExists3(path) {
|
|
@@ -32033,6 +32801,36 @@ var init_hub_service = __esm({
|
|
|
32033
32801
|
const chat = this.requireOperatorChat();
|
|
32034
32802
|
return chat.getConciergeHistory();
|
|
32035
32803
|
}
|
|
32804
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
32805
|
+
/**
|
|
32806
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
32807
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
32808
|
+
* surface is unavailable on a given fortress.
|
|
32809
|
+
*/
|
|
32810
|
+
hasConciergeMemory() {
|
|
32811
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
32812
|
+
}
|
|
32813
|
+
async listConciergeMemoryThreads(opts) {
|
|
32814
|
+
const chat = this.requireOperatorChat();
|
|
32815
|
+
if (!chat.hasConciergeMemory()) {
|
|
32816
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32817
|
+
}
|
|
32818
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
32819
|
+
}
|
|
32820
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
32821
|
+
const chat = this.requireOperatorChat();
|
|
32822
|
+
if (!chat.hasConciergeMemory()) {
|
|
32823
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32824
|
+
}
|
|
32825
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
32826
|
+
}
|
|
32827
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
32828
|
+
const chat = this.requireOperatorChat();
|
|
32829
|
+
if (!chat.hasConciergeMemory()) {
|
|
32830
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32831
|
+
}
|
|
32832
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
32833
|
+
}
|
|
32036
32834
|
/**
|
|
32037
32835
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
32038
32836
|
* panel surfaces recent activity routed through this agent, pending
|
|
@@ -32171,7 +32969,20 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32171
32969
|
* affordance now opens an inspect/approve panel (recent activity +
|
|
32172
32970
|
* pending approvals + policy summary) instead of a chat session.
|
|
32173
32971
|
*/
|
|
32174
|
-
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened"
|
|
32972
|
+
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
|
|
32973
|
+
/**
|
|
32974
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
32975
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
32976
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
32977
|
+
* raw turn content never crosses the audit surface.
|
|
32978
|
+
*/
|
|
32979
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
32980
|
+
/**
|
|
32981
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
32982
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
32983
|
+
* the deleted bundle.
|
|
32984
|
+
*/
|
|
32985
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
32175
32986
|
};
|
|
32176
32987
|
}
|
|
32177
32988
|
});
|
|
@@ -32233,6 +33044,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32233
33044
|
contextProviders;
|
|
32234
33045
|
piiFilter;
|
|
32235
33046
|
conciergeMaxTokens;
|
|
33047
|
+
memory;
|
|
33048
|
+
/**
|
|
33049
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
33050
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
33051
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
33052
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33053
|
+
*/
|
|
33054
|
+
activeMemoryThreadId;
|
|
32236
33055
|
constructor(deps) {
|
|
32237
33056
|
this.store = deps.store;
|
|
32238
33057
|
this.auditLog = deps.auditLog;
|
|
@@ -32243,6 +33062,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32243
33062
|
}
|
|
32244
33063
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
32245
33064
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33065
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32246
33066
|
}
|
|
32247
33067
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
32248
33068
|
/**
|
|
@@ -32273,6 +33093,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32273
33093
|
CONCIERGE_THREAD_KEY,
|
|
32274
33094
|
operatorMessage
|
|
32275
33095
|
);
|
|
33096
|
+
if (this.memory) {
|
|
33097
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33098
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
33099
|
+
});
|
|
33100
|
+
}
|
|
32276
33101
|
const start = Date.now();
|
|
32277
33102
|
let conciergeBody;
|
|
32278
33103
|
let servedBy = "disabled";
|
|
@@ -32328,6 +33153,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32328
33153
|
CONCIERGE_THREAD_KEY,
|
|
32329
33154
|
responseMessage
|
|
32330
33155
|
);
|
|
33156
|
+
if (this.memory) {
|
|
33157
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33158
|
+
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
|
|
33159
|
+
});
|
|
33160
|
+
}
|
|
32331
33161
|
const payload = {
|
|
32332
33162
|
version: "1.2",
|
|
32333
33163
|
event_id: makeEventId("conc"),
|
|
@@ -32360,6 +33190,105 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
32360
33190
|
);
|
|
32361
33191
|
return thread ? thread.messages : [];
|
|
32362
33192
|
}
|
|
33193
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
33194
|
+
/**
|
|
33195
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
33196
|
+
* 503 cleanly when called against an unwired service.
|
|
33197
|
+
*/
|
|
33198
|
+
hasConciergeMemory() {
|
|
33199
|
+
return this.memory !== void 0;
|
|
33200
|
+
}
|
|
33201
|
+
/**
|
|
33202
|
+
* List concierge memory threads, newest-first. Emits the
|
|
33203
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
33204
|
+
*/
|
|
33205
|
+
async listConciergeMemoryThreads(opts) {
|
|
33206
|
+
if (!this.memory) {
|
|
33207
|
+
throw new Error("concierge memory store not configured");
|
|
33208
|
+
}
|
|
33209
|
+
const summaries = await this.memory.listThreads(opts);
|
|
33210
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
33211
|
+
const payload = {
|
|
33212
|
+
version: "1.2",
|
|
33213
|
+
event_id: makeEventId("conc-hist"),
|
|
33214
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33215
|
+
identity_id: this.identityId,
|
|
33216
|
+
kind: "operator_concierge_history_read",
|
|
33217
|
+
surface: "concierge",
|
|
33218
|
+
thread_id: "*",
|
|
33219
|
+
turn_count: totalTurns
|
|
33220
|
+
};
|
|
33221
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33222
|
+
return summaries;
|
|
33223
|
+
}
|
|
33224
|
+
/**
|
|
33225
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
33226
|
+
* `operator_concierge_history_read` audit event with the named
|
|
33227
|
+
* thread_id and the count of turns surfaced.
|
|
33228
|
+
*/
|
|
33229
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
33230
|
+
if (!this.memory) {
|
|
33231
|
+
throw new Error("concierge memory store not configured");
|
|
33232
|
+
}
|
|
33233
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
33234
|
+
const payload = {
|
|
33235
|
+
version: "1.2",
|
|
33236
|
+
event_id: makeEventId("conc-hist"),
|
|
33237
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33238
|
+
identity_id: this.identityId,
|
|
33239
|
+
kind: "operator_concierge_history_read",
|
|
33240
|
+
surface: "concierge",
|
|
33241
|
+
thread_id: threadId,
|
|
33242
|
+
turn_count: turns.length
|
|
33243
|
+
};
|
|
33244
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33245
|
+
return turns;
|
|
33246
|
+
}
|
|
33247
|
+
/**
|
|
33248
|
+
* Delete a concierge memory thread. Emits
|
|
33249
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
33250
|
+
* removed; absent threads return false without an audit event.
|
|
33251
|
+
*/
|
|
33252
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
33253
|
+
if (!this.memory) {
|
|
33254
|
+
throw new Error("concierge memory store not configured");
|
|
33255
|
+
}
|
|
33256
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
33257
|
+
if (turnsBefore.length === 0) {
|
|
33258
|
+
return await this.memory.deleteThread(threadId);
|
|
33259
|
+
}
|
|
33260
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
33261
|
+
if (!removed) return false;
|
|
33262
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
33263
|
+
this.activeMemoryThreadId = void 0;
|
|
33264
|
+
}
|
|
33265
|
+
const payload = {
|
|
33266
|
+
version: "1.2",
|
|
33267
|
+
event_id: makeEventId("conc-del"),
|
|
33268
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33269
|
+
identity_id: this.identityId,
|
|
33270
|
+
kind: "operator_concierge_thread_deleted",
|
|
33271
|
+
surface: "concierge",
|
|
33272
|
+
thread_id: threadId,
|
|
33273
|
+
turn_count: turnsBefore.length
|
|
33274
|
+
};
|
|
33275
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
33276
|
+
return true;
|
|
33277
|
+
}
|
|
33278
|
+
/**
|
|
33279
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
33280
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
33281
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
33282
|
+
*/
|
|
33283
|
+
resetConciergeMemoryThread() {
|
|
33284
|
+
this.activeMemoryThreadId = void 0;
|
|
33285
|
+
}
|
|
33286
|
+
ensureActiveMemoryThread() {
|
|
33287
|
+
if (!this.activeMemoryThreadId) {
|
|
33288
|
+
this.activeMemoryThreadId = crypto.randomUUID();
|
|
33289
|
+
}
|
|
33290
|
+
return this.activeMemoryThreadId;
|
|
33291
|
+
}
|
|
32363
33292
|
/**
|
|
32364
33293
|
* Stitch fortress state into a single context blob the substrate
|
|
32365
33294
|
* folds into its summarization prompt.
|
|
@@ -32526,11 +33455,250 @@ var init_operator_chat_store = __esm({
|
|
|
32526
33455
|
}
|
|
32527
33456
|
});
|
|
32528
33457
|
|
|
33458
|
+
// src/chat/concierge-memory-store.ts
|
|
33459
|
+
function bundleKey(threadId) {
|
|
33460
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
33461
|
+
}
|
|
33462
|
+
function stripKeyPrefix(key) {
|
|
33463
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
33464
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
33465
|
+
}
|
|
33466
|
+
function lastTurnId(bundle) {
|
|
33467
|
+
let max = 0;
|
|
33468
|
+
for (const t of bundle.turns) {
|
|
33469
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
33470
|
+
}
|
|
33471
|
+
return max;
|
|
33472
|
+
}
|
|
33473
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO2, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES2, ConciergeMemoryStore;
|
|
33474
|
+
var init_concierge_memory_store = __esm({
|
|
33475
|
+
"src/chat/concierge-memory-store.ts"() {
|
|
33476
|
+
init_encryption();
|
|
33477
|
+
init_key_derivation();
|
|
33478
|
+
init_encoding();
|
|
33479
|
+
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
33480
|
+
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
33481
|
+
HKDF_INFO2 = "concierge-memory-store-v1";
|
|
33482
|
+
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
33483
|
+
MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
33484
|
+
ConciergeMemoryStore = class {
|
|
33485
|
+
storage;
|
|
33486
|
+
encryptionKey;
|
|
33487
|
+
fortressId;
|
|
33488
|
+
retentionDays;
|
|
33489
|
+
locks;
|
|
33490
|
+
constructor(opts) {
|
|
33491
|
+
this.storage = opts.storage;
|
|
33492
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
33493
|
+
this.fortressId = opts.fortressId;
|
|
33494
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
33495
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
33496
|
+
}
|
|
33497
|
+
/**
|
|
33498
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
33499
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
33500
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
33501
|
+
* monotonicity even under concurrent callers.
|
|
33502
|
+
*/
|
|
33503
|
+
async appendTurn(threadId, role, content) {
|
|
33504
|
+
return this.withLock(threadId, async () => {
|
|
33505
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
33506
|
+
const now = /* @__PURE__ */ new Date();
|
|
33507
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
33508
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
33509
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
33510
|
+
const turn = {
|
|
33511
|
+
thread_id: threadId,
|
|
33512
|
+
fortress_id: this.fortressId,
|
|
33513
|
+
turn_id: nextTurnId,
|
|
33514
|
+
role,
|
|
33515
|
+
content,
|
|
33516
|
+
created_at: now.toISOString(),
|
|
33517
|
+
retention_until: retentionUntil.toISOString()
|
|
33518
|
+
};
|
|
33519
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
33520
|
+
version: 1,
|
|
33521
|
+
thread_id: threadId,
|
|
33522
|
+
fortress_id: this.fortressId,
|
|
33523
|
+
created_at: now.toISOString(),
|
|
33524
|
+
turns: [turn]
|
|
33525
|
+
};
|
|
33526
|
+
await this.saveBundle(next);
|
|
33527
|
+
return turn;
|
|
33528
|
+
});
|
|
33529
|
+
}
|
|
33530
|
+
/**
|
|
33531
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
33532
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
33533
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
33534
|
+
*/
|
|
33535
|
+
async readThread(threadId, opts) {
|
|
33536
|
+
const bundle = await this.loadBundle(threadId);
|
|
33537
|
+
if (!bundle) return [];
|
|
33538
|
+
let turns = bundle.turns;
|
|
33539
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
33540
|
+
const cutoff = opts.sinceTurnId;
|
|
33541
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
33542
|
+
}
|
|
33543
|
+
if (opts?.limit !== void 0) {
|
|
33544
|
+
turns = turns.slice(0, opts.limit);
|
|
33545
|
+
}
|
|
33546
|
+
return turns;
|
|
33547
|
+
}
|
|
33548
|
+
/**
|
|
33549
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33550
|
+
* Sorted newest-first by last_turn_at.
|
|
33551
|
+
*/
|
|
33552
|
+
async listThreads(opts) {
|
|
33553
|
+
const entries = await this.storage.list(
|
|
33554
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33555
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33556
|
+
);
|
|
33557
|
+
const summaries = [];
|
|
33558
|
+
for (const meta of entries) {
|
|
33559
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33560
|
+
if (threadId === null) continue;
|
|
33561
|
+
const bundle = await this.loadBundle(threadId);
|
|
33562
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
33563
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
33564
|
+
summaries.push({
|
|
33565
|
+
thread_id: bundle.thread_id,
|
|
33566
|
+
created_at: bundle.created_at,
|
|
33567
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
33568
|
+
turn_count: bundle.turns.length
|
|
33569
|
+
});
|
|
33570
|
+
}
|
|
33571
|
+
summaries.sort(
|
|
33572
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
33573
|
+
);
|
|
33574
|
+
if (opts?.limit !== void 0) {
|
|
33575
|
+
return summaries.slice(0, opts.limit);
|
|
33576
|
+
}
|
|
33577
|
+
return summaries;
|
|
33578
|
+
}
|
|
33579
|
+
/**
|
|
33580
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
33581
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
33582
|
+
* caller's responsibility.
|
|
33583
|
+
*/
|
|
33584
|
+
async deleteThread(threadId) {
|
|
33585
|
+
const key = bundleKey(threadId);
|
|
33586
|
+
return this.withLock(threadId, async () => {
|
|
33587
|
+
const existed = await this.storage.exists(
|
|
33588
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33589
|
+
key
|
|
33590
|
+
);
|
|
33591
|
+
if (!existed) return false;
|
|
33592
|
+
try {
|
|
33593
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33594
|
+
} catch {
|
|
33595
|
+
return false;
|
|
33596
|
+
}
|
|
33597
|
+
return true;
|
|
33598
|
+
});
|
|
33599
|
+
}
|
|
33600
|
+
/**
|
|
33601
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
33602
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
33603
|
+
*/
|
|
33604
|
+
async pruneExpired(now) {
|
|
33605
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
33606
|
+
const entries = await this.storage.list(
|
|
33607
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33608
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33609
|
+
);
|
|
33610
|
+
let pruned = 0;
|
|
33611
|
+
for (const meta of entries) {
|
|
33612
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33613
|
+
if (threadId === null) continue;
|
|
33614
|
+
pruned += await this.withLock(threadId, async () => {
|
|
33615
|
+
const bundle = await this.loadBundle(threadId);
|
|
33616
|
+
if (!bundle) return 0;
|
|
33617
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
33618
|
+
const dropped = bundle.turns.length - kept.length;
|
|
33619
|
+
if (dropped === 0) return 0;
|
|
33620
|
+
if (kept.length === 0) {
|
|
33621
|
+
await this.storage.delete(
|
|
33622
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33623
|
+
bundleKey(threadId)
|
|
33624
|
+
);
|
|
33625
|
+
} else {
|
|
33626
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
33627
|
+
}
|
|
33628
|
+
return dropped;
|
|
33629
|
+
});
|
|
33630
|
+
}
|
|
33631
|
+
return { pruned };
|
|
33632
|
+
}
|
|
33633
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
33634
|
+
async loadBundle(threadId) {
|
|
33635
|
+
const key = bundleKey(threadId);
|
|
33636
|
+
let raw;
|
|
33637
|
+
try {
|
|
33638
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33639
|
+
} catch {
|
|
33640
|
+
return null;
|
|
33641
|
+
}
|
|
33642
|
+
if (!raw) return null;
|
|
33643
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
33644
|
+
try {
|
|
33645
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
33646
|
+
const aad = stringToBytes(threadId);
|
|
33647
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
33648
|
+
const parsed = JSON.parse(
|
|
33649
|
+
bytesToString(plaintext)
|
|
33650
|
+
);
|
|
33651
|
+
if (parsed.version !== 1) return null;
|
|
33652
|
+
if (parsed.thread_id !== threadId) return null;
|
|
33653
|
+
return parsed;
|
|
33654
|
+
} catch {
|
|
33655
|
+
return null;
|
|
33656
|
+
}
|
|
33657
|
+
}
|
|
33658
|
+
async saveBundle(bundle) {
|
|
33659
|
+
const key = bundleKey(bundle.thread_id);
|
|
33660
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
33661
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
33662
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
33663
|
+
await this.storage.write(
|
|
33664
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33665
|
+
key,
|
|
33666
|
+
stringToBytes(JSON.stringify(envelope))
|
|
33667
|
+
);
|
|
33668
|
+
}
|
|
33669
|
+
/**
|
|
33670
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
33671
|
+
* once the task settles (success or failure). Generic helper so
|
|
33672
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
33673
|
+
*/
|
|
33674
|
+
async withLock(threadId, task) {
|
|
33675
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
33676
|
+
let release;
|
|
33677
|
+
const next = new Promise((resolve8) => {
|
|
33678
|
+
release = resolve8;
|
|
33679
|
+
});
|
|
33680
|
+
const chained = previous.then(() => next);
|
|
33681
|
+
this.locks.set(threadId, chained);
|
|
33682
|
+
try {
|
|
33683
|
+
await previous;
|
|
33684
|
+
return await task();
|
|
33685
|
+
} finally {
|
|
33686
|
+
release();
|
|
33687
|
+
if (this.locks.get(threadId) === chained) {
|
|
33688
|
+
this.locks.delete(threadId);
|
|
33689
|
+
}
|
|
33690
|
+
}
|
|
33691
|
+
}
|
|
33692
|
+
};
|
|
33693
|
+
}
|
|
33694
|
+
});
|
|
33695
|
+
|
|
32529
33696
|
// src/chat/operator-chat-index.ts
|
|
32530
33697
|
var init_operator_chat_index = __esm({
|
|
32531
33698
|
"src/chat/operator-chat-index.ts"() {
|
|
32532
33699
|
init_operator_chat_service();
|
|
32533
33700
|
init_operator_chat_store();
|
|
33701
|
+
init_concierge_memory_store();
|
|
32534
33702
|
init_operator_chat_audit_events();
|
|
32535
33703
|
init_operator_chat_types();
|
|
32536
33704
|
}
|
|
@@ -32544,6 +33712,14 @@ function buildV11Bindings(inputs) {
|
|
|
32544
33712
|
let operatorChatService;
|
|
32545
33713
|
if (inputs.storage && inputs.masterKey) {
|
|
32546
33714
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
33715
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
33716
|
+
storage: inputs.storage,
|
|
33717
|
+
masterKey: inputs.masterKey,
|
|
33718
|
+
fortressId: inputs.fortressId,
|
|
33719
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
33720
|
+
});
|
|
33721
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
33722
|
+
});
|
|
32547
33723
|
operatorChatService = new OperatorChatService({
|
|
32548
33724
|
store: chatStore,
|
|
32549
33725
|
auditLog: inputs.auditLog,
|
|
@@ -32554,7 +33730,8 @@ function buildV11Bindings(inputs) {
|
|
|
32554
33730
|
identityId: inputs.identityId,
|
|
32555
33731
|
registry
|
|
32556
33732
|
}),
|
|
32557
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
33733
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
33734
|
+
conciergeMemory
|
|
32558
33735
|
});
|
|
32559
33736
|
}
|
|
32560
33737
|
const hubService = new HubService({
|
|
@@ -32789,7 +33966,7 @@ var init_defaults = __esm({
|
|
|
32789
33966
|
});
|
|
32790
33967
|
|
|
32791
33968
|
// src/intelligence/policy-store.ts
|
|
32792
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
33969
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO3, IntelligenceConfigStore;
|
|
32793
33970
|
var init_policy_store = __esm({
|
|
32794
33971
|
"src/intelligence/policy-store.ts"() {
|
|
32795
33972
|
init_encryption();
|
|
@@ -32798,13 +33975,13 @@ var init_policy_store = __esm({
|
|
|
32798
33975
|
init_defaults();
|
|
32799
33976
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
32800
33977
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
32801
|
-
|
|
33978
|
+
HKDF_INFO3 = "intelligence-substrate-config";
|
|
32802
33979
|
IntelligenceConfigStore = class {
|
|
32803
33980
|
storage;
|
|
32804
33981
|
encryptionKey;
|
|
32805
33982
|
constructor(storage, masterKey) {
|
|
32806
33983
|
this.storage = storage;
|
|
32807
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33984
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
32808
33985
|
}
|
|
32809
33986
|
/**
|
|
32810
33987
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -34826,7 +36003,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34826
36003
|
);
|
|
34827
36004
|
}
|
|
34828
36005
|
}
|
|
34829
|
-
const
|
|
36006
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
36007
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
36008
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
34830
36009
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
34831
36010
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
34832
36011
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -34835,6 +36014,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34835
36014
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
34836
36015
|
);
|
|
34837
36016
|
}
|
|
36017
|
+
let detailedFailureClass;
|
|
36018
|
+
if (identityFailed) {
|
|
36019
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
36020
|
+
} else if (reputationBundleFailed) {
|
|
36021
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
36022
|
+
} else if (reputationAttestationFailed) {
|
|
36023
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
36024
|
+
} else if (unverifiableFailed) {
|
|
36025
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
36026
|
+
}
|
|
34838
36027
|
return {
|
|
34839
36028
|
version: "1.1",
|
|
34840
36029
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -34854,7 +36043,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34854
36043
|
identity,
|
|
34855
36044
|
audit,
|
|
34856
36045
|
reputation,
|
|
34857
|
-
failure_class:
|
|
36046
|
+
failure_class: detailedFailureClass
|
|
34858
36047
|
};
|
|
34859
36048
|
}
|
|
34860
36049
|
var InvalidExitBundleError, PRIVATE_MATERIAL_KEYS;
|
|
@@ -35822,7 +37011,19 @@ async function runExitCommand(args) {
|
|
|
35822
37011
|
}
|
|
35823
37012
|
const config = await loadConfig();
|
|
35824
37013
|
const ctx = await openExitContext(argv, env);
|
|
35825
|
-
|
|
37014
|
+
let policy;
|
|
37015
|
+
try {
|
|
37016
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
37017
|
+
} catch (policyErr) {
|
|
37018
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
37019
|
+
write(err, `
|
|
37020
|
+
Sanctuary cannot proceed.
|
|
37021
|
+
${policyErr.message}
|
|
37022
|
+
`);
|
|
37023
|
+
return 1;
|
|
37024
|
+
}
|
|
37025
|
+
throw policyErr;
|
|
37026
|
+
}
|
|
35826
37027
|
const result = await exportExitBundle({
|
|
35827
37028
|
bundleDir: outDir,
|
|
35828
37029
|
storage: ctx.storage,
|
|
@@ -36522,7 +37723,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36522
37723
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
36523
37724
|
await profileStore.load();
|
|
36524
37725
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
36525
|
-
|
|
37726
|
+
let policy;
|
|
37727
|
+
try {
|
|
37728
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
37729
|
+
} catch (err) {
|
|
37730
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
37731
|
+
console.error(`
|
|
37732
|
+
Sanctuary cannot start.
|
|
37733
|
+
${err.message}
|
|
37734
|
+
`);
|
|
37735
|
+
process.exit(1);
|
|
37736
|
+
}
|
|
37737
|
+
throw err;
|
|
37738
|
+
}
|
|
36526
37739
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
36527
37740
|
await baseline.load();
|
|
36528
37741
|
let approvalChannel;
|
|
@@ -36620,6 +37833,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36620
37833
|
});
|
|
36621
37834
|
} : void 0;
|
|
36622
37835
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37836
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37837
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37838
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
37839
|
+
storage,
|
|
37840
|
+
masterKey,
|
|
37841
|
+
auditLog,
|
|
37842
|
+
identityId: aggregatorIdentityId,
|
|
37843
|
+
fortressId: fortressIdForAggregator
|
|
37844
|
+
});
|
|
37845
|
+
gate.setApprovalEventCallback((event) => {
|
|
37846
|
+
void approvalAggregator.ingest(event);
|
|
37847
|
+
});
|
|
37848
|
+
if (dashboard) {
|
|
37849
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
37850
|
+
}
|
|
36623
37851
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
36624
37852
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
36625
37853
|
config,
|
|
@@ -36810,6 +38038,7 @@ var init_src = __esm({
|
|
|
36810
38038
|
init_dashboard();
|
|
36811
38039
|
init_webhook();
|
|
36812
38040
|
init_gate();
|
|
38041
|
+
init_approval_aggregator();
|
|
36813
38042
|
init_tools4();
|
|
36814
38043
|
init_router();
|
|
36815
38044
|
init_router();
|
|
@@ -41173,7 +42402,8 @@ var init_agents = __esm({
|
|
|
41173
42402
|
// src/cli/reset-passphrase.ts
|
|
41174
42403
|
var reset_passphrase_exports = {};
|
|
41175
42404
|
__export(reset_passphrase_exports, {
|
|
41176
|
-
runResetPassphraseCommand: () => runResetPassphraseCommand
|
|
42405
|
+
runResetPassphraseCommand: () => runResetPassphraseCommand,
|
|
42406
|
+
zeroizeBuffers: () => zeroizeBuffers
|
|
41177
42407
|
});
|
|
41178
42408
|
async function runResetPassphraseCommand(args) {
|
|
41179
42409
|
const out = args.out ?? process.stdout;
|
|
@@ -41201,34 +42431,42 @@ Then re-run this command.
|
|
|
41201
42431
|
return 1;
|
|
41202
42432
|
}
|
|
41203
42433
|
const lines = new LineReader(stdin);
|
|
42434
|
+
let code = 1;
|
|
42435
|
+
let nukeSucceeded = false;
|
|
41204
42436
|
try {
|
|
41205
42437
|
const availability = await surveyAvailableModes(storagePath);
|
|
41206
42438
|
const mode = parsed.mode ?? await selectMode(lines, out, err, availability);
|
|
41207
42439
|
if (!mode) {
|
|
41208
42440
|
err.write("Aborted: no recovery mode selected.\n");
|
|
41209
|
-
|
|
41210
|
-
}
|
|
41211
|
-
|
|
41212
|
-
|
|
41213
|
-
|
|
41214
|
-
|
|
41215
|
-
|
|
42441
|
+
code = 1;
|
|
42442
|
+
} else if (mode === "shares") {
|
|
42443
|
+
code = await runSharesPath(out, err, availability);
|
|
42444
|
+
} else if (mode === "guardian") {
|
|
42445
|
+
code = await runGuardianPath(out, err, availability);
|
|
42446
|
+
} else {
|
|
42447
|
+
code = await runNukePath({
|
|
42448
|
+
out,
|
|
42449
|
+
err,
|
|
42450
|
+
lines,
|
|
42451
|
+
storagePath,
|
|
42452
|
+
home,
|
|
42453
|
+
plat,
|
|
42454
|
+
exec: args.exec ?? defaultExec2
|
|
42455
|
+
});
|
|
42456
|
+
nukeSucceeded = mode === "nuke" && code === 0;
|
|
41216
42457
|
}
|
|
41217
|
-
return await runNukePath({
|
|
41218
|
-
out,
|
|
41219
|
-
err,
|
|
41220
|
-
lines,
|
|
41221
|
-
storagePath,
|
|
41222
|
-
home,
|
|
41223
|
-
plat,
|
|
41224
|
-
exec: args.exec ?? defaultExec2
|
|
41225
|
-
});
|
|
41226
42458
|
} finally {
|
|
42459
|
+
zeroizeBuffers(args.keyMaterialToZeroize);
|
|
41227
42460
|
lines.close();
|
|
41228
42461
|
}
|
|
42462
|
+
if (parsed.exitOnCompletion && nukeSucceeded) {
|
|
42463
|
+
const doExit = args.exitProcess ?? ((c) => process.exit(c));
|
|
42464
|
+
doExit(0);
|
|
42465
|
+
}
|
|
42466
|
+
return code;
|
|
41229
42467
|
}
|
|
41230
42468
|
function parseArgs2(argv) {
|
|
41231
|
-
const out = { help: false };
|
|
42469
|
+
const out = { exitOnCompletion: false, help: false };
|
|
41232
42470
|
for (let i = 0; i < argv.length; i++) {
|
|
41233
42471
|
const a = argv[i];
|
|
41234
42472
|
if (a === "--help" || a === "-h") {
|
|
@@ -41245,6 +42483,8 @@ function parseArgs2(argv) {
|
|
|
41245
42483
|
out.storage = argv[++i];
|
|
41246
42484
|
} else if (a === "--fortress" && argv[i + 1]) {
|
|
41247
42485
|
out.fortress = argv[++i];
|
|
42486
|
+
} else if (a === "--exit-on-completion") {
|
|
42487
|
+
out.exitOnCompletion = true;
|
|
41248
42488
|
} else if (a && a.startsWith("--")) {
|
|
41249
42489
|
throw new Error(`Unknown flag: ${a}`);
|
|
41250
42490
|
}
|
|
@@ -41277,6 +42517,16 @@ Options:
|
|
|
41277
42517
|
--fortress <path> Override the fortress storage path.
|
|
41278
42518
|
Consistent with "sanctuary wrap --fortress".
|
|
41279
42519
|
--storage <path> Alias for --fortress.
|
|
42520
|
+
--exit-on-completion After a successful nuke, call process.exit(0)
|
|
42521
|
+
immediately so the post-wipe heap is reaped
|
|
42522
|
+
by the OS without re-entering the shell. Use
|
|
42523
|
+
on extreme-threat-model deployments where an
|
|
42524
|
+
attacker-on-host with heap-dump access could
|
|
42525
|
+
recover residual passphrase or key bytes
|
|
42526
|
+
between the wipe and the next operator
|
|
42527
|
+
command. JS strings cannot be explicitly
|
|
42528
|
+
zeroed; this flag is the supported way to
|
|
42529
|
+
bound the heap-dump window.
|
|
41280
42530
|
--help, -h Show this help.
|
|
41281
42531
|
|
|
41282
42532
|
Without --mode, the command surveys which paths are operationally available
|
|
@@ -41546,6 +42796,16 @@ async function prompt(lines, err, question) {
|
|
|
41546
42796
|
err.write(question);
|
|
41547
42797
|
return await lines.next();
|
|
41548
42798
|
}
|
|
42799
|
+
function zeroizeBuffers(buffers) {
|
|
42800
|
+
if (!buffers) return;
|
|
42801
|
+
for (const b of buffers) {
|
|
42802
|
+
if (!b) continue;
|
|
42803
|
+
try {
|
|
42804
|
+
b.fill(0);
|
|
42805
|
+
} catch {
|
|
42806
|
+
}
|
|
42807
|
+
}
|
|
42808
|
+
}
|
|
41549
42809
|
async function defaultExec2(cmd, args) {
|
|
41550
42810
|
return await new Promise((resolve8, reject) => {
|
|
41551
42811
|
const child = child_process.spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -42226,7 +43486,19 @@ Refusing to start the dashboard while the reset-history marker is unreadable.`
|
|
|
42226
43486
|
}
|
|
42227
43487
|
throw err;
|
|
42228
43488
|
}
|
|
42229
|
-
|
|
43489
|
+
let policy;
|
|
43490
|
+
try {
|
|
43491
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
43492
|
+
} catch (err) {
|
|
43493
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
43494
|
+
console.error(`
|
|
43495
|
+
Sanctuary cannot start.
|
|
43496
|
+
${err.message}
|
|
43497
|
+
`);
|
|
43498
|
+
process.exit(1);
|
|
43499
|
+
}
|
|
43500
|
+
throw err;
|
|
43501
|
+
}
|
|
42230
43502
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
42231
43503
|
await baseline.load();
|
|
42232
43504
|
const dashboardPort = options.port ?? config.dashboard.port;
|