Skip to content

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

Open
mstanbCO wants to merge 2 commits into
mainfrom
mstanbCO/IGA-1212/pending-user-status
Open

feat: add PENDING status for invitation-style principals#1104
mstanbCO wants to merge 2 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

RESOURCE_STATUS_ENABLED = 1;
RESOURCE_STATUS_DISABLED = 2;
RESOURCE_STATUS_DELETED = 3;
RESOURCE_STATUS_PENDING = 4;

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 (medium confidence): the v3 mirror is maintained by hand — the comment above StatusRecord says "changes to v2 require an explicit mirror update here (no automatic propagation)" — and translate_v2.go casts numerically between v2.Status_ResourceStatus and v3.StatusRecord_ResourceStatus. The new test only proves PENDING specifically round-trips; the next enum value added to one side and not the other silently mistranslates into durable c1z data. Consider a value-agnostic invariant test that asserts v2.Status_ResourceStatus_name, v2.UserTrait_Status_Status_name, and v3.StatusRecord_ResourceStatus_name agree on every shared number, so drift fails CI instead of shipping.

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.

Addressed in 8a66dfb: added TestStatusEnumMirrorsStayAligned (pkg/dotc1z/engine/pebble/translate_v2_test.go), which asserts all three name maps agree on every value — set-equality between v2 Status.ResourceStatus, v3 StatusRecord.ResourceStatus, and (prefix-normalized) UserTrait.Status — so the next one-sided enum addition fails CI rather than mistranslating stored data.

RESOURCE_STATUS_DELETED = 3;
// Account creation was initiated but the account is not yet usable, such
// as an invitation that has not been accepted.
RESOURCE_STATUS_PENDING = 4;

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 (high confidence): ResourceStatus and AgentTrait.AgentStatus are no longer value-identical after this — AgentStatus stops at AGENT_STATUS_DELETED = 3. Two comments now overstate the invariant: pkg/types/resource/resource.go:174-175 and pkg/types/resource/resource_attrs.go:90-91 both say "AgentTrait_AgentStatus and Status_ResourceStatus enum values are identical" right above a numeric cast. The cast direction used is still safe, but if anyone later adds AGENT_STATUS_* = 4 it will silently surface as RESOURCE_STATUS_PENDING. Worth reworking those comments to state the actual contract (AgentStatus is a prefix of ResourceStatus; new AgentStatus values must not collide) and adding a note here.

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.

Addressed in 8a66dfb: reworded both comments (resource.go, resource_attrs.go) to state the actual contract — AgentTrait_AgentStatus is a numeric prefix of Status_ResourceStatus, and new AgentStatus values must not reuse ResourceStatus numbers with different meanings. The new alignment test also asserts every AgentStatus number exists in ResourceStatus, so an AGENT_STATUS_* = 4 addition would fail CI and force the collision decision explicitly.

@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: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 2c05c0b993c0.
Review mode: incremental since 991ca452
Review run: https://github.com/ConductorOne/baton-sdk/actions/runs/32913312482

Review Summary

The full PR diff was re-scanned for security and correctness; it remains an additive proto3 enum value 4 on three hand-mirrored status enums plus tests, with no renumbering, no field-number reuse, and no type change, and with matching regenerated pb/.../*.pb.go output for every .proto edit in both the open and protoopaque variants. The new commits address both prior findings: TestStatusEnumMirrorsStayAligned at pkg/dotc1z/engine/pebble/translate_v2_test.go:205 is the requested value-agnostic parity guard over the three _name maps, and the stale "enum values are identical" comments at pkg/types/resource/resource.go:174 and pkg/types/resource/resource_attrs.go:90 now correctly describe AgentTrait_AgentStatus as a numeric prefix. One residual gap remains: the AgentTrait arm of that new guard is existence-only, so it does not actually catch the drift its own comment describes.

Risk triage (per docs/BUG_CATCHING.md section 2): silence - yes, a mistranslated status is a well-formed wrong row, not a crash; durability - yes, StatusRecord.ResourceStatus is written into c1z artifacts read by future SDK versions; uncontrolled dimensions - version-pair dependence exists, since an older generated .pb.validate.go rejects value 4, as the PR description calls out; consumer distance - downstream connectors and the platform. That scores HIGH for the PR as a whole, and the instrument that gives real coverage for a hand-mirrored durable enum is exactly a parity guard plus a cross-enum roundtrip - both are now present in the diff as TestStatusEnumMirrorsStayAligned and TestV2ResourceStatusPendingRoundtrip. The incremental delta under review here is tests and comments only, so it adds no durable risk of its own.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • pkg/dotc1z/engine/pebble/translate_v2_test.go:229 - the AgentTrait_AgentStatus arm of the new parity guard only asserts that each number exists in Status_ResourceStatus_name. Because ResourceStatus now has value 4 and AgentStatus stops at 3, the next AgentStatus value added will pass the guard while being silently cast to RESOURCE_STATUS_PENDING - the exact drift the comment above it says it prevents. Pin the mapping by name instead. (confidence: high)
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/dotc1z/engine/pebble/translate_v2_test.go`:
- Around lines 225-232: The AgentTrait arm of TestStatusEnumMirrorsStayAligned only
  checks that each AgentTrait_AgentStatus number has some entry in
  Status_ResourceStatus_name. That does not catch the drift the comment above it
  claims to catch: AgentStatus currently stops at 3 and ResourceStatus now goes to 4,
  so a future AGENT_STATUS value 4 will find RESOURCE_STATUS_PENDING, pass the test,
  and then be silently mistranslated by the numeric cast in syncAgentTraitToResource
  at pkg/types/resource/resource.go:174 and GetStatus at
  pkg/types/resource/resource_attrs.go:90.
  Replace the existence check with a name-pinned expectation table: declare a local
  map from int32 to string named expectedAgentMirror holding the four reviewed pairs
  0 to RESOURCE_STATUS_UNSPECIFIED, 1 to RESOURCE_STATUS_ENABLED, 2 to
  RESOURCE_STATUS_DISABLED, and 3 to RESOURCE_STATUS_DELETED; assert require.Len on
  v2.AgentTrait_AgentStatus_name against the length of expectedAgentMirror, with a
  message telling the next author to review the numeric cast before extending the
  table; then loop over v2.AgentTrait_AgentStatus_name asserting that
  v2.Status_ResourceStatus_name at that number equals expectedAgentMirror at that
  number, failing with the AgentStatus number and name. Update the comment above the
  loop to say the mapping is pinned by name rather than merely required to exist. A
  ready-to-apply suggestion block is on the inline review comment for these lines.

@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)
}

@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.

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.

1 participant