Skip to content

feat(cloud-agent): add token exchange endpoint - #8438

Merged
eshurakov merged 3 commits into
mainfrom
eshurakov/auburn-juniper
Oct 21, 2026
Merged

eshurakov merged 3 commits into
mainfrom
eshurakov/auburn-juniper

Conversation

@eshurakov

@eshurakov eshurakov commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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.

@eshurakov eshurakov changed the title feat(cloud-agent): add CLI credential negotiation feat(cloud-agent): add token exchange endpoint Oct 21, 2026
@kilo-code-bot

kilo-code-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The follow-up commits (e99d1095c, 1b3a095a6) resolve all 14 prior findings: the rollout gate now defers to the issuer for modern sources while still blocking legacy fallback, the body/header organization is reconciled with a 400 on mismatch, every route error uses the success: false shape with Cache-Control: no-store, the handler is wrapped in withRestTiming, and the contract jsdoc matches the actual status codes (401 for modern org-bound, 403 for legacy/restricted org-bound, 403 for header non-member via validateUserAuthorization).

Assumptions: [INFERENCE] The new tests were not executed here (read-only review); the PR reports them passing.

Files Reviewed (2 files)
  • apps/web/src/app/api/cloud-agent-next/token/route.ts - incremental diff + full file
  • apps/web/src/app/api/cloud-agent-next/token/route.test.ts - incremental diff + full file
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)
  • apps/web/src/app/api/cloud-agent-next/token/route.ts
  • apps/web/src/app/api/cloud-agent-next/token/route.test.ts

Reviewed by deepseek-flash-latest · Input: 65K · Output: 13.9K · Cached: 1.1M

Review guidance: REVIEW.md from base branch main

@eshurakov
eshurakov enabled auto-merge October 21, 2026 14:52

@iscekic iscekic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 organizationId is rejected at apps/web/src/lib/auth/resource-delegation.ts: hasRestrictedPrincipalClaims omits organizationId, so the modern branch calls unauthorized() and returns 401.
  • A legacy generateOrganizationApiToken token (15 minutes, with organizationId and organizationRole) is rejected by hasUnsafeLegacyClaims with 403 Unsupported 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 both route.test.ts and route.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-store is present on the 403 path.

@eshurakov
eshurakov added this pull request to the merge queue Oct 21, 2026
@eshurakov
eshurakov removed this pull request from the merge queue due to a manual request Oct 21, 2026
- 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.
@eshurakov

Copy link
Copy Markdown
Contributor Author

Review-body items without a thread:

  • Accepted endpoints / personal source: the route jsdoc now states the cloud-agent-next audience (Worker and /api/cloud-agent-next/balance only; not /api/trpc/* or stream-ticket) and that the source credential must be personal. Modern org-bound bearers return 401; legacy org-bound bearers return 403.
  • Test gaps: added a non-member organization UUID case and a no-store assertion on the 403 path. The route.auth.test.ts split (real outer auth) is deferred to a follow-up; it needs the large mock set that balance/route.auth.test.ts carries.
  • Production prerequisite: unchanged by this PR. Both flags must be exact true in production before the CLI adopts the route.

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.

2 participants