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