seisin 0.3.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -16
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/commands/init.js +13 -5
- package/src/commands/log.js +11 -4
- package/src/commands/requests.js +26 -18
- package/src/commands/run.js +15 -2
- package/src/commands/ui.js +59 -7
- package/src/config.js +79 -7
- package/src/hook.js +50 -0
- package/src/intake.js +32 -1
- package/src/log.js +194 -16
- package/src/mcp.js +63 -24
- package/src/notify.js +3 -3
- package/src/owners.js +140 -15
- package/src/render.js +13 -2
- package/src/requests.js +90 -13
- package/src/review.js +15 -7
- package/src/serve.js +68 -24
- package/src/surface.js +15 -1
- package/src/uilink.js +55 -0
- package/ui/index.html +30 -12
package/src/uilink.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a running `seisin ui` leaves its live link, so it can be found again.
|
|
3
|
+
*
|
|
4
|
+
* The console's token never travels in an HTTP response — that was the whole
|
|
5
|
+
* point of the redesign in serve.js: a page that could GET `/` used to walk
|
|
6
|
+
* away with it. The cost was that a tab which lost the fragment had nowhere to
|
|
7
|
+
* recover the token from but the terminal that printed it once. When that
|
|
8
|
+
* terminal is gone, so is the link.
|
|
9
|
+
*
|
|
10
|
+
* This is the recovery channel that does not reopen the leak: the link (token
|
|
11
|
+
* and all) is written to a file only this user can read — under `$TMPDIR/snr/`,
|
|
12
|
+
* the same 0700 directory runs already keep their sockets and keys in — and
|
|
13
|
+
* never sent over the wire. `seisin ui` on a busy port reads it and reopens the
|
|
14
|
+
* live link instead of failing; `seisin ui --link` prints it. The token now
|
|
15
|
+
* rests on disk (0600) for the life of the run rather than only in memory, a
|
|
16
|
+
* bounded, local-user exposure, and it is removed when the run exits.
|
|
17
|
+
*
|
|
18
|
+
* The pid is stored so a file left by a crashed run is recognisable: a link
|
|
19
|
+
* whose writer is gone is stale, not a running console.
|
|
20
|
+
*/
|
|
21
|
+
import { writeFileSync, readFileSync, unlinkSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { runsRoot } from "./rundir.js";
|
|
24
|
+
|
|
25
|
+
function linkFile(port, base) {
|
|
26
|
+
return join(runsRoot(base), `ui-${port}.url`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Records this run's live URL for the port it bound. */
|
|
30
|
+
export function writeUiLink(port, url, base) {
|
|
31
|
+
writeFileSync(linkFile(port, base), JSON.stringify({ url, pid: process.pid }) + "\n", { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The live URL a `seisin ui` left for this port, with whether its writer is
|
|
36
|
+
* still alive — or null if there is no readable record.
|
|
37
|
+
*/
|
|
38
|
+
export function readUiLink(port, base) {
|
|
39
|
+
let raw;
|
|
40
|
+
try { raw = readFileSync(linkFile(port, base), "utf8"); } catch { return null; }
|
|
41
|
+
let e;
|
|
42
|
+
try { e = JSON.parse(raw); } catch { return null; }
|
|
43
|
+
if (!e || typeof e.url !== "string") return null;
|
|
44
|
+
let alive = null;
|
|
45
|
+
if (typeof e.pid === "number") {
|
|
46
|
+
// Signal 0 tests for the process without touching it: it throws if gone.
|
|
47
|
+
try { process.kill(e.pid, 0); alive = true; } catch { alive = false; }
|
|
48
|
+
}
|
|
49
|
+
return { url: e.url, pid: e.pid ?? null, alive };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Removes the record. Called on exit, and when a stale one is found. */
|
|
53
|
+
export function clearUiLink(port, base) {
|
|
54
|
+
try { unlinkSync(linkFile(port, base)); } catch {}
|
|
55
|
+
}
|
package/ui/index.html
CHANGED
|
@@ -320,7 +320,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
320
320
|
<a href="#activity"><i>◷</i>Activity</a>
|
|
321
321
|
<a href="#configfile"><i>⚙</i>Config file</a>
|
|
322
322
|
<div class="cap">Docs</div>
|
|
323
|
-
<a href="https://github.com/
|
|
323
|
+
<a href="https://github.com/anthropics/sandbox-runtime" target="_blank" rel="noopener"><i>↗</i>sandbox-runtime</a>
|
|
324
324
|
</div>
|
|
325
325
|
</nav>
|
|
326
326
|
|
|
@@ -522,6 +522,9 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
522
522
|
];
|
|
523
523
|
|
|
524
524
|
function esc(s){return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
|
|
525
|
+
// For attribute values: the text an agent chose (a request's id is its path)
|
|
526
|
+
// must not be able to close the quote it sits in.
|
|
527
|
+
function escA(s){return esc(s).replace(/"/g,""").replace(/'/g,"'");}
|
|
525
528
|
function pad(n){return String(n).padStart(2,"0");}
|
|
526
529
|
|
|
527
530
|
function theme(v){
|
|
@@ -886,10 +889,19 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
886
889
|
person decides who owns them — a decision, not a count. Added after a
|
|
887
890
|
field review pointed out that the percentage above was the console
|
|
888
891
|
reading the log back to somebody who had already read it. */
|
|
892
|
+
/* Where each path stands against the policy NOW, not what the log wrote the day of the
|
|
893
|
+
refusal: that was read here once, and every grant made since kept its paths counted as
|
|
894
|
+
nobody's. Protected paths and ports are said apart, because they ask the opposite of a
|
|
895
|
+
decision \u2014 there is nothing to grant. */
|
|
896
|
+
var st = f.standing || {};
|
|
889
897
|
if (f.unowned)
|
|
890
|
-
lede += " <b>" + f.unowned + "</b> of the " + (f.distinct || f.causes.length).toLocaleString() +
|
|
898
|
+
lede += " <b>" + f.unowned.toLocaleString() + "</b> of the " + (f.distinct || f.causes.length).toLocaleString() +
|
|
891
899
|
" are on paths <b>no role owns</b> \u2014 a grant cannot settle those until " +
|
|
892
900
|
"somebody claims them.";
|
|
901
|
+
var closed = (st.protected ? st.protected.paths : 0);
|
|
902
|
+
if (closed)
|
|
903
|
+
lede += " <b>" + closed.toLocaleString() + "</b> " + (closed === 1 ? "is" : "are") +
|
|
904
|
+
" protected on purpose \u2014 the boundary working, nothing to grant.";
|
|
893
905
|
document.getElementById("ca-lede").innerHTML = lede;
|
|
894
906
|
var win = document.getElementById("dn-window");
|
|
895
907
|
if (win) win.textContent = rangeLabel() + " \u00b7 " + f.total.toLocaleString() + " denials \u00b7 " +
|
|
@@ -901,12 +913,16 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
901
913
|
// A cause the policy no longer denies is history, not live. It is
|
|
902
914
|
// shown, greyed, rather than dropped: it explains a number somebody may
|
|
903
915
|
// remember from yesterday instead of silently changing it.
|
|
904
|
-
|
|
916
|
+
// `stillRefused` is what the server sends. This read `stillDeclined`, which it never
|
|
917
|
+
// did, so nothing was ever greyed.
|
|
918
|
+
var cold = c.stillRefused === 0;
|
|
905
919
|
var who = c.roles.length === 1 ? c.roles[0] : c.roles.length + " roles";
|
|
906
920
|
return '<div class="bar' + (cold ? ' cold' : '') + '">' +
|
|
907
921
|
'<div class="lbl"><div class="p">' + esc(c.action) + ' ' + esc(c.target) + '</div>' +
|
|
908
922
|
'<div class="who">' + esc(who) +
|
|
909
|
-
(c.
|
|
923
|
+
(c.standing === "protected" ? ' · protected: ' + esc(c.why || "") :
|
|
924
|
+
c.standing === "outside" ? ' · ' + esc(c.why || "outside the map") :
|
|
925
|
+
c.owners && c.owners.length ? ' · owned by ' + esc(c.owners.join(", ")) : ' · owned by nobody') +
|
|
910
926
|
(cold ? ' · <b>granted since</b>' : '') + '</div>' +
|
|
911
927
|
'<div class="t"><span class="f" style="width:' + w + '%"></span></div></div>' +
|
|
912
928
|
'<div class="num">' + c.times.toLocaleString() +
|
|
@@ -1056,7 +1072,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1056
1072
|
host.innerHTML = REQS.map(function(r, i){
|
|
1057
1073
|
var owners = (r.owners && r.owners.length) ? " — belongs to <b>" + esc(r.owners.join(", ")) + "</b>" : "";
|
|
1058
1074
|
var times = r.times > 1 ? ' · <span class="asked">asked ' + r.times + "×</span>" : "";
|
|
1059
|
-
// Marked, not moved: Approve and Decline post this row's
|
|
1075
|
+
// Marked, not moved: Approve and Decline post this row's id.
|
|
1060
1076
|
var stale = r.stale ? ' · not asked again in ' + Number(r.stale.runs) + ' runs since — likely no longer needed' : "";
|
|
1061
1077
|
return '<div class="req' + (r.stale ? ' stale' : '') + '">' +
|
|
1062
1078
|
'<div class="who">' +
|
|
@@ -1066,8 +1082,8 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1066
1082
|
"</div>" +
|
|
1067
1083
|
'<div class="act">' +
|
|
1068
1084
|
'<input id="why-' + i + '" placeholder="why — this outlives you" aria-label="reason">' +
|
|
1069
|
-
'<button class="btn" data-n="' +
|
|
1070
|
-
'<button class="btn warn" data-n="' +
|
|
1085
|
+
'<button class="btn" data-n="' + i + '" data-k="' + escA(r.key) + '" data-d="granted" type="button">Approve</button>' +
|
|
1086
|
+
'<button class="btn warn" data-n="' + i + '" data-k="' + escA(r.key) + '" data-d="denied" type="button">Decline</button>' +
|
|
1071
1087
|
"</div>" +
|
|
1072
1088
|
"</div>";
|
|
1073
1089
|
}).join("");
|
|
@@ -1122,19 +1138,21 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1122
1138
|
/**
|
|
1123
1139
|
* One decision, sent once.
|
|
1124
1140
|
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1141
|
+
* By the request's id, taken from the row as it was drawn: a position would
|
|
1142
|
+
* be counted against a queue that agents are still writing to. A second click
|
|
1143
|
+
* finds the request already settled and is refused.
|
|
1127
1144
|
*/
|
|
1128
1145
|
async function act(btn){
|
|
1129
|
-
var
|
|
1130
|
-
var
|
|
1146
|
+
var i = Number(btn.dataset.n);
|
|
1147
|
+
var key = btn.dataset.k;
|
|
1148
|
+
var why = (document.getElementById("why-" + i) || {}).value || "";
|
|
1131
1149
|
var row = btn.closest(".req");
|
|
1132
1150
|
row.querySelectorAll("button,input").forEach(function(e){ e.disabled = true; });
|
|
1133
1151
|
try {
|
|
1134
1152
|
var r = await fetch("/api/decide", {
|
|
1135
1153
|
method: "POST",
|
|
1136
1154
|
headers: { "content-type": "application/json", "x-seisin-token": TOKEN },
|
|
1137
|
-
body: JSON.stringify({
|
|
1155
|
+
body: JSON.stringify({ key: key, decision: btn.dataset.d, reason: why })
|
|
1138
1156
|
});
|
|
1139
1157
|
var out = await r.json();
|
|
1140
1158
|
if (!r.ok) { var err = new Error(out.error || "denied"); err.status = r.status; throw err; }
|