humanish 0.110.4 → 0.111.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/CONTEXT.md +2 -2
- package/dist/actors/local-agent/claude-session.js +1 -1
- package/dist/actors/local-agent/cli.js +1 -1
- package/dist/actors/scripted-browser/actor.d.ts +2 -2
- package/dist/cli/commands/study.js +2 -4
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +4 -6
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +1 -1
- package/dist/routes/computer-use/route.js +1 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/route.js +1 -1
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +1 -1
- package/dist/routes/shared-world/plan.js +3 -4
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +1 -1
- package/dist/routes/terminal/plan.js +1 -1
- package/dist/routes/terminal/route.js +1 -1
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/results.d.ts +1 -1
- package/dist/run-study.d.ts +5 -3
- package/dist/run-study.js +7 -11
- package/dist/study/adapter-scorer-loader.d.ts +1 -1
- package/dist/study/adapter-scorer-loader.js +3 -3
- package/dist/study/composition-rules.js +21 -11
- package/dist/study/config.d.ts +29 -13
- package/dist/study/config.js +174 -157
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/migrate/convert.js +3 -4
- package/dist/study/migrate/v2.d.ts +17 -7
- package/dist/study/migrate/v2.js +150 -23
- package/dist/study/parse/actors.d.ts +11 -32
- package/dist/study/parse/actors.js +11 -156
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +4 -39
- package/dist/study/parse/{study-v3.d.ts → front.d.ts} +6 -17
- package/dist/study/parse/{study-v3.js → front.js} +22 -91
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.js +7 -9
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +3 -4
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +1 -1
- package/dist/study/preflight-probes.js +2 -2
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +1 -1
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +5 -5
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +12 -21
- package/dist/study/study-fields.js +29 -34
- package/dist/study/summary.js +9 -4
- package/dist/study/types.d.ts +50 -128
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -4
- package/dist/study/validation.d.ts +2 -10
- package/dist/study/validation.js +16 -46
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +40 -109
- package/dist/substrates/local/runtime-config.js +9 -11
- package/docs/contracts/schemas.md +23 -4
- package/package.json +1 -1
- package/dist/study/parse/study.d.ts +0 -38
- package/dist/study/parse/study.js +0 -225
package/dist/study/validation.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isMaxOutputTokens } from "../actors/output-token-limit.js";
|
|
2
|
-
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN
|
|
2
|
+
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors.js";
|
|
3
3
|
import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
|
|
4
4
|
import { declaredTargets } from "./plan-participants.js";
|
|
5
5
|
import { actorResolvesToComputerUse, computerUseParticipantCount, MAX_COMPUTER_USE_PARTICIPANTS, registeredComputerUseActors, resolveEntryUrl, isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
|
|
6
|
-
import { actorOf,
|
|
6
|
+
import { actorOf, participantList, declaresSharedWorld } from "./study-fields.js";
|
|
7
7
|
/**
|
|
8
8
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
9
9
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
@@ -11,25 +11,18 @@ import { actorOf, actorsOf, participantList, declaredParticipantCount, declaresS
|
|
|
11
11
|
* validity, unique ids) first, then the route-scoped XOR, cap and policy checks.
|
|
12
12
|
*/
|
|
13
13
|
export function computerUseValidationReason(config) {
|
|
14
|
-
const actor = actorOf(config);
|
|
15
14
|
const roster = participantList(config);
|
|
16
15
|
const structuralReason = rosterStructuralValidationReason(config);
|
|
17
16
|
if (structuralReason) {
|
|
18
17
|
return structuralReason;
|
|
19
18
|
}
|
|
20
19
|
// clone.fanout is a declared behavior change: rejected on the computer-use route (was
|
|
21
|
-
// inert-warned). Fan-out is declared
|
|
22
|
-
//
|
|
20
|
+
// inert-warned). Fan-out is declared with `participants`; subject.clone.fanout never applied
|
|
21
|
+
// here.
|
|
23
22
|
if (config.subject.clone?.fanout !== undefined) {
|
|
24
23
|
return "`subject.clone.fanout` is not used on the computer-use route: declare fan-out with participants (homogeneous) or participants (a roster of participants). (No current route reads clone.fanout.)";
|
|
25
24
|
}
|
|
26
25
|
if (roster !== undefined) {
|
|
27
|
-
if (declaredParticipantCount(config) !== undefined) {
|
|
28
|
-
return "Set either `actors[0].count` (identical participants) or `actors[0].lanes` (a roster of distinct participants), not both.";
|
|
29
|
-
}
|
|
30
|
-
if (focusOf(actor) !== undefined) {
|
|
31
|
-
return "actors[0].laneFocus and actors[0].lanes are mutually exclusive: each roster entry's `instruction` is the fan-out steer; laneFocus is the steer for a single participant.";
|
|
32
|
-
}
|
|
33
26
|
if (config.execution?.desktop?.resolution !== undefined &&
|
|
34
27
|
roster.some((entry) => entry.device !== undefined)) {
|
|
35
28
|
return "participants[].device and a raw execution.desktop.resolution are mutually exclusive: a per-participant device preset and a single hand-set resolution cannot both govern participant geometry.";
|
|
@@ -54,7 +47,7 @@ export function computerUseValidationReason(config) {
|
|
|
54
47
|
// Public targets fan out into N independent worlds driving the same public app, which is an
|
|
55
48
|
// ambiguous shared-world-ish shape, not a per-participant target swarm. Permit N>1 public runs only
|
|
56
49
|
// when every roster entry declares its own target, making the adapter-owned topology explicit. But when
|
|
57
|
-
// `
|
|
50
|
+
// `route: shared-world` is also declared, N participants against one public target is the
|
|
58
51
|
// external-public shared-world topology, so route it there (a real public deployment
|
|
59
52
|
// as the shared plane) instead of refusing; externalPublicSharedWorldValidationReason then applies.
|
|
60
53
|
if (participantCount > 1 &&
|
|
@@ -98,7 +91,7 @@ function rosterStructuralValidationReason(config) {
|
|
|
98
91
|
return null;
|
|
99
92
|
}
|
|
100
93
|
/**
|
|
101
|
-
* Cross-validate a `
|
|
94
|
+
* Cross-validate a `route: shared-world` declaration. Returns the failure message, or
|
|
102
95
|
* null when the one shared-world route can run it: the external-public checks for an app-url
|
|
103
96
|
* subject, the provisioned checks otherwise. Enforced at parse and again by the route, since
|
|
104
97
|
* runStudyWith takes a config that skipped the parser.
|
|
@@ -110,8 +103,8 @@ export function sharedWorldValidationReason(config) {
|
|
|
110
103
|
}
|
|
111
104
|
/**
|
|
112
105
|
* The structural checks every provisioned shared world shares. It requires: a clone or local-tree source + e2b-desktop
|
|
113
|
-
* target + a computer-use actor + a `subject.serve` block +
|
|
114
|
-
*
|
|
106
|
+
* target + a computer-use actor + a `subject.serve` block + a `participants` list of ≥2 (the list
|
|
107
|
+
* is the role roster; there is no separate roles[] field), and every role `entry` must resolve
|
|
115
108
|
* same-origin (loopback) with serve.url. Fail-closed: a half-declared shared-world is rejected,
|
|
116
109
|
* never silently downgraded.
|
|
117
110
|
*/
|
|
@@ -181,8 +174,10 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
|
|
|
181
174
|
return "execution.desktop.media requires Chrome or Chromium; Firefox cannot receive the declared synthetic capture device. Set execution.desktop.browser: chrome or chromium.";
|
|
182
175
|
}
|
|
183
176
|
if (media.microphone !== undefined) {
|
|
184
|
-
|
|
185
|
-
|
|
177
|
+
const actor = actorOf(config);
|
|
178
|
+
if (actor !== undefined &&
|
|
179
|
+
(actor.type !== "local-agent" ||
|
|
180
|
+
(actor.localAgent !== undefined && actor.localAgent !== "codex"))) {
|
|
186
181
|
return "Participant speech currently requires local-agent with Codex, on a local or hosted desktop.";
|
|
187
182
|
}
|
|
188
183
|
if (config.execution?.target !== "local" && media.camera !== undefined) {
|
|
@@ -199,7 +194,7 @@ export function receivingEmailValidationReason(config) {
|
|
|
199
194
|
!["app-url", "clone", "local-tree"].includes(config.subject.source)) {
|
|
200
195
|
return "Real email receiving requires a hosted computer-use browser study with an app-url, clone, or local-tree subject. Scripted, terminal, desktop-cli and local-app routes are unsupported.";
|
|
201
196
|
}
|
|
202
|
-
if (
|
|
197
|
+
if (actorOf(config)?.type === "local-agent") {
|
|
203
198
|
return "Real email receiving is unavailable for local-agent: its host process does not isolate the inbox management credential. Use a hosted first-party computer-use actor.";
|
|
204
199
|
}
|
|
205
200
|
return undefined;
|
|
@@ -207,15 +202,8 @@ export function receivingEmailValidationReason(config) {
|
|
|
207
202
|
/** Refuse task declarations that the selected execution path would discard.
|
|
208
203
|
* Direct runners pass their actual support rather than trusting the config's dispatch shape. */
|
|
209
204
|
export function taskProtocolValidationReason(config, supportsTasks = isComputerUseComposition(config) && !isSharedWorldComposition(config)) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
continue;
|
|
213
|
-
if (index > 0) {
|
|
214
|
-
return `actors[${index}].tasks is unsupported: current runners consume only actors[0]. Use the first actor's computer-use participants for a task protocol.`;
|
|
215
|
-
}
|
|
216
|
-
if (!supportsTasks) {
|
|
217
|
-
return "actor.tasks is unsupported on this execution path. Task protocols require the computer-use route, with one world per participant; shared-world, terminal-product, scripted-browser and synthetic routes do not consume them. Remove tasks only if a mission-only study is intended.";
|
|
218
|
-
}
|
|
205
|
+
if (actorOf(config)?.tasks !== undefined && !supportsTasks) {
|
|
206
|
+
return "actor.tasks is unsupported on this execution path. Task protocols require the computer-use route, with one world per participant; shared-world, terminal-product, scripted-browser and synthetic routes do not consume them. Remove tasks only if a mission-only study is intended.";
|
|
219
207
|
}
|
|
220
208
|
return null;
|
|
221
209
|
}
|
|
@@ -233,24 +221,6 @@ export function cloneTargetValidationReason(config) {
|
|
|
233
221
|
const got = target === undefined ? "it is absent" : `got "${target}"`;
|
|
234
222
|
return `clone subjects require \`execution.target: e2b-desktop\` (${got}): humanish clones and serves the repo inside a hosted desktop sandbox. \`execution.target: local\` applies to app-url and local-app subjects.`;
|
|
235
223
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Refuse a positive `scenario.caps.maxUsd` or `maxTotalUsd` on a computer-use study.
|
|
238
|
-
* `scenario.caps` belongs to the terminal route; the computer-use route stops on `execution.caps`,
|
|
239
|
-
* so a dollar figure here would read as a cap while the study ran uncapped. Zero spends nothing
|
|
240
|
-
* either way and stays a warning. Enforced at parse and again on the computer-use routes for
|
|
241
|
-
* library callers.
|
|
242
|
-
*/
|
|
243
|
-
export function scenarioCapsValidationReason(config) {
|
|
244
|
-
if (!isComputerUseComposition(config))
|
|
245
|
-
return null;
|
|
246
|
-
for (const key of ["maxUsd", "maxTotalUsd"]) {
|
|
247
|
-
const value = config.scenario?.caps?.[key];
|
|
248
|
-
if (value === undefined || value <= 0)
|
|
249
|
-
continue;
|
|
250
|
-
return `scenario.caps.${key} (${value}) does not cap a computer-use study: this route stops on execution.caps.${key}. Move the value to execution.caps.${key}; scenario.caps applies only to terminal-product studies.`;
|
|
251
|
-
}
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
224
|
/** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
|
|
255
225
|
export function outputTokenLimitValidationReason(config) {
|
|
256
226
|
const actor = actorOf(config);
|
|
@@ -317,7 +287,7 @@ export function concurrentSharedWorldValidationReason(config) {
|
|
|
317
287
|
* it rejects every provisioned-subject field (serve/state.seed/state.checkpoint/exposure/clone/repos
|
|
318
288
|
* are inert with no sandbox, so they fail closed), and requires a
|
|
319
289
|
* non-loopback appUrl + allowPublicTargets + the operator-ownership attestation subject.publicTarget +
|
|
320
|
-
* concurrency >= 2 +
|
|
290
|
+
* concurrency >= 2 + a `participants` list of ≥2 with exactly one host participant. The getHost synthetic
|
|
321
291
|
* gate is deliberately unreachable here (there is no internet-reachable harness-owned URL to attest).
|
|
322
292
|
* Enforced at parse and re-enforced in the engine (runStudyWith takes a config that skipped the parser).
|
|
323
293
|
*/
|
package/dist/study/warnings.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { StudyConfig } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* The fields a config sets that its route does not read, each as `field` or `field (reason)`.
|
|
4
|
-
*
|
|
3
|
+
* The fields a config sets that its route does not read, each as `field` or `field (reason)`.
|
|
4
|
+
* parseStudy refuses a study for any of them.
|
|
5
5
|
*/
|
|
6
6
|
export declare function inertFieldLabels(config: StudyConfig): string[];
|
|
7
7
|
/**
|
|
8
8
|
* The same fields as `inertFieldLabels`, as paths without their reasons, such as
|
|
9
|
-
* `
|
|
9
|
+
* `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
|
|
10
10
|
*/
|
|
11
11
|
export declare function inertFieldPaths(config: StudyConfig): string[];
|
|
12
12
|
export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
|
package/dist/study/warnings.js
CHANGED
|
@@ -1,73 +1,54 @@
|
|
|
1
1
|
import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
|
|
2
2
|
import { declaredParticipantIds } from "./plan-participants.js";
|
|
3
3
|
import { addressedRecipients } from "./parse/comms.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
// compose the agent prompt and bundle provenance); laneFocus.id/label stay inert. On the computer-use
|
|
8
|
-
// E2B route actors[0].lanes is consumed (the fan-out roster).
|
|
4
|
+
import { actorOf, participantList, declaredParticipantCount, scenarioRefOf, } from "./study-fields.js";
|
|
5
|
+
// The computer-use and terminal routes consume mission/persona/model (they compose the agent prompt
|
|
6
|
+
// and bundle provenance).
|
|
9
7
|
const promptRoute = (routes) => routes.cua || routes.terminal;
|
|
10
|
-
// The scripted-browser route consumes persona and
|
|
11
|
-
//
|
|
8
|
+
// The scripted-browser route consumes persona and surfaces (trace/bundle provenance). The prompt
|
|
9
|
+
// fields can never act there: the scripted actor runs no model.
|
|
12
10
|
const scriptedRoute = (routes) => !promptRoute(routes) && routes.scripted;
|
|
13
11
|
const otherRoute = (routes) => !promptRoute(routes) && !routes.scripted;
|
|
14
|
-
/** Rows for
|
|
12
|
+
/** Rows for the actor and its participants, reported first, in this order. */
|
|
15
13
|
const ACTOR_ROWS = [
|
|
16
14
|
// Shared-world-only fields on the roster: a participant's `entry` is inert anywhere else
|
|
17
15
|
// (claims match mechanism).
|
|
18
16
|
{
|
|
19
|
-
field: "
|
|
20
|
-
reason: "the per-participant loopback entry is a shared-world capability; needs
|
|
21
|
-
applies: (
|
|
17
|
+
field: "participants[].entry",
|
|
18
|
+
reason: "the per-participant loopback entry is a shared-world capability; needs route: shared-world",
|
|
19
|
+
applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.entry !== undefined)) &&
|
|
20
|
+
!routes.shared,
|
|
22
21
|
},
|
|
23
22
|
// The host marker acts only on the external-public shared-world route; inert elsewhere.
|
|
24
23
|
{
|
|
25
|
-
field: "
|
|
24
|
+
field: "participants[].host",
|
|
26
25
|
reason: "the designated host-participant marker; needs the external-public shared-world route: app-url × topology shared-world × allowPublicTargets",
|
|
27
|
-
applies: (
|
|
26
|
+
applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.host === true)) &&
|
|
27
|
+
!routes.externalPublic,
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
field: "
|
|
31
|
-
applies: (actor, routes) => promptRoute(routes) && Boolean(focusOf(actor)?.id),
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
field: "laneFocus.label",
|
|
35
|
-
applies: (actor, routes) => promptRoute(routes) && Boolean(focusOf(actor)?.label),
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
field: "lanes",
|
|
39
|
-
reason: "only the computer-use route runs more than one participant",
|
|
40
|
-
applies: (actor, routes) => routes.terminal && Boolean(rosterOf(actor)),
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
field: "mission",
|
|
30
|
+
field: "actor.mission",
|
|
44
31
|
reason: "the scripted-browser actor runs no model",
|
|
45
|
-
applies: (
|
|
32
|
+
applies: (config, routes) => scriptedRoute(routes) && Boolean(actorOf(config)?.mission),
|
|
46
33
|
},
|
|
47
34
|
{
|
|
48
|
-
field: "
|
|
35
|
+
field: "actor.model",
|
|
49
36
|
reason: "the scripted-browser actor runs no model",
|
|
50
|
-
applies: (
|
|
37
|
+
applies: (config, routes) => scriptedRoute(routes) && Boolean(actorOf(config)?.model),
|
|
51
38
|
},
|
|
39
|
+
// On every other route the prompt fields and persona are inert.
|
|
52
40
|
{
|
|
53
|
-
field: "
|
|
54
|
-
|
|
55
|
-
applies: (actor, routes) => scriptedRoute(routes) && Boolean(actor.model),
|
|
41
|
+
field: "actor.mission",
|
|
42
|
+
applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.mission),
|
|
56
43
|
},
|
|
57
44
|
{
|
|
58
|
-
field: "
|
|
59
|
-
|
|
60
|
-
applies: (actor, routes) => scriptedRoute(routes) && Boolean(rosterOf(actor)),
|
|
45
|
+
field: "actor.persona",
|
|
46
|
+
applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.persona),
|
|
61
47
|
},
|
|
62
|
-
// On every other route the prompt fields, persona and the `lanes[]` roster are inert.
|
|
63
|
-
{ field: "mission", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.mission) },
|
|
64
48
|
{
|
|
65
|
-
field: "
|
|
66
|
-
applies: (
|
|
49
|
+
field: "actor.model",
|
|
50
|
+
applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.model),
|
|
67
51
|
},
|
|
68
|
-
{ field: "persona", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.persona) },
|
|
69
|
-
{ field: "model", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.model) },
|
|
70
|
-
{ field: "lanes", applies: (actor, routes) => otherRoute(routes) && Boolean(rosterOf(actor)) },
|
|
71
52
|
];
|
|
72
53
|
const TERMINAL_ONLY = "needs subject.source: terminal-product + a registered terminal actor";
|
|
73
54
|
const RESERVED = "reserved; no route reads it yet";
|
|
@@ -91,17 +72,10 @@ const CONFIG_ROWS = [
|
|
|
91
72
|
field: "subject.state",
|
|
92
73
|
applies: (config, routes) => Boolean(config.subject.state) && !routes.cua && !routes.scripted,
|
|
93
74
|
},
|
|
94
|
-
//
|
|
95
|
-
// (incl. an explicit per-lane-worlds, which the computer-use route already is by mechanism) warns
|
|
96
|
-
// inert.
|
|
97
|
-
{
|
|
98
|
-
field: "subject.topology",
|
|
99
|
-
reason: "drives behavior only on the shared-world route; needs subject.topology: shared-world + clone × e2b-desktop × a computer-use actor + a roster of ≥2 participants",
|
|
100
|
-
applies: (config, routes) => config.subject.topology !== undefined && !routes.shared,
|
|
101
|
-
},
|
|
75
|
+
// The checkpoint acts only on the shared-world route.
|
|
102
76
|
{
|
|
103
77
|
field: "subject.state.checkpoint",
|
|
104
|
-
reason: "the shared-world state-checkpoint probe; needs
|
|
78
|
+
reason: "the shared-world state-checkpoint probe; needs route: shared-world",
|
|
105
79
|
applies: (config, routes) => config.subject.state?.checkpoint !== undefined && !routes.shared,
|
|
106
80
|
},
|
|
107
81
|
// publicTarget (the external-public ownership attestation) acts only on the external-public
|
|
@@ -161,18 +135,10 @@ const CONFIG_ROWS = [
|
|
|
161
135
|
field: "execution.concurrency",
|
|
162
136
|
applies: (config, routes) => config.execution?.concurrency !== undefined && !routes.cua,
|
|
163
137
|
},
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
field: "execution.caps",
|
|
169
|
-
reason: "only the computer-use route enforces it, with a computer-use actor on e2b-desktop",
|
|
170
|
-
applies: (config, routes) => Boolean(config.execution?.caps) && !routes.cua,
|
|
171
|
-
},
|
|
172
|
-
// terminal-product consumes subject.product, scenario.caps, execution.{terminal,runtimeAuth}:
|
|
173
|
-
// dry-run records the contract; live execution enforces caps and command-scoped auth. On every
|
|
174
|
-
// other route they are inert and must warn so a
|
|
175
|
-
// misplaced safety/budget field is never trusted to do something it cannot.
|
|
138
|
+
// parse/front.ts refuses a `caps` key the declared route does not read, so no row lists `caps`.
|
|
139
|
+
// terminal-product consumes subject.product and execution.{terminal,runtimeAuth}: dry-run records
|
|
140
|
+
// the contract; live execution enforces command-scoped auth. On every other route they are inert
|
|
141
|
+
// and must warn so a misplaced safety field is never trusted to do something it cannot.
|
|
176
142
|
{
|
|
177
143
|
field: "subject.product",
|
|
178
144
|
reason: "needs subject.source: terminal-product or desktop-cli with the matching actor",
|
|
@@ -180,18 +146,6 @@ const CONFIG_ROWS = [
|
|
|
180
146
|
!routes.terminal &&
|
|
181
147
|
config.subject.source !== "desktop-cli",
|
|
182
148
|
},
|
|
183
|
-
{
|
|
184
|
-
field: "scenario.caps",
|
|
185
|
-
reason: TERMINAL_ONLY,
|
|
186
|
-
applies: (config, routes) => Boolean(config.scenario?.caps) && !routes.terminal,
|
|
187
|
-
},
|
|
188
|
-
// The study-level budget is a CUA-route capability; the terminal route is a single agent whose
|
|
189
|
-
// maxUsd already caps the whole run, so a maxTotalUsd there would be trusted and unenforced.
|
|
190
|
-
{
|
|
191
|
-
field: "scenario.caps.maxTotalUsd",
|
|
192
|
-
reason: "the study-level budget is a computer-use route capability; the terminal route's maxUsd already caps the whole run",
|
|
193
|
-
applies: (config, routes) => config.scenario?.caps?.maxTotalUsd !== undefined && routes.terminal,
|
|
194
|
-
},
|
|
195
149
|
{
|
|
196
150
|
field: "execution.terminal",
|
|
197
151
|
reason: TERMINAL_ONLY,
|
|
@@ -247,13 +201,12 @@ const CONFIG_ROWS = [
|
|
|
247
201
|
reason: "no current route reads it",
|
|
248
202
|
applies: (config) => config.execution?.desktop?.codexAppServer !== undefined,
|
|
249
203
|
},
|
|
250
|
-
// scenario
|
|
204
|
+
// `scenario` is consumed on the scripted-browser route (required there); forward-declared
|
|
251
205
|
// everywhere else.
|
|
252
206
|
{
|
|
253
|
-
field: "scenario
|
|
207
|
+
field: "scenario",
|
|
254
208
|
applies: (config, routes) => Boolean(scenarioRefOf(config)) && !routes.scripted,
|
|
255
209
|
},
|
|
256
|
-
{ field: "scenario.inline", applies: (config) => Boolean(config.scenario?.inline) },
|
|
257
210
|
// review.{scoring,milestones,vocabulary} stay forward-declared on every route.
|
|
258
211
|
// review.scorer is consumed (loaded + wired, or fail-closed at load) on every scorer-capable
|
|
259
212
|
// route, so it does not warn there; on the scripted-browser route the actor carries no scorer seam,
|
|
@@ -278,7 +231,6 @@ const CONFIG_ROWS = [
|
|
|
278
231
|
reason: "the scripted-browser actor has no adopter-scorer seam; declare it on a terminal / computer-use / shared-world route",
|
|
279
232
|
applies: (config, routes) => Boolean(config.review?.scorer) && routes.scripted,
|
|
280
233
|
},
|
|
281
|
-
{ field: "personas", applies: (config) => Boolean(config.personas) },
|
|
282
234
|
];
|
|
283
235
|
function routesOf(config) {
|
|
284
236
|
const cua = isComputerUseComposition(config);
|
|
@@ -295,44 +247,23 @@ function routesOf(config) {
|
|
|
295
247
|
config.execution?.target === "e2b-desktop",
|
|
296
248
|
};
|
|
297
249
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
: `${prefix}${row.field} (${row.reason})`;
|
|
250
|
+
function inertRows(config) {
|
|
251
|
+
const routes = routesOf(config);
|
|
252
|
+
return [...ACTOR_ROWS, ...CONFIG_ROWS].filter((row) => row.applies(config, routes));
|
|
302
253
|
}
|
|
303
254
|
/**
|
|
304
|
-
* The fields a config sets that its route does not read, each as `field` or `field (reason)`.
|
|
305
|
-
*
|
|
255
|
+
* The fields a config sets that its route does not read, each as `field` or `field (reason)`.
|
|
256
|
+
* parseStudy refuses a study for any of them.
|
|
306
257
|
*/
|
|
307
258
|
export function inertFieldLabels(config) {
|
|
308
|
-
|
|
309
|
-
const inert = [];
|
|
310
|
-
for (const [index, actor] of actorsOf(config).entries()) {
|
|
311
|
-
for (const row of ACTOR_ROWS)
|
|
312
|
-
if (row.applies(actor, routes))
|
|
313
|
-
inert.push(rowLabel(row, `actors[${index}].`));
|
|
314
|
-
}
|
|
315
|
-
for (const row of CONFIG_ROWS)
|
|
316
|
-
if (row.applies(config, routes))
|
|
317
|
-
inert.push(rowLabel(row));
|
|
318
|
-
return inert;
|
|
259
|
+
return inertRows(config).map((row) => row.reason === undefined ? row.field : `${row.field} (${row.reason})`);
|
|
319
260
|
}
|
|
320
261
|
/**
|
|
321
262
|
* The same fields as `inertFieldLabels`, as paths without their reasons, such as
|
|
322
|
-
* `
|
|
263
|
+
* `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
|
|
323
264
|
*/
|
|
324
265
|
export function inertFieldPaths(config) {
|
|
325
|
-
|
|
326
|
-
const paths = [];
|
|
327
|
-
for (const [index, actor] of actorsOf(config).entries()) {
|
|
328
|
-
for (const row of ACTOR_ROWS)
|
|
329
|
-
if (row.applies(actor, routes))
|
|
330
|
-
paths.push(`actors[${index}].${row.field}`);
|
|
331
|
-
}
|
|
332
|
-
for (const row of CONFIG_ROWS)
|
|
333
|
-
if (row.applies(config, routes))
|
|
334
|
-
paths.push(row.field);
|
|
335
|
-
return paths;
|
|
266
|
+
return inertRows(config).map((row) => row.field);
|
|
336
267
|
}
|
|
337
268
|
// Report fields that are present but not yet consumed by the engine, so a user never trusts a
|
|
338
269
|
// setting that silently does nothing.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { actorOf,
|
|
1
|
+
import { actorOf, capsOf, participantList } from "../../study/study-fields.js";
|
|
2
2
|
// The runtime image enforces a 30-minute lifetime; reserve setup/teardown room.
|
|
3
3
|
export const LOCAL_BROWSER_LIFETIME_MS = 30 * 60_000;
|
|
4
4
|
const MAX_SESSION_MS = 20 * 60_000;
|
|
@@ -9,15 +9,16 @@ export function isLocalBrowserStudy(config) {
|
|
|
9
9
|
}
|
|
10
10
|
/** Defaults for the explicitly selected local substrate; hosted configurations are untouched. */
|
|
11
11
|
export function localBrowserDefaults(config) {
|
|
12
|
-
|
|
12
|
+
const actor = actorOf(config);
|
|
13
|
+
if (!isLocalBrowserStudy(config) || actor === undefined)
|
|
13
14
|
return config;
|
|
14
15
|
return {
|
|
15
16
|
...config,
|
|
16
|
-
|
|
17
|
+
actor: {
|
|
17
18
|
...actor,
|
|
18
19
|
model: actor.model ?? "gpt-6-astra",
|
|
19
20
|
reasoningEffort: actor.reasoningEffort ?? "low",
|
|
20
|
-
}
|
|
21
|
+
},
|
|
21
22
|
execution: {
|
|
22
23
|
...config.execution,
|
|
23
24
|
timeoutMs: config.execution?.timeoutMs ?? MAX_SESSION_MS,
|
|
@@ -26,7 +27,7 @@ export function localBrowserDefaults(config) {
|
|
|
26
27
|
resolution: config.execution?.desktop?.resolution ?? [960, 720],
|
|
27
28
|
},
|
|
28
29
|
},
|
|
29
|
-
...(config.review?.analysis === undefined &&
|
|
30
|
+
...(config.review?.analysis === undefined && actor.type === "local-agent"
|
|
30
31
|
? { review: { ...config.review, analysis: { provider: "codex" } } }
|
|
31
32
|
: {}),
|
|
32
33
|
};
|
|
@@ -37,8 +38,7 @@ export function localBrowserUnsupportedReason(config) {
|
|
|
37
38
|
if ((config.execution?.timeoutMs ?? MAX_SESSION_MS) > MAX_SESSION_MS) {
|
|
38
39
|
return "Local browser sessions currently support at most 20 minutes, within the runtime's 30-minute lifetime.";
|
|
39
40
|
}
|
|
40
|
-
if (
|
|
41
|
-
!actor ||
|
|
41
|
+
if (!actor ||
|
|
42
42
|
(actor.type === "local-agent" && actor.localAgent !== undefined && actor.localAgent !== "codex")) {
|
|
43
43
|
return "Local browser studies support only Codex local-agent or openai-computer-use participants.";
|
|
44
44
|
}
|
|
@@ -63,10 +63,8 @@ export function localBrowserUnsupportedReason(config) {
|
|
|
63
63
|
actor.reasoningEffort !== "low" ||
|
|
64
64
|
participantList(config)?.some((entry) => entry.reasoningEffort !== undefined && entry.reasoningEffort !== "low") ||
|
|
65
65
|
actor.maxOutputTokens !== undefined ||
|
|
66
|
-
config
|
|
67
|
-
config
|
|
68
|
-
config.scenario?.caps?.maxUsd !== undefined ||
|
|
69
|
-
config.scenario?.caps?.maxTotalUsd !== undefined)) {
|
|
66
|
+
capsOf(config)?.maxUsd !== undefined ||
|
|
67
|
+
capsOf(config)?.maxTotalUsd !== undefined)) {
|
|
70
68
|
return "Local Codex participants currently use gpt-6-astra at low effort. Account dollar/output-token caps are unavailable; use API participants for those controls.";
|
|
71
69
|
}
|
|
72
70
|
for (const target of [
|
|
@@ -59,10 +59,19 @@ workflow without leaking private upstream truth into core.
|
|
|
59
59
|
|
|
60
60
|
## Study File
|
|
61
61
|
|
|
62
|
-
Schema: `humanish.study.v3`. `src/study/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`humanish migrate <path>`; migrate
|
|
62
|
+
Schema: `humanish.study.v3`. `src/study/config.ts` parses a v3 document into `StudyConfig`
|
|
63
|
+
(`src/study/types.ts`), which has the file's keys; `src/study/parse/front.ts` checks the route and
|
|
64
|
+
the forms of `participants`, `surfaces` and `caps` it takes. humanish refuses a `humanish.lab.v2`
|
|
65
|
+
file with `HUMANISH_STUDY_V2_UNSUPPORTED`, whose message names `humanish migrate <path>`; migrate
|
|
66
|
+
converts the file to v3.
|
|
67
|
+
|
|
68
|
+
The parsed config is the file with defaults filled: a `participants` entry with a `count` expanded
|
|
69
|
+
into its participants, `comms.email.recipients` and a shared world's `execution.concurrency`
|
|
70
|
+
filled, `kind: real` added beside an email `connection`, and a local browser study's model,
|
|
71
|
+
effort, timeout and resolution. `humanish study show --json` prints that config, and `parseStudy`
|
|
72
|
+
reads it back to the same config. Before 0.111.0, `StudyConfig` had the `humanish.lab.v2` keys
|
|
73
|
+
(`actors`, `execution.caps`, the `scenario` object, `subject.topology`); the 0.111.0 release notes
|
|
74
|
+
map each one to its v3 key.
|
|
66
75
|
|
|
67
76
|
A study declares its route and composes code primitives; it is not a hardcoded kind. The
|
|
68
77
|
top-level keys:
|
|
@@ -1552,6 +1561,16 @@ Refusals:
|
|
|
1552
1561
|
a preview or scripted study, `createProvider` off computer use, or `prepareDesktop` where no E2B
|
|
1553
1562
|
desktop exists. The message names the option and the route. The same code refuses a field
|
|
1554
1563
|
`RunStudyOptions` no longer has (below); that message names the field and where its job went.
|
|
1564
|
+
- `HUMANISH_STUDY_V2_UNSUPPORTED`: the config is `humanish.lab.v2`, or it still sets a
|
|
1565
|
+
`StudyConfig` field of humanish 0.110 that 0.111.0 renamed: `actors`, a `scenario` object,
|
|
1566
|
+
`execution.caps`, `subject.topology`, `personas`, or `count`, `lanes`, `roster` or `laneFocus` on
|
|
1567
|
+
`actor`. The message names each one's v3 field. The
|
|
1568
|
+
planners read only the v3 fields, so a budget left in `execution.caps` would otherwise run
|
|
1569
|
+
uncapped.
|
|
1570
|
+
- `HUMANISH_STUDY_INVALID`: the config has no `actor` object, or its `route` is missing or is not
|
|
1571
|
+
the route its subject and actor take. The message is the one `parseStudy` gives.
|
|
1572
|
+
|
|
1573
|
+
`runStudy` checks the config before the options, so a config refusal comes first.
|
|
1555
1574
|
|
|
1556
1575
|
`rerun.laneIds`, the older name of `rerun.participantIds`, was removed in 0.107.0. A caller that
|
|
1557
1576
|
still passes it is refused with `HUMANISH_STUDY_OPTION_UNSUPPORTED`; use `rerun.participantIds`.
|
package/package.json
CHANGED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { type StudyConfig, type StudyParseFailure, type StudyScenarioCaps, type StudyV3 } from "../types.js";
|
|
2
|
-
interface StudyV3Options {
|
|
3
|
-
/**
|
|
4
|
-
* `report`, for `humanish migrate`: list the fields the route does not read in `inert`, as v2
|
|
5
|
-
* paths, in place of refusing the study for them, and give the checks' messages as they wrote
|
|
6
|
-
* them, in the v2 spelling of the file migrate converts. A shared world may then give
|
|
7
|
-
* `participants` as a count or leave it out, as a v2 file could (readStudyFront).
|
|
8
|
-
*/
|
|
9
|
-
readonly inert?: "refuse" | "report";
|
|
10
|
-
}
|
|
11
|
-
type StudyV3ParseResult = {
|
|
12
|
-
ok: true;
|
|
13
|
-
config: StudyV3;
|
|
14
|
-
warnings: string[];
|
|
15
|
-
inert?: string[];
|
|
16
|
-
} | StudyParseFailure;
|
|
17
|
-
type Parsed<T> = {
|
|
18
|
-
ok: true;
|
|
19
|
-
value: T;
|
|
20
|
-
} | StudyParseFailure;
|
|
21
|
-
/**
|
|
22
|
-
* Validate a parsed humanish.study.v3 document into a StudyV3, with the messages parseStudy gives.
|
|
23
|
-
* Pure: the caller owns file IO. parseStudy refuses a humanish.lab.v2 document before this runs.
|
|
24
|
-
*/
|
|
25
|
-
export declare function parseStudyV3(raw: unknown, options?: StudyV3Options): StudyV3ParseResult;
|
|
26
|
-
/**
|
|
27
|
-
* A v3 study in the v2 spelling: the actor holds its participants, `caps` moves under `execution`
|
|
28
|
-
* or `scenario`, and a shared world is `subject.topology: shared-world`. The checks that read more
|
|
29
|
-
* than one section run on this view until StudyConfig has the v3 shape.
|
|
30
|
-
*/
|
|
31
|
-
export declare function toLegacy(study: StudyV3): StudyConfig;
|
|
32
|
-
/**
|
|
33
|
-
* A caps block's values. A value that is not a non-negative number refuses the study: a cap that
|
|
34
|
-
* does nothing must not look like one that holds. readStudyFront has checked the keys against the
|
|
35
|
-
* route; migrate checks a v2 file's two blocks with it.
|
|
36
|
-
*/
|
|
37
|
-
export declare function parseCaps(raw: unknown): Parsed<StudyScenarioCaps | undefined>;
|
|
38
|
-
export {};
|