humanish 0.111.1 → 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 (78) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/CHANGELOG.md +29 -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/observer-app.html +6 -6
  9. package/dist/routes/computer-use/bundle-parts.d.ts +1 -32
  10. package/dist/routes/computer-use/bundle-parts.js +5 -92
  11. package/dist/routes/computer-use/e2b-desktop/prepare.js +11 -4
  12. package/dist/routes/computer-use/fanout-bundle.js +3 -2
  13. package/dist/routes/computer-use/live-phase.js +3 -7
  14. package/dist/routes/computer-use/participant-deps.d.ts +46 -0
  15. package/dist/routes/computer-use/participant-deps.js +48 -0
  16. package/dist/routes/computer-use/participant-facts.d.ts +5 -4
  17. package/dist/routes/computer-use/participant-facts.js +37 -2
  18. package/dist/routes/computer-use/participant-feedback.d.ts +33 -0
  19. package/dist/routes/computer-use/participant-feedback.js +92 -0
  20. package/dist/routes/computer-use/participant-model.d.ts +11 -0
  21. package/dist/routes/computer-use/participant-model.js +15 -0
  22. package/dist/routes/computer-use/preflight.js +33 -65
  23. package/dist/routes/computer-use/result.js +13 -52
  24. package/dist/routes/computer-use/route.js +6 -7
  25. package/dist/routes/computer-use/setup.d.ts +2 -6
  26. package/dist/routes/computer-use/setup.js +47 -93
  27. package/dist/routes/computer-use/single-bundle.js +3 -2
  28. package/dist/routes/computer-use/types.d.ts +2 -2
  29. package/dist/routes/preview.js +2 -8
  30. package/dist/routes/scripted/bundle.d.ts +2 -6
  31. package/dist/routes/scripted/result.d.ts +2 -1
  32. package/dist/routes/scripted/result.js +4 -4
  33. package/dist/routes/scripted/route.js +13 -38
  34. package/dist/routes/scripted/setup.d.ts +2 -1
  35. package/dist/routes/scripted/setup.js +21 -28
  36. package/dist/routes/scripted/subject.d.ts +3 -18
  37. package/dist/routes/scripted/subject.js +22 -78
  38. package/dist/routes/scripted/types.d.ts +2 -2
  39. package/dist/routes/shared-world/bundle.js +36 -1
  40. package/dist/routes/shared-world/comms.d.ts +2 -1
  41. package/dist/routes/shared-world/comms.js +3 -7
  42. package/dist/routes/shared-world/handoff.d.ts +2 -2
  43. package/dist/routes/shared-world/handoff.js +1 -1
  44. package/dist/routes/shared-world/participant-specs.d.ts +4 -5
  45. package/dist/routes/shared-world/participant-specs.js +19 -25
  46. package/dist/routes/shared-world/plan.js +11 -7
  47. package/dist/routes/shared-world/provisioned.d.ts +2 -1
  48. package/dist/routes/shared-world/provisioned.js +37 -80
  49. package/dist/routes/shared-world/result.js +17 -35
  50. package/dist/routes/shared-world/route.js +33 -36
  51. package/dist/routes/shared-world/setup.d.ts +2 -3
  52. package/dist/routes/shared-world/setup.js +11 -29
  53. package/dist/routes/shared-world/types.d.ts +6 -15
  54. package/dist/routes/terminal/dry-run.js +14 -24
  55. package/dist/routes/terminal/live-finish.d.ts +0 -1
  56. package/dist/routes/terminal/live-finish.js +2 -2
  57. package/dist/routes/terminal/route.js +9 -15
  58. package/dist/routes/terminal/session.d.ts +2 -3
  59. package/dist/routes/terminal/session.js +28 -40
  60. package/dist/routes/terminal/trace.d.ts +1 -1
  61. package/dist/routes/terminal/types.d.ts +2 -2
  62. package/dist/run/run.d.ts +55 -1
  63. package/dist/run/run.js +38 -2
  64. package/dist/run/secrets.d.ts +22 -0
  65. package/dist/run/secrets.js +33 -0
  66. package/dist/run/study-result.d.ts +34 -0
  67. package/dist/run/study-result.js +16 -0
  68. package/dist/study/requirements.d.ts +60 -5
  69. package/dist/study/requirements.js +88 -2
  70. package/dist/study/run-study-events.d.ts +4 -3
  71. package/dist/study/run-study-events.js +9 -14
  72. package/dist/study/run-study-options.d.ts +4 -1
  73. package/dist/study/run-study-options.js +22 -32
  74. package/dist/substrates/e2b/subject-sandbox.d.ts +58 -0
  75. package/dist/substrates/e2b/subject-sandbox.js +94 -0
  76. package/docs/contracts/core.md +5 -4
  77. package/docs/contracts/feedback.md +12 -5
  78. package/package.json +1 -1
