Skip to content

feat(runtime): add the codex agent runtime (ADR 0099, ADR 0100) - #6926

Merged
waynesun09 merged 20 commits into
mainfrom
codex-runtime-core
Sep 3, 2026
Merged

feat(runtime): add the codex agent runtime (ADR 0099, ADR 0100)#6926
waynesun09 merged 20 commits into
mainfrom
codex-runtime-core

Conversation

@waynesun09

@waynesun09 waynesun09 commented Sep 2, 2026

Copy link
Copy Markdown
Member

Summary

CodexRuntime: Codex CLI as a fullsend agent runtime with the same secretless OpenAI credential path pi uses. ADR 0099.

  • Credential: a custom codex model provider fullsend-openai (base_url api.openai.com, wire_api = responses, HTTP/SSE — no websockets) whose auth.command prints the OpenShell gateway placeholder from a runner-seeded token file under CODEX_HOME; codex re-runs it on its refresh interval and on 401, so a running iteration follows every credential rotation. The runner re-seeds the file after each refresh through the seeder interface from PR C. The built-in openai provider reads the key once at startup and cannot be overridden, so it is not used.
  • Bootstrap: runner-owned config.toml (approvals off, codex's own sandbox off inside OpenShell, developer_instructions from the agent definition, bundled skills off, web search off), hooks.json + an embedded adapter that runs fullsend's shared hook scripts and translates the wire protocol (fullsend exit-1 block → codex exit-2 block; PostToolUse can block but not rewrite built-in tool output, so the sanitizer stages warn the model), skills to CODEX_HOME/skills.
  • Integrity: whole-file sha256 digests of config.toml and hooks.json held in the runner's process memory (the sandbox manifest is agent-writable, so it is not an anchor) and checked in the launch command; the hooks directory must contain exactly the expected files, each bound to its own digest from the binary's embedded scripts; absolute python3 -I -s -B for adapter and children with a stripped env; -c pins for approval, sandbox, provider, base_url and auth.command; the repo stays untrusted so its .codex/ layer never loads; OPENAI_BASE_URL and loader/Python variables unset after the agent-writable .env.
  • Run: codex exec --json with the prompt on stdin, PR B's parser for events/metrics (verdict from the stream, not the exit code), rollout JSONL transcripts validated as codex envelopes and pattern-redacted, output.jsonl and codex-debug.log redacted, .jsonl.zst not trusted by extension, per-iteration cleanup with the shared stray-process sweep.
  • Model rule: OpenAI ids only (openai/<id> or bare id); Claude aliases are refused with an error naming FULLSEND_CODEX_MODEL=openai/<id> or model: openai/<id> on the agents: entry — Claude aliases are never mixed with GPT.

Review rounds

Two full rounds (Codex gpt-5.6-sol + Grok 4.6) and three targeted re-reviews. Every HIGH was reproduced before fixing: grep-based config guard bypassed by a dotted-key project-trust line (repo .codex/ layer loaded — reproduced with a repo-supplied model); existential hooks.json check; bare python3 via PATH; adapter exceptions and malformed payloads fail-open; hook-script digests bound to a set, not filenames; planted hooks/json/__init__.py executed by unisolated children; .zst files kept unredacted; children writing hooks/__pycache__ that the exhaustive guard then refused on the next iteration (self-inflicted lockout).

Verification

  • Container smoke against api.openai.com (macOS, image from PR A): PreToolUse block shown to the model as a declined command (Tirith dotfile_overwrite), PostToolUse canary block withholds the output, hooks.json loads with no [hooks] table, repo .agents/skills discovered under an untrusted project, codex doctor reports the custom provider and no websocket transport.
  • OpenShell gateway smoke with the run-scoped provider: POST /v1/responses allowed; GET /v1/models denied once plus one retry (harmless); chatgpt.com and api.github.com probes denied at L4; metrics runtime: codex; credential rotation on a kept sandbox → re-seed → second successful turn; provider deleted at the end. A run refused at model resolution deletes its sandbox and provider.
  • Patch coverage 87% (every file above 80%).

Part of a five-PR stack for #6920 (Codex as an agent runtime): A image pin → B stream parser → C OpenAI credential seeder → D runtime core (ADR 0099) → E enable + docs. Each PR is reviewable on its own diff; they merge bottom-up. Plan and verified Codex facts: research/fullsend-codex-runtime-plan.md in the ai-workspace-public research repo (to be linked once pushed).

Refs #6920

Assisted-by: Claude (implementation and review orchestration), Codex gpt-5.6-sol (review), Grok 4.6 (review)

ADRs and living docs (after review)

  • Split per the one-decision rule: ADR 0099 decides codex credential delivery (custom provider + auth.command + runner-seeded token file) and amends ADR 0092, whose in-sandbox half was pi-specific (auth.json, **/node); ADR 0092 receives only a one-line cross-reference annotation, as docs/contributing/adrs.md allows for accepted ADRs. ADR 0100 decides sandbox hooks on codex (adapter, exit-code translation, detect + block, per-invocation digest re-verification, pinned interpreter/PATH, security env re-exported after .env).
  • docs/problems/security-threat-model.md: the claim that agents cannot modify their own guardrails is qualified — hook scripts stay writable between iterations on Claude Code and pi; codex re-verifies them per invocation (ADR 0100).
  • Not pinned on codex either: FULLSEND_CANARY_TOKEN and FULLSEND_TOOL_ALLOWLIST come from the harness env/host_files, so the runtime has no runner-side copy to re-export; stated in ADR 0100, follow-up on the runner side.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Implement secure Codex agent runtime

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Implements end-to-end Codex runtime internals while keeping selection gated.
• Adds rotating secretless OpenAI authentication and fail-closed sandbox hook translation.
• Guards and redacts agent-writable configuration, streams, logs, and rollout artifacts.
Diagram

sequenceDiagram
    participant R as Runner
    participant C as CODEX_HOME
    participant X as Codex CLI
    participant A as Hook Adapter
    participant H as Hook Scripts
    participant G as OpenShell
    participant O as OpenAI API
    participant F as Artifacts
    R->>C: Bootstrap config and digests
    R->>C: Seed rotating placeholder
    R->>X: Launch guarded exec
    X->>C: Read auth command
    X->>G: Responses request
    G->>O: Forward credentialed request
    X->>A: Emit tool hook
    A->>H: Run shared scripts
    H-->>A: Return verdict
    A-->>X: Translate control result
    X-->>F: Write JSONL and logs
    R->>F: Validate and redact
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use built-in OpenAI provider
  • ➕ Eliminates the custom provider and authentication script.
  • ➕ Uses Codex's default credential configuration path.
  • ➖ Reads the environment credential only at startup.
  • ➖ Cannot follow revision-scoped placeholder rotation during long iterations.
2. Bake managed hooks into the image
  • ➕ Uses Codex's managed hook trust mechanism.
  • ➕ Reduces runtime-generated hook wiring under CODEX_HOME.
  • ➖ Requires image releases and fleet repins for every hook change.
  • ➖ Still needs protocol translation for Fullsend hook semantics.
  • ➖ Separates hook deployment from the Fullsend binary version.
3. Trust repository Codex configuration
  • ➕ Allows native project-level Codex settings, instructions, and hooks.
  • ➕ Requires less runner-owned configuration.
  • ➖ Lets reviewed repositories inject instructions and hooks into the agent.
  • ➖ Conflicts with the threat model's untrusted-project boundary.

Recommendation: Retain the PR's custom provider, runtime-rendered adapter, and process-memory integrity anchors. They are necessary to follow OpenAI credential rotation, preserve the shared hook contract, and prevent an untrusted repository from replacing runtime configuration; the simpler alternatives violate at least one of those requirements.

Files changed (22) +5796 / -134

Enhancement (9) +2469 / -65
codex.goReplace Codex runtime stubs with credential support +51/-65

Replace Codex runtime stubs with credential support

• Updates runtime metadata and implements the rotating OpenAI placeholder file contract. Leaves runtime selection gated until follow-up documentation and behavior coverage land.

internal/runtime/codex.go

codex_bootstrap.goImplement Codex runtime bootstrap +478/-0

Implement Codex runtime bootstrap

• Renders agent instructions, installs skills and security hooks, preflights Codex and Python, and writes the runtime manifest. Records per-run integrity digests in runner memory.

internal/runtime/codex_bootstrap.go

codex_config.goRender hardened Codex configuration and hooks +400/-0

Render hardened Codex configuration and hooks

• Defines the custom OpenAI provider, safely escapes developer instructions, and translates runtime-neutral hook plans into synchronous Codex handlers. Embeds the hook adapter and authentication script.

internal/runtime/codex_config.go

fullsend-codex-hook.pyAdd the Codex security hook adapter +438/-0

Add the Codex security hook adapter

• Translates Codex tool payloads into the shared hook protocol and converts blocks into Codex exit code 2. Runs scripts sequentially with isolated Python and warns when built-in tool output cannot be rewritten.

internal/runtime/codex_hook/fullsend-codex-hook.py

openai-token.shAdd the Codex external bearer command +55/-0

Add the Codex external bearer command

• Reads the runner-seeded OpenShell placeholder and emits it to Codex. Rejects missing, malformed, or real credentials without logging their values.

internal/runtime/codex_hook/openai-token.sh

codex_integrity.goAnchor Codex runtime file integrity +129/-0

Anchor Codex runtime file integrity

• Stores per-sandbox digests in runner memory and generates exhaustive hook-directory guards. Binds every expected script digest to its filename and rejects added or non-regular entries.

internal/runtime/codex_integrity.go

codex_redact.goRedact and validate Codex artifacts +221/-0

Redact and validate Codex artifacts

• Adds bounded JSONL redaction for streamed and downloaded artifacts plus text-log filtering. Validates rollout envelopes before accepting session files as transcripts.

internal/runtime/codex_redact.go

codex_run.goImplement guarded Codex iteration execution +512/-0

Implement guarded Codex iteration execution

• Validates OpenAI models and reasoning effort, constructs the hardened 'codex exec --json' command, and derives results and metrics from the stream. Adds debug capture and per-iteration process and artifact cleanup.

internal/runtime/codex_run.go

codex_transcript.goImplement Codex transcript and debug extraction +185/-0

Implement Codex transcript and debug extraction

• Downloads contained plain JSONL rollouts, validates and redacts them, and safely extracts redacted debug logs. Parses streamed transcripts for runner-level failure verdicts.

internal/runtime/codex_transcript.go

Tests (10) +2886 / -47
run_openai_test.goVerify Codex credential reseeding integration +12/-6

Verify Codex credential reseeding integration

• Extends selected-backend tests to assert Codex supplies its own token seed fragment and destination file.

internal/cli/run_openai_test.go

codex_bootstrap_test.goTest Codex bootstrap and preflight behavior +498/-0

Test Codex bootstrap and preflight behavior

• Covers configuration and manifest generation, hook installation, skill uploads, cleanup, version checks, Python isolation requirements, and infrastructure failures.

internal/runtime/codex_bootstrap_test.go

codex_config_test.goTest Codex configuration and hook rendering +345/-0

Test Codex configuration and hook rendering

• Validates TOML escaping, provider hygiene, tool matcher translation, synchronous hook wiring, Codex-compatible JSON shape, and embedded asset paths.

internal/runtime/codex_config_test.go

codex_hook_test.goTest fail-closed Codex hook translation +498/-0

Test fail-closed Codex hook translation

• Exercises allow and block semantics, tool-name mapping, sanitizer warnings, canary blocking, malformed payloads, script ordering, interpreter isolation, and bytecode suppression.

internal/runtime/codex_hook_test.go

codex_integrity_test.goTest Codex integrity anchors +97/-0

Test Codex integrity anchors

• Verifies digest storage is isolated by sandbox and hook guards are deterministic and filename-bound.

internal/runtime/codex_integrity_test.go

codex_redact_test.goTest Codex artifact redaction +266/-0

Test Codex artifact redaction

• Covers nested secret masking, split writes, partial lines, buffer limits, file rewriting, sink errors, rollout checks, and end-to-end stream redaction.

internal/runtime/codex_redact_test.go

codex_run_test.goTest Codex launch guards and credentials +606/-0

Test Codex launch guards and credentials

• Exercises model translation, command ordering, pinned overrides, shell integrity guards, placeholder seeding, authentication scripts, debug mode, and tampering scenarios.

internal/runtime/codex_run_test.go

codex_test.goReplace Codex stub tests with runtime contracts +15/-38

Replace Codex stub tests with runtime contracts

• Removes not-implemented assertions and verifies native AGENTS.md handling, required bootstrap input, metadata, and transcript error emission.

internal/runtime/codex_test.go

codex_transcript_test.goTest Codex transcript lifecycle +540/-0

Test Codex transcript lifecycle

• Covers rollout downloads, path containment, spoofed-file rejection, debug extraction, stream verdict overrides, metrics, missing terminal events, and failure cleanup.

internal/runtime/codex_transcript_test.go

openai_seeder_test.goActivate Codex OpenAI seeder expectations +9/-3

Activate Codex OpenAI seeder expectations

• Replaces the stub assertion with checks for Codex's token file and credential reseeding shell fragment.

internal/runtime/openai_seeder_test.go

Documentation (3) +441 / -22
0099-codex-agent-runtime.mdDocument the Codex runtime architecture decision +98/-0

Document the Codex runtime architecture decision

• Records the custom provider, rotating token file, hook adapter, integrity model, and known runtime limitations. Explains why built-in credentials, managed hooks, and project trust were rejected.

docs/ADRs/0099-codex-agent-runtime.md

architecture.mdAdd Codex to the runtime architecture +3/-3

Add Codex to the runtime architecture

• Describes Codex as an implemented but not yet selectable runtime. Updates instruction translation and rotating OpenAI credential delivery decisions.

docs/architecture.md

runtime-implementation.mdDocument Codex runtime internals and security posture +340/-19

Document Codex runtime internals and security posture

• Expands runtime matrices with Codex hook and artifact behavior. Documents verified CLI semantics, integrity anchors, smoke-test evidence, limitations, and version-bump checks.

docs/contributing/runtime-implementation.md

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 6:54 PM UTC · Ended 7:00 PM UTC

Commit: 4981b92 · View workflow run →

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Site preview

Preview: https://e1a3d757-site.fullsend-ai.workers.dev

Commit: 11a562d5c03ed9b4eabe9219541738c5e5a38a5f

@qodo-code-review

qodo-code-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Mutable environment bypasses SSRF ✓ Resolved 🐞 Bug ⛨ Security
Description
The adapter forwards security variables from Codex's inherited environment, which was populated by
sourcing the agent-writable workspace .env. An agent can rewrite FULLSEND_EGRESS_ALLOWLIST
during one iteration so the next iteration's SSRF hook permits attacker-selected destinations.
Code

internal/runtime/codex_hook/fullsend-codex-hook.py[R109-112]

+    env = {k: v for k, v in os.environ.items() if not k.startswith("PYTHON")}
+    # Inert under `-I`, which already ignores the user site directory. Kept as
+    # a second latch so dropping the flag does not silently re-enable it.
+    env["PYTHONNOUSERSITE"] = "1"
Relevance

●●● Strong

Recent security precedents accept hardening agent-controlled inputs and preventing policy or
command-injection bypasses.

PR-#3820
PR-#1780

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Bootstrap writes the SSRF allowlist into the workspace .env, and the Codex launch sources that
mutable file. _child_env then copies every non-Python variable, while the SSRF hook uses
FULLSEND_EGRESS_ALLOWLIST as its policy source; the sandbox is reused across validation
iterations.

internal/runtime/codex_hook/fullsend-codex-hook.py[98-113]
internal/runtime/codex_run.go[262-285]
internal/runtime/sandbox_hooks_bootstrap.go[70-107]
internal/cli/run.go[2004-2034]
internal/security/hooks/ssrf_pretool.py[1-240]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Security hooks consume policy values inherited from an agent-writable `.env`. On a validation retry, modified values such as `FULLSEND_EGRESS_ALLOWLIST` become authoritative and can weaken SSRF enforcement.

## Issue Context
Preserve hook policy in runner-owned memory and inject or verify it after the mutable `.env` is sourced. The child environment should receive only runner-authorized values for security-sensitive variables, not arbitrary inherited replacements.

## Fix Focus Areas
- internal/runtime/codex_hook/fullsend-codex-hook.py[98-113]
- internal/runtime/codex_run.go[262-290]
- internal/runtime/sandbox_hooks_bootstrap.go[70-107]
- internal/runtime/codex_integrity.go[36-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Hooks mutable during execution ✓ Resolved 🐞 Bug ⛨ Security
Description
Integrity checks run only before Codex starts, while the same-user agent can subsequently overwrite
the adapter or hook scripts. The modified implementation is used by PostToolUse or later hook
invocations, allowing blocking and sanitization controls to be replaced with exit-0 behavior.
Code

internal/runtime/codex_run.go[R283-284]

+		"&& "+codexAssetGuard(r, hooksEnabled, hashes),
+		"&& "+codexConfigGuard(r, hashes),
Relevance

●●● Strong

Accepted security precedents prioritize eliminating mutable runtime assets and host-to-sandbox
integrity gaps.

PR-#1780
PR-#6695

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The asset guards are included in the one-time launch command, after which Codex invokes the adapter
as a fresh process for each hook event. Bootstrap does not make these files immutable, and the
implementation explicitly acknowledges that CODEX_HOME is agent-writable.

internal/runtime/codex_run.go[143-177]
internal/runtime/codex_run.go[258-290]
internal/runtime/codex_run.go[412-414]
internal/runtime/codex_integrity.go[10-20]
internal/runtime/codex_hook/fullsend-codex-hook.py[376-418]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Codex hook assets are hash-checked only before the Codex process starts, but remain writable by the agent during the iteration. A tool command can replace the adapter or a hook script before the next hook subprocess executes, bypassing security enforcement.

## Issue Context
The expected hashes are trusted because they originate in runner memory or embedded assets. Carry those trusted values into a runner-generated per-hook wrapper or command that verifies the adapter, expected scripts, and directory contents immediately before every hook invocation and fails closed on any mismatch.

## Fix Focus Areas
- internal/runtime/codex_run.go[143-177]
- internal/runtime/codex_config.go[329-393]
- internal/runtime/codex_integrity.go[82-129]
- internal/runtime/codex_hook/fullsend-codex-hook.py[203-265]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. ADR records multiple decisions ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0099 decides both the OpenAI credential-provider mechanism and the separate sandbox-hook adapter
architecture. These are independently consequential decisions and must be split into separate ADRs.
Code

docs/ADRs/0099-codex-agent-runtime.md[R61-64]

+Sandbox hooks are wired through `$CODEX_HOME/hooks.json`, rendered from `security.HookPlan`, with
+every handler invoking one embedded **adapter** that runs the shared hook scripts and translates the
+wire protocol in both directions. The adapter is mandatory rather than optional because codex's
+protocol differs from the scripts' in ways that fail *open* if forwarded verbatim: a hook that exits
Relevance

●● Moderate

Splitting consequential decisions is plausible, but no close precedent establishes this as a firm
repository requirement.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Decision section first selects a custom provider and runner-seeded token mechanism, then
separately mandates a hook adapter and its protocol, trust, and integrity behavior. These are
distinct architectural choices under PR Compliance ID 1062089.

docs/ADRs/0099-codex-agent-runtime.md[52-75]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ADR 0099 records distinct decisions for credential delivery and sandbox-hook adaptation, violating the one-decision-per-ADR requirement.

## Issue Context
Keep one decision in ADR 0099 and move the other into a separately numbered ADR with appropriate cross-references, status, context, and consequences.

## Fix Focus Areas
- docs/ADRs/0099-codex-agent-runtime.md[52-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Context omits problem links ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0099 declares agent-architecture and security-threat-model in relates_to, but its Context
links only to other ADRs and the runtime guide. The accepted ADR therefore neither links to nor
updates its related problem documents.
Code

docs/ADRs/0099-codex-agent-runtime.md[R23-26]

+Fullsend runs agents on `claude` and `pi` ([ADR 0091](0091-per-agent-runtime-model-effort.md)).
+Adding [openai/codex](https://github.com/openai/codex) needs answers to two questions the runtime
+contract leaves to each backend: how the agent gets an OpenAI credential without one entering the
+sandbox, and how the runtime-neutral sandbox tool hooks ([ADR 0090](0090-runtime-neutral-sandbox-hooks-contract.md))
Relevance

●●● Strong

Recent ADR precedents explicitly require Context links to related problem documents.

PR-#5016
PR-#5798

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The frontmatter identifies two related problem areas, while the complete Context section contains
links to ADRs 0090, 0091, and 0092 and the runtime implementation guide but no links to either
corresponding problem document. This violates the Context-link requirement and the accepted-ADR
living-document requirement.

Rule 1062062: Update architecture overview and problem docs when ADR is accepted
docs/ADRs/0099-codex-agent-runtime.md[4-6]
docs/ADRs/0099-codex-agent-runtime.md[21-37]
docs/problems/agent-architecture.md[1-3]
docs/problems/security-threat-model.md[1-3]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ADR 0099's Context does not link to the problem documents named by its `relates_to` frontmatter, and those documents are not updated by this PR.

## Issue Context
Add concise contextual links to the existing `agent-architecture.md` and `security-threat-model.md` documents. If the decision resolves or partially resolves questions in either document, annotate those questions in the same PR.

## Fix Focus Areas
- docs/ADRs/0099-codex-agent-runtime.md[21-37]
- docs/problems/agent-architecture.md[212-224]
- docs/problems/security-threat-model.md[93-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Manifest controls retry model ✓ Resolved 🐞 Bug ⛨ Security
Description
When RunParams.Model is empty, Run selects the model from an agent-writable, unhashed manifest.
An agent can edit that field during an iteration and make a validation retry use a different OpenAI
model and cost tier.
Code

internal/runtime/codex_run.go[366]

+	modelID, err := translateCodexModel(EffectiveModel(params.Model, m.Model))
Relevance

●● Moderate

The model-selection risk is security-relevant, but no close precedent addresses agent-writable retry
manifests specifically.

PR-#6467
PR-#6695

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI supplies only the harness model through RunParams; Codex falls back to the manifest model
when that value is empty. The manifest is reread from the persistent sandbox on every iteration and
is explicitly excluded from the trusted integrity anchors.

internal/runtime/codex_run.go[350-368]
internal/cli/run.go[2069-2074]
internal/runtime/codex_bootstrap.go[16-40]
internal/runtime/codex_bootstrap.go[199-204]
internal/runtime/codex_bootstrap.go[459-477]
internal/runtime/codex_integrity.go[10-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The effective Codex model falls back to `m.Model`, even though the manifest containing that field is explicitly agent-writable and not integrity-protected. This lets an agent change the model used by a later validation iteration.

## Issue Context
Record the parsed agent model in trusted runner memory during Bootstrap, alongside the uploaded artifact hashes, and use that value in Run. Alternatively, integrity-protect the manifest and fail closed if it changes, but do not use mutable sandbox data as model-selection authority.

## Fix Focus Areas
- internal/runtime/codex_run.go[350-368]
- internal/runtime/codex_bootstrap.go[199-255]
- internal/runtime/codex_bootstrap.go[459-477]
- internal/runtime/codex_integrity.go[36-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Contract lacks normative specification ✗ Dismissed 📜 Skill insight ⚙ Maintainability
Description
ADR 0099 inlines field-level provider and hook wire contracts, including exact configuration keys,
refresh behavior, exit-code translation, and accepted protocol fields. These details must live in a
versioned docs/normative/ specification that the ADR links to.
Code

docs/ADRs/0099-codex-agent-runtime.md[R54-57]

+Codex is configured with a **custom model provider** (`fullsend-openai`, `wire_api = "responses"`)
+whose `auth.command` is a runner-written script that prints the current placeholder from a
+runner-owned token file under `CODEX_HOME`. Codex re-runs that command every
+`refresh_interval_ms`, and the runner re-seeds the file after every credential refresh through the
Relevance

●● Moderate

Normative-spec separation is supported, but recent precedent rejected moving detailed ADR content
into other documentation.

PR-#6769
PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The ADR specifies exact TOML fields such as wire_api, auth.command, and refresh_interval_ms,
then defines hook wire-protocol translation and accepted control behavior. PR Compliance ID 1525847
requires such field-level contracts and compatibility artifacts to be versioned under
docs/normative/ rather than embedded in an ADR.

docs/ADRs/0099-codex-agent-runtime.md[54-74]
docs/contributing/runtime-implementation.md[660-731]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ADR 0099 embeds detailed configuration and hook protocol contracts instead of linking to a versioned normative specification.

## Issue Context
Move field-level and compatibility-sensitive details—such as provider keys, refresh semantics, hook exit-code translation, supported output fields, and override requirements—under `docs/normative/<topic>/v1/`. Keep only the architectural decision and a link to that specification in the ADR.

## Fix Focus Areas
- docs/ADRs/0099-codex-agent-runtime.md[52-75]
- docs/contributing/runtime-implementation.md[660-731]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
7. Transcript redaction exhausts memory ✓ Resolved 🐞 Bug ☼ Reliability
Description
codexRedactFile reads an agent-controlled rollout entirely into memory and allocates another
output buffer of similar size. A planted large .jsonl file with one valid rollout envelope can
therefore exhaust the runner's memory during artifact extraction.
Code

internal/runtime/codex_redact.go[R139-142]

+	data, err := os.ReadFile(path)
+	if err != nil {
+		return err
+	}
Relevance

●● Moderate

Resource-exhaustion concerns align with accepted runtime hardening, but the closest transcript
robustness precedent was rejected.

PR-#1780
PR-#764

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Transcript extraction accepts a candidate after validating only its first non-empty line. Redaction
then calls os.ReadFile, grows a second buffer to the complete input size, and processes the whole
file without any total-size bound.

internal/runtime/codex_transcript.go[84-102]
internal/runtime/codex_redact.go[138-160]
internal/runtime/codex_redact.go[186-220]
internal/sandbox/sandbox.go[1687-1702]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Downloaded rollout files are agent-controlled but are loaded wholly into memory and duplicated during redaction. A sufficiently large file can terminate or destabilize the host runner.

## Issue Context
Redact through bounded buffered I/O into a temporary file and atomically replace the original. Enforce explicit per-line and total artifact-size limits, dropping files that exceed them rather than allocating proportional memory.

## Fix Focus Areas
- internal/runtime/codex_redact.go[138-160]
- internal/runtime/codex_redact.go[186-220]
- internal/runtime/codex_transcript.go[84-102]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 65 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/ADRs/0099-codex-agent-runtime.md Outdated
Comment thread docs/ADRs/0099-codex-agent-runtime.md Outdated
Comment thread docs/ADRs/0099-codex-agent-runtime.md
Comment thread internal/runtime/codex_run.go Outdated
Comment thread internal/runtime/codex_hook/fullsend-codex-hook.py Outdated
Comment thread internal/runtime/codex_run.go Outdated
Comment thread internal/runtime/codex_redact.go Outdated
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Ended 7:02 PM UTC

Commit: d54cb89 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 7:04 PM UTC · Ended 7:13 PM UTC

Commit: ff0eab8 · View workflow run →

@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

@waynesun09 waynesun09 changed the title feat(runtime): add the codex agent runtime (ADR 0099) feat(runtime): add the codex agent runtime (ADR 0099, ADR 0100) Sep 2, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Ended 7:15 PM UTC

Commit: 30e4029 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 7:17 PM UTC · Ended 7:18 PM UTC

Commit: 08c2f8b · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 7:19 PM UTC · Ended 7:56 PM UTC

Commit: 7c0b764 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 7:57 PM UTC · Ended 8:01 PM UTC

Commit: ed2338b · View workflow run →

Adds what a real `codex exec --json` run against api.openai.com inside the
pinned sandbox image proved, and folds the findings into ADR 0099's
consequences and the runtime matrix.

The three questions the source could not settle are answered: hooks.json loads
with no `[hooks]` table (no empty table needed); a hook block reaches the model
as a decline, not a command failure, in both phases; and a repo's own
`.agents/skills` are discovered even with the project untrusted, which is
Claude Code parity and is covered by the existing SKILL.md context scans.

Two findings change what a reader should expect:

  * with a custom provider codex also issues `GET /v1/models` at startup, which
    the fullsend-openai egress profile denies — non-fatal and logged, but
    "only POST /v1/responses is attempted" is not true of codex the way it is
    of pi;
  * the tee'd output.jsonl keeps each command's raw aggregated_output even when
    a PostToolUse hook blocked the result, so the hooks protect the model's
    context and not the run artifact.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
…earances

The guard asserted "every handler still invokes the adapter" but only checked
that the adapter appeared somewhere in the file, so an agent that replaced one
handler's command with `true` and left another pointing at the adapter passed
it. It now compares the number of `"command":` keys with the number of adapter
references, which a replaced or added handler breaks. Deleting handlers
outright still passes — that narrows the wiring rather than redirecting it, and
is the same residue Claude Code has with its own hooks.json; both the comment
and the contributing page now say so.

Counting is by occurrence (`grep -o | wc -l`), not by matching line: the runner
writes one handler per line but an agent rewriting the file is under no such
obligation, and a compacted hooks.json would make a line count collapse to
1 = 1 and pass. The pattern is the key with its colon, since a bare `"command"`
also matches the value in `"type": "command"`.

Also corrects a claim the local smoke contradicted: codex's tracing is not
silent without RUST_LOG. It logs at error level by default — the denied
GET /v1/models and every hook block — so a codex run writes to the runner
console where a pi run does not.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
The sandbox hook chain protects the model's context: a PostToolUse block
withholds the tool result from the model, and a sanitizer's rewrite is dropped
because codex accepts no output rewrite for built-in tools. Neither touches
what codex writes down. The `exec --json` stream keeps each command's raw
aggregated_output and the rollout session file keeps the same output, and both
are uploaded as run artifacts — so a credential the chain masked for the model
still landed in output.jsonl and transcripts/. Claude Code does not have this
gap: its stream carries the post-hook result.

Both are now filtered through the same security.SecretRedactor the other
runtimes' progress parsers use — the shared pattern list, not a second copy.
The stream is redacted on the tee branch, so the parser still sees exactly what
codex emitted and only the file on disk is masked; the rollout is rewritten in
place after download, preserving its mode. A line that is not JSON is redacted
as text rather than passed through, since that is where a truncated or hostile
payload would sit, and numbers decode through json.Number so an exit_code or a
token count cannot come back in exponent form.

This is redaction, not the hook chain: a canary is masked in the artifact if it
looks credential-shaped, not withheld, and nothing is condensed or normalized.
The matrix cells and ADR 0099 say so. A compressed rollout (.jsonl.zst) is
reported as unredacted rather than silently shipped — decompressing one would
mean shipping a zstd decoder for a file the current iteration did not write.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
The config guard checked config.toml with grep line patterns — that base_url
was the pinned one, that no `[projects` header appeared. TOML's dotted-key
form sets the same value with no bracket header:

    projects."/sandbox/workspace/repo".trust_level = "trusted"

and slipped straight past it. That line makes codex load the target repo's own
.codex/config.toml, which then supplies developer_instructions, model and —
under --dangerously-bypass-hook-trust — repo-authored hooks. Reproduced
against codex 0.152.1 with a deterministic signal: with the line the repo
layer's model was used, without it the runner's was. Indented `base_url` or
`command` lines dodged the duplicate-count checks the same way.

Both files are now pinned by whole-file SHA-256, which has no such blind spot.
The expected digests come from the runner's own memory: Bootstrap records what
it uploaded and Run reads it back. They are deliberately NOT recorded in the
manifest, which is a file in the same agent-writable directory — an agent that
rewrote config.toml could rewrite a hash recorded there to match. Run therefore
requires Bootstrap to have run in the same process for the same sandbox
(internal/cli/run.go does) and fails closed with an error naming that
assumption otherwise. The runtime is absent from config.ValidRuntimes(), so no
configuration can reach this path yet.

Three more holes in the same area:

  * the hook scripts had no integrity check at all, so tirith_check.py could be
    rewritten to exit 0 while every other guard passed. Every file in the hooks
    directory must now match one of this binary's embedded script digests —
    compile-time constants, no anchor in the sandbox needed. This makes codex
    stricter than Claude Code and pi, where the scripts stay unchecked.
  * hooks.json invoked a bare `python3`, resolved through a PATH the agent
    controls because codex spawns hooks after .env is sourced. Bootstrap now
    resolves the absolute interpreter in its preflight and renders it with -I;
    Run also unsets PYTHONPATH, PYTHONHOME and PYTHONSTARTUP.
  * the adapter had no top-level exception handler, so an unexpected failure
    would exit 1 — which codex records as Failed, which does not block. Any
    exception now routes to a block, and a malformed payload blocks on both
    phases rather than only PreToolUse.

base_url and auth.command are additionally passed as `-c` SessionFlags, which
were verified to beat the file. There is no such pin for project trust:
`-c projects={}` and a scalar trust_level="untrusted" were both tried and
neither overrides it, which is why config.toml integrity is enforced by digest
rather than by overrides alone.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
…n the hook block

Three fixes the end-to-end gateway smoke and the review round surfaced.

`codex --version` prints "codex-cli X.Y.Z" where pi prints a bare number, and
the renderer writes "(v" + this + ")", so a run logged
`Agent: gpt-5-mini (vcodex-cli 0.152.1)`. The preflight now parses the number
out and fails closed on an unexpected shape rather than rendering it verbatim.

`--output-last-message` pointed at /sandbox/workspace/output/, which is the
agent's own result directory: nothing creates it up front, so codex reported
`Failed to write last message file ... No such file or directory`, and had it
succeeded the file would have been extracted as one of the agent's output
files. It goes under the runner-owned config dir instead, where
ClearIterationArtifacts sweeps it; the final message is in the stream as an
agent_message either way.

Hardening, all fail-open paths:

  * the adapter's block() wrote to stderr unguarded. A stderr that is already a
    broken pipe would take the interpreter down with exit 1 — which codex
    records as Failed, and a failed hook does not block. The write is now
    suppressed and exit 2 is reached unconditionally: a block without its
    reason still beats a block that never happens.
  * Run now asserts that the runner's hooks signal and the recorded hooks.json
    digest agree. Both derive from SecurityEnabled() today, but nothing said
    so, and a refactor that split them would drop hooks.json from the guard
    while the adapter still loaded, with no failing test.
  * codexHooksAdapterCheck's comment now says it is defense in depth, not the
    boundary: the count comparison is bypassable on its own (pad the adapter
    path into the free-form description field to restore parity), and what
    makes hooks.json tamper-evident is the whole-file digest.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
…file suffixes

Three holes in the layer around the guards, each reproduced before it was
fixed.

The hook-script guard bound digests to a *set*, not to filenames. Overwriting
hooks/tirith_check.py with the bytes of hook_io.py therefore passed: the digest
is in the allowed set, hooks.json is untouched, and hook_io.py is a library
module with no __main__, so it exits 0 with empty stdout — which the adapter
reads as "allow". Tirith silently neutered with every guard green. Each digest
is now bound to its filename, the expected names come from the runner's memory
(Bootstrap records the set the harness enabled), and the directory must hold
exactly those entries: every one enumerated with find, every one a regular
file. That also refuses a symlink to an allowed file, which test -f and
sha256sum both accept and find does not.

The old check globbed `*.py`, so an agent could plant a package directory it
never saw — hooks/json/__init__.py — and normal Python, which prepends a
script's own directory to sys.path, imported it when a hook script did
`import json`. The exhaustive directory check closes the planting vector, and
the adapter now also runs each script with -I. That needed care: the scripts
import their siblings (hook_io, the sanitizer stages), so the verified hooks
directory goes back on sys.path explicitly — appended, not prepended, because
prepending puts it ahead of the standard library and re-opens the very hole -I
closes. Verified both ways, and that tirith_check and posttool_chain still
block correctly under isolation.

ExtractTranscripts trusted a file suffix in an agent-writable directory: it
collected `.jsonl.zst`, and codexRedactFile skipped `.zst` with a warning, so a
plaintext file merely *named* that shipped as an unredacted artifact. Codex
writes the running session uncompressed, so `.zst` is never this iteration's
transcript and is no longer collected at all; every remaining candidate must
parse as a codex rollout envelope on its first line, and anything that fails
that — or that cannot be redacted — is discarded rather than presented as the
run's transcript.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
…block unreadable hook output

Follow-ups from the second review pass, on the layer around the guards.

ExtractTranscripts passed `find` output straight to a download. A path from an
agent-writable directory is now checked before it is followed: it must sit
under the sessions directory — a sibling like `<sessions>-evil/` is refused,
not just a `..` segment — and carry no control characters, since a newline in a
filename splits one listing entry into two. `find -type f` already skipped
symlinks, so a link to /sandbox/workspace/.env was never listed; the prefix
check covers the rest of the class.

codex-debug.log was downloaded unredacted. codex logs at error level by default
and raises to whatever RUST_LOG asks for, so it carries the same material
output.jsonl does and now gets the same pattern redaction; a redaction that
fails deletes the file rather than shipping it, since it is a convenience
artifact the run does not depend on.

The adapter treated hook output it could not interpret as "nothing to do": a
script that printed a rewrite or a decision in a shape the adapter did not
recognise was read as an allow. That is now a block, which is the whole reason
the adapter exists. Its children also run with `-s` and a PYTHON*-stripped
environment carrying PYTHONNOUSERSITE=1, so a `sitecustomize.py` cannot run at
interpreter start — the scripts' own configuration (FULLSEND_CANARY_TOKEN,
TIRITH_*, the allowlists) is passed through, and a container check confirms the
allowlist hook still sees it and still blocks.

Tests: the hooks directory now also refuses __pycache__, an extension module
and a nested package; a spoofed transcript is absent from the artifact set; an
empty or truncated stream with codex exiting 0 still fails the run.

Verified in the sandbox image after the isolation change — allow 0, tirith
block 2, PostToolUse chain 0, canary block 2, tool-allowlist block 2, and a
planted hooks/json package no longer executes.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
Lifts codex_redact.go from 79.6% to 96.3% and codex_transcript.go from 79.2%
to 89.6%, so the per-file view a reviewer clicks on matches the aggregate.
Both were short only on error branches the fake sandbox always succeeds at, so
the tests inject the failures: a sink that fails mid-tee, a stream with no
newline reaching the line cap, a missing file, a download that fails, and an
unreachable sandbox.

Two of them document behaviour rather than just exercising it. The tee side of
an io.TeeReader must report a full write even when the sink fails — reporting
short would abort the read of an otherwise fine stream — while still surfacing
the error, and both halves are now asserted. And a path outside the sessions
directory is asserted never to reach a download at all, which is the property
the prefix check exists for rather than an incidental one.

Writing them turned up one thing worth stating in the test: a `find` that exits
non-zero is deliberately not an error, because the command carries `|| true` —
find reports a non-zero status for an unreadable subdirectory, which is no
reason to fail a run. The error path there is an unreachable sandbox, and that
is what the test now covers.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
…guarded dir

`-I` does not imply `-B`, and `-E` makes PYTHONDONTWRITEBYTECODE inert, so the
first hook that imported a sibling wrote hooks/__pycache__/*.pyc. Nothing
clears the hooks directory between iterations and Run's guard requires it to
hold exactly the files fullsend installed, so iteration 1 of a run armed a
fail-closed lockout for iteration 2: a validation-loop retry would refuse to
start and blame tampering. Reproduced as four .pyc files after a single
PostToolUse chain run.

The children now run with -B and set sys.dont_write_bytecode, and a test runs
both phases three times over and asserts both that the directory is unchanged
and that the guard still passes afterwards — the property that actually
matters for the next iteration. An agent-planted __pycache__ is still refused.

Four things around it:

  * the sys.path ordering rests on -I keeping a script's own directory off
    sys.path, so Bootstrap's preflight now asserts Python >= 3.11 rather than
    assuming it, and an adapter-level test plants hooks/json/__init__.py and
    proves the standard library still wins.
  * "security enabled, every hook disabled" was a hard error. That is a legal
    harness — pi allows it, distinguishing a nil hook plan from an empty one —
    so the check is now on a nil digest map rather than an empty one.
  * codexIsRolloutFile swallowed scanner.Err(), reporting an oversized first
    line as "file is empty"; it still fails closed, now for the real reason.
  * the contributing page still claimed .jsonl.zst rollouts are collected.

PYTHONNOUSERSITE is inert under -I; it stays as a second latch with a comment
saying so.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
…er invocation

Three more ways the hook chain could be neutered with every digest green.

PATH was never re-pinned after the agent-writable .env was sourced, and the
hook scripts resolve their tools by name — tirith_check.py runs a bare
`tirith`. A .env prepending a directory with a fake `tirith` that exits 0
therefore disabled the entire PreToolUse chain for the whole run. Reproduced:
the same blocked command returned exit 0 with the plant and exit 2 without it.
Run now captures PATH into a readonly variable before .env and restores it
after, and exports it so the adapter can set its children's PATH from a value
that never passed through .env rather than trusting what it inherits — the
defence should not rest on nothing having touched PATH in between. Verified in
the sandbox image both ways.

LD_PRELOAD, LD_LIBRARY_PATH and LD_AUDIT reached codex and the hook children
from .env, where they would load code into any dynamically linked program the
run starts — codex's own binary, tirith, git — before its main, which no digest
of ours would see. They are unset after .env and stripped from the children's
environment.

The hook-script digests were checked once, at launch, leaving the agent the
whole iteration to rewrite a script before the next tool call reached it — a
window Claude Code and pi leave open too. The run command now exports the
name→digest map into the codex process's environment after .env and before
launch, and the adapter re-reads and re-verifies each script immediately before
spawning it. A running process's environment is not something the agent can
reach, so this is trustworthy at every invocation; a missing or malformed map
is itself a block.

Also: a rollout is validated at every line rather than only its first, so a
file cannot open with one genuine envelope and carry anything after it;
transcripts are downloaded to a staging name and renamed into place only after
validation and redaction, so a crash cannot leave raw tool output where the
artifact collector reads; and reads are bounded, so a planted multi-gigabyte
file is refused rather than pulled into the runner's memory.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
The adapter forwards the hook scripts' configuration —
FULLSEND_EGRESS_ALLOWLIST, TIRITH_REQUIRED, TIRITH_FAIL_ON — from codex's
environment, and appendHookEnv writes those into the workspace .env at
bootstrap. That file is agent-writable, so iteration 1 could widen the SSRF
allowlist or clear TIRITH_REQUIRED for iteration 2 and the hooks would read
the agent's values. Reproduced: sourcing an agent-appended .env yields the
attacker's allowlist.

The runner now re-exports what it derived from SandboxHookConfig after .env,
so the .env copy cannot win, and the adapter passes exactly those to its
children — the same shape as the PATH pin. FULLSEND_CANARY_TOKEN and
FULLSEND_TOOL_ALLOWLIST are deliberately not covered: they come from the
harness's own env/host_files rather than from the hook config, so there is no
runner-side copy to re-export, and ADR 0100 says so rather than implying the
whole set is pinned.

ADR 0099 decided two things, which the repo's writing-adrs skill forbids and
three reviewers have now flagged. Split: 0099 keeps the credential decision
(custom provider, auth.command, runner-seeded token file) and states plainly
that it amends ADR 0092, whose in-sandbox half is pi-specific; 0100 takes the
sandbox-hooks decision (protocol translation, detect+block, per-invocation
digest re-verification, pinned interpreter and PATH). Both are Accepted, both
within the skill's sizing, and the implementation evidence stays in the
contributing page.

Living documents updated with them, as acceptance requires: a cross-reference
annotation on 0092 (permitted for an accepted ADR; no rewrite of its Context,
Decision or Consequences), and the threat model's claim that "agents cannot
modify their own guardrails" now carries the qualification it needs — the hook
scripts stay writable between iterations on Claude Code and pi, and are
digest-verified per invocation only on codex.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
`agent-architecture.md` has no open question ADR 0100 resolves and no passage
that substantively discusses sandbox tool hooks — its only "runtime" mention
is about orchestration frameworks. `tool-call-risk-assessment.md` does: it
names Tirith, the SSRF validator, the canary hook and the unicode normalizer,
and has a "Relationship to existing hooks" section about exactly the layer this
ADR wires into codex.

So `relates_to` names that doc instead, Context links it, and the reciprocal
backlink AGENTS.md asks for goes at the passage that motivates it — noting
that how far those hooks reach depends on the runtime carrying them, which is
the difference 0100 records.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
"Process memory" reads as agent memory in a codebase full of agents, which is
the opposite of what it means here. The concept is now named for what it is
throughout: **runner-held digests** — digests the runner records outside the
sandbox at Bootstrap and injects into the launch command at Run.

Renames codexArtifactHashes to codexRunnerHeldDigests, its record/lookup/forget
helpers to match, codexUploadedHashes to codexRunnerHeldDigestSet, the local
`hashes` variables to `digests`, and the affected test names. The guard's
failure text now says "no runner-held config digests" rather than referring to
recorded hashes. ADRs 0099 and 0100, the contributing page's anchor table and
its prose carry the same term and state the definition once.

"Anchor" survives in exactly one place, which is the sentence it earns: why the
agent-writable manifest was rejected as the place to keep these digests.

No behaviour change; the guards, their inputs and their exit codes are
untouched.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
…ting the manifest model

Three items from the review of #6926.

PR B renamed the stream fixtures to `.ndjson`, and the codex runtime tests
still asked for `.jsonl`. The fake sandbox then streamed nothing, so
TestCodexRun_StreamVerdictOverridesExitZero passed for the wrong reason (an
empty stream is also a failed run) while _SuccessfulRunReportsMetrics and
_AcceptsManifestHookPlan failed outright. Every fixture reference now names
`.ndjson`; the destination names in ParseTranscriptErrors stay `.jsonl`,
because that is what the runtime actually writes and what it scans for. One
stale `basic_run.jsonl` mention in PR B's own doc comment is corrected in
passing.

An empty PreToolUse payload exited 0, which the adapter reads as allow. The
scripts treat empty stdin as "no tool call" and that is right for them — they
also run standalone — but the adapter was invoked *because* a tool call is
about to happen, so an empty payload means the call cannot be scanned rather
than that there is nothing to scan. It now blocks. PostToolUse keeps the no-op:
the call has already run and there is nothing left to prevent.

Run took its fallback model from the manifest, which sits in the agent-writable
config directory and carries no digest — so an agent could change the model,
and the cost tier, a validation retry runs on. Bootstrap now records the model
it parsed from the agent definition alongside the runner-held digests, and Run
resolves from that; the manifest's copy is never read. The test seeds a
manifest naming a different model and asserts the launch ignores it.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
FULLSEND_CANARY_TOKEN and FULLSEND_TOOL_ALLOWLIST reach the hooks through the
agent-writable workspace .env like the other three, so iteration 1 could clear
the canary token or rewrite the tool allowlist and disable those checks for
iteration 2. I had recorded this as unfixable in the runtime because
SandboxHookConfig does not carry them — that was wrong, and the review was
right.

The runner writes .env and then calls Bootstrap, before any agent iteration
runs, so at that moment the file is still exactly what the runner put there.
Bootstrap reads the two values back from it and records them beside the other
security values in the runner-held state; Run re-exports all five after .env,
so the agent's copy cannot win, and the adapter passes them to its children.
Reading them in Run instead would read whatever the previous iteration left
behind, which is the exposure itself.

Unset values are skipped: there is nothing to re-assert, and an agent that
*sets* one later can only cause spurious blocks, not slip past a check. A
malformed answer from the sandbox fails the run rather than silently yielding
no pins.

ADR 0100's consequence loses its "these two stay at Claude/pi exposure" caveat,
and the matrix and contributing text say the canary token is pinned on codex.

Refs #6920

Assisted-by: Claude (implementation)
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:50 AM UTC · Completed 1:11 AM UTC

Commit: 11a562d · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $12.62

@waynesun09
waynesun09 added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit baf6379 Sep 3, 2026
22 checks passed
@waynesun09
waynesun09 deleted the codex-runtime-core branch September 3, 2026 01:10
@fullsend-ai-review

Copy link
Copy Markdown

Review skipped — this PR is already merged.

The /fs-review command only reviews open PRs/MRs.

Posted by fullsend post-review check

@fullsend-ai-retro

fullsend-ai-retro Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 1:12 AM UTC · Completed 1:32 AM UTC

Commit: 11a562d · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $6.72

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #6926 — Codex agent runtime (ADR 0099, ADR 0100)

PR: #6926 by waynesun09 — 6,651 additions across 26 files implementing the Codex CLI as a fullsend agent runtime with secretless OpenAI credential delivery, sandbox hooks, and security hardening.

Timeline

Time (UTC) Event
18:52 PR opened
18:57 Qodo bot posts 7 findings (3 HIGH security, 4 MEDIUM)
19:06 Author responds to Qodo findings, begins fixing
20:45 First fullsend-ai-review run fails — both iterations timed out at 20m writing agent-result.json
21:16 ralphbean approves with 2 inline comments (test fixture naming, fail-open on empty stdin)
22:24 Author fixes ralphbean's findings
23:09 First successful review agent run completes ($14.57) — finds fail-open on canary token/tool allowlist
00:01 Second successful review agent run ($16.11) — re-posts same findings
00:27 Author fixes canary token fail-open vulnerability
00:50 Third successful review agent run ($12.62) — confirms fix, notes remaining low-severity items
01:10 PR merged

What went well

Both human and agent reviews drove real security improvements. The human reviewer caught a test-correctness bug (fixture naming mismatch causing vacuously-passing tests) and a security gap (fail-open on empty stdin for PreToolUse) that no agent identified. The review agent found a distinct security gap (canary token and tool allowlist not re-asserted after .env sourcing) that the human missed. Qodo additionally flagged hooks mutability, SSRF via mutable environment, and manifest-controlled model — all fixed before merge. The combined review surface was significantly stronger than either human or agent review alone.

The author iterated diligently after approval. Despite receiving human approval at 21:16Z, the author continued addressing agent findings for 4 more hours, ultimately fixing the medium-severity canary token fail-open before merging. The system worked as designed.

Evidence for existing issues

  • #2886 (review agent output guarantee): The first review run failed because both iterations timed out at exactly 20 minutes while writing agent-result.json (exit code -1). This is a related but distinct variant — the agent didn't forget to write output (as in Review agent should guarantee output file is written before exit #2886), it ran out of time during the write. See proposal below for the time-awareness fix.
  • #1013 / #5007 (duplicate comments): The review agent posted 10 inline comments across 3 successful runs, with several duplicates (off-by-one posted at two locations, naming-convention posted twice, code-organization posted twice).
  • #4069 (rapid iteration dispatch): 9 review runs were cancelled due to the author pushing 20 commits over ~7 hours. Each push triggered a new dispatch, wasting runner time.
  • #6936 / #6806 (cost telemetry): The failed review run did not report cost in its status comment, despite consuming two full 20-minute Opus iterations (~$14-16 estimated). Total review agent spend for this PR was approximately $57-59.
  • #4107 (Qodo/fullsend overlap): Both Qodo and fullsend-ai-review flagged security concerns in the same files (codex_integrity.go, codex_run.go, fullsend-codex-hook.py), though their specific findings were complementary rather than identical.

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/moderate PR risk: moderate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants