Skip to content

feat(auth): add owner-aware OAuth storage foundation - #1588

Open
bsbds wants to merge 77 commits into
xorbitsai:mainfrom
bsbds:feat/user-oauth-owner-foundation
Open

feat(auth): add owner-aware OAuth storage foundation#1588
bsbds wants to merge 77 commits into
xorbitsai:mainfrom
bsbds:feat/user-oauth-owner-foundation

Conversation

@bsbds

@bsbds bsbds commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Replacement for #1529. This PR preserves the owner-storage foundation while moving the complete Gmail ownership boundary to its own stacked PR.

Stack 1/5. Merge first. Complete Gmail ownership and lifecycle safety follows in bsbds/xagent#82 before actor-owned credential creation.

Summary

  • add nullable UserOAuth.resource_owner_key
  • replace owner-blind uniqueness with ordinary-owner and actor-owner partial indexes
  • scope non-Gmail OAuth consumers to ordinary credentials
  • leave Gmail provisioning, triggers, callbacks, renewal, and reconciliation behavior unchanged in this foundation
  • preserve user-deletion cascade behavior
  • make interrupted SQLite owner-index installation safely retryable when zero or one existing replacement index has the exact expected definition
  • retain fail-closed handling for malformed columns, malformed indexes, conflicting schema objects, duplicate identities, unsupported dialects, and unsafe downgrade

Verification

  • focused owner model, service, ordinary-consumer, SQLite cascade, and migration suites passed
  • SQLite recovery tests cover zero-index, ordinary-only, actor-only, duplicate-identity, malformed-index, current-schema, and legacy-schema states
  • PostgreSQL index ordering remains add column → create replacement indexes → drop the old constraint
  • one Alembic head: 20260818_user_oauth_resource_owner
  • Gmail service and trigger files have no PR diff against main
  • full pre-commit suite passed

Rollout

This PR does not contain a production writer for actor-owned UserOAuth rows. Do not enable one until Gmail PR #82 has landed. Follow docs/deployment.md for database-specific migration, retry, verification, and rollback procedures.

bsbds added 30 commits August 20, 2026 12:48
@bsbds

bsbds commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Finalization Summary

Push

  • Branch: feat/user-oauth-owner-foundation
  • Head: 140e22ef7ad7e2785604253cc4f30387c201cefe
  • Destination: bsbds/xagent:feat/user-oauth-owner-foundation
  • Result: success
  • Summary comment: this comment
  • PR description: not requested

Carry-over audit

I also re-audited the accepted PR #1529 findings against this split PR and its stack:

Resolved

  1. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:F3
    Finding: Prevent pure Alembic-from-empty from reaching the owner revision without user_oauth.user_id → users.id ON DELETE CASCADE.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Major
    Change: df272eb153142f698106939c4a589b346714ee8e and PostgreSQL fixture follow-up 140e22ef7ad7e2785604253cc4f30387c201cefe
    GitHub: report-only

  2. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-1
    Finding: Detect a leftover _alembic_tmp_user_oauth before SQLite migration retry.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 2c41cbcb28c7c5ea5681a3f0d6135d90197798bb
    GitHub: report-only

  3. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-2
    Finding: Mirror the Gmail watch-state row-count safety check during SQLite downgrade.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: c04ddeb77252f5972db9db8c460d2f854fc04ea5
    GitHub: report-only

  4. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-4
    Finding: Reject a bare string passed as the provider sequence to owner-scoped bulk deletion.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 8243de51caec83269887eaa7d3f8e6e0e9f3c9a0
    GitHub: report-only

  5. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-8
    Finding: Add interrupted SQLite batch-rebuild coverage for both temporary-table states.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 2c41cbcb28c7c5ea5681a3f0d6135d90197798bb
    GitHub: report-only

  6. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-9
    Finding: Add endpoint-level MCP actor-row read and delete isolation coverage.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: fe2e4f74daae38b27c32a394902f77456f9fffd8
    GitHub: report-only

  7. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-10
    Finding: Prove a generic OAuth callback preserves a same-user, same-provider actor-owned row.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 5882bf8325ad6f9d6c5965f8c5fbb010875498d0
    GitHub: report-only

Rejected

  1. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:C1
    Finding: Treat process exit after SQLite batch drop and before rename as silent credential destruction.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: unsound
    Emergency level: Minor
    Reason: Alembic copies rows before dropping the legacy table. In the configured sqlite3 transaction mode, that DML opens a transaction and later DDL does not commit it, so process exit before rename rolls back the copy/drop. The distinct leftover-temporary-table retry failure was resolved under minor-1.
    Sources: Alembic batch sequence; Python sqlite3 transaction control; current migration batch
    Claim-to-source mapping: Copy precedes drop — Alembic batch sequence and current migration batch; DML transaction protects the later DDL — Python sqlite3 transaction control.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-1
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  2. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-3
    Finding: File a PostgreSQL full-chain/current-metadata idempotence follow-up.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: outdated
    Emergency level: Minor
    Reason: The bounded test remains intentional, and the requested follow-up already exists.
    Sources: focused test; tracking issue #1534
    Claim-to-source mapping: Current bounded scope — focused test; requested tracking outcome — issue test(migrations): restore PostgreSQL full-chain idempotence coverage #1534.
    Related finding: N/A
    Tracking issue: test(migrations): restore PostgreSQL full-chain idempotence coverage #1534
    Reply: Not applicable — no inline thread
    GitHub: report-only

  3. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-5
    Finding: Generalize historical predicate normalization for hypothetical compound predicates.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: The historical helper accepts only this revision's two fixed single-term predicates; no compound predicate reaches it.
    Sources: fixed predicate definitions
    Claim-to-source mapping: Fixed inputs and absence of a compound consumer — fixed predicate definitions.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  4. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-6
    Finding: Require mutable current-model and immutable historical-migration owner lengths to remain equal.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: A future model length change must ship a new migration; it must not require an old historical revision to change.
    Sources: current model constant; historical migration constant
    Claim-to-source mapping: Current metadata contract — model constant; historical schema contract — migration constant.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  5. Finding ID: PRR_kwDORQ4Kr88AAAABKbXviA:minor-7
    Finding: Replace the scoped query with intent-only helpers or a session-wide loader criterion.
    Source: Review body PRR_kwDORQ4Kr88AAAABKbXviAfeat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: unsound
    Emergency level: Minor
    Reason: The helper installs user and owner predicates before returning the query. The cited consumers only append conjunctive filters and cannot widen it; a session-wide criterion would interfere with legitimate actor-scoped queries.
    Sources: scoped query; cloud-storage narrowing; tool-config narrowing
    Claim-to-source mapping: Existing owner/user scope — scoped query; cited consumers only narrow — cloud-storage and tool-config callers.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  6. Finding ID: PRRT_kwDORQ4Kr86bM1W1
    Finding: Detect alleged silent data loss after SQLite batch drop and before rename.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpSOfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1W1feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: This duplicates C1's unsupported data-loss mechanism; the real temporary-table retry problem was fixed under minor-1.
    Sources: canonical review body; Alembic batch sequence; Python sqlite3 transaction control
    Claim-to-source mapping: Duplicate request — canonical review body; batch order and transaction behavior — Alembic and Python documentation.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:C1
    Tracking issue: Not applicable
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  7. Finding ID: PRRT_kwDORQ4Kr86bM1W9
    Finding: Detect a leftover _alembic_tmp_user_oauth before retry.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpSZfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1W9feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as actionable body finding minor-1, implemented in 2c41cbcb28c7c5ea5681a3f0d6135d90197798bb.
    Sources: canonical review body
    Claim-to-source mapping: Duplicate temporary-table recovery outcome — canonical review body.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-1
    Tracking issue: Not applicable
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  8. Finding ID: PRRT_kwDORQ4Kr86bM1XD
    Finding: Generalize predicate normalization for compound predicates.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpShfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1XDfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same hypothetical outcome as rejected body finding minor-5; no compound consumer exists.
    Sources: fixed predicate definitions
    Claim-to-source mapping: No compound consumer — fixed predicate definitions.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-5
    Tracking issue: Not applicable
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  9. Finding ID: PRRT_kwDORQ4Kr86bM1XH
    Finding: Add the Gmail watch-state row-count check to rollback.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpSnfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1XHfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as actionable body finding minor-2, implemented in c04ddeb77252f5972db9db8c460d2f854fc04ea5.
    Sources: canonical review body
    Claim-to-source mapping: Duplicate rollback-check outcome — canonical review body.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-2
    Tracking issue: Not applicable
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  10. Finding ID: PRRT_kwDORQ4Kr86bM1XJ
    Finding: File a PostgreSQL full-chain idempotence follow-up.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpSqfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1XJfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: The requested tracking outcome already exists as issue test(migrations): restore PostgreSQL full-chain idempotence coverage #1534.
    Sources: tracking issue #1534
    Claim-to-source mapping: Existing follow-up — issue test(migrations): restore PostgreSQL full-chain idempotence coverage #1534.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-3
    Tracking issue: test(migrations): restore PostgreSQL full-chain idempotence coverage #1534
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  11. Finding ID: PRRT_kwDORQ4Kr86bM1XS
    Finding: Reject a bare string provider sequence.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpS0feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1XSfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as actionable body finding minor-4, implemented in 8243de51caec83269887eaa7d3f8e6e0e9f3c9a0.
    Sources: canonical review body
    Claim-to-source mapping: Duplicate provider-input outcome — canonical review body.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-4
    Tracking issue: Not applicable
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  12. Finding ID: PRRT_kwDORQ4Kr86bM1XV
    Finding: Require current model and historical migration owner lengths to remain equal.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpS6feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1XVfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as rejected body finding minor-6; current metadata and historical revisions are distinct contracts.
    Sources: current model constant; historical migration constant
    Claim-to-source mapping: Distinct current and historical contracts — model and migration constants.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-6
    Tracking issue: Not applicable
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  13. Finding ID: PRRT_kwDORQ4Kr86bM1Xe
    Finding: Replace the scoped query with a deny-by-default session mechanism.
    Source: Inline comment PRRC_kwDORQ4Kr87kXpTEfeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bM1Xefeat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as rejected body finding minor-7; appended filters cannot remove the installed owner predicate.
    Sources: scoped query; cited consumer
    Claim-to-source mapping: Existing predicate cannot be removed by appended filters — scoped query and cited consumer.
    Related finding: PRR_kwDORQ4Kr88AAAABKbXviA:minor-7
    Tracking issue: Not applicable
    Reply: posted — feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

Unresolved

