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.
- package/README.md +121 -615
- package/dist/actor-contract.d.ts +30 -0
- package/dist/actor-contract.js.map +1 -1
- package/dist/chrome-cdp-probe.js +13 -0
- package/dist/chrome-cdp-probe.js.map +1 -1
- package/dist/computer-use.d.ts +13 -3
- package/dist/computer-use.js +179 -15
- package/dist/computer-use.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +20 -8
- package/dist/cua-actor-lab.js +154 -49
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-desktop-executor.d.ts +9 -1
- package/dist/e2b-desktop-executor.js +67 -4
- package/dist/e2b-desktop-executor.js.map +1 -1
- package/dist/e2b-desktop-launch.d.ts +36 -1
- package/dist/e2b-desktop-launch.js +88 -1
- package/dist/e2b-desktop-launch.js.map +1 -1
- package/dist/e2b-desktop-resources.d.ts +11 -0
- package/dist/e2b-desktop-resources.js +33 -0
- package/dist/e2b-desktop-resources.js.map +1 -0
- package/dist/e2b-desktop-screenshot-cleanup.d.ts +15 -0
- package/dist/e2b-desktop-screenshot-cleanup.js +67 -0
- package/dist/e2b-desktop-screenshot-cleanup.js.map +1 -0
- package/dist/e2b-terminal-lab.d.ts +5 -2
- package/dist/e2b-terminal-lab.js +238 -86
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/first-run-path.js +2 -2
- package/dist/first-run-path.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lab-config.d.ts +15 -14
- package/dist/lab-config.js +11 -2
- package/dist/lab-config.js.map +1 -1
- package/dist/observer-data.d.ts +2 -0
- package/dist/observer-data.js +10 -1
- package/dist/observer-data.js.map +1 -1
- package/dist/openai-responses-cu.js +76 -49
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/pricing.d.ts +19 -1
- package/dist/pricing.js +43 -10
- package/dist/pricing.js.map +1 -1
- package/dist/program.js +17 -8
- package/dist/program.js.map +1 -1
- package/dist/redaction.js +9 -2
- package/dist/redaction.js.map +1 -1
- package/dist/run.d.ts +14 -2
- package/dist/run.js.map +1 -1
- package/dist/terminal-node-bootstrap.d.ts +4 -0
- package/dist/terminal-node-bootstrap.js +58 -0
- package/dist/terminal-node-bootstrap.js.map +1 -0
- package/dist/terminal-runtime-auth.d.ts +13 -0
- package/dist/terminal-runtime-auth.js +24 -0
- package/dist/terminal-runtime-auth.js.map +1 -0
- package/dist/terminal-runtime.d.ts +16 -0
- package/dist/terminal-runtime.js +42 -0
- package/dist/terminal-runtime.js.map +1 -0
- package/dist/terminal-token-usage.d.ts +2 -1
- package/dist/terminal-token-usage.js +3 -2
- package/dist/terminal-token-usage.js.map +1 -1
- package/docs/architecture/actor-contract.md +22 -0
- package/docs/architecture/state-driven-executor.md +10 -1
- package/docs/architecture/terminal-product-lane.md +142 -6
- package/docs/contracts/feedback.md +14 -0
- package/docs/contracts/run-bundle.md +5 -1
- package/docs/contracts/schemas.md +17 -8
- package/docs/goals/current.md +61 -13
- package/docs/ramp/README.md +1 -1
- package/package.json +4 -2
package/dist/e2b-terminal-lab.js
CHANGED
|
@@ -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
|
|
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.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
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
|
-
|
|
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
|
|
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)
|
|
671
|
-
: `Egress
|
|
672
|
-
|
|
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:
|
|
676
|
-
// The stock
|
|
677
|
-
//
|
|
678
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
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 (
|
|
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 (
|
|
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
|
-
//
|
|
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({
|
|
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
|
|
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
|
-
//
|
|
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) =>
|
|
809
|
-
onStderr: (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
|
-
//
|
|
898
|
+
// Reconcile the SDK's returned aggregate against bytes already delivered by callbacks.
|
|
817
899
|
if (result.value.stdout)
|
|
818
|
-
|
|
900
|
+
appendReturnedTerminalOutput("stdout", result.value.stdout);
|
|
819
901
|
if (result.value.stderr)
|
|
820
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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;
|
|
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
|
|
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.
|
|
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",
|