@sanctuary-framework/mcp-server 0.5.15 → 0.6.1

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.js CHANGED
@@ -73,6 +73,12 @@ function defaultConfig() {
73
73
  secret: "",
74
74
  callback_port: 3502,
75
75
  callback_host: "127.0.0.1"
76
+ },
77
+ verascore: {
78
+ url: "https://verascore.ai",
79
+ auto_publish_to_verascore: true,
80
+ // DELTA-04: default OFF for privacy. Enable explicitly per deployment.
81
+ auto_publish_handshakes: false
76
82
  }
77
83
  };
78
84
  }
@@ -143,6 +149,21 @@ async function loadConfig(configPath) {
143
149
  if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
144
150
  config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
145
151
  }
152
+ if (process.env.SANCTUARY_VERASCORE_URL) {
153
+ config.verascore.url = process.env.SANCTUARY_VERASCORE_URL;
154
+ }
155
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "true") {
156
+ config.verascore.auto_publish_to_verascore = true;
157
+ }
158
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "false") {
159
+ config.verascore.auto_publish_to_verascore = false;
160
+ }
161
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "true") {
162
+ config.verascore.auto_publish_handshakes = true;
163
+ }
164
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
165
+ config.verascore.auto_publish_handshakes = false;
166
+ }
146
167
  config.version = PKG_VERSION;
147
168
  validateConfig(config);
148
169
  return config;
@@ -1643,9 +1664,10 @@ tier1_always_approve:
1643
1664
  - reputation_import
1644
1665
  - reputation_export
1645
1666
  - bootstrap_provide_guarantee
1646
- - reputation_publish
1647
1667
  - sovereignty_profile_update
1648
1668
  - governor_reset
1669
+ - sanctuary_bootstrap
1670
+ - sanctuary_export_identity_bundle
1649
1671
 
1650
1672
  # \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
1651
1673
  # Triggers approval when agent behavior deviates from its baseline.
@@ -1711,6 +1733,10 @@ tier3_always_allow:
1711
1733
  - dashboard_open
1712
1734
  - sovereignty_profile_get
1713
1735
  - governor_status
1736
+ - reputation_publish
1737
+ - sanctuary_policy_status
1738
+ - sanctuary_link_to_human
1739
+ - sanctuary_sign_challenge
1714
1740
 
1715
1741
  # \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
1716
1742
  # How Sanctuary reaches you when approval is needed.
@@ -1764,12 +1790,14 @@ var init_loader = __esm({
1764
1790
  "reputation_export",
1765
1791
  "bootstrap_provide_guarantee",
1766
1792
  "decommission_certificate",
1767
- "reputation_publish",
1768
- // SEC-039: Explicit Tier 1 — sends data to external API
1769
1793
  "sovereignty_profile_update",
1770
1794
  // Changes enforcement behavior — always requires approval
1771
- "governor_reset"
1795
+ "governor_reset",
1772
1796
  // Clears all runtime governance state — always requires approval
1797
+ "sanctuary_bootstrap",
1798
+ // Creates new Ed25519 identity + publishes — always requires approval
1799
+ "sanctuary_export_identity_bundle"
1800
+ // Exports portable identity — always requires approval
1773
1801
  ],
1774
1802
  tier2_anomaly: DEFAULT_TIER2,
1775
1803
  tier3_always_allow: [
@@ -1827,7 +1855,11 @@ var init_loader = __esm({
1827
1855
  "sovereignty_profile_get",
1828
1856
  "sovereignty_profile_generate_prompt",
1829
1857
  // Agent needs its own config to generate system prompt
1830
- "governor_status"
1858
+ "governor_status",
1859
+ "reputation_publish",
1860
+ // Auto-allow: publishing sovereignty data to Verascore is routine
1861
+ "sanctuary_policy_status"
1862
+ // Read-only policy summary
1831
1863
  ],
1832
1864
  approval_channel: DEFAULT_CHANNEL
1833
1865
  };
@@ -2341,7 +2373,7 @@ function generateLoginHTML(options) {
2341
2373
  if (response.ok) {
2342
2374
  const data = await response.json();
2343
2375
  sessionStorage.setItem('authToken', token);
2344
- window.location.href = '/dashboard';
2376
+ window.location.href = '/'; // Dashboard is served at root path
2345
2377
  } else if (response.status === 401) {
2346
2378
  showError('Invalid token. Please check and try again.');
2347
2379
  } else {
@@ -5292,7 +5324,24 @@ var init_dashboard = __esm({
5292
5324
  }
5293
5325
  resolve();
5294
5326
  });
5295
- this.httpServer.on("error", reject);
5327
+ this.httpServer.on("error", (err) => {
5328
+ if (err.code === "EADDRINUSE") {
5329
+ const port = this.config.port;
5330
+ process.stderr.write(
5331
+ `
5332
+ \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
5333
+ \u2551 Port ${port} is already in use. \u2551
5334
+ \u2551 \u2551
5335
+ \u2551 Another Sanctuary Dashboard may still be running. \u2551
5336
+ \u2551 To fix: lsof -ti:${port} | xargs kill \u2551
5337
+ \u2551 Then restart the dashboard. \u2551
5338
+ \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
5339
+
5340
+ `
5341
+ );
5342
+ }
5343
+ reject(err);
5344
+ });
5296
5345
  });
5297
5346
  }