None.

Totals

  • Current findings: 20
  • Resolved: 7
  • Rejected: 13
  • Unresolved: 0

@bsbds
bsbds requested review from rogercloud and removed request for rogercloud August 21, 2026 16:49
@bsbds
bsbds requested a review from rogercloud August 21, 2026 18:00

@rogercloud rogercloud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

This PR lays the schema and service foundation for owner-aware builtin OAuth credentials: UserOAuth gains a nullable resource_owner_key column, the owner-blind uq_user_provider_account constraint is replaced by two partial unique indexes (uq_user_oauth_ordinary_account on resource_owner_key IS NULL, uq_user_oauth_actor_account on IS NOT NULL), a user_id -> users.id ON DELETE CASCADE FK is backfilled, and a new src/xagent/web/services/user_oauth.py centralizes owner-scoped read/delete of OAuth rows. Non-Gmail consumers (api/auth.py, api/cloud_storage.py, api/mcp.py, tools/config.py) are converted to the scoped helpers, and the migration carries substantial SQLite batch-rebuild repair machinery plus a dialect allowlist shared with app startup.

Deliberately, no production code writes an actor-owned row yet — verified: all 13 resource_owner_key= sites in src/ pass literal None. Gmail's full ownership boundary is deferred to stacked PR #82. The motivation is a multi-PR stack toward actor-owned / delegated OAuth credentials, where an actor-owned row shares the same user_id as its owning user and is distinguished only by resource_owner_key.

Approach verdict: acceptable-with-reservations

The schema/migration work is careful and well-tested, and owner-scoping is cohesive — one service module used at every converted call site. Three reservations:

  1. Filtered relationship trades a real guarantee for hiding invisible rows. User.oauth_accounts is primaryjoin-filtered to ordinary rows, which buys nothing today (nothing reads actor rows) at the cost of dropping them out of the ORM cascade.
  2. Nullable owner key diverges from this codebase's own convention (MCPOAuthGrant / MCPOAuthFlowState use NOT NULL + synthetic sentinel), and the divergence carries a long complexity chain as its price.
  3. The boundary to PR #82 is enforced by review convention and prose only, not by code. Finding M3 below is direct proof that convention-only enforcement has already missed a site inside this very PR.

Major findings (blocking)

M1. The documented rollback procedure silently deletes an unrelated Stripe MCP catalog row

docs/deployment.md:179 instructs alembic downgrade b1efe0dbe0af. But src/xagent/migrations/versions/0108d2704fc1_merge_stripe_and_oauth_owner_heads.py:13-16 merges two independent branches off that same parent: this PR's 20260818_user_oauth_resource_owner and an unrelated 20260818_seed_stripe_mcp_app (pulled in by an upstream-main merge during this PR's life). Downgrading straight to b1efe0dbe0af necessarily unwinds both branches, and 20260818_seed_stripe_mcp_app.py:86-98 executes DELETE FROM public_mcp_apps WHERE app_id='stripe'.

So the documented rollback removes the Stripe catalog entry and orphans MCPServer / UserMCPServer rows for users who already connected Stripe — with zero mention in the doc and zero test coverage. tests/migrations/test_migration_integration.py:484-493 runs this exact downgrade and never asserts anything about public_mcp_apps.

Please scope the documented rollback to unwind only the OAuth-owner branch (e.g. stepwise alembic downgrade -1 through 0108d2704fc1 then the OAuth-owner revision), or explicitly document and accept the Stripe-seed revert if that is genuinely intended — plus a test asserting the Stripe row's fate across this downgrade path.

M2. FK-cascade gap on a from-empty pure-Alembic bootstrap is only partially fixed, and the one test that exercised the scenario was changed to stop exercising it

This was raised in round 1, rejected on the basis of a test that did not cover it, then reproduced directly in round 2 (empty SQLite DB → alembic upgrade headuser_oauth created with no FK at all, because c7dfa28cc67a_add_user_oauth_table.py:36 only adds the FK when users already exists) and reopened as blocking. Commits df272eb1 + 140e22ef claim a fix.

Current state, verified: the fix converts silent corruption into a hard failure — src/xagent/migrations/versions/20260818_add_user_oauth_resource_owner.py:226-230 now raises RuntimeError when users is absent. That is a genuine improvement, but it does not guarantee the FK ends up present. A from-empty pure-Alembic run now aborts mid-chain, leaving a half-migrated database that needs manual operator recovery.

The larger concern is the test change. test_empty_database_alembic_upgrade_to_head_completes was renamed in the final commit f0e712f2 to test_database_with_core_users_table_alembic_upgrade_to_head_completes and altered to pre-create users before the upgrade (tests/alembic/test_20260706_add_connector_runtime_context.py:160-173) — so it no longer covers the empty-database path and does not assert the new fail-closed behavior. The same pattern appears on the CLI-invocation path the round-2 reviewer used to reproduce the bug: tests/migrations/test_migration_integration.py:488 now calls create_metadata_owned_users_table() before upgrading.

This is the second time in this PR's history that a response to this specific finding did not hold up on close reading, so it stays blocking. Please either (a) make the migration chain guarantee the FK end-to-end from a genuinely empty database without aborting, or (b) if fail-closed-with-manual-recovery is the accepted design, restore an automated test that runs the full chain from empty and asserts the RuntimeError, and document the manual recovery procedure.

M3. services/triggers.py:603 is an owner-blind, user-controlled UserOAuth lookup missed by this PR's scoping pass

account = db.query(UserOAuth).filter(UserOAuth.id == int(oauth_account_id)).first()

src/xagent/web/services/triggers.py:603, in _resolve_gmail_resource, guarded only by the subsequent account.user_id == user_id check on line 604. oauth_account_id arrives straight from user-supplied Gmail trigger config (GmailTriggerConfig.oauth_account_id, validated at src/xagent/web/services/triggers.py:708-713) — an ordinary request body field.

This PR introduced user_oauth_owner_clause / get_scoped_user_oauth_account precisely to stop a direct-ID lookup from widening into another namespace, and migrated api/auth.py, api/cloud_storage.py, api/mcp.py, tools/config.py — but missed this file. src/xagent/web/services/triggers.py is entirely unchanged by this PR, which is consistent with the scoping pass having implicitly scanned gmail_*-prefixed filenames; this is the generic, non-gmail-prefixed trigger service.

It is latent today (no writer of actor-owned rows exists). But per this PR's own model docstring, an actor-owned row shares the same user_id as its owning user and differs only in resource_owner_key. The moment #82 adds an actor-owned writer, an ordinary user can bind a Gmail trigger's oauth_account_id to their own actor-owned credential, bypassing exactly the ordinary/actor separation this PR exists to establish.

Please either route this query through the new helper now — cheap while it is harmless — or record it as a hard blocker on #82's review checklist. It should not be left implicitly for #82, since #82 is scoped to gmail_* files and would plausibly miss this same non-gmail-named file a second time.

M4. The PR's central regression test cannot detect the regression it exists to catch

tests/web/test_github_oauth.py:173-177 asserts that an OAuth callback preserves an actor-owned row:

preserved_actor = db.get(UserOAuth, actor_account_id)
assert preserved_actor is actor_account
assert preserved_actor.access_token == "actor-token"

delete_scoped_user_oauth_accounts (src/xagent/web/services/user_oauth.py:149) hardcodes query.delete(synchronize_session=False) — a deliberate change from the implicit "auto" used by the pre-PR callback code. As a result the identity-mapped actor_account object created earlier in the same session is never evicted or expired, and db.get() returns the cached in-memory object with its pre-delete attribute values regardless of what the DELETE actually did. assert preserved_actor is actor_account is in fact an assertion about the identity map, not the database. The test would pass even if the callback's delete wrongly matched and removed the actor row.

Verified separately: the WHERE clause itself (user_oauth_owner_clause, scoped to resource_owner_key IS NULL) is correct today, so there is no live production bug — only a test that cannot protect the guarantee it names. Please assert real database state: db.expire_all() before re-fetching, db.get(..., populate_existing=True), or a COUNT(*) on a separate session/connection.

Design-level notes (non-blocking)

D1. User.oauth_accounts (src/xagent/web/models/user.py:68-79) is primaryjoin-filtered to ordinary rows, which removes actor-owned rows from the delete-orphan cascade at the one ORM user-delete site, src/xagent/web/api/admin_users.py:234. Deletion of actor rows on user delete now rests entirely on the DB-level ON DELETE CASCADE. Mitigating: SQLite's pragma setup in src/xagent/db/sqlite.py is fail-closed (raises if PRAGMA foreign_keys=ON does not take, rather than degrading silently), and there is no writer of actor rows yet. So this is a dormant, forward-looking tradeoff — worth an explicit note carried into the #82 stack rather than a current defect.

D2. The nullable resource_owner_key diverges from the sibling MCPOAuthGrant / MCPOAuthFlowState convention (always NOT NULL with a synthetic sentinel). The cost is a real complexity chain: a dialect allowlist enforced at app startup, two partial unique indexes instead of one plain index, SQLite batch-rebuild-with-interrupted-state-repair machinery, and a filtered relationship. The implementation is sound, but the PR description should carry a written rationale for diverging from the established pattern.

D3. src/xagent/web/api/mcp.py uses the field name resource_owner_key for two semantically different conventions in the same file — this PR's nullable UserOAuth convention and the pre-existing always-non-null MCPOAuthGrant / MCPOAuthFlowState convention. Each site is individually correct; this is a maintainability nit worth a comment at minimum.

D4. src/xagent/migrations/versions/20260818_add_user_oauth_resource_owner.py:33 imports xagent.db.migration_support — the only migration version file in the repo importing application code. The shared-invariant rationale (accepted in a prior round) is sound, but it creates forward coupling: if that module is moved or renamed, this one historical revision stops being replayable against existing databases, unlike every other self-contained revision. Informational only.

Minor findings

N1. The SQLite interrupted-migration retry branch (20260818_add_user_oauth_resource_owner.py:247-260) calls _missing_owner_index_definitions() but never _sqlite_global_owner_relation_names(), so a same-named unrelated table/index/view collision on this narrow path surfaces as a raw sqlite3.OperationalError instead of the guided RuntimeError the fresh-install branch at line 264-271 produces. Narrow (needs an interrupted migration and a coincidental name collision), untested.

N2. downgrade() (20260818_add_user_oauth_resource_owner.py:313-358) has no leftover-_alembic_tmp_user_oauth guard, unlike upgrade() (which got one in commit 2c41cbcb, lines 217-222). An interrupted downgrade, retried, fails with a raw OperationalError instead of the guided message. Rollback path only.

N3. The PR description's "one Alembic head: 20260818_user_oauth_resource_owner" claim is stale again — the upstream-main merge that brought in the Stripe seed migration made 0108d2704fc1 the actual single head. This exact claim was corrected once earlier in this PR's life and has drifted a second time. No code anchor; please refresh the description.

Checked, no further action

  • SQLite batch-rebuild DROP-before-RENAME data-loss claim (round 2, C1) — the rebuttal (a preceding INSERT opens an implicit pysqlite transaction that a subsequent DROP does not force-commit, so a mid-sequence crash rolls back cleanly) was independently verified against this codebase's actual SQLAlchemy/Alembic/pysqlite configuration: no isolation_level override, and ApplyBatchImpl keeps DROP+RENAME in that same implicit transaction. Waiver stands.
  • PostgreSQL idempotence test narrowing (test_postgresql_owner_migration_accepts_current_metadata via command.stamp()) — unchanged since the last round and already tracked in open issue #1534. Correctly deferred; not a new gap.
  • test_postgresql_upgrade's apparently-lost assertions (models/users/tasks tables, _api_key_encrypted column) — those checked tables that no Alembic migration in this repo has ever created (they are SQLAlchemy-metadata-owned only), so the assertions were unreachable before this PR touched the test. Not a coverage regression.
  • Previously waived round-1 items — predicate paren-balance robustness note, owner-column-length constant duplication between model and migration, the query-scoping "convention not enforced" ask, and migration code-quality nits 8/9/11/12: all previously rejected with unrebutted technical rationale, and no new information changes that.

Simplification opportunities (optional, not blocking)

A separate mechanical pass over the diff (not line-by-line re-verified — treat as suggestions):

L5:   shrink: OWNER_AWARE_UNIQUE_INDEX_DIALECTS frozenset has one use site. Inline {"sqlite", "postgresql"}.
L15:  delete: redundant `not isinstance(dialect, str)` clause in migration_support.py — a non-str already fails the `not in` check.
L55:  delete: the third tuple field in OWNER_INDEX_DEFINITIONS is always True — hardcode unique=True.
L67:  shrink: _require_partial_unique_index_support is a one-line alias with two callers — call the helper directly.
L71:  shrink: _table_exists / _users_table_exists are the same one-liner with a different constant — collapse to _has_table(name).
L100: delete: get_user_oauth_account_by_id has zero production callers — drop until #82 needs it.
L191: delete: dead postgresql_where fallback in a test helper that only ever inspects SQLite.

The fuller pass found roughly 18 similar items, mostly delete: / shrink: of near-duplicate branches in the migration-repair helpers; net: approximately -60 to -80 lines possible. Happy to enumerate the rest if useful.

Blocking status & recommended decision

Blocking: yes — 4 major findings.

  • M1 [new] — the documented rollback procedure silently deletes the Stripe MCP catalog row via the merged sibling branch, untested and undocumented.
  • M2 [prior, reopened — partially fixed] — from-empty FK gap now fails closed instead of corrupting, but does not guarantee the FK, and the one test covering the scenario was changed to pre-create users rather than assert the new behavior.
  • M3 [new]services/triggers.py:603 is an owner-blind user-controlled UserOAuth lookup missed by the scoping pass; an ownership bypass the moment #82 lands.
  • M4 [new] — the actor-row-preservation test asserts against the identity map, not the database, so it cannot catch a regression of this PR's central safety guarantee.

Recommended event: REQUEST_CHANGES.

Comment thread docs/deployment.md Outdated
1. Stop all workers before the downgrade.
2. If the database is SQLite, create a current database backup.
3. If the database is SQLite, run `PRAGMA integrity_check;` against the backup and record `SELECT count(*) FROM gmail_watch_states;`. The integrity result must be `ok`.
4. Run `alembic downgrade b1efe0dbe0af`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Major: this documented rollback silently deletes an unrelated Stripe catalog row.

0108d2704fc1_merge_stripe_and_oauth_owner_heads.py:13-16 merges two independent branches off b1efe0dbe0af: this PR's 20260818_user_oauth_resource_owner and the unrelated 20260818_seed_stripe_mcp_app (pulled in by the upstream-main merge). Downgrading straight to b1efe0dbe0af necessarily unwinds both, and 20260818_seed_stripe_mcp_app.py:86-98 runs DELETE FROM public_mcp_apps WHERE app_id='stripe' — removing the Stripe catalog entry and orphaning MCPServer/UserMCPServer rows for users who already connected Stripe. Neither this doc nor any test mentions it; tests/migrations/test_migration_integration.py:484-493 runs this exact downgrade without asserting on public_mcp_apps.

Please scope the documented rollback to the OAuth-owner branch only (stepwise alembic downgrade -1 through 0108d2704fc1, then the OAuth-owner revision), or explicitly document and accept the Stripe-seed revert — and add a test pinning the Stripe row's fate across this path.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWYo8 because it is a duplicate of canonical finding PRR_kwDORQ4Kr88AAAABKfaP0Q:M1, which covers the same Stripe-preserving rollback outcome (canonical review finding).

The underlying collateral deletion occurs because the Stripe revision's downgrade deletes the Stripe catalog row (Stripe downgrade implementation). This outcome is handled once by M1 rather than through a second independent resolution.

return

