@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/index.js CHANGED
@@ -426,6 +426,12 @@ function defaultConfig() {
426
426
  secret: "",
427
427
  callback_port: 3502,
428
428
  callback_host: "127.0.0.1"
429
+ },
430
+ verascore: {
431
+ url: "https://verascore.ai",
432
+ auto_publish_to_verascore: true,
433
+ // DELTA-04: default OFF for privacy. Enable explicitly per deployment.
434
+ auto_publish_handshakes: false
429
435
  }
430
436
  };
431
437
  }
@@ -496,6 +502,21 @@ async function loadConfig(configPath) {
496
502
  if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
497
503
  config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
498
504
  }
505
+ if (process.env.SANCTUARY_VERASCORE_URL) {
506
+ config.verascore.url = process.env.SANCTUARY_VERASCORE_URL;
507
+ }
508
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "true") {
509
+ config.verascore.auto_publish_to_verascore = true;
510
+ }
511
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "false") {
512
+ config.verascore.auto_publish_to_verascore = false;
513
+ }
514
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "true") {
515
+ config.verascore.auto_publish_handshakes = true;
516
+ }
517
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
518
+ config.verascore.auto_publish_handshakes = false;
519
+ }
499
520
  config.version = PKG_VERSION;
500
521
  validateConfig(config);
501
522
  return config;
@@ -3185,7 +3206,7 @@ function tierDistribution(tiers) {
3185
3206
  }
3186
3207
 
3187
3208
  // src/l4-reputation/tools.ts
3188
- function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
3209
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
3189
3210
  const reputationStore = new ReputationStore(storage, masterKey);
3190
3211
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
3191
3212
  const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
@@ -3675,8 +3696,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3675
3696
  });
3676
3697
  }
3677
3698
  const publishType = args.type;
3678
- const veracoreUrl = args.verascore_url || "https://verascore.ai";
3679
- const ALLOWED_VERASCORE_HOSTS = ["verascore.ai", "www.verascore.ai", "api.verascore.ai"];
3699
+ const configuredVerascoreUrl = verascoreUrl || "https://verascore.ai";
3700
+ const veracoreUrl = args.verascore_url || configuredVerascoreUrl;
3701
+ const ALLOWED_VERASCORE_HOSTS = /* @__PURE__ */ new Set([
3702
+ "verascore.ai",
3703
+ "www.verascore.ai",
3704
+ "api.verascore.ai"
3705
+ ]);
3706
+ try {
3707
+ const configuredHost = new URL(configuredVerascoreUrl).hostname;
3708
+ ALLOWED_VERASCORE_HOSTS.add(configuredHost);
3709
+ } catch {
3710
+ }
3680
3711
  try {
3681
3712
  const parsed = new URL(veracoreUrl);
3682
3713
  if (parsed.protocol !== "https:") {
@@ -3684,9 +3715,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3684
3715
  error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
3685
3716
  });
3686
3717
  }
3687
- if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
3718
+ if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
3688
3719
  return toolResult({
3689
- error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
3720
+ error: `verascore_url must point to a known Verascore domain (${[...ALLOWED_VERASCORE_HOSTS].join(", ")}). Got: ${parsed.hostname}`
3690
3721
  });
3691
3722
  }
