Skip to content

fix(node): honor pre-assigned task assignee on claim - #275

Open
Ayush7614 wants to merge 4 commits into
Gitlawb:mainfrom
Ayush7614:fix/claim-task-assignee-integrity
Open

fix(node): honor pre-assigned task assignee on claim#275
Ayush7614 wants to merge 4 commits into
Gitlawb:mainfrom
Ayush7614:fix/claim-task-assignee-integrity

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • claim_task previously updated any pending row and overwrote assignee_did, so a stranger could steal a task pre-assigned to someone else and receive its ucan_token / payload.
  • Claim now admits only the reserved assignee (DID-normalized) or first-claimer on open (NULL) tasks; the UPDATE re-checks the assignee slot to close the race.
  • finish_task now binds the stored assignee in SQL so complete/fail cannot win a check-then-act race after the handler gate.

Why (direct PR)

Real authz gap on the agent-task surface (REST + GraphQL + DB). Not tracked by an existing issue/PR (distinct from #268 anonymous UCAN reads).

Test plan

  • claim_task_honors_preassigned_assignee (REST) — thief gets 409, no UCAN; reserved assignee claims and receives UCAN
  • claim_task_open_still_first_claimer_wins — open pending tasks still claimable
  • claim_task_honors_preassigned_assignee (GraphQL) — same steal/admit behavior
  • Existing complete_task_authorizes_assignee_only / GraphQL complete tests still pass
  • cargo clippy -p gitlawb-node --all-targets -- -D warnings clean
  • CI test (stable) with Postgres

Summary by CodeRabbit

  • Bug Fixes
    • Task claims now correctly reject attempts to claim tasks reserved for another assignee.
    • Unauthorized users can no longer complete or fail tasks assigned to someone else.
    • Reserved tasks preserve assignment details, while unassigned or blank-assignee tasks remain claimable.
    • Failed or unauthorized actions no longer expose task access tokens or alter task state.
    • Assignment formatting is handled consistently during authorization and filtering.
    • Task activity events accurately identify the persisted assignee.
    • Task creation responses now reflect the task as stored.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Ayush7614, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38395381-5bf5-4284-9c1c-40bd8122cdeb

📥 Commits

Reviewing files that changed from the base of the PR and between e5a8468 and 0484740.

📒 Files selected for processing (1)
  • crates/git-remote-gitlawb/tests/real_git_fetch.rs
📝 Walkthrough

Walkthrough

Task creation now returns the persisted task. Task claiming enforces reserved-assignee authorization. Completion and failure require actor DID matching. API and GraphQL events use persisted assignee data. Tests cover reservation, DID normalization, UCAN handling, and concurrency safeguards.

Changes

Task claim and completion authorization

Layer / File(s) Summary
Database task authorization
crates/gitlawb-node/src/db/mod.rs
Task creation normalizes blank assignees and returns the stored task. Claiming validates reservations. Finishing requires the matching actor DID and exact stored assignment.
API and GraphQL task wiring
crates/gitlawb-node/src/api/tasks.rs, crates/gitlawb-node/src/graphql/mutation.rs
Handlers use persisted task records, map reservation errors, pass actor DIDs, and attribute events to persisted assignees.
Authorization and concurrency coverage
crates/gitlawb-node/src/graphql/mutation.rs, crates/gitlawb-node/src/test_support.rs
Tests cover unauthorized claims, UCAN withholding, open reservations, DID formatting, successful claims, assignee-only completion, and guarded updates.

Repository rule link

Layer / File(s) Summary
Cursor rule link
.cursor/rules/rtk-token-savings.mdc
The Cursor rule file now links to a local rule path.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant GraphQLMutation
  participant Db

  Caller->>GraphQLMutation: claim task
  GraphQLMutation->>Db: claim_task(id, caller_did)
  Db-->>Db: validate pending status and reservation
  alt unauthorized reservation
    Db-->>GraphQLMutation: TaskReservedForOtherAssignee
    GraphQLMutation-->>Caller: reservation error without UCAN
  else authorized claim
    Db-->>GraphQLMutation: persisted claimed task and UCAN
    GraphQLMutation-->>Caller: claimed task
  end

  Caller->>GraphQLMutation: complete or fail task
  GraphQLMutation->>Db: finish_task(id, status, result, actor_did)
  Db-->>Db: verify exact persisted assignee
  Db-->>GraphQLMutation: updated task
  GraphQLMutation-->>Caller: event attributed to persisted assignee
Loading

Possibly related PRs

  • Gitlawb/node#219: This PR also changes GraphQL mutation authorization behavior.

Suggested labels: needs-tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: honoring pre-assigned task assignees during claims.
Description check ✅ Passed The description explains the authorization bug, lists the fix, and provides regression tests and validation results, so it is mostly complete.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the needs-tests Source changed without accompanying tests (advisory) label Jul 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the contribution. A couple of things will help us review this faster:

  • This changes Rust source but no tests changed. Tests are required for fixes and strongly encouraged for features.

See CONTRIBUTING.md. Update the PR and these notes will clear automatically.

@beardthelion beardthelion added crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior labels Jul 29, 2026

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The claim gate holds up. I reverted the did_matches reservation check and both new tests went RED, so the premise is covered. Findings are about the denial's shape and about what the race half of the change actually proves.

Ignore the needs-tests label: the triage job looks for an added #[test] or #[cfg(test)] line and does not know about #[sqlx::test], which is how this repo writes DB tests. Your three tests are there. That regex is ours to fix, not yours, and #277 does it.

One scope note, since it affects how much your tests can claim: the anonymous read of ucan_token through GET /api/v1/tasks/{id} and the GraphQL task query is tracked in #268 and is not in scope here. Your UCAN assertions bind the claim path, which is the right thing for this PR to do, but the token is readable without claiming anything until #268 lands.

Findings

  • [P2] Return 403 for a reserved-task claim, not 409
    crates/gitlawb-node/src/api/tasks.rs:177
    The reservation denial at db/mod.rs:2624 is an anyhow error, and the handler maps every claim_task error to CONFLICT. Driving a thief through the route gives 409 {"error":"task not claimable: reserved for another assignee"}, while the signer-binding denial three lines above uses forbidden() and returns 403 {"error":"forbidden","message":...}. Same concept, two statuses and two body shapes, and a permanent authorization denial is now indistinguishable from a lost race, so a retrying client never stops. I verified a fix: a marker error type on the reserved branch plus a downcast_ref in the handler gives 403 for the steal and leaves an ordinary already-claimed race at 409 {"error":"task not claimable: not found or already claimed"}. The assertion at test_support.rs:504 flips with it; that was the only test that changed.

  • [P2] Add a direct test for the finish_task assignee gate
    crates/gitlawb-node/src/db/mod.rs:2670
    Both handlers already reject a non-assignee with 403 before calling in, so nothing committed reaches the new DB-layer check. I deleted the whole gate (the did_matches branch, the AND assignee_did=$5, and the bind) and all six task tests stayed green, which leaves the signature change unproven. A direct db.finish_task(id, "completed", None, stranger) asserting Err and that the row stays claimed covers it; I wrote that test and confirmed it goes RED with the gate removed and GREEN with it in place.

  • [P2] Keep the stored DID form when claiming a reserved task
    crates/gitlawb-node/src/db/mod.rs:2635
    The UPDATE writes assignee_did=$2, the caller-presented form, even when the row already held a different-but-matching one. Executed: a reservation stored as zBAREKEY... claimed as did:key:zBAREKEY... succeeds and rewrites the row to the did:key: form. list_tasks filters on exact assignee_did=$1 (db/mod.rs:2572, 2589), so the delegator who reserved the task by the bare key stops finding it afterwards. SET assignee_did=COALESCE(assignee_did, $2) fixes it; I ran it and confirmed a reserved task keeps its stored form while an open task still takes the claimer's DID.

  • [P3] Treat a blank reservation as open
    crates/gitlawb-node/src/db/mod.rs:2624
    create_task binds assignee_did from the body without validation, so "" reaches the gate as Some(""), enters the reserved branch and matches nobody. Executed: every claimer gets task not claimable: reserved for another assignee, permanently. Filtering blank at the gate (.as_deref().filter(|r| !r.trim().is_empty())) makes it claimable again; verified.

  • [P3] Narrow the pre-check to the columns the decision needs
    crates/gitlawb-node/src/db/mod.rs:2615
    claim_task now loads the full row, including payload, result and ucan_token, before rejecting a caller who is not the reservation holder. The old single-statement UPDATE rejected without returning anything, so a permissionless caller can replay a denial and make the node read the whole row each time. Bounded by the default body limit rather than unbounded, hence P3, but a SELECT status, assignee_did would keep the denial path cheap.

Not an ask, recorded only: the doc comment and the PR body both credit the new assignee clause in the UPDATE with closing the race. assignee_did has one writer, claim_task's own UPDATE, which requires status='pending', so the pre-existing status predicate already closes that window. I removed the new clause and all six task tests stayed green. It is reasonable defense-in-depth against a future writer; the wording is what overstates it.

@Ayush7614

Copy link
Copy Markdown
Contributor Author

@beardthelion addressed your review findings:

  1. [P2] 403 for reserved claimTaskReservedForOtherAssignee marker; REST handler downcasts to forbidden() (403). Ordinary claim races stay 409. Test assertion flipped to FORBIDDEN.
  2. [P2] finish_task DB gate testfinish_task_rejects_non_assignee_at_db (stranger Err + row stays claimed; assignee completes).
  3. [P2] Keep stored DID formCOALESCE(NULLIF(BTRIM(assignee_did), ''), $2) on claim; claim_task_keeps_stored_assignee_did_form covers bare→full claim.
  4. [P3] Blank reservation = open — blank/whitespace treated as open; claim_task_blank_reservation_is_open.
  5. [P3] Narrow pre-checkSELECT status, assignee_did only before the gate.

Also softened the race wording to defense-in-depth (status predicate remains the primary window closer). Ready for another look.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Findings

  • [P2] Required PR checks did not run on the new head
    GitHub Actions run 30586459176 for cdbee5a0 completed with action_required and zero jobs (test (stable), fmt + clippy, build --release, etc. are absent from statusCheckRollup). Only triage and CodeRabbit report success. Please have a maintainer approve/trigger the fork workflow and confirm green checks on this head before merge.

  • [P3] Align Rust and SQL whitespace handling for blank/open reservations
    crates/gitlawb-node/src/db/mod.rs:2638, 2651-2652
    The "" case is fixed, but Rust trim().is_empty() and PostgreSQL BTRIM() still disagree on tab/newline-only assignee_did values ("\t", "\n"). Rust treats them as open (reserved_exact = None); SQL still sees an occupied slot, so claim always returns 409 and the row stays pending. Apply the same blank definition in Rust and SQL (or normalize whitespace-only values to NULL on create). Also trim consistently in finish_task did_matches (db/mod.rs:2690) to match the claim gate (2640) so tab-padded stored values cannot claim then fail complete/fail.

  • [P3] Emit TaskEventBroadcast.by_did from the persisted assignee
    crates/gitlawb-node/src/api/tasks.rs:188, 242, 296; crates/gitlawb-node/src/graphql/mutation.rs:80, 122, 164
    The COALESCE fix keeps bare-key assignee_did in the row while broadcasts still send the signer's full did:key:… form. That skew is new with this PR (previously claim overwrote to caller form). Subscribers or filters doing exact-string correlation with list_tasks will miss events. Please set by_did from the returned task's assignee_did on claim, complete, and fail.

  • [P3] Match GraphQL steal test parity with REST
    crates/gitlawb-node/src/graphql/mutation.rs:326
    REST reloads the row after a failed stranger claim and asserts status == "pending" and unchanged assignee_did (test_support.rs:302-304). The GraphQL test only checks error strings and UCAN absence. Please add the same get_task assertions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/gitlawb-node/src/db/mod.rs (1)

2543-2569: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Normalize assignee_did before returning create responses.

create_task stores whitespace-only assignment requests as NULL, but both create_task(&task) call sites return task_to_json(&task) / AgentTaskType::from(task) from the same pre-normalized input struct. If a request sends assignee_did: " " for a non-pre-set value, the response exposes it as reserved while the persisted row and later task reads expose it as open. Normalize the returned task struct in both GraphQL and REST create paths, or mutate it in Db::create_task and return the normalized value from the stored row.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/db/mod.rs` around lines 2543 - 2569, Normalize
whitespace-only assignee_did consistently in both create response paths that
call Db::create_task, covering task_to_json and AgentTaskType::from conversions.
Ensure the returned task uses None/null when assignee_slot_blank detects a blank
value, matching the persisted database row and subsequent reads.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 2662-2680: Update the UPDATE statement in the task-claim flow to
use the already-read exact reserved value parameter ($4) directly in the SET
expression, preserving the stored assignee DID including whitespace; keep the
existing blank-assignee fallback behavior for unreserved claims. Extend
claim_task_keeps_stored_assignee_did_form with a whitespace-padded reserved DID
to verify the persisted value remains unchanged.

---

Outside diff comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 2543-2569: Normalize whitespace-only assignee_did consistently in
both create response paths that call Db::create_task, covering task_to_json and
AgentTaskType::from conversions. Ensure the returned task uses None/null when
assignee_slot_blank detects a blank value, matching the persisted database row
and subsequent reads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b2c0922b-9cd4-4545-9099-ceea98e047d8

📥 Commits

Reviewing files that changed from the base of the PR and between 111cff7 and 139b8da.

📒 Files selected for processing (4)
  • crates/gitlawb-node/src/api/tasks.rs
  • crates/gitlawb-node/src/db/mod.rs
  • crates/gitlawb-node/src/graphql/mutation.rs
  • crates/gitlawb-node/src/test_support.rs

Comment thread crates/gitlawb-node/src/db/mod.rs
@Ayush7614

Copy link
Copy Markdown
Contributor Author

@jatmn @beardthelion addressed the latest review on head 139b8da:

  1. Whitespace Rust vs SQL — blank assignee slots now use the same definition in Rust (trim_matches on space/tab/LF/CR) and SQL (BTRIM(..., E' \t\n\r')). Whitespace-only values are normalized to NULL on create_task. finish_task / GraphQL complete+fail trim the same way so tab-padded stored DIDs can finish after a successful claim.
  2. TaskEventBroadcast.by_did — claim/complete/fail (REST + GraphQL) now emit the persisted task.assignee_did (fallback to signer only if missing).
  3. GraphQL steal parity — after a failed stranger claimTask, assert via get_task that status stays pending and assignee_did is unchanged.

CI (P2): fork PR Checks still need a maintainer to approve workflows on the latest head — please approve when you can so the suite can run against this commit.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Run and satisfy the required checks on this head
    .github/workflows/pr-checks.yml / current head 139b8da
    The only check runs attached to this head are the two triage jobs and CodeRabbit; the Rust test, fmt/clippy, release-build, audit, MSRV, and Docker checks have not run. The fork workflow needs maintainer approval/triggering before merge. In addition, cargo fmt --all -- --check currently fails on this submitted head (including the new task changes), so the format job will fail once it runs. Please format the patch and get the full required workflow green on this head.

  • [P2] Preserve the exact nonblank reservation when claiming
    crates/gitlawb-node/src/db/mod.rs:2664
    The authorization and race predicate bind $4 to the exact stored assignee_did, but the update stores BTRIM(assignee_did, ...) instead. A reservation such as " did:key:z...\t" is accepted for its matching signer and then silently rewritten without the surrounding whitespace. That contradicts the new stored-form preservation contract and makes exact list_tasks(..., assignee_did=<original>) filters lose the task after claim. Use the exact reserved value for the reserved branch, and apply the blank normalization only when filling an open slot; add a padded-reservation regression test.

  • [P2] Return the normalized assignee value from task creation
    crates/gitlawb-node/src/db/mod.rs:2543, crates/gitlawb-node/src/api/tasks.rs:126, crates/gitlawb-node/src/graphql/mutation.rs:58
    create_task now persists an all-whitespace assignee_did as SQL NULL, but both REST and GraphQL serialize the original in-memory AgentTask. Consequently, a create response for assignee_did: " \t" says the task is reserved, while an immediate GET/list reports it as open and another agent can claim it. Normalize the returned task (or return the persisted row) in both paths and cover both response contracts.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The claim gate still holds on 139b8dac. Reverting the reserved did_matches deny still reds both steal tests. jatmn's two code points are true on this head: the claim SET re-derives through BTRIM instead of keeping the reserved form, and create returns the input assignee after the DB has nulled a blank. cargo fmt --all -- --check fails too, and every hunk is in files this PR touches.

One scope note carried from last round: the anonymous read of ucan_token through GET /api/v1/tasks/{id} and the GraphQL task query is still open (#268). The new "UCAN must not leak on a failed steal" assertions only inspect the 403 envelope, which never carried task fields, so they pass while the token stays readable without claiming. Keep the claim-path assertions; the leak framing still overclaims.

Findings

  • [P2] Keep the exact nonblank reservation on claim, and trim REST finish if you do
    crates/gitlawb-node/src/db/mod.rs:2664
    Same ask as jatmn and CodeRabbit. The SET is still COALESCE(NULLIF(BTRIM(assignee_did, E' \t\n\r'), ''), $2), so a padded reservation is rewritten on claim and an exact list filter loses it. The docstring at :2630 still says the reserved form is kept via COALESCE. Do not land COALESCE($4, $2) alone: REST complete/fail still compare untrimmed while GraphQL trims, and that couple creates a cross-surface 403 for the real assignee. Related: reserve bare, claim as did:key:<same>, then list by the claimer's full DID returns 0 rows while the stored bare form returns 1, because list_tasks (:2582) is plain equality while claim went through did_matches. Either keep BTRIM and fix the docstring, or keep the stored form and trim REST on the same ASCII blank set. Add a padded-reservation regression, and a bare-then-full list regression, either way.

  • [P2] Return the normalized assignee from create
    crates/gitlawb-node/src/api/tasks.rs:120
    Same as jatmn. Db::create_task nulls a whitespace-only assignee; REST still returns task_to_json(&task) and GraphQL create_task (mutation.rs:55) still returns AgentTaskType::from(task), so create can show "" while the row is NULL/open. Normalize or re-read before serializing; cover both surfaces.

  • [P2] Pin the named guards with fail-on-remove proofs
    crates/gitlawb-node/src/db/mod.rs:2633
    The claim UPDATE assignee re-check, create_task's blank filter, and finish_task's AND assignee_did=$5 can each be removed with the task suite staying green (the finish case because the Rust gate returns first). Related: claim_task_blank_reservation_is_open seeds Some("") through create_task, which now stores NULL, so it never reaches either blank branch it is named for. Seed the row directly, and add proofs that fail when each of those three guards is removed.

  • [P2] Format the touched files
    crates/gitlawb-node/src/api/tasks.rs
    cargo fmt --all -- --check exits 1 on this head. The six diffs are in api/tasks.rs, db/mod.rs, and graphql/mutation.rs, so this is yours to clear before the next push.

  • [P3] Surface the reserved deny distinctly in GraphQL claimTask
    crates/gitlawb-node/src/graphql/mutation.rs:77
    REST now returns 403 for a reserved steal and 409 for a lost race. claimTask still maps every db error through e.to_string() with no downcast, so a GraphQL client cannot tell those apart without parsing the message. That is also why the new test has to assert on contains("reserved") || contains("not claimable").

One process note, not a finding: PR Checks is action_required with zero jobs on this fork head. That is mine to approve, not yours to clear. The fmt finding above is the part you can fix on your own.

@Ayush7614

Copy link
Copy Markdown
Contributor Author

@beardthelion @jatmn addressed review 4831516189 on head 62172cf:

  1. Keep exact nonblank reservation — claim SET is now COALESCE($4, $2) (no BTRIM rewrite). Padded reservation regression + bare→full claim with list-by-bare / list-by-full coverage.
  2. Trim REST finish — complete/fail use trim_assignee_did (same ASCII blank set as claim/SQL). Padded claim can finish.
  3. Normalize create responsescreate_task returns the persisted shape (whitespace-only → None); REST + GraphQL serialize that.
  4. Fail-on-remove proofs — blank/tab blank seeded via raw INSERT (claim SQL open path); create_task_normalizes_whitespace_assignee; finish_task_sql_assignee_predicate_is_load_bearing for AND assignee_did=$5.
  5. cargo fmt — touched files formatted.
  6. GraphQL reserved denyclaimTask downcasts TaskReservedForOtherAssignee to a distinct "reserved for another assignee" message (test asserts contains("reserved")).

UCAN anonymous-read framing note acknowledged — still #268; claim-path assertions kept.

CI: please approve fork PR Checks on this head when you can.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The claim gate still holds on 62172cf. Gutting the reserved did_matches deny reds both steal tests again (REST stranger lands 200). The r2 behavioral asks landed: claim SET is COALESCE($4, $2), padded reservations survive and can finish after trim_assignee_did, create returns the normalized row (REST create with " \t\n " returns assignee_did: null), GraphQL surfaces the reserved deny distinctly, and cargo fmt --check is clean. create_task_normalizes_whitespace_assignee goes RED when the blank filter is removed, and the padded test goes RED when SET is reverted to BTRIM.

Two named guards from the r2 pin-guards ask are still unpinned. Replacing the claim UPDATE assignee slot re-check with AND (true) leaves all ten filtered task tests green. Removing AND assignee_did=$5 from Db::finish_task leaves both finish tests green: the Rust gate returns first, and finish_task_sql_assignee_predicate_is_load_bearing runs a parallel UPDATE that never calls production.

One scope note carried again: anonymous GET /api/v1/tasks/{id} and the GraphQL task query still serve ucan_token (#268). Keep the claim-path assertions; the deny-envelope "must not leak" framing still overclaims.

Findings

  • [P2] Pin the claim UPDATE assignee re-check with a fail-on-remove proof
    crates/gitlawb-node/src/db/mod.rs:2673
    The docstring still calls the AND (($4 IS NULL AND open) OR assignee_did = $4) clause defense-in-depth against a writer racing the pre-check. I deleted that clause (left AND (true)) and the claim/finish/create task filter suite stayed 10/10 green, including both steal tests (the Rust reserved deny fires first). Add a proof that fails when that production predicate is removed.

  • [P2] Pin Db::finish_task's AND assignee_did=$5, not a parallel query
    crates/gitlawb-node/src/test_support.rs:807
    finish_task_sql_assignee_predicate_is_load_bearing runs its own UPDATE ... AND assignee_did=$4 with a mismatched bind and asserts zero rows. That stays green after I remove AND assignee_did=$5 from db/mod.rs:2720, because the test never calls Db::finish_task and the stranger path is stopped by the Rust did_matches gate at :2713. Replace it with a proof that fails when the production clause is gone.

Not an ask, recorded only: collision band is EXPECT-REBASE (mechanical overlap on db/mod.rs); expect a rebase, not a redone review of the claim gate.

@beardthelion
beardthelion dismissed stale reviews from themself July 31, 2026 21:02

Superseded by re-review on 62172cf

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Pin the claim UPDATE assignee re-check with a fail-on-remove proof
    crates/gitlawb-node/src/db/mod.rs:2673
    The re-check is still documented as defense-in-depth against a writer changing the reservation after the pre-check, but no current test reaches that production predicate. Replacing the whole condition with AND true leaves the task filter suite green because the existing steal tests stop at the earlier Rust denial. Add a production-path race (or focused equivalent) that changes the pending row's assignee slot after the pre-read and asserts that this claim cannot update it.

  • [P2] Make the claimed fail-on-remove test exercise finish_task
    crates/gitlawb-node/src/test_support.rs:807
    This test still executes a second hand-written UPDATE rather than Db::finish_task, so it remains green if AND assignee_did=$5 is removed from the production update. The other new DB test also stops at the Rust did_matches check, leaving the check-then-act protection untested. Please test the production path with an assignee change between its read and update (for example through a controlled concurrent update/trigger) and assert that it leaves the task claimed; otherwise the new race guard can be deleted without a regression signal.

beardthelion added a commit that referenced this pull request Aug 4, 2026
The needs-tests detector matched an added `#[test]` or `#[cfg(test)]` line
and nothing else, so it missed every test written with a harness attribute.
That is most of this repo: 345 `#[tokio::test]` and 189 `#[sqlx::test]`
against 540 bare `#[test]`. Any PR whose tests are all DB or async tests got
labeled needs-tests, which is what happened on #275 (three added
`#[sqlx::test]` functions) and #262 (eight).

