Skip to content

feat(cloud-agent-next): consume Kilo opaque session capabilities#4469

Open
eshurakov wants to merge 2 commits into
mainfrom
tundra-count
Open

feat(cloud-agent-next): consume Kilo opaque session capabilities#4469
eshurakov wants to merge 2 commits into
mainfrom
tundra-count

Conversation

@eshurakov

Copy link
Copy Markdown
Contributor

Summary

Why

Cloud Agent sandboxes currently receive the raw, long-lived Kilo user token in plaintext across many carriers — KILOCODE_TOKEN, KILO_AUTH_CONTENT, auth.json, provider apiKey/kilocodeToken, and the wrapper-to-Worker workerAuthToken. Any compromise or accidental logging inside the untrusted sandbox exposes a reusable user credential. This PR contains that token by issuing a short-lived opaque kka1.* Kilo capability per dispatch and redeeming it per-request at the existing managed-SCM outbound interceptor, so contained sandboxes never see the raw token.

What was done

  • Issue one opaque kka1.* Kilo capability per wrapper dispatch (via git-token-service) when the session uses managed SCM containment, and replace every sandbox-visible carrier of the raw token (KILOCODE_TOKEN, KILO_AUTH_CONTENT, auth.json, provider apiKey/kilocodeToken/baseURL) with that capability. The raw token now lives only in trusted Worker/DO/broker metadata.
  • Extend the managed-SCM outbound interceptor (sandbox-outbound.ts) to classify and redeem kka1.* capabilities through a new route-aware redeemKiloSessionCapability broker policy, reusing the same per-container egress interception already used for GitHub/GitLab capabilities. The capability carries normalized targets so runtime policy cannot drift.
  • Replace the raw Kilo JWT used as workerAuthToken with a fenced wrapper_dispatch_ticket JWT (4h lifetime) carrying the dispatch fence (cloud-agent session + kiloSessionId + wrapperRunId/wrapperGeneration/wrapperConnectionId). Wrapper-owned routes (/ingest, /kilo-global-ingest, /logs) validate the ticket and reject fence mismatches; a legacy raw-Kilo-JWT path keeps already-bound wrappers working until their next dispatch.
  • Authorize the session-ingest bootstrap: a bounded clone-based parse of POST /api/session extracts the bootstrap sessionId and permits it only when it matches the capability-bound Kilo session. Existing exact-path ingest routes (/api/session/<id>/{export,import,ingest}) continue to cover historical full-sync and incremental batches.
  • Wrapper auth.json is now written from the materialized KILOCODE_TOKEN capability and refreshed on every ready request; the log uploader reads the current dispatch ticket lazily (it outlives any single ticket) and aborts active uploads before swapping uploaders.
  • Fallback: uncontained sessions and DIND sandboxes (which don't set HTTPS interception) fall back to the real token and authenticate directly.
  • Presentation boundary: MessageBubble renders the Worker-published sanitized string assistant errors instead of throwing when the error is a plain string.

High-level architecture

sequenceDiagram
  participant DO as SessionService (DO, trusted)
  participant GTS as git-token-service (trusted broker)
  participant SB as Sandbox wrapper/SDK (untrusted)
  participant INT as Outbound interceptor (egress)
  participant UP as Kilo upstream (backend/provider/ingest)

  rect rgb(220,240,220)
  Note over DO,GTS: Trusted: raw user token never leaves here
  DO->>GTS: issueKiloSessionCapability(userToken, targets, containerId)
  GTS-->>DO: opaque kka1.* capability
  end

  DO->>SB: KILOCODE_TOKEN / KILO_AUTH_CONTENT / auth.json = capability<br/>workerAuthToken = wrapper_dispatch_ticket (fenced)
  SB->>INT: Kilo SDK outbound (Bearer kka1.*)
  INT->>GTS: redeemKiloSessionCapability(capability, containerId, method, url, bootstrapSessionId?)
  GTS-->>INT: real authorization + routeClass
  INT->>UP: forward with real Authorization
  UP-->>INT: response
  INT-->>SB: response

  alt bootstrap POST /api/session
    INT->>INT: parse body.sessionId, permit only if == capability-bound session
  end
  alt uncontained / DIND sandbox
    DO->>SB: KILOCODE_TOKEN = raw userToken (direct egress, no interception)
  end
Loading

Two distinct credentials are introduced, on purpose:

  • kka1.* Kilo capability — sandbox → Kilo upstream egress, redeemed per-request at the outbound interceptor (the headline containment change).
  • wrapper_dispatch_ticket JWT — wrapper → Worker routes only (/ingest, /kilo-global-ingest, /logs), minted with NEXTAUTH_SECRET and carrying the dispatch fence so a stolen ticket cannot be replayed against a different session/connection.

Architecture decision

Decision: Use one opaque kka1.* Kilo capability everywhere the sandbox previously received the raw Kilo token, issued once per dispatch and redeemed per-request through the existing route-aware managed-SCM broker.

Context: The sandbox is an untrusted execution environment that must present a Kilo credential to call backend API, provider-model, and session-ingest routes. The raw user token is long-lived and crossed many plaintext carriers inside the sandbox. The repo already has a managed-SCM containment + per-container outbound-interception mechanism for GitHub/GitLab capabilities, gated by MANAGED_SCM_CONTAINMENT_ORG_IDS.

Rationale: Reusing the existing outbound interception and the git-token-service broker (with SCM_SESSION_CAPABILITY_ENCRYPTION_KEY) keeps the raw token in trusted metadata only, leaves the Kilo CLI unchanged, and adds no new secret. A per-dispatch capability with a 4h lifetime (intentionally no background refresh — a new trusted dispatch mints a fresh one) bounds exposure, and the route-aware allowlist narrows what a leaked capability can reach.

Alternatives considered:

  • Short-lived raw Kilo token rotation inside the sandbox. Rejected because the raw token would still be plaintext and reusable for its lifetime inside the sandbox, and it establishes no route-aware allowlist.

Consequences: Contained sessions get strong token containment but depend on the outbound interceptor being able to intercept egress (ports 80/443); DIND sandboxes and uncontained orgs fall back to the raw token. The wrapper-to-Worker auth surface changes from a raw Kilo JWT to a fenced dispatch ticket, so already-running wrappers rely on a legacy-JWT path until their next dispatch. Route classification is strict: the provider baseURL baked into the capability targets must match what the sandbox actually calls, or valid capabilities are rejected as upstream_not_allowed.

Verification

  • Standard and DIND outbound probes against a real contained sandbox: capability redemption, HTTPS interception, response streaming, request cancellation, CA propagation, and egress cleanup.
  • Deployed test-Worker smoke: a fresh contained kg session reported OPAQUE_CAPABILITY_PRESENT, and Logpush confirmed kka1 redemption by the containment container with 200 responses on backend API, organization-model discovery, and provider-model routes.

Session-history persistence through the ingest bootstrap path was discovered broken during that smoke (Kilo's import bootstrap emits POST /api/session, which the capability policy rejected as upstream_not_allowed) and fixed in this commit. It has unit/integration coverage but has not been re-verified end-to-end against a deployed Worker — that is the main open verification item.

Visual Changes

Minor: MessageBubble now renders the Worker-published sanitized string assistant errors instead of throwing when the error field is a plain string. No screenshot; this is a fallback rendering path for error strings already produced by the Worker sanitization boundary.

Reviewer Notes

  • Open verification: the ingest bootstrap fix needs a deployed re-test to confirm session history is persisted to ingest. The redemption/forwarding paths above were verified before that fix and are unaffected by it.
  • Rollout order: requires the git-token-service deployment that implements issueKiloSessionCapability / redeemKiloSessionCapability. Containment is gated by MANAGED_SCM_CONTAINMENT_ORG_IDS; the wrangler.jsonc change setting it to * is the local env.dev block only (for branch testing), not production.
  • Compatibility: a legacy raw-Kilo-JWT workerAuthToken path keeps already-bound wrappers working until their next dispatch; DIND sandboxes and uncontained orgs fall back to the raw token and authenticate directly.
  • Strict route matching: the sandbox's provider baseURL must match the capability's baked-in targets, or valid capabilities are rejected as upstream_not_allowed.
  • Debug flag: LOG_REJECTED_KILO_URLS=1 is local-debug-only and logs full rejected URLs (which may contain tokens); it must never be set on a deployed Worker.
  • Package unit, integration, wrapper, typecheck, lint, and formatting checks pass.

Comment thread services/cloud-agent-next/src/auth.ts Outdated
Comment thread services/cloud-agent-next/src/server.ts
Comment thread services/cloud-agent-next/src/session-service.ts Outdated
Comment thread services/cloud-agent-next/src/sandbox-outbound.ts
Comment thread services/cloud-agent-next/src/sandbox-outbound.ts Outdated
Comment thread services/cloud-agent-next/wrapper/src/server.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

This incremental pass reviewed the latest commit (775c382), which correctly and completely fixes all 3 outstanding must-fix/warning items from the prior human review round (session-service.ts capability issuance retryability, sessionIngestBaseUrl propagation, and the server.ts legacy-wrapper kiloSessionId regression) with matching new unit tests; no new issues were found in the changed code.

Files Reviewed (4 files)
  • services/cloud-agent-next/src/server.ts - kiloSessionId query param is now required only for wrapper_dispatch_ticket claims; legacy raw-JWT uploads fall back to the DB-resolved kiloSessionId from requireCurrentSessionAccess
  • services/cloud-agent-next/src/server.test.ts - new coverage for the legacy log-upload URL without a kiloSessionId query parameter
  • services/cloud-agent-next/src/session-service.ts - issueKiloSessionCapability now returns and propagates sessionIngestBaseUrl to all call sites (fresh sessions, restores, devcontainer env); transient issuance failures (rpc_error, service_not_configured, temporarily_unavailable) now map to retryable WORKSPACE_SETUP_FAILED instead of non-retryable INVALID_REQUEST
  • services/cloud-agent-next/src/session-service.test.ts - new coverage for KILO_SESSION_INGEST_URL materialization and retryable capability-issuance failures
Previous Review Summaries (2 snapshots, latest commit 033002e)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 033002e)

Status: No Issues Found | Recommendation: Merge

Executive Summary

All 6 previously flagged issues (the CRITICAL JWT-audience bypass and 5 WARNING/SUGGESTION findings across auth, server, session-service, sandbox-outbound, and the wrapper log uploader) are correctly and completely fixed in this incremental diff, with no new issues found in the changed code.

Files Reviewed (10 files)
  • services/cloud-agent-next/src/auth.ts / auth.test.ts - legacy JWT fallback now re-verifies via verifyKiloToken, restoring the audience check
  • services/cloud-agent-next/src/server.ts - /ingest now rejects malformed wrapperGeneration with 400 instead of silently skipping the fence check
  • services/cloud-agent-next/src/session-service.ts / session-service.test.ts - buildSessionForContext now forwards kiloProviderBaseUrl from both call sites
  • services/cloud-agent-next/src/sandbox-outbound.ts / sandbox-outbound.test.ts - bootstrap body is now read via a bounded reader that stops once it exceeds the byte limit; debug diagnostics now redact the query string before logging
  • services/cloud-agent-next/wrapper/src/log-uploader.ts / log-uploader.test.ts - kiloSessionId is now read lazily via getKiloSessionId(), staying paired with the auth token after a rebind
  • services/cloud-agent-next/wrapper/src/server.ts - passes the lazy getKiloSessionId getter into the log uploader

Previous review (commit 71a01cf)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

The new legacy-JWT fallback in validateWrapperDispatchTicket (auth.ts) drops the audience check that the primary auth path enforces, letting narrowly-scoped Kilo JWTs authenticate as full wrapper auth on /kilo-global-ingest, /ingest, and /logs.

Overview

Severity Count
CRITICAL 1
WARNING 4
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
services/cloud-agent-next/src/auth.ts 143 Legacy JWT fallback skips the audience check verifyKiloToken enforces, widening accepted tokens for /kilo-global-ingest, /ingest, /logs

WARNING

File Line Issue
services/cloud-agent-next/src/session-service.ts 2429 buildSessionForContext doesn't forward kiloProviderBaseUrl, risking upstream_not_allowed for sessions built via this path under managed SCM containment
services/cloud-agent-next/src/sandbox-outbound.ts 439 Debug diagnostics (LOG_REJECTED_KILO_URLS=1) log the full rejected URL including credential-bearing query params
services/cloud-agent-next/wrapper/src/server.ts 353 Log uploader's kiloSessionId is captured once at creation while the paired auth token is now read lazily, risking a stale/mismatched pair after a rebind

SUGGESTION

File Line Issue
services/cloud-agent-next/src/server.ts 172 Malformed wrapperGeneration silently disables that fence check instead of rejecting, unlike the sibling /kilo-global-ingest route
services/cloud-agent-next/src/sandbox-outbound.ts 145 Session-bootstrap body size is checked after fully buffering, not while reading, despite the "bounded" parse intent
Files Reviewed (27 files)
  • services/cloud-agent-next/src/auth.ts / auth.test.ts - 1 issue
  • services/cloud-agent-next/src/server.ts / server.test.ts - 1 issue
  • services/cloud-agent-next/src/session-service.ts / session-service.test.ts - 1 issue
  • services/cloud-agent-next/src/sandbox-outbound.ts / sandbox-outbound.test.ts - 2 issues
  • services/cloud-agent-next/wrapper/src/server.ts / server.test.ts - 1 issue
  • services/cloud-agent-next/wrapper/src/main.ts
  • services/cloud-agent-next/wrapper/src/session-bootstrap.ts / session-bootstrap.test.ts
  • services/cloud-agent-next/wrapper/src/log-uploader.ts / log-uploader.test.ts
  • services/cloud-agent-next/src/kilo/kilo-targets.ts / kilo-targets.test.ts
  • services/cloud-agent-next/src/services/git-token-service-client.ts / git-token-service-client.test.ts
  • services/cloud-agent-next/src/model-validation.ts / model-validation.test.ts
  • services/cloud-agent-next/src/persistence/types.ts
  • services/cloud-agent-next/src/types.ts
  • services/cloud-agent-next/src/session/agent-runtime.test.ts
  • services/cloud-agent-next/worker-configuration.d.ts
  • services/cloud-agent-next/wrangler.jsonc
  • services/cloud-agent-next/.dev.vars.example
  • apps/web/src/components/cloud-agent-next/MessageBubble.tsx / MessageBubble.test.ts

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-5 · Input: 78 · Output: 18.4K · Cached: 2.6M

Review guidance: REVIEW.md from base branch main

@pandemicsyn pandemicsyn 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.

lgtm, couple of feedback items but pre-approved

KILOCODE_TOKEN: kilocodeToken,
KILO_AUTH_CONTENT: JSON.stringify({ kilo: { type: 'api', key: originalToken } }),
// Opaque Kilo capability — redeemed for the real credential at the outbound interceptor
KILOCODE_TOKEN: kiloCapability,

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.

Must-fix: This capability is exposed to sandbox code, but redemption currently treats almost every path under backendBaseUrl as backend_api and substitutes the full user bearer token. The sandbox can therefore call a credential-minting route such as POST /api/organizations/:id/user-tokens and receive an exportable organization JWT, defeating credential containment. Replace the backend catch-all with an explicit method-and-path allowlist for the Kilo runtime operations that are actually required.

Comment thread services/cloud-agent-next/src/session-service.ts
Comment thread services/cloud-agent-next/src/server.ts
Comment thread services/cloud-agent-next/src/session-service.ts Outdated
…sumption

- Materialize the derived sessionIngestBaseUrl into normal and devcontainer
  runtime env so contained restore/import/ingest calls match the interceptor
  origin baked into the capability, not the raw localhost value
- Accept the legacy log-upload URL without kiloSessionId for legacy_kilo_token
  claims; resolve the authoritative kiloSessionId from current session access,
  keeping the query/claim requirement for dispatch tickets
- Map transient Kilo capability issuance failures (rpc_error,
  service_not_configured, temporarily_unavailable) to retryable
  workspaceSetupFailed instead of terminal invalidRequest
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