Skip to content

feat: add org support to skill ownership transfers#1603

Open
TommYDeeee wants to merge 12 commits intoopenclaw:mainfrom
TommYDeeee:feat/skill-transfer-org-support
Open

feat: add org support to skill ownership transfers#1603
TommYDeeee wants to merge 12 commits intoopenclaw:mainfrom
TommYDeeee:feat/skill-transfer-org-support

Conversation

@TommYDeeee
Copy link
Copy Markdown

@TommYDeeee TommYDeeee commented Apr 9, 2026

Summary

Extends the existing skill transfer system to support org-targeted transfers (user→org, org→user, org→org), in addition to the existing user→user flow.

This is PR 1 of 2 — it lays the foundation with shared transfer helpers and org-aware skill transfers. #1604 (package ownership transfers) builds on top of this and is chained to this branch.

The split keeps each PR focused and independently reviewable: this PR touches only the skill transfer path and shared infrastructure, while PR 2 adds the new package transfer system as a clean incremental layer.

Resolves part of #1431

Motivation

Skill transfers were previously user-to-user only. Users who wanted to move a skill to an organization they admin had no way to do so after publishing. This PR enables publisher-based transfer flows so org admins can transfer skills to and from organizations.

What's new

Shared transfer helpers (convex/lib/transfers.ts)

  • Extracted common logic used by both skill and (upcoming) package transfers: expiry checking, handle normalization, ownership validation, accept permission checks
  • validateTransferOwnership — supports both personal ownership (ownerUserId match) and org admin/owner role checks via publisherMembers
  • validateTransferAcceptPermission — validates accept permission for personal and org targets, rejects transfers to inactive/deleted publishers
  • 23 unit tests covering all helpers

Org-targeted skill transfers

  • User → Org: Owner transfers their skill to an org they admin
  • Org → User: Org admin transfers an org-owned skill to another user
  • Org → Org: Admin of source org transfers to a target org (recipient must be admin of target)
  • Permission checks: admin or owner role required on source/target publisher
  • Ownership re-validation at accept time uses fromPublisherId for org transfers (not fromUserId), correctly handling the case where the requesting admin differs from ownerUserId
  • Any org admin can accept, reject, or cancel — not just the original toUserId or fromUserId
  • Self-recipient allowed when transferring to an org (enables single-admin org transfers)
  • Explicit publisherId override on accept is validated against org membership
  • Skill slug aliases updated along with the skill on transfer (existing behavior preserved)

Schema changes

  • Added optional fromPublisherId and toPublisherId fields to skillOwnershipTransfers
  • Made toUserId optional to support org-targeted transfers

API updates

  • POST /api/v1/skills/{slug}/transfer — optional toPublisherHandle in body for org targeting
  • POST /api/v1/skills/{slug}/transfer/accept — optional publisherHandle in body to assign to org on accept
  • HTTP handlers resolve publisher handles to IDs server-side
  • Transfer listing adds type: "skill" discriminator field (preparation for unified listing in PR 2)
  • Org admin fallback lookup in HTTP handlers — if toUserId/fromUserId lookup misses, falls back to unfiltered pending transfer lookup so any org admin can act

CLI updates

  • --type skill|package flag — explicit type selection with auto-detection fallback (probes both APIs, errors on ambiguity)
  • --publisher @org-handle flag — target an org on request, or assign to org on accept
  • Auto-detection correctly distinguishes skills from packages by parsing response body (the packages endpoint falls back to returning skills, so a simple 2xx check is insufficient)
  • Updated transfer list output with TYPE column
  • Improved error message for private items suggesting --type flag

Backwards compatibility

  • Existing user→user skill transfers work unchanged
  • All new fields are optional — no migration needed
  • Existing tests pass unmodified alongside new org transfer tests

Files changed (13 files, +858 / -118)

