@sanctuary-framework/mcp-server 1.2.2 → 1.2.3

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
@@ -4443,13 +4443,23 @@ function parseScalar(value) {
4443
4443
  return value.replace(/^["']|["']$/g, "");
4444
4444
  }
4445
4445
  function validatePolicy(raw) {
4446
+ if (!("tier1_always_approve" in raw)) {
4447
+ throw new Error(
4448
+ "Policy file must include 'tier1_always_approve' as an explicit list (use [] for empty). Remove specific entries instead of removing the whole key."
4449
+ );
4450
+ }
4451
+ if (!("approval_channel" in raw)) {
4452
+ throw new Error(
4453
+ "Policy file must include 'approval_channel' as an explicit object (use {} for defaults). Remove specific entries instead of removing the whole key."
4454
+ );
4455
+ }
4446
4456
  const userTier3 = raw.tier3_always_allow ?? [];
4447
4457
  const mergedTier3 = [
4448
4458
  .../* @__PURE__ */ new Set([...userTier3, ...DEFAULT_POLICY.tier3_always_allow])
4449
4459
  ];
4450
4460
  return {
4451
4461
  version: raw.version ?? 1,
4452
- tier1_always_approve: raw.tier1_always_approve ?? DEFAULT_POLICY.tier1_always_approve,
4462
+ tier1_always_approve: raw.tier1_always_approve,
4453
4463
  tier2_anomaly: {
4454
4464
  ...DEFAULT_TIER2,
4455
4465
  ...raw.tier2_anomaly ?? {}
@@ -4470,6 +4480,11 @@ function generateDefaultPolicyYaml() {
4470
4480
  # This file controls what your agent can do without asking.
4471
4481
  # Edit this file directly. Your agent cannot modify it.
4472
4482
  # Changes take effect on server restart.
4483
+ #
4484
+ # Required keys (must be present; use [] or {} for empty):
4485
+ # tier1_always_approve, approval_channel
4486
+ # Optional keys (omit to use defaults; new defaults merge automatically):
4487
+ # tier2_anomaly, tier3_always_allow
4473
4488
 
4474
4489
  version: 1
4475
4490
 
@@ -11120,10 +11135,11 @@ function initTemplate(params) {
11120
11135
  var DEFAULT_STORAGE_DIR = ".sanctuary";
11121
11136
  var KEYCHAIN_SERVICE_DEFAULT = "sanctuary-passphrase";
11122
11137
  function keychainServiceFor(storagePath, home = os.homedir()) {
11123
- const defaultPath = path.join(home, DEFAULT_STORAGE_DIR);
11124
- if (storagePath === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11125
- const digest = sha256.sha256(Buffer.from(storagePath, "utf-8"));
11126
- const suffix = Buffer.from(digest).toString("hex").slice(0, 12);
11138
+ const defaultPath = path.resolve(path.join(home, DEFAULT_STORAGE_DIR));
11139
+ const canonicalStorage = path.resolve(storagePath);
11140
+ if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11141
+ const digest = sha256.sha256(Buffer.from(canonicalStorage, "utf-8"));
11142
+ const suffix = Buffer.from(digest).toString("hex").slice(0, 16);
11127
11143
  return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
11128
11144
  }
11129
11145
  var RUNTIME_FILE_NAME = "runtime.json";
@@ -11264,7 +11280,7 @@ async function discoverTenants(options = {}) {
11264
11280
  for (const child of children) {
11265
11281
  const childPath = path.join(root, child);
11266
11282
  if (child.startsWith(".")) continue;
11267
- if (child === "state" || child === "backup" || child === "config") continue;
11283
+ if (child === "state" || child === "backup" || child === "config" || child === "default") continue;
11268
11284
  const s = await promises.stat(childPath).catch(() => null);
11269
11285
  if (!s || !s.isDirectory()) continue;
11270
11286
  const desc = await describeTenant(child, childPath, home);
@@ -11276,6 +11292,17 @@ async function discoverTenants(options = {}) {
11276
11292
  const desc = await describeTenant(path.basename(extra), extra, home);
11277
11293
  if (desc) tenants.push(desc);
11278
11294
  }
11295
+ const seen = /* @__PURE__ */ new Map();
11296
+ for (const t of tenants) {
11297
+ seen.set(t.name, (seen.get(t.name) ?? 0) + 1);
11298
+ }
11299
+ for (const [name, count] of seen) {
11300
+ if (count > 1) {
11301
+ console.error(
11302
+ `[sanctuary] warning: ${count} tenants share the name "${name}". Use --tenant with a unique name or storage path to disambiguate.`
11303
+ );
11304
+ }
11305
+ }
11279
11306
  tenants.sort((a, b) => {
11280
11307
  if (a.name === "default") return -1;
11281
11308
  if (b.name === "default") return 1;
@@ -15644,6 +15671,8 @@ var IntelligenceRouterError = class extends Error {
15644
15671
  this.code = code;
15645
15672
  this.name = "IntelligenceRouterError";
15646
15673
  }
15674
+ statusCode;
15675
+ code;
15647
15676
  };
15648
15677
  function writeJSON3(res, status, payload) {
15649
15678
  res.writeHead(status, {
@@ -16227,7 +16256,7 @@ var DashboardApprovalChannel = class {
16227
16256
  server = http.createServer(handler);
16228
16257
  }
16229
16258
  this.httpServer = server;
16230
- return new Promise((resolve4, reject) => {
16259
+ return new Promise((resolve6, reject) => {
16231
16260
  const protocol = this.useTLS ? "https" : "http";
16232
16261
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
16233
16262
  server.listen(this.config.port, this.config.host, () => {
@@ -16252,7 +16281,7 @@ var DashboardApprovalChannel = class {
16252
16281
  if (shouldAutoOpen) {
16253
16282
  this.openInBrowser(sessionUrl);
16254
16283
  }
16255
- resolve4();
16284
+ resolve6();
16256
16285
  });
16257
16286
  server.on("error", (err) => {
16258
16287
  if (err.code === "EADDRINUSE") {
@@ -16298,8 +16327,8 @@ var DashboardApprovalChannel = class {
16298
16327
  }
16299
16328
  this.rateLimits.clear();
16300
16329
  if (this.httpServer) {
16301
- return new Promise((resolve4) => {
16302
- this.httpServer.close(() => resolve4());
16330
+ return new Promise((resolve6) => {
16331
+ this.httpServer.close(() => resolve6());
16303
16332
  });
16304
16333
  }
16305
16334
  }
@@ -16313,7 +16342,7 @@ var DashboardApprovalChannel = class {
16313
16342
  `[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
16314
16343
  `
16315
16344
  );
16316
- return new Promise((resolve4) => {
16345
+ return new Promise((resolve6) => {
16317
16346
  const timer = setTimeout(() => {
16318
16347
  this.pending.delete(id);
16319
16348
  const response = {
@@ -16327,12 +16356,12 @@ var DashboardApprovalChannel = class {
16327
16356
  decision: response.decision,
16328
16357
  decided_by: "timeout"
16329
16358
  });
16330
- resolve4(response);
16359
+ resolve6(response);
16331
16360
  }, this.config.timeout_seconds * 1e3);
16332
16361
  const pending = {
16333
16362
  id,
16334
16363
  request,
16335
- resolve: resolve4,
16364
+ resolve: resolve6,
16336
16365
  timer,
16337
16366
  created_at: (/* @__PURE__ */ new Date()).toISOString()
16338
16367
  };
@@ -17193,7 +17222,7 @@ var WebhookApprovalChannel = class {
17193
17222
  * Start the callback listener server.
17194
17223
  */
17195
17224
  async start() {
17196
- return new Promise((resolve4, reject) => {
17225
+ return new Promise((resolve6, reject) => {
17197
17226
  this.callbackServer = http.createServer(
17198
17227
  (req, res) => this.handleCallback(req, res)
17199
17228
  );
@@ -17208,7 +17237,7 @@ var WebhookApprovalChannel = class {
17208
17237
 
17209
17238
  `
17210
17239
  );
17211
- resolve4();
17240
+ resolve6();
17212
17241
  }
17213
17242
  );
17214
17243
  this.callbackServer.on("error", reject);
@@ -17228,8 +17257,8 @@ var WebhookApprovalChannel = class {
17228
17257
  }
17229
17258
  this.pending.clear();
17230
17259
  if (this.callbackServer) {
17231
- return new Promise((resolve4) => {
17232
- this.callbackServer.close(() => resolve4());
17260
+ return new Promise((resolve6) => {
17261
+ this.callbackServer.close(() => resolve6());
17233
17262
  });
17234
17263
  }
17235
17264
  }
@@ -17242,7 +17271,7 @@ var WebhookApprovalChannel = class {
17242
17271
  `[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
17243
17272
  `
17244
17273
  );
17245
- return new Promise((resolve4) => {
17274
+ return new Promise((resolve6) => {
17246
17275
  const timer = setTimeout(() => {
17247
17276
  this.pending.delete(id);
17248
17277
  const response = {
@@ -17251,12 +17280,12 @@ var WebhookApprovalChannel = class {
17251
17280
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
17252
17281
  decided_by: "timeout"
17253
17282
  };
17254
- resolve4(response);
17283
+ resolve6(response);
17255
17284
  }, this.config.timeout_seconds * 1e3);
17256
17285
  const pending = {
17257
17286
  id,
17258
17287
  request,
17259
- resolve: resolve4,
17288
+ resolve: resolve6,
17260
17289
  timer,
17261
17290
  created_at: (/* @__PURE__ */ new Date()).toISOString()
17262
17291
  };
@@ -18542,6 +18571,11 @@ var InjectionDetector = class {
18542
18571
  }
18543
18572
  };
18544
18573
 
18574
+ // src/principal-policy/deny-vocabulary.ts
18575
+ var AGENT_VISIBLE_DENY_REASONS = {
18576
+ REQUIRES_APPROVAL: "operation requires operator approval",
18577
+ NOT_PERMITTED: "operation not permitted"};
18578
+
18545
18579
  // src/principal-policy/gate.ts
18546
18580
  var ApprovalGate = class {
18547
18581
  policy;
@@ -18594,10 +18628,16 @@ var ApprovalGate = class {
18594
18628
  });
18595
18629
  }
18596
18630
  if (injectionResult.recommendation === "block") {
18631
+ this.auditLog.append("l2", `gate_injection_block:${operation}`, "system", {
18632
+ tier: 1,
18633
+ operation,
18634
+ injection_confidence: injectionResult.confidence,
18635
+ signal_count: injectionResult.signals.length
18636
+ });
18597
18637
  return {
18598
18638
  allowed: false,
18599
18639
  tier: 1,
18600
- reason: `Blocked: prompt injection detected in "${operation}" (confidence: ${(injectionResult.confidence * 100).toFixed(0)}%)`,
18640
+ reason: AGENT_VISIBLE_DENY_REASONS.NOT_PERMITTED,
18601
18641
  approval_required: false
18602
18642
  };
18603
18643
  }
@@ -18674,7 +18714,7 @@ var ApprovalGate = class {
18674
18714
  this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
18675
18715
  tier: 1,
18676
18716
  operation,
18677
- warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
18717
+ warning: "Operation is not classified in any policy tier, defaulting to Tier 1 (require approval)"
18678
18718
  });
18679
18719
  return this.requestApproval(
18680
18720
  operation,
@@ -18803,7 +18843,7 @@ var ApprovalGate = class {
18803
18843
  return {
18804
18844
  allowed: response.decision === "approve",
18805
18845
  tier,
18806
- reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : `Tier ${tier} operation requires approval`,
18846
+ reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
18807
18847
  approval_required: true,
18808
18848
  approval_response: response
18809
18849
  };
@@ -19373,7 +19413,11 @@ init_identity();
19373
19413
  init_encoding();
19374
19414
  init_random();
19375
19415
  function generateNonce() {
19376
- return toBase64url(randomBytes(32));
19416
+ const nonce = randomBytes(32);
19417
+ if (!nonce || nonce.length !== 32) {
19418
+ throw new Error("Nonce generation failed: randomBytes returned unexpected length");
19419
+ }
19420
+ return toBase64url(nonce);
19377
19421
  }
19378
19422
  function initiateHandshake(ourSHR) {
19379
19423
  const nonce = generateNonce();
@@ -19484,6 +19528,18 @@ function completeHandshake(response, session, identityManager, masterKey, identi
19484
19528
  return { completion, result };
19485
19529
  }
19486
19530
  function verifyCompletion(completion, session) {
19531
+ if (completion.protocol_version !== "1.0") {
19532
+ return {
19533
+ counterparty_id: "unknown",
19534
+ counterparty_shr: session.our_shr,
19535
+ verified: false,
19536
+ sovereignty_level: "unverified",
19537
+ trust_tier: "unverified",
19538
+ completed_at: completion.completed_at,
19539
+ expires_at: (/* @__PURE__ */ new Date()).toISOString(),
19540
+ errors: [`Unsupported protocol version: ${completion.protocol_version}`]
19541
+ };
19542
+ }
19487
19543
  const errors = [];
19488
19544
  if (!session.their_shr) {
19489
19545
  return {
@@ -21585,6 +21641,9 @@ Inspect the file and either correct the JSON or delete it manually before re-run
21585
21641
  this.cause = cause;
21586
21642
  this.name = "ResetHistoryMalformedError";
21587
21643
  }
21644
+ markerPath;
21645
+ lineNumber;
21646
+ cause;
21588
21647
  };
21589
21648
  function parseResetHistory(content, markerPath) {
21590
21649
  const markerHash = hashToString(stringToBytes(content));
@@ -23826,7 +23885,7 @@ async function runOpenAIPrivacyFilter(text, config) {
23826
23885
  return parsed;
23827
23886
  }
23828
23887
  function runCommand(command, input, timeoutMs) {
23829
- return new Promise((resolve4, reject) => {
23888
+ return new Promise((resolve6, reject) => {
23830
23889
  const child = child_process.spawn(command, [], {
23831
23890
  stdio: ["pipe", "pipe", "pipe"],
23832
23891
  shell: false
@@ -23857,7 +23916,7 @@ function runCommand(command, input, timeoutMs) {
23857
23916
  ));
23858
23917
  return;
23859
23918
  }
23860
- resolve4(stdout);
23919
+ resolve6(stdout);
23861
23920
  });
23862
23921
  child.stdin.end(input);
23863
23922
  });
@@ -25678,13 +25737,13 @@ var ProxyRouter = class {
25678
25737
  * Call an upstream tool with a timeout.
25679
25738
  */
25680
25739
  async callWithTimeout(serverName, toolName, args, timeoutMs) {
25681
- return new Promise((resolve4, reject) => {
25740
+ return new Promise((resolve6, reject) => {
25682
25741
  const timer = setTimeout(() => {
25683
25742
  reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
25684
25743
  }, timeoutMs);
25685
25744
  this.clientManager.callTool(serverName, toolName, args).then((result) => {
25686
25745
  clearTimeout(timer);
25687
- resolve4(result);
25746
+ resolve6(result);
25688
25747
  }).catch((err) => {
25689
25748
  clearTimeout(timer);
25690
25749
  reject(err);
@@ -30731,6 +30790,33 @@ var CONCIERGE_THREAD_KEY = "_fortress";
30731
30790
 
30732
30791
  // src/chat/operator-chat-service.ts
30733
30792
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
30793
+ var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
30794
+ 1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
30795
+ 2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
30796
+ 3. Charter (Cooperative MCP): the sovereignty surface for compliant agents. Policy gates, approval tiers, audit logging, and encrypted state all live here.
30797
+ 4. Heralds: Concordia receipts and Verascore reputation. Cross-fortress accountability after an action completes.
30798
+
30799
+ Five channel templates (canonical names):
30800
+ - request-approve-act: agent proposes an action, operator approves or denies before execution.
30801
+ - read-then-report: agent reads outputs from a data source and reports summaries to the operator.
30802
+ - scheduled-digest: agent runs on a schedule and delivers a periodic digest.
30803
+ - plan-draft-only: agent drafts plans; operator reviews before any execution step.
30804
+ - fortress-relay: agent relays messages between fortresses under operator-scoped policy.
30805
+
30806
+ Four canonical policy slots:
30807
+ - memory: governs what the agent may persist and retrieve from encrypted state.
30808
+ - credentials: governs access to secrets, API keys, and tokens held in the broker.
30809
+ - plans: governs the agent's ability to create, modify, or execute plans.
30810
+ - outputs: governs what the agent may emit to external surfaces (files, APIs, messages).
30811
+
30812
+ Key concepts:
30813
+ - Fortress: the operator-owned sovereignty harness. All state is encrypted at rest under the cocoon.
30814
+ - Cocoon: master-key-wrapped storage derived from the operator's passphrase via Argon2id.
30815
+ - Identity: Ed25519 keypair with a DID, owned by the operator. Private keys never leave the cocoon.
30816
+ - Audit log: append-only encrypted blobs, sequential, recording every gate decision and tool call.
30817
+ - Wrapped agent: any agent runtime that connects to Sanctuary as an MCP client. Tier A (native), Tier B (adapter-wrapped), Tier C (escape hatch).
30818
+
30819
+ Note: this is a static reference block (v1.2.x). Dynamic context injection (live template list, policy schema) ships in v1.3.`;
30734
30820
  var OperatorChatService = class {
30735
30821
  store;
30736
30822
  auditLog;
@@ -30875,6 +30961,9 @@ var OperatorChatService = class {
30875
30961
  * than nested structures. Format:
30876
30962
  *
30877
30963
  * ```
30964
+ * ## Sanctuary reference
30965
+ * <static domain reference block>
30966
+ *
30878
30967
  * ## Recent activity
30879
30968
  * <recentActivity output>
30880
30969
  *
@@ -30886,15 +30975,28 @@ var OperatorChatService = class {
30886
30975
  * ```
30887
30976
  */
30888
30977
  async assembleConciergeContext() {
30978
+ const ref = `## Sanctuary reference
30979
+ ${SANCTUARY_DOMAIN_REFERENCE}`;
30889
30980
  if (!this.contextProviders) {
30890
- return "## Recent activity\n(no providers wired)\n\n## Wrapped agents\n(no providers wired)\n\n## Open inbox\n(no providers wired)";
30981
+ return `${ref}
30982
+
30983
+ ## Recent activity
30984
+ (no providers wired)
30985
+
30986
+ ## Wrapped agents
30987
+ (no providers wired)
30988
+
30989
+ ## Open inbox
30990
+ (no providers wired)`;
30891
30991
  }
30892
30992
  const [activity, agents, inbox] = await Promise.all([
30893
30993
  this.contextProviders.recentActivity(),
30894
30994
  this.contextProviders.agentInventory(),
30895
30995
  this.contextProviders.openInbox()
30896
30996
  ]);
30897
- return `## Recent activity
30997
+ return `${ref}
30998
+
30999
+ ## Recent activity
30898
31000
  ${activity}
30899
31001
 
30900
31002
  ## Wrapped agents
@@ -33999,6 +34101,9 @@ async function importExitBundle(opts) {
33999
34101
  reputationArtifact?.json ?? null,
34000
34102
  manifest
34001
34103
  );
34104
+ if (!conflicts.public_identity_exists && identityArtifact?.json && opts.identityManager.getPrimaryIdentityId() !== null && opts.identityManager.getPrimaryIdentityId() !== identityArtifact.json.bundle.identity_id) {
34105
+ conflicts.public_identity_exists = true;
34106
+ }
34002
34107
  if (!opts.activate) {
34003
34108
  return {
34004
34109
  verified: true,
@@ -34025,7 +34130,7 @@ async function importExitBundle(opts) {
34025
34130
  if (conflicts.public_identity_exists && !opts.forceRebind) {
34026
34131
  throw new ExitBundleImportError(
34027
34132
  "IDENTITY_OVERWRITE_REFUSED",
34028
- "Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
34133
+ "Importing this exit bundle would overwrite an existing fortress public identity (either the same identity already imported, or a different identity is currently active). Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
34029
34134
  );
34030
34135
  }
34031
34136
  if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
@@ -34383,6 +34488,26 @@ async function runExitCommand(args) {
34383
34488
  write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
34384
34489
  return 2;
34385
34490
  }
34491
+ const bundleRoot = path.resolve(dir);
34492
+ try {
34493
+ await promises.access(bundleRoot);
34494
+ } catch {
34495
+ write(err, `Error: bundle directory not found: ${bundleRoot}
34496
+ `);
34497
+ return 1;
34498
+ }
34499
+ const manifestPath = path.join(bundleRoot, "manifest.json");
34500
+ try {
34501
+ const raw = await promises.readFile(manifestPath, "utf8");
34502
+ JSON.parse(raw);
34503
+ } catch {
34504
+ write(
34505
+ err,
34506
+ `Error: bundle manifest missing or malformed at ${manifestPath}
34507
+ `
34508
+ );
34509
+ return 1;
34510
+ }
34386
34511
  const activate = hasFlag(argv, "--activate");
34387
34512
  const forceRebind = hasFlag(argv, "--force-rebind");
34388
34513
  const acceptUnverifiableAttestations = hasFlag(
@@ -34523,11 +34648,11 @@ async function startDashboardServer(options) {
34523
34648
  }
34524
34649
  }
34525
34650
  });
34526
- await new Promise((resolve4, reject) => {
34651
+ await new Promise((resolve6, reject) => {
34527
34652
  server.once("error", reject);
34528
34653
  server.listen(port, host, () => {
34529
34654
  server.off("error", reject);
34530
- resolve4();
34655
+ resolve6();
34531
34656
  });
34532
34657
  });
34533
34658
  const actualPort = (() => {
@@ -34540,8 +34665,8 @@ async function startDashboardServer(options) {
34540
34665
  url,
34541
34666
  port: actualPort,
34542
34667
  host,
34543
- stop: () => new Promise((resolve4, reject) => {
34544
- server.close((err) => err ? reject(err) : resolve4());
34668
+ stop: () => new Promise((resolve6, reject) => {
34669
+ server.close((err) => err ? reject(err) : resolve6());
34545
34670
  }),
34546
34671
  publish,
34547
34672
  publishActivity: (entry) => publish({ type: "activity", data: entry }),
@@ -35199,7 +35324,7 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
35199
35324
  clientManager.configure(enabledServers).catch((err) => {
35200
35325
  console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
35201
35326
  });
35202
- await new Promise((resolve4) => setTimeout(resolve4, 2e3));
35327
+ await new Promise((resolve6) => setTimeout(resolve6, 2e3));
35203
35328
  const proxiedTools = proxyRouter.getProxiedTools();
35204
35329
  if (proxiedTools.length > 0) {
35205
35330
  allTools.push(...proxiedTools);