5298
5347
  /**
@@ -5572,7 +5621,7 @@ var init_dashboard = __esm({
5572
5621
  if (!this.checkAuth(req, url, res)) return;
5573
5622
  if (!this.checkRateLimit(req, res, "general")) return;
5574
5623
  try {
5575
- if (method === "GET" && url.pathname === "/") {
5624
+ if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
5576
5625
  this.serveDashboard(res);
5577
5626
  } else if (method === "GET" && url.pathname === "/events") {
5578
5627
  this.handleSSE(req, res);
@@ -8183,7 +8232,7 @@ function tierDistribution(tiers) {
8183
8232
  }
8184
8233
 
8185
8234
  // src/l4-reputation/tools.ts
8186
- function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
8235
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
8187
8236
  const reputationStore = new ReputationStore(storage, masterKey);
8188
8237
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8189
8238
  const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
@@ -8673,8 +8722,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8673
8722
  });
8674
8723
  }
8675
8724
  const publishType = args.type;
8676
- const veracoreUrl = args.verascore_url || "https://verascore.ai";
8677
- const ALLOWED_VERASCORE_HOSTS = ["verascore.ai", "www.verascore.ai", "api.verascore.ai"];
8725
+ const configuredVerascoreUrl = verascoreUrl || "https://verascore.ai";
8726
+ const veracoreUrl = args.verascore_url || configuredVerascoreUrl;
8727
+ const ALLOWED_VERASCORE_HOSTS = /* @__PURE__ */ new Set([
8728
+ "verascore.ai",
8729
+ "www.verascore.ai",
8730
+ "api.verascore.ai"
8731
+ ]);
8732
+ try {
8733
+ const configuredHost = new URL(configuredVerascoreUrl).hostname;
8734
+ ALLOWED_VERASCORE_HOSTS.add(configuredHost);
8735
+ } catch {
8736
+ }
8678
8737
  try {
8679
8738
  const parsed = new URL(veracoreUrl);
8680
8739
  if (parsed.protocol !== "https:") {
@@ -8682,9 +8741,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8682
8741
  error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
8683
8742
  });
8684
8743
  }
8685
- if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
8744
+ if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
8686
8745
  return toolResult({
8687
- error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
8746
+ error: `verascore_url must point to a known Verascore domain (${[...ALLOWED_VERASCORE_HOSTS].join(", ")}). Got: ${parsed.hostname}`
8688
8747
  });
8689
8748
  }
