@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.cjs CHANGED
@@ -76,6 +76,12 @@ function defaultConfig() {
76
76
  secret: "",
77
77
  callback_port: 3502,
78
78
  callback_host: "127.0.0.1"
79
+ },
80
+ verascore: {
81
+ url: "https://verascore.ai",
82
+ auto_publish_to_verascore: true,
83
+ // DELTA-04: default OFF for privacy. Enable explicitly per deployment.
84
+ auto_publish_handshakes: false
79
85
  }
80
86
  };
81
87
  }
@@ -146,6 +152,21 @@ async function loadConfig(configPath) {
146
152
  if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
147
153
  config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
148
154
  }
155
+ if (process.env.SANCTUARY_VERASCORE_URL) {
156
+ config.verascore.url = process.env.SANCTUARY_VERASCORE_URL;
157
+ }
158
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "true") {
159
+ config.verascore.auto_publish_to_verascore = true;
160
+ }
161
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "false") {
162
+ config.verascore.auto_publish_to_verascore = false;
163
+ }
164
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "true") {
165
+ config.verascore.auto_publish_handshakes = true;
166
+ }
167
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
168
+ config.verascore.auto_publish_handshakes = false;
169
+ }
149
170
  config.version = PKG_VERSION;
150
171
  validateConfig(config);
151
172
  return config;
@@ -1646,9 +1667,10 @@ tier1_always_approve:
1646
1667
  - reputation_import
1647
1668
  - reputation_export
1648
1669
  - bootstrap_provide_guarantee
1649
- - reputation_publish
1650
1670
  - sovereignty_profile_update
1651
1671
  - governor_reset
1672
+ - sanctuary_bootstrap
1673
+ - sanctuary_export_identity_bundle
1652
1674
 
1653
1675
  # \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
1654
1676
  # Triggers approval when agent behavior deviates from its baseline.
@@ -1714,6 +1736,10 @@ tier3_always_allow:
1714
1736
  - dashboard_open
1715
1737
  - sovereignty_profile_get
1716
1738
  - governor_status
1739
+ - reputation_publish
1740
+ - sanctuary_policy_status
1741
+ - sanctuary_link_to_human
1742
+ - sanctuary_sign_challenge
1717
1743
 
1718
1744
  # \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
1719
1745
  # How Sanctuary reaches you when approval is needed.
@@ -1767,12 +1793,14 @@ var init_loader = __esm({
1767
1793
  "reputation_export",
1768
1794
  "bootstrap_provide_guarantee",
1769
1795
  "decommission_certificate",
1770
- "reputation_publish",
1771
- // SEC-039: Explicit Tier 1 — sends data to external API
1772
1796
  "sovereignty_profile_update",
1773
1797
  // Changes enforcement behavior — always requires approval
1774
- "governor_reset"
1798
+ "governor_reset",
1775
1799
  // Clears all runtime governance state — always requires approval
1800
+ "sanctuary_bootstrap",
1801
+ // Creates new Ed25519 identity + publishes — always requires approval
1802
+ "sanctuary_export_identity_bundle"
1803
+ // Exports portable identity — always requires approval
1776
1804
  ],
1777
1805
  tier2_anomaly: DEFAULT_TIER2,
1778
1806
  tier3_always_allow: [
@@ -1830,7 +1858,11 @@ var init_loader = __esm({
1830
1858
  "sovereignty_profile_get",
1831
1859
  "sovereignty_profile_generate_prompt",
1832
1860
  // Agent needs its own config to generate system prompt
1833
- "governor_status"
1861
+ "governor_status",
1862
+ "reputation_publish",
1863
+ // Auto-allow: publishing sovereignty data to Verascore is routine
1864
+ "sanctuary_policy_status"
1865
+ // Read-only policy summary
1834
1866
  ],
1835
1867
  approval_channel: DEFAULT_CHANNEL
1836
1868
  };
@@ -8203,7 +8235,7 @@ function tierDistribution(tiers) {
8203
8235
  }
8204
8236
 
8205
8237
  // src/l4-reputation/tools.ts
8206
- function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
8238
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
8207
8239
  const reputationStore = new ReputationStore(storage, masterKey);
8208
8240
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8209
8241
  const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
@@ -8693,8 +8725,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8693
8725
  });
