@sanctuary-framework/mcp-server 1.2.0 → 1.2.2

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.
Files changed (36) hide show
  1. package/dist/cli.cjs +1952 -405
  2. package/dist/cli.cjs.map +1 -1
  3. package/dist/cli.js +1953 -406
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.cjs +1646 -305
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +105 -18
  8. package/dist/index.d.ts +105 -18
  9. package/dist/index.js +1646 -306
  10. package/dist/index.js.map +1 -1
  11. package/dist/templates/coding-assistant/commitments.json +14 -0
  12. package/dist/templates/coding-assistant/defaults.json +34 -0
  13. package/dist/templates/coding-assistant/onboarding.md +24 -0
  14. package/dist/templates/coding-assistant/policy.md +1 -0
  15. package/dist/templates/coding-assistant/template.json +23 -0
  16. package/dist/templates/handoff-coordinator/commitments.json +14 -0
  17. package/dist/templates/handoff-coordinator/defaults.json +10 -0
  18. package/dist/templates/handoff-coordinator/onboarding.md +23 -0
  19. package/dist/templates/handoff-coordinator/policy.md +1 -0
  20. package/dist/templates/handoff-coordinator/template.json +17 -0
  21. package/dist/templates/ops-runner/commitments.json +14 -0
  22. package/dist/templates/ops-runner/defaults.json +12 -0
  23. package/dist/templates/ops-runner/onboarding.md +25 -0
  24. package/dist/templates/ops-runner/policy.md +1 -0
  25. package/dist/templates/ops-runner/template.json +16 -0
  26. package/dist/templates/planner/commitments.json +9 -0
  27. package/dist/templates/planner/defaults.json +10 -0
  28. package/dist/templates/planner/onboarding.md +22 -0
  29. package/dist/templates/planner/policy.md +1 -0
  30. package/dist/templates/planner/template.json +8 -0
  31. package/dist/templates/research-assistant/commitments.json +9 -0
  32. package/dist/templates/research-assistant/defaults.json +25 -0
  33. package/dist/templates/research-assistant/onboarding.md +21 -0
  34. package/dist/templates/research-assistant/policy.md +1 -0
  35. package/dist/templates/research-assistant/template.json +8 -0
  36. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { argon2id } from 'hash-wasm';
12
12
  import { hkdf } from '@noble/hashes/hkdf';
13
13
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
14
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
- import { existsSync, readFileSync, statSync, mkdirSync, writeFileSync, renameSync, chmodSync } from 'fs';
15
+ import { existsSync, readFileSync, statSync, mkdirSync, writeFileSync, renameSync, chmodSync, readdirSync } from 'fs';
16
16
  import { fileURLToPath } from 'url';
17
17
  import { exec, execSync, spawn } from 'child_process';
18
18
  import { createServer as createServer$2, get as get$1 } from 'http';
@@ -391,23 +391,44 @@ var init_random = __esm({
391
391
  "src/core/random.ts"() {
392
392
  }
393
393
  });
394
- var FilesystemStorage;
394
+ function bijectiveEncode(name) {
395
+ return name.replace(
396
+ SAFE_CHARS,
397
+ (ch) => "!" + ch.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()
398
+ );
399
+ }
400
+ function legacyNamespaceSanitize(name) {
401
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
402
+ }
403
+ function legacyKeySanitize(name) {
404
+ return name.replace(/[^a-zA-Z0-9_.-]/g, "_");
405
+ }
406
+ var SAFE_CHARS, FilesystemStorage;
395
407
  var init_filesystem = __esm({
396
408
  "src/storage/filesystem.ts"() {
397
409
  init_random();
410
+ SAFE_CHARS = /[^A-Za-z0-9_.\-]/g;
398
411
  FilesystemStorage = class {
399
412
  basePath;
400
413
  constructor(basePath) {
401
414
  this.basePath = basePath;
402
415
  }
403
416
  entryPath(namespace, key) {
404
- const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
405
- const safeKey = key.replace(/[^a-zA-Z0-9_.-]/g, "_");
417
+ const safeNamespace = bijectiveEncode(namespace);
418
+ const safeKey = bijectiveEncode(key);
406
419
  return join(this.basePath, safeNamespace, `${safeKey}.enc`);
407
420
  }
408
421
  namespacePath(namespace) {
409
- const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
410
- return join(this.basePath, safeNamespace);
422
+ return join(this.basePath, bijectiveEncode(namespace));
423
+ }
424
+ // Legacy on-disk paths produced by the pre-#41 sanitizer. Returned for
425
+ // ENOENT-fallback in read/exists/delete; never written to.
426
+ legacyEntryPath(namespace, key) {
427
+ return join(
428
+ this.basePath,
429
+ legacyNamespaceSanitize(namespace),
430
+ `${legacyKeySanitize(key)}.enc`
431
+ );
411
432
  }
412
433
  async write(namespace, key, data) {
413
434
  const dirPath = this.namespacePath(namespace);
@@ -416,7 +437,13 @@ var init_filesystem = __esm({
416
437
  await writeFile(filePath, data, { mode: 384 });
417
438
  }
418
439
  async read(namespace, key) {
419
- const filePath = this.entryPath(namespace, key);
440
+ const buf = await this.readAtPath(this.entryPath(namespace, key));
441
+ if (buf !== null) return buf;
442
+ const legacy = this.legacyEntryPath(namespace, key);
443
+ if (legacy === this.entryPath(namespace, key)) return null;
444
+ return this.readAtPath(legacy);
445
+ }
446
+ async readAtPath(filePath) {
420
447
  try {
421
448
  const buf = await readFile(filePath);
422
449
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
@@ -428,7 +455,13 @@ var init_filesystem = __esm({
428
455
  }
429
456
  }
430
457
  async delete(namespace, key, secureOverwrite = true) {
431
- const filePath = this.entryPath(namespace, key);
458
+ const newPath = this.entryPath(namespace, key);
459
+ if (await this.deleteAtPath(newPath, secureOverwrite)) return true;
460
+ const legacy = this.legacyEntryPath(namespace, key);
461
+ if (legacy === newPath) return false;
462
+ return this.deleteAtPath(legacy, secureOverwrite);
463
+ }
464
+ async deleteAtPath(filePath, secureOverwrite) {
432
465
  try {
433
466
  if (secureOverwrite) {
434
467
  const fileStat = await stat(filePath);
@@ -474,12 +507,19 @@ var init_filesystem = __esm({
474
507
  }
475
508
  }
476
509
  async exists(namespace, key) {
477
- const filePath = this.entryPath(namespace, key);
510
+ const newPath = this.entryPath(namespace, key);
478
511
  try {
479
- await stat(filePath);
512
+ await stat(newPath);
480
513
  return true;
481
514
  } catch {
482
- return false;
515
+ const legacy = this.legacyEntryPath(namespace, key);
516
+ if (legacy === newPath) return false;
517
+ try {
518
+ await stat(legacy);
519
+ return true;
520
+ } catch {
521
+ return false;
522
+ }
483
523
  }
484
524
  }
485
525
  async totalSize() {
@@ -836,6 +876,14 @@ var init_identity = __esm({
836
876
  init_random();
837
877
  }
838
878
  });
879
+
880
+ // src/core/key-derivation.ts
881
+ var key_derivation_exports = {};
882
+ __export(key_derivation_exports, {
883
+ deriveMasterKey: () => deriveMasterKey,
884
+ deriveNamespaceKey: () => deriveNamespaceKey,
885
+ derivePurposeKey: () => derivePurposeKey
886
+ });
839
887
  async function deriveMasterKey(passphrase, existingParams) {
840
888
  const salt = existingParams ? fromBase64url(existingParams.salt) : generateSalt();
841
889
  const params = existingParams ?? {
@@ -1496,6 +1544,11 @@ var init_router = __esm({
1496
1544
  });
1497
1545
 
1498
1546
  // src/l1-cognitive/tools.ts
1547
+ var tools_exports = {};
1548
+ __export(tools_exports, {
1549
+ IdentityManager: () => IdentityManager,
1550
+ createL1Tools: () => createL1Tools
1551
+ });
1499
1552
  function getReservedNamespaceViolation(namespace) {
1500
1553
  for (const prefix of RESERVED_NAMESPACE_PREFIXES2) {
1501
1554
  if (namespace === prefix || namespace.startsWith(prefix + "/")) {
@@ -4874,6 +4927,35 @@ var init_types = __esm({
4874
4927
  }
4875
4928
  });
4876
4929
 
4930
+ // src/mesh/constants.ts
4931
+ function isReservedEventType(s) {
4932
+ return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
4933
+ }
4934
+ function isReservedExtensionKey(k) {
4935
+ return RESERVED_EXTENSION_ENVELOPE_KEYS.includes(k);
4936
+ }
4937
+ var PROTOCOL_VERSION, SIGNATURE_SCHEME_V1, RESERVED_EVENT_TYPE_PREFIXES, RESERVED_EXTENSION_ENVELOPE_KEYS;
4938
+ var init_constants = __esm({
4939
+ "src/mesh/constants.ts"() {
4940
+ PROTOCOL_VERSION = "0.1";
4941
+ SIGNATURE_SCHEME_V1 = "ed25519-v1";
4942
+ RESERVED_EVENT_TYPE_PREFIXES = [
4943
+ "EXTENSION_",
4944
+ "cross_fortress_",
4945
+ "multi_master_"
4946
+ ];
4947
+ RESERVED_EXTENSION_ENVELOPE_KEYS = [
4948
+ "cross_fortress_read_grant",
4949
+ "cross_fortress_read_query",
4950
+ "cross_fortress_read_response",
4951
+ "multi_master_policy_merge",
4952
+ "audit_replication_full_n_way",
4953
+ "auto_promote_canonical_audit",
4954
+ "agent_live_migration"
4955
+ ];
4956
+ }
4957
+ });
4958
+
4877
4959
  // src/shr/generator.ts
4878
4960
  function deriveL4Degradations(evidence, now = /* @__PURE__ */ new Date()) {
4879
4961
  const out = [];
@@ -5022,6 +5104,7 @@ function generateSHR(identityId, opts) {
5022
5104
  return {
5023
5105
  body,
5024
5106
  signed_by: identity.public_key,
5107
+ signature_scheme: SIGNATURE_SCHEME_V1,
5025
5108
  signature: toBase64url(signatureBytes)
5026
5109
  };
5027
5110
  }
@@ -5032,6 +5115,7 @@ var init_generator = __esm({
5032
5115
  init_identity();
5033
5116
  init_encoding();
5034
5117
  init_key_derivation();
5118
+ init_constants();
5035
5119
  DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
5036
5120
  DEFAULT_FRESHNESS_WINDOW_DAYS = 30;
5037
5121
  DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD = 0.6;
@@ -7270,14 +7354,14 @@ function generateDashboardHTML(options) {
7270
7354
  // cookie (set by /auth/session and sent automatically by the
7271
7355
  // browser) or as a ?session= query parameter, both of which Stack
7272
7356
  // A's checkAuth honours. Loopback callers also bypass auth via the
7273
- // v0.10.2 _autoAuthLocalhost path, which is the path moltbook
7357
+ // v0.10.2 _autoAuthLocalhost path, which is the path Mini1
7274
7358
  // hits when the dashboard is auto-opened on 127.0.0.1.
7275
7359
  //
7276
7360
  // The endpoint itself is /events \u2014 Stack A's route table mounts it
7277
7361
  // there, and the previous /api/events URL was a 404 in every real
7278
7362
  // boot from v0.10.0 through v0.10.4. The retry loop that result
7279
7363
  // produced is exactly the "status bar flashing blue continuously"
7280
- // moltbook reported on v0.10.4.
7364
+ // Mini1 reported on v0.10.4.
7281
7365
  const eventSource = new EventSource(API_BASE + '/events');
7282
7366
 
7283
7367
  eventSource.addEventListener('init', (e) => {
@@ -10302,8 +10386,8 @@ var init_html = __esm({
10302
10386
  function isPolicySlot(value) {
10303
10387
  return typeof value === "string" && POLICY_SLOTS.includes(value);
10304
10388
  }
10305
- var COMPILED_POLICY_SCHEMA_VERSION, POLICY_UPDATE_EVENT_TYPE, POLICY_SLOTS, CHANNEL_TEMPLATE_IDS, COUNTERPARTY_WILDCARD, BUDGET_UNITS;
10306
- var init_constants = __esm({
10389
+ var COMPILED_POLICY_SCHEMA_VERSION, POLICY_UPDATE_EVENT_TYPE, POLICY_SLOTS, CHANNEL_TEMPLATE_IDS, BUDGET_UNITS;
10390
+ var init_constants2 = __esm({
10307
10391
  "src/policy-engine/constants.ts"() {
10308
10392
  COMPILED_POLICY_SCHEMA_VERSION = "0.1";
10309
10393
  POLICY_UPDATE_EVENT_TYPE = "policy_update";
@@ -10318,10 +10402,8 @@ var init_constants = __esm({
10318
10402
  "read-then-report",
10319
10403
  "scheduled-digest",
10320
10404
  "plan-draft-only",
10321
- "fortress-relay",
10322
- "concierge-loop"
10405
+ "fortress-relay"
10323
10406
  ];
10324
- COUNTERPARTY_WILDCARD = "*";
10325
10407
  BUDGET_UNITS = ["tokens", "usd"];
10326
10408
  }
10327
10409
  });
@@ -10476,8 +10558,12 @@ function lintOnboarding(_name, content) {
10476
10558
  function resolveTemplatesDir() {
10477
10559
  const thisFile = fileURLToPath(import.meta.url);
10478
10560
  const thisDir = dirname(thisFile);
10479
- if (thisDir.includes("/dist/")) {
10480
- return thisDir.replace("/dist/templates", "/src/templates");
10561
+ if (thisDir.includes("/dist")) {
10562
+ const templatesSubdir = join(thisDir, "templates");
10563
+ const candidateDir = existsSync(join(thisDir, TEMPLATE_NAMES[0])) ? thisDir : existsSync(join(templatesSubdir, TEMPLATE_NAMES[0])) ? templatesSubdir : null;
10564
+ if (candidateDir) return candidateDir;
10565
+ const srcFallback = thisDir.endsWith("/templates") ? thisDir.replace("/dist/templates", "/src/templates") : join(thisDir.replace("/dist", "/src"), "templates");
10566
+ return srcFallback;
10481
10567
  }
10482
10568
  return thisDir;
10483
10569
  }
@@ -10544,7 +10630,7 @@ function getTemplateEntry(name) {
10544
10630
  var TEMPLATE_NAMES, TemplateValidationError, _cache;
10545
10631
  var init_registry = __esm({
10546
10632
  "src/templates/registry.ts"() {
10547
- init_constants();
10633
+ init_constants2();
10548
10634
  TEMPLATE_NAMES = [
10549
10635
  "research-assistant",
10550
10636
  "coding-assistant",
@@ -10698,10 +10784,10 @@ function applyChannelTemplate(id, params) {
10698
10784
  if (!entry) throw new Error(`unknown channel template: ${id}`);
10699
10785
  return entry.factory(params);
10700
10786
  }
10701
- var requestApproveAct, readThenReport, scheduledDigest, planDraftOnly, fortressRelay, conciergeLoop, REGISTRY;
10787
+ var requestApproveAct, readThenReport, scheduledDigest, planDraftOnly, fortressRelay, REGISTRY;
10702
10788
  var init_channel_templates = __esm({
10703
10789
  "src/policy-engine/channel-templates.ts"() {
10704
- init_constants();
10790
+ init_constants2();
10705
10791
  init_null_policy();
10706
10792
  requestApproveAct = (params) => {
10707
10793
  const p = basePolicy(params);
@@ -10784,23 +10870,6 @@ var init_channel_templates = __esm({
10784
10870
  setRetentionDays(p, 90);
10785
10871
  return p;
10786
10872
  };
10787
- conciergeLoop = (params) => {
10788
- const p = basePolicy(params);
10789
- p.source_english = "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward.";
10790
- grantOn(p, "memory", {
10791
- counterparty: params.counterparty,
10792
- action: "read",
10793
- scope: { local_fortress_state_only: true, ...params.scope ?? {} }
10794
- });
10795
- grantOn(p, "outputs", {
10796
- counterparty: params.counterparty || COUNTERPARTY_WILDCARD,
10797
- action: "read",
10798
- scope: { operator_chat_only: true, ...params.scope ?? {} }
10799
- });
10800
- p.egress = { allowlist: [] };
10801
- setRetentionDays(p, 14);
10802
- return p;
10803
- };
10804
10873
  REGISTRY = {
10805
10874
  "request-approve-act": {
10806
10875
  id: "request-approve-act",
@@ -10836,13 +10905,6 @@ var init_channel_templates = __esm({
10836
10905
  label: "Fortress relay",
10837
10906
  description: "Routes signed events between peer fortresses. Commits bind only when both sides sign.",
10838
10907
  factory: fortressRelay
10839
- },
10840
- "concierge-loop": {
10841
- id: "concierge-loop",
10842
- severity: "LOW",
10843
- label: "Concierge loop",
10844
- description: "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward.",
10845
- factory: conciergeLoop
10846
10908
  }
10847
10909
  };
10848
10910
  }
@@ -10912,8 +10974,14 @@ function encode(value) {
10912
10974
  }
10913
10975
  function encodeArray(arr) {
10914
10976
  const parts = [];
10915
- for (const item of arr) {
10916
- parts.push(item === void 0 ? "null" : encode(item));
10977
+ for (let i = 0; i < arr.length; i++) {
10978
+ const item = arr[i];
10979
+ if (item === void 0) {
10980
+ throw new MeshCanonicalJsonError(
10981
+ `canonicalize(): undefined is not a valid JSON value at array index ${i}`
10982
+ );
10983
+ }
10984
+ parts.push(encode(item));
10917
10985
  }
10918
10986
  return "[" + parts.join(",") + "]";
10919
10987
  }
@@ -11231,45 +11299,17 @@ var init_canonical_policy = __esm({
11231
11299
  "src/policy-engine/canonical-policy.ts"() {
11232
11300
  init_canonical_json();
11233
11301
  init_encoding();
11234
- init_constants();
11302
+ init_constants2();
11235
11303
  init_errors2();
11236
11304
  }
11237
11305
  });
11238
-
11239
- // src/mesh/constants.ts
11240
- function isReservedEventType(s) {
11241
- return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
11242
- }
11243
- function isReservedExtensionKey(k) {
11244
- return RESERVED_EXTENSION_ENVELOPE_KEYS.includes(k);
11245
- }
11246
- var PROTOCOL_VERSION, RESERVED_EVENT_TYPE_PREFIXES, RESERVED_EXTENSION_ENVELOPE_KEYS;
11247
- var init_constants2 = __esm({
11248
- "src/mesh/constants.ts"() {
11249
- PROTOCOL_VERSION = "0.1";
11250
- RESERVED_EVENT_TYPE_PREFIXES = [
11251
- "EXTENSION_",
11252
- "cross_fortress_",
11253
- "multi_master_"
11254
- ];
11255
- RESERVED_EXTENSION_ENVELOPE_KEYS = [
11256
- "cross_fortress_read_grant",
11257
- "cross_fortress_read_query",
11258
- "cross_fortress_read_response",
11259
- "multi_master_policy_merge",
11260
- "audit_replication_full_n_way",
11261
- "auto_promote_canonical_audit",
11262
- "agent_live_migration"
11263
- ];
11264
- }
11265
- });
11266
11306
  var init_trust_root = __esm({
11267
11307
  "src/mesh/trust-root.ts"() {
11268
11308
  init_encoding();
11269
11309
  init_identity();
11270
11310
  init_random();
11271
11311
  init_canonical_json();
11272
- init_constants2();
11312
+ init_constants();
11273
11313
  init_errors();
11274
11314
  }
11275
11315
  });
@@ -11322,7 +11362,7 @@ var init_envelope = __esm({
11322
11362
  init_encoding();
11323
11363
  init_random();
11324
11364
  init_canonical_json();
11325
- init_constants2();
11365
+ init_constants();
11326
11366
  init_errors();
11327
11367
  init_trust_root();
11328
11368
  }
@@ -11352,7 +11392,7 @@ function packPolicyUpdate(params) {
11352
11392
  }
11353
11393
  var init_envelope2 = __esm({
11354
11394
  "src/policy-engine/envelope.ts"() {
11355
- init_constants();
11395
+ init_constants2();
11356
11396
  init_canonical_policy();
11357
11397
  init_errors2();
11358
11398
  init_envelope();
@@ -11486,7 +11526,7 @@ function initTemplate(params) {
11486
11526
  var init_init = __esm({
11487
11527
  "src/templates/init.ts"() {
11488
11528
  init_channel_templates();
11489
- init_constants();
11529
+ init_constants2();
11490
11530
  init_canonical_policy();
11491
11531
  init_envelope2();
11492
11532
  init_registry();
@@ -11764,7 +11804,7 @@ function deriveMachineKey(home) {
11764
11804
  return hkdf(sha256, material, void 0, "sanctuary-passphrase-v1", 32);
11765
11805
  }
11766
11806
  async function defaultExec(cmd, args, input) {
11767
- return new Promise((resolve5, reject) => {
11807
+ return new Promise((resolve6, reject) => {
11768
11808
  const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
11769
11809
  let stdout = "";
11770
11810
  let stderr = "";
@@ -11775,7 +11815,7 @@ async function defaultExec(cmd, args, input) {
11775
11815
  stderr += d.toString();
11776
11816
  });
11777
11817
  child.on("error", reject);
11778
- child.on("close", (code) => resolve5({ stdout, stderr, code }));
11818
+ child.on("close", (code) => resolve6({ stdout, stderr, code }));
11779
11819
  if (input !== void 0) {
11780
11820
  child.stdin.write(input);
11781
11821
  }
@@ -12897,7 +12937,7 @@ async function api(path, opts) {
12897
12937
  // /policies, /activity responses on subsequent GETs even when the
12898
12938
  // server-side state has changed (e.g. recent-failures buffer cleared
12899
12939
  // on substrate flip). The pre-rc.5 client used bare fetch with no
12900
- // cache control, which on moltbook Safari produced a stale view of
12940
+ // cache control, which on Mini1 Safari produced a stale view of
12901
12941
  // server state and made the operator-visible badge color stick to
12902
12942
  // its prior value across substrate changes. Belt + suspenders:
12903
12943
  // cache: "no-store" turns off the response cache; the _t query
@@ -13059,12 +13099,6 @@ const CHANNEL_TEMPLATES = [
13059
13099
  severity: "MEDIUM",
13060
13100
  title: "Fortress relay",
13061
13101
  description: "Routes signed events between peer fortresses. Commits bind only when both sides sign."
13062
- },
13063
- {
13064
- id: "concierge-loop",
13065
- severity: "LOW",
13066
- title: "Concierge loop",
13067
- description: "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward."
13068
13102
  }
13069
13103
  ];
13070
13104
 
@@ -13111,6 +13145,24 @@ function setRoute(route) {
13111
13145
  renderFortress();
13112
13146
  }
13113
13147
 
13148
+ // Renders the global attestation badge (Q1 layer 1, persistent across
13149
+ // surfaces). Tone is driven by state.topbarPills.attestation. Pending
13150
+ // state shows a dashed seal ring; verified shows solid; degraded shows
13151
+ // outlined core; unverified shows the broken-seal mark. Observation
13152
+ // language only; Castle Layer 1 enforcement ships in WP-V1.x-CASTLE-WALL.
13153
+ function renderTopbarAttestationBadge(stateName) {
13154
+ const valid = stateName === "verified" || stateName === "degraded" || stateName === "unverified" || stateName === "pending";
13155
+ const cls = valid ? stateName : "pending";
13156
+ const ringDashed = cls === "pending" ? " dashed" : "";
13157
+ return '<span class="att-global ' + cls + '" data-pill="attestation" title="Fortress attestation">' +
13158
+ '<span class="seal">' +
13159
+ '<span class="seal-ring' + ringDashed + '"></span>' +
13160
+ '<span class="seal-core"></span>' +
13161
+ '</span>' +
13162
+ '<span class="label">' + escHtml(cls) + '</span>' +
13163
+ '</span>';
13164
+ }
13165
+
13114
13166
  function renderTopbar() {
13115
13167
  const pillEl = document.getElementById("topbar-pills");
13116
13168
  if (!pillEl) return;
@@ -13127,7 +13179,7 @@ function renderTopbar() {
13127
13179
  versionPill,
13128
13180
  '<span class="pill" data-pill="deployment">deployment: ' + escHtml(state.topbarPills.deployment) + '</span>',
13129
13181
  '<span class="pill" data-pill="mode">mode: ' + escHtml(state.topbarPills.mode) + '</span>',
13130
- '<span class="pill tone-' + escHtml(state.topbarPills.attestation) + '" data-pill="attestation">attestation: ' + escHtml(state.topbarPills.attestation) + '</span>'
13182
+ renderTopbarAttestationBadge(state.topbarPills.attestation)
13131
13183
  ].join("");
13132
13184
  // Lockdown button three-state UX (binding addendum 3).
13133
13185
  const btn = document.getElementById("btn-lockdown");
@@ -13223,6 +13275,7 @@ function renderMain() {
13223
13275
  case "agent-detail": nextHtml = renderAgentDetail(); break;
13224
13276
  case "policy": nextHtml = renderPolicyCenter(); break;
13225
13277
  case "intelligence": nextHtml = renderIntelligenceCenter(); break;
13278
+ case "attestation": nextHtml = renderAttestation(); break;
13226
13279
  case "privacy": nextHtml = renderPrivacyPage(); break;
13227
13280
  case "coordination": nextHtml = renderCoordinationPage(); break;
13228
13281
  case "health": nextHtml = renderHealthPage(); break;
@@ -13301,9 +13354,9 @@ function renderMain() {
13301
13354
  // "Concierge unavailable; substrate not configured") sourced from the
13302
13355
  // last response's served_by + display_label.
13303
13356
  const CONCIERGE_SUGGESTIONS = [
13304
- { id: "summarize-hour", label: "summarize the last hour", query: "Summarize what happened in this fortress in the last hour." },
13305
- { id: "agent-touched", label: "what has each agent touched today", query: "What has each wrapped agent done today? Group by agent." },
13306
- { id: "open-approvals", label: "any open approvals?", query: "Are there any open Tier 1 approvals or pending inbox items I should look at?" }
13357
+ { id: "summarize-hour", category: "Summarize", label: "summarize the last hour", query: "Summarize what happened in this fortress in the last hour." },
13358
+ { id: "agent-touched", category: "Inspect", label: "what has each agent touched today", query: "What has each wrapped agent done today? Group by agent." },
13359
+ { id: "open-approvals", category: "Approvals", label: "any open approvals?", query: "Are there any open Tier 1 approvals or pending inbox items I should look at?" }
13307
13360
  ];
13308
13361
 
13309
13362
  // Direct-agent chat surface was removed in the v1.2 reshape; the
@@ -13320,59 +13373,316 @@ function renderDashboardConcierge() {
13320
13373
  const badge = c.badge && c.badge.displayLabel
13321
13374
  ? '<span class="pill mono concierge-badge" title="Substrate that served the most recent response">' + escHtml(c.badge.displayLabel) + '</span>'
13322
13375
  : '<span class="pill muted concierge-badge">Concierge: substrate not yet contacted</span>';
13323
- const messages = c.messages.length
13376
+ const sendDisabled = c.sending ? ' disabled' : '';
13377
+ const sendLabel = c.sending ? 'Sending...' : 'Send';
13378
+ // Sprint Piece 2 PR 2: empty state lives INSIDE the concierge-history
13379
+ // container so the DDD e2e selector .concierge-history matches both
13380
+ // empty and active state. The container's flex layout hosts a single
13381
+ // .concierge-empty child that fills the available height with a serif
13382
+ // headline and a 3-up suggest grid; the grid replaces the v1.2 bottom
13383
+ // chip row, which is retired with this polish.
13384
+ const emptyState =
13385
+ '<div class="concierge-empty">' +
13386
+ '<div class="concierge-empty-headline">' +
13387
+ '<h2>Where would you like to begin.</h2>' +
13388
+ '<p>Ask anything about your fortress. Sanctuary holds your context, your agents, your policy. It will answer plainly, or hand you to the right surface.</p>' +
13389
+ '</div>' +
13390
+ '<div class="concierge-suggest-grid">' +
13391
+ CONCIERGE_SUGGESTIONS.map(function (s) {
13392
+ return '<button class="concierge-suggest" data-action="concierge-suggestion" data-suggestion-id="' + escHtml(s.id) + '"' + sendDisabled + '>' +
13393
+ '<span class="label">' + escHtml(s.category || '') + '</span>' +
13394
+ escHtml(s.label) +
13395
+ '</button>';
13396
+ }).join("") +
13397
+ '</div>' +
13398
+ '</div>';
13399
+ const messagesHtml = c.messages.length
13324
13400
  ? c.messages.map(function (m) {
13325
13401
  const cls = m.role === "operator" ? "concierge-msg-operator" : "concierge-msg-concierge";
13326
- const author = m.role === "operator" ? "you" : "Sanctuary Fortress concierge";
13402
+ const authorLabel = m.role === "operator" ? "you" : "sanctuary";
13403
+ const metaParts = [];
13404
+ if (m.created_at) metaParts.push(escHtml(shortTime(m.created_at)));
13405
+ if (m.role === "concierge" && m.served_by) metaParts.push('substrate: ' + escHtml(m.served_by));
13406
+ const meta = metaParts.length
13407
+ ? '<div class="concierge-msg-meta"><span>' + metaParts.join(' · ') + '</span></div>'
13408
+ : '';
13327
13409
  return '<div class="concierge-msg ' + cls + '">' +
13328
- '<div class="concierge-msg-author muted">' + escHtml(author) + ' · ' + escHtml(shortTime(m.created_at)) + '</div>' +
13410
+ '<span class="concierge-msg-author">' + escHtml(authorLabel) + '</span>' +
13329
13411
  '<div class="concierge-msg-body">' + escHtml(m.body) + '</div>' +
13412
+ meta +
13330
13413
  '</div>';
13331
13414
  }).join("\n")
13332
- : '<p class="muted concierge-empty">No messages yet. Ask the concierge anything about your fortress: it can summarize agent activity, surface open approvals, or describe the current policy.</p>';
13415
+ : emptyState;
13333
13416
  const errorBanner = c.error
13334
13417
  ? '<div class="banner banner-warn">' + escHtml(c.error) + '</div>'
13335
13418
  : "";
13336
- const sendDisabled = c.sending ? ' disabled' : '';
13337
- const sendLabel = c.sending ? 'Sending...' : 'Send';
13338
- const chips = CONCIERGE_SUGGESTIONS.map(function (s) {
13339
- return '<button class="btn chip" data-action="concierge-suggestion" data-suggestion-id="' + escHtml(s.id) + '"' + sendDisabled + '>' + escHtml(s.label) + '</button>';
13340
- }).join("\n");
13341
13419
  const activeChatsPanel = renderActiveChatsPanel();
13342
13420
  return [
13343
- '<h1>Chat <span class="muted">/ This fortress</span></h1>',
13344
- activeChatsPanel,
13345
- '<div class="card concierge-card">',
13346
- '<div class="concierge-header">',
13347
- '<div class="concierge-persona"><strong>Sanctuary Fortress concierge</strong> <span class="muted">read-only over fortress state</span></div>',
13348
- badge,
13421
+ '<div class="concierge-wrap">',
13422
+ '<div class="page-head"><div>',
13423
+ '<p class="eyebrow">Concierge</p>',
13424
+ '<h1>Talk to your fortress.</h1>',
13425
+ '<p class="sub">A direct line to Sanctuary, routed through the substrate you chose. Nothing leaves without your hand on it.</p>',
13426
+ '</div></div>',
13427
+ activeChatsPanel,
13428
+ '<div class="card concierge-card">',
13429
+ '<div class="concierge-header">',
13430
+ '<div class="concierge-persona">',
13431
+ '<div class="glyph-ring"></div>',
13432
+ '<div class="concierge-persona-text"><strong>Sanctuary Fortress concierge</strong><small>read-only over fortress state</small></div>',
13433
+ '</div>',
13434
+ '<div class="concierge-meta">' + badge + '</div>',
13435
+ '</div>',
13436
+ errorBanner,
13437
+ '<div class="concierge-history" id="concierge-history">' + messagesHtml + '</div>',
13438
+ '<form class="concierge-composer" data-action="concierge-submit">',
13439
+ '<div class="input-wrap">',
13440
+ '<input type="text" name="concierge-input" placeholder="Type to Sanctuary. Enter to send." value="' + escHtml(c.composer) + '" data-action="concierge-input"' + sendDisabled + ' autocomplete="off">',
13441
+ '<span class="composer-meta">Enter</span>',
13442
+ '</div>',
13443
+ '<button type="submit" class="btn btn-primary" data-action="concierge-send"' + sendDisabled + '>' + escHtml(sendLabel) + '</button>',
13444
+ '</form>',
13445
+ '<p class="muted concierge-foot">First time? <a href="#intelligence">Pick a substrate</a> to enable concierge replies.</p>',
13349
13446
  '</div>',
13350
- errorBanner,
13351
- '<div class="concierge-history" id="concierge-history">' + messages + '</div>',
13352
- '<form class="concierge-composer" data-action="concierge-submit">',
13353
- '<input type="text" name="concierge-input" placeholder="Ask the concierge about this fortress..." value="' + escHtml(c.composer) + '" data-action="concierge-input"' + sendDisabled + ' autocomplete="off">',
13354
- '<button type="submit" class="btn btn-primary" data-action="concierge-send"' + sendDisabled + '>' + escHtml(sendLabel) + '</button>',
13355
- '</form>',
13356
- '<div class="concierge-chips">' + chips + '</div>',
13357
- '<p class="muted concierge-foot">First time? <a href="#intelligence">Pick a substrate</a> to enable concierge replies.</p>',
13358
13447
  '</div>'
13359
13448
  ].join("");
13360
13449
  }
13361
13450
 
13362
13451
  // ── Render: agents list / detail ───────────────────────────────────────
13452
+ //
13453
+ // Sprint Piece 2 PR 4 polish: empty state uses .agents-empty with the
13454
+ // concentric icon-frame + a terminal-block CTA. Populated state uses the
13455
+ // .agents-layout grid with the .agents-list 4-column table (Agent /
13456
+ // State / Attestation / Last seen). The empty-state branch keeps the
13457
+ // literal '<h1>Agents</h1>' start and the "No wrapped agents yet." copy
13458
+ // because agents-empty-state-canary.test.ts pins both.
13459
+ function agentInitials(agentId) {
13460
+ const tail = String(agentId || "").split(":").pop() || "";
13461
+ const cleaned = tail.replace(/[^a-zA-Z0-9]/g, "");
13462
+ return (cleaned.slice(0, 2) || "??").toUpperCase();
13463
+ }
13464
+ function agentStateClass(status) {
13465
+ if (status === "active") return "live";
13466
+ if (status === "locked_down" || status === "error") return "off";
13467
+ return "idle";
13468
+ }
13469
+ // Per-agent attestation badge (Q1 layer 2). Square chip beside each
13470
+ // agent: a bounded glyph beside a bounded entity. Color and fill pattern
13471
+ // carry meaning together so the badge reads even monochrome. The "locked"
13472
+ // status maps to the unverified visual (rust + hatched mark) since a
13473
+ // locked-down agent has no current attestation; the inspect-pane copy
13474
+ // explains the distinction. Pure visual surface; no state derivation.
13475
+ function renderAgentAttestationBadge(status) {
13476
+ let cls;
13477
+ let label;
13478
+ if (status === "active") { cls = "verified"; label = "verified"; }
13479
+ else if (status === "locked_down") { cls = "unverified"; label = "locked"; }
13480
+ else if (status === "error") { cls = "unverified"; label = "unverified"; }
13481
+ else { cls = "degraded"; label = "degraded"; }
13482
+ return '<span class="att-agent ' + cls + '" title="Agent attestation"><span class="mark"></span>' + escHtml(label) + '</span>';
13483
+ }
13484
+ // Per-action attestation tick (Q1 layer 3). Tiny inline shape on every
13485
+ // timeline row. Two-byte signature fragment is enough at low resolution;
13486
+ // the full signature is one click away. Neutral state shows a circle
13487
+ // instead of a tick when the signer was unreachable; the action is still
13488
+ // recorded. Visual surface only.
13489
+ function renderActionAttestationBadge(stateName, sig) {
13490
+ const valid = stateName === "verified" || stateName === "degraded" || stateName === "unverified" || stateName === "neutral";
13491
+ const cls = valid ? stateName : "neutral";
13492
+ const sigText = sig ? String(sig) : "--";
13493
+ return '<span class="att-action ' + cls + '" title="Action attestation">' +
13494
+ '<span class="tick"></span>' +
13495
+ '<span>' + escHtml(sigText) + '</span>' +
13496
+ '</span>';
13497
+ }
13498
+ // Attestation gallery surface (Q1 four classes: global / per-agent /
13499
+ // per-action / per-transaction custody-provenance stub). Reference for
13500
+ // operators: shows what each badge looks like across verified, degraded,
13501
+ // unverified, and (where applicable) pending or neutral states. Pure
13502
+ // visual; no derivation, no live data. Castle Layer 3 cooperative-MCP UX
13503
+ // surface; Castle Layer 1 enforcement ships in WP-V1.x-CASTLE-WALL.
13504
+ function renderAttestation() {
13505
+ return '<div class="att-gallery">' +
13506
+ '<div class="page-head"><div>' +
13507
+ '<p class="eyebrow">Attestation</p>' +
13508
+ '<h1>Four classes of badge.</h1>' +
13509
+ '<p class="sub">A signature you can see. From the whole fortress, down to a single action. Degrade, never destroy: a failed signature becomes neutral with a tooltip; the surface keeps working.</p>' +
13510
+ '</div></div>' +
13511
+ // Global
13512
+ '<div class="att-section">' +
13513
+ '<div class="att-section-head"><div>' +
13514
+ '<h2>Global. The fortress itself.</h2>' +
13515
+ '<p>Lives in the topbar. Visible on every surface. Tells you the fortress identity is currently signed and matches the binary you installed.</p>' +
13516
+ '</div><span class="label">topbar</span></div>' +
13517
+ attRow(renderTopbarAttestationBadge("verified"), "Verified", "Identity matches. Binary matches. Default state for a healthy fortress.") +
13518
+ attRow(renderTopbarAttestationBadge("degraded"), "Degraded", "The signature is older than the staleness window, or one of two co-signers is unreachable. The fortress keeps running.") +
13519
+ attRow(renderTopbarAttestationBadge("unverified"), "Unverified", "The signature did not validate. The surface still works; lockdown is still available; the badge tells you to investigate.") +
13520
+ attRow(renderTopbarAttestationBadge("pending"), "Pending", "First-run state. Fortress is signing for the first time. Settles in seconds.") +
13521
+ '</div>' +
13522
+ // Per-agent
13523
+ '<div class="att-section">' +
13524
+ '<div class="att-section-head"><div>' +
13525
+ '<h2>Per-agent. In the agents list and inspect pane.</h2>' +
13526
+ '<p>A square chip beside each agent. Square because an agent is bounded; the fortress (a circle) contains it.</p>' +
13527
+ '</div><span class="label">agents view</span></div>' +
13528
+ '<div class="att-row">' +
13529
+ '<div class="demo" style="display:flex; gap:8px; flex-wrap:wrap;">' +
13530
+ renderAgentAttestationBadge("active") +
13531
+ renderAgentAttestationBadgeForState("degraded", "degraded") +
13532
+ renderAgentAttestationBadgeForState("unverified", "unverified") +
13533
+ '</div>' +
13534
+ '<div class="desc"><strong>Verified, degraded, unverified</strong>' +
13535
+ '<small>Color and the fill pattern carry meaning together. A solid square reads "attested" at a glance; a hatched square reads "trouble" at a glance, even monochrome.</small>' +
13536
+ '</div>' +
13537
+ '</div>' +
13538
+ '</div>' +
13539
+ // Per-action
13540
+ '<div class="att-section">' +
13541
+ '<div class="att-section-head"><div>' +
13542
+ '<h2>Per-action. Inline in the activity timeline.</h2>' +
13543
+ '<p>Each entry in any timeline carries a small signature fragment. Hover to expand. A tick instead of a fill keeps the row visually quiet at low resolution.</p>' +
13544
+ '</div><span class="label">timeline</span></div>' +
13545
+ '<div class="att-row">' +
13546
+ '<div class="demo" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">' +
13547
+ '<span style="font-size:13px; color:var(--ink-2);">14:22:08 doc-reviewer summarized intake.pdf</span>' +
13548
+ renderActionAttestationBadge("verified", "9c7d..2a") +
13549
+ '</div>' +
13550
+ '<div class="desc"><strong>Verified action</strong>' +
13551
+ '<small>The most common shape. Two-byte signature fragment is enough; the full signature is one click away.</small>' +
13552
+ '</div>' +
13553
+ '</div>' +
13554
+ '<div class="att-row">' +
13555
+ '<div class="demo" style="display:flex; gap:10px; align-items:center;">' +
13556
+ '<span style="font-size:13px; color:var(--ink-2);">14:11:47 privacy filter redacted payload</span>' +
13557
+ renderActionAttestationBadge("degraded", "b440..71") +
13558
+ '</div>' +
13559
+ '<div class="desc"><strong>Degraded action</strong>' +
13560
+ '<small>The action signed, but the signature class was less than the policy preferred. Useful when a substrate is still warming up.</small>' +
13561
+ '</div>' +
13562
+ '</div>' +
13563
+ '<div class="att-row">' +
13564
+ '<div class="demo" style="display:flex; gap:10px; align-items:center;">' +
13565
+ '<span style="font-size:13px; color:var(--ink-2);">14:09:02 agent attempted external link</span>' +
13566
+ renderActionAttestationBadge("neutral", "--") +
13567
+ '</div>' +
13568
+ '<div class="desc"><strong>Neutral. Degrade, not destroy.</strong>' +
13569
+ '<small>The signer was unreachable. Rather than hide the action, the badge becomes neutral and a tooltip explains. The action is still recorded.</small>' +
13570
+ '</div>' +
13571
+ '</div>' +
13572
+ '</div>' +
13573
+ // Custody stub
13574
+ '<div class="att-section">' +
13575
+ '<div class="att-section-head"><div>' +
13576
+ '<h2>Custody. Stub for v1.x.</h2>' +
13577
+ '<p>A fourth class, surfaced conservatively. Reserved for forthcoming custody-provenance signatures (x402 payment receipts, ERC-8004 identity assertions). Visible, dashed, clearly stubbed.</p>' +
13578
+ '</div><span class="label">stub</span></div>' +
13579
+ '<div class="att-row">' +
13580
+ '<div class="demo">' +
13581
+ '<span class="att-custody" title="Custody-provenance, v1.x">' +
13582
+ '<span class="seal-stub"></span>' +
13583
+ '<span class="stub-tag">custody. stub</span>' +
13584
+ '</span>' +
13585
+ '</div>' +
13586
+ '<div class="desc"><strong>Custody. Stub.</strong>' +
13587
+ '<small>Dashed border signals "shape reserved, content pending." Will populate when custody signatures land in a future release. Cannot be confused with a verified badge at any zoom level.</small>' +
13588
+ '</div>' +
13589
+ '</div>' +
13590
+ '</div>' +
13591
+ // Tooltip
13592
+ '<div class="att-section">' +
13593
+ '<div class="att-section-head"><div>' +
13594
+ '<h2>Tooltip on failure.</h2>' +
13595
+ '<p>A failed badge is never silent. The tooltip explains in plain language, suggests one action, and confirms the surface is still working.</p>' +
13596
+ '</div><span class="label">degrade not destroy</span></div>' +
13597
+ '<div class="att-row">' +
13598
+ '<div class="demo">' +
13599
+ '<span class="att-tooltip">The signer at sig.fortress.local did not respond in 4s. Your fortress kept working. Try: open Health to see the signer status.</span>' +
13600
+ '</div>' +
13601
+ '<div class="desc"><strong>Plain-language tooltip</strong>' +
13602
+ '<small>Three lines, in order: what happened, what did not break, what to do. No jargon, no stack trace.</small>' +
13603
+ '</div>' +
13604
+ '</div>' +
13605
+ '</div>' +
13606
+ '</div>';
13607
+ }
13608
+ function attRow(demoHtml, strong, smallText) {
13609
+ return '<div class="att-row">' +
13610
+ '<div class="demo">' + demoHtml + '</div>' +
13611
+ '<div class="desc"><strong>' + escHtml(strong) + '</strong>' +
13612
+ '<small>' + escHtml(smallText) + '</small>' +
13613
+ '</div>' +
13614
+ '</div>';
13615
+ }
13616
+ // Gallery-only variant: render a per-agent badge for a given visual state
13617
+ // (verified / degraded / unverified) without going through the agent
13618
+ // status mapping. Used by renderAttestation to show all three states
13619
+ // side by side as design reference.
13620
+ function renderAgentAttestationBadgeForState(cls, label) {
13621
+ return '<span class="att-agent ' + escHtml(cls) + '" title="Agent attestation"><span class="mark"></span>' + escHtml(label) + '</span>';
13622
+ }
13623
+ function relTimeFromIso(iso) {
13624
+ if (!iso) return "";
13625
+ const d = new Date(iso);
13626
+ if (isNaN(d.getTime())) return iso;
13627
+ const diffMs = Date.now() - d.getTime();
13628
+ const diffSec = Math.max(0, Math.floor(diffMs / 1000));
13629
+ if (diffSec < 60) return diffSec + "s ago";
13630
+ const diffMin = Math.floor(diffSec / 60);
13631
+ if (diffMin < 60) return diffMin + "m ago";
13632
+ const diffHr = Math.floor(diffMin / 60);
13633
+ if (diffHr < 24) return diffHr + "h ago";
13634
+ const diffDay = Math.floor(diffHr / 24);
13635
+ return diffDay + "d ago";
13636
+ }
13363
13637
  function renderAgentsList() {
13364
- if (!state.agents.length) return '<h1>Agents</h1><p class="muted">No wrapped agents yet. Run <code>sanctuary wrap</code> to wrap a harness.</p>';
13638
+ if (!state.agents.length) return '<h1>Agents</h1>' +
13639
+ '<div class="agents-empty">' +
13640
+ '<div class="icon-frame"><div class="core"></div></div>' +
13641
+ '<h2>No wrapped agents yet.</h2>' +
13642
+ '<p>Wrap an agent to give it a portable identity, a charter, and approval gates. Run <code>sanctuary wrap</code> in any project where your agent lives.</p>' +
13643
+ '<div class="terminal-block"><span class="cmd"><span class="prompt">$</span>sanctuary wrap</span></div>' +
13644
+ '</div>';
13645
+ const count = state.agents.length;
13646
+ const subCopy = count + ' wrapped. Click one to inspect its activity, policy, and pending approvals.';
13365
13647
  const rows = state.agents.map(function (a) {
13366
13648
  const map = STATUS_MAP[a.status] || STATUS_MAP.unknown;
13367
- const reason = a.status_reason_class ? (REASON_LABELS[a.status_reason_class] || "") : "";
13368
- return '<div class="row">' +
13369
- '<span class="glyph ' + map.glyph + '"></span>' +
13370
- '<div class="grow"><strong>' + escHtml(a.agent_id) + '</strong> <span class="muted mono">' + escHtml(a.harness) + '</span></div>' +
13371
- '<span class="pill" title="' + escHtml(reason) + '">' + escHtml(map.label) + '</span>' +
13372
- '<button class="btn" data-action="open-agent" data-agent-id="' + escHtml(a.agent_id) + '">Open</button>' +
13649
+ const dotCls = agentStateClass(a.status);
13650
+ const initials = agentInitials(a.agent_id);
13651
+ const role = escHtml(a.harness) + (a.model_provider && a.model_provider.model_id ? ' · ' + escHtml(a.model_provider.model_id) : '');
13652
+ const isSelected = state.selectedAgentId === a.agent_id;
13653
+ return '<div class="agent-row' + (isSelected ? ' selected' : '') + '" data-action="open-agent" data-agent-id="' + escHtml(a.agent_id) + '" role="button" tabindex="0" title="Open inspect panel for ' + escHtml(a.agent_id) + '">' +
13654
+ '<div class="agent-identity">' +
13655
+ '<div class="agent-glyph">' + escHtml(initials) + '</div>' +
13656
+ '<div class="agent-name">' +
13657
+ '<strong>' + escHtml(a.agent_id) + '</strong>' +
13658
+ '<small>' + role + '</small>' +
13659
+ '</div>' +
13660
+ '</div>' +
13661
+ '<span class="agent-state">' +
13662
+ '<span class="state-dot ' + dotCls + '"></span>' +
13663
+ escHtml(map.label) +
13664
+ '</span>' +
13665
+ renderAgentAttestationBadge(a.status) +
13666
+ '<span class="agent-last">' + escHtml(relTimeFromIso(a.last_activity_at)) + '</span>' +
13373
13667
  '</div>';
13374
13668
  }).join("\n");
13375
- return '<h1>Agents</h1><div class="card">' + rows + '</div>';
13669
+ return '<div class="agents-wrap">' +
13670
+ '<div class="page-head">' +
13671
+ '<div>' +
13672
+ '<p class="eyebrow">Agents</p>' +
13673
+ '<h1>Agents.</h1>' +
13674
+ '<p class="sub">' + escHtml(subCopy) + '</p>' +
13675
+ '</div>' +
13676
+ '</div>' +
13677
+ '<div class="agents-layout">' +
13678
+ '<div class="agents-list">' +
13679
+ '<div class="agents-list-head">' +
13680
+ '<span>Agent</span><span>State</span><span>Attestation</span><span>Last seen</span>' +
13681
+ '</div>' +
13682
+ rows +
13683
+ '</div>' +
13684
+ '</div>' +
13685
+ '</div>';
13376
13686
  }
13377
13687
 
13378
13688
  function renderAgentDetail() {
@@ -13383,7 +13693,10 @@ function renderAgentDetail() {
13383
13693
  const timeline = events.length
13384
13694
  ? events.map(function (e) {
13385
13695
  const t = renderTemplate(e.display_template_id, e.display_template_args);
13386
- return '<div class="row"><span class="muted">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + '</span></div>';
13696
+ const badgeHtml = e.attestation
13697
+ ? ' ' + renderActionAttestationBadge(e.attestation.state, e.attestation.fragment)
13698
+ : '';
13699
+ return '<div class="row"><span class="muted">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + badgeHtml + '</span></div>';
13387
13700
  }).join("\n")
13388
13701
  : '<p class="muted">No activity yet.</p>';
13389
13702
  // WP-V1.2 reshape click-to-inspect surface. Clicking "Open inspect
@@ -13427,53 +13740,99 @@ function renderAgentInspectPanel(agent) {
13427
13740
  : "";
13428
13741
 
13429
13742
  // State 2: panel loaded.
13743
+ // Sprint Piece 2 PR 4 polish: outer wrapper combines .card with
13744
+ // .inspect-pane (sticky right rail, internal scroll, sectioned body).
13745
+ // The .card class is preserved so the rendered surface keeps its
13746
+ // shared card chrome; .inspect-pane overrides .card padding so the
13747
+ // inspect-head and inspect-body control their own spacing per design.
13430
13748
  if (panel) {
13749
+ const dotCls = agentStateClass(agent.status);
13750
+ const stateMap = STATUS_MAP[agent.status] || STATUS_MAP.unknown;
13431
13751
  const activity = (panel.recent_activity || []).slice(0, 20);
13432
13752
  const activityHtml = activity.length
13433
- ? activity.map(function (e) {
13753
+ ? '<div class="timeline">' +
13754
+ activity.map(function (e) {
13434
13755
  const t = renderTemplate(e.display_template_id, e.display_template_args);
13435
- return '<div class="row"><span class="muted mono">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + '</span></div>';
13436
- }).join("\n")
13756
+ const badgeHtml = e.attestation
13757
+ ? renderActionAttestationBadge(e.attestation.state, e.attestation.fragment)
13758
+ : '';
13759
+ return '<div class="timeline-item ok">' +
13760
+ '<div class="ts">' + escHtml(shortTime(e.emitted_at)) + '</div>' +
13761
+ '<div class="what">' + escHtml(t) + '</div>' +
13762
+ (badgeHtml ? '<div class="att">' + badgeHtml + '</div>' : '') +
13763
+ '</div>';
13764
+ }).join("") +
13765
+ '</div>'
13437
13766
  : '<p class="muted">No recent activity for this agent.</p>';
13438
13767
 
13439
13768
  const approvals = panel.pending_approvals || [];
13440
13769
  const approvalsHtml = approvals.length
13441
13770
  ? approvals.map(function (item) {
13442
13771
  const promptText = renderTemplate(item.display_template_id, item.display_template_args);
13443
- return '<div class="row">' +
13444
- '<span class="pill tone-info">' + escHtml(item.tier || "tier1") + '</span>' +
13445
- '<div class="grow">' + escHtml(promptText) + '</div>' +
13446
- '<button class="btn btn-primary" data-action="inbox-approve" data-item-id="' + escHtml(item.item_id) + '">Approve</button>' +
13447
- '<button class="btn" data-action="inbox-deny" data-item-id="' + escHtml(item.item_id) + '">Deny</button>' +
13772
+ return '<div class="approval-row">' +
13773
+ '<div class="what">' +
13774
+ '<span class="pill tone-degraded">' + escHtml(item.tier || "tier1") + '</span>' +
13775
+ escHtml(promptText) +
13776
+ '</div>' +
13777
+ '<div class="actions">' +
13778
+ '<button class="btn" data-action="inbox-deny" data-item-id="' + escHtml(item.item_id) + '">Deny</button>' +
13779
+ '<button class="btn btn-primary" data-action="inbox-approve" data-item-id="' + escHtml(item.item_id) + '">Approve once</button>' +
13780
+ '</div>' +
13448
13781
  '</div>';
13449
- }).join("\n")
13782
+ }).join("")
13450
13783
  : '<p class="muted">No pending approvals routed through this agent.</p>';
13451
13784
 
13452
- const policyLine = panel.policy_summary
13453
- ? '<dt>Policy</dt><dd class="mono">' + escHtml(panel.policy_summary.display_label || panel.policy_summary.policy_id) + '</dd>' +
13785
+ const policySection = panel.policy_summary
13786
+ ? '<div class="policy-line"><span class="k">Policy</span><span class="v">' + escHtml(panel.policy_summary.display_label || panel.policy_summary.policy_id) + '</span></div>' +
13454
13787
  (panel.policy_summary.channel_template_id
13455
- ? '<dt>Template</dt><dd class="mono">' + escHtml(panel.policy_summary.channel_template_id) + '</dd>'
13788
+ ? '<div class="policy-line"><span class="k">Template</span><span class="v">' + escHtml(panel.policy_summary.channel_template_id) + '</span></div>'
13456
13789
  : '') +
13457
- '<dt>Bound</dt><dd class="mono">' + escHtml(shortTime(panel.policy_summary.bound_at)) + '</dd>'
13458
- : '<dt>Policy</dt><dd class="muted">No bound policy yet.</dd>';
13459
-
13460
- return '<div class="card">' +
13461
- '<div class="concierge-header">' +
13462
- '<div class="concierge-persona"><strong>Inspect ' + escHtml(agent.agent_id) + '</strong> ' +
13463
- '<span class="muted">opened ' + escHtml(shortTime(panel.opened_at)) + '</span></div>' +
13464
- '<button class="btn" data-action="agent-inspect-open" data-agent-id="' + escHtml(agent.agent_id) + '">Refresh</button>' +
13790
+ '<div class="policy-line"><span class="k">Bound</span><span class="v">' + escHtml(shortTime(panel.policy_summary.bound_at)) + '</span></div>'
13791
+ : '<div class="policy-line"><span class="k">Policy</span><span class="v">No bound policy yet.</span></div>';
13792
+
13793
+ const modelLine = agent.model_provider
13794
+ ? '<div class="policy-line"><span class="k">Model</span><span class="v">' + escHtml(agent.model_provider.vendor) + ' / ' + escHtml(agent.model_provider.model_id) + '</span></div>'
13795
+ : '';
13796
+
13797
+ return '<div class="card inspect-pane">' +
13798
+ '<div class="inspect-head">' +
13799
+ '<div class="row1">' +
13800
+ '<div class="agent-glyph">' + escHtml(agentInitials(agent.agent_id)) + '</div>' +
13801
+ '<h3>' + escHtml(agent.agent_id) + '</h3>' +
13802
+ '<span style="margin-left:auto;">' + renderAgentAttestationBadge(agent.status) + '</span>' +
13803
+ '</div>' +
13804
+ '<div class="meta">' +
13805
+ '<span class="pill ' + (dotCls === "live" ? "tone-verified" : "tone-degraded") + '"><span class="state-dot ' + dotCls + '" style="margin-right:4px;"></span>' + escHtml(stateMap.label) + '</span>' +
13806
+ '<span class="pill">opened ' + escHtml(shortTime(panel.opened_at)) + '</span>' +
13807
+ '<button class="btn btn-quiet" data-action="agent-inspect-open" data-agent-id="' + escHtml(agent.agent_id) + '" title="Refresh inspect panel">Refresh</button>' +
13808
+ '</div>' +
13809
+ '</div>' +
13810
+ '<div class="inspect-body">' +
13811
+ errorBanner +
13812
+ '<div class="inspect-section">' +
13813
+ '<h4>Pending approvals' + (approvals.length ? ' <span class="count">' + approvals.length + '</span>' : '') + '</h4>' +
13814
+ approvalsHtml +
13815
+ '</div>' +
13816
+ '<div class="inspect-section">' +
13817
+ '<h4>Recent activity</h4>' +
13818
+ activityHtml +
13819
+ '</div>' +
13820
+ '<div class="inspect-section">' +
13821
+ '<h4>Policy summary</h4>' +
13822
+ policySection +
13823
+ '</div>' +
13824
+ '<div class="inspect-section">' +
13825
+ '<h4>Identity</h4>' +
13826
+ '<div class="policy-line"><span class="k">Agent id</span><span class="v">' + escHtml(agent.agent_id) + '</span></div>' +
13827
+ '<div class="policy-line"><span class="k">Harness</span><span class="v">' + escHtml(agent.harness) + '</span></div>' +
13828
+ modelLine +
13829
+ '<div class="policy-line"><span class="k">Wrapped at</span><span class="v">' + escHtml(shortTime(agent.wrapped_at)) + '</span></div>' +
13830
+ '</div>' +
13831
+ '<p class="muted" style="margin-top:10px;font-size:12px;">' +
13832
+ '<a href="#activity?agent=' + escHtml(agent.agent_id) + '">View full activity</a> · ' +
13833
+ '<a href="#policy">Edit policy</a>' +
13834
+ '</p>' +
13465
13835
  '</div>' +
13466
- errorBanner +
13467
- '<h3>Pending approvals</h3>' +
13468
- approvalsHtml +
13469
- '<h3 style="margin-top:14px;">Recent activity</h3>' +
13470
- activityHtml +
13471
- '<h3 style="margin-top:14px;">Policy</h3>' +
13472
- '<dl class="kv">' + policyLine + '</dl>' +
13473
- '<p class="muted" style="margin-top:10px;font-size:12px;">' +
13474
- '<a href="#activity?agent=' + escHtml(agent.agent_id) + '">View full activity</a> · ' +
13475
- '<a href="#policy">Edit policy</a>' +
13476
- '</p>' +
13477
13836
  '</div>';
13478
13837
  }
13479
13838
 
@@ -13622,6 +13981,16 @@ function statusDotClass(status) {
13622
13981
  return "red";
13623
13982
  }
13624
13983
 
13984
+ // Card-grid polish (Sprint Piece 2 PR 3) maps the badge dot class onto
13985
+ // the shaped glyph token. Sage circle for ok, ochre triangle for warn,
13986
+ // rust diamond for fail. Keep aligned with .status-glyph rules in
13987
+ // html.ts and the .intel-card-status modifier classes.
13988
+ function statusGlyphClass(dotClass) {
13989
+ if (dotClass === "green") return "ok";
13990
+ if (dotClass === "yellow") return "warn";
13991
+ return "fail";
13992
+ }
13993
+
13625
13994
  function statusLabel(health) {
13626
13995
  if (health === "ok") return "Working";
13627
13996
  if (health === "degraded") return "Degraded";
@@ -13661,13 +14030,18 @@ function renderIntelligenceCenter() {
13661
14030
  }
13662
14031
  const status = state.intelligence.status;
13663
14032
  const config = state.intelligence.config || {};
13664
- const surfaceRows = SURFACES_ORDER.map(function (surfaceId) {
14033
+ const surfaceCards = SURFACES_ORDER.map(function (surfaceId) {
13665
14034
  const surfaceStatus = (status.surfaces || []).find(function (s) { return s.surface === surfaceId; });
13666
14035
  if (!surfaceStatus) {
13667
- return '<div class="intel-row"><div class="intel-row-name">' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) +
13668
- '<small>' + escHtml(surfaceId) + '</small></div>' +
13669
- '<div class="intel-row-body muted">No status reported.</div>' +
13670
- '<div></div></div>';
14036
+ return '<div class="intel-row intel-card" data-intel-surface="' + escHtml(surfaceId) + '">' +
14037
+ '<div class="intel-card-head">' +
14038
+ '<div class="intel-card-name">' +
14039
+ '<strong>' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) + '</strong>' +
14040
+ '<small>' + escHtml(surfaceId) + '</small>' +
14041
+ '</div>' +
14042
+ '</div>' +
14043
+ '<div class="muted">No status reported.</div>' +
14044
+ '</div>';
13671
14045
  }
13672
14046
  const substrate = surfaceStatus.chosen;
13673
14047
  const localPick = (config.local_model_picks || {})[surfaceId];
@@ -13685,41 +14059,62 @@ function renderIntelligenceCenter() {
13685
14059
  if (provider) currentBadge = currentBadge + " (" + (FRONTIER_PROVIDER_LABELS[provider] || provider) + ")";
13686
14060
  }
13687
14061
  const dotClass = statusDotClass((surfaceStatus.badge || {}).status || "red");
14062
+ const glyphClass = statusGlyphClass(dotClass);
13688
14063
  const failures = surfaceStatus.recentFailures || [];
13689
14064
  const expanded = !!state.intelligence.expandedFailures[surfaceId];
13690
- let failuresBlock = "";
14065
+
14066
+ // Card foot. The failures toggle is the load-bearing affordance for
14067
+ // the rc.6 ZZ test (button[data-action="intel-failures-toggle"] with
14068
+ // text "recent failures (N)"). Pluralization is "failures" regardless
14069
+ // of N for backward compatibility with the seeded test contract.
14070
+ // Surface zero-failure state as a quiet mono note so the card still
14071
+ // has visual rhythm in its foot row.
14072
+ let footHtml;
13691
14073
  if (failures.length > 0) {
13692
14074
  const toggleLabel = (expanded ? "Hide" : "View") + " recent failures (" + failures.length + ")";
13693
- const list = expanded
13694
- ? '<ul class="intel-row-failures-list">' +
13695
- failures.slice().reverse().map(function (f) {
13696
- return '<li><span class="muted mono">' + escHtml(shortTime(f.ts)) + '</span> ' +
13697
- '<span class="pill warn">' + escHtml(f.failureClass) + '</span> ' +
13698
- '<span class="muted">' + escHtml(f.snippet) + '</span></li>';
13699
- }).join("") +
13700
- '</ul>'
13701
- : '';
13702
- failuresBlock =
13703
- '<div class="intel-row-failures">' +
13704
- '<button class="btn btn-link" data-action="intel-failures-toggle" data-intel-surface="' + escHtml(surfaceId) + '">' +
13705
- escHtml(toggleLabel) +
13706
- '</button>' +
13707
- list +
14075
+ footHtml =
14076
+ '<button class="intel-failures-toggle' + (expanded ? ' open' : '') + '" data-action="intel-failures-toggle" data-intel-surface="' + escHtml(surfaceId) + '">' +
14077
+ '<span class="caret"></span>' +
14078
+ escHtml(toggleLabel) +
14079
+ '</button>';
14080
+ } else {
14081
+ footHtml = '<span class="muted mono" style="font-size: 11px;">no recent failures</span>';
14082
+ }
14083
+
14084
+ let failuresBlock = "";
14085
+ if (failures.length > 0 && expanded) {
14086
+ const rows = failures.slice().reverse().map(function (f) {
14087
+ return '<div class="intel-failure-row">' +
14088
+ '<span class="ts">' + escHtml(shortTime(f.ts)) + '</span>' +
14089
+ '<div>' +
14090
+ '<div class="err-class">' + escHtml(f.failureClass) + '</div>' +
14091
+ '<div>' + escHtml(f.snippet) + '</div>' +
14092
+ '</div>' +
13708
14093
  '</div>';
14094
+ }).join("");
14095
+ failuresBlock = '<div class="intel-failures">' + rows + '</div>';
13709
14096
  }
13710
- return '<div class="intel-row" data-intel-surface="' + escHtml(surfaceId) + '">' +
13711
- '<div class="intel-row-name">' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) +
13712
- '<small>' + escHtml(surfaceId) + '</small></div>' +
13713
- '<div class="intel-row-body">' +
13714
- '<div class="intel-row-current">' +
13715
- '<span class="intel-status-dot ' + dotClass + '" title="' + escHtml(statusLabel(surfaceStatus.health)) + '"></span>' +
13716
- '<span class="pill">' + escHtml(currentBadge) + '</span>' +
13717
- '<span class="muted mono">' + escHtml(statusLabel(surfaceStatus.health)) + '</span>' +
14097
+
14098
+ return '<div class="intel-row intel-card" data-intel-surface="' + escHtml(surfaceId) + '">' +
14099
+ '<div class="intel-card-head">' +
14100
+ '<div class="intel-card-name">' +
14101
+ '<strong>' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) + '</strong>' +
14102
+ '<small>' + escHtml(surfaceId) + '</small>' +
13718
14103
  '</div>' +
13719
- '<div class="intel-row-tradeoff">' + escHtml(substrateTradeoff(substrate)) + '</div>' +
13720
- failuresBlock +
14104
+ '<span class="intel-card-status ' + glyphClass + '" title="' + escHtml(statusLabel(surfaceStatus.health)) + '">' +
14105
+ '<span class="status-glyph ' + glyphClass + '"></span>' +
14106
+ escHtml(statusLabel(surfaceStatus.health)) +
14107
+ '</span>' +
13721
14108
  '</div>' +
13722
- '<div><button class="btn" data-action="intel-picker-open" data-intel-surface="' + escHtml(surfaceId) + '">Change</button></div>' +
14109
+ '<div class="intel-substrate">' +
14110
+ '<div class="sub-line primary">' +
14111
+ '<span>' + escHtml(currentBadge) + '</span>' +
14112
+ '<button class="btn-quiet" data-action="intel-picker-open" data-intel-surface="' + escHtml(surfaceId) + '">Change</button>' +
14113
+ '</div>' +
14114
+ '</div>' +
14115
+ '<div class="intel-row-tradeoff">' + escHtml(substrateTradeoff(substrate)) + '</div>' +
14116
+ '<div class="intel-card-foot">' + footHtml + '</div>' +
14117
+ failuresBlock +
13723
14118
  '</div>';
13724
14119
  }).join("\n");
13725
14120
 
@@ -13731,11 +14126,15 @@ function renderIntelligenceCenter() {
13731
14126
 
13732
14127
  const modal = state.intelligence.picker.open ? renderIntelligencePicker() : "";
13733
14128
 
13734
- return '<section class="intel-center">' +
13735
- '<p class="eyebrow">INTELLIGENCE</p>' +
13736
- '<h1>Intelligence Substrate</h1>' +
13737
- '<p class="intel-subtitle">Choose how Sanctuary thinks. Tradeoffs visible per surface. Multi-option framing is preserved: no single substrate is the right answer.</p>' +
13738
- '<section class="intel-panel"><h2>Surfaces</h2>' + surfaceRows + '</section>' +
14129
+ return '<section class="intel-wrap">' +
14130
+ '<div class="page-head">' +
14131
+ '<div>' +
14132
+ '<p class="eyebrow">Intelligence</p>' +
14133
+ '<h1>Substrate routing.</h1>' +
14134
+ '<p class="sub">Six surfaces, six choices. Each surface picks where its thinking happens. Local for privacy. Hosted for capability. Hybrid for both.</p>' +
14135
+ '</div>' +
14136
+ '</div>' +
14137
+ '<div class="intel-grid">' + surfaceCards + '</div>' +
13739
14138
  '<section class="intel-panel"><h2>Host capability</h2>' +
13740
14139
  '<dl class="intel-hardware">' +
13741
14140
  '<dt>Total RAM</dt><dd>' + escHtml(hardware.totalRamGb || "?") + ' GB</dd>' +
@@ -14528,6 +14927,13 @@ document.addEventListener("click", function (ev) {
14528
14927
  const intelLocalModel = tgt.getAttribute("data-intel-local-model");
14529
14928
  const intelFrontierProvider = tgt.getAttribute("data-intel-frontier-provider");
14530
14929
  if (action === "lockdown") return void onLockdownClick();
14930
+ if (action === "theme-toggle") {
14931
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
14932
+ const next = isDark ? "light" : "dark";
14933
+ sessionStorage.setItem(THEME_KEY, next);
14934
+ applyTheme(next);
14935
+ return;
14936
+ }
14531
14937
  if (action === "intel-reload") { return void fetchIntelligenceState().then(rerender); }
14532
14938
  if (action === "intel-picker-open" && intelSurface) return void onIntelPickerOpen(intelSurface);
14533
14939
  if (action === "intel-picker-close") return onIntelPickerClose();
@@ -14681,12 +15087,30 @@ document.addEventListener("input", function (ev) {
14681
15087
  // then the dashboard view renders a static welcome card with no form
14682
15088
  // inputs that could be confused for a working command surface.
14683
15089
 
14684
- // Theme: system preference only at v1.1.
15090
+ // Theme: explicit operator preference (sessionStorage) overrides system
15091
+ // pref. The toggle button in the topbar dispatches data-action
15092
+ // "theme-toggle" which writes the chosen theme and updates the
15093
+ // [data-theme] attribute. When no explicit choice exists, fall back to
15094
+ // system preference and track changes so dark-mode-at-sunset behavior
15095
+ // keeps working on macOS / Windows.
15096
+ const THEME_KEY = "sanctuary-v11-theme";
15097
+ function applyTheme(theme) {
15098
+ if (theme === "dark") document.documentElement.setAttribute("data-theme", "dark");
15099
+ else document.documentElement.removeAttribute("data-theme");
15100
+ }
15101
+ const explicitTheme = sessionStorage.getItem(THEME_KEY);
14685
15102
  const mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
14686
- if (mq && mq.matches) document.documentElement.setAttribute("data-theme", "dark");
15103
+ if (explicitTheme === "dark" || explicitTheme === "light") {
15104
+ applyTheme(explicitTheme);
15105
+ } else if (mq && mq.matches) {
15106
+ applyTheme("dark");
15107
+ }
14687
15108
  if (mq) mq.addEventListener("change", function (e) {
14688
- if (e.matches) document.documentElement.setAttribute("data-theme", "dark");
14689
- else document.documentElement.removeAttribute("data-theme");
15109
+ // Only honor system pref changes when the operator has not made an
15110
+ // explicit choice. Once they toggle, the choice sticks for the
15111
+ // session.
15112
+ if (sessionStorage.getItem(THEME_KEY)) return;
15113
+ applyTheme(e.matches ? "dark" : "light");
14690
15114
  });
14691
15115
 
14692
15116
  // Boot.
@@ -14710,9 +15134,11 @@ function renderDashboardV11Html(options = {}) {
14710
15134
  const fortressId = options.fortressId ?? "fortress";
14711
15135
  const sanctuaryVersion = options.sanctuaryVersion ?? SANCTUARY_VERSION;
14712
15136
  const embedClient = options.embedClient !== false;
14713
- const nav = NAV_ITEMS.map(
14714
- (n) => `<a href="#${n.id}" data-route="${n.id}"><span>${escHtml3(n.label)}</span></a>`
14715
- ).join("\n ");
15137
+ const nav = NAV_ITEMS.map((n) => {
15138
+ const iconPath = NAV_ICON_PATHS[n.id] ?? "";
15139
+ const icon = iconPath ? SVG_OPEN + iconPath + "</svg>" : "";
15140
+ return `<a href="#${n.id}" data-route="${n.id}">${icon}<span>${escHtml3(n.label)}</span></a>`;
15141
+ }).join("\n ");
14716
15142
  const config = JSON.stringify({
14717
15143
  authToken,
14718
15144
  hubApiBase,
@@ -14744,8 +15170,12 @@ function renderDashboardV11Html(options = {}) {
14744
15170
  <span class="pill" data-pill="version">v${escHtml3(sanctuaryVersion)}</span>
14745
15171
  <span class="pill" data-pill="deployment">deployment: local</span>
14746
15172
  <span class="pill" data-pill="mode">mode: solo</span>
14747
- <span class="pill" data-pill="attestation">attestation: pending</span>
15173
+ <span class="att-global pending" data-pill="attestation" title="Fortress attestation"><span class="seal"><span class="seal-ring dashed"></span><span class="seal-core"></span></span><span class="label">pending</span></span>
14748
15174
  </div>
15175
+ <button class="btn btn-icon" id="btn-theme-toggle" data-action="theme-toggle" aria-label="Toggle theme" title="Toggle theme">
15176
+ <span class="icon-moon">${THEME_ICON_MOON}</span>
15177
+ <span class="icon-sun">${THEME_ICON_SUN}</span>
15178
+ </button>
14749
15179
  <button class="btn btn-danger" id="btn-lockdown" data-action="lockdown">Lockdown</button>
14750
15180
  </header>
14751
15181
  <main class="main" id="main"><p class="muted">Loading dashboard.</p></main>
@@ -14757,7 +15187,7 @@ function renderDashboardV11Html(options = {}) {
14757
15187
  </body>
14758
15188
  </html>`;
14759
15189
  }
14760
- var STYLES, NAV_ITEMS;
15190
+ var STYLES, NAV_ITEMS, NAV_ICON_PATHS, SVG_OPEN, THEME_ICON_MOON, THEME_ICON_SUN;
14761
15191
  var init_html2 = __esm({
14762
15192
  "src/dashboard/v1_1/html.ts"() {
14763
15193
  init_client();
@@ -14788,6 +15218,26 @@ var init_html2 = __esm({
14788
15218
  --rad: 6px;
14789
15219
  --rad-lg: 10px;
14790
15220
  --shadow: 0 1px 2px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.02);
15221
+ /* Type scale. Names are size-relative, not semantic, so refactors do
15222
+ not need to invent new names. Existing rules used these literal px
15223
+ values; tokens make future polish a one-line change. */
15224
+ --text-xs: 11px;
15225
+ --text-sm: 12px;
15226
+ --text-base: 13px;
15227
+ --text-md: 14px;
15228
+ --text-lg: 16px;
15229
+ --text-xl: 22px;
15230
+ --text-display: 36px;
15231
+ /* Spacing scale (4px multiples). Layout-specific magic numbers
15232
+ (220px sidebar, 360px fortress rail, concierge-card heights) stay
15233
+ as literals because the token system is for component padding /
15234
+ margin / gap, not grid track sizing. */
15235
+ --space-1: 4px;
15236
+ --space-2: 8px;
15237
+ --space-3: 12px;
15238
+ --space-4: 16px;
15239
+ --space-5: 24px;
15240
+ --space-6: 32px;
14791
15241
  }
14792
15242
  [data-theme="dark"] {
14793
15243
  --paper: #121210;
@@ -14814,7 +15264,7 @@ var init_html2 = __esm({
14814
15264
  html, body { margin: 0; padding: 0; }
14815
15265
  body {
14816
15266
  font-family: var(--sans);
14817
- font-size: 14px;
15267
+ font-size: var(--text-md);
14818
15268
  background: var(--paper);
14819
15269
  color: var(--ink);
14820
15270
  line-height: 1.45;
@@ -14835,20 +15285,27 @@ body {
14835
15285
  "sidebar main";
14836
15286
  }
14837
15287
  .sidebar { grid-area: sidebar; background: var(--paper-2); border-right: 1px solid var(--rule); padding: 12px 8px; }
14838
- .sidebar h1 { font-family: var(--serif); font-size: 16px; margin: 4px 8px 16px; }
15288
+ .sidebar h1 { font-family: var(--serif); font-size: var(--text-lg); margin: 4px 8px 16px; }
14839
15289
  .sidebar nav { display: flex; flex-direction: column; gap: 2px; }
14840
15290
  .sidebar nav a {
14841
- display: block; padding: 6px 10px; border-radius: var(--rad);
14842
- color: var(--ink-2); text-decoration: none; font-size: 13px;
15291
+ display: flex; align-items: center; gap: var(--space-2);
15292
+ padding: 6px 10px; border-radius: var(--rad);
15293
+ color: var(--ink-2); text-decoration: none; font-size: var(--text-base);
15294
+ }
15295
+ .sidebar nav a svg {
15296
+ flex-shrink: 0; width: 16px; height: 16px;
15297
+ color: var(--ink-3);
14843
15298
  }
14844
15299
  .sidebar nav a:hover { background: var(--paper-3); }
15300
+ .sidebar nav a:hover svg { color: var(--ink-2); }
14845
15301
  .sidebar nav a.active { background: var(--surface); color: var(--ink); border: 1px solid var(--rule); }
14846
- .topbar { grid-area: topbar; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--rule); background: var(--surface); }
14847
- .topbar .brand { font-family: var(--serif); font-size: 14px; }
15302
+ .sidebar nav a.active svg { color: var(--ink); }
15303
+ .topbar { grid-area: topbar; display: flex; align-items: center; gap: var(--space-3); padding: 0 16px; border-bottom: 1px solid var(--rule); background: var(--surface); }
15304
+ .topbar .brand { font-family: var(--serif); font-size: var(--text-md); }
14848
15305
  .topbar .pills { display: flex; gap: 6px; flex: 1; }
14849
15306
  .pill {
14850
15307
  display: inline-flex; align-items: center; gap: 4px;
14851
- padding: 2px 8px; border-radius: 12px; font-size: 11px;
15308
+ padding: 2px 8px; border-radius: 12px; font-size: var(--text-xs);
14852
15309
  font-family: var(--mono); border: 1px solid var(--rule);
14853
15310
  background: var(--surface-2); color: var(--ink-2);
14854
15311
  }
@@ -14860,7 +15317,7 @@ body {
14860
15317
  display: inline-flex; align-items: center; gap: 4px;
14861
15318
  padding: 4px 10px; border-radius: var(--rad);
14862
15319
  background: var(--surface); border: 1px solid var(--rule);
14863
- font-family: var(--sans); font-size: 12px; color: var(--ink);
15320
+ font-family: var(--sans); font-size: var(--text-sm); color: var(--ink);
14864
15321
  cursor: pointer;
14865
15322
  }
14866
15323
  .btn:hover:not(:disabled) { background: var(--surface-2); }
@@ -14870,21 +15327,30 @@ body {
14870
15327
  .btn.btn-danger { background: var(--rust-bg); color: var(--rust); border-color: var(--rust); }
14871
15328
  .btn.tier1-pending { background: var(--ochre-bg); color: var(--ochre); border-color: var(--ochre); }
14872
15329
  .btn.tier1-engaged { background: var(--rust-bg); color: var(--rust); border-color: var(--rust); }
15330
+ .btn.btn-icon {
15331
+ padding: 4px 6px;
15332
+ display: inline-flex; align-items: center; justify-content: center;
15333
+ color: var(--ink-2);
15334
+ }
15335
+ .btn.btn-icon svg { width: 16px; height: 16px; }
15336
+ .btn.btn-icon .icon-sun { display: none; }
15337
+ [data-theme="dark"] .btn.btn-icon .icon-moon { display: none; }
15338
+ [data-theme="dark"] .btn.btn-icon .icon-sun { display: inline; }
14873
15339
  .main { grid-area: main; overflow-y: auto; padding: 16px 24px; }
14874
- .fortress { grid-area: fortress; overflow-y: auto; border-left: 1px solid var(--rule); background: var(--paper-2); padding: 12px; display: flex; flex-direction: column; gap: 10px; }
15340
+ .fortress { grid-area: fortress; overflow-y: auto; border-left: 1px solid var(--rule); background: var(--paper-2); padding: var(--space-3); display: flex; flex-direction: column; gap: 10px; }
14875
15341
  .app.route-full .fortress { display: none; }
14876
15342
  .card {
14877
15343
  background: var(--surface); border: 1px solid var(--rule);
14878
- border-radius: var(--rad); padding: 12px;
15344
+ border-radius: var(--rad); padding: var(--space-3);
14879
15345
  }
14880
- .card h3 { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: var(--ink); }
15346
+ .card h3 { margin: 0 0 8px; font-size: var(--text-base); font-weight: 600; color: var(--ink); }
14881
15347
  .muted { color: var(--ink-3); }
14882
- .mono { font-family: var(--mono); font-size: 12px; }
14883
- .row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px dashed var(--rule); }
15348
+ .mono { font-family: var(--mono); font-size: var(--text-sm); }
15349
+ .row { display: flex; align-items: center; gap: var(--space-2); padding: 6px 0; border-bottom: 1px dashed var(--rule); }
14884
15350
  .row:last-child { border-bottom: 0; }
14885
15351
  .row .grow { flex: 1; min-width: 0; }
14886
15352
  .agent-row { flex-direction: column; align-items: stretch; gap: 6px; }
14887
- .agent-row-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
15353
+ .agent-row-head { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
14888
15354
  .agent-row-head .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
14889
15355
  .agent-row-actions { display: flex; flex-wrap: wrap; gap: 4px; }
14890
15356
  /* Click-to-inspect affordance: the head sub-row of a fortress-column
@@ -14894,7 +15360,7 @@ body {
14894
15360
  .agent-row-head[data-action="agent-row-inspect-open"] { cursor: pointer; border-radius: var(--rad); padding: 4px 6px; margin: -4px -6px; }
14895
15361
  .agent-row-head[data-action="agent-row-inspect-open"]:hover { background: var(--paper-3); }
14896
15362
  .agent-row-head[data-action="agent-row-inspect-open"]:focus-visible { outline: 2px solid var(--ink-3); outline-offset: 1px; }
14897
- .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 12px; }
15363
+ .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: var(--text-sm); }
14898
15364
  .kv dt { color: var(--ink-3); }
14899
15365
  .kv dd { margin: 0; color: var(--ink); }
14900
15366
  .glyph { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--ink-4); }
@@ -14905,74 +15371,74 @@ body {
14905
15371
  .toast {
14906
15372
  position: fixed; bottom: 16px; right: 16px;
14907
15373
  background: var(--ink); color: var(--paper); padding: 8px 12px;
14908
- border-radius: var(--rad); font-size: 12px; z-index: 1000;
15374
+ border-radius: var(--rad); font-size: var(--text-sm); z-index: 1000;
14909
15375
  max-width: 360px;
14910
15376
  }
14911
15377
  .toast.error { background: var(--rust); color: var(--paper); }
14912
- .layer-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: 8px; }
14913
- .layer-card h4 { margin: 0 0 4px; font-size: 12px; font-weight: 600; }
14914
- .layer-card p { margin: 0; font-size: 11px; color: var(--ink-3); }
14915
- .chat-thread { display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; }
15378
+ .layer-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: var(--space-2); }
15379
+ .layer-card h4 { margin: 0 0 4px; font-size: var(--text-sm); font-weight: 600; }
15380
+ .layer-card p { margin: 0; font-size: var(--text-xs); color: var(--ink-3); }
15381
+ .chat-thread { display: flex; flex-direction: column; gap: var(--space-2); padding-bottom: 12px; }
14916
15382
  .chat-msg { padding: 8px 10px; border-radius: var(--rad); border: 1px solid var(--rule); background: var(--surface); max-width: 78%; }
14917
- .chat-msg.system { background: var(--paper-3); color: var(--ink-3); font-size: 12px; max-width: 100%; }
15383
+ .chat-msg.system { background: var(--paper-3); color: var(--ink-3); font-size: var(--text-sm); max-width: 100%; }
14918
15384
  .chat-msg.agent { align-self: flex-start; }
14919
15385
  .chat-msg.operator { align-self: flex-end; background: var(--ink); color: var(--paper); }
14920
15386
  .chat-msg .meta { font-size: 10px; color: var(--ink-4); margin-top: 4px; }
14921
- .composer { display: flex; gap: 8px; padding: 8px; border-top: 1px solid var(--rule); }
15387
+ .composer { display: flex; gap: var(--space-2); padding: var(--space-2); border-top: 1px solid var(--rule); }
14922
15388
  .composer input { flex: 1; padding: 6px 8px; border: 1px solid var(--rule); border-radius: var(--rad); font-family: var(--sans); }
14923
15389
  .wizard-step { padding: 10px; border: 1px solid var(--rule); border-radius: var(--rad); margin-bottom: 8px; background: var(--surface); }
14924
15390
  .wizard-step.active { border-color: var(--ink); }
14925
15391
  .wizard-step.done { background: var(--sage-bg); border-color: var(--sage); }
14926
- .code-block { font-family: var(--mono); background: var(--paper-3); padding: 8px; border-radius: var(--rad); font-size: 12px; overflow-x: auto; }
15392
+ .code-block { font-family: var(--mono); background: var(--paper-3); padding: var(--space-2); border-radius: var(--rad); font-size: var(--text-sm); overflow-x: auto; }
14927
15393
  .policy-center { max-width: 980px; margin: 0 auto; }
14928
- .policy-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; }
14929
- .policy-center h1 { font-family: var(--serif); font-size: 36px; line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
15394
+ .policy-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; }
15395
+ .policy-center h1 { font-family: var(--serif); font-size: var(--text-display); line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14930
15396
  .policy-subtitle { max-width: 860px; color: var(--ink-2); font-size: 15px; margin: 0 0 24px; }
14931
15397
  .policy-panel { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--rad-lg); padding: 20px; margin: 18px 0; }
14932
- .policy-panel h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 14px; }
14933
- .template-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
15398
+ .policy-panel h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 14px; }
15399
+ .template-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
14934
15400
  .template-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: 14px; min-height: 132px; }
14935
- .template-card-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 12px; }
14936
- .severity { border-radius: 999px; padding: 2px 9px; font-family: var(--mono); font-size: 11px; font-weight: 700; }
15401
+ .template-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); margin-bottom: 12px; }
15402
+ .severity { border-radius: 999px; padding: 2px 9px; font-family: var(--mono); font-size: var(--text-xs); font-weight: 700; }
14937
15403
  .severity.low { color: var(--sage); background: var(--sage-bg); }
14938
15404
  .severity.medium { color: var(--ochre); background: var(--ochre-bg); }
14939
15405
  .template-id { background: var(--paper-3); border-radius: var(--rad); padding: 2px 7px; color: var(--ink-3); }
14940
- .template-card h3 { font-size: 16px; margin: 0 0 6px; }
14941
- .template-card p { color: var(--ink-3); margin: 0; font-size: 14px; }
15406
+ .template-card h3 { font-size: var(--text-lg); margin: 0 0 6px; }
15407
+ .template-card p { color: var(--ink-3); margin: 0; font-size: var(--text-md); }
14942
15408
  .rules-scroll { overflow-x: auto; }
14943
15409
  .rules-table { width: 100%; border-collapse: collapse; min-width: 760px; }
14944
- .rules-table th { text-align: left; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; padding: 8px 10px; border-bottom: 1px solid var(--rule); }
15410
+ .rules-table th { text-align: left; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; padding: 8px 10px; border-bottom: 1px solid var(--rule); }
14945
15411
  .rules-table td { padding: 12px 10px; border-bottom: 1px solid var(--rule); vertical-align: top; }
14946
15412
  .link-btn, .template-cell { border: 0; background: transparent; color: var(--ink); padding: 0; cursor: pointer; font: inherit; text-align: left; }
14947
15413
  .template-cell { font-family: var(--mono); max-width: 180px; overflow-wrap: anywhere; }
14948
15414
  .template-picker { position: absolute; z-index: 20; margin-top: 8px; width: min(420px, calc(100vw - 80px)); background: var(--surface); border: 1px solid var(--rule-2); border-radius: var(--rad); box-shadow: var(--shadow); padding: 10px; }
14949
15415
  .template-picker-options { display: grid; gap: 6px; max-height: 320px; overflow-y: auto; }
14950
- .template-option { display: grid; grid-template-columns: 18px 1fr; gap: 8px; padding: 8px; border: 1px solid var(--rule); border-radius: var(--rad); background: var(--surface-2); }
15416
+ .template-option { display: grid; grid-template-columns: 18px 1fr; gap: var(--space-2); padding: var(--space-2); border: 1px solid var(--rule); border-radius: var(--rad); background: var(--surface-2); }
14951
15417
  .template-option small { display: block; color: var(--ink-3); margin-top: 2px; }
14952
- .template-picker-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
15418
+ .template-picker-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: 10px; }
14953
15419
  .allow-count { color: var(--sage); font-weight: 700; }
14954
15420
  .block-count { color: var(--rust); }
14955
15421
  .toggle-on { display: inline-block; width: 28px; height: 16px; border-radius: 999px; background: var(--sage); position: relative; }
14956
15422
  .toggle-on::after { content: ""; position: absolute; right: 2px; top: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--surface); }
14957
15423
  .error-text { color: var(--rust); margin: 8px 0 0; }
14958
15424
  .intel-center { max-width: 980px; margin: 0 auto; }
14959
- .intel-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; }
14960
- .intel-center h1 { font-family: var(--serif); font-size: 36px; line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
15425
+ .intel-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; }
15426
+ .intel-center h1 { font-family: var(--serif); font-size: var(--text-display); line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14961
15427
  .intel-subtitle { max-width: 860px; color: var(--ink-2); font-size: 15px; margin: 0 0 24px; }
14962
15428
  .intel-panel { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--rad-lg); padding: 20px; margin: 18px 0; }
14963
- .intel-panel h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 14px; }
14964
- .intel-row { display: grid; grid-template-columns: 200px 1fr auto; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--rule); align-items: start; }
15429
+ .intel-panel h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 14px; }
15430
+ .intel-row { display: grid; grid-template-columns: 200px 1fr auto; gap: var(--space-4); padding: 14px 0; border-bottom: 1px solid var(--rule); align-items: start; }
14965
15431
  .intel-row:last-child { border-bottom: 0; }
14966
15432
  .intel-row-name { font-weight: 600; }
14967
- .intel-row-name small { display: block; color: var(--ink-3); font-weight: 400; font-size: 12px; margin-top: 2px; font-family: var(--mono); }
15433
+ .intel-row-name small { display: block; color: var(--ink-3); font-weight: 400; font-size: var(--text-sm); margin-top: 2px; font-family: var(--mono); }
14968
15434
  .intel-row-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
14969
- .intel-row-current { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
14970
- .intel-row-tradeoff { color: var(--ink-2); font-size: 13px; line-height: 1.5; }
15435
+ .intel-row-current { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
15436
+ .intel-row-tradeoff { color: var(--ink-2); font-size: var(--text-base); line-height: 1.5; }
14971
15437
  .intel-status-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
14972
15438
  .intel-status-dot.green { background: var(--sage); }
14973
15439
  .intel-status-dot.yellow { background: var(--ochre); }
14974
15440
  .intel-status-dot.red { background: var(--rust); }
14975
- .intel-hardware { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: 13px; }
15441
+ .intel-hardware { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: var(--text-base); }
14976
15442
  .intel-hardware dt { color: var(--ink-3); font-family: var(--mono); }
14977
15443
  .intel-hardware dd { margin: 0; }
14978
15444
  .intel-modal-backdrop {
@@ -14985,33 +15451,162 @@ body {
14985
15451
  box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 24px;
14986
15452
  width: 100%; max-width: 640px;
14987
15453
  }
14988
- .intel-modal h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 8px; }
14989
- .intel-modal-subtitle { color: var(--ink-3); margin: 0 0 18px; font-size: 13px; }
15454
+ .intel-modal h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 8px; }
15455
+ .intel-modal-subtitle { color: var(--ink-3); margin: 0 0 18px; font-size: var(--text-base); }
14990
15456
  .intel-option {
14991
- border: 1px solid var(--rule); border-radius: var(--rad); padding: 12px;
15457
+ border: 1px solid var(--rule); border-radius: var(--rad); padding: var(--space-3);
14992
15458
  margin-bottom: 10px; background: var(--surface-2); cursor: pointer;
14993
15459
  display: grid; grid-template-columns: 18px 1fr; gap: 10px; align-items: start;
14994
15460
  }
14995
15461
  .intel-option.selected { border-color: var(--ink); background: var(--surface); }
14996
- .intel-option-body strong { display: block; font-size: 14px; margin-bottom: 4px; }
14997
- .intel-option-body small { display: block; color: var(--ink-3); font-size: 12px; line-height: 1.5; }
15462
+ .intel-option-body strong { display: block; font-size: var(--text-md); margin-bottom: 4px; }
15463
+ .intel-option-body small { display: block; color: var(--ink-3); font-size: var(--text-sm); line-height: 1.5; }
14998
15464
  .intel-suboptions { margin-top: 10px; padding: 10px; background: var(--paper-3); border-radius: var(--rad); }
14999
- .intel-suboptions label { display: block; margin: 6px 0; font-size: 13px; }
15465
+ .intel-suboptions label { display: block; margin: 6px 0; font-size: var(--text-base); }
15000
15466
  .intel-suboptions input[type="text"], .intel-suboptions input[type="password"] {
15001
15467
  width: 100%; padding: 6px 8px; border: 1px solid var(--rule); border-radius: var(--rad);
15002
- font-family: var(--mono); font-size: 12px; box-sizing: border-box;
15468
+ font-family: var(--mono); font-size: var(--text-sm); box-sizing: border-box;
15469
+ }
15470
+ .intel-modal-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: 18px; }
15471
+ /*
15472
+ * Intelligence panel polish (Sprint Piece 2 PR 3). Card grid layout
15473
+ * replaces the legacy 3-col row visual. The legacy .intel-row and
15474
+ * .intel-status-dot rules above are retained for the e2e selector
15475
+ * contract (.intel-row[data-intel-surface="..."]) and as the responsive
15476
+ * fallback. Cards render as a flex column with a substrate inset,
15477
+ * status badge with shaped glyph, and a recent-failures toggle in the
15478
+ * card foot.
15479
+ */
15480
+ .intel-wrap { max-width: 1000px; margin: 0 auto; }
15481
+ .intel-grid {
15482
+ display: grid;
15483
+ grid-template-columns: repeat(2, minmax(0, 1fr));
15484
+ gap: 12px;
15485
+ }
15486
+ .intel-card {
15487
+ background: var(--surface);
15488
+ border: 1px solid var(--rule);
15489
+ border-radius: var(--rad-lg);
15490
+ padding: 16px;
15491
+ display: flex; flex-direction: column;
15492
+ gap: 12px;
15493
+ }
15494
+ .intel-card-head {
15495
+ display: flex; align-items: flex-start; justify-content: space-between;
15496
+ gap: 10px;
15497
+ }
15498
+ .intel-card-name {
15499
+ display: flex; flex-direction: column; gap: 2px;
15500
+ min-width: 0;
15003
15501
  }
15004
- .intel-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
15502
+ .intel-card-name strong {
15503
+ font-family: var(--serif); font-weight: 500;
15504
+ font-size: 15px; letter-spacing: 0.005em;
15505
+ }
15506
+ .intel-card-name small {
15507
+ color: var(--ink-3); font-size: 11px; font-family: var(--mono);
15508
+ }
15509
+ .intel-card-status {
15510
+ display: inline-flex; align-items: center; gap: 6px;
15511
+ font-family: var(--mono); font-size: 11px;
15512
+ padding: 3px 9px; border-radius: 999px;
15513
+ border: 1px solid var(--rule); background: var(--surface-2);
15514
+ flex-shrink: 0;
15515
+ }
15516
+ .intel-card-status.ok { color: var(--sage); border-color: var(--sage); background: var(--sage-bg); }
15517
+ .intel-card-status.warn { color: var(--ochre); border-color: var(--ochre); background: var(--ochre-bg); }
15518
+ .intel-card-status.fail { color: var(--rust); border-color: var(--rust); background: var(--rust-bg); }
15519
+ .status-glyph {
15520
+ width: 10px; height: 10px;
15521
+ position: relative; flex-shrink: 0;
15522
+ }
15523
+ .status-glyph.ok::before {
15524
+ content: ""; position: absolute; inset: 0;
15525
+ border-radius: 50%; background: currentColor;
15526
+ }
15527
+ .status-glyph.warn::before {
15528
+ content: ""; position: absolute; inset: 0;
15529
+ background: currentColor;
15530
+ clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
15531
+ }
15532
+ .status-glyph.fail::before {
15533
+ content: ""; position: absolute; inset: 1px;
15534
+ background: currentColor;
15535
+ clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
15536
+ }
15537
+ .intel-substrate {
15538
+ display: flex; flex-direction: column; gap: 4px;
15539
+ padding: 10px 12px;
15540
+ background: var(--paper-2);
15541
+ border: 1px solid var(--rule);
15542
+ border-radius: var(--rad);
15543
+ }
15544
+ .intel-substrate .sub-line {
15545
+ display: flex; justify-content: space-between; align-items: center;
15546
+ gap: 10px; font-size: 12px;
15547
+ }
15548
+ .intel-substrate .sub-line.primary {
15549
+ font-family: var(--mono); font-size: 13px; color: var(--ink);
15550
+ }
15551
+ .intel-substrate .sub-line.secondary { color: var(--ink-3); font-size: 11px; }
15552
+ .intel-card-foot {
15553
+ display: flex; align-items: center; justify-content: space-between;
15554
+ gap: 8px; padding-top: 4px;
15555
+ }
15556
+ .intel-failures-toggle {
15557
+ display: inline-flex; align-items: center; gap: 6px;
15558
+ font-size: 12px; font-family: var(--mono);
15559
+ color: var(--ink-3);
15560
+ background: transparent; border: 0; padding: 0;
15561
+ cursor: pointer;
15562
+ }
15563
+ .intel-failures-toggle:hover { color: var(--ink); }
15564
+ .intel-failures-toggle .caret {
15565
+ display: inline-block; width: 0; height: 0;
15566
+ border-left: 4px solid transparent;
15567
+ border-right: 4px solid transparent;
15568
+ border-top: 5px solid currentColor;
15569
+ transition: transform 160ms ease;
15570
+ }
15571
+ .intel-failures-toggle.open .caret { transform: rotate(180deg); }
15572
+ .intel-failures {
15573
+ border-top: 1px solid var(--rule);
15574
+ padding-top: 12px;
15575
+ display: flex; flex-direction: column;
15576
+ gap: 8px;
15577
+ }
15578
+ .intel-failure-row {
15579
+ display: grid; grid-template-columns: 88px 1fr; gap: 12px;
15580
+ font-size: 12px; padding: 8px 10px;
15581
+ border-radius: var(--rad);
15582
+ background: var(--paper-2);
15583
+ border: 1px solid var(--rule);
15584
+ }
15585
+ .intel-failure-row .ts {
15586
+ font-family: var(--mono); font-size: 11px; color: var(--ink-3);
15587
+ }
15588
+ .intel-failure-row .err-class {
15589
+ font-family: var(--mono); font-size: 10px;
15590
+ letter-spacing: 0.04em; text-transform: uppercase;
15591
+ color: var(--rust); margin-bottom: 2px;
15592
+ }
15593
+ .btn-quiet {
15594
+ background: transparent; border: 1px solid var(--rule);
15595
+ padding: 2px 6px; font-size: 11px;
15596
+ border-radius: var(--rad); cursor: pointer;
15597
+ color: var(--ink-2); font-family: var(--sans);
15598
+ }
15599
+ .btn-quiet:hover { background: var(--surface-2); color: var(--ink); }
15005
15600
  .banner-warn {
15006
15601
  background: var(--ochre-bg); color: var(--ochre); border: 1px solid var(--ochre);
15007
- border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: 13px;
15602
+ border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: var(--text-base);
15008
15603
  }
15009
15604
  .banner-info {
15010
15605
  background: var(--indigo-bg); color: var(--indigo); border: 1px solid var(--indigo);
15011
- border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: 13px;
15606
+ border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: var(--text-base);
15012
15607
  }
15013
15608
  .btn.chip {
15014
- border-radius: 999px; padding: 4px 12px; font-size: 12px;
15609
+ border-radius: 999px; padding: 4px 12px; font-size: var(--text-sm);
15015
15610
  background: var(--surface-2); border-color: var(--rule);
15016
15611
  }
15017
15612
  .btn.chip:hover:not(:disabled) { background: var(--paper-3); }
@@ -15028,85 +15623,210 @@ body {
15028
15623
  * dynamically, but the input box is below the fold, so I still have
15029
15624
  * to scroll." rc.5 closes that with a structural layout fix.
15030
15625
  */
15626
+ /* Sprint Piece 2 PR 2 (2026-05-03): concierge surface polish.
15627
+ * Translates Claude Design references at
15628
+ * server/docs/design-refs/sprint-piece-2/surface-concierge.jsx and the
15629
+ * Surface 1 block of surfaces.css. The bounded-card layout above is
15630
+ * preserved verbatim because rc.5 and the DDD e2e suite depend on it;
15631
+ * the Claude Design reference uses height: 720px for the card, but
15632
+ * production keeps the calc-based bounded height so the card adapts
15633
+ * to the operator viewport. The polish lands the persona glyph-ring,
15634
+ * mono uppercase author labels, paper-2 background for concierge
15635
+ * replies, suggest-grid empty-state cards, and the composer input-wrap
15636
+ * with the keyboard-shortcut pill.
15637
+ */
15638
+ .concierge-wrap { max-width: 880px; margin: 0 auto; }
15639
+ .page-head {
15640
+ display: flex; align-items: flex-end; justify-content: space-between;
15641
+ gap: var(--space-4);
15642
+ margin-bottom: 18px; padding-bottom: 14px;
15643
+ border-bottom: 1px solid var(--rule);
15644
+ }
15645
+ .page-head .eyebrow {
15646
+ font-family: var(--mono); font-size: var(--text-xs);
15647
+ letter-spacing: 0.08em; text-transform: uppercase;
15648
+ color: var(--ink-3);
15649
+ margin: 0 0 6px;
15650
+ }
15651
+ .page-head h1 {
15652
+ font-family: var(--serif); font-weight: 400;
15653
+ font-size: 28px; letter-spacing: -0.01em;
15654
+ margin: 0 0 4px;
15655
+ }
15656
+ .page-head .sub {
15657
+ color: var(--ink-3); margin: 0;
15658
+ font-size: var(--text-base); max-width: 60ch;
15659
+ }
15031
15660
  .concierge-card {
15032
15661
  display: flex; flex-direction: column;
15033
15662
  height: calc(100vh - 180px);
15034
15663
  max-height: calc(100vh - 180px);
15035
15664
  min-height: 360px;
15036
- padding: 16px 18px;
15665
+ padding: 18px 22px 14px;
15037
15666
  gap: 0;
15038
15667
  }
15039
15668
  .concierge-header {
15040
15669
  display: flex; align-items: center; justify-content: space-between;
15041
- gap: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--rule);
15670
+ gap: var(--space-3); padding-bottom: 14px;
15671
+ border-bottom: 1px solid var(--rule);
15042
15672
  flex-wrap: wrap;
15043
15673
  flex-shrink: 0;
15044
15674
  }
15045
- .concierge-persona {
15046
- display: flex; align-items: baseline; gap: 8px;
15047
- font-size: 14px;
15675
+ .concierge-persona { display: flex; align-items: center; gap: 10px; }
15676
+ .concierge-persona .glyph-ring {
15677
+ width: 26px; height: 26px;
15678
+ border: 1.5px solid var(--ink-2);
15679
+ border-radius: 50%;
15680
+ position: relative;
15681
+ flex-shrink: 0;
15682
+ }
15683
+ .concierge-persona .glyph-ring::after {
15684
+ content: ""; position: absolute; inset: 5px;
15685
+ border-radius: 50%; background: var(--ink-2);
15686
+ }
15687
+ .concierge-persona-text { display: flex; flex-direction: column; }
15688
+ .concierge-persona-text strong {
15689
+ font-family: var(--serif); font-weight: 500;
15690
+ font-size: 15px; letter-spacing: 0.005em;
15691
+ }
15692
+ .concierge-persona-text small {
15693
+ color: var(--ink-3); font-size: var(--text-xs);
15694
+ font-family: var(--mono);
15695
+ }
15696
+ .concierge-meta {
15697
+ display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
15048
15698
  }
15049
- .concierge-persona strong { font-size: 14px; }
15050
15699
  .concierge-badge { white-space: nowrap; }
15051
15700
  .concierge-history {
15052
15701
  flex: 1 1 auto;
15053
15702
  min-height: 0;
15054
15703
  overflow-y: auto;
15055
- padding: 14px 4px;
15056
- display: flex; flex-direction: column; gap: 14px;
15704
+ padding: 18px 4px 6px;
15705
+ display: flex; flex-direction: column; gap: 18px;
15057
15706
  }
15058
15707
  .concierge-msg {
15059
- display: flex; flex-direction: column; gap: 4px;
15060
- max-width: 80%;
15708
+ display: flex; flex-direction: column; gap: 5px;
15709
+ max-width: 78%;
15061
15710
  }
15062
15711
  .concierge-msg-author {
15063
- font-size: 11px;
15712
+ font-size: 10px;
15713
+ font-family: var(--mono);
15714
+ letter-spacing: 0.04em;
15715
+ text-transform: uppercase;
15716
+ color: var(--ink-4);
15064
15717
  }
15065
15718
  .concierge-msg-body {
15066
- padding: 10px 14px; border-radius: 12px;
15067
- border: 1px solid var(--rule); background: var(--surface);
15068
- white-space: pre-wrap; word-wrap: break-word;
15069
- font-size: 14px; line-height: 1.5;
15719
+ padding: 11px 14px;
15720
+ border-radius: 12px;
15721
+ border: 1px solid var(--rule);
15722
+ background: var(--surface);
15723
+ font-size: var(--text-md);
15724
+ line-height: 1.55;
15725
+ white-space: pre-wrap;
15726
+ word-wrap: break-word;
15070
15727
  }
15071
15728
  .concierge-msg-concierge { align-self: flex-start; }
15729
+ .concierge-msg-concierge .concierge-msg-body {
15730
+ background: var(--paper-2);
15731
+ }
15072
15732
  .concierge-msg-operator { align-self: flex-end; align-items: flex-end; }
15073
15733
  .concierge-msg-operator .concierge-msg-body {
15074
15734
  background: var(--ink); color: var(--paper); border-color: var(--ink);
15075
15735
  }
15736
+ .concierge-msg-meta {
15737
+ display: flex; gap: 6px;
15738
+ font-size: 10px;
15739
+ color: var(--ink-4);
15740
+ font-family: var(--mono);
15741
+ }
15076
15742
  .concierge-empty {
15077
- padding: 24px 8px; text-align: left;
15078
- font-size: 13px; line-height: 1.6;
15743
+ flex: 1 1 auto;
15744
+ display: flex; flex-direction: column; gap: 22px;
15745
+ justify-content: center;
15746
+ padding: 24px 12px;
15747
+ }
15748
+ .concierge-empty-headline { max-width: 52ch; }
15749
+ .concierge-empty-headline h2 {
15750
+ font-family: var(--serif); font-weight: 400;
15751
+ font-size: var(--text-xl); margin: 0 0 6px;
15752
+ letter-spacing: -0.005em;
15753
+ }
15754
+ .concierge-empty-headline p {
15755
+ color: var(--ink-3); margin: 0;
15756
+ font-size: var(--text-md); line-height: 1.55;
15757
+ }
15758
+ .concierge-suggest-grid {
15759
+ display: grid;
15760
+ grid-template-columns: repeat(3, minmax(0, 1fr));
15761
+ gap: 10px;
15762
+ }
15763
+ .concierge-suggest {
15764
+ background: var(--surface);
15765
+ border: 1px solid var(--rule);
15766
+ border-radius: var(--rad);
15767
+ padding: 12px 14px;
15768
+ font-size: var(--text-base);
15769
+ cursor: pointer;
15770
+ display: flex; flex-direction: column;
15771
+ gap: 6px;
15772
+ text-align: left;
15773
+ font-family: var(--sans);
15774
+ color: var(--ink);
15775
+ }
15776
+ .concierge-suggest:hover:not(:disabled) {
15777
+ background: var(--surface-2);
15778
+ border-color: var(--rule-2);
15779
+ }
15780
+ .concierge-suggest:disabled {
15781
+ cursor: not-allowed; color: var(--ink-4); opacity: 0.7;
15782
+ }
15783
+ .concierge-suggest .label {
15784
+ font-family: var(--mono);
15785
+ font-size: 10px;
15786
+ letter-spacing: 0.06em;
15787
+ text-transform: uppercase;
15788
+ color: var(--ink-3);
15079
15789
  }
15080
15790
  .concierge-composer {
15081
15791
  display: flex; gap: 10px; align-items: center;
15082
- padding: 12px 0 8px;
15792
+ padding: 12px 0 4px;
15083
15793
  border-top: 1px solid var(--rule);
15084
15794
  flex-shrink: 0;
15085
15795
  }
15796
+ .concierge-composer .input-wrap {
15797
+ flex: 1;
15798
+ display: flex; align-items: center; gap: var(--space-2);
15799
+ padding: 8px 12px;
15800
+ border: 1px solid var(--rule);
15801
+ border-radius: var(--rad);
15802
+ background: var(--surface);
15803
+ }
15804
+ .concierge-composer .input-wrap:focus-within {
15805
+ border-color: var(--ink-3);
15806
+ }
15086
15807
  .concierge-composer input {
15087
15808
  flex: 1; min-width: 0;
15088
- padding: 10px 14px;
15089
- border: 1px solid var(--rule); border-radius: var(--rad);
15090
- font-family: var(--sans); font-size: 14px;
15091
- background: var(--surface); color: var(--ink);
15809
+ padding: 4px 0;
15810
+ border: 0;
15811
+ background: transparent;
15812
+ color: var(--ink);
15813
+ font-family: var(--sans);
15814
+ font-size: var(--text-md);
15815
+ outline: none;
15092
15816
  }
15093
- .concierge-composer input:focus {
15094
- outline: none; border-color: var(--ink-3);
15817
+ .concierge-composer input::placeholder { color: var(--ink-4); }
15818
+ .concierge-composer .composer-meta {
15819
+ font-family: var(--mono);
15820
+ font-size: 10px;
15821
+ color: var(--ink-4);
15822
+ letter-spacing: 0.04em;
15095
15823
  }
15096
15824
  .concierge-composer .btn-primary {
15097
- padding: 8px 18px; font-size: 13px; flex-shrink: 0;
15098
- }
15099
- .concierge-chips {
15100
- display: flex; flex-wrap: wrap; gap: 6px;
15101
- padding: 10px 0 0;
15102
- }
15103
- .concierge-chips::before {
15104
- content: "Try:"; color: var(--ink-3); font-size: 12px;
15105
- align-self: center; margin-right: 4px;
15825
+ padding: 8px 18px; font-size: var(--text-base); flex-shrink: 0;
15106
15826
  }
15107
15827
  .concierge-foot {
15108
15828
  margin: 12px 0 0; padding-top: 10px; border-top: 1px dashed var(--rule);
15109
- font-size: 12px;
15829
+ font-size: var(--text-sm);
15110
15830
  }
15111
15831
  .concierge-foot a { color: var(--ink-2); }
15112
15832
  .tier1-approval-card {
@@ -15114,20 +15834,532 @@ body {
15114
15834
  border-radius: var(--rad); padding: 14px 16px; margin: 12px 0;
15115
15835
  }
15116
15836
  .tier1-approval-card h3 {
15117
- margin: 0 0 8px; color: var(--ochre); font-size: 14px;
15837
+ margin: 0 0 8px; color: var(--ochre); font-size: var(--text-md);
15118
15838
  }
15119
- .tier1-approval-card p { margin: 0 0 12px; font-size: 13px; }
15839
+ .tier1-approval-card p { margin: 0 0 12px; font-size: var(--text-base); }
15120
15840
  .tier1-approval-card .actions {
15121
- display: flex; gap: 8px; flex-wrap: wrap;
15841
+ display: flex; gap: var(--space-2); flex-wrap: wrap;
15842
+ }
15843
+ /* Sprint Piece 2 PR 4 (2026-05-04): Agents view + Inspect pane polish.
15844
+ * Translates Claude Design references at
15845
+ * server/docs/design-refs/sprint-piece-2/surface-agents.jsx and the
15846
+ * Surface 3 block of surfaces.css. The fortress-column .agent-row,
15847
+ * .agent-row-head, and .agent-row-actions rules above are kept verbatim
15848
+ * because Finding DD tests pin them; the new Agents-view list scopes
15849
+ * its grid layout under .agents-list (descendant selector) so the
15850
+ * fortress-column rules are unaffected. The inspect pane combines the
15851
+ * existing .card surface with .inspect-pane structure (sticky right
15852
+ * rail, internal scroll, sectioned body) for the agent-detail view.
15853
+ */
15854
+ .agents-wrap { max-width: 1080px; margin: 0 auto; }
15855
+ .agents-layout {
15856
+ display: grid;
15857
+ grid-template-columns: 1fr 420px;
15858
+ gap: 20px;
15859
+ align-items: start;
15860
+ }
15861
+ .agents-list {
15862
+ background: var(--surface);
15863
+ border: 1px solid var(--rule);
15864
+ border-radius: var(--rad-lg);
15865
+ overflow: hidden;
15866
+ }
15867
+ .agents-list-head {
15868
+ display: grid;
15869
+ grid-template-columns: minmax(0, 1fr) 110px 120px 88px;
15870
+ gap: 12px;
15871
+ padding: 10px 16px;
15872
+ border-bottom: 1px solid var(--rule);
15873
+ background: var(--paper-2);
15874
+ font-family: var(--mono);
15875
+ font-size: 10px;
15876
+ letter-spacing: 0.06em;
15877
+ text-transform: uppercase;
15878
+ color: var(--ink-3);
15879
+ }
15880
+ .agents-list .agent-row {
15881
+ display: grid;
15882
+ grid-template-columns: minmax(0, 1fr) 110px 120px 88px;
15883
+ gap: 12px;
15884
+ padding: 14px 16px;
15885
+ border-bottom: 1px solid var(--rule);
15886
+ align-items: center;
15887
+ cursor: pointer;
15888
+ transition: background 120ms ease;
15889
+ }
15890
+ .agents-list .agent-row:last-child { border-bottom: 0; }
15891
+ .agents-list .agent-row:hover { background: var(--paper-2); }
15892
+ .agents-list .agent-row.selected {
15893
+ background: var(--paper-2);
15894
+ box-shadow: inset 3px 0 0 var(--ink);
15895
+ }
15896
+ .agent-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
15897
+ .agent-glyph {
15898
+ width: 28px; height: 28px;
15899
+ border-radius: var(--rad);
15900
+ background: var(--paper-3);
15901
+ border: 1px solid var(--rule);
15902
+ display: grid; place-items: center;
15903
+ flex-shrink: 0;
15904
+ font-family: var(--mono);
15905
+ font-size: 11px;
15906
+ color: var(--ink-2);
15907
+ font-weight: 600;
15908
+ }
15909
+ .agent-name {
15910
+ display: flex; flex-direction: column; min-width: 0;
15911
+ }
15912
+ .agent-name strong {
15913
+ font-size: var(--text-base); font-weight: 500;
15914
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
15915
+ }
15916
+ .agent-name small {
15917
+ font-family: var(--mono); font-size: var(--text-xs); color: var(--ink-3);
15918
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
15919
+ display: block;
15920
+ }
15921
+ .agent-state {
15922
+ display: inline-flex; align-items: center; gap: 6px;
15923
+ font-size: var(--text-xs);
15924
+ font-family: var(--mono);
15925
+ }
15926
+ .state-dot {
15927
+ width: 7px; height: 7px; border-radius: 50%;
15928
+ background: var(--ink-4);
15929
+ }
15930
+ .state-dot.live { background: var(--sage); animation: pulse-soft 2.4s ease-in-out infinite; }
15931
+ .state-dot.idle { background: var(--ochre); }
15932
+ .state-dot.off { background: var(--ink-4); }
15933
+ @keyframes pulse-soft {
15934
+ 0%, 100% { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
15935
+ 50% { box-shadow: 0 0 0 4px transparent; opacity: 0.7; }
15936
+ }
15937
+ .agent-last {
15938
+ font-family: var(--mono); font-size: var(--text-xs); color: var(--ink-3);
15939
+ }
15940
+ /* Inspect pane (combined with .card outer wrapper for the
15941
+ * renderAgentInspectPanel return-shape regex anchored in
15942
+ * dashboard-welcome.test.ts:152). The .inspect-pane modifier overrides
15943
+ * .card padding so internal sections control their own spacing.
15944
+ */
15945
+ .inspect-pane {
15946
+ padding: 0;
15947
+ display: flex; flex-direction: column;
15948
+ position: sticky;
15949
+ top: 20px;
15950
+ max-height: calc(100vh - 100px);
15951
+ overflow: hidden;
15952
+ }
15953
+ .inspect-head {
15954
+ padding: 16px 18px;
15955
+ border-bottom: 1px solid var(--rule);
15956
+ display: flex; flex-direction: column; gap: 10px;
15957
+ }
15958
+ .inspect-head .row1 {
15959
+ display: flex; align-items: center; gap: 10px;
15960
+ }
15961
+ .inspect-head h3 {
15962
+ font-family: var(--serif); font-weight: 500;
15963
+ font-size: 17px; margin: 0;
15964
+ }
15965
+ .inspect-head .meta {
15966
+ display: flex; gap: 6px; flex-wrap: wrap;
15967
+ }
15968
+ .inspect-body {
15969
+ overflow-y: auto;
15970
+ padding: 4px 18px 18px;
15971
+ }
15972
+ .inspect-section {
15973
+ padding: 14px 0;
15974
+ border-bottom: 1px solid var(--rule);
15975
+ }
15976
+ .inspect-section:last-child { border-bottom: 0; }
15977
+ .inspect-section h4 {
15978
+ font-family: var(--mono);
15979
+ font-size: 10px;
15980
+ letter-spacing: 0.08em;
15981
+ text-transform: uppercase;
15982
+ color: var(--ink-3);
15983
+ margin: 0 0 10px;
15984
+ display: flex; align-items: center; justify-content: space-between;
15985
+ }
15986
+ .inspect-section h4 .count {
15987
+ font-family: var(--mono);
15988
+ background: var(--paper-3);
15989
+ border-radius: 999px;
15990
+ padding: 1px 7px;
15991
+ color: var(--ink-2);
15992
+ font-size: 10px;
15993
+ }
15994
+ .approval-row {
15995
+ background: var(--ochre-bg);
15996
+ border: 1px solid var(--ochre);
15997
+ border-radius: var(--rad);
15998
+ padding: 10px 12px;
15999
+ margin-bottom: 8px;
16000
+ display: flex; flex-direction: column; gap: 8px;
16001
+ }
16002
+ .approval-row .what { font-size: var(--text-base); color: var(--ink); }
16003
+ .approval-row .what .pill { margin-right: 6px; }
16004
+ .approval-row .why {
16005
+ font-size: var(--text-sm); color: var(--ink-2);
16006
+ padding-left: 10px;
16007
+ border-left: 2px solid var(--ochre);
16008
+ }
16009
+ .approval-row .actions { display: flex; gap: 6px; justify-content: flex-end; }
16010
+ .timeline {
16011
+ display: flex; flex-direction: column; gap: 0;
16012
+ position: relative;
16013
+ padding-left: 14px;
16014
+ }
16015
+ .timeline::before {
16016
+ content: "";
16017
+ position: absolute;
16018
+ left: 4px; top: 6px; bottom: 6px;
16019
+ width: 1px;
16020
+ background: var(--rule);
16021
+ }
16022
+ .timeline-item {
16023
+ position: relative;
16024
+ padding: 6px 0 10px;
16025
+ font-size: var(--text-sm);
16026
+ }
16027
+ .timeline-item::before {
16028
+ content: "";
16029
+ position: absolute;
16030
+ left: -14px; top: 11px;
16031
+ width: 8px; height: 8px;
16032
+ border-radius: 50%;
16033
+ background: var(--surface);
16034
+ border: 1.5px solid var(--ink-4);
16035
+ }
16036
+ .timeline-item.ok::before { border-color: var(--sage); }
16037
+ .timeline-item.warn::before { border-color: var(--ochre); }
16038
+ .timeline-item.fail::before { border-color: var(--rust); }
16039
+ .timeline-item .ts {
16040
+ font-family: var(--mono); font-size: 10px;
16041
+ color: var(--ink-3);
16042
+ letter-spacing: 0.02em;
16043
+ }
16044
+ .timeline-item .what {
16045
+ margin-top: 2px; color: var(--ink); font-size: var(--text-base);
16046
+ }
16047
+ .timeline-item .att {
16048
+ margin-top: 4px;
16049
+ display: inline-flex;
16050
+ }
16051
+ .policy-line {
16052
+ display: flex; justify-content: space-between; align-items: center;
16053
+ padding: 5px 0; font-size: var(--text-base);
16054
+ border-bottom: 1px dashed var(--rule);
16055
+ }
16056
+ .policy-line:last-child { border-bottom: 0; }
16057
+ .policy-line .k { color: var(--ink-3); }
16058
+ .policy-line .v { font-family: var(--mono); font-size: var(--text-sm); color: var(--ink); }
16059
+ /* Empty-state block for when no agents are wrapped. The
16060
+ * renderAgentsList empty-state branch begins with the literal
16061
+ * '<h1>Agents</h1>' (regex-pinned in agents-empty-state-canary.test.ts)
16062
+ * and the "No wrapped agents yet." copy is preserved verbatim.
16063
+ */
16064
+ .agents-empty {
16065
+ background: var(--surface);
16066
+ border: 1px dashed var(--rule-2);
16067
+ border-radius: var(--rad-lg);
16068
+ padding: 56px 40px;
16069
+ text-align: center;
16070
+ max-width: 720px;
16071
+ margin: 32px auto;
16072
+ }
16073
+ .agents-empty .icon-frame {
16074
+ width: 64px; height: 64px;
16075
+ margin: 0 auto 18px;
16076
+ border: 1px solid var(--rule);
16077
+ border-radius: 50%;
16078
+ display: grid; place-items: center;
16079
+ position: relative;
16080
+ }
16081
+ .agents-empty .icon-frame::before,
16082
+ .agents-empty .icon-frame::after {
16083
+ content: "";
16084
+ position: absolute;
16085
+ border: 1px solid var(--rule);
16086
+ border-radius: 50%;
16087
+ }
16088
+ .agents-empty .icon-frame::before { inset: -8px; opacity: 0.6; }
16089
+ .agents-empty .icon-frame::after { inset: -16px; opacity: 0.3; }
16090
+ .agents-empty .icon-frame .core {
16091
+ width: 22px; height: 22px;
16092
+ background: var(--ink);
16093
+ border-radius: 50%;
16094
+ }
16095
+ .agents-empty h2 {
16096
+ font-family: var(--serif);
16097
+ font-weight: 400;
16098
+ font-size: var(--text-xl);
16099
+ margin: 0 0 8px;
16100
+ }
16101
+ .agents-empty p {
16102
+ color: var(--ink-3);
16103
+ margin: 0 0 20px;
16104
+ font-size: var(--text-md);
16105
+ line-height: 1.55;
16106
+ max-width: 50ch;
16107
+ margin-left: auto; margin-right: auto;
16108
+ }
16109
+ .terminal-block {
16110
+ text-align: left;
16111
+ background: var(--paper-3);
16112
+ border: 1px solid var(--rule);
16113
+ border-radius: var(--rad);
16114
+ padding: 14px 16px;
16115
+ font-family: var(--mono);
16116
+ font-size: var(--text-base);
16117
+ margin: 0 auto 16px;
16118
+ max-width: 480px;
16119
+ display: flex; align-items: center; justify-content: space-between;
16120
+ }
16121
+ .terminal-block .cmd { color: var(--ink); }
16122
+ .terminal-block .cmd .prompt { color: var(--ink-3); margin-right: 8px; user-select: none; }
16123
+ .copy-btn {
16124
+ background: transparent; border: 0;
16125
+ color: var(--ink-3); cursor: pointer;
16126
+ font-family: var(--mono); font-size: var(--text-xs);
16127
+ padding: 2px 6px;
16128
+ border-radius: var(--rad);
16129
+ }
16130
+ .copy-btn:hover { color: var(--ink); background: var(--paper-2); }
16131
+
16132
+ /* Surface 5. Attestation badge gallery. */
16133
+ .att-gallery {
16134
+ display: flex; flex-direction: column; gap: 24px;
16135
+ max-width: 1000px;
16136
+ margin: 0 auto;
16137
+ }
16138
+ .att-section {
16139
+ background: var(--surface);
16140
+ border: 1px solid var(--rule);
16141
+ border-radius: var(--rad-lg);
16142
+ padding: 22px 24px;
16143
+ }
16144
+ .att-section-head {
16145
+ display: flex; justify-content: space-between; align-items: baseline;
16146
+ gap: 12px;
16147
+ margin-bottom: 16px;
16148
+ padding-bottom: 12px;
16149
+ border-bottom: 1px solid var(--rule);
16150
+ }
16151
+ .att-section-head h2 {
16152
+ font-family: var(--serif);
16153
+ font-weight: 400;
16154
+ font-size: 19px;
16155
+ margin: 0 0 4px;
16156
+ }
16157
+ .att-section-head p {
16158
+ color: var(--ink-3);
16159
+ margin: 0;
16160
+ font-size: 13px;
16161
+ line-height: 1.5;
16162
+ max-width: 64ch;
16163
+ }
16164
+ .att-section-head .label {
16165
+ font-family: var(--mono);
16166
+ font-size: 10px;
16167
+ letter-spacing: 0.08em;
16168
+ text-transform: uppercase;
16169
+ color: var(--ink-3);
16170
+ }
16171
+ .att-row {
16172
+ display: grid;
16173
+ grid-template-columns: 240px 1fr;
16174
+ gap: 24px;
16175
+ padding: 14px 0;
16176
+ border-bottom: 1px dashed var(--rule);
16177
+ align-items: center;
16178
+ }
16179
+ .att-row:last-child { border-bottom: 0; }
16180
+ .att-row .demo {
16181
+ display: flex; align-items: center; justify-content: flex-start;
16182
+ padding: 12px 16px;
16183
+ background: var(--paper-2);
16184
+ border: 1px solid var(--rule);
16185
+ border-radius: var(--rad);
16186
+ min-height: 56px;
16187
+ }
16188
+ .att-row .desc strong {
16189
+ font-size: 13px; display: block; margin-bottom: 3px;
16190
+ }
16191
+ .att-row .desc small {
16192
+ color: var(--ink-3); font-size: 12px;
16193
+ line-height: 1.5;
16194
+ }
16195
+
16196
+ /* Global persistent badge. Lives in the topbar across every surface. */
16197
+ .att-global {
16198
+ display: inline-flex; align-items: center;
16199
+ gap: 8px;
16200
+ padding: 4px 10px 4px 6px;
16201
+ border: 1px solid var(--rule);
16202
+ border-radius: 999px;
16203
+ background: var(--surface-2);
16204
+ font-family: var(--mono);
16205
+ font-size: 11px;
16206
+ color: var(--ink-2);
16207
+ }
16208
+ .att-global.verified { border-color: var(--sage); background: var(--sage-bg); color: var(--sage); }
16209
+ .att-global.degraded { border-color: var(--ochre); background: var(--ochre-bg); color: var(--ochre); }
16210
+ .att-global.unverified { border-color: var(--rust); background: var(--rust-bg); color: var(--rust); }
16211
+ .att-global .seal {
16212
+ width: 18px; height: 18px;
16213
+ position: relative;
16214
+ flex-shrink: 0;
16215
+ }
16216
+ .att-global .seal-ring {
16217
+ position: absolute; inset: 0;
16218
+ border: 1.5px solid currentColor;
16219
+ border-radius: 50%;
16220
+ }
16221
+ .att-global .seal-ring.dashed { border-style: dashed; }
16222
+ .att-global .seal-core {
16223
+ position: absolute; inset: 4px;
16224
+ background: currentColor;
16225
+ border-radius: 50%;
16226
+ opacity: 0.85;
16227
+ }
16228
+ .att-global.degraded .seal-core { background: transparent; border: 1px solid currentColor; }
16229
+ .att-global.unverified .seal-core {
16230
+ background: transparent;
16231
+ border: 1px solid currentColor;
15122
16232
  }
16233
+ .att-global.unverified .seal-core::after {
16234
+ content: ""; position: absolute; inset: 0;
16235
+ background: currentColor; opacity: 0.4;
16236
+ clip-path: polygon(0 0, 100% 100%, 100% 90%, 10% 0);
16237
+ }
16238
+ .att-global .label {
16239
+ font-family: var(--mono);
16240
+ font-size: 11px;
16241
+ letter-spacing: 0.02em;
16242
+ text-transform: uppercase;
16243
+ }
16244
+ .att-global .hash {
16245
+ font-family: var(--mono);
16246
+ font-size: 10px;
16247
+ opacity: 0.7;
16248
+ border-left: 1px solid currentColor;
16249
+ padding-left: 8px;
16250
+ margin-left: 2px;
16251
+ }
16252
+
16253
+ /* Per-agent badge. Square chip beside each agent. */
16254
+ .att-agent {
16255
+ display: inline-flex; align-items: center;
16256
+ gap: 6px;
16257
+ padding: 3px 7px;
16258
+ border-radius: var(--rad);
16259
+ border: 1px solid var(--rule);
16260
+ background: var(--surface);
16261
+ font-family: var(--mono);
16262
+ font-size: 10px;
16263
+ color: var(--ink-2);
16264
+ }
16265
+ .att-agent .mark {
16266
+ width: 10px; height: 10px;
16267
+ border: 1.5px solid currentColor;
16268
+ border-radius: 2px;
16269
+ position: relative;
16270
+ }
16271
+ .att-agent.verified { color: var(--sage); border-color: var(--sage); background: var(--sage-bg); }
16272
+ .att-agent.verified .mark { background: currentColor; }
16273
+ .att-agent.degraded { color: var(--ochre); border-color: var(--ochre); background: var(--ochre-bg); }
16274
+ .att-agent.unverified { color: var(--rust); border-color: var(--rust); background: var(--rust-bg); }
16275
+ .att-agent.unverified .mark {
16276
+ background: repeating-linear-gradient(
16277
+ 45deg, currentColor, currentColor 1px,
16278
+ transparent 1px, transparent 3px
16279
+ );
16280
+ }
16281
+
16282
+ /* Per-action badge. Tiny inline tick on timeline rows. */
16283
+ .att-action {
16284
+ display: inline-flex; align-items: center; gap: 4px;
16285
+ font-family: var(--mono);
16286
+ font-size: 10px;
16287
+ color: var(--ink-3);
16288
+ padding: 1px 6px;
16289
+ border-radius: 4px;
16290
+ background: var(--paper-3);
16291
+ border: 1px solid transparent;
16292
+ }
16293
+ .att-action .tick {
16294
+ width: 6px; height: 6px;
16295
+ border-radius: 1px;
16296
+ background: currentColor;
16297
+ }
16298
+ .att-action.verified { color: var(--sage); }
16299
+ .att-action.degraded { color: var(--ochre); }
16300
+ .att-action.unverified { color: var(--rust); }
16301
+ .att-action.neutral .tick { background: var(--ink-4); border-radius: 50%; }
16302
+
16303
+ /* Custody-provenance badge stub (v1.x). Visibly stubbed with dashed border. */
16304
+ .att-custody {
16305
+ display: inline-flex; align-items: center; gap: 8px;
16306
+ padding: 4px 10px 4px 6px;
16307
+ border-radius: var(--rad);
16308
+ border: 1px dashed var(--rule-2);
16309
+ background: var(--paper-3);
16310
+ color: var(--ink-3);
16311
+ font-family: var(--mono);
16312
+ font-size: 10px;
16313
+ }
16314
+ .att-custody .seal-stub {
16315
+ width: 16px; height: 16px;
16316
+ border: 1px dashed var(--ink-4);
16317
+ border-radius: 50%;
16318
+ position: relative;
16319
+ flex-shrink: 0;
16320
+ }
16321
+ .att-custody .seal-stub::after {
16322
+ content: ""; position: absolute; inset: 4px;
16323
+ border: 1px dashed var(--ink-4);
16324
+ border-radius: 50%;
16325
+ }
16326
+ .att-custody .stub-tag {
16327
+ letter-spacing: 0.06em;
16328
+ text-transform: uppercase;
16329
+ }
16330
+
16331
+ /* Tooltip surface for badges. */
16332
+ .att-tooltip {
16333
+ background: var(--ink);
16334
+ color: var(--paper);
16335
+ font-family: var(--mono);
16336
+ font-size: 11px;
16337
+ padding: 8px 10px;
16338
+ border-radius: var(--rad);
16339
+ max-width: 280px;
16340
+ line-height: 1.5;
16341
+ display: inline-block;
16342
+ }
16343
+ [data-theme="dark"] .att-tooltip {
16344
+ background: var(--paper-3);
16345
+ color: var(--ink);
16346
+ }
16347
+
15123
16348
  @media (max-width: 1100px) {
15124
16349
  .app, .app.route-full { grid-template-columns: 56px 1fr; grid-template-areas: "sidebar topbar" "sidebar main"; }
15125
16350
  .fortress { display: none; }
15126
16351
  .sidebar h1, .sidebar nav a span { display: none; }
16352
+ .sidebar nav a { justify-content: center; padding: 8px 6px; }
15127
16353
  .template-grid { grid-template-columns: 1fr; }
15128
16354
  .policy-center h1 { font-size: 30px; }
15129
16355
  .intel-center h1 { font-size: 30px; }
15130
16356
  .intel-row { grid-template-columns: 1fr; }
16357
+ .intel-grid { grid-template-columns: 1fr; }
16358
+ .intel-failure-row { grid-template-columns: 1fr; }
16359
+ .agents-layout { grid-template-columns: 1fr; }
16360
+ .agents-list-head, .agents-list .agent-row { grid-template-columns: minmax(0, 1fr) 90px 90px; }
16361
+ .agents-list-head span:nth-child(4), .agents-list .agent-row > .agent-last { display: none; }
16362
+ .inspect-pane { position: static; max-height: none; }
15131
16363
  }
15132
16364
  `;
15133
16365
  NAV_ITEMS = [
@@ -15135,11 +16367,26 @@ body {
15135
16367
  { id: "agents", label: "Agents" },
15136
16368
  { id: "policy", label: "Policy" },
15137
16369
  { id: "intelligence", label: "Intelligence" },
16370
+ { id: "attestation", label: "Attestation" },
15138
16371
  { id: "privacy", label: "Privacy" },
15139
16372
  { id: "coordination", label: "Coordination" },
15140
16373
  { id: "health", label: "Health" },
15141
16374
  { id: "exit-drill", label: "Exit drill" }
15142
16375
  ];
16376
+ NAV_ICON_PATHS = {
16377
+ dashboard: '<rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/>',
16378
+ agents: '<circle cx="6" cy="5.5" r="2.3"/><path d="M2 13.5c0-2.2 1.8-4 4-4s4 1.8 4 4"/><circle cx="11.5" cy="6" r="1.8"/><path d="M14 12.5c0-1.7-1.1-2.7-2.5-3"/>',
16379
+ policy: '<path d="M4 2h5l3 3v9H4z"/><path d="M9 2v3h3"/><path d="M6 9h4M6 11.5h4"/>',
16380
+ intelligence: '<rect x="3.5" y="3.5" width="9" height="9" rx="0.5"/><rect x="6" y="6" width="4" height="4"/><path d="M6 1.5v2M10 1.5v2M6 12.5v2M10 12.5v2M1.5 6h2M1.5 10h2M12.5 6h2M12.5 10h2"/>',
16381
+ attestation: '<circle cx="8" cy="8" r="5.5"/><circle cx="8" cy="8" r="2.2"/><path d="M8 1.5v1.5M8 13v1.5M1.5 8h1.5M13 8h1.5"/>',
16382
+ privacy: '<path d="M8 1.5L3 3v4.5c0 3 2.2 5.4 5 7 2.8-1.6 5-4 5-7V3z"/><path d="M6 8l1.5 1.5L10.5 6"/>',
16383
+ coordination: '<circle cx="4" cy="3.5" r="1.4"/><circle cx="4" cy="12.5" r="1.4"/><circle cx="12" cy="8" r="1.4"/><path d="M4 4.9V11.1M4 5c0 3 3 3 6.7 3"/>',
16384
+ health: '<path d="M2 8h2.5l1.5-4 3 8 1.5-4H14"/>',
16385
+ "exit-drill": '<path d="M9.5 2H3v12h6.5"/><path d="M11 5l3 3-3 3"/><path d="M14 8H6.5"/>'
16386
+ };
16387
+ SVG_OPEN = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">';
16388
+ THEME_ICON_MOON = SVG_OPEN + '<path d="M13 9.5A5.5 5.5 0 0 1 6.5 3 5.5 5.5 0 1 0 13 9.5z"/></svg>';
16389
+ THEME_ICON_SUN = SVG_OPEN + '<circle cx="8" cy="8" r="2.5"/><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.2 3.2l1.4 1.4M11.4 11.4l1.4 1.4M3.2 12.8l1.4-1.4M11.4 4.6l1.4-1.4"/></svg>';
15143
16390
  }
15144
16391
  });
15145
16392
 
@@ -15848,7 +17095,7 @@ var init_dashboard = __esm({
15848
17095
  server = createServer$2(handler);
15849
17096
  }
15850
17097
  this.httpServer = server;
15851
- return new Promise((resolve5, reject) => {
17098
+ return new Promise((resolve6, reject) => {
15852
17099
  const protocol = this.useTLS ? "https" : "http";
15853
17100
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
15854
17101
  server.listen(this.config.port, this.config.host, () => {
@@ -15873,7 +17120,7 @@ var init_dashboard = __esm({
15873
17120
  if (shouldAutoOpen) {
15874
17121
  this.openInBrowser(sessionUrl);
15875
17122
  }
15876
- resolve5();
17123
+ resolve6();
15877
17124
  });
15878
17125
  server.on("error", (err) => {
15879
17126
  if (err.code === "EADDRINUSE") {
@@ -15919,8 +17166,8 @@ var init_dashboard = __esm({
15919
17166
  }
15920
17167
  this.rateLimits.clear();
15921
17168
  if (this.httpServer) {
15922
- return new Promise((resolve5) => {
15923
- this.httpServer.close(() => resolve5());
17169
+ return new Promise((resolve6) => {
17170
+ this.httpServer.close(() => resolve6());
15924
17171
  });
15925
17172
  }
15926
17173
  }
@@ -15934,7 +17181,7 @@ var init_dashboard = __esm({
15934
17181
  `[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
15935
17182
  `
15936
17183
  );
15937
- return new Promise((resolve5) => {
17184
+ return new Promise((resolve6) => {
15938
17185
  const timer = setTimeout(() => {
15939
17186
  this.pending.delete(id);
15940
17187
  const response = {
@@ -15948,12 +17195,12 @@ var init_dashboard = __esm({
15948
17195
  decision: response.decision,
15949
17196
  decided_by: "timeout"
15950
17197
  });
15951
- resolve5(response);
17198
+ resolve6(response);
15952
17199
  }, this.config.timeout_seconds * 1e3);
15953
17200
  const pending = {
15954
17201
  id,
15955
17202
  request,
15956
- resolve: resolve5,
17203
+ resolve: resolve6,
15957
17204
  timer,
15958
17205
  created_at: (/* @__PURE__ */ new Date()).toISOString()
15959
17206
  };
@@ -16819,7 +18066,7 @@ var init_webhook = __esm({
16819
18066
  * Start the callback listener server.
16820
18067
  */
16821
18068
  async start() {
16822
- return new Promise((resolve5, reject) => {
18069
+ return new Promise((resolve6, reject) => {
16823
18070
  this.callbackServer = createServer$2(
16824
18071
  (req, res) => this.handleCallback(req, res)
16825
18072
  );
@@ -16834,7 +18081,7 @@ var init_webhook = __esm({
16834
18081
 
16835
18082
  `
16836
18083
  );
16837
- resolve5();
18084
+ resolve6();
16838
18085
  }
16839
18086
  );
16840
18087
  this.callbackServer.on("error", reject);
@@ -16854,8 +18101,8 @@ var init_webhook = __esm({
16854
18101
  }
16855
18102
  this.pending.clear();
16856
18103
  if (this.callbackServer) {
16857
- return new Promise((resolve5) => {
16858
- this.callbackServer.close(() => resolve5());
18104
+ return new Promise((resolve6) => {
18105
+ this.callbackServer.close(() => resolve6());
16859
18106
  });
16860
18107
  }
16861
18108
  }
@@ -16868,7 +18115,7 @@ var init_webhook = __esm({
16868
18115
  `[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
16869
18116
  `
16870
18117
  );
16871
- return new Promise((resolve5) => {
18118
+ return new Promise((resolve6) => {
16872
18119
  const timer = setTimeout(() => {
16873
18120
  this.pending.delete(id);
16874
18121
  const response = {
@@ -16877,12 +18124,12 @@ var init_webhook = __esm({
16877
18124
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
16878
18125
  decided_by: "timeout"
16879
18126
  };
16880
- resolve5(response);
18127
+ resolve6(response);
16881
18128
  }, this.config.timeout_seconds * 1e3);
16882
18129
  const pending = {
16883
18130
  id,
16884
18131
  request,
16885
- resolve: resolve5,
18132
+ resolve: resolve6,
16886
18133
  timer,
16887
18134
  created_at: (/* @__PURE__ */ new Date()).toISOString()
16888
18135
  };
@@ -18547,8 +19794,8 @@ function verifySHR(shr, now) {
18547
19794
  const errors = [];
18548
19795
  const warnings = [];
18549
19796
  const currentTime = /* @__PURE__ */ new Date();
18550
- if (!shr.body || !shr.signed_by || !shr.signature) {
18551
- errors.push("Missing required SHR fields (body, signed_by, or signature)");
19797
+ if (!shr.body || !shr.signed_by || !shr.signature || !shr.signature_scheme) {
19798
+ errors.push("Missing required SHR fields (body, signed_by, signature_scheme, or signature)");
18552
19799
  return {
18553
19800
  valid: false,
18554
19801
  errors,
@@ -18561,6 +19808,9 @@ function verifySHR(shr, now) {
18561
19808
  if (shr.body.shr_version !== "1.0") {
18562
19809
  errors.push(`Unsupported SHR version: ${shr.body.shr_version}`);
18563
19810
  }
19811
+ if (shr.signature_scheme !== SIGNATURE_SCHEME_V1) {
19812
+ errors.push(`Unsupported SHR signature_scheme: ${String(shr.signature_scheme)}`);
19813
+ }
18564
19814
  const expiresAt = new Date(shr.body.expires_at);
18565
19815
  if (isNaN(expiresAt.getTime())) {
18566
19816
  errors.push("Invalid expires_at timestamp");
@@ -18622,6 +19872,7 @@ var init_verifier = __esm({
18622
19872
  init_types();
18623
19873
  init_identity();
18624
19874
  init_encoding();
19875
+ init_constants();
18625
19876
  }
18626
19877
  });
18627
19878
 
@@ -23581,7 +24832,7 @@ async function runOpenAIPrivacyFilter(text, config) {
23581
24832
  return parsed;
23582
24833
  }
23583
24834
  function runCommand(command, input, timeoutMs) {
23584
- return new Promise((resolve5, reject) => {
24835
+ return new Promise((resolve6, reject) => {
23585
24836
  const child = spawn(command, [], {
23586
24837
  stdio: ["pipe", "pipe", "pipe"],
23587
24838
  shell: false
@@ -23612,7 +24863,7 @@ function runCommand(command, input, timeoutMs) {
23612
24863
  ));
23613
24864
  return;
23614
24865
  }
23615
- resolve5(stdout);
24866
+ resolve6(stdout);
23616
24867
  });
23617
24868
  child.stdin.end(input);
23618
24869
  });
@@ -25488,13 +26739,13 @@ var init_proxy_router = __esm({
25488
26739
  * Call an upstream tool with a timeout.
25489
26740
  */
25490
26741
  async callWithTimeout(serverName, toolName, args, timeoutMs) {
25491
- return new Promise((resolve5, reject) => {
26742
+ return new Promise((resolve6, reject) => {
25492
26743
  const timer = setTimeout(() => {
25493
26744
  reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
25494
26745
  }, timeoutMs);
25495
26746
  this.clientManager.callTool(serverName, toolName, args).then((result) => {
25496
26747
  clearTimeout(timer);
25497
- resolve5(result);
26748
+ resolve6(result);
25498
26749
  }).catch((err) => {
25499
26750
  clearTimeout(timer);
25500
26751
  reject(err);
@@ -29999,8 +31250,6 @@ var init_inbox_aggregator = __esm({
29999
31250
  "src/hub/inbox-aggregator.ts"() {
30000
31251
  }
30001
31252
  });
30002
-
30003
- // src/hub/activity-feed.ts
30004
31253
  function categorizeOperation(layer, operation) {
30005
31254
  const op = operation.toLowerCase();
30006
31255
  if (op === "privacy_event" || op.startsWith("privacy_")) return "privacy";
@@ -30051,18 +31300,30 @@ function extractAgentIdHint(entry) {
30051
31300
  const value = details.agent_id;
30052
31301
  return typeof value === "string" && value.length > 0 ? value : void 0;
30053
31302
  }
31303
+ function deriveAttestationFragment(entryId) {
31304
+ const hash2 = createHash("sha256").update(entryId).digest("hex");
31305
+ return `${hash2.slice(0, 4)}..${hash2.slice(4, 6)}`;
31306
+ }
31307
+ function deriveAttestationState(entry) {
31308
+ return entry.result === "success" ? "verified" : "degraded";
31309
+ }
30054
31310
  function projectEntry(entry) {
30055
31311
  const agentIdHint = extractAgentIdHint(entry);
30056
31312
  const category = categorizeOperation(entry.layer, entry.operation);
31313
+ const entryId = `${entry.timestamp}|${entry.operation}|${entry.identity_id}`;
30057
31314
  return {
30058
31315
  version: "1.1",
30059
- entry_id: `${entry.timestamp}|${entry.operation}|${entry.identity_id}`,
31316
+ entry_id: entryId,
30060
31317
  emitted_at: entry.timestamp,
30061
31318
  ...agentIdHint ? { agent_id: agentIdHint } : {},
30062
31319
  identity_id: entry.identity_id,
30063
31320
  category,
30064
31321
  display_template_id: templateIdFor(category, entry.operation),
30065
- display_template_args: buildTemplateArgs(entry, agentIdHint)
31322
+ display_template_args: buildTemplateArgs(entry, agentIdHint),
31323
+ attestation: {
31324
+ state: deriveAttestationState(entry),
31325
+ fragment: deriveAttestationFragment(entryId)
31326
+ }
30066
31327
  };
30067
31328
  }
30068
31329
  async function aggregateActivity(sources, filter = {}) {
@@ -30127,7 +31388,7 @@ function snapshotForRecord(record, openInboxItemIds) {
30127
31388
  var HubService;
30128
31389
  var init_hub_service = __esm({
30129
31390
  "src/hub/hub-service.ts"() {
30130
- init_constants();
31391
+ init_constants2();
30131
31392
  init_channel_templates();
30132
31393
  init_constants3();
30133
31394
  init_errors4();
@@ -33040,10 +34301,10 @@ var init_memory = __esm({
33040
34301
  });
33041
34302
 
33042
34303
  // src/contracts/v1.1/constants.ts
33043
- var SIGNATURE_SCHEME_V1, EXIT_BUNDLE_MANIFEST_VERSION, EXIT_BUNDLE_ARTIFACT_KINDS;
34304
+ var SIGNATURE_SCHEME_V12, EXIT_BUNDLE_MANIFEST_VERSION, EXIT_BUNDLE_ARTIFACT_KINDS;
33044
34305
  var init_constants4 = __esm({
33045
34306
  "src/contracts/v1.1/constants.ts"() {
33046
- SIGNATURE_SCHEME_V1 = "ed25519-v1";
34307
+ SIGNATURE_SCHEME_V12 = "ed25519-v1";
33047
34308
  EXIT_BUNDLE_MANIFEST_VERSION = "SANCTUARY_EXIT_BUNDLE_V1";
33048
34309
  EXIT_BUNDLE_ARTIFACT_KINDS = [
33049
34310
  "public_identity",
@@ -33236,7 +34497,7 @@ function verifyReputationArtifact(reputationArtifact, publicKeysByDid) {
33236
34497
  unverifiable_attestations: unverifiable
33237
34498
  };
33238
34499
  }
33239
- async function verifyExitBundle(bundleDir) {
34500
+ async function verifyExitBundle(bundleDir, options = {}) {
33240
34501
  const root = resolve(bundleDir);
33241
34502
  let manifest;
33242
34503
  let manifestBytes;
@@ -33245,7 +34506,9 @@ async function verifyExitBundle(bundleDir) {
33245
34506
  manifestBytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
33246
34507
  manifest = JSON.parse(Buffer.from(raw).toString("utf8"));
33247
34508
  } catch {
33248
- return fail(root, null, "other", ["manifest.json is missing or unreadable"]);
34509
+ throw new InvalidExitBundleError(
34510
+ `Not a valid SANCTUARY_EXIT_BUNDLE_V1 directory: manifest.json missing at ${join(root, "manifest.json")}`
34511
+ );
33249
34512
  }
33250
34513
  const warnings = [];
33251
34514
  const unsupportedArtifacts = [];
@@ -33253,7 +34516,7 @@ async function verifyExitBundle(bundleDir) {
33253
34516
  if (!body || body.manifest_version !== EXIT_BUNDLE_MANIFEST_VERSION) {
33254
34517
  return fail(root, manifest, "manifest_unknown_version", warnings, unsupportedArtifacts);
33255
34518
  }
33256
- if (body.signature_scheme !== SIGNATURE_SCHEME_V1) {
34519
+ if (body.signature_scheme !== SIGNATURE_SCHEME_V12) {
33257
34520
  return fail(
33258
34521
  root,
33259
34522
  manifest,
@@ -33413,9 +34676,16 @@ async function verifyExitBundle(bundleDir) {
33413
34676
  }
33414
34677
  const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
33415
34678
  const identityFailed = identity ? !identity.signature_valid : false;
34679
+ const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
34680
+ const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
34681
+ if (unverifiableFailed) {
34682
+ warnings.push(
34683
+ `${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
34684
+ );
34685
+ }
33416
34686
  return {
33417
34687
  version: "1.1",
33418
- passed: !reputationFailed && !identityFailed,
34688
+ passed: !reputationFailed && !identityFailed && !unverifiableFailed,
33419
34689
  verified_at: (/* @__PURE__ */ new Date()).toISOString(),
33420
34690
  manifest_path: join(root, "manifest.json"),
33421
34691
  manifest_hash: sha256Hex2(manifestBytes),
@@ -33432,10 +34702,10 @@ async function verifyExitBundle(bundleDir) {
33432
34702
  identity,
33433
34703
  audit,
33434
34704
  reputation,
33435
- failure_class: reputationFailed || identityFailed ? "other" : void 0
34705
+ failure_class: reputationFailed || identityFailed || unverifiableFailed ? "other" : void 0
33436
34706
  };
33437
34707
  }
33438
- var PRIVATE_MATERIAL_KEYS;
34708
+ var InvalidExitBundleError, PRIVATE_MATERIAL_KEYS;
33439
34709
  var init_verifier2 = __esm({
33440
34710
  "src/exit/verifier.ts"() {
33441
34711
  init_constants4();
@@ -33443,6 +34713,12 @@ var init_verifier2 = __esm({
33443
34713
  init_encoding();
33444
34714
  init_hashing();
33445
34715
  init_canonical_json();
34716
+ InvalidExitBundleError = class extends Error {
34717
+ constructor(message) {
34718
+ super(message);
34719
+ this.name = "InvalidExitBundleError";
34720
+ }
34721
+ };
33446
34722
  PRIVATE_MATERIAL_KEYS = /* @__PURE__ */ new Set([
33447
34723
  "private_key",
33448
34724
  "privatekey",
@@ -33748,7 +35024,7 @@ async function exportExitBundle(opts) {
33748
35024
  ),
33749
35025
  artifacts_aggregate_hash_alg: "sha256",
33750
35026
  export_approval_audit_id: exportApprovalAuditId,
33751
- signature_scheme: SIGNATURE_SCHEME_V1
35027
+ signature_scheme: SIGNATURE_SCHEME_V12
33752
35028
  };
33753
35029
  const signature = sign(
33754
35030
  canonicalizeToBytes(body),
@@ -33933,7 +35209,9 @@ async function stageArtifact(storage, namespace, key, value) {
33933
35209
  await storage.write(namespace, key, jsonBytes(value));
33934
35210
  }
33935
35211
  async function importExitBundle(opts) {
33936
- const verification = await verifyExitBundle(opts.bundleDir);
35212
+ const verification = await verifyExitBundle(opts.bundleDir, {
35213
+ acceptUnverifiableAttestations: opts.acceptUnverifiableAttestations
35214
+ });
33937
35215
  if (!verification.passed) {
33938
35216
  return {
33939
35217
  verified: false,
@@ -34029,6 +35307,23 @@ async function importExitBundle(opts) {
34029
35307
  unsupported_artifacts: verification.unsupported_artifacts
34030
35308
  };
34031
35309
  }
35310
+ if (conflicts.public_identity_exists && !opts.forceRebind) {
35311
+ throw new ExitBundleImportError(
35312
+ "IDENTITY_OVERWRITE_REFUSED",
35313
+ "Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
35314
+ );
35315
+ }
35316
+ if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
35317
+ opts.auditLog.append(
35318
+ "l1",
35319
+ "exit_bundle_force_rebind",
35320
+ identityArtifact.json.bundle.identity_id,
35321
+ {
35322
+ manifest_version: manifest.body.manifest_version,
35323
+ fortress_id: manifest.body.identity_binding.fortress_id
35324
+ }
35325
+ );
35326
+ }
34032
35327
  const importId = importIdForManifest(manifest);
34033
35328
  const stagedArtifacts = [];
34034
35329
  if (identityArtifact) {
@@ -34139,7 +35434,7 @@ function exitBundleManifestShape() {
34139
35434
  manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
34140
35435
  artifacts: [...EXIT_BUNDLE_ARTIFACT_KINDS],
34141
35436
  hash_alg: "sha256",
34142
- signature_scheme: SIGNATURE_SCHEME_V1,
35437
+ signature_scheme: SIGNATURE_SCHEME_V12,
34143
35438
  required_top_level_file: "manifest.json",
34144
35439
  artifact_paths: [
34145
35440
  "artifacts/public_identity.json",
@@ -34152,7 +35447,7 @@ function exitBundleManifestShape() {
34152
35447
  ]
34153
35448
  };
34154
35449
  }
34155
- var ARTIFACT_DIR, EXIT_IMPORT_NAMESPACE, EXIT_PUBLIC_IDENTITIES_NAMESPACE, EXIT_AUDIT_RECEIPTS_NAMESPACE, EXIT_POLICY_SETS_NAMESPACE, EXIT_COMMITMENTS_NAMESPACE, EXIT_PLACEHOLDER_METADATA_NAMESPACE, PRIVACY_PLACEHOLDER_NAMESPACE;
35450
+ var ARTIFACT_DIR, EXIT_IMPORT_NAMESPACE, EXIT_PUBLIC_IDENTITIES_NAMESPACE, EXIT_AUDIT_RECEIPTS_NAMESPACE, EXIT_POLICY_SETS_NAMESPACE, EXIT_COMMITMENTS_NAMESPACE, EXIT_PLACEHOLDER_METADATA_NAMESPACE, PRIVACY_PLACEHOLDER_NAMESPACE, ExitBundleImportError;
34156
35451
  var init_bundle = __esm({
34157
35452
  "src/exit/bundle.ts"() {
34158
35453
  init_state_store();
@@ -34174,6 +35469,14 @@ var init_bundle = __esm({
34174
35469
  EXIT_COMMITMENTS_NAMESPACE = "_exit_commitments";
34175
35470
  EXIT_PLACEHOLDER_METADATA_NAMESPACE = "_exit_placeholder_metadata";
34176
35471
  PRIVACY_PLACEHOLDER_NAMESPACE = "_privacy_placeholder_vault";
35472
+ ExitBundleImportError = class extends Error {
35473
+ code;
35474
+ constructor(code, message) {
35475
+ super(message);
35476
+ this.name = "ExitBundleImportError";
35477
+ this.code = code;
35478
+ }
35479
+ };
34177
35480
  }
34178
35481
  });
34179
35482
  function write(stream, text) {
@@ -34272,6 +35575,11 @@ Options:
34272
35575
  --destination-identity-id <id> Destination signer for re-keyed state
34273
35576
  --state-namespace <name> Export a namespace; repeatable
34274
35577
  --conflict <skip|overwrite|version>
35578
+ --force-rebind On import: explicitly replace an existing fortress
35579
+ public identity (Tier 1 confirmation)
35580
+ --accept-unverifiable-attestations
35581
+ On import: accept reputation attestations whose
35582
+ signer DID is not in the bundle (Tier 1 confirmation)
34275
35583
  --json
34276
35584
  --yes, -y Explicit non-interactive Tier 1 approval
34277
35585
  --help, -h
@@ -34300,7 +35608,22 @@ async function runExitCommand(args) {
34300
35608
  write(err, "Usage: sanctuary exit verify <dir>\n");
34301
35609
  return 2;
34302
35610
  }
34303
- const result = await verifyExitBundle(dir);
35611
+ let result;
35612
+ try {
35613
+ result = await verifyExitBundle(dir, {
35614
+ acceptUnverifiableAttestations: hasFlag(
35615
+ argv,
35616
+ "--accept-unverifiable-attestations"
35617
+ )
35618
+ });
35619
+ } catch (e) {
35620
+ if (e instanceof InvalidExitBundleError) {
35621
+ write(err, `Error: ${e.message}
35622
+ `);
35623
+ return 1;
35624
+ }
35625
+ throw e;
35626
+ }
34304
35627
  if (json) {
34305
35628
  write(out, JSON.stringify(result, null, 2) + "\n");
34306
35629
  } else {
@@ -34378,9 +35701,15 @@ async function runExitCommand(args) {
34378
35701
  return 2;
34379
35702
  }
34380
35703
  const activate = hasFlag(argv, "--activate");
35704
+ const forceRebind = hasFlag(argv, "--force-rebind");
35705
+ const acceptUnverifiableAttestations = hasFlag(
35706
+ argv,
35707
+ "--accept-unverifiable-attestations"
35708
+ );
34381
35709
  if (activate) {
35710
+ const prompt2 = forceRebind ? "Tier 1 approval required: activate verified imported exit bundle AND replace the existing fortress public identity (force-rebind)?" : "Tier 1 approval required: activate verified imported exit bundle?";
34382
35711
  const approved = await confirmTier1(
34383
- "Tier 1 approval required: activate verified imported exit bundle?",
35712
+ prompt2,
34384
35713
  hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
34385
35714
  stdin,
34386
35715
  err
@@ -34389,6 +35718,18 @@ async function runExitCommand(args) {
34389
35718
  write(err, "Aborted.\n");
34390
35719
  return 1;
34391
35720
  }
35721
+ if (acceptUnverifiableAttestations) {
35722
+ const acceptApproved = await confirmTier1(
35723
+ "Tier 1 approval required: accept unverifiable reputation attestations on import?",
35724
+ hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
35725
+ stdin,
35726
+ err
35727
+ );
35728
+ if (!acceptApproved) {
35729
+ write(err, "Aborted.\n");
35730
+ return 1;
35731
+ }
35732
+ }
34392
35733
  }
34393
35734
  const ctx = await openExitContext(argv, env);
34394
35735
  const conflict = flagValue(argv, "--conflict") ?? "skip";
@@ -34396,19 +35737,31 @@ async function runExitCommand(args) {
34396
35737
  write(err, "--conflict must be skip, overwrite, or version\n");
34397
35738
  return 2;
34398
35739
  }
34399
- const result = await importExitBundle({
34400
- bundleDir: dir,
34401
- storage: ctx.storage,
34402
- masterKey: ctx.masterKey,
34403
- identityManager: ctx.identityManager,
34404
- auditLog: ctx.auditLog,
34405
- reputationStore: ctx.reputationStore,
34406
- activate,
34407
- conflictResolution: conflict,
34408
- sourcePassphrase: flagValue(argv, "--source-passphrase"),
34409
- sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
34410
- destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
34411
- });
35740
+ let result;
35741
+ try {
35742
+ result = await importExitBundle({
35743
+ bundleDir: dir,
35744
+ storage: ctx.storage,
35745
+ masterKey: ctx.masterKey,
35746
+ identityManager: ctx.identityManager,
35747
+ auditLog: ctx.auditLog,
35748
+ reputationStore: ctx.reputationStore,
35749
+ activate,
35750
+ forceRebind,
35751
+ acceptUnverifiableAttestations,
35752
+ conflictResolution: conflict,
35753
+ sourcePassphrase: flagValue(argv, "--source-passphrase"),
35754
+ sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
35755
+ destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
35756
+ });
35757
+ } catch (e) {
35758
+ if (e instanceof InvalidExitBundleError) {
35759
+ write(err, `Error: ${e.message}
35760
+ `);
35761
+ return 1;
35762
+ }
35763
+ throw e;
35764
+ }
34412
35765
  if (json) write(out, JSON.stringify(result, null, 2) + "\n");
34413
35766
  else {
34414
35767
  write(out, `verified: ${result.verified}
@@ -34463,6 +35816,7 @@ var init_cli = __esm({
34463
35816
  // src/exit/index.ts
34464
35817
  var exit_exports = {};
34465
35818
  __export(exit_exports, {
35819
+ ExitBundleImportError: () => ExitBundleImportError,
34466
35820
  exitBundleManifestShape: () => exitBundleManifestShape,
34467
35821
  exportExitBundle: () => exportExitBundle,
34468
35822
  importExitBundle: () => importExitBundle,
@@ -34519,11 +35873,11 @@ async function startDashboardServer(options) {
34519
35873
  }
34520
35874
  }
34521
35875
  });
34522
- await new Promise((resolve5, reject) => {
35876
+ await new Promise((resolve6, reject) => {
34523
35877
  server.once("error", reject);
34524
35878
  server.listen(port, host, () => {
34525
35879
  server.off("error", reject);
34526
- resolve5();
35880
+ resolve6();
34527
35881
  });
34528
35882
  });
34529
35883
  const actualPort = (() => {
@@ -34536,8 +35890,8 @@ async function startDashboardServer(options) {
34536
35890
  url,
34537
35891
  port: actualPort,
34538
35892
  host,
34539
- stop: () => new Promise((resolve5, reject) => {
34540
- server.close((err) => err ? reject(err) : resolve5());
35893
+ stop: () => new Promise((resolve6, reject) => {
35894
+ server.close((err) => err ? reject(err) : resolve6());
34541
35895
  }),
34542
35896
  publish,
34543
35897
  publishActivity: (entry) => publish({ type: "activity", data: entry }),
@@ -34685,7 +36039,7 @@ async function createSanctuaryServer(options) {
34685
36039
  const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
34686
36040
  if (hasKeyParams) {
34687
36041
  throw new Error(
34688
- "Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
36042
+ "Sanctuary: passphrase required.\n\nThe fortress at this path uses passphrase-mode key derivation.\nSet SANCTUARY_PASSPHRASE in your environment, or run\n'sanctuary export-passphrase' to retrieve it from the macOS Keychain."
34689
36043
  );
34690
36044
  }
34691
36045
  masterKey = generateRandomKey();
@@ -35210,7 +36564,7 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
35210
36564
  clientManager.configure(enabledServers).catch((err) => {
35211
36565
  console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
35212
36566
  });
35213
- await new Promise((resolve5) => setTimeout(resolve5, 2e3));
36567
+ await new Promise((resolve6) => setTimeout(resolve6, 2e3));
35214
36568
  const proxiedTools = proxyRouter.getProxiedTools();
35215
36569
  if (proxiedTools.length > 0) {
35216
36570
  allTools.push(...proxiedTools);
@@ -36049,8 +37403,8 @@ async function runWrap(options, deps = {}) {
36049
37403
  passphraseValue = process.env.SANCTUARY_PASSPHRASE;
36050
37404
  } else {
36051
37405
  try {
36052
- const resolve5 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
36053
- const resolved = await resolve5();
37406
+ const resolve6 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
37407
+ const resolved = await resolve6();
36054
37408
  passphraseLocation = resolved.location;
36055
37409
  passphraseSource = resolved.source;
36056
37410
  passphraseValue = resolved.value;
@@ -36090,24 +37444,31 @@ async function runWrap(options, deps = {}) {
36090
37444
  }
36091
37445
  await mkdir(storagePath, { recursive: true, mode: 448 });
36092
37446
  if (passphraseSource === "generated" && passphraseValue !== void 0) {
36093
- try {
36094
- await disclosePassphrase({
36095
- passphrase: passphraseValue,
36096
- storagePath,
36097
- fortressId: fortressIdFromStoragePath(storagePath),
36098
- // --no-open (CI / scripted) or non-TTY stdin both skip the prompt
36099
- // the same way init's --no-confirm does. Operator who scripted the
36100
- // call still gets the banner + the file; they will not see a hang.
36101
- mode: options.noOpen || process.stdin.isTTY !== true ? "no-confirm" : "interactive"
36102
- });
36103
- } catch (err) {
36104
- if (err instanceof PassphraseConfirmationDeclinedError || err instanceof PassphraseConfirmationNonInteractiveError) {
36105
- console.error(`
37447
+ if (options.writePassphraseBackup) {
37448
+ try {
37449
+ await disclosePassphrase({
37450
+ passphrase: passphraseValue,
37451
+ storagePath: dirname(options.writePassphraseBackup),
37452
+ fortressId: fortressIdFromStoragePath(storagePath),
37453
+ mode: options.noOpen || process.stdin.isTTY !== true ? "no-confirm" : "interactive"
37454
+ });
37455
+ } catch (err) {
37456
+ if (err instanceof PassphraseConfirmationDeclinedError || err instanceof PassphraseConfirmationNonInteractiveError) {
37457
+ console.error(`
36106
37458
  Sanctuary wrap: ${err.message}
36107
37459
  `);
36108
- process.exit(2);
37460
+ process.exit(2);
37461
+ }
37462
+ throw err;
36109
37463
  }
36110
- throw err;
37464
+ } else {
37465
+ process.stderr.write(
37466
+ `
37467
+ Passphrase stored in macOS Keychain.
37468
+ Run 'sanctuary export-passphrase' to retrieve it.
37469
+ To write a plaintext backup: sanctuary wrap ... --write-passphrase-backup <path>
37470
+ `
37471
+ );
36111
37472
  }
36112
37473
  }
36113
37474
  const profile = createWrapProfile(upstreamServers);
@@ -36198,6 +37559,8 @@ async function runWrap(options, deps = {}) {
36198
37559
  serverCount: upstreamServers.length});
36199
37560
  return;
36200
37561
  }
37562
+ let intelligenceHealthy;
37563
+ let intelligenceError;
36201
37564
  const authToken = generateAuthToken();
36202
37565
  const startFn = deps.startDashboard ?? ((opts) => startDashboard({
36203
37566
  port: opts.port,
@@ -36233,6 +37596,30 @@ async function runWrap(options, deps = {}) {
36233
37596
  );
36234
37597
  }
36235
37598
  const wrapAuditLog = new AuditLog(v11Storage, derived.key);
37599
+ try {
37600
+ const { IdentityManager: IdentityManager2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
37601
+ const { createIdentity: createIdentity2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
37602
+ const { derivePurposeKey: derivePurposeKey2 } = await Promise.resolve().then(() => (init_key_derivation(), key_derivation_exports));
37603
+ const identityMgr = new IdentityManager2(v11Storage, derived.key);
37604
+ const loadResult = await identityMgr.load();
37605
+ if (loadResult.loaded === 0) {
37606
+ const identityEncKey = derivePurposeKey2(derived.key, "identity-encryption");
37607
+ const { storedIdentity, publicIdentity } = createIdentity2(
37608
+ "default",
37609
+ identityEncKey,
37610
+ "passphrase"
37611
+ );
37612
+ await identityMgr.save(storedIdentity);
37613
+ wrapAuditLog.append("l1", "identity_create", publicIdentity.identity_id, {
37614
+ label: "default",
37615
+ source: "wrap-auto"
37616
+ });
37617
+ }
37618
+ } catch (err) {
37619
+ console.error(
37620
+ ` Note: default identity not created at wrap time (${err.message}).`
37621
+ );
37622
+ }
36236
37623
  let wrapIntelligenceSelector;
36237
37624
  try {
36238
37625
  wrapIntelligenceSelector = new SubstrateSelector({
@@ -36242,7 +37629,10 @@ async function runWrap(options, deps = {}) {
36242
37629
  identityId: `fortress:${storagePath}`
36243
37630
  });
36244
37631
  await wrapIntelligenceSelector.load();
37632
+ intelligenceHealthy = true;
36245
37633
  } catch (err) {
37634
+ intelligenceHealthy = false;
37635
+ intelligenceError = err.message;
36246
37636
  console.error(
36247
37637
  ` Note: Intelligence panel unavailable on wrap URL (${err.message}).`
36248
37638
  );
@@ -36310,7 +37700,10 @@ async function runWrap(options, deps = {}) {
36310
37700
  toolCount: countUpstreamTools(upstreamServers),
36311
37701
  serverCount: upstreamServers.length,
36312
37702
  dashboardUrl,
36313
- browserOpened: !options.noOpen});
37703
+ browserOpened: !options.noOpen,
37704
+ intelligenceHealthy,
37705
+ intelligenceError
37706
+ });
36314
37707
  }
36315
37708
  async function runCocoon(options) {
36316
37709
  console.error(
@@ -36366,12 +37759,12 @@ async function defaultOpenBrowser(url) {
36366
37759
  cmd = "xdg-open";
36367
37760
  args = [url];
36368
37761
  }
36369
- await new Promise((resolve5) => {
37762
+ await new Promise((resolve6) => {
36370
37763
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
36371
- child.on("error", () => resolve5());
37764
+ child.on("error", () => resolve6());
36372
37765
  child.on("spawn", () => {
36373
37766
  child.unref();
36374
- resolve5();
37767
+ resolve6();
36375
37768
  });
36376
37769
  });
36377
37770
  }
@@ -36397,7 +37790,15 @@ function formatWrapSuccess(info) {
36397
37790
  lines.push(` ${d("(browser auto-open suppressed)")}`);
36398
37791
  }
36399
37792
  lines.push("");
36400
- lines.push(` ${b("Your agent is protected.")} L1 Full / L2 Degraded (no TEE) / L3 Full / L4 Full.`);
37793
+ const l2Status = info.intelligenceHealthy === false ? "L2 Degraded (intelligence disabled)" : "L2 Degraded (no TEE)";
37794
+ lines.push(` ${b("Your agent is protected.")} L1 Full / ${l2Status} / L3 Full / L4 Full.`);
37795
+ if (info.intelligenceHealthy === false && info.intelligenceError) {
37796
+ const w = (s) => `\x1B[33m${s}\x1B[0m`;
37797
+ lines.push("");
37798
+ lines.push(` ${w("\u26A0")} L2 intelligence disabled: ${info.intelligenceError}`);
37799
+ lines.push(` Concierge chat and substrate-driven explanations will not work until this is resolved.`);
37800
+ lines.push(` Run 'sanctuary intelligence diagnose' to inspect substrate config.`);
37801
+ }
36401
37802
  lines.push("");
36402
37803
  return lines.join("\n");
36403
37804
  }
@@ -36421,9 +37822,16 @@ function formatWrapSuccessNoDashboard(info) {
36421
37822
  ` ${d("Dashboard spawn skipped per --no-dashboard. Run `sanctuary dashboard` separately for a persistent dashboard.")}`
36422
37823
  );
36423
37824
  lines.push("");
37825
+ const l2Status = info.intelligenceHealthy === false ? "L2 Degraded (intelligence disabled)" : "L2 Degraded (no TEE)";
36424
37826
  lines.push(
36425
- ` ${b("Your agent is protected.")} L1 Full / L2 Degraded (no TEE) / L3 Full / L4 Full.`
37827
+ ` ${b("Your agent is protected.")} L1 Full / ${l2Status} / L3 Full / L4 Full.`
36426
37828
  );
37829
+ if (info.intelligenceHealthy === false && info.intelligenceError) {
37830
+ const w = (s) => `\x1B[33m${s}\x1B[0m`;
37831
+ lines.push("");
37832
+ lines.push(` ${w("\u26A0")} L2 intelligence disabled: ${info.intelligenceError}`);
37833
+ lines.push(` Run 'sanctuary intelligence diagnose' to inspect substrate config.`);
37834
+ }
36427
37835
  lines.push("");
36428
37836
  return lines.join("\n");
36429
37837
  }
@@ -36623,7 +38031,22 @@ function rawConfigContainsSanctuary(raw, agentPlatform) {
36623
38031
  function parseWrapArgs(argv) {
36624
38032
  const options = {};
36625
38033
  for (let i = 0; i < argv.length; i++) {
36626
- switch (argv[i]) {
38034
+ const arg = argv[i];
38035
+ if (!arg.startsWith("-")) {
38036
+ const suggestion = WRAP_HARNESS_FLAGS.find(
38037
+ (f) => f.replace(/^--/, "") === arg
38038
+ );
38039
+ const hint = suggestion ? ` Did you mean ${suggestion}?` : "";
38040
+ throw new Error(
38041
+ `Unrecognized argument '${arg}'.${hint} Run 'sanctuary wrap --help' for valid flags.`
38042
+ );
38043
+ }
38044
+ if (!WRAP_BOOLEAN_FLAGS.has(arg) && !WRAP_VALUE_FLAGS.has(arg)) {
38045
+ throw new Error(
38046
+ `Unrecognized flag '${arg}'. Run 'sanctuary wrap --help' for valid flags.`
38047
+ );
38048
+ }
38049
+ switch (arg) {
36627
38050
  case "--wrap":
36628
38051
  options.wrap = argv[++i];
36629
38052
  break;
@@ -36666,6 +38089,9 @@ function parseWrapArgs(argv) {
36666
38089
  case "--dev-dist":
36667
38090
  options.devDist = argv[++i];
36668
38091
  break;
38092
+ case "--write-passphrase-backup":
38093
+ options.writePassphraseBackup = argv[++i];
38094
+ break;
36669
38095
  case "--help":
36670
38096
  case "-h":
36671
38097
  printWrapHelp();
@@ -36725,7 +38151,7 @@ function printWrapHelp() {
36725
38151
  5. Every tool call is logged, scanned, and tier-gated
36726
38152
  `);
36727
38153
  }
36728
- var COCOON_GOVERNOR_DEFAULTS, PORT_FALLBACK_ATTEMPTS, parseCocoonArgs;
38154
+ var COCOON_GOVERNOR_DEFAULTS, PORT_FALLBACK_ATTEMPTS, WRAP_VALUE_FLAGS, WRAP_BOOLEAN_FLAGS, WRAP_HARNESS_FLAGS, parseCocoonArgs;
36729
38155
  var init_cli2 = __esm({
36730
38156
  "src/cocoon/cli.ts"() {
36731
38157
  init_config_reader();
@@ -36748,6 +38174,28 @@ var init_cli2 = __esm({
36748
38174
  lifetime_limit: 1e3
36749
38175
  };
36750
38176
  PORT_FALLBACK_ATTEMPTS = 20;
38177
+ WRAP_VALUE_FLAGS = /* @__PURE__ */ new Set([
38178
+ "--wrap",
38179
+ "--passphrase",
38180
+ "--port",
38181
+ "--fortress",
38182
+ "--dev-dist",
38183
+ "--write-passphrase-backup"
38184
+ ]);
38185
+ WRAP_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
38186
+ "--openclaw",
38187
+ "--hermes",
38188
+ "--claude-code",
38189
+ "--cursor",
38190
+ "--cline",
38191
+ "--unwrap",
38192
+ "--dry-run",
38193
+ "--no-open",
38194
+ "--no-dashboard",
38195
+ "--help",
38196
+ "-h"
38197
+ ]);
38198
+ WRAP_HARNESS_FLAGS = ["--openclaw", "--hermes", "--claude-code", "--cursor", "--cline"];
36751
38199
  parseCocoonArgs = parseWrapArgs;
36752
38200
  }
36753
38201
  });
@@ -37645,7 +39093,7 @@ var init_backend_interface = __esm({
37645
39093
  }
37646
39094
  });
37647
39095
  async function runSecurity(args, input) {
37648
- return new Promise((resolve5, reject) => {
39096
+ return new Promise((resolve6, reject) => {
37649
39097
  const child = spawn(SECURITY_BIN, args, { stdio: ["pipe", "pipe", "pipe"] });
37650
39098
  let stdout = "";
37651
39099
  let stderr = "";
@@ -37667,7 +39115,7 @@ async function runSecurity(args, input) {
37667
39115
  reject(err);
37668
39116
  });
37669
39117
  child.on("close", (code) => {
37670
- resolve5({ stdout, stderr, code: code ?? -1 });
39118
+ resolve6({ stdout, stderr, code: code ?? -1 });
37671
39119
  });
37672
39120
  if (input !== void 0) {
37673
39121
  child.stdin.write(input);
@@ -38743,7 +40191,7 @@ async function readValue(stdin, prompt2) {
38743
40191
  return await readFirstLine(stdin);
38744
40192
  }
38745
40193
  async function readFirstLine(stdin) {
38746
- return new Promise((resolve5, reject) => {
40194
+ return new Promise((resolve6, reject) => {
38747
40195
  const rl = createInterface$1({ input: stdin });
38748
40196
  let resolved = false;
38749
40197
  const finish = (value) => {
@@ -38754,7 +40202,7 @@ async function readFirstLine(stdin) {
38754
40202
  rl.close();
38755
40203
  } catch {
38756
40204
  }
38757
- resolve5(value);
40205
+ resolve6(value);
38758
40206
  };
38759
40207
  const deadline = setTimeout(() => {
38760
40208
  finish("");
@@ -38773,7 +40221,7 @@ async function promptSilently(stdin, prompt2) {
38773
40221
  process.stderr.write(`${prompt2}: `);
38774
40222
  stdin.setRawMode?.(true);
38775
40223
  stdin.resume();
38776
- return await new Promise((resolve5) => {
40224
+ return await new Promise((resolve6) => {
38777
40225
  let buf = "";
38778
40226
  const onData = (chunk) => {
38779
40227
  const s = chunk.toString("utf8");
@@ -38783,7 +40231,7 @@ async function promptSilently(stdin, prompt2) {
38783
40231
  stdin.pause();
38784
40232
  stdin.off("data", onData);
38785
40233
  process.stderr.write("\n");
38786
- resolve5(buf);
40234
+ resolve6(buf);
38787
40235
  return;
38788
40236
  }
38789
40237
  if (ch === "") {
@@ -39056,7 +40504,7 @@ async function probeTenantDashboard(tenant, options = {}) {
39056
40504
  return { running: false, status: null, reason: "no runtime.json" };
39057
40505
  }
39058
40506
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
39059
- return await new Promise((resolve5) => {
40507
+ return await new Promise((resolve6) => {
39060
40508
  const req = get$1(
39061
40509
  {
39062
40510
  host: rt.dashboard_host,
@@ -39068,9 +40516,9 @@ async function probeTenantDashboard(tenant, options = {}) {
39068
40516
  res.resume();
39069
40517
  const status = res.statusCode ?? 0;
39070
40518
  if (status > 0 && status < 500) {
39071
- resolve5({ running: true, status, reason: null });
40519
+ resolve6({ running: true, status, reason: null });
39072
40520
  } else {
39073
- resolve5({
40521
+ resolve6({
39074
40522
  running: false,
39075
40523
  status,
39076
40524
  reason: `dashboard returned ${status}`
@@ -39080,10 +40528,10 @@ async function probeTenantDashboard(tenant, options = {}) {
39080
40528
  );
39081
40529
  req.on("timeout", () => {
39082
40530
  req.destroy();
39083
- resolve5({ running: false, status: null, reason: "timeout" });
40531
+ resolve6({ running: false, status: null, reason: "timeout" });
39084
40532
  });
39085
40533
  req.on("error", (err) => {
39086
- resolve5({
40534
+ resolve6({
39087
40535
  running: false,
39088
40536
  status: null,
39089
40537
  reason: err.code ?? err.message
@@ -39745,7 +41193,7 @@ async function prompt(lines, err, question) {
39745
41193
  return await lines.next();
39746
41194
  }
39747
41195
  async function defaultExec2(cmd, args) {
39748
- return await new Promise((resolve5, reject) => {
41196
+ return await new Promise((resolve6, reject) => {
39749
41197
  const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
39750
41198
  let stdout = "";
39751
41199
  let stderr = "";
@@ -39756,7 +41204,7 @@ async function defaultExec2(cmd, args) {
39756
41204
  stderr += d.toString();
39757
41205
  });
39758
41206
  child.on("error", reject);
39759
- child.on("close", (code) => resolve5({ stdout, stderr, code }));
41207
+ child.on("close", (code) => resolve6({ stdout, stderr, code }));
39760
41208
  });
39761
41209
  }
39762
41210
  var LineReader;
@@ -39792,8 +41240,8 @@ var init_reset_passphrase = __esm({
39792
41240
  return Promise.resolve(this.queue.shift());
39793
41241
  }
39794
41242
  if (this.closed) return Promise.resolve("");
39795
- return new Promise((resolve5) => {
39796
- this.waiters.push(resolve5);
41243
+ return new Promise((resolve6) => {
41244
+ this.waiters.push(resolve6);
39797
41245
  });
39798
41246
  }
39799
41247
  close() {
@@ -39808,6 +41256,100 @@ var init_reset_passphrase = __esm({
39808
41256
  }
39809
41257
  });
39810
41258
 
41259
+ // src/cli/intelligence.ts
41260
+ var intelligence_exports = {};
41261
+ __export(intelligence_exports, {
41262
+ runIntelligenceCommand: () => runIntelligenceCommand
41263
+ });
41264
+ async function runIntelligenceCommand(opts) {
41265
+ const subcommand = opts.argv[0];
41266
+ if (subcommand === "diagnose" || subcommand === void 0) {
41267
+ return runDiagnose();
41268
+ }
41269
+ if (subcommand === "--help" || subcommand === "-h") {
41270
+ printHelp2();
41271
+ return 0;
41272
+ }
41273
+ console.error(`Unknown intelligence subcommand: ${subcommand}`);
41274
+ printHelp2();
41275
+ return 2;
41276
+ }
41277
+ function printHelp2() {
41278
+ console.error(`
41279
+ Usage: sanctuary intelligence <subcommand>
41280
+
41281
+ Subcommands:
41282
+ diagnose Print intelligence substrate config and last error.
41283
+ --help Show this help.
41284
+ `);
41285
+ }
41286
+ async function runDiagnose() {
41287
+ const storagePath = resolve(
41288
+ process.env.SANCTUARY_STORAGE_PATH ?? process.env.SANCTUARY_FORTRESS_PATH ?? `${process.env.HOME}/.sanctuary`
41289
+ );
41290
+ console.error(`Intelligence substrate diagnostics`);
41291
+ console.error(`Fortress: ${storagePath}`);
41292
+ console.error("");
41293
+ const intelligenceDir = resolve(storagePath, "state", "_intelligence");
41294
+ if (!existsSync(intelligenceDir)) {
41295
+ console.error(
41296
+ `No intelligence config directory found at ${intelligenceDir}.`
41297
+ );
41298
+ console.error(
41299
+ "Intelligence substrate may not have been initialized yet."
41300
+ );
41301
+ console.error(
41302
+ "Ensure at least one substrate API key is set in your environment:"
41303
+ );
41304
+ console.error(
41305
+ " ANTHROPIC_API_KEY, OPENAI_API_KEY, VENICE_API_KEY, or OLLAMA_HOST"
41306
+ );
41307
+ return 1;
41308
+ }
41309
+ try {
41310
+ const entries = readdirSync(intelligenceDir);
41311
+ console.error(`Intelligence config entries: ${entries.length}`);
41312
+ for (const entry of entries) {
41313
+ console.error(` ${entry}`);
41314
+ }
41315
+ } catch {
41316
+ console.error(`Could not read intelligence config directory.`);
41317
+ }
41318
+ const auditDir = resolve(storagePath, "state", "_audit");
41319
+ if (existsSync(auditDir)) {
41320
+ try {
41321
+ const auditFiles = readdirSync(auditDir).sort().reverse();
41322
+ const recentFiles = auditFiles.slice(0, 20);
41323
+ console.error("");
41324
+ console.error(`Recent audit entries (${recentFiles.length} of ${auditFiles.length}):`);
41325
+ for (const file of recentFiles) {
41326
+ console.error(` ${file}`);
41327
+ }
41328
+ } catch {
41329
+ console.error(`Could not read audit directory.`);
41330
+ }
41331
+ }
41332
+ console.error("");
41333
+ console.error("Substrate environment check:");
41334
+ const keys = [
41335
+ "ANTHROPIC_API_KEY",
41336
+ "OPENAI_API_KEY",
41337
+ "VENICE_API_KEY",
41338
+ "OLLAMA_HOST"
41339
+ ];
41340
+ for (const key of keys) {
41341
+ const val = process.env[key];
41342
+ console.error(
41343
+ ` ${key}: ${val ? `set (${val.slice(0, 4)}...)` : "not set"}`
41344
+ );
41345
+ }
41346
+ return 0;
41347
+ }
41348
+ var init_intelligence = __esm({
41349
+ "src/cli/intelligence.ts"() {
41350
+ }
41351
+ });
41352
+
39811
41353
  // src/mcp/broker-server.ts
39812
41354
  var broker_server_exports = {};
39813
41355
  __export(broker_server_exports, {
@@ -40098,11 +41640,11 @@ async function startMultiDashboardServer(options = {}) {
40098
41640
  }
40099
41641
  }
40100
41642
  });
40101
- await new Promise((resolve5, reject) => {
41643
+ await new Promise((resolve6, reject) => {
40102
41644
  server.once("error", reject);
40103
41645
  server.listen(port, host, () => {
40104
41646
  server.off("error", reject);
40105
- resolve5();
41647
+ resolve6();
40106
41648
  });
40107
41649
  });
40108
41650
  const addr = server.address();
@@ -40111,8 +41653,8 @@ async function startMultiDashboardServer(options = {}) {
40111
41653
  url: `http://${host}:${actualPort}`,
40112
41654
  port: actualPort,
40113
41655
  host,
40114
- stop: () => new Promise((resolve5, reject) => {
40115
- server.close((err) => err ? reject(err) : resolve5());
41656
+ stop: () => new Promise((resolve6, reject) => {
41657
+ server.close((err) => err ? reject(err) : resolve6());
40116
41658
  })
40117
41659
  };
40118
41660
  }
@@ -40512,7 +42054,7 @@ function formatUpdateMessage(current, latest) {
40512
42054
  return `[Sanctuary] Update available: ${current} \u2192 ${latest}. Run: npx @sanctuary-framework/mcp-server@latest`;
40513
42055
  }
40514
42056
  function fetchLatestVersion(currentVersion) {
40515
- return new Promise((resolve5) => {
42057
+ return new Promise((resolve6) => {
40516
42058
  const req = get(
40517
42059
  REGISTRY_URL,
40518
42060
  {
@@ -40522,7 +42064,7 @@ function fetchLatestVersion(currentVersion) {
40522
42064
  (res) => {
40523
42065
  if (res.statusCode !== 200) {
40524
42066
  res.resume();
40525
- resolve5(null);
42067
+ resolve6(null);
40526
42068
  return;
40527
42069
  }
40528
42070
  let data = "";
@@ -40531,7 +42073,7 @@ function fetchLatestVersion(currentVersion) {
40531
42073
  data += chunk;
40532
42074
  if (data.length > 32768) {
40533
42075
  res.destroy();
40534
- resolve5(null);
42076
+ resolve6(null);
40535
42077
  }
40536
42078
  });
40537
42079
  res.on("end", () => {
@@ -40539,20 +42081,20 @@ function fetchLatestVersion(currentVersion) {
40539
42081
  const json = JSON.parse(data);
40540
42082
  const latest = json.version;
40541
42083
  if (typeof latest === "string" && isNewerVersion(currentVersion, latest)) {
40542
- resolve5(latest);
42084
+ resolve6(latest);
40543
42085
  } else {
40544
- resolve5(null);
42086
+ resolve6(null);
40545
42087
  }
40546
42088
  } catch {
40547
- resolve5(null);
42089
+ resolve6(null);
40548
42090
  }
40549
42091
  });
40550
42092
  }
40551
42093
  );
40552
- req.on("error", () => resolve5(null));
42094
+ req.on("error", () => resolve6(null));
40553
42095
  req.on("timeout", () => {
40554
42096
  req.destroy();
40555
- resolve5(null);
42097
+ resolve6(null);
40556
42098
  });
40557
42099
  });
40558
42100
  }
@@ -40645,6 +42187,11 @@ async function main() {
40645
42187
  const code = await runResetPassphraseCommand2({ argv: args.slice(1) });
40646
42188
  process.exit(code);
40647
42189
  }
42190
+ if (args[0] === "intelligence") {
42191
+ const { runIntelligenceCommand: runIntelligenceCommand2 } = await Promise.resolve().then(() => (init_intelligence(), intelligence_exports));
42192
+ const code = await runIntelligenceCommand2({ argv: args.slice(1) });
42193
+ process.exit(code);
42194
+ }
40648
42195
  if (args[0] === "broker-server") {
40649
42196
  const { openBroker: openBroker2 } = await Promise.resolve().then(() => (init_open(), open_exports));
40650
42197
  const { createBrokerMcpServer: createBrokerMcpServer2 } = await Promise.resolve().then(() => (init_broker_server(), broker_server_exports));
@@ -40668,7 +42215,7 @@ async function main() {
40668
42215
  );
40669
42216
  passphrase = args[++i];
40670
42217
  } else if (args[i] === "--help" || args[i] === "-h") {
40671
- printHelp2();
42218
+ printHelp3();
40672
42219
  process.exit(0);
40673
42220
  } else if (args[i] === "--version" || args[i] === "-v") {
40674
42221
  console.log(`@sanctuary-framework/mcp-server ${PKG_VERSION4}`);
@@ -40813,7 +42360,7 @@ async function runExportPassphrase(args) {
40813
42360
  }
40814
42361
  process.stdout.write(stored.value + "\n");
40815
42362
  }
40816
- function printHelp2() {
42363
+ function printHelp3() {
40817
42364
  console.log(`
40818
42365
  @sanctuary-framework/mcp-server v${PKG_VERSION4}
40819
42366