@sanctuary-framework/mcp-server 1.2.3 → 1.2.5
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 +2329 -136
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2329 -136
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2009 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +689 -7
- package/dist/index.d.ts +689 -7
- package/dist/index.js +2009 -133
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4254,6 +4254,10 @@ var DEFAULT_CHANNEL = {
|
|
|
4254
4254
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4255
4255
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4256
4256
|
};
|
|
4257
|
+
var DEFAULT_APPROVAL_REDIRECT = {
|
|
4258
|
+
enabled: false,
|
|
4259
|
+
mode: "replace"
|
|
4260
|
+
};
|
|
4257
4261
|
var DEFAULT_POLICY = {
|
|
4258
4262
|
version: 1,
|
|
4259
4263
|
tier1_always_approve: [
|
|
@@ -4327,6 +4331,7 @@ var DEFAULT_POLICY = {
|
|
|
4327
4331
|
"handshake_status",
|
|
4328
4332
|
"handshake_exchange",
|
|
4329
4333
|
"handshake_verify_attestation",
|
|
4334
|
+
"handshake_abort",
|
|
4330
4335
|
"reputation_query_weighted",
|
|
4331
4336
|
"federation_peers",
|
|
4332
4337
|
"federation_trust_evaluate",
|
|
@@ -4368,7 +4373,8 @@ var DEFAULT_POLICY = {
|
|
|
4368
4373
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4369
4374
|
// Read-only; rule-based Annex III classifier
|
|
4370
4375
|
],
|
|
4371
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4376
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4377
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4372
4378
|
};
|
|
4373
4379
|
function extractOperationName(toolName) {
|
|
4374
4380
|
if (toolName.startsWith("proxy/")) {
|
|
@@ -4465,9 +4471,35 @@ function validatePolicy(raw) {
|
|
|
4465
4471
|
};
|
|
4466
4472
|
delete merged.auto_deny;
|
|
4467
4473
|
return merged;
|
|
4468
|
-
})()
|
|
4474
|
+
})(),
|
|
4475
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4469
4476
|
};
|
|
4470
4477
|
}
|
|
4478
|
+
function parseApprovalRedirect(raw) {
|
|
4479
|
+
if (raw === void 0 || raw === null) {
|
|
4480
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4481
|
+
}
|
|
4482
|
+
if (typeof raw !== "object") {
|
|
4483
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4484
|
+
}
|
|
4485
|
+
const obj = raw;
|
|
4486
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4487
|
+
const modeRaw = obj.mode;
|
|
4488
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4489
|
+
if (modeRaw !== void 0) {
|
|
4490
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4491
|
+
throw new Error(
|
|
4492
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4493
|
+
);
|
|
4494
|
+
}
|
|
4495
|
+
mode = modeRaw;
|
|
4496
|
+
}
|
|
4497
|
+
const result = { enabled, mode };
|
|
4498
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4499
|
+
result.per_agent = obj.per_agent;
|
|
4500
|
+
}
|
|
4501
|
+
return result;
|
|
4502
|
+
}
|
|
4471
4503
|
function generateDefaultPolicyYaml() {
|
|
4472
4504
|
return `# Sanctuary Principal Policy v1
|
|
4473
4505
|
# This file controls what your agent can do without asking.
|
|
@@ -4546,6 +4578,7 @@ tier3_always_allow:
|
|
|
4546
4578
|
- handshake_status
|
|
4547
4579
|
- handshake_exchange
|
|
4548
4580
|
- handshake_verify_attestation
|
|
4581
|
+
- handshake_abort
|
|
4549
4582
|
- reputation_query_weighted
|
|
4550
4583
|
- federation_peers
|
|
4551
4584
|
- federation_trust_evaluate
|
|
@@ -4580,22 +4613,69 @@ tier3_always_allow:
|
|
|
4580
4613
|
approval_channel:
|
|
4581
4614
|
type: stderr
|
|
4582
4615
|
timeout_seconds: 300
|
|
4616
|
+
|
|
4617
|
+
# \u2500\u2500\u2500 Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4618
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4619
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4620
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4621
|
+
#
|
|
4622
|
+
# mode:
|
|
4623
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4624
|
+
# decision from the inbox (default once enabled).
|
|
4625
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4626
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4627
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4628
|
+
approval_redirect:
|
|
4629
|
+
enabled: false
|
|
4630
|
+
mode: replace
|
|
4583
4631
|
`;
|
|
4584
4632
|
}
|
|
4633
|
+
var MalformedPrincipalPolicyError = class extends Error {
|
|
4634
|
+
constructor(policyPath, reason) {
|
|
4635
|
+
super(
|
|
4636
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4637
|
+
Reason: ${reason}
|
|
4638
|
+
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.`
|
|
4639
|
+
);
|
|
4640
|
+
this.policyPath = policyPath;
|
|
4641
|
+
this.reason = reason;
|
|
4642
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4643
|
+
}
|
|
4644
|
+
policyPath;
|
|
4645
|
+
reason;
|
|
4646
|
+
};
|
|
4585
4647
|
async function loadPrincipalPolicy(storagePath) {
|
|
4586
4648
|
const policyPath = join(storagePath, "principal-policy.yaml");
|
|
4649
|
+
let content;
|
|
4650
|
+
try {
|
|
4651
|
+
content = await readFile(policyPath, "utf-8");
|
|
4652
|
+
} catch (err) {
|
|
4653
|
+
const code = err?.code;
|
|
4654
|
+
if (code === "ENOENT") {
|
|
4655
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4656
|
+
try {
|
|
4657
|
+
await writeFile(policyPath, defaultYaml, "utf-8");
|
|
4658
|
+
await chmod(policyPath, 384);
|
|
4659
|
+
} catch (writeErr) {
|
|
4660
|
+
console.warn(
|
|
4661
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4662
|
+
);
|
|
4663
|
+
}
|
|
4664
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4665
|
+
}
|
|
4666
|
+
throw new MalformedPrincipalPolicyError(
|
|
4667
|
+
policyPath,
|
|
4668
|
+
`read failed: ${err.message}`
|
|
4669
|
+
);
|
|
4670
|
+
}
|
|
4587
4671
|
try {
|
|
4588
|
-
const content = await readFile(policyPath, "utf-8");
|
|
4589
4672
|
const policy = parsePolicy(content);
|
|
4590
4673
|
return Object.freeze(policy);
|
|
4591
|
-
} catch {
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
} catch {
|
|
4597
|
-
}
|
|
4598
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4674
|
+
} catch (parseErr) {
|
|
4675
|
+
throw new MalformedPrincipalPolicyError(
|
|
4676
|
+
policyPath,
|
|
4677
|
+
parseErr.message
|
|
4678
|
+
);
|
|
4599
4679
|
}
|
|
4600
4680
|
}
|
|
4601
4681
|
|
|
@@ -4822,7 +4902,7 @@ function deepSortKeys(obj) {
|
|
|
4822
4902
|
return sorted;
|
|
4823
4903
|
}
|
|
4824
4904
|
function canonicalizeForSigning(body) {
|
|
4825
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4905
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4826
4906
|
}
|
|
4827
4907
|
|
|
4828
4908
|
// src/shr/generator.ts
|
|
@@ -11644,6 +11724,16 @@ var HUB_ROUTES = {
|
|
|
11644
11724
|
*/
|
|
11645
11725
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
11646
11726
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
11727
|
+
/**
|
|
11728
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
11729
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
11730
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
11731
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
11732
|
+
* v1.3 conversational sovereignty depth.
|
|
11733
|
+
*/
|
|
11734
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
11735
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11736
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
11647
11737
|
/**
|
|
11648
11738
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
11649
11739
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -11667,6 +11757,10 @@ var HUB_TIER_1_AGENT_CONTROL_ACTIONS = [
|
|
|
11667
11757
|
];
|
|
11668
11758
|
var HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
11669
11759
|
var HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
11760
|
+
var HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
11761
|
+
var HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
11762
|
+
var HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
11763
|
+
var HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
11670
11764
|
var HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
11671
11765
|
var HUB_INBOX_MAX_LIMIT = 500;
|
|
11672
11766
|
var HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -11838,6 +11932,23 @@ function checkChatMessage(value) {
|
|
|
11838
11932
|
}
|
|
11839
11933
|
return trimmed;
|
|
11840
11934
|
}
|
|
11935
|
+
function matchConciergeThreadRoute(path) {
|
|
11936
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
11937
|
+
if (!path.startsWith(prefix)) return null;
|
|
11938
|
+
const rest = path.slice(prefix.length);
|
|
11939
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
11940
|
+
const decoded = decodeURIComponent(rest);
|
|
11941
|
+
if (decoded.length === 0) return null;
|
|
11942
|
+
return { threadId: decoded };
|
|
11943
|
+
}
|
|
11944
|
+
function parseSince(raw) {
|
|
11945
|
+
if (raw === null || raw === "") return void 0;
|
|
11946
|
+
const parsed = Number.parseInt(raw, 10);
|
|
11947
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
11948
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
11949
|
+
}
|
|
11950
|
+
return parsed;
|
|
11951
|
+
}
|
|
11841
11952
|
function matchInboxRoute(path) {
|
|
11842
11953
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
11843
11954
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12021,6 +12132,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12021
12132
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12022
12133
|
return true;
|
|
12023
12134
|
}
|
|
12135
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12136
|
+
const limit = parseLimit(
|
|
12137
|
+
url.searchParams.get("limit"),
|
|
12138
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12139
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12140
|
+
);
|
|
12141
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12142
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12143
|
+
return true;
|
|
12144
|
+
}
|
|
12145
|
+
{
|
|
12146
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12147
|
+
if (threadMatch) {
|
|
12148
|
+
if (method === "GET") {
|
|
12149
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12150
|
+
const limit = parseLimit(
|
|
12151
|
+
url.searchParams.get("limit"),
|
|
12152
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12153
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12154
|
+
);
|
|
12155
|
+
const readOpts = { limit };
|
|
12156
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12157
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12158
|
+
threadMatch.threadId,
|
|
12159
|
+
readOpts
|
|
12160
|
+
);
|
|
12161
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12162
|
+
return true;
|
|
12163
|
+
}
|
|
12164
|
+
if (method === "DELETE") {
|
|
12165
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12166
|
+
threadMatch.threadId
|
|
12167
|
+
);
|
|
12168
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12169
|
+
ok: removed,
|
|
12170
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12171
|
+
});
|
|
12172
|
+
return true;
|
|
12173
|
+
}
|
|
12174
|
+
}
|
|
12175
|
+
}
|
|
12024
12176
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12025
12177
|
return true;
|
|
12026
12178
|
} catch (err) {
|
|
@@ -16070,6 +16222,162 @@ async function dispatchV11Request(inputs, req, res, url, method) {
|
|
|
16070
16222
|
return false;
|
|
16071
16223
|
}
|
|
16072
16224
|
|
|
16225
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
16226
|
+
var APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
16227
|
+
var APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
16228
|
+
var APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
16229
|
+
var APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
16230
|
+
function writeJSON4(res, status, payload) {
|
|
16231
|
+
res.writeHead(status, {
|
|
16232
|
+
"Content-Type": "application/json",
|
|
16233
|
+
"Cache-Control": "no-store"
|
|
16234
|
+
});
|
|
16235
|
+
res.end(JSON.stringify(payload));
|
|
16236
|
+
}
|
|
16237
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
16238
|
+
if (raw === null || raw === "") return defaultValue;
|
|
16239
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16240
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
16241
|
+
return defaultValue;
|
|
16242
|
+
}
|
|
16243
|
+
return Math.min(parsed, max);
|
|
16244
|
+
}
|
|
16245
|
+
function isStatusFilter(value) {
|
|
16246
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
16247
|
+
}
|
|
16248
|
+
function matchEntryRoute(path) {
|
|
16249
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
16250
|
+
if (!path.startsWith(prefix)) return null;
|
|
16251
|
+
const rest = path.slice(prefix.length);
|
|
16252
|
+
if (rest.length === 0) return null;
|
|
16253
|
+
const slash = rest.indexOf("/");
|
|
16254
|
+
if (slash === -1) {
|
|
16255
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
16256
|
+
}
|
|
16257
|
+
return {
|
|
16258
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
16259
|
+
action: rest.slice(slash + 1)
|
|
16260
|
+
};
|
|
16261
|
+
}
|
|
16262
|
+
async function handleStream2(deps, res) {
|
|
16263
|
+
res.writeHead(200, {
|
|
16264
|
+
"Content-Type": "text/event-stream",
|
|
16265
|
+
"Cache-Control": "no-cache, no-transform",
|
|
16266
|
+
Connection: "keep-alive",
|
|
16267
|
+
"X-Accel-Buffering": "no"
|
|
16268
|
+
});
|
|
16269
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
16270
|
+
res.write(
|
|
16271
|
+
`event: approval_inbox_snapshot
|
|
16272
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
16273
|
+
|
|
16274
|
+
`
|
|
16275
|
+
);
|
|
16276
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
16277
|
+
try {
|
|
16278
|
+
res.write(
|
|
16279
|
+
`event: approval_inbox_${event.type}
|
|
16280
|
+
data: ${JSON.stringify(event.entry)}
|
|
16281
|
+
|
|
16282
|
+
`
|
|
16283
|
+
);
|
|
16284
|
+
} catch {
|
|
16285
|
+
}
|
|
16286
|
+
});
|
|
16287
|
+
const keepAlive = setInterval(() => {
|
|
16288
|
+
try {
|
|
16289
|
+
res.write(": keepalive\n\n");
|
|
16290
|
+
} catch {
|
|
16291
|
+
}
|
|
16292
|
+
}, 25e3);
|
|
16293
|
+
const cleanup = () => {
|
|
16294
|
+
clearInterval(keepAlive);
|
|
16295
|
+
unsubscribe();
|
|
16296
|
+
};
|
|
16297
|
+
res.on("close", cleanup);
|
|
16298
|
+
res.on("error", cleanup);
|
|
16299
|
+
}
|
|
16300
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
16301
|
+
const host = req.headers.host || "localhost";
|
|
16302
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
16303
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
16304
|
+
const path = url.pathname;
|
|
16305
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
16306
|
+
return false;
|
|
16307
|
+
}
|
|
16308
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
16309
|
+
if (!checkAuth(req, res, url)) return true;
|
|
16310
|
+
try {
|
|
16311
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
16312
|
+
await handleStream2(deps, res);
|
|
16313
|
+
return true;
|
|
16314
|
+
}
|
|
16315
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
16316
|
+
const limit = parseLimit2(
|
|
16317
|
+
url.searchParams.get("limit"),
|
|
16318
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
16319
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
16320
|
+
);
|
|
16321
|
+
const statusRaw = url.searchParams.get("status");
|
|
16322
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
16323
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
16324
|
+
const entries = await deps.aggregator.list({
|
|
16325
|
+
status,
|
|
16326
|
+
limit,
|
|
16327
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
16328
|
+
});
|
|
16329
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
16330
|
+
return true;
|
|
16331
|
+
}
|
|
16332
|
+
const entryMatch = matchEntryRoute(path);
|
|
16333
|
+
if (entryMatch === null) {
|
|
16334
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16335
|
+
return true;
|
|
16336
|
+
}
|
|
16337
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
16338
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
16339
|
+
const entry = entries.find(
|
|
16340
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
16341
|
+
);
|
|
16342
|
+
if (!entry) {
|
|
16343
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16344
|
+
return true;
|
|
16345
|
+
}
|
|
16346
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
16347
|
+
entryMatch.aggregatorId
|
|
16348
|
+
);
|
|
16349
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
16350
|
+
return true;
|
|
16351
|
+
}
|
|
16352
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
16353
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
16354
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
16355
|
+
try {
|
|
16356
|
+
const entry = await deps.aggregator.resolve(
|
|
16357
|
+
entryMatch.aggregatorId,
|
|
16358
|
+
decision,
|
|
16359
|
+
operatorId
|
|
16360
|
+
);
|
|
16361
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
16362
|
+
} catch (err) {
|
|
16363
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16364
|
+
if (msg === "approval-aggregator: not_found") {
|
|
16365
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
16366
|
+
} else {
|
|
16367
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16368
|
+
}
|
|
16369
|
+
}
|
|
16370
|
+
return true;
|
|
16371
|
+
}
|
|
16372
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
16373
|
+
return true;
|
|
16374
|
+
} catch (err) {
|
|
16375
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16376
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16377
|
+
return true;
|
|
16378
|
+
}
|
|
16379
|
+
}
|
|
16380
|
+
|
|
16073
16381
|
// src/principal-policy/dashboard.ts
|
|
16074
16382
|
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16075
16383
|
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -16134,6 +16442,14 @@ var DashboardApprovalChannel = class {
|
|
|
16134
16442
|
* regardless. Default route flip is deferred to v1.2.
|
|
16135
16443
|
*/
|
|
16136
16444
|
v11Bindings = null;
|
|
16445
|
+
/**
|
|
16446
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
16447
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
16448
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
16449
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
16450
|
+
* the operator-facing query / decision surface.
|
|
16451
|
+
*/
|
|
16452
|
+
approvalAggregator = null;
|
|
16137
16453
|
constructor(config) {
|
|
16138
16454
|
this.config = config;
|
|
16139
16455
|
this.authToken = config.auth_token;
|
|
@@ -16184,6 +16500,34 @@ var DashboardApprovalChannel = class {
|
|
|
16184
16500
|
setV11Bindings(bindings) {
|
|
16185
16501
|
this.v11Bindings = bindings;
|
|
16186
16502
|
}
|
|
16503
|
+
/**
|
|
16504
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
16505
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
16506
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
16507
|
+
* tests + during shutdown).
|
|
16508
|
+
*/
|
|
16509
|
+
setApprovalAggregator(aggregator) {
|
|
16510
|
+
this.approvalAggregator = aggregator;
|
|
16511
|
+
}
|
|
16512
|
+
/**
|
|
16513
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
16514
|
+
* before the legacy approval route table. Returns true when served.
|
|
16515
|
+
*/
|
|
16516
|
+
async dispatchApprovalInbox(req, res) {
|
|
16517
|
+
if (!this.approvalAggregator) return false;
|
|
16518
|
+
return handleApprovalInboxRoute(
|
|
16519
|
+
{
|
|
16520
|
+
authConfig: {
|
|
16521
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
16522
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
16523
|
+
},
|
|
16524
|
+
aggregator: this.approvalAggregator,
|
|
16525
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
16526
|
+
},
|
|
16527
|
+
req,
|
|
16528
|
+
res
|
|
16529
|
+
);
|
|
16530
|
+
}
|
|
16187
16531
|
/**
|
|
16188
16532
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
16189
16533
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -16559,6 +16903,18 @@ var DashboardApprovalChannel = class {
|
|
|
16559
16903
|
res.end();
|
|
16560
16904
|
return;
|
|
16561
16905
|
}
|
|
16906
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
16907
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
16908
|
+
if (handled) return;
|
|
16909
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
16910
|
+
}).catch(() => {
|
|
16911
|
+
if (!res.headersSent) {
|
|
16912
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
16913
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
16914
|
+
}
|
|
16915
|
+
});
|
|
16916
|
+
return;
|
|
16917
|
+
}
|
|
16562
16918
|
if (this.v11Bindings) {
|
|
16563
16919
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
16564
16920
|
if (handled) return;
|
|
@@ -18577,14 +18933,25 @@ var ApprovalGate = class {
|
|
|
18577
18933
|
auditLog;
|
|
18578
18934
|
injectionDetector;
|
|
18579
18935
|
onInjectionAlert;
|
|
18936
|
+
onApprovalEvent;
|
|
18580
18937
|
proxyTierResolver;
|
|
18581
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
18938
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
18582
18939
|
this.policy = policy;
|
|
18583
18940
|
this.baseline = baseline;
|
|
18584
18941
|
this.channel = channel;
|
|
18585
18942
|
this.auditLog = auditLog;
|
|
18586
18943
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
18587
18944
|
this.onInjectionAlert = onInjectionAlert;
|
|
18945
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
18946
|
+
}
|
|
18947
|
+
/**
|
|
18948
|
+
* Set the approval-event callback after construction. Used by the
|
|
18949
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
18950
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
18951
|
+
* constructor so existing call sites continue to work unchanged.
|
|
18952
|
+
*/
|
|
18953
|
+
setApprovalEventCallback(cb) {
|
|
18954
|
+
this.onApprovalEvent = cb;
|
|
18588
18955
|
}
|
|
18589
18956
|
/**
|
|
18590
18957
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -18818,21 +19185,105 @@ var ApprovalGate = class {
|
|
|
18818
19185
|
}
|
|
18819
19186
|
/**
|
|
18820
19187
|
* Request approval from the human principal.
|
|
19188
|
+
*
|
|
19189
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
19190
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
19191
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
19192
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
19193
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
19194
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
18821
19195
|
*/
|
|
18822
19196
|
async requestApproval(operation, tier, reason, context) {
|
|
19197
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
18823
19198
|
const request = {
|
|
18824
19199
|
operation,
|
|
18825
19200
|
tier,
|
|
18826
19201
|
reason,
|
|
18827
19202
|
context,
|
|
18828
|
-
timestamp:
|
|
19203
|
+
timestamp: requestTimestamp
|
|
18829
19204
|
};
|
|
18830
|
-
const
|
|
19205
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
19206
|
+
if (this.onApprovalEvent) {
|
|
19207
|
+
try {
|
|
19208
|
+
this.onApprovalEvent({
|
|
19209
|
+
phase: "requested",
|
|
19210
|
+
operation,
|
|
19211
|
+
tier,
|
|
19212
|
+
reason,
|
|
19213
|
+
context,
|
|
19214
|
+
request_timestamp: requestTimestamp,
|
|
19215
|
+
correlation_id: correlationId
|
|
19216
|
+
});
|
|
19217
|
+
} catch {
|
|
19218
|
+
}
|
|
19219
|
+
}
|
|
19220
|
+
let response;
|
|
19221
|
+
try {
|
|
19222
|
+
response = await this.channel.requestApproval(request);
|
|
19223
|
+
} catch (err) {
|
|
19224
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
19225
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19226
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
19227
|
+
tier,
|
|
19228
|
+
reason,
|
|
19229
|
+
decided_by: "channel_failure",
|
|
19230
|
+
channel_error: errMessage
|
|
19231
|
+
});
|
|
19232
|
+
if (this.onApprovalEvent) {
|
|
19233
|
+
try {
|
|
19234
|
+
this.onApprovalEvent({
|
|
19235
|
+
phase: "resolved",
|
|
19236
|
+
operation,
|
|
19237
|
+
tier,
|
|
19238
|
+
reason,
|
|
19239
|
+
context,
|
|
19240
|
+
request_timestamp: requestTimestamp,
|
|
19241
|
+
resolution: {
|
|
19242
|
+
decision: "deny",
|
|
19243
|
+
decided_at: decidedAt,
|
|
19244
|
+
decided_by: "channel_failure"
|
|
19245
|
+
},
|
|
19246
|
+
correlation_id: correlationId
|
|
19247
|
+
});
|
|
19248
|
+
} catch {
|
|
19249
|
+
}
|
|
19250
|
+
}
|
|
19251
|
+
return {
|
|
19252
|
+
allowed: false,
|
|
19253
|
+
tier,
|
|
19254
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
19255
|
+
approval_required: true,
|
|
19256
|
+
approval_response: {
|
|
19257
|
+
decision: "deny",
|
|
19258
|
+
decided_at: decidedAt,
|
|
19259
|
+
decided_by: "channel_failure"
|
|
19260
|
+
}
|
|
19261
|
+
};
|
|
19262
|
+
}
|
|
18831
19263
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
18832
19264
|
tier,
|
|
18833
19265
|
reason,
|
|
18834
19266
|
decided_by: response.decided_by
|
|
18835
19267
|
});
|
|
19268
|
+
if (this.onApprovalEvent) {
|
|
19269
|
+
try {
|
|
19270
|
+
this.onApprovalEvent({
|
|
19271
|
+
phase: "resolved",
|
|
19272
|
+
operation,
|
|
19273
|
+
tier,
|
|
19274
|
+
reason,
|
|
19275
|
+
context,
|
|
19276
|
+
request_timestamp: requestTimestamp,
|
|
19277
|
+
resolution: {
|
|
19278
|
+
decision: response.decision,
|
|
19279
|
+
decided_at: response.decided_at,
|
|
19280
|
+
decided_by: response.decided_by
|
|
19281
|
+
},
|
|
19282
|
+
correlation_id: correlationId
|
|
19283
|
+
});
|
|
19284
|
+
} catch {
|
|
19285
|
+
}
|
|
19286
|
+
}
|
|
18836
19287
|
return {
|
|
18837
19288
|
allowed: response.decision === "approve",
|
|
18838
19289
|
tier,
|
|
@@ -18866,50 +19317,559 @@ var ApprovalGate = class {
|
|
|
18866
19317
|
}
|
|
18867
19318
|
};
|
|
18868
19319
|
|
|
18869
|
-
// src/principal-policy/
|
|
18870
|
-
|
|
18871
|
-
|
|
18872
|
-
|
|
18873
|
-
|
|
18874
|
-
|
|
18875
|
-
|
|
18876
|
-
|
|
18877
|
-
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
|
|
18885
|
-
|
|
18886
|
-
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
19320
|
+
// src/principal-policy/approval-aggregator.ts
|
|
19321
|
+
init_encryption();
|
|
19322
|
+
init_encoding();
|
|
19323
|
+
var APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
19324
|
+
var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
19325
|
+
var APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
19326
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
19327
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
19328
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
19329
|
+
};
|
|
19330
|
+
var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
19331
|
+
var DEFAULT_MAX_LIST_LIMIT = 200;
|
|
19332
|
+
var DEFAULT_LIST_PAGE_SIZE = 50;
|
|
19333
|
+
var ApprovalAggregator = class {
|
|
19334
|
+
storage;
|
|
19335
|
+
encryptionKey;
|
|
19336
|
+
auditLog;
|
|
19337
|
+
identityId;
|
|
19338
|
+
fortressId;
|
|
19339
|
+
pendingTtlMs;
|
|
19340
|
+
maxListLimit;
|
|
19341
|
+
now;
|
|
19342
|
+
resolveSourceContext;
|
|
19343
|
+
resolveHubInboxItemId;
|
|
19344
|
+
/** Cached entries by `aggregator_id`. */
|
|
19345
|
+
entries = /* @__PURE__ */ new Map();
|
|
19346
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
19347
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
19348
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
19349
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
19350
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
19351
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
19352
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
19353
|
+
hydrated = false;
|
|
19354
|
+
/** Active SSE listeners. */
|
|
19355
|
+
listeners = /* @__PURE__ */ new Set();
|
|
19356
|
+
constructor(deps) {
|
|
19357
|
+
this.storage = deps.storage;
|
|
19358
|
+
this.encryptionKey = derivePurposeKey(
|
|
19359
|
+
deps.masterKey,
|
|
19360
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
19361
|
+
);
|
|
19362
|
+
this.auditLog = deps.auditLog;
|
|
19363
|
+
this.identityId = deps.identityId;
|
|
19364
|
+
this.fortressId = deps.fortressId;
|
|
19365
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
19366
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
19367
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
19368
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
19369
|
+
source_harness: this.fortressId,
|
|
19370
|
+
source_agent_id: this.fortressId
|
|
19371
|
+
}));
|
|
19372
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
19373
|
+
}
|
|
19374
|
+
/**
|
|
19375
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
19376
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
19377
|
+
*/
|
|
19378
|
+
onEvent(listener) {
|
|
19379
|
+
this.listeners.add(listener);
|
|
19380
|
+
return () => this.listeners.delete(listener);
|
|
19381
|
+
}
|
|
19382
|
+
/**
|
|
19383
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
19384
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
19385
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
19386
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
19387
|
+
* a record for).
|
|
19388
|
+
*/
|
|
19389
|
+
async ingest(event) {
|
|
19390
|
+
await this.hydrate();
|
|
19391
|
+
if (event.phase === "requested") {
|
|
19392
|
+
return this.ingestRequested(event);
|
|
19393
|
+
}
|
|
19394
|
+
if (event.phase === "resolved") {
|
|
19395
|
+
return this.ingestResolved(event);
|
|
19396
|
+
}
|
|
19397
|
+
return null;
|
|
19398
|
+
}
|
|
19399
|
+
/**
|
|
19400
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
19401
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
19402
|
+
* snapshot is returned.
|
|
19403
|
+
*/
|
|
19404
|
+
async list(opts) {
|
|
19405
|
+
await this.hydrate();
|
|
19406
|
+
await this.expireStale();
|
|
19407
|
+
const limit = Math.min(
|
|
19408
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
19409
|
+
this.maxListLimit
|
|
19410
|
+
);
|
|
19411
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
19412
|
+
const matching = [];
|
|
19413
|
+
for (const entry of this.entries.values()) {
|
|
19414
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
19415
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
19416
|
+
matching.push(entry);
|
|
19417
|
+
}
|
|
19418
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
19419
|
+
return matching.slice(0, limit);
|
|
19420
|
+
}
|
|
19421
|
+
/**
|
|
19422
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
19423
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
19424
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
19425
|
+
*/
|
|
19426
|
+
async getFullPayload(aggregatorId) {
|
|
19427
|
+
await this.hydrate();
|
|
19428
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
19429
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
19430
|
+
}
|
|
19431
|
+
/**
|
|
19432
|
+
* Resolve an entry. Used by both:
|
|
19433
|
+
* 1. The gate wire-up on channel-decision return.
|
|
19434
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
19435
|
+
*
|
|
19436
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
19437
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
19438
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
19439
|
+
* routes return 404.
|
|
19440
|
+
*/
|
|
19441
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
19442
|
+
await this.hydrate();
|
|
19443
|
+
const entry = this.entries.get(aggregatorId);
|
|
19444
|
+
if (!entry) {
|
|
19445
|
+
throw new Error("approval-aggregator: not_found");
|
|
19446
|
+
}
|
|
19447
|
+
if (entry.status !== "pending") {
|
|
19448
|
+
return entry;
|
|
19449
|
+
}
|
|
19450
|
+
entry.status = decision;
|
|
19451
|
+
entry.resolved_at = this.now().toISOString();
|
|
19452
|
+
entry.resolved_by = operatorId;
|
|
19453
|
+
await this.persist(entry);
|
|
19454
|
+
this.auditLog.append(
|
|
19455
|
+
"l2",
|
|
19456
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19457
|
+
this.identityId,
|
|
19458
|
+
{
|
|
19459
|
+
aggregator_id: entry.aggregator_id,
|
|
19460
|
+
source_harness: entry.source_harness,
|
|
19461
|
+
source_agent_id: entry.source_agent_id,
|
|
19462
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19463
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19464
|
+
decision,
|
|
19465
|
+
decided_by: operatorId,
|
|
19466
|
+
decided_at: entry.resolved_at
|
|
19467
|
+
}
|
|
19468
|
+
);
|
|
19469
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19470
|
+
return entry;
|
|
19471
|
+
}
|
|
19472
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
19473
|
+
async ingestRequested(event) {
|
|
19474
|
+
const ctx = this.resolveSourceContext(event);
|
|
19475
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
19476
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
19477
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
19478
|
+
if (existing) {
|
|
19479
|
+
const existingEntry = this.entries.get(existing);
|
|
19480
|
+
if (existingEntry) {
|
|
19481
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
19482
|
+
this.auditLog.append(
|
|
19483
|
+
"l2",
|
|
19484
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
19485
|
+
this.identityId,
|
|
19486
|
+
{
|
|
19487
|
+
aggregator_id: existing,
|
|
19488
|
+
source_harness: ctx.source_harness,
|
|
19489
|
+
source_agent_id: ctx.source_agent_id,
|
|
19490
|
+
audit_log_entry_id: auditId,
|
|
19491
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19492
|
+
correlation_id: event.correlation_id
|
|
19493
|
+
}
|
|
19494
|
+
);
|
|
19495
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
19496
|
+
return null;
|
|
19497
|
+
}
|
|
19498
|
+
}
|
|
19499
|
+
const id = randomUUID();
|
|
19500
|
+
const now = this.now();
|
|
19501
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
19502
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
19503
|
+
const entry = {
|
|
19504
|
+
aggregator_id: id,
|
|
19505
|
+
source_harness: ctx.source_harness,
|
|
19506
|
+
source_agent_id: ctx.source_agent_id,
|
|
19507
|
+
audit_log_entry_id: auditId,
|
|
19508
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
19509
|
+
action_summary: this.deriveActionSummary(event),
|
|
19510
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
19511
|
+
status: "pending",
|
|
19512
|
+
created_at: now.toISOString(),
|
|
19513
|
+
expires_at: expires.toISOString(),
|
|
19514
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19515
|
+
};
|
|
19516
|
+
this.entries.set(id, entry);
|
|
19517
|
+
this.dedupIndex.set(dedupKey, id);
|
|
19518
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
19519
|
+
this.fullPayloads.set(id, event.context);
|
|
19520
|
+
await this.persist(entry);
|
|
19521
|
+
this.auditLog.append(
|
|
19522
|
+
"l2",
|
|
19523
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
19524
|
+
this.identityId,
|
|
19525
|
+
{
|
|
19526
|
+
aggregator_id: id,
|
|
19527
|
+
source_harness: ctx.source_harness,
|
|
19528
|
+
source_agent_id: ctx.source_agent_id,
|
|
19529
|
+
audit_log_entry_id: auditId,
|
|
19530
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19531
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
19532
|
+
}
|
|
19533
|
+
);
|
|
19534
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
19535
|
+
return entry;
|
|
19536
|
+
}
|
|
19537
|
+
async ingestResolved(event) {
|
|
19538
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
19539
|
+
if (!id) return null;
|
|
19540
|
+
const entry = this.entries.get(id);
|
|
19541
|
+
if (!entry) return null;
|
|
19542
|
+
if (entry.status !== "pending") return entry;
|
|
19543
|
+
if (!event.resolution) return entry;
|
|
19544
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
19545
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
19546
|
+
entry.status = status;
|
|
19547
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
19548
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
19549
|
+
await this.persist(entry);
|
|
19550
|
+
this.auditLog.append(
|
|
19551
|
+
"l2",
|
|
19552
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19553
|
+
this.identityId,
|
|
19554
|
+
{
|
|
19555
|
+
aggregator_id: id,
|
|
19556
|
+
source_harness: entry.source_harness,
|
|
19557
|
+
source_agent_id: entry.source_agent_id,
|
|
19558
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19559
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19560
|
+
decision: status,
|
|
19561
|
+
decided_by: entry.resolved_by,
|
|
19562
|
+
decided_at: entry.resolved_at,
|
|
19563
|
+
fail_closed: failClosed
|
|
19564
|
+
}
|
|
19565
|
+
);
|
|
19566
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19567
|
+
return entry;
|
|
19568
|
+
}
|
|
19569
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
19570
|
+
/**
|
|
19571
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
19572
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
19573
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
19574
|
+
* the audit entry the gate appended on the same call.
|
|
19575
|
+
*/
|
|
19576
|
+
auditEntryIdForEvent(event) {
|
|
19577
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
19578
|
+
}
|
|
19579
|
+
derivePolicyRuleId(event) {
|
|
19580
|
+
return `tier${event.tier}:${event.operation}`;
|
|
19581
|
+
}
|
|
19582
|
+
deriveActionSummary(event) {
|
|
19583
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
19584
|
+
}
|
|
19585
|
+
/**
|
|
19586
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
19587
|
+
* identical payloads always hash the same, even when key insertion order
|
|
19588
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
19589
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
19590
|
+
*/
|
|
19591
|
+
hashPayload(payload) {
|
|
19592
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
19593
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
19594
|
+
}
|
|
19595
|
+
emit(event) {
|
|
19596
|
+
for (const listener of this.listeners) {
|
|
19597
|
+
try {
|
|
19598
|
+
listener(event);
|
|
19599
|
+
} catch {
|
|
19600
|
+
}
|
|
19601
|
+
}
|
|
19602
|
+
}
|
|
19603
|
+
async expireStale() {
|
|
19604
|
+
const nowMs = this.now().getTime();
|
|
19605
|
+
for (const entry of this.entries.values()) {
|
|
19606
|
+
if (entry.status !== "pending") continue;
|
|
19607
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
19608
|
+
entry.status = "expired";
|
|
19609
|
+
entry.resolved_at = this.now().toISOString();
|
|
19610
|
+
entry.resolved_by = "system_ttl";
|
|
19611
|
+
await this.persist(entry);
|
|
19612
|
+
this.auditLog.append(
|
|
19613
|
+
"l2",
|
|
19614
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
19615
|
+
this.identityId,
|
|
19616
|
+
{
|
|
19617
|
+
aggregator_id: entry.aggregator_id,
|
|
19618
|
+
source_harness: entry.source_harness,
|
|
19619
|
+
source_agent_id: entry.source_agent_id,
|
|
19620
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
19621
|
+
policy_rule_id: entry.policy_rule_id,
|
|
19622
|
+
decision: "expired",
|
|
19623
|
+
decided_by: "system_ttl",
|
|
19624
|
+
decided_at: entry.resolved_at
|
|
19625
|
+
}
|
|
19626
|
+
);
|
|
19627
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
19628
|
+
}
|
|
19629
|
+
}
|
|
19630
|
+
async persist(entry) {
|
|
19631
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
19632
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
19633
|
+
await this.storage.write(
|
|
19634
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19635
|
+
entry.aggregator_id,
|
|
19636
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
19637
|
+
);
|
|
19638
|
+
}
|
|
19639
|
+
async hydrate() {
|
|
19640
|
+
if (this.hydrated) return;
|
|
19641
|
+
this.hydrated = true;
|
|
19642
|
+
try {
|
|
19643
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
19644
|
+
for (const meta of metas) {
|
|
19645
|
+
const raw = await this.storage.read(
|
|
19646
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
19647
|
+
meta.key
|
|
19648
|
+
);
|
|
19649
|
+
if (!raw) continue;
|
|
19650
|
+
try {
|
|
19651
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
19652
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
19653
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
19654
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
19655
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
19656
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
19657
|
+
} catch {
|
|
19658
|
+
}
|
|
19659
|
+
}
|
|
19660
|
+
} catch {
|
|
19661
|
+
this.hydrated = false;
|
|
19662
|
+
}
|
|
19663
|
+
}
|
|
19664
|
+
};
|
|
19665
|
+
|
|
19666
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
19667
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
19668
|
+
function auditEntryIdFor(request) {
|
|
19669
|
+
return `${request.timestamp}:${request.operation}`;
|
|
19670
|
+
}
|
|
19671
|
+
function statusToDecision(entry) {
|
|
19672
|
+
switch (entry.status) {
|
|
19673
|
+
case "approved":
|
|
19674
|
+
return {
|
|
19675
|
+
decision: "approve",
|
|
19676
|
+
decided_by: "human"
|
|
19677
|
+
};
|
|
19678
|
+
case "denied":
|
|
19679
|
+
return {
|
|
19680
|
+
decision: "deny",
|
|
19681
|
+
decided_by: "human"
|
|
19682
|
+
};
|
|
19683
|
+
case "timeout":
|
|
19684
|
+
case "expired":
|
|
19685
|
+
return {
|
|
19686
|
+
decision: "deny",
|
|
19687
|
+
decided_by: "timeout"
|
|
19688
|
+
};
|
|
19689
|
+
default:
|
|
19690
|
+
return null;
|
|
19691
|
+
}
|
|
19692
|
+
}
|
|
19693
|
+
var AggregatorBackedChannel = class {
|
|
19694
|
+
underlying;
|
|
19695
|
+
aggregator;
|
|
19696
|
+
resolveRedirect;
|
|
19697
|
+
replaceModeTimeoutMs;
|
|
19698
|
+
now;
|
|
19699
|
+
constructor(opts) {
|
|
19700
|
+
this.underlying = opts.underlying;
|
|
19701
|
+
this.aggregator = opts.aggregator;
|
|
19702
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
19703
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
19704
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
19705
|
+
}
|
|
19706
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
19707
|
+
getUnderlying() {
|
|
19708
|
+
return this.underlying;
|
|
19709
|
+
}
|
|
19710
|
+
async requestApproval(request) {
|
|
19711
|
+
const cfg = this.resolveRedirect(request);
|
|
19712
|
+
if (!cfg.enabled) {
|
|
19713
|
+
return this.underlying.requestApproval(request);
|
|
19714
|
+
}
|
|
19715
|
+
if (cfg.mode === "replace") {
|
|
19716
|
+
return this.awaitAggregatorDecision(request);
|
|
19717
|
+
}
|
|
19718
|
+
return this.notifyMode(request);
|
|
19719
|
+
}
|
|
19720
|
+
/**
|
|
19721
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
19722
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
19723
|
+
* between list and subscribe). Match incoming events to this request by
|
|
19724
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
19725
|
+
*/
|
|
19726
|
+
async awaitAggregatorDecision(request) {
|
|
19727
|
+
const auditId = auditEntryIdFor(request);
|
|
19728
|
+
return new Promise((resolveOuter) => {
|
|
19729
|
+
let settled = false;
|
|
19730
|
+
let unsubscribe = null;
|
|
19731
|
+
let timeoutHandle = null;
|
|
19732
|
+
const settle = (response) => {
|
|
19733
|
+
if (settled) return;
|
|
19734
|
+
settled = true;
|
|
19735
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
19736
|
+
if (unsubscribe) {
|
|
19737
|
+
try {
|
|
19738
|
+
unsubscribe();
|
|
19739
|
+
} catch {
|
|
19740
|
+
}
|
|
19741
|
+
}
|
|
19742
|
+
resolveOuter(response);
|
|
19743
|
+
};
|
|
19744
|
+
const onEvent = (emit) => {
|
|
19745
|
+
if (emit.type !== "resolved") return;
|
|
19746
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
19747
|
+
const mapped = statusToDecision(emit.entry);
|
|
19748
|
+
if (!mapped) return;
|
|
19749
|
+
settle({
|
|
19750
|
+
decision: mapped.decision,
|
|
19751
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
19752
|
+
decided_by: mapped.decided_by
|
|
19753
|
+
});
|
|
19754
|
+
};
|
|
19755
|
+
try {
|
|
19756
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
19757
|
+
} catch (err) {
|
|
19758
|
+
settle({
|
|
19759
|
+
decision: "deny",
|
|
19760
|
+
decided_at: this.now().toISOString(),
|
|
19761
|
+
decided_by: "channel_failure"
|
|
19762
|
+
});
|
|
19763
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
19764
|
+
}
|
|
19765
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
19766
|
+
for (const entry of entries) {
|
|
19767
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
19768
|
+
const mapped = statusToDecision(entry);
|
|
19769
|
+
if (!mapped) return;
|
|
19770
|
+
settle({
|
|
19771
|
+
decision: mapped.decision,
|
|
19772
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
19773
|
+
decided_by: mapped.decided_by
|
|
19774
|
+
});
|
|
19775
|
+
return;
|
|
19776
|
+
}
|
|
19777
|
+
}).catch(() => {
|
|
19778
|
+
});
|
|
19779
|
+
timeoutHandle = setTimeout(() => {
|
|
19780
|
+
settle({
|
|
19781
|
+
decision: "deny",
|
|
19782
|
+
decided_at: this.now().toISOString(),
|
|
19783
|
+
decided_by: "timeout"
|
|
19784
|
+
});
|
|
19785
|
+
}, this.replaceModeTimeoutMs);
|
|
19786
|
+
});
|
|
19787
|
+
}
|
|
19788
|
+
/**
|
|
19789
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
19790
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
19791
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
19792
|
+
* downstream audit logging is unchanged.
|
|
19793
|
+
*
|
|
19794
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
19795
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
19796
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
19797
|
+
*/
|
|
19798
|
+
async notifyMode(request) {
|
|
19799
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
19800
|
+
let underlyingPromise;
|
|
19801
|
+
try {
|
|
19802
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
19803
|
+
} catch (err) {
|
|
19804
|
+
const response = await aggregatorPromise;
|
|
19805
|
+
return response;
|
|
19806
|
+
}
|
|
19807
|
+
return Promise.race([
|
|
19808
|
+
aggregatorPromise,
|
|
19809
|
+
underlyingPromise.catch(
|
|
19810
|
+
() => new Promise(() => {
|
|
19811
|
+
})
|
|
19812
|
+
)
|
|
19813
|
+
]);
|
|
19814
|
+
}
|
|
19815
|
+
};
|
|
19816
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
19817
|
+
return (_request) => {
|
|
19818
|
+
const cfg = supplier().approval_redirect;
|
|
19819
|
+
if (!cfg || cfg.enabled !== true) {
|
|
19820
|
+
return { enabled: false, mode: "replace" };
|
|
19821
|
+
}
|
|
19822
|
+
return {
|
|
19823
|
+
enabled: true,
|
|
19824
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
19825
|
+
};
|
|
19826
|
+
};
|
|
19827
|
+
}
|
|
19828
|
+
|
|
19829
|
+
// src/principal-policy/tools.ts
|
|
19830
|
+
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
19831
|
+
return [
|
|
19832
|
+
{
|
|
19833
|
+
name: "principal_policy_view",
|
|
19834
|
+
description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
|
|
19835
|
+
inputSchema: {
|
|
19836
|
+
type: "object",
|
|
19837
|
+
properties: {
|
|
19838
|
+
include_defaults: {
|
|
19839
|
+
type: "boolean",
|
|
19840
|
+
description: "Include tier3_always_allow list (can be long)",
|
|
19841
|
+
default: false
|
|
19842
|
+
}
|
|
19843
|
+
}
|
|
19844
|
+
},
|
|
19845
|
+
handler: async (args) => {
|
|
19846
|
+
const includeDefaults = args.include_defaults ?? false;
|
|
19847
|
+
const view = {
|
|
19848
|
+
version: policy.version,
|
|
19849
|
+
tier1_always_approve: policy.tier1_always_approve,
|
|
19850
|
+
tier2_anomaly: policy.tier2_anomaly,
|
|
19851
|
+
approval_channel: {
|
|
19852
|
+
type: policy.approval_channel.type,
|
|
19853
|
+
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
19854
|
+
auto_deny: true
|
|
19855
|
+
// SEC-002: hardcoded, not configurable
|
|
19856
|
+
}
|
|
19857
|
+
};
|
|
19858
|
+
if (includeDefaults) {
|
|
19859
|
+
view.tier3_always_allow = policy.tier3_always_allow;
|
|
19860
|
+
} else {
|
|
19861
|
+
view.tier3_always_allow_count = policy.tier3_always_allow.length;
|
|
19862
|
+
view.note = "Pass include_defaults: true to see the full tier3_always_allow list";
|
|
19863
|
+
}
|
|
19864
|
+
auditLog.append("l2", "principal_policy_view", "system", {
|
|
19865
|
+
include_defaults: includeDefaults
|
|
19866
|
+
});
|
|
19867
|
+
return toolResult(view);
|
|
19868
|
+
}
|
|
19869
|
+
},
|
|
19870
|
+
{
|
|
19871
|
+
name: "principal_baseline_view",
|
|
19872
|
+
description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
|
|
18913
19873
|
inputSchema: {
|
|
18914
19874
|
type: "object",
|
|
18915
19875
|
properties: {}
|
|
@@ -19736,6 +20696,71 @@ function verifyAttestation(attestation, now) {
|
|
|
19736
20696
|
};
|
|
19737
20697
|
}
|
|
19738
20698
|
|
|
20699
|
+
// src/handshake/audit.ts
|
|
20700
|
+
var HANDSHAKE_LIFECYCLE_OPS = {
|
|
20701
|
+
INITIATED: "handshake_initiated",
|
|
20702
|
+
COMPLETED: "handshake_completed",
|
|
20703
|
+
FAILED: "handshake_failed",
|
|
20704
|
+
ABORTED: "handshake_aborted"
|
|
20705
|
+
};
|
|
20706
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
20707
|
+
auditLog.append(
|
|
20708
|
+
"l4",
|
|
20709
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
20710
|
+
ctx.identity_id,
|
|
20711
|
+
detailsFromContext(ctx),
|
|
20712
|
+
"success"
|
|
20713
|
+
);
|
|
20714
|
+
}
|
|
20715
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
20716
|
+
const details = detailsFromContext(ctx);
|
|
20717
|
+
if (ctx.trust_tier !== void 0) {
|
|
20718
|
+
details.trust_tier = ctx.trust_tier;
|
|
20719
|
+
}
|
|
20720
|
+
auditLog.append(
|
|
20721
|
+
"l4",
|
|
20722
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
20723
|
+
ctx.identity_id,
|
|
20724
|
+
details,
|
|
20725
|
+
"success"
|
|
20726
|
+
);
|
|
20727
|
+
}
|
|
20728
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
20729
|
+
const details = detailsFromContext(ctx);
|
|
20730
|
+
details.reason = ctx.reason;
|
|
20731
|
+
if (ctx.error !== void 0) {
|
|
20732
|
+
details.error = ctx.error;
|
|
20733
|
+
}
|
|
20734
|
+
auditLog.append(
|
|
20735
|
+
"l4",
|
|
20736
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
20737
|
+
ctx.identity_id,
|
|
20738
|
+
details,
|
|
20739
|
+
"failure"
|
|
20740
|
+
);
|
|
20741
|
+
}
|
|
20742
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
20743
|
+
const details = detailsFromContext(ctx);
|
|
20744
|
+
details.reason = ctx.reason;
|
|
20745
|
+
auditLog.append(
|
|
20746
|
+
"l4",
|
|
20747
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
20748
|
+
ctx.identity_id,
|
|
20749
|
+
details,
|
|
20750
|
+
"failure"
|
|
20751
|
+
);
|
|
20752
|
+
}
|
|
20753
|
+
function detailsFromContext(ctx) {
|
|
20754
|
+
const details = {
|
|
20755
|
+
session_id: ctx.session_id,
|
|
20756
|
+
role: ctx.role
|
|
20757
|
+
};
|
|
20758
|
+
if (ctx.counterparty_id !== void 0) {
|
|
20759
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
20760
|
+
}
|
|
20761
|
+
return details;
|
|
20762
|
+
}
|
|
20763
|
+
|
|
19739
20764
|
// src/handshake/tools.ts
|
|
19740
20765
|
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
19741
20766
|
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
@@ -19769,6 +20794,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19769
20794
|
const { challenge, session } = initiateHandshake(shr);
|
|
19770
20795
|
sessions.set(session.session_id, session);
|
|
19771
20796
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
20797
|
+
auditHandshakeInitiated(auditLog, {
|
|
20798
|
+
session_id: session.session_id,
|
|
20799
|
+
role: "initiator",
|
|
20800
|
+
identity_id: shr.body.instance_id
|
|
20801
|
+
});
|
|
19772
20802
|
return toolResult({
|
|
19773
20803
|
session_id: session.session_id,
|
|
19774
20804
|
challenge,
|
|
@@ -19808,10 +20838,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19808
20838
|
);
|
|
19809
20839
|
if ("error" in result) {
|
|
19810
20840
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
20841
|
+
auditHandshakeFailed(auditLog, {
|
|
20842
|
+
session_id: "unknown",
|
|
20843
|
+
role: "responder",
|
|
20844
|
+
identity_id: shr.body.instance_id,
|
|
20845
|
+
reason: classifyRespondFailure(result.error),
|
|
20846
|
+
error: result.error
|
|
20847
|
+
});
|
|
19811
20848
|
return toolResult({ error: result.error });
|
|
19812
20849
|
}
|
|
19813
20850
|
sessions.set(result.session.session_id, result.session);
|
|
19814
20851
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
20852
|
+
auditHandshakeInitiated(auditLog, {
|
|
20853
|
+
session_id: result.session.session_id,
|
|
20854
|
+
role: "responder",
|
|
20855
|
+
identity_id: shr.body.instance_id,
|
|
20856
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
20857
|
+
});
|
|
19815
20858
|
let autoPublishResult;
|
|
19816
20859
|
if (autoPublishHandshakes) {
|
|
19817
20860
|
autoPublishResult = { attempted: true };
|
|
@@ -19919,9 +20962,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19919
20962
|
const response = args.response;
|
|
19920
20963
|
const session = sessions.get(sessionId);
|
|
19921
20964
|
if (!session) {
|
|
20965
|
+
auditHandshakeFailed(auditLog, {
|
|
20966
|
+
session_id: sessionId,
|
|
20967
|
+
role: "initiator",
|
|
20968
|
+
identity_id: "unknown",
|
|
20969
|
+
reason: "session_unknown",
|
|
20970
|
+
error: `No handshake session found: ${sessionId}`
|
|
20971
|
+
});
|
|
19922
20972
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
19923
20973
|
}
|
|
19924
20974
|
if (session.state !== "initiated") {
|
|
20975
|
+
auditHandshakeFailed(auditLog, {
|
|
20976
|
+
session_id: sessionId,
|
|
20977
|
+
role: "initiator",
|
|
20978
|
+
identity_id: session.our_shr.body.instance_id,
|
|
20979
|
+
reason: "session_state_mismatch",
|
|
20980
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
20981
|
+
});
|
|
19925
20982
|
return toolResult({
|
|
19926
20983
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
19927
20984
|
});
|
|
@@ -19935,6 +20992,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19935
20992
|
if ("error" in result) {
|
|
19936
20993
|
session.state = "failed";
|
|
19937
20994
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
20995
|
+
auditHandshakeFailed(auditLog, {
|
|
20996
|
+
session_id: sessionId,
|
|
20997
|
+
role: "initiator",
|
|
20998
|
+
identity_id: session.our_shr.body.instance_id,
|
|
20999
|
+
reason: classifyCompleteFailure(result.error),
|
|
21000
|
+
error: result.error
|
|
21001
|
+
});
|
|
19938
21002
|
return toolResult({ error: result.error });
|
|
19939
21003
|
}
|
|
19940
21004
|
session.state = "completed";
|
|
@@ -19943,6 +21007,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19943
21007
|
session.result = result.result;
|
|
19944
21008
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
19945
21009
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
21010
|
+
auditHandshakeCompleted(auditLog, {
|
|
21011
|
+
session_id: sessionId,
|
|
21012
|
+
role: "initiator",
|
|
21013
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21014
|
+
counterparty_id: result.result.counterparty_id,
|
|
21015
|
+
trust_tier: result.result.trust_tier
|
|
21016
|
+
});
|
|
19946
21017
|
return toolResult({
|
|
19947
21018
|
completion: result.completion,
|
|
19948
21019
|
result: result.result,
|
|
@@ -19990,6 +21061,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
19990
21061
|
void 0,
|
|
19991
21062
|
result.verified ? "success" : "failure"
|
|
19992
21063
|
);
|
|
21064
|
+
if (result.verified) {
|
|
21065
|
+
auditHandshakeCompleted(auditLog, {
|
|
21066
|
+
session_id: session.session_id,
|
|
21067
|
+
role: "responder",
|
|
21068
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21069
|
+
counterparty_id: result.counterparty_id,
|
|
21070
|
+
trust_tier: result.trust_tier
|
|
21071
|
+
});
|
|
21072
|
+
} else {
|
|
21073
|
+
auditHandshakeFailed(auditLog, {
|
|
21074
|
+
session_id: session.session_id,
|
|
21075
|
+
role: "responder",
|
|
21076
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21077
|
+
counterparty_id: result.counterparty_id,
|
|
21078
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
21079
|
+
error: result.errors.join("; ")
|
|
21080
|
+
});
|
|
21081
|
+
}
|
|
19993
21082
|
return toolResult({ result });
|
|
19994
21083
|
}
|
|
19995
21084
|
return toolResult({
|
|
@@ -20097,10 +21186,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
20097
21186
|
_content_trust: "external"
|
|
20098
21187
|
});
|
|
20099
21188
|
}
|
|
21189
|
+
},
|
|
21190
|
+
{
|
|
21191
|
+
name: "handshake_abort",
|
|
21192
|
+
description: "Abort an in-flight handshake session. Drops the session record and appends a session-lifecycle audit entry (handshake_aborted) so the operator can distinguish operator-cancelled, timed-out, and dropped sessions from sessions that simply fell off the protocol path.",
|
|
21193
|
+
inputSchema: {
|
|
21194
|
+
type: "object",
|
|
21195
|
+
properties: {
|
|
21196
|
+
session_id: {
|
|
21197
|
+
type: "string",
|
|
21198
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
21199
|
+
},
|
|
21200
|
+
reason: {
|
|
21201
|
+
type: "string",
|
|
21202
|
+
enum: [
|
|
21203
|
+
"operator_cancelled",
|
|
21204
|
+
"session_timeout",
|
|
21205
|
+
"transport_dropped",
|
|
21206
|
+
"shutdown",
|
|
21207
|
+
"other"
|
|
21208
|
+
],
|
|
21209
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
21210
|
+
}
|
|
21211
|
+
},
|
|
21212
|
+
required: ["session_id"]
|
|
21213
|
+
},
|
|
21214
|
+
handler: async (args) => {
|
|
21215
|
+
const sessionId = args.session_id;
|
|
21216
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
21217
|
+
const session = sessions.get(sessionId);
|
|
21218
|
+
if (!session) {
|
|
21219
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21220
|
+
}
|
|
21221
|
+
if (session.state === "completed") {
|
|
21222
|
+
return toolResult({
|
|
21223
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
21224
|
+
});
|
|
21225
|
+
}
|
|
21226
|
+
sessions.delete(sessionId);
|
|
21227
|
+
auditHandshakeAborted(auditLog, {
|
|
21228
|
+
session_id: sessionId,
|
|
21229
|
+
role: session.role,
|
|
21230
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21231
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
21232
|
+
reason
|
|
21233
|
+
});
|
|
21234
|
+
return toolResult({
|
|
21235
|
+
aborted: true,
|
|
21236
|
+
session_id: sessionId,
|
|
21237
|
+
reason
|
|
21238
|
+
});
|
|
21239
|
+
}
|
|
20100
21240
|
}
|
|
20101
21241
|
];
|
|
20102
21242
|
return { tools, handshakeResults };
|
|
20103
21243
|
}
|
|
21244
|
+
function classifyRespondFailure(error) {
|
|
21245
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21246
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
21247
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21248
|
+
return "other";
|
|
21249
|
+
}
|
|
21250
|
+
function classifyCompleteFailure(error) {
|
|
21251
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
21252
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
21253
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
21254
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
21255
|
+
return "other";
|
|
21256
|
+
}
|
|
20104
21257
|
|
|
20105
21258
|
// src/federation/registry.ts
|
|
20106
21259
|
var DEFAULT_CAPABILITIES = {
|
|
@@ -21718,6 +22871,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
21718
22871
|
}
|
|
21719
22872
|
async function consumeResetHistoryMarker(options) {
|
|
21720
22873
|
const markerPath = join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
22874
|
+
const consumedPath = markerPath + ".consumed";
|
|
22875
|
+
if (await fileExists3(consumedPath)) {
|
|
22876
|
+
await rm(markerPath, { force: true });
|
|
22877
|
+
await rm(consumedPath, { force: true });
|
|
22878
|
+
return { emitted: 0, markerPath };
|
|
22879
|
+
}
|
|
21721
22880
|
if (!await fileExists3(markerPath)) {
|
|
21722
22881
|
return { emitted: 0, markerPath };
|
|
21723
22882
|
}
|
|
@@ -21742,7 +22901,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
21742
22901
|
});
|
|
21743
22902
|
}
|
|
21744
22903
|
await options.auditLog.flush();
|
|
22904
|
+
await writeFile(consumedPath, "", "utf-8");
|
|
21745
22905
|
await rm(markerPath, { force: true });
|
|
22906
|
+
await rm(consumedPath, { force: true });
|
|
21746
22907
|
return { emitted: markers.length, markerHash, markerPath };
|
|
21747
22908
|
}
|
|
21748
22909
|
async function fileExists3(path) {
|
|
@@ -30686,7 +31847,37 @@ var HubService = class {
|
|
|
30686
31847
|
*/
|
|
30687
31848
|
async getConciergeHistory() {
|
|
30688
31849
|
const chat = this.requireOperatorChat();
|
|
30689
|
-
return chat.getConciergeHistory();
|
|
31850
|
+
return chat.getConciergeHistory();
|
|
31851
|
+
}
|
|
31852
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
31853
|
+
/**
|
|
31854
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
31855
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
31856
|
+
* surface is unavailable on a given fortress.
|
|
31857
|
+
*/
|
|
31858
|
+
hasConciergeMemory() {
|
|
31859
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
31860
|
+
}
|
|
31861
|
+
async listConciergeMemoryThreads(opts) {
|
|
31862
|
+
const chat = this.requireOperatorChat();
|
|
31863
|
+
if (!chat.hasConciergeMemory()) {
|
|
31864
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31865
|
+
}
|
|
31866
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
31867
|
+
}
|
|
31868
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
31869
|
+
const chat = this.requireOperatorChat();
|
|
31870
|
+
if (!chat.hasConciergeMemory()) {
|
|
31871
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31872
|
+
}
|
|
31873
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
31874
|
+
}
|
|
31875
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
31876
|
+
const chat = this.requireOperatorChat();
|
|
31877
|
+
if (!chat.hasConciergeMemory()) {
|
|
31878
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
31879
|
+
}
|
|
31880
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
30690
31881
|
}
|
|
30691
31882
|
/**
|
|
30692
31883
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
@@ -30775,7 +31966,28 @@ init_encoding();
|
|
|
30775
31966
|
|
|
30776
31967
|
// src/chat/operator-chat-audit-events.ts
|
|
30777
31968
|
var OPERATOR_CHAT_OPS = {
|
|
30778
|
-
CONCIERGE_CHAT: "operator_concierge_chat"
|
|
31969
|
+
CONCIERGE_CHAT: "operator_concierge_chat",
|
|
31970
|
+
/**
|
|
31971
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
31972
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
31973
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
31974
|
+
* raw turn content never crosses the audit surface.
|
|
31975
|
+
*/
|
|
31976
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
31977
|
+
/**
|
|
31978
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
31979
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
31980
|
+
* the deleted bundle.
|
|
31981
|
+
*/
|
|
31982
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
31983
|
+
/**
|
|
31984
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
31985
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
31986
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
31987
|
+
* carries thread_id + a stable failure_reason enum.
|
|
31988
|
+
*/
|
|
31989
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
|
|
31990
|
+
};
|
|
30779
31991
|
|
|
30780
31992
|
// src/chat/operator-chat-types.ts
|
|
30781
31993
|
var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
|
|
@@ -30783,6 +31995,13 @@ var CONCIERGE_THREAD_KEY = "_fortress";
|
|
|
30783
31995
|
|
|
30784
31996
|
// src/chat/operator-chat-service.ts
|
|
30785
31997
|
var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
31998
|
+
var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
31999
|
+
var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
32000
|
+
var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
32001
|
+
var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32002
|
+
function approxTokenLen(text) {
|
|
32003
|
+
return Math.ceil(text.length / 4);
|
|
32004
|
+
}
|
|
30786
32005
|
var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
30787
32006
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
30788
32007
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -30818,6 +32037,27 @@ var OperatorChatService = class {
|
|
|
30818
32037
|
contextProviders;
|
|
30819
32038
|
piiFilter;
|
|
30820
32039
|
conciergeMaxTokens;
|
|
32040
|
+
memory;
|
|
32041
|
+
historyWindowTurns;
|
|
32042
|
+
historyFreshnessMs;
|
|
32043
|
+
historyTokenBudget;
|
|
32044
|
+
sessionTtlMs;
|
|
32045
|
+
clock;
|
|
32046
|
+
/**
|
|
32047
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
32048
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
32049
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
32050
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
32051
|
+
*/
|
|
32052
|
+
activeMemoryThreadId;
|
|
32053
|
+
/**
|
|
32054
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
32055
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
32056
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
32057
|
+
* allocates a new thread_id even though the prior one is still
|
|
32058
|
+
* readable from the memory store.
|
|
32059
|
+
*/
|
|
32060
|
+
lastInteractionAt;
|
|
30821
32061
|
constructor(deps) {
|
|
30822
32062
|
this.store = deps.store;
|
|
30823
32063
|
this.auditLog = deps.auditLog;
|
|
@@ -30828,6 +32068,12 @@ var OperatorChatService = class {
|
|
|
30828
32068
|
}
|
|
30829
32069
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
30830
32070
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
32071
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32072
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
32073
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
32074
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
32075
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
32076
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
30831
32077
|
}
|
|
30832
32078
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
30833
32079
|
/**
|
|
@@ -30846,6 +32092,10 @@ var OperatorChatService = class {
|
|
|
30846
32092
|
throw new Error("concierge query must not be empty");
|
|
30847
32093
|
}
|
|
30848
32094
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
32095
|
+
const nowMs = this.clock();
|
|
32096
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
32097
|
+
this.activeMemoryThreadId = void 0;
|
|
32098
|
+
}
|
|
30849
32099
|
const operatorMessage = {
|
|
30850
32100
|
message_id: randomUUID(),
|
|
30851
32101
|
surface: "concierge",
|
|
@@ -30858,6 +32108,30 @@ var OperatorChatService = class {
|
|
|
30858
32108
|
CONCIERGE_THREAD_KEY,
|
|
30859
32109
|
operatorMessage
|
|
30860
32110
|
);
|
|
32111
|
+
let priorTurns = [];
|
|
32112
|
+
let memoryReadFailureReason = null;
|
|
32113
|
+
let activeThreadIdForRound;
|
|
32114
|
+
if (this.memory) {
|
|
32115
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
32116
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
32117
|
+
if (result.ok) {
|
|
32118
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
32119
|
+
const fresh = result.turns.filter((t) => {
|
|
32120
|
+
const ts = Date.parse(t.created_at);
|
|
32121
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
32122
|
+
});
|
|
32123
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
32124
|
+
priorTurns = recent;
|
|
32125
|
+
} else {
|
|
32126
|
+
memoryReadFailureReason = result.reason;
|
|
32127
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
32128
|
+
}
|
|
32129
|
+
}
|
|
32130
|
+
if (this.memory) {
|
|
32131
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
32132
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
32133
|
+
});
|
|
32134
|
+
}
|
|
30861
32135
|
const start = Date.now();
|
|
30862
32136
|
let conciergeBody;
|
|
30863
32137
|
let servedBy = "disabled";
|
|
@@ -30874,7 +32148,7 @@ var OperatorChatService = class {
|
|
|
30874
32148
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
30875
32149
|
outcome = "substrate_disabled";
|
|
30876
32150
|
} else {
|
|
30877
|
-
const context = await this.assembleConciergeContext();
|
|
32151
|
+
const context = await this.assembleConciergeContext(priorTurns);
|
|
30878
32152
|
const response = await this.substrateSelector.invokeSummarize(
|
|
30879
32153
|
"concierge",
|
|
30880
32154
|
{
|
|
@@ -30913,6 +32187,15 @@ var OperatorChatService = class {
|
|
|
30913
32187
|
CONCIERGE_THREAD_KEY,
|
|
30914
32188
|
responseMessage
|
|
30915
32189
|
);
|
|
32190
|
+
let assistantTurnId;
|
|
32191
|
+
if (this.memory) {
|
|
32192
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
32193
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
32194
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
32195
|
+
}
|
|
32196
|
+
if (this.memory && activeThreadIdForRound) {
|
|
32197
|
+
this.lastInteractionAt = nowMs;
|
|
32198
|
+
}
|
|
30916
32199
|
const payload = {
|
|
30917
32200
|
version: "1.2",
|
|
30918
32201
|
event_id: makeEventId("conc"),
|
|
@@ -30924,7 +32207,12 @@ var OperatorChatService = class {
|
|
|
30924
32207
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
30925
32208
|
substrate: servedBy,
|
|
30926
32209
|
latency_ms: latencyMs,
|
|
30927
|
-
outcome
|
|
32210
|
+
outcome,
|
|
32211
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
32212
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
32213
|
+
...this.memory ? {
|
|
32214
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
32215
|
+
} : {}
|
|
30928
32216
|
};
|
|
30929
32217
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
30930
32218
|
return {
|
|
@@ -30934,6 +32222,25 @@ var OperatorChatService = class {
|
|
|
30934
32222
|
outcome
|
|
30935
32223
|
};
|
|
30936
32224
|
}
|
|
32225
|
+
/**
|
|
32226
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
32227
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
32228
|
+
* with `result: "failure"` since the concierge fell back to
|
|
32229
|
+
* single-turn mode for this round-trip.
|
|
32230
|
+
*/
|
|
32231
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
32232
|
+
const payload = {
|
|
32233
|
+
version: "1.2",
|
|
32234
|
+
event_id: makeEventId("conc-memfail"),
|
|
32235
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32236
|
+
identity_id: this.identityId,
|
|
32237
|
+
kind: "operator_concierge_memory_read_failed",
|
|
32238
|
+
surface: "concierge",
|
|
32239
|
+
thread_id: threadId,
|
|
32240
|
+
failure_reason: reason
|
|
32241
|
+
};
|
|
32242
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
32243
|
+
}
|
|
30937
32244
|
/**
|
|
30938
32245
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
30939
32246
|
* an empty array when no thread exists yet.
|
|
@@ -30945,6 +32252,105 @@ var OperatorChatService = class {
|
|
|
30945
32252
|
);
|
|
30946
32253
|
return thread ? thread.messages : [];
|
|
30947
32254
|
}
|
|
32255
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
32256
|
+
/**
|
|
32257
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
32258
|
+
* 503 cleanly when called against an unwired service.
|
|
32259
|
+
*/
|
|
32260
|
+
hasConciergeMemory() {
|
|
32261
|
+
return this.memory !== void 0;
|
|
32262
|
+
}
|
|
32263
|
+
/**
|
|
32264
|
+
* List concierge memory threads, newest-first. Emits the
|
|
32265
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
32266
|
+
*/
|
|
32267
|
+
async listConciergeMemoryThreads(opts) {
|
|
32268
|
+
if (!this.memory) {
|
|
32269
|
+
throw new Error("concierge memory store not configured");
|
|
32270
|
+
}
|
|
32271
|
+
const summaries = await this.memory.listThreads(opts);
|
|
32272
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
32273
|
+
const payload = {
|
|
32274
|
+
version: "1.2",
|
|
32275
|
+
event_id: makeEventId("conc-hist"),
|
|
32276
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32277
|
+
identity_id: this.identityId,
|
|
32278
|
+
kind: "operator_concierge_history_read",
|
|
32279
|
+
surface: "concierge",
|
|
32280
|
+
thread_id: "*",
|
|
32281
|
+
turn_count: totalTurns
|
|
32282
|
+
};
|
|
32283
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
32284
|
+
return summaries;
|
|
32285
|
+
}
|
|
32286
|
+
/**
|
|
32287
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
32288
|
+
* `operator_concierge_history_read` audit event with the named
|
|
32289
|
+
* thread_id and the count of turns surfaced.
|
|
32290
|
+
*/
|
|
32291
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
32292
|
+
if (!this.memory) {
|
|
32293
|
+
throw new Error("concierge memory store not configured");
|
|
32294
|
+
}
|
|
32295
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
32296
|
+
const payload = {
|
|
32297
|
+
version: "1.2",
|
|
32298
|
+
event_id: makeEventId("conc-hist"),
|
|
32299
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32300
|
+
identity_id: this.identityId,
|
|
32301
|
+
kind: "operator_concierge_history_read",
|
|
32302
|
+
surface: "concierge",
|
|
32303
|
+
thread_id: threadId,
|
|
32304
|
+
turn_count: turns.length
|
|
32305
|
+
};
|
|
32306
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
32307
|
+
return turns;
|
|
32308
|
+
}
|
|
32309
|
+
/**
|
|
32310
|
+
* Delete a concierge memory thread. Emits
|
|
32311
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
32312
|
+
* removed; absent threads return false without an audit event.
|
|
32313
|
+
*/
|
|
32314
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
32315
|
+
if (!this.memory) {
|
|
32316
|
+
throw new Error("concierge memory store not configured");
|
|
32317
|
+
}
|
|
32318
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
32319
|
+
if (turnsBefore.length === 0) {
|
|
32320
|
+
return await this.memory.deleteThread(threadId);
|
|
32321
|
+
}
|
|
32322
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
32323
|
+
if (!removed) return false;
|
|
32324
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
32325
|
+
this.activeMemoryThreadId = void 0;
|
|
32326
|
+
}
|
|
32327
|
+
const payload = {
|
|
32328
|
+
version: "1.2",
|
|
32329
|
+
event_id: makeEventId("conc-del"),
|
|
32330
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32331
|
+
identity_id: this.identityId,
|
|
32332
|
+
kind: "operator_concierge_thread_deleted",
|
|
32333
|
+
surface: "concierge",
|
|
32334
|
+
thread_id: threadId,
|
|
32335
|
+
turn_count: turnsBefore.length
|
|
32336
|
+
};
|
|
32337
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
32338
|
+
return true;
|
|
32339
|
+
}
|
|
32340
|
+
/**
|
|
32341
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
32342
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
32343
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
32344
|
+
*/
|
|
32345
|
+
resetConciergeMemoryThread() {
|
|
32346
|
+
this.activeMemoryThreadId = void 0;
|
|
32347
|
+
}
|
|
32348
|
+
ensureActiveMemoryThread() {
|
|
32349
|
+
if (!this.activeMemoryThreadId) {
|
|
32350
|
+
this.activeMemoryThreadId = randomUUID();
|
|
32351
|
+
}
|
|
32352
|
+
return this.activeMemoryThreadId;
|
|
32353
|
+
}
|
|
30948
32354
|
/**
|
|
30949
32355
|
* Stitch fortress state into a single context blob the substrate
|
|
30950
32356
|
* folds into its summarization prompt.
|
|
@@ -30957,6 +32363,11 @@ var OperatorChatService = class {
|
|
|
30957
32363
|
* ## Sanctuary reference
|
|
30958
32364
|
* <static domain reference block>
|
|
30959
32365
|
*
|
|
32366
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
32367
|
+
* OPERATOR: ...
|
|
32368
|
+
* CONCIERGE: ...
|
|
32369
|
+
* ---
|
|
32370
|
+
*
|
|
30960
32371
|
* ## Recent activity
|
|
30961
32372
|
* <recentActivity output>
|
|
30962
32373
|
*
|
|
@@ -30966,37 +32377,69 @@ var OperatorChatService = class {
|
|
|
30966
32377
|
* ## Open inbox
|
|
30967
32378
|
* <openInbox output>
|
|
30968
32379
|
* ```
|
|
32380
|
+
*
|
|
32381
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
32382
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
32383
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
32384
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
32385
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
32386
|
+
* serialization is the canonical path for v1.3.
|
|
30969
32387
|
*/
|
|
30970
|
-
async assembleConciergeContext() {
|
|
32388
|
+
async assembleConciergeContext(priorTurns = []) {
|
|
30971
32389
|
const ref = `## Sanctuary reference
|
|
30972
32390
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
32391
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
30973
32392
|
if (!this.contextProviders) {
|
|
30974
|
-
return
|
|
30975
|
-
|
|
30976
|
-
|
|
30977
|
-
(no providers wired)
|
|
30978
|
-
|
|
30979
|
-
##
|
|
30980
|
-
(
|
|
30981
|
-
|
|
30982
|
-
## Open inbox
|
|
30983
|
-
(no providers wired)`;
|
|
32393
|
+
return [
|
|
32394
|
+
ref,
|
|
32395
|
+
...priorSection ? [priorSection] : [],
|
|
32396
|
+
"## Recent activity\n(no providers wired)",
|
|
32397
|
+
"## Wrapped agents\n(no providers wired)",
|
|
32398
|
+
"## Open inbox\n(no providers wired)"
|
|
32399
|
+
].join("\n\n");
|
|
30984
32400
|
}
|
|
30985
32401
|
const [activity, agents, inbox] = await Promise.all([
|
|
30986
32402
|
this.contextProviders.recentActivity(),
|
|
30987
32403
|
this.contextProviders.agentInventory(),
|
|
30988
32404
|
this.contextProviders.openInbox()
|
|
30989
32405
|
]);
|
|
30990
|
-
return
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
|
|
30995
|
-
|
|
30996
|
-
${agents}
|
|
30997
|
-
|
|
30998
|
-
|
|
30999
|
-
|
|
32406
|
+
return [
|
|
32407
|
+
ref,
|
|
32408
|
+
...priorSection ? [priorSection] : [],
|
|
32409
|
+
`## Recent activity
|
|
32410
|
+
${activity}`,
|
|
32411
|
+
`## Wrapped agents
|
|
32412
|
+
${agents}`,
|
|
32413
|
+
`## Open inbox
|
|
32414
|
+
${inbox}`
|
|
32415
|
+
].join("\n\n");
|
|
32416
|
+
}
|
|
32417
|
+
/**
|
|
32418
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
32419
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
32420
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
32421
|
+
* the input is empty or when the budget excludes every turn.
|
|
32422
|
+
*/
|
|
32423
|
+
formatPriorTurnsSection(turns) {
|
|
32424
|
+
if (turns.length === 0) return "";
|
|
32425
|
+
const HEADER = "## Prior conversation";
|
|
32426
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
32427
|
+
const headerTokens = approxTokenLen(`${HEADER}
|
|
32428
|
+
`);
|
|
32429
|
+
const sepTokens = approxTokenLen("\n");
|
|
32430
|
+
let runningTokens = headerTokens;
|
|
32431
|
+
let runningLines = [];
|
|
32432
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
32433
|
+
const line = lines[i];
|
|
32434
|
+
const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
32435
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
32436
|
+
runningTokens += tokens;
|
|
32437
|
+
runningLines.push(line);
|
|
32438
|
+
}
|
|
32439
|
+
if (runningLines.length === 0) return "";
|
|
32440
|
+
runningLines = runningLines.reverse();
|
|
32441
|
+
return `${HEADER}
|
|
32442
|
+
${runningLines.join("\n")}`;
|
|
31000
32443
|
}
|
|
31001
32444
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
31002
32445
|
emit(operation, payload, result) {
|
|
@@ -31012,6 +32455,10 @@ ${inbox}`;
|
|
|
31012
32455
|
function makeEventId(prefix) {
|
|
31013
32456
|
return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
31014
32457
|
}
|
|
32458
|
+
function formatPriorTurnLine(turn) {
|
|
32459
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
32460
|
+
return `${label}: ${turn.content}`;
|
|
32461
|
+
}
|
|
31015
32462
|
function hashOf(input) {
|
|
31016
32463
|
return hashToString(sha256(stringToBytes(input)));
|
|
31017
32464
|
}
|
|
@@ -31108,6 +32555,297 @@ var OperatorChatStore = class {
|
|
|
31108
32555
|
}
|
|
31109
32556
|
};
|
|
31110
32557
|
|
|
32558
|
+
// src/chat/concierge-memory-store.ts
|
|
32559
|
+
init_encryption();
|
|
32560
|
+
init_encoding();
|
|
32561
|
+
var CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
32562
|
+
var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
32563
|
+
var HKDF_INFO2 = "concierge-memory-store-v1";
|
|
32564
|
+
var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
32565
|
+
var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
32566
|
+
var ConciergeMemoryStore = class {
|
|
32567
|
+
storage;
|
|
32568
|
+
encryptionKey;
|
|
32569
|
+
fortressId;
|
|
32570
|
+
retentionDays;
|
|
32571
|
+
locks;
|
|
32572
|
+
constructor(opts) {
|
|
32573
|
+
this.storage = opts.storage;
|
|
32574
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
32575
|
+
this.fortressId = opts.fortressId;
|
|
32576
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
32577
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
32578
|
+
}
|
|
32579
|
+
/**
|
|
32580
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
32581
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
32582
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
32583
|
+
* monotonicity even under concurrent callers.
|
|
32584
|
+
*/
|
|
32585
|
+
async appendTurn(threadId, role, content) {
|
|
32586
|
+
return this.withLock(threadId, async () => {
|
|
32587
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
32588
|
+
const now = /* @__PURE__ */ new Date();
|
|
32589
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
32590
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
32591
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
32592
|
+
const turn = {
|
|
32593
|
+
thread_id: threadId,
|
|
32594
|
+
fortress_id: this.fortressId,
|
|
32595
|
+
turn_id: nextTurnId,
|
|
32596
|
+
role,
|
|
32597
|
+
content,
|
|
32598
|
+
created_at: now.toISOString(),
|
|
32599
|
+
retention_until: retentionUntil.toISOString()
|
|
32600
|
+
};
|
|
32601
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
32602
|
+
version: 1,
|
|
32603
|
+
thread_id: threadId,
|
|
32604
|
+
fortress_id: this.fortressId,
|
|
32605
|
+
created_at: now.toISOString(),
|
|
32606
|
+
turns: [turn]
|
|
32607
|
+
};
|
|
32608
|
+
await this.saveBundle(next);
|
|
32609
|
+
return turn;
|
|
32610
|
+
});
|
|
32611
|
+
}
|
|
32612
|
+
/**
|
|
32613
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
32614
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
32615
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
32616
|
+
*/
|
|
32617
|
+
async readThread(threadId, opts) {
|
|
32618
|
+
const bundle = await this.loadBundle(threadId);
|
|
32619
|
+
if (!bundle) return [];
|
|
32620
|
+
let turns = bundle.turns;
|
|
32621
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
32622
|
+
const cutoff = opts.sinceTurnId;
|
|
32623
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
32624
|
+
}
|
|
32625
|
+
if (opts?.limit !== void 0) {
|
|
32626
|
+
turns = turns.slice(0, opts.limit);
|
|
32627
|
+
}
|
|
32628
|
+
return turns;
|
|
32629
|
+
}
|
|
32630
|
+
/**
|
|
32631
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
32632
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
32633
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
32634
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
32635
|
+
* with a concrete cause.
|
|
32636
|
+
*
|
|
32637
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
32638
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
32639
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
32640
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
32641
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
32642
|
+
* - Storage IO error → `io_failed`.
|
|
32643
|
+
*/
|
|
32644
|
+
async readThreadStrict(threadId, opts) {
|
|
32645
|
+
const key = bundleKey(threadId);
|
|
32646
|
+
let raw;
|
|
32647
|
+
try {
|
|
32648
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32649
|
+
} catch {
|
|
32650
|
+
return { ok: false, reason: "io_failed" };
|
|
32651
|
+
}
|
|
32652
|
+
if (!raw) return { ok: true, turns: [] };
|
|
32653
|
+
if (raw.length > MAX_BUNDLE_BYTES2) {
|
|
32654
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
32655
|
+
}
|
|
32656
|
+
let envelope;
|
|
32657
|
+
try {
|
|
32658
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
32659
|
+
} catch {
|
|
32660
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
32661
|
+
}
|
|
32662
|
+
let plaintext;
|
|
32663
|
+
try {
|
|
32664
|
+
const aad = stringToBytes(threadId);
|
|
32665
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
32666
|
+
} catch {
|
|
32667
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
32668
|
+
}
|
|
32669
|
+
let parsed;
|
|
32670
|
+
try {
|
|
32671
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
32672
|
+
} catch {
|
|
32673
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
32674
|
+
}
|
|
32675
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
32676
|
+
if (parsed.thread_id !== threadId) {
|
|
32677
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
32678
|
+
}
|
|
32679
|
+
let turns = parsed.turns;
|
|
32680
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
32681
|
+
const cutoff = opts.sinceTurnId;
|
|
32682
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
32683
|
+
}
|
|
32684
|
+
if (opts?.limit !== void 0) {
|
|
32685
|
+
turns = turns.slice(0, opts.limit);
|
|
32686
|
+
}
|
|
32687
|
+
return { ok: true, turns };
|
|
32688
|
+
}
|
|
32689
|
+
/**
|
|
32690
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
32691
|
+
* Sorted newest-first by last_turn_at.
|
|
32692
|
+
*/
|
|
32693
|
+
async listThreads(opts) {
|
|
32694
|
+
const entries = await this.storage.list(
|
|
32695
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32696
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32697
|
+
);
|
|
32698
|
+
const summaries = [];
|
|
32699
|
+
for (const meta of entries) {
|
|
32700
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32701
|
+
if (threadId === null) continue;
|
|
32702
|
+
const bundle = await this.loadBundle(threadId);
|
|
32703
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
32704
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
32705
|
+
summaries.push({
|
|
32706
|
+
thread_id: bundle.thread_id,
|
|
32707
|
+
created_at: bundle.created_at,
|
|
32708
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
32709
|
+
turn_count: bundle.turns.length
|
|
32710
|
+
});
|
|
32711
|
+
}
|
|
32712
|
+
summaries.sort(
|
|
32713
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
32714
|
+
);
|
|
32715
|
+
if (opts?.limit !== void 0) {
|
|
32716
|
+
return summaries.slice(0, opts.limit);
|
|
32717
|
+
}
|
|
32718
|
+
return summaries;
|
|
32719
|
+
}
|
|
32720
|
+
/**
|
|
32721
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
32722
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
32723
|
+
* caller's responsibility.
|
|
32724
|
+
*/
|
|
32725
|
+
async deleteThread(threadId) {
|
|
32726
|
+
const key = bundleKey(threadId);
|
|
32727
|
+
return this.withLock(threadId, async () => {
|
|
32728
|
+
const existed = await this.storage.exists(
|
|
32729
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32730
|
+
key
|
|
32731
|
+
);
|
|
32732
|
+
if (!existed) return false;
|
|
32733
|
+
try {
|
|
32734
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32735
|
+
} catch {
|
|
32736
|
+
return false;
|
|
32737
|
+
}
|
|
32738
|
+
return true;
|
|
32739
|
+
});
|
|
32740
|
+
}
|
|
32741
|
+
/**
|
|
32742
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
32743
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
32744
|
+
*/
|
|
32745
|
+
async pruneExpired(now) {
|
|
32746
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
32747
|
+
const entries = await this.storage.list(
|
|
32748
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32749
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
32750
|
+
);
|
|
32751
|
+
let pruned = 0;
|
|
32752
|
+
for (const meta of entries) {
|
|
32753
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
32754
|
+
if (threadId === null) continue;
|
|
32755
|
+
pruned += await this.withLock(threadId, async () => {
|
|
32756
|
+
const bundle = await this.loadBundle(threadId);
|
|
32757
|
+
if (!bundle) return 0;
|
|
32758
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
32759
|
+
const dropped = bundle.turns.length - kept.length;
|
|
32760
|
+
if (dropped === 0) return 0;
|
|
32761
|
+
if (kept.length === 0) {
|
|
32762
|
+
await this.storage.delete(
|
|
32763
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32764
|
+
bundleKey(threadId)
|
|
32765
|
+
);
|
|
32766
|
+
} else {
|
|
32767
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
32768
|
+
}
|
|
32769
|
+
return dropped;
|
|
32770
|
+
});
|
|
32771
|
+
}
|
|
32772
|
+
return { pruned };
|
|
32773
|
+
}
|
|
32774
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
32775
|
+
async loadBundle(threadId) {
|
|
32776
|
+
const key = bundleKey(threadId);
|
|
32777
|
+
let raw;
|
|
32778
|
+
try {
|
|
32779
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
32780
|
+
} catch {
|
|
32781
|
+
return null;
|
|
32782
|
+
}
|
|
32783
|
+
if (!raw) return null;
|
|
32784
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
32785
|
+
try {
|
|
32786
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
32787
|
+
const aad = stringToBytes(threadId);
|
|
32788
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
32789
|
+
const parsed = JSON.parse(
|
|
32790
|
+
bytesToString(plaintext)
|
|
32791
|
+
);
|
|
32792
|
+
if (parsed.version !== 1) return null;
|
|
32793
|
+
if (parsed.thread_id !== threadId) return null;
|
|
32794
|
+
return parsed;
|
|
32795
|
+
} catch {
|
|
32796
|
+
return null;
|
|
32797
|
+
}
|
|
32798
|
+
}
|
|
32799
|
+
async saveBundle(bundle) {
|
|
32800
|
+
const key = bundleKey(bundle.thread_id);
|
|
32801
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
32802
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
32803
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
32804
|
+
await this.storage.write(
|
|
32805
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
32806
|
+
key,
|
|
32807
|
+
stringToBytes(JSON.stringify(envelope))
|
|
32808
|
+
);
|
|
32809
|
+
}
|
|
32810
|
+
/**
|
|
32811
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
32812
|
+
* once the task settles (success or failure). Generic helper so
|
|
32813
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
32814
|
+
*/
|
|
32815
|
+
async withLock(threadId, task) {
|
|
32816
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
32817
|
+
let release;
|
|
32818
|
+
const next = new Promise((resolve6) => {
|
|
32819
|
+
release = resolve6;
|
|
32820
|
+
});
|
|
32821
|
+
const chained = previous.then(() => next);
|
|
32822
|
+
this.locks.set(threadId, chained);
|
|
32823
|
+
try {
|
|
32824
|
+
await previous;
|
|
32825
|
+
return await task();
|
|
32826
|
+
} finally {
|
|
32827
|
+
release();
|
|
32828
|
+
if (this.locks.get(threadId) === chained) {
|
|
32829
|
+
this.locks.delete(threadId);
|
|
32830
|
+
}
|
|
32831
|
+
}
|
|
32832
|
+
}
|
|
32833
|
+
};
|
|
32834
|
+
function bundleKey(threadId) {
|
|
32835
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
32836
|
+
}
|
|
32837
|
+
function stripKeyPrefix(key) {
|
|
32838
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
32839
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
32840
|
+
}
|
|
32841
|
+
function lastTurnId(bundle) {
|
|
32842
|
+
let max = 0;
|
|
32843
|
+
for (const t of bundle.turns) {
|
|
32844
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
32845
|
+
}
|
|
32846
|
+
return max;
|
|
32847
|
+
}
|
|
32848
|
+
|
|
31111
32849
|
// src/dashboard/v1_1/wiring.ts
|
|
31112
32850
|
var CapabilityErrorAgentController = class {
|
|
31113
32851
|
fail(action) {
|
|
@@ -31146,6 +32884,14 @@ function buildV11Bindings(inputs) {
|
|
|
31146
32884
|
let operatorChatService;
|
|
31147
32885
|
if (inputs.storage && inputs.masterKey) {
|
|
31148
32886
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
32887
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
32888
|
+
storage: inputs.storage,
|
|
32889
|
+
masterKey: inputs.masterKey,
|
|
32890
|
+
fortressId: inputs.fortressId,
|
|
32891
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
32892
|
+
});
|
|
32893
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
32894
|
+
});
|
|
31149
32895
|
operatorChatService = new OperatorChatService({
|
|
31150
32896
|
store: chatStore,
|
|
31151
32897
|
auditLog: inputs.auditLog,
|
|
@@ -31156,7 +32902,8 @@ function buildV11Bindings(inputs) {
|
|
|
31156
32902
|
identityId: inputs.identityId,
|
|
31157
32903
|
registry
|
|
31158
32904
|
}),
|
|
31159
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
32905
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
32906
|
+
conciergeMemory
|
|
31160
32907
|
});
|
|
31161
32908
|
}
|
|
31162
32909
|
const hubService = new HubService({
|
|
@@ -31348,13 +33095,13 @@ init_encryption();
|
|
|
31348
33095
|
init_encoding();
|
|
31349
33096
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
31350
33097
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
31351
|
-
var
|
|
33098
|
+
var HKDF_INFO3 = "intelligence-substrate-config";
|
|
31352
33099
|
var IntelligenceConfigStore = class {
|
|
31353
33100
|
storage;
|
|
31354
33101
|
encryptionKey;
|
|
31355
33102
|
constructor(storage, masterKey) {
|
|
31356
33103
|
this.storage = storage;
|
|
31357
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33104
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
31358
33105
|
}
|
|
31359
33106
|
/**
|
|
31360
33107
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -33493,7 +35240,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33493
35240
|
);
|
|
33494
35241
|
}
|
|
33495
35242
|
}
|
|
33496
|
-
const
|
|
35243
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
35244
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
35245
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
33497
35246
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
33498
35247
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
33499
35248
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -33502,6 +35251,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33502
35251
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
33503
35252
|
);
|
|
33504
35253
|
}
|
|
35254
|
+
let detailedFailureClass;
|
|
35255
|
+
if (identityFailed) {
|
|
35256
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
35257
|
+
} else if (reputationBundleFailed) {
|
|
35258
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
35259
|
+
} else if (reputationAttestationFailed) {
|
|
35260
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
35261
|
+
} else if (unverifiableFailed) {
|
|
35262
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
35263
|
+
}
|
|
33505
35264
|
return {
|
|
33506
35265
|
version: "1.1",
|
|
33507
35266
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -33521,7 +35280,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
33521
35280
|
identity,
|
|
33522
35281
|
audit,
|
|
33523
35282
|
reputation,
|
|
33524
|
-
failure_class:
|
|
35283
|
+
failure_class: detailedFailureClass
|
|
33525
35284
|
};
|
|
33526
35285
|
}
|
|
33527
35286
|
|
|
@@ -33911,7 +35670,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
33911
35670
|
}
|
|
33912
35671
|
return null;
|
|
33913
35672
|
}
|
|
33914
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
35673
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
33915
35674
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
33916
35675
|
if (!destinationSigner) {
|
|
33917
35676
|
return {
|
|
@@ -33973,8 +35732,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
33973
35732
|
}
|
|
33974
35733
|
}
|
|
33975
35734
|
}
|
|
35735
|
+
let plaintext;
|
|
33976
35736
|
try {
|
|
33977
|
-
|
|
35737
|
+
plaintext = decrypt(
|
|
33978
35738
|
item.entry.payload,
|
|
33979
35739
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
33980
35740
|
);
|
|
@@ -33983,28 +35743,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
33983
35743
|
skipped++;
|
|
33984
35744
|
continue;
|
|
33985
35745
|
}
|
|
33986
|
-
await stateStore.write(
|
|
33987
|
-
item.namespace,
|
|
33988
|
-
item.key,
|
|
33989
|
-
bytesToString(plaintext),
|
|
33990
|
-
destinationSigner.identity_id,
|
|
33991
|
-
destinationSigner.encrypted_private_key,
|
|
33992
|
-
identityEncryptionKey,
|
|
33993
|
-
{
|
|
33994
|
-
content_type: item.entry.metadata.content_type,
|
|
33995
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
33996
|
-
tags: [
|
|
33997
|
-
...item.entry.metadata.tags ?? [],
|
|
33998
|
-
"exit-import",
|
|
33999
|
-
`source:${item.entry.kid}`
|
|
34000
|
-
]
|
|
34001
|
-
}
|
|
34002
|
-
);
|
|
34003
|
-
imported++;
|
|
34004
35746
|
} catch {
|
|
34005
35747
|
skippedInvalidSig++;
|
|
34006
35748
|
skipped++;
|
|
35749
|
+
continue;
|
|
34007
35750
|
}
|
|
35751
|
+
await stateStore.write(
|
|
35752
|
+
item.namespace,
|
|
35753
|
+
item.key,
|
|
35754
|
+
bytesToString(plaintext),
|
|
35755
|
+
destinationSigner.identity_id,
|
|
35756
|
+
destinationSigner.encrypted_private_key,
|
|
35757
|
+
identityEncryptionKey,
|
|
35758
|
+
{
|
|
35759
|
+
content_type: item.entry.metadata.content_type,
|
|
35760
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
35761
|
+
tags: [
|
|
35762
|
+
...item.entry.metadata.tags ?? [],
|
|
35763
|
+
"exit-import",
|
|
35764
|
+
`source:${item.entry.kid}`
|
|
35765
|
+
]
|
|
35766
|
+
}
|
|
35767
|
+
);
|
|
35768
|
+
imported++;
|
|
35769
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
34008
35770
|
}
|
|
34009
35771
|
return {
|
|
34010
35772
|
status: "rekeyed",
|
|
@@ -34015,6 +35777,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
34015
35777
|
conflicts
|
|
34016
35778
|
};
|
|
34017
35779
|
}
|
|
35780
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
35781
|
+
let removed = 0;
|
|
35782
|
+
const failed = [];
|
|
35783
|
+
for (const loc of staged) {
|
|
35784
|
+
try {
|
|
35785
|
+
const ok = await storage.delete(loc.namespace, loc.key);
|
|
35786
|
+
if (ok) {
|
|
35787
|
+
removed++;
|
|
35788
|
+
} else {
|
|
35789
|
+
failed.push(loc);
|
|
35790
|
+
}
|
|
35791
|
+
} catch {
|
|
35792
|
+
failed.push(loc);
|
|
35793
|
+
}
|
|
35794
|
+
}
|
|
35795
|
+
return { removed, failed };
|
|
35796
|
+
}
|
|
34018
35797
|
async function stageArtifact(storage, namespace, key, value) {
|
|
34019
35798
|
await storage.write(namespace, key, jsonBytes(value));
|
|
34020
35799
|
}
|
|
@@ -34139,6 +35918,8 @@ async function importExitBundle(opts) {
|
|
|
34139
35918
|
}
|
|
34140
35919
|
const importId = importIdForManifest(manifest);
|
|
34141
35920
|
const stagedArtifacts = [];
|
|
35921
|
+
const stagedLocations = [];
|
|
35922
|
+
const importedRekeyEntries = [];
|
|
34142
35923
|
if (identityArtifact) {
|
|
34143
35924
|
await stageArtifact(
|
|
34144
35925
|
opts.storage,
|
|
@@ -34147,10 +35928,15 @@ async function importExitBundle(opts) {
|
|
|
34147
35928
|
identityArtifact.json
|
|
34148
35929
|
);
|
|
34149
35930
|
stagedArtifacts.push("public_identity");
|
|
35931
|
+
stagedLocations.push({
|
|
35932
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
35933
|
+
key: identityArtifact.json.bundle.identity_id
|
|
35934
|
+
});
|
|
34150
35935
|
}
|
|
34151
35936
|
if (policySet) {
|
|
34152
35937
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
34153
35938
|
stagedArtifacts.push("policy_set");
|
|
35939
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
34154
35940
|
}
|
|
34155
35941
|
if (auditReceipts) {
|
|
34156
35942
|
await stageArtifact(
|
|
@@ -34160,10 +35946,12 @@ async function importExitBundle(opts) {
|
|
|
34160
35946
|
auditReceipts.json
|
|
34161
35947
|
);
|
|
34162
35948
|
stagedArtifacts.push("audit_receipts");
|
|
35949
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
34163
35950
|
}
|
|
34164
35951
|
if (commitments) {
|
|
34165
35952
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
34166
35953
|
stagedArtifacts.push("commitments");
|
|
35954
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
34167
35955
|
}
|
|
34168
35956
|
if (placeholderMetadata) {
|
|
34169
35957
|
await stageArtifact(
|
|
@@ -34173,12 +35961,17 @@ async function importExitBundle(opts) {
|
|
|
34173
35961
|
placeholderMetadata.json
|
|
34174
35962
|
);
|
|
34175
35963
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
35964
|
+
stagedLocations.push({
|
|
35965
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
35966
|
+
key: importId
|
|
35967
|
+
});
|
|
34176
35968
|
}
|
|
34177
35969
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
34178
35970
|
manifest: manifest.body,
|
|
34179
35971
|
verified_at: verification.verified_at,
|
|
34180
35972
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
34181
35973
|
});
|
|
35974
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
34182
35975
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
34183
35976
|
let reputationResult = {
|
|
34184
35977
|
imported_attestations: 0,
|
|
@@ -34203,26 +35996,57 @@ async function importExitBundle(opts) {
|
|
|
34203
35996
|
encryptedState?.json ?? null,
|
|
34204
35997
|
opts
|
|
34205
35998
|
);
|
|
34206
|
-
|
|
34207
|
-
|
|
34208
|
-
|
|
34209
|
-
|
|
34210
|
-
|
|
34211
|
-
|
|
34212
|
-
|
|
34213
|
-
|
|
34214
|
-
|
|
34215
|
-
|
|
34216
|
-
|
|
34217
|
-
|
|
34218
|
-
|
|
34219
|
-
|
|
34220
|
-
|
|
34221
|
-
|
|
34222
|
-
|
|
34223
|
-
|
|
34224
|
-
|
|
34225
|
-
|
|
35999
|
+
let stateResult;
|
|
36000
|
+
try {
|
|
36001
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
36002
|
+
encryptedState.json,
|
|
36003
|
+
opts,
|
|
36004
|
+
sourceMasterKey,
|
|
36005
|
+
publicKeys.byIdentityId,
|
|
36006
|
+
importedRekeyEntries
|
|
36007
|
+
) : {
|
|
36008
|
+
status: "staged_requires_source_key",
|
|
36009
|
+
imported_keys: 0,
|
|
36010
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
36011
|
+
skipped_invalid_sig: 0,
|
|
36012
|
+
skipped_unknown_kid: 0,
|
|
36013
|
+
conflicts: conflicts.state_conflicts.length
|
|
36014
|
+
} : {
|
|
36015
|
+
status: "not_requested",
|
|
36016
|
+
imported_keys: 0,
|
|
36017
|
+
skipped_keys: 0,
|
|
36018
|
+
skipped_invalid_sig: 0,
|
|
36019
|
+
skipped_unknown_kid: 0,
|
|
36020
|
+
conflicts: 0
|
|
36021
|
+
};
|
|
36022
|
+
} catch (err) {
|
|
36023
|
+
const toCleanup = [
|
|
36024
|
+
...importedRekeyEntries,
|
|
36025
|
+
...stagedLocations
|
|
36026
|
+
];
|
|
36027
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
36028
|
+
opts.auditLog.append(
|
|
36029
|
+
"l1",
|
|
36030
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
36031
|
+
manifest.body.identity_binding.identity_id,
|
|
36032
|
+
{
|
|
36033
|
+
import_id: importId,
|
|
36034
|
+
manifest_version: manifest.body.manifest_version,
|
|
36035
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
36036
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
36037
|
+
removed_total: cleanup.removed,
|
|
36038
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
36039
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
36040
|
+
},
|
|
36041
|
+
"failure"
|
|
36042
|
+
);
|
|
36043
|
+
await opts.auditLog.flush();
|
|
36044
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
36045
|
+
throw new ExitBundleImportError(
|
|
36046
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
36047
|
+
`Exit-bundle re-key failed: ${originalMessage}. Cleanup removed ${cleanup.removed} of ${toCleanup.length} staged paths (${importedRekeyEntries.length} re-keyed entries plus ${stagedLocations.length} staged artifacts; ${cleanup.failed.length} cleanup deletes failed).`
|
|
36048
|
+
);
|
|
36049
|
+
}
|
|
34226
36050
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
34227
36051
|
import_id: importId,
|
|
34228
36052
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -34448,7 +36272,19 @@ async function runExitCommand(args) {
|
|
|
34448
36272
|
}
|
|
34449
36273
|
const config = await loadConfig();
|
|
34450
36274
|
const ctx = await openExitContext(argv, env);
|
|
34451
|
-
|
|
36275
|
+
let policy;
|
|
36276
|
+
try {
|
|
36277
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
36278
|
+
} catch (policyErr) {
|
|
36279
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
36280
|
+
write(err, `
|
|
36281
|
+
Sanctuary cannot proceed.
|
|
36282
|
+
${policyErr.message}
|
|
36283
|
+
`);
|
|
36284
|
+
return 1;
|
|
36285
|
+
}
|
|
36286
|
+
throw policyErr;
|
|
36287
|
+
}
|
|
34452
36288
|
const result = await exportExitBundle({
|
|
34453
36289
|
bundleDir: outDir,
|
|
34454
36290
|
storage: ctx.storage,
|
|
@@ -35100,7 +36936,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35100
36936
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
35101
36937
|
await profileStore.load();
|
|
35102
36938
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
35103
|
-
|
|
36939
|
+
let policy;
|
|
36940
|
+
try {
|
|
36941
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
36942
|
+
} catch (err) {
|
|
36943
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
36944
|
+
console.error(`
|
|
36945
|
+
Sanctuary cannot start.
|
|
36946
|
+
${err.message}
|
|
36947
|
+
`);
|
|
36948
|
+
process.exit(1);
|
|
36949
|
+
}
|
|
36950
|
+
throw err;
|
|
36951
|
+
}
|
|
35104
36952
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
35105
36953
|
await baseline.load();
|
|
35106
36954
|
let approvalChannel;
|
|
@@ -35197,7 +37045,35 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35197
37045
|
timestamp: alert.timestamp
|
|
35198
37046
|
});
|
|
35199
37047
|
} : void 0;
|
|
35200
|
-
const
|
|
37048
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37049
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37050
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
37051
|
+
storage,
|
|
37052
|
+
masterKey,
|
|
37053
|
+
auditLog,
|
|
37054
|
+
identityId: aggregatorIdentityId,
|
|
37055
|
+
fortressId: fortressIdForAggregator
|
|
37056
|
+
});
|
|
37057
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
37058
|
+
underlying: approvalChannel,
|
|
37059
|
+
aggregator: approvalAggregator,
|
|
37060
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
37061
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
37062
|
+
});
|
|
37063
|
+
const gate = new ApprovalGate(
|
|
37064
|
+
policy,
|
|
37065
|
+
baseline,
|
|
37066
|
+
wrappedApprovalChannel,
|
|
37067
|
+
auditLog,
|
|
37068
|
+
injectionDetector,
|
|
37069
|
+
onInjectionAlert
|
|
37070
|
+
);
|
|
37071
|
+
gate.setApprovalEventCallback((event) => {
|
|
37072
|
+
void approvalAggregator.ingest(event);
|
|
37073
|
+
});
|
|
37074
|
+
if (dashboard) {
|
|
37075
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
37076
|
+
}
|
|
35201
37077
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
35202
37078
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
35203
37079
|
config,
|
|
@@ -35373,6 +37249,6 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
35373
37249
|
};
|
|
35374
37250
|
}
|
|
35375
37251
|
|
|
35376
|
-
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, ExitBundleImportError, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
37252
|
+
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, ExitBundleImportError, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MalformedPrincipalPolicyError, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
35377
37253
|
//# sourceMappingURL=index.js.map
|
|
35378
37254
|
//# sourceMappingURL=index.js.map
|