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.
Files changed (76) hide show
  1. package/README.md +385 -129
  2. package/package.json +13 -5
  3. package/src/browsers.js +52 -0
  4. package/src/canary.js +149 -0
  5. package/src/cli.js +47 -23
  6. package/src/commands/check.js +154 -3
  7. package/src/commands/explain.js +2 -5
  8. package/src/commands/hook.js +7 -3
  9. package/src/commands/init.js +57 -50
  10. package/src/commands/requests.js +31 -56
  11. package/src/commands/review.js +33 -6
  12. package/src/commands/run.js +332 -110
  13. package/src/commands/walls.js +39 -7
  14. package/src/commands/whose.js +2 -1
  15. package/src/config.js +321 -15
  16. package/src/controls.js +5 -5
  17. package/src/diagnose.js +67 -16
  18. package/src/env.js +3 -0
  19. package/src/grants.js +107 -2
  20. package/src/handoff.js +285 -10
  21. package/src/help.js +39 -19
  22. package/src/hook.js +59 -30
  23. package/src/index.d.ts +217 -0
  24. package/src/index.js +20 -12
  25. package/src/inspect/codex.js +64 -0
  26. package/src/inspect/credentials.js +48 -0
  27. package/src/inspect/enforce.js +24 -0
  28. package/src/inspect/files.js +71 -0
  29. package/src/inspect/home.js +43 -0
  30. package/src/inspect/inject.js +68 -0
  31. package/src/inspect/keys.js +74 -0
  32. package/src/inspect/layout.js +258 -0
  33. package/src/inspect/limits.js +98 -0
  34. package/src/inspect/machine.js +107 -0
  35. package/src/inspect/network.js +37 -0
  36. package/src/inspect/providers.js +71 -0
  37. package/src/inspect/read-territory.js +105 -0
  38. package/src/inspect/role-keys.js +136 -0
  39. package/src/inspect/shared.js +37 -0
  40. package/src/inspect/warnings.js +58 -0
  41. package/src/inspect.js +13 -815
  42. package/src/intake.js +131 -21
  43. package/src/keys.js +223 -67
  44. package/src/kinds.js +42 -0
  45. package/src/lines.js +26 -0
  46. package/src/log.js +7 -2
  47. package/src/mcp.js +28 -43
  48. package/src/nested.js +12 -3
  49. package/src/notify.js +53 -12
  50. package/src/owners.js +165 -3
  51. package/src/paths.js +17 -1
  52. package/src/provider-run.js +81 -0
  53. package/src/redact.js +34 -15
  54. package/src/render.js +35 -9
  55. package/src/requests.js +363 -45
  56. package/src/review.js +21 -9
  57. package/src/rundir.js +29 -8
  58. package/src/sandboxes/command.js +93 -0
  59. package/src/sandboxes/index.js +113 -0
  60. package/src/sandboxes/mach-profile.js +31 -0
  61. package/src/sandboxes/srt-mach.mjs +43 -0
  62. package/src/sandboxes/srt.js +163 -0
  63. package/src/sandboxes/tartaros.js +80 -0
  64. package/src/scan.js +7 -3
  65. package/src/serve.js +35 -17
  66. package/src/spool.js +8 -17
  67. package/src/srt.js +263 -33
  68. package/src/surface.js +129 -16
  69. package/src/territory.js +261 -0
  70. package/src/trace.js +33 -0
  71. package/src/uilink.js +15 -6
  72. package/src/views.js +40 -14
  73. package/src/violations.js +143 -7
  74. package/src/walls.js +62 -9
  75. package/ui/index.html +85 -13
  76. 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.stillRefused = g.roles.filter(function(r){ return still(r, g); }).length;
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
- return Object.assign({ key: q.role + ":" + q.action + ":" + q.target, owners: ownersIn(roles, q.target),
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
- // `seisin run` always sandboxes; there is nothing to switch here.
998
- c4.innerHTML='<span class="state"><i></i>confined</span>';
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.stillRefused === 0;
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 " + req.grant + " with " + req.role
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(req.grant) === -1) r0.writes.push(req.grant);
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(req.grant) + "</code> to <b>" + esc(req.role) + "</b> in the example. Live, this line is written to seisin.toml:" +
1857
- '<span class="ln">"' + esc(req.grant) + '" # granted … · «' + esc(why) + "»</span>Nothing was saved. " +
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>