humanish 0.38.0 → 0.40.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.
Files changed (57) hide show
  1. package/README.md +5 -2
  2. package/dist/actor-contract.d.ts +24 -1
  3. package/dist/actor-contract.js +4 -0
  4. package/dist/actor-contract.js.map +1 -1
  5. package/dist/claude-agent-sdk.js +4 -0
  6. package/dist/claude-agent-sdk.js.map +1 -1
  7. package/dist/comms-catch-host.d.ts +31 -0
  8. package/dist/comms-catch-host.js +82 -4
  9. package/dist/comms-catch-host.js.map +1 -1
  10. package/dist/comms-sandbox-catch.d.ts +32 -1
  11. package/dist/comms-sandbox-catch.js +226 -19
  12. package/dist/comms-sandbox-catch.js.map +1 -1
  13. package/dist/computer-use-actor.d.ts +1 -1
  14. package/dist/computer-use.d.ts +13 -1
  15. package/dist/computer-use.js +100 -14
  16. package/dist/computer-use.js.map +1 -1
  17. package/dist/concurrent-shared-world-lab.js +7 -2
  18. package/dist/concurrent-shared-world-lab.js.map +1 -1
  19. package/dist/cua-actor-lab.d.ts +8 -0
  20. package/dist/cua-actor-lab.js +110 -9
  21. package/dist/cua-actor-lab.js.map +1 -1
  22. package/dist/e2b-desktop-executor.d.ts +21 -6
  23. package/dist/e2b-desktop-executor.js +65 -21
  24. package/dist/e2b-desktop-executor.js.map +1 -1
  25. package/dist/lab-config.d.ts +36 -0
  26. package/dist/lab-config.js +87 -2
  27. package/dist/lab-config.js.map +1 -1
  28. package/dist/observer-data.js +5 -0
  29. package/dist/observer-data.js.map +1 -1
  30. package/dist/openai-responses-cu.js +10 -1
  31. package/dist/openai-responses-cu.js.map +1 -1
  32. package/dist/orientation.d.ts +29 -0
  33. package/dist/orientation.js +95 -0
  34. package/dist/orientation.js.map +1 -0
  35. package/dist/persona-resolve.d.ts +44 -0
  36. package/dist/persona-resolve.js +109 -0
  37. package/dist/persona-resolve.js.map +1 -0
  38. package/dist/pi-agent-core.js +4 -0
  39. package/dist/pi-agent-core.js.map +1 -1
  40. package/dist/pricing.d.ts +8 -0
  41. package/dist/pricing.js +22 -6
  42. package/dist/pricing.js.map +1 -1
  43. package/dist/program.js +36 -1
  44. package/dist/program.js.map +1 -1
  45. package/dist/run.d.ts +1 -1
  46. package/dist/run.js +27 -1
  47. package/dist/run.js.map +1 -1
  48. package/dist/shared-world-lab.js +7 -2
  49. package/dist/shared-world-lab.js.map +1 -1
  50. package/dist/subject-runtime.d.ts +23 -0
  51. package/dist/subject-runtime.js +70 -0
  52. package/dist/subject-runtime.js.map +1 -0
  53. package/docs/contracts/schemas.md +1 -1
  54. package/docs/goals/current.md +2 -2
  55. package/docs/principles/three-roles.md +68 -0
  56. package/docs/ramp/README.md +1 -1
  57. package/package.json +1 -1
@@ -40,6 +40,9 @@ import { mapWithConcurrency } from "./concurrency.js";
40
40
  import { appendSandboxReceipt } from "./sandbox-receipts.js";
41
41
  import { assertScreenshotEvidence } from "./image-evidence.js";
42
42
  import { buildObserverData } from "./observer-data.js";
43
+ import { corepackCommandFor, needsNodeRuntime, nodeBootstrapCommand } from "./subject-runtime.js";
44
+ import { personaToDirectives, renderPersonaPromptSection } from "./persona.js";
45
+ import { labPersonaIds, resolveCommittedPersonas } from "./persona-resolve.js";
43
46
  import { attachObserverRuntimeStreamUrls, renderObserver } from "./observer.js";