8694
8726
  }
8695
8727
  const publishType = args.type;
8696
- const veracoreUrl = args.verascore_url || "https://verascore.ai";
8697
- const ALLOWED_VERASCORE_HOSTS = ["verascore.ai", "www.verascore.ai", "api.verascore.ai"];
8728
+ const configuredVerascoreUrl = verascoreUrl || "https://verascore.ai";
8729
+ const veracoreUrl = args.verascore_url || configuredVerascoreUrl;
8730
+ const ALLOWED_VERASCORE_HOSTS = /* @__PURE__ */ new Set([
8731
+ "verascore.ai",
8732
+ "www.verascore.ai",
8733
+ "api.verascore.ai"
8734
+ ]);
8735
+ try {
8736
+ const configuredHost = new URL(configuredVerascoreUrl).hostname;
8737
+ ALLOWED_VERASCORE_HOSTS.add(configuredHost);
8738
+ } catch {
8739
+ }
8698
8740
  try {
8699
8741
  const parsed = new URL(veracoreUrl);
8700
8742
  if (parsed.protocol !== "https:") {
@@ -8702,9 +8744,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8702
8744
  error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
8703
8745
  });
8704
8746
  }
8705
- if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
8747
+ if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
8706
8748
  return toolResult({
8707
- error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
8749
+ error: `verascore_url must point to a known Verascore domain (${[...ALLOWED_VERASCORE_HOSTS].join(", ")}). Got: ${parsed.hostname}`
8708
8750
  });
8709
8751
  }
