Skip to content

Add signed Codex PreToolUse adapter - #162

Open
Lrd0036 wants to merge 6 commits into
GenAI-Security-Project:integrationfrom
Lrd0036:refimpl/codex-pretool
Open

Lrd0036 wants to merge 6 commits into
GenAI-Security-Project:integrationfrom
Lrd0036:refimpl/codex-pretool

Conversation

@Lrd0036

@Lrd0036 Lrd0036 commented Sep 19, 2026

Copy link
Copy Markdown

What changed

Add a signed Codex PreToolUse adapter: tool calls become steps/toolCallRequest, and Guardian decisions become native allow, deny, or supported parameter overrides. It reuses #22's Python signing, handshake, binding, and decision helpers, with configuration, mapping, CI registration, and runnable verification.

Ready for review — depends on #22. This branch is #22 at 7174a033c15f69ee58caaa5eb0a19279592171c7 plus the original Codex commit 3d4bdb3, ServerHello repair d077978, and TypeScript coverage b2d4b49 described below. The full PR diff temporarily includes the inherited adapter stack. For the original Codex slice, review the Codex commit: 15 files, 1,063 insertions, 9 deletions. The optional shared-transport cleanup is excluded.

Before merging, I will rebase the Codex slice and any still-needed repair changes onto integration after #22 lands, reconcile upstream changes, and rerun the checks and live enforcement on that final base. The original results below remain tied to 3d4bdb3; the review follow-up records separate verification of the repaired tree.

Which issue does this implement

Closes #170

Refs #89 (status:accepted, help wanted). This implements the tool-call slice and leaves the issue open for lifecycle/result coverage.

Base branch

  • integration, because this adds an adapter, tests, and CI.

This PR exposes the completed slice while its dependency remains open; it is not ready to merge until #22 lands and the final rebase is complete.

Type of change

  • Reference implementation or adapter
  • Documentation
  • Tooling or CI

I tested this — original commit (historical)

