Skip to content

feat: add PENDING status for invitation-style principals - #1104

Merged
mstanbCO merged 3 commits into
mainfrom
mstanbCO/IGA-1212/pending-user-status
Aug 27, 2026
Merged

feat: add PENDING status for invitation-style principals#1104
mstanbCO merged 3 commits into
mainfrom
mstanbCO/IGA-1212/pending-user-status

Conversation

@mstanbCO

Copy link
Copy Markdown
Contributor

Problem

Connectors have no way to say "this principal is a pending invitation, not a usable account." Both status enums stop at DELETED = 3, so an invitation-style principal can only be expressed as STATUS_UNSPECIFIED — and even that requires the deprecated WithStatus option, because NewUserTrait force-defaults an unset status to ENABLED. Consumers are left inferring "pending" from an absent value, which is indistinguishable from a connector that simply never set a status.

Change

Additive enum value 4 on both status enums:

  • c1.connector.v2.Status.ResourceStatusRESOURCE_STATUS_PENDING = 4
  • c1.connector.v2.UserTrait.Status.StatusSTATUS_PENDING = 4

c1.storage.v3.StatusRecord.ResourceStatus is mirrored as well — that enum documents itself as an explicit mirror of the v2 one, and the pebble translation layer casts between them numerically, so it has to move in lockstep.

Trait ↔ resource status mirroring (GetStatus, syncUserTraitToResource, WithResourceStatus / WithDetailedStatus / WithStatus) already round-trips by numeric cast, so PENDING flows through unchanged with no new branches. The one enumerating switch, getUserStatus in the CSV exporter, gains a Pending case.

NewUserTrait's enabled-by-default behavior for an unset status is unchanged; an explicitly-set PENDING is not overwritten, and there's a test proving it.

Wire compatibility

Proto3 enums are open, so this is additive and existing values are untouched — nothing is renumbered or reused. The one caveat: the status fields carry (validate.rules).enum = {defined_only: true}, which is enforced against the generated _name map. A consumer running an older generated .pb.validate.go will reject value 4 until it regenerates. Consumers should upgrade before connectors start emitting PENDING.

Testing

go test -tags=baton_lambda_support ./... and golangci-lint run both clean. Protos regenerated with buf generate (no hand edits); buf lint and buf breaking --against origin/main pass.

Refs IGA-1212.

🤖 Generated with Claude Code

Add RESOURCE_STATUS_PENDING and STATUS_PENDING as value 4 on the
resource and user-trait status enums so connectors can express a
principal whose account creation was initiated but is not yet usable,
such as an unaccepted invitation. Mirror the value onto the v3
StatusRecord enum so pebble translation stays in lockstep.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 25, 2026

Copy link
Copy Markdown

IGA-1212

Comment thread proto/c1/storage/v3/records.proto
Comment thread proto/c1/connector/v2/resource.proto
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

General PR Review: feat: add PENDING status for invitation-style principals

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 2c05c0b993c0.
Review mode: incremental since 8a66dfbd
View review run

Review Summary

The new commit is test-only: TestStatusEnumMirrorsStayAligned now pins the AgentTrait_AgentStatus -> Status_ResourceStatus cast by name via expectedAgentMirror, plus a require.Len on AgentTrait_AgentStatus_name, so a future AGENT_STATUS_* = 4 fails with an instruction to review the cast rather than silently inheriting RESOURCE_STATUS_PENDING. That resolves the previous finding — existence-only checking stopped guarding anything the moment ResourceStatus outgrew AgentStatus, and a renumber is still caught because an unmapped number fails the expectedAgentMirror lookup. The full PR diff was re-scanned for security and correctness: the enum addition is additive at 4 across all three mirrors with no renumbering or field reuse, the checked-in .pb.go and rawDesc bytes match the .proto changes, defined_only validation reads the runtime _name map (pb/c1/connector/v2/resource.pb.validate.go:5125) so no .pb.validate.go regeneration is needed, and getUserStatus (cmd/baton/csv.go:245) is the only enumerating switch in the tree. No new issues found.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

…ents

The v2 ResourceStatus, v2 UserTrait status, and v3 StatusRecord status
enums are hand-mirrored and cast numerically; assert every shared value
agrees by name so drift fails CI instead of mistranslating stored data.
AgentTrait_AgentStatus is a numeric prefix of ResourceStatus, not a
mirror - reword the two comments that overstated it as identical and
assert the subset property.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +225 to +232
// AgentTrait_AgentStatus is cast numerically into Status_ResourceStatus.
// It is a prefix, not a mirror (READY maps to ENABLED); every AgentStatus
// number must exist in ResourceStatus so a new AgentStatus value cannot
// silently surface as an unrelated resource status.
for num, name := range v2.AgentTrait_AgentStatus_name {
_, ok := v2.Status_ResourceStatus_name[num]
require.Truef(t, ok, "AgentTrait_AgentStatus value %d (%s) has no Status_ResourceStatus counterpart", num, name)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Suggestion: this guard doesn't catch the failure mode its comment describes. AgentStatus currently stops at 3, and ResourceStatus now goes to 4 — so the next AgentStatus value added (4) will find a counterpart in Status_ResourceStatus_name and pass, while syncAgentTraitToResource's numeric cast silently renders it as RESOURCE_STATUS_PENDING. Existence-only checking is exactly what the added PENDING value made insufficient. Pinning the mapping by name, so a new AgentStatus value forces the table to be extended deliberately, would close it:

Suggested change
// AgentTrait_AgentStatus is cast numerically into Status_ResourceStatus.
// It is a prefix, not a mirror (READY maps to ENABLED); every AgentStatus
// number must exist in ResourceStatus so a new AgentStatus value cannot
// silently surface as an unrelated resource status.
for num, name := range v2.AgentTrait_AgentStatus_name {
_, ok := v2.Status_ResourceStatus_name[num]
require.Truef(t, ok, "AgentTrait_AgentStatus value %d (%s) has no Status_ResourceStatus counterpart", num, name)
}
// AgentTrait_AgentStatus is cast numerically into Status_ResourceStatus.
// It is a prefix, not a mirror (READY maps to ENABLED). Pin the mapping by
// name so a new AgentStatus value cannot silently inherit an unrelated
// ResourceStatus meaning — extend this table deliberately when adding one.
expectedAgentMirror := map[int32]string{
0: "RESOURCE_STATUS_UNSPECIFIED",
1: "RESOURCE_STATUS_ENABLED",
2: "RESOURCE_STATUS_DISABLED",
3: "RESOURCE_STATUS_DELETED",
}
require.Len(t, v2.AgentTrait_AgentStatus_name, len(expectedAgentMirror),
"new AgentTrait_AgentStatus value: confirm its numeric cast into Status_ResourceStatus is still meaningful, then extend expectedAgentMirror")
for num, name := range v2.AgentTrait_AgentStatus_name {
want, ok := expectedAgentMirror[num]
require.Truef(t, ok, "AgentTrait_AgentStatus value %d (%s) has no reviewed Status_ResourceStatus counterpart", num, name)
require.Equalf(t, want, v2.Status_ResourceStatus_name[num],
"AgentTrait_AgentStatus value %d (%s) casts to an unexpected Status_ResourceStatus", num, name)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — existence-only stopped guarding anything the moment ResourceStatus outgrew AgentStatus. Applied your suggestion as-is in 9de8e65: the mapping is now pinned by name via expectedAgentMirror, with a Len assertion so a new AgentStatus value fails with an instruction to review the cast and extend the table deliberately.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

Existence-only subset checking stopped guarding anything once
ResourceStatus grew past AgentStatus: a new AGENT_STATUS_* = 4 would
pass while casting to RESOURCE_STATUS_PENDING. Pin each value's
counterpart by name so extending AgentStatus forces a deliberate
table update.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

@mstanbCO
mstanbCO merged commit 70559f5 into main Aug 27, 2026
12 checks passed
@mstanbCO
mstanbCO deleted the mstanbCO/IGA-1212/pending-user-status branch August 27, 2026 21:54
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.

3 participants