Skip to content

feat(collab): add member_kind=human + collab invite kind + two-sided consent (B1)#2045

Merged
jaylfc merged 4 commits into
jaylfc:devfrom
hognek:feat/collab-b1-human-member-kind
Jul 19, 2026
Merged

feat(collab): add member_kind=human + collab invite kind + two-sided consent (B1)#2045
jaylfc merged 4 commits into
jaylfc:devfrom
hognek:feat/collab-b1-human-member-kind

Conversation

@hognek

@hognek hognek commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements milestone B1 from the cross-user collaboration epic (#2012).

Changes

  • member_kind=human in project_store — widen validation from to
  • kind column on project_invites — (default, backward compatible) or
  • pin_required toggle on project_invites — default on, waivable per-invite for auto-mediated flows
  • contact_id column on project_invites — the target contact for collab invites
  • POST /api/projects/{id}/invites/collab — mint a collab invite and deliver as signed envelope over peer channel
  • Peer inbox dispatch — envelopes create Decisions cards (approve_deny); adds , expires the invite
  • Projects routes — now accepts with field

Design

Follows section 4. Reuses #2002 invite state machine (mint/TTL/attempt-cap/claimed). PIN delivered out of band per spec; envelope carries invite_id and project info but not the PIN. Two-sided consent: inviter consents by minting, invitee consents via Decisions card.

Tests

  • 12 new tests: 4 project_store (human member kind), 8 invite_store (collab kind, pin_required, contact_id, migration, mark_accepted)
  • All existing tests pass (34 store + 57 peer + invite route tests)

Fixes #2016

Summary by CodeRabbit

  • New Features
    • Added human-collaboration invite flow over peer connections, including owner/admin enforcement, active contact validation, duplicate pending prevention, and delivery reporting.
    • Extended invite minting to support invite kind, PIN requirement, and contact metadata, with migration defaults applied for existing data.
    • Added collab acceptance/decline routing, including recording acceptance, expiring pending invites, and creating decisions when available.
    • Added support for human project members (add/remove) alongside existing member types.
  • Tests
    • Expanded coverage for collab invite behaviors, migration backfill defaults, and human member handling.

…consent (B1)

- Widen project_store add_member validation from (native, clone) to
  (native, clone, human), accepting contact_id as member_id.

- Add kind (agent|collab), pin_required (bool), and contact_id columns
  to project_invites table with boot-time migrations for existing DBs.

- Extend ProjectInviteStore.mint() with kind, pin_required, and
  contact_id parameters, plus mark_accepted() for collab response flow.

- Add POST /api/projects/{id}/invites/collab route for minting collab
  invites and delivering them as signed envelopes over the peer channel.

- Update peer inbox to dispatch collab_invite envelopes to Decisions
  cards (approve_deny type) and handle collab_invite_accept/decline
  response envelopes (add member_kind=human on accept, expire on decline).

- Widen projects routes add_member to accept mode=human with
  contact_id field.

- Add 12 tests covering human member kind, collab invite minting,
  migration safety, and mark_accepted flow.

Reuses jaylfc#2002 invite state machine. Design: docs/design/cross-user-collaboration.md
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@hognek
hognek marked this pull request as ready for review July 19, 2026 11:54
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 311aef7c-9fbf-4ee4-b4e5-d57120b84705

📥 Commits

Reviewing files that changed from the base of the PR and between da3feb9 and b920939.

📒 Files selected for processing (4)
  • tests/projects/test_invite_store.py
  • tinyagentos/projects/invite_store.py
  • tinyagentos/routes/peer.py
  • tinyagentos/routes/project_invites.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tinyagentos/projects/invite_store.py
  • tinyagentos/routes/peer.py
  • tests/projects/test_invite_store.py
  • tinyagentos/routes/project_invites.py

📝 Walkthrough

Walkthrough

Adds collab invite metadata and delivery, human project membership, peer-based approval handling, invite acceptance lifecycle updates, and coverage for validation, migration, coexistence, and removal behavior.

Changes

Collab B1

Layer / File(s) Summary
Invite metadata and lifecycle
tinyagentos/projects/invite_store.py, tests/projects/test_invite_store.py
Invites store kind, pin_required, and contact_id, migrate legacy schemas with defaults, list pending collab invites, and record acceptance or expiration state.
Human project membership
tinyagentos/projects/project_store.py, tinyagentos/routes/projects.py, tests/projects/test_project_store.py
Project members accept human as a kind, and human-mode requests derive membership from contact_id with human-specific defaults.
Collab invite minting and delivery
tinyagentos/routes/project_invites.py
Adds validated collab invite requests and an endpoint that checks project and contact state, prevents duplicates, mints invites, and attempts peer delivery.
Peer consent dispatch and response handling
tinyagentos/routes/peer.py
Peer envelopes dispatch to approval decisions, while acceptance adds a human member and updates the invite; declines expire the invite.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • jaylfc/taOS#2025: Provides peer-envelope infrastructure used by collab invite delivery.
  • jaylfc/taOS#2032: Adds related peer envelope processing and dispatch foundations.

Sequence Diagram(s)

sequenceDiagram
  participant ProjectClient
  participant mint_collab_invite
  participant contacts_store
  participant peer_inbox
  participant decision_store
  participant project_store
  ProjectClient->>mint_collab_invite: Submit collab invite
  mint_collab_invite->>contacts_store: Resolve active contact and peer links
  mint_collab_invite->>peer_inbox: Deliver collab_invite envelope
  peer_inbox->>decision_store: Create approval decision
  peer_inbox->>project_store: Add accepted contact as human member
  peer_inbox->>project_store: Update invite acceptance or expiration
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 concisely describes the main changes: human members, collab invite kind, and two-sided consent.
Linked Issues check ✅ Passed The patch covers human members, collab invite kinds with pin_required, peer delivery, Decisions handling, and acceptance/expiration flows.
Out of Scope Changes check ✅ Passed All modified files implement the collab B1 flow or its tests; no unrelated feature work appears in the patch.
✨ 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.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@gitar-bot

gitar-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

"error": "project_store not available"}

