humanish 0.111.2 → 0.111.3

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.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/dist/cli/commands/study-route-computer-use.js +3 -8
  4. package/dist/cli/commands/study-route-preview.js +4 -6
  5. package/dist/cli/commands/study-route-shared-world.js +6 -7
  6. package/dist/evidence/redaction.d.ts +2 -2
  7. package/dist/evidence/redaction.js +3 -3
  8. package/dist/routes/computer-use/bundle-parts.d.ts +1 -32
  9. package/dist/routes/computer-use/bundle-parts.js +5 -92
  10. package/dist/routes/computer-use/e2b-desktop/prepare.js +11 -4
  11. package/dist/routes/computer-use/fanout-bundle.js +3 -2
  12. package/dist/routes/computer-use/live-phase.js +3 -7
  13. package/dist/routes/computer-use/participant-deps.d.ts +46 -0
  14. package/dist/routes/computer-use/participant-deps.js +48 -0
  15. package/dist/routes/computer-use/participant-facts.d.ts +5 -4
  16. package/dist/routes/computer-use/participant-facts.js +37 -2
  17. package/dist/routes/computer-use/participant-feedback.d.ts +33 -0
  18. package/dist/routes/computer-use/participant-feedback.js +92 -0
  19. package/dist/routes/computer-use/participant-model.d.ts +11 -0
  20. package/dist/routes/computer-use/participant-model.js +15 -0
  21. package/dist/routes/computer-use/preflight.js +33 -65
  22. package/dist/routes/computer-use/result.js +13 -52
  23. package/dist/routes/computer-use/route.js +6 -7
  24. package/dist/routes/computer-use/setup.d.ts +2 -6
  25. package/dist/routes/computer-use/setup.js +47 -93
  26. package/dist/routes/computer-use/single-bundle.js +3 -2
  27. package/dist/routes/computer-use/types.d.ts +2 -2
  28. package/dist/routes/preview.js +2 -8
  29. package/dist/routes/scripted/bundle.d.ts +2 -6
  30. package/dist/routes/scripted/result.d.ts +2 -1
  31. package/dist/routes/scripted/result.js +4 -4
  32. package/dist/routes/scripted/route.js +13 -38
  33. package/dist/routes/scripted/setup.d.ts +2 -1
  34. package/dist/routes/scripted/setup.js +21 -28
  35. package/dist/routes/scripted/subject.d.ts +3 -18
  36. package/dist/routes/scripted/subject.js +22 -78
  37. package/dist/routes/scripted/types.d.ts +2 -2
  38. package/dist/routes/shared-world/bundle.js +36 -1
  39. package/dist/routes/shared-world/comms.d.ts +2 -1
  40. package/dist/routes/shared-world/comms.js +3 -7
  41. package/dist/routes/shared-world/handoff.d.ts +2 -2
  42. package/dist/routes/shared-world/handoff.js +1 -1
  43. package/dist/routes/shared-world/participant-specs.d.ts +4 -5
  44. package/dist/routes/shared-world/participant-specs.js +19 -25
  45. package/dist/routes/shared-world/plan.js +11 -7
  46. package/dist/routes/shared-world/provisioned.d.ts +2 -1
  47. package/dist/routes/shared-world/provisioned.js +37 -80
  48. package/dist/routes/shared-world/result.js +17 -35
  49. package/dist/routes/shared-world/route.js +33 -36
  50. package/dist/routes/shared-world/setup.d.ts +2 -3
  51. package/dist/routes/shared-world/setup.js +11 -29
  52. package/dist/routes/shared-world/types.d.ts +6 -15
  53. package/dist/routes/terminal/dry-run.js +14 -24
  54. package/dist/routes/terminal/live-finish.d.ts +0 -1
  55. package/dist/routes/terminal/live-finish.js +2 -2
  56. package/dist/routes/terminal/route.js +9 -15
  57. package/dist/routes/terminal/session.d.ts +2 -3
  58. package/dist/routes/terminal/session.js +28 -40
  59. package/dist/routes/terminal/trace.d.ts +1 -1
  60. package/dist/routes/terminal/types.d.ts +2 -2
  61. package/dist/run/run.d.ts +55 -1
  62. package/dist/run/run.js +38 -2
  63. package/dist/run/secrets.d.ts +22 -0
  64. package/dist/run/secrets.js +33 -0
  65. package/dist/run/study-result.d.ts +34 -0
  66. package/dist/run/study-result.js +16 -0
  67. package/dist/study/requirements.d.ts +60 -5
  68. package/dist/study/requirements.js +88 -2
  69. package/dist/study/run-study-events.d.ts +4 -3
  70. package/dist/study/run-study-events.js +9 -14
  71. package/dist/study/run-study-options.d.ts +4 -1
  72. package/dist/study/run-study-options.js +22 -32
  73. package/dist/substrates/e2b/subject-sandbox.d.ts +58 -0
  74. package/dist/substrates/e2b/subject-sandbox.js +94 -0
  75. package/docs/contracts/core.md +5 -4
  76. package/docs/contracts/feedback.md +12 -5
  77. package/package.json +1 -1
@@ -12,9 +12,9 @@ import { provisionLocalTreeSubject } from "../../subject/local-tree.js";
12
12
  import { defaultSharedWorldPhaseSink } from "../../subject/steps.js";