3692
3723
  } catch {
@@ -3817,12 +3848,14 @@ var DEFAULT_POLICY = {
3817
3848
  "reputation_export",
3818
3849
  "bootstrap_provide_guarantee",
3819
3850
  "decommission_certificate",
3820
- "reputation_publish",
3821
- // SEC-039: Explicit Tier 1 — sends data to external API
3822
3851
  "sovereignty_profile_update",
3823
3852
  // Changes enforcement behavior — always requires approval
3824
- "governor_reset"
3853
+ "governor_reset",
3825
3854
  // Clears all runtime governance state — always requires approval
3855
+ "sanctuary_bootstrap",
3856
+ // Creates new Ed25519 identity + publishes — always requires approval
3857
+ "sanctuary_export_identity_bundle"
3858
+ // Exports portable identity — always requires approval
3826
3859
  ],
3827
3860
  tier2_anomaly: DEFAULT_TIER2,
3828
3861
  tier3_always_allow: [
@@ -3880,7 +3913,11 @@ var DEFAULT_POLICY = {
3880
3913
  "sovereignty_profile_get",
3881
3914
  "sovereignty_profile_generate_prompt",
3882
3915
  // Agent needs its own config to generate system prompt
3883
- "governor_status"
3916
+ "governor_status",
3917
+ "reputation_publish",
3918
+ // Auto-allow: publishing sovereignty data to Verascore is routine
3919
+ "sanctuary_policy_status"
3920
+ // Read-only policy summary
3884
3921
  ],
3885
3922
  approval_channel: DEFAULT_CHANNEL
3886
3923
  };
@@ -3991,9 +4028,10 @@ tier1_always_approve:
3991
4028
  - reputation_import
3992
4029
  - reputation_export
3993
4030
  - bootstrap_provide_guarantee
3994
- - reputation_publish
3995
4031
  - sovereignty_profile_update
3996
4032
  - governor_reset
4033
+ - sanctuary_bootstrap
4034
+ - sanctuary_export_identity_bundle
3997
4035
 
3998
4036
  # \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
3999
4037
  # Triggers approval when agent behavior deviates from its baseline.
@@ -4059,6 +4097,10 @@ tier3_always_allow:
4059
4097
  - dashboard_open
4060
4098
  - sovereignty_profile_get
4061
4099
  - governor_status
4100
+ - reputation_publish
4101
+ - sanctuary_policy_status
4102
+ - sanctuary_link_to_human
4103
+ - sanctuary_sign_challenge
4062
4104
 
4063
4105
  # \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
4064
4106
  # How Sanctuary reaches you when approval is needed.
@@ -4631,7 +4673,7 @@ function generateLoginHTML(options) {
4631
4673
  if (response.ok) {
4632
4674
  const data = await response.json();
4633
4675
  sessionStorage.setItem('authToken', token);
4634
- window.location.href = '/dashboard';
4676
+ window.location.href = '/'; // Dashboard is served at root path
4635
4677
  } else if (response.status === 401) {
4636
4678
  showError('Invalid token. Please check and try again.');
4637
4679
  } else {
@@ -7568,7 +7610,24 @@ var DashboardApprovalChannel = class {
7568
7610
  }
7569
7611
  resolve();
7570
7612
  });
7571
- this.httpServer.on("error", reject);
7613
+ this.httpServer.on("error", (err) => {
7614
+ if (err.code === "EADDRINUSE") {
7615
+ const port = this.config.port;
7616
+ process.stderr.write(
7617
+ `
7618
+ \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
7619
+ \u2551 Port ${port} is already in use. \u2551
7620
+ \u2551 \u2551
7621
+ \u2551 Another Sanctuary Dashboard may still be running. \u2551
7622
+ \u2551 To fix: lsof -ti:${port} | xargs kill \u2551
7623
+ \u2551 Then restart the dashboard. \u2551
7624
+ \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
7625
+
7626
+ `
7627
+ );
7628
+ }
7629
+ reject(err);
7630
+ });
7572
7631
  });
7573
7632
  }
7574
7633
  /**
@@ -7848,7 +7907,7 @@ var DashboardApprovalChannel = class {
7848
7907
  if (!this.checkAuth(req, url, res)) return;
7849
7908
  if (!this.checkRateLimit(req, res, "general")) return;
7850
7909
  try {
7851
- if (method === "GET" && url.pathname === "/") {
7910
+ if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
7852
7911
  this.serveDashboard(res);
7853
7912
  } else if (method === "GET" && url.pathname === "/events") {
7854
7913
  this.handleSSE(req, res);
@@ -10560,6 +10619,10 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10560
10619
  return { tools };
10561
10620
  }
10562
10621
 
10622
+ // src/handshake/tools.ts
10623
+ init_identity();
10624
+ init_encoding();
10625
+
10563
10626
  // src/handshake/protocol.ts
10564
10627
  init_identity();
10565
10628
  init_encoding();
@@ -10880,7 +10943,10 @@ function verifyAttestation(attestation, now) {
10880
10943
  }
10881
10944
 
10882
10945
  // src/handshake/tools.ts
10883
- function createHandshakeTools(config, identityManager, masterKey, auditLog) {
10946
+ function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
10947
+ const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
10948
+ const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
10949
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
10884
10950
  const sessions = /* @__PURE__ */ new Map();
10885
10951
  const handshakeResults = /* @__PURE__ */ new Map();
10886
10952
  const shrOpts = {
@@ -10952,10 +11018,86 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
10952
11018
  }
