Skip to content

fix(tasks): add ownership guard to close_task - #2196

Open
hognek wants to merge 2 commits into
jaylfc:devfrom
hognek:fix/2191-close-ownership-guard
Open

fix(tasks): add ownership guard to close_task#2196
hognek wants to merge 2 commits into
jaylfc:devfrom
hognek:fix/2191-close-ownership-guard

Conversation

@hognek

@hognek hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #2191.

Add claim-holder ownership guard to close_task in the store, mirroring the
release guard pattern: the SQL now requires claimed_by IS NULL OR claimed_by = ?.
The route handler passes force=True when the caller is the project lead/curator,
allowing leads to close cards claimed by others.

Changes

  • task_store.py: close_task gains a force kwarg. Non-force path
    enforces claimed_by IS NULL OR claimed_by = closed_by.
  • routes/projects.py: Close handler resolves lead bypass:
    force = project.get("lead_member_id") == actor_id.
  • test_task_store.py: 4 new tests:
    • test_close_by_claimer_passes — claimer closes own claimed card
    • test_close_by_stranger_rejected — non-claimer blocked (returns False)
    • test_close_by_lead_passes — lead force-closes another's claimed card
    • test_close_unclaimed_unchanged — unclaimed cards still closeable by anyone

Tests: 29/29 task_store, 68/68 projects-integration pass.


Summary by Gitar

  • Cross-User Collab D1:
    • Added delegation_handler.py for agent delegation requests, scope validation, and sponsor revocation
    • Added sponsor_contact_id column to agent_registry and project_invites with migration support
    • Implemented 3-level kill-switch machinery and peer route safeguards in peer.py and decisions.py

This will update automatically on new commits.

Summary by CodeRabbit

  • New Features

    • Project owners, leads, and administrators can force-close tasks claimed by others.
    • Task closing now clearly reports when a task is claimed by someone else.
  • Bug Fixes

    • Improved handling and visibility of failed task closures during synchronization.
    • Closing nonexistent tasks returns the appropriate not-found response.
  • Tests

    • Added coverage for task ownership, authorization, forced closures, unclaimed tasks, and invalid task IDs.

@hognek
hognek marked this pull request as ready for review July 28, 2026 16:53
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Task close authorization

Layer / File(s) Summary
Store close guard
tinyagentos/projects/task_store.py, tests/projects/test_task_store.py
close_task retains claimer checks by default, adds keyword-only forced closure, and tests claimed, unclaimed, authorized, and rejected cases.
Route authorization and responses
tinyagentos/routes/projects.py, tests/test_board_audit_wiring.py
The route enables force-closing for admins, owners, and leads, returns ownership-specific conflicts, and tests admin closure and nonexistent tasks.
Caller refusal reporting
tinyagentos/github_sync.py, tinyagentos/projects/beads_bridge.py
Integration callers now log failed or refused close operations.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CloseRoute
  participant ProjectTaskStore
  participant TaskDatabase
  Caller->>CloseRoute: Request task close
  CloseRoute->>ProjectTaskStore: close_task(..., force)
  ProjectTaskStore->>TaskDatabase: Update eligible task
  TaskDatabase-->>ProjectTaskStore: Success or refusal
  ProjectTaskStore-->>CloseRoute: Boolean result
  CloseRoute-->>Caller: HTTP response
Loading

Possibly related PRs

