@sanctuary-framework/mcp-server 0.10.0 → 0.10.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 +141 -26
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +141 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +70 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +70 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -2040,6 +2040,7 @@ var init_audit_log = __esm({
|
|
|
2040
2040
|
maxTotalSizeBytes;
|
|
2041
2041
|
maxEntries;
|
|
2042
2042
|
rotationInFlight = false;
|
|
2043
|
+
pendingWrites = /* @__PURE__ */ new Set();
|
|
2043
2044
|
constructor(storage, masterKey, config) {
|
|
2044
2045
|
this.storage = storage;
|
|
2045
2046
|
this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
|
|
@@ -2048,6 +2049,15 @@ var init_audit_log = __esm({
|
|
|
2048
2049
|
}
|
|
2049
2050
|
/**
|
|
2050
2051
|
* Append an audit entry.
|
|
2052
|
+
*
|
|
2053
|
+
* The on-disk persist is async and tracked via `pendingWrites`. Long-lived
|
|
2054
|
+
* callers (the main MCP server) can ignore that tracking and let writes
|
|
2055
|
+
* drain naturally. Short-lived callers — the `sanctuary secrets` CLI which
|
|
2056
|
+
* `process.exit()`s immediately after returning from a broker mutation —
|
|
2057
|
+
* MUST await `flush()` before exiting, or in-flight writes get killed
|
|
2058
|
+
* with the event loop and the entry is silently lost. That was the
|
|
2059
|
+
* v0.10.0-rc.2 soak failure mode where `secrets audit` returned empty
|
|
2060
|
+
* after a clean 7-verb lifecycle.
|
|
2051
2061
|
*/
|
|
2052
2062
|
append(layer, operation, identityId, details, result = "success") {
|
|
2053
2063
|
const entry = {
|
|
@@ -2059,8 +2069,22 @@ var init_audit_log = __esm({
|
|
|
2059
2069
|
details
|
|
2060
2070
|
};
|
|
2061
2071
|
this.entries.push(entry);
|
|
2062
|
-
this.persistEntry(entry).catch(() => {
|
|
2072
|
+
const writePromise = this.persistEntry(entry).catch(() => {
|
|
2063
2073
|
});
|
|
2074
|
+
this.pendingWrites.add(writePromise);
|
|
2075
|
+
void writePromise.finally(() => this.pendingWrites.delete(writePromise));
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Wait for every in-flight `append()` persist (and its rotation pass) to
|
|
2079
|
+
* settle. Safe to call multiple times — newly-appended entries during a
|
|
2080
|
+
* flush are also awaited. Re-entrant only at the granularity of "drain
|
|
2081
|
+
* everything queued so far". Short-lived CLIs MUST call this before
|
|
2082
|
+
* `process.exit()` to keep audit writes durable.
|
|
2083
|
+
*/
|
|
2084
|
+
async flush() {
|
|
2085
|
+
while (this.pendingWrites.size > 0) {
|
|
2086
|
+
await Promise.allSettled([...this.pendingWrites]);
|
|
2087
|
+
}
|
|
2064
2088
|
}
|
|
2065
2089
|
async persistEntry(entry) {
|
|
2066
2090
|
const key = `${Date.now()}-${this.counter++}`;
|
|
@@ -2071,7 +2095,7 @@ var init_audit_log = __esm({
|
|
|
2071
2095
|
key,
|
|
2072
2096
|
stringToBytes(JSON.stringify(encrypted))
|
|
2073
2097
|
);
|
|
2074
|
-
this.maybeRotate().catch(() => {
|
|
2098
|
+
await this.maybeRotate().catch(() => {
|
|
2075
2099
|
});
|
|
2076
2100
|
}
|
|
2077
2101
|
/**
|
|
@@ -8808,6 +8832,24 @@ var init_dashboard = __esm({
|
|
|
8808
8832
|
rateLimits = /* @__PURE__ */ new Map();
|
|
8809
8833
|
/** Whether the dashboard is running in standalone mode (no MCP server) */
|
|
8810
8834
|
_standaloneMode = false;
|
|
8835
|
+
/**
|
|
8836
|
+
* v0.10.2: when set, requests from loopback addresses (127.0.0.1 / ::1)
|
|
8837
|
+
* are treated as authenticated without requiring a Bearer token or
|
|
8838
|
+
* dashboard session cookie. Only the `startStandaloneDashboard` boot
|
|
8839
|
+
* path enables this, and ONLY after the supplied passphrase successfully
|
|
8840
|
+
* decrypts at least one stored identity — proving the caller already
|
|
8841
|
+
* holds the primary secret that protects every piece of Sanctuary state.
|
|
8842
|
+
*
|
|
8843
|
+
* Rationale: the dashboard auth token is a dashboard-access credential
|
|
8844
|
+
* layered on top of the master-key unlock. Once the operator has already
|
|
8845
|
+
* presented the passphrase on the command line (terminal-side auth), a
|
|
8846
|
+
* second login prompt in the auto-opened browser just trains users to
|
|
8847
|
+
* paste secrets into web forms — the exact habit Sanctuary exists to
|
|
8848
|
+
* discourage. Remote (non-loopback) callers still require the bearer
|
|
8849
|
+
* token, so this is a localhost-only ergonomics unlock, not a network
|
|
8850
|
+
* policy change.
|
|
8851
|
+
*/
|
|
8852
|
+
_autoAuthLocalhost = false;
|
|
8811
8853
|
constructor(config) {
|
|
8812
8854
|
this.config = config;
|
|
8813
8855
|
this.authToken = config.auth_token;
|
|
@@ -8843,6 +8885,26 @@ var init_dashboard = __esm({
|
|
|
8843
8885
|
setStandaloneMode(standalone) {
|
|
8844
8886
|
this._standaloneMode = standalone;
|
|
8845
8887
|
}
|
|
8888
|
+
/**
|
|
8889
|
+
* v0.10.2: enable (or disable) the loopback auto-auth fast path. See
|
|
8890
|
+
* {@link _autoAuthLocalhost} for the rationale and threat model. Callers
|
|
8891
|
+
* should gate this on both (a) the dashboard host being a loopback
|
|
8892
|
+
* interface and (b) the master-key unlock having succeeded against
|
|
8893
|
+
* on-disk state.
|
|
8894
|
+
*/
|
|
8895
|
+
setAutoAuthLocalhost(enabled) {
|
|
8896
|
+
this._autoAuthLocalhost = enabled;
|
|
8897
|
+
}
|
|
8898
|
+
/**
|
|
8899
|
+
* v0.10.2: is this request from a loopback interface? We treat the
|
|
8900
|
+
* standard IPv4/IPv6 loopback addresses plus the IPv4-mapped IPv6 form
|
|
8901
|
+
* as loopback so LAN clients never accidentally hit the unauthenticated
|
|
8902
|
+
* fast path even on hosts where the HTTP server binds 0.0.0.0.
|
|
8903
|
+
*/
|
|
8904
|
+
isLoopbackRequest(req) {
|
|
8905
|
+
const addr = this.getRemoteAddr(req);
|
|
8906
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "localhost";
|
|
8907
|
+
}
|
|
8846
8908
|
/**
|
|
8847
8909
|
* Start the HTTP(S) server for the dashboard.
|
|
8848
8910
|
*/
|
|
@@ -8992,6 +9054,9 @@ var init_dashboard = __esm({
|
|
|
8992
9054
|
*/
|
|
8993
9055
|
checkAuth(req, url, res) {
|
|
8994
9056
|
if (!this.authToken) return true;
|
|
9057
|
+
if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
|
|
9058
|
+
return true;
|
|
9059
|
+
}
|
|
8995
9060
|
const authHeader = req.headers.authorization;
|
|
8996
9061
|
if (authHeader) {
|
|
8997
9062
|
const parts = authHeader.split(" ");
|
|
@@ -9017,6 +9082,9 @@ var init_dashboard = __esm({
|
|
|
9017
9082
|
*/
|
|
9018
9083
|
isAuthenticated(req, url) {
|
|
9019
9084
|
if (!this.authToken) return true;
|
|
9085
|
+
if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
|
|
9086
|
+
return true;
|
|
9087
|
+
}
|
|
9020
9088
|
const authHeader = req.headers.authorization;
|
|
9021
9089
|
if (authHeader) {
|
|
9022
9090
|
const parts = authHeader.split(" ");
|
|
@@ -24131,11 +24199,13 @@ var init_runtime = __esm({
|
|
|
24131
24199
|
var cli_exports = {};
|
|
24132
24200
|
__export(cli_exports, {
|
|
24133
24201
|
COCOON_GOVERNOR_DEFAULTS: () => COCOON_GOVERNOR_DEFAULTS,
|
|
24202
|
+
PORT_FALLBACK_ATTEMPTS: () => PORT_FALLBACK_ATTEMPTS,
|
|
24134
24203
|
formatWrapSuccess: () => formatWrapSuccess,
|
|
24135
24204
|
parseCocoonArgs: () => parseCocoonArgs,
|
|
24136
24205
|
parseWrapArgs: () => parseWrapArgs,
|
|
24137
24206
|
runCocoon: () => runCocoon,
|
|
24138
|
-
runWrap: () => runWrap
|
|
24207
|
+
runWrap: () => runWrap,
|
|
24208
|
+
startDashboardWithFallback: () => startDashboardWithFallback
|
|
24139
24209
|
});
|
|
24140
24210
|
async function runWrap(options, deps = {}) {
|
|
24141
24211
|
if (options.unwrap) {
|
|
@@ -24379,7 +24449,8 @@ async function runCocoon(options) {
|
|
|
24379
24449
|
}
|
|
24380
24450
|
async function startDashboardWithFallback(startFn, preferredPort, authToken, serverVersion) {
|
|
24381
24451
|
let lastErr;
|
|
24382
|
-
for (let
|
|
24452
|
+
for (let i = 0; i < PORT_FALLBACK_ATTEMPTS; i++) {
|
|
24453
|
+
const port = preferredPort + i;
|
|
24383
24454
|
try {
|
|
24384
24455
|
const handle = await startFn({
|
|
24385
24456
|
port,
|
|
@@ -24398,8 +24469,9 @@ async function startDashboardWithFallback(startFn, preferredPort, authToken, ser
|
|
|
24398
24469
|
if (!isAddressInUse(err)) throw err;
|
|
24399
24470
|
}
|
|
24400
24471
|
}
|
|
24472
|
+
const lastPort = preferredPort + PORT_FALLBACK_ATTEMPTS - 1;
|
|
24401
24473
|
throw new Error(
|
|
24402
|
-
`No free dashboard port in
|
|
24474
|
+
`No free dashboard port in the ${PORT_FALLBACK_ATTEMPTS} ports starting at ${preferredPort} (tried ${preferredPort}-${lastPort}): ${lastErr?.message ?? "unknown"}`
|
|
24403
24475
|
);
|
|
24404
24476
|
}
|
|
24405
24477
|
function isAddressInUse(err) {
|
|
@@ -24647,7 +24719,7 @@ function printWrapHelp() {
|
|
|
24647
24719
|
5. Every tool call is logged, scanned, and tier-gated
|
|
24648
24720
|
`);
|
|
24649
24721
|
}
|
|
24650
|
-
var COCOON_GOVERNOR_DEFAULTS,
|
|
24722
|
+
var COCOON_GOVERNOR_DEFAULTS, PORT_FALLBACK_ATTEMPTS, parseCocoonArgs;
|
|
24651
24723
|
var init_cli = __esm({
|
|
24652
24724
|
"src/cocoon/cli.ts"() {
|
|
24653
24725
|
init_config_reader();
|
|
@@ -24661,7 +24733,7 @@ var init_cli = __esm({
|
|
|
24661
24733
|
rate_limit_per_tool: 20,
|
|
24662
24734
|
lifetime_limit: 1e3
|
|
24663
24735
|
};
|
|
24664
|
-
|
|
24736
|
+
PORT_FALLBACK_ATTEMPTS = 20;
|
|
24665
24737
|
parseCocoonArgs = parseWrapArgs;
|
|
24666
24738
|
}
|
|
24667
24739
|
});
|
|
@@ -26165,6 +26237,7 @@ async function openBroker(opts = {}) {
|
|
|
26165
26237
|
return {
|
|
26166
26238
|
broker,
|
|
26167
26239
|
close: async () => {
|
|
26240
|
+
await auditLog.flush();
|
|
26168
26241
|
}
|
|
26169
26242
|
};
|
|
26170
26243
|
}
|
|
@@ -27189,12 +27262,12 @@ function optionalNumber(args, key) {
|
|
|
27189
27262
|
const v = args[key];
|
|
27190
27263
|
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
27191
27264
|
}
|
|
27192
|
-
var
|
|
27265
|
+
var PKG_VERSION3;
|
|
27193
27266
|
var init_broker_server = __esm({
|
|
27194
27267
|
"src/mcp/broker-server.ts"() {
|
|
27268
|
+
init_config();
|
|
27195
27269
|
init_token_issuer();
|
|
27196
|
-
|
|
27197
|
-
({ version: PKG_VERSION3 } = require4("../../package.json"));
|
|
27270
|
+
PKG_VERSION3 = SANCTUARY_VERSION;
|
|
27198
27271
|
}
|
|
27199
27272
|
});
|
|
27200
27273
|
|
|
@@ -27358,7 +27431,28 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27358
27431
|
await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
27359
27432
|
const storage = new FilesystemStorage(`${config.storage_path}/state`);
|
|
27360
27433
|
let masterKey;
|
|
27361
|
-
|
|
27434
|
+
let passphrase = options.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
|
|
27435
|
+
let passphraseSource = null;
|
|
27436
|
+
if (passphrase) {
|
|
27437
|
+
passphraseSource = options.passphrase !== void 0 ? "option" : "env";
|
|
27438
|
+
} else {
|
|
27439
|
+
try {
|
|
27440
|
+
const stored = await readStoredPassphrase({
|
|
27441
|
+
storagePath: config.storage_path
|
|
27442
|
+
});
|
|
27443
|
+
if (stored) {
|
|
27444
|
+
passphrase = stored.value;
|
|
27445
|
+
passphraseSource = stored.source === "keychain" ? "keychain" : "fallback-file";
|
|
27446
|
+
console.error(
|
|
27447
|
+
`Passphrase: loaded from ${stored.location} (service ${keychainServiceFor(config.storage_path, os.homedir())})`
|
|
27448
|
+
);
|
|
27449
|
+
}
|
|
27450
|
+
} catch (err) {
|
|
27451
|
+
if (err instanceof PassphraseUnreadableError) {
|
|
27452
|
+
throw err;
|
|
27453
|
+
}
|
|
27454
|
+
}
|
|
27455
|
+
}
|
|
27362
27456
|
if (passphrase) {
|
|
27363
27457
|
let existingParams;
|
|
27364
27458
|
try {
|
|
@@ -27371,6 +27465,14 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27371
27465
|
}
|
|
27372
27466
|
const result = await deriveMasterKey(passphrase, existingParams);
|
|
27373
27467
|
masterKey = result.key;
|
|
27468
|
+
if (!existingParams) {
|
|
27469
|
+
const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
27470
|
+
await storage.write(
|
|
27471
|
+
"_meta",
|
|
27472
|
+
"key-params",
|
|
27473
|
+
stringToBytes2(JSON.stringify(result.params))
|
|
27474
|
+
);
|
|
27475
|
+
}
|
|
27374
27476
|
} else {
|
|
27375
27477
|
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
27376
27478
|
const { stringToBytes: stringToBytes2, bytesToString: bytesToString2, fromBase64url: fromBase64url2, constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
@@ -27469,6 +27571,10 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27469
27571
|
profileStore
|
|
27470
27572
|
});
|
|
27471
27573
|
dashboard.setStandaloneMode(true);
|
|
27574
|
+
const hostIsLoopback = dashboardHost === "127.0.0.1" || dashboardHost === "::1" || dashboardHost === "localhost";
|
|
27575
|
+
if (hostIsLoopback && loadResult.loaded > 0) {
|
|
27576
|
+
dashboard.setAutoAuthLocalhost(true);
|
|
27577
|
+
}
|
|
27472
27578
|
await dashboard.start();
|
|
27473
27579
|
await writeTenantRuntime(config.storage_path, {
|
|
27474
27580
|
version: SANCTUARY_VERSION,
|
|
@@ -27494,21 +27600,29 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27494
27600
|
console.error(`Identities loaded: ${loadResult.loaded}`);
|
|
27495
27601
|
console.error(`Listening: http://${dashboardHost}:${dashboardPort}`);
|
|
27496
27602
|
if (loadResult.total > 0 && loadResult.loaded === 0) {
|
|
27603
|
+
const service = keychainServiceFor(config.storage_path, os.homedir());
|
|
27604
|
+
const sourceLabel = passphraseSource === "option" ? "--passphrase option" : passphraseSource === "env" ? "SANCTUARY_PASSPHRASE env var" : passphraseSource === "keychain" ? `macOS Keychain (service ${service})` : passphraseSource === "fallback-file" ? "encrypted fallback file" : "recovery key";
|
|
27497
27605
|
console.error(
|
|
27498
27606
|
`
|
|
27499
|
-
\
|
|
27500
|
-
|
|
27501
|
-
|
|
27502
|
-
|
|
27503
|
-
|
|
27504
|
-
|
|
27505
|
-
|
|
27506
|
-
\
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
\
|
|
27510
|
-
\
|
|
27511
|
-
\
|
|
27607
|
+
\u26A0 WARNING: Encrypted identities found but NONE loaded
|
|
27608
|
+
${loadResult.total} encrypted identity file(s) in ${config.storage_path}/state/_identities/
|
|
27609
|
+
0 could be decrypted with the master key derived from the ${sourceLabel}.
|
|
27610
|
+
|
|
27611
|
+
The dashboard will show empty panels. Since v0.10.0 each wrapped
|
|
27612
|
+
tenant has its OWN passphrase, stored under a per-tenant Keychain
|
|
27613
|
+
service name. A single SANCTUARY_PASSPHRASE unlocks at most one
|
|
27614
|
+
tenant \u2014 it is not a global master credential.
|
|
27615
|
+
|
|
27616
|
+
To diagnose:
|
|
27617
|
+
\u2022 List tenants on this host: sanctuary agents
|
|
27618
|
+
\u2022 Multi-tenant overview (no decrypt): sanctuary dashboard --multi
|
|
27619
|
+
\u2022 Point at a specific tenant: SANCTUARY_STORAGE_PATH=<path> sanctuary dashboard
|
|
27620
|
+
\u2022 Inspect this tenant's Keychain: security find-generic-password -s '${service}' -w
|
|
27621
|
+
|
|
27622
|
+
If this tenant's passphrase lives only in a different Keychain item
|
|
27623
|
+
or on another machine, restore it before this dashboard can read
|
|
27624
|
+
any state. Sanctuary will never auto-regenerate \u2014 that would
|
|
27625
|
+
permanently destroy the data encrypted under the prior key.
|
|
27512
27626
|
`
|
|
27513
27627
|
);
|
|
27514
27628
|
} else if (loadResult.failed > 0) {
|
|
@@ -27538,6 +27652,7 @@ var init_dashboard_standalone = __esm({
|
|
|
27538
27652
|
init_tools();
|
|
27539
27653
|
init_sovereignty_profile();
|
|
27540
27654
|
init_runtime();
|
|
27655
|
+
init_passphrase();
|
|
27541
27656
|
}
|
|
27542
27657
|
});
|
|
27543
27658
|
|
|
@@ -27613,8 +27728,8 @@ async function checkForUpdate(currentVersion) {
|
|
|
27613
27728
|
} catch {
|
|
27614
27729
|
}
|
|
27615
27730
|
}
|
|
27616
|
-
var
|
|
27617
|
-
var { version: PKG_VERSION4 } =
|
|
27731
|
+
var require4 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
27732
|
+
var { version: PKG_VERSION4 } = require4("../package.json");
|
|
27618
27733
|
async function main() {
|
|
27619
27734
|
const args = process.argv.slice(2);
|
|
27620
27735
|
let passphrase = process.env.SANCTUARY_PASSPHRASE;
|