@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.js
CHANGED
|
@@ -4582,20 +4582,52 @@ approval_channel:
|
|
|
4582
4582
|
timeout_seconds: 300
|
|
4583
4583
|
`;
|
|
4584
4584
|
}
|
|
4585
|
+
var MalformedPrincipalPolicyError = class extends Error {
|
|
4586
|
+
constructor(policyPath, reason) {
|
|
4587
|
+
super(
|
|
4588
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4589
|
+
Reason: ${reason}
|
|
4590
|
+
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.`
|
|
4591
|
+
);
|
|
4592
|
+
this.policyPath = policyPath;
|
|
4593
|
+
this.reason = reason;
|
|
4594
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4595
|
+
}
|
|
4596
|
+
policyPath;
|
|
4597
|
+
reason;
|
|
4598
|
+
};
|
|
4585
4599
|
async function loadPrincipalPolicy(storagePath) {
|
|
4586
4600
|
const policyPath = join(storagePath, "principal-policy.yaml");
|
|
4601
|
+
let content;
|
|
4602
|
+
try {
|
|
4603
|
+
content = await readFile(policyPath, "utf-8");
|
|
4604
|
+
} catch (err) {
|
|
4605
|
+
const code = err?.code;
|
|
4606
|
+
if (code === "ENOENT") {
|
|
4607
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4608
|
+
try {
|
|
4609
|
+
await writeFile(policyPath, defaultYaml, "utf-8");
|
|
4610
|
+
await chmod(policyPath, 384);
|
|
4611
|
+
} catch (writeErr) {
|
|
4612
|
+
console.warn(
|
|
4613
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4614
|
+
);
|
|
4615
|
+
}
|
|
4616
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4617
|
+
}
|
|
4618
|
+
throw new MalformedPrincipalPolicyError(
|
|
4619
|
+
policyPath,
|
|
4620
|
+
`read failed: ${err.message}`
|
|
4621
|
+
);
|
|
4622
|
+
}
|
|
4587
4623
|
try {
|
|
4588
|
-
const content = await readFile(policyPath, "utf-8");
|
|
4589
4624
|
const policy = parsePolicy(content);
|
|
4590
4625
|
return Object.freeze(policy);
|
|
4591
|
-
} catch {
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
} catch {
|
|
4597
|
-
}
|
|
4598
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4626
|
+
} catch (parseErr) {
|
|
4627
|
+
throw new MalformedPrincipalPolicyError(
|
|
4628
|
+
policyPath,
|
|
4629
|
+
parseErr.message
|
|
4630
|
+
);
|
|
4599
4631
|
}
|
|
4600
4632
|
}
|
|
4601
4633
|
|
|
@@ -4822,7 +4854,7 @@ function deepSortKeys(obj) {
|
|
|
4822
4854
|
return sorted;
|
|
4823
4855
|
}
|
|
4824
4856
|
function canonicalizeForSigning(body) {
|
|
4825
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4857
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4826
4858
|
}
|
|
4827
4859
|
|
|
4828
4860
|
// src/shr/generator.ts
|
|
@@ -11644,6 +11676,16 @@ var HUB_ROUTES = {
|
|
|
11644
11676
|
*/
|
|
11645
11677
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
11646
11678
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
11679
|
+
/**
|
|
11680
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
11681
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
11682
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
11683
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
11684
|
+
* v1.3 conversational sovereignty depth.
|
|
11685
|
+
*/
|
|
11686
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
11687
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11688
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11647
11689
|
/**
|
|
11648
11690
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
11649
11691
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -11667,6 +11709,10 @@ var HUB_TIER_1_AGENT_CONTROL_ACTIONS = [
|
|
|
11667
11709
|
];
|
|
11668
11710
|
var HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
11669
11711
|
var HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
11712
|
+
var HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
11713
|
+
var HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
11714
|
+
var HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
11715
|
+
var HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
11670
11716
|
var HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
11671
11717
|
var HUB_INBOX_MAX_LIMIT = 500;
|
|
11672
11718
|
var HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -11838,6 +11884,23 @@ function checkChatMessage(value) {
|
|
|
11838
11884
|
}
|
|
11839
11885
|
return trimmed;
|
|
11840
11886
|
}
|
|
11887
|
+
function matchConciergeThreadRoute(path) {
|
|
11888
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
11889
|
+
if (!path.startsWith(prefix)) return null;
|
|
11890
|
+
const rest = path.slice(prefix.length);
|
|
11891
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
11892
|
+
const decoded = decodeURIComponent(rest);
|
|
11893
|
+
if (decoded.length === 0) return null;
|
|
11894
|
+
return { threadId: decoded };
|
|
11895
|
+
}
|
|
11896
|
+
function parseSince(raw) {
|
|
11897
|
+
if (raw === null || raw === "") return void 0;
|
|
11898
|
+
const parsed = Number.parseInt(raw, 10);
|
|
11899
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
11900
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
11901
|
+
}
|
|
11902
|
+
return parsed;
|
|
11903
|
+
}
|
|
11841
11904
|
function matchInboxRoute(path) {
|
|
11842
11905
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
11843
11906
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12021,6 +12084,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12021
12084
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12022
12085
|
return true;
|
|
12023
12086
|
}
|
|
12087
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12088
|
+
const limit = parseLimit(
|
|
12089
|
+
url.searchParams.get("limit"),
|
|
12090
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12091
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12092
|
+
);
|
|
12093
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12094
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12095
|
+
return true;
|
|
12096
|
+
}
|
|
12097
|
+
{
|
|
12098
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12099
|
+
if (threadMatch) {
|
|
12100
|
+
if (method === "GET") {
|
|
12101
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12102
|
+
const limit = parseLimit(
|
|
12103
|
+
url.searchParams.get("limit"),
|
|
12104
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12105
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12106
|
+
);
|
|
12107
|
+
const readOpts = { limit };
|
|
12108
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12109
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12110
|
+
threadMatch.threadId,
|
|
12111
|
+
readOpts
|
|
12112
|
+
);
|
|
12113
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12114
|
+
return true;
|
|
12115
|
+
}
|
|
12116
|
+
if (method === "DELETE") {
|
|
12117
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12118
|
+
threadMatch.threadId
|
|
12119
|
+
);
|
|
12120
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12121
|
+
ok: removed,
|
|
12122
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12123
|
+
});
|
|
12124
|
+
return true;
|
|
12125
|
+
}
|
|
12126
|
+
}
|
|
12127
|
+
}
|
|
12024
12128
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12025
12129
|
return true;
|
|
12026
12130
|
} catch (err) {
|
|
@@ -16070,6 +16174,162 @@ async function dispatchV11Request(inputs, req, res, url, method) {
|
|
|
16070
16174
|
return false;
|
|
16071
16175
|
}
|
|
16072
16176
|
|
|
16177
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
16178
|
+
var APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
16179
|
+
var APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
16180
|
+
var APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
16181
|
+
var APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
16182
|
+
function writeJSON4(res, status, payload) {
|
|
16183
|
+
res.writeHead(status, {
|
|
16184
|
+
"Content-Type": "application/json",
|
|
16185
|
+
"Cache-Control": "no-store"
|
|
16186
|
+
});
|
|
16187
|
+
res.end(JSON.stringify(payload));
|
|
16188
|
+
}
|
|
16189
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
16190
|
+
if (raw === null || raw === "") return defaultValue;
|
|
16191
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16192
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
16193
|
+
return defaultValue;
|
|
16194
|
+
}
|
|
16195
|
+
return Math.min(parsed, max);
|
|
16196
|
+
}
|
|
16197
|
+
function isStatusFilter(value) {
|
|
16198
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
16199
|
+
}
|
|
16200
|
+
function matchEntryRoute(path) {
|
|
16201
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
16202
|
+
if (!path.startsWith(prefix)) return null;
|
|
16203
|
+
const rest = path.slice(prefix.length);
|
|
16204
|
+
if (rest.length === 0) return null;
|
|
16205
|
+
const slash = rest.indexOf("/");
|
|
16206
|
+
if (slash === -1) {
|
|
16207
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
16208
|
+
}
|
|
16209
|
+
return {
|
|
16210
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
16211
|
+
action: rest.slice(slash + 1)
|
|
16212
|
+
};
|
|
16213
|
+
}
|
|
16214
|
+
async function handleStream2(deps, res) {
|
|
16215
|
+
res.writeHead(200, {
|
|
16216
|
+
"Content-Type": "text/event-stream",
|
|
16217
|
+
"Cache-Control": "no-cache, no-transform",
|
|
16218
|
+
Connection: "keep-alive",
|
|
16219
|
+
"X-Accel-Buffering": "no"
|
|
16220
|
+
});
|
|
16221
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
16222
|
+
res.write(
|
|
16223
|
+
`event: approval_inbox_snapshot
|
|
16224
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
16225
|
+
|
|
16226
|
+
`
|
|
16227
|
+
);
|
|
16228
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
16229
|
+
try {
|
|
16230
|
+
res.write(
|
|
16231
|
+
`event: approval_inbox_${event.type}
|
|
16232
|
+
data: ${JSON.stringify(event.entry)}
|
|
16233
|
+
|
|
16234
|
+
`
|
|
16235
|
+
);
|
|
16236
|
+
} catch {
|
|
16237
|
+
}
|
|
16238
|
+
});
|
|
16239
|
+
const keepAlive = setInterval(() => {
|
|
16240
|
+
try {
|
|
16241
|
+
res.write(": keepalive\n\n");
|
|
16242
|
+
} catch {
|
|
16243
|
+
}
|
|
16244
|
+
}, 25e3);
|
|
16245
|
+
const cleanup = () => {
|
|
16246
|
+
clearInterval(keepAlive);
|
|
16247
|
+
unsubscribe();
|
|
16248
|
+
};
|
|
16249
|
+
res.on("close", cleanup);
|
|
16250
|
+
res.on("error", cleanup);
|
|
16251
|
+
}
|
|
16252
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
16253
|
+
const host = req.headers.host || "localhost";
|
|
16254
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
16255
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
16256
|
+
const path = url.pathname;
|
|
16257
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
16258
|
+
return false;
|
|
16259
|
+
}
|
|
16260
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
16261
|
+
if (!checkAuth(req, res, url)) return true;
|
|
16262
|
+
try {
|
|
16263
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
16264
|
+
await handleStream2(deps, res);
|
|
16265
|
+
return true;
|
|
16266
|
+
}
|
|
16267
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
16268
|
+
const limit = parseLimit2(
|
|
16269
|
+
url.searchParams.get("limit"),
|
|
16270
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
16271
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
16272
|
+
);
|
|
16273
|
+
const statusRaw = url.searchParams.get("status");
|
|
16274
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
16275
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
16276
|
+
const entries = await deps.aggregator.list({
|
|
16277
|
+
status,
|
|
16278
|
+
limit,
|
|
16279
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
16280
|
+
});
|
|
16281
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
16282
|
+
return true;
|
|
16283
|
+
}
|
|
16284
|
+
const entryMatch = matchEntryRoute(path);
|
|
16285
|
+
if (entryMatch === null) {
|
|
16286
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16287
|
+
return true;
|
|
16288
|
+
}
|
|
16289
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
16290
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
16291
|
+
const entry = entries.find(
|
|
16292
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
16293
|
+
);
|
|
16294
|
+
if (!entry) {
|
|
16295
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16296
|
+
return true;
|
|
16297
|
+
}
|
|
16298
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
16299
|
+
entryMatch.aggregatorId
|
|
16300
|
+
);
|
|
16301
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
16302
|
+
return true;
|
|
16303
|
+
}
|
|
16304
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
16305
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
16306
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16307
|
+
try {
|
|
16308
|
+
const entry = await deps.aggregator.resolve(
|
|
16309
|
+
entryMatch.aggregatorId,
|
|
16310
|
+
decision,
|
|
16311
|
+
operatorId
|
|
16312
|
+
);
|
|
16313
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
16314
|
+
} catch (err) {
|
|
16315
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16316
|
+
if (msg === "approval-aggregator: not_found") {
|
|
16317
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16318
|
+
} else {
|
|
16319
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16320
|
+
}
|
|
16321
|
+
}
|
|
16322
|
+
return true;
|
|
16323
|
+
}
|
|
16324
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16325
|
+
return true;
|
|
16326
|
+
} catch (err) {
|
|
16327
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16328
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16329
|
+
return true;
|
|
16330
|
+
}
|
|
16331
|
+
}
|
|
16332
|
+
|
|
16073
16333
|
// src/principal-policy/dashboard.ts
|
|
16074
16334
|
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16075
16335
|
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -16134,6 +16394,14 @@ var DashboardApprovalChannel = class {
|
|
|
16134
16394
|
* regardless. Default route flip is deferred to v1.2.
|
|
16135
16395
|
*/
|
|
16136
16396
|
v11Bindings = null;
|
|
16397
|
+
/**
|
|
16398
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
16399
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
16400
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
16401
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
16402
|
+
* the operator-facing query / decision surface.
|
|
16403
|
+
*/
|
|
16404
|
+
approvalAggregator = null;
|
|
16137
16405
|
constructor(config) {
|
|
16138
16406
|
this.config = config;
|
|
16139
16407
|
this.authToken = config.auth_token;
|
|
@@ -16184,6 +16452,34 @@ var DashboardApprovalChannel = class {
|
|
|
16184
16452
|
setV11Bindings(bindings) {
|
|
16185
16453
|
this.v11Bindings = bindings;
|
|
16186
16454
|
}
|
|
16455
|
+
/**
|
|
16456
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
16457
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
16458
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
16459
|
+
* tests + during shutdown).
|
|
16460
|
+
*/
|
|
16461
|
+
setApprovalAggregator(aggregator) {
|
|
16462
|
+
this.approvalAggregator = aggregator;
|
|
16463
|
+
}
|
|
16464
|
+
/**
|
|
16465
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
16466
|
+
* before the legacy approval route table. Returns true when served.
|
|
16467
|
+
*/
|
|
16468
|
+
async dispatchApprovalInbox(req, res) {
|
|
16469
|
+
if (!this.approvalAggregator) return false;
|
|
16470
|
+
return handleApprovalInboxRoute(
|
|
16471
|
+
{
|
|
16472
|
+
authConfig: {
|
|
16473
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
16474
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
16475
|
+
},
|
|
16476
|
+
aggregator: this.approvalAggregator,
|
|
16477
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
16478
|
+
},
|
|
16479
|
+
req,
|
|
16480
|
+
res
|
|
16481
|
+
);
|
|
16482
|
+
}
|
|
16187
16483
|
/**
|
|
16188
16484
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
16189
16485
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -16559,6 +16855,18 @@ var DashboardApprovalChannel = class {
|
|
|
16559
16855
|
res.end();
|
|
16560
16856
|
return;
|
|
16561
16857
|
}
|
|
16858
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
16859
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
16860
|
+
if (handled) return;
|
|
16861
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
16862
|
+
}).catch(() => {
|
|
16863
|
+
if (!res.headersSent) {
|
|
16864
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
16865
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
16866
|
+
}
|
|
16867
|
+
});
|
|
16868
|
+
return;
|
|
16869
|
+
}
|
|
16562
16870
|
if (this.v11Bindings) {
|
|
16563
16871
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
16564
16872
|
if (handled) return;
|
|
@@ -18577,14 +18885,25 @@ var ApprovalGate = class {
|
|
|
18577
18885
|
auditLog;
|
|
18578
18886
|
injectionDetector;
|
|
18579
18887
|
onInjectionAlert;
|
|
18888
|
+
onApprovalEvent;
|
|
18580
18889
|
proxyTierResolver;
|
|
18581
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
18890
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
18582
18891
|
this.policy = policy;
|
|
18583
18892
|
this.baseline = baseline;
|
|
18584
18893
|
this.channel = channel;
|
|
18585
18894
|
this.auditLog = auditLog;
|
|
18586
18895
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
18587
18896
|
this.onInjectionAlert = onInjectionAlert;
|
|
18897
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
18898
|
+
}
|
|
18899
|
+
/**
|
|
18900
|
+
* Set the approval-event callback after construction. Used by the
|
|
18901
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
18902
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
18903
|
+
* constructor so existing call sites continue to work unchanged.
|
|
18904
|
+
*/
|
|
18905
|
+
setApprovalEventCallback(cb) {
|
|
18906
|
+
this.onApprovalEvent = cb;
|
|
18588
18907
|
}
|
|
18589
18908
|
/**
|
|
18590
18909
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -18818,21 +19137,105 @@ var ApprovalGate = class {
|
|
|
18818
19137
|
}
|
|
18819
19138
|
/**
|
|
18820
19139
|
* Request approval from the human principal.
|
|
19140
|
+
*
|
|
19141
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
19142
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
19143
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
19144
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
19145
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
19146
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
18821
19147
|
*/
|
|
18822
19148
|
async requestApproval(operation, tier, reason, context) {
|
|
19149
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
18823
19150
|
const request = {
|
|
18824
19151
|
operation,
|
|
18825
19152
|
tier,
|
|
18826
19153
|
reason,
|
|
18827
19154
|
context,
|
|
18828
|
-
timestamp:
|
|
19155
|
+
timestamp: requestTimestamp
|
|
18829
19156
|
};
|
|
18830
|
-
const
|
|
19157
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
19158
|
+
if (this.onApprovalEvent) {
|
|
19159
|
+
try {
|
|
19160
|
+
this.onApprovalEvent({
|
|
19161
|
+
phase: "requested",
|
|
19162
|
+
operation,
|
|
19163
|
+
tier,
|
|
19164
|
+
reason,
|
|
19165
|
+
context,
|
|
19166
|
+
request_timestamp: requestTimestamp,
|
|
19167
|
+
correlation_id: correlationId
|
|
19168
|
+
});
|
|
19169
|
+
} catch {
|
|
19170
|
+
}
|
|
19171
|
+
}
|
|
19172
|
+
let response;
|
|
19173
|
+
try {
|
|
19174
|
+
response = await this.channel.requestApproval(request);
|
|
19175
|
+
} catch (err) {
|
|
19176
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
19177
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19178
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
19179
|
+
tier,
|
|
19180
|
+
reason,
|
|
19181
|
+
decided_by: "channel_failure",
|
|
19182
|
+
channel_error: errMessage
|
|
19183
|
+
});
|
|
19184
|
+
if (this.onApprovalEvent) {
|
|
19185
|
+
try {
|
|
19186
|
+
this.onApprovalEvent({
|
|
19187
|
+
phase: "resolved",
|
|
19188
|
+
operation,
|
|
19189
|
+
tier,
|
|
19190
|
+
reason,
|
|
19191
|
+
context,
|
|
19192
|
+
request_timestamp: requestTimestamp,
|
|
19193
|
+
resolution: {
|
|
19194
|
+
decision: "deny",
|
|
19195
|
+
decided_at: decidedAt,
|
|
19196
|
+
decided_by: "channel_failure"
|
|
19197
|
+
},
|
|
19198
|
+
correlation_id: correlationId
|
|
19199
|
+
});
|
|
19200
|
+
} catch {
|
|
19201
|
+
}
|
|
19202
|
+
}
|
|
19203
|
+
return {
|
|
19204
|
+
allowed: false,
|
|
19205
|
+
tier,
|
|
19206
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
19207
|
+
approval_required: true,
|
|
19208
|
+
approval_response: {
|
|
19209
|
+
decision: "deny",
|
|
19210
|
+
decided_at: decidedAt,
|
|
19211
|
+
decided_by: "channel_failure"
|
|
19212
|
+
}
|
|
19213
|
+
};
|
|
19214
|
+
}
|
|
18831
19215
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
18832
19216
|
tier,
|
|
18833
19217
|
reason,
|
|
18834
19218
|
decided_by: response.decided_by
|
|
18835
19219
|
});
|
|
19220
|
+
if (this.onApprovalEvent) {
|
|
19221
|
+
try {
|
|
19222
|
+
this.onApprovalEvent({
|
|
19223
|
+
phase: "resolved",
|
|
19224
|
+
operation,
|
|
19225
|
+
tier,
|
|
19226
|
+
reason,
|
|
19227
|
+
context,
|
|
19228
|
+
request_timestamp: requestTimestamp,
|
|
19229
|
+
resolution: {
|
|
19230
|
+
decision: response.decision,
|
|
19231
|
+
decided_at: response.decided_at,
|
|
19232
|
+
decided_by: response.decided_by
|
|
19233
|
+
},
|
|
19234
|
+
correlation_id: correlationId
|
|
19235
|
+
});
|
|
19236
|
+
} catch {
|
|
19237
|
+
}
|
|
19238
|
+
}
|
|
18836
19239
|
return {
|
|
18837
19240
|
allowed: response.decision === "approve",
|
|
18838
19241
|
tier,
|
|
@@ -18866,6 +19269,352 @@ var ApprovalGate = class {
|
|
|
18866
19269
|
}
|
|
18867
19270
|
};
|
|
18868
19271
|
|
|
19272
|
+
// src/principal-policy/approval-aggregator.ts
|
|
19273
|
+
init_encryption();
|
|
19274
|
+
init_encoding();
|
|
19275
|
+
var APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
19276
|
+
var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
19277
|
+
var APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
19278
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
19279
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
19280
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
19281
|
+
};
|
|
19282
|
+
var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
19283
|
+
var DEFAULT_MAX_LIST_LIMIT = 200;
|
|
19284
|
+
var DEFAULT_LIST_PAGE_SIZE = 50;
|
|
19285
|
+
var ApprovalAggregator = class {
|
|
19286
|
+
storage;
|
|
19287
|
+
encryptionKey;
|
|
19288
|
+
auditLog;
|
|
19289
|
+
identityId;
|
|
19290
|
+
fortressId;
|
|
19291
|
+
pendingTtlMs;
|
|
19292
|
+
maxListLimit;
|
|
19293
|
+
now;
|
|
19294
|
+
resolveSourceContext;
|
|
19295
|
+
resolveHubInboxItemId;
|
|
19296
|
+
/** Cached entries by `aggregator_id`. */
|
|
19297
|
+
entries = /* @__PURE__ */ new Map();
|
|
19298
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
19299
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
19300
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
19301
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
19302
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
19303
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
19304
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
19305
|
+
hydrated = false;
|
|
19306
|
+
/** Active SSE listeners. */
|
|
19307
|
+
listeners = /* @__PURE__ */ new Set();
|
|
19308
|
+
constructor(deps) {
|
|
19309
|
+
this.storage = deps.storage;
|
|
19310
|
+
this.encryptionKey = derivePurposeKey(
|
|
19311
|
+
deps.masterKey,
|
|
19312
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
19313
|
+
);
|
|
19314
|
+
this.auditLog = deps.auditLog;
|
|
19315
|
+
this.identityId = deps.identityId;
|
|
19316
|
+
this.fortressId = deps.fortressId;
|
|
19317
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
19318
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
19319
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
19320
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
19321
|
+
source_harness: this.fortressId,
|
|
19322
|
+
source_agent_id: this.fortressId
|
|
19323
|
+
}));
|
|
19324
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
19325
|
+
}
|
|
19326
|
+
/**
|
|
19327
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
19328
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
19329
|
+
*/
|
|
19330
|
+
onEvent(listener) {
|
|
19331
|
+
this.listeners.add(listener);
|
|
19332
|
+
return () => this.listeners.delete(listener);
|
|
19333
|
+
}
|
|
19334
|
+
/**
|
|
19335
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
19336
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
19337
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
19338
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
19339
|
+
* a record for).
|
|
19340
|
+
*/
|
|
19341
|
+
async ingest(event) {
|
|
19342
|
+
await this.hydrate();
|
|
19343
|
+
if (event.phase === "requested") {
|
|
19344
|
+
return this.ingestRequested(event);
|
|
19345
|
+
}
|
|
19346
|
+
if (event.phase === "resolved") {
|
|
19347
|
+
return this.ingestResolved(event);
|
|
19348
|
+
}
|
|
19349
|
+
return null;
|
|
19350
|
+
}
|
|
19351
|
+
/**
|
|
19352
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
19353
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
19354
|
+
* snapshot is returned.
|
|
19355
|
+
*/
|
|
19356
|
+
async list(opts) {
|
|
19357
|
+
await this.hydrate();
|
|
19358
|
+
await this.expireStale();
|
|
19359
|
+
const limit = Math.min(
|
|
19360
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
19361
|
+
this.maxListLimit
|
|
19362
|
+
);
|
|
19363
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
19364
|
+
const matching = [];
|
|
19365
|
+
for (const entry of this.entries.values()) {
|
|
19366
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
19367
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
19368
|
+
matching.push(entry);
|
|
19369
|
+
}
|
|
19370
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
19371
|
+
return matching.slice(0, limit);
|
|
19372
|
+
}
|
|
19373
|
+
/**
|
|
19374
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
19375
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
19376
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
19377
|
+
*/
|
|
19378
|
+
async getFullPayload(aggregatorId) {
|
|
19379
|
+
await this.hydrate();
|
|
19380
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
19381
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
19382
|
+
}
|
|
19383
|
+
/**
|
|
19384
|
+
* Resolve an entry. Used by both:
|
|
19385
|
+
* 1. The gate wire-up on channel-decision return.
|
|
19386
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
19387
|
+
*
|
|
19388
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
19389
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
19390
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
19391
|
+
* routes return 404.
|
|
19392
|
+
*/
|
|
19393
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
19394
|
+
await this.hydrate();
|
|
19395
|
+
const entry = this.entries.get(aggregatorId);
|
|
19396
|
+
if (!entry) {
|
|
19397
|
+
throw new Error("approval-aggregator: not_found");
|
|
19398
|
+
}
|
|
19399
|
+
if (entry.status !== "pending") {
|
|
19400
|
+
return entry;
|
|
19401
|
+
}
|
|
19402
|
+
entry.status = decision;
|
|
19403
|
+
entry.resolved_at = this.now().toISOString();
|
|
19404
|
+
entry.resolved_by = operatorId;
|
|
19405
|
+
await this.persist(entry);
|
|
19406
|
+
this.auditLog.append(
|
|
19407
|
+
"l2",
|
|
19408
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19409
|
+
this.identityId,
|
|
19410
|
+
{
|
|
19411
|
+
aggregator_id: entry.aggregator_id,
|
|
19412
|
+
source_harness: entry.source_harness,
|
|
19413
|
+
source_agent_id: entry.source_agent_id,
|
|
19414
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19415
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19416
|
+
decision,
|
|
19417
|
+
decided_by: operatorId,
|
|
19418
|
+
decided_at: entry.resolved_at
|
|
19419
|
+
}
|
|
19420
|
+
);
|
|
19421
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19422
|
+
return entry;
|
|
19423
|
+
}
|
|
19424
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
19425
|
+
async ingestRequested(event) {
|
|
19426
|
+
const ctx = this.resolveSourceContext(event);
|
|
19427
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
19428
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
19429
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
19430
|
+
if (existing) {
|
|
19431
|
+
const existingEntry = this.entries.get(existing);
|
|
19432
|
+
if (existingEntry) {
|
|
19433
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
19434
|
+
this.auditLog.append(
|
|
19435
|
+
"l2",
|
|
19436
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
19437
|
+
this.identityId,
|
|
19438
|
+
{
|
|
19439
|
+
aggregator_id: existing,
|
|
19440
|
+
source_harness: ctx.source_harness,
|
|
19441
|
+
source_agent_id: ctx.source_agent_id,
|
|
19442
|
+
audit_log_entry_id: auditId,
|
|
19443
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19444
|
+
correlation_id: event.correlation_id
|
|
19445
|
+
}
|
|
19446
|
+
);
|
|
19447
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
19448
|
+
return null;
|
|
19449
|
+
}
|
|
19450
|
+
}
|
|
19451
|
+
const id = randomUUID();
|
|
19452
|
+
const now = this.now();
|
|
19453
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
19454
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
19455
|
+
const entry = {
|
|
19456
|
+
aggregator_id: id,
|
|
19457
|
+
source_harness: ctx.source_harness,
|
|
19458
|
+
source_agent_id: ctx.source_agent_id,
|
|
19459
|
+
audit_log_entry_id: auditId,
|
|
19460
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19461
|
+
action_summary: this.deriveActionSummary(event),
|
|
19462
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
19463
|
+
status: "pending",
|
|
19464
|
+
created_at: now.toISOString(),
|
|
19465
|
+
expires_at: expires.toISOString(),
|
|
19466
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19467
|
+
};
|
|
19468
|
+
this.entries.set(id, entry);
|
|
19469
|
+
this.dedupIndex.set(dedupKey, id);
|
|
19470
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
19471
|
+
this.fullPayloads.set(id, event.context);
|
|
19472
|
+
await this.persist(entry);
|
|
19473
|
+
this.auditLog.append(
|
|
19474
|
+
"l2",
|
|
19475
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
19476
|
+
this.identityId,
|
|
19477
|
+
{
|
|
19478
|
+
aggregator_id: id,
|
|
19479
|
+
source_harness: ctx.source_harness,
|
|
19480
|
+
source_agent_id: ctx.source_agent_id,
|
|
19481
|
+
audit_log_entry_id: auditId,
|
|
19482
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19483
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19484
|
+
}
|
|
19485
|
+
);
|
|
19486
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
19487
|
+
return entry;
|
|
19488
|
+
}
|
|
19489
|
+
async ingestResolved(event) {
|
|
19490
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
19491
|
+
if (!id) return null;
|
|
19492
|
+
const entry = this.entries.get(id);
|
|
19493
|
+
if (!entry) return null;
|
|
19494
|
+
if (entry.status !== "pending") return entry;
|
|
19495
|
+
if (!event.resolution) return entry;
|
|
19496
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
19497
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
19498
|
+
entry.status = status;
|
|
19499
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
19500
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
19501
|
+
await this.persist(entry);
|
|
19502
|
+
this.auditLog.append(
|
|
19503
|
+
"l2",
|
|
19504
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19505
|
+
this.identityId,
|
|
19506
|
+
{
|
|
19507
|
+
aggregator_id: id,
|
|
19508
|
+
source_harness: entry.source_harness,
|
|
19509
|
+
source_agent_id: entry.source_agent_id,
|
|
19510
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19511
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19512
|
+
decision: status,
|
|
19513
|
+
decided_by: entry.resolved_by,
|
|
19514
|
+
decided_at: entry.resolved_at,
|
|
19515
|
+
fail_closed: failClosed
|
|
19516
|
+
}
|
|
19517
|
+
);
|
|
19518
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19519
|
+
return entry;
|
|
19520
|
+
}
|
|
19521
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
19522
|
+
/**
|
|
19523
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
19524
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
19525
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
19526
|
+
* the audit entry the gate appended on the same call.
|
|
19527
|
+
*/
|
|
19528
|
+
auditEntryIdForEvent(event) {
|
|
19529
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
19530
|
+
}
|
|
19531
|
+
derivePolicyRuleId(event) {
|
|
19532
|
+
return `tier${event.tier}:${event.operation}`;
|
|
19533
|
+
}
|
|
19534
|
+
deriveActionSummary(event) {
|
|
19535
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
19536
|
+
}
|
|
19537
|
+
/**
|
|
19538
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
19539
|
+
* identical payloads always hash the same, even when key insertion order
|
|
19540
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
19541
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
19542
|
+
*/
|
|
19543
|
+
hashPayload(payload) {
|
|
19544
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
19545
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
19546
|
+
}
|
|
19547
|
+
emit(event) {
|
|
19548
|
+
for (const listener of this.listeners) {
|
|
19549
|
+
try {
|
|
19550
|
+
listener(event);
|
|
19551
|
+
} catch {
|
|
19552
|
+
}
|
|
19553
|
+
}
|
|
19554
|
+
}
|
|
19555
|
+
async expireStale() {
|
|
19556
|
+
const nowMs = this.now().getTime();
|
|
19557
|
+
for (const entry of this.entries.values()) {
|
|
19558
|
+
if (entry.status !== "pending") continue;
|
|
19559
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
19560
|
+
entry.status = "expired";
|
|
19561
|
+
entry.resolved_at = this.now().toISOString();
|
|
19562
|
+
entry.resolved_by = "system_ttl";
|
|
19563
|
+
await this.persist(entry);
|
|
19564
|
+
this.auditLog.append(
|
|
19565
|
+
"l2",
|
|
19566
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19567
|
+
this.identityId,
|
|
19568
|
+
{
|
|
19569
|
+
aggregator_id: entry.aggregator_id,
|
|
19570
|
+
source_harness: entry.source_harness,
|
|
19571
|
+
source_agent_id: entry.source_agent_id,
|
|
19572
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19573
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19574
|
+
decision: "expired",
|
|
19575
|
+
decided_by: "system_ttl",
|
|
19576
|
+
decided_at: entry.resolved_at
|
|
19577
|
+
}
|
|
19578
|
+
);
|
|
19579
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19580
|
+
}
|
|
19581
|
+
}
|
|
19582
|
+
async persist(entry) {
|
|
19583
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
19584
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
19585
|
+
await this.storage.write(
|
|
19586
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19587
|
+
entry.aggregator_id,
|
|
19588
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
19589
|
+
);
|
|
19590
|
+
}
|
|
19591
|
+
async hydrate() {
|
|
19592
|
+
if (this.hydrated) return;
|
|
19593
|
+
this.hydrated = true;
|
|
19594
|
+
try {
|
|
19595
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
19596
|
+
for (const meta of metas) {
|
|
19597
|
+
const raw = await this.storage.read(
|
|
19598
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19599
|
+
meta.key
|
|
19600
|
+
);
|
|
19601
|
+
if (!raw) continue;
|
|
19602
|
+
try {
|
|
19603
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
19604
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
19605
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
19606
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
19607
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
19608
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
19609
|
+
} catch {
|
|
19610
|
+
}
|
|
19611
|
+
}
|
|
19612
|
+
} catch {
|
|
19613
|
+
this.hydrated = false;
|
|
19614
|
+
}
|
|
19615
|
+
}
|
|
19616
|
+
};
|
|
19617
|
+
|
|
18869
19618
|
// src/principal-policy/tools.ts
|
|
18870
19619
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
18871
19620
|
return [
|
|
@@ -21718,6 +22467,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
21718
22467
|
}
|
|
21719
22468
|
async function consumeResetHistoryMarker(options) {
|
|
21720
22469
|
const markerPath = join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
22470
|
+
const consumedPath = markerPath + ".consumed";
|
|
22471
|
+
if (await fileExists3(consumedPath)) {
|
|
22472
|
+
await rm(markerPath, { force: true });
|
|
22473
|
+
await rm(consumedPath, { force: true });
|
|
22474
|
+
return { emitted: 0, markerPath };
|
|
22475
|
+
}
|
|
21721
22476
|
if (!await fileExists3(markerPath)) {
|
|
21722
22477
|
return { emitted: 0, markerPath };
|
|
21723
22478
|
}
|
|
@@ -21742,7 +22497,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
21742
22497
|
});
|
|
21743
22498
|
}
|
|
21744
22499
|
await options.auditLog.flush();
|
|
22500
|
+
await writeFile(consumedPath, "", "utf-8");
|
|
21745
22501
|
await rm(markerPath, { force: true });
|
|
22502
|
+
await rm(consumedPath, { force: true });
|
|
21746
22503
|
return { emitted: markers.length, markerHash, markerPath };
|
|
21747
22504
|
}
|
|
21748
22505
|
async function fileExists3(path) {
|
|
@@ -30688,6 +31445,36 @@ var HubService = class {
|
|
|
30688
31445
|
const chat = this.requireOperatorChat();
|
|
30689
31446
|
return chat.getConciergeHistory();
|
|
30690
31447
|
}
|
|
31448
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
31449
|
+
/**
|
|
31450
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
31451
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
31452
|
+
* surface is unavailable on a given fortress.
|
|
31453
|
+
*/
|
|
31454
|
+
hasConciergeMemory() {
|
|
31455
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
31456
|
+
}
|
|
31457
|
+
async listConciergeMemoryThreads(opts) {
|
|
31458
|
+
const chat = this.requireOperatorChat();
|
|
31459
|
+
if (!chat.hasConciergeMemory()) {
|
|
31460
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31461
|
+
}
|
|
31462
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
31463
|
+
}
|
|
31464
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
31465
|
+
const chat = this.requireOperatorChat();
|
|
31466
|
+
if (!chat.hasConciergeMemory()) {
|
|
31467
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31468
|
+
}
|
|
31469
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
31470
|
+
}
|
|
31471
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
31472
|
+
const chat = this.requireOperatorChat();
|
|
31473
|
+
if (!chat.hasConciergeMemory()) {
|
|
31474
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31475
|
+
}
|
|
31476
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
31477
|
+
}
|
|
30691
31478
|
/**
|
|
30692
31479
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
30693
31480
|
* panel surfaces recent activity routed through this agent, pending
|
|
@@ -30775,7 +31562,21 @@ init_encoding();
|
|
|
30775
31562
|
|
|
30776
31563
|
// src/chat/operator-chat-audit-events.ts
|
|
30777
31564
|
var OPERATOR_CHAT_OPS = {
|
|
30778
|
-
CONCIERGE_CHAT: "operator_concierge_chat"
|
|
31565
|
+
CONCIERGE_CHAT: "operator_concierge_chat",
|
|
31566
|
+
/**
|
|
31567
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
31568
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
31569
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
31570
|
+
* raw turn content never crosses the audit surface.
|
|
31571
|
+
*/
|
|
31572
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
31573
|
+
/**
|
|
31574
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
31575
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
31576
|
+
* the deleted bundle.
|
|
31577
|
+
*/
|
|
31578
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
31579
|
+
};
|
|
30779
31580
|
|
|
30780
31581
|
// src/chat/operator-chat-types.ts
|
|
30781
31582
|
var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
|
|
@@ -30818,6 +31619,14 @@ var OperatorChatService = class {
|
|
|
30818
31619
|
contextProviders;
|
|
30819
31620
|
piiFilter;
|
|
30820
31621
|
conciergeMaxTokens;
|
|
31622
|
+
memory;
|
|
31623
|
+
/**
|
|
31624
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
31625
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
31626
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
31627
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
31628
|
+
*/
|
|
31629
|
+
activeMemoryThreadId;
|
|
30821
31630
|
constructor(deps) {
|
|
30822
31631
|
this.store = deps.store;
|
|
30823
31632
|
this.auditLog = deps.auditLog;
|
|
@@ -30828,6 +31637,7 @@ var OperatorChatService = class {
|
|
|
30828
31637
|
}
|
|
30829
31638
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
30830
31639
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
31640
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
30831
31641
|
}
|
|
30832
31642
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
30833
31643
|
/**
|
|
@@ -30858,6 +31668,11 @@ var OperatorChatService = class {
|
|
|
30858
31668
|
CONCIERGE_THREAD_KEY,
|
|
30859
31669
|
operatorMessage
|
|
30860
31670
|
);
|
|
31671
|
+
if (this.memory) {
|
|
31672
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
31673
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
31674
|
+
});
|
|
31675
|
+
}
|
|
30861
31676
|
const start = Date.now();
|
|
30862
31677
|
let conciergeBody;
|
|
30863
31678
|
let servedBy = "disabled";
|
|
@@ -30913,6 +31728,11 @@ var OperatorChatService = class {
|
|
|
30913
31728
|
CONCIERGE_THREAD_KEY,
|
|
30914
31729
|
responseMessage
|
|
30915
31730
|
);
|
|
31731
|
+
if (this.memory) {
|
|
31732
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
31733
|
+
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
|
|
31734
|
+
});
|
|
31735
|
+
}
|
|
30916
31736
|
const payload = {
|
|
30917
31737
|
version: "1.2",
|
|
30918
31738
|
event_id: makeEventId("conc"),
|
|
@@ -30945,6 +31765,105 @@ var OperatorChatService = class {
|
|
|
30945
31765
|
);
|
|
30946
31766
|
return thread ? thread.messages : [];
|
|
30947
31767
|
}
|
|
31768
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
31769
|
+
/**
|
|
31770
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
31771
|
+
* 503 cleanly when called against an unwired service.
|
|
31772
|
+
*/
|
|
31773
|
+
hasConciergeMemory() {
|
|
31774
|
+
return this.memory !== void 0;
|
|
31775
|
+
}
|
|
31776
|
+
/**
|
|
31777
|
+
* List concierge memory threads, newest-first. Emits the
|
|
31778
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
31779
|
+
*/
|
|
31780
|
+
async listConciergeMemoryThreads(opts) {
|
|
31781
|
+
if (!this.memory) {
|
|
31782
|
+
throw new Error("concierge memory store not configured");
|
|
31783
|
+
}
|
|
31784
|
+
const summaries = await this.memory.listThreads(opts);
|
|
31785
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
31786
|
+
const payload = {
|
|
31787
|
+
version: "1.2",
|
|
31788
|
+
event_id: makeEventId("conc-hist"),
|
|
31789
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31790
|
+
identity_id: this.identityId,
|
|
31791
|
+
kind: "operator_concierge_history_read",
|
|
31792
|
+
surface: "concierge",
|
|
31793
|
+
thread_id: "*",
|
|
31794
|
+
turn_count: totalTurns
|
|
31795
|
+
};
|
|
31796
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
31797
|
+
return summaries;
|
|
31798
|
+
}
|
|
31799
|
+
/**
|
|
31800
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
31801
|
+
* `operator_concierge_history_read` audit event with the named
|
|
31802
|
+
* thread_id and the count of turns surfaced.
|
|
31803
|
+
*/
|
|
31804
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
31805
|
+
if (!this.memory) {
|
|
31806
|
+
throw new Error("concierge memory store not configured");
|
|
31807
|
+
}
|
|
31808
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
31809
|
+
const payload = {
|
|
31810
|
+
version: "1.2",
|
|
31811
|
+
event_id: makeEventId("conc-hist"),
|
|
31812
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31813
|
+
identity_id: this.identityId,
|
|
31814
|
+
kind: "operator_concierge_history_read",
|
|
31815
|
+
surface: "concierge",
|
|
31816
|
+
thread_id: threadId,
|
|
31817
|
+
turn_count: turns.length
|
|
31818
|
+
};
|
|
31819
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
31820
|
+
return turns;
|
|
31821
|
+
}
|
|
31822
|
+
/**
|
|
31823
|
+
* Delete a concierge memory thread. Emits
|
|
31824
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
31825
|
+
* removed; absent threads return false without an audit event.
|
|
31826
|
+
*/
|
|
31827
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
31828
|
+
if (!this.memory) {
|
|
31829
|
+
throw new Error("concierge memory store not configured");
|
|
31830
|
+
}
|
|
31831
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
31832
|
+
if (turnsBefore.length === 0) {
|
|
31833
|
+
return await this.memory.deleteThread(threadId);
|
|
31834
|
+
}
|
|
31835
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
31836
|
+
if (!removed) return false;
|
|
31837
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
31838
|
+
this.activeMemoryThreadId = void 0;
|
|
31839
|
+
}
|
|
31840
|
+
const payload = {
|
|
31841
|
+
version: "1.2",
|
|
31842
|
+
event_id: makeEventId("conc-del"),
|
|
31843
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31844
|
+
identity_id: this.identityId,
|
|
31845
|
+
kind: "operator_concierge_thread_deleted",
|
|
31846
|
+
surface: "concierge",
|
|
31847
|
+
thread_id: threadId,
|
|
31848
|
+
turn_count: turnsBefore.length
|
|
31849
|
+
};
|
|
31850
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
31851
|
+
return true;
|
|
31852
|
+
}
|
|
31853
|
+
/**
|
|
31854
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
31855
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
31856
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
31857
|
+
*/
|
|
31858
|
+
resetConciergeMemoryThread() {
|
|
31859
|
+
this.activeMemoryThreadId = void 0;
|
|
31860
|
+
}
|
|
31861
|
+
ensureActiveMemoryThread() {
|
|
31862
|
+
if (!this.activeMemoryThreadId) {
|
|
31863
|
+
this.activeMemoryThreadId = randomUUID();
|
|
31864
|
+
}
|
|
31865
|
+
return this.activeMemoryThreadId;
|
|
31866
|
+
}
|
|
30948
31867
|
/**
|
|
30949
31868
|
* Stitch fortress state into a single context blob the substrate
|
|
30950
31869
|
* folds into its summarization prompt.
|
|
@@ -31108,6 +32027,238 @@ var OperatorChatStore = class {
|
|
|
31108
32027
|
}
|
|
31109
32028
|
};
|
|
31110
32029
|
|
|
32030
|
+
// src/chat/concierge-memory-store.ts
|
|
32031
|
+
init_encryption();
|
|
32032
|
+
init_encoding();
|
|
32033
|
+
var CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
32034
|
+
var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
32035
|
+
var HKDF_INFO2 = "concierge-memory-store-v1";
|
|
32036
|
+
var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
32037
|
+
var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
32038
|
+
var ConciergeMemoryStore = class {
|
|
32039
|
+
storage;
|
|
32040
|
+
encryptionKey;
|
|
32041
|
+
fortressId;
|
|
32042
|
+
retentionDays;
|
|
32043
|
+
locks;
|
|
32044
|
+
constructor(opts) {
|
|
32045
|
+
this.storage = opts.storage;
|
|
32046
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
32047
|
+
this.fortressId = opts.fortressId;
|
|
32048
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
32049
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
32050
|
+
}
|
|
32051
|
+
/**
|
|
32052
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
32053
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
32054
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
32055
|
+
* monotonicity even under concurrent callers.
|
|
32056
|
+
*/
|
|
32057
|
+
async appendTurn(threadId, role, content) {
|
|
32058
|
+
return this.withLock(threadId, async () => {
|
|
32059
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
32060
|
+
const now = /* @__PURE__ */ new Date();
|
|
32061
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
32062
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
32063
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
32064
|
+
const turn = {
|
|
32065
|
+
thread_id: threadId,
|
|
32066
|
+
fortress_id: this.fortressId,
|
|
32067
|
+
turn_id: nextTurnId,
|
|
32068
|
+
role,
|
|
32069
|
+
content,
|
|
32070
|
+
created_at: now.toISOString(),
|
|
32071
|
+
retention_until: retentionUntil.toISOString()
|
|
32072
|
+
};
|
|
32073
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
32074
|
+
version: 1,
|
|
32075
|
+
thread_id: threadId,
|
|
32076
|
+
fortress_id: this.fortressId,
|
|
32077
|
+
created_at: now.toISOString(),
|
|
32078
|
+
turns: [turn]
|
|
32079
|
+
};
|
|
32080
|
+
await this.saveBundle(next);
|
|
32081
|
+
return turn;
|
|
32082
|
+
});
|
|
32083
|
+
}
|
|
32084
|
+
/**
|
|
32085
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
32086
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
32087
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
32088
|
+
*/
|
|
32089
|
+
async readThread(threadId, opts) {
|
|
32090
|
+
const bundle = await this.loadBundle(threadId);
|
|
32091
|
+
if (!bundle) return [];
|
|
32092
|
+
let turns = bundle.turns;
|
|
32093
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
32094
|
+
const cutoff = opts.sinceTurnId;
|
|
32095
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
32096
|
+
}
|
|
32097
|
+
if (opts?.limit !== void 0) {
|
|
32098
|
+
turns = turns.slice(0, opts.limit);
|
|
32099
|
+
}
|
|
32100
|
+
return turns;
|
|
32101
|
+
}
|
|
32102
|
+
/**
|
|
32103
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
32104
|
+
* Sorted newest-first by last_turn_at.
|
|
32105
|
+
*/
|
|
32106
|
+
async listThreads(opts) {
|
|
32107
|
+
const entries = await this.storage.list(
|
|
32108
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32109
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32110
|
+
);
|
|
32111
|
+
const summaries = [];
|
|
32112
|
+
for (const meta of entries) {
|
|
32113
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32114
|
+
if (threadId === null) continue;
|
|
32115
|
+
const bundle = await this.loadBundle(threadId);
|
|
32116
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
32117
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
32118
|
+
summaries.push({
|
|
32119
|
+
thread_id: bundle.thread_id,
|
|
32120
|
+
created_at: bundle.created_at,
|
|
32121
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
32122
|
+
turn_count: bundle.turns.length
|
|
32123
|
+
});
|
|
32124
|
+
}
|
|
32125
|
+
summaries.sort(
|
|
32126
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
32127
|
+
);
|
|
32128
|
+
if (opts?.limit !== void 0) {
|
|
32129
|
+
return summaries.slice(0, opts.limit);
|
|
32130
|
+
}
|
|
32131
|
+
return summaries;
|
|
32132
|
+
}
|
|
32133
|
+
/**
|
|
32134
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
32135
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
32136
|
+
* caller's responsibility.
|
|
32137
|
+
*/
|
|
32138
|
+
async deleteThread(threadId) {
|
|
32139
|
+
const key = bundleKey(threadId);
|
|
32140
|
+
return this.withLock(threadId, async () => {
|
|
32141
|
+
const existed = await this.storage.exists(
|
|
32142
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32143
|
+
key
|
|
32144
|
+
);
|
|
32145
|
+
if (!existed) return false;
|
|
32146
|
+
try {
|
|
32147
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32148
|
+
} catch {
|
|
32149
|
+
return false;
|
|
32150
|
+
}
|
|
32151
|
+
return true;
|
|
32152
|
+
});
|
|
32153
|
+
}
|
|
32154
|
+
/**
|
|
32155
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
32156
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
32157
|
+
*/
|
|
32158
|
+
async pruneExpired(now) {
|
|
32159
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
32160
|
+
const entries = await this.storage.list(
|
|
32161
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32162
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32163
|
+
);
|
|
32164
|
+
let pruned = 0;
|
|
32165
|
+
for (const meta of entries) {
|
|
32166
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32167
|
+
if (threadId === null) continue;
|
|
32168
|
+
pruned += await this.withLock(threadId, async () => {
|
|
32169
|
+
const bundle = await this.loadBundle(threadId);
|
|
32170
|
+
if (!bundle) return 0;
|
|
32171
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
32172
|
+
const dropped = bundle.turns.length - kept.length;
|
|
32173
|
+
if (dropped === 0) return 0;
|
|
32174
|
+
if (kept.length === 0) {
|
|
32175
|
+
await this.storage.delete(
|
|
32176
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32177
|
+
bundleKey(threadId)
|
|
32178
|
+
);
|
|
32179
|
+
} else {
|
|
32180
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
32181
|
+
}
|
|
32182
|
+
return dropped;
|
|
32183
|
+
});
|
|
32184
|
+
}
|
|
32185
|
+
return { pruned };
|
|
32186
|
+
}
|
|
32187
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
32188
|
+
async loadBundle(threadId) {
|
|
32189
|
+
const key = bundleKey(threadId);
|
|
32190
|
+
let raw;
|
|
32191
|
+
try {
|
|
32192
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32193
|
+
} catch {
|
|
32194
|
+
return null;
|
|
32195
|
+
}
|
|
32196
|
+
if (!raw) return null;
|
|
32197
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
32198
|
+
try {
|
|
32199
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
32200
|
+
const aad = stringToBytes(threadId);
|
|
32201
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
32202
|
+
const parsed = JSON.parse(
|
|
32203
|
+
bytesToString(plaintext)
|
|
32204
|
+
);
|
|
32205
|
+
if (parsed.version !== 1) return null;
|
|
32206
|
+
if (parsed.thread_id !== threadId) return null;
|
|
32207
|
+
return parsed;
|
|
32208
|
+
} catch {
|
|
32209
|
+
return null;
|
|
32210
|
+
}
|
|
32211
|
+
}
|
|
32212
|
+
async saveBundle(bundle) {
|
|
32213
|
+
const key = bundleKey(bundle.thread_id);
|
|
32214
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
32215
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
32216
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
32217
|
+
await this.storage.write(
|
|
32218
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32219
|
+
key,
|
|
32220
|
+
stringToBytes(JSON.stringify(envelope))
|
|
32221
|
+
);
|
|
32222
|
+
}
|
|
32223
|
+
/**
|
|
32224
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
32225
|
+
* once the task settles (success or failure). Generic helper so
|
|
32226
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
32227
|
+
*/
|
|
32228
|
+
async withLock(threadId, task) {
|
|
32229
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
32230
|
+
let release;
|
|
32231
|
+
const next = new Promise((resolve6) => {
|
|
32232
|
+
release = resolve6;
|
|
32233
|
+
});
|
|
32234
|
+
const chained = previous.then(() => next);
|
|
32235
|
+
this.locks.set(threadId, chained);
|
|
32236
|
+
try {
|
|
32237
|
+
await previous;
|
|
32238
|
+
return await task();
|
|
32239
|
+
} finally {
|
|
32240
|
+
release();
|
|
32241
|
+
if (this.locks.get(threadId) === chained) {
|
|
32242
|
+
this.locks.delete(threadId);
|
|
32243
|
+
}
|
|
32244
|
+
}
|
|
32245
|
+
}
|
|
32246
|
+
};
|
|
32247
|
+
function bundleKey(threadId) {
|
|
32248
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
32249
|
+
}
|
|
32250
|
+
function stripKeyPrefix(key) {
|
|
32251
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
32252
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
32253
|
+
}
|
|
32254
|
+
function lastTurnId(bundle) {
|
|
32255
|
+
let max = 0;
|
|
32256
|
+
for (const t of bundle.turns) {
|
|
32257
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
32258
|
+
}
|
|
32259
|
+
return max;
|
|
32260
|
+
}
|
|
32261
|
+
|
|
31111
32262
|
// src/dashboard/v1_1/wiring.ts
|
|
31112
32263
|
var CapabilityErrorAgentController = class {
|
|
31113
32264
|
fail(action) {
|
|
@@ -31146,6 +32297,14 @@ function buildV11Bindings(inputs) {
|
|
|
31146
32297
|
let operatorChatService;
|
|
31147
32298
|
if (inputs.storage && inputs.masterKey) {
|
|
31148
32299
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
32300
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
32301
|
+
storage: inputs.storage,
|
|
32302
|
+
masterKey: inputs.masterKey,
|
|
32303
|
+
fortressId: inputs.fortressId,
|
|
32304
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
32305
|
+
});
|
|
32306
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
32307
|
+
});
|
|
31149
32308
|
operatorChatService = new OperatorChatService({
|
|
31150
32309
|
store: chatStore,
|
|
31151
32310
|
auditLog: inputs.auditLog,
|
|
@@ -31156,7 +32315,8 @@ function buildV11Bindings(inputs) {
|
|
|
31156
32315
|
identityId: inputs.identityId,
|
|
31157
32316
|
registry
|
|
31158
32317
|
}),
|
|
31159
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
32318
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
32319
|
+
conciergeMemory
|
|
31160
32320
|
});
|
|
31161
32321
|
}
|
|
31162
32322
|
const hubService = new HubService({
|
|
@@ -31348,13 +32508,13 @@ init_encryption();
|
|
|
31348
32508
|
init_encoding();
|
|
31349
32509
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
31350
32510
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
31351
|
-
var
|
|
32511
|
+
var HKDF_INFO3 = "intelligence-substrate-config";
|
|
31352
32512
|
var IntelligenceConfigStore = class {
|
|
31353
32513
|
storage;
|
|
31354
32514
|
encryptionKey;
|
|
31355
32515
|
constructor(storage, masterKey) {
|
|
31356
32516
|
this.storage = storage;
|
|
31357
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
32517
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
31358
32518
|
}
|
|
31359
32519
|
/**
|
|
31360
32520
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -33493,7 +34653,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33493
34653
|
);
|
|
33494
34654
|
}
|
|
33495
34655
|
}
|
|
33496
|
-
const
|
|
34656
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
34657
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
34658
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
33497
34659
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
33498
34660
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
33499
34661
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -33502,6 +34664,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33502
34664
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
33503
34665
|
);
|
|
33504
34666
|
}
|
|
34667
|
+
let detailedFailureClass;
|
|
34668
|
+
if (identityFailed) {
|
|
34669
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
34670
|
+
} else if (reputationBundleFailed) {
|
|
34671
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
34672
|
+
} else if (reputationAttestationFailed) {
|
|
34673
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
34674
|
+
} else if (unverifiableFailed) {
|
|
34675
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
34676
|
+
}
|
|
33505
34677
|
return {
|
|
33506
34678
|
version: "1.1",
|
|
33507
34679
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -33521,7 +34693,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33521
34693
|
identity,
|
|
33522
34694
|
audit,
|
|
33523
34695
|
reputation,
|
|
33524
|
-
failure_class:
|
|
34696
|
+
failure_class: detailedFailureClass
|
|
33525
34697
|
};
|
|
33526
34698
|
}
|
|
33527
34699
|
|
|
@@ -34448,7 +35620,19 @@ async function runExitCommand(args) {
|
|
|
34448
35620
|
}
|
|
34449
35621
|
const config = await loadConfig();
|
|
34450
35622
|
const ctx = await openExitContext(argv, env);
|
|
34451
|
-
|
|
35623
|
+
let policy;
|
|
35624
|
+
try {
|
|
35625
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
35626
|
+
} catch (policyErr) {
|
|
35627
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
35628
|
+
write(err, `
|
|
35629
|
+
Sanctuary cannot proceed.
|
|
35630
|
+
${policyErr.message}
|
|
35631
|
+
`);
|
|
35632
|
+
return 1;
|
|
35633
|
+
}
|
|
35634
|
+
throw policyErr;
|
|
35635
|
+
}
|
|
34452
35636
|
const result = await exportExitBundle({
|
|
34453
35637
|
bundleDir: outDir,
|
|
34454
35638
|
storage: ctx.storage,
|
|
@@ -35100,7 +36284,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35100
36284
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
35101
36285
|
await profileStore.load();
|
|
35102
36286
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
35103
|
-
|
|
36287
|
+
let policy;
|
|
36288
|
+
try {
|
|
36289
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
36290
|
+
} catch (err) {
|
|
36291
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
36292
|
+
console.error(`
|
|
36293
|
+
Sanctuary cannot start.
|
|
36294
|
+
${err.message}
|
|
36295
|
+
`);
|
|
36296
|
+
process.exit(1);
|
|
36297
|
+
}
|
|
36298
|
+
throw err;
|
|
36299
|
+
}
|
|
35104
36300
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
35105
36301
|
await baseline.load();
|
|
35106
36302
|
let approvalChannel;
|
|
@@ -35198,6 +36394,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35198
36394
|
});
|
|
35199
36395
|
} : void 0;
|
|
35200
36396
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
36397
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
36398
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
36399
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
36400
|
+
storage,
|
|
36401
|
+
masterKey,
|
|
36402
|
+
auditLog,
|
|
36403
|
+
identityId: aggregatorIdentityId,
|
|
36404
|
+
fortressId: fortressIdForAggregator
|
|
36405
|
+
});
|
|
36406
|
+
gate.setApprovalEventCallback((event) => {
|
|
36407
|
+
void approvalAggregator.ingest(event);
|
|
36408
|
+
});
|
|
36409
|
+
if (dashboard) {
|
|
36410
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
36411
|
+
}
|
|
35201
36412
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
35202
36413
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
35203
36414
|
config,
|
|
@@ -35373,6 +36584,6 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35373
36584
|
};
|
|
35374
36585
|
}
|
|
35375
36586
|
|
|
35376
|
-
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, ExitBundleImportError, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
36587
|
+
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, ExitBundleImportError, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MalformedPrincipalPolicyError, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
35377
36588
|
//# sourceMappingURL=index.js.map
|
|
35378
36589
|
//# sourceMappingURL=index.js.map
|