humanish 0.80.0 → 0.82.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 (69) hide show
  1. package/README.md +121 -615
  2. package/dist/actor-contract.d.ts +30 -0
  3. package/dist/actor-contract.js.map +1 -1
  4. package/dist/chrome-cdp-probe.js +13 -0
  5. package/dist/chrome-cdp-probe.js.map +1 -1
  6. package/dist/computer-use.d.ts +13 -3
  7. package/dist/computer-use.js +179 -15
  8. package/dist/computer-use.js.map +1 -1
  9. package/dist/cua-actor-lab.d.ts +20 -8
  10. package/dist/cua-actor-lab.js +154 -49
  11. package/dist/cua-actor-lab.js.map +1 -1
  12. package/dist/e2b-desktop-executor.d.ts +9 -1
  13. package/dist/e2b-desktop-executor.js +67 -4
  14. package/dist/e2b-desktop-executor.js.map +1 -1
  15. package/dist/e2b-desktop-launch.d.ts +36 -1
  16. package/dist/e2b-desktop-launch.js +88 -1
  17. package/dist/e2b-desktop-launch.js.map +1 -1
  18. package/dist/e2b-desktop-resources.d.ts +11 -0
  19. package/dist/e2b-desktop-resources.js +33 -0
  20. package/dist/e2b-desktop-resources.js.map +1 -0
  21. package/dist/e2b-desktop-screenshot-cleanup.d.ts +15 -0
  22. package/dist/e2b-desktop-screenshot-cleanup.js +67 -0
  23. package/dist/e2b-desktop-screenshot-cleanup.js.map +1 -0
  24. package/dist/e2b-terminal-lab.d.ts +5 -2
  25. package/dist/e2b-terminal-lab.js +238 -86
  26. package/dist/e2b-terminal-lab.js.map +1 -1
  27. package/dist/first-run-path.js +2 -2
  28. package/dist/first-run-path.js.map +1 -1
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/lab-config.d.ts +15 -14
  33. package/dist/lab-config.js +11 -2
  34. package/dist/lab-config.js.map +1 -1
  35. package/dist/observer-data.d.ts +2 -0
  36. package/dist/observer-data.js +10 -1
  37. package/dist/observer-data.js.map +1 -1
  38. package/dist/openai-responses-cu.js +76 -49
  39. package/dist/openai-responses-cu.js.map +1 -1
  40. package/dist/pricing.d.ts +19 -1
  41. package/dist/pricing.js +43 -10
  42. package/dist/pricing.js.map +1 -1
  43. package/dist/program.js +17 -8
  44. package/dist/program.js.map +1 -1
  45. package/dist/redaction.js +9 -2
  46. package/dist/redaction.js.map +1 -1
  47. package/dist/run.d.ts +14 -2
  48. package/dist/run.js.map +1 -1
  49. package/dist/terminal-node-bootstrap.d.ts +4 -0
  50. package/dist/terminal-node-bootstrap.js +58 -0
  51. package/dist/terminal-node-bootstrap.js.map +1 -0
  52. package/dist/terminal-runtime-auth.d.ts +13 -0
  53. package/dist/terminal-runtime-auth.js +24 -0
  54. package/dist/terminal-runtime-auth.js.map +1 -0
  55. package/dist/terminal-runtime.d.ts +16 -0
  56. package/dist/terminal-runtime.js +42 -0
  57. package/dist/terminal-runtime.js.map +1 -0
  58. package/dist/terminal-token-usage.d.ts +2 -1
  59. package/dist/terminal-token-usage.js +3 -2
  60. package/dist/terminal-token-usage.js.map +1 -1
  61. package/docs/architecture/actor-contract.md +22 -0
  62. package/docs/architecture/state-driven-executor.md +10 -1
  63. package/docs/architecture/terminal-product-lane.md +142 -6
  64. package/docs/contracts/feedback.md +14 -0
  65. package/docs/contracts/run-bundle.md +5 -1
  66. package/docs/contracts/schemas.md +17 -8
  67. package/docs/goals/current.md +61 -13
  68. package/docs/ramp/README.md +1 -1
  69. package/package.json +4 -2
@@ -1,5 +1,5 @@
1
1
  // The terminal-product lab backend: a real autonomous agent (Codex) studying a CLI/product from
2
- // PUBLIC SURFACES ONLY, running INSIDE an E2B shell with command-scoped runtime auth, capturing
2
+ // PUBLIC SURFACES ONLY, running INSIDE an E2B shell with explicit runtime-auth placement, capturing
3
3
  // its non-interactive exec output (stdin disabled) as a redacted event stream + normalized
4
4
  // transcript, capped at no-spend, emitting durable terminal/substrate/cost/no-spend/cleanup/
5
5
  // intervention proof. Mirrors cua-actor-lab.ts / scripted-browser-lab.ts.
@@ -12,10 +12,10 @@
12
12
  //
13
13
  // THE SAFETY CONTRACT (docs/goals/terminal-product-lane/goal.md) is enforced BY CONSTRUCTION here
14
14
  // and CHECKED by the verifier (run.ts validateTerminalProductEvidence):
15
- // 1. COMMAND-SCOPED KEY. The runtime LLM key is injected ONLY into the per-command `envs` of the
16
- // `codex exec` invocation (commands.run({envs})), NEVER Sandbox.create({envs}) driven off
17
- // the registered actor's keyPlacement: "in-sandbox-command-scoped" capability (engine-
18
- // enforced: a terminal actor lacking that metadata FAILS CLOSED before any sandbox exists).
15
+ // 1. EXPLICIT KEY PLACEMENT. openai-env (default) injects the raw runtime key command-scoped,
16
+ // NEVER Sandbox.create({envs}). Opt-in openai-egress sends it only in the host-side E2B
17
+ // header transform and passes an inert command placeholder. The proxy is spendable by every
18
+ // sandbox process from creation; this protects the raw key, not provider spending.
19
19
  // 2. FAIL-CLOSED CAP. The live key is never exercised without scenario.caps in force: maxUsd
20
20
  // (default/require 0 = no-spend) + maxMinutes (wall-clock kill of the codex command).
21
21
  // 3. PUBLIC SURFACES ONLY. The mission references only subject.product.publicSurfaces + the
@@ -36,8 +36,11 @@
36
36
  // Sandbox.getInfo(id) when the SDK exposes it (a thrown SandboxNotFoundError means gone).
37
37
  // humanish NEVER calls Sandbox.list to prove cleanup, so a shared operator key never reaches a
38
38
  // sandbox it did not create. A live run that cannot prove teardown fails closed.