10953
11019
  sessions.set(result.session.session_id, result.session);
10954
11020
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
11021
+ let autoPublishResult;
11022
+ if (autoPublishHandshakes) {
11023
+ autoPublishResult = { attempted: true };
11024
+ try {
11025
+ const parsed = new URL(verascoreUrl);
11026
+ if (parsed.protocol !== "https:") {
11027
+ autoPublishResult.error = `verascore URL must use HTTPS (got ${parsed.protocol})`;
11028
+ } else {
11029
+ const attestationPayload = {
11030
+ type: "handshake",
11031
+ our_shr_signed_by: shr.signed_by,
11032
+ counterparty_signed_by: "redacted",
11033
+ session_id: result.session.session_id,
11034
+ responded_at: (/* @__PURE__ */ new Date()).toISOString()
11035
+ };
11036
+ const responderIdentity = identityManager.get(shr.body.instance_id);
11037
+ if (!responderIdentity) {
11038
+ autoPublishResult.error = `responder identity ${shr.body.instance_id} not found; skipping auto-publish`;
11039
+ auditLog.append(
11040
+ "l4",
11041
+ "handshake_auto_publish",
11042
+ shr.body.instance_id,
11043
+ { error: autoPublishResult.error },
11044
+ "failure"
11045
+ );
11046
+ } else {
11047
+ const payloadBytes = new TextEncoder().encode(
11048
+ JSON.stringify(attestationPayload)
11049
+ );
11050
+ const sigBytes = sign(
11051
+ payloadBytes,
11052
+ responderIdentity.encrypted_private_key,
11053
+ identityEncKey
11054
+ );
11055
+ const signatureB64 = toBase64url(sigBytes);
11056
+ const resp = await fetch(
11057
+ `${verascoreUrl.replace(/\/$/, "")}/api/publish`,
11058
+ {
11059
+ method: "POST",
11060
+ headers: { "Content-Type": "application/json" },
11061
+ body: JSON.stringify({
11062
+ agentId: shr.body.instance_id,
11063
+ publicKey: shr.signed_by,
11064
+ signature: signatureB64,
11065
+ type: "handshake",
11066
+ data: attestationPayload
11067
+ })
11068
+ }
11069
+ );
11070
+ autoPublishResult.ok = resp.ok;
11071
+ autoPublishResult.status = resp.status;
11072
+ auditLog.append(
11073
+ "l4",
11074
+ "handshake_auto_publish",
11075
+ shr.body.instance_id,
11076
+ {
11077
+ verascore_url: verascoreUrl,
11078
+ status: resp.status,
11079
+ ok: resp.ok
11080
+ },
11081
+ resp.ok ? "success" : "failure"
11082
+ );
11083
+ }
11084
+ }
11085
+ } catch (err) {
11086
+ autoPublishResult.error = err instanceof Error ? err.message : String(err);
11087
+ auditLog.append(
11088
+ "l4",
11089
+ "handshake_auto_publish",
11090
+ shr.body.instance_id,
11091
+ { verascore_url: verascoreUrl, error: autoPublishResult.error },
11092
+ "failure"
11093
+ );
11094
+ }
11095
+ }
10955
11096
  return toolResult({
10956
11097
  session_id: result.session.session_id,
10957
11098
  response: result.response,
10958
11099
  instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
11100
+ auto_publish: autoPublishResult,
10959
11101
  // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
10960
11102
  _content_trust: "external"
10961
11103
  });
@@ -15797,6 +15939,435 @@ function createGovernorTools(governor, auditLog) {
15797
15939
  return { tools };
15798
15940
  }
15799
15941
 
