Realtime gateway: sharee access-check endpoint + SSE reconnect cursor#2336
Realtime gateway: sharee access-check endpoint + SSE reconnect cursor#2336mrkoreye wants to merge 22 commits into
Conversation
…sharee visibility
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Here's a visual recap of what changed: Open the full interactive recap |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Code reviewTwo minor, non-blocking suggestions:
agent-native/packages/core/src/server/short-lived-token.ts Lines 394 to 438 in be9709b (compare with the sibling check it omits: agent-native/packages/core/src/server/short-lived-token.ts Lines 314 to 317 in be9709b
agent-native/packages/core/src/server/gateway-access-check.ts Lines 1 to 8 in be9709b agent-native/packages/core/src/server/short-lived-token.ts Lines 327 to 334 in be9709b |
This comment has been minimized.
This comment has been minimized.
|
One additional cursor-contract issue to fix or explicitly resolve before enabling this path: The new hosted SSE reconnect URL now sends the client cursor via Please use a DB-assigned monotonic sequence, or overlap reads below the cursor and deduplicate by deterministic event ID, before treating reconnect replay as lossless. This is separate from the existing stale- References: client reconnect change, version allocation, gateway implementation. |
ed98910 to
be9709b
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ent-native into korey/moar-updates-realtime
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Both done, plus the stale-stream item from the follow-up review:
|
Agreed it had to be resolved, so I implemented it in this PR rather than deferring. New Design notes, since you called out both candidate mechanisms:
Gating: app instances enable it automatically under hosted transport; the gateway side is a one-line opt-in in ai-services that lands with the core version bump, and the LD flag stays off until both are in. Covered by 11 tests including two skewed writers sharing one allocator and the reconnect spec. |
|
@steve8708 made some big changes based on the suggestions, this PR could use a full new review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Builder reviewed your changes and found 1 potential issue 🔴
Review Details
Code Review Summary
This incremental review covers the expanded PR #2336 scope: opt-in Postgres DB-assigned sync versions, serialized/deferred durable event writes, clock fallback behavior, hosted SSE reconnect hardening, and the gateway access-check route. The allocator's single-statement update/insert, deterministic-ID winner-version behavior, seed alignment, and hosted-only gating are generally sound. The existing local path remains unchanged when hosted transport configuration is absent. Two reviewers independently examined the allocator and poll flow; one confirmed a high-severity cursor-integrity issue in the fallback path, while another found no issue and specifically considered the allocator, dedupe, seed, and reconnect behavior sound.
Key Finding
🔴 HIGH — Fallback version can advance a client cursor beyond durable DB versions. If the allocating statement commits but the client call times out, or if allocation fails transiently, recordWithDbVersion emits a locally clock-assigned version while the durable row may retain a lower DB-assigned version and the shared allocator is not advanced to the emitted fallback value. A client receiving that event advances its max-only cursor; subsequent events from another writer with versions below that fallback cursor can then be filtered permanently. Reusing the event ID prevents duplication but does not restore the version invariant. The fallback should recover/reuse the durable row's version when possible, or advance the shared allocator before exposing the fallback version.
Risk level: High, due to shared realtime data ordering and authorization-sensitive changes. The HMAC access-check and stale EventSource protections remain well-tested. Browser verification was attempted, but Chrome automation was unavailable; server-side route checks and 43 targeted unit tests passed.
| if (version == null) { | ||
| // Availability fallback: a DB blip must not stall the in-process fast | ||
| // path. Clock-allocate and emit; the allocator's GREATEST(v+1, now) | ||
| // re-aligns the domain on recovery. The ordering guarantee is soft | ||
| // exactly while the DB is unhealthy. | ||
| this.dbVersionFallbacks++; | ||
| if (this.dbVersionFallbacks === 1) { | ||
| console.warn( | ||
| "[agent-native] sync version allocation failed; falling back to clock-assigned versions", | ||
| ); | ||
| } | ||
| this.version = Math.max(this.version + 1, Date.now()); | ||
| const entry: ChangeEvent = { ...event, version: this.version }; | ||
| this.commitEntryForChain(entry); | ||
| void this.persistSyncEvent(entry, dedupeKey, id); |
There was a problem hiding this comment.
🔴 Do not emit a fallback cursor above the shared allocator
If the allocating statement commits but the client call times out, or allocation fails after the allocator has advanced, this fallback emits a local clock version while persistSyncEvent can leave the durable row at its lower DB-assigned version. A client can advance its max-only cursor past subsequent events from other writers and permanently filter them. Recover the committed row's version/retry the same ID before emitting, or advance the shared allocator to the fallback version before exposing it.
There was a problem hiding this comment.
Good catch, this was the real gap left after the id-reuse fix. Two changes: on a failed allocating call the writer first reads back the row by the shared id, so a commit-then-timeout emits the actual allocator-assigned version instead of a clock one (no fallback at all in that case). Only a genuinely unrecoverable failure clock-falls-back now, and it lifts the shared allocator to the fallback value before emitting, so other writers' next allocations land above any cursor the emit advances. A hard-down DB is still the documented soft window since the lift fails with everything else. Both paths covered by tests.
…ent-native into korey/moar-updates-realtime
…ent-native into korey/moar-updates-realtime
…ent-native into korey/moar-updates-realtime

Summary
Two follow-ups for the hosted Realtime Sync Gateway, both opt-in. Apps without hosted-realtime config are unchanged. These unblock the corresponding wiring in the ai-services gateway (
ai-codegenPR #5746).What's in this PR
Sharee visibility (
can-see)The hosted gateway is a generic multi-tenant service and has no copy of an app's shareable-resource registry, so it cannot resolve sharee access itself. It now asks the app:
GET /_agent-native/can-see, mounted by core-routes. It verifies a gateway access-check token against the app's per-project HMAC secret, runs the app's own registry-basedresolveAccess, and returns{ allowed }. Fails closed (allowed: false) on an unknown resource type or lookup error, 404s when the app has no realtime secret, and isCache-Control: private, no-store.signGatewayAccessToken/verifyGatewayAccessToken(exported from./server/short-lived-token). Per-project HMAC key, atypdiscriminator so they can't be swapped for subscribe or media tokens, and the full access query (resourceType,resourceId,userEmail,orgId) bound into the signature so the app authenticates the params, not just the caller.SSE reconnect replay
The hosted client opened the gateway stream with
?token=only, so the gateway's connect-time catch-up ran withsince=0and deferred the reconnect gap to the next poll.use-db-syncnow sends&since=<cursor>on the hosted SSE URL (first connect stays at 0, nothing to replay), so the gateway replays the gap on connect. The gateway already readssince, so no gateway change is needed.Why
Share-aware delivery is a spec acceptance criterion, and the gateway had no way to honor it without the app's access logic. Keeping
resolveAccessin the app (source of truth for shares, ownership, and visibility) and exposing it behind a signed endpoint is the correct boundary. The reconnect change turns a poll-cadence delay into an immediate replay.Backward compatibility
Opt-in only.
can-seereturns 404 unless the app is provisioned with a realtime secret, the new token type is additive, and the SSEsinceis only appended on the hosted-gateway path. Self-hosted and non-hosted apps are untouched.Testing
short-lived-token.spec.ts: gateway access-check token round-trip, wrong key, tampered query, wrong type, expiry.gateway-access-check.spec.ts: allowed / denied / fail-closed-on-throw, 401 on bad or missing token, 404 without a secret, 405 on non-GET.use-db-syncsuite green (no regression from the SSE URL change).@agent-native/coreminor).Follow-up
After this publishes, the ai-services gateway gets its
resolveAccessinjected to callcan-see(staged, small change). Until then the gateway delivers owner and org-scoped events; sharee-only events wait on that wiring.