@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/index.cjs CHANGED
@@ -429,6 +429,12 @@ function defaultConfig() {
429
429
  secret: "",
430
430
  callback_port: 3502,
431
431
  callback_host: "127.0.0.1"
432
+ },
433
+ verascore: {
434
+ url: "https://verascore.ai",
435
+ auto_publish_to_verascore: true,
436
+ // DELTA-04: default OFF for privacy. Enable explicitly per deployment.
437
+ auto_publish_handshakes: false
432
438
  }
433
439
  };
434
440
  }
@@ -499,6 +505,21 @@ async function loadConfig(configPath) {
499
505
  if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
500
506
  config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
501
507
  }
508
+ if (process.env.SANCTUARY_VERASCORE_URL) {
509
+ config.verascore.url = process.env.SANCTUARY_VERASCORE_URL;
510
+ }
511
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "true") {
512
+ config.verascore.auto_publish_to_verascore = true;
513
+ }
514
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "false") {
515
+ config.verascore.auto_publish_to_verascore = false;
516
+ }
517
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "true") {
518
+ config.verascore.auto_publish_handshakes = true;
519
+ }
520
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
521
+ config.verascore.auto_publish_handshakes = false;
522
+ }
502
523
  config.version = PKG_VERSION;
503
524
  validateConfig(config);
504
525
  return config;
@@ -3188,7 +3209,7 @@ function tierDistribution(tiers) {
3188
3209
  }
3189
3210
 
3190
3211
  // src/l4-reputation/tools.ts
3191
- function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
3212
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
3192
3213
  const reputationStore = new ReputationStore(storage, masterKey);
3193
3214
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
3194
3215
  const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
@@ -3678,8 +3699,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3678
3699
  });
3679
3700
  }
3680
3701
  const publishType = args.type;
3681
- const veracoreUrl = args.verascore_url || "https://verascore.ai";
3682
- const ALLOWED_VERASCORE_HOSTS = ["verascore.ai", "www.verascore.ai", "api.verascore.ai"];
3702
+ const configuredVerascoreUrl = verascoreUrl || "https://verascore.ai";
3703
+ const veracoreUrl = args.verascore_url || configuredVerascoreUrl;
3704
+ const ALLOWED_VERASCORE_HOSTS = /* @__PURE__ */ new Set([
3705
+ "verascore.ai",
3706
+ "www.verascore.ai",
3707
+ "api.verascore.ai"
3708
+ ]);
3709
+ try {
3710
+ const configuredHost = new URL(configuredVerascoreUrl).hostname;
3711
+ ALLOWED_VERASCORE_HOSTS.add(configuredHost);
3712
+ } catch {
3713
+ }
3683
3714
  try {
3684
3715
  const parsed = new URL(veracoreUrl);
3685
3716
  if (parsed.protocol !== "https:") {
@@ -3687,9 +3718,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3687
3718
  error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
3688
3719
  });
3689
3720
  }
3690
- if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
3721
+ if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
3691
3722
  return toolResult({
3692
- error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
3723
+ error: `verascore_url must point to a known Verascore domain (${[...ALLOWED_VERASCORE_HOSTS].join(", ")}). Got: ${parsed.hostname}`
3693
3724
  });
3694
3725
  }
3695
3726
  } catch {
@@ -3820,12 +3851,14 @@ var DEFAULT_POLICY = {
3820
3851
  "reputation_export",
3821
3852
  "bootstrap_provide_guarantee",
3822
3853
  "decommission_certificate",
3823
- "reputation_publish",
3824
- // SEC-039: Explicit Tier 1 — sends data to external API
3825
3854
  "sovereignty_profile_update",
3826
3855
  // Changes enforcement behavior — always requires approval
3827
- "governor_reset"
3856
+ "governor_reset",
3828
3857
  // Clears all runtime governance state — always requires approval
3858
+ "sanctuary_bootstrap",
3859
+ // Creates new Ed25519 identity + publishes — always requires approval
3860
+ "sanctuary_export_identity_bundle"
3861
+ // Exports portable identity — always requires approval
3829
3862
  ],
3830
3863
  tier2_anomaly: DEFAULT_TIER2,
3831
3864
  tier3_always_allow: [
@@ -3883,7 +3916,11 @@ var DEFAULT_POLICY = {
3883
3916
  "sovereignty_profile_get",
3884
3917
  "sovereignty_profile_generate_prompt",
3885
3918
  // Agent needs its own config to generate system prompt
3886
- "governor_status"
3919
+ "governor_status",
3920
+ "reputation_publish",
3921
+ // Auto-allow: publishing sovereignty data to Verascore is routine
3922
+ "sanctuary_policy_status"
3923
+ // Read-only policy summary
3887
3924
  ],
3888
3925
  approval_channel: DEFAULT_CHANNEL
3889
3926
  };
@@ -3994,9 +4031,10 @@ tier1_always_approve:
3994
4031
  - reputation_import
3995
4032
  - reputation_export
3996
4033
  - bootstrap_provide_guarantee
3997
- - reputation_publish
3998
4034
  - sovereignty_profile_update
3999
4035
  - governor_reset
4036
+ - sanctuary_bootstrap
4037
+ - sanctuary_export_identity_bundle
4000
4038
 
4001
4039
  # \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
4002
4040
  # Triggers approval when agent behavior deviates from its baseline.
@@ -4062,6 +4100,10 @@ tier3_always_allow:
4062
4100
  - dashboard_open
4063
4101
  - sovereignty_profile_get
4064
4102
  - governor_status
4103
+ - reputation_publish
4104
+ - sanctuary_policy_status
4105
+ - sanctuary_link_to_human
4106
+ - sanctuary_sign_challenge
4065
4107
 
4066
4108
  # \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
4067
4109
  # How Sanctuary reaches you when approval is needed.
@@ -10580,6 +10622,10 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10580
10622
  return { tools };
10581
10623
  }
10582
10624
 
10625
+ // src/handshake/tools.ts
10626
+ init_identity();
10627
+ init_encoding();
10628
+
10583
10629
  // src/handshake/protocol.ts
10584
10630
  init_identity();
10585
10631
  init_encoding();
@@ -10900,7 +10946,10 @@ function verifyAttestation(attestation, now) {
10900
10946
  }
10901
10947
 
10902
10948
  // src/handshake/tools.ts