Match an added attribute whose last path component contains `test`, followed
by `]` or `(`. That covers the four forms in the tree today and the harnesses
a contributor might reach for later (`#[rstest]`, `#[test_case]`,
`#[wasm_bindgen_test]`) without enumerating them.

Deliberately not keying on an added `assert!`: a production assertion is
ordinary Rust, so that signal would let a testless PR clear the label
silently. The label is a nudge, so it should fail loud. Labeling a PR that
did add tests gets corrected by the author; clearing one that did not is
invisible.

Anchoring to the start of the added line also drops three false positives the
old `.*` prefix accepted: a comment, a string literal, and a doc example that
merely mention `#[test]`.

Verified against the real patches of 14 open PRs: #275 and #262 stop being
labeled, both of which do add tests, and no other verdict changes.
@github-actions github-actions Bot removed the needs-tests Source changed without accompanying tests (advisory) label Aug 10, 2026
@Ayush7614
Ayush7614 force-pushed the fix/claim-task-assignee-integrity branch 3 times, most recently from 664a12c to 3c126b7 Compare August 10, 2026 12:11
@Ayush7614

Copy link
Copy Markdown
Contributor Author

@beardthelion @jatmn — rebased onto current main and squashed to a single commit (3c126b7). All 13 PR Checks jobs are green on this head.

