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