15942
+ // src/sanctuary-tools.ts
15943
+ init_identity();
15944
+ init_encoding();
15945
+ init_identity();
15946
+ function validateVerascoreUrl(urlStr, configuredUrl) {
15947
+ const allowed = /* @__PURE__ */ new Set([
15948
+ "verascore.ai",
15949
+ "www.verascore.ai",
15950
+ "api.verascore.ai"
15951
+ ]);
15952
+ try {
15953
+ allowed.add(new URL(configuredUrl).hostname);
15954
+ } catch {
15955
+ }
15956
+ try {
15957
+ const parsed = new URL(urlStr);
15958
+ if (parsed.protocol !== "https:") {
15959
+ return { ok: false, error: `Verascore URL must use HTTPS. Got: ${parsed.protocol}` };
15960
+ }
15961
+ if (!allowed.has(parsed.hostname)) {
15962
+ return {
15963
+ ok: false,
15964
+ error: `Verascore URL must point to a known Verascore host (${[...allowed].join(", ")}). Got: ${parsed.hostname}`
15965
+ };
15966
+ }
15967
+ return { ok: true };
15968
+ } catch {
15969
+ return { ok: false, error: `Invalid Verascore URL: ${urlStr}` };
15970
+ }
15971
+ }
15972
+ function createSanctuaryTools(opts) {
15973
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
15974
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
15975
+ const tools = [
15976
+ // ─── sanctuary_bootstrap ───────────────────────────────────────────
15977
+ {
15978
+ name: "sanctuary/sanctuary_bootstrap",
15979
+ 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.",
15980
+ inputSchema: {
15981
+ type: "object",
15982
+ properties: {
15983
+ label: {
15984
+ type: "string",
15985
+ description: "Human-readable label for the new identity (default: 'sovereign-agent')"
15986
+ },
15987
+ verascore_url: {
15988
+ type: "string",
15989
+ description: "Verascore base URL. Defaults to server config / SANCTUARY_VERASCORE_URL."
15990
+ },
15991
+ publish: {
15992
+ type: "boolean",
15993
+ description: "Whether to publish the SHR to Verascore. Defaults to true."
15994
+ }
15995
+ }
15996
+ },
15997
+ handler: async (args) => {
15998
+ const label = args.label || "sovereign-agent";
15999
+ const publish = args.publish === void 0 ? true : Boolean(args.publish);
16000
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16001
+ const { publicIdentity, storedIdentity } = createIdentity(
16002
+ label,
16003
+ identityEncKey,
16004
+ keyProtection
16005
+ );
16006
+ await identityManager.save(storedIdentity);
16007
+ auditLog.append("l1", "sanctuary_bootstrap:identity_create", publicIdentity.identity_id, {
16008
+ label,
16009
+ did: publicIdentity.did
16010
+ });
16011
+ const shr = generateSHR(publicIdentity.identity_id, {
16012
+ config,
16013
+ identityManager,
16014
+ masterKey
16015
+ });
16016
+ if (typeof shr === "string") {
16017
+ return toolResult({
16018
+ error: `Identity created but SHR generation failed: ${shr}`,
16019
+ did: publicIdentity.did,
16020
+ identity_id: publicIdentity.identity_id
16021
+ });
16022
+ }
16023
+ const agentSlug = publicIdentity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
16024
+ const profileUrl = `${verascoreUrl.replace(/\/$/, "")}/agent/${publicIdentity.did}`;
16025
+ if (!publish || !config.verascore.auto_publish_to_verascore) {
16026
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16027
+ did: publicIdentity.did,
16028
+ published: false
16029
+ });
16030
+ return toolResult({
16031
+ did: publicIdentity.did,
16032
+ identity_id: publicIdentity.identity_id,
16033
+ profileUrl,
16034
+ tier: "self-attested",
16035
+ published: false
16036
+ });
16037
+ }
16038
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16039
+ if (!urlCheck.ok) {
16040
+ return toolResult({
16041
+ error: urlCheck.error,
16042
+ did: publicIdentity.did,
16043
+ identity_id: publicIdentity.identity_id
16044
+ });
16045
+ }
16046
+ const publishData = {
16047
+ sovereigntyLayers: shr.body.layers,
16048
+ capabilities: shr.body.capabilities,
16049
+ degradations: shr.body.degradations,
16050
+ did: publicIdentity.did,
16051
+ label
16052
+ };
16053
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
16054
+ let signatureB64;
16055
+ try {
16056
+ const sigBytes = sign(
16057
+ payloadBytes,
16058
+ storedIdentity.encrypted_private_key,
16059
+ identityEncKey
16060
+ );
16061
+ signatureB64 = toBase64url(sigBytes);
16062
+ } catch (err) {
16063
+ return toolResult({
16064
+ error: "Failed to sign bootstrap payload",
16065
+ details: err instanceof Error ? err.message : String(err),
16066
+ did: publicIdentity.did
16067
+ });
16068
+ }
16069
+ const body = {
16070
+ agentId: agentSlug,
16071
+ signature: signatureB64,
16072
+ publicKey: publicIdentity.public_key,
16073
+ type: "shr",
16074
+ data: publishData
16075
+ };
16076
+ try {
16077
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/publish`, {
16078
+ method: "POST",
16079
+ headers: { "Content-Type": "application/json" },
16080
+ body: JSON.stringify(body)
16081
+ });
16082
+ const result = await response.json().catch(() => ({}));
16083
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16084
+ did: publicIdentity.did,
16085
+ verascore_url: verascoreUrl,
16086
+ status: response.status,
16087
+ published: response.ok
16088
+ });
16089
+ return toolResult({
16090
+ did: publicIdentity.did,
16091
+ identity_id: publicIdentity.identity_id,
16092
+ profileUrl,
16093
+ tier: "self-attested",
16094
+ published: response.ok,
16095
+ verascore_status: response.status,
16096
+ verascore_response: result
16097
+ });
16098
+ } catch (err) {
16099
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16100
+ did: publicIdentity.did,
16101
+ error: err instanceof Error ? err.message : String(err)
16102
+ });
16103
+ return toolResult({
16104
+ did: publicIdentity.did,
16105
+ identity_id: publicIdentity.identity_id,
16106
+ profileUrl,
16107
+ tier: "self-attested",
16108
+ published: false,
16109
+ warning: `Identity created but Verascore publish failed: ${err instanceof Error ? err.message : String(err)}`
16110
+ });
16111
+ }
16112
+ }
16113
+ },
16114
+ // ─── sanctuary_policy_status ───────────────────────────────────────
16115
+ {
16116
+ name: "sanctuary/sanctuary_policy_status",
16117
+ 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).",
16118
+ inputSchema: {
16119
+ type: "object",
16120
+ properties: {}
16121
+ },
16122
+ handler: async () => {
16123
+ const tier1 = [...policy.tier1_always_approve].sort();
16124
+ const tier3 = [...policy.tier3_always_allow].sort();
16125
+ const tier2Config = policy.tier2_anomaly;
16126
+ auditLog.append("l2", "sanctuary_policy_status", "system", {
16127
+ tier1_count: tier1.length,
16128
+ tier3_count: tier3.length
16129
+ });
16130
+ return toolResult({
16131
+ tier1,
16132
+ tier2: [],
16133
+ tier3,
16134
+ tier2_anomaly_config: tier2Config,
16135
+ counts: {
16136
+ tier1: tier1.length,
16137
+ tier2: 0,
16138
+ tier3: tier3.length
16139
+ },
16140
+ note: "Tier 2 is not a named list in Sanctuary \u2014 it is behavioral anomaly detection applied to all operations. See tier2_anomaly_config."
16141
+ });
16142
+ }
16143
+ },
16144
+ // ─── sanctuary_export_identity_bundle ──────────────────────────────
16145
+ {
16146
+ name: "sanctuary/sanctuary_export_identity_bundle",
16147
+ 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.",
16148
+ inputSchema: {
16149
+ type: "object",
16150
+ properties: {
16151
+ identity_id: {
16152
+ type: "string",
16153
+ description: "Identity to export (defaults to primary identity)."
16154
+ },
16155
+ attestations: {
16156
+ type: "array",
16157
+ items: { type: "object" },
16158
+ description: "Optional list of attestation objects to include in the bundle."
16159
+ }
16160
+ }
16161
+ },
16162
+ handler: async (args) => {
16163
+ const identityId = args.identity_id;
16164
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16165
+ if (!identity) {
16166
+ return toolResult({
16167
+ error: "No identity found. Create one with identity_create first."
16168
+ });
16169
+ }
16170
+ const shr = generateSHR(identity.identity_id, {
16171
+ config,
16172
+ identityManager,
16173
+ masterKey
16174
+ });
16175
+ const attestations = args.attestations ?? [];
16176
+ const body = {
16177
+ format: "SANCTUARY_IDENTITY_BUNDLE_V1",
16178
+ publicKey: identity.public_key,
16179
+ did: identity.did,
16180
+ identity_id: identity.identity_id,
16181
+ label: identity.label,
16182
+ key_type: identity.key_type,
16183
+ shr: typeof shr === "string" ? null : shr,
16184
+ attestations,
16185
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
16186
+ };
16187
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
16188
+ let signatureB64;
16189
+ try {
16190
+ const sigBytes = sign(
16191
+ bodyBytes,
16192
+ identity.encrypted_private_key,
16193
+ identityEncKey
16194
+ );
16195
+ signatureB64 = toBase64url(sigBytes);
16196
+ } catch (err) {
16197
+ return toolResult({
16198
+ error: "Failed to sign identity bundle.",
16199
+ details: err instanceof Error ? err.message : String(err)
16200
+ });
16201
+ }
16202
+ auditLog.append("l1", "sanctuary_export_identity_bundle", identity.identity_id, {
16203
+ did: identity.did,
16204
+ attestation_count: attestations.length
16205
+ });
16206
+ return toolResult({
16207
+ bundle: body,
16208
+ signature: signatureB64,
16209
+ signed_by: identity.did
16210
+ });
16211
+ }
16212
+ },
16213
+ // ─── sanctuary_link_to_human ───────────────────────────────────────
16214
+ {
16215
+ name: "sanctuary/sanctuary_link_to_human",
16216
+ 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.",
16217
+ inputSchema: {
16218
+ type: "object",
16219
+ properties: {
16220
+ email: {
16221
+ type: "string",
16222
+ description: "Email address of the human to link this agent to."
16223
+ },
16224
+ verascore_url: {
16225
+ type: "string",
16226
+ description: "Verascore base URL. Defaults to server config."
16227
+ }
16228
+ },
16229
+ required: ["email"]
16230
+ },
16231
+ handler: async (args) => {
16232
+ const email = args.email;
16233
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16234
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16235
+ if (!urlCheck.ok) {
16236
+ return toolResult({ ok: false, error: urlCheck.error });
16237
+ }
16238
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
16239
+ return toolResult({ ok: false, error: "Invalid email format." });
16240
+ }
16241
+ try {
16242
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/auth/request`, {
16243
+ method: "POST",
16244
+ headers: { "Content-Type": "application/json" },
16245
+ body: JSON.stringify({ email })
16246
+ });
16247
+ await response.json().catch(() => ({}));
16248
+ auditLog.append("l4", "sanctuary_link_to_human", "system", {
16249
+ verascore_url: verascoreUrl,
16250
+ status: response.status,
16251
+ // Do not log the email to the audit trail — keep it local.
16252
+ email_domain: email.split("@")[1] ?? null
16253
+ });
16254
+ return toolResult({
16255
+ ok: response.ok,
16256
+ message: "Check your email for a login link. After logging in, visit verascore.ai to claim this agent's DID.",
16257
+ email_redacted: `***@${email.split("@")[1] ?? "***"}`,
16258
+ verascore_status: response.status
16259
+ });
16260
+ } catch (err) {
16261
+ return toolResult({
16262
+ ok: false,
16263
+ error: `Failed to reach Verascore at ${verascoreUrl}: ${err instanceof Error ? err.message : String(err)}`
16264
+ });
16265
+ }
16266
+ }
16267
+ },
16268
+ // ─── sanctuary_sign_challenge ──────────────────────────────────────
16269
+ {
16270
+ name: "sanctuary/sanctuary_sign_challenge",
16271
+ 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.",
16272
+ inputSchema: {
16273
+ type: "object",
16274
+ properties: {
16275
+ nonce: {
16276
+ type: "string",
16277
+ description: "The nonce / challenge string to sign."
16278
+ },
16279
+ purpose: {
16280
+ type: "string",
16281
+ description: "Domain-separation tag identifying what the signature will be used for (e.g. 'verascore-claim'). Required. Max 128 chars, printable ASCII only."
16282
+ },
16283
+ identity_id: {
16284
+ type: "string",
16285
+ description: "Identity to sign with (defaults to primary)."
16286
+ }
16287
+ },
16288
+ required: ["nonce", "purpose"]
16289
+ },
16290
+ handler: async (args) => {
16291
+ const nonce = args.nonce;
16292
+ const purpose = args.purpose;
16293
+ if (!nonce || nonce.length === 0) {
16294
+ return toolResult({ error: "nonce must be a non-empty string." });
16295
+ }
16296
+ if (nonce.length > 4096) {
16297
+ return toolResult({ error: "nonce exceeds maximum length (4096)." });
16298
+ }
16299
+ if (typeof purpose !== "string" || purpose.length === 0) {
16300
+ return toolResult({
16301
+ error: "purpose is required (domain-separation tag, e.g. 'verascore-claim')."
16302
+ });
16303
+ }
16304
+ if (purpose.length > 128) {
16305
+ return toolResult({ error: "purpose exceeds maximum length (128)." });
16306
+ }
16307
+ if (!/^[\x20-\x7E]+$/.test(purpose)) {
16308
+ return toolResult({
16309
+ error: "purpose must be printable ASCII only (no NUL, no non-ASCII)."
16310
+ });
16311
+ }
16312
+ const identityId = args.identity_id;
16313
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16314
+ if (!identity) {
16315
+ return toolResult({
16316
+ error: "No identity found. Create one with identity_create first."
16317
+ });
16318
+ }
16319
+ const domainTag = "sanctuary-sign-challenge-v1";
16320
+ const enc = new TextEncoder();
16321
+ const tagBytes = enc.encode(domainTag);
16322
+ const purposeBytes = enc.encode(purpose);
16323
+ const nonceBytes = enc.encode(nonce);
16324
+ const sep = new Uint8Array([0]);
16325
+ const message = new Uint8Array(
16326
+ tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
16327
+ );
16328
+ let offset = 0;
16329
+ message.set(tagBytes, offset);
16330
+ offset += tagBytes.length;
16331
+ message.set(sep, offset);
16332
+ offset += 1;
16333
+ message.set(purposeBytes, offset);
16334
+ offset += purposeBytes.length;
16335
+ message.set(sep, offset);
16336
+ offset += 1;
16337
+ message.set(nonceBytes, offset);
16338
+ let sigB64;
16339
+ try {
16340
+ const sig = sign(
16341
+ message,
16342
+ identity.encrypted_private_key,
16343
+ identityEncKey
16344
+ );
16345
+ sigB64 = toBase64url(sig);
16346
+ } catch (err) {
16347
+ return toolResult({
16348
+ error: "Failed to sign nonce.",
16349
+ details: err instanceof Error ? err.message : String(err)
16350
+ });
16351
+ }
16352
+ auditLog.append("l1", "sanctuary_sign_challenge", identity.identity_id, {
16353
+ did: identity.did,
16354
+ nonce_len: nonce.length,
16355
+ purpose
16356
+ });
16357
+ return toolResult({
16358
+ signature: sigB64,
16359
+ did: identity.did,
16360
+ public_key: identity.public_key,
16361
+ signed_by: identity.did,
16362
+ domain_tag: domainTag,
16363
+ purpose
16364
+ });
16365
+ }
16366
+ }
16367
+ ];
16368
+ return { tools };
16369
+ }
16370
+
15800
16371
  // src/index.ts
