humanish 0.110.3 → 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 +29 -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/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +7 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study.js +2 -1
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +9 -10
- 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 +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +6 -2
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- 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 +6 -2
- package/dist/routes/shared-world/plan.js +8 -8
- package/dist/routes/shared-world/route.js +3 -3
- 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 +4 -3
- package/dist/routes/terminal/route.js +3 -2
- package/dist/routes/terminal/session.js +1 -0
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run.d.ts +11 -1
- package/dist/run/run.js +24 -5
- package/dist/run/status.d.ts +6 -0
- package/dist/run/status.js +1 -0
- package/dist/run/stored-runs.js +26 -11
- 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 +49 -39
- package/dist/study/config.d.ts +31 -8
- package/dist/study/config.js +181 -155
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +28 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +78 -0
- package/dist/study/migrate/v2.js +607 -0
- package/dist/study/parse/actors.d.ts +34 -10
- package/dist/study/parse/actors.js +76 -174
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +6 -41
- package/dist/study/parse/front.d.ts +36 -0
- package/dist/study/parse/{study-v3.js → front.js} +37 -88
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +7 -8
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +6 -6
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +5 -4
- package/dist/study/preflight-probes.js +4 -4
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +3 -3
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +17 -20
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +19 -0
- package/dist/study/study-fields.js +46 -0
- package/dist/study/summary.js +19 -18
- package/dist/study/types.d.ts +66 -104
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -3
- package/dist/study/validation.d.ts +4 -10
- package/dist/study/validation.js +54 -79
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +42 -110
- package/dist/substrates/local/runtime-config.js +14 -16
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +36 -8
- package/package.json +1 -1
- package/dist/study/parse/study-v3.d.ts +0 -21
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Every URL a study config declares, checked for a credential. Route planners call it, since a
|
|
2
2
|
// library caller's config skips the parser that refuses these first.
|
|
3
|
-
import { rosterOf } from "./parse/actors.js";
|
|
4
3
|
import { entryCredentialReason, urlCredentialReason } from "./parse/url-credentials.js";
|
|
4
|
+
import { participantList } from "./study-fields.js";
|
|
5
5
|
/**
|
|
6
6
|
* The first URL in `config` that carries a credential, with why: the subject's app, serve and
|
|
7
7
|
* public surface URLs, and each participant's target and entry.
|
|
@@ -16,14 +16,14 @@ export function studyUrlCredentialReason(config) {
|
|
|
16
16
|
? undefined
|
|
17
17
|
: urlCredentialReason("subject.serve.url", subject.serve.url),
|
|
18
18
|
...(subject.product?.publicSurfaces ?? []).map((surface) => () => urlCredentialReason("subject.product.publicSurfaces", surface)),
|
|
19
|
-
...
|
|
19
|
+
...(participantList(config) ?? []).flatMap((participant, index) => [
|
|
20
20
|
() => participant.target === undefined
|
|
21
21
|
? undefined
|
|
22
22
|
: urlCredentialReason(`participants[${index}].target`, participant.target),
|
|
23
23
|
() => participant.entry === undefined
|
|
24
24
|
? undefined
|
|
25
25
|
: entryCredentialReason(`participants[${index}].entry`, participant.entry),
|
|
26
|
-
])
|
|
26
|
+
]),
|
|
27
27
|
];
|
|
28
28
|
for (const reason of reasons) {
|
|
29
29
|
const found = reason();
|
|
@@ -7,12 +7,14 @@ import type { StudyConfig } from "./types.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function computerUseValidationReason(config: StudyConfig): string | null;
|
|
9
9
|
/**
|
|
10
|
-
* Cross-validate a `
|
|
10
|
+
* Cross-validate a `route: shared-world` declaration. Returns the failure message, or
|
|
11
11
|
* null when the one shared-world route can run it: the external-public checks for an app-url
|
|
12
12
|
* subject, the provisioned checks otherwise. Enforced at parse and again by the route, since
|
|
13
13
|
* runStudyWith takes a config that skipped the parser.
|
|
14
14
|
*/
|
|
15
15
|
export declare function sharedWorldValidationReason(config: StudyConfig): string | null;
|
|
16
|
+
/** SMTP capture runs only where each participant has its own world, so a shared world refuses it. */
|
|
17
|
+
export declare function smtpValidationReason(config: StudyConfig): string | undefined;
|
|
16
18
|
/**
|
|
17
19
|
* Declared capture devices must be implemented by the selected execution route.
|
|
18
20
|
* Unsupported backends pass false when called directly, where the declared
|
|
@@ -32,14 +34,6 @@ export declare function taskProtocolValidationReason(config: StudyConfig, suppor
|
|
|
32
34
|
* at parse and again on the computer-use and scripted-browser routes for library callers.
|
|
33
35
|
*/
|
|
34
36
|
export declare function cloneTargetValidationReason(config: StudyConfig): string | null;
|
|
35
|
-
/**
|
|
36
|
-
* Refuse a positive `scenario.caps.maxUsd` or `maxTotalUsd` on a computer-use study.
|
|
37
|
-
* `scenario.caps` belongs to the terminal route; the computer-use route stops on `execution.caps`,
|
|
38
|
-
* so a dollar figure here would read as a cap while the study ran uncapped. Zero spends nothing
|
|
39
|
-
* either way and stays a warning. Enforced at parse and again on the computer-use routes for
|
|
40
|
-
* library callers.
|
|
41
|
-
*/
|
|
42
|
-
export declare function scenarioCapsValidationReason(config: StudyConfig): string | null;
|
|
43
37
|
/** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
|
|
44
38
|
export declare function outputTokenLimitValidationReason(config: StudyConfig): string | null;
|
|
45
39
|
/**
|
|
@@ -58,7 +52,7 @@ export declare function concurrentSharedWorldValidationReason(config: StudyConfi
|
|
|
58
52
|
* it rejects every provisioned-subject field (serve/state.seed/state.checkpoint/exposure/clone/repos
|
|
59
53
|
* are inert with no sandbox, so they fail closed), and requires a
|
|
60
54
|
* non-loopback appUrl + allowPublicTargets + the operator-ownership attestation subject.publicTarget +
|
|
61
|
-
* concurrency >= 2 +
|
|
55
|
+
* concurrency >= 2 + a `participants` list of ≥2 with exactly one host participant. The getHost synthetic
|
|
62
56
|
* gate is deliberately unreachable here (there is no internet-reachable harness-owned URL to attest).
|
|
63
57
|
* Enforced at parse and re-enforced in the engine (runStudyWith takes a config that skipped the parser).
|
|
64
58
|
*/
|
package/dist/study/validation.js
CHANGED
|
@@ -1,8 +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, participantList, declaresSharedWorld } from "./study-fields.js";
|
|
6
7
|
/**
|
|
7
8
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
8
9
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
@@ -10,39 +11,32 @@ import { actorResolvesToComputerUse, computerUseParticipantCount, MAX_COMPUTER_U
|
|
|
10
11
|
* validity, unique ids) first, then the route-scoped XOR, cap and policy checks.
|
|
11
12
|
*/
|
|
12
13
|
export function computerUseValidationReason(config) {
|
|
13
|
-
const
|
|
14
|
-
const roster = rosterOf(actor);
|
|
14
|
+
const roster = participantList(config);
|
|
15
15
|
const structuralReason = rosterStructuralValidationReason(config);
|
|
16
16
|
if (structuralReason) {
|
|
17
17
|
return structuralReason;
|
|
18
18
|
}
|
|
19
19
|
// clone.fanout is a declared behavior change: rejected on the computer-use route (was
|
|
20
|
-
// inert-warned). Fan-out is declared
|
|
21
|
-
//
|
|
20
|
+
// inert-warned). Fan-out is declared with `participants`; subject.clone.fanout never applied
|
|
21
|
+
// here.
|
|
22
22
|
if (config.subject.clone?.fanout !== undefined) {
|
|
23
|
-
return "`subject.clone.fanout` is not used on the computer-use route: declare fan-out with
|
|
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.)";
|
|
24
24
|
}
|
|
25
25
|
if (roster !== undefined) {
|
|
26
|
-
if (actor?.count !== undefined) {
|
|
27
|
-
return "Set either `actors[0].count` (identical participants) or `actors[0].lanes` (a roster of distinct participants), not both.";
|
|
28
|
-
}
|
|
29
|
-
if (focusOf(actor) !== undefined) {
|
|
30
|
-
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.";
|
|
31
|
-
}
|
|
32
26
|
if (config.execution?.desktop?.resolution !== undefined &&
|
|
33
27
|
roster.some((entry) => entry.device !== undefined)) {
|
|
34
|
-
return "
|
|
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.";
|
|
35
29
|
}
|
|
36
30
|
const targeted = roster.filter((entry) => entry.target !== undefined);
|
|
37
31
|
if (targeted.length > 0) {
|
|
38
32
|
if (config.subject.source !== "app-url") {
|
|
39
|
-
return "`
|
|
33
|
+
return "`participants[].target` works only on app-url computer-use studies. Clone, shared-world and local-app studies set each participant's entry URL themselves; remove `target`.";
|
|
40
34
|
}
|
|
41
35
|
if (roster.some((entry) => entry.entry !== undefined)) {
|
|
42
|
-
return "
|
|
36
|
+
return "participants[].target and participants[].entry are mutually exclusive: target is an app-url fan-out browser URL; entry is a shared-world same-origin participant path.";
|
|
43
37
|
}
|
|
44
38
|
if (targeted.length !== roster.length) {
|
|
45
|
-
return "When any
|
|
39
|
+
return "When any participants[].target is declared, every participant in the roster must declare target; this keeps the setup-produced target contract explicit and prevents accidental mixed worlds.";
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
42
|
}
|
|
@@ -53,14 +47,14 @@ export function computerUseValidationReason(config) {
|
|
|
53
47
|
// Public targets fan out into N independent worlds driving the same public app, which is an
|
|
54
48
|
// ambiguous shared-world-ish shape, not a per-participant target swarm. Permit N>1 public runs only
|
|
55
49
|
// when every roster entry declares its own target, making the adapter-owned topology explicit. But when
|
|
56
|
-
// `
|
|
50
|
+
// `route: shared-world` is also declared, N participants against one public target is the
|
|
57
51
|
// external-public shared-world topology, so route it there (a real public deployment
|
|
58
52
|
// as the shared plane) instead of refusing; externalPublicSharedWorldValidationReason then applies.
|
|
59
53
|
if (participantCount > 1 &&
|
|
60
54
|
config.policies?.allowPublicTargets === true &&
|
|
61
55
|
declaredTargets(config).length === 0 &&
|
|
62
|
-
config
|
|
63
|
-
return "`policies.allowPublicTargets` with more than one participant sends them all to one public app, which is a shared world. Set `
|
|
56
|
+
!declaresSharedWorld(config)) {
|
|
57
|
+
return "`policies.allowPublicTargets` with more than one participant sends them all to one public app, which is a shared world. Set `route: shared-world` to run them together there, give each `participants[]` entry its own `target`, or run one participant.";
|
|
64
58
|
}
|
|
65
59
|
return null;
|
|
66
60
|
}
|
|
@@ -69,15 +63,15 @@ export function computerUseValidationReason(config) {
|
|
|
69
63
|
* public TypeScript/JavaScript API instead of parseStudy.
|
|
70
64
|
*/
|
|
71
65
|
function rosterStructuralValidationReason(config) {
|
|
72
|
-
const roster =
|
|
66
|
+
const roster = participantList(config);
|
|
73
67
|
const seenIds = new Set();
|
|
74
68
|
if (roster !== undefined) {
|
|
75
69
|
if (!Array.isArray(roster) || roster.length === 0) {
|
|
76
|
-
return "
|
|
70
|
+
return "participants must be a non-empty array when set.";
|
|
77
71
|
}
|
|
78
72
|
for (const [index, entry] of roster.entries()) {
|
|
79
73
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
80
|
-
return `
|
|
74
|
+
return `participants[${index}] must be an object.`;
|
|
81
75
|
}
|
|
82
76
|
const id = entry.id;
|
|
83
77
|
if (id === undefined) {
|
|
@@ -86,10 +80,10 @@ function rosterStructuralValidationReason(config) {
|
|
|
86
80
|
if (typeof id !== "string" ||
|
|
87
81
|
!PARTICIPANT_ID_PATTERN.test(id) ||
|
|
88
82
|
id.length > PARTICIPANT_ID_MAX_CHARS) {
|
|
89
|
-
return `
|
|
83
|
+
return `participants[${index}].id must be a public-safe path token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars.`;
|
|
90
84
|
}
|
|
91
85
|
if (seenIds.has(id)) {
|
|
92
|
-
return `
|
|
86
|
+
return `participants ids must be unique (duplicate "${id}").`;
|
|
93
87
|
}
|
|
94
88
|
seenIds.add(id);
|
|
95
89
|
}
|
|
@@ -97,7 +91,7 @@ function rosterStructuralValidationReason(config) {
|
|
|
97
91
|
return null;
|
|
98
92
|
}
|
|
99
93
|
/**
|
|
100
|
-
* Cross-validate a `
|
|
94
|
+
* Cross-validate a `route: shared-world` declaration. Returns the failure message, or
|
|
101
95
|
* null when the one shared-world route can run it: the external-public checks for an app-url
|
|
102
96
|
* subject, the provisioned checks otherwise. Enforced at parse and again by the route, since
|
|
103
97
|
* runStudyWith takes a config that skipped the parser.
|
|
@@ -109,8 +103,8 @@ export function sharedWorldValidationReason(config) {
|
|
|
109
103
|
}
|
|
110
104
|
/**
|
|
111
105
|
* The structural checks every provisioned shared world shares. It requires: a clone or local-tree source + e2b-desktop
|
|
112
|
-
* target + a computer-use actor + a `subject.serve` block +
|
|
113
|
-
*
|
|
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
|
|
114
108
|
* same-origin (loopback) with serve.url. Fail-closed: a half-declared shared-world is rejected,
|
|
115
109
|
* never silently downgraded.
|
|
116
110
|
*/
|
|
@@ -120,32 +114,38 @@ function provisionedSharedWorldStructureReason(config) {
|
|
|
120
114
|
return structuralReason;
|
|
121
115
|
}
|
|
122
116
|
if (config.subject.source !== "clone" && config.subject.source !== "local-tree") {
|
|
123
|
-
return "`
|
|
117
|
+
return "`route: shared-world` needs `subject.source: clone` or `local-tree`, which humanish serves as one seeded app every participant uses, or `app-url` for a public deployment you own.";
|
|
124
118
|
}
|
|
125
119
|
if (config.execution?.target !== "e2b-desktop") {
|
|
126
|
-
return "`
|
|
120
|
+
return "`route: shared-world` requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against one in-sandbox app.";
|
|
127
121
|
}
|
|
128
|
-
if (!actorResolvesToComputerUse(config
|
|
129
|
-
return `\`
|
|
122
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
123
|
+
return `\`route: shared-world\` requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
|
|
130
124
|
}
|
|
131
125
|
const serve = config.subject.serve;
|
|
132
126
|
if (!serve) {
|
|
133
|
-
return "`
|
|
127
|
+
return "`route: shared-world` needs `subject.serve` (start and url): humanish starts one copy of the app in the sandbox, and every participant uses it.";
|
|
134
128
|
}
|
|
135
|
-
const roster =
|
|
129
|
+
const roster = participantList(config);
|
|
136
130
|
if (!roster || roster.length < 2) {
|
|
137
|
-
return "`
|
|
131
|
+
return "`route: shared-world` needs a `participants` list of at least 2 participants: with one participant there is nobody to interact with.";
|
|
138
132
|
}
|
|
139
133
|
if (!config.subject.state?.checkpoint || config.subject.state.checkpoint.length === 0) {
|
|
140
|
-
return "`
|
|
134
|
+
return "`route: shared-world` needs at least one read-only `subject.state.checkpoint` probe: the checkpoints are how the run shows that participants changed the shared app.";
|
|
141
135
|
}
|
|
142
136
|
for (const entry of roster) {
|
|
143
137
|
if (entry.entry !== undefined && resolveEntryUrl(serve.url, entry.entry) === null) {
|
|
144
|
-
return `
|
|
138
|
+
return `participants role "${entry.id ?? "(unnamed)"}".entry must resolve same-origin (loopback) with subject.serve.url (${serve.url}); got "${entry.entry}".`;
|
|
145
139
|
}
|
|
146
140
|
}
|
|
147
141
|
return null;
|
|
148
142
|
}
|
|
143
|
+
/** SMTP capture runs only where each participant has its own world, so a shared world refuses it. */
|
|
144
|
+
export function smtpValidationReason(config) {
|
|
145
|
+
return config.comms?.email?.smtp && declaresSharedWorld(config)
|
|
146
|
+
? "SMTP capture is not supported yet for shared-world studies. Use the default per-lane-worlds topology for SMTP, or configure supported HTTP email capture for concurrent shared-world studies."
|
|
147
|
+
: undefined;
|
|
148
|
+
}
|
|
149
149
|
/**
|
|
150
150
|
* Declared capture devices must be implemented by the selected execution route.
|
|
151
151
|
* Unsupported backends pass false when called directly, where the declared
|
|
@@ -153,9 +153,7 @@ function provisionedSharedWorldStructureReason(config) {
|
|
|
153
153
|
*/
|
|
154
154
|
export function desktopMediaValidationReason(config, supportsMedia = isComputerUseComposition(config)) {
|
|
155
155
|
if (config.execution?.desktop?.recording !== undefined &&
|
|
156
|
-
(!supportsMedia ||
|
|
157
|
-
config.subject.topology === "shared-world" ||
|
|
158
|
-
config.subject.source === "local-app")) {
|
|
156
|
+
(!supportsMedia || declaresSharedWorld(config) || config.subject.source === "local-app")) {
|
|
159
157
|
return "execution.desktop.recording is supported only for computer-use participants on independent desktops. Remove the declaration or select a supported route.";
|
|
160
158
|
}
|
|
161
159
|
const media = config.execution?.desktop?.media;
|
|
@@ -164,7 +162,7 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
|
|
|
164
162
|
if (media.microphone !== undefined && media.microphone.source !== "speech") {
|
|
165
163
|
return "execution.desktop.media.microphone.source must be speech. Microphone source-file injection is unsupported.";
|
|
166
164
|
}
|
|
167
|
-
if (config
|
|
165
|
+
if (declaresSharedWorld(config)) {
|
|
168
166
|
return "execution.desktop.media is unsupported on shared-world routes; declared capture devices would not be provisioned. Use independent computer-use browser participants or remove the declaration.";
|
|
169
167
|
}
|
|
170
168
|
if (!supportsMedia ||
|
|
@@ -176,8 +174,10 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
|
|
|
176
174
|
return "execution.desktop.media requires Chrome or Chromium; Firefox cannot receive the declared synthetic capture device. Set execution.desktop.browser: chrome or chromium.";
|
|
177
175
|
}
|
|
178
176
|
if (media.microphone !== undefined) {
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
const actor = actorOf(config);
|
|
178
|
+
if (actor !== undefined &&
|
|
179
|
+
(actor.type !== "local-agent" ||
|
|
180
|
+
(actor.localAgent !== undefined && actor.localAgent !== "codex"))) {
|
|
181
181
|
return "Participant speech currently requires local-agent with Codex, on a local or hosted desktop.";
|
|
182
182
|
}
|
|
183
183
|
if (config.execution?.target !== "local" && media.camera !== undefined) {
|
|
@@ -194,7 +194,7 @@ export function receivingEmailValidationReason(config) {
|
|
|
194
194
|
!["app-url", "clone", "local-tree"].includes(config.subject.source)) {
|
|
195
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.";
|
|
196
196
|
}
|
|
197
|
-
if (config
|
|
197
|
+
if (actorOf(config)?.type === "local-agent") {
|
|
198
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.";
|
|
199
199
|
}
|
|
200
200
|
return undefined;
|
|
@@ -202,15 +202,8 @@ export function receivingEmailValidationReason(config) {
|
|
|
202
202
|
/** Refuse task declarations that the selected execution path would discard.
|
|
203
203
|
* Direct runners pass their actual support rather than trusting the config's dispatch shape. */
|
|
204
204
|
export function taskProtocolValidationReason(config, supportsTasks = isComputerUseComposition(config) && !isSharedWorldComposition(config)) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
continue;
|
|
208
|
-
if (index > 0) {
|
|
209
|
-
return `actors[${index}].tasks is unsupported: current runners consume only actors[0]. Use the first actor's computer-use participants for a task protocol.`;
|
|
210
|
-
}
|
|
211
|
-
if (!supportsTasks) {
|
|
212
|
-
return "actors[0].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.";
|
|
213
|
-
}
|
|
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.";
|
|
214
207
|
}
|
|
215
208
|
return null;
|
|
216
209
|
}
|
|
@@ -228,35 +221,17 @@ export function cloneTargetValidationReason(config) {
|
|
|
228
221
|
const got = target === undefined ? "it is absent" : `got "${target}"`;
|
|
229
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.`;
|
|
230
223
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Refuse a positive `scenario.caps.maxUsd` or `maxTotalUsd` on a computer-use study.
|
|
233
|
-
* `scenario.caps` belongs to the terminal route; the computer-use route stops on `execution.caps`,
|
|
234
|
-
* so a dollar figure here would read as a cap while the study ran uncapped. Zero spends nothing
|
|
235
|
-
* either way and stays a warning. Enforced at parse and again on the computer-use routes for
|
|
236
|
-
* library callers.
|
|
237
|
-
*/
|
|
238
|
-
export function scenarioCapsValidationReason(config) {
|
|
239
|
-
if (!isComputerUseComposition(config))
|
|
240
|
-
return null;
|
|
241
|
-
for (const key of ["maxUsd", "maxTotalUsd"]) {
|
|
242
|
-
const value = config.scenario?.caps?.[key];
|
|
243
|
-
if (value === undefined || value <= 0)
|
|
244
|
-
continue;
|
|
245
|
-
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.`;
|
|
246
|
-
}
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
224
|
/** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
|
|
250
225
|
export function outputTokenLimitValidationReason(config) {
|
|
251
|
-
const actor = config
|
|
226
|
+
const actor = actorOf(config);
|
|
252
227
|
if (actor?.maxOutputTokens === undefined)
|
|
253
228
|
return null;
|
|
254
229
|
if (!isMaxOutputTokens(actor.maxOutputTokens))
|
|
255
|
-
return "
|
|
230
|
+
return "actor.maxOutputTokens must be a positive safe integer.";
|
|
256
231
|
if (actor.type !== "openai-computer-use" ||
|
|
257
232
|
!isComputerUseComposition(config) ||
|
|
258
233
|
config.subject.source === "local-app") {
|
|
259
|
-
return "
|
|
234
|
+
return "actor.maxOutputTokens is supported only by first-party OpenAI computer-use routes; terminal, local-agent, scripted and custom in-process routes cannot enforce it.";
|
|
260
235
|
}
|
|
261
236
|
return null;
|
|
262
237
|
}
|
|
@@ -270,7 +245,7 @@ export function outputTokenLimitValidationReason(config) {
|
|
|
270
245
|
function sharedWorldConcurrencyReason(config) {
|
|
271
246
|
// Direct library callers skip the parser, so an omitted value defaults here exactly as the
|
|
272
247
|
// route does: to the participant count. A missing roster reads as 0 and is refused.
|
|
273
|
-
const participants =
|
|
248
|
+
const participants = participantList(config)?.length ?? 0;
|
|
274
249
|
const concurrency = config.execution?.concurrency ?? participants;
|
|
275
250
|
if (concurrency >= 2)
|
|
276
251
|
return null;
|
|
@@ -312,7 +287,7 @@ export function concurrentSharedWorldValidationReason(config) {
|
|
|
312
287
|
* it rejects every provisioned-subject field (serve/state.seed/state.checkpoint/exposure/clone/repos
|
|
313
288
|
* are inert with no sandbox, so they fail closed), and requires a
|
|
314
289
|
* non-loopback appUrl + allowPublicTargets + the operator-ownership attestation subject.publicTarget +
|
|
315
|
-
* concurrency >= 2 +
|
|
290
|
+
* concurrency >= 2 + a `participants` list of ≥2 with exactly one host participant. The getHost synthetic
|
|
316
291
|
* gate is deliberately unreachable here (there is no internet-reachable harness-owned URL to attest).
|
|
317
292
|
* Enforced at parse and re-enforced in the engine (runStudyWith takes a config that skipped the parser).
|
|
318
293
|
*/
|
|
@@ -327,12 +302,12 @@ export function externalPublicSharedWorldValidationReason(config) {
|
|
|
327
302
|
if (config.execution?.target !== "e2b-desktop") {
|
|
328
303
|
return "the external-public shared-world route requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against the one public deployment.";
|
|
329
304
|
}
|
|
330
|
-
if (!actorResolvesToComputerUse(config
|
|
305
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
331
306
|
return `the external-public shared-world route requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
|
|
332
307
|
}
|
|
333
|
-
const roster =
|
|
308
|
+
const roster = participantList(config);
|
|
334
309
|
if (!roster || roster.length < 2) {
|
|
335
|
-
return "the external-public shared-world route requires
|
|
310
|
+
return "the external-public shared-world route requires a `participants` list of at least 2 roles (a single-participant shared world proves no shared session).";
|
|
336
311
|
}
|
|
337
312
|
const sharedWorldConcurrency = sharedWorldConcurrencyReason(config);
|
|
338
313
|
if (sharedWorldConcurrency)
|
|
@@ -367,7 +342,7 @@ export function externalPublicSharedWorldValidationReason(config) {
|
|
|
367
342
|
return "`subject.clone` and `subject.repos` do not apply to an external-public shared-world study: the participants use the public deployment, so nothing is cloned. Remove them.";
|
|
368
343
|
}
|
|
369
344
|
if (roster.some((entry) => entry.entry !== undefined)) {
|
|
370
|
-
return "`
|
|
345
|
+
return "`participants[].entry` (the loopback same-origin participant path) is forbidden on the external-public shared-world route: there is no harness-served serve.url to resolve it against; participants open the public appUrl and reach the shared session through the real UI.";
|
|
371
346
|
}
|
|
372
347
|
const hostEntries = roster.filter((entry) => entry.host === true);
|
|
373
348
|
if (hostEntries.length !== 1) {
|
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[];
|