if not _users_table_exists():
raise RuntimeError(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Major (prior finding, reopened — partially fixed): this converts the from-empty FK gap from silent corruption into a hard failure, which is a real improvement, but it does not guarantee the FK ends up present. c7dfa28cc67a_add_user_oauth_table.py:36 still creates user_oauth without any FK when users is absent, so a from-empty pure-Alembic run now aborts here mid-chain and leaves a half-migrated database requiring manual operator recovery.

Please either make the chain guarantee the FK end-to-end from a genuinely empty database without aborting, or — if fail-closed-with-manual-recovery is the intended design — document the manual recovery procedure and restore an automated test that runs the full chain from empty and asserts this RuntimeError (see the related comments on the two tests that were changed to pre-create users).

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWYo_ because it is a duplicate of canonical finding PRR_kwDORQ4Kr88AAAABKfaP0Q:M2, which covers the same full-chain negative-coverage and recovery-documentation outcome (canonical review finding).

Pure-Alembic empty bootstrap is intentionally outside the supported application-startup path, which stamps an empty database before metadata-owned tables are created (startup migration contract). M2 accounts for the remaining fail-closed test and recovery guidance without treating unsupported bootstrap as a supported initialization mechanism.

with engine.begin() as connection:
# Application startup creates metadata-owned core tables before
# Alembic upgrades the migration-owned schema.
User.__table__.create(bind=connection)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Major (test coverage): this was the one end-to-end test exercising the disputed scenario. test_empty_database_alembic_upgrade_to_head_completes was renamed to test_database_with_core_users_table_... and altered to pre-create users before the upgrade, so it no longer covers the empty-database path — and it does not assert the new fail-closed RuntimeError either.

That leaves the reopened FK-cascade finding with no automated coverage in either direction. Please keep a test that runs the full chain from a genuinely empty database and asserts whichever behavior is intended (FK present, or RuntimeError raised), in addition to this core-tables-present case.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWYpE because it is a duplicate of canonical finding PRR_kwDORQ4Kr88AAAABKfaP0Q:M2, which contains the same required full-chain negative-coverage outcome (canonical review finding).

The existing supported-path test deliberately creates the metadata-owned parent before migration (supported startup-path test); M2 separately accounts for the genuinely empty fail-closed case rather than duplicating that resolution here.

"""Rollback removes owner storage and retains the existing merge head."""
parent = "b1efe0dbe0af"

sqlite_tester.create_metadata_owned_users_table()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same concern as the tests/alembic change: this is the CLI-invocation path that was used to reproduce the original missing-FK bug, and it now pre-creates users via create_metadata_owned_users_table() before upgrading — so it no longer reaches the empty-database branch. Please add (not replace) a case that upgrades from empty on this path and asserts the intended outcome.

Separately, this test runs command.downgrade(..., "b1efe0dbe0af") — the exact path flagged in docs/deployment.md:179 — and would be the natural place to assert what happens to the public_mcp_apps Stripe row.

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.

We are not making separate changes for findings PRRT_kwDORQ4Kr86bWYpJ:finding-1 and PRRT_kwDORQ4Kr86bWYpJ:finding-2 because they duplicate canonical findings PRR_kwDORQ4Kr88AAAABKfaP0Q:M2 and PRR_kwDORQ4Kr88AAAABKfaP0Q:M1, respectively (canonical review findings).

M2 owns the empty-bootstrap full-chain coverage outcome, while M1 owns the Stripe-preserving rollback and regression-test outcome. Handling each outcome once avoids independent, overlapping resolutions for this combined thread.

assert oauth_account.email == "octocat"

preserved_actor = db.get(UserOAuth, actor_account_id)
assert preserved_actor is actor_account

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Major: this assertion cannot detect the regression it exists to catch.

delete_scoped_user_oauth_accounts (src/xagent/web/services/user_oauth.py:149) hardcodes query.delete(synchronize_session=False), a deliberate change from the implicit "auto" the pre-PR callback used. The identity-mapped actor_account object created earlier in this same session is therefore never evicted or expired, so db.get() returns the cached object with its pre-delete attribute values regardless of what the DELETE actually did. assert preserved_actor is actor_account asserts on the identity map, not the database — the test passes even if the callback's delete wrongly matched and removed the actor row.

The WHERE clause is correct today (user_oauth_owner_clause scopes to resource_owner_key IS NULL), so there is no live bug — but this is the PR's central safety guarantee and it is currently unprotected. Please assert real DB state: db.expire_all() before re-fetching, db.get(UserOAuth, actor_account_id, populate_existing=True), or a COUNT(*) on a separate session/connection.

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.

We are not making this change for finding PRRT_kwDORQ4Kr86bWYpM because it duplicates M4's incorrect identity-map premise. The callback commits before the assertion (callback commit).

SQLAlchemy sessions default to expire_on_commit=True, so that commit fully expires persistent instances and subsequent access reloads current database state (SQLAlchemy expire_on_commit documentation). Consequently, the existing db.get() assertion does not silently accept a deleted actor row, and an additional explicit expiration would be redundant.

# the first replacement index is created. Existing indexes were
# validated above, so creating only the missing definitions safely
# completes that exact interrupted state without accepting drift.
_create_owner_indexes(missing_indexes)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: this interrupted-migration retry branch calls _missing_owner_index_definitions() but never _sqlite_global_owner_relation_names(), unlike the fresh-install branch at lines 264-271. On this path a same-named unrelated table/index/view surfaces as a raw sqlite3.OperationalError rather than the guided RuntimeError operators get elsewhere. Narrow (needs both an interrupted migration and a coincidental name collision) and currently untested, but the guard is one call.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWYpP because it is a duplicate of canonical finding PRR_kwDORQ4Kr88AAAABKfaP0Q:N1, which contains the same interrupted-repair collision diagnostic and focused regression-test outcome (canonical review finding).


has_old_constraint = OLD_CONSTRAINT in _constraint_names()
dialect = op.get_bind().dialect.name
if dialect == "sqlite" and (OWNER_COLUMN in columns or not has_old_constraint):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: downgrade() has no leftover-_alembic_tmp_user_oauth guard, unlike upgrade() (lines 217-222, added in 2c41cbcb). An interrupted SQLite downgrade that is retried fails with a raw OperationalError instead of the guided "restore the verified backup" message — which matters here, since docs/deployment.md explicitly tells operators not to retry a failed downgrade against the changed database. Rollback path only.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWYpR because it is a duplicate of canonical finding PRR_kwDORQ4Kr88AAAABKfaP0Q:N2, which contains the same interrupted-downgrade temporary-table diagnostic and regression-test outcome (canonical review finding).

import sqlalchemy as sa
from alembic import op

from xagent.db.migration_support import require_owner_aware_unique_index_dialect

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Informational (no action required): this is the only migration version file in the repo that imports application code. The shared-invariant rationale accepted in the earlier round is sound, but note the forward coupling — if xagent.db.migration_support is ever moved or renamed, this historical revision stops being replayable against existing databases, unlike every other self-contained revision here. Worth a short comment recording that constraint for whoever refactors that module later.

Minor cleanup while you are here: OWNER_AWARE_UNIQUE_INDEX_DIALECTS has a single use site and could be inlined, and the not isinstance(dialect, str) clause in migration_support.py:15 is redundant since a non-str already fails the not in check.

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.

We are not making the import-comment change for PRRT_kwDORQ4Kr86bWYpU:finding-1 because the helper module already documents that it deliberately shares one startup/migration invariant, while a comment cannot enforce future import-path compatibility (shared invariant module). The earlier review likewise accepted this shared import as intentional (prior review disposition).

We are not making a separate change for PRRT_kwDORQ4Kr86bWYpU:finding-2 because it duplicates L5. The named dialect constant communicates the shared invariant rather than adding accidental complexity (shared invariant module).

We are not making a separate change for PRRT_kwDORQ4Kr86bWYpU:finding-3 because it duplicates L15 and is technically unsafe. The helper deliberately accepts an object-valued runtime boundary and converts invalid values to RuntimeError (dialect validation helper); set membership requires hashable values, so removing the explicit type guard can instead expose TypeError for unhashable non-strings (Python set-type documentation).

@rogercloud rogercloud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR adds the nullable UserOAuth.resource_owner_key ownership boundary, ordinary-only OAuth consumers, and SQLite/PostgreSQL migration and recovery coverage for the staged actor-credential rollout. It intentionally leaves actor writers and the Gmail ownership lifecycle for the downstream stack, while changing the migration graph and operational rollback procedure. Blocking: yes — recommended event: REQUEST_CHANGES

Since the previous reviewed head 140e22ef, commit 70922bef merged upstream main and the Stripe branch, leaving the Stripe seed and owner revision as two heads. Commit 8c4d2383 added 0108d2704fc1_merge_stripe_and_oauth_owner_heads, making 0108d2704fc1 the current sole head and making the rollback traversal material. Commit f0e712f2 added metadata-bootstrap handling in the migration tests.

Round 0 approach verdict

Verdict: acceptable-with-reservations. The nullable ownership model, ordinary-only read boundary, transaction ownership, and bounded SQLite recovery design fit the staged rollout. The reservation is the migration graph: the new owner revision still descends from b1efe0dbe0af even though the base Stripe seed revision has the same parent, so the no-op merge couples an OAuth rollback to an unrelated Stripe data downgrade. That macro concern is the same canonical root as N1 below, not a second finding.

Confirmed findings

Major

N1 — Documented OAuth rollback deletes the Stripe catalog row [new]

docs/deployment.md:179 directs operators to run alembic downgrade b1efe0dbe0af. The added merge revision src/xagent/migrations/versions/0108d2704fc1_merge_stripe_and_oauth_owner_heads.py:12-16 has both 20260818_user_oauth_resource_owner and 20260818_seed_stripe_mcp_app as parents, and both branches descend from b1efe0dbe0af; Alembic therefore traverses and downgrades the Stripe sibling as well as the OAuth branch. The Stripe downgrade at src/xagent/migrations/versions/20260818_seed_stripe_mcp_app.py:86-98 unconditionally deletes public_mcp_apps.app_id='stripe'. Because administrators can customize the Stripe catalog row, re-upgrading can reseed defaults but cannot restore those persisted customizations.

Please either linearize the new owner revision after 20260818_seed_stripe_mcp_app and remove the no-op merge, or keep the merge and change the runbook target/expected revision to 20260818_seed_stripe_mcp_app. Add SQLite and PostgreSQL downgrade coverage that edits the Stripe row before rollback and asserts both row presence and the customization afterward.

Minor

N2 — OAuth state owner validation still coerces malformed numeric claims [new]

At src/xagent/web/api/auth.py:1712, int(user_id_claim) accepts booleans and floats (True becomes 1, and 7.9 becomes 7). A signed malformed state can consequently reach the ordinary-account delete/recreate path, while an infinity-sized float can raise OverflowError; the outer callback handler begins later and does not catch that exception. This is a narrow validation/error-boundary defect, not a newly introduced major authentication bypass, because normal state issuance uses an existing integer User.id.

Require exact positive-integer semantics without coercion, for example type(user_id_claim) is int and user_id_claim > 0, preserve the explicit legacy None policy, and reject/catch overflow before provider exchange. Add malformed-state tests for booleans, fractional and integral floats, Infinity/overflow, and oversized values, asserting an invalid-state response, no provider call, and no database mutation.

N4 — SQLite recovery documentation and tests omit the valid both-indexes-present state [new]

docs/deployment.md:92 says automatic recovery is limited to zero or one existing owner-aware index. The migration safely accepts both exact replacement indexes when SQLite DDL completed before Alembic recorded the revision: _missing_owner_index_definitions() returns no missing definitions and the retry proceeds without duplicate CREATE INDEX calls. Update the runbook to describe zero, one, or both exact indexes, explain that both-present means DDL completed before the revision stamp, and add (ORDINARY_INDEX, ACTOR_INDEX) to the recovery parameterization at tests/alembic/test_20260818_add_user_oauth_resource_owner.py:258-264.

Minor/nit

N5 — Post-commit helper retains the pre-validation raw-claim contract [new]

After validation moved before provider exchange, _run_post_commit_oauth_side_effects in src/xagent/web/api/auth.py:82-126 still uses user_id: Any, its :91-95 docstring says it receives the raw OAuth-state claim and performs coercion inside the helper, and :126 repeats int(user_id). The current callback passes an already-normalized integer under the non-null guard, so this is documentation and local-contract drift rather than a reachable malformed-state bug. Change the parameter to int, describe the validated callback boundary, and remove the redundant conversion, or explicitly document why defensive conversion is intentionally retained.

Prior body-only finding still open

P22 — NOT FIXED (body-only, prior). The PR description still says 20260818_user_oauth_resource_owner is the sole Alembic head. After 8c4d2383, the current sole head is 0108d2704fc1; update the Verification section to name the merge head. Source: PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-14, prior review.

Prior findings checklist

Root Status Source IDs / URLs and current-code disposition
P1 REFACTORED PRR_kwDORQ4Kr88AAAABKZxBXQ:F1; inline 3830054546, 3830054557, 3830054567, 3830054577; replies 3830866389, 3830866392, 3830866398, 3830866400; review. Gmail lifecycle moved to bsbds/xagent#82; no current #1588 Gmail diff.
P2 REFACTORED PRR_kwDORQ4Kr88AAAABKZxBXQ:F2; inline 3830054584; reply 3830866395; review. The Gmail lock path moved to #82 and is absent from this PR.
P3 FIXED PRR_kwDORQ4Kr88AAAABKZxBXQ:F3, PRR_kwDORQ4Kr88AAAABKbXviA:F3; inline 3830054587; reply 3830866385; reviews 4993073501 / 4994756488. Supported paths require the users/cascade invariant and the unsupported bare-empty path now fails closed.
P4 FIXED PRR_kwDORQ4Kr88AAAABKZxBXQ:F4; inline 3830054587; reply 3830866385; review. Bounded exact-index recovery and tests are present.
P5 REFACTORED PRR_kwDORQ4Kr88AAAABKZxBXQ:F5a; review. Gmail behavior and its regression tests moved to #82.
P6 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:F5b; review. The sessionless test intentionally covers Python-side sync_backref=False; persisted and production cascade tests cover separate seams.
P7 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:F5c; review. An adjacent production-engine test verifies real SQLite pragma wiring.
P8 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-1; review. Gmail relationship scoping belongs to and exists in #82.
P9 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-2; review. The current SQLite docstring already explains mandatory FK integrity versus optional concurrency pragmas.
P10 DROPPED PRR_kwDORQ4Kr88AAAABKZLRPw:finding-1, PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-3; inline 3829556093, reply 3830866377; bot review, duplicate review. Sequence[str] and upstream candidate filtering make nullable elements unreachable for current callers.
P11 DROPPED Body occurrences in reviews 4993073501 and 4994756488; predecessor inline IDs 3827121809, 3828379577, 3820038532, 3820300940 in #1529. The ID-only and user-scoped helpers intentionally have different trust contracts.
P12 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-4; review. A future raw-query CI guard is not an actionable current defect; no changed caller bypasses the scoped service.
P13 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-5, PRR_kwDORQ4Kr88AAAABKbXviA:minor-6; inline 3831403706; reply 3831834089; reviews 4993073501 / 4994756488. Current model and immutable historical migration lengths are separate contracts.
P14 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-6; review. Callback normalization and the shared non-null guard make the nullable annotation non-actionable.
P15 FIXED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-7; review. _owner_index_names is removed and no dead replacement remains.
P16 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-8; review. Validation and later dialect access use the same migration bind; the observation is harmless style.
P17 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-9; review. SQLite permits same-named triggers and indexes; trigger probing would not prevent a failure.
P18 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-10; review. Hatch packages the shared helper and Alembic already imports the application package.
P19 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-11; review. object is intentional because runtime type rejection is part of the validation boundary.
P20 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-12; review. Original exception text is logged and re-raised.
P21 DROPPED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-13, PRR_kwDORQ4Kr88AAAABKbXviA:minor-3; inline 3831403690; reply 3831833484; reviews 4993073501 / 4994756488. The bounded test is intentional and the historical full-chain follow-up is tracked in issue #1534.
P22 NOT FIXED PRR_kwDORQ4Kr88AAAABKZxBXQ:finding-14; review. Body-only stale head description; update it to 0108d2704fc1.
P23 DROPPED PRR_kwDORQ4Kr88AAAABKbXviA:C1; inline 3831403662; reply 3831832426; review. Configured SQLite transaction semantics protect copy/drop; the leftover-temp retry issue is separate P24.
P24 FIXED PRR_kwDORQ4Kr88AAAABKbXviA:minor-1; inline 3831403673; reply 3831832691; review. Early temp-table detection and both interruption-state tests are present.
P25 FIXED PRR_kwDORQ4Kr88AAAABKbXviA:minor-2; inline 3831403687; reply 3831833199; review. Rollback now records and compares the Gmail watch-state count.
P26 FIXED PRR_kwDORQ4Kr88AAAABKbXviA:minor-4; inline 3831403700; reply 3831833769; review. Bare provider strings now raise before query construction and have regression coverage.
P27 FIXED PRR_kwDORQ4Kr88AAAABKbXviA:minor-8; review. Both temporary-table interruption states are covered.
P28 FIXED PRR_kwDORQ4Kr88AAAABKbXviA:minor-9; review. MCP read/delete actor isolation tests exercise the changed endpoints.
P29 FIXED PRR_kwDORQ4Kr88AAAABKbXviA:minor-10; review. Generic callback coverage preserves a same-user, same-provider actor row.
P30 DROPPED PRR_kwDORQ4Kr88AAAABKbXviA:minor-5; inline 3831403681; reply 3831832946; predecessor IDs 3823039337, 3823443205 in #1529. The private normalizer receives only two fixed single-term predicates.
P31 DROPPED PRR_kwDORQ4Kr88AAAABKbXviA:minor-7; inline 3831403716; reply 3831834353; review. Existing user/owner predicates remain in force because callers only append conjunctive filters.

Review coverage and limitations

No local tests, builds, linters, or formatters were run by review policy; the supplied CI preflight recorded 15/15 successful checks, and the changed tests were statically inspected. The complete 27-file Round 0/1 scope, current/base migration graph, caller contracts, and complete review history were inspected; no dropped, fixed, or refactored root is being re-reported. The Simplification Lens was unavailable because of usage_limit_reached, so no simplification opportunity is asserted. Static verification cannot substitute for executing the migration or malformed-state cases locally; the requested regression tests are therefore part of the fixes above.

Blocking status & recommended decision

Blocking: yes
Recommended event: REQUEST_CHANGES

Blocking issues:

  • docs/deployment.md:179 — major — the documented downgrade traverses the Stripe sibling and can delete customized public_mcp_apps.app_id='stripe' data; [new]

Comment thread docs/deployment.md Outdated
1. Stop all workers before the downgrade.
2. If the database is SQLite, create a current database backup.
3. If the database is SQLite, run `PRAGMA integrity_check;` against the backup and record `SELECT count(*) FROM gmail_watch_states;`. The integrity result must be `ok`.
4. Run `alembic downgrade b1efe0dbe0af`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking [new] — alembic downgrade b1efe0dbe0af is not an OAuth-only rollback here: merge revision 0108d2704fc1 has both owner and Stripe parents (see src/xagent/migrations/versions/0108d2704fc1_merge_stripe_and_oauth_owner_heads.py:12-16), so this command also runs Stripe's downgrade and deletes public_mcp_apps.app_id='stripe'. Please either linearize the owner revision after 20260818_seed_stripe_mcp_app and remove the no-op merge, or target 20260818_seed_stripe_mcp_app and expect that head; add SQLite/PostgreSQL coverage preserving an edited Stripe row.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWmEw because it duplicates review-body finding PRR_kwDORQ4Kr88AAAABKfhWRA:N1, which requested the same Stripe-preserving rollback outcome (canonical review-body finding).

The referenced merge traversal no longer exists: the current owner revision follows 20260818_seed_stripe_mcp_app directly (current migration graph).

Comment thread src/xagent/web/api/auth.py Outdated
user_id = payload.get("user_id")
user_id_claim = payload.get("user_id")
try:
user_id = int(user_id_claim) if user_id_claim is not None else None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor [new] — int(user_id_claim) accepts True and floats such as 7.9 and can raise OverflowError for infinity, while the outer handler starts later. Require type(user_id_claim) is int and user_id_claim > 0 (with an explicit legacy None policy), reject/catch overflow before provider exchange, and add bool/float/infinity tests asserting invalid-state response, no provider call, and no database mutation.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWmFS because it requests the same exact pre-exchange user-ID validation and no-side-effect coverage as canonical finding PRR_kwDORQ4Kr88AAAABKfhWRA:N2 (canonical review-body finding).

This inline comment is the second occurrence of that same requested outcome (inline finding), so it does not require an independent resolution.

Comment thread docs/deployment.md Outdated

On SQLite the migration rejects globally colliding owner-index names before rebuilding the table in batch mode. Stop every worker before this rebuild and keep SQLite quiesced until the migration completes. Take and verify a database backup before the rebuild: under the driver's legacy transaction mode, SQLite DDL can commit independently of Alembic's outer transaction.

If the SQLite migration process exits after the rebuild starts, keep every worker stopped and retry `alembic upgrade head` once with the same release. The migration automatically completes only an unambiguous interrupted index-installation state: `resource_owner_key` has its expected nullable `VARCHAR(512)` definition, `uq_user_provider_account` is absent, and zero or one existing owner-aware index has the exact expected definition. It validates every existing owner index and creates only the missing definitions. Do not start workers until both owner-aware indexes pass the verification below.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor [new] — the runbook says recovery handles only zero or one owner index, but the migration safely accepts both exact replacement indexes when DDL completed before the revision stamp. Document zero/one/both, explain that both-present is a validated no-op, and add (ORDINARY_INDEX, ACTOR_INDEX) to the recovery parameterization in tests/alembic/test_20260818_add_user_oauth_resource_owner.py:258-264.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWmGT because it requests the same both-index recovery documentation and parameterized test as canonical finding PRR_kwDORQ4Kr88AAAABKfhWRA:N4 (canonical review-body finding).

This inline comment is the second occurrence of that same requested outcome (inline finding), so it does not require an independent resolution.

@rogercloud

Copy link
Copy Markdown
Collaborator

I reviewed PR #1588 at head f0e712f2 against the previous reviewed head 140e22ef. The staged owner-aware OAuth foundation is coherent overall, but the new merge head makes the documented rollback data-destructive.

Major — blocking

  • N1 [new], docs/deployment.md:179: alembic downgrade b1efe0dbe0af traverses both parents of merge revision 0108d2704fc1, including the already-existing Stripe seed revision. Stripe's downgrade deletes public_mcp_apps.app_id='stripe', so an administrator's customized catalog row can be lost. Linearize the owner revision after 20260818_seed_stripe_mcp_app or change the rollback target to that Stripe revision, and add SQLite/PostgreSQL preservation coverage.

Minor and nit findings

  • N2 [new], src/xagent/web/api/auth.py:1712: int() accepts booleans/floats and can let OverflowError escape before the outer handler. Require an exact positive int, reject overflow before provider exchange, and add bool/float/infinity malformed-state tests with no provider call or DB mutation.
  • N4 [new], docs/deployment.md:92: the runbook says recovery handles only zero or one owner index, but the code safely accepts both exact indexes before the revision stamp. Document zero/one/both and add the both-index recovery case to tests/alembic/test_20260818_add_user_oauth_resource_owner.py.
  • N5 [new], src/xagent/web/api/auth.py:82-126: the post-commit helper still advertises Any/raw-claim input and repeats int() after validation moved earlier. Update the type/docstring and remove the redundant conversion or document it.

Prior status highlights

P1, P2, and P5 were REFACTORED to the Gmail lifecycle stack in bsbds/xagent#82. P3, P4, P15, and P24–P29 were FIXED after current-code verification. P6–P14, P16–P21, P23, P30, and P31 were DROPPED as safe, non-actionable, or tracked; P21 remains tracked in issue #1534. P22 is NOT FIXED: the PR description still names 20260818_user_oauth_resource_owner as the sole head, but the current sole head is 0108d2704fc1; update the description.

No local tests were run under review policy. The supplied CI preflight recorded 15/15 successful checks, and tests were statically inspected. The Simplification Lens was unavailable due to usage_limit_reached; no simplification findings were inferred.

Blocking: yes
Recommended event: REQUEST_CHANGES

@rogercloud rogercloud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a history-update verification for PR #1588, not a replacement for the existing review. The PR establishes the owner-aware UserOAuth schema and service boundary, ordinary-only consumers, and migration/recovery support while actor writers remain staged for the downstream stack. Late review body 4998991825 arrived after the first history export and the first consolidated review; the late claims were independently checked against the current and base code.

Late-history verification

The independent checks retain only one new finding. The interrupted SQLite owner-index retry path has a real, minor collision-diagnostics defect; the late M2, M4, and downgrade-temp N2 claims do not add blocking issues, and M3 is a downstream rollout prerequisite rather than a new #1588 finding.

Late root Status Independent result Source IDs
M2 / late P3 FIXED (late blocking reopening dropped) Bare empty Alembic initialization is explicitly unsupported and now fails closed when users is absent; supported paths require the users table and the ON DELETE CASCADE invariant. 4998991825; related late occurrences 3835129384, 3835129387, 3835129392
M3 REFACTORED (out of scope for #1588) src/xagent/web/services/triggers.py:603 is a real latent owner-blind lookup, but that file is unchanged here and this PR has no actor-row writer. PR #82 fixes the ordinary-owner lookup and must land and be deployed before PR #78 enables actor-owned writers; treat that ordering as a hard rollout prerequisite. 4998991825
M4 DROPPED (duplicate of fixed P29) The callback commits after the bulk delete, and SQLAlchemy commit expiration makes the later db.get() reload SQL state. The test therefore does not rely on a permanently cached actor object; no duplicate finding is warranted. 4998991825, 3835129397
Late N1 CONFIRMED — minor, new On the interrupted-state retry, only missing owner indexes are computed before _create_owner_indexes; global SQLite table/index/view names are not preflighted. This is distinct from fixed P4/P24 and can produce an uncontextualized sqlite3.OperationalError. 4998991825, 3835129402
Late N2 (downgrade-temp) DROPPED The documented rollback procedure explicitly forbids retrying a changed database after an interrupted SQLite downgrade and requires restoring the verified backup first, so the missing retry guard is not an actionable supported-path defect. 4998991825, 3835129406

Minor — interrupted SQLite retry misses global relation collisions [new]

At src/xagent/migrations/versions/20260818_add_user_oauth_resource_owner.py:257, the interrupted-state retry calls _missing_owner_index_definitions(dialect) and passes the missing definitions directly to _create_owner_indexes. It does not check the global SQLite relation namespace first. If an unrelated table, index, or view already uses the name of a missing owner index, the retry raises a raw sqlite3.OperationalError instead of the guided collision error used by the fresh legacy-schema path; the migration remains recoverable, so this is minor. Intersect _sqlite_global_owner_relation_names() with only the missing owner-index names before _create_owner_indexes (valid owner indexes already present must not count as collisions), and add regression tests for unrelated TABLE, INDEX, and VIEW collisions while retaining an existing valid owner index. This is a distinct root from P4/P24.

The existing major Stripe rollback finding at docs/deployment.md:179 remains confirmed and blocking. The prior REQUEST_CHANGES review and its decision are unchanged; this follow-up does not re-report that root.

No local tests, builds, linters, or formatters were run under the review constraints. The supplied CI preflight was 15/15 successful, and the late paths and relevant tests were statically inspected.

Blocking status & recommended decision

Blocking: yes — carried forward solely by the existing major Stripe rollback finding at docs/deployment.md:179.

Recommended event for this follow-up: COMMENT. The existing REQUEST_CHANGES decision remains unchanged; the newly confirmed retry-collision issue is minor and non-blocking.

# the first replacement index is created. Existing indexes were
# validated above, so creating only the missing definitions safely
# completes that exact interrupted state without accepting drift.
_create_owner_indexes(missing_indexes)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor [new] — this interrupted-state retry computes only missing owner indexes and calls _create_owner_indexes without preflighting SQLite's global table/index/view namespace. If an unrelated relation already uses a missing owner-index name, retry raises a raw sqlite3.OperationalError instead of the guided collision error. Before _create_owner_indexes, intersect _sqlite_global_owner_relation_names() with only the missing owner-index names (do not treat already-present valid owner indexes as collisions), and add regression tests for unrelated TABLE, INDEX, and VIEW collisions.

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.

We are not making a separate change for finding PRRT_kwDORQ4Kr86bWtWZ because it duplicates review-body finding PRR_kwDORQ4Kr88AAAABKflIJA:Late-N1, which requested the same interrupted-repair collision preflight (canonical follow-up review finding).

The current retry path already filters SQLite's global relation namespace by only the missing owner-index names before creating indexes (current interrupted-repair logic).

@rogercloud

Copy link
Copy Markdown
Collaborator

Follow-up history verification for PR #1588: late review body 4998991825 was checked after the initial history export and first consolidated review. The existing REQUEST_CHANGES review remains unchanged, including the confirmed major Stripe rollback finding; this follow-up does not duplicate prior roots.

New minor finding

At src/xagent/migrations/versions/20260818_add_user_oauth_resource_owner.py:257 (late N1, inline source 3835129402), the interrupted SQLite owner-index retry computes only missing owner-index definitions and calls _create_owner_indexes without checking the global SQLite relation namespace. If an unrelated table, index, or view already has a missing owner-index name, retry fails with a raw sqlite3.OperationalError rather than the guided collision error used by the fresh-install path. This is distinct from fixed P4/P24 and remains a recoverable, minor diagnostics defect. Before _create_owner_indexes, intersect _sqlite_global_owner_relation_names() with only the missing owner-index names, then raise the guided collision error; add collision tests for unrelated TABLE, INDEX, and VIEW names while retaining one valid owner index.

Downstream rollout prerequisite

Late M3 (4998991825) is a real latent bypass in unchanged src/xagent/web/services/triggers.py:603: the Gmail trigger path currently looks up UserOAuth by ID without the ordinary-owner predicate. It is REFACTORED/out of scope for #1588, not a new blocking finding here, because this PR has no actor-row writer and PR #82 implements the scoped ordinary lookup. PR #82 must land and be deployed before PR #78 enables actor-owned writers; that ordering is a hard rollout gate. The unchanged triggers.py path is intentionally not given a new inline comment in this follow-up.

Late claims independently dropped or retained

  • M2 / late P3 (4998991825; late occurrences 3835129384, 3835129387, 3835129392) remains FIXED. Bare empty Alembic initialization is explicitly unsupported and now fails closed when users is absent; supported migration paths require users and the cascade invariant. The late proposal to reopen this as a major blocker is dropped.
  • M4 (4998991825, inline 3835129397) is DROPPED as a duplicate of fixed P29. The callback commits after the bulk delete, and commit expiration makes the subsequent db.get() reload database state, so the test can observe deletion rather than only an identity-map cache.
  • Late downgrade-temp N2 (4998991825, inline 3835129406) is DROPPED. The rollback documentation explicitly forbids retrying a changed database after an interrupted SQLite downgrade and requires restoring the verified backup first.

No local tests were run. The supplied CI preflight was 15/15 successful. A follow-up COMMENT review carries the one new minor inline finding; the existing REQUEST_CHANGES decision remains the blocking decision.

@bsbds

bsbds commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Finalization Summary

Push

  • Branch: split/pr1529-foundation
  • Head: aabf87d8b4c11783a53cfca62492ed93619b119b
  • Destination: personal/feat/user-oauth-owner-foundation (bsbds/xagent)
  • Result: success
  • Summary comment: this comment
  • PR description: not requested

Resolved

  1. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:M1
    Finding: Prevent the OAuth rollback procedure from also removing the Stripe catalog seed.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Major
    Change: 482c2161da1b097d3b4eda8181ff9c6373b2d6d4
    GitHub: report-only

  2. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:M2
    Finding: Add full-chain coverage and recovery guidance for unsupported pure-Alembic empty bootstrap.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 21d8a38dec83fcccc873040fd36eb8085ff33415
    GitHub: report-only

  3. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:N1
    Finding: Detect owner-index relation-name collisions during interrupted SQLite index repair.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 5cca0378a4e5a1edd77feb331bb302bba62fb85e
    GitHub: report-only

  4. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:N2
    Finding: Detect a leftover _alembic_tmp_user_oauth before SQLite downgrade retry.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 76ae32293c5b97af65050096e0a2803d50c63183
    GitHub: report-only

  5. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:N3
    Finding: Correct the Alembic-head claim in the PR description.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 482c2161da1b097d3b4eda8181ff9c6373b2d6d4 restored 20260818_user_oauth_resource_owner as the sole head, making the existing description claim accurate.
    GitHub: report-only

  6. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:L55
    Finding: Remove the always-true uniqueness field from OWNER_INDEX_DEFINITIONS.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: d11c7e91b0599b3212d98fe8dd8452cf858674f1
    GitHub: report-only

  7. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:L191
    Finding: Remove the unused PostgreSQL predicate fallback from the SQLite-only test helper.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: aabf87d8b4c11783a53cfca62492ed93619b119b
    GitHub: report-only

Rejected

  1. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:M3
    Finding: Scope the Gmail trigger account lookup before PR Xagent Development Roadmap #82.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: outdated
    Emergency level: Minor
    Reason: Foundation PR feat(auth): add owner-aware OAuth storage foundation #1588 intentionally leaves Gmail behavior unchanged and cannot create actor rows. PR Xagent Development Roadmap #82 already routes this exact lookup through get_scoped_user_oauth_account(..., resource_owner_key=None) and tests rejection of actor-owned Gmail rows.
    Sources: PR #82 trigger implementation; PR #82 regression test
    Claim-to-source mapping: Exact scoped lookup — PR Xagent Development Roadmap #82 trigger implementation; actor-row rejection — PR Xagent Development Roadmap #82 regression test.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  2. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:M4
    Finding: Add explicit expiration because the callback test allegedly reads only stale identity-map state.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: unsound
    Emergency level: Minor
    Reason: The callback commits before the assertion. The fixture does not override SQLAlchemy's default expire_on_commit=True, so db.get() refreshes expired state from the database. If the actor row were deleted, the existing assertions would fail.
    Sources: test session and assertion; callback commit; SQLAlchemy expiration behavior
    Claim-to-source mapping: Session uses defaults — test session and assertion; callback expires instances through commit — callback commit and SQLAlchemy expiration behavior; subsequent access reloads database state — SQLAlchemy expiration behavior.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  3. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:D1
    Finding: Add an explicit note that actor deletion depends on the database cascade.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: outdated
    Emergency level: Minor
    Reason: The relationship comment already states that actor deletion depends on ON DELETE CASCADE; SQLite and PostgreSQL behavior have dedicated tests.
    Sources: relationship documentation; cascade integration tests
    Claim-to-source mapping: Dependency is documented — relationship documentation; behavior is tested — cascade integration tests.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  4. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:D2
    Finding: Document the nullable-owner design rationale.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: outdated
    Emergency level: Minor
    Reason: The model already documents the deliberate difference from MCPOAuthGrant and explains that nullable ownership avoids backfilling existing rows.
    Sources: model rationale; deployment semantics
    Claim-to-source mapping: Design divergence and no-backfill reason — model rationale; operational meaning — deployment semantics.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  5. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:D3
    Finding: Add another comment distinguishing owner-key conventions in mcp.py.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: The model-level rationale is authoritative, the MCP grant response is explicitly non-null, and ordinary UserOAuth call sites already carry actor/ordinary comments. Another local comment would duplicate established documentation.
    Sources: model rationale; ordinary MCP lookup comment
    Claim-to-source mapping: Convention difference — model rationale; local ordinary-owner intent — ordinary MCP lookup comment.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  6. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:L5
    Finding: Inline the supported-dialect constant.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: The named constant communicates the shared startup/migration invariant. Inlining it removes a useful name without reducing meaningful complexity.
    Sources: shared invariant module
    Claim-to-source mapping: The module and constant define one shared invariant — shared invariant module.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  7. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:L15
    Finding: Remove the explicit string-type guard from dialect validation.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: unsound
    Emergency level: Minor
    Reason: The function deliberately accepts object and converts invalid types into the same RuntimeError. Membership testing an unhashable non-string against a frozenset can instead raise TypeError; removing the guard is not equivalent.
    Sources: runtime validation boundary; non-string test; Python set requirements
    Claim-to-source mapping: Object-valued validation contract — runtime validation boundary and non-string test; set membership requires hashable values — Python set requirements.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  8. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:L67
    Finding: Remove the migration's dialect-validation wrapper.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: The wrapper prevents duplicate bind/dialect extraction in upgrade and downgrade and names a migration-specific precondition. Removing it does not materially simplify the code.
    Sources: migration helper
    Claim-to-source mapping: The wrapper names and centralizes both call sites — migration helper.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  9. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:L71
    Finding: Replace _table_exists and _users_table_exists with a generic helper.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: The two names represent distinct migration gates. A generic helper merely trades semantic names for repeated table constants.
    Sources: table precondition helpers
    Claim-to-source mapping: Separate helpers encode separate preconditions — table precondition helpers.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  10. Finding ID: PRR_kwDORQ4Kr88AAAABKfaP0Q:L100
    Finding: Delete get_user_oauth_account_by_id because the foundation has no production caller.
    Source: review body PRR_kwDORQ4Kr88AAAABKfaP0Q, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: The helper intentionally has a different trust contract and is used by the immediate Gmail child PR. Deleting it would create remove-and-readd churn across the documented stack.
    Sources: helper contract; downstream production consumer; prior rejection
    Claim-to-source mapping: Distinct helper contract — helper contract; current stack consumer — downstream production consumer; prior trust-contract rejection — prior rejection.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  11. Finding ID: PRRT_kwDORQ4Kr86bWYo8
    Finding: Prevent rollback from deleting the Stripe catalog entry.
    Source: inline comment PRRC_kwDORQ4Kr87kl24l, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYo8, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Major
    Reason: Same rollback outcome as canonical M1.
    Sources: canonical review; Stripe downgrade
    Claim-to-source mapping: Duplicate request — canonical review; collateral deletion — Stripe downgrade.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:M1
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved; thread became outdated after the merge revision was removed

  12. Finding ID: PRRT_kwDORQ4Kr86bWYo_
    Finding: Test and document the empty-bootstrap fail-closed path.
    Source: inline comment PRRC_kwDORQ4Kr87kl24o, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYo_, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same remaining coverage and recovery outcome as canonical M2.
    Sources: canonical review; startup contract
    Claim-to-source mapping: Duplicate request — canonical review; unsupported empty-bootstrap architecture — startup contract.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:M2
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  13. Finding ID: PRRT_kwDORQ4Kr86bWYpE
    Finding: Preserve a full-chain genuinely empty database test.
    Source: inline comment PRRC_kwDORQ4Kr87kl24r, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpE, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same full-chain negative-coverage outcome as canonical M2.
    Sources: canonical review; current supported-path test
    Claim-to-source mapping: Duplicate request — canonical review; current test covers only core-table-present startup — current supported-path test.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:M2
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  14. Finding ID: PRRT_kwDORQ4Kr86bWYpJ:finding-1
    Finding: Add the empty-bootstrap CLI case.
    Source: inline comment PRRC_kwDORQ4Kr87kl24w, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpJ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same negative full-chain coverage as canonical M2.
    Sources: canonical review
    Claim-to-source mapping: Same required full-chain test — canonical review.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:M2
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  15. Finding ID: PRRT_kwDORQ4Kr86bWYpJ:finding-2
    Finding: Assert the Stripe row's fate during rollback.
    Source: inline comment PRRC_kwDORQ4Kr87kl24w, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpJ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Major
    Reason: Same rollback preservation and testing outcome as canonical M1.
    Sources: canonical review
    Claim-to-source mapping: Same rollback outcome — canonical review.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:M1
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  16. Finding ID: PRRT_kwDORQ4Kr86bWYpM
    Finding: Explicitly expire the callback test session before checking the actor row.
    Source: inline comment PRRC_kwDORQ4Kr87kl241, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpM, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of M4's incorrect identity-map premise.
    Sources: callback commit; SQLAlchemy expiration behavior
    Claim-to-source mapping: Commit expires session state by default — callback commit and SQLAlchemy expiration behavior.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:M4
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  17. Finding ID: PRRT_kwDORQ4Kr86bWYpP
    Finding: Detect relation-name collisions during interrupted index repair.
    Source: inline comment PRRC_kwDORQ4Kr87kl246, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpP, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as canonical N1.
    Sources: canonical review
    Claim-to-source mapping: Same interrupted-repair diagnostic — canonical review.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:N1
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  18. Finding ID: PRRT_kwDORQ4Kr86bWYpR
    Finding: Detect leftover Alembic temporary tables during downgrade.
    Source: inline comment PRRC_kwDORQ4Kr87kl24-, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpR, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as canonical N2.
    Sources: canonical review
    Claim-to-source mapping: Same interrupted-downgrade diagnostic — canonical review.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:N2
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  19. Finding ID: PRRT_kwDORQ4Kr86bWYpU:finding-1
    Finding: Add a comment warning that the historical migration depends on an application-module import path.
    Source: inline comment PRRC_kwDORQ4Kr87kl25D, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpU, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: non-actionable
    Emergency level: Minor
    Reason: The import deliberately shares one startup/migration invariant and the helper module already documents that purpose. A comment cannot enforce future path compatibility; a compatibility shim would be required if the module is ever moved.
    Sources: shared invariant module; prior disposition
    Claim-to-source mapping: Shared purpose is documented — shared invariant module; import concern was previously accepted as intentional — prior disposition.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  20. Finding ID: PRRT_kwDORQ4Kr86bWYpU:finding-2
    Finding: Inline the supported-dialect constant.
    Source: inline comment PRRC_kwDORQ4Kr87kl25D, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpU, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same outcome as canonical L5.
    Sources: shared invariant module
    Claim-to-source mapping: Named shared invariant — shared invariant module.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:L5
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  21. Finding ID: PRRT_kwDORQ4Kr86bWYpU:finding-3
    Finding: Remove explicit non-string dialect validation.
    Source: inline comment PRRC_kwDORQ4Kr87kl25D, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWYpU, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Same technically unsound outcome as canonical L15.
    Sources: validation helper; Python set requirements
    Claim-to-source mapping: Runtime type boundary — validation helper; unhashable membership behavior — Python set requirements.
    Related finding: PRR_kwDORQ4Kr88AAAABKfaP0Q:L15
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

Unresolved

None.

Totals

  • Current findings: 28
  • Resolved: 7
  • Rejected: 21
  • Unresolved: 0

@bsbds

bsbds commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Finalization Summary

Push

  • Branch: split/pr1529-foundation
  • Head: 8f50a03dca5fbff33a6d31c47a319a25394d11ab
  • Destination: personal/feat/user-oauth-owner-foundation (bsbds/xagent)
  • Result: success
  • Summary comment: this comment
  • PR description: not requested

Resolved

  1. Finding ID: PRR_kwDORQ4Kr88AAAABKfhWRA:N2
    Finding: Reject malformed OAuth-state user IDs before provider exchange.
    Source: review body PRR_kwDORQ4Kr88AAAABKfhWRA, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: aa5f1ea2b98e3921353a14bc70196a5249a784da
    GitHub: report-only

  2. Finding ID: PRR_kwDORQ4Kr88AAAABKfhWRA:N4
    Finding: Document and test the valid both-owner-indexes-present recovery state.
    Source: review body PRR_kwDORQ4Kr88AAAABKfhWRA, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 324b28d3ce98629424cd119cfce2a691623e1605
    GitHub: report-only

  3. Finding ID: PRR_kwDORQ4Kr88AAAABKfhWRA:N5
    Finding: Align the post-commit OAuth helper contract with its validated input.
    Source: review body PRR_kwDORQ4Kr88AAAABKfhWRA, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: resolved
    Classification: sound/actionable
    Emergency level: Minor
    Change: 8f50a03dca5fbff33a6d31c47a319a25394d11ab
    GitHub: report-only

Rejected

  1. Finding ID: PRR_kwDORQ4Kr88AAAABKfhWRA:N1
    Finding: Prevent OAuth rollback from deleting customized Stripe catalog data.
    Source: review body PRR_kwDORQ4Kr88AAAABKfhWRA, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: outdated
    Emergency level: Minor
    Reason: The review targeted f0e712f2. Current history linearizes the owner revision after Stripe, removes the merge revision, targets Stripe during rollback, and verifies that Stripe remains installed. The original destructive traversal no longer exists. Additional custom-value and duplicate PostgreSQL assertions would not cover a distinct migration-graph branch.
    Sources: current migration graph; current rollback; preservation test
    Claim-to-source mapping: Linear graph — current migration graph; safe rollback target — current rollback; regression protection — preservation test.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  2. Finding ID: PRR_kwDORQ4Kr88AAAABKfhWRA:P22
    Finding: Update the PR description's sole-head revision.
    Source: review body PRR_kwDORQ4Kr88AAAABKfhWRA, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: outdated
    Emergency level: Minor
    Reason: The graph changed after this review. 20260818_user_oauth_resource_owner is again the sole head, so the existing PR-description statement is accurate.
    Sources: current owner revision; current PR description
    Claim-to-source mapping: Sole-head graph — current owner revision; matching description — current PR description.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  3. Finding ID: PRRT_kwDORQ4Kr86bWmEw
    Finding: Prevent Stripe deletion during OAuth rollback.
    Source: inline comment PRRC_kwDORQ4Kr87kmMkX, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWmEw, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of current finding PRR_kwDORQ4Kr88AAAABKfhWRA:N1, whose referenced behavior is now fixed.
    Sources: review-body finding; current graph
    Claim-to-source mapping: Duplicate outcome — review-body finding; obsolete merge traversal — current graph.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:N1
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved; thread is outdated

  4. Finding ID: PRRT_kwDORQ4Kr86bWmFS
    Finding: Reject malformed numeric OAuth-state claims.
    Source: inline comment PRRC_kwDORQ4Kr87kmMlW, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWmFS, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of resolved finding PRR_kwDORQ4Kr88AAAABKfhWRA:N2.
    Sources: review-body finding; inline finding
    Claim-to-source mapping: Both request exact pre-exchange claim validation — review-body and inline findings.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:N2
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved; thread is outdated

  5. Finding ID: PRRT_kwDORQ4Kr86bWmGT
    Finding: Cover the both-indexes-present SQLite recovery state.
    Source: inline comment PRRC_kwDORQ4Kr87kmMm8, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWmGT, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of resolved finding PRR_kwDORQ4Kr88AAAABKfhWRA:N4.
    Sources: review-body finding; inline finding
    Claim-to-source mapping: Both request the same recovery documentation and parameterized test — review-body and inline findings.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:N4
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved; thread is outdated

  6. Finding ID: IC_kwDORQ4Kr88AAAABQI8eOQ:N1
    Finding: Preserve Stripe during OAuth rollback.
    Source: issue comment IC_kwDORQ4Kr88AAAABQI8eOQ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: Not applicable
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of current finding PRR_kwDORQ4Kr88AAAABKfhWRA:N1.
    Sources: review-body finding; summary issue comment
    Claim-to-source mapping: Same rollback outcome — review-body finding and summary issue comment.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:N1
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  7. Finding ID: IC_kwDORQ4Kr88AAAABQI8eOQ:N2
    Finding: Reject malformed OAuth-state user IDs.
    Source: issue comment IC_kwDORQ4Kr88AAAABQI8eOQ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: Not applicable
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of resolved finding PRR_kwDORQ4Kr88AAAABKfhWRA:N2.
    Sources: review-body finding; summary issue comment
    Claim-to-source mapping: Same validation outcome — review-body finding and summary issue comment.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:N2
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  8. Finding ID: IC_kwDORQ4Kr88AAAABQI8eOQ:N4
    Finding: Cover both-index recovery.
    Source: issue comment IC_kwDORQ4Kr88AAAABQI8eOQ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: Not applicable
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of resolved finding PRR_kwDORQ4Kr88AAAABKfhWRA:N4.
    Sources: review-body finding; summary issue comment
    Claim-to-source mapping: Same recovery outcome — review-body finding and summary issue comment.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:N4
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  9. Finding ID: IC_kwDORQ4Kr88AAAABQI8eOQ:N5
    Finding: Correct the post-commit helper contract.
    Source: issue comment IC_kwDORQ4Kr88AAAABQI8eOQ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: Not applicable
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of resolved finding PRR_kwDORQ4Kr88AAAABKfhWRA:N5.
    Sources: review-body finding; summary issue comment
    Claim-to-source mapping: Same helper-contract outcome — review-body finding and summary issue comment.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:N5
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  10. Finding ID: IC_kwDORQ4Kr88AAAABQI8eOQ:P22
    Finding: Correct the PR description's Alembic head.
    Source: issue comment IC_kwDORQ4Kr88AAAABQI8eOQ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: Not applicable
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of outdated finding PRR_kwDORQ4Kr88AAAABKfhWRA:P22.
    Sources: review-body finding; current owner revision
    Claim-to-source mapping: Duplicate request — review-body finding; current sole head — current owner revision.
    Related finding: PRR_kwDORQ4Kr88AAAABKfhWRA:P22
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  11. Finding ID: PRR_kwDORQ4Kr88AAAABKflIJA:Late-N1
    Finding: Preflight relation-name collisions during interrupted SQLite index repair.
    Source: review body PRR_kwDORQ4Kr88AAAABKflIJA, feat(auth): add owner-aware OAuth storage foundation #1588 (review)
    Thread: Not applicable
    Local outcome: rejected
    Classification: outdated
    Emergency level: Minor
    Reason: Current head filters the global SQLite namespace by only the missing owner-index names before creating indexes. Regression coverage preserves one valid owner index while detecting a missing-name collision. Existing fresh-path tests cover INDEX, TABLE, and VIEW relation types.
    Sources: current repair logic; interrupted-state regression; TABLE/VIEW coverage
    Claim-to-source mapping: Missing-name filtering — current repair logic; retained valid index — interrupted-state regression; relation types — TABLE/VIEW coverage.
    Related finding: N/A
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

  12. Finding ID: PRRT_kwDORQ4Kr86bWtWZ
    Finding: Detect interrupted-repair global relation collisions.
    Source: inline comment PRRC_kwDORQ4Kr87kmXfg, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: PRRT_kwDORQ4Kr86bWtWZ, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of outdated finding PRR_kwDORQ4Kr88AAAABKflIJA:Late-N1.
    Sources: review-body finding; current repair logic
    Claim-to-source mapping: Duplicate outcome — review-body finding; implemented behavior — current repair logic.
    Related finding: PRR_kwDORQ4Kr88AAAABKflIJA:Late-N1
    Tracking issue: Not applicable
    Reply: posted, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    GitHub: left unresolved

  13. Finding ID: IC_kwDORQ4Kr88AAAABQJCEog
    Finding: Detect interrupted-repair global relation collisions.
    Source: issue comment IC_kwDORQ4Kr88AAAABQJCEog, feat(auth): add owner-aware OAuth storage foundation #1588 (comment)
    Thread: Not applicable
    Local outcome: rejected
    Classification: duplicate
    Emergency level: Minor
    Reason: Duplicate of outdated finding PRR_kwDORQ4Kr88AAAABKflIJA:Late-N1.
    Sources: review-body finding; summary issue comment; current repair logic
    Claim-to-source mapping: Duplicate request — review-body finding and summary issue comment; implemented behavior — current repair logic.
    Related finding: PRR_kwDORQ4Kr88AAAABKflIJA:Late-N1
    Tracking issue: Not applicable
    Reply: Not applicable — no inline thread
    GitHub: report-only

Unresolved

None.

Totals

  • Current findings: 16
  • Resolved: 3
  • Rejected: 13
  • Unresolved: 0

@bsbds
bsbds requested a review from rogercloud August 22, 2026 09:16
@bsbds

bsbds commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Final concern disposition

This summary applies to commit a6357ac4. All seven reviewed concerns are handled in the current PR.

Review contract and scope boundaries

These dispositions depend on the following accepted contracts:

  • Production deployments, including self-hosted production, use PostgreSQL. SQLite is limited to local development and CI.
  • Empty databases must use normal application startup. Bare Alembic supports only an already initialized application schema.
  • This PR does not enable actor-owned credential writers. PR Xagent Development Roadmap #82 must land before any writer stores a non-null resource_owner_key.
  • A missing callback user_id remains accepted for legacy compatibility. This path does not persist credentials or run user post-commit work. Normal login does not issue such states.
  • SQLite automatically repairs only an unambiguous interrupted index installation. Other partial, conflicting, or temporary-table states require restoration or operator inspection.
  • The migration supports repository-produced legacy schemas. It rejects arbitrary malformed or manually modified schemas instead of attempting speculative repair.
  • Application startup provides migration locking and SQLite foreign-key checks. Standalone Alembic execution relies on the documented operator procedure.
  • xagent.db.migration_support is a stable historical-migration dependency. Its path and dialect contract must remain available for future migration replay.

A disagreement with one of these contracts is a product or deployment decision, not an unresolved implementation defect in this PR.

1. PostgreSQL migration safety — Handled

The owner migration follows the Stripe seed and keeps one Alembic head. PostgreSQL runs the revision in one transaction.

The migration adds the nullable owner column and the required cascade foreign key. It creates both partial unique indexes before it removes the old constraint. A failure rolls back the complete revision.

The migration does not backfill or rewrite credential rows. Downgrade stops before schema changes when actor-owned rows exist. This prevents namespace collapse and credential loss.

The deployment guide requires a controlled OAuth migration window. It also describes lock impact, relation-name collisions, rollback behavior, and post-migration checks.

2. Unsupported bare-empty Alembic initialization — Handled

Bare Alembic is not a supported initializer for an empty application database. Normal startup stamps an empty database first. It then creates the complete metadata-owned schema.

A bare-empty run fails closed when the owner revision finds user_oauth without users. It does not install a partial owner schema or advance the revision stamp.

The deployment guide tells operators not to create users manually. Disposable databases must use normal startup. Non-disposable databases require backup restoration or operator inspection.

3. Nullable owner representation — Handled

resource_owner_key is a nullable VARCHAR(512) column without a default. Existing rows remain NULL, and the migration performs no backfill.

A null owner identifies an ordinary credential. A non-null owner identifies an actor credential. Two disjoint partial unique indexes enforce these namespaces.

Legacy behavior for a null provider_user_id remains unchanged. Existing ordinary readers and writers explicitly use resource_owner_key=None. This PR does not enable an actor-owned credential writer.

4. Filtered ORM relationship and database cascade — Handled

User.oauth_accounts contains only rows where resource_owner_key IS NULL. Both relationship directions disable back-reference synchronization.

An actor row cannot enter the ordinary delete-orphan collection through normal relationship assignment. Actor-row cleanup depends on the database foreign key instead.

The model declares user_oauth.user_id -> users.id ON DELETE CASCADE. The migration installs or verifies that exact foreign key. It rejects an owner-aware schema that lacks the cascade.

The tests cover ordinary collection filtering and deletion of ordinary and actor rows on PostgreSQL and SQLite.

5. Legacy callback None policy — Handled

The callback validates each non-null user_id claim before token exchange or database mutation. It accepts only a positive integer within the database range.

The callback rejects strings, booleans, floats, zero, negative values, infinity, and oversized integers. Rejected claims do not call the provider or create OAuth rows.

The legacy missing or None claim behavior remains unchanged. That path can complete the provider flow but skips persistence and post-commit user work. Normal login does not issue new states with a missing user ID.

6. SQLite recovery implementation — Handled

SQLite recovery repairs only an unambiguous interrupted index-installation state. Existing indexes must match the expected columns, uniqueness, and predicate.

Collision checks now use SQLite's case-insensitive identifier rules. They detect case variants of conflicting tables, indexes, views, and Alembic temporary tables.

Error messages preserve the stored spelling of the conflicting object. The migration rejects collisions before a batch rebuild or repair attempt.

Normal application startup guards foreign-key state around batch migration. Downgrade rejects actor-owned rows. The deployment guide limits SQLite to local development and CI.

The new case-variant regression started with five expected failures. The final related migration run passed all 68 tests.

7. Historical migration import coupling — Handled

The historical revision imports only xagent.db.migration_support. This neutral module contains the shared dialect allowlist and its validation function.

The helper has no model imports, configuration reads, database connections, or startup side effects. The package and migration revision ship together in supported installations.

Startup and direct Alembic execution therefore enforce the same dialect contract. The stable helper path remains a maintenance constraint, but no current replay or packaging defect exists.

Final result

No reviewed concern needs another product contract decision or manual clarification. PR #82 must still land before any actor-owned credential writer becomes active.

Local verification at a6357ac4:

  • Related migration tests: 68 passed
  • Pre-commit hooks: passed
  • git diff --check: passed

@bsbds
bsbds requested review from rogercloud and removed request for rogercloud August 22, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants