Skip to content

feat(reader): add c1.reader.v3 grants reader service (un-nerfed records)#1031

Open
manojacs wants to merge 1 commit into
mainfrom
manojacs/v3-grant-reader
Open

feat(reader): add c1.reader.v3 grants reader service (un-nerfed records)#1031
manojacs wants to merge 1 commit into
mainfrom
manojacs/v3-grant-reader

Conversation

@manojacs

Copy link
Copy Markdown
Contributor

What & why

The v2 reader (c1.reader.v2 GrantsReaderService) returns c1.connector.v2.Grant, which drops fields the storage record holds — notably discovered_at — when it downshifts each v3.GrantRecord through V3GrantToV2. c1's uplift needs the per-grant discovered_at (the connector's actual observation time) to decide overlay deletion safely, so the read has to stop losing it.

This adds a parallel c1.reader.v3 GrantsReaderService that returns the rich c1.storage.v3.GrantRecord directly — nothing dropped on read.

reader_v2: v3.GrantRecord ──V3GrantToV2──► v2.Grant       (drops discovered_at)
reader_v3: v3.GrantRecord ─────────────────► v3.GrantRecord   (nothing dropped)

Shape

  • protoproto/c1/reader/v3/grant.proto: same 5 RPCs and request shapes as reader_v2 (GetGrant, ListGrantsForEntitlement, ListGrantsForResourceType, ListGrantsForEntitlements, ListGrantsForPrincipal); only the response record type changes to c1.storage.v3.GrantRecord.
  • pebbleengineV3Grants implements the generated server, reusing the exact same sync-resolution, fetch, pagination, cursor and filter helpers as the v2 methods and skipping only the V3GrantToV2 conversion. v2 behavior is byte-for-byte unchanged.
  • connectorstore — optional V3GrantReader capability + V3GrantReaderProvider discovery interface: type-asserted, Pebble-only, callers fall back to the v2 reader. Deliberately not part of the required Reader interface, so it never forces SQLite/gRPC readers to implement it.

Note on the provider indirection

The v3 RPC method names (GetGrant, …) are identical to the reader_v2 methods *Engine already carries, and Go has no method overloading — one type can't declare both. So the v3 methods live on a wrapper and *Engine exposes them via V3GrantReader(); discover the capability by asserting the store for V3GrantReaderProvider, not V3GrantReader directly.

Compatibility

Additive and backward-compatible: existing c1/connector/v2 wire contract untouched, no proto renumbering, no go.mod/go.sum/vendor changes, no default-behavior change. The value returned is the stored discovered_at, never a fresh time.Now().

Tests

All 5 v3 RPCs covered in adapter_reader_v3_test.go: records returned with their stored discovered_at, principal/resource-type filters honored (decoys excluded), and a pagination round-trip proving the cursor + records survive PageSize:1 page boundaries. go build ./..., go test ./pkg/connectorstore/... ./pkg/dotc1z/..., and golangci-lint on changed files all pass.

Scope / follow-ups

Grants-only (the must-have that unblocks the uplift/discovered_at work). The other record types (resources, entitlements, resource types, syncs) can follow the same reader_v3 pattern as needed. No gRPC server registration is wired to a network endpoint yet — this is the service definition + in-process engine implementation.

🤖 Generated with Claude Code

The reader_v2 GrantsReaderService returns c1.connector.v2.Grant, which
drops fields the storage record holds — notably discovered_at — when it
downshifts each v3.GrantRecord via V3GrantToV2. c1's uplift needs the
per-grant discovered_at (the connector's actual observation time) to
decide overlay deletion safely, so the read must stop losing it.

Add a parallel c1.reader.v3 GrantsReaderService that returns the rich
c1.storage.v3.GrantRecord directly — nothing dropped on read.

- proto: new proto/c1/reader/v3/grant.proto — same 5 RPCs and request
  shapes as reader_v2 (GetGrant, ListGrantsForEntitlement,
  ListGrantsForResourceType, ListGrantsForEntitlements,
  ListGrantsForPrincipal); responses return c1.storage.v3.GrantRecord.
- pebble: engineV3Grants implements the generated server, reusing the
  exact sync-resolution, fetch, pagination, cursor and filter helpers as
  the v2 methods and skipping only the V3GrantToV2 conversion. v2
  behavior is byte-for-byte unchanged.
- connectorstore: optional V3GrantReader capability + V3GrantReaderProvider
  discovery interface (type-asserted, Pebble-only; callers fall back to
  the v2 reader). Not part of the required Reader interface, so it never
  forces the other engines to implement it. The provider indirection is
  required because the v3 RPC names collide with the v2 ones on the store
  and Go has no method overloading.
- tests: all 5 v3 RPCs covered — records returned with their STORED
  discovered_at (never time.Now()), principal/resource-type filters
  honored, and a pagination round-trip proving cursor + records survive
  page boundaries.

Additive and backward-compatible; no proto renumbering; grants-only
(other record types follow the same pattern as needed).

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
@manojacs
manojacs requested a review from a team July 22, 2026 23:13
@github-actions

Copy link
Copy Markdown
Contributor

General PR Review: feat(reader): add c1.reader.v3 grants reader service (un-nerfed records)

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base dd39a54a9c95.
Review mode: full
View review run

Review Summary

Scanned the full PR diff for security and correctness. This is an additive change: a new c1.reader.v3 GrantsReaderService (proto + generated Go) plus a Pebble-only engineV3Grants implementation, an optional V3GrantReader/V3GrantReaderProvider capability in connectorstore, and tests. The v3 engine methods are byte-for-byte mirrors of the existing reader_v2 methods (same sync resolution, pagination, cursor, filter and error paths), differing only by skipping the V3GrantToV2 downshift so discovered_at and other rich fields survive. No proto renumbering, no changes to existing v2/wire contracts, no go.mod/go.sum/vendor changes, and the new interface is opt-in (not added to the required Reader interface), so existing SQLite/gRPC readers are unaffected. Proto source and generated pb/ output are consistent, and tests cover all 5 RPCs plus a PageSize:1 pagination round-trip. No new issues found.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • pkg/dotc1z/engine/pebble/adapter_reader_v3.go (whole file): the v3 methods duplicate ~200 lines of pagination/filter/cursor logic from adapter_reader.go and grants_for_entitlements.go; a future bug fix to the v2 path won't propagate to v3. Consider extracting a shared helper returning a v3.GrantRecord slice plus cursor that both v2 (with V3GrantToV2) and v3 wrap. Non-blocking; the duplication is intentional per the PR description.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In pkg/dotc1z/engine/pebble/adapter_reader_v3.go:
- Whole file: The five v3 grants-reader methods reimplement the same
  sync-resolution, pagination, cursor, and principal/resource-type filter
  logic that already exists in the reader_v2 methods (ListGrantsForEntitlement
  and ListGrantsForResourceType/ListGrantsForPrincipal in
  pkg/dotc1z/engine/pebble/adapter_reader.go, and ListGrantsForEntitlements
  in pkg/dotc1z/engine/pebble/grants_for_entitlements.go). This is ~200 lines
  of duplicated logic; a bug fix applied to the v2 path will silently not
  reach the v3 path. Optionally refactor so the shared logic lives in helpers
  that return a v3.GrantRecord slice plus the next cursor, and have the v2
  methods wrap them with V3GrantToV2 while the v3 methods return the records
  directly. This is a maintainability suggestion only; the current
  duplication is a deliberate, documented choice to keep v2 behavior
  byte-for-byte unchanged.

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

@manojacs

Copy link
Copy Markdown
Contributor Author

pkg/dotc1z/engine/pebble/adapter_reader_v3.go (whole file): the v3 methods duplicate ~200 lines of pagination/filter/cursor logic from adapter_reader.go and grants_for_entitlements.go; a future bug fix to the v2 path won't propagate to v3. Consider extracting a shared helper returning a v3.GrantRecord slice plus cursor that both v2 (with V3GrantToV2) and v3 wrap. Non-blocking; the duplication is intentional per the PR description.

Leaning toward seperation of concerns here as the co-existence of v2 and v3 readers are for a limited time and planning to deprecate v2.

ctx context.Context,
req *reader_v3.GrantsReaderServiceGetGrantRequest,
) (*reader_v3.GrantsReaderServiceGetGrantResponse, error) {
syncID := r.e.CurrentSyncID()

@kans kans Jul 23, 2026

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.

Why not use syncID, err := e.resolveActiveSyncForReader(ctx, req.GetAnnotations()) here like in the others? Ostensibly, we could drop the sync check entirely at some point.

edit, oh pkg/dotc1z/engine/pebble/adapter.go:922 does this too. Thats probably why this method is written this way. Would you fix that one, too?

edit edit: It looks like we don't preserve the sync id through a EndFreshSync? In which case, this API, and the v2 maybe won't work (in prod)?

cursorFor := func(rec *v3.GrantRecord) string {
id, err := grantIdentityFromRecord(rec)
if err != nil {
return ""

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.

^ swallowing this error is probably not correct

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