8710
8752
  } catch {
@@ -11033,6 +11075,9 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11033
11075
  // src/handshake/tools.ts
11034
11076
  init_router();
11035
11077
  init_generator();
11078
+ init_identity();
11079
+ init_key_derivation();
11080
+ init_encoding();
11036
11081
 
11037
11082
  // src/handshake/protocol.ts
11038
11083
  init_identity();
@@ -11357,7 +11402,10 @@ function verifyAttestation(attestation, now) {
11357
11402
  }
11358
11403
 
11359
11404
  // src/handshake/tools.ts
11360
- function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11405
+ function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
11406
+ const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
11407
+ const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
11408
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
11361
11409
  const sessions = /* @__PURE__ */ new Map();
11362
11410
  const handshakeResults = /* @__PURE__ */ new Map();
11363
11411
  const shrOpts = {
@@ -11429,10 +11477,86 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11429
11477
  }
11430
11478
  sessions.set(result.session.session_id, result.session);
11431
11479
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
11480
+ let autoPublishResult;
11481
+ if (autoPublishHandshakes) {
11482
+ autoPublishResult = { attempted: true };
11483
+ try {
11484
+ const parsed = new URL(verascoreUrl);
11485
+ if (parsed.protocol !== "https:") {
11486
+ autoPublishResult.error = `verascore URL must use HTTPS (got ${parsed.protocol})`;
11487
+ } else {
11488
+ const attestationPayload = {
11489
+ type: "handshake",
11490
+ our_shr_signed_by: shr.signed_by,
11491
+ counterparty_signed_by: "redacted",
11492
+ session_id: result.session.session_id,
11493
+ responded_at: (/* @__PURE__ */ new Date()).toISOString()
11494
+ };
11495
+ const responderIdentity = identityManager.get(shr.body.instance_id);
11496
+ if (!responderIdentity) {
11497
+ autoPublishResult.error = `responder identity ${shr.body.instance_id} not found; skipping auto-publish`;
11498
+ auditLog.append(
11499
+ "l4",
11500
+ "handshake_auto_publish",
11501
+ shr.body.instance_id,
11502
+ { error: autoPublishResult.error },
11503
+ "failure"
11504
+ );
11505
+ } else {
11506
+ const payloadBytes = new TextEncoder().encode(
11507
+ JSON.stringify(attestationPayload)
11508
+ );
11509
+ const sigBytes = sign(
11510
+ payloadBytes,
11511
+ responderIdentity.encrypted_private_key,
11512
+ identityEncKey
11513
+ );
11514
+ const signatureB64 = toBase64url(sigBytes);
11515
+ const resp = await fetch(
11516
+ `${verascoreUrl.replace(/\/$/, "")}/api/publish`,
11517
+ {
11518
+ method: "POST",
11519
+ headers: { "Content-Type": "application/json" },
11520
+ body: JSON.stringify({
11521
+ agentId: shr.body.instance_id,
11522
+ publicKey: shr.signed_by,
11523
+ signature: signatureB64,
11524
+ type: "handshake",
11525
+ data: attestationPayload
11526
+ })
11527
+ }
11528
+ );
11529
+ autoPublishResult.ok = resp.ok;
11530
+ autoPublishResult.status = resp.status;
11531
+ auditLog.append(
11532
+ "l4",
11533
+ "handshake_auto_publish",
11534
+ shr.body.instance_id,
11535
+ {
11536
+ verascore_url: verascoreUrl,
11537
+ status: resp.status,
11538
+ ok: resp.ok
11539
+ },
11540
+ resp.ok ? "success" : "failure"
11541
+ );
11542
+ }
11543
+ }
11544
+ } catch (err) {
11545
+ autoPublishResult.error = err instanceof Error ? err.message : String(err);
11546
+ auditLog.append(
11547
+ "l4",
11548
+ "handshake_auto_publish",
11549
+ shr.body.instance_id,
11550
+ { verascore_url: verascoreUrl, error: autoPublishResult.error },
11551
+ "failure"
11552
+ );
11553
+ }
11554
+ }
11432
11555
  return toolResult({
11433
11556
  session_id: result.session.session_id,
11434
11557
  response: result.response,
11435
11558
  instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
11559
+ auto_publish: autoPublishResult,
11436
11560
  // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
11437
11561
  _content_trust: "external"
11438
11562
  });
@@ -16112,6 +16236,438 @@ function createGovernorTools(governor, auditLog) {
16112
16236
  return { tools };
16113
16237
  }
16114
16238
 
16239
+ // src/sanctuary-tools.ts
16240
+ init_router();
16241
+ init_identity();
16242
+ init_key_derivation();
16243
+ init_encoding();
16244
+ init_identity();
16245
+ init_generator();
16246
+ function validateVerascoreUrl(urlStr, configuredUrl) {
16247
+ const allowed = /* @__PURE__ */ new Set([
16248
+ "verascore.ai",
16249
+ "www.verascore.ai",
16250
+ "api.verascore.ai"
16251
+ ]);
16252
+ try {
16253
+ allowed.add(new URL(configuredUrl).hostname);
16254
+ } catch {
16255
+ }
16256
+ try {
16257
+ const parsed = new URL(urlStr);
16258
+ if (parsed.protocol !== "https:") {
16259
+ return { ok: false, error: `Verascore URL must use HTTPS. Got: ${parsed.protocol}` };
16260
+ }
16261
+ if (!allowed.has(parsed.hostname)) {
16262
+ return {
16263
+ ok: false,
16264
+ error: `Verascore URL must point to a known Verascore host (${[...allowed].join(", ")}). Got: ${parsed.hostname}`
16265
+ };
16266
+ }
16267
+ return { ok: true };
16268
+ } catch {
16269
+ return { ok: false, error: `Invalid Verascore URL: ${urlStr}` };
16270
+ }
16271
+ }
16272
+ function createSanctuaryTools(opts) {
16273
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
16274
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
16275
+ const tools = [
16276
+ // ─── sanctuary_bootstrap ───────────────────────────────────────────
16277
+ {
16278
+ name: "sanctuary/sanctuary_bootstrap",
16279
+ 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.",
16280
+ inputSchema: {
16281
+ type: "object",
16282
+ properties: {
16283
+ label: {
16284
+ type: "string",
16285
+ description: "Human-readable label for the new identity (default: 'sovereign-agent')"
16286
+ },
16287
+ verascore_url: {
16288
+ type: "string",
16289
+ description: "Verascore base URL. Defaults to server config / SANCTUARY_VERASCORE_URL."
16290
+ },
16291
+ publish: {
16292
+ type: "boolean",
16293
+ description: "Whether to publish the SHR to Verascore. Defaults to true."
16294
+ }
16295
+ }
16296
+ },
16297
+ handler: async (args) => {
16298
+ const label = args.label || "sovereign-agent";
16299
+ const publish = args.publish === void 0 ? true : Boolean(args.publish);
16300
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16301
+ const { publicIdentity, storedIdentity } = createIdentity(
16302
+ label,
16303
+ identityEncKey,
16304
+ keyProtection
16305
+ );
16306
+ await identityManager.save(storedIdentity);
16307
+ auditLog.append("l1", "sanctuary_bootstrap:identity_create", publicIdentity.identity_id, {
16308
+ label,
16309
+ did: publicIdentity.did
16310
+ });
16311
+ const shr = generateSHR(publicIdentity.identity_id, {
16312
+ config,
16313
+ identityManager,
16314
+ masterKey
16315
+ });
16316
+ if (typeof shr === "string") {
16317
+ return toolResult({
16318
+ error: `Identity created but SHR generation failed: ${shr}`,
16319
+ did: publicIdentity.did,
16320
+ identity_id: publicIdentity.identity_id
16321
+ });
16322
+ }
16323
+ const agentSlug = publicIdentity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
16324
+ const profileUrl = `${verascoreUrl.replace(/\/$/, "")}/agent/${publicIdentity.did}`;
16325
+ if (!publish || !config.verascore.auto_publish_to_verascore) {
16326
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16327
+ did: publicIdentity.did,
16328
+ published: false
16329
+ });
16330
+ return toolResult({
16331
+ did: publicIdentity.did,
16332
+ identity_id: publicIdentity.identity_id,
16333
+ profileUrl,
16334
+ tier: "self-attested",
16335
+ published: false
16336
+ });
16337
+ }
16338
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16339
+ if (!urlCheck.ok) {
16340
+ return toolResult({
16341
+ error: urlCheck.error,
16342
+ did: publicIdentity.did,
16343
+ identity_id: publicIdentity.identity_id
16344
+ });
16345
+ }
16346
+ const publishData = {
16347
+ sovereigntyLayers: shr.body.layers,
16348
+ capabilities: shr.body.capabilities,
16349
+ degradations: shr.body.degradations,
16350
+ did: publicIdentity.did,
16351
+ label
16352
+ };
16353
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
16354
+ let signatureB64;
16355
+ try {
16356
+ const sigBytes = sign(
16357
+ payloadBytes,
16358
+ storedIdentity.encrypted_private_key,
16359
+ identityEncKey
16360
+ );
16361
+ signatureB64 = toBase64url(sigBytes);
16362
+ } catch (err) {
16363
+ return toolResult({
16364
+ error: "Failed to sign bootstrap payload",
16365
+ details: err instanceof Error ? err.message : String(err),
16366
+ did: publicIdentity.did
16367
+ });
16368
+ }
16369
+ const body = {
16370
+ agentId: agentSlug,
16371
+ signature: signatureB64,
16372
+ publicKey: publicIdentity.public_key,
16373
+ type: "shr",
16374
+ data: publishData
16375
+ };
16376
+ try {
16377
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/publish`, {
16378
+ method: "POST",
16379
+ headers: { "Content-Type": "application/json" },
16380
+ body: JSON.stringify(body)
16381
+ });
16382
+ const result = await response.json().catch(() => ({}));
16383
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16384
+ did: publicIdentity.did,
16385
+ verascore_url: verascoreUrl,
16386
+ status: response.status,
16387
+ published: response.ok
16388
+ });
16389
+ return toolResult({
16390
+ did: publicIdentity.did,
16391
+ identity_id: publicIdentity.identity_id,
16392
+ profileUrl,
16393
+ tier: "self-attested",
16394
+ published: response.ok,
16395
+ verascore_status: response.status,
16396
+ verascore_response: result
16397
+ });
16398
+ } catch (err) {
16399
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16400
+ did: publicIdentity.did,
16401
+ error: err instanceof Error ? err.message : String(err)
16402
+ });
16403
+ return toolResult({
16404
+ did: publicIdentity.did,
16405
+ identity_id: publicIdentity.identity_id,
16406
+ profileUrl,
16407
+ tier: "self-attested",
16408
+ published: false,
16409
+ warning: `Identity created but Verascore publish failed: ${err instanceof Error ? err.message : String(err)}`
16410
+ });
16411
+ }
16412
+ }
16413
+ },
16414
+ // ─── sanctuary_policy_status ───────────────────────────────────────
16415
+ {
16416
+ name: "sanctuary/sanctuary_policy_status",
16417
+ 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).",
16418
+ inputSchema: {
16419
+ type: "object",
16420
+ properties: {}
16421
+ },
16422
+ handler: async () => {
16423
+ const tier1 = [...policy.tier1_always_approve].sort();
16424
+ const tier3 = [...policy.tier3_always_allow].sort();
16425
+ const tier2Config = policy.tier2_anomaly;
16426
+ auditLog.append("l2", "sanctuary_policy_status", "system", {
16427
+ tier1_count: tier1.length,
16428
+ tier3_count: tier3.length
16429
+ });
16430
+ return toolResult({
16431
+ tier1,
16432
+ tier2: [],
16433
+ tier3,
16434
+ tier2_anomaly_config: tier2Config,
16435
+ counts: {
16436
+ tier1: tier1.length,
16437
+ tier2: 0,
16438
+ tier3: tier3.length
16439
+ },
16440
+ note: "Tier 2 is not a named list in Sanctuary \u2014 it is behavioral anomaly detection applied to all operations. See tier2_anomaly_config."
16441
+ });
16442
+ }
16443
+ },
16444
+ // ─── sanctuary_export_identity_bundle ──────────────────────────────
16445
+ {
16446
+ name: "sanctuary/sanctuary_export_identity_bundle",
16447
+ 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.",
16448
+ inputSchema: {
16449
+ type: "object",
16450
+ properties: {
16451
+ identity_id: {
16452
+ type: "string",
16453
+ description: "Identity to export (defaults to primary identity)."
16454
+ },
16455
+ attestations: {
16456
+ type: "array",
16457
+ items: { type: "object" },
16458
+ description: "Optional list of attestation objects to include in the bundle."
16459
+ }
16460
+ }
16461
+ },
16462
+ handler: async (args) => {
16463
+ const identityId = args.identity_id;
16464
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16465
+ if (!identity) {
16466
+ return toolResult({
16467
+ error: "No identity found. Create one with identity_create first."
16468
+ });
16469
+ }
16470
+ const shr = generateSHR(identity.identity_id, {
16471
+ config,
16472
+ identityManager,
16473
+ masterKey
16474
+ });
16475
+ const attestations = args.attestations ?? [];
16476
+ const body = {
16477
+ format: "SANCTUARY_IDENTITY_BUNDLE_V1",
16478
+ publicKey: identity.public_key,
16479
+ did: identity.did,
16480
+ identity_id: identity.identity_id,
16481
+ label: identity.label,
16482
+ key_type: identity.key_type,
16483
+ shr: typeof shr === "string" ? null : shr,
16484
+ attestations,
16485
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
16486
+ };
16487
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
16488
+ let signatureB64;
16489
+ try {
16490
+ const sigBytes = sign(
16491
+ bodyBytes,
16492
+ identity.encrypted_private_key,
16493
+ identityEncKey
16494
+ );
16495
+ signatureB64 = toBase64url(sigBytes);
16496
+ } catch (err) {
16497
+ return toolResult({
16498
+ error: "Failed to sign identity bundle.",
16499
+ details: err instanceof Error ? err.message : String(err)
16500
+ });
16501
+ }
16502
+ auditLog.append("l1", "sanctuary_export_identity_bundle", identity.identity_id, {
16503
+ did: identity.did,
16504
+ attestation_count: attestations.length
16505
+ });
16506
+ return toolResult({
16507
+ bundle: body,
16508
+ signature: signatureB64,
16509
+ signed_by: identity.did
16510
+ });
16511
+ }
16512
+ },
16513
+ // ─── sanctuary_link_to_human ───────────────────────────────────────
16514
+ {
16515
+ name: "sanctuary/sanctuary_link_to_human",
16516
+ 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.",
16517
+ inputSchema: {
16518
+ type: "object",
16519
+ properties: {
16520
+ email: {
16521
+ type: "string",
16522
+ description: "Email address of the human to link this agent to."
16523
+ },
16524
+ verascore_url: {
16525
+ type: "string",
16526
+ description: "Verascore base URL. Defaults to server config."
16527
+ }
16528
+ },
16529
+ required: ["email"]
16530
+ },
16531
+ handler: async (args) => {
16532
+ const email = args.email;
16533
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16534
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16535
+ if (!urlCheck.ok) {
16536
+ return toolResult({ ok: false, error: urlCheck.error });
16537
+ }
16538
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
16539
+ return toolResult({ ok: false, error: "Invalid email format." });
16540
+ }
16541
+ try {
16542
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/auth/request`, {
16543
+ method: "POST",
16544
+ headers: { "Content-Type": "application/json" },
16545
+ body: JSON.stringify({ email })
16546
+ });
16547
+ await response.json().catch(() => ({}));
16548
+ auditLog.append("l4", "sanctuary_link_to_human", "system", {
16549
+ verascore_url: verascoreUrl,
16550
+ status: response.status,
16551
+ // Do not log the email to the audit trail — keep it local.
16552
+ email_domain: email.split("@")[1] ?? null
16553
+ });
16554
+ return toolResult({
16555
+ ok: response.ok,
16556
+ message: "Check your email for a login link. After logging in, visit verascore.ai to claim this agent's DID.",
16557
+ email_redacted: `***@${email.split("@")[1] ?? "***"}`,
16558
+ verascore_status: response.status
16559
+ });
16560
+ } catch (err) {
16561
+ return toolResult({
16562
+ ok: false,
16563
+ error: `Failed to reach Verascore at ${verascoreUrl}: ${err instanceof Error ? err.message : String(err)}`
16564
+ });
16565
+ }
16566
+ }
16567
+ },
16568
+ // ─── sanctuary_sign_challenge ──────────────────────────────────────
16569
+ {
16570
+ name: "sanctuary/sanctuary_sign_challenge",
16571
+ 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.",
16572
+ inputSchema: {
16573
+ type: "object",
16574
+ properties: {
16575
+ nonce: {
16576
+ type: "string",
16577
+ description: "The nonce / challenge string to sign."
16578
+ },
16579
+ purpose: {
16580
+ type: "string",
16581
+ description: "Domain-separation tag identifying what the signature will be used for (e.g. 'verascore-claim'). Required. Max 128 chars, printable ASCII only."
16582
+ },
16583
+ identity_id: {
16584
+ type: "string",
16585
+ description: "Identity to sign with (defaults to primary)."
16586
+ }
16587
+ },
16588
+ required: ["nonce", "purpose"]
16589
+ },
16590
+ handler: async (args) => {
16591
+ const nonce = args.nonce;
16592
+ const purpose = args.purpose;
16593
+ if (!nonce || nonce.length === 0) {
16594
+ return toolResult({ error: "nonce must be a non-empty string." });
16595
+ }
16596
+ if (nonce.length > 4096) {
16597
+ return toolResult({ error: "nonce exceeds maximum length (4096)." });
16598
+ }
16599
+ if (typeof purpose !== "string" || purpose.length === 0) {
16600
+ return toolResult({
16601
+ error: "purpose is required (domain-separation tag, e.g. 'verascore-claim')."
16602
+ });
16603
+ }
16604
+ if (purpose.length > 128) {
16605
+ return toolResult({ error: "purpose exceeds maximum length (128)." });
16606
+ }
16607
+ if (!/^[\x20-\x7E]+$/.test(purpose)) {
16608
+ return toolResult({
16609
+ error: "purpose must be printable ASCII only (no NUL, no non-ASCII)."
16610
+ });
16611
+ }
16612
+ const identityId = args.identity_id;
16613
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16614
+ if (!identity) {
16615
+ return toolResult({
16616
+ error: "No identity found. Create one with identity_create first."
16617
+ });
16618
+ }
16619
+ const domainTag = "sanctuary-sign-challenge-v1";
16620
+ const enc = new TextEncoder();
16621
+ const tagBytes = enc.encode(domainTag);
16622
+ const purposeBytes = enc.encode(purpose);
16623
+ const nonceBytes = enc.encode(nonce);
16624
+ const sep = new Uint8Array([0]);
16625
+ const message = new Uint8Array(
16626
+ tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
16627
+ );
16628
+ let offset = 0;
16629
+ message.set(tagBytes, offset);
16630
+ offset += tagBytes.length;
16631
+ message.set(sep, offset);
16632
+ offset += 1;
16633
+ message.set(purposeBytes, offset);
16634
+ offset += purposeBytes.length;
16635
+ message.set(sep, offset);
16636
+ offset += 1;
16637
+ message.set(nonceBytes, offset);
16638
+ let sigB64;
16639
+ try {
16640
+ const sig = sign(
16641
+ message,
16642
+ identity.encrypted_private_key,
16643
+ identityEncKey
16644
+ );
16645
+ sigB64 = toBase64url(sig);
16646
+ } catch (err) {
16647
+ return toolResult({
16648
+ error: "Failed to sign nonce.",
16649
+ details: err instanceof Error ? err.message : String(err)
16650
+ });
16651
+ }
16652
+ auditLog.append("l1", "sanctuary_sign_challenge", identity.identity_id, {
16653
+ did: identity.did,
16654
+ nonce_len: nonce.length,
16655
+ purpose
16656
+ });
16657
+ return toolResult({
16658
+ signature: sigB64,
16659
+ did: identity.did,
16660
+ public_key: identity.public_key,
16661
+ signed_by: identity.did,
16662
+ domain_tag: domainTag,
16663
+ purpose
16664
+ });
16665
+ }
16666
+ }
16667
+ ];
16668
+ return { tools };
16669
+ }
16670
+
16115
16671
  // src/index.ts