try:
await project_store.add_member(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: Accept path adds the authenticated contact to an arbitrary project without verifying the invite.

invite_id and project_id come straight from the signed envelope body, and the only checks are non-empty strings (lines 456-461). There is no lookup of the invite to confirm (a) it exists, (b) its contact_id equals the authenticated contact_id, or (c) it actually belongs to the named project_id. Because the envelope is signed by the contact themselves, any contact can forge a collab_invite_accept body naming any project_id they know and get themselves added as a human member to that project — broken access control / privilege escalation. Add a invite_store.get(invite_id) lookup and assert row["contact_id"] == contact_id and row["project_id"] == project_id before adding the member.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/projects/invite_store.py Outdated
"""
UPDATE project_invites
SET redeemed_by = ?, status = 'redeemed'
WHERE invite_id = ? AND status = 'claimed'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: mark_accepted is a no-op for collab invites — they are never transitioned out of pending.

The UPDATE only matches rows WHERE status = 'claimed' (line 412). But collab invites are minted with status='pending' and the collab acceptance flow (peer _handle_collab_response) never calls redeem(), so they never reach claimed. Consequently mark_accepted matches zero rows for every collab accept, the invite stays pending forever, and list_pending_collab_for_contact keeps returning it as a live pending invite. Either flip pending -> redeemed directly here, or route collab accepts through redeem() first so claimed is reached. As written, accepted collab invites are never marked redeemed.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/project_invites.py Outdated
"Content-Type": "application/json",
},
)
if resp.status_code < 500:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: A non-2xx response < 500 is treated as successful delivery.

if resp.status_code < 500: delivered = True counts any 4xx (e.g. 401/403 bad outbound token, 404 wrong endpoint path) as a successful delivery. The inviter then sees delivered: true while the invitee never received the envelope, and no further endpoints are tried. This both masks delivery failures and can be exploited to spoof delivery. Only treat an explicit success (e.g. resp.status_code == 200 or resp.is_success) as delivered, and keep iterating on any non-2xx.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/peer.py Outdated
try:
row = await invite_store.get(invite_id)
if row and row.get("status") == "pending":
await invite_store._db.execute(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Decline path reaches into the private invite_store._db instead of using a store method.

Lines 508-512 issue a raw UPDATE project_invites ... SET status='expired' against invite_store._db. This bypasses the store abstraction (and would break if _db were ever wrapped, e.g. for transactions). Add a mark_expired(invite_id) / revoke-style method on ProjectInviteStore and call it here, mirroring mark_accepted. Also note this expires regardless of current status beyond the pending guard, so a claimed/redeemed invite could be force-expired by a forged decline — pair this with the invite-binding check from the accept finding.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/project_invites.py Outdated
from tinyagentos.peer import build_envelope, resolve_local_identity_id

# Resolve the local hub identity and the contact's hub username.
contact_username = contact.get("hub_username", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: contact.get("hub_username", "") defaults to an empty string; an empty to_username builds an undeliverable envelope.

If a contact has no hub_username, build_envelope is still called and the envelope is POSTed to the endpoints, where it will fail with a 4xx. Combined with the status_code < 500 delivery check above, this is silently reported as delivered: true. Validate that hub_username is non-empty before building/delivering, and return a clear delivery_error otherwise.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Overview

Incremental diff since 7d3ab828 touches a single file (tinyagentos/projects/invite_store.py). The change rejects scoped collab invites (kind == "collab" and scopesValueError), which is correct and matches the new test. All five prior CRITICAL/WARNING/SUGGESTION findings were re-verified against current HEAD (b9209390) and are resolved:

  • tinyagentos/routes/peer.py:472-487 — accept path now verifies invite exists, is pending, and matches project + contact (CRITICAL resolved).
  • tinyagentos/projects/invite_store.py:411-413mark_accepted now transitions pending/claimedredeemed (CRITICAL no-op resolved).
  • tinyagentos/routes/project_invites.py:960 — delivery success now requires 200 <= status < 300 (WARNING resolved).
  • tinyagentos/routes/peer.py:523 — decline uses new mark_expired() store method instead of private _db (WARNING resolved).
  • tinyagentos/routes/project_invites.py:909-911 — empty hub_username is now guarded before envelope build (SUGGESTION resolved).
Files Reviewed (1 file in incremental diff)
  • tinyagentos/projects/invite_store.py - 0 new issues
Previous Review Summaries (3 snapshots, latest commit 7d3ab82)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 7d3ab82)

Status: No Issues Found | Recommendation: Merge

Overview

The incremental diff since commit d4d2be6 touches a single file (tinyagentos/routes/project_invites.py). The only change wraps the blocking call resolve_local_identity_id in asyncio.to_thread(...), which is the correct pattern inside an async route handler. No new issues introduced.

Files Reviewed (1 file)
  • tinyagentos/routes/project_invites.py - 0 new issues (prior findings on this file were resolved in d4d2be6 and are not re-touched here)
Prior findings (not in incremental diff / already resolved)
  • tinyagentos/routes/peer.py:490 (CRITICAL, invite verification) — resolved in d4d2be6.
  • tinyagentos/projects/invite_store.py (CRITICAL, mark_accepted no-op) — resolved in d4d2be6.
  • tinyagentos/routes/project_invites.py:955 (WARNING, non-2xx delivery) — resolved in d4d2be6.
  • tinyagentos/routes/peer.py (WARNING, private _db access) — resolved in d4d2be6.
  • tinyagentos/routes/project_invites.py:908 (SUGGESTION, empty hub_username) — resolved in d4d2be6.

Previous review (commit d4d2be6)

Status: No Issues Found | Recommendation: Merge

All 5 prior findings have been resolved by commit d4d2be6:

  • peer.py:473 — Accept path now verifies the invite exists, is pending, and matches project_id + contact_id before adding the member (privilege-escalation fix).
  • invite_store.pymark_accepted now matches status IN ('pending','claimed'), so collab invites transition to redeemed (was a no-op).
  • project_invites.py:955 — Delivery success now requires 200 <= status_code < 300; 4xx/5xx are no longer misreported as delivered.
  • peer.py:520 — Decline path now calls invite_store.mark_expired() instead of reaching into private _db.
  • project_invites.py:908 — Empty hub_username now short-circuits with a delivery_error instead of building an undeliverable envelope.
Files Reviewed (4 files)
  • tinyagentos/projects/invite_store.py - 0 new issues (prior issue resolved)
  • tinyagentos/routes/peer.py - 0 new issues (prior issues resolved)
  • tinyagentos/routes/project_invites.py - 0 new issues (prior issues resolved)
  • tests/projects/test_invite_store.py - 0 new issues

Previous review (commit da3feb9)

Status: 5 Issues Found | Recommendation: Address before merge

Overview

| Severity | Count |
|----------|-------:|CRITICAL | 2 |
| WARNING | 2 |
| SUGGESTION | 1 |

Issue Details (click to expand)

CRITICAL

File Line Issue
tinyagentos/routes/peer.py 473 Accept path adds contact to arbitrary project without verifying the invite (broken access control / privilege escalation)
tinyagentos/projects/invite_store.py 412 mark_accepted only matches status='claimed'; collab invites never reach claimed, so accepted invites stay pending forever (no-op)

WARNING

File Line Issue
tinyagentos/routes/project_invites.py 955 status_code < 500 treated as delivered; 4xx (401/403/404) misreported as success and masks failures
tinyagentos/routes/peer.py 508 Decline path reaches into private invite_store._db instead of a store method

SUGGESTION

File Line Issue
tinyagentos/routes/project_invites.py 908 Empty hub_username builds undeliverable envelope, silently reported as delivered
Files Reviewed (7 files)
  • tinyagentos/projects/invite_store.py - 1 issue
  • tinyagentos/projects/project_store.py - 0 issues
  • tinyagentos/routes/peer.py - 2 issues
  • tinyagentos/routes/project_invites.py - 2 issues
  • tinyagentos/routes/projects.py - 0 issues
  • tests/projects/test_invite_store.py - 0 issues
  • tests/projects/test_project_store.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 76.3K · Output: 2K · Cached: 160.5K

CRITICAL fixes:
- Accept path now verifies invite exists, is pending, and matches
  project_id + contact_id before adding member (broken access control).
- mark_accepted now accepts both 'pending' and 'claimed' status so
  collab invites (minted as pending) transition correctly.

WARNING fixes:
- Delivery status check changed from <500 to 200-299 so 4xx
  failures are not misreported as success.
- Decline path uses new mark_expired() store method instead of
  reaching into private _db attribute.
- Empty hub_username is now guarded before building envelope.

Added: mark_expired() store method, updated tests for direct
pending→redeemed mark_accepted, and from-claimed path.

@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: 1

🧹 Nitpick comments (1)
tinyagentos/routes/project_invites.py (1)

909-909: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Offload the blocking resolve_local_identity_id call.

resolve_local_identity_id opens a synchronous sqlite3 connection and reads hub.db, which blocks the event loop when called directly on this async request path. peer_inbox in tinyagentos/routes/peer.py (Line 181) already wraps the same call in await asyncio.to_thread(...); mirror that here for consistency.

♻️ Proposed change
-        local_id = resolve_local_identity_id(request.app.state.data_dir)
+        local_id = await asyncio.to_thread(
+            resolve_local_identity_id, request.app.state.data_dir
+        )

(ensure asyncio is imported in this module)

🤖 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/project_invites.py` at line 909, Update the async request
path containing resolve_local_identity_id to call it via await
asyncio.to_thread(...), matching the existing peer_inbox pattern. Ensure asyncio
is imported in the module and preserve the resulting local_id behavior.
🤖 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/routes/peer.py`:
- Around line 466-494: Validate the invite before mutating membership in the
accepted branch of the peer-inbox handler. Use the project/contact identifiers
to load the pending collab invite, confirm it exists and is addressed to
contact_id, and return the existing received/error response without calling
add_member or log_activity when validation fails. Keep the current membership
mutation only after this authorization guard succeeds.

---

Nitpick comments:
In `@tinyagentos/routes/project_invites.py`:
- Line 909: Update the async request path containing resolve_local_identity_id
to call it via await asyncio.to_thread(...), matching the existing peer_inbox
pattern. Ensure asyncio is imported in the module and preserve the resulting
local_id behavior.
🪄 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: 790ec8a3-d572-43f8-857d-8b3ad84cf190

📥 Commits

Reviewing files that changed from the base of the PR and between 992d797 and da3feb9.

📒 Files selected for processing (7)
  • tests/projects/test_invite_store.py
  • tests/projects/test_project_store.py
  • tinyagentos/projects/invite_store.py
  • tinyagentos/projects/project_store.py
  • tinyagentos/routes/peer.py
  • tinyagentos/routes/project_invites.py
  • tinyagentos/routes/projects.py

Comment thread tinyagentos/routes/peer.py
CodeRabbit nitpick: the blocking sqlite3 call inside
resolve_local_identity_id must be offloaded to a thread to avoid
blocking the async event loop in the collab invite route handler.
Mirrors the pattern already used in routes/peer.py.
@hognek
hognek force-pushed the feat/collab-b1-human-member-kind branch from 443235a to 7d3ab82 Compare July 19, 2026 12:03
@jaylfc

jaylfc commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Close to mergeable: CI genuinely green on post-fix dev, zero bot findings, the kind whitelist + dual SCHEMA/migration pattern for the new column are both correct, and the #1993 burn/scope protections are untouched. One MUST-FIX before I take it:

mint() validates kind against _VALID_KINDS but never constrains scopes for kind=collab. As written, mint(kind="collab", scopes=["a2a_send", ...]) succeeds and the invite row stores those scopes. The locked design (cross-user-collaboration.md, decision 2 + the scope hard-deny) says human collaborators carry NO agent scopes; delegation arrives later via the D-milestone handshake, never on the human invite. Even if the collab redeem path ignores stored scopes today, a scope-carrying collab invite is a footgun for whoever wires redeem next.

Fold: in mint(), when kind == "collab" raise ValueError unless scopes is empty, plus a test asserting the rejection (mirror of test_default_kind_is_agent). Pitfall 1 spirit: the constraint plus its negative test land together.

Everything else in the slice reads clean. Fix that one gate and I merge on the next pass.

Collab invites (kind=collab) must carry no agent scopes —
delegation arrives later via the D-milestone handshake.
Raise ValueError when kind=collab and scopes is non-empty.

Fixes jaylfc MUST-FIX on PR jaylfc#2045.
@hognek

hognek commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the MUST-FIX: mint() now raises ValueError when kind=collab and scopes is non-empty, with test test_mint_collab_rejects_scopes. All 38 invite_store tests pass. Bots clean on incremental diff.

@jaylfc
jaylfc merged commit 0badcf8 into jaylfc:dev Jul 19, 2026
9 checks passed
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