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