@sanctuary-framework/mcp-server 1.2.2 → 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 +1759 -132
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1759 -132
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1410 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +541 -7
- package/dist/index.d.ts +541 -7
- package/dist/index.js +1395 -59
- package/dist/index.js.map +1 -1
- package/package.json +17 -16
package/dist/index.cjs
CHANGED
|
@@ -4443,13 +4443,23 @@ function parseScalar(value) {
|
|
|
4443
4443
|
return value.replace(/^["']|["']$/g, "");
|
|
4444
4444
|
}
|
|
4445
4445
|
function validatePolicy(raw) {
|
|
4446
|
+
if (!("tier1_always_approve" in raw)) {
|
|
4447
|
+
throw new Error(
|
|
4448
|
+
"Policy file must include 'tier1_always_approve' as an explicit list (use [] for empty). Remove specific entries instead of removing the whole key."
|
|
4449
|
+
);
|
|
4450
|
+
}
|
|
4451
|
+
if (!("approval_channel" in raw)) {
|
|
4452
|
+
throw new Error(
|
|
4453
|
+
"Policy file must include 'approval_channel' as an explicit object (use {} for defaults). Remove specific entries instead of removing the whole key."
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4446
4456
|
const userTier3 = raw.tier3_always_allow ?? [];
|
|
4447
4457
|
const mergedTier3 = [
|
|
4448
4458
|
.../* @__PURE__ */ new Set([...userTier3, ...DEFAULT_POLICY.tier3_always_allow])
|
|
4449
4459
|
];
|
|
4450
4460
|
return {
|
|
4451
4461
|
version: raw.version ?? 1,
|
|
4452
|
-
tier1_always_approve: raw.tier1_always_approve
|
|
4462
|
+
tier1_always_approve: raw.tier1_always_approve,
|
|
4453
4463
|
tier2_anomaly: {
|
|
4454
4464
|
...DEFAULT_TIER2,
|
|
4455
4465
|
...raw.tier2_anomaly ?? {}
|
|
@@ -4470,6 +4480,11 @@ function generateDefaultPolicyYaml() {
|
|
|
4470
4480
|
# This file controls what your agent can do without asking.
|
|
4471
4481
|
# Edit this file directly. Your agent cannot modify it.
|
|
4472
4482
|
# Changes take effect on server restart.
|
|
4483
|
+
#
|
|
4484
|
+
# Required keys (must be present; use [] or {} for empty):
|
|
4485
|
+
# tier1_always_approve, approval_channel
|
|
4486
|
+
# Optional keys (omit to use defaults; new defaults merge automatically):
|
|
4487
|
+
# tier2_anomaly, tier3_always_allow
|
|
4473
4488
|
|
|
4474
4489
|
version: 1
|
|
4475
4490
|
|
|
@@ -4574,20 +4589,52 @@ approval_channel:
|
|
|
4574
4589
|
timeout_seconds: 300
|
|
4575
4590
|
`;
|
|
4576
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
|
+
};
|
|
4577
4606
|
async function loadPrincipalPolicy(storagePath) {
|
|
4578
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
|
+
}
|
|
4579
4630
|
try {
|
|
4580
|
-
const content = await promises.readFile(policyPath, "utf-8");
|
|
4581
4631
|
const policy = parsePolicy(content);
|
|
4582
4632
|
return Object.freeze(policy);
|
|
4583
|
-
} catch {
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
} catch {
|
|
4589
|
-
}
|
|
4590
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4633
|
+
} catch (parseErr) {
|
|
4634
|
+
throw new MalformedPrincipalPolicyError(
|
|
4635
|
+
policyPath,
|
|
4636
|
+
parseErr.message
|
|
4637
|
+
);
|
|
4591
4638
|
}
|
|
4592
4639
|
}
|
|
4593
4640
|
|
|
@@ -4814,7 +4861,7 @@ function deepSortKeys(obj) {
|
|
|
4814
4861
|
return sorted;
|
|
4815
4862
|
}
|
|
4816
4863
|
function canonicalizeForSigning(body) {
|
|
4817
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4864
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4818
4865
|
}
|
|
4819
4866
|
|
|
4820
4867
|
// src/shr/generator.ts
|
|
@@ -11120,10 +11167,11 @@ function initTemplate(params) {
|
|
|
11120
11167
|
var DEFAULT_STORAGE_DIR = ".sanctuary";
|
|
11121
11168
|
var KEYCHAIN_SERVICE_DEFAULT = "sanctuary-passphrase";
|
|
11122
11169
|
function keychainServiceFor(storagePath, home = os.homedir()) {
|
|
11123
|
-
const defaultPath = path.join(home, DEFAULT_STORAGE_DIR);
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
const
|
|
11170
|
+
const defaultPath = path.resolve(path.join(home, DEFAULT_STORAGE_DIR));
|
|
11171
|
+
const canonicalStorage = path.resolve(storagePath);
|
|
11172
|
+
if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
|
|
11173
|
+
const digest = sha256.sha256(Buffer.from(canonicalStorage, "utf-8"));
|
|
11174
|
+
const suffix = Buffer.from(digest).toString("hex").slice(0, 16);
|
|
11127
11175
|
return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
|
|
11128
11176
|
}
|
|
11129
11177
|
var RUNTIME_FILE_NAME = "runtime.json";
|
|
@@ -11264,7 +11312,7 @@ async function discoverTenants(options = {}) {
|
|
|
11264
11312
|
for (const child of children) {
|
|
11265
11313
|
const childPath = path.join(root, child);
|
|
11266
11314
|
if (child.startsWith(".")) continue;
|
|
11267
|
-
if (child === "state" || child === "backup" || child === "config") continue;
|
|
11315
|
+
if (child === "state" || child === "backup" || child === "config" || child === "default") continue;
|
|
11268
11316
|
const s = await promises.stat(childPath).catch(() => null);
|
|
11269
11317
|
if (!s || !s.isDirectory()) continue;
|
|
11270
11318
|
const desc = await describeTenant(child, childPath, home);
|
|
@@ -11276,6 +11324,17 @@ async function discoverTenants(options = {}) {
|
|
|
11276
11324
|
const desc = await describeTenant(path.basename(extra), extra, home);
|
|
11277
11325
|
if (desc) tenants.push(desc);
|
|
11278
11326
|
}
|
|
11327
|
+
const seen = /* @__PURE__ */ new Map();
|
|
11328
|
+
for (const t of tenants) {
|
|
11329
|
+
seen.set(t.name, (seen.get(t.name) ?? 0) + 1);
|
|
11330
|
+
}
|
|
11331
|
+
for (const [name, count] of seen) {
|
|
11332
|
+
if (count > 1) {
|
|
11333
|
+
console.error(
|
|
11334
|
+
`[sanctuary] warning: ${count} tenants share the name "${name}". Use --tenant with a unique name or storage path to disambiguate.`
|
|
11335
|
+
);
|
|
11336
|
+
}
|
|
11337
|
+
}
|
|
11279
11338
|
tenants.sort((a, b) => {
|
|
11280
11339
|
if (a.name === "default") return -1;
|
|
11281
11340
|
if (b.name === "default") return 1;
|
|
@@ -11624,6 +11683,16 @@ var HUB_ROUTES = {
|
|
|
11624
11683
|
*/
|
|
11625
11684
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
11626
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",
|
|
11627
11696
|
/**
|
|
11628
11697
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
11629
11698
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -11647,6 +11716,10 @@ var HUB_TIER_1_AGENT_CONTROL_ACTIONS = [
|
|
|
11647
11716
|
];
|
|
11648
11717
|
var HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
11649
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;
|
|
11650
11723
|
var HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
11651
11724
|
var HUB_INBOX_MAX_LIMIT = 500;
|
|
11652
11725
|
var HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -11818,6 +11891,23 @@ function checkChatMessage(value) {
|
|
|
11818
11891
|
}
|
|
11819
11892
|
return trimmed;
|
|
11820
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
|
+
}
|
|
11821
11911
|
function matchInboxRoute(path) {
|
|
11822
11912
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
11823
11913
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12001,6 +12091,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12001
12091
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12002
12092
|
return true;
|
|
12003
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
|
+
}
|
|
12004
12135
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12005
12136
|
return true;
|
|
12006
12137
|
} catch (err) {
|
|
@@ -15644,6 +15775,8 @@ var IntelligenceRouterError = class extends Error {
|
|
|
15644
15775
|
this.code = code;
|
|
15645
15776
|
this.name = "IntelligenceRouterError";
|
|
15646
15777
|
}
|
|
15778
|
+
statusCode;
|
|
15779
|
+
code;
|
|
15647
15780
|
};
|
|
15648
15781
|
function writeJSON3(res, status, payload) {
|
|
15649
15782
|
res.writeHead(status, {
|
|
@@ -16048,6 +16181,162 @@ async function dispatchV11Request(inputs, req, res, url, method) {
|
|
|
16048
16181
|
return false;
|
|
16049
16182
|
}
|
|
16050
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
|
+
|
|
16051
16340
|
// src/principal-policy/dashboard.ts
|
|
16052
16341
|
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16053
16342
|
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -16112,6 +16401,14 @@ var DashboardApprovalChannel = class {
|
|
|
16112
16401
|
* regardless. Default route flip is deferred to v1.2.
|
|
16113
16402
|
*/
|
|
16114
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;
|
|
16115
16412
|
constructor(config) {
|
|
16116
16413
|
this.config = config;
|
|
16117
16414
|
this.authToken = config.auth_token;
|
|
@@ -16162,6 +16459,34 @@ var DashboardApprovalChannel = class {
|
|
|
16162
16459
|
setV11Bindings(bindings) {
|
|
16163
16460
|
this.v11Bindings = bindings;
|
|
16164
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
|
+
}
|
|
16165
16490
|
/**
|
|
16166
16491
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
16167
16492
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -16227,7 +16552,7 @@ var DashboardApprovalChannel = class {
|
|
|
16227
16552
|
server = http.createServer(handler);
|
|
16228
16553
|
}
|
|
16229
16554
|
this.httpServer = server;
|
|
16230
|
-
return new Promise((
|
|
16555
|
+
return new Promise((resolve6, reject) => {
|
|
16231
16556
|
const protocol = this.useTLS ? "https" : "http";
|
|
16232
16557
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
16233
16558
|
server.listen(this.config.port, this.config.host, () => {
|
|
@@ -16252,7 +16577,7 @@ var DashboardApprovalChannel = class {
|
|
|
16252
16577
|
if (shouldAutoOpen) {
|
|
16253
16578
|
this.openInBrowser(sessionUrl);
|
|
16254
16579
|
}
|
|
16255
|
-
|
|
16580
|
+
resolve6();
|
|
16256
16581
|
});
|
|
16257
16582
|
server.on("error", (err) => {
|
|
16258
16583
|
if (err.code === "EADDRINUSE") {
|
|
@@ -16298,8 +16623,8 @@ var DashboardApprovalChannel = class {
|
|
|
16298
16623
|
}
|
|
16299
16624
|
this.rateLimits.clear();
|
|
16300
16625
|
if (this.httpServer) {
|
|
16301
|
-
return new Promise((
|
|
16302
|
-
this.httpServer.close(() =>
|
|
16626
|
+
return new Promise((resolve6) => {
|
|
16627
|
+
this.httpServer.close(() => resolve6());
|
|
16303
16628
|
});
|
|
16304
16629
|
}
|
|
16305
16630
|
}
|
|
@@ -16313,7 +16638,7 @@ var DashboardApprovalChannel = class {
|
|
|
16313
16638
|
`[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
|
|
16314
16639
|
`
|
|
16315
16640
|
);
|
|
16316
|
-
return new Promise((
|
|
16641
|
+
return new Promise((resolve6) => {
|
|
16317
16642
|
const timer = setTimeout(() => {
|
|
16318
16643
|
this.pending.delete(id);
|
|
16319
16644
|
const response = {
|
|
@@ -16327,12 +16652,12 @@ var DashboardApprovalChannel = class {
|
|
|
16327
16652
|
decision: response.decision,
|
|
16328
16653
|
decided_by: "timeout"
|
|
16329
16654
|
});
|
|
16330
|
-
|
|
16655
|
+
resolve6(response);
|
|
16331
16656
|
}, this.config.timeout_seconds * 1e3);
|
|
16332
16657
|
const pending = {
|
|
16333
16658
|
id,
|
|
16334
16659
|
request,
|
|
16335
|
-
resolve:
|
|
16660
|
+
resolve: resolve6,
|
|
16336
16661
|
timer,
|
|
16337
16662
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
16338
16663
|
};
|
|
@@ -16537,6 +16862,18 @@ var DashboardApprovalChannel = class {
|
|
|
16537
16862
|
res.end();
|
|
16538
16863
|
return;
|
|
16539
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
|
+
}
|
|
16540
16877
|
if (this.v11Bindings) {
|
|
16541
16878
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
16542
16879
|
if (handled) return;
|
|
@@ -17193,7 +17530,7 @@ var WebhookApprovalChannel = class {
|
|
|
17193
17530
|
* Start the callback listener server.
|
|
17194
17531
|
*/
|
|
17195
17532
|
async start() {
|
|
17196
|
-
return new Promise((
|
|
17533
|
+
return new Promise((resolve6, reject) => {
|
|
17197
17534
|
this.callbackServer = http.createServer(
|
|
17198
17535
|
(req, res) => this.handleCallback(req, res)
|
|
17199
17536
|
);
|
|
@@ -17208,7 +17545,7 @@ var WebhookApprovalChannel = class {
|
|
|
17208
17545
|
|
|
17209
17546
|
`
|
|
17210
17547
|
);
|
|
17211
|
-
|
|
17548
|
+
resolve6();
|
|
17212
17549
|
}
|
|
17213
17550
|
);
|
|
17214
17551
|
this.callbackServer.on("error", reject);
|
|
@@ -17228,8 +17565,8 @@ var WebhookApprovalChannel = class {
|
|
|
17228
17565
|
}
|
|
17229
17566
|
this.pending.clear();
|
|
17230
17567
|
if (this.callbackServer) {
|
|
17231
|
-
return new Promise((
|
|
17232
|
-
this.callbackServer.close(() =>
|
|
17568
|
+
return new Promise((resolve6) => {
|
|
17569
|
+
this.callbackServer.close(() => resolve6());
|
|
17233
17570
|
});
|
|
17234
17571
|
}
|
|
17235
17572
|
}
|
|
@@ -17242,7 +17579,7 @@ var WebhookApprovalChannel = class {
|
|
|
17242
17579
|
`[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
|
|
17243
17580
|
`
|
|
17244
17581
|
);
|
|
17245
|
-
return new Promise((
|
|
17582
|
+
return new Promise((resolve6) => {
|
|
17246
17583
|
const timer = setTimeout(() => {
|
|
17247
17584
|
this.pending.delete(id);
|
|
17248
17585
|
const response = {
|
|
@@ -17251,12 +17588,12 @@ var WebhookApprovalChannel = class {
|
|
|
17251
17588
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17252
17589
|
decided_by: "timeout"
|
|
17253
17590
|
};
|
|
17254
|
-
|
|
17591
|
+
resolve6(response);
|
|
17255
17592
|
}, this.config.timeout_seconds * 1e3);
|
|
17256
17593
|
const pending = {
|
|
17257
17594
|
id,
|
|
17258
17595
|
request,
|
|
17259
|
-
resolve:
|
|
17596
|
+
resolve: resolve6,
|
|
17260
17597
|
timer,
|
|
17261
17598
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
17262
17599
|
};
|
|
@@ -18542,6 +18879,11 @@ var InjectionDetector = class {
|
|
|
18542
18879
|
}
|
|
18543
18880
|
};
|
|
18544
18881
|
|
|
18882
|
+
// src/principal-policy/deny-vocabulary.ts
|
|
18883
|
+
var AGENT_VISIBLE_DENY_REASONS = {
|
|
18884
|
+
REQUIRES_APPROVAL: "operation requires operator approval",
|
|
18885
|
+
NOT_PERMITTED: "operation not permitted"};
|
|
18886
|
+
|
|
18545
18887
|
// src/principal-policy/gate.ts
|
|
18546
18888
|
var ApprovalGate = class {
|
|
18547
18889
|
policy;
|
|
@@ -18550,14 +18892,25 @@ var ApprovalGate = class {
|
|
|
18550
18892
|
auditLog;
|
|
18551
18893
|
injectionDetector;
|
|
18552
18894
|
onInjectionAlert;
|
|
18895
|
+
onApprovalEvent;
|
|
18553
18896
|
proxyTierResolver;
|
|
18554
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
18897
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
18555
18898
|
this.policy = policy;
|
|
18556
18899
|
this.baseline = baseline;
|
|
18557
18900
|
this.channel = channel;
|
|
18558
18901
|
this.auditLog = auditLog;
|
|
18559
18902
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
18560
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;
|
|
18561
18914
|
}
|
|
18562
18915
|
/**
|
|
18563
18916
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -18594,10 +18947,16 @@ var ApprovalGate = class {
|
|
|
18594
18947
|
});
|
|
18595
18948
|
}
|
|
18596
18949
|
if (injectionResult.recommendation === "block") {
|
|
18950
|
+
this.auditLog.append("l2", `gate_injection_block:${operation}`, "system", {
|
|
18951
|
+
tier: 1,
|
|
18952
|
+
operation,
|
|
18953
|
+
injection_confidence: injectionResult.confidence,
|
|
18954
|
+
signal_count: injectionResult.signals.length
|
|
18955
|
+
});
|
|
18597
18956
|
return {
|
|
18598
18957
|
allowed: false,
|
|
18599
18958
|
tier: 1,
|
|
18600
|
-
reason:
|
|
18959
|
+
reason: AGENT_VISIBLE_DENY_REASONS.NOT_PERMITTED,
|
|
18601
18960
|
approval_required: false
|
|
18602
18961
|
};
|
|
18603
18962
|
}
|
|
@@ -18674,7 +19033,7 @@ var ApprovalGate = class {
|
|
|
18674
19033
|
this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
|
|
18675
19034
|
tier: 1,
|
|
18676
19035
|
operation,
|
|
18677
|
-
warning: "Operation is not classified in any policy tier
|
|
19036
|
+
warning: "Operation is not classified in any policy tier, defaulting to Tier 1 (require approval)"
|
|
18678
19037
|
});
|
|
18679
19038
|
return this.requestApproval(
|
|
18680
19039
|
operation,
|
|
@@ -18785,51 +19144,481 @@ var ApprovalGate = class {
|
|
|
18785
19144
|
}
|
|
18786
19145
|
/**
|
|
18787
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.
|
|
18788
19154
|
*/
|
|
18789
19155
|
async requestApproval(operation, tier, reason, context) {
|
|
19156
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
18790
19157
|
const request = {
|
|
18791
19158
|
operation,
|
|
18792
19159
|
tier,
|
|
18793
19160
|
reason,
|
|
18794
19161
|
context,
|
|
18795
|
-
timestamp:
|
|
19162
|
+
timestamp: requestTimestamp
|
|
18796
19163
|
};
|
|
18797
|
-
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
|
+
}
|
|
18798
19222
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
18799
19223
|
tier,
|
|
18800
19224
|
reason,
|
|
18801
19225
|
decided_by: response.decided_by
|
|
18802
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
|
+
}
|
|
18803
19246
|
return {
|
|
18804
19247
|
allowed: response.decision === "approve",
|
|
18805
19248
|
tier,
|
|
18806
|
-
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` :
|
|
19249
|
+
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
18807
19250
|
approval_required: true,
|
|
18808
19251
|
approval_response: response
|
|
18809
19252
|
};
|
|
18810
19253
|
}
|
|
18811
19254
|
/**
|
|
18812
|
-
* Summarize tool arguments for the approval prompt.
|
|
18813
|
-
* Strips potentially large values to keep the prompt readable.
|
|
19255
|
+
* Summarize tool arguments for the approval prompt.
|
|
19256
|
+
* Strips potentially large values to keep the prompt readable.
|
|
19257
|
+
*/
|
|
19258
|
+
summarizeArgs(args) {
|
|
19259
|
+
const summary = {};
|
|
19260
|
+
for (const [key, value] of Object.entries(args)) {
|
|
19261
|
+
if (typeof value === "string" && value.length > 100) {
|
|
19262
|
+
summary[key] = value.slice(0, 100) + "...";
|
|
19263
|
+
} else {
|
|
19264
|
+
summary[key] = value;
|
|
19265
|
+
}
|
|
19266
|
+
}
|
|
19267
|
+
return summary;
|
|
19268
|
+
}
|
|
19269
|
+
/** Get the baseline tracker for saving at session end */
|
|
19270
|
+
getBaseline() {
|
|
19271
|
+
return this.baseline;
|
|
19272
|
+
}
|
|
19273
|
+
/** Get the injection detector for stats/configuration access */
|
|
19274
|
+
getInjectionDetector() {
|
|
19275
|
+
return this.injectionDetector;
|
|
19276
|
+
}
|
|
19277
|
+
};
|
|
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.
|
|
18814
19534
|
*/
|
|
18815
|
-
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
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 {
|
|
18822
19559
|
}
|
|
18823
19560
|
}
|
|
18824
|
-
return summary;
|
|
18825
19561
|
}
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
|
|
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
|
+
}
|
|
18829
19588
|
}
|
|
18830
|
-
|
|
18831
|
-
|
|
18832
|
-
|
|
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
|
+
}
|
|
18833
19622
|
}
|
|
18834
19623
|
};
|
|
18835
19624
|
|
|
@@ -19373,7 +20162,11 @@ init_identity();
|
|
|
19373
20162
|
init_encoding();
|
|
19374
20163
|
init_random();
|
|
19375
20164
|
function generateNonce() {
|
|
19376
|
-
|
|
20165
|
+
const nonce = randomBytes(32);
|
|
20166
|
+
if (!nonce || nonce.length !== 32) {
|
|
20167
|
+
throw new Error("Nonce generation failed: randomBytes returned unexpected length");
|
|
20168
|
+
}
|
|
20169
|
+
return toBase64url(nonce);
|
|
19377
20170
|
}
|
|
19378
20171
|
function initiateHandshake(ourSHR) {
|
|
19379
20172
|
const nonce = generateNonce();
|
|
@@ -19484,6 +20277,18 @@ function completeHandshake(response, session, identityManager, masterKey, identi
|
|
|
19484
20277
|
return { completion, result };
|
|
19485
20278
|
}
|
|
19486
20279
|
function verifyCompletion(completion, session) {
|
|
20280
|
+
if (completion.protocol_version !== "1.0") {
|
|
20281
|
+
return {
|
|
20282
|
+
counterparty_id: "unknown",
|
|
20283
|
+
counterparty_shr: session.our_shr,
|
|
20284
|
+
verified: false,
|
|
20285
|
+
sovereignty_level: "unverified",
|
|
20286
|
+
trust_tier: "unverified",
|
|
20287
|
+
completed_at: completion.completed_at,
|
|
20288
|
+
expires_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20289
|
+
errors: [`Unsupported protocol version: ${completion.protocol_version}`]
|
|
20290
|
+
};
|
|
20291
|
+
}
|
|
19487
20292
|
const errors = [];
|
|
19488
20293
|
if (!session.their_shr) {
|
|
19489
20294
|
return {
|
|
@@ -21585,6 +22390,9 @@ Inspect the file and either correct the JSON or delete it manually before re-run
|
|
|
21585
22390
|
this.cause = cause;
|
|
21586
22391
|
this.name = "ResetHistoryMalformedError";
|
|
21587
22392
|
}
|
|
22393
|
+
markerPath;
|
|
22394
|
+
lineNumber;
|
|
22395
|
+
cause;
|
|
21588
22396
|
};
|
|
21589
22397
|
function parseResetHistory(content, markerPath) {
|
|
21590
22398
|
const markerHash = hashToString(stringToBytes(content));
|
|
@@ -21666,6 +22474,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
21666
22474
|
}
|
|
21667
22475
|
async function consumeResetHistoryMarker(options) {
|
|
21668
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
|
+
}
|
|
21669
22483
|
if (!await fileExists3(markerPath)) {
|
|
21670
22484
|
return { emitted: 0, markerPath };
|
|
21671
22485
|
}
|
|
@@ -21690,7 +22504,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
21690
22504
|
});
|
|
21691
22505
|
}
|
|
21692
22506
|
await options.auditLog.flush();
|
|
22507
|
+
await promises.writeFile(consumedPath, "", "utf-8");
|
|
21693
22508
|
await promises.rm(markerPath, { force: true });
|
|
22509
|
+
await promises.rm(consumedPath, { force: true });
|
|
21694
22510
|
return { emitted: markers.length, markerHash, markerPath };
|
|
21695
22511
|
}
|
|
21696
22512
|
async function fileExists3(path) {
|
|
@@ -23826,7 +24642,7 @@ async function runOpenAIPrivacyFilter(text, config) {
|
|
|
23826
24642
|
return parsed;
|
|
23827
24643
|
}
|
|
23828
24644
|
function runCommand(command, input, timeoutMs) {
|
|
23829
|
-
return new Promise((
|
|
24645
|
+
return new Promise((resolve6, reject) => {
|
|
23830
24646
|
const child = child_process.spawn(command, [], {
|
|
23831
24647
|
stdio: ["pipe", "pipe", "pipe"],
|
|
23832
24648
|
shell: false
|
|
@@ -23857,7 +24673,7 @@ function runCommand(command, input, timeoutMs) {
|
|
|
23857
24673
|
));
|
|
23858
24674
|
return;
|
|
23859
24675
|
}
|
|
23860
|
-
|
|
24676
|
+
resolve6(stdout);
|
|
23861
24677
|
});
|
|
23862
24678
|
child.stdin.end(input);
|
|
23863
24679
|
});
|
|
@@ -25678,13 +26494,13 @@ var ProxyRouter = class {
|
|
|
25678
26494
|
* Call an upstream tool with a timeout.
|
|
25679
26495
|
*/
|
|
25680
26496
|
async callWithTimeout(serverName, toolName, args, timeoutMs) {
|
|
25681
|
-
return new Promise((
|
|
26497
|
+
return new Promise((resolve6, reject) => {
|
|
25682
26498
|
const timer = setTimeout(() => {
|
|
25683
26499
|
reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
|
|
25684
26500
|
}, timeoutMs);
|
|
25685
26501
|
this.clientManager.callTool(serverName, toolName, args).then((result) => {
|
|
25686
26502
|
clearTimeout(timer);
|
|
25687
|
-
|
|
26503
|
+
resolve6(result);
|
|
25688
26504
|
}).catch((err) => {
|
|
25689
26505
|
clearTimeout(timer);
|
|
25690
26506
|
reject(err);
|
|
@@ -30636,6 +31452,36 @@ var HubService = class {
|
|
|
30636
31452
|
const chat = this.requireOperatorChat();
|
|
30637
31453
|
return chat.getConciergeHistory();
|
|
30638
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
|
+
}
|
|
30639
31485
|
/**
|
|
30640
31486
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
30641
31487
|
* panel surfaces recent activity routed through this agent, pending
|
|
@@ -30723,7 +31569,21 @@ init_encoding();
|
|
|
30723
31569
|
|
|
30724
31570
|
// src/chat/operator-chat-audit-events.ts
|
|
30725
31571
|
var OPERATOR_CHAT_OPS = {
|
|
30726
|
-
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
|
+
};
|
|
30727
31587
|
|
|
30728
31588
|
// src/chat/operator-chat-types.ts
|
|
30729
31589
|
var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
|
|
@@ -30731,6 +31591,33 @@ var CONCIERGE_THREAD_KEY = "_fortress";
|
|
|
30731
31591
|
|
|
30732
31592
|
// src/chat/operator-chat-service.ts
|
|
30733
31593
|
var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
31594
|
+
var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
31595
|
+
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
31596
|
+
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
31597
|
+
3. Charter (Cooperative MCP): the sovereignty surface for compliant agents. Policy gates, approval tiers, audit logging, and encrypted state all live here.
|
|
31598
|
+
4. Heralds: Concordia receipts and Verascore reputation. Cross-fortress accountability after an action completes.
|
|
31599
|
+
|
|
31600
|
+
Five channel templates (canonical names):
|
|
31601
|
+
- request-approve-act: agent proposes an action, operator approves or denies before execution.
|
|
31602
|
+
- read-then-report: agent reads outputs from a data source and reports summaries to the operator.
|
|
31603
|
+
- scheduled-digest: agent runs on a schedule and delivers a periodic digest.
|
|
31604
|
+
- plan-draft-only: agent drafts plans; operator reviews before any execution step.
|
|
31605
|
+
- fortress-relay: agent relays messages between fortresses under operator-scoped policy.
|
|
31606
|
+
|
|
31607
|
+
Four canonical policy slots:
|
|
31608
|
+
- memory: governs what the agent may persist and retrieve from encrypted state.
|
|
31609
|
+
- credentials: governs access to secrets, API keys, and tokens held in the broker.
|
|
31610
|
+
- plans: governs the agent's ability to create, modify, or execute plans.
|
|
31611
|
+
- outputs: governs what the agent may emit to external surfaces (files, APIs, messages).
|
|
31612
|
+
|
|
31613
|
+
Key concepts:
|
|
31614
|
+
- Fortress: the operator-owned sovereignty harness. All state is encrypted at rest under the cocoon.
|
|
31615
|
+
- Cocoon: master-key-wrapped storage derived from the operator's passphrase via Argon2id.
|
|
31616
|
+
- Identity: Ed25519 keypair with a DID, owned by the operator. Private keys never leave the cocoon.
|
|
31617
|
+
- Audit log: append-only encrypted blobs, sequential, recording every gate decision and tool call.
|
|
31618
|
+
- Wrapped agent: any agent runtime that connects to Sanctuary as an MCP client. Tier A (native), Tier B (adapter-wrapped), Tier C (escape hatch).
|
|
31619
|
+
|
|
31620
|
+
Note: this is a static reference block (v1.2.x). Dynamic context injection (live template list, policy schema) ships in v1.3.`;
|
|
30734
31621
|
var OperatorChatService = class {
|
|
30735
31622
|
store;
|
|
30736
31623
|
auditLog;
|
|
@@ -30739,6 +31626,14 @@ var OperatorChatService = class {
|
|
|
30739
31626
|
contextProviders;
|
|
30740
31627
|
piiFilter;
|
|
30741
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;
|
|
30742
31637
|
constructor(deps) {
|
|
30743
31638
|
this.store = deps.store;
|
|
30744
31639
|
this.auditLog = deps.auditLog;
|
|
@@ -30749,6 +31644,7 @@ var OperatorChatService = class {
|
|
|
30749
31644
|
}
|
|
30750
31645
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
30751
31646
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
31647
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
30752
31648
|
}
|
|
30753
31649
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
30754
31650
|
/**
|
|
@@ -30779,6 +31675,11 @@ var OperatorChatService = class {
|
|
|
30779
31675
|
CONCIERGE_THREAD_KEY,
|
|
30780
31676
|
operatorMessage
|
|
30781
31677
|
);
|
|
31678
|
+
if (this.memory) {
|
|
31679
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
31680
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
31681
|
+
});
|
|
31682
|
+
}
|
|
30782
31683
|
const start = Date.now();
|
|
30783
31684
|
let conciergeBody;
|
|
30784
31685
|
let servedBy = "disabled";
|
|
@@ -30834,6 +31735,11 @@ var OperatorChatService = class {
|
|
|
30834
31735
|
CONCIERGE_THREAD_KEY,
|
|
30835
31736
|
responseMessage
|
|
30836
31737
|
);
|
|
31738
|
+
if (this.memory) {
|
|
31739
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
31740
|
+
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
|
|
31741
|
+
});
|
|
31742
|
+
}
|
|
30837
31743
|
const payload = {
|
|
30838
31744
|
version: "1.2",
|
|
30839
31745
|
event_id: makeEventId("conc"),
|
|
@@ -30866,6 +31772,105 @@ var OperatorChatService = class {
|
|
|
30866
31772
|
);
|
|
30867
31773
|
return thread ? thread.messages : [];
|
|
30868
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
|
+
}
|
|
30869
31874
|
/**
|
|
30870
31875
|
* Stitch fortress state into a single context blob the substrate
|
|
30871
31876
|
* folds into its summarization prompt.
|
|
@@ -30875,6 +31880,9 @@ var OperatorChatService = class {
|
|
|
30875
31880
|
* than nested structures. Format:
|
|
30876
31881
|
*
|
|
30877
31882
|
* ```
|
|
31883
|
+
* ## Sanctuary reference
|
|
31884
|
+
* <static domain reference block>
|
|
31885
|
+
*
|
|
30878
31886
|
* ## Recent activity
|
|
30879
31887
|
* <recentActivity output>
|
|
30880
31888
|
*
|
|
@@ -30886,15 +31894,28 @@ var OperatorChatService = class {
|
|
|
30886
31894
|
* ```
|
|
30887
31895
|
*/
|
|
30888
31896
|
async assembleConciergeContext() {
|
|
31897
|
+
const ref = `## Sanctuary reference
|
|
31898
|
+
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
30889
31899
|
if (!this.contextProviders) {
|
|
30890
|
-
return
|
|
31900
|
+
return `${ref}
|
|
31901
|
+
|
|
31902
|
+
## Recent activity
|
|
31903
|
+
(no providers wired)
|
|
31904
|
+
|
|
31905
|
+
## Wrapped agents
|
|
31906
|
+
(no providers wired)
|
|
31907
|
+
|
|
31908
|
+
## Open inbox
|
|
31909
|
+
(no providers wired)`;
|
|
30891
31910
|
}
|
|
30892
31911
|
const [activity, agents, inbox] = await Promise.all([
|
|
30893
31912
|
this.contextProviders.recentActivity(),
|
|
30894
31913
|
this.contextProviders.agentInventory(),
|
|
30895
31914
|
this.contextProviders.openInbox()
|
|
30896
31915
|
]);
|
|
30897
|
-
return
|
|
31916
|
+
return `${ref}
|
|
31917
|
+
|
|
31918
|
+
## Recent activity
|
|
30898
31919
|
${activity}
|
|
30899
31920
|
|
|
30900
31921
|
## Wrapped agents
|
|
@@ -31013,6 +32034,238 @@ var OperatorChatStore = class {
|
|
|
31013
32034
|
}
|
|
31014
32035
|
};
|
|
31015
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
|
+
|
|
31016
32269
|
// src/dashboard/v1_1/wiring.ts
|
|
31017
32270
|
var CapabilityErrorAgentController = class {
|
|
31018
32271
|
fail(action) {
|
|
@@ -31051,6 +32304,14 @@ function buildV11Bindings(inputs) {
|
|
|
31051
32304
|
let operatorChatService;
|
|
31052
32305
|
if (inputs.storage && inputs.masterKey) {
|
|
31053
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
|
+
});
|
|
31054
32315
|
operatorChatService = new OperatorChatService({
|
|
31055
32316
|
store: chatStore,
|
|
31056
32317
|
auditLog: inputs.auditLog,
|
|
@@ -31061,7 +32322,8 @@ function buildV11Bindings(inputs) {
|
|
|
31061
32322
|
identityId: inputs.identityId,
|
|
31062
32323
|
registry
|
|
31063
32324
|
}),
|
|
31064
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
32325
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
32326
|
+
conciergeMemory
|
|
31065
32327
|
});
|
|
31066
32328
|
}
|
|
31067
32329
|
const hubService = new HubService({
|
|
@@ -31253,13 +32515,13 @@ init_encryption();
|
|
|
31253
32515
|
init_encoding();
|
|
31254
32516
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
31255
32517
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
31256
|
-
var
|
|
32518
|
+
var HKDF_INFO3 = "intelligence-substrate-config";
|
|
31257
32519
|
var IntelligenceConfigStore = class {
|
|
31258
32520
|
storage;
|
|
31259
32521
|
encryptionKey;
|
|
31260
32522
|
constructor(storage, masterKey) {
|
|
31261
32523
|
this.storage = storage;
|
|
31262
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
32524
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
31263
32525
|
}
|
|
31264
32526
|
/**
|
|
31265
32527
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -33398,7 +34660,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33398
34660
|
);
|
|
33399
34661
|
}
|
|
33400
34662
|
}
|
|
33401
|
-
const
|
|
34663
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
34664
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
34665
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
33402
34666
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
33403
34667
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
33404
34668
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -33407,6 +34671,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33407
34671
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
33408
34672
|
);
|
|
33409
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
|
+
}
|
|
33410
34684
|
return {
|
|
33411
34685
|
version: "1.1",
|
|
33412
34686
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -33426,7 +34700,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33426
34700
|
identity,
|
|
33427
34701
|
audit,
|
|
33428
34702
|
reputation,
|
|
33429
|
-
failure_class:
|
|
34703
|
+
failure_class: detailedFailureClass
|
|
33430
34704
|
};
|
|
33431
34705
|
}
|
|
33432
34706
|
|
|
@@ -33999,6 +35273,9 @@ async function importExitBundle(opts) {
|
|
|
33999
35273
|
reputationArtifact?.json ?? null,
|
|
34000
35274
|
manifest
|
|
34001
35275
|
);
|
|
35276
|
+
if (!conflicts.public_identity_exists && identityArtifact?.json && opts.identityManager.getPrimaryIdentityId() !== null && opts.identityManager.getPrimaryIdentityId() !== identityArtifact.json.bundle.identity_id) {
|
|
35277
|
+
conflicts.public_identity_exists = true;
|
|
35278
|
+
}
|
|
34002
35279
|
if (!opts.activate) {
|
|
34003
35280
|
return {
|
|
34004
35281
|
verified: true,
|
|
@@ -34025,7 +35302,7 @@ async function importExitBundle(opts) {
|
|
|
34025
35302
|
if (conflicts.public_identity_exists && !opts.forceRebind) {
|
|
34026
35303
|
throw new ExitBundleImportError(
|
|
34027
35304
|
"IDENTITY_OVERWRITE_REFUSED",
|
|
34028
|
-
"Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
|
|
35305
|
+
"Importing this exit bundle would overwrite an existing fortress public identity (either the same identity already imported, or a different identity is currently active). Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
|
|
34029
35306
|
);
|
|
34030
35307
|
}
|
|
34031
35308
|
if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
|
|
@@ -34350,7 +35627,19 @@ async function runExitCommand(args) {
|
|
|
34350
35627
|
}
|
|
34351
35628
|
const config = await loadConfig();
|
|
34352
35629
|
const ctx = await openExitContext(argv, env);
|
|
34353
|
-
|
|
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
|
+
}
|
|
34354
35643
|
const result = await exportExitBundle({
|
|
34355
35644
|
bundleDir: outDir,
|
|
34356
35645
|
storage: ctx.storage,
|
|
@@ -34383,6 +35672,26 @@ async function runExitCommand(args) {
|
|
|
34383
35672
|
write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
|
|
34384
35673
|
return 2;
|
|
34385
35674
|
}
|
|
35675
|
+
const bundleRoot = path.resolve(dir);
|
|
35676
|
+
try {
|
|
35677
|
+
await promises.access(bundleRoot);
|
|
35678
|
+
} catch {
|
|
35679
|
+
write(err, `Error: bundle directory not found: ${bundleRoot}
|
|
35680
|
+
`);
|
|
35681
|
+
return 1;
|
|
35682
|
+
}
|
|
35683
|
+
const manifestPath = path.join(bundleRoot, "manifest.json");
|
|
35684
|
+
try {
|
|
35685
|
+
const raw = await promises.readFile(manifestPath, "utf8");
|
|
35686
|
+
JSON.parse(raw);
|
|
35687
|
+
} catch {
|
|
35688
|
+
write(
|
|
35689
|
+
err,
|
|
35690
|
+
`Error: bundle manifest missing or malformed at ${manifestPath}
|
|
35691
|
+
`
|
|
35692
|
+
);
|
|
35693
|
+
return 1;
|
|
35694
|
+
}
|
|
34386
35695
|
const activate = hasFlag(argv, "--activate");
|
|
34387
35696
|
const forceRebind = hasFlag(argv, "--force-rebind");
|
|
34388
35697
|
const acceptUnverifiableAttestations = hasFlag(
|
|
@@ -34523,11 +35832,11 @@ async function startDashboardServer(options) {
|
|
|
34523
35832
|
}
|
|
34524
35833
|
}
|
|
34525
35834
|
});
|
|
34526
|
-
await new Promise((
|
|
35835
|
+
await new Promise((resolve6, reject) => {
|
|
34527
35836
|
server.once("error", reject);
|
|
34528
35837
|
server.listen(port, host, () => {
|
|
34529
35838
|
server.off("error", reject);
|
|
34530
|
-
|
|
35839
|
+
resolve6();
|
|
34531
35840
|
});
|
|
34532
35841
|
});
|
|
34533
35842
|
const actualPort = (() => {
|
|
@@ -34540,8 +35849,8 @@ async function startDashboardServer(options) {
|
|
|
34540
35849
|
url,
|
|
34541
35850
|
port: actualPort,
|
|
34542
35851
|
host,
|
|
34543
|
-
stop: () => new Promise((
|
|
34544
|
-
server.close((err) => err ? reject(err) :
|
|
35852
|
+
stop: () => new Promise((resolve6, reject) => {
|
|
35853
|
+
server.close((err) => err ? reject(err) : resolve6());
|
|
34545
35854
|
}),
|
|
34546
35855
|
publish,
|
|
34547
35856
|
publishActivity: (entry) => publish({ type: "activity", data: entry }),
|
|
@@ -34982,7 +36291,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
34982
36291
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
34983
36292
|
await profileStore.load();
|
|
34984
36293
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
34985
|
-
|
|
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
|
+
}
|
|
34986
36307
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
34987
36308
|
await baseline.load();
|
|
34988
36309
|
let approvalChannel;
|
|
@@ -35080,6 +36401,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35080
36401
|
});
|
|
35081
36402
|
} : void 0;
|
|
35082
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
|
+
}
|
|
35083
36419
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
35084
36420
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
35085
36421
|
config,
|
|
@@ -35199,7 +36535,7 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35199
36535
|
clientManager.configure(enabledServers).catch((err) => {
|
|
35200
36536
|
console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
35201
36537
|
});
|
|
35202
|
-
await new Promise((
|
|
36538
|
+
await new Promise((resolve6) => setTimeout(resolve6, 2e3));
|
|
35203
36539
|
const proxiedTools = proxyRouter.getProxiedTools();
|
|
35204
36540
|
if (proxiedTools.length > 0) {
|
|
35205
36541
|
allTools.push(...proxiedTools);
|
|
@@ -35274,6 +36610,7 @@ exports.HERO_COPY = HERO_COPY;
|
|
|
35274
36610
|
exports.InMemoryModelProvenanceStore = InMemoryModelProvenanceStore;
|
|
35275
36611
|
exports.InjectionDetector = InjectionDetector;
|
|
35276
36612
|
exports.MODEL_PRESETS = MODEL_PRESETS;
|
|
36613
|
+
exports.MalformedPrincipalPolicyError = MalformedPrincipalPolicyError;
|
|
35277
36614
|
exports.MemoryStorage = MemoryStorage;
|
|
35278
36615
|
exports.PolicyStore = PolicyStore;
|
|
35279
36616
|
exports.ProxyRouter = ProxyRouter;
|