seisin 0.5.0 → 0.6.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/README.md +385 -129
- package/package.json +13 -5
- package/src/browsers.js +52 -0
- package/src/canary.js +149 -0
- package/src/cli.js +47 -23
- package/src/commands/check.js +154 -3
- package/src/commands/explain.js +2 -5
- package/src/commands/hook.js +7 -3
- package/src/commands/init.js +57 -50
- package/src/commands/requests.js +31 -56
- package/src/commands/review.js +33 -6
- package/src/commands/run.js +332 -110
- package/src/commands/walls.js +39 -7
- package/src/commands/whose.js +2 -1
- package/src/config.js +321 -15
- package/src/controls.js +5 -5
- package/src/diagnose.js +67 -16
- package/src/env.js +3 -0
- package/src/grants.js +107 -2
- package/src/handoff.js +285 -10
- package/src/help.js +39 -19
- package/src/hook.js +59 -30
- package/src/index.d.ts +217 -0
- package/src/index.js +20 -12
- package/src/inspect/codex.js +64 -0
- package/src/inspect/credentials.js +48 -0
- package/src/inspect/enforce.js +24 -0
- package/src/inspect/files.js +71 -0
- package/src/inspect/home.js +43 -0
- package/src/inspect/inject.js +68 -0
- package/src/inspect/keys.js +74 -0
- package/src/inspect/layout.js +258 -0
- package/src/inspect/limits.js +98 -0
- package/src/inspect/machine.js +107 -0
- package/src/inspect/network.js +37 -0
- package/src/inspect/providers.js +71 -0
- package/src/inspect/read-territory.js +105 -0
- package/src/inspect/role-keys.js +136 -0
- package/src/inspect/shared.js +37 -0
- package/src/inspect/warnings.js +58 -0
- package/src/inspect.js +13 -815
- package/src/intake.js +131 -21
- package/src/keys.js +223 -67
- package/src/kinds.js +42 -0
- package/src/lines.js +26 -0
- package/src/log.js +7 -2
- package/src/mcp.js +28 -43
- package/src/nested.js +12 -3
- package/src/notify.js +53 -12
- package/src/owners.js +165 -3
- package/src/paths.js +17 -1
- package/src/provider-run.js +81 -0
- package/src/redact.js +34 -15
- package/src/render.js +35 -9
- package/src/requests.js +363 -45
- package/src/review.js +21 -9
- package/src/rundir.js +29 -8
- package/src/sandboxes/command.js +93 -0
- package/src/sandboxes/index.js +113 -0
- package/src/sandboxes/mach-profile.js +31 -0
- package/src/sandboxes/srt-mach.mjs +43 -0
- package/src/sandboxes/srt.js +163 -0
- package/src/sandboxes/tartaros.js +80 -0
- package/src/scan.js +7 -3
- package/src/serve.js +35 -17
- package/src/spool.js +8 -17
- package/src/srt.js +263 -33
- package/src/surface.js +129 -16
- package/src/territory.js +261 -0
- package/src/trace.js +33 -0
- package/src/uilink.js +15 -6
- package/src/views.js +40 -14
- package/src/violations.js +143 -7
- package/src/walls.js +62 -9
- package/ui/index.html +85 -13
- package/ui/favicon.svg +0 -1
package/ui/index.html
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
+
<!-- Built from ui/src/ by ui/build.js. Edit there, then run: npm run build:ui -->
|
|
2
3
|
<html lang="en">
|
|
3
4
|
<head>
|
|
4
5
|
<meta charset="utf-8">
|
|
@@ -304,6 +305,8 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
304
305
|
.req .who b{font-weight:600}
|
|
305
306
|
.req .path{font-family:var(--mono);font-size:12.5px;color:var(--fg)}
|
|
306
307
|
.req .sub{font-size:12px;color:var(--dim);margin-top:3px}
|
|
308
|
+
.req .handoff{font-size:12px;color:var(--dim);margin-top:3px}
|
|
309
|
+
.req .handoff.person{color:var(--amber)}
|
|
307
310
|
.req .act{display:flex;gap:8px;align-items:center;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}
|
|
308
311
|
.req input{font:inherit;font-size:12.5px;padding:6px 9px;border:1px solid var(--line);border-radius:6px;
|
|
309
312
|
background:var(--bg);color:var(--fg);width:min(30vw,220px)}
|
|
@@ -371,6 +374,11 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
371
374
|
.req .scope{font-size:12.5px;margin-top:4px;color:var(--fg)}
|
|
372
375
|
.req .scope.wide>b{color:var(--amber)}
|
|
373
376
|
.req .scope .own{display:block;color:var(--mid);font-size:12px;margin-top:2px}
|
|
377
|
+
.req .pick{display:flex;gap:14px;flex-wrap:wrap;font-size:12.5px;margin-top:4px;color:var(--mid)}
|
|
378
|
+
.req .pick label{display:inline-flex;gap:5px;align-items:center;cursor:pointer}
|
|
379
|
+
.req .pick input{padding:0;margin:0;width:auto;border:0}
|
|
380
|
+
.req .fnote{font-size:12px;color:var(--amber);margin-top:3px}
|
|
381
|
+
.req .fnote[hidden]{display:none}
|
|
374
382
|
.req .err{font-size:12px;color:var(--amber);margin-top:4px}
|
|
375
383
|
.req .err[hidden]{display:none}
|
|
376
384
|
.req input[aria-invalid="true"]{border-color:var(--amber)}
|
|
@@ -807,7 +815,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
807
815
|
groups.forEach(function(g){
|
|
808
816
|
about[g.about].paths++; about[g.about].denials += g.times;
|
|
809
817
|
g.share = g.times / total;
|
|
810
|
-
g.
|
|
818
|
+
g.stillDenied = g.roles.filter(function(r){ return still(r, g); }).length;
|
|
811
819
|
if (g.about !== "file") { g.standing = "outside"; return; }
|
|
812
820
|
if (g.owners.length) { g.standing = "owned"; return; }
|
|
813
821
|
g.standing = "unowned"; unowned++;
|
|
@@ -853,7 +861,9 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
853
861
|
blocks: denied.filter(function(e){ return e.role === r.name; }).length };
|
|
854
862
|
}),
|
|
855
863
|
requests: DEMO.requests.map(function(q){
|
|
856
|
-
|
|
864
|
+
var owners = ownersIn(roles, q.target);
|
|
865
|
+
return Object.assign({ key: q.role + ":" + q.action + ":" + q.target, owners: owners,
|
|
866
|
+
handoffNote: demoHandoff(q, owners),
|
|
857
867
|
last: new Date(now - q.first * 6e4).toISOString() }, q);
|
|
858
868
|
}),
|
|
859
869
|
causes: { total: total, distinct: groups.length, unowned: unowned, about: about,
|
|
@@ -864,6 +874,15 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
864
874
|
};
|
|
865
875
|
}
|
|
866
876
|
|
|
877
|
+
/** Where an example request goes. Live, the server says it (handoff.js); the example has no
|
|
878
|
+
* server, so it says the three cases its requests can reach, in the same words. */
|
|
879
|
+
function demoHandoff(q, owners){
|
|
880
|
+
if (owners.length === 1) return owners[0] === q.role
|
|
881
|
+
? "no handoff needed — the policy moved and " + q.role + " has it now" : "handed to " + owners[0];
|
|
882
|
+
return owners.length ? "a person decides: more than one role owns this (" + owners.join(", ") + ")"
|
|
883
|
+
: "a person decides: no role owns this";
|
|
884
|
+
}
|
|
885
|
+
|
|
867
886
|
/** The example's state again, after a change made in it. */
|
|
868
887
|
function demoRefresh(){ if (!LIVE) applyState(demoState()); }
|
|
869
888
|
|
|
@@ -994,8 +1013,13 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
994
1013
|
|
|
995
1014
|
var c4=document.createElement("td"); c4.className="st"; c4.setAttribute("data-label","confined");
|
|
996
1015
|
if (LIVE) {
|
|
997
|
-
//
|
|
998
|
-
|
|
1016
|
+
// The last canary is evidence from a run, not a promise inferred from the policy.
|
|
1017
|
+
if (r.canary) {
|
|
1018
|
+
var passed=r.canary.confined===true;
|
|
1019
|
+
c4.innerHTML='<span class="state" title="'+(passed?'Last canary passed':'Last canary failed')+'"><i></i>'+
|
|
1020
|
+
(passed?'canary passed':'canary failed')+'</span>';
|
|
1021
|
+
if(!passed)c4.querySelector(".state").classList.add("warn");
|
|
1022
|
+
} else c4.innerHTML='<span class="state" title="No run has recorded a canary yet"><i></i>not run</span>';
|
|
999
1023
|
} else {
|
|
1000
1024
|
var sw=document.createElement("button"); sw.type="button"; sw.className="sw";
|
|
1001
1025
|
var txt=r.sandbox?"confined":"unconfined";
|
|
@@ -1454,6 +1478,8 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1454
1478
|
/** One cause's line under its path: whose it is, or what kind of thing it is. */
|
|
1455
1479
|
function causeSays(c){
|
|
1456
1480
|
if (c.about && c.about !== "file") return unownedSays(c.about);
|
|
1481
|
+
// Refused correctly and nothing to grant: said as expected, before "protected".
|
|
1482
|
+
if (c.expected) return "expected · " + c.expected.hint;
|
|
1457
1483
|
if (c.standing === "protected") return "protected: " + (c.why || "");
|
|
1458
1484
|
if (c.standing === "outside") return c.why || "outside the map";
|
|
1459
1485
|
if (c.owners && c.owners.length) return "owned by " + c.owners.join(", ");
|
|
@@ -1463,7 +1489,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1463
1489
|
function bar(c, max){
|
|
1464
1490
|
var w = Math.max(2, Math.round((c.times / max) * 100));
|
|
1465
1491
|
// A cause the policy no longer denies is history, not live: shown greyed, not dropped.
|
|
1466
|
-
var cold = c.
|
|
1492
|
+
var cold = c.stillDenied === 0;
|
|
1467
1493
|
var who = c.roles.length === 1 ? c.roles[0] : c.roles.length + " roles";
|
|
1468
1494
|
return '<div class="bar' + (cold ? ' cold' : '') + '">' +
|
|
1469
1495
|
'<div class="lbl"><div class="p">' + esc(c.action) + ' ' + esc(c.target) + '</div>' +
|
|
@@ -1518,6 +1544,10 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1518
1544
|
["git", "temporary", "build", "credential"].filter(function(k){ return kinds[k] && kinds[k].paths; })
|
|
1519
1545
|
.map(function(k){ return kinds[k].paths.toLocaleString() + " " + k; }).join(", ") +
|
|
1520
1546
|
", each with its own fix and none of them an owner.";
|
|
1547
|
+
var ex = f.expected;
|
|
1548
|
+
if (ex && ex.denials)
|
|
1549
|
+
h += " <b>" + plural(ex.denials, "denial") + "</b> " + (ex.denials === 1 ? "is" : "are") +
|
|
1550
|
+
" expected: Claude Code updating its plugins from inside the sandbox. They run and update outside it — nothing to grant.";
|
|
1521
1551
|
var closed = (st.protected ? st.protected.paths : 0);
|
|
1522
1552
|
if (closed)
|
|
1523
1553
|
h += " <b>" + closed.toLocaleString() + "</b> " + (closed === 1 ? "is" : "are") +
|
|
@@ -1582,7 +1612,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1582
1612
|
}
|
|
1583
1613
|
|
|
1584
1614
|
/** Retries on a list of walls: every call after the first denial of each. */
|
|
1585
|
-
function retriesOf(list){ return list.reduce(function(n, w){ return n + (w.retried !== undefined ? w.retried : w.times - 1); }, 0); }
|
|
1615
|
+
function retriesOf(list){ return list.reduce(function(n, w){ return n + (w.expected ? 0 : w.retried !== undefined ? w.retried : w.times - 1); }, 0); }
|
|
1586
1616
|
|
|
1587
1617
|
/**
|
|
1588
1618
|
* What a wall says under its path: the sentence Denied gives the same path, from the same
|
|
@@ -1694,6 +1724,41 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1694
1724
|
esc(r.grant) + "</code></b>, not only that file." + own + "</div>";
|
|
1695
1725
|
}
|
|
1696
1726
|
|
|
1727
|
+
/** Where the request goes, as the server decided it: handed to its owner, or a person decides.
|
|
1728
|
+
* Not worked out here — the page shows the decision seisin requests and the MCP show. */
|
|
1729
|
+
function handoffOf(r){
|
|
1730
|
+
if (typeof r.handoffNote !== "string" || !r.handoffNote) return "";
|
|
1731
|
+
var routed = /^handed to /.test(r.handoffNote);
|
|
1732
|
+
return '<div class="handoff' + (routed ? "" : " person") + '">' + esc(r.handoffNote) + "</div>";
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/** A write keyed by a folder can be granted as the one file instead. */
|
|
1736
|
+
function narrowable(r){ return r.action === "write" && r.grant !== r.target; }
|
|
1737
|
+
|
|
1738
|
+
/** The line a one-file grant carries. Live, the server says it from the role's last run;
|
|
1739
|
+
* the example has no runs, so it says it conditionally. */
|
|
1740
|
+
function fileNoteOf(r){
|
|
1741
|
+
if (typeof r.fileNote === "string") return r.fileNote;
|
|
1742
|
+
return "If " + r.role + " runs Claude Code, a one-file grant will not let it edit: Claude Code's Write and Edit " +
|
|
1743
|
+
"save to a temporary beside the file and rename it over, and the temporary is outside a one-file grant (EPERM).";
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/** Folder (as before) or only the file: the choice, and what the file choice costs. */
|
|
1747
|
+
function scopeChoice(r, i){
|
|
1748
|
+
if (!narrowable(r)) return "";
|
|
1749
|
+
return '<div class="pick" role="radiogroup" aria-label="What to grant">' +
|
|
1750
|
+
'<label><input type="radio" name="scope-' + i + '" value="folder" checked> the folder <code>' + esc(r.grant) + "</code></label>" +
|
|
1751
|
+
'<label><input type="radio" name="scope-' + i + '" value="file"> only the file <code>' + esc(r.target) + "</code></label>" +
|
|
1752
|
+
"</div>" +
|
|
1753
|
+
'<div class="fnote" id="fnote-' + i + '" hidden>' + esc(fileNoteOf(r)) + "</div>";
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/** What the row's Grant button would write: the folder, or the one file chosen instead. */
|
|
1757
|
+
function chosen(r, i){
|
|
1758
|
+
var f = document.querySelector('input[name="scope-' + i + '"][value="file"]');
|
|
1759
|
+
return narrowable(r) && f && f.checked ? { scope: "file", grant: r.target } : { scope: "folder", grant: r.grant };
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1697
1762
|
function drawReqs(){
|
|
1698
1763
|
var host = document.getElementById("reqs");
|
|
1699
1764
|
pill("req-pin", REQS.length, REQS.length === 1 ? "request" : "requests");
|
|
@@ -1711,7 +1776,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1711
1776
|
return '<div class="req' + (r.stale ? ' stale' : '') + '">' +
|
|
1712
1777
|
'<div class="who">' +
|
|
1713
1778
|
"<div><b>" + esc(r.role) + "</b> wants to " + esc(r.action) + ' <span class="path">' + esc(r.target) + "</span></div>" +
|
|
1714
|
-
scopeOf(r) +
|
|
1779
|
+
scopeOf(r) + handoffOf(r) + scopeChoice(r, i) +
|
|
1715
1780
|
'<div class="sub"><span class="asked">' + times + "</span>" +
|
|
1716
1781
|
(w.text ? ' · last <span title="' + escA(w.title) + '">' + esc(w.text) + "</span>" : "") + stale + "</div>" +
|
|
1717
1782
|
'<div class="err" id="err-' + i + '" role="alert" hidden></div>' +
|
|
@@ -1728,6 +1793,12 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1728
1793
|
host.querySelectorAll("button[data-n]").forEach(function(b){
|
|
1729
1794
|
b.addEventListener("click", function(){ act(b); });
|
|
1730
1795
|
});
|
|
1796
|
+
host.querySelectorAll('input[type="radio"][name^="scope-"]').forEach(function(x){
|
|
1797
|
+
x.addEventListener("change", function(){
|
|
1798
|
+
var n = x.name.slice(6), note = document.getElementById("fnote-" + n);
|
|
1799
|
+
if (note) note.hidden = !(x.value === "file" && x.checked && note.textContent);
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1731
1802
|
drawBulk();
|
|
1732
1803
|
lockWrites();
|
|
1733
1804
|
}
|
|
@@ -1837,9 +1908,10 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1837
1908
|
input.value = "declined from the console";
|
|
1838
1909
|
filled = true;
|
|
1839
1910
|
}
|
|
1911
|
+
var pick = chosen(req, i);
|
|
1840
1912
|
var needsArm = granting ? (req.owners || []).length > 0 : filled;
|
|
1841
1913
|
if (needsArm && !btn.dataset.armed) {
|
|
1842
|
-
arm(btn, granting ? "Click again to share " +
|
|
1914
|
+
arm(btn, granting ? "Click again to share " + pick.grant + " with " + req.role
|
|
1843
1915
|
: "Click again to decline with this reason", rest);
|
|
1844
1916
|
return;
|
|
1845
1917
|
}
|
|
@@ -1849,15 +1921,15 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1849
1921
|
if (!LIVE) {
|
|
1850
1922
|
if (granting) {
|
|
1851
1923
|
var r0 = ROLES.filter(function(x){ return x.name === req.role; })[0];
|
|
1852
|
-
if (r0 && r0.writes.indexOf(
|
|
1924
|
+
if (r0 && r0.writes.indexOf(pick.grant) === -1) r0.writes.push(pick.grant);
|
|
1853
1925
|
}
|
|
1854
1926
|
DEMO.requests = DEMO.requests.filter(function(q){ return q.role + ":" + q.action + ":" + q.target !== key; });
|
|
1855
1927
|
decided(granting
|
|
1856
|
-
? "Granted <code>" + esc(
|
|
1857
|
-
'<span class="ln">"' + esc(
|
|
1928
|
+
? "Granted <code>" + esc(pick.grant) + "</code> to <b>" + esc(req.role) + "</b> in the example. Live, this line is written to seisin.toml:" +
|
|
1929
|
+
'<span class="ln">"' + esc(pick.grant) + '" # granted … · «' + esc(why) + "»</span>Nothing was saved. " +
|
|
1858
1930
|
'<a href="#configfile">see the example seisin.toml</a>'
|
|
1859
1931
|
: "Declined <b>" + esc(req.role) + "</b>’s request for <code>" + esc(req.grant) + "</code> in the example · reason «" + esc(why) + "».");
|
|
1860
|
-
note(granting ? "granted" : "declined", req.role + " · " + req.grant);
|
|
1932
|
+
note(granting ? "granted" : "declined", req.role + " · " + (granting ? pick.grant : req.grant));
|
|
1861
1933
|
return;
|
|
1862
1934
|
}
|
|
1863
1935
|
|
|
@@ -1866,7 +1938,7 @@ button.btn:active,button.tag:active,button.add:active,button.sw:active{transform
|
|
|
1866
1938
|
var r = await fetch("/api/decide", {
|
|
1867
1939
|
method: "POST",
|
|
1868
1940
|
headers: { "content-type": "application/json", "x-seisin-token": TOKEN },
|
|
1869
|
-
body: JSON.stringify({ key: key, decision: btn.dataset.d, reason: why })
|
|
1941
|
+
body: JSON.stringify({ key: key, decision: btn.dataset.d, reason: why, scope: granting ? pick.scope : "folder" })
|
|
1870
1942
|
});
|
|
1871
1943
|
var out = await r.json();
|
|
1872
1944
|
if (!r.ok) { var e2 = new Error(out.error || "the console did not accept that"); e2.status = r.status; throw e2; }
|
package/ui/favicon.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#1f2328"/><path d="M16 5.5C8 5.5 8 16 16 16S24 26.5 16 26.5H26.5V5.5Z" fill="#2da44e"/><path d="M16 5.5C8 5.5 8 16 16 16S24 26.5 16 26.5" fill="none" stroke="#fff" stroke-width="2.8" stroke-linecap="round"/></svg>
|