File Change
convex/lib/transfers.ts New — shared helpers (expiry, validation, permissions)
convex/lib/transfers.test.ts New — 23 tests for shared helpers
convex/schema.ts Extended skillOwnershipTransfers with publisher fields
convex/skillTransfers.ts Refactored to use shared helpers, added org support
convex/skillTransfers.test.ts Added org admin transfer test
convex/httpApiV1/skillsV1.ts Publisher handle resolution, org admin fallback lookup
convex/httpApiV1/transfersV1.ts Added type: "skill" discriminator to listing results
packages/clawhub/src/cli/commands/transfer.ts --type, --publisher, auto-detection
packages/clawhub/src/cli.ts Updated command descriptions and options
packages/clawhub/src/schema/schemas.ts Response schema updates
docs/http-api.md Skill transfer endpoint docs with org params
docs/cli.md Updated transfer CLI command reference
docs/orgs.md Updated transfer model section

Test plan

  • 23 tests pass (shared helpers + skill transfers)
  • TypeScript compiles with zero new errors
  • Backwards compatible — existing user→user skill transfers work unchanged
  • Org transfer scenarios verified: user→org, org→user, org→org, self-recipient, multi-admin accept/reject/cancel, expired transfers, ownership-changed detection

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 9, 2026

@TommYDeeee is attempting to deploy a commit to the 0xBuns Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 9, 2026

Greptile Summary

This PR extends skill ownership transfers to support org-targeted flows (user→org, org→user, org→org) by extracting shared helpers into convex/lib/transfers.ts and updating the schema, internal mutations, HTTP handlers, and CLI. The backwards-compatible schema changes and shared helper extraction are well done and thoroughly tested.

  • P1 regression: listOutgoingInternal no longer filters pending transfers where the personal recipient has been deleted/deactivated — they now surface with toUser: undefined ("unknown user" in CLI) instead of being skipped. Since validateTransferAcceptPermission bars acceptance for a deleted user, these entries are permanently stuck until expiry.
  • P2 gap: listIncomingInternal queries only by toUserId, so org admins who are not the originally designated toUserId cannot discover incoming org-targeted transfers via transfer list — they must know the skill slug out-of-band to act.

Confidence Score: 4/5

Safe to merge with one behavioral regression in the outgoing transfer list to address first.

One clear P1 regression: listOutgoingInternal now includes pending user→user transfers to deleted users (showing as "(unknown user)") instead of filtering them, which was the previous correct behavior. The org-admin discoverability gap in listIncomingInternal is a real UX limitation but appears intentional per the PR description. The rest of the implementation — shared helpers, permission validation, HTTP fallback, CLI flags, schema — is correct and well-tested.

convex/skillTransfers.ts — specifically the listOutgoingInternal loop and the ownerUserId stamping in acceptTransferInternal.

Vulnerabilities

No security concerns identified. The two-query HTTP fallback (specific-user lookup → any-pending) is safe because the downstream mutations re-validate org membership via validateTransferOwnership/validateTransferAcceptPermission before making any changes. Unauthorized actors are blocked at the mutation layer with a 403. Publisher handle resolution happens server-side, preventing client-supplied IDs from bypassing validation.

Comments Outside Diff (2)

  1. convex/skillTransfers.ts, line 409-437 (link)

    P1 Behavioral regression: transfers to deleted recipients no longer filtered from outgoing list

    The new code includes pending user→user transfers where the recipient has been deleted/deactivated (toUser will be undefined), while the old code filtered them out with continue. Since validateTransferAcceptPermission requires actorUserId === toUserId for personal targets and the user is gone, these transfers can never be accepted — they'll just sit until expiry showing as "(unknown user)" in the CLI. The old filter was the correct behavior for this case.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: convex/skillTransfers.ts
    Line: 409-437
    
    Comment:
    **Behavioral regression: transfers to deleted recipients no longer filtered from outgoing list**
    
    The new code includes pending user→user transfers where the recipient has been deleted/deactivated (`toUser` will be `undefined`), while the old code filtered them out with `continue`. Since `validateTransferAcceptPermission` requires `actorUserId === toUserId` for personal targets and the user is gone, these transfers can never be accepted — they'll just sit until expiry showing as `"(unknown user)"` in the CLI. The old filter was the correct behavior for this case.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. convex/skillTransfers.ts, line 338-346 (link)

    P2 Org-targeted incoming transfers invisible to non-designated org admins

    listIncomingInternal queries only by toUserId, so only the user specified in toUserHandle at transfer-request time sees the transfer in their incoming list. Other admins of the target org (who can accept/reject per the HTTP fallback) have no way to discover pending org-targeted transfers through the list API — they'd need to know the exact skill slug out-of-band.

    The PR description says "any org admin can accept, reject, or cancel", but the list is the main discovery surface. Consider either adding a separate listIncomingForPublisher query (or augmenting this one with a publisherId argument) so that all org admins can discover transfers addressed to their org.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: convex/skillTransfers.ts
    Line: 338-346
    
    Comment:
    **Org-targeted incoming transfers invisible to non-designated org admins**
    
    `listIncomingInternal` queries only by `toUserId`, so only the user specified in `toUserHandle` at transfer-request time sees the transfer in their incoming list. Other admins of the target org (who can accept/reject per the HTTP fallback) have no way to discover pending org-targeted transfers through the list API — they'd need to know the exact skill slug out-of-band.
    
    The PR description says "any org admin can accept, reject, or cancel", but the list is the main discovery surface. Consider either adding a separate `listIncomingForPublisher` query (or augmenting this one with a `publisherId` argument) so that all org admins can discover transfers addressed to their org.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: convex/skillTransfers.ts
