@sanctuary-framework/mcp-server 0.5.11 → 0.5.13

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 CHANGED
@@ -560,6 +560,18 @@ var init_hashing = __esm({
560
560
  init_encoding();
561
561
  }
562
562
  });
563
+
564
+ // src/core/identity.ts
565
+ var identity_exports = {};
566
+ __export(identity_exports, {
567
+ createIdentity: () => createIdentity,
568
+ generateIdentityId: () => generateIdentityId,
569
+ generateKeypair: () => generateKeypair,
570
+ publicKeyToDid: () => publicKeyToDid,
571
+ rotateKeys: () => rotateKeys,
572
+ sign: () => sign,
573
+ verify: () => verify
574
+ });
563
575
  function generateKeypair() {
564
576
  const privateKey = randomBytes(32);
565
577
  const publicKey = ed25519.ed25519.getPublicKey(privateKey);
@@ -1361,9 +1373,13 @@ var init_tools = __esm({
1361
1373
  get encryptionKey() {
1362
1374
  return derivePurposeKey(this.masterKey, "identity-encryption");
1363
1375
  }
1364
- /** Load identities from storage on startup */
1376
+ /** Load identities from storage on startup.
1377
+ * Returns { total: number of encrypted files found, loaded: number successfully decrypted }.
1378
+ * A mismatch (total > 0, loaded === 0) indicates a wrong master key / missing passphrase.
1379
+ */
1365
1380
  async load() {
1366
1381
  const entries = await this.storage.list("_identities");
1382
+ let failed = 0;
1367
1383
  for (const entry of entries) {
1368
1384
  const raw = await this.storage.read("_identities", entry.key);
1369
1385
  if (!raw) continue;
@@ -1376,8 +1392,10 @@ var init_tools = __esm({
1376
1392
  this.primaryIdentityId = identity.identity_id;
1377
1393
  }
1378
1394
  } catch {
1395
+ failed++;
1379
1396
  }
1380
1397
  }
1398
+ return { total: entries.length, loaded: this.identities.size, failed };
1381
1399
  }
1382
1400
  /** Save an identity to storage */
1383
1401
  async save(identity) {
@@ -1620,6 +1638,7 @@ tier1_always_approve:
1620
1638
  - reputation_import
1621
1639
  - reputation_export
1622
1640
  - bootstrap_provide_guarantee
1641
+ - reputation_publish
1623
1642
 
1624
1643
  # \u2500\u2500\u2500 Tier 2: Behavioral Anomaly Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1625
1644
  # Triggers approval when agent behavior deviates from its baseline.
@@ -1682,6 +1701,7 @@ tier3_always_allow:
1682
1701
  - bridge_commit
1683
1702
  - bridge_verify
1684
1703
  - bridge_attest
1704
+ - dashboard_open
1685
1705
 
1686
1706
  # \u2500\u2500\u2500 Approval Channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1687
1707
  # How Sanctuary reaches you when approval is needed.
@@ -1734,7 +1754,9 @@ var init_loader = __esm({
1734
1754
  "reputation_import",
1735
1755
  "reputation_export",
1736
1756
  "bootstrap_provide_guarantee",
1737
- "decommission_certificate"
1757
+ "decommission_certificate",
1758
+ "reputation_publish"
1759
+ // SEC-039: Explicit Tier 1 — sends data to external API
1738
1760
  ],
1739
1761
  tier2_anomaly: DEFAULT_TIER2,
1740
1762
  tier3_always_allow: [
@@ -1786,7 +1808,9 @@ var init_loader = __esm({
1786
1808
  "shr_gateway_export",
1787
1809
  "bridge_commit",
1788
1810
  "bridge_verify",
1789
- "bridge_attest"
1811
+ "bridge_attest",
1812
+ "dashboard_open"
1813
+ // SEC-039: Explicit Tier 3 — only generates a URL
1790
1814
  ],
1791
1815
  approval_channel: DEFAULT_CHANNEL
1792
1816
  };
@@ -3362,7 +3386,9 @@ function generateDashboardHTML(options) {
3362
3386
 
3363
3387
  <script>
3364
3388
  // Constants
3365
- const AUTH_TOKEN = '${options.authToken || ""}' || sessionStorage.getItem('authToken') || '';
3389
+ // SEC-038: Do NOT embed the long-lived auth token in page source.
3390
+ // Use only the session token stored in sessionStorage by the login flow.
3391
+ const AUTH_TOKEN = sessionStorage.getItem('authToken') || '';
3366
3392
  const TIMEOUT_SECONDS = ${options.timeoutSeconds};
3367
3393
  const API_BASE = '';
3368
3394
 
@@ -4924,7 +4950,7 @@ async function startStandaloneDashboard(options = {}) {
4924
4950
  // Default to auto-open in standalone mode
4925
4951
  });
4926
4952
  const identityManager = new IdentityManager(storage, masterKey);
4927
- await identityManager.load();
4953
+ const loadResult = await identityManager.load();
4928
4954
  const shrOpts = { config, identityManager, masterKey };
4929
4955
  const handshakeResults = /* @__PURE__ */ new Map();
4930
4956
  dashboard.setDependencies({
@@ -4940,9 +4966,31 @@ async function startStandaloneDashboard(options = {}) {
4940
4966
  await dashboard.start();
4941
4967
  console.error(`Sanctuary Dashboard v${SANCTUARY_VERSION} (standalone mode)`);
4942
4968
  console.error(`Storage: ${config.storage_path}`);
4943
- const identityCount = identityManager.list().length;
4944
- console.error(`Identities loaded: ${identityCount}`);
4969
+ console.error(`Identities loaded: ${loadResult.loaded}`);
4945
4970
  console.error(`Listening: http://${dashboardHost}:${dashboardPort}`);
4971
+ if (loadResult.total > 0 && loadResult.loaded === 0) {
4972
+ console.error(
4973
+ `
4974
+ \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
4975
+ \u2551 \u26A0 WARNING: Encrypted identities found but NONE loaded \u2551
4976
+ \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
4977
+ \u2551 ${loadResult.total} encrypted identity file(s) found on disk \u2551
4978
+ \u2551 0 could be decrypted with the current master key \u2551
4979
+ \u2551 \u2551
4980
+ \u2551 This usually means SANCTUARY_PASSPHRASE is missing or \u2551
4981
+ \u2551 incorrect. The dashboard will show empty panels. \u2551
4982
+ \u2551 \u2551
4983
+ \u2551 To fix: restart with the correct SANCTUARY_PASSPHRASE: \u2551
4984
+ \u2551 SANCTUARY_PASSPHRASE=<your-passphrase> npx \\ \u2551
4985
+ \u2551 @sanctuary-framework/mcp-server dashboard \u2551
4986
+ \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
4987
+ `
4988
+ );
4989
+ } else if (loadResult.failed > 0) {
4990
+ console.error(
4991
+ `Warning: ${loadResult.failed} of ${loadResult.total} identity files could not be decrypted (possibly corrupted).`
4992
+ );
4993
+ }
4946
4994
  const saveBaseline = () => {
4947
4995
  baseline.save().catch(() => {
4948
4996
  });
@@ -7151,6 +7199,24 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
7151
7199
  }
7152
7200
  const publishType = args.type;
7153
7201
  const veracoreUrl = args.verascore_url || "https://verascore.ai";
7202
+ const ALLOWED_VERASCORE_HOSTS = ["verascore.ai", "www.verascore.ai", "api.verascore.ai"];
7203
+ try {
7204
+ const parsed = new URL(veracoreUrl);
7205
+ if (parsed.protocol !== "https:") {
7206
+ return toolResult({
7207
+ error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
7208
+ });
7209
+ }
7210
+ if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
7211
+ return toolResult({
7212
+ error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
7213
+ });
7214
+ }
7215
+ } catch {
7216
+ return toolResult({
7217
+ error: `verascore_url is not a valid URL: ${veracoreUrl}`
7218
+ });
7219
+ }
7154
7220
  const agentId = args.verascore_agent_id || identity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
7155
7221
  let publishData;
7156
7222
  if (args.data) {
@@ -7180,24 +7246,21 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
7180
7246
  return toolResult({ error: `Unknown publish type: ${publishType}` });
7181
7247
  }
7182
7248
  }
7183
- const { sign: sign2, createPrivateKey } = await import('crypto');
7184
- const payloadBytes = Buffer.from(JSON.stringify(publishData), "utf-8");
7249
+ const { sign: identitySign } = await Promise.resolve().then(() => (init_identity(), identity_exports));
7250
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
7185
7251
  let signatureB64;
7186
7252
  try {
7187
- const signingKey = derivePurposeKey(masterKey, "verascore-publish");
7188
- const privateKey = createPrivateKey({
7189
- key: Buffer.concat([
7190
- Buffer.from("302e020100300506032b657004220420", "hex"),
7191
- // Ed25519 DER PKCS8 prefix
7192
- Buffer.from(signingKey.slice(0, 32))
7193
- ]),
7194
- format: "der",
7195
- type: "pkcs8"
7196
- });
7197
- const sig = sign2(null, payloadBytes, privateKey);
7198
- signatureB64 = sig.toString("base64url");
7253
+ const signingBytes = identitySign(
7254
+ payloadBytes,
7255
+ identity.encrypted_private_key,
7256
+ identityEncryptionKey
7257
+ );
7258
+ signatureB64 = toBase64url(signingBytes);
7199
7259
  } catch (signError) {
7200
- signatureB64 = toBase64url(new Uint8Array(64));
7260
+ return toolResult({
7261
+ error: "Failed to sign publish payload. Identity key may be corrupted.",
7262
+ details: signError instanceof Error ? signError.message : String(signError)
7263
+ });
7201
7264
  }
7202
7265
  const requestBody = {
7203
7266
  agentId,
@@ -13019,7 +13082,29 @@ async function createSanctuaryServer(options) {
13019
13082
  keyProtection,
13020
13083
  auditLog
13021
13084
  );
13022
- await identityManager.load();
13085
+ const loadResult = await identityManager.load();
13086
+ if (loadResult.total > 0 && loadResult.loaded === 0) {
13087
+ console.error(
13088
+ `
13089
+ \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
13090
+ \u2551 \u26A0 WARNING: Encrypted identities found but NONE loaded \u2551
13091
+ \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
13092
+ \u2551 ${loadResult.total} encrypted identity file(s) found on disk \u2551
13093
+ \u2551 0 could be decrypted with the current master key \u2551
13094
+ \u2551 \u2551
13095
+ \u2551 This usually means SANCTUARY_PASSPHRASE is missing or \u2551
13096
+ \u2551 incorrect. The server will start but with NO identity data. \u2551
13097
+ \u2551 \u2551
13098
+ \u2551 To fix: set SANCTUARY_PASSPHRASE to the passphrase used \u2551
13099
+ \u2551 when this Sanctuary instance was first configured. \u2551
13100
+ \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
13101
+ `
13102
+ );
13103
+ } else if (loadResult.failed > 0) {
13104
+ console.error(
13105
+ `Warning: ${loadResult.failed} of ${loadResult.total} identity files could not be decrypted (possibly corrupted).`
13106
+ );
13107
+ }
13023
13108
  const l2Tools = [
13024
13109
  {
13025
13110
  name: "sanctuary/exec_attest",