39
- import { randomBytes, randomUUID } from "node:crypto";
39
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
40
+ import { TERMINAL_NODE_BOOTSTRAP_COMMAND } from "./terminal-node-bootstrap.js";
40
41
  import { describeTokenUsage, parseTerminalTokenUsage } from "./terminal-token-usage.js";
42
+ import { buildRuntimeExecPrefix, buildRuntimeVersionCommand, declaredRuntimeProvenance, isExactRuntimeVersion, parseTerminalRuntimeVersion, TERMINAL_RUNTIME_VERSION_TIMEOUT_MS } from "./terminal-runtime.js";
43
+ import { isReasoningEffort } from "./reasoning-effort.js";
41
44
  import { readFile, realpath, stat } from "node:fs/promises";
42
45
  import path from "node:path";
43
46
  import { parse as parseYaml } from "yaml";
@@ -45,6 +48,7 @@ import { beginRunStatus, withRunStatusScope } from "./run-status.js";
45
48
  import { ACTOR_TRACE_SCHEMA, TERMINAL_AGENT_CAPABILITIES } from "./actor-contract.js";
46
49
  import { actorRegistry, isTerminalActorDescriptor } from "./actor-registry.js";
47
50
  import { toErrorMessage } from "./command-failure.js";
51
+ import { buildOpenAiEgressNetwork, E2B_SYSTEM_CA_BUNDLE, OPENAI_EGRESS_PLACEHOLDER } from "./terminal-runtime-auth.js";
48
52
  import { isSandboxNotFoundError, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
49
53
  import { renderObserver } from "./observer.js";
50
54
  import { parseResolvedPersona, personaToDirectives, renderPersonaPromptSection } from "./persona.js";
@@ -74,24 +78,8 @@ const UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
74
78
  // Server-side reclamation buffer past the codex command's own wall-clock (caps.maxMinutes) kill.
75
79
  const SANDBOX_TIMEOUT_BUFFER_MS = 5 * 60_000;
76
80
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
77
- // The runtime-bootstrap step (ensure Node/npm) can run an apt-get install; the SDK's commands.run
78
- // timeoutMs default (60s) is far too short for that, so this step gets an explicit generous budget.
81
+ // Allow the pinned runtime download and install enough time while retaining a finite deadline.
79
82
  const RUNTIME_BOOTSTRAP_TIMEOUT_MS = 300_000;
80
- // UNKEYED (no envs) shell command that ensures Node/npm are present before the keyed codex exec.
81
- // Reuses the oss-meta-lab.ts ensure_node() shape: check node's major version, else install
82
- // Node 22 via NodeSource plus passwordless sudo (the stock @e2b/desktop image ships neither codex
83
- // nor a recent Node, per issue #159). A final presence check makes the whole command exit non-zero
84
- // (so the bootstrap step fails closed) if the install still leaves node/npm missing.
85
- const RUNTIME_BOOTSTRAP_COMMAND = [
86
- `node_major=0`,
87
- `if command -v node >/dev/null 2>&1; then node_major=$(node -e 'console.log(Number(process.versions.node.split(".")[0]))' 2>/dev/null || echo 0); fi`,
88
- `if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 && [ "$node_major" -ge 20 ]; then exit 0; fi`,
89
- `sudo -n apt-get update`,
90
- `sudo -n apt-get install -y ca-certificates curl gnupg`,
91
- `curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -n -E bash -`,
92
- `sudo -n apt-get install -y nodejs`,
93
- `command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1`
94
- ].join(" && ");
95
83
  // How much of a captured stream / log tail rides a (redacted) message field.
96
84
  const TAIL_CHARS = 2000;
97
85
  // Hard cap on the retained event-stream + transcript size, so a runaway agent cannot balloon the
@@ -135,6 +123,13 @@ async function runTerminalProductLabInScope(options) {
135
123
  if (!descriptor || !isTerminalActorDescriptor(descriptor)) {
136
124
  return failed("HUMANISH_TERMINAL_LAB_ACTOR_UNSUPPORTED", `actors[0].type "${actorType}" is not a registered terminal actor.`);
137
125
  }
126
+ const runtimeVersion = config.execution?.runtime?.version;
127
+ const actor = config.actors[0];
128
+ if ((config.execution?.runtime !== undefined && !isExactRuntimeVersion(runtimeVersion))
129
+ || (actor?.model !== undefined && (typeof actor.model !== "string" || actor.model.trim().length === 0))
130
+ || (actor?.reasoningEffort !== undefined && !isReasoningEffort(actor.reasoningEffort))) {
131
+ return failed("HUMANISH_TERMINAL_LAB_FAILED", "Terminal runtime settings require an exact Codex version, a nonempty model when declared, and a supported reasoning-effort value.");
132
+ }
138
133
  // Re-enforce the subject shape at the engine (the parser rejects these too, but this is exported
139
134
  // npm surface). A terminal-product subject MUST declare product.name + public surfaces.
140
135
  if (!product || !product.name || product.publicSurfaces.length === 0) {
@@ -207,6 +202,17 @@ async function runTerminalProductLabInScope(options) {
207
202
  runId,
208
203
  source
209
204
  });
205
+ bundle.events.push({
206
+ id: "event-terminal-runtime-declared",
207
+ at: createdAt,
208
+ level: "info",
209
+ type: "terminal-lab.runtime.declared",
210
+ message: redactText(JSON.stringify(declaredRuntimeProvenance({
211
+ ...(config.execution?.runtime?.version === undefined ? {} : { version: config.execution.runtime.version }),
212
+ ...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model }),
213
+ ...(config.actors[0]?.reasoningEffort === undefined ? {} : { reasoningEffort: config.actors[0].reasoningEffort })
214
+ })))
215
+ });
210
216
  await writeContainedOutputFile(runPaths, "run.json", `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
211
217
  // Finalize identity+liveness from the bundle just written; a throw before this leaves the record
212
218
  // stale, which reads as interrupted rather than as a false outcome (#455).
@@ -416,17 +422,12 @@ function roundUsd(value) {
416
422
  return Math.round(value * 1_000_000) / 1_000_000;
417
423
  }
418
424
  /**
419
- * Build the per-command runtime-auth env from a DENY-BY-DEFAULT ALLOWLIST containing ONLY the
420
- * declared runtime key (safety contract item 4). The key NAME is derived from the actor's
421
- * keyPlacement capability plus the declared runtimeAuth channel, not a hardcoded string the caller
422
- * can widen. Banned credential names (GitHub/payment/deploy/db/media) are excluded by construction
423
- * AND guarded: if a banned name is ever requested the lane fails closed. Returns the allowlisted
424
- * env (values from `env`) and the resolved key name, or a structured failure.
425
- *
426
- * Engine-enforced placement (safety contract item 1): the key is only ever returned as a
427
- * COMMAND-scoped env here; the caller passes it to commands.run({envs}), never Sandbox.create.
425
+ * Resolve the runtime key on the host. Legacy openai-env passes it command-scoped; openai-egress
426
+ * returns an inert command env while retaining the actual value for the external transform and
427
+ * literal redaction. Only CODEX_API_KEY/OPENAI_API_KEY are accepted as sources. No other operator
428
+ * credential is forwarded. The real value must never be logged or persisted in either mode.
428
429
  */
429
- function buildCommandScopedRuntimeEnv(args) {
430
+ function buildRuntimeAuth(args) {
430
431
  // The "openai-env" channel accepts CODEX_API_KEY or OPENAI_API_KEY as the runtime key SOURCE
431
432
  // name, read in this preference order. CODEX_API_KEY is preferred: the official Codex docs
432
433
  // (developers.openai.com/codex/noninteractive) document it as the channel for a SINGLE codex exec
@@ -458,7 +459,7 @@ function buildCommandScopedRuntimeEnv(args) {
458
459
  return {
459
460
  ok: false,
460
461
  code: "HUMANISH_TERMINAL_LAB_RUNTIME_AUTH_MISSING",
461
- message: `Live terminal-product labs declare runtimeAuth "${String(args.runtimeAuth)}" and need ${ALLOWED_RUNTIME_KEY_NAMES.join(" or ")} in the environment (pass via --env-file; the value is injected ONLY into the command-scoped codex invocation and is never persisted).`
462
+ message: `Live terminal-product labs declare runtimeAuth "${String(args.runtimeAuth)}" and need ${ALLOWED_RUNTIME_KEY_NAMES.join(" or ")} in the environment (pass via --env-file; the selected auth mode places the value in command-scoped env or an external E2B header transform; the value is never persisted).`
462
463
  };
463
464
  }
464
465
  const keyValue = args.env[keyName];
@@ -466,9 +467,15 @@ function buildCommandScopedRuntimeEnv(args) {
466
467
  // GITHUB_TOKEN/GH_TOKEN, no payment/deploy/db/media key, excluded by construction. When the
467
468
  // SOURCE was OPENAI_API_KEY, the SAME value is also injected as CODEX_API_KEY so codex exec's
468
469
  // documented single-invocation auth channel is populated either way (see the comment above).
469
- const envs = keyName === "OPENAI_API_KEY" ? { CODEX_API_KEY: keyValue, OPENAI_API_KEY: keyValue } : { [keyName]: keyValue };
470
+ const mode = args.runtimeAuth ?? "openai-env";
471
+ const envs = mode === "openai-egress"
472
+ // Codex documents this verified-TLS trust channel. The stock image's default OpenSSL CA
473
+ // file can be absent even though E2B has installed its proxy CA in the system bundle.
474
+ ? { CODEX_API_KEY: OPENAI_EGRESS_PLACEHOLDER, CODEX_CA_CERTIFICATE: E2B_SYSTEM_CA_BUNDLE }
475
+ : keyName === "OPENAI_API_KEY" ? { CODEX_API_KEY: keyValue, OPENAI_API_KEY: keyValue } : { [keyName]: keyValue };
470
476
  return {
471
477
  ok: true,
478
+ mode,
472
479
  envs,
473
480
  keyName,
474
481
  keyValue
@@ -525,21 +532,20 @@ async function runLiveTerminalSession(args) {
525
532
  const env = hooks.env ?? process.env;
526
533
  const now = hooks.now ?? (() => Date.now());
527
534
  const nowIso = () => new Date(now()).toISOString();
528
- // --- Safety contract item 1: ENGINE-ENFORCED command-scoped key placement. ---
529
- // Drive the placement off the registered actor's keyPlacement CAPABILITY, not a code convention.
530
- // A terminal actor that does not declare in-sandbox-command-scoped placement FAILS CLOSED here,
531
- // before any sandbox exists — the engine refuses to guess where the key goes.
535
+ // Check the registered terminal actor's default placement contract before launching. The
536
+ // explicit openai-egress mode overrides the resolved trace's placement to external; registry
537
+ // metadata continues to describe the compatible openai-env default.
532
538
  const descriptor = actorRegistry[descriptorId];
533
539
  const keyPlacement = descriptor?.capabilities.keyPlacement;
534
540
  if (keyPlacement !== "in-sandbox-command-scoped") {
535
- return failed("HUMANISH_TERMINAL_LAB_KEYPLACEMENT_INVALID", `Terminal actor "${descriptorId}" must declare keyPlacement "in-sandbox-command-scoped" for the live lane (got "${String(keyPlacement)}"). The engine routes the runtime key by this capability; without it the lane cannot place the key safely and fails closed.`, { actor: descriptorId });
541
+ return failed("HUMANISH_TERMINAL_LAB_KEYPLACEMENT_INVALID", `Terminal actor "${descriptorId}" must declare keyPlacement "in-sandbox-command-scoped" for the live lane (got "${String(keyPlacement)}"). The engine requires this registered default before applying the declared runtime-auth mode.`, { actor: descriptorId });
536
542
  }
537
543
  // --- Safety contract item 2: a fail-closed cap MUST be in force before the live key runs. ---
538
544
  const caps = config.scenario?.caps;
539
545
  const maxUsd = caps?.maxUsd;
540
546
  const maxMinutes = caps?.maxMinutes;
541
547
  if (caps === undefined || maxUsd === undefined || maxMinutes === undefined || maxMinutes <= 0) {
542
- return failed("HUMANISH_TERMINAL_LAB_CAPS_MISSING", "A live terminal-product run passes a real key to the in-sandbox agent command and so REQUIRES a fail-closed cap: scenario.caps with maxUsd (0 = no-spend) and a positive maxMinutes (the codex command's wall-clock kill). The live key is never exercised without a cap in force.", { actor: descriptorId });
548
+ return failed("HUMANISH_TERMINAL_LAB_CAPS_MISSING", "A live terminal-product run grants provider access to the in-sandbox agent and so REQUIRES a fail-closed cap: scenario.caps with maxUsd (0 = no-spend) and a positive maxMinutes (the codex command's wall-clock kill). The live key is never exercised without a cap in force.", { actor: descriptorId });
543
549
  }
544
550
  // maxUsd is ENFORCED fail-closed against the cost ledger (evaluateCapsAgainstLedger after the
545
551
  // session), not advisory. A positive maxUsd is permitted, but core still has no
@@ -552,7 +558,7 @@ async function runLiveTerminalSession(args) {
552
558
  warnings.push(`scenario.caps.maxUsd=${maxUsd} declares a non-zero spend budget. maxUsd is enforced fail-closed against the cost ledger, but core meters only the provider line from tokenUsage; product/media/payment stay null (UNMEASURED, never guessed zero) unless an adapter supplies those signals through costProbe. The no-spend proof reports unmeasured lines honestly.`);
553
559
  }
554
560
  // --- Safety contract item 4: deny-by-default credentials; build the command-scoped allowlist. ---
555
- const runtimeEnv = buildCommandScopedRuntimeEnv({ runtimeAuth: config.execution?.runtimeAuth, env });
561
+ const runtimeEnv = buildRuntimeAuth({ runtimeAuth: config.execution?.runtimeAuth, env });
556
562
  if (!runtimeEnv.ok) {
557
563
  return failed(runtimeEnv.code, runtimeEnv.message, { actor: descriptorId });
558
564
  }
@@ -610,6 +616,10 @@ async function runLiveTerminalSession(args) {
610
616
  const lifecycle = [];
611
617
  const commandLog = [];
612
618
  const terminalEvents = [];
619
+ // Capture may stop inside a known key. Keep only enough following characters to finish the
620
+ // cross-chunk redaction below; this overlap is never added to terminal events/artifacts.
621
+ const discardedPrefixes = { stdout: "", stderr: "", combined: "" };
622
+ const maxDiscardedPrefixChars = Math.max(0, ...knownSecretValues.map((value) => value.length - 1));
613
623
  const interventions = []; // ALWAYS empty while no assisted-input path ships.
614
624
  let transcriptBytes = 0;
615
625
  let cleanup = { killed: false, remaining: -1, reason: "teardown not reached" };
@@ -617,12 +627,49 @@ async function runLiveTerminalSession(args) {
617
627
  lifecycle.push({ at: nowIso(), event, message: sanitize(message) });
618
628
  };
619
629
  const appendTerminalChunk = (stream, raw) => {
620
- if (transcriptBytes >= MAX_TRANSCRIPT_BYTES)
630
+ if (transcriptBytes >= MAX_TRANSCRIPT_BYTES) {
631
+ for (const order of [stream, "combined"]) {
632
+ const remaining = maxDiscardedPrefixChars - discardedPrefixes[order].length;
633
+ if (remaining > 0)
634
+ discardedPrefixes[order] += raw.slice(0, remaining);
635
+ }
621
636
  return;
637
+ }
622
638
  transcriptBytes += Buffer.byteLength(raw, "utf8");
623
639
  // Scrub THEN redact at the SOURCE — raw bytes never leave this function (safety contract item 5).
624
640
  terminalEvents.push({ at: nowIso(), stream, chunk: sanitize(raw) });
625
641
  };
642
+ // E2B can stream every byte through callbacks AND return the same complete output (#667).
643
+ // Track transport delivery, independently per stream, rather than deduplicating participant
644
+ // lines or equal usage records. Hash raw callback bytes before redaction/truncation so the
645
+ // comparison cannot confuse two values that redact identically or lose capped-away delivery.
646
+ // Delivery tracking retains only counts and hashes; payloads still pass the artifact sanitizer.
647
+ const streamedOutput = {
648
+ stdout: { bytes: 0, hash: createHash("sha256") },
649
+ stderr: { bytes: 0, hash: createHash("sha256") }
650
+ };
651
+ const recordStreamedTerminalChunk = (stream, raw) => {
652
+ streamedOutput[stream].bytes += Buffer.byteLength(raw, "utf8");
653
+ streamedOutput[stream].hash.update(raw, "utf8");
654
+ appendTerminalChunk(stream, raw);
655
+ };
656
+ const appendReturnedTerminalOutput = (stream, raw) => {
657
+ const delivered = streamedOutput[stream];
658
+ const returned = Buffer.from(raw, "utf8");
659
+ if (delivered.bytes > 0 && returned.length >= delivered.bytes) {
660
+ const returnedPrefixHash = createHash("sha256").update(returned.subarray(0, delivered.bytes)).digest("hex");
661
+ if (returnedPrefixHash === delivered.hash.copy().digest("hex")) {
662
+ // A complete replay adds nothing; a partly streamed prefix keeps only the unseen tail.
663
+ const suffix = returned.subarray(delivered.bytes).toString("utf8");
664
+ if (suffix)
665
+ appendTerminalChunk(stream, suffix);
666
+ return;
667
+ }
668
+ }
669
+ // Older/final-only SDK delivery, or output that does not match the streamed prefix: keep it.
670
+ // Guessing at overlap here could erase legitimate repeated participant text.
671
+ appendTerminalChunk(stream, raw);
672
+ };
626
673
  let sandbox;
627
674
  let sandboxModule;
628
675
  let sandboxId;
@@ -631,6 +678,11 @@ async function runLiveTerminalSession(args) {
631
678
  let sessionReason = "live terminal-product session did not start";
632
679
  let sessionError;
633
680
  let timedOut = false;
681
+ const runtime = declaredRuntimeProvenance({
682
+ ...(config.execution?.runtime?.version === undefined ? {} : { version: config.execution.runtime.version }),
683
+ ...(config.actors[0]?.model === undefined ? {} : { model: sanitize(config.actors[0].model) }),
684
+ ...(config.actors[0]?.reasoningEffort === undefined ? {} : { reasoningEffort: config.actors[0].reasoningEffort })
685
+ });
634
686
  recordLifecycle("terminal-lab.run.created", `Created live terminal-product run ${runId} (actor ${descriptorId}, product ${product.name}). Caps: maxUsd=${maxUsd}, maxMinutes=${maxMinutes}. Subject provenance UNPINNED (public surfaces only).`);
635
687
  const requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
636
688
  const wallClockMs = maxMinutes * 60_000;
@@ -639,50 +691,48 @@ async function runLiveTerminalSession(args) {
639
691
  try {
640
692
  sandboxModule = await (hooks.loadModule ?? loadE2BDesktopModule)();
641
693
  await validatePreparedRunArtifactPaths(runPaths);
642
- // SAFETY CONTRACT ITEM 1 (enforced HERE): Sandbox.create carries metadata (positive allowlist)
643
- // + lifecycle kill-on-timeout, and DELIBERATELY NO `envs` the runtime key is NEVER passed
644
- // sandbox-global. It is injected ONLY into the per-command codex `envs` below.
694
+ // No sandbox-global env in either mode. In openai-egress, only this host-side SDK request
695
+ // carries the real runtime key; participant commands receive an inert placeholder. The proxy
696
+ // capability is available from sandbox creation, including during bootstrap/product setup.
697
+ const routing = egressAllow === undefined ? undefined : { allowOut: egressAllow, denyOut: ["0.0.0.0/0"] };
698
+ const network = runtimeEnv.mode === "openai-egress"
699
+ ? buildOpenAiEgressNetwork(runtimeEnv.keyValue, routing)
700
+ : routing;
645
701
  sandbox = await sandboxModule.Sandbox.create({
646
702
  apiKey: e2bApiKey,
647
703
  requestTimeoutMs,
648
704
  timeoutMs: sandboxTimeoutMs,
649
705
  metadata,
650
- // Egress allowlist when the lab declares one (#538). This is the ONE bound on the injected
651
- // runtime key that does not depend on the participant's cooperation: codex spawns the
652
- // participant's shell as a child, so it inherits that key and can spend it anywhere it can
653
- // reach. It cannot reach a host that is not on this list. Absent means unrestricted, the
654
- // historical default, because a wrong host list fails studies in confusing ways.
655
- ...(egressAllow === undefined
656
- ? {}
657
- : { network: { allowOut: egressAllow, denyOut: ["0.0.0.0/0"] } }),
706
+ ...(network === undefined ? {} : { network }),
658
707
  lifecycle: { onTimeout: "kill" }
659
- // NOTE: no `envs` key — see the credential boundary above. (A sandbox-global key would leak
660
- // into every process in the sandbox; command-scoped bounds it to the codex invocation.)
661
708
  });
662
709
  await validatePreparedRunArtifactPaths(runPaths);
663
710
  sandboxId = sandbox.sandboxId;
664
711
  // #358 salvage: durable id receipt the moment the sandbox exists (reclaim by exact id).
665
712
  await appendSandboxReceipt(runPaths, { at: nowIso(), laneId: "terminal", sandboxId, timeoutMs: sandboxTimeoutMs });
666
- recordLifecycle("terminal-lab.sandbox.created", `E2B shell sandbox ${sandboxId} created with positive-allowlist metadata and kill-on-timeout; NO sandbox-global env (runtime key is command-scoped).`);
713
+ recordLifecycle("terminal-lab.sandbox.created", `E2B shell sandbox ${sandboxId} created with positive-allowlist metadata and kill-on-timeout; NO sandbox-global env.`);
667
714
  // The allowlist is evidence: a reader of the ledger can see exactly what the participant was
668
715
  // able to reach, without the ledger carrying any secret.
669
716
  recordLifecycle("terminal-lab.egress.policy", egressAllow === undefined
670
- ? "Egress UNRESTRICTED (no execution.egressAllow declared): the injected runtime key can reach any host."
671
- : `Egress DENIED except ${egressAllow.length} declared host(s): ${egressAllow.join(", ")}.`);
672
- // Readiness: a tiny in-sandbox probe (no key) confirms the shell answers before the keyed run.
717
+ ? "Egress UNRESTRICTED (no execution.egressAllow declared)."
718
+ : `Egress routing allowlist: ${egressAllow.length} declared host(s): ${egressAllow.join(", ")}; deny-all fallback. Domain routing is not strict destination isolation on shared infrastructure.`);
719
+ recordLifecycle("terminal-lab.runtime-auth", runtimeEnv.mode === "openai-egress"
720
+ ? "Runtime auth openai-egress: raw key remains outside the sandbox in the api.openai.com HTTPS Authorization transform; Codex receives an inert CODEX_API_KEY placeholder and the default OpenAI endpoint. Every sandbox process, including bootstrap/setup, can spend via this proxy; no added routing restriction or provider spending limit."
721
+ : `Runtime auth openai-env: raw key from ${runtimeEnv.keyName} is passed command-scoped to Codex and inherited by its child processes.`);
722
+ if (runtimeEnv.mode === "openai-egress") {
723
+ warnings.push("openai-egress keeps the raw runtime key outside the sandbox, but every sandbox process can spend through the api.openai.com proxy from creation until teardown. It adds no egress restriction or provider-enforced budget; extra provider calls may be absent from the Codex usage ledger.");
724
+ }
725
+ // Readiness: a tiny probe receives no runtime env; openai-egress's proxy is already available.
673
726
  const ready = await sandbox.commands.run(`mkdir -p ${SANDBOX_WORKDIR} && echo HUMANISH_SHELL_READY`, { requestTimeoutMs });
674
727
  recordLifecycle("terminal-lab.sandbox.ready", `Shell readiness probe exit=${ready.exitCode ?? "null"}; workdir ${SANDBOX_WORKDIR} prepared.`);
675
- // --- Runtime bootstrap: ensure Node/npm are present (UNKEYED, no envs) before the keyed exec. ---
676
- // The stock @e2b/desktop image does not ship a recent Node (issue #159); codex is now invoked
677
- // via `npx` (buildCodexExecCommand), which needs Node/npm on PATH. Reuses the proven
678
- // oss-meta-lab.ts ensure_node() shape: a node major-version check, else install Node 22 via
679
- // NodeSource plus passwordless sudo. UNKEYED: no runtime key touches this step. An apt-get
680
- // install can exceed the SDK's default 60s commands.run timeout, so this step gets an
681
- // explicit, generous timeoutMs (requestTimeoutMs is passed through unchanged, as everywhere else).
728
+ // --- Runtime bootstrap: no runtime env; openai-egress proxy capability is already available. ---
729
+ // The stock desktop needs Node/npm on PATH before npx can run Codex. Reuse a working
730
+ // installation or install the pinned official binary after checksum verification (#674).
731
+ // No raw runtime key touches this step; the egress proxy, when selected, is already available.
682
732
  const bootstrapStartedAt = now();
683
733
  let bootstrapError;
684
734
  try {
685
- const bootstrap = await sandbox.commands.run(RUNTIME_BOOTSTRAP_COMMAND, {
735
+ const bootstrap = await sandbox.commands.run(TERMINAL_NODE_BOOTSTRAP_COMMAND, {
686
736
  requestTimeoutMs,
687
737
  timeoutMs: RUNTIME_BOOTSTRAP_TIMEOUT_MS
688
738
  });
@@ -707,7 +757,34 @@ async function runLiveTerminalSession(args) {
707
757
  sessionReason = `runtime bootstrap could not ensure Node/npm before codex exec: ${sessionError}`;
708
758
  }
709
759
  else if (await (async () => {
710
- // --- Optional product setup (UNKEYED, no envs), before the keyed exec. ---
760
+ // Observe the executable without command-scoped auth, then use only that exact version.
761
+ // The SDK bounds the request and command; version failures reach the owned cleanup path.
762
+ try {
763
+ const versionProbe = await sandbox.commands.run(buildRuntimeVersionCommand(config.execution?.runtime?.version), {
764
+ requestTimeoutMs,
765
+ timeoutMs: TERMINAL_RUNTIME_VERSION_TIMEOUT_MS
766
+ });
767
+ const observed = parseTerminalRuntimeVersion(versionProbe.stdout ?? "");
768
+ if (observed !== undefined)
769
+ runtime.observedVersion = observed;
770
+ if (versionProbe.exitCode !== 0 || observed === undefined)
771
+ throw new Error("Codex version probe did not return a successful `codex-cli <exact-version>` result.");
772
+ if (config.execution?.runtime?.version !== undefined && observed !== config.execution.runtime.version) {
773
+ throw new Error(`Codex version mismatch: requested ${config.execution.runtime.version}, observed ${observed}.`);
774
+ }
775
+ runtime.versionStatus = "verified";
776
+ recordLifecycle("terminal-lab.runtime.version", `Codex requested ${runtime.requestedVersion}, observed ${observed}; exact version selected for execution. Model ${runtime.requestedModel ?? "runtime default (unobserved)"}; reasoning effort ${runtime.requestedReasoningEffort ?? "runtime default (unobserved)"}.`);
777
+ }
778
+ catch (error) {
779
+ runtime.versionStatus = "failed";
780
+ sessionStatus = "failed";
781
+ completionReason = "harness_error";
782
+ sessionError = sanitize(toErrorMessage(error));
783
+ sessionReason = `Codex runtime version could not be verified before execution: ${sessionError}`;
784
+ recordLifecycle("terminal-lab.runtime.version.error", sessionReason);
785
+ return false;
786
+ }
787
+ // --- Optional product setup (no runtime env), before the Codex exec. ---
711
788
  // Same channel and same guarantees as the runtime bootstrap above: no runtime key touches it,
712
789
  // and a failure fails the lane closed rather than handing the agent a half-built world. It
713
790
  // exists so a study can put the participant IN a prepared project — asking an agent what
@@ -745,7 +822,7 @@ async function runLiveTerminalSession(args) {
745
822
  // lane that carries envs is the keyed codex exec, and that invariant is worth more than
746
823
  // the convenience of a second envs channel.
747
824
  uploadAssignment = `export HUMANISH_PRODUCT_UPLOAD='${destination.replace(/'/g, "'\\''")}'; `;