Line: 409-437

Comment:
**Behavioral regression: transfers to deleted recipients no longer filtered from outgoing list**

The new code includes pending user→user transfers where the recipient has been deleted/deactivated (`toUser` will be `undefined`), while the old code filtered them out with `continue`. Since `validateTransferAcceptPermission` requires `actorUserId === toUserId` for personal targets and the user is gone, these transfers can never be accepted — they'll just sit until expiry showing as `"(unknown user)"` in the CLI. The old filter was the correct behavior for this case.

```suggestion
      let toUser:
        | { _id: Id<"users">; handle: string | null; displayName: string | null }
        | undefined;
      if (transfer.toUserId) {
        const tu = await ctx.db.get(transfer.toUserId);
        if (tu && !tu.deletedAt && !tu.deactivatedAt) {
          toUser = {
            _id: tu._id,
            handle: tu.handle ?? null,
            displayName: tu.displayName ?? null,
          };
        } else if (!transfer.toPublisherId) {
          // Personal transfer to a now-deleted user — skip (can never be accepted)
          continue;
        }
      }
```

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

---

This is a comment left during a code review.
Path: convex/skillTransfers.ts
Line: 338-346

Comment:
**Org-targeted incoming transfers invisible to non-designated org admins**

`listIncomingInternal` queries only by `toUserId`, so only the user specified in `toUserHandle` at transfer-request time sees the transfer in their incoming list. Other admins of the target org (who can accept/reject per the HTTP fallback) have no way to discover pending org-targeted transfers through the list API — they'd need to know the exact skill slug out-of-band.

The PR description says "any org admin can accept, reject, or cancel", but the list is the main discovery surface. Consider either adding a separate `listIncomingForPublisher` query (or augmenting this one with a `publisherId` argument) so that all org admins can discover transfers addressed to their org.

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

---

This is a comment left during a code review.
Path: convex/skillTransfers.ts
Line: 223-244

Comment:
**`ownerUserId` set to the accepting admin, not the designated `toUserId`**

For org-targeted transfers (where `toPublisherId` is set), any org admin can accept. When a different admin accepts (e.g., Alice requests transfer to org-B naming Bob, but Carol accepts), `ownerUserId` is stamped with `args.actorUserId` (Carol), not `transfer.toUserId` (Bob). The `toUserId` in the transfer record is also overwritten with Carol on line 244.

If the intent is that `toUserHandle` designates the individual owner-within-org after the transfer, this is incorrect. If "whoever clicks accept becomes the individual owner" is the intended semantics, this is fine — but it's worth an explicit comment, since it means the nominated `toUserHandle` has no ownership effect for org-targeted transfers when a different admin accepts.

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

---

This is a comment left during a code review.
Path: convex/httpApiV1/transfersV1.ts
Line: 19-26

Comment:
**Redundant `type: "skill"` remapping**

`listIncomingInternal` and `listOutgoingInternal` already return objects with `type: "skill" as const`. The `.map((t) => ({ ...t, type: "skill" as const }))` here is a no-op spread — it replaces the field with the identical value. The sort on `requestedAt` is fine, but the map can be dropped.