Rerun against Codex-only commit 3d4bdb3e84665e6db13835bc075ee8ec876bc14b:

  • Synced onto the final integration base (pending Add adapters/ with Claude Code, Cursor, and NAT reference implementations #22).
  • uv run --locked pytest -v: 205 passed, one case-insensitive-filesystem skip.
  • uv run --locked mkdocs build --strict: passed.
  • python run_conformance.py claude cursor nat codex: 291 passed, no failures, three expected live Claude/Cursor skips. All 35 NAT tests and all 26 Codex tests ran.
  • Real Codex CLI 0.142.5 plus the unchanged shipped Python Guardian: hooks-disabled control, ALLOW, DENY, and Guardian outage all passed with filesystem assertions. The denied command safely printed a literal policy trigger; it ran with hooks disabled and was blocked with ACS enabled. Signing used a temporary 0600 key file.
  • Real CLI plus the signed fixture Guardian: ALLOW/DENY/MODIFY produced the expected filesystem effects, with emitted signatures and schemas independently validated.

The local model endpoint supplies a fixed tool call; these runs test execution enforcement, not model inference. Live runs used explicit inline hook configuration and the one-shot hook-trust bypass. Project-file hook discovery and interactive trust remain unverified. An isolated project-file probe sent no Guardian requests and was not counted as a pass. The shipped teaching policy does not generate MODIFY, so that path has fixture coverage only. MCP/patch translation has subprocess coverage, not live host coverage.

Review follow-up — d077978

Repair commit d07797805945801e443ab04eaf3a1f8c5c332ff8 reads ServerHello directly from result for fresh and cached handshakes, corrects both Python producers, migrates affected fixtures, and adds 13 regressions. Request-ID binding, signature verification, and whole-envelope caching remain in place. Cached legacy wrappers trigger renegotiation.

Separate local verification on the repaired working tree, before committing:

  • Adapter gate: 304 passed, 3 expected live Claude/Cursor skips; all 35 NAT and 26 Codex tests ran. The 13 new regressions passed; on the original tree, 6 failed and 3 errored, with 4 passing.
  • Repository guards: 205 passed, 1 filesystem skip. Strict MkDocs build passed.
  • Real Codex CLI 0.142.5: signed fixture ALLOW/DENY/MODIFY passed; repaired shipped Python Guardian control/ALLOW/DENY/outage passed. The live-host limitations stated above still apply.

The local security assessment reviewed an uncommitted snapshot. Reconstructing the saved repair on 3d4bdb3 reproduces its snapshot SHA-256 a126300c22a821a33c5b84c29b7b4b37ec7cf2f9c624879a609da69db67bc9e7 and the published commit's complete Git tree 95757e340f91579273287a33e582997d5c5f9e3e. No source changed between that snapshot and publication; the local commit amendment changed only its message. These are pre-commit local results linked by identical source content, not post-push CI results.

TypeScript diagnostic at d077978 (historical). A separate local probe against the actual TypeScript Guardian validated the direct ServerHello shape and observed an unsigned response; configured-key Codex refused it. Its documented endpoint lacks authentication and request signing. This proves shape compatibility and unsigned-peer rejection, not successful authenticated integration. That probe is not committed coverage in d077978. The b2d4b49 follow-up below now commits that coverage. Positive signed interoperability remains pending TypeScript signing support; signature verification stays enabled.

Public CI for d077978: Deploy Pages and adapter-tests both report action_required, with zero jobs reported as of 2026-09-20. They establish neither passing validation nor test failures. Maintainer action and completed checks are still pending.

TypeScript coverage follow-up — b2d4b49

Commit b2d4b49092861bf7cee7cddff83572e6f8e5d395 adds three tests against the shipped TypeScript Guardian process, a dedicated CI job, and reproduction instructions. It validates the direct ServerHello against the canonical schema, verifies that the configured-key shared helper refuses the unsigned response without caching it, and verifies that default-deny Codex refuses it before sending a tool request. The job also runs the signed Python controls. Missing Bun or workspace dependencies fail the job rather than skip coverage.

Local verification on the source tree published in this commit:

  • Live TypeScript suite: 3 passed, no skips, with Bun 1.3.14 and Python 3.12.13.
  • Signed Python ServerHello regressions: 13 passed.
  • Headless adapter gate: 304 passed, 3 expected Claude/Cursor skips; all 35 NAT and 26 Codex tests ran.
  • Repository guards: 205 passed, 1 filesystem skip. Strict MkDocs build passed.

An initial adapter run found the locally installed Claude CLI but failed its two live tests because it was not logged in. The headless rerun excluded that optional CLI from PATH and reported the expected skips. Claude live enforcement is not verified here.

This commit changes only tests, CI, and documentation. The prior security snapshot remains evidence for d077978; it does not cover this later commit. These tests establish wire-shape compatibility and unsigned-peer rejection, not successful signed TypeScript interoperability. The Codex test runs the production adapter subprocess, not the Codex CLI.

Public CI for b2d4b49: adapter-tests and Deploy Pages report action_required. Public validation is still pending; these are local results.

Checklist

  • Commits are signed off with git commit -s.
  • Prose follows STYLE.md.
  • No secrets, credentials, or private service URLs in the patch; localhost URLs and dummy fixture credentials are intentional.

Security and scope

Missing signing configuration, invalid or misbound decisions, and unsupported modifications deny. ASK/DEFER become audited denial because this slice has no supported approval/resumption path. Temporary Guardian failures follow the configured posture; the example sets default-deny.

The adapter advertises one method, HTTP, and no ACS-Core profile. Results, lifecycle events, hosted tools, continued process input, and hook-disabled/untrusted paths remain outside coverage. No specification or policy-engine changes are included in the Codex commit.

bar-capsule and others added 3 commits September 8, 2026 14:49
One squashed commit carrying the full PR GenAI-Security-Project#22 branch, rebuilt on current
main (DCO: all history signed; branch is a direct child of main).

Adapters (each: acs_adapter, wire.py, mapping.md, README, tests):
- Claude Code: hook-to-steps translation with explicit turn tracking
  (turnStart/turnEnd, per-session state), native allow/deny/ask,
  modify via merged updatedInput, defer substituted to deny + audit.
  SubagentStop is deliberately unmapped: steps/subagentStop requires
  final_chain_hash, which a chain-less framework cannot honestly
  produce; a separate schema PR proposes making it optional.
  ADAPTER_VERSION 0.1.3.
- Cursor: documented-field payload builders (docs.cursor.com,
  2026-08-22), turn tracking, beforeSubmitPrompt exit-2 blocking,
  failure_type-to-exit_status mapping, fail-closed contradictory
  modifications per §6.3.
- NAT: pre/post-invoke middleware with negotiated timeouts, durable
  audit sink, redaction-or-deny output gate.

Shared infrastructure:
- acs_common: RFC 8785 (JCS) + HKDF per-session HMAC signing (§10),
  handshake with signed ServerHello binding and negative cache,
  total decision normalization, §6.3 composition-violation check.
- example_guardian: signed error envelopes, capped regex scanning,
  durable file-locked replay state, subagent gate no weaker than the
  generic tool gate. Binds --port 0 and announces the assigned port
  on stdout so test spawns own their port by construction.
- Conformance + emission suites (230 checks) driving both CLI
  adapters and the Guardian; NAT covered by its own suite (35).

Guardian refusal handling is deliberately stricter than v0.1 spec
text (always fail closed); tracked as spec issue GenAI-Security-Project#32.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bar Kaduri <bar@capsule.security>
Translate tool calls through the shared ACS signing and handshake helpers.
Apply allow, deny, and supported parameter modifications using Codex's native
hook output; deny unsupported ASK/DEFER and unusable modifications.

Include configuration, mapping, emission tests, CLI enforcement verification,
and conformance-runner/CI registration. Advertise only the implemented method
and transport, with no ACS-Core profile claim.

Refs GenAI-Security-Project#89; first tool-call slice.
Based on GenAI-Security-Project#22.

Signed-off-by: Lrd0036 <lrd0036@auburn.edu>
@github-project-automation github-project-automation Bot moved this to Needs triage in ACS Project Tracker Sep 19, 2026
@acs-board-reconciler acs-board-reconciler Bot moved this from Needs triage to In progress in ACS Project Tracker Sep 19, 2026
@President

President commented Sep 20, 2026

Copy link
Copy Markdown

Thanks for the Codex slice. I found one interoperability issue in ensure_session_handshake.

The current code reads response.get("result") and then looks for a nested payload field:

server_hello = (response.get("result") or {}).get("payload")

ACS v0.1 returns ServerHello directly as result; there is no payload field inside it. The code should read the result itself:

server_hello = response.get("result")

The TypeScript Guardian uses this standard handshake shape too. Could this be corrected and covered by an interoperability test against the TypeScript Guardian?

Read ServerHello directly from the JSON-RPC result on fresh and cached
handshakes. Update both Python response producers and the affected
fixtures while retaining response binding, signature verification, and
whole-envelope cache validation.

Add focused regressions for direct-result negotiation, cache reuse,
tampering, unsigned responses, request binding, legacy wrappers,
schema conformance, and the shipped Python Guardian round trip.

Signed-off-by: Lrd0036 <lrd0036@auburn.edu>
@Lrd0036

Lrd0036 commented Sep 20, 2026

Copy link
Copy Markdown
Author

Yes! Thanks for catching this. d077978 fixes both fresh and cached reads, both Python response producers, and the affected fixtures. It adds 13 regressions while preserving signature verification, request binding, and whole-envelope caching. The repaired local adapter gate passed 304 tests with three expected live-product skips; the PR description now separates those results from the original commit's results.

The TypeScript portion is still open. I ran the TypeScript Guardian, and its direct ServerHello validates against the handshake schema... but the response is unsigned. Its README documents the authentication/signing bit. With a nonempty key and signature verification enabled, Codex rejects that peer. This is wire-shape compatibility and rejection coverage, not a successful signed integration, and the diagnostic is not committed in this repair.

Would a committed test that launches the TypeScript Guardian, validates its direct-result shape, and asserts rejection by the signed Python/Codex path meet the coverage expected for this repair, with positive signed end-to-end interoperability tracked separately pending TypeScript signing support?

The new head's Deploy Pages and adapter-tests runs both show action_required with no jobs reported, so public CI has not validated the repaired tree yet.

Follow-up: I’ve committed the TypeScript coverage in b2d4b49. It launches the actual Guardian, validates the direct ServerHello shape, and checks that the shared helper and default-deny Codex adapter reject its unsigned response. Signature verification remains enabled.
All three tests pass locally and now have a dedicated CI job. The headless adapter gate passes 304 checks with three expected skips. Successful signed interoperability still requires TypeScript signing support.
The current adapter-tests and Deploy Pages runs need maintainer approval.

Launch the shipped TypeScript Guardian and validate its direct ServerHello
against the canonical schema. Exercise the signed shared helper and Codex
adapter against its unsigned reply, asserting rejection without a cached
handshake or a tool request.

Run this coverage in a dedicated Bun/Python CI job alongside the signed
Python controls. Document the distinction between wire-shape compatibility
and successful authenticated interoperability.

Signed-off-by: Lrd0036 <lrd0036@auburn.edu>
@rocklambros

Copy link
Copy Markdown
Contributor

@Lrd0036 you had the ServerHello fix up the same day @President found it, with 13 regression tests behind it, and b2d4b49 put a TypeScript interop job on top. The description also keeps local results and pending CI results in separate sections, which makes it easy to see what has run where.

I've got one housekeeping ask. integration is this repo's default branch, so GitHub closes any issue a PR names with Closes the moment that PR merges. Writing Closes #89 would shut #89 while result and lifecycle coverage are still open. I filed #170 for the tool-call slice you built, accepted it, and put it under #89 as a sub-issue.

Could you add Closes #170 on its own line under "Which issue does this implement"? Keep Refs #89 where it is. The ... menu at the top right of the description has an Edit option, or gh pr edit 162 --body-file <file> does it from a terminal.

@Lrd0036

Lrd0036 commented Sep 21, 2026

Copy link
Copy Markdown
Author

Done, thanks!

@Lrd0036

Lrd0036 commented Sep 21, 2026

Copy link
Copy Markdown
Author

Oops. Both authoritative and conformance failures come from the same citation guard in adapters/test_acs_core_conformance.py: an upstream diagram moved the specification’s line numbers.
The fix anchors citations to the named requirements within ACS-Core. Prod code and existing security assertions are unchanged. Looks like Codex and TypeScript checks already passed in CI.
Local validation with the fix applied:

  • Exact failing CI snapshot: 69 passed.
  • Combined PR/base tree: 307 passed, 3 expected skips.
  • Repository checks and strict docs build passed.
  • Fresh ServerHello, Codex, and TypeScript regression checks passed with zero skips.
    No regressions against the published PR head. The fix is still local; GitHub CI needs to rerun after I push it.

Anchor the Core citation guard to its section and ordered requirement names so upstream diagrams do not break conformance checks. Preserve behavioral assertions and cover moved, missing, and renamed citations.

Disable checkout credential persistence in the adapter jobs and correct the setup-bun version comment to match its existing pin.

Signed-off-by: Lrd0036 <lrd0036@auburn.edu>
@Lrd0036

Lrd0036 commented Sep 21, 2026

Copy link
Copy Markdown
Author

Fixed & pushed 49f5232. Citation guard now follows ACS-core reqs. instead of the fixed line numbers. Also addressed that workflow cleanup flagged by #179.
Both authoritative and conformance now pass. The full adapter run finished w/ 307 passed and 3 expected skips; Codex, TypeScript, DCO, and docs checks passed too. CI results. Production adapter code & existing assertations remain unchanged. Thanks!

rocklambros added a commit that referenced this pull request Sep 22, 2026
…179)

