Skip to content

fix(workbench): record a content digest for committed diff targets - #241

Open
rohanpoudel2 wants to merge 3 commits into
openai:mainfrom
rohanpoudel2:fix/diff-snapshot-digest
Open

fix(workbench): record a content digest for committed diff targets#241
rohanpoudel2 wants to merge 3 commits into
openai:mainfrom
rohanpoudel2:fix/diff-snapshot-digest

Conversation

@rohanpoudel2

Copy link
Copy Markdown

Fixes #53

Problem

The contract requires a git_diff target to carry snapshotDigest. scan-manifest.schema.json:

{
  "if": { "properties": { "kind": { "enum": ["git_worktree", "git_diff", "directory_snapshot"] } } },
  "then": { "required": ["snapshotDigest"] }
}

_validate_target enforces the same, and references/scan-contract.md states it in the table of required coordinates, adding that for diffs the digest is calculated "from a deterministic representation of the reviewed content".

The workbench only ever computed one for working-tree diffs. require_diff_target returns no contentDigest for the commit or range kinds:

    return {"kind": kind, "baseRevision": parent, "headRevision": head}
...
    return {"kind": kind, "baseRevision": base, "headRevision": head}

and register_cli_scan set one only when the requested target was working_tree:

if requested_target["kind"] == "working_tree":
    ...
    diff_target["contentDigest"] = worktree_content_digest(repository)

So a committed diff had no digest anywhere. workbench_completion_binding gated on working_tree, api.ts reads diffTarget["contentDigest"] and got undefined, so CODEX_SECURITY_TARGET_SNAPSHOT_DIGEST was never exported. The only instruction the agent has is conditional:

'When "$CODEX_SECURITY_TARGET_SNAPSHOT_DIGEST" is set, use its exact value as scan.target.snapshotDigest. For git_revision, omit scan.target.snapshotDigest.',

With the variable unset and git_revision not applicable, the agent was left to invent a value the contract demands. A draft that omitted it failed at the seal step, after the whole analysis had run — 90 minutes in the report — with:

codex-security: Could not save the Codex Security scan: scan.target.snapshotDigest: expected a non-empty string

--diff origin/main --head HEAD is a range target, so this is the ordinary committed-diff path, not an edge case.

Worth noting the field was also unverifiable where it was optional. verify_manifest_binding compared snapshotDigest only when diff_target_kind == "working_tree", so any pattern-valid string an agent invented for a committed range passed unchecked.

Change

Compute the digest for commit and range targets the same way working-tree diffs already do — over git diff base head with the same flags, format label and field framing as worktree_content_digest, minus the untracked-file component, which has no meaning for committed revisions.

The value then flows through registration into the completion binding, so _populate_unsealed_target_binding fills the field in during draft population and the agent no longer decides it. That is the same path git_worktree digests already take, and it means a draft that omits snapshotDigest is corrected rather than rejected.

Completion binding and manifest verification are extended to any diff kind that recorded a digest, rather than working_tree alone, so the field is now verified instead of merely present.

On redundancy, stated plainly

A base..head pair of resolved 40-character SHAs already pins the reviewed content, so this digest is strictly redundant as an integrity anchor. The alternative fix is to relax the schema so git_diff requires baseRevision/headRevision and only working-tree diffs require snapshotDigest.

I did not take that route because it changes a published contract at schemaVersion 1.0, and manifests produced under a relaxed rule would be rejected by any SDK that predates the change. Computing the digest keeps the contract exactly as documented and as already validated on both sides, and it converts a field the agent was guessing into one the workbench owns and checks. If you would rather relax the schema, the change is small and I am happy to redo it that way.

Compatibility

diff_content_digest is already a nullable column, so no migration is involved. Scans registered before this change have no digest: the completion binding keeps omitting snapshotDigest for them and verification keeps skipping it, exactly as today. Only newly registered committed diffs gain the field.

Cost is one git diff base head at registration, the same shape of work worktree_content_digest already does against HEAD.

Verification

Added a test to scan-recovery.test.ts that drives the real workbench end to end: it builds a two-commit repository, registers a refs diff scan for the committed range, and drafts exactly what the report describes — a git_diff target with baseRevision and headRevision but no snapshotDigest. It asserts the registration recorded a well-formed content digest, that the scan seals, and that the sealed manifest carries that digest along with the true base and head.

With the fix reverted, the registration reports no digest and the test fails:

error: Received value must be a string: undefined
(fail) seals a committed range diff scan with the registered content digest
16 pass / 1 fail

Driving the same draft through complete-scan against the unfixed source reproduces the reported message directly:

registered contentDigest: undefined
FAILED: Could not run the Codex Security workbench: scan.target.snapshotDigest: expected a non-empty string

