org: invite an email address to an organization - #2561
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical token-handling findings remain unresolved in org join.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 2
Open (3)
What changed in this PR
Adds organization invitation management and token-based invitation acceptance to the CLI, alongside a refreshed Core API client.
Changes:
- Adds organization invite, list, revoke, and join workflows.
- Regenerates API models and adapts related CLI integrations.
- Adds compatibility, pagination, and token-safety tests.
| File | Summary |
|---|---|
internal/coreapi/UPSTREAM.md |
Documents API compatibility workarounds; command path needs correction. |
internal/coreapi/spec/normalize.go |
Preserves forward-compatible read-model values. |
internal/coreapi/repo_readiness_test.go |
Updates repository response-wrapper tests. |
internal/coreapi/oas_validators_gen.go |
Regenerated API validators. |
internal/coreapi/oas_security_gen.go |
Regenerated security mappings. |
internal/coreapi/oas_request_encoders_gen.go |
Regenerated request encoders. |
internal/coreapi/oas_parameters_gen.go |
Regenerated request parameters. |
internal/coreapi/oas_operations_gen.go |
Regenerated operation definitions. |
internal/coreapi/oas_interfaces_gen.go |
Adds regenerated response interfaces. |
internal/coreapi/oas_defaults_gen.go |
Regenerated request defaults. |
internal/coreapi/oas_cfg_gen.go |
Regenerated configuration. |
internal/coreapi/client_test.go |
Tests unknown enum compatibility. |
cmd/entire/cli/repo.go |
Handles wrapped repository responses. |
cmd/entire/cli/repo_readiness.go |
Converts created repositories for readiness polling. |
cmd/entire/cli/repo_protection.go |
Uses updated branch-rule models. |
cmd/entire/cli/repo_protection_test.go |
Updates branch-protection fixtures. |
cmd/entire/cli/repo_mirror.go |
Uses refreshed mirror models. |
cmd/entire/cli/repo_mirror_test.go |
Updates mirror assertions. |
cmd/entire/cli/repo_mirror_request_test.go |
Updates asynchronous mirror tests. |
cmd/entire/cli/repo_mirror_probe.go |
Handles refreshed mirror results. |
cmd/entire/cli/project.go |
Handles wrapped project responses. |
cmd/entire/cli/org.go |
Registers joining and adapts organization responses. |
cmd/entire/cli/org_join.go |
Accepts invitations; critical findings remain for JSON token leakage and redaction edge cases. |
cmd/entire/cli/org_join_test.go |
Tests token handling; wording cleanup is needed. |
cmd/entire/cli/org_invite.go |
Implements invitation management commands. |
cmd/entire/cli/org_invite_test.go |
Tests invitation workflows. |
cmd/entire/cli/grant.go |
Adds invitation commands and shared validation. |
cmd/entire/cli/grant_test.go |
Tests grant validation changes. |
cmd/entire/cli/corecmd_json_flag_test.go |
Verifies JSON flag coverage. |
Files not reviewed (7)
- internal/coreapi/oas_cfg_gen.go: Generated file
- internal/coreapi/oas_defaults_gen.go: Generated file
- internal/coreapi/oas_interfaces_gen.go: Generated file
- internal/coreapi/oas_operations_gen.go: Generated file
- internal/coreapi/oas_parameters_gen.go: Generated file
- internal/coreapi/oas_request_encoders_gen.go: Generated file
- internal/coreapi/oas_security_gen.go: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Contributor
Author
|
bugbot run |
khaong
force-pushed
the
alex/cor-1744-cli-invite-commands
branch
from
September 23, 2026 05:50
69b6a7e to
992a300
Compare
Contributor
Author
|
bugbot run |
Contributor
Author
|
bugbot run |
…tion Add the four verbs that let an org grant membership to someone the control plane cannot name yet. `grant add` needs an existing provider account; an invitation needs only an email address. entire org grant invite <org> <email> [--role owner|admin|member] [--json] entire org grant invites <org> [--status open|accepted|revoked|expired|all] entire org grant uninvite <org> <email|id> entire org join <token> The first three sit in the existing `grant` subtree, beside add/list/remove, and reuse its resolution, listing, revoke and --json plumbing. `join` hangs off `entire org` instead, because it acts on the caller's own account: the caller is the invitee, not a manager addressing someone else. `uninvite` accepts an email address as well as an invitation ULID, resolving it through the open listing, because the revoke route addresses an invitation by ULID and nobody reads a ULID out of an email. It searches only the open invitations, so re-revoking an accepted one is not mistaken for the request. Who may invite with which role stays the server's decision. The CLI checks only that --role spells a value the API declares, so one place decides that an admin may not mint owners, and a 403 reaches the user with the server's own words. The invitation token is a bearer credential, so no stream this code writes may carry it. The accept response holds no token, so success and --json are safe by construction. Two paths could reintroduce it, and both are closed and tested: a flag parse error, which quotes the argument cobra could not read, and a server problem detail that names what it rejected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74
AcceptedInvitation/AcceptInvitationOK (and the nested Membership) carry an ogen additionalProperties bag that round-trips any response property the schema doesn't declare, verbatim, into --json output. Blank it before rendering rather than trust the endpoint's contract never grows one. redactToken substituted a fixed "<redacted>" placeholder. The token schema only requires a non-empty string, so a caller can supply that exact placeholder text as their token; replacing a token with text equal to itself is a no-op and left it sitting in the message. Delete the token instead of substituting a lookalike. Also fixes a doc typo: internal/coreapi/UPSTREAM.md named the nonexistent `entire grant org invites`; the shipped verb is `entire org grant invites`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74
Invitation (and so CreateOrgInvitationCreated/CreateOrgInvitationOK) carries the same ogen additionalProperties bag blanked on AcceptedInvitation/AcceptInvitationOK in org_join.go. An invitation is the one other object in this PR with an accept token, so the same defense applies to the whole family rather than a new one: the create path blanked it on the single returned Invitation, and the list path blanks it on every item, since it loops. TestOrgInvite_JSONDropsUnmodeledResponseProperties and TestOrgInvites_JSONDropsUnmodeledResponsePropertiesOnEveryItem mirror TestOrgJoin_JSONDropsUnmodeledResponseProperties's shape; both were checked to fail against the pre-fix code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74
khaong
force-pushed
the
alex/cor-1744-cli-invite-commands
branch
from
September 24, 2026 05:26
44b961d to
2bfba08
Compare
Contributor
Author
|
bugbot run |
Contributor
Author
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 206c9f4. Configure here.
gtrrz-victor
added a commit
that referenced
this pull request
Sep 24, 2026
coreapi: land vendored Core spec refresh before #2561
Soph
previously approved these changes
Sep 24, 2026
The invitation verbs lived under the grant subtree as `org grant invite`, `org grant invites` and `org grant uninvite`. They now form their own subtree: `entire org invite <org> <email>` sends an invitation, with `invite list` and `invite revoke` managing sent ones. The grant subtree's extraCmds hook, added only for these verbs, is removed. Drop `entire org join`; accepting an invitation is not a CLI flow. The invite commands address the org by name, so their help and their unknown-org error no longer mention ULIDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M3C16S05RRS6XVCNB110S4HA
`entire org invite` becomes a plain group, and creating an invitation moves to `entire org invite send <org> --email <email>` beside `list` and `revoke`. With no verb on the group, an org named `list` or `revoke` no longer collides with a subcommand, so the --json test's group-as-verb walk is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M3C90EN60B0VC3FVAB81C6SR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M3C9S5WD81VYSG834QWHD43M
gtrrz-victor
force-pushed
the
alex/cor-1744-cli-invite-commands
branch
from
September 25, 2026 13:10
819ad24 to
f6e7953
Compare
`invite revoke` accepts an invitation's ID as well as its email, but the table hid it, so it was only reachable through --json. The list table now leads with an ID column, and send's success line ends with the ID. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M3CC5YPSMKWQWAH435FH2N4A
`entire org invite revoke <org> --email <email>` matches `invite send`: the org is the one positional and the invitee is a required flag. The invitation ID is no longer accepted; the CLI still resolves the email to the open invitation's ID, which the revoke route needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M3CGMWY935HXKGKECZ7KTRH9
Soph
approved these changes
Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


https://entire.io/gh/entireio/cli/trails/1401
Why
COR-1744. An org can only grant membership to someone the control plane can
already name:
entire org grant addtakes a provider handle, so it cannotreach a colleague who has never signed in. Invitations close that gap — an
email address is enough — and this adds the CLI commands for managing them.
What
inviteis its own subtree underentire org, besidegrant:sendcreates an invitation, and
listandrevokemanage the ones already sent.The org is addressed by name; an unknown
name reports
no org named "x" (run `entire org list` to see org names).The commands reuse the org resolution, paging, revoke and
--jsonplumbingthe grant subtree already has, and
validateRoleis generalized tovalidateChoiceso--roleand--statusshare one check.The invitee accepts through the link in the invitation email; there is no
CLI accept command.
Notes on decisions a reviewer may want to check:
sendandrevokeboth take the invitee as--email. The org is theone positional on every invite verb. The revoke route addresses an
invitation by ULID, so
revokeresolves the address through the listing.It searches only the open invitations, so re-revoking an accepted one is
never mistaken for the request.
DELETE /orgs/{orgId}/invitations/{id}; there is no org-less invitationlookup, so
revokecannot drop<org>without a server change.invite listleads with anIDcolumn andinvite sendprints the new invitation's ID, as the orgcommands do for org IDs.
server's decision, so a 403 reaches the user in the server's own words. The
CLI checks only that
--rolespells a value the API declares.revokehas no--json. It revokes with a 204 and has no object torender.
TestControlPlaneJSONFlag_OnlyOnHonoringCommandspins the rule thatthe flag exists only where it is honored;
grant removeis the same shape.AdditionalPropsis blanked on everyInvitation. An invitation is anobject with an accept token. Its modeled fields carry none, but ogen
round-trips any undeclared response property verbatim into
--json, so thecreate path and every item of the list path clear the bag.
No jurisdiction handling is added.
crossjuris.Transportalready sits underevery
coreapiconstructor and follows a 421 once, so these verbs inherit it.The vendored spec refresh these commands need has landed on
mainseparately, so this diff is handwritten code only: 258 production lines and
378 test lines.
Verification
mise run check— pass (format, lint, unit, integration, canary)mise run lint— 0 issuesTests covering the invite behavior:
AdditionalPropsblanking on create / listTestOrgInviteSend_JSONDropsUnmodeledResponseProperties,TestOrgInviteList_JSONDropsUnmodeledResponsePropertiesOnEveryItemTestOrgInviteSend_ResendReportsTheStoredRole--rolevalueTestOrgInviteSend_SendsTheDefaultRoleWhenFlagOmittedTestOrgInviteList_ListsAndFiltersByStatus,TestOrgInviteSend_CreatesAndReportsTheRole,TestOrgInviteSend_ResendReportsTheStoredRole--emailis required onsendandrevokeTestOrgInvite_SendAndRevokeRequireEmailTestOrgInviteSend_ForbiddenRoleSurfacesTheServerMessage--statusvalidationTestOrgInviteList_RejectsAnUnknownStatusTestOrgInviteRevoke_ResolvesAnEmailThroughTheOpenListingTestOrgInvite_UnknownOrgNameHintsAtNamesOnlyInvitation/Membershipenum looseningTestListOrgInvitations_UnknownEnumValuesPassThrough,TestListOrgMembers_UnknownEnumValuesPassThroughFollow-ups
entire grant org invite. They ship asentire org invite …, alongsideentire org grant ….entire org invite listwalks every page. The grant listings do the same,so the
--all/--limit/--page-sizetreatment the repo listings have isworth giving them all at once rather than this one verb.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PLWcdMjoLpAaiATYzY3v74
Note
Medium Risk
Touches org membership and bearer invitation tokens; mitigations are explicit but credential handling and authorization still depend on correct CLI and API behavior.
Overview
Adds org invitation support to the CLI so membership can be granted by email before the invitee has a provider account, plus an invitee path to accept.
Under
entire org grant, new verbsinvite,invites, anduninvitecall the control-plane invitation APIs (create/resend, paginated list with--status, revoke by ULID or open invitation email). They hang off the shared grant subtree via a newextraCmdshook ongrantTarget, reuse org ref resolution and listing helpers, andvalidateChoice(renamed fromvalidateRole) for--roleand--status.uninvitehas no--json;invite,invites, andorg joindo, per the existing--jsonregistry test.entire org join <token>accepts an invitation for the logged-in user. Invitation tokens are treated as secrets: errors areredactToken-scrubbed, dash-prefixed tokens get a custom flag-parse message, andAdditionalPropsare cleared on invitation/join responses so accept tokens cannot leak through--json.Broad integration tests cover API wiring, resend messaging, server-side role policy (403), email→ULID uninvite, and token non-disclosure.
UPSTREAM.mdnotes invitation list fields in the read-model enum loosening story.Reviewed by Cursor Bugbot for commit 206c9f4. Configure here.