@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.js
CHANGED
|
@@ -4421,13 +4421,23 @@ function parseScalar(value) {
|
|
|
4421
4421
|
return value.replace(/^["']|["']$/g, "");
|
|
4422
4422
|
}
|
|
4423
4423
|
function validatePolicy(raw) {
|
|
4424
|
+
if (!("tier1_always_approve" in raw)) {
|
|
4425
|
+
throw new Error(
|
|
4426
|
+
"Policy file must include 'tier1_always_approve' as an explicit list (use [] for empty). Remove specific entries instead of removing the whole key."
|
|
4427
|
+
);
|
|
4428
|
+
}
|
|
4429
|
+
if (!("approval_channel" in raw)) {
|
|
4430
|
+
throw new Error(
|
|
4431
|
+
"Policy file must include 'approval_channel' as an explicit object (use {} for defaults). Remove specific entries instead of removing the whole key."
|
|
4432
|
+
);
|
|
4433
|
+
}
|
|
4424
4434
|
const userTier3 = raw.tier3_always_allow ?? [];
|
|
4425
4435
|
const mergedTier3 = [
|
|
4426
4436
|
.../* @__PURE__ */ new Set([...userTier3, ...DEFAULT_POLICY.tier3_always_allow])
|
|
4427
4437
|
];
|
|
4428
4438
|
return {
|
|
4429
4439
|
version: raw.version ?? 1,
|
|
4430
|
-
tier1_always_approve: raw.tier1_always_approve
|
|
4440
|
+
tier1_always_approve: raw.tier1_always_approve,
|
|
4431
4441
|
tier2_anomaly: {
|
|
4432
4442
|
...DEFAULT_TIER2,
|
|
4433
4443
|
...raw.tier2_anomaly ?? {}
|
|
@@ -4448,6 +4458,11 @@ function generateDefaultPolicyYaml() {
|
|
|
4448
4458
|
# This file controls what your agent can do without asking.
|
|
4449
4459
|
# Edit this file directly. Your agent cannot modify it.
|
|
4450
4460
|
# Changes take effect on server restart.
|
|
4461
|
+
#
|
|
4462
|
+
# Required keys (must be present; use [] or {} for empty):
|
|
4463
|
+
# tier1_always_approve, approval_channel
|
|
4464
|
+
# Optional keys (omit to use defaults; new defaults merge automatically):
|
|
4465
|
+
# tier2_anomaly, tier3_always_allow
|
|
4451
4466
|
|
|
4452
4467
|
version: 1
|
|
4453
4468
|
|
|
@@ -4554,21 +4569,39 @@ approval_channel:
|
|
|
4554
4569
|
}
|
|
4555
4570
|
async function loadPrincipalPolicy(storagePath) {
|
|
4556
4571
|
const policyPath = join(storagePath, "principal-policy.yaml");
|
|
4572
|
+
let content;
|
|
4573
|
+
try {
|
|
4574
|
+
content = await readFile(policyPath, "utf-8");
|
|
4575
|
+
} catch (err) {
|
|
4576
|
+
const code = err?.code;
|
|
4577
|
+
if (code === "ENOENT") {
|
|
4578
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
4579
|
+
try {
|
|
4580
|
+
await writeFile(policyPath, defaultYaml, "utf-8");
|
|
4581
|
+
await chmod(policyPath, 384);
|
|
4582
|
+
} catch (writeErr) {
|
|
4583
|
+
console.warn(
|
|
4584
|
+
`Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
|
|
4585
|
+
);
|
|
4586
|
+
}
|
|
4587
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4588
|
+
}
|
|
4589
|
+
throw new MalformedPrincipalPolicyError(
|
|
4590
|
+
policyPath,
|
|
4591
|
+
`read failed: ${err.message}`
|
|
4592
|
+
);
|
|
4593
|
+
}
|
|
4557
4594
|
try {
|
|
4558
|
-
const content = await readFile(policyPath, "utf-8");
|
|
4559
4595
|
const policy = parsePolicy(content);
|
|
4560
4596
|
return Object.freeze(policy);
|
|
4561
|
-
} catch {
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
} catch {
|
|
4567
|
-
}
|
|
4568
|
-
return Object.freeze({ ...DEFAULT_POLICY });
|
|
4597
|
+
} catch (parseErr) {
|
|
4598
|
+
throw new MalformedPrincipalPolicyError(
|
|
4599
|
+
policyPath,
|
|
4600
|
+
parseErr.message
|
|
4601
|
+
);
|
|
4569
4602
|
}
|
|
4570
4603
|
}
|
|
4571
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY;
|
|
4604
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4572
4605
|
var init_loader = __esm({
|
|
4573
4606
|
"src/principal-policy/loader.ts"() {
|
|
4574
4607
|
DEFAULT_TIER2 = {
|
|
@@ -4701,6 +4734,20 @@ var init_loader = __esm({
|
|
|
4701
4734
|
],
|
|
4702
4735
|
approval_channel: DEFAULT_CHANNEL
|
|
4703
4736
|
};
|
|
4737
|
+
MalformedPrincipalPolicyError = class extends Error {
|
|
4738
|
+
constructor(policyPath, reason) {
|
|
4739
|
+
super(
|
|
4740
|
+
`Principal policy at ${policyPath} is malformed and cannot be loaded.
|
|
4741
|
+
Reason: ${reason}
|
|
4742
|
+
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.`
|
|
4743
|
+
);
|
|
4744
|
+
this.policyPath = policyPath;
|
|
4745
|
+
this.reason = reason;
|
|
4746
|
+
this.name = "MalformedPrincipalPolicyError";
|
|
4747
|
+
}
|
|
4748
|
+
policyPath;
|
|
4749
|
+
reason;
|
|
4750
|
+
};
|
|
4704
4751
|
}
|
|
4705
4752
|
});
|
|
4706
4753
|
|
|
@@ -4920,7 +4967,7 @@ function deepSortKeys(obj) {
|
|
|
4920
4967
|
return sorted;
|
|
4921
4968
|
}
|
|
4922
4969
|
function canonicalizeForSigning(body) {
|
|
4923
|
-
return JSON.stringify(deepSortKeys(body));
|
|
4970
|
+
return JSON.stringify(deepSortKeys(body)).normalize("NFC");
|
|
4924
4971
|
}
|
|
4925
4972
|
var init_types = __esm({
|
|
4926
4973
|
"src/shr/types.ts"() {
|
|
@@ -11567,6 +11614,7 @@ __export(passphrase_exports, {
|
|
|
11567
11614
|
getOrCreatePassphrase: () => getOrCreatePassphrase,
|
|
11568
11615
|
isOsKeyringLocation: () => isOsKeyringLocation,
|
|
11569
11616
|
keychainServiceFor: () => keychainServiceFor,
|
|
11617
|
+
legacyKeychainServiceFor: () => legacyKeychainServiceFor,
|
|
11570
11618
|
persistUserProvidedPassphrase: () => persistUserProvidedPassphrase,
|
|
11571
11619
|
readStoredPassphrase: () => readStoredPassphrase
|
|
11572
11620
|
});
|
|
@@ -11580,16 +11628,29 @@ async function getOrCreatePassphrase(opts = {}) {
|
|
|
11580
11628
|
const plat = opts.platformOverride ?? platform();
|
|
11581
11629
|
const exec2 = opts.exec ?? defaultExec;
|
|
11582
11630
|
const derive = opts.deriveMachineKey ?? deriveMachineKey;
|
|
11631
|
+
const legacyService = legacyKeychainServiceFor(storagePath, home);
|
|
11583
11632
|
if (plat === "darwin") {
|
|
11584
11633
|
const fromKc = await readFromKeychain(exec2, service);
|
|
11585
11634
|
if (fromKc) {
|
|
11586
11635
|
return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11587
11636
|
}
|
|
11637
|
+
if (legacyService !== service) {
|
|
11638
|
+
const fromLegacy = await readFromKeychain(exec2, legacyService);
|
|
11639
|
+
if (fromLegacy) {
|
|
11640
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11641
|
+
}
|
|
11642
|
+
}
|
|
11588
11643
|
} else if (plat === "linux") {
|
|
11589
11644
|
const fromSs = await readFromSecretService(exec2, service);
|
|
11590
11645
|
if (fromSs) {
|
|
11591
11646
|
return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11592
11647
|
}
|
|
11648
|
+
if (legacyService !== service) {
|
|
11649
|
+
const fromLegacy = await readFromSecretService(exec2, legacyService);
|
|
11650
|
+
if (fromLegacy) {
|
|
11651
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11652
|
+
}
|
|
11653
|
+
}
|
|
11593
11654
|
}
|
|
11594
11655
|
const fallback = fallbackFilePath(home, storagePath);
|
|
11595
11656
|
const fromFile = await readFromFallbackFile(fallback, home, derive);
|
|
@@ -11622,6 +11683,7 @@ async function readStoredPassphrase(opts = {}) {
|
|
|
11622
11683
|
const home = opts.home ?? homedir();
|
|
11623
11684
|
const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
|
|
11624
11685
|
const service = keychainServiceFor(storagePath, home);
|
|
11686
|
+
const legacyService = legacyKeychainServiceFor(storagePath, home);
|
|
11625
11687
|
const plat = opts.platformOverride ?? platform();
|
|
11626
11688
|
const exec2 = opts.exec ?? defaultExec;
|
|
11627
11689
|
const derive = opts.deriveMachineKey ?? deriveMachineKey;
|
|
@@ -11630,11 +11692,23 @@ async function readStoredPassphrase(opts = {}) {
|
|
|
11630
11692
|
if (fromKc) {
|
|
11631
11693
|
return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11632
11694
|
}
|
|
11695
|
+
if (legacyService !== service) {
|
|
11696
|
+
const fromLegacy = await readFromKeychain(exec2, legacyService);
|
|
11697
|
+
if (fromLegacy) {
|
|
11698
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
|
|
11699
|
+
}
|
|
11700
|
+
}
|
|
11633
11701
|
} else if (plat === "linux") {
|
|
11634
11702
|
const fromSs = await readFromSecretService(exec2, service);
|
|
11635
11703
|
if (fromSs) {
|
|
11636
11704
|
return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11637
11705
|
}
|
|
11706
|
+
if (legacyService !== service) {
|
|
11707
|
+
const fromLegacy = await readFromSecretService(exec2, legacyService);
|
|
11708
|
+
if (fromLegacy) {
|
|
11709
|
+
return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
|
|
11710
|
+
}
|
|
11711
|
+
}
|
|
11638
11712
|
}
|
|
11639
11713
|
const fallback = fallbackFilePath(home, storagePath);
|
|
11640
11714
|
const fromFile = await readFromFallbackFile(fallback, home, derive);
|
|
@@ -11715,9 +11789,18 @@ async function writeToKeychain(value, exec2, service = KEYCHAIN_SERVICE_DEFAULT)
|
|
|
11715
11789
|
}
|
|
11716
11790
|
}
|
|
11717
11791
|
function keychainServiceFor(storagePath, home = homedir()) {
|
|
11718
|
-
const defaultPath = join(home, DEFAULT_STORAGE_DIR);
|
|
11719
|
-
|
|
11720
|
-
|
|
11792
|
+
const defaultPath = resolve(join(home, DEFAULT_STORAGE_DIR));
|
|
11793
|
+
const canonicalStorage = resolve(storagePath);
|
|
11794
|
+
if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
|
|
11795
|
+
const digest = sha256(Buffer.from(canonicalStorage, "utf-8"));
|
|
11796
|
+
const suffix = Buffer.from(digest).toString("hex").slice(0, 16);
|
|
11797
|
+
return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
|
|
11798
|
+
}
|
|
11799
|
+
function legacyKeychainServiceFor(storagePath, home = homedir()) {
|
|
11800
|
+
const defaultPath = resolve(join(home, DEFAULT_STORAGE_DIR));
|
|
11801
|
+
const canonicalStorage = resolve(storagePath);
|
|
11802
|
+
if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
|
|
11803
|
+
const digest = sha256(Buffer.from(canonicalStorage, "utf-8"));
|
|
11721
11804
|
const suffix = Buffer.from(digest).toString("hex").slice(0, 12);
|
|
11722
11805
|
return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
|
|
11723
11806
|
}
|
|
@@ -11804,7 +11887,7 @@ function deriveMachineKey(home) {
|
|
|
11804
11887
|
return hkdf(sha256, material, void 0, "sanctuary-passphrase-v1", 32);
|
|
11805
11888
|
}
|
|
11806
11889
|
async function defaultExec(cmd, args, input) {
|
|
11807
|
-
return new Promise((
|
|
11890
|
+
return new Promise((resolve8, reject) => {
|
|
11808
11891
|
const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
11809
11892
|
let stdout = "";
|
|
11810
11893
|
let stderr = "";
|
|
@@ -11815,7 +11898,7 @@ async function defaultExec(cmd, args, input) {
|
|
|
11815
11898
|
stderr += d.toString();
|
|
11816
11899
|
});
|
|
11817
11900
|
child.on("error", reject);
|
|
11818
|
-
child.on("close", (code) =>
|
|
11901
|
+
child.on("close", (code) => resolve8({ stdout, stderr, code }));
|
|
11819
11902
|
if (input !== void 0) {
|
|
11820
11903
|
child.stdin.write(input);
|
|
11821
11904
|
}
|
|
@@ -12009,7 +12092,7 @@ async function discoverTenants(options = {}) {
|
|
|
12009
12092
|
for (const child of children) {
|
|
12010
12093
|
const childPath = join(root, child);
|
|
12011
12094
|
if (child.startsWith(".")) continue;
|
|
12012
|
-
if (child === "state" || child === "backup" || child === "config") continue;
|
|
12095
|
+
if (child === "state" || child === "backup" || child === "config" || child === "default") continue;
|
|
12013
12096
|
const s = await stat(childPath).catch(() => null);
|
|
12014
12097
|
if (!s || !s.isDirectory()) continue;
|
|
12015
12098
|
const desc = await describeTenant(child, childPath, home);
|
|
@@ -12021,6 +12104,17 @@ async function discoverTenants(options = {}) {
|
|
|
12021
12104
|
const desc = await describeTenant(basename(extra), extra, home);
|
|
12022
12105
|
if (desc) tenants.push(desc);
|
|
12023
12106
|
}
|
|
12107
|
+
const seen = /* @__PURE__ */ new Map();
|
|
12108
|
+
for (const t of tenants) {
|
|
12109
|
+
seen.set(t.name, (seen.get(t.name) ?? 0) + 1);
|
|
12110
|
+
}
|
|
12111
|
+
for (const [name, count] of seen) {
|
|
12112
|
+
if (count > 1) {
|
|
12113
|
+
console.error(
|
|
12114
|
+
`[sanctuary] warning: ${count} tenants share the name "${name}". Use --tenant with a unique name or storage path to disambiguate.`
|
|
12115
|
+
);
|
|
12116
|
+
}
|
|
12117
|
+
}
|
|
12024
12118
|
tenants.sort((a, b) => {
|
|
12025
12119
|
if (a.name === "default") return -1;
|
|
12026
12120
|
if (b.name === "default") return 1;
|
|
@@ -12372,7 +12466,7 @@ var init_auth_middleware = __esm({
|
|
|
12372
12466
|
});
|
|
12373
12467
|
|
|
12374
12468
|
// src/hub/constants.ts
|
|
12375
|
-
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;
|
|
12469
|
+
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;
|
|
12376
12470
|
var init_constants3 = __esm({
|
|
12377
12471
|
"src/hub/constants.ts"() {
|
|
12378
12472
|
HUB_API_PREFIX = "/api/hub";
|
|
@@ -12400,6 +12494,16 @@ var init_constants3 = __esm({
|
|
|
12400
12494
|
*/
|
|
12401
12495
|
CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
|
|
12402
12496
|
CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
|
|
12497
|
+
/**
|
|
12498
|
+
* Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
|
|
12499
|
+
* scrollback, and operator-initiated thread delete. Distinct from the
|
|
12500
|
+
* v1.2 `/history` route, which surfaces the active in-session thread
|
|
12501
|
+
* shape; the new routes target persisted multi-thread memory used by
|
|
12502
|
+
* v1.3 conversational sovereignty depth.
|
|
12503
|
+
*/
|
|
12504
|
+
CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
|
|
12505
|
+
CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12506
|
+
CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
|
|
12403
12507
|
/**
|
|
12404
12508
|
* Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
|
|
12405
12509
|
* recent activity feed, pending Tier 1 approvals routed through this
|
|
@@ -12423,6 +12527,10 @@ var init_constants3 = __esm({
|
|
|
12423
12527
|
];
|
|
12424
12528
|
HUB_ACTIVITY_DEFAULT_LIMIT = 50;
|
|
12425
12529
|
HUB_ACTIVITY_MAX_LIMIT = 500;
|
|
12530
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
|
|
12531
|
+
HUB_CHAT_THREADS_MAX_LIMIT = 500;
|
|
12532
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
|
|
12533
|
+
HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
|
|
12426
12534
|
HUB_INBOX_DEFAULT_LIMIT = 100;
|
|
12427
12535
|
HUB_INBOX_MAX_LIMIT = 500;
|
|
12428
12536
|
HUB_AGENTS_DEFAULT_LIMIT = 100;
|
|
@@ -12605,6 +12713,23 @@ function checkChatMessage(value) {
|
|
|
12605
12713
|
}
|
|
12606
12714
|
return trimmed;
|
|
12607
12715
|
}
|
|
12716
|
+
function matchConciergeThreadRoute(path) {
|
|
12717
|
+
const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
|
|
12718
|
+
if (!path.startsWith(prefix)) return null;
|
|
12719
|
+
const rest = path.slice(prefix.length);
|
|
12720
|
+
if (rest.length === 0 || rest.includes("/")) return null;
|
|
12721
|
+
const decoded = decodeURIComponent(rest);
|
|
12722
|
+
if (decoded.length === 0) return null;
|
|
12723
|
+
return { threadId: decoded };
|
|
12724
|
+
}
|
|
12725
|
+
function parseSince(raw) {
|
|
12726
|
+
if (raw === null || raw === "") return void 0;
|
|
12727
|
+
const parsed = Number.parseInt(raw, 10);
|
|
12728
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
12729
|
+
throw new HubValidationError("since must be a non-negative integer");
|
|
12730
|
+
}
|
|
12731
|
+
return parsed;
|
|
12732
|
+
}
|
|
12608
12733
|
function matchInboxRoute(path) {
|
|
12609
12734
|
const prefix = `${HUB_API_PREFIX}/inbox/`;
|
|
12610
12735
|
if (!path.startsWith(prefix)) return null;
|
|
@@ -12788,6 +12913,47 @@ async function handleHubRoute(deps, req, res) {
|
|
|
12788
12913
|
writeJSON2(res, 200, { ok: true, data: { messages } });
|
|
12789
12914
|
return true;
|
|
12790
12915
|
}
|
|
12916
|
+
if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
|
|
12917
|
+
const limit = parseLimit(
|
|
12918
|
+
url.searchParams.get("limit"),
|
|
12919
|
+
HUB_CHAT_THREADS_DEFAULT_LIMIT,
|
|
12920
|
+
HUB_CHAT_THREADS_MAX_LIMIT
|
|
12921
|
+
);
|
|
12922
|
+
const threads = await deps.service.listConciergeMemoryThreads({ limit });
|
|
12923
|
+
writeJSON2(res, 200, { ok: true, data: { threads } });
|
|
12924
|
+
return true;
|
|
12925
|
+
}
|
|
12926
|
+
{
|
|
12927
|
+
const threadMatch = matchConciergeThreadRoute(path);
|
|
12928
|
+
if (threadMatch) {
|
|
12929
|
+
if (method === "GET") {
|
|
12930
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
12931
|
+
const limit = parseLimit(
|
|
12932
|
+
url.searchParams.get("limit"),
|
|
12933
|
+
HUB_CHAT_TURNS_DEFAULT_LIMIT,
|
|
12934
|
+
HUB_CHAT_TURNS_MAX_LIMIT
|
|
12935
|
+
);
|
|
12936
|
+
const readOpts = { limit };
|
|
12937
|
+
if (since !== void 0) readOpts.sinceTurnId = since;
|
|
12938
|
+
const turns = await deps.service.readConciergeMemoryThread(
|
|
12939
|
+
threadMatch.threadId,
|
|
12940
|
+
readOpts
|
|
12941
|
+
);
|
|
12942
|
+
writeJSON2(res, 200, { ok: true, data: { turns } });
|
|
12943
|
+
return true;
|
|
12944
|
+
}
|
|
12945
|
+
if (method === "DELETE") {
|
|
12946
|
+
const removed = await deps.service.deleteConciergeMemoryThread(
|
|
12947
|
+
threadMatch.threadId
|
|
12948
|
+
);
|
|
12949
|
+
writeJSON2(res, removed ? 200 : 404, {
|
|
12950
|
+
ok: removed,
|
|
12951
|
+
data: { thread_id: threadMatch.threadId, removed }
|
|
12952
|
+
});
|
|
12953
|
+
return true;
|
|
12954
|
+
}
|
|
12955
|
+
}
|
|
12956
|
+
}
|
|
12791
12957
|
writeJSON2(res, 404, { ok: false, error: "not_found", path });
|
|
12792
12958
|
return true;
|
|
12793
12959
|
} catch (err) {
|
|
@@ -16819,6 +16985,8 @@ var init_intelligence_api_router = __esm({
|
|
|
16819
16985
|
this.code = code;
|
|
16820
16986
|
this.name = "IntelligenceRouterError";
|
|
16821
16987
|
}
|
|
16988
|
+
statusCode;
|
|
16989
|
+
code;
|
|
16822
16990
|
};
|
|
16823
16991
|
}
|
|
16824
16992
|
});
|
|
@@ -16908,6 +17076,168 @@ var init_dispatch = __esm({
|
|
|
16908
17076
|
init_intelligence_api_router();
|
|
16909
17077
|
}
|
|
16910
17078
|
});
|
|
17079
|
+
|
|
17080
|
+
// src/principal-policy/approval-aggregator-routes.ts
|
|
17081
|
+
function writeJSON4(res, status, payload) {
|
|
17082
|
+
res.writeHead(status, {
|
|
17083
|
+
"Content-Type": "application/json",
|
|
17084
|
+
"Cache-Control": "no-store"
|
|
17085
|
+
});
|
|
17086
|
+
res.end(JSON.stringify(payload));
|
|
17087
|
+
}
|
|
17088
|
+
function parseLimit2(raw, defaultValue, max) {
|
|
17089
|
+
if (raw === null || raw === "") return defaultValue;
|
|
17090
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17091
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
17092
|
+
return defaultValue;
|
|
17093
|
+
}
|
|
17094
|
+
return Math.min(parsed, max);
|
|
17095
|
+
}
|
|
17096
|
+
function isStatusFilter(value) {
|
|
17097
|
+
return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
|
|
17098
|
+
}
|
|
17099
|
+
function matchEntryRoute(path) {
|
|
17100
|
+
const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
|
|
17101
|
+
if (!path.startsWith(prefix)) return null;
|
|
17102
|
+
const rest = path.slice(prefix.length);
|
|
17103
|
+
if (rest.length === 0) return null;
|
|
17104
|
+
const slash = rest.indexOf("/");
|
|
17105
|
+
if (slash === -1) {
|
|
17106
|
+
return { aggregatorId: decodeURIComponent(rest), action: null };
|
|
17107
|
+
}
|
|
17108
|
+
return {
|
|
17109
|
+
aggregatorId: decodeURIComponent(rest.slice(0, slash)),
|
|
17110
|
+
action: rest.slice(slash + 1)
|
|
17111
|
+
};
|
|
17112
|
+
}
|
|
17113
|
+
async function handleStream2(deps, res) {
|
|
17114
|
+
res.writeHead(200, {
|
|
17115
|
+
"Content-Type": "text/event-stream",
|
|
17116
|
+
"Cache-Control": "no-cache, no-transform",
|
|
17117
|
+
Connection: "keep-alive",
|
|
17118
|
+
"X-Accel-Buffering": "no"
|
|
17119
|
+
});
|
|
17120
|
+
const initial = await deps.aggregator.list({ status: "pending" });
|
|
17121
|
+
res.write(
|
|
17122
|
+
`event: approval_inbox_snapshot
|
|
17123
|
+
data: ${JSON.stringify({ entries: initial })}
|
|
17124
|
+
|
|
17125
|
+
`
|
|
17126
|
+
);
|
|
17127
|
+
const unsubscribe = deps.aggregator.onEvent((event) => {
|
|
17128
|
+
try {
|
|
17129
|
+
res.write(
|
|
17130
|
+
`event: approval_inbox_${event.type}
|
|
17131
|
+
data: ${JSON.stringify(event.entry)}
|
|
17132
|
+
|
|
17133
|
+
`
|
|
17134
|
+
);
|
|
17135
|
+
} catch {
|
|
17136
|
+
}
|
|
17137
|
+
});
|
|
17138
|
+
const keepAlive = setInterval(() => {
|
|
17139
|
+
try {
|
|
17140
|
+
res.write(": keepalive\n\n");
|
|
17141
|
+
} catch {
|
|
17142
|
+
}
|
|
17143
|
+
}, 25e3);
|
|
17144
|
+
const cleanup = () => {
|
|
17145
|
+
clearInterval(keepAlive);
|
|
17146
|
+
unsubscribe();
|
|
17147
|
+
};
|
|
17148
|
+
res.on("close", cleanup);
|
|
17149
|
+
res.on("error", cleanup);
|
|
17150
|
+
}
|
|
17151
|
+
async function handleApprovalInboxRoute(deps, req, res) {
|
|
17152
|
+
const host = req.headers.host || "localhost";
|
|
17153
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
17154
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
17155
|
+
const path = url.pathname;
|
|
17156
|
+
if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
|
|
17157
|
+
return false;
|
|
17158
|
+
}
|
|
17159
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
17160
|
+
if (!checkAuth(req, res, url)) return true;
|
|
17161
|
+
try {
|
|
17162
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
|
|
17163
|
+
await handleStream2(deps, res);
|
|
17164
|
+
return true;
|
|
17165
|
+
}
|
|
17166
|
+
if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
|
|
17167
|
+
const limit = parseLimit2(
|
|
17168
|
+
url.searchParams.get("limit"),
|
|
17169
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
17170
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
17171
|
+
);
|
|
17172
|
+
const statusRaw = url.searchParams.get("status");
|
|
17173
|
+
const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
|
|
17174
|
+
const sinceTs = url.searchParams.get("since") ?? void 0;
|
|
17175
|
+
const entries = await deps.aggregator.list({
|
|
17176
|
+
status,
|
|
17177
|
+
limit,
|
|
17178
|
+
...sinceTs !== void 0 ? { sinceTs } : {}
|
|
17179
|
+
});
|
|
17180
|
+
writeJSON4(res, 200, { ok: true, data: { entries } });
|
|
17181
|
+
return true;
|
|
17182
|
+
}
|
|
17183
|
+
const entryMatch = matchEntryRoute(path);
|
|
17184
|
+
if (entryMatch === null) {
|
|
17185
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17186
|
+
return true;
|
|
17187
|
+
}
|
|
17188
|
+
if (method === "GET" && entryMatch.action === null) {
|
|
17189
|
+
const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
|
|
17190
|
+
const entry = entries.find(
|
|
17191
|
+
(e) => e.aggregator_id === entryMatch.aggregatorId
|
|
17192
|
+
);
|
|
17193
|
+
if (!entry) {
|
|
17194
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17195
|
+
return true;
|
|
17196
|
+
}
|
|
17197
|
+
const payload = await deps.aggregator.getFullPayload(
|
|
17198
|
+
entryMatch.aggregatorId
|
|
17199
|
+
);
|
|
17200
|
+
writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
|
|
17201
|
+
return true;
|
|
17202
|
+
}
|
|
17203
|
+
if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
|
|
17204
|
+
const decision = entryMatch.action === "approve" ? "approved" : "denied";
|
|
17205
|
+
const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
|
|
17206
|
+
try {
|
|
17207
|
+
const entry = await deps.aggregator.resolve(
|
|
17208
|
+
entryMatch.aggregatorId,
|
|
17209
|
+
decision,
|
|
17210
|
+
operatorId
|
|
17211
|
+
);
|
|
17212
|
+
writeJSON4(res, 200, { ok: true, data: { entry } });
|
|
17213
|
+
} catch (err) {
|
|
17214
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17215
|
+
if (msg === "approval-aggregator: not_found") {
|
|
17216
|
+
writeJSON4(res, 404, { ok: false, error: "not_found" });
|
|
17217
|
+
} else {
|
|
17218
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17219
|
+
}
|
|
17220
|
+
}
|
|
17221
|
+
return true;
|
|
17222
|
+
}
|
|
17223
|
+
writeJSON4(res, 404, { ok: false, error: "not_found", path });
|
|
17224
|
+
return true;
|
|
17225
|
+
} catch (err) {
|
|
17226
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17227
|
+
writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17228
|
+
return true;
|
|
17229
|
+
}
|
|
17230
|
+
}
|
|
17231
|
+
var APPROVAL_INBOX_API_PREFIX, APPROVAL_INBOX_OPERATOR_DEFAULT, APPROVAL_INBOX_DEFAULT_LIMIT, APPROVAL_INBOX_MAX_LIMIT;
|
|
17232
|
+
var init_approval_aggregator_routes = __esm({
|
|
17233
|
+
"src/principal-policy/approval-aggregator-routes.ts"() {
|
|
17234
|
+
init_auth_middleware();
|
|
17235
|
+
APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
|
|
17236
|
+
APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
|
|
17237
|
+
APPROVAL_INBOX_DEFAULT_LIMIT = 50;
|
|
17238
|
+
APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
17239
|
+
}
|
|
17240
|
+
});
|
|
16911
17241
|
function isDashboardViewRoute(method, path) {
|
|
16912
17242
|
if (method !== "GET") return false;
|
|
16913
17243
|
return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
|
|
@@ -16921,6 +17251,7 @@ var init_dashboard = __esm({
|
|
|
16921
17251
|
init_fortress_view();
|
|
16922
17252
|
init_system_prompt_generator();
|
|
16923
17253
|
init_dispatch();
|
|
17254
|
+
init_approval_aggregator_routes();
|
|
16924
17255
|
SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16925
17256
|
SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
16926
17257
|
MAX_SESSIONS = 1e3;
|
|
@@ -16980,6 +17311,14 @@ var init_dashboard = __esm({
|
|
|
16980
17311
|
* regardless. Default route flip is deferred to v1.2.
|
|
16981
17312
|
*/
|
|
16982
17313
|
v11Bindings = null;
|
|
17314
|
+
/**
|
|
17315
|
+
* v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
|
|
17316
|
+
* additively at `/api/approval-inbox/*` when set. Legacy approval
|
|
17317
|
+
* routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
|
|
17318
|
+
* aggregator is a passive subscriber to the gate; the routes here are
|
|
17319
|
+
* the operator-facing query / decision surface.
|
|
17320
|
+
*/
|
|
17321
|
+
approvalAggregator = null;
|
|
16983
17322
|
constructor(config) {
|
|
16984
17323
|
this.config = config;
|
|
16985
17324
|
this.authToken = config.auth_token;
|
|
@@ -17030,6 +17369,34 @@ var init_dashboard = __esm({
|
|
|
17030
17369
|
setV11Bindings(bindings) {
|
|
17031
17370
|
this.v11Bindings = bindings;
|
|
17032
17371
|
}
|
|
17372
|
+
/**
|
|
17373
|
+
* v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
|
|
17374
|
+
* aggregator. Once set, requests to `/api/approval-inbox/*` route
|
|
17375
|
+
* through `handleApprovalInboxRoute`. Pass `null` to detach (used by
|
|
17376
|
+
* tests + during shutdown).
|
|
17377
|
+
*/
|
|
17378
|
+
setApprovalAggregator(aggregator) {
|
|
17379
|
+
this.approvalAggregator = aggregator;
|
|
17380
|
+
}
|
|
17381
|
+
/**
|
|
17382
|
+
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
17383
|
+
* before the legacy approval route table. Returns true when served.
|
|
17384
|
+
*/
|
|
17385
|
+
async dispatchApprovalInbox(req, res) {
|
|
17386
|
+
if (!this.approvalAggregator) return false;
|
|
17387
|
+
return handleApprovalInboxRoute(
|
|
17388
|
+
{
|
|
17389
|
+
authConfig: {
|
|
17390
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
17391
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
17392
|
+
},
|
|
17393
|
+
aggregator: this.approvalAggregator,
|
|
17394
|
+
operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
|
|
17395
|
+
},
|
|
17396
|
+
req,
|
|
17397
|
+
res
|
|
17398
|
+
);
|
|
17399
|
+
}
|
|
17033
17400
|
/**
|
|
17034
17401
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
17035
17402
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -17095,7 +17462,7 @@ var init_dashboard = __esm({
|
|
|
17095
17462
|
server = createServer$2(handler);
|
|
17096
17463
|
}
|
|
17097
17464
|
this.httpServer = server;
|
|
17098
|
-
return new Promise((
|
|
17465
|
+
return new Promise((resolve8, reject) => {
|
|
17099
17466
|
const protocol = this.useTLS ? "https" : "http";
|
|
17100
17467
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
17101
17468
|
server.listen(this.config.port, this.config.host, () => {
|
|
@@ -17120,7 +17487,7 @@ var init_dashboard = __esm({
|
|
|
17120
17487
|
if (shouldAutoOpen) {
|
|
17121
17488
|
this.openInBrowser(sessionUrl);
|
|
17122
17489
|
}
|
|
17123
|
-
|
|
17490
|
+
resolve8();
|
|
17124
17491
|
});
|
|
17125
17492
|
server.on("error", (err) => {
|
|
17126
17493
|
if (err.code === "EADDRINUSE") {
|
|
@@ -17166,8 +17533,8 @@ var init_dashboard = __esm({
|
|
|
17166
17533
|
}
|
|
17167
17534
|
this.rateLimits.clear();
|
|
17168
17535
|
if (this.httpServer) {
|
|
17169
|
-
return new Promise((
|
|
17170
|
-
this.httpServer.close(() =>
|
|
17536
|
+
return new Promise((resolve8) => {
|
|
17537
|
+
this.httpServer.close(() => resolve8());
|
|
17171
17538
|
});
|
|
17172
17539
|
}
|
|
17173
17540
|
}
|
|
@@ -17181,7 +17548,7 @@ var init_dashboard = __esm({
|
|
|
17181
17548
|
`[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
|
|
17182
17549
|
`
|
|
17183
17550
|
);
|
|
17184
|
-
return new Promise((
|
|
17551
|
+
return new Promise((resolve8) => {
|
|
17185
17552
|
const timer = setTimeout(() => {
|
|
17186
17553
|
this.pending.delete(id);
|
|
17187
17554
|
const response = {
|
|
@@ -17195,12 +17562,12 @@ var init_dashboard = __esm({
|
|
|
17195
17562
|
decision: response.decision,
|
|
17196
17563
|
decided_by: "timeout"
|
|
17197
17564
|
});
|
|
17198
|
-
|
|
17565
|
+
resolve8(response);
|
|
17199
17566
|
}, this.config.timeout_seconds * 1e3);
|
|
17200
17567
|
const pending = {
|
|
17201
17568
|
id,
|
|
17202
17569
|
request,
|
|
17203
|
-
resolve:
|
|
17570
|
+
resolve: resolve8,
|
|
17204
17571
|
timer,
|
|
17205
17572
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
17206
17573
|
};
|
|
@@ -17405,6 +17772,18 @@ var init_dashboard = __esm({
|
|
|
17405
17772
|
res.end();
|
|
17406
17773
|
return;
|
|
17407
17774
|
}
|
|
17775
|
+
if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
|
|
17776
|
+
this.dispatchApprovalInbox(req, res).then((handled) => {
|
|
17777
|
+
if (handled) return;
|
|
17778
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
17779
|
+
}).catch(() => {
|
|
17780
|
+
if (!res.headersSent) {
|
|
17781
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
17782
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
17783
|
+
}
|
|
17784
|
+
});
|
|
17785
|
+
return;
|
|
17786
|
+
}
|
|
17408
17787
|
if (this.v11Bindings) {
|
|
17409
17788
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
17410
17789
|
if (handled) return;
|
|
@@ -18066,7 +18445,7 @@ var init_webhook = __esm({
|
|
|
18066
18445
|
* Start the callback listener server.
|
|
18067
18446
|
*/
|
|
18068
18447
|
async start() {
|
|
18069
|
-
return new Promise((
|
|
18448
|
+
return new Promise((resolve8, reject) => {
|
|
18070
18449
|
this.callbackServer = createServer$2(
|
|
18071
18450
|
(req, res) => this.handleCallback(req, res)
|
|
18072
18451
|
);
|
|
@@ -18081,7 +18460,7 @@ var init_webhook = __esm({
|
|
|
18081
18460
|
|
|
18082
18461
|
`
|
|
18083
18462
|
);
|
|
18084
|
-
|
|
18463
|
+
resolve8();
|
|
18085
18464
|
}
|
|
18086
18465
|
);
|
|
18087
18466
|
this.callbackServer.on("error", reject);
|
|
@@ -18101,8 +18480,8 @@ var init_webhook = __esm({
|
|
|
18101
18480
|
}
|
|
18102
18481
|
this.pending.clear();
|
|
18103
18482
|
if (this.callbackServer) {
|
|
18104
|
-
return new Promise((
|
|
18105
|
-
this.callbackServer.close(() =>
|
|
18483
|
+
return new Promise((resolve8) => {
|
|
18484
|
+
this.callbackServer.close(() => resolve8());
|
|
18106
18485
|
});
|
|
18107
18486
|
}
|
|
18108
18487
|
}
|
|
@@ -18115,7 +18494,7 @@ var init_webhook = __esm({
|
|
|
18115
18494
|
`[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
|
|
18116
18495
|
`
|
|
18117
18496
|
);
|
|
18118
|
-
return new Promise((
|
|
18497
|
+
return new Promise((resolve8) => {
|
|
18119
18498
|
const timer = setTimeout(() => {
|
|
18120
18499
|
this.pending.delete(id);
|
|
18121
18500
|
const response = {
|
|
@@ -18124,12 +18503,12 @@ var init_webhook = __esm({
|
|
|
18124
18503
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18125
18504
|
decided_by: "timeout"
|
|
18126
18505
|
};
|
|
18127
|
-
|
|
18506
|
+
resolve8(response);
|
|
18128
18507
|
}, this.config.timeout_seconds * 1e3);
|
|
18129
18508
|
const pending = {
|
|
18130
18509
|
id,
|
|
18131
18510
|
request,
|
|
18132
|
-
resolve:
|
|
18511
|
+
resolve: resolve8,
|
|
18133
18512
|
timer,
|
|
18134
18513
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18135
18514
|
};
|
|
@@ -19422,12 +19801,25 @@ var init_injection_detector = __esm({
|
|
|
19422
19801
|
}
|
|
19423
19802
|
});
|
|
19424
19803
|
|
|
19804
|
+
// src/principal-policy/deny-vocabulary.ts
|
|
19805
|
+
var AGENT_VISIBLE_DENY_REASONS;
|
|
19806
|
+
var init_deny_vocabulary = __esm({
|
|
19807
|
+
"src/principal-policy/deny-vocabulary.ts"() {
|
|
19808
|
+
AGENT_VISIBLE_DENY_REASONS = {
|
|
19809
|
+
REQUIRES_APPROVAL: "operation requires operator approval",
|
|
19810
|
+
NOT_PERMITTED: "operation not permitted",
|
|
19811
|
+
REQUIRES_OPERATOR: "operation requires operator action"
|
|
19812
|
+
};
|
|
19813
|
+
}
|
|
19814
|
+
});
|
|
19815
|
+
|
|
19425
19816
|
// src/principal-policy/gate.ts
|
|
19426
19817
|
var ApprovalGate;
|
|
19427
19818
|
var init_gate = __esm({
|
|
19428
19819
|
"src/principal-policy/gate.ts"() {
|
|
19429
19820
|
init_loader();
|
|
19430
19821
|
init_injection_detector();
|
|
19822
|
+
init_deny_vocabulary();
|
|
19431
19823
|
ApprovalGate = class {
|
|
19432
19824
|
policy;
|
|
19433
19825
|
baseline;
|
|
@@ -19435,14 +19827,25 @@ var init_gate = __esm({
|
|
|
19435
19827
|
auditLog;
|
|
19436
19828
|
injectionDetector;
|
|
19437
19829
|
onInjectionAlert;
|
|
19830
|
+
onApprovalEvent;
|
|
19438
19831
|
proxyTierResolver;
|
|
19439
|
-
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
|
|
19832
|
+
constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
|
|
19440
19833
|
this.policy = policy;
|
|
19441
19834
|
this.baseline = baseline;
|
|
19442
19835
|
this.channel = channel;
|
|
19443
19836
|
this.auditLog = auditLog;
|
|
19444
19837
|
this.injectionDetector = injectionDetector ?? new InjectionDetector();
|
|
19445
19838
|
this.onInjectionAlert = onInjectionAlert;
|
|
19839
|
+
this.onApprovalEvent = onApprovalEvent;
|
|
19840
|
+
}
|
|
19841
|
+
/**
|
|
19842
|
+
* Set the approval-event callback after construction. Used by the
|
|
19843
|
+
* Upsilon-1 wire-up when the aggregator is constructed alongside the
|
|
19844
|
+
* gate. The aggregator subscribes through this setter rather than the
|
|
19845
|
+
* constructor so existing call sites continue to work unchanged.
|
|
19846
|
+
*/
|
|
19847
|
+
setApprovalEventCallback(cb) {
|
|
19848
|
+
this.onApprovalEvent = cb;
|
|
19446
19849
|
}
|
|
19447
19850
|
/**
|
|
19448
19851
|
* Set the proxy tier resolver. Called after the proxy router is initialized.
|
|
@@ -19479,10 +19882,16 @@ var init_gate = __esm({
|
|
|
19479
19882
|
});
|
|
19480
19883
|
}
|
|
19481
19884
|
if (injectionResult.recommendation === "block") {
|
|
19885
|
+
this.auditLog.append("l2", `gate_injection_block:${operation}`, "system", {
|
|
19886
|
+
tier: 1,
|
|
19887
|
+
operation,
|
|
19888
|
+
injection_confidence: injectionResult.confidence,
|
|
19889
|
+
signal_count: injectionResult.signals.length
|
|
19890
|
+
});
|
|
19482
19891
|
return {
|
|
19483
19892
|
allowed: false,
|
|
19484
19893
|
tier: 1,
|
|
19485
|
-
reason:
|
|
19894
|
+
reason: AGENT_VISIBLE_DENY_REASONS.NOT_PERMITTED,
|
|
19486
19895
|
approval_required: false
|
|
19487
19896
|
};
|
|
19488
19897
|
}
|
|
@@ -19559,7 +19968,7 @@ var init_gate = __esm({
|
|
|
19559
19968
|
this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
|
|
19560
19969
|
tier: 1,
|
|
19561
19970
|
operation,
|
|
19562
|
-
warning: "Operation is not classified in any policy tier
|
|
19971
|
+
warning: "Operation is not classified in any policy tier, defaulting to Tier 1 (require approval)"
|
|
19563
19972
|
});
|
|
19564
19973
|
return this.requestApproval(
|
|
19565
19974
|
operation,
|
|
@@ -19670,25 +20079,109 @@ var init_gate = __esm({
|
|
|
19670
20079
|
}
|
|
19671
20080
|
/**
|
|
19672
20081
|
* Request approval from the human principal.
|
|
20082
|
+
*
|
|
20083
|
+
* Fail-closed contract (full-sweep #49): if the channel throws (network
|
|
20084
|
+
* down, callback unreachable, dashboard SSE peer dropped, webhook DNS
|
|
20085
|
+
* failure, etc.), the gate denies the operation and audit-logs the cause.
|
|
20086
|
+
* Channel-internal timeouts already resolve with decision: "deny" per
|
|
20087
|
+
* SEC-002; this catch covers the remaining "channel raised" path so an
|
|
20088
|
+
* unhandled rejection cannot turn into an indeterminate state at the gate.
|
|
19673
20089
|
*/
|
|
19674
20090
|
async requestApproval(operation, tier, reason, context) {
|
|
20091
|
+
const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19675
20092
|
const request = {
|
|
19676
20093
|
operation,
|
|
19677
20094
|
tier,
|
|
19678
20095
|
reason,
|
|
19679
20096
|
context,
|
|
19680
|
-
timestamp:
|
|
20097
|
+
timestamp: requestTimestamp
|
|
19681
20098
|
};
|
|
19682
|
-
const
|
|
20099
|
+
const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
|
|
20100
|
+
if (this.onApprovalEvent) {
|
|
20101
|
+
try {
|
|
20102
|
+
this.onApprovalEvent({
|
|
20103
|
+
phase: "requested",
|
|
20104
|
+
operation,
|
|
20105
|
+
tier,
|
|
20106
|
+
reason,
|
|
20107
|
+
context,
|
|
20108
|
+
request_timestamp: requestTimestamp,
|
|
20109
|
+
correlation_id: correlationId
|
|
20110
|
+
});
|
|
20111
|
+
} catch {
|
|
20112
|
+
}
|
|
20113
|
+
}
|
|
20114
|
+
let response;
|
|
20115
|
+
try {
|
|
20116
|
+
response = await this.channel.requestApproval(request);
|
|
20117
|
+
} catch (err) {
|
|
20118
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
20119
|
+
const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20120
|
+
this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
|
|
20121
|
+
tier,
|
|
20122
|
+
reason,
|
|
20123
|
+
decided_by: "channel_failure",
|
|
20124
|
+
channel_error: errMessage
|
|
20125
|
+
});
|
|
20126
|
+
if (this.onApprovalEvent) {
|
|
20127
|
+
try {
|
|
20128
|
+
this.onApprovalEvent({
|
|
20129
|
+
phase: "resolved",
|
|
20130
|
+
operation,
|
|
20131
|
+
tier,
|
|
20132
|
+
reason,
|
|
20133
|
+
context,
|
|
20134
|
+
request_timestamp: requestTimestamp,
|
|
20135
|
+
resolution: {
|
|
20136
|
+
decision: "deny",
|
|
20137
|
+
decided_at: decidedAt,
|
|
20138
|
+
decided_by: "channel_failure"
|
|
20139
|
+
},
|
|
20140
|
+
correlation_id: correlationId
|
|
20141
|
+
});
|
|
20142
|
+
} catch {
|
|
20143
|
+
}
|
|
20144
|
+
}
|
|
20145
|
+
return {
|
|
20146
|
+
allowed: false,
|
|
20147
|
+
tier,
|
|
20148
|
+
reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
20149
|
+
approval_required: true,
|
|
20150
|
+
approval_response: {
|
|
20151
|
+
decision: "deny",
|
|
20152
|
+
decided_at: decidedAt,
|
|
20153
|
+
decided_by: "channel_failure"
|
|
20154
|
+
}
|
|
20155
|
+
};
|
|
20156
|
+
}
|
|
19683
20157
|
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
19684
20158
|
tier,
|
|
19685
20159
|
reason,
|
|
19686
20160
|
decided_by: response.decided_by
|
|
19687
20161
|
});
|
|
20162
|
+
if (this.onApprovalEvent) {
|
|
20163
|
+
try {
|
|
20164
|
+
this.onApprovalEvent({
|
|
20165
|
+
phase: "resolved",
|
|
20166
|
+
operation,
|
|
20167
|
+
tier,
|
|
20168
|
+
reason,
|
|
20169
|
+
context,
|
|
20170
|
+
request_timestamp: requestTimestamp,
|
|
20171
|
+
resolution: {
|
|
20172
|
+
decision: response.decision,
|
|
20173
|
+
decided_at: response.decided_at,
|
|
20174
|
+
decided_by: response.decided_by
|
|
20175
|
+
},
|
|
20176
|
+
correlation_id: correlationId
|
|
20177
|
+
});
|
|
20178
|
+
} catch {
|
|
20179
|
+
}
|
|
20180
|
+
}
|
|
19688
20181
|
return {
|
|
19689
20182
|
allowed: response.decision === "approve",
|
|
19690
20183
|
tier,
|
|
19691
|
-
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` :
|
|
20184
|
+
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
|
|
19692
20185
|
approval_required: true,
|
|
19693
20186
|
approval_response: response
|
|
19694
20187
|
};
|
|
@@ -19719,6 +20212,356 @@ var init_gate = __esm({
|
|
|
19719
20212
|
};
|
|
19720
20213
|
}
|
|
19721
20214
|
});
|
|
20215
|
+
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;
|
|
20216
|
+
var init_approval_aggregator = __esm({
|
|
20217
|
+
"src/principal-policy/approval-aggregator.ts"() {
|
|
20218
|
+
init_encryption();
|
|
20219
|
+
init_key_derivation();
|
|
20220
|
+
init_encoding();
|
|
20221
|
+
APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
|
|
20222
|
+
APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
|
|
20223
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS = {
|
|
20224
|
+
AGGREGATED: "cross_harness_approval_aggregated",
|
|
20225
|
+
RESOLVED: "cross_harness_approval_resolved",
|
|
20226
|
+
DEDUPED: "cross_harness_approval_deduped"
|
|
20227
|
+
};
|
|
20228
|
+
DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
|
|
20229
|
+
DEFAULT_MAX_LIST_LIMIT = 200;
|
|
20230
|
+
DEFAULT_LIST_PAGE_SIZE = 50;
|
|
20231
|
+
ApprovalAggregator = class {
|
|
20232
|
+
storage;
|
|
20233
|
+
encryptionKey;
|
|
20234
|
+
auditLog;
|
|
20235
|
+
identityId;
|
|
20236
|
+
fortressId;
|
|
20237
|
+
pendingTtlMs;
|
|
20238
|
+
maxListLimit;
|
|
20239
|
+
now;
|
|
20240
|
+
resolveSourceContext;
|
|
20241
|
+
resolveHubInboxItemId;
|
|
20242
|
+
/** Cached entries by `aggregator_id`. */
|
|
20243
|
+
entries = /* @__PURE__ */ new Map();
|
|
20244
|
+
/** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
|
|
20245
|
+
dedupIndex = /* @__PURE__ */ new Map();
|
|
20246
|
+
/** Correlation index: gate `correlation_id` -> aggregator_id. */
|
|
20247
|
+
correlationIndex = /* @__PURE__ */ new Map();
|
|
20248
|
+
/** Original request payloads kept in-memory for `getFullPayload()`. */
|
|
20249
|
+
fullPayloads = /* @__PURE__ */ new Map();
|
|
20250
|
+
/** Has the aggregator hydrated persisted entries on this process? */
|
|
20251
|
+
hydrated = false;
|
|
20252
|
+
/** Active SSE listeners. */
|
|
20253
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20254
|
+
constructor(deps) {
|
|
20255
|
+
this.storage = deps.storage;
|
|
20256
|
+
this.encryptionKey = derivePurposeKey(
|
|
20257
|
+
deps.masterKey,
|
|
20258
|
+
APPROVAL_AGGREGATOR_HKDF_INFO
|
|
20259
|
+
);
|
|
20260
|
+
this.auditLog = deps.auditLog;
|
|
20261
|
+
this.identityId = deps.identityId;
|
|
20262
|
+
this.fortressId = deps.fortressId;
|
|
20263
|
+
this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
20264
|
+
this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
|
|
20265
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
20266
|
+
this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
|
|
20267
|
+
source_harness: this.fortressId,
|
|
20268
|
+
source_agent_id: this.fortressId
|
|
20269
|
+
}));
|
|
20270
|
+
this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
|
|
20271
|
+
}
|
|
20272
|
+
/**
|
|
20273
|
+
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
20274
|
+
* use this to forward aggregator emissions to the dashboard.
|
|
20275
|
+
*/
|
|
20276
|
+
onEvent(listener) {
|
|
20277
|
+
this.listeners.add(listener);
|
|
20278
|
+
return () => this.listeners.delete(listener);
|
|
20279
|
+
}
|
|
20280
|
+
/**
|
|
20281
|
+
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
20282
|
+
* `null` when deduped. Resolution events update the existing record;
|
|
20283
|
+
* unmatched resolutions are dropped silently (caller's gate emitted a
|
|
20284
|
+
* resolved-without-requested pair, which the aggregator does not invent
|
|
20285
|
+
* a record for).
|
|
20286
|
+
*/
|
|
20287
|
+
async ingest(event) {
|
|
20288
|
+
await this.hydrate();
|
|
20289
|
+
if (event.phase === "requested") {
|
|
20290
|
+
return this.ingestRequested(event);
|
|
20291
|
+
}
|
|
20292
|
+
if (event.phase === "resolved") {
|
|
20293
|
+
return this.ingestResolved(event);
|
|
20294
|
+
}
|
|
20295
|
+
return null;
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* List pending or recently resolved entries. Pending entries past TTL
|
|
20299
|
+
* are lazily transitioned to `expired` and persisted before the list
|
|
20300
|
+
* snapshot is returned.
|
|
20301
|
+
*/
|
|
20302
|
+
async list(opts) {
|
|
20303
|
+
await this.hydrate();
|
|
20304
|
+
await this.expireStale();
|
|
20305
|
+
const limit = Math.min(
|
|
20306
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
20307
|
+
this.maxListLimit
|
|
20308
|
+
);
|
|
20309
|
+
const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
|
|
20310
|
+
const matching = [];
|
|
20311
|
+
for (const entry of this.entries.values()) {
|
|
20312
|
+
if (opts?.status && entry.status !== opts.status) continue;
|
|
20313
|
+
if (Date.parse(entry.created_at) < sinceMs) continue;
|
|
20314
|
+
matching.push(entry);
|
|
20315
|
+
}
|
|
20316
|
+
matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
20317
|
+
return matching.slice(0, limit);
|
|
20318
|
+
}
|
|
20319
|
+
/**
|
|
20320
|
+
* Return the original (unhashed) request payload for the entry. Returns
|
|
20321
|
+
* `null` when the entry is unknown or the payload was evicted (e.g. the
|
|
20322
|
+
* process restarted; payloads are in-memory only at v1.3 Upsilon-1).
|
|
20323
|
+
*/
|
|
20324
|
+
async getFullPayload(aggregatorId) {
|
|
20325
|
+
await this.hydrate();
|
|
20326
|
+
if (!this.entries.has(aggregatorId)) return null;
|
|
20327
|
+
return this.fullPayloads.get(aggregatorId) ?? null;
|
|
20328
|
+
}
|
|
20329
|
+
/**
|
|
20330
|
+
* Resolve an entry. Used by both:
|
|
20331
|
+
* 1. The gate wire-up on channel-decision return.
|
|
20332
|
+
* 2. The HTTP `approve`/`deny` routes when an operator clicks.
|
|
20333
|
+
*
|
|
20334
|
+
* Idempotent: resolving an already-resolved entry is a no-op (the record
|
|
20335
|
+
* keeps its first decision and the audit log is not double-fired).
|
|
20336
|
+
* Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
|
|
20337
|
+
* routes return 404.
|
|
20338
|
+
*/
|
|
20339
|
+
async resolve(aggregatorId, decision, operatorId) {
|
|
20340
|
+
await this.hydrate();
|
|
20341
|
+
const entry = this.entries.get(aggregatorId);
|
|
20342
|
+
if (!entry) {
|
|
20343
|
+
throw new Error("approval-aggregator: not_found");
|
|
20344
|
+
}
|
|
20345
|
+
if (entry.status !== "pending") {
|
|
20346
|
+
return entry;
|
|
20347
|
+
}
|
|
20348
|
+
entry.status = decision;
|
|
20349
|
+
entry.resolved_at = this.now().toISOString();
|
|
20350
|
+
entry.resolved_by = operatorId;
|
|
20351
|
+
await this.persist(entry);
|
|
20352
|
+
this.auditLog.append(
|
|
20353
|
+
"l2",
|
|
20354
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20355
|
+
this.identityId,
|
|
20356
|
+
{
|
|
20357
|
+
aggregator_id: entry.aggregator_id,
|
|
20358
|
+
source_harness: entry.source_harness,
|
|
20359
|
+
source_agent_id: entry.source_agent_id,
|
|
20360
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20361
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20362
|
+
decision,
|
|
20363
|
+
decided_by: operatorId,
|
|
20364
|
+
decided_at: entry.resolved_at
|
|
20365
|
+
}
|
|
20366
|
+
);
|
|
20367
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20368
|
+
return entry;
|
|
20369
|
+
}
|
|
20370
|
+
// ── Internal: ingest paths ─────────────────────────────────────────────
|
|
20371
|
+
async ingestRequested(event) {
|
|
20372
|
+
const ctx = this.resolveSourceContext(event);
|
|
20373
|
+
const auditId = this.auditEntryIdForEvent(event);
|
|
20374
|
+
const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
|
|
20375
|
+
const existing = this.dedupIndex.get(dedupKey);
|
|
20376
|
+
if (existing) {
|
|
20377
|
+
const existingEntry = this.entries.get(existing);
|
|
20378
|
+
if (existingEntry) {
|
|
20379
|
+
this.correlationIndex.set(event.correlation_id, existing);
|
|
20380
|
+
this.auditLog.append(
|
|
20381
|
+
"l2",
|
|
20382
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
|
|
20383
|
+
this.identityId,
|
|
20384
|
+
{
|
|
20385
|
+
aggregator_id: existing,
|
|
20386
|
+
source_harness: ctx.source_harness,
|
|
20387
|
+
source_agent_id: ctx.source_agent_id,
|
|
20388
|
+
audit_log_entry_id: auditId,
|
|
20389
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20390
|
+
correlation_id: event.correlation_id
|
|
20391
|
+
}
|
|
20392
|
+
);
|
|
20393
|
+
this.emit({ type: "deduped", entry: { ...existingEntry } });
|
|
20394
|
+
return null;
|
|
20395
|
+
}
|
|
20396
|
+
}
|
|
20397
|
+
const id = randomUUID();
|
|
20398
|
+
const now = this.now();
|
|
20399
|
+
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
20400
|
+
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
20401
|
+
const entry = {
|
|
20402
|
+
aggregator_id: id,
|
|
20403
|
+
source_harness: ctx.source_harness,
|
|
20404
|
+
source_agent_id: ctx.source_agent_id,
|
|
20405
|
+
audit_log_entry_id: auditId,
|
|
20406
|
+
policy_rule_id: this.derivePolicyRuleId(event),
|
|
20407
|
+
action_summary: this.deriveActionSummary(event),
|
|
20408
|
+
request_payload_hash: this.hashPayload(event.context),
|
|
20409
|
+
status: "pending",
|
|
20410
|
+
created_at: now.toISOString(),
|
|
20411
|
+
expires_at: expires.toISOString(),
|
|
20412
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20413
|
+
};
|
|
20414
|
+
this.entries.set(id, entry);
|
|
20415
|
+
this.dedupIndex.set(dedupKey, id);
|
|
20416
|
+
this.correlationIndex.set(event.correlation_id, id);
|
|
20417
|
+
this.fullPayloads.set(id, event.context);
|
|
20418
|
+
await this.persist(entry);
|
|
20419
|
+
this.auditLog.append(
|
|
20420
|
+
"l2",
|
|
20421
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
|
|
20422
|
+
this.identityId,
|
|
20423
|
+
{
|
|
20424
|
+
aggregator_id: id,
|
|
20425
|
+
source_harness: ctx.source_harness,
|
|
20426
|
+
source_agent_id: ctx.source_agent_id,
|
|
20427
|
+
audit_log_entry_id: auditId,
|
|
20428
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20429
|
+
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
|
|
20430
|
+
}
|
|
20431
|
+
);
|
|
20432
|
+
this.emit({ type: "aggregated", entry: { ...entry } });
|
|
20433
|
+
return entry;
|
|
20434
|
+
}
|
|
20435
|
+
async ingestResolved(event) {
|
|
20436
|
+
const id = this.correlationIndex.get(event.correlation_id);
|
|
20437
|
+
if (!id) return null;
|
|
20438
|
+
const entry = this.entries.get(id);
|
|
20439
|
+
if (!entry) return null;
|
|
20440
|
+
if (entry.status !== "pending") return entry;
|
|
20441
|
+
if (!event.resolution) return entry;
|
|
20442
|
+
const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
|
|
20443
|
+
const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
|
|
20444
|
+
entry.status = status;
|
|
20445
|
+
entry.resolved_at = event.resolution.decided_at;
|
|
20446
|
+
entry.resolved_by = event.resolution.decided_by;
|
|
20447
|
+
await this.persist(entry);
|
|
20448
|
+
this.auditLog.append(
|
|
20449
|
+
"l2",
|
|
20450
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20451
|
+
this.identityId,
|
|
20452
|
+
{
|
|
20453
|
+
aggregator_id: id,
|
|
20454
|
+
source_harness: entry.source_harness,
|
|
20455
|
+
source_agent_id: entry.source_agent_id,
|
|
20456
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20457
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20458
|
+
decision: status,
|
|
20459
|
+
decided_by: entry.resolved_by,
|
|
20460
|
+
decided_at: entry.resolved_at,
|
|
20461
|
+
fail_closed: failClosed
|
|
20462
|
+
}
|
|
20463
|
+
);
|
|
20464
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20465
|
+
return entry;
|
|
20466
|
+
}
|
|
20467
|
+
// ── Internal: helpers ──────────────────────────────────────────────────
|
|
20468
|
+
/**
|
|
20469
|
+
* Audit-log entry id for the dedup tuple. The audit log itself does not
|
|
20470
|
+
* surface a stable per-entry id (counter-prefixed keys are internal); the
|
|
20471
|
+
* aggregator uses the request timestamp + operation, which together pin
|
|
20472
|
+
* the audit entry the gate appended on the same call.
|
|
20473
|
+
*/
|
|
20474
|
+
auditEntryIdForEvent(event) {
|
|
20475
|
+
return `${event.request_timestamp}:${event.operation}`;
|
|
20476
|
+
}
|
|
20477
|
+
derivePolicyRuleId(event) {
|
|
20478
|
+
return `tier${event.tier}:${event.operation}`;
|
|
20479
|
+
}
|
|
20480
|
+
deriveActionSummary(event) {
|
|
20481
|
+
return `${event.operation} (tier ${event.tier})`;
|
|
20482
|
+
}
|
|
20483
|
+
/**
|
|
20484
|
+
* Canonical SHA-256 of the request context. Sorted-keys serialization so
|
|
20485
|
+
* identical payloads always hash the same, even when key insertion order
|
|
20486
|
+
* varies. Defends against payload-replay smuggling (the aggregator can
|
|
20487
|
+
* tell the same payload was seen twice without storing it cleartext).
|
|
20488
|
+
*/
|
|
20489
|
+
hashPayload(payload) {
|
|
20490
|
+
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
|
|
20491
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
20492
|
+
}
|
|
20493
|
+
emit(event) {
|
|
20494
|
+
for (const listener of this.listeners) {
|
|
20495
|
+
try {
|
|
20496
|
+
listener(event);
|
|
20497
|
+
} catch {
|
|
20498
|
+
}
|
|
20499
|
+
}
|
|
20500
|
+
}
|
|
20501
|
+
async expireStale() {
|
|
20502
|
+
const nowMs = this.now().getTime();
|
|
20503
|
+
for (const entry of this.entries.values()) {
|
|
20504
|
+
if (entry.status !== "pending") continue;
|
|
20505
|
+
if (Date.parse(entry.expires_at) > nowMs) continue;
|
|
20506
|
+
entry.status = "expired";
|
|
20507
|
+
entry.resolved_at = this.now().toISOString();
|
|
20508
|
+
entry.resolved_by = "system_ttl";
|
|
20509
|
+
await this.persist(entry);
|
|
20510
|
+
this.auditLog.append(
|
|
20511
|
+
"l2",
|
|
20512
|
+
APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
|
|
20513
|
+
this.identityId,
|
|
20514
|
+
{
|
|
20515
|
+
aggregator_id: entry.aggregator_id,
|
|
20516
|
+
source_harness: entry.source_harness,
|
|
20517
|
+
source_agent_id: entry.source_agent_id,
|
|
20518
|
+
audit_log_entry_id: entry.audit_log_entry_id,
|
|
20519
|
+
policy_rule_id: entry.policy_rule_id,
|
|
20520
|
+
decision: "expired",
|
|
20521
|
+
decided_by: "system_ttl",
|
|
20522
|
+
decided_at: entry.resolved_at
|
|
20523
|
+
}
|
|
20524
|
+
);
|
|
20525
|
+
this.emit({ type: "resolved", entry: { ...entry } });
|
|
20526
|
+
}
|
|
20527
|
+
}
|
|
20528
|
+
async persist(entry) {
|
|
20529
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
20530
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
20531
|
+
await this.storage.write(
|
|
20532
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20533
|
+
entry.aggregator_id,
|
|
20534
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
20535
|
+
);
|
|
20536
|
+
}
|
|
20537
|
+
async hydrate() {
|
|
20538
|
+
if (this.hydrated) return;
|
|
20539
|
+
this.hydrated = true;
|
|
20540
|
+
try {
|
|
20541
|
+
const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
|
|
20542
|
+
for (const meta of metas) {
|
|
20543
|
+
const raw = await this.storage.read(
|
|
20544
|
+
APPROVAL_AGGREGATOR_NAMESPACE,
|
|
20545
|
+
meta.key
|
|
20546
|
+
);
|
|
20547
|
+
if (!raw) continue;
|
|
20548
|
+
try {
|
|
20549
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
20550
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
20551
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
20552
|
+
this.entries.set(entry.aggregator_id, entry);
|
|
20553
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
20554
|
+
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
20555
|
+
} catch {
|
|
20556
|
+
}
|
|
20557
|
+
}
|
|
20558
|
+
} catch {
|
|
20559
|
+
this.hydrated = false;
|
|
20560
|
+
}
|
|
20561
|
+
}
|
|
20562
|
+
};
|
|
20563
|
+
}
|
|
20564
|
+
});
|
|
19722
20565
|
|
|
19723
20566
|
// src/principal-policy/tools.ts
|
|
19724
20567
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
@@ -20277,7 +21120,11 @@ var init_tools5 = __esm({
|
|
|
20277
21120
|
|
|
20278
21121
|
// src/handshake/protocol.ts
|
|
20279
21122
|
function generateNonce() {
|
|
20280
|
-
|
|
21123
|
+
const nonce = randomBytes(32);
|
|
21124
|
+
if (!nonce || nonce.length !== 32) {
|
|
21125
|
+
throw new Error("Nonce generation failed: randomBytes returned unexpected length");
|
|
21126
|
+
}
|
|
21127
|
+
return toBase64url(nonce);
|
|
20281
21128
|
}
|
|
20282
21129
|
function initiateHandshake(ourSHR) {
|
|
20283
21130
|
const nonce = generateNonce();
|
|
@@ -20388,6 +21235,18 @@ function completeHandshake(response, session, identityManager, masterKey, identi
|
|
|
20388
21235
|
return { completion, result };
|
|
20389
21236
|
}
|
|
20390
21237
|
function verifyCompletion(completion, session) {
|
|
21238
|
+
if (completion.protocol_version !== "1.0") {
|
|
21239
|
+
return {
|
|
21240
|
+
counterparty_id: "unknown",
|
|
21241
|
+
counterparty_shr: session.our_shr,
|
|
21242
|
+
verified: false,
|
|
21243
|
+
sovereignty_level: "unverified",
|
|
21244
|
+
trust_tier: "unverified",
|
|
21245
|
+
completed_at: completion.completed_at,
|
|
21246
|
+
expires_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21247
|
+
errors: [`Unsupported protocol version: ${completion.protocol_version}`]
|
|
21248
|
+
};
|
|
21249
|
+
}
|
|
20391
21250
|
const errors = [];
|
|
20392
21251
|
if (!session.their_shr) {
|
|
20393
21252
|
return {
|
|
@@ -22620,6 +23479,12 @@ function typed(markerPath, lineNumber, field, expected) {
|
|
|
22620
23479
|
}
|
|
22621
23480
|
async function consumeResetHistoryMarker(options) {
|
|
22622
23481
|
const markerPath = join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
23482
|
+
const consumedPath = markerPath + ".consumed";
|
|
23483
|
+
if (await fileExists3(consumedPath)) {
|
|
23484
|
+
await rm(markerPath, { force: true });
|
|
23485
|
+
await rm(consumedPath, { force: true });
|
|
23486
|
+
return { emitted: 0, markerPath };
|
|
23487
|
+
}
|
|
22623
23488
|
if (!await fileExists3(markerPath)) {
|
|
22624
23489
|
return { emitted: 0, markerPath };
|
|
22625
23490
|
}
|
|
@@ -22644,7 +23509,9 @@ async function consumeResetHistoryMarker(options) {
|
|
|
22644
23509
|
});
|
|
22645
23510
|
}
|
|
22646
23511
|
await options.auditLog.flush();
|
|
23512
|
+
await writeFile(consumedPath, "", "utf-8");
|
|
22647
23513
|
await rm(markerPath, { force: true });
|
|
23514
|
+
await rm(consumedPath, { force: true });
|
|
22648
23515
|
return { emitted: markers.length, markerHash, markerPath };
|
|
22649
23516
|
}
|
|
22650
23517
|
async function fileExists3(path) {
|
|
@@ -22674,6 +23541,9 @@ Inspect the file and either correct the JSON or delete it manually before re-run
|
|
|
22674
23541
|
this.cause = cause;
|
|
22675
23542
|
this.name = "ResetHistoryMalformedError";
|
|
22676
23543
|
}
|
|
23544
|
+
markerPath;
|
|
23545
|
+
lineNumber;
|
|
23546
|
+
cause;
|
|
22677
23547
|
};
|
|
22678
23548
|
}
|
|
22679
23549
|
});
|
|
@@ -24832,7 +25702,7 @@ async function runOpenAIPrivacyFilter(text, config) {
|
|
|
24832
25702
|
return parsed;
|
|
24833
25703
|
}
|
|
24834
25704
|
function runCommand(command, input, timeoutMs) {
|
|
24835
|
-
return new Promise((
|
|
25705
|
+
return new Promise((resolve8, reject) => {
|
|
24836
25706
|
const child = spawn(command, [], {
|
|
24837
25707
|
stdio: ["pipe", "pipe", "pipe"],
|
|
24838
25708
|
shell: false
|
|
@@ -24863,7 +25733,7 @@ function runCommand(command, input, timeoutMs) {
|
|
|
24863
25733
|
));
|
|
24864
25734
|
return;
|
|
24865
25735
|
}
|
|
24866
|
-
|
|
25736
|
+
resolve8(stdout);
|
|
24867
25737
|
});
|
|
24868
25738
|
child.stdin.end(input);
|
|
24869
25739
|
});
|
|
@@ -26739,13 +27609,13 @@ var init_proxy_router = __esm({
|
|
|
26739
27609
|
* Call an upstream tool with a timeout.
|
|
26740
27610
|
*/
|
|
26741
27611
|
async callWithTimeout(serverName, toolName, args, timeoutMs) {
|
|
26742
|
-
return new Promise((
|
|
27612
|
+
return new Promise((resolve8, reject) => {
|
|
26743
27613
|
const timer = setTimeout(() => {
|
|
26744
27614
|
reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
|
|
26745
27615
|
}, timeoutMs);
|
|
26746
27616
|
this.clientManager.callTool(serverName, toolName, args).then((result) => {
|
|
26747
27617
|
clearTimeout(timer);
|
|
26748
|
-
|
|
27618
|
+
resolve8(result);
|
|
26749
27619
|
}).catch((err) => {
|
|
26750
27620
|
clearTimeout(timer);
|
|
26751
27621
|
reject(err);
|
|
@@ -31924,6 +32794,36 @@ var init_hub_service = __esm({
|
|
|
31924
32794
|
const chat = this.requireOperatorChat();
|
|
31925
32795
|
return chat.getConciergeHistory();
|
|
31926
32796
|
}
|
|
32797
|
+
// ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
|
|
32798
|
+
/**
|
|
32799
|
+
* Whether the operator-chat service has the WP-V1.3-9 memory store
|
|
32800
|
+
* wired. Routes use this to 503 cleanly when the foundation memory
|
|
32801
|
+
* surface is unavailable on a given fortress.
|
|
32802
|
+
*/
|
|
32803
|
+
hasConciergeMemory() {
|
|
32804
|
+
return Boolean(this.deps.operatorChat?.hasConciergeMemory());
|
|
32805
|
+
}
|
|
32806
|
+
async listConciergeMemoryThreads(opts) {
|
|
32807
|
+
const chat = this.requireOperatorChat();
|
|
32808
|
+
if (!chat.hasConciergeMemory()) {
|
|
32809
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32810
|
+
}
|
|
32811
|
+
return chat.listConciergeMemoryThreads(opts);
|
|
32812
|
+
}
|
|
32813
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
32814
|
+
const chat = this.requireOperatorChat();
|
|
32815
|
+
if (!chat.hasConciergeMemory()) {
|
|
32816
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32817
|
+
}
|
|
32818
|
+
return chat.readConciergeMemoryThread(threadId, opts);
|
|
32819
|
+
}
|
|
32820
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
32821
|
+
const chat = this.requireOperatorChat();
|
|
32822
|
+
if (!chat.hasConciergeMemory()) {
|
|
32823
|
+
throw new HubCapabilityError("concierge_memory_not_wired");
|
|
32824
|
+
}
|
|
32825
|
+
return chat.deleteConciergeMemoryThread(threadId);
|
|
32826
|
+
}
|
|
31927
32827
|
/**
|
|
31928
32828
|
* Open the click-to-inspect/approve panel for a wrapped agent. The
|
|
31929
32829
|
* panel surfaces recent activity routed through this agent, pending
|
|
@@ -32062,7 +32962,20 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32062
32962
|
* affordance now opens an inspect/approve panel (recent activity +
|
|
32063
32963
|
* pending approvals + policy summary) instead of a chat session.
|
|
32064
32964
|
*/
|
|
32065
|
-
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened"
|
|
32965
|
+
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
|
|
32966
|
+
/**
|
|
32967
|
+
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
32968
|
+
* when the operator hits the list-threads or read-thread route. Body
|
|
32969
|
+
* carries the thread_id (or `*` for the list endpoint) and a count;
|
|
32970
|
+
* raw turn content never crosses the audit surface.
|
|
32971
|
+
*/
|
|
32972
|
+
CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
|
|
32973
|
+
/**
|
|
32974
|
+
* Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
|
|
32975
|
+
* successful thread removal. Body carries thread_id + turn_count of
|
|
32976
|
+
* the deleted bundle.
|
|
32977
|
+
*/
|
|
32978
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
32066
32979
|
};
|
|
32067
32980
|
}
|
|
32068
32981
|
});
|
|
@@ -32081,7 +32994,7 @@ function makeEventId(prefix) {
|
|
|
32081
32994
|
function hashOf(input) {
|
|
32082
32995
|
return hashToString(sha256(stringToBytes(input)));
|
|
32083
32996
|
}
|
|
32084
|
-
var DEFAULT_CONCIERGE_MAX_TOKENS, OperatorChatService;
|
|
32997
|
+
var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
32085
32998
|
var init_operator_chat_service = __esm({
|
|
32086
32999
|
"src/chat/operator-chat-service.ts"() {
|
|
32087
33000
|
init_hashing();
|
|
@@ -32089,6 +33002,33 @@ var init_operator_chat_service = __esm({
|
|
|
32089
33002
|
init_operator_chat_audit_events();
|
|
32090
33003
|
init_operator_chat_types();
|
|
32091
33004
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
33005
|
+
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
33006
|
+
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
33007
|
+
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
33008
|
+
3. Charter (Cooperative MCP): the sovereignty surface for compliant agents. Policy gates, approval tiers, audit logging, and encrypted state all live here.
|
|
33009
|
+
4. Heralds: Concordia receipts and Verascore reputation. Cross-fortress accountability after an action completes.
|
|
33010
|
+
|
|
33011
|
+
Five channel templates (canonical names):
|
|
33012
|
+
- request-approve-act: agent proposes an action, operator approves or denies before execution.
|
|
33013
|
+
- read-then-report: agent reads outputs from a data source and reports summaries to the operator.
|
|
33014
|
+
- scheduled-digest: agent runs on a schedule and delivers a periodic digest.
|
|
33015
|
+
- plan-draft-only: agent drafts plans; operator reviews before any execution step.
|
|
33016
|
+
- fortress-relay: agent relays messages between fortresses under operator-scoped policy.
|
|
33017
|
+
|
|
33018
|
+
Four canonical policy slots:
|
|
33019
|
+
- memory: governs what the agent may persist and retrieve from encrypted state.
|
|
33020
|
+
- credentials: governs access to secrets, API keys, and tokens held in the broker.
|
|
33021
|
+
- plans: governs the agent's ability to create, modify, or execute plans.
|
|
33022
|
+
- outputs: governs what the agent may emit to external surfaces (files, APIs, messages).
|
|
33023
|
+
|
|
33024
|
+
Key concepts:
|
|
33025
|
+
- Fortress: the operator-owned sovereignty harness. All state is encrypted at rest under the cocoon.
|
|
33026
|
+
- Cocoon: master-key-wrapped storage derived from the operator's passphrase via Argon2id.
|
|
33027
|
+
- Identity: Ed25519 keypair with a DID, owned by the operator. Private keys never leave the cocoon.
|
|
33028
|
+
- Audit log: append-only encrypted blobs, sequential, recording every gate decision and tool call.
|
|
33029
|
+
- Wrapped agent: any agent runtime that connects to Sanctuary as an MCP client. Tier A (native), Tier B (adapter-wrapped), Tier C (escape hatch).
|
|
33030
|
+
|
|
33031
|
+
Note: this is a static reference block (v1.2.x). Dynamic context injection (live template list, policy schema) ships in v1.3.`;
|
|
32092
33032
|
OperatorChatService = class {
|
|
32093
33033
|
store;
|
|
32094
33034
|
auditLog;
|
|
@@ -32097,6 +33037,14 @@ var init_operator_chat_service = __esm({
|
|
|
32097
33037
|
contextProviders;
|
|
32098
33038
|
piiFilter;
|
|
32099
33039
|
conciergeMaxTokens;
|
|
33040
|
+
memory;
|
|
33041
|
+
/**
|
|
33042
|
+
* In-memory thread_id assigned to the active concierge session.
|
|
33043
|
+
* The first sendConcierge call after construction allocates a fresh
|
|
33044
|
+
* UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
|
|
33045
|
+
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33046
|
+
*/
|
|
33047
|
+
activeMemoryThreadId;
|
|
32100
33048
|
constructor(deps) {
|
|
32101
33049
|
this.store = deps.store;
|
|
32102
33050
|
this.auditLog = deps.auditLog;
|
|
@@ -32107,6 +33055,7 @@ var init_operator_chat_service = __esm({
|
|
|
32107
33055
|
}
|
|
32108
33056
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
32109
33057
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33058
|
+
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
32110
33059
|
}
|
|
32111
33060
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
32112
33061
|
/**
|
|
@@ -32137,6 +33086,11 @@ var init_operator_chat_service = __esm({
|
|
|
32137
33086
|
CONCIERGE_THREAD_KEY,
|
|
32138
33087
|
operatorMessage
|
|
32139
33088
|
);
|
|
33089
|
+
if (this.memory) {
|
|
33090
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33091
|
+
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
33092
|
+
});
|
|
33093
|
+
}
|
|
32140
33094
|
const start = Date.now();
|
|
32141
33095
|
let conciergeBody;
|
|
32142
33096
|
let servedBy = "disabled";
|
|
@@ -32192,6 +33146,11 @@ var init_operator_chat_service = __esm({
|
|
|
32192
33146
|
CONCIERGE_THREAD_KEY,
|
|
32193
33147
|
responseMessage
|
|
32194
33148
|
);
|
|
33149
|
+
if (this.memory) {
|
|
33150
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
33151
|
+
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
|
|
33152
|
+
});
|
|
33153
|
+
}
|
|
32195
33154
|
const payload = {
|
|
32196
33155
|
version: "1.2",
|
|
32197
33156
|
event_id: makeEventId("conc"),
|
|
@@ -32224,6 +33183,105 @@ var init_operator_chat_service = __esm({
|
|
|
32224
33183
|
);
|
|
32225
33184
|
return thread ? thread.messages : [];
|
|
32226
33185
|
}
|
|
33186
|
+
// ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
|
|
33187
|
+
/**
|
|
33188
|
+
* Whether the foundation memory store is wired. Routes use this to
|
|
33189
|
+
* 503 cleanly when called against an unwired service.
|
|
33190
|
+
*/
|
|
33191
|
+
hasConciergeMemory() {
|
|
33192
|
+
return this.memory !== void 0;
|
|
33193
|
+
}
|
|
33194
|
+
/**
|
|
33195
|
+
* List concierge memory threads, newest-first. Emits the
|
|
33196
|
+
* `operator_concierge_history_read` audit event with `thread_id="*"`.
|
|
33197
|
+
*/
|
|
33198
|
+
async listConciergeMemoryThreads(opts) {
|
|
33199
|
+
if (!this.memory) {
|
|
33200
|
+
throw new Error("concierge memory store not configured");
|
|
33201
|
+
}
|
|
33202
|
+
const summaries = await this.memory.listThreads(opts);
|
|
33203
|
+
const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
|
|
33204
|
+
const payload = {
|
|
33205
|
+
version: "1.2",
|
|
33206
|
+
event_id: makeEventId("conc-hist"),
|
|
33207
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33208
|
+
identity_id: this.identityId,
|
|
33209
|
+
kind: "operator_concierge_history_read",
|
|
33210
|
+
surface: "concierge",
|
|
33211
|
+
thread_id: "*",
|
|
33212
|
+
turn_count: totalTurns
|
|
33213
|
+
};
|
|
33214
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33215
|
+
return summaries;
|
|
33216
|
+
}
|
|
33217
|
+
/**
|
|
33218
|
+
* Read a concierge memory thread, oldest turn first. Emits the
|
|
33219
|
+
* `operator_concierge_history_read` audit event with the named
|
|
33220
|
+
* thread_id and the count of turns surfaced.
|
|
33221
|
+
*/
|
|
33222
|
+
async readConciergeMemoryThread(threadId, opts) {
|
|
33223
|
+
if (!this.memory) {
|
|
33224
|
+
throw new Error("concierge memory store not configured");
|
|
33225
|
+
}
|
|
33226
|
+
const turns = await this.memory.readThread(threadId, opts);
|
|
33227
|
+
const payload = {
|
|
33228
|
+
version: "1.2",
|
|
33229
|
+
event_id: makeEventId("conc-hist"),
|
|
33230
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33231
|
+
identity_id: this.identityId,
|
|
33232
|
+
kind: "operator_concierge_history_read",
|
|
33233
|
+
surface: "concierge",
|
|
33234
|
+
thread_id: threadId,
|
|
33235
|
+
turn_count: turns.length
|
|
33236
|
+
};
|
|
33237
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
|
|
33238
|
+
return turns;
|
|
33239
|
+
}
|
|
33240
|
+
/**
|
|
33241
|
+
* Delete a concierge memory thread. Emits
|
|
33242
|
+
* `operator_concierge_thread_deleted` only when a bundle was actually
|
|
33243
|
+
* removed; absent threads return false without an audit event.
|
|
33244
|
+
*/
|
|
33245
|
+
async deleteConciergeMemoryThread(threadId) {
|
|
33246
|
+
if (!this.memory) {
|
|
33247
|
+
throw new Error("concierge memory store not configured");
|
|
33248
|
+
}
|
|
33249
|
+
const turnsBefore = await this.memory.readThread(threadId);
|
|
33250
|
+
if (turnsBefore.length === 0) {
|
|
33251
|
+
return await this.memory.deleteThread(threadId);
|
|
33252
|
+
}
|
|
33253
|
+
const removed = await this.memory.deleteThread(threadId);
|
|
33254
|
+
if (!removed) return false;
|
|
33255
|
+
if (this.activeMemoryThreadId === threadId) {
|
|
33256
|
+
this.activeMemoryThreadId = void 0;
|
|
33257
|
+
}
|
|
33258
|
+
const payload = {
|
|
33259
|
+
version: "1.2",
|
|
33260
|
+
event_id: makeEventId("conc-del"),
|
|
33261
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33262
|
+
identity_id: this.identityId,
|
|
33263
|
+
kind: "operator_concierge_thread_deleted",
|
|
33264
|
+
surface: "concierge",
|
|
33265
|
+
thread_id: threadId,
|
|
33266
|
+
turn_count: turnsBefore.length
|
|
33267
|
+
};
|
|
33268
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
|
|
33269
|
+
return true;
|
|
33270
|
+
}
|
|
33271
|
+
/**
|
|
33272
|
+
* Reset the active session memory thread. Subsequent sendConcierge
|
|
33273
|
+
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
33274
|
+
* conversation" affordance; not currently called by the dashboard.
|
|
33275
|
+
*/
|
|
33276
|
+
resetConciergeMemoryThread() {
|
|
33277
|
+
this.activeMemoryThreadId = void 0;
|
|
33278
|
+
}
|
|
33279
|
+
ensureActiveMemoryThread() {
|
|
33280
|
+
if (!this.activeMemoryThreadId) {
|
|
33281
|
+
this.activeMemoryThreadId = randomUUID();
|
|
33282
|
+
}
|
|
33283
|
+
return this.activeMemoryThreadId;
|
|
33284
|
+
}
|
|
32227
33285
|
/**
|
|
32228
33286
|
* Stitch fortress state into a single context blob the substrate
|
|
32229
33287
|
* folds into its summarization prompt.
|
|
@@ -32233,6 +33291,9 @@ var init_operator_chat_service = __esm({
|
|
|
32233
33291
|
* than nested structures. Format:
|
|
32234
33292
|
*
|
|
32235
33293
|
* ```
|
|
33294
|
+
* ## Sanctuary reference
|
|
33295
|
+
* <static domain reference block>
|
|
33296
|
+
*
|
|
32236
33297
|
* ## Recent activity
|
|
32237
33298
|
* <recentActivity output>
|
|
32238
33299
|
*
|
|
@@ -32244,15 +33305,28 @@ var init_operator_chat_service = __esm({
|
|
|
32244
33305
|
* ```
|
|
32245
33306
|
*/
|
|
32246
33307
|
async assembleConciergeContext() {
|
|
33308
|
+
const ref = `## Sanctuary reference
|
|
33309
|
+
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
32247
33310
|
if (!this.contextProviders) {
|
|
32248
|
-
return
|
|
33311
|
+
return `${ref}
|
|
33312
|
+
|
|
33313
|
+
## Recent activity
|
|
33314
|
+
(no providers wired)
|
|
33315
|
+
|
|
33316
|
+
## Wrapped agents
|
|
33317
|
+
(no providers wired)
|
|
33318
|
+
|
|
33319
|
+
## Open inbox
|
|
33320
|
+
(no providers wired)`;
|
|
32249
33321
|
}
|
|
32250
33322
|
const [activity, agents, inbox] = await Promise.all([
|
|
32251
33323
|
this.contextProviders.recentActivity(),
|
|
32252
33324
|
this.contextProviders.agentInventory(),
|
|
32253
33325
|
this.contextProviders.openInbox()
|
|
32254
33326
|
]);
|
|
32255
|
-
return
|
|
33327
|
+
return `${ref}
|
|
33328
|
+
|
|
33329
|
+
## Recent activity
|
|
32256
33330
|
${activity}
|
|
32257
33331
|
|
|
32258
33332
|
## Wrapped agents
|
|
@@ -32374,11 +33448,250 @@ var init_operator_chat_store = __esm({
|
|
|
32374
33448
|
}
|
|
32375
33449
|
});
|
|
32376
33450
|
|
|
33451
|
+
// src/chat/concierge-memory-store.ts
|
|
33452
|
+
function bundleKey(threadId) {
|
|
33453
|
+
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
33454
|
+
}
|
|
33455
|
+
function stripKeyPrefix(key) {
|
|
33456
|
+
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
33457
|
+
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
33458
|
+
}
|
|
33459
|
+
function lastTurnId(bundle) {
|
|
33460
|
+
let max = 0;
|
|
33461
|
+
for (const t of bundle.turns) {
|
|
33462
|
+
if (t.turn_id > max) max = t.turn_id;
|
|
33463
|
+
}
|
|
33464
|
+
return max;
|
|
33465
|
+
}
|
|
33466
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO2, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES2, ConciergeMemoryStore;
|
|
33467
|
+
var init_concierge_memory_store = __esm({
|
|
33468
|
+
"src/chat/concierge-memory-store.ts"() {
|
|
33469
|
+
init_encryption();
|
|
33470
|
+
init_key_derivation();
|
|
33471
|
+
init_encoding();
|
|
33472
|
+
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
33473
|
+
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
33474
|
+
HKDF_INFO2 = "concierge-memory-store-v1";
|
|
33475
|
+
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
33476
|
+
MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
|
|
33477
|
+
ConciergeMemoryStore = class {
|
|
33478
|
+
storage;
|
|
33479
|
+
encryptionKey;
|
|
33480
|
+
fortressId;
|
|
33481
|
+
retentionDays;
|
|
33482
|
+
locks;
|
|
33483
|
+
constructor(opts) {
|
|
33484
|
+
this.storage = opts.storage;
|
|
33485
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
33486
|
+
this.fortressId = opts.fortressId;
|
|
33487
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
33488
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
33489
|
+
}
|
|
33490
|
+
/**
|
|
33491
|
+
* Append a turn to the named thread, creating the bundle if no record
|
|
33492
|
+
* exists. Returns the persisted turn (with assigned turn_id +
|
|
33493
|
+
* retention_until). Per-thread serialisation guarantees turn_id
|
|
33494
|
+
* monotonicity even under concurrent callers.
|
|
33495
|
+
*/
|
|
33496
|
+
async appendTurn(threadId, role, content) {
|
|
33497
|
+
return this.withLock(threadId, async () => {
|
|
33498
|
+
const bundle = await this.loadBundle(threadId) ?? null;
|
|
33499
|
+
const now = /* @__PURE__ */ new Date();
|
|
33500
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
33501
|
+
const retentionUntil = new Date(now.getTime() + retentionMs);
|
|
33502
|
+
const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
|
|
33503
|
+
const turn = {
|
|
33504
|
+
thread_id: threadId,
|
|
33505
|
+
fortress_id: this.fortressId,
|
|
33506
|
+
turn_id: nextTurnId,
|
|
33507
|
+
role,
|
|
33508
|
+
content,
|
|
33509
|
+
created_at: now.toISOString(),
|
|
33510
|
+
retention_until: retentionUntil.toISOString()
|
|
33511
|
+
};
|
|
33512
|
+
const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
|
|
33513
|
+
version: 1,
|
|
33514
|
+
thread_id: threadId,
|
|
33515
|
+
fortress_id: this.fortressId,
|
|
33516
|
+
created_at: now.toISOString(),
|
|
33517
|
+
turns: [turn]
|
|
33518
|
+
};
|
|
33519
|
+
await this.saveBundle(next);
|
|
33520
|
+
return turn;
|
|
33521
|
+
});
|
|
33522
|
+
}
|
|
33523
|
+
/**
|
|
33524
|
+
* Read turns from a thread, oldest-first. Returns an empty array if
|
|
33525
|
+
* the thread does not exist or its bundle is corrupt. Does not emit
|
|
33526
|
+
* audit events; the caller (HTTP route handler) owns audit semantics.
|
|
33527
|
+
*/
|
|
33528
|
+
async readThread(threadId, opts) {
|
|
33529
|
+
const bundle = await this.loadBundle(threadId);
|
|
33530
|
+
if (!bundle) return [];
|
|
33531
|
+
let turns = bundle.turns;
|
|
33532
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
33533
|
+
const cutoff = opts.sinceTurnId;
|
|
33534
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
33535
|
+
}
|
|
33536
|
+
if (opts?.limit !== void 0) {
|
|
33537
|
+
turns = turns.slice(0, opts.limit);
|
|
33538
|
+
}
|
|
33539
|
+
return turns;
|
|
33540
|
+
}
|
|
33541
|
+
/**
|
|
33542
|
+
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33543
|
+
* Sorted newest-first by last_turn_at.
|
|
33544
|
+
*/
|
|
33545
|
+
async listThreads(opts) {
|
|
33546
|
+
const entries = await this.storage.list(
|
|
33547
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33548
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33549
|
+
);
|
|
33550
|
+
const summaries = [];
|
|
33551
|
+
for (const meta of entries) {
|
|
33552
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33553
|
+
if (threadId === null) continue;
|
|
33554
|
+
const bundle = await this.loadBundle(threadId);
|
|
33555
|
+
if (!bundle || bundle.turns.length === 0) continue;
|
|
33556
|
+
const last = bundle.turns[bundle.turns.length - 1];
|
|
33557
|
+
summaries.push({
|
|
33558
|
+
thread_id: bundle.thread_id,
|
|
33559
|
+
created_at: bundle.created_at,
|
|
33560
|
+
last_turn_at: last ? last.created_at : bundle.created_at,
|
|
33561
|
+
turn_count: bundle.turns.length
|
|
33562
|
+
});
|
|
33563
|
+
}
|
|
33564
|
+
summaries.sort(
|
|
33565
|
+
(a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
|
|
33566
|
+
);
|
|
33567
|
+
if (opts?.limit !== void 0) {
|
|
33568
|
+
return summaries.slice(0, opts.limit);
|
|
33569
|
+
}
|
|
33570
|
+
return summaries;
|
|
33571
|
+
}
|
|
33572
|
+
/**
|
|
33573
|
+
* Delete a thread's bundle. Returns true if the bundle existed and
|
|
33574
|
+
* was removed; false if no bundle was present. Audit emission is the
|
|
33575
|
+
* caller's responsibility.
|
|
33576
|
+
*/
|
|
33577
|
+
async deleteThread(threadId) {
|
|
33578
|
+
const key = bundleKey(threadId);
|
|
33579
|
+
return this.withLock(threadId, async () => {
|
|
33580
|
+
const existed = await this.storage.exists(
|
|
33581
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33582
|
+
key
|
|
33583
|
+
);
|
|
33584
|
+
if (!existed) return false;
|
|
33585
|
+
try {
|
|
33586
|
+
await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33587
|
+
} catch {
|
|
33588
|
+
return false;
|
|
33589
|
+
}
|
|
33590
|
+
return true;
|
|
33591
|
+
});
|
|
33592
|
+
}
|
|
33593
|
+
/**
|
|
33594
|
+
* Drop expired turns across all threads. Threads emptied by pruning
|
|
33595
|
+
* are removed entirely. Returns the count of turns pruned.
|
|
33596
|
+
*/
|
|
33597
|
+
async pruneExpired(now) {
|
|
33598
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
33599
|
+
const entries = await this.storage.list(
|
|
33600
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33601
|
+
CONCIERGE_MEMORY_KEY_PREFIX
|
|
33602
|
+
);
|
|
33603
|
+
let pruned = 0;
|
|
33604
|
+
for (const meta of entries) {
|
|
33605
|
+
const threadId = stripKeyPrefix(meta.key);
|
|
33606
|
+
if (threadId === null) continue;
|
|
33607
|
+
pruned += await this.withLock(threadId, async () => {
|
|
33608
|
+
const bundle = await this.loadBundle(threadId);
|
|
33609
|
+
if (!bundle) return 0;
|
|
33610
|
+
const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
|
|
33611
|
+
const dropped = bundle.turns.length - kept.length;
|
|
33612
|
+
if (dropped === 0) return 0;
|
|
33613
|
+
if (kept.length === 0) {
|
|
33614
|
+
await this.storage.delete(
|
|
33615
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33616
|
+
bundleKey(threadId)
|
|
33617
|
+
);
|
|
33618
|
+
} else {
|
|
33619
|
+
await this.saveBundle({ ...bundle, turns: kept });
|
|
33620
|
+
}
|
|
33621
|
+
return dropped;
|
|
33622
|
+
});
|
|
33623
|
+
}
|
|
33624
|
+
return { pruned };
|
|
33625
|
+
}
|
|
33626
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
33627
|
+
async loadBundle(threadId) {
|
|
33628
|
+
const key = bundleKey(threadId);
|
|
33629
|
+
let raw;
|
|
33630
|
+
try {
|
|
33631
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
33632
|
+
} catch {
|
|
33633
|
+
return null;
|
|
33634
|
+
}
|
|
33635
|
+
if (!raw) return null;
|
|
33636
|
+
if (raw.length > MAX_BUNDLE_BYTES2) return null;
|
|
33637
|
+
try {
|
|
33638
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
33639
|
+
const aad = stringToBytes(threadId);
|
|
33640
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
33641
|
+
const parsed = JSON.parse(
|
|
33642
|
+
bytesToString(plaintext)
|
|
33643
|
+
);
|
|
33644
|
+
if (parsed.version !== 1) return null;
|
|
33645
|
+
if (parsed.thread_id !== threadId) return null;
|
|
33646
|
+
return parsed;
|
|
33647
|
+
} catch {
|
|
33648
|
+
return null;
|
|
33649
|
+
}
|
|
33650
|
+
}
|
|
33651
|
+
async saveBundle(bundle) {
|
|
33652
|
+
const key = bundleKey(bundle.thread_id);
|
|
33653
|
+
const aad = stringToBytes(bundle.thread_id);
|
|
33654
|
+
const plaintext = stringToBytes(JSON.stringify(bundle));
|
|
33655
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
33656
|
+
await this.storage.write(
|
|
33657
|
+
CONCIERGE_MEMORY_NAMESPACE,
|
|
33658
|
+
key,
|
|
33659
|
+
stringToBytes(JSON.stringify(envelope))
|
|
33660
|
+
);
|
|
33661
|
+
}
|
|
33662
|
+
/**
|
|
33663
|
+
* Run `task` while holding the per-thread async lock. Lock is released
|
|
33664
|
+
* once the task settles (success or failure). Generic helper so
|
|
33665
|
+
* appendTurn / deleteThread / pruneExpired share serialisation.
|
|
33666
|
+
*/
|
|
33667
|
+
async withLock(threadId, task) {
|
|
33668
|
+
const previous = this.locks.get(threadId) ?? Promise.resolve();
|
|
33669
|
+
let release;
|
|
33670
|
+
const next = new Promise((resolve8) => {
|
|
33671
|
+
release = resolve8;
|
|
33672
|
+
});
|
|
33673
|
+
const chained = previous.then(() => next);
|
|
33674
|
+
this.locks.set(threadId, chained);
|
|
33675
|
+
try {
|
|
33676
|
+
await previous;
|
|
33677
|
+
return await task();
|
|
33678
|
+
} finally {
|
|
33679
|
+
release();
|
|
33680
|
+
if (this.locks.get(threadId) === chained) {
|
|
33681
|
+
this.locks.delete(threadId);
|
|
33682
|
+
}
|
|
33683
|
+
}
|
|
33684
|
+
}
|
|
33685
|
+
};
|
|
33686
|
+
}
|
|
33687
|
+
});
|
|
33688
|
+
|
|
32377
33689
|
// src/chat/operator-chat-index.ts
|
|
32378
33690
|
var init_operator_chat_index = __esm({
|
|
32379
33691
|
"src/chat/operator-chat-index.ts"() {
|
|
32380
33692
|
init_operator_chat_service();
|
|
32381
33693
|
init_operator_chat_store();
|
|
33694
|
+
init_concierge_memory_store();
|
|
32382
33695
|
init_operator_chat_audit_events();
|
|
32383
33696
|
init_operator_chat_types();
|
|
32384
33697
|
}
|
|
@@ -32392,6 +33705,14 @@ function buildV11Bindings(inputs) {
|
|
|
32392
33705
|
let operatorChatService;
|
|
32393
33706
|
if (inputs.storage && inputs.masterKey) {
|
|
32394
33707
|
const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
|
|
33708
|
+
const conciergeMemory = new ConciergeMemoryStore({
|
|
33709
|
+
storage: inputs.storage,
|
|
33710
|
+
masterKey: inputs.masterKey,
|
|
33711
|
+
fortressId: inputs.fortressId,
|
|
33712
|
+
...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
|
|
33713
|
+
});
|
|
33714
|
+
void conciergeMemory.pruneExpired().catch(() => {
|
|
33715
|
+
});
|
|
32395
33716
|
operatorChatService = new OperatorChatService({
|
|
32396
33717
|
store: chatStore,
|
|
32397
33718
|
auditLog: inputs.auditLog,
|
|
@@ -32402,7 +33723,8 @@ function buildV11Bindings(inputs) {
|
|
|
32402
33723
|
identityId: inputs.identityId,
|
|
32403
33724
|
registry
|
|
32404
33725
|
}),
|
|
32405
|
-
conciergePiiFilter: buildConciergePiiFilter()
|
|
33726
|
+
conciergePiiFilter: buildConciergePiiFilter(),
|
|
33727
|
+
conciergeMemory
|
|
32406
33728
|
});
|
|
32407
33729
|
}
|
|
32408
33730
|
const hubService = new HubService({
|
|
@@ -32637,7 +33959,7 @@ var init_defaults = __esm({
|
|
|
32637
33959
|
});
|
|
32638
33960
|
|
|
32639
33961
|
// src/intelligence/policy-store.ts
|
|
32640
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
33962
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO3, IntelligenceConfigStore;
|
|
32641
33963
|
var init_policy_store = __esm({
|
|
32642
33964
|
"src/intelligence/policy-store.ts"() {
|
|
32643
33965
|
init_encryption();
|
|
@@ -32646,13 +33968,13 @@ var init_policy_store = __esm({
|
|
|
32646
33968
|
init_defaults();
|
|
32647
33969
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
32648
33970
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
32649
|
-
|
|
33971
|
+
HKDF_INFO3 = "intelligence-substrate-config";
|
|
32650
33972
|
IntelligenceConfigStore = class {
|
|
32651
33973
|
storage;
|
|
32652
33974
|
encryptionKey;
|
|
32653
33975
|
constructor(storage, masterKey) {
|
|
32654
33976
|
this.storage = storage;
|
|
32655
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
33977
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
32656
33978
|
}
|
|
32657
33979
|
/**
|
|
32658
33980
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -34674,7 +35996,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34674
35996
|
);
|
|
34675
35997
|
}
|
|
34676
35998
|
}
|
|
34677
|
-
const
|
|
35999
|
+
const reputationBundleFailed = reputation?.bundle_signature_valid === false;
|
|
36000
|
+
const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
|
|
36001
|
+
const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
|
|
34678
36002
|
const identityFailed = identity ? !identity.signature_valid : false;
|
|
34679
36003
|
const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
|
|
34680
36004
|
const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
|
|
@@ -34683,6 +36007,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34683
36007
|
`${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
|
|
34684
36008
|
);
|
|
34685
36009
|
}
|
|
36010
|
+
let detailedFailureClass;
|
|
36011
|
+
if (identityFailed) {
|
|
36012
|
+
detailedFailureClass = "identity_signature_invalid";
|
|
36013
|
+
} else if (reputationBundleFailed) {
|
|
36014
|
+
detailedFailureClass = "reputation_bundle_signature_invalid";
|
|
36015
|
+
} else if (reputationAttestationFailed) {
|
|
36016
|
+
detailedFailureClass = "reputation_attestation_signature_invalid";
|
|
36017
|
+
} else if (unverifiableFailed) {
|
|
36018
|
+
detailedFailureClass = "reputation_unverifiable_attestations";
|
|
36019
|
+
}
|
|
34686
36020
|
return {
|
|
34687
36021
|
version: "1.1",
|
|
34688
36022
|
passed: !reputationFailed && !identityFailed && !unverifiableFailed,
|
|
@@ -34702,7 +36036,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
|
|
|
34702
36036
|
identity,
|
|
34703
36037
|
audit,
|
|
34704
36038
|
reputation,
|
|
34705
|
-
failure_class:
|
|
36039
|
+
failure_class: detailedFailureClass
|
|
34706
36040
|
};
|
|
34707
36041
|
}
|
|
34708
36042
|
var InvalidExitBundleError, PRIVATE_MATERIAL_KEYS;
|
|
@@ -35284,6 +36618,9 @@ async function importExitBundle(opts) {
|
|
|
35284
36618
|
reputationArtifact?.json ?? null,
|
|
35285
36619
|
manifest
|
|
35286
36620
|
);
|
|
36621
|
+
if (!conflicts.public_identity_exists && identityArtifact?.json && opts.identityManager.getPrimaryIdentityId() !== null && opts.identityManager.getPrimaryIdentityId() !== identityArtifact.json.bundle.identity_id) {
|
|
36622
|
+
conflicts.public_identity_exists = true;
|
|
36623
|
+
}
|
|
35287
36624
|
if (!opts.activate) {
|
|
35288
36625
|
return {
|
|
35289
36626
|
verified: true,
|
|
@@ -35310,7 +36647,7 @@ async function importExitBundle(opts) {
|
|
|
35310
36647
|
if (conflicts.public_identity_exists && !opts.forceRebind) {
|
|
35311
36648
|
throw new ExitBundleImportError(
|
|
35312
36649
|
"IDENTITY_OVERWRITE_REFUSED",
|
|
35313
|
-
"Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
|
|
36650
|
+
"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."
|
|
35314
36651
|
);
|
|
35315
36652
|
}
|
|
35316
36653
|
if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
|
|
@@ -35667,7 +37004,19 @@ async function runExitCommand(args) {
|
|
|
35667
37004
|
}
|
|
35668
37005
|
const config = await loadConfig();
|
|
35669
37006
|
const ctx = await openExitContext(argv, env);
|
|
35670
|
-
|
|
37007
|
+
let policy;
|
|
37008
|
+
try {
|
|
37009
|
+
policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
37010
|
+
} catch (policyErr) {
|
|
37011
|
+
if (policyErr instanceof MalformedPrincipalPolicyError) {
|
|
37012
|
+
write(err, `
|
|
37013
|
+
Sanctuary cannot proceed.
|
|
37014
|
+
${policyErr.message}
|
|
37015
|
+
`);
|
|
37016
|
+
return 1;
|
|
37017
|
+
}
|
|
37018
|
+
throw policyErr;
|
|
37019
|
+
}
|
|
35671
37020
|
const result = await exportExitBundle({
|
|
35672
37021
|
bundleDir: outDir,
|
|
35673
37022
|
storage: ctx.storage,
|
|
@@ -35700,6 +37049,26 @@ async function runExitCommand(args) {
|
|
|
35700
37049
|
write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
|
|
35701
37050
|
return 2;
|
|
35702
37051
|
}
|
|
37052
|
+
const bundleRoot = resolve(dir);
|
|
37053
|
+
try {
|
|
37054
|
+
await access(bundleRoot);
|
|
37055
|
+
} catch {
|
|
37056
|
+
write(err, `Error: bundle directory not found: ${bundleRoot}
|
|
37057
|
+
`);
|
|
37058
|
+
return 1;
|
|
37059
|
+
}
|
|
37060
|
+
const manifestPath = join(bundleRoot, "manifest.json");
|
|
37061
|
+
try {
|
|
37062
|
+
const raw = await readFile(manifestPath, "utf8");
|
|
37063
|
+
JSON.parse(raw);
|
|
37064
|
+
} catch {
|
|
37065
|
+
write(
|
|
37066
|
+
err,
|
|
37067
|
+
`Error: bundle manifest missing or malformed at ${manifestPath}
|
|
37068
|
+
`
|
|
37069
|
+
);
|
|
37070
|
+
return 1;
|
|
37071
|
+
}
|
|
35703
37072
|
const activate = hasFlag(argv, "--activate");
|
|
35704
37073
|
const forceRebind = hasFlag(argv, "--force-rebind");
|
|
35705
37074
|
const acceptUnverifiableAttestations = hasFlag(
|
|
@@ -35873,11 +37242,11 @@ async function startDashboardServer(options) {
|
|
|
35873
37242
|
}
|
|
35874
37243
|
}
|
|
35875
37244
|
});
|
|
35876
|
-
await new Promise((
|
|
37245
|
+
await new Promise((resolve8, reject) => {
|
|
35877
37246
|
server.once("error", reject);
|
|
35878
37247
|
server.listen(port, host, () => {
|
|
35879
37248
|
server.off("error", reject);
|
|
35880
|
-
|
|
37249
|
+
resolve8();
|
|
35881
37250
|
});
|
|
35882
37251
|
});
|
|
35883
37252
|
const actualPort = (() => {
|
|
@@ -35890,8 +37259,8 @@ async function startDashboardServer(options) {
|
|
|
35890
37259
|
url,
|
|
35891
37260
|
port: actualPort,
|
|
35892
37261
|
host,
|
|
35893
|
-
stop: () => new Promise((
|
|
35894
|
-
server.close((err) => err ? reject(err) :
|
|
37262
|
+
stop: () => new Promise((resolve8, reject) => {
|
|
37263
|
+
server.close((err) => err ? reject(err) : resolve8());
|
|
35895
37264
|
}),
|
|
35896
37265
|
publish,
|
|
35897
37266
|
publishActivity: (entry) => publish({ type: "activity", data: entry }),
|
|
@@ -36347,7 +37716,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36347
37716
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
36348
37717
|
await profileStore.load();
|
|
36349
37718
|
const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
|
|
36350
|
-
|
|
37719
|
+
let policy;
|
|
37720
|
+
try {
|
|
37721
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
37722
|
+
} catch (err) {
|
|
37723
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
37724
|
+
console.error(`
|
|
37725
|
+
Sanctuary cannot start.
|
|
37726
|
+
${err.message}
|
|
37727
|
+
`);
|
|
37728
|
+
process.exit(1);
|
|
37729
|
+
}
|
|
37730
|
+
throw err;
|
|
37731
|
+
}
|
|
36351
37732
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
36352
37733
|
await baseline.load();
|
|
36353
37734
|
let approvalChannel;
|
|
@@ -36445,6 +37826,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36445
37826
|
});
|
|
36446
37827
|
} : void 0;
|
|
36447
37828
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37829
|
+
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37830
|
+
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37831
|
+
const approvalAggregator = new ApprovalAggregator({
|
|
37832
|
+
storage,
|
|
37833
|
+
masterKey,
|
|
37834
|
+
auditLog,
|
|
37835
|
+
identityId: aggregatorIdentityId,
|
|
37836
|
+
fortressId: fortressIdForAggregator
|
|
37837
|
+
});
|
|
37838
|
+
gate.setApprovalEventCallback((event) => {
|
|
37839
|
+
void approvalAggregator.ingest(event);
|
|
37840
|
+
});
|
|
37841
|
+
if (dashboard) {
|
|
37842
|
+
dashboard.setApprovalAggregator(approvalAggregator);
|
|
37843
|
+
}
|
|
36448
37844
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
36449
37845
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
36450
37846
|
config,
|
|
@@ -36564,7 +37960,7 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
|
36564
37960
|
clientManager.configure(enabledServers).catch((err) => {
|
|
36565
37961
|
console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
36566
37962
|
});
|
|
36567
|
-
await new Promise((
|
|
37963
|
+
await new Promise((resolve8) => setTimeout(resolve8, 2e3));
|
|
36568
37964
|
const proxiedTools = proxyRouter.getProxiedTools();
|
|
36569
37965
|
if (proxiedTools.length > 0) {
|
|
36570
37966
|
allTools.push(...proxiedTools);
|
|
@@ -36635,6 +38031,7 @@ var init_src = __esm({
|
|
|
36635
38031
|
init_dashboard();
|
|
36636
38032
|
init_webhook();
|
|
36637
38033
|
init_gate();
|
|
38034
|
+
init_approval_aggregator();
|
|
36638
38035
|
init_tools4();
|
|
36639
38036
|
init_router();
|
|
36640
38037
|
init_router();
|
|
@@ -37403,8 +38800,8 @@ async function runWrap(options, deps = {}) {
|
|
|
37403
38800
|
passphraseValue = process.env.SANCTUARY_PASSPHRASE;
|
|
37404
38801
|
} else {
|
|
37405
38802
|
try {
|
|
37406
|
-
const
|
|
37407
|
-
const resolved = await
|
|
38803
|
+
const resolve8 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
|
|
38804
|
+
const resolved = await resolve8();
|
|
37408
38805
|
passphraseLocation = resolved.location;
|
|
37409
38806
|
passphraseSource = resolved.source;
|
|
37410
38807
|
passphraseValue = resolved.value;
|
|
@@ -37759,12 +39156,12 @@ async function defaultOpenBrowser(url) {
|
|
|
37759
39156
|
cmd = "xdg-open";
|
|
37760
39157
|
args = [url];
|
|
37761
39158
|
}
|
|
37762
|
-
await new Promise((
|
|
39159
|
+
await new Promise((resolve8) => {
|
|
37763
39160
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
37764
|
-
child.on("error", () =>
|
|
39161
|
+
child.on("error", () => resolve8());
|
|
37765
39162
|
child.on("spawn", () => {
|
|
37766
39163
|
child.unref();
|
|
37767
|
-
|
|
39164
|
+
resolve8();
|
|
37768
39165
|
});
|
|
37769
39166
|
});
|
|
37770
39167
|
}
|
|
@@ -38733,32 +40130,32 @@ endstream`;
|
|
|
38733
40130
|
const offsets = new Array(totalObjects + 1).fill(0);
|
|
38734
40131
|
const chunks = [];
|
|
38735
40132
|
let bytePos = 0;
|
|
38736
|
-
const
|
|
40133
|
+
const write3 = (s) => {
|
|
38737
40134
|
const buf = Buffer.from(s, "latin1");
|
|
38738
40135
|
chunks.push(buf);
|
|
38739
40136
|
bytePos += buf.length;
|
|
38740
40137
|
};
|
|
38741
|
-
|
|
40138
|
+
write3("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
|
|
38742
40139
|
for (let i = 1; i <= totalObjects; i++) {
|
|
38743
40140
|
offsets[i] = bytePos;
|
|
38744
|
-
|
|
40141
|
+
write3(`${i} 0 obj
|
|
38745
40142
|
${objectBodies[i]}
|
|
38746
40143
|
endobj
|
|
38747
40144
|
`);
|
|
38748
40145
|
}
|
|
38749
40146
|
const xrefPos = bytePos;
|
|
38750
|
-
|
|
40147
|
+
write3(`xref
|
|
38751
40148
|
0 ${totalObjects + 1}
|
|
38752
40149
|
`);
|
|
38753
|
-
|
|
40150
|
+
write3("0000000000 65535 f \n");
|
|
38754
40151
|
for (let i = 1; i <= totalObjects; i++) {
|
|
38755
|
-
|
|
40152
|
+
write3(`${offsets[i].toString().padStart(10, "0")} 00000 n
|
|
38756
40153
|
`);
|
|
38757
40154
|
}
|
|
38758
|
-
|
|
40155
|
+
write3(`trailer
|
|
38759
40156
|
<< /Size ${totalObjects + 1} /Root 1 0 R >>
|
|
38760
40157
|
`);
|
|
38761
|
-
|
|
40158
|
+
write3(`startxref
|
|
38762
40159
|
${xrefPos}
|
|
38763
40160
|
%%EOF
|
|
38764
40161
|
`);
|
|
@@ -39093,7 +40490,7 @@ var init_backend_interface = __esm({
|
|
|
39093
40490
|
}
|
|
39094
40491
|
});
|
|
39095
40492
|
async function runSecurity(args, input) {
|
|
39096
|
-
return new Promise((
|
|
40493
|
+
return new Promise((resolve8, reject) => {
|
|
39097
40494
|
const child = spawn(SECURITY_BIN, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
39098
40495
|
let stdout = "";
|
|
39099
40496
|
let stderr = "";
|
|
@@ -39115,7 +40512,7 @@ async function runSecurity(args, input) {
|
|
|
39115
40512
|
reject(err);
|
|
39116
40513
|
});
|
|
39117
40514
|
child.on("close", (code) => {
|
|
39118
|
-
|
|
40515
|
+
resolve8({ stdout, stderr, code: code ?? -1 });
|
|
39119
40516
|
});
|
|
39120
40517
|
if (input !== void 0) {
|
|
39121
40518
|
child.stdin.write(input);
|
|
@@ -40191,7 +41588,7 @@ async function readValue(stdin, prompt2) {
|
|
|
40191
41588
|
return await readFirstLine(stdin);
|
|
40192
41589
|
}
|
|
40193
41590
|
async function readFirstLine(stdin) {
|
|
40194
|
-
return new Promise((
|
|
41591
|
+
return new Promise((resolve8, reject) => {
|
|
40195
41592
|
const rl = createInterface$1({ input: stdin });
|
|
40196
41593
|
let resolved = false;
|
|
40197
41594
|
const finish = (value) => {
|
|
@@ -40202,7 +41599,7 @@ async function readFirstLine(stdin) {
|
|
|
40202
41599
|
rl.close();
|
|
40203
41600
|
} catch {
|
|
40204
41601
|
}
|
|
40205
|
-
|
|
41602
|
+
resolve8(value);
|
|
40206
41603
|
};
|
|
40207
41604
|
const deadline = setTimeout(() => {
|
|
40208
41605
|
finish("");
|
|
@@ -40221,7 +41618,7 @@ async function promptSilently(stdin, prompt2) {
|
|
|
40221
41618
|
process.stderr.write(`${prompt2}: `);
|
|
40222
41619
|
stdin.setRawMode?.(true);
|
|
40223
41620
|
stdin.resume();
|
|
40224
|
-
return await new Promise((
|
|
41621
|
+
return await new Promise((resolve8) => {
|
|
40225
41622
|
let buf = "";
|
|
40226
41623
|
const onData = (chunk) => {
|
|
40227
41624
|
const s = chunk.toString("utf8");
|
|
@@ -40231,7 +41628,7 @@ async function promptSilently(stdin, prompt2) {
|
|
|
40231
41628
|
stdin.pause();
|
|
40232
41629
|
stdin.off("data", onData);
|
|
40233
41630
|
process.stderr.write("\n");
|
|
40234
|
-
|
|
41631
|
+
resolve8(buf);
|
|
40235
41632
|
return;
|
|
40236
41633
|
}
|
|
40237
41634
|
if (ch === "") {
|
|
@@ -40498,13 +41895,179 @@ var init_cli4 = __esm({
|
|
|
40498
41895
|
init_discovery();
|
|
40499
41896
|
}
|
|
40500
41897
|
});
|
|
41898
|
+
|
|
41899
|
+
// src/cli/identity.ts
|
|
41900
|
+
var identity_exports2 = {};
|
|
41901
|
+
__export(identity_exports2, {
|
|
41902
|
+
runIdentityCommand: () => runIdentityCommand
|
|
41903
|
+
});
|
|
41904
|
+
function write2(stream, text) {
|
|
41905
|
+
stream.write(text);
|
|
41906
|
+
}
|
|
41907
|
+
function flagValue2(argv, name) {
|
|
41908
|
+
const index = argv.indexOf(name);
|
|
41909
|
+
if (index === -1) return void 0;
|
|
41910
|
+
return argv[index + 1];
|
|
41911
|
+
}
|
|
41912
|
+
function hasFlag2(argv, name) {
|
|
41913
|
+
return argv.includes(name);
|
|
41914
|
+
}
|
|
41915
|
+
function printUsage3(out) {
|
|
41916
|
+
write2(
|
|
41917
|
+
out,
|
|
41918
|
+
`Usage: sanctuary identity <command> [options]
|
|
41919
|
+
|
|
41920
|
+
Commands:
|
|
41921
|
+
show Print the active identity (DID, identity_id, public key).
|
|
41922
|
+
|
|
41923
|
+
Options:
|
|
41924
|
+
--fortress <path> Override the storage path.
|
|
41925
|
+
--passphrase <val> Passphrase for master-key derivation.
|
|
41926
|
+
--json Output as JSON.
|
|
41927
|
+
--help, -h Show this help.
|
|
41928
|
+
|
|
41929
|
+
Environment variables:
|
|
41930
|
+
SANCTUARY_PASSPHRASE Key derivation passphrase.
|
|
41931
|
+
SANCTUARY_STORAGE_PATH State directory (default: ~/.sanctuary).
|
|
41932
|
+
SANCTUARY_FORTRESS_PATH Operator-friendly alias for STORAGE_PATH.
|
|
41933
|
+
SANCTUARY_RECOVERY_KEY Recovery key (alternative to passphrase).
|
|
41934
|
+
|
|
41935
|
+
Identity data is encrypted at rest. A passphrase or recovery key is
|
|
41936
|
+
required to decrypt and display identity information.
|
|
41937
|
+
`
|
|
41938
|
+
);
|
|
41939
|
+
}
|
|
41940
|
+
async function runIdentityCommand(args) {
|
|
41941
|
+
const argv = args.argv;
|
|
41942
|
+
const out = args.out ?? process.stdout;
|
|
41943
|
+
const err = args.err ?? process.stderr;
|
|
41944
|
+
const env = args.env ?? process.env;
|
|
41945
|
+
if (argv.length === 0 || hasFlag2(argv, "--help") || hasFlag2(argv, "-h")) {
|
|
41946
|
+
printUsage3(out);
|
|
41947
|
+
return 0;
|
|
41948
|
+
}
|
|
41949
|
+
const command = argv[0];
|
|
41950
|
+
if (command === "show") {
|
|
41951
|
+
return await cmdShow(argv.slice(1), out, err, env);
|
|
41952
|
+
}
|
|
41953
|
+
write2(err, `Unknown identity command: ${command}
|
|
41954
|
+
`);
|
|
41955
|
+
write2(err, `Run "sanctuary identity --help" for usage.
|
|
41956
|
+
`);
|
|
41957
|
+
return 2;
|
|
41958
|
+
}
|
|
41959
|
+
async function cmdShow(argv, out, err, env) {
|
|
41960
|
+
const json = hasFlag2(argv, "--json");
|
|
41961
|
+
const fortressFlag = flagValue2(argv, "--fortress");
|
|
41962
|
+
if (fortressFlag) {
|
|
41963
|
+
process.env.SANCTUARY_STORAGE_PATH = fortressFlag;
|
|
41964
|
+
}
|
|
41965
|
+
const passphrase = flagValue2(argv, "--passphrase") ?? env.SANCTUARY_PASSPHRASE;
|
|
41966
|
+
const recoveryKey = env.SANCTUARY_RECOVERY_KEY;
|
|
41967
|
+
if (!passphrase && !recoveryKey) {
|
|
41968
|
+
write2(
|
|
41969
|
+
err,
|
|
41970
|
+
"Error: sanctuary identity show requires SANCTUARY_PASSPHRASE, --passphrase, or SANCTUARY_RECOVERY_KEY.\n"
|
|
41971
|
+
);
|
|
41972
|
+
return 1;
|
|
41973
|
+
}
|
|
41974
|
+
try {
|
|
41975
|
+
const config = await loadConfig();
|
|
41976
|
+
await mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
41977
|
+
const stateStoragePath = join(config.storage_path, "state");
|
|
41978
|
+
const storage = new FilesystemStorage(stateStoragePath);
|
|
41979
|
+
let masterKey;
|
|
41980
|
+
if (passphrase) {
|
|
41981
|
+
let existingParams;
|
|
41982
|
+
const raw = await storage.read("_meta", "key-params");
|
|
41983
|
+
if (raw)
|
|
41984
|
+
existingParams = JSON.parse(bytesToString(raw));
|
|
41985
|
+
const derived = await deriveMasterKey(passphrase, existingParams);
|
|
41986
|
+
masterKey = derived.key;
|
|
41987
|
+
} else {
|
|
41988
|
+
masterKey = fromBase64url(recoveryKey);
|
|
41989
|
+
if (masterKey.length !== 32) {
|
|
41990
|
+
write2(err, "Error: SANCTUARY_RECOVERY_KEY must decode to 32 bytes.\n");
|
|
41991
|
+
return 1;
|
|
41992
|
+
}
|
|
41993
|
+
}
|
|
41994
|
+
const identityManager = new IdentityManager(storage, masterKey);
|
|
41995
|
+
const loadResult = await identityManager.load();
|
|
41996
|
+
if (loadResult.loaded === 0) {
|
|
41997
|
+
write2(
|
|
41998
|
+
err,
|
|
41999
|
+
loadResult.total > 0 ? "Error: identity files found but none could be decrypted. Wrong passphrase?\n" : "No identities found in this fortress.\n"
|
|
42000
|
+
);
|
|
42001
|
+
return 1;
|
|
42002
|
+
}
|
|
42003
|
+
const primary = identityManager.getDefault();
|
|
42004
|
+
if (!primary) {
|
|
42005
|
+
write2(err, "No primary identity set.\n");
|
|
42006
|
+
return 1;
|
|
42007
|
+
}
|
|
42008
|
+
if (json) {
|
|
42009
|
+
write2(
|
|
42010
|
+
out,
|
|
42011
|
+
JSON.stringify(
|
|
42012
|
+
{
|
|
42013
|
+
identity_id: primary.identity_id,
|
|
42014
|
+
did: primary.did,
|
|
42015
|
+
public_key: primary.public_key,
|
|
42016
|
+
label: primary.label,
|
|
42017
|
+
key_type: primary.key_type,
|
|
42018
|
+
created_at: primary.created_at,
|
|
42019
|
+
storage_path: config.storage_path,
|
|
42020
|
+
total_identities: loadResult.loaded
|
|
42021
|
+
},
|
|
42022
|
+
null,
|
|
42023
|
+
2
|
|
42024
|
+
) + "\n"
|
|
42025
|
+
);
|
|
42026
|
+
} else {
|
|
42027
|
+
write2(out, `identity_id: ${primary.identity_id}
|
|
42028
|
+
`);
|
|
42029
|
+
write2(out, `did: ${primary.did}
|
|
42030
|
+
`);
|
|
42031
|
+
write2(out, `public_key: ${primary.public_key}
|
|
42032
|
+
`);
|
|
42033
|
+
write2(out, `label: ${primary.label}
|
|
42034
|
+
`);
|
|
42035
|
+
write2(out, `key_type: ${primary.key_type}
|
|
42036
|
+
`);
|
|
42037
|
+
write2(out, `created_at: ${primary.created_at}
|
|
42038
|
+
`);
|
|
42039
|
+
write2(out, `storage_path: ${config.storage_path}
|
|
42040
|
+
`);
|
|
42041
|
+
write2(out, `total_identities: ${loadResult.loaded}
|
|
42042
|
+
`);
|
|
42043
|
+
}
|
|
42044
|
+
return 0;
|
|
42045
|
+
} catch (error) {
|
|
42046
|
+
write2(
|
|
42047
|
+
err,
|
|
42048
|
+
error instanceof Error ? `Error: ${error.message}
|
|
42049
|
+
` : `Error: ${String(error)}
|
|
42050
|
+
`
|
|
42051
|
+
);
|
|
42052
|
+
return 1;
|
|
42053
|
+
}
|
|
42054
|
+
}
|
|
42055
|
+
var init_identity2 = __esm({
|
|
42056
|
+
"src/cli/identity.ts"() {
|
|
42057
|
+
init_filesystem();
|
|
42058
|
+
init_tools();
|
|
42059
|
+
init_key_derivation();
|
|
42060
|
+
init_encoding();
|
|
42061
|
+
init_config();
|
|
42062
|
+
}
|
|
42063
|
+
});
|
|
40501
42064
|
async function probeTenantDashboard(tenant, options = {}) {
|
|
40502
42065
|
const rt = tenant.runtime;
|
|
40503
42066
|
if (!rt) {
|
|
40504
42067
|
return { running: false, status: null, reason: "no runtime.json" };
|
|
40505
42068
|
}
|
|
40506
42069
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
|
|
40507
|
-
return await new Promise((
|
|
42070
|
+
return await new Promise((resolve8) => {
|
|
40508
42071
|
const req = get$1(
|
|
40509
42072
|
{
|
|
40510
42073
|
host: rt.dashboard_host,
|
|
@@ -40516,9 +42079,9 @@ async function probeTenantDashboard(tenant, options = {}) {
|
|
|
40516
42079
|
res.resume();
|
|
40517
42080
|
const status = res.statusCode ?? 0;
|
|
40518
42081
|
if (status > 0 && status < 500) {
|
|
40519
|
-
|
|
42082
|
+
resolve8({ running: true, status, reason: null });
|
|
40520
42083
|
} else {
|
|
40521
|
-
|
|
42084
|
+
resolve8({
|
|
40522
42085
|
running: false,
|
|
40523
42086
|
status,
|
|
40524
42087
|
reason: `dashboard returned ${status}`
|
|
@@ -40528,10 +42091,10 @@ async function probeTenantDashboard(tenant, options = {}) {
|
|
|
40528
42091
|
);
|
|
40529
42092
|
req.on("timeout", () => {
|
|
40530
42093
|
req.destroy();
|
|
40531
|
-
|
|
42094
|
+
resolve8({ running: false, status: null, reason: "timeout" });
|
|
40532
42095
|
});
|
|
40533
42096
|
req.on("error", (err) => {
|
|
40534
|
-
|
|
42097
|
+
resolve8({
|
|
40535
42098
|
running: false,
|
|
40536
42099
|
status: null,
|
|
40537
42100
|
reason: err.code ?? err.message
|
|
@@ -40563,10 +42126,17 @@ function resolveCtx(args) {
|
|
|
40563
42126
|
};
|
|
40564
42127
|
}
|
|
40565
42128
|
async function runAgentsCommand(args) {
|
|
42129
|
+
const fortressIdx = args.argv.indexOf("--fortress");
|
|
42130
|
+
if (fortressIdx !== -1 && args.argv[fortressIdx + 1]) {
|
|
42131
|
+
args = { ...args, root: args.argv[fortressIdx + 1] };
|
|
42132
|
+
const filtered = [...args.argv];
|
|
42133
|
+
filtered.splice(fortressIdx, 2);
|
|
42134
|
+
args = { ...args, argv: filtered };
|
|
42135
|
+
}
|
|
40566
42136
|
const ctx = resolveCtx(args);
|
|
40567
42137
|
const [sub, ...rest] = args.argv;
|
|
40568
42138
|
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
|
40569
|
-
|
|
42139
|
+
printUsage4(ctx.out);
|
|
40570
42140
|
return 0;
|
|
40571
42141
|
}
|
|
40572
42142
|
try {
|
|
@@ -40574,13 +42144,13 @@ async function runAgentsCommand(args) {
|
|
|
40574
42144
|
case "list":
|
|
40575
42145
|
return await cmdList3(rest, ctx);
|
|
40576
42146
|
case "show":
|
|
40577
|
-
return await
|
|
42147
|
+
return await cmdShow2(rest, ctx);
|
|
40578
42148
|
case "status":
|
|
40579
42149
|
return await cmdStatus(rest, ctx);
|
|
40580
42150
|
default:
|
|
40581
42151
|
ctx.err.write(`Unknown subcommand: ${sub}
|
|
40582
42152
|
`);
|
|
40583
|
-
|
|
42153
|
+
printUsage4(ctx.err);
|
|
40584
42154
|
return 2;
|
|
40585
42155
|
}
|
|
40586
42156
|
} catch (e) {
|
|
@@ -40590,13 +42160,17 @@ async function runAgentsCommand(args) {
|
|
|
40590
42160
|
return 1;
|
|
40591
42161
|
}
|
|
40592
42162
|
}
|
|
40593
|
-
function
|
|
42163
|
+
function printUsage4(s) {
|
|
40594
42164
|
s.write(`Usage: sanctuary agents <command> [flags]
|
|
40595
42165
|
|
|
40596
42166
|
list [--json] List every tenant visible on this host.
|
|
40597
42167
|
show <tenant> [--json] Show details for one tenant.
|
|
40598
42168
|
status [--json] One-line-per-tenant running/stopped summary.
|
|
40599
42169
|
|
|
42170
|
+
Options:
|
|
42171
|
+
--fortress <path> Scope discovery to a specific storage path
|
|
42172
|
+
instead of scanning ~/.sanctuary.
|
|
42173
|
+
|
|
40600
42174
|
Tenants are discovered by scanning ~/.sanctuary and any storage paths in
|
|
40601
42175
|
SANCTUARY_AGENTS_EXTRA_PATHS or ~/.sanctuary/agents-extra.json. Tenant
|
|
40602
42176
|
creation is done via \`sanctuary wrap\` with SANCTUARY_STORAGE_PATH set.
|
|
@@ -40680,7 +42254,7 @@ async function cmdList3(argv, ctx) {
|
|
|
40680
42254
|
}
|
|
40681
42255
|
return 0;
|
|
40682
42256
|
}
|
|
40683
|
-
async function
|
|
42257
|
+
async function cmdShow2(argv, ctx) {
|
|
40684
42258
|
const positional = argv.find((a) => !a.startsWith("--"));
|
|
40685
42259
|
if (!positional) {
|
|
40686
42260
|
ctx.err.write("Missing tenant. Usage: sanctuary agents show <tenant>\n");
|
|
@@ -40821,7 +42395,8 @@ var init_agents = __esm({
|
|
|
40821
42395
|
// src/cli/reset-passphrase.ts
|
|
40822
42396
|
var reset_passphrase_exports = {};
|
|
40823
42397
|
__export(reset_passphrase_exports, {
|
|
40824
|
-
runResetPassphraseCommand: () => runResetPassphraseCommand
|
|
42398
|
+
runResetPassphraseCommand: () => runResetPassphraseCommand,
|
|
42399
|
+
zeroizeBuffers: () => zeroizeBuffers
|
|
40825
42400
|
});
|
|
40826
42401
|
async function runResetPassphraseCommand(args) {
|
|
40827
42402
|
const out = args.out ?? process.stdout;
|
|
@@ -40831,10 +42406,10 @@ async function runResetPassphraseCommand(args) {
|
|
|
40831
42406
|
const plat = args.platformOverride ?? process.platform;
|
|
40832
42407
|
const parsed = parseArgs2(args.argv);
|
|
40833
42408
|
if (parsed.help) {
|
|
40834
|
-
|
|
42409
|
+
printUsage5(out);
|
|
40835
42410
|
return 0;
|
|
40836
42411
|
}
|
|
40837
|
-
const storagePath = parsed.storage ?? args.storagePath ?? resolveStoragePath(process.env, home);
|
|
42412
|
+
const storagePath = parsed.storage ?? parsed.fortress ?? args.storagePath ?? resolveStoragePath(process.env, home);
|
|
40838
42413
|
out.write(banner(storagePath));
|
|
40839
42414
|
const runtimeFile = join(storagePath, "runtime.json");
|
|
40840
42415
|
if (await fileExists4(runtimeFile)) {
|
|
@@ -40849,34 +42424,42 @@ Then re-run this command.
|
|
|
40849
42424
|
return 1;
|
|
40850
42425
|
}
|
|
40851
42426
|
const lines = new LineReader(stdin);
|
|
42427
|
+
let code = 1;
|
|
42428
|
+
let nukeSucceeded = false;
|
|
40852
42429
|
try {
|
|
40853
42430
|
const availability = await surveyAvailableModes(storagePath);
|
|
40854
42431
|
const mode = parsed.mode ?? await selectMode(lines, out, err, availability);
|
|
40855
42432
|
if (!mode) {
|
|
40856
42433
|
err.write("Aborted: no recovery mode selected.\n");
|
|
40857
|
-
|
|
40858
|
-
}
|
|
40859
|
-
|
|
40860
|
-
|
|
40861
|
-
|
|
40862
|
-
|
|
40863
|
-
|
|
42434
|
+
code = 1;
|
|
42435
|
+
} else if (mode === "shares") {
|
|
42436
|
+
code = await runSharesPath(out, err, availability);
|
|
42437
|
+
} else if (mode === "guardian") {
|
|
42438
|
+
code = await runGuardianPath(out, err, availability);
|
|
42439
|
+
} else {
|
|
42440
|
+
code = await runNukePath({
|
|
42441
|
+
out,
|
|
42442
|
+
err,
|
|
42443
|
+
lines,
|
|
42444
|
+
storagePath,
|
|
42445
|
+
home,
|
|
42446
|
+
plat,
|
|
42447
|
+
exec: args.exec ?? defaultExec2
|
|
42448
|
+
});
|
|
42449
|
+
nukeSucceeded = mode === "nuke" && code === 0;
|
|
40864
42450
|
}
|
|
40865
|
-
return await runNukePath({
|
|
40866
|
-
out,
|
|
40867
|
-
err,
|
|
40868
|
-
lines,
|
|
40869
|
-
storagePath,
|
|
40870
|
-
home,
|
|
40871
|
-
plat,
|
|
40872
|
-
exec: args.exec ?? defaultExec2
|
|
40873
|
-
});
|
|
40874
42451
|
} finally {
|
|
42452
|
+
zeroizeBuffers(args.keyMaterialToZeroize);
|
|
40875
42453
|
lines.close();
|
|
40876
42454
|
}
|
|
42455
|
+
if (parsed.exitOnCompletion && nukeSucceeded) {
|
|
42456
|
+
const doExit = args.exitProcess ?? ((c) => process.exit(c));
|
|
42457
|
+
doExit(0);
|
|
42458
|
+
}
|
|
42459
|
+
return code;
|
|
40877
42460
|
}
|
|
40878
42461
|
function parseArgs2(argv) {
|
|
40879
|
-
const out = { help: false };
|
|
42462
|
+
const out = { exitOnCompletion: false, help: false };
|
|
40880
42463
|
for (let i = 0; i < argv.length; i++) {
|
|
40881
42464
|
const a = argv[i];
|
|
40882
42465
|
if (a === "--help" || a === "-h") {
|
|
@@ -40891,13 +42474,17 @@ function parseArgs2(argv) {
|
|
|
40891
42474
|
out.mode = v;
|
|
40892
42475
|
} else if (a === "--storage" && argv[i + 1]) {
|
|
40893
42476
|
out.storage = argv[++i];
|
|
42477
|
+
} else if (a === "--fortress" && argv[i + 1]) {
|
|
42478
|
+
out.fortress = argv[++i];
|
|
42479
|
+
} else if (a === "--exit-on-completion") {
|
|
42480
|
+
out.exitOnCompletion = true;
|
|
40894
42481
|
} else if (a && a.startsWith("--")) {
|
|
40895
42482
|
throw new Error(`Unknown flag: ${a}`);
|
|
40896
42483
|
}
|
|
40897
42484
|
}
|
|
40898
42485
|
return out;
|
|
40899
42486
|
}
|
|
40900
|
-
function
|
|
42487
|
+
function printUsage5(out) {
|
|
40901
42488
|
out.write(`
|
|
40902
42489
|
Usage: sanctuary reset-passphrase [options]
|
|
40903
42490
|
|
|
@@ -40920,9 +42507,19 @@ Recover a fortress whose passphrase has been lost or corrupted. Three modes:
|
|
|
40920
42507
|
|
|
40921
42508
|
Options:
|
|
40922
42509
|
--mode <shares|guardian|nuke> Pick a path non-interactively.
|
|
40923
|
-
--
|
|
40924
|
-
|
|
40925
|
-
|
|
42510
|
+
--fortress <path> Override the fortress storage path.
|
|
42511
|
+
Consistent with "sanctuary wrap --fortress".
|
|
42512
|
+
--storage <path> Alias for --fortress.
|
|
42513
|
+
--exit-on-completion After a successful nuke, call process.exit(0)
|
|
42514
|
+
immediately so the post-wipe heap is reaped
|
|
42515
|
+
by the OS without re-entering the shell. Use
|
|
42516
|
+
on extreme-threat-model deployments where an
|
|
42517
|
+
attacker-on-host with heap-dump access could
|
|
42518
|
+
recover residual passphrase or key bytes
|
|
42519
|
+
between the wipe and the next operator
|
|
42520
|
+
command. JS strings cannot be explicitly
|
|
42521
|
+
zeroed; this flag is the supported way to
|
|
42522
|
+
bound the heap-dump window.
|
|
40926
42523
|
--help, -h Show this help.
|
|
40927
42524
|
|
|
40928
42525
|
Without --mode, the command surveys which paths are operationally available
|
|
@@ -41192,8 +42789,18 @@ async function prompt(lines, err, question) {
|
|
|
41192
42789
|
err.write(question);
|
|
41193
42790
|
return await lines.next();
|
|
41194
42791
|
}
|
|
42792
|
+
function zeroizeBuffers(buffers) {
|
|
42793
|
+
if (!buffers) return;
|
|
42794
|
+
for (const b of buffers) {
|
|
42795
|
+
if (!b) continue;
|
|
42796
|
+
try {
|
|
42797
|
+
b.fill(0);
|
|
42798
|
+
} catch {
|
|
42799
|
+
}
|
|
42800
|
+
}
|
|
42801
|
+
}
|
|
41195
42802
|
async function defaultExec2(cmd, args) {
|
|
41196
|
-
return await new Promise((
|
|
42803
|
+
return await new Promise((resolve8, reject) => {
|
|
41197
42804
|
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
41198
42805
|
let stdout = "";
|
|
41199
42806
|
let stderr = "";
|
|
@@ -41204,7 +42811,7 @@ async function defaultExec2(cmd, args) {
|
|
|
41204
42811
|
stderr += d.toString();
|
|
41205
42812
|
});
|
|
41206
42813
|
child.on("error", reject);
|
|
41207
|
-
child.on("close", (code) =>
|
|
42814
|
+
child.on("close", (code) => resolve8({ stdout, stderr, code }));
|
|
41208
42815
|
});
|
|
41209
42816
|
}
|
|
41210
42817
|
var LineReader;
|
|
@@ -41240,8 +42847,8 @@ var init_reset_passphrase = __esm({
|
|
|
41240
42847
|
return Promise.resolve(this.queue.shift());
|
|
41241
42848
|
}
|
|
41242
42849
|
if (this.closed) return Promise.resolve("");
|
|
41243
|
-
return new Promise((
|
|
41244
|
-
this.waiters.push(
|
|
42850
|
+
return new Promise((resolve8) => {
|
|
42851
|
+
this.waiters.push(resolve8);
|
|
41245
42852
|
});
|
|
41246
42853
|
}
|
|
41247
42854
|
close() {
|
|
@@ -41640,11 +43247,11 @@ async function startMultiDashboardServer(options = {}) {
|
|
|
41640
43247
|
}
|
|
41641
43248
|
}
|
|
41642
43249
|
});
|
|
41643
|
-
await new Promise((
|
|
43250
|
+
await new Promise((resolve8, reject) => {
|
|
41644
43251
|
server.once("error", reject);
|
|
41645
43252
|
server.listen(port, host, () => {
|
|
41646
43253
|
server.off("error", reject);
|
|
41647
|
-
|
|
43254
|
+
resolve8();
|
|
41648
43255
|
});
|
|
41649
43256
|
});
|
|
41650
43257
|
const addr = server.address();
|
|
@@ -41653,8 +43260,8 @@ async function startMultiDashboardServer(options = {}) {
|
|
|
41653
43260
|
url: `http://${host}:${actualPort}`,
|
|
41654
43261
|
port: actualPort,
|
|
41655
43262
|
host,
|
|
41656
|
-
stop: () => new Promise((
|
|
41657
|
-
server.close((err) => err ? reject(err) :
|
|
43263
|
+
stop: () => new Promise((resolve8, reject) => {
|
|
43264
|
+
server.close((err) => err ? reject(err) : resolve8());
|
|
41658
43265
|
})
|
|
41659
43266
|
};
|
|
41660
43267
|
}
|
|
@@ -41872,7 +43479,19 @@ Refusing to start the dashboard while the reset-history marker is unreadable.`
|
|
|
41872
43479
|
}
|
|
41873
43480
|
throw err;
|
|
41874
43481
|
}
|
|
41875
|
-
|
|
43482
|
+
let policy;
|
|
43483
|
+
try {
|
|
43484
|
+
policy = await loadPrincipalPolicy(config.storage_path);
|
|
43485
|
+
} catch (err) {
|
|
43486
|
+
if (err instanceof MalformedPrincipalPolicyError) {
|
|
43487
|
+
console.error(`
|
|
43488
|
+
Sanctuary cannot start.
|
|
43489
|
+
${err.message}
|
|
43490
|
+
`);
|
|
43491
|
+
process.exit(1);
|
|
43492
|
+
}
|
|
43493
|
+
throw err;
|
|
43494
|
+
}
|
|
41876
43495
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
41877
43496
|
await baseline.load();
|
|
41878
43497
|
const dashboardPort = options.port ?? config.dashboard.port;
|
|
@@ -42054,7 +43673,7 @@ function formatUpdateMessage(current, latest) {
|
|
|
42054
43673
|
return `[Sanctuary] Update available: ${current} \u2192 ${latest}. Run: npx @sanctuary-framework/mcp-server@latest`;
|
|
42055
43674
|
}
|
|
42056
43675
|
function fetchLatestVersion(currentVersion) {
|
|
42057
|
-
return new Promise((
|
|
43676
|
+
return new Promise((resolve8) => {
|
|
42058
43677
|
const req = get(
|
|
42059
43678
|
REGISTRY_URL,
|
|
42060
43679
|
{
|
|
@@ -42064,7 +43683,7 @@ function fetchLatestVersion(currentVersion) {
|
|
|
42064
43683
|
(res) => {
|
|
42065
43684
|
if (res.statusCode !== 200) {
|
|
42066
43685
|
res.resume();
|
|
42067
|
-
|
|
43686
|
+
resolve8(null);
|
|
42068
43687
|
return;
|
|
42069
43688
|
}
|
|
42070
43689
|
let data = "";
|
|
@@ -42073,7 +43692,7 @@ function fetchLatestVersion(currentVersion) {
|
|
|
42073
43692
|
data += chunk;
|
|
42074
43693
|
if (data.length > 32768) {
|
|
42075
43694
|
res.destroy();
|
|
42076
|
-
|
|
43695
|
+
resolve8(null);
|
|
42077
43696
|
}
|
|
42078
43697
|
});
|
|
42079
43698
|
res.on("end", () => {
|
|
@@ -42081,20 +43700,20 @@ function fetchLatestVersion(currentVersion) {
|
|
|
42081
43700
|
const json = JSON.parse(data);
|
|
42082
43701
|
const latest = json.version;
|
|
42083
43702
|
if (typeof latest === "string" && isNewerVersion(currentVersion, latest)) {
|
|
42084
|
-
|
|
43703
|
+
resolve8(latest);
|
|
42085
43704
|
} else {
|
|
42086
|
-
|
|
43705
|
+
resolve8(null);
|
|
42087
43706
|
}
|
|
42088
43707
|
} catch {
|
|
42089
|
-
|
|
43708
|
+
resolve8(null);
|
|
42090
43709
|
}
|
|
42091
43710
|
});
|
|
42092
43711
|
}
|
|
42093
43712
|
);
|
|
42094
|
-
req.on("error", () =>
|
|
43713
|
+
req.on("error", () => resolve8(null));
|
|
42095
43714
|
req.on("timeout", () => {
|
|
42096
43715
|
req.destroy();
|
|
42097
|
-
|
|
43716
|
+
resolve8(null);
|
|
42098
43717
|
});
|
|
42099
43718
|
});
|
|
42100
43719
|
}
|
|
@@ -42172,6 +43791,11 @@ async function main() {
|
|
|
42172
43791
|
const code = await runTemplateCommand2({ argv: args.slice(1) });
|
|
42173
43792
|
process.exit(code);
|
|
42174
43793
|
}
|
|
43794
|
+
if (args[0] === "identity") {
|
|
43795
|
+
const { runIdentityCommand: runIdentityCommand2 } = await Promise.resolve().then(() => (init_identity2(), identity_exports2));
|
|
43796
|
+
const code = await runIdentityCommand2({ argv: args.slice(1) });
|
|
43797
|
+
process.exit(code);
|
|
43798
|
+
}
|
|
42175
43799
|
if (args[0] === "agents") {
|
|
42176
43800
|
const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
42177
43801
|
const code = await runAgentsCommand2({ argv: args.slice(1) });
|
|
@@ -42393,6 +44017,9 @@ Subcommands:
|
|
|
42393
44017
|
Use "sanctuary dashboard --help" for options.
|
|
42394
44018
|
Pass --multi to render the multi-tenant overview.
|
|
42395
44019
|
|
|
44020
|
+
identity Inspect the active identity (DID, public key).
|
|
44021
|
+
Use "sanctuary identity --help" for options.
|
|
44022
|
+
|
|
42396
44023
|
template Manage policy templates (list, init).
|
|
42397
44024
|
Use "sanctuary template --help" for options.
|
|
42398
44025
|
|