8690
8749
  } catch {
@@ -11013,6 +11072,9 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11013
11072
  // src/handshake/tools.ts
11014
11073
  init_router();
11015
11074
  init_generator();
11075
+ init_identity();
11076
+ init_key_derivation();
11077
+ init_encoding();
11016
11078
 
11017
11079
  // src/handshake/protocol.ts
11018
11080
  init_identity();
@@ -11337,7 +11399,10 @@ function verifyAttestation(attestation, now) {
11337
11399
  }
11338
11400
 
11339
11401
  // src/handshake/tools.ts
11340
- function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11402
+ function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
11403
+ const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
11404
+ const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
11405
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
11341
11406
  const sessions = /* @__PURE__ */ new Map();
11342
11407
  const handshakeResults = /* @__PURE__ */ new Map();
11343
11408
  const shrOpts = {
@@ -11409,10 +11474,86 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11409
11474
  }
11410
11475
  sessions.set(result.session.session_id, result.session);
11411
11476
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
11477
+ let autoPublishResult;
11478
+ if (autoPublishHandshakes) {
11479
+ autoPublishResult = { attempted: true };
11480
+ try {
11481
+ const parsed = new URL(verascoreUrl);
11482
+ if (parsed.protocol !== "https:") {
11483
+ autoPublishResult.error = `verascore URL must use HTTPS (got ${parsed.protocol})`;
11484
+ } else {
11485
+ const attestationPayload = {
11486
+ type: "handshake",
11487
+ our_shr_signed_by: shr.signed_by,
11488
+ counterparty_signed_by: "redacted",
11489
+ session_id: result.session.session_id,
11490
+ responded_at: (/* @__PURE__ */ new Date()).toISOString()
11491
+ };
11492
+ const responderIdentity = identityManager.get(shr.body.instance_id);
11493
+ if (!responderIdentity) {
11494
+ autoPublishResult.error = `responder identity ${shr.body.instance_id} not found; skipping auto-publish`;
11495
+ auditLog.append(
11496
+ "l4",
11497
+ "handshake_auto_publish",
11498
+ shr.body.instance_id,
11499
+ { error: autoPublishResult.error },
11500
+ "failure"
11501
+ );
11502
+ } else {
11503
+ const payloadBytes = new TextEncoder().encode(
11504
+ JSON.stringify(attestationPayload)
11505
+ );
11506
+ const sigBytes = sign(
11507
+ payloadBytes,
11508
+ responderIdentity.encrypted_private_key,
11509
+ identityEncKey
11510
+ );
11511
+ const signatureB64 = toBase64url(sigBytes);
11512
+ const resp = await fetch(
11513
+ `${verascoreUrl.replace(/\/$/, "")}/api/publish`,
11514
+ {
11515
+ method: "POST",
11516
+ headers: { "Content-Type": "application/json" },
11517
+ body: JSON.stringify({
11518
+ agentId: shr.body.instance_id,
11519
+ publicKey: shr.signed_by,
11520
+ signature: signatureB64,
11521
+ type: "handshake",
11522
+ data: attestationPayload
11523
+ })
11524
+ }
11525
+ );
11526
+ autoPublishResult.ok = resp.ok;
11527
+ autoPublishResult.status = resp.status;
11528
+ auditLog.append(
11529
+ "l4",
11530
+ "handshake_auto_publish",
11531
+ shr.body.instance_id,
11532
+ {
11533
+ verascore_url: verascoreUrl,
11534
+ status: resp.status,
11535
+ ok: resp.ok
11536
+ },
11537
+ resp.ok ? "success" : "failure"
11538
+ );
11539
+ }
11540
+ }
11541
+ } catch (err) {
11542
+ autoPublishResult.error = err instanceof Error ? err.message : String(err);
11543
+ auditLog.append(
11544
+ "l4",
11545
+ "handshake_auto_publish",
11546
+ shr.body.instance_id,
11547
+ { verascore_url: verascoreUrl, error: autoPublishResult.error },
11548
+ "failure"
11549
+ );
11550
+ }
11551
+ }
11412
11552
  return toolResult({
11413
11553
  session_id: result.session.session_id,
11414
11554
  response: result.response,
11415
11555
  instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
11556
+ auto_publish: autoPublishResult,
11416
11557
  // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
11417
11558
  _content_trust: "external"
11418
11559
  });
@@ -16092,6 +16233,438 @@ function createGovernorTools(governor, auditLog) {
16092
16233
  return { tools };
16093
16234
  }
16094
16235
 
16236
+ // src/sanctuary-tools.ts
16237
+ init_router();
16238
+ init_identity();
16239
+ init_key_derivation();
16240
+ init_encoding();
16241
+ init_identity();
16242
+ init_generator();
16243
+ function validateVerascoreUrl(urlStr, configuredUrl) {
16244
+ const allowed = /* @__PURE__ */ new Set([
16245
+ "verascore.ai",
16246
+ "www.verascore.ai",
16247
+ "api.verascore.ai"
16248
+ ]);
16249
+ try {
16250
+ allowed.add(new URL(configuredUrl).hostname);
16251
+ } catch {
16252
+ }
16253
+ try {
16254
+ const parsed = new URL(urlStr);
16255
+ if (parsed.protocol !== "https:") {
16256
+ return { ok: false, error: `Verascore URL must use HTTPS. Got: ${parsed.protocol}` };
16257
+ }
16258
+ if (!allowed.has(parsed.hostname)) {
16259
+ return {
16260
+ ok: false,
16261
+ error: `Verascore URL must point to a known Verascore host (${[...allowed].join(", ")}). Got: ${parsed.hostname}`
16262
+ };
16263
+ }
16264
+ return { ok: true };
16265
+ } catch {
16266
+ return { ok: false, error: `Invalid Verascore URL: ${urlStr}` };
16267
+ }
16268
+ }
16269
+ function createSanctuaryTools(opts) {
16270
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
16271
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
16272
+ const tools = [
16273
+ // ─── sanctuary_bootstrap ───────────────────────────────────────────
16274
+ {
16275
+ name: "sanctuary/sanctuary_bootstrap",
16276
+ description: "One-shot bootstrap for a new sovereign agent identity. Generates an Ed25519 keypair, stores the encrypted identity, constructs a Sovereignty Health Report (SHR), and publishes it to Verascore. Returns { did, profileUrl, tier } for the newly-minted agent.",
16277
+ inputSchema: {
16278
+ type: "object",
16279
+ properties: {
16280
+ label: {
16281
+ type: "string",
16282
+ description: "Human-readable label for the new identity (default: 'sovereign-agent')"
16283
+ },
16284
+ verascore_url: {
16285
+ type: "string",
16286
+ description: "Verascore base URL. Defaults to server config / SANCTUARY_VERASCORE_URL."
16287
+ },
16288
+ publish: {
16289
+ type: "boolean",
16290
+ description: "Whether to publish the SHR to Verascore. Defaults to true."
16291
+ }
16292
+ }
16293
+ },
16294
+ handler: async (args) => {
16295
+ const label = args.label || "sovereign-agent";
16296
+ const publish = args.publish === void 0 ? true : Boolean(args.publish);
16297
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16298
+ const { publicIdentity, storedIdentity } = createIdentity(
16299
+ label,
16300
+ identityEncKey,
16301
+ keyProtection
16302
+ );
16303
+ await identityManager.save(storedIdentity);
16304
+ auditLog.append("l1", "sanctuary_bootstrap:identity_create", publicIdentity.identity_id, {
16305
+ label,
16306
+ did: publicIdentity.did
16307
+ });
16308
+ const shr = generateSHR(publicIdentity.identity_id, {
16309
+ config,
16310
+ identityManager,
16311
+ masterKey
16312
+ });
16313
+ if (typeof shr === "string") {
16314
+ return toolResult({
16315
+ error: `Identity created but SHR generation failed: ${shr}`,
16316
+ did: publicIdentity.did,
16317
+ identity_id: publicIdentity.identity_id
16318
+ });
16319
+ }
16320
+ const agentSlug = publicIdentity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
16321
+ const profileUrl = `${verascoreUrl.replace(/\/$/, "")}/agent/${publicIdentity.did}`;
16322
+ if (!publish || !config.verascore.auto_publish_to_verascore) {
16323
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16324
+ did: publicIdentity.did,
16325
+ published: false
16326
+ });
16327
+ return toolResult({
16328
+ did: publicIdentity.did,
16329
+ identity_id: publicIdentity.identity_id,
16330
+ profileUrl,
16331
+ tier: "self-attested",
16332
+ published: false
16333
+ });
16334
+ }
16335
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16336
+ if (!urlCheck.ok) {
16337
+ return toolResult({
16338
+ error: urlCheck.error,
16339
+ did: publicIdentity.did,
16340
+ identity_id: publicIdentity.identity_id
16341
+ });
16342
+ }
16343
+ const publishData = {
16344
+ sovereigntyLayers: shr.body.layers,
16345
+ capabilities: shr.body.capabilities,
16346
+ degradations: shr.body.degradations,
16347
+ did: publicIdentity.did,
16348
+ label
16349
+ };
16350
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
16351
+ let signatureB64;
16352
+ try {
16353
+ const sigBytes = sign(
16354
+ payloadBytes,
16355
+ storedIdentity.encrypted_private_key,
16356
+ identityEncKey
16357
+ );
16358
+ signatureB64 = toBase64url(sigBytes);
16359
+ } catch (err) {
16360
+ return toolResult({
16361
+ error: "Failed to sign bootstrap payload",
16362
+ details: err instanceof Error ? err.message : String(err),
16363
+ did: publicIdentity.did
16364
+ });
16365
+ }
16366
+ const body = {
16367
+ agentId: agentSlug,
16368
+ signature: signatureB64,
16369
+ publicKey: publicIdentity.public_key,
16370
+ type: "shr",
16371
+ data: publishData
16372
+ };
16373
+ try {
16374
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/publish`, {
16375
+ method: "POST",
16376
+ headers: { "Content-Type": "application/json" },
16377
+ body: JSON.stringify(body)
16378
+ });
16379
+ const result = await response.json().catch(() => ({}));
16380
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16381
+ did: publicIdentity.did,
16382
+ verascore_url: verascoreUrl,
16383
+ status: response.status,
16384
+ published: response.ok
16385
+ });
16386
+ return toolResult({
16387
+ did: publicIdentity.did,
16388
+ identity_id: publicIdentity.identity_id,
16389
+ profileUrl,
16390
+ tier: "self-attested",
16391
+ published: response.ok,
16392
+ verascore_status: response.status,
16393
+ verascore_response: result
16394
+ });
16395
+ } catch (err) {
16396
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16397
+ did: publicIdentity.did,
16398
+ error: err instanceof Error ? err.message : String(err)
16399
+ });
16400
+ return toolResult({
16401
+ did: publicIdentity.did,
16402
+ identity_id: publicIdentity.identity_id,
16403
+ profileUrl,
16404
+ tier: "self-attested",
16405
+ published: false,
16406
+ warning: `Identity created but Verascore publish failed: ${err instanceof Error ? err.message : String(err)}`
16407
+ });
16408
+ }
16409
+ }
16410
+ },
16411
+ // ─── sanctuary_policy_status ───────────────────────────────────────
16412
+ {
16413
+ name: "sanctuary/sanctuary_policy_status",
16414
+ description: "Return a summary of the active Principal Policy: which operations require approval (Tier 1), which are subject to anomaly detection (Tier 2), and which auto-allow with audit (Tier 3).",
16415
+ inputSchema: {
16416
+ type: "object",
16417
+ properties: {}
16418
+ },
16419
+ handler: async () => {
16420
+ const tier1 = [...policy.tier1_always_approve].sort();
16421
+ const tier3 = [...policy.tier3_always_allow].sort();
16422
+ const tier2Config = policy.tier2_anomaly;
16423
+ auditLog.append("l2", "sanctuary_policy_status", "system", {
16424
+ tier1_count: tier1.length,
16425
+ tier3_count: tier3.length
16426
+ });
16427
+ return toolResult({
16428
+ tier1,
16429
+ tier2: [],
16430
+ tier3,
16431
+ tier2_anomaly_config: tier2Config,
16432
+ counts: {
16433
+ tier1: tier1.length,
16434
+ tier2: 0,
16435
+ tier3: tier3.length
16436
+ },
16437
+ note: "Tier 2 is not a named list in Sanctuary \u2014 it is behavioral anomaly detection applied to all operations. See tier2_anomaly_config."
16438
+ });
16439
+ }
16440
+ },
16441
+ // ─── sanctuary_export_identity_bundle ──────────────────────────────
16442
+ {
16443
+ name: "sanctuary/sanctuary_export_identity_bundle",
16444
+ description: "Export a signed, portable identity bundle: { publicKey, did, shr, attestations }. The bundle is signed with the identity's Ed25519 key so a recipient can verify authenticity against the public key. Private keys are never included.",
16445
+ inputSchema: {
16446
+ type: "object",
16447
+ properties: {
16448
+ identity_id: {
16449
+ type: "string",
16450
+ description: "Identity to export (defaults to primary identity)."
16451
+ },
16452
+ attestations: {
16453
+ type: "array",
16454
+ items: { type: "object" },
16455
+ description: "Optional list of attestation objects to include in the bundle."
16456
+ }
16457
+ }
16458
+ },
16459
+ handler: async (args) => {
16460
+ const identityId = args.identity_id;
16461
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16462
+ if (!identity) {
16463
+ return toolResult({
16464
+ error: "No identity found. Create one with identity_create first."
16465
+ });
16466
+ }
16467
+ const shr = generateSHR(identity.identity_id, {
16468
+ config,
16469
+ identityManager,
16470
+ masterKey
16471
+ });
16472
+ const attestations = args.attestations ?? [];
16473
+ const body = {
16474
+ format: "SANCTUARY_IDENTITY_BUNDLE_V1",
16475
+ publicKey: identity.public_key,
16476
+ did: identity.did,
16477
+ identity_id: identity.identity_id,
16478
+ label: identity.label,
16479
+ key_type: identity.key_type,
16480
+ shr: typeof shr === "string" ? null : shr,
16481
+ attestations,
16482
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
16483
+ };
16484
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
16485
+ let signatureB64;
16486
+ try {
16487
+ const sigBytes = sign(
16488
+ bodyBytes,
16489
+ identity.encrypted_private_key,
16490
+ identityEncKey
16491
+ );
16492
+ signatureB64 = toBase64url(sigBytes);
16493
+ } catch (err) {
16494
+ return toolResult({
16495
+ error: "Failed to sign identity bundle.",
16496
+ details: err instanceof Error ? err.message : String(err)
16497
+ });
16498
+ }
16499
+ auditLog.append("l1", "sanctuary_export_identity_bundle", identity.identity_id, {
16500
+ did: identity.did,
16501
+ attestation_count: attestations.length
16502
+ });
16503
+ return toolResult({
16504
+ bundle: body,
16505
+ signature: signatureB64,
16506
+ signed_by: identity.did
16507
+ });
16508
+ }
16509
+ },
16510
+ // ─── sanctuary_link_to_human ───────────────────────────────────────
16511
+ {
16512
+ name: "sanctuary/sanctuary_link_to_human",
16513
+ description: "Trigger a Verascore magic-link login flow so a human principal can authenticate and subsequently claim this agent's DID. The email is sent by Verascore to the supplied address. This tool only initiates the flow \u2014 it does not directly bind the DID.",
16514
+ inputSchema: {
16515
+ type: "object",
16516
+ properties: {
16517
+ email: {
16518
+ type: "string",
16519
+ description: "Email address of the human to link this agent to."
16520
+ },
16521
+ verascore_url: {
16522
+ type: "string",
16523
+ description: "Verascore base URL. Defaults to server config."
16524
+ }
16525
+ },
16526
+ required: ["email"]
16527
+ },
16528
+ handler: async (args) => {
16529
+ const email = args.email;
16530
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16531
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16532
+ if (!urlCheck.ok) {
16533
+ return toolResult({ ok: false, error: urlCheck.error });
16534
+ }
16535
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
16536
+ return toolResult({ ok: false, error: "Invalid email format." });
16537
+ }
16538
+ try {
16539
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/auth/request`, {
16540
+ method: "POST",
16541
+ headers: { "Content-Type": "application/json" },
16542
+ body: JSON.stringify({ email })
16543
+ });
16544
+ await response.json().catch(() => ({}));
16545
+ auditLog.append("l4", "sanctuary_link_to_human", "system", {
16546
+ verascore_url: verascoreUrl,
16547
+ status: response.status,
16548
+ // Do not log the email to the audit trail — keep it local.
16549
+ email_domain: email.split("@")[1] ?? null
16550
+ });
16551
+ return toolResult({
16552
+ ok: response.ok,
16553
+ message: "Check your email for a login link. After logging in, visit verascore.ai to claim this agent's DID.",
16554
+ email_redacted: `***@${email.split("@")[1] ?? "***"}`,
16555
+ verascore_status: response.status
16556
+ });
16557
+ } catch (err) {
16558
+ return toolResult({
16559
+ ok: false,
16560
+ error: `Failed to reach Verascore at ${verascoreUrl}: ${err instanceof Error ? err.message : String(err)}`
16561
+ });
16562
+ }
16563
+ }
16564
+ },
16565
+ // ─── sanctuary_sign_challenge ──────────────────────────────────────
16566
+ {
16567
+ name: "sanctuary/sanctuary_sign_challenge",
16568
+ description: "Sign a domain-separated nonce with the agent's Ed25519 key. Used in DID-ownership proof flows. The signed message is constructed as: 'sanctuary-sign-challenge-v1\\x00' + purpose + '\\x00' + nonce. The verifier MUST reconstruct the same domain-prefixed message before calling Ed25519 verify \u2014 a raw-nonce signature is NOT valid for this tool. The `purpose` field binds the signature to a specific use case (e.g. 'verascore-claim') so a signature produced for one purpose cannot be replayed against a different verifier.",
16569
+ inputSchema: {
16570
+ type: "object",
16571
+ properties: {
16572
+ nonce: {
16573
+ type: "string",
16574
+ description: "The nonce / challenge string to sign."
16575
+ },
16576
+ purpose: {
16577
+ type: "string",
16578
+ description: "Domain-separation tag identifying what the signature will be used for (e.g. 'verascore-claim'). Required. Max 128 chars, printable ASCII only."
16579
+ },
16580
+ identity_id: {
16581
+ type: "string",
16582
+ description: "Identity to sign with (defaults to primary)."
16583
+ }
16584
+ },
16585
+ required: ["nonce", "purpose"]
16586
+ },
16587
+ handler: async (args) => {
16588
+ const nonce = args.nonce;
16589
+ const purpose = args.purpose;
16590
+ if (!nonce || nonce.length === 0) {
16591
+ return toolResult({ error: "nonce must be a non-empty string." });
16592
+ }
16593
+ if (nonce.length > 4096) {
16594
+ return toolResult({ error: "nonce exceeds maximum length (4096)." });
16595
+ }
16596
+ if (typeof purpose !== "string" || purpose.length === 0) {
16597
+ return toolResult({
16598
+ error: "purpose is required (domain-separation tag, e.g. 'verascore-claim')."
16599
+ });
16600
+ }
16601
+ if (purpose.length > 128) {
16602
+ return toolResult({ error: "purpose exceeds maximum length (128)." });
16603
+ }
16604
+ if (!/^[\x20-\x7E]+$/.test(purpose)) {
16605
+ return toolResult({
16606
+ error: "purpose must be printable ASCII only (no NUL, no non-ASCII)."
16607
+ });
16608
+ }
16609
+ const identityId = args.identity_id;
16610
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16611
+ if (!identity) {
16612
+ return toolResult({
16613
+ error: "No identity found. Create one with identity_create first."
16614
+ });
16615
+ }
16616
+ const domainTag = "sanctuary-sign-challenge-v1";
16617
+ const enc = new TextEncoder();
16618
+ const tagBytes = enc.encode(domainTag);
16619
+ const purposeBytes = enc.encode(purpose);
16620
+ const nonceBytes = enc.encode(nonce);
16621
+ const sep = new Uint8Array([0]);
16622
+ const message = new Uint8Array(
16623
+ tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
16624
+ );
16625
+ let offset = 0;
16626
+ message.set(tagBytes, offset);
16627
+ offset += tagBytes.length;
16628
+ message.set(sep, offset);
16629
+ offset += 1;
16630
+ message.set(purposeBytes, offset);
16631
+ offset += purposeBytes.length;
16632
+ message.set(sep, offset);
16633
+ offset += 1;
16634
+ message.set(nonceBytes, offset);
16635
+ let sigB64;
16636
+ try {
16637
+ const sig = sign(
16638
+ message,
16639
+ identity.encrypted_private_key,
16640
+ identityEncKey
16641
+ );
16642
+ sigB64 = toBase64url(sig);
16643
+ } catch (err) {
16644
+ return toolResult({
16645
+ error: "Failed to sign nonce.",
16646
+ details: err instanceof Error ? err.message : String(err)
16647
+ });
16648
+ }
16649
+ auditLog.append("l1", "sanctuary_sign_challenge", identity.identity_id, {
16650
+ did: identity.did,
16651
+ nonce_len: nonce.length,
16652
+ purpose
16653
+ });
16654
+ return toolResult({
16655
+ signature: sigB64,
16656
+ did: identity.did,
16657
+ public_key: identity.public_key,
16658
+ signed_by: identity.did,
16659
+ domain_tag: domainTag,
16660
+ purpose
16661
+ });
16662
+ }
16663
+ }
16664
+ ];
16665
+ return { tools };
16666
+ }
16667
+
16095
16668
  // src/index.ts
