@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/index.cjs
CHANGED
|
@@ -4589,20 +4589,52 @@ approval_channel:
|
|
|
4589
4589
|
timeout_seconds: 300
|
|
4590
4590
|
`;
|
|
4591
4591
|
}
|
|
4592
|
+
var MalformedPrincipalPolicyError = class extends Error {
|
|
4593
|
+
constructor(policyPath, reason) {
|
|
4594
|
+
super(
|
|
4595
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4596
|
+
Reason: ${reason}
|
|
4597
|
+
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.`
|
|
4598
|
+
);
|
|
4599
|
+
this.policyPath = policyPath;
|
|
4600
|
+
this.reason = reason;
|
|
4601
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4602
|
+
}
|
|
4603
|
+
policyPath;
|
|
4604
|
+
reason;
|
|
4605
|
+
};
|
|
4592
4606
|
async function loadPrincipalPolicy(storagePath) {
|
|
4593
4607
|
const policyPath = path.join(storagePath, "principal-policy.yaml");
|
|
4608
|
+
let content;
|
|
4609
|
+
try {
|
|
4610
|
+
content = await promises.readFile(policyPath, "utf-8");
|
|
4611
|
+
} catch (err) {
|
|
4612
|
+
const code = err?.code;
|
|
4613
|
+
if (code === "ENOENT") {
|
|
4614
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4615
|
+
try {
|
|
4616
|
+
await promises.writeFile(policyPath, defaultYaml, "utf-8");
|
|
4617
|
+
await promises.chmod(policyPath, 384);
|
|
4618
|
+
} catch (writeErr) {
|
|
4619
|
+
console.warn(
|
|
4620
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4621
|
+
);
|
|
4622
|
+
}
|
|
4623
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4624
|
+
}
|
|
4625
|
+
throw new MalformedPrincipalPolicyError(
|
|
4626
|
+
policyPath,
|
|
4627
|
+
`read failed: ${err.message}`
|
|
4628
|
+
);
|
|
4629
|
+
}
|
|
4594
4630
|
try {
|
|
4595
|
-
const content = await promises.readFile(policyPath, "utf-8");
|
|
4596
4631
|
const policy = parsePolicy(content);
|
|
4597
4632
|
return Object.freeze(policy);
|
|
4598
|
-
} catch {
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
} catch {
|
|
4604
|
-
}
|
|
4605
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4633
|
+
} catch (parseErr) {
|
|
4634
|
+
throw new MalformedPrincipalPolicyError(
|
|
4635
|
+
policyPath,
|
|
4636
|
+
parseErr.message
|
|
4637
|
+
);
|
|
4606
4638
|
}
|
|
4607
4639
|
}
|
|
4608
4640
|
|
|
@@ -4829,7 +4861,7 @@ function deepSortKeys(obj) {
|
|
|
4829
4861
|
return sorted;
|
|
4830
4862
|
}
|
|
4831
4863
|
function canonicalizeForSigning(body) {
|
|
4832
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4864
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4833
4865
|
}
|
|
4834
4866
|
|
|
4835
4867
|
// src/shr/generator.ts
|
|
@@ -11651,6 +11683,16 @@ var HUB_ROUTES = {
|
|
|
11651
11683
|
*/
|
|
11652
11684
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
11653
11685
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
11686
|
+
/**
|
|
11687
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
11688
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
11689
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
11690
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
11691
|
+
* v1.3 conversational sovereignty depth.
|
|
11692
|
+
*/
|
|
11693
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
11694
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11695
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11654
11696
|
/**
|
|
11655
11697
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
11656
11698
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -11674,6 +11716,10 @@ var HUB_TIER_1_AGENT_CONTROL_ACTIONS = [
|
|
|
11674
11716
|
];
|
|
11675
11717
|
var HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
11676
11718
|
var HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
11719
|
+
var HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
11720
|
+
var HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
11721
|
+
var HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
11722
|
+
var HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
11677
11723
|
var HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
11678
11724
|
var HUB_INBOX_MAX_LIMIT = 500;
|
|
11679
11725
|
var HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -11845,6 +11891,23 @@ function checkChatMessage(value) {
|
|
|
11845
11891
|
}
|
|
11846
11892
|
return trimmed;
|
|
11847
11893
|
}
|
|
11894
|
+
function matchConciergeThreadRoute(path) {
|
|
11895
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
11896
|
+
if (!path.startsWith(prefix)) return null;
|
|
11897
|
+
const rest = path.slice(prefix.length);
|
|
11898
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
11899
|
+
const decoded = decodeURIComponent(rest);
|
|
11900
|
+
if (decoded.length === 0) return null;
|
|
11901
|
+
return { threadId: decoded };
|
|
11902
|
+
}
|
|
11903
|
+
function parseSince(raw) {
|
|
11904
|
+
if (raw === null || raw === "") return void 0;
|
|
11905
|
+
const parsed = Number.parseInt(raw, 10);
|
|
11906
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
11907
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
11908
|
+
}
|
|
11909
|
+
return parsed;
|
|
11910
|
+
}
|
|
11848
11911
|
function matchInboxRoute(path) {
|
|
11849
11912
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
11850
11913
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12028,6 +12091,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12028
12091
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12029
12092
|
return true;
|
|
12030
12093
|
}
|
|
12094
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12095
|
+
const limit = parseLimit(
|
|
12096
|
+
url.searchParams.get("limit"),
|
|
12097
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12098
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12099
|
+
);
|
|
12100
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12101
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12102
|
+
return true;
|
|
12103
|
+
}
|
|
12104
|
+
{
|
|
12105
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12106
|
+
if (threadMatch) {
|
|
12107
|
+
if (method === "GET") {
|
|
12108
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12109
|
+
const limit = parseLimit(
|
|
12110
|
+
url.searchParams.get("limit"),
|
|
12111
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12112
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12113
|
+
);
|
|
12114
|
+
const readOpts = { limit };
|
|
12115
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12116
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12117
|
+
threadMatch.threadId,
|
|
12118
|
+
readOpts
|
|
12119
|
+
);
|
|
12120
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12121
|
+
return true;
|
|
12122
|
+
}
|
|
12123
|
+
if (method === "DELETE") {
|
|
12124
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12125
|
+
threadMatch.threadId
|
|
12126
|
+
);
|
|
12127
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12128
|
+
ok: removed,
|
|
12129
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12130
|
+
});
|
|
12131
|
+
return true;
|
|
12132
|
+
}
|
|
12133
|
+
}
|
|
12134
|
+
}
|
|
12031
12135
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12032
12136
|
return true;
|
|
12033
12137
|
} catch (err) {
|
|
@@ -16077,6 +16181,162 @@ async function dispatchV11Request(inputs, req, res, url, method) {
|
|
|
16077
16181
|
return false;
|
|
16078
16182
|
}
|
|
16079
16183
|
|
|
16184
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
16185
|
+
var APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
16186
|
+
var APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
16187
|
+
var APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
16188
|
+
var APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
16189
|
+
function writeJSON4(res, status, payload) {
|
|
16190
|
+
res.writeHead(status, {
|
|
16191
|
+
"Content-Type": "application/json",
|
|
16192
|
+
"Cache-Control": "no-store"
|
|
16193
|
+
});
|
|
16194
|
+
res.end(JSON.stringify(payload));
|
|
16195
|
+
}
|
|
16196
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
16197
|
+
if (raw === null || raw === "") return defaultValue;
|
|
16198
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16199
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
16200
|
+
return defaultValue;
|
|
16201
|
+
}
|
|
16202
|
+
return Math.min(parsed, max);
|
|
16203
|
+
}
|
|
16204
|
+
function isStatusFilter(value) {
|
|
16205
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
16206
|
+
}
|
|
16207
|
+
function matchEntryRoute(path) {
|
|
16208
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
16209
|
+
if (!path.startsWith(prefix)) return null;
|
|
16210
|
+
const rest = path.slice(prefix.length);
|
|
16211
|
+
if (rest.length === 0) return null;
|
|
16212
|
+
const slash = rest.indexOf("/");
|
|
16213
|
+
if (slash === -1) {
|
|
16214
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
16215
|
+
}
|
|
16216
|
+
return {
|
|
16217
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
16218
|
+
action: rest.slice(slash + 1)
|
|
16219
|
+
};
|
|
16220
|
+
}
|
|
16221
|
+
async function handleStream2(deps, res) {
|
|
16222
|
+
res.writeHead(200, {
|
|
16223
|
+
"Content-Type": "text/event-stream",
|
|
16224
|
+
"Cache-Control": "no-cache, no-transform",
|
|
16225
|
+
Connection: "keep-alive",
|
|
16226
|
+
"X-Accel-Buffering": "no"
|
|
16227
|
+
});
|
|
16228
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
16229
|
+
res.write(
|
|
16230
|
+
`event: approval_inbox_snapshot
|
|
16231
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
16232
|
+
|
|
16233
|
+
`
|
|
16234
|
+
);
|
|
16235
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
16236
|
+
try {
|
|
16237
|
+
res.write(
|
|
16238
|
+
`event: approval_inbox_${event.type}
|
|
16239
|
+
data: ${JSON.stringify(event.entry)}
|
|
16240
|
+
|
|
16241
|
+
`
|
|
16242
|
+
);
|
|
16243
|
+
} catch {
|
|
16244
|
+
}
|
|
16245
|
+
});
|
|
16246
|
+
const keepAlive = setInterval(() => {
|
|
16247
|
+
try {
|
|
16248
|
+
res.write(": keepalive\n\n");
|
|
16249
|
+
} catch {
|
|
16250
|
+
}
|
|
16251
|
+
}, 25e3);
|
|
16252
|
+
const cleanup = () => {
|
|
16253
|
+
clearInterval(keepAlive);
|
|
16254
|
+
unsubscribe();
|
|
16255
|
+
};
|
|
16256
|
+
res.on("close", cleanup);
|
|
16257
|
+
res.on("error", cleanup);
|
|
16258
|
+
}
|
|
16259
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
16260
|
+
const host = req.headers.host || "localhost";
|
|
16261
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
16262
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
16263
|
+
const path = url.pathname;
|
|
16264
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
16265
|
+
return false;
|
|
16266
|
+
}
|
|
16267
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
16268
|
+
if (!checkAuth(req, res, url)) return true;
|
|
16269
|
+
try {
|
|
16270
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
16271
|
+
await handleStream2(deps, res);
|
|
16272
|
+
return true;
|
|
16273
|
+
}
|
|
16274
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
16275
|
+
const limit = parseLimit2(
|
|
16276
|
+
url.searchParams.get("limit"),
|
|
16277
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
16278
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
16279
|
+
);
|
|
16280
|
+
const statusRaw = url.searchParams.get("status");
|
|
16281
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
16282
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
16283
|
+
const entries = await deps.aggregator.list({
|
|
16284
|
+
status,
|
|
16285
|
+
limit,
|
|
16286
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
16287
|
+
});
|
|
16288
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
16289
|
+
return true;
|
|
16290
|
+
}
|
|
16291
|
+
const entryMatch = matchEntryRoute(path);
|
|
16292
|
+
if (entryMatch === null) {
|
|
16293
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16294
|
+
return true;
|
|
16295
|
+
}
|
|
16296
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
16297
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
16298
|
+
const entry = entries.find(
|
|
16299
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
16300
|
+
);
|
|
16301
|
+
if (!entry) {
|
|
16302
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16303
|
+
return true;
|
|
16304
|
+
}
|
|
16305
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
16306
|
+
entryMatch.aggregatorId
|
|
16307
|
+
);
|
|
16308
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
16309
|
+
return true;
|
|
16310
|
+
}
|
|
16311
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
16312
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
16313
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16314
|
+
try {
|
|
16315
|
+
const entry = await deps.aggregator.resolve(
|
|
16316
|
+
entryMatch.aggregatorId,
|
|
16317
|
+
decision,
|
|
16318
|
+
operatorId
|
|
16319
|
+
);
|
|
16320
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
16321
|
+
} catch (err) {
|
|
16322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16323
|
+
if (msg === "approval-aggregator: not_found") {
|
|
16324
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16325
|
+
} else {
|
|
16326
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16327
|
+
}
|
|
16328
|
+
}
|
|
16329
|
+
return true;
|
|
16330
|
+
}
|
|
16331
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16332
|
+
return true;
|
|
16333
|
+
} catch (err) {
|
|
16334
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16335
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16336
|
+
return true;
|
|
16337
|
+
}
|
|
16338
|
+
}
|
|
16339
|
+
|
|
16080
16340
|
// src/principal-policy/dashboard.ts
|
|
16081
16341
|
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16082
16342
|
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -16141,6 +16401,14 @@ var DashboardApprovalChannel = class {
|
|
|
16141
16401
|
* regardless. Default route flip is deferred to v1.2.
|
|
16142
16402
|
*/
|
|
16143
16403
|
v11Bindings = null;
|
|
16404
|
+
/**
|
|
16405
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
16406
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
16407
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
16408
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
16409
|
+
* the operator-facing query / decision surface.
|
|
16410
|
+
*/
|
|
16411
|
+
approvalAggregator = null;
|
|
16144
16412
|
constructor(config) {
|
|
16145
16413
|
this.config = config;
|
|
16146
16414
|
this.authToken = config.auth_token;
|
|
@@ -16191,6 +16459,34 @@ var DashboardApprovalChannel = class {
|
|
|
16191
16459
|
setV11Bindings(bindings) {
|
|
16192
16460
|
this.v11Bindings = bindings;
|
|
16193
16461
|
}
|
|
16462
|
+
/**
|
|
16463
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
16464
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
16465
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
16466
|
+
* tests + during shutdown).
|
|
16467
|
+
*/
|
|
16468
|
+
setApprovalAggregator(aggregator) {
|
|
16469
|
+
this.approvalAggregator = aggregator;
|
|
16470
|
+
}
|
|
16471
|
+
/**
|
|
16472
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
16473
|
+
* before the legacy approval route table. Returns true when served.
|
|
16474
|
+
*/
|
|
16475
|
+
async dispatchApprovalInbox(req, res) {
|
|
16476
|
+
if (!this.approvalAggregator) return false;
|
|
16477
|
+
return handleApprovalInboxRoute(
|
|
16478
|
+
{
|
|
16479
|
+
authConfig: {
|
|
16480
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
16481
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
16482
|
+
},
|
|
16483
|
+
aggregator: this.approvalAggregator,
|
|
16484
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
16485
|
+
},
|
|
16486
|
+
req,
|
|
16487
|
+
res
|
|
16488
|
+
);
|
|
16489
|
+
}
|
|
16194
16490
|
/**
|
|
16195
16491
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
16196
16492
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -16566,6 +16862,18 @@ var DashboardApprovalChannel = class {
|
|
|
16566
16862
|
res.end();
|
|
16567
16863
|
return;
|
|
16568
16864
|
}
|
|
16865
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
16866
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
16867
|
+
if (handled) return;
|
|
16868
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
16869
|
+
}).catch(() => {
|
|
16870
|
+
if (!res.headersSent) {
|
|
16871
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
16872
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
16873
|
+
}
|
|
16874
|
+
});
|
|
16875
|
+
return;
|
|
16876
|
+
}
|
|
16569
16877
|
if (this.v11Bindings) {
|
|
16570
16878
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
16571
16879
|
if (handled) return;
|
|
@@ -18584,14 +18892,25 @@ var ApprovalGate = class {
|
|
|
18584
18892
|
auditLog;
|
|
18585
18893
|
injectionDetector;
|
|
18586
18894
|
onInjectionAlert;
|
|
18895
|
+
onApprovalEvent;
|
|
18587
18896
|
proxyTierResolver;
|
|
18588
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
18897
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
18589
18898
|
this.policy = policy;
|
|
18590
18899
|
this.baseline = baseline;
|
|
18591
18900
|
this.channel = channel;
|
|
18592
18901
|
this.auditLog = auditLog;
|
|
18593
18902
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
18594
18903
|
this.onInjectionAlert = onInjectionAlert;
|
|
18904
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
18905
|
+
}
|
|
18906
|
+
/**
|
|
18907
|
+
* Set the approval-event callback after construction. Used by the
|
|
18908
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
18909
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
18910
|
+
* constructor so existing call sites continue to work unchanged.
|
|
18911
|
+
*/
|
|
18912
|
+
setApprovalEventCallback(cb) {
|
|
18913
|
+
this.onApprovalEvent = cb;
|
|
18595
18914
|
}
|
|
18596
18915
|
/**
|
|
18597
18916
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -18825,21 +19144,105 @@ var ApprovalGate = class {
|
|
|
18825
19144
|
}
|
|
18826
19145
|
/**
|
|
18827
19146
|
* Request approval from the human principal.
|
|
19147
|
+
*
|
|
19148
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
19149
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
19150
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
19151
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
19152
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
19153
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
18828
19154
|
*/
|
|
18829
19155
|
async requestApproval(operation, tier, reason, context) {
|
|
19156
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
18830
19157
|
const request = {
|
|
18831
19158
|
operation,
|
|
18832
19159
|
tier,
|
|
18833
19160
|
reason,
|
|
18834
19161
|
context,
|
|
18835
|
-
timestamp:
|
|
19162
|
+
timestamp: requestTimestamp
|
|
18836
19163
|
};
|
|
18837
|
-
const
|
|
19164
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
19165
|
+
if (this.onApprovalEvent) {
|
|
19166
|
+
try {
|
|
19167
|
+
this.onApprovalEvent({
|
|
19168
|
+
phase: "requested",
|
|
19169
|
+
operation,
|
|
19170
|
+
tier,
|
|
19171
|
+
reason,
|
|
19172
|
+
context,
|
|
19173
|
+
request_timestamp: requestTimestamp,
|
|
19174
|
+
correlation_id: correlationId
|
|
19175
|
+
});
|
|
19176
|
+
} catch {
|
|
19177
|
+
}
|
|
19178
|
+
}
|
|
19179
|
+
let response;
|
|
19180
|
+
try {
|
|
19181
|
+
response = await this.channel.requestApproval(request);
|
|
19182
|
+
} catch (err) {
|
|
19183
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
19184
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19185
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
19186
|
+
tier,
|
|
19187
|
+
reason,
|
|
19188
|
+
decided_by: "channel_failure",
|
|
19189
|
+
channel_error: errMessage
|
|
19190
|
+
});
|
|
19191
|
+
if (this.onApprovalEvent) {
|
|
19192
|
+
try {
|
|
19193
|
+
this.onApprovalEvent({
|
|
19194
|
+
phase: "resolved",
|
|
19195
|
+
operation,
|
|
19196
|
+
tier,
|
|
19197
|
+
reason,
|
|
19198
|
+
context,
|
|
19199
|
+
request_timestamp: requestTimestamp,
|
|
19200
|
+
resolution: {
|
|
19201
|
+
decision: "deny",
|
|
19202
|
+
decided_at: decidedAt,
|
|
19203
|
+
decided_by: "channel_failure"
|
|
19204
|
+
},
|
|
19205
|
+
correlation_id: correlationId
|
|
19206
|
+
});
|
|
19207
|
+
} catch {
|
|
19208
|
+
}
|
|
19209
|
+
}
|
|
19210
|
+
return {
|
|
19211
|
+
allowed: false,
|
|
19212
|
+
tier,
|
|
19213
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
19214
|
+
approval_required: true,
|
|
19215
|
+
approval_response: {
|
|
19216
|
+
decision: "deny",
|
|
19217
|
+
decided_at: decidedAt,
|
|
19218
|
+
decided_by: "channel_failure"
|
|
19219
|
+
}
|
|
19220
|
+
};
|
|
19221
|
+
}
|
|
18838
19222
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
18839
19223
|
tier,
|
|
18840
19224
|
reason,
|
|
18841
19225
|
decided_by: response.decided_by
|
|
18842
19226
|
});
|
|
19227
|
+
if (this.onApprovalEvent) {
|
|
19228
|
+
try {
|
|
19229
|
+
this.onApprovalEvent({
|
|
19230
|
+
phase: "resolved",
|
|
19231
|
+
operation,
|
|
19232
|
+
tier,
|
|
19233
|
+
reason,
|
|
19234
|
+
context,
|
|
19235
|
+
request_timestamp: requestTimestamp,
|
|
19236
|
+
resolution: {
|
|
19237
|
+
decision: response.decision,
|
|
19238
|
+
decided_at: response.decided_at,
|
|
19239
|
+
decided_by: response.decided_by
|
|
19240
|
+
},
|
|
19241
|
+
correlation_id: correlationId
|
|
19242
|
+
});
|
|
19243
|
+
} catch {
|
|
19244
|
+
}
|
|
19245
|
+
}
|
|
18843
19246
|
return {
|
|
18844
19247
|
allowed: response.decision === "approve",
|
|
18845
19248
|
tier,
|
|
@@ -18873,6 +19276,352 @@ var ApprovalGate = class {
|
|
|
18873
19276
|
}
|
|
18874
19277
|
};
|
|
18875
19278
|
|
|
19279
|
+
// src/principal-policy/approval-aggregator.ts
|
|
19280
|
+
init_encryption();
|
|
19281
|
+
init_encoding();
|
|
19282
|
+
var APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
19283
|
+
var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
19284
|
+
var APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
19285
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
19286
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
19287
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
19288
|
+
};
|
|
19289
|
+
var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
19290
|
+
var DEFAULT_MAX_LIST_LIMIT = 200;
|
|
19291
|
+
var DEFAULT_LIST_PAGE_SIZE = 50;
|
|
19292
|
+
var ApprovalAggregator = class {
|
|
19293
|
+
storage;
|
|
19294
|
+
encryptionKey;
|
|
19295
|
+
auditLog;
|
|
19296
|
+
identityId;
|
|
19297
|
+
fortressId;
|
|
19298
|
+
pendingTtlMs;
|
|
19299
|
+
maxListLimit;
|
|
19300
|
+
now;
|
|
19301
|
+
resolveSourceContext;
|
|
19302
|
+
resolveHubInboxItemId;
|
|
19303
|
+
/** Cached entries by `aggregator_id`. */
|
|
19304
|
+
entries = /* @__PURE__ */ new Map();
|
|
19305
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
19306
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
19307
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
19308
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
19309
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
19310
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
19311
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
19312
|
+
hydrated = false;
|
|
19313
|
+
/** Active SSE listeners. */
|
|
19314
|
+
listeners = /* @__PURE__ */ new Set();
|
|
19315
|
+
constructor(deps) {
|
|
19316
|
+
this.storage = deps.storage;
|
|
19317
|
+
this.encryptionKey = derivePurposeKey(
|
|
19318
|
+
deps.masterKey,
|
|
19319
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
19320
|
+
);
|
|
19321
|
+
this.auditLog = deps.auditLog;
|
|
19322
|
+
this.identityId = deps.identityId;
|
|
19323
|
+
this.fortressId = deps.fortressId;
|
|
19324
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
19325
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
19326
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
19327
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
19328
|
+
source_harness: this.fortressId,
|
|
19329
|
+
source_agent_id: this.fortressId
|
|
19330
|
+
}));
|
|
19331
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
19332
|
+
}
|
|
19333
|
+
/**
|
|
19334
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
19335
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
19336
|
+
*/
|
|
19337
|
+
onEvent(listener) {
|
|
19338
|
+
this.listeners.add(listener);
|
|
19339
|
+
return () => this.listeners.delete(listener);
|
|
19340
|
+
}
|
|
19341
|
+
/**
|
|
19342
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
19343
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
19344
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
19345
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
19346
|
+
* a record for).
|
|
19347
|
+
*/
|
|
19348
|
+
async ingest(event) {
|
|
19349
|
+
await this.hydrate();
|
|
19350
|
+
if (event.phase === "requested") {
|
|
19351
|
+
return this.ingestRequested(event);
|
|
19352
|
+
}
|
|
19353
|
+
if (event.phase === "resolved") {
|
|
19354
|
+
return this.ingestResolved(event);
|
|
19355
|
+
}
|
|
19356
|
+
return null;
|
|
19357
|
+
}
|
|
19358
|
+
/**
|
|
19359
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
19360
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
19361
|
+
* snapshot is returned.
|
|
19362
|
+
*/
|
|
19363
|
+
async list(opts) {
|
|
19364
|
+
await this.hydrate();
|
|
19365
|
+
await this.expireStale();
|
|
19366
|
+
const limit = Math.min(
|
|
19367
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
19368
|
+
this.maxListLimit
|
|
19369
|
+
);
|
|
19370
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
19371
|
+
const matching = [];
|
|
19372
|
+
for (const entry of this.entries.values()) {
|
|
19373
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
19374
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
19375
|
+
matching.push(entry);
|
|
19376
|
+
}
|
|
19377
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
19378
|
+
return matching.slice(0, limit);
|
|
19379
|
+
}
|
|
19380
|
+
/**
|
|
19381
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
19382
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
19383
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
19384
|
+
*/
|
|
19385
|
+
async getFullPayload(aggregatorId) {
|
|
19386
|
+
await this.hydrate();
|
|
19387
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
19388
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
19389
|
+
}
|
|
19390
|
+
/**
|
|
19391
|
+
* Resolve an entry. Used by both:
|
|
19392
|
+
* 1. The gate wire-up on channel-decision return.
|
|
19393
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
19394
|
+
*
|
|
19395
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
19396
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
19397
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
19398
|
+
* routes return 404.
|
|
19399
|
+
*/
|
|
19400
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
19401
|
+
await this.hydrate();
|
|
19402
|
+
const entry = this.entries.get(aggregatorId);
|
|
19403
|
+
if (!entry) {
|
|
19404
|
+
throw new Error("approval-aggregator: not_found");
|
|
19405
|
+
}
|
|
19406
|
+
if (entry.status !== "pending") {
|
|
19407
|
+
return entry;
|
|
19408
|
+
}
|
|
19409
|
+
entry.status = decision;
|
|
19410
|
+
entry.resolved_at = this.now().toISOString();
|
|
19411
|
+
entry.resolved_by = operatorId;
|
|
19412
|
+
await this.persist(entry);
|
|
19413
|
+
this.auditLog.append(
|
|
19414
|
+
"l2",
|
|
19415
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19416
|
+
this.identityId,
|
|
19417
|
+
{
|
|
19418
|
+
aggregator_id: entry.aggregator_id,
|
|
19419
|
+
source_harness: entry.source_harness,
|
|
19420
|
+
source_agent_id: entry.source_agent_id,
|
|
19421
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19422
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19423
|
+
decision,
|
|
19424
|
+
decided_by: operatorId,
|
|
19425
|
+
decided_at: entry.resolved_at
|
|
19426
|
+
}
|
|
19427
|
+
);
|
|
19428
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19429
|
+
return entry;
|
|
19430
|
+
}
|
|
19431
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
19432
|
+
async ingestRequested(event) {
|
|
19433
|
+
const ctx = this.resolveSourceContext(event);
|
|
19434
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
19435
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
19436
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
19437
|
+
if (existing) {
|
|
19438
|
+
const existingEntry = this.entries.get(existing);
|
|
19439
|
+
if (existingEntry) {
|
|
19440
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
19441
|
+
this.auditLog.append(
|
|
19442
|
+
"l2",
|
|
19443
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
19444
|
+
this.identityId,
|
|
19445
|
+
{
|
|
19446
|
+
aggregator_id: existing,
|
|
19447
|
+
source_harness: ctx.source_harness,
|
|
19448
|
+
source_agent_id: ctx.source_agent_id,
|
|
19449
|
+
audit_log_entry_id: auditId,
|
|
19450
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19451
|
+
correlation_id: event.correlation_id
|
|
19452
|
+
}
|
|
19453
|
+
);
|
|
19454
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
19455
|
+
return null;
|
|
19456
|
+
}
|
|
19457
|
+
}
|
|
19458
|
+
const id = crypto.randomUUID();
|
|
19459
|
+
const now = this.now();
|
|
19460
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
19461
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
19462
|
+
const entry = {
|
|
19463
|
+
aggregator_id: id,
|
|
19464
|
+
source_harness: ctx.source_harness,
|
|
19465
|
+
source_agent_id: ctx.source_agent_id,
|
|
19466
|
+
audit_log_entry_id: auditId,
|
|
19467
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19468
|
+
action_summary: this.deriveActionSummary(event),
|
|
19469
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
19470
|
+
status: "pending",
|
|
19471
|
+
created_at: now.toISOString(),
|
|
19472
|
+
expires_at: expires.toISOString(),
|
|
19473
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19474
|
+
};
|
|
19475
|
+
this.entries.set(id, entry);
|
|
19476
|
+
this.dedupIndex.set(dedupKey, id);
|
|
19477
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
19478
|
+
this.fullPayloads.set(id, event.context);
|
|
19479
|
+
await this.persist(entry);
|
|
19480
|
+
this.auditLog.append(
|
|
19481
|
+
"l2",
|
|
19482
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
19483
|
+
this.identityId,
|
|
19484
|
+
{
|
|
19485
|
+
aggregator_id: id,
|
|
19486
|
+
source_harness: ctx.source_harness,
|
|
19487
|
+
source_agent_id: ctx.source_agent_id,
|
|
19488
|
+
audit_log_entry_id: auditId,
|
|
19489
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19490
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19491
|
+
}
|
|
19492
|
+
);
|
|
19493
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
19494
|
+
return entry;
|
|
19495
|
+
}
|
|
19496
|
+
async ingestResolved(event) {
|
|
19497
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
19498
|
+
if (!id) return null;
|
|
19499
|
+
const entry = this.entries.get(id);
|
|
19500
|
+
if (!entry) return null;
|
|
19501
|
+
if (entry.status !== "pending") return entry;
|
|
19502
|
+
if (!event.resolution) return entry;
|
|
19503
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
19504
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
19505
|
+
entry.status = status;
|
|
19506
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
19507
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
19508
|
+
await this.persist(entry);
|
|
19509
|
+
this.auditLog.append(
|
|
19510
|
+
"l2",
|
|
19511
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19512
|
+
this.identityId,
|
|
19513
|
+
{
|
|
19514
|
+
aggregator_id: id,
|
|
19515
|
+
source_harness: entry.source_harness,
|
|
19516
|
+
source_agent_id: entry.source_agent_id,
|
|
19517
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19518
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19519
|
+
decision: status,
|
|
19520
|
+
decided_by: entry.resolved_by,
|
|
19521
|
+
decided_at: entry.resolved_at,
|
|
19522
|
+
fail_closed: failClosed
|
|
19523
|
+
}
|
|
19524
|
+
);
|
|
19525
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19526
|
+
return entry;
|
|
19527
|
+
}
|
|
19528
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
19529
|
+
/**
|
|
19530
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
19531
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
19532
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
19533
|
+
* the audit entry the gate appended on the same call.
|
|
19534
|
+
*/
|
|
19535
|
+
auditEntryIdForEvent(event) {
|
|
19536
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
19537
|
+
}
|
|
19538
|
+
derivePolicyRuleId(event) {
|
|
19539
|
+
return `tier${event.tier}:${event.operation}`;
|
|
19540
|
+
}
|
|
19541
|
+
deriveActionSummary(event) {
|
|
19542
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
19543
|
+
}
|
|
19544
|
+
/**
|
|
19545
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
19546
|
+
* identical payloads always hash the same, even when key insertion order
|
|
19547
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
19548
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
19549
|
+
*/
|
|
19550
|
+
hashPayload(payload) {
|
|
19551
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
19552
|
+
return crypto.createHash("sha256").update(canonical).digest("hex");
|
|
19553
|
+
}
|
|
19554
|
+
emit(event) {
|
|
19555
|
+
for (const listener of this.listeners) {
|
|
19556
|
+
try {
|
|
19557
|
+
listener(event);
|
|
19558
|
+
} catch {
|
|
19559
|
+
}
|
|
19560
|
+
}
|
|
19561
|
+
}
|
|
19562
|
+
async expireStale() {
|
|
19563
|
+
const nowMs = this.now().getTime();
|
|
19564
|
+
for (const entry of this.entries.values()) {
|
|
19565
|
+
if (entry.status !== "pending") continue;
|
|
19566
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
19567
|
+
entry.status = "expired";
|
|
19568
|
+
entry.resolved_at = this.now().toISOString();
|
|
19569
|
+
entry.resolved_by = "system_ttl";
|
|
19570
|
+
await this.persist(entry);
|
|
19571
|
+
this.auditLog.append(
|
|
19572
|
+
"l2",
|
|
19573
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19574
|
+
this.identityId,
|
|
19575
|
+
{
|
|
19576
|
+
aggregator_id: entry.aggregator_id,
|
|
19577
|
+
source_harness: entry.source_harness,
|
|
19578
|
+
source_agent_id: entry.source_agent_id,
|
|
19579
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19580
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19581
|
+
decision: "expired",
|
|
19582
|
+
decided_by: "system_ttl",
|
|
19583
|
+
decided_at: entry.resolved_at
|
|
19584
|
+
}
|
|
19585
|
+
);
|
|
19586
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19587
|
+
}
|
|
19588
|
+
}
|
|
19589
|
+
async persist(entry) {
|
|
19590
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
19591
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
19592
|
+
await this.storage.write(
|
|
19593
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19594
|
+
entry.aggregator_id,
|
|
19595
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
19596
|
+
);
|
|
19597
|
+
}
|
|
19598
|
+
async hydrate() {
|
|
19599
|
+
if (this.hydrated) return;
|
|
19600
|
+
this.hydrated = true;
|
|
19601
|
+
try {
|
|
19602
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
19603
|
+
for (const meta of metas) {
|
|
19604
|
+
const raw = await this.storage.read(
|
|
19605
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19606
|
+
meta.key
|
|
19607
|
+
);
|
|
19608
|
+
if (!raw) continue;
|
|
19609
|
+
try {
|
|
19610
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
19611
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
19612
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
19613
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
19614
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
19615
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
19616
|
+
} catch {
|
|
19617
|
+
}
|
|
19618
|
+
}
|
|
19619
|
+
} catch {
|
|
19620
|
+
this.hydrated = false;
|
|
19621
|
+
}
|
|
19622
|
+
}
|
|
19623
|
+
};
|
|
19624
|
+
|
|
18876
19625
|
// src/principal-policy/tools.ts
|
|
18877
19626
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
18878
19627
|
return [
|
|
@@ -21725,6 +22474,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
21725
22474
|
}
|
|
21726
22475
|
async function consumeResetHistoryMarker(options) {
|
|
21727
22476
|
const markerPath = path.join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
22477
|
+
const consumedPath = markerPath + ".consumed";
|
|
22478
|
+
if (await fileExists3(consumedPath)) {
|
|
22479
|
+
await promises.rm(markerPath, { force: true });
|
|
22480
|
+
await promises.rm(consumedPath, { force: true });
|
|
22481
|
+
return { emitted: 0, markerPath };
|
|
22482
|
+
}
|
|
21728
22483
|
if (!await fileExists3(markerPath)) {
|
|
21729
22484
|
return { emitted: 0, markerPath };
|
|
21730
22485
|
}
|
|
@@ -21749,7 +22504,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
21749
22504
|
});
|
|
21750
22505
|
}
|
|
21751
22506
|
await options.auditLog.flush();
|
|
22507
|
+
await promises.writeFile(consumedPath, "", "utf-8");
|
|
21752
22508
|
await promises.rm(markerPath, { force: true });
|
|
22509
|
+
await promises.rm(consumedPath, { force: true });
|
|
21753
22510
|
return { emitted: markers.length, markerHash, markerPath };
|
|
21754
22511
|
}
|
|
21755
22512
|
async function fileExists3(path) {
|
|
@@ -30695,6 +31452,36 @@ var HubService = class {
|
|
|
30695
31452
|
const chat = this.requireOperatorChat();
|
|
30696
31453
|
return chat.getConciergeHistory();
|
|
30697
31454
|
}
|
|
31455
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
31456
|
+
/**
|
|
31457
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
31458
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
31459
|
+
* surface is unavailable on a given fortress.
|
|
31460
|
+
*/
|
|
31461
|
+
hasConciergeMemory() {
|
|
31462
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
31463
|
+
}
|
|
31464
|
+
async listConciergeMemoryThreads(opts) {
|
|
31465
|
+
const chat = this.requireOperatorChat();
|
|
31466
|
+
if (!chat.hasConciergeMemory()) {
|
|
31467
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31468
|
+
}
|
|
31469
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
31470
|
+
}
|
|
31471
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
31472
|
+
const chat = this.requireOperatorChat();
|
|
31473
|
+
if (!chat.hasConciergeMemory()) {
|
|
31474
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31475
|
+
}
|
|
31476
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
31477
|
+
}
|
|
31478
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
31479
|
+
const chat = this.requireOperatorChat();
|
|
31480
|
+
if (!chat.hasConciergeMemory()) {
|
|
31481
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31482
|
+
}
|
|
31483
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
31484
|
+
}
|
|
30698
31485
|
/**
|
|
30699
31486
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
30700
31487
|
* panel surfaces recent activity routed through this agent, pending
|
|
@@ -30782,7 +31569,21 @@ init_encoding();
|
|
|
30782
31569
|
|
|
30783
31570
|
// src/chat/operator-chat-audit-events.ts
|
|
30784
31571
|
var OPERATOR_CHAT_OPS = {
|
|
30785
|
-
CONCIERGE_CHAT: "operator_concierge_chat"
|
|
31572
|
+
CONCIERGE_CHAT: "operator_concierge_chat",
|
|
31573
|
+
/**
|
|
31574
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
31575
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
31576
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
31577
|
+
* raw turn content never crosses the audit surface.
|
|
31578
|
+
*/
|
|
31579
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
31580
|
+
/**
|
|
31581
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
31582
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
31583
|
+
* the deleted bundle.
|
|
31584
|
+
*/
|
|
31585
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
31586
|
+
};
|
|
30786
31587
|
|
|
30787
31588
|
// src/chat/operator-chat-types.ts
|
|
30788
31589
|
var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
|
|
@@ -30825,6 +31626,14 @@ var OperatorChatService = class {
|
|
|
30825
31626
|
contextProviders;
|
|
30826
31627
|
piiFilter;
|
|
30827
31628
|
conciergeMaxTokens;
|
|
31629
|
+
memory;
|
|
31630
|
+
/**
|
|
31631
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
31632
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
31633
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
31634
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
31635
|
+
*/
|
|
31636
|
+
activeMemoryThreadId;
|
|
30828
31637
|
constructor(deps) {
|
|
30829
31638
|
this.store = deps.store;
|
|
30830
31639
|
this.auditLog = deps.auditLog;
|
|
@@ -30835,6 +31644,7 @@ var OperatorChatService = class {
|
|
|
30835
31644
|
}
|
|
30836
31645
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
30837
31646
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
31647
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
30838
31648
|
}
|
|
30839
31649
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
30840
31650
|
/**
|
|
@@ -30865,6 +31675,11 @@ var OperatorChatService = class {
|
|
|
30865
31675
|
CONCIERGE_THREAD_KEY,
|
|
30866
31676
|
operatorMessage
|
|
30867
31677
|
);
|
|
31678
|
+
if (this.memory) {
|
|
31679
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
31680
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
31681
|
+
});
|
|
31682
|
+
}
|
|
30868
31683
|
const start = Date.now();
|
|
30869
31684
|
let conciergeBody;
|
|
30870
31685
|
let servedBy = "disabled";
|
|
@@ -30920,6 +31735,11 @@ var OperatorChatService = class {
|
|
|
30920
31735
|
CONCIERGE_THREAD_KEY,
|
|
30921
31736
|
responseMessage
|
|
30922
31737
|
);
|
|
31738
|
+
if (this.memory) {
|
|
31739
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
31740
|
+
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
|
|
31741
|
+
});
|
|
31742
|
+
}
|
|
30923
31743
|
const payload = {
|
|
30924
31744
|
version: "1.2",
|
|
30925
31745
|
event_id: makeEventId("conc"),
|
|
@@ -30952,6 +31772,105 @@ var OperatorChatService = class {
|
|
|
30952
31772
|
);
|
|
30953
31773
|
return thread ? thread.messages : [];
|
|
30954
31774
|
}
|
|
31775
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
31776
|
+
/**
|
|
31777
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
31778
|
+
* 503 cleanly when called against an unwired service.
|
|
31779
|
+
*/
|
|
31780
|
+
hasConciergeMemory() {
|
|
31781
|
+
return this.memory !== void 0;
|
|
31782
|
+
}
|
|
31783
|
+
/**
|
|
31784
|
+
* List concierge memory threads, newest-first. Emits the
|
|
31785
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
31786
|
+
*/
|
|
31787
|
+
async listConciergeMemoryThreads(opts) {
|
|
31788
|
+
if (!this.memory) {
|
|
31789
|
+
throw new Error("concierge memory store not configured");
|
|
31790
|
+
}
|
|
31791
|
+
const summaries = await this.memory.listThreads(opts);
|
|
31792
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
31793
|
+
const payload = {
|
|
31794
|
+
version: "1.2",
|
|
31795
|
+
event_id: makeEventId("conc-hist"),
|
|
31796
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31797
|
+
identity_id: this.identityId,
|
|
31798
|
+
kind: "operator_concierge_history_read",
|
|
31799
|
+
surface: "concierge",
|
|
31800
|
+
thread_id: "*",
|
|
31801
|
+
turn_count: totalTurns
|
|
31802
|
+
};
|
|
31803
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
31804
|
+
return summaries;
|
|
31805
|
+
}
|
|
31806
|
+
/**
|
|
31807
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
31808
|
+
* `operator_concierge_history_read` audit event with the named
|
|
31809
|
+
* thread_id and the count of turns surfaced.
|
|
31810
|
+
*/
|
|
31811
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
31812
|
+
if (!this.memory) {
|
|
31813
|
+
throw new Error("concierge memory store not configured");
|
|
31814
|
+
}
|
|
31815
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
31816
|
+
const payload = {
|
|
31817
|
+
version: "1.2",
|
|
31818
|
+
event_id: makeEventId("conc-hist"),
|
|
31819
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31820
|
+
identity_id: this.identityId,
|
|
31821
|
+
kind: "operator_concierge_history_read",
|
|
31822
|
+
surface: "concierge",
|
|
31823
|
+
thread_id: threadId,
|
|
31824
|
+
turn_count: turns.length
|
|
31825
|
+
};
|
|
31826
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
31827
|
+
return turns;
|
|
31828
|
+
}
|
|
31829
|
+
/**
|
|
31830
|
+
* Delete a concierge memory thread. Emits
|
|
31831
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
31832
|
+
* removed; absent threads return false without an audit event.
|
|
31833
|
+
*/
|
|
31834
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
31835
|
+
if (!this.memory) {
|
|
31836
|
+
throw new Error("concierge memory store not configured");
|
|
31837
|
+
}
|
|
31838
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
31839
|
+
if (turnsBefore.length === 0) {
|
|
31840
|
+
return await this.memory.deleteThread(threadId);
|
|
31841
|
+
}
|
|
31842
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
31843
|
+
if (!removed) return false;
|
|
31844
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
31845
|
+
this.activeMemoryThreadId = void 0;
|
|
31846
|
+
}
|
|
31847
|
+
const payload = {
|
|
31848
|
+
version: "1.2",
|
|
31849
|
+
event_id: makeEventId("conc-del"),
|
|
31850
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31851
|
+
identity_id: this.identityId,
|
|
31852
|
+
kind: "operator_concierge_thread_deleted",
|
|
31853
|
+
surface: "concierge",
|
|
31854
|
+
thread_id: threadId,
|
|
31855
|
+
turn_count: turnsBefore.length
|
|
31856
|
+
};
|
|
31857
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
31858
|
+
return true;
|
|
31859
|
+
}
|
|
31860
|
+
/**
|
|
31861
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
31862
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
31863
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
31864
|
+
*/
|
|
31865
|
+
resetConciergeMemoryThread() {
|
|
31866
|
+
this.activeMemoryThreadId = void 0;
|
|
31867
|
+
}
|
|
31868
|
+
ensureActiveMemoryThread() {
|
|
31869
|
+
if (!this.activeMemoryThreadId) {
|
|
31870
|
+
this.activeMemoryThreadId = crypto.randomUUID();
|
|
31871
|
+
}
|
|
31872
|
+
return this.activeMemoryThreadId;
|
|
31873
|
+
}
|
|
30955
31874
|
/**
|
|
30956
31875
|
* Stitch fortress state into a single context blob the substrate
|
|
30957
31876
|
* folds into its summarization prompt.
|
|
@@ -31115,6 +32034,238 @@ var OperatorChatStore = class {
|
|
|
31115
32034
|
}
|
|
31116
32035
|
};
|
|
31117
32036
|
|
|
32037
|
+
// src/chat/concierge-memory-store.ts
|
|
32038
|
+
init_encryption();
|
|
32039
|
+
init_encoding();
|
|
32040
|
+
var CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
32041
|
+
var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
32042
|
+
var HKDF_INFO2 = "concierge-memory-store-v1";
|
|
32043
|
+
var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
32044
|
+
var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
32045
|
+
var ConciergeMemoryStore = class {
|
|
32046
|
+
storage;
|
|
32047
|
+
encryptionKey;
|
|
32048
|
+
fortressId;
|
|
32049
|
+
retentionDays;
|
|
32050
|
+
locks;
|
|
32051
|
+
constructor(opts) {
|
|
32052
|
+
this.storage = opts.storage;
|
|
32053
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
32054
|
+
this.fortressId = opts.fortressId;
|
|
32055
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
32056
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
32057
|
+
}
|
|
32058
|
+
/**
|
|
32059
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
32060
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
32061
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
32062
|
+
* monotonicity even under concurrent callers.
|
|
32063
|
+
*/
|
|
32064
|
+
async appendTurn(threadId, role, content) {
|
|
32065
|
+
return this.withLock(threadId, async () => {
|
|
32066
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
32067
|
+
const now = /* @__PURE__ */ new Date();
|
|
32068
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
32069
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
32070
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
32071
|
+
const turn = {
|
|
32072
|
+
thread_id: threadId,
|
|
32073
|
+
fortress_id: this.fortressId,
|
|
32074
|
+
turn_id: nextTurnId,
|
|
32075
|
+
role,
|
|
32076
|
+
content,
|
|
32077
|
+
created_at: now.toISOString(),
|
|
32078
|
+
retention_until: retentionUntil.toISOString()
|
|
32079
|
+
};
|
|
32080
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
32081
|
+
version: 1,
|
|
32082
|
+
thread_id: threadId,
|
|
32083
|
+
fortress_id: this.fortressId,
|
|
32084
|
+
created_at: now.toISOString(),
|
|
32085
|
+
turns: [turn]
|
|
32086
|
+
};
|
|
32087
|
+
await this.saveBundle(next);
|
|
32088
|
+
return turn;
|
|
32089
|
+
});
|
|
32090
|
+
}
|
|
32091
|
+
/**
|
|
32092
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
32093
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
32094
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
32095
|
+
*/
|
|
32096
|
+
async readThread(threadId, opts) {
|
|
32097
|
+
const bundle = await this.loadBundle(threadId);
|
|
32098
|
+
if (!bundle) return [];
|
|
32099
|
+
let turns = bundle.turns;
|
|
32100
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
32101
|
+
const cutoff = opts.sinceTurnId;
|
|
32102
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
32103
|
+
}
|
|
32104
|
+
if (opts?.limit !== void 0) {
|
|
32105
|
+
turns = turns.slice(0, opts.limit);
|
|
32106
|
+
}
|
|
32107
|
+
return turns;
|
|
32108
|
+
}
|
|
32109
|
+
/**
|
|
32110
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
32111
|
+
* Sorted newest-first by last_turn_at.
|
|
32112
|
+
*/
|
|
32113
|
+
async listThreads(opts) {
|
|
32114
|
+
const entries = await this.storage.list(
|
|
32115
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32116
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32117
|
+
);
|
|
32118
|
+
const summaries = [];
|
|
32119
|
+
for (const meta of entries) {
|
|
32120
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32121
|
+
if (threadId === null) continue;
|
|
32122
|
+
const bundle = await this.loadBundle(threadId);
|
|
32123
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
32124
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
32125
|
+
summaries.push({
|
|
32126
|
+
thread_id: bundle.thread_id,
|
|
32127
|
+
created_at: bundle.created_at,
|
|
32128
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
32129
|
+
turn_count: bundle.turns.length
|
|
32130
|
+
});
|
|
32131
|
+
}
|
|
32132
|
+
summaries.sort(
|
|
32133
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
32134
|
+
);
|
|
32135
|
+
if (opts?.limit !== void 0) {
|
|
32136
|
+
return summaries.slice(0, opts.limit);
|
|
32137
|
+
}
|
|
32138
|
+
return summaries;
|
|
32139
|
+
}
|
|
32140
|
+
/**
|
|
32141
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
32142
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
32143
|
+
* caller's responsibility.
|
|
32144
|
+
*/
|
|
32145
|
+
async deleteThread(threadId) {
|
|
32146
|
+
const key = bundleKey(threadId);
|
|
32147
|
+
return this.withLock(threadId, async () => {
|
|
32148
|
+
const existed = await this.storage.exists(
|
|
32149
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32150
|
+
key
|
|
32151
|
+
);
|
|
32152
|
+
if (!existed) return false;
|
|
32153
|
+
try {
|
|
32154
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32155
|
+
} catch {
|
|
32156
|
+
return false;
|
|
32157
|
+
}
|
|
32158
|
+
return true;
|
|
32159
|
+
});
|
|
32160
|
+
}
|
|
32161
|
+
/**
|
|
32162
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
32163
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
32164
|
+
*/
|
|
32165
|
+
async pruneExpired(now) {
|
|
32166
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
32167
|
+
const entries = await this.storage.list(
|
|
32168
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32169
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32170
|
+
);
|
|
32171
|
+
let pruned = 0;
|
|
32172
|
+
for (const meta of entries) {
|
|
32173
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32174
|
+
if (threadId === null) continue;
|
|
32175
|
+
pruned += await this.withLock(threadId, async () => {
|
|
32176
|
+
const bundle = await this.loadBundle(threadId);
|
|
32177
|
+
if (!bundle) return 0;
|
|
32178
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
32179
|
+
const dropped = bundle.turns.length - kept.length;
|
|
32180
|
+
if (dropped === 0) return 0;
|
|
32181
|
+
if (kept.length === 0) {
|
|
32182
|
+
await this.storage.delete(
|
|
32183
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32184
|
+
bundleKey(threadId)
|
|
32185
|
+
);
|
|
32186
|
+
} else {
|
|
32187
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
32188
|
+
}
|
|
32189
|
+
return dropped;
|
|
32190
|
+
});
|
|
32191
|
+
}
|
|
32192
|
+
return { pruned };
|
|
32193
|
+
}
|
|
32194
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
32195
|
+
async loadBundle(threadId) {
|
|
32196
|
+
const key = bundleKey(threadId);
|
|
32197
|
+
let raw;
|
|
32198
|
+
try {
|
|
32199
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32200
|
+
} catch {
|
|
32201
|
+
return null;
|
|
32202
|
+
}
|
|
32203
|
+
if (!raw) return null;
|
|
32204
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
32205
|
+
try {
|
|
32206
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
32207
|
+
const aad = stringToBytes(threadId);
|
|
32208
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
32209
|
+
const parsed = JSON.parse(
|
|
32210
|
+
bytesToString(plaintext)
|
|
32211
|
+
);
|
|
32212
|
+
if (parsed.version !== 1) return null;
|
|
32213
|
+
if (parsed.thread_id !== threadId) return null;
|
|
32214
|
+
return parsed;
|
|
32215
|
+
} catch {
|
|
32216
|
+
return null;
|
|
32217
|
+
}
|
|
32218
|
+
}
|
|
32219
|
+
async saveBundle(bundle) {
|
|
32220
|
+
const key = bundleKey(bundle.thread_id);
|
|
32221
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
32222
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
32223
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
32224
|
+
await this.storage.write(
|
|
32225
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32226
|
+
key,
|
|
32227
|
+
stringToBytes(JSON.stringify(envelope))
|
|
32228
|
+
);
|
|
32229
|
+
}
|
|
32230
|
+
/**
|
|
32231
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
32232
|
+
* once the task settles (success or failure). Generic helper so
|
|
32233
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
32234
|
+
*/
|
|
32235
|
+
async withLock(threadId, task) {
|
|
32236
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
32237
|
+
let release;
|
|
32238
|
+
const next = new Promise((resolve6) => {
|
|
32239
|
+
release = resolve6;
|
|
32240
|
+
});
|
|
32241
|
+
const chained = previous.then(() => next);
|
|
32242
|
+
this.locks.set(threadId, chained);
|
|
32243
|
+
try {
|
|
32244
|
+
await previous;
|
|
32245
|
+
return await task();
|
|
32246
|
+
} finally {
|
|
32247
|
+
release();
|
|
32248
|
+
if (this.locks.get(threadId) === chained) {
|
|
32249
|
+
this.locks.delete(threadId);
|
|
32250
|
+
}
|
|
32251
|
+
}
|
|
32252
|
+
}
|
|
32253
|
+
};
|
|
32254
|
+
function bundleKey(threadId) {
|
|
32255
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
32256
|
+
}
|
|
32257
|
+
function stripKeyPrefix(key) {
|
|
32258
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
32259
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
32260
|
+
}
|
|
32261
|
+
function lastTurnId(bundle) {
|
|
32262
|
+
let max = 0;
|
|
32263
|
+
for (const t of bundle.turns) {
|
|
32264
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
32265
|
+
}
|
|
32266
|
+
return max;
|
|
32267
|
+
}
|
|
32268
|
+
|
|
31118
32269
|
// src/dashboard/v1_1/wiring.ts
|
|
31119
32270
|
var CapabilityErrorAgentController = class {
|
|
31120
32271
|
fail(action) {
|
|
@@ -31153,6 +32304,14 @@ function buildV11Bindings(inputs) {
|
|
|
31153
32304
|
let operatorChatService;
|
|
31154
32305
|
if (inputs.storage && inputs.masterKey) {
|
|
31155
32306
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
32307
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
32308
|
+
storage: inputs.storage,
|
|
32309
|
+
masterKey: inputs.masterKey,
|
|
32310
|
+
fortressId: inputs.fortressId,
|
|
32311
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
32312
|
+
});
|
|
32313
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
32314
|
+
});
|
|
31156
32315
|
operatorChatService = new OperatorChatService({
|
|
31157
32316
|
store: chatStore,
|
|
31158
32317
|
auditLog: inputs.auditLog,
|
|
@@ -31163,7 +32322,8 @@ function buildV11Bindings(inputs) {
|
|
|
31163
32322
|
identityId: inputs.identityId,
|
|
31164
32323
|
registry
|
|
31165
32324
|
}),
|
|
31166
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
32325
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
32326
|
+
conciergeMemory
|
|
31167
32327
|
});
|
|
31168
32328
|
}
|
|
31169
32329
|
const hubService = new HubService({
|
|
@@ -31355,13 +32515,13 @@ init_encryption();
|
|
|
31355
32515
|
init_encoding();
|
|
31356
32516
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
31357
32517
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
31358
|
-
var
|
|
32518
|
+
var HKDF_INFO3 = "intelligence-substrate-config";
|
|
31359
32519
|
var IntelligenceConfigStore = class {
|
|
31360
32520
|
storage;
|
|
31361
32521
|
encryptionKey;
|
|
31362
32522
|
constructor(storage, masterKey) {
|
|
31363
32523
|
this.storage = storage;
|
|
31364
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
32524
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
31365
32525
|
}
|
|
31366
32526
|
/**
|
|
31367
32527
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -33500,7 +34660,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33500
34660
|
);
|
|
33501
34661
|
}
|
|
33502
34662
|
}
|
|
33503
|
-
const
|
|
34663
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
34664
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
34665
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
33504
34666
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
33505
34667
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
33506
34668
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -33509,6 +34671,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33509
34671
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
33510
34672
|
);
|
|
33511
34673
|
}
|
|
34674
|
+
let detailedFailureClass;
|
|
34675
|
+
if (identityFailed) {
|
|
34676
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
34677
|
+
} else if (reputationBundleFailed) {
|
|
34678
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
34679
|
+
} else if (reputationAttestationFailed) {
|
|
34680
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
34681
|
+
} else if (unverifiableFailed) {
|
|
34682
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
34683
|
+
}
|
|
33512
34684
|
return {
|
|
33513
34685
|
version: "1.1",
|
|
33514
34686
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -33528,7 +34700,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33528
34700
|
identity,
|
|
33529
34701
|
audit,
|
|
33530
34702
|
reputation,
|
|
33531
|
-
failure_class:
|
|
34703
|
+
failure_class: detailedFailureClass
|
|
33532
34704
|
};
|
|
33533
34705
|
}
|
|
33534
34706
|
|
|
@@ -34455,7 +35627,19 @@ async function runExitCommand(args) {
|
|
|
34455
35627
|
}
|
|
34456
35628
|
const config = await loadConfig();
|
|
34457
35629
|
const ctx = await openExitContext(argv, env);
|
|
34458
|
-
|
|
35630
|
+
let policy;
|
|
35631
|
+
try {
|
|
35632
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
35633
|
+
} catch (policyErr) {
|
|
35634
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
35635
|
+
write(err, `
|
|
35636
|
+
Sanctuary cannot proceed.
|
|
35637
|
+
${policyErr.message}
|
|
35638
|
+
`);
|
|
35639
|
+
return 1;
|
|
35640
|
+
}
|
|
35641
|
+
throw policyErr;
|
|
35642
|
+
}
|
|
34459
35643
|
const result = await exportExitBundle({
|
|
34460
35644
|
bundleDir: outDir,
|
|
34461
35645
|
storage: ctx.storage,
|
|
@@ -35107,7 +36291,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35107
36291
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
35108
36292
|
await profileStore.load();
|
|
35109
36293
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
35110
|
-
|
|
36294
|
+
let policy;
|
|
36295
|
+
try {
|
|
36296
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
36297
|
+
} catch (err) {
|
|
36298
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
36299
|
+
console.error(`
|
|
36300
|
+
Sanctuary cannot start.
|
|
36301
|
+
${err.message}
|
|
36302
|
+
`);
|
|
36303
|
+
process.exit(1);
|
|
36304
|
+
}
|
|
36305
|
+
throw err;
|
|
36306
|
+
}
|
|
35111
36307
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
35112
36308
|
await baseline.load();
|
|
35113
36309
|
let approvalChannel;
|
|
@@ -35205,6 +36401,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35205
36401
|
});
|
|
35206
36402
|
} : void 0;
|
|
35207
36403
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
36404
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
36405
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
36406
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
36407
|
+
storage,
|
|
36408
|
+
masterKey,
|
|
36409
|
+
auditLog,
|
|
36410
|
+
identityId: aggregatorIdentityId,
|
|
36411
|
+
fortressId: fortressIdForAggregator
|
|
36412
|
+
});
|
|
36413
|
+
gate.setApprovalEventCallback((event) => {
|
|
36414
|
+
void approvalAggregator.ingest(event);
|
|
36415
|
+
});
|
|
36416
|
+
if (dashboard) {
|
|
36417
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
36418
|
+
}
|
|
35208
36419
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
35209
36420
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
35210
36421
|
config,
|
|
@@ -35399,6 +36610,7 @@ exports.HERO_COPY = HERO_COPY;
|
|
|
35399
36610
|
exports.InMemoryModelProvenanceStore = InMemoryModelProvenanceStore;
|
|
35400
36611
|
exports.InjectionDetector = InjectionDetector;
|
|
35401
36612
|
exports.MODEL_PRESETS = MODEL_PRESETS;
|
|
36613
|
+
exports.MalformedPrincipalPolicyError = MalformedPrincipalPolicyError;
|
|
35402
36614
|
exports.MemoryStorage = MemoryStorage;
|
|
35403
36615
|
exports.PolicyStore = PolicyStore;
|
|
35404
36616
|
exports.ProxyRouter = ProxyRouter;
|