seisin 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/package.json +2 -2
- package/src/canary.js +10 -0
- package/src/cli.js +2 -1
- package/src/commands/run.js +8 -0
- package/src/commands/ui.js +46 -10
- package/src/help.js +6 -4
- package/src/inspect/inject.js +16 -5
- package/src/keys.js +15 -16
- package/src/runs.js +298 -0
- package/src/sandboxes/command.js +3 -0
- package/src/sandboxes/index.js +5 -1
- package/src/sandboxes/srt.js +3 -0
- package/src/sandboxes/tartaros.js +3 -0
- package/src/serve.js +72 -11
- package/src/uilink.js +10 -3
- package/ui/index.html +289 -18
package/ui/index.html
CHANGED
|
@@ -447,6 +447,28 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
447
447
|
.preview input{width:100%}
|
|
448
448
|
.places li{flex-direction:column;gap:2px}
|
|
449
449
|
}
|
|
450
|
+
|
|
451
|
+
/* ── a directory of runs (seisin ui --runs) ─────────────────────────────── */
|
|
452
|
+
/* `hidden` has to win over the display these set, or the runs view keeps the policy's nav and cards. */
|
|
453
|
+
.side a[hidden],.side .cap[hidden],.cards[hidden],.range[hidden]{display:none}
|
|
454
|
+
.banner.run{border-left:3px solid var(--blue)}
|
|
455
|
+
.banner.run .meta{color:var(--mid);font-size:12px;margin-top:4px}
|
|
456
|
+
.runs-filters{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center}
|
|
457
|
+
.runs-filters label{display:flex;gap:6px;align-items:center;font-size:12px;color:var(--mid)}
|
|
458
|
+
.runs-filters select,.runs-filters input{font:inherit;font-size:12px;padding:4px 6px;border:1px solid var(--line);
|
|
459
|
+
border-radius:6px;background:var(--panel);color:var(--fg);max-width:220px}
|
|
460
|
+
table.runs{width:100%;border-collapse:collapse;font-size:12px}
|
|
461
|
+
table.runs th{text-align:left;font-weight:600;color:var(--dim);padding:6px 10px;border-bottom:1px solid var(--line);white-space:nowrap}
|
|
462
|
+
table.runs td{padding:6px 10px;border-bottom:1px solid var(--line2);vertical-align:top}
|
|
463
|
+
table.runs td.num,table.runs th.num{text-align:right;font-variant-numeric:tabular-nums}
|
|
464
|
+
table.runs td code{font-family:var(--mono);font-size:11px;word-break:break-all}
|
|
465
|
+
table.runs tr.sys td{color:var(--dim)}
|
|
466
|
+
.oc{font-weight:600;font-size:11px;padding:1px 6px;border-radius:9px;border:1px solid var(--line)}
|
|
467
|
+
.oc.clean{color:var(--green);border-color:var(--green)}
|
|
468
|
+
.oc.denied{color:var(--amber);border-color:var(--amber)}
|
|
469
|
+
.oc.refused,.oc.unread{color:var(--fg);background:var(--amber-bg);border-color:var(--amber)}
|
|
470
|
+
.ro-ask{font-size:12px;color:var(--mid);max-width:320px}
|
|
471
|
+
.ro-ask code{font-family:var(--mono)}
|
|
450
472
|
</style>
|
|
451
473
|
</head>
|
|
452
474
|
<body>
|
|
@@ -462,7 +484,13 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
462
484
|
</div>
|
|
463
485
|
</div>
|
|
464
486
|
<div class="list">
|
|
465
|
-
|
|
487
|
+
<!-- Over a directory of runs (seisin ui --runs) the first item is every run, and the
|
|
488
|
+
rest is the one run that is open; with none open, only this one is shown. -->
|
|
489
|
+
<div id="nav-runs" hidden>
|
|
490
|
+
<div class="cap">Runs</div>
|
|
491
|
+
<a href="./" id="nav-all" title="Every run in the directory, read only"><i>☰</i>All runs<span class="pin" id="runs-pin"></span></a>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="cap" id="nav-cap">Policy</div>
|
|
466
494
|
<a class="on" href="#requests" id="nav-req" aria-current="page" title="Requests: access an agent asked for and cannot have"><i>⚖</i>Waiting on you<span class="pin" id="req-pin"></span></a>
|
|
467
495
|
<a href="#denied" title="Every denial in the window: what an agent tried and the OS denied"><i>⊘</i>Denied<span class="pin" id="dn-pin"></span></a>
|
|
468
496
|
<a href="#walls" title="A wall: something a role was denied twice or more and would still be denied today"><i>▦</i>Walls<span class="pin" id="wl-pin"></span></a>
|
|
@@ -490,6 +518,8 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
490
518
|
<code>seisin ui</code> in it.
|
|
491
519
|
</div>
|
|
492
520
|
<div class="banner off" id="off-banner" role="alert" hidden></div>
|
|
521
|
+
<!-- One run out of a directory of them: who it was, and that nothing here is granted. -->
|
|
522
|
+
<div class="banner run" id="run-banner" hidden></div>
|
|
493
523
|
|
|
494
524
|
<div class="head">
|
|
495
525
|
<div>
|
|
@@ -533,6 +563,34 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
533
563
|
<h2>spent retrying</h2><div class="n" id="c-retry">0</div><div class="s" id="c-retry-s">calls on known walls</div></a>
|
|
534
564
|
</div>
|
|
535
565
|
|
|
566
|
+
<!-- Every run in the directory (seisin ui --runs): filtered, summed, newest first. -->
|
|
567
|
+
<section data-view="runs" hidden>
|
|
568
|
+
<div class="box">
|
|
569
|
+
<header>
|
|
570
|
+
<h2>Filter</h2>
|
|
571
|
+
<span class="note" id="rn-count"></span>
|
|
572
|
+
</header>
|
|
573
|
+
<div class="pad runs-filters" id="rn-filters">
|
|
574
|
+
<label>team <select data-f="team"></select></label>
|
|
575
|
+
<label>role <select data-f="role"></select></label>
|
|
576
|
+
<label>work item <select data-f="work_item"></select></label>
|
|
577
|
+
<label>status <select data-f="status"></select></label>
|
|
578
|
+
<label>outcome <select data-f="outcome"></select></label>
|
|
579
|
+
<label>search <input type="search" id="rn-q" spellcheck="false" autocomplete="off" placeholder="run id, work item, repo"></label>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
<div class="cards" id="rn-kpis"></div>
|
|
583
|
+
<div class="box">
|
|
584
|
+
<header><h2>Denied across these runs</h2><span class="note">the same denial in many runs is one line · the OS's own housekeeping last</span></header>
|
|
585
|
+
<div class="scrollx"><table class="runs" id="rn-causes"></table></div>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="box">
|
|
588
|
+
<header><h2>Runs</h2><span class="note">newest first · open one to see its policy, its denials and its log</span></header>
|
|
589
|
+
<div class="scrollx"><table class="runs" id="rn-list"></table></div>
|
|
590
|
+
<p class="blank quiet" id="rn-more" hidden></p>
|
|
591
|
+
</div>
|
|
592
|
+
</section>
|
|
593
|
+
|
|
536
594
|
<section data-view="roles">
|
|
537
595
|
|
|
538
596
|
<div class="box">
|
|
@@ -587,7 +645,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
587
645
|
<section data-view="requests" hidden>
|
|
588
646
|
<div class="box" id="requests">
|
|
589
647
|
<header>
|
|
590
|
-
<h2>Pending decisions</h2>
|
|
648
|
+
<h2 id="req-h">Pending decisions</h2>
|
|
591
649
|
<span class="note" id="req-note">granting edits seisin.toml · here or in the terminal, never through MCP</span>
|
|
592
650
|
<span class="bulk" id="bulk" hidden>
|
|
593
651
|
<input id="why-all" placeholder="reason, for all of them (required)" aria-label="Reason for declining all">
|
|
@@ -656,7 +714,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
656
714
|
<div class="foot">
|
|
657
715
|
<b id="foot-mode">Preview.</b> <span id="foot-text">Opened as a file, this page edits an example policy in memory and
|
|
658
716
|
shows what would be generated. Nothing is saved; run <b>seisin ui</b> in a repository to see yours.</span><br>
|
|
659
|
-
The JSON
|
|
717
|
+
The JSON is <b>@anthropic-ai/sandbox-runtime</b>'s settings schema, which the test suite exercises against the real binary.
|
|
660
718
|
</div>
|
|
661
719
|
</main>
|
|
662
720
|
</div>
|
|
@@ -1245,7 +1303,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1245
1303
|
aria: "instruction files, for every role",
|
|
1246
1304
|
click: function(){ propose({ protect: !prot }, "ctl-sw-protect", "instructions", "pw"); }
|
|
1247
1305
|
}));
|
|
1248
|
-
if (!r) { drawPreview(); return; }
|
|
1306
|
+
if (!r) { drawPreview(); if (READONLY) document.querySelectorAll("#pw button").forEach(function(b){ b.disabled = true; }); return; }
|
|
1249
1307
|
["ide", "instructions"].forEach(function(f){
|
|
1250
1308
|
var mine = (r.controlFiles || []).indexOf(f) !== -1;
|
|
1251
1309
|
var idle = f === "instructions" && !prot;
|
|
@@ -1267,6 +1325,9 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1267
1325
|
}));
|
|
1268
1326
|
});
|
|
1269
1327
|
drawPreview();
|
|
1328
|
+
if (READONLY) document.querySelectorAll("#pw button, #ctl button").forEach(function(b){
|
|
1329
|
+
b.disabled = true; b.title = "read only: this run's policy was lent by its launcher";
|
|
1330
|
+
});
|
|
1270
1331
|
}
|
|
1271
1332
|
|
|
1272
1333
|
async function callControls(body){
|
|
@@ -1687,7 +1748,8 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1687
1748
|
}
|
|
1688
1749
|
|
|
1689
1750
|
set("k-wait", waiting ? "live" : "calm", waiting,
|
|
1690
|
-
waiting ? (waiting === 1 ? "
|
|
1751
|
+
READONLY ? (waiting ? (waiting === 1 ? "request, asked for elsewhere" : "requests, asked for elsewhere") : "nothing asked for")
|
|
1752
|
+
: waiting ? (waiting === 1 ? "decision" : "decisions") : "nothing to decide");
|
|
1691
1753
|
set("k-open", off ? "alarm" : "calm", off,
|
|
1692
1754
|
off ? (off === 1 ? "role running unconfined" : "roles running unconfined") : "every role confined");
|
|
1693
1755
|
set("k-ref", f.total ? "live" : "calm", f.total.toLocaleString(),
|
|
@@ -1759,6 +1821,12 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1759
1821
|
return narrowable(r) && f && f.checked ? { scope: "file", grant: r.target } : { scope: "folder", grant: r.grant };
|
|
1760
1822
|
}
|
|
1761
1823
|
|
|
1824
|
+
/** Read only, where a request goes instead: the launcher that lent the run its territory. */
|
|
1825
|
+
function askedWhere(){
|
|
1826
|
+
var who = RUNINFO && RUNINFO.launcher;
|
|
1827
|
+
return who ? "Not granted here — ask for it in <b>" + esc(who) + "</b>:" : "Not granted here — ask for it where the run was launched:";
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1762
1830
|
function drawReqs(){
|
|
1763
1831
|
var host = document.getElementById("reqs");
|
|
1764
1832
|
pill("req-pin", REQS.length, REQS.length === 1 ? "request" : "requests");
|
|
@@ -1776,17 +1844,19 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1776
1844
|
return '<div class="req' + (r.stale ? ' stale' : '') + '">' +
|
|
1777
1845
|
'<div class="who">' +
|
|
1778
1846
|
"<div><b>" + esc(r.role) + "</b> wants to " + esc(r.action) + ' <span class="path">' + esc(r.target) + "</span></div>" +
|
|
1779
|
-
scopeOf(r) + handoffOf(r) + scopeChoice(r, i) +
|
|
1847
|
+
scopeOf(r) + handoffOf(r) + (READONLY ? "" : scopeChoice(r, i)) +
|
|
1780
1848
|
'<div class="sub"><span class="asked">' + times + "</span>" +
|
|
1781
1849
|
(w.text ? ' · last <span title="' + escA(w.title) + '">' + esc(w.text) + "</span>" : "") + stale + "</div>" +
|
|
1782
1850
|
'<div class="err" id="err-' + i + '" role="alert" hidden></div>' +
|
|
1783
1851
|
"</div>" +
|
|
1784
|
-
|
|
1852
|
+
(READONLY
|
|
1853
|
+
? '<div class="act ro-ask">' + askedWhere() + ' <code>' + esc(r.grant) + "</code></div>"
|
|
1854
|
+
: '<div class="act">' +
|
|
1785
1855
|
'<input id="why-' + i + '" placeholder="reason (required)" aria-label="Reason for ' +
|
|
1786
1856
|
escA(r.role) + '’s request" aria-describedby="err-' + i + '">' +
|
|
1787
1857
|
'<button class="btn primary" data-n="' + i + '" data-k="' + escA(r.key) + '" data-d="granted" type="button" data-write>Grant</button>' +
|
|
1788
1858
|
'<button class="btn" data-n="' + i + '" data-k="' + escA(r.key) + '" data-d="declined" type="button" data-write>Decline</button>' +
|
|
1789
|
-
"</div>" +
|
|
1859
|
+
"</div>") +
|
|
1790
1860
|
"</div>";
|
|
1791
1861
|
}).join("");
|
|
1792
1862
|
|
|
@@ -1829,7 +1899,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1829
1899
|
var bulk = document.getElementById("bulk");
|
|
1830
1900
|
var btn = document.getElementById("decline-all");
|
|
1831
1901
|
var why = document.getElementById("why-all");
|
|
1832
|
-
bulk.hidden = REQS.length < 2;
|
|
1902
|
+
bulk.hidden = REQS.length < 2 || READONLY;
|
|
1833
1903
|
btn.disabled = false;
|
|
1834
1904
|
var rest = "Decline all (" + REQS.length + ")";
|
|
1835
1905
|
disarm(btn, rest);
|
|
@@ -1979,15 +2049,29 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1979
2049
|
* server, and it is moved into this tab's storage and wiped from the address
|
|
1980
2050
|
* bar and the history entry at once.
|
|
1981
2051
|
*/
|
|
2052
|
+
/**
|
|
2053
|
+
* Over a directory of runs, the run that is open: `&run=<id>` in the link a
|
|
2054
|
+
* launcher hands out (beside the token, in the fragment), kept in the query
|
|
2055
|
+
* once the fragment is wiped, so a reload stays on it.
|
|
2056
|
+
*/
|
|
2057
|
+
var RUN = (function(){
|
|
2058
|
+
var h = /(?:^#|&)run=([A-Za-z0-9][A-Za-z0-9._-]{0,127})(?:&|$)/.exec(location.hash || "");
|
|
2059
|
+
var q = new URLSearchParams(location.search).get("run");
|
|
2060
|
+
var id = h ? h[1] : q;
|
|
2061
|
+
return id && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) ? id : null;
|
|
2062
|
+
})();
|
|
1982
2063
|
var TOKEN = (function(){
|
|
1983
2064
|
var m = /(?:^#|&)t=([a-f0-9]+)/.exec(location.hash || "");
|
|
1984
2065
|
try {
|
|
1985
2066
|
if (m) { sessionStorage.setItem("seisin-token", m[1]); }
|
|
1986
2067
|
var t = m ? m[1] : (sessionStorage.getItem("seisin-token") || "");
|
|
1987
|
-
if (m) history.replaceState(null, "", location.pathname + location.search);
|
|
2068
|
+
if (m) history.replaceState(null, "", location.pathname + (RUN ? "?run=" + encodeURIComponent(RUN) : location.search));
|
|
1988
2069
|
return t;
|
|
1989
2070
|
} catch (_) { return m ? m[1] : ""; }
|
|
1990
2071
|
})();
|
|
2072
|
+
/** "policy" — one seisin.toml, the console as it always was — or "runs", read only. */
|
|
2073
|
+
var MODE = "policy";
|
|
2074
|
+
var READONLY = false;
|
|
1991
2075
|
|
|
1992
2076
|
/** What to tell a person when a request to this server fails. */
|
|
1993
2077
|
function whyFailed(e, status){
|
|
@@ -2050,6 +2134,8 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2050
2134
|
}
|
|
2051
2135
|
function stateUrl(){
|
|
2052
2136
|
var s = sinceIso();
|
|
2137
|
+
if (MODE === "runs" && RUN)
|
|
2138
|
+
return "/api/run?run=" + encodeURIComponent(RUN) + (s ? "&since=" + encodeURIComponent(s) : "");
|
|
2053
2139
|
return "/api/state" + (s ? "?since=" + encodeURIComponent(s) : "");
|
|
2054
2140
|
}
|
|
2055
2141
|
function rangeLabel(){
|
|
@@ -2180,7 +2266,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2180
2266
|
if (ASKING[name]) return;
|
|
2181
2267
|
ASKING[name] = true;
|
|
2182
2268
|
try {
|
|
2183
|
-
var r = await fetch("/api/settings?role=" + encodeURIComponent(name),
|
|
2269
|
+
var r = await fetch("/api/settings?role=" + encodeURIComponent(name) + (RUN ? "&run=" + encodeURIComponent(RUN) : ""),
|
|
2184
2270
|
{ cache: "no-store", headers: { "x-seisin-token": TOKEN } });
|
|
2185
2271
|
var out = await r.json();
|
|
2186
2272
|
SETTINGS[name] = r.ok ? { settings: out.settings } : { error: out.error || "could not read the settings" };
|
|
@@ -2198,6 +2284,12 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2198
2284
|
try {
|
|
2199
2285
|
// Opened as a file there is no server to ask; the fetch only printed a CORS error.
|
|
2200
2286
|
if (location.protocol === "file:") throw new Error("no server");
|
|
2287
|
+
var info = await fetch("/api/info", { cache: "no-store", headers: { "x-seisin-token": TOKEN } });
|
|
2288
|
+
if (info.ok) {
|
|
2289
|
+
var mode = (await info.json()).mode;
|
|
2290
|
+
if (mode === "runs") { MODE = "runs"; READONLY = true; }
|
|
2291
|
+
}
|
|
2292
|
+
if (MODE === "runs" && !RUN) { await bootRuns(); return; }
|
|
2201
2293
|
got = await fetchState(true);
|
|
2202
2294
|
// A live server that refuses this tab's token: `seisin ui` was restarted
|
|
2203
2295
|
// and minted a new one. Falling back to the example here showed a policy
|
|
@@ -2236,6 +2328,10 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2236
2328
|
LIVE = true;
|
|
2237
2329
|
applyState(s);
|
|
2238
2330
|
LIVE_LABEL = s.config.replace(/^.*\//, "") + " · live";
|
|
2331
|
+
if (MODE === "runs") {
|
|
2332
|
+
RUNINFO = s.run || null; LIVE_LABEL = "run " + RUN + " · read only"; drawRunBanner();
|
|
2333
|
+
document.getElementById("req-note").textContent = "read only · asked for where the run was launched, not here";
|
|
2334
|
+
}
|
|
2239
2335
|
label.textContent = LIVE_LABEL;
|
|
2240
2336
|
label.style.color = "var(--green)";
|
|
2241
2337
|
// Live, the what-if button goes for good: "run every role unconfined" against a real repo is
|
|
@@ -2246,10 +2342,19 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2246
2342
|
document.getElementById("kill").hidden = true;
|
|
2247
2343
|
// And so does the card that counts what the button would do: live it could only say 0.
|
|
2248
2344
|
document.getElementById("k-open").style.display = "none";
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2345
|
+
// Read only nothing waits on the person here: what the run asked for is asked for where it was launched.
|
|
2346
|
+
if (READONLY) {
|
|
2347
|
+
var nav = document.getElementById("nav-req");
|
|
2348
|
+
nav.childNodes[1].textContent = "Asked for";
|
|
2349
|
+
document.querySelector("#k-wait h2").textContent = "asked for";
|
|
2350
|
+
document.getElementById("req-h").textContent = "What this run asked for";
|
|
2351
|
+
}
|
|
2352
|
+
document.getElementById("toml-note").textContent = READONLY ? "this run's policy, as its launcher archived it" : "the file on disk, as it stands";
|
|
2353
|
+
document.getElementById("foot-mode").textContent = READONLY ? "Live, read only." : "Live.";
|
|
2354
|
+
document.getElementById("foot-text").innerHTML = READONLY
|
|
2355
|
+
? "This page reads <b>" + esc(s.config) + "</b>, one archived run's policy, and its log. Nothing is written from here: " +
|
|
2356
|
+
"the run's territory was lent by whoever launched it, and access is asked for there."
|
|
2357
|
+
: "This page reads <b>" + esc(s.config) + "</b> and the log on disk. Two actions write, each in place and stamped " +
|
|
2253
2358
|
"with its reason: a decision on a request, and saving a control-files change after its preview. Territories " +
|
|
2254
2359
|
"and keys are changed in seisin.toml itself.";
|
|
2255
2360
|
draw();
|
|
@@ -2257,6 +2362,168 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2257
2362
|
poll();
|
|
2258
2363
|
}
|
|
2259
2364
|
|
|
2365
|
+
/* ── a directory of runs ──────────────────────────────────────────────────
|
|
2366
|
+
* `seisin ui --runs`: every run, filtered and summed, read only. One run opens
|
|
2367
|
+
* as the console always looked — its policy, its denials, its log — with no
|
|
2368
|
+
* grant on it. */
|
|
2369
|
+
var RUNINFO = null;
|
|
2370
|
+
var RFILTER = (function(){
|
|
2371
|
+
var q = new URLSearchParams(location.search), out = {};
|
|
2372
|
+
["team", "role", "work_item", "status", "outcome", "q"].forEach(function(k){ var v = q.get(k); if (v) out[k] = v; });
|
|
2373
|
+
return out;
|
|
2374
|
+
})();
|
|
2375
|
+
var RLAST = { url: null, etag: null, v: null };
|
|
2376
|
+
|
|
2377
|
+
function runsUrl(){
|
|
2378
|
+
var q = new URLSearchParams();
|
|
2379
|
+
Object.keys(RFILTER).forEach(function(k){ if (RFILTER[k]) q.set(k, RFILTER[k]); });
|
|
2380
|
+
var s = q.toString();
|
|
2381
|
+
return "/api/runs" + (s ? "?" + s : "");
|
|
2382
|
+
}
|
|
2383
|
+
async function fetchRuns(fresh){
|
|
2384
|
+
var url = runsUrl();
|
|
2385
|
+
var h = { "x-seisin-token": TOKEN };
|
|
2386
|
+
if (!fresh && RLAST.etag && RLAST.url === url) h["if-none-match"] = RLAST.etag;
|
|
2387
|
+
var r = await fetch(url, { cache: "no-store", headers: h });
|
|
2388
|
+
if (r.status === 304) return { same: true };
|
|
2389
|
+
if (!r.ok) return { status: r.status };
|
|
2390
|
+
var v = await r.json();
|
|
2391
|
+
RLAST = { url: url, etag: r.headers.get("etag"), v: v };
|
|
2392
|
+
return { v: v };
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function runMeta(r){
|
|
2396
|
+
var bits = [];
|
|
2397
|
+
if (r.team) bits.push("team <b>" + esc(r.team) + "</b>");
|
|
2398
|
+
if (r.role) bits.push("role <b>" + esc(r.role) + "</b>");
|
|
2399
|
+
if (r.work_item) bits.push("<b>" + esc(r.work_item) + "</b>");
|
|
2400
|
+
if (r.repo) bits.push("repo " + esc(r.repo));
|
|
2401
|
+
if (r.status) bits.push("status " + esc(r.status));
|
|
2402
|
+
var w = when(r.started_at || "");
|
|
2403
|
+
if (w.text) bits.push('started <span title="' + escA(w.title) + '">' + esc(w.text) + "</span>");
|
|
2404
|
+
return bits.join(" · ");
|
|
2405
|
+
}
|
|
2406
|
+
function outcomeTag(o){
|
|
2407
|
+
var says = { clean: "clean", denied: "denied", refused: "canary failed", unread: "policy unreadable" };
|
|
2408
|
+
return '<span class="oc ' + escA(o) + '">' + esc(says[o] || o) + "</span>";
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
function drawRunBanner(){
|
|
2412
|
+
var b = document.getElementById("run-banner");
|
|
2413
|
+
var r = RUNINFO || { id: RUN };
|
|
2414
|
+
b.innerHTML = "<b>Run " + esc(r.run_id || r.id) + "</b> " + outcomeTag(r.outcome || "") +
|
|
2415
|
+
' · <a href="./">all runs</a>' +
|
|
2416
|
+
'<div class="meta">' + (runMeta(r) || "no run.json: only what its policy and log say") +
|
|
2417
|
+
" · read only: nothing is granted from here</div>";
|
|
2418
|
+
b.hidden = false;
|
|
2419
|
+
document.getElementById("nav-runs").hidden = false;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function drawRunsView(v){
|
|
2423
|
+
// The filters: every value any run has, with how many runs have it.
|
|
2424
|
+
document.querySelectorAll("#rn-filters select").forEach(function(sel){
|
|
2425
|
+
var f = sel.dataset.f;
|
|
2426
|
+
var opts = (v.facets[f] || []);
|
|
2427
|
+
var cur = RFILTER[f] || "";
|
|
2428
|
+
sel.innerHTML = '<option value="">all</option>' + opts.map(function(o){
|
|
2429
|
+
return '<option value="' + escA(o.value) + '"' + (o.value === cur ? " selected" : "") + ">" + esc(o.value) + " (" + o.runs + ")</option>";
|
|
2430
|
+
}).join("");
|
|
2431
|
+
sel.disabled = !opts.length;
|
|
2432
|
+
});
|
|
2433
|
+
document.getElementById("rn-count").textContent = v.totals.runs === v.total
|
|
2434
|
+
? plural(v.total, "run") : plural(v.totals.runs, "run") + " of " + v.total;
|
|
2435
|
+
pill("runs-pin", v.total, v.total === 1 ? "run" : "runs");
|
|
2436
|
+
|
|
2437
|
+
var t = v.totals, k = t.byKind || {};
|
|
2438
|
+
function card(cls, title, n, sub){
|
|
2439
|
+
return '<div class="card kpi ' + cls + '"><h2>' + esc(title) + '</h2><div class="n">' + esc(Number(n).toLocaleString()) +
|
|
2440
|
+
'</div><div class="s">' + sub + "</div></div>";
|
|
2441
|
+
}
|
|
2442
|
+
document.getElementById("rn-kpis").innerHTML =
|
|
2443
|
+
card("b", "runs", t.runs, (t.outcome.denied || 0) + " denied something · " + (t.outcome.clean || 0) + " clean") +
|
|
2444
|
+
card(t.outcome.refused ? "a" : "g", "canary failed", t.outcome.refused || 0, "runs that never started") +
|
|
2445
|
+
card(t.denied ? "a" : "g", "denied", t.denied, (k.file || 0).toLocaleString() + " files · " + (k.network || 0).toLocaleString() + " network") +
|
|
2446
|
+
card(t.retried ? "a" : "g", "spent retrying", t.retried, "calls after the first denial of the same thing");
|
|
2447
|
+
|
|
2448
|
+
var causes = v.causes || [];
|
|
2449
|
+
document.getElementById("rn-causes").innerHTML = causes.length
|
|
2450
|
+
? "<thead><tr><th>denied</th><th>what</th><th class=num>runs</th><th class=num>times</th><th>teams</th><th>roles</th></tr></thead><tbody>" +
|
|
2451
|
+
causes.map(function(c){
|
|
2452
|
+
return '<tr class="' + (c.about === "system" ? "sys" : "") + '"><td>' + esc(c.action) + "</td><td><code>" + esc(c.target) +
|
|
2453
|
+
"</code></td><td class=num>" + c.runs + "</td><td class=num>" + c.times + "</td><td>" + esc((c.teams || []).join(", ")) +
|
|
2454
|
+
"</td><td>" + esc((c.roles || []).join(", ")) + "</td></tr>";
|
|
2455
|
+
}).join("") + "</tbody>"
|
|
2456
|
+
: '<tbody><tr><td class="blank quiet">Nothing denied in these runs.</td></tr></tbody>';
|
|
2457
|
+
|
|
2458
|
+
var SHOWN = 300, runs = v.runs || [];
|
|
2459
|
+
document.getElementById("rn-list").innerHTML = runs.length
|
|
2460
|
+
? "<thead><tr><th>run</th><th>started</th><th>team</th><th>role</th><th>work item</th><th>status</th><th>outcome</th>" +
|
|
2461
|
+
"<th class=num>denied</th><th class=num>retried</th><th>hosts denied</th></tr></thead><tbody>" +
|
|
2462
|
+
runs.slice(0, SHOWN).map(function(r){
|
|
2463
|
+
var w = when(r.started_at || "");
|
|
2464
|
+
return "<tr><td><a href=\"./?run=" + encodeURIComponent(r.id) + "#requests\"><code>" + esc(r.run_id || r.id) + "</code></a></td>" +
|
|
2465
|
+
'<td title="' + escA(w.title || "") + '">' + esc(w.text || "") + "</td><td>" + esc(r.team || "") + "</td><td>" + esc(r.role || "") +
|
|
2466
|
+
"</td><td>" + esc(r.work_item || "") + "</td><td>" + esc(r.status || "") + "</td><td>" + outcomeTag(r.outcome) +
|
|
2467
|
+
"</td><td class=num>" + r.denied + "</td><td class=num>" + r.retried + "</td><td>" +
|
|
2468
|
+
(r.hosts || []).slice(0, 3).map(function(h){ return "<code>" + esc(h.target) + "</code>"; }).join(" ") + "</td></tr>";
|
|
2469
|
+
}).join("") + "</tbody>"
|
|
2470
|
+
: '<tbody><tr><td class="blank quiet">' + (v.total ? "No run matches these filters." : "No runs in " + esc(v.dir) + " yet.") + "</td></tr></tbody>";
|
|
2471
|
+
var more = document.getElementById("rn-more");
|
|
2472
|
+
more.hidden = runs.length <= SHOWN;
|
|
2473
|
+
more.textContent = "Showing the newest " + SHOWN + " of " + runs.length + ": narrow the filters for the rest.";
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function setRunFilter(k, val){
|
|
2477
|
+
if (val) RFILTER[k] = val; else delete RFILTER[k];
|
|
2478
|
+
var q = new URLSearchParams();
|
|
2479
|
+
Object.keys(RFILTER).forEach(function(x){ q.set(x, RFILTER[x]); });
|
|
2480
|
+
history.replaceState(null, "", location.pathname + (q.toString() ? "?" + q : ""));
|
|
2481
|
+
fetchRuns(true).then(function(g){ if (g.v) drawRunsView(g.v); }).catch(function(){});
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
async function bootRuns(){
|
|
2485
|
+
var label = document.getElementById("source");
|
|
2486
|
+
var got = await fetchRuns(true);
|
|
2487
|
+
if (!got.v) {
|
|
2488
|
+
label.textContent = got.status === 403 ? "link expired — open the new one" : "the console did not answer";
|
|
2489
|
+
label.style.color = "var(--amber)";
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
LIVE = true;
|
|
2493
|
+
LIVE_LABEL = "runs · read only";
|
|
2494
|
+
label.textContent = LIVE_LABEL;
|
|
2495
|
+
label.style.color = "var(--green)";
|
|
2496
|
+
document.getElementById("nav-runs").hidden = false;
|
|
2497
|
+
document.getElementById("nav-all").classList.add("on");
|
|
2498
|
+
document.getElementById("nav-cap").hidden = true;
|
|
2499
|
+
document.querySelectorAll(".side .list > a[href^='#']").forEach(function(a){ a.hidden = true; });
|
|
2500
|
+
// Inline, not `hidden`: the cards' own display rules outrank the attribute.
|
|
2501
|
+
["kpis", "range", "kill"].forEach(function(id){ var e = document.getElementById(id); if (e) { e.hidden = true; e.style.display = "none"; } });
|
|
2502
|
+
document.getElementById("foot-mode").textContent = "Live, read only.";
|
|
2503
|
+
document.getElementById("foot-text").innerHTML = "This page reads the runs in <b>" + esc(got.v.dir) +
|
|
2504
|
+
"</b>, one folder each, as their launcher archived them. Nothing is written from here.";
|
|
2505
|
+
document.querySelectorAll("#rn-filters select").forEach(function(sel){
|
|
2506
|
+
sel.addEventListener("change", function(){ setRunFilter(sel.dataset.f, sel.value); });
|
|
2507
|
+
});
|
|
2508
|
+
var q = document.getElementById("rn-q");
|
|
2509
|
+
q.value = RFILTER.q || "";
|
|
2510
|
+
var tq = null;
|
|
2511
|
+
q.addEventListener("input", function(){ clearTimeout(tq); tq = setTimeout(function(){ setRunFilter("q", q.value.trim()); }, 250); });
|
|
2512
|
+
drawRunsView(got.v);
|
|
2513
|
+
show("runs");
|
|
2514
|
+
(function again(){
|
|
2515
|
+
setTimeout(async function(){
|
|
2516
|
+
try {
|
|
2517
|
+
var g = await fetchRuns(false);
|
|
2518
|
+
if (g.v) drawRunsView(g.v);
|
|
2519
|
+
if (g.v || g.same) { FAILS = 0; if (OFFLINE) setOffline(null); }
|
|
2520
|
+
else if (++FAILS >= 2) setOffline(g.status === 403 ? "expired" : "down");
|
|
2521
|
+
} catch (_) { if (++FAILS >= 2) setOffline("down"); }
|
|
2522
|
+
again();
|
|
2523
|
+
}, 2000);
|
|
2524
|
+
})();
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2260
2527
|
/** Every two seconds after the last answer, not every two seconds regardless. */
|
|
2261
2528
|
function poll(){
|
|
2262
2529
|
setTimeout(async function(){
|
|
@@ -2269,11 +2536,15 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2269
2536
|
* it has to keep working opened straight off the disk with no server at all. */
|
|
2270
2537
|
function views(){
|
|
2271
2538
|
return {
|
|
2539
|
+
runs: {view:"runs", title:"Runs",
|
|
2540
|
+
sub:"Every run in the directory, read only: what each was denied and what the denials cost. Open one for its policy and its log."},
|
|
2272
2541
|
policy: {view:"roles", title:"Roles",
|
|
2273
2542
|
sub:"Each role's territory — the folders it may write — and the keys it may read. " +
|
|
2274
2543
|
(LIVE ? "Read-only here: the policy is seisin.toml." : "In this example you can edit them; nothing is saved.")},
|
|
2275
|
-
requests: {view:"requests", title:"Waiting on you",
|
|
2276
|
-
sub:
|
|
2544
|
+
requests: {view:"requests", title: READONLY ? "Asked for" : "Waiting on you",
|
|
2545
|
+
sub: READONLY
|
|
2546
|
+
? "Requests: access this run asked for and did not have. Read only: they are asked for where the run was launched."
|
|
2547
|
+
: "Requests: access an agent asked for and cannot have. Granting one adds a line to seisin.toml."},
|
|
2277
2548
|
denied: {view:"denied", title:"Denied", tab:"path",
|
|
2278
2549
|
sub:"Everything an agent tried in the window and the OS denied, grouped by what it was about."},
|
|
2279
2550
|
causes: {view:"denied", title:"Denied", tab:"name",
|
|
@@ -2320,7 +2591,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
2320
2591
|
}
|
|
2321
2592
|
|
|
2322
2593
|
/** The landing view is what is waiting on you, not the policy table. */
|
|
2323
|
-
function route(){ return (location.hash || "#requests").slice(1); }
|
|
2594
|
+
function route(){ return MODE === "runs" && !RUN ? "runs" : (location.hash || "#requests").slice(1); }
|
|
2324
2595
|
window.addEventListener("hashchange", function(){ show(route()); });
|
|
2325
2596
|
show(route());
|
|
2326
2597
|
|