CodeQL default setup was turned on for this repository on 2026-09-21 and covers the Actions language. This adds zizmor, which the [OWASP GitHub Actions Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/GitHub_Actions_Security_Cheat_Sheet.html) recommends beside CodeQL for defense in depth. zizmor reads each workflow's logic and checks what CodeQL does not: hash pins no tag points to, version comments that disagree with their pin, App tokens broader than the job needs, and credentials a checkout leaves in `.git/config`.

Run against `integration` before this change with online audits on, zizmor 1.30.1 reported 15 findings. This clears 13. The other two are `github-app` findings on `board-reconcile.yml`, whose App token takes the installation's full grant instead of naming its repository and permissions. Token scopes are not changing right now, so those two stay open in code scanning, where they record the deferred decision instead of disappearing behind a suppression.

- **`pr-intake.yml`, 1 high.** zizmor flags every `pull_request_target`. This one checks out no code and reads the pull request body as data through an environment variable, which the workflow's header already explains. It carries an inline `zizmor: ignore` that points at that explanation.
- **Eleven checkouts across nine workflows, 11 low.** Each left the job's token in `.git/config`. None of those jobs pushes with git. They call `gh` with `GH_TOKEN` or touch no network at all, and `sync_version.yml` hands its push to `create-pull-request`, which configures its own token. All eleven now set `persist-credentials: false`. The checkout in `validate-owasp-metadata.yaml` also gains the `# v7.0.1` comment every other pin carries.
- **`reference-implementation.yml`, 1 medium.** The setup-bun pin `735343b` is v2.0.2, and its comment said v2.0.1. Only the comment changes. Dependabot's #83 already treats it as 2.0.2.

