@sanctuary-framework/mcp-server 1.0.0-rc.1 → 1.0.0-rc.2

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 CHANGED
@@ -23080,9 +23080,7 @@ var init_registry2 = __esm({
23080
23080
  "coding-assistant",
23081
23081
  "ops-runner",
23082
23082
  "planner",
23083
- "handoff-coordinator",
23084
- "x-miner",
23085
- "github-miner"
23083
+ "handoff-coordinator"
23086
23084
  ];
23087
23085
  TemplateValidationError = class extends Error {
23088
23086
  constructor(templateName, message) {
@@ -23909,1686 +23907,1836 @@ var init_init = __esm({
23909
23907
  init_registry2();
23910
23908
  }
23911
23909
  });
23912
- function constantTimeEquals(a, b) {
23913
- if (a.length !== b.length) return false;
23914
- let diff = 0;
23915
- for (let i = 0; i < a.length; i++) {
23916
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
23917
- }
23918
- return diff === 0;
23910
+ function resolveStoragePath(env = process.env, home = os.homedir()) {
23911
+ const override = env.SANCTUARY_STORAGE_PATH;
23912
+ if (override && override.length > 0) return override;
23913
+ return path.join(home, DEFAULT_STORAGE_DIR);
23919
23914
  }
23920
- function extractToken(req, url) {
23921
- const header = req.headers.authorization;
23922
- if (header && header.startsWith("Bearer ")) {
23923
- return header.slice(7).trim();
23915
+ function resolveDashboardPort(explicitPort, env = process.env) {
23916
+ if (typeof explicitPort === "number" && !Number.isNaN(explicitPort)) {
23917
+ return explicitPort;
23924
23918
  }
23925
- const q = url.searchParams.get("token");
23926
- return q ?? null;
23927
- }
23928
- function isAuthorized(deps, req, url) {
23929
- if (!deps.authToken) return true;
23930
- const token = extractToken(req, url);
23931
- if (!token) return false;
23932
- return constantTimeEquals(token, deps.authToken);
23933
- }
23934
- function writeJSON(res, status, payload) {
23935
- res.writeHead(status, {
23936
- "Content-Type": "application/json",
23937
- "Cache-Control": "no-store"
23938
- });
23939
- res.end(JSON.stringify(payload));
23940
- }
23941
- async function readJSONBody(req) {
23942
- const chunks = [];
23943
- let size = 0;
23944
- const MAX = 256 * 1024;
23945
- for await (const chunk of req) {
23946
- size += chunk.length;
23947
- if (size > MAX) throw new Error("request body too large");
23948
- chunks.push(chunk);
23919
+ const envPort = env.SANCTUARY_DASHBOARD_PORT;
23920
+ if (envPort) {
23921
+ const parsed = parseInt(envPort, 10);
23922
+ if (!Number.isNaN(parsed)) return parsed;
23949
23923
  }
23950
- const body = Buffer.concat(chunks).toString("utf-8");
23951
- if (!body) return {};
23952
- return JSON.parse(body);
23953
- }
23954
- function generateEphemeralKey() {
23955
- return new Uint8Array(crypto.randomBytes(32));
23956
- }
23957
- function writeText(res, status, body, contentType = "text/plain") {
23958
- res.writeHead(status, {
23959
- "Content-Type": contentType,
23960
- "Cache-Control": "no-store"
23961
- });
23962
- res.end(body);
23924
+ return DEFAULT_DASHBOARD_PORT;
23963
23925
  }
23964
- async function handleRequest(deps, req, res) {
23965
- const host = req.headers.host || "localhost";
23966
- const url = new URL(req.url ?? "/", `http://${host}`);
23967
- const method = (req.method ?? "GET").toUpperCase();
23968
- const path = url.pathname;
23969
- if (!isAuthorized(deps, req, url)) {
23970
- writeJSON(res, 401, { error: "unauthorized" });
23971
- return true;
23926
+ var DEFAULT_STORAGE_DIR, DEFAULT_DASHBOARD_PORT;
23927
+ var init_paths = __esm({
23928
+ "src/paths.ts"() {
23929
+ DEFAULT_STORAGE_DIR = ".sanctuary";
23930
+ DEFAULT_DASHBOARD_PORT = 3501;
23972
23931
  }
23973
- if (method === "GET" && path === "/api/health") {
23974
- writeJSON(res, 200, { ok: true, mode: deps.sources.mode });
23975
- return true;
23932
+ });
23933
+
23934
+ // src/cocoon/passphrase.ts
23935
+ var passphrase_exports = {};
23936
+ __export(passphrase_exports, {
23937
+ PassphraseUnreadableError: () => PassphraseUnreadableError,
23938
+ fallbackFilePath: () => fallbackFilePath,
23939
+ generatePassphrase: () => generatePassphrase,
23940
+ getOrCreatePassphrase: () => getOrCreatePassphrase,
23941
+ keychainServiceFor: () => keychainServiceFor,
23942
+ persistUserProvidedPassphrase: () => persistUserProvidedPassphrase,
23943
+ readStoredPassphrase: () => readStoredPassphrase
23944
+ });
23945
+ async function getOrCreatePassphrase(opts = {}) {
23946
+ const home = opts.home ?? os.homedir();
23947
+ const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
23948
+ const service = keychainServiceFor(storagePath, home);
23949
+ const plat = opts.platformOverride ?? os.platform();
23950
+ const exec2 = opts.exec ?? defaultExec;
23951
+ const derive = opts.deriveMachineKey ?? deriveMachineKey;
23952
+ if (plat === "darwin") {
23953
+ const fromKc = await readFromKeychain(exec2, service);
23954
+ if (fromKc) {
23955
+ return { value: fromKc, source: "keychain", location: "macOS Keychain" };
23956
+ }
23976
23957
  }
23977
- if (method === "GET" && (path === "/" || path === "/index.html")) {
23978
- const snapshot = await getProtectionSnapshot(deps.sources);
23979
- const html = renderDashboardHTML({ snapshot, authToken: deps.authToken });
23980
- writeText(res, 200, html, "text/html; charset=utf-8");
23981
- return true;
23958
+ const fallback = fallbackFilePath(home, storagePath);
23959
+ const fromFile = await readFromFallbackFile(fallback, home, derive);
23960
+ if (fromFile.status === "ok") {
23961
+ return {
23962
+ value: fromFile.value,
23963
+ source: "fallback-file",
23964
+ location: fallback
23965
+ };
23982
23966
  }
23983
- if (method === "GET" && path === "/api/snapshot") {
23984
- const snapshot = await getProtectionSnapshot(deps.sources);
23985
- writeJSON(res, 200, snapshot);
23986
- return true;
23967
+ if (fromFile.status === "unreadable") {
23968
+ throw new PassphraseUnreadableError(fallback, fromFile.reason);
23987
23969
  }
23988
- const approvalMatch = /^\/api\/approvals\/([^/]+)\/(allow|deny)$/.exec(path);
23989
- if (method === "POST" && approvalMatch) {
23990
- const id = decodeURIComponent(approvalMatch[1]);
23991
- const action = approvalMatch[2];
23992
- if (!deps.approvals) {
23993
- writeJSON(res, 503, { error: "approvals_unavailable" });
23994
- return true;
23995
- }
23996
- const handler = action === "allow" ? deps.approvals.allow : deps.approvals.deny;
23997
- try {
23998
- const ok2 = await handler(id);
23999
- writeJSON(res, ok2 ? 200 : 404, { id, action, ok: ok2 });
24000
- } catch (err) {
24001
- writeJSON(res, 500, { error: "approval_failed", message: err.message });
23970
+ const value = generatePassphrase();
23971
+ if (plat === "darwin") {
23972
+ const ok2 = await writeToKeychain(value, exec2, service);
23973
+ if (ok2) {
23974
+ return { value, source: "generated", location: "macOS Keychain" };
24002
23975
  }
24003
- return true;
24004
- }
24005
- if (method === "GET" && path === "/api/stream") {
24006
- await handleStream(deps, res);
24007
- return true;
24008
23976
  }
24009
- if (method === "GET" && path === "/api/templates") {
24010
- try {
24011
- const templates = listTemplates();
24012
- writeJSON(res, 200, { templates });
24013
- } catch (err) {
24014
- writeJSON(res, 500, {
24015
- error: "template_load_failed",
24016
- message: err.message
24017
- });
23977
+ await writeToFallbackFile(fallback, value, home, derive);
23978
+ return { value, source: "generated", location: fallback };
23979
+ }
23980
+ async function readStoredPassphrase(opts = {}) {
23981
+ const home = opts.home ?? os.homedir();
23982
+ const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
23983
+ const service = keychainServiceFor(storagePath, home);
23984
+ const plat = opts.platformOverride ?? os.platform();
23985
+ const exec2 = opts.exec ?? defaultExec;
23986
+ const derive = opts.deriveMachineKey ?? deriveMachineKey;
23987
+ if (plat === "darwin") {
23988
+ const fromKc = await readFromKeychain(exec2, service);
23989
+ if (fromKc) {
23990
+ return { value: fromKc, source: "keychain", location: "macOS Keychain" };
24018
23991
  }
24019
- return true;
24020
23992
  }
24021
- const templateMatch = /^\/api\/templates\/([^/]+)$/.exec(path);
24022
- if (method === "GET" && templateMatch) {
24023
- const name = decodeURIComponent(templateMatch[1]);
24024
- try {
24025
- const entry = getTemplateEntry(name);
24026
- if (!entry) {
24027
- writeJSON(res, 404, { error: "template_not_found", name });
24028
- return true;
24029
- }
24030
- writeJSON(res, 200, entry);
24031
- } catch (err) {
24032
- writeJSON(res, 500, {
24033
- error: "template_load_failed",
24034
- message: err.message
24035
- });
24036
- }
24037
- return true;
23993
+ const fallback = fallbackFilePath(home, storagePath);
23994
+ const fromFile = await readFromFallbackFile(fallback, home, derive);
23995
+ if (fromFile.status === "ok") {
23996
+ return {
23997
+ value: fromFile.value,
23998
+ source: "fallback-file",
23999
+ location: fallback
24000
+ };
24038
24001
  }
24039
- const initMatch = /^\/api\/templates\/([^/]+)\/init$/.exec(path);
24040
- if (method === "POST" && initMatch) {
24041
- const name = decodeURIComponent(initMatch[1]);
24042
- try {
24043
- const bundle = getTemplate2(name);
24044
- if (!bundle) {
24045
- writeJSON(res, 404, { error: "template_not_found", name });
24046
- return true;
24047
- }
24048
- const body = await readJSONBody(req);
24049
- if (!body.agent_name || typeof body.agent_name !== "string") {
24050
- writeJSON(res, 400, {
24051
- error: "validation_error",
24052
- message: "agent_name is required and must be a string"
24053
- });
24054
- return true;
24055
- }
24056
- if (!/^[a-zA-Z0-9_-]+$/.test(body.agent_name)) {
24057
- writeJSON(res, 400, {
24058
- error: "validation_error",
24059
- message: "agent_name must contain only alphanumeric characters, hyphens, and underscores"
24060
- });
24061
- return true;
24062
- }
24063
- const nodeId = deps.nodeId ?? "dashboard-node";
24064
- const nodePrivateKey = deps.nodePrivateKey ?? generateEphemeralKey();
24065
- const principalId = deps.principalId ?? "dashboard-principal";
24066
- const fortressId = deps.fortressId ?? "default";
24067
- const result = initTemplate({
24068
- template_name: name,
24069
- agent_id: body.agent_name,
24070
- fortress_id: fortressId,
24071
- counterparty: "*",
24072
- policy_version: 1,
24073
- emitter_node: nodeId,
24074
- emitter_principal: principalId,
24075
- monotonic_seq: 1,
24076
- node_private_key: nodePrivateKey
24077
- });
24078
- writeJSON(res, 200, {
24079
- agent_id: body.agent_name,
24080
- signed_event_id: result.signed_event.event_id,
24081
- policy_version: result.compiled.policy_version,
24082
- template_name: name,
24083
- attestation_panel_url: `/console#agent_roster`
24084
- });
24085
- } catch (err) {
24086
- writeJSON(res, 500, {
24087
- error: "template_init_failed",
24088
- message: err.message
24089
- });
24090
- }
24091
- return true;
24002
+ if (fromFile.status === "unreadable") {
24003
+ throw new PassphraseUnreadableError(fallback, fromFile.reason);
24092
24004
  }
24093
- return false;
24005
+ return null;
24094
24006
  }
24095
- async function handleStream(deps, res) {
24096
- res.writeHead(200, {
24097
- "Content-Type": "text/event-stream",
24098
- "Cache-Control": "no-cache, no-transform",
24099
- Connection: "keep-alive",
24100
- "X-Accel-Buffering": "no"
24101
- });
24102
- const snapshot = await getProtectionSnapshot(deps.sources);
24103
- res.write(`event: snapshot
24104
- data: ${JSON.stringify(snapshot)}
24105
-
24106
- `);
24107
- const unsubscribe = deps.onEvent ? deps.onEvent((event) => {
24108
- try {
24109
- res.write(`event: ${event.type}
24110
- data: ${JSON.stringify(event.data)}
24111
-
24112
- `);
24113
- } catch {
24114
- }
24115
- }) : () => {
24116
- };
24117
- const keepAlive = setInterval(() => {
24118
- try {
24119
- res.write(": keepalive\n\n");
24120
- } catch {
24007
+ async function persistUserProvidedPassphrase(value, opts = {}) {
24008
+ const home = opts.home ?? os.homedir();
24009
+ const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
24010
+ const service = keychainServiceFor(storagePath, home);
24011
+ const plat = opts.platformOverride ?? os.platform();
24012
+ const exec2 = opts.exec ?? defaultExec;
24013
+ const derive = opts.deriveMachineKey ?? deriveMachineKey;
24014
+ if (plat === "darwin") {
24015
+ const ok2 = await writeToKeychain(value, exec2, service);
24016
+ if (ok2) {
24017
+ return { location: "macOS Keychain", source: "keychain" };
24121
24018
  }
24122
- }, 25e3);
24123
- const cleanup = () => {
24124
- clearInterval(keepAlive);
24125
- unsubscribe();
24126
- };
24127
- res.on("close", cleanup);
24128
- res.on("error", cleanup);
24019
+ }
24020
+ const fallback = fallbackFilePath(home, storagePath);
24021
+ try {
24022
+ await writeToFallbackFile(fallback, value, home, derive);
24023
+ } catch (err) {
24024
+ throw new Error(
24025
+ `Could not persist the provided passphrase to either Keychain or ${fallback}: ${err.message}. Refusing to proceed \u2014 writing the passphrase into the rewritten agent config would leak it as plaintext at rest and in process argv.`
24026
+ );
24027
+ }
24028
+ return { location: fallback, source: "fallback-file" };
24129
24029
  }
24130
- var init_api = __esm({
24131
- "src/dashboard/api.ts"() {
24132
- init_aggregator();
24133
- init_html();
24134
- init_registry2();
24135
- init_init();
24030
+ function generatePassphrase() {
24031
+ return crypto.randomBytes(32).toString("base64");
24032
+ }
24033
+ async function readFromKeychain(exec2, service = KEYCHAIN_SERVICE_DEFAULT) {
24034
+ try {
24035
+ const result = await exec2(
24036
+ "security",
24037
+ ["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"]
24038
+ );
24039
+ if (result.code !== 0) return null;
24040
+ const value = result.stdout.trim();
24041
+ return value.length > 0 ? value : null;
24042
+ } catch {
24043
+ return null;
24136
24044
  }
24137
- });
24138
- async function startDashboardServer(options) {
24139
- const port = options.port ?? DEFAULT_PORT;
24140
- const host = options.host ?? DEFAULT_HOST;
24141
- const listeners = /* @__PURE__ */ new Set();
24142
- const onEvent = (listener) => {
24143
- listeners.add(listener);
24144
- return () => listeners.delete(listener);
24145
- };
24146
- const publish = (event) => {
24147
- for (const listener of listeners) {
24148
- try {
24149
- listener(event);
24150
- } catch {
24151
- }
24152
- }
24153
- };
24154
- const deps = {
24155
- sources: options.sources,
24156
- authToken: options.authToken,
24157
- approvals: options.approvals,
24158
- onEvent
24159
- };
24160
- const server = http.createServer(async (req, res) => {
24161
- try {
24162
- const served = await handleRequest(deps, req, res);
24163
- if (!served) {
24164
- res.writeHead(404, { "Content-Type": "application/json" });
24165
- res.end(JSON.stringify({ error: "not_found", path: req.url }));
24166
- }
24167
- } catch (err) {
24168
- try {
24169
- res.writeHead(500, { "Content-Type": "application/json" });
24170
- res.end(JSON.stringify({ error: "internal", message: err.message }));
24171
- } catch {
24172
- }
24045
+ }
24046
+ async function writeToKeychain(value, exec2, service = KEYCHAIN_SERVICE_DEFAULT) {
24047
+ try {
24048
+ const result = await exec2(
24049
+ "security",
24050
+ [
24051
+ "add-generic-password",
24052
+ "-U",
24053
+ "-a",
24054
+ KEYCHAIN_ACCOUNT,
24055
+ "-s",
24056
+ service,
24057
+ "-w",
24058
+ value
24059
+ ]
24060
+ );
24061
+ return result.code === 0;
24062
+ } catch {
24063
+ return false;
24064
+ }
24065
+ }
24066
+ function keychainServiceFor(storagePath, home = os.homedir()) {
24067
+ const defaultPath = path.join(home, DEFAULT_STORAGE_DIR);
24068
+ if (storagePath === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
24069
+ const digest = sha256.sha256(Buffer.from(storagePath, "utf-8"));
24070
+ const suffix = Buffer.from(digest).toString("hex").slice(0, 12);
24071
+ return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
24072
+ }
24073
+ function fallbackFilePath(home, storagePath) {
24074
+ if (storagePath !== void 0) return path.join(storagePath, "passphrase.enc");
24075
+ return path.join(home, DEFAULT_STORAGE_DIR, "passphrase.enc");
24076
+ }
24077
+ async function readFromFallbackFile(path, home, derive = deriveMachineKey) {
24078
+ try {
24079
+ await promises.access(path);
24080
+ } catch {
24081
+ return { status: "not-found" };
24082
+ }
24083
+ try {
24084
+ const raw = await promises.readFile(path);
24085
+ if (raw.length < 13) {
24086
+ return { status: "unreadable", reason: "file too short to contain a valid nonce + ciphertext" };
24173
24087
  }
24174
- });
24175
- await new Promise((resolve2, reject) => {
24176
- server.once("error", reject);
24177
- server.listen(port, host, () => {
24178
- server.off("error", reject);
24179
- resolve2();
24088
+ const nonce = raw.subarray(0, 12);
24089
+ const ciphertext = raw.subarray(12);
24090
+ const key = derive(home);
24091
+ const cipher = aes_js.gcm(key, nonce);
24092
+ const plain = cipher.decrypt(ciphertext);
24093
+ return { status: "ok", value: Buffer.from(plain).toString("utf-8") };
24094
+ } catch (err) {
24095
+ return {
24096
+ status: "unreadable",
24097
+ reason: err.message ?? "unknown decryption error"
24098
+ };
24099
+ }
24100
+ }
24101
+ async function writeToFallbackFile(path$1, value, home, derive = deriveMachineKey) {
24102
+ const dir = path.dirname(path$1);
24103
+ await promises.mkdir(dir, { recursive: true, mode: 448 });
24104
+ const nonce = crypto.randomBytes(12);
24105
+ const key = derive(home);
24106
+ const cipher = aes_js.gcm(key, nonce);
24107
+ const ciphertext = cipher.encrypt(Buffer.from(value, "utf-8"));
24108
+ const payload = Buffer.concat([nonce, Buffer.from(ciphertext)]);
24109
+ await promises.writeFile(path$1, payload, { mode: 384 });
24110
+ }
24111
+ function deriveMachineKey(home) {
24112
+ const info = os.userInfo();
24113
+ const material = Buffer.from(
24114
+ `${os.hostname()}:${info.uid}:${info.username}:${home}`,
24115
+ "utf-8"
24116
+ );
24117
+ return hkdf.hkdf(sha256.sha256, material, void 0, "sanctuary-passphrase-v1", 32);
24118
+ }
24119
+ async function defaultExec(cmd, args, input) {
24120
+ return new Promise((resolve2, reject) => {
24121
+ const child = child_process.spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
24122
+ let stdout = "";
24123
+ let stderr = "";
24124
+ child.stdout.on("data", (d) => {
24125
+ stdout += d.toString();
24126
+ });
24127
+ child.stderr.on("data", (d) => {
24128
+ stderr += d.toString();
24180
24129
  });
24130
+ child.on("error", reject);
24131
+ child.on("close", (code) => resolve2({ stdout, stderr, code }));
24132
+ if (input !== void 0) {
24133
+ child.stdin.write(input);
24134
+ }
24135
+ child.stdin.end();
24181
24136
  });
24182
- const actualPort = (() => {
24183
- const addr = server.address();
24184
- if (addr && typeof addr === "object") return addr.port;
24185
- return port;
24186
- })();
24187
- const url = `http://${host}:${actualPort}`;
24188
- return {
24189
- url,
24190
- port: actualPort,
24191
- host,
24192
- stop: () => new Promise((resolve2, reject) => {
24193
- server.close((err) => err ? reject(err) : resolve2());
24194
- }),
24195
- publish,
24196
- publishActivity: (entry) => publish({ type: "activity", data: entry }),
24197
- publishApproval: (approval) => publish({ type: "approval", data: approval })
24198
- };
24199
24137
  }
24200
- var DEFAULT_PORT, DEFAULT_HOST;
24201
- var init_server = __esm({
24202
- "src/dashboard/server.ts"() {
24203
- init_api();
24204
- DEFAULT_PORT = 3501;
24205
- DEFAULT_HOST = "127.0.0.1";
24138
+ var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_DEFAULT, PassphraseUnreadableError;
24139
+ var init_passphrase = __esm({
24140
+ "src/cocoon/passphrase.ts"() {
24141
+ init_paths();
24142
+ KEYCHAIN_ACCOUNT = "sanctuary";
24143
+ KEYCHAIN_SERVICE_DEFAULT = "sanctuary-passphrase";
24144
+ PassphraseUnreadableError = class extends Error {
24145
+ path;
24146
+ reason;
24147
+ constructor(path, reason) {
24148
+ super(
24149
+ `Sanctuary passphrase file at ${path} exists but could not be decrypted (${reason}).
24150
+
24151
+ Your existing encrypted state cannot be recovered with a new passphrase. Options:
24152
+ 1. Restore ${path} from a backup.
24153
+ 2. Re-import the original passphrase via SANCTUARY_PASSPHRASE=<value> sanctuary wrap ...
24154
+ 3. Run \`sanctuary reset-passphrase\` (coming soon) to wipe state and start fresh.
24155
+
24156
+ Refusing to regenerate the passphrase \u2014 that would permanently destroy the data encrypted under the previous key.`
24157
+ );
24158
+ this.name = "PassphraseUnreadableError";
24159
+ this.path = path;
24160
+ this.reason = reason;
24161
+ }
24162
+ };
24206
24163
  }
24207
24164
  });
24208
-
24209
- // src/dashboard/index.ts
24210
- async function startDashboard(options) {
24211
- const activity = options.initialActivity ? [...options.initialActivity] : [];
24212
- const pending = options.initialPendingApprovals ? [...options.initialPendingApprovals] : [];
24213
- const sources = {
24214
- mode: options.mode,
24215
- server_version: options.serverVersion,
24216
- ...options.auditLog ? { auditLog: options.auditLog } : {},
24217
- ...options.identityManager ? { identityManager: options.identityManager } : {},
24218
- ...options.clientManager ? { clientManager: options.clientManager } : {},
24219
- ...options.baseline ? { baseline: options.baseline } : {},
24220
- ...options.policy ? { policy: options.policy } : {},
24221
- ...options.reputation ? { reputation: options.reputation } : {},
24222
- ...options.teeAvailable != null ? { teeAvailable: options.teeAvailable } : {},
24223
- ...options.l4Evidence ? { l4Evidence: options.l4Evidence } : {},
24224
- activity,
24225
- pendingApprovals: pending
24226
- };
24227
- const serverOpts = {
24228
- mode: options.mode,
24229
- sources,
24230
- ...options.port != null ? { port: options.port } : {},
24231
- ...options.host ? { host: options.host } : {},
24232
- ...options.authToken ? { authToken: options.authToken } : {},
24233
- ...options.approvals ? { approvals: options.approvals } : {}
24234
- };
24235
- const handle = await startDashboardServer(serverOpts);
24236
- const wrapped = {
24237
- ...handle,
24238
- publishActivity: (entry) => {
24239
- activity.unshift(entry);
24240
- if (activity.length > 50) activity.length = 50;
24241
- handle.publishActivity(entry);
24242
- },
24243
- publishApproval: (approval) => {
24244
- pending.push(approval);
24245
- handle.publishApproval(approval);
24165
+ function runtimePath(storagePath) {
24166
+ return path.join(storagePath, RUNTIME_FILE_NAME);
24167
+ }
24168
+ async function writeTenantRuntime(storagePath, state) {
24169
+ try {
24170
+ await promises.writeFile(
24171
+ runtimePath(storagePath),
24172
+ JSON.stringify(state, null, 2),
24173
+ { mode: 384 }
24174
+ );
24175
+ } catch {
24176
+ }
24177
+ }
24178
+ async function clearTenantRuntime(storagePath) {
24179
+ try {
24180
+ await promises.unlink(runtimePath(storagePath));
24181
+ } catch {
24182
+ }
24183
+ }
24184
+ async function readTenantRuntime(storagePath) {
24185
+ try {
24186
+ const raw = await promises.readFile(runtimePath(storagePath), "utf-8");
24187
+ const parsed = JSON.parse(raw);
24188
+ if (typeof parsed.dashboard_port !== "number" || typeof parsed.pid !== "number" || typeof parsed.started_at !== "string" || typeof parsed.version !== "string" || typeof parsed.dashboard_host !== "string" || typeof parsed.mode !== "string") {
24189
+ return null;
24246
24190
  }
24247
- };
24248
- return wrapped;
24191
+ const state = {
24192
+ version: parsed.version,
24193
+ pid: parsed.pid,
24194
+ started_at: parsed.started_at,
24195
+ dashboard_host: parsed.dashboard_host,
24196
+ dashboard_port: parsed.dashboard_port,
24197
+ mode: parsed.mode
24198
+ };
24199
+ if (typeof parsed.webhook_callback_port === "number") {
24200
+ state.webhook_callback_port = parsed.webhook_callback_port;
24201
+ }
24202
+ if (typeof parsed.webhook_callback_host === "string") {
24203
+ state.webhook_callback_host = parsed.webhook_callback_host;
24204
+ }
24205
+ return state;
24206
+ } catch {
24207
+ return null;
24208
+ }
24249
24209
  }
24250
- var init_dashboard2 = __esm({
24251
- "src/dashboard/index.ts"() {
24252
- init_server();
24253
- init_aggregator();
24254
- init_html();
24255
- init_api();
24256
- init_server();
24210
+ var RUNTIME_FILE_NAME;
24211
+ var init_runtime = __esm({
24212
+ "src/cli/agents/runtime.ts"() {
24213
+ RUNTIME_FILE_NAME = "runtime.json";
24257
24214
  }
24258
24215
  });
24259
- async function createSanctuaryServer(options) {
24260
- const config = await loadConfig(options?.configPath);
24261
- await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
24262
- await tightenStoragePermissions(config.storage_path);
24263
- const storage = options?.storage ?? new FilesystemStorage(
24264
- `${config.storage_path}/state`
24265
- );
24266
- let masterKey;
24267
- let keyProtection;
24268
- let recoveryKey;
24269
- const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
24270
- if (passphrase) {
24271
- keyProtection = "passphrase";
24272
- let existingParams;
24216
+ async function isTenantDir(path$1) {
24217
+ const [hasState, hasProfile, hasFallback] = await Promise.all([
24218
+ dirExists(path.join(path$1, "state")),
24219
+ fileExists2(path.join(path$1, "cocoon-profile.json")),
24220
+ fileExists2(path.join(path$1, "passphrase.enc"))
24221
+ ]);
24222
+ const initialized = hasState;
24223
+ let passphraseStatus;
24224
+ if (hasFallback) passphraseStatus = "fallback-file";
24225
+ else if (hasProfile || hasState) passphraseStatus = "keychain";
24226
+ else passphraseStatus = "not-initialized";
24227
+ return { initialized, hasProfile, passphraseStatus };
24228
+ }
24229
+ async function dirExists(path) {
24230
+ try {
24231
+ const s = await promises.stat(path);
24232
+ return s.isDirectory();
24233
+ } catch {
24234
+ return false;
24235
+ }
24236
+ }
24237
+ async function fileExists2(path) {
24238
+ try {
24239
+ const s = await promises.stat(path);
24240
+ return s.isFile();
24241
+ } catch {
24242
+ return false;
24243
+ }
24244
+ }
24245
+ async function newestAuditMtime(storagePath) {
24246
+ const auditDir = path.join(storagePath, "state", "_audit");
24247
+ let entries = [];
24248
+ try {
24249
+ entries = await promises.readdir(auditDir);
24250
+ } catch {
24251
+ return null;
24252
+ }
24253
+ let newest = 0;
24254
+ for (const name of entries) {
24273
24255
  try {
24274
- const raw = await storage.read("_meta", "key-params");
24275
- if (raw) {
24276
- const { bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24277
- existingParams = JSON.parse(bytesToString2(raw));
24278
- }
24256
+ const s = await promises.stat(path.join(auditDir, name));
24257
+ if (s.isFile() && s.mtimeMs > newest) newest = s.mtimeMs;
24279
24258
  } catch {
24280
24259
  }
24281
- const result = await deriveMasterKey(passphrase, existingParams);
24282
- masterKey = result.key;
24283
- if (!existingParams) {
24284
- const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24285
- await storage.write(
24286
- "_meta",
24287
- "key-params",
24288
- stringToBytes2(JSON.stringify(result.params))
24289
- );
24260
+ }
24261
+ if (newest === 0) return null;
24262
+ return new Date(newest).toISOString();
24263
+ }
24264
+ async function readExtraPaths(root, env) {
24265
+ const out = [];
24266
+ const fromEnv = env.SANCTUARY_AGENTS_EXTRA_PATHS;
24267
+ if (fromEnv && fromEnv.length > 0) {
24268
+ for (const part of fromEnv.split(":")) {
24269
+ const trimmed = part.trim();
24270
+ if (trimmed.length > 0) out.push(path.resolve(trimmed));
24290
24271
  }
24291
- } else {
24292
- keyProtection = "recovery-key";
24293
- const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
24294
- const { stringToBytes: stringToBytes2, bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24295
- const { fromBase64url: fromBase64url2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24296
- const { constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24297
- const existingHash = await storage.read("_meta", "recovery-key-hash");
24298
- if (existingHash) {
24299
- const envRecoveryKey = process.env.SANCTUARY_RECOVERY_KEY;
24300
- if (!envRecoveryKey) {
24301
- throw new Error(
24302
- "Sanctuary: Existing encrypted data found but no credentials provided.\nThis installation was previously set up with a recovery key.\n\nTo start the server, provide one of:\n - SANCTUARY_PASSPHRASE (if you later configured a passphrase)\n - SANCTUARY_RECOVERY_KEY (the recovery key shown at first run)\n\nWithout the correct credentials, encrypted state cannot be accessed.\nRefusing to start to prevent silent data loss."
24303
- );
24304
- }
24305
- let recoveryKeyBytes;
24306
- try {
24307
- recoveryKeyBytes = fromBase64url2(envRecoveryKey);
24308
- } catch {
24309
- throw new Error(
24310
- "Sanctuary: SANCTUARY_RECOVERY_KEY is not valid base64url. The recovery key should be the exact string shown at first run."
24311
- );
24312
- }
24313
- if (recoveryKeyBytes.length !== 32) {
24314
- throw new Error(
24315
- "Sanctuary: SANCTUARY_RECOVERY_KEY has incorrect length. The recovery key should be the exact string shown at first run."
24316
- );
24317
- }
24318
- const providedHash = hashToString2(recoveryKeyBytes);
24319
- const storedHash = bytesToString2(existingHash);
24320
- const providedHashBytes = stringToBytes2(providedHash);
24321
- const storedHashBytes = stringToBytes2(storedHash);
24322
- if (!constantTimeEqual2(providedHashBytes, storedHashBytes)) {
24323
- throw new Error(
24324
- "Sanctuary: Recovery key does not match the stored key hash.\nThe recovery key provided via SANCTUARY_RECOVERY_KEY is incorrect.\nUse the exact recovery key that was displayed at first run."
24325
- );
24326
- }
24327
- masterKey = recoveryKeyBytes;
24328
- } else {
24329
- const existingNamespaces = await storage.list("_meta");
24330
- const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
24331
- if (hasKeyParams) {
24332
- throw new Error(
24333
- "Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
24334
- );
24272
+ }
24273
+ try {
24274
+ const raw = await promises.readFile(path.join(root, EXTRAS_FILE_NAME), "utf-8");
24275
+ const parsed = JSON.parse(raw);
24276
+ if (Array.isArray(parsed)) {
24277
+ for (const p of parsed) {
24278
+ if (typeof p === "string" && p.trim().length > 0) out.push(path.resolve(p));
24335
24279
  }
24336
- masterKey = generateRandomKey();
24337
- recoveryKey = toBase64url(masterKey);
24338
- const keyHash = hashToString2(masterKey);
24339
- await storage.write(
24340
- "_meta",
24341
- "recovery-key-hash",
24342
- stringToBytes2(keyHash)
24343
- );
24344
24280
  }
24281
+ } catch {
24345
24282
  }
24346
- const auditLog = new AuditLog(storage, masterKey);
24347
- const stateStore = new StateStore(storage, masterKey);
24348
- const { tools: l1Tools, identityManager } = createL1Tools(
24349
- stateStore,
24350
- storage,
24351
- masterKey,
24352
- keyProtection,
24353
- auditLog
24354
- );
24355
- const loadResult = await identityManager.load();
24356
- if (loadResult.total > 0 && loadResult.loaded === 0) {
24357
- console.error(
24358
- `
24359
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
24360
- \u2551 \u26A0 WARNING: Encrypted identities found but NONE loaded \u2551
24361
- \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
24362
- \u2551 ${loadResult.total} encrypted identity file(s) found on disk \u2551
24363
- \u2551 0 could be decrypted with the current master key \u2551
24364
- \u2551 \u2551
24365
- \u2551 This usually means SANCTUARY_PASSPHRASE is missing or \u2551
24366
- \u2551 incorrect. The server will start but with NO identity data. \u2551
24367
- \u2551 \u2551
24368
- \u2551 To fix: set SANCTUARY_PASSPHRASE to the passphrase used \u2551
24369
- \u2551 when this Sanctuary instance was first configured. \u2551
24370
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
24371
- `
24372
- );
24373
- } else if (loadResult.failed > 0) {
24374
- console.error(
24375
- `Warning: ${loadResult.failed} of ${loadResult.total} identity files could not be decrypted (possibly corrupted).`
24376
- );
24377
- }
24378
- const l2Tools = [
24379
- {
24380
- name: "exec_attest",
24381
- description: "Generate an attestation of the current execution environment, including sovereignty assessment and degradation report.",
24382
- inputSchema: {
24383
- type: "object",
24384
- properties: {
24385
- include_hardware: { type: "boolean", default: true },
24386
- include_software: { type: "boolean", default: true },
24387
- include_network: { type: "boolean", default: true }
24388
- }
24389
- },
24390
- handler: async () => {
24391
- const degradations = [];
24392
- degradations.push(
24393
- "L2 isolation is process-level only; no TEE available"
24394
- );
24395
- return toolResult({
24396
- attestation: {
24397
- environment_type: config.execution.environment,
24398
- hardware: {
24399
- cpu_vendor: process.arch,
24400
- tee_available: false,
24401
- tee_type: void 0
24402
- },
24403
- software: {
24404
- os: `${process.platform}-${process.arch}`,
24405
- runtime: `node-${process.version}`,
24406
- sanctuary_version: config.version,
24407
- mcp_sdk_version: "1.26.0"
24408
- },
24409
- network: {
24410
- internet_accessible: true,
24411
- // Conservative assumption
24412
- listening_ports: [],
24413
- egress_restricted: false
24414
- },
24415
- isolation_level: "process",
24416
- sovereignty_assessment: {
24417
- l1_state_encrypted: true,
24418
- l2_execution_isolated: false,
24419
- l2_isolation_type: "process-level",
24420
- l3_proofs_available: true,
24421
- l4_reputation_active: true,
24422
- overall_level: "mvs",
24423
- degradations
24424
- }
24425
- },
24426
- attested_at: (/* @__PURE__ */ new Date()).toISOString()
24427
- });
24428
- }
24429
- },
24430
- {
24431
- name: "monitor_health",
24432
- description: "Sanctuary Health Report (SHR) \u2014 standardized sovereignty status.",
24433
- inputSchema: { type: "object", properties: {} },
24434
- handler: async () => {
24435
- const storageSizeBytes = await storage.totalSize();
24436
- const degradations = [];
24437
- degradations.push({
24438
- layer: "l2",
24439
- description: "Process-level isolation only (no TEE)",
24440
- severity: "warning",
24441
- mitigation: "TEE support planned for a future release"
24442
- });
24443
- return toolResult({
24444
- status: degradations.some((d) => d.severity === "critical") ? "compromised" : degradations.some((d) => d.severity === "warning") ? "degraded" : "healthy",
24445
- storage_bytes: storageSizeBytes,
24446
- layers: {
24447
- l1: {
24448
- status: "active",
24449
- encryption_algorithm: "aes-256-gcm",
24450
- key_count: identityManager.list().length,
24451
- state_integrity: "verified",
24452
- last_integrity_check: (/* @__PURE__ */ new Date()).toISOString()
24453
- },
24454
- l2: {
24455
- status: "degraded",
24456
- isolation_type: "process-level",
24457
- attestation_available: true,
24458
- last_attestation: (/* @__PURE__ */ new Date()).toISOString()
24459
- },
24460
- l3: {
24461
- status: "active",
24462
- proof_system: config.disclosure.proof_system,
24463
- circuits_loaded: 0,
24464
- proofs_generated_total: 0
24465
- },
24466
- l4: {
24467
- status: "active",
24468
- mode: config.reputation.mode,
24469
- interaction_count: 0,
24470
- // TODO: track from reputation store
24471
- reputation_exportable: true
24472
- }
24473
- },
24474
- degradations,
24475
- checked_at: (/* @__PURE__ */ new Date()).toISOString()
24476
- });
24477
- }
24478
- },
24479
- {
24480
- name: "monitor_audit_log",
24481
- description: "Query the sovereignty audit log.",
24482
- inputSchema: {
24483
- type: "object",
24484
- properties: {
24485
- since: { type: "string", description: "ISO 8601 timestamp" },
24486
- layer: {
24487
- type: "string",
24488
- enum: ["l1", "l2", "l3", "l4"]
24489
- },
24490
- operation_type: { type: "string" },
24491
- limit: { type: "number", default: 50 }
24492
- }
24493
- },
24494
- handler: async (args) => {
24495
- const result = await auditLog.query({
24496
- since: args.since,
24497
- layer: args.layer,
24498
- operation_type: args.operation_type,
24499
- limit: args.limit ?? 50
24500
- });
24501
- return toolResult(result);
24502
- }
24503
- }
24504
- ];
24505
- const manifestTool = {
24506
- name: "manifest",
24507
- description: "Generate the Sanctuary Interface Manifest (SIM) \u2014 a machine-readable declaration of this server's capabilities.",
24508
- inputSchema: { type: "object", properties: {} },
24509
- handler: async () => {
24510
- return toolResult({
24511
- sanctuary_version: "0.2",
24512
- implementation: {
24513
- name: "@sanctuary-framework/mcp-server",
24514
- version: config.version,
24515
- language: "typescript",
24516
- license: "Apache-2.0"
24517
- },
24518
- layers: {
24519
- l1: {
24520
- implemented: true,
24521
- interfaces: ["StateStore", "IdentityRoot"],
24522
- encryption: ["aes-256-gcm"],
24523
- identity: ["ed25519"],
24524
- properties: {
24525
- "S1.1_participant_held_keys": "full",
24526
- "S1.2_encryption_at_rest": "full",
24527
- "S1.3_integrity_verification": "full",
24528
- "S1.4_selective_state_sharing": "full",
24529
- "S1.5_state_portability": "full",
24530
- "S1.6_deletion_rights": "full",
24531
- "S1.7_identity_anchoring": "partial"
24532
- }
24533
- },
24534
- l2: {
24535
- implemented: true,
24536
- interfaces: ["ExecutionEnvironment", "RuntimeMonitor"],
24537
- isolation_types: [config.execution.environment],
24538
- properties: {
24539
- "S2.1_execution_confidentiality": "documented",
24540
- "S2.2_verifiable_execution": "self-reported",
24541
- "S2.5_attestation": "self-reported"
24542
- }
24543
- },
24544
- l3: {
24545
- implemented: true,
24546
- interfaces: ["ProofEngine", "DisclosurePolicy"],
24547
- proof_systems: [config.disclosure.proof_system],
24548
- properties: {
24549
- "S3.1_minimum_disclosure": "policy-based",
24550
- "S3.3_proof_without_revelation": "commitment"
24551
- }
24552
- },
24553
- l4: {
24554
- implemented: true,
24555
- interfaces: ["ReputationStore", "TrustBootstrap"],
24556
- modes: [config.reputation.mode],
24557
- properties: {
24558
- "S4.1_earned_reputation": "full",
24559
- "S4.2_participant_owned": "full",
24560
- "S4.5_sybil_resistance": "basic",
24561
- "S4.7_trust_bootstrapping": "full"
24562
- }
24563
- }
24564
- },
24565
- composition: {
24566
- sim_version: "1.0",
24567
- spf_supported: false,
24568
- shr_supported: true,
24569
- delegation_depth: 1
24570
- },
24571
- limitations: [
24572
- "L1 identity uses ed25519 only; KERI support planned for v0.2.0",
24573
- "L2 isolation is process-level only; TEE support planned for a future release",
24574
- "L3 uses commitment schemes only; ZK proofs planned for v0.2.0",
24575
- "L4 Sybil resistance is escrow-based only",
24576
- "Spec license: CC-BY-4.0 | Code license: Apache-2.0"
24577
- ]
24578
- });
24579
- }
24580
- };
24581
- const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
24582
- const { tools: handshakeTools, handshakeResults } = createHandshakeTools(
24583
- config,
24584
- identityManager,
24585
- masterKey,
24586
- auditLog,
24587
- {
24588
- autoPublishHandshakes: config.verascore.auto_publish_handshakes,
24589
- verascoreUrl: config.verascore.url
24590
- }
24591
- );
24592
- const { tools: l4Tools, reputationStore } = createL4Tools(
24593
- storage,
24594
- masterKey,
24595
- identityManager,
24596
- auditLog,
24597
- handshakeResults,
24598
- config.verascore.url
24599
- );
24600
- const { tools: shrTools } = createSHRTools(
24601
- config,
24602
- identityManager,
24603
- masterKey,
24604
- auditLog,
24605
- reputationStore
24606
- );
24607
- const { tools: federationTools } = createFederationTools(
24608
- auditLog,
24609
- handshakeResults
24610
- );
24611
- const { tools: bridgeTools } = createBridgeTools(
24612
- storage,
24613
- masterKey,
24614
- identityManager,
24615
- auditLog,
24616
- handshakeResults
24617
- );
24618
- const { tools: auditTools } = createAuditTools(config);
24619
- const { tools: siemTools } = createSIEMTools(auditLog);
24620
- const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog);
24621
- const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
24622
- const profileStore = new SovereigntyProfileStore(storage, masterKey);
24623
- await profileStore.load();
24624
- const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
24625
- const policy = await loadPrincipalPolicy(config.storage_path);
24626
- const baseline = new BaselineTracker(storage, masterKey);
24627
- await baseline.load();
24628
- let approvalChannel;
24629
- let dashboard;
24630
- if (config.dashboard.enabled) {
24631
- let authToken = config.dashboard.auth_token;
24632
- if (authToken === "auto") {
24633
- const { randomBytes: rb } = await import('crypto');
24634
- authToken = rb(32).toString("hex");
24635
- }
24636
- dashboard = new DashboardApprovalChannel({
24637
- port: config.dashboard.port,
24638
- host: config.dashboard.host,
24639
- timeout_seconds: policy.approval_channel.timeout_seconds,
24640
- // SEC-002: auto_deny removed — timeout always denies
24641
- auth_token: authToken,
24642
- tls: config.dashboard.tls,
24643
- auto_open: config.dashboard.auto_open
24644
- });
24645
- dashboard.setDependencies({
24646
- policy,
24647
- baseline,
24648
- auditLog,
24649
- identityManager,
24650
- handshakeResults,
24651
- shrOpts: { config, identityManager, masterKey },
24652
- sanctuaryConfig: config,
24653
- profileStore
24654
- });
24655
- await dashboard.start();
24656
- approvalChannel = dashboard;
24657
- } else if (config.webhook.enabled && config.webhook.url && config.webhook.secret) {
24658
- const webhook = new WebhookApprovalChannel({
24659
- webhook_url: config.webhook.url,
24660
- webhook_secret: config.webhook.secret,
24661
- callback_port: config.webhook.callback_port,
24662
- callback_host: config.webhook.callback_host,
24663
- timeout_seconds: policy.approval_channel.timeout_seconds
24664
- // SEC-002: auto_deny removed — timeout always denies
24665
- });
24666
- await webhook.start();
24667
- approvalChannel = webhook;
24668
- } else {
24669
- approvalChannel = new StderrApprovalChannel(policy.approval_channel);
24670
- }
24671
- const injectionDetector = new InjectionDetector({
24672
- enabled: true,
24673
- sensitivity: "medium",
24674
- on_detection: "escalate"
24675
- });
24676
- const onInjectionAlert = dashboard ? (alert) => {
24677
- dashboard.broadcastSSE("injection-alert", {
24678
- tool: alert.toolName,
24679
- confidence: alert.result.confidence,
24680
- signals: alert.result.signals.map((s) => ({
24681
- type: s.type,
24682
- location: s.location,
24683
- severity: s.severity
24684
- })),
24685
- recommendation: alert.result.recommendation,
24686
- timestamp: alert.timestamp
24687
- });
24688
- } : void 0;
24689
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
24690
- const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
24691
- const { tools: sanctuaryMetaTools } = createSanctuaryTools({
24692
- config,
24693
- identityManager,
24694
- masterKey,
24695
- auditLog,
24696
- policy,
24697
- keyProtection,
24698
- reputationStore
24699
- });
24700
- const { tools: memoryAttestTools } = createMemoryAttestTools(
24701
- identityManager,
24702
- masterKey,
24703
- auditLog
24704
- );
24705
- const { tools: complianceTools } = createComplianceTools({
24706
- config,
24707
- identityManager,
24708
- masterKey,
24709
- auditLog,
24710
- policy
24711
- });
24712
- const dashboardTools = [];
24713
- if (dashboard) {
24714
- dashboardTools.push({
24715
- name: "dashboard_open",
24716
- description: "Generate a one-click URL to open the Principal Dashboard in a browser. Returns a pre-authenticated link \u2014 no manual token entry needed.",
24717
- inputSchema: {
24718
- type: "object",
24719
- properties: {}
24720
- },
24721
- handler: async () => {
24722
- const url = dashboard.createSessionUrl();
24723
- return {
24724
- content: [
24725
- {
24726
- type: "text",
24727
- text: JSON.stringify({
24728
- dashboard_url: url,
24729
- base_url: dashboard.getBaseUrl(),
24730
- note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
24731
- }, null, 2)
24732
- }
24733
- ]
24734
- };
24735
- }
24736
- });
24283
+ return Array.from(new Set(out));
24284
+ }
24285
+ async function describeTenant(name, storagePath, home) {
24286
+ const exists = await dirExists(storagePath);
24287
+ if (!exists) return null;
24288
+ const { initialized, hasProfile, passphraseStatus } = await isTenantDir(storagePath);
24289
+ if (!initialized && !hasProfile && passphraseStatus === "not-initialized") {
24290
+ return null;
24737
24291
  }
24738
- let allTools = [
24739
- ...l1Tools,
24740
- ...l2Tools,
24741
- ...l3Tools,
24742
- ...l4Tools,
24743
- ...policyTools,
24744
- ...shrTools,
24745
- ...handshakeTools,
24746
- ...federationTools,
24747
- ...bridgeTools,
24748
- ...auditTools,
24749
- ...siemTools,
24750
- ...contextGateTools,
24751
- ...hardeningTools,
24752
- ...profileTools,
24753
- ...dashboardTools,
24754
- ...sanctuaryMetaTools,
24755
- ...memoryAttestTools,
24756
- ...complianceTools,
24757
- manifestTool
24758
- ];
24759
- let clientManager;
24760
- let proxyRouter;
24761
- const governor = new CallGovernor();
24762
- const { tools: governorTools } = createGovernorTools(governor, auditLog);
24763
- allTools.push(...governorTools);
24764
- const profile = profileStore.get();
24765
- if (profile.upstream_servers && profile.upstream_servers.length > 0) {
24766
- const enabledServers = profile.upstream_servers.filter((s) => s.enabled);
24767
- if (enabledServers.length > 0) {
24768
- clientManager = new ClientManager({
24769
- onStateChange: (serverName, state, toolCount, error) => {
24770
- if (dashboard) {
24771
- dashboard.broadcastSSE("proxy-server-status", {
24772
- server: serverName,
24773
- state,
24774
- tool_count: toolCount,
24775
- error,
24776
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24777
- });
24778
- }
24779
- auditLog.append("l2", `proxy_server_${state}`, "system", {
24780
- server: serverName,
24781
- tool_count: toolCount,
24782
- error
24783
- });
24784
- }
24785
- });
24786
- proxyRouter = new ProxyRouter(
24787
- clientManager,
24788
- injectionDetector,
24789
- auditLog,
24790
- {
24791
- contextGateFilter: async (_toolName, args) => {
24792
- const activeProfile = profileStore.get();
24793
- if (activeProfile.features.context_gating.enabled) {
24794
- return args;
24795
- }
24796
- return args;
24797
- },
24798
- governor,
24799
- onProxyCall: (data) => {
24800
- if (dashboard) {
24801
- dashboard.broadcastProxyCall(data);
24802
- }
24803
- }
24804
- }
24805
- );
24806
- clientManager.configure(enabledServers).catch((err) => {
24807
- console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
24808
- });
24809
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
24810
- const proxiedTools = proxyRouter.getProxiedTools();
24811
- if (proxiedTools.length > 0) {
24812
- allTools.push(...proxiedTools);
24813
- }
24814
- if (dashboard) {
24815
- dashboard.setDependencies({
24816
- policy,
24817
- baseline,
24818
- auditLog,
24819
- clientManager
24820
- });
24821
- dashboard.enableFortressView(enabledServers.length);
24822
- }
24823
- }
24292
+ const last_activity = await newestAuditMtime(storagePath);
24293
+ const runtime = await readTenantRuntime(storagePath);
24294
+ return {
24295
+ name,
24296
+ storage_path: storagePath,
24297
+ exists: true,
24298
+ initialized,
24299
+ has_cocoon_profile: hasProfile,
24300
+ keychain_service: keychainServiceFor(storagePath, home),
24301
+ passphrase_status: passphraseStatus,
24302
+ last_activity,
24303
+ runtime
24304
+ };
24305
+ }
24306
+ async function discoverTenants(options = {}) {
24307
+ const home = options.home ?? os.homedir();
24308
+ const env = options.env ?? process.env;
24309
+ const root = options.root ?? path.join(home, DEFAULT_STORAGE_DIR);
24310
+ const tenants = [];
24311
+ const rootTenant = await describeTenant("default", root, home);
24312
+ if (rootTenant) tenants.push(rootTenant);
24313
+ let children = [];
24314
+ try {
24315
+ children = await promises.readdir(root);
24316
+ } catch {
24824
24317
  }
24825
- allTools = allTools.map((tool) => ({
24826
- ...tool,
24827
- handler: contextGateEnforcer.wrapHandler(tool.name, tool.handler)
24828
- }));
24829
- if (proxyRouter) {
24830
- gate.setProxyTierResolver((toolName) => {
24831
- const parsed = ProxyRouter.parseProxyToolName(toolName);
24832
- if (!parsed) return null;
24833
- return proxyRouter.getTierForTool(parsed.serverName, parsed.toolName);
24834
- });
24318
+ for (const child of children) {
24319
+ const childPath = path.join(root, child);
24320
+ if (child.startsWith(".")) continue;
24321
+ if (child === "state" || child === "backup" || child === "config") continue;
24322
+ const s = await promises.stat(childPath).catch(() => null);
24323
+ if (!s || !s.isDirectory()) continue;
24324
+ const desc = await describeTenant(child, childPath, home);
24325
+ if (desc) tenants.push(desc);
24835
24326
  }
24836
- const server = createServer(allTools, { gate });
24837
- await saveConfig(config);
24838
- const cleanup = () => {
24839
- baseline.save().catch(() => {
24840
- });
24841
- if (clientManager) {
24842
- clientManager.shutdown().catch(() => {
24843
- });
24844
- }
24845
- };
24846
- process.on("SIGINT", cleanup);
24847
- process.on("SIGTERM", cleanup);
24848
- if (recoveryKey) {
24849
- console.error(
24850
- `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
24851
- \u2551 SANCTUARY: First Run \u2014 Recovery Key Generated \u2551
24852
- \u2551 \u2551
24853
- \u2551 Recovery Key: ${recoveryKey.slice(0, 20)}... \u2551
24854
- \u2551 \u2551
24855
- \u2551 SAVE THIS KEY. It will not be shown again. \u2551
24856
- \u2551 Without it, your encrypted state is unrecoverable. \u2551
24857
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`
24858
- );
24327
+ const extras = await readExtraPaths(root, env);
24328
+ for (const extra of extras) {
24329
+ if (tenants.some((t) => t.storage_path === extra)) continue;
24330
+ const desc = await describeTenant(path.basename(extra), extra, home);
24331
+ if (desc) tenants.push(desc);
24859
24332
  }
24860
- return {
24861
- server,
24862
- config,
24863
- identityManager,
24864
- masterKey,
24865
- auditLog,
24866
- policy
24867
- };
24333
+ tenants.sort((a, b) => {
24334
+ if (a.name === "default") return -1;
24335
+ if (b.name === "default") return 1;
24336
+ return a.name.localeCompare(b.name);
24337
+ });
24338
+ return tenants;
24868
24339
  }
24869
- var init_src = __esm({
24870
- "src/index.ts"() {
24871
- init_permissions();
24872
- init_config();
24873
- init_filesystem();
24874
- init_state_store();
24875
- init_tools();
24876
- init_audit_log();
24877
- init_tools2();
24878
- init_tools3();
24879
- init_loader();
24880
- init_baseline();
24881
- init_approval_channel();
24882
- init_dashboard();
24883
- init_webhook();
24884
- init_gate();
24885
- init_tools4();
24886
- init_router();
24887
- init_router();
24888
- init_tools5();
24889
- init_tools6();
24890
- init_tools7();
24891
- init_tools8();
24892
- init_tools9();
24893
- init_siem_tools();
24894
- init_context_gate_tools();
24895
- init_hardening_tools();
24896
- init_sovereignty_profile();
24897
- init_sovereignty_profile_tools();
24898
- init_injection_detector();
24899
- init_client_manager();
24900
- init_proxy_router();
24901
- init_call_governor();
24902
- init_governor_tools();
24903
- init_sanctuary_tools();
24904
- init_memory_attest();
24905
- init_generator2();
24906
- init_key_derivation();
24907
- init_random();
24908
- init_encoding();
24909
- init_config();
24910
- init_state_store();
24911
- init_audit_log();
24912
- init_commitments();
24913
- init_zk_proofs();
24914
- init_policies();
24915
- init_reputation_store();
24916
- init_tiers();
24917
- init_registry();
24918
- init_context_gate();
24919
- init_context_gate_templates();
24920
- init_context_gate_recommend();
24921
- init_model_provenance();
24922
- init_context_gate();
24923
- init_injection_detector();
24924
- init_context_gate_enforcer();
24925
- init_sovereignty_profile();
24926
- init_client_manager();
24927
- init_proxy_router();
24928
- init_system_prompt_generator();
24929
- init_memory();
24930
- init_filesystem();
24931
- init_gate();
24932
- init_baseline();
24933
- init_loader();
24934
- init_approval_channel();
24935
- init_dashboard();
24936
- init_webhook();
24937
- init_generator();
24938
- init_verifier();
24939
- init_protocol();
24940
- init_attestation();
24941
- init_bridge();
24942
- init_dashboard2();
24340
+ async function findTenant(name, options = {}) {
24341
+ const tenants = await discoverTenants(options);
24342
+ return tenants.find((t) => t.name === name) ?? null;
24343
+ }
24344
+ var EXTRAS_FILE_NAME;
24345
+ var init_discovery = __esm({
24346
+ "src/cli/agents/discovery.ts"() {
24347
+ init_paths();
24348
+ init_passphrase();
24349
+ init_runtime();
24350
+ EXTRAS_FILE_NAME = "agents-extra.json";
24943
24351
  }
24944
24352
  });
24945
- function resolveStoragePath(env = process.env, home = os.homedir()) {
24946
- const override = env.SANCTUARY_STORAGE_PATH;
24947
- if (override && override.length > 0) return override;
24948
- return path.join(home, DEFAULT_STORAGE_DIR);
24949
- }
24950
- function resolveDashboardPort(explicitPort, env = process.env) {
24951
- if (typeof explicitPort === "number" && !Number.isNaN(explicitPort)) {
24952
- return explicitPort;
24353
+ function constantTimeEquals(a, b) {
24354
+ if (a.length !== b.length) return false;
24355
+ let diff = 0;
24356
+ for (let i = 0; i < a.length; i++) {
24357
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
24953
24358
  }
24954
- const envPort = env.SANCTUARY_DASHBOARD_PORT;
24955
- if (envPort) {
24956
- const parsed = parseInt(envPort, 10);
24957
- if (!Number.isNaN(parsed)) return parsed;
24359
+ return diff === 0;
24360
+ }
24361
+ function extractToken(req, url) {
24362
+ const header = req.headers.authorization;
24363
+ if (header && header.startsWith("Bearer ")) {
24364
+ return header.slice(7).trim();
24958
24365
  }
24959
- return DEFAULT_DASHBOARD_PORT;
24366
+ const q = url.searchParams.get("token");
24367
+ return q ?? null;
24960
24368
  }
24961
- var DEFAULT_STORAGE_DIR, DEFAULT_DASHBOARD_PORT;
24962
- var init_paths = __esm({
24963
- "src/paths.ts"() {
24964
- DEFAULT_STORAGE_DIR = ".sanctuary";
24965
- DEFAULT_DASHBOARD_PORT = 3501;
24369
+ function isAuthorized(deps, req, url) {
24370
+ if (!deps.authToken) return true;
24371
+ const token = extractToken(req, url);
24372
+ if (!token) return false;
24373
+ return constantTimeEquals(token, deps.authToken);
24374
+ }
24375
+ function writeJSON(res, status, payload) {
24376
+ res.writeHead(status, {
24377
+ "Content-Type": "application/json",
24378
+ "Cache-Control": "no-store"
24379
+ });
24380
+ res.end(JSON.stringify(payload));
24381
+ }
24382
+ async function readJSONBody(req) {
24383
+ const chunks = [];
24384
+ let size = 0;
24385
+ const MAX = 256 * 1024;
24386
+ for await (const chunk of req) {
24387
+ size += chunk.length;
24388
+ if (size > MAX) throw new Error("request body too large");
24389
+ chunks.push(chunk);
24966
24390
  }
24967
- });
24968
- function getPlatformPaths() {
24969
- const home = os.homedir();
24970
- return {
24971
- "openclaw": [
24972
- path.join(home, ".openclaw", "openclaw.json"),
24973
- path.join(home, ".openclaw", "config.json"),
24974
- path.join(home, "Library", "Application Support", "OpenClaw", "openclaw.json"),
24975
- path.join(home, "Library", "Application Support", "OpenClaw", "config.json")
24976
- ],
24977
- // Hermes Agent (NousResearch, v0.9.0) canonicals live under ~/.hermes.
24978
- // Hermes ships `cli-config.yaml` as the primary surface per upstream docs.
24979
- // Sanctuary wrap v1.0 detects the JSON variant only: operators who keep
24980
- // YAML can still wrap via `sanctuary wrap --wrap <path>` after exporting
24981
- // to JSON. YAML-native detection is flagged as a v1.x follow-up.
24982
- "hermes": [
24983
- path.join(home, ".hermes", "cli-config.json"),
24984
- path.join(home, ".hermes", "config.json"),
24985
- path.join(home, ".config", "hermes", "cli-config.json")
24986
- ],
24987
- // Claude Code's modern canonical surface is ~/.claude.json (`claude mcp
24988
- // add` writes here). The legacy ~/.claude/settings.json shape predates
24989
- // it and is still respected if present. Probe order = preference order:
24990
- // wrap operates on the first one that exists, and bootstraps a fresh
24991
- // ~/.claude.json when neither is present (per the cli.ts bootstrap).
24992
- "claude-code": [
24993
- path.join(home, ".claude.json"),
24994
- path.join(home, ".claude", "settings.json"),
24995
- path.join(home, ".config", "claude-code", "settings.json")
24996
- ],
24997
- "cursor": [
24998
- path.join(home, ".cursor", "mcp.json")
24999
- ],
25000
- // Cline is a VS Code extension (saoudrizwan.claude-dev). Its MCP settings
25001
- // live under the VS Code globalStorage tree, which is OS-specific. We
25002
- // enumerate the three supported OS layouts; at detection time only the
25003
- // one matching the running OS will exist.
25004
- "cline": [
25005
- // macOS
25006
- path.join(
25007
- home,
25008
- "Library",
25009
- "Application Support",
25010
- "Code",
25011
- "User",
25012
- "globalStorage",
25013
- "saoudrizwan.claude-dev",
25014
- "settings",
25015
- "cline_mcp_settings.json"
25016
- ),
25017
- // Linux
25018
- path.join(
25019
- home,
25020
- ".config",
25021
- "Code",
25022
- "User",
25023
- "globalStorage",
25024
- "saoudrizwan.claude-dev",
25025
- "settings",
25026
- "cline_mcp_settings.json"
25027
- ),
25028
- // Windows (honour APPDATA when set, otherwise reconstruct under home)
25029
- process.env.APPDATA ? path.join(
25030
- process.env.APPDATA,
25031
- "Code",
25032
- "User",
25033
- "globalStorage",
25034
- "saoudrizwan.claude-dev",
25035
- "settings",
25036
- "cline_mcp_settings.json"
25037
- ) : path.join(
25038
- home,
25039
- "AppData",
25040
- "Roaming",
25041
- "Code",
25042
- "User",
25043
- "globalStorage",
25044
- "saoudrizwan.claude-dev",
25045
- "settings",
25046
- "cline_mcp_settings.json"
25047
- )
25048
- ],
25049
- "generic": []
25050
- };
24391
+ const body = Buffer.concat(chunks).toString("utf-8");
24392
+ if (!body) return {};
24393
+ return JSON.parse(body);
25051
24394
  }
25052
- function backupDir() {
25053
- return path.join(resolveStoragePath(), "backup");
24395
+ function generateEphemeralKey() {
24396
+ return new Uint8Array(crypto.randomBytes(32));
25054
24397
  }
25055
- async function backupConfig(configPath) {
25056
- const dir = backupDir();
25057
- await promises.mkdir(dir, { recursive: true, mode: 448 });
25058
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
25059
- const backupPath = path.join(dir, `config-backup-${timestamp}.json`);
25060
- await promises.copyFile(configPath, backupPath);
25061
- return backupPath;
24398
+ function writeText(res, status, body, contentType = "text/plain") {
24399
+ res.writeHead(status, {
24400
+ "Content-Type": contentType,
24401
+ "Cache-Control": "no-store"
24402
+ });
24403
+ res.end(body);
24404
+ }
24405
+ async function handleRequest(deps, req, res) {
24406
+ const host = req.headers.host || "localhost";
24407
+ const url = new URL(req.url ?? "/", `http://${host}`);
24408
+ const method = (req.method ?? "GET").toUpperCase();
24409
+ const path = url.pathname;
24410
+ if (!isAuthorized(deps, req, url)) {
24411
+ writeJSON(res, 401, { error: "unauthorized" });
24412
+ return true;
24413
+ }
24414
+ if (method === "GET" && path === "/api/health") {
24415
+ writeJSON(res, 200, { ok: true, mode: deps.sources.mode });
24416
+ return true;
24417
+ }
24418
+ if (method === "GET" && (path === "/" || path === "/index.html")) {
24419
+ const snapshot = await getProtectionSnapshot(deps.sources);
24420
+ const html = renderDashboardHTML({ snapshot, authToken: deps.authToken });
24421
+ writeText(res, 200, html, "text/html; charset=utf-8");
24422
+ return true;
24423
+ }
24424
+ if (method === "GET" && path === "/api/snapshot") {
24425
+ const snapshot = await getProtectionSnapshot(deps.sources);
24426
+ writeJSON(res, 200, snapshot);
24427
+ return true;
24428
+ }
24429
+ const approvalMatch = /^\/api\/approvals\/([^/]+)\/(allow|deny)$/.exec(path);
24430
+ if (method === "POST" && approvalMatch) {
24431
+ const id = decodeURIComponent(approvalMatch[1]);
24432
+ const action = approvalMatch[2];
24433
+ if (!deps.approvals) {
24434
+ writeJSON(res, 503, { error: "approvals_unavailable" });
24435
+ return true;
24436
+ }
24437
+ const handler = action === "allow" ? deps.approvals.allow : deps.approvals.deny;
24438
+ try {
24439
+ const ok2 = await handler(id);
24440
+ writeJSON(res, ok2 ? 200 : 404, { id, action, ok: ok2 });
24441
+ } catch (err) {
24442
+ writeJSON(res, 500, { error: "approval_failed", message: err.message });
24443
+ }
24444
+ return true;
24445
+ }
24446
+ if (method === "GET" && path === "/api/stream") {
24447
+ await handleStream(deps, res);
24448
+ return true;
24449
+ }
24450
+ if (method === "GET" && path === "/api/templates") {
24451
+ try {
24452
+ const templates = listTemplates();
24453
+ writeJSON(res, 200, { templates });
24454
+ } catch (err) {
24455
+ writeJSON(res, 500, {
24456
+ error: "template_load_failed",
24457
+ message: err.message
24458
+ });
24459
+ }
24460
+ return true;
24461
+ }
24462
+ const templateMatch = /^\/api\/templates\/([^/]+)$/.exec(path);
24463
+ if (method === "GET" && templateMatch) {
24464
+ const name = decodeURIComponent(templateMatch[1]);
24465
+ try {
24466
+ const entry = getTemplateEntry(name);
24467
+ if (!entry) {
24468
+ writeJSON(res, 404, { error: "template_not_found", name });
24469
+ return true;
24470
+ }
24471
+ writeJSON(res, 200, entry);
24472
+ } catch (err) {
24473
+ writeJSON(res, 500, {
24474
+ error: "template_load_failed",
24475
+ message: err.message
24476
+ });
24477
+ }
24478
+ return true;
24479
+ }
24480
+ const initMatch = /^\/api\/templates\/([^/]+)\/init$/.exec(path);
24481
+ if (method === "POST" && initMatch) {
24482
+ const name = decodeURIComponent(initMatch[1]);
24483
+ try {
24484
+ const bundle = getTemplate2(name);
24485
+ if (!bundle) {
24486
+ writeJSON(res, 404, { error: "template_not_found", name });
24487
+ return true;
24488
+ }
24489
+ const body = await readJSONBody(req);
24490
+ if (!body.agent_name || typeof body.agent_name !== "string") {
24491
+ writeJSON(res, 400, {
24492
+ error: "validation_error",
24493
+ message: "agent_name is required and must be a string"
24494
+ });
24495
+ return true;
24496
+ }
24497
+ if (!/^[a-zA-Z0-9_-]+$/.test(body.agent_name)) {
24498
+ writeJSON(res, 400, {
24499
+ error: "validation_error",
24500
+ message: "agent_name must contain only alphanumeric characters, hyphens, and underscores"
24501
+ });
24502
+ return true;
24503
+ }
24504
+ const isAgentWrapped = deps.isAgentWrapped ?? (async (agentId) => {
24505
+ const tenant = await findTenant(agentId);
24506
+ if (!tenant) return false;
24507
+ return tenant.initialized || tenant.has_cocoon_profile;
24508
+ });
24509
+ if (!await isAgentWrapped(body.agent_name)) {
24510
+ writeJSON(res, 400, {
24511
+ error: "orphan_agent_id",
24512
+ message: `No wrapped harness found for agent_id "${body.agent_name}". Run \`sanctuary wrap\` to wrap the harness first, then retry template init.`
24513
+ });
24514
+ return true;
24515
+ }
24516
+ const nodeId = deps.nodeId ?? "dashboard-node";
24517
+ const nodePrivateKey = deps.nodePrivateKey ?? generateEphemeralKey();
24518
+ const principalId = deps.principalId ?? "dashboard-principal";
24519
+ const fortressId = deps.fortressId ?? "default";
24520
+ const result = initTemplate({
24521
+ template_name: name,
24522
+ agent_id: body.agent_name,
24523
+ fortress_id: fortressId,
24524
+ counterparty: "*",
24525
+ policy_version: 1,
24526
+ emitter_node: nodeId,
24527
+ emitter_principal: principalId,
24528
+ monotonic_seq: 1,
24529
+ node_private_key: nodePrivateKey
24530
+ });
24531
+ writeJSON(res, 200, {
24532
+ agent_id: body.agent_name,
24533
+ signed_event_id: result.signed_event.event_id,
24534
+ policy_version: result.compiled.policy_version,
24535
+ template_name: name,
24536
+ attestation_panel_url: `/console#agent_roster`
24537
+ });
24538
+ } catch (err) {
24539
+ writeJSON(res, 500, {
24540
+ error: "template_init_failed",
24541
+ message: err.message
24542
+ });
24543
+ }
24544
+ return true;
24545
+ }
24546
+ return false;
25062
24547
  }
25063
- async function restoreConfig(backupPath, targetPath) {
25064
- await promises.copyFile(backupPath, targetPath);
24548
+ async function handleStream(deps, res) {
24549
+ res.writeHead(200, {
24550
+ "Content-Type": "text/event-stream",
24551
+ "Cache-Control": "no-cache, no-transform",
24552
+ Connection: "keep-alive",
24553
+ "X-Accel-Buffering": "no"
24554
+ });
24555
+ const snapshot = await getProtectionSnapshot(deps.sources);
24556
+ res.write(`event: snapshot
24557
+ data: ${JSON.stringify(snapshot)}
24558
+
24559
+ `);
24560
+ const unsubscribe = deps.onEvent ? deps.onEvent((event) => {
24561
+ try {
24562
+ res.write(`event: ${event.type}
24563
+ data: ${JSON.stringify(event.data)}
24564
+
24565
+ `);
24566
+ } catch {
24567
+ }
24568
+ }) : () => {
24569
+ };
24570
+ const keepAlive = setInterval(() => {
24571
+ try {
24572
+ res.write(": keepalive\n\n");
24573
+ } catch {
24574
+ }
24575
+ }, 25e3);
24576
+ const cleanup = () => {
24577
+ clearInterval(keepAlive);
24578
+ unsubscribe();
24579
+ };
24580
+ res.on("close", cleanup);
24581
+ res.on("error", cleanup);
25065
24582
  }
25066
- async function findLatestBackup() {
25067
- const metaPath = path.join(backupDir(), "cocoon-meta.json");
25068
- try {
25069
- const raw = await promises.readFile(metaPath, "utf-8");
25070
- const meta = JSON.parse(raw);
25071
- return {
25072
- backupPath: meta.backupPath,
25073
- originalPath: meta.originalPath
25074
- };
25075
- } catch {
25076
- return null;
24583
+ var init_api = __esm({
24584
+ "src/dashboard/api.ts"() {
24585
+ init_aggregator();
24586
+ init_html();
24587
+ init_registry2();
24588
+ init_init();
24589
+ init_discovery();
25077
24590
  }
24591
+ });
24592
+ async function startDashboardServer(options) {
24593
+ const port = options.port ?? DEFAULT_PORT;
24594
+ const host = options.host ?? DEFAULT_HOST;
24595
+ const listeners = /* @__PURE__ */ new Set();
24596
+ const onEvent = (listener) => {
24597
+ listeners.add(listener);
24598
+ return () => listeners.delete(listener);
24599
+ };
24600
+ const publish = (event) => {
24601
+ for (const listener of listeners) {
24602
+ try {
24603
+ listener(event);
24604
+ } catch {
24605
+ }
24606
+ }
24607
+ };
24608
+ const deps = {
24609
+ sources: options.sources,
24610
+ authToken: options.authToken,
24611
+ approvals: options.approvals,
24612
+ onEvent
24613
+ };
24614
+ const server = http.createServer(async (req, res) => {
24615
+ try {
24616
+ const served = await handleRequest(deps, req, res);
24617
+ if (!served) {
24618
+ res.writeHead(404, { "Content-Type": "application/json" });
24619
+ res.end(JSON.stringify({ error: "not_found", path: req.url }));
24620
+ }
24621
+ } catch (err) {
24622
+ try {
24623
+ res.writeHead(500, { "Content-Type": "application/json" });
24624
+ res.end(JSON.stringify({ error: "internal", message: err.message }));
24625
+ } catch {
24626
+ }
24627
+ }
24628
+ });
24629
+ await new Promise((resolve2, reject) => {
24630
+ server.once("error", reject);
24631
+ server.listen(port, host, () => {
24632
+ server.off("error", reject);
24633
+ resolve2();
24634
+ });
24635
+ });
24636
+ const actualPort = (() => {
24637
+ const addr = server.address();
24638
+ if (addr && typeof addr === "object") return addr.port;
24639
+ return port;
24640
+ })();
24641
+ const url = `http://${host}:${actualPort}`;
24642
+ return {
24643
+ url,
24644
+ port: actualPort,
24645
+ host,
24646
+ stop: () => new Promise((resolve2, reject) => {
24647
+ server.close((err) => err ? reject(err) : resolve2());
24648
+ }),
24649
+ publish,
24650
+ publishActivity: (entry) => publish({ type: "activity", data: entry }),
24651
+ publishApproval: (approval) => publish({ type: "approval", data: approval })
24652
+ };
25078
24653
  }
25079
- async function saveCocoonMeta(meta) {
25080
- const dir = backupDir();
25081
- await promises.mkdir(dir, { recursive: true, mode: 448 });
25082
- const metaPath = path.join(dir, "cocoon-meta.json");
25083
- await promises.writeFile(metaPath, JSON.stringify(meta, null, 2), { mode: 384 });
24654
+ var DEFAULT_PORT, DEFAULT_HOST;
24655
+ var init_server = __esm({
24656
+ "src/dashboard/server.ts"() {
24657
+ init_api();
24658
+ DEFAULT_PORT = 3501;
24659
+ DEFAULT_HOST = "127.0.0.1";
24660
+ }
24661
+ });
24662
+
24663
+ // src/dashboard/index.ts
24664
+ async function startDashboard(options) {
24665
+ const activity = options.initialActivity ? [...options.initialActivity] : [];
24666
+ const pending = options.initialPendingApprovals ? [...options.initialPendingApprovals] : [];
24667
+ const sources = {
24668
+ mode: options.mode,
24669
+ server_version: options.serverVersion,
24670
+ ...options.auditLog ? { auditLog: options.auditLog } : {},
24671
+ ...options.identityManager ? { identityManager: options.identityManager } : {},
24672
+ ...options.clientManager ? { clientManager: options.clientManager } : {},
24673
+ ...options.baseline ? { baseline: options.baseline } : {},
24674
+ ...options.policy ? { policy: options.policy } : {},
24675
+ ...options.reputation ? { reputation: options.reputation } : {},
24676
+ ...options.teeAvailable != null ? { teeAvailable: options.teeAvailable } : {},
24677
+ ...options.l4Evidence ? { l4Evidence: options.l4Evidence } : {},
24678
+ activity,
24679
+ pendingApprovals: pending
24680
+ };
24681
+ const serverOpts = {
24682
+ mode: options.mode,
24683
+ sources,
24684
+ ...options.port != null ? { port: options.port } : {},
24685
+ ...options.host ? { host: options.host } : {},
24686
+ ...options.authToken ? { authToken: options.authToken } : {},
24687
+ ...options.approvals ? { approvals: options.approvals } : {}
24688
+ };
24689
+ const handle = await startDashboardServer(serverOpts);
24690
+ const wrapped = {
24691
+ ...handle,
24692
+ publishActivity: (entry) => {
24693
+ activity.unshift(entry);
24694
+ if (activity.length > 50) activity.length = 50;
24695
+ handle.publishActivity(entry);
24696
+ },
24697
+ publishApproval: (approval) => {
24698
+ pending.push(approval);
24699
+ handle.publishApproval(approval);
24700
+ }
24701
+ };
24702
+ return wrapped;
25084
24703
  }
25085
- async function detectAgentConfigWithDiagnostics(platform4, configPath) {
25086
- const pathsChecked = [];
25087
- const errors = [];
25088
- if (configPath) {
25089
- pathsChecked.push(configPath);
25090
- const { config, error } = await readConfigFileWithError(configPath, platform4 ?? "generic");
25091
- if (error) errors.push({ path: configPath, error });
25092
- return { config, pathsChecked, errors };
24704
+ var init_dashboard2 = __esm({
24705
+ "src/dashboard/index.ts"() {
24706
+ init_server();
24707
+ init_aggregator();
24708
+ init_html();
24709
+ init_api();
24710
+ init_server();
25093
24711
  }
25094
- if (platform4) {
25095
- const paths = getPlatformPaths()[platform4];
25096
- for (const path of paths) {
25097
- pathsChecked.push(path);
25098
- const { config, error } = await readConfigFileWithError(path, platform4);
25099
- if (error) errors.push({ path, error });
25100
- if (config) return { config, pathsChecked, errors };
24712
+ });
24713
+ async function createSanctuaryServer(options) {
24714
+ const config = await loadConfig(options?.configPath);
24715
+ await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
24716
+ await tightenStoragePermissions(config.storage_path);
24717
+ const storage = options?.storage ?? new FilesystemStorage(
24718
+ `${config.storage_path}/state`
24719
+ );
24720
+ let masterKey;
24721
+ let keyProtection;
24722
+ let recoveryKey;
24723
+ const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
24724
+ if (passphrase) {
24725
+ keyProtection = "passphrase";
24726
+ let existingParams;
24727
+ try {
24728
+ const raw = await storage.read("_meta", "key-params");
24729
+ if (raw) {
24730
+ const { bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24731
+ existingParams = JSON.parse(bytesToString2(raw));
24732
+ }
24733
+ } catch {
24734
+ }
24735
+ const result = await deriveMasterKey(passphrase, existingParams);
24736
+ masterKey = result.key;
24737
+ if (!existingParams) {
24738
+ const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24739
+ await storage.write(
24740
+ "_meta",
24741
+ "key-params",
24742
+ stringToBytes2(JSON.stringify(result.params))
24743
+ );
25101
24744
  }
25102
- return { config: null, pathsChecked, errors };
25103
- }
25104
- for (const [plat, paths] of Object.entries(getPlatformPaths())) {
25105
- for (const path of paths) {
25106
- pathsChecked.push(path);
25107
- const { config, error } = await readConfigFileWithError(path, plat);
25108
- if (error) errors.push({ path, error });
25109
- if (config) return { config, pathsChecked, errors };
24745
+ } else {
24746
+ keyProtection = "recovery-key";
24747
+ const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
24748
+ const { stringToBytes: stringToBytes2, bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24749
+ const { fromBase64url: fromBase64url2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24750
+ const { constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
24751
+ const existingHash = await storage.read("_meta", "recovery-key-hash");
24752
+ if (existingHash) {
24753
+ const envRecoveryKey = process.env.SANCTUARY_RECOVERY_KEY;
24754
+ if (!envRecoveryKey) {
24755
+ throw new Error(
24756
+ "Sanctuary: Existing encrypted data found but no credentials provided.\nThis installation was previously set up with a recovery key.\n\nTo start the server, provide one of:\n - SANCTUARY_PASSPHRASE (if you later configured a passphrase)\n - SANCTUARY_RECOVERY_KEY (the recovery key shown at first run)\n\nWithout the correct credentials, encrypted state cannot be accessed.\nRefusing to start to prevent silent data loss."
24757
+ );
24758
+ }
24759
+ let recoveryKeyBytes;
24760
+ try {
24761
+ recoveryKeyBytes = fromBase64url2(envRecoveryKey);
24762
+ } catch {
24763
+ throw new Error(
24764
+ "Sanctuary: SANCTUARY_RECOVERY_KEY is not valid base64url. The recovery key should be the exact string shown at first run."
24765
+ );
24766
+ }
24767
+ if (recoveryKeyBytes.length !== 32) {
24768
+ throw new Error(
24769
+ "Sanctuary: SANCTUARY_RECOVERY_KEY has incorrect length. The recovery key should be the exact string shown at first run."
24770
+ );
24771
+ }
24772
+ const providedHash = hashToString2(recoveryKeyBytes);
24773
+ const storedHash = bytesToString2(existingHash);
24774
+ const providedHashBytes = stringToBytes2(providedHash);
24775
+ const storedHashBytes = stringToBytes2(storedHash);
24776
+ if (!constantTimeEqual2(providedHashBytes, storedHashBytes)) {
24777
+ throw new Error(
24778
+ "Sanctuary: Recovery key does not match the stored key hash.\nThe recovery key provided via SANCTUARY_RECOVERY_KEY is incorrect.\nUse the exact recovery key that was displayed at first run."
24779
+ );
24780
+ }
24781
+ masterKey = recoveryKeyBytes;
24782
+ } else {
24783
+ const existingNamespaces = await storage.list("_meta");
24784
+ const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
24785
+ if (hasKeyParams) {
24786
+ throw new Error(
24787
+ "Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
24788
+ );
24789
+ }
24790
+ masterKey = generateRandomKey();
24791
+ recoveryKey = toBase64url(masterKey);
24792
+ const keyHash = hashToString2(masterKey);
24793
+ await storage.write(
24794
+ "_meta",
24795
+ "recovery-key-hash",
24796
+ stringToBytes2(keyHash)
24797
+ );
25110
24798
  }
25111
24799
  }
25112
- return { config: null, pathsChecked, errors };
25113
- }
25114
- async function readConfigFileWithError(path, platform4) {
25115
- try {
25116
- await promises.access(path);
25117
- } catch {
25118
- return { config: null };
25119
- }
25120
- let raw;
25121
- try {
25122
- raw = await promises.readFile(path, "utf-8");
25123
- } catch (err) {
25124
- return { config: null, error: `Cannot read file: ${err.message}` };
25125
- }
25126
- let config;
25127
- try {
25128
- config = JSON.parse(raw);
25129
- } catch (err) {
25130
- return { config: null, error: `Invalid JSON: ${err.message}` };
24800
+ const auditLog = new AuditLog(storage, masterKey);
24801
+ const stateStore = new StateStore(storage, masterKey);
24802
+ const { tools: l1Tools, identityManager } = createL1Tools(
24803
+ stateStore,
24804
+ storage,
24805
+ masterKey,
24806
+ keyProtection,
24807
+ auditLog
24808
+ );
24809
+ const loadResult = await identityManager.load();
24810
+ if (loadResult.total > 0 && loadResult.loaded === 0) {
24811
+ console.error(
24812
+ `
24813
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
24814
+ \u2551 \u26A0 WARNING: Encrypted identities found but NONE loaded \u2551
24815
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
24816
+ \u2551 ${loadResult.total} encrypted identity file(s) found on disk \u2551
24817
+ \u2551 0 could be decrypted with the current master key \u2551
24818
+ \u2551 \u2551
24819
+ \u2551 This usually means SANCTUARY_PASSPHRASE is missing or \u2551
24820
+ \u2551 incorrect. The server will start but with NO identity data. \u2551
24821
+ \u2551 \u2551
24822
+ \u2551 To fix: set SANCTUARY_PASSPHRASE to the passphrase used \u2551
24823
+ \u2551 when this Sanctuary instance was first configured. \u2551
24824
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
24825
+ `
24826
+ );
24827
+ } else if (loadResult.failed > 0) {
24828
+ console.error(
24829
+ `Warning: ${loadResult.failed} of ${loadResult.total} identity files could not be decrypted (possibly corrupted).`
24830
+ );
25131
24831
  }
25132
- const servers = extractServers(config, platform4);
25133
- return { config: { platform: platform4, configPath: path, servers, rawConfig: config } };
25134
- }
25135
- function extractServers(config, platform4) {
25136
- if (!config || typeof config !== "object") return [];
25137
- const servers = [];
25138
- const obj = config;
25139
- if (platform4 === "openclaw" || platform4 === "generic") {
25140
- const mcp = obj.mcp;
25141
- const nestedServers = mcp?.servers;
25142
- if (nestedServers && typeof nestedServers === "object") {
25143
- for (const [name, serverConfig] of Object.entries(nestedServers)) {
25144
- const entry = parseServerEntry(name, serverConfig);
25145
- if (entry) servers.push(entry);
24832
+ const l2Tools = [
24833
+ {
24834
+ name: "exec_attest",
24835
+ description: "Generate an attestation of the current execution environment, including sovereignty assessment and degradation report.",
24836
+ inputSchema: {
24837
+ type: "object",
24838
+ properties: {
24839
+ include_hardware: { type: "boolean", default: true },
24840
+ include_software: { type: "boolean", default: true },
24841
+ include_network: { type: "boolean", default: true }
24842
+ }
24843
+ },
24844
+ handler: async () => {
24845
+ const degradations = [];
24846
+ degradations.push(
24847
+ "L2 isolation is process-level only; no TEE available"
24848
+ );
24849
+ return toolResult({
24850
+ attestation: {
24851
+ environment_type: config.execution.environment,
24852
+ hardware: {
24853
+ cpu_vendor: process.arch,
24854
+ tee_available: false,
24855
+ tee_type: void 0
24856
+ },
24857
+ software: {
24858
+ os: `${process.platform}-${process.arch}`,
24859
+ runtime: `node-${process.version}`,
24860
+ sanctuary_version: config.version,
24861
+ mcp_sdk_version: "1.26.0"
24862
+ },
24863
+ network: {
24864
+ internet_accessible: true,
24865
+ // Conservative assumption
24866
+ listening_ports: [],
24867
+ egress_restricted: false
24868
+ },
24869
+ isolation_level: "process",
24870
+ sovereignty_assessment: {
24871
+ l1_state_encrypted: true,
24872
+ l2_execution_isolated: false,
24873
+ l2_isolation_type: "process-level",
24874
+ l3_proofs_available: true,
24875
+ l4_reputation_active: true,
24876
+ overall_level: "mvs",
24877
+ degradations
24878
+ }
24879
+ },
24880
+ attested_at: (/* @__PURE__ */ new Date()).toISOString()
24881
+ });
25146
24882
  }
25147
- }
25148
- if (servers.length === 0) {
25149
- const mcpServers = obj.mcpServers;
25150
- if (mcpServers && typeof mcpServers === "object") {
25151
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
25152
- const entry = parseServerEntry(name, serverConfig);
25153
- if (entry) servers.push(entry);
24883
+ },
24884
+ {
24885
+ name: "monitor_health",
24886
+ description: "Sanctuary Health Report (SHR) \u2014 standardized sovereignty status.",
24887
+ inputSchema: { type: "object", properties: {} },
24888
+ handler: async () => {
24889
+ const storageSizeBytes = await storage.totalSize();
24890
+ const degradations = [];
24891
+ degradations.push({
24892
+ layer: "l2",
24893
+ description: "Process-level isolation only (no TEE)",
24894
+ severity: "warning",
24895
+ mitigation: "TEE support planned for a future release"
24896
+ });
24897
+ return toolResult({
24898
+ status: degradations.some((d) => d.severity === "critical") ? "compromised" : degradations.some((d) => d.severity === "warning") ? "degraded" : "healthy",
24899
+ storage_bytes: storageSizeBytes,
24900
+ layers: {
24901
+ l1: {
24902
+ status: "active",
24903
+ encryption_algorithm: "aes-256-gcm",
24904
+ key_count: identityManager.list().length,
24905
+ state_integrity: "verified",
24906
+ last_integrity_check: (/* @__PURE__ */ new Date()).toISOString()
24907
+ },
24908
+ l2: {
24909
+ status: "degraded",
24910
+ isolation_type: "process-level",
24911
+ attestation_available: true,
24912
+ last_attestation: (/* @__PURE__ */ new Date()).toISOString()
24913
+ },
24914
+ l3: {
24915
+ status: "active",
24916
+ proof_system: config.disclosure.proof_system,
24917
+ circuits_loaded: 0,
24918
+ proofs_generated_total: 0
24919
+ },
24920
+ l4: {
24921
+ status: "active",
24922
+ mode: config.reputation.mode,
24923
+ interaction_count: 0,
24924
+ // TODO: track from reputation store
24925
+ reputation_exportable: true
24926
+ }
24927
+ },
24928
+ degradations,
24929
+ checked_at: (/* @__PURE__ */ new Date()).toISOString()
24930
+ });
24931
+ }
24932
+ },
24933
+ {
24934
+ name: "monitor_audit_log",
24935
+ description: "Query the sovereignty audit log.",
24936
+ inputSchema: {
24937
+ type: "object",
24938
+ properties: {
24939
+ since: { type: "string", description: "ISO 8601 timestamp" },
24940
+ layer: {
24941
+ type: "string",
24942
+ enum: ["l1", "l2", "l3", "l4"]
24943
+ },
24944
+ operation_type: { type: "string" },
24945
+ limit: { type: "number", default: 50 }
25154
24946
  }
24947
+ },
24948
+ handler: async (args) => {
24949
+ const result = await auditLog.query({
24950
+ since: args.since,
24951
+ layer: args.layer,
24952
+ operation_type: args.operation_type,
24953
+ limit: args.limit ?? 50
24954
+ });
24955
+ return toolResult(result);
25155
24956
  }
25156
24957
  }
25157
- }
25158
- if (platform4 === "claude-code") {
25159
- const mcpServers = obj.mcpServers;
25160
- if (mcpServers && typeof mcpServers === "object") {
25161
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
25162
- if (isCanonicalSanctuaryName(name)) continue;
25163
- const entry = parseServerEntry(name, serverConfig);
25164
- if (entry) servers.push(entry);
25165
- }
24958
+ ];
24959
+ const manifestTool = {
24960
+ name: "manifest",
24961
+ description: "Generate the Sanctuary Interface Manifest (SIM) \u2014 a machine-readable declaration of this server's capabilities.",
24962
+ inputSchema: { type: "object", properties: {} },
24963
+ handler: async () => {
24964
+ return toolResult({
24965
+ sanctuary_version: "0.2",
24966
+ implementation: {
24967
+ name: "@sanctuary-framework/mcp-server",
24968
+ version: config.version,
24969
+ language: "typescript",
24970
+ license: "Apache-2.0"
24971
+ },
24972
+ layers: {
24973
+ l1: {
24974
+ implemented: true,
24975
+ interfaces: ["StateStore", "IdentityRoot"],
24976
+ encryption: ["aes-256-gcm"],
24977
+ identity: ["ed25519"],
24978
+ properties: {
24979
+ "S1.1_participant_held_keys": "full",
24980
+ "S1.2_encryption_at_rest": "full",
24981
+ "S1.3_integrity_verification": "full",
24982
+ "S1.4_selective_state_sharing": "full",
24983
+ "S1.5_state_portability": "full",
24984
+ "S1.6_deletion_rights": "full",
24985
+ "S1.7_identity_anchoring": "partial"
24986
+ }
24987
+ },
24988
+ l2: {
24989
+ implemented: true,
24990
+ interfaces: ["ExecutionEnvironment", "RuntimeMonitor"],
24991
+ isolation_types: [config.execution.environment],
24992
+ properties: {
24993
+ "S2.1_execution_confidentiality": "documented",
24994
+ "S2.2_verifiable_execution": "self-reported",
24995
+ "S2.5_attestation": "self-reported"
24996
+ }
24997
+ },
24998
+ l3: {
24999
+ implemented: true,
25000
+ interfaces: ["ProofEngine", "DisclosurePolicy"],
25001
+ proof_systems: [config.disclosure.proof_system],
25002
+ properties: {
25003
+ "S3.1_minimum_disclosure": "policy-based",
25004
+ "S3.3_proof_without_revelation": "commitment"
25005
+ }
25006
+ },
25007
+ l4: {
25008
+ implemented: true,
25009
+ interfaces: ["ReputationStore", "TrustBootstrap"],
25010
+ modes: [config.reputation.mode],
25011
+ properties: {
25012
+ "S4.1_earned_reputation": "full",
25013
+ "S4.2_participant_owned": "full",
25014
+ "S4.5_sybil_resistance": "basic",
25015
+ "S4.7_trust_bootstrapping": "full"
25016
+ }
25017
+ }
25018
+ },
25019
+ composition: {
25020
+ sim_version: "1.0",
25021
+ spf_supported: false,
25022
+ shr_supported: true,
25023
+ delegation_depth: 1
25024
+ },
25025
+ limitations: [
25026
+ "L1 identity uses ed25519 only; KERI support planned for v0.2.0",
25027
+ "L2 isolation is process-level only; TEE support planned for a future release",
25028
+ "L3 uses commitment schemes only; ZK proofs planned for v0.2.0",
25029
+ "L4 Sybil resistance is escrow-based only",
25030
+ "Spec license: CC-BY-4.0 | Code license: Apache-2.0"
25031
+ ]
25032
+ });
25166
25033
  }
25167
- }
25168
- if (platform4 === "cursor") {
25169
- const mcpServers = obj.mcpServers;
25170
- if (mcpServers && typeof mcpServers === "object") {
25171
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
25172
- if (isCanonicalSanctuaryName(name)) continue;
25173
- const entry = parseServerEntry(name, serverConfig);
25174
- if (entry) servers.push(entry);
25175
- }
25034
+ };
25035
+ const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
25036
+ const { tools: handshakeTools, handshakeResults } = createHandshakeTools(
25037
+ config,
25038
+ identityManager,
25039
+ masterKey,
25040
+ auditLog,
25041
+ {
25042
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
25043
+ verascoreUrl: config.verascore.url
25176
25044
  }
25177
- }
25178
- if (platform4 === "hermes") {
25179
- const mcpServers = obj.mcp_servers;
25180
- if (mcpServers && typeof mcpServers === "object") {
25181
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
25182
- if (isCanonicalSanctuaryName(name)) continue;
25183
- const entry = parseServerEntry(name, serverConfig);
25184
- if (entry) servers.push(entry);
25185
- }
25045
+ );
25046
+ const { tools: l4Tools, reputationStore } = createL4Tools(
25047
+ storage,
25048
+ masterKey,
25049
+ identityManager,
25050
+ auditLog,
25051
+ handshakeResults,
25052
+ config.verascore.url
25053
+ );
25054
+ const { tools: shrTools } = createSHRTools(
25055
+ config,
25056
+ identityManager,
25057
+ masterKey,
25058
+ auditLog,
25059
+ reputationStore
25060
+ );
25061
+ const { tools: federationTools } = createFederationTools(
25062
+ auditLog,
25063
+ handshakeResults
25064
+ );
25065
+ const { tools: bridgeTools } = createBridgeTools(
25066
+ storage,
25067
+ masterKey,
25068
+ identityManager,
25069
+ auditLog,
25070
+ handshakeResults
25071
+ );
25072
+ const { tools: auditTools } = createAuditTools(config);
25073
+ const { tools: siemTools } = createSIEMTools(auditLog);
25074
+ const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog);
25075
+ const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
25076
+ const profileStore = new SovereigntyProfileStore(storage, masterKey);
25077
+ await profileStore.load();
25078
+ const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
25079
+ const policy = await loadPrincipalPolicy(config.storage_path);
25080
+ const baseline = new BaselineTracker(storage, masterKey);
25081
+ await baseline.load();
25082
+ let approvalChannel;
25083
+ let dashboard;
25084
+ if (config.dashboard.enabled) {
25085
+ let authToken = config.dashboard.auth_token;
25086
+ if (authToken === "auto") {
25087
+ const { randomBytes: rb } = await import('crypto');
25088
+ authToken = rb(32).toString("hex");
25186
25089
  }
25090
+ dashboard = new DashboardApprovalChannel({
25091
+ port: config.dashboard.port,
25092
+ host: config.dashboard.host,
25093
+ timeout_seconds: policy.approval_channel.timeout_seconds,
25094
+ // SEC-002: auto_deny removed — timeout always denies
25095
+ auth_token: authToken,
25096
+ tls: config.dashboard.tls,
25097
+ auto_open: config.dashboard.auto_open
25098
+ });
25099
+ dashboard.setDependencies({
25100
+ policy,
25101
+ baseline,
25102
+ auditLog,
25103
+ identityManager,
25104
+ handshakeResults,
25105
+ shrOpts: { config, identityManager, masterKey },
25106
+ sanctuaryConfig: config,
25107
+ profileStore
25108
+ });
25109
+ await dashboard.start();
25110
+ approvalChannel = dashboard;
25111
+ } else if (config.webhook.enabled && config.webhook.url && config.webhook.secret) {
25112
+ const webhook = new WebhookApprovalChannel({
25113
+ webhook_url: config.webhook.url,
25114
+ webhook_secret: config.webhook.secret,
25115
+ callback_port: config.webhook.callback_port,
25116
+ callback_host: config.webhook.callback_host,
25117
+ timeout_seconds: policy.approval_channel.timeout_seconds
25118
+ // SEC-002: auto_deny removed — timeout always denies
25119
+ });
25120
+ await webhook.start();
25121
+ approvalChannel = webhook;
25122
+ } else {
25123
+ approvalChannel = new StderrApprovalChannel(policy.approval_channel);
25187
25124
  }
25188
- if (platform4 === "cline") {
25189
- const mcpServers = obj.mcpServers;
25190
- if (mcpServers && typeof mcpServers === "object") {
25191
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
25192
- if (isCanonicalSanctuaryName(name)) continue;
25193
- const entry = parseServerEntry(name, serverConfig);
25194
- if (entry) servers.push(entry);
25125
+ const injectionDetector = new InjectionDetector({
25126
+ enabled: true,
25127
+ sensitivity: "medium",
25128
+ on_detection: "escalate"
25129
+ });
25130
+ const onInjectionAlert = dashboard ? (alert) => {
25131
+ dashboard.broadcastSSE("injection-alert", {
25132
+ tool: alert.toolName,
25133
+ confidence: alert.result.confidence,
25134
+ signals: alert.result.signals.map((s) => ({
25135
+ type: s.type,
25136
+ location: s.location,
25137
+ severity: s.severity
25138
+ })),
25139
+ recommendation: alert.result.recommendation,
25140
+ timestamp: alert.timestamp
25141
+ });
25142
+ } : void 0;
25143
+ const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
25144
+ const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
25145
+ const { tools: sanctuaryMetaTools } = createSanctuaryTools({
25146
+ config,
25147
+ identityManager,
25148
+ masterKey,
25149
+ auditLog,
25150
+ policy,
25151
+ keyProtection,
25152
+ reputationStore
25153
+ });
25154
+ const { tools: memoryAttestTools } = createMemoryAttestTools(
25155
+ identityManager,
25156
+ masterKey,
25157
+ auditLog
25158
+ );
25159
+ const { tools: complianceTools } = createComplianceTools({
25160
+ config,
25161
+ identityManager,
25162
+ masterKey,
25163
+ auditLog,
25164
+ policy
25165
+ });
25166
+ const dashboardTools = [];
25167
+ if (dashboard) {
25168
+ dashboardTools.push({
25169
+ name: "dashboard_open",
25170
+ description: "Generate a one-click URL to open the Principal Dashboard in a browser. Returns a pre-authenticated link \u2014 no manual token entry needed.",
25171
+ inputSchema: {
25172
+ type: "object",
25173
+ properties: {}
25174
+ },
25175
+ handler: async () => {
25176
+ const url = dashboard.createSessionUrl();
25177
+ return {
25178
+ content: [
25179
+ {
25180
+ type: "text",
25181
+ text: JSON.stringify({
25182
+ dashboard_url: url,
25183
+ base_url: dashboard.getBaseUrl(),
25184
+ note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
25185
+ }, null, 2)
25186
+ }
25187
+ ]
25188
+ };
25195
25189
  }
25196
- }
25197
- }
25198
- return servers;
25199
- }
25200
- function isCanonicalSanctuaryName(name) {
25201
- return name.toLowerCase() === "sanctuary";
25202
- }
25203
- function parseServerEntry(name, config) {
25204
- if (!config || typeof config !== "object") return null;
25205
- const c = config;
25206
- const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "-").substring(0, 128);
25207
- if (!safeName) return null;
25208
- if (c.url && typeof c.url === "string") {
25209
- return {
25210
- name: safeName,
25211
- transport: "sse",
25212
- url: c.url,
25213
- env: extractEnv(c.env)
25214
- };
25215
- }
25216
- if (c.command && typeof c.command === "string") {
25217
- return {
25218
- name: safeName,
25219
- transport: "stdio",
25220
- command: c.command,
25221
- args: Array.isArray(c.args) ? c.args.filter((a) => typeof a === "string") : void 0,
25222
- env: extractEnv(c.env)
25223
- };
25224
- }
25225
- return null;
25226
- }
25227
- function extractEnv(env) {
25228
- if (!env || typeof env !== "object") return void 0;
25229
- const result = {};
25230
- for (const [k, v] of Object.entries(env)) {
25231
- if (typeof v === "string") result[k] = v;
25232
- }
25233
- return Object.keys(result).length > 0 ? result : void 0;
25234
- }
25235
- async function rewriteConfigForCocoon(agentConfig, sanctuaryCommand, sanctuaryArgs, sanctuaryEnv) {
25236
- const raw = agentConfig.rawConfig;
25237
- let existingServers = {};
25238
- if (agentConfig.platform === "openclaw") {
25239
- const existingMcp = raw.mcp ?? {};
25240
- existingServers = existingMcp.servers ?? {};
25241
- } else if (agentConfig.platform === "hermes") {
25242
- existingServers = raw.mcp_servers ?? {};
25243
- } else {
25244
- existingServers = raw.mcpServers ?? {};
25245
- }
25246
- let resolvedEnv = sanctuaryEnv;
25247
- if (!resolvedEnv) {
25248
- const existingSanctuary = existingServers.sanctuary;
25249
- if (existingSanctuary?.env && typeof existingSanctuary.env === "object") {
25250
- const extracted = extractEnv(existingSanctuary.env);
25251
- if (extracted) resolvedEnv = extracted;
25252
- }
25190
+ });
25253
25191
  }
25254
- const CRITICAL_VARS = [
25255
- "SANCTUARY_PASSPHRASE",
25256
- "SANCTUARY_DASHBOARD_AUTH_TOKEN",
25257
- "SANCTUARY_DASHBOARD_ENABLED"
25192
+ let allTools = [
25193
+ ...l1Tools,
25194
+ ...l2Tools,
25195
+ ...l3Tools,
25196
+ ...l4Tools,
25197
+ ...policyTools,
25198
+ ...shrTools,
25199
+ ...handshakeTools,
25200
+ ...federationTools,
25201
+ ...bridgeTools,
25202
+ ...auditTools,
25203
+ ...siemTools,
25204
+ ...contextGateTools,
25205
+ ...hardeningTools,
25206
+ ...profileTools,
25207
+ ...dashboardTools,
25208
+ ...sanctuaryMetaTools,
25209
+ ...memoryAttestTools,
25210
+ ...complianceTools,
25211
+ manifestTool
25258
25212
  ];
25259
- for (const key of CRITICAL_VARS) {
25260
- if (process.env[key] && (!resolvedEnv || !resolvedEnv[key])) {
25261
- if (!resolvedEnv) resolvedEnv = {};
25262
- resolvedEnv[key] = process.env[key];
25263
- }
25264
- }
25265
- const sanctuaryEntry = {
25266
- command: sanctuaryCommand,
25267
- args: sanctuaryArgs
25268
- };
25269
- if (resolvedEnv && Object.keys(resolvedEnv).length > 0) {
25270
- sanctuaryEntry.env = resolvedEnv;
25271
- }
25272
- let rewritten;
25273
- if (agentConfig.platform === "openclaw") {
25274
- const existingMcp = raw.mcp ?? {};
25275
- rewritten = {
25276
- ...raw,
25277
- mcp: {
25278
- ...existingMcp,
25279
- servers: {
25280
- ...existingServers,
25281
- sanctuary: sanctuaryEntry
25213
+ let clientManager;
25214
+ let proxyRouter;
25215
+ const governor = new CallGovernor();
25216
+ const { tools: governorTools } = createGovernorTools(governor, auditLog);
25217
+ allTools.push(...governorTools);
25218
+ const profile = profileStore.get();
25219
+ if (profile.upstream_servers && profile.upstream_servers.length > 0) {
25220
+ const enabledServers = profile.upstream_servers.filter((s) => s.enabled);
25221
+ if (enabledServers.length > 0) {
25222
+ clientManager = new ClientManager({
25223
+ onStateChange: (serverName, state, toolCount, error) => {
25224
+ if (dashboard) {
25225
+ dashboard.broadcastSSE("proxy-server-status", {
25226
+ server: serverName,
25227
+ state,
25228
+ tool_count: toolCount,
25229
+ error,
25230
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
25231
+ });
25232
+ }
25233
+ auditLog.append("l2", `proxy_server_${state}`, "system", {
25234
+ server: serverName,
25235
+ tool_count: toolCount,
25236
+ error
25237
+ });
25282
25238
  }
25239
+ });
25240
+ proxyRouter = new ProxyRouter(
25241
+ clientManager,
25242
+ injectionDetector,
25243
+ auditLog,
25244
+ {
25245
+ contextGateFilter: async (_toolName, args) => {
25246
+ const activeProfile = profileStore.get();
25247
+ if (activeProfile.features.context_gating.enabled) {
25248
+ return args;
25249
+ }
25250
+ return args;
25251
+ },
25252
+ governor,
25253
+ onProxyCall: (data) => {
25254
+ if (dashboard) {
25255
+ dashboard.broadcastProxyCall(data);
25256
+ }
25257
+ }
25258
+ }
25259
+ );
25260
+ clientManager.configure(enabledServers).catch((err) => {
25261
+ console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
25262
+ });
25263
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
25264
+ const proxiedTools = proxyRouter.getProxiedTools();
25265
+ if (proxiedTools.length > 0) {
25266
+ allTools.push(...proxiedTools);
25267
+ }
25268
+ if (dashboard) {
25269
+ dashboard.setDependencies({
25270
+ policy,
25271
+ baseline,
25272
+ auditLog,
25273
+ clientManager
25274
+ });
25275
+ dashboard.enableFortressView(enabledServers.length);
25283
25276
  }
25284
- };
25285
- delete rewritten.mcpServers;
25286
- } else if (agentConfig.platform === "hermes") {
25287
- rewritten = {
25288
- ...raw,
25289
- mcp_servers: {
25290
- ...existingServers,
25291
- sanctuary: sanctuaryEntry
25292
- }
25293
- };
25294
- } else {
25295
- rewritten = {
25296
- ...raw,
25297
- mcpServers: {
25298
- ...existingServers,
25299
- sanctuary: sanctuaryEntry
25300
- }
25301
- };
25302
- }
25303
- await promises.writeFile(agentConfig.configPath, JSON.stringify(rewritten, null, 2), { mode: 384 });
25304
- return agentConfig.configPath;
25305
- }
25306
- var init_config_reader = __esm({
25307
- "src/cocoon/config-reader.ts"() {
25308
- init_paths();
25309
- }
25310
- });
25311
-
25312
- // src/cocoon/passphrase.ts
25313
- var passphrase_exports = {};
25314
- __export(passphrase_exports, {
25315
- PassphraseUnreadableError: () => PassphraseUnreadableError,
25316
- fallbackFilePath: () => fallbackFilePath,
25317
- generatePassphrase: () => generatePassphrase,
25318
- getOrCreatePassphrase: () => getOrCreatePassphrase,
25319
- keychainServiceFor: () => keychainServiceFor,
25320
- persistUserProvidedPassphrase: () => persistUserProvidedPassphrase,
25321
- readStoredPassphrase: () => readStoredPassphrase
25322
- });
25323
- async function getOrCreatePassphrase(opts = {}) {
25324
- const home = opts.home ?? os.homedir();
25325
- const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
25326
- const service = keychainServiceFor(storagePath, home);
25327
- const plat = opts.platformOverride ?? os.platform();
25328
- const exec2 = opts.exec ?? defaultExec;
25329
- const derive = opts.deriveMachineKey ?? deriveMachineKey;
25330
- if (plat === "darwin") {
25331
- const fromKc = await readFromKeychain(exec2, service);
25332
- if (fromKc) {
25333
- return { value: fromKc, source: "keychain", location: "macOS Keychain" };
25334
- }
25335
- }
25336
- const fallback = fallbackFilePath(home, storagePath);
25337
- const fromFile = await readFromFallbackFile(fallback, home, derive);
25338
- if (fromFile.status === "ok") {
25339
- return {
25340
- value: fromFile.value,
25341
- source: "fallback-file",
25342
- location: fallback
25343
- };
25344
- }
25345
- if (fromFile.status === "unreadable") {
25346
- throw new PassphraseUnreadableError(fallback, fromFile.reason);
25347
- }
25348
- const value = generatePassphrase();
25349
- if (plat === "darwin") {
25350
- const ok2 = await writeToKeychain(value, exec2, service);
25351
- if (ok2) {
25352
- return { value, source: "generated", location: "macOS Keychain" };
25353
- }
25354
- }
25355
- await writeToFallbackFile(fallback, value, home, derive);
25356
- return { value, source: "generated", location: fallback };
25357
- }
25358
- async function readStoredPassphrase(opts = {}) {
25359
- const home = opts.home ?? os.homedir();
25360
- const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
25361
- const service = keychainServiceFor(storagePath, home);
25362
- const plat = opts.platformOverride ?? os.platform();
25363
- const exec2 = opts.exec ?? defaultExec;
25364
- const derive = opts.deriveMachineKey ?? deriveMachineKey;
25365
- if (plat === "darwin") {
25366
- const fromKc = await readFromKeychain(exec2, service);
25367
- if (fromKc) {
25368
- return { value: fromKc, source: "keychain", location: "macOS Keychain" };
25369
25277
  }
25370
25278
  }
25371
- const fallback = fallbackFilePath(home, storagePath);
25372
- const fromFile = await readFromFallbackFile(fallback, home, derive);
25373
- if (fromFile.status === "ok") {
25374
- return {
25375
- value: fromFile.value,
25376
- source: "fallback-file",
25377
- location: fallback
25378
- };
25379
- }
25380
- if (fromFile.status === "unreadable") {
25381
- throw new PassphraseUnreadableError(fallback, fromFile.reason);
25279
+ allTools = allTools.map((tool) => ({
25280
+ ...tool,
25281
+ handler: contextGateEnforcer.wrapHandler(tool.name, tool.handler)
25282
+ }));
25283
+ if (proxyRouter) {
25284
+ gate.setProxyTierResolver((toolName) => {
25285
+ const parsed = ProxyRouter.parseProxyToolName(toolName);
25286
+ if (!parsed) return null;
25287
+ return proxyRouter.getTierForTool(parsed.serverName, parsed.toolName);
25288
+ });
25382
25289
  }
25383
- return null;
25384
- }
25385
- async function persistUserProvidedPassphrase(value, opts = {}) {
25386
- const home = opts.home ?? os.homedir();
25387
- const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
25388
- const service = keychainServiceFor(storagePath, home);
25389
- const plat = opts.platformOverride ?? os.platform();
25390
- const exec2 = opts.exec ?? defaultExec;
25391
- const derive = opts.deriveMachineKey ?? deriveMachineKey;
25392
- if (plat === "darwin") {
25393
- const ok2 = await writeToKeychain(value, exec2, service);
25394
- if (ok2) {
25395
- return { location: "macOS Keychain", source: "keychain" };
25290
+ const server = createServer(allTools, { gate });
25291
+ await saveConfig(config);
25292
+ const cleanup = () => {
25293
+ baseline.save().catch(() => {
25294
+ });
25295
+ if (clientManager) {
25296
+ clientManager.shutdown().catch(() => {
25297
+ });
25396
25298
  }
25397
- }
25398
- const fallback = fallbackFilePath(home, storagePath);
25399
- try {
25400
- await writeToFallbackFile(fallback, value, home, derive);
25401
- } catch (err) {
25402
- throw new Error(
25403
- `Could not persist the provided passphrase to either Keychain or ${fallback}: ${err.message}. Refusing to proceed \u2014 writing the passphrase into the rewritten agent config would leak it as plaintext at rest and in process argv.`
25299
+ };
25300
+ process.on("SIGINT", cleanup);
25301
+ process.on("SIGTERM", cleanup);
25302
+ if (recoveryKey) {
25303
+ console.error(
25304
+ `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
25305
+ \u2551 SANCTUARY: First Run \u2014 Recovery Key Generated \u2551
25306
+ \u2551 \u2551
25307
+ \u2551 Recovery Key: ${recoveryKey.slice(0, 20)}... \u2551
25308
+ \u2551 \u2551
25309
+ \u2551 SAVE THIS KEY. It will not be shown again. \u2551
25310
+ \u2551 Without it, your encrypted state is unrecoverable. \u2551
25311
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`
25404
25312
  );
25405
25313
  }
25406
- return { location: fallback, source: "fallback-file" };
25407
- }
25408
- function generatePassphrase() {
25409
- return crypto.randomBytes(32).toString("base64");
25314
+ return {
25315
+ server,
25316
+ config,
25317
+ identityManager,
25318
+ masterKey,
25319
+ auditLog,
25320
+ policy
25321
+ };
25410
25322
  }
25411
- async function readFromKeychain(exec2, service = KEYCHAIN_SERVICE_DEFAULT) {
25412
- try {
25413
- const result = await exec2(
25414
- "security",
25415
- ["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"]
25416
- );
25417
- if (result.code !== 0) return null;
25418
- const value = result.stdout.trim();
25419
- return value.length > 0 ? value : null;
25420
- } catch {
25421
- return null;
25323
+ var init_src = __esm({
25324
+ "src/index.ts"() {
25325
+ init_permissions();
25326
+ init_config();
25327
+ init_filesystem();
25328
+ init_state_store();
25329
+ init_tools();
25330
+ init_audit_log();
25331
+ init_tools2();
25332
+ init_tools3();
25333
+ init_loader();
25334
+ init_baseline();
25335
+ init_approval_channel();
25336
+ init_dashboard();
25337
+ init_webhook();
25338
+ init_gate();
25339
+ init_tools4();
25340
+ init_router();
25341
+ init_router();
25342
+ init_tools5();
25343
+ init_tools6();
25344
+ init_tools7();
25345
+ init_tools8();
25346
+ init_tools9();
25347
+ init_siem_tools();
25348
+ init_context_gate_tools();
25349
+ init_hardening_tools();
25350
+ init_sovereignty_profile();
25351
+ init_sovereignty_profile_tools();
25352
+ init_injection_detector();
25353
+ init_client_manager();
25354
+ init_proxy_router();
25355
+ init_call_governor();
25356
+ init_governor_tools();
25357
+ init_sanctuary_tools();
25358
+ init_memory_attest();
25359
+ init_generator2();
25360
+ init_key_derivation();
25361
+ init_random();
25362
+ init_encoding();
25363
+ init_config();
25364
+ init_state_store();
25365
+ init_audit_log();
25366
+ init_commitments();
25367
+ init_zk_proofs();
25368
+ init_policies();
25369
+ init_reputation_store();
25370
+ init_tiers();
25371
+ init_registry();
25372
+ init_context_gate();
25373
+ init_context_gate_templates();
25374
+ init_context_gate_recommend();
25375
+ init_model_provenance();
25376
+ init_context_gate();
25377
+ init_injection_detector();
25378
+ init_context_gate_enforcer();
25379
+ init_sovereignty_profile();
25380
+ init_client_manager();
25381
+ init_proxy_router();
25382
+ init_system_prompt_generator();
25383
+ init_memory();
25384
+ init_filesystem();
25385
+ init_gate();
25386
+ init_baseline();
25387
+ init_loader();
25388
+ init_approval_channel();
25389
+ init_dashboard();
25390
+ init_webhook();
25391
+ init_generator();
25392
+ init_verifier();
25393
+ init_protocol();
25394
+ init_attestation();
25395
+ init_bridge();
25396
+ init_dashboard2();
25422
25397
  }
25398
+ });
25399
+ function getPlatformPaths() {
25400
+ const home = os.homedir();
25401
+ return {
25402
+ "openclaw": [
25403
+ path.join(home, ".openclaw", "openclaw.json"),
25404
+ path.join(home, ".openclaw", "config.json"),
25405
+ path.join(home, "Library", "Application Support", "OpenClaw", "openclaw.json"),
25406
+ path.join(home, "Library", "Application Support", "OpenClaw", "config.json")
25407
+ ],
25408
+ // Hermes Agent (NousResearch, v0.9.0) canonicals live under ~/.hermes.
25409
+ // Hermes ships `cli-config.yaml` as the primary surface per upstream docs.
25410
+ // Sanctuary wrap v1.0 detects the JSON variant only: operators who keep
25411
+ // YAML can still wrap via `sanctuary wrap --wrap <path>` after exporting
25412
+ // to JSON. YAML-native detection is flagged as a v1.x follow-up.
25413
+ "hermes": [
25414
+ path.join(home, ".hermes", "cli-config.json"),
25415
+ path.join(home, ".hermes", "config.json"),
25416
+ path.join(home, ".config", "hermes", "cli-config.json")
25417
+ ],
25418
+ // Claude Code's modern canonical surface is ~/.claude.json (`claude mcp
25419
+ // add` writes here). The legacy ~/.claude/settings.json shape predates
25420
+ // it and is still respected if present. Probe order = preference order:
25421
+ // wrap operates on the first one that exists, and bootstraps a fresh
25422
+ // ~/.claude.json when neither is present (per the cli.ts bootstrap).
25423
+ "claude-code": [
25424
+ path.join(home, ".claude.json"),
25425
+ path.join(home, ".claude", "settings.json"),
25426
+ path.join(home, ".config", "claude-code", "settings.json")
25427
+ ],
25428
+ "cursor": [
25429
+ path.join(home, ".cursor", "mcp.json")
25430
+ ],
25431
+ // Cline is a VS Code extension (saoudrizwan.claude-dev). Its MCP settings
25432
+ // live under the VS Code globalStorage tree, which is OS-specific. We
25433
+ // enumerate the three supported OS layouts; at detection time only the
25434
+ // one matching the running OS will exist.
25435
+ "cline": [
25436
+ // macOS
25437
+ path.join(
25438
+ home,
25439
+ "Library",
25440
+ "Application Support",
25441
+ "Code",
25442
+ "User",
25443
+ "globalStorage",
25444
+ "saoudrizwan.claude-dev",
25445
+ "settings",
25446
+ "cline_mcp_settings.json"
25447
+ ),
25448
+ // Linux
25449
+ path.join(
25450
+ home,
25451
+ ".config",
25452
+ "Code",
25453
+ "User",
25454
+ "globalStorage",
25455
+ "saoudrizwan.claude-dev",
25456
+ "settings",
25457
+ "cline_mcp_settings.json"
25458
+ ),
25459
+ // Windows (honour APPDATA when set, otherwise reconstruct under home)
25460
+ process.env.APPDATA ? path.join(
25461
+ process.env.APPDATA,
25462
+ "Code",
25463
+ "User",
25464
+ "globalStorage",
25465
+ "saoudrizwan.claude-dev",
25466
+ "settings",
25467
+ "cline_mcp_settings.json"
25468
+ ) : path.join(
25469
+ home,
25470
+ "AppData",
25471
+ "Roaming",
25472
+ "Code",
25473
+ "User",
25474
+ "globalStorage",
25475
+ "saoudrizwan.claude-dev",
25476
+ "settings",
25477
+ "cline_mcp_settings.json"
25478
+ )
25479
+ ],
25480
+ "generic": []
25481
+ };
25482
+ }
25483
+ function backupDir() {
25484
+ return path.join(resolveStoragePath(), "backup");
25485
+ }
25486
+ async function backupConfig(configPath) {
25487
+ const dir = backupDir();
25488
+ await promises.mkdir(dir, { recursive: true, mode: 448 });
25489
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
25490
+ const backupPath = path.join(dir, `config-backup-${timestamp}.json`);
25491
+ await promises.copyFile(configPath, backupPath);
25492
+ return backupPath;
25423
25493
  }
25424
- async function writeToKeychain(value, exec2, service = KEYCHAIN_SERVICE_DEFAULT) {
25494
+ async function restoreConfig(backupPath, targetPath) {
25495
+ await promises.copyFile(backupPath, targetPath);
25496
+ }
25497
+ async function findLatestBackup() {
25498
+ const metaPath = path.join(backupDir(), "cocoon-meta.json");
25425
25499
  try {
25426
- const result = await exec2(
25427
- "security",
25428
- [
25429
- "add-generic-password",
25430
- "-U",
25431
- "-a",
25432
- KEYCHAIN_ACCOUNT,
25433
- "-s",
25434
- service,
25435
- "-w",
25436
- value
25437
- ]
25438
- );
25439
- return result.code === 0;
25500
+ const raw = await promises.readFile(metaPath, "utf-8");
25501
+ const meta = JSON.parse(raw);
25502
+ return {
25503
+ backupPath: meta.backupPath,
25504
+ originalPath: meta.originalPath
25505
+ };
25440
25506
  } catch {
25441
- return false;
25507
+ return null;
25442
25508
  }
25443
25509
  }
25444
- function keychainServiceFor(storagePath, home = os.homedir()) {
25445
- const defaultPath = path.join(home, DEFAULT_STORAGE_DIR);
25446
- if (storagePath === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
25447
- const digest = sha256.sha256(Buffer.from(storagePath, "utf-8"));
25448
- const suffix = Buffer.from(digest).toString("hex").slice(0, 12);
25449
- return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
25510
+ async function saveCocoonMeta(meta) {
25511
+ const dir = backupDir();
25512
+ await promises.mkdir(dir, { recursive: true, mode: 448 });
25513
+ const metaPath = path.join(dir, "cocoon-meta.json");
25514
+ await promises.writeFile(metaPath, JSON.stringify(meta, null, 2), { mode: 384 });
25450
25515
  }
25451
- function fallbackFilePath(home, storagePath) {
25452
- if (storagePath !== void 0) return path.join(storagePath, "passphrase.enc");
25453
- return path.join(home, DEFAULT_STORAGE_DIR, "passphrase.enc");
25516
+ async function detectAgentConfigWithDiagnostics(platform4, configPath) {
25517
+ const pathsChecked = [];
25518
+ const errors = [];
25519
+ if (configPath) {
25520
+ pathsChecked.push(configPath);
25521
+ const { config, error } = await readConfigFileWithError(configPath, platform4 ?? "generic");
25522
+ if (error) errors.push({ path: configPath, error });
25523
+ return { config, pathsChecked, errors };
25524
+ }
25525
+ if (platform4) {
25526
+ const paths = getPlatformPaths()[platform4];
25527
+ for (const path of paths) {
25528
+ pathsChecked.push(path);
25529
+ const { config, error } = await readConfigFileWithError(path, platform4);
25530
+ if (error) errors.push({ path, error });
25531
+ if (config) return { config, pathsChecked, errors };
25532
+ }
25533
+ return { config: null, pathsChecked, errors };
25534
+ }
25535
+ for (const [plat, paths] of Object.entries(getPlatformPaths())) {
25536
+ for (const path of paths) {
25537
+ pathsChecked.push(path);
25538
+ const { config, error } = await readConfigFileWithError(path, plat);
25539
+ if (error) errors.push({ path, error });
25540
+ if (config) return { config, pathsChecked, errors };
25541
+ }
25542
+ }
25543
+ return { config: null, pathsChecked, errors };
25454
25544
  }
25455
- async function readFromFallbackFile(path, home, derive = deriveMachineKey) {
25545
+ async function readConfigFileWithError(path, platform4) {
25456
25546
  try {
25457
25547
  await promises.access(path);
25458
25548
  } catch {
25459
- return { status: "not-found" };
25549
+ return { config: null };
25460
25550
  }
25551
+ let raw;
25461
25552
  try {
25462
- const raw = await promises.readFile(path);
25463
- if (raw.length < 13) {
25464
- return { status: "unreadable", reason: "file too short to contain a valid nonce + ciphertext" };
25465
- }
25466
- const nonce = raw.subarray(0, 12);
25467
- const ciphertext = raw.subarray(12);
25468
- const key = derive(home);
25469
- const cipher = aes_js.gcm(key, nonce);
25470
- const plain = cipher.decrypt(ciphertext);
25471
- return { status: "ok", value: Buffer.from(plain).toString("utf-8") };
25553
+ raw = await promises.readFile(path, "utf-8");
25472
25554
  } catch (err) {
25473
- return {
25474
- status: "unreadable",
25475
- reason: err.message ?? "unknown decryption error"
25476
- };
25555
+ return { config: null, error: `Cannot read file: ${err.message}` };
25477
25556
  }
25557
+ let config;
25558
+ try {
25559
+ config = JSON.parse(raw);
25560
+ } catch (err) {
25561
+ return { config: null, error: `Invalid JSON: ${err.message}` };
25562
+ }
25563
+ const servers = extractServers(config, platform4);
25564
+ return { config: { platform: platform4, configPath: path, servers, rawConfig: config } };
25478
25565
  }
25479
- async function writeToFallbackFile(path$1, value, home, derive = deriveMachineKey) {
25480
- const dir = path.dirname(path$1);
25481
- await promises.mkdir(dir, { recursive: true, mode: 448 });
25482
- const nonce = crypto.randomBytes(12);
25483
- const key = derive(home);
25484
- const cipher = aes_js.gcm(key, nonce);
25485
- const ciphertext = cipher.encrypt(Buffer.from(value, "utf-8"));
25486
- const payload = Buffer.concat([nonce, Buffer.from(ciphertext)]);
25487
- await promises.writeFile(path$1, payload, { mode: 384 });
25488
- }
25489
- function deriveMachineKey(home) {
25490
- const info = os.userInfo();
25491
- const material = Buffer.from(
25492
- `${os.hostname()}:${info.uid}:${info.username}:${home}`,
25493
- "utf-8"
25494
- );
25495
- return hkdf.hkdf(sha256.sha256, material, void 0, "sanctuary-passphrase-v1", 32);
25496
- }
25497
- async function defaultExec(cmd, args, input) {
25498
- return new Promise((resolve2, reject) => {
25499
- const child = child_process.spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
25500
- let stdout = "";
25501
- let stderr = "";
25502
- child.stdout.on("data", (d) => {
25503
- stdout += d.toString();
25504
- });
25505
- child.stderr.on("data", (d) => {
25506
- stderr += d.toString();
25507
- });
25508
- child.on("error", reject);
25509
- child.on("close", (code) => resolve2({ stdout, stderr, code }));
25510
- if (input !== void 0) {
25511
- child.stdin.write(input);
25566
+ function extractServers(config, platform4) {
25567
+ if (!config || typeof config !== "object") return [];
25568
+ const servers = [];
25569
+ const obj = config;
25570
+ if (platform4 === "openclaw" || platform4 === "generic") {
25571
+ const mcp = obj.mcp;
25572
+ const nestedServers = mcp?.servers;
25573
+ if (nestedServers && typeof nestedServers === "object") {
25574
+ for (const [name, serverConfig] of Object.entries(nestedServers)) {
25575
+ const entry = parseServerEntry(name, serverConfig);
25576
+ if (entry) servers.push(entry);
25577
+ }
25512
25578
  }
25513
- child.stdin.end();
25514
- });
25515
- }
25516
- var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_DEFAULT, PassphraseUnreadableError;
25517
- var init_passphrase = __esm({
25518
- "src/cocoon/passphrase.ts"() {
25519
- init_paths();
25520
- KEYCHAIN_ACCOUNT = "sanctuary";
25521
- KEYCHAIN_SERVICE_DEFAULT = "sanctuary-passphrase";
25522
- PassphraseUnreadableError = class extends Error {
25523
- path;
25524
- reason;
25525
- constructor(path, reason) {
25526
- super(
25527
- `Sanctuary passphrase file at ${path} exists but could not be decrypted (${reason}).
25528
-
25529
- Your existing encrypted state cannot be recovered with a new passphrase. Options:
25530
- 1. Restore ${path} from a backup.
25531
- 2. Re-import the original passphrase via SANCTUARY_PASSPHRASE=<value> sanctuary wrap ...
25532
- 3. Run \`sanctuary reset-passphrase\` (coming soon) to wipe state and start fresh.
25533
-
25534
- Refusing to regenerate the passphrase \u2014 that would permanently destroy the data encrypted under the previous key.`
25535
- );
25536
- this.name = "PassphraseUnreadableError";
25537
- this.path = path;
25538
- this.reason = reason;
25579
+ if (servers.length === 0) {
25580
+ const mcpServers = obj.mcpServers;
25581
+ if (mcpServers && typeof mcpServers === "object") {
25582
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25583
+ const entry = parseServerEntry(name, serverConfig);
25584
+ if (entry) servers.push(entry);
25585
+ }
25539
25586
  }
25540
- };
25587
+ }
25541
25588
  }
25542
- });
25543
- function runtimePath(storagePath) {
25544
- return path.join(storagePath, RUNTIME_FILE_NAME);
25589
+ if (platform4 === "claude-code") {
25590
+ const mcpServers = obj.mcpServers;
25591
+ if (mcpServers && typeof mcpServers === "object") {
25592
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25593
+ if (isCanonicalSanctuaryName(name)) continue;
25594
+ const entry = parseServerEntry(name, serverConfig);
25595
+ if (entry) servers.push(entry);
25596
+ }
25597
+ }
25598
+ }
25599
+ if (platform4 === "cursor") {
25600
+ const mcpServers = obj.mcpServers;
25601
+ if (mcpServers && typeof mcpServers === "object") {
25602
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25603
+ if (isCanonicalSanctuaryName(name)) continue;
25604
+ const entry = parseServerEntry(name, serverConfig);
25605
+ if (entry) servers.push(entry);
25606
+ }
25607
+ }
25608
+ }
25609
+ if (platform4 === "hermes") {
25610
+ const mcpServers = obj.mcp_servers;
25611
+ if (mcpServers && typeof mcpServers === "object") {
25612
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25613
+ if (isCanonicalSanctuaryName(name)) continue;
25614
+ const entry = parseServerEntry(name, serverConfig);
25615
+ if (entry) servers.push(entry);
25616
+ }
25617
+ }
25618
+ }
25619
+ if (platform4 === "cline") {
25620
+ const mcpServers = obj.mcpServers;
25621
+ if (mcpServers && typeof mcpServers === "object") {
25622
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25623
+ if (isCanonicalSanctuaryName(name)) continue;
25624
+ const entry = parseServerEntry(name, serverConfig);
25625
+ if (entry) servers.push(entry);
25626
+ }
25627
+ }
25628
+ }
25629
+ return servers;
25545
25630
  }
25546
- async function writeTenantRuntime(storagePath, state) {
25547
- try {
25548
- await promises.writeFile(
25549
- runtimePath(storagePath),
25550
- JSON.stringify(state, null, 2),
25551
- { mode: 384 }
25552
- );
25553
- } catch {
25631
+ function isCanonicalSanctuaryName(name) {
25632
+ return name.toLowerCase() === "sanctuary";
25633
+ }
25634
+ function parseServerEntry(name, config) {
25635
+ if (!config || typeof config !== "object") return null;
25636
+ const c = config;
25637
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "-").substring(0, 128);
25638
+ if (!safeName) return null;
25639
+ if (c.url && typeof c.url === "string") {
25640
+ return {
25641
+ name: safeName,
25642
+ transport: "sse",
25643
+ url: c.url,
25644
+ env: extractEnv(c.env)
25645
+ };
25646
+ }
25647
+ if (c.command && typeof c.command === "string") {
25648
+ return {
25649
+ name: safeName,
25650
+ transport: "stdio",
25651
+ command: c.command,
25652
+ args: Array.isArray(c.args) ? c.args.filter((a) => typeof a === "string") : void 0,
25653
+ env: extractEnv(c.env)
25654
+ };
25554
25655
  }
25656
+ return null;
25555
25657
  }
25556
- async function clearTenantRuntime(storagePath) {
25557
- try {
25558
- await promises.unlink(runtimePath(storagePath));
25559
- } catch {
25658
+ function extractEnv(env) {
25659
+ if (!env || typeof env !== "object") return void 0;
25660
+ const result = {};
25661
+ for (const [k, v] of Object.entries(env)) {
25662
+ if (typeof v === "string") result[k] = v;
25560
25663
  }
25664
+ return Object.keys(result).length > 0 ? result : void 0;
25561
25665
  }
25562
- async function readTenantRuntime(storagePath) {
25563
- try {
25564
- const raw = await promises.readFile(runtimePath(storagePath), "utf-8");
25565
- const parsed = JSON.parse(raw);
25566
- if (typeof parsed.dashboard_port !== "number" || typeof parsed.pid !== "number" || typeof parsed.started_at !== "string" || typeof parsed.version !== "string" || typeof parsed.dashboard_host !== "string" || typeof parsed.mode !== "string") {
25567
- return null;
25568
- }
25569
- const state = {
25570
- version: parsed.version,
25571
- pid: parsed.pid,
25572
- started_at: parsed.started_at,
25573
- dashboard_host: parsed.dashboard_host,
25574
- dashboard_port: parsed.dashboard_port,
25575
- mode: parsed.mode
25576
- };
25577
- if (typeof parsed.webhook_callback_port === "number") {
25578
- state.webhook_callback_port = parsed.webhook_callback_port;
25666
+ async function rewriteConfigForCocoon(agentConfig, sanctuaryCommand, sanctuaryArgs, sanctuaryEnv) {
25667
+ const raw = agentConfig.rawConfig;
25668
+ let existingServers = {};
25669
+ if (agentConfig.platform === "openclaw") {
25670
+ const existingMcp = raw.mcp ?? {};
25671
+ existingServers = existingMcp.servers ?? {};
25672
+ } else if (agentConfig.platform === "hermes") {
25673
+ existingServers = raw.mcp_servers ?? {};
25674
+ } else {
25675
+ existingServers = raw.mcpServers ?? {};
25676
+ }
25677
+ let resolvedEnv = sanctuaryEnv;
25678
+ if (!resolvedEnv) {
25679
+ const existingSanctuary = existingServers.sanctuary;
25680
+ if (existingSanctuary?.env && typeof existingSanctuary.env === "object") {
25681
+ const extracted = extractEnv(existingSanctuary.env);
25682
+ if (extracted) resolvedEnv = extracted;
25579
25683
  }
25580
- if (typeof parsed.webhook_callback_host === "string") {
25581
- state.webhook_callback_host = parsed.webhook_callback_host;
25684
+ }
25685
+ const CRITICAL_VARS = [
25686
+ "SANCTUARY_PASSPHRASE",
25687
+ "SANCTUARY_DASHBOARD_AUTH_TOKEN",
25688
+ "SANCTUARY_DASHBOARD_ENABLED"
25689
+ ];
25690
+ for (const key of CRITICAL_VARS) {
25691
+ if (process.env[key] && (!resolvedEnv || !resolvedEnv[key])) {
25692
+ if (!resolvedEnv) resolvedEnv = {};
25693
+ resolvedEnv[key] = process.env[key];
25582
25694
  }
25583
- return state;
25584
- } catch {
25585
- return null;
25586
25695
  }
25696
+ const sanctuaryEntry = {
25697
+ command: sanctuaryCommand,
25698
+ args: sanctuaryArgs
25699
+ };
25700
+ if (resolvedEnv && Object.keys(resolvedEnv).length > 0) {
25701
+ sanctuaryEntry.env = resolvedEnv;
25702
+ }
25703
+ let rewritten;
25704
+ if (agentConfig.platform === "openclaw") {
25705
+ const existingMcp = raw.mcp ?? {};
25706
+ rewritten = {
25707
+ ...raw,
25708
+ mcp: {
25709
+ ...existingMcp,
25710
+ servers: {
25711
+ ...existingServers,
25712
+ sanctuary: sanctuaryEntry
25713
+ }
25714
+ }
25715
+ };
25716
+ delete rewritten.mcpServers;
25717
+ } else if (agentConfig.platform === "hermes") {
25718
+ rewritten = {
25719
+ ...raw,
25720
+ mcp_servers: {
25721
+ ...existingServers,
25722
+ sanctuary: sanctuaryEntry
25723
+ }
25724
+ };
25725
+ } else {
25726
+ rewritten = {
25727
+ ...raw,
25728
+ mcpServers: {
25729
+ ...existingServers,
25730
+ sanctuary: sanctuaryEntry
25731
+ }
25732
+ };
25733
+ }
25734
+ await promises.writeFile(agentConfig.configPath, JSON.stringify(rewritten, null, 2), { mode: 384 });
25735
+ return agentConfig.configPath;
25587
25736
  }
25588
- var RUNTIME_FILE_NAME;
25589
- var init_runtime = __esm({
25590
- "src/cli/agents/runtime.ts"() {
25591
- RUNTIME_FILE_NAME = "runtime.json";
25737
+ var init_config_reader = __esm({
25738
+ "src/cocoon/config-reader.ts"() {
25739
+ init_paths();
25592
25740
  }
25593
25741
  });
25594
25742
 
@@ -28124,7 +28272,7 @@ async function runTemplateCommand(args) {
28124
28272
  case "list":
28125
28273
  return cmdList2(out);
28126
28274
  case "init":
28127
- return cmdInit(rest, out, err);
28275
+ return cmdInit(rest, out, err, args.isAgentWrapped ?? defaultIsAgentWrapped);
28128
28276
  default:
28129
28277
  err.write(`Unknown template subcommand: ${sub}
28130
28278
  `);
@@ -28132,6 +28280,11 @@ async function runTemplateCommand(args) {
28132
28280
  return 1;
28133
28281
  }
28134
28282
  }
28283
+ async function defaultIsAgentWrapped(agentId) {
28284
+ const tenant = await findTenant(agentId);
28285
+ if (!tenant) return false;
28286
+ return tenant.initialized || tenant.has_cocoon_profile;
28287
+ }
28135
28288
  function cmdList2(out, _err) {
28136
28289
  const templates = listTemplates();
28137
28290
  out.write("\nSanctuary Template Library\n");
@@ -28155,7 +28308,7 @@ function cmdList2(out, _err) {
28155
28308
  function padRight(str, len) {
28156
28309
  return str.length >= len ? str + " " : str + " ".repeat(len - str.length);
28157
28310
  }
28158
- function cmdInit(argv, out, err) {
28311
+ async function cmdInit(argv, out, err, isAgentWrapped) {
28159
28312
  let templateName;
28160
28313
  let agentId;
28161
28314
  let fortressId = "fortress-default";
@@ -28191,6 +28344,20 @@ function cmdInit(argv, out, err) {
28191
28344
  err.write("Error: --agent-id is required.\n");
28192
28345
  return 1;
28193
28346
  }
28347
+ const wrapped = await isAgentWrapped(agentId);
28348
+ if (!wrapped) {
28349
+ err.write(
28350
+ `Error: no wrapped harness found for agent-id "${agentId}".
28351
+
28352
+ A channel-shape template binds to an already-wrapped harness.
28353
+ Wrap the harness first, then re-run template init:
28354
+
28355
+ sanctuary wrap --claude-code # or --openclaw, --hermes, --cursor, --cline
28356
+ sanctuary template init ${templateName} --agent-id ${agentId}
28357
+ `
28358
+ );
28359
+ return 1;
28360
+ }
28194
28361
  const bundle = getTemplate2(templateName);
28195
28362
  if (!bundle) {
28196
28363
  err.write(`Error: template "${templateName}" not found.
@@ -28328,143 +28495,7 @@ var init_cli3 = __esm({
28328
28495
  init_registry2();
28329
28496
  init_init();
28330
28497
  init_canonical_policy();
28331
- }
28332
- });
28333
- async function isTenantDir(path$1) {
28334
- const [hasState, hasProfile, hasFallback] = await Promise.all([
28335
- dirExists(path.join(path$1, "state")),
28336
- fileExists2(path.join(path$1, "cocoon-profile.json")),
28337
- fileExists2(path.join(path$1, "passphrase.enc"))
28338
- ]);
28339
- const initialized = hasState;
28340
- let passphraseStatus;
28341
- if (hasFallback) passphraseStatus = "fallback-file";
28342
- else if (hasProfile || hasState) passphraseStatus = "keychain";
28343
- else passphraseStatus = "not-initialized";
28344
- return { initialized, hasProfile, passphraseStatus };
28345
- }
28346
- async function dirExists(path) {
28347
- try {
28348
- const s = await promises.stat(path);
28349
- return s.isDirectory();
28350
- } catch {
28351
- return false;
28352
- }
28353
- }
28354
- async function fileExists2(path) {
28355
- try {
28356
- const s = await promises.stat(path);
28357
- return s.isFile();
28358
- } catch {
28359
- return false;
28360
- }
28361
- }
28362
- async function newestAuditMtime(storagePath) {
28363
- const auditDir = path.join(storagePath, "state", "_audit");
28364
- let entries = [];
28365
- try {
28366
- entries = await promises.readdir(auditDir);
28367
- } catch {
28368
- return null;
28369
- }
28370
- let newest = 0;
28371
- for (const name of entries) {
28372
- try {
28373
- const s = await promises.stat(path.join(auditDir, name));
28374
- if (s.isFile() && s.mtimeMs > newest) newest = s.mtimeMs;
28375
- } catch {
28376
- }
28377
- }
28378
- if (newest === 0) return null;
28379
- return new Date(newest).toISOString();
28380
- }
28381
- async function readExtraPaths(root, env) {
28382
- const out = [];
28383
- const fromEnv = env.SANCTUARY_AGENTS_EXTRA_PATHS;
28384
- if (fromEnv && fromEnv.length > 0) {
28385
- for (const part of fromEnv.split(":")) {
28386
- const trimmed = part.trim();
28387
- if (trimmed.length > 0) out.push(path.resolve(trimmed));
28388
- }
28389
- }
28390
- try {
28391
- const raw = await promises.readFile(path.join(root, EXTRAS_FILE_NAME), "utf-8");
28392
- const parsed = JSON.parse(raw);
28393
- if (Array.isArray(parsed)) {
28394
- for (const p of parsed) {
28395
- if (typeof p === "string" && p.trim().length > 0) out.push(path.resolve(p));
28396
- }
28397
- }
28398
- } catch {
28399
- }
28400
- return Array.from(new Set(out));
28401
- }
28402
- async function describeTenant(name, storagePath, home) {
28403
- const exists = await dirExists(storagePath);
28404
- if (!exists) return null;
28405
- const { initialized, hasProfile, passphraseStatus } = await isTenantDir(storagePath);
28406
- if (!initialized && !hasProfile && passphraseStatus === "not-initialized") {
28407
- return null;
28408
- }
28409
- const last_activity = await newestAuditMtime(storagePath);
28410
- const runtime = await readTenantRuntime(storagePath);
28411
- return {
28412
- name,
28413
- storage_path: storagePath,
28414
- exists: true,
28415
- initialized,
28416
- has_cocoon_profile: hasProfile,
28417
- keychain_service: keychainServiceFor(storagePath, home),
28418
- passphrase_status: passphraseStatus,
28419
- last_activity,
28420
- runtime
28421
- };
28422
- }
28423
- async function discoverTenants(options = {}) {
28424
- const home = options.home ?? os.homedir();
28425
- const env = options.env ?? process.env;
28426
- const root = options.root ?? path.join(home, DEFAULT_STORAGE_DIR);
28427
- const tenants = [];
28428
- const rootTenant = await describeTenant("default", root, home);
28429
- if (rootTenant) tenants.push(rootTenant);
28430
- let children = [];
28431
- try {
28432
- children = await promises.readdir(root);
28433
- } catch {
28434
- }
28435
- for (const child of children) {
28436
- const childPath = path.join(root, child);
28437
- if (child.startsWith(".")) continue;
28438
- if (child === "state" || child === "backup" || child === "config") continue;
28439
- const s = await promises.stat(childPath).catch(() => null);
28440
- if (!s || !s.isDirectory()) continue;
28441
- const desc = await describeTenant(child, childPath, home);
28442
- if (desc) tenants.push(desc);
28443
- }
28444
- const extras = await readExtraPaths(root, env);
28445
- for (const extra of extras) {
28446
- if (tenants.some((t) => t.storage_path === extra)) continue;
28447
- const desc = await describeTenant(path.basename(extra), extra, home);
28448
- if (desc) tenants.push(desc);
28449
- }
28450
- tenants.sort((a, b) => {
28451
- if (a.name === "default") return -1;
28452
- if (b.name === "default") return 1;
28453
- return a.name.localeCompare(b.name);
28454
- });
28455
- return tenants;
28456
- }
28457
- async function findTenant(name, options = {}) {
28458
- const tenants = await discoverTenants(options);
28459
- return tenants.find((t) => t.name === name) ?? null;
28460
- }
28461
- var EXTRAS_FILE_NAME;
28462
- var init_discovery = __esm({
28463
- "src/cli/agents/discovery.ts"() {
28464
- init_paths();
28465
- init_passphrase();
28466
- init_runtime();
28467
- EXTRAS_FILE_NAME = "agents-extra.json";
28498
+ init_discovery();
28468
28499
  }
28469
28500
  });
28470
28501
  async function probeTenantDashboard(tenant, options = {}) {