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
package/dist/study/doctor.js
CHANGED
|
@@ -2,11 +2,13 @@ import path from "node:path";
|
|
|
2
2
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
3
3
|
import { localRuntimeStatus } from "../substrates/local/runtime.js";
|
|
4
4
|
import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
5
|
+
import { localAgentReady, localAgentVersionRefusal, } from "../actors/local-agent/cli.js";
|
|
5
6
|
import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
|
|
6
7
|
import { automaticAnalysisBudget } from "../analysis/automatic-config.js";
|
|
7
8
|
import { externalCatchHealthy } from "../comms/sandbox-catch.js";
|
|
8
9
|
import { receivingRequiredKey } from "../comms/setup.js";
|
|
9
10
|
import { classifyCodexInstallation, codexInstallAdvice, codexVersionRecovery, } from "../actors/codex/codex-admission.js";
|
|
11
|
+
import { cli } from "../cli/invocation.js";
|
|
10
12
|
/** A ready row's message, with the schema values the launch recorded. */
|
|
11
13
|
function readyMessage(message, readiness) {
|
|
12
14
|
const additions = protocolAdditionsWarning(readiness.cliVersion, readiness.protocolAdditions);
|
|
@@ -211,7 +213,7 @@ async function localBrowserChecks(config, args) {
|
|
|
211
213
|
ok: healthy,
|
|
212
214
|
message: healthy
|
|
213
215
|
? "Recipient inbox routes are ready. Point your app's email sends at this catch; delivery remains untested. No mailbox-provider credentials are needed, and this does not receive arbitrary internet mail."
|
|
214
|
-
:
|
|
216
|
+
: `Captured inbox is unavailable or outdated. Start or upgrade and restart ${cli("comms catch")}, then check comms.email.external.catchBaseUrl (and inboxBaseUrl if set). No participant was allocated.`,
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
219
|
return checks;
|
|
@@ -228,7 +230,7 @@ async function realEmailCheck(connection, keys, args) {
|
|
|
228
230
|
? "The selected email connection is missing or invalid. Open Connections in the TUI."
|
|
229
231
|
: !args.keyPresent(name)
|
|
230
232
|
? `Missing ${name} for the selected email connection. Provide it through process env or --dotenv. Authentication has not been checked.`
|
|
231
|
-
:
|
|
233
|
+
: `Fresh hosted inbox per participant. Local presence only; run ${cli("comms check --online")} to authenticate. Provider permissions/capacity and delivery remain untested.`,
|
|
232
234
|
};
|
|
233
235
|
}
|
|
234
236
|
/** How the participant authenticates: the terminal model key, or a local agent's login. */
|
|
@@ -254,12 +256,14 @@ async function participantChecks(config, route, keys, local, args, checkAccount)
|
|
|
254
256
|
// that the operator's config, release and model admit a launch.
|
|
255
257
|
if (choice === "codex" && agent?.authStatus === "authenticated")
|
|
256
258
|
return [await hostedCodexParticipantCheck(args, config.actors[0])];
|
|
259
|
+
const outdated = agent === undefined ? undefined : localAgentVersionRefusal(agent);
|
|
257
260
|
return [
|
|
258
261
|
{
|
|
259
262
|
name: "local participant authentication",
|
|
260
|
-
ok: agent
|
|
263
|
+
ok: agent !== undefined && localAgentReady(agent),
|
|
261
264
|
message: agent?.authStatus === "authenticated"
|
|
262
|
-
?
|
|
265
|
+
? (outdated ??
|
|
266
|
+
`${agent.label}${agent.version === undefined ? "" : ` ${agent.version}`} reports authenticated on the host. E2B supplies the desktop; no OpenAI API key is required for this participant.`)
|
|
263
267
|
: agent
|
|
264
268
|
? `${agent.label} ${agent.authStatus === "unauthenticated" ? "reports not signed in" : "authentication could not be checked"}. Run \`${choice === "codex" ? "codex login status" : "claude auth status"}\`; sign in or update the CLI before running.`
|
|
265
269
|
: `${choice} is not on this process's PATH. Install and sign in to that CLI, or choose openai-computer-use with OPENAI_API_KEY.`,
|
|
@@ -194,24 +194,6 @@ steps:
|
|
|
194
194
|
expectation: The user sees a clear next state.
|
|
195
195
|
- name: Capture review notes
|
|
196
196
|
expectation: Notes are public-safe and evidence-backed.
|
|
197
|
-
`,
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
path: "humanish/scenarios/onboarding-regression.yaml",
|
|
201
|
-
plane: "source",
|
|
202
|
-
contents: `schema: humanish.scenario.v1
|
|
203
|
-
id: onboarding-regression
|
|
204
|
-
title: Onboarding regression
|
|
205
|
-
persona: skeptical-power-user
|
|
206
|
-
goal: Exercise onboarding friction using synthetic inputs and explicit recovery checks.
|
|
207
|
-
mode: dry-run
|
|
208
|
-
steps:
|
|
209
|
-
- name: Start onboarding
|
|
210
|
-
expectation: Required information is clear.
|
|
211
|
-
- name: Use synthetic fixture data
|
|
212
|
-
expectation: No real user data is entered.
|
|
213
|
-
- name: Check recovery path
|
|
214
|
-
expectation: The user can back out or retry safely.
|
|
215
197
|
`,
|
|
216
198
|
},
|
|
217
199
|
{
|
|
@@ -308,83 +290,6 @@ execution:
|
|
|
308
290
|
# allowPublicTargets: true # required to drive a non-loopback app-url (a deployment you own)
|
|
309
291
|
defaults:
|
|
310
292
|
open: true
|
|
311
|
-
`,
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
path: "humanish/studies/lobby-trivia-3player.yaml",
|
|
315
|
-
plane: "source",
|
|
316
|
-
contents: `schema: humanish.study.v3
|
|
317
|
-
id: lobby-trivia-3player
|
|
318
|
-
title: "Shared world on a public app: three mobile participants in one lobby"
|
|
319
|
-
description: >-
|
|
320
|
-
A worked example of the external-public shared-world route: three participants on mobile layouts
|
|
321
|
-
play the same multiplayer lobby on a public deployment at the same time. The public site is the
|
|
322
|
-
shared plane, so there is no clone, subject sandbox or seed. The host participant creates the
|
|
323
|
-
lobby, humanish reads the session code from the host's page URL, and the other participants get
|
|
324
|
-
it in their missions and join through the app's own join flow. Starts as a dry run, which writes
|
|
325
|
-
one evidence bundle at no cost. A live run needs OPENAI_API_KEY, E2B_API_KEY and mode: live. Point appUrl and publicTarget at a public deployment you own or operate. Mobile layout:
|
|
326
|
-
Chrome's physical window is at least 500px wide and has no touch emulation, desktop pixel ratio
|
|
327
|
-
and a desktop user agent. Set execution.desktop.fidelity.mobileEmulation to true to request the
|
|
328
|
-
preset's CSS viewport, touch, pixel ratio and mobile user agent on Chrome or Chromium; the bundle
|
|
329
|
-
records the measured geometry and emulation fidelity. A run on a public site proves less than a
|
|
330
|
-
provisioned one: its provenance is external-public, it carries no synthetic-data attestation and
|
|
331
|
-
no authoritative shared-state proof, and concurrency rests on overlapping sessions and every
|
|
332
|
-
participant reaching the same lobby.
|
|
333
|
-
route: shared-world
|
|
334
|
-
mode: dry-run # a live run opens three mobile-layout desktops against the public app
|
|
335
|
-
subject:
|
|
336
|
-
source: app-url # the public deployment is the shared plane: no clone and no getHost
|
|
337
|
-
appUrl: https://your-public-app.example/ # a deployment you own or operate
|
|
338
|
-
publicTarget: # required: your attestation that you own or operate this deployment
|
|
339
|
-
owner: your-org/your-app
|
|
340
|
-
authorized: true
|
|
341
|
-
policies:
|
|
342
|
-
allowPublicTargets: true # required: the shared plane is a public, non-loopback deployment
|
|
343
|
-
actor:
|
|
344
|
-
type: openai-computer-use
|
|
345
|
-
mission: You are one of several users on the same shared session at once. Play your role, then stop.
|
|
346
|
-
participants: # at least two participants, exactly one with host: true
|
|
347
|
-
- id: host
|
|
348
|
-
host: true # this participant creates the shared session
|
|
349
|
-
device: mobile # 414x896 preset; physical window width floors to 500 (see emulation above)
|
|
350
|
-
instruction: Create a shared lobby, wait for the others to join, then start and play.
|
|
351
|
-
- id: player-2
|
|
352
|
-
device: mobile
|
|
353
|
-
instruction: Join the lobby you're told the code for, then play.
|
|
354
|
-
- id: player-3
|
|
355
|
-
device: small-mobile
|
|
356
|
-
instruction: Join the lobby you're told the code for, then play.
|
|
357
|
-
execution:
|
|
358
|
-
target: e2b-desktop
|
|
359
|
-
# Each participant's session budget, which also bounds the host's handoff deadline. It has to
|
|
360
|
-
# cover provisioning, joining the shared session and play; 420000 ms (7 minutes) does.
|
|
361
|
-
timeoutMs: 420000
|
|
362
|
-
concurrency: 3 # all three participants at once, the default; lower it only to limit paid desktops
|
|
363
|
-
# desktop:
|
|
364
|
-
# fidelity: { mobileEmulation: true } # opt into mobile CSS viewport, touch, DPR and user agent
|
|
365
|
-
defaults:
|
|
366
|
-
open: true
|
|
367
|
-
`,
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
path: "humanish/coverage-map.md",
|
|
371
|
-
plane: "source",
|
|
372
|
-
contents: `# Coverage Map
|
|
373
|
-
|
|
374
|
-
This file should enumerate screens, roles, states, and paths before scenarios are treated as complete.
|
|
375
|
-
|
|
376
|
-
Current starter coverage is intentionally minimal and synthetic.
|
|
377
|
-
`,
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
path: "humanish/coverage-matrix.md",
|
|
381
|
-
plane: "source",
|
|
382
|
-
contents: `# Coverage Matrix
|
|
383
|
-
|
|
384
|
-
| Area | Persona | Happy Path | Sad Path | Status |
|
|
385
|
-
| --- | --- | --- | --- | --- |
|
|
386
|
-
| First run | synthetic-new-user | planned | planned | starter |
|
|
387
|
-
| Onboarding | skeptical-power-user | planned | planned | starter |
|
|
388
293
|
`,
|
|
389
294
|
},
|
|
390
295
|
];
|
package/dist/study/init.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { agentsSection, firstRunGuidance, starterActorFor, starterLocalAgentFor, } from "../cli/first-run-path.js";
|
|
4
4
|
import { detectLocalAgents } from "../actors/local-agent/cli.js";
|
|
5
|
+
import { humanishCommand } from "../cli/invocation.js";
|
|
6
|
+
import { replaceAgentsSection } from "./agents-section.js";
|
|
7
|
+
import { probeLocalBrowserHost } from "./local-browser-host.js";
|
|
5
8
|
import { probeKeySources } from "../keys/key-resolution.js";
|
|
6
9
|
import { DEFAULT_LOCAL_BROWSER_STARTER, runtimeDirectories, starterFilesFor, } from "./init-templates.js";
|
|
7
10
|
import { assertPreparedSelectedOutputDirectory, prepareContainedOutputDirectory, prepareSelectedOutputDirectory, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
11
|
+
import { desktopPeerAdvice, humanishInstall, } from "../substrates/e2b/peer-install.js";
|
|
8
12
|
import { validateCwd } from "../run/project.js";
|
|
9
13
|
import { pathExists, readTextIfExists, validateInitProjectPaths } from "./init-paths.js";
|
|
10
14
|
import { planGitignore, planPackageJson } from "./init-plan.js";
|
|
@@ -37,11 +41,11 @@ export async function runInit(options) {
|
|
|
37
41
|
if (initialPathCheck) {
|
|
38
42
|
return failed(initialPathCheck);
|
|
39
43
|
}
|
|
40
|
-
await planAgentsFile(preparedProjectRoot, cwd, plan);
|
|
41
44
|
// The starter live study is written for the brain this machine can actually use. Shipping it as
|
|
42
45
|
// openai-computer-use on a machine with no provider key but a signed-in Codex would hand someone
|
|
43
46
|
// a file that asks for a credential they were just told they do not need.
|
|
44
47
|
const machine = await firstRunEnvironment(options.env ?? process.env, requestedCwd);
|
|
48
|
+
await planAgentsFile(preparedProjectRoot, cwd, plan, humanishCommand());
|
|
45
49
|
const starterActor = starterActorFor(machine);
|
|
46
50
|
await planStarterFiles(preparedProjectRoot, cwd, starterFilesFor(starterActor, localBrowser.value, starterLocalAgentFor(machine)), options.localBrowser !== undefined, plan);
|
|
47
51
|
await planRuntimeDirectories(preparedProjectRoot, cwd, plan);
|
|
@@ -87,47 +91,40 @@ export async function runInit(options) {
|
|
|
87
91
|
// `humanish init` is a coding agent doing setup on someone's behalf. Without this, the agent
|
|
88
92
|
// that arrives tomorrow finds a humanish/ directory and no idea what to do with it.
|
|
89
93
|
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
94
|
+
// An existing `AGENTS.md` is a file someone wrote. init appends its own section once, and on a
|
|
95
|
+
// rerun replaces only that section, between its two markers, so the commands match the installed
|
|
96
|
+
// humanish. Every byte outside the markers stays as it was.
|
|
97
|
+
async function planAgentsFile(preparedProjectRoot, cwd, plan, humanish) {
|
|
93
98
|
const agentsPath = "AGENTS.md";
|
|
94
99
|
const existingAgents = await readTextIfExists(preparedProjectRoot, agentsPath);
|
|
95
|
-
const section = agentsSection();
|
|
96
|
-
|
|
97
|
-
plan.changes.push({
|
|
98
|
-
path: agentsPath,
|
|
99
|
-
action: "create",
|
|
100
|
-
target: "source",
|
|
101
|
-
reason: "how a coding agent runs humanish here",
|
|
102
|
-
});
|
|
100
|
+
const section = agentsSection(humanish);
|
|
101
|
+
const write = (contents, action, reason) => {
|
|
102
|
+
plan.changes.push({ path: agentsPath, action, target: "source", reason });
|
|
103
103
|
plan.writes.push({
|
|
104
104
|
absolutePath: path.join(cwd, agentsPath),
|
|
105
105
|
relativePath: agentsPath,
|
|
106
|
-
contents
|
|
106
|
+
contents,
|
|
107
107
|
target: "source",
|
|
108
108
|
});
|
|
109
|
+
};
|
|
110
|
+
const skip = (reason) => plan.changes.push({ path: agentsPath, action: "skip", target: "source", reason });
|
|
111
|
+
if (existingAgents === null) {
|
|
112
|
+
write(`# AGENTS.md\n${section}`, "create", "how a coding agent runs humanish here");
|
|
113
|
+
return;
|
|
109
114
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
115
|
+
const replaced = replaceAgentsSection(existingAgents, section);
|
|
116
|
+
if (replaced.kind === "absent") {
|
|
117
|
+
write(`${existingAgents.replace(/\s*$/, "")}\n${section}`, "update", "append how a coding agent runs humanish");
|
|
118
|
+
}
|
|
119
|
+
else if (replaced.kind === "unknown") {
|
|
120
|
+
skip("humanish section edited by hand");
|
|
121
|
+
plan.warnings.push(`Left the humanish section of ${agentsPath} unchanged: it has no end marker and does not match a version init wrote, so it may hold your edits. To get the current section, delete it from the "## humanish" heading to the end of the section and run init again.`);
|
|
122
|
+
}
|
|
123
|
+
else if (replaced.contents === existingAgents) {
|
|
124
|
+
skip("humanish section is current");
|
|
117
125
|
}
|
|
118
126
|
else {
|
|
119
|
-
|
|
120
|
-
path: agentsPath,
|
|
121
|
-
action: "update",
|
|
122
|
-
target: "source",
|
|
123
|
-
reason: "append how a coding agent runs humanish",
|
|
124
|
-
});
|
|
125
|
-
plan.writes.push({
|
|
126
|
-
absolutePath: path.join(cwd, agentsPath),
|
|
127
|
-
relativePath: agentsPath,
|
|
128
|
-
contents: `${existingAgents.replace(/\s*$/, "")}\n${section}`,
|
|
129
|
-
target: "source",
|
|
130
|
-
});
|
|
127
|
+
write(replaced.contents, "update", "refresh the humanish section");
|
|
131
128
|
}
|
|
132
129
|
}
|
|
133
130
|
/** Create each starter file that is missing; never overwrite one that exists. */
|
|
@@ -270,6 +267,18 @@ function validateLocalBrowserStarter(input) {
|
|
|
270
267
|
return { ok: false, message: "--local-mission must be 4,000 characters or fewer." };
|
|
271
268
|
return { ok: true, value };
|
|
272
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* The first-run hint's peer install, ahead of `npx humanish`: beside humanish in its project, else
|
|
272
|
+
* humanish and the peer together here, where `npx humanish` then finds them. None where `npm i`
|
|
273
|
+
* would prune a node_modules that no package.json declares.
|
|
274
|
+
*/
|
|
275
|
+
function initDesktopPeerCommand(install) {
|
|
276
|
+
if (install.kind === "project")
|
|
277
|
+
return desktopPeerAdvice(install).command;
|
|
278
|
+
if ((install.kind === "one-shot" || install.kind === "other") && install.unmanagedCwd)
|
|
279
|
+
return undefined;
|
|
280
|
+
return "npm i -D humanish @e2b/desktop";
|
|
281
|
+
}
|
|
273
282
|
/**
|
|
274
283
|
* What this machine can run, for the starter study and the next steps. Local CLI status is
|
|
275
284
|
* classified without returning its output or reading its credential file. Provider keys go
|
|
@@ -297,18 +306,17 @@ async function firstRunEnvironment(env, cwd) {
|
|
|
297
306
|
catch {
|
|
298
307
|
hasDesktopSdk = false;
|
|
299
308
|
}
|
|
300
|
-
const { fileURLToPath } = await import("node:url");
|
|
301
|
-
const here = fileURLToPath(import.meta.url);
|
|
302
309
|
return {
|
|
303
|
-
|
|
310
|
+
desktopPeerCommand: initDesktopPeerCommand(humanishInstall()),
|
|
304
311
|
hasDesktopSdk,
|
|
305
312
|
hasE2bKey: present("E2B_API_KEY"),
|
|
306
313
|
hasProviderKey: present("OPENAI_API_KEY"),
|
|
307
314
|
localAgents: agents
|
|
308
315
|
.filter((agent) => agent.authStatus === "authenticated")
|
|
309
316
|
.map((agent) => ({ id: agent.id, label: agent.label })),
|
|
317
|
+
localBrowserHost: await probeLocalBrowserHost(),
|
|
310
318
|
platform: process.platform,
|
|
311
|
-
|
|
319
|
+
humanish: humanishCommand(),
|
|
312
320
|
};
|
|
313
321
|
}
|
|
314
322
|
function getMode(options) {
|
package/dist/study/keys.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a dotted key path, such as `caps.maxUsd` or `participants[0].persona`, names a key a
|
|
3
|
+
* humanish.study.v3 file can set. A path below a leaf is not one: `scenario.ref`, where `scenario`
|
|
4
|
+
* is a string, and `review.analysis.question`, whose keys resolveAutomaticAnalysis checks.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isStudyKeyPath(path: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The dotted path of every key a humanish.study.v3 file can set, down to `depth` levels:
|
|
9
|
+
* `route`, `caps.maxUsd`, `participants.persona`. scripts/check-doc-study-fields.ts holds the
|
|
10
|
+
* study file reference to these paths.
|
|
11
|
+
*/
|
|
12
|
+
export declare function studyKeyPaths(depth: number): string[];
|
|
1
13
|
/** The first key in `raw` that V2_KEYS does not list, as an error message; undefined if none. */
|
|
2
14
|
export declare function findUnknownV2Key(raw: unknown): string | undefined;
|
|
3
15
|
/** The first key in a humanish.study.v3 document that the format does not have; undefined if none. */
|
package/dist/study/keys.js
CHANGED
|
@@ -230,6 +230,38 @@ const STUDY_KEYS = {
|
|
|
230
230
|
defaults: V2_KEYS.defaults,
|
|
231
231
|
comms: V2_KEYS.comms,
|
|
232
232
|
};
|
|
233
|
+
/**
|
|
234
|
+
* Whether a dotted key path, such as `caps.maxUsd` or `participants[0].persona`, names a key a
|
|
235
|
+
* humanish.study.v3 file can set. A path below a leaf is not one: `scenario.ref`, where `scenario`
|
|
236
|
+
* is a string, and `review.analysis.question`, whose keys resolveAutomaticAnalysis checks.
|
|
237
|
+
*/
|
|
238
|
+
export function isStudyKeyPath(path) {
|
|
239
|
+
let shape = STUDY_KEYS;
|
|
240
|
+
for (const segment of path.replaceAll(/\[\d*\]/g, "").split(".")) {
|
|
241
|
+
if (shape === true || !Object.hasOwn(shape, segment))
|
|
242
|
+
return false;
|
|
243
|
+
shape = shape[segment];
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The dotted path of every key a humanish.study.v3 file can set, down to `depth` levels:
|
|
249
|
+
* `route`, `caps.maxUsd`, `participants.persona`. scripts/check-doc-study-fields.ts holds the
|
|
250
|
+
* study file reference to these paths.
|
|
251
|
+
*/
|
|
252
|
+
export function studyKeyPaths(depth) {
|
|
253
|
+
const paths = [];
|
|
254
|
+
const visit = (shape, prefix, level) => {
|
|
255
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
256
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
257
|
+
paths.push(path);
|
|
258
|
+
if (child !== true && level < depth)
|
|
259
|
+
visit(child, path, level + 1);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
visit(STUDY_KEYS, "", 1);
|
|
263
|
+
return paths;
|
|
264
|
+
}
|
|
233
265
|
/** The first key in `raw` that V2_KEYS does not list, as an error message; undefined if none. */
|
|
234
266
|
export function findUnknownV2Key(raw) {
|
|
235
267
|
return walk(raw, V2_KEYS, "", "study");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LocalBrowserHost } from "../cli/first-run-path.js";
|
|
2
|
+
export interface LocalBrowserHostProbe {
|
|
3
|
+
platform: NodeJS.Platform;
|
|
4
|
+
arch: string;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
/** Resolves when `file` can be opened with `mode`; rejects otherwise. */
|
|
7
|
+
access: (file: string, mode: number) => Promise<void>;
|
|
8
|
+
/** The Mac's chip name, as `sysctl -n machdep.cpu.brand_string` prints it. */
|
|
9
|
+
macChip: () => Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function probeLocalBrowserHost(probe?: LocalBrowserHostProbe): Promise<LocalBrowserHost>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// init's quick read of whether this host can run a local browser study, so its next steps never
|
|
2
|
+
// send someone to a route the host cannot run. It reads devices, `PATH` and the Mac chip name only;
|
|
3
|
+
// it starts no container and no VM. `doctor --study local-browser` runs the full check, including
|
|
4
|
+
// whether Docker is rootful and local.
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { access } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
const defaultProbe = () => ({
|
|
12
|
+
platform: process.platform,
|
|
13
|
+
arch: process.arch,
|
|
14
|
+
env: process.env,
|
|
15
|
+
access,
|
|
16
|
+
macChip: async () => (await exec("sysctl", ["-n", "machdep.cpu.brand_string"], { timeout: 5_000 })).stdout,
|
|
17
|
+
});
|
|
18
|
+
export async function probeLocalBrowserHost(probe = defaultProbe()) {
|
|
19
|
+
const can = async (file, mode) => probe.access(file, mode).then(() => true, () => false);
|
|
20
|
+
if (probe.platform === "linux" && probe.arch === "x64") {
|
|
21
|
+
// The same device checks doctor makes. The rootful Docker daemon opens these devices, so this
|
|
22
|
+
// user needs no access to them.
|
|
23
|
+
if (!(await can("/dev/kvm", constants.F_OK)))
|
|
24
|
+
return { ok: false, reason: "/dev/kvm is missing" };
|
|
25
|
+
if (!(await can("/dev/net/tun", constants.F_OK)))
|
|
26
|
+
return { ok: false, reason: "/dev/net/tun is missing" };
|
|
27
|
+
const dirs = (probe.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
28
|
+
for (const dir of dirs)
|
|
29
|
+
if (await can(path.join(dir, "docker"), constants.X_OK))
|
|
30
|
+
return { ok: true };
|
|
31
|
+
return { ok: false, reason: "the docker command is not on the search path" };
|
|
32
|
+
}
|
|
33
|
+
if (probe.platform === "darwin" && probe.arch === "arm64") {
|
|
34
|
+
// The local route needs nested virtualization, which Apple added with the M3.
|
|
35
|
+
const chip = await probe.macChip().catch(() => "");
|
|
36
|
+
const generation = /^Apple M(\d+)/.exec(chip.trim());
|
|
37
|
+
if (generation !== null && Number(generation[1]) >= 3)
|
|
38
|
+
return { ok: true };
|
|
39
|
+
return { ok: false, reason: "they need an M3 or newer Mac" };
|
|
40
|
+
}
|
|
41
|
+
return { ok: false, reason: "they need Linux x64 or an M3-or-newer Mac" };
|
|
42
|
+
}
|
|
@@ -2,6 +2,7 @@ import { DEVICE_PRESET_NAMES, isDevicePresetName } from "../device-presets.js";
|
|
|
2
2
|
import { isReasoningEffort, reasoningEffortNames } from "../../actors/reasoning-effort.js";
|
|
3
3
|
import { isMaxOutputTokens } from "../../actors/output-token-limit.js";
|
|
4
4
|
import { isHttpUrl } from "./subject.js";
|
|
5
|
+
import { entryCredentialReason, urlCredentialReason } from "./url-credentials.js";
|
|
5
6
|
import { registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, } from "../routing.js";
|
|
6
7
|
import { invalid, posInt, str } from "./values.js";
|
|
7
8
|
import { isRecord } from "../../run/type-guards.js";
|
|
@@ -265,13 +266,20 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
265
266
|
if (!isHttpUrl(target)) {
|
|
266
267
|
return invalid(`actors[${actorIndex}].lanes[${entryIndex}].target must be an absolute http(s) URL.`);
|
|
267
268
|
}
|
|
269
|
+
const credential = urlCredentialReason(`actors[${actorIndex}].lanes[${entryIndex}].target`, target);
|
|
270
|
+
if (credential)
|
|
271
|
+
return invalid(credential);
|
|
268
272
|
parsedEntry.target = target;
|
|
269
273
|
}
|
|
270
274
|
// `entry` is shape-captured here; the same-origin-with-serve.url check needs serve context, so
|
|
271
275
|
// it runs in sharedWorldValidationReason (where the route + serve.url are known).
|
|
272
276
|
const entryPath = str(entry.entry);
|
|
273
|
-
if (entryPath !== undefined)
|
|
277
|
+
if (entryPath !== undefined) {
|
|
278
|
+
const credential = entryCredentialReason(`actors[${actorIndex}].lanes[${entryIndex}].entry`, entryPath);
|
|
279
|
+
if (credential)
|
|
280
|
+
return invalid(credential);
|
|
274
281
|
parsedEntry.entry = entryPath;
|
|
282
|
+
}
|
|
275
283
|
// `host` marks the designated host on the external-public shared-world route; the
|
|
276
284
|
// exactly-one-host check runs in externalPublicSharedWorldValidationReason (route context).
|
|
277
285
|
if (entry.host !== undefined) {
|
|
@@ -2,6 +2,7 @@ import { normalizeExtraExcludeEntry } from "../../subject/local-tree-archive.js"
|
|
|
2
2
|
import { ENV_NAME_PATTERN, invalid, posInt, str, strList } from "./values.js";
|
|
3
3
|
import { parseEnvValues, parseState, subjectStateInvalidReason } from "./subject-state.js";
|
|
4
4
|
import { isRecord } from "../../run/type-guards.js";
|
|
5
|
+
import { urlCredentialReason } from "./url-credentials.js";
|
|
5
6
|
export function parseSubject(raw) {
|
|
6
7
|
if (!isRecord(raw)) {
|
|
7
8
|
return invalid("`subject` is required and must be an object.");
|
|
@@ -202,6 +203,9 @@ function parseAppSubject(raw, source, subject) {
|
|
|
202
203
|
else if (!isHttpUrl(appUrl)) {
|
|
203
204
|
return invalid("`subject.appUrl` must be an http(s) URL.");
|
|
204
205
|
}
|
|
206
|
+
const credential = urlCredentialReason("subject.appUrl", appUrl);
|
|
207
|
+
if (credential)
|
|
208
|
+
return invalid(credential);
|
|
205
209
|
subject.appUrl = appUrl;
|
|
206
210
|
// publicTarget (external-public shared-world ownership attestation) is app-url-only. Shape it
|
|
207
211
|
// here; its required-on-that-route semantics live in externalPublicSharedWorldValidationReason.
|
|
@@ -278,6 +282,11 @@ function parseProduct(raw) {
|
|
|
278
282
|
if (badSurface) {
|
|
279
283
|
return invalid(`subject.product.publicSurfaces entries must be http(s) URLs (got "${badSurface}").`);
|
|
280
284
|
}
|
|
285
|
+
const surfaceCredential = publicSurfaces
|
|
286
|
+
.map((surface) => urlCredentialReason("subject.product.publicSurfaces", surface))
|
|
287
|
+
.find((reason) => reason !== undefined);
|
|
288
|
+
if (surfaceCredential)
|
|
289
|
+
return invalid(surfaceCredential);
|
|
281
290
|
return {
|
|
282
291
|
ok: true,
|
|
283
292
|
value: {
|
|
@@ -332,6 +341,9 @@ function parseServe(raw) {
|
|
|
332
341
|
if (!url || !isLoopbackUrl(url)) {
|
|
333
342
|
return invalid("`subject.serve.url` must be a loopback http(s) URL (127.0.0.1 or localhost), because the app runs inside the sandbox.");
|
|
334
343
|
}
|
|
344
|
+
const credential = urlCredentialReason("subject.serve.url", url);
|
|
345
|
+
if (credential)
|
|
346
|
+
return invalid(credential);
|
|
335
347
|
const serve = { start, url };
|
|
336
348
|
const install = str(raw.install);
|
|
337
349
|
if (install)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the URL declared at `field` cannot be used: it has userinfo, or its path, query or fragment
|
|
3
|
+
* holds a value verify flags as a secret, as written or encoded. Undefined for a URL with neither,
|
|
4
|
+
* and for text that is not a URL, which the field's own shape check refuses. `base` resolves a
|
|
5
|
+
* relative entry path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function urlCredentialReason(field: string, value: string, base?: string): string | undefined;
|
|
8
|
+
/** Why the participant entry at `field` cannot be used, resolved as a path on the subject. */
|
|
9
|
+
export declare function entryCredentialReason(field: string, entry: string): string | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// A URL a study declares is recorded in the run, shown in the participant's address bar and so in
|
|
2
|
+
// every screenshot and model request. It may not carry a credential. Ordinary query parameters,
|
|
3
|
+
// such as a filter or a page number, stay as they are.
|
|
4
|
+
import { scanEncodedText } from "../../evidence/encoded-text.js";
|
|
5
|
+
// A relative participant entry is resolved against this before it is read.
|
|
6
|
+
const ENTRY_BASE = "http://127.0.0.1/";
|
|
7
|
+
/**
|
|
8
|
+
* Why the URL declared at `field` cannot be used: it has userinfo, or its path, query or fragment
|
|
9
|
+
* holds a value verify flags as a secret, as written or encoded. Undefined for a URL with neither,
|
|
10
|
+
* and for text that is not a URL, which the field's own shape check refuses. `base` resolves a
|
|
11
|
+
* relative entry path.
|
|
12
|
+
*/
|
|
13
|
+
export function urlCredentialReason(field, value, base) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(value, base);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
// The URL parser drops tabs and line breaks, which can split a key the scan would otherwise read.
|
|
22
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
23
|
+
return `\`${field}\` has a tab, line break or other control character in it, which the URL parser drops. Write the URL without it.`;
|
|
24
|
+
if (url.username !== "" || url.password !== "")
|
|
25
|
+
return `\`${field}\` has a user name or password in it. humanish records the URL in the run, and the participant's browser shows it in every screenshot, so the credential would reach the bundle and the model. Remove it from the URL and give the participant a test account to sign in with.`;
|
|
26
|
+
if (urlPartsHoldSecret(value, url))
|
|
27
|
+
return `\`${field}\` carries a credential in its path, query or fragment, such as a token, a signature or a preview bypass parameter. humanish records the URL in the run, and the participant's browser shows it in every screenshot. Remove it from the URL. For a protected preview, make the deployment reachable without a token for the study, or build the app in the desktop with a clone subject.`;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Why the participant entry at `field` cannot be used, resolved as a path on the subject. */
|
|
31
|
+
export function entryCredentialReason(field, entry) {
|
|
32
|
+
return urlCredentialReason(field, entry, ENTRY_BASE);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether the URL's path, query or fragment holds a secret: read whole, as the parser normalized
|
|
36
|
+
* it and as written (`..` segments can drop a segment from the normalized path), and one segment,
|
|
37
|
+
* value or fragment piece at a time, so the text around an encoded key cannot shift its decoding.
|
|
38
|
+
*/
|
|
39
|
+
function urlPartsHoldSecret(value, url) {
|
|
40
|
+
const written = value.replace(/^[a-z][a-z0-9+.-]{0,31}:\/\/[^/?#]*/i, "");
|
|
41
|
+
const normalized = `${url.pathname}${url.search}${url.hash}`;
|
|
42
|
+
const pieces = [
|
|
43
|
+
written,
|
|
44
|
+
normalized,
|
|
45
|
+
...written.split(/[/?#&=;]/),
|
|
46
|
+
...normalized.split(/[/?#&=;]/),
|
|
47
|
+
];
|
|
48
|
+
return pieces.some((piece) => piece.length > 0 &&
|
|
49
|
+
scanEncodedText(piece, { secretsOnly: true, allowOpaqueBase64: true }).sensitive);
|
|
50
|
+
}
|
package/dist/study/persona.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface PersonaDirectives {
|
|
|
26
26
|
/** Directives included in the prompt, not proof of behavioral adherence. */
|
|
27
27
|
traitsApplied: string[];
|
|
28
28
|
}
|
|
29
|
+
/** The fields a persona file can set; any other warns and is not sent. The study file reference lists each. */
|
|
30
|
+
export declare const PERSONA_FIELDS: Set<string>;
|
|
31
|
+
/** The traits under `traits:` that become directives; any other warns and is not sent. */
|
|
32
|
+
export declare const TRAIT_FIELDS: Set<string>;
|
|
29
33
|
/** Legacy short fields keep their bounds, but every discarded value is diagnosed. */
|
|
30
34
|
export declare function parseResolvedPersona(raw: unknown, fallback: {
|
|
31
35
|
id: string;
|
package/dist/study/persona.js
CHANGED
|
@@ -9,7 +9,8 @@ export class PersonaConfigError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
const LEVELS = ["low", "medium", "high"];
|
|
12
|
-
|
|
12
|
+
/** The fields a persona file can set; any other warns and is not sent. The study file reference lists each. */
|
|
13
|
+
export const PERSONA_FIELDS = new Set([
|
|
13
14
|
"schema",
|
|
14
15
|
"id",
|
|
15
16
|
"name",
|
|
@@ -18,7 +19,8 @@ const PERSONA_FIELDS = new Set([
|
|
|
18
19
|
"traits",
|
|
19
20
|
"constraints",
|
|
20
21
|
]);
|
|
21
|
-
|
|
22
|
+
/** The traits under `traits:` that become directives; any other warns and is not sent. */
|
|
23
|
+
export const TRAIT_FIELDS = new Set(["patience", "technical_confidence", "accessibility_needs"]);
|
|
22
24
|
function cleanText(value) {
|
|
23
25
|
return value
|
|
24
26
|
.replace(/\r\n?/g, "\n")
|
|
@@ -14,6 +14,7 @@ import { redactText } from "../evidence/redaction.js";
|
|
|
14
14
|
import { digest, fail, finalize } from "./preflight-result.js";
|
|
15
15
|
import { rosterOf } from "./parse/actors.js";
|
|
16
16
|
import { plural } from "../run/text.js";
|
|
17
|
+
import { cli } from "../cli/invocation.js";
|
|
17
18
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
18
19
|
// Room on the probe's lease for desktop boot and teardown around the work it does.
|
|
19
20
|
const PREFLIGHT_LEASE_BUFFER_MS = 5 * 60_000;
|
|
@@ -240,7 +241,7 @@ async function settlePreflightJournal(ctx, journal, gone) {
|
|
|
240
241
|
return;
|
|
241
242
|
if (!gone) {
|
|
242
243
|
await abandonPreflightJournal(journal).catch(() => undefined);
|
|
243
|
-
ctx.warnings.push(`The preflight sandbox's receipt stays in .humanish/preflight/${journal.id}; run
|
|
244
|
+
ctx.warnings.push(`The preflight sandbox's receipt stays in .humanish/preflight/${journal.id}; run \`${cli("reclaim --preflight")}\` to kill it by id.`);
|
|
244
245
|
return;
|
|
245
246
|
}
|
|
246
247
|
await discardPreflightJournal(journal).catch((error) => {
|
package/dist/study/types.d.ts
CHANGED
|
@@ -448,10 +448,11 @@ export interface StudyExecutionTerminal {
|
|
|
448
448
|
stdin?: StudyTerminalStdin;
|
|
449
449
|
}
|
|
450
450
|
/**
|
|
451
|
-
* The terminal agent's runtime-auth channel. "openai-
|
|
452
|
-
* key
|
|
453
|
-
*
|
|
454
|
-
*
|
|
451
|
+
* The terminal agent's runtime-auth channel. "openai-egress" (the default) keeps the raw runtime
|
|
452
|
+
* key in an E2B outbound header transform for the default OpenAI endpoint and passes an inert
|
|
453
|
+
* placeholder to Codex. It still gives every sandbox process a spendable OpenAI proxy capability;
|
|
454
|
+
* it is not a spend cap or an egress restriction. "openai-env" passes the raw key command-scoped,
|
|
455
|
+
* where the agent and its child processes can read it.
|
|
455
456
|
*/
|
|
456
457
|
export type StudyRuntimeAuth = "openai-env" | "openai-egress";
|
|
457
458
|
export type StudyDesktopBrowser = "default" | "chrome" | "chromium" | "firefox";
|
|
@@ -559,8 +560,9 @@ export interface StudyExecution {
|
|
|
559
560
|
caps?: StudyScenarioCaps;
|
|
560
561
|
/** `terminal-product` route: the terminal transport + stdin posture. Consumed on that route. */
|
|
561
562
|
terminal?: StudyExecutionTerminal;
|
|
562
|
-
/** `terminal-product` route: runtime key placement, defaulting to openai-
|
|
563
|
-
*
|
|
563
|
+
/** `terminal-product` route: runtime key placement, defaulting to openai-egress, an external
|
|
564
|
+
* header transform. openai-env passes the key to the agent command. Dry-runs record declarations
|
|
565
|
+
* only. Inert on other routes. */
|
|
564
566
|
runtimeAuth?: StudyRuntimeAuth;
|
|
565
567
|
/** Terminal Codex package pin. Omit to resolve latest once, observe it, then execute that version. */
|
|
566
568
|
runtime?: {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StudyConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The first URL in `config` that carries a credential, with why: the subject's app, serve and
|
|
4
|
+
* public surface URLs, and each participant's target and entry.
|
|
5
|
+
*/
|
|
6
|
+
export declare function studyUrlCredentialReason(config: StudyConfig): string | undefined;
|