humanish 0.115.0 → 0.116.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/CHANGELOG.md +22 -0
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/computer-use/loop/debrief.js +61 -27
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +2 -1
- package/dist/cli/commands/run-signals.js +5 -1
- package/dist/cli/commands/runs.js +2 -1
- package/dist/cli/commands/setup.js +3 -3
- package/dist/cli/commands/study-format.d.ts +5 -2
- package/dist/cli/commands/study-format.js +29 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +1 -1
- package/dist/cli/commands/tui.js +59 -17
- package/dist/cli/doctor.js +9 -2
- package/dist/cli/findings-text.d.ts +5 -0
- package/dist/cli/findings-text.js +169 -0
- package/dist/cli/findings.d.ts +0 -4
- package/dist/cli/findings.js +5 -167
- package/dist/cli/io.js +6 -1
- package/dist/cli/update-check.js +4 -1
- package/dist/comms/connections.js +4 -3
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/redaction.js +9 -1
- package/dist/evidence/secret-scrub.d.ts +15 -5
- package/dist/evidence/secret-scrub.js +67 -9
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/export-bundle.js +18 -14
- package/dist/feedback/feedback.js +25 -7
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +60 -48
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/pinned-files.js +1 -1
- package/dist/observer/render.js +3 -2
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
- package/dist/routes/computer-use/bundle-parts.js +2 -2
- package/dist/routes/computer-use/bundle.d.ts +4 -5
- package/dist/routes/computer-use/bundle.js +1 -3
- package/dist/routes/computer-use/fanout-records.js +10 -11
- package/dist/routes/computer-use/live-phase.d.ts +1 -1
- package/dist/routes/computer-use/live-phase.js +0 -1
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-runs.d.ts +6 -2
- package/dist/routes/computer-use/participant-runs.js +9 -5
- package/dist/routes/computer-use/plan.d.ts +3 -0
- package/dist/routes/computer-use/plan.js +14 -11
- package/dist/routes/computer-use/rerun-selection.js +2 -2
- package/dist/routes/computer-use/result.js +3 -6
- package/dist/routes/computer-use/setup.d.ts +4 -6
- package/dist/routes/computer-use/setup.js +2 -6
- package/dist/routes/computer-use/single-bundle.d.ts +4 -5
- package/dist/routes/computer-use/single-bundle.js +22 -25
- package/dist/routes/computer-use/subject-projection.d.ts +4 -8
- package/dist/routes/computer-use/subject-projection.js +9 -31
- package/dist/routes/computer-use/types.d.ts +4 -41
- package/dist/routes/scripted/scenario.js +28 -12
- package/dist/routes/scripted/session-result.js +6 -3
- package/dist/routes/shared-world/bundle-records.js +2 -8
- package/dist/routes/shared-world/participant-specs.d.ts +0 -2
- package/dist/routes/shared-world/participant-specs.js +1 -20
- package/dist/routes/shared-world/plan.d.ts +3 -0
- package/dist/routes/shared-world/plan.js +23 -3
- package/dist/routes/shared-world/setup.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +9 -9
- package/dist/routes/terminal/ledger.js +63 -40
- package/dist/routes/terminal/live-finish.js +7 -10
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/token-usage.d.ts +1 -2
- package/dist/routes/terminal/token-usage.js +4 -6
- package/dist/routes/terminal/types.d.ts +9 -1
- package/dist/run/bundle-shape.js +6 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/contained-output.d.ts +53 -5
- package/dist/run/contained-output.js +141 -42
- package/dist/run/detail.d.ts +2 -1
- package/dist/run/detail.js +12 -7
- package/dist/run/escape-sequences.d.ts +11 -0
- package/dist/run/escape-sequences.js +93 -0
- package/dist/run/evidence-files.d.ts +8 -8
- package/dist/run/evidence-files.js +19 -23
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/notes.d.ts +0 -8
- package/dist/run/notes.js +5 -51
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/paths.d.ts +2 -0
- package/dist/run/paths.js +5 -1
- package/dist/run/preflight-receipts.js +13 -8
- package/dist/run/project.js +7 -4
- package/dist/run/reclaim-outcomes.d.ts +8 -6
- package/dist/run/reclaim-outcomes.js +10 -6
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +43 -16
- package/dist/run/run-clock.d.ts +50 -0
- package/dist/run/run-clock.js +66 -0
- package/dist/run/run.js +5 -2
- package/dist/run/sandbox-ids.d.ts +7 -2
- package/dist/run/sandbox-ids.js +27 -11
- package/dist/run/secrets.d.ts +2 -1
- package/dist/run/secrets.js +18 -22
- package/dist/run/stored-runs.js +14 -7
- package/dist/run/transient-comms-secrets.js +6 -17
- package/dist/run-study.js +22 -13
- package/dist/study/adapter-scorer-loader.js +6 -4
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-types.d.ts +2 -2
- package/dist/study/plan.d.ts +5 -3
- package/dist/study/plan.js +18 -9
- package/dist/study/preflight-probes.js +11 -3
- package/dist/study/preflight.js +9 -5
- package/dist/study/summary.d.ts +8 -2
- package/dist/study/summary.js +20 -22
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +38 -17
- package/dist/substrates/e2b/connection.d.ts +12 -0
- package/dist/substrates/e2b/connection.js +35 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
- package/dist/substrates/e2b/desktop-executor.js +42 -9
- package/dist/substrates/e2b/lifetime.d.ts +24 -4
- package/dist/substrates/e2b/lifetime.js +41 -4
- package/dist/substrates/e2b/sandbox.d.ts +4 -3
- package/dist/substrates/e2b/sandbox.js +13 -18
- package/dist/substrates/e2b/sdk.d.ts +20 -9
- package/dist/substrates/local/capacity.d.ts +18 -2
- package/dist/substrates/local/capacity.js +21 -2
- package/dist/substrates/local/runtime-host.js +21 -14
- package/dist/substrates/local/runtime.js +3 -11
- package/dist/tui/contract.d.ts +30 -5
- package/dist/tui/contract.js +4 -1
- package/dist/tui-app.js +149 -147
- package/dist/verify/artifacts.d.ts +9 -2
- package/dist/verify/artifacts.js +37 -13
- package/dist/verify/liveness.js +19 -5
- package/dist/verify/verify.js +54 -27
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/state-driven-executor.md +4 -2
- package/docs/contracts/run-bundle.md +18 -10
- package/docs/contracts/schemas.md +27 -18
- package/docs/contracts/study-analysis.md +10 -1
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +2 -2
package/dist/comms/setup.js
CHANGED
|
@@ -13,6 +13,14 @@ import { resolveStudyManifest } from "../study/discover.js";
|
|
|
13
13
|
import { assertPreparedSelectedOutputDirectory, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
14
14
|
import { otherStudyFiles } from "../study/files.js";
|
|
15
15
|
import { isNodeError } from "../run/type-guards.js";
|
|
16
|
+
/** The most of a study file the receiving copy reads, for its source and its destination. */
|
|
17
|
+
const MAX_STUDY_COPY_BYTES = 1024 * 1024;
|
|
18
|
+
/** The read found exactly `expected`, or found nothing where `expected` is null. */
|
|
19
|
+
function readsAs(read, expected) {
|
|
20
|
+
return expected === null
|
|
21
|
+
? read.status === "missing"
|
|
22
|
+
: read.status === "read" && read.bytes.equals(expected);
|
|
23
|
+
}
|
|
16
24
|
export async function checkCommsConnection(args) {
|
|
17
25
|
const base = {
|
|
18
26
|
schema: "humanish.comms-check.v1",
|
|
@@ -128,9 +136,10 @@ export async function configureCommsStudy(args) {
|
|
|
128
136
|
const rel = path
|
|
129
137
|
.relative(root.requestedPath, path.resolve(args.cwd, source.path))
|
|
130
138
|
.replace(/\\/g, "/");
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
139
|
+
const sourceRead = await readContainedRegularFile(root, rel, MAX_STUDY_COPY_BYTES);
|
|
140
|
+
if (sourceRead.status !== "read")
|
|
133
141
|
return { ...base, message: "The study file is missing or too large." };
|
|
142
|
+
const bytes = sourceRead.bytes;
|
|
134
143
|
const text = bytes.toString("utf8");
|
|
135
144
|
const raw = parse(text);
|
|
136
145
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -182,13 +191,12 @@ export async function configureCommsStudy(args) {
|
|
|
182
191
|
...base,
|
|
183
192
|
message: `${others.join(", ")} already uses the name ${stem}. Rename or remove it before configuring email, so \`run ${stem}\` reads one file.`,
|
|
184
193
|
};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
catch {
|
|
194
|
+
// An existing destination is replaced only when its bytes are in the plan token, so one that
|
|
195
|
+
// cannot be read whole is refused instead of being taken as absent.
|
|
196
|
+
const destinationRead = await readContainedRegularFile(root, destination, MAX_STUDY_COPY_BYTES);
|
|
197
|
+
if (destinationRead.status === "refused")
|
|
190
198
|
return { ...base, message: "The local destination could not be read safely." };
|
|
191
|
-
|
|
199
|
+
const prior = destinationRead.status === "read" ? destinationRead.bytes : null;
|
|
192
200
|
const content = String(study);
|
|
193
201
|
const token = createHash("sha256")
|
|
194
202
|
.update(bytes)
|
|
@@ -219,10 +227,8 @@ export async function configureCommsStudy(args) {
|
|
|
219
227
|
const lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
220
228
|
const identity = await lock.stat();
|
|
221
229
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
if (!currentSource?.equals(bytes) ||
|
|
225
|
-
(prior === null ? currentDestination !== null : !currentDestination?.equals(prior)))
|
|
230
|
+
const reread = (file) => readContainedRegularFile(root, file, MAX_STUDY_COPY_BYTES);
|
|
231
|
+
if (!readsAs(await reread(rel), bytes) || !readsAs(await reread(destination), prior))
|
|
226
232
|
return { ...base, message: "Files changed while saving. Preview again." };
|
|
227
233
|
await writeContainedOutputFile(directory, filename, content, "utf8");
|
|
228
234
|
}
|
|
@@ -182,7 +182,15 @@ export function containsSensitive(text) {
|
|
|
182
182
|
// suffix so redaction cannot turn a string's contents into JSON delimiters.
|
|
183
183
|
// This also works when the closing quote arrives in a later stream callback.
|
|
184
184
|
function redactLocalPaths(text, label) {
|
|
185
|
-
return LOCAL_PATH_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, (match) => (label ?? replacement) + (match
|
|
185
|
+
return LOCAL_PATH_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, (match) => (label ?? replacement) + trailingBackslashes(match)), text);
|
|
186
|
+
}
|
|
187
|
+
// Read from the end: `/\\+$/` retries a run of backslashes from each of its characters when
|
|
188
|
+
// something follows the run, which took 1.4 s on a path holding 40,000 of them.
|
|
189
|
+
function trailingBackslashes(text) {
|
|
190
|
+
let start = text.length;
|
|
191
|
+
while (start > 0 && text.charCodeAt(start - 1) === 0x5c)
|
|
192
|
+
start -= 1;
|
|
193
|
+
return text.slice(start);
|
|
186
194
|
}
|
|
187
195
|
/**
|
|
188
196
|
* What a shared copy of a run says in place of a provider sandbox id. scripts/public-surface-scan.mjs
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A value as written, and percent-encoded, JSON-escaped once (also with non-ASCII characters as
|
|
3
|
-
* `\u` escapes) and twice, its UTF-8 bytes read one per character and the reverse, base64
|
|
4
|
-
* byte offset, base64url and hex. Twice, because a JSON event can carry a
|
|
5
|
-
* a string. An
|
|
6
|
-
*
|
|
7
|
-
* is
|
|
3
|
+
* `\u` escapes) and twice, its UTF-8 bytes read one per character and the reverse, base64 padded,
|
|
4
|
+
* unpadded and at each byte offset, base64url and hex. Twice, because a JSON event can carry a
|
|
5
|
+
* command's JSON output as a string. An escaped form is searched for at any length, as the value
|
|
6
|
+
* is: it holds a backslash or a `%`, so it is not ordinary text. A base64 or hex form is searched
|
|
7
|
+
* for from MIN_ENCODED_FORM characters, so a four-byte value is found in base64 written whole and
|
|
8
|
+
* not inside a longer run, where four or five of its characters do not depend on its neighbours.
|
|
8
9
|
*/
|
|
9
10
|
export declare function encodedForms(value: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* A scrub that replaces each non-empty value as written with `[REDACTED_SECRET]`, markers
|
|
13
|
+
* included. From the start of the text, the longest value that starts at a position is replaced
|
|
14
|
+
* and the search goes on after it, so the marker it writes is never searched. One global regex of
|
|
15
|
+
* the values, longest first, does that fastest. V8 compiles it at its first use and refuses it
|
|
16
|
+
* once a value has 32,768 characters; the scrub then finds each value itself, with the same
|
|
17
|
+
* output, in time linear in the text for each value.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scrubValuesAsWritten(values: readonly string[]): (text: string) => string;
|
|
10
20
|
/**
|
|
11
21
|
* A check that the text holds one of the values' forms outside the markers in any of its
|
|
12
22
|
* readingsOf. A scrub's output must pass it.
|
|
@@ -10,9 +10,12 @@ const REDACTED = REDACTION_MARKERS.secret;
|
|
|
10
10
|
// of the marker's name. Other bracketed text, such as `[REDACTED_373433393231]`, is searched like
|
|
11
11
|
// any text.
|
|
12
12
|
const MARKER = new RegExp(Object.values(REDACTION_MARKERS).map(escapeRegExp).join("|"), "g");
|
|
13
|
-
// The shortest encoded form searched for
|
|
14
|
-
//
|
|
15
|
-
|
|
13
|
+
// The shortest encoded form searched for: the unpadded base64 of a four-byte value, such as a
|
|
14
|
+
// four-digit code. A given form of n characters turns up by chance in random base64 about once in
|
|
15
|
+
// 64^n characters, once in 69 billion at 6 and once in 17 million at 4. In the text of 9 real runs
|
|
16
|
+
// (2.5 million characters) no 6- or 7-character form of any 4-, 5- or 6-digit code occurred. A
|
|
17
|
+
// value itself is searched for at any length.
|
|
18
|
+
const MIN_ENCODED_FORM = 6;
|
|
16
19
|
/**
|
|
17
20
|
* The characters of a value's base64 encoding that do not depend on its neighbours, with 0, 1 or
|
|
18
21
|
* 2 bytes before it. A prefix byte shares the next character with the value, and so does the byte
|
|
@@ -31,11 +34,12 @@ function base64Middles(bytes, encoding) {
|
|
|
31
34
|
}
|
|
32
35
|
/**
|
|
33
36
|
* A value as written, and percent-encoded, JSON-escaped once (also with non-ASCII characters as
|
|
34
|
-
* `\u` escapes) and twice, its UTF-8 bytes read one per character and the reverse, base64
|
|
35
|
-
* byte offset, base64url and hex. Twice, because a JSON event can carry a
|
|
36
|
-
* a string. An
|
|
37
|
-
*
|
|
38
|
-
* is
|
|
37
|
+
* `\u` escapes) and twice, its UTF-8 bytes read one per character and the reverse, base64 padded,
|
|
38
|
+
* unpadded and at each byte offset, base64url and hex. Twice, because a JSON event can carry a
|
|
39
|
+
* command's JSON output as a string. An escaped form is searched for at any length, as the value
|
|
40
|
+
* is: it holds a backslash or a `%`, so it is not ordinary text. A base64 or hex form is searched
|
|
41
|
+
* for from MIN_ENCODED_FORM characters, so a four-byte value is found in base64 written whole and
|
|
42
|
+
* not inside a longer run, where four or five of its characters do not depend on its neighbours.
|
|
39
43
|
*/
|
|
40
44
|
export function encodedForms(value) {
|
|
41
45
|
const bytes = Buffer.from(value, "utf8");
|
|
@@ -51,8 +55,10 @@ export function encodedForms(value) {
|
|
|
51
55
|
catch {
|
|
52
56
|
// no percent-encoded form
|
|
53
57
|
}
|
|
58
|
+
const base64 = bytes.toString("base64");
|
|
54
59
|
const binary = [
|
|
55
|
-
|
|
60
|
+
base64,
|
|
61
|
+
base64.replace(/=+$/, ""),
|
|
56
62
|
bytes.toString("base64url"),
|
|
57
63
|
bytes.toString("hex"),
|
|
58
64
|
...base64Middles(bytes, "base64"),
|
|
@@ -127,6 +133,58 @@ function eachOccurrence(text, form, found) {
|
|
|
127
133
|
at = matched > 0 ? -1 : text.indexOf(form, next);
|
|
128
134
|
}
|
|
129
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Each value's occurrences, the longest value that starts at a position replaced from the start of
|
|
138
|
+
* the text on, in time linear in the text for each value.
|
|
139
|
+
*/
|
|
140
|
+
function replaceLongestFirst(text, values) {
|
|
141
|
+
// The length of the longest value that starts at each position, allocated at the first find.
|
|
142
|
+
let longest;
|
|
143
|
+
for (const value of values)
|
|
144
|
+
eachOccurrence(text, value, (at) => {
|
|
145
|
+
longest ??= new Uint32Array(text.length);
|
|
146
|
+
longest[at] = Math.max(longest[at], value.length);
|
|
147
|
+
});
|
|
148
|
+
if (longest === undefined)
|
|
149
|
+
return text;
|
|
150
|
+
let result = "";
|
|
151
|
+
let cursor = 0;
|
|
152
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
153
|
+
if (longest[at] === 0)
|
|
154
|
+
continue;
|
|
155
|
+
result += text.slice(cursor, at) + REDACTED;
|
|
156
|
+
cursor = at + longest[at];
|
|
157
|
+
at = cursor - 1;
|
|
158
|
+
}
|
|
159
|
+
return result + text.slice(cursor);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A scrub that replaces each non-empty value as written with `[REDACTED_SECRET]`, markers
|
|
163
|
+
* included. From the start of the text, the longest value that starts at a position is replaced
|
|
164
|
+
* and the search goes on after it, so the marker it writes is never searched. One global regex of
|
|
165
|
+
* the values, longest first, does that fastest. V8 compiles it at its first use and refuses it
|
|
166
|
+
* once a value has 32,768 characters; the scrub then finds each value itself, with the same
|
|
167
|
+
* output, in time linear in the text for each value.
|
|
168
|
+
*/
|
|
169
|
+
export function scrubValuesAsWritten(values) {
|
|
170
|
+
const written = [...new Set(values)].filter((value) => value.length > 0);
|
|
171
|
+
if (written.length === 0)
|
|
172
|
+
return (text) => text;
|
|
173
|
+
let pattern = new RegExp([...written]
|
|
174
|
+
.sort((left, right) => right.length - left.length)
|
|
175
|
+
.map(escapeRegExp)
|
|
176
|
+
.join("|"), "g");
|
|
177
|
+
return (text) => {
|
|
178
|
+
if (pattern !== undefined)
|
|
179
|
+
try {
|
|
180
|
+
return text.replace(pattern, REDACTED);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
pattern = undefined;
|
|
184
|
+
}
|
|
185
|
+
return replaceLongestFirst(text, written);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
130
188
|
/** Every occurrence of every form not wholly inside a marker, in no order. */
|
|
131
189
|
function occurrences(text, forms) {
|
|
132
190
|
const markers = [];
|
package/dist/feedback/draft.js
CHANGED
|
@@ -11,7 +11,7 @@ import { loadRunBundlePrepared } from "../run/locate.js";
|
|
|
11
11
|
import { isFeedbackIdempotencyKey } from "../run/feedback-shape.js";
|
|
12
12
|
import { runNoteFile } from "../run/note-files.js";
|
|
13
13
|
import { byRunTime } from "../run/note-shape.js";
|
|
14
|
-
import { formatRunTime } from "../run/
|
|
14
|
+
import { formatRunTime } from "../run/run-clock.js";
|
|
15
15
|
import { runParticipantCaptions } from "../run/participant-records.js";
|
|
16
16
|
import { isRecord } from "../run/type-guards.js";
|
|
17
17
|
import { feedbackProofCommands, projectFeedbackAcceptanceProof } from "./proof.js";
|
|
@@ -12,16 +12,16 @@ import { inventorySandboxIds, scrubSandboxIdBytes } from "../run/sandbox-ids.js"
|
|
|
12
12
|
import { containsSensitive, redactSandboxIds, redactScreenshot, redactText, } from "../evidence/redaction.js";
|
|
13
13
|
import { decodeEscapes, readingsOf } from "../evidence/encoded-text.js";
|
|
14
14
|
import { readPlainText } from "../evidence/plain-text.js";
|
|
15
|
-
import { streamScreenshotPaths } from "../verify/artifacts.js";
|
|
15
|
+
import { MAX_RUN_ENTRIES, streamScreenshotPaths } from "../verify/artifacts.js";
|
|
16
16
|
import { verifyRunPrepared } from "../verify/verify.js";
|
|
17
17
|
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
21
|
import { LOCAL_ONLY_RUN_FILES } from "../run/local-only-files.js";
|
|
22
|
-
import {
|
|
22
|
+
import { readUnchangedFile } from "../run/evidence-files.js";
|
|
23
|
+
import { assertPreparedSelectedOutputDirectory, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
23
24
|
const DERIVATION_SCHEMA = "humanish.redacted-derivation.v1";
|
|
24
|
-
const MAX_FILES = 10_000;
|
|
25
25
|
const TEXT_EXTENSIONS = new Set([
|
|
26
26
|
".json",
|
|
27
27
|
".ndjson",
|
|
@@ -69,8 +69,8 @@ async function inventory(root, maxBytes) {
|
|
|
69
69
|
if (!before.isDirectory() || before.isSymbolicLink())
|
|
70
70
|
throw new Error("Source contains an unsafe directory.");
|
|
71
71
|
for (const name of (await readdir(directory)).sort()) {
|
|
72
|
-
if (++entriesSeen >
|
|
73
|
-
throw new Error(`Source exceeds ${
|
|
72
|
+
if (++entriesSeen > MAX_RUN_ENTRIES)
|
|
73
|
+
throw new Error(`Source exceeds ${MAX_RUN_ENTRIES} inventory entries.`);
|
|
74
74
|
if (name.includes("\\") || name.includes("\0"))
|
|
75
75
|
throw new Error("Source contains an unsafe path segment.");
|
|
76
76
|
const rel = relative ? `${relative}/${name}` : name;
|
|
@@ -86,15 +86,17 @@ async function inventory(root, maxBytes) {
|
|
|
86
86
|
else {
|
|
87
87
|
if (path.extname(rel).toLowerCase() === ".mp4")
|
|
88
88
|
throw new Error("Continuous video/audio cannot be redacted by bundle export. Use --local-only HTML export for a snapshot-only copy.");
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
89
|
+
// At most what is left of --max-bytes, so a file that grows after this lstat is refused
|
|
90
|
+
// unread. With nothing left, an empty file still reads and any other is refused.
|
|
91
|
+
const read = await readUnchangedFile(root, rel, Math.max(1, maxBytes - total));
|
|
92
|
+
if (read.state === "unavailable")
|
|
93
93
|
throw new Error("Source artifact could not be read through its bound identity.");
|
|
94
|
-
|
|
94
|
+
if (read.state === "limit")
|
|
95
|
+
throw new Error("Source inventory exceeds --max-bytes.");
|
|
96
|
+
total += read.bytes.length;
|
|
95
97
|
if (total > maxBytes)
|
|
96
98
|
throw new Error("Source inventory exceeds --max-bytes.");
|
|
97
|
-
files.push({ path: rel, bytes, sha256: hash(bytes) });
|
|
99
|
+
files.push({ path: rel, bytes: read.bytes, sha256: hash(read.bytes) });
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
const after = await lstat(directory, { bigint: true });
|
|
@@ -541,10 +543,12 @@ async function buildDerivative(stage, source, runId, bundle, maxBytes) {
|
|
|
541
543
|
throw new Error("Derivative Observer could not be rebuilt.");
|
|
542
544
|
const generated = [];
|
|
543
545
|
for (const relative of ["observer/index.html", "observer/observer-data.json"]) {
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
546
|
+
const read = await readUnchangedFile(stagePaths, relative, maxBytes);
|
|
547
|
+
if (read.state === "limit")
|
|
548
|
+
throw new Error("The regenerated Observer exceeds --max-bytes.");
|
|
549
|
+
if (read.state === "unavailable")
|
|
546
550
|
throw new Error("Regenerated Observer artifact could not be read safely.");
|
|
547
|
-
generated.push({ path: relative, sha256: hash(bytes) });
|
|
551
|
+
generated.push({ path: relative, sha256: hash(read.bytes) });
|
|
548
552
|
}
|
|
549
553
|
await writeContainedOutputFile(stagePaths, "derivation.json", jsonBytes({ ...receipt, generated }));
|
|
550
554
|
const derivativeVerify = await verifyRunPrepared(stage.physicalPath, runId, stagePaths);
|
|
@@ -4,7 +4,7 @@ import { realpath } from "node:fs/promises";
|
|
|
4
4
|
import { runNotFoundMessage } from "../run/run-not-found.js";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveLatestRunDirectory, validatePreparedRunArtifactPaths, } from "../run/paths.js";
|
|
7
|
-
import { bindExistingManagedHumanishOutputDirectory, readContainedRegularFile, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
7
|
+
import { bindExistingManagedHumanishOutputDirectory, ContainedReadRefusedError, LATEST_POINTER_MAX_BYTES, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
8
8
|
import { loadRunBundlePrepared } from "../run/locate.js";
|
|
9
9
|
import { readNotesForSharing } from "../run/note-files.js";
|
|
10
10
|
import { cli } from "../cli/invocation.js";
|
|
@@ -208,7 +208,9 @@ async function isSafeFeedbackEvidenceFile(context, evidencePath) {
|
|
|
208
208
|
path.isAbsolute(relative)) {
|
|
209
209
|
return false;
|
|
210
210
|
}
|
|
211
|
-
|
|
211
|
+
// Evidence a draft cites must be readable: a refused file is listed with the missing ones.
|
|
212
|
+
const read = await readContainedRegularFile(context.preparedRunPaths, relative, RUN_ARTIFACT_MAX_BYTES);
|
|
213
|
+
return read.status === "read";
|
|
212
214
|
}
|
|
213
215
|
export async function renderIssueMarkdown(cwdInput, runInput, repo, options = {}) {
|
|
214
216
|
const verified = await verifyFeedbackBound(cwdInput, runInput, options);
|
|
@@ -253,9 +255,22 @@ export async function listFeedback(cwdInput, runInput) {
|
|
|
253
255
|
},
|
|
254
256
|
};
|
|
255
257
|
}
|
|
256
|
-
const
|
|
257
|
-
const draft = draftBytes === null ? undefined : JSON.parse(draftBytes.toString("utf8"));
|
|
258
|
+
const draftRead = await readContainedRegularFile(context.preparedRunPaths, path.join("feedback", "draft.json"), RUN_ARTIFACT_MAX_BYTES);
|
|
258
259
|
const draftPath = path.join(context.preparedRunPaths.relativeRunRoot, "feedback", "draft.json");
|
|
260
|
+
if (draftRead.status === "refused")
|
|
261
|
+
return {
|
|
262
|
+
schema: FEEDBACK_RESULT_SCHEMA,
|
|
263
|
+
ok: false,
|
|
264
|
+
cwd,
|
|
265
|
+
run: runInput,
|
|
266
|
+
error: {
|
|
267
|
+
code: "HUMANISH_INVALID_FEEDBACK_DRAFT",
|
|
268
|
+
message: `The feedback draft could not be read: ${refusalText(draftPath, draftRead)}.`,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
const draft = draftRead.status === "missing"
|
|
272
|
+
? undefined
|
|
273
|
+
: JSON.parse(draftRead.bytes.toString("utf8"));
|
|
259
274
|
return {
|
|
260
275
|
schema: FEEDBACK_RESULT_SCHEMA,
|
|
261
276
|
ok: true,
|
|
@@ -284,10 +299,13 @@ async function resolveFeedbackRunContext(cwd, runInput) {
|
|
|
284
299
|
const runsRoot = await bindExistingManagedHumanishOutputDirectory(physicalCwd, "runs");
|
|
285
300
|
if (!runsRoot)
|
|
286
301
|
return null;
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
302
|
+
const pointerRead = await readContainedRegularFile(runsRoot, "latest.json", LATEST_POINTER_MAX_BYTES);
|
|
303
|
+
if (pointerRead.status === "missing")
|
|
289
304
|
return null;
|
|
290
|
-
|
|
305
|
+
// A pointer that is there and refused names no run, and saying so beats "run not found".
|
|
306
|
+
if (pointerRead.status === "refused")
|
|
307
|
+
throw new ContainedReadRefusedError(path.join(".humanish", "runs", "latest.json"), pointerRead);
|
|
308
|
+
const pointer = JSON.parse(pointerRead.bytes.toString("utf8"));
|
|
291
309
|
if (typeof pointer.runId !== "string" ||
|
|
292
310
|
typeof pointer.path !== "string" ||
|
|
293
311
|
!resolveLatestRunDirectory(physicalCwd, { path: pointer.path, runId: pointer.runId })) {
|
|
@@ -3,18 +3,35 @@ import { ComputerUseExecutorError } from "../actors/computer-use/executor-error.
|
|
|
3
3
|
// hand these strings to xdotool, which has a command language, so only names from these tables
|
|
4
4
|
// ever reach it. The guest runtime ships this module, so it lives in src/guest/, which CI's
|
|
5
5
|
// guest-desktop job and scripts/guest-desktop-proof.mjs key on.
|
|
6
|
+
//
|
|
7
|
+
// The tables take the names in OpenAI's computer-use key map, the names @e2b/desktop's press()
|
|
8
|
+
// mapped (the hosted desktop sent keys through it before), and every printable ASCII
|
|
9
|
+
// punctuation character. xdotool finds the key and the Shift level for a keysym itself, so `?`
|
|
10
|
+
// arrives as `question` with Shift held on an American English keymap.
|
|
6
11
|
/** The modifiers a pointer action may hold, keyed by the upper-cased provider name. */
|
|
7
12
|
const modifierNames = Object.freeze({
|
|
8
13
|
CTRL: "ctrl",
|
|
9
14
|
CONTROL: "ctrl",
|
|
10
15
|
ALT: "alt",
|
|
16
|
+
OPTION: "alt",
|
|
11
17
|
SHIFT: "shift",
|
|
12
18
|
META: "super",
|
|
13
19
|
SUPER: "super",
|
|
14
20
|
CMD: "super",
|
|
21
|
+
COMMAND: "super",
|
|
22
|
+
WIN: "super",
|
|
23
|
+
WINDOWS: "super",
|
|
15
24
|
});
|
|
16
25
|
const keyNames = Object.freeze({
|
|
17
26
|
...modifierNames,
|
|
27
|
+
CONTROL_LEFT: "Control_L",
|
|
28
|
+
CONTROL_RIGHT: "Control_R",
|
|
29
|
+
ALT_LEFT: "Alt_L",
|
|
30
|
+
ALT_RIGHT: "Alt_R",
|
|
31
|
+
SHIFT_LEFT: "Shift_L",
|
|
32
|
+
SHIFT_RIGHT: "Shift_R",
|
|
33
|
+
SUPER_LEFT: "Super_L",
|
|
34
|
+
SUPER_RIGHT: "Super_R",
|
|
18
35
|
ENTER: "Return",
|
|
19
36
|
RETURN: "Return",
|
|
20
37
|
TAB: "Tab",
|
|
@@ -24,11 +41,14 @@ const keyNames = Object.freeze({
|
|
|
24
41
|
" ": "space",
|
|
25
42
|
BACKSPACE: "BackSpace",
|
|
26
43
|
DELETE: "Delete",
|
|
44
|
+
DEL: "Delete",
|
|
27
45
|
INSERT: "Insert",
|
|
28
46
|
HOME: "Home",
|
|
29
47
|
END: "End",
|
|
30
48
|
PAGEUP: "Prior",
|
|
31
49
|
PAGEDOWN: "Next",
|
|
50
|
+
PAGE_UP: "Prior",
|
|
51
|
+
PAGE_DOWN: "Next",
|
|
32
52
|
ARROWUP: "Up",
|
|
33
53
|
ARROWDOWN: "Down",
|
|
34
54
|
ARROWLEFT: "Left",
|
|
@@ -37,18 +57,45 @@ const keyNames = Object.freeze({
|
|
|
37
57
|
DOWN: "Down",
|
|
38
58
|
LEFT: "Left",
|
|
39
59
|
RIGHT: "Right",
|
|
60
|
+
CAPS_LOCK: "Caps_Lock",
|
|
61
|
+
NUM_LOCK: "Num_Lock",
|
|
62
|
+
SCROLL_LOCK: "Scroll_Lock",
|
|
63
|
+
PAUSE: "Pause",
|
|
64
|
+
BREAK: "Pause",
|
|
65
|
+
PRINT: "Print",
|
|
66
|
+
MENU: "Menu",
|
|
67
|
+
"!": "exclam",
|
|
68
|
+
'"': "quotedbl",
|
|
69
|
+
"#": "numbersign",
|
|
70
|
+
$: "dollar",
|
|
71
|
+
"%": "percent",
|
|
72
|
+
"&": "ampersand",
|
|
73
|
+
"'": "apostrophe",
|
|
74
|
+
"(": "parenleft",
|
|
75
|
+
")": "parenright",
|
|
76
|
+
"*": "asterisk",
|
|
40
77
|
"+": "plus",
|
|
41
|
-
"-": "minus",
|
|
42
|
-
"=": "equal",
|
|
43
78
|
",": "comma",
|
|
79
|
+
"-": "minus",
|
|
44
80
|
".": "period",
|
|
45
81
|
"/": "slash",
|
|
46
|
-
"
|
|
82
|
+
":": "colon",
|
|
47
83
|
";": "semicolon",
|
|
48
|
-
"
|
|
84
|
+
"<": "less",
|
|
85
|
+
"=": "equal",
|
|
86
|
+
">": "greater",
|
|
87
|
+
"?": "question",
|
|
88
|
+
"@": "at",
|
|
49
89
|
"[": "bracketleft",
|
|
90
|
+
"\\": "backslash",
|
|
50
91
|
"]": "bracketright",
|
|
92
|
+
"^": "asciicircum",
|
|
93
|
+
_: "underscore",
|
|
51
94
|
"`": "grave",
|
|
95
|
+
"{": "braceleft",
|
|
96
|
+
"|": "bar",
|
|
97
|
+
"}": "braceright",
|
|
98
|
+
"~": "asciitilde",
|
|
52
99
|
});
|
|
53
100
|
function lookup(table, key) {
|
|
54
101
|
const upper = key.toUpperCase();
|
|
@@ -59,9 +106,16 @@ function chord(names) {
|
|
|
59
106
|
throw new ComputerUseExecutorError("action_rejected", "not_dispatched");
|
|
60
107
|
return names.join("+");
|
|
61
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* A name that is itself a chord (`Control+a`, `ctrl++`) split at each plus sign that joins two
|
|
111
|
+
* names. A lone `+` and a trailing `+` after a joining one are the plus key.
|
|
112
|
+
*/
|
|
113
|
+
function chordParts(key) {
|
|
114
|
+
return key.length > 1 ? key.split(/\+(?!$)/) : [key];
|
|
115
|
+
}
|
|
62
116
|
/** A keypress chord in xdotool syntax. Unknown or repeated keys are refused before dispatch. */
|
|
63
117
|
export function xdotoolChord(keys) {
|
|
64
|
-
return chord(keys.map((key) => {
|
|
118
|
+
return chord(keys.flatMap(chordParts).map((key) => {
|
|
65
119
|
if (/^[a-z0-9]$/i.test(key))
|
|
66
120
|
return key.toLowerCase();
|
|
67
121
|
if (/^F(?:[1-9]|1[0-2])$/i.test(key))
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** The vendor aliases `humanish keys set` accepts. A raw ENV_NAME is also accepted. */
|
|
2
|
+
export declare const KEY_VENDORS: string[];
|
|
3
|
+
/** The keys `humanish keys` lists, in its order, each with what humanish uses it for. */
|
|
4
|
+
export declare const LISTED_KEYS: {
|
|
5
|
+
name: string;
|
|
6
|
+
use: string;
|
|
7
|
+
}[];
|
|
8
|
+
/** The keys `humanish doctor` reports on every run, in its row order. */
|
|
9
|
+
export declare const DOCTOR_KEYS: string[];
|
|
1
10
|
export interface ResolvedKeyFill {
|
|
2
11
|
name: string;
|
|
3
12
|
/** Human-readable source label (a path or command name), never a value. */
|
|
@@ -24,33 +24,56 @@ import { loadEnvFile } from "./env-file.js";
|
|
|
24
24
|
import { cli } from "../cli/invocation.js";
|
|
25
25
|
import { humanishConfigFile } from "../cli/user-config.js";
|
|
26
26
|
import { OPENAI_EGRESS_PLACEHOLDER } from "../routes/terminal/runtime-auth.js";
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The provider keys, in the order `humanish keys set` names their vendors. Implicit discovery fills
|
|
29
|
+
* only these names and the user store holds only these. Everything else in an overlay or store file
|
|
30
|
+
* is ignored and named: a repo-planted NODE_OPTIONS/LD_PRELOAD must never enter process env off a
|
|
31
|
+
* file the operator did not explicitly pass (an explicit --dotenv remains the operator's own
|
|
32
|
+
* full-file load). ANTHROPIC_API_KEY is not listed, and doctor does not report it: a Claude Code
|
|
33
|
+
* participant runs on its own login and never receives it. CODEX_API_KEY is an alternative spelling
|
|
34
|
+
* of the OpenAI key for terminal studies. Doctor reports AGENTMAIL_API_KEY only under `--study`, for
|
|
35
|
+
* a study whose email connection names it, since a connection may name another variable.
|
|
36
|
+
*/
|
|
37
|
+
const PROVIDER_KEYS = [
|
|
38
|
+
{
|
|
39
|
+
name: "OPENAI_API_KEY",
|
|
40
|
+
vendor: "openai",
|
|
41
|
+
listed: { order: 2, use: "participant model and analysis" },
|
|
42
|
+
doctor: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "E2B_API_KEY",
|
|
46
|
+
vendor: "e2b",
|
|
47
|
+
login: "e2b auth login",
|
|
48
|
+
listed: { order: 1, use: "hosted desktops" },
|
|
49
|
+
doctor: true,
|
|
50
|
+
},
|
|
51
|
+
{ name: "ANTHROPIC_API_KEY", vendor: "anthropic" },
|
|
52
|
+
{
|
|
53
|
+
name: "GH_TOKEN",
|
|
54
|
+
vendor: "github",
|
|
55
|
+
login: "gh auth login",
|
|
56
|
+
listed: { order: 3, use: "private repository subjects" },
|
|
57
|
+
doctor: true,
|
|
58
|
+
},
|
|
59
|
+
{ name: "GITHUB_TOKEN", spellingOf: "GH_TOKEN" },
|
|
60
|
+
{ name: "CODEX_API_KEY", doctor: true },
|
|
61
|
+
{ name: "AGENTMAIL_API_KEY", vendor: "agentmail", listed: { order: 4, use: "email in studies" } },
|
|
39
62
|
];
|
|
40
|
-
const PROVIDER_KEY_SET = new Set(
|
|
63
|
+
const PROVIDER_KEY_SET = new Set(PROVIDER_KEYS.map((key) => key.name));
|
|
64
|
+
const providerKey = (name) => PROVIDER_KEYS.find((key) => key.name === name);
|
|
65
|
+
/** The vendor aliases `humanish keys set` accepts. A raw ENV_NAME is also accepted. */
|
|
66
|
+
export const KEY_VENDORS = PROVIDER_KEYS.flatMap((key) => key.vendor === undefined ? [] : [key.vendor]);
|
|
67
|
+
/** The keys `humanish keys` lists, in its order, each with what humanish uses it for. */
|
|
68
|
+
export const LISTED_KEYS = PROVIDER_KEYS.flatMap(({ name, listed }) => listed === undefined ? [] : [{ name, ...listed }])
|
|
69
|
+
.sort((a, b) => a.order - b.order)
|
|
70
|
+
.map(({ name, use }) => ({ name, use }));
|
|
71
|
+
/** The keys `humanish doctor` reports on every run, in its row order. */
|
|
72
|
+
export const DOCTOR_KEYS = PROVIDER_KEYS.flatMap(({ name, doctor }) => (doctor ? [name] : []));
|
|
41
73
|
/** An env for a vendor CLI that discovery runs: the caller's, without any provider key name. */
|
|
42
74
|
function withoutProviderKeys(env) {
|
|
43
75
|
return Object.fromEntries(Object.entries(env).filter(([name]) => !PROVIDER_KEY_SET.has(name)));
|
|
44
76
|
}
|
|
45
|
-
/** `humanish keys set <vendor>` aliases; a raw ENV_NAME is also accepted. A Map, so a name such as
|
|
46
|
-
* `constructor` is no alias. */
|
|
47
|
-
const KEY_VENDOR_ALIASES = new Map([
|
|
48
|
-
["openai", "OPENAI_API_KEY"],
|
|
49
|
-
["e2b", "E2B_API_KEY"],
|
|
50
|
-
["anthropic", "ANTHROPIC_API_KEY"],
|
|
51
|
-
["github", "GH_TOKEN"],
|
|
52
|
-
["agentmail", "AGENTMAIL_API_KEY"],
|
|
53
|
-
]);
|
|
54
77
|
const PROJECT_OVERLAY_RELATIVE = path.join(".humanish", "local", "provider.env");
|
|
55
78
|
export function userKeyStorePath(env, deps = {}) {
|
|
56
79
|
return humanishConfigFile(env, "keys.env", deps.homeDir);
|
|
@@ -230,21 +253,10 @@ function storeLabel(env, deps) {
|
|
|
230
253
|
/** The nearest fill instruction for a missing key. Doctor rows use it, and it is appended to
|
|
231
254
|
* *_KEYS_MISSING errors so the failure names the fix. */
|
|
232
255
|
export function missingKeyHint(name) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
case "GITHUB_TOKEN":
|
|
238
|
-
return `run \`gh auth login\`, or \`${cli("keys set github")}\``;
|
|
239
|
-
case "OPENAI_API_KEY":
|
|
240
|
-
return `run \`${cli("keys set openai")}\``;
|
|
241
|
-
case "ANTHROPIC_API_KEY":
|
|
242
|
-
return `run \`${cli("keys set anthropic")}\``;
|
|
243
|
-
case "AGENTMAIL_API_KEY":
|
|
244
|
-
return `run \`${cli("keys set agentmail")}\``;
|
|
245
|
-
default:
|
|
246
|
-
return `run \`${cli(`keys set ${name}`)}\``;
|
|
247
|
-
}
|
|
256
|
+
const named = providerKey(name);
|
|
257
|
+
const key = named?.spellingOf === undefined ? named : providerKey(named.spellingOf);
|
|
258
|
+
const set = `\`${cli(`keys set ${key?.vendor ?? name}`)}\``;
|
|
259
|
+
return key?.login === undefined ? `run ${set}` : `run \`${key.login}\`, or ${set}`;
|
|
248
260
|
}
|
|
249
261
|
/** One shared suffix for *_KEYS_MISSING messages: where discovery looked, and what fills each
|
|
250
262
|
* missing key. Pure text; never values. */
|
|
@@ -276,19 +288,18 @@ export async function probeKeySources(names, args) {
|
|
|
276
288
|
...(args.deps === undefined ? {} : { deps: args.deps }),
|
|
277
289
|
});
|
|
278
290
|
const bySource = new Map(fills.map((fill) => [fill.name, fill.source]));
|
|
291
|
+
const setInEnv = (name) => args.env[name] !== undefined && args.env[name]?.trim() !== "";
|
|
279
292
|
return names.map((name) => {
|
|
280
|
-
const inEnv =
|
|
281
|
-
//
|
|
282
|
-
// "missing" while
|
|
283
|
-
const
|
|
284
|
-
args.env.GITHUB_TOKEN !== undefined &&
|
|
285
|
-
args.env.GITHUB_TOKEN.trim() !== "";
|
|
293
|
+
const inEnv = setInEnv(name) && !placeholder(name);
|
|
294
|
+
// A second spelling of the key in the env, such as GITHUB_TOKEN for GH_TOKEN, is the same
|
|
295
|
+
// credential; a doctor row that says "missing" while it sits in the env would be wrong.
|
|
296
|
+
const spelling = PROVIDER_KEYS.find((key) => key.spellingOf === name && setInEnv(key.name))?.name;
|
|
286
297
|
const source = inEnv
|
|
287
298
|
? args.dotenv?.names.includes(name)
|
|
288
299
|
? `--dotenv ${args.dotenv.path}`
|
|
289
300
|
: "process env"
|
|
290
|
-
:
|
|
291
|
-
?
|
|
301
|
+
: spelling !== undefined
|
|
302
|
+
? `process env (${spelling})`
|
|
292
303
|
: (bySource.get(name) ?? null);
|
|
293
304
|
return {
|
|
294
305
|
name,
|
|
@@ -301,9 +312,10 @@ export async function probeKeySources(names, args) {
|
|
|
301
312
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
302
313
|
/** Resolve a `humanish keys set` target: a vendor alias or a raw env name. Null = invalid. */
|
|
303
314
|
export function resolveKeyName(vendorOrName) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
const vendor = vendorOrName.toLowerCase();
|
|
316
|
+
const key = PROVIDER_KEYS.find((candidate) => candidate.vendor === vendor);
|
|
317
|
+
if (key !== undefined)
|
|
318
|
+
return key.name;
|
|
307
319
|
return ENV_NAME.test(vendorOrName) ? vendorOrName : null;
|
|
308
320
|
}
|
|
309
321
|
/** Write one key into the user store (0700 dir, 0600 file). Only allowlisted provider names
|