16116
16672
  init_key_derivation();
16117
16673
  init_random();
@@ -16457,14 +17013,19 @@ async function createSanctuaryServer(options) {
16457
17013
  config,
16458
17014
  identityManager,
16459
17015
  masterKey,
16460
- auditLog
17016
+ auditLog,
17017
+ {
17018
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
17019
+ verascoreUrl: config.verascore.url
17020
+ }
16461
17021
  );
16462
17022
  const { tools: l4Tools} = createL4Tools(
16463
17023
  storage,
16464
17024
  masterKey,
16465
17025
  identityManager,
16466
17026
  auditLog,
16467
- handshakeResults
17027
+ handshakeResults,
17028
+ config.verascore.url
16468
17029
  );
16469
17030
  const { tools: federationTools } = createFederationTools(
16470
17031
  auditLog,
@@ -16549,6 +17110,14 @@ async function createSanctuaryServer(options) {
16549
17110
  } : void 0;
16550
17111
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
16551
17112
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
17113
+ const { tools: sanctuaryMetaTools } = createSanctuaryTools({
17114
+ config,
17115
+ identityManager,
17116
+ masterKey,
17117
+ auditLog,
17118
+ policy,
17119
+ keyProtection
17120
+ });
16552
17121
  const dashboardTools = [];
16553
17122
  if (dashboard) {
16554
17123
  dashboardTools.push({
@@ -16590,6 +17159,7 @@ async function createSanctuaryServer(options) {
16590
17159
  ...hardeningTools,
16591
17160
  ...profileTools,
16592
17161
  ...dashboardTools,
17162
+ ...sanctuaryMetaTools,
16593
17163
  manifestTool
16594
17164
  ];
16595
17165
  let clientManager;