humanish 0.140.0 → 0.142.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/ARCHITECTURE.md +1 -0
- package/CHANGELOG.md +23 -0
- package/README.md +2 -0
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/external-handoff.d.ts +22 -0
- package/dist/actors/computer-use/external-handoff.js +83 -0
- package/dist/actors/computer-use/loop/observation.d.ts +7 -0
- package/dist/actors/computer-use/loop/observation.js +17 -1
- package/dist/actors/computer-use/loop/types.d.ts +6 -0
- package/dist/analysis/admission.d.ts +8 -0
- package/dist/analysis/admission.js +15 -4
- package/dist/analysis/analysis-limits.d.ts +6 -0
- package/dist/analysis/analysis-limits.js +6 -0
- package/dist/analysis/analysis-lock.d.ts +8 -0
- package/dist/analysis/analysis-lock.js +129 -0
- package/dist/analysis/automatic-completion.js +7 -0
- package/dist/analysis/automatic-config.d.ts +5 -2
- package/dist/analysis/automatic-config.js +23 -8
- package/dist/analysis/automatic.js +13 -7
- package/dist/analysis/execute.d.ts +17 -6
- package/dist/analysis/execute.js +37 -19
- package/dist/analysis/gemini-wire.d.ts +8 -0
- package/dist/analysis/gemini-wire.js +165 -0
- package/dist/analysis/image-tokens.d.ts +4 -2
- package/dist/analysis/image-tokens.js +24 -3
- package/dist/analysis/job.d.ts +2 -0
- package/dist/analysis/job.js +7 -2
- package/dist/analysis/openai-wire.d.ts +5 -0
- package/dist/analysis/openai-wire.js +101 -0
- package/dist/analysis/provider.d.ts +15 -7
- package/dist/analysis/provider.js +21 -101
- package/dist/analysis/recording-ended.d.ts +16 -0
- package/dist/analysis/recording-ended.js +29 -0
- package/dist/analysis/responses.d.ts +3 -0
- package/dist/analysis/responses.js +19 -0
- package/dist/analysis/service.d.ts +1 -3
- package/dist/analysis/service.js +34 -53
- package/dist/analysis/source.d.ts +1 -1
- package/dist/analysis/types.d.ts +8 -2
- package/dist/analysis/validation.d.ts +1 -0
- package/dist/analysis/validation.js +13 -12
- package/dist/browser-control/protocol.d.ts +8 -0
- package/dist/browser-control/protocol.js +21 -0
- package/dist/cli/commands/analysis-signals.d.ts +10 -4
- package/dist/cli/commands/analysis-signals.js +28 -6
- package/dist/cli/commands/analyze.js +9 -7
- package/dist/cli/commands/observe.js +26 -1
- package/dist/cli/commands/run-command.d.ts +1 -1
- package/dist/cli/commands/run-command.js +12 -0
- package/dist/cli/commands/runs.js +8 -2
- package/dist/cli/commands/study-remote.d.ts +13 -0
- package/dist/cli/commands/study-remote.js +125 -0
- package/dist/cli/commands/study-route-computer-use.js +28 -4
- package/dist/cli/commands/study-route-scripted.js +3 -2
- package/dist/cli/commands/study-route-shared-world.js +3 -2
- package/dist/cli/commands/study-route-terminal.js +3 -2
- package/dist/cli/commands/study-run.js +34 -0
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/findings.d.ts +30 -0
- package/dist/cli/findings.js +114 -15
- package/dist/cli/io.d.ts +8 -0
- package/dist/cli/io.js +3 -1
- package/dist/evidence/browser-hygiene.d.ts +12 -1
- package/dist/evidence/browser-hygiene.js +45 -2
- package/dist/feedback/export-bundle.js +1 -1
- package/dist/guest/external-links.d.ts +14 -0
- package/dist/guest/external-links.js +48 -0
- package/dist/guest/runtime-desktop.js +5 -2
- package/dist/keys/key-resolution.d.ts +10 -0
- package/dist/keys/key-resolution.js +33 -5
- package/dist/observer/data.d.ts +4 -0
- package/dist/observer/listen.d.ts +7 -2
- package/dist/observer/listen.js +10 -4
- package/dist/observer/render.d.ts +2 -0
- package/dist/observer/render.js +1 -1
- package/dist/observer/run-routes.js +12 -3
- package/dist/observer-app.html +2 -2
- package/dist/remote/launch.d.ts +53 -0
- package/dist/remote/launch.js +320 -0
- package/dist/remote/observer-proxy.d.ts +11 -0
- package/dist/remote/observer-proxy.js +66 -0
- package/dist/remote/plan.d.ts +35 -0
- package/dist/remote/plan.js +114 -0
- package/dist/remote/reattach.d.ts +17 -0
- package/dist/remote/reattach.js +81 -0
- package/dist/remote/runner.d.ts +68 -0
- package/dist/remote/runner.js +216 -0
- package/dist/routes/computer-use/e2b-desktop/desktop.js +27 -17
- package/dist/routes/computer-use/e2b-desktop/start.js +1 -1
- package/dist/routes/computer-use/e2b-desktop/teardown.js +1 -1
- package/dist/routes/computer-use/in-process-desktop.js +2 -2
- package/dist/routes/computer-use/live-phase.js +1 -1
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/computer-use/types.d.ts +2 -0
- package/dist/routes/scripted/types.d.ts +2 -0
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/types.d.ts +2 -0
- package/dist/run/costs.d.ts +2 -1
- package/dist/run/display.d.ts +6 -1
- package/dist/run/display.js +11 -0
- package/dist/run/host-suspension.js +4 -1
- package/dist/run/preflight-receipts.d.ts +3 -12
- package/dist/run/preflight-receipts.js +7 -67
- package/dist/run/pricing.d.ts +13 -1
- package/dist/run/pricing.js +50 -4
- package/dist/run/process-owner.d.ts +28 -0
- package/dist/run/process-owner.js +83 -0
- package/dist/run/projection.js +1 -1
- package/dist/run/reclaim.d.ts +6 -0
- package/dist/run/reclaim.js +32 -2
- package/dist/run/results.d.ts +1 -1
- package/dist/run/route-shell.js +1 -1
- package/dist/run/run-cost.d.ts +2 -1
- package/dist/run/run-cost.js +6 -1
- package/dist/run/run-index.d.ts +3 -1
- package/dist/run/run-index.js +23 -11
- package/dist/run/run.d.ts +29 -5
- package/dist/run/run.js +43 -17
- package/dist/run/sandbox-creates.d.ts +10 -3
- package/dist/run/sandbox-creates.js +48 -6
- package/dist/run/status.d.ts +57 -2
- package/dist/run/status.js +87 -1
- package/dist/run/stored-runs.d.ts +3 -0
- package/dist/run/stored-runs.js +9 -4
- package/dist/run-study.d.ts +5 -0
- package/dist/study/doctor.js +14 -1
- package/dist/study/requirements.d.ts +1 -1
- package/dist/study/requirements.js +8 -6
- package/dist/study/route-inputs.js +4 -0
- package/dist/study/run-study-events.d.ts +8 -2
- package/dist/study/run-study-events.js +9 -2
- package/dist/substrates/e2b/base-module.d.ts +3 -0
- package/dist/substrates/e2b/base-module.js +40 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +1 -1
- package/dist/substrates/e2b/desktop-browser.js +27 -7
- package/dist/substrates/e2b/desktop-executor.d.ts +7 -0
- package/dist/substrates/e2b/desktop-executor.js +3 -0
- package/dist/substrates/e2b/desktop-handoff.d.ts +22 -0
- package/dist/substrates/e2b/desktop-handoff.js +86 -0
- package/dist/substrates/e2b/sdk.d.ts +24 -0
- package/dist/substrates/e2b/sdk.js +5 -1
- package/dist/substrates/e2b/shell-only.d.ts +5 -0
- package/dist/substrates/e2b/shell-only.js +27 -0
- package/dist/substrates/local/runtime-release.js +8 -8
- package/dist/substrates/modal/sandbox.js +27 -28
- package/dist/substrates/modal/sdk.d.ts +7 -0
- package/dist/tui-app.js +115 -115
- package/docs/architecture/observer.md +7 -3
- package/docs/contracts/schemas.md +24 -3
- package/docs/contracts/study-analysis.md +52 -4
- package/docs/product/automatic-analysis.md +55 -9
- package/package.json +2 -2
- package/skills/humanish/SKILL.md +118 -5
package/dist/cli/findings.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnalysisArtifact, LoadedAnalysis } from "../analysis/types.js";
|
|
2
|
+
import { type RunAnalysisPhase } from "../run/status.js";
|
|
2
3
|
import { type RunStudyProvenance } from "../run/study-provenance.js";
|
|
3
4
|
export declare const ANALYSIS_FINDINGS_SCHEMA = "humanish.analysis-findings.v1";
|
|
4
5
|
type Finding = NonNullable<AnalysisArtifact["result"]>["findings"][number];
|
|
@@ -116,11 +117,35 @@ interface FindingsSource {
|
|
|
116
117
|
liveStudy?: LiveStudy;
|
|
117
118
|
/** The run's study, from its bundle. */
|
|
118
119
|
study?: Pick<RunStudyProvenance, "id" | "path">;
|
|
120
|
+
/** The analyst the run's study gets, read only for an analysis skipped for a missing key. */
|
|
121
|
+
analyst?: Analyst;
|
|
119
122
|
/**
|
|
120
123
|
* Whether Codex is signed in with a ChatGPT account on this machine, read only for an analysis
|
|
121
124
|
* skipped for a missing OpenAI key, whose next step it picks.
|
|
122
125
|
*/
|
|
123
126
|
codexSignedIn?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Where the run is before its findings, from status.json: its participants still running, or
|
|
129
|
+
* the automatic analysis its own process started. Undefined once neither applies.
|
|
130
|
+
*/
|
|
131
|
+
progress?: {
|
|
132
|
+
stage: "participants";
|
|
133
|
+
}
|
|
134
|
+
/** The run ended while a participant stream was still running: nothing complete to analyze. */
|
|
135
|
+
| {
|
|
136
|
+
stage: "cut-off";
|
|
137
|
+
running: number;
|
|
138
|
+
streams: number;
|
|
139
|
+
} | {
|
|
140
|
+
stage: "analysis";
|
|
141
|
+
phase: RunAnalysisPhase;
|
|
142
|
+
/** status.json's last heartbeat. */
|
|
143
|
+
updatedAt: string;
|
|
144
|
+
/** The analyst the run's study gets, read only for an analysis whose process stopped. */
|
|
145
|
+
analyst?: Analyst;
|
|
146
|
+
};
|
|
147
|
+
/** When the view was built, for how long an analysis has run. */
|
|
148
|
+
nowMs?: number;
|
|
124
149
|
}
|
|
125
150
|
/** A study `run` can start live after a dry run. */
|
|
126
151
|
interface LiveStudy {
|
|
@@ -133,6 +158,11 @@ interface LiveStudy {
|
|
|
133
158
|
}
|
|
134
159
|
/** ` --cwd <dir>`, or "" when `cwd` is the current directory. */
|
|
135
160
|
export declare function cwdFlag(cwd: string): string;
|
|
161
|
+
/** An analysis provider and model, as a study or an analysis record names them. */
|
|
162
|
+
interface Analyst {
|
|
163
|
+
provider?: string | undefined;
|
|
164
|
+
model?: string;
|
|
165
|
+
}
|
|
136
166
|
/** The findings view of a run's selected analysis, or why it has none. */
|
|
137
167
|
export declare function analysisFindings(source: FindingsSource): AnalysisFindings;
|
|
138
168
|
/**
|
package/dist/cli/findings.js
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
// starts an analysis.
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { costRefusal } from "../analysis/admission.js";
|
|
8
|
-
import { DEFAULT_ANALYSIS_MAX_COST_USD } from "../analysis/automatic-config.js";
|
|
8
|
+
import { DEFAULT_ANALYSIS_MAX_COST_USD, resolveAutomaticAnalysis, } from "../analysis/automatic-config.js";
|
|
9
|
+
import { ANALYSIS_USUALLY_TAKES } from "../analysis/analysis-limits.js";
|
|
9
10
|
import { bySeverity } from "../analysis/finding-lead.js";
|
|
10
11
|
import { loadAnalysis } from "../analysis/load.js";
|
|
11
12
|
import { projectShareCheckedAnalysis } from "../analysis/sharing.js";
|
|
12
|
-
import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
|
|
13
|
+
import { loadRunBundlePrepared, readRunJsonIfExists, resolveRunPath, runJsonValue, } from "../run/locate.js";
|
|
14
|
+
import { classifyRunStatus, isRunStatusRecord, RUN_STATUS_FILE, runAnalysisPhase, } from "../run/status.js";
|
|
15
|
+
import { TERMINAL_SIMULATION_STATUSES } from "../run/streams.js";
|
|
16
|
+
import { formatDuration } from "../run/projection.js";
|
|
13
17
|
import { resolvePhysicalCwd, runIdOf } from "../run/paths.js";
|
|
14
18
|
import { studyProvenanceOf } from "../run/study-provenance.js";
|
|
15
19
|
import { resolveStudyManifest } from "../study/discover.js";
|
|
@@ -18,6 +22,7 @@ import { plural } from "../run/text.js";
|
|
|
18
22
|
import { shellQuote } from "../substrates/shell.js";
|
|
19
23
|
import { analysisOutcomeText } from "./io.js";
|
|
20
24
|
import { cli } from "./invocation.js";
|
|
25
|
+
import { analysisKeyNames } from "../keys/key-resolution.js";
|
|
21
26
|
export const ANALYSIS_FINDINGS_SCHEMA = "humanish.analysis-findings.v1";
|
|
22
27
|
/** ` --cwd <dir>`, or "" when `cwd` is the current directory. */
|
|
23
28
|
export function cwdFlag(cwd) {
|
|
@@ -25,11 +30,20 @@ export function cwdFlag(cwd) {
|
|
|
25
30
|
return "";
|
|
26
31
|
return ` --cwd ${/^[\w@%+=:,./-]+$/.test(cwd) ? cwd : shellQuote(cwd)}`;
|
|
27
32
|
}
|
|
28
|
-
|
|
33
|
+
/** The skip reasons for an analysis whose API key was missing, one per API analyst. */
|
|
34
|
+
const KEY_MISSING_REASONS = new Set([
|
|
35
|
+
"AUTOMATIC_ANALYSIS_KEY_MISSING",
|
|
36
|
+
"AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING",
|
|
37
|
+
]);
|
|
38
|
+
/** The `analyze` command that runs `analyst` on the run; undefined is the OpenAI default. */
|
|
39
|
+
function analyzeCommand(source, analyst) {
|
|
29
40
|
const run = cli(`analyze --run ${source.runId}${source.cwdFlag}`);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
if (analyst?.provider === "codex")
|
|
42
|
+
return `${run} --provider codex`;
|
|
43
|
+
const gemini = analyst?.provider === "gemini"
|
|
44
|
+
? ` --provider gemini${analyst.model === undefined ? "" : ` --model ${analyst.model}`}`
|
|
45
|
+
: "";
|
|
46
|
+
return `${run}${gemini} --max-cost ${DEFAULT_ANALYSIS_MAX_COST_USD}`;
|
|
33
47
|
}
|
|
34
48
|
/** The cited items the analysis evidence resolves, each once, in citation order. */
|
|
35
49
|
function citedEvidence(ids, analysis, runRoot) {
|
|
@@ -153,16 +167,75 @@ function dryRunFindings(source) {
|
|
|
153
167
|
return withoutFindings(source, "dry_run", null, `${opening} Findings come from live runs: ${howLive}`, cli(`run ${live.run}${source.cwdFlag}`));
|
|
154
168
|
}
|
|
155
169
|
/**
|
|
156
|
-
* An analysis skipped because
|
|
170
|
+
* An analysis skipped because its API key is not set. For OpenAI with Codex signed in, the next step is the
|
|
157
171
|
* Codex account analyst, named as an opt-in: the study chose the OpenAI analyst, and switching
|
|
158
172
|
* the account that pays for it is the operator's choice.
|
|
159
173
|
*/
|
|
160
174
|
function missingKeyFindings(source, reason) {
|
|
175
|
+
// The skip reason records which analyst's key was missing; the study file only adds its model.
|
|
176
|
+
if (reason === "AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING")
|
|
177
|
+
return withoutFindings(source, "skipped", reason, `The automatic analysis was skipped because ${analysisKeyNames("gemini").join(" and ")} are not set. Store the key with ${cli("keys set gemini")}, then run the next command.`, analyzeCommand(source, {
|
|
178
|
+
provider: "gemini",
|
|
179
|
+
...(source.analyst?.provider === "gemini" ? { model: source.analyst.model } : {}),
|
|
180
|
+
}));
|
|
161
181
|
const opening = "The automatic analysis was skipped because OPENAI_API_KEY is not set.";
|
|
162
182
|
if (source.codexSignedIn !== true)
|
|
163
183
|
return withoutFindings(source, "skipped", reason, `${opening} Set it with ${cli("keys set openai")}, then run the next command.`, analyzeCommand(source, undefined));
|
|
164
184
|
const file = source.study?.path === undefined ? "the study file" : source.study.path;
|
|
165
|
-
return withoutFindings(source, "skipped", reason, `${opening} Codex is signed in on this machine, so its ChatGPT account can analyze the run instead, sending the evidence to OpenAI under that account. The next command does it once; review.analysis.provider: codex in ${file} makes it this study's analyst.`, analyzeCommand(source, "codex"));
|
|
185
|
+
return withoutFindings(source, "skipped", reason, `${opening} Codex is signed in on this machine, so its ChatGPT account can analyze the run instead, sending the evidence to OpenAI under that account. The next command does it once; review.analysis.provider: codex in ${file} makes it this study's analyst.`, analyzeCommand(source, { provider: "codex" }));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The view of a run whose findings are not written yet: participants still running, an analysis
|
|
189
|
+
* running, or an analysis whose process stopped before it finished. Undefined otherwise.
|
|
190
|
+
*/
|
|
191
|
+
function pendingFindings(source, job) {
|
|
192
|
+
const again = cli(`review --run ${source.runId}${source.cwdFlag}`);
|
|
193
|
+
const progress = source.progress;
|
|
194
|
+
if (progress?.stage === "participants" && job === undefined)
|
|
195
|
+
return withoutFindings(source, "running", null, source.analysisOffIn === undefined
|
|
196
|
+
? "Participants are still running in this run, so no analysis has started. Automatic analysis starts when they finish, and the findings follow it."
|
|
197
|
+
: "Participants are still running in this run. Analysis is turned off in its study, so none starts when they finish.", again);
|
|
198
|
+
if (progress?.stage === "cut-off" && job === undefined)
|
|
199
|
+
return withoutFindings(source, "skipped", "ANALYSIS_RUN_INCOMPLETE", `This run ended while ${progress.running} of ${progress.streams} participant streams were still running, so its recording is incomplete and no analysis ran. Run the study again with the next command.`, cli(`run ${source.study?.id ?? "<study>"}${source.cwdFlag}`));
|
|
200
|
+
const phase = progress?.stage === "analysis" ? progress.phase : undefined;
|
|
201
|
+
const jobPending = job?.state === "queued" || job?.state === "running";
|
|
202
|
+
if (jobPending || (job === undefined && phase?.state === "running")) {
|
|
203
|
+
const since = phase === undefined
|
|
204
|
+
? ""
|
|
205
|
+
: ` It started at ${phase.startedAt}, ${formatDuration(Math.max(0, (source.nowMs ?? Date.now()) - Date.parse(phase.startedAt)))} ago.`;
|
|
206
|
+
return withoutFindings(source, "running", null, `The automatic analysis is running.${since} Findings usually take ${ANALYSIS_USUALLY_TAKES}; check again then.`, again);
|
|
207
|
+
}
|
|
208
|
+
const stopped = (job?.state === "unknown" && job.reason === "AUTOMATIC_ANALYSIS_STOPPED") ||
|
|
209
|
+
(job === undefined && phase?.state === "stopped");
|
|
210
|
+
if (!stopped)
|
|
211
|
+
return undefined;
|
|
212
|
+
const at = job?.updatedAt ?? (progress?.stage === "analysis" ? progress.updatedAt : undefined);
|
|
213
|
+
return withoutFindings(source, "failed", "AUTOMATIC_ANALYSIS_STOPPED", `The automatic analysis stopped before it wrote findings: the process running it stopped updating its record${at === undefined ? "" : ` at ${at}`}, most likely because it ended. Analyze the run again with the next command.`, analyzeCommand(source, progress?.stage === "analysis" ? progress.analyst : undefined));
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* A run that ended, by a signal, a process that left without publishing, or a heartbeat that
|
|
217
|
+
* stopped, while some participant stream was still running. Undefined for any other run.
|
|
218
|
+
*/
|
|
219
|
+
function cutOff(bundle, record, nowMs) {
|
|
220
|
+
if (bundle === undefined || bundle.outcome?.state === "finished")
|
|
221
|
+
return undefined;
|
|
222
|
+
if (bundle.outcome === undefined &&
|
|
223
|
+
(record === null || classifyRunStatus(record, nowMs) !== "interrupted"))
|
|
224
|
+
return undefined;
|
|
225
|
+
const running = bundle.streams.filter((stream) => !TERMINAL_SIMULATION_STATUSES.has(stream.status) || stream.liveActor !== undefined).length;
|
|
226
|
+
return running === 0 ? undefined : { stage: "cut-off", running, streams: bundle.streams.length };
|
|
227
|
+
}
|
|
228
|
+
/** The analyst the run's study gets, when its file still names the same study. */
|
|
229
|
+
async function studyAnalyst(cwd, study) {
|
|
230
|
+
if (study?.path === undefined)
|
|
231
|
+
return undefined;
|
|
232
|
+
const resolved = await resolveStudyManifest(cwd, study.path).catch(() => null);
|
|
233
|
+
if (!resolved?.ok || resolved.config.id !== study.id)
|
|
234
|
+
return undefined;
|
|
235
|
+
const analysis = resolveAutomaticAnalysis(resolved.config.review?.analysis, resolved.config.actor);
|
|
236
|
+
return analysis.ok && analysis.config !== undefined
|
|
237
|
+
? { provider: analysis.config.provider, model: analysis.config.model }
|
|
238
|
+
: undefined;
|
|
166
239
|
}
|
|
167
240
|
/** The findings view of a run's selected analysis, or why it has none. */
|
|
168
241
|
export function analysisFindings(source) {
|
|
@@ -181,7 +254,9 @@ export function analysisFindings(source) {
|
|
|
181
254
|
message: stale
|
|
182
255
|
? `The run's evidence changed after analysis ${analysis.id}, so its findings may not match the run.`
|
|
183
256
|
: `Analysis ${analysis.id} is ${analysis.status}, with ${plural(result.findings.length, "finding")}.`,
|
|
184
|
-
next: stale
|
|
257
|
+
next: stale
|
|
258
|
+
? analyzeCommand(source, { provider: analysis.provider, model: analysis.config.model })
|
|
259
|
+
: null,
|
|
185
260
|
analysisId: analysis.id,
|
|
186
261
|
status: analysis.status,
|
|
187
262
|
provider: analysis.provider,
|
|
@@ -200,19 +275,20 @@ export function analysisFindings(source) {
|
|
|
200
275
|
if (loaded.warnings.includes("ANALYSIS_SENSITIVE_TEXT_QUARANTINED"))
|
|
201
276
|
return withoutFindings(source, "unavailable", "ANALYSIS_SENSITIVE_TEXT_QUARANTINED", `The analysis text matched a sensitive-text pattern, so it is not printed. ${cli(`analyze show --run ${source.runId}${source.cwdFlag} --json`)} prints the raw record.`, null);
|
|
202
277
|
const job = loaded.automatic;
|
|
203
|
-
|
|
204
|
-
|
|
278
|
+
const pending = pendingFindings(source, job);
|
|
279
|
+
if (pending !== undefined)
|
|
280
|
+
return pending;
|
|
205
281
|
if (analysis && analysis.result === null)
|
|
206
|
-
return withoutFindings(source, "failed", analysis.error, `Analysis attempt ${analysis.id} ${analysis.status === "cancelled" ? "was cancelled" : "failed"}${analysis.error ? ` (${analysis.error})` : ""} and left no findings.`, analyzeCommand(source, analysis.provider));
|
|
282
|
+
return withoutFindings(source, "failed", analysis.error, `Analysis attempt ${analysis.id} ${analysis.status === "cancelled" ? "was cancelled" : "failed"}${analysis.error ? ` (${analysis.error})` : ""} and left no findings.`, analyzeCommand(source, { provider: analysis.provider, model: analysis.config.model }));
|
|
207
283
|
if (job?.state === "failed" || job?.state === "cancelled") {
|
|
208
284
|
const outcome = analysisOutcomeText(job);
|
|
209
|
-
return withoutFindings(source, "failed", job.reason, `The automatic analysis ${outcome.startsWith("failed") ? "" : "was "}${outcome}.`, analyzeCommand(source, job.reason === "AUTOMATIC_ANALYSIS_CODEX_UNAVAILABLE" ? "codex" : undefined));
|
|
285
|
+
return withoutFindings(source, "failed", job.reason, `The automatic analysis ${outcome.startsWith("failed") ? "" : "was "}${outcome}.`, analyzeCommand(source, job.reason === "AUTOMATIC_ANALYSIS_CODEX_UNAVAILABLE" ? { provider: "codex" } : undefined));
|
|
210
286
|
}
|
|
211
287
|
if (job?.state === "skipped" && job.admission) {
|
|
212
288
|
const refusal = costRefusal(job.admission, `--run ${source.runId}${source.cwdFlag}`, cli);
|
|
213
289
|
return withoutFindings(source, "skipped", job.reason, `The automatic analysis was refused before it started. ${refusal.text}`, refusal.command);
|
|
214
290
|
}
|
|
215
|
-
if (job?.state === "skipped" && job.reason
|
|
291
|
+
if (job?.state === "skipped" && job.reason !== null && KEY_MISSING_REASONS.has(job.reason))
|
|
216
292
|
return missingKeyFindings(source, job.reason);
|
|
217
293
|
if (job?.state === "skipped") {
|
|
218
294
|
const refused = job.reason === "AUTOMATIC_ANALYSIS_ADMISSION_REFUSED";
|
|
@@ -278,7 +354,27 @@ export async function readRunAnalysis(cwd, run, analysisId, deps = {}) {
|
|
|
278
354
|
const analysisOffIn = mode === "live" ? await studyAnalysisOff(physical, study) : undefined;
|
|
279
355
|
const liveStudy = mode !== undefined && mode !== "live" ? await liveStudyAfterDryRun(physical, study) : undefined;
|
|
280
356
|
const loaded = await loadAnalysis(prepared, analysisId);
|
|
281
|
-
const
|
|
357
|
+
const nowMs = Date.now();
|
|
358
|
+
const status = runJsonValue(await readRunJsonIfExists(prepared, RUN_STATUS_FILE));
|
|
359
|
+
const record = isRunStatusRecord(status) && status.runId === runIdOf(prepared) ? status : null;
|
|
360
|
+
const phase = record === null ? undefined : runAnalysisPhase(record, nowMs);
|
|
361
|
+
const stopped = phase?.state === "stopped" || loaded.automatic?.reason === "AUTOMATIC_ANALYSIS_STOPPED";
|
|
362
|
+
const missingKeyReason = loaded.automatic?.reason ?? null;
|
|
363
|
+
const keyMissing = missingKeyReason !== null && KEY_MISSING_REASONS.has(missingKeyReason);
|
|
364
|
+
const analyst = stopped || keyMissing ? await studyAnalyst(physical, study) : undefined;
|
|
365
|
+
const progress = phase !== undefined && record !== null
|
|
366
|
+
? {
|
|
367
|
+
stage: "analysis",
|
|
368
|
+
phase,
|
|
369
|
+
updatedAt: record.updatedAt,
|
|
370
|
+
...(analyst === undefined ? {} : { analyst }),
|
|
371
|
+
}
|
|
372
|
+
: bundle?.bundle.outcome === undefined &&
|
|
373
|
+
record !== null &&
|
|
374
|
+
classifyRunStatus(record, nowMs) === "running"
|
|
375
|
+
? { stage: "participants" }
|
|
376
|
+
: cutOff(bundle?.bundle, record, nowMs);
|
|
377
|
+
const codexSignedIn = missingKeyReason === "AUTOMATIC_ANALYSIS_KEY_MISSING"
|
|
282
378
|
? await (deps.codexSignedIn ?? codexSignedInHere)().catch(() => false)
|
|
283
379
|
: undefined;
|
|
284
380
|
return {
|
|
@@ -293,6 +389,9 @@ export async function readRunAnalysis(cwd, run, analysisId, deps = {}) {
|
|
|
293
389
|
? {}
|
|
294
390
|
: { study: { id: study.id, ...(study.path === undefined ? {} : { path: study.path }) } }),
|
|
295
391
|
...(codexSignedIn === undefined ? {} : { codexSignedIn }),
|
|
392
|
+
...(keyMissing && analyst !== undefined ? { analyst } : {}),
|
|
393
|
+
...(progress === undefined ? {} : { progress }),
|
|
394
|
+
nowMs,
|
|
296
395
|
};
|
|
297
396
|
}
|
|
298
397
|
async function codexSignedInHere() {
|
package/dist/cli/io.d.ts
CHANGED
|
@@ -45,6 +45,14 @@ export interface StudyCommandOptions {
|
|
|
45
45
|
port?: string | undefined;
|
|
46
46
|
rerunFailedFrom?: string | undefined;
|
|
47
47
|
runId?: string | undefined;
|
|
48
|
+
/** Run the study in a runner sandbox on its participants' provider. */
|
|
49
|
+
remote?: boolean | undefined;
|
|
50
|
+
/** Hidden: the run key the run's sandboxes carry, chosen by a remote run's launcher. */
|
|
51
|
+
ownerKey?: string | undefined;
|
|
52
|
+
/** Hidden, watch only: bind the served Observer on every interface (a remote run's runner). */
|
|
53
|
+
allInterfaces?: boolean | undefined;
|
|
54
|
+
/** Hidden, watch only: also write the run's result and exit code to this file as JSON. */
|
|
55
|
+
resultFile?: string | undefined;
|
|
48
56
|
/** watch only: serve and follow the Observer even without an interactive terminal. */
|
|
49
57
|
serve?: boolean | undefined;
|
|
50
58
|
/** Repo-relative path to an adopter scorer module; overrides review.scorer.ref when set. */
|
package/dist/cli/io.js
CHANGED
|
@@ -2,7 +2,7 @@ import { automaticAnalysisSucceeded, } from "../analysis/automatic-completion.js
|
|
|
2
2
|
import { costRefusal, refusedCost } from "../analysis/admission.js";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { loadEnvFile, recordDotenvNames } from "../keys/env-file.js";
|
|
5
|
-
import { discoverProviderKeys } from "../keys/key-resolution.js";
|
|
5
|
+
import { analysisKeyNames, discoverProviderKeys } from "../keys/key-resolution.js";
|
|
6
6
|
import { deriveRunFacts } from "./telemetry.js";
|
|
7
7
|
import { warnAndQueue, withQueuedWarnings } from "./deprecations.js";
|
|
8
8
|
import { forTerminal } from "../routes/terminal/encoding.js";
|
|
@@ -297,6 +297,7 @@ export function wantsJson(command) {
|
|
|
297
297
|
const ANALYSIS_REASON_TEXT = {
|
|
298
298
|
AUTOMATIC_ANALYSIS_DRY_RUN: "skipped for dry runs",
|
|
299
299
|
AUTOMATIC_ANALYSIS_KEY_MISSING: "skipped because OPENAI_API_KEY is not set",
|
|
300
|
+
AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING: `skipped because ${analysisKeyNames("gemini").join(" and ")} are not set`,
|
|
300
301
|
AUTOMATIC_ANALYSIS_NO_PARTICIPANT_EVIDENCE: "skipped because no participant left evidence",
|
|
301
302
|
AUTOMATIC_ANALYSIS_ACTOR_CANCELLED: "skipped because the run stopped before its participants finished",
|
|
302
303
|
AUTOMATIC_ANALYSIS_SOURCE_UNAVAILABLE: "skipped because the run's evidence could not be read",
|
|
@@ -315,6 +316,7 @@ const ANALYSIS_REASON_TEXT = {
|
|
|
315
316
|
AUTOMATIC_ANALYSIS_CANCELLATION_UNAVAILABLE: "outcome unknown: a cancellation could not be recorded",
|
|
316
317
|
AUTOMATIC_ANALYSIS_STORAGE_UNAVAILABLE: "outcome unknown: analysis storage is unavailable",
|
|
317
318
|
AUTOMATIC_ANALYSIS_OUTCOME_UNKNOWN: "outcome unknown",
|
|
319
|
+
AUTOMATIC_ANALYSIS_STOPPED: "outcome unknown: the process running it stopped before it finished",
|
|
318
320
|
};
|
|
319
321
|
export function analysisOutcomeText(analysis) {
|
|
320
322
|
if (analysis.reason === null)
|
|
@@ -4,4 +4,15 @@
|
|
|
4
4
|
* place so browser-backed routes can prefer product pixels over browser UI.
|
|
5
5
|
*/
|
|
6
6
|
export declare const CHROMIUM_EVIDENCE_HYGIENE_FLAGS: readonly ["--disable-background-networking", "--disable-component-update", "--disable-default-apps", "--disable-domain-reliability", "--disable-extensions", "--disable-sync", "--no-default-browser-check", "--no-first-run", "--password-store=basic", "--use-mock-keychain"];
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The profile preference that lets a web `targetUrl`'s origin open the external app schemes
|
|
9
|
+
* without Chrome's prompt: the "always allow" choice Chrome stores per origin, which needs no root
|
|
10
|
+
* and no managed policy. Links from other origins still prompt. Undefined for any other target.
|
|
11
|
+
*/
|
|
12
|
+
export declare function externalAppPreferences(targetUrl: string | undefined): {
|
|
13
|
+
protocol_handler: {
|
|
14
|
+
allowed_origin_protocol_pairs: Record<string, Record<string, true>>;
|
|
15
|
+
};
|
|
16
|
+
} | undefined;
|
|
17
|
+
/** The profile's Preferences file, with externalAppPreferences for a web `targetUrl`. */
|
|
18
|
+
export declare function chromiumEvidenceProfilePreferencesJson(targetUrl?: string): string;
|
|
@@ -37,6 +37,49 @@ const CHROMIUM_EVIDENCE_PROFILE_PREFERENCES = {
|
|
|
37
37
|
password_manager_enabled: false,
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Schemes whose links Chrome hands to another app after asking "Open xdg-open?". The desktop's
|
|
42
|
+
* stand-ins answer them (src/substrates/e2b/desktop-handoff.ts), so the prompt is browser chrome
|
|
43
|
+
* a person on their own computer would not see for that app. Chrome never asks for mailto.
|
|
44
|
+
*/
|
|
45
|
+
const EXTERNAL_APP_SCHEMES = [
|
|
46
|
+
"callto",
|
|
47
|
+
"facetime",
|
|
48
|
+
"facetime-audio",
|
|
49
|
+
"geo",
|
|
50
|
+
"maps",
|
|
51
|
+
"mms",
|
|
52
|
+
"msteams",
|
|
53
|
+
"sip",
|
|
54
|
+
"skype",
|
|
55
|
+
"slack",
|
|
56
|
+
"sms",
|
|
57
|
+
"tel",
|
|
58
|
+
"webcal",
|
|
59
|
+
"whatsapp",
|
|
60
|
+
"zoommtg",
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* The profile preference that lets a web `targetUrl`'s origin open the external app schemes
|
|
64
|
+
* without Chrome's prompt: the "always allow" choice Chrome stores per origin, which needs no root
|
|
65
|
+
* and no managed policy. Links from other origins still prompt. Undefined for any other target.
|
|
66
|
+
*/
|
|
67
|
+
export function externalAppPreferences(targetUrl) {
|
|
68
|
+
const target = targetUrl === undefined ? null : URL.parse(targetUrl);
|
|
69
|
+
if (target === null || !/^https?:$/.test(target.protocol))
|
|
70
|
+
return undefined;
|
|
71
|
+
return {
|
|
72
|
+
protocol_handler: {
|
|
73
|
+
allowed_origin_protocol_pairs: {
|
|
74
|
+
[target.origin]: Object.fromEntries(EXTERNAL_APP_SCHEMES.map((scheme) => [scheme, true])),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The profile's Preferences file, with externalAppPreferences for a web `targetUrl`. */
|
|
80
|
+
export function chromiumEvidenceProfilePreferencesJson(targetUrl) {
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
...CHROMIUM_EVIDENCE_PROFILE_PREFERENCES,
|
|
83
|
+
...externalAppPreferences(targetUrl),
|
|
84
|
+
});
|
|
42
85
|
}
|
|
@@ -47,7 +47,7 @@ function omittedReason(relative) {
|
|
|
47
47
|
return "automatic execution intent and liveness do not transfer to a derivative";
|
|
48
48
|
if (isAnalysisRecordPath(relative))
|
|
49
49
|
return "analysis records bind original evidence hashes; reanalyze the derivative";
|
|
50
|
-
if (relative.startsWith(".analysis-lock
|
|
50
|
+
if (relative.startsWith(".analysis-lock"))
|
|
51
51
|
return "local analysis execution lock";
|
|
52
52
|
return (OMITTED.get(relative) ??
|
|
53
53
|
(["feedback/draft.json", "feedback/issue.md"].includes(relative)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CuaExecutor } from "../actors/computer-use/loop.js";
|
|
2
|
+
/** Where the image keeps the stand-ins. */
|
|
3
|
+
export declare const GUEST_EXTERNAL_LINK_BIN = "/opt/humanish/control/bin";
|
|
4
|
+
/**
|
|
5
|
+
* Write the browser profile's Preferences before the first launch, so the initial app's origin
|
|
6
|
+
* opens tel:, sms: and the other app schemes without Chromium's prompt. No initial URL, no file.
|
|
7
|
+
*/
|
|
8
|
+
export declare function prepareGuestBrowserProfile(profileDir: string, initialUrl: string | undefined): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* The executor with each observation carrying the links appended to `log` since the previous
|
|
11
|
+
* observation, at most EXTERNAL_HANDOFF_LIMITS.perObservation. A line still being written waits
|
|
12
|
+
* for the next observation. Every other property of the executor is kept.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withExternalHandoffs(executor: CuaExecutor, log: string): CuaExecutor;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Links the guest browser hands to another app (mailto:, tel:, sms:). The guest has no email,
|
|
2
|
+
// phone or messaging app. The image's stand-ins for xdg-open and xdg-email, first on the guest's
|
|
3
|
+
// search path, append each such link to a log in the runtime directory, and every observation the
|
|
4
|
+
// executor returns carries the links appended since the previous one.
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { EXTERNAL_HANDOFF_LIMITS, externalHandoffOf, } from "../actors/computer-use/external-handoff.js";
|
|
7
|
+
import { externalAppPreferences } from "../evidence/browser-hygiene.js";
|
|
8
|
+
/** Where the image keeps the stand-ins. */
|
|
9
|
+
export const GUEST_EXTERNAL_LINK_BIN = "/opt/humanish/control/bin";
|
|
10
|
+
/**
|
|
11
|
+
* Write the browser profile's Preferences before the first launch, so the initial app's origin
|
|
12
|
+
* opens tel:, sms: and the other app schemes without Chromium's prompt. No initial URL, no file.
|
|
13
|
+
*/
|
|
14
|
+
export async function prepareGuestBrowserProfile(profileDir, initialUrl) {
|
|
15
|
+
const preferences = externalAppPreferences(initialUrl);
|
|
16
|
+
if (preferences === undefined)
|
|
17
|
+
return;
|
|
18
|
+
await mkdir(`${profileDir}/Default`, { recursive: true, mode: 0o700 });
|
|
19
|
+
await writeFile(`${profileDir}/Default/Preferences`, JSON.stringify(preferences), {
|
|
20
|
+
mode: 0o600,
|
|
21
|
+
flag: "wx",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The executor with each observation carrying the links appended to `log` since the previous
|
|
26
|
+
* observation, at most EXTERNAL_HANDOFF_LIMITS.perObservation. A line still being written waits
|
|
27
|
+
* for the next observation. Every other property of the executor is kept.
|
|
28
|
+
*/
|
|
29
|
+
export function withExternalHandoffs(executor, log) {
|
|
30
|
+
let consumed = 0;
|
|
31
|
+
return {
|
|
32
|
+
...executor,
|
|
33
|
+
observe: async () => {
|
|
34
|
+
const observation = await executor.observe();
|
|
35
|
+
const text = await readFile(log, "utf8").catch(() => "");
|
|
36
|
+
const lines = text
|
|
37
|
+
.split("\n")
|
|
38
|
+
.slice(0, -1)
|
|
39
|
+
.slice(consumed, consumed + EXTERNAL_HANDOFF_LIMITS.perObservation);
|
|
40
|
+
consumed += lines.length;
|
|
41
|
+
const externalHandoffs = lines.flatMap((line) => {
|
|
42
|
+
const handoff = externalHandoffOf(line);
|
|
43
|
+
return handoff === undefined ? [] : [handoff];
|
|
44
|
+
});
|
|
45
|
+
return externalHandoffs.length === 0 ? observation : { ...observation, externalHandoffs };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -14,6 +14,7 @@ import { GUEST_BOOTSTRAP_LIMITS, validateGuestInitialUrl } from "./bootstrap.js"
|
|
|
14
14
|
import { startDesktopMedia, } from "./desktop-media.js";
|
|
15
15
|
import { startDesktopRecorder } from "../evidence/desktop-recorder.js";
|
|
16
16
|
import { createGuestProcesses, setupFailed } from "./runtime-processes.js";
|
|
17
|
+
import { GUEST_EXTERNAL_LINK_BIN, prepareGuestBrowserProfile, withExternalHandoffs, } from "./external-links.js";
|
|
17
18
|
/** The guest image's fixed layout. Tests derive guest paths from it rather than spelling them. */
|
|
18
19
|
export const GUEST_RUNTIME_PATHS = Object.freeze({
|
|
19
20
|
root: "/opt/humanish/control",
|
|
@@ -21,7 +22,8 @@ export const GUEST_RUNTIME_PATHS = Object.freeze({
|
|
|
21
22
|
home: "/home/humanish",
|
|
22
23
|
});
|
|
23
24
|
const GUEST_RUNTIME_ENV = Object.freeze({
|
|
24
|
-
|
|
25
|
+
// The stand-ins come first, so the browser hands a mailto: or tel: link to them.
|
|
26
|
+
PATH: `${GUEST_EXTERNAL_LINK_BIN}:/usr/bin:/bin`,
|
|
25
27
|
HOME: "/home/humanish",
|
|
26
28
|
USER: "humanish",
|
|
27
29
|
LOGNAME: "humanish",
|
|
@@ -390,6 +392,7 @@ export async function createGuestRuntimeDesktop(options) {
|
|
|
390
392
|
progress("layout");
|
|
391
393
|
check();
|
|
392
394
|
await verifyGuestLayout(check);
|
|
395
|
+
await prepareGuestBrowserProfile(`${GUEST_RUNTIME_PATHS.home}/browser`, options.initialUrl);
|
|
393
396
|
progress("xauthority");
|
|
394
397
|
await authorizeDisplay(processes, check);
|
|
395
398
|
progress("display");
|
|
@@ -434,7 +437,7 @@ export async function createGuestRuntimeDesktop(options) {
|
|
|
434
437
|
await openGuestFixture(browser.page, check);
|
|
435
438
|
}
|
|
436
439
|
return {
|
|
437
|
-
executor: state.media?.wrap(executor) ?? executor,
|
|
440
|
+
executor: withExternalHandoffs(state.media?.wrap(executor) ?? executor, `${GUEST_RUNTIME_ENV.XDG_RUNTIME_DIR}/humanish-links.log`),
|
|
438
441
|
close,
|
|
439
442
|
...(options.recording === undefined
|
|
440
443
|
? {}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** The key each API analyst reads, under its first spelling. */
|
|
2
|
+
declare const ANALYSIS_KEYS: {
|
|
3
|
+
readonly openai: "OPENAI_API_KEY";
|
|
4
|
+
readonly gemini: "GEMINI_API_KEY";
|
|
5
|
+
};
|
|
6
|
+
/** The env names an API analyst's key may have, in the order they are read. */
|
|
7
|
+
export declare const analysisKeyNames: (vendor: keyof typeof ANALYSIS_KEYS) => readonly string[];
|
|
8
|
+
/** An API analyst's key from `env`: the first of its names with a non-blank value, else "". */
|
|
9
|
+
export declare function analysisApiKey(vendor: keyof typeof ANALYSIS_KEYS, env: Readonly<Record<string, string | undefined>>): string;
|
|
1
10
|
/** The vendor aliases `humanish keys set` accepts. A raw ENV_NAME is also accepted. */
|
|
2
11
|
export declare const KEY_VENDORS: string[];
|
|
3
12
|
/** The keys `humanish keys` lists, in its order, each with what humanish uses it for. */
|
|
@@ -77,3 +86,4 @@ export declare function setUserKey(name: string, value: string, env: NodeJS.Proc
|
|
|
77
86
|
export declare function unsetUserKey(name: string, env: NodeJS.ProcessEnv, deps?: KeyResolutionDeps): boolean;
|
|
78
87
|
/** The names (never values) currently in the user store. */
|
|
79
88
|
export declare function listUserKeys(env: NodeJS.ProcessEnv, deps?: KeyResolutionDeps): string[];
|
|
89
|
+
export {};
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// GH_TOKEN <- `gh auth token` (the gh CLI's credential chain);
|
|
12
12
|
// 4. the humanish user-level store `$XDG_CONFIG_HOME/humanish/keys.env`, written only by
|
|
13
13
|
// `humanish keys set` (0600, prompted or --stdin), the fallback for vendors that ship
|
|
14
|
-
// no machine chain of their own (OpenAI, Anthropic).
|
|
14
|
+
// no machine chain of their own (OpenAI, Anthropic, Gemini).
|
|
15
15
|
//
|
|
16
16
|
// Every fill is announced by key name + source, never by value. The subject app's own
|
|
17
17
|
// `.env`/`.env.local` are never read: those hold product credentials, a different class
|
|
@@ -34,7 +34,8 @@ import { OPENAI_EGRESS_PLACEHOLDER } from "../routes/terminal/runtime-auth.js";
|
|
|
34
34
|
* full-file load). ANTHROPIC_API_KEY is not listed, and doctor does not report it: a Claude Code
|
|
35
35
|
* participant runs on its own login and never receives it. CODEX_API_KEY is an alternative spelling
|
|
36
36
|
* of the OpenAI key for terminal studies. Doctor reports AGENTMAIL_API_KEY only under `--study`, for
|
|
37
|
-
* a study whose email connection names it, since a connection may name another variable
|
|
37
|
+
* a study whose email connection names it, since a connection may name another variable, and
|
|
38
|
+
* GEMINI_API_KEY only for a study whose analysis runs on Gemini.
|
|
38
39
|
*/
|
|
39
40
|
const PROVIDER_KEYS = [
|
|
40
41
|
{
|
|
@@ -69,9 +70,33 @@ const PROVIDER_KEYS = [
|
|
|
69
70
|
{ name: "GITHUB_TOKEN", spellingOf: "GH_TOKEN" },
|
|
70
71
|
{ name: "CODEX_API_KEY", doctor: true },
|
|
71
72
|
{ name: "AGENTMAIL_API_KEY", vendor: "agentmail", listed: { order: 4, use: "email in studies" } },
|
|
73
|
+
{ name: "GEMINI_API_KEY", vendor: "gemini", listed: { order: 5, use: "analysis on Gemini" } },
|
|
74
|
+
// Google's client libraries also read GOOGLE_API_KEY, and prefer it when both are set. humanish
|
|
75
|
+
// reads GEMINI_API_KEY first: a GOOGLE_API_KEY exported for Maps or Firebase must not take the
|
|
76
|
+
// place of the key `humanish keys set gemini` stored.
|
|
77
|
+
{ name: "GOOGLE_API_KEY", spellingOf: "GEMINI_API_KEY" },
|
|
72
78
|
];
|
|
73
79
|
const PROVIDER_KEY_SET = new Set(PROVIDER_KEYS.map((key) => key.name));
|
|
74
80
|
const providerKey = (name) => PROVIDER_KEYS.find((key) => key.name === name);
|
|
81
|
+
/**
|
|
82
|
+
* Every env name of the key `name` is a spelling of, in the order humanish reads them: the key's
|
|
83
|
+
* own name, then its second spellings. Discovery treats them as one key.
|
|
84
|
+
*/
|
|
85
|
+
function keyNames(name) {
|
|
86
|
+
const own = providerKey(name)?.spellingOf ?? name;
|
|
87
|
+
return [own, ...PROVIDER_KEYS.filter((key) => key.spellingOf === own).map((key) => key.name)];
|
|
88
|
+
}
|
|
89
|
+
/** The key each API analyst reads, under its first spelling. */
|
|
90
|
+
const ANALYSIS_KEYS = { openai: "OPENAI_API_KEY", gemini: "GEMINI_API_KEY" };
|
|
91
|
+
/** The env names an API analyst's key may have, in the order they are read. */
|
|
92
|
+
export const analysisKeyNames = (vendor) => keyNames(ANALYSIS_KEYS[vendor]);
|
|
93
|
+
/** An API analyst's key from `env`: the first of its names with a non-blank value, else "". */
|
|
94
|
+
export function analysisApiKey(vendor, env) {
|
|
95
|
+
for (const name of analysisKeyNames(vendor))
|
|
96
|
+
if (env[name]?.trim())
|
|
97
|
+
return env[name];
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
75
100
|
/** The vendor aliases `humanish keys set` accepts. A raw ENV_NAME is also accepted. */
|
|
76
101
|
export const KEY_VENDORS = PROVIDER_KEYS.flatMap((key) => key.vendor === undefined ? [] : [key.vendor]);
|
|
77
102
|
/** The keys `humanish keys` lists, in its order, each with what humanish uses it for. */
|
|
@@ -212,8 +237,11 @@ export async function discoverProviderKeys(args) {
|
|
|
212
237
|
const ignored = [];
|
|
213
238
|
// Fill-only means presence wins, including an explicitly-set empty value: an operator who
|
|
214
239
|
// exported OPENAI_API_KEY="" said "off", and a rung that "fixes" that has overridden them.
|
|
215
|
-
// Only a truly-unset name is fillable
|
|
216
|
-
|
|
240
|
+
// Only a truly-unset name is fillable, and only while no spelling of its key was set before
|
|
241
|
+
// discovery: a repo overlay's GOOGLE_API_KEY must not stand beside, and outrank, an exported
|
|
242
|
+
// GEMINI_API_KEY. Spellings that discovery itself fills may stand together.
|
|
243
|
+
const explicit = new Set(Object.keys(env).filter((name) => env[name] !== undefined));
|
|
244
|
+
const fillable = (name) => env[name] === undefined && keyNames(name).every((spelling) => !explicit.has(spelling));
|
|
217
245
|
const fill = (name, value, source) => {
|
|
218
246
|
env[name] = value;
|
|
219
247
|
fills.push({ name, source });
|
|
@@ -259,7 +287,7 @@ export async function discoverProviderKeys(args) {
|
|
|
259
287
|
// Rung 3c: the gh CLI's credential chain. Only consulted when neither GitHub env name is
|
|
260
288
|
// present; fills GH_TOKEN (the name humanish reads first). gh runs without any provider key in
|
|
261
289
|
// its environment, so the keys the rungs above just filled stay in this process.
|
|
262
|
-
if (
|
|
290
|
+
if (keyNames("GH_TOKEN").every((name) => env[name] === undefined)) {
|
|
263
291
|
const exec = deps.execText ?? defaultExecText;
|
|
264
292
|
const token = await exec("gh", ["auth", "token"], 3_000, withoutProviderKeys(env));
|
|
265
293
|
if (token !== null)
|
package/dist/observer/data.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface ObserverData {
|
|
|
23
23
|
state: "running" | "finished" | "interrupted" | "unknown";
|
|
24
24
|
observedAt: string;
|
|
25
25
|
source: "local-run-status";
|
|
26
|
+
/** Present while the run's own process analyzes it after the run ended: when it started. */
|
|
27
|
+
analysis?: {
|
|
28
|
+
startedAt: string;
|
|
29
|
+
};
|
|
26
30
|
};
|
|
27
31
|
run: {
|
|
28
32
|
runId: string;
|
|
@@ -12,5 +12,10 @@ export declare class PortInUseError extends Error {
|
|
|
12
12
|
* that cannot decide says "other", which is the answer that suggests the safer action.
|
|
13
13
|
*/
|
|
14
14
|
export declare function probePortHolder(port: number, fetchFn?: typeof fetch): Promise<PortHolder>;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Bind on loopback; resolve the port that was actually bound; name a taken port. `allInterfaces`
|
|
17
|
+
* binds every interface instead, for the Observer of a remote run's runner sandbox: Modal's proxy
|
|
18
|
+
* cannot reach a loopback-only server, and only the provider's authenticated proxy reaches the
|
|
19
|
+
* sandbox's ports.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listenOnLoopback(server: Server, port: number, probe?: (port: number) => Promise<PortHolder>, allInterfaces?: boolean): Promise<number>;
|
package/dist/observer/listen.js
CHANGED
|
@@ -61,8 +61,13 @@ async function probeRoutes(port, fetchFn) {
|
|
|
61
61
|
}
|
|
62
62
|
return "other";
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Bind on loopback; resolve the port that was actually bound; name a taken port. `allInterfaces`
|
|
66
|
+
* binds every interface instead, for the Observer of a remote run's runner sandbox: Modal's proxy
|
|
67
|
+
* cannot reach a loopback-only server, and only the provider's authenticated proxy reaches the
|
|
68
|
+
* sandbox's ports.
|
|
69
|
+
*/
|
|
70
|
+
export function listenOnLoopback(server, port, probe = probePortHolder, allInterfaces = false) {
|
|
66
71
|
return new Promise((resolve, reject) => {
|
|
67
72
|
const onError = (error) => {
|
|
68
73
|
if (error.code === "EADDRINUSE") {
|
|
@@ -72,8 +77,9 @@ export function listenOnLoopback(server, port, probe = probePortHolder) {
|
|
|
72
77
|
reject(error);
|
|
73
78
|
};
|
|
74
79
|
server.once("error", onError);
|
|
75
|
-
//
|
|
76
|
-
|
|
80
|
+
// Otherwise never beyond loopback; exposure only ever happens through a tunnel or proxy
|
|
81
|
+
// forwarding here.
|
|
82
|
+
server.listen(port, allInterfaces ? "0.0.0.0" : "127.0.0.1", () => {
|
|
77
83
|
server.off("error", onError);
|
|
78
84
|
const address = server.address();
|
|
79
85
|
if (!address || typeof address === "string") {
|