Skip to content

fix(claude-lanes): treat an already-reviewed new-SHA contract as satisfying lockstep - #890

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/lockstep-existing-contract
Oct 26, 2026
Merged

kyle-sexton merged 2 commits into
mainfrom
fix/lockstep-existing-contract

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: the defect was found while the claude-lanes repin lane PR (#844) stayed on the human checklist after the contracts were registered (#889).

Summary

planLockstep declined any re-pinned caller whose security surface changed from its old pin. It never checked whether policy.json already holds a reviewed contract for <workflowPath>@<newSha>, though copyReusableContract does. The repin lane PR (#844) carried "Runner-policy lockstep requires a human" from 2026-10-22 and kept it after #889 registered the v0.65.14 contracts, with CI green.

Fix

planLockstep takes a hasContract(path, sha) predicate (default: none). After the renamed-path decline, a pin whose new-SHA contract exists is skipped: neither declined nor copied, and no upstream fetch. The renamed-path decline is unchanged, since a rename always needs a hand-reviewed entry. main passes a lookup into policy.approvedReusableWorkflowContracts.

Verification

  • node --test components/claude-lanes/repin-policy-lockstep.test.mjs: 26 pass, including three new cases (surface changed with an existing contract is satisfied; surface changed with none is still declined; a rename with a contract at the new path is still declined).
  • bash harness/shell/run-tests.sh components/claude-lanes/repin-callers.test.sh: pass.
  • npx biome check components/claude-lanes/: clean.

Related

Refs: #844, #889

🤖 Generated with Claude Code

https://claude.ai/code/session_01DpzEJAE4EMndcWQXiKSZYn

…sfying lockstep

planLockstep declined a re-pinned caller whose surface changed without checking
whether policy.json already holds a reviewed contract for the new SHA, so the
repin lane PR kept "Runner-policy lockstep requires a human" after the
contracts were registered. A same-path pin with an existing new-SHA contract is
now satisfied (neither declined nor copied). The renamed-path decline is
unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpzEJAE4EMndcWQXiKSZYn
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T10:16:18.440289Z 537b835 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

claude-security-review reviewed b9bca366e3207ac654419c867604881911d7a83c: 0 finding(s), 0 as inline comments.

Security review (incremental, b9bca36)

No security findings.

The change since 537b835 touches only components/claude-lanes/repin-policy-lockstep.mjs and its test. It replaces an object-index lookup, policy.approvedReusableWorkflowContracts[key], with a new policyHasContract helper that calls .has(key). The policy there comes from validatePolicy, which builds that field as a Map (components/runner-policy/runner-policy.mjs:167, :354). The old indexing on a Map always returned undefined, so the check always failed closed. The new .has() reads the real validated entries. The key is still the exact workflowPath@sha string.

  • No new input path. The key is built from the caller pins' workflow path and the new SHA. Both are the same values as before, and the SHAs are validated as 40-character hex earlier in main.
  • No widening of trust. The lookup reads only the repo's own validated policy.json. It adds no contract and does not write to the policy. A pin whose surface changed is still declined unless a contract for the new SHA is already registered. Renamed paths are still always declined.
  • Not a listed trust control. The change touches none of the lanes' App-token, kill-switch, trusted-actor, merge-hold or token-broker files.
  • Out of this diff's scope. The copyReusableContract helper still indexes by key on the policy it receives, and that code is unchanged here. The only caller I read, main(), passes it the validated policy, so a Map would defeat it. I did not read the settle code path that receives the policy, so I haven't confirmed that.
  • Tests. The added test covers both a present and an absent contract. It is not a security surface.

No manifest or instruction-surface files changed.

critical-findings: 0

@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: 537b8359ed

ℹ️ 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".

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

claude-review reviewed b9bca366e3207ac654419c867604881911d7a83c: 0 finding(s), 0 as inline comments.

No findings in the incremental change since 537b835.

The fix. main previously looked up contracts with policy.approvedReusableWorkflowContracts[key]. policy there comes from validatePolicy(...), and components/runner-policy/runner-policy.mjs:167 builds that field as a Map, so bracket indexing always returned undefined. The new policyHasContract uses .has(...) and fixes this. The components/claude-lanes/repin-policy-lockstep.mjs change is correct.

Raw-JSON paths. copyReusableContract still indexes with brackets, which is correct because copyForwardContracts works on the raw parsed JSON object (a plain object), not the validated policy.

Test. The new policyHasContract test builds a validatePolicy result from the fixture (policyWithHistory). It checks a registered contract (CURRENT_REVIEW_SHA, true) and an absent one (FRESH_SHA, false), so it exercises the Map path that the earlier tests, which pass a () => false stub, could not.

I read the diff and the surrounding code. I did not run the test suite. The PR body's verification counts are author-claimed. The earlier increments were reviewed at 537b835.

The closing issue is No related issue: …, so there are no acceptance criteria to check. The PR body follows the contract.

…cy's Map

validatePolicy returns approvedReusableWorkflowContracts as a Map, so the
bracket lookup never matched. policyHasContract uses Map.has and is tested
against a validatePolicy result.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpzEJAE4EMndcWQXiKSZYn
@kyle-sexton
kyle-sexton merged commit d927ff2 into main Oct 26, 2026
22 checks passed
@kyle-sexton
kyle-sexton deleted the fix/lockstep-existing-contract branch October 26, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant