codeburn/docs/sync/DEVELOPER.md
Resham Joshi 8e61ac76af
Some checks failed
CI / semgrep (push) Has been cancelled
Tests / test (22) (push) Has been cancelled
Tests / test (22.13.0) (push) Has been cancelled
feat(sync): additive CB-3 usage-span fields and coverage watermark (Teams boundary spec section 7) (#1135, #1140) (#1147)
2026-08-25 11:32:45 -07:00

12 KiB

Sync — Developer Documentation

Architecture, protocol, server contract, and testing for codeburn sync.

Architecture

Developer machine                          Remote backend
──────────────────                         ──────────────
~/.config/codeburn/sync.json  (config)
~/.config/codeburn/.sync-token (credential)
~/.cache/codeburn/sync-ledger.json (sent-ledger)

codeburn sync push
  │
  ├─ Read config → baseUrl, clientId, issuer, tracesPath
  ├─ Read refresh token from OS store
  ├─ POST {issuer}/oauth2/token (refresh_token grant) → access_token
  ├─ Collect ParsedProviderCall[] for window
  ├─ Filter against sent-ledger (only unsent calls)
  ├─ Build OTLP/HTTP JSON payload
  ├─ POST {baseUrl}{tracesPath} with Bearer token
  ├─ On success → append deduplicationKeys to ledger
  └─ Update lastSync in config

Discovery Protocol

Server discovery document

GET {baseUrl}/.well-known/codeburn-export.json
{
  "version": 1,
  "issuer": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXX",
  "client_id": "70e6sgst2ju6ff9dnrmv4l1tcb",
  "scopes": ["openid", "email"],
  "traces_path": "/v1/traces",
  "max_batch_size": 1000
}
Field Required Default Description
version No 1 Client rejects version > 1
issuer Yes OIDC issuer URL. Client fetches {issuer}/.well-known/openid-configuration
client_id Yes OAuth client ID for this deployment
scopes No ["openid"] Scopes to request. offline_access added dynamically if IdP supports it
traces_path No /v1/traces Path for OTLP POST
max_batch_size No 1000 Max spans per HTTP request

Why not proxy .well-known/openid-configuration?

OIDC requires the issuer claim inside the discovery doc to match the URL it was fetched from. Serving Cognito's doc from a different domain violates this constraint. The codeburn-export.json doc decouples the metrics endpoint from the identity provider.

OIDC Authentication

Flow: Authorization Code + PKCE

  1. Client generates code_verifier (32 random bytes, base64url) and code_challenge (SHA-256 of verifier, base64url)
  2. Client starts callback server on 127.0.0.1:19876 (fallback: 19877, 19878)
  3. Browser opens {authorization_endpoint}?response_type=code&client_id=...&redirect_uri=http://127.0.0.1:{port}/callback&code_challenge=...&code_challenge_method=S256&state=...&scope=...
  4. User logs in at IdP → IdP redirects to http://127.0.0.1:{port}/callback?code=...&state=...
  5. Callback server validates state, extracts code
  6. Client POSTs to {token_endpoint} with grant_type=authorization_code, code, code_verifier, redirect_uri, client_id
  7. IdP returns access_token + refresh_token

Fixed ports

Cognito (and Okta) do exact string comparison on callback URLs. Ephemeral ports fail. We register three fixed ports: 19876, 19877, 19878. The client tries in order, falling back if a port is in use.

RFC 8252 recommends 127.0.0.1 (IP literal) over localhost to avoid IPv6 ::1 resolution.

Token refresh

On every sync push:

  1. Read refresh token from OS store
  2. POST {token_endpoint} with grant_type=refresh_token
  3. Store whatever refresh token the server returns (handles rotation transparently)
  4. On invalid_grant → stop, prompt user to re-run sync setup

Scope resolution

  • Request scopes from codeburn-export.json
  • Add offline_access only if scopes_supported in OIDC discovery includes it
  • Cognito rejects offline_access as invalid_scope — it issues refresh tokens without it

Credential Storage

Platform Method Implementation
macOS Keychain security add-generic-password / find-generic-password
Linux libsecret secret-tool store / secret-tool lookup
Windows DPAPI PowerShell ConvertTo-SecureString / ConvertFrom-SecureString
Fallback File ~/.config/codeburn/.sync-token with 0600 permissions

No native modules (keytar is archived). Shell out to OS CLIs. Fallback reported honestly in sync status.

OTLP Encoding

Strict protobuf-JSON mapping of ExportTraceServiceRequest. lowerCamelCase fields, hex-encoded IDs, integer enums.

Span identity (deterministic)

span_id   = first 8 bytes of SHA-256(deduplicationKey) → hex (16 chars)
trace_id  = first 16 bytes of SHA-256(sessionId)       → hex (32 chars)

Re-sends are byte-identical. Server-side dedup is defense-in-depth.

Resource attributes

{
  "resource": {
    "attributes": [
      { "key": "codeburn.device_id", "value": { "stringValue": "<SHA-256(hostname+username)[:16]>" } },
      { "key": "codeburn.coverage_through", "value": { "stringValue": "2026-08-24" } }
    ]
  }
}

codeburn.coverage_through is optional. It is the ISO date the local corpus is complete through, taken from the daily-cache watermark, and is stamped only when a complete local parse finalized that watermark (complete and watermarkTrusted both set). Receivers should treat its absence as "coverage unknown", never as "no history".

Span attributes

{
  "attributes": [
    { "key": "ai.provider", "value": { "stringValue": "kiro" } },
    { "key": "ai.model", "value": { "stringValue": "claude-sonnet-4-6" } },
    { "key": "ai.input_tokens", "value": { "intValue": "12500" } },
    { "key": "ai.output_tokens", "value": { "intValue": "3200" } },
    { "key": "ai.cost_usd", "value": { "doubleValue": 0.085 } },
    { "key": "ai.project", "value": { "stringValue": "my-app" } },
    { "key": "ai.tools", "value": { "arrayValue": { "values": [{ "stringValue": "Edit" }] } } },
    { "key": "ai.speed", "value": { "stringValue": "standard" } },
    { "key": "ai.cost_estimated", "value": { "boolValue": true } },
    { "key": "ai.work_unit_id", "value": { "stringValue": "ff1b1358ef64c52f80e50e7ae47ca176" } },
    { "key": "ai.session_role", "value": { "stringValue": "child" } },
    { "key": "ai.lineage_evidence", "value": { "stringValue": "provider-recorded" } },
    { "key": "ai.cache_read_tokens", "value": { "intValue": "800" } },
    { "key": "ai.cache_write_tokens", "value": { "intValue": "200" } },
    { "key": "ai.call_count", "value": { "intValue": "3" } },
    { "key": "ai.session_duration_ms", "value": { "intValue": "61000" } },
    { "key": "ai.subscription_covered", "value": { "boolValue": true } }
  ]
}

Every attribute from ai.work_unit_id down is optional and sent only when its value is proven; an old receiver ignoring unknown attributes loses nothing:

  • ai.work_unit_id, ai.session_role, ai.lineage_evidence are emitted together or not at all, and only for sessions whose provider durably recorded lineage (#1140). ai.work_unit_id is deriveTraceId of the root session id resolved by the #1145 work-unit resolver: the exact trace-id derivation above, so a unit's identity matches the root trace already on the wire. Lineage is never inferred: a session without recorded lineage carries none of the three, and a child whose parent is out of range (or whose link is ambiguous or cyclic) fails closed and carries none either.
  • ai.cache_read_tokens / ai.cache_write_tokens are the provider-recorded cache token counts, billable-consistent with ai.input_tokens (cache read takes the display layer's max(cacheReadInputTokens, cachedInputTokens) convention across the Anthropic and OpenAI vocabularies). Each is sent only when non-zero.
  • ai.call_count is the number of usage spans the span's session contributes in the synced window. ai.session_duration_ms is the last-minus-first provider-recorded event time of that session, omitted when either timestamp is missing or out of order.
  • ai.subscription_covered is the plan/proxy-path machinery's decision: true when a configured plan covers the call's provider or the session's provider-recorded cwd sits under a configured proxy path, false when both are ruled out, and omitted when the machinery cannot decide (no plan match and no cwd to check).

ai.project is optional. Usage spans include it only when CodeBurn can derive one safe basename from a provider-recorded absolute working directory. Attribution spans derive it only from the normalized git.repo; PR-only evidence omits it. Receivers must group a missing project as unattributed and must not require the field. When usage and attribution spans for one trace carry different safe basenames (for example, a fork checkout whose directory name differs from the upstream repository), the attribution span's normalized git.repo basename is authoritative for project aggregation. The usage cwd basename is provisional; receivers must not count both as separate projects.

ai.output_tokens is the billable output total. For providers that meter reasoning separately from response tokens, CodeBurn includes that reasoning in this field; providers whose response count already includes reasoning are left unchanged.

Sent-Ledger

Client-side deduplication source of truth at ~/.cache/codeburn/sync-ledger.json.

Format: JSON array of { key: string, ts: string } objects.

Push logic: collect all calls in window → subtract ledger entries → send remainder → append to ledger on success.

Pruning: entries older than 6 months removed on every push.

Why not a watermark? Timestamp watermarks silently skip late-arriving calls (long sessions, providers that update rows). The ledger is exact.

Partial success

OTLP returns partial_success.rejected_spans in the response body. Because OTLP does not identify which spans were rejected, the client ledgers nothing for a partially-rejected batch — the entire batch retries on the next push. This is safe: span IDs are deterministic (derived from the deduplication key), so servers that store by span ID treat re-sent spans as idempotent upserts.

Rate limiting (429)

A push runs to completion — there is no routine per-push cap (only a 50,000-call safety valve). Server rate limits are the intended brake:

  • On HTTP 429 the client honors Retry-After (delta-seconds or HTTP-date), capped at 120 seconds per wait, defaulting to 5 seconds when the header is absent
  • The same batch is retried up to 3 consecutive times; if the server is still rate-limiting after that, the push stops and the remaining (unledgered) calls are sent on the next push
  • On 401 or 5xx the push stops immediately with the same resume-on-next-push behavior

Server contract

The backend must implement:

  1. GET {baseUrl}/.well-known/codeburn-export.json — returns the discovery doc (public, no auth)
  2. POST {baseUrl}{traces_path} — accepts OTLP/HTTP JSON with Bearer token
    • Validate JWT (issued by the configured IdP)
    • Derive developer identity from token's sub claim
    • Accept startTimeUnixNano up to 6 months in the past
    • Return standard OTLP response body

No PII is included in the payload. The server derives identity solely from the authenticated token.

Testing

Unit tests (tests/sync.test.ts)

26 tests covering pure functions: discovery parsing, PKCE generation, auth URL construction, scope resolution, callback server, config read/write. No network, no browser.

Mock IdP e2e (tests/sync-e2e.test.ts)

6 tests with a localhost mock IdP server. Exercises the full auth round-trip, token refresh, rotation, revocation — fully offline, runs in CI.

Headless browser e2e (tests/sync-headless-e2e.test.ts)

1 test with Playwright headless Chromium against real Cognito. Proves the actual browser PKCE flow works including Cognito Hosted UI form submission and localhost redirect.

Developer-only — requires:

  • Deployed test backend (CDK stack at ../codeburn-sync-backend/)
  • Cognito user with confirmed password
  • Environment variables: CODEBURN_SYNC_URL, CODEBURN_SYNC_EMAIL, CODEBURN_SYNC_PASSWORD
  • Playwright Chromium installed (PLAYWRIGHT_BROWSERS_PATH)

Skipped by default when env vars are not set. Never runs in CI.

Test CDK stack (codeburn-sync-backend/)

Minimal AWS backend for the headless e2e test:

  • Cognito User Pool (PKCE, fixed callback ports)
  • HTTP API with JWT authorizer
  • Discovery Lambda (serves codeburn-export.json)
  • Ingest Lambda (logs OTLP spans to CloudWatch)

Deploy: npx cdk deploy --profile andklee-dev Cost: ~$0/mo idle (pay-per-request)

This is a test fixture, not a production reference. Any OIDC provider + OTLP-accepting endpoint satisfies the server contract.