@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.js
CHANGED
|
@@ -2037,6 +2037,7 @@ var init_audit_log = __esm({
|
|
|
2037
2037
|
maxTotalSizeBytes;
|
|
2038
2038
|
maxEntries;
|
|
2039
2039
|
rotationInFlight = false;
|
|
2040
|
+
pendingWrites = /* @__PURE__ */ new Set();
|
|
2040
2041
|
constructor(storage, masterKey, config) {
|
|
2041
2042
|
this.storage = storage;
|
|
2042
2043
|
this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
|
|
@@ -2045,6 +2046,15 @@ var init_audit_log = __esm({
|
|
|
2045
2046
|
}
|
|
2046
2047
|
/**
|
|
2047
2048
|
* Append an audit entry.
|
|
2049
|
+
*
|
|
2050
|
+
* The on-disk persist is async and tracked via `pendingWrites`. Long-lived
|
|
2051
|
+
* callers (the main MCP server) can ignore that tracking and let writes
|
|
2052
|
+
* drain naturally. Short-lived callers — the `sanctuary secrets` CLI which
|
|
2053
|
+
* `process.exit()`s immediately after returning from a broker mutation —
|
|
2054
|
+
* MUST await `flush()` before exiting, or in-flight writes get killed
|
|
2055
|
+
* with the event loop and the entry is silently lost. That was the
|
|
2056
|
+
* v0.10.0-rc.2 soak failure mode where `secrets audit` returned empty
|
|
2057
|
+
* after a clean 7-verb lifecycle.
|
|
2048
2058
|
*/
|
|
2049
2059
|
append(layer, operation, identityId, details, result = "success") {
|
|
2050
2060
|
const entry = {
|
|
@@ -2056,8 +2066,22 @@ var init_audit_log = __esm({
|
|
|
2056
2066
|
details
|
|
2057
2067
|
};
|
|
2058
2068
|
this.entries.push(entry);
|
|
2059
|
-
this.persistEntry(entry).catch(() => {
|
|
2069
|
+
const writePromise = this.persistEntry(entry).catch(() => {
|
|
2060
2070
|
});
|
|
2071
|
+
this.pendingWrites.add(writePromise);
|
|
2072
|
+
void writePromise.finally(() => this.pendingWrites.delete(writePromise));
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Wait for every in-flight `append()` persist (and its rotation pass) to
|
|
2076
|
+
* settle. Safe to call multiple times — newly-appended entries during a
|
|
2077
|
+
* flush are also awaited. Re-entrant only at the granularity of "drain
|
|
2078
|
+
* everything queued so far". Short-lived CLIs MUST call this before
|
|
2079
|
+
* `process.exit()` to keep audit writes durable.
|
|
2080
|
+
*/
|
|
2081
|
+
async flush() {
|
|
2082
|
+
while (this.pendingWrites.size > 0) {
|
|
2083
|
+
await Promise.allSettled([...this.pendingWrites]);
|
|
2084
|
+
}
|
|
2061
2085
|
}
|
|
2062
2086
|
async persistEntry(entry) {
|
|
2063
2087
|
const key = `${Date.now()}-${this.counter++}`;
|
|
@@ -2068,7 +2092,7 @@ var init_audit_log = __esm({
|
|
|
2068
2092
|
key,
|
|
2069
2093
|
stringToBytes(JSON.stringify(encrypted))
|
|
2070
2094
|
);
|
|
2071
|
-
this.maybeRotate().catch(() => {
|
|
2095
|
+
await this.maybeRotate().catch(() => {
|
|
2072
2096
|
});
|
|
2073
2097
|
}
|
|
2074
2098
|
/**
|
|
@@ -8805,6 +8829,24 @@ var init_dashboard = __esm({
|
|
|
8805
8829
|
rateLimits = /* @__PURE__ */ new Map();
|
|
8806
8830
|
/** Whether the dashboard is running in standalone mode (no MCP server) */
|
|
8807
8831
|
_standaloneMode = false;
|
|
8832
|
+
/**
|
|
8833
|
+
* v0.10.2: when set, requests from loopback addresses (127.0.0.1 / ::1)
|
|
8834
|
+
* are treated as authenticated without requiring a Bearer token or
|
|
8835
|
+
* dashboard session cookie. Only the `startStandaloneDashboard` boot
|
|
8836
|
+
* path enables this, and ONLY after the supplied passphrase successfully
|
|
8837
|
+
* decrypts at least one stored identity — proving the caller already
|
|
8838
|
+
* holds the primary secret that protects every piece of Sanctuary state.
|
|
8839
|
+
*
|
|
8840
|
+
* Rationale: the dashboard auth token is a dashboard-access credential
|
|
8841
|
+
* layered on top of the master-key unlock. Once the operator has already
|
|
8842
|
+
* presented the passphrase on the command line (terminal-side auth), a
|
|
8843
|
+
* second login prompt in the auto-opened browser just trains users to
|
|
8844
|
+
* paste secrets into web forms — the exact habit Sanctuary exists to
|
|
8845
|
+
* discourage. Remote (non-loopback) callers still require the bearer
|
|
8846
|
+
* token, so this is a localhost-only ergonomics unlock, not a network
|
|
8847
|
+
* policy change.
|
|
8848
|
+
*/
|
|
8849
|
+
_autoAuthLocalhost = false;
|
|
8808
8850
|
constructor(config) {
|
|
8809
8851
|
this.config = config;
|
|
8810
8852
|
this.authToken = config.auth_token;
|
|
@@ -8840,6 +8882,26 @@ var init_dashboard = __esm({
|
|
|
8840
8882
|
setStandaloneMode(standalone) {
|
|
8841
8883
|
this._standaloneMode = standalone;
|
|
8842
8884
|
}
|
|
8885
|
+
/**
|
|
8886
|
+
* v0.10.2: enable (or disable) the loopback auto-auth fast path. See
|
|
8887
|
+
* {@link _autoAuthLocalhost} for the rationale and threat model. Callers
|
|
8888
|
+
* should gate this on both (a) the dashboard host being a loopback
|
|
8889
|
+
* interface and (b) the master-key unlock having succeeded against
|
|
8890
|
+
* on-disk state.
|
|
8891
|
+
*/
|
|
8892
|
+
setAutoAuthLocalhost(enabled) {
|
|
8893
|
+
this._autoAuthLocalhost = enabled;
|
|
8894
|
+
}
|
|
8895
|
+
/**
|
|
8896
|
+
* v0.10.2: is this request from a loopback interface? We treat the
|
|
8897
|
+
* standard IPv4/IPv6 loopback addresses plus the IPv4-mapped IPv6 form
|
|
8898
|
+
* as loopback so LAN clients never accidentally hit the unauthenticated
|
|
8899
|
+
* fast path even on hosts where the HTTP server binds 0.0.0.0.
|
|
8900
|
+
*/
|
|
8901
|
+
isLoopbackRequest(req) {
|
|
8902
|
+
const addr = this.getRemoteAddr(req);
|
|
8903
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "localhost";
|
|
8904
|
+
}
|
|
8843
8905
|
/**
|
|
8844
8906
|
* Start the HTTP(S) server for the dashboard.
|
|
8845
8907
|
*/
|
|
@@ -8989,6 +9051,9 @@ var init_dashboard = __esm({
|
|
|
8989
9051
|
*/
|
|
8990
9052
|
checkAuth(req, url, res) {
|
|
8991
9053
|
if (!this.authToken) return true;
|
|
9054
|
+
if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
|
|
9055
|
+
return true;
|
|
9056
|
+
}
|
|
8992
9057
|
const authHeader = req.headers.authorization;
|
|
8993
9058
|
if (authHeader) {
|
|
8994
9059
|
const parts = authHeader.split(" ");
|
|
@@ -9014,6 +9079,9 @@ var init_dashboard = __esm({
|
|
|
9014
9079
|
*/
|
|
9015
9080
|
isAuthenticated(req, url) {
|
|
9016
9081
|
if (!this.authToken) return true;
|
|
9082
|
+
if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
|
|
9083
|
+
return true;
|
|
9084
|
+
}
|
|
9017
9085
|
const authHeader = req.headers.authorization;
|
|
9018
9086
|
if (authHeader) {
|
|
9019
9087
|
const parts = authHeader.split(" ");
|
|
@@ -24128,11 +24196,13 @@ var init_runtime = __esm({
|
|
|
24128
24196
|
var cli_exports = {};
|
|
24129
24197
|
__export(cli_exports, {
|
|
24130
24198
|
COCOON_GOVERNOR_DEFAULTS: () => COCOON_GOVERNOR_DEFAULTS,
|
|
24199
|
+
PORT_FALLBACK_ATTEMPTS: () => PORT_FALLBACK_ATTEMPTS,
|
|
24131
24200
|
formatWrapSuccess: () => formatWrapSuccess,
|
|
24132
24201
|
parseCocoonArgs: () => parseCocoonArgs,
|
|
24133
24202
|
parseWrapArgs: () => parseWrapArgs,
|
|
24134
24203
|
runCocoon: () => runCocoon,
|
|
24135
|
-
runWrap: () => runWrap
|
|
24204
|
+
runWrap: () => runWrap,
|
|
24205
|
+
startDashboardWithFallback: () => startDashboardWithFallback
|
|
24136
24206
|
});
|
|
24137
24207
|
async function runWrap(options, deps = {}) {
|
|
24138
24208
|
if (options.unwrap) {
|
|
@@ -24376,7 +24446,8 @@ async function runCocoon(options) {
|
|
|
24376
24446
|
}
|
|
24377
24447
|
async function startDashboardWithFallback(startFn, preferredPort, authToken, serverVersion) {
|
|
24378
24448
|
let lastErr;
|
|
24379
|
-
for (let
|
|
24449
|
+
for (let i = 0; i < PORT_FALLBACK_ATTEMPTS; i++) {
|
|
24450
|
+
const port = preferredPort + i;
|
|
24380
24451
|
try {
|
|
24381
24452
|
const handle = await startFn({
|
|
24382
24453
|
port,
|
|
@@ -24395,8 +24466,9 @@ async function startDashboardWithFallback(startFn, preferredPort, authToken, ser
|
|
|
24395
24466
|
if (!isAddressInUse(err)) throw err;
|
|
24396
24467
|
}
|
|
24397
24468
|
}
|
|
24469
|
+
const lastPort = preferredPort + PORT_FALLBACK_ATTEMPTS - 1;
|
|
24398
24470
|
throw new Error(
|
|
24399
|
-
`No free dashboard port in
|
|
24471
|
+
`No free dashboard port in the ${PORT_FALLBACK_ATTEMPTS} ports starting at ${preferredPort} (tried ${preferredPort}-${lastPort}): ${lastErr?.message ?? "unknown"}`
|
|
24400
24472
|
);
|
|
24401
24473
|
}
|
|
24402
24474
|
function isAddressInUse(err) {
|
|
@@ -24644,7 +24716,7 @@ function printWrapHelp() {
|
|
|
24644
24716
|
5. Every tool call is logged, scanned, and tier-gated
|
|
24645
24717
|
`);
|
|
24646
24718
|
}
|
|
24647
|
-
var COCOON_GOVERNOR_DEFAULTS,
|
|
24719
|
+
var COCOON_GOVERNOR_DEFAULTS, PORT_FALLBACK_ATTEMPTS, parseCocoonArgs;
|
|
24648
24720
|
var init_cli = __esm({
|
|
24649
24721
|
"src/cocoon/cli.ts"() {
|
|
24650
24722
|
init_config_reader();
|
|
@@ -24658,7 +24730,7 @@ var init_cli = __esm({
|
|
|
24658
24730
|
rate_limit_per_tool: 20,
|
|
24659
24731
|
lifetime_limit: 1e3
|
|
24660
24732
|
};
|
|
24661
|
-
|
|
24733
|
+
PORT_FALLBACK_ATTEMPTS = 20;
|
|
24662
24734
|
parseCocoonArgs = parseWrapArgs;
|
|
24663
24735
|
}
|
|
24664
24736
|
});
|
|
@@ -26162,6 +26234,7 @@ async function openBroker(opts = {}) {
|
|
|
26162
26234
|
return {
|
|
26163
26235
|
broker,
|
|
26164
26236
|
close: async () => {
|
|
26237
|
+
await auditLog.flush();
|
|
26165
26238
|
}
|
|
26166
26239
|
};
|
|
26167
26240
|
}
|
|
@@ -27186,12 +27259,12 @@ function optionalNumber(args, key) {
|
|
|
27186
27259
|
const v = args[key];
|
|
27187
27260
|
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
27188
27261
|
}
|
|
27189
|
-
var
|
|
27262
|
+
var PKG_VERSION3;
|
|
27190
27263
|
var init_broker_server = __esm({
|
|
27191
27264
|
"src/mcp/broker-server.ts"() {
|
|
27265
|
+
init_config();
|
|
27192
27266
|
init_token_issuer();
|
|
27193
|
-
|
|
27194
|
-
({ version: PKG_VERSION3 } = require4("../../package.json"));
|
|
27267
|
+
PKG_VERSION3 = SANCTUARY_VERSION;
|
|
27195
27268
|
}
|
|
27196
27269
|
});
|
|
27197
27270
|
|
|
@@ -27355,7 +27428,28 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27355
27428
|
await mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
27356
27429
|
const storage = new FilesystemStorage(`${config.storage_path}/state`);
|
|
27357
27430
|
let masterKey;
|
|
27358
|
-
|
|
27431
|
+
let passphrase = options.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
|
|
27432
|
+
let passphraseSource = null;
|
|
27433
|
+
if (passphrase) {
|
|
27434
|
+
passphraseSource = options.passphrase !== void 0 ? "option" : "env";
|
|
27435
|
+
} else {
|
|
27436
|
+
try {
|
|
27437
|
+
const stored = await readStoredPassphrase({
|
|
27438
|
+
storagePath: config.storage_path
|
|
27439
|
+
});
|
|
27440
|
+
if (stored) {
|
|
27441
|
+
passphrase = stored.value;
|
|
27442
|
+
passphraseSource = stored.source === "keychain" ? "keychain" : "fallback-file";
|
|
27443
|
+
console.error(
|
|
27444
|
+
`Passphrase: loaded from ${stored.location} (service ${keychainServiceFor(config.storage_path, homedir())})`
|
|
27445
|
+
);
|
|
27446
|
+
}
|
|
27447
|
+
} catch (err) {
|
|
27448
|
+
if (err instanceof PassphraseUnreadableError) {
|
|
27449
|
+
throw err;
|
|
27450
|
+
}
|
|
27451
|
+
}
|
|
27452
|
+
}
|
|
27359
27453
|
if (passphrase) {
|
|
27360
27454
|
let existingParams;
|
|
27361
27455
|
try {
|
|
@@ -27368,6 +27462,14 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27368
27462
|
}
|
|
27369
27463
|
const result = await deriveMasterKey(passphrase, existingParams);
|
|
27370
27464
|
masterKey = result.key;
|
|
27465
|
+
if (!existingParams) {
|
|
27466
|
+
const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
27467
|
+
await storage.write(
|
|
27468
|
+
"_meta",
|
|
27469
|
+
"key-params",
|
|
27470
|
+
stringToBytes2(JSON.stringify(result.params))
|
|
27471
|
+
);
|
|
27472
|
+
}
|
|
27371
27473
|
} else {
|
|
27372
27474
|
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
27373
27475
|
const { stringToBytes: stringToBytes2, bytesToString: bytesToString2, fromBase64url: fromBase64url2, constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
@@ -27466,6 +27568,10 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27466
27568
|
profileStore
|
|
27467
27569
|
});
|
|
27468
27570
|
dashboard.setStandaloneMode(true);
|
|
27571
|
+
const hostIsLoopback = dashboardHost === "127.0.0.1" || dashboardHost === "::1" || dashboardHost === "localhost";
|
|
27572
|
+
if (hostIsLoopback && loadResult.loaded > 0) {
|
|
27573
|
+
dashboard.setAutoAuthLocalhost(true);
|
|
27574
|
+
}
|
|
27469
27575
|
await dashboard.start();
|
|
27470
27576
|
await writeTenantRuntime(config.storage_path, {
|
|
27471
27577
|
version: SANCTUARY_VERSION,
|
|
@@ -27491,21 +27597,29 @@ async function startStandaloneDashboard(options = {}) {
|
|
|
27491
27597
|
console.error(`Identities loaded: ${loadResult.loaded}`);
|
|
27492
27598
|
console.error(`Listening: http://${dashboardHost}:${dashboardPort}`);
|
|
27493
27599
|
if (loadResult.total > 0 && loadResult.loaded === 0) {
|
|
27600
|
+
const service = keychainServiceFor(config.storage_path, homedir());
|
|
27601
|
+
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";
|
|
27494
27602
|
console.error(
|
|
27495
27603
|
`
|
|
27496
|
-
\
|
|
27497
|
-
|
|
27498
|
-
|
|
27499
|
-
|
|
27500
|
-
|
|
27501
|
-
|
|
27502
|
-
|
|
27503
|
-
\
|
|
27504
|
-
|
|
27505
|
-
|
|
27506
|
-
\
|
|
27507
|
-
\
|
|
27508
|
-
\
|
|
27604
|
+
\u26A0 WARNING: Encrypted identities found but NONE loaded
|
|
27605
|
+
${loadResult.total} encrypted identity file(s) in ${config.storage_path}/state/_identities/
|
|
27606
|
+
0 could be decrypted with the master key derived from the ${sourceLabel}.
|
|
27607
|
+
|
|
27608
|
+
The dashboard will show empty panels. Since v0.10.0 each wrapped
|
|
27609
|
+
tenant has its OWN passphrase, stored under a per-tenant Keychain
|
|
27610
|
+
service name. A single SANCTUARY_PASSPHRASE unlocks at most one
|
|
27611
|
+
tenant \u2014 it is not a global master credential.
|
|
27612
|
+
|
|
27613
|
+
To diagnose:
|
|
27614
|
+
\u2022 List tenants on this host: sanctuary agents
|
|
27615
|
+
\u2022 Multi-tenant overview (no decrypt): sanctuary dashboard --multi
|
|
27616
|
+
\u2022 Point at a specific tenant: SANCTUARY_STORAGE_PATH=<path> sanctuary dashboard
|
|
27617
|
+
\u2022 Inspect this tenant's Keychain: security find-generic-password -s '${service}' -w
|
|
27618
|
+
|
|
27619
|
+
If this tenant's passphrase lives only in a different Keychain item
|
|
27620
|
+
or on another machine, restore it before this dashboard can read
|
|
27621
|
+
any state. Sanctuary will never auto-regenerate \u2014 that would
|
|
27622
|
+
permanently destroy the data encrypted under the prior key.
|
|
27509
27623
|
`
|
|
27510
27624
|
);
|
|
27511
27625
|
} else if (loadResult.failed > 0) {
|
|
@@ -27535,6 +27649,7 @@ var init_dashboard_standalone = __esm({
|
|
|
27535
27649
|
init_tools();
|
|
27536
27650
|
init_sovereignty_profile();
|
|
27537
27651
|
init_runtime();
|
|
27652
|
+
init_passphrase();
|
|
27538
27653
|
}
|
|
27539
27654
|
});
|
|
27540
27655
|
|
|
@@ -27610,8 +27725,8 @@ async function checkForUpdate(currentVersion) {
|
|
|
27610
27725
|
} catch {
|
|
27611
27726
|
}
|
|
27612
27727
|
}
|
|
27613
|
-
var
|
|
27614
|
-
var { version: PKG_VERSION4 } =
|
|
27728
|
+
var require4 = createRequire(import.meta.url);
|
|
27729
|
+
var { version: PKG_VERSION4 } = require4("../package.json");
|
|
27615
27730
|
async function main() {
|
|
27616
27731
|
const args = process.argv.slice(2);
|
|
27617
27732
|
let passphrase = process.env.SANCTUARY_PASSPHRASE;
|