@@ -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;
@@ -1,14 +1,15 @@
1
1
  // The terminal route's dry run: a contract-only bundle with the persona and prompt digest bound,
2
2
  // published through the run scope with no sandbox, key or spend. session.ts holds the live path.
3
- import { digestText, redactText, scrubLiterals } from "../../evidence/redaction.js";
3
+ import { digestText, redactText } from "../../evidence/redaction.js";
4
4
  import { participantAssignment } from "../../study/participant-assignment.js";
5
- import { buildRunSource } from "../../run/bundle.js";
5
+ import {} from "../../run/bundle.js";
6
6
  import { judgeExecution, judgeTerminal, OUTCOME_POLICIES, resultOk } from "../../run/judge.js";
7
7
  import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
8
+ import { RunSecrets } from "../../run/secrets.js";
8
9
  import { buildTerminalProductBundle, renderTerminalReviewMarkdown } from "./bundle.js";
9
10
  import { resolveTerminalPersona, terminalPersonaRef } from "./persona.js";
10
11
  import { declaredRuntimeProvenance } from "./runtime.js";
11
- import { defaultMission, makeTerminalRunId } from "./session.js";
12
+ import { defaultMission } from "./session.js";
12
13
  import {} from "./types.js";
13
14
  import { studyResultIdentity } from "../../run/study-result.js";