16096
16669
  init_key_derivation();
16097
16670
  init_random();
@@ -16437,14 +17010,19 @@ async function createSanctuaryServer(options) {
16437
17010
  config,
16438
17011
  identityManager,
16439
17012
  masterKey,
16440
- auditLog
17013
+ auditLog,
17014
+ {
17015
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
17016
+ verascoreUrl: config.verascore.url
17017
+ }
16441
17018
  );
16442
17019
  const { tools: l4Tools} = createL4Tools(
16443
17020
  storage,
16444
17021
  masterKey,
16445
17022
  identityManager,
16446
17023
  auditLog,
16447
- handshakeResults
17024
+ handshakeResults,
17025
+ config.verascore.url
16448
17026
  );
16449
17027
  const { tools: federationTools } = createFederationTools(
16450
17028
  auditLog,
@@ -16529,6 +17107,14 @@ async function createSanctuaryServer(options) {
16529
17107
  } : void 0;
16530
17108
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
16531
17109
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
17110
+ const { tools: sanctuaryMetaTools } = createSanctuaryTools({
17111
+ config,
17112
+ identityManager,
17113
+ masterKey,
17114
+ auditLog,
17115
+ policy,
17116
+ keyProtection
17117
+ });
16532
17118
  const dashboardTools = [];
16533
17119
  if (dashboard) {
16534
17120
  dashboardTools.push({
@@ -16570,6 +17156,7 @@ async function createSanctuaryServer(options) {
16570
17156
  ...hardeningTools,
16571
17157
  ...profileTools,
16572
17158
  ...dashboardTools,
17159
+ ...sanctuaryMetaTools,
16573
17160
  manifestTool
16574
17161
  ];
16575
17162
  let clientManager;