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/src/redact.js
CHANGED
|
@@ -59,6 +59,10 @@ export function secretsOf(settings, readFile) {
|
|
|
59
59
|
* half of a surrogate pair on either side.
|
|
60
60
|
*/
|
|
61
61
|
export function redactor(secrets, label = "redacted") {
|
|
62
|
+
// Embedders use this export directly; neither order nor empty values may
|
|
63
|
+
// decide how much of a split credential is retained between chunks.
|
|
64
|
+
secrets = [...new Set(secrets.filter((s) => typeof s === "string" && s.length > 0))]
|
|
65
|
+
.sort((a, b) => b.length - a.length);
|
|
62
66
|
if (secrets.length === 0) return null;
|
|
63
67
|
const longest = secrets[0].length;
|
|
64
68
|
const decoder = new StringDecoder("utf8");
|
|
@@ -66,21 +70,22 @@ export function redactor(secrets, label = "redacted") {
|
|
|
66
70
|
|
|
67
71
|
return new Transform({
|
|
68
72
|
transform(chunk, _enc, done) {
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// first; the split-buffer test caught it on the first run.
|
|
73
|
+
// Retain original input, not replacement text. Replacing a shorter
|
|
74
|
+
// credential before a longer one finishes destroys their shared prefix;
|
|
75
|
+
// carrying a marker forward also lets another secret replace its label.
|
|
73
76
|
const text = typeof chunk === "string" ? chunk : decoder.write(chunk);
|
|
74
|
-
const
|
|
75
|
-
let cut =
|
|
76
|
-
if (cut > 0 && isHighSurrogate(
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const buffered = tail + text;
|
|
78
|
+
let cut = Math.max(0, buffered.length - longest + 1);
|
|
79
|
+
if (cut > 0 && isHighSurrogate(buffered.charCodeAt(cut - 1))) cut--;
|
|
80
|
+
const result = maskPrefix(buffered, cut, secrets, label);
|
|
81
|
+
if (result.output) this.push(result.output);
|
|
82
|
+
tail = result.tail;
|
|
79
83
|
done();
|
|
80
84
|
},
|
|
81
85
|
flush(done) {
|
|
82
|
-
const
|
|
83
|
-
|
|
86
|
+
const text = tail + decoder.end();
|
|
87
|
+
const result = maskPrefix(text, text.length, secrets, label);
|
|
88
|
+
if (result.output) this.push(result.output);
|
|
84
89
|
tail = "";
|
|
85
90
|
done();
|
|
86
91
|
},
|
|
@@ -89,8 +94,22 @@ export function redactor(secrets, label = "redacted") {
|
|
|
89
94
|
|
|
90
95
|
const isHighSurrogate = (c) => c >= 0xd800 && c <= 0xdbff;
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
/** Only match starts with enough input to decide the longest credential. */
|
|
98
|
+
function maskPrefix(text, cut, secrets, label) {
|
|
99
|
+
const next = secrets.map((s) => text.indexOf(s));
|
|
100
|
+
let from = 0, output = "";
|
|
101
|
+
while (from < cut) {
|
|
102
|
+
let at = Infinity, chosen = -1;
|
|
103
|
+
for (let i = 0; i < next.length; i++) {
|
|
104
|
+
if (next[i] >= 0 && next[i] < at) { at = next[i]; chosen = i; }
|
|
105
|
+
}
|
|
106
|
+
if (at >= cut) { output += text.slice(from, cut); from = cut; break; }
|
|
107
|
+
output += text.slice(from, at) + `‹${label}›`;
|
|
108
|
+
from = at + secrets[chosen].length;
|
|
109
|
+
// Keep each search moving forward. In particular, a value absent from
|
|
110
|
+
// this buffer is not searched again for every occurrence of another.
|
|
111
|
+
for (let i = 0; i < next.length; i++)
|
|
112
|
+
if (next[i] >= 0 && next[i] < from) next[i] = text.indexOf(secrets[i], from);
|
|
113
|
+
}
|
|
114
|
+
return { output, tail: text.slice(from) };
|
|
96
115
|
}
|
package/src/render.js
CHANGED
|
@@ -86,6 +86,8 @@ export function renderReport(report, { verbose = true } = {}) {
|
|
|
86
86
|
lines.push(` ${C.b}${r.name.padEnd(width)}${C.off} ${C.blue}writes${C.off} ${writes}\n`);
|
|
87
87
|
// Only when present: absent is the default, and a "never_writes nothing" line
|
|
88
88
|
// on every role is noise that teaches the reader to skip the one that matters.
|
|
89
|
+
if (r.private?.length)
|
|
90
|
+
lines.push(` ${" ".repeat(width)} ${C.yellow}private${C.off} ${r.private.join(" ")} ${C.dim}(only this role reads or writes it)${C.off}\n`);
|
|
89
91
|
if (r.neverWrites?.length)
|
|
90
92
|
lines.push(` ${" ".repeat(width)} ${C.yellow}never${C.off} ${collapseSidecars(r.neverWrites).join(" ")}\n`);
|
|
91
93
|
if (r.trustd)
|
|
@@ -96,6 +98,12 @@ export function renderReport(report, { verbose = true } = {}) {
|
|
|
96
98
|
lines.push(` ${" ".repeat(width)} ${C.yellow}mcp${C.off} ${r.mcp.length ? r.mcp.join(" ") : "none"} ${C.dim}(declared; the launcher enforces it)${C.off}\n`);
|
|
97
99
|
if (r.localPorts?.length)
|
|
98
100
|
lines.push(` ${" ".repeat(width)} ${C.yellow}reach${C.off} localhost ${r.localPorts.join(" ")} ${C.dim}(local_ports, through the proxy)${C.off}\n`);
|
|
101
|
+
if (r.reads?.length)
|
|
102
|
+
lines.push(` ${" ".repeat(width)} ${C.blue}reads${C.off} ${r.reads.join(" ")}\n`);
|
|
103
|
+
if (r.toolchain?.length)
|
|
104
|
+
lines.push(` ${" ".repeat(width)} ${C.blue}tools${C.off} ${r.toolchain.join(" ")}\n`);
|
|
105
|
+
if (r.verify)
|
|
106
|
+
lines.push(` ${" ".repeat(width)} ${C.blue}verify${C.off} ${r.verify.join(" ")}\n`);
|
|
99
107
|
lines.push(` ${" ".repeat(width)} ${C.green}keys${C.off} ${keys}\n\n`);
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -129,28 +137,33 @@ export function renderReport(report, { verbose = true } = {}) {
|
|
|
129
137
|
const more = [
|
|
130
138
|
report.protected?.length ? `${report.protected.length} protected path(s)` : "",
|
|
131
139
|
report.warnings.some((w) => w.detail) ? "why each warning matters" : "",
|
|
132
|
-
report.limits?.length ? `${report.limits.length} standing limit(s)` : "",
|
|
133
140
|
].filter(Boolean);
|
|
134
|
-
if (more.length) lines.push(` ${C.dim}seisin check --verbose: ${more.join(", ")}${C.off}\n
|
|
141
|
+
if (more.length) lines.push(` ${C.dim}seisin check --verbose: ${more.join(", ")}${C.off}\n`);
|
|
142
|
+
lines.push(limitsLine(report), "\n");
|
|
135
143
|
return lines.join("");
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
for (const w of report.warnings) {
|
|
139
147
|
lines.push(` ${C.yellow}${w.headline}${C.off}\n`);
|
|
140
148
|
if (w.detail) lines.push(` ${C.dim}${w.detail}${C.off}\n`);
|
|
149
|
+
// A list is a list: one entry per line, not a sentence of twenty paths.
|
|
150
|
+
for (const item of w.items ?? []) lines.push(` ${C.dim}${item}${C.off}\n`);
|
|
141
151
|
lines.push("\n");
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (l.detail) lines.push(` ${C.dim}${l.detail}${C.off}\n`);
|
|
149
|
-
lines.push("\n");
|
|
150
|
-
}
|
|
154
|
+
// The standing limits are the same in every repo, so they are one line here
|
|
155
|
+
// and the paragraphs are `seisin help limits` — six of them closed every
|
|
156
|
+
// verbose report, under the part that was about this repo.
|
|
157
|
+
lines.push(limitsLine(report), "\n");
|
|
151
158
|
return lines.join("");
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
/** Where the standing limits are, or nothing when the report carries none. */
|
|
162
|
+
function limitsLine(report) {
|
|
163
|
+
const n = report.limits?.length ?? 0;
|
|
164
|
+
return n ? ` ${C.dim}${n} standing limit(s) apply to every repo: seisin help limits${C.off}\n` : "";
|
|
165
|
+
}
|
|
166
|
+
|
|
154
167
|
/* ── one verdict ──────────────────────────────────────────────────────── */
|
|
155
168
|
|
|
156
169
|
/** The `explain` answer, as text. Takes what `owners.explain()` returns. */
|
|
@@ -169,6 +182,19 @@ export function renderVerdict(role, action, target, verdict) {
|
|
|
169
182
|
* people to stop reading the colour.
|
|
170
183
|
*/
|
|
171
184
|
export function renderEntry(e) {
|
|
185
|
+
if (e.event === "canary") {
|
|
186
|
+
const mark = e.confined ? `${C.green}canary passed${C.off}` : `${C.red}canary failed${C.off}`;
|
|
187
|
+
const why = e.reasons?.length ? ` ${C.dim}${e.reasons.join("; ")}${C.off}` : "";
|
|
188
|
+
return ` ${C.dim}${(e.at ?? "").slice(11, 19)}${C.off} ${mark} ${C.b}${e.role}${C.off}${why}\n`;
|
|
189
|
+
}
|
|
190
|
+
if (e.event === "log-dropped") {
|
|
191
|
+
const mark = `${C.yellow}log dropped${C.off}`;
|
|
192
|
+
return ` ${C.dim}${(e.at ?? "").slice(11, 19)}${C.off} ${mark} ${C.b}${e.role}${C.off} ${C.dim}${e.batches} batch(es) of system log messages lost — kernel denials may be missing${C.off}\n`;
|
|
193
|
+
}
|
|
194
|
+
if (e.event === "key") {
|
|
195
|
+
const mark = e.ok ? `${C.green}key resolved${C.off}` : `${C.red}key failed${C.off}`;
|
|
196
|
+
return ` ${C.dim}${(e.at ?? "").slice(11, 19)}${C.off} ${mark} ${C.b}${e.role}${C.off} ${safe(e.key)} ${C.dim}${e.mode}${C.off}\n`;
|
|
197
|
+
}
|
|
172
198
|
const mark =
|
|
173
199
|
e.verdict === "denied" ? `${C.yellow}denied ${C.off}`
|
|
174
200
|
: e.verdict === "observed" ? `${C.dim}seen ${C.off}`
|