and with the fix the same draft seals, with snapshotDigest populated from the registration:

registered contentDigest: codex-security-snapshot/v1:sha256:644304fa1420b6f1ed4f02fe9f9470062fc742b2a3b68f2b0db61b67ed6c10bc
COMPLETE: complete
sealed target: {"baseRevision":"2da3f546...","headRevision":"90daba6f...","kind":"git_diff","snapshotDigest":"codex-security-snapshot/v1:sha256:644304fa..."}

The existing working-tree diff coverage is unchanged, so the digest path that already worked is not disturbed.

Full suite on this base: 727 pass / 5 skip / 0 fail. pnpm run types and pnpm run format are clean, and both changed Python modules compile under python3 -m py_compile.

The contract requires a git_diff target to carry scan.target.snapshotDigest,
both in scan-manifest.schema.json and in _validate_target. The workbench only
computed one for working-tree diffs: require_diff_target returned no
contentDigest for commit or range kinds, and register_cli_scan set one only when
the requested target was working_tree.

So a committed diff had no digest to record. CODEX_SECURITY_TARGET_SNAPSHOT_DIGEST
was left unset, and the scan prompt only says to copy that value when it is set,
which left the agent to invent a value the contract demands. A draft that omitted
it failed at the seal step with "scan.target.snapshotDigest: expected a non-empty
string" after the whole analysis had run, which is 90 minutes in the report.

Compute the digest for commit and range targets the same way working-tree
diffs already do, over git diff base head, using the same format label and
field framing as worktree_content_digest. The value then flows through
registration into the completion binding, so draft population fills it in and
the agent no longer decides it. Extend the completion binding and manifest
verification to any diff kind that recorded a digest rather than working_tree
alone, so the field is verified instead of merely present. Scans registered
before this have no digest and keep omitting it.

Fixes openai#53
@github-actions github-actions Bot added the bug Something isn't working label Aug 3, 2026
@mldangelo-oai

Copy link
Copy Markdown
Collaborator

@codex review exact head 0bb5d75

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 0bb5d75aab

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bb5d75aab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"kind": kind,
"baseRevision": base,
"headRevision": head,
"contentDigest": committed_diff_content_digest(target, base, head),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject changed committed-target digests

When a saved range is revalidated at scan start, content_digest contains the digest recorded during selection, but this branch ignores it and silently returns a newly computed value. The same revisions can produce different reviewed bytes if mutable Git state changes—for example, a replace ref changes the objects Git reads, or diff.renames changes the generated patch—so the scan may review a different snapshot without asking the user to reselect it. Compare a supplied digest with the computed value and fail on mismatch, as the working-tree branch already does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex address that feedback

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"kind": kind,
"baseRevision": parent,
"headRevision": head,
"contentDigest": committed_diff_content_digest(target, parent, head),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive the empty tree for SHA-256 repositories

When the selected commit is the root commit of a SHA-256 repository, parent is the hard-coded SHA-1 empty-tree ID 4b825d...; the new digest call then passes that nonexistent object to git diff and exits with “bad revision,” so the root commit cannot be selected for review. Derive the empty-tree object ID using the repository's object format instead of passing the SHA-1 constant into this call.

Useful? React with 👍 / 👎.

"""

repository, pathspec = git_worktree_context(target)
tracked = git_bytes(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stream committed diffs into the digest

For a committed range containing a large changed binary, git_bytes captures the entire git diff --binary output in memory before hashing it. Binary patches can be substantially larger than the underlying files, so merely registering or starting such a scan can exhaust the workbench process's memory before review begins; stream stdout incrementally into the digest rather than materializing the full patch.

Useful? React with 👍 / 👎.

…y tree

Selecting a root commit resolved its base to the hard-coded SHA-1 empty-tree id.
That object does not exist in a SHA-256 repository, so the new content digest ran
`git diff` against an unknown revision and failed with "Could not snapshot the
selected committed changes.", leaving the root commit unselectable. Look the
empty tree up by the repository's object format instead. Git before 2.29 cannot
report a format and only ever wrote SHA-1, so the SHA-1 constant stays the
fallback, and SHA-1 repositories keep the same base revision and byte-identical
digests.

Revalidating a saved commit or range also discarded the digest recorded when the
selection was made and returned a freshly computed one. Commit ids pin the
revisions but not the bytes Git reports for them: a replace ref substitutes the
objects Git reads, and diff configuration changes the generated patch, so a scan
could review content nobody selected. Compare a supplied digest against the
computed one and ask for a new selection on mismatch, the way the working-tree
branch already does. Selections saved before the workbench recorded digests
carry none and still revalidate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

scan.target.snapshotDigest: expected a non-empty string

2 participants