Repository navigation
feat(cloud-agent): add token exchange endpoint - #8438
Conversation
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe follow-up commits ( Assumptions: Files Reviewed (2 files)
Previous Review Summary (commit e0ae5e1)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit e0ae5e1)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Reviewed by deepseek-flash-latest · Input: 65K · Output: 13.9K · Cached: 1.1M Review guidance: REVIEW.md from base branch |
iscekic
left a comment
There was a problem hiding this comment.
bot: Approving with remarks. The security shape is right: bearer-only with no cookie fallback, a strict body schema, no-store on every path, re-validation inside the issuer (pepper, blocked, env, audience), scoped cloudAgentAuthority credentials denied here, and the legacy five-year fallback prevented. The 15-minute cap is safe because the Worker's delegationExpiresAt comes from runtimeAuthorizationMaximumLifetimeMs, not the control token's exp (packages/worker-utils/src/runtime-authorization.ts), so session renewal is unaffected.
Inline remarks cover the 503 gate, the discarded organization header, the error body shape, the missing timing wrapper, the jsdoc contract, and two test gaps.
Two remarks need a stated contract rather than a code change.
Which endpoints accept the minted token
validateAuthorizationHeader defaults the accepted audience to KILO_API_AUDIENCE (packages/web-shared/src/lib/tokens.ts), and a present aud must match exactly (packages/worker-utils/src/kilo-token-policy.ts). A token with aud: cloud-agent-next is rejected by /api/trpc/* and by /api/cloud-agent-next/sessions/stream-ticket. The only web route that accepts it is apps/web/src/app/api/cloud-agent-next/balance/route.ts, which passes expectedAudience: CLOUD_AGENT_NEXT_AUDIENCE.
So the minted token reaches the Worker and balance only. If the CLI must still call tRPC or fetch a stream ticket in the same session, it has to present the source credential there. Please state that in the jsdoc; the line "The token authorizes the Cloud Agent service, including environments that share auth" does not carry it.
Organization-bound source credentials
An org-bound kilo-api credential is rejected before issuance, so a CLI configured with a dashboard organization API key can never exchange, not even for a personal-scope token.
- A modern token carrying
organizationIdis rejected atapps/web/src/lib/auth/resource-delegation.ts:hasRestrictedPrincipalClaimsomitsorganizationId, so the modern branch callsunauthorized()and returns 401. - A legacy
generateOrganizationApiTokentoken (15 minutes, withorganizationIdandorganizationRole) is rejected byhasUnsafeLegacyClaimswith 403Unsupported legacy credential context.
For those inputs the guard if (authority.organizationId) forbidden(...) is unreachable. The current intent is pinned by the route test case rejects restricted legacy credentials. If organization API keys must work, this endpoint needs an explicit org-bound path. If they must not, the jsdoc should say the source credential has to be personal.
Production prerequisite
The endpoint returns 503 unless SHARED_RESOURCE_TOKENS_ENABLED and CLOUD_AGENT_RESOURCE_TOKENS_ENABLED are both exactly true in production. Confirm both before the CLI adopts the route.
Remaining test gaps
- The outer auth is mocked throughout, so nothing proves the real end-to-end path (a legacy CLI token reaching issuance). The sibling
balance/route carries bothroute.test.tsandroute.auth.test.ts; consider the same split here. - No case for a well-formed organization UUID the caller is not a member of; the existing cases start from a real organization.
- No assertion that
cache-control: no-storeis present on the 403 path.
- Refuse only legacy sources when the issuance gate is off, so active
modern device credentials keep the issuer's bounded rollback issuance.
- Use the organization header when the body omits organizationId, and
reject a body/header mismatch with 400.
- Return { success: false, error } on every error path.
- Wrap the handler in withRestTiming.
- Document the status codes, the token audience and the personal-source
requirement.
- Remove a dead mock and cover rollback, header scope, non-member
organizations and no-store on 403.
|
Review-body items without a thread:
|
Summary
Adds an authenticated Cloud Agent token exchange endpoint through the existing control-token issuer. Supports personal and organization scope, a maximum token lifetime of 15 minutes, and non-cacheable responses.
Verification
No manual endpoint test was run. Validation used the route handler and issuer against isolated local PostgreSQL.
Visual Changes
N/A
Reviewer Notes
Automated checks passed: 66 focused tests, web typecheck, file-scoped lint, formatting, and whitespace checks. Live provider execution was not exercised.