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