Skip to content

Conversation

@aronchick
Copy link

@aronchick aronchick commented Feb 7, 2026

Summary

Implements Phase 1 of the ownership architecture improvements: skill ownership transfer.

What This Adds

Schema

  • New skillOwnershipTransfers table tracking transfer requests
  • Status: pending → accepted/rejected/cancelled/expired
  • 7-day expiry on transfer requests

Backend (convex/skillTransfers.ts)

Mutation Description
requestTransfer Owner requests transfer to another user
acceptTransfer Recipient accepts, becomes new owner
rejectTransfer Recipient declines
cancelTransfer Owner cancels pending request
Query Description
listIncoming Pending transfers to current user
listOutgoing Pending transfers from current user
countIncoming Count for notification badge

API Routes

POST /api/v1/skills/{slug}/transfer         - Request transfer
POST /api/v1/skills/{slug}/transfer/accept  - Accept transfer
POST /api/v1/skills/{slug}/transfer/reject  - Reject transfer
POST /api/v1/skills/{slug}/transfer/cancel  - Cancel transfer
GET  /api/v1/transfers/incoming             - List incoming
GET  /api/v1/transfers/outgoing             - List outgoing

CLI

clawhub transfer request cool-skill @newowner --message "Taking over!"
clawhub transfer list                    # Show incoming requests
clawhub transfer list --outgoing         # Show outgoing requests
clawhub transfer accept cool-skill       # Accept incoming transfer
clawhub transfer reject cool-skill       # Reject incoming transfer
clawhub transfer cancel cool-skill       # Cancel outgoing transfer

Audit Trail

All transfer actions are logged to auditLogs table:

  • skill.transfer.request
  • skill.transfer.accept
  • skill.transfer.reject

What's NOT Included (Future Phases)

  • Web UI for transfers (Phase 1.5)
  • Collaborators/maintainers (Phase 2)
  • Fallback owners / orphan handling (Phase 3)
  • Organizations (Phase 4)

Testing

# Request transfer
clawhub transfer request my-skill @friend

# Recipient accepts
clawhub transfer accept my-skill

# Verify new owner
clawhub inspect my-skill

Closes #167 (Phase 1)

Greptile Overview

Greptile Summary

This PR adds Phase 1 of skill ownership transfers.

  • Data model: introduces a new skillOwnershipTransfers table (pending → accepted/rejected/cancelled/expired) with timestamps and expiry.
  • Backend: adds Convex mutations/queries to request/accept/reject/cancel transfers, plus helper internal queries for the HTTP layer.
  • HTTP API: extends the v1 skills POST router with /skills/{slug}/transfer[/accept|reject|cancel] and adds a new GET router under /api/v1/transfers/{incoming|outgoing}.
  • CLI: adds a clawhub transfer command group to request/list/accept/reject/cancel transfers via the new endpoints.

Confidence Score: 3/5

  • This PR is close to mergeable but has a correctness issue in transfer acceptance that can overwrite ownership in edge cases.
  • Core flow is implemented end-to-end (schema, Convex functions, HTTP routes, CLI), but acceptTransfer does not revalidate the skill’s current owner or deletion state at acceptance time, which can lead to incorrect ownership changes if the skill changed after the request. Minor CLI output robustness issue also present.
  • convex/skillTransfers.ts, packages/clawdhub/src/cli/commands/transfer.ts

This implements Phase 1 of the ownership architecture improvements:

Schema:
- Add skillOwnershipTransfers table with indexes

Backend (convex/skillTransfers.ts):
- requestTransfer: Owner can request transfer to another user
- acceptTransfer: Recipient accepts and becomes new owner
- rejectTransfer: Recipient declines
- cancelTransfer: Owner cancels pending request
- listIncoming/listOutgoing: Query pending transfers
- countIncoming: For notification badges
- Transfer expires after 7 days

API Routes:
- POST /api/v1/skills/{slug}/transfer - Request transfer
- POST /api/v1/skills/{slug}/transfer/accept - Accept
- POST /api/v1/skills/{slug}/transfer/reject - Reject
- POST /api/v1/skills/{slug}/transfer/cancel - Cancel
- GET /api/v1/transfers/incoming - List incoming requests
- GET /api/v1/transfers/outgoing - List outgoing requests

CLI:
- clawhub transfer request <slug> <handle> [--message]
- clawhub transfer list [--outgoing]
- clawhub transfer accept <slug>
- clawhub transfer reject <slug>
- clawhub transfer cancel <slug>

Audit logging included for all transfer actions.

Closes openclaw#167
@vercel
Copy link
Contributor

vercel bot commented Feb 7, 2026

@aronchick is attempting to deploy a commit to the Amantus Machina Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +82 to +84
export const acceptTransfer = mutation({
args: {
transferId: v.id('skillOwnershipTransfers'),
Copy link

Choose a reason for hiding this comment

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

Ownership/state not revalidated
acceptTransfer updates skills.ownerUserId based on a previously-created transfer without re-checking the current skill state. If the skill became soft-deleted after the request, or if skill.ownerUserId no longer equals transfer.fromUserId (e.g. ownership changed via another action), accepting will still overwrite the owner.

Consider validating !skill.softDeletedAt and skill.ownerUserId === transfer.fromUserId immediately before patching, and failing otherwise to avoid transferring deleted skills or clobbering a newer owner.

Prompt To Fix With AI
This is a comment left during a code review.
Path: convex/skillTransfers.ts
Line: 82:84

Comment:
**Ownership/state not revalidated**
`acceptTransfer` updates `skills.ownerUserId` based on a previously-created transfer without re-checking the current skill state. If the skill became soft-deleted after the request, or if `skill.ownerUserId` no longer equals `transfer.fromUserId` (e.g. ownership changed via another action), accepting will still overwrite the owner.

Consider validating `!skill.softDeletedAt` and `skill.ownerUserId === transfer.fromUserId` immediately before patching, and failing otherwise to avoid transferring deleted skills or clobbering a newer owner.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +98 to +101
console.log(options.outgoing ? 'Outgoing transfers:' : 'Incoming transfers:')
for (const t of result.transfers) {
const other = options.outgoing ? t.toUser?.handle : t.fromUser?.handle
const expiresIn = Math.ceil((t.expiresAt - Date.now()) / (24 * 60 * 60 * 1000))
Copy link

Choose a reason for hiding this comment

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

May print @undefined
In transfer list, other is derived from t.toUser?.handle / t.fromUser?.handle, then interpolated as @${other}. If the API response ever omits those nested objects (your local type already allows it), the CLI will output @undefined.

Either make the API contract require fromUser for incoming and toUser for outgoing (and validate it), or handle the missing case here (e.g. print (unknown) and avoid always prefixing @).

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/clawdhub/src/cli/commands/transfer.ts
Line: 98:101

Comment:
**May print `@undefined`**
In `transfer list`, `other` is derived from `t.toUser?.handle` / `t.fromUser?.handle`, then interpolated as `@${other}`. If the API response ever omits those nested objects (your local type already allows it), the CLI will output `@undefined`.

Either make the API contract require `fromUser` for incoming and `toUser` for outgoing (and validate it), or handle the missing case here (e.g. print `(unknown)` and avoid always prefixing `@`).

How can I resolve this? If you propose a fix, please make it concise.

@gthu-bmc
Copy link

This is a really good feature.

Small question, the old namespace URL (clawhub.ai/old-owner/skill-slug) will not resolve after transfer right?

It might be worth keeping the old URL and redirecting it (301) to the new owner URL to avoid breaking existing links or docs. Has it been considered here?

@aronchick
Copy link
Author

Terrific idea, should I do that or do maintainers want to pick that up?

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.

RFC: Skill ownership, transfer, and orphan handling

2 participants