@contextecf/guardian-cli 0.1.0 → 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.
|
@@ -15741,8 +15741,7 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:
|
|
|
15741
15741
|
import { readFileSync } from "node:fs";
|
|
15742
15742
|
import { copyFile, mkdir, readFile, stat } from "node:fs/promises";
|
|
15743
15743
|
import path from "node:path";
|
|
15744
|
-
import
|
|
15745
|
-
import PageEncryptedDatabase from "better-sqlite3-multiple-ciphers";
|
|
15744
|
+
import SQLiteDatabase from "better-sqlite3-multiple-ciphers";
|
|
15746
15745
|
function migrateSQLitePersonalFabricStore(db) {
|
|
15747
15746
|
db.exec(`
|
|
15748
15747
|
CREATE TABLE IF NOT EXISTS guardian_ecl_events (
|
|
@@ -16047,12 +16046,12 @@ function buildSQLitePersonalFabricStorePageEncryption(options) {
|
|
|
16047
16046
|
}
|
|
16048
16047
|
function openSQLitePersonalFabricDatabase(databasePath, pageEncryption) {
|
|
16049
16048
|
if (!pageEncryption) {
|
|
16050
|
-
return new
|
|
16049
|
+
return new SQLiteDatabase(databasePath ?? ":memory:");
|
|
16051
16050
|
}
|
|
16052
16051
|
if (!databasePath) {
|
|
16053
16052
|
throw new Error("Guardian SQLite page encryption requires a filesystem database path.");
|
|
16054
16053
|
}
|
|
16055
|
-
const db = new
|
|
16054
|
+
const db = new SQLiteDatabase(databasePath);
|
|
16056
16055
|
if (pageEncryption.cipher === "sqlcipher") {
|
|
16057
16056
|
db.pragma(`cipher=${sqliteStringLiteral("sqlcipher")}`);
|
|
16058
16057
|
db.pragma(`legacy=${pageEncryption.legacy ?? 4}`);
|
|
@@ -17594,7 +17593,7 @@ async function runStartCommand(flags, options) {
|
|
|
17594
17593
|
const home = resolveGuardianHome(options.env, options.platform);
|
|
17595
17594
|
const profile = await readProfile(home);
|
|
17596
17595
|
if (!profile) {
|
|
17597
|
-
write(stderr, "Guardian is not installed. Run guardian
|
|
17596
|
+
write(stderr, "Guardian is not installed. Run guardian setup first.\n");
|
|
17598
17597
|
return 1;
|
|
17599
17598
|
}
|
|
17600
17599
|
const host = stringFlag(flags, "host") ?? "127.0.0.1";
|
|
@@ -17602,10 +17601,8 @@ async function runStartCommand(flags, options) {
|
|
|
17602
17601
|
write(stderr, "Guardian start only accepts loopback hosts: 127.0.0.1, localhost, or ::1.\n");
|
|
17603
17602
|
return 1;
|
|
17604
17603
|
}
|
|
17605
|
-
const
|
|
17606
|
-
const
|
|
17607
|
-
const requestedControlTowerUrl = `${requestedDaemonUrl}/control-tower`;
|
|
17608
|
-
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;
|
|
17609
17606
|
let token;
|
|
17610
17607
|
try {
|
|
17611
17608
|
token = await readRuntimeToken(home);
|
|
@@ -17617,14 +17614,66 @@ async function runStartCommand(flags, options) {
|
|
|
17617
17614
|
);
|
|
17618
17615
|
return 1;
|
|
17619
17616
|
}
|
|
17620
|
-
const
|
|
17621
|
-
|
|
17622
|
-
|
|
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}
|
|
17623
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));
|
|
17624
17674
|
return 1;
|
|
17625
17675
|
}
|
|
17626
|
-
const
|
|
17627
|
-
const existing = await healthChecker({ url: healthUrl.url, token });
|
|
17676
|
+
const { requestedDaemonUrl, requestedControlTowerUrl, healthUrl, controlTowerSource, existing } = selected;
|
|
17628
17677
|
if (existing.ok && existing.reachable) {
|
|
17629
17678
|
profile.runtime.daemon_status = "start_requested";
|
|
17630
17679
|
profile.control_tower = {
|
|
@@ -17638,7 +17687,7 @@ async function runStartCommand(flags, options) {
|
|
|
17638
17687
|
daemonStatus: profile.runtime.daemon_status,
|
|
17639
17688
|
daemonUrl: requestedDaemonUrl,
|
|
17640
17689
|
controlTowerUrl: requestedControlTowerUrl,
|
|
17641
|
-
healthUrl
|
|
17690
|
+
healthUrl,
|
|
17642
17691
|
healthStatus: "reachable",
|
|
17643
17692
|
nextCommands: ["guardian open", "guardian status", "guardian daemon pair --json"]
|
|
17644
17693
|
};
|
|
@@ -17655,20 +17704,12 @@ async function runStartCommand(flags, options) {
|
|
|
17655
17704
|
);
|
|
17656
17705
|
return 0;
|
|
17657
17706
|
}
|
|
17658
|
-
if (existing.statusCode === 401) {
|
|
17659
|
-
write(
|
|
17660
|
-
stderr,
|
|
17661
|
-
`Guardian daemon at ${requestedDaemonUrl} rejected the local runtime token. Run guardian status or guardian stop before starting another daemon.
|
|
17662
|
-
`
|
|
17663
|
-
);
|
|
17664
|
-
return 1;
|
|
17665
|
-
}
|
|
17666
17707
|
if (!options.startDetachedDaemon) {
|
|
17667
17708
|
write(stderr, "Guardian detached daemon launcher is unavailable in this runtime.\n");
|
|
17668
17709
|
return 1;
|
|
17669
17710
|
}
|
|
17670
|
-
const started = await options.startDetachedDaemon({ home, host, port });
|
|
17671
|
-
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);
|
|
17672
17713
|
const controlTowerUrl = `${daemonUrl}/control-tower`;
|
|
17673
17714
|
profile.runtime.daemon_status = "start_requested";
|
|
17674
17715
|
profile.control_tower = {
|
|
@@ -17683,7 +17724,7 @@ async function runStartCommand(flags, options) {
|
|
|
17683
17724
|
daemonUrl,
|
|
17684
17725
|
controlTowerUrl,
|
|
17685
17726
|
pid: started.pid,
|
|
17686
|
-
healthUrl
|
|
17727
|
+
healthUrl,
|
|
17687
17728
|
healthStatus: existing.error ? "unreachable" : "not_reachable",
|
|
17688
17729
|
nextCommands: ["guardian open", "guardian status", "guardian daemon pair --json"]
|
|
17689
17730
|
};
|
|
@@ -17701,6 +17742,15 @@ async function runStartCommand(flags, options) {
|
|
|
17701
17742
|
);
|
|
17702
17743
|
return 0;
|
|
17703
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
|
+
}
|
|
17704
17754
|
async function runStopCommand(flags, options) {
|
|
17705
17755
|
const stdout = options.stdout ?? process.stdout;
|
|
17706
17756
|
const stderr = options.stderr ?? process.stderr;
|
|
@@ -22236,7 +22286,7 @@ var init_runtime = __esm({
|
|
|
22236
22286
|
"use strict";
|
|
22237
22287
|
init_src();
|
|
22238
22288
|
init_src2();
|
|
22239
|
-
GUARDIAN_CLI_VERSION = "0.1.
|
|
22289
|
+
GUARDIAN_CLI_VERSION = "0.1.2";
|
|
22240
22290
|
GUARDIAN_PROFILE_SCHEMA_VERSION = "contextecf/project-guardian-profile/v1";
|
|
22241
22291
|
GUARDIAN_NPM_PACKAGE_NAME = "@contextecf/guardian-cli";
|
|
22242
22292
|
GUARDIAN_GLOBAL_INSTALL_COMMAND = `npm install -g ${GUARDIAN_NPM_PACKAGE_NAME}`;
|
|
@@ -22999,6 +23049,14 @@ function detectRisk(promptManifest, policyPacks) {
|
|
|
22999
23049
|
risks.add("pii");
|
|
23000
23050
|
matchedPolicyRefs.push("guardian:builtin:pii-detection");
|
|
23001
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
|
+
}
|
|
23002
23060
|
if (FINANCIAL_PATTERN.test(prompt)) {
|
|
23003
23061
|
risks.add("financial_data");
|
|
23004
23062
|
matchedPolicyRefs.push("guardian:builtin:financial-data");
|
|
@@ -23030,7 +23088,7 @@ function detectRisk(promptManifest, policyPacks) {
|
|
|
23030
23088
|
}
|
|
23031
23089
|
}
|
|
23032
23090
|
const detectedRisks = Array.from(risks);
|
|
23033
|
-
const riskTier = tierForRisks(detectedRisks);
|
|
23091
|
+
const riskTier = hasFullSsnRisk(matchedPolicyRefs) ? "high" : tierForRisks(detectedRisks);
|
|
23034
23092
|
const recommendedDisposition = recommendedDispositionForTier(
|
|
23035
23093
|
riskTier,
|
|
23036
23094
|
detectedRisks,
|
|
@@ -23057,6 +23115,9 @@ function decideDisposition(session, promptManifest, policyPacks, riskReport) {
|
|
|
23057
23115
|
if (riskReport.detected_risks.includes("secret") || riskReport.detected_risks.includes("prompt_injection")) {
|
|
23058
23116
|
return "block";
|
|
23059
23117
|
}
|
|
23118
|
+
if (hasFullSsnRisk(riskReport.matched_policy_refs)) {
|
|
23119
|
+
return promptCoachEnabled(policyPacks) ? "rewrite" : "block";
|
|
23120
|
+
}
|
|
23060
23121
|
const thresholds = effectiveRiskThresholds(policyPacks);
|
|
23061
23122
|
if (riskTierRank(riskReport.risk_tier) >= riskTierRank(thresholds.block_at)) {
|
|
23062
23123
|
return "block";
|
|
@@ -23116,6 +23177,7 @@ function buildFinalPrompt(prompt, disposition, riskReport) {
|
|
|
23116
23177
|
);
|
|
23117
23178
|
}
|
|
23118
23179
|
if (riskReport.detected_risks.includes("pii")) {
|
|
23180
|
+
rewritten = rewritten.replace(SSN_PATTERN, "XXX-XX-$3");
|
|
23119
23181
|
rewritten = rewritten.replace(EMAIL_PATTERN, "[REDACTED_EMAIL]");
|
|
23120
23182
|
}
|
|
23121
23183
|
return rewritten;
|
|
@@ -23129,11 +23191,12 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
|
|
|
23129
23191
|
if (!coachDecision.should_present_card || coachDecision.intervention_level === "none") {
|
|
23130
23192
|
return void 0;
|
|
23131
23193
|
}
|
|
23194
|
+
const fullSsnRisk = hasFullSsnRisk(riskReport.matched_policy_refs);
|
|
23132
23195
|
const card = {
|
|
23133
23196
|
card_id: `coach:${riskReport.risk_report_id}`,
|
|
23134
23197
|
intervention_level: coachDecision.intervention_level,
|
|
23135
|
-
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",
|
|
23136
|
-
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.",
|
|
23137
23200
|
recommended_choice_id: disposition === "confirm" ? "confirm-after-review" : coachDecision.intervention_level === "soft_tag_along" ? "add-context" : "use-guardian-version",
|
|
23138
23201
|
choices: disposition === "confirm" ? [
|
|
23139
23202
|
{
|
|
@@ -23163,6 +23226,20 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
|
|
|
23163
23226
|
description: "Send without adding context.",
|
|
23164
23227
|
resulting_disposition: "allow"
|
|
23165
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
|
+
}
|
|
23166
23243
|
] : [
|
|
23167
23244
|
{
|
|
23168
23245
|
choice_id: "use-guardian-version",
|
|
@@ -23262,6 +23339,13 @@ function promptCoachEnabled(policyPacks) {
|
|
|
23262
23339
|
function isDestructiveTool(toolName) {
|
|
23263
23340
|
return /\b(delete|drop|send|publish|pay|write|exec|shell|migration)\b/i.test(toolName);
|
|
23264
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
|
+
}
|
|
23265
23349
|
function createPolicyPackPreset(overrides) {
|
|
23266
23350
|
return {
|
|
23267
23351
|
version: POLICY_PACK_PRESET_VERSION,
|
|
@@ -23357,7 +23441,7 @@ function digest2(value) {
|
|
|
23357
23441
|
function isPresent(value) {
|
|
23358
23442
|
return value !== void 0;
|
|
23359
23443
|
}
|
|
23360
|
-
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;
|
|
23361
23445
|
var init_src3 = __esm({
|
|
23362
23446
|
"packages/personal-fabric-runtime/src/index.ts"() {
|
|
23363
23447
|
"use strict";
|
|
@@ -23378,6 +23462,8 @@ var init_src3 = __esm({
|
|
|
23378
23462
|
/\b(password|secret|token)\s*[:=]\s*\S+/i
|
|
23379
23463
|
];
|
|
23380
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;
|
|
23381
23467
|
FINANCIAL_PATTERN = /\b(bank|wire|routing number|account number|payroll|invoice|revenue|forecast|contract value|credit card)\b/i;
|
|
23382
23468
|
HEALTHCARE_PATTERN = /\b(patient|diagnosis|medication|hipaa|medical record|prescription|clinical)\b/i;
|
|
23383
23469
|
WORK_PATTERN = /\b(contract|client|customer|salesforce|pipeline|renewal|board|employee|confidential)\b/i;
|
|
@@ -24811,6 +24897,7 @@ __export(daemon_exports, {
|
|
|
24811
24897
|
buildControlTowerLocalDataStatus: () => buildControlTowerLocalDataStatus,
|
|
24812
24898
|
buildControlTowerReleaseDoctorStatus: () => buildControlTowerReleaseDoctorStatus,
|
|
24813
24899
|
processGuardianBrowserBridgeRequest: () => processGuardianBrowserBridgeRequest,
|
|
24900
|
+
processGuardianControlTowerActivityRequest: () => processGuardianControlTowerActivityRequest,
|
|
24814
24901
|
processGuardianControlTowerAppPermissionRequest: () => processGuardianControlTowerAppPermissionRequest,
|
|
24815
24902
|
processGuardianControlTowerDataRequest: () => processGuardianControlTowerDataRequest,
|
|
24816
24903
|
processGuardianControlTowerPolicyRequest: () => processGuardianControlTowerPolicyRequest,
|
|
@@ -24974,6 +25061,14 @@ async function handleRequest(input2) {
|
|
|
24974
25061
|
);
|
|
24975
25062
|
return;
|
|
24976
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
|
+
}
|
|
24977
25072
|
if (input2.request.method === "GET" && input2.request.url === "/v1/guardian/admin/health") {
|
|
24978
25073
|
const result2 = processGuardianDaemonHealthRequest({
|
|
24979
25074
|
token: input2.token,
|
|
@@ -25130,6 +25225,20 @@ function processGuardianDaemonHealthRequest(input2) {
|
|
|
25130
25225
|
}
|
|
25131
25226
|
};
|
|
25132
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
|
+
}
|
|
25133
25242
|
function processGuardianDaemonStopRequest(input2) {
|
|
25134
25243
|
if (!isAuthorized(input2.token, input2.suppliedToken)) {
|
|
25135
25244
|
return {
|
|
@@ -25920,12 +26029,31 @@ function closeGuardianDaemonServer(server) {
|
|
|
25920
26029
|
} catch {
|
|
25921
26030
|
}
|
|
25922
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
|
+
}
|
|
25923
26042
|
function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { nodes: [], edges: [] }, learningEvents = [], localDataStatus = createEmptyControlTowerLocalDataStatus(profile), releaseDoctorStatus = createEmptyControlTowerReleaseDoctorStatus(
|
|
25924
26043
|
profile
|
|
25925
26044
|
)) {
|
|
25926
26045
|
const policyPacks = profile.policy_packs.available.length;
|
|
25927
26046
|
const activePolicyPacks = profile.policy_packs.active.join(", ") || "none";
|
|
25928
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;
|
|
25929
26057
|
const receiptRows = renderReceiptRows(recentReceipts);
|
|
25930
26058
|
const graphNodes = searchGraph.nodes.slice(0, CONTROL_TOWER_GRAPH_NODE_LIMIT);
|
|
25931
26059
|
const graphEdges = searchGraph.edges.slice(0, CONTROL_TOWER_GRAPH_EDGE_LIMIT);
|
|
@@ -26005,6 +26133,92 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
|
|
|
26005
26133
|
).join("\n");
|
|
26006
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";
|
|
26007
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" ? "☾" : postureProfile.id === "balanced" ? "⚖" : postureProfile.id === "guided" ? "🎓" : postureProfile.id === "high_assurance" ? "🔒" : "⚙";
|
|
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");
|
|
26008
26222
|
const rows = [
|
|
26009
26223
|
["Profile", profile.profile_dir],
|
|
26010
26224
|
["Daemon", profile.runtime.daemon_status],
|
|
@@ -26057,53 +26271,464 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
|
|
|
26057
26271
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
26058
26272
|
<title>Project Guardian Control Tower</title>
|
|
26059
26273
|
<style>
|
|
26060
|
-
:root {
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
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; }
|
|
26071
26318
|
form { margin: 0; }
|
|
26072
|
-
button { min-width: 96px; min-height: 34px; border: 1px solid
|
|
26073
|
-
button:hover { background:
|
|
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); }
|
|
26074
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
|
+
}
|
|
26075
26331
|
details { max-width: 100%; }
|
|
26076
|
-
summary { cursor: pointer; font-weight: 650; }
|
|
26332
|
+
summary { cursor: pointer; font-weight: 650; color: #eff6ff; }
|
|
26077
26333
|
dl { display: grid; grid-template-columns: minmax(120px, 180px) 1fr; gap: 8px 14px; margin: 12px 0 0; }
|
|
26078
|
-
dt { color: #
|
|
26079
|
-
dd { margin: 0; overflow-wrap: anywhere; }
|
|
26334
|
+
dt { color: #8fa0b7; font-weight: 650; }
|
|
26335
|
+
dd { margin: 0; overflow-wrap: anywhere; color: #c8d2e3; }
|
|
26080
26336
|
tr:last-child th, tr:last-child td { border-bottom: 0; }
|
|
26081
|
-
.
|
|
26082
|
-
|
|
26083
|
-
|
|
26084
|
-
|
|
26085
|
-
|
|
26086
|
-
|
|
26087
|
-
|
|
26088
|
-
|
|
26089
|
-
|
|
26090
|
-
|
|
26091
|
-
|
|
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; }
|
|
26092
26654
|
}
|
|
26093
26655
|
</style>
|
|
26094
26656
|
</head>
|
|
26095
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">⚙</span>
|
|
26663
|
+
</div>
|
|
26664
|
+
</header>
|
|
26096
26665
|
<main>
|
|
26097
|
-
<div class="
|
|
26666
|
+
<div class="eyebrow">Local-first status</div>
|
|
26098
26667
|
<h1>Project Guardian Control Tower</h1>
|
|
26099
|
-
<p>This
|
|
26100
|
-
<
|
|
26101
|
-
<
|
|
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>
|
|
26102
26726
|
${rows.map(
|
|
26103
26727
|
([label, value]) => ` <tr><th scope="row">${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`
|
|
26104
26728
|
).join("\n")}
|
|
26105
|
-
|
|
26106
|
-
|
|
26729
|
+
</tbody>
|
|
26730
|
+
</table>
|
|
26731
|
+
</section>
|
|
26107
26732
|
<section aria-labelledby="privacy-posture">
|
|
26108
26733
|
<h2 id="privacy-posture">Privacy Posture</h2>
|
|
26109
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>
|
|
@@ -26230,8 +26855,16 @@ ${releaseDoctorRows}
|
|
|
26230
26855
|
</table>
|
|
26231
26856
|
</section>
|
|
26232
26857
|
<section aria-labelledby="receipt-drilldowns">
|
|
26233
|
-
<
|
|
26234
|
-
|
|
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 & Receipts.</span>Inspect recent Guardian decisions without raw prompt, response, or local-action payload content.</p>
|
|
26235
26868
|
<table aria-label="Guardian receipt drilldowns">
|
|
26236
26869
|
<thead>
|
|
26237
26870
|
<tr><th scope="col">Receipt</th><th scope="col">Type</th><th scope="col">Decision</th><th scope="col">Details</th></tr>
|
|
@@ -26309,7 +26942,79 @@ ${learningEventRows}
|
|
|
26309
26942
|
</tbody>
|
|
26310
26943
|
</table>
|
|
26311
26944
|
</section>
|
|
26945
|
+
</div>
|
|
26946
|
+
</details>
|
|
26312
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>
|
|
26313
27018
|
</body>
|
|
26314
27019
|
</html>
|
|
26315
27020
|
`;
|
|
@@ -26323,6 +27028,24 @@ function renderDataPathForm(action, pathName, label) {
|
|
|
26323
27028
|
"</form>"
|
|
26324
27029
|
].join("");
|
|
26325
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
|
+
}
|
|
26326
27049
|
function renderDataConfirmForm(action, pathName, confirmation, label) {
|
|
26327
27050
|
return [
|
|
26328
27051
|
'<form method="post" action="/v1/guardian/admin/data">',
|
|
@@ -26661,10 +27384,105 @@ function renderReceiptRows(receipts) {
|
|
|
26661
27384
|
}
|
|
26662
27385
|
return receipts.map((receipt) => renderReceiptRow(receipt)).join("\n");
|
|
26663
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">✅</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
|
+
}
|
|
26664
27433
|
function renderReceiptRow(receipt) {
|
|
26665
27434
|
const policyBasis = receipt.policy_basis.length > 0 ? receipt.policy_basis.join(", ") : "none recorded";
|
|
26666
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>`;
|
|
26667
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 "🛡";
|
|
27444
|
+
if (disposition === "warn" || disposition === "confirm") return "⚠";
|
|
27445
|
+
return "✅";
|
|
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
|
+
}
|
|
26668
27486
|
function buildControlTowerProviderPermissionRows(profile) {
|
|
26669
27487
|
const revokedProviders = new Set(profile.permissions?.revoked_provider_ids ?? []);
|
|
26670
27488
|
return GUARDIAN_PROVIDER_IDS.map((providerId) => ({
|