13
13
  import { phaseEvent } from "../../study/run-study-events.js";
14
14
  import { loadE2BDesktopModule } from "../../substrates/e2b/sdk.js";
15
- import { observeDesktopResources, } from "../../substrates/e2b/desktop-resources.js";
16
- import { acquireE2BDesktopSandbox, readE2BRelease } from "../../substrates/e2b/sandbox.js";
15
+ import { E2BSubjectSandbox, } from "../../substrates/e2b/subject-sandbox.js";
17
16
  import { e2bShell } from "../../substrates/e2b/shell.js";
17
+ import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
18
18
  import { defaultPackLocalTree } from "../computer-use/local-tree-pack.js";
19
19
  import { inboxRecipientFor, participantHasInboxRecipient, } from "../computer-use/participant-desktop.js";
20
20
  import { resolveSubjectState } from "../computer-use/subject-projection.js";
@@ -25,8 +25,6 @@ import { buildConcurrentSharedWorldBundle, judgeSharedWorldRun } from "./bundle.
25
25
  import { runCheckpointSnapshot } from "./checkpoints.js";
26
26
  import { drainSubjectComms } from "./comms.js";
27
27
  import { buildSubjectProvenance, hostOriginDigest, isTokenlessHost, servePort, } from "./provenance.js";
28
- import { SANDBOX_TIMEOUT_BUFFER_MS, SUBJECT_PROVISION_BUDGET_MS, } from "../../substrates/e2b/lifetime.js";
29
- import { DEFAULT_STATE_STEP_TIMEOUT_MS } from "../../subject/state.js";
30
28
  import { resolveActorEntryUrl, participantRunDeps, startParticipantFlush, } from "./participant-specs.js";
31
29
  import { CONCURRENT_SHARED_WORLD_PROVIDER_METADATA, } from "./types.js";
32
30
  import { addressedRecipients } from "../../study/parse/comms.js";