14
15
  /**
@@ -18,32 +19,22 @@ import { studyResultIdentity } from "../../run/study-result.js";
18
19
  export async function runDryTerminalStudy(args) {
19
20
  const { plan, input, cwd, warnings, failed, scope } = args;
20
21
  const { product } = plan;
21
- const { evidenceMission, physicalCwd, persona } = await prepareDryPersona({
22
+ const { evidenceMission, physicalCwd, persona, secrets } = await prepareDryPersona({
22
23
  plan,
23
24
  cwd,
24
25
  env: input.env ?? process.env,
25
26
  warnings,
26
27
  });
27
- const started = await scope.startRun({
28
+ const started = await scope.startRun(plan, input, {
28
29
  cwd: physicalCwd,
29
- runId: input.runId,
30
- mintRunId: makeTerminalRunId,
31
- mode: "dry-run",
32
- study: plan.study,
33
- warnings: plan.warnings,
30
+ prefix: "terminal",
34
31
  renderReview: renderTerminalReviewMarkdown,
35
- observer: { open: input.open === true, render: input.deps?.renderObserver },
32
+ secrets,
36
33
  });
37
34
  if (!started.ok)
38
35
  return failed(started.code, started.message);
39
36
  const { run } = started;
40
- const { runId, createdAt } = run;
41
- const source = await buildRunSource({
42
- capturedAt: createdAt,
43
- cwd: physicalCwd,
44
- humanishSource: "present",
45
- packageName: "humanish",
46
- });
37
+ const { runId, createdAt, source } = run;
47
38
  const policies = plan.residual.policies;
48
39
  const judgment = judgeTerminal({ dryRun: true, participant: undefined });
49
40
  const bundle = buildTerminalProductBundle({
@@ -107,10 +98,9 @@ async function prepareDryPersona(args) {
107
98
  const { plan, cwd, env, warnings } = args;
108
99
  const { product } = plan;
109
100
  const mission = plan.mission ?? defaultMission(product.name);
110
- const knownSecretValues = [env.CODEX_API_KEY, env.OPENAI_API_KEY, env.E2B_API_KEY]
111
- .map((value) => value?.trim() ?? "")
112
- .filter((value) => value.length >= 4);
113
- const evidenceMission = participantAssignment({ mission }, scrubLiterals(knownSecretValues)).mission;
101
+ // A dry run checks no key, so it scrubs every key the live run could read.
102
+ const secrets = new RunSecrets([env.CODEX_API_KEY, env.OPENAI_API_KEY, env.E2B_API_KEY].map((value) => value?.trim() ?? ""));
103
+ const evidenceMission = participantAssignment({ mission }, secrets.scrub).mission;
114
104
  const terminalPersona = await resolveTerminalPersona({ plan, cwd, warnings });
115
105
  // The composed prompt = mission + persona + public-surface manifest. Only the author mission
116
106
  // goes plaintext into evidence (it is public-safe committed study text); the full composed prompt
@@ -121,9 +111,9 @@ async function prepareDryPersona(args) {
121
111
  productName: product.name,
122
112
  publicSurfaces: product.publicSurfaces,
123
113
  });
124
- const persona = terminalPersonaRef(terminalPersona, digestText(composedPrompt), scrubLiterals(knownSecretValues));
114
+ const persona = terminalPersonaRef(terminalPersona, digestText(composedPrompt), secrets.scrub);
125
115
  const { physicalCwd } = terminalPersona;
126
- return { evidenceMission, physicalCwd, persona };
116
+ return { evidenceMission, physicalCwd, persona, secrets };
127
117
  }
128
118
  /** The declared runtime provenance as a dry-run event; nothing is observed without a sandbox. */