```suggestion
  const transfers = skillTransfers
    .sort((a, b) => (b.requestedAt ?? 0) - (a.requestedAt ?? 0));
```

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

Reviews (1): Last reviewed commit: "feat: add org (publisher) support to ski..." | Re-trigger Greptile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4d7528980

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@momothemage
Copy link
Copy Markdown
Member

Thanks for the contribution! Just a quick note: there's a failing test case that needs to be fixed. That would be the last step before merge.

@momothemage momothemage self-assigned this Apr 10, 2026
@momothemage
Copy link
Copy Markdown
Member

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 475809be79

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@momothemage
Copy link
Copy Markdown
Member

Thanks for the quick response! Really appreciate it. @TommYDeeee There are 2 more new review comments from codex, please take a look.

@TommYDeeee
Copy link
Copy Markdown
Author

Thanks for the quick response! Really appreciate it. @TommYDeeee There are 2 more new review comments from codex, please take a look.

Thanks! Both of them were minor but valid, addressed in latest commit

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7246c855d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@momothemage
Copy link
Copy Markdown
Member

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d829719c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1050 to +1052
const parsed = await parseJsonPayload(request, headers);
if (parsed.ok) {
const publisherHandleRaw =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject malformed JSON in transfer accept requests

Return a 400 when the accept request body is invalid JSON instead of silently continuing. In this branch, parse failures are ignored (if (parsed.ok)), so a malformed payload intended to set publisherHandle still proceeds with acceptance and falls back to default ownership assignment, which can complete the transfer with the wrong destination publisher.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I would push this back. It is intentional behavior. The publisherHandle field is optional — an empty or missing body means "use the default publisher." Rejecting parse failures would break clients that send Content-Type: application/json with no body (the exact scenario flagged in the earlier review comment). The current pattern is standard for optional request bodies: parse if possible, default if not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The pushback is reasonable. Parse failures are not silently ignored — the if (parsed && !parsed.ok) return parsed.response guard already returns an error response when Content-Type: application/json is declared but the body is malformed JSON. The "silent continue" path only applies when no Content-Type: application/json header is present, in which case parsed is null and the body is intentionally skipped. That's the correct behavior for an optional field: don't attempt JSON parsing if the client didn't declare a JSON body.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the feedback, just to be sure is there anything else needed?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Made a little adjustment on listIncomingInternal
Before: only queried transfers where toUserId === userId.
After: also queries transfers directed at orgs where the user is an owner or admin, then merges + deduplicates the two result sets. Also adds toPublisherId to the returned shape so the caller knows whether the transfer is org-targeted.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for adding this, I like it. Good call 👍

TommYDeeee and others added 9 commits April 10, 2026 17:07
Add fromPublisherId/toPublisherId fields to skillOwnershipTransfers schema
and make toUserId optional to support org-to-org, user-to-org, and
org-to-user transfer flows.

- Extract shared transfer helpers into convex/lib/transfers.ts
- Update skillTransfers.ts with org-aware request/accept/reject/cancel
- Add publisher handle resolution and org admin fallback in skillsV1.ts
- Add type:"skill" field to transfer listing responses in transfersV1.ts
- Update CLI with --type and --publisher flags for transfer commands
- Update response schemas, docs (http-api, cli, orgs)
- Add clarifying comment that ownerUserId reflects the accepting admin,
  not the original toUserId, for org-targeted transfers
- Remove redundant .map() type remapping in transfer listing — the
  query already returns type: "skill"
- Fix double JSON encoding in CLI transfer accept body
Remove package auto-detection, --type flag, and package-specific
response schema fields. PR 1 only adds skill org support — package
transfer CLI support belongs in PR 2 alongside the backend endpoints.
…JSON

The existing test expected body as a JSON string, but apiRequest
already calls JSON.stringify internally — passing a pre-stringified
body causes double-encoding. Updated test to verify the body object
directly, matching the corrected CLI behavior.
…n cancel

- Accept handler no longer rejects empty JSON bodies. If parsing fails
  (empty body, no content-type), publisherId stays undefined and the
  transfer proceeds with the default publisher. Fixes clients that
  send Content-Type: application/json with no body.
- Cancel always validates org membership when fromPublisherId is set,
  even for the original requester. Prevents cancelled-after-removal
  where a user who lost org admin role could still cancel.
requestTransferInternal now checks that toPublisherId is not
deleted/deactivated before persisting the transfer. Prevents
creating dead transfers that can never be accepted.
When the sender specified a target publisher (toPublisherId), the
acceptor's publisherId arg is now ignored. The publisher override
only applies to user-targeted transfers where the recipient chooses
where to assign ownership.
@momothemage momothemage force-pushed the feat/skill-transfer-org-support branch from c112eb9 to edb702b Compare April 10, 2026 09:43
@momothemage
Copy link
Copy Markdown
Member

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: edb702bb57

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +41 to +45
if (!params.ownerPublisherId) {
// Personally owned — actor must be the owner
if (params.actorUserId !== params.ownerUserId) {
throw new Error("Forbidden");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate personal transfers by owner user, not publisher membership

validateTransferOwnership only uses ownerUserId when ownerPublisherId is absent, but skills are commonly stored with a personal ownerPublisherId, so this branch treats personal ownership like org ownership. Because publishers.addMember allows adding admins/owners without a publisher-kind guard, a collaborator on a personal publisher can now request/cancel transfers for skills they do not own, which is a privilege escalation compared to the previous skill.ownerUserId === actorUserId check. The ownership check should key off publisher kind (or still require owner user for user-kind publishers), not just presence of ownerPublisherId.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Valid observation. We replaced the strict ownerUserId === actorUserId check with membership-based validation, which doesn't distinguish between personal publishers (kind: "user") and org publishers (kind: "org"). In theory, if someone was added as admin to a personal publisher via addMember (which has no kind guard), they could request transfers for skills they don't own. In practice, personal publishers are auto-created with only the owner as member and there's no UI flow to add members to them. Happy to add a publisher kind check here if you'd prefer, it's a small change. However, this could be addressed at the addMember level as a broader and cleaner fix separately.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@TommYDeeee This comment sounds reasonable, lets see if there is maybe a privilege misescalation

Copy link
Copy Markdown
Author

@TommYDeeee TommYDeeee Apr 10, 2026

Choose a reason for hiding this comment

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

Like if I understand it correctly. The possible attack vector is:

  • someone would need to signup and create a personal publisher
  • the same person as an admin would need to add a user (via API as there is no UI for that) and give them admin privileges
  • only after this the admin/owner person can do the transfer of a plugin.

But that would be self-sabotage from the very beginning, it can be easily addressed on a level of the API for transfers here or at the addMember level, but the use-case is very strange neverthless

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In this case the owner didn't intend to grant transfer authority. The addMember call is the entry point, but the impact lands in the transfer flow. I think it would be better to add a check in validateTransferOwnership to avoid this threat.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Understood. Fixed in 9964342. validateTransferOwnership now checks publisher.kind — personal publishers (kind: "user") require direct ownerUserId match (same security level as before this PR), org publishers (kind: "org") use membership-based authorization.

Added also two tests covering the personal publisher path: owner can transfer their own skill, admin member on personal publisher is rejected.

Personal publishers (kind: "user") now require direct ownerUserId
match, same as the original pre-PR check. Only org publishers
(kind: "org") use membership-based authorization. Prevents a
theoretical privilege escalation where a member added to a personal
publisher could request transfers for skills they don't own.
@TommYDeeee TommYDeeee force-pushed the feat/skill-transfer-org-support branch from da680f5 to 9964342 Compare April 10, 2026 11:49
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 996434288d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

validateTransferAcceptPermission now checks publisher.kind and
rejects personal publishers (kind: "user") as transfer targets,
consistent with the validateTransferOwnership fix. Prevents
accepting transfers into a personal publisher via membership.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25a09399f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

requestTransferInternal now rejects toPublisherId with kind: "user",
preventing creation of dead transfers that can never be accepted.
@TommYDeeee
Copy link
Copy Markdown
Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bff5a2595b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@TommYDeeee
Copy link
Copy Markdown
Author

@momothemage Is this codex configured to always find something ahah :D like I can't beat it. I would say right now everything we have discussed or was pointed out is resolved.

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