@sanctuary-framework/mcp-server 0.10.5 → 1.0.0-rc.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
@@ -21,6 +21,7 @@ var fs = require('fs');
21
21
  var index_js$1 = require('@modelcontextprotocol/sdk/client/index.js');
22
22
  var stdio_js$1 = require('@modelcontextprotocol/sdk/client/stdio.js');
23
23
  var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
24
+ var url = require('url');
24
25
  var readline = require('readline');
25
26
  var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
26
27
 
@@ -846,7 +847,8 @@ var init_state_store = __esm({
846
847
  "_handshake",
847
848
  "_shr",
848
849
  "_sovereignty_profile",
849
- "_context_gate_policies"
850
+ "_context_gate_policies",
851
+ "_fortress_mode"
850
852
  ];
851
853
  StateStore = class _StateStore {
852
854
  storage;
@@ -1889,7 +1891,8 @@ var init_tools = __esm({
1889
1891
  "_handshake",
1890
1892
  "_shr",
1891
1893
  "_sovereignty_profile",
1892
- "_context_gate_policies"
1894
+ "_context_gate_policies",
1895
+ "_fortress_mode"
1893
1896
  ];
1894
1897
  IdentityManager = class {
1895
1898
  storage;
@@ -4453,8 +4456,12 @@ var init_loader = __esm({
4453
4456
  // Clears all runtime governance state — always requires approval
4454
4457
  "sanctuary_bootstrap",
4455
4458
  // Creates new Ed25519 identity + publishes — always requires approval
4456
- "sanctuary_export_identity_bundle"
4459
+ "sanctuary_export_identity_bundle",
4457
4460
  // Exports portable identity — always requires approval
4461
+ // WP-MVP-2 Operator Console: federation-node-join requires explicit
4462
+ // operator confirmation per Key 8. No auto-approve path. The console's
4463
+ // JoinApprover drives this gate via `MeshConsoleClient.makeJoinApprover`.
4464
+ "federation_node_join"
4458
4465
  ],
4459
4466
  tier2_anomaly: DEFAULT_TIER2,
4460
4467
  tier3_always_allow: [
@@ -6513,6 +6520,28 @@ function generateDashboardHTML(options) {
6513
6520
  </div>
6514
6521
  </div>
6515
6522
 
6523
+ <!--
6524
+ Mesh Health panel (WP-MVP-3 Follow-up #3).
6525
+ Subscribes to the existing /events SSE channel \u2014 no new transport.
6526
+ Render shape: per-node row with presence + flags + rollup.
6527
+ On open alert: list inline with operator-decision CTAs (rollback /
6528
+ split-brain / canonical-audit promotion).
6529
+ On post-recovery prompt: render rotation-prompt overlay with
6530
+ broker-credential list + "rotate now" buttons (rotation flow itself
6531
+ is the existing v0.10.x broker rotation surface).
6532
+ -->
6533
+ <div class="mesh-health-panel" id="mesh-health-panel">
6534
+ <div class="panel-header">
6535
+ <div class="panel-title">Mesh Health</div>
6536
+ <span class="card-value" id="mesh-health-updated-at" style="font-size: 11px; color: var(--text-secondary);">\u2014</span>
6537
+ </div>
6538
+ <div id="mesh-health-rows" class="mesh-health-rows">
6539
+ <div class="empty-state">Waiting for mesh health data\u2026</div>
6540
+ </div>
6541
+ <div id="mesh-health-alerts" class="mesh-health-alerts" style="margin-top: 12px;"></div>
6542
+ <div id="mesh-post-recovery-prompt" class="mesh-post-recovery-prompt" style="margin-top: 12px; display: none;"></div>
6543
+ </div>
6544
+
6516
6545
  <!-- Sovereignty Profile Panel -->
6517
6546
  <div class="profile-panel" id="sovereignty-profile-panel">
6518
6547
  <div class="panel-header">
@@ -6715,6 +6744,11 @@ function generateDashboardHTML(options) {
6715
6744
  // SEC-038: Do NOT embed the long-lived auth token in page source.
6716
6745
  // Use only the session token stored in sessionStorage by the login flow.
6717
6746
  const AUTH_TOKEN = sessionStorage.getItem('authToken') || '';
6747
+ // v0.10.6: server-baked flag mirroring _autoAuthLocalhost. When true,
6748
+ // the init-time auth gate does NOT redirect to '/' on empty AUTH_TOKEN,
6749
+ // because the server already admitted this loopback caller without a
6750
+ // bearer token. See dashboard-html.ts generateDashboardHTML() doc.
6751
+ const LOOPBACK_AUTH = ${JSON.stringify(options.loopbackAutoAuth === true)};
6718
6752
  const TIMEOUT_SECONDS = ${options.timeoutSeconds};
6719
6753
  const API_BASE = '';
6720
6754
 
@@ -7217,12 +7251,96 @@ function generateDashboardHTML(options) {
7217
7251
  loadProxyServers();
7218
7252
  });
7219
7253
 
7254
+ // \u2500\u2500 Mesh Health (WP-MVP-3 Follow-up #3) \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
7255
+ // The federation FailureModeDetector pushes per-tick snapshots and
7256
+ // per-detection alerts via the existing /events channel. Renders are
7257
+ // best-effort \u2014 if the panel DOM is absent (older HTML cache), we
7258
+ // silently skip rather than crashing.
7259
+ eventSource.addEventListener('mesh-health', (e) => {
7260
+ try {
7261
+ const snap = JSON.parse(e.data);
7262
+ renderMeshHealth(snap);
7263
+ } catch (err) { console.error('mesh-health render failed', err); }
7264
+ });
7265
+ eventSource.addEventListener('mesh-failure-mode-alert', (e) => {
7266
+ try {
7267
+ const alert = JSON.parse(e.data);
7268
+ appendMeshAlert(alert);
7269
+ } catch (err) { console.error('mesh alert render failed', err); }
7270
+ });
7271
+ eventSource.addEventListener('mesh-post-recovery-prompt', (e) => {
7272
+ try {
7273
+ const prompt = JSON.parse(e.data);
7274
+ renderMeshPostRecoveryPrompt(prompt);
7275
+ } catch (err) { console.error('mesh post-recovery render failed', err); }
7276
+ });
7277
+
7220
7278
  eventSource.onerror = () => {
7221
7279
  console.error('SSE error');
7222
7280
  setTimeout(setupSSE, 5000);
7223
7281
  };
7224
7282
  }
7225
7283
 
7284
+ // \u2500\u2500 Mesh Health rendering (WP-MVP-3 Follow-up #3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7285
+ function renderMeshHealth(snap) {
7286
+ const updatedAt = document.getElementById('mesh-health-updated-at');
7287
+ const rows = document.getElementById('mesh-health-rows');
7288
+ if (!rows || !snap) return;
7289
+ if (updatedAt) updatedAt.textContent = formatTime(snap.generated_at);
7290
+ const nodes = Array.isArray(snap.nodes) ? snap.nodes : [];
7291
+ if (nodes.length === 0) {
7292
+ rows.innerHTML = '<div class="empty-state">No mesh nodes seen yet.</div>';
7293
+ return;
7294
+ }
7295
+ rows.innerHTML = nodes.map((n) => {
7296
+ const flags = (n.flags || []).map((f) => '<span class="mesh-flag">' + esc(f) + '</span>').join(' ');
7297
+ return '<div class="mesh-row" data-rollup="' + esc(n.rollup) + '">' +
7298
+ '<span class="mesh-node-id">' + esc(n.node_id) + '</span>' +
7299
+ '<span class="mesh-presence">' + esc(n.presence) + '</span>' +
7300
+ '<span class="mesh-rollup">' + esc(n.rollup) + '</span>' +
7301
+ '<span class="mesh-flags">' + flags + '</span>' +
7302
+ '</div>';
7303
+ }).join('');
7304
+ const alertsBox = document.getElementById('mesh-health-alerts');
7305
+ if (alertsBox && Array.isArray(snap.open_alerts)) {
7306
+ alertsBox.innerHTML = snap.open_alerts.map((a) =>
7307
+ '<div class="mesh-alert" data-mode="' + esc(a.mode) + '">' +
7308
+ '<div class="mesh-alert-mode">' + esc(a.mode) + ' \u2014 ' + esc(a.target_node) + '</div>' +
7309
+ '<div class="mesh-alert-message">' + esc(a.message) + '</div>' +
7310
+ '</div>'
7311
+ ).join('');
7312
+ }
7313
+ }
7314
+
7315
+ function appendMeshAlert(alert) {
7316
+ const alertsBox = document.getElementById('mesh-health-alerts');
7317
+ if (!alertsBox || !alert) return;
7318
+ const html = '<div class="mesh-alert" data-mode="' + esc(alert.mode) + '">' +
7319
+ '<div class="mesh-alert-mode">' + esc(alert.mode) + ' \u2014 ' + esc(alert.target_node) + '</div>' +
7320
+ '<div class="mesh-alert-message">' + esc(alert.message) + '</div>' +
7321
+ '</div>';
7322
+ alertsBox.insertAdjacentHTML('afterbegin', html);
7323
+ }
7324
+
7325
+ function renderMeshPostRecoveryPrompt(prompt) {
7326
+ const box = document.getElementById('mesh-post-recovery-prompt');
7327
+ if (!box || !prompt) return;
7328
+ box.style.display = 'block';
7329
+ const creds = Array.isArray(prompt.credentials) ? prompt.credentials : [];
7330
+ box.innerHTML = '<div class="mesh-rotation-banner">' +
7331
+ '<strong>Post-rotation hygiene:</strong> we just rotated your fortress root key. ' +
7332
+ 'Rotate externally-scoped broker credentials that third parties may still associate with ' +
7333
+ 'the pre-rotation key material.' +
7334
+ '</div>' +
7335
+ '<ul class="mesh-rotation-list">' +
7336
+ creds.map((c) => '<li>' +
7337
+ '<span class="mesh-cred-name">' + esc(c.secret_name) + '</span>' +
7338
+ '<button class="mesh-cred-rotate" data-secret="' + esc(c.secret_name) + '">' +
7339
+ 'rotate now</button>' +
7340
+ '</li>').join('') +
7341
+ '</ul>';
7342
+ }
7343
+
7226
7344
  // Activity Feed
7227
7345
  function addActivityItem(item) {
7228
7346
  activityLog.unshift(item);
@@ -7822,7 +7940,13 @@ function generateDashboardHTML(options) {
7822
7940
 
7823
7941
  // Initialize
7824
7942
  async function initialize() {
7825
- if (!AUTH_TOKEN) {
7943
+ // v0.10.6: gate on BOTH sessionStorage and the server-baked loopback
7944
+ // auto-auth mirror. Pre-fix, a fresh loopback tab had empty
7945
+ // sessionStorage.authToken AND was admitted by the server via
7946
+ // _autoAuthLocalhost \u2014 this single-operand gate redirected to '/'
7947
+ // which reloaded the same page, which redirected again, infinitely.
7948
+ // See generateDashboardHTML() header comment for full threat model.
7949
+ if (!AUTH_TOKEN && !LOOPBACK_AUTH) {
7826
7950
  redirectToLogin();
7827
7951
  return;
7828
7952
  }
@@ -8867,7 +8991,11 @@ var init_dashboard = __esm({
8867
8991
  this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
8868
8992
  this.dashboardHTML = generateDashboardHTML({
8869
8993
  timeoutSeconds: config.timeout_seconds,
8870
- serverVersion: SANCTUARY_VERSION
8994
+ serverVersion: SANCTUARY_VERSION,
8995
+ // Construction-time default; real value is set by setAutoAuthLocalhost()
8996
+ // below (which regenerates this HTML). Default false preserves the
8997
+ // pre-v0.10.6 remote-deployment behavior when auto-auth is not enabled.
8998
+ loopbackAutoAuth: this._autoAuthLocalhost
8871
8999
  });
8872
9000
  this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
8873
9001
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
@@ -8903,6 +9031,11 @@ var init_dashboard = __esm({
8903
9031
  */
8904
9032
  setAutoAuthLocalhost(enabled) {
8905
9033
  this._autoAuthLocalhost = enabled;
9034
+ this.dashboardHTML = generateDashboardHTML({
9035
+ timeoutSeconds: this.config.timeout_seconds,
9036
+ serverVersion: SANCTUARY_VERSION,
9037
+ loopbackAutoAuth: this._autoAuthLocalhost
9038
+ });
8906
9039
  }
8907
9040
  /**
8908
9041
  * v0.10.2: is this request from a loopback interface? We treat the
@@ -9787,6 +9920,27 @@ data: ${JSON.stringify(data)}
9787
9920
  broadcastProtectionStatus(data) {
9788
9921
  this.broadcastSSE("protection-status", data);
9789
9922
  }
9923
+ // ── Mesh-health surface (WP-MVP-3 Follow-up #3) ─────────────────────
9924
+ //
9925
+ // The federation FailureModeDetector pushes per-tick health snapshots and
9926
+ // per-detection alerts here; the existing /events SSE channel transports
9927
+ // them to the browser. No new transport.
9928
+ //
9929
+ // Spec §8 + §9. Spawn-prompt acceptance criterion 7: "Mesh Health dashboard
9930
+ // panel renders via existing SSE /events channel — no new transport. Every
9931
+ // state transition produces an observable SSE event."
9932
+ /** Push a Mesh Health snapshot (full re-render trigger on the client). */
9933
+ broadcastMeshHealth(snapshot) {
9934
+ this.broadcastSSE("mesh-health", snapshot);
9935
+ }
9936
+ /** Push a single failure-mode alert (incremental client update). */
9937
+ broadcastMeshFailureModeAlert(alert) {
9938
+ this.broadcastSSE("mesh-failure-mode-alert", alert);
9939
+ }
9940
+ /** Push a post-recovery prompt update (master rotation hygiene flow). */
9941
+ broadcastMeshPostRecoveryPrompt(prompt) {
9942
+ this.broadcastSSE("mesh-post-recovery-prompt", prompt);
9943
+ }
9790
9944
  /**
9791
9945
  * Open a URL in the system's default browser.
9792
9946
  * Cross-platform: macOS (open), Linux (xdg-open), Windows (start).
@@ -13183,7 +13337,7 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
13183
13337
  const now = (/* @__PURE__ */ new Date()).toISOString();
13184
13338
  const canonicalBytes = canonicalize(outcome);
13185
13339
  const canonicalString = new TextDecoder().decode(canonicalBytes);
13186
- const sha2566 = createCommitment(canonicalString);
13340
+ const sha2568 = createCommitment(canonicalString);
13187
13341
  let pedersenData;
13188
13342
  if (includePedersen && Number.isInteger(outcome.rounds) && outcome.rounds >= 0) {
13189
13343
  const pedersen = createPedersenCommitment(outcome.rounds);
@@ -13195,7 +13349,7 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
13195
13349
  const commitmentPayload = {
13196
13350
  bridge_commitment_id: commitmentId,
13197
13351
  session_id: outcome.session_id,
13198
- sha256_commitment: sha2566.commitment,
13352
+ sha256_commitment: sha2568.commitment,
13199
13353
  terms_hash: outcome.terms_hash,
13200
13354
  committer_did: identity.did,
13201
13355
  committed_at: now,
@@ -13206,8 +13360,8 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
13206
13360
  return {
13207
13361
  bridge_commitment_id: commitmentId,
13208
13362
  session_id: outcome.session_id,
13209
- sha256_commitment: sha2566.commitment,
13210
- blinding_factor: sha2566.blinding_factor,
13363
+ sha256_commitment: sha2568.commitment,
13364
+ blinding_factor: sha2568.blinding_factor,
13211
13365
  committer_did: identity.did,
13212
13366
  signature: toBase64url(signature),
13213
13367
  pedersen_commitment: pedersenData,
@@ -22052,7 +22206,13 @@ function l3Card(l3) {
22052
22206
  );
22053
22207
  }
22054
22208
  function l4Card(l4) {
22055
- const score = l4.score != null ? `<div class="score-block"><span class="score-value">${escHtml2(l4.score)}</span><span class="score-label">Verascore</span></div>` : `<div class="claim-block">${escHtml2(l4.claim_cta ?? "Claim your profile at verascore.ai")}</div>`;
22209
+ let score;
22210
+ if (l4.score != null) {
22211
+ score = `<div class="score-block"><span class="score-value">${escHtml2(l4.score)}</span><span class="score-label">Verascore</span></div>`;
22212
+ } else {
22213
+ const claimText = l4.claim_cta ?? "Claim your profile at verascore.ai";
22214
+ score = `<div class="claim-block"><a class="claim-link" href="${VERASCORE_CLAIM_URL}" target="_blank" rel="noopener noreferrer">${escHtml2(claimText)}</a></div>`;
22215
+ }
22056
22216
  return layerCard(
22057
22217
  l4,
22058
22218
  `<div class="layer-cta">${score}</div>${l4EvidenceBlock(l4)}`
@@ -22714,221 +22874,1349 @@ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px
22714
22874
  </body>
22715
22875
  </html>`;
22716
22876
  }
22717
- var HERO_COPY;
22877
+ var HERO_COPY, VERASCORE_CLAIM_URL;
22718
22878
  var init_html = __esm({
22719
22879
  "src/dashboard/html.ts"() {
22720
22880
  init_multi_html();
22721
22881
  HERO_COPY = "Your agent is protected.";
22882
+ VERASCORE_CLAIM_URL = "https://www.verascore.ai";
22722
22883
  }
22723
22884
  });
22724
22885
 
22725
- // src/dashboard/api.ts
22726
- function constantTimeEquals(a, b) {
22727
- if (a.length !== b.length) return false;
22728
- let diff = 0;
22729
- for (let i = 0; i < a.length; i++) {
22730
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
22731
- }
22732
- return diff === 0;
22886
+ // src/policy-engine/constants.ts
22887
+ function isPolicySlot(value) {
22888
+ return typeof value === "string" && POLICY_SLOTS.includes(value);
22733
22889
  }
22734
- function extractToken(req, url) {
22735
- const header = req.headers.authorization;
22736
- if (header && header.startsWith("Bearer ")) {
22737
- return header.slice(7).trim();
22890
+ var COMPILED_POLICY_SCHEMA_VERSION, POLICY_UPDATE_EVENT_TYPE, POLICY_SLOTS, CHANNEL_TEMPLATE_IDS, BUDGET_UNITS;
22891
+ var init_constants = __esm({
22892
+ "src/policy-engine/constants.ts"() {
22893
+ COMPILED_POLICY_SCHEMA_VERSION = "0.1";
22894
+ POLICY_UPDATE_EVENT_TYPE = "policy_update";
22895
+ POLICY_SLOTS = [
22896
+ "memory",
22897
+ "credentials",
22898
+ "plans",
22899
+ "outputs"
22900
+ ];
22901
+ CHANNEL_TEMPLATE_IDS = [
22902
+ "read-outputs-only",
22903
+ "bidirectional-sync",
22904
+ "credential-share-scoped",
22905
+ "plan-inspect-read-only",
22906
+ "escrow-handoff"
22907
+ ];
22908
+ BUDGET_UNITS = ["tokens", "usd"];
22738
22909
  }
22739
- const q = url.searchParams.get("token");
22740
- return q ?? null;
22741
- }
22742
- function isAuthorized(deps, req, url) {
22743
- if (!deps.authToken) return true;
22744
- const token = extractToken(req, url);
22745
- if (!token) return false;
22746
- return constantTimeEquals(token, deps.authToken);
22747
- }
22748
- function writeJSON(res, status, payload) {
22749
- res.writeHead(status, {
22750
- "Content-Type": "application/json",
22751
- "Cache-Control": "no-store"
22752
- });
22753
- res.end(JSON.stringify(payload));
22754
- }
22755
- function writeText(res, status, body, contentType = "text/plain") {
22756
- res.writeHead(status, {
22757
- "Content-Type": contentType,
22758
- "Cache-Control": "no-store"
22759
- });
22760
- res.end(body);
22910
+ });
22911
+ function isTemplateName(value) {
22912
+ return typeof value === "string" && TEMPLATE_NAMES.includes(value);
22761
22913
  }
22762
- async function handleRequest(deps, req, res) {
22763
- const host = req.headers.host || "localhost";
22764
- const url = new URL(req.url ?? "/", `http://${host}`);
22765
- const method = (req.method ?? "GET").toUpperCase();
22766
- const path = url.pathname;
22767
- if (!isAuthorized(deps, req, url)) {
22768
- writeJSON(res, 401, { error: "unauthorized" });
22769
- return true;
22914
+ function validateMetadata(name, data) {
22915
+ if (typeof data !== "object" || data === null) {
22916
+ throw new TemplateValidationError(name, "template.json must be an object");
22770
22917
  }
22771
- if (method === "GET" && path === "/api/health") {
22772
- writeJSON(res, 200, { ok: true, mode: deps.sources.mode });
22773
- return true;
22918
+ const d = data;
22919
+ if (typeof d.name !== "string" || d.name !== name) {
22920
+ throw new TemplateValidationError(name, `template.json name must be "${name}"`);
22774
22921
  }
22775
- if (method === "GET" && (path === "/" || path === "/index.html")) {
22776
- const snapshot = await getProtectionSnapshot(deps.sources);
22777
- const html = renderDashboardHTML({ snapshot, authToken: deps.authToken });
22778
- writeText(res, 200, html, "text/html; charset=utf-8");
22779
- return true;
22922
+ if (typeof d.version !== "string" || !/^\d+\.\d+\.\d+$/.test(d.version)) {
22923
+ throw new TemplateValidationError(name, "template.json version must be semver");
22780
22924
  }
22781
- if (method === "GET" && path === "/api/snapshot") {
22782
- const snapshot = await getProtectionSnapshot(deps.sources);
22783
- writeJSON(res, 200, snapshot);
22784
- return true;
22925
+ if (typeof d.channel !== "string" || !CHANNEL_TEMPLATE_IDS.includes(d.channel)) {
22926
+ throw new TemplateValidationError(
22927
+ name,
22928
+ `template.json channel must be one of: ${CHANNEL_TEMPLATE_IDS.join(", ")}`
22929
+ );
22785
22930
  }
22786
- const approvalMatch = /^\/api\/approvals\/([^/]+)\/(allow|deny)$/.exec(path);
22787
- if (method === "POST" && approvalMatch) {
22788
- const id = decodeURIComponent(approvalMatch[1]);
22789
- const action = approvalMatch[2];
22790
- if (!deps.approvals) {
22791
- writeJSON(res, 503, { error: "approvals_unavailable" });
22792
- return true;
22931
+ if (typeof d.tier !== "string" || !["A", "B", "C"].includes(d.tier)) {
22932
+ throw new TemplateValidationError(name, "template.json tier must be A, B, or C");
22933
+ }
22934
+ if (typeof d.target_archetype !== "string" || d.target_archetype.length === 0) {
22935
+ throw new TemplateValidationError(name, "template.json target_archetype must be a non-empty string");
22936
+ }
22937
+ if (typeof d.description !== "string" || d.description.length === 0) {
22938
+ throw new TemplateValidationError(name, "template.json description must be a non-empty string");
22939
+ }
22940
+ }
22941
+ function validateDefaults(name, data) {
22942
+ if (typeof data !== "object" || data === null) {
22943
+ throw new TemplateValidationError(name, "defaults.json must be an object");
22944
+ }
22945
+ const d = data;
22946
+ if (!Array.isArray(d.egress)) {
22947
+ throw new TemplateValidationError(name, "defaults.json egress must be an array");
22948
+ }
22949
+ for (const entry of d.egress) {
22950
+ if (typeof entry !== "object" || entry === null) {
22951
+ throw new TemplateValidationError(name, "defaults.json egress entry must be an object");
22793
22952
  }
22794
- const handler = action === "allow" ? deps.approvals.allow : deps.approvals.deny;
22795
- try {
22796
- const ok2 = await handler(id);
22797
- writeJSON(res, ok2 ? 200 : 404, { id, action, ok: ok2 });
22798
- } catch (err) {
22799
- writeJSON(res, 500, { error: "approval_failed", message: err.message });
22953
+ const e = entry;
22954
+ if (typeof e.destination !== "string" || e.destination.length === 0) {
22955
+ throw new TemplateValidationError(name, "egress entry destination must be a non-empty string");
22956
+ }
22957
+ if (!Array.isArray(e.methods)) {
22958
+ throw new TemplateValidationError(name, "egress entry methods must be an array");
22800
22959
  }
22801
- return true;
22802
22960
  }
22803
- if (method === "GET" && path === "/api/stream") {
22804
- await handleStream(deps, res);
22805
- return true;
22961
+ if (typeof d.budgets !== "object" || d.budgets === null) {
22962
+ throw new TemplateValidationError(name, "defaults.json budgets must be an object");
22963
+ }
22964
+ if (typeof d.retention !== "object" || d.retention === null) {
22965
+ throw new TemplateValidationError(name, "defaults.json retention must be an object");
22966
+ }
22967
+ const ret = d.retention;
22968
+ if (typeof ret.windows !== "object" || ret.windows === null) {
22969
+ throw new TemplateValidationError(name, "defaults.json retention.windows must be an object");
22806
22970
  }
22807
- return false;
22808
22971
  }
22809
- async function handleStream(deps, res) {
22810
- res.writeHead(200, {
22811
- "Content-Type": "text/event-stream",
22812
- "Cache-Control": "no-cache, no-transform",
22813
- Connection: "keep-alive",
22814
- "X-Accel-Buffering": "no"
22815
- });
22816
- const snapshot = await getProtectionSnapshot(deps.sources);
22817
- res.write(`event: snapshot
22818
- data: ${JSON.stringify(snapshot)}
22819
-
22820
- `);
22821
- const unsubscribe = deps.onEvent ? deps.onEvent((event) => {
22822
- try {
22823
- res.write(`event: ${event.type}
22824
- data: ${JSON.stringify(event.data)}
22825
-
22826
- `);
22827
- } catch {
22972
+ function validateCommitments(name, data) {
22973
+ if (typeof data !== "object" || data === null) {
22974
+ throw new TemplateValidationError(name, "commitments.json must be an object");
22975
+ }
22976
+ const d = data;
22977
+ if (!Array.isArray(d.shapes)) {
22978
+ throw new TemplateValidationError(name, "commitments.json shapes must be an array");
22979
+ }
22980
+ for (const shape of d.shapes) {
22981
+ if (typeof shape !== "object" || shape === null) {
22982
+ throw new TemplateValidationError(name, "commitment shape must be an object");
22828
22983
  }
22829
- }) : () => {
22830
- };
22831
- const keepAlive = setInterval(() => {
22832
- try {
22833
- res.write(": keepalive\n\n");
22834
- } catch {
22984
+ const s = shape;
22985
+ if (typeof s.commitment_class !== "string" || s.commitment_class.length === 0) {
22986
+ throw new TemplateValidationError(name, "commitment shape commitment_class must be a non-empty string");
22835
22987
  }
22836
- }, 25e3);
22837
- const cleanup = () => {
22838
- clearInterval(keepAlive);
22839
- unsubscribe();
22840
- };
22841
- res.on("close", cleanup);
22842
- res.on("error", cleanup);
22988
+ if (typeof s.example_deliverable !== "string") {
22989
+ throw new TemplateValidationError(name, "commitment shape example_deliverable must be a string");
22990
+ }
22991
+ if (typeof s.example_deadline_or_terminal !== "string") {
22992
+ throw new TemplateValidationError(name, "commitment shape example_deadline_or_terminal must be a string");
22993
+ }
22994
+ }
22843
22995
  }
22844
- var init_api = __esm({
22845
- "src/dashboard/api.ts"() {
22846
- init_aggregator();
22847
- init_html();
22996
+ function lintOnboarding(_name, content) {
22997
+ const violations = [];
22998
+ if (content.includes("\u2014")) {
22999
+ violations.push("onboarding.md contains em-dashes (U+2014). Replace with commas, semicolons, colons, or parentheses.");
22848
23000
  }
22849
- });
22850
- async function startDashboardServer(options) {
22851
- const port = options.port ?? DEFAULT_PORT;
22852
- const host = options.host ?? DEFAULT_HOST;
22853
- const listeners = /* @__PURE__ */ new Set();
22854
- const onEvent = (listener) => {
22855
- listeners.add(listener);
22856
- return () => listeners.delete(listener);
22857
- };
22858
- const publish = (event) => {
22859
- for (const listener of listeners) {
22860
- try {
22861
- listener(event);
22862
- } catch {
22863
- }
22864
- }
22865
- };
22866
- const deps = {
22867
- sources: options.sources,
22868
- authToken: options.authToken,
22869
- approvals: options.approvals,
22870
- onEvent
23001
+ if (content.includes("\u2013")) {
23002
+ violations.push("onboarding.md contains en-dashes (U+2013). Replace with hyphens or other punctuation.");
23003
+ }
23004
+ return { clean: violations.length === 0, violations };
23005
+ }
23006
+ function resolveTemplatesDir() {
23007
+ const thisFile = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
23008
+ const thisDir = path.dirname(thisFile);
23009
+ if (thisDir.includes("/dist/")) {
23010
+ return thisDir.replace("/dist/templates", "/src/templates");
23011
+ }
23012
+ return thisDir;
23013
+ }
23014
+ function loadTemplateBundle(name, templatesDir) {
23015
+ const dir = path.join(templatesDir, name);
23016
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
23017
+ throw new TemplateValidationError(name, `template directory not found: ${dir}`);
23018
+ }
23019
+ const metadataRaw = JSON.parse(fs.readFileSync(path.join(dir, "template.json"), "utf-8"));
23020
+ validateMetadata(name, metadataRaw);
23021
+ const policyEnglish = fs.readFileSync(path.join(dir, "policy.md"), "utf-8").trim();
23022
+ if (policyEnglish.length === 0) {
23023
+ throw new TemplateValidationError(name, "policy.md must not be empty");
23024
+ }
23025
+ const defaultsRaw = JSON.parse(fs.readFileSync(path.join(dir, "defaults.json"), "utf-8"));
23026
+ validateDefaults(name, defaultsRaw);
23027
+ const commitmentsRaw = JSON.parse(fs.readFileSync(path.join(dir, "commitments.json"), "utf-8"));
23028
+ validateCommitments(name, commitmentsRaw);
23029
+ const onboarding = fs.readFileSync(path.join(dir, "onboarding.md"), "utf-8").trim();
23030
+ if (onboarding.length === 0) {
23031
+ throw new TemplateValidationError(name, "onboarding.md must not be empty");
23032
+ }
23033
+ const lint = lintOnboarding(name, onboarding);
23034
+ if (!lint.clean) {
23035
+ throw new TemplateValidationError(name, lint.violations.join("; "));
23036
+ }
23037
+ return {
23038
+ metadata: metadataRaw,
23039
+ policy_english: policyEnglish,
23040
+ defaults: defaultsRaw,
23041
+ commitments: commitmentsRaw,
23042
+ onboarding
22871
23043
  };
22872
- const server = http.createServer(async (req, res) => {
22873
- try {
22874
- const served = await handleRequest(deps, req, res);
22875
- if (!served) {
22876
- res.writeHead(404, { "Content-Type": "application/json" });
22877
- res.end(JSON.stringify({ error: "not_found", path: req.url }));
22878
- }
22879
- } catch (err) {
22880
- try {
22881
- res.writeHead(500, { "Content-Type": "application/json" });
22882
- res.end(JSON.stringify({ error: "internal", message: err.message }));
22883
- } catch {
22884
- }
22885
- }
23044
+ }
23045
+ function ensureLoaded() {
23046
+ if (_cache) return _cache;
23047
+ _cache = /* @__PURE__ */ new Map();
23048
+ const dir = resolveTemplatesDir();
23049
+ for (const name of TEMPLATE_NAMES) {
23050
+ _cache.set(name, loadTemplateBundle(name, dir));
23051
+ }
23052
+ return _cache;
23053
+ }
23054
+ function listTemplates() {
23055
+ const cache = ensureLoaded();
23056
+ return TEMPLATE_NAMES.map((name) => {
23057
+ const bundle = cache.get(name);
23058
+ return {
23059
+ metadata: bundle.metadata,
23060
+ onboarding: bundle.onboarding
23061
+ };
22886
23062
  });
22887
- await new Promise((resolve2, reject) => {
22888
- server.once("error", reject);
22889
- server.listen(port, host, () => {
22890
- server.off("error", reject);
22891
- resolve2();
22892
- });
23063
+ }
23064
+ function getTemplate2(name) {
23065
+ if (!isTemplateName(name)) return null;
23066
+ const cache = ensureLoaded();
23067
+ return cache.get(name) ?? null;
23068
+ }
23069
+ function getTemplateEntry(name) {
23070
+ const bundle = getTemplate2(name);
23071
+ if (!bundle) return null;
23072
+ return { metadata: bundle.metadata, onboarding: bundle.onboarding };
23073
+ }
23074
+ var TEMPLATE_NAMES, TemplateValidationError, _cache;
23075
+ var init_registry2 = __esm({
23076
+ "src/templates/registry.ts"() {
23077
+ init_constants();
23078
+ TEMPLATE_NAMES = [
23079
+ "research-assistant",
23080
+ "coding-assistant",
23081
+ "ops-runner",
23082
+ "planner",
23083
+ "handoff-coordinator",
23084
+ "x-miner",
23085
+ "github-miner"
23086
+ ];
23087
+ TemplateValidationError = class extends Error {
23088
+ constructor(templateName, message) {
23089
+ super(`template "${templateName}": ${message}`);
23090
+ this.name = "TemplateValidationError";
23091
+ }
23092
+ };
23093
+ _cache = null;
23094
+ }
23095
+ });
23096
+
23097
+ // src/policy-engine/null-policy.ts
23098
+ function buildNullPolicy(params) {
23099
+ const denyRule = (slot) => ({
23100
+ slot,
23101
+ mode: "deny",
23102
+ grants: []
22893
23103
  });
22894
- const actualPort = (() => {
22895
- const addr = server.address();
22896
- if (addr && typeof addr === "object") return addr.port;
22897
- return port;
22898
- })();
22899
- const url = `http://${host}:${actualPort}`;
22900
23104
  return {
22901
- url,
22902
- port: actualPort,
22903
- host,
22904
- stop: () => new Promise((resolve2, reject) => {
22905
- server.close((err) => err ? reject(err) : resolve2());
22906
- }),
22907
- publish,
22908
- publishActivity: (entry) => publish({ type: "activity", data: entry }),
22909
- publishApproval: (approval) => publish({ type: "approval", data: approval })
23105
+ schema_version: "0.1",
23106
+ agent_id: params.agent_id,
23107
+ fortress_id: params.fortress_id,
23108
+ policy_version: 0,
23109
+ slots: {
23110
+ memory: denyRule("memory"),
23111
+ credentials: denyRule("credentials"),
23112
+ plans: denyRule("plans"),
23113
+ outputs: denyRule("outputs")
23114
+ },
23115
+ capabilities: {
23116
+ concordia_commitment_classes: [],
23117
+ honeypot_skill_ids: [],
23118
+ is_sentinel: false
23119
+ },
23120
+ auto_trigger_ladder: {
23121
+ honeypot_auto_freeze: true,
23122
+ threshold_rule_action: "operator_approved",
23123
+ ml_anomaly_action: "operator_approved"
23124
+ },
23125
+ source_english: "(null policy \u2014 no operator authoring yet; hermetic default denies all four slots)",
23126
+ compiled_at: "1970-01-01T00:00:00.000Z"
22910
23127
  };
22911
23128
  }
22912
- var DEFAULT_PORT, DEFAULT_HOST;
22913
- var init_server = __esm({
22914
- "src/dashboard/server.ts"() {
22915
- init_api();
22916
- DEFAULT_PORT = 3501;
22917
- DEFAULT_HOST = "127.0.0.1";
23129
+ var init_null_policy = __esm({
23130
+ "src/policy-engine/null-policy.ts"() {
22918
23131
  }
22919
23132
  });
22920
23133
 
22921
- // src/dashboard/index.ts
22922
- async function startDashboard(options) {
22923
- const activity = options.initialActivity ? [...options.initialActivity] : [];
22924
- const pending = options.initialPendingApprovals ? [...options.initialPendingApprovals] : [];
22925
- const sources = {
22926
- mode: options.mode,
22927
- server_version: options.serverVersion,
22928
- ...options.auditLog ? { auditLog: options.auditLog } : {},
22929
- ...options.identityManager ? { identityManager: options.identityManager } : {},
22930
- ...options.clientManager ? { clientManager: options.clientManager } : {},
22931
- ...options.baseline ? { baseline: options.baseline } : {},
23134
+ // src/policy-engine/channel-templates.ts
23135
+ function basePolicy(params) {
23136
+ if (params.merge_into) {
23137
+ const p2 = {
23138
+ ...params.merge_into,
23139
+ policy_version: params.policy_version,
23140
+ compiled_at: (/* @__PURE__ */ new Date()).toISOString(),
23141
+ slots: {
23142
+ memory: cloneRule(params.merge_into.slots.memory),
23143
+ credentials: cloneRule(params.merge_into.slots.credentials),
23144
+ plans: cloneRule(params.merge_into.slots.plans),
23145
+ outputs: cloneRule(params.merge_into.slots.outputs)
23146
+ },
23147
+ capabilities: {
23148
+ ...params.merge_into.capabilities,
23149
+ concordia_commitment_classes: [
23150
+ ...params.merge_into.capabilities.concordia_commitment_classes
23151
+ ],
23152
+ honeypot_skill_ids: [...params.merge_into.capabilities.honeypot_skill_ids]
23153
+ },
23154
+ auto_trigger_ladder: { ...params.merge_into.auto_trigger_ladder }
23155
+ };
23156
+ if (params.parent_version !== void 0) p2.parent_version = params.parent_version;
23157
+ else delete p2.parent_version;
23158
+ return p2;
23159
+ }
23160
+ const p = buildNullPolicy({
23161
+ agent_id: params.agent_id,
23162
+ fortress_id: params.fortress_id
23163
+ });
23164
+ p.policy_version = params.policy_version;
23165
+ p.compiled_at = (/* @__PURE__ */ new Date()).toISOString();
23166
+ if (params.parent_version !== void 0) p.parent_version = params.parent_version;
23167
+ return p;
23168
+ }
23169
+ function cloneRule(r) {
23170
+ return {
23171
+ slot: r.slot,
23172
+ mode: r.mode,
23173
+ grants: r.grants.map((g) => ({ ...g, scope: g.scope ? { ...g.scope } : void 0 }))
23174
+ };
23175
+ }
23176
+ function grantOn(policy, slot, grant) {
23177
+ const rule = policy.slots[slot];
23178
+ rule.mode = "grant";
23179
+ const dup = rule.grants.find(
23180
+ (g) => g.counterparty === grant.counterparty && g.action === grant.action
23181
+ );
23182
+ if (dup) {
23183
+ if (grant.scope) {
23184
+ dup.scope = { ...dup.scope ?? {}, ...grant.scope };
23185
+ }
23186
+ return;
23187
+ }
23188
+ rule.grants.push(grant);
23189
+ rule.grants.sort((a, b) => {
23190
+ if (a.counterparty === b.counterparty) return a.action.localeCompare(b.action);
23191
+ return a.counterparty.localeCompare(b.counterparty);
23192
+ });
23193
+ }
23194
+ function applyChannelTemplate(id, params) {
23195
+ const entry = REGISTRY[id];
23196
+ if (!entry) {
23197
+ throw new Error(`unknown channel template: ${id}`);
23198
+ }
23199
+ return entry.factory(params);
23200
+ }
23201
+ var readOutputsOnly, bidirectionalSync, credentialShareScoped, planInspectReadOnly, escrowHandoff, REGISTRY;
23202
+ var init_channel_templates = __esm({
23203
+ "src/policy-engine/channel-templates.ts"() {
23204
+ init_constants();
23205
+ init_null_policy();
23206
+ readOutputsOnly = (params) => {
23207
+ const p = basePolicy(params);
23208
+ p.source_english = `${params.counterparty} may read ${params.agent_id}'s outputs \u2014 read-only, no other access.`;
23209
+ grantOn(p, "outputs", {
23210
+ counterparty: params.counterparty,
23211
+ action: "read",
23212
+ ...params.scope ? { scope: { ...params.scope } } : {}
23213
+ });
23214
+ return p;
23215
+ };
23216
+ bidirectionalSync = (params) => {
23217
+ const p = basePolicy(params);
23218
+ p.source_english = `Bidirectional sync between ${params.agent_id} and ${params.counterparty} on memory + outputs.`;
23219
+ for (const slot of ["memory", "outputs"]) {
23220
+ for (const action of ["read", "subscribe"]) {
23221
+ grantOn(p, slot, {
23222
+ counterparty: params.counterparty,
23223
+ action,
23224
+ ...params.scope ? { scope: { ...params.scope } } : {}
23225
+ });
23226
+ }
23227
+ }
23228
+ return p;
23229
+ };
23230
+ credentialShareScoped = (params) => {
23231
+ const p = basePolicy(params);
23232
+ const credentialId = (params.scope && typeof params.scope.credential_id === "string" ? params.scope.credential_id : void 0) ?? "(unspecified)";
23233
+ p.source_english = `${params.agent_id} may share credential "${credentialId}" with ${params.counterparty}; no other credential access.`;
23234
+ grantOn(p, "credentials", {
23235
+ counterparty: params.counterparty,
23236
+ action: "share",
23237
+ scope: {
23238
+ credential_id: credentialId,
23239
+ ...params.scope ?? {}
23240
+ }
23241
+ });
23242
+ return p;
23243
+ };
23244
+ planInspectReadOnly = (params) => {
23245
+ const p = basePolicy(params);
23246
+ p.source_english = `${params.counterparty} may read-only inspect ${params.agent_id}'s plans.`;
23247
+ grantOn(p, "plans", {
23248
+ counterparty: params.counterparty,
23249
+ action: "read",
23250
+ ...params.scope ? { scope: { ...params.scope } } : {}
23251
+ });
23252
+ return p;
23253
+ };
23254
+ escrowHandoff = (params) => {
23255
+ const p = basePolicy(params);
23256
+ p.source_english = `${params.agent_id} may escrow-handoff outputs and plan-read to ${params.counterparty}. Commitment class: intra-mesh-escrow.`;
23257
+ grantOn(p, "plans", {
23258
+ counterparty: params.counterparty,
23259
+ action: "read",
23260
+ ...params.scope ? { scope: { ...params.scope } } : {}
23261
+ });
23262
+ for (const action of ["read", "subscribe"]) {
23263
+ grantOn(p, "outputs", {
23264
+ counterparty: params.counterparty,
23265
+ action,
23266
+ ...params.scope ? { scope: { ...params.scope } } : {}
23267
+ });
23268
+ }
23269
+ const cls = "intra-mesh-escrow";
23270
+ if (!p.capabilities.concordia_commitment_classes.includes(cls)) {
23271
+ p.capabilities.concordia_commitment_classes.push(cls);
23272
+ p.capabilities.concordia_commitment_classes.sort();
23273
+ }
23274
+ return p;
23275
+ };
23276
+ REGISTRY = {
23277
+ "read-outputs-only": {
23278
+ id: "read-outputs-only",
23279
+ label: "Read outputs only",
23280
+ description: "Counterparty may read this agent's outputs. No memory, credentials, or plans access.",
23281
+ factory: readOutputsOnly
23282
+ },
23283
+ "bidirectional-sync": {
23284
+ id: "bidirectional-sync",
23285
+ label: "Bidirectional memory + output sync",
23286
+ description: "Both agents may read and subscribe to each other's memory and outputs. Credentials and plans remain hermetic.",
23287
+ factory: bidirectionalSync
23288
+ },
23289
+ "credential-share-scoped": {
23290
+ id: "credential-share-scoped",
23291
+ label: "Scoped credential share",
23292
+ description: "Share one specific credential with counterparty. Requires scope.credential_id. No broad credential access.",
23293
+ factory: credentialShareScoped
23294
+ },
23295
+ "plan-inspect-read-only": {
23296
+ id: "plan-inspect-read-only",
23297
+ label: "Plan inspect (read-only)",
23298
+ description: "Counterparty may read-only inspect this agent's plans. Intended for supervisor / sentinel patterns.",
23299
+ factory: planInspectReadOnly
23300
+ },
23301
+ "escrow-handoff": {
23302
+ id: "escrow-handoff",
23303
+ label: "Escrow handoff",
23304
+ description: "Escrow-style handoff. Counterparty reads plan, reads + subscribes to outputs, and the intra-mesh-escrow commitment class is declared. Memory and credentials remain hermetic.",
23305
+ factory: escrowHandoff
23306
+ }
23307
+ };
23308
+ }
23309
+ });
23310
+
23311
+ // src/mesh/errors.ts
23312
+ var MeshError, MeshEnvelopeError, MeshReservedExtensionKeyError, MeshReservedEventTypeError;
23313
+ var init_errors = __esm({
23314
+ "src/mesh/errors.ts"() {
23315
+ MeshError = class extends Error {
23316
+ constructor(message) {
23317
+ super(message);
23318
+ this.name = "MeshError";
23319
+ }
23320
+ };
23321
+ MeshEnvelopeError = class extends MeshError {
23322
+ constructor(message) {
23323
+ super(message);
23324
+ this.name = "MeshEnvelopeError";
23325
+ }
23326
+ };
23327
+ MeshReservedExtensionKeyError = class extends MeshEnvelopeError {
23328
+ constructor(key) {
23329
+ super(
23330
+ `v0.1 emitters MUST NOT populate reserved extension_envelope key: ${key}`
23331
+ );
23332
+ this.name = "MeshReservedExtensionKeyError";
23333
+ }
23334
+ };
23335
+ MeshReservedEventTypeError = class extends MeshEnvelopeError {
23336
+ constructor(eventType) {
23337
+ super(
23338
+ `v0.1 emitters MUST NOT emit reserved-namespace event_type: ${eventType}`
23339
+ );
23340
+ this.name = "MeshReservedEventTypeError";
23341
+ }
23342
+ };
23343
+ }
23344
+ });
23345
+
23346
+ // src/mesh/canonical-json.ts
23347
+ function canonicalize2(value) {
23348
+ if (value === void 0) {
23349
+ throw new MeshCanonicalJsonError(
23350
+ "canonicalize(): top-level undefined is not serializable"
23351
+ );
23352
+ }
23353
+ return encode(value);
23354
+ }
23355
+ function encode(value) {
23356
+ if (value === null) return "null";
23357
+ if (typeof value === "boolean") return value ? "true" : "false";
23358
+ if (typeof value === "number") {
23359
+ if (!Number.isFinite(value)) {
23360
+ throw new MeshCanonicalJsonError(
23361
+ `canonicalize(): non-finite number (${String(value)}) is not serializable`
23362
+ );
23363
+ }
23364
+ return JSON.stringify(value);
23365
+ }
23366
+ if (typeof value === "string") return JSON.stringify(value);
23367
+ if (Array.isArray(value)) return encodeArray(value);
23368
+ if (typeof value === "object") return encodeObject(value);
23369
+ throw new MeshCanonicalJsonError(
23370
+ `canonicalize(): unsupported type ${typeof value}`
23371
+ );
23372
+ }
23373
+ function encodeArray(arr) {
23374
+ const parts = [];
23375
+ for (const item of arr) {
23376
+ parts.push(item === void 0 ? "null" : encode(item));
23377
+ }
23378
+ return "[" + parts.join(",") + "]";
23379
+ }
23380
+ function encodeObject(obj) {
23381
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
23382
+ const parts = [];
23383
+ for (const k of keys) {
23384
+ parts.push(JSON.stringify(k) + ":" + encode(obj[k]));
23385
+ }
23386
+ return "{" + parts.join(",") + "}";
23387
+ }
23388
+ function canonicalizeToBytes(value) {
23389
+ return new TextEncoder().encode(canonicalize2(value));
23390
+ }
23391
+ var MeshCanonicalJsonError;
23392
+ var init_canonical_json = __esm({
23393
+ "src/mesh/canonical-json.ts"() {
23394
+ init_errors();
23395
+ MeshCanonicalJsonError = class extends MeshError {
23396
+ constructor(message) {
23397
+ super(message);
23398
+ this.name = "MeshCanonicalJsonError";
23399
+ }
23400
+ };
23401
+ }
23402
+ });
23403
+
23404
+ // src/policy-engine/errors.ts
23405
+ var PolicyEngineError, CompiledPolicyShapeError;
23406
+ var init_errors2 = __esm({
23407
+ "src/policy-engine/errors.ts"() {
23408
+ PolicyEngineError = class extends Error {
23409
+ constructor(message) {
23410
+ super(message);
23411
+ this.name = "PolicyEngineError";
23412
+ }
23413
+ };
23414
+ CompiledPolicyShapeError = class extends PolicyEngineError {
23415
+ constructor(message) {
23416
+ super(message);
23417
+ this.name = "CompiledPolicyShapeError";
23418
+ }
23419
+ };
23420
+ }
23421
+ });
23422
+
23423
+ // src/policy-engine/canonical-policy.ts
23424
+ function checkSlotGrant(value, path) {
23425
+ if (typeof value !== "object" || value === null) {
23426
+ throw new CompiledPolicyShapeError(`${path}: grant must be an object`);
23427
+ }
23428
+ const g = value;
23429
+ if (typeof g.counterparty !== "string" || g.counterparty.length === 0) {
23430
+ throw new CompiledPolicyShapeError(
23431
+ `${path}.counterparty must be a non-empty string`
23432
+ );
23433
+ }
23434
+ if (typeof g.action !== "string" || g.action.length === 0) {
23435
+ throw new CompiledPolicyShapeError(
23436
+ `${path}.action must be a non-empty string`
23437
+ );
23438
+ }
23439
+ if (g.scope !== void 0) {
23440
+ if (typeof g.scope !== "object" || g.scope === null || Array.isArray(g.scope)) {
23441
+ throw new CompiledPolicyShapeError(
23442
+ `${path}.scope must be an object when present`
23443
+ );
23444
+ }
23445
+ }
23446
+ if (g.max_uses_per_day !== void 0) {
23447
+ if (typeof g.max_uses_per_day !== "number" || !Number.isInteger(g.max_uses_per_day) || g.max_uses_per_day < 0) {
23448
+ throw new CompiledPolicyShapeError(
23449
+ `${path}.max_uses_per_day must be a non-negative integer when present`
23450
+ );
23451
+ }
23452
+ }
23453
+ }
23454
+ function checkSlotRule(slot, value, path) {
23455
+ if (typeof value !== "object" || value === null) {
23456
+ throw new CompiledPolicyShapeError(`${path}: slot rule must be an object`);
23457
+ }
23458
+ const r = value;
23459
+ if (r.slot !== slot) {
23460
+ throw new CompiledPolicyShapeError(
23461
+ `${path}.slot expected ${slot}, got ${String(r.slot)}`
23462
+ );
23463
+ }
23464
+ if (r.mode !== "deny" && r.mode !== "grant") {
23465
+ throw new CompiledPolicyShapeError(
23466
+ `${path}.mode must be "deny" or "grant"`
23467
+ );
23468
+ }
23469
+ if (!Array.isArray(r.grants)) {
23470
+ throw new CompiledPolicyShapeError(
23471
+ `${path}.grants must be an array (possibly empty)`
23472
+ );
23473
+ }
23474
+ if (r.mode === "deny" && r.grants.length > 0) {
23475
+ throw new CompiledPolicyShapeError(
23476
+ `${path}: deny-mode slot rule must carry zero grants`
23477
+ );
23478
+ }
23479
+ r.grants.forEach((g, i) => checkSlotGrant(g, `${path}.grants[${i}]`));
23480
+ }
23481
+ function validateCompiledPolicyShape(candidate) {
23482
+ if (typeof candidate !== "object" || candidate === null) {
23483
+ throw new CompiledPolicyShapeError("compiled policy must be an object");
23484
+ }
23485
+ const p = candidate;
23486
+ if (p.schema_version !== COMPILED_POLICY_SCHEMA_VERSION) {
23487
+ throw new CompiledPolicyShapeError(
23488
+ `schema_version ${String(p.schema_version)} not supported at v0.1 (expected "${COMPILED_POLICY_SCHEMA_VERSION}")`
23489
+ );
23490
+ }
23491
+ if (typeof p.agent_id !== "string" || p.agent_id.length === 0) {
23492
+ throw new CompiledPolicyShapeError("agent_id must be a non-empty string");
23493
+ }
23494
+ if (typeof p.fortress_id !== "string" || p.fortress_id.length === 0) {
23495
+ throw new CompiledPolicyShapeError(
23496
+ "fortress_id must be a non-empty string"
23497
+ );
23498
+ }
23499
+ if (typeof p.policy_version !== "number" || !Number.isInteger(p.policy_version) || p.policy_version < 0) {
23500
+ throw new CompiledPolicyShapeError(
23501
+ "policy_version must be a non-negative integer"
23502
+ );
23503
+ }
23504
+ if (p.parent_version !== void 0 && (typeof p.parent_version !== "number" || !Number.isInteger(p.parent_version) || p.parent_version < 0)) {
23505
+ throw new CompiledPolicyShapeError(
23506
+ "parent_version must be a non-negative integer when present"
23507
+ );
23508
+ }
23509
+ if (typeof p.slots !== "object" || p.slots === null) {
23510
+ throw new CompiledPolicyShapeError("slots must be an object");
23511
+ }
23512
+ const slots = p.slots;
23513
+ const slotKeys = Object.keys(slots).sort();
23514
+ const expectedSlotKeys = [...POLICY_SLOTS].sort();
23515
+ if (slotKeys.length !== expectedSlotKeys.length || !slotKeys.every((k, i) => k === expectedSlotKeys[i])) {
23516
+ throw new CompiledPolicyShapeError(
23517
+ `slots must contain exactly ${expectedSlotKeys.join(", ")}; got ${slotKeys.join(", ") || "(none)"}`
23518
+ );
23519
+ }
23520
+ for (const slot of POLICY_SLOTS) {
23521
+ checkSlotRule(slot, slots[slot], `slots.${slot}`);
23522
+ }
23523
+ if (typeof p.capabilities !== "object" || p.capabilities === null) {
23524
+ throw new CompiledPolicyShapeError("capabilities must be an object");
23525
+ }
23526
+ const caps = p.capabilities;
23527
+ if (!Array.isArray(caps.concordia_commitment_classes) || !caps.concordia_commitment_classes.every((c) => typeof c === "string")) {
23528
+ throw new CompiledPolicyShapeError(
23529
+ "capabilities.concordia_commitment_classes must be a string[]"
23530
+ );
23531
+ }
23532
+ if (!Array.isArray(caps.honeypot_skill_ids) || !caps.honeypot_skill_ids.every((c) => typeof c === "string")) {
23533
+ throw new CompiledPolicyShapeError(
23534
+ "capabilities.honeypot_skill_ids must be a string[]"
23535
+ );
23536
+ }
23537
+ if (typeof caps.is_sentinel !== "boolean") {
23538
+ throw new CompiledPolicyShapeError(
23539
+ "capabilities.is_sentinel must be boolean"
23540
+ );
23541
+ }
23542
+ if (typeof p.auto_trigger_ladder !== "object" || p.auto_trigger_ladder === null) {
23543
+ throw new CompiledPolicyShapeError(
23544
+ "auto_trigger_ladder must be an object"
23545
+ );
23546
+ }
23547
+ const lad = p.auto_trigger_ladder;
23548
+ if (typeof lad.honeypot_auto_freeze !== "boolean") {
23549
+ throw new CompiledPolicyShapeError(
23550
+ "auto_trigger_ladder.honeypot_auto_freeze must be boolean"
23551
+ );
23552
+ }
23553
+ if (lad.threshold_rule_action !== "operator_approved" && lad.threshold_rule_action !== "auto") {
23554
+ throw new CompiledPolicyShapeError(
23555
+ 'auto_trigger_ladder.threshold_rule_action must be "operator_approved" or "auto"'
23556
+ );
23557
+ }
23558
+ if (lad.ml_anomaly_action !== "operator_approved" && lad.ml_anomaly_action !== "auto") {
23559
+ throw new CompiledPolicyShapeError(
23560
+ 'auto_trigger_ladder.ml_anomaly_action must be "operator_approved" or "auto"'
23561
+ );
23562
+ }
23563
+ if (typeof p.source_english !== "string") {
23564
+ throw new CompiledPolicyShapeError("source_english must be a string");
23565
+ }
23566
+ if (typeof p.compiled_at !== "string" || Number.isNaN(Date.parse(p.compiled_at))) {
23567
+ throw new CompiledPolicyShapeError(
23568
+ "compiled_at must be an ISO8601 timestamp string"
23569
+ );
23570
+ }
23571
+ for (const key of slotKeys) {
23572
+ if (!isPolicySlot(key)) {
23573
+ throw new CompiledPolicyShapeError(
23574
+ `slots.${key} is not a recognized policy slot`
23575
+ );
23576
+ }
23577
+ }
23578
+ if (p.egress !== void 0) {
23579
+ checkEgressPolicy(p.egress, "egress");
23580
+ }
23581
+ if (p.budgets !== void 0) {
23582
+ checkBudgetPolicy(p.budgets, "budgets");
23583
+ }
23584
+ if (p.retention !== void 0) {
23585
+ checkRetentionPolicy(p.retention, "retention");
23586
+ }
23587
+ }
23588
+ function checkEgressPolicy(value, path) {
23589
+ if (typeof value !== "object" || value === null) {
23590
+ throw new CompiledPolicyShapeError(`${path} must be an object`);
23591
+ }
23592
+ const eg = value;
23593
+ if (!Array.isArray(eg.allowlist)) {
23594
+ throw new CompiledPolicyShapeError(`${path}.allowlist must be an array`);
23595
+ }
23596
+ for (let i = 0; i < eg.allowlist.length; i++) {
23597
+ const rule = eg.allowlist[i];
23598
+ if (typeof rule !== "object" || rule === null) {
23599
+ throw new CompiledPolicyShapeError(
23600
+ `${path}.allowlist[${i}] must be an object`
23601
+ );
23602
+ }
23603
+ const r = rule;
23604
+ if (typeof r.destination !== "string" || r.destination.length === 0) {
23605
+ throw new CompiledPolicyShapeError(
23606
+ `${path}.allowlist[${i}].destination must be a non-empty string`
23607
+ );
23608
+ }
23609
+ if (!Array.isArray(r.methods) || !r.methods.every((m) => typeof m === "string")) {
23610
+ throw new CompiledPolicyShapeError(
23611
+ `${path}.allowlist[${i}].methods must be a string[]`
23612
+ );
23613
+ }
23614
+ }
23615
+ }
23616
+ function checkBudgetLimit(value, path) {
23617
+ if (typeof value !== "object" || value === null) {
23618
+ throw new CompiledPolicyShapeError(`${path} must be an object`);
23619
+ }
23620
+ const lim = value;
23621
+ if (typeof lim.amount !== "number" || lim.amount <= 0 || !Number.isFinite(lim.amount)) {
23622
+ throw new CompiledPolicyShapeError(
23623
+ `${path}.amount must be a positive finite number`
23624
+ );
23625
+ }
23626
+ if (!BUDGET_UNITS.includes(lim.unit)) {
23627
+ throw new CompiledPolicyShapeError(
23628
+ `${path}.unit must be one of: ${BUDGET_UNITS.join(", ")}`
23629
+ );
23630
+ }
23631
+ if (lim.soft_warn_threshold !== void 0) {
23632
+ if (typeof lim.soft_warn_threshold !== "number" || lim.soft_warn_threshold <= 0 || lim.soft_warn_threshold >= 1) {
23633
+ throw new CompiledPolicyShapeError(
23634
+ `${path}.soft_warn_threshold must be in (0, 1) when present`
23635
+ );
23636
+ }
23637
+ }
23638
+ }
23639
+ function checkBudgetPolicy(value, path) {
23640
+ if (typeof value !== "object" || value === null) {
23641
+ throw new CompiledPolicyShapeError(`${path} must be an object`);
23642
+ }
23643
+ const bp = value;
23644
+ if (bp.daily !== void 0) checkBudgetLimit(bp.daily, `${path}.daily`);
23645
+ if (bp.monthly !== void 0) checkBudgetLimit(bp.monthly, `${path}.monthly`);
23646
+ if (bp.daily === void 0 && bp.monthly === void 0) {
23647
+ throw new CompiledPolicyShapeError(
23648
+ `${path} must have at least one of daily or monthly`
23649
+ );
23650
+ }
23651
+ }
23652
+ function checkRetentionPolicy(value, path) {
23653
+ if (typeof value !== "object" || value === null) {
23654
+ throw new CompiledPolicyShapeError(`${path} must be an object`);
23655
+ }
23656
+ const rp = value;
23657
+ if (typeof rp.windows !== "object" || rp.windows === null) {
23658
+ throw new CompiledPolicyShapeError(`${path}.windows must be an object`);
23659
+ }
23660
+ const wins = rp.windows;
23661
+ for (const key of Object.keys(wins)) {
23662
+ if (!isPolicySlot(key)) {
23663
+ throw new CompiledPolicyShapeError(
23664
+ `${path}.windows.${key} is not a recognized policy slot`
23665
+ );
23666
+ }
23667
+ const w = wins[key];
23668
+ if (typeof w !== "object" || w === null) {
23669
+ throw new CompiledPolicyShapeError(
23670
+ `${path}.windows.${key} must be an object`
23671
+ );
23672
+ }
23673
+ const win = w;
23674
+ if (typeof win.max_age_seconds !== "number" || !Number.isInteger(win.max_age_seconds) || win.max_age_seconds <= 0) {
23675
+ throw new CompiledPolicyShapeError(
23676
+ `${path}.windows.${key}.max_age_seconds must be a positive integer`
23677
+ );
23678
+ }
23679
+ if (win.archive !== void 0 && typeof win.archive !== "boolean") {
23680
+ throw new CompiledPolicyShapeError(
23681
+ `${path}.windows.${key}.archive must be boolean when present`
23682
+ );
23683
+ }
23684
+ }
23685
+ }
23686
+ function encodePolicyBlob(policy) {
23687
+ validateCompiledPolicyShape(policy);
23688
+ return toBase64url(canonicalizeToBytes(policy));
23689
+ }
23690
+ var init_canonical_policy = __esm({
23691
+ "src/policy-engine/canonical-policy.ts"() {
23692
+ init_canonical_json();
23693
+ init_encoding();
23694
+ init_constants();
23695
+ init_errors2();
23696
+ }
23697
+ });
23698
+
23699
+ // src/mesh/constants.ts
23700
+ function isReservedEventType(s) {
23701
+ return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
23702
+ }
23703
+ function isReservedExtensionKey(k) {
23704
+ return RESERVED_EXTENSION_ENVELOPE_KEYS.includes(k);
23705
+ }
23706
+ var PROTOCOL_VERSION, RESERVED_EVENT_TYPE_PREFIXES, RESERVED_EXTENSION_ENVELOPE_KEYS;
23707
+ var init_constants2 = __esm({
23708
+ "src/mesh/constants.ts"() {
23709
+ PROTOCOL_VERSION = "0.1";
23710
+ RESERVED_EVENT_TYPE_PREFIXES = [
23711
+ "EXTENSION_",
23712
+ "cross_fortress_",
23713
+ "multi_master_"
23714
+ ];
23715
+ RESERVED_EXTENSION_ENVELOPE_KEYS = [
23716
+ "cross_fortress_read_grant",
23717
+ "cross_fortress_read_query",
23718
+ "cross_fortress_read_response",
23719
+ "multi_master_policy_merge",
23720
+ "audit_replication_full_n_way",
23721
+ "auto_promote_canonical_audit",
23722
+ "agent_live_migration"
23723
+ ];
23724
+ }
23725
+ });
23726
+ var init_trust_root = __esm({
23727
+ "src/mesh/trust-root.ts"() {
23728
+ init_encoding();
23729
+ init_identity();
23730
+ init_random();
23731
+ init_canonical_json();
23732
+ init_constants2();
23733
+ init_errors();
23734
+ }
23735
+ });
23736
+ function packSignedEvent(params) {
23737
+ if (isReservedEventType(params.event_type)) {
23738
+ throw new MeshReservedEventTypeError(params.event_type);
23739
+ }
23740
+ const ext = params.extension_envelope ?? {};
23741
+ for (const key of Object.keys(ext)) {
23742
+ if (isReservedExtensionKey(key)) {
23743
+ throw new MeshReservedExtensionKeyError(key);
23744
+ }
23745
+ }
23746
+ const payloadBytes = canonicalizeToBytes(params.payload);
23747
+ const payload_hash = toBase64url(sha256.sha256(payloadBytes));
23748
+ const body = {
23749
+ protocol_version: PROTOCOL_VERSION,
23750
+ event_type: params.event_type,
23751
+ event_id: generateEventId(),
23752
+ emitter_node: params.emitter_node,
23753
+ emitter_principal: params.emitter_principal,
23754
+ fortress_id: params.fortress_id,
23755
+ causal_parents: params.causal_parents ?? [],
23756
+ payload: params.payload,
23757
+ payload_hash,
23758
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
23759
+ monotonic_seq: params.monotonic_seq,
23760
+ extension_envelope: ext
23761
+ };
23762
+ const bytesToSign = canonicalizeToBytes(body);
23763
+ const nodeSig = ed25519.ed25519.sign(bytesToSign, params.node_private_key);
23764
+ const evt = {
23765
+ ...body,
23766
+ node_signature: toBase64url(nodeSig)
23767
+ };
23768
+ if (params.principal_private_key) {
23769
+ const principalSig = ed25519.ed25519.sign(bytesToSign, params.principal_private_key);
23770
+ evt.principal_signature = toBase64url(principalSig);
23771
+ }
23772
+ return evt;
23773
+ }
23774
+ function generateEventId() {
23775
+ const bytes = randomBytes(16);
23776
+ let hex = "";
23777
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
23778
+ return hex;
23779
+ }
23780
+ var init_envelope = __esm({
23781
+ "src/mesh/envelope.ts"() {
23782
+ init_encoding();
23783
+ init_random();
23784
+ init_canonical_json();
23785
+ init_constants2();
23786
+ init_errors();
23787
+ init_trust_root();
23788
+ }
23789
+ });
23790
+
23791
+ // src/policy-engine/envelope.ts
23792
+ function packPolicyUpdate(params) {
23793
+ validateCompiledPolicyShape(params.policy);
23794
+ const blob = encodePolicyBlob(params.policy);
23795
+ const payload = {
23796
+ agent_id: params.policy.agent_id,
23797
+ policy_version: params.policy.policy_version,
23798
+ policy_blob: blob,
23799
+ ...params.policy.parent_version !== void 0 ? { parent_version: params.policy.parent_version } : {}
23800
+ };
23801
+ return packSignedEvent({
23802
+ event_type: POLICY_UPDATE_EVENT_TYPE,
23803
+ emitter_node: params.emitter_node,
23804
+ emitter_principal: params.emitter_principal,
23805
+ fortress_id: params.policy.fortress_id,
23806
+ causal_parents: params.causal_parents,
23807
+ payload,
23808
+ monotonic_seq: params.monotonic_seq,
23809
+ node_private_key: params.node_private_key,
23810
+ principal_private_key: params.principal_private_key
23811
+ });
23812
+ }
23813
+ var init_envelope2 = __esm({
23814
+ "src/policy-engine/envelope.ts"() {
23815
+ init_constants();
23816
+ init_canonical_policy();
23817
+ init_errors2();
23818
+ init_envelope();
23819
+ init_errors();
23820
+ }
23821
+ });
23822
+
23823
+ // src/templates/init.ts
23824
+ function deterministicCompiledAt(version) {
23825
+ const [major, minor, patch] = version.split(".").map(Number);
23826
+ const epoch = /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
23827
+ epoch.setUTCDate(epoch.getUTCDate() + (major - 1) * 365 + minor * 30 + patch);
23828
+ return epoch.toISOString();
23829
+ }
23830
+ function buildCompiledPolicyFromTemplate(bundle, params) {
23831
+ const policy = applyChannelTemplate(
23832
+ bundle.metadata.channel,
23833
+ {
23834
+ agent_id: params.agent_id,
23835
+ counterparty: params.counterparty,
23836
+ fortress_id: params.fortress_id,
23837
+ policy_version: params.policy_version
23838
+ }
23839
+ );
23840
+ policy.compiled_at = deterministicCompiledAt(bundle.metadata.version);
23841
+ policy.source_english = bundle.policy_english;
23842
+ if (bundle.defaults.egress.length > 0) {
23843
+ const egress = {
23844
+ allowlist: bundle.defaults.egress.map((e) => ({
23845
+ destination: e.destination,
23846
+ methods: [...e.methods]
23847
+ }))
23848
+ };
23849
+ policy.egress = egress;
23850
+ }
23851
+ const budgets = {};
23852
+ if (bundle.defaults.budgets.daily) {
23853
+ budgets.daily = { ...bundle.defaults.budgets.daily };
23854
+ }
23855
+ if (bundle.defaults.budgets.monthly) {
23856
+ budgets.monthly = { ...bundle.defaults.budgets.monthly };
23857
+ }
23858
+ if (budgets.daily || budgets.monthly) {
23859
+ policy.budgets = budgets;
23860
+ }
23861
+ const windows = bundle.defaults.retention.windows;
23862
+ if (Object.keys(windows).length > 0) {
23863
+ const retention = {
23864
+ windows: {}
23865
+ };
23866
+ for (const [slot, win] of Object.entries(windows)) {
23867
+ if (win) {
23868
+ retention.windows[slot] = { ...win };
23869
+ }
23870
+ }
23871
+ policy.retention = retention;
23872
+ }
23873
+ const classes = bundle.commitments.shapes.map((s) => s.commitment_class);
23874
+ for (const cls of classes) {
23875
+ if (!policy.capabilities.concordia_commitment_classes.includes(cls)) {
23876
+ policy.capabilities.concordia_commitment_classes.push(cls);
23877
+ }
23878
+ }
23879
+ policy.capabilities.concordia_commitment_classes.sort();
23880
+ validateCompiledPolicyShape(policy);
23881
+ return policy;
23882
+ }
23883
+ function initTemplate(params) {
23884
+ const bundle = getTemplate2(params.template_name);
23885
+ if (!bundle) {
23886
+ throw new Error(`unknown template: ${params.template_name}`);
23887
+ }
23888
+ const compiled = buildCompiledPolicyFromTemplate(bundle, {
23889
+ agent_id: params.agent_id,
23890
+ fortress_id: params.fortress_id,
23891
+ counterparty: params.counterparty ?? "*",
23892
+ policy_version: params.policy_version ?? 1
23893
+ });
23894
+ const signed_event = packPolicyUpdate({
23895
+ policy: compiled,
23896
+ emitter_node: params.emitter_node,
23897
+ emitter_principal: params.emitter_principal,
23898
+ monotonic_seq: params.monotonic_seq,
23899
+ node_private_key: params.node_private_key,
23900
+ principal_private_key: params.principal_private_key
23901
+ });
23902
+ return { compiled, signed_event, bundle };
23903
+ }
23904
+ var init_init = __esm({
23905
+ "src/templates/init.ts"() {
23906
+ init_channel_templates();
23907
+ init_canonical_policy();
23908
+ init_envelope2();
23909
+ init_registry2();
23910
+ }
23911
+ });
23912
+ function constantTimeEquals(a, b) {
23913
+ if (a.length !== b.length) return false;
23914
+ let diff = 0;
23915
+ for (let i = 0; i < a.length; i++) {
23916
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
23917
+ }
23918
+ return diff === 0;
23919
+ }
23920
+ function extractToken(req, url) {
23921
+ const header = req.headers.authorization;
23922
+ if (header && header.startsWith("Bearer ")) {
23923
+ return header.slice(7).trim();
23924
+ }
23925
+ const q = url.searchParams.get("token");
23926
+ return q ?? null;
23927
+ }
23928
+ function isAuthorized(deps, req, url) {
23929
+ if (!deps.authToken) return true;
23930
+ const token = extractToken(req, url);
23931
+ if (!token) return false;
23932
+ return constantTimeEquals(token, deps.authToken);
23933
+ }
23934
+ function writeJSON(res, status, payload) {
23935
+ res.writeHead(status, {
23936
+ "Content-Type": "application/json",
23937
+ "Cache-Control": "no-store"
23938
+ });
23939
+ res.end(JSON.stringify(payload));
23940
+ }
23941
+ async function readJSONBody(req) {
23942
+ const chunks = [];
23943
+ let size = 0;
23944
+ const MAX = 256 * 1024;
23945
+ for await (const chunk of req) {
23946
+ size += chunk.length;
23947
+ if (size > MAX) throw new Error("request body too large");
23948
+ chunks.push(chunk);
23949
+ }
23950
+ const body = Buffer.concat(chunks).toString("utf-8");
23951
+ if (!body) return {};
23952
+ return JSON.parse(body);
23953
+ }
23954
+ function generateEphemeralKey() {
23955
+ return new Uint8Array(crypto.randomBytes(32));
23956
+ }
23957
+ function writeText(res, status, body, contentType = "text/plain") {
23958
+ res.writeHead(status, {
23959
+ "Content-Type": contentType,
23960
+ "Cache-Control": "no-store"
23961
+ });
23962
+ res.end(body);
23963
+ }
23964
+ async function handleRequest(deps, req, res) {
23965
+ const host = req.headers.host || "localhost";
23966
+ const url = new URL(req.url ?? "/", `http://${host}`);
23967
+ const method = (req.method ?? "GET").toUpperCase();
23968
+ const path = url.pathname;
23969
+ if (!isAuthorized(deps, req, url)) {
23970
+ writeJSON(res, 401, { error: "unauthorized" });
23971
+ return true;
23972
+ }
23973
+ if (method === "GET" && path === "/api/health") {
23974
+ writeJSON(res, 200, { ok: true, mode: deps.sources.mode });
23975
+ return true;
23976
+ }
23977
+ if (method === "GET" && (path === "/" || path === "/index.html")) {
23978
+ const snapshot = await getProtectionSnapshot(deps.sources);
23979
+ const html = renderDashboardHTML({ snapshot, authToken: deps.authToken });
23980
+ writeText(res, 200, html, "text/html; charset=utf-8");
23981
+ return true;
23982
+ }
23983
+ if (method === "GET" && path === "/api/snapshot") {
23984
+ const snapshot = await getProtectionSnapshot(deps.sources);
23985
+ writeJSON(res, 200, snapshot);
23986
+ return true;
23987
+ }
23988
+ const approvalMatch = /^\/api\/approvals\/([^/]+)\/(allow|deny)$/.exec(path);
23989
+ if (method === "POST" && approvalMatch) {
23990
+ const id = decodeURIComponent(approvalMatch[1]);
23991
+ const action = approvalMatch[2];
23992
+ if (!deps.approvals) {
23993
+ writeJSON(res, 503, { error: "approvals_unavailable" });
23994
+ return true;
23995
+ }
23996
+ const handler = action === "allow" ? deps.approvals.allow : deps.approvals.deny;
23997
+ try {
23998
+ const ok2 = await handler(id);
23999
+ writeJSON(res, ok2 ? 200 : 404, { id, action, ok: ok2 });
24000
+ } catch (err) {
24001
+ writeJSON(res, 500, { error: "approval_failed", message: err.message });
24002
+ }
24003
+ return true;
24004
+ }
24005
+ if (method === "GET" && path === "/api/stream") {
24006
+ await handleStream(deps, res);
24007
+ return true;
24008
+ }
24009
+ if (method === "GET" && path === "/api/templates") {
24010
+ try {
24011
+ const templates = listTemplates();
24012
+ writeJSON(res, 200, { templates });
24013
+ } catch (err) {
24014
+ writeJSON(res, 500, {
24015
+ error: "template_load_failed",
24016
+ message: err.message
24017
+ });
24018
+ }
24019
+ return true;
24020
+ }
24021
+ const templateMatch = /^\/api\/templates\/([^/]+)$/.exec(path);
24022
+ if (method === "GET" && templateMatch) {
24023
+ const name = decodeURIComponent(templateMatch[1]);
24024
+ try {
24025
+ const entry = getTemplateEntry(name);
24026
+ if (!entry) {
24027
+ writeJSON(res, 404, { error: "template_not_found", name });
24028
+ return true;
24029
+ }
24030
+ writeJSON(res, 200, entry);
24031
+ } catch (err) {
24032
+ writeJSON(res, 500, {
24033
+ error: "template_load_failed",
24034
+ message: err.message
24035
+ });
24036
+ }
24037
+ return true;
24038
+ }
24039
+ const initMatch = /^\/api\/templates\/([^/]+)\/init$/.exec(path);
24040
+ if (method === "POST" && initMatch) {
24041
+ const name = decodeURIComponent(initMatch[1]);
24042
+ try {
24043
+ const bundle = getTemplate2(name);
24044
+ if (!bundle) {
24045
+ writeJSON(res, 404, { error: "template_not_found", name });
24046
+ return true;
24047
+ }
24048
+ const body = await readJSONBody(req);
24049
+ if (!body.agent_name || typeof body.agent_name !== "string") {
24050
+ writeJSON(res, 400, {
24051
+ error: "validation_error",
24052
+ message: "agent_name is required and must be a string"
24053
+ });
24054
+ return true;
24055
+ }
24056
+ if (!/^[a-zA-Z0-9_-]+$/.test(body.agent_name)) {
24057
+ writeJSON(res, 400, {
24058
+ error: "validation_error",
24059
+ message: "agent_name must contain only alphanumeric characters, hyphens, and underscores"
24060
+ });
24061
+ return true;
24062
+ }
24063
+ const nodeId = deps.nodeId ?? "dashboard-node";
24064
+ const nodePrivateKey = deps.nodePrivateKey ?? generateEphemeralKey();
24065
+ const principalId = deps.principalId ?? "dashboard-principal";
24066
+ const fortressId = deps.fortressId ?? "default";
24067
+ const result = initTemplate({
24068
+ template_name: name,
24069
+ agent_id: body.agent_name,
24070
+ fortress_id: fortressId,
24071
+ counterparty: "*",
24072
+ policy_version: 1,
24073
+ emitter_node: nodeId,
24074
+ emitter_principal: principalId,
24075
+ monotonic_seq: 1,
24076
+ node_private_key: nodePrivateKey
24077
+ });
24078
+ writeJSON(res, 200, {
24079
+ agent_id: body.agent_name,
24080
+ signed_event_id: result.signed_event.event_id,
24081
+ policy_version: result.compiled.policy_version,
24082
+ template_name: name,
24083
+ attestation_panel_url: `/console#agent_roster`
24084
+ });
24085
+ } catch (err) {
24086
+ writeJSON(res, 500, {
24087
+ error: "template_init_failed",
24088
+ message: err.message
24089
+ });
24090
+ }
24091
+ return true;
24092
+ }
24093
+ return false;
24094
+ }
24095
+ async function handleStream(deps, res) {
24096
+ res.writeHead(200, {
24097
+ "Content-Type": "text/event-stream",
24098
+ "Cache-Control": "no-cache, no-transform",
24099
+ Connection: "keep-alive",
24100
+ "X-Accel-Buffering": "no"
24101
+ });
24102
+ const snapshot = await getProtectionSnapshot(deps.sources);
24103
+ res.write(`event: snapshot
24104
+ data: ${JSON.stringify(snapshot)}
24105
+
24106
+ `);
24107
+ const unsubscribe = deps.onEvent ? deps.onEvent((event) => {
24108
+ try {
24109
+ res.write(`event: ${event.type}
24110
+ data: ${JSON.stringify(event.data)}
24111
+
24112
+ `);
24113
+ } catch {
24114
+ }
24115
+ }) : () => {
24116
+ };
24117
+ const keepAlive = setInterval(() => {
24118
+ try {
24119
+ res.write(": keepalive\n\n");
24120
+ } catch {
24121
+ }
24122
+ }, 25e3);
24123
+ const cleanup = () => {
24124
+ clearInterval(keepAlive);
24125
+ unsubscribe();
24126
+ };
24127
+ res.on("close", cleanup);
24128
+ res.on("error", cleanup);
24129
+ }
24130
+ var init_api = __esm({
24131
+ "src/dashboard/api.ts"() {
24132
+ init_aggregator();
24133
+ init_html();
24134
+ init_registry2();
24135
+ init_init();
24136
+ }
24137
+ });
24138
+ async function startDashboardServer(options) {
24139
+ const port = options.port ?? DEFAULT_PORT;
24140
+ const host = options.host ?? DEFAULT_HOST;
24141
+ const listeners = /* @__PURE__ */ new Set();
24142
+ const onEvent = (listener) => {
24143
+ listeners.add(listener);
24144
+ return () => listeners.delete(listener);
24145
+ };
24146
+ const publish = (event) => {
24147
+ for (const listener of listeners) {
24148
+ try {
24149
+ listener(event);
24150
+ } catch {
24151
+ }
24152
+ }
24153
+ };
24154
+ const deps = {
24155
+ sources: options.sources,
24156
+ authToken: options.authToken,
24157
+ approvals: options.approvals,
24158
+ onEvent
24159
+ };
24160
+ const server = http.createServer(async (req, res) => {
24161
+ try {
24162
+ const served = await handleRequest(deps, req, res);
24163
+ if (!served) {
24164
+ res.writeHead(404, { "Content-Type": "application/json" });
24165
+ res.end(JSON.stringify({ error: "not_found", path: req.url }));
24166
+ }
24167
+ } catch (err) {
24168
+ try {
24169
+ res.writeHead(500, { "Content-Type": "application/json" });
24170
+ res.end(JSON.stringify({ error: "internal", message: err.message }));
24171
+ } catch {
24172
+ }
24173
+ }
24174
+ });
24175
+ await new Promise((resolve2, reject) => {
24176
+ server.once("error", reject);
24177
+ server.listen(port, host, () => {
24178
+ server.off("error", reject);
24179
+ resolve2();
24180
+ });
24181
+ });
24182
+ const actualPort = (() => {
24183
+ const addr = server.address();
24184
+ if (addr && typeof addr === "object") return addr.port;
24185
+ return port;
24186
+ })();
24187
+ const url = `http://${host}:${actualPort}`;
24188
+ return {
24189
+ url,
24190
+ port: actualPort,
24191
+ host,
24192
+ stop: () => new Promise((resolve2, reject) => {
24193
+ server.close((err) => err ? reject(err) : resolve2());
24194
+ }),
24195
+ publish,
24196
+ publishActivity: (entry) => publish({ type: "activity", data: entry }),
24197
+ publishApproval: (approval) => publish({ type: "approval", data: approval })
24198
+ };
24199
+ }
24200
+ var DEFAULT_PORT, DEFAULT_HOST;
24201
+ var init_server = __esm({
24202
+ "src/dashboard/server.ts"() {
24203
+ init_api();
24204
+ DEFAULT_PORT = 3501;
24205
+ DEFAULT_HOST = "127.0.0.1";
24206
+ }
24207
+ });
24208
+
24209
+ // src/dashboard/index.ts
24210
+ async function startDashboard(options) {
24211
+ const activity = options.initialActivity ? [...options.initialActivity] : [];
24212
+ const pending = options.initialPendingApprovals ? [...options.initialPendingApprovals] : [];
24213
+ const sources = {
24214
+ mode: options.mode,
24215
+ server_version: options.serverVersion,
24216
+ ...options.auditLog ? { auditLog: options.auditLog } : {},
24217
+ ...options.identityManager ? { identityManager: options.identityManager } : {},
24218
+ ...options.clientManager ? { clientManager: options.clientManager } : {},
24219
+ ...options.baseline ? { baseline: options.baseline } : {},
22932
24220
  ...options.policy ? { policy: options.policy } : {},
22933
24221
  ...options.reputation ? { reputation: options.reputation } : {},
22934
24222
  ...options.teeAvailable != null ? { teeAvailable: options.teeAvailable } : {},
@@ -23677,6 +24965,90 @@ var init_paths = __esm({
23677
24965
  DEFAULT_DASHBOARD_PORT = 3501;
23678
24966
  }
23679
24967
  });
24968
+ function getPlatformPaths() {
24969
+ const home = os.homedir();
24970
+ return {
24971
+ "openclaw": [
24972
+ path.join(home, ".openclaw", "openclaw.json"),
24973
+ path.join(home, ".openclaw", "config.json"),
24974
+ path.join(home, "Library", "Application Support", "OpenClaw", "openclaw.json"),
24975
+ path.join(home, "Library", "Application Support", "OpenClaw", "config.json")
24976
+ ],
24977
+ // Hermes Agent (NousResearch, v0.9.0) canonicals live under ~/.hermes.
24978
+ // Hermes ships `cli-config.yaml` as the primary surface per upstream docs.
24979
+ // Sanctuary wrap v1.0 detects the JSON variant only: operators who keep
24980
+ // YAML can still wrap via `sanctuary wrap --wrap <path>` after exporting
24981
+ // to JSON. YAML-native detection is flagged as a v1.x follow-up.
24982
+ "hermes": [
24983
+ path.join(home, ".hermes", "cli-config.json"),
24984
+ path.join(home, ".hermes", "config.json"),
24985
+ path.join(home, ".config", "hermes", "cli-config.json")
24986
+ ],
24987
+ // Claude Code's modern canonical surface is ~/.claude.json (`claude mcp
24988
+ // add` writes here). The legacy ~/.claude/settings.json shape predates
24989
+ // it and is still respected if present. Probe order = preference order:
24990
+ // wrap operates on the first one that exists, and bootstraps a fresh
24991
+ // ~/.claude.json when neither is present (per the cli.ts bootstrap).
24992
+ "claude-code": [
24993
+ path.join(home, ".claude.json"),
24994
+ path.join(home, ".claude", "settings.json"),
24995
+ path.join(home, ".config", "claude-code", "settings.json")
24996
+ ],
24997
+ "cursor": [
24998
+ path.join(home, ".cursor", "mcp.json")
24999
+ ],
25000
+ // Cline is a VS Code extension (saoudrizwan.claude-dev). Its MCP settings
25001
+ // live under the VS Code globalStorage tree, which is OS-specific. We
25002
+ // enumerate the three supported OS layouts; at detection time only the
25003
+ // one matching the running OS will exist.
25004
+ "cline": [
25005
+ // macOS
25006
+ path.join(
25007
+ home,
25008
+ "Library",
25009
+ "Application Support",
25010
+ "Code",
25011
+ "User",
25012
+ "globalStorage",
25013
+ "saoudrizwan.claude-dev",
25014
+ "settings",
25015
+ "cline_mcp_settings.json"
25016
+ ),
25017
+ // Linux
25018
+ path.join(
25019
+ home,
25020
+ ".config",
25021
+ "Code",
25022
+ "User",
25023
+ "globalStorage",
25024
+ "saoudrizwan.claude-dev",
25025
+ "settings",
25026
+ "cline_mcp_settings.json"
25027
+ ),
25028
+ // Windows (honour APPDATA when set, otherwise reconstruct under home)
25029
+ process.env.APPDATA ? path.join(
25030
+ process.env.APPDATA,
25031
+ "Code",
25032
+ "User",
25033
+ "globalStorage",
25034
+ "saoudrizwan.claude-dev",
25035
+ "settings",
25036
+ "cline_mcp_settings.json"
25037
+ ) : path.join(
25038
+ home,
25039
+ "AppData",
25040
+ "Roaming",
25041
+ "Code",
25042
+ "User",
25043
+ "globalStorage",
25044
+ "saoudrizwan.claude-dev",
25045
+ "settings",
25046
+ "cline_mcp_settings.json"
25047
+ )
25048
+ ],
25049
+ "generic": []
25050
+ };
25051
+ }
23680
25052
  function backupDir() {
23681
25053
  return path.join(resolveStoragePath(), "backup");
23682
25054
  }
@@ -23720,7 +25092,7 @@ async function detectAgentConfigWithDiagnostics(platform4, configPath) {
23720
25092
  return { config, pathsChecked, errors };
23721
25093
  }
23722
25094
  if (platform4) {
23723
- const paths = PLATFORM_PATHS[platform4];
25095
+ const paths = getPlatformPaths()[platform4];
23724
25096
  for (const path of paths) {
23725
25097
  pathsChecked.push(path);
23726
25098
  const { config, error } = await readConfigFileWithError(path, platform4);
@@ -23729,7 +25101,7 @@ async function detectAgentConfigWithDiagnostics(platform4, configPath) {
23729
25101
  }
23730
25102
  return { config: null, pathsChecked, errors };
23731
25103
  }
23732
- for (const [plat, paths] of Object.entries(PLATFORM_PATHS)) {
25104
+ for (const [plat, paths] of Object.entries(getPlatformPaths())) {
23733
25105
  for (const path of paths) {
23734
25106
  pathsChecked.push(path);
23735
25107
  const { config, error } = await readConfigFileWithError(path, plat);
@@ -23787,7 +25159,7 @@ function extractServers(config, platform4) {
23787
25159
  const mcpServers = obj.mcpServers;
23788
25160
  if (mcpServers && typeof mcpServers === "object") {
23789
25161
  for (const [name, serverConfig] of Object.entries(mcpServers)) {
23790
- if (name.toLowerCase().includes("sanctuary")) continue;
25162
+ if (isCanonicalSanctuaryName(name)) continue;
23791
25163
  const entry = parseServerEntry(name, serverConfig);
23792
25164
  if (entry) servers.push(entry);
23793
25165
  }
@@ -23797,7 +25169,27 @@ function extractServers(config, platform4) {
23797
25169
  const mcpServers = obj.mcpServers;
23798
25170
  if (mcpServers && typeof mcpServers === "object") {
23799
25171
  for (const [name, serverConfig] of Object.entries(mcpServers)) {
23800
- if (name.toLowerCase().includes("sanctuary")) continue;
25172
+ if (isCanonicalSanctuaryName(name)) continue;
25173
+ const entry = parseServerEntry(name, serverConfig);
25174
+ if (entry) servers.push(entry);
25175
+ }
25176
+ }
25177
+ }
25178
+ if (platform4 === "hermes") {
25179
+ const mcpServers = obj.mcp_servers;
25180
+ if (mcpServers && typeof mcpServers === "object") {
25181
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25182
+ if (isCanonicalSanctuaryName(name)) continue;
25183
+ const entry = parseServerEntry(name, serverConfig);
25184
+ if (entry) servers.push(entry);
25185
+ }
25186
+ }
25187
+ }
25188
+ if (platform4 === "cline") {
25189
+ const mcpServers = obj.mcpServers;
25190
+ if (mcpServers && typeof mcpServers === "object") {
25191
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25192
+ if (isCanonicalSanctuaryName(name)) continue;
23801
25193
  const entry = parseServerEntry(name, serverConfig);
23802
25194
  if (entry) servers.push(entry);
23803
25195
  }
@@ -23805,6 +25197,9 @@ function extractServers(config, platform4) {
23805
25197
  }
23806
25198
  return servers;
23807
25199
  }
25200
+ function isCanonicalSanctuaryName(name) {
25201
+ return name.toLowerCase() === "sanctuary";
25202
+ }
23808
25203
  function parseServerEntry(name, config) {
23809
25204
  if (!config || typeof config !== "object") return null;
23810
25205
  const c = config;
@@ -23843,6 +25238,8 @@ async function rewriteConfigForCocoon(agentConfig, sanctuaryCommand, sanctuaryAr
23843
25238
  if (agentConfig.platform === "openclaw") {
23844
25239
  const existingMcp = raw.mcp ?? {};
23845
25240
  existingServers = existingMcp.servers ?? {};
25241
+ } else if (agentConfig.platform === "hermes") {
25242
+ existingServers = raw.mcp_servers ?? {};
23846
25243
  } else {
23847
25244
  existingServers = raw.mcpServers ?? {};
23848
25245
  }
@@ -23886,6 +25283,14 @@ async function rewriteConfigForCocoon(agentConfig, sanctuaryCommand, sanctuaryAr
23886
25283
  }
23887
25284
  };
23888
25285
  delete rewritten.mcpServers;
25286
+ } else if (agentConfig.platform === "hermes") {
25287
+ rewritten = {
25288
+ ...raw,
25289
+ mcp_servers: {
25290
+ ...existingServers,
25291
+ sanctuary: sanctuaryEntry
25292
+ }
25293
+ };
23889
25294
  } else {
23890
25295
  rewritten = {
23891
25296
  ...raw,
@@ -23898,26 +25303,9 @@ async function rewriteConfigForCocoon(agentConfig, sanctuaryCommand, sanctuaryAr
23898
25303
  await promises.writeFile(agentConfig.configPath, JSON.stringify(rewritten, null, 2), { mode: 384 });
23899
25304
  return agentConfig.configPath;
23900
25305
  }
23901
- var PLATFORM_PATHS;
23902
25306
  var init_config_reader = __esm({
23903
25307
  "src/cocoon/config-reader.ts"() {
23904
25308
  init_paths();
23905
- PLATFORM_PATHS = {
23906
- "openclaw": [
23907
- path.join(os.homedir(), ".openclaw", "openclaw.json"),
23908
- path.join(os.homedir(), ".openclaw", "config.json"),
23909
- path.join(os.homedir(), "Library", "Application Support", "OpenClaw", "openclaw.json"),
23910
- path.join(os.homedir(), "Library", "Application Support", "OpenClaw", "config.json")
23911
- ],
23912
- "claude-code": [
23913
- path.join(os.homedir(), ".claude", "settings.json"),
23914
- path.join(os.homedir(), ".config", "claude-code", "settings.json")
23915
- ],
23916
- "cursor": [
23917
- path.join(os.homedir(), ".cursor", "mcp.json")
23918
- ],
23919
- "generic": []
23920
- };
23921
25309
  }
23922
25310
  });
23923
25311
 
@@ -24223,13 +25611,46 @@ async function runWrap(options, deps = {}) {
24223
25611
  }
24224
25612
  let platformHint;
24225
25613
  if (options.openclaw) platformHint = "openclaw";
25614
+ else if (options.hermes) platformHint = "hermes";
24226
25615
  else if (options.claudeCode) platformHint = "claude-code";
24227
25616
  else if (options.cursor) platformHint = "cursor";
24228
- const detection = await detectAgentConfigWithDiagnostics(
25617
+ else if (options.cline) platformHint = "cline";
25618
+ let detection = await detectAgentConfigWithDiagnostics(
24229
25619
  platformHint,
24230
25620
  options.wrap
24231
25621
  );
24232
- const agentConfig = detection.config;
25622
+ let agentConfig = detection.config;
25623
+ if (!agentConfig && platformHint && !options.wrap) {
25624
+ const candidatePaths = getPlatformPaths()[platformHint];
25625
+ const canonicalPath = candidatePaths[0];
25626
+ if (canonicalPath) {
25627
+ try {
25628
+ await promises.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 448 });
25629
+ await promises.writeFile(canonicalPath, "{}", { mode: 384 });
25630
+ console.error(
25631
+ `
25632
+ No existing ${platformHint} config found.`
25633
+ );
25634
+ console.error(
25635
+ ` Bootstrapped a fresh config at ${canonicalPath}.
25636
+ `
25637
+ );
25638
+ detection = await detectAgentConfigWithDiagnostics(
25639
+ platformHint,
25640
+ options.wrap
25641
+ );
25642
+ agentConfig = detection.config;
25643
+ } catch (err) {
25644
+ console.error(
25645
+ `
25646
+ Sanctuary: could not bootstrap ${platformHint} config at ${canonicalPath}`
25647
+ );
25648
+ console.error(` Error: ${err.message}
25649
+ `);
25650
+ process.exit(1);
25651
+ }
25652
+ }
25653
+ }
24233
25654
  if (!agentConfig) {
24234
25655
  console.error(`
24235
25656
  Sanctuary \u2014 Configuration Not Found
@@ -24241,7 +25662,7 @@ async function runWrap(options, deps = {}) {
24241
25662
  } else {
24242
25663
  console.error(" Could not auto-detect any agent configuration.");
24243
25664
  console.error(
24244
- " Use --openclaw, --claude-code, --cursor, or --wrap /path/to/config.json"
25665
+ " Use --openclaw, --hermes, --claude-code, --cursor, --cline, or --wrap /path/to/config.json"
24245
25666
  );
24246
25667
  }
24247
25668
  if (detection.pathsChecked.length > 0) {
@@ -24259,25 +25680,25 @@ async function runWrap(options, deps = {}) {
24259
25680
  console.error("");
24260
25681
  process.exit(1);
24261
25682
  }
24262
- if (agentConfig.servers.length === 0) {
25683
+ const hasSanctuaryInRaw = rawConfigContainsSanctuary(
25684
+ agentConfig.rawConfig,
25685
+ agentConfig.platform
25686
+ );
25687
+ if (hasSanctuaryInRaw) {
24263
25688
  console.error(
24264
25689
  `
24265
- Found ${agentConfig.platform} config at ${agentConfig.configPath},`
25690
+ Sanctuary already wrapped: updating the existing Sanctuary entry.
25691
+ `
24266
25692
  );
24267
- console.error(` but no MCP servers are configured in it.
24268
- `);
24269
- process.exit(1);
24270
- }
24271
- const hasSanctuary = agentConfig.servers.some(
24272
- (s) => s.name.toLowerCase() === "sanctuary"
24273
- );
24274
- if (hasSanctuary) {
25693
+ } else if (agentConfig.servers.length === 0) {
24275
25694
  console.error(
24276
25695
  `
24277
- Warning: This agent already has a Sanctuary server configured.`
25696
+ Found ${agentConfig.platform} config at ${agentConfig.configPath} with no MCP servers yet.`
25697
+ );
25698
+ console.error(
25699
+ ` Sanctuary will be installed as the only MCP server.
25700
+ `
24278
25701
  );
24279
- console.error(` Re-wrapping will update the existing Sanctuary entry.
24280
- `);
24281
25702
  }
24282
25703
  console.error(`
24283
25704
  Sanctuary wrap`);
@@ -24553,7 +25974,7 @@ async function verifyRewrittenConfig(configPath, backupPath) {
24553
25974
  await restoreFromBackup(configPath, backupPath);
24554
25975
  return false;
24555
25976
  }
24556
- const servers = parsed.mcp?.servers ?? parsed.mcpServers ?? {};
25977
+ const servers = parsed.mcp?.servers ?? parsed.mcpServers ?? parsed.mcp_servers ?? {};
24557
25978
  if (!servers.sanctuary) {
24558
25979
  console.error(`
24559
25980
  Verification FAILED: No sanctuary entry in rewritten config.`);
@@ -24644,10 +26065,14 @@ function toolNameFor(platform4, _servers) {
24644
26065
  switch (platform4) {
24645
26066
  case "openclaw":
24646
26067
  return "OpenClaw";
26068
+ case "hermes":
26069
+ return "Hermes Agent";
24647
26070
  case "claude-code":
24648
26071
  return "Claude Code";
24649
26072
  case "cursor":
24650
26073
  return "Cursor";
26074
+ case "cline":
26075
+ return "Cline";
24651
26076
  default:
24652
26077
  return "your agent";
24653
26078
  }
@@ -24658,6 +26083,23 @@ function countUpstreamTools(servers) {
24658
26083
  function readPackageVersion() {
24659
26084
  return SANCTUARY_VERSION;
24660
26085
  }
26086
+ function rawConfigContainsSanctuary(raw, agentPlatform) {
26087
+ if (!raw || typeof raw !== "object") return false;
26088
+ const obj = raw;
26089
+ let serversBag;
26090
+ if (agentPlatform === "openclaw") {
26091
+ const mcp = obj.mcp;
26092
+ serversBag = mcp?.servers ?? obj.mcpServers;
26093
+ } else if (agentPlatform === "hermes") {
26094
+ serversBag = obj.mcp_servers;
26095
+ } else {
26096
+ serversBag = obj.mcpServers;
26097
+ }
26098
+ if (!serversBag || typeof serversBag !== "object") return false;
26099
+ return Object.keys(serversBag).some(
26100
+ (name) => name.toLowerCase() === "sanctuary"
26101
+ );
26102
+ }
24661
26103
  function parseWrapArgs(argv) {
24662
26104
  const options = {};
24663
26105
  for (let i = 0; i < argv.length; i++) {
@@ -24668,12 +26110,18 @@ function parseWrapArgs(argv) {
24668
26110
  case "--openclaw":
24669
26111
  options.openclaw = true;
24670
26112
  break;
26113
+ case "--hermes":
26114
+ options.hermes = true;
26115
+ break;
24671
26116
  case "--claude-code":
24672
26117
  options.claudeCode = true;
24673
26118
  break;
24674
26119
  case "--cursor":
24675
26120
  options.cursor = true;
24676
26121
  break;
26122
+ case "--cline":
26123
+ options.cline = true;
26124
+ break;
24677
26125
  case "--unwrap":
24678
26126
  options.unwrap = true;
24679
26127
  break;
@@ -24703,15 +26151,19 @@ function printWrapHelp() {
24703
26151
 
24704
26152
  Usage:
24705
26153
  sanctuary wrap --openclaw Wrap OpenClaw
26154
+ sanctuary wrap --hermes Wrap Hermes Agent (NousResearch)
24706
26155
  sanctuary wrap --claude-code Wrap Claude Code
24707
26156
  sanctuary wrap --cursor Wrap Cursor
26157
+ sanctuary wrap --cline Wrap Cline (VS Code extension)
24708
26158
  sanctuary wrap --wrap <path> Wrap a specific MCP config file
24709
26159
  sanctuary wrap --unwrap Restore original config
24710
26160
 
24711
26161
  Options:
24712
26162
  --openclaw Auto-detect and wrap OpenClaw
26163
+ --hermes Auto-detect and wrap Hermes Agent
24713
26164
  --claude-code Auto-detect and wrap Claude Code
24714
26165
  --cursor Auto-detect and wrap Cursor
26166
+ --cline Auto-detect and wrap Cline (VS Code extension)
24715
26167
  --wrap <path> Wrap a specific MCP config file
24716
26168
  --unwrap Restore original config from backup
24717
26169
  --passphrase <p> Override the stored passphrase (one-off)
@@ -26654,6 +28106,230 @@ var init_secrets = __esm({
26654
28106
  STDIN_READ_DEADLINE_MS = 3e4;
26655
28107
  }
26656
28108
  });
28109
+
28110
+ // src/templates/cli.ts
28111
+ var cli_exports3 = {};
28112
+ __export(cli_exports3, {
28113
+ runTemplateCommand: () => runTemplateCommand
28114
+ });
28115
+ async function runTemplateCommand(args) {
28116
+ const out = args.out ?? process.stdout;
28117
+ const err = args.err ?? process.stderr;
28118
+ const [sub, ...rest] = args.argv;
28119
+ if (!sub || sub === "--help" || sub === "-h") {
28120
+ printTemplateHelp(out);
28121
+ return 0;
28122
+ }
28123
+ switch (sub) {
28124
+ case "list":
28125
+ return cmdList2(out);
28126
+ case "init":
28127
+ return cmdInit(rest, out, err);
28128
+ default:
28129
+ err.write(`Unknown template subcommand: ${sub}
28130
+ `);
28131
+ printTemplateHelp(err);
28132
+ return 1;
28133
+ }
28134
+ }
28135
+ function cmdList2(out, _err) {
28136
+ const templates = listTemplates();
28137
+ out.write("\nSanctuary Template Library\n");
28138
+ out.write("=".repeat(60) + "\n\n");
28139
+ out.write(
28140
+ padRight("NAME", 24) + padRight("CHANNEL", 26) + padRight("TIER", 6) + "DESCRIPTION\n"
28141
+ );
28142
+ out.write("-".repeat(100) + "\n");
28143
+ for (const entry of templates) {
28144
+ const m = entry.metadata;
28145
+ out.write(
28146
+ padRight(m.name, 24) + padRight(m.channel, 26) + padRight(m.tier, 6) + m.description + "\n"
28147
+ );
28148
+ }
28149
+ out.write("\n");
28150
+ out.write(`Use "sanctuary template init <name> --agent-id <id>" to load a template.
28151
+
28152
+ `);
28153
+ return 0;
28154
+ }
28155
+ function padRight(str, len) {
28156
+ return str.length >= len ? str + " " : str + " ".repeat(len - str.length);
28157
+ }
28158
+ function cmdInit(argv, out, err) {
28159
+ let templateName;
28160
+ let agentId;
28161
+ let fortressId = "fortress-default";
28162
+ let counterparty = "*";
28163
+ for (let i = 0; i < argv.length; i++) {
28164
+ if (argv[i] === "--agent-id" && argv[i + 1]) {
28165
+ agentId = argv[++i];
28166
+ } else if (argv[i] === "--fortress-id" && argv[i + 1]) {
28167
+ fortressId = argv[++i];
28168
+ } else if (argv[i] === "--counterparty" && argv[i + 1]) {
28169
+ counterparty = argv[++i];
28170
+ } else if (argv[i] === "--help" || argv[i] === "-h") {
28171
+ printInitHelp(out);
28172
+ return 0;
28173
+ } else if (!argv[i].startsWith("--")) {
28174
+ templateName = argv[i];
28175
+ }
28176
+ }
28177
+ if (!templateName) {
28178
+ err.write("Error: template name is required.\n");
28179
+ err.write(`Available templates: ${TEMPLATE_NAMES.join(", ")}
28180
+ `);
28181
+ return 1;
28182
+ }
28183
+ if (!isTemplateName(templateName)) {
28184
+ err.write(`Error: unknown template "${templateName}".
28185
+ `);
28186
+ err.write(`Available templates: ${TEMPLATE_NAMES.join(", ")}
28187
+ `);
28188
+ return 1;
28189
+ }
28190
+ if (!agentId) {
28191
+ err.write("Error: --agent-id is required.\n");
28192
+ return 1;
28193
+ }
28194
+ const bundle = getTemplate2(templateName);
28195
+ if (!bundle) {
28196
+ err.write(`Error: template "${templateName}" not found.
28197
+ `);
28198
+ return 1;
28199
+ }
28200
+ const compiled = buildCompiledPolicyFromTemplate(bundle, {
28201
+ agent_id: agentId,
28202
+ fortress_id: fortressId,
28203
+ counterparty,
28204
+ policy_version: 1
28205
+ });
28206
+ const blob = encodePolicyBlob(compiled);
28207
+ out.write("\nTemplate initialized successfully.\n");
28208
+ out.write("=".repeat(60) + "\n\n");
28209
+ out.write(`Template: ${bundle.metadata.name}
28210
+ `);
28211
+ out.write(`Channel: ${bundle.metadata.channel}
28212
+ `);
28213
+ out.write(`Tier: ${bundle.metadata.tier}
28214
+ `);
28215
+ out.write(`Agent ID: ${compiled.agent_id}
28216
+ `);
28217
+ out.write(`Fortress ID: ${compiled.fortress_id}
28218
+ `);
28219
+ out.write(`Policy Version: ${compiled.policy_version}
28220
+ `);
28221
+ out.write(`Schema Version: ${compiled.schema_version}
28222
+ `);
28223
+ out.write("\n");
28224
+ out.write("Slot access:\n");
28225
+ for (const slot of ["memory", "credentials", "plans", "outputs"]) {
28226
+ const rule = compiled.slots[slot];
28227
+ if (rule.mode === "deny") {
28228
+ out.write(` ${slot}: deny (hermetic)
28229
+ `);
28230
+ } else {
28231
+ const grants = rule.grants.map(
28232
+ (g) => `${g.action} by ${g.counterparty}`
28233
+ );
28234
+ out.write(` ${slot}: ${grants.join(", ")}
28235
+ `);
28236
+ }
28237
+ }
28238
+ if (compiled.capabilities.concordia_commitment_classes.length > 0) {
28239
+ out.write(
28240
+ `
28241
+ Commitment classes: ${compiled.capabilities.concordia_commitment_classes.join(", ")}
28242
+ `
28243
+ );
28244
+ }
28245
+ if (compiled.egress) {
28246
+ out.write(`
28247
+ Egress allowlist (${compiled.egress.allowlist.length} entries):
28248
+ `);
28249
+ for (const rule of compiled.egress.allowlist) {
28250
+ const methods = rule.methods.length > 0 ? rule.methods.join(",") : "*";
28251
+ out.write(` ${rule.destination} [${methods}]
28252
+ `);
28253
+ }
28254
+ } else {
28255
+ out.write("\nEgress: hermetic (no outbound access)\n");
28256
+ }
28257
+ if (compiled.budgets) {
28258
+ out.write("\nBudgets:\n");
28259
+ if (compiled.budgets.daily) {
28260
+ out.write(
28261
+ ` Daily: ${compiled.budgets.daily.amount} ${compiled.budgets.daily.unit}
28262
+ `
28263
+ );
28264
+ }
28265
+ if (compiled.budgets.monthly) {
28266
+ out.write(
28267
+ ` Monthly: ${compiled.budgets.monthly.amount} ${compiled.budgets.monthly.unit}
28268
+ `
28269
+ );
28270
+ }
28271
+ }
28272
+ if (compiled.retention) {
28273
+ out.write("\nRetention windows:\n");
28274
+ for (const [slot, win] of Object.entries(compiled.retention.windows)) {
28275
+ if (win) {
28276
+ const days = Math.round(win.max_age_seconds / 86400);
28277
+ out.write(` ${slot}: ${days} days${win.archive ? " (archive)" : ""}
28278
+ `);
28279
+ }
28280
+ }
28281
+ }
28282
+ out.write(`
28283
+ Policy blob (base64url, ${blob.length} chars):
28284
+ `);
28285
+ out.write(` ${blob.slice(0, 80)}...
28286
+
28287
+ `);
28288
+ out.write(
28289
+ "To sign and emit this policy as a policy_update event, use the\npolicy engine's packPolicyUpdate with your node signing key.\n\n"
28290
+ );
28291
+ return 0;
28292
+ }
28293
+ function printTemplateHelp(out) {
28294
+ out.write(`
28295
+ sanctuary template \u2014 Manage Sanctuary policy templates.
28296
+
28297
+ Usage:
28298
+ sanctuary template list List available templates
28299
+ sanctuary template init <name> [options] Load a template
28300
+
28301
+ Options:
28302
+ --help, -h Show this help
28303
+
28304
+ Use "sanctuary template init --help" for init-specific options.
28305
+ `);
28306
+ }
28307
+ function printInitHelp(out) {
28308
+ out.write(`
28309
+ sanctuary template init \u2014 Load a template into a fortress.
28310
+
28311
+ Usage:
28312
+ sanctuary template init <name> --agent-id <id> [options]
28313
+
28314
+ Arguments:
28315
+ <name> Template name (see "sanctuary template list")
28316
+
28317
+ Options:
28318
+ --agent-id <id> Agent ID to bind the policy to (required)
28319
+ --fortress-id <id> Fortress ID (default: "fortress-default")
28320
+ --counterparty <id> Counterparty for channel grants (default: "*")
28321
+ --help, -h Show this help
28322
+
28323
+ Available templates: ${TEMPLATE_NAMES.join(", ")}
28324
+ `);
28325
+ }
28326
+ var init_cli3 = __esm({
28327
+ "src/templates/cli.ts"() {
28328
+ init_registry2();
28329
+ init_init();
28330
+ init_canonical_policy();
28331
+ }
28332
+ });
26657
28333
  async function isTenantDir(path$1) {
26658
28334
  const [hasState, hasProfile, hasFallback] = await Promise.all([
26659
28335
  dirExists(path.join(path$1, "state")),
@@ -26865,7 +28541,7 @@ async function runAgentsCommand(args) {
26865
28541
  try {
26866
28542
  switch (sub) {
26867
28543
  case "list":
26868
- return await cmdList2(rest, ctx);
28544
+ return await cmdList3(rest, ctx);
26869
28545
  case "show":
26870
28546
  return await cmdShow(rest, ctx);
26871
28547
  case "status":
@@ -26924,11 +28600,11 @@ function passphraseLabel(tenant) {
26924
28600
  return "not-init";
26925
28601
  }
26926
28602
  }
26927
- function padRight(s, width) {
28603
+ function padRight2(s, width) {
26928
28604
  if (s.length >= width) return s;
26929
28605
  return s + " ".repeat(width - s.length);
26930
28606
  }
26931
- async function cmdList2(argv, ctx) {
28607
+ async function cmdList3(argv, ctx) {
26932
28608
  const json = hasJsonFlag(argv);
26933
28609
  const tenants = await discoverTenants(ctx.discoverOpts);
26934
28610
  if (json) {
@@ -26960,7 +28636,7 @@ async function cmdList2(argv, ctx) {
26960
28636
  }));
26961
28637
  const nameW = Math.max(4, ...rows.map((r) => r.tenant.name.length));
26962
28638
  const pathW = Math.max(12, ...rows.map((r) => r.tenant.storage_path.length));
26963
- const header = padRight("NAME", nameW) + " " + padRight("STATUS", 8) + padRight("DASH", 6) + padRight("HOOK", 6) + padRight("PP", 15) + padRight("STORAGE", pathW);
28639
+ const header = padRight2("NAME", nameW) + " " + padRight2("STATUS", 8) + padRight2("DASH", 6) + padRight2("HOOK", 6) + padRight2("PP", 15) + padRight2("STORAGE", pathW);
26964
28640
  ctx.out.write(header + "\n");
26965
28641
  for (const { tenant, probe } of rows) {
26966
28642
  const status = probe.running ? "running" : tenant.initialized ? "stopped" : "empty";
@@ -26968,7 +28644,7 @@ async function cmdList2(argv, ctx) {
26968
28644
  const webhook = tenant.runtime?.webhook_callback_port != null ? String(tenant.runtime.webhook_callback_port) : "-";
26969
28645
  const pp = passphraseLabel(tenant);
26970
28646
  ctx.out.write(
26971
- padRight(tenant.name, nameW) + " " + padRight(status, 8) + padRight(dashboard, 6) + padRight(webhook, 6) + padRight(pp, 15) + padRight(tenant.storage_path, pathW) + "\n"
28647
+ padRight2(tenant.name, nameW) + " " + padRight2(status, 8) + padRight2(dashboard, 6) + padRight2(webhook, 6) + padRight2(pp, 15) + padRight2(tenant.storage_path, pathW) + "\n"
26972
28648
  );
26973
28649
  }
26974
28650
  return 0;
@@ -27077,13 +28753,13 @@ async function cmdStatus(argv, ctx) {
27077
28753
  const dash = tenant.runtime?.dashboard_port != null ? `:${tenant.runtime.dashboard_port}` : "";
27078
28754
  const last = tenant.last_activity ? ` \xB7 last ${formatRelative2(tenant.last_activity)}` : "";
27079
28755
  ctx.out.write(
27080
- `${padRight(tenant.name, nameW)} ${padRight(state, 8)}${dash}${last}
28756
+ `${padRight2(tenant.name, nameW)} ${padRight2(state, 8)}${dash}${last}
27081
28757
  `
27082
28758
  );
27083
28759
  }
27084
28760
  return 0;
27085
28761
  }
27086
- var init_cli3 = __esm({
28762
+ var init_cli4 = __esm({
27087
28763
  "src/cli/agents/cli.ts"() {
27088
28764
  init_discovery();
27089
28765
  init_health();
@@ -27107,7 +28783,7 @@ var init_agents = __esm({
27107
28783
  init_discovery();
27108
28784
  init_health();
27109
28785
  init_runtime();
27110
- init_cli3();
28786
+ init_cli4();
27111
28787
  }
27112
28788
  });
27113
28789
 
@@ -27621,8 +29297,8 @@ Refusing to generate a new recovery key over the default root \u2014 that would
27621
29297
  const dashboardHost = options.host ?? config.dashboard.host;
27622
29298
  let authToken = config.dashboard.auth_token;
27623
29299
  if (authToken === "auto") {
27624
- const { randomBytes: randomBytes7 } = await import('crypto');
27625
- authToken = randomBytes7(32).toString("hex");
29300
+ const { randomBytes: randomBytes8 } = await import('crypto');
29301
+ authToken = randomBytes8(32).toString("hex");
27626
29302
  }
27627
29303
  const dashboard = new DashboardApprovalChannel({
27628
29304
  port: dashboardPort,
@@ -27847,6 +29523,11 @@ async function main() {
27847
29523
  const code = await runSecretsCommand2({ argv: args.slice(1) });
27848
29524
  process.exit(code);
27849
29525
  }
29526
+ if (args[0] === "template") {
29527
+ const { runTemplateCommand: runTemplateCommand2 } = await Promise.resolve().then(() => (init_cli3(), cli_exports3));
29528
+ const code = await runTemplateCommand2({ argv: args.slice(1) });
29529
+ process.exit(code);
29530
+ }
27850
29531
  if (args[0] === "agents") {
27851
29532
  const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
27852
29533
  const code = await runAgentsCommand2({ argv: args.slice(1) });
@@ -28043,6 +29724,9 @@ Subcommands:
28043
29724
  Use "sanctuary dashboard --help" for options.
28044
29725
  Pass --multi to render the multi-tenant overview.
28045
29726
 
29727
+ template Manage policy templates (list, init).
29728
+ Use "sanctuary template --help" for options.
29729
+
28046
29730
  agents List / inspect tenants on a multi-agent host.
28047
29731
  Use "sanctuary agents --help" for options.
28048
29732