10903
- function createHandshakeTools(config, identityManager, masterKey, auditLog) {
10949
+ function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
10950
+ const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
10951
+ const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
10952
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
10904
10953
  const sessions = /* @__PURE__ */ new Map();
10905
10954
  const handshakeResults = /* @__PURE__ */ new Map();
10906
10955
  const shrOpts = {
@@ -10972,10 +11021,86 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
10972
11021
  }
10973
11022
  sessions.set(result.session.session_id, result.session);
10974
11023
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
11024
+ let autoPublishResult;
11025
+ if (autoPublishHandshakes) {
11026
+ autoPublishResult = { attempted: true };
11027
+ try {
11028
+ const parsed = new URL(verascoreUrl);
11029
+ if (parsed.protocol !== "https:") {
11030
+ autoPublishResult.error = `verascore URL must use HTTPS (got ${parsed.protocol})`;
11031
+ } else {
11032
+ const attestationPayload = {
11033
+ type: "handshake",
11034
+ our_shr_signed_by: shr.signed_by,
11035
+ counterparty_signed_by: "redacted",
11036
+ session_id: result.session.session_id,
11037
+ responded_at: (/* @__PURE__ */ new Date()).toISOString()
11038
+ };
11039
+ const responderIdentity = identityManager.get(shr.body.instance_id);
11040
+ if (!responderIdentity) {
11041
+ autoPublishResult.error = `responder identity ${shr.body.instance_id} not found; skipping auto-publish`;
11042
+ auditLog.append(
11043
+ "l4",
11044
+ "handshake_auto_publish",
11045
+ shr.body.instance_id,
11046
+ { error: autoPublishResult.error },
11047
+ "failure"
11048
+ );
11049
+ } else {
11050
+ const payloadBytes = new TextEncoder().encode(
11051
+ JSON.stringify(attestationPayload)
11052
+ );
11053
+ const sigBytes = sign(
11054
+ payloadBytes,
11055
+ responderIdentity.encrypted_private_key,
11056
+ identityEncKey
11057
+ );
11058
+ const signatureB64 = toBase64url(sigBytes);
11059
+ const resp = await fetch(
11060
+ `${verascoreUrl.replace(/\/$/, "")}/api/publish`,
11061
+ {
11062
+ method: "POST",
11063
+ headers: { "Content-Type": "application/json" },
11064
+ body: JSON.stringify({
11065
+ agentId: shr.body.instance_id,
11066
+ publicKey: shr.signed_by,
11067
+ signature: signatureB64,
11068
+ type: "handshake",
11069
+ data: attestationPayload
11070
+ })
11071
+ }
11072
+ );
11073
+ autoPublishResult.ok = resp.ok;
11074
+ autoPublishResult.status = resp.status;
11075
+ auditLog.append(
11076
+ "l4",
11077
+ "handshake_auto_publish",
11078
+ shr.body.instance_id,
11079
+ {
11080
+ verascore_url: verascoreUrl,
11081
+ status: resp.status,
11082
+ ok: resp.ok
11083
+ },
11084
+ resp.ok ? "success" : "failure"
11085
+ );
11086
+ }
11087
+ }
11088
+ } catch (err) {
11089
+ autoPublishResult.error = err instanceof Error ? err.message : String(err);
11090
+ auditLog.append(
11091
+ "l4",
11092
+ "handshake_auto_publish",
11093
+ shr.body.instance_id,
11094
+ { verascore_url: verascoreUrl, error: autoPublishResult.error },
11095
+ "failure"
11096
+ );
11097
+ }
11098
+ }
10975
11099
  return toolResult({
10976
11100
  session_id: result.session.session_id,
10977
11101
  response: result.response,
10978
11102
  instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
11103
+ auto_publish: autoPublishResult,
10979
11104
  // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
10980
11105
  _content_trust: "external"
10981
11106
  });
@@ -15817,6 +15942,435 @@ function createGovernorTools(governor, auditLog) {
15817
15942
  return { tools };
15818
15943
  }
15819
15944
 
15945
+ // src/sanctuary-tools.ts
15946
+ init_identity();
15947
+ init_encoding();
15948
+ init_identity();
15949
+ function validateVerascoreUrl(urlStr, configuredUrl) {
15950
+ const allowed = /* @__PURE__ */ new Set([
15951
+ "verascore.ai",
15952
+ "www.verascore.ai",
15953
+ "api.verascore.ai"
15954
+ ]);
15955
+ try {
15956
+ allowed.add(new URL(configuredUrl).hostname);
15957
+ } catch {
15958
+ }
15959
+ try {
15960
+ const parsed = new URL(urlStr);
15961
+ if (parsed.protocol !== "https:") {
15962
+ return { ok: false, error: `Verascore URL must use HTTPS. Got: ${parsed.protocol}` };
15963
+ }
15964
+ if (!allowed.has(parsed.hostname)) {
15965
+ return {
15966
+ ok: false,
15967
+ error: `Verascore URL must point to a known Verascore host (${[...allowed].join(", ")}). Got: ${parsed.hostname}`
15968
+ };
15969
+ }
15970
+ return { ok: true };
15971
+ } catch {
15972
+ return { ok: false, error: `Invalid Verascore URL: ${urlStr}` };
15973
+ }
15974
+ }
15975
+ function createSanctuaryTools(opts) {
15976
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
15977
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
15978
+ const tools = [
15979
+ // ─── sanctuary_bootstrap ───────────────────────────────────────────
15980
+ {
15981
+ name: "sanctuary/sanctuary_bootstrap",
15982
+ 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.",
15983
+ inputSchema: {
15984
+ type: "object",
15985
+ properties: {
15986
+ label: {
15987
+ type: "string",
15988
+ description: "Human-readable label for the new identity (default: 'sovereign-agent')"
15989
+ },
15990
+ verascore_url: {
15991
+ type: "string",
15992
+ description: "Verascore base URL. Defaults to server config / SANCTUARY_VERASCORE_URL."
15993
+ },
15994
+ publish: {
15995
+ type: "boolean",
15996
+ description: "Whether to publish the SHR to Verascore. Defaults to true."
15997
+ }
15998
+ }
15999
+ },
16000
+ handler: async (args) => {
16001
+ const label = args.label || "sovereign-agent";
16002
+ const publish = args.publish === void 0 ? true : Boolean(args.publish);
16003
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16004
+ const { publicIdentity, storedIdentity } = createIdentity(
16005
+ label,
16006
+ identityEncKey,
16007
+ keyProtection
16008
+ );
16009
+ await identityManager.save(storedIdentity);
16010
+ auditLog.append("l1", "sanctuary_bootstrap:identity_create", publicIdentity.identity_id, {
16011
+ label,
16012
+ did: publicIdentity.did
16013
+ });
16014
+ const shr = generateSHR(publicIdentity.identity_id, {
16015
+ config,
16016
+ identityManager,
16017
+ masterKey
16018
+ });
16019
+ if (typeof shr === "string") {
16020
+ return toolResult({
16021
+ error: `Identity created but SHR generation failed: ${shr}`,
16022
+ did: publicIdentity.did,
16023
+ identity_id: publicIdentity.identity_id
16024
+ });
16025
+ }
16026
+ const agentSlug = publicIdentity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
16027
+ const profileUrl = `${verascoreUrl.replace(/\/$/, "")}/agent/${publicIdentity.did}`;
16028
+ if (!publish || !config.verascore.auto_publish_to_verascore) {
16029
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16030
+ did: publicIdentity.did,
16031
+ published: false
16032
+ });
16033
+ return toolResult({
16034
+ did: publicIdentity.did,
16035
+ identity_id: publicIdentity.identity_id,
16036
+ profileUrl,
16037
+ tier: "self-attested",
16038
+ published: false
16039
+ });
16040
+ }
16041
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16042
+ if (!urlCheck.ok) {
16043
+ return toolResult({
16044
+ error: urlCheck.error,
16045
+ did: publicIdentity.did,
16046
+ identity_id: publicIdentity.identity_id
16047
+ });
16048
+ }
16049
+ const publishData = {
16050
+ sovereigntyLayers: shr.body.layers,
16051
+ capabilities: shr.body.capabilities,
16052
+ degradations: shr.body.degradations,
16053
+ did: publicIdentity.did,
16054
+ label
16055
+ };
16056
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
16057
+ let signatureB64;
16058
+ try {
16059
+ const sigBytes = sign(
16060
+ payloadBytes,
16061
+ storedIdentity.encrypted_private_key,
16062
+ identityEncKey
16063
+ );
16064
+ signatureB64 = toBase64url(sigBytes);
16065
+ } catch (err) {
16066
+ return toolResult({
16067
+ error: "Failed to sign bootstrap payload",
16068
+ details: err instanceof Error ? err.message : String(err),
16069
+ did: publicIdentity.did
16070
+ });
16071
+ }
16072
+ const body = {
16073
+ agentId: agentSlug,
16074
+ signature: signatureB64,
16075
+ publicKey: publicIdentity.public_key,
16076
+ type: "shr",
16077
+ data: publishData
16078
+ };
16079
+ try {
16080
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/publish`, {
16081
+ method: "POST",
16082
+ headers: { "Content-Type": "application/json" },
16083
+ body: JSON.stringify(body)
16084
+ });
16085
+ const result = await response.json().catch(() => ({}));
16086
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16087
+ did: publicIdentity.did,
16088
+ verascore_url: verascoreUrl,
16089
+ status: response.status,
16090
+ published: response.ok
16091
+ });
16092
+ return toolResult({
16093
+ did: publicIdentity.did,
16094
+ identity_id: publicIdentity.identity_id,
16095
+ profileUrl,
16096
+ tier: "self-attested",
16097
+ published: response.ok,
16098
+ verascore_status: response.status,
16099
+ verascore_response: result
16100
+ });
16101
+ } catch (err) {
16102
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
16103
+ did: publicIdentity.did,
16104
+ error: err instanceof Error ? err.message : String(err)
16105
+ });
16106
+ return toolResult({
16107
+ did: publicIdentity.did,
16108
+ identity_id: publicIdentity.identity_id,
16109
+ profileUrl,
16110
+ tier: "self-attested",
16111
+ published: false,
16112
+ warning: `Identity created but Verascore publish failed: ${err instanceof Error ? err.message : String(err)}`
16113
+ });
16114
+ }
16115
+ }
16116
+ },
16117
+ // ─── sanctuary_policy_status ───────────────────────────────────────
16118
+ {
16119
+ name: "sanctuary/sanctuary_policy_status",
16120
+ 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).",
16121
+ inputSchema: {
16122
+ type: "object",
16123
+ properties: {}
16124
+ },
16125
+ handler: async () => {
16126
+ const tier1 = [...policy.tier1_always_approve].sort();
16127
+ const tier3 = [...policy.tier3_always_allow].sort();
16128
+ const tier2Config = policy.tier2_anomaly;
16129
+ auditLog.append("l2", "sanctuary_policy_status", "system", {
16130
+ tier1_count: tier1.length,
16131
+ tier3_count: tier3.length
16132
+ });
16133
+ return toolResult({
16134
+ tier1,
16135
+ tier2: [],
16136
+ tier3,
16137
+ tier2_anomaly_config: tier2Config,
16138
+ counts: {
16139
+ tier1: tier1.length,
16140
+ tier2: 0,
16141
+ tier3: tier3.length
16142
+ },
16143
+ note: "Tier 2 is not a named list in Sanctuary \u2014 it is behavioral anomaly detection applied to all operations. See tier2_anomaly_config."
16144
+ });
16145
+ }
16146
+ },
16147
+ // ─── sanctuary_export_identity_bundle ──────────────────────────────
16148
+ {
16149
+ name: "sanctuary/sanctuary_export_identity_bundle",
16150
+ 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.",
16151
+ inputSchema: {
16152
+ type: "object",
16153
+ properties: {
16154
+ identity_id: {
16155
+ type: "string",
16156
+ description: "Identity to export (defaults to primary identity)."
16157
+ },
16158
+ attestations: {
16159
+ type: "array",
16160
+ items: { type: "object" },
16161
+ description: "Optional list of attestation objects to include in the bundle."
16162
+ }
16163
+ }
16164
+ },
16165
+ handler: async (args) => {
16166
+ const identityId = args.identity_id;
16167
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16168
+ if (!identity) {
16169
+ return toolResult({
16170
+ error: "No identity found. Create one with identity_create first."
16171
+ });
16172
+ }
16173
+ const shr = generateSHR(identity.identity_id, {
16174
+ config,
16175
+ identityManager,
16176
+ masterKey
16177
+ });
16178
+ const attestations = args.attestations ?? [];
16179
+ const body = {
16180
+ format: "SANCTUARY_IDENTITY_BUNDLE_V1",
16181
+ publicKey: identity.public_key,
16182
+ did: identity.did,
16183
+ identity_id: identity.identity_id,
16184
+ label: identity.label,
16185
+ key_type: identity.key_type,
16186
+ shr: typeof shr === "string" ? null : shr,
16187
+ attestations,
16188
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
16189
+ };
16190
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
16191
+ let signatureB64;
16192
+ try {
16193
+ const sigBytes = sign(
16194
+ bodyBytes,
16195
+ identity.encrypted_private_key,
16196
+ identityEncKey
16197
+ );
16198
+ signatureB64 = toBase64url(sigBytes);
16199
+ } catch (err) {
16200
+ return toolResult({
16201
+ error: "Failed to sign identity bundle.",
16202
+ details: err instanceof Error ? err.message : String(err)
16203
+ });
16204
+ }
16205
+ auditLog.append("l1", "sanctuary_export_identity_bundle", identity.identity_id, {
16206
+ did: identity.did,
16207
+ attestation_count: attestations.length
16208
+ });
16209
+ return toolResult({
16210
+ bundle: body,
16211
+ signature: signatureB64,
16212
+ signed_by: identity.did
16213
+ });
16214
+ }
16215
+ },
16216
+ // ─── sanctuary_link_to_human ───────────────────────────────────────
16217
+ {
16218
+ name: "sanctuary/sanctuary_link_to_human",
16219
+ 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.",
16220
+ inputSchema: {
16221
+ type: "object",
16222
+ properties: {
16223
+ email: {
16224
+ type: "string",
16225
+ description: "Email address of the human to link this agent to."
16226
+ },
16227
+ verascore_url: {
16228
+ type: "string",
16229
+ description: "Verascore base URL. Defaults to server config."
16230
+ }
16231
+ },
16232
+ required: ["email"]
16233
+ },
16234
+ handler: async (args) => {
16235
+ const email = args.email;
16236
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
16237
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
16238
+ if (!urlCheck.ok) {
16239
+ return toolResult({ ok: false, error: urlCheck.error });
16240
+ }
16241
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
16242
+ return toolResult({ ok: false, error: "Invalid email format." });
16243
+ }
16244
+ try {
16245
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/auth/request`, {
16246
+ method: "POST",
16247
+ headers: { "Content-Type": "application/json" },
16248
+ body: JSON.stringify({ email })
16249
+ });
16250
+ await response.json().catch(() => ({}));
16251
+ auditLog.append("l4", "sanctuary_link_to_human", "system", {
16252
+ verascore_url: verascoreUrl,
16253
+ status: response.status,
16254
+ // Do not log the email to the audit trail — keep it local.
16255
+ email_domain: email.split("@")[1] ?? null
16256
+ });
16257
+ return toolResult({
16258
+ ok: response.ok,
16259
+ message: "Check your email for a login link. After logging in, visit verascore.ai to claim this agent's DID.",
16260
+ email_redacted: `***@${email.split("@")[1] ?? "***"}`,
16261
+ verascore_status: response.status
16262
+ });
16263
+ } catch (err) {
16264
+ return toolResult({
16265
+ ok: false,
16266
+ error: `Failed to reach Verascore at ${verascoreUrl}: ${err instanceof Error ? err.message : String(err)}`
16267
+ });
16268
+ }
16269
+ }
16270
+ },
16271
+ // ─── sanctuary_sign_challenge ──────────────────────────────────────
16272
+ {
16273
+ name: "sanctuary/sanctuary_sign_challenge",
16274
+ 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.",
16275
+ inputSchema: {
16276
+ type: "object",
16277
+ properties: {
16278
+ nonce: {
16279
+ type: "string",
16280
+ description: "The nonce / challenge string to sign."
16281
+ },
16282
+ purpose: {
16283
+ type: "string",
16284
+ description: "Domain-separation tag identifying what the signature will be used for (e.g. 'verascore-claim'). Required. Max 128 chars, printable ASCII only."
16285
+ },
16286
+ identity_id: {
16287
+ type: "string",
16288
+ description: "Identity to sign with (defaults to primary)."
16289
+ }
16290
+ },
16291
+ required: ["nonce", "purpose"]
16292
+ },
16293
+ handler: async (args) => {
16294
+ const nonce = args.nonce;
16295
+ const purpose = args.purpose;
16296
+ if (!nonce || nonce.length === 0) {
16297
+ return toolResult({ error: "nonce must be a non-empty string." });
16298
+ }
16299
+ if (nonce.length > 4096) {
16300
+ return toolResult({ error: "nonce exceeds maximum length (4096)." });
16301
+ }
16302
+ if (typeof purpose !== "string" || purpose.length === 0) {
16303
+ return toolResult({
16304
+ error: "purpose is required (domain-separation tag, e.g. 'verascore-claim')."
16305
+ });
16306
+ }
16307
+ if (purpose.length > 128) {
16308
+ return toolResult({ error: "purpose exceeds maximum length (128)." });
16309
+ }
16310
+ if (!/^[\x20-\x7E]+$/.test(purpose)) {
16311
+ return toolResult({
16312
+ error: "purpose must be printable ASCII only (no NUL, no non-ASCII)."
16313
+ });
16314
+ }
16315
+ const identityId = args.identity_id;
16316
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
16317
+ if (!identity) {
16318
+ return toolResult({
16319
+ error: "No identity found. Create one with identity_create first."
16320
+ });
16321
+ }
16322
+ const domainTag = "sanctuary-sign-challenge-v1";
16323
+ const enc = new TextEncoder();
16324
+ const tagBytes = enc.encode(domainTag);
16325
+ const purposeBytes = enc.encode(purpose);
16326
+ const nonceBytes = enc.encode(nonce);
16327
+ const sep = new Uint8Array([0]);
16328
+ const message = new Uint8Array(
16329
+ tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
16330
+ );
16331
+ let offset = 0;
16332
+ message.set(tagBytes, offset);
16333
+ offset += tagBytes.length;
16334
+ message.set(sep, offset);
16335
+ offset += 1;
16336
+ message.set(purposeBytes, offset);
16337
+ offset += purposeBytes.length;
16338
+ message.set(sep, offset);
16339
+ offset += 1;
16340
+ message.set(nonceBytes, offset);
16341
+ let sigB64;
16342
+ try {
16343
+ const sig = sign(
16344
+ message,
16345
+ identity.encrypted_private_key,
16346
+ identityEncKey
16347
+ );
16348
+ sigB64 = toBase64url(sig);
16349
+ } catch (err) {
16350
+ return toolResult({
16351
+ error: "Failed to sign nonce.",
16352
+ details: err instanceof Error ? err.message : String(err)
16353
+ });
16354
+ }
16355
+ auditLog.append("l1", "sanctuary_sign_challenge", identity.identity_id, {
16356
+ did: identity.did,
16357
+ nonce_len: nonce.length,
16358
+ purpose
16359
+ });
16360
+ return toolResult({
16361
+ signature: sigB64,
16362
+ did: identity.did,
16363
+ public_key: identity.public_key,
16364
+ signed_by: identity.did,
16365
+ domain_tag: domainTag,
16366
+ purpose
16367
+ });
16368
+ }
16369
+ }
16370
+ ];
16371
+ return { tools };
16372
+ }
16373
+
15820
16374
  // src/index.ts
15821
16375
  init_random();
15822
16376
  init_encoding();
@@ -16302,14 +16856,19 @@ async function createSanctuaryServer(options) {
16302
16856
  config,
16303
16857
  identityManager,
16304
16858
  masterKey,
16305
- auditLog
16859
+ auditLog,
16860
+ {
16861
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
16862
+ verascoreUrl: config.verascore.url
16863
+ }
16306
16864
  );
16307
16865
  const { tools: l4Tools} = createL4Tools(
16308
16866
  storage,
16309
16867
  masterKey,
16310
16868
  identityManager,
16311
16869
  auditLog,
16312
- handshakeResults
16870
+ handshakeResults,
16871
+ config.verascore.url
16313
16872
  );
16314
16873
  const { tools: federationTools } = createFederationTools(
16315
16874
  auditLog,
@@ -16394,6 +16953,14 @@ async function createSanctuaryServer(options) {
16394
16953
  } : void 0;
16395
16954
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
16396
16955
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
16956
+ const { tools: sanctuaryMetaTools } = createSanctuaryTools({
16957
+ config,
16958
+ identityManager,
16959
+ masterKey,
16960
+ auditLog,
16961
+ policy,
16962
+ keyProtection
16963
+ });
16397
16964
  const dashboardTools = [];
16398
16965
  if (dashboard) {
16399
16966
  dashboardTools.push({
@@ -16435,6 +17002,7 @@ async function createSanctuaryServer(options) {
16435
17002
  ...hardeningTools,
16436
17003
  ...profileTools,
16437
17004
  ...dashboardTools,
17005
+ ...sanctuaryMetaTools,
16438
17006
  manifestTool
16439
17007
  ];
16440
17008
  let clientManager;