748
- recordLifecycle("terminal-lab.product.uploaded", `Uploaded ${info.size} bytes to the sandbox in ${Math.max(0, now() - uploadStartedAt)}ms (UNKEYED).`);
825
+ recordLifecycle("terminal-lab.product.uploaded", `Uploaded ${info.size} bytes to the sandbox in ${Math.max(0, now() - uploadStartedAt)}ms (no runtime env; declared egress auth may already be available).`);
749
826
  }
750
827
  catch (error) {
751
828
  sessionStatus = "failed";
@@ -775,7 +852,7 @@ async function runLiveTerminalSession(args) {
775
852
  }
776
853
  recordLifecycle("terminal-lab.product.prepared", setupError
777
854
  ? `Product setup FAILED after ${Math.max(0, now() - setupStartedAt)}ms: ${sanitize(setupError)}`
778
- : `Product setup completed in ${Math.max(0, now() - setupStartedAt)}ms (UNKEYED).`);
855
+ : `Product setup completed in ${Math.max(0, now() - setupStartedAt)}ms (no runtime env; declared egress auth may already be available).`);
779
856
  if (setupError) {
780
857
  sessionStatus = "failed";
781
858
  completionReason = "harness_error";
@@ -786,38 +863,43 @@ async function runLiveTerminalSession(args) {
786
863
  return true;
787
864
  })()) {
788
865
  // --- The keyed run: `codex exec --json` non-interactively (stdin disabled). ---
789
- // The runtime key is injected ONLY here, command-scoped (safety contract item 1). stdin is
866
+ // openai-env passes the real key here; openai-egress passes an inert placeholder. stdin is
790
867
  // never wired (safety contract item 7) — commands.run takes no stdin channel. The command's
791
868
  // wall-clock is bounded by maxMinutes (safety contract item 2): commands.run timeoutMs +
792
869
  // an injected-clock guard so a mock/real run that exceeds it is killed and fails closed.
793
- const codexCommand = buildCodexExecCommand({ workdir: SANDBOX_WORKDIR, prompt: composedPrompt });
870
+ const codexCommand = buildCodexExecCommand({
871
+ workdir: SANDBOX_WORKDIR, prompt: composedPrompt, runtimeAuth: runtimeEnv.mode,
872
+ version: runtime.observedVersion,
873
+ ...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model }),
874
+ ...(config.actors[0]?.reasoningEffort === undefined ? {} : { reasoningEffort: config.actors[0].reasoningEffort })
875
+ });
794
876
  const commandDigest = digestText(codexCommand);
