mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 01:45:29 +02:00
* feat: add a restricted and supervised Claude Code runner Preserve configured authentication and models while enforcing tool access, strict completion JSON, bounded output and process cleanup. Cover argv, failure handling, session metadata and Windows process containment. * feat: route outside reviews by harness and migrate wrapper installs Use Claude Code from Codex and Codex from other supported hosts, with shared invocation rendering, positive gate validation and per-phase provenance. Rename /claude to /claude-code, repair managed shared and copied installations safely, and generate native Kiro skills. Add installed-workflow, failure-injection and live cross-harness regression coverage. * test: recognize CEO mode labels without terminal spacing The paid workflow rendered SCOPEEXPANSION at option 4, but its driver required a literal space. Match the leading mode title without cursor-spacing artifacts and ignore adjacent preview text. Preserve missing-target failures and downstream posture assertions. * test: isolate plan-count fixtures before starting review workflows Seed the complete test plan in a private git repository before launching Claude, so a bare slash command cannot review the live workspace while a delayed fixture message remains queued. Preserve count thresholds, parsers and budgets. Add initial-context and installed-discovery tests, and retain startup/terminal diagnostics on failed evaluations. * test: stabilize review fixtures and Claude eval startup Preserve source boundaries in workflow judge inputs, isolate CEO mode plans, and wait for interactive trust input readiness. Keep startup failure evidence and retain existing models, budgets, and assertions. Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: classify collapsed review modes and isolate seeded findings Keep review questions out of the setup count when terminal cursor positioning removes spaces. State existing webhook safeguards so the five-finding control measures its seeded defects without accidental extra security and concurrency gaps. Preserve question bands and the paired control. Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: isolate browser daemon state across free shards Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: stabilize native review counting and interactive navigation Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: prepare v1.82.0.0 release Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: eliminate browser and process-cleanup test flakes Pin every CI surface to Bun 1.4.0 to avoid extra-stdio finalizers closing reused live sockets. Add an isolated GC/listener regression that fails on Bun 1.3.13, and prevent coordinated rollback to an affected CI runtime. Check renderer cleanup against the render's own staging directory so concurrent renders cannot invalidate the assertion. Make the no-pgrep process-tree walk tolerate disappearing /proc entries, and synchronize its test fixture through child readiness and pipe EOF instead of sleeps. Validation: 9,157 passed, 31 skipped, zero failures across 556 files with retries disabled. Build, all-host generation freshness, and skill checks passed. All three races have failing-before/passing-after regressions. * fix: count completed native review questions in evals * fix: drive review navigation from confirmed native choices * fix: require complete section-loading eval reports * test: isolate telemetry HTTP transport from local assertions * fix: keep review input on the active native question * test: let tunnel revocation daemon choose an available port * test: allocate available ports for pairing and watchdog fixtures * fix: stabilize planning eval navigation and phase reporting * test: isolate installed runtime paths in planning evals * test: stabilize review evidence and concurrent refresh fixtures * fix: resolve design findings before editing the plan * fix: honor and persist disabled outside plan reviews * fix: preserve planning decisions and terminal evidence Load installed host reviews at autoplan phase entry and wait for completed reviewers and saved artifacts. Reuse approved remedies while preserving individual finding decisions. Drive interactive evals from the current terminal viewport, bind native questions across scrolling, and require complete native report evidence. Cover captured stale menus, permission lifecycles, setup classification, and disabled-review tool availability with deterministic regressions. Advance release metadata and the upgrade migration to the unclaimed 1.83.0.0 slot. * fix: drive native review questions and preserve current plans Use the native single-choice keyboard protocol and current terminal viewport, with per-question navigation inside packets and completed-call coverage. Keep permissions, multi-select menus, and Submit controls distinct. Send Autoplan reviewers the amended implementation plan, keep its review record separate, and supply retained application contracts in the chain fixture. Clarify individual DevEx decisions and complete CEO fix options; use one active plan destination for the section-loading report. * fix: preserve complete plan-review decisions * fix: recognize native plan dialogs and reviewer controls * fix: preserve review decisions and phase completion * fix: recognize completed reviews without losing findings * fix: preserve review continuity and native eval completion * test: fix native review completion and eval retry isolation * test: handle native review menus and complete eval fixtures * test: fix native review setup, completion, and isolation failures * test: limit native skill discovery to runtime assets * fix: bind Autoplan reviews to full ordered phase inputs * test: fix planning eval routing, counting, and timeout handling * chore: advance queued release to v1.84.0.0 * fix: preserve complete review inputs and planning decisions * fix: reconcile review approvals and preserve phase obligations * fix: preserve review obligations and unblock eval permissions Carry recorded Autoplan requirements into blind phase inputs, require Eng review approvals before exit, and exercise combined asynchronous flows in CEO reviews. Correct native finding and handoff classification and unblock repeated report edits using scoped request identities. * fix: retain plan requirements and complete native review dialogs * fix: complete native review prompts and retain plan references * fix: preserve review inputs and classify native eval evidence * fix: check competing completion orders in CEO reviews * fix: recognize review decisions and require phase methodology Require the current phase methodology before Autoplan snapshots. Correct substantive decision, closed handoff, and cache-finding classification, and honor the recommended implementation approach in native review dialogs. Add captured-transcript regressions without changing review thresholds, provider models, retries, or deadlines. * test: bind native review decisions and close completed handoffs * fix: complete review dialogs and verify methodology delivery * fix: preserve review evidence and unblock native eval prompts * fix: handle native review question completions * fix: recognize native review narration and controls * fix: count native review decisions and isolate eval fixtures * test: verify seeded review coverage and current artifact permissions * test: isolate model and brain-aware skill renders * fix: repair native workflow evaluation and clarify review steps * fix: stabilize workflow eval evidence and review guidance * test: repair native workflow observation and fixture isolation * fix: recognize completed workflow evidence and owned skill reads * test: repair seeded workflow delivery and completion evidence * test: recognize current review evidence across native forms * test: handle native review variants and permission redraws * fix: honor review preferences and recognize native eval evidence * test: recognize completed review decisions and queued permissions * test: match current review contracts and partial-line edits * test: recognize completed workflow evidence and bounded human waits * fix: preserve review entry gates and native eval interactions * fix: recognize native workflow evidence and preserve review gates * test: recognize current review evidence and preconfigure workflow fixtures * test: recognize completed review findings and scoped artifact permissions * fix: stabilize native workflow review and permission evidence * fix: recognize current review evidence and scoped edit confirmations Clarify Design and engineering review entry instructions and Design scoring. Recognize required legacy coverage and public Autoplan completion recaps. Bind the pending Edit confirmation to its exact file, ordered digest, and one-request approval when a preceding command display remains visible. Keep reviews within their existing size limits and preserve scope gates when extracting workflow fixtures from either supported preamble header. Keep failure outcomes, review thresholds, provider choices, and eval budgets. * fix: recover review workflow progress and eval evidence * fix: recognize valid review evidence and scope selection * test: fix review evidence parsing and repeated artifact prompts * test: recognize valid review decisions and pending native cards * fix(plan-eng-review): keep final navigation consistent with approved tasks * test: recognize valid review evidence and bind legacy diff requests * fix: stabilize review eval evidence and harness repair guidance * docs: update project documentation for v1.85.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: fix Windows CI fixtures and credential scan Rebase captured JSON values and filesystem evidence using the appropriate path convention. Compile native fake CLIs on Windows and synchronize pipe holder readiness, with cleanup retained when assertions fail. Assemble synthetic credential fixtures at runtime so the added-line scan keeps enforcing the same gate without flagging its own rejection controls. Discover generated skills directly for the empty-find regression check, avoiding a recursive scan through saved evaluation artifacts and dependencies. * fix: preserve source renders on Windows Compare canonical generator paths using native separators so an output sidecar pointing at the source cannot overwrite its skill or metadata. Keep the regression fixture isolated from the real checkout and expose freshness diagnostics before asserting subprocess status. Detach Windows drain-test pipe holders from the fake provider's automatic child cleanup while preserving the enclosing runner job and its assertions. * fix: clarify outside review fallback and CEO decisions Render one applicable own-harness fallback path and retain native review, disabled policy, and missing-coverage semantics. Align report field names and mode labels, and make the existing per-cut scope approval explicit. Regenerate skill outputs and keep the workflow judge's model, thresholds, and retry policy unchanged. * chore: move release to free version slot (v1.86.0.0) PR #2852 now claims v1.85.0.0. Align the release metadata and rename migration so upgrades from that version still receive it. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: include engineering review prerequisites and restore branch context * fix: recognize coverage diagrams and clarify design review instructions * fix: preserve file identities and join Windows test processes --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
453 lines
29 KiB
Markdown
453 lines
29 KiB
Markdown
# Plan: Multi-tenant Auth Refactor (reviewed by /plan-eng-review, 2026-09-10)
|
||
|
||
Source plan: `PLAN.md` @ commit 974c858, branch `main`.
|
||
Review mode: SCOPE_REDUCED (complexity gate fired; reduction accepted in D1).
|
||
All seven decisions (D1–D7) were put to the user individually and resolved; each chose the
|
||
complete option. The remedies below are approved and folded in.
|
||
|
||
## Context
|
||
|
||
The original plan (PLAN.md) describes a refactor of the tenant-aware auth path: two new
|
||
services (`AuthBroker`, `SessionMint`) plus `TokenStore`, `AuthCache`, and `RequestPolicy`,
|
||
a rewrite of `legacyAuthFlow()`, and a change to how token validation talks to the identity
|
||
provider (IDP). It does not state the problem being solved, the success criteria, or how the
|
||
change reaches production. This reviewed plan keeps the original intent, records what the
|
||
review found, and folds the approved remedies in so implementation starts complete.
|
||
|
||
Auth is the highest-blast-radius code in a multi-tenant system: a wrong cache key is a
|
||
cross-tenant data leak, a swallowed error is a silent auth bypass or a silent outage. The
|
||
review is calibrated to that.
|
||
|
||
## Step 0: Scope Challenge
|
||
|
||
**Complexity check: TRIGGERED.** PLAN.md:35-36 says "touches 12 files and introduces 4 new
|
||
classes (TokenStore, SessionMint, AuthCache, RequestPolicy)". PLAN.md:19 adds `AuthBroker`
|
||
as a new service too, so it is 5 new types, not 4. The plan is internally inconsistent on
|
||
its own scope.
|
||
|
||
**What already exists (reuse check):**
|
||
- The existing cache adapter (PLAN.md:7-9) already keys by tenant ID, issuer, audience, and
|
||
policy version, evicts expired tokens, and invalidates on logout / revocation / tenant
|
||
suspension. Its invalidation hooks and tests stay in use unchanged (PLAN.md:12-13). Good.
|
||
- `AuthCache` is "a service-facing facade over that same existing adapter, with one backing
|
||
cache" that "retains these unchanged validity and tenant-key rules" (PLAN.md:10-12). A facade
|
||
that changes no rule and adds no serialization is a pass-through class. It is accidental
|
||
complexity (Brooks). **Decision: dropped; inject the adapter directly.**
|
||
- `TokenStore` is never defined relative to the "one backing cache" (PLAN.md:12). If it is a
|
||
second store of tokens, tenant-key rules now live in two places. If it is the adapter under
|
||
another name, it is a duplicate. **Decision: define it against the one backing cache or merge
|
||
it into the adapter; it does not ship as a separate token-holding class.**
|
||
- `legacyAuthFlow()` already works in production (PLAN.md:27). Its behavior is the
|
||
specification the new path must match. The plan treated it as disposable; it is now the
|
||
golden reference (see Tests).
|
||
|
||
**Minimum change that achieves the goal (ACCEPTED as D1 = 1A):** `AuthBroker` + `SessionMint`
|
||
taking the existing adapter by constructor injection, `RequestPolicy` as a pure policy
|
||
evaluator, the `validateAndDispatch()` cleanup, the IDP call change, all behind a per-tenant
|
||
flag. That is 3 new types instead of 5 and removes the two classes that carry the most
|
||
tenant-key risk.
|
||
|
||
**Search check** (Aside not installed; WebSearch used, read-only):
|
||
- **[Layer 1]** Module-level mutable singletons in Node are a known race and test-isolation
|
||
hazard even single-threaded, because many requests are in flight against the same object;
|
||
the standard remedy is factory / dependency injection. Sources:
|
||
[Singleton pitfalls under load](https://dev-aditya.medium.com/the-singleton-pattern-in-node-js-power-pitfalls-and-performance-under-load-3d841ea5c226),
|
||
[Singletons: tool or trap](https://blog.openreplay.com/singletons-javascript-tool-trap/),
|
||
[Event loop and safe singletons](https://dev.to/devunionx/understanding-nodejs-the-event-loop-and-the-safe-use-of-singletons-fmn).
|
||
- **[Layer 1]** `Promise.all` is the built-in for the IDP fan-out; it fails fast, which is the
|
||
correct all-or-nothing semantics for validation. `Promise.allSettled` is for partial-success
|
||
cases, which auth is not. Sources:
|
||
[MDN Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all),
|
||
[all vs allSettled](https://leapcell.io/blog/handling-multiple-api-requests-with-promise-all-and-promise-allsettled).
|
||
- **[Layer 1]** Catch-and-continue without surfacing is the documented "error hiding"
|
||
anti-pattern; let errors bubble to one boundary that maps them to typed results. Sources:
|
||
[Error hiding](https://en.wikipedia.org/wiki/Error_hiding),
|
||
[TypeScript error handling pitfalls](https://www.dhiwise.com/post/typescript-error-handling-pitfalls-and-how-to-avoid-them).
|
||
- **[Layer 3 / EUREKA]** The plan's performance fix is "parallelize 5 calls". First-principles:
|
||
in OIDC-style validation, several of those calls are usually discovery-document and JWKS
|
||
fetches that change on the order of hours. The bigger win is making fewer calls (cache
|
||
discovery + JWKS with TTL), then parallelizing what remains. See Performance #1. Logged to
|
||
the eureka journal.
|
||
|
||
**TODOS.md:** does not exist in this repo. Nothing to cross-reference; one item is proposed
|
||
below (D7) and approved.
|
||
|
||
**Completeness check:** the plan took one explicit shortcut, "no regression test for the
|
||
prior behavior is planned" (PLAN.md:28) while its coverage "does not exercise
|
||
legacyAuthFlow() or assert compatibility with its prior behavior" (PLAN.md:15-16). With
|
||
CC+gstack a characterization suite is ~30 minutes. The REGRESSION RULE makes it mandatory
|
||
(see Tests). No decision was needed there.
|
||
|
||
**Distribution check:** no new artifact type (binary, package, image). Not applicable.
|
||
|
||
### Decision D1 (complexity gate) — RESOLVED: 1A Reduce
|
||
Drop the `AuthCache` facade and inject the adapter; define or merge `TokenStore`; ship 3 new
|
||
types (`AuthBroker`, `SessionMint`, `RequestPolicy`). Completeness 9/10. Not re-argued in
|
||
later sections.
|
||
|
||
## Architecture (reviewed)
|
||
|
||
### Data flow (target design)
|
||
|
||
```
|
||
request(tenant, token)
|
||
│
|
||
▼
|
||
┌─────────────────┐ flag OFF ┌──────────────────┐
|
||
│ per-tenant flag │────────────▶│ legacyAuthFlow() │──▶ response (unchanged)
|
||
└─────────────────┘ └──────────────────┘
|
||
│ flag ON
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ AuthBroker │
|
||
│ validate(req) ──▶ RequestPolicy.evaluate(policyVersion) │
|
||
│ │ │ allow / deny / unknown │
|
||
│ │ cache miss ▼ │
|
||
│ ├──▶ idpClient.validateAll(token) [5 calls, parallel] │
|
||
│ │ └─ discovery+JWKS from TTL cache │
|
||
│ ▼ │
|
||
│ dispatch(result) ──▶ Ok | Denied | AuthError(class) │
|
||
└───────────────┬─────────────────────────────────────────────┘
|
||
│ reads only
|
||
▼
|
||
┌────────────────────┐ writes (single owner) ┌─────────────┐
|
||
│ existing cache │◀──────────────────────────│ SessionMint │
|
||
│ adapter (injected) │ └─────────────┘
|
||
│ key: tenant,issuer,│◀── invalidation hooks: logout / revoke / suspend
|
||
│ audience,policyVer │
|
||
└────────────────────┘
|
||
```
|
||
|
||
### Cache write ordering (approved in D2)
|
||
|
||
```
|
||
SessionMint.mint() adapter revoke hook
|
||
─────────────────── ─────── ───────────
|
||
v = adapter.version(key) ───▶ v=7
|
||
... IDP round trip ...
|
||
v=8 ◀──────────────── invalidate(key) (revocation)
|
||
write(key, entry, ifVersion=7) ─▶ REJECTED (7 ≠ 8) → mint returns Denied/re-validate
|
||
```
|
||
|
||
### Findings
|
||
|
||
**#1 [P1] (confidence: 8/10) PLAN.md:19-20 — global mutable `AuthCache` mutated by two services.**
|
||
Quoted: "share a global mutable `AuthCache` instance via module-level export. Both services
|
||
mutate it." Combined with PLAN.md:10 "they do not serialize mutations". Realistic production
|
||
failure: `SessionMint` writes a freshly minted entry while `AuthBroker` processes a revocation
|
||
for the same key. Without ordering, the revoked token is written back after the invalidation
|
||
and stays valid until expiry. Nothing in the original plan detects this. Second failure: test
|
||
files reset module caches differently (Jest/Vitest workers), so the singleton under test is not
|
||
the singleton the other file mutated, hiding the race in CI.
|
||
|
||
**Decision D2 — RESOLVED: 2A.** Constructor-inject one cache instance from a composition root
|
||
(`src/auth/composition.ts`); `SessionMint` is the only writer, `AuthBroker` is read-only;
|
||
invalidation goes through the existing hooks; writes carry the version they started from and
|
||
are rejected if an invalidation landed in between (diagram above). One shared key builder.
|
||
Completeness 10/10. Maps to: explicit over clever; DRY.
|
||
|
||
**#2 [P1] (confidence: 8/10) PLAN.md:27 — no rollout path for replacing `legacyAuthFlow()`.**
|
||
Quoted: "The existing `legacyAuthFlow()` will get rewritten as part of this work". No flag,
|
||
canary, or rollback anywhere in the plan. Realistic failure: one tenant's IDP returns a claim
|
||
shape the new path rejects; every request for that tenant fails at once, with no switch back.
|
||
|
||
**Decision D3 — RESOLVED: 3A.** Per-tenant feature flag with a global kill switch; keep
|
||
`legacyAuthFlow()` callable until the flag is at 100% for two weeks, then delete flag and
|
||
legacy path in a follow-up PR. The characterization suite (Tests) is the parity gate: both
|
||
flag states must pass it. Completeness 10/10. Reversibility preference; strangler fig.
|
||
|
||
**#3 [P2] (confidence: 6/10) PLAN.md:35 — `TokenStore` relationship to the single backing cache undefined.**
|
||
Medium confidence. Quoted: "one backing cache" (PLAN.md:12) alongside a new `TokenStore` class
|
||
(PLAN.md:35). If `TokenStore` holds tokens outside the adapter, tenant-key isolation is
|
||
duplicated and the adapter's invalidation hooks do not reach it: suspend a tenant and its
|
||
tokens survive in `TokenStore`. **Resolved by D1**: define against the one backing cache or
|
||
merge.
|
||
|
||
**Security architecture:** tenant isolation depends entirely on the adapter's composite key.
|
||
Every write path builds keys through one shared key builder. A test asserts that a token
|
||
minted for tenant A is rejected on a tenant B request with identical issuer/audience.
|
||
|
||
**Distribution architecture:** none, no new artifact.
|
||
|
||
## Code quality (reviewed)
|
||
|
||
**#1 [P1] (confidence: 9/10) PLAN.md:23-24 — `validateAndDispatch()` swallows three error classes.**
|
||
Quoted: "60 lines with three nested try/catch blocks; each catch swallows a different error
|
||
class." Textbook error hiding. Failure the user sees: an IDP outage, a malformed token, and a
|
||
policy-engine bug all look identical from outside, or worse, look like success.
|
||
|
||
**Decision D4 — RESOLVED: 4A.** Split into `validate()` (pure: token + policy → typed result)
|
||
and `dispatch()` (side effects). One error boundary maps each error class to a discriminated
|
||
union `Ok | Denied | AuthError<kind>`, logs it with tenant + kind, and returns an explicit
|
||
response. No catch block exits without either rethrowing or producing a typed value. Callers
|
||
that relied on the swallow are updated to handle the explicit error. Completeness 10/10.
|
||
Explicit over clever; one boundary instead of three is DRY.
|
||
|
||
**#2 [P2] (confidence: 7/10) PLAN.md:19-20 vs 35 — DRY: two writers, two key builders, and an inconsistent class list.**
|
||
Two services mutating the same cache means cache-write and key-construction code exists twice.
|
||
Remedy is included in D2 (single writer, one key builder). The "4 new classes" list omitting
|
||
`AuthBroker` is corrected in this document (5 types as written, 3 after D1).
|
||
|
||
**Diagrams:** the plan had none. This document adds the request-flow and write-ordering
|
||
diagrams above. In code: `AuthBroker` gets the request-flow diagram as a header comment,
|
||
`SessionMint` gets the mint / invalidate ordering diagram, and the cache adapter gets a
|
||
key-shape + invalidation-trigger diagram. No existing ASCII diagrams were found to go stale
|
||
(no source in this repo).
|
||
|
||
## Tests (reviewed)
|
||
|
||
Test framework: none detected in this repo (0 test files, no `package.json`). Coverage
|
||
requirements are stated as plan items; file names assume a TypeScript `*.test.ts` convention
|
||
and should be adjusted to the target repo's layout.
|
||
|
||
### REGRESSION RULE (mandatory, no decision required)
|
||
`legacyAuthFlow()` is existing behavior being rewritten (PLAN.md:27-28) with no covering
|
||
test (PLAN.md:15-16, 28). **CRITICAL:** before any rewrite, record a characterization suite
|
||
in `tests/auth/legacyAuthFlow.regression.test.ts`: for each supported tenant configuration,
|
||
capture inputs (token, tenant, policy version, IDP responses) and the exact output (status,
|
||
claims, cache side effects, error shape). The new path must pass the same suite with the
|
||
flag on. This is the parity gate for D3.
|
||
|
||
### Coverage diagram (state of the original plan)
|
||
|
||
```
|
||
CODE PATHS USER FLOWS
|
||
[+] src/auth/AuthBroker.ts [+] Login → mint → first authed request
|
||
├── validate() └── [GAP] [→E2E] per tenant, two tenants same run
|
||
│ ├── [GAP] valid token + policy allow [+] Logout → retry last request
|
||
│ ├── [GAP] error class 1 surfaced (typed, logged) └── [GAP] [→E2E] expect 401, no cached success
|
||
│ ├── [GAP] error class 2 surfaced [+] Tenant suspended mid-session
|
||
│ ├── [GAP] error class 3 surfaced └── [GAP] [→E2E] next request rejected, others unaffected
|
||
│ └── [GAP] IDP partial failure (1 of 5 rejects/times out) [+] Token expires mid-session
|
||
└── dispatch() └── [GAP] re-mint or clear re-login, never stale success
|
||
├── [GAP] tenant isolation (A's token on B's request) [+] Two tabs / concurrent requests
|
||
└── [GAP] revoke-during-mint: revoked token not revived └── [GAP] no duplicate mint, no lost invalidation
|
||
[+] src/auth/SessionMint.ts [+] IDP slow (10 s on one call)
|
||
├── [GAP] mint() happy path writes one keyed entry └── [GAP] user sees timeout error, not a hang
|
||
└── [GAP] mint() when IDP unreachable → typed error [+] Flag OFF → legacy parity
|
||
[+] src/auth/RequestPolicy.ts └── [GAP] [→E2E] identical to golden recording
|
||
├── [GAP] allow
|
||
├── [GAP] deny
|
||
└── [GAP] unknown / bumped policy version → old entries not served
|
||
[+] existing cache adapter (unchanged)
|
||
└── [★★★ TESTED] eviction + logout/revoke/suspend invalidation — existing suite
|
||
[+] legacyAuthFlow() rewrite
|
||
└── [GAP] [CRITICAL] characterization suite (REGRESSION RULE)
|
||
|
||
COVERAGE: 1/21 paths tested (5%) | Code paths: 1/14 (7%) | User flows: 0/7 (0%)
|
||
QUALITY: ★★★:1 ★★:0 ★:0 | GAPS: 20 (4 E2E, 0 eval, 1 CRITICAL regression)
|
||
```
|
||
|
||
Legend: ★★★ behavior + edge + error | ★★ happy path | ★ smoke check | [→E2E] integration test
|
||
|
||
**Decision D5 — RESOLVED: 5A.** All 19 non-regression gaps close in this PR: unit tests plus
|
||
the four E2E journeys against a fake IDP fixture. Target after implementation: 21/21.
|
||
Completeness 10/10.
|
||
|
||
### Test requirements (all in scope)
|
||
- `tests/auth/legacyAuthFlow.regression.test.ts` — CRITICAL golden characterization, flag off
|
||
and flag on must both pass.
|
||
- `tests/auth/AuthBroker.test.ts` — validate() allow; each of the 3 error classes yields its
|
||
typed variant and a log line with tenant + kind; 1-of-5 IDP rejection and 1-of-5 timeout both
|
||
fail closed within the request budget.
|
||
- `tests/auth/tenantIsolation.test.ts` — token for tenant A with identical issuer/audience is
|
||
rejected for tenant B; key builder output differs only by tenant.
|
||
- `tests/auth/cacheOrdering.test.ts` — start mint, fire revoke, complete mint: cache holds no
|
||
valid entry (write-version check rejects the late write).
|
||
- `tests/auth/SessionMint.test.ts` — one keyed write per mint; IDP unreachable → typed error,
|
||
no cache write.
|
||
- `tests/auth/RequestPolicy.test.ts` — allow, deny, unknown version, version bump does not
|
||
serve prior entries.
|
||
- `tests/auth/idpClient.test.ts` — warm cache makes ≤2 network calls; unknown kid triggers
|
||
JWKS refetch once; per-call timeout aborts and surfaces a typed error.
|
||
- `tests/e2e/auth.e2e.ts` — login→request→logout per tenant; suspend tenant; flag-off parity;
|
||
slow-IDP timeout is visible to the user.
|
||
|
||
QA test plan artifact written to
|
||
`~/.gstack/projects/gstack-plan-count-RacHBI/vercel-sandbox-main-eng-review-test-plan-20260910-153621.md`
|
||
for `/qa` and `/qa-only`.
|
||
|
||
## Performance (reviewed)
|
||
|
||
**#1 [P2] (confidence: 8/10) PLAN.md:31-32 — five sequential IDP calls.**
|
||
Quoted: "Token validation issues 5 sequential API calls to the IDP; they could be parallelized
|
||
via Promise.all trivially (calls are independent)." Parallelizing is correct and `Promise.all`
|
||
fail-fast is the right semantics for validation. "Trivially" hides two things: (1) five
|
||
concurrent calls per request multiplies IDP burst load by 5 and can trip provider rate limits
|
||
under a login storm; (2) no per-call timeout means one slow call still hangs the request.
|
||
**[EUREKA]** the larger win is fewer calls: discovery and JWKS documents are cached with TTL so
|
||
the hot path is typically one or two network calls.
|
||
|
||
**Decision D6 — RESOLVED: 6A.** `Promise.all` + per-call timeout (AbortSignal) + TTL cache for
|
||
discovery/JWKS with refetch-on-unknown-kid + a metric on IDP call count per validation.
|
||
Completeness 10/10. Built-ins only, no new dependency.
|
||
|
||
No N+1 or memory concern found: one backing cache, keyed entries, existing eviction.
|
||
|
||
## Failure modes (per new codepath)
|
||
|
||
| Codepath | Realistic failure | Test (after this plan) | Handling (after this plan) | User sees | Critical gap in original? |
|
||
|---|---|---|---|---|---|
|
||
| SessionMint write vs revoke | Revoked token written back after invalidation | cacheOrdering.test.ts | write-version check rejects late write (D2) | mint re-validates or Denied | **YES → closed** |
|
||
| validateAndDispatch catch blocks | IDP outage classed as generic failure or success | 3 error-class tests | typed boundary, logged with kind (D4) | distinct explicit error | **YES → closed** |
|
||
| legacyAuthFlow rewrite | Claim-shape difference for one tenant | characterization suite (mandatory) | per-tenant flag + kill switch (D3) | flag flip, no outage | **YES → closed** |
|
||
| IDP fan-out | One of five calls hangs | idpClient timeout test | AbortSignal timeout (D6) | timeout error within budget | no |
|
||
| TokenStore (if separate) | Suspend does not reach it | suspend E2E | merged into adapter (D1) | session rejected | resolved by D1 |
|
||
| RequestPolicy version bump | Old entries served | RequestPolicy.test.ts | key includes version (existing) | none | no |
|
||
|
||
Critical gaps flagged in the original plan: **3**. All three are closed by approved remedies
|
||
(D2, D4, D3 + mandatory regression suite). Remaining critical gaps in this reviewed plan: 0.
|
||
|
||
## NOT in scope
|
||
- Deleting `legacyAuthFlow()` and the flag — follow-up PR after the flag has been 100% for two
|
||
weeks (D3).
|
||
- Replacing the existing cache adapter — it works, is keyed correctly, and is tested; reuse it.
|
||
- Repo-wide dependency-injection composition — only the auth composition root is in scope;
|
||
broader adoption is the approved TODO (D7).
|
||
- `AuthCache` facade and a standalone `TokenStore` — cut in D1.
|
||
- IDP provider change or protocol migration — unrelated to this refactor.
|
||
- Distribution / packaging — no new artifact.
|
||
|
||
## What already exists
|
||
- Existing cache adapter with tenant/issuer/audience/policy-version keys, eviction, and
|
||
invalidation hooks + tests (PLAN.md:7-13): **reused as-is**, injected directly (D1, D2).
|
||
- `legacyAuthFlow()` (PLAN.md:27): **becomes the specification** via the characterization suite
|
||
and stays live behind the flag (D3).
|
||
- Invalidation hooks for logout / revoke / suspend (PLAN.md:8-9): **reused**; the single-writer
|
||
design routes through them instead of adding a second invalidation path.
|
||
|
||
## Worktree parallelization strategy
|
||
|
||
| Step | Modules touched | Depends on |
|
||
|---|---|---|
|
||
| S1 Regression characterization suite | tests/auth/ | — |
|
||
| S2 Composition root + single-writer cache wiring + write-version check | src/auth/ (AuthBroker, SessionMint, composition) | — |
|
||
| S3 validateAndDispatch split + typed errors | src/auth/AuthBroker, src/auth/errors | S2 (injection shape) |
|
||
| S4 Feature flag + rollout | src/auth/flags, src/auth/AuthBroker | S2 |
|
||
| S5 IDP client: parallel + timeout + TTL cache | src/auth/idpClient | — |
|
||
| S6 Unit tests for new paths | tests/auth/ | S2, S3, S5 |
|
||
| S7 E2E journeys + fake IDP | tests/e2e/ | S4 |
|
||
|
||
Lane A: S1 (independent, tests/auth/)
|
||
Lane B: S2 → S3 → S4 (sequential, shared src/auth/)
|
||
Lane C: S5 (independent, src/auth/idpClient only)
|
||
Then: S6 and S7 in parallel after A, B, C merge.
|
||
|
||
Execution: launch A + B + C in parallel worktrees. Merge. Then S6 ∥ S7.
|
||
Conflict flag: Lanes B and C both live under `src/auth/`; C touches only `idpClient`, so
|
||
conflicts are unlikely, but rebase C onto B before merge.
|
||
|
||
## Implementation Tasks
|
||
Synthesized from this review's findings. Each task derives from a specific finding above and
|
||
an approved decision. Run with Claude Code or Codex; checkbox as you ship.
|
||
|
||
- [ ] **T1 (P1, human: ~1 day / CC: ~30 min)** — tests/auth — Write the `legacyAuthFlow()` characterization (regression) suite before any rewrite
|
||
- Surfaced by: Tests — REGRESSION RULE, PLAN.md:27-28
|
||
- Files: tests/auth/legacyAuthFlow.regression.test.ts
|
||
- Verify: suite green on current code; green again with flag on after rewrite
|
||
- [ ] **T2 (P1, human: ~1 day / CC: ~20 min)** — src/auth — Replace module-level `AuthCache` export with constructor injection and a single cache writer with write-version check
|
||
- Surfaced by: Architecture #1, PLAN.md:19-20 (D2 = 2A)
|
||
- Files: src/auth/AuthBroker.ts, src/auth/SessionMint.ts, src/auth/composition.ts
|
||
- Verify: tests/auth/cacheOrdering.test.ts; grep shows no module-level cache export
|
||
- [ ] **T3 (P1, human: ~4 h / CC: ~15 min)** — src/auth — Split `validateAndDispatch()` into `validate()` and `dispatch()` with one typed error boundary
|
||
- Surfaced by: Code quality #1, PLAN.md:23-24 (D4 = 4A)
|
||
- Files: src/auth/AuthBroker.ts, src/auth/errors.ts
|
||
- Verify: three error-class tests in tests/auth/AuthBroker.test.ts; no catch without rethrow or typed return
|
||
- [ ] **T4 (P1, human: ~4 h / CC: ~15 min)** — src/auth — Per-tenant feature flag with kill switch; keep `legacyAuthFlow()` callable
|
||
- Surfaced by: Architecture #2 (D3 = 3A)
|
||
- Files: src/auth/flags.ts, src/auth/AuthBroker.ts
|
||
- Verify: flag-off parity E2E; flag-on passes T1 suite
|
||
- [ ] **T5 (P1, human: ~1 day / CC: ~30 min)** — tests/auth — Tenant-isolation, revoke-during-mint, error-class surfacing, IDP partial-failure, policy-version tests
|
||
- Surfaced by: Tests — coverage diagram, 20 gaps, 3 critical (D5 = 5A)
|
||
- Files: tests/auth/AuthBroker.test.ts, tests/auth/SessionMint.test.ts, tests/auth/tenantIsolation.test.ts, tests/auth/RequestPolicy.test.ts, tests/auth/idpClient.test.ts
|
||
- Verify: coverage diagram code paths 14/14
|
||
- [ ] **T6 (P2, human: ~4 h / CC: ~15 min)** — src/auth/idpClient — `Promise.all` + per-call timeout + TTL cache for discovery/JWKS, refetch on unknown kid, call-count metric
|
||
- Surfaced by: Performance #1, PLAN.md:31-32 (D6 = 6A)
|
||
- Files: src/auth/idpClient.ts
|
||
- Verify: 1-of-5 timeout test fails closed within budget; metric shows ≤2 calls on warm cache
|
||
- [ ] **T7 (P2, human: ~2 h / CC: ~10 min)** — src/auth — Drop the `AuthCache` facade; define `TokenStore` against the one backing cache or merge it into the adapter
|
||
- Surfaced by: Step 0 complexity check, PLAN.md:11-13, 35-36 (D1 = 1A)
|
||
- Files: src/auth/AuthCache.ts (delete), src/auth/TokenStore.ts (define or delete)
|
||
- Verify: new-type count is 3; suspend-tenant E2E rejects the session
|
||
- [ ] **T8 (P2, human: ~1 h / CC: ~5 min)** — src/auth — ASCII request-flow and write-ordering diagrams as header comments
|
||
- Surfaced by: Required outputs — Diagrams
|
||
- Files: src/auth/AuthBroker.ts, src/auth/SessionMint.ts, cache adapter
|
||
- Verify: diagrams match the flow in this document
|
||
- [ ] **T9 (P2, human: ~1 day / CC: ~30 min)** — tests/e2e — login→request→logout, tenant suspend, flag-off parity, slow-IDP journeys with a fake IDP
|
||
- Surfaced by: Tests — user flows marked [→E2E] (D5 = 5A)
|
||
- Files: tests/e2e/auth.e2e.ts, tests/e2e/fakeIdp.ts
|
||
- Verify: 4 E2E journeys green in CI
|
||
|
||
Tasks JSONL: `~/.gstack/projects/gstack-plan-count-RacHBI/tasks-eng-review-20260910-153621.jsonl` (9 tasks).
|
||
|
||
## TODOS.md (approved item, D7 = 7A)
|
||
|
||
Plan mode forbids editing repo files other than the plan, so TODOS.md is created at
|
||
implementation start with this entry (format per gstack TODOS-format):
|
||
|
||
### Adopt a composition-root / DI pattern for auth-adjacent services
|
||
|
||
**What:** Extend the composition root introduced for `AuthBroker` / `SessionMint` to the other
|
||
services that currently import shared mutable state at module level.
|
||
|
||
**Why:** The singleton race found in Architecture #1 likely exists elsewhere; one pattern
|
||
repo-wide keeps the fix from being a one-off.
|
||
|
||
**Context:** Start from `src/auth/composition.ts` once T2 lands; grep for module-level
|
||
`export const` of mutable objects to size the work. Pros: testable services, no hidden
|
||
coupling, one place to see the object graph. Cons: touches files outside the auth refactor; a
|
||
migration, not a patch.
|
||
|
||
**Effort:** M
|
||
**Priority:** P2
|
||
**Depends on:** T2
|
||
|
||
## Outside voice
|
||
Codex review skipped (codex_reviews disabled). Re-enable: `gstack-config set codex_reviews enabled`.
|
||
No outside coverage this run; logged as `outside_status: disabled`. No Claude-subagent
|
||
fallback was dispatched, per the disabled branch. No cross-model tension to report.
|
||
|
||
## Retrospective learning
|
||
Git history is a single seed commit (974c858). No prior review cycle to compare against.
|
||
|
||
## Next steps
|
||
Backend-only change, no UI surface: `/plan-design-review` not applicable. Refactor, not a
|
||
product-direction change: `/plan-ceo-review` optional and not suggested. All relevant reviews
|
||
complete. Run /ship when ready.
|
||
|
||
## Completion summary
|
||
- Step 0: Scope Challenge — scope reduced per recommendation (5 → 3 new types, D1 = 1A)
|
||
- Architecture Review: 3 issues found (2 P1, 1 P2), all resolved (D2 = 2A, D3 = 3A, #3 via D1)
|
||
- Code Quality Review: 2 issues found (1 P1, 1 P2), all resolved (D4 = 4A, #2 via D2)
|
||
- Test Review: diagram produced, 20 gaps identified (1 CRITICAL regression, mandatory); all 20 in scope (D5 = 5A)
|
||
- Performance Review: 1 issue found (P2, with a fewer-calls eureka), resolved (D6 = 6A)
|
||
- NOT in scope: written
|
||
- What already exists: written
|
||
- TODOS.md updates: 1 item proposed, approved (D7 = 7A)
|
||
- Failure modes: 3 critical gaps flagged in the original plan, 3 closed by approved remedies, 0 remaining
|
||
- Outside voice: skipped (codex_reviews disabled)
|
||
- Parallelization: 3 lanes, 3 parallel then 2 parallel test lanes
|
||
- Lake Score: 7/7 recommendations chose the complete option
|
||
- Session setup items (not issue approvals, deferred to next healthy run): gstack CLAUDE.md routing rules, cross-project learnings preference
|
||
- Durable learning logged: this repo is a fixture with only CLAUDE.md and PLAN.md; findings are evidenced by PLAN.md line quotes, no source or test framework to verify against
|
||
|
||
Review log written (`plan-eng-review`, status clean, unresolved 0, critical_gaps 0,
|
||
issues_found 26, mode SCOPE_REDUCED, commit 974c858). Decision log written. Telemetry
|
||
(`gstack-skill-end`, outcome success) run.
|
||
|
||
## GSTACK REVIEW REPORT
|
||
|
||
| Review | Trigger | Why | Runs | Status | Findings |
|
||
|--------|---------|-----|------|--------|----------|
|
||
| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |
|
||
| Outside Review | codex via `/plan-eng-review` (plan-review phase) | Independent 2nd opinion | 1 | disabled | skipped, no outside coverage |
|
||
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 2 | clean (PLAN) | 26 issues, 0 critical gaps remaining (3 flagged, 3 closed); 7/7 decisions resolved |
|
||
| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — |
|
||
| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — |
|
||
|
||
**OUTSIDE COVERAGE:** provider codex, phase plan-review, outside_status disabled (user setting `codex_reviews=disabled`), 0 findings; native fallback not dispatched. Host claude.
|
||
|
||
**VERDICT:** ENG CLEARED — ready to implement (scope reduced to 3 new types; all remedies approved and folded in).
|
||
|
||
NO UNRESOLVED DECISIONS
|