humanish 0.110.0 → 0.110.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/ARCHITECTURE.md +15 -12
- package/CHANGELOG.md +26 -0
- package/CONTEXT.md +8 -6
- package/README.md +103 -53
- package/dist/actors/computer-use/loop/ending.js +4 -4
- package/dist/actors/local-agent/claude-participant.d.ts +74 -0
- package/dist/actors/local-agent/claude-participant.js +243 -0
- package/dist/actors/local-agent/claude-session.d.ts +16 -3
- package/dist/actors/local-agent/claude-session.js +64 -23
- package/dist/actors/local-agent/cli.d.ts +17 -0
- package/dist/actors/local-agent/cli.js +111 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +20 -12
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/observe.js +15 -2
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +106 -45
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +113 -74
- package/dist/cli/commands/setup.js +23 -14
- package/dist/cli/commands/study-route-computer-use.js +9 -3
- package/dist/cli/commands/study-route-open.js +1 -0
- package/dist/cli/commands/study-route-preview.js +1 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +6 -2
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/commands/watch.js +14 -4
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +51 -10
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +317 -0
- package/dist/cli/first-run-path.d.ts +32 -11
- package/dist/cli/first-run-path.js +113 -69
- package/dist/cli/invocation.d.ts +7 -0
- package/dist/cli/invocation.js +32 -0
- package/dist/cli/io.d.ts +3 -1
- package/dist/cli/io.js +8 -15
- package/dist/cli/observer-follow.d.ts +20 -0
- package/dist/cli/observer-follow.js +36 -1
- package/dist/cli/orientation.js +16 -14
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +11 -0
- package/dist/evidence/encoded-text.d.ts +9 -6
- package/dist/evidence/encoded-text.js +84 -19
- package/dist/evidence/redaction.d.ts +2 -0
- package/dist/evidence/redaction.js +123 -21
- package/dist/feedback/export-bundle.js +2 -2
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/keys/key-resolution.js +6 -5
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/exposure.js +1 -1
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +21 -17
- package/dist/observer/render.js +10 -3
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +51 -35
- package/dist/observer/serve.js +2 -1
- package/dist/observer-app.html +3 -3
- package/dist/routes/computer-use/bundle.js +3 -1
- package/dist/routes/computer-use/fanout-records.js +1 -1
- package/dist/routes/computer-use/participant-execution.js +3 -1
- package/dist/routes/computer-use/participant-model.js +5 -2
- package/dist/routes/computer-use/plan.js +9 -4
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- package/dist/routes/scripted/bundle.js +4 -2
- package/dist/routes/scripted/plan.js +2 -2
- package/dist/routes/scripted/result.d.ts +3 -5
- package/dist/routes/scripted/result.js +16 -22
- package/dist/routes/scripted/scenario.js +4 -4
- package/dist/routes/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.js +2 -0
- package/dist/routes/shared-world/plan.js +6 -2
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/terminal/bundle.js +6 -3
- package/dist/routes/terminal/credentials.d.ts +5 -5
- package/dist/routes/terminal/credentials.js +14 -6
- package/dist/routes/terminal/dry-run.js +8 -12
- package/dist/routes/terminal/ledger.d.ts +3 -3
- package/dist/routes/terminal/ledger.js +6 -6
- package/dist/routes/terminal/live-finish.js +15 -13
- package/dist/routes/terminal/plan.js +12 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- package/dist/routes/terminal/route.js +5 -4
- package/dist/routes/terminal/runtime-auth.d.ts +6 -0
- package/dist/routes/terminal/runtime-auth.js +5 -0
- package/dist/routes/terminal/session.js +1 -1
- package/dist/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +25 -1
- package/dist/run/bundle.d.ts +24 -0
- package/dist/run/display.d.ts +59 -0
- package/dist/run/display.js +106 -0
- package/dist/run/dry-run.js +13 -8
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +12 -1
- package/dist/run/local-only-files.d.ts +13 -0
- package/dist/run/local-only-files.js +35 -0
- package/dist/run/pricing.js +1 -1
- package/dist/run/projection.d.ts +0 -2
- package/dist/run/projection.js +0 -11
- package/dist/run/reclaim-outcomes.d.ts +89 -0
- package/dist/run/reclaim-outcomes.js +213 -0
- package/dist/run/reclaim.d.ts +23 -17
- package/dist/run/reclaim.js +213 -114
- package/dist/run/results.d.ts +10 -20
- package/dist/run/results.js +2 -2
- package/dist/run/run-index.d.ts +11 -1
- package/dist/run/run-index.js +11 -0
- package/dist/run/run-not-found.js +3 -2
- package/dist/run/run.d.ts +34 -18
- package/dist/run/run.js +166 -67
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +140 -0
- package/dist/run/sandbox-receipts.d.ts +20 -5
- package/dist/run/sandbox-receipts.js +56 -7
- package/dist/run/stats.d.ts +5 -0
- package/dist/run/stats.js +8 -4
- package/dist/run/status.d.ts +6 -9
- package/dist/run/status.js +7 -23
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -146
- package/dist/run/streams.d.ts +3 -2
- package/dist/study/agents-section.d.ts +21 -0
- package/dist/study/agents-section.js +86 -0
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +8 -4
- package/dist/study/init-templates.js +0 -95
- package/dist/study/init.js +45 -37
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/local-browser-host.d.ts +11 -0
- package/dist/study/local-browser-host.js +42 -0
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/subject.js +12 -0
- package/dist/study/parse/url-credentials.d.ts +9 -0
- package/dist/study/parse/url-credentials.js +50 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- package/dist/study/preflight-probes.js +2 -1
- package/dist/study/types.d.ts +8 -6
- package/dist/study/url-credentials.d.ts +6 -0
- package/dist/study/url-credentials.js +34 -0
- package/dist/substrates/e2b/peer-install.d.ts +51 -0
- package/dist/substrates/e2b/peer-install.js +161 -0
- package/dist/substrates/e2b/sandbox.d.ts +34 -6
- package/dist/substrates/e2b/sandbox.js +141 -30
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/local/firecracker-desktop.js +3 -2
- package/dist/substrates/local/runtime-host.js +5 -4
- package/dist/substrates/local/runtime.js +5 -4
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui/contract.js +5 -4
- package/dist/tui-app.js +133 -133
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +47 -25
- package/dist/verify/verify.d.ts +3 -0
- package/dist/verify/verify.js +8 -3
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +15 -13
- package/docs/contracts/run-bundle.md +95 -10
- package/docs/contracts/schemas.md +44 -26
- package/docs/contracts/study-analysis.md +45 -0
- package/docs/contracts/study-costs.md +8 -0
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
- package/docs/principles/invariants-and-defaults.md +11 -8
- package/package.json +6 -3
- package/skills/humanish/SKILL.md +38 -20
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// judge the same decoded text.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { readPlainText } from "./plain-text.js";
|
|
7
|
-
import { containsSensitive } from "./redaction.js";
|
|
7
|
+
import { containsSecret, containsSensitive } from "./redaction.js";
|
|
8
8
|
// HTML5 named references that stand for printable ASCII. Letters and digits have only numeric
|
|
9
9
|
// references, which decodeEscapes handles.
|
|
10
10
|
const ASCII_ENTITIES = {
|
|
@@ -57,15 +57,13 @@ export function decodeEscapes(text) {
|
|
|
57
57
|
}
|
|
58
58
|
// Sixteen characters hold twelve bytes, enough for the start of a key. Shorter runs are mostly
|
|
59
59
|
// words and identifiers.
|
|
60
|
-
|
|
60
|
+
// Each open-ended repeat is written `[...]{n}[...]*`: V8 runs `{n,}` with a backtrack entry per
|
|
61
|
+
// character and overflows its stack on a run of several megabytes.
|
|
62
|
+
const BASE64_RUN = /[A-Za-z0-9+/]{16}[A-Za-z0-9+/]*={0,2}/g;
|
|
61
63
|
// The URL-safe alphabet swaps `+/` for `-_`. Only runs that use `-` or `_` need this pass.
|
|
62
|
-
const BASE64URL_RUN = /[A-Za-z0-9_-]{16
|
|
63
|
-
// MIME and PEM wrap base64 at 64 or 76 characters a line. The lookbehind starts a match only at the
|
|
64
|
-
// start of a run, and each lookahead-backreference pair takes a line whole, so a long unwrapped run
|
|
65
|
-
// cannot backtrack.
|
|
66
|
-
const WRAPPED_BASE64 = /(?<![A-Za-z0-9+/])(?=([A-Za-z0-9+/]{16,}))\1(?:[ \t]*\r?\n[ \t]*(?=([A-Za-z0-9+/]{4,}))\2)+={0,2}/g;
|
|
64
|
+
const BASE64URL_RUN = /[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*={0,2}/g;
|
|
67
65
|
// Hex-encoded text, at least sixteen bytes of it.
|
|
68
|
-
const HEX_RUN = /
|
|
66
|
+
const HEX_RUN = /[0-9a-f]{32}[0-9a-f]*/gi;
|
|
69
67
|
// A base64 run that decodes to bytes that are neither text nor an archive is unreadable only past
|
|
70
68
|
// this length. Across 173 real run folders surveyed on 2026-10-01, the longest such run outside
|
|
71
69
|
// observer/index.html was a 69-character URL path in a terminal log; base64 of a 94-byte payload,
|
|
@@ -84,6 +82,61 @@ const ARCHIVE_MAGIC = [
|
|
|
84
82
|
[0x28, 0xb5, 0x2f, 0xfd],
|
|
85
83
|
];
|
|
86
84
|
const CONTROL_CHARACTERS = /[\x00-\x08\x0b\x0c\x0e-\x1f]/;
|
|
85
|
+
// A run such as `com/x/<base64>` from a URL path starts with segments that shift the alignment of
|
|
86
|
+
// the base64 after them. A run up to this long is also decoded one slash-separated piece at a time,
|
|
87
|
+
// and from just after each slash in its first SLASH_START_SPAN characters, for base64 that holds a
|
|
88
|
+
// slash itself. Longer runs are images, fonts and archives, not URLs.
|
|
89
|
+
const MAX_SLASH_START_RUN = 4096;
|
|
90
|
+
const SLASH_START_SPAN = 64;
|
|
91
|
+
function isBase64Code(code) {
|
|
92
|
+
return ((code >= 0x30 && code <= 0x39) ||
|
|
93
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
94
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
95
|
+
code === 0x2b ||
|
|
96
|
+
code === 0x2f);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Base64 that MIME and PEM wrap at 64 or 76 characters a line: a run of 16 or more characters that
|
|
100
|
+
* ends its line, then the leading run of 4 or more on each following line, for as long as a line
|
|
101
|
+
* holds nothing else. Read line by line, so a long wrapped block costs linear time and no regex
|
|
102
|
+
* stack.
|
|
103
|
+
*/
|
|
104
|
+
function wrappedBase64Runs(text) {
|
|
105
|
+
const runs = [];
|
|
106
|
+
let current = [];
|
|
107
|
+
const flush = () => {
|
|
108
|
+
if (current.length > 1)
|
|
109
|
+
runs.push(current.join(""));
|
|
110
|
+
current = [];
|
|
111
|
+
};
|
|
112
|
+
for (const raw of text.split("\n")) {
|
|
113
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
114
|
+
let end = line.length;
|
|
115
|
+
while (end > 0 && (line[end - 1] === " " || line[end - 1] === "\t"))
|
|
116
|
+
end -= 1;
|
|
117
|
+
let start = 0;
|
|
118
|
+
while (start < end && (line[start] === " " || line[start] === "\t"))
|
|
119
|
+
start += 1;
|
|
120
|
+
if (current.length > 0) {
|
|
121
|
+
let lead = start;
|
|
122
|
+
while (lead < end && isBase64Code(line.charCodeAt(lead)))
|
|
123
|
+
lead += 1;
|
|
124
|
+
if (lead - start >= 4) {
|
|
125
|
+
current.push(line.slice(start, lead));
|
|
126
|
+
if (lead === end)
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
flush();
|
|
130
|
+
}
|
|
131
|
+
let tail = end;
|
|
132
|
+
while (tail > 0 && isBase64Code(line.charCodeAt(tail - 1)))
|
|
133
|
+
tail -= 1;
|
|
134
|
+
if (end - tail >= 16)
|
|
135
|
+
current = [line.slice(tail, end)];
|
|
136
|
+
}
|
|
137
|
+
flush();
|
|
138
|
+
return runs;
|
|
139
|
+
}
|
|
87
140
|
// Base64 inside base64 is read this many levels deep.
|
|
88
141
|
const MAX_DEPTH = 2;
|
|
89
142
|
function startsWithArchive(bytes) {
|
|
@@ -136,9 +189,11 @@ function decodeTransferEscapes(text) {
|
|
|
136
189
|
.replace(/=\r?\n/g, "")
|
|
137
190
|
.replace(/=([0-9A-F]{2})/g, (match, hex) => codePoint(Number.parseInt(hex, 16), match));
|
|
138
191
|
}
|
|
192
|
+
const matcherOf = (options) => options.secretsOnly === true ? containsSecret : containsSensitive;
|
|
139
193
|
const CLEAN = Object.freeze({ sensitive: false, opaque: false });
|
|
140
194
|
const SENSITIVE = Object.freeze({ sensitive: true, opaque: false });
|
|
141
195
|
function inspectDecoded(bytes, run, options, depth) {
|
|
196
|
+
const matches = matcherOf(options);
|
|
142
197
|
if (startsWithArchive(bytes))
|
|
143
198
|
return { sensitive: false, opaque: options.allowOpaqueBase64 !== true };
|
|
144
199
|
const plain = readPlainText(bytes);
|
|
@@ -147,12 +202,10 @@ function inspectDecoded(bytes, run, options, depth) {
|
|
|
147
202
|
if (depth < MAX_DEPTH)
|
|
148
203
|
return scanEncodedText(inner, options, depth + 1);
|
|
149
204
|
const innerDecoded = decodeEscapes(inner);
|
|
150
|
-
return
|
|
151
|
-
? SENSITIVE
|
|
152
|
-
: CLEAN;
|
|
205
|
+
return matches(inner) || (innerDecoded !== inner && matches(innerDecoded)) ? SENSITIVE : CLEAN;
|
|
153
206
|
}
|
|
154
207
|
// A key next to a few binary bytes is still a printable stretch.
|
|
155
|
-
if (
|
|
208
|
+
if (matches(printableStretches(bytes)))
|
|
156
209
|
return SENSITIVE;
|
|
157
210
|
return {
|
|
158
211
|
sensitive: false,
|
|
@@ -168,28 +221,36 @@ function inspectDecoded(bytes, run, options, depth) {
|
|
|
168
221
|
* is opaque unless the caller allows opaque runs.
|
|
169
222
|
*/
|
|
170
223
|
export function scanEncodedText(text, options = {}, depth = 0) {
|
|
224
|
+
const matches = matcherOf(options);
|
|
171
225
|
const decoded = decodeEscapes(text);
|
|
172
226
|
const expanded = decodeTransferEscapes(decoded);
|
|
173
227
|
// A decoding that returns the same string would match the same way, so it is not matched again.
|
|
174
|
-
if (
|
|
175
|
-
(decoded !== text &&
|
|
176
|
-
(expanded !== decoded &&
|
|
228
|
+
if (matches(text) ||
|
|
229
|
+
(decoded !== text && matches(decoded)) ||
|
|
230
|
+
(expanded !== decoded && matches(expanded)))
|
|
177
231
|
return SENSITIVE;
|
|
178
232
|
const runs = [];
|
|
233
|
+
// A piece or a run decoded from a slash only adds findings; its bytes are not judged opaque.
|
|
234
|
+
const slashStarts = [];
|
|
179
235
|
for (const [match] of expanded.matchAll(BASE64_RUN)) {
|
|
180
236
|
// Hex digests and ids decode to noise; the hex pass below reads hex as hex.
|
|
181
237
|
if (/^[0-9a-f]+$/i.test(match.replace(/=+$/, "")))
|
|
182
238
|
continue;
|
|
183
239
|
runs.push({ run: match, bytes: Buffer.from(match, "base64") });
|
|
240
|
+
if (match.length > MAX_SLASH_START_RUN)
|
|
241
|
+
continue;
|
|
242
|
+
for (const piece of match.split("/"))
|
|
243
|
+
if (piece.length >= 16 && piece.length < match.length)
|
|
244
|
+
slashStarts.push(Buffer.from(piece, "base64"));
|
|
245
|
+
for (let slash = match.indexOf("/"); slash !== -1 && slash < SLASH_START_SPAN && match.length - slash > 16; slash = match.indexOf("/", slash + 1))
|
|
246
|
+
slashStarts.push(Buffer.from(match.slice(slash + 1), "base64"));
|
|
184
247
|
}
|
|
185
248
|
for (const [match] of expanded.matchAll(BASE64URL_RUN)) {
|
|
186
249
|
if (/[-_]/.test(match))
|
|
187
250
|
runs.push({ run: match, bytes: Buffer.from(match, "base64url") });
|
|
188
251
|
}
|
|
189
|
-
for (const
|
|
190
|
-
const run = match.replace(/\s+/g, "");
|
|
252
|
+
for (const run of wrappedBase64Runs(expanded))
|
|
191
253
|
runs.push({ run, bytes: Buffer.from(run, "base64") });
|
|
192
|
-
}
|
|
193
254
|
let opaque = false;
|
|
194
255
|
for (const { run, bytes } of runs) {
|
|
195
256
|
const result = inspectDecoded(bytes, run, options, depth);
|
|
@@ -197,6 +258,9 @@ export function scanEncodedText(text, options = {}, depth = 0) {
|
|
|
197
258
|
return result;
|
|
198
259
|
opaque ||= result.opaque;
|
|
199
260
|
}
|
|
261
|
+
for (const bytes of slashStarts)
|
|
262
|
+
if (inspectDecoded(bytes, "", { ...options, allowOpaqueBase64: true }, depth).sensitive)
|
|
263
|
+
return SENSITIVE;
|
|
200
264
|
for (const [match] of expanded.matchAll(HEX_RUN)) {
|
|
201
265
|
const plain = readPlainText(Buffer.from(match, "hex"));
|
|
202
266
|
if (plain.ok && depth < MAX_DEPTH && scanEncodedText(plain.text, options, depth + 1).sensitive)
|
|
@@ -207,7 +271,7 @@ export function scanEncodedText(text, options = {}, depth = 0) {
|
|
|
207
271
|
// Bump when scanEncodedText, decodeEscapes or the sensitive patterns change what they return. The
|
|
208
272
|
// cache lives in one process, so the version guards results across a hot reload or a test that
|
|
209
273
|
// swaps the scanner.
|
|
210
|
-
const ENCODED_SCAN_VERSION =
|
|
274
|
+
const ENCODED_SCAN_VERSION = 2;
|
|
211
275
|
// Distinct files one process verifies in a burst (a run's files, a serve library's runs).
|
|
212
276
|
const SCAN_CACHE_LIMIT = 256;
|
|
213
277
|
const scanCache = new Map();
|
|
@@ -222,6 +286,7 @@ export function scanEncodedTextCached(text, options = {}) {
|
|
|
222
286
|
const key = [
|
|
223
287
|
ENCODED_SCAN_VERSION,
|
|
224
288
|
options.allowOpaqueBase64 === true ? "opaque-allowed" : "opaque-unscanned",
|
|
289
|
+
options.secretsOnly === true ? "secrets-only" : "secrets-and-paths",
|
|
225
290
|
createHash("sha256").update(Buffer.from(text, "utf16le")).digest("hex"),
|
|
226
291
|
].join(":");
|
|
227
292
|
const cached = scanCache.get(key);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Every pattern containsSensitive tests. A test pins each to ASCII, which scanEncodedText relies on. */
|
|
2
2
|
export declare function sensitivePatterns(): readonly RegExp[];
|
|
3
|
+
/** True if the text contains a secret-shaped token. Local paths do not count. */
|
|
4
|
+
export declare function containsSecret(text: string): boolean;
|
|
3
5
|
/** True if the text contains any secret-shaped token or known local path. */
|
|
4
6
|
export declare function containsSensitive(text: string): boolean;
|
|
5
7
|
/**
|
|
@@ -3,26 +3,113 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { PNG } from "pngjs";
|
|
5
5
|
import { SCREENSHOT_MAX_WIDTH_CAP, pngDecodeRefusal } from "./image.js";
|
|
6
|
+
import { isRecord } from "../run/type-guards.js";
|
|
6
7
|
// Single source of truth for public-safety redaction patterns. Producers and the verify gate
|
|
7
8
|
// both use these, so the denylist cannot drift between them. See docs/contracts/policy.md for
|
|
8
9
|
// the enforcement-scope policy.
|
|
10
|
+
// A whole value that stands in for a secret rather than being one, as written or percent-encoded:
|
|
11
|
+
// a redaction marker, a shell or template variable, a placeholder in angle brackets, or a mask of
|
|
12
|
+
// asterisks. A context pattern skips a value only when one of these is all of it, up to a
|
|
13
|
+
// character that ends that pattern's value, so a password that merely starts with `*` or `$`
|
|
14
|
+
// still counts.
|
|
15
|
+
const PLACEHOLDER = [
|
|
16
|
+
String.raw `\[REDACTED_[A-Z0-9_]{1,40}\]`,
|
|
17
|
+
String.raw `%5[Bb]REDACTED_[A-Z0-9_]{1,40}%5[Dd]`,
|
|
18
|
+
String.raw `\$\{[A-Za-z_][A-Za-z0-9_]{0,63}\}`,
|
|
19
|
+
String.raw `%24%7[Bb][A-Za-z_][A-Za-z0-9_]{0,63}%7[Dd]`,
|
|
20
|
+
String.raw `\$[A-Za-z_][A-Za-z0-9_]{0,63}`,
|
|
21
|
+
String.raw `%24[A-Za-z_][A-Za-z0-9_]{0,63}`,
|
|
22
|
+
String.raw `\{\{[A-Za-z0-9_. -]{1,64}\}\}`,
|
|
23
|
+
String.raw `<[A-Za-z0-9_. -]{1,64}>`,
|
|
24
|
+
String.raw `%3[Cc][A-Za-z0-9_.-]{1,64}%3[Ee]`,
|
|
25
|
+
String.raw `\*{3,64}`,
|
|
26
|
+
String.raw `(?:%2[Aa]){3,64}`,
|
|
27
|
+
].join("|");
|
|
28
|
+
const notAPlaceholder = (valueEnd) => String.raw `(?!(?:${PLACEHOLDER})(?:${valueEnd}|$))`;
|
|
29
|
+
// An optional quote around a name or a value, also as JSON writes it inside other JSON strings,
|
|
30
|
+
// four levels deep (\", \\\", ...).
|
|
31
|
+
const QUOTE = String.raw `(?:\\{0,15}["'])?`;
|
|
32
|
+
// A JSON number, which a credential-named key can hold and redaction must not turn into a string.
|
|
33
|
+
// The bounds are far past any number a run writes and keep the lookahead linear.
|
|
34
|
+
const NOT_A_NUMBER = String.raw `(?!-?[0-9]{1,512}(?:\.[0-9]{0,512})?(?:[eE][+-]?[0-9]{1,64})?(?:[\s,}\]]|$))`;
|
|
35
|
+
// The characters that end each context pattern's value.
|
|
36
|
+
const USERINFO_END = String.raw `[\s@/?#"'<>\\]`;
|
|
37
|
+
const PARAMETER_END = String.raw `[\s&#"'<>\\]`;
|
|
38
|
+
const VARIABLE_END = String.raw `[\s"'\\,;&]`;
|
|
39
|
+
// Query and fragment parameters that carry a credential. `page_token` and other cursors match too;
|
|
40
|
+
// their values are long and opaque, so a reader cannot tell them from a credential either.
|
|
41
|
+
const CREDENTIAL_PARAMETER = [
|
|
42
|
+
String.raw `(?:[a-z0-9]{1,24}[_-]){0,2}token`,
|
|
43
|
+
String.raw `api[_-]?key`,
|
|
44
|
+
String.raw `(?:client[_-])?secret`,
|
|
45
|
+
"password",
|
|
46
|
+
"passwd",
|
|
47
|
+
"pwd",
|
|
48
|
+
"sig",
|
|
49
|
+
"signature",
|
|
50
|
+
String.raw `session[_-]?id`,
|
|
51
|
+
"jwt",
|
|
52
|
+
"x-vercel-protection-bypass",
|
|
53
|
+
"_vercel_share",
|
|
54
|
+
String.raw `x-amz-(?:signature|security-token|credential)`,
|
|
55
|
+
String.raw `x-goog-(?:signature|credential)`,
|
|
56
|
+
"auth",
|
|
57
|
+
].join("|");
|
|
58
|
+
// Upper-case variable names that hold a credential, as an env file, a shell or a JSON dump writes
|
|
59
|
+
// them: GITHUB_TOKEN, VERCEL_TOKEN, AWS_SECRET_ACCESS_KEY, DATABASE_PASSWORD.
|
|
60
|
+
const CREDENTIAL_VARIABLE = String.raw `\b(?:[A-Z][A-Z0-9_]{0,48}_)?(?:TOKEN|SECRET|SECRET_KEY|PASSWORD|PASSWD|API_KEY|APIKEY|ACCESS_KEY|PRIVATE_KEY)`;
|
|
61
|
+
// A pattern that matches a credential by its context puts the context in a `keep` group, which
|
|
62
|
+
// redaction leaves in place: `?token=[REDACTED_SECRET]`. Context goes in a group and not in a
|
|
63
|
+
// lookbehind, because a pattern that starts with a lookbehind is tried at every position of the
|
|
64
|
+
// text and scans many times slower. An open-ended repeat is written `[...]{n}[...]*`: V8 runs
|
|
65
|
+
// `{n,}` with a backtrack entry per character and overflows its stack on a run of several
|
|
66
|
+
// megabytes, and a star loop does not.
|
|
9
67
|
const SECRET_PATTERNS = [
|
|
10
|
-
/\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20
|
|
11
|
-
/\bsk-ant-[A-Za-z0-9_-]{20
|
|
12
|
-
/\be2b_[A-Za-z0-9]{16
|
|
13
|
-
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20
|
|
14
|
-
/\bgithub_pat_[A-Za-z0-9_]{20
|
|
15
|
-
/\
|
|
16
|
-
/\
|
|
17
|
-
|
|
18
|
-
/\
|
|
19
|
-
/\
|
|
20
|
-
/\
|
|
68
|
+
/\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*\b/g,
|
|
69
|
+
/\bsk-ant-[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*\b/g,
|
|
70
|
+
/\be2b_[A-Za-z0-9]{16}[A-Za-z0-9]*\b/g,
|
|
71
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20}[A-Za-z0-9_]*\b/g,
|
|
72
|
+
/\bgithub_pat_[A-Za-z0-9_]{20}[A-Za-z0-9_]*\b/g,
|
|
73
|
+
/\bglpat-[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*/g,
|
|
74
|
+
/\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
75
|
+
// Vercel personal, integration, app access, app refresh and API key tokens.
|
|
76
|
+
/\bvc[pciark]_[A-Za-z0-9]{24}[A-Za-z0-9]*\b/g,
|
|
77
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
|
|
78
|
+
/\bAIza[0-9A-Za-z_-]{20}[0-9A-Za-z_-]*\b/g,
|
|
79
|
+
/\bya29\.[0-9A-Za-z_-]{20}[0-9A-Za-z_-]*/g,
|
|
80
|
+
/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16}[A-Za-z0-9]*\b/g,
|
|
81
|
+
/\bwhsec_[A-Za-z0-9]{24}[A-Za-z0-9]*\b/g,
|
|
82
|
+
/\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g,
|
|
83
|
+
/\bhf_[A-Za-z0-9]{30}[A-Za-z0-9]*\b/g,
|
|
84
|
+
/\bxox[abeoprs]-[A-Za-z0-9-]{20}[A-Za-z0-9-]*\b/g,
|
|
85
|
+
/\bxapp-[0-9]-[A-Za-z0-9-]{20}[A-Za-z0-9-]*\b/g,
|
|
86
|
+
/\bhooks\.slack\.com\/(?:services|workflows|triggers)\/[A-Za-z0-9_/-]{20}[A-Za-z0-9_/-]*/g,
|
|
87
|
+
// A JWT starts only where a token run starts, so a run of `eyJ-` repeated is read once.
|
|
88
|
+
/eyJ(?<![A-Za-z0-9_-]eyJ)[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{10}[A-Za-z0-9_-]*\b/g,
|
|
21
89
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----/g,
|
|
22
|
-
/\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^:@/\s]+:[^@/\s]
|
|
23
|
-
/_authToken\s*=\s*[A-Za-z0-9._~+/=-]{20
|
|
24
|
-
/\bBearer\s+[A-Za-z0-9._~+/-]{24
|
|
25
|
-
|
|
90
|
+
/\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^:@/\s"'<>\\]+:[^@/\s"'<>\\]+@[^\s"'<>\\]+/g,
|
|
91
|
+
/_authToken\s*=\s*[A-Za-z0-9._~+/=-]{20}[A-Za-z0-9._~+/=-]*/g,
|
|
92
|
+
/\bBearer\s+[A-Za-z0-9._~+/-]{24}[A-Za-z0-9._~+/-]*\b/g,
|
|
93
|
+
new RegExp(String.raw `(?<keep>\bAuthorization${QUOTE}[ \t]{0,4}[:=][ \t]{0,4}${QUOTE}Basic[ \t]{1,4})[A-Za-z0-9+/]{16}[A-Za-z0-9+/]*={0,2}`, "gi"),
|
|
94
|
+
// The password in a URL's userinfo, any scheme, with or without a user name:
|
|
95
|
+
// https://user:password@host, https://:password@host.
|
|
96
|
+
new RegExp(String.raw `(?<keep>\b[a-z][a-z0-9+.-]{0,31}:\/\/[^\s:@/?#"'<>\\[\]]{0,256}:)${notAPlaceholder(USERINFO_END)}[^\s@/?#"'<>\\]{1,256}(?=@)`, "gi"),
|
|
97
|
+
// A credential-named query or fragment parameter's value, up to the next delimiter, so a
|
|
98
|
+
// password with punctuation in it counts whole. `;` also follows an HTML `&`.
|
|
99
|
+
new RegExp(String.raw `(?<keep>[?&#;](?:${CREDENTIAL_PARAMETER})=)${notAPlaceholder(PARAMETER_END)}[^\s&#"'<>\\]{16}[^\s&#"'<>\\]*`, "gi"),
|
|
100
|
+
// An Amazon Web Services secret access key next to its name, as a credentials file, an env
|
|
101
|
+
// line or a temporary-credentials JSON response writes it. The key is 40 characters, no prefix.
|
|
102
|
+
new RegExp(String.raw `(?<keep>\b(?:aws_?)?secret_?access_?key${QUOTE}[ \t]{0,4}[:=][ \t]{0,4}${QUOTE})${NOT_A_NUMBER}[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+=])`, "gi"),
|
|
103
|
+
// A credential-named variable set to a value of 16 characters or more, up to the next space,
|
|
104
|
+
// quote or separator, with a digit and a letter in its first 256 characters. Words, placeholders
|
|
105
|
+
// such as humanish-egress-auth-placeholder and JSON numbers do not count. The lookaheads are
|
|
106
|
+
// bounded, so a long run of `TOKEN=` costs linear time.
|
|
107
|
+
new RegExp(String.raw `(?<keep>${CREDENTIAL_VARIABLE}${QUOTE}[ \t]{0,4}[:=][ \t]{0,4}${QUOTE})${notAPlaceholder(VARIABLE_END)}${NOT_A_NUMBER}(?=[^\s"'\\,;&]{0,255}[0-9])(?=[^\s"'\\,;&]{0,255}[A-Za-z])[^\s"'\\,;&]{16}[^\s"'\\,;&]*`, "g"),
|
|
108
|
+
// Any URL on an E2B host: a sandbox host names its sandbox, and a stream URL carries its auth
|
|
109
|
+
// key. One URL is exempt: E2B's dashboard, as the SDK's missing-key error names it
|
|
110
|
+
// (https://e2b.dev/dashboard?tab=keys), with no userinfo, no path below /dashboard, no query but
|
|
111
|
+
// one short `tab`, and only closing punctuation before the next space or ")".
|
|
112
|
+
/(?!https:\/\/(?:www\.)?e2b\.dev\/dashboard\/?(?:\?tab=[a-z]{1,16})?[.,;:!?'"]*(?:[)\s]|$))https?:\/\/[^/\s]*e2b[^)\s]+/gi,
|
|
26
113
|
/BEGIN (RSA|OPENSSH|PRIVATE) KEY/gi,
|
|
27
114
|
];
|
|
28
115
|
const LOCAL_PATH_PATTERNS = [
|
|
@@ -32,6 +119,12 @@ const LOCAL_PATH_PATTERNS = [
|
|
|
32
119
|
[/\/tmp\/[^\s"'`<>)]*/g, "[REDACTED_LOCAL_PATH]"],
|
|
33
120
|
[/\/Users\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>)]*)?/g, "[REDACTED_LOCAL_PATH]"],
|
|
34
121
|
[/\/home\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>)]*)?/g, "[REDACTED_RUNTIME_PATH]"],
|
|
122
|
+
// A Windows profile path, with `\`, `\\` (JSON-escaped) or `/` between segments. A match never
|
|
123
|
+
// ends in a backslash, so it cannot swallow the escape of a closing quote.
|
|
124
|
+
[
|
|
125
|
+
/\b[A-Za-z]:(?:\\\\|\\|\/)Users(?:\\\\|\\|\/)[^\\/\s"'`<>)]+[^\s"'`<>)]*/g,
|
|
126
|
+
"[REDACTED_LOCAL_PATH]",
|
|
127
|
+
],
|
|
35
128
|
];
|
|
36
129
|
/** Every pattern containsSensitive tests. A test pins each to ASCII, which scanEncodedText relies on. */
|
|
37
130
|
export function sensitivePatterns() {
|
|
@@ -43,10 +136,13 @@ function matchesPattern(pattern, text) {
|
|
|
43
136
|
pattern.lastIndex = 0;
|
|
44
137
|
return pattern.test(text);
|
|
45
138
|
}
|
|
139
|
+
/** True if the text contains a secret-shaped token. Local paths do not count. */
|
|
140
|
+
export function containsSecret(text) {
|
|
141
|
+
return SECRET_PATTERNS.some((pattern) => matchesPattern(pattern, text));
|
|
142
|
+
}
|
|
46
143
|
/** True if the text contains any secret-shaped token or known local path. */
|
|
47
144
|
export function containsSensitive(text) {
|
|
48
|
-
return (
|
|
49
|
-
LOCAL_PATH_PATTERNS.some(([pattern]) => matchesPattern(pattern, text)));
|
|
145
|
+
return (containsSecret(text) || LOCAL_PATH_PATTERNS.some(([pattern]) => matchesPattern(pattern, text)));
|
|
50
146
|
}
|
|
51
147
|
// A local-path match may end in the backslashes escaping its closing quote in
|
|
52
148
|
// serialized JSON (including JSON nested inside a terminal event). Keep that
|
|
@@ -142,15 +238,21 @@ export function redactSandboxIds(value) {
|
|
|
142
238
|
entries.some(([key, child]) => child !== value[key]);
|
|
143
239
|
return changed ? Object.fromEntries(entries) : value;
|
|
144
240
|
}
|
|
241
|
+
/** Each secret replaced by [REDACTED_SECRET], with the context a pattern keeps left in place. */
|
|
242
|
+
function redactSecrets(text) {
|
|
243
|
+
return SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, (...args) => {
|
|
244
|
+
const groups = args.at(-1);
|
|
245
|
+
const keep = isRecord(groups) && typeof groups.keep === "string" ? groups.keep : "";
|
|
246
|
+
return `${keep}[REDACTED_SECRET]`;
|
|
247
|
+
}), text);
|
|
248
|
+
}
|
|
145
249
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
146
250
|
export function redactText(text) {
|
|
147
|
-
|
|
148
|
-
return redactLocalPaths(withoutSecrets);
|
|
251
|
+
return redactLocalPaths(redactSecrets(text));
|
|
149
252
|
}
|
|
150
253
|
/** Redact every sensitive match (secrets and paths) to a single [REDACTED_SECRET] label. */
|
|
151
254
|
export function redactToSecretLabel(text) {
|
|
152
|
-
|
|
153
|
-
return redactLocalPaths(withoutSecrets, "[REDACTED_SECRET]");
|
|
255
|
+
return redactLocalPaths(redactSecrets(text), "[REDACTED_SECRET]");
|
|
154
256
|
}
|
|
155
257
|
function canonicalizePath(value) {
|
|
156
258
|
const resolved = path.resolve(value);
|
|
@@ -18,6 +18,7 @@ import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
|
|
|
18
18
|
import {} from "../run/bundle.js";
|
|
19
19
|
import { runIdOf, isPathInside, prepareRunArtifactPaths, validatePreparedRunArtifactPaths, } from "../run/paths.js";
|
|
20
20
|
import { isAnalysisRecordPath } from "../analysis/sharing.js";
|
|
21
|
+
import { LOCAL_ONLY_RUN_FILES } from "../run/local-only-files.js";
|
|
21
22
|
import { assertPreparedSelectedOutputDirectory, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
22
23
|
const DERIVATION_SCHEMA = "humanish.redacted-derivation.v1";
|
|
23
24
|
const MAX_FILES = 10_000;
|
|
@@ -36,8 +37,7 @@ const OMITTED = new Map([
|
|
|
36
37
|
["observer/index.html", "regenerated Observer"],
|
|
37
38
|
["observer/observer-data.json", "regenerated Observer projection"],
|
|
38
39
|
["observer/study-analysis.json", "analysis must be regenerated against derivative evidence"],
|
|
39
|
-
|
|
40
|
-
["sandbox-receipts.ndjson", "operational journal does not confer a derivative resource lease"],
|
|
40
|
+
...LOCAL_ONLY_RUN_FILES,
|
|
41
41
|
]);
|
|
42
42
|
const PNG_MAGIC = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
43
43
|
function hash(bytes) {
|
package/dist/keys/env-file.d.ts
CHANGED
|
@@ -11,5 +11,9 @@ export interface EnvFileLoadResult {
|
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
/** Records in `env` the names a `--dotenv` file set there. */
|
|
15
|
+
export declare function recordDotenvNames(env: NodeJS.ProcessEnv, names: readonly string[]): void;
|
|
16
|
+
/** The names `--dotenv` set in `env`, as recordDotenvNames recorded them. */
|
|
17
|
+
export declare function dotenvSetNames(env: Readonly<Record<string, string | undefined>>): Set<string>;
|
|
14
18
|
export declare function loadEnvFile(cwd: string, envFile: string, env?: NodeJS.ProcessEnv): Promise<EnvFileLoadResult>;
|
|
15
19
|
export {};
|
package/dist/keys/env-file.js
CHANGED
|
@@ -2,6 +2,23 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const ENV_FILE_RESULT_SCHEMA = "humanish.env-file-result.v1";
|
|
4
4
|
const envNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5
|
+
/**
|
|
6
|
+
* Names a `--dotenv` file set, kept as a comma list in the environment the values went into, so a
|
|
7
|
+
* process humanish starts with that environment (a run the TUI launches) knows them too. A
|
|
8
|
+
* host-side participant (the Claude Code local agent) never gets these names, even ones it would
|
|
9
|
+
* otherwise keep, such as a proxy.
|
|
10
|
+
*/
|
|
11
|
+
const DOTENV_NAMES = "HUMANISH_DOTENV_NAMES";
|
|
12
|
+
/** Records in `env` the names a `--dotenv` file set there. */
|
|
13
|
+
export function recordDotenvNames(env, names) {
|
|
14
|
+
if (names.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
env[DOTENV_NAMES] = [...new Set([...dotenvSetNames(env), ...names])].join(",");
|
|
17
|
+
}
|
|
18
|
+
/** The names `--dotenv` set in `env`, as recordDotenvNames recorded them. */
|
|
19
|
+
export function dotenvSetNames(env) {
|
|
20
|
+
return new Set((env[DOTENV_NAMES] ?? "").split(",").filter((name) => name.length > 0));
|
|
21
|
+
}
|
|
5
22
|
export async function loadEnvFile(cwd, envFile, env = process.env) {
|
|
6
23
|
const resolvedCwd = path.resolve(cwd);
|
|
7
24
|
const envPath = path.resolve(resolvedCwd, envFile);
|
|
@@ -21,6 +21,7 @@ import { chmodSync, closeSync, constants as fsConstants, existsSync, ftruncateSy
|
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import { loadEnvFile } from "./env-file.js";
|
|
24
|
+
import { cli } from "../cli/invocation.js";
|
|
24
25
|
/** The only names implicit discovery may fill (and `humanish keys set` may store). Everything
|
|
25
26
|
* else in an overlay/store file is ignored-and-named: a repo-planted NODE_OPTIONS/LD_PRELOAD
|
|
26
27
|
* must never enter process env off a file the operator did not explicitly pass (an explicit
|
|
@@ -235,16 +236,16 @@ function storeLabel(env, deps) {
|
|
|
235
236
|
export function missingKeyHint(name) {
|
|
236
237
|
switch (name) {
|
|
237
238
|
case "E2B_API_KEY":
|
|
238
|
-
return
|
|
239
|
+
return `run \`e2b auth login\`, or \`${cli("keys set e2b")}\``;
|
|
239
240
|
case "GH_TOKEN":
|
|
240
241
|
case "GITHUB_TOKEN":
|
|
241
|
-
return
|
|
242
|
+
return `run \`gh auth login\`, or \`${cli("keys set github")}\``;
|
|
242
243
|
case "OPENAI_API_KEY":
|
|
243
|
-
return
|
|
244
|
+
return `run \`${cli("keys set openai")}\``;
|
|
244
245
|
case "ANTHROPIC_API_KEY":
|
|
245
|
-
return
|
|
246
|
+
return `run \`${cli("keys set anthropic")}\``;
|
|
246
247
|
default:
|
|
247
|
-
return `run
|
|
248
|
+
return `run \`${cli(`keys set ${name}`)}\``;
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
/** One shared suffix for *_KEYS_MISSING messages: where discovery looked, and what fills each
|
package/dist/observer/data.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ActorEnding } from "../actors/stop-cause.js";
|
|
2
2
|
import type { RunBundle, RunCostSummary, RunEvent, RunSimulation } from "../run/bundle.js";
|
|
3
|
+
import { type RunDisplay } from "../run/display.js";
|
|
3
4
|
import type { RunStream, RunStreamKind } from "../run/streams.js";
|
|
4
5
|
export declare const OBSERVER_DATA_SCHEMA = "humanish.observer-data.v1";
|
|
5
6
|
interface ObserverArtifactLink {
|
|
@@ -20,7 +21,14 @@ export interface ObserverData {
|
|
|
20
21
|
run: {
|
|
21
22
|
runId: string;
|
|
22
23
|
mode: RunBundle["mode"];
|
|
24
|
+
/** The participants' verdict. `display` says whether the run passed. */
|
|
23
25
|
status: RunBundle["review"]["verdict"];
|
|
26
|
+
/**
|
|
27
|
+
* How the run reads (runDisplay in src/run/display.ts): `passed` only when the verdict is pass
|
|
28
|
+
* and run.json's outcome does not say the run failed. Absent in observer-data.json written
|
|
29
|
+
* before it existed.
|
|
30
|
+
*/
|
|
31
|
+
display?: RunDisplay;
|
|
24
32
|
title: string;
|
|
25
33
|
createdAt: string;
|
|
26
34
|
simCount: number;
|
|
@@ -109,7 +117,12 @@ interface ObserverLaneGroup {
|
|
|
109
117
|
surface?: string;
|
|
110
118
|
caseGroup?: string;
|
|
111
119
|
}
|
|
112
|
-
|
|
120
|
+
/**
|
|
121
|
+
* The Observer's projection of a bundle. `statusRecord` is the run's status.json, read for a
|
|
122
|
+
* bundle with no outcome in run.json: a run in progress, interrupted, or recorded before run.json
|
|
123
|
+
* carried its outcome.
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildObserverData(bundle: RunBundle, generatedAt?: string, statusRecord?: unknown): ObserverData;
|
|
113
126
|
/** Refresh presentation from recorded actor evidence, including older exported snapshots. */
|
|
114
127
|
export declare function withObserverEndings(data: ObserverData): ObserverData;
|
|
115
128
|
export {};
|
package/dist/observer/data.js
CHANGED
|
@@ -2,6 +2,7 @@ import { actorEnding } from "../actors/stop-cause.js";
|
|
|
2
2
|
import { isCommsReceivingEvidence, receivingAnalysisContext } from "../comms/receiving-evidence.js";
|
|
3
3
|
import { formatParticipantOutcomes, formatRunTaskFunnel, participantOutcomeDetails, withCuaReviewProvenance, } from "../run/outcomes.js";
|
|
4
4
|
import { cuaGoalSource, CUA_COMPLETION_NOTE } from "../actors/goal-source.js";
|
|
5
|
+
import { bundleDisplayFacts, runDisplay } from "../run/display.js";
|
|
5
6
|
export const OBSERVER_DATA_SCHEMA = "humanish.observer-data.v1";
|
|
6
7
|
/** Discard a forged or stale runtime grant before projecting persisted evidence. */
|
|
7
8
|
export function recordedStreamEmbed(embed) {
|
|
@@ -19,7 +20,12 @@ const allKinds = [
|
|
|
19
20
|
"artifact",
|
|
20
21
|
"summary",
|
|
21
22
|
];
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The Observer's projection of a bundle. `statusRecord` is the run's status.json, read for a
|
|
25
|
+
* bundle with no outcome in run.json: a run in progress, interrupted, or recorded before run.json
|
|
26
|
+
* carried its outcome.
|
|
27
|
+
*/
|
|
28
|
+
export function buildObserverData(bundle, generatedAt = new Date().toISOString(), statusRecord) {
|
|
23
29
|
const byKind = Object.fromEntries(allKinds.map((kind) => [kind, 0]));
|
|
24
30
|
const events = [...(bundle.events ?? [])];
|
|
25
31
|
// Analysis cites this source-backed aggregate as context, not a participant
|
|
@@ -32,14 +38,17 @@ export function buildObserverData(bundle, generatedAt = new Date().toISOString()
|
|
|
32
38
|
const streams = (bundle.streams ?? []).map((stream) => {
|
|
33
39
|
const sim = bundle.simulations.find((candidate) => candidate.id === stream.simId) ??
|
|
34
40
|
fallbackSimulation(bundle, stream);
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// The judge's status for the participant wins where the run recorded one, so a participant
|
|
42
|
+
// the verdict counted as blocked or as never engaging shows that way here. A bundle written
|
|
43
|
+
// before the judge's status was recorded keeps the typed-outcome reading: a natural session
|
|
44
|
+
// that finished while declaring a blocker. Never turn an active or failed harness into a
|
|
45
|
+
// participant outcome.
|
|
46
|
+
const status = stream.judgedStatus ??
|
|
47
|
+
((stream.status === "passed" || stream.status === "complete") &&
|
|
48
|
+
stream.actor?.completionReason === "goal_satisfied" &&
|
|
49
|
+
stream.actor.declaredOutcome === "blocked"
|
|
50
|
+
? "blocked"
|
|
51
|
+
: stream.status);
|
|
43
52
|
byKind[stream.kind] += 1;
|
|
44
53
|
return {
|
|
45
54
|
...stream,
|
|
@@ -77,6 +86,7 @@ export function buildObserverData(bundle, generatedAt = new Date().toISOString()
|
|
|
77
86
|
runId: bundle.runId,
|
|
78
87
|
mode: bundle.mode,
|
|
79
88
|
status: bundle.review.verdict,
|
|
89
|
+
display: runDisplay(bundleDisplayFacts(bundle, statusRecord)),
|
|
80
90
|
title: `${bundle.scenario.title} - ${bundle.persona.name}`,
|
|
81
91
|
createdAt: bundle.createdAt,
|
|
82
92
|
simCount: bundle.simCount ?? bundle.simulations.length,
|
|
@@ -73,7 +73,7 @@ export function validateExposure(surface, request, live) {
|
|
|
73
73
|
// admit nothing and expose nothing: watch --expose always requires edge auth, and the live-follow
|
|
74
74
|
// preconditions are checked first so `--dry-run`/`--detach`/`--json` fail with the clearer reason.
|
|
75
75
|
if (live && (live.dryRun || live.detach || live.json)) {
|
|
76
|
-
return fail("EXPOSE_REQUIRES_LIVE_FOLLOW", "watch --expose streams a live desktop over an attached follow channel
|
|
76
|
+
return fail("EXPOSE_REQUIRES_LIVE_FOLLOW", "watch --expose streams a live desktop over an attached follow channel. It cannot combine with --dry-run, --detach or --json, and without an interactive terminal it needs --serve.");
|
|
77
77
|
}
|
|
78
78
|
// --safe is a share_ready library filter for `serve`; watch streams a single live run that is
|
|
79
79
|
// never share_ready, so --safe would silently do nothing here. Reject it rather than ignore it.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RunDisplay } from "../run/display.js";
|
|
1
2
|
export interface LibraryHistory {
|
|
2
3
|
latestRunId: string | null;
|
|
3
4
|
runs: Array<{
|
|
@@ -5,7 +6,10 @@ export interface LibraryHistory {
|
|
|
5
6
|
createdAt: string | null;
|
|
6
7
|
mode: string | null;
|
|
7
8
|
href: string;
|
|
9
|
+
/** The participants' verdict. `display` says whether the run passed. */
|
|
8
10
|
status: string;
|
|
11
|
+
/** How the run reads (runDisplay in src/run/display.ts); absent when its data could not be read. */
|
|
12
|
+
display?: RunDisplay;
|
|
9
13
|
/** Current contained status-record observation, separate from the evidence verdict. */
|
|
10
14
|
runtimeState?: "running" | "finished" | "interrupted" | "unknown";
|
|
11
15
|
streamCount: number;
|