humanish 0.106.0 → 0.106.1
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/AGENTS.md +3 -1
- package/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +4 -2
- package/dist/actors/codex/qualified-versions.d.ts +1 -1
- package/dist/actors/codex/qualified-versions.js +1 -1
- package/dist/actors/contract.d.ts +1 -1
- package/dist/actors/contract.js +7 -1
- package/dist/lab/composition-rules.d.ts +2 -0
- package/dist/lab/composition-rules.js +15 -6
- package/dist/lab/plan-base.d.ts +6 -0
- package/dist/lab/plan-base.js +16 -3
- package/dist/lab/plan-types.d.ts +12 -4
- package/dist/observer-app.html +1 -1
- package/dist/routes/computer-use/participant-model.js +10 -9
- package/dist/routes/computer-use/plan.js +7 -5
- package/dist/routes/computer-use/preflight.d.ts +3 -6
- package/dist/routes/computer-use/preflight.js +11 -7
- package/dist/routes/computer-use/route.d.ts +3 -3
- package/dist/routes/computer-use/route.js +3 -3
- package/dist/routes/computer-use/run-lanes.js +3 -3
- package/dist/routes/computer-use/setup.d.ts +0 -3
- package/dist/routes/computer-use/setup.js +3 -15
- package/dist/routes/computer-use/types.d.ts +3 -4
- package/dist/routes/computer-use/types.js +0 -1
- package/dist/routes/scripted/subject.js +2 -1
- package/dist/routes/shared-world/provisioned.js +2 -1
- package/dist/routes/shared-world/seats.js +3 -3
- package/dist/routes/terminal/live-sandbox.d.ts +1 -0
- package/dist/routes/terminal/live-sandbox.js +5 -1
- package/dist/routes/terminal/sandbox.d.ts +2 -0
- package/dist/routes/terminal/sandbox.js +4 -7
- package/dist/run/reclaim.d.ts +1 -1
- package/dist/run/reclaim.js +11 -1
- package/dist/substrates/desktop-session.d.ts +2 -0
- package/dist/substrates/e2b/sandbox.d.ts +2 -0
- package/dist/substrates/e2b/sandbox.js +21 -7
- package/dist/substrates/e2b/sdk.d.ts +12 -1
- package/dist/substrates/e2b/sdk.js +28 -7
- package/docs/architecture/restricted-codex-analysis.md +4 -2
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// finished session. The desktop side stays with the lane runner.
|
|
4
4
|
import { startClaudeSession } from "../../actors/local-agent/claude-session.js";
|
|
5
5
|
import { createLocalAgentProvider } from "../../actors/local-agent/cli.js";
|
|
6
|
-
import {
|
|
6
|
+
import { pricedModel } from "../../lab/plan-base.js";
|
|
7
7
|
import { estimateActorCostForExecution, round6 } from "../../run/pricing.js";
|
|
8
8
|
import { redactText } from "../../evidence/redaction.js";
|
|
9
9
|
import { createRestrictedCodexParticipant } from "../../actors/codex/restricted-participant.js";
|
|
@@ -14,7 +14,7 @@ import { hollowCompletion } from "../../run/judge.js";
|
|
|
14
14
|
import { legacyHookSpecOf } from "./legacy-lane-spec.js";
|
|
15
15
|
/** Starts the lane's model: a caller's provider, the operator's Codex, a Claude session, or none. */
|
|
16
16
|
export async function startParticipantModel(spec, deps, executor) {
|
|
17
|
-
const { config, env } = deps;
|
|
17
|
+
const { config, env, brain } = deps;
|
|
18
18
|
if (deps.hooks.buildProvider) {
|
|
19
19
|
return {
|
|
20
20
|
provider: await deps.hooks.buildProvider({
|
|
@@ -26,7 +26,8 @@ export async function startParticipantModel(spec, deps, executor) {
|
|
|
26
26
|
}),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
const localAgent = brain.kind === "local-agent" ? brain.agent : undefined;
|
|
30
|
+
if (localAgent === "codex") {
|
|
30
31
|
// Hosted local-agent studies use the same native participant engine as local desktops.
|
|
31
32
|
// Operator auth deliberately retains the operator's Codex home, config and supported auth
|
|
32
33
|
// stores instead of applying the isolated restricted-account profile used by local studies.
|
|
@@ -35,13 +36,13 @@ export async function startParticipantModel(spec, deps, executor) {
|
|
|
35
36
|
...(spec.planned.limits.reasoningEffort === undefined
|
|
36
37
|
? {}
|
|
37
38
|
: { reasoningEffort: spec.planned.limits.reasoningEffort }),
|
|
38
|
-
...(
|
|
39
|
+
...(brain.declaredModel === undefined ? {} : { model: brain.declaredModel }),
|
|
39
40
|
...(executor.speechEnabled === true ? { speechEnabled: true } : {}),
|
|
40
41
|
session: { env },
|
|
41
42
|
});
|
|
42
43
|
return { provider: codexParticipant.provider, codexParticipant };
|
|
43
44
|
}
|
|
44
|
-
if (
|
|
45
|
+
if (localAgent === "claude") {
|
|
45
46
|
// One session for the whole run, like the codex thread above (#520). The one-shot
|
|
46
47
|
// provider (createLocalAgentProvider) spawned `claude -p` per turn, and every turn
|
|
47
48
|
// started with no memory of the last. HUMANISH_LOCAL_AGENT_ONE_SHOT=1 keeps that path
|
|
@@ -58,7 +59,7 @@ export async function startParticipantModel(spec, deps, executor) {
|
|
|
58
59
|
...(spec.planned.limits.reasoningEffort === undefined
|
|
59
60
|
? {}
|
|
60
61
|
: { reasoningEffort: spec.planned.limits.reasoningEffort }),
|
|
61
|
-
...(
|
|
62
|
+
...(brain.declaredModel === undefined ? {} : { model: brain.declaredModel }),
|
|
62
63
|
}),
|
|
63
64
|
};
|
|
64
65
|
}
|
|
@@ -66,7 +67,7 @@ export async function startParticipantModel(spec, deps, executor) {
|
|
|
66
67
|
...(spec.planned.limits.reasoningEffort === undefined
|
|
67
68
|
? {}
|
|
68
69
|
: { reasoningEffort: spec.planned.limits.reasoningEffort }),
|
|
69
|
-
...(
|
|
70
|
+
...(brain.declaredModel === undefined ? {} : { model: brain.declaredModel }),
|
|
70
71
|
});
|
|
71
72
|
return { provider: claudeSession.provider, claudeSession };
|
|
72
73
|
}
|
|
@@ -80,7 +81,7 @@ export function participantSessionOptions(spec, deps, ready, provider, writeScre
|
|
|
80
81
|
// injected pure per-turn estimator keyed on the resolved model. Preflight already refused a
|
|
81
82
|
// cap on an unpriced model, so the estimate is measurable whenever a cap is in force. The
|
|
82
83
|
// model id here matches provider.version (openai-responses-cu resolves the default when unset).
|
|
83
|
-
const capModelId =
|
|
84
|
+
const capModelId = pricedModel(deps.brain);
|
|
84
85
|
const maxUsd = config.execution?.caps?.maxUsd;
|
|
85
86
|
return {
|
|
86
87
|
instructions: inbox
|
|
@@ -94,7 +95,7 @@ export function participantSessionOptions(spec, deps, ready, provider, writeScre
|
|
|
94
95
|
...(provider === undefined ? {} : { provider: provider }),
|
|
95
96
|
openai: {
|
|
96
97
|
apiKey: deps.openaiApiKey,
|
|
97
|
-
...(
|
|
98
|
+
...(deps.brain.declaredModel ? { model: deps.brain.declaredModel } : {}),
|
|
98
99
|
// Per-LANE, not per-actor: two lanes at different efforts is the control this exists for.
|
|
99
100
|
...(spec.planned.limits.reasoningEffort === undefined
|
|
100
101
|
? {}
|
|
@@ -6,10 +6,11 @@ import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
|
6
6
|
import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
7
7
|
import { isHttpUrl, isLoopbackUrl } from "../../lab/parse/subject.js";
|
|
8
8
|
import { subjectStateInvalidReason } from "../../lab/parse/subject-state.js";
|
|
9
|
-
import { brainOf, capsOf, desktopRequirements, planBase, provisionedSubject, } from "../../lab/plan-base.js";
|
|
9
|
+
import { brainOf, callerBrainOf, capsOf, desktopRequirements, planBase, provisionedSubject, } from "../../lab/plan-base.js";
|
|
10
10
|
import { computerUseParticipants, declaredTargets } from "../../lab/plan-participants.js";
|
|
11
11
|
import { MAX_CUA_LANES } from "../../lab/routing.js";
|
|
12
12
|
import { cloneTargetValidationReason, cuaLaneValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, taskProtocolValidationReason, } from "../../lab/validation.js";
|
|
13
|
+
import { desktopCliProductReason } from "../../lab/composition-rules.js";
|
|
13
14
|
import { isLocalBrowserLab } from "../../substrates/local/runtime-config.js";
|
|
14
15
|
import { boundedConcurrency, defaultSessionTimeoutMs, resolveParticipantSandboxMs, } from "./participant-runs.js";
|
|
15
16
|
import { MAX_SANDBOX_MS } from "../../substrates/e2b/lifetime.js";
|
|
@@ -236,9 +237,10 @@ export function planComputerUseLab(config, input) {
|
|
|
236
237
|
if (hooks.buildExecutor !== undefined &&
|
|
237
238
|
(source === "clone" || source === "local-tree" || source === "desktop-cli"))
|
|
238
239
|
return refuse("in-scope", "HUMANISH_CUA_LAB_SUBJECT_INVALID", `cuaHooks.buildExecutor drives subject.appUrl in this process, and a ${source} subject needs the hosted desktop the in-process route never creates. Use an app-url or local-app subject with buildExecutor, or remove buildExecutor to run on a hosted desktop.`, actor);
|
|
239
|
-
// The parser
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
// The parser's rule; without a product the desktop study fails later.
|
|
241
|
+
const productReason = desktopCliProductReason(config);
|
|
242
|
+
if (productReason)
|
|
243
|
+
return refuse("in-scope", "HUMANISH_CUA_LAB_SUBJECT_INVALID", productReason, actor);
|
|
242
244
|
const participants = computerUseParticipants(config, input.countOverride);
|
|
243
245
|
if (participants.length > MAX_CUA_LANES)
|
|
244
246
|
return refuse("after-personas", "HUMANISH_CUA_LAB_FANOUT_INVALID", `Computer-use fan-out is capped at ${MAX_CUA_LANES} participants (resolved ${participants.length}); N concurrent paid desktops is real spend.`, actor);
|
|
@@ -261,7 +263,7 @@ export function planComputerUseLab(config, input) {
|
|
|
261
263
|
const runner = hooks.buildExecutor !== undefined
|
|
262
264
|
? {
|
|
263
265
|
desktop: "in-process",
|
|
264
|
-
brain:
|
|
266
|
+
brain: callerBrainOf(config),
|
|
265
267
|
participants: [first],
|
|
266
268
|
subject: source === "local-app" ? { kind: "local-app", appUrl } : appUrlSubject,
|
|
267
269
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { ComputerUsePlan } from "../../lab/plan-types.js";
|
|
1
|
+
import type { Brain, ComputerUsePlan } from "../../lab/plan-types.js";
|
|
2
2
|
import type { LabCommsExternal } from "../../lab/types.js";
|
|
3
|
-
import { type LocalAgentId } from "../../actors/local-agent/cli.js";
|
|
4
3
|
import type { CuaActorLabErrorCode, CuaActorLabHooks } from "./types.js";
|
|
5
4
|
/**
|
|
6
5
|
* The first reason a live computer-use run cannot start on this machine: missing keys, a missing
|
|
@@ -10,14 +9,12 @@ import type { CuaActorLabErrorCode, CuaActorLabHooks } from "./types.js";
|
|
|
10
9
|
*/
|
|
11
10
|
export declare function liveCuaRejection(args: {
|
|
12
11
|
caps: ComputerUsePlan["caps"];
|
|
13
|
-
/** The
|
|
14
|
-
|
|
12
|
+
/** The plan's brain: whether a local agent drives the participant, and the model a cap prices. */
|
|
13
|
+
brain: Brain;
|
|
15
14
|
hooks: CuaActorLabHooks;
|
|
16
15
|
env: Record<string, string | undefined>;
|
|
17
16
|
openaiApiKey: string;
|
|
18
17
|
e2bApiKey: string;
|
|
19
|
-
localAgentRoute: boolean;
|
|
20
|
-
preferredLocalAgent: LocalAgentId;
|
|
21
18
|
subjectEnvNames: string[];
|
|
22
19
|
externalCommsConfig: LabCommsExternal | undefined;
|
|
23
20
|
}): Promise<{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pricedModel } from "../../lab/plan-base.js";
|
|
2
2
|
import { detectLocalAgents } from "../../actors/local-agent/cli.js";
|
|
3
3
|
import { localAgentRefusal } from "../../actors/local-agent/readiness.js";
|
|
4
4
|
import { describeMissingKeys } from "../../keys/key-resolution.js";
|
|
@@ -19,9 +19,12 @@ const LOCAL_AGENT_REFUSAL_CODES = {
|
|
|
19
19
|
* already paid for a desktop.
|
|
20
20
|
*/
|
|
21
21
|
export async function liveCuaRejection(args) {
|
|
22
|
-
const { caps,
|
|
22
|
+
const { caps, brain, hooks, env, openaiApiKey, e2bApiKey, subjectEnvNames, externalCommsConfig } = args;
|
|
23
|
+
// The operator's own signed-in coding agent is the brain, so there is no provider key to ask
|
|
24
|
+
// for. E2B is still required: the persona needs a machine.
|
|
25
|
+
const localAgent = brain.kind === "local-agent" ? brain.agent : undefined;
|
|
23
26
|
const missingKeys = [
|
|
24
|
-
...(openaiApiKey ||
|
|
27
|
+
...(openaiApiKey || localAgent || hooks.buildProvider ? [] : ["OPENAI_API_KEY"]),
|
|
25
28
|
...(e2bApiKey || participantDesktopOf(hooks) !== undefined ? [] : ["E2B_API_KEY"]),
|
|
26
29
|
];
|
|
27
30
|
if (missingKeys.length > 0) {
|
|
@@ -42,10 +45,10 @@ export async function liveCuaRejection(args) {
|
|
|
42
45
|
message: `Live computer-use labs need ${missingKeys.join(" and ")} in the environment (values are never persisted). ${describeMissingKeys(missingKeys, env)}${suggestion}`,
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
|
-
if (
|
|
48
|
+
if (localAgent && !hooks.buildProvider) {
|
|
46
49
|
// Refuse HERE, before a sandbox exists. "codex is not installed" discovered after the
|
|
47
50
|
// machine is paid for is the same information delivered at the worst possible moment.
|
|
48
|
-
const refusal = await localAgentRefusal({ agent:
|
|
51
|
+
const refusal = await localAgentRefusal({ agent: localAgent, env, caps });
|
|
49
52
|
if (refusal)
|
|
50
53
|
return { code: LOCAL_AGENT_REFUSAL_CODES[refusal.kind], message: refusal.message };
|
|
51
54
|
}
|
|
@@ -63,11 +66,12 @@ export async function liveCuaRejection(args) {
|
|
|
63
66
|
// uncapped: an unenforceable cap is more dangerous than none. The operator adds a rate to
|
|
64
67
|
// src/run/pricing.ts (the honest place) or removes the cap.
|
|
65
68
|
if (caps.maxUsd !== undefined || caps.maxTotalUsd !== undefined) {
|
|
66
|
-
const
|
|
69
|
+
const model = pricedModel(brain);
|
|
70
|
+
const capModelId = model.trim().toLowerCase();
|
|
67
71
|
if (!MODEL_RATES[capModelId]) {
|
|
68
72
|
return {
|
|
69
73
|
code: "HUMANISH_CUA_LAB_UNPRICED_CAP",
|
|
70
|
-
message: `execution.caps declares a spend cap (maxUsd/maxTotalUsd) but src/run/pricing.ts has no rate for model "${model
|
|
74
|
+
message: `execution.caps declares a spend cap (maxUsd/maxTotalUsd) but src/run/pricing.ts has no rate for model "${model}"; add a rate or remove the cap — an unenforceable cap is refused rather than run uncapped.`,
|
|
71
75
|
};
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -26,8 +26,8 @@ export declare function admitComputerUsePlan(plan: ComputerUsePlan, input: Compu
|
|
|
26
26
|
* Run a computer-use plan: its local checks, then the run. The run scope in runAdmittedCuaRun
|
|
27
27
|
* finalizes any status record the run opened, on every exit, so a test or library caller does not
|
|
28
28
|
* leave the 5 s status cadence writing into a directory something else is deleting (an unrelated
|
|
29
|
-
* ENOTEMPTY). Participants
|
|
30
|
-
*
|
|
31
|
-
* cap) and by the compatibility hooks, which take the whole config.
|
|
29
|
+
* ENOTEMPTY). Participants and the brain (model and local agent) come from the plan. `config` is
|
|
30
|
+
* still read for values the plan does not carry yet (persona ids, comms settings, the
|
|
31
|
+
* per-participant cap) and by the compatibility hooks, which take the whole config.
|
|
32
32
|
*/
|
|
33
33
|
export declare function runComputerUsePlan(plan: ComputerUsePlan, input: ComputerUseRunInput, config: LabConfig): Promise<CuaActorLabResult>;
|
|
@@ -107,9 +107,9 @@ function cuaOutcome(result) {
|
|
|
107
107
|
* Run a computer-use plan: its local checks, then the run. The run scope in runAdmittedCuaRun
|
|
108
108
|
* finalizes any status record the run opened, on every exit, so a test or library caller does not
|
|
109
109
|
* leave the 5 s status cadence writing into a directory something else is deleting (an unrelated
|
|
110
|
-
* ENOTEMPTY). Participants
|
|
111
|
-
*
|
|
112
|
-
* cap) and by the compatibility hooks, which take the whole config.
|
|
110
|
+
* ENOTEMPTY). Participants and the brain (model and local agent) come from the plan. `config` is
|
|
111
|
+
* still read for values the plan does not carry yet (persona ids, comms settings, the
|
|
112
|
+
* per-participant cap) and by the compatibility hooks, which take the whole config.
|
|
113
113
|
*/
|
|
114
114
|
export async function runComputerUsePlan(plan, input, config) {
|
|
115
115
|
const admission = await admitCuaRun(plan, input, config);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// receiving, the lanes themselves, and the drain of an operator-hosted inbox.
|
|
3
3
|
import { prepareReceivingRun } from "../../comms/receiving-runtime.js";
|
|
4
4
|
import { liveObserverResult } from "../../observer/live.js";
|
|
5
|
-
import {
|
|
5
|
+
import { pricedModel } from "../../lab/plan-base.js";
|
|
6
6
|
import { runAllCuaParticipants } from "./lanes.js";
|
|
7
7
|
import { startLiveTraceFlush } from "./live-flush.js";
|
|
8
8
|
import { drainExternalComms } from "./external-comms.js";
|
|
@@ -13,7 +13,7 @@ import {} from "./types.js";
|
|
|
13
13
|
* the participant outcomes and the warnings the finish records.
|
|
14
14
|
*/
|
|
15
15
|
export async function runLabParticipants(setup, participants) {
|
|
16
|
-
const { plan, input,
|
|
16
|
+
const { plan, input, cwd, streams, descriptor, subjectRoute, run } = setup;
|
|
17
17
|
const { participantRuns, participantPlan, scrubKnownValues, bundleBase } = setup;
|
|
18
18
|
const { env, knownSecretValues, deps, liveTrace, externalComms, inProgress, fail } = participants;
|
|
19
19
|
const { dryRun } = plan;
|
|
@@ -53,7 +53,7 @@ export async function runLabParticipants(setup, participants) {
|
|
|
53
53
|
const liveFlush = startLiveTraceFlush({
|
|
54
54
|
bundle: inProgressBundle,
|
|
55
55
|
participantRuns: participantRuns,
|
|
56
|
-
model:
|
|
56
|
+
model: pricedModel(plan.runner.brain),
|
|
57
57
|
write: (bundle) => run.writeSnapshot(bundle),
|
|
58
58
|
});
|
|
59
59
|
liveTrace.flush = liveFlush.flush;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type RunScope } from "../../run/run.js";
|
|
2
|
-
import { type LocalAgentId } from "../../actors/local-agent/cli.js";
|
|
3
2
|
import type { CuaActorDescriptor } from "../../actors/registry.js";
|
|
4
3
|
import type { LabCommsEmail, LabCommsExternal, LabConfig } from "../../lab/types.js";
|
|
5
4
|
import { type RunRerunLineage } from "../../run/bundle.js";
|
|
@@ -125,8 +124,6 @@ export declare function admitCuaRun(plan: ComputerUsePlan, input: ComputerUseRun
|
|
|
125
124
|
scrubKnownValues: (text: string) => string;
|
|
126
125
|
publicRepo: string | undefined;
|
|
127
126
|
hasGithubToken: boolean;
|
|
128
|
-
localAgentRoute: boolean;
|
|
129
|
-
preferredLocalAgent: LocalAgentId;
|
|
130
127
|
localTreeArchive: LocalTreeArchive | undefined;
|
|
131
128
|
localTreeArchiveBuffer: ArrayBuffer | undefined;
|
|
132
129
|
};
|
|
@@ -6,7 +6,6 @@ import { realpath } from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import {} from "../../run/run.js";
|
|
8
8
|
import { externalInboxUrl } from "../../comms/sandbox-catch.js";
|
|
9
|
-
import {} from "../../actors/local-agent/cli.js";
|
|
10
9
|
import { redactText, scrubLiterals, toErrorMessage } from "../../evidence/redaction.js";
|
|
11
10
|
import { buildRunSource } from "../../run/bundle.js";
|
|
12
11
|
import { assertPreparedSelectedOutputDirectory, prepareContainedOutputDirectory, prepareSelectedOutputDirectory, } from "../../run/contained-output.js";
|
|
@@ -127,25 +126,16 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
127
126
|
const redactRepoLabel = plan.residual.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
|
|
128
127
|
const publicRepo = cloneRoute && subjectRepo ? (redactRepoLabel ? "repo-01" : subjectRepo) : undefined;
|
|
129
128
|
const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
|
|
130
|
-
// The operator's own signed-in coding agent is the brain, so there is no provider key to ask
|
|
131
|
-
// for — the entire point of the actor. E2B is still required: the persona needs a machine.
|
|
132
|
-
const localAgentRoute = descriptor.id === "local-agent";
|
|
133
|
-
// Which local CLI, from its OWN field: `model` means the model, so that "Claude Code running
|
|
134
|
-
// Opus" is sayable. Preflight below refuses when the chosen one is missing or signed out — that
|
|
135
|
-
// news is worthless after a sandbox is paid for.
|
|
136
|
-
const preferredLocalAgent = config.actors[0]?.localAgent ?? "codex";
|
|
137
129
|
// Key-gating is route-aware: the in-process route uses the caller's OWN model + executor, and
|
|
138
130
|
// the local-agent route uses a CLI the operator has already signed in to.
|
|
139
131
|
if (!dryRun && !inProcessRoute) {
|
|
140
132
|
const rejection = await liveCuaRejection({
|
|
141
133
|
caps: plan.caps,
|
|
142
|
-
|
|
134
|
+
brain: plan.runner.brain,
|
|
143
135
|
hooks,
|
|
144
136
|
env,
|
|
145
137
|
openaiApiKey,
|
|
146
138
|
e2bApiKey,
|
|
147
|
-
localAgentRoute,
|
|
148
|
-
preferredLocalAgent,
|
|
149
139
|
subjectEnvNames,
|
|
150
140
|
externalCommsConfig,
|
|
151
141
|
});
|
|
@@ -195,8 +185,6 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
195
185
|
scrubKnownValues,
|
|
196
186
|
publicRepo,
|
|
197
187
|
hasGithubToken,
|
|
198
|
-
localAgentRoute,
|
|
199
|
-
preferredLocalAgent,
|
|
200
188
|
localTreeArchive,
|
|
201
189
|
localTreeArchiveBuffer,
|
|
202
190
|
},
|
|
@@ -312,7 +300,7 @@ export async function startCuaRun(plan, input, admitted, scope) {
|
|
|
312
300
|
/** The lane deps every lane reads: the route, keys, timeouts, scrubber, budget and hooks. */
|
|
313
301
|
function cuaParticipantDeps(plan, input, admitted, run) {
|
|
314
302
|
const { config, dryRun, hooks, streams, env, descriptor, runSession, participantCount } = admitted;
|
|
315
|
-
const {
|
|
303
|
+
const { hasGithubToken, localTreeArchiveBuffer } = admitted;
|
|
316
304
|
const { openaiApiKey, e2bApiKey, scrubKnownValues } = admitted;
|
|
317
305
|
const { externalCommsConfig, externalCommsEmail } = admitted;
|
|
318
306
|
const { appUrl, cloneRoute, desktopCliRoute, localTreeRoute, serve, subjectRepo } = admitted.subjectRoute;
|
|
@@ -327,7 +315,7 @@ function cuaParticipantDeps(plan, input, admitted, run) {
|
|
|
327
315
|
config,
|
|
328
316
|
descriptor,
|
|
329
317
|
appUrl,
|
|
330
|
-
|
|
318
|
+
brain: plan.runner.brain,
|
|
331
319
|
cloneRoute,
|
|
332
320
|
desktopCliRoute,
|
|
333
321
|
localTreeRoute,
|
|
@@ -14,9 +14,8 @@ import type { CuaActorSessionOptions } from "../../actors/computer-use/actor.js"
|
|
|
14
14
|
import { type E2BDesktopModule, type E2BDesktopSandbox } from "../../substrates/e2b/sdk.js";
|
|
15
15
|
import { type DesktopResourceObservation } from "../../substrates/e2b/desktop-resources.js";
|
|
16
16
|
import { type DetachedTimers } from "../../substrates/detached.js";
|
|
17
|
-
import type { ComputerUsePlan } from "../../lab/plan-types.js";
|
|
17
|
+
import type { Brain, ComputerUsePlan } from "../../lab/plan-types.js";
|
|
18
18
|
import { type LabCommsEmail, type LabConfig, type LabSubjectServe } from "../../lab/types.js";
|
|
19
|
-
import { type LocalAgentId } from "../../actors/local-agent/cli.js";
|
|
20
19
|
import { renderObserver, type ObserverResult } from "../../observer/render.js";
|
|
21
20
|
import { type RunLabProvenance } from "../../run/status.js";
|
|
22
21
|
import { type RunBundle, type RunRerunLineage, type RunScorerProvenance, type RunSubjectProvenance, type RunSubjectStateStepRecord } from "../../run/bundle.js";
|
|
@@ -398,8 +397,8 @@ export interface CuaParticipantDeps {
|
|
|
398
397
|
config: LabConfig;
|
|
399
398
|
descriptor: CuaActorDescriptor;
|
|
400
399
|
appUrl: string;
|
|
401
|
-
/**
|
|
402
|
-
|
|
400
|
+
/** The plan's brain: the model and, for a local agent, which signed-in CLI drives the participant. */
|
|
401
|
+
brain: Brain;
|
|
403
402
|
cloneRoute: boolean;
|
|
404
403
|
/** desktop-cli (#495): a CLI studied at a desktop. Nothing is cloned and no browser is opened. */
|
|
405
404
|
desktopCliRoute?: boolean;
|
|
@@ -7,7 +7,6 @@ import {} from "../../substrates/e2b/sdk.js";
|
|
|
7
7
|
import {} from "../../substrates/e2b/desktop-resources.js";
|
|
8
8
|
import {} from "../../substrates/detached.js";
|
|
9
9
|
import {} from "../../lab/types.js";
|
|
10
|
-
import {} from "../../actors/local-agent/cli.js";
|
|
11
10
|
import { renderObserver } from "../../observer/render.js";
|
|
12
11
|
import {} from "../../run/status.js";
|
|
13
12
|
import {} from "../../run/bundle.js";
|
|
@@ -137,7 +137,8 @@ export class ScriptedSubject {
|
|
|
137
137
|
this.killed = reading.released;
|
|
138
138
|
if (reading.warning)
|
|
139
139
|
warnings.push(reading.warning);
|
|
140
|
-
// Without a kill method
|
|
140
|
+
// Without a kill method, or in E2B debug mode, no kill reached E2B, so there is no teardown
|
|
141
|
+
// time to record.
|
|
141
142
|
if (released.status !== "unconfirmed" || released.reason !== "release_unavailable")
|
|
142
143
|
this.tornDownAtMs = now();
|
|
143
144
|
}
|
|
@@ -345,7 +345,8 @@ class SubjectPlane {
|
|
|
345
345
|
this.subjectKilled = reading.released;
|
|
346
346
|
if (reading.warning)
|
|
347
347
|
warnings.push(reading.warning);
|
|
348
|
-
// Without a kill method
|
|
348
|
+
// Without a kill method, or in E2B debug mode, no kill reached E2B, so there is no teardown
|
|
349
|
+
// time to record.
|
|
349
350
|
if (released.status !== "unconfirmed" || released.reason !== "release_unavailable")
|
|
350
351
|
this.subjectTornDownAtMs = this.ctx.now();
|
|
351
352
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Builds each participant's actor spec and mission, resolves its entry URL, derives the session
|
|
2
2
|
// and sandbox time budgets, and records a follower that never received the host's lobby code.
|
|
3
|
-
import {
|
|
3
|
+
import { pricedModel } from "../../lab/plan-base.js";
|
|
4
4
|
import { scrubPersonaBrief } from "../../lab/persona.js";
|
|
5
5
|
import { resolveParticipant } from "../../run/participant.js";
|
|
6
6
|
import { attachObserverRuntimeStreamUrls } from "../../observer/render.js";
|
|
@@ -111,7 +111,7 @@ export function startParticipantFlush(ctx, live, bundle) {
|
|
|
111
111
|
live.flush = startLiveTraceFlush({
|
|
112
112
|
bundle,
|
|
113
113
|
participantRuns: ctx.actorSpecs,
|
|
114
|
-
model: ctx.
|
|
114
|
+
model: pricedModel(ctx.plan.brain),
|
|
115
115
|
write: (snapshot) => ctx.run.writeSnapshot(snapshot),
|
|
116
116
|
});
|
|
117
117
|
}
|
|
@@ -176,7 +176,7 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
|
|
|
176
176
|
scrubKnownValues,
|
|
177
177
|
runSession: ctx.runSession,
|
|
178
178
|
// A local-agent brain runs each seat on the operator's signed-in agent, as on computer use.
|
|
179
|
-
|
|
179
|
+
brain: ctx.plan.brain,
|
|
180
180
|
...(receiving ? { receiving } : {}),
|
|
181
181
|
now: ctx.now,
|
|
182
182
|
hooks: runtimeStreamHooks(ctx.hooks, live),
|
|
@@ -38,6 +38,7 @@ export class LiveTerminalSandbox {
|
|
|
38
38
|
module;
|
|
39
39
|
allocation;
|
|
40
40
|
startupCleanup;
|
|
41
|
+
startupCleanupDetail;
|
|
41
42
|
// The sandbox's billed span (acquired to cleanup) and size price its compute time.
|
|
42
43
|
createdAtMs;
|
|
43
44
|
tornDownAtMs;
|
|
@@ -389,8 +390,10 @@ export class LiveTerminalSandbox {
|
|
|
389
390
|
recordSessionError(error) {
|
|
390
391
|
const { sanitize } = this.inputs;
|
|
391
392
|
const { recordLifecycle } = this.inputs.recorder;
|
|
392
|
-
if (error instanceof E2BDesktopStartupError)
|
|
393
|
+
if (error instanceof E2BDesktopStartupError) {
|
|
393
394
|
this.startupCleanup = error.cleanup;
|
|
395
|
+
this.startupCleanupDetail = error.cleanupDetail;
|
|
396
|
+
}
|
|
394
397
|
this.error = sanitize(toErrorMessage(error));
|
|
395
398
|
this.status = "failed";
|
|
396
399
|
this.completionReason = "harness_error";
|
|
@@ -406,6 +409,7 @@ export class LiveTerminalSandbox {
|
|
|
406
409
|
allocation: this.allocation,
|
|
407
410
|
sandboxModule: this.module,
|
|
408
411
|
...(this.startupCleanup === undefined ? {} : { startupCleanup: this.startupCleanup }),
|
|
412
|
+
startupCleanupDetail: this.startupCleanupDetail,
|
|
409
413
|
requestTimeoutMs,
|
|
410
414
|
sanitize,
|
|
411
415
|
recordLifecycle,
|
|
@@ -17,6 +17,8 @@ export declare function teardownSandbox(args: {
|
|
|
17
17
|
/** For the Sandbox.getInfo(id) re-check only. */
|
|
18
18
|
sandboxModule: E2BDesktopModule | undefined;
|
|
19
19
|
startupCleanup?: E2BDesktopStartupError["cleanup"];
|
|
20
|
+
/** Why the startup guard's cleanup is unconfirmed, when it knows. */
|
|
21
|
+
startupCleanupDetail?: string | undefined;
|
|
20
22
|
requestTimeoutMs: number;
|
|
21
23
|
sanitize: (text: string) => string;
|
|
22
24
|
recordLifecycle: (event: string, message: string) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toErrorMessage } from "../../evidence/redaction.js";
|
|
2
2
|
import { E2BDesktopStartupError, isSandboxNotFoundError, } from "../../substrates/e2b/sdk.js";
|
|
3
|
+
import { releaseUnavailableDetail } from "../../substrates/e2b/sandbox.js";
|
|
3
4
|
/**
|
|
4
5
|
* Tear the sandbox down and PROVE it BY EXACT ID -- NEVER Sandbox.list (humanish must never
|
|
5
6
|
* enumerate the operator's E2B account; see docs/principles/invariants-and-defaults.md). The
|
|
@@ -11,7 +12,7 @@ import { E2BDesktopStartupError, isSandboxNotFoundError, } from "../../substrate
|
|
|
11
12
|
* fails closed on an unproven teardown.
|
|
12
13
|
*/
|
|
13
14
|
export async function teardownSandbox(args) {
|
|
14
|
-
const { allocation, sandboxModule, startupCleanup, requestTimeoutMs, sanitize, recordLifecycle, warnings, } = args;
|
|
15
|
+
const { allocation, sandboxModule, startupCleanup, startupCleanupDetail, requestTimeoutMs, sanitize, recordLifecycle, warnings, } = args;
|
|
15
16
|
if (allocation === undefined || !sandboxModule) {
|
|
16
17
|
// create() can reject AFTER its constructor acquired a handle. The default loader retains
|
|
17
18
|
// that authority and reclaims it before rejecting; the lane itself never receives its ID.
|
|
@@ -21,7 +22,7 @@ export async function teardownSandbox(args) {
|
|
|
21
22
|
return { killed: true, remaining: 0, reason };
|
|
22
23
|
}
|
|
23
24
|
const reason = startupCleanup === "unconfirmed"
|
|
24
|
-
?
|
|
25
|
+
? `desktop startup guard could not confirm cleanup of its acquired sandbox${startupCleanupDetail === undefined ? "" : ` (${startupCleanupDetail})`}; provider timeout remains the backstop`
|
|
25
26
|
: "create did not return a sandbox; the participant has no acquired handle and cannot establish allocation or cleanup";
|
|
26
27
|
recordLifecycle("terminal-lab.cleanup.unconfirmed", reason);
|
|
27
28
|
return { killed: false, remaining: -1, reason };
|
|
@@ -30,11 +31,7 @@ export async function teardownSandbox(args) {
|
|
|
30
31
|
const released = await allocation.close();
|
|
31
32
|
if (released.status !== "released") {
|
|
32
33
|
if (released.status === "unconfirmed" && released.reason === "release_unavailable") {
|
|
33
|
-
return {
|
|
34
|
-
killed: false,
|
|
35
|
-
remaining: -1,
|
|
36
|
-
reason: "installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the sandbox",
|
|
37
|
-
};
|
|
34
|
+
return { killed: false, remaining: -1, reason: releaseUnavailableDetail(released.detail) };
|
|
38
35
|
}
|
|
39
36
|
if (released.status === "unconfirmed" && released.reason === "release_failed") {
|
|
40
37
|
const sanitizedError = sanitize(toErrorMessage(released.error));
|
package/dist/run/reclaim.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface ReclaimResult {
|
|
|
21
21
|
outcomes: ReclaimOutcome[];
|
|
22
22
|
warnings: string[];
|
|
23
23
|
error?: {
|
|
24
|
-
code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
|
|
24
|
+
code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_E2B_DEBUG" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
|
|
25
25
|
message: string;
|
|
26
26
|
};
|
|
27
27
|
}
|
package/dist/run/reclaim.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the managed run dir, kills by id, and never lists the account: an account-wide operation once
|
|
4
4
|
// destroyed unrelated infrastructure. Without it, orphaned sandboxes run until the server-side
|
|
5
5
|
// create-time TTL.
|
|
6
|
-
import { loadE2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
6
|
+
import { E2B_DEBUG_ENV, e2bDebugMode, loadE2BDesktopModule, } from "../substrates/e2b/sdk.js";
|
|
7
7
|
import { containedPathAbsent, readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
|
|
8
8
|
import { discardPreflightJournal, listPreflightJournals, preflightReclaimDecision, } from "./preflight-receipts.js";
|
|
9
9
|
import { resolveRunPath } from "./locate.js";
|
|
@@ -23,6 +23,8 @@ export async function reclaimRunSandboxes(cwd, runInput, hooks = {}) {
|
|
|
23
23
|
outcomes: [],
|
|
24
24
|
warnings,
|
|
25
25
|
};
|
|
26
|
+
if (e2bDebugMode())
|
|
27
|
+
return { ...base, ok: false, error: E2B_DEBUG_REFUSAL };
|
|
26
28
|
const runPaths = await resolveRunPath(cwd, runInput);
|
|
27
29
|
if (!runPaths) {
|
|
28
30
|
return {
|
|
@@ -79,6 +81,8 @@ export async function reclaimPreflightSandboxes(cwd, hooks = {}) {
|
|
|
79
81
|
let receiptCount = 0;
|
|
80
82
|
let unreadable = false;
|
|
81
83
|
const base = { schema: RECLAIM_RESULT_SCHEMA, cwd, runId: "preflight", warnings };
|
|
84
|
+
if (e2bDebugMode())
|
|
85
|
+
return { ...base, ok: false, receiptCount, outcomes, error: E2B_DEBUG_REFUSAL };
|
|
82
86
|
const journals = await listPreflightJournals(cwd);
|
|
83
87
|
if (journals.length === 0) {
|
|
84
88
|
warnings.push("No preflight journals in .humanish/preflight: every probe's teardown was confirmed, or no probe ran here. Nothing to reclaim by id.");
|
|
@@ -120,6 +124,12 @@ export async function reclaimPreflightSandboxes(cwd, hooks = {}) {
|
|
|
120
124
|
}
|
|
121
125
|
return { ...base, ok: !unreadable && allGone(outcomes), receiptCount, outcomes };
|
|
122
126
|
}
|
|
127
|
+
// In debug mode the SDK returns true from Sandbox.kill without contacting E2B, so every receipt
|
|
128
|
+
// would read killed and preflight journals would be discarded for sandboxes that may still run.
|
|
129
|
+
const E2B_DEBUG_REFUSAL = {
|
|
130
|
+
code: "HUMANISH_RECLAIM_E2B_DEBUG",
|
|
131
|
+
message: `${E2B_DEBUG_ENV}=true puts the E2B SDK in debug mode, where Sandbox.kill reports success without contacting E2B. Nothing was killed and the receipts were kept; unset ${E2B_DEBUG_ENV} and run reclaim again.`,
|
|
132
|
+
};
|
|
123
133
|
const UNREADABLE_MESSAGE = "sandbox-receipts.ndjson is present but could not be read safely (not a single regular file, or a read error). Nothing was killed and the receipts were kept; check the file, then run reclaim again.";
|
|
124
134
|
/** Kill every sandbox one receipts journal records, once per sandbox, by exact id. */
|
|
125
135
|
async function reclaimJournal(root, hooks) {
|
|
@@ -10,6 +10,8 @@ export type DesktopReleaseResult = {
|
|
|
10
10
|
status: "unconfirmed";
|
|
11
11
|
reason: "release_unavailable" | "invalid_result" | "release_failed";
|
|
12
12
|
error?: unknown;
|
|
13
|
+
/** Why release is unavailable, when it is not the missing kill method. */
|
|
14
|
+
detail?: string;
|
|
13
15
|
};
|
|
14
16
|
export interface DesktopSession {
|
|
15
17
|
readonly resourceId: string;
|
|
@@ -48,6 +48,8 @@ export declare function readE2BRelease(result: DesktopReleaseResult, options: {
|
|
|
48
48
|
scrub: (text: string) => string;
|
|
49
49
|
costSpan?: boolean;
|
|
50
50
|
}): E2BReleaseReading;
|
|
51
|
+
/** A release_unavailable result as one lowercase line, for routes and reclaim that report it. */
|
|
52
|
+
export declare function releaseUnavailableDetail(detail: string | undefined): string;
|
|
51
53
|
/** Reclaim's persisted outcome; run/reclaim.ts writes these strings. */
|
|
52
54
|
export type E2BSandboxDestroyOutcome = {
|
|
53
55
|
state: "killed" | "already-gone";
|