R3 pin-guards (your latest round on 62172cf)

  1. Claim UPDATE assignee re-check — replaced the hand-written parallel UPDATE with claim_task_update_assignee_slot_recheck_is_load_bearing: a second connection holds FOR UPDATE on the pending row, changes assignee_did after the pre-read, then the production Db::claim_task must return TaskNotClaimable (0 rows updated). Removing the AND (($4 IS NULL AND open) OR assignee_did = $4) predicate should make this go red.

  2. finish_task AND assignee_did=$5 — replaced finish_task_sql_assignee_predicate_is_load_bearing (which never called production) with the same FOR UPDATE race pattern against Db::finish_task: hog swaps the stored assignee between read and update; production finish must leave the task claimed. Removing AND assignee_did=$5 should make this go red.

Prior rounds (still on this head)

  • Claim SET is COALESCE($4, $2) (exact reserved form preserved); padded-reservation + bare/full list regressions retained.
  • create_task returns the normalized persisted row on both REST and GraphQL.
  • GraphQL claimTask surfaces the reserved deny distinctly (not a generic db string).
  • cargo fmt --check clean.

Scope note (unchanged): anonymous GET /api/v1/tasks/{id} / GraphQL task still serve ucan_token (#268); claim-path deny-envelope assertions kept as-is.

Ready for another look.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Rebase onto current main and resolve the conflict
    crates/gitlawb-node/src/db/mod.rs:7035
    GitHub currently reports this head as CONFLICTING / DIRTY. The three-way merge conflicts in the peer-writer guard ledger, so this cannot merge as submitted. Please rebase, resolve the conflict while preserving the current-main security changes, and rerun the checks; the resolved head needs a fresh review.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both round-3 asks are closed, and I checked by execution rather than by reading the replies. Deleting the claim UPDATE's assignee-slot predicate now reds claim_task_update_assignee_slot_recheck_is_load_bearing; neutering finish_task's AND assignee_did=$5 reds finish_task_sql_assignee_predicate_is_load_bearing. Last round both of those deletions left the suite green. I re-ran the full named-guard set on this head, six mutations, each red on its own assertion message: the reserved deny on REST and GraphQL, those two predicates, create_task's blank-to-NULL filter, and the COALESCE($4, $2) exact-form SET. Baseline is 21 passed.

The blocker is the conflict jatmn already flagged, and it resolves more easily than it looks. Both peer test functions already existed at your base while the ledger there omitted them, so your addition repaired a guard that was failing at that base; main fixed the same thing in af821e09 with slightly different wording. It is a duplicate add, so take main's version of the doc table and the LEDGER rows and keep the rest of your diff intact. None of your task changes conflict. #247 landed on main today and touched only the error and events modules, so the rebase has no gate to lose on your surfaces, and your GraphQL path already picked up graphql_db_err correctly.

Three small things to fold into that push.

Findings

  • [P3] Trim the assignee before putting it in the task event
    crates/gitlawb-node/src/api/tasks.rs:188
    I drove the REST claim handler with a reservation stored as " zKEY\t" and read the broadcast: by_did came out as " zKEY\t" while the authenticated caller was did:key:zKEY. A field named by_did should not carry padding or a bare key. trim_assignee_did is already in scope. Same one-liner at the other five sites.

  • [P3] Make the claim race assertion provable only through the SQL re-check
    crates/gitlawb-node/src/test_support.rs:1159
    The test asserts contains("not claimable") and both denials say exactly that: the Rust pre-check is "task not claimable: reserved for another assignee", the SQL miss is "task not claimable: not found or already claimed". If the background writer commits before the pre-read, the test passes through the pre-check and proves nothing about the predicate it is named for. It does exercise that predicate today, since removing it goes red, but the assertion should be what guarantees it. Assert the specific message, or that the error is not a TaskReservedForOtherAssignee. The finish test does not have this problem: "not in claimed state" cannot match its Rust gate's message.

  • [P3] Cover the other blank characters
    crates/gitlawb-node/src/test_support.rs:830
    ASSIGNEE_BLANK and the SQL BTRIM(assignee_did, E' \t\n\r') are the same four characters today, and a comment says they must stay that way. The SQL blank branch is only ever seeded with '' and a tab, so space, LF and CR never reach it. Extending the tab test to the remaining three is the cheapest guard against those two definitions drifting.

Nothing else holds this up. Once the rebase is clean I will re-run the guard set on the resolved head.

@beardthelion
beardthelion dismissed their stale review August 10, 2026 18:33

Both asks in this review are satisfied on 3c126b7 and I re-ran them: deleting the claim UPDATE assignee-slot predicate and neutering finish_task AND assignee_did=$5 each go red now. Superseded by my review on the current head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/gitlawb-node/src/db/mod.rs (1)

3809-3853: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Make normalized profile selection deterministic.

agent_profiles uses raw did as its primary key, so z6Mkfoo and did:key:z6Mkfoo can coexist. Both match PROFILE_DID_CASE_SQL.

Use one deterministic row selector in get_profile and set_profile_cid. The current lookup can return either row, while the current update changes every matching row.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/db/mod.rs` around lines 3809 - 3853, The
PROFILE_DID_CASE_SQL predicates in get_profile and set_profile_cid must select
the same deterministic profile when both bare and did:key forms coexist. Add an
ordering that prefers the canonical did:key representation, limit get_profile to
one row, and constrain set_profile_cid to update only that selected row rather
than every match.
🧹 Nitpick comments (1)
crates/gitlawb-node/src/db/mod.rs (1)

2267-2275: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Do not swallow the existence-probe error.

unwrap_or(false) converts a transient database failure into "the row does not exist". The strict DID validation arm then runs and refuses a legacy row that is present, so an operator sees methodNotSupported or cannot resolve DID instead of the real database fault. Propagate the error with ? so the caller reports the true cause.

♻️ Proposed change
             PeerWriteAuthority::Unproven
                 if !sqlx::query_scalar::<_, bool>(
                     "SELECT EXISTS(SELECT 1 FROM peers WHERE did = $1)",
                 )
                 .bind(did)
                 .fetch_one(&self.pool)
-                .await
-                .unwrap_or(false) =>
+                .await? =>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/db/mod.rs` around lines 2267 - 2275, Update the
PeerWriteAuthority::Unproven existence probe to propagate fetch_one errors with
?, removing unwrap_or(false), so database failures reach the caller instead of
being treated as a missing peer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.cursor/rules/rtk-token-savings.mdc:
- Line 1: The .cursor/rules/rtk-token-savings.mdc symlink uses a
developer-specific absolute target and must be portable. Replace it with the
tracked rule content or a repository-relative symlink to the corresponding
tracked file, ensuring other checkouts can load the rule.

---

Outside diff comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 3809-3853: The PROFILE_DID_CASE_SQL predicates in get_profile and
set_profile_cid must select the same deterministic profile when both bare and
did:key forms coexist. Add an ordering that prefers the canonical did:key
representation, limit get_profile to one row, and constrain set_profile_cid to
update only that selected row rather than every match.

---

Nitpick comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 2267-2275: Update the PeerWriteAuthority::Unproven existence probe
to propagate fetch_one errors with ?, removing unwrap_or(false), so database
failures reach the caller instead of being treated as a missing peer.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eb482e20-c340-4892-acd5-3b42794815e9

📥 Commits

Reviewing files that changed from the base of the PR and between 62172cf and e5a8468.

📒 Files selected for processing (5)
  • .cursor/rules/rtk-token-savings.mdc
  • crates/gitlawb-node/src/api/tasks.rs
  • crates/gitlawb-node/src/db/mod.rs
  • crates/gitlawb-node/src/graphql/mutation.rs
  • crates/gitlawb-node/src/test_support.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/gitlawb-node/src/api/tasks.rs
  • crates/gitlawb-node/src/graphql/mutation.rs
  • crates/gitlawb-node/src/test_support.rs

Comment thread .cursor/rules/rtk-token-savings.mdc Outdated
@@ -0,0 +1 @@
/Users/ayushkumar/.cursor/rules/rtk-token-savings.mdc No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace the developer-specific absolute symlink target.

Line 1 points to /Users/ayushkumar/.cursor/rules/rtk-token-savings.mdc. Other checkouts will have a dangling link, so Cursor cannot load this rule. Commit the rule in the repository or use a repository-relative symlink to a tracked file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/rules/rtk-token-savings.mdc at line 1, The
.cursor/rules/rtk-token-savings.mdc symlink uses a developer-specific absolute
target and must be portable. Replace it with the tracked rule content or a
repository-relative symlink to the corresponding tracked file, ensuring other
checkouts can load the rule.

Stop strangers from overwriting a reserved assignee_did (and receiving
the UCAN). Bind finish_task to the stored assignee; return 403 on
reserved steals; keep exact stored DID forms; normalize blank assignees;
pin production claim/finish SQL race guards with FOR UPDATE tests; trim
broadcast by_did; and tighten blank/SQL re-check regressions.
@Ayush7614
Ayush7614 force-pushed the fix/claim-task-assignee-integrity branch from e5a8468 to 1676eb0 Compare August 10, 2026 20:19
@Ayush7614

Copy link
Copy Markdown
Contributor Author

@beardthelion @jatmn — rebased onto current main (241b366, includes merged #247) and pushed 1676eb0.

Conflict: took main's peer-writer LEDGER wording (duplicate of our prior add from #248 fixtures).

Your latest P3 round:

  1. by_did trim — claim/complete/fail broadcasts now use trim_assignee_did on the persisted assignee (REST + GraphQL, all six sites).
  2. Claim race assertionclaim_task_update_assignee_slot_recheck_is_load_bearing now requires "not found or already claimed" and asserts the error is not TaskReservedForOtherAssignee (proves the SQL UPDATE predicate, not the Rust pre-check).
  3. ASCII blank coverage — merged blank/tab tests into claim_task_ascii_blank_reservations_are_open covering ASSIGNEE_BLANK ("", space, tab, LF, CR).

Prior pin-guards and behavioral fixes remain on this head. Ready for re-review when CI is green.

Windows CI can race: git fetch connects before the shim accept loop is
polling, yielding connection aborted (os error 10053) with 0 POSTs.
Probe the listener after spawn so multi-round and withheld-path tests
start only once the shim is ready.
@Ayush7614

Copy link
Copy Markdown
Contributor Author

Pushed 1e6b065 to fix the red test (windows, non-blocking) job.

The failure was a race on Windows CI: real_git_multi_round_fetch_completes and real_git_withheld_shaped_first_post connected before the in-test HTTP shim entered its accept loop (connection aborted, os error 10053, 0 POSTs). Added a readiness probe after start_shim spawns.

All other required checks were already green on 1676eb0; re-running the full suite on this head.

…rors

Windows CI can still abort localhost connections (os error 10053) after the
shim is ready. Retry fetch_with_helper up to three times on transient
connection failures before asserting.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Do not retry the stateful withheld-first-POST scenario against the same shim
    crates/git-remote-gitlawb/tests/real_git_fetch.rs:264
    fetch_with_helper now retries every matching connection error, but real_git_withheld_shaped_first_post reuses one ShimMode::WithheldFirstPost instance. If the first fetch reaches its first POST and then fails with one of those transient diagnostics, that POST consumes the shim's one special NAK+full-pack response. The retry is served ordinary upload-pack; if it succeeds, the test takes its success branch and falsely certifies the withheld-first-POST behavior without a successful fetch ever exercising that behavior. Keep retries out of this stateful case, or recreate/reset the shim and fixture for each attempt.

  • [P2] Synchronize with the accept-loop thread instead of the listener backlog
    crates/git-remote-gitlawb/tests/real_git_fetch.rs:131
    TcpListener::bind has already put the socket in the listening state before the accept-loop thread is spawned, so TcpStream::connect_timeout succeeds once the OS queues the TCP handshake; it does not require the spawned thread to have reached accept(). wait_for_shim_ready can therefore return while the scheduling race described in its comment remains, leaving the Windows flake unsynchronized. Have the spawned thread send an explicit readiness acknowledgement (or complete a protocol-level probe) before start_shim returns.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The task work is done, and I checked it by execution on this head rather than by reading the replies. All three of last round's asks landed: trim_assignee_did is applied at the by_did sites, the race test now asserts the error is not a TaskReservedForOtherAssignee so the SQL predicate is what denied it, and the blank test seeds empty, space, tab, LF and CR. I re-ran the guard set on 0484740, one gut at a time. Removing the reserved deny in claim_task reds both steal tests; deleting the claim UPDATE's assignee-slot predicate reds the race guard; neutering finish_task's AND assignee_did = $5 reds its twin; dropping create_task's blank-to-NULL filter reds the normalization test; rewriting the COALESCE($4, $2) SET to a BTRIM reds the padded-reservation test. Baseline is 22 passed. The rebase came out the way I asked and the duplicate peer-writer ledger rows are gone.

What holds this up is the Windows CI work, and the answer is to take it out rather than patch it. That covers both of jatmn's asks on this head. Separately, the open CodeRabbit thread on .cursor/rules/rtk-token-savings.mdc points at a file that is no longer in the diff, so you can resolve it.

Findings

  • [P2] Revert real_git_fetch.rs to main and let #312 fix the Windows lane
    crates/git-remote-gitlawb/tests/real_git_fetch.rs:262
    The retry keys on 10053, and that is the signature of a real defect rather than a flake: on Windows the accepted socket inherits the listener's non-blocking state, so read_line returns WouldBlock, the shim drops the connection without writing a response, and the request never reaches the branch that counts POSTs. #312 fixes that at the source and is approved and mergeable, so this retries a live bug into silence. It also lets the withheld test certify a shape it never sent. posts is shim-global and the withheld response fires once, so I set that arm to never fire, which is exactly the state a retry leaves behind, and the test passed through its success branch with no note printed. wait_for_shim_ready does not close the gap either, since a listener that is bound completes a connect with accept() never called. The same three hunks are byte-identical in #276. Your base blob for this file matches main, so the revert is clean, and the Windows job is non-blocking, so nothing here goes red.

The retry-on-10053 and wait-for-shim probes papered over the accepted-socket
inheriting the listener's non-blocking state. Gitlawb#312 fixed that at the source
(normalize_accepted_stream) and is merged, so take the workarounds back out
and let the real fix carry the Windows lane.
@Ayush7614

Ayush7614 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Re: the last review round — the Windows CI workaround is reverted on this head (6813340).

Both workaround commits (1e6b065 wait-for-shim, 0484740 retry-on-10053) are taken back out of crates/git-remote-gitlawb/tests/real_git_fetch.rs, restoring the file to the base blob that matches main. #312 fixed the root cause (normalize_accepted_stream) at the source and is merged, so the Windows lane is now carried by the real fix rather than retried into silence.

Verified on this head:

  • cargo test -p git-remote-gitlawb --test real_git_fetch — 6 passed, 4 fixture-ignored
  • cargo fmt --all -- --check — clean
  • cargo clippy --all-targets -- -D warnings (both crates) — clean
  • Task/claim suite against Postgres — 22 passed, incl. the guarded proofs (claim_task_update_assignee_slot_recheck_is_load_bearing, finish_task_sql_assignee_predicate_is_load_bearing, padded-reservation, ascii-blank)

The remaining gitlawb-node failures in a full-suite run are pre-existing timing/deadline flakes (run_bounded_git, store, visibility_pack) — confirmed identical at the base commit.

@beardthelion @jatmn — the round-4 asks are closed; requesting re-review on the new head. CodeRabbit thread is on a file no longer in the diff (GitHub rejects API-resolving it), but it is non-blocking as you noted.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Last round's asks landed, and I checked them by execution on this head rather than by reading the replies. The retry is gone (grep -c retry returns 0), the readiness probe is gone, and the withheld test's outcome guards are intact. All 12 checks are green, and I confirmed the Windows result at the step level rather than the job level, since that lane runs continue-on-error: true and would report SUCCESS over a failed step. Step 5 genuinely passed.

Two asks before this merges.

Findings

  • [P2] Rebase onto current main so the revert's premise holds at this head
    crates/git-remote-gitlawb/tests/real_git_fetch.rs
    The commit message credits #312 with carrying the Windows lane, but this head does not contain #312. The file here is blob 26d88f27, byte-identical to the pre-#312 base: normalize_accepted_stream appears 0 times against 10 on main, and the stream.set_nonblocking(false) that main carries at line 181 is absent. git merge-base --is-ancestor 10e0840d 68133409 returns NO, and the base is 18 commits behind. The shipped state is fine, since the three-way merge resolves this file to 72fdd5c2, main's exact blob. The head is what worries me: its Windows run passed on the unmitigated shape, so the green tells us the race did not fire on that run, not that the fix is present. Rebase or cherry-pick #312 and let the lane run against the code it is credited to.

  • [P3] Pin the retry's absence with a committed guard
    crates/git-remote-gitlawb/tests/real_git_fetch.rs:249
    Nothing goes red if a fetch-level retry comes back. The shim counts POSTs only, incrementing at line 209 on the POST branch, and both assertions are lower bounds: posts >= 2 at 778 and posts >= 1 at 892. A second full fetch satisfies both while masking the withheld shape the test exists to observe. A GET counter asserted to be exactly one per test would make a reintroduced retry fail on the spot.

One housekeeping item, not an ask: the CodeRabbit thread on .cursor/rules/rtk-token-savings.mdc is still open, but that file is absent at this head and never appeared in the PR's net diff, so the work behind it is done and the thread can be resolved.

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

Labels

crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants