@sanctuary-framework/mcp-server 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1759 -132
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1759 -132
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1410 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +541 -7
- package/dist/index.d.ts +541 -7
- package/dist/index.js +1395 -59
- package/dist/index.js.map +1 -1
- package/package.json +17 -16
package/dist/cli.cjs
CHANGED
|
@@ -4428,13 +4428,23 @@ function parseScalar(value) {
|
|
|
4428
4428
|
return value.replace(/^["']|["']$/g, "");
|
|
4429
4429
|
}
|
|
4430
4430
|
function validatePolicy(raw) {
|
|
4431
|
+
if (!("tier1_always_approve" in raw)) {
|
|
4432
|
+
throw new Error(
|
|
4433
|
+
"Policy file must include 'tier1_always_approve' as an explicit list (use [] for empty). Remove specific entries instead of removing the whole key."
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
if (!("approval_channel" in raw)) {
|
|
4437
|
+
throw new Error(
|
|
4438
|
+
"Policy file must include 'approval_channel' as an explicit object (use {} for defaults). Remove specific entries instead of removing the whole key."
|
|
4439
|
+
);
|
|
4440
|
+
}
|
|
4431
4441
|
const userTier3 = raw.tier3_always_allow ?? [];
|
|
4432
4442
|
const mergedTier3 = [
|
|
4433
4443
|
.../* @__PURE__ */ new Set([...userTier3, ...DEFAULT_POLICY.tier3_always_allow])
|
|
4434
4444
|
];
|
|
4435
4445
|
return {
|
|
4436
4446
|
version: raw.version ?? 1,
|
|
4437
|
-
tier1_always_approve: raw.tier1_always_approve
|
|
4447
|
+
tier1_always_approve: raw.tier1_always_approve,
|
|
4438
4448
|
tier2_anomaly: {
|
|
4439
4449
|
...DEFAULT_TIER2,
|
|
4440
4450
|
...raw.tier2_anomaly ?? {}
|
|
@@ -4455,6 +4465,11 @@ function generateDefaultPolicyYaml() {
|
|
|
4455
4465
|
# This file controls what your agent can do without asking.
|
|
4456
4466
|
# Edit this file directly. Your agent cannot modify it.
|
|
4457
4467
|
# Changes take effect on server restart.
|
|
4468
|
+
#
|
|
4469
|
+
# Required keys (must be present; use [] or {} for empty):
|
|
4470
|
+
# tier1_always_approve, approval_channel
|
|
4471
|
+
# Optional keys (omit to use defaults; new defaults merge automatically):
|
|
4472
|
+
# tier2_anomaly, tier3_always_allow
|
|
4458
4473
|
|
|
4459
4474
|
version: 1
|
|
4460
4475
|
|
|
@@ -4561,21 +4576,39 @@ approval_channel:
|
|
|
4561
4576
|
}
|
|
4562
4577
|
async function loadPrincipalPolicy(storagePath) {
|
|
4563
4578
|
const policyPath = path.join(storagePath, "principal-policy.yaml");
|
|
4579
|
+
let content;
|
|
4580
|
+
try {
|
|
4581
|
+
content = await promises.readFile(policyPath, "utf-8");
|
|
4582
|
+
} catch (err) {
|
|
4583
|
+
const code = err?.code;
|
|
4584
|
+
if (code === "ENOENT") {
|
|
4585
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4586
|
+
try {
|
|
4587
|
+
await promises.writeFile(policyPath, defaultYaml, "utf-8");
|
|
4588
|
+
await promises.chmod(policyPath, 384);
|
|
4589
|
+
} catch (writeErr) {
|
|
4590
|
+
console.warn(
|
|
4591
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4592
|
+
);
|
|
4593
|
+
}
|
|
4594
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4595
|
+
}
|
|
4596
|
+
throw new MalformedPrincipalPolicyError(
|
|
4597
|
+
policyPath,
|
|
4598
|
+
`read failed: ${err.message}`
|
|
4599
|
+
);
|
|
4600
|
+
}
|
|
4564
4601
|
try {
|
|
4565
|
-
const content = await promises.readFile(policyPath, "utf-8");
|
|
4566
4602
|
const policy = parsePolicy(content);
|
|
4567
4603
|
return Object.freeze(policy);
|
|
4568
|
-
} catch {
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
} catch {
|
|
4574
|
-
}
|
|
4575
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4604
|
+
} catch (parseErr) {
|
|
4605
|
+
throw new MalformedPrincipalPolicyError(
|
|
4606
|
+
policyPath,
|
|
4607
|
+
parseErr.message
|
|
4608
|
+
);
|
|
4576
4609
|
}
|
|
4577
4610
|
}
|
|
4578
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY;
|
|
4611
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4579
4612
|
var init_loader = __esm({
|
|
4580
4613
|
"src/principal-policy/loader.ts"() {
|
|
4581
4614
|
DEFAULT_TIER2 = {
|
|
@@ -4708,6 +4741,20 @@ var init_loader = __esm({
|
|
|
4708
4741
|
],
|
|
4709
4742
|
approval_channel: DEFAULT_CHANNEL
|
|
4710
4743
|
};
|
|
4744
|
+
MalformedPrincipalPolicyError = class extends Error {
|
|
4745
|
+
constructor(policyPath, reason) {
|
|
4746
|
+
super(
|
|
4747
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4748
|
+
Reason: ${reason}
|
|
4749
|
+
Sanctuary refuses to substitute a default policy when an existing file is present, to avoid silently overriding operator intent. Fix the file or delete it to regenerate the default.`
|
|
4750
|
+
);
|
|
4751
|
+
this.policyPath = policyPath;
|
|
4752
|
+
this.reason = reason;
|
|
4753
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4754
|
+
}
|
|
4755
|
+
policyPath;
|
|
4756
|
+
reason;
|
|
4757
|
+
};
|
|
4711
4758
|
}
|
|
4712
4759
|
});
|
|
4713
4760
|
|
|
@@ -4927,7 +4974,7 @@ function deepSortKeys(obj) {
|
|
|
4927
4974
|
return sorted;
|
|
4928
4975
|
}
|
|
4929
4976
|
function canonicalizeForSigning(body) {
|
|
4930
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4977
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4931
4978
|
}
|
|
4932
4979
|
var init_types = __esm({
|
|
4933
4980
|
"src/shr/types.ts"() {
|
|
@@ -11574,6 +11621,7 @@ __export(passphrase_exports, {
|
|
|
11574
11621
|
getOrCreatePassphrase: () => getOrCreatePassphrase,
|
|
11575
11622
|
isOsKeyringLocation: () => isOsKeyringLocation,
|
|
11576
11623
|
keychainServiceFor: () => keychainServiceFor,
|
|
11624
|
+
legacyKeychainServiceFor: () => legacyKeychainServiceFor,
|
|
11577
11625
|
persistUserProvidedPassphrase: () => persistUserProvidedPassphrase,
|
|
11578
11626
|
readStoredPassphrase: () => readStoredPassphrase
|
|
11579
11627
|
});
|
|
@@ -11587,16 +11635,29 @@ async function getOrCreatePassphrase(opts = {}) {
|
|
|
11587
11635
|
const plat = opts.platformOverride ?? os.platform();
|
|
11588
11636
|
const exec2 = opts.exec ?? defaultExec;
|
|
11589
11637
|
const derive = opts.deriveMachineKey ?? deriveMachineKey;
|
|
11638
|
+
const legacyService = legacyKeychainServiceFor(storagePath, home);
|
|
11590
11639
|
if (plat === "darwin") {
|
|
11591
11640
|
const fromKc = await readFromKeychain(exec2, service);
|
|
11592
11641
|
if (fromKc) {
|
|
11593
11642
|
return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11594
11643
|
}
|
|
11644
|
+
if (legacyService !== service) {
|
|
11645
|
+
const fromLegacy = await readFromKeychain(exec2, legacyService);
|
|
11646
|
+
if (fromLegacy) {
|
|
11647
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11648
|
+
}
|
|
11649
|
+
}
|
|
11595
11650
|
} else if (plat === "linux") {
|
|
11596
11651
|
const fromSs = await readFromSecretService(exec2, service);
|
|
11597
11652
|
if (fromSs) {
|
|
11598
11653
|
return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11599
11654
|
}
|
|
11655
|
+
if (legacyService !== service) {
|
|
11656
|
+
const fromLegacy = await readFromSecretService(exec2, legacyService);
|
|
11657
|
+
if (fromLegacy) {
|
|
11658
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11659
|
+
}
|
|
11660
|
+
}
|
|
11600
11661
|
}
|
|
11601
11662
|
const fallback = fallbackFilePath(home, storagePath);
|
|
11602
11663
|
const fromFile = await readFromFallbackFile(fallback, home, derive);
|
|
@@ -11629,6 +11690,7 @@ async function readStoredPassphrase(opts = {}) {
|
|
|
11629
11690
|
const home = opts.home ?? os.homedir();
|
|
11630
11691
|
const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
|
|
11631
11692
|
const service = keychainServiceFor(storagePath, home);
|
|
11693
|
+
const legacyService = legacyKeychainServiceFor(storagePath, home);
|
|
11632
11694
|
const plat = opts.platformOverride ?? os.platform();
|
|
11633
11695
|
const exec2 = opts.exec ?? defaultExec;
|
|
11634
11696
|
const derive = opts.deriveMachineKey ?? deriveMachineKey;
|
|
@@ -11637,11 +11699,23 @@ async function readStoredPassphrase(opts = {}) {
|
|
|
11637
11699
|
if (fromKc) {
|
|
11638
11700
|
return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11639
11701
|
}
|
|
11702
|
+
if (legacyService !== service) {
|
|
11703
|
+
const fromLegacy = await readFromKeychain(exec2, legacyService);
|
|
11704
|
+
if (fromLegacy) {
|
|
11705
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11706
|
+
}
|
|
11707
|
+
}
|
|
11640
11708
|
} else if (plat === "linux") {
|
|
11641
11709
|
const fromSs = await readFromSecretService(exec2, service);
|
|
11642
11710
|
if (fromSs) {
|
|
11643
11711
|
return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11644
11712
|
}
|
|
11713
|
+
if (legacyService !== service) {
|
|
11714
|
+
const fromLegacy = await readFromSecretService(exec2, legacyService);
|
|
11715
|
+
if (fromLegacy) {
|
|
11716
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11717
|
+
}
|
|
11718
|
+
}
|
|
11645
11719
|
}
|
|
11646
11720
|
const fallback = fallbackFilePath(home, storagePath);
|
|
11647
11721
|
const fromFile = await readFromFallbackFile(fallback, home, derive);
|
|
@@ -11722,9 +11796,18 @@ async function writeToKeychain(value, exec2, service = KEYCHAIN_SERVICE_DEFAULT)
|
|
|
11722
11796
|
}
|
|
11723
11797
|
}
|
|
11724
11798
|
function keychainServiceFor(storagePath, home = os.homedir()) {
|
|
11725
|
-
const defaultPath = path.join(home, DEFAULT_STORAGE_DIR);
|
|
11726
|
-
|
|
11727
|
-
|
|
11799
|
+
const defaultPath = path.resolve(path.join(home, DEFAULT_STORAGE_DIR));
|
|
11800
|
+
const canonicalStorage = path.resolve(storagePath);
|
|
11801
|
+
if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
|
|
11802
|
+
const digest = sha256.sha256(Buffer.from(canonicalStorage, "utf-8"));
|
|
11803
|
+
const suffix = Buffer.from(digest).toString("hex").slice(0, 16);
|
|
11804
|
+
return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
|
|
11805
|
+
}
|
|
11806
|
+
function legacyKeychainServiceFor(storagePath, home = os.homedir()) {
|
|
11807
|
+
const defaultPath = path.resolve(path.join(home, DEFAULT_STORAGE_DIR));
|
|
11808
|
+
const canonicalStorage = path.resolve(storagePath);
|
|
11809
|
+
if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
|
|
11810
|
+
const digest = sha256.sha256(Buffer.from(canonicalStorage, "utf-8"));
|
|
11728
11811
|
const suffix = Buffer.from(digest).toString("hex").slice(0, 12);
|
|
11729
11812
|
return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
|
|
11730
11813
|
}
|
|
@@ -11811,7 +11894,7 @@ function deriveMachineKey(home) {
|
|
|
11811
11894
|
return hkdf.hkdf(sha256.sha256, material, void 0, "sanctuary-passphrase-v1", 32);
|
|
11812
11895
|
}
|
|
11813
11896
|
async function defaultExec(cmd, args, input) {
|
|
11814
|
-
return new Promise((
|
|
11897
|
+
return new Promise((resolve8, reject) => {
|
|
11815
11898
|
const child = child_process.spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
11816
11899
|
let stdout = "";
|
|
11817
11900
|
let stderr = "";
|
|
@@ -11822,7 +11905,7 @@ async function defaultExec(cmd, args, input) {
|
|
|
11822
11905
|
stderr += d.toString();
|
|
11823
11906
|
});
|
|
11824
11907
|
child.on("error", reject);
|
|
11825
|
-
child.on("close", (code) =>
|
|
11908
|
+
child.on("close", (code) => resolve8({ stdout, stderr, code }));
|
|
11826
11909
|
if (input !== void 0) {
|
|
11827
11910
|
child.stdin.write(input);
|
|
11828
11911
|
}
|
|
@@ -12016,7 +12099,7 @@ async function discoverTenants(options = {}) {
|
|
|
12016
12099
|
for (const child of children) {
|
|
12017
12100
|
const childPath = path.join(root, child);
|
|
12018
12101
|
if (child.startsWith(".")) continue;
|
|
12019
|
-
if (child === "state" || child === "backup" || child === "config") continue;
|
|
12102
|
+
if (child === "state" || child === "backup" || child === "config" || child === "default") continue;
|
|
12020
12103
|
const s = await promises.stat(childPath).catch(() => null);
|
|
12021
12104
|
if (!s || !s.isDirectory()) continue;
|
|
12022
12105
|
const desc = await describeTenant(child, childPath, home);
|
|
@@ -12028,6 +12111,17 @@ async function discoverTenants(options = {}) {
|
|
|
12028
12111
|
const desc = await describeTenant(path.basename(extra), extra, home);
|
|
12029
12112
|
if (desc) tenants.push(desc);
|
|
12030
12113
|
}
|
|
12114
|
+
const seen = /* @__PURE__ */ new Map();
|
|
12115
|
+
for (const t of tenants) {
|
|
12116
|
+
seen.set(t.name, (seen.get(t.name) ?? 0) + 1);
|
|
12117
|
+
}
|
|
12118
|
+
for (const [name, count] of seen) {
|
|
12119
|
+
if (count > 1) {
|
|
12120
|
+
console.error(
|
|
12121
|
+
`[sanctuary] warning: ${count} tenants share the name "${name}". Use --tenant with a unique name or storage path to disambiguate.`
|
|
12122
|
+
);
|
|
12123
|
+
}
|
|
12124
|
+
}
|
|
12031
12125
|
tenants.sort((a, b) => {
|
|
12032
12126
|
if (a.name === "default") return -1;
|
|
12033
12127
|
if (b.name === "default") return 1;
|
|
@@ -12379,7 +12473,7 @@ var init_auth_middleware = __esm({
|
|
|
12379
12473
|
});
|
|
12380
12474
|
|
|
12381
12475
|
// src/hub/constants.ts
|
|
12382
|
-
var HUB_API_PREFIX, HUB_ROUTES, HUB_FORTRESS_AGENT_ID_SENTINEL, HUB_INBOX_ACTIONS, HUB_AGENT_CONTROL_ACTIONS, HUB_TIER_1_AGENT_CONTROL_ACTIONS, HUB_ACTIVITY_DEFAULT_LIMIT, HUB_ACTIVITY_MAX_LIMIT, HUB_INBOX_DEFAULT_LIMIT, HUB_INBOX_MAX_LIMIT, HUB_AGENTS_DEFAULT_LIMIT, HUB_AGENTS_MAX_LIMIT, HUB_MAX_REQUEST_BODY_BYTES, HUB_CHAT_MESSAGE_MAX_CHARS, HUB_INBOX_TEMPLATE_NAMESPACES, HUB_ACTIVITY_TEMPLATE_NAMESPACES;
|
|
12476
|
+
var HUB_API_PREFIX, HUB_ROUTES, HUB_FORTRESS_AGENT_ID_SENTINEL, HUB_INBOX_ACTIONS, HUB_AGENT_CONTROL_ACTIONS, HUB_TIER_1_AGENT_CONTROL_ACTIONS, HUB_ACTIVITY_DEFAULT_LIMIT, HUB_ACTIVITY_MAX_LIMIT, HUB_CHAT_THREADS_DEFAULT_LIMIT, HUB_CHAT_THREADS_MAX_LIMIT, HUB_CHAT_TURNS_DEFAULT_LIMIT, HUB_CHAT_TURNS_MAX_LIMIT, HUB_INBOX_DEFAULT_LIMIT, HUB_INBOX_MAX_LIMIT, HUB_AGENTS_DEFAULT_LIMIT, HUB_AGENTS_MAX_LIMIT, HUB_MAX_REQUEST_BODY_BYTES, HUB_CHAT_MESSAGE_MAX_CHARS, HUB_INBOX_TEMPLATE_NAMESPACES, HUB_ACTIVITY_TEMPLATE_NAMESPACES;
|
|
12383
12477
|
var init_constants3 = __esm({
|
|
12384
12478
|
"src/hub/constants.ts"() {
|
|
12385
12479
|
HUB_API_PREFIX = "/api/hub";
|
|
@@ -12407,6 +12501,16 @@ var init_constants3 = __esm({
|
|
|
12407
12501
|
*/
|
|
12408
12502
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
12409
12503
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
12504
|
+
/**
|
|
12505
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
12506
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
12507
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
12508
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
12509
|
+
* v1.3 conversational sovereignty depth.
|
|
12510
|
+
*/
|
|
12511
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
12512
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12513
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12410
12514
|
/**
|
|
12411
12515
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
12412
12516
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -12430,6 +12534,10 @@ var init_constants3 = __esm({
|
|
|
12430
12534
|
];
|
|
12431
12535
|
HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
12432
12536
|
HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
12537
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
12538
|
+
HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
12539
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
12540
|
+
HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
12433
12541
|
HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
12434
12542
|
HUB_INBOX_MAX_LIMIT = 500;
|
|
12435
12543
|
HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -12612,6 +12720,23 @@ function checkChatMessage(value) {
|
|
|
12612
12720
|
}
|
|
12613
12721
|
return trimmed;
|
|
12614
12722
|
}
|
|
12723
|
+
function matchConciergeThreadRoute(path) {
|
|
12724
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
12725
|
+
if (!path.startsWith(prefix)) return null;
|
|
12726
|
+
const rest = path.slice(prefix.length);
|
|
12727
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
12728
|
+
const decoded = decodeURIComponent(rest);
|
|
12729
|
+
if (decoded.length === 0) return null;
|
|
12730
|
+
return { threadId: decoded };
|
|
12731
|
+
}
|
|
12732
|
+
function parseSince(raw) {
|
|
12733
|
+
if (raw === null || raw === "") return void 0;
|
|
12734
|
+
const parsed = Number.parseInt(raw, 10);
|
|
12735
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
12736
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
12737
|
+
}
|
|
12738
|
+
return parsed;
|
|
12739
|
+
}
|
|
12615
12740
|
function matchInboxRoute(path) {
|
|
12616
12741
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
12617
12742
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12795,6 +12920,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12795
12920
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12796
12921
|
return true;
|
|
12797
12922
|
}
|
|
12923
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12924
|
+
const limit = parseLimit(
|
|
12925
|
+
url.searchParams.get("limit"),
|
|
12926
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12927
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12928
|
+
);
|
|
12929
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12930
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12931
|
+
return true;
|
|
12932
|
+
}
|
|
12933
|
+
{
|
|
12934
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12935
|
+
if (threadMatch) {
|
|
12936
|
+
if (method === "GET") {
|
|
12937
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12938
|
+
const limit = parseLimit(
|
|
12939
|
+
url.searchParams.get("limit"),
|
|
12940
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12941
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12942
|
+
);
|
|
12943
|
+
const readOpts = { limit };
|
|
12944
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12945
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12946
|
+
threadMatch.threadId,
|
|
12947
|
+
readOpts
|
|
12948
|
+
);
|
|
12949
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12950
|
+
return true;
|
|
12951
|
+
}
|
|
12952
|
+
if (method === "DELETE") {
|
|
12953
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12954
|
+
threadMatch.threadId
|
|
12955
|
+
);
|
|
12956
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12957
|
+
ok: removed,
|
|
12958
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12959
|
+
});
|
|
12960
|
+
return true;
|
|
12961
|
+
}
|
|
12962
|
+
}
|
|
12963
|
+
}
|
|
12798
12964
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12799
12965
|
return true;
|
|
12800
12966
|
} catch (err) {
|
|
@@ -16826,6 +16992,8 @@ var init_intelligence_api_router = __esm({
|
|
|
16826
16992
|
this.code = code;
|
|
16827
16993
|
this.name = "IntelligenceRouterError";
|
|
16828
16994
|
}
|
|
16995
|
+
statusCode;
|
|
16996
|
+
code;
|
|
16829
16997
|
};
|
|
16830
16998
|
}
|
|
16831
16999
|
});
|
|
@@ -16915,6 +17083,168 @@ var init_dispatch = __esm({
|
|
|
16915
17083
|
init_intelligence_api_router();
|
|
16916
17084
|
}
|
|
16917
17085
|
});
|
|
17086
|
+
|
|
17087
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
17088
|
+
function writeJSON4(res, status, payload) {
|
|
17089
|
+
res.writeHead(status, {
|
|
17090
|
+
"Content-Type": "application/json",
|
|
17091
|
+
"Cache-Control": "no-store"
|
|
17092
|
+
});
|
|
17093
|
+
res.end(JSON.stringify(payload));
|
|
17094
|
+
}
|
|
17095
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
17096
|
+
if (raw === null || raw === "") return defaultValue;
|
|
17097
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17098
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
17099
|
+
return defaultValue;
|
|
17100
|
+
}
|
|
17101
|
+
return Math.min(parsed, max);
|
|
17102
|
+
}
|
|
17103
|
+
function isStatusFilter(value) {
|
|
17104
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
17105
|
+
}
|
|
17106
|
+
function matchEntryRoute(path) {
|
|
17107
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
17108
|
+
if (!path.startsWith(prefix)) return null;
|
|
17109
|
+
const rest = path.slice(prefix.length);
|
|
17110
|
+
if (rest.length === 0) return null;
|
|
17111
|
+
const slash = rest.indexOf("/");
|
|
17112
|
+
if (slash === -1) {
|
|
17113
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
17114
|
+
}
|
|
17115
|
+
return {
|
|
17116
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
17117
|
+
action: rest.slice(slash + 1)
|
|
17118
|
+
};
|
|
17119
|
+
}
|
|
17120
|
+
async function handleStream2(deps, res) {
|
|
17121
|
+
res.writeHead(200, {
|
|
17122
|
+
"Content-Type": "text/event-stream",
|
|
17123
|
+
"Cache-Control": "no-cache, no-transform",
|
|
17124
|
+
Connection: "keep-alive",
|
|
17125
|
+
"X-Accel-Buffering": "no"
|
|
17126
|
+
});
|
|
17127
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
17128
|
+
res.write(
|
|
17129
|
+
`event: approval_inbox_snapshot
|
|
17130
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
17131
|
+
|
|
17132
|
+
`
|
|
17133
|
+
);
|
|
17134
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
17135
|
+
try {
|
|
17136
|
+
res.write(
|
|
17137
|
+
`event: approval_inbox_${event.type}
|
|
17138
|
+
data: ${JSON.stringify(event.entry)}
|
|
17139
|
+
|
|
17140
|
+
`
|
|
17141
|
+
);
|
|
17142
|
+
} catch {
|
|
17143
|
+
}
|
|
17144
|
+
});
|
|
17145
|
+
const keepAlive = setInterval(() => {
|
|
17146
|
+
try {
|
|
17147
|
+
res.write(": keepalive\n\n");
|
|
17148
|
+
} catch {
|
|
17149
|
+
}
|
|
17150
|
+
}, 25e3);
|
|
17151
|
+
const cleanup = () => {
|
|
17152
|
+
clearInterval(keepAlive);
|
|
17153
|
+
unsubscribe();
|
|
17154
|
+
};
|
|
17155
|
+
res.on("close", cleanup);
|
|
17156
|
+
res.on("error", cleanup);
|
|
17157
|
+
}
|
|
17158
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
17159
|
+
const host = req.headers.host || "localhost";
|
|
17160
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
17161
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
17162
|
+
const path = url.pathname;
|
|
17163
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
17164
|
+
return false;
|
|
17165
|
+
}
|
|
17166
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
17167
|
+
if (!checkAuth(req, res, url)) return true;
|
|
17168
|
+
try {
|
|
17169
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
17170
|
+
await handleStream2(deps, res);
|
|
17171
|
+
return true;
|
|
17172
|
+
}
|
|
17173
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
17174
|
+
const limit = parseLimit2(
|
|
17175
|
+
url.searchParams.get("limit"),
|
|
17176
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
17177
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
17178
|
+
);
|
|
17179
|
+
const statusRaw = url.searchParams.get("status");
|
|
17180
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
17181
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
17182
|
+
const entries = await deps.aggregator.list({
|
|
17183
|
+
status,
|
|
17184
|
+
limit,
|
|
17185
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
17186
|
+
});
|
|
17187
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
17188
|
+
return true;
|
|
17189
|
+
}
|
|
17190
|
+
const entryMatch = matchEntryRoute(path);
|
|
17191
|
+
if (entryMatch === null) {
|
|
17192
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17193
|
+
return true;
|
|
17194
|
+
}
|
|
17195
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
17196
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
17197
|
+
const entry = entries.find(
|
|
17198
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
17199
|
+
);
|
|
17200
|
+
if (!entry) {
|
|
17201
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17202
|
+
return true;
|
|
17203
|
+
}
|
|
17204
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
17205
|
+
entryMatch.aggregatorId
|
|
17206
|
+
);
|
|
17207
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
17208
|
+
return true;
|
|
17209
|
+
}
|
|
17210
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
17211
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
17212
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17213
|
+
try {
|
|
17214
|
+
const entry = await deps.aggregator.resolve(
|
|
17215
|
+
entryMatch.aggregatorId,
|
|
17216
|
+
decision,
|
|
17217
|
+
operatorId
|
|
17218
|
+
);
|
|
17219
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
17220
|
+
} catch (err) {
|
|
17221
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17222
|
+
if (msg === "approval-aggregator: not_found") {
|
|
17223
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17224
|
+
} else {
|
|
17225
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17226
|
+
}
|
|
17227
|
+
}
|
|
17228
|
+
return true;
|
|
17229
|
+
}
|
|
17230
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17231
|
+
return true;
|
|
17232
|
+
} catch (err) {
|
|
17233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17234
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17235
|
+
return true;
|
|
17236
|
+
}
|
|
17237
|
+
}
|
|
17238
|
+
var APPROVAL_INBOX_API_PREFIX, APPROVAL_INBOX_OPERATOR_DEFAULT, APPROVAL_INBOX_DEFAULT_LIMIT, APPROVAL_INBOX_MAX_LIMIT;
|
|
17239
|
+
var init_approval_aggregator_routes = __esm({
|
|
17240
|
+
"src/principal-policy/approval-aggregator-routes.ts"() {
|
|
17241
|
+
init_auth_middleware();
|
|
17242
|
+
APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
17243
|
+
APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
17244
|
+
APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
17245
|
+
APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
17246
|
+
}
|
|
17247
|
+
});
|
|
16918
17248
|
function isDashboardViewRoute(method, path) {
|
|
16919
17249
|
if (method !== "GET") return false;
|
|
16920
17250
|
return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
|
|
@@ -16928,6 +17258,7 @@ var init_dashboard = __esm({
|
|
|
16928
17258
|
init_fortress_view();
|
|
16929
17259
|
init_system_prompt_generator();
|
|
16930
17260
|
init_dispatch();
|
|
17261
|
+
init_approval_aggregator_routes();
|
|
16931
17262
|
SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16932
17263
|
SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
16933
17264
|
MAX_SESSIONS = 1e3;
|
|
@@ -16987,6 +17318,14 @@ var init_dashboard = __esm({
|
|
|
16987
17318
|
* regardless. Default route flip is deferred to v1.2.
|
|
16988
17319
|
*/
|
|
16989
17320
|
v11Bindings = null;
|
|
17321
|
+
/**
|
|
17322
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
17323
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
17324
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
17325
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
17326
|
+
* the operator-facing query / decision surface.
|
|
17327
|
+
*/
|
|
17328
|
+
approvalAggregator = null;
|
|
16990
17329
|
constructor(config) {
|
|
16991
17330
|
this.config = config;
|
|
16992
17331
|
this.authToken = config.auth_token;
|
|
@@ -17037,6 +17376,34 @@ var init_dashboard = __esm({
|
|
|
17037
17376
|
setV11Bindings(bindings) {
|
|
17038
17377
|
this.v11Bindings = bindings;
|
|
17039
17378
|
}
|
|
17379
|
+
/**
|
|
17380
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
17381
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
17382
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
17383
|
+
* tests + during shutdown).
|
|
17384
|
+
*/
|
|
17385
|
+
setApprovalAggregator(aggregator) {
|
|
17386
|
+
this.approvalAggregator = aggregator;
|
|
17387
|
+
}
|
|
17388
|
+
/**
|
|
17389
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
17390
|
+
* before the legacy approval route table. Returns true when served.
|
|
17391
|
+
*/
|
|
17392
|
+
async dispatchApprovalInbox(req, res) {
|
|
17393
|
+
if (!this.approvalAggregator) return false;
|
|
17394
|
+
return handleApprovalInboxRoute(
|
|
17395
|
+
{
|
|
17396
|
+
authConfig: {
|
|
17397
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
17398
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
17399
|
+
},
|
|
17400
|
+
aggregator: this.approvalAggregator,
|
|
17401
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
17402
|
+
},
|
|
17403
|
+
req,
|
|
17404
|
+
res
|
|
17405
|
+
);
|
|
17406
|
+
}
|
|
17040
17407
|
/**
|
|
17041
17408
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
17042
17409
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -17102,7 +17469,7 @@ var init_dashboard = __esm({
|
|
|
17102
17469
|
server = http.createServer(handler);
|
|
17103
17470
|
}
|
|
17104
17471
|
this.httpServer = server;
|
|
17105
|
-
return new Promise((
|
|
17472
|
+
return new Promise((resolve8, reject) => {
|
|
17106
17473
|
const protocol = this.useTLS ? "https" : "http";
|
|
17107
17474
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
17108
17475
|
server.listen(this.config.port, this.config.host, () => {
|
|
@@ -17127,7 +17494,7 @@ var init_dashboard = __esm({
|
|
|
17127
17494
|
if (shouldAutoOpen) {
|
|
17128
17495
|
this.openInBrowser(sessionUrl);
|
|
17129
17496
|
}
|
|
17130
|
-
|
|
17497
|
+
resolve8();
|
|
17131
17498
|
});
|
|
17132
17499
|
server.on("error", (err) => {
|
|
17133
17500
|
if (err.code === "EADDRINUSE") {
|
|
@@ -17173,8 +17540,8 @@ var init_dashboard = __esm({
|
|
|
17173
17540
|
}
|
|
17174
17541
|
this.rateLimits.clear();
|
|
17175
17542
|
if (this.httpServer) {
|
|
17176
|
-
return new Promise((
|
|
17177
|
-
this.httpServer.close(() =>
|
|
17543
|
+
return new Promise((resolve8) => {
|
|
17544
|
+
this.httpServer.close(() => resolve8());
|
|
17178
17545
|
});
|
|
17179
17546
|
}
|
|
17180
17547
|
}
|
|
@@ -17188,7 +17555,7 @@ var init_dashboard = __esm({
|
|
|
17188
17555
|
`[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
|
|
17189
17556
|
`
|
|
17190
17557
|
);
|
|
17191
|
-
return new Promise((
|
|
17558
|
+
return new Promise((resolve8) => {
|
|
17192
17559
|
const timer = setTimeout(() => {
|
|
17193
17560
|
this.pending.delete(id);
|
|
17194
17561
|
const response = {
|
|
@@ -17202,12 +17569,12 @@ var init_dashboard = __esm({
|
|
|
17202
17569
|
decision: response.decision,
|
|
17203
17570
|
decided_by: "timeout"
|
|
17204
17571
|
});
|
|
17205
|
-
|
|
17572
|
+
resolve8(response);
|
|
17206
17573
|
}, this.config.timeout_seconds * 1e3);
|
|
17207
17574
|
const pending = {
|
|
17208
17575
|
id,
|
|
17209
17576
|
request,
|
|
17210
|
-
resolve:
|
|
17577
|
+
resolve: resolve8,
|
|
17211
17578
|
timer,
|
|
17212
17579
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
17213
17580
|
};
|
|
@@ -17412,6 +17779,18 @@ var init_dashboard = __esm({
|
|
|
17412
17779
|
res.end();
|
|
17413
17780
|
return;
|
|
17414
17781
|
}
|
|
17782
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
17783
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
17784
|
+
if (handled) return;
|
|
17785
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
17786
|
+
}).catch(() => {
|
|
17787
|
+
if (!res.headersSent) {
|
|
17788
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
17789
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
17790
|
+
}
|
|
17791
|
+
});
|
|
17792
|
+
return;
|
|
17793
|
+
}
|
|
17415
17794
|
if (this.v11Bindings) {
|
|
17416
17795
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
17417
17796
|
if (handled) return;
|
|
@@ -18073,7 +18452,7 @@ var init_webhook = __esm({
|
|
|
18073
18452
|
* Start the callback listener server.
|
|
18074
18453
|
*/
|
|
18075
18454
|
async start() {
|
|
18076
|
-
return new Promise((
|
|
18455
|
+
return new Promise((resolve8, reject) => {
|
|
18077
18456
|
this.callbackServer = http.createServer(
|
|
18078
18457
|
(req, res) => this.handleCallback(req, res)
|
|
18079
18458
|
);
|
|
@@ -18088,7 +18467,7 @@ var init_webhook = __esm({
|
|
|
18088
18467
|
|
|
18089
18468
|
`
|
|
18090
18469
|
);
|
|
18091
|
-
|
|
18470
|
+
resolve8();
|
|
18092
18471
|
}
|
|
18093
18472
|
);
|
|
18094
18473
|
this.callbackServer.on("error", reject);
|
|
@@ -18108,8 +18487,8 @@ var init_webhook = __esm({
|
|
|
18108
18487
|
}
|
|
18109
18488
|
this.pending.clear();
|
|
18110
18489
|
if (this.callbackServer) {
|
|
18111
|
-
return new Promise((
|
|
18112
|
-
this.callbackServer.close(() =>
|
|
18490
|
+
return new Promise((resolve8) => {
|
|
18491
|
+
this.callbackServer.close(() => resolve8());
|
|
18113
18492
|
});
|
|
18114
18493
|
}
|
|
18115
18494
|
}
|
|
@@ -18122,7 +18501,7 @@ var init_webhook = __esm({
|
|
|
18122
18501
|
`[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
|
|
18123
18502
|
`
|
|
18124
18503
|
);
|
|
18125
|
-
return new Promise((
|
|
18504
|
+
return new Promise((resolve8) => {
|
|
18126
18505
|
const timer = setTimeout(() => {
|
|
18127
18506
|
this.pending.delete(id);
|
|
18128
18507
|
const response = {
|
|
@@ -18131,12 +18510,12 @@ var init_webhook = __esm({
|
|
|
18131
18510
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18132
18511
|
decided_by: "timeout"
|
|
18133
18512
|
};
|
|
18134
|
-
|
|
18513
|
+
resolve8(response);
|
|
18135
18514
|
}, this.config.timeout_seconds * 1e3);
|
|
18136
18515
|
const pending = {
|
|
18137
18516
|
id,
|
|
18138
18517
|
request,
|
|
18139
|
-
resolve:
|
|
18518
|
+
resolve: resolve8,
|
|
18140
18519
|
timer,
|
|
18141
18520
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18142
18521
|
};
|
|
@@ -19429,12 +19808,25 @@ var init_injection_detector = __esm({
|
|
|
19429
19808
|
}
|
|
19430
19809
|
});
|
|
19431
19810
|
|
|
19811
|
+
// src/principal-policy/deny-vocabulary.ts
|
|
19812
|
+
var AGENT_VISIBLE_DENY_REASONS;
|
|
19813
|
+
var init_deny_vocabulary = __esm({
|
|
19814
|
+
"src/principal-policy/deny-vocabulary.ts"() {
|
|
19815
|
+
AGENT_VISIBLE_DENY_REASONS = {
|
|
19816
|
+
REQUIRES_APPROVAL: "operation requires operator approval",
|
|
19817
|
+
NOT_PERMITTED: "operation not permitted",
|
|
19818
|
+
REQUIRES_OPERATOR: "operation requires operator action"
|
|
19819
|
+
};
|
|
19820
|
+
}
|
|
19821
|
+
});
|
|
19822
|
+
|
|
19432
19823
|
// src/principal-policy/gate.ts
|
|
19433
19824
|
var ApprovalGate;
|
|
19434
19825
|
var init_gate = __esm({
|
|
19435
19826
|
"src/principal-policy/gate.ts"() {
|
|
19436
19827
|
init_loader();
|
|
19437
19828
|
init_injection_detector();
|
|
19829
|
+
init_deny_vocabulary();
|
|
19438
19830
|
ApprovalGate = class {
|
|
19439
19831
|
policy;
|
|
19440
19832
|
baseline;
|
|
@@ -19442,14 +19834,25 @@ var init_gate = __esm({
|
|
|
19442
19834
|
auditLog;
|
|
19443
19835
|
injectionDetector;
|
|
19444
19836
|
onInjectionAlert;
|
|
19837
|
+
onApprovalEvent;
|
|
19445
19838
|
proxyTierResolver;
|
|
19446
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
19839
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
19447
19840
|
this.policy = policy;
|
|
19448
19841
|
this.baseline = baseline;
|
|
19449
19842
|
this.channel = channel;
|
|
19450
19843
|
this.auditLog = auditLog;
|
|
19451
19844
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
19452
19845
|
this.onInjectionAlert = onInjectionAlert;
|
|
19846
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
19847
|
+
}
|
|
19848
|
+
/**
|
|
19849
|
+
* Set the approval-event callback after construction. Used by the
|
|
19850
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
19851
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
19852
|
+
* constructor so existing call sites continue to work unchanged.
|
|
19853
|
+
*/
|
|
19854
|
+
setApprovalEventCallback(cb) {
|
|
19855
|
+
this.onApprovalEvent = cb;
|
|
19453
19856
|
}
|
|
19454
19857
|
/**
|
|
19455
19858
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -19486,10 +19889,16 @@ var init_gate = __esm({
|
|
|
19486
19889
|
});
|
|
19487
19890
|
}
|
|
19488
19891
|
if (injectionResult.recommendation === "block") {
|
|
19892
|
+
this.auditLog.append("l2", `gate_injection_block:${operation}`, "system", {
|
|
19893
|
+
tier: 1,
|
|
19894
|
+
operation,
|
|
19895
|
+
injection_confidence: injectionResult.confidence,
|
|
19896
|
+
signal_count: injectionResult.signals.length
|
|
19897
|
+
});
|
|
19489
19898
|
return {
|
|
19490
19899
|
allowed: false,
|
|
19491
19900
|
tier: 1,
|
|
19492
|
-
reason:
|
|
19901
|
+
reason: AGENT_VISIBLE_DENY_REASONS.NOT_PERMITTED,
|
|
19493
19902
|
approval_required: false
|
|
19494
19903
|
};
|
|
19495
19904
|
}
|
|
@@ -19566,7 +19975,7 @@ var init_gate = __esm({
|
|
|
19566
19975
|
this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
|
|
19567
19976
|
tier: 1,
|
|
19568
19977
|
operation,
|
|
19569
|
-
warning: "Operation is not classified in any policy tier
|
|
19978
|
+
warning: "Operation is not classified in any policy tier, defaulting to Tier 1 (require approval)"
|
|
19570
19979
|
});
|
|
19571
19980
|
return this.requestApproval(
|
|
19572
19981
|
operation,
|
|
@@ -19677,25 +20086,109 @@ var init_gate = __esm({
|
|
|
19677
20086
|
}
|
|
19678
20087
|
/**
|
|
19679
20088
|
* Request approval from the human principal.
|
|
20089
|
+
*
|
|
20090
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
20091
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
20092
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
20093
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
20094
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
20095
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
19680
20096
|
*/
|
|
19681
20097
|
async requestApproval(operation, tier, reason, context) {
|
|
20098
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19682
20099
|
const request = {
|
|
19683
20100
|
operation,
|
|
19684
20101
|
tier,
|
|
19685
20102
|
reason,
|
|
19686
20103
|
context,
|
|
19687
|
-
timestamp:
|
|
20104
|
+
timestamp: requestTimestamp
|
|
19688
20105
|
};
|
|
19689
|
-
const
|
|
20106
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
20107
|
+
if (this.onApprovalEvent) {
|
|
20108
|
+
try {
|
|
20109
|
+
this.onApprovalEvent({
|
|
20110
|
+
phase: "requested",
|
|
20111
|
+
operation,
|
|
20112
|
+
tier,
|
|
20113
|
+
reason,
|
|
20114
|
+
context,
|
|
20115
|
+
request_timestamp: requestTimestamp,
|
|
20116
|
+
correlation_id: correlationId
|
|
20117
|
+
});
|
|
20118
|
+
} catch {
|
|
20119
|
+
}
|
|
20120
|
+
}
|
|
20121
|
+
let response;
|
|
20122
|
+
try {
|
|
20123
|
+
response = await this.channel.requestApproval(request);
|
|
20124
|
+
} catch (err) {
|
|
20125
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
20126
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20127
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
20128
|
+
tier,
|
|
20129
|
+
reason,
|
|
20130
|
+
decided_by: "channel_failure",
|
|
20131
|
+
channel_error: errMessage
|
|
20132
|
+
});
|
|
20133
|
+
if (this.onApprovalEvent) {
|
|
20134
|
+
try {
|
|
20135
|
+
this.onApprovalEvent({
|
|
20136
|
+
phase: "resolved",
|
|
20137
|
+
operation,
|
|
20138
|
+
tier,
|
|
20139
|
+
reason,
|
|
20140
|
+
context,
|
|
20141
|
+
request_timestamp: requestTimestamp,
|
|
20142
|
+
resolution: {
|
|
20143
|
+
decision: "deny",
|
|
20144
|
+
decided_at: decidedAt,
|
|
20145
|
+
decided_by: "channel_failure"
|
|
20146
|
+
},
|
|
20147
|
+
correlation_id: correlationId
|
|
20148
|
+
});
|
|
20149
|
+
} catch {
|
|
20150
|
+
}
|
|
20151
|
+
}
|
|
20152
|
+
return {
|
|
20153
|
+
allowed: false,
|
|
20154
|
+
tier,
|
|
20155
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
20156
|
+
approval_required: true,
|
|
20157
|
+
approval_response: {
|
|
20158
|
+
decision: "deny",
|
|
20159
|
+
decided_at: decidedAt,
|
|
20160
|
+
decided_by: "channel_failure"
|
|
20161
|
+
}
|
|
20162
|
+
};
|
|
20163
|
+
}
|
|
19690
20164
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
19691
20165
|
tier,
|
|
19692
20166
|
reason,
|
|
19693
20167
|
decided_by: response.decided_by
|
|
19694
20168
|
});
|
|
20169
|
+
if (this.onApprovalEvent) {
|
|
20170
|
+
try {
|
|
20171
|
+
this.onApprovalEvent({
|
|
20172
|
+
phase: "resolved",
|
|
20173
|
+
operation,
|
|
20174
|
+
tier,
|
|
20175
|
+
reason,
|
|
20176
|
+
context,
|
|
20177
|
+
request_timestamp: requestTimestamp,
|
|
20178
|
+
resolution: {
|
|
20179
|
+
decision: response.decision,
|
|
20180
|
+
decided_at: response.decided_at,
|
|
20181
|
+
decided_by: response.decided_by
|
|
20182
|
+
},
|
|
20183
|
+
correlation_id: correlationId
|
|
20184
|
+
});
|
|
20185
|
+
} catch {
|
|
20186
|
+
}
|
|
20187
|
+
}
|
|
19695
20188
|
return {
|
|
19696
20189
|
allowed: response.decision === "approve",
|
|
19697
20190
|
tier,
|
|
19698
|
-
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` :
|
|
20191
|
+
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
19699
20192
|
approval_required: true,
|
|
19700
20193
|
approval_response: response
|
|
19701
20194
|
};
|
|
@@ -19726,6 +20219,356 @@ var init_gate = __esm({
|
|
|
19726
20219
|
};
|
|
19727
20220
|
}
|
|
19728
20221
|
});
|
|
20222
|
+
var APPROVAL_AGGREGATOR_NAMESPACE, APPROVAL_AGGREGATOR_HKDF_INFO, APPROVAL_AGGREGATOR_AUDIT_OPS, DEFAULT_PENDING_TTL_MS, DEFAULT_MAX_LIST_LIMIT, DEFAULT_LIST_PAGE_SIZE, ApprovalAggregator;
|
|
20223
|
+
var init_approval_aggregator = __esm({
|
|
20224
|
+
"src/principal-policy/approval-aggregator.ts"() {
|
|
20225
|
+
init_encryption();
|
|
20226
|
+
init_key_derivation();
|
|
20227
|
+
init_encoding();
|
|
20228
|
+
APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
20229
|
+
APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
20230
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
20231
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
20232
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
20233
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
20234
|
+
};
|
|
20235
|
+
DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
20236
|
+
DEFAULT_MAX_LIST_LIMIT = 200;
|
|
20237
|
+
DEFAULT_LIST_PAGE_SIZE = 50;
|
|
20238
|
+
ApprovalAggregator = class {
|
|
20239
|
+
storage;
|
|
20240
|
+
encryptionKey;
|
|
20241
|
+
auditLog;
|
|
20242
|
+
identityId;
|
|
20243
|
+
fortressId;
|
|
20244
|
+
pendingTtlMs;
|
|
20245
|
+
maxListLimit;
|
|
20246
|
+
now;
|
|
20247
|
+
resolveSourceContext;
|
|
20248
|
+
resolveHubInboxItemId;
|
|
20249
|
+
/** Cached entries by `aggregator_id`. */
|
|
20250
|
+
entries = /* @__PURE__ */ new Map();
|
|
20251
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
20252
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
20253
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
20254
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
20255
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
20256
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
20257
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
20258
|
+
hydrated = false;
|
|
20259
|
+
/** Active SSE listeners. */
|
|
20260
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20261
|
+
constructor(deps) {
|
|
20262
|
+
this.storage = deps.storage;
|
|
20263
|
+
this.encryptionKey = derivePurposeKey(
|
|
20264
|
+
deps.masterKey,
|
|
20265
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
20266
|
+
);
|
|
20267
|
+
this.auditLog = deps.auditLog;
|
|
20268
|
+
this.identityId = deps.identityId;
|
|
20269
|
+
this.fortressId = deps.fortressId;
|
|
20270
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
20271
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
20272
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
20273
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
20274
|
+
source_harness: this.fortressId,
|
|
20275
|
+
source_agent_id: this.fortressId
|
|
20276
|
+
}));
|
|
20277
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
20278
|
+
}
|
|
20279
|
+
/**
|
|
20280
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
20281
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
20282
|
+
*/
|
|
20283
|
+
onEvent(listener) {
|
|
20284
|
+
this.listeners.add(listener);
|
|
20285
|
+
return () => this.listeners.delete(listener);
|
|
20286
|
+
}
|
|
20287
|
+
/**
|
|
20288
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
20289
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
20290
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
20291
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
20292
|
+
* a record for).
|
|
20293
|
+
*/
|
|
20294
|
+
async ingest(event) {
|
|
20295
|
+
await this.hydrate();
|
|
20296
|
+
if (event.phase === "requested") {
|
|
20297
|
+
return this.ingestRequested(event);
|
|
20298
|
+
}
|
|
20299
|
+
if (event.phase === "resolved") {
|
|
20300
|
+
return this.ingestResolved(event);
|
|
20301
|
+
}
|
|
20302
|
+
return null;
|
|
20303
|
+
}
|
|
20304
|
+
/**
|
|
20305
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
20306
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
20307
|
+
* snapshot is returned.
|
|
20308
|
+
*/
|
|
20309
|
+
async list(opts) {
|
|
20310
|
+
await this.hydrate();
|
|
20311
|
+
await this.expireStale();
|
|
20312
|
+
const limit = Math.min(
|
|
20313
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
20314
|
+
this.maxListLimit
|
|
20315
|
+
);
|
|
20316
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
20317
|
+
const matching = [];
|
|
20318
|
+
for (const entry of this.entries.values()) {
|
|
20319
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
20320
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
20321
|
+
matching.push(entry);
|
|
20322
|
+
}
|
|
20323
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
20324
|
+
return matching.slice(0, limit);
|
|
20325
|
+
}
|
|
20326
|
+
/**
|
|
20327
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
20328
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
20329
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
20330
|
+
*/
|
|
20331
|
+
async getFullPayload(aggregatorId) {
|
|
20332
|
+
await this.hydrate();
|
|
20333
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
20334
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
20335
|
+
}
|
|
20336
|
+
/**
|
|
20337
|
+
* Resolve an entry. Used by both:
|
|
20338
|
+
* 1. The gate wire-up on channel-decision return.
|
|
20339
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
20340
|
+
*
|
|
20341
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
20342
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
20343
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
20344
|
+
* routes return 404.
|
|
20345
|
+
*/
|
|
20346
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
20347
|
+
await this.hydrate();
|
|
20348
|
+
const entry = this.entries.get(aggregatorId);
|
|
20349
|
+
if (!entry) {
|
|
20350
|
+
throw new Error("approval-aggregator: not_found");
|
|
20351
|
+
}
|
|
20352
|
+
if (entry.status !== "pending") {
|
|
20353
|
+
return entry;
|
|
20354
|
+
}
|
|
20355
|
+
entry.status = decision;
|
|
20356
|
+
entry.resolved_at = this.now().toISOString();
|
|
20357
|
+
entry.resolved_by = operatorId;
|
|
20358
|
+
await this.persist(entry);
|
|
20359
|
+
this.auditLog.append(
|
|
20360
|
+
"l2",
|
|
20361
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20362
|
+
this.identityId,
|
|
20363
|
+
{
|
|
20364
|
+
aggregator_id: entry.aggregator_id,
|
|
20365
|
+
source_harness: entry.source_harness,
|
|
20366
|
+
source_agent_id: entry.source_agent_id,
|
|
20367
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20368
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20369
|
+
decision,
|
|
20370
|
+
decided_by: operatorId,
|
|
20371
|
+
decided_at: entry.resolved_at
|
|
20372
|
+
}
|
|
20373
|
+
);
|
|
20374
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20375
|
+
return entry;
|
|
20376
|
+
}
|
|
20377
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
20378
|
+
async ingestRequested(event) {
|
|
20379
|
+
const ctx = this.resolveSourceContext(event);
|
|
20380
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
20381
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
20382
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
20383
|
+
if (existing) {
|
|
20384
|
+
const existingEntry = this.entries.get(existing);
|
|
20385
|
+
if (existingEntry) {
|
|
20386
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
20387
|
+
this.auditLog.append(
|
|
20388
|
+
"l2",
|
|
20389
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
20390
|
+
this.identityId,
|
|
20391
|
+
{
|
|
20392
|
+
aggregator_id: existing,
|
|
20393
|
+
source_harness: ctx.source_harness,
|
|
20394
|
+
source_agent_id: ctx.source_agent_id,
|
|
20395
|
+
audit_log_entry_id: auditId,
|
|
20396
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20397
|
+
correlation_id: event.correlation_id
|
|
20398
|
+
}
|
|
20399
|
+
);
|
|
20400
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
20401
|
+
return null;
|
|
20402
|
+
}
|
|
20403
|
+
}
|
|
20404
|
+
const id = crypto.randomUUID();
|
|
20405
|
+
const now = this.now();
|
|
20406
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
20407
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
20408
|
+
const entry = {
|
|
20409
|
+
aggregator_id: id,
|
|
20410
|
+
source_harness: ctx.source_harness,
|
|
20411
|
+
source_agent_id: ctx.source_agent_id,
|
|
20412
|
+
audit_log_entry_id: auditId,
|
|
20413
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20414
|
+
action_summary: this.deriveActionSummary(event),
|
|
20415
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
20416
|
+
status: "pending",
|
|
20417
|
+
created_at: now.toISOString(),
|
|
20418
|
+
expires_at: expires.toISOString(),
|
|
20419
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20420
|
+
};
|
|
20421
|
+
this.entries.set(id, entry);
|
|
20422
|
+
this.dedupIndex.set(dedupKey, id);
|
|
20423
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
20424
|
+
this.fullPayloads.set(id, event.context);
|
|
20425
|
+
await this.persist(entry);
|
|
20426
|
+
this.auditLog.append(
|
|
20427
|
+
"l2",
|
|
20428
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
20429
|
+
this.identityId,
|
|
20430
|
+
{
|
|
20431
|
+
aggregator_id: id,
|
|
20432
|
+
source_harness: ctx.source_harness,
|
|
20433
|
+
source_agent_id: ctx.source_agent_id,
|
|
20434
|
+
audit_log_entry_id: auditId,
|
|
20435
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20436
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20437
|
+
}
|
|
20438
|
+
);
|
|
20439
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
20440
|
+
return entry;
|
|
20441
|
+
}
|
|
20442
|
+
async ingestResolved(event) {
|
|
20443
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
20444
|
+
if (!id) return null;
|
|
20445
|
+
const entry = this.entries.get(id);
|
|
20446
|
+
if (!entry) return null;
|
|
20447
|
+
if (entry.status !== "pending") return entry;
|
|
20448
|
+
if (!event.resolution) return entry;
|
|
20449
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
20450
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
20451
|
+
entry.status = status;
|
|
20452
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
20453
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
20454
|
+
await this.persist(entry);
|
|
20455
|
+
this.auditLog.append(
|
|
20456
|
+
"l2",
|
|
20457
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20458
|
+
this.identityId,
|
|
20459
|
+
{
|
|
20460
|
+
aggregator_id: id,
|
|
20461
|
+
source_harness: entry.source_harness,
|
|
20462
|
+
source_agent_id: entry.source_agent_id,
|
|
20463
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20464
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20465
|
+
decision: status,
|
|
20466
|
+
decided_by: entry.resolved_by,
|
|
20467
|
+
decided_at: entry.resolved_at,
|
|
20468
|
+
fail_closed: failClosed
|
|
20469
|
+
}
|
|
20470
|
+
);
|
|
20471
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20472
|
+
return entry;
|
|
20473
|
+
}
|
|
20474
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
20475
|
+
/**
|
|
20476
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
20477
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
20478
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
20479
|
+
* the audit entry the gate appended on the same call.
|
|
20480
|
+
*/
|
|
20481
|
+
auditEntryIdForEvent(event) {
|
|
20482
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
20483
|
+
}
|
|
20484
|
+
derivePolicyRuleId(event) {
|
|
20485
|
+
return `tier${event.tier}:${event.operation}`;
|
|
20486
|
+
}
|
|
20487
|
+
deriveActionSummary(event) {
|
|
20488
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
20489
|
+
}
|
|
20490
|
+
/**
|
|
20491
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
20492
|
+
* identical payloads always hash the same, even when key insertion order
|
|
20493
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
20494
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
20495
|
+
*/
|
|
20496
|
+
hashPayload(payload) {
|
|
20497
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
20498
|
+
return crypto.createHash("sha256").update(canonical).digest("hex");
|
|
20499
|
+
}
|
|
20500
|
+
emit(event) {
|
|
20501
|
+
for (const listener of this.listeners) {
|
|
20502
|
+
try {
|
|
20503
|
+
listener(event);
|
|
20504
|
+
} catch {
|
|
20505
|
+
}
|
|
20506
|
+
}
|
|
20507
|
+
}
|
|
20508
|
+
async expireStale() {
|
|
20509
|
+
const nowMs = this.now().getTime();
|
|
20510
|
+
for (const entry of this.entries.values()) {
|
|
20511
|
+
if (entry.status !== "pending") continue;
|
|
20512
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
20513
|
+
entry.status = "expired";
|
|
20514
|
+
entry.resolved_at = this.now().toISOString();
|
|
20515
|
+
entry.resolved_by = "system_ttl";
|
|
20516
|
+
await this.persist(entry);
|
|
20517
|
+
this.auditLog.append(
|
|
20518
|
+
"l2",
|
|
20519
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20520
|
+
this.identityId,
|
|
20521
|
+
{
|
|
20522
|
+
aggregator_id: entry.aggregator_id,
|
|
20523
|
+
source_harness: entry.source_harness,
|
|
20524
|
+
source_agent_id: entry.source_agent_id,
|
|
20525
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20526
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20527
|
+
decision: "expired",
|
|
20528
|
+
decided_by: "system_ttl",
|
|
20529
|
+
decided_at: entry.resolved_at
|
|
20530
|
+
}
|
|
20531
|
+
);
|
|
20532
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20533
|
+
}
|
|
20534
|
+
}
|
|
20535
|
+
async persist(entry) {
|
|
20536
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
20537
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
20538
|
+
await this.storage.write(
|
|
20539
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20540
|
+
entry.aggregator_id,
|
|
20541
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
20542
|
+
);
|
|
20543
|
+
}
|
|
20544
|
+
async hydrate() {
|
|
20545
|
+
if (this.hydrated) return;
|
|
20546
|
+
this.hydrated = true;
|
|
20547
|
+
try {
|
|
20548
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
20549
|
+
for (const meta of metas) {
|
|
20550
|
+
const raw = await this.storage.read(
|
|
20551
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20552
|
+
meta.key
|
|
20553
|
+
);
|
|
20554
|
+
if (!raw) continue;
|
|
20555
|
+
try {
|
|
20556
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
20557
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
20558
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
20559
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
20560
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
20561
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
20562
|
+
} catch {
|
|
20563
|
+
}
|
|
20564
|
+
}
|
|
20565
|
+
} catch {
|
|
20566
|
+
this.hydrated = false;
|
|
20567
|
+
}
|
|
20568
|
+
}
|
|
20569
|
+
};
|
|
20570
|
+
}
|
|
20571
|
+
});
|
|
19729
20572
|
|
|
19730
20573
|
// src/principal-policy/tools.ts
|
|
19731
20574
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
@@ -20284,7 +21127,11 @@ var init_tools5 = __esm({
|
|
|
20284
21127
|
|
|
20285
21128
|
// src/handshake/protocol.ts
|
|
20286
21129
|
function generateNonce() {
|
|
20287
|
-
|
|
21130
|
+
const nonce = randomBytes(32);
|
|
21131
|
+
if (!nonce || nonce.length !== 32) {
|
|
21132
|
+
throw new Error("Nonce generation failed: randomBytes returned unexpected length");
|
|
21133
|
+
}
|
|
21134
|
+
return toBase64url(nonce);
|
|
20288
21135
|
}
|
|
20289
21136
|
function initiateHandshake(ourSHR) {
|
|
20290
21137
|
const nonce = generateNonce();
|
|
@@ -20395,6 +21242,18 @@ function completeHandshake(response, session, identityManager, masterKey, identi
|
|
|
20395
21242
|
return { completion, result };
|
|
20396
21243
|
}
|
|
20397
21244
|
function verifyCompletion(completion, session) {
|
|
21245
|
+
if (completion.protocol_version !== "1.0") {
|
|
21246
|
+
return {
|
|
21247
|
+
counterparty_id: "unknown",
|
|
21248
|
+
counterparty_shr: session.our_shr,
|
|
21249
|
+
verified: false,
|
|
21250
|
+
sovereignty_level: "unverified",
|
|
21251
|
+
trust_tier: "unverified",
|
|
21252
|
+
completed_at: completion.completed_at,
|
|
21253
|
+
expires_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21254
|
+
errors: [`Unsupported protocol version: ${completion.protocol_version}`]
|
|
21255
|
+
};
|
|
21256
|
+
}
|
|
20398
21257
|
const errors = [];
|
|
20399
21258
|
if (!session.their_shr) {
|
|
20400
21259
|
return {
|
|
@@ -22627,6 +23486,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
22627
23486
|
}
|
|
22628
23487
|
async function consumeResetHistoryMarker(options) {
|
|
22629
23488
|
const markerPath = path.join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
23489
|
+
const consumedPath = markerPath + ".consumed";
|
|
23490
|
+
if (await fileExists3(consumedPath)) {
|
|
23491
|
+
await promises.rm(markerPath, { force: true });
|
|
23492
|
+
await promises.rm(consumedPath, { force: true });
|
|
23493
|
+
return { emitted: 0, markerPath };
|
|
23494
|
+
}
|
|
22630
23495
|
if (!await fileExists3(markerPath)) {
|
|
22631
23496
|
return { emitted: 0, markerPath };
|
|
22632
23497
|
}
|
|
@@ -22651,7 +23516,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
22651
23516
|
});
|
|
22652
23517
|
}
|
|
22653
23518
|
await options.auditLog.flush();
|
|
23519
|
+
await promises.writeFile(consumedPath, "", "utf-8");
|
|
22654
23520
|
await promises.rm(markerPath, { force: true });
|
|
23521
|
+
await promises.rm(consumedPath, { force: true });
|
|
22655
23522
|
return { emitted: markers.length, markerHash, markerPath };
|
|
22656
23523
|
}
|
|
22657
23524
|
async function fileExists3(path) {
|
|
@@ -22681,6 +23548,9 @@ Inspect the file and either correct the JSON or delete it manually before re-run
|
|
|
22681
23548
|
this.cause = cause;
|
|
22682
23549
|
this.name = "ResetHistoryMalformedError";
|
|
22683
23550
|
}
|
|
23551
|
+
markerPath;
|
|
23552
|
+
lineNumber;
|
|
23553
|
+
cause;
|
|
22684
23554
|
};
|
|
22685
23555
|
}
|
|
22686
23556
|
});
|
|
@@ -24839,7 +25709,7 @@ async function runOpenAIPrivacyFilter(text, config) {
|
|
|
24839
25709
|
return parsed;
|
|
24840
25710
|
}
|
|
24841
25711
|
function runCommand(command, input, timeoutMs) {
|
|
24842
|
-
return new Promise((
|
|
25712
|
+
return new Promise((resolve8, reject) => {
|
|
24843
25713
|
const child = child_process.spawn(command, [], {
|
|
24844
25714
|
stdio: ["pipe", "pipe", "pipe"],
|
|
24845
25715
|
shell: false
|
|
@@ -24870,7 +25740,7 @@ function runCommand(command, input, timeoutMs) {
|
|
|
24870
25740
|
));
|
|
24871
25741
|
return;
|
|
24872
25742
|
}
|
|
24873
|
-
|
|
25743
|
+
resolve8(stdout);
|
|
24874
25744
|
});
|
|
24875
25745
|
child.stdin.end(input);
|
|
24876
25746
|
});
|
|
@@ -26746,13 +27616,13 @@ var init_proxy_router = __esm({
|
|
|
26746
27616
|
* Call an upstream tool with a timeout.
|
|
26747
27617
|
*/
|
|
26748
27618
|
async callWithTimeout(serverName, toolName, args, timeoutMs) {
|
|
26749
|
-
return new Promise((
|
|
27619
|
+
return new Promise((resolve8, reject) => {
|
|
26750
27620
|
const timer = setTimeout(() => {
|
|
26751
27621
|
reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
|
|
26752
27622
|
}, timeoutMs);
|
|
26753
27623
|
this.clientManager.callTool(serverName, toolName, args).then((result) => {
|
|
26754
27624
|
clearTimeout(timer);
|
|
26755
|
-
|
|
27625
|
+
resolve8(result);
|
|
26756
27626
|
}).catch((err) => {
|
|
26757
27627
|
clearTimeout(timer);
|
|
26758
27628
|
reject(err);
|
|
@@ -31931,6 +32801,36 @@ var init_hub_service = __esm({
|
|
|
31931
32801
|
const chat = this.requireOperatorChat();
|
|
31932
32802
|
return chat.getConciergeHistory();
|
|
31933
32803
|
}
|
|
32804
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
32805
|
+
/**
|
|
32806
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
32807
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
32808
|
+
* surface is unavailable on a given fortress.
|
|
32809
|
+
*/
|
|
32810
|
+
hasConciergeMemory() {
|
|
32811
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
32812
|
+
}
|
|
32813
|
+
async listConciergeMemoryThreads(opts) {
|
|
32814
|
+
const chat = this.requireOperatorChat();
|
|
32815
|
+
if (!chat.hasConciergeMemory()) {
|
|
32816
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32817
|
+
}
|
|
32818
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
32819
|
+
}
|
|
32820
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
32821
|
+
const chat = this.requireOperatorChat();
|
|
32822
|
+
if (!chat.hasConciergeMemory()) {
|
|
32823
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32824
|
+
}
|
|
32825
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
32826
|
+
}
|
|
32827
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
32828
|
+
const chat = this.requireOperatorChat();
|
|
32829
|
+
if (!chat.hasConciergeMemory()) {
|
|
32830
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32831
|
+
}
|
|
32832
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
32833
|
+
}
|
|
31934
32834
|
/**
|
|
31935
32835
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
31936
32836
|
* panel surfaces recent activity routed through this agent, pending
|
|
@@ -32069,7 +32969,20 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32069
32969
|
* affordance now opens an inspect/approve panel (recent activity +
|
|
32070
32970
|
* pending approvals + policy summary) instead of a chat session.
|
|
32071
32971
|
*/
|
|
32072
|
-
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened"
|
|
32972
|
+
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
|
|
32973
|
+
/**
|
|
32974
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
32975
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
32976
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
32977
|
+
* raw turn content never crosses the audit surface.
|
|
32978
|
+
*/
|
|
32979
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
32980
|
+
/**
|
|
32981
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
32982
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
32983
|
+
* the deleted bundle.
|
|
32984
|
+
*/
|
|
32985
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
32073
32986
|
};
|
|
32074
32987
|
}
|
|
32075
32988
|
});
|
|
@@ -32088,7 +33001,7 @@ function makeEventId(prefix) {
|
|
|
32088
33001
|
function hashOf(input) {
|
|
32089
33002
|
return hashToString(sha256.sha256(stringToBytes(input)));
|
|
32090
33003
|
}
|
|
32091
|
-
var DEFAULT_CONCIERGE_MAX_TOKENS, OperatorChatService;
|
|
33004
|
+
var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
32092
33005
|
var init_operator_chat_service = __esm({
|
|
32093
33006
|
"src/chat/operator-chat-service.ts"() {
|
|
32094
33007
|
init_hashing();
|
|
@@ -32096,6 +33009,33 @@ var init_operator_chat_service = __esm({
|
|
|
32096
33009
|
init_operator_chat_audit_events();
|
|
32097
33010
|
init_operator_chat_types();
|
|
32098
33011
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
33012
|
+
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
33013
|
+
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
33014
|
+
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
33015
|
+
3. Charter (Cooperative MCP): the sovereignty surface for compliant agents. Policy gates, approval tiers, audit logging, and encrypted state all live here.
|
|
33016
|
+
4. Heralds: Concordia receipts and Verascore reputation. Cross-fortress accountability after an action completes.
|
|
33017
|
+
|
|
33018
|
+
Five channel templates (canonical names):
|
|
33019
|
+
- request-approve-act: agent proposes an action, operator approves or denies before execution.
|
|
33020
|
+
- read-then-report: agent reads outputs from a data source and reports summaries to the operator.
|
|
33021
|
+
- scheduled-digest: agent runs on a schedule and delivers a periodic digest.
|
|
33022
|
+
- plan-draft-only: agent drafts plans; operator reviews before any execution step.
|
|
33023
|
+
- fortress-relay: agent relays messages between fortresses under operator-scoped policy.
|
|
33024
|
+
|
|
33025
|
+
Four canonical policy slots:
|
|
33026
|
+
- memory: governs what the agent may persist and retrieve from encrypted state.
|
|
33027
|
+
- credentials: governs access to secrets, API keys, and tokens held in the broker.
|
|
33028
|
+
- plans: governs the agent's ability to create, modify, or execute plans.
|
|
33029
|
+
- outputs: governs what the agent may emit to external surfaces (files, APIs, messages).
|
|
33030
|
+
|
|
33031
|
+
Key concepts:
|
|
33032
|
+
- Fortress: the operator-owned sovereignty harness. All state is encrypted at rest under the cocoon.
|
|
33033
|
+
- Cocoon: master-key-wrapped storage derived from the operator's passphrase via Argon2id.
|
|
33034
|
+
- Identity: Ed25519 keypair with a DID, owned by the operator. Private keys never leave the cocoon.
|
|
33035
|
+
- Audit log: append-only encrypted blobs, sequential, recording every gate decision and tool call.
|
|
33036
|
+
- Wrapped agent: any agent runtime that connects to Sanctuary as an MCP client. Tier A (native), Tier B (adapter-wrapped), Tier C (escape hatch).
|
|
33037
|
+
|
|
33038
|
+
Note: this is a static reference block (v1.2.x). Dynamic context injection (live template list, policy schema) ships in v1.3.`;
|
|
32099
33039
|
OperatorChatService = class {
|
|
32100
33040
|
store;
|
|
32101
33041
|
auditLog;
|
|
@@ -32104,6 +33044,14 @@ var init_operator_chat_service = __esm({
|
|
|
32104
33044
|
contextProviders;
|
|
32105
33045
|
piiFilter;
|
|
32106
33046
|
conciergeMaxTokens;
|
|
33047
|
+
memory;
|
|
33048
|
+
/**
|
|
33049
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
33050
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
33051
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
33052
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33053
|
+
*/
|
|
33054
|
+
activeMemoryThreadId;
|
|
32107
33055
|
constructor(deps) {
|
|
32108
33056
|
this.store = deps.store;
|
|
32109
33057
|
this.auditLog = deps.auditLog;
|
|
@@ -32114,6 +33062,7 @@ var init_operator_chat_service = __esm({
|
|
|
32114
33062
|
}
|
|
32115
33063
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
32116
33064
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33065
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32117
33066
|
}
|
|
32118
33067
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
32119
33068
|
/**
|
|
@@ -32144,6 +33093,11 @@ var init_operator_chat_service = __esm({
|
|
|
32144
33093
|
CONCIERGE_THREAD_KEY,
|
|
32145
33094
|
operatorMessage
|
|
32146
33095
|
);
|
|
33096
|
+
if (this.memory) {
|
|
33097
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33098
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
33099
|
+
});
|
|
33100
|
+
}
|
|
32147
33101
|
const start = Date.now();
|
|
32148
33102
|
let conciergeBody;
|
|
32149
33103
|
let servedBy = "disabled";
|
|
@@ -32199,6 +33153,11 @@ var init_operator_chat_service = __esm({
|
|
|
32199
33153
|
CONCIERGE_THREAD_KEY,
|
|
32200
33154
|
responseMessage
|
|
32201
33155
|
);
|
|
33156
|
+
if (this.memory) {
|
|
33157
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33158
|
+
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
|
|
33159
|
+
});
|
|
33160
|
+
}
|
|
32202
33161
|
const payload = {
|
|
32203
33162
|
version: "1.2",
|
|
32204
33163
|
event_id: makeEventId("conc"),
|
|
@@ -32231,6 +33190,105 @@ var init_operator_chat_service = __esm({
|
|
|
32231
33190
|
);
|
|
32232
33191
|
return thread ? thread.messages : [];
|
|
32233
33192
|
}
|
|
33193
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
33194
|
+
/**
|
|
33195
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
33196
|
+
* 503 cleanly when called against an unwired service.
|
|
33197
|
+
*/
|
|
33198
|
+
hasConciergeMemory() {
|
|
33199
|
+
return this.memory !== void 0;
|
|
33200
|
+
}
|
|
33201
|
+
/**
|
|
33202
|
+
* List concierge memory threads, newest-first. Emits the
|
|
33203
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
33204
|
+
*/
|
|
33205
|
+
async listConciergeMemoryThreads(opts) {
|
|
33206
|
+
if (!this.memory) {
|
|
33207
|
+
throw new Error("concierge memory store not configured");
|
|
33208
|
+
}
|
|
33209
|
+
const summaries = await this.memory.listThreads(opts);
|
|
33210
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
33211
|
+
const payload = {
|
|
33212
|
+
version: "1.2",
|
|
33213
|
+
event_id: makeEventId("conc-hist"),
|
|
33214
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33215
|
+
identity_id: this.identityId,
|
|
33216
|
+
kind: "operator_concierge_history_read",
|
|
33217
|
+
surface: "concierge",
|
|
33218
|
+
thread_id: "*",
|
|
33219
|
+
turn_count: totalTurns
|
|
33220
|
+
};
|
|
33221
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33222
|
+
return summaries;
|
|
33223
|
+
}
|
|
33224
|
+
/**
|
|
33225
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
33226
|
+
* `operator_concierge_history_read` audit event with the named
|
|
33227
|
+
* thread_id and the count of turns surfaced.
|
|
33228
|
+
*/
|
|
33229
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
33230
|
+
if (!this.memory) {
|
|
33231
|
+
throw new Error("concierge memory store not configured");
|
|
33232
|
+
}
|
|
33233
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
33234
|
+
const payload = {
|
|
33235
|
+
version: "1.2",
|
|
33236
|
+
event_id: makeEventId("conc-hist"),
|
|
33237
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33238
|
+
identity_id: this.identityId,
|
|
33239
|
+
kind: "operator_concierge_history_read",
|
|
33240
|
+
surface: "concierge",
|
|
33241
|
+
thread_id: threadId,
|
|
33242
|
+
turn_count: turns.length
|
|
33243
|
+
};
|
|
33244
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33245
|
+
return turns;
|
|
33246
|
+
}
|
|
33247
|
+
/**
|
|
33248
|
+
* Delete a concierge memory thread. Emits
|
|
33249
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
33250
|
+
* removed; absent threads return false without an audit event.
|
|
33251
|
+
*/
|
|
33252
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
33253
|
+
if (!this.memory) {
|
|
33254
|
+
throw new Error("concierge memory store not configured");
|
|
33255
|
+
}
|
|
33256
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
33257
|
+
if (turnsBefore.length === 0) {
|
|
33258
|
+
return await this.memory.deleteThread(threadId);
|
|
33259
|
+
}
|
|
33260
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
33261
|
+
if (!removed) return false;
|
|
33262
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
33263
|
+
this.activeMemoryThreadId = void 0;
|
|
33264
|
+
}
|
|
33265
|
+
const payload = {
|
|
33266
|
+
version: "1.2",
|
|
33267
|
+
event_id: makeEventId("conc-del"),
|
|
33268
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33269
|
+
identity_id: this.identityId,
|
|
33270
|
+
kind: "operator_concierge_thread_deleted",
|
|
33271
|
+
surface: "concierge",
|
|
33272
|
+
thread_id: threadId,
|
|
33273
|
+
turn_count: turnsBefore.length
|
|
33274
|
+
};
|
|
33275
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
33276
|
+
return true;
|
|
33277
|
+
}
|
|
33278
|
+
/**
|
|
33279
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
33280
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
33281
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
33282
|
+
*/
|
|
33283
|
+
resetConciergeMemoryThread() {
|
|
33284
|
+
this.activeMemoryThreadId = void 0;
|
|
33285
|
+
}
|
|
33286
|
+
ensureActiveMemoryThread() {
|
|
33287
|
+
if (!this.activeMemoryThreadId) {
|
|
33288
|
+
this.activeMemoryThreadId = crypto.randomUUID();
|
|
33289
|
+
}
|
|
33290
|
+
return this.activeMemoryThreadId;
|
|
33291
|
+
}
|
|
32234
33292
|
/**
|
|
32235
33293
|
* Stitch fortress state into a single context blob the substrate
|
|
32236
33294
|
* folds into its summarization prompt.
|
|
@@ -32240,6 +33298,9 @@ var init_operator_chat_service = __esm({
|
|
|
32240
33298
|
* than nested structures. Format:
|
|
32241
33299
|
*
|
|
32242
33300
|
* ```
|
|
33301
|
+
* ## Sanctuary reference
|
|
33302
|
+
* <static domain reference block>
|
|
33303
|
+
*
|
|
32243
33304
|
* ## Recent activity
|
|
32244
33305
|
* <recentActivity output>
|
|
32245
33306
|
*
|
|
@@ -32251,15 +33312,28 @@ var init_operator_chat_service = __esm({
|
|
|
32251
33312
|
* ```
|
|
32252
33313
|
*/
|
|
32253
33314
|
async assembleConciergeContext() {
|
|
33315
|
+
const ref = `## Sanctuary reference
|
|
33316
|
+
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
32254
33317
|
if (!this.contextProviders) {
|
|
32255
|
-
return
|
|
33318
|
+
return `${ref}
|
|
33319
|
+
|
|
33320
|
+
## Recent activity
|
|
33321
|
+
(no providers wired)
|
|
33322
|
+
|
|
33323
|
+
## Wrapped agents
|
|
33324
|
+
(no providers wired)
|
|
33325
|
+
|
|
33326
|
+
## Open inbox
|
|
33327
|
+
(no providers wired)`;
|
|
32256
33328
|
}
|
|
32257
33329
|
const [activity, agents, inbox] = await Promise.all([
|
|
32258
33330
|
this.contextProviders.recentActivity(),
|
|
32259
33331
|
this.contextProviders.agentInventory(),
|
|
32260
33332
|
this.contextProviders.openInbox()
|
|
32261
33333
|
]);
|
|
32262
|
-
return
|
|
33334
|
+
return `${ref}
|
|
33335
|
+
|
|
33336
|
+
## Recent activity
|
|
32263
33337
|
${activity}
|
|
32264
33338
|
|
|
32265
33339
|
## Wrapped agents
|
|
@@ -32381,11 +33455,250 @@ var init_operator_chat_store = __esm({
|
|
|
32381
33455
|
}
|
|
32382
33456
|
});
|
|
32383
33457
|
|
|
33458
|
+
// src/chat/concierge-memory-store.ts
|
|
33459
|
+
function bundleKey(threadId) {
|
|
33460
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
33461
|
+
}
|
|
33462
|
+
function stripKeyPrefix(key) {
|
|
33463
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
33464
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
33465
|
+
}
|
|
33466
|
+
function lastTurnId(bundle) {
|
|
33467
|
+
let max = 0;
|
|
33468
|
+
for (const t of bundle.turns) {
|
|
33469
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
33470
|
+
}
|
|
33471
|
+
return max;
|
|
33472
|
+
}
|
|
33473
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO2, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES2, ConciergeMemoryStore;
|
|
33474
|
+
var init_concierge_memory_store = __esm({
|
|
33475
|
+
"src/chat/concierge-memory-store.ts"() {
|
|
33476
|
+
init_encryption();
|
|
33477
|
+
init_key_derivation();
|
|
33478
|
+
init_encoding();
|
|
33479
|
+
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
33480
|
+
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
33481
|
+
HKDF_INFO2 = "concierge-memory-store-v1";
|
|
33482
|
+
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
33483
|
+
MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
33484
|
+
ConciergeMemoryStore = class {
|
|
33485
|
+
storage;
|
|
33486
|
+
encryptionKey;
|
|
33487
|
+
fortressId;
|
|
33488
|
+
retentionDays;
|
|
33489
|
+
locks;
|
|
33490
|
+
constructor(opts) {
|
|
33491
|
+
this.storage = opts.storage;
|
|
33492
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
33493
|
+
this.fortressId = opts.fortressId;
|
|
33494
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
33495
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
33496
|
+
}
|
|
33497
|
+
/**
|
|
33498
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
33499
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
33500
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
33501
|
+
* monotonicity even under concurrent callers.
|
|
33502
|
+
*/
|
|
33503
|
+
async appendTurn(threadId, role, content) {
|
|
33504
|
+
return this.withLock(threadId, async () => {
|
|
33505
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
33506
|
+
const now = /* @__PURE__ */ new Date();
|
|
33507
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
33508
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
33509
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
33510
|
+
const turn = {
|
|
33511
|
+
thread_id: threadId,
|
|
33512
|
+
fortress_id: this.fortressId,
|
|
33513
|
+
turn_id: nextTurnId,
|
|
33514
|
+
role,
|
|
33515
|
+
content,
|
|
33516
|
+
created_at: now.toISOString(),
|
|
33517
|
+
retention_until: retentionUntil.toISOString()
|
|
33518
|
+
};
|
|
33519
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
33520
|
+
version: 1,
|
|
33521
|
+
thread_id: threadId,
|
|
33522
|
+
fortress_id: this.fortressId,
|
|
33523
|
+
created_at: now.toISOString(),
|
|
33524
|
+
turns: [turn]
|
|
33525
|
+
};
|
|
33526
|
+
await this.saveBundle(next);
|
|
33527
|
+
return turn;
|
|
33528
|
+
});
|
|
33529
|
+
}
|
|
33530
|
+
/**
|
|
33531
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
33532
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
33533
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
33534
|
+
*/
|
|
33535
|
+
async readThread(threadId, opts) {
|
|
33536
|
+
const bundle = await this.loadBundle(threadId);
|
|
33537
|
+
if (!bundle) return [];
|
|
33538
|
+
let turns = bundle.turns;
|
|
33539
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
33540
|
+
const cutoff = opts.sinceTurnId;
|
|
33541
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
33542
|
+
}
|
|
33543
|
+
if (opts?.limit !== void 0) {
|
|
33544
|
+
turns = turns.slice(0, opts.limit);
|
|
33545
|
+
}
|
|
33546
|
+
return turns;
|
|
33547
|
+
}
|
|
33548
|
+
/**
|
|
33549
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33550
|
+
* Sorted newest-first by last_turn_at.
|
|
33551
|
+
*/
|
|
33552
|
+
async listThreads(opts) {
|
|
33553
|
+
const entries = await this.storage.list(
|
|
33554
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33555
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33556
|
+
);
|
|
33557
|
+
const summaries = [];
|
|
33558
|
+
for (const meta of entries) {
|
|
33559
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33560
|
+
if (threadId === null) continue;
|
|
33561
|
+
const bundle = await this.loadBundle(threadId);
|
|
33562
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
33563
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
33564
|
+
summaries.push({
|
|
33565
|
+
thread_id: bundle.thread_id,
|
|
33566
|
+
created_at: bundle.created_at,
|
|
33567
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
33568
|
+
turn_count: bundle.turns.length
|
|
33569
|
+
});
|
|
33570
|
+
}
|
|
33571
|
+
summaries.sort(
|
|
33572
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
33573
|
+
);
|
|
33574
|
+
if (opts?.limit !== void 0) {
|
|
33575
|
+
return summaries.slice(0, opts.limit);
|
|
33576
|
+
}
|
|
33577
|
+
return summaries;
|
|
33578
|
+
}
|
|
33579
|
+
/**
|
|
33580
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
33581
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
33582
|
+
* caller's responsibility.
|
|
33583
|
+
*/
|
|
33584
|
+
async deleteThread(threadId) {
|
|
33585
|
+
const key = bundleKey(threadId);
|
|
33586
|
+
return this.withLock(threadId, async () => {
|
|
33587
|
+
const existed = await this.storage.exists(
|
|
33588
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33589
|
+
key
|
|
33590
|
+
);
|
|
33591
|
+
if (!existed) return false;
|
|
33592
|
+
try {
|
|
33593
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33594
|
+
} catch {
|
|
33595
|
+
return false;
|
|
33596
|
+
}
|
|
33597
|
+
return true;
|
|
33598
|
+
});
|
|
33599
|
+
}
|
|
33600
|
+
/**
|
|
33601
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
33602
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
33603
|
+
*/
|
|
33604
|
+
async pruneExpired(now) {
|
|
33605
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
33606
|
+
const entries = await this.storage.list(
|
|
33607
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33608
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33609
|
+
);
|
|
33610
|
+
let pruned = 0;
|
|
33611
|
+
for (const meta of entries) {
|
|
33612
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33613
|
+
if (threadId === null) continue;
|
|
33614
|
+
pruned += await this.withLock(threadId, async () => {
|
|
33615
|
+
const bundle = await this.loadBundle(threadId);
|
|
33616
|
+
if (!bundle) return 0;
|
|
33617
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
33618
|
+
const dropped = bundle.turns.length - kept.length;
|
|
33619
|
+
if (dropped === 0) return 0;
|
|
33620
|
+
if (kept.length === 0) {
|
|
33621
|
+
await this.storage.delete(
|
|
33622
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33623
|
+
bundleKey(threadId)
|
|
33624
|
+
);
|
|
33625
|
+
} else {
|
|
33626
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
33627
|
+
}
|
|
33628
|
+
return dropped;
|
|
33629
|
+
});
|
|
33630
|
+
}
|
|
33631
|
+
return { pruned };
|
|
33632
|
+
}
|
|
33633
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
33634
|
+
async loadBundle(threadId) {
|
|
33635
|
+
const key = bundleKey(threadId);
|
|
33636
|
+
let raw;
|
|
33637
|
+
try {
|
|
33638
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33639
|
+
} catch {
|
|
33640
|
+
return null;
|
|
33641
|
+
}
|
|
33642
|
+
if (!raw) return null;
|
|
33643
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
33644
|
+
try {
|
|
33645
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
33646
|
+
const aad = stringToBytes(threadId);
|
|
33647
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
33648
|
+
const parsed = JSON.parse(
|
|
33649
|
+
bytesToString(plaintext)
|
|
33650
|
+
);
|
|
33651
|
+
if (parsed.version !== 1) return null;
|
|
33652
|
+
if (parsed.thread_id !== threadId) return null;
|
|
33653
|
+
return parsed;
|
|
33654
|
+
} catch {
|
|
33655
|
+
return null;
|
|
33656
|
+
}
|
|
33657
|
+
}
|
|
33658
|
+
async saveBundle(bundle) {
|
|
33659
|
+
const key = bundleKey(bundle.thread_id);
|
|
33660
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
33661
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
33662
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
33663
|
+
await this.storage.write(
|
|
33664
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33665
|
+
key,
|
|
33666
|
+
stringToBytes(JSON.stringify(envelope))
|
|
33667
|
+
);
|
|
33668
|
+
}
|
|
33669
|
+
/**
|
|
33670
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
33671
|
+
* once the task settles (success or failure). Generic helper so
|
|
33672
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
33673
|
+
*/
|
|
33674
|
+
async withLock(threadId, task) {
|
|
33675
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
33676
|
+
let release;
|
|
33677
|
+
const next = new Promise((resolve8) => {
|
|
33678
|
+
release = resolve8;
|
|
33679
|
+
});
|
|
33680
|
+
const chained = previous.then(() => next);
|
|
33681
|
+
this.locks.set(threadId, chained);
|
|
33682
|
+
try {
|
|
33683
|
+
await previous;
|
|
33684
|
+
return await task();
|
|
33685
|
+
} finally {
|
|
33686
|
+
release();
|
|
33687
|
+
if (this.locks.get(threadId) === chained) {
|
|
33688
|
+
this.locks.delete(threadId);
|
|
33689
|
+
}
|
|
33690
|
+
}
|
|
33691
|
+
}
|
|
33692
|
+
};
|
|
33693
|
+
}
|
|
33694
|
+
});
|
|
33695
|
+
|
|
32384
33696
|
// src/chat/operator-chat-index.ts
|
|
32385
33697
|
var init_operator_chat_index = __esm({
|
|
32386
33698
|
"src/chat/operator-chat-index.ts"() {
|
|
32387
33699
|
init_operator_chat_service();
|
|
32388
33700
|
init_operator_chat_store();
|
|
33701
|
+
init_concierge_memory_store();
|
|
32389
33702
|
init_operator_chat_audit_events();
|
|
32390
33703
|
init_operator_chat_types();
|
|
32391
33704
|
}
|
|
@@ -32399,6 +33712,14 @@ function buildV11Bindings(inputs) {
|
|
|
32399
33712
|
let operatorChatService;
|
|
32400
33713
|
if (inputs.storage && inputs.masterKey) {
|
|
32401
33714
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
33715
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
33716
|
+
storage: inputs.storage,
|
|
33717
|
+
masterKey: inputs.masterKey,
|
|
33718
|
+
fortressId: inputs.fortressId,
|
|
33719
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
33720
|
+
});
|
|
33721
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
33722
|
+
});
|
|
32402
33723
|
operatorChatService = new OperatorChatService({
|
|
32403
33724
|
store: chatStore,
|
|
32404
33725
|
auditLog: inputs.auditLog,
|
|
@@ -32409,7 +33730,8 @@ function buildV11Bindings(inputs) {
|
|
|
32409
33730
|
identityId: inputs.identityId,
|
|
32410
33731
|
registry
|
|
32411
33732
|
}),
|
|
32412
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
33733
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
33734
|
+
conciergeMemory
|
|
32413
33735
|
});
|
|
32414
33736
|
}
|
|
32415
33737
|
const hubService = new HubService({
|
|
@@ -32644,7 +33966,7 @@ var init_defaults = __esm({
|
|
|
32644
33966
|
});
|
|
32645
33967
|
|
|
32646
33968
|
// src/intelligence/policy-store.ts
|
|
32647
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
33969
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO3, IntelligenceConfigStore;
|
|
32648
33970
|
var init_policy_store = __esm({
|
|
32649
33971
|
"src/intelligence/policy-store.ts"() {
|
|
32650
33972
|
init_encryption();
|
|
@@ -32653,13 +33975,13 @@ var init_policy_store = __esm({
|
|
|
32653
33975
|
init_defaults();
|
|
32654
33976
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
32655
33977
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
32656
|
-
|
|
33978
|
+
HKDF_INFO3 = "intelligence-substrate-config";
|
|
32657
33979
|
IntelligenceConfigStore = class {
|
|
32658
33980
|
storage;
|
|
32659
33981
|
encryptionKey;
|
|
32660
33982
|
constructor(storage, masterKey) {
|
|
32661
33983
|
this.storage = storage;
|
|
32662
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33984
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
32663
33985
|
}
|
|
32664
33986
|
/**
|
|
32665
33987
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -34681,7 +36003,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34681
36003
|
);
|
|
34682
36004
|
}
|
|
34683
36005
|
}
|
|
34684
|
-
const
|
|
36006
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
36007
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
36008
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
34685
36009
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
34686
36010
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
34687
36011
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -34690,6 +36014,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34690
36014
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
34691
36015
|
);
|
|
34692
36016
|
}
|
|
36017
|
+
let detailedFailureClass;
|
|
36018
|
+
if (identityFailed) {
|
|
36019
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
36020
|
+
} else if (reputationBundleFailed) {
|
|
36021
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
36022
|
+
} else if (reputationAttestationFailed) {
|
|
36023
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
36024
|
+
} else if (unverifiableFailed) {
|
|
36025
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
36026
|
+
}
|
|
34693
36027
|
return {
|
|
34694
36028
|
version: "1.1",
|
|
34695
36029
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -34709,7 +36043,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34709
36043
|
identity,
|
|
34710
36044
|
audit,
|
|
34711
36045
|
reputation,
|
|
34712
|
-
failure_class:
|
|
36046
|
+
failure_class: detailedFailureClass
|
|
34713
36047
|
};
|
|
34714
36048
|
}
|
|
34715
36049
|
var InvalidExitBundleError, PRIVATE_MATERIAL_KEYS;
|
|
@@ -35291,6 +36625,9 @@ async function importExitBundle(opts) {
|
|
|
35291
36625
|
reputationArtifact?.json ?? null,
|
|
35292
36626
|
manifest
|
|
35293
36627
|
);
|
|
36628
|
+
if (!conflicts.public_identity_exists && identityArtifact?.json && opts.identityManager.getPrimaryIdentityId() !== null && opts.identityManager.getPrimaryIdentityId() !== identityArtifact.json.bundle.identity_id) {
|
|
36629
|
+
conflicts.public_identity_exists = true;
|
|
36630
|
+
}
|
|
35294
36631
|
if (!opts.activate) {
|
|
35295
36632
|
return {
|
|
35296
36633
|
verified: true,
|
|
@@ -35317,7 +36654,7 @@ async function importExitBundle(opts) {
|
|
|
35317
36654
|
if (conflicts.public_identity_exists && !opts.forceRebind) {
|
|
35318
36655
|
throw new ExitBundleImportError(
|
|
35319
36656
|
"IDENTITY_OVERWRITE_REFUSED",
|
|
35320
|
-
"Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
|
|
36657
|
+
"Importing this exit bundle would overwrite an existing fortress public identity (either the same identity already imported, or a different identity is currently active). Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
|
|
35321
36658
|
);
|
|
35322
36659
|
}
|
|
35323
36660
|
if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
|
|
@@ -35674,7 +37011,19 @@ async function runExitCommand(args) {
|
|
|
35674
37011
|
}
|
|
35675
37012
|
const config = await loadConfig();
|
|
35676
37013
|
const ctx = await openExitContext(argv, env);
|
|
35677
|
-
|
|
37014
|
+
let policy;
|
|
37015
|
+
try {
|
|
37016
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
37017
|
+
} catch (policyErr) {
|
|
37018
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
37019
|
+
write(err, `
|
|
37020
|
+
Sanctuary cannot proceed.
|
|
37021
|
+
${policyErr.message}
|
|
37022
|
+
`);
|
|
37023
|
+
return 1;
|
|
37024
|
+
}
|
|
37025
|
+
throw policyErr;
|
|
37026
|
+
}
|
|
35678
37027
|
const result = await exportExitBundle({
|
|
35679
37028
|
bundleDir: outDir,
|
|
35680
37029
|
storage: ctx.storage,
|
|
@@ -35707,6 +37056,26 @@ async function runExitCommand(args) {
|
|
|
35707
37056
|
write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
|
|
35708
37057
|
return 2;
|
|
35709
37058
|
}
|
|
37059
|
+
const bundleRoot = path.resolve(dir);
|
|
37060
|
+
try {
|
|
37061
|
+
await promises.access(bundleRoot);
|
|
37062
|
+
} catch {
|
|
37063
|
+
write(err, `Error: bundle directory not found: ${bundleRoot}
|
|
37064
|
+
`);
|
|
37065
|
+
return 1;
|
|
37066
|
+
}
|
|
37067
|
+
const manifestPath = path.join(bundleRoot, "manifest.json");
|
|
37068
|
+
try {
|
|
37069
|
+
const raw = await promises.readFile(manifestPath, "utf8");
|
|
37070
|
+
JSON.parse(raw);
|
|
37071
|
+
} catch {
|
|
37072
|
+
write(
|
|
37073
|
+
err,
|
|
37074
|
+
`Error: bundle manifest missing or malformed at ${manifestPath}
|
|
37075
|
+
`
|
|
37076
|
+
);
|
|
37077
|
+
return 1;
|
|
37078
|
+
}
|
|
35710
37079
|
const activate = hasFlag(argv, "--activate");
|
|
35711
37080
|
const forceRebind = hasFlag(argv, "--force-rebind");
|
|
35712
37081
|
const acceptUnverifiableAttestations = hasFlag(
|
|
@@ -35880,11 +37249,11 @@ async function startDashboardServer(options) {
|
|
|
35880
37249
|
}
|
|
35881
37250
|
}
|
|
35882
37251
|
});
|
|
35883
|
-
await new Promise((
|
|
37252
|
+
await new Promise((resolve8, reject) => {
|
|
35884
37253
|
server.once("error", reject);
|
|
35885
37254
|
server.listen(port, host, () => {
|
|
35886
37255
|
server.off("error", reject);
|
|
35887
|
-
|
|
37256
|
+
resolve8();
|
|
35888
37257
|
});
|
|
35889
37258
|
});
|
|
35890
37259
|
const actualPort = (() => {
|
|
@@ -35897,8 +37266,8 @@ async function startDashboardServer(options) {
|
|
|
35897
37266
|
url,
|
|
35898
37267
|
port: actualPort,
|
|
35899
37268
|
host,
|
|
35900
|
-
stop: () => new Promise((
|
|
35901
|
-
server.close((err) => err ? reject(err) :
|
|
37269
|
+
stop: () => new Promise((resolve8, reject) => {
|
|
37270
|
+
server.close((err) => err ? reject(err) : resolve8());
|
|
35902
37271
|
}),
|
|
35903
37272
|
publish,
|
|
35904
37273
|
publishActivity: (entry) => publish({ type: "activity", data: entry }),
|
|
@@ -36354,7 +37723,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36354
37723
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
36355
37724
|
await profileStore.load();
|
|
36356
37725
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
36357
|
-
|
|
37726
|
+
let policy;
|
|
37727
|
+
try {
|
|
37728
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
37729
|
+
} catch (err) {
|
|
37730
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
37731
|
+
console.error(`
|
|
37732
|
+
Sanctuary cannot start.
|
|
37733
|
+
${err.message}
|
|
37734
|
+
`);
|
|
37735
|
+
process.exit(1);
|
|
37736
|
+
}
|
|
37737
|
+
throw err;
|
|
37738
|
+
}
|
|
36358
37739
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
36359
37740
|
await baseline.load();
|
|
36360
37741
|
let approvalChannel;
|
|
@@ -36452,6 +37833,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36452
37833
|
});
|
|
36453
37834
|
} : void 0;
|
|
36454
37835
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37836
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37837
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37838
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
37839
|
+
storage,
|
|
37840
|
+
masterKey,
|
|
37841
|
+
auditLog,
|
|
37842
|
+
identityId: aggregatorIdentityId,
|
|
37843
|
+
fortressId: fortressIdForAggregator
|
|
37844
|
+
});
|
|
37845
|
+
gate.setApprovalEventCallback((event) => {
|
|
37846
|
+
void approvalAggregator.ingest(event);
|
|
37847
|
+
});
|
|
37848
|
+
if (dashboard) {
|
|
37849
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
37850
|
+
}
|
|
36455
37851
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
36456
37852
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
36457
37853
|
config,
|
|
@@ -36571,7 +37967,7 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36571
37967
|
clientManager.configure(enabledServers).catch((err) => {
|
|
36572
37968
|
console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
36573
37969
|
});
|
|
36574
|
-
await new Promise((
|
|
37970
|
+
await new Promise((resolve8) => setTimeout(resolve8, 2e3));
|
|
36575
37971
|
const proxiedTools = proxyRouter.getProxiedTools();
|
|
36576
37972
|
if (proxiedTools.length > 0) {
|
|
36577
37973
|
allTools.push(...proxiedTools);
|
|
@@ -36642,6 +38038,7 @@ var init_src = __esm({
|
|
|
36642
38038
|
init_dashboard();
|
|
36643
38039
|
init_webhook();
|
|
36644
38040
|
init_gate();
|
|
38041
|
+
init_approval_aggregator();
|
|
36645
38042
|
init_tools4();
|
|
36646
38043
|
init_router();
|
|
36647
38044
|
init_router();
|
|
@@ -37410,8 +38807,8 @@ async function runWrap(options, deps = {}) {
|
|
|
37410
38807
|
passphraseValue = process.env.SANCTUARY_PASSPHRASE;
|
|
37411
38808
|
} else {
|
|
37412
38809
|
try {
|
|
37413
|
-
const
|
|
37414
|
-
const resolved = await
|
|
38810
|
+
const resolve8 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
|
|
38811
|
+
const resolved = await resolve8();
|
|
37415
38812
|
passphraseLocation = resolved.location;
|
|
37416
38813
|
passphraseSource = resolved.source;
|
|
37417
38814
|
passphraseValue = resolved.value;
|
|
@@ -37766,12 +39163,12 @@ async function defaultOpenBrowser(url) {
|
|
|
37766
39163
|
cmd = "xdg-open";
|
|
37767
39164
|
args = [url];
|
|
37768
39165
|
}
|
|
37769
|
-
await new Promise((
|
|
39166
|
+
await new Promise((resolve8) => {
|
|
37770
39167
|
const child = child_process.spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
37771
|
-
child.on("error", () =>
|
|
39168
|
+
child.on("error", () => resolve8());
|
|
37772
39169
|
child.on("spawn", () => {
|
|
37773
39170
|
child.unref();
|
|
37774
|
-
|
|
39171
|
+
resolve8();
|
|
37775
39172
|
});
|
|
37776
39173
|
});
|
|
37777
39174
|
}
|
|
@@ -38740,32 +40137,32 @@ endstream`;
|
|
|
38740
40137
|
const offsets = new Array(totalObjects + 1).fill(0);
|
|
38741
40138
|
const chunks = [];
|
|
38742
40139
|
let bytePos = 0;
|
|
38743
|
-
const
|
|
40140
|
+
const write3 = (s) => {
|
|
38744
40141
|
const buf = Buffer.from(s, "latin1");
|
|
38745
40142
|
chunks.push(buf);
|
|
38746
40143
|
bytePos += buf.length;
|
|
38747
40144
|
};
|
|
38748
|
-
|
|
40145
|
+
write3("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
|
|
38749
40146
|
for (let i = 1; i <= totalObjects; i++) {
|
|
38750
40147
|
offsets[i] = bytePos;
|
|
38751
|
-
|
|
40148
|
+
write3(`${i} 0 obj
|
|
38752
40149
|
${objectBodies[i]}
|
|
38753
40150
|
endobj
|
|
38754
40151
|
`);
|
|
38755
40152
|
}
|
|
38756
40153
|
const xrefPos = bytePos;
|
|
38757
|
-
|
|
40154
|
+
write3(`xref
|
|
38758
40155
|
0 ${totalObjects + 1}
|
|
38759
40156
|
`);
|
|
38760
|
-
|
|
40157
|
+
write3("0000000000 65535 f \n");
|
|
38761
40158
|
for (let i = 1; i <= totalObjects; i++) {
|
|
38762
|
-
|
|
40159
|
+
write3(`${offsets[i].toString().padStart(10, "0")} 00000 n
|
|
38763
40160
|
`);
|
|
38764
40161
|
}
|
|
38765
|
-
|
|
40162
|
+
write3(`trailer
|
|
38766
40163
|
<< /Size ${totalObjects + 1} /Root 1 0 R >>
|
|
38767
40164
|
`);
|
|
38768
|
-
|
|
40165
|
+
write3(`startxref
|
|
38769
40166
|
${xrefPos}
|
|
38770
40167
|
%%EOF
|
|
38771
40168
|
`);
|
|
@@ -39100,7 +40497,7 @@ var init_backend_interface = __esm({
|
|
|
39100
40497
|
}
|
|
39101
40498
|
});
|
|
39102
40499
|
async function runSecurity(args, input) {
|
|
39103
|
-
return new Promise((
|
|
40500
|
+
return new Promise((resolve8, reject) => {
|
|
39104
40501
|
const child = child_process.spawn(SECURITY_BIN, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
39105
40502
|
let stdout = "";
|
|
39106
40503
|
let stderr = "";
|
|
@@ -39122,7 +40519,7 @@ async function runSecurity(args, input) {
|
|
|
39122
40519
|
reject(err);
|
|
39123
40520
|
});
|
|
39124
40521
|
child.on("close", (code) => {
|
|
39125
|
-
|
|
40522
|
+
resolve8({ stdout, stderr, code: code ?? -1 });
|
|
39126
40523
|
});
|
|
39127
40524
|
if (input !== void 0) {
|
|
39128
40525
|
child.stdin.write(input);
|
|
@@ -40198,7 +41595,7 @@ async function readValue(stdin, prompt2) {
|
|
|
40198
41595
|
return await readFirstLine(stdin);
|
|
40199
41596
|
}
|
|
40200
41597
|
async function readFirstLine(stdin) {
|
|
40201
|
-
return new Promise((
|
|
41598
|
+
return new Promise((resolve8, reject) => {
|
|
40202
41599
|
const rl = readline.createInterface({ input: stdin });
|
|
40203
41600
|
let resolved = false;
|
|
40204
41601
|
const finish = (value) => {
|
|
@@ -40209,7 +41606,7 @@ async function readFirstLine(stdin) {
|
|
|
40209
41606
|
rl.close();
|
|
40210
41607
|
} catch {
|
|
40211
41608
|
}
|
|
40212
|
-
|
|
41609
|
+
resolve8(value);
|
|
40213
41610
|
};
|
|
40214
41611
|
const deadline = setTimeout(() => {
|
|
40215
41612
|
finish("");
|
|
@@ -40228,7 +41625,7 @@ async function promptSilently(stdin, prompt2) {
|
|
|
40228
41625
|
process.stderr.write(`${prompt2}: `);
|
|
40229
41626
|
stdin.setRawMode?.(true);
|
|
40230
41627
|
stdin.resume();
|
|
40231
|
-
return await new Promise((
|
|
41628
|
+
return await new Promise((resolve8) => {
|
|
40232
41629
|
let buf = "";
|
|
40233
41630
|
const onData = (chunk) => {
|
|
40234
41631
|
const s = chunk.toString("utf8");
|
|
@@ -40238,7 +41635,7 @@ async function promptSilently(stdin, prompt2) {
|
|
|
40238
41635
|
stdin.pause();
|
|
40239
41636
|
stdin.off("data", onData);
|
|
40240
41637
|
process.stderr.write("\n");
|
|
40241
|
-
|
|
41638
|
+
resolve8(buf);
|
|
40242
41639
|
return;
|
|
40243
41640
|
}
|
|
40244
41641
|
if (ch === "") {
|
|
@@ -40505,13 +41902,179 @@ var init_cli4 = __esm({
|
|
|
40505
41902
|
init_discovery();
|
|
40506
41903
|
}
|
|
40507
41904
|
});
|
|
41905
|
+
|
|
41906
|
+
// src/cli/identity.ts
|
|
41907
|
+
var identity_exports2 = {};
|
|
41908
|
+
__export(identity_exports2, {
|
|
41909
|
+
runIdentityCommand: () => runIdentityCommand
|
|
41910
|
+
});
|
|
41911
|
+
function write2(stream, text) {
|
|
41912
|
+
stream.write(text);
|
|
41913
|
+
}
|
|
41914
|
+
function flagValue2(argv, name) {
|
|
41915
|
+
const index = argv.indexOf(name);
|
|
41916
|
+
if (index === -1) return void 0;
|
|
41917
|
+
return argv[index + 1];
|
|
41918
|
+
}
|
|
41919
|
+
function hasFlag2(argv, name) {
|
|
41920
|
+
return argv.includes(name);
|
|
41921
|
+
}
|
|
41922
|
+
function printUsage3(out) {
|
|
41923
|
+
write2(
|
|
41924
|
+
out,
|
|
41925
|
+
`Usage: sanctuary identity <command> [options]
|
|
41926
|
+
|
|
41927
|
+
Commands:
|
|
41928
|
+
show Print the active identity (DID, identity_id, public key).
|
|
41929
|
+
|
|
41930
|
+
Options:
|
|
41931
|
+
--fortress <path> Override the storage path.
|
|
41932
|
+
--passphrase <val> Passphrase for master-key derivation.
|
|
41933
|
+
--json Output as JSON.
|
|
41934
|
+
--help, -h Show this help.
|
|
41935
|
+
|
|
41936
|
+
Environment variables:
|
|
41937
|
+
SANCTUARY_PASSPHRASE Key derivation passphrase.
|
|
41938
|
+
SANCTUARY_STORAGE_PATH State directory (default: ~/.sanctuary).
|
|
41939
|
+
SANCTUARY_FORTRESS_PATH Operator-friendly alias for STORAGE_PATH.
|
|
41940
|
+
SANCTUARY_RECOVERY_KEY Recovery key (alternative to passphrase).
|
|
41941
|
+
|
|
41942
|
+
Identity data is encrypted at rest. A passphrase or recovery key is
|
|
41943
|
+
required to decrypt and display identity information.
|
|
41944
|
+
`
|
|
41945
|
+
);
|
|
41946
|
+
}
|
|
41947
|
+
async function runIdentityCommand(args) {
|
|
41948
|
+
const argv = args.argv;
|
|
41949
|
+
const out = args.out ?? process.stdout;
|
|
41950
|
+
const err = args.err ?? process.stderr;
|
|
41951
|
+
const env = args.env ?? process.env;
|
|
41952
|
+
if (argv.length === 0 || hasFlag2(argv, "--help") || hasFlag2(argv, "-h")) {
|
|
41953
|
+
printUsage3(out);
|
|
41954
|
+
return 0;
|
|
41955
|
+
}
|
|
41956
|
+
const command = argv[0];
|
|
41957
|
+
if (command === "show") {
|
|
41958
|
+
return await cmdShow(argv.slice(1), out, err, env);
|
|
41959
|
+
}
|
|
41960
|
+
write2(err, `Unknown identity command: ${command}
|
|
41961
|
+
`);
|
|
41962
|
+
write2(err, `Run "sanctuary identity --help" for usage.
|
|
41963
|
+
`);
|
|
41964
|
+
return 2;
|
|
41965
|
+
}
|
|
41966
|
+
async function cmdShow(argv, out, err, env) {
|
|
41967
|
+
const json = hasFlag2(argv, "--json");
|
|
41968
|
+
const fortressFlag = flagValue2(argv, "--fortress");
|
|
41969
|
+
if (fortressFlag) {
|
|
41970
|
+
process.env.SANCTUARY_STORAGE_PATH = fortressFlag;
|
|
41971
|
+
}
|
|
41972
|
+
const passphrase = flagValue2(argv, "--passphrase") ?? env.SANCTUARY_PASSPHRASE;
|
|
41973
|
+
const recoveryKey = env.SANCTUARY_RECOVERY_KEY;
|
|
41974
|
+
if (!passphrase && !recoveryKey) {
|
|
41975
|
+
write2(
|
|
41976
|
+
err,
|
|
41977
|
+
"Error: sanctuary identity show requires SANCTUARY_PASSPHRASE, --passphrase, or SANCTUARY_RECOVERY_KEY.\n"
|
|
41978
|
+
);
|
|
41979
|
+
return 1;
|
|
41980
|
+
}
|
|
41981
|
+
try {
|
|
41982
|
+
const config = await loadConfig();
|
|
41983
|
+
await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
41984
|
+
const stateStoragePath = path.join(config.storage_path, "state");
|
|
41985
|
+
const storage = new FilesystemStorage(stateStoragePath);
|
|
41986
|
+
let masterKey;
|
|
41987
|
+
if (passphrase) {
|
|
41988
|
+
let existingParams;
|
|
41989
|
+
const raw = await storage.read("_meta", "key-params");
|
|
41990
|
+
if (raw)
|
|
41991
|
+
existingParams = JSON.parse(bytesToString(raw));
|
|
41992
|
+
const derived = await deriveMasterKey(passphrase, existingParams);
|
|
41993
|
+
masterKey = derived.key;
|
|
41994
|
+
} else {
|
|
41995
|
+
masterKey = fromBase64url(recoveryKey);
|
|
41996
|
+
if (masterKey.length !== 32) {
|
|
41997
|
+
write2(err, "Error: SANCTUARY_RECOVERY_KEY must decode to 32 bytes.\n");
|
|
41998
|
+
return 1;
|
|
41999
|
+
}
|
|
42000
|
+
}
|
|
42001
|
+
const identityManager = new IdentityManager(storage, masterKey);
|
|
42002
|
+
const loadResult = await identityManager.load();
|
|
42003
|
+
if (loadResult.loaded === 0) {
|
|
42004
|
+
write2(
|
|
42005
|
+
err,
|
|
42006
|
+
loadResult.total > 0 ? "Error: identity files found but none could be decrypted. Wrong passphrase?\n" : "No identities found in this fortress.\n"
|
|
42007
|
+
);
|
|
42008
|
+
return 1;
|
|
42009
|
+
}
|
|
42010
|
+
const primary = identityManager.getDefault();
|
|
42011
|
+
if (!primary) {
|
|
42012
|
+
write2(err, "No primary identity set.\n");
|
|
42013
|
+
return 1;
|
|
42014
|
+
}
|
|
42015
|
+
if (json) {
|
|
42016
|
+
write2(
|
|
42017
|
+
out,
|
|
42018
|
+
JSON.stringify(
|
|
42019
|
+
{
|
|
42020
|
+
identity_id: primary.identity_id,
|
|
42021
|
+
did: primary.did,
|
|
42022
|
+
public_key: primary.public_key,
|
|
42023
|
+
label: primary.label,
|
|
42024
|
+
key_type: primary.key_type,
|
|
42025
|
+
created_at: primary.created_at,
|
|
42026
|
+
storage_path: config.storage_path,
|
|
42027
|
+
total_identities: loadResult.loaded
|
|
42028
|
+
},
|
|
42029
|
+
null,
|
|
42030
|
+
2
|
|
42031
|
+
) + "\n"
|
|
42032
|
+
);
|
|
42033
|
+
} else {
|
|
42034
|
+
write2(out, `identity_id: ${primary.identity_id}
|
|
42035
|
+
`);
|
|
42036
|
+
write2(out, `did: ${primary.did}
|
|
42037
|
+
`);
|
|
42038
|
+
write2(out, `public_key: ${primary.public_key}
|
|
42039
|
+
`);
|
|
42040
|
+
write2(out, `label: ${primary.label}
|
|
42041
|
+
`);
|
|
42042
|
+
write2(out, `key_type: ${primary.key_type}
|
|
42043
|
+
`);
|
|
42044
|
+
write2(out, `created_at: ${primary.created_at}
|
|
42045
|
+
`);
|
|
42046
|
+
write2(out, `storage_path: ${config.storage_path}
|
|
42047
|
+
`);
|
|
42048
|
+
write2(out, `total_identities: ${loadResult.loaded}
|
|
42049
|
+
`);
|
|
42050
|
+
}
|
|
42051
|
+
return 0;
|
|
42052
|
+
} catch (error) {
|
|
42053
|
+
write2(
|
|
42054
|
+
err,
|
|
42055
|
+
error instanceof Error ? `Error: ${error.message}
|
|
42056
|
+
` : `Error: ${String(error)}
|
|
42057
|
+
`
|
|
42058
|
+
);
|
|
42059
|
+
return 1;
|
|
42060
|
+
}
|
|
42061
|
+
}
|
|
42062
|
+
var init_identity2 = __esm({
|
|
42063
|
+
"src/cli/identity.ts"() {
|
|
42064
|
+
init_filesystem();
|
|
42065
|
+
init_tools();
|
|
42066
|
+
init_key_derivation();
|
|
42067
|
+
init_encoding();
|
|
42068
|
+
init_config();
|
|
42069
|
+
}
|
|
42070
|
+
});
|
|
40508
42071
|
async function probeTenantDashboard(tenant, options = {}) {
|
|
40509
42072
|
const rt = tenant.runtime;
|
|
40510
42073
|
if (!rt) {
|
|
40511
42074
|
return { running: false, status: null, reason: "no runtime.json" };
|
|
40512
42075
|
}
|
|
40513
42076
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
|
|
40514
|
-
return await new Promise((
|
|
42077
|
+
return await new Promise((resolve8) => {
|
|
40515
42078
|
const req = http.get(
|
|
40516
42079
|
{
|
|
40517
42080
|
host: rt.dashboard_host,
|
|
@@ -40523,9 +42086,9 @@ async function probeTenantDashboard(tenant, options = {}) {
|
|
|
40523
42086
|
res.resume();
|
|
40524
42087
|
const status = res.statusCode ?? 0;
|
|
40525
42088
|
if (status > 0 && status < 500) {
|
|
40526
|
-
|
|
42089
|
+
resolve8({ running: true, status, reason: null });
|
|
40527
42090
|
} else {
|
|
40528
|
-
|
|
42091
|
+
resolve8({
|
|
40529
42092
|
running: false,
|
|
40530
42093
|
status,
|
|
40531
42094
|
reason: `dashboard returned ${status}`
|
|
@@ -40535,10 +42098,10 @@ async function probeTenantDashboard(tenant, options = {}) {
|
|
|
40535
42098
|
);
|
|
40536
42099
|
req.on("timeout", () => {
|
|
40537
42100
|
req.destroy();
|
|
40538
|
-
|
|
42101
|
+
resolve8({ running: false, status: null, reason: "timeout" });
|
|
40539
42102
|
});
|
|
40540
42103
|
req.on("error", (err) => {
|
|
40541
|
-
|
|
42104
|
+
resolve8({
|
|
40542
42105
|
running: false,
|
|
40543
42106
|
status: null,
|
|
40544
42107
|
reason: err.code ?? err.message
|
|
@@ -40570,10 +42133,17 @@ function resolveCtx(args) {
|
|
|
40570
42133
|
};
|
|
40571
42134
|
}
|
|
40572
42135
|
async function runAgentsCommand(args) {
|
|
42136
|
+
const fortressIdx = args.argv.indexOf("--fortress");
|
|
42137
|
+
if (fortressIdx !== -1 && args.argv[fortressIdx + 1]) {
|
|
42138
|
+
args = { ...args, root: args.argv[fortressIdx + 1] };
|
|
42139
|
+
const filtered = [...args.argv];
|
|
42140
|
+
filtered.splice(fortressIdx, 2);
|
|
42141
|
+
args = { ...args, argv: filtered };
|
|
42142
|
+
}
|
|
40573
42143
|
const ctx = resolveCtx(args);
|
|
40574
42144
|
const [sub, ...rest] = args.argv;
|
|
40575
42145
|
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
|
40576
|
-
|
|
42146
|
+
printUsage4(ctx.out);
|
|
40577
42147
|
return 0;
|
|
40578
42148
|
}
|
|
40579
42149
|
try {
|
|
@@ -40581,13 +42151,13 @@ async function runAgentsCommand(args) {
|
|
|
40581
42151
|
case "list":
|
|
40582
42152
|
return await cmdList3(rest, ctx);
|
|
40583
42153
|
case "show":
|
|
40584
|
-
return await
|
|
42154
|
+
return await cmdShow2(rest, ctx);
|
|
40585
42155
|
case "status":
|
|
40586
42156
|
return await cmdStatus(rest, ctx);
|
|
40587
42157
|
default:
|
|
40588
42158
|
ctx.err.write(`Unknown subcommand: ${sub}
|
|
40589
42159
|
`);
|
|
40590
|
-
|
|
42160
|
+
printUsage4(ctx.err);
|
|
40591
42161
|
return 2;
|
|
40592
42162
|
}
|
|
40593
42163
|
} catch (e) {
|
|
@@ -40597,13 +42167,17 @@ async function runAgentsCommand(args) {
|
|
|
40597
42167
|
return 1;
|
|
40598
42168
|
}
|
|
40599
42169
|
}
|
|
40600
|
-
function
|
|
42170
|
+
function printUsage4(s) {
|
|
40601
42171
|
s.write(`Usage: sanctuary agents <command> [flags]
|
|
40602
42172
|
|
|
40603
42173
|
list [--json] List every tenant visible on this host.
|
|
40604
42174
|
show <tenant> [--json] Show details for one tenant.
|
|
40605
42175
|
status [--json] One-line-per-tenant running/stopped summary.
|
|
40606
42176
|
|
|
42177
|
+
Options:
|
|
42178
|
+
--fortress <path> Scope discovery to a specific storage path
|
|
42179
|
+
instead of scanning ~/.sanctuary.
|
|
42180
|
+
|
|
40607
42181
|
Tenants are discovered by scanning ~/.sanctuary and any storage paths in
|
|
40608
42182
|
SANCTUARY_AGENTS_EXTRA_PATHS or ~/.sanctuary/agents-extra.json. Tenant
|
|
40609
42183
|
creation is done via \`sanctuary wrap\` with SANCTUARY_STORAGE_PATH set.
|
|
@@ -40687,7 +42261,7 @@ async function cmdList3(argv, ctx) {
|
|
|
40687
42261
|
}
|
|
40688
42262
|
return 0;
|
|
40689
42263
|
}
|
|
40690
|
-
async function
|
|
42264
|
+
async function cmdShow2(argv, ctx) {
|
|
40691
42265
|
const positional = argv.find((a) => !a.startsWith("--"));
|
|
40692
42266
|
if (!positional) {
|
|
40693
42267
|
ctx.err.write("Missing tenant. Usage: sanctuary agents show <tenant>\n");
|
|
@@ -40828,7 +42402,8 @@ var init_agents = __esm({
|
|
|
40828
42402
|
// src/cli/reset-passphrase.ts
|
|
40829
42403
|
var reset_passphrase_exports = {};
|
|
40830
42404
|
__export(reset_passphrase_exports, {
|
|
40831
|
-
runResetPassphraseCommand: () => runResetPassphraseCommand
|
|
42405
|
+
runResetPassphraseCommand: () => runResetPassphraseCommand,
|
|
42406
|
+
zeroizeBuffers: () => zeroizeBuffers
|
|
40832
42407
|
});
|
|
40833
42408
|
async function runResetPassphraseCommand(args) {
|
|
40834
42409
|
const out = args.out ?? process.stdout;
|
|
@@ -40838,10 +42413,10 @@ async function runResetPassphraseCommand(args) {
|
|
|
40838
42413
|
const plat = args.platformOverride ?? process.platform;
|
|
40839
42414
|
const parsed = parseArgs2(args.argv);
|
|
40840
42415
|
if (parsed.help) {
|
|
40841
|
-
|
|
42416
|
+
printUsage5(out);
|
|
40842
42417
|
return 0;
|
|
40843
42418
|
}
|
|
40844
|
-
const storagePath = parsed.storage ?? args.storagePath ?? resolveStoragePath(process.env, home);
|
|
42419
|
+
const storagePath = parsed.storage ?? parsed.fortress ?? args.storagePath ?? resolveStoragePath(process.env, home);
|
|
40845
42420
|
out.write(banner(storagePath));
|
|
40846
42421
|
const runtimeFile = path.join(storagePath, "runtime.json");
|
|
40847
42422
|
if (await fileExists4(runtimeFile)) {
|
|
@@ -40856,34 +42431,42 @@ Then re-run this command.
|
|
|
40856
42431
|
return 1;
|
|
40857
42432
|
}
|
|
40858
42433
|
const lines = new LineReader(stdin);
|
|
42434
|
+
let code = 1;
|
|
42435
|
+
let nukeSucceeded = false;
|
|
40859
42436
|
try {
|
|
40860
42437
|
const availability = await surveyAvailableModes(storagePath);
|
|
40861
42438
|
const mode = parsed.mode ?? await selectMode(lines, out, err, availability);
|
|
40862
42439
|
if (!mode) {
|
|
40863
42440
|
err.write("Aborted: no recovery mode selected.\n");
|
|
40864
|
-
|
|
40865
|
-
}
|
|
40866
|
-
|
|
40867
|
-
|
|
40868
|
-
|
|
40869
|
-
|
|
40870
|
-
|
|
42441
|
+
code = 1;
|
|
42442
|
+
} else if (mode === "shares") {
|
|
42443
|
+
code = await runSharesPath(out, err, availability);
|
|
42444
|
+
} else if (mode === "guardian") {
|
|
42445
|
+
code = await runGuardianPath(out, err, availability);
|
|
42446
|
+
} else {
|
|
42447
|
+
code = await runNukePath({
|
|
42448
|
+
out,
|
|
42449
|
+
err,
|
|
42450
|
+
lines,
|
|
42451
|
+
storagePath,
|
|
42452
|
+
home,
|
|
42453
|
+
plat,
|
|
42454
|
+
exec: args.exec ?? defaultExec2
|
|
42455
|
+
});
|
|
42456
|
+
nukeSucceeded = mode === "nuke" && code === 0;
|
|
40871
42457
|
}
|
|
40872
|
-
return await runNukePath({
|
|
40873
|
-
out,
|
|
40874
|
-
err,
|
|
40875
|
-
lines,
|
|
40876
|
-
storagePath,
|
|
40877
|
-
home,
|
|
40878
|
-
plat,
|
|
40879
|
-
exec: args.exec ?? defaultExec2
|
|
40880
|
-
});
|
|
40881
42458
|
} finally {
|
|
42459
|
+
zeroizeBuffers(args.keyMaterialToZeroize);
|
|
40882
42460
|
lines.close();
|
|
40883
42461
|
}
|
|
42462
|
+
if (parsed.exitOnCompletion && nukeSucceeded) {
|
|
42463
|
+
const doExit = args.exitProcess ?? ((c) => process.exit(c));
|
|
42464
|
+
doExit(0);
|
|
42465
|
+
}
|
|
42466
|
+
return code;
|
|
40884
42467
|
}
|
|
40885
42468
|
function parseArgs2(argv) {
|
|
40886
|
-
const out = { help: false };
|
|
42469
|
+
const out = { exitOnCompletion: false, help: false };
|
|
40887
42470
|
for (let i = 0; i < argv.length; i++) {
|
|
40888
42471
|
const a = argv[i];
|
|
40889
42472
|
if (a === "--help" || a === "-h") {
|
|
@@ -40898,13 +42481,17 @@ function parseArgs2(argv) {
|
|
|
40898
42481
|
out.mode = v;
|
|
40899
42482
|
} else if (a === "--storage" && argv[i + 1]) {
|
|
40900
42483
|
out.storage = argv[++i];
|
|
42484
|
+
} else if (a === "--fortress" && argv[i + 1]) {
|
|
42485
|
+
out.fortress = argv[++i];
|
|
42486
|
+
} else if (a === "--exit-on-completion") {
|
|
42487
|
+
out.exitOnCompletion = true;
|
|
40901
42488
|
} else if (a && a.startsWith("--")) {
|
|
40902
42489
|
throw new Error(`Unknown flag: ${a}`);
|
|
40903
42490
|
}
|
|
40904
42491
|
}
|
|
40905
42492
|
return out;
|
|
40906
42493
|
}
|
|
40907
|
-
function
|
|
42494
|
+
function printUsage5(out) {
|
|
40908
42495
|
out.write(`
|
|
40909
42496
|
Usage: sanctuary reset-passphrase [options]
|
|
40910
42497
|
|
|
@@ -40927,9 +42514,19 @@ Recover a fortress whose passphrase has been lost or corrupted. Three modes:
|
|
|
40927
42514
|
|
|
40928
42515
|
Options:
|
|
40929
42516
|
--mode <shares|guardian|nuke> Pick a path non-interactively.
|
|
40930
|
-
--
|
|
40931
|
-
|
|
40932
|
-
|
|
42517
|
+
--fortress <path> Override the fortress storage path.
|
|
42518
|
+
Consistent with "sanctuary wrap --fortress".
|
|
42519
|
+
--storage <path> Alias for --fortress.
|
|
42520
|
+
--exit-on-completion After a successful nuke, call process.exit(0)
|
|
42521
|
+
immediately so the post-wipe heap is reaped
|
|
42522
|
+
by the OS without re-entering the shell. Use
|
|
42523
|
+
on extreme-threat-model deployments where an
|
|
42524
|
+
attacker-on-host with heap-dump access could
|
|
42525
|
+
recover residual passphrase or key bytes
|
|
42526
|
+
between the wipe and the next operator
|
|
42527
|
+
command. JS strings cannot be explicitly
|
|
42528
|
+
zeroed; this flag is the supported way to
|
|
42529
|
+
bound the heap-dump window.
|
|
40933
42530
|
--help, -h Show this help.
|
|
40934
42531
|
|
|
40935
42532
|
Without --mode, the command surveys which paths are operationally available
|
|
@@ -41199,8 +42796,18 @@ async function prompt(lines, err, question) {
|
|
|
41199
42796
|
err.write(question);
|
|
41200
42797
|
return await lines.next();
|
|
41201
42798
|
}
|
|
42799
|
+
function zeroizeBuffers(buffers) {
|
|
42800
|
+
if (!buffers) return;
|
|
42801
|
+
for (const b of buffers) {
|
|
42802
|
+
if (!b) continue;
|
|
42803
|
+
try {
|
|
42804
|
+
b.fill(0);
|
|
42805
|
+
} catch {
|
|
42806
|
+
}
|
|
42807
|
+
}
|
|
42808
|
+
}
|
|
41202
42809
|
async function defaultExec2(cmd, args) {
|
|
41203
|
-
return await new Promise((
|
|
42810
|
+
return await new Promise((resolve8, reject) => {
|
|
41204
42811
|
const child = child_process.spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
41205
42812
|
let stdout = "";
|
|
41206
42813
|
let stderr = "";
|
|
@@ -41211,7 +42818,7 @@ async function defaultExec2(cmd, args) {
|
|
|
41211
42818
|
stderr += d.toString();
|
|
41212
42819
|
});
|
|
41213
42820
|
child.on("error", reject);
|
|
41214
|
-
child.on("close", (code) =>
|
|
42821
|
+
child.on("close", (code) => resolve8({ stdout, stderr, code }));
|
|
41215
42822
|
});
|
|
41216
42823
|
}
|
|
41217
42824
|
var LineReader;
|
|
@@ -41247,8 +42854,8 @@ var init_reset_passphrase = __esm({
|
|
|
41247
42854
|
return Promise.resolve(this.queue.shift());
|
|
41248
42855
|
}
|
|
41249
42856
|
if (this.closed) return Promise.resolve("");
|
|
41250
|
-
return new Promise((
|
|
41251
|
-
this.waiters.push(
|
|
42857
|
+
return new Promise((resolve8) => {
|
|
42858
|
+
this.waiters.push(resolve8);
|
|
41252
42859
|
});
|
|
41253
42860
|
}
|
|
41254
42861
|
close() {
|
|
@@ -41647,11 +43254,11 @@ async function startMultiDashboardServer(options = {}) {
|
|
|
41647
43254
|
}
|
|
41648
43255
|
}
|
|
41649
43256
|
});
|
|
41650
|
-
await new Promise((
|
|
43257
|
+
await new Promise((resolve8, reject) => {
|
|
41651
43258
|
server.once("error", reject);
|
|
41652
43259
|
server.listen(port, host, () => {
|
|
41653
43260
|
server.off("error", reject);
|
|
41654
|
-
|
|
43261
|
+
resolve8();
|
|
41655
43262
|
});
|
|
41656
43263
|
});
|
|
41657
43264
|
const addr = server.address();
|
|
@@ -41660,8 +43267,8 @@ async function startMultiDashboardServer(options = {}) {
|
|
|
41660
43267
|
url: `http://${host}:${actualPort}`,
|
|
41661
43268
|
port: actualPort,
|
|
41662
43269
|
host,
|
|
41663
|
-
stop: () => new Promise((
|
|
41664
|
-
server.close((err) => err ? reject(err) :
|
|
43270
|
+
stop: () => new Promise((resolve8, reject) => {
|
|
43271
|
+
server.close((err) => err ? reject(err) : resolve8());
|
|
41665
43272
|
})
|
|
41666
43273
|
};
|
|
41667
43274
|
}
|
|
@@ -41879,7 +43486,19 @@ Refusing to start the dashboard while the reset-history marker is unreadable.`
|
|
|
41879
43486
|
}
|
|
41880
43487
|
throw err;
|
|
41881
43488
|
}
|
|
41882
|
-
|
|
43489
|
+
let policy;
|
|
43490
|
+
try {
|
|
43491
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
43492
|
+
} catch (err) {
|
|
43493
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
43494
|
+
console.error(`
|
|
43495
|
+
Sanctuary cannot start.
|
|
43496
|
+
${err.message}
|
|
43497
|
+
`);
|
|
43498
|
+
process.exit(1);
|
|
43499
|
+
}
|
|
43500
|
+
throw err;
|
|
43501
|
+
}
|
|
41883
43502
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
41884
43503
|
await baseline.load();
|
|
41885
43504
|
const dashboardPort = options.port ?? config.dashboard.port;
|
|
@@ -42061,7 +43680,7 @@ function formatUpdateMessage(current, latest) {
|
|
|
42061
43680
|
return `[Sanctuary] Update available: ${current} \u2192 ${latest}. Run: npx @sanctuary-framework/mcp-server@latest`;
|
|
42062
43681
|
}
|
|
42063
43682
|
function fetchLatestVersion(currentVersion) {
|
|
42064
|
-
return new Promise((
|
|
43683
|
+
return new Promise((resolve8) => {
|
|
42065
43684
|
const req = https.get(
|
|
42066
43685
|
REGISTRY_URL,
|
|
42067
43686
|
{
|
|
@@ -42071,7 +43690,7 @@ function fetchLatestVersion(currentVersion) {
|
|
|
42071
43690
|
(res) => {
|
|
42072
43691
|
if (res.statusCode !== 200) {
|
|
42073
43692
|
res.resume();
|
|
42074
|
-
|
|
43693
|
+
resolve8(null);
|
|
42075
43694
|
return;
|
|
42076
43695
|
}
|
|
42077
43696
|
let data = "";
|
|
@@ -42080,7 +43699,7 @@ function fetchLatestVersion(currentVersion) {
|
|
|
42080
43699
|
data += chunk;
|
|
42081
43700
|
if (data.length > 32768) {
|
|
42082
43701
|
res.destroy();
|
|
42083
|
-
|
|
43702
|
+
resolve8(null);
|
|
42084
43703
|
}
|
|
42085
43704
|
});
|
|
42086
43705
|
res.on("end", () => {
|
|
@@ -42088,20 +43707,20 @@ function fetchLatestVersion(currentVersion) {
|
|
|
42088
43707
|
const json = JSON.parse(data);
|
|
42089
43708
|
const latest = json.version;
|
|
42090
43709
|
if (typeof latest === "string" && isNewerVersion(currentVersion, latest)) {
|
|
42091
|
-
|
|
43710
|
+
resolve8(latest);
|
|
42092
43711
|
} else {
|
|
42093
|
-
|
|
43712
|
+
resolve8(null);
|
|
42094
43713
|
}
|
|
42095
43714
|
} catch {
|
|
42096
|
-
|
|
43715
|
+
resolve8(null);
|
|
42097
43716
|
}
|
|
42098
43717
|
});
|
|
42099
43718
|
}
|
|
42100
43719
|
);
|
|
42101
|
-
req.on("error", () =>
|
|
43720
|
+
req.on("error", () => resolve8(null));
|
|
42102
43721
|
req.on("timeout", () => {
|
|
42103
43722
|
req.destroy();
|
|
42104
|
-
|
|
43723
|
+
resolve8(null);
|
|
42105
43724
|
});
|
|
42106
43725
|
});
|
|
42107
43726
|
}
|
|
@@ -42179,6 +43798,11 @@ async function main() {
|
|
|
42179
43798
|
const code = await runTemplateCommand2({ argv: args.slice(1) });
|
|
42180
43799
|
process.exit(code);
|
|
42181
43800
|
}
|
|
43801
|
+
if (args[0] === "identity") {
|
|
43802
|
+
const { runIdentityCommand: runIdentityCommand2 } = await Promise.resolve().then(() => (init_identity2(), identity_exports2));
|
|
43803
|
+
const code = await runIdentityCommand2({ argv: args.slice(1) });
|
|
43804
|
+
process.exit(code);
|
|
43805
|
+
}
|
|
42182
43806
|
if (args[0] === "agents") {
|
|
42183
43807
|
const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
42184
43808
|
const code = await runAgentsCommand2({ argv: args.slice(1) });
|
|
@@ -42400,6 +44024,9 @@ Subcommands:
|
|
|
42400
44024
|
Use "sanctuary dashboard --help" for options.
|
|
42401
44025
|
Pass --multi to render the multi-tenant overview.
|
|
42402
44026
|
|
|
44027
|
+
identity Inspect the active identity (DID, public key).
|
|
44028
|
+
Use "sanctuary identity --help" for options.
|
|
44029
|
+
|
|
42403
44030
|
template Manage policy templates (list, init).
|
|
42404
44031
|
Use "sanctuary template --help" for options.
|
|
42405
44032
|
|