@shield-agent/kya 0.14.0 → 0.16.0
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/.mcp.json +1 -1
- package/README.md +6 -4
- package/amp/settings.example.json +1 -1
- 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/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/start.d.ts +44 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +171 -2
- package/dist/commands/start.js.map +1 -1
- package/dist/gate/config.d.ts +2 -0
- package/dist/gate/config.d.ts.map +1 -1
- package/dist/gate/config.js +9 -0
- package/dist/gate/config.js.map +1 -1
- package/dist/gate/discover.d.ts +57 -0
- package/dist/gate/discover.d.ts.map +1 -0
- package/dist/gate/discover.js +380 -0
- package/dist/gate/discover.js.map +1 -0
- package/dist/receipt/enrich.d.ts +1 -0
- package/dist/receipt/enrich.d.ts.map +1 -1
- package/dist/receipt/enrich.js +1 -0
- package/dist/receipt/enrich.js.map +1 -1
- package/dist/receipt/gate-card.d.ts +4 -0
- package/dist/receipt/gate-card.d.ts.map +1 -1
- package/dist/receipt/gate-card.js +3 -1
- package/dist/receipt/gate-card.js.map +1 -1
- package/dist/receipt/gate-page.d.ts +47 -0
- package/dist/receipt/gate-page.d.ts.map +1 -0
- package/dist/receipt/gate-page.js +116 -0
- package/dist/receipt/gate-page.js.map +1 -0
- package/dist/receipt/live-server.d.ts +8 -0
- package/dist/receipt/live-server.d.ts.map +1 -1
- package/dist/receipt/live-server.js +134 -1
- package/dist/receipt/live-server.js.map +1 -1
- package/dist/receipt/receipt-css.d.ts +6 -5
- package/dist/receipt/receipt-css.d.ts.map +1 -1
- package/dist/receipt/receipt-css.js +255 -55
- package/dist/receipt/receipt-css.js.map +1 -1
- package/dist/receipt/render-receipt.d.ts +2 -2
- package/dist/receipt/render-receipt.d.ts.map +1 -1
- package/dist/receipt/render-receipt.js +420 -55
- package/dist/receipt/render-receipt.js.map +1 -1
- 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 +1 -1
- 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 +1 -1
- package/qwen/settings.example.json +1 -1
- package/server.json +2 -2
|
@@ -10,7 +10,7 @@ import { loadCertifyCard, loadIdentity, loadOrrCard, loadSandboxes, loadShowback
|
|
|
10
10
|
import { SHOWBACK_DISCLAIMER, } from "../showback/cost-per-task.js";
|
|
11
11
|
import { computeDashboard } from "./dashboard.js";
|
|
12
12
|
import { buildChangesModel } from "./changes.js";
|
|
13
|
-
import {
|
|
13
|
+
import { GATE_NOT_SETUP_QUICKSTART, loadGatePage, LOOPBACK_RULE, toolNameFromToolId, toolServerPrefix, } from "./gate-page.js";
|
|
14
14
|
import { receiptCss } from "./receipt-css.js";
|
|
15
15
|
function esc(s) {
|
|
16
16
|
return stripEscapes(s)
|
|
@@ -249,7 +249,7 @@ export function loadReceiptModel(input) {
|
|
|
249
249
|
identity: loadIdentity(input.cwd),
|
|
250
250
|
sandboxes: loadSandboxes(input.cwd),
|
|
251
251
|
wiredHosts: loadWiredHosts(input.cwd),
|
|
252
|
-
gate:
|
|
252
|
+
gate: loadGatePage(events),
|
|
253
253
|
orr: loadOrrCard(input.cwd),
|
|
254
254
|
certify: loadCertifyCard(input.cwd),
|
|
255
255
|
showback,
|
|
@@ -265,6 +265,25 @@ export function loadReceiptModel(input) {
|
|
|
265
265
|
}
|
|
266
266
|
return buildWindowReceiptModel(events, days, extras);
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Build the data-* filter facet attribute string shared by feed articles and
|
|
270
|
+
* change entries. `sessionId` is passed explicitly because change entries roll
|
|
271
|
+
* up under their session heading.
|
|
272
|
+
*/
|
|
273
|
+
function filterDataAttrs(e, sessionId) {
|
|
274
|
+
const never = e.neverEvent || e.reasonCode === "NEVER_EVENT";
|
|
275
|
+
const project = e.project?.trim();
|
|
276
|
+
const server = parseMcpToolId(e.toolId);
|
|
277
|
+
return (`data-verdict="${esc(e.verdict.toUpperCase())}"` +
|
|
278
|
+
(never ? ` data-never="1"` : "") +
|
|
279
|
+
` data-mode="${esc(e.mode)}"` +
|
|
280
|
+
` data-plane="${esc(e.host?.trim() || "unknown")}"` +
|
|
281
|
+
` data-product="${esc(e.product ?? "other")}"` +
|
|
282
|
+
` data-tool="${esc(clip(e.toolId, 60))}"` +
|
|
283
|
+
` data-session="${esc(sessionId)}"` +
|
|
284
|
+
(server ? ` data-server="${esc(mcpServerLabel(server.server))}"` : "") +
|
|
285
|
+
(project ? ` data-project="${esc(project)}"` : ""));
|
|
286
|
+
}
|
|
268
287
|
function modeBadge(mode) {
|
|
269
288
|
const letter = mode === "observe" ? "O" : mode === "hold" ? "H" : mode === "offline" ? "X" : "";
|
|
270
289
|
if (!letter)
|
|
@@ -309,17 +328,7 @@ function renderFeed(events, nowMs, live) {
|
|
|
309
328
|
// omitted when there is none. data-tool is clipped to 60 chars and must
|
|
310
329
|
// match the tool filter values stamped on dashboard rows. data-server is
|
|
311
330
|
// the recognized MCP server label, omitted for non-MCP or unknown servers.
|
|
312
|
-
const
|
|
313
|
-
const server = parseMcpToolId(e.toolId);
|
|
314
|
-
const dataAttrs = `data-verdict="${esc(e.verdict.toUpperCase())}"` +
|
|
315
|
-
(never ? ` data-never="1"` : "") +
|
|
316
|
-
` data-mode="${esc(e.mode)}"` +
|
|
317
|
-
` data-plane="${esc(e.host?.trim() || "unknown")}"` +
|
|
318
|
-
` data-product="${esc(e.product ?? "other")}"` +
|
|
319
|
-
` data-tool="${esc(clip(e.toolId, 60))}"` +
|
|
320
|
-
` data-session="${esc(e.sessionId || "unknown")}"` +
|
|
321
|
-
(server ? ` data-server="${esc(mcpServerLabel(server.server))}"` : "") +
|
|
322
|
-
(project ? ` data-project="${esc(project)}"` : "");
|
|
331
|
+
const dataAttrs = filterDataAttrs(e, e.sessionId || "unknown");
|
|
323
332
|
parts.push(`<article class="ev ${t}${never ? " never" : ""}" ${dataAttrs}>
|
|
324
333
|
<div class="rail" aria-hidden="true"><span class="tick"></span></div>
|
|
325
334
|
<div class="main">
|
|
@@ -363,17 +372,7 @@ function changesPanel(changes, nowMs) {
|
|
|
363
372
|
const entries = f.entries
|
|
364
373
|
.map((e) => {
|
|
365
374
|
const t = tone(e.verdict);
|
|
366
|
-
const
|
|
367
|
-
const server = parseMcpToolId(e.toolId);
|
|
368
|
-
const dataAttrs = `data-verdict="${esc(e.verdict.toUpperCase())}"` +
|
|
369
|
-
(e.never ? ` data-never="1"` : "") +
|
|
370
|
-
` data-mode="${esc(e.mode)}"` +
|
|
371
|
-
` data-plane="${esc(e.host?.trim() || "unknown")}"` +
|
|
372
|
-
` data-product="${esc(e.product ?? "other")}"` +
|
|
373
|
-
` data-tool="${esc(clip(e.toolId, 60))}"` +
|
|
374
|
-
` data-session="${esc(s.sessionId)}"` +
|
|
375
|
-
(server ? ` data-server="${esc(mcpServerLabel(server.server))}"` : "") +
|
|
376
|
-
(project ? ` data-project="${esc(project)}"` : "");
|
|
375
|
+
const dataAttrs = filterDataAttrs(e, s.sessionId);
|
|
377
376
|
return ` <div class="chg-entry ${t}${e.never ? " never" : ""}" ${dataAttrs}>
|
|
378
377
|
<div class="top">
|
|
379
378
|
<span class="verdict">${esc(verdictWord(e.verdict))}</span>${modeBadge(e.mode)}
|
|
@@ -417,6 +416,14 @@ function identityLine(identity) {
|
|
|
417
416
|
// baseUrl is inert text here — never an anchor.
|
|
418
417
|
return `<div class="identity">${esc(clip(bits.join(" · "), 160))}</div>`;
|
|
419
418
|
}
|
|
419
|
+
function navItem(href, label, count, runningDot) {
|
|
420
|
+
const countSpan = count !== undefined ? `<span class="nav-count">${count}</span>` : "";
|
|
421
|
+
const dot = runningDot ? '<span class="status-dot" aria-hidden="true"></span>' : "";
|
|
422
|
+
return `<a class="nav-link" id="tab-${href.slice(1)}" href="${href}">
|
|
423
|
+
<span class="nav-label">${dot}${esc(label)}</span>
|
|
424
|
+
${countSpan}
|
|
425
|
+
</a>`;
|
|
426
|
+
}
|
|
420
427
|
function statChips(agg) {
|
|
421
428
|
// Chips are filter toggles: native buttons keep identical styling via .stat.
|
|
422
429
|
const chip = (label, n, group, value, cls = "") => n > 0
|
|
@@ -607,11 +614,11 @@ function gatePanel(card, nowMs) {
|
|
|
607
614
|
<div class="orrline">
|
|
608
615
|
<span class="pill">not set up</span>
|
|
609
616
|
</div>
|
|
610
|
-
<p class="line">Gateway not set up — <code>kya
|
|
617
|
+
<p class="line">Gateway not set up — <code>kya start</code> puts a governed gateway in front of any MCP server automatically: every tool call proxied, evaluated, and on this trail.</p>
|
|
611
618
|
<ol class="steps">
|
|
612
|
-
<li
|
|
613
|
-
<li><code>kya
|
|
614
|
-
<li><code>kya gate
|
|
619
|
+
<li>Add any MCP server to a host config (or <code>kya gate init</code> to pick from recipes)</li>
|
|
620
|
+
<li><code>kya start</code> — imports it, installs the gateway, and routes the host through it (originals backed up under .kya/backups)</li>
|
|
621
|
+
<li><code>kya gate doctor</code> — binary, config, listener health, loopback-only posture</li>
|
|
615
622
|
</ol>
|
|
616
623
|
</section>`;
|
|
617
624
|
}
|
|
@@ -662,6 +669,207 @@ ${rows}
|
|
|
662
669
|
<p class="mute small">${card.events} gateway event${card.events === 1 ? "" : "s"} in this window · <code>kya gate stop</code> stops everything</p>
|
|
663
670
|
</section>`;
|
|
664
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Full Gateway dashboard for the #gateway zone. Extends the compact System-tab
|
|
674
|
+
* panel with stat cards, listener details, targets table, policy cards, a dry-run
|
|
675
|
+
* playground, quick actions, and a gateway-native events table. All user-controlled
|
|
676
|
+
* values (server ids, commands, urls, tool names) are escaped.
|
|
677
|
+
*/
|
|
678
|
+
function gatePagePanel(page, nowMs) {
|
|
679
|
+
if (!page) {
|
|
680
|
+
return `<section class="zone" id="gateway" aria-labelledby="tab-gateway">
|
|
681
|
+
<p class="mute small zone-empty">Gateway details will appear here after setup.</p>
|
|
682
|
+
</section>`;
|
|
683
|
+
}
|
|
684
|
+
if (page.state === "not-set-up") {
|
|
685
|
+
return `<section class="zone gate-zone" id="gateway" aria-labelledby="tab-gateway">
|
|
686
|
+
<h2>Gateway</h2>
|
|
687
|
+
<p class="sub">Local MCP gate — every proxied tool call is evaluated and audited.</p>
|
|
688
|
+
<div class="card">
|
|
689
|
+
<span class="pill">not set up</span>
|
|
690
|
+
<p class="line">${esc(GATE_NOT_SETUP_QUICKSTART)}</p>
|
|
691
|
+
<div class="quick-actions">
|
|
692
|
+
<button type="button" class="stat" data-action="doctor" id="gate-action-doctor">Doctor</button>
|
|
693
|
+
</div>
|
|
694
|
+
<div id="gate-doctor-result"></div>
|
|
695
|
+
</div>
|
|
696
|
+
</section>`;
|
|
697
|
+
}
|
|
698
|
+
const denyCount = page.gateEvents.filter((e) => e.verdict.toUpperCase() === "DENY" || e.neverEvent || e.reasonCode === "NEVER_EVENT").length;
|
|
699
|
+
const listenerStatus = page.state === "running"
|
|
700
|
+
? `<span class="pill ok">running</span>`
|
|
701
|
+
: `<span class="pill">stopped</span>`;
|
|
702
|
+
const bindBadge = page.bindScope.loopbackOnly
|
|
703
|
+
? `<span class="pill ok" title="${esc(page.bindScope.detail)}">loopback-only</span>`
|
|
704
|
+
: `<span class="pill warn" title="${esc(page.bindScope.detail)}">not loopback-only</span>`;
|
|
705
|
+
const statCards = `<div class="stat-cards">
|
|
706
|
+
<div class="stat-card">
|
|
707
|
+
<span class="lab">Listener</span>
|
|
708
|
+
<span class="val">${listenerStatus} ${bindBadge}</span>
|
|
709
|
+
</div>
|
|
710
|
+
<div class="stat-card">
|
|
711
|
+
<span class="lab">Targets</span>
|
|
712
|
+
<span class="num">${page.servers.length}</span>
|
|
713
|
+
</div>
|
|
714
|
+
<div class="stat-card">
|
|
715
|
+
<span class="lab">Events</span>
|
|
716
|
+
<span class="num">${page.gateEvents.length}</span>
|
|
717
|
+
</div>
|
|
718
|
+
<div class="stat-card">
|
|
719
|
+
<span class="lab">Denies</span>
|
|
720
|
+
<span class="num${denyCount > 0 ? " bad" : ""}">${denyCount}</span>
|
|
721
|
+
</div>
|
|
722
|
+
</div>`;
|
|
723
|
+
const uptime = page.startedAt
|
|
724
|
+
? `<p class="line">Up since <time datetime="${esc(page.startedAt)}">${esc(relativeTime(page.startedAt, nowMs))}</time></p>`
|
|
725
|
+
: "";
|
|
726
|
+
const binaryLine = page.binaryPresent
|
|
727
|
+
? `<p class="line">Binary <code>${page.binaryVersion ? esc(clip(page.binaryVersion, 40)) : "installed"}</code> · <code>${esc(page.binaryPath)}</code></p>`
|
|
728
|
+
: `<p class="line">Binary missing — <code>kya gate setup</code></p>`;
|
|
729
|
+
const listenerUrl = page.url
|
|
730
|
+
? `<p class="line">Listener <code>${esc(clip(page.url, 80))}</code> · port ${page.port ?? page.otlpPort}</p>`
|
|
731
|
+
: `<p class="line">Listener port ${page.port ?? 3930} (stopped)</p>`;
|
|
732
|
+
const listenerCard = `<div class="card">
|
|
733
|
+
<h3>Listener</h3>
|
|
734
|
+
${listenerUrl}
|
|
735
|
+
${uptime}
|
|
736
|
+
${binaryLine}
|
|
737
|
+
<p class="line">OTLP receiver port ${page.otlpPort} · failure mode ${esc(page.failureMode)}</p>
|
|
738
|
+
</div>`;
|
|
739
|
+
const targetsTable = (() => {
|
|
740
|
+
if (page.servers.length === 0) {
|
|
741
|
+
return `<div class="card">
|
|
742
|
+
<h3>Targets</h3>
|
|
743
|
+
<p class="empty small">No servers configured — add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
|
|
744
|
+
</div>`;
|
|
745
|
+
}
|
|
746
|
+
const rows = page.servers
|
|
747
|
+
.map((s) => {
|
|
748
|
+
const transport = s.transport === "stdio"
|
|
749
|
+
? `<code title="${esc(s.cmd?.join(" ") ?? "")}">${esc(clip(s.cmd?.join(" ") ?? s.transport, 50))}</code>`
|
|
750
|
+
: `<code title="${esc(s.url ?? "")}">${esc(clip(s.url ?? s.transport, 50))}</code>`;
|
|
751
|
+
const provenance = s.importedFrom
|
|
752
|
+
.map((h) => `<span class="chip">${esc(h)}</span>`)
|
|
753
|
+
.join("");
|
|
754
|
+
const policy = `<span class="pill">${esc(s.policy.defaultTier ?? "unknown")}</span> · ${s.policy.denyPatterns.length} pattern${s.policy.denyPatterns.length === 1 ? "" : "s"}`;
|
|
755
|
+
const dot = s.worst === "none"
|
|
756
|
+
? ""
|
|
757
|
+
: `<span class="wdot ${s.worst}" title="worst verdict: ${s.worst}"></span>`;
|
|
758
|
+
const denied = s.deniedTools.length === 0
|
|
759
|
+
? '<span class="mute">—</span>'
|
|
760
|
+
: s.deniedTools.map((t) => `<span class="chip bad">${esc(t)}</span>`).join("");
|
|
761
|
+
return ` <tr data-server="${esc(s.serverFacet)}">
|
|
762
|
+
<td><span class="wdot ${s.worst === "none" ? "mute" : s.worst}"></span> <code>${esc(s.id)}</code></td>
|
|
763
|
+
<td>${transport}</td>
|
|
764
|
+
<td>${provenance}</td>
|
|
765
|
+
<td>${policy}</td>
|
|
766
|
+
<td>${dot} ${s.events}</td>
|
|
767
|
+
<td>${denied}</td>
|
|
768
|
+
</tr>`;
|
|
769
|
+
})
|
|
770
|
+
.join("\n");
|
|
771
|
+
return `<div class="card">
|
|
772
|
+
<h3>Targets</h3>
|
|
773
|
+
<table class="tbl targets-table">
|
|
774
|
+
<thead><tr><th>Server</th><th>Transport</th><th>Provenance</th><th>Policy</th><th>Events</th><th>Denied tools</th></tr></thead>
|
|
775
|
+
<tbody>
|
|
776
|
+
${rows}
|
|
777
|
+
</tbody>
|
|
778
|
+
</table>
|
|
779
|
+
</div>`;
|
|
780
|
+
})();
|
|
781
|
+
const policiesCard = `<div class="card">
|
|
782
|
+
<h3>Policies</h3>
|
|
783
|
+
<p class="sub">Network rule</p>
|
|
784
|
+
<pre><code>${esc(LOOPBACK_RULE)}</code></pre>
|
|
785
|
+
<p class="sub">Per-server deny patterns</p>
|
|
786
|
+
${page.servers.length === 0 ? '<p class="mute small">No servers configured.</p>' : page.servers
|
|
787
|
+
.map((s) => `<details><summary><code>${esc(s.id)}</code> · ${s.policy.denyPatterns.length} pattern${s.policy.denyPatterns.length === 1 ? "" : "s"}</summary><pre><code>${esc(s.policy.denyPatterns.join("\n"))}</code></pre></details>`)
|
|
788
|
+
.join("\n")}
|
|
789
|
+
<p class="mute small">READ and WRITE tools are allowed; WRITE calls are observed/audited. Only destructive/admin patterns are denied.</p>
|
|
790
|
+
</div>`;
|
|
791
|
+
const serverOptions = page.servers
|
|
792
|
+
.map((s) => `<option value="${esc(s.id)}">${esc(s.id)}</option>`)
|
|
793
|
+
.join("");
|
|
794
|
+
const playgroundCard = `<div class="card">
|
|
795
|
+
<h3>Playground</h3>
|
|
796
|
+
<p class="mute small">Dry-run — no tool is executed.</p>
|
|
797
|
+
<form class="playground-form" id="gate-play-form">
|
|
798
|
+
<label class="sr-only" for="gate-play-server">Server</label>
|
|
799
|
+
<select id="gate-play-server" name="server" required>
|
|
800
|
+
<option value="" disabled selected>Server</option>
|
|
801
|
+
${serverOptions}
|
|
802
|
+
</select>
|
|
803
|
+
<label class="sr-only" for="gate-play-tool">Tool</label>
|
|
804
|
+
<input id="gate-play-tool" name="tool" type="text" placeholder="tool_name" required />
|
|
805
|
+
<button type="submit" class="stat">Evaluate</button>
|
|
806
|
+
</form>
|
|
807
|
+
<div id="gate-play-result"></div>
|
|
808
|
+
</div>`;
|
|
809
|
+
const quickActions = (() => {
|
|
810
|
+
if (page.state === "configured-stopped") {
|
|
811
|
+
return `<button type="button" class="stat ok" data-action="start" id="gate-action-start">Start</button>
|
|
812
|
+
<button type="button" class="stat" data-action="doctor" id="gate-action-doctor">Doctor</button>`;
|
|
813
|
+
}
|
|
814
|
+
return `<button type="button" class="stat bad" data-action="stop" id="gate-action-stop">Stop</button>
|
|
815
|
+
<button type="button" class="stat warn" data-action="restart" id="gate-action-restart">Restart</button>
|
|
816
|
+
<button type="button" class="stat" data-action="doctor" id="gate-action-doctor">Doctor</button>`;
|
|
817
|
+
})();
|
|
818
|
+
const eventsTable = (() => {
|
|
819
|
+
if (page.gateEvents.length === 0) {
|
|
820
|
+
return `<div class="card">
|
|
821
|
+
<h3>Gateway events</h3>
|
|
822
|
+
<p class="empty small">No gateway events in this window.</p>
|
|
823
|
+
</div>`;
|
|
824
|
+
}
|
|
825
|
+
const rows = [...page.gateEvents]
|
|
826
|
+
.sort((a, b) => b.ts.localeCompare(a.ts))
|
|
827
|
+
.slice(0, 25)
|
|
828
|
+
.map((e) => {
|
|
829
|
+
const server = toolServerPrefix(e.toolId) ?? "";
|
|
830
|
+
const tool = toolNameFromToolId(e.toolId) ?? e.toolId;
|
|
831
|
+
const t = tone(e.verdict);
|
|
832
|
+
const never = e.neverEvent || e.reasonCode === "NEVER_EVENT";
|
|
833
|
+
return ` <tr>
|
|
834
|
+
<td><time datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(clock(e.ts))} <span class="rel">${esc(relativeTime(e.ts, nowMs))}</span></time></td>
|
|
835
|
+
<td><a href="?f=server:${esc(mcpServerLabel(server))}#activity">${esc(server)}</a></td>
|
|
836
|
+
<td><code>${esc(tool)}</code></td>
|
|
837
|
+
<td><span class="pill ${t}${never ? " orr-red" : ""}">${esc(verdictWord(e.verdict))}${never ? " · never" : ""}</span></td>
|
|
838
|
+
</tr>`;
|
|
839
|
+
})
|
|
840
|
+
.join("\n");
|
|
841
|
+
return `<div class="card">
|
|
842
|
+
<h3>Gateway events <span class="mute">latest 25</span></h3>
|
|
843
|
+
<table class="tbl">
|
|
844
|
+
<thead><tr><th>Time</th><th>Server</th><th>Tool</th><th>Verdict</th></tr></thead>
|
|
845
|
+
<tbody>
|
|
846
|
+
${rows}
|
|
847
|
+
</tbody>
|
|
848
|
+
</table>
|
|
849
|
+
</div>`;
|
|
850
|
+
})();
|
|
851
|
+
return `<section class="zone gate-zone" id="gateway" aria-labelledby="tab-gateway">
|
|
852
|
+
<h2>Gateway</h2>
|
|
853
|
+
<p class="sub">Local MCP gate — every proxied tool call is evaluated and audited.</p>
|
|
854
|
+
${statCards}
|
|
855
|
+
<div class="card-grid">
|
|
856
|
+
${listenerCard}
|
|
857
|
+
${policiesCard}
|
|
858
|
+
</div>
|
|
859
|
+
${targetsTable}
|
|
860
|
+
<div class="card-grid">
|
|
861
|
+
${playgroundCard}
|
|
862
|
+
<div class="card">
|
|
863
|
+
<h3>Quick actions</h3>
|
|
864
|
+
<div class="quick-actions">
|
|
865
|
+
${quickActions}
|
|
866
|
+
</div>
|
|
867
|
+
<div id="gate-doctor-result"></div>
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
${eventsTable}
|
|
871
|
+
</section>`;
|
|
872
|
+
}
|
|
665
873
|
function sandboxesPanel(card, nowMs) {
|
|
666
874
|
if (!card)
|
|
667
875
|
return "";
|
|
@@ -936,7 +1144,7 @@ function gateHeroCard(card) {
|
|
|
936
1144
|
? `${card.url ? `<code>${esc(clip(card.url, 48))}</code> · ` : ""}${card.servers.length} server${card.servers.length === 1 ? "" : "s"} · ${card.events} event${card.events === 1 ? "" : "s"}`
|
|
937
1145
|
: card.state === "configured-stopped"
|
|
938
1146
|
? `${card.servers.length} server${card.servers.length === 1 ? "" : "s"} configured — <code>kya gate run</code>`
|
|
939
|
-
:
|
|
1147
|
+
: `add an MCP server to a host config — <code>kya start</code> governs it`;
|
|
940
1148
|
const pill = card.state === "running"
|
|
941
1149
|
? `<span class="pill ok">running</span>`
|
|
942
1150
|
: `<span class="pill">${card.state === "configured-stopped" ? "stopped" : "not set up"}</span>`;
|
|
@@ -990,7 +1198,7 @@ export function renderReceiptHtml(model) {
|
|
|
990
1198
|
</footer>`;
|
|
991
1199
|
// liveToken is minted base64url by the live server — safe in a JS string
|
|
992
1200
|
// literal; static renders omit it. No post-hoc html.replace (spoofable).
|
|
993
|
-
const eventsUrl = model.liveToken ? `/events?t=${model.liveToken}` : "/events";
|
|
1201
|
+
const eventsUrl = model.liveToken ? `/events?t=${encodeURIComponent(model.liveToken)}` : "/events";
|
|
994
1202
|
const liveScript = model.live
|
|
995
1203
|
? `<script>
|
|
996
1204
|
(function(){
|
|
@@ -1131,6 +1339,148 @@ export function renderReceiptHtml(model) {
|
|
|
1131
1339
|
})();
|
|
1132
1340
|
</script>`;
|
|
1133
1341
|
const livePill = model.live ? `<span class="live" title="Watching trail.jsonl">Live</span>` : "";
|
|
1342
|
+
const themeScript = `<script>
|
|
1343
|
+
(function(){
|
|
1344
|
+
var root = document.documentElement;
|
|
1345
|
+
var btn = document.getElementById('theme-toggle');
|
|
1346
|
+
function apply(theme){ root.setAttribute('data-theme', theme); }
|
|
1347
|
+
var explicit = root.getAttribute('data-theme');
|
|
1348
|
+
var stored = null;
|
|
1349
|
+
try { stored = localStorage.getItem('kya-theme'); } catch(e){}
|
|
1350
|
+
if (explicit) {
|
|
1351
|
+
// honor an explicit theme set on the static artifact (e.g. screenshots)
|
|
1352
|
+
} else if (stored) apply(stored);
|
|
1353
|
+
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) apply('light');
|
|
1354
|
+
else apply('dark');
|
|
1355
|
+
if (btn) btn.addEventListener('click', function(){
|
|
1356
|
+
apply(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
|
|
1357
|
+
try { localStorage.setItem('kya-theme', root.getAttribute('data-theme')); } catch(e){}
|
|
1358
|
+
});
|
|
1359
|
+
})();
|
|
1360
|
+
</script>`;
|
|
1361
|
+
// Gateway interactive controls: playground evaluator, quick actions, and the
|
|
1362
|
+
// sidebar stop-report link. Only wired when the page is served by the live
|
|
1363
|
+
// loopback server (token present). Token is read from the query string so it
|
|
1364
|
+
// matches the same origin as the SSE endpoint; all user-controlled output is
|
|
1365
|
+
// escaped before DOM insertion.
|
|
1366
|
+
const gateScript = model.liveToken
|
|
1367
|
+
? `<script>
|
|
1368
|
+
(function(){
|
|
1369
|
+
var T = '';
|
|
1370
|
+
var s = location.search;
|
|
1371
|
+
if (s.indexOf('?') === 0) {
|
|
1372
|
+
var params = s.slice(1).split('&');
|
|
1373
|
+
for (var i = 0; i < params.length; i++) {
|
|
1374
|
+
if (params[i].indexOf('t=') === 0) {
|
|
1375
|
+
try { T = decodeURIComponent(params[i].slice(2)); } catch(e){ T = params[i].slice(2); }
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (!T) return;
|
|
1381
|
+
function esc(str){
|
|
1382
|
+
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1383
|
+
}
|
|
1384
|
+
function setResult(el, html, isError){
|
|
1385
|
+
if (!el) return;
|
|
1386
|
+
el.innerHTML = '<div class="gate-result' + (isError ? ' error' : '') + '">' + html + '</div>';
|
|
1387
|
+
}
|
|
1388
|
+
function post(path, body){
|
|
1389
|
+
return fetch(path + '?t=' + encodeURIComponent(T), {
|
|
1390
|
+
method: 'POST',
|
|
1391
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1392
|
+
body: JSON.stringify(body)
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
function parseJson(res){ return res.json(); }
|
|
1396
|
+
|
|
1397
|
+
var form = document.getElementById('gate-play-form');
|
|
1398
|
+
if (form) {
|
|
1399
|
+
form.addEventListener('submit', function(e){
|
|
1400
|
+
e.preventDefault();
|
|
1401
|
+
var server = form.elements.server.value.trim();
|
|
1402
|
+
var tool = form.elements.tool.value.trim();
|
|
1403
|
+
var resultEl = document.getElementById('gate-play-result');
|
|
1404
|
+
var btn = form.querySelector('button[type="submit"]');
|
|
1405
|
+
if (btn) btn.disabled = true;
|
|
1406
|
+
setResult(resultEl, 'Evaluating...', false);
|
|
1407
|
+
post('/gate/playground', { server: server, tool: tool })
|
|
1408
|
+
.then(parseJson)
|
|
1409
|
+
.then(function(data){
|
|
1410
|
+
if (data && data.ok) {
|
|
1411
|
+
var pill = '<span class="pill ' + (data.verdict === 'allow' ? 'ok' : 'bad') + '">' + esc(data.verdict.toUpperCase()) + '</span>';
|
|
1412
|
+
setResult(resultEl, pill + '<p>' + esc(data.reason) + '</p>', false);
|
|
1413
|
+
} else {
|
|
1414
|
+
setResult(resultEl, '<p>Error: ' + esc(data && data.error ? data.error : 'unknown') + '</p>', true);
|
|
1415
|
+
}
|
|
1416
|
+
})
|
|
1417
|
+
.catch(function(err){
|
|
1418
|
+
setResult(resultEl, '<p>Error: ' + esc(err.message) + '</p>', true);
|
|
1419
|
+
})
|
|
1420
|
+
.finally(function(){
|
|
1421
|
+
if (btn) btn.disabled = false;
|
|
1422
|
+
});
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
var gateway = document.getElementById('gateway');
|
|
1427
|
+
if (gateway) {
|
|
1428
|
+
gateway.querySelectorAll('[data-action]').forEach(function(btn){
|
|
1429
|
+
btn.addEventListener('click', function(){
|
|
1430
|
+
var action = btn.getAttribute('data-action');
|
|
1431
|
+
var resultEl = document.getElementById('gate-doctor-result');
|
|
1432
|
+
btn.disabled = true;
|
|
1433
|
+
if (action === 'doctor') setResult(resultEl, 'Running doctor...', false);
|
|
1434
|
+
post('/gate/action', { action: action })
|
|
1435
|
+
.then(parseJson)
|
|
1436
|
+
.then(function(data){
|
|
1437
|
+
if (data && data.ok) {
|
|
1438
|
+
if (action === 'doctor') {
|
|
1439
|
+
setResult(resultEl, '<pre>' + esc(JSON.stringify(data.report, null, 2)) + '</pre>', false);
|
|
1440
|
+
btn.disabled = false;
|
|
1441
|
+
} else {
|
|
1442
|
+
setTimeout(function(){ location.reload(); }, 800);
|
|
1443
|
+
}
|
|
1444
|
+
} else {
|
|
1445
|
+
setResult(resultEl, '<p>Error: ' + esc(data && data.error ? data.error : 'unknown') + '</p>', true);
|
|
1446
|
+
btn.disabled = false;
|
|
1447
|
+
}
|
|
1448
|
+
})
|
|
1449
|
+
.catch(function(err){
|
|
1450
|
+
setResult(resultEl, '<p>Error: ' + esc(err.message) + '</p>', true);
|
|
1451
|
+
btn.disabled = false;
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
var stopLink = document.getElementById('stop-report');
|
|
1458
|
+
if (stopLink) {
|
|
1459
|
+
stopLink.addEventListener('click', function(e){
|
|
1460
|
+
e.preventDefault();
|
|
1461
|
+
stopLink.textContent = 'Stopping...';
|
|
1462
|
+
post('/stop', {})
|
|
1463
|
+
.then(parseJson)
|
|
1464
|
+
.then(function(data){
|
|
1465
|
+
if (data && data.ok) {
|
|
1466
|
+
stopLink.textContent = 'Report stopped';
|
|
1467
|
+
stopLink.setAttribute('aria-disabled', 'true');
|
|
1468
|
+
stopLink.style.pointerEvents = 'none';
|
|
1469
|
+
stopLink.style.opacity = '0.6';
|
|
1470
|
+
} else {
|
|
1471
|
+
stopLink.textContent = 'Stop failed';
|
|
1472
|
+
window.alert('Failed to stop report: ' + esc(data && data.error ? data.error : 'unknown'));
|
|
1473
|
+
}
|
|
1474
|
+
})
|
|
1475
|
+
.catch(function(err){
|
|
1476
|
+
stopLink.textContent = 'Stop failed';
|
|
1477
|
+
window.alert('Failed to stop report: ' + esc(err.message));
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
})();
|
|
1482
|
+
</script>`
|
|
1483
|
+
: "";
|
|
1134
1484
|
// Zones never render as bare whitespace: with no panels they get a hint.
|
|
1135
1485
|
const zoneBody = (parts, empty) => parts.some((p) => p.length > 0)
|
|
1136
1486
|
? parts.filter(Boolean).join("\n ")
|
|
@@ -1153,23 +1503,35 @@ export function renderReceiptHtml(model) {
|
|
|
1153
1503
|
<style>${receiptCss()}</style>
|
|
1154
1504
|
</head>
|
|
1155
1505
|
<body>
|
|
1506
|
+
<aside class="sidebar">
|
|
1507
|
+
<header class="sidebar-header">
|
|
1508
|
+
<div class="brand">
|
|
1509
|
+
<span class="mark" aria-hidden="true">k</span>
|
|
1510
|
+
<span class="wordmark">kya</span>
|
|
1511
|
+
</div>
|
|
1512
|
+
</header>
|
|
1513
|
+
<nav class="sidebar-nav" aria-label="Report sections">
|
|
1514
|
+
${navItem("#overview", "Overview", events.length)}
|
|
1515
|
+
${navItem("#activity", "Activity", events.length)}
|
|
1516
|
+
${navItem("#changes", "Changes", changes.fileCount)}
|
|
1517
|
+
${navItem("#certify", "Certify", model.certify?.gap ?? 0)}
|
|
1518
|
+
${navItem("#gateway", "Gateway", model.gate?.servers.length ?? 0, model.gate?.state === "running")}
|
|
1519
|
+
${navItem("#system", "System", undefined)}
|
|
1520
|
+
</nav>
|
|
1521
|
+
<footer class="sidebar-footer">
|
|
1522
|
+
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">Theme</button>
|
|
1523
|
+
<!-- Stop-report action is wired by the live-server POST handler. -->
|
|
1524
|
+
<a href="#" id="stop-report">Stop report</a>
|
|
1525
|
+
</footer>
|
|
1526
|
+
</aside>
|
|
1156
1527
|
<main>
|
|
1157
|
-
<
|
|
1158
|
-
<
|
|
1159
|
-
<
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
</header>
|
|
1165
|
-
<nav class="tabs" aria-label="Report sections">
|
|
1166
|
-
<a class="tab" id="tab-overview" href="#overview">Overview</a>
|
|
1167
|
-
<a class="tab" id="tab-changes" href="#changes">Changes</a>
|
|
1168
|
-
<a class="tab" id="tab-certify" href="#certify">Certify</a>
|
|
1169
|
-
<a class="tab" id="tab-activity" href="#activity">Activity</a>
|
|
1170
|
-
<a class="tab" id="tab-system" href="#system">System</a>
|
|
1171
|
-
</nav>
|
|
1172
|
-
</div>
|
|
1528
|
+
<header class="page-header">
|
|
1529
|
+
<div class="title-row">
|
|
1530
|
+
<h1>${esc(model.title)}</h1>
|
|
1531
|
+
<div class="range">${esc(model.rangeLabel)}${livePill}</div>
|
|
1532
|
+
</div>
|
|
1533
|
+
${identityLine(model.identity)}
|
|
1534
|
+
</header>
|
|
1173
1535
|
${blockedBanner}
|
|
1174
1536
|
<div class="hero">
|
|
1175
1537
|
${heroCertifyPanel(model.certify)}
|
|
@@ -1181,14 +1543,6 @@ export function renderReceiptHtml(model) {
|
|
|
1181
1543
|
<section class="zone" id="overview" aria-labelledby="tab-overview">
|
|
1182
1544
|
${overviewBody}
|
|
1183
1545
|
</section>
|
|
1184
|
-
<section class="zone" id="changes" aria-labelledby="tab-changes">
|
|
1185
|
-
<section class="feed chg" id="changes-list" aria-label="Changes by session and file">
|
|
1186
|
-
${changesPanel(changes, nowMs)}
|
|
1187
|
-
</section>
|
|
1188
|
-
</section>
|
|
1189
|
-
<section class="zone" id="certify" aria-labelledby="tab-certify">
|
|
1190
|
-
${certifyBody}
|
|
1191
|
-
</section>
|
|
1192
1546
|
<section class="zone" id="activity" aria-labelledby="tab-activity">
|
|
1193
1547
|
<div class="feed-head">
|
|
1194
1548
|
<div class="stats" aria-label="Counts">
|
|
@@ -1207,11 +1561,22 @@ ${changesPanel(changes, nowMs)}
|
|
|
1207
1561
|
${renderFeed(events, nowMs, model.live)}
|
|
1208
1562
|
</section>
|
|
1209
1563
|
</section>
|
|
1564
|
+
<section class="zone" id="changes" aria-labelledby="tab-changes">
|
|
1565
|
+
<section class="feed chg" id="changes-list" aria-label="Changes by session and file">
|
|
1566
|
+
${changesPanel(changes, nowMs)}
|
|
1567
|
+
</section>
|
|
1568
|
+
</section>
|
|
1569
|
+
<section class="zone" id="certify" aria-labelledby="tab-certify">
|
|
1570
|
+
${certifyBody}
|
|
1571
|
+
</section>
|
|
1572
|
+
${gatePagePanel(model.gate, nowMs)}
|
|
1210
1573
|
<section class="zone" id="system" aria-labelledby="tab-system">
|
|
1211
1574
|
${systemBody}
|
|
1212
1575
|
</section>
|
|
1213
1576
|
</main>
|
|
1214
1577
|
${filterScript}
|
|
1578
|
+
${themeScript}
|
|
1579
|
+
${gateScript}
|
|
1215
1580
|
${liveScript}
|
|
1216
1581
|
</body>
|
|
1217
1582
|
</html>`;
|
|
@@ -1346,7 +1711,7 @@ export function renderReceiptMarkdown(model) {
|
|
|
1346
1711
|
const g = model.gate;
|
|
1347
1712
|
lines.push("## Gateway");
|
|
1348
1713
|
if (g.state === "not-set-up") {
|
|
1349
|
-
lines.push(
|
|
1714
|
+
lines.push(`not set up — ${GATE_NOT_SETUP_QUICKSTART}`);
|
|
1350
1715
|
}
|
|
1351
1716
|
else {
|
|
1352
1717
|
const binary = g.binaryPresent
|