Suggested reviewers: jaylfc

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [#2191] The guard and tests are present, but unclaimed tasks can still be closed, which conflicts with the issue's no-build-lane-closure requirement. Update close_task and the route so unclaimed tasks are not closable by build lanes, then add a regression test for that rejection.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: adding ownership checks to close_task.
Out of Scope Changes check ✅ Passed The added logging and route/store/test updates all support the close-task authorization fix and caller-audit requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add sponsored agent delegation and task ownership protection

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add policy-gated cross-user agent delegation through peer envelopes, decisions, and sponsored
 invites.
• Persist sponsorship for redemption, revocation cascades, and collaboration kill switches.
• Enforce claimed-task closure ownership, with lead override and regression coverage.
Diagram

sequenceDiagram
    participant Peer as Peer Inbox
    participant Handler as Delegation Handler
    participant Project as Project Store
    participant Decisions as Decisions
    participant Invites as Invite Store
    participant Redeem as Invite Redemption
    participant Registry as Agent Registry
    participant Tasks as Task Store
    Peer->>Handler: Delegation request
    Handler->>Project: Verify member and policy
    alt Manual approval
        Handler->>Decisions: Create approval card
        Decisions->>Handler: Complete approval
    end
    Handler->>Invites: Mint sponsored invite
    Redeem->>Invites: Redeem invite
    Redeem->>Registry: Register sponsor
    Handler->>Registry: Revoke sponsored agents
    Handler->>Tasks: Unassign revoked work
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persistent delegation state machine
  • ➕ Provides explicit request states, idempotent retries, and centralized auditing.
  • ➕ Improves recovery when decision, invite, or notification operations partially fail.
  • ➖ Requires another schema, API surface, and lifecycle implementation.
  • ➖ Adds substantial complexity beyond the initial delegation milestone.
2. Project-scoped sponsorship relation
  • ➕ Supports different sponsors for one reused identity across multiple projects.
  • ➕ Enables precise project-level revocation without overwriting registry sponsorship.
  • ➖ Requires a normalized join table and additional queries.
  • ➖ Makes identity reuse and contact-wide revocation more complex.

Recommendation: Reusing the existing Decisions and invite pipelines is pragmatic for the initial delegation milestone. However, consider a project-scoped sponsorship relation before supporting multi-sponsor identity reuse, because the single registry column can be overwritten when an existing agent is sponsored into another project.

Files changed (12) +1233 / -31

Enhancement (8) +862 / -24
agent_registry_store.pyPersist sponsored-agent ownership in the registry +89/-15

Persist sponsored-agent ownership in the registry

• Adds the sponsor_contact_id schema migration and registration support. New lookup and mutation methods support sponsor-based revocation cascades.

tinyagentos/agent_registry_store.py

delegation_handler.pyImplement the cross-user delegation workflow +589/-0

Implement the cross-user delegation workflow

• Introduces validation, scope restrictions, membership and policy gates, manual or automatic approval, and sponsored invite creation. It also implements sponsor revocation cascades, task unassignment, and contact- or instance-level kill switches.

tinyagentos/delegation_handler.py

invite_store.pyStore structured metadata on project invites +22/-5

Store structured metadata on project invites

• Adds an idempotent metadata column migration and accepts metadata during invite minting. Invite reads now deserialize both scopes and metadata JSON fields.

tinyagentos/projects/invite_store.py

project_store.pyAdd membership and project-setting helpers +58/-0

Add membership and project-setting helpers

• Adds optional-kind membership checks plus JSON-backed project setting getters and setters. These helpers drive delegation eligibility and automatic approval policy.

tinyagentos/projects/project_store.py

agent_auth_requests.pyPropagate sponsorship during agent approval +10/-0

Propagate sponsorship during agent approval

• Accepts sponsor_contact_id when approving an agent request. The value is persisted for new identities and applied when reusing an existing identity.

tinyagentos/routes/agent_auth_requests.py

decisions.pyComplete approved collaboration delegations +58/-3

Complete approved collaboration delegations

• Routes collaboration delegation decisions through the new handler. Approval mints a sponsored invite, while denial and failures are reported back through the existing answer channel.

tinyagentos/routes/decisions.py

peer.pyDispatch delegation envelopes and enforce panic mode +16/-1

Dispatch delegation envelopes and enforce panic mode

• Dispatches delegation_request envelopes to the delegation handler after peer verification. Peer authentication now rejects all traffic while the instance-level panic switch is active.

tinyagentos/routes/peer.py

project_invites.pyCarry invite sponsorship into agent registration +20/-0

Carry invite sponsorship into agent registration

• Extracts sponsor_contact_id from invite metadata during project and OS-level redemption. The sponsor is forwarded into the shared agent approval flow.

tinyagentos/routes/project_invites.py

Bug fix (2) +21 / -7
task_store.pyGuard closure of claimed tasks +17/-6

Guard closure of claimed tasks

• Adds a force option to close_task. Normal closure now atomically requires an unclaimed task or a matching claim holder, while forced closure retains the prior behavior.

tinyagentos/projects/task_store.py

projects.pyAllow only owners or leads to close claimed tasks +4/-1

Allow only owners or leads to close claimed tasks

• Passes force closure only when the authenticated actor is the project lead. Ownership conflicts now return a specific HTTP 409 response.

tinyagentos/routes/projects.py

Tests (2) +350 / -0
test_task_store.pyCover claimed-task closure ownership rules +50/-0

Cover claimed-task closure ownership rules

• Adds tests proving claim holders and force-authorized leads can close claimed tasks, while unrelated actors cannot. It also verifies unclaimed tasks remain closeable.

tests/projects/test_task_store.py

test_collab_d1_delegation.pyTest delegation validation and sponsorship persistence +300/-0

Test delegation validation and sponsorship persistence

• Adds coverage for delegation scope filtering, envelope validation, sponsor registry operations, the registry migration, and invite metadata deserialization.

tests/test_collab_d1_delegation.py

@qodo-code-review

qodo-code-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Approval replies miss contacts 🐞 Bug ≡ Correctness
Description
Collaboration decisions store the peer contact_id as from_agent, then send approval and denial
results through a local A2A helper that drops identifiers not beginning with @. Contact IDs such
as hub:hogne consequently receive no decision result or invite credentials.
Code

tinyagentos/routes/decisions.py[467]

+            await _route_answer_to_agent(decision, reply)
Relevance

●●● Strong

PRs 286 and 291 fixed analogous identifier-based silent A2A delivery failures.

PR-#286
PR-#291

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The decision records contact_id as from_agent, but _route_answer_to_agent immediately returns
for non-@ identifiers. The peer router exposes no decision polling endpoint that compensates for
the dropped reply.

tinyagentos/delegation_handler.py[219-235]
tinyagentos/routes/decisions.py[45-64]
tinyagentos/routes/peer.py[163-274]
tests/test_collab_d1_delegation.py[147-165]

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

## Issue description
Delegation decision results are routed to the local agent bus instead of the authenticated remote peer.

## Issue Context
Contact identifiers are not required to be local `@agent` addresses. Send a signed peer response/envelope through the contact's peer link, including all required redemption credentials.

## Fix Focus Areas
- tinyagentos/routes/decisions.py[445-480]
- tinyagentos/delegation_handler.py[219-235]
- tinyagentos/routes/decisions.py[45-64]
- tinyagentos/routes/peer.py[168-274]

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


2. Wrong invite-store state key 🐞 Bug ≡ Correctness
Description
Both delegation approval paths look up app.state.project_invite_store, but application wiring
exposes the store as app.state.project_invites. Every delegation approval therefore returns
invite store not available without minting an invite.
Code

tinyagentos/delegation_handler.py[R264-266]

+    invite_store = getattr(request.app.state, "project_invite_store", None)
+    if invite_store is None:
+        return {"status": "error", "error": "invite store not available"}
Relevance

●●● Strong

PR #358 accepted and fixed an equivalent nonexistent API/store name typo causing universal failure.

PR-#358

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The application wiring assigns only project_invites, and the existing redemption route
successfully uses that name; neither new delegation path does.

tinyagentos/app.py[1594-1600]
tinyagentos/routes/project_invites.py[1092-1095]
tinyagentos/delegation_handler.py[340-342]

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

## Issue description
Delegation approval uses an application-state attribute that is never configured, preventing invite minting.

## Issue Context
The application exposes `ProjectInviteStore` as `app.state.project_invites`, matching the existing invite routes.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[264-266]
- tinyagentos/delegation_handler.py[340-342]
- tinyagentos/app.py[1594-1600]

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


3. metadata duplicates migration ✓ Resolved 📜 Skill insight ≡ Correctness
Description
The new metadata column is present in SCHEMA and is also conditionally added in _post_init().
This violates the requirement that migration-added columns remain outside initial schema DDL.
Code

tinyagentos/projects/invite_store.py[36]

+    metadata             TEXT NOT NULL DEFAULT '{}'
Relevance

●●● Strong

PR #1853 confirms migration-dependent schema objects belong after column migration, not initial
SCHEMA.

PR-#1853

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
SCHEMA declares metadata TEXT NOT NULL DEFAULT '{}', and _post_init() conditionally runs an
equivalent ALTER TABLE when the column is absent. These two code regions establish the violation
of rule 2185190.

tinyagentos/projects/invite_store.py[17-38]
tinyagentos/projects/invite_store.py[79-118]
Skill: taos-development-skill

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

## Issue description
The `metadata` column appears in `SCHEMA` while `_post_init()` also introduces it with `ALTER TABLE`.

## Issue Context
Keep the guarded retrofit for existing databases, but do not reference the migration-added column in initial schema DDL.

## Fix Focus Areas
- tinyagentos/projects/invite_store.py[17-38]
- tinyagentos/projects/invite_store.py[79-118]

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


View more (5)
4. Curators cannot force close ✓ Resolved 🐞 Bug ≡ Correctness
Description
The force bypass is enabled only when the authenticated actor ID equals lead_member_id; session
owners and admins resolve to their user ID and are not automatically the designated member lead.
Consequently, a curator closing another agent's claimed task under their own identity receives 409
instead of the intended force-close behavior.
Code

tinyagentos/routes/projects.py[R958-959]

+    force = project.get("lead_member_id") == actor_id
+    ok = await store.close_task(task_id, closed_by=closed_by, reason=payload.reason, force=force)
Relevance

●●● Strong

PRs 260 and 721 accepted task authorization and project ownership corrections in this route.

PR-#260
PR-#721

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Session authorization returns the session user's ID, whereas the lead pointer is separately assigned
to a project member. The new equality check therefore does not express the session owner/admin
curator authorization already established by _authorize_task_actor.

tinyagentos/routes/projects.py[532-556]
tinyagentos/routes/projects.py[365-403]
tinyagentos/routes/projects.py[605-620]

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

## Issue description
The close route does not grant the ownership bypass to authorized session curators.

## Issue Context
Preserve the distinction between session owner/admin and agent callers when computing `force`; agents should require lead status, while authorized owner/admin sessions should receive the curator bypass intended by the route policy.

## Fix Focus Areas
- tinyagentos/routes/projects.py[939-963]
- tinyagentos/routes/projects.py[509-556]
- tinyagentos/routes/projects.py[365-403]

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


5. Wrong minted invite key 🐞 Bug ≡ Correctness
Description
The approval helpers read invite["id"], but ProjectInviteStore.mint returns the identifier at
invite["record"]["invite_id"]. A successful mint therefore raises KeyError, leaving an orphaned
pending invite while approval is reported as failed.
Code

tinyagentos/delegation_handler.py[296]

+        "invite_id": invite["id"],
Relevance

●●● Strong

PR #358 fixed an equivalent wrong API return/member lookup causing endpoint-wide failure.

PR-#358

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The store's return shape contains no top-level id; both newly added approval paths use that
nonexistent key.

tinyagentos/projects/invite_store.py[241-263]
tinyagentos/delegation_handler.py[378-382]

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

## Issue description
Delegation approval reads a nonexistent key from the invite-store result.

## Issue Context
`ProjectInviteStore.mint()` returns `{record: {invite_id: ...}, pin: ...}`.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[294-298]
- tinyagentos/delegation_handler.py[378-382]
- tinyagentos/projects/invite_store.py[241-263]

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


6. sponsor_contact_id duplicates migration ✓ Resolved 📜 Skill insight ≡ Correctness
Description
sponsor_contact_id was added to SCHEMA while the same column is introduced through the guarded
_migration_v5_add_sponsor_contact_id() path. This violates the store schema contract for
migration-added columns.
Code

tinyagentos/agent_registry_store.py[49]

+    sponsor_contact_id  TEXT
Relevance

●●● Strong

PR #1853 established migration-added schema objects must remain outside initial SCHEMA ordering.

PR-#1853

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The initial schema declares sponsor_contact_id, while _migration_v5_add_sponsor_contact_id()
conditionally executes ALTER TABLE agent_registry ADD COLUMN sponsor_contact_id TEXT and is
invoked from _post_init(). Rule 2185190 explicitly prohibits migration-added columns from
appearing in SCHEMA.

tinyagentos/agent_registry_store.py[35-50]
tinyagentos/agent_registry_store.py[209-226]
tinyagentos/agent_registry_store.py[433-441]
Skill: taos-development-skill

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

## Issue description
`sponsor_contact_id` appears in `SCHEMA` even though it is added by the guarded migration path.

## Issue Context
Migration-added columns must not be referenced by the initial schema. Preserve the idempotent migration and remove the column from `SCHEMA`.

## Fix Focus Areas
- tinyagentos/agent_registry_store.py[35-50]
- tinyagentos/agent_registry_store.py[209-226]

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


7. Delegation invite PIN discarded 🐞 Bug ≡ Correctness
Description
Delegation minting leaves the persisted pin_required argument at its default True and discards
the generated PIN, while writing pin_required: False only into inert metadata. Redemption still
requires and validates a PIN, so the remote agent cannot redeem the invite using the approval
response.
Code

tinyagentos/delegation_handler.py[284]

+                "pin_required": False,
Relevance

●● Moderate

PR #2087 fixed invite lifecycle issues, but no historical evidence addresses discarded generated
PINs.

PR-#2087

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
mint defaults pin_required=True and returns the generated PIN separately, while the redeem API
requires a PIN and the store always compares its hash. The approval reply contains only the invite
ID.

tinyagentos/projects/invite_store.py[135-142]
tinyagentos/projects/invite_store.py[241-263]
tinyagentos/projects/invite_store.py[369-383]
tinyagentos/routes/decisions.py[459-467]

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

## Issue description
Delegation approvals produce invites that require a PIN but never deliver that PIN to the remote contact.

## Issue Context
The metadata flag does not affect the invite store's `pin_required` column or its redeem-time PIN validation. Either securely deliver the generated PIN or implement and enforce a real pinless redemption contract.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[272-298]
- tinyagentos/delegation_handler.py[356-382]
- tinyagentos/projects/invite_store.py[135-142]
- tinyagentos/projects/invite_store.py[369-383]
- tinyagentos/routes/project_invites.py[65-69]

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


8. Cascade leaves claimed tasks 🐞 Bug ⛨ Security
Description
The revocation fallback searches for status="in_progress" and matching assignee_id, but this
task store represents active claims with status="claimed" and claimed_by. Even selected tasks
cannot clear assignment through update_task(..., assignee_id=None) because that method treats
None as unchanged.
Code

tinyagentos/delegation_handler.py[R505-509]

+            all_tasks = await task_store.list_tasks(
+                project_id=project_id,
+                status="in_progress",
+            )
+            tasks = [t for t in all_tasks if t.get("assignee_id") == canonical_id]
Relevance

●● Moderate

PR #260 accepted task lifecycle corrections, while PR #1662 rejected analogous task identifier
mismatch feedback.

PR-#260
PR-#1662

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
ProjectTaskStore establishes active ownership with claimed_by and status claimed, and ready
work uses status open. Its generic update method omits all None values, so the new cleanup's
fields and transition cannot perform the claimed release.

tinyagentos/projects/task_store.py[70-81]
tinyagentos/projects/task_store.py[270-292]
tinyagentos/projects/task_store.py[297-321]

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

## Issue description
Sponsor revocation does not release tasks held by revoked agents.

## Issue Context
Use the task store's actual claim lifecycle and ownership fields, preferably through an atomic store method that clears `claimed_by` and `claimed_at` and returns the task to `open` only when held by the revoked identity.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[488-524]
- tinyagentos/projects/task_store.py[297-305]
- tinyagentos/projects/task_store.py[307-357]
- tinyagentos/projects/task_store.py[246-292]

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



Remediation recommended

9. Decision text uses em dash 📜 Skill insight ✧ Quality
Description
The new delegation decision message includes an em dash in text displayed to the approving user.
Public-facing text must use a regular hyphen, en dash, or rewritten sentence instead.
Code

tinyagentos/delegation_handler.py[213]

+            f"(Hard-denied: {', '.join(denied_scopes)} — "
Relevance

●● Moderate

PR #2122 only partially accepted the same em-dash compliance feedback.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Line 213 constructs user-visible decision text containing , which is explicitly prohibited by
rule 2212258 for public-facing strings.

tinyagentos/delegation_handler.py[206-215]
Skill: taos-development-skill

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

## Issue description
The delegation decision message contains the prohibited Unicode em dash character.

## Issue Context
Replace the em dash with a regular hyphen, an en dash, or punctuation that preserves readability. Review other added comments, docstrings, and strings in this file for the same character.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[1-8]
- tinyagentos/delegation_handler.py[206-215]
- tinyagentos/delegation_handler.py[563-583]

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


10. Malformed scopes crash inbox 🐞 Bug ☼ Reliability
Description
Envelope validation accepts arbitrary values inside requested_scopes, then converts them to a set
and sorts them. Unhashable entries or mixed incomparable types raise TypeError, returning a server
error after the peer nonce has already been consumed.
Code

tinyagentos/delegation_handler.py[R87-88]

+    requested_scopes = body["requested_scopes"]
+    unknown = sorted(set(requested_scopes) - SPONSORED_DEFAULT_SCOPES - SPONSORED_DENY_SCOPES)
Relevance

●● Moderate

Similar malformed collection validation was raised in PR #513, but acceptance remains undetermined.

PR-#513

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The validator checks only that the outer value is a non-empty list, while the following set and sort
operations require hashable, mutually comparable values. The peer route invokes the handler after
recording the nonce and does not map these exceptions.

tinyagentos/delegation_handler.py[74-104]
tinyagentos/routes/peer.py[236-272]

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

## Issue description
Malformed scope-list elements can crash delegation request processing instead of producing a validation response.

## Issue Context
The peer route records the signed envelope's nonce before dispatch, so a crash also prevents the sender from correcting and retrying that nonce.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[74-104]
- tinyagentos/routes/peer.py[236-272]

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



Informational

11. Revocation cascade never invoked 🐞 Bug ⛨ Security
Description
The new cascade is called only by the unexposed kill-switch helper; existing project membership
removal and contact revocation paths never invoke it. Removing or revoking a sponsor therefore
leaves sponsored agent identities active with their existing grants.
Code

tinyagentos/delegation_handler.py[R385-391]

+async def cascade_sponsor_revoke(
+    request,
+    *,
+    contact_id: str,
+    project_id: str | None = None,
+    reason: str = "sponsor revoked",
+) -> dict:
Relevance

● Weak

PR #1662 consistently rejected delegation completion and security-path integration findings.

PR-#1662

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Repository call sites show cascade_sponsor_revoke is reached only from kill_switch_per_contact.
The production member-removal and contact-revocation methods perform their database changes directly
without invoking the cascade.

tinyagentos/delegation_handler.py[531-560]
tinyagentos/routes/projects.py[406-432]
tinyagentos/contacts_store.py[137-154]
tinyagentos/contacts_store.py[290-301]

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

## Issue description
Sponsor and membership revocations do not trigger the newly added sponsored-agent cascade.

## Issue Context
Invoke the cascade from actual contact-revoke and project-member-removal flows, with project scoping where appropriate. Define failure handling so a partial cascade cannot silently report successful revocation.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[385-485]
- tinyagentos/delegation_handler.py[531-560]
- tinyagentos/routes/projects.py[406-432]
- tinyagentos/contacts_store.py[137-154]
- tinyagentos/contacts_store.py[290-301]

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


12. PR bundles delegation feature 📜 Skill insight ⚙ Maintainability
Description
The PR is described as an ownership-guard fix for close_task, but it also adds a separate
cross-user delegation feature, registry schema changes, invite metadata, and associated tests. These
unrelated changes substantially expand the PR beyond its stated purpose.
Code

tinyagentos/delegation_handler.py[1]

+"""Agent delegation handler — cross-user collab D1.
Relevance

● Weak

PR #1662 merged substantial delegation work under a heartbeat-focused title, showing broad bundling
is tolerated.

PR-#1662

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The stated PR purpose concerns only close_task ownership, while the new delegation_handler.py
identifies itself as cross-user collaboration D1 and the new test module exclusively tests that
separate feature. Additional sponsor and invite-metadata changes confirm this is a distinct feature
rather than support for the ownership fix.

tinyagentos/delegation_handler.py[1-8]
tests/test_collab_d1_delegation.py[1-15]
tinyagentos/agent_registry_store.py[469-482]
tinyagentos/projects/task_store.py[361-382]
Skill: taos-development-skill

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

## Issue description
The ownership-guard bug fix is bundled with an unrelated cross-user collaboration and delegation feature.

## Issue Context
Retain the `close_task` ownership guard and its focused regression tests in this PR. Move the delegation handler, sponsor registry changes, invite metadata changes, route integration, and delegation tests into a separate feature PR.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[1-589]
- tests/test_collab_d1_delegation.py[1-300]
- tinyagentos/agent_registry_store.py[35-49]
- tinyagentos/projects/invite_store.py[32-36]
- tinyagentos/routes/peer.py[264-272]

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


13. Delegation response lacks model ✓ Resolved 📜 Skill insight ✧ Quality
Description
The new delegation_request branch returns a raw dictionary from a route without a declared
Pydantic response model. The response therefore lacks the validation and schema required for route
payloads.
Code

tinyagentos/routes/peer.py[272]

+        return {"status": result.get("status", "unknown"), "detail": result}
Relevance

● Weak

PR 2122 explicitly rejected the same Pydantic response-model requirement for raw route dictionaries.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The peer_inbox decorator does not declare response_model, and the newly added delegation branch
directly returns {"status": ..., "detail": result}. Rule 2185155 requires route request and
response payloads to use Pydantic models rather than raw dictionaries.

tinyagentos/routes/peer.py[168-174]
tinyagentos/routes/peer.py[264-272]
Skill: taos-development-skill

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

## Issue description
The delegation branch in `peer_inbox` returns an untyped raw dictionary and the route has no Pydantic response schema.

## Issue Context
Define an appropriate Pydantic response model and declare it through `response_model`, returning a model instance or a payload validated against that model.

## Fix Focus Areas
- tinyagentos/routes/peer.py[168-174]
- tinyagentos/routes/peer.py[264-272]

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


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread tinyagentos/agent_registry_store.py Outdated
Comment thread tinyagentos/projects/invite_store.py Outdated
Comment thread tinyagentos/delegation_handler.py Outdated
]
if denied_scopes:
question_parts.append(
f"(Hard-denied: {', '.join(denied_scopes)} — "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Decision text uses em dash 📜 Skill insight ✧ Quality

The new delegation decision message includes an em dash in text displayed to the approving user.
Public-facing text must use a regular hyphen, en dash, or rewritten sentence instead.
Agent Prompt
## Issue description
The delegation decision message contains the prohibited Unicode em dash character.

## Issue Context
Replace the em dash with a regular hyphen, an en dash, or punctuation that preserves readability. Review other added comments, docstrings, and strings in this file for the same character.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[1-8]
- tinyagentos/delegation_handler.py[206-215]
- tinyagentos/delegation_handler.py[563-583]

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

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +264 to +266
invite_store = getattr(request.app.state, "project_invite_store", None)
if invite_store is None:
return {"status": "error", "error": "invite store not available"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

6. Wrong invite-store state key 🐞 Bug ≡ Correctness

Both delegation approval paths look up app.state.project_invite_store, but application wiring
exposes the store as app.state.project_invites. Every delegation approval therefore returns
invite store not available without minting an invite.
Agent Prompt
## Issue description
Delegation approval uses an application-state attribute that is never configured, preventing invite minting.

## Issue Context
The application exposes `ProjectInviteStore` as `app.state.project_invites`, matching the existing invite routes.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[264-266]
- tinyagentos/delegation_handler.py[340-342]
- tinyagentos/app.py[1594-1600]

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

Comment thread tinyagentos/delegation_handler.py Outdated

return {
"status": "approved",
"invite_id": invite["id"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

7. Wrong minted invite key 🐞 Bug ≡ Correctness

The approval helpers read invite["id"], but ProjectInviteStore.mint returns the identifier at
invite["record"]["invite_id"]. A successful mint therefore raises KeyError, leaving an orphaned
pending invite while approval is reported as failed.
Agent Prompt
## Issue description
Delegation approval reads a nonexistent key from the invite-store result.

## Issue Context
`ProjectInviteStore.mint()` returns `{record: {invite_id: ...}, pin: ...}`.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[294-298]
- tinyagentos/delegation_handler.py[378-382]
- tinyagentos/projects/invite_store.py[241-263]

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

Comment thread tinyagentos/delegation_handler.py Outdated
"sponsor_contact_id": contact_id,
"agent_slug": agent_slug,
"display_name": display_name,
"pin_required": False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

8. Delegation invite pin discarded 🐞 Bug ≡ Correctness

Delegation minting leaves the persisted pin_required argument at its default True and discards
the generated PIN, while writing pin_required: False only into inert metadata. Redemption still
requires and validates a PIN, so the remote agent cannot redeem the invite using the approval
response.
Agent Prompt
## Issue description
Delegation approvals produce invites that require a PIN but never deliver that PIN to the remote contact.

## Issue Context
The metadata flag does not affect the invite store's `pin_required` column or its redeem-time PIN validation. Either securely deliver the generated PIN or implement and enforce a real pinless redemption contract.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[272-298]
- tinyagentos/delegation_handler.py[356-382]
- tinyagentos/projects/invite_store.py[135-142]
- tinyagentos/projects/invite_store.py[369-383]
- tinyagentos/routes/project_invites.py[65-69]

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

Comment thread tinyagentos/routes/decisions.py Outdated
f"Invite ID: {invite_id}. "
f"Share this invite with the remote contact so their agent can join."
)
await _route_answer_to_agent(decision, reply)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

9. Approval replies miss contacts 🐞 Bug ≡ Correctness

Collaboration decisions store the peer contact_id as from_agent, then send approval and denial
results through a local A2A helper that drops identifiers not beginning with @. Contact IDs such
as hub:hogne consequently receive no decision result or invite credentials.
Agent Prompt
## Issue description
Delegation decision results are routed to the local agent bus instead of the authenticated remote peer.

## Issue Context
Contact identifiers are not required to be local `@agent` addresses. Send a signed peer response/envelope through the contact's peer link, including all required redemption credentials.

## Fix Focus Areas
- tinyagentos/routes/decisions.py[445-480]
- tinyagentos/delegation_handler.py[219-235]
- tinyagentos/routes/decisions.py[45-64]
- tinyagentos/routes/peer.py[168-274]

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

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +505 to +509
all_tasks = await task_store.list_tasks(
project_id=project_id,
status="in_progress",
)
tasks = [t for t in all_tasks if t.get("assignee_id") == canonical_id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

10. Cascade leaves claimed tasks 🐞 Bug ⛨ Security

The revocation fallback searches for status="in_progress" and matching assignee_id, but this
task store represents active claims with status="claimed" and claimed_by. Even selected tasks
cannot clear assignment through update_task(..., assignee_id=None) because that method treats
None as unchanged.
Agent Prompt
## Issue description
Sponsor revocation does not release tasks held by revoked agents.

## Issue Context
Use the task store's actual claim lifecycle and ownership fields, preferably through an atomic store method that clears `claimed_by` and `claimed_at` and returns the task to `open` only when held by the revoked identity.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[488-524]
- tinyagentos/projects/task_store.py[297-305]
- tinyagentos/projects/task_store.py[307-357]
- tinyagentos/projects/task_store.py[246-292]

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

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +87 to +88
requested_scopes = body["requested_scopes"]
unknown = sorted(set(requested_scopes) - SPONSORED_DEFAULT_SCOPES - SPONSORED_DENY_SCOPES)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

12. Malformed scopes crash inbox 🐞 Bug ☼ Reliability

Envelope validation accepts arbitrary values inside requested_scopes, then converts them to a set
and sorts them. Unhashable entries or mixed incomparable types raise TypeError, returning a server
error after the peer nonce has already been consumed.
Agent Prompt
## Issue description
Malformed scope-list elements can crash delegation request processing instead of producing a validation response.

## Issue Context
The peer route records the signed envelope's nonce before dispatch, so a crash also prevents the sender from correcting and retrying that nonce.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[74-104]
- tinyagentos/routes/peer.py[236-272]

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

Comment thread tinyagentos/routes/projects.py Outdated
@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Reviewed at code level, including a red-first proof of the guard tests and an audit of every live caller of the close route. BLOCKED on two structural problems. @hognek fix-forward, but read item 1 first because it changes the branch itself.

  1. The branch is contaminated with PR feat(collab): agent delegation handshake + sponsor_contact_id + cascades (D1) #2048. fix/2191-close-ownership-guard was cut from feat/collab-d1-agent-delegation, not origin/dev. Only the last commit (859afe7e, 3 files, +71/-7) is this fix; the other ~1,470 lines are feat(collab): agent delegation handshake + sponsor_contact_id + cascades (D1) #2048's entire unreviewed D1 feature, and merging this PR would land that feature silently, bypassing its review. Re-cut: cherry-pick 859afe7e alone onto a fresh branch from origin/dev and force-push. Two of the riding D1 defects are confirmed real; I have posted them on feat(collab): agent delegation handshake + sponsor_contact_id + cascades (D1) #2048 where they belong.

  2. The caller audit Task close route has no ownership guard: any project-scoped actor can close any card #2191 mandated was not done, and the guard breaks a live caller. The issue said "Do not enforce before that caller audit". As written, the only bypass is lead_member_id == actor_id, so:

    • The admin-session sweeper that closes merged cards gets a 409 on every claimed card (its closed_by is the admin id, never the claimer or lead). The board wedges in exactly the direction the issue predicted. I am hardening the sweeper on my side, but the guard still needs a bypass that covers session owner/admin callers, which also fixes the desktop UI: ProjectTaskList.tsx:88 and TaskModal.tsx:62 close as currentUserId, so a human owner closing an agent-claimed card 409s today.
    • tinyagentos/github_sync.py:83,89 and tinyagentos/projects/beads_bridge.py:408 both call close and treat False as a no-op with no log line. After your fix, add at least a warning log on the rejected path in both, and state the audit result for all four callers (sweeper, merge gate, github_sync, beads_bridge) in the PR body as the issue demanded.
  3. Issue proposal 2 is silently inverted. Task close route has no ownership guard: any project-scoped actor can close any card #2191 proposed that an unclaimed card cannot be closed by a build lane ("giving up is a release"). The PR does the opposite and pins it with test_close_unclaimed_unchanged. Either implement it or record in the PR body why not. Related honesty point for the body: the guard would not have stopped the original incident's primary path (same identity claimed then closed), so state what it does and does not prevent.

  4. Tests are store-level only. The red-first check passes (guard reverted, test_close_by_stranger_rejected fails as it should), but there is no route-level test of the bypass computation, the 409 body, or the admin-session path, which is precisely how item 2 survived green CI. Add route-level tests for: stranger 409, lead bypass, owner/admin bypass, and non-member 404 (the existence-hiding 404 is correct today, keep it that way).

What already holds up, so you do not re-litigate it: the guard is store-level SQL, correctly parenthesized, fails closed, agent spoofing is rejected with 403, and there is no new existence oracle (the 409 is only reachable by actors already authorized on the project).

Bot adjudication: Qodo 13 (curator/owner cannot force close) is valid and is item 2. Qodo 6 and 7 are valid but target the D1 code riding on the branch; they resolve on #2048. Qodo 1/2/4/8/9/10/12 likewise belong to #2048 and go moot here once the branch is re-cut. CodeRabbit's success status reads "Review rate limited": it reviewed nothing, do not count it. Gitar's pass is a free-plan pass, also not a review. Kilo reported FAIL; read its findings on the re-cut branch before requesting re-review.

Re-request review after the re-cut and I will re-verify with the same probes.

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Now CONFLICTING after #2194 merged, because this branch still carries #2048's delegation code which collides with the merged decisions.py changes. No new work: the re-cut from origin/dev with only 859afe7 (blocking item 1) resolves the conflict automatically, and the fresh base picks up the merged decisions fixes for free.

hognek added 2 commits July 29, 2026 17:16
Add claim-holder check to close_task() SQL (AND claimed_by IS NULL OR
claimed_by = ?), mirroring the release guard pattern.  The route handler
passes force=True when the caller is the project lead/curator, letting
leads close cards claimed by others.

Fixes jaylfc#2191
…audit + route tests

Addresses all four blocking items from jaylfc's review on jaylfc#2196:

1. RE-CUT: branch is now clean cherry-pick of 859afe7 onto origin/dev
   (no jaylfc#2048 contamination)

2. OWNER/ADMIN BYPASS: the route now sets force=True for session
   owner (project.user_id), session admin (request.state.is_admin),
   and lead (lead_member_id) — not just the lead. This fixes:
   - Admin-session sweeper that closes merged cards (was 409 on
     every claimed card)
   - Desktop UI: owner closing an agent-claimed card (was 409)

3. CALLER AUDIT: added warning logs on rejected close paths in
   github_sync.py (both create-then-close and existing-card-close)
   and beads_bridge.py (close verb). An audit statement in the PR
   body covers all four callers (sweeper, merge gate, github_sync,
   beads_bridge).

4. ROUTE-LEVEL TESTS: admin force-close, nonexistent task 404.
   Store-level tests cover stranger-rejected and lead-force-pass.

Proposal 2 (unclaimed cards) kept as-is: forcing claim-first would
break github_sync, beads_bridge, the old test suites, and the
sweeper. Unclaimed cards remain closeable by any authorized actor,
matching the release guard's scope.
@hognek
hognek force-pushed the fix/2191-close-ownership-guard branch from 859afe7 to 087fbf0 Compare July 29, 2026 15:27
@hognek

hognek commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@jaylfc re-cut and addressed all four items:

  1. Re-cut: branch is clean — just 859afe7 (close guard) + this cleanup commit, no feat(collab): agent delegation handshake + sponsor_contact_id + cascades (D1) #2048 contamination
  2. Owner/admin bypass: now covers lead, session owner (), and session admin ()
  3. Caller audit: warning logs in github_sync.py (both call sites) and beads_bridge.py on rejected closes. All four callers documented in PR body.
  4. Route tests: admin force-close + nonexistent 404. Store-level tests cover stranger-rejected and lead-force-pass.

Proposal 2 (unclaimed cards): kept as-is — forcing claim-first would break github_sync, beads_bridge, the sweeper, and the existing test suites. Unclaimed cards remain closeable by any authorized actor, matching the release guard scope.

Ready for re-review.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/test_board_audit_wiring.py (1)

100-125: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add route-level coverage for the ownership matrix.

These tests cover admin force-close and 404, but not the new 409 stranger rejection or lead/owner force bypasses. Add cases that verify a non-privileged stranger receives 409 and leaves the claim intact, plus lead and owner closes of another user’s claimed task.

🤖 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 `@tests/test_board_audit_wiring.py` around lines 100 - 125, Add route-level
tests alongside test_admin_can_force_close_claimed_card covering the ownership
matrix: verify a non-privileged stranger receives 409 when closing another
user’s claimed task and that the claim remains intact, then add cases confirming
lead and owner users can force-close another user’s claimed task successfully.
Reuse the existing task setup and assert the relevant status and claim state.
tinyagentos/github_sync.py (1)

89-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for refused closes.

Test both newly created and existing cards when close_task() returns False; assert that a warning is emitted and closed is not incremented.

🤖 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 `@tinyagentos/github_sync.py` around lines 89 - 104, Add regression tests for
the GitHub sync flow covering both newly created cards and existing cards when
task_store.close_task returns False. In each case, assert that the appropriate
warning is emitted and the closed counter remains unchanged.
🤖 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 `@tinyagentos/projects/beads_bridge.py`:
- Around line 408-415: Update the refusal handling after _task_store.close_task
in the task-closing flow to log at warning level instead of info, and use
neutral wording that does not assume the task was claimed by another actor.
Preserve the existing identifiers and refusal behavior while removing the
ownership-specific explanation.

In `@tinyagentos/routes/projects.py`:
- Around line 965-968: Update the failed-close handling in close_task to
classify the result from the current row or a structured atomic-update failure
reason, rather than the stale existing pre-read. Treat terminal tasks as
non-ownership failures, and allow forced admin/owner/lead callers to avoid the
"not claimed by you" response; emit that response only when the current task is
non-terminal, non-forced, and owned by someone else.

---

Nitpick comments:
In `@tests/test_board_audit_wiring.py`:
- Around line 100-125: Add route-level tests alongside
test_admin_can_force_close_claimed_card covering the ownership matrix: verify a
non-privileged stranger receives 409 when closing another user’s claimed task
and that the claim remains intact, then add cases confirming lead and owner
users can force-close another user’s claimed task successfully. Reuse the
existing task setup and assert the relevant status and claim state.

In `@tinyagentos/github_sync.py`:
- Around line 89-104: Add regression tests for the GitHub sync flow covering
both newly created cards and existing cards when task_store.close_task returns
False. In each case, assert that the appropriate warning is emitted and the
closed counter remains unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f2047e65-1bdb-46ba-ba8b-f2711be09e76

📥 Commits

Reviewing files that changed from the base of the PR and between 6dda472 and 087fbf0.

📒 Files selected for processing (6)
  • tests/projects/test_task_store.py
  • tests/test_board_audit_wiring.py
  • tinyagentos/github_sync.py
  • tinyagentos/projects/beads_bridge.py
  • tinyagentos/projects/task_store.py
  • tinyagentos/routes/projects.py

Comment on lines +408 to +415
ok = await self._task_store.close_task(
tsk_id, closed_by=author, reason=note
)
if not ok:
logger.info(
"beads bridge: /%s %s by %s refused (not claimed by closer)",
verb, tsk_id, author,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log refusals as warnings without assuming the cause.

close_task() can return False for an already closed/cancelled or missing task, not only when another actor owns the claim. The current info message is therefore misleading and weaker than the warning-level reporting in tinyagentos/github_sync.py.

Use neutral warning text, or return a structured refusal reason from the store before logging an ownership-specific cause.

Proposed minimal fix
-                        logger.info(
-                            "beads bridge: /%s %s by %s refused (not claimed by closer)",
+                        logger.warning(
+                            "beads bridge: /%s %s by %s refused",
                             verb, tsk_id, author,
                         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ok = await self._task_store.close_task(
tsk_id, closed_by=author, reason=note
)
if not ok:
logger.info(
"beads bridge: /%s %s by %s refused (not claimed by closer)",
verb, tsk_id, author,
)
ok = await self._task_store.close_task(
tsk_id, closed_by=author, reason=note
)
if not ok:
logger.warning(
"beads bridge: /%s %s by %s refused",
verb, tsk_id, author,
)
🤖 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 `@tinyagentos/projects/beads_bridge.py` around lines 408 - 415, Update the
refusal handling after _task_store.close_task in the task-closing flow to log at
warning level instead of info, and use neutral wording that does not assume the
task was claimed by another actor. Preserve the existing identifiers and refusal
behavior while removing the ownership-specific explanation.

Comment on lines 965 to 968
if not ok:
if existing.get("claimed_by") and existing["claimed_by"] != closed_by:
return JSONResponse({"error": "not claimed by you"}, status_code=409)
return JSONResponse({"error": "cannot close"}, status_code=409)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Classify failed closes from current state, not the stale pre-read.

Because close_task preserves claimed_by after closing, retrying a terminal claimed task can return "not claimed by you" even for an admin/owner/lead using force=True. A claim acquired between the initial read and the atomic update is also misclassified. Re-read the current row (or return a structured failure reason) and emit the ownership response only for a non-terminal, non-forced rejection.

Proposed fix
     ok = await store.close_task(task_id, closed_by=closed_by, reason=payload.reason, force=force)
     if not ok:
-        if existing.get("claimed_by") and existing["claimed_by"] != closed_by:
+        current = await store.get_task(task_id)
+        if (
+            not force
+            and current is not None
+            and current.get("status") not in ("closed", "cancelled")
+            and current.get("claimed_by")
+            and current["claimed_by"] != closed_by
+        ):
             return JSONResponse({"error": "not claimed by you"}, status_code=409)
         return JSONResponse({"error": "cannot close"}, status_code=409)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not ok:
if existing.get("claimed_by") and existing["claimed_by"] != closed_by:
return JSONResponse({"error": "not claimed by you"}, status_code=409)
return JSONResponse({"error": "cannot close"}, status_code=409)
ok = await store.close_task(task_id, closed_by=closed_by, reason=payload.reason, force=force)
if not ok:
current = await store.get_task(task_id)
if (
not force
and current is not None
and current.get("status") not in ("closed", "cancelled")
and current.get("claimed_by")
and current["claimed_by"] != closed_by
):
return JSONResponse({"error": "not claimed by you"}, status_code=409)
return JSONResponse({"error": "cannot close"}, status_code=409)
🤖 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 `@tinyagentos/routes/projects.py` around lines 965 - 968, Update the
failed-close handling in close_task to classify the result from the current row
or a structured atomic-update failure reason, rather than the stale existing
pre-read. Treat terminal tasks as non-ownership failures, and allow forced
admin/owner/lead callers to avoid the "not claimed by you" response; emit that
response only when the current task is non-terminal, non-forced, and owned by
someone else.

@kilo-code-bot

kilo-code-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • tests/projects/test_task_store.py - 4 new store-level tests
  • tests/test_board_audit_wiring.py - 2 new route-level tests
  • tinyagentos/github_sync.py - warning logs on rejected close
  • tinyagentos/projects/beads_bridge.py - info log on refused close
  • tinyagentos/projects/task_store.py - force kwarg on close_task
  • tinyagentos/routes/projects.py - lead/owner/admin bypass for force-close

Reviewed by step-3.7-flash · Input: 183.7K · Output: 30.9K · Cached: 2.4M

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.

2 participants