humanish 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/actor-registry.d.ts +6 -2
- package/dist/actor-registry.js +12 -0
- package/dist/actor-registry.js.map +1 -1
- package/dist/agent-session.d.ts +11 -0
- package/dist/agent-session.js +43 -0
- package/dist/agent-session.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +13 -0
- package/dist/cua-actor-lab.js +220 -25
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-terminal-lab.js +79 -2
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/lab-config.d.ts +42 -1
- package/dist/lab-config.js +63 -8
- package/dist/lab-config.js.map +1 -1
- package/dist/lab-engine.js +6 -0
- package/dist/lab-engine.js.map +1 -1
- package/dist/local-agent-appserver.d.ts +44 -0
- package/dist/local-agent-appserver.js +267 -0
- package/dist/local-agent-appserver.js.map +1 -0
- package/dist/local-agent-cli.d.ts +104 -0
- package/dist/local-agent-cli.js +337 -0
- package/dist/local-agent-cli.js.map +1 -0
- package/dist/program.d.ts +2 -0
- package/dist/program.js +36 -3
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +9 -0
- package/dist/run.js +16 -7
- package/dist/run.js.map +1 -1
- package/dist/terminal-encoding.d.ts +17 -0
- package/dist/terminal-encoding.js +80 -0
- package/dist/terminal-encoding.js.map +1 -0
- package/dist/tui-app.js +80 -80
- package/docs/contracts/schemas.md +9 -2
- package/docs/goals/current.md +1 -1
- package/docs/ramp/README.md +29 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -67,6 +67,44 @@ npx skills add danielgwilson/humanish --skill humanish
|
|
|
67
67
|
The skill lives at [`skills/humanish/SKILL.md`](skills/humanish/SKILL.md)
|
|
68
68
|
for skills.sh discovery.
|
|
69
69
|
|
|
70
|
+
## A First Live Run Without a Provider API Key
|
|
71
|
+
|
|
72
|
+
A live study normally needs a provider API key. If you already have a coding
|
|
73
|
+
agent signed in — Codex on a ChatGPT plan, Claude Code on a Max plan — humanish
|
|
74
|
+
can use it as the participant's brain instead, and then the only credential it
|
|
75
|
+
needs is `E2B_API_KEY`.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
humanish doctor # says which local agents are installed and signed in
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
actors:
|
|
83
|
+
- type: local-agent # instead of openai-computer-use
|
|
84
|
+
persona: synthetic-new-user
|
|
85
|
+
mission: >-
|
|
86
|
+
...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
humanish never reads those credentials. It checks that the credential file
|
|
90
|
+
**exists**, spawns the CLI tool-restricted (`--sandbox read-only` for Codex,
|
|
91
|
+
`--allowedTools Read` for Claude Code) in a scratch directory, and hands it one
|
|
92
|
+
screenshot per turn. The agent only **decides**; humanish performs the action
|
|
93
|
+
inside the E2B sandbox, so nothing the persona chooses ever runs on your machine.
|
|
94
|
+
|
|
95
|
+
Three things to know before you rely on it:
|
|
96
|
+
|
|
97
|
+
- **It is not free.** Subscription usage consumes your own plan. Runs driven this
|
|
98
|
+
way record `estimatedCostUsd: null` with `reason: "no_token_usage"` rather than
|
|
99
|
+
`$0`, because `$0` would be untrue. Rate limits on those plans are built for
|
|
100
|
+
interactive coding; humanish fails closed with the CLI's own message rather
|
|
101
|
+
than retrying into them.
|
|
102
|
+
- **It is slower.** Roughly 9 seconds per turn against about 3 for a direct API
|
|
103
|
+
call, so give the lane a longer `execution.timeoutMs` than you would otherwise.
|
|
104
|
+
- **The evidence says which brain ran it.** The trace records
|
|
105
|
+
`ids.model: "codex (local, operator-authenticated)"`, so a local-agent run is
|
|
106
|
+
never silently compared against an API one.
|
|
107
|
+
|
|
70
108
|
## Public-Safety Boundary
|
|
71
109
|
|
|
72
110
|
Humanish is designed for public repositories and public issue queues. The
|
package/dist/actor-registry.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type ClaudeAgentSessionOptions, type ClaudeAgentSessionResult, type Cla
|
|
|
6
6
|
import { type CuaActorSessionOptions } from "./computer-use-actor.js";
|
|
7
7
|
import type { CuaLoopResult } from "./computer-use.js";
|
|
8
8
|
import { type ScriptedBrowserSessionOptions, type ScriptedBrowserSessionResult } from "./scripted-browser-actor.js";
|
|
9
|
-
export type ActorId = "codex-app-server" | "pi-agent-core" | "claude-agent-sdk" | "openai-computer-use" | "scripted-browser" | "codex-exec";
|
|
9
|
+
export type ActorId = "codex-app-server" | "pi-agent-core" | "claude-agent-sdk" | "openai-computer-use" | "local-agent" | "scripted-browser" | "codex-exec";
|
|
10
10
|
interface ActorDescriptorBase {
|
|
11
11
|
id: ActorId;
|
|
12
12
|
label: string;
|
|
@@ -30,6 +30,10 @@ export interface CuaActorDescriptor extends ActorDescriptorBase {
|
|
|
30
30
|
id: "openai-computer-use";
|
|
31
31
|
runSession(options: CuaActorSessionOptions): Promise<CuaLoopResult>;
|
|
32
32
|
}
|
|
33
|
+
export interface LocalAgentActorDescriptor extends ActorDescriptorBase {
|
|
34
|
+
id: "local-agent";
|
|
35
|
+
runSession(options: CuaActorSessionOptions): Promise<CuaLoopResult>;
|
|
36
|
+
}
|
|
33
37
|
export interface ScriptedBrowserActorDescriptor extends ActorDescriptorBase {
|
|
34
38
|
id: "scripted-browser";
|
|
35
39
|
runSession(options: ScriptedBrowserSessionOptions): Promise<ScriptedBrowserSessionResult>;
|
|
@@ -38,7 +42,7 @@ export interface TerminalActorDescriptor extends ActorDescriptorBase {
|
|
|
38
42
|
id: "codex-exec";
|
|
39
43
|
runSession(options: TerminalAgentSessionOptions): Promise<TerminalAgentSessionResult>;
|
|
40
44
|
}
|
|
41
|
-
export type ActorDescriptor = CodexActorDescriptor | PiActorDescriptor | ClaudeActorDescriptor | CuaActorDescriptor | ScriptedBrowserActorDescriptor | TerminalActorDescriptor;
|
|
45
|
+
export type ActorDescriptor = CodexActorDescriptor | PiActorDescriptor | ClaudeActorDescriptor | CuaActorDescriptor | LocalAgentActorDescriptor | ScriptedBrowserActorDescriptor | TerminalActorDescriptor;
|
|
42
46
|
/**
|
|
43
47
|
* REGISTRY CONTRACT: an actor whose capabilities include the "computer-use" lane is a
|
|
44
48
|
* CuaActorDescriptor — its runSession takes CuaActorSessionOptions and returns a CuaLoopResult.
|
package/dist/actor-registry.js
CHANGED
|
@@ -4,6 +4,7 @@ import { runTerminalAgentSession } from "./terminal-agent-actor.js";
|
|
|
4
4
|
import { piSessionToActorTrace } from "./pi-agent-core.js";
|
|
5
5
|
import { claudeSessionToActorTrace, runClaudeAgentSession } from "./claude-agent-sdk.js";
|
|
6
6
|
import { runCuaActorSession } from "./computer-use-actor.js";
|
|
7
|
+
import { LOCAL_AGENT_CAPABILITIES } from "./local-agent-cli.js";
|
|
7
8
|
import { OPENAI_RESPONSES_CU_CAPABILITIES } from "./openai-responses-cu.js";
|
|
8
9
|
import { runScriptedBrowserSession } from "./scripted-browser-actor.js";
|
|
9
10
|
/**
|
|
@@ -58,6 +59,17 @@ export const actorRegistry = {
|
|
|
58
59
|
},
|
|
59
60
|
// The ActorId names the actor slot (keeps the lane open for a future stagehand-cua provider);
|
|
60
61
|
// the trace's `provider` string stays "openai-responses-cu" (the concrete model adapter).
|
|
62
|
+
// The operator's own signed-in coding agent as the computer-use brain (Codex on a ChatGPT plan,
|
|
63
|
+
// Claude Code on a Max plan). Same lane, same loop, same evidence — the only difference is where
|
|
64
|
+
// the next action comes from, which is exactly why it is a provider swap and not a new lane.
|
|
65
|
+
// It exists so someone new can watch a persona drive a real desktop without first going to find
|
|
66
|
+
// an API key; the machine they are on very often already has one of these signed in.
|
|
67
|
+
"local-agent": {
|
|
68
|
+
id: "local-agent",
|
|
69
|
+
label: "Local coding agent (operator-authenticated)",
|
|
70
|
+
capabilities: LOCAL_AGENT_CAPABILITIES,
|
|
71
|
+
runSession: runCuaActorSession
|
|
72
|
+
},
|
|
61
73
|
"openai-computer-use": {
|
|
62
74
|
id: "openai-computer-use",
|
|
63
75
|
label: "OpenAI Computer Use",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor-registry.js","sourceRoot":"","sources":["../src/actor-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EAGzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC7B,0BAA0B,EAC1B,6BAA6B,EAC7B,2BAA2B,EAC3B,uBAAuB,EAIxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uBAAuB,EAGxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,qBAAqB,EAAwB,MAAM,oBAAoB,CAAC;AACjF,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EAItB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAA+B,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"actor-registry.js","sourceRoot":"","sources":["../src/actor-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface AgentSession {
|
|
2
|
+
/** Human-readable runner name, for the refusal message. */
|
|
3
|
+
runner: string;
|
|
4
|
+
/** The environment variable that identified it — named so the reader can check us. */
|
|
5
|
+
marker: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The agent runner driving this process, when one identifies itself. `undefined` means nothing
|
|
9
|
+
* claimed to be an agent — which is NOT proof a person is there, only the absence of a claim.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectAgentSession(env?: NodeJS.ProcessEnv): AgentSession | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Is the terminal on the other end of this process a PERSON's, or an agent's?
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS: `humanish tui` refused a non-TTY, on the reasoning that an agent driving an
|
|
4
|
+
// interactive surface has asked for something that cannot exist. A study of that refusal
|
|
5
|
+
// (labs/handed-a-human-surface.yaml) found it never fires: `codex exec` allocates a PTY for the
|
|
6
|
+
// commands it runs, so both streams ARE terminals. The TUI launched, the agent navigated the labs
|
|
7
|
+
// list, opened one, and — its own words — "accidentally triggered a zero-cost dry run while
|
|
8
|
+
// navigating". A stray Enter on a live row is the same two keystrokes as a deliberate one.
|
|
9
|
+
//
|
|
10
|
+
// So a TTY is a real answer to the wrong question. It says a terminal exists; it does not say
|
|
11
|
+
// anyone is reading it. Agent runners announce themselves in the environment, which is the only
|
|
12
|
+
// signal available before the first keystroke, and is what `is-in-ci` has always done for CI.
|
|
13
|
+
//
|
|
14
|
+
// EVERY MARKER BELOW WAS OBSERVED, not guessed. Two runtimes are covered because two are what we
|
|
15
|
+
// could verify: Claude Code (read off a live session) and Codex (read off the study sandbox, by a
|
|
16
|
+
// names-only `env | cut -d= -f1` probe that never touched a value). Others certainly exist — add
|
|
17
|
+
// them the same way, from a real session, rather than from a plausible-looking guess. A marker
|
|
18
|
+
// that is wrong refuses a person for no reason.
|
|
19
|
+
const MARKERS = [
|
|
20
|
+
{ marker: "CLAUDECODE", runner: "Claude Code" },
|
|
21
|
+
{ marker: "CLAUDE_CODE_SESSION_ID", runner: "Claude Code" },
|
|
22
|
+
{ marker: "CODEX_SESSION_ID", runner: "Codex" },
|
|
23
|
+
{ marker: "CODEX_THREAD_ID", runner: "Codex" },
|
|
24
|
+
// Generic, and set alongside the Claude Code markers on the machine this was written on. Kept
|
|
25
|
+
// last so a named runner wins the attribution.
|
|
26
|
+
{ marker: "AI_AGENT", runner: "an AI agent runner" }
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* The agent runner driving this process, when one identifies itself. `undefined` means nothing
|
|
30
|
+
* claimed to be an agent — which is NOT proof a person is there, only the absence of a claim.
|
|
31
|
+
*/
|
|
32
|
+
export function detectAgentSession(env = process.env) {
|
|
33
|
+
for (const { marker, runner } of MARKERS) {
|
|
34
|
+
const value = env[marker];
|
|
35
|
+
// Presence is the signal, but an explicitly empty or "0" value is treated as absence: a
|
|
36
|
+
// wrapper that unsets a marker by blanking it means it.
|
|
37
|
+
if (value !== undefined && value.trim().length > 0 && value.trim() !== "0") {
|
|
38
|
+
return { runner, marker };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=agent-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-session.js","sourceRoot":"","sources":["../src/agent-session.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,+FAA+F;AAC/F,yFAAyF;AACzF,gGAAgG;AAChG,kGAAkG;AAClG,4FAA4F;AAC5F,2FAA2F;AAC3F,EAAE;AACF,8FAA8F;AAC9F,gGAAgG;AAChG,8FAA8F;AAC9F,EAAE;AACF,iGAAiG;AACjG,kGAAkG;AAClG,iGAAiG;AACjG,+FAA+F;AAC/F,gDAAgD;AAShD,MAAM,OAAO,GAAsD;IACjE,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE;IAC/C,EAAE,MAAM,EAAE,wBAAwB,EAAE,MAAM,EAAE,aAAa,EAAE;IAC3D,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,OAAO,EAAE;IAC/C,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,OAAO,EAAE;IAC9C,8FAA8F;IAC9F,+CAA+C;IAC/C,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,oBAAoB,EAAE;CACrD,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACrE,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1B,wFAAwF;QACxF,wDAAwD;QACxD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YAC3E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/cua-actor-lab.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type CuaActorDescriptor } from "./actor-registry.js";
|
|
|
5
5
|
import type { CuaActorSessionOptions } from "./computer-use-actor.js";
|
|
6
6
|
import type { CuaExecutor, CuaLoopResult, CuaProvider } from "./computer-use.js";
|
|
7
7
|
import type { ReasoningEffort } from "./reasoning-effort.js";
|
|
8
|
+
import { type LocalAgentId } from "./local-agent-cli.js";
|
|
8
9
|
import { type E2BDesktopModule, type E2BDesktopSandbox } from "./e2b-desktop-launch.js";
|
|
9
10
|
import { type DetachedTimers } from "./e2b-detached.js";
|
|
10
11
|
import { type DevicePreset } from "./device-presets.js";
|
|
@@ -416,6 +417,14 @@ export declare function composeLaneInstructions(args: {
|
|
|
416
417
|
* no committed file, unparseable YAML) keeps the honest fallback: the bare line and an EMPTY
|
|
417
418
|
* traitsApplied, never fabricated traits. Resolved by the caller so this stays pure. */
|
|
418
419
|
resolvedPersona?: ResolvedPersona;
|
|
420
|
+
/**
|
|
421
|
+
* desktop-cli (#495): the surface under study is a terminal window, not a page. Said plainly
|
|
422
|
+
* because a participant whose every prior world was a browser will look for one — and because a
|
|
423
|
+
* capability nobody declares is one the recording cannot later be read against. It states that a
|
|
424
|
+
* terminal is open and NOT what to type in it: naming commands would answer the question the
|
|
425
|
+
* study is asking.
|
|
426
|
+
*/
|
|
427
|
+
surface?: "desktop-cli";
|
|
419
428
|
}): {
|
|
420
429
|
instructions: string;
|
|
421
430
|
persona: ActorPersonaRef;
|
|
@@ -504,7 +513,11 @@ export interface CuaLaneDeps {
|
|
|
504
513
|
config: LabConfig;
|
|
505
514
|
descriptor: CuaActorDescriptor;
|
|
506
515
|
appUrl: string;
|
|
516
|
+
/** When set, the computer-use brain is this locally-signed-in CLI instead of a keyed API. */
|
|
517
|
+
localAgent?: LocalAgentId;
|
|
507
518
|
cloneRoute: boolean;
|
|
519
|
+
/** desktop-cli (#495): a CLI studied at a desktop. Nothing is cloned and no browser is opened. */
|
|
520
|
+
desktopCliRoute?: boolean;
|
|
508
521
|
/** Optional so out-of-scope callers building CuaLaneDeps directly (other engines reusing
|
|
509
522
|
* runCuaLane) do not need to know about the local-tree route; undefined behaves as false. */
|
|
510
523
|
localTreeRoute?: boolean;
|
package/dist/cua-actor-lab.js
CHANGED
|
@@ -31,6 +31,8 @@ import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-
|
|
|
31
31
|
import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
|
|
32
32
|
import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS, chromiumEvidenceProfilePreferencesJson } from "./browser-evidence-hygiene.js";
|
|
33
33
|
import { DEFAULT_OPENAI_CU_MODEL } from "./openai-responses-cu.js";
|
|
34
|
+
import { createLocalAgentProvider, detectLocalAgents } from "./local-agent-cli.js";
|
|
35
|
+
import { startAppServerSession } from "./local-agent-appserver.js";
|
|
34
36
|
import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
35
37
|
import { probeUrl, readDetachedLog, runDetachedStep, startDetachedProcess } from "./e2b-detached.js";
|
|
36
38
|
import { DEFAULT_SANDBOX_CATCH_PORT, collectCommsThread, collectExternalCommsThread, deployCommsCatch, externalCatchHealthy, externalInboxUrl, refreshInboxSurface, writeInboxSurface } from "./comms-sandbox-catch.js";
|
|
@@ -136,9 +138,13 @@ export function composeLaneInstructions(args) {
|
|
|
136
138
|
? renderPersonaPromptSection(args.resolvedPersona)
|
|
137
139
|
: args.persona ? `Persona: ${args.persona}.` : undefined;
|
|
138
140
|
const traitsApplied = args.resolvedPersona ? personaToDirectives(args.resolvedPersona).traitsApplied : [];
|
|
141
|
+
const surfaceLine = args.surface === "desktop-cli"
|
|
142
|
+
? "A terminal window is already open on this desktop, and there is a terminal in the dock at the bottom of the screen if you want another. Everything you need is on this machine; there is no browser task here."
|
|
143
|
+
: undefined;
|
|
139
144
|
const parts = [
|
|
140
145
|
personaLine,
|
|
141
146
|
deviceLine,
|
|
147
|
+
surfaceLine,
|
|
142
148
|
args.mission,
|
|
143
149
|
taskLines,
|
|
144
150
|
args.instruction ? `Lane focus: ${args.instruction}` : undefined
|
|
@@ -291,7 +297,8 @@ function laneSpecsAndPlan(config, opts = {}) {
|
|
|
291
297
|
...(personaId === undefined ? {} : { persona: personaId }),
|
|
292
298
|
...(resolvedPersona === undefined ? {} : { resolvedPersona }),
|
|
293
299
|
...(((roster ? lane?.instruction : actor?.laneFocus?.instruction)) === undefined ? {} : { instruction: (roster ? lane?.instruction : actor?.laneFocus?.instruction) }),
|
|
294
|
-
device: { name: device.name, preset: device.preset }
|
|
300
|
+
device: { name: device.name, preset: device.preset },
|
|
301
|
+
...(config.subject.source === "desktop-cli" ? { surface: "desktop-cli" } : {})
|
|
295
302
|
});
|
|
296
303
|
lanes.push({
|
|
297
304
|
laneId,
|
|
@@ -1069,6 +1076,91 @@ export async function captureDesktopBrowserGeometry(args) {
|
|
|
1069
1076
|
function shellSingleQuote(value) {
|
|
1070
1077
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
1071
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Put a CLI on the desktop before the participant arrives (#495).
|
|
1081
|
+
*
|
|
1082
|
+
* The install runs UNKEYED and before the session starts, for the same reason the clone route
|
|
1083
|
+
* provisions its subject first: what is being studied begins when the participant looks at the
|
|
1084
|
+
* screen, and making them fight an install first would be a study of the install.
|
|
1085
|
+
*/
|
|
1086
|
+
async function provisionDesktopCli(desktop, args) {
|
|
1087
|
+
const install = args.install;
|
|
1088
|
+
if (install === undefined)
|
|
1089
|
+
return;
|
|
1090
|
+
const now = () => Date.now();
|
|
1091
|
+
if (needsNodeRuntime([install])) {
|
|
1092
|
+
const startedAt = now();
|
|
1093
|
+
emitPhaseStarted(args.onPhase, now, "runtime", "providing the Node runtime the install needs");
|
|
1094
|
+
const bootstrap = await runDetachedStep(desktop, {
|
|
1095
|
+
name: "desktop-cli-runtime-node",
|
|
1096
|
+
command: nodeBootstrapCommand(),
|
|
1097
|
+
cwd: "/home/user",
|
|
1098
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
1099
|
+
requestTimeoutMs: args.requestTimeoutMs
|
|
1100
|
+
});
|
|
1101
|
+
emitPhaseCompleted(args.onPhase, now, startedAt, "runtime", bootstrap.ok, bootstrap.ok
|
|
1102
|
+
? "Node runtime ready"
|
|
1103
|
+
: "Node runtime bootstrap failed");
|
|
1104
|
+
if (!bootstrap.ok) {
|
|
1105
|
+
throw new Error(`desktop-cli runtime bootstrap failed for "${args.product}"`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const startedAt = now();
|
|
1109
|
+
emitPhaseStarted(args.onPhase, now, "install", `installing ${args.product} on the desktop`);
|
|
1110
|
+
const result = await runDetachedStep(desktop, {
|
|
1111
|
+
name: "desktop-cli-install",
|
|
1112
|
+
command: install,
|
|
1113
|
+
cwd: "/home/user",
|
|
1114
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
1115
|
+
requestTimeoutMs: args.requestTimeoutMs
|
|
1116
|
+
});
|
|
1117
|
+
emitPhaseCompleted(args.onPhase, now, startedAt, "install", result.ok, result.ok
|
|
1118
|
+
? `${args.product} installed`
|
|
1119
|
+
: `installing ${args.product} failed`);
|
|
1120
|
+
if (!result.ok) {
|
|
1121
|
+
// Fail closed: a participant handed a desktop where the product is not installed would produce
|
|
1122
|
+
// a transcript about a missing command, and that finding belongs to the harness, not the tool.
|
|
1123
|
+
// The tail rides along, scrubbed before truncation like every other provisioning failure — a
|
|
1124
|
+
// bare "install failed" is unactionable to whoever wrote the command.
|
|
1125
|
+
throw new Error(args.scrub(`desktop-cli install failed for "${args.product}" (${result.timedOut ? "timed out" : `exit ${result.exitCode ?? "?"}`}): ${tailOf(args.scrub(result.logTail))}`));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Open a terminal window on the desktop.
|
|
1130
|
+
*
|
|
1131
|
+
* The stock template is XFCE and ships xfce4-terminal (also aliased x-terminal-emulator), verified
|
|
1132
|
+
* live before this route was built. `x-terminal-emulator` is tried first so a template that swaps
|
|
1133
|
+
* the emulator still works; a desktop with neither is a template problem and fails closed rather
|
|
1134
|
+
* than handing a participant an empty screen and calling it a study.
|
|
1135
|
+
*/
|
|
1136
|
+
async function openDesktopTerminal(desktop, requestTimeoutMs, workdir) {
|
|
1137
|
+
const dir = workdir ?? "/home/user";
|
|
1138
|
+
const result = await runDetachedStep(desktop, {
|
|
1139
|
+
name: "desktop-cli-terminal",
|
|
1140
|
+
command: [
|
|
1141
|
+
"for candidate in x-terminal-emulator xfce4-terminal gnome-terminal konsole xterm; do",
|
|
1142
|
+
' if command -v "$candidate" >/dev/null 2>&1; then',
|
|
1143
|
+
// LANG is set on the terminal we open, not globally: the stock image declares no locale, and
|
|
1144
|
+
// a study that measures our own mojibake against an unconfigured template would be measuring
|
|
1145
|
+
// the template. The PRODUCT-side fix (an ASCII fallback when the locale is not UTF-8) is in
|
|
1146
|
+
// src/terminal-encoding.ts, and it is the one that matters for real users.
|
|
1147
|
+
` (cd ${shellSingleQuote(dir)} 2>/dev/null || cd /home/user; DISPLAY=:0 LANG=C.UTF-8 LC_ALL=C.UTF-8 nohup "$candidate" >/dev/null 2>&1 &)`,
|
|
1148
|
+
" sleep 3",
|
|
1149
|
+
' echo "humanish: opened $candidate"',
|
|
1150
|
+
" exit 0",
|
|
1151
|
+
" fi",
|
|
1152
|
+
"done",
|
|
1153
|
+
"echo 'humanish: no terminal emulator on this desktop template' >&2",
|
|
1154
|
+
"exit 1"
|
|
1155
|
+
].join("\n"),
|
|
1156
|
+
cwd: "/home/user",
|
|
1157
|
+
timeoutMs: 60_000,
|
|
1158
|
+
requestTimeoutMs
|
|
1159
|
+
});
|
|
1160
|
+
if (!result.ok) {
|
|
1161
|
+
throw new Error("desktop-cli lane could not open a terminal on this desktop template");
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1072
1164
|
async function startDesktopStream(desktop, browserWindowId) {
|
|
1073
1165
|
if (!browserWindowId) {
|
|
1074
1166
|
await desktop.stream.start({ requireAuth: true });
|
|
@@ -1182,6 +1274,10 @@ export function resolveSelfReportedFriction(session) {
|
|
|
1182
1274
|
*/
|
|
1183
1275
|
export async function runCuaLane(spec, deps) {
|
|
1184
1276
|
const { config, appUrl, cloneRoute, localTreeRoute, serve, subjectRepo, subjectEnvNames } = deps;
|
|
1277
|
+
const desktopCliRoute = deps.desktopCliRoute === true;
|
|
1278
|
+
// The local brain, when there is one. `appServer` owns a process, so the lane closes it.
|
|
1279
|
+
let appServer;
|
|
1280
|
+
let localAgentProvider;
|
|
1185
1281
|
const subjectEnvValues = config.subject.envValues ?? {};
|
|
1186
1282
|
const targetUrl = spec.targetUrl ?? appUrl;
|
|
1187
1283
|
const env = deps.env;
|
|
@@ -1409,6 +1505,18 @@ export async function runCuaLane(spec, deps) {
|
|
|
1409
1505
|
warnings: [...(desktopGeometry.warnings ?? []), mismatchWarning]
|
|
1410
1506
|
};
|
|
1411
1507
|
}
|
|
1508
|
+
if (desktopCliRoute) {
|
|
1509
|
+
// Put the product on the desktop before the participant sees it. UNKEYED, like every other
|
|
1510
|
+
// provisioning step: the participant's world is prepared by the harness, and what is being
|
|
1511
|
+
// studied starts at the moment they look at the screen.
|
|
1512
|
+
await provisionDesktopCli(desktop, {
|
|
1513
|
+
product: config.subject.product?.name ?? "",
|
|
1514
|
+
...(config.subject.product?.install === undefined ? {} : { install: config.subject.product.install }),
|
|
1515
|
+
requestTimeoutMs: deps.requestTimeoutMs,
|
|
1516
|
+
scrub: deps.scrubKnownValues,
|
|
1517
|
+
onPhase: onSubjectPhase
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1412
1520
|
if (cloneRoute && serve && subjectRepo) {
|
|
1413
1521
|
subjectCommit = await provisionCloneSubject(desktop, {
|
|
1414
1522
|
repo: subjectRepo,
|
|
@@ -1442,28 +1550,62 @@ export async function runCuaLane(spec, deps) {
|
|
|
1442
1550
|
...(deps.hooks.detachedTimers ?? {})
|
|
1443
1551
|
});
|
|
1444
1552
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1553
|
+
if (!desktopCliRoute) {
|
|
1554
|
+
const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, config.execution?.desktop?.browser);
|
|
1555
|
+
desktopBrowser = browserLaunch.evidence;
|
|
1556
|
+
launchedBrowserFamily = browserLaunch.family;
|
|
1557
|
+
browserLaunchIdentity = browserLaunch.identity;
|
|
1558
|
+
browserLaunched = true;
|
|
1559
|
+
await desktop.wait(BROWSER_SETTLE_MS).catch(() => undefined);
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
// A terminal window, opened the way the browser is opened on every other route: the
|
|
1563
|
+
// participant arrives at a desktop with the thing they were asked to use already in front
|
|
1564
|
+
// of them. They can still open another from the dock — that is the point of a desktop.
|
|
1565
|
+
await openDesktopTerminal(desktop, deps.requestTimeoutMs, config.subject.product?.workdir);
|
|
1566
|
+
await desktop.wait(BROWSER_SETTLE_MS).catch(() => undefined);
|
|
1567
|
+
}
|
|
1568
|
+
// Start the brain BEFORE the first screenshot: the app-server handshake is ~500ms, and it
|
|
1569
|
+
// is paid here, while the sandbox is still settling, rather than inside turn one.
|
|
1570
|
+
if (deps.localAgent === "codex") {
|
|
1571
|
+
appServer = await startAppServerSession({
|
|
1572
|
+
...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
|
|
1573
|
+
...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model }),
|
|
1574
|
+
// The persona lives on the THREAD, so it is stated once instead of re-sent every turn.
|
|
1575
|
+
baseInstructions: spec.instructions
|
|
1576
|
+
});
|
|
1577
|
+
localAgentProvider = appServer.provider;
|
|
1578
|
+
}
|
|
1579
|
+
else if (deps.localAgent === "claude") {
|
|
1580
|
+
localAgentProvider = createLocalAgentProvider({
|
|
1581
|
+
agent: "claude",
|
|
1582
|
+
...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
|
|
1583
|
+
...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model })
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1451
1586
|
// World is ready: release the pipeline gate so the remaining lanes may start.
|
|
1452
1587
|
provisioned = true;
|
|
1453
1588
|
signal(true);
|
|
1454
1589
|
try {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1590
|
+
// No browser means no browser geometry, and none is invented: the CSS-viewport facts a
|
|
1591
|
+
// browser reports have no counterpart in a terminal window, and an empty record shaped like
|
|
1592
|
+
// a measurement would read as one. The screen geometry above is still verified.
|
|
1593
|
+
if (!desktopCliRoute) {
|
|
1594
|
+
const browserGeometry = await captureDesktopBrowserGeometry({
|
|
1595
|
+
desktop,
|
|
1596
|
+
browserFamily: launchedBrowserFamily,
|
|
1597
|
+
...(browserLaunchIdentity === undefined ? {} : { launchIdentity: browserLaunchIdentity }),
|
|
1598
|
+
laneId: spec.laneId,
|
|
1599
|
+
targetUrl,
|
|
1600
|
+
requestedScreen: spec.resolution,
|
|
1601
|
+
requestTimeoutMs: deps.requestTimeoutMs
|
|
1602
|
+
});
|
|
1603
|
+
initialBrowserGeometry = browserGeometry;
|
|
1604
|
+
browserWindowId = browserGeometry.browserWindowId;
|
|
1605
|
+
browserTargetId = browserGeometry.browserTargetId;
|
|
1606
|
+
}
|
|
1607
|
+
// The WHOLE desktop, not one window: a person studying a terminal app opens other windows,
|
|
1608
|
+
// and a stream bound to the first one would quietly stop being evidence.
|
|
1467
1609
|
await startDesktopStream(desktop, browserWindowId);
|
|
1468
1610
|
const candidateStreamUrl = desktop.stream.getUrl({
|
|
1469
1611
|
authKey: desktop.stream.getAuthKey(),
|
|
@@ -1506,6 +1648,10 @@ export async function runCuaLane(spec, deps) {
|
|
|
1506
1648
|
: spec.instructions,
|
|
1507
1649
|
persona: spec.persona,
|
|
1508
1650
|
timeoutMs: deps.timeoutMs,
|
|
1651
|
+
// The brain is either a keyed API client or a CLI the operator is already signed in to.
|
|
1652
|
+
// Everything below this line — loop, executor, trace, affordances — is identical either
|
|
1653
|
+
// way, which is what makes a local-agent run comparable to an API one.
|
|
1654
|
+
...(localAgentProvider === undefined ? {} : { provider: localAgentProvider }),
|
|
1509
1655
|
openai: {
|
|
1510
1656
|
apiKey: deps.openaiApiKey,
|
|
1511
1657
|
...(config.actors[0]?.model ? { model: config.actors[0].model } : {}),
|
|
@@ -1568,6 +1714,9 @@ export async function runCuaLane(spec, deps) {
|
|
|
1568
1714
|
sessionError = redactText(deps.scrubKnownValues(toErrorMessage(error)));
|
|
1569
1715
|
}
|
|
1570
1716
|
finally {
|
|
1717
|
+
// The local brain owns a process. Close it before anything else can throw: a leaked
|
|
1718
|
+
// app-server per lane would outlive the run and keep a thread open on the operator's plan.
|
|
1719
|
+
appServer?.close();
|
|
1571
1720
|
// Stop the mid-run inbox-surface loop FIRST — before the teardown evidence drain below — so the two
|
|
1572
1721
|
// `cat`s never overlap and the final surface state is deterministic. A surface failure can never
|
|
1573
1722
|
// block teardown (the loop body is fully try/caught and this await is on its already-caught promise).
|
|
@@ -2085,6 +2234,8 @@ async function runCuaActorLabInScope(options) {
|
|
|
2085
2234
|
const env = hooks.env ?? process.env;
|
|
2086
2235
|
const render = hooks.renderObserverFn ?? renderObserver;
|
|
2087
2236
|
const cloneRoute = config.subject.source === "clone";
|
|
2237
|
+
// A CLI studied at a desktop (#495): nothing cloned, no browser, a terminal instead.
|
|
2238
|
+
const desktopCliRoute = config.subject.source === "desktop-cli";
|
|
2088
2239
|
const localTreeRoute = config.subject.source === "local-tree";
|
|
2089
2240
|
// Both routes provision the subject in-sandbox (clone via git, local-tree via pack+upload)
|
|
2090
2241
|
// and then share the identical install/build/state/start/probe pipeline, so every seam that
|
|
@@ -2151,10 +2302,12 @@ async function runCuaActorLabInScope(options) {
|
|
|
2151
2302
|
return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", stateReason, descriptor.id);
|
|
2152
2303
|
}
|
|
2153
2304
|
}
|
|
2154
|
-
// Re-enforce the entry-target boundary (library API surface).
|
|
2305
|
+
// Re-enforce the entry-target boundary (library API surface). A desktop-cli study has no entry
|
|
2306
|
+
// target at all — the subject is a program on the machine, not an address — so the boundary is
|
|
2307
|
+
// vacuous there rather than violated by an empty string.
|
|
2155
2308
|
const allowPublicTargets = config.policies?.allowPublicTargets === true;
|
|
2156
2309
|
const declaredTargets = [appUrl, ...(actor?.lanes ?? []).map((lane) => lane.target).filter((target) => target !== undefined)];
|
|
2157
|
-
const entryTargetSafe = declaredTargets.every((target) => provisionedRoute || localAppSubject
|
|
2310
|
+
const entryTargetSafe = desktopCliRoute || declaredTargets.every((target) => provisionedRoute || localAppSubject
|
|
2158
2311
|
? isLoopbackUrl(target)
|
|
2159
2312
|
: allowPublicTargets
|
|
2160
2313
|
? isHttpUrl(target)
|
|
@@ -2248,14 +2401,48 @@ async function runCuaActorLabInScope(options) {
|
|
|
2248
2401
|
const redactRepoLabel = config.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
|
|
2249
2402
|
const publicRepo = cloneRoute && subjectRepo ? (redactRepoLabel ? "repo-01" : subjectRepo) : undefined;
|
|
2250
2403
|
const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
|
|
2251
|
-
//
|
|
2404
|
+
// The operator's own signed-in coding agent is the brain, so there is no provider key to ask
|
|
2405
|
+
// for — the entire point of the actor. E2B is still required: the persona needs a machine.
|
|
2406
|
+
const localAgentRoute = actorType === "local-agent";
|
|
2407
|
+
// Which local CLI, from its OWN field: `model` means the model, so that "Claude Code running
|
|
2408
|
+
// Opus" is sayable. Preflight below refuses when the chosen one is missing or signed out — that
|
|
2409
|
+
// news is worthless after a sandbox is paid for.
|
|
2410
|
+
const preferredLocalAgent = config.actors[0]?.localAgent ?? "codex";
|
|
2411
|
+
// Key-gating is route-aware: the in-process route uses the caller's OWN model + executor, and
|
|
2412
|
+
// the local-agent route uses a CLI the operator has already signed in to.
|
|
2252
2413
|
if (!dryRun && !inProcessRoute) {
|
|
2253
2414
|
const missingKeys = [
|
|
2254
|
-
...(openaiApiKey ? [] : ["OPENAI_API_KEY"]),
|
|
2415
|
+
...(openaiApiKey || localAgentRoute ? [] : ["OPENAI_API_KEY"]),
|
|
2255
2416
|
...(e2bApiKey ? [] : ["E2B_API_KEY"])
|
|
2256
2417
|
];
|
|
2257
2418
|
if (missingKeys.length > 0) {
|
|
2258
|
-
|
|
2419
|
+
// The moment someone new actually hits the wall. If a signed-in coding agent is sitting
|
|
2420
|
+
// right there, say so HERE rather than making them go and find an API key — that detour is
|
|
2421
|
+
// where most people trying humanish stop.
|
|
2422
|
+
const suggestion = missingKeys.includes("OPENAI_API_KEY")
|
|
2423
|
+
? await (async () => {
|
|
2424
|
+
const ready = (await detectLocalAgents()).filter((agent) => agent.credentialsPresent);
|
|
2425
|
+
return ready.length === 0
|
|
2426
|
+
? ""
|
|
2427
|
+
: ` You have ${ready.map((agent) => agent.label).join(" and ")} signed in on this machine`
|
|
2428
|
+
+ ` — set actors[0].type: local-agent to use ${ready.length === 1 ? "it" : "one"} instead of a key.`;
|
|
2429
|
+
})()
|
|
2430
|
+
: "";
|
|
2431
|
+
return fail("HUMANISH_CUA_LAB_KEYS_MISSING", `Live computer-use labs need ${missingKeys.join(" and ")} in the environment (values are never persisted). ${describeMissingKeys(missingKeys, env)}${suggestion}`, descriptor.id);
|
|
2432
|
+
}
|
|
2433
|
+
if (localAgentRoute) {
|
|
2434
|
+
// Refuse HERE, before a sandbox exists. "codex is not installed" discovered after the
|
|
2435
|
+
// machine is paid for is the same information delivered at the worst possible moment.
|
|
2436
|
+
const available = await detectLocalAgents();
|
|
2437
|
+
const chosen = available.find((agent) => agent.id === preferredLocalAgent);
|
|
2438
|
+
if (chosen === undefined) {
|
|
2439
|
+
return fail("HUMANISH_CUA_LAB_KEYS_MISSING", `actors[0].type: local-agent needs the ${preferredLocalAgent} CLI on PATH and signed in. `
|
|
2440
|
+
+ `Install it, or set OPENAI_API_KEY and use actors[0].type: openai-computer-use instead.`, descriptor.id);
|
|
2441
|
+
}
|
|
2442
|
+
if (!chosen.credentialsPresent) {
|
|
2443
|
+
return fail("HUMANISH_CUA_LAB_KEYS_MISSING", `${chosen.label} is installed but not signed in — run \`${chosen.bin}\` once to log in. `
|
|
2444
|
+
+ "humanish never reads its credentials; it only checks that the file exists.", descriptor.id);
|
|
2445
|
+
}
|
|
2259
2446
|
}
|
|
2260
2447
|
const missingSubjectEnv = subjectEnvNames.filter((name) => !env[name]?.trim());
|
|
2261
2448
|
if (missingSubjectEnv.length > 0) {
|
|
@@ -2343,7 +2530,9 @@ async function runCuaActorLabInScope(options) {
|
|
|
2343
2530
|
config,
|
|
2344
2531
|
descriptor,
|
|
2345
2532
|
appUrl,
|
|
2533
|
+
...(localAgentRoute ? { localAgent: preferredLocalAgent } : {}),
|
|
2346
2534
|
cloneRoute,
|
|
2535
|
+
desktopCliRoute,
|
|
2347
2536
|
localTreeRoute,
|
|
2348
2537
|
...(serve === undefined ? {} : { serve }),
|
|
2349
2538
|
...(subjectRepo === undefined ? {} : { subjectRepo }),
|
|
@@ -4103,7 +4292,13 @@ export function buildCuaFanoutBundle(args) {
|
|
|
4103
4292
|
// collapse the run to one word; this does not (docs/principles/three-roles.md).
|
|
4104
4293
|
const terminalOutcomes = (outcomes ?? []).filter((outcome) => outcome?.session?.status !== undefined);
|
|
4105
4294
|
const participants = terminalOutcomes.length > 0
|
|
4106
|
-
? tallyParticipantOutcomes(
|
|
4295
|
+
? tallyParticipantOutcomes(
|
|
4296
|
+
// A NO-ENGAGEMENT lane is not a participant who reached the goal. It said "done" having
|
|
4297
|
+
// taken zero actions and said nothing, and `passedLanes` below already refuses to count
|
|
4298
|
+
// it — but `reachedGoal` was reading the trace status directly, so one run could be both
|
|
4299
|
+
// "not a passed lane" AND "1/1 reached the goal". The headline number a researcher reads
|
|
4300
|
+
// first was the dishonest one. Found by a provider bug that ended a study on turn one.
|
|
4301
|
+
terminalOutcomes.map((outcome) => (outcome.noEngagement === true ? "incomplete" : outcome.session.status)),
|
|
4107
4302
|
// A participant who reached the goal AND told you the road there was broken is the most
|
|
4108
4303
|
// useful result a study produces; reporting only the outcome would bury it.
|
|
4109
4304
|
terminalOutcomes.map((outcome) => outcome.reportedFriction === true))
|