795
877
  const startedAt = now();
796
- recordLifecycle("terminal-lab.exec.started", `Launching codex exec (command-scoped runtime key ${runtimeEnv.keyName}); wall-clock bound ${wallClockMs}ms.`);
878
+ recordLifecycle("terminal-lab.exec.started", `Launching codex exec (runtime auth ${runtimeEnv.mode}; command env names: ${Object.keys(runtimeEnv.envs).join(", ")}); wall-clock bound ${wallClockMs}ms.`);
797
879
  let exitCode;
798
880
  let runError;
799
881
  try {
800
882
  const result = await runWithWallClock(sandbox.commands.run(codexCommand, {
801
- // <-- THE command-scoped key channel. The ONLY place the key goes. The participant
883
+ // The selected command env (raw key or inert placeholder). The participant
802
884
  // marker rides the same command: humanish telemetry from inside a study reads as a new
803
885
  // adopter otherwise. #546 added the flag and nothing set it; the 0.66.0 dogfood
804
886
  // participant's twelve commands arrived unmarked.
805
887
  envs: { ...runtimeEnv.envs, HUMANISH_STUDY_PARTICIPANT: "1" },
806
888
  requestTimeoutMs,
807
889
  timeoutMs: wallClockMs,
808
- onStdout: (data) => appendTerminalChunk("stdout", data),
809
- onStderr: (data) => appendTerminalChunk("stderr", data)
890
+ onStdout: (data) => recordStreamedTerminalChunk("stdout", data),
891
+ onStderr: (data) => recordStreamedTerminalChunk("stderr", data)
810
892
  }), wallClockMs, now);
811
893
  if (result.timedOut) {
812
894
  timedOut = true;
813
895
  }
814
896
  else {
815
897
  exitCode = result.value.exitCode;
816
- // Some SDK shapes return final stdout/stderr in the result too (not only via callbacks).
898
+ // Reconcile the SDK's returned aggregate against bytes already delivered by callbacks.
817
899
  if (result.value.stdout)
818
- appendTerminalChunk("stdout", result.value.stdout);
900
+ appendReturnedTerminalOutput("stdout", result.value.stdout);
819
901
  if (result.value.stderr)
820
- appendTerminalChunk("stderr", result.value.stderr);
902
+ appendReturnedTerminalOutput("stderr", result.value.stderr);
821
903
  if (result.value.error)
822
904
  runError = result.value.error;
823
905
  }
@@ -888,6 +970,10 @@ async function runLiveTerminalSession(args) {
888
970
  warnings
889
971
  });
