@sanctuary-framework/mcp-server 0.5.16 → 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
  };
@@ -8200,7 +8232,7 @@ function tierDistribution(tiers) {
8200
8232
  }
8201
8233
 
8202
8234
  // src/l4-reputation/tools.ts
8203
- function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
8235
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
8204
8236
  const reputationStore = new ReputationStore(storage, masterKey);
8205
8237
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8206
8238
  const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
@@ -8690,8 +8722,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8690
8722
  });
8691
8723
  }
8692
8724
  const publishType = args.type;
8693
- const veracoreUrl = args.verascore_url || "https://verascore.ai";
8694
- 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
+ }
8695
8737
  try {
8696
8738
  const parsed = new URL(veracoreUrl);
8697
8739
  if (parsed.protocol !== "https:") {
@@ -8699,9 +8741,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8699
8741
  error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
8700
8742
  });
8701
8743
  }
8702
- if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
8744
+ if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
8703
8745
  return toolResult({
8704
- 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}`
8705
8747
  });
8706
8748
  }
8707
8749
  } catch {
@@ -11030,6 +11072,9 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11030
11072
  // src/handshake/tools.ts
11031
11073
  init_router();
11032
11074
  init_generator();
11075
+ init_identity();
11076
+ init_key_derivation();
11077
+ init_encoding();
11033
11078
 
11034
11079
  // src/handshake/protocol.ts
11035
11080
  init_identity();
@@ -11354,7 +11399,10 @@ function verifyAttestation(attestation, now) {
11354
11399
  }
11355
11400
 
11356
11401
  // src/handshake/tools.ts
11357
- 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");
11358
11406
  const sessions = /* @__PURE__ */ new Map();
11359
11407
  const handshakeResults = /* @__PURE__ */ new Map();
11360
11408
  const shrOpts = {
@@ -11426,10 +11474,86 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11426
11474
  }
11427
11475
  sessions.set(result.session.session_id, result.session);
11428
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
+ }
11429
11552
  return toolResult({
11430
11553
  session_id: result.session.session_id,
11431
11554
  response: result.response,
11432
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,
11433
11557
  // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
11434
11558
  _content_trust: "external"
11435
11559
  });
@@ -16109,6 +16233,438 @@ function createGovernorTools(governor, auditLog) {
16109
16233
  return { tools };
16110
16234
  }
16111
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
+
16112
16668
  // src/index.ts
16113
16669
  init_key_derivation();
16114
16670
  init_random();
@@ -16454,14 +17010,19 @@ async function createSanctuaryServer(options) {
16454
17010
  config,
16455
17011
  identityManager,
16456
17012
  masterKey,
16457
- auditLog
17013
+ auditLog,
17014
+ {
17015
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
17016
+ verascoreUrl: config.verascore.url
17017
+ }
16458
17018
  );
16459
17019
  const { tools: l4Tools} = createL4Tools(
16460
17020
  storage,
16461
17021
  masterKey,
16462
17022
  identityManager,
16463
17023
  auditLog,
16464
- handshakeResults
17024
+ handshakeResults,
17025
+ config.verascore.url
16465
17026
  );
16466
17027
  const { tools: federationTools } = createFederationTools(
16467
17028
  auditLog,
@@ -16546,6 +17107,14 @@ async function createSanctuaryServer(options) {
16546
17107
  } : void 0;
16547
17108
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
16548
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
+ });
16549
17118
  const dashboardTools = [];
16550
17119
  if (dashboard) {
16551
17120
  dashboardTools.push({
@@ -16587,6 +17156,7 @@ async function createSanctuaryServer(options) {
16587
17156
  ...hardeningTools,
16588
17157
  ...profileTools,
16589
17158
  ...dashboardTools,
17159
+ ...sanctuaryMetaTools,
16590
17160
  manifestTool
16591
17161
  ];
16592
17162
  let clientManager;