@contextecf/guardian-cli 0.1.1 → 0.1.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.
@@ -17593,7 +17593,7 @@ async function runStartCommand(flags, options) {
17593
17593
  const home = resolveGuardianHome(options.env, options.platform);
17594
17594
  const profile = await readProfile(home);
17595
17595
  if (!profile) {
17596
- write(stderr, "Guardian is not installed. Run guardian install first.\n");
17596
+ write(stderr, "Guardian is not installed. Run guardian setup first.\n");
17597
17597
  return 1;
17598
17598
  }
17599
17599
  const host = stringFlag(flags, "host") ?? "127.0.0.1";
@@ -17601,10 +17601,8 @@ async function runStartCommand(flags, options) {
17601
17601
  write(stderr, "Guardian start only accepts loopback hosts: 127.0.0.1, localhost, or ::1.\n");
17602
17602
  return 1;
17603
17603
  }
17604
- const port = numberFlag(flags, "port") ?? 4317;
17605
- const requestedDaemonUrl = formatDaemonBaseUrl(host, port);
17606
- const requestedControlTowerUrl = `${requestedDaemonUrl}/control-tower`;
17607
- const controlTowerSource = host === "127.0.0.1" && port === 4317 ? "default_loopback" : "env_or_flag";
17604
+ const explicitPort = numberFlag(flags, "port");
17605
+ const port = explicitPort ?? 4317;
17608
17606
  let token;
17609
17607
  try {
17610
17608
  token = await readRuntimeToken(home);
@@ -17616,14 +17614,66 @@ async function runStartCommand(flags, options) {
17616
17614
  );
17617
17615
  return 1;
17618
17616
  }
17619
- const healthUrl = resolveDaemonHealthUrl(requestedControlTowerUrl);
17620
- if (!healthUrl.ok) {
17621
- write(stderr, `${healthUrl.error}
17617
+ const healthChecker = options.healthDaemon ?? requestGuardianDaemonHealth;
17618
+ const candidates = explicitPort ? [explicitPort] : Array.from({ length: 8 }, (_, index) => port + index);
17619
+ let selected;
17620
+ let firstTokenMismatch;
17621
+ for (const candidatePort of candidates) {
17622
+ const requestedDaemonUrl2 = formatDaemonBaseUrl(host, candidatePort);
17623
+ const requestedControlTowerUrl2 = `${requestedDaemonUrl2}/control-tower`;
17624
+ const controlTowerSource2 = host === "127.0.0.1" && candidatePort === 4317 ? "default_loopback" : "env_or_flag";
17625
+ const healthUrl2 = resolveDaemonHealthUrl(requestedControlTowerUrl2);
17626
+ if (!healthUrl2.ok) {
17627
+ write(stderr, `${healthUrl2.error}
17622
17628
  `);
17629
+ return 1;
17630
+ }
17631
+ const existing2 = await healthChecker({ url: healthUrl2.url, token });
17632
+ if (existing2.ok && existing2.reachable) {
17633
+ selected = {
17634
+ port: candidatePort,
17635
+ requestedDaemonUrl: requestedDaemonUrl2,
17636
+ requestedControlTowerUrl: requestedControlTowerUrl2,
17637
+ healthUrl: healthUrl2.url,
17638
+ controlTowerSource: controlTowerSource2,
17639
+ existing: existing2
17640
+ };
17641
+ break;
17642
+ }
17643
+ if (existing2.statusCode === 401) {
17644
+ firstTokenMismatch ??= {
17645
+ requestedDaemonUrl: requestedDaemonUrl2,
17646
+ port: candidatePort,
17647
+ healthUrl: healthUrl2.url,
17648
+ existing: existing2
17649
+ };
17650
+ if (explicitPort) {
17651
+ write(stderr, renderDaemonTokenMismatchMessage(requestedDaemonUrl2, candidatePort));
17652
+ return 1;
17653
+ }
17654
+ continue;
17655
+ }
17656
+ selected = {
17657
+ port: candidatePort,
17658
+ requestedDaemonUrl: requestedDaemonUrl2,
17659
+ requestedControlTowerUrl: requestedControlTowerUrl2,
17660
+ healthUrl: healthUrl2.url,
17661
+ controlTowerSource: controlTowerSource2,
17662
+ existing: existing2
17663
+ };
17664
+ break;
17665
+ }
17666
+ if (!selected) {
17667
+ const mismatch = firstTokenMismatch ?? {
17668
+ requestedDaemonUrl: formatDaemonBaseUrl(host, port),
17669
+ port,
17670
+ healthUrl: `${formatDaemonBaseUrl(host, port)}/v1/guardian/admin/health`,
17671
+ existing: { ok: false, reachable: false, statusCode: 401, error: "unauthorized" }
17672
+ };
17673
+ write(stderr, renderDaemonTokenMismatchMessage(mismatch.requestedDaemonUrl, mismatch.port));
17623
17674
  return 1;
17624
17675
  }
17625
- const healthChecker = options.healthDaemon ?? requestGuardianDaemonHealth;
17626
- const existing = await healthChecker({ url: healthUrl.url, token });
17676
+ const { requestedDaemonUrl, requestedControlTowerUrl, healthUrl, controlTowerSource, existing } = selected;
17627
17677
  if (existing.ok && existing.reachable) {
17628
17678
  profile.runtime.daemon_status = "start_requested";
17629
17679
  profile.control_tower = {
@@ -17637,7 +17687,7 @@ async function runStartCommand(flags, options) {
17637
17687
  daemonStatus: profile.runtime.daemon_status,
17638
17688
  daemonUrl: requestedDaemonUrl,
17639
17689
  controlTowerUrl: requestedControlTowerUrl,
17640
- healthUrl: healthUrl.url,
17690
+ healthUrl,
17641
17691
  healthStatus: "reachable",
17642
17692
  nextCommands: ["guardian open", "guardian status", "guardian daemon pair --json"]
17643
17693
  };
@@ -17654,20 +17704,12 @@ async function runStartCommand(flags, options) {
17654
17704
  );
17655
17705
  return 0;
17656
17706
  }
17657
- if (existing.statusCode === 401) {
17658
- write(
17659
- stderr,
17660
- `Guardian daemon at ${requestedDaemonUrl} rejected the local runtime token. Run guardian status or guardian stop before starting another daemon.
17661
- `
17662
- );
17663
- return 1;
17664
- }
17665
17707
  if (!options.startDetachedDaemon) {
17666
17708
  write(stderr, "Guardian detached daemon launcher is unavailable in this runtime.\n");
17667
17709
  return 1;
17668
17710
  }
17669
- const started = await options.startDetachedDaemon({ home, host, port });
17670
- const daemonUrl = normalizeDaemonBaseUrl(started.url, host, port);
17711
+ const started = await options.startDetachedDaemon({ home, host, port: selected.port });
17712
+ const daemonUrl = normalizeDaemonBaseUrl(started.url, host, selected.port);
17671
17713
  const controlTowerUrl = `${daemonUrl}/control-tower`;
17672
17714
  profile.runtime.daemon_status = "start_requested";
17673
17715
  profile.control_tower = {
@@ -17682,7 +17724,7 @@ async function runStartCommand(flags, options) {
17682
17724
  daemonUrl,
17683
17725
  controlTowerUrl,
17684
17726
  pid: started.pid,
17685
- healthUrl: healthUrl.url,
17727
+ healthUrl,
17686
17728
  healthStatus: existing.error ? "unreachable" : "not_reachable",
17687
17729
  nextCommands: ["guardian open", "guardian status", "guardian daemon pair --json"]
17688
17730
  };
@@ -17700,6 +17742,15 @@ async function runStartCommand(flags, options) {
17700
17742
  );
17701
17743
  return 0;
17702
17744
  }
17745
+ function renderDaemonTokenMismatchMessage(requestedDaemonUrl, port) {
17746
+ return [
17747
+ `Guardian found another daemon or container at ${requestedDaemonUrl}, but it rejected this profile's local runtime token.`,
17748
+ "This usually means an older Guardian daemon or Docker demo is still using the port.",
17749
+ `Recovery: stop the other process/container, or run Guardian on a different port with guardian launch --port=${port + 1}.`,
17750
+ `Diagnostics: guardian status; docker ps --filter publish=${port}`,
17751
+ ""
17752
+ ].join("\n");
17753
+ }
17703
17754
  async function runStopCommand(flags, options) {
17704
17755
  const stdout = options.stdout ?? process.stdout;
17705
17756
  const stderr = options.stderr ?? process.stderr;
@@ -22235,7 +22286,7 @@ var init_runtime = __esm({
22235
22286
  "use strict";
22236
22287
  init_src();
22237
22288
  init_src2();
22238
- GUARDIAN_CLI_VERSION = "0.1.1";
22289
+ GUARDIAN_CLI_VERSION = "0.1.2";
22239
22290
  GUARDIAN_PROFILE_SCHEMA_VERSION = "contextecf/project-guardian-profile/v1";
22240
22291
  GUARDIAN_NPM_PACKAGE_NAME = "@contextecf/guardian-cli";
22241
22292
  GUARDIAN_GLOBAL_INSTALL_COMMAND = `npm install -g ${GUARDIAN_NPM_PACKAGE_NAME}`;
@@ -22998,6 +23049,14 @@ function detectRisk(promptManifest, policyPacks) {
22998
23049
  risks.add("pii");
22999
23050
  matchedPolicyRefs.push("guardian:builtin:pii-detection");
23000
23051
  }
23052
+ if (containsFullSsn(prompt)) {
23053
+ risks.add("pii");
23054
+ matchedPolicyRefs.push("guardian:builtin:ssn-detection");
23055
+ matchedPolicyRefs.push("guardian:builtin:ssn-full-value");
23056
+ } else if (SSN_CONTEXT_PATTERN.test(prompt)) {
23057
+ risks.add("pii");
23058
+ matchedPolicyRefs.push("guardian:builtin:ssn-detection");
23059
+ }
23001
23060
  if (FINANCIAL_PATTERN.test(prompt)) {
23002
23061
  risks.add("financial_data");
23003
23062
  matchedPolicyRefs.push("guardian:builtin:financial-data");
@@ -23029,7 +23088,7 @@ function detectRisk(promptManifest, policyPacks) {
23029
23088
  }
23030
23089
  }
23031
23090
  const detectedRisks = Array.from(risks);
23032
- const riskTier = tierForRisks(detectedRisks);
23091
+ const riskTier = hasFullSsnRisk(matchedPolicyRefs) ? "high" : tierForRisks(detectedRisks);
23033
23092
  const recommendedDisposition = recommendedDispositionForTier(
23034
23093
  riskTier,
23035
23094
  detectedRisks,
@@ -23056,6 +23115,9 @@ function decideDisposition(session, promptManifest, policyPacks, riskReport) {
23056
23115
  if (riskReport.detected_risks.includes("secret") || riskReport.detected_risks.includes("prompt_injection")) {
23057
23116
  return "block";
23058
23117
  }
23118
+ if (hasFullSsnRisk(riskReport.matched_policy_refs)) {
23119
+ return promptCoachEnabled(policyPacks) ? "rewrite" : "block";
23120
+ }
23059
23121
  const thresholds = effectiveRiskThresholds(policyPacks);
23060
23122
  if (riskTierRank(riskReport.risk_tier) >= riskTierRank(thresholds.block_at)) {
23061
23123
  return "block";
@@ -23115,6 +23177,7 @@ function buildFinalPrompt(prompt, disposition, riskReport) {
23115
23177
  );
23116
23178
  }
23117
23179
  if (riskReport.detected_risks.includes("pii")) {
23180
+ rewritten = rewritten.replace(SSN_PATTERN, "XXX-XX-$3");
23118
23181
  rewritten = rewritten.replace(EMAIL_PATTERN, "[REDACTED_EMAIL]");
23119
23182
  }
23120
23183
  return rewritten;
@@ -23128,11 +23191,12 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
23128
23191
  if (!coachDecision.should_present_card || coachDecision.intervention_level === "none") {
23129
23192
  return void 0;
23130
23193
  }
23194
+ const fullSsnRisk = hasFullSsnRisk(riskReport.matched_policy_refs);
23131
23195
  const card = {
23132
23196
  card_id: `coach:${riskReport.risk_report_id}`,
23133
23197
  intervention_level: coachDecision.intervention_level,
23134
- title: disposition === "block" ? "Guardian blocked this prompt" : disposition === "confirm" ? "Guardian needs your confirmation" : coachDecision.intervention_level === "soft_tag_along" ? "Guardian found useful context" : "Guardian can improve this prompt",
23135
- body: disposition === "confirm" ? "This prompt includes higher-risk content and needs confirmation before it leaves your machine." : coachDecision.intervention_level === "soft_tag_along" ? "This prompt may work better with a little more local context attached first." : "Guardian found context or privacy improvements that may make this prompt safer and more useful.",
23198
+ title: disposition === "block" ? "Guardian blocked this prompt" : disposition === "confirm" ? "Guardian needs your confirmation" : coachDecision.intervention_level === "soft_tag_along" ? "Guardian found useful context" : fullSsnRisk ? "Guardian found an identity number" : "Guardian can improve this prompt",
23199
+ body: disposition === "confirm" ? "This prompt includes higher-risk content and needs confirmation before it leaves your machine." : coachDecision.intervention_level === "soft_tag_along" ? "This prompt may work better with a little more local context attached first." : fullSsnRisk ? "Guardian can mask the Social Security number before anything leaves your machine." : "Guardian found context or privacy improvements that may make this prompt safer and more useful.",
23136
23200
  recommended_choice_id: disposition === "confirm" ? "confirm-after-review" : coachDecision.intervention_level === "soft_tag_along" ? "add-context" : "use-guardian-version",
23137
23201
  choices: disposition === "confirm" ? [
23138
23202
  {
@@ -23162,6 +23226,20 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
23162
23226
  description: "Send without adding context.",
23163
23227
  resulting_disposition: "allow"
23164
23228
  }
23229
+ ] : fullSsnRisk ? [
23230
+ {
23231
+ choice_id: "use-guardian-version",
23232
+ label: "Use masked prompt",
23233
+ description: "Replace the Social Security number with XXX-XX-1234 before sending.",
23234
+ recommended: true,
23235
+ resulting_disposition: "rewrite"
23236
+ },
23237
+ {
23238
+ choice_id: "cancel",
23239
+ label: "Cancel",
23240
+ description: "Do not send this prompt.",
23241
+ resulting_disposition: "block"
23242
+ }
23165
23243
  ] : [
23166
23244
  {
23167
23245
  choice_id: "use-guardian-version",
@@ -23261,6 +23339,13 @@ function promptCoachEnabled(policyPacks) {
23261
23339
  function isDestructiveTool(toolName) {
23262
23340
  return /\b(delete|drop|send|publish|pay|write|exec|shell|migration)\b/i.test(toolName);
23263
23341
  }
23342
+ function containsFullSsn(prompt) {
23343
+ SSN_PATTERN.lastIndex = 0;
23344
+ return SSN_PATTERN.test(prompt);
23345
+ }
23346
+ function hasFullSsnRisk(policyRefs) {
23347
+ return policyRefs.includes("guardian:builtin:ssn-full-value");
23348
+ }
23264
23349
  function createPolicyPackPreset(overrides) {
23265
23350
  return {
23266
23351
  version: POLICY_PACK_PRESET_VERSION,
@@ -23356,7 +23441,7 @@ function digest2(value) {
23356
23441
  function isPresent(value) {
23357
23442
  return value !== void 0;
23358
23443
  }
23359
- var DEFAULT_RISK_THRESHOLDS, POLICY_PACK_PRESET_VERSION, HIGH_RISK_TERMS, SECRET_PATTERNS, EMAIL_PATTERN, FINANCIAL_PATTERN, HEALTHCARE_PATTERN, WORK_PATTERN, PROMPT_INJECTION_PATTERN, GUARDIAN_POLICY_PACK_PRESETS;
23444
+ var DEFAULT_RISK_THRESHOLDS, POLICY_PACK_PRESET_VERSION, HIGH_RISK_TERMS, SECRET_PATTERNS, EMAIL_PATTERN, SSN_PATTERN, SSN_CONTEXT_PATTERN, FINANCIAL_PATTERN, HEALTHCARE_PATTERN, WORK_PATTERN, PROMPT_INJECTION_PATTERN, GUARDIAN_POLICY_PACK_PRESETS;
23360
23445
  var init_src3 = __esm({
23361
23446
  "packages/personal-fabric-runtime/src/index.ts"() {
23362
23447
  "use strict";
@@ -23377,6 +23462,8 @@ var init_src3 = __esm({
23377
23462
  /\b(password|secret|token)\s*[:=]\s*\S+/i
23378
23463
  ];
23379
23464
  EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
23465
+ SSN_PATTERN = /\b(?!000|666|9\d\d)(\d{3})[-\s]?(?!00)(\d{2})[-\s]?(?!0000)(\d{4})\b/g;
23466
+ SSN_CONTEXT_PATTERN = /\b(ssn|social security(?: number)?)\b/i;
23380
23467
  FINANCIAL_PATTERN = /\b(bank|wire|routing number|account number|payroll|invoice|revenue|forecast|contract value|credit card)\b/i;
23381
23468
  HEALTHCARE_PATTERN = /\b(patient|diagnosis|medication|hipaa|medical record|prescription|clinical)\b/i;
23382
23469
  WORK_PATTERN = /\b(contract|client|customer|salesforce|pipeline|renewal|board|employee|confidential)\b/i;
@@ -24810,6 +24897,7 @@ __export(daemon_exports, {
24810
24897
  buildControlTowerLocalDataStatus: () => buildControlTowerLocalDataStatus,
24811
24898
  buildControlTowerReleaseDoctorStatus: () => buildControlTowerReleaseDoctorStatus,
24812
24899
  processGuardianBrowserBridgeRequest: () => processGuardianBrowserBridgeRequest,
24900
+ processGuardianControlTowerActivityRequest: () => processGuardianControlTowerActivityRequest,
24813
24901
  processGuardianControlTowerAppPermissionRequest: () => processGuardianControlTowerAppPermissionRequest,
24814
24902
  processGuardianControlTowerDataRequest: () => processGuardianControlTowerDataRequest,
24815
24903
  processGuardianControlTowerPolicyRequest: () => processGuardianControlTowerPolicyRequest,
@@ -24973,6 +25061,14 @@ async function handleRequest(input2) {
24973
25061
  );
24974
25062
  return;
24975
25063
  }
25064
+ const activityUrl = parseGuardianUrl(input2.request.url);
25065
+ if (input2.request.method === "GET" && activityUrl?.pathname === "/v1/guardian/activity") {
25066
+ const filter = parseActivityFilter(activityUrl.searchParams.get("filter") ?? "all");
25067
+ const receipts = (await input2.store.listReceipts()).slice().sort((left, right) => right.created_at.localeCompare(left.created_at)).slice(0, CONTROL_TOWER_RECEIPT_LIMIT);
25068
+ const result2 = processGuardianControlTowerActivityRequest(receipts, filter);
25069
+ writeJson(input2.response, result2.statusCode, result2.body);
25070
+ return;
25071
+ }
24976
25072
  if (input2.request.method === "GET" && input2.request.url === "/v1/guardian/admin/health") {
24977
25073
  const result2 = processGuardianDaemonHealthRequest({
24978
25074
  token: input2.token,
@@ -25129,6 +25225,20 @@ function processGuardianDaemonHealthRequest(input2) {
25129
25225
  }
25130
25226
  };
25131
25227
  }
25228
+ function processGuardianControlTowerActivityRequest(receipts, filterValue) {
25229
+ const filter = parseActivityFilter(filterValue);
25230
+ return {
25231
+ statusCode: 200,
25232
+ body: {
25233
+ ok: true,
25234
+ schema_version: "contextecf/project-guardian-control-tower-activity/v1",
25235
+ filter,
25236
+ raw_content_included: false,
25237
+ token_printed: false,
25238
+ activities: buildActivityItems(receipts, filter)
25239
+ }
25240
+ };
25241
+ }
25132
25242
  function processGuardianDaemonStopRequest(input2) {
25133
25243
  if (!isAuthorized(input2.token, input2.suppliedToken)) {
25134
25244
  return {
@@ -25919,12 +26029,31 @@ function closeGuardianDaemonServer(server) {
25919
26029
  } catch {
25920
26030
  }
25921
26031
  }
26032
+ function parseGuardianUrl(url2) {
26033
+ if (!url2) {
26034
+ return void 0;
26035
+ }
26036
+ try {
26037
+ return new URL(url2, "http://127.0.0.1");
26038
+ } catch {
26039
+ return void 0;
26040
+ }
26041
+ }
25922
26042
  function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { nodes: [], edges: [] }, learningEvents = [], localDataStatus = createEmptyControlTowerLocalDataStatus(profile), releaseDoctorStatus = createEmptyControlTowerReleaseDoctorStatus(
25923
26043
  profile
25924
26044
  )) {
25925
26045
  const policyPacks = profile.policy_packs.available.length;
25926
26046
  const activePolicyPacks = profile.policy_packs.active.join(", ") || "none";
25927
26047
  const recentReceipts = receipts.slice().sort((left, right) => right.created_at.localeCompare(left.created_at)).slice(0, CONTROL_TOWER_RECEIPT_LIMIT);
26048
+ const blockedReceipts = recentReceipts.filter(
26049
+ (receipt) => receipt.disposition === "block"
26050
+ ).length;
26051
+ const riskyReceipts = recentReceipts.filter(
26052
+ (receipt) => receipt.disposition === "warn" || receipt.disposition === "confirm"
26053
+ ).length;
26054
+ const allowedReceipts = recentReceipts.filter(
26055
+ (receipt) => receipt.disposition === "allow"
26056
+ ).length;
25928
26057
  const receiptRows = renderReceiptRows(recentReceipts);
25929
26058
  const graphNodes = searchGraph.nodes.slice(0, CONTROL_TOWER_GRAPH_NODE_LIMIT);
25930
26059
  const graphEdges = searchGraph.edges.slice(0, CONTROL_TOWER_GRAPH_EDGE_LIMIT);
@@ -26004,6 +26133,92 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
26004
26133
  ).join("\n");
26005
26134
  const privacyDefaults = profile.privacy.privacy_mode_enabled && !profile.privacy.raw_content_capture_enabled && !profile.privacy.cloud_sync_enabled && !profile.privacy.local_app_autopilot_enabled ? "on" : "needs review";
26006
26135
  const promptCoachBudget = profile.learning_graph.prompt_coach_interruption_budget;
26136
+ const protectedState = releaseDoctorStatus.doctor.checkCounts.fail === 0 && privacyDefaults === "on";
26137
+ const protectedStateLabel = protectedState ? "You're Protected" : "Needs Review";
26138
+ const protectedStateDetail = protectedState ? "Guardian is running locally with metadata-only privacy defaults." : "Guardian is running, but one or more install checks need attention.";
26139
+ const protectionRingClass = protectedState ? "protection-ring is-protected" : "protection-ring";
26140
+ const heroMetrics = [
26141
+ {
26142
+ label: "Threats blocked",
26143
+ value: String(blockedReceipts),
26144
+ detail: "Recent metadata receipts",
26145
+ tone: "blocked"
26146
+ },
26147
+ {
26148
+ label: "Risky prompts flagged",
26149
+ value: String(riskyReceipts),
26150
+ detail: "Warn or confirmation decisions",
26151
+ tone: "flagged"
26152
+ },
26153
+ {
26154
+ label: "Safe actions approved",
26155
+ value: String(allowedReceipts),
26156
+ detail: "Allowed recent decisions",
26157
+ tone: "allowed"
26158
+ },
26159
+ {
26160
+ label: "AI tools watched",
26161
+ value: String(providerPermissionRows.length),
26162
+ detail: `${appPermissionRows.length} local app permission surface(s)`,
26163
+ tone: "watched"
26164
+ }
26165
+ ];
26166
+ const heroMetricCards = heroMetrics.map(
26167
+ (metric) => ` <article class="metric-card metric-${escapeHtml(metric.tone)}">
26168
+ <div class="metric-value">${escapeHtml(metric.value)}</div>
26169
+ <div class="metric-label">${escapeHtml(metric.label)}</div>
26170
+ <div class="metric-detail">${escapeHtml(metric.detail)}</div>
26171
+ </article>`
26172
+ ).join("\n");
26173
+ const activityFeedHtml = renderActivityFeed(recentReceipts);
26174
+ const postureCardsHtml = GUARDIAN_POSTURE_PROFILE_CATALOG.map((postureProfile) => {
26175
+ const selected = profile.posture_profile.selected === postureProfile.id;
26176
+ const icon = postureProfile.id === "calm" ? "&#9790;" : postureProfile.id === "balanced" ? "&#9878;" : postureProfile.id === "guided" ? "&#127891;" : postureProfile.id === "high_assurance" ? "&#128274;" : "&#9881;";
26177
+ return ` <form class="posture-card${selected ? " is-active" : ""}" method="post" action="/v1/guardian/admin/preferences">
26178
+ <input type="hidden" name="postureProfileId" value="${escapeHtml(postureProfile.id)}">
26179
+ <button type="submit" class="posture-button" ${selected || profile.posture_profile.managed ? "disabled" : ""}>
26180
+ <span class="posture-icon">${icon}</span>
26181
+ <span class="posture-title">${escapeHtml(postureProfile.displayName)}</span>
26182
+ <span class="posture-summary">${escapeHtml(postureProfile.summary)}</span>
26183
+ ${selected ? '<span class="posture-state">ACTIVE</span>' : '<span class="posture-state">Use this setting</span>'}
26184
+ </button>
26185
+ </form>`;
26186
+ }).join("\n");
26187
+ const privacySettingRowsHtml = [
26188
+ {
26189
+ label: "Privacy mode",
26190
+ detail: "Never stores your actual prompts",
26191
+ enabled: profile.privacy.privacy_mode_enabled,
26192
+ control: renderOutcomePrivacyModeControl(profile.privacy.privacy_mode_enabled)
26193
+ },
26194
+ {
26195
+ label: "Prompt coaching",
26196
+ detail: "Tips to improve your AI requests",
26197
+ enabled: profile.prompt_coach_mode !== "off",
26198
+ control: renderOutcomePromptCoachControl(profile.prompt_coach_mode)
26199
+ },
26200
+ {
26201
+ label: "Stays on this device",
26202
+ detail: "All data kept local, never synced",
26203
+ enabled: !profile.privacy.cloud_sync_enabled,
26204
+ control: '<span class="toggle is-on" aria-label="Stays on this device on"><span></span></span>'
26205
+ }
26206
+ ].map(
26207
+ (setting) => ` <div class="setting-row">
26208
+ <div>
26209
+ <h3>${escapeHtml(setting.label)}</h3>
26210
+ <p>${escapeHtml(setting.detail)}</p>
26211
+ </div>
26212
+ ${setting.control}
26213
+ </div>`
26214
+ ).join("\n");
26215
+ const providerPillsHtml = providerPermissionRows.filter((row) => row.state !== "revoked").map(
26216
+ (row) => ` <form method="post" action="/v1/guardian/admin/provider-permission" class="provider-pill-form">
26217
+ <input type="hidden" name="action" value="revoke">
26218
+ <input type="hidden" name="providerId" value="${escapeHtml(row.id)}">
26219
+ <button type="submit" class="provider-pill" aria-label="Revoke ${escapeHtml(providerDisplayName(row.id))}"><span></span>${escapeHtml(providerDisplayName(row.id))}</button>
26220
+ </form>`
26221
+ ).join("\n");
26007
26222
  const rows = [
26008
26223
  ["Profile", profile.profile_dir],
26009
26224
  ["Daemon", profile.runtime.daemon_status],
@@ -26056,53 +26271,464 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
26056
26271
  <meta name="viewport" content="width=device-width, initial-scale=1">
26057
26272
  <title>Project Guardian Control Tower</title>
26058
26273
  <style>
26059
- :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
26060
- body { margin: 0; background: #f7f8f3; color: #171916; }
26061
- main { max-width: 860px; margin: 0 auto; padding: 40px 20px; }
26062
- h1 { margin: 0 0 8px; font-size: 32px; line-height: 1.1; }
26063
- h2 { margin: 28px 0 8px; font-size: 20px; line-height: 1.2; }
26064
- p { margin: 0 0 24px; color: #4d5249; }
26065
- section { margin-top: 28px; }
26066
- table { width: 100%; border-collapse: collapse; background: #ffffff; border: 1px solid #d8ddd1; }
26067
- th, td { padding: 12px 14px; border-bottom: 1px solid #e7eadf; text-align: left; vertical-align: top; }
26068
- th { width: 190px; color: #2f372b; font-weight: 650; }
26069
- code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
26274
+ :root {
26275
+ color-scheme: dark;
26276
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26277
+ background: #070c16;
26278
+ color: #f6f8ff;
26279
+ }
26280
+ * { box-sizing: border-box; }
26281
+ body {
26282
+ min-height: 100vh;
26283
+ margin: 0;
26284
+ background:
26285
+ radial-gradient(circle at 18% 12%, rgba(46, 184, 122, 0.16), transparent 30%),
26286
+ radial-gradient(circle at 85% 18%, rgba(79, 126, 255, 0.12), transparent 32%),
26287
+ linear-gradient(180deg, #0a111f 0%, #070c16 52%, #050812 100%);
26288
+ color: #f6f8ff;
26289
+ }
26290
+ body::before {
26291
+ position: fixed;
26292
+ inset: 0;
26293
+ z-index: -1;
26294
+ content: "";
26295
+ background-image:
26296
+ linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
26297
+ linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
26298
+ background-size: 44px 44px;
26299
+ mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), transparent 72%);
26300
+ }
26301
+ main { max-width: 1180px; margin: 0 auto; padding: 104px 20px 56px; }
26302
+ h1 { margin: 0 0 10px; font-size: clamp(34px, 5vw, 64px); line-height: 1; letter-spacing: 0; }
26303
+ h2 { margin: 0 0 10px; font-size: 22px; line-height: 1.2; letter-spacing: 0; }
26304
+ p { margin: 0 0 20px; color: #aab4c5; }
26305
+ section {
26306
+ margin-top: 24px;
26307
+ padding: 22px;
26308
+ border: 1px solid rgba(148, 163, 184, 0.18);
26309
+ border-radius: 8px;
26310
+ background: rgba(12, 20, 34, 0.82);
26311
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.22);
26312
+ }
26313
+ table { width: 100%; border-collapse: collapse; overflow: hidden; border: 1px solid rgba(148, 163, 184, 0.15); border-radius: 8px; background: rgba(6, 12, 24, 0.62); }
26314
+ th, td { padding: 12px 14px; border-bottom: 1px solid rgba(148, 163, 184, 0.13); text-align: left; vertical-align: top; }
26315
+ th { width: 210px; color: #d8e1f0; font-weight: 650; }
26316
+ td { color: #c8d2e3; overflow-wrap: anywhere; }
26317
+ code { padding: 2px 5px; border-radius: 6px; background: rgba(15, 23, 42, 0.9); color: #d8f8e7; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
26070
26318
  form { margin: 0; }
26071
- button { min-width: 96px; min-height: 34px; border: 1px solid #7b8d70; border-radius: 6px; background: #eef5e9; color: #172211; font: inherit; font-weight: 650; cursor: pointer; }
26072
- button:hover { background: #e2eed8; }
26319
+ button { min-width: 96px; min-height: 34px; border: 1px solid rgba(69, 201, 126, 0.44); border-radius: 8px; background: rgba(24, 96, 62, 0.62); color: #ecfff4; font: inherit; font-weight: 650; cursor: pointer; }
26320
+ button:hover { background: rgba(39, 132, 82, 0.72); }
26073
26321
  button[disabled] { cursor: default; opacity: 0.56; }
26322
+ input {
26323
+ min-height: 34px;
26324
+ max-width: 100%;
26325
+ border: 1px solid rgba(148, 163, 184, 0.24);
26326
+ border-radius: 8px;
26327
+ background: rgba(8, 13, 24, 0.92);
26328
+ color: #f6f8ff;
26329
+ padding: 7px 9px;
26330
+ }
26074
26331
  details { max-width: 100%; }
26075
- summary { cursor: pointer; font-weight: 650; }
26332
+ summary { cursor: pointer; font-weight: 650; color: #eff6ff; }
26076
26333
  dl { display: grid; grid-template-columns: minmax(120px, 180px) 1fr; gap: 8px 14px; margin: 12px 0 0; }
26077
- dt { color: #596052; font-weight: 650; }
26078
- dd { margin: 0; overflow-wrap: anywhere; }
26334
+ dt { color: #8fa0b7; font-weight: 650; }
26335
+ dd { margin: 0; overflow-wrap: anywhere; color: #c8d2e3; }
26079
26336
  tr:last-child th, tr:last-child td { border-bottom: 0; }
26080
- .badge { display: inline-block; margin-bottom: 16px; padding: 5px 9px; border: 1px solid #91a582; color: #26341f; background: #e8f1df; font-size: 13px; }
26081
- @media (prefers-color-scheme: dark) {
26082
- body { background: #10130f; color: #f2f4ee; }
26083
- p { color: #bdc5b6; }
26084
- table { background: #181d16; border-color: #333b2f; }
26085
- th, td { border-bottom-color: #2b3228; }
26086
- th { color: #dce6d2; }
26087
- dt { color: #aeb8a6; }
26088
- button { background: #263520; border-color: #58724d; color: #f2f7ec; }
26089
- button:hover { background: #314629; }
26090
- .badge { background: #20311d; border-color: #496642; color: #dbeed0; }
26337
+ .topbar {
26338
+ position: fixed;
26339
+ top: 0;
26340
+ right: 0;
26341
+ left: 0;
26342
+ z-index: 10;
26343
+ display: flex;
26344
+ align-items: center;
26345
+ justify-content: space-between;
26346
+ min-height: 68px;
26347
+ padding: 0 24px;
26348
+ border-bottom: 1px solid rgba(148, 163, 184, 0.13);
26349
+ background: rgba(7, 12, 22, 0.78);
26350
+ backdrop-filter: blur(18px);
26351
+ }
26352
+ .brand, .topbar-actions, .status-pill, .activity-heading, .filter-chips {
26353
+ display: flex;
26354
+ align-items: center;
26355
+ }
26356
+ .brand { gap: 10px; color: #f8fbff; font-size: 18px; font-weight: 750; }
26357
+ .brand-mark {
26358
+ display: inline-grid;
26359
+ place-items: center;
26360
+ width: 34px;
26361
+ height: 34px;
26362
+ border-radius: 8px;
26363
+ background: linear-gradient(145deg, #22c55e, #128452);
26364
+ color: #02120b;
26365
+ font-weight: 900;
26366
+ box-shadow: 0 0 22px rgba(34, 197, 94, 0.34);
26367
+ }
26368
+ .topbar-actions { gap: 10px; }
26369
+ .status-pill {
26370
+ gap: 8px;
26371
+ min-height: 34px;
26372
+ padding: 0 12px;
26373
+ border: 1px solid rgba(34, 197, 94, 0.26);
26374
+ border-radius: 999px;
26375
+ background: rgba(10, 40, 28, 0.86);
26376
+ color: #c9f8d8;
26377
+ font-size: 14px;
26378
+ font-weight: 650;
26379
+ }
26380
+ .status-dot { width: 8px; height: 8px; border-radius: 999px; background: #22c55e; box-shadow: 0 0 14px rgba(34, 197, 94, 0.9); }
26381
+ .icon-button {
26382
+ display: inline-grid;
26383
+ place-items: center;
26384
+ width: 34px;
26385
+ height: 34px;
26386
+ border: 1px solid rgba(148, 163, 184, 0.2);
26387
+ border-radius: 8px;
26388
+ color: #d8e1f0;
26389
+ background: rgba(15, 23, 42, 0.78);
26390
+ font-size: 16px;
26391
+ }
26392
+ .eyebrow {
26393
+ width: fit-content;
26394
+ margin-bottom: 16px;
26395
+ padding: 6px 10px;
26396
+ border: 1px solid rgba(46, 184, 122, 0.35);
26397
+ border-radius: 999px;
26398
+ color: #9cf2bf;
26399
+ background: rgba(16, 82, 55, 0.45);
26400
+ font-size: 13px;
26401
+ font-weight: 700;
26402
+ }
26403
+ .intro { max-width: 720px; font-size: 17px; line-height: 1.55; }
26404
+ .hero-grid {
26405
+ display: grid;
26406
+ grid-template-columns: minmax(280px, 1.05fr) minmax(320px, 0.95fr);
26407
+ gap: 18px;
26408
+ margin-top: 28px;
26409
+ }
26410
+ .protection-card, .metric-card {
26411
+ border: 1px solid rgba(148, 163, 184, 0.17);
26412
+ border-radius: 8px;
26413
+ background: linear-gradient(180deg, rgba(13, 24, 40, 0.92), rgba(8, 14, 26, 0.94));
26414
+ box-shadow: 0 22px 80px rgba(0, 0, 0, 0.28);
26415
+ }
26416
+ .protection-card {
26417
+ display: grid;
26418
+ justify-items: center;
26419
+ align-content: center;
26420
+ min-height: 372px;
26421
+ padding: 38px 24px;
26422
+ text-align: center;
26423
+ }
26424
+ .protection-card h2 { margin-top: 22px; font-size: 30px; }
26425
+ .protection-card p { max-width: 420px; margin-bottom: 0; }
26426
+ .protection-ring {
26427
+ display: grid;
26428
+ place-items: center;
26429
+ width: min(56vw, 218px);
26430
+ aspect-ratio: 1;
26431
+ border: 1px solid rgba(245, 158, 11, 0.48);
26432
+ border-radius: 999px;
26433
+ background:
26434
+ radial-gradient(circle, rgba(245, 158, 11, 0.22) 0%, rgba(245, 158, 11, 0.06) 52%, transparent 70%),
26435
+ conic-gradient(from 20deg, rgba(245, 158, 11, 0.08), rgba(245, 158, 11, 0.8), rgba(245, 158, 11, 0.08));
26436
+ box-shadow: 0 0 64px rgba(245, 158, 11, 0.22);
26437
+ }
26438
+ .protection-ring.is-protected {
26439
+ border-color: rgba(34, 197, 94, 0.48);
26440
+ background:
26441
+ radial-gradient(circle, rgba(34, 197, 94, 0.22) 0%, rgba(34, 197, 94, 0.06) 52%, transparent 70%),
26442
+ conic-gradient(from 20deg, rgba(34, 197, 94, 0.08), rgba(34, 197, 94, 0.86), rgba(34, 197, 94, 0.08));
26443
+ box-shadow: 0 0 72px rgba(34, 197, 94, 0.32);
26444
+ }
26445
+ .ring-core {
26446
+ display: grid;
26447
+ place-items: center;
26448
+ width: 84px;
26449
+ aspect-ratio: 1;
26450
+ border: 1px solid rgba(255, 255, 255, 0.12);
26451
+ border-radius: 999px;
26452
+ background: linear-gradient(145deg, #22c55e, #0f8b57);
26453
+ color: #03140b;
26454
+ font-size: 34px;
26455
+ font-weight: 950;
26456
+ }
26457
+ .metric-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
26458
+ .metric-card { min-height: 176px; padding: 22px; }
26459
+ .metric-value { margin-bottom: 8px; color: #e6fff0; font-size: 46px; line-height: 1; font-weight: 850; }
26460
+ .metric-label { color: #f5f7fb; font-size: 16px; font-weight: 760; }
26461
+ .metric-detail { margin-top: 8px; color: #91a1b8; font-size: 13px; line-height: 1.35; }
26462
+ .metric-blocked .metric-value { color: #ff8b8b; }
26463
+ .metric-flagged .metric-value { color: #ffd166; }
26464
+ .metric-allowed .metric-value { color: #70f0a3; }
26465
+ .metric-watched .metric-value { color: #8cc7ff; }
26466
+ .activity-list { display: grid; gap: 14px; }
26467
+ .activity-card {
26468
+ display: grid;
26469
+ grid-template-columns: 64px 1fr auto;
26470
+ gap: 18px;
26471
+ align-items: center;
26472
+ min-height: 96px;
26473
+ padding: 18px 22px;
26474
+ border: 1px solid rgba(148, 163, 184, 0.16);
26475
+ border-radius: 8px;
26476
+ background: rgba(10, 18, 32, 0.82);
26477
+ }
26478
+ .activity-card.block { border-color: rgba(244, 63, 94, 0.26); background: rgba(43, 12, 32, 0.7); }
26479
+ .activity-card.warn, .activity-card.confirm { border-color: rgba(245, 158, 11, 0.28); background: rgba(42, 34, 22, 0.62); }
26480
+ .activity-card.allow, .activity-card.rewrite { border-color: rgba(34, 197, 94, 0.24); background: rgba(7, 36, 32, 0.64); }
26481
+ .activity-icon {
26482
+ display: grid;
26483
+ place-items: center;
26484
+ width: 48px;
26485
+ height: 48px;
26486
+ border-radius: 8px;
26487
+ background: rgba(255, 255, 255, 0.06);
26488
+ font-size: 24px;
26489
+ }
26490
+ .activity-title { color: #f5f7fb; font-size: 17px; font-weight: 780; }
26491
+ .activity-meta { margin-top: 6px; color: #91a1b8; font-size: 14px; font-weight: 650; }
26492
+ .activity-badge {
26493
+ padding: 7px 12px;
26494
+ border: 1px solid rgba(148, 163, 184, 0.22);
26495
+ border-radius: 999px;
26496
+ color: #d8e1f0;
26497
+ font-size: 13px;
26498
+ font-weight: 850;
26499
+ }
26500
+ .activity-badge.block { border-color: rgba(244, 63, 94, 0.42); color: #ff5c7a; background: rgba(244, 63, 94, 0.12); }
26501
+ .activity-badge.warn, .activity-badge.confirm { border-color: rgba(245, 158, 11, 0.42); color: #ffb020; background: rgba(245, 158, 11, 0.12); }
26502
+ .activity-badge.allow, .activity-badge.rewrite { border-color: rgba(34, 197, 94, 0.36); color: #38e678; background: rgba(34, 197, 94, 0.11); }
26503
+ .watch-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.65fr); gap: 22px; }
26504
+ .posture-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
26505
+ .posture-card, .settings-card {
26506
+ min-height: 178px;
26507
+ padding: 22px;
26508
+ border: 1px solid rgba(148, 163, 184, 0.18);
26509
+ border-radius: 8px;
26510
+ background: rgba(11, 24, 40, 0.86);
26511
+ }
26512
+ .posture-card.is-active { border-color: rgba(34, 197, 94, 0.48); background: rgba(6, 45, 37, 0.82); }
26513
+ .posture-button {
26514
+ display: grid;
26515
+ width: 100%;
26516
+ min-width: 0;
26517
+ min-height: 0;
26518
+ padding: 0;
26519
+ border: 0;
26520
+ border-radius: 0;
26521
+ background: transparent;
26522
+ color: inherit;
26523
+ text-align: left;
26524
+ cursor: pointer;
26525
+ }
26526
+ .posture-button:hover { background: transparent; }
26527
+ .posture-button[disabled] { opacity: 1; }
26528
+ .posture-icon { min-height: 32px; color: #e5eefb; font-size: 28px; }
26529
+ .posture-title, .settings-card h3 { margin: 10px 0 8px; color: #f6f8ff; font-size: 19px; font-weight: 820; }
26530
+ .posture-summary, .settings-card p { margin: 0; color: #aab4c5; line-height: 1.5; }
26531
+ .posture-state { margin-top: 14px; color: #38e678; font-size: 13px; font-weight: 900; }
26532
+ .settings-stack { display: grid; gap: 16px; }
26533
+ .setting-row { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
26534
+ .setting-row h3 { margin: 0 0 4px; }
26535
+ .toggle-form { margin: 0; }
26536
+ .toggle {
26537
+ display: inline-flex;
26538
+ align-items: center;
26539
+ width: 56px;
26540
+ height: 32px;
26541
+ min-width: 56px;
26542
+ min-height: 32px;
26543
+ padding: 4px;
26544
+ border: 0;
26545
+ border-radius: 999px;
26546
+ background: rgba(71, 85, 105, 0.55);
26547
+ cursor: pointer;
26548
+ }
26549
+ .toggle:hover { background: rgba(71, 85, 105, 0.7); }
26550
+ .toggle span { width: 24px; height: 24px; border-radius: 999px; background: #d8e1f0; }
26551
+ .toggle.is-on { justify-content: flex-end; background: #22c55e; box-shadow: 0 0 24px rgba(34, 197, 94, 0.28); }
26552
+ .toggle.is-on:hover { background: #27d768; }
26553
+ .toggle[disabled] { cursor: default; opacity: 1; }
26554
+ .provider-list { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
26555
+ .provider-pill-form { margin: 0; }
26556
+ .provider-pill {
26557
+ display: inline-flex;
26558
+ align-items: center;
26559
+ gap: 8px;
26560
+ min-width: 0;
26561
+ min-height: 0;
26562
+ padding: 8px 12px;
26563
+ border: 1px solid rgba(148, 163, 184, 0.22);
26564
+ border-radius: 999px;
26565
+ color: #d8e1f0;
26566
+ background: rgba(15, 23, 42, 0.52);
26567
+ font: inherit;
26568
+ font-weight: 750;
26569
+ cursor: pointer;
26570
+ }
26571
+ .provider-pill:hover { background: rgba(30, 41, 59, 0.78); }
26572
+ .provider-pill span { width: 8px; height: 8px; border-radius: 999px; background: #22c55e; }
26573
+ .activity-heading { justify-content: space-between; gap: 16px; margin-bottom: 12px; }
26574
+ .activity-heading h2 { margin: 0; }
26575
+ .filter-chips { gap: 8px; flex-wrap: wrap; }
26576
+ .chip {
26577
+ min-width: auto;
26578
+ min-height: 36px;
26579
+ padding: 6px 10px;
26580
+ border: 1px solid rgba(148, 163, 184, 0.19);
26581
+ border-radius: 999px;
26582
+ color: #aab4c5;
26583
+ background: rgba(15, 23, 42, 0.64);
26584
+ font: inherit;
26585
+ font-size: 13px;
26586
+ font-weight: 700;
26587
+ cursor: pointer;
26588
+ }
26589
+ .chip:hover { color: #f5f7fb; background: rgba(30, 41, 59, 0.78); }
26590
+ .chip.is-active, .chip[aria-pressed="true"] { border-color: rgba(34, 197, 94, 0.38); color: #d7ffe5; background: rgba(21, 128, 61, 0.22); }
26591
+ .activity-card[hidden], .empty-filter-state[hidden] { display: none; }
26592
+ .empty-filter-state {
26593
+ padding: 22px;
26594
+ border: 1px dashed rgba(148, 163, 184, 0.24);
26595
+ border-radius: 8px;
26596
+ color: #aab4c5;
26597
+ background: rgba(15, 23, 42, 0.42);
26598
+ }
26599
+ .detail-panel {
26600
+ margin-top: 24px;
26601
+ border: 1px solid rgba(148, 163, 184, 0.18);
26602
+ border-radius: 8px;
26603
+ background: rgba(12, 20, 34, 0.72);
26604
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
26605
+ }
26606
+ .detail-panel > summary {
26607
+ display: flex;
26608
+ align-items: center;
26609
+ justify-content: space-between;
26610
+ gap: 16px;
26611
+ padding: 18px 20px;
26612
+ list-style: none;
26613
+ }
26614
+ .detail-panel > summary::-webkit-details-marker { display: none; }
26615
+ .detail-panel > summary span { color: #f6f8ff; font-size: 18px; font-weight: 820; }
26616
+ .detail-panel > summary small { color: #91a1b8; font-size: 13px; line-height: 1.35; text-align: right; }
26617
+ .detail-panel > summary::after {
26618
+ content: "+";
26619
+ display: grid;
26620
+ place-items: center;
26621
+ width: 28px;
26622
+ height: 28px;
26623
+ flex: 0 0 auto;
26624
+ border: 1px solid rgba(148, 163, 184, 0.2);
26625
+ border-radius: 8px;
26626
+ color: #d8e1f0;
26627
+ }
26628
+ .detail-panel[open] > summary::after { content: "-"; }
26629
+ .panel-body { padding: 0 18px 18px; }
26630
+ .panel-body section:first-child { margin-top: 0; }
26631
+ .visually-hidden {
26632
+ position: absolute;
26633
+ width: 1px;
26634
+ height: 1px;
26635
+ padding: 0;
26636
+ margin: -1px;
26637
+ overflow: hidden;
26638
+ clip: rect(0, 0, 0, 0);
26639
+ white-space: nowrap;
26640
+ border: 0;
26641
+ }
26642
+ @media (max-width: 820px) {
26643
+ main { padding: 92px 14px 40px; }
26644
+ .topbar { padding: 0 14px; }
26645
+ .hero-grid, .metric-grid, .watch-grid, .posture-grid { grid-template-columns: 1fr; }
26646
+ .protection-card { min-height: 320px; }
26647
+ .activity-card { grid-template-columns: 48px 1fr; }
26648
+ .activity-badge { grid-column: 2; width: fit-content; }
26649
+ section { padding: 16px; overflow-x: auto; }
26650
+ table { min-width: 720px; }
26651
+ .activity-heading { align-items: flex-start; flex-direction: column; }
26652
+ .detail-panel > summary { align-items: flex-start; flex-direction: column; }
26653
+ .detail-panel > summary small { text-align: left; }
26091
26654
  }
26092
26655
  </style>
26093
26656
  </head>
26094
26657
  <body>
26658
+ <header class="topbar">
26659
+ <div class="brand"><span class="brand-mark">G</span><span>Guardian</span></div>
26660
+ <div class="topbar-actions">
26661
+ <span class="status-pill"><span class="status-dot"></span>${escapeHtml(profile.runtime.daemon_status)}</span>
26662
+ <span class="icon-button" aria-label="Settings">&#9881;</span>
26663
+ </div>
26664
+ </header>
26095
26665
  <main>
26096
- <div class="badge">Local-first status</div>
26666
+ <div class="eyebrow">Local-first status</div>
26097
26667
  <h1>Project Guardian Control Tower</h1>
26098
- <p>This lightweight local page confirms Guardian is running. It shows metadata only and never exposes the runtime token, raw prompts, or raw responses.</p>
26099
- <table aria-label="Guardian status">
26100
- <tbody>
26668
+ <p class="intro">This local page confirms Guardian is watching your AI surface with privacy-first, metadata-only governance. It never exposes the runtime token, raw prompts, raw responses, or local action payloads.</p>
26669
+ <div class="hero-grid" aria-label="Guardian protection overview">
26670
+ <article class="protection-card">
26671
+ <div class="${protectionRingClass}"><div class="ring-core">G</div></div>
26672
+ <h2>${escapeHtml(protectedStateLabel)}</h2>
26673
+ <p>${escapeHtml(protectedStateDetail)}</p>
26674
+ </article>
26675
+ <div class="metric-grid">
26676
+ ${heroMetricCards}
26677
+ </div>
26678
+ </div>
26679
+ <section aria-labelledby="receipt-feed">
26680
+ <div class="activity-heading">
26681
+ <h2 id="receipt-feed">What Guardian did today</h2>
26682
+ <div class="filter-chips" aria-label="Activity filters">
26683
+ <button type="button" class="chip is-active" data-activity-filter="all" aria-pressed="true">All</button>
26684
+ <button type="button" class="chip" data-activity-filter="blocked" aria-pressed="false">Blocked</button>
26685
+ <button type="button" class="chip" data-activity-filter="flagged" aria-pressed="false">Flagged</button>
26686
+ <button type="button" class="chip" data-activity-filter="allowed" aria-pressed="false">Allowed</button>
26687
+ </div>
26688
+ </div>
26689
+ <div class="activity-list" data-activity-list>
26690
+ ${activityFeedHtml}
26691
+ <div class="empty-filter-state" data-activity-empty hidden>No activity matches this filter yet.</div>
26692
+ </div>
26693
+ </section>
26694
+ <section aria-labelledby="watch-level">
26695
+ <h2 id="watch-level">How closely should Guardian watch?</h2>
26696
+ <div class="watch-grid">
26697
+ <div class="posture-grid">
26698
+ ${postureCardsHtml}
26699
+ </div>
26700
+ <div class="settings-stack">
26701
+ <article class="settings-card">
26702
+ <h2>Your privacy settings</h2>
26703
+ ${privacySettingRowsHtml}
26704
+ </article>
26705
+ <article class="settings-card">
26706
+ <h2>AI tools being watched</h2>
26707
+ <p>Guardian monitors these in real time.</p>
26708
+ <div class="provider-list">
26709
+ ${providerPillsHtml}
26710
+ </div>
26711
+ </article>
26712
+ </div>
26713
+ </div>
26714
+ </section>
26715
+ <details class="detail-panel" aria-labelledby="technical-details-summary">
26716
+ <summary id="technical-details-summary">
26717
+ <span>Technical details</span>
26718
+ <small>Runtime status, policy packs, permissions, local data, release evidence, receipts, Search Graph, and Learning Graph.</small>
26719
+ </summary>
26720
+ <div class="panel-body">
26721
+ <section aria-labelledby="install-snapshot">
26722
+ <h2 id="install-snapshot">Install Snapshot</h2>
26723
+ <p>Quick local runtime, privacy, policy, release, and graph status.</p>
26724
+ <table aria-label="Guardian status">
26725
+ <tbody>
26101
26726
  ${rows.map(
26102
26727
  ([label, value]) => ` <tr><th scope="row">${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`
26103
26728
  ).join("\n")}
26104
- </tbody>
26105
- </table>
26729
+ </tbody>
26730
+ </table>
26731
+ </section>
26106
26732
  <section aria-labelledby="privacy-posture">
26107
26733
  <h2 id="privacy-posture">Privacy Posture</h2>
26108
26734
  <p>Review Guardian's local-first privacy defaults and pending privacy controls without exposing runtime tokens, raw prompts, raw responses, or local action payloads.</p>
@@ -26229,8 +26855,16 @@ ${releaseDoctorRows}
26229
26855
  </table>
26230
26856
  </section>
26231
26857
  <section aria-labelledby="receipt-drilldowns">
26232
- <h2 id="receipt-drilldowns">Activity &amp; Receipts</h2>
26233
- <p>Inspect recent Guardian decisions without raw prompt, response, or local-action payload content.</p>
26858
+ <div class="activity-heading">
26859
+ <h2 id="receipt-drilldowns">What Guardian did today</h2>
26860
+ <div class="filter-chips" aria-label="Activity filters">
26861
+ <span class="chip is-active">All</span>
26862
+ <span class="chip">Blocked</span>
26863
+ <span class="chip">Flagged</span>
26864
+ <span class="chip">Allowed</span>
26865
+ </div>
26866
+ </div>
26867
+ <p><span class="visually-hidden">Activity &amp; Receipts.</span>Inspect recent Guardian decisions without raw prompt, response, or local-action payload content.</p>
26234
26868
  <table aria-label="Guardian receipt drilldowns">
26235
26869
  <thead>
26236
26870
  <tr><th scope="col">Receipt</th><th scope="col">Type</th><th scope="col">Decision</th><th scope="col">Details</th></tr>
@@ -26308,7 +26942,79 @@ ${learningEventRows}
26308
26942
  </tbody>
26309
26943
  </table>
26310
26944
  </section>
26945
+ </div>
26946
+ </details>
26311
26947
  </main>
26948
+ <script>
26949
+ (() => {
26950
+ const filters = Array.from(document.querySelectorAll('[data-activity-filter]'));
26951
+ const list = document.querySelector('[data-activity-list]');
26952
+ const emptyState = document.querySelector('[data-activity-empty]');
26953
+ const setSelectedFilter = (selectedFilter) => {
26954
+ for (const filter of filters) {
26955
+ const selected = filter.getAttribute('data-activity-filter') === selectedFilter;
26956
+ filter.classList.toggle('is-active', selected);
26957
+ filter.setAttribute('aria-pressed', selected ? 'true' : 'false');
26958
+ }
26959
+ };
26960
+ const createActivityCard = (activity) => {
26961
+ const card = document.createElement('article');
26962
+ card.className = 'activity-card ' + activity.tone;
26963
+ card.setAttribute('data-activity-state', activity.state);
26964
+ const icon = document.createElement('div');
26965
+ icon.className = 'activity-icon';
26966
+ icon.textContent = activity.icon;
26967
+ const body = document.createElement('div');
26968
+ const title = document.createElement('div');
26969
+ title.className = 'activity-title';
26970
+ title.textContent = activity.title;
26971
+ const meta = document.createElement('div');
26972
+ meta.className = 'activity-meta';
26973
+ meta.textContent = activity.meta;
26974
+ body.append(title, meta);
26975
+ const badge = document.createElement('div');
26976
+ badge.className = 'activity-badge ' + activity.tone;
26977
+ badge.textContent = activity.label;
26978
+ card.append(icon, body, badge);
26979
+ return card;
26980
+ };
26981
+ const renderActivities = (activities) => {
26982
+ if (!list) return;
26983
+ for (const card of Array.from(list.querySelectorAll('[data-activity-state]'))) {
26984
+ card.remove();
26985
+ }
26986
+ for (const activity of activities) {
26987
+ list.insertBefore(createActivityCard(activity), emptyState);
26988
+ }
26989
+ if (emptyState) emptyState.hidden = activities.length > 0;
26990
+ };
26991
+ const applyFilter = async (selectedFilter) => {
26992
+ setSelectedFilter(selectedFilter);
26993
+ const controller = new AbortController();
26994
+ const timeoutId = window.setTimeout(() => controller.abort(), 2500);
26995
+ try {
26996
+ const response = await fetch('/v1/guardian/activity?filter=' + encodeURIComponent(selectedFilter), {
26997
+ headers: { accept: 'application/json' },
26998
+ signal: controller.signal,
26999
+ });
27000
+ if (!response.ok) return;
27001
+ const body = await response.json();
27002
+ if (body && body.ok && Array.isArray(body.activities)) {
27003
+ renderActivities(body.activities);
27004
+ }
27005
+ } catch {
27006
+ // Keep the server-rendered activity feed if a local refresh is unavailable.
27007
+ } finally {
27008
+ window.clearTimeout(timeoutId);
27009
+ }
27010
+ };
27011
+ for (const filter of filters) {
27012
+ filter.addEventListener('click', () => {
27013
+ applyFilter(filter.getAttribute('data-activity-filter') || 'all');
27014
+ });
27015
+ }
27016
+ })();
27017
+ </script>
26312
27018
  </body>
26313
27019
  </html>
26314
27020
  `;
@@ -26322,6 +27028,24 @@ function renderDataPathForm(action, pathName, label) {
26322
27028
  "</form>"
26323
27029
  ].join("");
26324
27030
  }
27031
+ function renderOutcomePrivacyModeControl(enabled) {
27032
+ return [
27033
+ '<form method="post" action="/v1/guardian/admin/preferences" class="toggle-form">',
27034
+ ' <input type="hidden" name="privacyModeEnabled" value="on">',
27035
+ ` <button type="submit" class="toggle${enabled ? " is-on" : ""}" aria-label="Privacy mode ${enabled ? "on" : "off"}" ${enabled ? "disabled" : ""}><span></span></button>`,
27036
+ "</form>"
27037
+ ].join("");
27038
+ }
27039
+ function renderOutcomePromptCoachControl(mode) {
27040
+ const enabled = mode !== "off";
27041
+ const nextMode = enabled ? "off" : "balanced";
27042
+ return [
27043
+ '<form method="post" action="/v1/guardian/admin/preferences" class="toggle-form">',
27044
+ ` <input type="hidden" name="promptCoachMode" value="${escapeHtml(nextMode)}">`,
27045
+ ` <button type="submit" class="toggle${enabled ? " is-on" : ""}" aria-label="Prompt coaching ${enabled ? "on" : "off"}"><span></span></button>`,
27046
+ "</form>"
27047
+ ].join("");
27048
+ }
26325
27049
  function renderDataConfirmForm(action, pathName, confirmation, label) {
26326
27050
  return [
26327
27051
  '<form method="post" action="/v1/guardian/admin/data">',
@@ -26660,10 +27384,105 @@ function renderReceiptRows(receipts) {
26660
27384
  }
26661
27385
  return receipts.map((receipt) => renderReceiptRow(receipt)).join("\n");
26662
27386
  }
27387
+ function renderActivityFeed(receipts) {
27388
+ if (receipts.length === 0) {
27389
+ return ` <article class="activity-card allow" data-activity-state="allowed">
27390
+ <div class="activity-icon">&#9989;</div>
27391
+ <div>
27392
+ <div class="activity-title">No risky activity recorded yet</div>
27393
+ <div class="activity-meta">Guardian is watching locally</div>
27394
+ </div>
27395
+ <div class="activity-badge allow">READY</div>
27396
+ </article>`;
27397
+ }
27398
+ return receipts.map((receipt) => renderActivityCard(receipt)).join("\n");
27399
+ }
27400
+ function parseActivityFilter(value) {
27401
+ if (value === "blocked" || value === "flagged" || value === "allowed") {
27402
+ return value;
27403
+ }
27404
+ return "all";
27405
+ }
27406
+ function buildActivityItems(receipts, filter) {
27407
+ return receipts.filter((receipt) => {
27408
+ const group = activityFilterGroup(receipt.disposition);
27409
+ return filter === "all" || group === filter;
27410
+ }).map((receipt) => ({
27411
+ id: receipt.receipt_id,
27412
+ state: activityFilterGroup(receipt.disposition),
27413
+ tone: activityTone(receipt.disposition),
27414
+ icon: activityPlainIcon(receipt.disposition),
27415
+ title: receipt.summary,
27416
+ meta: `${formatActivityTime(receipt.created_at)} \xB7 ${receipt.receipt_type}`,
27417
+ label: activityLabel(receipt.disposition),
27418
+ raw_content_included: false
27419
+ }));
27420
+ }
27421
+ function renderActivityCard(receipt) {
27422
+ const tone = activityTone(receipt.disposition);
27423
+ const filterGroup = activityFilterGroup(receipt.disposition);
27424
+ return ` <article class="activity-card ${tone}" data-activity-state="${escapeHtml(filterGroup)}">
27425
+ <div class="activity-icon">${activityIcon(receipt.disposition)}</div>
27426
+ <div>
27427
+ <div class="activity-title">${escapeHtml(receipt.summary)}</div>
27428
+ <div class="activity-meta">${escapeHtml(formatActivityTime(receipt.created_at))} \xB7 ${escapeHtml(receipt.receipt_type)}</div>
27429
+ </div>
27430
+ <div class="activity-badge ${tone}">${escapeHtml(activityLabel(receipt.disposition))}</div>
27431
+ </article>`;
27432
+ }
26663
27433
  function renderReceiptRow(receipt) {
26664
27434
  const policyBasis = receipt.policy_basis.length > 0 ? receipt.policy_basis.join(", ") : "none recorded";
26665
27435
  return ` <tr><th scope="row"><code>${escapeHtml(receipt.receipt_id)}</code></th><td>${escapeHtml(receipt.receipt_type)}</td><td>${escapeHtml(receipt.disposition)}</td><td><details><summary>${escapeHtml(receipt.summary)}</summary><dl><dt>Transaction</dt><dd><code>${escapeHtml(receipt.transaction_id)}</code></dd><dt>Created</dt><dd>${escapeHtml(receipt.created_at)}</dd><dt>Policy basis</dt><dd>${escapeHtml(policyBasis)}</dd><dt>Receipt hash</dt><dd><code>${escapeHtml(receipt.receipt_hash)}</code></dd><dt>Raw content</dt><dd>${receipt.raw_content_included ? "included" : "metadata-only"}</dd></dl></details></td></tr>`;
26666
27436
  }
27437
+ function activityTone(disposition) {
27438
+ if (disposition === "block") return "block";
27439
+ if (disposition === "warn" || disposition === "confirm") return disposition;
27440
+ return disposition === "rewrite" ? "rewrite" : "allow";
27441
+ }
27442
+ function activityIcon(disposition) {
27443
+ if (disposition === "block") return "&#128737;";
27444
+ if (disposition === "warn" || disposition === "confirm") return "&#9888;";
27445
+ return "&#9989;";
27446
+ }
27447
+ function activityPlainIcon(disposition) {
27448
+ if (disposition === "block") return "SHIELD";
27449
+ if (disposition === "warn" || disposition === "confirm") return "!";
27450
+ return "OK";
27451
+ }
27452
+ function activityLabel(disposition) {
27453
+ if (disposition === "block") return "BLOCKED";
27454
+ if (disposition === "warn") return "FLAGGED";
27455
+ if (disposition === "confirm") return "REVIEW";
27456
+ if (disposition === "rewrite") return "MASKED";
27457
+ return "ALLOWED";
27458
+ }
27459
+ function activityFilterGroup(disposition) {
27460
+ if (disposition === "block") return "blocked";
27461
+ if (disposition === "warn" || disposition === "confirm") return "flagged";
27462
+ return "allowed";
27463
+ }
27464
+ function formatActivityTime(createdAt) {
27465
+ const parsed = Date.parse(createdAt);
27466
+ if (Number.isNaN(parsed)) {
27467
+ return createdAt;
27468
+ }
27469
+ return new Date(parsed).toLocaleString("en-US", {
27470
+ dateStyle: "medium",
27471
+ timeStyle: "short",
27472
+ timeZone: "UTC"
27473
+ });
27474
+ }
27475
+ function providerDisplayName(providerId) {
27476
+ if (providerId === "chatgpt") return "ChatGPT";
27477
+ if (providerId === "claude") return "Claude";
27478
+ if (providerId === "gemini") return "Gemini";
27479
+ if (providerId === "perplexity") return "Perplexity";
27480
+ if (providerId === "cursor") return "Cursor";
27481
+ if (providerId === "windsurf") return "Windsurf";
27482
+ if (providerId === "openrouter") return "OpenRouter";
27483
+ if (providerId === "local_model") return "Local AI";
27484
+ return providerId;
27485
+ }
26667
27486
  function buildControlTowerProviderPermissionRows(profile) {
26668
27487
  const revokedProviders = new Set(profile.permissions?.revoked_provider_ids ?? []);
26669
27488
  return GUARDIAN_PROVIDER_IDS.map((providerId) => ({