890
972
  }
973
+ // Prefix reconciliation may cut through a known key. Scrub literal values across the retained
974
+ // chunks before any transcript/trace/event artifact is persisted. Check both each stream and
975
+ // the combined event order that the transcript uses; either view can assemble a split value.
976
+ scrubSplitKnownValues(terminalEvents, knownSecretValues, discardedPrefixes);
891
977
  // Build the actor trace FIRST (the cost ledger reads its tokenUsage).
892
978
  const normalizedTranscript = normalizeLocalActorTranscript(terminalEvents.map((e) => e.chunk).join(""));
893
979
  // Parsed from the FULL stream, not the tail: usage records arrive once per turn and the tail
@@ -905,6 +991,8 @@ async function runLiveTerminalSession(args) {
905
991
  terminalEvents,
906
992
  commandLog,
907
993
  transcriptTail: tailOf(normalizedTranscript),
994
+ runtimeAuth: runtimeEnv.mode,
995
+ runtime,
908
996
  ...(terminalTokenUsage === undefined ? {} : { tokenUsage: terminalTokenUsage })
909
997
  });
910
998
  // --- Spend ledger + no-spend proof + full caps enforcement (fail-closed). ---
@@ -945,6 +1033,7 @@ async function runLiveTerminalSession(args) {
945
1033
  // event stream, the normalized transcript, the actor trace, and the run bundle.
946
1034
  const ledgers = {
947
1035
  schema: "humanish.terminal-ledgers.v1",
1036
+ runtime,
948
1037
  lifecycle,
949
1038
  commandLog,
950
1039
  interventions, // ALWAYS present, ALWAYS empty while no assisted-input path ships.
@@ -968,6 +1057,7 @@ async function runLiveTerminalSession(args) {
968
1057
  publicSurfaces: product.publicSurfaces,
969
1058
  caps,
970
1059
  runtimeAuthKeyName: runtimeEnv.keyName,
1060
+ runtimeAuth: runtimeEnv.mode,
971
1061
  policies: {
972
1062
  allowPrivateRepoAccess: config.policies?.allowPrivateRepoAccess ?? false,
973
1063
  allowProviderCredentials: config.policies?.allowProviderCredentials ?? false,
@@ -1350,6 +1440,58 @@ async function runWithWallClock(promise, wallClockMs, now) {
1350
1440
  }
1351
1441
  return value;
1352
1442
  }
1443
+ /**
1444
+ * Per-chunk sanitization cannot recognize a value split across deliveries. Redact those complete
1445
+ * known values before persistence without collapsing events or changing stdout/stderr ordering.
1446
+ * Work backwards through matches so edits to later text leave earlier offsets valid.
1447
+ */
1448
+ function scrubSplitKnownValues(events, knownValues, discardedPrefixes) {
1449
+ for (const order of ["stdout", "stderr", "combined"]) {
1450
+ const chunks = order === "combined"
1451
+ ? [...events]
1452
+ : events.filter((event) => event.stream === order);
1453
+ // A virtual final chunk makes a key crossing the capture cap recognizable. Edits to retained
1454
+ // events redact evidence; the raw overlap and this virtual chunk are never persisted.
1455
+ if (discardedPrefixes[order])
1456
+ chunks.push({ chunk: discardedPrefixes[order] });
1457
+ for (const value of knownValues) {
1458
+ if (!value)
1459
+ continue;
1460
+ let offset = 0;
1461
+ const starts = chunks.map((event) => {
1462
+ const start = offset;
1463
+ offset += event.chunk.length;
1464
+ return start;
1465
+ });
1466
+ const text = chunks.map((event) => event.chunk).join("");
1467
+ const matches = [];
1468
+ for (let at = text.indexOf(value); at !== -1; at = text.indexOf(value, at + value.length))
1469
+ matches.push(at);
1470
+ for (const at of matches.reverse()) {
1471
+ let first = 0;
1472
+ while (first + 1 < starts.length && (starts[first + 1] ?? Infinity) <= at)
1473
+ first += 1;
1474
+ let last = first;
1475
+ while (last + 1 < starts.length && (starts[last + 1] ?? Infinity) < at + value.length)
1476
+ last += 1;
1477
+ const firstChunk = chunks[first];
1478
+ const lastChunk = chunks[last];
1479
+ if (!firstChunk || !lastChunk)
1480
+ continue;
1481
+ const before = firstChunk.chunk.slice(0, at - (starts[first] ?? 0));
1482
+ const after = lastChunk.chunk.slice(at + value.length - (starts[last] ?? 0));
1483
+ firstChunk.chunk = `${before}[REDACTED_SECRET]${first === last ? after : ""}`;
1484
+ for (let index = first + 1; index < last; index += 1) {
1485
+ const middle = chunks[index];
1486
+ if (middle)
1487
+ middle.chunk = "";
1488
+ }
1489
+ if (first !== last)
1490
+ lastChunk.chunk = after;
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1353
1495
  /** Build the in-sandbox `codex exec` command (non-interactive, JSON, stdin disabled by mechanism). */
1354
1496
  function buildCodexExecCommand(args) {
1355
1497
  // The prompt is passed via a heredoc on stdin of a wrapper? NO, stdin is DISABLED (item 7), so
@@ -1364,7 +1506,13 @@ function buildCodexExecCommand(args) {
1364
1506
  // The E2B sandbox is the trust boundary (the disposable machine); the sibling
1365
1507
  // oss-meta-lab lane carries the same flag at both live call sites for the
1366
1508
  // same reason, and exec mode has no interactive approval channel at all.
1367
- return `cd ${args.workdir} && npm_config_update_notifier=false npx -y @openai/codex@latest exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --json ${quotedPrompt}`;
1509
+ // The egress transform protects only the default OpenAI host. Pin the effective built-in
1510
+ // provider/base URL above config-file settings so setup-written custom endpoints cannot make
1511
+ // this invocation silently claim protection for another provider. openai-env is unchanged.
1512
+ const providerConfig = args.runtimeAuth === "openai-egress"
1513
+ ? ` -c 'model_provider="openai"' -c 'openai_base_url="https://api.openai.com/v1"'`
1514
+ : "";
1515
+ return `cd ${args.workdir} && ${buildRuntimeExecPrefix(args.version, args.model, args.reasoningEffort)} --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check${providerConfig} --json ${quotedPrompt}`;
1368
1516
  }
1369
1517
  /** Compose the live prompt: PUBLIC surfaces + author mission + the verdict-nonce marker contract. */
1370
1518
  function composeLivePrompt(args) {
@@ -1409,6 +1557,8 @@ function buildTerminalActorTrace(args) {
1409
1557
  return {
1410
1558
  schema: ACTOR_TRACE_SCHEMA,
1411
1559
  provider: "codex",
1560
+ ...(args.runtime.versionStatus === "verified" ? { providerVersion: args.runtime.observedVersion } : {}),
1561
+ runtime: args.runtime,
1412
1562
  protocol: "terminal-exec",
1413
1563
  lane: "terminal",
1414
1564
  persona: args.persona,
@@ -1423,7 +1573,7 @@ function buildTerminalActorTrace(args) {
1423
1573
  status: args.status,
1424
1574
  completionReason: args.completionReason,
1425
1575
  reason: args.reason,
1426
- ids: { model: "codex" },
1576
+ ids: {}, // Runtime model requests are not observed; declarations live in runtime provenance.
1427
1577
  ...(args.tokenUsage ? { tokenUsage: args.tokenUsage } : {}),
1428
1578
  counts: {
1429
1579
  commands: args.commandLog.length,
@@ -1434,7 +1584,9 @@ function buildTerminalActorTrace(args) {
1434
1584
  terminalEvents: args.terminalEvents.length
1435
1585
  },
1436
1586
  items,
1437
- capabilities: TERMINAL_AGENT_CAPABILITIES
1587
+ capabilities: args.runtimeAuth === "openai-egress"
1588
+ ? { ...TERMINAL_AGENT_CAPABILITIES, keyPlacement: "external" }
1589
+ : TERMINAL_AGENT_CAPABILITIES
1438
1590
  };
1439
1591
  }
1440
1592
  /**
@@ -1518,7 +1670,7 @@ export function buildTerminalProductBundle(args) {
1518
1670
  type: "terminal-lab.credentials.declared",
1519
1671
  // Names-only evidence (invariant 1): the runtime-auth CHANNEL is declared; no value is ever
1520
1672
  // recorded. The deny-by-default policies are recorded so the credential posture is auditable.
1521
- message: `Runtime auth channel: ${args.runtimeAuth ?? "none declared"} (names only; values never persist; command-scoped injection is enforced by the shipped live engine, while this dry-run performs no injection). Credential policies (deny-by-default): allowPrivateRepoAccess=${args.policies.allowPrivateRepoAccess}, allowProviderCredentials=${args.policies.allowProviderCredentials}, allowPaymentCredentials=${args.policies.allowPaymentCredentials}, allowGitHubMutation=${args.policies.allowGitHubMutation}.`,
1673
+ message: `Runtime auth channel: ${args.runtimeAuth ?? "none declared"} (names only; values never persist; the live engine applies the selected key placement, while this dry-run performs no injection). Credential policies (deny-by-default): allowPrivateRepoAccess=${args.policies.allowPrivateRepoAccess}, allowProviderCredentials=${args.policies.allowProviderCredentials}, allowPaymentCredentials=${args.policies.allowPaymentCredentials}, allowGitHubMutation=${args.policies.allowGitHubMutation}.`,
1522
1674
  simId: "sim-001",
1523
1675
  streamId: "stream-001"
1524
1676
  },
@@ -1536,7 +1688,7 @@ export function buildTerminalProductBundle(args) {
1536
1688
  at: args.createdAt,
1537
1689
  level: "info",
1538
1690
  type: "terminal-lab.contract.ready",
1539
- message: "Dry-run contract bundle ready. Switch scenario.mode to live with the required runtime auth and caps to exercise the in-sandbox agent route, captured exec stream, and command-scoped credential boundary.",
1691
+ message: "Dry-run contract bundle ready. Switch scenario.mode to live with the required runtime auth and caps to exercise the in-sandbox agent route, captured exec stream, and declared runtime-auth placement.",
1540
1692
  simId: "sim-001",
1541
1693
  streamId: "stream-001"
1542
1694
  }
@@ -1737,7 +1889,7 @@ export function buildLiveTerminalProductBundle(args) {
1737
1889
  events: lifecycleEvents,
1738
1890
  redaction: {
1739
1891
  status: "passed",
1740
- notes: `Live terminal-product run: the in-sandbox agent's output was captured via commands.run onStdout/onStderr and scrubbed (literal known values incl. the runtime key) THEN redacted (shape patterns) AT THE SOURCE before persisting. The runtime key (${args.runtimeAuthKeyName}) was injected ONLY into the command-scoped codex invocation, never sandbox-global env or metadata; only its NAME appears in evidence. Subject provenance is UNPINNED (public-surface study).`
1892
+ notes: `Live terminal-product run: the in-sandbox agent's output was captured via commands.run onStdout/onStderr and scrubbed (literal known values incl. the runtime key) THEN redacted (shape patterns) AT THE SOURCE before persisting. ${args.runtimeAuth === "openai-egress" ? `Runtime auth openai-egress: the raw key from ${args.runtimeAuthKeyName} is reserved for E2B's external api.openai.com HTTPS header transform. ${args.ledgers.commandLog.some((command) => command.label === "codex-exec") ? "Codex received an inert CODEX_API_KEY placeholder." : "Codex was not launched."} Any created sandbox retains a spendable OpenAI proxy capability until teardown; additional provider calls may not appear in the Codex usage ledger.` : `Runtime auth openai-env: the runtime key (${args.runtimeAuthKeyName}) was injected ONLY into the command-scoped codex invocation, never sandbox-global env or metadata; only its NAME appears in evidence.`} Subject provenance is UNPINNED (public-surface study).`
1741
1893
  },
1742
1894
  artifacts: {
1743
1895
  run: "run.json",