@shield-agent/kya 0.34.0 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +2 -2
- package/.mcp.json +1 -1
- package/LIMITATIONS.md +2 -1
- package/README.md +37 -655
- package/amp/settings.example.json +1 -1
- package/assets/report-blocked.png +0 -0
- package/assets/report-certify.png +0 -0
- package/assets/report-overview.png +0 -0
- package/assets/screenshot-features.json +1 -0
- package/claude/claude_desktop_config.example.json +1 -1
- package/cline/cline_mcp_settings.example.json +1 -1
- package/copilot/mcp-config.example.json +1 -1
- package/dist/alerts/triage-setup.d.ts +4 -0
- package/dist/alerts/triage-setup.d.ts.map +1 -1
- package/dist/alerts/triage-setup.js +11 -1
- package/dist/alerts/triage-setup.js.map +1 -1
- package/dist/card-png.d.ts +3 -0
- package/dist/card-png.d.ts.map +1 -0
- package/dist/card-png.js +54 -0
- package/dist/card-png.js.map +1 -0
- package/dist/card.d.ts +62 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +154 -0
- package/dist/card.js.map +1 -0
- package/dist/certify/evaluate.d.ts +11 -1
- package/dist/certify/evaluate.d.ts.map +1 -1
- package/dist/certify/evaluate.js +8 -1
- package/dist/certify/evaluate.js.map +1 -1
- package/dist/certify/render.d.ts.map +1 -1
- package/dist/certify/render.js +10 -1
- package/dist/certify/render.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +223 -52
- package/dist/cli.js.map +1 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +4 -3
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hook.d.ts.map +1 -1
- package/dist/commands/hook.js +94 -8
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/receipt.d.ts +19 -0
- package/dist/commands/receipt.d.ts.map +1 -1
- package/dist/commands/receipt.js +21 -1
- package/dist/commands/receipt.js.map +1 -1
- package/dist/commands/serve-mcp.d.ts +2 -0
- package/dist/commands/serve-mcp.d.ts.map +1 -1
- package/dist/commands/serve-mcp.js +2 -0
- package/dist/commands/serve-mcp.js.map +1 -1
- package/dist/commands/start.d.ts +50 -2
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +233 -27
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/team.d.ts +19 -2
- package/dist/commands/team.d.ts.map +1 -1
- package/dist/commands/team.js +93 -13
- package/dist/commands/team.js.map +1 -1
- package/dist/commands/uninstall.d.ts +20 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +378 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/wire-hooks.d.ts +16 -2
- package/dist/commands/wire-hooks.d.ts.map +1 -1
- package/dist/commands/wire-hooks.js +86 -21
- package/dist/commands/wire-hooks.js.map +1 -1
- package/dist/cta.d.ts +3 -1
- package/dist/cta.d.ts.map +1 -1
- package/dist/cta.js +11 -6
- package/dist/cta.js.map +1 -1
- package/dist/fs-atomic.d.ts.map +1 -1
- package/dist/fs-atomic.js +9 -2
- package/dist/fs-atomic.js.map +1 -1
- package/dist/guard-demo.d.ts +23 -0
- package/dist/guard-demo.d.ts.map +1 -0
- package/dist/guard-demo.js +34 -0
- package/dist/guard-demo.js.map +1 -0
- package/dist/guard-entry.d.ts +2 -0
- package/dist/guard-entry.d.ts.map +1 -0
- package/dist/guard-entry.js +50 -0
- package/dist/guard-entry.js.map +1 -0
- package/dist/guard-lite.d.ts +27 -0
- package/dist/guard-lite.d.ts.map +1 -0
- package/dist/guard-lite.js +274 -0
- package/dist/guard-lite.js.map +1 -0
- package/dist/guard-pack.d.ts +7 -2
- package/dist/guard-pack.d.ts.map +1 -1
- package/dist/guard-pack.js +202 -4
- package/dist/guard-pack.js.map +1 -1
- package/dist/hook-health.d.ts +7 -0
- package/dist/hook-health.d.ts.map +1 -0
- package/dist/hook-health.js +105 -0
- package/dist/hook-health.js.map +1 -0
- package/dist/manifest.d.ts +106 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +454 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mcp/http.d.ts +1 -0
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/http.js +1 -0
- package/dist/mcp/http.js.map +1 -1
- package/dist/mcp/protocol.d.ts +2 -0
- package/dist/mcp/protocol.d.ts.map +1 -1
- package/dist/mcp/protocol.js +5 -1
- package/dist/mcp/protocol.js.map +1 -1
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/never-list-input.d.ts +2 -0
- package/dist/never-list-input.d.ts.map +1 -1
- package/dist/never-list-input.js +30 -30
- package/dist/never-list-input.js.map +1 -1
- package/dist/offline-evaluate.d.ts.map +1 -1
- package/dist/offline-evaluate.js +2 -1
- package/dist/offline-evaluate.js.map +1 -1
- package/dist/parse-args.js +1 -1
- package/dist/parse-args.js.map +1 -1
- package/dist/receipt/changes.d.ts +2 -1
- package/dist/receipt/changes.d.ts.map +1 -1
- package/dist/receipt/changes.js +9 -0
- package/dist/receipt/changes.js.map +1 -1
- package/dist/receipt/dashboard.d.ts +14 -1
- package/dist/receipt/dashboard.d.ts.map +1 -1
- package/dist/receipt/dashboard.js +20 -2
- package/dist/receipt/dashboard.js.map +1 -1
- package/dist/receipt/live-server.d.ts +15 -0
- package/dist/receipt/live-server.d.ts.map +1 -1
- package/dist/receipt/live-server.js +71 -0
- package/dist/receipt/live-server.js.map +1 -1
- package/dist/receipt/receipt-css.d.ts.map +1 -1
- package/dist/receipt/receipt-css.js +45 -0
- package/dist/receipt/receipt-css.js.map +1 -1
- package/dist/receipt/render-receipt.d.ts +14 -0
- package/dist/receipt/render-receipt.d.ts.map +1 -1
- package/dist/receipt/render-receipt.js +451 -79
- package/dist/receipt/render-receipt.js.map +1 -1
- package/dist/receipt/search.d.ts.map +1 -1
- package/dist/receipt/search.js +2 -0
- package/dist/receipt/search.js.map +1 -1
- package/dist/receipt/semantic-setup.d.ts +4 -0
- package/dist/receipt/semantic-setup.d.ts.map +1 -1
- package/dist/receipt/semantic-setup.js +10 -1
- package/dist/receipt/semantic-setup.js.map +1 -1
- package/dist/receipt/share.d.ts +15 -0
- package/dist/receipt/share.d.ts.map +1 -1
- package/dist/receipt/share.js +34 -1
- package/dist/receipt/share.js.map +1 -1
- package/dist/telemetry.d.ts +17 -2
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +67 -7
- package/dist/telemetry.js.map +1 -1
- package/dist/trail.d.ts +39 -0
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +27 -0
- package/dist/trail.js.map +1 -1
- package/dist/write-journal.d.ts +39 -0
- package/dist/write-journal.d.ts.map +1 -0
- package/dist/write-journal.js +107 -0
- package/dist/write-journal.js.map +1 -0
- package/droid/README.md +1 -1
- package/droid/mcp.example.json +1 -1
- package/gemini/settings.example.json +1 -1
- package/grok/README.md +1 -1
- package/kilo/kilo.example.json +1 -1
- package/kimi/mcp.example.json +1 -1
- package/kiro/mcp.example.json +1 -1
- package/manifest.json +2 -2
- package/mastracode/mcp.example.json +1 -1
- package/mcp.json +1 -1
- package/openai/codex.config.example.toml +1 -1
- package/opencode/opencode.example.json +1 -1
- package/package.json +9 -3
- package/qwen/settings.example.json +1 -1
- package/server.json +3 -3
|
@@ -6,7 +6,9 @@ import { semanticInstallCommand, semanticRuntimeInstalled } from "./semantic-sta
|
|
|
6
6
|
import { assertNoSecrets, clip, stripEscapes } from "../dash/render.js";
|
|
7
7
|
import { clipMultiline, DIFF_MAX_TOTAL_CHARS } from "../diff-preview.js";
|
|
8
8
|
import { DOMAIN_LABELS, domainOrder } from "../certify/render.js";
|
|
9
|
-
import { productLabel, eventsSince, readTrail } from "../trail.js";
|
|
9
|
+
import { productLabel, eventsSince, readTrail, countOutcomes, outcomeOf, wouldHoldInStrict } from "../trail.js";
|
|
10
|
+
import { eventIdOf, isNearMiss } from "../card.js";
|
|
11
|
+
import { readCredentials } from "../login/credentials.js";
|
|
10
12
|
import { mcpServerLabel, parseMcpToolId } from "../mcp-servers.js";
|
|
11
13
|
import { loadInvestigateLastRun, } from "../investigate/last-run.js";
|
|
12
14
|
import { readNotifyLog, summarizeNotifyLog, } from "../notify/log.js";
|
|
@@ -19,6 +21,7 @@ import { computeDashboard } from "./dashboard.js";
|
|
|
19
21
|
import { buildChangesModel } from "./changes.js";
|
|
20
22
|
import { GATE_NOT_SETUP_QUICKSTART, loadGatePage, toolNameFromToolId, toolServerPrefix, } from "./gate-page.js";
|
|
21
23
|
import { receiptCss } from "./receipt-css.js";
|
|
24
|
+
import { DEMO_CALLS, PROMISE } from "../guard-demo.js";
|
|
22
25
|
import { applyChipFilters, rankEvents } from "./search.js";
|
|
23
26
|
import { emptyOpenshellCard, loadOpenshellCard } from "./openshell-card.js";
|
|
24
27
|
import { readIocResult } from "../ioc/result.js";
|
|
@@ -63,6 +66,11 @@ function mdFence(content) {
|
|
|
63
66
|
function mdInline(s) {
|
|
64
67
|
return stripEscapes(s).replace(/`/g, "'");
|
|
65
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Counts for the report headline. A row with an outcome is counted by what happened to the call (`outcomes`); only a
|
|
71
|
+
* row without one (written before outcomes existed) is counted by its stored verdict (allow, deny, require).
|
|
72
|
+
* `never` is a policy overlay and counts every row.
|
|
73
|
+
*/
|
|
66
74
|
function countVerdicts(events) {
|
|
67
75
|
let allow = 0;
|
|
68
76
|
let deny = 0;
|
|
@@ -72,6 +80,8 @@ function countVerdicts(events) {
|
|
|
72
80
|
const v = e.verdict.toUpperCase();
|
|
73
81
|
if (e.neverEvent || e.reasonCode === "NEVER_EVENT")
|
|
74
82
|
never++;
|
|
83
|
+
if (outcomeOf(e))
|
|
84
|
+
continue;
|
|
75
85
|
if (v === "ALLOW")
|
|
76
86
|
allow++;
|
|
77
87
|
else if (v === "DENY")
|
|
@@ -79,7 +89,55 @@ function countVerdicts(events) {
|
|
|
79
89
|
else if (v === "REQUIRE_APPROVE")
|
|
80
90
|
require++;
|
|
81
91
|
}
|
|
82
|
-
return { allow, deny, require, never };
|
|
92
|
+
return { allow, deny, require, never, outcomes: countOutcomes(events) };
|
|
93
|
+
}
|
|
94
|
+
const STRICT_NOTE = "would hold in strict mode";
|
|
95
|
+
/** One line under the headline: how to turn strict mode on. Shown only when a call ran that strict mode would hold (STRICT_NOTE is the row note). */
|
|
96
|
+
function strictHowTo(wouldHold, code) {
|
|
97
|
+
return `${wouldHold} ${wouldHold === 1 ? "call that ran" : "calls that ran"} would hold in strict mode. To turn strict mode on, add ${code("--strict")} to the ${code("kya hook")} command in your tool's hook settings.`;
|
|
98
|
+
}
|
|
99
|
+
/** Headline text for the Markdown report: "Ran 11, Blocked 5, Held 0". Error is listed only when it is not 0. */
|
|
100
|
+
function outcomeHeadline(o) {
|
|
101
|
+
return `Ran ${o.ran}, Blocked ${o.blocked}, Held ${o.held}${o.error > 0 ? `, Error ${o.error}` : ""}`;
|
|
102
|
+
}
|
|
103
|
+
const OUTCOME_WORD = { ran: "RAN", blocked: "BLOCKED", held: "HELD", error: "ERROR" };
|
|
104
|
+
/** A short note for a call that ran although policy said otherwise (REQUIRE_APPROVE, or DENY in shadow mode). */
|
|
105
|
+
function ranNote(e) {
|
|
106
|
+
if (wouldHoldInStrict(e))
|
|
107
|
+
return STRICT_NOTE;
|
|
108
|
+
if (outcomeOf(e) === "ran" && e.verdict.toUpperCase() === "DENY")
|
|
109
|
+
return "would block (shadow mode)";
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Feed order: blocked rows (newest first) above everything else (newest first). Rows without an outcome sort as before. */
|
|
113
|
+
function compareFeed(a, b) {
|
|
114
|
+
const rank = (e) => (outcomeOf(e) === "blocked" ? 0 : 1);
|
|
115
|
+
return rank(a) - rank(b) || b.ts.localeCompare(a.ts);
|
|
116
|
+
}
|
|
117
|
+
/** Key for rows that may collapse. Only rows with an outcome collapse; a blocked row, or one with a change preview, never does. */
|
|
118
|
+
function collapseKey(e) {
|
|
119
|
+
const o = outcomeOf(e);
|
|
120
|
+
if (!o || o === "blocked" || e.diffPreview?.trim())
|
|
121
|
+
return undefined;
|
|
122
|
+
return JSON.stringify([e.toolId, o, e.verdict, e.reasonCode, e.summary ?? "", e.targetPath ?? "", e.argsHash ?? "", e.sessionId, e.product ?? "", e.project ?? "", e.mode]);
|
|
123
|
+
}
|
|
124
|
+
/** Merge identical consecutive rows ("shell echo, 10 times"). Input order is kept. */
|
|
125
|
+
export function collapseFeedRows(ordered) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const e of ordered) {
|
|
128
|
+
const key = collapseKey(e);
|
|
129
|
+
const last = out[out.length - 1];
|
|
130
|
+
if (last && key !== undefined && last.key === key)
|
|
131
|
+
last.count++;
|
|
132
|
+
else
|
|
133
|
+
out.push({ event: e, count: 1, key });
|
|
134
|
+
}
|
|
135
|
+
return out.map(({ event, count }) => ({ event, count }));
|
|
136
|
+
}
|
|
137
|
+
/** "shell echo (+1 args), 3 times" or just "3 times" when the row has no summary. */
|
|
138
|
+
function repeatText(e, count) {
|
|
139
|
+
const base = e.summary?.trim();
|
|
140
|
+
return `${base ? `${base}, ` : ""}${count} times`;
|
|
83
141
|
}
|
|
84
142
|
function relativeTime(ts, nowMs) {
|
|
85
143
|
const t = Date.parse(ts);
|
|
@@ -142,6 +200,21 @@ function verdictWord(verdict) {
|
|
|
142
200
|
return "REVIEW";
|
|
143
201
|
return v;
|
|
144
202
|
}
|
|
203
|
+
/** Row tone: by outcome when the row has one (a call that ran is ok, even if policy said review), else by verdict. */
|
|
204
|
+
function rowTone(e) {
|
|
205
|
+
switch (outcomeOf(e)) {
|
|
206
|
+
case "ran": return "ok";
|
|
207
|
+
case "blocked": return "bad";
|
|
208
|
+
case "held": return "warn";
|
|
209
|
+
case "error": return "mute";
|
|
210
|
+
default: return tone(e.verdict);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Row label: RAN, BLOCKED, HELD or ERROR when the row has an outcome, else the verdict word as before. */
|
|
214
|
+
function rowWord(e) {
|
|
215
|
+
const o = outcomeOf(e);
|
|
216
|
+
return o ? OUTCOME_WORD[o] : verdictWord(e.verdict);
|
|
217
|
+
}
|
|
145
218
|
/** Pure rollup of trail events for the report's stat chips and sections. */
|
|
146
219
|
export function aggregateEvents(events) {
|
|
147
220
|
const modes = { observe: 0, hold: 0, offline: 0 };
|
|
@@ -182,8 +255,15 @@ export function aggregateEvents(events) {
|
|
|
182
255
|
s.events++;
|
|
183
256
|
if (e.neverEvent || e.reasonCode === "NEVER_EVENT")
|
|
184
257
|
s.never++;
|
|
258
|
+
const o = outcomeOf(e);
|
|
185
259
|
const v = e.verdict.toUpperCase();
|
|
186
|
-
if (
|
|
260
|
+
if (o) {
|
|
261
|
+
if (o === "blocked")
|
|
262
|
+
s.deny++;
|
|
263
|
+
else if (o === "held")
|
|
264
|
+
s.hold++;
|
|
265
|
+
}
|
|
266
|
+
else if (v === "DENY")
|
|
187
267
|
s.deny++;
|
|
188
268
|
else if (v === "REQUIRE_APPROVE")
|
|
189
269
|
s.hold++;
|
|
@@ -275,6 +355,7 @@ export function buildReceiptModel(sessionId, events, extras) {
|
|
|
275
355
|
liveToken: extras?.liveToken,
|
|
276
356
|
searchQuery: extras?.searchQuery,
|
|
277
357
|
identity: extras?.identity,
|
|
358
|
+
planeConfigured: extras?.planeConfigured,
|
|
278
359
|
sandboxes: extras?.sandboxes,
|
|
279
360
|
openshell: extras?.openshell,
|
|
280
361
|
wiredHosts: extras?.wiredHosts,
|
|
@@ -306,6 +387,7 @@ export function buildWindowReceiptModel(events, days, extras) {
|
|
|
306
387
|
liveToken: extras?.liveToken,
|
|
307
388
|
searchQuery: extras?.searchQuery,
|
|
308
389
|
identity: extras?.identity,
|
|
390
|
+
planeConfigured: extras?.planeConfigured,
|
|
309
391
|
sandboxes: extras?.sandboxes,
|
|
310
392
|
openshell: extras?.openshell,
|
|
311
393
|
wiredHosts: extras?.wiredHosts,
|
|
@@ -342,6 +424,7 @@ export function loadReceiptModel(input) {
|
|
|
342
424
|
liveToken: input.liveToken,
|
|
343
425
|
searchQuery: input.searchQuery,
|
|
344
426
|
identity: loadIdentity(input.cwd),
|
|
427
|
+
planeConfigured: isPlaneConfigured(loadIdentity(input.cwd), env),
|
|
345
428
|
sandboxes: loadSandboxes(input.cwd),
|
|
346
429
|
openshell: loadOpenshellCard({ cwd: input.cwd, events: importEvents, env, certify }),
|
|
347
430
|
wiredHosts: loadWiredHosts(input.cwd),
|
|
@@ -379,6 +462,7 @@ function filterDataAttrs(e, sessionId) {
|
|
|
379
462
|
const project = e.project?.trim();
|
|
380
463
|
const server = parseMcpToolId(e.toolId);
|
|
381
464
|
return (`data-verdict="${esc(e.verdict.toUpperCase())}"` +
|
|
465
|
+
(outcomeOf(e) ? ` data-outcome="${outcomeOf(e)}"` : "") +
|
|
382
466
|
(never ? ` data-never="1"` : "") +
|
|
383
467
|
` data-mode="${esc(e.mode)}"` +
|
|
384
468
|
` data-plane="${esc(e.host?.trim() || "unknown")}"` +
|
|
@@ -414,6 +498,18 @@ function noMatchHint(query, env = process.env) {
|
|
|
414
498
|
return (`${base} Plain-language search is off, so only exact words match. ` +
|
|
415
499
|
`Turn it on once with <code>${esc(semanticInstallCommand(env))}</code>, then search again.`);
|
|
416
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* "Make card" next to a stopped row (G5). Only a denied call gets it. The live report gets a button that calls the local
|
|
503
|
+
* /action/card; a static file has no server, so it shows the CLI command. Never in the deny text the agent reads.
|
|
504
|
+
*/
|
|
505
|
+
function cardControl(e, live) {
|
|
506
|
+
if (!isNearMiss(e))
|
|
507
|
+
return "";
|
|
508
|
+
const id = esc(eventIdOf(e));
|
|
509
|
+
return live
|
|
510
|
+
? `<span class="card-ctl"><button type="button" class="stat" data-card-id="${id}">Make card</button><span class="mute small card-result" role="status" aria-live="polite"></span></span>`
|
|
511
|
+
: `<span class="card-ctl mute small">Make card: <code>kya receipt --card --id ${id}</code></span>`;
|
|
512
|
+
}
|
|
417
513
|
function renderFeed(events, nowMs, live, preserveOrder, noMatch) {
|
|
418
514
|
if (events.length === 0) {
|
|
419
515
|
if (noMatch)
|
|
@@ -423,19 +519,34 @@ function renderFeed(events, nowMs, live, preserveOrder, noMatch) {
|
|
|
423
519
|
: "No events yet - evaluate a tool call with <code>kya wrap</code>, then regenerate this receipt.";
|
|
424
520
|
return `<p class="empty">${hint}</p>`;
|
|
425
521
|
}
|
|
426
|
-
const ordered = preserveOrder ? [...events] : [...events].sort(
|
|
522
|
+
const ordered = preserveOrder ? [...events] : [...events].sort(compareFeed);
|
|
523
|
+
// Search ranking keeps its order and never collapses; the plain feed merges identical consecutive rows.
|
|
524
|
+
const rows = preserveOrder ? ordered.map((event) => ({ event, count: 1 })) : collapseFeedRows(ordered);
|
|
427
525
|
const parts = [];
|
|
428
526
|
let lastDay = "";
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
527
|
+
let pinnedHeading = false;
|
|
528
|
+
for (const { event: e, count } of rows) {
|
|
529
|
+
// Blocked rows sit above everything else, under one heading, so they never hide below repeated rows.
|
|
530
|
+
const pinned = !preserveOrder && outcomeOf(e) === "blocked";
|
|
531
|
+
if (pinned) {
|
|
532
|
+
if (!pinnedHeading) {
|
|
533
|
+
pinnedHeading = true;
|
|
534
|
+
parts.push(`<div class="day" role="presentation">Blocked</div>`);
|
|
535
|
+
}
|
|
434
536
|
}
|
|
435
|
-
|
|
537
|
+
else {
|
|
538
|
+
const key = localDayKey(e.ts);
|
|
539
|
+
if (key !== lastDay) {
|
|
540
|
+
lastDay = key;
|
|
541
|
+
parts.push(`<div class="day" role="presentation">${esc(dayLabel(key, nowMs))}</div>`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const t = rowTone(e);
|
|
436
545
|
const never = e.neverEvent || e.reasonCode === "NEVER_EVENT";
|
|
437
|
-
const
|
|
438
|
-
|
|
546
|
+
const note = ranNote(e);
|
|
547
|
+
const summaryText = count > 1 ? repeatText(e, count) : e.summary?.trim();
|
|
548
|
+
const summary = summaryText
|
|
549
|
+
? `<div class="summary">${esc(summaryText)}</div>`
|
|
439
550
|
: "";
|
|
440
551
|
const preview = e.diffPreview?.trim()
|
|
441
552
|
? `<details class="diff"><summary>Change preview</summary><pre>${esc(e.diffPreview.trim())}</pre></details>`
|
|
@@ -450,9 +561,10 @@ function renderFeed(events, nowMs, live, preserveOrder, noMatch) {
|
|
|
450
561
|
<div class="rail" aria-hidden="true"><span class="tick"></span></div>
|
|
451
562
|
<div class="main">
|
|
452
563
|
<div class="top">
|
|
453
|
-
<span class="verdict">${esc(
|
|
564
|
+
<span class="verdict">${esc(rowWord(e))}</span>${note ? `<span class="strict-note">${esc(note)}</span>` : ""}${modeBadge(e.mode)}
|
|
454
565
|
<code class="tool">${esc(e.toolId)}</code>
|
|
455
566
|
<time class="when" datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(clock(e.ts))} <span class="rel">${esc(relativeTime(e.ts, nowMs))}</span></time>
|
|
567
|
+
${cardControl(e, live)}
|
|
456
568
|
</div>
|
|
457
569
|
${summary}
|
|
458
570
|
${preview}
|
|
@@ -488,11 +600,11 @@ function changesPanel(changes, nowMs) {
|
|
|
488
600
|
.map((f) => {
|
|
489
601
|
const entries = f.entries
|
|
490
602
|
.map((e) => {
|
|
491
|
-
const t =
|
|
603
|
+
const t = rowTone(e);
|
|
492
604
|
const dataAttrs = filterDataAttrs(e, s.sessionId);
|
|
493
605
|
return ` <div class="chg-entry ${t}${e.never ? " never" : ""}" ${dataAttrs}>
|
|
494
606
|
<div class="top">
|
|
495
|
-
<span class="verdict">${esc(
|
|
607
|
+
<span class="verdict">${esc(rowWord(e))}</span>${modeBadge(e.mode)}
|
|
496
608
|
<code class="tool">${esc(e.toolId)}</code>
|
|
497
609
|
<time class="when" datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(clock(e.ts))} <span class="rel">${esc(relativeTime(e.ts, nowMs))}</span></time>
|
|
498
610
|
</div>
|
|
@@ -521,13 +633,41 @@ ${files}
|
|
|
521
633
|
})
|
|
522
634
|
.join("\n");
|
|
523
635
|
}
|
|
524
|
-
|
|
636
|
+
/** Default base URL `kya init` writes. It is a placeholder, not a plane that runs. */
|
|
637
|
+
const DEFAULT_PLANE_URL = "http://127.0.0.1:8090";
|
|
638
|
+
/** A control plane counts as configured with an API key, saved hosted login, or a base URL other than the init default. */
|
|
639
|
+
export function isPlaneConfigured(identity, env = process.env) {
|
|
640
|
+
if (env.KYA_API_KEY?.trim() || env.SHIELD_API_KEY?.trim() || env.KYA_BASE_URL?.trim())
|
|
641
|
+
return true;
|
|
642
|
+
if (readCredentials(env))
|
|
643
|
+
return true;
|
|
644
|
+
const base = identity?.baseUrl?.trim().replace(/\/+$/, "");
|
|
645
|
+
return Boolean(base && base !== DEFAULT_PLANE_URL);
|
|
646
|
+
}
|
|
647
|
+
/** Who the report is for. Host and plane address show only when a plane is configured. */
|
|
648
|
+
function identityBits(identity, planeConfigured) {
|
|
525
649
|
if (!identity)
|
|
526
|
-
return
|
|
650
|
+
return [];
|
|
651
|
+
// Hidden fields are still checked: a credential in the saved base URL must stop the render, shown or not.
|
|
652
|
+
assertNoSecrets([identity.agentName, identity.agentId, identity.host, identity.baseUrl].filter(Boolean).join(" · "));
|
|
527
653
|
const nameId = identity.agentName && identity.agentId
|
|
528
654
|
? `${identity.agentName}/${identity.agentId}`
|
|
529
655
|
: (identity.agentName ?? identity.agentId);
|
|
530
|
-
|
|
656
|
+
return [nameId, ...(planeConfigured ? [identity.host, identity.baseUrl] : [])].filter((b) => typeof b === "string" && b.trim().length > 0);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Header block of the live report (F4): the "Share this report" button and the place for its result. The button
|
|
660
|
+
* calls POST /action/share (the same redacted payload as `kya receipt --share`). Static files do not get it:
|
|
661
|
+
* there is no server to answer.
|
|
662
|
+
*/
|
|
663
|
+
function shareHeaderBlock() {
|
|
664
|
+
return `<div class="share-row" id="share-report" style="margin-top:0.75rem">
|
|
665
|
+
<button type="button" class="stat" id="share-report-btn">Share this report</button>
|
|
666
|
+
<div id="share-report-result" role="status" aria-live="polite"></div>
|
|
667
|
+
</div>`;
|
|
668
|
+
}
|
|
669
|
+
function identityLine(identity, planeConfigured) {
|
|
670
|
+
const bits = identityBits(identity, planeConfigured);
|
|
531
671
|
if (bits.length === 0)
|
|
532
672
|
return "";
|
|
533
673
|
// baseUrl is inert text here - never an anchor.
|
|
@@ -645,7 +785,20 @@ function dashboardPanel(db) {
|
|
|
645
785
|
</button>`;
|
|
646
786
|
// Zero-count rows are hidden, consistent with the header chips.
|
|
647
787
|
const mixRow = (label, group, value, count, fill) => count > 0 ? barRow(label, group, value, pct(count), `${pct(count)}%`, fill) : "";
|
|
648
|
-
const
|
|
788
|
+
const om = db.outcomeMix;
|
|
789
|
+
const mixCard = om.recorded > 0
|
|
790
|
+
? `<div class="db-card">
|
|
791
|
+
<h3>Outcome mix</h3>
|
|
792
|
+
${mixRow("Ran", "outcome", "ran", om.ran, "ok")}
|
|
793
|
+
${mixRow("Blocked", "outcome", "blocked", om.blocked, "bad")}
|
|
794
|
+
${mixRow("Held", "outcome", "held", om.held, "warn")}
|
|
795
|
+
${mixRow("Error", "outcome", "error", om.error, "warn")}
|
|
796
|
+
${mixRow("Earlier: Allow", "verdict", "ALLOW", om.legacyMix.allow, "ok")}
|
|
797
|
+
${mixRow("Earlier: Review", "verdict", "REQUIRE_APPROVE", om.legacyMix.review, "warn")}
|
|
798
|
+
${mixRow("Earlier: Deny", "verdict", "DENY", om.legacyMix.deny, "bad")}
|
|
799
|
+
${mixRow("Never", "never", "1", db.verdictMix.never, "bad")}
|
|
800
|
+
</div>`
|
|
801
|
+
: `<div class="db-card">
|
|
649
802
|
<h3>Verdict mix</h3>
|
|
650
803
|
${mixRow("Allow", "verdict", "ALLOW", db.verdictMix.allow, "ok")}
|
|
651
804
|
${mixRow("Review", "verdict", "REQUIRE_APPROVE", db.verdictMix.review, "warn")}
|
|
@@ -1378,6 +1531,46 @@ function kpiTile(label, value, cls = "") {
|
|
|
1378
1531
|
const toned = typeof value === "number" && value > 0 && cls ? ` ${cls}` : "";
|
|
1379
1532
|
return `<div class="kpi"><span class="kpi-num${toned}">${v}</span><span class="kpi-lab">${label}</span></div>`;
|
|
1380
1533
|
}
|
|
1534
|
+
/** Rows the hook stopped (outcome "blocked", F3), newest first. Rows with no outcome are not counted. */
|
|
1535
|
+
export function blockedCalls(events) {
|
|
1536
|
+
return events.filter((e) => outcomeOf(e) === "blocked").sort((a, b) => b.ts.localeCompare(a.ts));
|
|
1537
|
+
}
|
|
1538
|
+
const BLOCKED_ROWS_SHOWN = 5;
|
|
1539
|
+
/**
|
|
1540
|
+
* First panel of the report (guard first): how many calls the guard stopped and the last five.
|
|
1541
|
+
* With none blocked it shows the three built-in demo calls, labelled as a demo, never as activity.
|
|
1542
|
+
*/
|
|
1543
|
+
function heroBlockedPanel(events, nowMs, windowLabel, live) {
|
|
1544
|
+
const blocked = blockedCalls(events);
|
|
1545
|
+
if (blocked.length === 0) {
|
|
1546
|
+
const demo = DEMO_CALLS.map((c) => ` <li><code>${esc(c.label)}</code><span class="blk-what">${esc(c.what)}</span><span class="pill">demo</span></li>`).join("\n");
|
|
1547
|
+
return `<section class="panel hero-blocked demo" id="frag-hero-blocked" aria-label="Blocked">
|
|
1548
|
+
<h2>Blocked <span class="pill">demo</span></h2>
|
|
1549
|
+
<p class="blk-lead"><span class="blk-num">0</span> calls blocked ${esc(windowLabel)}. Demo: three calls your agent could make, and the guard stops all three.</p>
|
|
1550
|
+
<ul class="blk-rows">
|
|
1551
|
+
${demo}
|
|
1552
|
+
</ul>
|
|
1553
|
+
<p class="mute small">These rows are samples, not your activity. Real blocked calls replace them here.</p>
|
|
1554
|
+
</section>`;
|
|
1555
|
+
}
|
|
1556
|
+
const rows = blocked
|
|
1557
|
+
.slice(0, BLOCKED_ROWS_SHOWN)
|
|
1558
|
+
.map((e) => {
|
|
1559
|
+
const what = e.summary?.trim() ? esc(clip(e.summary.trim(), 140)) : "";
|
|
1560
|
+
const rule = e.neverRule ? `<span class="mute small">rule <code>${esc(e.neverRule)}</code></span>` : "";
|
|
1561
|
+
return ` <li><code>${esc(e.toolId)}</code><span class="blk-what">${what}</span>${rule}<span class="mute small">${esc(relativeTime(e.ts, nowMs))}</span>${cardControl(e, live)}</li>`;
|
|
1562
|
+
})
|
|
1563
|
+
.join("\n");
|
|
1564
|
+
const more = blocked.length > BLOCKED_ROWS_SHOWN ? `<p class="mute small">Showing the last ${BLOCKED_ROWS_SHOWN} of ${blocked.length}. The Activity tab lists every call.</p>` : "";
|
|
1565
|
+
return `<section class="panel hero-blocked" id="frag-hero-blocked" aria-label="Blocked">
|
|
1566
|
+
<h2>Blocked</h2>
|
|
1567
|
+
<p class="blk-lead"><span class="blk-num bad">${blocked.length}</span> ${blocked.length === 1 ? "call" : "calls"} blocked ${esc(windowLabel)}.</p>
|
|
1568
|
+
<ul class="blk-rows">
|
|
1569
|
+
${rows}
|
|
1570
|
+
</ul>
|
|
1571
|
+
${more}
|
|
1572
|
+
</section>`;
|
|
1573
|
+
}
|
|
1381
1574
|
/**
|
|
1382
1575
|
* Hero Certify panel: purpose-built rich card for the live Agent Trust
|
|
1383
1576
|
* Baseline. The section carries the result state (`hero-certify pass|gap|
|
|
@@ -1540,6 +1733,23 @@ function verdictsHeroCard(c, total) {
|
|
|
1540
1733
|
// raw event count keeps every bar inside the base the Overview card uses.
|
|
1541
1734
|
const pct = (n) => (total > 0 ? Math.round((n / total) * 100) : 0);
|
|
1542
1735
|
const mixRow = (label, n, fill) => `<div class="mix-row"><span class="mix-lab">${label}</span><span class="mix-bar"><span class="mix-fill ${fill}" style="width:${pct(n)}%"></span></span><span class="mix-num">${n}</span></div>`;
|
|
1736
|
+
const o = c.outcomes;
|
|
1737
|
+
if (o.recorded > 0) {
|
|
1738
|
+
const row = (label, n, fill) => (n > 0 ? mixRow(label, n, fill) : "");
|
|
1739
|
+
return `<section class="panel" aria-label="Verdicts">
|
|
1740
|
+
<h2>Decisions</h2>
|
|
1741
|
+
<div class="mix">
|
|
1742
|
+
${mixRow("Ran", o.ran, "ok")}
|
|
1743
|
+
${mixRow("Blocked", o.blocked, "bad")}
|
|
1744
|
+
${mixRow("Held", o.held, "warn")}
|
|
1745
|
+
${row("Error", o.error, "warn")}
|
|
1746
|
+
${row("Earlier: Allow", c.allow, "ok")}
|
|
1747
|
+
${row("Earlier: Deny", c.deny, "bad")}
|
|
1748
|
+
${row("Earlier: Review", c.require, "warn")}
|
|
1749
|
+
${mixRow("Never", c.never, "bad")}
|
|
1750
|
+
</div>
|
|
1751
|
+
</section>`;
|
|
1752
|
+
}
|
|
1543
1753
|
return `<section class="panel" aria-label="Verdicts">
|
|
1544
1754
|
<h2>Decisions</h2>
|
|
1545
1755
|
<div class="mix">
|
|
@@ -1904,7 +2114,7 @@ export function renderFeedPage(events, filters, page, nowMs, live, query, embedd
|
|
|
1904
2114
|
total = ranked.length;
|
|
1905
2115
|
}
|
|
1906
2116
|
else {
|
|
1907
|
-
const sorted = [...events].sort(
|
|
2117
|
+
const sorted = [...events].sort(compareFeed);
|
|
1908
2118
|
ordered = applyChipFilters(sorted, filters);
|
|
1909
2119
|
total = ordered.length;
|
|
1910
2120
|
}
|
|
@@ -1948,6 +2158,21 @@ const zoneBody = (parts, empty) => parts.some((p) => p.length > 0)
|
|
|
1948
2158
|
: `<p class="mute small zone-empty">${empty}</p>`;
|
|
1949
2159
|
/** Inner markup of the Activity counts bar (verdict buttons + mode/plane chips). */
|
|
1950
2160
|
function statCountsInner(c, chips) {
|
|
2161
|
+
const o = c.outcomes;
|
|
2162
|
+
if (o.recorded > 0) {
|
|
2163
|
+
const btn = (label, n, group, value, cls, title) => `<button type="button" class="stat${cls ? ` ${cls}` : ""}" data-fgroup="${group}" data-fvalue="${value}" aria-pressed="false" title="Filter: ${title}">${label}<b>${n}</b></button>`;
|
|
2164
|
+
// Rows from before outcomes existed keep their verdict counts, labelled as earlier rows.
|
|
2165
|
+
const earlier = (label, n, value, cls) => n > 0 ? btn(`Earlier: ${label}`, n, "verdict", value, cls, `${label} (rows with no outcome recorded)`) : "";
|
|
2166
|
+
const howTo = o.wouldHold > 0 ? `\n <span class="strict-howto">${strictHowTo(o.wouldHold, (t) => `<code>${t}</code>`)}</span>` : "";
|
|
2167
|
+
return `${btn("Ran", o.ran, "outcome", "ran", "ok", "Ran")}
|
|
2168
|
+
${btn("Blocked", o.blocked, "outcome", "blocked", "bad", "Blocked")}
|
|
2169
|
+
${btn("Held", o.held, "outcome", "held", "warn", "Held")}
|
|
2170
|
+
${o.error > 0 ? btn("Error", o.error, "outcome", "error", "", "Error") : ""}
|
|
2171
|
+
${earlier("Allow", c.allow, "ALLOW", "ok")}${earlier("Deny", c.deny, "DENY", "bad")}${earlier("Review", c.require, "REQUIRE_APPROVE", "warn")}
|
|
2172
|
+
<button type="button" class="stat" data-fgroup="never" data-fvalue="1" aria-pressed="false" title="Filter: Never">Never<b>${c.never}</b></button>
|
|
2173
|
+
${chips.modes}${chips.planes}${howTo}
|
|
2174
|
+
<button type="button" class="stat clear-filters" id="clear-filters" hidden>Clear filters ×</button>`;
|
|
2175
|
+
}
|
|
1951
2176
|
return `<button type="button" class="stat ok" data-fgroup="verdict" data-fvalue="ALLOW" aria-pressed="false" title="Filter: Allow">Allow<b>${c.allow}</b></button>
|
|
1952
2177
|
<button type="button" class="stat bad" data-fgroup="verdict" data-fvalue="DENY" aria-pressed="false" title="Filter: Deny">Deny<b>${c.deny}</b></button>
|
|
1953
2178
|
<button type="button" class="stat warn" data-fgroup="verdict" data-fvalue="REQUIRE_APPROVE" aria-pressed="false" title="Filter: Review">Review<b>${c.require}</b></button>
|
|
@@ -1975,7 +2200,7 @@ export function buildLiveFragments(model, livePaged) {
|
|
|
1975
2200
|
chipProducts: chips.products,
|
|
1976
2201
|
chipProjects: chips.projects,
|
|
1977
2202
|
chipServers: chips.servers,
|
|
1978
|
-
|
|
2203
|
+
heroBlocked: heroBlockedPanel(events, nowMs, `in the ${model.rangeLabel}`, model.live),
|
|
1979
2204
|
heroTiles: [
|
|
1980
2205
|
verdictsHeroCard(c, events.length),
|
|
1981
2206
|
activityHeroCard(agg, dashboard, events.length),
|
|
@@ -1983,18 +2208,24 @@ export function buildLiveFragments(model, livePaged) {
|
|
|
1983
2208
|
importHeroCard(model.imports, nowMs, livePaged),
|
|
1984
2209
|
discoveryHeroCard(model.discovery, nowMs, livePaged),
|
|
1985
2210
|
investigateHeroCard(model.investigate, nowMs, livePaged),
|
|
1986
|
-
showbackHeroCard(model.showback, events),
|
|
1987
2211
|
].join("\n "),
|
|
2212
|
+
showbackPanel: showbackHeroCard(model.showback, events),
|
|
2213
|
+
connectedBody: zoneBody([wiredHostsPanel(model.wiredHosts)], "No coding agent is connected yet. Run kya connect <host> to add one."),
|
|
1988
2214
|
overviewPanels: zoneBody([incidentAlertsPanel(model.incidentAlerts, nowMs), dashboardPanel(dashboard), sessionsPanel(agg, nowMs, changes), reasonsPanel(agg)], "No activity in this window yet - analytics appear once events land."),
|
|
1989
|
-
certifyBody: zoneBody([
|
|
2215
|
+
certifyBody: zoneBody([
|
|
2216
|
+
heroCertifyPanel(model.certify),
|
|
2217
|
+
model.certify
|
|
2218
|
+
? certifyDetailPanel(model.certify.requirements)
|
|
2219
|
+
: `<p class="mute small zone-empty">The full rule table appears here after the first check.</p>`,
|
|
2220
|
+
], "No check yet - use your coding tools with kya connected, then run kya certify."),
|
|
1990
2221
|
orrPanel: orrPanel(model.orr, nowMs, livePaged),
|
|
1991
2222
|
changesList: changesPanel(changes, nowMs),
|
|
1992
2223
|
},
|
|
1993
2224
|
nav: {
|
|
1994
|
-
overview: events.length,
|
|
2225
|
+
overview: blockedCalls(events).length,
|
|
1995
2226
|
activity: events.length,
|
|
1996
2227
|
changes: changes.fileCount,
|
|
1997
|
-
certify:
|
|
2228
|
+
certify: null,
|
|
1998
2229
|
orr: model.orr ? model.orr.scorecards.fail : null,
|
|
1999
2230
|
},
|
|
2000
2231
|
};
|
|
@@ -2398,8 +2629,10 @@ export function renderReceiptHtml(model) {
|
|
|
2398
2629
|
chipProducts: 'chip-products',
|
|
2399
2630
|
chipProjects: 'chip-projects',
|
|
2400
2631
|
chipServers: 'chip-servers',
|
|
2401
|
-
|
|
2632
|
+
heroBlocked: 'frag-hero-blocked',
|
|
2402
2633
|
heroTiles: 'frag-hero-tiles',
|
|
2634
|
+
showbackPanel: 'frag-showback',
|
|
2635
|
+
connectedBody: 'frag-connected',
|
|
2403
2636
|
overviewPanels: 'frag-overview-panels',
|
|
2404
2637
|
certifyBody: 'frag-certify',
|
|
2405
2638
|
orrPanel: 'frag-orr',
|
|
@@ -2616,6 +2849,25 @@ export function renderReceiptHtml(model) {
|
|
|
2616
2849
|
try { localStorage.setItem('kya-theme', root.getAttribute('data-theme')); } catch(e){}
|
|
2617
2850
|
});
|
|
2618
2851
|
})();
|
|
2852
|
+
</script>`;
|
|
2853
|
+
// The "More" menu: closed by default, remembered per browser, and open when the page was reached through
|
|
2854
|
+
// a link to a tab inside it (a hash such as #certify). Without JS or localStorage the page still works.
|
|
2855
|
+
const moreScript = `<script>
|
|
2856
|
+
(function(){
|
|
2857
|
+
var more = document.getElementById('nav-more');
|
|
2858
|
+
if (!more) return;
|
|
2859
|
+
var inside = false;
|
|
2860
|
+
try { inside = !!(location.hash && more.querySelector('a[href="' + location.hash + '"]')); } catch(e){}
|
|
2861
|
+
var stored = null;
|
|
2862
|
+
try { stored = localStorage.getItem('kya-nav-more'); } catch(e){}
|
|
2863
|
+
if (inside || stored === 'open') more.open = true;
|
|
2864
|
+
more.addEventListener('toggle', function(){
|
|
2865
|
+
try { localStorage.setItem('kya-nav-more', more.open ? 'open' : 'closed'); } catch(e){}
|
|
2866
|
+
});
|
|
2867
|
+
window.addEventListener('hashchange', function(){
|
|
2868
|
+
try { if (location.hash && more.querySelector('a[href="' + location.hash + '"]')) more.open = true; } catch(e){}
|
|
2869
|
+
});
|
|
2870
|
+
})();
|
|
2619
2871
|
</script>`;
|
|
2620
2872
|
// Gateway interactive controls: playground evaluator, quick actions, and the
|
|
2621
2873
|
// sidebar stop-report link. Only wired when the page is served by the live
|
|
@@ -2775,6 +3027,68 @@ export function renderReceiptHtml(model) {
|
|
|
2775
3027
|
});
|
|
2776
3028
|
});
|
|
2777
3029
|
|
|
3030
|
+
// The card control next to a stopped row. Delegated: the Blocked panel is replaced on every live refresh.
|
|
3031
|
+
document.addEventListener('click', function(ev){
|
|
3032
|
+
var b = ev.target && ev.target.closest ? ev.target.closest('[data-card-id]') : null;
|
|
3033
|
+
if (!b) return;
|
|
3034
|
+
var out = b.parentNode.querySelector('.card-result');
|
|
3035
|
+
b.disabled = true;
|
|
3036
|
+
out.textContent = 'Making the card...';
|
|
3037
|
+
post('/action/card', { id: b.getAttribute('data-card-id') }).then(parseJson).then(function(d){
|
|
3038
|
+
out.textContent = d && d.ok ? 'Saved ' + (d.png || d.svg) + '. Caption: ' + d.caption.replace(/\\n/g, ' ') : 'No card: ' + (d && d.error ? d.error : 'unknown error');
|
|
3039
|
+
b.disabled = false;
|
|
3040
|
+
}).catch(function(err){ out.textContent = 'No card: ' + err.message; b.disabled = false; });
|
|
3041
|
+
});
|
|
3042
|
+
|
|
3043
|
+
var shareBtn = document.getElementById('share-report-btn');
|
|
3044
|
+
if (shareBtn) {
|
|
3045
|
+
var shareOut = document.getElementById('share-report-result');
|
|
3046
|
+
var shareFail = function(msg){
|
|
3047
|
+
setResult(shareOut, '<p>Could not share the report: ' + esc(msg) + '. The report still works. Try again when you are online.</p>', true);
|
|
3048
|
+
shareBtn.disabled = false;
|
|
3049
|
+
};
|
|
3050
|
+
var copyText = function(text, field, btn){
|
|
3051
|
+
var done = function(){ btn.textContent = 'Copied'; };
|
|
3052
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
3053
|
+
navigator.clipboard.writeText(text).then(done, function(){ field.select(); btn.textContent = 'Press Ctrl+C to copy'; });
|
|
3054
|
+
} else { field.select(); btn.textContent = 'Press Ctrl+C to copy'; }
|
|
3055
|
+
};
|
|
3056
|
+
shareBtn.addEventListener('click', function(){
|
|
3057
|
+
shareBtn.disabled = true;
|
|
3058
|
+
shareOut.textContent = 'Sharing...';
|
|
3059
|
+
post('/action/share', {}).then(parseJson).then(function(d){
|
|
3060
|
+
if (!d || !d.ok) { shareFail(d && d.error ? d.error : 'unknown error'); return; }
|
|
3061
|
+
// Show and open only an http(s) link. share.ts already checked it is on the server that was asked.
|
|
3062
|
+
if (!/^https?:/.test(String(d.url))) { shareFail('the server sent a link that is not a web address'); return; }
|
|
3063
|
+
shareOut.textContent = '';
|
|
3064
|
+
var label = document.createElement('p');
|
|
3065
|
+
label.textContent = 'Public link. Send it to a colleague:';
|
|
3066
|
+
var field = document.createElement('input');
|
|
3067
|
+
field.type = 'text'; field.readOnly = true; field.id = 'share-url'; field.value = d.url; field.size = 44;
|
|
3068
|
+
var copy = document.createElement('button');
|
|
3069
|
+
copy.type = 'button'; copy.className = 'stat'; copy.id = 'share-copy'; copy.textContent = 'Copy link';
|
|
3070
|
+
copy.addEventListener('click', function(){ copyText(d.url, field, copy); });
|
|
3071
|
+
shareOut.appendChild(label); shareOut.appendChild(field); shareOut.appendChild(copy);
|
|
3072
|
+
var claim = document.createElement('p');
|
|
3073
|
+
var a = document.createElement('a');
|
|
3074
|
+
a.id = 'share-claim'; a.href = d.url; a.target = '_blank'; a.rel = 'noopener';
|
|
3075
|
+
a.textContent = 'Open the page to claim it, keep it and get a weekly summary';
|
|
3076
|
+
claim.appendChild(a);
|
|
3077
|
+
shareOut.appendChild(claim);
|
|
3078
|
+
var connect = document.createElement('p');
|
|
3079
|
+
connect.id = 'share-connect';
|
|
3080
|
+
connect.textContent = 'To connect this computer to your account, run kya login.';
|
|
3081
|
+
shareOut.appendChild(connect);
|
|
3082
|
+
if (d.expiresAt) {
|
|
3083
|
+
var exp = document.createElement('p');
|
|
3084
|
+
exp.textContent = 'If nobody claims the report, it is deleted on ' + String(d.expiresAt).slice(0, 10) + '.';
|
|
3085
|
+
shareOut.appendChild(exp);
|
|
3086
|
+
}
|
|
3087
|
+
shareBtn.disabled = false;
|
|
3088
|
+
}).catch(function(err){ shareFail(err.message); });
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
|
|
2778
3092
|
var importForm = document.getElementById('import-form');
|
|
2779
3093
|
if (importForm) {
|
|
2780
3094
|
importForm.addEventListener('submit', function(e){
|
|
@@ -2840,7 +3154,6 @@ export function renderReceiptHtml(model) {
|
|
|
2840
3154
|
postureSectionHtml(model.posture, !!model.liveToken),
|
|
2841
3155
|
capabilitiesPanel(model),
|
|
2842
3156
|
gatePanel(model.gate, nowMs),
|
|
2843
|
-
wiredHostsPanel(model.wiredHosts),
|
|
2844
3157
|
sandboxesPanel(model.sandboxes, nowMs),
|
|
2845
3158
|
openshellPanel(model.openshell, nowMs),
|
|
2846
3159
|
alertsPanel(model.alerts, nowMs, livePaged),
|
|
@@ -2852,7 +3165,7 @@ export function renderReceiptHtml(model) {
|
|
|
2852
3165
|
<head>
|
|
2853
3166
|
<meta charset="utf-8"/>
|
|
2854
3167
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
2855
|
-
<title>${esc(
|
|
3168
|
+
<title>${esc(PROMISE)} - ${esc(model.rangeLabel)}</title>
|
|
2856
3169
|
<style>${receiptCss()}${model.liveToken ? MAP_CSS + MAP_LIVE_ZONE_CSS : ""}</style>
|
|
2857
3170
|
</head>
|
|
2858
3171
|
<body>
|
|
@@ -2864,20 +3177,25 @@ export function renderReceiptHtml(model) {
|
|
|
2864
3177
|
</div>
|
|
2865
3178
|
</header>
|
|
2866
3179
|
<nav class="sidebar-nav" aria-label="Report sections">
|
|
2867
|
-
${navItem("#overview", "
|
|
3180
|
+
${navItem("#overview", "Blocked", blockedCalls(events).length)}
|
|
2868
3181
|
${navItem("#activity", "Activity", events.length)}
|
|
2869
|
-
${navItem("#
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3182
|
+
${navItem("#connected", "Connected tools", undefined)}
|
|
3183
|
+
<details class="nav-more" id="nav-more">
|
|
3184
|
+
<summary class="nav-more-summary">More</summary>
|
|
3185
|
+
${navItem("#changes", "Changes", changes.fileCount)}
|
|
3186
|
+
${navItem("#certify", "Certify", undefined)}
|
|
3187
|
+
${navItem("#orr", "ORR", model.orr ? model.orr.scorecards.fail : undefined)}
|
|
3188
|
+
${model.liveToken ? navItem("#map", "Map", undefined) : ""}
|
|
3189
|
+
${model.showback ? navItem("#showback", "Showback", undefined) : ""}
|
|
3190
|
+
${navGroup("Gateway", model.gate?.state === "running")}
|
|
3191
|
+
${navSubItem("#gateway-home", "Home", model.gate?.servers.length ?? 0)}
|
|
3192
|
+
${navSubItem("#gateway-listeners", "Listeners", model.gate?.listeners?.length ?? 0)}
|
|
3193
|
+
${navSubItem("#gateway-routes", "Routes", model.gate?.routes?.length ?? 0)}
|
|
3194
|
+
${navSubItem("#gateway-backends", "Backends", model.gate?.servers.length ?? 0)}
|
|
3195
|
+
${navSubItem("#gateway-policies", "Policies", model.gate?.policySummary?.totalPolicies ?? 0)}
|
|
3196
|
+
${navSubItem("#gateway-playground", "Playground", model.gate?.playgroundSamples?.length ?? 0)}
|
|
3197
|
+
${navItem("#system", "System", undefined)}
|
|
3198
|
+
</details>
|
|
2881
3199
|
</nav>
|
|
2882
3200
|
<footer class="sidebar-footer">
|
|
2883
3201
|
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">Theme</button>
|
|
@@ -2889,16 +3207,17 @@ export function renderReceiptHtml(model) {
|
|
|
2889
3207
|
${model.liveToken ? '<main data-live-token="1">' : '<main>'}
|
|
2890
3208
|
<header class="page-header">
|
|
2891
3209
|
<div class="title-row">
|
|
2892
|
-
<h1>${esc(
|
|
3210
|
+
<h1>${esc(PROMISE)}</h1>
|
|
2893
3211
|
<div class="range">${esc(model.rangeLabel)}${livePill}</div>
|
|
2894
3212
|
</div>
|
|
2895
|
-
${identityLine(model.identity)}
|
|
3213
|
+
${identityLine(model.identity, model.planeConfigured)}
|
|
3214
|
+
${model.liveToken ? shareHeaderBlock() : ""}
|
|
2896
3215
|
</header>
|
|
2897
3216
|
${blockedBanner}
|
|
2898
3217
|
${iocBanner}
|
|
2899
3218
|
<section class="zone" id="overview" aria-labelledby="tab-overview">
|
|
2900
3219
|
<div class="hero">
|
|
2901
|
-
${fr.fragments.
|
|
3220
|
+
${fr.fragments.heroBlocked}
|
|
2902
3221
|
<div class="hero-tiles" id="frag-hero-tiles">
|
|
2903
3222
|
${fr.fragments.heroTiles}
|
|
2904
3223
|
</div>
|
|
@@ -2924,6 +3243,9 @@ ${initialFeed.feedHtml}
|
|
|
2924
3243
|
${initialFeed.paginationHtml}
|
|
2925
3244
|
</div>
|
|
2926
3245
|
</section>
|
|
3246
|
+
<section class="zone" id="connected" aria-labelledby="tab-connected">
|
|
3247
|
+
<div id="frag-connected">${fr.fragments.connectedBody}</div>
|
|
3248
|
+
</section>
|
|
2927
3249
|
<section class="zone" id="changes" aria-labelledby="tab-changes">
|
|
2928
3250
|
<section class="feed chg" id="changes-list" aria-label="Changes by session and file">
|
|
2929
3251
|
${fr.fragments.changesList}
|
|
@@ -2936,6 +3258,7 @@ ${fr.fragments.changesList}
|
|
|
2936
3258
|
<div id="frag-orr">${fr.fragments.orrPanel}</div>
|
|
2937
3259
|
</section>
|
|
2938
3260
|
${model.liveToken ? '<section class="zone" id="map" aria-labelledby="tab-map"><div id="frag-map"><p class="mute">Loading the map.</p></div></section>' : ""}
|
|
3261
|
+
${model.showback ? `<section class="zone" id="showback" aria-labelledby="tab-showback"><div id="frag-showback">${fr.fragments.showbackPanel}</div></section>` : ""}
|
|
2939
3262
|
${gatePagePanel(model.gate, nowMs)}
|
|
2940
3263
|
<section class="zone" id="system" aria-labelledby="tab-system">
|
|
2941
3264
|
${systemBody}
|
|
@@ -2945,6 +3268,7 @@ ${filterScript}
|
|
|
2945
3268
|
${model.liveToken ? liveMapScripts() : ""}
|
|
2946
3269
|
${staticSearchScript}
|
|
2947
3270
|
${themeScript}
|
|
3271
|
+
${moreScript}
|
|
2948
3272
|
${gateScript}
|
|
2949
3273
|
</body>
|
|
2950
3274
|
</html>`;
|
|
@@ -2952,28 +3276,83 @@ ${gateScript}
|
|
|
2952
3276
|
assertNoSecrets(html);
|
|
2953
3277
|
return html;
|
|
2954
3278
|
}
|
|
3279
|
+
/** "## Blocked" section: the count and the last five blocked calls, or the demo when none was blocked. */
|
|
3280
|
+
function blockedMarkdown(events, nowMs) {
|
|
3281
|
+
const blocked = blockedCalls(events);
|
|
3282
|
+
if (blocked.length === 0) {
|
|
3283
|
+
return [
|
|
3284
|
+
"## Blocked (demo)",
|
|
3285
|
+
"",
|
|
3286
|
+
"0 calls blocked in this window. Demo: three calls your agent could make, and the guard stops all three. These rows are samples, not your activity.",
|
|
3287
|
+
"",
|
|
3288
|
+
...DEMO_CALLS.map((c) => `- \`${mdInline(c.label)}\` - ${mdText(c.what)} (demo)`),
|
|
3289
|
+
"",
|
|
3290
|
+
];
|
|
3291
|
+
}
|
|
3292
|
+
return [
|
|
3293
|
+
"## Blocked",
|
|
3294
|
+
"",
|
|
3295
|
+
`${blocked.length} ${blocked.length === 1 ? "call" : "calls"} blocked in this window. Last ${Math.min(5, blocked.length)}:`,
|
|
3296
|
+
"",
|
|
3297
|
+
...blocked.slice(0, 5).map((e) => {
|
|
3298
|
+
const sum = e.summary?.trim() ? ` - ${mdText(clip(e.summary.trim(), 140))}` : "";
|
|
3299
|
+
return `- \`${mdInline(e.toolId)}\`${sum} - ${mdText(relativeTime(e.ts, nowMs))}`;
|
|
3300
|
+
}),
|
|
3301
|
+
"",
|
|
3302
|
+
];
|
|
3303
|
+
}
|
|
2955
3304
|
export function renderReceiptMarkdown(model) {
|
|
2956
3305
|
const c = countVerdicts(model.events);
|
|
2957
3306
|
const agg = aggregateEvents(model.events);
|
|
2958
3307
|
const nowMs = Date.now();
|
|
3308
|
+
const o = c.outcomes;
|
|
2959
3309
|
const lines = [
|
|
2960
|
-
`# ${mdText(
|
|
3310
|
+
`# ${mdText(PROMISE)}`,
|
|
2961
3311
|
"",
|
|
2962
3312
|
mdText(model.rangeLabel),
|
|
2963
3313
|
"",
|
|
2964
|
-
|
|
3314
|
+
...blockedMarkdown(model.events, nowMs),
|
|
3315
|
+
];
|
|
3316
|
+
if (o.recorded > 0) {
|
|
3317
|
+
// Counted by what happened to each call. Rows from before outcomes existed keep their verdict counts, on a second line.
|
|
3318
|
+
lines.push(`${outcomeHeadline(o)} | Never ${c.never}`, "");
|
|
3319
|
+
if (o.wouldHold > 0)
|
|
3320
|
+
lines.push(strictHowTo(o.wouldHold, (t) => `\`${t}\``), "");
|
|
3321
|
+
if (o.legacy > 0) {
|
|
3322
|
+
lines.push(`Earlier calls (no outcome recorded): Allow ${c.allow} | Deny ${c.deny} | Review ${c.require}`, "");
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
else {
|
|
3326
|
+
lines.push(`Allow ${c.allow} | Deny ${c.deny} | Review ${c.require} | Never ${c.never}`, "");
|
|
3327
|
+
}
|
|
3328
|
+
// Feed rows. With outcomes recorded the feed leads (blocked rows first); without, it stays where it was.
|
|
3329
|
+
const feedLines = [
|
|
3330
|
+
"## Feed",
|
|
3331
|
+
...collapseFeedRows(o.recorded > 0 ? [...model.events].sort(compareFeed) : [...model.events].sort((a, b) => b.ts.localeCompare(a.ts)))
|
|
3332
|
+
.flatMap(({ event: e, count }) => {
|
|
3333
|
+
const text = count > 1 ? repeatText(e, count) : e.summary?.trim();
|
|
3334
|
+
const sum = text ? ` - ${mdText(text)}` : "";
|
|
3335
|
+
const note = ranNote(e);
|
|
3336
|
+
const head = `- **${mdText(rowWord(e))}**${note ? ` (${note})` : ""} \`${mdInline(e.toolId)}\`${sum} - ${productLabel(e.product)} - ${mdText(e.reasonCode)}`;
|
|
3337
|
+
if (!e.diffPreview?.trim())
|
|
3338
|
+
return [head];
|
|
3339
|
+
// Fenced content is verbatim (stripEscapes only - backslash escapes
|
|
3340
|
+
// are literal in code fences); the dynamically sized tilde fence
|
|
3341
|
+
// prevents breakout and backticks cannot close a tilde fence.
|
|
3342
|
+
const diff = stripEscapes(e.diffPreview.trim());
|
|
3343
|
+
const fence = mdFence(diff);
|
|
3344
|
+
return [head, "", fence, diff, fence, ""];
|
|
3345
|
+
}),
|
|
2965
3346
|
"",
|
|
2966
3347
|
];
|
|
3348
|
+
if (o.recorded > 0)
|
|
3349
|
+
lines.push(...feedLines);
|
|
2967
3350
|
if (model.ioc) {
|
|
2968
3351
|
const k = model.ioc.findings.length;
|
|
2969
3352
|
lines.push(mdText(`IoC check (advisory): ${k === 0 ? "no findings" : `${k} finding${k === 1 ? "" : "s"}`}, list ${model.ioc.listVersion}, checked ${model.ioc.checkedAt}.`), "");
|
|
2970
3353
|
}
|
|
2971
3354
|
if (model.identity) {
|
|
2972
|
-
const
|
|
2973
|
-
const nameId = id.agentName && id.agentId
|
|
2974
|
-
? `${id.agentName}/${id.agentId}`
|
|
2975
|
-
: (id.agentName ?? id.agentId);
|
|
2976
|
-
const bits = [nameId, id.host, id.baseUrl].filter((b) => typeof b === "string" && b.trim().length > 0);
|
|
3355
|
+
const bits = identityBits(model.identity, model.planeConfigured);
|
|
2977
3356
|
if (bits.length > 0) {
|
|
2978
3357
|
lines.push(mdText(bits.join(" · ")), "");
|
|
2979
3358
|
}
|
|
@@ -2988,7 +3367,7 @@ export function renderReceiptMarkdown(model) {
|
|
|
2988
3367
|
agg.planes.runtime > 0 ? `Runtime ${agg.planes.runtime}` : "",
|
|
2989
3368
|
].filter(Boolean);
|
|
2990
3369
|
if (modeBits.length > 0 || planeBits.length > 0) {
|
|
2991
|
-
lines.push(`Modes: ${modeBits.join(" | ") || "-"} ·
|
|
3370
|
+
lines.push(`Modes: ${modeBits.join(" | ") || "-"} · Source: ${planeBits.join(" | ") || "-"}`, "");
|
|
2992
3371
|
}
|
|
2993
3372
|
if (agg.products.length >= 2) {
|
|
2994
3373
|
lines.push("## Products", ...agg.products.map((p) => `- ${mdText(p.label)} × ${p.count}`), "");
|
|
@@ -2996,23 +3375,16 @@ export function renderReceiptMarkdown(model) {
|
|
|
2996
3375
|
if (agg.projects.length >= 2) {
|
|
2997
3376
|
lines.push("## Projects", ...agg.projects.map((p) => `- ${mdText(p.label)} × ${p.count}`), "");
|
|
2998
3377
|
}
|
|
2999
|
-
if (model.certify) {
|
|
3000
|
-
const c = model.certify;
|
|
3001
|
-
lines.push("## Certify", `result: ${c.result} · counts: ${c.pass} pass · ${c.gap} gap · ${c.insufficientEvidence} insufficient · ${c.attested} attested`);
|
|
3002
|
-
if (c.topGaps.length > 0) {
|
|
3003
|
-
lines.push("Top gaps:");
|
|
3004
|
-
for (const g of c.topGaps) {
|
|
3005
|
-
lines.push(`- \`${mdInline(g.id)}\` - ${mdText(g.title)} (${mdText(g.severity)})`);
|
|
3006
|
-
}
|
|
3007
|
-
}
|
|
3008
|
-
lines.push(`checked live - last ${c.windowDays} days, ${c.trailEvents} recorded events · the Certify tab lists every rule · kya certify writes the gap report and signed proof file`, "");
|
|
3009
|
-
}
|
|
3010
3378
|
const db = computeDashboard(model.events);
|
|
3011
3379
|
if (db.verdictMix.total > 0) {
|
|
3012
3380
|
const pct = (n) => Math.round((n / db.verdictMix.total) * 100);
|
|
3013
3381
|
// never is a reason overlay: the Never share is computed from neverEvent
|
|
3014
3382
|
// rows on top of the same 100% = total events base as the verdict buckets.
|
|
3015
|
-
|
|
3383
|
+
const om = db.outcomeMix;
|
|
3384
|
+
const mixLine = om.recorded > 0
|
|
3385
|
+
? `Outcome mix: Ran ${pct(om.ran)}% · Blocked ${pct(om.blocked)}% · Held ${pct(om.held)}%${om.error > 0 ? ` · Error ${pct(om.error)}%` : ""}${om.legacy > 0 ? ` · Earlier (no outcome): Allow ${pct(om.legacyMix.allow)}% · Review ${pct(om.legacyMix.review)}% · Deny ${pct(om.legacyMix.deny)}%` : ""} · Never ${pct(db.verdictMix.never)}%`
|
|
3386
|
+
: `Verdict mix: Allow ${pct(db.verdictMix.allow)}% · Review ${pct(db.verdictMix.review)}% · Deny ${pct(db.verdictMix.deny)}% · Never ${pct(db.verdictMix.never)}%`;
|
|
3387
|
+
lines.push("## Analytics", "", mixLine, "");
|
|
3016
3388
|
if (db.activity.buckets.length > 0) {
|
|
3017
3389
|
const max = Math.max(1, ...db.activity.buckets.map((b) => b.count));
|
|
3018
3390
|
lines.push(`Activity (${db.activity.granularity === "hour" ? "hourly" : "daily"}):`);
|
|
@@ -3040,20 +3412,8 @@ export function renderReceiptMarkdown(model) {
|
|
|
3040
3412
|
lines.push("");
|
|
3041
3413
|
}
|
|
3042
3414
|
}
|
|
3043
|
-
|
|
3044
|
-
.
|
|
3045
|
-
.flatMap((e) => {
|
|
3046
|
-
const sum = e.summary?.trim() ? ` - ${mdText(e.summary.trim())}` : "";
|
|
3047
|
-
const head = `- **${mdText(verdictWord(e.verdict))}** \`${mdInline(e.toolId)}\`${sum} - ${productLabel(e.product)} - ${mdText(e.reasonCode)}`;
|
|
3048
|
-
if (!e.diffPreview?.trim())
|
|
3049
|
-
return [head];
|
|
3050
|
-
// Fenced content is verbatim (stripEscapes only - backslash escapes
|
|
3051
|
-
// are literal in code fences); the dynamically sized tilde fence
|
|
3052
|
-
// prevents breakout and backticks cannot close a tilde fence.
|
|
3053
|
-
const diff = stripEscapes(e.diffPreview.trim());
|
|
3054
|
-
const fence = mdFence(diff);
|
|
3055
|
-
return [head, "", fence, diff, fence, ""];
|
|
3056
|
-
}), "");
|
|
3415
|
+
if (o.recorded === 0)
|
|
3416
|
+
lines.push(...feedLines);
|
|
3057
3417
|
// Same normalization as the HTML path: targetPath flows into single-line
|
|
3058
3418
|
// `#### ` code-span headers, so embedded newlines must be flattened first.
|
|
3059
3419
|
const changes = buildChangesModel(normalizeTrailEvents(model.events));
|
|
@@ -3068,7 +3428,7 @@ export function renderReceiptMarkdown(model) {
|
|
|
3068
3428
|
// fence one longer than the longest run inside.
|
|
3069
3429
|
const diff = stripEscapes(e.preview);
|
|
3070
3430
|
const fence = mdFence(diff);
|
|
3071
|
-
lines.push(`- **${mdText(
|
|
3431
|
+
lines.push(`- **${mdText(rowWord(e))}** \`${mdInline(e.toolId)}\` - ${mdText(e.ts)}`, "", fence, diff, fence, "");
|
|
3072
3432
|
}
|
|
3073
3433
|
}
|
|
3074
3434
|
}
|
|
@@ -3174,6 +3534,18 @@ export function renderReceiptMarkdown(model) {
|
|
|
3174
3534
|
const ot = model.otel;
|
|
3175
3535
|
lines.push("## Export to your monitoring (OTel)", `${ot.endpoint ? `\`${mdInline(ot.endpoint)}\` · ` : ""}${ot.spansSent} sent · ${ot.spansFailed} failed${ot.lastExportAt ? ` · last export ${relativeTime(ot.lastExportAt, nowMs)}` : ""}`, "");
|
|
3176
3536
|
}
|
|
3537
|
+
// Certify last: no gap count reaches the top of the report.
|
|
3538
|
+
if (model.certify) {
|
|
3539
|
+
const c = model.certify;
|
|
3540
|
+
lines.push("## Certify", `result: ${c.result} · counts: ${c.pass} pass · ${c.gap} gap · ${c.insufficientEvidence} insufficient · ${c.attested} attested`);
|
|
3541
|
+
if (c.topGaps.length > 0) {
|
|
3542
|
+
lines.push("Top gaps:");
|
|
3543
|
+
for (const g of c.topGaps) {
|
|
3544
|
+
lines.push(`- \`${mdInline(g.id)}\` - ${mdText(g.title)} (${mdText(g.severity)})`);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
lines.push(`checked live - last ${c.windowDays} days, ${c.trailEvents} recorded events · the Certify tab lists every rule · kya certify writes the gap report and signed proof file`, "");
|
|
3548
|
+
}
|
|
3177
3549
|
const md = `${lines.join("\n")}\n`;
|
|
3178
3550
|
assertNoSecrets(md);
|
|
3179
3551
|
return md;
|