Skip to content

feat(auth): split bearer auth into worker and maintainer token tiers - #1122

Open
itsmiso-ai wants to merge 4 commits into
mainfrom
courier/misospace/dispatch/issue-1111
Open

itsmiso-ai wants to merge 4 commits into
mainfrom
courier/misospace/dispatch/issue-1111

Conversation

@itsmiso-ai

Copy link
Copy Markdown
Contributor

Summary

Closes #1111

Split the single bearer token into two tiers, enforced centrally from one route→tier table:

  • maintainer — DISPATCH_AGENT_TOKEN (unchanged rights) plus an optional DISPATCH_MAINTAINER_TOKEN alias. Everything, including pr-fix-queue/requeue, QUEUED/IGNORED marks, force claims, releasing other agents' claims, groomer, lanes, admission overrides, automation/*, syncs, the agent-work operator release/reassign surface, and agent-work/sweep.
  • worker — new DISPATCH_WORKER_TOKEN for autonomous executors. Explicit, fail-closed allowlist: next-task, tasks/report, heartbeat, active-work, queue, work-summary; agent-work start/checkpoint/finish + GET listing; non-force issues/claim; own-claim issues/unclaim; issues/state (GET) and issues/status; issue and PR-fix queue reads; pr-fix-queue/mark with FIXED/BLOCKED/STALE (generation still required per fix(queue): settle only the issued PR-fix attempt and start head #1074).

OIDC, Basic, and auth-disabled all resolve to maintainer, and routes not on the allowlist default to maintainer by construction — so deploying changes nothing for existing callers (rollout per the issue).

Implementation

  • src/lib/dispatch-env.ts: getAcceptedTokenTiers() token→tier table + timing-safe getBearerTokenTier() (maintainer wins if a value is configured for both tiers); getAcceptedAgentTokens/isAuthorizedBearerToken preserved. Module stays Edge-runtime safe.
  • src/lib/auth.ts: authorizeRequest resolves the bearer tier and enforces requiredTierForRoute(pathname, method) centrally; worker-on-maintainer returns { authorized: false, forbidden: true }, and the new authErrorResponse() helper maps that to a 403 naming the required tier while keeping 401 for ordinary auth failures. All ~59 routes migrated to authErrorResponse. Tier denials write a best-effort auth_tier_denied audit row (lazy prisma import; a DB failure never changes the auth decision).
  • Body-based splits inside the route (same path serves both tiers): issues/claim refuses force: true for workers; issues/unclaim verifies the x-agent-name identity matches the body agentName; pr-fix-queue/mark refuses QUEUED/IGNORED. Each denial is audited and returned before any state mutation.
  • authorizeGroomerRequest preserves the forbidden result so a worker token on the groomer route still gets 403.

Tests

  • Table-driven lib tests (src/lib/auth.test.ts): every worker-allowlisted route accepts a worker token; a sample of maintainer routes (including POST /api/agent-work, agent-work/sweep, pr-fix-queue/requeue, sync, groom, automation DELETE) returns forbidden; auth_tier_denied audit asserted; unknown routes default to maintainer; lookalike paths / wrong methods are rejected; maintainer + non-bearer modes unaffected.
  • Token-tier lookup tests in src/lib/dispatch-env.test.ts.
  • Route-level tests for the three body splits (worker force → 403, worker releasing another agent → 403, worker QUEUED/IGNORED → 403, maintainer paths proceed through the real authorizeRequest stack).
  • Full suite green locally: 3606 tests, typecheck clean, lint 0 errors (1 pre-existing unrelated warning).

Review notes addressed

An independent review pass caught two allowlist bugs during development, both fixed: the original agent-work/* wildcard exposed the operator release/reassign surface and sweep to worker tokens, and issues/state was listed with the wrong method (it is a GET-only route).

Rollout

Deploy is a no-op for current callers. Next step (in home-ops, out of scope here): point Courier's courier-dispatch secret at a DISPATCH_WORKER_TOKEN value; the MCP bridge and operator agents keep the maintainer token.

…1111)

DISPATCH_AGENT_TOKEN keeps maintainer rights (plus an optional
DISPATCH_MAINTAINER_TOKEN alias); the new DISPATCH_WORKER_TOKEN is
accepted with an explicit, fail-closed allowlist enforced centrally
from a single route->tier table. Worker tokens get a 403 naming the
required tier (with an auth_tier_denied audit row) on maintainer
routes; issues/claim, issues/unclaim, and pr-fix-queue/mark are split
by request body inside the route. OIDC/basic/disabled resolve to
maintainer, so deploying changes nothing for existing callers.
…module

The tier-denial audit's lazy import("./prisma") inside src/lib/auth.ts was
statically traced into the browser bundle via the client-reachable
auth-controls component (pg module-not-found, breaking the CI build).
Extract getAuthMode into a dependency-free src/lib/auth-mode.ts and import
it there from auth-controls; auth.ts re-exports it unchanged.
its-saffron[bot]

This comment was marked as outdated.

- Throttle auth_tier_denied audit rows to one per actor/method/path per
  minute so a misconfigured worker cannot write-amplify the audit table
  (the 403 itself is never throttled)
- Document GET /api/agent-work in the worker token-tier allowlist
- Test authorizeGroomerRequest preserving the worker-tier forbidden
  result (403) and the groomer-token path still resolving to maintainer
- Regression test: a truthy non-boolean force value cannot override
  another agent's claim on a worker token
its-saffron[bot]

This comment was marked as outdated.

- Wrap the whole recordTierDenialAudit body (limiter import, actor
  resolution, throttle checks) in the try/catch so nothing in the
  best-effort audit path can turn a 403 into a 500
- Add an overall per-actor ceiling (10/min) on top of the per
  actor/method/path bucket, so cycling dynamic maintainer paths cannot
  open a fresh bucket per path
- Tests: denial stays forbidden on throttled requests, auditing re-opens
  after the window, the per-actor cap binds across distinct paths, and
  the audit actor is the x-agent-name value (never the bearer token)

@its-saffron its-saffron Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Automated Review (incremental)

Incremental review: reviewed the changes since the last managed review; unresolved findings from that review are carried forward.

Analysis engine: qwen3.8-flash-next-chat@https://litellm.jory.dev/v1 (openai) — fallback (primary failed)

Recommendation

Approve with notes. The incremental delta hardens tier-denial audit throttling and adds coverage for the worker/maintainer bearer split described in PR 1111. CI is green for typecheck, lint, tests, build and migrations. No path-handling code is changed in this delta and the auth flow changes are covered by table-driven lib tests and route-level tests. Session token handling is not touched by the diff; path traversal / edge-case path testing is not applicable to this change.

Change-by-change findings

src/lib/auth.ts – recordTierDenialAudit throttling

  • The denial audit now applies two rate limits: per-actor 10/min and per-actor/method/pathname 1/min, with the actor resolved via resolveBearerActor(request). The try/catch guarantees a DB or limiter failure never changes the auth decision. The PR description notes the 403 response is never throttled and audit is best-effort.
  • Evidence: diff shows checkRateLimit(auth_tier_denied:${actor}, { limit: 10, windowMs: 60_000 }) and checkRateLimit(auth_tier_denied:${actor}:${method}:${pathname}, { limit: 1, windowMs: 60_000 }) added, and comment updated to describe dual throttling.

src/lib/auth.test.ts – tier denial audit tests

  • New tests verify per-path throttling resets after 61s, per-actor ceiling caps audits at 10 across distinct paths, and denial audits are attributed to x-agent-name and never contain the bearer token.
  • Evidence: diff adds it("throttles denial audit rows to one per actor/method/path per window", ...), it("caps per-actor denial audits even across distinct paths", ...), it("attributes denial audits to x-agent-name, never the bearer token", ...) with assertion expect(JSON.stringify(call.data)).not.toContain(WORKER_TOKEN).

Sources

  • PR metadata: feat(auth): split bearer auth into worker and maintainer token tiers, closes PR 1111, changed files include .env.example, src/components/auth-controls.tsx, src/lib/auth-mode.ts, src/lib/auth.test.ts, src/lib/auth.ts.
  • CI Check Results: Typecheck success, Lint success, Tests success, Build success.
  • Incremental delta diff for src/lib/auth.ts and src/lib/auth.test.ts.

Standards Compliance

AGENTS.md conventions applied:

  • Environment variables: PR adds DISPATCH_WORKER_TOKEN and optional DISPATCH_MAINTAINER_TOKEN alias documentation; .env.example is changed. No real credentials are visible in the corpus.
  • Secrets handling: The PR description and new test assert denial audits are attributed to x-agent-name and the audit payload does not contain the bearer token. Repository standard requires DISPATCH_AGENT_TOKEN and GITHUB_TOKEN never be logged, echoed or persisted. Direct evidence of logging beyond audit is not present in the corpus.
  • Auth & audit actor identity: lib/auth.ts authorizeRequest / getAuthorizedActor remains the source of truth; tier enforcement is centralized via requiredTierForRoute and authErrorResponse.

Linked Issue Fit

PR body states it closes PR 1111 and implements the worker/maintainer bearer split with a central route→tier table, worker allowlist, body-based splits for issues/claim, issues/unclaim, pr-fix-queue/mark, and audit of tier denials. The incremental delta is a hardening of the audit throttling introduced in that feature. No acceptance criteria from the issue body are provided in the corpus, so fit is assessed from the PR summary and tests.

Unknowns or Needs Verification

  • Session token handling: Must-check requires verify session token handling is correct. The diff touches bearer tier logic only; src/components/auth-controls.tsx and src/lib/auth-mode.ts are listed in risk flags but no diff content is provided in the corpus. No evidence of session/NextAuth token handling changes is available to verify.
  • Secrets logging: Requirement DISPATCH_AGENT_TOKEN and GITHUB_TOKEN must never be logged, echoed or persisted. The new test confirms the denial audit row does not contain the worker token, but the corpus does not provide evidence that .env.example contains only placeholders or that no other logging paths emit tokens.
  • Path traversal / edge-case paths: Risk flag path_handling_changes is present but risk_flags_with_files.path_handling_changes is empty. No path manipulation code is changed in the incremental delta; the must-check for null bytes / symlinks therefore does not apply to this change.

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.

Split bearer auth into worker and maintainer token tiers

1 participant