Skip to content

feat(gatekeeper-kit): replayable runs with account-adjudicated expiry - #441

Open
ndisidore wants to merge 2 commits into
mainfrom
nathan/gatekeeper-kit-replayable-refresh
Open

feat(gatekeeper-kit): replayable runs with account-adjudicated expiry#441
ndisidore wants to merge 2 commits into
mainfrom
nathan/gatekeeper-kit-replayable-refresh

Conversation

@ndisidore

@ndisidore ndisidore commented Sep 3, 2026

Copy link
Copy Markdown
Member

CredentialSource.run gains a replayable option: when the provider rejects credentials, the account decides whether they are expired, superseded, or unavailable, and a replayable operation may retry once with a same-connection successor without crossing a reconnect.

This is an improved version of fetchWithAuthRetry that many gatekeepers hand roll

Derived-bearer ports can heal a rejected current credential inside reportCredentialsRejected, with identity-keyed single-flight coalescing concurrent mints; grant-death ports instead notify the Workshop and return expired.

Each attempt receives its own { identity, generation } fence, while named CredentialsChangedError and CredentialsExpiredError results survive RPC boundaries and stale reads, reports, and refetches cannot re-establish cache authority.

@github-actions github-actions Bot added the gatekeeper Changes to a gatekeeper integration label Sep 3, 2026

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Preview: pr441-nathan-gateke-b32dece1

https://pr441-nathan-gateke-b32dece1-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 3, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding. Gatekeeper-kit tests pass: 343 tests.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 3, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

This comment was marked as outdated.

@ask-bonk

This comment was marked as outdated.

@ask-bonk

This comment was marked as outdated.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

Posted 2 actionable inline findings.

github run

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

No new findings. Previously published inline findings remain applicable to the current head.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

This comment was marked as outdated.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

This comment was marked as outdated.

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

No new findings. Previously published findings remain applicable to 831417a. Tests were not rerun because pnpm is unavailable.

github run

@ndisidore

Copy link
Copy Markdown
Member Author

P2 packages/gatekeeper-kit/src/credentials.ts:341: A reconnect during notify() leaves snapshot() rethrowing the old grant’s expiry despite new credentials being current. Recheck identity after notification.

Not worth actioning. The window is the notify RPC only; fresh()'s #overtaken already covers the wide mint-await window. The delta is error flavor for one caller — it sees expired instead of the reconnect that landed mid-notify — and its re-entry refetches the live credentials. No false notify is created (the grant was confirmed dead when notify fired; the latch handles dedup).

P2 packages/gatekeeper-kit/src/credentials.ts:635: A reconnect adopted while the rejection report is pending is unconditionally cleared; a failed report then falsely expires the live connection. Reconcile the verdict with any adopted successor before clearing authority.

The scenario needs a triple coincidence: 401 + a concurrent fetch adopting a successor inside the ask window + the report reply lost (fail-closed "expired"). Net damage: the one reporting caller gets CredentialsExpiredError instead of CredentialsChangedError. Consider this accepted

@ask-bonk

This comment was marked as outdated.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding. Tests were not run because pnpm is unavailable.

github run

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

@github-actions github-actions Bot added the delivery Changes to CI or release delivery label Sep 4, 2026
ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding. Current GitHub build, test, and lint checks pass; local tests were unavailable because pnpm is not installed.

github run

@ask-bonk

This comment was marked as outdated.

@ndisidore
ndisidore force-pushed the nathan/gatekeeper-kit-replayable-refresh branch from 8d3db43 to 2c1c7a4 Compare September 4, 2026 18:45
@github-actions github-actions Bot removed the delivery Changes to CI or release delivery label Sep 4, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

This comment was marked as outdated.

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

No new findings. Previously published inline findings remain applicable to the current head.

github run

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

No new findings. Previously published findings remain applicable.

github run

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

LGTM!

github run

@ndisidore
ndisidore force-pushed the nathan/gatekeeper-kit-replayable-refresh branch from c288f8b to 6754a17 Compare September 4, 2026 23:39
@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

LGTM!

github run

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 new potential issue.

⚠️ 1 issue in files not directly in the diff

⚠️ Empty vendor IDs lose log attribution

An empty vendor ID makes defineActions discard the configured attribution. Both credential helpers and observer tracking repeat this check, leaving their logs unattributed.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown

LGTM!

github run

}
this.#logger.error("malformed credential rejection verdict", {
event: "credentials.rejection.verdict.malformed",
error: new Error(`unexpected verdict: ${String(verdict)}`),

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 Do not log the malformed verdict value

This fail-closed branch copies an arbitrary out-of-contract RPC result into the logged error message. If a hand-written account implementation accidentally returns a provider response or bearer string instead of a verdict, that credential is persisted in server logs, contrary to the logging rule forbidding tokens and response bodies. Log only static/type metadata, and add a test that a malformed secret-like value is absent from the logged event.

@ask-bonk

ask-bonk Bot commented Sep 5, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding. Tests were not run because pnpm is unavailable.

github run

// Dual guard, neither subsumes the other: the fence blocks fetches started before an expiry
// "" is reserved for a never-connected read: adopting live credentials under it would wedge
// every rejection as retryable, since "" always adjudicates superseded.
if (current.identity === "") {

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 Clear the previous cache authority before rejecting this read

After a successful id-a/gen-a fetch, this source may hold cache entries partitioned by gen-a. If the account reconnects as B and a hand-written stub accidentally serves B under the reserved empty identity, this branch throws but leaves #generation === "gen-a"; a later cache-first request can therefore return A’s data to B without another credential fetch. Since this response cannot establish which principal is current, clear/fence the authority before throwing, and extend this test to start from an established partition.

@ask-bonk

ask-bonk Bot commented Sep 5, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread packages/gatekeeper-kit/src/action-journal.ts
@ask-bonk

ask-bonk Bot commented Sep 5, 2026

Copy link
Copy Markdown
  • P2 packages/gatekeeper-kit/src/actions.ts:51: Submission serialization is keyed by the ActionJournal object, not its shared KV keyspace. Multiple wrappers can bypass the lane, allowing capacity pruning to remove an accepted in-flight staged record. Key serialization by storage/keyspace or enforce a single wrapper, with concurrency coverage.

github run

@ndisidore
ndisidore force-pushed the nathan/gatekeeper-kit-replayable-refresh branch from d521cdd to afcf9ed Compare September 6, 2026 15:03
Comment thread packages/gatekeeper-kit/src/action-journal.ts
@ask-bonk

ask-bonk Bot commented Sep 6, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

CredentialSource.run gains a replayable option: a credential rejection is
retried once with credentials minted through a refreshCredentials channel,
and only a rejection of those reports expiry — so derived-bearer ports stop
reporting routine stale bearers as grant death. The flag without a channel
throws at the call.

The refresh is observed, never adopted: plain reads stay the snapshot's
only writer, which removes the second concurrent writer earlier revisions
fenced around. noteCredentialsExpired now returns the account's verdict on
the reported identity — an explicit false resolves as the fixed retry
message with the cache authority dropped; anything else (lost answers
included) fails closed as accepted. The verdict is asked first, then clear
and fence land as one synchronous transition, and a reconnect crossing the
refresh fences any authority not adopted past it. Replays coalesce per
rejected read via SingleFlight's new object keys.

Plan §4.6/§4.13/§5.6/§5.8/§6 reconciled with the reduced model.
@ndisidore
ndisidore force-pushed the nathan/gatekeeper-kit-replayable-refresh branch from afcf9ed to 3feafe3 Compare September 6, 2026 15:31
[Symbol.dispose](): void {
if (this.#disposed) return;
this.#disposed = true;
this.#dispose?.();

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 Defer owned-resource disposal until submitted cursor calls settle

A client can issue its final next() and immediately dispose the cursor stub while retaining the returned RPC promise. Cap’n Web releases the target when the stub is dropped, so this hook can dispose the callback-owned RPC stub while that fetchPage() is still using it, making the already-submitted page fail. Queue the cleanup behind #queue (which synchronously claims each submitted call), and add a gated test covering disposal with running and queued next() calls.

@ask-bonk

ask-bonk Bot commented Sep 6, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

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

Labels

gatekeeper Changes to a gatekeeper integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant