Slice 2 of #788. Depends on #795. This is the slice that does the security work.
An OAuth-authenticated request bypasses CredentialEpochMiddleware, where the app runs its per-request fail-closed checks for JWT callers. This slice must re-establish every one of those checks on the OAuth path. The OAuth path inherits none of them for free.
Checks that must run on every OAuth-authenticated request
| Check |
Why |
Existing reference |
| User disabled |
A disabled worker's assistant must stop immediately |
CredentialEpochMiddleware reads DisabledAt |
| Farm suspended |
A suspended account's assistants must stop |
The same correlated read, Account.IsActive (#532) |
| Must-change-password |
When the app forces a user to change their password, it blocks everything except the change (#283). An assistant that kept working would bypass that gate |
|
| Flock scoping |
A worker assigned to two flocks must see only those two through their assistant (#388). This is the hazard #787 is open about. Make it deliberate |
|
| Rate limits |
An assistant calls far faster than a human. The limit must key on the shared IFixedWindowCounter (#543/#544), never a process-local limiter. A process-local limiter is the #271 blocker shape, which earlier derivations got wrong twice |
|
Instant revocation
#788 chose reference tokens so that disconnecting an assistant takes effect on the next call. Verify this end to end. Revoke, then assert that the next request fails. This app does an uncached per-request credential read (#364) because revocation must not wait for an expiry.
Scheme coexistence
This will be the second authentication scheme the app has ever registered. Today there is only AddJwtBearer, with no AddPolicyScheme. Decide and document how the app routes a request to the right handler, and make sure the app cannot mistake an OAuth token for a session JWT or the reverse.
Guards
Per AGENTS.md, each check needs a test that states its red mutation. The test most likely to go wrong asserts FlockScope.IsUnrestricted instead of IsResolved. IsUnrestricted defaults to true, so a guard written against it passes while the hole stays open. docs/plans/770-mcp-server/02-guards.md documents that exact trap.
Done when
Each check has a test that fails when the check is removed, and a revoked token stops working on the next request.
Slice 2 of #788. Depends on #795. This is the slice that does the security work.
An OAuth-authenticated request bypasses
CredentialEpochMiddleware, where the app runs its per-request fail-closed checks for JWT callers. This slice must re-establish every one of those checks on the OAuth path. The OAuth path inherits none of them for free.Checks that must run on every OAuth-authenticated request
CredentialEpochMiddlewarereadsDisabledAtAccount.IsActive(#532)IFixedWindowCounter(#543/#544), never a process-local limiter. A process-local limiter is the #271 blocker shape, which earlier derivations got wrong twiceInstant revocation
#788 chose reference tokens so that disconnecting an assistant takes effect on the next call. Verify this end to end. Revoke, then assert that the next request fails. This app does an uncached per-request credential read (#364) because revocation must not wait for an expiry.
Scheme coexistence
This will be the second authentication scheme the app has ever registered. Today there is only
AddJwtBearer, with noAddPolicyScheme. Decide and document how the app routes a request to the right handler, and make sure the app cannot mistake an OAuth token for a session JWT or the reverse.Guards
Per
AGENTS.md, each check needs a test that states its red mutation. The test most likely to go wrong assertsFlockScope.IsUnrestrictedinstead ofIsResolved.IsUnrestricteddefaults totrue, so a guard written against it passes while the hole stays open.docs/plans/770-mcp-server/02-guards.mddocuments that exact trap.Done when
Each check has a test that fails when the check is removed, and a revoked token stops working on the next request.