@thotischner/observability-mcp 3.6.1 → 3.8.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/dist/auth/policy/loader.js +1 -1
- package/dist/auth/rbac.d.ts +1 -1
- package/dist/auth/rbac.js +3 -1
- package/dist/auth/rbac.test.js +5 -3
- package/dist/conformance/inspect-e2e.test.d.ts +1 -0
- package/dist/conformance/inspect-e2e.test.js +104 -0
- package/dist/connectors/loader.js +30 -0
- package/dist/connectors/loader.test.js +11 -0
- package/dist/enrich/rdap.d.ts +40 -0
- package/dist/enrich/rdap.js +122 -0
- package/dist/enrich/rdap.test.d.ts +1 -0
- package/dist/enrich/rdap.test.js +78 -0
- package/dist/enterprise-gate.d.ts +28 -0
- package/dist/enterprise-gate.js +51 -0
- package/dist/enterprise-gate.test.js +21 -1
- package/dist/index.js +296 -8
- package/dist/inspect/enforcer.d.ts +19 -0
- package/dist/inspect/enforcer.js +69 -0
- package/dist/inspect/enforcer.test.d.ts +1 -0
- package/dist/inspect/enforcer.test.js +76 -0
- package/dist/inspect/graph.d.ts +33 -0
- package/dist/inspect/graph.js +0 -0
- package/dist/inspect/graph.test.d.ts +1 -0
- package/dist/inspect/graph.test.js +74 -0
- package/dist/inspect/index.d.ts +8 -0
- package/dist/inspect/index.js +13 -0
- package/dist/inspect/mode.d.ts +20 -0
- package/dist/inspect/mode.js +57 -0
- package/dist/inspect/mode.test.d.ts +1 -0
- package/dist/inspect/mode.test.js +53 -0
- package/dist/inspect/profile-store.d.ts +42 -0
- package/dist/inspect/profile-store.js +139 -0
- package/dist/inspect/profile-store.test.d.ts +1 -0
- package/dist/inspect/profile-store.test.js +82 -0
- package/dist/inspect/profile.d.ts +51 -0
- package/dist/inspect/profile.js +111 -0
- package/dist/inspect/profile.test.d.ts +1 -0
- package/dist/inspect/profile.test.js +96 -0
- package/dist/inspect/recorder.d.ts +42 -0
- package/dist/inspect/recorder.js +72 -0
- package/dist/inspect/recorder.test.d.ts +1 -0
- package/dist/inspect/recorder.test.js +112 -0
- package/dist/inspect/signature.d.ts +32 -0
- package/dist/inspect/signature.js +200 -0
- package/dist/inspect/signature.test.d.ts +1 -0
- package/dist/inspect/signature.test.js +136 -0
- package/dist/inspect/store.d.ts +62 -0
- package/dist/inspect/store.js +76 -0
- package/dist/inspect/store.test.d.ts +1 -0
- package/dist/inspect/store.test.js +78 -0
- package/dist/metrics/self.d.ts +3 -0
- package/dist/metrics/self.js +19 -0
- package/dist/net/egress-policy.js +1 -0
- package/dist/tenancy/context.d.ts +7 -0
- package/dist/tenancy/context.js +15 -0
- package/dist/tenancy/context.test.js +18 -1
- package/dist/tools/enrich-ips.d.ts +6 -2
- package/dist/tools/enrich-ips.js +32 -11
- package/dist/tools/enrich-ips.test.js +55 -11
- package/dist/ui/index.html +742 -0
- package/package.json +3 -2
package/dist/ui/index.html
CHANGED
|
@@ -1392,11 +1392,66 @@
|
|
|
1392
1392
|
filter: drop-shadow(0 0 4px var(--accent-soft));
|
|
1393
1393
|
}
|
|
1394
1394
|
|
|
1395
|
+
/* ===== Inspect — flow graph ===== */
|
|
1396
|
+
#inspect-graph-svg:focus { outline: none; }
|
|
1397
|
+
#inspect-graph-svg g[data-id]:focus { outline: none; }
|
|
1398
|
+
#inspect-graph-svg g[data-id]:focus-visible .inode {
|
|
1399
|
+
stroke: var(--accent); stroke-width: 3;
|
|
1400
|
+
filter: drop-shadow(0 0 5px var(--accent-soft));
|
|
1401
|
+
}
|
|
1402
|
+
#inspect-graph-svg .iedge { fill: none; transition: opacity var(--t-fast); }
|
|
1403
|
+
/* Animated traffic dash — a subtle "flow" along live (allowed) edges.
|
|
1404
|
+
Disabled under reduced-motion + when the graph is paused. */
|
|
1405
|
+
#inspect-graph-svg .iedge.flow {
|
|
1406
|
+
stroke-dasharray: 2 6;
|
|
1407
|
+
animation: inspect-flow 1s linear infinite;
|
|
1408
|
+
}
|
|
1409
|
+
#inspect-graph-svg.paused .iedge.flow { animation: none; }
|
|
1410
|
+
@keyframes inspect-flow { to { stroke-dashoffset: -8; } }
|
|
1411
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1412
|
+
#inspect-graph-svg .iedge.flow { animation: none; }
|
|
1413
|
+
}
|
|
1414
|
+
#inspect-graph-svg .inode { cursor: pointer; transition: opacity var(--t-fast); }
|
|
1415
|
+
#inspect-graph-svg .ilabel { font-size: 11px; fill: var(--text-muted); pointer-events: none; user-select: none; }
|
|
1416
|
+
#inspect-graph-svg .icol-label { font-size: 10px; fill: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; pointer-events: none; }
|
|
1417
|
+
#inspect-graph-svg .dim { opacity: 0.18; }
|
|
1418
|
+
.inspect-kv { display:grid; grid-template-columns: auto 1fr; gap: 4px 10px; margin: 8px 0; }
|
|
1419
|
+
.inspect-kv dt { color: var(--text-dim); }
|
|
1420
|
+
.inspect-kv dd { margin: 0; color: var(--text); font-family: var(--font-mono, monospace); }
|
|
1421
|
+
|
|
1422
|
+
/* ===== Governance — clarifying subtitles + overview flow ===== */
|
|
1423
|
+
.gov-sub { font-size: var(--fs-sm); color: var(--text-muted); margin-top: 4px; max-width: 70ch; line-height: 1.5; }
|
|
1424
|
+
.gov-sub em { color: var(--text); font-style: normal; font-weight: 600; }
|
|
1425
|
+
.gov-overview { background: var(--surface); }
|
|
1426
|
+
.gov-flow { display: flex; flex-wrap: wrap; align-items: stretch; gap: 6px; }
|
|
1427
|
+
.gov-step { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); min-width: 120px; }
|
|
1428
|
+
.gov-step b { font-size: var(--fs-md); color: var(--text); }
|
|
1429
|
+
.gov-step small { font-size: var(--fs-xs); color: var(--text-muted); }
|
|
1430
|
+
.gov-arrow { align-self: center; color: var(--text-dim); font-size: var(--fs-lg); }
|
|
1431
|
+
|
|
1432
|
+
/* ===== Inspect — granular rule editor chips ===== */
|
|
1433
|
+
.rule-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1434
|
+
.rule-chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border: 1px solid var(--border-strong); border-radius: var(--radius-pill); font-size: var(--fs-sm); cursor: pointer; background: var(--surface-2); }
|
|
1435
|
+
.rule-chip input { margin: 0; }
|
|
1436
|
+
|
|
1395
1437
|
/* ===== Playground (Q13) — restrained, monochrome ===== */
|
|
1396
1438
|
.pg-seg { display:inline-flex; gap:0; border:1px solid var(--border); border-radius:6px; overflow:hidden; }
|
|
1397
1439
|
.pg-seg button { border:none; background:transparent; color:var(--text-2,var(--text)); font:inherit; font-size:12px; padding:3px 11px; cursor:pointer; }
|
|
1398
1440
|
.pg-seg button + button { border-left:1px solid var(--border); }
|
|
1399
1441
|
.pg-seg button.active { background:var(--surface-3); color:var(--text); font-weight:600; }
|
|
1442
|
+
.pg-seg button.locked { color:var(--text-dim); }
|
|
1443
|
+
/* Entitlement lock optic — consistent across every enterprise feature.
|
|
1444
|
+
The nav badge sits after the label; the page banner explains the lock. */
|
|
1445
|
+
.nav-btn .ent-lock { margin-left:auto; font-size:11px; opacity:.65; flex:0 0 auto; }
|
|
1446
|
+
.nav-btn.ent-locked .nav-label { opacity:.7; }
|
|
1447
|
+
.ent-banner { display:flex; gap:10px; align-items:flex-start; margin:0 0 16px; padding:11px 14px;
|
|
1448
|
+
border:1px solid var(--border); border-left:3px solid var(--accent,#6b8afd);
|
|
1449
|
+
background:var(--surface-2,var(--surface)); border-radius:6px; font-size:var(--fs-sm,13px); }
|
|
1450
|
+
.ent-banner .ent-banner-ico { font-size:15px; line-height:1.3; flex:0 0 auto; }
|
|
1451
|
+
.ent-banner b { color:var(--text); }
|
|
1452
|
+
.ent-banner .ent-banner-body { color:var(--text-2,var(--text)); }
|
|
1453
|
+
.ent-banner code { font-family:var(--mono); font-size:12px; background:var(--bg); padding:1px 5px; border-radius:3px; }
|
|
1454
|
+
.pol-subtab .ent-lock { margin-left:5px; font-size:10px; opacity:.65; }
|
|
1400
1455
|
/* JSON: two tones only — keys at full strength, scalars muted. */
|
|
1401
1456
|
.pg-json { white-space:pre-wrap; font-family:var(--mono); font-size:12px; background:var(--bg); padding:12px; border:1px solid var(--border); border-radius:4px; max-height:540px; overflow:auto; margin:0; color:var(--text-3,#8a93a5); }
|
|
1402
1457
|
.pg-json .k { color:var(--text); }
|
|
@@ -1467,6 +1522,7 @@
|
|
|
1467
1522
|
<button class="nav-btn" data-page="access" title="Access Control" onclick="showPage('access')"><span class="nav-ico">⛨</span><span class="nav-label">Access Control</span></button>
|
|
1468
1523
|
<button class="nav-btn" data-page="policies" title="Policies" onclick="showPage('policies')" data-rbac="users:delete"><span class="nav-ico">≣</span><span class="nav-label">Policies</span></button>
|
|
1469
1524
|
<button class="nav-btn" data-page="audit" title="Audit Log" onclick="showPage('audit')"><span class="nav-ico">❒</span><span class="nav-label">Audit Log</span></button>
|
|
1525
|
+
<button class="nav-btn" data-page="inspect" title="Inspect" onclick="showPage('inspect')"><span class="nav-ico">◉</span><span class="nav-label">Inspect</span></button>
|
|
1470
1526
|
</div>
|
|
1471
1527
|
</div>
|
|
1472
1528
|
<div class="rail-grp" data-grp="system">
|
|
@@ -1521,6 +1577,19 @@
|
|
|
1521
1577
|
ephemeral session secret case). -->
|
|
1522
1578
|
<div id="omcp-warning-bar" class="omcp-warning-bar" style="display:none" role="alert"></div>
|
|
1523
1579
|
|
|
1580
|
+
<!-- Inspect: granular rule editor — toggle which observed values/buckets a rule allows -->
|
|
1581
|
+
<div class="modal-overlay" id="inspect-rule-modal">
|
|
1582
|
+
<div class="modal" style="width:min(620px,92vw)">
|
|
1583
|
+
<div class="modal-header"><h3 id="inspect-rule-modal-title">Edit rule</h3><button class="btn-icon" onclick="closeModal('inspect-rule-modal')">×</button></div>
|
|
1584
|
+
<div class="modal-body" id="inspect-rule-modal-body"></div>
|
|
1585
|
+
<div class="modal-footer">
|
|
1586
|
+
<button class="btn" onclick="closeModal('inspect-rule-modal')">Cancel</button>
|
|
1587
|
+
<button class="btn" onclick="inspectSaveRule(false)">Save</button>
|
|
1588
|
+
<button class="btn btn-primary" onclick="inspectSaveRule(true)">Save & accept</button>
|
|
1589
|
+
</div>
|
|
1590
|
+
</div>
|
|
1591
|
+
</div>
|
|
1592
|
+
|
|
1524
1593
|
<!-- Login modal — shown when /api/* returns 401 OMCP_AUTH_REQUIRED -->
|
|
1525
1594
|
<div class="modal-overlay" id="login-modal">
|
|
1526
1595
|
<div class="modal" style="width:380px;">
|
|
@@ -1785,6 +1854,119 @@
|
|
|
1785
1854
|
</div>
|
|
1786
1855
|
</div>
|
|
1787
1856
|
|
|
1857
|
+
<!-- ===== Inspect ===== -->
|
|
1858
|
+
<div class="page" id="page-inspect">
|
|
1859
|
+
<div class="page-head">
|
|
1860
|
+
<div class="ph-left">
|
|
1861
|
+
<div class="breadcrumb">Console / Governance / <b>Inspect</b></div>
|
|
1862
|
+
<h1>Inspect — agent activity</h1>
|
|
1863
|
+
<div class="gov-sub">Whether a call is <em>normal</em> vs. the learned baseline for that identity — behavioral guardrails. Distinct from <b>Policies</b> (allowed at all).</div>
|
|
1864
|
+
</div>
|
|
1865
|
+
<div class="ph-actions">
|
|
1866
|
+
<span id="inspect-mode-chip" class="stchip" title="Current inspection mode">…</span>
|
|
1867
|
+
<label class="muted" style="display:flex; gap:6px; align-items:center; font-size: var(--fs-sm);">
|
|
1868
|
+
Window
|
|
1869
|
+
<select id="inspect-window" onchange="loadInspectFlows(true)" style="background:var(--surface); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:4px 8px;">
|
|
1870
|
+
<option value="5m">5m</option>
|
|
1871
|
+
<option value="1h" selected>1h</option>
|
|
1872
|
+
<option value="24h">24h</option>
|
|
1873
|
+
</select>
|
|
1874
|
+
</label>
|
|
1875
|
+
<button id="inspect-live-btn" class="btn btn-sm" onclick="toggleInspectLive()" title="Pause / resume live updates">⏸ Live</button>
|
|
1876
|
+
<button class="btn btn-sm" onclick="loadInspectFlows(true)">Refresh</button>
|
|
1877
|
+
</div>
|
|
1878
|
+
</div>
|
|
1879
|
+
<div class="tabs">
|
|
1880
|
+
<button class="tab-btn active" onclick="showInspectTab('flows')">Flows</button>
|
|
1881
|
+
<button class="tab-btn" onclick="showInspectTab('profile')">Profile</button>
|
|
1882
|
+
<button class="tab-btn" onclick="showInspectTab('deviations')">Deviations</button>
|
|
1883
|
+
</div>
|
|
1884
|
+
<div id="inspect-tab-flows" class="tab-content active">
|
|
1885
|
+
<div class="card">
|
|
1886
|
+
<div class="card-header">
|
|
1887
|
+
<h2>Flows <span class="muted t-xs" style="font-weight:400;">identities → tools → backends · edge width = call volume · colour = allowed / deviation / blocked</span></h2>
|
|
1888
|
+
<span id="inspect-flow-stat" class="muted t-xs"></span>
|
|
1889
|
+
</div>
|
|
1890
|
+
<div style="display:grid; grid-template-columns: 1fr 340px; gap: 0; border-top: 1px solid var(--border); height: 660px;">
|
|
1891
|
+
<div id="inspect-graph-host" style="position:relative; overflow:hidden; background: var(--surface-2); border-right: 1px solid var(--border);">
|
|
1892
|
+
<svg id="inspect-graph-svg" style="position:absolute; inset:0; width:100%; height:100%; cursor: grab;" tabindex="0" role="application" aria-label="Flow graph — Tab through nodes, Enter to inspect, Esc to clear"></svg>
|
|
1893
|
+
<div class="topo-controls" role="toolbar" aria-label="Graph zoom">
|
|
1894
|
+
<button class="btn-icon" title="Zoom in (or wheel up)" aria-label="Zoom in" onclick="inspectZoom(1.2)">+</button>
|
|
1895
|
+
<button class="btn-icon" title="Zoom out (or wheel down)" aria-label="Zoom out" onclick="inspectZoom(1/1.2)">−</button>
|
|
1896
|
+
<button class="btn-icon" title="Reset view" aria-label="Reset view" onclick="inspectResetView()">⤾</button>
|
|
1897
|
+
</div>
|
|
1898
|
+
<div class="topo-hint">Tab to focus · Enter to inspect · arrows to move focus · Esc to clear</div>
|
|
1899
|
+
<div id="inspect-graph-legend" style="position:absolute; bottom:10px; left:10px; font-size: var(--fs-xs); padding:8px 10px; background: var(--surface); border:1px solid var(--border); border-radius: 6px;"></div>
|
|
1900
|
+
<div id="inspect-graph-empty" class="empty" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; text-align:center; padding:24px;">Loading flows…</div>
|
|
1901
|
+
</div>
|
|
1902
|
+
<aside id="inspect-inspector" style="background: var(--surface); padding: 16px; overflow-y: auto; font-size: var(--fs-sm);">
|
|
1903
|
+
<div id="inspect-inspector-empty" class="empty" style="margin: 24px 0;">Select a node or edge on the left to inspect its calls, argument shapes and posture.</div>
|
|
1904
|
+
<div id="inspect-inspector-body" style="display:none;"></div>
|
|
1905
|
+
</aside>
|
|
1906
|
+
</div>
|
|
1907
|
+
</div>
|
|
1908
|
+
</div>
|
|
1909
|
+
<div id="inspect-tab-profile" class="tab-content">
|
|
1910
|
+
<div class="card" style="margin-bottom:8px; padding:12px 14px;">
|
|
1911
|
+
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
|
1912
|
+
<div>
|
|
1913
|
+
<div style="font-weight:600;">Mode</div>
|
|
1914
|
+
<div class="muted t-xs" id="inspect-mode-desc">…</div>
|
|
1915
|
+
</div>
|
|
1916
|
+
<div class="pg-seg" id="inspect-mode-seg" role="group" aria-label="Inspection mode">
|
|
1917
|
+
<button data-mode="observe" onclick="inspectSetMode('observe')">Observe</button>
|
|
1918
|
+
<button data-mode="dryrun" onclick="inspectSetMode('dryrun')">Dry-run</button>
|
|
1919
|
+
<button data-mode="enforce" onclick="inspectRequestEnforce()">Enforce</button>
|
|
1920
|
+
</div>
|
|
1921
|
+
</div>
|
|
1922
|
+
<div id="inspect-enforce-confirm" style="display:none; margin-top:10px; padding:10px 12px; border:1px solid var(--danger); background:var(--danger-soft); border-radius:6px;">
|
|
1923
|
+
<div id="inspect-enforce-msg" style="margin-bottom:8px; font-size:var(--fs-sm);"></div>
|
|
1924
|
+
<div style="display:flex; gap:8px;">
|
|
1925
|
+
<button class="btn btn-sm btn-danger" id="inspect-enforce-go" onclick="inspectSetMode('enforce', true)">Enforce — block deviations</button>
|
|
1926
|
+
<button class="btn btn-sm" onclick="inspectCancelEnforce()">Cancel</button>
|
|
1927
|
+
</div>
|
|
1928
|
+
</div>
|
|
1929
|
+
</div>
|
|
1930
|
+
<div class="card">
|
|
1931
|
+
<div class="card-header">
|
|
1932
|
+
<h2>Behavior profile <span class="muted t-xs" style="font-weight:400;">learn a baseline from observed traffic, then accept the rules that look right</span></h2>
|
|
1933
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
1934
|
+
<span id="inspect-profile-counts" class="muted t-xs"></span>
|
|
1935
|
+
<button class="btn btn-sm btn-primary" onclick="inspectDerive()" title="Derive suggested rules from the observed window">Learn from traffic</button>
|
|
1936
|
+
</div>
|
|
1937
|
+
</div>
|
|
1938
|
+
<div style="padding: 0 16px 16px;">
|
|
1939
|
+
<div style="margin: 4px 0 8px; display:flex; align-items:center; justify-content:space-between; gap:8px;">
|
|
1940
|
+
<h3 style="font-size: var(--fs-md); margin:0;">Review queue <span class="muted t-xs" id="inspect-sugg-count"></span></h3>
|
|
1941
|
+
<div id="inspect-sugg-bulk" style="display:none; gap:6px;">
|
|
1942
|
+
<button class="btn btn-sm" onclick="inspectBulk('accepted')">Accept all</button>
|
|
1943
|
+
<button class="btn btn-sm btn-ghost" onclick="inspectBulk('rejected')">Reject all</button>
|
|
1944
|
+
</div>
|
|
1945
|
+
</div>
|
|
1946
|
+
<div id="inspect-review-queue"><div class="empty" style="margin: 16px 0;">No suggestions yet. Click <b>Learn from traffic</b> to derive rules from what agents have actually done.</div></div>
|
|
1947
|
+
|
|
1948
|
+
<h3 style="font-size: var(--fs-md); margin: 20px 0 8px;">Accepted rules <span class="muted t-xs" id="inspect-acc-count"></span></h3>
|
|
1949
|
+
<div id="inspect-accepted-rules"><div class="empty" style="margin: 16px 0;">No accepted rules yet. Accept suggestions above to build the enforceable profile.</div></div>
|
|
1950
|
+
|
|
1951
|
+
<h3 style="font-size: var(--fs-md); margin: 20px 0 8px;">Rejected rules <span class="muted t-xs" id="inspect-rej-count"></span></h3>
|
|
1952
|
+
<div id="inspect-rejected-rules"><div class="empty" style="margin: 16px 0;">No rejected rules. Rejected suggestions are kept here (not deleted) and won't reappear in the review queue on re-learn — restore one anytime.</div></div>
|
|
1953
|
+
</div>
|
|
1954
|
+
</div>
|
|
1955
|
+
</div>
|
|
1956
|
+
<div id="inspect-tab-deviations" class="tab-content">
|
|
1957
|
+
<div class="card">
|
|
1958
|
+
<div class="card-header">
|
|
1959
|
+
<h2>Deviations <span class="muted t-xs" style="font-weight:400;">calls outside the accepted profile — would-block in dry-run, blocked in enforce</span></h2>
|
|
1960
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
1961
|
+
<span id="inspect-dev-stat" class="muted t-xs"></span>
|
|
1962
|
+
<button class="btn btn-sm" onclick="inspectLoadDeviations()">Refresh</button>
|
|
1963
|
+
</div>
|
|
1964
|
+
</div>
|
|
1965
|
+
<div id="inspect-deviations" style="padding:0 16px 16px;"><div class="empty" style="margin:16px 0;">Switch to <b>Dry-run</b> and accept some rules — calls that fall outside them show up here as “would-block”, so you can verify the profile before enforcing.</div></div>
|
|
1966
|
+
</div>
|
|
1967
|
+
</div>
|
|
1968
|
+
</div>
|
|
1969
|
+
|
|
1788
1970
|
<!-- ===== Settings ===== -->
|
|
1789
1971
|
<div class="page" id="page-settings">
|
|
1790
1972
|
<div class="page-head">
|
|
@@ -1904,9 +2086,24 @@
|
|
|
1904
2086
|
<div class="ph-left">
|
|
1905
2087
|
<div class="breadcrumb">Console / Governance / <b>Access Control</b></div>
|
|
1906
2088
|
<h1>Access Control</h1>
|
|
2089
|
+
<div class="gov-sub">Who can sign in and which role they hold — identities, SSO/OIDC, sessions. <em>Authentication.</em></div>
|
|
1907
2090
|
</div>
|
|
1908
2091
|
<div class="ph-actions"><button class="btn btn-primary btn-sm" onclick="entEditNew('rbac')">+ New role</button><button class="btn btn-sm" id="ent-rbac-editbtn" onclick="entEditOpen('rbac')">Edit policy</button></div>
|
|
1909
2092
|
</div>
|
|
2093
|
+
<div class="ent-banner" data-ent-feature="access-control" style="display:none">
|
|
2094
|
+
<span class="ent-banner-ico">🔒</span>
|
|
2095
|
+
<div class="ent-banner-body"><b>Enterprise feature.</b> Enforcing role-based access control requires an entitlement (<code>access-control</code>). You can review the configuration freely; rules only take effect with a license. <a href="#" onclick="showPage('entitlement');return false;">View entitlement →</a></div>
|
|
2096
|
+
</div>
|
|
2097
|
+
<div class="card gov-overview">
|
|
2098
|
+
<div class="card-header"><h2>How governance fits together</h2></div>
|
|
2099
|
+
<div class="gov-flow">
|
|
2100
|
+
<span class="gov-step"><b>Access Control</b><small>who you are</small></span><span class="gov-arrow">→</span>
|
|
2101
|
+
<span class="gov-step"><b>Products</b><small>what you're offered</small></span><span class="gov-arrow">→</span>
|
|
2102
|
+
<span class="gov-step"><b>Policies</b><small>what you're allowed</small></span><span class="gov-arrow">→</span>
|
|
2103
|
+
<span class="gov-step"><b>Inspect</b><small>is it normal</small></span>
|
|
2104
|
+
</div>
|
|
2105
|
+
<div class="muted t-xs" style="margin-top:8px;">Each layer answers a different question. <b>Catalog</b> runs alongside, adding resource context (owners, criticality). <b>Audit Log</b> records every change. <b>Policies</b> = allowed at all (static); <b>Inspect</b> = normal vs. the learned baseline (behavioral).</div>
|
|
2106
|
+
</div>
|
|
1910
2107
|
<div class="card">
|
|
1911
2108
|
<div class="card-header"><h2>Roles & bindings
|
|
1912
2109
|
<button class="info" aria-label="About roles and bindings"
|
|
@@ -2069,8 +2266,13 @@ curl -X PUT http://localhost:3000/api/enterprise/policy \
|
|
|
2069
2266
|
onclick="infoPop(this)">?</button>
|
|
2070
2267
|
</h1>
|
|
2071
2268
|
<p class="ph-sub">Curated, governed tool bundles you expose to an agent or credential.</p>
|
|
2269
|
+
<div class="gov-sub">Which tools a credential is <em>offered</em> (packaging). Distinct from <b>Policies</b>, which decide what's <em>permitted</em>.</div>
|
|
2072
2270
|
</div>
|
|
2073
2271
|
</div>
|
|
2272
|
+
<div class="ent-banner" data-ent-feature="access-control" style="display:none">
|
|
2273
|
+
<span class="ent-banner-ico">🔒</span>
|
|
2274
|
+
<div class="ent-banner-body"><b>Enterprise feature.</b> Governed context products are enforced with an entitlement (<code>access-control</code>). You can define products here; the binding only filters <code>tools/list</code> with a license. <a href="#" onclick="showPage('entitlement');return false;">View entitlement →</a></div>
|
|
2275
|
+
</div>
|
|
2074
2276
|
|
|
2075
2277
|
<!-- Primary surface: MCP Products via /api/products (RBAC-gated). -->
|
|
2076
2278
|
<div class="card" id="mcp-products-card">
|
|
@@ -2164,9 +2366,14 @@ curl -X PUT http://localhost:3000/api/enterprise/catalog \
|
|
|
2164
2366
|
<div class="ph-left">
|
|
2165
2367
|
<div class="breadcrumb">Console / Governance / <b>Policies</b></div>
|
|
2166
2368
|
<h1>Policies</h1>
|
|
2369
|
+
<div class="gov-sub">What a role is <em>allowed</em> to call at all — coarse RBAC (resource × action). Distinct from <b>Inspect</b>, which judges whether a call is <em>normal</em>.</div>
|
|
2167
2370
|
</div>
|
|
2168
2371
|
<div class="ph-actions"><span id="pol-engine" class="badge"></span></div>
|
|
2169
2372
|
</div>
|
|
2373
|
+
<div class="ent-banner" data-ent-feature="access-control" style="display:none">
|
|
2374
|
+
<span class="ent-banner-ico">🔒</span>
|
|
2375
|
+
<div class="ent-banner-body"><b>Enterprise feature.</b> The policy engine enforces verdicts with an entitlement (<code>access-control</code>). Roles and bindings are editable here; they only gate calls with a license. <a href="#" onclick="showPage('entitlement');return false;">View entitlement →</a></div>
|
|
2376
|
+
</div>
|
|
2170
2377
|
|
|
2171
2378
|
<!-- Engine banner — colour + copy distinguishes editable file
|
|
2172
2379
|
vs read-only builtin / opa modes at a glance. -->
|
|
@@ -2330,6 +2537,10 @@ curl -X PUT http://localhost:3000/api/enterprise/catalog \
|
|
|
2330
2537
|
<!-- Provisioning sub-tab (C2b) — read-only view of the SCIM-provisioned
|
|
2331
2538
|
Users/Groups an identity provider has pushed via /scim/v2. -->
|
|
2332
2539
|
<div class="card pol-pane" id="pol-pane-provisioning" role="tabpanel" hidden>
|
|
2540
|
+
<div class="ent-banner" data-ent-feature="scim" style="display:none">
|
|
2541
|
+
<span class="ent-banner-ico">🔒</span>
|
|
2542
|
+
<div class="ent-banner-body"><b>Enterprise feature.</b> SCIM 2.0 provisioning requires an entitlement (<code>scim</code>). With <code>OMCP_SCIM_TOKEN</code> set but unlicensed, the <code>/scim/v2</code> routes are not mounted (fail-closed). <a href="#" onclick="showPage('entitlement');return false;">View entitlement →</a></div>
|
|
2543
|
+
</div>
|
|
2333
2544
|
<div class="card-header"><h2>Provisioning
|
|
2334
2545
|
<button class="info" aria-label="About provisioning"
|
|
2335
2546
|
data-title="SCIM provisioning"
|
|
@@ -2394,9 +2605,14 @@ curl -X PUT http://localhost:3000/api/enterprise/catalog \
|
|
|
2394
2605
|
<div class="ph-left">
|
|
2395
2606
|
<div class="breadcrumb">Console / Governance / <b>Audit Log</b></div>
|
|
2396
2607
|
<h1>Audit Log</h1>
|
|
2608
|
+
<div class="gov-sub">An append-only, tamper-evident record of every mutating action — who did what, when.</div>
|
|
2397
2609
|
</div>
|
|
2398
2610
|
<div class="ph-actions"><span id="ent-chain"></span></div>
|
|
2399
2611
|
</div>
|
|
2612
|
+
<div class="ent-banner" data-ent-feature="audit" style="display:none">
|
|
2613
|
+
<span class="ent-banner-ico">🔒</span>
|
|
2614
|
+
<div class="ent-banner-body"><b>Enterprise feature.</b> The tamper-evident audit log requires an entitlement (<code>audit</code>). Without it, decisions are not recorded to the hash-chained sink. <a href="#" onclick="showPage('entitlement');return false;">View entitlement →</a></div>
|
|
2615
|
+
</div>
|
|
2400
2616
|
<div class="card">
|
|
2401
2617
|
<div class="card-header"><h2>Recent decisions
|
|
2402
2618
|
<button class="info" aria-label="About the audit log"
|
|
@@ -2705,6 +2921,7 @@ function showPage(name) {
|
|
|
2705
2921
|
if(name==='policies') loadPolicies();
|
|
2706
2922
|
if(name==='postmortems') pmLoadList();
|
|
2707
2923
|
if(name==='playground') pgInit();
|
|
2924
|
+
if(name==='inspect') inspectInit();
|
|
2708
2925
|
}
|
|
2709
2926
|
|
|
2710
2927
|
// --- Playground tab (Q13 / v3.1) ---------------------------------------
|
|
@@ -7341,10 +7558,46 @@ function loadingBlock(label) {
|
|
|
7341
7558
|
}
|
|
7342
7559
|
async function refresh(){await Promise.all([loadSources(),loadServices(),loadDashUsage()]);}
|
|
7343
7560
|
|
|
7561
|
+
// Entitlement lock optic. /api/info.governance.entitlements is a flat
|
|
7562
|
+
// { feature: bool } map; all false on the OSS default. We render a consistent
|
|
7563
|
+
// 🔒 badge + explanatory banner on every entitled feature so the enterprise
|
|
7564
|
+
// surface reads as "visible but locked" rather than silently inert. Purely
|
|
7565
|
+
// informational — the real enforcement is server-side and fail-closed.
|
|
7566
|
+
window.__entitlements = window.__entitlements || {};
|
|
7567
|
+
function omcpFeatureLocked(feature){ return !window.__entitlements[feature]; }
|
|
7568
|
+
// nav-rail page → required feature
|
|
7569
|
+
const ENT_NAV_FEATURES = { products:'access-control', access:'access-control', policies:'access-control', audit:'audit' };
|
|
7570
|
+
function applyEntitlementLocks(gov){
|
|
7571
|
+
window.__entitlements = (gov && gov.entitlements) || {};
|
|
7572
|
+
// 1. Nav-rail badges.
|
|
7573
|
+
for(const page in ENT_NAV_FEATURES){
|
|
7574
|
+
const btn=document.querySelector(`.nav-btn[data-page="${page}"]`);
|
|
7575
|
+
if(!btn) continue;
|
|
7576
|
+
const locked=omcpFeatureLocked(ENT_NAV_FEATURES[page]);
|
|
7577
|
+
btn.classList.toggle('ent-locked', locked);
|
|
7578
|
+
let lk=btn.querySelector('.ent-lock');
|
|
7579
|
+
if(locked && !lk){ lk=document.createElement('span'); lk.className='ent-lock'; lk.textContent='🔒'; lk.setAttribute('aria-label','Enterprise feature — requires an entitlement'); lk.title='Enterprise feature — requires an entitlement'; btn.appendChild(lk); }
|
|
7580
|
+
else if(!locked && lk){ lk.remove(); }
|
|
7581
|
+
}
|
|
7582
|
+
// 2. Page / sub-tab banners (declarative: any [data-ent-feature]).
|
|
7583
|
+
document.querySelectorAll('.ent-banner[data-ent-feature]').forEach(el=>{
|
|
7584
|
+
el.style.display=omcpFeatureLocked(el.getAttribute('data-ent-feature'))?'flex':'none';
|
|
7585
|
+
});
|
|
7586
|
+
// 3. Provisioning sub-tab badge (SCIM).
|
|
7587
|
+
const provTab=document.querySelector('.pol-subtab[data-pol-tab="provisioning"]');
|
|
7588
|
+
if(provTab){
|
|
7589
|
+
const locked=omcpFeatureLocked('scim');
|
|
7590
|
+
let lk=provTab.querySelector('.ent-lock');
|
|
7591
|
+
if(locked && !lk){ lk=document.createElement('span'); lk.className='ent-lock'; lk.textContent='🔒'; provTab.appendChild(lk); }
|
|
7592
|
+
else if(!locked && lk){ lk.remove(); }
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7595
|
+
|
|
7344
7596
|
async function loadInfo(){
|
|
7345
7597
|
try{
|
|
7346
7598
|
const r=await fetch('/api/info'); if(!r.ok) return;
|
|
7347
7599
|
const d=await r.json();
|
|
7600
|
+
try{ applyEntitlementLocks(d.governance); }catch(e){/* lock optic is best-effort */}
|
|
7348
7601
|
const footer=document.getElementById('info-footer');
|
|
7349
7602
|
if(!footer) return;
|
|
7350
7603
|
const plugins=(d.plugins||[]).map(p=>`<span class="footer-pill">${esc(p.name)}<span class="footer-pill-meta">${esc(p.source)}${p.version?` · ${esc(p.version)}`:''}</span></span>`).join('');
|
|
@@ -7364,6 +7617,495 @@ async function loadInfo(){
|
|
|
7364
7617
|
// Show the endpoint the user is actually reaching the server through
|
|
7365
7618
|
// (localhost, a port-forward, or an ingress) — not a hardcoded host.
|
|
7366
7619
|
document.getElementById('mcp-url').textContent = window.location.origin + '/mcp';
|
|
7620
|
+
// ===== Inspect — Flows live graph =====
|
|
7621
|
+
let inspectStarted=false, inspectLive=true, inspectTimer=null, inspectData=null, inspectRev='', inspectSel=null;
|
|
7622
|
+
const inspectView={tx:0,ty:0,scale:1};
|
|
7623
|
+
const INSPECT_COL={identity:170,tool:500,backend:830};
|
|
7624
|
+
const INSPECT_NODE_W=150, INSPECT_ROW_H=46, INSPECT_PAD_TOP=46;
|
|
7625
|
+
|
|
7626
|
+
function inspectInit(){
|
|
7627
|
+
if(!inspectStarted){
|
|
7628
|
+
inspectStarted=true;
|
|
7629
|
+
wireInspectGraph();
|
|
7630
|
+
// Delegated handler for profile rule actions (data-rule + data-act) — keeps
|
|
7631
|
+
// user-derived rule ids out of inline onclick (XSS-safe).
|
|
7632
|
+
const ptab=document.getElementById('inspect-tab-profile');
|
|
7633
|
+
if(ptab) ptab.addEventListener('click',(ev)=>{ const b=ev.target.closest('button[data-rule]'); if(!b) return; const id=b.getAttribute('data-rule'), act=b.getAttribute('data-act'); if(act==='edit') inspectOpenRuleEditor(id); else inspectRuleAction(id, act); });
|
|
7634
|
+
// Self-checking poll: only refreshes while the tab is active AND live.
|
|
7635
|
+
inspectTimer=setInterval(()=>{
|
|
7636
|
+
if(document.getElementById('page-inspect').classList.contains('active') && inspectLive) loadInspectFlows(false);
|
|
7637
|
+
}, 2500);
|
|
7638
|
+
}
|
|
7639
|
+
loadInspectFlows(true);
|
|
7640
|
+
}
|
|
7641
|
+
|
|
7642
|
+
function showInspectTab(name){
|
|
7643
|
+
document.querySelectorAll('#page-inspect .tab-btn').forEach(b=>b.classList.remove('active'));
|
|
7644
|
+
document.querySelectorAll('#page-inspect .tab-content').forEach(c=>c.classList.remove('active'));
|
|
7645
|
+
const btn=[...document.querySelectorAll('#page-inspect .tab-btn')].find(b=>b.textContent.trim().toLowerCase()===name);
|
|
7646
|
+
if(btn) btn.classList.add('active');
|
|
7647
|
+
const c=document.getElementById('inspect-tab-'+name); if(c) c.classList.add('active');
|
|
7648
|
+
if(name==='profile'){ inspectRefreshMode(); inspectLoadProfile(); }
|
|
7649
|
+
if(name==='deviations') inspectLoadDeviations();
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7652
|
+
const INSPECT_MODE_DESC={
|
|
7653
|
+
off:'Disabled — no recording.',
|
|
7654
|
+
observe:'Recording only — every tool call is captured; nothing is evaluated or blocked.',
|
|
7655
|
+
dryrun:'Evaluating against accepted rules — deviations are flagged (see Deviations) but still allowed.',
|
|
7656
|
+
enforce:'Enforcing — calls outside the accepted profile are blocked.'
|
|
7657
|
+
};
|
|
7658
|
+
|
|
7659
|
+
async function inspectRefreshMode(){
|
|
7660
|
+
try{ const r=await fetch('/api/inspect/mode'); if(r.ok) updateInspectModeChip(await r.json()); }catch(e){}
|
|
7661
|
+
}
|
|
7662
|
+
|
|
7663
|
+
async function inspectSetMode(mode, confirmed){
|
|
7664
|
+
// Enforce is destructive (it blocks live tool calls) — gate behind a confirm
|
|
7665
|
+
// that shows how many calls would have been blocked recently.
|
|
7666
|
+
if(mode==='enforce' && !confirmed){ inspectRequestEnforce(); return; }
|
|
7667
|
+
inspectCancelEnforce();
|
|
7668
|
+
try{
|
|
7669
|
+
const r=await fetch('/api/inspect/mode',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({mode})});
|
|
7670
|
+
if(!r.ok){ let msg='Mode change failed'; try{ const e=await r.json(); if(e&&e.error) msg=e.error; }catch(_){} toast(msg,'error'); return; }
|
|
7671
|
+
const j=await r.json();
|
|
7672
|
+
toast('Inspection mode: '+(j.mode||mode),'ok');
|
|
7673
|
+
inspectRefreshMode();
|
|
7674
|
+
if(document.getElementById('inspect-tab-deviations').classList.contains('active')) inspectLoadDeviations();
|
|
7675
|
+
}catch(e){ toast('Mode change failed','error'); }
|
|
7676
|
+
}
|
|
7677
|
+
|
|
7678
|
+
async function inspectRequestEnforce(){
|
|
7679
|
+
const msg=document.getElementById('inspect-enforce-msg');
|
|
7680
|
+
const go=document.getElementById('inspect-enforce-go');
|
|
7681
|
+
// Enforce (active blocking) is an entitled control — show an Enterprise
|
|
7682
|
+
// notice instead of the confirm action when unlicensed.
|
|
7683
|
+
if(window.__inspectEnforceEntitled===false){
|
|
7684
|
+
msg.innerHTML='🔒 <b>Enforce is an Enterprise feature.</b> Blocking out-of-profile calls requires an entitlement (inspect-enforce). <b>Observe</b> and <b>Dry-run</b> — including the live graph, learning, and would-block deviations — are available without a license.';
|
|
7685
|
+
if(go) go.style.display='none';
|
|
7686
|
+
document.getElementById('inspect-enforce-confirm').style.display='block';
|
|
7687
|
+
return;
|
|
7688
|
+
}
|
|
7689
|
+
if(go) go.style.display='';
|
|
7690
|
+
let accepted=0, wouldBlock=0;
|
|
7691
|
+
try{ const p=await fetch('/api/inspect/profile'); if(p.ok){ const j=await p.json(); accepted=(j.counts&&j.counts.accepted)||0; } }catch(e){}
|
|
7692
|
+
try{ const d=await fetch('/api/inspect/deviations?window=24h'); if(d.ok){ const j=await d.json(); wouldBlock=j.total||0; } }catch(e){}
|
|
7693
|
+
if(accepted===0){
|
|
7694
|
+
msg.innerHTML='<b>No accepted rules.</b> Enforcing now would block <b>every</b> call (nothing is allow-listed). Accept some rules first, or continue only if you really intend a default-deny.';
|
|
7695
|
+
}else{
|
|
7696
|
+
msg.innerHTML='Enforce will <b>block</b> any call outside the <b>'+accepted+'</b> accepted rule'+(accepted===1?'':'s')+'. In the last 24h, <b>'+wouldBlock+'</b> call'+(wouldBlock===1?'':'s')+' would have been blocked. Review them under Deviations first.';
|
|
7697
|
+
}
|
|
7698
|
+
document.getElementById('inspect-enforce-confirm').style.display='block';
|
|
7699
|
+
}
|
|
7700
|
+
function inspectCancelEnforce(){
|
|
7701
|
+
const c=document.getElementById('inspect-enforce-confirm'); if(c) c.style.display='none';
|
|
7702
|
+
}
|
|
7703
|
+
|
|
7704
|
+
async function inspectLoadDeviations(){
|
|
7705
|
+
try{
|
|
7706
|
+
const win=(document.getElementById('inspect-window')||{}).value||'24h';
|
|
7707
|
+
const r=await fetch('/api/inspect/deviations?window='+encodeURIComponent(win));
|
|
7708
|
+
const box=document.getElementById('inspect-deviations');
|
|
7709
|
+
if(!r.ok){ box.innerHTML='<div class="empty" style="margin:16px 0;">'+(r.status===403?'You don’t have permission to view deviations.':'Unavailable.')+'</div>'; return; }
|
|
7710
|
+
const j=await r.json();
|
|
7711
|
+
document.getElementById('inspect-dev-stat').textContent=(j.total||0)+' in window · mode '+(j.mode||'');
|
|
7712
|
+
const devs=j.deviations||[];
|
|
7713
|
+
window.__inspectDeviations=devs;
|
|
7714
|
+
if(!devs.length){ box.innerHTML='<div class="empty" style="margin:16px 0;">No deviations in this window. In <b>Observe</b> mode nothing is evaluated; switch to <b>Dry-run</b> with accepted rules to surface would-block calls.</div>'; return; }
|
|
7715
|
+
box.innerHTML='<table class="dtable"><thead><tr><th>When</th><th>Identity</th><th>Tool</th><th>Backend</th><th>Why</th><th>Decision</th><th></th></tr></thead><tbody>'
|
|
7716
|
+
+devs.map((d,i)=>{ const backend=d.service||d.source||d.namespace||'—'; const dec=d.decision==='blocked'?'<span class="stchip bad">blocked</span>':'<span class="stchip warn">would-block</span>';
|
|
7717
|
+
return '<tr><td class="muted t-xs">'+esc((d.ts||'').replace('T',' ').replace(/\..*/,''))+'</td><td>'+esc(d.principal)+'</td><td>'+esc(d.tool)+'</td><td>'+esc(backend)+'</td><td class="t-xs">'+esc(d.deviation||'deviation')+(d.detail?(' · '+esc(d.detail)):'')+'</td><td>'+dec+'</td>'
|
|
7718
|
+
+'<td style="text-align:right;white-space:nowrap;"><button class="btn btn-sm" onclick="inspectAddDeviation('+i+')" title="Add exactly this call shape to the accepted profile">Add to profile</button></td></tr>'; }).join('')
|
|
7719
|
+
+'</tbody></table>';
|
|
7720
|
+
}catch(e){}
|
|
7721
|
+
}
|
|
7722
|
+
|
|
7723
|
+
async function inspectAddDeviation(i){
|
|
7724
|
+
const d=(window.__inspectDeviations||[])[i]; if(!d) return;
|
|
7725
|
+
try{
|
|
7726
|
+
const r=await fetch('/api/inspect/profile/from-deviation',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({principal:d.principal,tool:d.tool,source:d.source,service:d.service,namespace:d.namespace,argShape:d.argShape||{}})});
|
|
7727
|
+
if(!r.ok){ toast(r.status===403?'Need inspection:write':'Failed','error'); return; }
|
|
7728
|
+
toast('Added '+d.principal+' → '+d.tool+' to profile','ok');
|
|
7729
|
+
}catch(e){ toast('Failed','error'); }
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7732
|
+
// --- Inspect: behavior profile (learn / review) ---
|
|
7733
|
+
function inspectConstraintSummary(c){
|
|
7734
|
+
const parts=[];
|
|
7735
|
+
['source','service','namespace'].forEach(k=>{ if(c[k]&&c[k].length) parts.push(k+'∈{'+c[k].map(esc).join(', ')+'}'); });
|
|
7736
|
+
if(c.argShape){ for(const k in c.argShape){ parts.push(esc(k)+'∈{'+c.argShape[k].map(esc).join(', ')+'}'); } }
|
|
7737
|
+
return parts.length?parts.join(' · '):'<span class="muted">any (no constraints observed)</span>';
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7740
|
+
async function inspectLoadProfile(){
|
|
7741
|
+
try{
|
|
7742
|
+
const r=await fetch('/api/inspect/profile');
|
|
7743
|
+
if(!r.ok){ document.getElementById('inspect-review-queue').innerHTML='<div class="empty" style="margin:16px 0;">'+(r.status===403?'You don’t have permission to view the profile.':'Profile unavailable.')+'</div>'; return; }
|
|
7744
|
+
renderInspectProfile(await r.json());
|
|
7745
|
+
}catch(e){}
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
async function inspectDerive(){
|
|
7749
|
+
try{
|
|
7750
|
+
const win=(document.getElementById('inspect-window')||{}).value||'24h';
|
|
7751
|
+
const r=await fetch('/api/inspect/profile/derive?window='+encodeURIComponent(win),{method:'POST'});
|
|
7752
|
+
if(!r.ok){ toast(r.status===403?'Need inspection:write to learn':'Learn failed', 'error'); return; }
|
|
7753
|
+
const j=await r.json();
|
|
7754
|
+
toast('Learned from '+j.learnedFrom+' calls · '+j.suggested+' suggestions', 'ok');
|
|
7755
|
+
inspectLoadProfile();
|
|
7756
|
+
}catch(e){ toast('Learn failed','error'); }
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
// G2: granular editor — toggle which observed values/buckets a rule allows.
|
|
7760
|
+
function inspectChip(attr, val){
|
|
7761
|
+
return '<label class="rule-chip"><input type="checkbox" '+attr+' value="'+escHtml(val)+'" checked> '+esc(val)+'</label>';
|
|
7762
|
+
}
|
|
7763
|
+
function inspectOpenRuleEditor(id){
|
|
7764
|
+
const rule=(window.__inspectRules||[]).find(r=>r.id===id); if(!rule){ toast('Rule not found','error'); return; }
|
|
7765
|
+
window.__inspectEditRuleId=id;
|
|
7766
|
+
document.getElementById('inspect-rule-modal-title').textContent='Edit rule — '+rule.subject+' → '+rule.tool;
|
|
7767
|
+
const c=rule.constraints||{}; let html='';
|
|
7768
|
+
for(const dim of ['source','service','namespace']){
|
|
7769
|
+
if(c[dim] && c[dim].length){
|
|
7770
|
+
html+='<div class="form-group"><label>'+dim+'</label><div class="rule-chips">'+c[dim].map(v=>inspectChip('data-dim="'+dim+'"',v)).join('')+'</div></div>';
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
if(c.argShape){
|
|
7774
|
+
for(const k in c.argShape){
|
|
7775
|
+
html+='<div class="form-group"><label>arg: '+esc(k)+'</label><div class="rule-chips">'+c.argShape[k].map(v=>inspectChip('data-argkey="'+escHtml(k)+'"',v)).join('')+'</div></div>';
|
|
7776
|
+
}
|
|
7777
|
+
}
|
|
7778
|
+
if(!html) html='<div class="muted t-xs">This rule has no constraints (it matches the tool with no resource/arg restrictions). Nothing to narrow.</div>';
|
|
7779
|
+
html+='<div class="form-hint">Unchecking a value removes it from the allow-list — calls using it then count as a deviation. Narrow only; you can re-learn to widen.</div>';
|
|
7780
|
+
document.getElementById('inspect-rule-modal-body').innerHTML=html;
|
|
7781
|
+
document.getElementById('inspect-rule-modal').classList.add('open');
|
|
7782
|
+
}
|
|
7783
|
+
async function inspectSaveRule(accept){
|
|
7784
|
+
const id=window.__inspectEditRuleId; if(!id) return;
|
|
7785
|
+
const body=document.getElementById('inspect-rule-modal-body');
|
|
7786
|
+
const constraints={};
|
|
7787
|
+
body.querySelectorAll('input[data-dim]:checked').forEach(i=>{ const d=i.getAttribute('data-dim'); (constraints[d]=constraints[d]||[]).push(i.value); });
|
|
7788
|
+
const arg={};
|
|
7789
|
+
body.querySelectorAll('input[data-argkey]:checked').forEach(i=>{ const k=i.getAttribute('data-argkey'); (arg[k]=arg[k]||[]).push(i.value); });
|
|
7790
|
+
if(Object.keys(arg).length) constraints.argShape=arg;
|
|
7791
|
+
try{
|
|
7792
|
+
const r=await fetch('/api/inspect/profile/rules/'+encodeURIComponent(id),{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({constraints})});
|
|
7793
|
+
if(!r.ok){ toast(r.status===403?'Need inspection:write':'Save failed','error'); return; }
|
|
7794
|
+
if(accept){ await fetch('/api/inspect/profile/rules/'+encodeURIComponent(id),{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({status:'accepted'})}); }
|
|
7795
|
+
closeModal('inspect-rule-modal');
|
|
7796
|
+
toast(accept?'Saved & accepted':'Rule updated','ok');
|
|
7797
|
+
inspectLoadProfile();
|
|
7798
|
+
}catch(e){ toast('Save failed','error'); }
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
async function inspectRuleAction(id,status){
|
|
7802
|
+
try{
|
|
7803
|
+
const r=await fetch('/api/inspect/profile/rules/'+encodeURIComponent(id),{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({status})});
|
|
7804
|
+
if(!r.ok){ toast(r.status===403?'Need inspection:write':'Update failed','error'); return; }
|
|
7805
|
+
inspectLoadProfile();
|
|
7806
|
+
}catch(e){ toast('Update failed','error'); }
|
|
7807
|
+
}
|
|
7808
|
+
|
|
7809
|
+
async function inspectBulk(status){
|
|
7810
|
+
const ids=(window.__inspectSuggestedIds||[]);
|
|
7811
|
+
for(const id of ids){ await inspectRuleAction(id,status); }
|
|
7812
|
+
}
|
|
7813
|
+
|
|
7814
|
+
function renderInspectProfile(data){
|
|
7815
|
+
const rules=data.rules||[]; const c=data.counts||{};
|
|
7816
|
+
document.getElementById('inspect-profile-counts').textContent=(c.suggested||0)+' suggested · '+(c.accepted||0)+' accepted · '+(c.rejected||0)+' rejected'+(data.persisted?' · persisted':'');
|
|
7817
|
+
const sugg=rules.filter(r=>r.status==='suggested');
|
|
7818
|
+
const acc=rules.filter(r=>r.status==='accepted');
|
|
7819
|
+
const rej=rules.filter(r=>r.status==='rejected');
|
|
7820
|
+
window.__inspectSuggestedIds=sugg.map(r=>r.id);
|
|
7821
|
+
window.__inspectRules=rules;
|
|
7822
|
+
document.getElementById('inspect-sugg-count').textContent=sugg.length?('('+sugg.length+')'):'';
|
|
7823
|
+
document.getElementById('inspect-acc-count').textContent=acc.length?('('+acc.length+')'):'';
|
|
7824
|
+
document.getElementById('inspect-rej-count').textContent=rej.length?('('+rej.length+')'):'';
|
|
7825
|
+
document.getElementById('inspect-sugg-bulk').style.display=sugg.length>1?'flex':'none';
|
|
7826
|
+
|
|
7827
|
+
const q=document.getElementById('inspect-review-queue');
|
|
7828
|
+
if(!sugg.length){ q.innerHTML='<div class="empty" style="margin:16px 0;">No pending suggestions. Click <b>Learn from traffic</b> to (re-)derive from the selected window.</div>'; }
|
|
7829
|
+
else{
|
|
7830
|
+
q.innerHTML=sugg.map(r=>'<div class="card" style="margin:0 0 8px; padding:12px 14px;">'
|
|
7831
|
+
+'<div style="display:flex; justify-content:space-between; gap:12px; align-items:flex-start;">'
|
|
7832
|
+
+'<div style="min-width:0;">'
|
|
7833
|
+
+'<div style="font-weight:600;">'+esc(r.subject)+' <span class="muted">→</span> <span style="color:var(--accent)">'+esc(r.tool)+'</span></div>'
|
|
7834
|
+
+'<div class="muted t-xs" style="margin:4px 0;">'+inspectConstraintSummary(r.constraints)+'</div>'
|
|
7835
|
+
+'<div class="muted t-xs">learned from '+r.provenance.learnedFrom+' call'+(r.provenance.learnedFrom===1?'':'s')+'</div>'
|
|
7836
|
+
+'</div>'
|
|
7837
|
+
+'<div style="display:flex; gap:6px; flex-shrink:0;">'
|
|
7838
|
+
+'<button class="btn btn-sm btn-primary" data-rule="'+escHtml(r.id)+'" data-act="accepted">Accept</button>'
|
|
7839
|
+
+'<button class="btn btn-sm" data-rule="'+escHtml(r.id)+'" data-act="edit" title="Narrow which values this rule allows">Edit</button>'
|
|
7840
|
+
+'<button class="btn btn-sm btn-ghost" data-rule="'+escHtml(r.id)+'" data-act="rejected">Reject</button>'
|
|
7841
|
+
+'</div>'
|
|
7842
|
+
+'</div></div>').join('');
|
|
7843
|
+
}
|
|
7844
|
+
|
|
7845
|
+
const a=document.getElementById('inspect-accepted-rules');
|
|
7846
|
+
if(!acc.length){ a.innerHTML='<div class="empty" style="margin:16px 0;">No accepted rules yet.</div>'; }
|
|
7847
|
+
else{
|
|
7848
|
+
a.innerHTML='<table class="dtable"><thead><tr><th>Subject</th><th>Tool</th><th>Constraints</th><th>Learned</th><th></th></tr></thead><tbody>'
|
|
7849
|
+
+acc.map(r=>'<tr><td>'+esc(r.subject)+'</td><td>'+esc(r.tool)+'</td><td class="muted t-xs">'+inspectConstraintSummary(r.constraints)+'</td><td>'+r.provenance.learnedFrom+'</td>'
|
|
7850
|
+
+'<td style="text-align:right; white-space:nowrap;"><button class="btn btn-sm" data-rule="'+escHtml(r.id)+'" data-act="edit" title="Narrow which values this rule allows">Edit</button> <button class="btn btn-sm btn-ghost" data-rule="'+escHtml(r.id)+'" data-act="suggested" title="Move back to review">Unaccept</button></td></tr>').join('')
|
|
7851
|
+
+'</tbody></table>';
|
|
7852
|
+
}
|
|
7853
|
+
|
|
7854
|
+
const rj=document.getElementById('inspect-rejected-rules');
|
|
7855
|
+
if(!rej.length){ rj.innerHTML='<div class="empty" style="margin:16px 0;">No rejected rules.</div>'; }
|
|
7856
|
+
else{
|
|
7857
|
+
rj.innerHTML='<table class="dtable"><thead><tr><th>Subject</th><th>Tool</th><th>Constraints</th><th>Learned</th><th></th></tr></thead><tbody>'
|
|
7858
|
+
+rej.map(r=>'<tr style="opacity:0.7;"><td>'+esc(r.subject)+'</td><td>'+esc(r.tool)+'</td><td class="muted t-xs">'+inspectConstraintSummary(r.constraints)+'</td><td>'+r.provenance.learnedFrom+'</td>'
|
|
7859
|
+
+'<td style="text-align:right; white-space:nowrap;"><button class="btn btn-sm" data-rule="'+escHtml(r.id)+'" data-act="suggested" title="Restore to the review queue">Restore</button> <button class="btn btn-sm btn-primary" data-rule="'+escHtml(r.id)+'" data-act="accepted" title="Accept directly">Accept</button></td></tr>').join('')
|
|
7860
|
+
+'</tbody></table>';
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
|
|
7864
|
+
function toggleInspectLive(){
|
|
7865
|
+
inspectLive=!inspectLive;
|
|
7866
|
+
const b=document.getElementById('inspect-live-btn');
|
|
7867
|
+
if(b) b.textContent=inspectLive?'⏸ Live':'▶ Paused';
|
|
7868
|
+
const svg=document.getElementById('inspect-graph-svg'); if(svg) svg.classList.toggle('paused',!inspectLive);
|
|
7869
|
+
if(inspectLive) loadInspectFlows(true);
|
|
7870
|
+
}
|
|
7871
|
+
|
|
7872
|
+
async function loadInspectFlows(force){
|
|
7873
|
+
try{
|
|
7874
|
+
const win=(document.getElementById('inspect-window')||{}).value||'1h';
|
|
7875
|
+
const [fRes,mRes]=await Promise.all([
|
|
7876
|
+
fetch('/api/inspect/flows?window='+encodeURIComponent(win)),
|
|
7877
|
+
fetch('/api/inspect/mode'),
|
|
7878
|
+
]);
|
|
7879
|
+
if(mRes.ok){ try{ updateInspectModeChip(await mRes.json()); }catch(e){} }
|
|
7880
|
+
if(!fRes.ok){ showInspectGraphMessage(fRes.status===403?'You don’t have permission to view Inspect (needs inspection:read).':'Inspect is unavailable.'); return; }
|
|
7881
|
+
const data=await fRes.json();
|
|
7882
|
+
inspectData=data;
|
|
7883
|
+
const rev=(data.total||0)+'|'+(data.nodes||[]).map(n=>n.id+':'+n.calls+':'+n.deviations).join(',')+'|'+win;
|
|
7884
|
+
if(!force && rev===inspectRev) return; // unchanged → keep view + selection
|
|
7885
|
+
inspectRev=rev;
|
|
7886
|
+
renderInspectGraph();
|
|
7887
|
+
}catch(e){ showInspectGraphMessage('Inspect is unavailable.'); }
|
|
7888
|
+
}
|
|
7889
|
+
|
|
7890
|
+
function updateInspectModeChip(m){
|
|
7891
|
+
const chip=document.getElementById('inspect-mode-chip'); if(!chip||!m) return;
|
|
7892
|
+
const mode=m.mode||'observe';
|
|
7893
|
+
const label={off:'Off',observe:'Observe',dryrun:'Dry-run',enforce:'Enforce'}[mode]||mode;
|
|
7894
|
+
chip.textContent='● '+label;
|
|
7895
|
+
chip.className='stchip '+(mode==='enforce'?'bad':mode==='dryrun'?'warn':mode==='off'?'':'ok');
|
|
7896
|
+
chip.title='Inspection mode: '+label+(m.size!=null?(' · '+m.size+' observations stored'):'');
|
|
7897
|
+
// Reflect into the Profile-tab segmented control + description, when present.
|
|
7898
|
+
const seg=document.getElementById('inspect-mode-seg');
|
|
7899
|
+
if(seg) seg.querySelectorAll('button[data-mode]').forEach(b=>b.classList.toggle('active', b.getAttribute('data-mode')===mode));
|
|
7900
|
+
const desc=document.getElementById('inspect-mode-desc');
|
|
7901
|
+
if(desc) desc.textContent=(typeof INSPECT_MODE_DESC!=='undefined'&&INSPECT_MODE_DESC[mode])||'';
|
|
7902
|
+
// Enforce is an entitled control — lock the segment when unlicensed.
|
|
7903
|
+
window.__inspectEnforceEntitled = (m.enforceEntitled !== false);
|
|
7904
|
+
const enf=seg&&seg.querySelector('button[data-mode="enforce"]');
|
|
7905
|
+
if(enf){
|
|
7906
|
+
const locked=m.enforceEntitled===false;
|
|
7907
|
+
enf.classList.toggle('locked', locked);
|
|
7908
|
+
enf.textContent=locked?'Enforce 🔒':'Enforce';
|
|
7909
|
+
enf.title=locked?'Enforce (active blocking) is an Enterprise feature — requires an entitlement':'';
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
|
|
7913
|
+
function showInspectGraphMessage(msg){
|
|
7914
|
+
const e=document.getElementById('inspect-graph-empty'); if(e){ e.style.display='flex'; e.textContent=msg; }
|
|
7915
|
+
const svg=document.getElementById('inspect-graph-svg'); if(svg) svg.innerHTML='';
|
|
7916
|
+
}
|
|
7917
|
+
|
|
7918
|
+
function inspectEdgePosture(e){
|
|
7919
|
+
if(e.denied>0) return 'denied';
|
|
7920
|
+
if(e.deviation>0) return 'deviation';
|
|
7921
|
+
return 'allow';
|
|
7922
|
+
}
|
|
7923
|
+
function inspectPostureColor(p){
|
|
7924
|
+
return p==='denied'?'var(--danger)':p==='deviation'?'var(--warning)':'var(--success)';
|
|
7925
|
+
}
|
|
7926
|
+
|
|
7927
|
+
function renderInspectGraph(){
|
|
7928
|
+
const svg=document.getElementById('inspect-graph-svg');
|
|
7929
|
+
const empty=document.getElementById('inspect-graph-empty');
|
|
7930
|
+
if(!svg||!inspectData) return;
|
|
7931
|
+
const cols={identity:[],tool:[],backend:[]};
|
|
7932
|
+
(inspectData.nodes||[]).forEach(n=>{ if(cols[n.kind]) cols[n.kind].push(n); });
|
|
7933
|
+
Object.values(cols).forEach(a=>a.sort((x,y)=>y.calls-x.calls));
|
|
7934
|
+
if((inspectData.total||0)===0 || (inspectData.nodes||[]).length===0){
|
|
7935
|
+
empty.style.display='flex';
|
|
7936
|
+
empty.textContent='No traffic in this window yet. Make a tool call (e.g. from the Playground) and it appears here live.';
|
|
7937
|
+
svg.innerHTML=''; renderInspectLegend(); updateInspectFlowStat(); return;
|
|
7938
|
+
}
|
|
7939
|
+
empty.style.display='none';
|
|
7940
|
+
const maxRows=Math.max(cols.identity.length,cols.tool.length,cols.backend.length,1);
|
|
7941
|
+
const pos={};
|
|
7942
|
+
for(const kind of ['identity','tool','backend']){
|
|
7943
|
+
const arr=cols[kind]; const colH=arr.length*INSPECT_ROW_H;
|
|
7944
|
+
const start=INSPECT_PAD_TOP+(maxRows*INSPECT_ROW_H-colH)/2;
|
|
7945
|
+
arr.forEach((n,i)=>{ pos[n.id]={x:INSPECT_COL[kind], y:start+i*INSPECT_ROW_H+INSPECT_ROW_H/2, node:n, kind}; });
|
|
7946
|
+
}
|
|
7947
|
+
const maxCount=Math.max(...(inspectData.edges||[]).map(e=>e.count),1);
|
|
7948
|
+
const hw=INSPECT_NODE_W/2;
|
|
7949
|
+
let edgesSvg='';
|
|
7950
|
+
(inspectData.edges||[]).forEach((e,i)=>{
|
|
7951
|
+
const a=pos[e.from], b=pos[e.to]; if(!a||!b) return;
|
|
7952
|
+
const x1=a.x+hw, y1=a.y, x2=b.x-hw, y2=b.y, mx=(x1+x2)/2;
|
|
7953
|
+
const w=(1.4+Math.log2(1+e.count)/Math.log2(1+maxCount)*5).toFixed(2);
|
|
7954
|
+
const posture=inspectEdgePosture(e);
|
|
7955
|
+
const col=inspectPostureColor(posture);
|
|
7956
|
+
const flow=(e.allow>0 && posture==='allow')?' flow':'';
|
|
7957
|
+
edgesSvg+='<path class="iedge'+flow+'" data-edge="'+i+'" d="M '+x1+' '+y1+' C '+mx+' '+y1+', '+mx+' '+y2+', '+x2+' '+y2+'" stroke="'+col+'" stroke-width="'+w+'" opacity="0.55"></path>';
|
|
7958
|
+
});
|
|
7959
|
+
let nodesSvg='';
|
|
7960
|
+
const kindDot={identity:'var(--info)',tool:'var(--accent)',backend:'var(--success)'};
|
|
7961
|
+
for(const id in pos){
|
|
7962
|
+
const p=pos[id], n=p.node;
|
|
7963
|
+
const x=p.x-hw, y=p.y-15;
|
|
7964
|
+
const lbl=esc(n.label.length>20?n.label.slice(0,19)+'…':n.label);
|
|
7965
|
+
const devTag=n.deviations>0?'<text class="ilabel" x="'+(p.x+hw-8)+'" y="'+(p.y+4)+'" text-anchor="end" fill="var(--warning)">⚠'+n.deviations+'</text>':'';
|
|
7966
|
+
nodesSvg+='<g class="inode-g" data-id="'+escHtml(id)+'" tabindex="0" role="button" aria-label="'+escHtml(n.kind+' '+n.label+', '+n.calls+' calls')+'">'
|
|
7967
|
+
+'<rect class="inode" x="'+x+'" y="'+y+'" width="'+INSPECT_NODE_W+'" height="30" rx="7" fill="var(--surface)" stroke="var(--border-strong)" stroke-width="1"></rect>'
|
|
7968
|
+
+'<circle cx="'+(x+12)+'" cy="'+p.y+'" r="4" fill="'+kindDot[p.kind]+'"></circle>'
|
|
7969
|
+
+'<text class="ilabel" x="'+(x+22)+'" y="'+(p.y-2)+'" fill="var(--text)">'+lbl+'</text>'
|
|
7970
|
+
+'<text class="ilabel" x="'+(x+22)+'" y="'+(p.y+10)+'">'+n.calls+' call'+(n.calls===1?'':'s')+(n.errors>0?(' · '+n.errors+' err'):'')+'</text>'
|
|
7971
|
+
+devTag+'</g>';
|
|
7972
|
+
}
|
|
7973
|
+
const colLabels='<text class="icol-label" x="'+INSPECT_COL.identity+'" y="22" text-anchor="middle">Identities</text>'
|
|
7974
|
+
+'<text class="icol-label" x="'+INSPECT_COL.tool+'" y="22" text-anchor="middle">Tools</text>'
|
|
7975
|
+
+'<text class="icol-label" x="'+INSPECT_COL.backend+'" y="22" text-anchor="middle">Backends</text>';
|
|
7976
|
+
svg.innerHTML='<g id="inspect-root">'+colLabels+edgesSvg+nodesSvg+'</g>';
|
|
7977
|
+
applyInspectTransform();
|
|
7978
|
+
renderInspectLegend();
|
|
7979
|
+
updateInspectFlowStat();
|
|
7980
|
+
// Re-assert selection highlight if a node is still present.
|
|
7981
|
+
if(inspectSel) highlightInspect(inspectSel);
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
function updateInspectFlowStat(){
|
|
7985
|
+
const s=document.getElementById('inspect-flow-stat'); if(!s||!inspectData) return;
|
|
7986
|
+
const dev=(inspectData.edges||[]).reduce((a,e)=>a+(e.deviation||0)+(e.denied||0),0);
|
|
7987
|
+
s.textContent=(inspectData.total||0)+' calls · '+(inspectData.nodes||[]).length+' nodes'+(dev>0?(' · '+dev+' deviations'):'');
|
|
7988
|
+
}
|
|
7989
|
+
|
|
7990
|
+
function renderInspectLegend(){
|
|
7991
|
+
const el=document.getElementById('inspect-graph-legend'); if(!el) return;
|
|
7992
|
+
el.innerHTML='<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">'
|
|
7993
|
+
+'<span><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--info);margin-right:4px;"></span>identity</span>'
|
|
7994
|
+
+'<span><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-right:4px;"></span>tool</span>'
|
|
7995
|
+
+'<span><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--success);margin-right:4px;"></span>backend</span>'
|
|
7996
|
+
+'<span style="color:var(--success);">— allowed</span><span style="color:var(--warning);">— deviation</span><span style="color:var(--danger);">— blocked</span>'
|
|
7997
|
+
+'</div>';
|
|
7998
|
+
}
|
|
7999
|
+
|
|
8000
|
+
function applyInspectTransform(){
|
|
8001
|
+
const root=document.getElementById('inspect-root'); if(!root) return;
|
|
8002
|
+
root.setAttribute('transform','translate('+inspectView.tx+','+inspectView.ty+') scale('+inspectView.scale+')');
|
|
8003
|
+
}
|
|
8004
|
+
function inspectZoom(k){ inspectView.scale=Math.min(3,Math.max(0.3,inspectView.scale*k)); applyInspectTransform(); }
|
|
8005
|
+
function inspectResetView(){ inspectView.tx=0; inspectView.ty=0; inspectView.scale=1; applyInspectTransform(); }
|
|
8006
|
+
|
|
8007
|
+
function highlightInspect(id){
|
|
8008
|
+
const svg=document.getElementById('inspect-graph-svg'); if(!svg) return;
|
|
8009
|
+
if(!id){ svg.querySelectorAll('.dim').forEach(n=>n.classList.remove('dim')); return; }
|
|
8010
|
+
const neighbours=new Set([id]);
|
|
8011
|
+
(inspectData.edges||[]).forEach(e=>{ if(e.from===id) neighbours.add(e.to); if(e.to===id) neighbours.add(e.from); });
|
|
8012
|
+
svg.querySelectorAll('.inode-g').forEach(g=>{ g.classList.toggle('dim', !neighbours.has(g.getAttribute('data-id'))); });
|
|
8013
|
+
svg.querySelectorAll('.iedge').forEach((p,i)=>{ const e=(inspectData.edges||[])[p.getAttribute('data-edge')]; p.classList.toggle('dim', !(e&&(e.from===id||e.to===id))); });
|
|
8014
|
+
}
|
|
8015
|
+
|
|
8016
|
+
function inspectNodeById(id){ return (inspectData.nodes||[]).find(n=>n.id===id); }
|
|
8017
|
+
|
|
8018
|
+
// G1 drill-down: map a flow node id (kind:label) to an events filter.
|
|
8019
|
+
function inspectNodeFilter(id){
|
|
8020
|
+
const i=id.indexOf(':'); const kind=id.slice(0,i), val=id.slice(i+1);
|
|
8021
|
+
if(kind==='identity') return { principal: val };
|
|
8022
|
+
if(kind==='tool') return { tool: val };
|
|
8023
|
+
if(kind==='backend') return { backend: val };
|
|
8024
|
+
return {};
|
|
8025
|
+
}
|
|
8026
|
+
function inspectArgShapeStr(sh){
|
|
8027
|
+
const e=Object.entries(sh||{}); if(!e.length) return '<span class="muted">—</span>';
|
|
8028
|
+
return e.map(([k,v])=>esc(k)+'=<b>'+esc(String(v))+'</b>').join(' · ');
|
|
8029
|
+
}
|
|
8030
|
+
// Fetch + render the real calls behind a node/edge selection (the "look into
|
|
8031
|
+
// the calls" drill). Filters: {principal?, tool?, backend?}. All cells escaped.
|
|
8032
|
+
async function inspectRenderDrill(filters, win){
|
|
8033
|
+
const el=document.getElementById('inspect-drill'); if(!el) return;
|
|
8034
|
+
el.innerHTML='<div class="muted t-xs">Loading calls…</div>';
|
|
8035
|
+
try{
|
|
8036
|
+
const qs=new URLSearchParams(); for(const k in filters){ if(filters[k]) qs.set(k, filters[k]); }
|
|
8037
|
+
qs.set('from', new Date(Date.now()-(inspectWinMs(win))).toISOString()); qs.set('limit','30');
|
|
8038
|
+
const r=await fetch('/api/inspect/events?'+qs.toString());
|
|
8039
|
+
if(!r.ok){ el.innerHTML='<div class="muted t-xs">'+(r.status===403?'No permission.':'Unavailable.')+'</div>'; return; }
|
|
8040
|
+
const evs=(await r.json()).events||[];
|
|
8041
|
+
if(!evs.length){ el.innerHTML='<div class="muted t-xs">No calls in window.</div>'; return; }
|
|
8042
|
+
el.innerHTML='<div class="muted t-xs" style="margin:10px 0 4px;">Recent calls ('+evs.length+')</div>'
|
|
8043
|
+
+evs.map(o=>{ const dec=o.decision==='blocked'?'<span class="stchip bad">blocked</span>':o.decision==='would-block'?'<span class="stchip warn">would-block</span>':(o.outcome==='error'?'<span class="stchip warn">error</span>':'<span class="stchip ok">ok</span>');
|
|
8044
|
+
const backend=o.service||o.source||o.namespace||'—';
|
|
8045
|
+
return '<div style="padding:6px 0;border-top:1px solid var(--border);">'
|
|
8046
|
+
+'<div style="display:flex;justify-content:space-between;gap:8px;"><span class="t-xs">'+esc(o.principal)+' <span class="muted">→</span> '+esc(o.tool)+'</span>'+dec+'</div>'
|
|
8047
|
+
+'<div class="muted t-xs" style="margin-top:2px;">'+esc((o.ts||'').replace('T',' ').replace(/\..*/,''))+' · '+esc(backend)+'</div>'
|
|
8048
|
+
+'<div class="t-xs" style="margin-top:2px;">'+inspectArgShapeStr(o.argShape)+'</div>'
|
|
8049
|
+
+'</div>'; }).join('');
|
|
8050
|
+
}catch(e){ el.innerHTML='<div class="muted t-xs">Unavailable.</div>'; }
|
|
8051
|
+
}
|
|
8052
|
+
function inspectWinMs(win){ const w=win||((document.getElementById('inspect-window')||{}).value)||'1h'; return {'5m':3e5,'1h':36e5,'24h':864e5}[w]||36e5; }
|
|
8053
|
+
|
|
8054
|
+
function selectInspectNode(id){
|
|
8055
|
+
inspectSel=id; highlightInspect(id);
|
|
8056
|
+
const n=inspectNodeById(id); if(!n) return;
|
|
8057
|
+
const ins=(inspectData.edges||[]).filter(e=>e.to===id);
|
|
8058
|
+
const outs=(inspectData.edges||[]).filter(e=>e.from===id);
|
|
8059
|
+
const rows=(list,dir)=>list.map(e=>{ const other=dir==='out'?e.to:e.from; const nm=(inspectNodeById(other)||{}).label||other; const p=inspectEdgePosture(e);
|
|
8060
|
+
return '<div style="display:flex;justify-content:space-between;gap:8px;padding:2px 0;"><span style="color:'+inspectPostureColor(p)+'">'+(dir==='out'?'→ ':'← ')+esc(nm)+'</span><span class="muted">'+e.count+'</span></div>'; }).join('')||'<span class="muted">none</span>';
|
|
8061
|
+
const body=document.getElementById('inspect-inspector-body'), empty=document.getElementById('inspect-inspector-empty');
|
|
8062
|
+
empty.style.display='none'; body.style.display='block';
|
|
8063
|
+
body.innerHTML='<div style="font-size:var(--fs-lg);font-weight:600;margin-bottom:2px;">'+esc(n.label)+'</div>'
|
|
8064
|
+
+'<div class="muted t-xs" style="margin-bottom:10px;text-transform:uppercase;letter-spacing:.06em;">'+n.kind+'</div>'
|
|
8065
|
+
+'<dl class="inspect-kv"><dt>calls</dt><dd>'+n.calls+'</dd><dt>errors</dt><dd>'+n.errors+'</dd><dt>deviations</dt><dd>'+n.deviations+'</dd></dl>'
|
|
8066
|
+
+'<div class="muted t-xs" style="margin:10px 0 4px;">Incoming</div>'+rows(ins,'in')
|
|
8067
|
+
+'<div class="muted t-xs" style="margin:10px 0 4px;">Outgoing</div>'+rows(outs,'out')
|
|
8068
|
+
+'<div id="inspect-drill"></div>';
|
|
8069
|
+
inspectRenderDrill(inspectNodeFilter(id));
|
|
8070
|
+
}
|
|
8071
|
+
|
|
8072
|
+
function selectInspectEdge(idx){
|
|
8073
|
+
const e=(inspectData.edges||[])[idx]; if(!e) return;
|
|
8074
|
+
inspectSel=null; highlightInspect(null);
|
|
8075
|
+
const from=(inspectNodeById(e.from)||{}).label||e.from, to=(inspectNodeById(e.to)||{}).label||e.to;
|
|
8076
|
+
const body=document.getElementById('inspect-inspector-body'), empty=document.getElementById('inspect-inspector-empty');
|
|
8077
|
+
empty.style.display='none'; body.style.display='block';
|
|
8078
|
+
body.innerHTML='<div style="font-size:var(--fs-lg);font-weight:600;margin-bottom:2px;">'+esc(from)+' → '+esc(to)+'</div>'
|
|
8079
|
+
+'<div class="muted t-xs" style="margin-bottom:10px;">flow edge</div>'
|
|
8080
|
+
+'<dl class="inspect-kv"><dt>calls</dt><dd>'+e.count+'</dd><dt>allowed</dt><dd>'+e.allow+'</dd><dt>deviation</dt><dd>'+e.deviation+'</dd><dt>blocked</dt><dd>'+e.denied+'</dd></dl>'
|
|
8081
|
+
+'<div id="inspect-drill"></div>';
|
|
8082
|
+
inspectRenderDrill({ ...inspectNodeFilter(e.from), ...inspectNodeFilter(e.to) });
|
|
8083
|
+
}
|
|
8084
|
+
|
|
8085
|
+
function wireInspectGraph(){
|
|
8086
|
+
const svg=document.getElementById('inspect-graph-svg'); const host=document.getElementById('inspect-graph-host'); if(!svg||!host) return;
|
|
8087
|
+
// click select (node or edge), background click clears
|
|
8088
|
+
svg.addEventListener('click',(ev)=>{
|
|
8089
|
+
const g=ev.target.closest('.inode-g'); if(g){ selectInspectNode(g.getAttribute('data-id')); return; }
|
|
8090
|
+
const p=ev.target.closest('.iedge'); if(p){ selectInspectEdge(p.getAttribute('data-edge')); return; }
|
|
8091
|
+
inspectSel=null; highlightInspect(null);
|
|
8092
|
+
document.getElementById('inspect-inspector-body').style.display='none';
|
|
8093
|
+
document.getElementById('inspect-inspector-empty').style.display='block';
|
|
8094
|
+
});
|
|
8095
|
+
// wheel zoom
|
|
8096
|
+
svg.addEventListener('wheel',(ev)=>{ ev.preventDefault(); inspectZoom(ev.deltaY<0?1.12:1/1.12); },{passive:false});
|
|
8097
|
+
// drag to pan
|
|
8098
|
+
let panning=false, sx=0, sy=0, ox=0, oy=0;
|
|
8099
|
+
svg.addEventListener('mousedown',(ev)=>{ if(ev.target.closest('.inode-g')) return; panning=true; sx=ev.clientX; sy=ev.clientY; ox=inspectView.tx; oy=inspectView.ty; svg.style.cursor='grabbing'; });
|
|
8100
|
+
window.addEventListener('mousemove',(ev)=>{ if(!panning) return; inspectView.tx=ox+(ev.clientX-sx); inspectView.ty=oy+(ev.clientY-sy); applyInspectTransform(); });
|
|
8101
|
+
window.addEventListener('mouseup',()=>{ panning=false; svg.style.cursor='grab'; });
|
|
8102
|
+
// keyboard: Enter inspects focused node, Esc clears
|
|
8103
|
+
svg.addEventListener('keydown',(ev)=>{
|
|
8104
|
+
if(ev.key==='Escape'){ inspectSel=null; highlightInspect(null); document.getElementById('inspect-inspector-body').style.display='none'; document.getElementById('inspect-inspector-empty').style.display='block'; }
|
|
8105
|
+
if((ev.key==='Enter'||ev.key===' ')){ const el=document.activeElement; if(el&&el.classList&&el.classList.contains('inode-g')){ ev.preventDefault(); selectInspectNode(el.getAttribute('data-id')); } }
|
|
8106
|
+
});
|
|
8107
|
+
}
|
|
8108
|
+
|
|
7367
8109
|
(async()=>{syncThemeToggle();initNav();await loadTypes();await refresh();await loadInfo();await updateHealthStat();await buildNotifications();setInterval(refresh,15000);setInterval(updateHealthStat,15000);setInterval(buildNotifications,15000);})();
|
|
7368
8110
|
</script>
|
|
7369
8111
|
|