@@ -46,17 +44,10 @@ class ObserverGateError extends Error {
46
44
  class SubjectPlane {
47
45
  commsInboxUrl;
48
46
  subjectCommit;
49
- subjectSandboxId;
50
- subjectKilled = false;
51
- subjectReleaseWarning;
47
+ subject;
52
48
  getHostUrl;
53
- // The subject desktop's host-side span and size, priced in the run's cost estimate.
54
- subjectCreatedAtMs;
55
- subjectTornDownAtMs;
56
- subjectResources;
57
49
  ctx;
58
50
  setup;
59
- subjectAllocation;
60
51
  subjectDesktop;
61
52
  subjectShell;
62
53
  // The in-sandbox email catch on the one subject sandbox; drained at teardown. Undefined
@@ -77,6 +68,11 @@ class SubjectPlane {
77
68
  constructor(ctx, setup) {
78
69
  this.ctx = ctx;
79
70
  this.setup = setup;
71
+ this.subject = new E2BSubjectSandbox({
72
+ warnings: ctx.warnings,
73
+ scrub: ctx.scrubKnownValues,
74
+ now: ctx.now,
75
+ });
80
76
  this.disposeSignal = new Promise((resolve) => {
81
77
  this.releaseDispose = resolve;
82
78
  });
@@ -106,46 +102,35 @@ class SubjectPlane {
106
102
  // The one subject sandbox: headless service host (no GUI participant). The subject env is
107
103
  // provisioned here; the actor sandboxes get none of it. A custom desktop template (image) is
108
104
  // honored on both the subject sandbox (here) and every actor sandbox (via runCuaParticipant, which
109
- // reads the same config); absent keeps the byte-stable Sandbox.create(opts) default. The
110
- // receipt is on disk before any work, so `humanish reclaim` can kill it by exact id.
111
- const subject = await acquireE2BDesktopSandbox({
105
+ // reads the same config); absent keeps the byte-stable Sandbox.create(opts) default.
106
+ this.subjectDesktop = await this.subject.acquire({
112
107
  module: subjectModule,
113
- options: {
114
- apiKey: this.ctx.e2bApiKey,
115
- requestTimeoutMs,
116
- timeoutMs: timeoutMs +
117
- SUBJECT_PROVISION_BUDGET_MS +
118
- (planeStateOf(plan)?.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0) +
119
- SANDBOX_TIMEOUT_BUFFER_MS,
120
- metadata: {
121
- ...CONCURRENT_SHARED_WORLD_PROVIDER_METADATA,
122
- labId: plan.studyId,
123
- topology: "shared-world",
124
- topologyMode: "concurrent",
125
- kind: "subject",
126
- participantCount: String(plan.plane.participants.length),
127
- },
128
- ...(subjectEnvNames.length > 0 || Object.keys(commsEnv).length > 0
129
- ? {
130
- envs: {
131
- ...Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])),
132
- ...commsEnv,
133
- },
134
- }
135
- : {}),
136
- dpi: 96,
137
- lifecycle: { onTimeout: "kill" },
108
+ apiKey: this.ctx.e2bApiKey,
109
+ requestTimeoutMs,
110
+ sessionTimeoutMs: timeoutMs,
111
+ seed: planeStateOf(plan)?.seed ?? [],
112
+ metadata: {
113
+ ...CONCURRENT_SHARED_WORLD_PROVIDER_METADATA,
114
+ labId: plan.studyId,
115
+ topology: "shared-world",
116
+ topologyMode: "concurrent",
117
+ kind: "subject",
118
+ participantCount: String(plan.plane.participants.length),
119
+ },
120
+ envs: {
121
+ ...Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])),
122
+ ...commsEnv,
138
123
  },
139
124
  template: plan.residual.execution?.desktop?.template,
140
- receipt: { root: this.ctx.runPaths, participantId: "subject" },
125
+ root: this.ctx.runPaths,
141
126
  });
142
- this.subjectDesktop = subject.sandbox;
143
127
  this.subjectShell = e2bShell(this.subjectDesktop);
144
- this.subjectAllocation = subject.allocation;
145
- this.subjectSandboxId = subject.allocation.resourceId;
146
- this.subjectCreatedAtMs = this.ctx.now();
147
- this.subjectResources = await observeDesktopResources(this.subjectDesktop);
148
- await this.ctx.input.prepareDesktop?.(this.subjectDesktop, { kind: "subject" });
128
+ if (this.ctx.input.prepareDesktop) {
129
+ await this.ctx.input.prepareDesktop(this.subjectDesktop, { kind: "subject" });
130
+ // The caller's hook runs with this process's file access, so the run directory is checked
131
+ // again before more evidence goes into it.
132
+ await validatePreparedRunArtifactPaths(this.ctx.runPaths);
133
+ }
149
134
  }
150
135
  // Start the in-sandbox email catch before the subject serve, so the app's send-API base URL
151
136
  // (injected into its env at create) resolves the moment it boots. Fail closed if the catch can't
@@ -320,7 +305,6 @@ class SubjectPlane {
320
305
  * Returns the comms thread's path when the drain wrote one.
321
306
  */
322
307
  async teardown() {
323
- const { warnings, scrubKnownValues } = this.ctx;
324
308
  this.proberDisposed = true;
325
309
  this.releaseDispose();
326
310
  if (this.proberLoop) {
@@ -339,36 +323,9 @@ class SubjectPlane {
339
323
  if (commsEmail && this.deployedComms?.ready && this.subjectShell) {
340
324
  commsArtifactPath = await drainSubjectComms(this.ctx, commsEmail, this.subjectShell, this.deployedComms);
341
325
  }
342
- if (this.subjectAllocation !== undefined) {
343
- const released = await this.subjectAllocation.close();
344
- const reading = readE2BRelease(released, {
345
- label: "Subject sandbox",
346
- scrub: scrubKnownValues,
347
- costSpan: true,
348
- });
349
- this.subjectKilled = reading.released;
350
- if (reading.warning)
351
- warnings.push(reading.warning);
352
- if (!reading.released)
353
- this.subjectReleaseWarning = reading.warning ?? "Subject sandbox release is unconfirmed.";
354
- // Without a kill method, or in E2B debug mode, no kill reached E2B, so there is no teardown
355
- // time to record.
356
- if (released.status !== "unconfirmed" || released.reason !== "release_unavailable")
357
- this.subjectTornDownAtMs = this.ctx.now();
358
- }
326
+ await this.subject.release();
359
327
  return commsArtifactPath;
360
328
  }
361
- desktopUsage() {
362
- if (this.subjectSandboxId === undefined)
363
- return undefined;
364
- return {
365
- durationMs: this.subjectCreatedAtMs === undefined || this.subjectTornDownAtMs === undefined
366
- ? undefined
367
- : Math.max(0, this.subjectTornDownAtMs - this.subjectCreatedAtMs),
368
- observation: this.subjectResources,
369
- killed: this.subjectKilled,
370
- };
371
- }
372
329
  }
373
330
  /** Writes the in-progress bundle, runs the caller's Observer gate and starts the participants'
374
331
  * flush. */
@@ -476,10 +433,10 @@ export async function runProvisionedPlane(ctx, live, setup) {
476
433
  actorResults,
477
434
  runError,
478
435
  subjectCommit: plane.subjectCommit,
479
- subjectSandboxId: plane.subjectSandboxId,
480
- subjectKilled: plane.subjectKilled,
481
- subjectReleaseWarning: plane.subjectReleaseWarning,
482
- subjectDesktop: plane.desktopUsage(),
436
+ subjectSandboxId: plane.subject.sandboxId,
437
+ subjectKilled: plane.subject.killed,
438
+ subjectReleaseWarning: plane.subject.releaseWarning,
439
+ subjectDesktop: plane.subject.desktopUsage(),
483
440
  getHostUrl: plane.getHostUrl,
484
441
  commsArtifactPath,
485
442
  };
@@ -3,14 +3,16 @@
3
3
  import { redactText } from "../../evidence/redaction.js";
4
4
  import { adapterScoreFailureMessage, applyBrowserScorer } from "../../study/adapter-extension.js";
5
5
  import { attachObserverRuntimeStreamUrls } from "../../observer/render.js";
6
- import { foldScorerFailures, judgeExecution, sandboxCleanupFailure, OUTCOME_POLICIES, participantHarnessFailed, resultOk, sharedWorldShortfall, } from "../../run/judge.js";
7
- import { participantFactsOf, unreleasedSandboxFailures, } from "../computer-use/participant-facts.js";
6
+ import { foldScorerFailures, judgeExecution, sandboxCleanupFailure, OUTCOME_POLICIES, resultOk, sharedWorldShortfall, } from "../../run/judge.js";
7
+ import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
8
+ import { participantExecutionFailures } from "../computer-use/participant-facts.js";
9
+ import { participantCapWarning } from "../computer-use/participant-model.js";
8
10
  import { resolveSubjectState } from "../computer-use/subject-projection.js";
9
11
  import { actorRunPassed, buildConcurrentSharedWorldBundle, judgeSharedWorldRun, maxSimultaneousWindows, } from "./bundle.js";
10
12
  import { planeStateOf } from "./plan.js";
11
13
  import { buildSubjectProvenance, hostOriginDigest } from "./provenance.js";
12
14
  import {} from "./types.js";
13
- import { studyResultIdentity } from "../../run/study-result.js";
15
+ import { refusedResult, studyResultIdentity } from "../../run/study-result.js";
14
16
  import { plural } from "../../run/text.js";
15
17
  /** The results of a run whose plane did not run (a dry run) or has not reported yet. */
16
18
  export function emptyPlaneResults() {
@@ -143,10 +145,7 @@ function concurrentStudyError(args) {
143
145
  * and the run id the caller asked for, if any. `actor` is the label when a refusal names none.
144
146
  */
145
147
  export function concurrentStudyFailure(envelope) {
146
- return (code, message, actorLabel) => ({
147
- ...studyResultIdentity("shared-world", envelope.studyId),
148
- ok: false,
149
- cwd: envelope.cwd,
148
+ return (code, message, actorLabel) => refusedResult("shared-world", { studyId: envelope.studyId, cwd: envelope.cwd, error: { code, message } }, {
150
149
  actor: actorLabel ?? envelope.actor,
151
150
  topology: "shared-world",
152
151
  topologyMode: "concurrent",
@@ -155,43 +154,18 @@ export function concurrentStudyFailure(envelope) {
155
154
  dryRun: envelope.dryRun,
156
155
  runId: envelope.runId ?? "not-created",
157
156
  roles: [],
158
- warnings: [],
159
- error: { code, message },
160
157
  });
161
158
  }
162
159
  /**
163
- * The run's execution failures before the Observer renders: a run error (the handoff, the plane),
164
- * each participant whose session failed in the harness, each provider whose cleanup is unconfirmed
165
- * or that reported a disallowed item after its last request, and each sandbox whose release is
160
+ * The run's execution failures before the Observer renders: a run error (the handoff, the plane)
161
+ * first, then the participants' failures, then the subject sandbox when its release is
166
162
  * unconfirmed. FinishedRun.renderObserver adds an Observer that did not render.
167
163
  */
168
164
  function sharedWorldExecutionFailures(args) {
169
165
  const { runId, runError, actorResults, subject } = args;
170
166
  return [
171
167
  ...(runError === undefined ? [] : [{ kind: "run", message: runError }]),
172
- ...actorResults
173
- .filter((result) => participantHarnessFailed(participantFactsOf(result.outcome)))
174
- .map((result) => ({
175
- kind: "harness",
176
- message: `${result.spec.planned.id}: ${result.outcome.sessionError ?? result.outcome.session?.reason ?? "harness error"}`,
177
- })),
178
- ...actorResults.flatMap((result) => result.outcome.providerCleanupError === undefined
179
- ? []
180
- : [
181
- {
182
- kind: "provider-cleanup",
183
- message: `${result.spec.planned.id}: ${result.outcome.providerCleanupError}`,
184
- },
185
- ]),
186
- ...actorResults.flatMap((result) => result.outcome.providerPolicyError === undefined
187
- ? []
188
- : [
189
- {
190
- kind: "provider-policy",
191
- message: `${result.spec.planned.id}: ${result.outcome.providerPolicyError}`,
192
- },
193
- ]),
194
- ...unreleasedSandboxFailures(actorResults.map((result) => result.outcome), runId),
168
+ ...participantExecutionFailures(actorResults.map((result) => result.outcome), runId),
195
169
  ...(subject.subjectSandboxId === undefined || subject.subjectKilled
196
170
  ? []
197
171
  : [sandboxCleanupFailure("subject", subject.subjectReleaseWarning, runId)]),
@@ -230,6 +204,9 @@ export async function finishConcurrentRun(ctx, live, results, plane) {
230
204
  for (const result of actorResults) {
231
205
  warnings.push(...result.outcome.warnings);
232
206
  }
207
+ const capWarning = participantCapWarning(plan.caps, participantCount, "each participant's model spend");
208
+ if (capWarning !== undefined)
209
+ warnings.push(capWarning);
233
210
  const bundleArgs = {
234
211
  run,
235
212
  plan,
@@ -293,6 +270,11 @@ export async function finishConcurrentRun(ctx, live, results, plane) {
293
270
  policy,
294
271
  });
295
272
  const observer = await finished.renderObserver();
273
+ // A caller's renderer runs with this process's file access, so after one that reports success
274
+ // the run directory is checked again before the result points anyone at it. A failed render is
275
+ // already an evidence failure, and its error code stays the run's diagnosis.
276
+ if (observer.ok)
277
+ await validatePreparedRunArtifactPaths(finished.paths);
296
278
  if (observer.ok && live.observer) {
297
279
  attachObserverRuntimeStreamUrls(observer, live.streamUrls);
298
280
  }
@@ -33,15 +33,12 @@
33
33
  // requires the author attestation subject.exposure: synthetic. This is author-trust + a provenance
34
34
  // gate; humanish cannot tell synthetic from real data, so it makes no claim about real data.
35
35
  import path from "node:path";
36
- import { missingKeys, missingSubjectEnv } from "../../study/requirements.js";
37
- import { scrubLiterals } from "../../evidence/redaction.js";
38
- import { describeMissingKeys } from "../../keys/key-resolution.js";
36
+ import { firstLiveRefusal, keysCheck, localAgentCheck, subjectEnvCheck, } from "../../study/requirements.js";
39
37
  import {} from "../../run/run.js";
38
+ import { RunSecrets } from "../../run/secrets.js";
40
39
  import { admitRoute, completeRefusalAnalysis, } from "../../run/route-shell.js";
41
- import { makeCuaRunBudget } from "../computer-use/participant-model.js";
42
40
  import { runExternalPublicPlane } from "./external-public.js";
43
41
  import { sharedWorldDescriptorOf } from "./plan.js";
44
- import { localAgentRefusal } from "../../actors/local-agent/readiness.js";
45
42
  import { runProvisionedPlane } from "./provisioned.js";
46
43
  import { concurrentStudyFailure, finishConcurrentRun } from "./result.js";
47
44
  import { prepareConcurrentRun } from "./setup.js";
@@ -99,29 +96,35 @@ async function admitSharedWorldRun(plan, input) {
99
96
  if (plan.dryRun)
100
97
  return admitted;
101
98
  const env = input.env ?? process.env;
102
- const fail = (code, message) => ({
99
+ const { requirements } = plan;
100
+ // The plan prices a cap before these, and its refusal wins over theirs.
101
+ const refusal = await firstLiveRefusal([
102
+ // The plan lists OPENAI_API_KEY for an openai brain's participants and the external-public
103
+ // plane's lobby-code reader; a local-agent brain's participants run on the operator's
104
+ // signed-in agent instead. Only on the provisioned plane does a local agent remove the key,
105
+ // so only there does the refusal suggest one.
106
+ () => keysCheck({
107
+ requirements,
108
+ env,
109
+ code: "HUMANISH_SHARED_WORLD_KEYS_MISSING",
110
+ need: (names) => `Live concurrent shared-world studies need ${names} in the environment (values are never persisted).`,
111
+ suggestLocalAgent: plan.plane.kind === "provisioned",
112
+ }),
113
+ () => localAgentCheck({
114
+ brain: plan.brain,
115
+ env,
116
+ caps: plan.caps,
117
+ codes: LOCAL_AGENT_REFUSAL_CODES,
118
+ }),
119
+ () => subjectEnvCheck({ requirements, env, code: "HUMANISH_SHARED_WORLD_SUBJECT_ENV_MISSING" }),
120
+ ]);
121
+ if (refusal === undefined)
122
+ return admitted;
123
+ const fail = sharedWorldFailure(plan, input);
124
+ return {
103
125
  ok: false,
104
- result: sharedWorldFailure(plan, input)(code, message, sharedWorldDescriptorOf(plan.actor).id),
105
- });
106
- // The plan lists OPENAI_API_KEY for an openai brain's participants and the external-public
107
- // plane's lobby-code reader; a local-agent brain's participants run on the operator's signed-in
108
- // agent instead.
109
- const missing = missingKeys(plan.requirements, env);
110
- if (missing.length > 0) {
111
- return fail("HUMANISH_SHARED_WORLD_KEYS_MISSING", `Live concurrent shared-world studies need ${missing.join(" and ")} in the environment (values are never persisted). ${describeMissingKeys(missing, env)}`);
112
- }
113
- if (plan.brain.kind === "local-agent") {
114
- // A missing or signed-out agent found after the participants' desktops are paid for is the same
115
- // news at the worst moment.
116
- const refusal = await localAgentRefusal({ agent: plan.brain.agent, env, caps: plan.caps });
117
- if (refusal)
118
- return fail(LOCAL_AGENT_REFUSAL_CODES[refusal.kind], refusal.message);
119
- }
120
- const unsetSubjectEnv = missingSubjectEnv(plan.requirements, env);
121
- if (unsetSubjectEnv.length > 0) {
122
- return fail("HUMANISH_SHARED_WORLD_SUBJECT_ENV_MISSING", `subject.env declares ${unsetSubjectEnv.join(", ")} but the environment does not provide ${unsetSubjectEnv.length === 1 ? "it" : "them"} (pass via --dotenv; values are never persisted).`);
123
- }
124
- return admitted;
126
+ result: fail(refusal.code, refusal.message, sharedWorldDescriptorOf(plan.actor).id),
127
+ };
125
128
  }
126
129
  /** The route's envelope for a run that stops before its bundle. */
127
130
  function sharedWorldFailure(plan, input) {
@@ -136,15 +139,12 @@ function sharedWorldFailure(plan, input) {
136
139
  });
137
140
  }
138
141
  async function runPlanInScope(plan, input, config, scope) {
139
- const { dryRun } = plan;
140
142
  const requestedCwd = path.resolve(input.cwd);
141
143
  const deps = input.deps ?? {};
142
144
  const env = input.env ?? process.env;
143
145
  const fail = sharedWorldFailure(plan, input);
144
146
  const descriptor = sharedWorldDescriptorOf(plan.actor);
145
147
  const planeClass = plan.plane.kind === "external-public" ? "external-public" : "provisioned-getHost";
146
- const { maxTotalUsd } = plan.caps;
147
- const runBudget = !dryRun && maxTotalUsd !== undefined ? makeCuaRunBudget(maxTotalUsd) : undefined;
148
148
  // The provisioned plane's subject; the external-public plane has none.
149
149
  const subject = plan.plane.kind === "provisioned" ? plan.plane.subject : undefined;
150
150
  const serve = subject?.serve;
@@ -155,13 +155,12 @@ async function runPlanInScope(plan, input, config, scope) {
155
155
  const runSession = deps.runSession ?? descriptor.runSession;
156
156
  const openaiApiKey = env.OPENAI_API_KEY?.trim() ?? "";
157
157
  const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
158
- const knownSecretValues = [
158
+ const secrets = new RunSecrets([
159
159
  openaiApiKey,
160
160
  e2bApiKey,
161
161
  ...subjectEnvNames.map((name) => env[name] ?? ""),
162
162
  ...checkpoints.flatMap((probe) => probe.redact ?? []),
163
- ].filter((value) => value.length >= 4);
164
- const scrubKnownValues = scrubLiterals(knownSecretValues);
163
+ ]);
165
164
  const redactRepoLabel = plan.residual.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
166
165
  const publicRepo = redactRepoLabel ? "repo-01" : subjectRepo;
167
166
  const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
@@ -174,7 +173,6 @@ async function runPlanInScope(plan, input, config, scope) {
174
173
  env,
175
174
  descriptor,
176
175
  planeClass,
177
- runBudget,
178
176
  runSession,
179
177
  serve,
180
178
  localTreeRoute,
@@ -183,8 +181,7 @@ async function runPlanInScope(plan, input, config, scope) {
183
181
  checkpoints,
184
182
  openaiApiKey,
185
183
  e2bApiKey,
186
- knownSecretValues,
187
- scrubKnownValues,
184
+ secrets,
188
185
  publicRepo,
189
186
  hasGithubToken,
190
187
  fail,
@@ -1,6 +1,7 @@
1
1
  import type { CuaActorDescriptor } from "../../actors/registry.js";
2
2
  import type { StudyConfig, StudySubjectServe, StudySubjectStateCheckpoint } from "../../study/types.js";
3
3
  import type { RunScope } from "../../run/run.js";
4
+ import type { RunSecrets } from "../../run/secrets.js";
4
5
  import type { ConcurrentSharedWorldStudyErrorCode, ConcurrentSharedWorldStudyResult, ConcurrentSharedWorldPlaneClass, FinishFacts, LiveParticipants, PlaneContext, PlaneResults, PlaneSelection, SharedWorldRunInput } from "./types.js";
5
6
  import type { StudyDeps } from "../../study/study-deps.js";
6
7
  import type { SharedWorldPlan } from "../../study/plan-types.js";
@@ -16,7 +17,6 @@ interface AdmittedStudy {
16
17
  env: Record<string, string | undefined>;
17
18
  descriptor: CuaActorDescriptor;
18
19
  planeClass: ConcurrentSharedWorldPlaneClass;
19
- runBudget: PlaneContext["runBudget"];
20
20
  runSession: PlaneContext["runSession"];
21
21
  serve: StudySubjectServe | undefined;
22
22
  localTreeRoute: boolean;
@@ -25,8 +25,7 @@ interface AdmittedStudy {
25
25
  checkpoints: StudySubjectStateCheckpoint[];
26
26
  openaiApiKey: string;
27
27
  e2bApiKey: string;
28
- knownSecretValues: string[];
29
- scrubKnownValues: (text: string) => string;
28
+ secrets: RunSecrets;
30
29
  publicRepo: string;
31
30
  hasGithubToken: boolean;
32
31
  fail: (code: ConcurrentSharedWorldStudyErrorCode, message: string, actorLabel?: string) => ConcurrentSharedWorldStudyResult;
@@ -1,8 +1,7 @@
1
1
  // Starts a concurrent shared-world run and prepares what its plane reads: the physical project,
2
2
  // the run, the participant specs, comms, the packed tree and email receiving.
3
- import { randomBytes } from "node:crypto";
4
3
  import { realpath } from "node:fs/promises";
5
- import { buildRunSource } from "../../run/bundle.js";
4
+ import {} from "../../run/bundle.js";
6
5
  import { prepareSelectedOutputDirectory } from "../../run/contained-output.js";
7
6
  import { renderConcurrentReviewMarkdown } from "./bundle.js";
8
7
  import { seedRecipeDigest } from "./checkpoints.js";
@@ -15,10 +14,6 @@ import { planeStateOf } from "./plan.js";
15
14
  import path from "node:path";
16
15
  import { e2bRequestTimeoutMs } from "../../substrates/e2b/lifetime.js";
17
16
  const DEFAULT_PROBER_CADENCE_MS = 1000;
18
- function makeRunId() {
19
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
20
- return `concurrent-shared-world-${stamp}-${randomBytes(4).toString("hex")}`;
21
- }
22
17
  /** The provisioned plane's setup, or undefined when the study declares no `subject.serve`. */
23
18
  function provisionedSetup(study, prepared) {
24
19
  if (!study.serve)
@@ -49,16 +44,11 @@ async function bindPhysicalProject(requestedCwd) {
49
44
  .physicalPath;
50
45
  }
51
46
  function startConcurrentRun(study, cwd, scope) {
52
- const { plan, input, deps } = study;
53
- return scope.startRun({
47
+ return scope.startRun(study.plan, study.input, {
54
48
  cwd,
55
- runId: input.runId,
56
- mintRunId: makeRunId,
57
- mode: plan.dryRun ? "dry-run" : "live",
58
- study: plan.study,
59
- warnings: plan.warnings,
49
+ prefix: "concurrent-shared-world",
60
50
  renderReview: renderConcurrentReviewMarkdown,
61
- observer: { open: input.open === true, render: deps.renderObserver },
51
+ secrets: study.secrets,
62
52
  });
63
53
  }
64
54
  /**
@@ -67,8 +57,8 @@ function startConcurrentRun(study, cwd, scope) {
67
57
  */
68
58
  export async function prepareConcurrentRun(study, scope) {
69
59
  const { plan, input, config, requestedCwd, deps, env, descriptor, planeClass, fail } = study;
70
- const { runBudget, runSession, localTreeRoute, subjectEnvNames, publicRepo } = study;
71
- const { openaiApiKey, e2bApiKey, knownSecretValues, scrubKnownValues } = study;
60
+ const { runSession, localTreeRoute, subjectEnvNames, publicRepo } = study;
61
+ const { openaiApiKey, e2bApiKey, secrets } = study;
72
62
  const { dryRun, concurrency } = plan;
73
63
  const cwd = await bindPhysicalProject(requestedCwd);
74
64
  const warnings = [];
@@ -81,7 +71,7 @@ export async function prepareConcurrentRun(study, scope) {
81
71
  // Pack the working tree once per run, on the host, before any sandbox exists: a packing
82
72
  // failure fails the run closed without sandbox cost. Dry-run packs nothing.
83
73
  const packed = localTreeRoute && !dryRun
84
- ? await packSubjectTree(cwd, plan.residual, deps, scrubKnownValues)
74
+ ? await packSubjectTree(cwd, plan.residual, deps, secrets.scrub)
85
75
  : { ok: true, archive: undefined, buffer: undefined };
86
76
  if (!packed.ok) {
87
77
  return {
@@ -95,7 +85,7 @@ export async function prepareConcurrentRun(study, scope) {
95
85
  if (!started.ok)
96
86
  return { ok: false, result: fail(started.code, started.message, descriptor.id) };
97
87
  const { run } = started;
98
- const { runId, createdAt, paths: runPaths } = run;
88
+ const { runId, createdAt, paths: runPaths, source } = run;
99
89
  const artifactRoot = runPaths.absoluteRunRoot;
100
90
  const timeoutMs = plan.sessionTimeoutMs ?? defaultSessionTimeoutMs(plan);
101
91
  const requestTimeoutMs = e2bRequestTimeoutMs(env);
@@ -104,15 +94,9 @@ export async function prepareConcurrentRun(study, scope) {
104
94
  const now = deps.now ?? Date.now;
105
95
  const proberCadenceMs = deps.proberCadenceMs ?? DEFAULT_PROBER_CADENCE_MS;
106
96
  const seedDigest = seedRecipeDigest(planeStateOf(plan));
107
- const source = await buildRunSource({
108
- capturedAt: createdAt,
109
- cwd,
110
- humanishSource: "present",
111
- packageName: "humanish",
112
- });
113
97
  const stateStepRecords = [];
114
98
  const stateSnapshots = [];
115
- const actorSpecs = await buildParticipantSpecs(plan.plane.participants, cwd, scrubKnownValues);
99
+ const actorSpecs = await buildParticipantSpecs(plan.plane.participants, cwd, run.secrets.scrub);
116
100
  const results = emptyPlaneResults();
117
101
  const live = { streamUrls: [] };
118
102
  const subjectComms = subjectCommsOf(plan.residual, planeClass);
@@ -124,7 +108,7 @@ export async function prepareConcurrentRun(study, scope) {
124
108
  env,
125
109
  participants: actorSpecs.map((spec) => spec.planned.id),
126
110
  runPaths,
127
- knownSecretValues,
111
+ secrets: run.secrets,
128
112
  dryRun,
129
113
  });
130
114
  if (!email.ok) {
@@ -148,12 +132,10 @@ export async function prepareConcurrentRun(study, scope) {
148
132
  deps,
149
133
  env,
150
134
  concurrency,
151
- runBudget,
152
135
  runSession,
153
136
  openaiApiKey,
154
137
  e2bApiKey,
155
- scrubKnownValues,
156
- knownSecretValues,
138
+ scrubKnownValues: run.secrets.scrub,
157
139
  cwd,
158
140
  run,
159
141
  runId,
@@ -7,7 +7,7 @@ import type { AutomaticAnalysisResult } from "../../analysis/automatic-completio
7
7
  import type { CuaActorDescriptor } from "../../actors/registry.js";
8
8
  import type { CommsReceivingRun } from "../../comms/receiving.js";
9
9
  import type { StudyCommsEmail, StudyCommsExternal, StudyConfig } from "../../study/types.js";
10
- import type { DesktopResourceObservation } from "../../substrates/e2b/desktop-resources.js";
10
+ import type { SubjectDesktopUsage } from "../../substrates/e2b/subject-sandbox.js";
11
11
  import type { ObserverResult } from "../../observer/render.js";
12
12
  import type { ObserverRuntimeStreamUrl } from "../../observer/run-routes.js";
13
13
  import type { BundleRun, RunBundle, RunScorerProvenance, RunSubjectProvenance, RunSubjectStateStepRecord } from "../../run/bundle.js";
@@ -15,10 +15,10 @@ import type { RunScope } from "../../run/run.js";
15
15
  import type { SharedWorldStateSnapshot } from "../../run/shared-world-evidence.js";
16
16
  import type { LocalTreeArchive } from "../../subject/local-tree-archive.js";
17
17
  import type { LiveTraceFlush } from "../computer-use/live-flush.js";
18
- import type { CuaParticipantDeps, DesktopParticipantRun, CuaRunBudget, ParticipantRunOutcome } from "../computer-use/types.js";
18
+ import type { CuaParticipantDeps, DesktopParticipantRun, ParticipantRunOutcome } from "../computer-use/types.js";
19
19
  import type { ProvisionedPlaneSetup } from "./provisioned.js";
20
20
  import type { SharedWorldPlan } from "../../study/plan-types.js";
21
- import { type StudyResultIdentity } from "../../run/study-result.js";
21
+ import { type StudyResultErrorCode, type StudyResultIdentity } from "../../run/study-result.js";
22
22
  export declare const CONCURRENT_SHARED_WORLD_PROVIDER_METADATA: {
23
23
  readonly mode: "concurrent-shared-world-lab";
24
24
  readonly tool: "humanish";
@@ -54,13 +54,13 @@ export interface SharedWorldRunInput {
54
54
  * core-stamped onto the bundle as evidence. Absent for library callers. */
55
55
  scorerProvenance?: RunScorerProvenance;
56
56
  }
57
- export type ConcurrentSharedWorldStudyErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_SHARED_WORLD_FAILED" | "HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_INVALID" | "HUMANISH_SHARED_WORLD_KEYS_MISSING"
57
+ export type ConcurrentSharedWorldStudyErrorCode = StudyResultErrorCode | "HUMANISH_SHARED_WORLD_FAILED" | "HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_INVALID" | "HUMANISH_SHARED_WORLD_KEYS_MISSING"
58
58
  /** A local-agent brain's CLI is not on `PATH`. */
59
59
  | "HUMANISH_SHARED_WORLD_AGENT_MISSING"
60
60
  /** A local-agent brain's CLI is signed out, or could not report its sign-in status. */
61
61
  | "HUMANISH_SHARED_WORLD_AGENT_SIGNIN_REQUIRED"
62
62
  /** A dollar cap that cannot be priced: an unpriced model, or a ChatGPT-account Codex brain. */
63
- | "HUMANISH_SHARED_WORLD_UNPRICED_CAP" | "HUMANISH_SHARED_WORLD_SUBJECT_ENV_MISSING" | "HUMANISH_SHARED_WORLD_GETHOST_UNAVAILABLE" | "HUMANISH_SHARED_WORLD_HANDOFF_TIMEOUT" | "HUMANISH_RUN_ID_IN_USE"
63
+ | "HUMANISH_SHARED_WORLD_UNPRICED_CAP" | "HUMANISH_SHARED_WORLD_SUBJECT_ENV_MISSING" | "HUMANISH_SHARED_WORLD_GETHOST_UNAVAILABLE" | "HUMANISH_SHARED_WORLD_HANDOFF_TIMEOUT"
64
64
  /** A declared adopter-hosted comms catch did not answer as a humanish catch. Fail closed
65
65
  * before any actor spend, since the funnel would silently collect nothing. */
66
66
  | "HUMANISH_SHARED_WORLD_COMMS_CATCH_UNREACHABLE"
@@ -134,12 +134,6 @@ export interface ConcurrentSharedWorldStudyResult extends AutomaticAnalysisResul
134
134
  message: string;
135
135
  };
136
136
  }
137
- /** The provisioned plane's own desktop, for the run's cost estimate. */
138
- export interface SubjectDesktopUsage {
139
- durationMs: number | undefined;
140
- observation: DesktopResourceObservation | undefined;
141
- killed: boolean;
142
- }
143
137
  /** One actor's measured run (internal). */
144
138
  export interface ActorRunResult {
145
139
  spec: DesktopParticipantRun;
@@ -163,14 +157,11 @@ export interface PlaneContext {
163
157
  deps: StudyDeps;
164
158
  env: Record<string, string | undefined>;
165
159
  concurrency: number;
166
- runBudget: CuaRunBudget | undefined;
167
160
  runSession: CuaParticipantDeps["runSession"];
168
161
  openaiApiKey: string;
169
162
  e2bApiKey: string;
163
+ /** `run.secrets.scrub`, which every plane step that writes text reads. */
170
164
  scrubKnownValues: (text: string) => string;
171
- /** The values scrubKnownValues removes. Real email receiving adds its secrets before participants
172
- * start. */
173
- knownSecretValues: readonly string[];
174
165
  cwd: string;
175
166
  run: StartedRun;
176
167
  runId: string;