15801
16372
  init_random();
15802
16373
  init_encoding();
@@ -16282,14 +16853,19 @@ async function createSanctuaryServer(options) {
16282
16853
  config,
16283
16854
  identityManager,
16284
16855
  masterKey,
16285
- auditLog
16856
+ auditLog,
16857
+ {
16858
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
16859
+ verascoreUrl: config.verascore.url
16860
+ }
16286
16861
  );
16287
16862
  const { tools: l4Tools} = createL4Tools(
16288
16863
  storage,
16289
16864
  masterKey,
16290
16865
  identityManager,
16291
16866
  auditLog,
16292
- handshakeResults
16867
+ handshakeResults,
16868
+ config.verascore.url
16293
16869
  );
16294
16870
  const { tools: federationTools } = createFederationTools(
16295
16871
  auditLog,
@@ -16374,6 +16950,14 @@ async function createSanctuaryServer(options) {
16374
16950
  } : void 0;
16375
16951
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
16376
16952
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
16953
+ const { tools: sanctuaryMetaTools } = createSanctuaryTools({
16954
+ config,
16955
+ identityManager,
16956
+ masterKey,
16957
+ auditLog,
16958
+ policy,
16959
+ keyProtection
16960
+ });
16377
16961
  const dashboardTools = [];
16378
16962
  if (dashboard) {
16379
16963
  dashboardTools.push({
@@ -16415,6 +16999,7 @@ async function createSanctuaryServer(options) {
16415
16999
  ...hardeningTools,
16416
17000
  ...profileTools,
16417
17001
  ...dashboardTools,
17002
+ ...sanctuaryMetaTools,
16418
17003
  manifestTool
16419
17004
  ];
16420
17005
  let clientManager;