44
47
  import { containsSensitive, digestText, redactedTail, redactText } from "./redaction.js";
45
48
  import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputDirectory, prepareSelectedOutputDirectory, writeContainedOutputFile, writePreparedRunLatestPointer } from "./selected-output-paths.js";
@@ -73,6 +76,10 @@ const BROWSER_SETTLE_MS = 8_000;
73
76
  const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
74
77
  // Room the clone route adds to the sandbox deadline for clone/install/build/start/probe.
75
78
  const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
79
+ /** E2B refuses a sandbox lifetime over one hour ("400: Timeout cannot be greater than 1 hours").
80
+ * The derived per-lane deadline has to stay under it, and saying so at plan time beats discovering
81
+ * it from a raw provider 400 after a plan has already printed. */
82
+ const MAX_SANDBOX_MS = 60 * 60_000;
76
83
  export const SUBJECT_DIR = "/home/user/subject";
77
84
  // Remote path for the once-per-run packed local-tree archive; removed by the extract step
78
85
  // after it unpacks into SUBJECT_DIR.
@@ -94,8 +101,15 @@ export function composeLaneInstructions(args) {
94
101
  const deviceLine = preset.isMobile
95
102
  ? `You are a mobile user on a ${name} device (${preset.width}x${preset.height} @${preset.deviceScaleFactor}x). Expect a mobile/touch layout.`
96
103
  : `You are a desktop user (${name}, ${preset.width}x${preset.height}).`;
104
+ // A resolved persona contributes its compiled directives (friction tolerance, skill bias,
105
+ // accessibility behavior, constraints) through the SAME persona.ts compiler the terminal lane
106
+ // uses, so one persona file means one behavior across every route.
107
+ const personaLine = args.resolvedPersona
108
+ ? renderPersonaPromptSection(args.resolvedPersona)
109
+ : args.persona ? `Persona: ${args.persona}.` : undefined;
110
+ const traitsApplied = args.resolvedPersona ? personaToDirectives(args.resolvedPersona).traitsApplied : [];
97
111
  const parts = [
98
- args.persona ? `Persona: ${args.persona}.` : undefined,
112
+ personaLine,
99
113
  deviceLine,
100
114
  args.mission,
101
115
  args.instruction ? `Lane focus: ${args.instruction}` : undefined
@@ -105,7 +119,7 @@ export function composeLaneInstructions(args) {
105
119
  instructions,
106
120
  persona: {
107
121
  id: args.persona ?? "cua-operator",
108
- traitsApplied: [],
122
+ traitsApplied,
109
123
  promptDigest: digestText(instructions, 16)
110
124
  }
111
125
  };
@@ -239,9 +253,12 @@ function laneSpecsAndPlan(config, opts = {}) {
239
253
  const simId = `sim-${String(i + 1).padStart(3, "0")}`;
240
254
  const streamId = `stream-${String(i + 1).padStart(3, "0")}`;
241
255
  const device = resolveLaneDevice(config, lane);
256
+ const personaId = (roster ? lane?.persona : actor?.persona);
257
+ const resolvedPersona = personaId === undefined ? undefined : opts.personas?.get(personaId);
242
258
  const composed = composeLaneInstructions({
243
259
  mission,
244
- ...(((roster ? lane?.persona : actor?.persona)) === undefined ? {} : { persona: (roster ? lane?.persona : actor?.persona) }),
260
+ ...(personaId === undefined ? {} : { persona: personaId }),
261
+ ...(resolvedPersona === undefined ? {} : { resolvedPersona }),
245
262
  ...(((roster ? lane?.instruction : actor?.laneFocus?.instruction)) === undefined ? {} : { instruction: (roster ? lane?.instruction : actor?.laneFocus?.instruction) }),
246
263
  device: { name: device.name, preset: device.preset }
247
264
  });
@@ -1049,6 +1066,7 @@ export function resolveSelfReportedBlocker(session) {
1049
1066
  */
1050
1067
  export async function runCuaLane(spec, deps) {
1051
1068
  const { config, appUrl, cloneRoute, localTreeRoute, serve, subjectRepo, subjectEnvNames } = deps;
1069
+ const subjectEnvValues = config.subject.envValues ?? {};
1052
1070
  const targetUrl = spec.targetUrl ?? appUrl;
1053
1071
  const env = deps.env;
1054
1072
  // Off-app comms (#297): on an in-sandbox subject route, redirect the app's email-API sends into an
@@ -1066,6 +1084,18 @@ export async function runCuaLane(spec, deps) {
1066
1084
  const commsEnv = commsEmail?.injectEnv !== undefined && commsPort !== undefined
1067
1085
  ? { [commsEmail.injectEnv]: `http://127.0.0.1:${commsPort}` }
1068
1086
  : {};
1087
+ // SMTP transport: the same idea as injectEnv, but an app that speaks SMTP needs a host and a port
1088
+ // rather than a base URL. The catch accepts any credentials (loopback only), yet many apps refuse
1089
+ // to boot unless the user/password vars exist at all, so those are injected when declared.
1090
+ const commsSmtpPort = commsEmail?.smtp?.port;
1091
+ if (commsEmail?.smtp && commsSmtpPort !== undefined) {
1092
+ commsEnv[commsEmail.smtp.hostEnv] = "127.0.0.1";
1093
+ commsEnv[commsEmail.smtp.portEnv] = String(commsSmtpPort);
1094
+ if (commsEmail.smtp.userEnv)
1095
+ commsEnv[commsEmail.smtp.userEnv] = commsEmail.smtp.user ?? "humanish";
1096
+ if (commsEmail.smtp.passwordEnv)
1097
+ commsEnv[commsEmail.smtp.passwordEnv] = commsEmail.smtp.password ?? "humanish";
1098
+ }
1069
1099
  // Persona inbox SURFACE (#297 slice B): the loopback URL the persona opens to read captured mail; the
1070
1100
  // origin-rewrite map (identity on this same-sandbox route, but covers localhost/0.0.0.0 alias skew + an
1071
1101
  // operator-declared linkOrigin); and a disposable background loop that renders the surface DURING the
@@ -1155,8 +1185,17 @@ export async function runCuaLane(spec, deps) {
1155
1185
  },
1156
1186
  // Env placement per the doctrine: the ACTOR's key never enters the sandbox (the model drives
1157
1187
  // from outside). The SUBJECT's declared env NAMES are provisioned here on the clone route.
1158
- ...(subjectEnvNames.length > 0 || Object.keys(commsEnv).length > 0
1159
- ? { envs: { ...Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])), ...commsEnv } }
1188
+ // Three sources, in precedence order: committed non-secret config (subject.envValues), then
1189
+ // secret values forwarded from the caller's environment (subject.env), then the harness's own
1190
+ // comms wiring, which must win because only it knows the catch's address.
1191
+ ...(subjectEnvNames.length > 0 || Object.keys(subjectEnvValues).length > 0 || Object.keys(commsEnv).length > 0
1192
+ ? {
1193
+ envs: {
1194
+ ...subjectEnvValues,
1195
+ ...Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])),
1196
+ ...commsEnv
1197
+ }
1198
+ }
1160
1199
  : {}),
1161
1200
  resolution: spec.resolution,
1162
1201
  dpi: 96,
@@ -1175,7 +1214,11 @@ export async function runCuaLane(spec, deps) {
1175
1214
  // into its env at create) resolves the moment it boots. A comms-declared lab that can't stand the
1176
1215
  // catch up is a setup failure (fail closed) rather than silently sending real mail.
1177
1216
  if (commsEmail && commsPort !== undefined) {
1178
- deployedComms = await deployCommsCatch(desktop, { port: commsPort, requestTimeoutMs: deps.requestTimeoutMs });
1217
+ deployedComms = await deployCommsCatch(desktop, {
1218
+ port: commsPort,
1219
+ ...(commsSmtpPort === undefined ? {} : { smtpPort: commsSmtpPort }),
1220
+ requestTimeoutMs: deps.requestTimeoutMs
1221
+ });
1179
1222
  if (!deployedComms.ready) {
1180
1223
  throw new Error(`comms email catch did not become ready on 127.0.0.1:${commsPort} in the subject sandbox`);
1181
1224
  }
@@ -1351,7 +1394,7 @@ export async function runCuaLane(spec, deps) {
1351
1394
  ? {}
1352
1395
  : {
1353
1396
  maxUsd,
1354
- estimateTurnCostUsd: (input, output) => estimateActorCost({ input, output }, capModelId).estimatedCostUsd
1397
+ estimateTurnCostUsd: (input, output, cachedInput) => estimateActorCost({ input, output, ...(cachedInput === undefined ? {} : { cachedInput }) }, capModelId).estimatedCostUsd
1355
1398
  }),
1356
1399
  desktop: desktop,
1357
1400
  ...(launchedBrowserFamily === "chromium"
@@ -1461,7 +1504,10 @@ export async function runCuaLane(spec, deps) {
1461
1504
  // Zero captures is the silent-broken shape (#351): the app never posted to the catch at
1462
1505
  // all, so the personas stared at an empty inbox. Most common cause: the app does not
1463
1506
  // actually read the declared injectEnv var for its email API base URL.
1464
- warnings.push(`Comms catch captured ZERO email sends — the app never delivered mail through the catch. Verify the app reads ${commsEmail.injectEnv} for its email API base URL (an SDK that ignores it sends real mail or throws) and that the flow reached an email step.`);
1507
+ const transportHint = commsEmail.smtp
1508
+ ? `Verify the app reads ${commsEmail.smtp.hostEnv}/${commsEmail.smtp.portEnv} for its SMTP host and port`
1509
+ : `Verify the app reads ${commsEmail.injectEnv} for its email API base URL (an SDK that ignores it sends real mail or throws)`;
1510
+ warnings.push(`Comms catch captured ZERO email sends — the app never delivered mail through the catch. ${transportHint} and that the flow reached an email step.`);
1465
1511
  }
1466
1512
  }
1467
1513
  catch (error) {
@@ -1965,11 +2011,28 @@ export async function runCuaActorLab(options) {
1965
2011
  if (fanoutReason) {
1966
2012
  return fail("HUMANISH_CUA_LAB_FANOUT_INVALID", fanoutReason, descriptor.id);
1967
2013
  }
2014
+ // The sandbox deadline is DERIVED from the session budget, so a lab can ask for a session that
2015
+ // cannot legally be provisioned. Catch it here, before anything is created, and show the
2016
+ // arithmetic — the provider's own error names a limit but not which knob produced it.
2017
+ const derivedSandboxMs = resolvePerLaneSandboxMs(config);
2018
+ if (derivedSandboxMs > MAX_SANDBOX_MS) {
2019
+ const provisionedRoute = config.subject.source === "clone" || config.subject.source === "local-tree";
2020
+ const sessionMs = config.execution?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
2021
+ const headroomMs = derivedSandboxMs - sessionMs;
2022
+ return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", `execution.timeoutMs ${Math.round(sessionMs / 60_000)}m derives a ${Math.round(derivedSandboxMs / 60_000)}m sandbox deadline, and a sandbox may not live longer than ${MAX_SANDBOX_MS / 60_000}m. The deadline is the session budget plus ${Math.round(headroomMs / 60_000)}m of provisioning and teardown headroom${provisionedRoute ? " (this route clones, installs, builds and serves the subject before the actor starts)" : ""}. Lower execution.timeoutMs to at most ${Math.round((MAX_SANDBOX_MS - headroomMs) / 60_000)}m, or set execution.desktop.sandboxTimeoutMs explicitly.`, descriptor.id);
2023
+ }
2024
+ // Compile any committed personas BEFORE planning, so the plan builder stays pure and each lane's
2025
+ // prompt carries real behavioral directives rather than a bare `Persona: <id>.` label (#381).
2026
+ const personaResolution = await resolveCommittedPersonas(projectRoot, labPersonaIds(config));
2027
+ for (const warning of personaResolution.warnings) {
2028
+ process.stderr.write(`humanish: ${warning}\n`);
2029
+ }
1968
2030
  // Resolve the lane plan (pure) — the SAME table for dry-run and live.
1969
2031
  let { lanes: laneSpecs, plan } = laneSpecsAndPlan(config, {
1970
2032
  ...(options.countOverride === undefined ? {} : { countOverride: options.countOverride }),
1971
2033
  env,
1972
- dryRun
2034
+ dryRun,
2035
+ personas: personaResolution.personas
1973
2036
  });
1974
2037
  let laneCount = laneSpecs.length;
1975
2038
  if (laneCount > MAX_CUA_LANES) {
@@ -2555,6 +2618,39 @@ async function runSubjectServePipeline(desktop, args) {
2555
2618
  }
2556
2619
  emitPhaseCompleted(args.onPhase, now, groupStartedAt, `state.${when}`, true, `subject state seed steps complete (${when})`);
2557
2620
  };
2621
+ // Provide the runtime the pipeline needs before running it (#371). The stock desktop template
2622
+ // ships python3 and curl but no Node, so an `npm install` here used to die at exit 127 after the
2623
+ // sandbox was already paid for. Probe-first, so a template that ships its own Node pays nothing.
2624
+ const serveCommands = [args.serve.install, args.serve.build, args.serve.start];
2625
+ if (needsNodeRuntime(serveCommands)) {
2626
+ const runtimeStartedAt = now();
2627
+ emitPhaseStarted(args.onPhase, now, "runtime", "providing the Node runtime the serve pipeline needs");
2628
+ const bootstrap = await runDetachedStep(desktop, {
2629
+ name: "subject-runtime-node",
2630
+ command: nodeBootstrapCommand(),
2631
+ cwd: SUBJECT_DIR,
2632
+ timeoutMs: args.serve.installTimeoutMs ?? INSTALL_TIMEOUT_MS,
2633
+ requestTimeoutMs: args.requestTimeoutMs,
2634
+ ...timers
2635
+ });
2636
+ let ok = bootstrap.ok;
2637
+ const corepack = ok ? corepackCommandFor(serveCommands) : undefined;
2638
+ if (corepack) {
2639
+ const pm = await runDetachedStep(desktop, {
2640
+ name: "subject-runtime-pm",
2641
+ command: corepack,
2642
+ cwd: SUBJECT_DIR,
2643
+ timeoutMs: args.serve.installTimeoutMs ?? INSTALL_TIMEOUT_MS,
2644
+ requestTimeoutMs: args.requestTimeoutMs,
2645
+ ...timers
2646
+ });
2647
+ ok = pm.ok;
2648
+ }
2649
+ emitPhaseCompleted(args.onPhase, now, runtimeStartedAt, "runtime", ok, ok ? "Node runtime ready" : "could not provide a Node runtime");
2650
+ if (!ok) {
2651
+ throw new Error(`the subject's serve pipeline needs a Node runtime and this desktop template has none, and bootstrapping one failed: ${tailOf(args.scrub(bootstrap.logTail))}. Use execution.desktop.template with an image that ships Node, or change serve.install to a runtime the template provides.`);
2652
+ }
2653
+ }
2558
2654
  if (args.serve.install) {
2559
2655
  const installStartedAt = now();
2560
2656
  emitPhaseStarted(args.onPhase, now, "install", "installing subject dependencies");
@@ -3661,6 +3757,11 @@ function verdictForStatus(status) {
3661
3757
  return "blocked";
3662
3758
  case "timed_out":
3663
3759
  return "timed_out";
3760
+ // A participant who abandoned, or a session that ran out before the goal, did not pass — but the
3761
+ // harness did not fail either. The run reports what happened rather than a verdict on the tool.
3762
+ case "abandoned":
3763
+ case "incomplete":
3764
+ return "fail";
3664
3765
  }
3665
3766
  }
3666
3767
  function renderCuaReviewMarkdown(bundle) {