Skip to content

Complete the authenticated, persistent Google review pilot - #5

Open
berry-13 wants to merge 15 commits into
devfrom
codex/production-pilot
Open

berry-13 wants to merge 15 commits into
devfrom
codex/production-pilot

Conversation

@berry-13

@berry-13 berry-13 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

What changes

The review workflow now persists tenant-scoped state rather than relying on a volatile demo store. Authenticated web and mobile screens expose Google onboarding, review operations, approved knowledge, rules, preferences and audit, with explicit loading/error/retry states and no silent demo fallback.

Publication uses an atomic state-and-intent commit, canonical Google revalidation and confirmation. A PUT accepted by Google with a lost response stays recoverable: reconciliation only performs GET and never blindly republishes. Stale or concurrent decisions cannot produce competing replies. Rejected unchanged reviews remain rejected on later duplicate events.

approval(version) → atomic publishing + intent → canonical GET → one PUT → confirmation GET
                                                     uncertain outcome → GET-only reconciliation

Knowledge approval indexes versioned, valid, location-scoped sources using PostgreSQL full-text and pgvector with a configurable Vertex embedding adapter. Sources are revalidated before sending. PDF/DOCX extraction is isolated in a bounded child process. Push submissions have a content-free persistent outbox, inspected Expo tickets, invalid-device removal and scheduled retry.

Identity Platform/TOTP, current account/revocation checks, KMS token encryption and forced RLS protect the controlled pilot. Schema migrations use a separate identity and database role; runtime startup refuses privileged or table-owner credentials. Terraform includes dashboard/worker/identity, bootstrap migrations, retention jobs and initial operational alerts. English README/setup/security documentation describes actual capabilities and external release gates.

Verification

  • Biome, all seven workspace typechecks and builds pass.
  • 74 automated tests pass; one optional TCP PostgreSQL test is skipped without TEST_DATABASE_URL.
  • Embedded PostgreSQL/pgvector tests exercise isolation, CAS rollback, audit immutability, expiry and hybrid retrieval.
  • Six Playwright desktop/mobile-web workflows pass.
  • Android and iOS Hermes exports pass (not signed APK/IPA builds).
  • Terraform fmt/validate pass; no cloud apply was executed.

Pilot boundaries

No approved Google Business Profile project or real location is available yet. Live Google/OAuth/PubSub, KMS/Cloud SQL/provider acceptance, physical push/device tests, signing/store release and privacy/Google operating-model approval remain external gates. Automatic publication is disabled by default and blocked by an operator release flag. This is a production-oriented controlled pilot, not a claim of production certification or a complete commercial SaaS.

Simulated onboarding also uses canonical Google resource names and the configured callback. Integration tests cover OAuth nonce replay rejection, authorized/consented import, no-send historical synchronization and immediate local disconnection.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T15:42:31.609510Z 416ce93 Manual request
🔒 Security Review Completed 2026-09-16T15:27:18.403477Z 80d71aa PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the exact remote head 416ce93e2ddf8d8705a7cb2812df10a8cb11fdbb, now integrated with current dev (4284bcc). Local verification: 70 tests passing, six desktop/mobile-web browser tests, seven workspace typechecks/builds, native Hermes exports, Terraform fmt/validate and production dependency audit. Pre-PUT failures recover to needs_attention; potentially accepted PUTs retain intent and only reconcile by GET. No live deployment or Google publication was performed.

Copy link
Copy Markdown
Collaborator Author

@codex security review

Please assess the exact remote head 416ce93e2ddf8d8705a7cb2812df10a8cb11fdbb, including tenant/RLS runtime privileges, identity revocation and MFA, web/native session races, OAuth nonce and resource authorization, uncertain publication, knowledge grounding, upload isolation and retention. This is a controlled single-workspace pilot; production acceptance still requires approved Google access and live-service/device evidence. Do not treat a review of an earlier head as coverage for this head.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

@Get("callback")
@Public()

P1 Badge Revalidate identity and MFA at the OAuth callback

The callback is public and treats the stored state record as sufficient authorization, so an owner can mint state without MFA because /start is a GET, and that state remains usable for ten minutes even if the account is subsequently disabled, its sessions are revoked, or its role is removed. In either case the callback can still replace the workspace's Google credentials; bind the nonce to the Identity UID/authentication context and revalidate the current account and required MFA immediately before storing tokens.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/src/store.ts
Comment on lines +125 to +129
const ownPublishedReply =
existing.value.status === "published" &&
existing.value.publishedReply === snapshot.existingReply &&
existing.value.snapshot.comment === snapshot.comment &&
existing.value.snapshot.starRating === snapshot.starRating;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconcile reply notifications received during publication

If Google's updated-review notification arrives after updateReply succeeds but before confirmPublished, the existing case is still publishing, so this condition is false and the webhook changes it to needs_attention, clears the draft, and advances its version. The in-flight confirmation then fails its version check, and later approval cannot enter the reconciliation branch because the case is no longer publishing, leaving a successfully posted reply permanently unconfirmed in local state. Treat a canonical reply matching the persisted publication intent as owned while publication is in progress.

AGENTS.md reference: AGENTS.md:L49-L50

Useful? React with 👍 / 👎.

Comment on lines +293 to +295
const sources = await this.store.listKnowledge(principal.tenantId);
if (
review.activeDraft.knowledgeSourceIds.some((sourceId) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revalidate every knowledge source supplied to the model

Publication checks only source IDs that the model returned in knowledgeSourceIds, even though generation supplied every retrieved source and persisted all of their versions in review.knowledgeVersions. With two retrieved sources, a valid model response can cite one while its text was influenced by the other; retiring, expiring, or editing the omitted source then does not invalidate the draft, so it can be published using knowledge that is no longer approved. Iterate the persisted input-source versions rather than trusting the model's citation subset.

AGENTS.md reference: AGENTS.md:L48-L48

Useful? React with 👍 / 👎.

Comment on lines +463 to +465
} catch (error) {
await this.store.releaseEvent(principal.tenantId, envelope.message.messageId);
throw error;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Audit webhook failures that occur before ingestion

When token refresh or the canonical Google getReview fails before ingestAndGenerate, this catch only releases the event lease and rethrows, so no audit record is produced for the failed asynchronous operation. Pub/Sub can retry and the review remains recoverable, but the required operational evidence is absent; append a metadata-only failure audit with a safe error classification before releasing the lease.

AGENTS.md reference: AGENTS.md:L49-L50

Useful? React with 👍 / 👎.

Comment on lines +122 to 124
private async send(tenantId: string, review: ReviewCase) {
const registrations = await this.store.listDeviceRegistrations(tenantId);
const userIds = [...new Set(registrations.map((registration) => registration.userId))];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop sending notifications to revoked users' devices

Device registrations are selected solely by tenant and are never checked against the user's current Identity account or grants. Disabling an account, revoking its sessions, or removing its workspace role therefore leaves its registered phones receiving future review-arrival and scheduled-publication metadata until Expo declares the token invalid or the entire Google integration is disconnected; remove per-user registrations during revocation or validate recipients against current identities before sending.

Useful? React with 👍 / 👎.

Comment thread apps/web/lib/session.ts
Comment on lines +59 to +63
// Do not issue Set-Cookie from a background refresh: an older in-flight response
// must not recreate a browser session after DELETE /api/session cleared it.
// Identity refresh tokens remain valid until account/session revocation.
try {
return (await pending).idToken;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Persist refreshed browser sessions without reviving logout

Once the cookie's original ID token reaches its one-hour expiry, this path returns the refreshed ID token but deliberately does not persist the new token, refresh token, or expiry. Consequently every subsequent proxied API request performs another Secure Token exchange, creating sustained Identity quota/latency pressure for every long-lived browser session and potentially logging the user out if the service rotates the refresh token. Preserve the logout generation/race protection while safely committing the refreshed session.

Useful? React with 👍 / 👎.

Comment thread infra/terraform/main.tf
Comment on lines 535 to +538
push_endpoint = "${google_cloud_run_v2_service.worker.uri}/events/google-business"
oidc_token {
service_account_email = google_service_account.push.email
audience = google_cloud_run_v2_service.worker.uri
audience = var.worker_public_url

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Align the worker push endpoint and OIDC audience

The Pub/Sub request targets the generated Cloud Run service URI but mints its ID token for the separately configured worker_public_url, which the supplied Terraform example sets to a custom origin. No Cloud Run custom audience is configured in this repository, so Cloud Run rejects the request before the worker can perform its own OIDC check; the Scheduler and notification-retry jobs repeat the same mismatch. Use the service URI as the audience or explicitly configure the custom audience for the service.

Useful? React with 👍 / 👎.

Comment on lines +222 to +224
await client.query(
"DELETE FROM runtime_knowledge_chunks WHERE tenant_id=$1 AND source_id=$2 AND source_version=$3",
[tenantId, sourceId, version],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove superseded document chunks during source lifecycle changes

Each approval writes a new source version, but this deletion removes only rows for that new version before reinserting them. Chunks from every earlier approved version therefore remain indefinitely, and editing or retiring the source has no cleanup path, preserving superseded uploaded document text and embeddings while causing unbounded storage growth. Delete obsolete versions when the new version commits and remove or expire all source chunks when the source is retired.

Useful? React with 👍 / 👎.

Comment on lines +336 to +337
globalKillSwitch:
settings.killSwitch || process.env.AUTOMATION_RELEASE_APPROVED !== "true",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make kill-switch changes invalidate in-flight automatic sends

An automatic task reads the kill switch and rule once, then reserves a slot, starts publication, fetches Google state, and performs the reply PUT without binding those authorization inputs to the publication transition. If an owner activates the kill switch or disables the rule after this read but before the PUT, the task still publishes, so the emergency stop is not deterministic for an operation that has not yet reached Google. Bind settings/rule versions into the atomic publication claim or revalidate them through a mechanism that invalidates the in-flight claim before writing.

AGENTS.md reference: AGENTS.md:L47-L47

Useful? React with 👍 / 👎.

Comment thread apps/api/src/store.ts
Comment on lines +387 to 392
async setGoogleTokens(tenantId: string, tokens: GoogleTokens) {
const current = await this.getGoogleTokens(tenantId);
const value = {
...tokens,
refreshToken: tokens.refreshToken ?? current?.refreshToken ?? null,
expiresAt: Date.now() + Math.max(60, tokens.expiresIn - 60) * 1_000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Never reuse a refresh token across OAuth authorizations

During reconnection, an authorization-code exchange that returns no refresh token is merged with the previous connection's refresh token. If the owner authorized a different Google identity, the stored access token initially represents the new account, but after expiry currentAccessToken refreshes with the old account's credential and silently switches identities; stale locations from that account can then become operable again. Treat a new OAuth exchange as a complete credential boundary and fail closed when it lacks the required offline token rather than inheriting one from another authorization.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants