@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/index.cjs CHANGED
@@ -710,19 +710,40 @@ function assertSanctuaryConfigShape(c) {
710
710
 
711
711
  // src/storage/filesystem.ts
712
712
  init_random();
713
+ var SAFE_CHARS = /[^A-Za-z0-9_.\-]/g;
714
+ function bijectiveEncode(name) {
715
+ return name.replace(
716
+ SAFE_CHARS,
717
+ (ch) => "!" + ch.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()
718
+ );
719
+ }
720
+ function legacyNamespaceSanitize(name) {
721
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
722
+ }
723
+ function legacyKeySanitize(name) {
724
+ return name.replace(/[^a-zA-Z0-9_.-]/g, "_");
725
+ }
713
726
  var FilesystemStorage = class {
714
727
  basePath;
715
728
  constructor(basePath) {
716
729
  this.basePath = basePath;
717
730
  }
718
731
  entryPath(namespace, key) {
719
- const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
720
- const safeKey = key.replace(/[^a-zA-Z0-9_.-]/g, "_");
732
+ const safeNamespace = bijectiveEncode(namespace);
733
+ const safeKey = bijectiveEncode(key);
721
734
  return path.join(this.basePath, safeNamespace, `${safeKey}.enc`);
722
735
  }
723
736
  namespacePath(namespace) {
724
- const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
725
- return path.join(this.basePath, safeNamespace);
737
+ return path.join(this.basePath, bijectiveEncode(namespace));
738
+ }
739
+ // Legacy on-disk paths produced by the pre-#41 sanitizer. Returned for
740
+ // ENOENT-fallback in read/exists/delete; never written to.
741
+ legacyEntryPath(namespace, key) {
742
+ return path.join(
743
+ this.basePath,
744
+ legacyNamespaceSanitize(namespace),
745
+ `${legacyKeySanitize(key)}.enc`
746
+ );
726
747
  }
727
748
  async write(namespace, key, data) {
728
749
  const dirPath = this.namespacePath(namespace);
@@ -731,7 +752,13 @@ var FilesystemStorage = class {
731
752
  await promises.writeFile(filePath, data, { mode: 384 });
732
753
  }
733
754
  async read(namespace, key) {
734
- const filePath = this.entryPath(namespace, key);
755
+ const buf = await this.readAtPath(this.entryPath(namespace, key));
756
+ if (buf !== null) return buf;
757
+ const legacy = this.legacyEntryPath(namespace, key);
758
+ if (legacy === this.entryPath(namespace, key)) return null;
759
+ return this.readAtPath(legacy);
760
+ }
761
+ async readAtPath(filePath) {
735
762
  try {
736
763
  const buf = await promises.readFile(filePath);
737
764
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
@@ -743,7 +770,13 @@ var FilesystemStorage = class {
743
770
  }
744
771
  }
745
772
  async delete(namespace, key, secureOverwrite = true) {
746
- const filePath = this.entryPath(namespace, key);
773
+ const newPath = this.entryPath(namespace, key);
774
+ if (await this.deleteAtPath(newPath, secureOverwrite)) return true;
775
+ const legacy = this.legacyEntryPath(namespace, key);
776
+ if (legacy === newPath) return false;
777
+ return this.deleteAtPath(legacy, secureOverwrite);
778
+ }
779
+ async deleteAtPath(filePath, secureOverwrite) {
747
780
  try {
748
781
  if (secureOverwrite) {
749
782
  const fileStat = await promises.stat(filePath);
@@ -789,12 +822,19 @@ var FilesystemStorage = class {
789
822
  }
790
823
  }
791
824
  async exists(namespace, key) {
792
- const filePath = this.entryPath(namespace, key);
825
+ const newPath = this.entryPath(namespace, key);
793
826
  try {
794
- await promises.stat(filePath);
827
+ await promises.stat(newPath);
795
828
  return true;
796
829
  } catch {
797
- return false;
830
+ const legacy = this.legacyEntryPath(namespace, key);
831
+ if (legacy === newPath) return false;
832
+ try {
833
+ await promises.stat(legacy);
834
+ return true;
835
+ } catch {
836
+ return false;
837
+ }
798
838
  }
799
839
  }
800
840
  async totalSize() {
@@ -4780,6 +4820,32 @@ function canonicalizeForSigning(body) {
4780
4820
  // src/shr/generator.ts
4781
4821
  init_identity();
4782
4822
  init_encoding();
4823
+
4824
+ // src/mesh/constants.ts
4825
+ var PROTOCOL_VERSION = "0.1";
4826
+ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
4827
+ var RESERVED_EVENT_TYPE_PREFIXES = [
4828
+ "EXTENSION_",
4829
+ "cross_fortress_",
4830
+ "multi_master_"
4831
+ ];
4832
+ function isReservedEventType(s) {
4833
+ return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
4834
+ }
4835
+ var RESERVED_EXTENSION_ENVELOPE_KEYS = [
4836
+ "cross_fortress_read_grant",
4837
+ "cross_fortress_read_query",
4838
+ "cross_fortress_read_response",
4839
+ "multi_master_policy_merge",
4840
+ "audit_replication_full_n_way",
4841
+ "auto_promote_canonical_audit",
4842
+ "agent_live_migration"
4843
+ ];
4844
+ function isReservedExtensionKey(k) {
4845
+ return RESERVED_EXTENSION_ENVELOPE_KEYS.includes(k);
4846
+ }
4847
+
4848
+ // src/shr/generator.ts
4783
4849
  var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
4784
4850
  var DEFAULT_FRESHNESS_WINDOW_DAYS = 30;
4785
4851
  var DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD = 0.6;
@@ -4930,6 +4996,7 @@ function generateSHR(identityId, opts) {
4930
4996
  return {
4931
4997
  body,
4932
4998
  signed_by: identity.public_key,
4999
+ signature_scheme: SIGNATURE_SCHEME_V1,
4933
5000
  signature: toBase64url(signatureBytes)
4934
5001
  };
4935
5002
  }
@@ -7166,14 +7233,14 @@ function generateDashboardHTML(options) {
7166
7233
  // cookie (set by /auth/session and sent automatically by the
7167
7234
  // browser) or as a ?session= query parameter, both of which Stack
7168
7235
  // A's checkAuth honours. Loopback callers also bypass auth via the
7169
- // v0.10.2 _autoAuthLocalhost path, which is the path moltbook
7236
+ // v0.10.2 _autoAuthLocalhost path, which is the path Mini1
7170
7237
  // hits when the dashboard is auto-opened on 127.0.0.1.
7171
7238
  //
7172
7239
  // The endpoint itself is /events \u2014 Stack A's route table mounts it
7173
7240
  // there, and the previous /api/events URL was a 404 in every real
7174
7241
  // boot from v0.10.0 through v0.10.4. The retry loop that result
7175
7242
  // produced is exactly the "status bar flashing blue continuously"
7176
- // moltbook reported on v0.10.4.
7243
+ // Mini1 reported on v0.10.4.
7177
7244
  const eventSource = new EventSource(API_BASE + '/events');
7178
7245
 
7179
7246
  eventSource.addEventListener('init', (e) => {
@@ -9985,10 +10052,8 @@ var CHANNEL_TEMPLATE_IDS = [
9985
10052
  "read-then-report",
9986
10053
  "scheduled-digest",
9987
10054
  "plan-draft-only",
9988
- "fortress-relay",
9989
- "concierge-loop"
10055
+ "fortress-relay"
9990
10056
  ];
9991
- var COUNTERPARTY_WILDCARD = "*";
9992
10057
  var BUDGET_UNITS = ["tokens", "usd"];
9993
10058
 
9994
10059
  // src/templates/registry.ts
@@ -10156,8 +10221,12 @@ function lintOnboarding(_name, content) {
10156
10221
  function resolveTemplatesDir() {
10157
10222
  const thisFile = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
10158
10223
  const thisDir = path.dirname(thisFile);
10159
- if (thisDir.includes("/dist/")) {
10160
- return thisDir.replace("/dist/templates", "/src/templates");
10224
+ if (thisDir.includes("/dist")) {
10225
+ const templatesSubdir = path.join(thisDir, "templates");
10226
+ const candidateDir = fs.existsSync(path.join(thisDir, TEMPLATE_NAMES[0])) ? thisDir : fs.existsSync(path.join(templatesSubdir, TEMPLATE_NAMES[0])) ? templatesSubdir : null;
10227
+ if (candidateDir) return candidateDir;
10228
+ const srcFallback = thisDir.endsWith("/templates") ? thisDir.replace("/dist/templates", "/src/templates") : path.join(thisDir.replace("/dist", "/src"), "templates");
10229
+ return srcFallback;
10161
10230
  }
10162
10231
  return thisDir;
10163
10232
  }
@@ -10426,23 +10495,6 @@ var fortressRelay = (params) => {
10426
10495
  setRetentionDays(p, 90);
10427
10496
  return p;
10428
10497
  };
10429
- var conciergeLoop = (params) => {
10430
- const p = basePolicy(params);
10431
- p.source_english = "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward.";
10432
- grantOn(p, "memory", {
10433
- counterparty: params.counterparty,
10434
- action: "read",
10435
- scope: { local_fortress_state_only: true, ...params.scope ?? {} }
10436
- });
10437
- grantOn(p, "outputs", {
10438
- counterparty: params.counterparty || COUNTERPARTY_WILDCARD,
10439
- action: "read",
10440
- scope: { operator_chat_only: true, ...params.scope ?? {} }
10441
- });
10442
- p.egress = { allowlist: [] };
10443
- setRetentionDays(p, 14);
10444
- return p;
10445
- };
10446
10498
  function allowedHostsFromScope(scope) {
10447
10499
  const raw = scope?.allowed_hosts;
10448
10500
  if (!Array.isArray(raw)) return [];
@@ -10483,13 +10535,6 @@ var REGISTRY = {
10483
10535
  label: "Fortress relay",
10484
10536
  description: "Routes signed events between peer fortresses. Commits bind only when both sides sign.",
10485
10537
  factory: fortressRelay
10486
- },
10487
- "concierge-loop": {
10488
- id: "concierge-loop",
10489
- severity: "LOW",
10490
- label: "Concierge loop",
10491
- description: "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward.",
10492
- factory: conciergeLoop
10493
10538
  }
10494
10539
  };
10495
10540
  function applyChannelTemplate(id, params) {
@@ -10563,8 +10608,14 @@ function encode(value) {
10563
10608
  }
10564
10609
  function encodeArray(arr) {
10565
10610
  const parts = [];
10566
- for (const item of arr) {
10567
- parts.push(item === void 0 ? "null" : encode(item));
10611
+ for (let i = 0; i < arr.length; i++) {
10612
+ const item = arr[i];
10613
+ if (item === void 0) {
10614
+ throw new MeshCanonicalJsonError(
10615
+ `canonicalize(): undefined is not a valid JSON value at array index ${i}`
10616
+ );
10617
+ }
10618
+ parts.push(encode(item));
10568
10619
  }
10569
10620
  return "[" + parts.join(",") + "]";
10570
10621
  }
@@ -10869,29 +10920,6 @@ function encodePolicyBlob(policy) {
10869
10920
  init_encoding();
10870
10921
  init_random();
10871
10922
 
10872
- // src/mesh/constants.ts
10873
- var PROTOCOL_VERSION = "0.1";
10874
- var RESERVED_EVENT_TYPE_PREFIXES = [
10875
- "EXTENSION_",
10876
- "cross_fortress_",
10877
- "multi_master_"
10878
- ];
10879
- function isReservedEventType(s) {
10880
- return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
10881
- }
10882
- var RESERVED_EXTENSION_ENVELOPE_KEYS = [
10883
- "cross_fortress_read_grant",
10884
- "cross_fortress_read_query",
10885
- "cross_fortress_read_response",
10886
- "multi_master_policy_merge",
10887
- "audit_replication_full_n_way",
10888
- "auto_promote_canonical_audit",
10889
- "agent_live_migration"
10890
- ];
10891
- function isReservedExtensionKey(k) {
10892
- return RESERVED_EXTENSION_ENVELOPE_KEYS.includes(k);
10893
- }
10894
-
10895
10923
  // src/mesh/trust-root.ts
10896
10924
  init_encoding();
10897
10925
  init_identity();
@@ -12112,7 +12140,7 @@ async function api(path, opts) {
12112
12140
  // /policies, /activity responses on subsequent GETs even when the
12113
12141
  // server-side state has changed (e.g. recent-failures buffer cleared
12114
12142
  // on substrate flip). The pre-rc.5 client used bare fetch with no
12115
- // cache control, which on moltbook Safari produced a stale view of
12143
+ // cache control, which on Mini1 Safari produced a stale view of
12116
12144
  // server state and made the operator-visible badge color stick to
12117
12145
  // its prior value across substrate changes. Belt + suspenders:
12118
12146
  // cache: "no-store" turns off the response cache; the _t query
@@ -12274,12 +12302,6 @@ const CHANNEL_TEMPLATES = [
12274
12302
  severity: "MEDIUM",
12275
12303
  title: "Fortress relay",
12276
12304
  description: "Routes signed events between peer fortresses. Commits bind only when both sides sign."
12277
- },
12278
- {
12279
- id: "concierge-loop",
12280
- severity: "LOW",
12281
- title: "Concierge loop",
12282
- description: "Bidirectional Q&A with the operator. Reads local fortress state; never writes outward."
12283
12305
  }
12284
12306
  ];
12285
12307
 
@@ -12326,6 +12348,24 @@ function setRoute(route) {
12326
12348
  renderFortress();
12327
12349
  }
12328
12350
 
12351
+ // Renders the global attestation badge (Q1 layer 1, persistent across
12352
+ // surfaces). Tone is driven by state.topbarPills.attestation. Pending
12353
+ // state shows a dashed seal ring; verified shows solid; degraded shows
12354
+ // outlined core; unverified shows the broken-seal mark. Observation
12355
+ // language only; Castle Layer 1 enforcement ships in WP-V1.x-CASTLE-WALL.
12356
+ function renderTopbarAttestationBadge(stateName) {
12357
+ const valid = stateName === "verified" || stateName === "degraded" || stateName === "unverified" || stateName === "pending";
12358
+ const cls = valid ? stateName : "pending";
12359
+ const ringDashed = cls === "pending" ? " dashed" : "";
12360
+ return '<span class="att-global ' + cls + '" data-pill="attestation" title="Fortress attestation">' +
12361
+ '<span class="seal">' +
12362
+ '<span class="seal-ring' + ringDashed + '"></span>' +
12363
+ '<span class="seal-core"></span>' +
12364
+ '</span>' +
12365
+ '<span class="label">' + escHtml(cls) + '</span>' +
12366
+ '</span>';
12367
+ }
12368
+
12329
12369
  function renderTopbar() {
12330
12370
  const pillEl = document.getElementById("topbar-pills");
12331
12371
  if (!pillEl) return;
@@ -12342,7 +12382,7 @@ function renderTopbar() {
12342
12382
  versionPill,
12343
12383
  '<span class="pill" data-pill="deployment">deployment: ' + escHtml(state.topbarPills.deployment) + '</span>',
12344
12384
  '<span class="pill" data-pill="mode">mode: ' + escHtml(state.topbarPills.mode) + '</span>',
12345
- '<span class="pill tone-' + escHtml(state.topbarPills.attestation) + '" data-pill="attestation">attestation: ' + escHtml(state.topbarPills.attestation) + '</span>'
12385
+ renderTopbarAttestationBadge(state.topbarPills.attestation)
12346
12386
  ].join("");
12347
12387
  // Lockdown button three-state UX (binding addendum 3).
12348
12388
  const btn = document.getElementById("btn-lockdown");
@@ -12438,6 +12478,7 @@ function renderMain() {
12438
12478
  case "agent-detail": nextHtml = renderAgentDetail(); break;
12439
12479
  case "policy": nextHtml = renderPolicyCenter(); break;
12440
12480
  case "intelligence": nextHtml = renderIntelligenceCenter(); break;
12481
+ case "attestation": nextHtml = renderAttestation(); break;
12441
12482
  case "privacy": nextHtml = renderPrivacyPage(); break;
12442
12483
  case "coordination": nextHtml = renderCoordinationPage(); break;
12443
12484
  case "health": nextHtml = renderHealthPage(); break;
@@ -12516,9 +12557,9 @@ function renderMain() {
12516
12557
  // "Concierge unavailable; substrate not configured") sourced from the
12517
12558
  // last response's served_by + display_label.
12518
12559
  const CONCIERGE_SUGGESTIONS = [
12519
- { id: "summarize-hour", label: "summarize the last hour", query: "Summarize what happened in this fortress in the last hour." },
12520
- { id: "agent-touched", label: "what has each agent touched today", query: "What has each wrapped agent done today? Group by agent." },
12521
- { id: "open-approvals", label: "any open approvals?", query: "Are there any open Tier 1 approvals or pending inbox items I should look at?" }
12560
+ { id: "summarize-hour", category: "Summarize", label: "summarize the last hour", query: "Summarize what happened in this fortress in the last hour." },
12561
+ { id: "agent-touched", category: "Inspect", label: "what has each agent touched today", query: "What has each wrapped agent done today? Group by agent." },
12562
+ { 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?" }
12522
12563
  ];
12523
12564
 
12524
12565
  // Direct-agent chat surface was removed in the v1.2 reshape; the
@@ -12535,59 +12576,316 @@ function renderDashboardConcierge() {
12535
12576
  const badge = c.badge && c.badge.displayLabel
12536
12577
  ? '<span class="pill mono concierge-badge" title="Substrate that served the most recent response">' + escHtml(c.badge.displayLabel) + '</span>'
12537
12578
  : '<span class="pill muted concierge-badge">Concierge: substrate not yet contacted</span>';
12538
- const messages = c.messages.length
12579
+ const sendDisabled = c.sending ? ' disabled' : '';
12580
+ const sendLabel = c.sending ? 'Sending...' : 'Send';
12581
+ // Sprint Piece 2 PR 2: empty state lives INSIDE the concierge-history
12582
+ // container so the DDD e2e selector .concierge-history matches both
12583
+ // empty and active state. The container's flex layout hosts a single
12584
+ // .concierge-empty child that fills the available height with a serif
12585
+ // headline and a 3-up suggest grid; the grid replaces the v1.2 bottom
12586
+ // chip row, which is retired with this polish.
12587
+ const emptyState =
12588
+ '<div class="concierge-empty">' +
12589
+ '<div class="concierge-empty-headline">' +
12590
+ '<h2>Where would you like to begin.</h2>' +
12591
+ '<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>' +
12592
+ '</div>' +
12593
+ '<div class="concierge-suggest-grid">' +
12594
+ CONCIERGE_SUGGESTIONS.map(function (s) {
12595
+ return '<button class="concierge-suggest" data-action="concierge-suggestion" data-suggestion-id="' + escHtml(s.id) + '"' + sendDisabled + '>' +
12596
+ '<span class="label">' + escHtml(s.category || '') + '</span>' +
12597
+ escHtml(s.label) +
12598
+ '</button>';
12599
+ }).join("") +
12600
+ '</div>' +
12601
+ '</div>';
12602
+ const messagesHtml = c.messages.length
12539
12603
  ? c.messages.map(function (m) {
12540
12604
  const cls = m.role === "operator" ? "concierge-msg-operator" : "concierge-msg-concierge";
12541
- const author = m.role === "operator" ? "you" : "Sanctuary Fortress concierge";
12605
+ const authorLabel = m.role === "operator" ? "you" : "sanctuary";
12606
+ const metaParts = [];
12607
+ if (m.created_at) metaParts.push(escHtml(shortTime(m.created_at)));
12608
+ if (m.role === "concierge" && m.served_by) metaParts.push('substrate: ' + escHtml(m.served_by));
12609
+ const meta = metaParts.length
12610
+ ? '<div class="concierge-msg-meta"><span>' + metaParts.join(' · ') + '</span></div>'
12611
+ : '';
12542
12612
  return '<div class="concierge-msg ' + cls + '">' +
12543
- '<div class="concierge-msg-author muted">' + escHtml(author) + ' · ' + escHtml(shortTime(m.created_at)) + '</div>' +
12613
+ '<span class="concierge-msg-author">' + escHtml(authorLabel) + '</span>' +
12544
12614
  '<div class="concierge-msg-body">' + escHtml(m.body) + '</div>' +
12615
+ meta +
12545
12616
  '</div>';
12546
12617
  }).join("\n")
12547
- : '<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>';
12618
+ : emptyState;
12548
12619
  const errorBanner = c.error
12549
12620
  ? '<div class="banner banner-warn">' + escHtml(c.error) + '</div>'
12550
12621
  : "";
12551
- const sendDisabled = c.sending ? ' disabled' : '';
12552
- const sendLabel = c.sending ? 'Sending...' : 'Send';
12553
- const chips = CONCIERGE_SUGGESTIONS.map(function (s) {
12554
- return '<button class="btn chip" data-action="concierge-suggestion" data-suggestion-id="' + escHtml(s.id) + '"' + sendDisabled + '>' + escHtml(s.label) + '</button>';
12555
- }).join("\n");
12556
12622
  const activeChatsPanel = renderActiveChatsPanel();
12557
12623
  return [
12558
- '<h1>Chat <span class="muted">/ This fortress</span></h1>',
12559
- activeChatsPanel,
12560
- '<div class="card concierge-card">',
12561
- '<div class="concierge-header">',
12562
- '<div class="concierge-persona"><strong>Sanctuary Fortress concierge</strong> <span class="muted">read-only over fortress state</span></div>',
12563
- badge,
12624
+ '<div class="concierge-wrap">',
12625
+ '<div class="page-head"><div>',
12626
+ '<p class="eyebrow">Concierge</p>',
12627
+ '<h1>Talk to your fortress.</h1>',
12628
+ '<p class="sub">A direct line to Sanctuary, routed through the substrate you chose. Nothing leaves without your hand on it.</p>',
12629
+ '</div></div>',
12630
+ activeChatsPanel,
12631
+ '<div class="card concierge-card">',
12632
+ '<div class="concierge-header">',
12633
+ '<div class="concierge-persona">',
12634
+ '<div class="glyph-ring"></div>',
12635
+ '<div class="concierge-persona-text"><strong>Sanctuary Fortress concierge</strong><small>read-only over fortress state</small></div>',
12636
+ '</div>',
12637
+ '<div class="concierge-meta">' + badge + '</div>',
12638
+ '</div>',
12639
+ errorBanner,
12640
+ '<div class="concierge-history" id="concierge-history">' + messagesHtml + '</div>',
12641
+ '<form class="concierge-composer" data-action="concierge-submit">',
12642
+ '<div class="input-wrap">',
12643
+ '<input type="text" name="concierge-input" placeholder="Type to Sanctuary. Enter to send." value="' + escHtml(c.composer) + '" data-action="concierge-input"' + sendDisabled + ' autocomplete="off">',
12644
+ '<span class="composer-meta">Enter</span>',
12645
+ '</div>',
12646
+ '<button type="submit" class="btn btn-primary" data-action="concierge-send"' + sendDisabled + '>' + escHtml(sendLabel) + '</button>',
12647
+ '</form>',
12648
+ '<p class="muted concierge-foot">First time? <a href="#intelligence">Pick a substrate</a> to enable concierge replies.</p>',
12564
12649
  '</div>',
12565
- errorBanner,
12566
- '<div class="concierge-history" id="concierge-history">' + messages + '</div>',
12567
- '<form class="concierge-composer" data-action="concierge-submit">',
12568
- '<input type="text" name="concierge-input" placeholder="Ask the concierge about this fortress..." value="' + escHtml(c.composer) + '" data-action="concierge-input"' + sendDisabled + ' autocomplete="off">',
12569
- '<button type="submit" class="btn btn-primary" data-action="concierge-send"' + sendDisabled + '>' + escHtml(sendLabel) + '</button>',
12570
- '</form>',
12571
- '<div class="concierge-chips">' + chips + '</div>',
12572
- '<p class="muted concierge-foot">First time? <a href="#intelligence">Pick a substrate</a> to enable concierge replies.</p>',
12573
12650
  '</div>'
12574
12651
  ].join("");
12575
12652
  }
12576
12653
 
12577
12654
  // ── Render: agents list / detail ───────────────────────────────────────
12655
+ //
12656
+ // Sprint Piece 2 PR 4 polish: empty state uses .agents-empty with the
12657
+ // concentric icon-frame + a terminal-block CTA. Populated state uses the
12658
+ // .agents-layout grid with the .agents-list 4-column table (Agent /
12659
+ // State / Attestation / Last seen). The empty-state branch keeps the
12660
+ // literal '<h1>Agents</h1>' start and the "No wrapped agents yet." copy
12661
+ // because agents-empty-state-canary.test.ts pins both.
12662
+ function agentInitials(agentId) {
12663
+ const tail = String(agentId || "").split(":").pop() || "";
12664
+ const cleaned = tail.replace(/[^a-zA-Z0-9]/g, "");
12665
+ return (cleaned.slice(0, 2) || "??").toUpperCase();
12666
+ }
12667
+ function agentStateClass(status) {
12668
+ if (status === "active") return "live";
12669
+ if (status === "locked_down" || status === "error") return "off";
12670
+ return "idle";
12671
+ }
12672
+ // Per-agent attestation badge (Q1 layer 2). Square chip beside each
12673
+ // agent: a bounded glyph beside a bounded entity. Color and fill pattern
12674
+ // carry meaning together so the badge reads even monochrome. The "locked"
12675
+ // status maps to the unverified visual (rust + hatched mark) since a
12676
+ // locked-down agent has no current attestation; the inspect-pane copy
12677
+ // explains the distinction. Pure visual surface; no state derivation.
12678
+ function renderAgentAttestationBadge(status) {
12679
+ let cls;
12680
+ let label;
12681
+ if (status === "active") { cls = "verified"; label = "verified"; }
12682
+ else if (status === "locked_down") { cls = "unverified"; label = "locked"; }
12683
+ else if (status === "error") { cls = "unverified"; label = "unverified"; }
12684
+ else { cls = "degraded"; label = "degraded"; }
12685
+ return '<span class="att-agent ' + cls + '" title="Agent attestation"><span class="mark"></span>' + escHtml(label) + '</span>';
12686
+ }
12687
+ // Per-action attestation tick (Q1 layer 3). Tiny inline shape on every
12688
+ // timeline row. Two-byte signature fragment is enough at low resolution;
12689
+ // the full signature is one click away. Neutral state shows a circle
12690
+ // instead of a tick when the signer was unreachable; the action is still
12691
+ // recorded. Visual surface only.
12692
+ function renderActionAttestationBadge(stateName, sig) {
12693
+ const valid = stateName === "verified" || stateName === "degraded" || stateName === "unverified" || stateName === "neutral";
12694
+ const cls = valid ? stateName : "neutral";
12695
+ const sigText = sig ? String(sig) : "--";
12696
+ return '<span class="att-action ' + cls + '" title="Action attestation">' +
12697
+ '<span class="tick"></span>' +
12698
+ '<span>' + escHtml(sigText) + '</span>' +
12699
+ '</span>';
12700
+ }
12701
+ // Attestation gallery surface (Q1 four classes: global / per-agent /
12702
+ // per-action / per-transaction custody-provenance stub). Reference for
12703
+ // operators: shows what each badge looks like across verified, degraded,
12704
+ // unverified, and (where applicable) pending or neutral states. Pure
12705
+ // visual; no derivation, no live data. Castle Layer 3 cooperative-MCP UX
12706
+ // surface; Castle Layer 1 enforcement ships in WP-V1.x-CASTLE-WALL.
12707
+ function renderAttestation() {
12708
+ return '<div class="att-gallery">' +
12709
+ '<div class="page-head"><div>' +
12710
+ '<p class="eyebrow">Attestation</p>' +
12711
+ '<h1>Four classes of badge.</h1>' +
12712
+ '<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>' +
12713
+ '</div></div>' +
12714
+ // Global
12715
+ '<div class="att-section">' +
12716
+ '<div class="att-section-head"><div>' +
12717
+ '<h2>Global. The fortress itself.</h2>' +
12718
+ '<p>Lives in the topbar. Visible on every surface. Tells you the fortress identity is currently signed and matches the binary you installed.</p>' +
12719
+ '</div><span class="label">topbar</span></div>' +
12720
+ attRow(renderTopbarAttestationBadge("verified"), "Verified", "Identity matches. Binary matches. Default state for a healthy fortress.") +
12721
+ attRow(renderTopbarAttestationBadge("degraded"), "Degraded", "The signature is older than the staleness window, or one of two co-signers is unreachable. The fortress keeps running.") +
12722
+ attRow(renderTopbarAttestationBadge("unverified"), "Unverified", "The signature did not validate. The surface still works; lockdown is still available; the badge tells you to investigate.") +
12723
+ attRow(renderTopbarAttestationBadge("pending"), "Pending", "First-run state. Fortress is signing for the first time. Settles in seconds.") +
12724
+ '</div>' +
12725
+ // Per-agent
12726
+ '<div class="att-section">' +
12727
+ '<div class="att-section-head"><div>' +
12728
+ '<h2>Per-agent. In the agents list and inspect pane.</h2>' +
12729
+ '<p>A square chip beside each agent. Square because an agent is bounded; the fortress (a circle) contains it.</p>' +
12730
+ '</div><span class="label">agents view</span></div>' +
12731
+ '<div class="att-row">' +
12732
+ '<div class="demo" style="display:flex; gap:8px; flex-wrap:wrap;">' +
12733
+ renderAgentAttestationBadge("active") +
12734
+ renderAgentAttestationBadgeForState("degraded", "degraded") +
12735
+ renderAgentAttestationBadgeForState("unverified", "unverified") +
12736
+ '</div>' +
12737
+ '<div class="desc"><strong>Verified, degraded, unverified</strong>' +
12738
+ '<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>' +
12739
+ '</div>' +
12740
+ '</div>' +
12741
+ '</div>' +
12742
+ // Per-action
12743
+ '<div class="att-section">' +
12744
+ '<div class="att-section-head"><div>' +
12745
+ '<h2>Per-action. Inline in the activity timeline.</h2>' +
12746
+ '<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>' +
12747
+ '</div><span class="label">timeline</span></div>' +
12748
+ '<div class="att-row">' +
12749
+ '<div class="demo" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">' +
12750
+ '<span style="font-size:13px; color:var(--ink-2);">14:22:08 doc-reviewer summarized intake.pdf</span>' +
12751
+ renderActionAttestationBadge("verified", "9c7d..2a") +
12752
+ '</div>' +
12753
+ '<div class="desc"><strong>Verified action</strong>' +
12754
+ '<small>The most common shape. Two-byte signature fragment is enough; the full signature is one click away.</small>' +
12755
+ '</div>' +
12756
+ '</div>' +
12757
+ '<div class="att-row">' +
12758
+ '<div class="demo" style="display:flex; gap:10px; align-items:center;">' +
12759
+ '<span style="font-size:13px; color:var(--ink-2);">14:11:47 privacy filter redacted payload</span>' +
12760
+ renderActionAttestationBadge("degraded", "b440..71") +
12761
+ '</div>' +
12762
+ '<div class="desc"><strong>Degraded action</strong>' +
12763
+ '<small>The action signed, but the signature class was less than the policy preferred. Useful when a substrate is still warming up.</small>' +
12764
+ '</div>' +
12765
+ '</div>' +
12766
+ '<div class="att-row">' +
12767
+ '<div class="demo" style="display:flex; gap:10px; align-items:center;">' +
12768
+ '<span style="font-size:13px; color:var(--ink-2);">14:09:02 agent attempted external link</span>' +
12769
+ renderActionAttestationBadge("neutral", "--") +
12770
+ '</div>' +
12771
+ '<div class="desc"><strong>Neutral. Degrade, not destroy.</strong>' +
12772
+ '<small>The signer was unreachable. Rather than hide the action, the badge becomes neutral and a tooltip explains. The action is still recorded.</small>' +
12773
+ '</div>' +
12774
+ '</div>' +
12775
+ '</div>' +
12776
+ // Custody stub
12777
+ '<div class="att-section">' +
12778
+ '<div class="att-section-head"><div>' +
12779
+ '<h2>Custody. Stub for v1.x.</h2>' +
12780
+ '<p>A fourth class, surfaced conservatively. Reserved for forthcoming custody-provenance signatures (x402 payment receipts, ERC-8004 identity assertions). Visible, dashed, clearly stubbed.</p>' +
12781
+ '</div><span class="label">stub</span></div>' +
12782
+ '<div class="att-row">' +
12783
+ '<div class="demo">' +
12784
+ '<span class="att-custody" title="Custody-provenance, v1.x">' +
12785
+ '<span class="seal-stub"></span>' +
12786
+ '<span class="stub-tag">custody. stub</span>' +
12787
+ '</span>' +
12788
+ '</div>' +
12789
+ '<div class="desc"><strong>Custody. Stub.</strong>' +
12790
+ '<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>' +
12791
+ '</div>' +
12792
+ '</div>' +
12793
+ '</div>' +
12794
+ // Tooltip
12795
+ '<div class="att-section">' +
12796
+ '<div class="att-section-head"><div>' +
12797
+ '<h2>Tooltip on failure.</h2>' +
12798
+ '<p>A failed badge is never silent. The tooltip explains in plain language, suggests one action, and confirms the surface is still working.</p>' +
12799
+ '</div><span class="label">degrade not destroy</span></div>' +
12800
+ '<div class="att-row">' +
12801
+ '<div class="demo">' +
12802
+ '<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>' +
12803
+ '</div>' +
12804
+ '<div class="desc"><strong>Plain-language tooltip</strong>' +
12805
+ '<small>Three lines, in order: what happened, what did not break, what to do. No jargon, no stack trace.</small>' +
12806
+ '</div>' +
12807
+ '</div>' +
12808
+ '</div>' +
12809
+ '</div>';
12810
+ }
12811
+ function attRow(demoHtml, strong, smallText) {
12812
+ return '<div class="att-row">' +
12813
+ '<div class="demo">' + demoHtml + '</div>' +
12814
+ '<div class="desc"><strong>' + escHtml(strong) + '</strong>' +
12815
+ '<small>' + escHtml(smallText) + '</small>' +
12816
+ '</div>' +
12817
+ '</div>';
12818
+ }
12819
+ // Gallery-only variant: render a per-agent badge for a given visual state
12820
+ // (verified / degraded / unverified) without going through the agent
12821
+ // status mapping. Used by renderAttestation to show all three states
12822
+ // side by side as design reference.
12823
+ function renderAgentAttestationBadgeForState(cls, label) {
12824
+ return '<span class="att-agent ' + escHtml(cls) + '" title="Agent attestation"><span class="mark"></span>' + escHtml(label) + '</span>';
12825
+ }
12826
+ function relTimeFromIso(iso) {
12827
+ if (!iso) return "";
12828
+ const d = new Date(iso);
12829
+ if (isNaN(d.getTime())) return iso;
12830
+ const diffMs = Date.now() - d.getTime();
12831
+ const diffSec = Math.max(0, Math.floor(diffMs / 1000));
12832
+ if (diffSec < 60) return diffSec + "s ago";
12833
+ const diffMin = Math.floor(diffSec / 60);
12834
+ if (diffMin < 60) return diffMin + "m ago";
12835
+ const diffHr = Math.floor(diffMin / 60);
12836
+ if (diffHr < 24) return diffHr + "h ago";
12837
+ const diffDay = Math.floor(diffHr / 24);
12838
+ return diffDay + "d ago";
12839
+ }
12578
12840
  function renderAgentsList() {
12579
- 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>';
12841
+ if (!state.agents.length) return '<h1>Agents</h1>' +
12842
+ '<div class="agents-empty">' +
12843
+ '<div class="icon-frame"><div class="core"></div></div>' +
12844
+ '<h2>No wrapped agents yet.</h2>' +
12845
+ '<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>' +
12846
+ '<div class="terminal-block"><span class="cmd"><span class="prompt">$</span>sanctuary wrap</span></div>' +
12847
+ '</div>';
12848
+ const count = state.agents.length;
12849
+ const subCopy = count + ' wrapped. Click one to inspect its activity, policy, and pending approvals.';
12580
12850
  const rows = state.agents.map(function (a) {
12581
12851
  const map = STATUS_MAP[a.status] || STATUS_MAP.unknown;
12582
- const reason = a.status_reason_class ? (REASON_LABELS[a.status_reason_class] || "") : "";
12583
- return '<div class="row">' +
12584
- '<span class="glyph ' + map.glyph + '"></span>' +
12585
- '<div class="grow"><strong>' + escHtml(a.agent_id) + '</strong> <span class="muted mono">' + escHtml(a.harness) + '</span></div>' +
12586
- '<span class="pill" title="' + escHtml(reason) + '">' + escHtml(map.label) + '</span>' +
12587
- '<button class="btn" data-action="open-agent" data-agent-id="' + escHtml(a.agent_id) + '">Open</button>' +
12852
+ const dotCls = agentStateClass(a.status);
12853
+ const initials = agentInitials(a.agent_id);
12854
+ const role = escHtml(a.harness) + (a.model_provider && a.model_provider.model_id ? ' · ' + escHtml(a.model_provider.model_id) : '');
12855
+ const isSelected = state.selectedAgentId === a.agent_id;
12856
+ 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) + '">' +
12857
+ '<div class="agent-identity">' +
12858
+ '<div class="agent-glyph">' + escHtml(initials) + '</div>' +
12859
+ '<div class="agent-name">' +
12860
+ '<strong>' + escHtml(a.agent_id) + '</strong>' +
12861
+ '<small>' + role + '</small>' +
12862
+ '</div>' +
12863
+ '</div>' +
12864
+ '<span class="agent-state">' +
12865
+ '<span class="state-dot ' + dotCls + '"></span>' +
12866
+ escHtml(map.label) +
12867
+ '</span>' +
12868
+ renderAgentAttestationBadge(a.status) +
12869
+ '<span class="agent-last">' + escHtml(relTimeFromIso(a.last_activity_at)) + '</span>' +
12588
12870
  '</div>';
12589
12871
  }).join("\n");
12590
- return '<h1>Agents</h1><div class="card">' + rows + '</div>';
12872
+ return '<div class="agents-wrap">' +
12873
+ '<div class="page-head">' +
12874
+ '<div>' +
12875
+ '<p class="eyebrow">Agents</p>' +
12876
+ '<h1>Agents.</h1>' +
12877
+ '<p class="sub">' + escHtml(subCopy) + '</p>' +
12878
+ '</div>' +
12879
+ '</div>' +
12880
+ '<div class="agents-layout">' +
12881
+ '<div class="agents-list">' +
12882
+ '<div class="agents-list-head">' +
12883
+ '<span>Agent</span><span>State</span><span>Attestation</span><span>Last seen</span>' +
12884
+ '</div>' +
12885
+ rows +
12886
+ '</div>' +
12887
+ '</div>' +
12888
+ '</div>';
12591
12889
  }
12592
12890
 
12593
12891
  function renderAgentDetail() {
@@ -12598,7 +12896,10 @@ function renderAgentDetail() {
12598
12896
  const timeline = events.length
12599
12897
  ? events.map(function (e) {
12600
12898
  const t = renderTemplate(e.display_template_id, e.display_template_args);
12601
- return '<div class="row"><span class="muted">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + '</span></div>';
12899
+ const badgeHtml = e.attestation
12900
+ ? ' ' + renderActionAttestationBadge(e.attestation.state, e.attestation.fragment)
12901
+ : '';
12902
+ return '<div class="row"><span class="muted">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + badgeHtml + '</span></div>';
12602
12903
  }).join("\n")
12603
12904
  : '<p class="muted">No activity yet.</p>';
12604
12905
  // WP-V1.2 reshape click-to-inspect surface. Clicking "Open inspect
@@ -12642,53 +12943,99 @@ function renderAgentInspectPanel(agent) {
12642
12943
  : "";
12643
12944
 
12644
12945
  // State 2: panel loaded.
12946
+ // Sprint Piece 2 PR 4 polish: outer wrapper combines .card with
12947
+ // .inspect-pane (sticky right rail, internal scroll, sectioned body).
12948
+ // The .card class is preserved so the rendered surface keeps its
12949
+ // shared card chrome; .inspect-pane overrides .card padding so the
12950
+ // inspect-head and inspect-body control their own spacing per design.
12645
12951
  if (panel) {
12952
+ const dotCls = agentStateClass(agent.status);
12953
+ const stateMap = STATUS_MAP[agent.status] || STATUS_MAP.unknown;
12646
12954
  const activity = (panel.recent_activity || []).slice(0, 20);
12647
12955
  const activityHtml = activity.length
12648
- ? activity.map(function (e) {
12956
+ ? '<div class="timeline">' +
12957
+ activity.map(function (e) {
12649
12958
  const t = renderTemplate(e.display_template_id, e.display_template_args);
12650
- return '<div class="row"><span class="muted mono">' + escHtml(shortTime(e.emitted_at)) + '</span><span>' + escHtml(t) + '</span></div>';
12651
- }).join("\n")
12959
+ const badgeHtml = e.attestation
12960
+ ? renderActionAttestationBadge(e.attestation.state, e.attestation.fragment)
12961
+ : '';
12962
+ return '<div class="timeline-item ok">' +
12963
+ '<div class="ts">' + escHtml(shortTime(e.emitted_at)) + '</div>' +
12964
+ '<div class="what">' + escHtml(t) + '</div>' +
12965
+ (badgeHtml ? '<div class="att">' + badgeHtml + '</div>' : '') +
12966
+ '</div>';
12967
+ }).join("") +
12968
+ '</div>'
12652
12969
  : '<p class="muted">No recent activity for this agent.</p>';
12653
12970
 
12654
12971
  const approvals = panel.pending_approvals || [];
12655
12972
  const approvalsHtml = approvals.length
12656
12973
  ? approvals.map(function (item) {
12657
12974
  const promptText = renderTemplate(item.display_template_id, item.display_template_args);
12658
- return '<div class="row">' +
12659
- '<span class="pill tone-info">' + escHtml(item.tier || "tier1") + '</span>' +
12660
- '<div class="grow">' + escHtml(promptText) + '</div>' +
12661
- '<button class="btn btn-primary" data-action="inbox-approve" data-item-id="' + escHtml(item.item_id) + '">Approve</button>' +
12662
- '<button class="btn" data-action="inbox-deny" data-item-id="' + escHtml(item.item_id) + '">Deny</button>' +
12975
+ return '<div class="approval-row">' +
12976
+ '<div class="what">' +
12977
+ '<span class="pill tone-degraded">' + escHtml(item.tier || "tier1") + '</span>' +
12978
+ escHtml(promptText) +
12979
+ '</div>' +
12980
+ '<div class="actions">' +
12981
+ '<button class="btn" data-action="inbox-deny" data-item-id="' + escHtml(item.item_id) + '">Deny</button>' +
12982
+ '<button class="btn btn-primary" data-action="inbox-approve" data-item-id="' + escHtml(item.item_id) + '">Approve once</button>' +
12983
+ '</div>' +
12663
12984
  '</div>';
12664
- }).join("\n")
12985
+ }).join("")
12665
12986
  : '<p class="muted">No pending approvals routed through this agent.</p>';
12666
12987
 
12667
- const policyLine = panel.policy_summary
12668
- ? '<dt>Policy</dt><dd class="mono">' + escHtml(panel.policy_summary.display_label || panel.policy_summary.policy_id) + '</dd>' +
12988
+ const policySection = panel.policy_summary
12989
+ ? '<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>' +
12669
12990
  (panel.policy_summary.channel_template_id
12670
- ? '<dt>Template</dt><dd class="mono">' + escHtml(panel.policy_summary.channel_template_id) + '</dd>'
12991
+ ? '<div class="policy-line"><span class="k">Template</span><span class="v">' + escHtml(panel.policy_summary.channel_template_id) + '</span></div>'
12671
12992
  : '') +
12672
- '<dt>Bound</dt><dd class="mono">' + escHtml(shortTime(panel.policy_summary.bound_at)) + '</dd>'
12673
- : '<dt>Policy</dt><dd class="muted">No bound policy yet.</dd>';
12674
-
12675
- return '<div class="card">' +
12676
- '<div class="concierge-header">' +
12677
- '<div class="concierge-persona"><strong>Inspect ' + escHtml(agent.agent_id) + '</strong> ' +
12678
- '<span class="muted">opened ' + escHtml(shortTime(panel.opened_at)) + '</span></div>' +
12679
- '<button class="btn" data-action="agent-inspect-open" data-agent-id="' + escHtml(agent.agent_id) + '">Refresh</button>' +
12993
+ '<div class="policy-line"><span class="k">Bound</span><span class="v">' + escHtml(shortTime(panel.policy_summary.bound_at)) + '</span></div>'
12994
+ : '<div class="policy-line"><span class="k">Policy</span><span class="v">No bound policy yet.</span></div>';
12995
+
12996
+ const modelLine = agent.model_provider
12997
+ ? '<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>'
12998
+ : '';
12999
+
13000
+ return '<div class="card inspect-pane">' +
13001
+ '<div class="inspect-head">' +
13002
+ '<div class="row1">' +
13003
+ '<div class="agent-glyph">' + escHtml(agentInitials(agent.agent_id)) + '</div>' +
13004
+ '<h3>' + escHtml(agent.agent_id) + '</h3>' +
13005
+ '<span style="margin-left:auto;">' + renderAgentAttestationBadge(agent.status) + '</span>' +
13006
+ '</div>' +
13007
+ '<div class="meta">' +
13008
+ '<span class="pill ' + (dotCls === "live" ? "tone-verified" : "tone-degraded") + '"><span class="state-dot ' + dotCls + '" style="margin-right:4px;"></span>' + escHtml(stateMap.label) + '</span>' +
13009
+ '<span class="pill">opened ' + escHtml(shortTime(panel.opened_at)) + '</span>' +
13010
+ '<button class="btn btn-quiet" data-action="agent-inspect-open" data-agent-id="' + escHtml(agent.agent_id) + '" title="Refresh inspect panel">Refresh</button>' +
13011
+ '</div>' +
13012
+ '</div>' +
13013
+ '<div class="inspect-body">' +
13014
+ errorBanner +
13015
+ '<div class="inspect-section">' +
13016
+ '<h4>Pending approvals' + (approvals.length ? ' <span class="count">' + approvals.length + '</span>' : '') + '</h4>' +
13017
+ approvalsHtml +
13018
+ '</div>' +
13019
+ '<div class="inspect-section">' +
13020
+ '<h4>Recent activity</h4>' +
13021
+ activityHtml +
13022
+ '</div>' +
13023
+ '<div class="inspect-section">' +
13024
+ '<h4>Policy summary</h4>' +
13025
+ policySection +
13026
+ '</div>' +
13027
+ '<div class="inspect-section">' +
13028
+ '<h4>Identity</h4>' +
13029
+ '<div class="policy-line"><span class="k">Agent id</span><span class="v">' + escHtml(agent.agent_id) + '</span></div>' +
13030
+ '<div class="policy-line"><span class="k">Harness</span><span class="v">' + escHtml(agent.harness) + '</span></div>' +
13031
+ modelLine +
13032
+ '<div class="policy-line"><span class="k">Wrapped at</span><span class="v">' + escHtml(shortTime(agent.wrapped_at)) + '</span></div>' +
13033
+ '</div>' +
13034
+ '<p class="muted" style="margin-top:10px;font-size:12px;">' +
13035
+ '<a href="#activity?agent=' + escHtml(agent.agent_id) + '">View full activity</a> · ' +
13036
+ '<a href="#policy">Edit policy</a>' +
13037
+ '</p>' +
12680
13038
  '</div>' +
12681
- errorBanner +
12682
- '<h3>Pending approvals</h3>' +
12683
- approvalsHtml +
12684
- '<h3 style="margin-top:14px;">Recent activity</h3>' +
12685
- activityHtml +
12686
- '<h3 style="margin-top:14px;">Policy</h3>' +
12687
- '<dl class="kv">' + policyLine + '</dl>' +
12688
- '<p class="muted" style="margin-top:10px;font-size:12px;">' +
12689
- '<a href="#activity?agent=' + escHtml(agent.agent_id) + '">View full activity</a> · ' +
12690
- '<a href="#policy">Edit policy</a>' +
12691
- '</p>' +
12692
13039
  '</div>';
12693
13040
  }
12694
13041
 
@@ -12837,6 +13184,16 @@ function statusDotClass(status) {
12837
13184
  return "red";
12838
13185
  }
12839
13186
 
13187
+ // Card-grid polish (Sprint Piece 2 PR 3) maps the badge dot class onto
13188
+ // the shaped glyph token. Sage circle for ok, ochre triangle for warn,
13189
+ // rust diamond for fail. Keep aligned with .status-glyph rules in
13190
+ // html.ts and the .intel-card-status modifier classes.
13191
+ function statusGlyphClass(dotClass) {
13192
+ if (dotClass === "green") return "ok";
13193
+ if (dotClass === "yellow") return "warn";
13194
+ return "fail";
13195
+ }
13196
+
12840
13197
  function statusLabel(health) {
12841
13198
  if (health === "ok") return "Working";
12842
13199
  if (health === "degraded") return "Degraded";
@@ -12876,13 +13233,18 @@ function renderIntelligenceCenter() {
12876
13233
  }
12877
13234
  const status = state.intelligence.status;
12878
13235
  const config = state.intelligence.config || {};
12879
- const surfaceRows = SURFACES_ORDER.map(function (surfaceId) {
13236
+ const surfaceCards = SURFACES_ORDER.map(function (surfaceId) {
12880
13237
  const surfaceStatus = (status.surfaces || []).find(function (s) { return s.surface === surfaceId; });
12881
13238
  if (!surfaceStatus) {
12882
- return '<div class="intel-row"><div class="intel-row-name">' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) +
12883
- '<small>' + escHtml(surfaceId) + '</small></div>' +
12884
- '<div class="intel-row-body muted">No status reported.</div>' +
12885
- '<div></div></div>';
13239
+ return '<div class="intel-row intel-card" data-intel-surface="' + escHtml(surfaceId) + '">' +
13240
+ '<div class="intel-card-head">' +
13241
+ '<div class="intel-card-name">' +
13242
+ '<strong>' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) + '</strong>' +
13243
+ '<small>' + escHtml(surfaceId) + '</small>' +
13244
+ '</div>' +
13245
+ '</div>' +
13246
+ '<div class="muted">No status reported.</div>' +
13247
+ '</div>';
12886
13248
  }
12887
13249
  const substrate = surfaceStatus.chosen;
12888
13250
  const localPick = (config.local_model_picks || {})[surfaceId];
@@ -12900,41 +13262,62 @@ function renderIntelligenceCenter() {
12900
13262
  if (provider) currentBadge = currentBadge + " (" + (FRONTIER_PROVIDER_LABELS[provider] || provider) + ")";
12901
13263
  }
12902
13264
  const dotClass = statusDotClass((surfaceStatus.badge || {}).status || "red");
13265
+ const glyphClass = statusGlyphClass(dotClass);
12903
13266
  const failures = surfaceStatus.recentFailures || [];
12904
13267
  const expanded = !!state.intelligence.expandedFailures[surfaceId];
12905
- let failuresBlock = "";
13268
+
13269
+ // Card foot. The failures toggle is the load-bearing affordance for
13270
+ // the rc.6 ZZ test (button[data-action="intel-failures-toggle"] with
13271
+ // text "recent failures (N)"). Pluralization is "failures" regardless
13272
+ // of N for backward compatibility with the seeded test contract.
13273
+ // Surface zero-failure state as a quiet mono note so the card still
13274
+ // has visual rhythm in its foot row.
13275
+ let footHtml;
12906
13276
  if (failures.length > 0) {
12907
13277
  const toggleLabel = (expanded ? "Hide" : "View") + " recent failures (" + failures.length + ")";
12908
- const list = expanded
12909
- ? '<ul class="intel-row-failures-list">' +
12910
- failures.slice().reverse().map(function (f) {
12911
- return '<li><span class="muted mono">' + escHtml(shortTime(f.ts)) + '</span> ' +
12912
- '<span class="pill warn">' + escHtml(f.failureClass) + '</span> ' +
12913
- '<span class="muted">' + escHtml(f.snippet) + '</span></li>';
12914
- }).join("") +
12915
- '</ul>'
12916
- : '';
12917
- failuresBlock =
12918
- '<div class="intel-row-failures">' +
12919
- '<button class="btn btn-link" data-action="intel-failures-toggle" data-intel-surface="' + escHtml(surfaceId) + '">' +
12920
- escHtml(toggleLabel) +
12921
- '</button>' +
12922
- list +
13278
+ footHtml =
13279
+ '<button class="intel-failures-toggle' + (expanded ? ' open' : '') + '" data-action="intel-failures-toggle" data-intel-surface="' + escHtml(surfaceId) + '">' +
13280
+ '<span class="caret"></span>' +
13281
+ escHtml(toggleLabel) +
13282
+ '</button>';
13283
+ } else {
13284
+ footHtml = '<span class="muted mono" style="font-size: 11px;">no recent failures</span>';
13285
+ }
13286
+
13287
+ let failuresBlock = "";
13288
+ if (failures.length > 0 && expanded) {
13289
+ const rows = failures.slice().reverse().map(function (f) {
13290
+ return '<div class="intel-failure-row">' +
13291
+ '<span class="ts">' + escHtml(shortTime(f.ts)) + '</span>' +
13292
+ '<div>' +
13293
+ '<div class="err-class">' + escHtml(f.failureClass) + '</div>' +
13294
+ '<div>' + escHtml(f.snippet) + '</div>' +
13295
+ '</div>' +
12923
13296
  '</div>';
13297
+ }).join("");
13298
+ failuresBlock = '<div class="intel-failures">' + rows + '</div>';
12924
13299
  }
12925
- return '<div class="intel-row" data-intel-surface="' + escHtml(surfaceId) + '">' +
12926
- '<div class="intel-row-name">' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) +
12927
- '<small>' + escHtml(surfaceId) + '</small></div>' +
12928
- '<div class="intel-row-body">' +
12929
- '<div class="intel-row-current">' +
12930
- '<span class="intel-status-dot ' + dotClass + '" title="' + escHtml(statusLabel(surfaceStatus.health)) + '"></span>' +
12931
- '<span class="pill">' + escHtml(currentBadge) + '</span>' +
12932
- '<span class="muted mono">' + escHtml(statusLabel(surfaceStatus.health)) + '</span>' +
13300
+
13301
+ return '<div class="intel-row intel-card" data-intel-surface="' + escHtml(surfaceId) + '">' +
13302
+ '<div class="intel-card-head">' +
13303
+ '<div class="intel-card-name">' +
13304
+ '<strong>' + escHtml(SURFACE_LABELS[surfaceId] || surfaceId) + '</strong>' +
13305
+ '<small>' + escHtml(surfaceId) + '</small>' +
12933
13306
  '</div>' +
12934
- '<div class="intel-row-tradeoff">' + escHtml(substrateTradeoff(substrate)) + '</div>' +
12935
- failuresBlock +
13307
+ '<span class="intel-card-status ' + glyphClass + '" title="' + escHtml(statusLabel(surfaceStatus.health)) + '">' +
13308
+ '<span class="status-glyph ' + glyphClass + '"></span>' +
13309
+ escHtml(statusLabel(surfaceStatus.health)) +
13310
+ '</span>' +
12936
13311
  '</div>' +
12937
- '<div><button class="btn" data-action="intel-picker-open" data-intel-surface="' + escHtml(surfaceId) + '">Change</button></div>' +
13312
+ '<div class="intel-substrate">' +
13313
+ '<div class="sub-line primary">' +
13314
+ '<span>' + escHtml(currentBadge) + '</span>' +
13315
+ '<button class="btn-quiet" data-action="intel-picker-open" data-intel-surface="' + escHtml(surfaceId) + '">Change</button>' +
13316
+ '</div>' +
13317
+ '</div>' +
13318
+ '<div class="intel-row-tradeoff">' + escHtml(substrateTradeoff(substrate)) + '</div>' +
13319
+ '<div class="intel-card-foot">' + footHtml + '</div>' +
13320
+ failuresBlock +
12938
13321
  '</div>';
12939
13322
  }).join("\n");
12940
13323
 
@@ -12946,11 +13329,15 @@ function renderIntelligenceCenter() {
12946
13329
 
12947
13330
  const modal = state.intelligence.picker.open ? renderIntelligencePicker() : "";
12948
13331
 
12949
- return '<section class="intel-center">' +
12950
- '<p class="eyebrow">INTELLIGENCE</p>' +
12951
- '<h1>Intelligence Substrate</h1>' +
12952
- '<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>' +
12953
- '<section class="intel-panel"><h2>Surfaces</h2>' + surfaceRows + '</section>' +
13332
+ return '<section class="intel-wrap">' +
13333
+ '<div class="page-head">' +
13334
+ '<div>' +
13335
+ '<p class="eyebrow">Intelligence</p>' +
13336
+ '<h1>Substrate routing.</h1>' +
13337
+ '<p class="sub">Six surfaces, six choices. Each surface picks where its thinking happens. Local for privacy. Hosted for capability. Hybrid for both.</p>' +
13338
+ '</div>' +
13339
+ '</div>' +
13340
+ '<div class="intel-grid">' + surfaceCards + '</div>' +
12954
13341
  '<section class="intel-panel"><h2>Host capability</h2>' +
12955
13342
  '<dl class="intel-hardware">' +
12956
13343
  '<dt>Total RAM</dt><dd>' + escHtml(hardware.totalRamGb || "?") + ' GB</dd>' +
@@ -13743,6 +14130,13 @@ document.addEventListener("click", function (ev) {
13743
14130
  const intelLocalModel = tgt.getAttribute("data-intel-local-model");
13744
14131
  const intelFrontierProvider = tgt.getAttribute("data-intel-frontier-provider");
13745
14132
  if (action === "lockdown") return void onLockdownClick();
14133
+ if (action === "theme-toggle") {
14134
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
14135
+ const next = isDark ? "light" : "dark";
14136
+ sessionStorage.setItem(THEME_KEY, next);
14137
+ applyTheme(next);
14138
+ return;
14139
+ }
13746
14140
  if (action === "intel-reload") { return void fetchIntelligenceState().then(rerender); }
13747
14141
  if (action === "intel-picker-open" && intelSurface) return void onIntelPickerOpen(intelSurface);
13748
14142
  if (action === "intel-picker-close") return onIntelPickerClose();
@@ -13896,12 +14290,30 @@ document.addEventListener("input", function (ev) {
13896
14290
  // then the dashboard view renders a static welcome card with no form
13897
14291
  // inputs that could be confused for a working command surface.
13898
14292
 
13899
- // Theme: system preference only at v1.1.
14293
+ // Theme: explicit operator preference (sessionStorage) overrides system
14294
+ // pref. The toggle button in the topbar dispatches data-action
14295
+ // "theme-toggle" which writes the chosen theme and updates the
14296
+ // [data-theme] attribute. When no explicit choice exists, fall back to
14297
+ // system preference and track changes so dark-mode-at-sunset behavior
14298
+ // keeps working on macOS / Windows.
14299
+ const THEME_KEY = "sanctuary-v11-theme";
14300
+ function applyTheme(theme) {
14301
+ if (theme === "dark") document.documentElement.setAttribute("data-theme", "dark");
14302
+ else document.documentElement.removeAttribute("data-theme");
14303
+ }
14304
+ const explicitTheme = sessionStorage.getItem(THEME_KEY);
13900
14305
  const mq = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
13901
- if (mq && mq.matches) document.documentElement.setAttribute("data-theme", "dark");
14306
+ if (explicitTheme === "dark" || explicitTheme === "light") {
14307
+ applyTheme(explicitTheme);
14308
+ } else if (mq && mq.matches) {
14309
+ applyTheme("dark");
14310
+ }
13902
14311
  if (mq) mq.addEventListener("change", function (e) {
13903
- if (e.matches) document.documentElement.setAttribute("data-theme", "dark");
13904
- else document.documentElement.removeAttribute("data-theme");
14312
+ // Only honor system pref changes when the operator has not made an
14313
+ // explicit choice. Once they toggle, the choice sticks for the
14314
+ // session.
14315
+ if (sessionStorage.getItem(THEME_KEY)) return;
14316
+ applyTheme(e.matches ? "dark" : "light");
13905
14317
  });
13906
14318
 
13907
14319
  // Boot.
@@ -13937,6 +14349,26 @@ var STYLES = String.raw`:root {
13937
14349
  --rad: 6px;
13938
14350
  --rad-lg: 10px;
13939
14351
  --shadow: 0 1px 2px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.02);
14352
+ /* Type scale. Names are size-relative, not semantic, so refactors do
14353
+ not need to invent new names. Existing rules used these literal px
14354
+ values; tokens make future polish a one-line change. */
14355
+ --text-xs: 11px;
14356
+ --text-sm: 12px;
14357
+ --text-base: 13px;
14358
+ --text-md: 14px;
14359
+ --text-lg: 16px;
14360
+ --text-xl: 22px;
14361
+ --text-display: 36px;
14362
+ /* Spacing scale (4px multiples). Layout-specific magic numbers
14363
+ (220px sidebar, 360px fortress rail, concierge-card heights) stay
14364
+ as literals because the token system is for component padding /
14365
+ margin / gap, not grid track sizing. */
14366
+ --space-1: 4px;
14367
+ --space-2: 8px;
14368
+ --space-3: 12px;
14369
+ --space-4: 16px;
14370
+ --space-5: 24px;
14371
+ --space-6: 32px;
13940
14372
  }
13941
14373
  [data-theme="dark"] {
13942
14374
  --paper: #121210;
@@ -13963,7 +14395,7 @@ var STYLES = String.raw`:root {
13963
14395
  html, body { margin: 0; padding: 0; }
13964
14396
  body {
13965
14397
  font-family: var(--sans);
13966
- font-size: 14px;
14398
+ font-size: var(--text-md);
13967
14399
  background: var(--paper);
13968
14400
  color: var(--ink);
13969
14401
  line-height: 1.45;
@@ -13984,20 +14416,27 @@ body {
13984
14416
  "sidebar main";
13985
14417
  }
13986
14418
  .sidebar { grid-area: sidebar; background: var(--paper-2); border-right: 1px solid var(--rule); padding: 12px 8px; }
13987
- .sidebar h1 { font-family: var(--serif); font-size: 16px; margin: 4px 8px 16px; }
14419
+ .sidebar h1 { font-family: var(--serif); font-size: var(--text-lg); margin: 4px 8px 16px; }
13988
14420
  .sidebar nav { display: flex; flex-direction: column; gap: 2px; }
13989
14421
  .sidebar nav a {
13990
- display: block; padding: 6px 10px; border-radius: var(--rad);
13991
- color: var(--ink-2); text-decoration: none; font-size: 13px;
14422
+ display: flex; align-items: center; gap: var(--space-2);
14423
+ padding: 6px 10px; border-radius: var(--rad);
14424
+ color: var(--ink-2); text-decoration: none; font-size: var(--text-base);
14425
+ }
14426
+ .sidebar nav a svg {
14427
+ flex-shrink: 0; width: 16px; height: 16px;
14428
+ color: var(--ink-3);
13992
14429
  }
13993
14430
  .sidebar nav a:hover { background: var(--paper-3); }
14431
+ .sidebar nav a:hover svg { color: var(--ink-2); }
13994
14432
  .sidebar nav a.active { background: var(--surface); color: var(--ink); border: 1px solid var(--rule); }
13995
- .topbar { grid-area: topbar; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--rule); background: var(--surface); }
13996
- .topbar .brand { font-family: var(--serif); font-size: 14px; }
14433
+ .sidebar nav a.active svg { color: var(--ink); }
14434
+ .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); }
14435
+ .topbar .brand { font-family: var(--serif); font-size: var(--text-md); }
13997
14436
  .topbar .pills { display: flex; gap: 6px; flex: 1; }
13998
14437
  .pill {
13999
14438
  display: inline-flex; align-items: center; gap: 4px;
14000
- padding: 2px 8px; border-radius: 12px; font-size: 11px;
14439
+ padding: 2px 8px; border-radius: 12px; font-size: var(--text-xs);
14001
14440
  font-family: var(--mono); border: 1px solid var(--rule);
14002
14441
  background: var(--surface-2); color: var(--ink-2);
14003
14442
  }
@@ -14009,7 +14448,7 @@ body {
14009
14448
  display: inline-flex; align-items: center; gap: 4px;
14010
14449
  padding: 4px 10px; border-radius: var(--rad);
14011
14450
  background: var(--surface); border: 1px solid var(--rule);
14012
- font-family: var(--sans); font-size: 12px; color: var(--ink);
14451
+ font-family: var(--sans); font-size: var(--text-sm); color: var(--ink);
14013
14452
  cursor: pointer;
14014
14453
  }
14015
14454
  .btn:hover:not(:disabled) { background: var(--surface-2); }
@@ -14019,21 +14458,30 @@ body {
14019
14458
  .btn.btn-danger { background: var(--rust-bg); color: var(--rust); border-color: var(--rust); }
14020
14459
  .btn.tier1-pending { background: var(--ochre-bg); color: var(--ochre); border-color: var(--ochre); }
14021
14460
  .btn.tier1-engaged { background: var(--rust-bg); color: var(--rust); border-color: var(--rust); }
14461
+ .btn.btn-icon {
14462
+ padding: 4px 6px;
14463
+ display: inline-flex; align-items: center; justify-content: center;
14464
+ color: var(--ink-2);
14465
+ }
14466
+ .btn.btn-icon svg { width: 16px; height: 16px; }
14467
+ .btn.btn-icon .icon-sun { display: none; }
14468
+ [data-theme="dark"] .btn.btn-icon .icon-moon { display: none; }
14469
+ [data-theme="dark"] .btn.btn-icon .icon-sun { display: inline; }
14022
14470
  .main { grid-area: main; overflow-y: auto; padding: 16px 24px; }
14023
- .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; }
14471
+ .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; }
14024
14472
  .app.route-full .fortress { display: none; }
14025
14473
  .card {
14026
14474
  background: var(--surface); border: 1px solid var(--rule);
14027
- border-radius: var(--rad); padding: 12px;
14475
+ border-radius: var(--rad); padding: var(--space-3);
14028
14476
  }
14029
- .card h3 { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: var(--ink); }
14477
+ .card h3 { margin: 0 0 8px; font-size: var(--text-base); font-weight: 600; color: var(--ink); }
14030
14478
  .muted { color: var(--ink-3); }
14031
- .mono { font-family: var(--mono); font-size: 12px; }
14032
- .row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px dashed var(--rule); }
14479
+ .mono { font-family: var(--mono); font-size: var(--text-sm); }
14480
+ .row { display: flex; align-items: center; gap: var(--space-2); padding: 6px 0; border-bottom: 1px dashed var(--rule); }
14033
14481
  .row:last-child { border-bottom: 0; }
14034
14482
  .row .grow { flex: 1; min-width: 0; }
14035
14483
  .agent-row { flex-direction: column; align-items: stretch; gap: 6px; }
14036
- .agent-row-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
14484
+ .agent-row-head { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
14037
14485
  .agent-row-head .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
14038
14486
  .agent-row-actions { display: flex; flex-wrap: wrap; gap: 4px; }
14039
14487
  /* Click-to-inspect affordance: the head sub-row of a fortress-column
@@ -14043,7 +14491,7 @@ body {
14043
14491
  .agent-row-head[data-action="agent-row-inspect-open"] { cursor: pointer; border-radius: var(--rad); padding: 4px 6px; margin: -4px -6px; }
14044
14492
  .agent-row-head[data-action="agent-row-inspect-open"]:hover { background: var(--paper-3); }
14045
14493
  .agent-row-head[data-action="agent-row-inspect-open"]:focus-visible { outline: 2px solid var(--ink-3); outline-offset: 1px; }
14046
- .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 12px; }
14494
+ .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: var(--text-sm); }
14047
14495
  .kv dt { color: var(--ink-3); }
14048
14496
  .kv dd { margin: 0; color: var(--ink); }
14049
14497
  .glyph { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--ink-4); }
@@ -14054,74 +14502,74 @@ body {
14054
14502
  .toast {
14055
14503
  position: fixed; bottom: 16px; right: 16px;
14056
14504
  background: var(--ink); color: var(--paper); padding: 8px 12px;
14057
- border-radius: var(--rad); font-size: 12px; z-index: 1000;
14505
+ border-radius: var(--rad); font-size: var(--text-sm); z-index: 1000;
14058
14506
  max-width: 360px;
14059
14507
  }
14060
14508
  .toast.error { background: var(--rust); color: var(--paper); }
14061
- .layer-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: 8px; }
14062
- .layer-card h4 { margin: 0 0 4px; font-size: 12px; font-weight: 600; }
14063
- .layer-card p { margin: 0; font-size: 11px; color: var(--ink-3); }
14064
- .chat-thread { display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; }
14509
+ .layer-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: var(--space-2); }
14510
+ .layer-card h4 { margin: 0 0 4px; font-size: var(--text-sm); font-weight: 600; }
14511
+ .layer-card p { margin: 0; font-size: var(--text-xs); color: var(--ink-3); }
14512
+ .chat-thread { display: flex; flex-direction: column; gap: var(--space-2); padding-bottom: 12px; }
14065
14513
  .chat-msg { padding: 8px 10px; border-radius: var(--rad); border: 1px solid var(--rule); background: var(--surface); max-width: 78%; }
14066
- .chat-msg.system { background: var(--paper-3); color: var(--ink-3); font-size: 12px; max-width: 100%; }
14514
+ .chat-msg.system { background: var(--paper-3); color: var(--ink-3); font-size: var(--text-sm); max-width: 100%; }
14067
14515
  .chat-msg.agent { align-self: flex-start; }
14068
14516
  .chat-msg.operator { align-self: flex-end; background: var(--ink); color: var(--paper); }
14069
14517
  .chat-msg .meta { font-size: 10px; color: var(--ink-4); margin-top: 4px; }
14070
- .composer { display: flex; gap: 8px; padding: 8px; border-top: 1px solid var(--rule); }
14518
+ .composer { display: flex; gap: var(--space-2); padding: var(--space-2); border-top: 1px solid var(--rule); }
14071
14519
  .composer input { flex: 1; padding: 6px 8px; border: 1px solid var(--rule); border-radius: var(--rad); font-family: var(--sans); }
14072
14520
  .wizard-step { padding: 10px; border: 1px solid var(--rule); border-radius: var(--rad); margin-bottom: 8px; background: var(--surface); }
14073
14521
  .wizard-step.active { border-color: var(--ink); }
14074
14522
  .wizard-step.done { background: var(--sage-bg); border-color: var(--sage); }
14075
- .code-block { font-family: var(--mono); background: var(--paper-3); padding: 8px; border-radius: var(--rad); font-size: 12px; overflow-x: auto; }
14523
+ .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; }
14076
14524
  .policy-center { max-width: 980px; margin: 0 auto; }
14077
- .policy-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; }
14078
- .policy-center h1 { font-family: var(--serif); font-size: 36px; line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14525
+ .policy-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; }
14526
+ .policy-center h1 { font-family: var(--serif); font-size: var(--text-display); line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14079
14527
  .policy-subtitle { max-width: 860px; color: var(--ink-2); font-size: 15px; margin: 0 0 24px; }
14080
14528
  .policy-panel { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--rad-lg); padding: 20px; margin: 18px 0; }
14081
- .policy-panel h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 14px; }
14082
- .template-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
14529
+ .policy-panel h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 14px; }
14530
+ .template-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
14083
14531
  .template-card { background: var(--surface-2); border: 1px solid var(--rule); border-radius: var(--rad); padding: 14px; min-height: 132px; }
14084
- .template-card-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 12px; }
14085
- .severity { border-radius: 999px; padding: 2px 9px; font-family: var(--mono); font-size: 11px; font-weight: 700; }
14532
+ .template-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); margin-bottom: 12px; }
14533
+ .severity { border-radius: 999px; padding: 2px 9px; font-family: var(--mono); font-size: var(--text-xs); font-weight: 700; }
14086
14534
  .severity.low { color: var(--sage); background: var(--sage-bg); }
14087
14535
  .severity.medium { color: var(--ochre); background: var(--ochre-bg); }
14088
14536
  .template-id { background: var(--paper-3); border-radius: var(--rad); padding: 2px 7px; color: var(--ink-3); }
14089
- .template-card h3 { font-size: 16px; margin: 0 0 6px; }
14090
- .template-card p { color: var(--ink-3); margin: 0; font-size: 14px; }
14537
+ .template-card h3 { font-size: var(--text-lg); margin: 0 0 6px; }
14538
+ .template-card p { color: var(--ink-3); margin: 0; font-size: var(--text-md); }
14091
14539
  .rules-scroll { overflow-x: auto; }
14092
14540
  .rules-table { width: 100%; border-collapse: collapse; min-width: 760px; }
14093
- .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); }
14541
+ .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); }
14094
14542
  .rules-table td { padding: 12px 10px; border-bottom: 1px solid var(--rule); vertical-align: top; }
14095
14543
  .link-btn, .template-cell { border: 0; background: transparent; color: var(--ink); padding: 0; cursor: pointer; font: inherit; text-align: left; }
14096
14544
  .template-cell { font-family: var(--mono); max-width: 180px; overflow-wrap: anywhere; }
14097
14545
  .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; }
14098
14546
  .template-picker-options { display: grid; gap: 6px; max-height: 320px; overflow-y: auto; }
14099
- .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); }
14547
+ .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); }
14100
14548
  .template-option small { display: block; color: var(--ink-3); margin-top: 2px; }
14101
- .template-picker-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
14549
+ .template-picker-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: 10px; }
14102
14550
  .allow-count { color: var(--sage); font-weight: 700; }
14103
14551
  .block-count { color: var(--rust); }
14104
14552
  .toggle-on { display: inline-block; width: 28px; height: 16px; border-radius: 999px; background: var(--sage); position: relative; }
14105
14553
  .toggle-on::after { content: ""; position: absolute; right: 2px; top: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--surface); }
14106
14554
  .error-text { color: var(--rust); margin: 8px 0 0; }
14107
14555
  .intel-center { max-width: 980px; margin: 0 auto; }
14108
- .intel-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: 12px; letter-spacing: 0; }
14109
- .intel-center h1 { font-family: var(--serif); font-size: 36px; line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14556
+ .intel-center .eyebrow { margin: 0 0 6px; color: var(--ink-3); font-family: var(--mono); font-size: var(--text-sm); letter-spacing: 0; }
14557
+ .intel-center h1 { font-family: var(--serif); font-size: var(--text-display); line-height: 1.08; font-weight: 400; margin: 0 0 10px; }
14110
14558
  .intel-subtitle { max-width: 860px; color: var(--ink-2); font-size: 15px; margin: 0 0 24px; }
14111
14559
  .intel-panel { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--rad-lg); padding: 20px; margin: 18px 0; }
14112
- .intel-panel h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 14px; }
14113
- .intel-row { display: grid; grid-template-columns: 200px 1fr auto; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--rule); align-items: start; }
14560
+ .intel-panel h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 14px; }
14561
+ .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; }
14114
14562
  .intel-row:last-child { border-bottom: 0; }
14115
14563
  .intel-row-name { font-weight: 600; }
14116
- .intel-row-name small { display: block; color: var(--ink-3); font-weight: 400; font-size: 12px; margin-top: 2px; font-family: var(--mono); }
14564
+ .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); }
14117
14565
  .intel-row-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
14118
- .intel-row-current { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
14119
- .intel-row-tradeoff { color: var(--ink-2); font-size: 13px; line-height: 1.5; }
14566
+ .intel-row-current { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
14567
+ .intel-row-tradeoff { color: var(--ink-2); font-size: var(--text-base); line-height: 1.5; }
14120
14568
  .intel-status-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
14121
14569
  .intel-status-dot.green { background: var(--sage); }
14122
14570
  .intel-status-dot.yellow { background: var(--ochre); }
14123
14571
  .intel-status-dot.red { background: var(--rust); }
14124
- .intel-hardware { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: 13px; }
14572
+ .intel-hardware { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: var(--text-base); }
14125
14573
  .intel-hardware dt { color: var(--ink-3); font-family: var(--mono); }
14126
14574
  .intel-hardware dd { margin: 0; }
14127
14575
  .intel-modal-backdrop {
@@ -14134,33 +14582,162 @@ body {
14134
14582
  box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 24px;
14135
14583
  width: 100%; max-width: 640px;
14136
14584
  }
14137
- .intel-modal h2 { font-family: var(--serif); font-size: 22px; font-weight: 400; margin: 0 0 8px; }
14138
- .intel-modal-subtitle { color: var(--ink-3); margin: 0 0 18px; font-size: 13px; }
14585
+ .intel-modal h2 { font-family: var(--serif); font-size: var(--text-xl); font-weight: 400; margin: 0 0 8px; }
14586
+ .intel-modal-subtitle { color: var(--ink-3); margin: 0 0 18px; font-size: var(--text-base); }
14139
14587
  .intel-option {
14140
- border: 1px solid var(--rule); border-radius: var(--rad); padding: 12px;
14588
+ border: 1px solid var(--rule); border-radius: var(--rad); padding: var(--space-3);
14141
14589
  margin-bottom: 10px; background: var(--surface-2); cursor: pointer;
14142
14590
  display: grid; grid-template-columns: 18px 1fr; gap: 10px; align-items: start;
14143
14591
  }
14144
14592
  .intel-option.selected { border-color: var(--ink); background: var(--surface); }
14145
- .intel-option-body strong { display: block; font-size: 14px; margin-bottom: 4px; }
14146
- .intel-option-body small { display: block; color: var(--ink-3); font-size: 12px; line-height: 1.5; }
14593
+ .intel-option-body strong { display: block; font-size: var(--text-md); margin-bottom: 4px; }
14594
+ .intel-option-body small { display: block; color: var(--ink-3); font-size: var(--text-sm); line-height: 1.5; }
14147
14595
  .intel-suboptions { margin-top: 10px; padding: 10px; background: var(--paper-3); border-radius: var(--rad); }
14148
- .intel-suboptions label { display: block; margin: 6px 0; font-size: 13px; }
14596
+ .intel-suboptions label { display: block; margin: 6px 0; font-size: var(--text-base); }
14149
14597
  .intel-suboptions input[type="text"], .intel-suboptions input[type="password"] {
14150
14598
  width: 100%; padding: 6px 8px; border: 1px solid var(--rule); border-radius: var(--rad);
14151
- font-family: var(--mono); font-size: 12px; box-sizing: border-box;
14599
+ font-family: var(--mono); font-size: var(--text-sm); box-sizing: border-box;
14600
+ }
14601
+ .intel-modal-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: 18px; }
14602
+ /*
14603
+ * Intelligence panel polish (Sprint Piece 2 PR 3). Card grid layout
14604
+ * replaces the legacy 3-col row visual. The legacy .intel-row and
14605
+ * .intel-status-dot rules above are retained for the e2e selector
14606
+ * contract (.intel-row[data-intel-surface="..."]) and as the responsive
14607
+ * fallback. Cards render as a flex column with a substrate inset,
14608
+ * status badge with shaped glyph, and a recent-failures toggle in the
14609
+ * card foot.
14610
+ */
14611
+ .intel-wrap { max-width: 1000px; margin: 0 auto; }
14612
+ .intel-grid {
14613
+ display: grid;
14614
+ grid-template-columns: repeat(2, minmax(0, 1fr));
14615
+ gap: 12px;
14616
+ }
14617
+ .intel-card {
14618
+ background: var(--surface);
14619
+ border: 1px solid var(--rule);
14620
+ border-radius: var(--rad-lg);
14621
+ padding: 16px;
14622
+ display: flex; flex-direction: column;
14623
+ gap: 12px;
14624
+ }
14625
+ .intel-card-head {
14626
+ display: flex; align-items: flex-start; justify-content: space-between;
14627
+ gap: 10px;
14628
+ }
14629
+ .intel-card-name {
14630
+ display: flex; flex-direction: column; gap: 2px;
14631
+ min-width: 0;
14632
+ }
14633
+ .intel-card-name strong {
14634
+ font-family: var(--serif); font-weight: 500;
14635
+ font-size: 15px; letter-spacing: 0.005em;
14636
+ }
14637
+ .intel-card-name small {
14638
+ color: var(--ink-3); font-size: 11px; font-family: var(--mono);
14639
+ }
14640
+ .intel-card-status {
14641
+ display: inline-flex; align-items: center; gap: 6px;
14642
+ font-family: var(--mono); font-size: 11px;
14643
+ padding: 3px 9px; border-radius: 999px;
14644
+ border: 1px solid var(--rule); background: var(--surface-2);
14645
+ flex-shrink: 0;
14646
+ }
14647
+ .intel-card-status.ok { color: var(--sage); border-color: var(--sage); background: var(--sage-bg); }
14648
+ .intel-card-status.warn { color: var(--ochre); border-color: var(--ochre); background: var(--ochre-bg); }
14649
+ .intel-card-status.fail { color: var(--rust); border-color: var(--rust); background: var(--rust-bg); }
14650
+ .status-glyph {
14651
+ width: 10px; height: 10px;
14652
+ position: relative; flex-shrink: 0;
14152
14653
  }
14153
- .intel-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
14654
+ .status-glyph.ok::before {
14655
+ content: ""; position: absolute; inset: 0;
14656
+ border-radius: 50%; background: currentColor;
14657
+ }
14658
+ .status-glyph.warn::before {
14659
+ content: ""; position: absolute; inset: 0;
14660
+ background: currentColor;
14661
+ clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
14662
+ }
14663
+ .status-glyph.fail::before {
14664
+ content: ""; position: absolute; inset: 1px;
14665
+ background: currentColor;
14666
+ clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
14667
+ }
14668
+ .intel-substrate {
14669
+ display: flex; flex-direction: column; gap: 4px;
14670
+ padding: 10px 12px;
14671
+ background: var(--paper-2);
14672
+ border: 1px solid var(--rule);
14673
+ border-radius: var(--rad);
14674
+ }
14675
+ .intel-substrate .sub-line {
14676
+ display: flex; justify-content: space-between; align-items: center;
14677
+ gap: 10px; font-size: 12px;
14678
+ }
14679
+ .intel-substrate .sub-line.primary {
14680
+ font-family: var(--mono); font-size: 13px; color: var(--ink);
14681
+ }
14682
+ .intel-substrate .sub-line.secondary { color: var(--ink-3); font-size: 11px; }
14683
+ .intel-card-foot {
14684
+ display: flex; align-items: center; justify-content: space-between;
14685
+ gap: 8px; padding-top: 4px;
14686
+ }
14687
+ .intel-failures-toggle {
14688
+ display: inline-flex; align-items: center; gap: 6px;
14689
+ font-size: 12px; font-family: var(--mono);
14690
+ color: var(--ink-3);
14691
+ background: transparent; border: 0; padding: 0;
14692
+ cursor: pointer;
14693
+ }
14694
+ .intel-failures-toggle:hover { color: var(--ink); }
14695
+ .intel-failures-toggle .caret {
14696
+ display: inline-block; width: 0; height: 0;
14697
+ border-left: 4px solid transparent;
14698
+ border-right: 4px solid transparent;
14699
+ border-top: 5px solid currentColor;
14700
+ transition: transform 160ms ease;
14701
+ }
14702
+ .intel-failures-toggle.open .caret { transform: rotate(180deg); }
14703
+ .intel-failures {
14704
+ border-top: 1px solid var(--rule);
14705
+ padding-top: 12px;
14706
+ display: flex; flex-direction: column;
14707
+ gap: 8px;
14708
+ }
14709
+ .intel-failure-row {
14710
+ display: grid; grid-template-columns: 88px 1fr; gap: 12px;
14711
+ font-size: 12px; padding: 8px 10px;
14712
+ border-radius: var(--rad);
14713
+ background: var(--paper-2);
14714
+ border: 1px solid var(--rule);
14715
+ }
14716
+ .intel-failure-row .ts {
14717
+ font-family: var(--mono); font-size: 11px; color: var(--ink-3);
14718
+ }
14719
+ .intel-failure-row .err-class {
14720
+ font-family: var(--mono); font-size: 10px;
14721
+ letter-spacing: 0.04em; text-transform: uppercase;
14722
+ color: var(--rust); margin-bottom: 2px;
14723
+ }
14724
+ .btn-quiet {
14725
+ background: transparent; border: 1px solid var(--rule);
14726
+ padding: 2px 6px; font-size: 11px;
14727
+ border-radius: var(--rad); cursor: pointer;
14728
+ color: var(--ink-2); font-family: var(--sans);
14729
+ }
14730
+ .btn-quiet:hover { background: var(--surface-2); color: var(--ink); }
14154
14731
  .banner-warn {
14155
14732
  background: var(--ochre-bg); color: var(--ochre); border: 1px solid var(--ochre);
14156
- border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: 13px;
14733
+ border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: var(--text-base);
14157
14734
  }
14158
14735
  .banner-info {
14159
14736
  background: var(--indigo-bg); color: var(--indigo); border: 1px solid var(--indigo);
14160
- border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: 13px;
14737
+ border-radius: var(--rad); padding: 8px 12px; margin: 8px 0; font-size: var(--text-base);
14161
14738
  }
14162
14739
  .btn.chip {
14163
- border-radius: 999px; padding: 4px 12px; font-size: 12px;
14740
+ border-radius: 999px; padding: 4px 12px; font-size: var(--text-sm);
14164
14741
  background: var(--surface-2); border-color: var(--rule);
14165
14742
  }
14166
14743
  .btn.chip:hover:not(:disabled) { background: var(--paper-3); }
@@ -14177,85 +14754,210 @@ body {
14177
14754
  * dynamically, but the input box is below the fold, so I still have
14178
14755
  * to scroll." rc.5 closes that with a structural layout fix.
14179
14756
  */
14757
+ /* Sprint Piece 2 PR 2 (2026-05-03): concierge surface polish.
14758
+ * Translates Claude Design references at
14759
+ * server/docs/design-refs/sprint-piece-2/surface-concierge.jsx and the
14760
+ * Surface 1 block of surfaces.css. The bounded-card layout above is
14761
+ * preserved verbatim because rc.5 and the DDD e2e suite depend on it;
14762
+ * the Claude Design reference uses height: 720px for the card, but
14763
+ * production keeps the calc-based bounded height so the card adapts
14764
+ * to the operator viewport. The polish lands the persona glyph-ring,
14765
+ * mono uppercase author labels, paper-2 background for concierge
14766
+ * replies, suggest-grid empty-state cards, and the composer input-wrap
14767
+ * with the keyboard-shortcut pill.
14768
+ */
14769
+ .concierge-wrap { max-width: 880px; margin: 0 auto; }
14770
+ .page-head {
14771
+ display: flex; align-items: flex-end; justify-content: space-between;
14772
+ gap: var(--space-4);
14773
+ margin-bottom: 18px; padding-bottom: 14px;
14774
+ border-bottom: 1px solid var(--rule);
14775
+ }
14776
+ .page-head .eyebrow {
14777
+ font-family: var(--mono); font-size: var(--text-xs);
14778
+ letter-spacing: 0.08em; text-transform: uppercase;
14779
+ color: var(--ink-3);
14780
+ margin: 0 0 6px;
14781
+ }
14782
+ .page-head h1 {
14783
+ font-family: var(--serif); font-weight: 400;
14784
+ font-size: 28px; letter-spacing: -0.01em;
14785
+ margin: 0 0 4px;
14786
+ }
14787
+ .page-head .sub {
14788
+ color: var(--ink-3); margin: 0;
14789
+ font-size: var(--text-base); max-width: 60ch;
14790
+ }
14180
14791
  .concierge-card {
14181
14792
  display: flex; flex-direction: column;
14182
14793
  height: calc(100vh - 180px);
14183
14794
  max-height: calc(100vh - 180px);
14184
14795
  min-height: 360px;
14185
- padding: 16px 18px;
14796
+ padding: 18px 22px 14px;
14186
14797
  gap: 0;
14187
14798
  }
14188
14799
  .concierge-header {
14189
14800
  display: flex; align-items: center; justify-content: space-between;
14190
- gap: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--rule);
14801
+ gap: var(--space-3); padding-bottom: 14px;
14802
+ border-bottom: 1px solid var(--rule);
14191
14803
  flex-wrap: wrap;
14192
14804
  flex-shrink: 0;
14193
14805
  }
14194
- .concierge-persona {
14195
- display: flex; align-items: baseline; gap: 8px;
14196
- font-size: 14px;
14806
+ .concierge-persona { display: flex; align-items: center; gap: 10px; }
14807
+ .concierge-persona .glyph-ring {
14808
+ width: 26px; height: 26px;
14809
+ border: 1.5px solid var(--ink-2);
14810
+ border-radius: 50%;
14811
+ position: relative;
14812
+ flex-shrink: 0;
14813
+ }
14814
+ .concierge-persona .glyph-ring::after {
14815
+ content: ""; position: absolute; inset: 5px;
14816
+ border-radius: 50%; background: var(--ink-2);
14817
+ }
14818
+ .concierge-persona-text { display: flex; flex-direction: column; }
14819
+ .concierge-persona-text strong {
14820
+ font-family: var(--serif); font-weight: 500;
14821
+ font-size: 15px; letter-spacing: 0.005em;
14822
+ }
14823
+ .concierge-persona-text small {
14824
+ color: var(--ink-3); font-size: var(--text-xs);
14825
+ font-family: var(--mono);
14826
+ }
14827
+ .concierge-meta {
14828
+ display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
14197
14829
  }
14198
- .concierge-persona strong { font-size: 14px; }
14199
14830
  .concierge-badge { white-space: nowrap; }
14200
14831
  .concierge-history {
14201
14832
  flex: 1 1 auto;
14202
14833
  min-height: 0;
14203
14834
  overflow-y: auto;
14204
- padding: 14px 4px;
14205
- display: flex; flex-direction: column; gap: 14px;
14835
+ padding: 18px 4px 6px;
14836
+ display: flex; flex-direction: column; gap: 18px;
14206
14837
  }
14207
14838
  .concierge-msg {
14208
- display: flex; flex-direction: column; gap: 4px;
14209
- max-width: 80%;
14839
+ display: flex; flex-direction: column; gap: 5px;
14840
+ max-width: 78%;
14210
14841
  }
14211
14842
  .concierge-msg-author {
14212
- font-size: 11px;
14843
+ font-size: 10px;
14844
+ font-family: var(--mono);
14845
+ letter-spacing: 0.04em;
14846
+ text-transform: uppercase;
14847
+ color: var(--ink-4);
14213
14848
  }
14214
14849
  .concierge-msg-body {
14215
- padding: 10px 14px; border-radius: 12px;
14216
- border: 1px solid var(--rule); background: var(--surface);
14217
- white-space: pre-wrap; word-wrap: break-word;
14218
- font-size: 14px; line-height: 1.5;
14850
+ padding: 11px 14px;
14851
+ border-radius: 12px;
14852
+ border: 1px solid var(--rule);
14853
+ background: var(--surface);
14854
+ font-size: var(--text-md);
14855
+ line-height: 1.55;
14856
+ white-space: pre-wrap;
14857
+ word-wrap: break-word;
14219
14858
  }
14220
14859
  .concierge-msg-concierge { align-self: flex-start; }
14860
+ .concierge-msg-concierge .concierge-msg-body {
14861
+ background: var(--paper-2);
14862
+ }
14221
14863
  .concierge-msg-operator { align-self: flex-end; align-items: flex-end; }
14222
14864
  .concierge-msg-operator .concierge-msg-body {
14223
14865
  background: var(--ink); color: var(--paper); border-color: var(--ink);
14224
14866
  }
14867
+ .concierge-msg-meta {
14868
+ display: flex; gap: 6px;
14869
+ font-size: 10px;
14870
+ color: var(--ink-4);
14871
+ font-family: var(--mono);
14872
+ }
14225
14873
  .concierge-empty {
14226
- padding: 24px 8px; text-align: left;
14227
- font-size: 13px; line-height: 1.6;
14874
+ flex: 1 1 auto;
14875
+ display: flex; flex-direction: column; gap: 22px;
14876
+ justify-content: center;
14877
+ padding: 24px 12px;
14878
+ }
14879
+ .concierge-empty-headline { max-width: 52ch; }
14880
+ .concierge-empty-headline h2 {
14881
+ font-family: var(--serif); font-weight: 400;
14882
+ font-size: var(--text-xl); margin: 0 0 6px;
14883
+ letter-spacing: -0.005em;
14884
+ }
14885
+ .concierge-empty-headline p {
14886
+ color: var(--ink-3); margin: 0;
14887
+ font-size: var(--text-md); line-height: 1.55;
14888
+ }
14889
+ .concierge-suggest-grid {
14890
+ display: grid;
14891
+ grid-template-columns: repeat(3, minmax(0, 1fr));
14892
+ gap: 10px;
14893
+ }
14894
+ .concierge-suggest {
14895
+ background: var(--surface);
14896
+ border: 1px solid var(--rule);
14897
+ border-radius: var(--rad);
14898
+ padding: 12px 14px;
14899
+ font-size: var(--text-base);
14900
+ cursor: pointer;
14901
+ display: flex; flex-direction: column;
14902
+ gap: 6px;
14903
+ text-align: left;
14904
+ font-family: var(--sans);
14905
+ color: var(--ink);
14906
+ }
14907
+ .concierge-suggest:hover:not(:disabled) {
14908
+ background: var(--surface-2);
14909
+ border-color: var(--rule-2);
14910
+ }
14911
+ .concierge-suggest:disabled {
14912
+ cursor: not-allowed; color: var(--ink-4); opacity: 0.7;
14913
+ }
14914
+ .concierge-suggest .label {
14915
+ font-family: var(--mono);
14916
+ font-size: 10px;
14917
+ letter-spacing: 0.06em;
14918
+ text-transform: uppercase;
14919
+ color: var(--ink-3);
14228
14920
  }
14229
14921
  .concierge-composer {
14230
14922
  display: flex; gap: 10px; align-items: center;
14231
- padding: 12px 0 8px;
14923
+ padding: 12px 0 4px;
14232
14924
  border-top: 1px solid var(--rule);
14233
14925
  flex-shrink: 0;
14234
14926
  }
14927
+ .concierge-composer .input-wrap {
14928
+ flex: 1;
14929
+ display: flex; align-items: center; gap: var(--space-2);
14930
+ padding: 8px 12px;
14931
+ border: 1px solid var(--rule);
14932
+ border-radius: var(--rad);
14933
+ background: var(--surface);
14934
+ }
14935
+ .concierge-composer .input-wrap:focus-within {
14936
+ border-color: var(--ink-3);
14937
+ }
14235
14938
  .concierge-composer input {
14236
14939
  flex: 1; min-width: 0;
14237
- padding: 10px 14px;
14238
- border: 1px solid var(--rule); border-radius: var(--rad);
14239
- font-family: var(--sans); font-size: 14px;
14240
- background: var(--surface); color: var(--ink);
14940
+ padding: 4px 0;
14941
+ border: 0;
14942
+ background: transparent;
14943
+ color: var(--ink);
14944
+ font-family: var(--sans);
14945
+ font-size: var(--text-md);
14946
+ outline: none;
14241
14947
  }
14242
- .concierge-composer input:focus {
14243
- outline: none; border-color: var(--ink-3);
14948
+ .concierge-composer input::placeholder { color: var(--ink-4); }
14949
+ .concierge-composer .composer-meta {
14950
+ font-family: var(--mono);
14951
+ font-size: 10px;
14952
+ color: var(--ink-4);
14953
+ letter-spacing: 0.04em;
14244
14954
  }
14245
14955
  .concierge-composer .btn-primary {
14246
- padding: 8px 18px; font-size: 13px; flex-shrink: 0;
14247
- }
14248
- .concierge-chips {
14249
- display: flex; flex-wrap: wrap; gap: 6px;
14250
- padding: 10px 0 0;
14251
- }
14252
- .concierge-chips::before {
14253
- content: "Try:"; color: var(--ink-3); font-size: 12px;
14254
- align-self: center; margin-right: 4px;
14956
+ padding: 8px 18px; font-size: var(--text-base); flex-shrink: 0;
14255
14957
  }
14256
14958
  .concierge-foot {
14257
14959
  margin: 12px 0 0; padding-top: 10px; border-top: 1px dashed var(--rule);
14258
- font-size: 12px;
14960
+ font-size: var(--text-sm);
14259
14961
  }
14260
14962
  .concierge-foot a { color: var(--ink-2); }
14261
14963
  .tier1-approval-card {
@@ -14263,20 +14965,532 @@ body {
14263
14965
  border-radius: var(--rad); padding: 14px 16px; margin: 12px 0;
14264
14966
  }
14265
14967
  .tier1-approval-card h3 {
14266
- margin: 0 0 8px; color: var(--ochre); font-size: 14px;
14968
+ margin: 0 0 8px; color: var(--ochre); font-size: var(--text-md);
14267
14969
  }
14268
- .tier1-approval-card p { margin: 0 0 12px; font-size: 13px; }
14970
+ .tier1-approval-card p { margin: 0 0 12px; font-size: var(--text-base); }
14269
14971
  .tier1-approval-card .actions {
14270
- display: flex; gap: 8px; flex-wrap: wrap;
14972
+ display: flex; gap: var(--space-2); flex-wrap: wrap;
14973
+ }
14974
+ /* Sprint Piece 2 PR 4 (2026-05-04): Agents view + Inspect pane polish.
14975
+ * Translates Claude Design references at
14976
+ * server/docs/design-refs/sprint-piece-2/surface-agents.jsx and the
14977
+ * Surface 3 block of surfaces.css. The fortress-column .agent-row,
14978
+ * .agent-row-head, and .agent-row-actions rules above are kept verbatim
14979
+ * because Finding DD tests pin them; the new Agents-view list scopes
14980
+ * its grid layout under .agents-list (descendant selector) so the
14981
+ * fortress-column rules are unaffected. The inspect pane combines the
14982
+ * existing .card surface with .inspect-pane structure (sticky right
14983
+ * rail, internal scroll, sectioned body) for the agent-detail view.
14984
+ */
14985
+ .agents-wrap { max-width: 1080px; margin: 0 auto; }
14986
+ .agents-layout {
14987
+ display: grid;
14988
+ grid-template-columns: 1fr 420px;
14989
+ gap: 20px;
14990
+ align-items: start;
14991
+ }
14992
+ .agents-list {
14993
+ background: var(--surface);
14994
+ border: 1px solid var(--rule);
14995
+ border-radius: var(--rad-lg);
14996
+ overflow: hidden;
14997
+ }
14998
+ .agents-list-head {
14999
+ display: grid;
15000
+ grid-template-columns: minmax(0, 1fr) 110px 120px 88px;
15001
+ gap: 12px;
15002
+ padding: 10px 16px;
15003
+ border-bottom: 1px solid var(--rule);
15004
+ background: var(--paper-2);
15005
+ font-family: var(--mono);
15006
+ font-size: 10px;
15007
+ letter-spacing: 0.06em;
15008
+ text-transform: uppercase;
15009
+ color: var(--ink-3);
15010
+ }
15011
+ .agents-list .agent-row {
15012
+ display: grid;
15013
+ grid-template-columns: minmax(0, 1fr) 110px 120px 88px;
15014
+ gap: 12px;
15015
+ padding: 14px 16px;
15016
+ border-bottom: 1px solid var(--rule);
15017
+ align-items: center;
15018
+ cursor: pointer;
15019
+ transition: background 120ms ease;
15020
+ }
15021
+ .agents-list .agent-row:last-child { border-bottom: 0; }
15022
+ .agents-list .agent-row:hover { background: var(--paper-2); }
15023
+ .agents-list .agent-row.selected {
15024
+ background: var(--paper-2);
15025
+ box-shadow: inset 3px 0 0 var(--ink);
15026
+ }
15027
+ .agent-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
15028
+ .agent-glyph {
15029
+ width: 28px; height: 28px;
15030
+ border-radius: var(--rad);
15031
+ background: var(--paper-3);
15032
+ border: 1px solid var(--rule);
15033
+ display: grid; place-items: center;
15034
+ flex-shrink: 0;
15035
+ font-family: var(--mono);
15036
+ font-size: 11px;
15037
+ color: var(--ink-2);
15038
+ font-weight: 600;
15039
+ }
15040
+ .agent-name {
15041
+ display: flex; flex-direction: column; min-width: 0;
15042
+ }
15043
+ .agent-name strong {
15044
+ font-size: var(--text-base); font-weight: 500;
15045
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
15046
+ }
15047
+ .agent-name small {
15048
+ font-family: var(--mono); font-size: var(--text-xs); color: var(--ink-3);
15049
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
15050
+ display: block;
15051
+ }
15052
+ .agent-state {
15053
+ display: inline-flex; align-items: center; gap: 6px;
15054
+ font-size: var(--text-xs);
15055
+ font-family: var(--mono);
15056
+ }
15057
+ .state-dot {
15058
+ width: 7px; height: 7px; border-radius: 50%;
15059
+ background: var(--ink-4);
15060
+ }
15061
+ .state-dot.live { background: var(--sage); animation: pulse-soft 2.4s ease-in-out infinite; }
15062
+ .state-dot.idle { background: var(--ochre); }
15063
+ .state-dot.off { background: var(--ink-4); }
15064
+ @keyframes pulse-soft {
15065
+ 0%, 100% { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
15066
+ 50% { box-shadow: 0 0 0 4px transparent; opacity: 0.7; }
15067
+ }
15068
+ .agent-last {
15069
+ font-family: var(--mono); font-size: var(--text-xs); color: var(--ink-3);
15070
+ }
15071
+ /* Inspect pane (combined with .card outer wrapper for the
15072
+ * renderAgentInspectPanel return-shape regex anchored in
15073
+ * dashboard-welcome.test.ts:152). The .inspect-pane modifier overrides
15074
+ * .card padding so internal sections control their own spacing.
15075
+ */
15076
+ .inspect-pane {
15077
+ padding: 0;
15078
+ display: flex; flex-direction: column;
15079
+ position: sticky;
15080
+ top: 20px;
15081
+ max-height: calc(100vh - 100px);
15082
+ overflow: hidden;
15083
+ }
15084
+ .inspect-head {
15085
+ padding: 16px 18px;
15086
+ border-bottom: 1px solid var(--rule);
15087
+ display: flex; flex-direction: column; gap: 10px;
15088
+ }
15089
+ .inspect-head .row1 {
15090
+ display: flex; align-items: center; gap: 10px;
15091
+ }
15092
+ .inspect-head h3 {
15093
+ font-family: var(--serif); font-weight: 500;
15094
+ font-size: 17px; margin: 0;
15095
+ }
15096
+ .inspect-head .meta {
15097
+ display: flex; gap: 6px; flex-wrap: wrap;
15098
+ }
15099
+ .inspect-body {
15100
+ overflow-y: auto;
15101
+ padding: 4px 18px 18px;
15102
+ }
15103
+ .inspect-section {
15104
+ padding: 14px 0;
15105
+ border-bottom: 1px solid var(--rule);
15106
+ }
15107
+ .inspect-section:last-child { border-bottom: 0; }
15108
+ .inspect-section h4 {
15109
+ font-family: var(--mono);
15110
+ font-size: 10px;
15111
+ letter-spacing: 0.08em;
15112
+ text-transform: uppercase;
15113
+ color: var(--ink-3);
15114
+ margin: 0 0 10px;
15115
+ display: flex; align-items: center; justify-content: space-between;
15116
+ }
15117
+ .inspect-section h4 .count {
15118
+ font-family: var(--mono);
15119
+ background: var(--paper-3);
15120
+ border-radius: 999px;
15121
+ padding: 1px 7px;
15122
+ color: var(--ink-2);
15123
+ font-size: 10px;
15124
+ }
15125
+ .approval-row {
15126
+ background: var(--ochre-bg);
15127
+ border: 1px solid var(--ochre);
15128
+ border-radius: var(--rad);
15129
+ padding: 10px 12px;
15130
+ margin-bottom: 8px;
15131
+ display: flex; flex-direction: column; gap: 8px;
15132
+ }
15133
+ .approval-row .what { font-size: var(--text-base); color: var(--ink); }
15134
+ .approval-row .what .pill { margin-right: 6px; }
15135
+ .approval-row .why {
15136
+ font-size: var(--text-sm); color: var(--ink-2);
15137
+ padding-left: 10px;
15138
+ border-left: 2px solid var(--ochre);
15139
+ }
15140
+ .approval-row .actions { display: flex; gap: 6px; justify-content: flex-end; }
15141
+ .timeline {
15142
+ display: flex; flex-direction: column; gap: 0;
15143
+ position: relative;
15144
+ padding-left: 14px;
15145
+ }
15146
+ .timeline::before {
15147
+ content: "";
15148
+ position: absolute;
15149
+ left: 4px; top: 6px; bottom: 6px;
15150
+ width: 1px;
15151
+ background: var(--rule);
15152
+ }
15153
+ .timeline-item {
15154
+ position: relative;
15155
+ padding: 6px 0 10px;
15156
+ font-size: var(--text-sm);
15157
+ }
15158
+ .timeline-item::before {
15159
+ content: "";
15160
+ position: absolute;
15161
+ left: -14px; top: 11px;
15162
+ width: 8px; height: 8px;
15163
+ border-radius: 50%;
15164
+ background: var(--surface);
15165
+ border: 1.5px solid var(--ink-4);
15166
+ }
15167
+ .timeline-item.ok::before { border-color: var(--sage); }
15168
+ .timeline-item.warn::before { border-color: var(--ochre); }
15169
+ .timeline-item.fail::before { border-color: var(--rust); }
15170
+ .timeline-item .ts {
15171
+ font-family: var(--mono); font-size: 10px;
15172
+ color: var(--ink-3);
15173
+ letter-spacing: 0.02em;
15174
+ }
15175
+ .timeline-item .what {
15176
+ margin-top: 2px; color: var(--ink); font-size: var(--text-base);
15177
+ }
15178
+ .timeline-item .att {
15179
+ margin-top: 4px;
15180
+ display: inline-flex;
15181
+ }
15182
+ .policy-line {
15183
+ display: flex; justify-content: space-between; align-items: center;
15184
+ padding: 5px 0; font-size: var(--text-base);
15185
+ border-bottom: 1px dashed var(--rule);
15186
+ }
15187
+ .policy-line:last-child { border-bottom: 0; }
15188
+ .policy-line .k { color: var(--ink-3); }
15189
+ .policy-line .v { font-family: var(--mono); font-size: var(--text-sm); color: var(--ink); }
15190
+ /* Empty-state block for when no agents are wrapped. The
15191
+ * renderAgentsList empty-state branch begins with the literal
15192
+ * '<h1>Agents</h1>' (regex-pinned in agents-empty-state-canary.test.ts)
15193
+ * and the "No wrapped agents yet." copy is preserved verbatim.
15194
+ */
15195
+ .agents-empty {
15196
+ background: var(--surface);
15197
+ border: 1px dashed var(--rule-2);
15198
+ border-radius: var(--rad-lg);
15199
+ padding: 56px 40px;
15200
+ text-align: center;
15201
+ max-width: 720px;
15202
+ margin: 32px auto;
15203
+ }
15204
+ .agents-empty .icon-frame {
15205
+ width: 64px; height: 64px;
15206
+ margin: 0 auto 18px;
15207
+ border: 1px solid var(--rule);
15208
+ border-radius: 50%;
15209
+ display: grid; place-items: center;
15210
+ position: relative;
15211
+ }
15212
+ .agents-empty .icon-frame::before,
15213
+ .agents-empty .icon-frame::after {
15214
+ content: "";
15215
+ position: absolute;
15216
+ border: 1px solid var(--rule);
15217
+ border-radius: 50%;
15218
+ }
15219
+ .agents-empty .icon-frame::before { inset: -8px; opacity: 0.6; }
15220
+ .agents-empty .icon-frame::after { inset: -16px; opacity: 0.3; }
15221
+ .agents-empty .icon-frame .core {
15222
+ width: 22px; height: 22px;
15223
+ background: var(--ink);
15224
+ border-radius: 50%;
15225
+ }
15226
+ .agents-empty h2 {
15227
+ font-family: var(--serif);
15228
+ font-weight: 400;
15229
+ font-size: var(--text-xl);
15230
+ margin: 0 0 8px;
15231
+ }
15232
+ .agents-empty p {
15233
+ color: var(--ink-3);
15234
+ margin: 0 0 20px;
15235
+ font-size: var(--text-md);
15236
+ line-height: 1.55;
15237
+ max-width: 50ch;
15238
+ margin-left: auto; margin-right: auto;
15239
+ }
15240
+ .terminal-block {
15241
+ text-align: left;
15242
+ background: var(--paper-3);
15243
+ border: 1px solid var(--rule);
15244
+ border-radius: var(--rad);
15245
+ padding: 14px 16px;
15246
+ font-family: var(--mono);
15247
+ font-size: var(--text-base);
15248
+ margin: 0 auto 16px;
15249
+ max-width: 480px;
15250
+ display: flex; align-items: center; justify-content: space-between;
15251
+ }
15252
+ .terminal-block .cmd { color: var(--ink); }
15253
+ .terminal-block .cmd .prompt { color: var(--ink-3); margin-right: 8px; user-select: none; }
15254
+ .copy-btn {
15255
+ background: transparent; border: 0;
15256
+ color: var(--ink-3); cursor: pointer;
15257
+ font-family: var(--mono); font-size: var(--text-xs);
15258
+ padding: 2px 6px;
15259
+ border-radius: var(--rad);
15260
+ }
15261
+ .copy-btn:hover { color: var(--ink); background: var(--paper-2); }
15262
+
15263
+ /* Surface 5. Attestation badge gallery. */
15264
+ .att-gallery {
15265
+ display: flex; flex-direction: column; gap: 24px;
15266
+ max-width: 1000px;
15267
+ margin: 0 auto;
14271
15268
  }
15269
+ .att-section {
15270
+ background: var(--surface);
15271
+ border: 1px solid var(--rule);
15272
+ border-radius: var(--rad-lg);
15273
+ padding: 22px 24px;
15274
+ }
15275
+ .att-section-head {
15276
+ display: flex; justify-content: space-between; align-items: baseline;
15277
+ gap: 12px;
15278
+ margin-bottom: 16px;
15279
+ padding-bottom: 12px;
15280
+ border-bottom: 1px solid var(--rule);
15281
+ }
15282
+ .att-section-head h2 {
15283
+ font-family: var(--serif);
15284
+ font-weight: 400;
15285
+ font-size: 19px;
15286
+ margin: 0 0 4px;
15287
+ }
15288
+ .att-section-head p {
15289
+ color: var(--ink-3);
15290
+ margin: 0;
15291
+ font-size: 13px;
15292
+ line-height: 1.5;
15293
+ max-width: 64ch;
15294
+ }
15295
+ .att-section-head .label {
15296
+ font-family: var(--mono);
15297
+ font-size: 10px;
15298
+ letter-spacing: 0.08em;
15299
+ text-transform: uppercase;
15300
+ color: var(--ink-3);
15301
+ }
15302
+ .att-row {
15303
+ display: grid;
15304
+ grid-template-columns: 240px 1fr;
15305
+ gap: 24px;
15306
+ padding: 14px 0;
15307
+ border-bottom: 1px dashed var(--rule);
15308
+ align-items: center;
15309
+ }
15310
+ .att-row:last-child { border-bottom: 0; }
15311
+ .att-row .demo {
15312
+ display: flex; align-items: center; justify-content: flex-start;
15313
+ padding: 12px 16px;
15314
+ background: var(--paper-2);
15315
+ border: 1px solid var(--rule);
15316
+ border-radius: var(--rad);
15317
+ min-height: 56px;
15318
+ }
15319
+ .att-row .desc strong {
15320
+ font-size: 13px; display: block; margin-bottom: 3px;
15321
+ }
15322
+ .att-row .desc small {
15323
+ color: var(--ink-3); font-size: 12px;
15324
+ line-height: 1.5;
15325
+ }
15326
+
15327
+ /* Global persistent badge. Lives in the topbar across every surface. */
15328
+ .att-global {
15329
+ display: inline-flex; align-items: center;
15330
+ gap: 8px;
15331
+ padding: 4px 10px 4px 6px;
15332
+ border: 1px solid var(--rule);
15333
+ border-radius: 999px;
15334
+ background: var(--surface-2);
15335
+ font-family: var(--mono);
15336
+ font-size: 11px;
15337
+ color: var(--ink-2);
15338
+ }
15339
+ .att-global.verified { border-color: var(--sage); background: var(--sage-bg); color: var(--sage); }
15340
+ .att-global.degraded { border-color: var(--ochre); background: var(--ochre-bg); color: var(--ochre); }
15341
+ .att-global.unverified { border-color: var(--rust); background: var(--rust-bg); color: var(--rust); }
15342
+ .att-global .seal {
15343
+ width: 18px; height: 18px;
15344
+ position: relative;
15345
+ flex-shrink: 0;
15346
+ }
15347
+ .att-global .seal-ring {
15348
+ position: absolute; inset: 0;
15349
+ border: 1.5px solid currentColor;
15350
+ border-radius: 50%;
15351
+ }
15352
+ .att-global .seal-ring.dashed { border-style: dashed; }
15353
+ .att-global .seal-core {
15354
+ position: absolute; inset: 4px;
15355
+ background: currentColor;
15356
+ border-radius: 50%;
15357
+ opacity: 0.85;
15358
+ }
15359
+ .att-global.degraded .seal-core { background: transparent; border: 1px solid currentColor; }
15360
+ .att-global.unverified .seal-core {
15361
+ background: transparent;
15362
+ border: 1px solid currentColor;
15363
+ }
15364
+ .att-global.unverified .seal-core::after {
15365
+ content: ""; position: absolute; inset: 0;
15366
+ background: currentColor; opacity: 0.4;
15367
+ clip-path: polygon(0 0, 100% 100%, 100% 90%, 10% 0);
15368
+ }
15369
+ .att-global .label {
15370
+ font-family: var(--mono);
15371
+ font-size: 11px;
15372
+ letter-spacing: 0.02em;
15373
+ text-transform: uppercase;
15374
+ }
15375
+ .att-global .hash {
15376
+ font-family: var(--mono);
15377
+ font-size: 10px;
15378
+ opacity: 0.7;
15379
+ border-left: 1px solid currentColor;
15380
+ padding-left: 8px;
15381
+ margin-left: 2px;
15382
+ }
15383
+
15384
+ /* Per-agent badge. Square chip beside each agent. */
15385
+ .att-agent {
15386
+ display: inline-flex; align-items: center;
15387
+ gap: 6px;
15388
+ padding: 3px 7px;
15389
+ border-radius: var(--rad);
15390
+ border: 1px solid var(--rule);
15391
+ background: var(--surface);
15392
+ font-family: var(--mono);
15393
+ font-size: 10px;
15394
+ color: var(--ink-2);
15395
+ }
15396
+ .att-agent .mark {
15397
+ width: 10px; height: 10px;
15398
+ border: 1.5px solid currentColor;
15399
+ border-radius: 2px;
15400
+ position: relative;
15401
+ }
15402
+ .att-agent.verified { color: var(--sage); border-color: var(--sage); background: var(--sage-bg); }
15403
+ .att-agent.verified .mark { background: currentColor; }
15404
+ .att-agent.degraded { color: var(--ochre); border-color: var(--ochre); background: var(--ochre-bg); }
15405
+ .att-agent.unverified { color: var(--rust); border-color: var(--rust); background: var(--rust-bg); }
15406
+ .att-agent.unverified .mark {
15407
+ background: repeating-linear-gradient(
15408
+ 45deg, currentColor, currentColor 1px,
15409
+ transparent 1px, transparent 3px
15410
+ );
15411
+ }
15412
+
15413
+ /* Per-action badge. Tiny inline tick on timeline rows. */
15414
+ .att-action {
15415
+ display: inline-flex; align-items: center; gap: 4px;
15416
+ font-family: var(--mono);
15417
+ font-size: 10px;
15418
+ color: var(--ink-3);
15419
+ padding: 1px 6px;
15420
+ border-radius: 4px;
15421
+ background: var(--paper-3);
15422
+ border: 1px solid transparent;
15423
+ }
15424
+ .att-action .tick {
15425
+ width: 6px; height: 6px;
15426
+ border-radius: 1px;
15427
+ background: currentColor;
15428
+ }
15429
+ .att-action.verified { color: var(--sage); }
15430
+ .att-action.degraded { color: var(--ochre); }
15431
+ .att-action.unverified { color: var(--rust); }
15432
+ .att-action.neutral .tick { background: var(--ink-4); border-radius: 50%; }
15433
+
15434
+ /* Custody-provenance badge stub (v1.x). Visibly stubbed with dashed border. */
15435
+ .att-custody {
15436
+ display: inline-flex; align-items: center; gap: 8px;
15437
+ padding: 4px 10px 4px 6px;
15438
+ border-radius: var(--rad);
15439
+ border: 1px dashed var(--rule-2);
15440
+ background: var(--paper-3);
15441
+ color: var(--ink-3);
15442
+ font-family: var(--mono);
15443
+ font-size: 10px;
15444
+ }
15445
+ .att-custody .seal-stub {
15446
+ width: 16px; height: 16px;
15447
+ border: 1px dashed var(--ink-4);
15448
+ border-radius: 50%;
15449
+ position: relative;
15450
+ flex-shrink: 0;
15451
+ }
15452
+ .att-custody .seal-stub::after {
15453
+ content: ""; position: absolute; inset: 4px;
15454
+ border: 1px dashed var(--ink-4);
15455
+ border-radius: 50%;
15456
+ }
15457
+ .att-custody .stub-tag {
15458
+ letter-spacing: 0.06em;
15459
+ text-transform: uppercase;
15460
+ }
15461
+
15462
+ /* Tooltip surface for badges. */
15463
+ .att-tooltip {
15464
+ background: var(--ink);
15465
+ color: var(--paper);
15466
+ font-family: var(--mono);
15467
+ font-size: 11px;
15468
+ padding: 8px 10px;
15469
+ border-radius: var(--rad);
15470
+ max-width: 280px;
15471
+ line-height: 1.5;
15472
+ display: inline-block;
15473
+ }
15474
+ [data-theme="dark"] .att-tooltip {
15475
+ background: var(--paper-3);
15476
+ color: var(--ink);
15477
+ }
15478
+
14272
15479
  @media (max-width: 1100px) {
14273
15480
  .app, .app.route-full { grid-template-columns: 56px 1fr; grid-template-areas: "sidebar topbar" "sidebar main"; }
14274
15481
  .fortress { display: none; }
14275
15482
  .sidebar h1, .sidebar nav a span { display: none; }
15483
+ .sidebar nav a { justify-content: center; padding: 8px 6px; }
14276
15484
  .template-grid { grid-template-columns: 1fr; }
14277
15485
  .policy-center h1 { font-size: 30px; }
14278
15486
  .intel-center h1 { font-size: 30px; }
14279
15487
  .intel-row { grid-template-columns: 1fr; }
15488
+ .intel-grid { grid-template-columns: 1fr; }
15489
+ .intel-failure-row { grid-template-columns: 1fr; }
15490
+ .agents-layout { grid-template-columns: 1fr; }
15491
+ .agents-list-head, .agents-list .agent-row { grid-template-columns: minmax(0, 1fr) 90px 90px; }
15492
+ .agents-list-head span:nth-child(4), .agents-list .agent-row > .agent-last { display: none; }
15493
+ .inspect-pane { position: static; max-height: none; }
14280
15494
  }
14281
15495
  `;
14282
15496
  var NAV_ITEMS = [
@@ -14284,11 +15498,26 @@ var NAV_ITEMS = [
14284
15498
  { id: "agents", label: "Agents" },
14285
15499
  { id: "policy", label: "Policy" },
14286
15500
  { id: "intelligence", label: "Intelligence" },
15501
+ { id: "attestation", label: "Attestation" },
14287
15502
  { id: "privacy", label: "Privacy" },
14288
15503
  { id: "coordination", label: "Coordination" },
14289
15504
  { id: "health", label: "Health" },
14290
15505
  { id: "exit-drill", label: "Exit drill" }
14291
15506
  ];
15507
+ var NAV_ICON_PATHS = {
15508
+ 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"/>',
15509
+ 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"/>',
15510
+ policy: '<path d="M4 2h5l3 3v9H4z"/><path d="M9 2v3h3"/><path d="M6 9h4M6 11.5h4"/>',
15511
+ 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"/>',
15512
+ 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"/>',
15513
+ 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"/>',
15514
+ 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"/>',
15515
+ health: '<path d="M2 8h2.5l1.5-4 3 8 1.5-4H14"/>',
15516
+ "exit-drill": '<path d="M9.5 2H3v12h6.5"/><path d="M11 5l3 3-3 3"/><path d="M14 8H6.5"/>'
15517
+ };
15518
+ var 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">';
15519
+ var 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>';
15520
+ var 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>';
14292
15521
  function escHtml2(value) {
14293
15522
  if (value == null) return "";
14294
15523
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -14301,9 +15530,11 @@ function renderDashboardV11Html(options = {}) {
14301
15530
  const fortressId = options.fortressId ?? "fortress";
14302
15531
  const sanctuaryVersion = options.sanctuaryVersion ?? SANCTUARY_VERSION;
14303
15532
  const embedClient = options.embedClient !== false;
14304
- const nav = NAV_ITEMS.map(
14305
- (n) => `<a href="#${n.id}" data-route="${n.id}"><span>${escHtml2(n.label)}</span></a>`
14306
- ).join("\n ");
15533
+ const nav = NAV_ITEMS.map((n) => {
15534
+ const iconPath = NAV_ICON_PATHS[n.id] ?? "";
15535
+ const icon = iconPath ? SVG_OPEN + iconPath + "</svg>" : "";
15536
+ return `<a href="#${n.id}" data-route="${n.id}">${icon}<span>${escHtml2(n.label)}</span></a>`;
15537
+ }).join("\n ");
14307
15538
  const config = JSON.stringify({
14308
15539
  authToken,
14309
15540
  hubApiBase,
@@ -14335,8 +15566,12 @@ function renderDashboardV11Html(options = {}) {
14335
15566
  <span class="pill" data-pill="version">v${escHtml2(sanctuaryVersion)}</span>
14336
15567
  <span class="pill" data-pill="deployment">deployment: local</span>
14337
15568
  <span class="pill" data-pill="mode">mode: solo</span>
14338
- <span class="pill" data-pill="attestation">attestation: pending</span>
15569
+ <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>
14339
15570
  </div>
15571
+ <button class="btn btn-icon" id="btn-theme-toggle" data-action="theme-toggle" aria-label="Toggle theme" title="Toggle theme">
15572
+ <span class="icon-moon">${THEME_ICON_MOON}</span>
15573
+ <span class="icon-sun">${THEME_ICON_SUN}</span>
15574
+ </button>
14340
15575
  <button class="btn btn-danger" id="btn-lockdown" data-action="lockdown">Lockdown</button>
14341
15576
  </header>
14342
15577
  <main class="main" id="main"><p class="muted">Loading dashboard.</p></main>
@@ -17669,8 +18904,8 @@ function verifySHR(shr, now) {
17669
18904
  const errors = [];
17670
18905
  const warnings = [];
17671
18906
  const currentTime = now ?? /* @__PURE__ */ new Date();
17672
- if (!shr.body || !shr.signed_by || !shr.signature) {
17673
- errors.push("Missing required SHR fields (body, signed_by, or signature)");
18907
+ if (!shr.body || !shr.signed_by || !shr.signature || !shr.signature_scheme) {
18908
+ errors.push("Missing required SHR fields (body, signed_by, signature_scheme, or signature)");
17674
18909
  return {
17675
18910
  valid: false,
17676
18911
  errors,
@@ -17683,6 +18918,9 @@ function verifySHR(shr, now) {
17683
18918
  if (shr.body.shr_version !== "1.0") {
17684
18919
  errors.push(`Unsupported SHR version: ${shr.body.shr_version}`);
17685
18920
  }
18921
+ if (shr.signature_scheme !== SIGNATURE_SCHEME_V1) {
18922
+ errors.push(`Unsupported SHR signature_scheme: ${String(shr.signature_scheme)}`);
18923
+ }
17686
18924
  const expiresAt = new Date(shr.body.expires_at);
17687
18925
  if (isNaN(expiresAt.getTime())) {
17688
18926
  errors.push("Invalid expires_at timestamp");
@@ -28740,8 +29978,6 @@ function aggregateInbox(sources, store) {
28740
29978
  }
28741
29979
  return store.list();
28742
29980
  }
28743
-
28744
- // src/hub/activity-feed.ts
28745
29981
  var LIFECYCLE_VERBS = [
28746
29982
  "wrap",
28747
29983
  "unwrap",
@@ -28800,18 +30036,30 @@ function extractAgentIdHint(entry) {
28800
30036
  const value = details.agent_id;
28801
30037
  return typeof value === "string" && value.length > 0 ? value : void 0;
28802
30038
  }
30039
+ function deriveAttestationFragment(entryId) {
30040
+ const hash2 = crypto.createHash("sha256").update(entryId).digest("hex");
30041
+ return `${hash2.slice(0, 4)}..${hash2.slice(4, 6)}`;
30042
+ }
30043
+ function deriveAttestationState(entry) {
30044
+ return entry.result === "success" ? "verified" : "degraded";
30045
+ }
28803
30046
  function projectEntry(entry) {
28804
30047
  const agentIdHint = extractAgentIdHint(entry);
28805
30048
  const category = categorizeOperation(entry.layer, entry.operation);
30049
+ const entryId = `${entry.timestamp}|${entry.operation}|${entry.identity_id}`;
28806
30050
  return {
28807
30051
  version: "1.1",
28808
- entry_id: `${entry.timestamp}|${entry.operation}|${entry.identity_id}`,
30052
+ entry_id: entryId,
28809
30053
  emitted_at: entry.timestamp,
28810
30054
  ...agentIdHint ? { agent_id: agentIdHint } : {},
28811
30055
  identity_id: entry.identity_id,
28812
30056
  category,
28813
30057
  display_template_id: templateIdFor(category, entry.operation),
28814
- display_template_args: buildTemplateArgs(entry, agentIdHint)
30058
+ display_template_args: buildTemplateArgs(entry, agentIdHint),
30059
+ attestation: {
30060
+ state: deriveAttestationState(entry),
30061
+ fragment: deriveAttestationFragment(entryId)
30062
+ }
28815
30063
  };
28816
30064
  }
28817
30065
  async function aggregateActivity(sources, filter = {}) {
@@ -31760,7 +33008,7 @@ var MemoryStorage = class {
31760
33008
  };
31761
33009
 
31762
33010
  // src/contracts/v1.1/constants.ts
31763
- var SIGNATURE_SCHEME_V1 = "ed25519-v1";
33011
+ var SIGNATURE_SCHEME_V12 = "ed25519-v1";
31764
33012
  var EXIT_BUNDLE_MANIFEST_VERSION = "SANCTUARY_EXIT_BUNDLE_V1";
31765
33013
  var EXIT_BUNDLE_ARTIFACT_KINDS = [
31766
33014
  "public_identity",
@@ -31785,6 +33033,12 @@ var EXIT_BUNDLE_PATH_MAX_BYTES = 256;
31785
33033
  // src/exit/verifier.ts
31786
33034
  init_encoding();
31787
33035
  init_hashing();
33036
+ var InvalidExitBundleError = class extends Error {
33037
+ constructor(message) {
33038
+ super(message);
33039
+ this.name = "InvalidExitBundleError";
33040
+ }
33041
+ };
31788
33042
  var PRIVATE_MATERIAL_KEYS = /* @__PURE__ */ new Set([
31789
33043
  "private_key",
31790
33044
  "privatekey",
@@ -31967,7 +33221,7 @@ function verifyReputationArtifact(reputationArtifact, publicKeysByDid) {
31967
33221
  unverifiable_attestations: unverifiable
31968
33222
  };
31969
33223
  }
31970
- async function verifyExitBundle(bundleDir) {
33224
+ async function verifyExitBundle(bundleDir, options = {}) {
31971
33225
  const root = path.resolve(bundleDir);
31972
33226
  let manifest;
31973
33227
  let manifestBytes;
@@ -31976,7 +33230,9 @@ async function verifyExitBundle(bundleDir) {
31976
33230
  manifestBytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
31977
33231
  manifest = JSON.parse(Buffer.from(raw).toString("utf8"));
31978
33232
  } catch {
31979
- return fail(root, null, "other", ["manifest.json is missing or unreadable"]);
33233
+ throw new InvalidExitBundleError(
33234
+ `Not a valid SANCTUARY_EXIT_BUNDLE_V1 directory: manifest.json missing at ${path.join(root, "manifest.json")}`
33235
+ );
31980
33236
  }
31981
33237
  const warnings = [];
31982
33238
  const unsupportedArtifacts = [];
@@ -31984,7 +33240,7 @@ async function verifyExitBundle(bundleDir) {
31984
33240
  if (!body || body.manifest_version !== EXIT_BUNDLE_MANIFEST_VERSION) {
31985
33241
  return fail(root, manifest, "manifest_unknown_version", warnings, unsupportedArtifacts);
31986
33242
  }
31987
- if (body.signature_scheme !== SIGNATURE_SCHEME_V1) {
33243
+ if (body.signature_scheme !== SIGNATURE_SCHEME_V12) {
31988
33244
  return fail(
31989
33245
  root,
31990
33246
  manifest,
@@ -32144,9 +33400,16 @@ async function verifyExitBundle(bundleDir) {
32144
33400
  }
32145
33401
  const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
32146
33402
  const identityFailed = identity ? !identity.signature_valid : false;
33403
+ const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
33404
+ const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
33405
+ if (unverifiableFailed) {
33406
+ warnings.push(
33407
+ `${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
33408
+ );
33409
+ }
32147
33410
  return {
32148
33411
  version: "1.1",
32149
- passed: !reputationFailed && !identityFailed,
33412
+ passed: !reputationFailed && !identityFailed && !unverifiableFailed,
32150
33413
  verified_at: (/* @__PURE__ */ new Date()).toISOString(),
32151
33414
  manifest_path: path.join(root, "manifest.json"),
32152
33415
  manifest_hash: sha256Hex2(manifestBytes),
@@ -32163,7 +33426,7 @@ async function verifyExitBundle(bundleDir) {
32163
33426
  identity,
32164
33427
  audit,
32165
33428
  reputation,
32166
- failure_class: reputationFailed || identityFailed ? "other" : void 0
33429
+ failure_class: reputationFailed || identityFailed || unverifiableFailed ? "other" : void 0
32167
33430
  };
32168
33431
  }
32169
33432
 
@@ -32176,6 +33439,14 @@ var EXIT_POLICY_SETS_NAMESPACE = "_exit_policy_sets";
32176
33439
  var EXIT_COMMITMENTS_NAMESPACE = "_exit_commitments";
32177
33440
  var EXIT_PLACEHOLDER_METADATA_NAMESPACE = "_exit_placeholder_metadata";
32178
33441
  var PRIVACY_PLACEHOLDER_NAMESPACE = "_privacy_placeholder_vault";
33442
+ var ExitBundleImportError = class extends Error {
33443
+ code;
33444
+ constructor(code, message) {
33445
+ super(message);
33446
+ this.name = "ExitBundleImportError";
33447
+ this.code = code;
33448
+ }
33449
+ };
32179
33450
  function sha256Hex3(bytes) {
32180
33451
  return Array.from(hash(bytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
32181
33452
  }
@@ -32468,7 +33739,7 @@ async function exportExitBundle(opts) {
32468
33739
  ),
32469
33740
  artifacts_aggregate_hash_alg: "sha256",
32470
33741
  export_approval_audit_id: exportApprovalAuditId,
32471
- signature_scheme: SIGNATURE_SCHEME_V1
33742
+ signature_scheme: SIGNATURE_SCHEME_V12
32472
33743
  };
32473
33744
  const signature = sign(
32474
33745
  canonicalizeToBytes(body),
@@ -32653,7 +33924,9 @@ async function stageArtifact(storage, namespace, key, value) {
32653
33924
  await storage.write(namespace, key, jsonBytes(value));
32654
33925
  }
32655
33926
  async function importExitBundle(opts) {
32656
- const verification = await verifyExitBundle(opts.bundleDir);
33927
+ const verification = await verifyExitBundle(opts.bundleDir, {
33928
+ acceptUnverifiableAttestations: opts.acceptUnverifiableAttestations
33929
+ });
32657
33930
  if (!verification.passed) {
32658
33931
  return {
32659
33932
  verified: false,
@@ -32749,6 +34022,23 @@ async function importExitBundle(opts) {
32749
34022
  unsupported_artifacts: verification.unsupported_artifacts
32750
34023
  };
32751
34024
  }
34025
+ if (conflicts.public_identity_exists && !opts.forceRebind) {
34026
+ throw new ExitBundleImportError(
34027
+ "IDENTITY_OVERWRITE_REFUSED",
34028
+ "Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
34029
+ );
34030
+ }
34031
+ if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
34032
+ opts.auditLog.append(
34033
+ "l1",
34034
+ "exit_bundle_force_rebind",
34035
+ identityArtifact.json.bundle.identity_id,
34036
+ {
34037
+ manifest_version: manifest.body.manifest_version,
34038
+ fortress_id: manifest.body.identity_binding.fortress_id
34039
+ }
34040
+ );
34041
+ }
32752
34042
  const importId = importIdForManifest(manifest);
32753
34043
  const stagedArtifacts = [];
32754
34044
  if (identityArtifact) {
@@ -32859,7 +34149,7 @@ function exitBundleManifestShape() {
32859
34149
  manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
32860
34150
  artifacts: [...EXIT_BUNDLE_ARTIFACT_KINDS],
32861
34151
  hash_alg: "sha256",
32862
- signature_scheme: SIGNATURE_SCHEME_V1,
34152
+ signature_scheme: SIGNATURE_SCHEME_V12,
32863
34153
  required_top_level_file: "manifest.json",
32864
34154
  artifact_paths: [
32865
34155
  "artifacts/public_identity.json",
@@ -32968,6 +34258,11 @@ Options:
32968
34258
  --destination-identity-id <id> Destination signer for re-keyed state
32969
34259
  --state-namespace <name> Export a namespace; repeatable
32970
34260
  --conflict <skip|overwrite|version>
34261
+ --force-rebind On import: explicitly replace an existing fortress
34262
+ public identity (Tier 1 confirmation)
34263
+ --accept-unverifiable-attestations
34264
+ On import: accept reputation attestations whose
34265
+ signer DID is not in the bundle (Tier 1 confirmation)
32971
34266
  --json
32972
34267
  --yes, -y Explicit non-interactive Tier 1 approval
32973
34268
  --help, -h
@@ -32996,7 +34291,22 @@ async function runExitCommand(args) {
32996
34291
  write(err, "Usage: sanctuary exit verify <dir>\n");
32997
34292
  return 2;
32998
34293
  }
32999
- const result = await verifyExitBundle(dir);
34294
+ let result;
34295
+ try {
34296
+ result = await verifyExitBundle(dir, {
34297
+ acceptUnverifiableAttestations: hasFlag(
34298
+ argv,
34299
+ "--accept-unverifiable-attestations"
34300
+ )
34301
+ });
34302
+ } catch (e) {
34303
+ if (e instanceof InvalidExitBundleError) {
34304
+ write(err, `Error: ${e.message}
34305
+ `);
34306
+ return 1;
34307
+ }
34308
+ throw e;
34309
+ }
33000
34310
  if (json) {
33001
34311
  write(out, JSON.stringify(result, null, 2) + "\n");
33002
34312
  } else {
@@ -33074,9 +34384,15 @@ async function runExitCommand(args) {
33074
34384
  return 2;
33075
34385
  }
33076
34386
  const activate = hasFlag(argv, "--activate");
34387
+ const forceRebind = hasFlag(argv, "--force-rebind");
34388
+ const acceptUnverifiableAttestations = hasFlag(
34389
+ argv,
34390
+ "--accept-unverifiable-attestations"
34391
+ );
33077
34392
  if (activate) {
34393
+ const prompt = 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?";
33078
34394
  const approved = await confirmTier1(
33079
- "Tier 1 approval required: activate verified imported exit bundle?",
34395
+ prompt,
33080
34396
  hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
33081
34397
  stdin,
33082
34398
  err
@@ -33085,6 +34401,18 @@ async function runExitCommand(args) {
33085
34401
  write(err, "Aborted.\n");
33086
34402
  return 1;
33087
34403
  }
34404
+ if (acceptUnverifiableAttestations) {
34405
+ const acceptApproved = await confirmTier1(
34406
+ "Tier 1 approval required: accept unverifiable reputation attestations on import?",
34407
+ hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
34408
+ stdin,
34409
+ err
34410
+ );
34411
+ if (!acceptApproved) {
34412
+ write(err, "Aborted.\n");
34413
+ return 1;
34414
+ }
34415
+ }
33088
34416
  }
33089
34417
  const ctx = await openExitContext(argv, env);
33090
34418
  const conflict = flagValue(argv, "--conflict") ?? "skip";
@@ -33092,19 +34420,31 @@ async function runExitCommand(args) {
33092
34420
  write(err, "--conflict must be skip, overwrite, or version\n");
33093
34421
  return 2;
33094
34422
  }
33095
- const result = await importExitBundle({
33096
- bundleDir: dir,
33097
- storage: ctx.storage,
33098
- masterKey: ctx.masterKey,
33099
- identityManager: ctx.identityManager,
33100
- auditLog: ctx.auditLog,
33101
- reputationStore: ctx.reputationStore,
33102
- activate,
33103
- conflictResolution: conflict,
33104
- sourcePassphrase: flagValue(argv, "--source-passphrase"),
33105
- sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
33106
- destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
33107
- });
34423
+ let result;
34424
+ try {
34425
+ result = await importExitBundle({
34426
+ bundleDir: dir,
34427
+ storage: ctx.storage,
34428
+ masterKey: ctx.masterKey,
34429
+ identityManager: ctx.identityManager,
34430
+ auditLog: ctx.auditLog,
34431
+ reputationStore: ctx.reputationStore,
34432
+ activate,
34433
+ forceRebind,
34434
+ acceptUnverifiableAttestations,
34435
+ conflictResolution: conflict,
34436
+ sourcePassphrase: flagValue(argv, "--source-passphrase"),
34437
+ sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
34438
+ destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
34439
+ });
34440
+ } catch (e) {
34441
+ if (e instanceof InvalidExitBundleError) {
34442
+ write(err, `Error: ${e.message}
34443
+ `);
34444
+ return 1;
34445
+ }
34446
+ throw e;
34447
+ }
33108
34448
  if (json) write(out, JSON.stringify(result, null, 2) + "\n");
33109
34449
  else {
33110
34450
  write(out, `verified: ${result.verified}
@@ -33334,7 +34674,7 @@ async function createSanctuaryServer(options) {
33334
34674
  const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
33335
34675
  if (hasKeyParams) {
33336
34676
  throw new Error(
33337
- "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."
34677
+ "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."
33338
34678
  );
33339
34679
  }
33340
34680
  masterKey = generateRandomKey();
@@ -33927,6 +35267,7 @@ exports.CommitmentStore = CommitmentStore;
33927
35267
  exports.ContextGateEnforcer = ContextGateEnforcer;
33928
35268
  exports.ContextGatePolicyStore = ContextGatePolicyStore;
33929
35269
  exports.DashboardApprovalChannel = DashboardApprovalChannel;
35270
+ exports.ExitBundleImportError = ExitBundleImportError;
33930
35271
  exports.FederationRegistry = FederationRegistry;
33931
35272
  exports.FilesystemStorage = FilesystemStorage;
33932
35273
  exports.HERO_COPY = HERO_COPY;