129
119
  function runtimeDeclaredEvent(runtime, createdAt) {
@@ -13,7 +13,6 @@ export interface LiveFinishInputs {
13
13
  cwd: string;
14
14
  sanitize: (text: string) => string;
15
15
  nowIso: () => string;
16
- knownSecretValues: string[];
17
16
  runtimeEnv: LiveSandboxInputs["runtimeEnv"];
18
17
  runtime: LiveSandboxInputs["runtime"];
19
18
  persona: ActorPersonaRef;
@@ -14,7 +14,7 @@ import { terminalExecutionFailures, terminalStudyResult, terminalParticipantFact
14
14
  import { parseTerminalTokenUsage } from "./token-usage.js";
15
15
  import { buildTerminalActorTrace, scrubSplitKnownValues, tailOf } from "./trace.js";
16
16
  function buildLiveTrace(inputs) {
17
- const { persona, sanitize, nowIso, runtimeEnv, runtime, knownSecretValues } = inputs;
17
+ const { persona, sanitize, nowIso, runtimeEnv, runtime } = inputs;
18
18
  const { product } = inputs.plan;
19
19
  const { session } = inputs;
20
20
  const { createdAt } = inputs.run;
@@ -22,7 +22,7 @@ function buildLiveTrace(inputs) {
22
22
  // Prefix reconciliation may cut through a known key. Scrub literal values across the retained
23
23
  // chunks before any transcript/trace/event artifact is persisted. Check both each stream and
24
24
  // the combined event order that the transcript uses; either view can assemble a split value.
25
- scrubSplitKnownValues(terminalEvents, knownSecretValues, discardedPrefixes);
25
+ scrubSplitKnownValues(terminalEvents, inputs.run.secrets.values(), discardedPrefixes);
26
26
  // Build the actor trace first (the cost ledger reads its tokenUsage). A session past the cap
27
27
  // ends its transcript with what was not stored, and its trace says the same.
28
28
  const cut = inputs.recorder.transcriptCut();
@@ -53,21 +53,20 @@ import { checkLiveTerminalMachine, runLiveTerminalSession } from "./session.js";
53
53
  import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
54
54
  import { withLateScorer } from "../../study/route-inputs.js";
55
55
  import {} from "./types.js";
56
- import { studyResultIdentity } from "../../run/study-result.js";
56
+ import { refusedResult } from "../../run/study-result.js";
57
57
  /** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
58
58
  export function terminalStudyRefusal(options, refusal) {
59
59
  const { config, dryRun } = options;
60
- const refused = {
61
- ...studyResultIdentity("terminal", config.id),
62
- ok: false,
60
+ const refused = refusedResult("terminal", {
61
+ studyId: config.id,
63
62
  cwd: path.resolve(options.cwd),
63
+ error: { code: refusal.code, message: refusal.message },
64
+ }, {
64
65
  actor: refusal.actor ?? config.actor?.type ?? "",
65
66
  product: config.subject.product?.name ?? "",
66
67
  dryRun,
67
68
  runId: options.runId ?? "not-created",
68
- warnings: [],
69
- error: { code: refusal.code, message: refusal.message },
70
- };
69
+ });
71
70
  return completeRefusalAnalysis(refused, config, options);
72
71
  }
73
72
  /**
@@ -86,11 +85,11 @@ export function admitTerminalPlan(plan, input) {
86
85
  });
87
86
  }
88
87
  /** A live plan's local checks, made outside any run scope. */
89
- function admitTerminalRun(plan, input) {
88
+ async function admitTerminalRun(plan, input) {
90
89
  const warnings = [];
91
90
  if (plan.dryRun)
92
91
  return { ok: true, admitted: { plan, warnings } };
93
- const checked = checkLiveTerminalMachine(plan, input, warnings);
92
+ const checked = await checkLiveTerminalMachine(plan, input, warnings);
94
93
  if (checked.ok)
95
94
  return { ok: true, admitted: { plan, warnings, runtimeEnv: checked.runtimeEnv } };
96
95
  return {
@@ -101,16 +100,11 @@ function admitTerminalRun(plan, input) {
101
100
  /** The route's envelope for a run that stops before its bundle. */
102
101
  function terminalFailure(plan, input, warnings) {
103
102
  const cwd = path.resolve(input.cwd);
104
- return (code, message) => ({
105
- ...studyResultIdentity("terminal", plan.studyId),
106
- ok: false,
107
- cwd,
103
+ return (code, message) => refusedResult("terminal", { studyId: plan.studyId, cwd, warnings, error: { code, message } }, {
108
104
  actor: plan.actor,
109
105
  product: plan.product.name,
110
106
  dryRun: plan.dryRun,
111
107
  runId: input.runId ?? "not-created",
112
- warnings,
113
- error: { code, message },
114
108
  });
115
109
  }
116
110
  async function runTerminalPlanInScope(admitted, input, scope) {
@@ -4,14 +4,14 @@ import type { LiveTerminalAuth, LiveTerminalPlan, RunLiveTerminalSessionArgs, Te
4
4
  * E2B_API_KEY. They read only the plan and the environment, so the CLI makes them before it loads
5
5
  * a declared scorer. Returns the runtime key's command-scoped placement, or the refusal.
6
6
  */
7
- export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input: TerminalRunInput, warnings: string[]): {
7
+ export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input: TerminalRunInput, warnings: string[]): Promise<{
8
8
  readonly ok: true;
9
9
  readonly runtimeEnv: LiveTerminalAuth;
10
10
  } | {
11
11
  readonly ok: false;
12
12
  readonly code: TerminalProductStudyErrorCode;
13
13
  readonly message: string;
14
- };
14
+ }>;
15
15
  /**
16
16
  * The live in-sandbox agent session orchestrator. Enforces the 8-point safety contract by
17
17
  * construction; fails closed before any sandbox/key/spend on any precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
@@ -21,4 +21,3 @@ export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input:
21
21
  export declare function runLiveTerminalSession(args: RunLiveTerminalSessionArgs): Promise<TerminalProductStudyResult>;
22
22
  /** The default mission when the study omits one. Public-safe, product-neutral author text. */
23
23
  export declare function defaultMission(productName: string): string;
24
- export declare function makeTerminalRunId(): string;
@@ -1,9 +1,8 @@
1
- import { randomBytes, randomUUID } from "node:crypto";
2
- import { missingKeys } from "../../study/requirements.js";
1
+ import { randomUUID } from "node:crypto";
2
+ import { keysCheck } from "../../study/requirements.js";
3
3
  import { declaredRuntimeProvenance } from "./runtime.js";
4
- import { digestText, redactText, scrubLiterals } from "../../evidence/redaction.js";
5
- import { describeMissingKeys } from "../../keys/key-resolution.js";
6
- import { buildRunSource } from "../../run/bundle.js";
4
+ import { digestText, redactText } from "../../evidence/redaction.js";
5
+ import { RunSecrets } from "../../run/secrets.js";
7
6
  import { renderTerminalReviewMarkdown } from "./bundle.js";
8
7
  import { buildRuntimeAuth, buildSandboxMetadata } from "./credentials.js";
9
8
  import { resolveTerminalPersona, terminalPersonaRef } from "./persona.js";
@@ -15,7 +14,7 @@ import { finishLiveTerminalSession } from "./live-finish.js";
15
14
  * E2B_API_KEY. They read only the plan and the environment, so the CLI makes them before it loads
16
15
  * a declared scorer. Returns the runtime key's command-scoped placement, or the refusal.
17
16
  */
18
- export function checkLiveTerminalMachine(plan, input, warnings) {
17
+ export async function checkLiveTerminalMachine(plan, input, warnings) {
19
18
  const env = input.env ?? process.env;
20
19
  const { maxUsd } = plan.caps;
21
20
  // planTerminalStudy refuses a positive maxUsd without a costProbe, so one is present here.
@@ -28,13 +27,14 @@ export function checkLiveTerminalMachine(plan, input, warnings) {
28
27
  return runtimeEnv;
29
28
  // The sandbox is created with E2B_API_KEY, so a missing key is refused before the run starts.
30
29
  // The runtime key above is the plan's `key-one-of`; E2B_API_KEY is its only `key`.
31
- if (missingKeys(plan.requirements, env).length > 0) {
32
- return {
33
- ok: false,
34
- code: "HUMANISH_TERMINAL_KEYS_MISSING",
35
- message: `Live terminal-product studies need E2B_API_KEY in the environment (values are never persisted). ${describeMissingKeys(["E2B_API_KEY"], env)}`,
36
- };
37
- }
30
+ const keys = await keysCheck({
31
+ requirements: plan.requirements,
32
+ env,
33
+ code: "HUMANISH_TERMINAL_KEYS_MISSING",
34
+ need: (names) => `Live terminal-product studies need ${names} in the environment (values are never persisted).`,
35
+ });
36
+ if (keys)
37
+ return { ok: false, ...keys };
38
38
  return { ok: true, runtimeEnv };
39
39
  }
40
40
  /**
@@ -53,32 +53,26 @@ export async function runLiveTerminalSession(args) {
53
53
  const nowIso = () => new Date(now()).toISOString();
54
54
  const prepared = await prepareLivePrompt({ plan, cwd, runtimeEnv, env, warnings });
55
55
  const { mission, physicalCwd, persona, composedPrompt, verdictNonce } = prepared;
56
- const { knownSecretValues, sanitize } = prepared;
57
- const started = await scope.startRun({
56
+ const { secrets, sanitize } = prepared;
57
+ const started = await scope.startRun(plan, input, {
58
58
  cwd: physicalCwd,
59
- runId: input.runId,
60
- mintRunId: makeTerminalRunId,
61
- // This entry point is the live terminal route; its dry-run sibling is a separate function.
62
- mode: "live",
63
- study: plan.study,
64
- warnings: plan.warnings,
59
+ prefix: "terminal",
65
60
  renderReview: renderTerminalReviewMarkdown,
66
- observer: { open: input.open === true, render: deps.renderObserver },
67
61
  now,
62
+ secrets,
68
63
  });
69
64
  if (!started.ok)
70
65
  return failed(started.code, started.message);
71
66
  const { run } = started;
72
- const { runId, createdAt, paths: runPaths } = run;
73
- const source = await buildRunSource({
74
- capturedAt: createdAt,
75
- cwd: physicalCwd,
76
- humanishSource: "present",
77
- packageName: "humanish",
78
- });
67
+ const { runId, paths: runPaths, source } = run;
79
68
  const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
80
69
  // The ledgers + capture buffers, mutated through the live lifecycle.
81
- const recorder = createTerminalRecorder({ nowIso, sanitize, knownSecretValues, verdictNonce });
70
+ const recorder = createTerminalRecorder({
71
+ nowIso,
72
+ sanitize,
73
+ knownSecretValues: run.secrets.values(),
74
+ verdictNonce,
75
+ });
82
76
  const { version, model, modelSource, reasoningEffort } = plan.runtime;
83
77
  const runtime = declaredRuntimeProvenance({
84
78
  ...(version === undefined ? {} : { version }),
@@ -128,7 +122,6 @@ export async function runLiveTerminalSession(args) {
128
122
  cwd,
129
123
  sanitize,
130
124
  nowIso,
131
- knownSecretValues,
132
125
  runtimeEnv,
133
126
  runtime,
134
127
  persona,
@@ -166,17 +159,16 @@ async function prepareLivePrompt(args) {
166
159
  // anything persists (a key has no detectable "shape" if it is an arbitrary token); redactText is
167
160
  // the second pass for secret-shaped content. Applied pre-truncation so a cut can never split a
168
161
  // value past the scrubber.
169
- const knownSecretValues = [runtimeEnv.keyValue, env.E2B_API_KEY?.trim() ?? ""].filter((v) => v.length >= 4);
170
- const scrubKnownValues = scrubLiterals(knownSecretValues);
171
- const sanitize = (text) => redactText(scrubKnownValues(text));
172
- const persona = terminalPersonaRef(terminalPersona, promptDigest, scrubKnownValues);
162
+ const secrets = new RunSecrets([runtimeEnv.keyValue, env.E2B_API_KEY?.trim() ?? ""]);
163
+ const sanitize = (text) => redactText(secrets.scrub(text));
164
+ const persona = terminalPersonaRef(terminalPersona, promptDigest, secrets.scrub);
173
165
  return {
174
166
  mission,
175
167
  physicalCwd: terminalPersona.physicalCwd,
176
168
  persona,
177
169
  composedPrompt,
178
170
  verdictNonce,
179
- knownSecretValues,
171
+ secrets,
180
172
  sanitize,
181
173
  };
182
174
  }
@@ -197,7 +189,3 @@ function composeLivePrompt(args) {
197
189
  export function defaultMission(productName) {
198
190
  return `You are an autonomous agent. Discover ${productName} from its public surfaces and determine whether it can help with a durable real task. Stay within the declared no-spend caps. Leave feedback if the workflow is confusing.`;
199
191
  }
200
- export function makeTerminalRunId() {
201
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
202
- return `terminal-${stamp}-${randomBytes(4).toString("hex")}`;
203
- }
@@ -7,7 +7,7 @@ import { type CommandLogRecord, type TerminalEventRecord } from "./types.js";
7
7
  * known values before persistence without collapsing events or changing stdout/stderr ordering.
8
8
  * Work backwards through matches so edits to later text leave earlier offsets valid.
9
9
  */
10
- export declare function scrubSplitKnownValues(events: TerminalEventRecord[], knownValues: string[], discardedPrefixes: Record<"stdout" | "stderr" | "combined", string>): void;
10
+ export declare function scrubSplitKnownValues(events: TerminalEventRecord[], knownValues: readonly string[], discardedPrefixes: Record<"stdout" | "stderr" | "combined", string>): void;
11
11
  /** Redacted, ellipsis-prefixed tail of a captured stream/log for a message field. */
12
12
  export declare function tailOf(text: string): string;
13
13
  /**
@@ -8,7 +8,7 @@ import type { TerminalPlan } from "../../study/plan-types.js";
8
8
  import type { StudyDeps } from "../../study/study-deps.js";
9
9
  import type { ObserverResult } from "../../observer/render.js";
10
10
  import { type RunAdapterScore, type RunBundle, type RunFeedbackCandidate, type RunScorerProvenance } from "../../run/bundle.js";
11
- import { type StudyResultIdentity } from "../../run/study-result.js";
11
+ import { type StudyResultErrorCode, type StudyResultIdentity } from "../../run/study-result.js";
12
12
  /** Provider-neutral metadata constant: the route's non-secret tag (mirrors CUA_ACTOR_STUDY_PROVIDER_METADATA). */
13
13
  export declare const TERMINAL_PRODUCT_STUDY_PROVIDER_METADATA: {
14
14
  readonly mode: "terminal-product-lab";
@@ -172,7 +172,7 @@ export interface TerminalProductStudyResult extends AutomaticAnalysisResult, Stu
172
172
  observer?: ObserverResult;
173
173
  warnings: string[];
174
174
  error?: {
175
- code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_TERMINAL_FAILED" | "HUMANISH_TERMINAL_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_SUBJECT_INVALID" | "HUMANISH_TERMINAL_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_KEYS_MISSING" | "HUMANISH_TERMINAL_CAPS_MISSING" | "HUMANISH_TERMINAL_CAPS_INVALID" | "HUMANISH_TERMINAL_UNPRICED_CAP" | "HUMANISH_TERMINAL_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_CLEANUP_UNPROVEN" | "HUMANISH_RUN_ID_IN_USE";
175
+ code: StudyResultErrorCode | "HUMANISH_TERMINAL_FAILED" | "HUMANISH_TERMINAL_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_SUBJECT_INVALID" | "HUMANISH_TERMINAL_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_KEYS_MISSING" | "HUMANISH_TERMINAL_CAPS_MISSING" | "HUMANISH_TERMINAL_CAPS_INVALID" | "HUMANISH_TERMINAL_UNPRICED_CAP" | "HUMANISH_TERMINAL_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_CLEANUP_UNPROVEN";
176
176
  message: string;
177
177
  };
178
178
  }
package/dist/run/run.d.ts CHANGED
@@ -2,6 +2,7 @@ import { renderObserver, type ObserverResult } from "../observer/render.js";
2
2
  import { type RunBundle } from "./bundle.js";
3
3
  import { type ExecutionFailure, type ExecutionOutcome, type OutcomePolicy } from "./judge.js";
4
4
  import { type PreparedRunArtifactPaths, type RunIdInUse } from "./paths.js";
5
+ import { RunSecrets } from "./secrets.js";
5
6
  import type { RunStudyProvenance } from "./study-provenance.js";
6
7
  interface StartRunOptions {
7
8
  /** Project directory, in the form the route resolved it. */
@@ -24,6 +25,41 @@ interface StartRunOptions {
24
25
  now?: (() => number) | undefined;
25
26
  /** Warnings about the study's own fields. Every bundle write records each as a warn event. */
26
27
  warnings?: readonly string[] | undefined;
28
+ /** The route's known values, created before the run so a refusal before it is scrubbed too. */
29
+ secrets?: RunSecrets | undefined;
30
+ }
31
+ /** The prefix of the run ids each recorded route mints. Preview mints its own `dryrun-` ids. */
32
+ type RunIdPrefix = "cua" | "concurrent-shared-world" | "scripted" | "terminal";
33
+ /** What a recorded route's plan holds for its run's start. */
34
+ interface StartRunPlan {
35
+ readonly dryRun: boolean;
36
+ readonly study?: RunStudyProvenance | undefined;
37
+ readonly warnings?: readonly string[] | undefined;
38
+ }
39
+ /** What a recorded route's input holds for its run's start. */
40
+ interface StartRunInput {
41
+ /** Caller-supplied id (`--run-id`, library `runId`); one is minted from the prefix when absent. */
42
+ readonly runId?: string | undefined;
43
+ /** Open the Observer page once it renders. */
44
+ readonly open?: boolean | undefined;
45
+ /** `renderObserver` is the seam FinishedRun.renderObserver calls. */
46
+ readonly deps?: {
47
+ readonly renderObserver?: typeof renderObserver | undefined;
48
+ } | undefined;
49
+ }
50
+ /** What a recorded route passes to startRun besides its plan and input. */
51
+ interface RouteRunOptions {
52
+ /** Project directory, in the form the route resolved it. */
53
+ cwd: string;
54
+ prefix: RunIdPrefix;
55
+ /** review.md for the published bundle. */
56
+ renderReview: (bundle: RunBundle) => string;
57
+ /** `none` when the route creates no sandbox for this run; status.json records it for reclaim. */
58
+ sandboxes?: "none" | undefined;
59
+ /** Clock for `createdAt` and the latest pointer. The minted id reads the wall clock. */
60
+ now?: (() => number) | undefined;
61
+ /** The route's known values, created before the run so a refusal before it is scrubbed too. */
62
+ secrets?: RunSecrets | undefined;
27
63
  }
28
64
  /** How a run ended as an execution, as run.json records it: the result's ok and execution outcome. */
29
65
  interface RecordedOutcome {
@@ -46,6 +82,8 @@ interface Run {
46
82
  readonly study?: RunStudyProvenance;
47
83
  /** Routes write their evidence files through these and hand them to participants. */
48
84
  readonly paths: PreparedRunArtifactPaths;
85
+ /** The literal values the route scrubs from this run's evidence; a value found later is added here. */
86
+ readonly secrets: RunSecrets;
49
87
  /**
50
88
  * Publish an in-progress bundle: run.json, review.json, review.md, events.ndjson and
51
89
  * observer/observer-data.json, plus the latest pointer until one pointer write has succeeded,
@@ -74,11 +112,27 @@ interface Run {
74
112
  */
75
113
  finish(bundle: RunBundle, outcome: FinishOutcome): Promise<FinishedRun>;
76
114
  }
115
+ /** A recorded route's run, with the source its bundle records. */
116
+ interface RecordedRun extends Run {
117
+ /** The package and the project's git state, captured once the run exists, at its createdAt. */
118
+ readonly source: RunBundle["source"];
119
+ }
77
120
  export interface RunScope {
121
+ /**
122
+ * Start a recorded route's run from its plan and input: the input's run id or one minted from
123
+ * `prefix`, the plan's mode, study and warnings, and an Observer that opens when the input says
124
+ * `open` and renders through the input's `deps.renderObserver`. Then capture the run's source.
125
+ * The run starts as the form below does.
126
+ */
127
+ startRun(plan: StartRunPlan, input: StartRunInput, options: RouteRunOptions): Promise<{
128
+ ok: true;
129
+ run: RecordedRun;
130
+ } | RunIdInUse>;
78
131
  /**
79
132
  * Create `.humanish/runs/<id>` exclusively, begin status.json and wait for its first write, so
80
133
  * a run that got past this point has a status record before any sandbox exists. An id whose
81
- * directory exists is refused before any write. A scope starts at most one run.
134
+ * directory exists is refused before any write. A scope starts at most one run. Preview calls
135
+ * this form, since it builds its source before the run and mints its own id.
82
136
  */
83
137
  startRun(options: StartRunOptions): Promise<{
84
138
  ok: true;