The new workflow runs zizmor from a `zizmor` dependency group in `uv.lock`, pinned with hashes. No new action enters the Actions allowlist, and Dependabot's uv ecosystem moves zizmor under the existing 7-day cooldown. The group is not a default group, so `uv sync --locked` in the deploy and test jobs never installs it. A default sync against this lockfile would uninstall it.

It runs on pull requests and pushes that touch `.github/`, an action definition, `pyproject.toml`, or `uv.lock`, and weekly, because the online audits compare pins against advisories published after the pin. Findings upload as SARIF under the category `zizmor`, and the job stays green on findings so code scanning tracks them and a ruleset can gate on them later. A tool or upload failure still fails the job. A pull request from a fork cannot upload with its read-only token, so it gets annotations instead. `upload-sarif` is pinned at v4.38.0 because v4.38.1 was three days old, inside the cooldown this repository applies to its dependencies.

Open pull requests #162 and #169 add workflows that zizmor will flag once they land, for unset `persist-credentials` and the same `# v2.0.1` setup-bun comment.

Verified locally: zizmor 1.30.1 with online audits reports only the two deferred `github-app` findings, with 1 ignored and 26 low-confidence findings suppressed by the default persona. The workflow's own command produces valid SARIF 2.1.0. `actionlint` is clean. `uv lock --check` passes under uv 0.9.9, the version CI pins, and the lockfile diff adds only zizmor. `uv run pytest` gives 278 passed, 1 skipped.

Signed-off-by: Rock Lambros <rock@rockcyber.com>

This branch has not been deployed

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

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[RefImpl] Codex adapter: tool calls through PreToolUse

4 participants