feat: codeburn sync — push usage telemetry to a team backend (preview)#660
Conversation
Adds an opt-in `codeburn sync` command group: setup, push, status,
logout, reset. Developers authenticate via standard OIDC browser login
(Authorization Code + PKCE, provider-agnostic — Cognito, Auth0, Okta,
Keycloak, etc.) and push per-call usage metadata to a team-operated
OTLP/HTTP endpoint.
Privacy is structural: the payload builder has no field that can carry
prompts, responses, file contents, or bash commands. Only metadata
leaves the machine (provider, model, tokens, cost, project, tool names,
timestamps). There is deliberately no flag to include more.
Setup derives everything from a single URL:
GET {base}/.well-known/codeburn-export.json
→ issuer, client_id, traces_path, max_batch_size
Modules:
- discovery.ts discovery doc fetch/validation (version-gated)
- auth.ts OIDC discovery, PKCE, localhost callback (:19876-78),
token exchange/refresh/revoke
- credentials.ts refresh token in OS keychain (macOS security / Linux
libsecret / Windows DPAPI / 0600 file fallback). All
subprocess calls use execFileSync arg arrays; tokens
flow via stdin or env var — never shell-interpolated
- config.ts ~/.config/codeburn/sync.json
- ledger.ts client-side sent-ledger for exact dedup (no watermark
races), 6-month prune
- otlp.ts ParsedApiCall[] → ExportTraceServiceRequest JSON with
deterministic IDs: span=SHA256(dedupKey)[:8],
trace=SHA256(sessionId)[:16] — every retry idempotent
- push.ts orchestration with typed outcomes; pushes run to
completion — 429 Retry-After honored (≤120s/wait,
3 retries/batch) before deferring; partial_success is
batch-atomic (nothing ledgered, whole batch retries);
50K safety valve, no routine cap
- cli.ts command layer; --dry-run reports exact counts/cost
without sending
Preview feature: protocol may change between releases.
AI-Origin: human
Offline, CI-safe (70 tests): - sync.test.ts (26): discovery parsing, PKCE, auth URL, scope negotiation, callback server (state mismatch, IdP error, timeout, port fallback), config round-trip - sync-ledger-otlp.test.ts (23): deterministic ID derivation, OTLP structure/attributes, batching, ledger append/dedup/prune/clear - sync-push.test.ts (15): pipeline against a scriptable mock OTLP server — success+ledger+cost, partial-success batch NOT ledgered, 401 stop, 429 wait-and-retry / persistent-429 give-up / Retry-After parsing (delta-seconds, HTTP-date, garbage), 5xx deferral, idempotent failure recovery - sync-e2e.test.ts (6) + fixtures/mock-idp.ts: full setup→refresh→ rotate→logout round-trip against an in-process mock IdP Developer-only (skip unless env vars set, never CI): - sync-headless-e2e.test.ts (1): real browser PKCE flow via Playwright - sync-infra-e2e.test.ts (4): push/auth-reject/batch/idempotent re-push against a deployed backend AI-Origin: human
- docs/sync/README.md: setup, push, status, logout, reset; privacy guarantees; FAQ - docs/sync/DEVELOPER.md: architecture, discovery/OTLP protocol, sent-ledger design rationale, partial-success and rate-limiting semantics, server contract, testing guide - README.md: sync command group listed under Commands (preview label) AI-Origin: human
codeburn sync — push usage telemetry to a team backend (preview)codeburn sync — push usage telemetry to a team backend (preview)
|
Reviewed the full diff at cec61e3, plus a local run of the offline suite on macOS. Short version: this is a solid implementation and it holds up against what we locked in #625. All twelve amendments are implemented as agreed; I verified each in code rather than from the description. Two-hop discovery (codeburn-export.json first, then OIDC discovery against the issuer), fixed loopback ports with the 127.0.0.1 literal, scope negotiation from Three items to address before merge, then smaller notes. Before merge1. Enforce https on every remote endpoint. 2. Keep the e2e tests off the real macOS keychain. The HOME stubbing in tests/sync-e2e.test.ts isolates the config and the file store, and it works on Linux. On darwin, 3. Pin golden values for the deterministic IDs. The encoding tests pin the timestamp and attribute mapping, which is good. Smaller items (fine as fast follows)
Your two open questions
One note for other reviewers: playwright lands as a devDependency for the skip-by-default headless test; no runtime dependency change (verified in the lockfile). Good work on this. With items 1 to 3 addressed, this is ready. |
…tion, golden ID pins Before-merge items: 1. HTTPS enforced on every remote endpoint (RFC 8252 §8.3): baseUrl, issuer, authorization/token/revocation endpoints, and the traces endpoint all reject non-https, with a loopback (127.0.0.1/::1/ localhost) exception for offline tests and local dev. Enforcement is central (assertHttps) — the browser-open guard is no longer the only check whose failure was swallowed. 2. Credential store test isolation: CODEBURN_SYNC_TOKEN_STORE=file forces the file store (honors HOME) so the offline suite never touches the real macOS login keychain. Set in the e2e suite. 3. Golden pins for deriveSpanId/deriveTraceId/deriveDeviceId with fixed inputs and expected hex — the idempotency contract depends on these encodings being stable across releases; a green-tests encoding change would silently double-count history on span-ID-keyed backends. getDeviceId refactored over a pure deriveDeviceId(host, user). Smaller review items: - Callback server: ready promise resolves the actually-bound port from the listening event (kills the 100ms-sleep race after port fallback); Connection: close on all responses + closeAllConnections() on shutdown (pooled keep-alive sockets from a closed server could swallow requests aimed at a later server on the same port); error handler guarded so a post-bind error can never rebind to a different port than advertised; optional ports param ([0] = ephemeral) removes fixed-port contention between parallel test workers. - fetchOidcConfig verifies the issuer claim matches the fetch origin (OIDC Discovery §4.3 mix-up defense). - partialSuccess.rejectedSpans wrapped in Number() — proto3 int64 JSON mapping sends strings from strict protojson servers; += would concatenate. - Ledger writes are atomic (temp + rename); corrupt-ledger recovery and no-tmp-left-behind tests added; XDG_CACHE_HOME honored (ledger is reconstructible state, not config). - Mock IdP now implements /oauth2/authorize (registers PKCE challenge, 302s to redirect_uri) and verifies S256 code_verifier + single-use codes at the token endpoint. The e2e drives the real redirect flow and asserts wrong-verifier and code-reuse are rejected — PKCE binding is now exercised end to end. - sync reset calls clearLedger() instead of reimplementing the path. - push sets exit code 1 on rate-limited/server-error outcomes so cron and script callers can detect incomplete pushes. Deferred (noted for fast-follow): macOS 'security -i' stdin mode (untestable on this Linux box), ai.cost_estimated as a real ParsedApiCall flag (touches core parser types). Sync suite: 81 passing (5x stable), 5 developer-only. AI-Origin: human
|
Added a commit to address review comments. The blockers and fast follows were addressed except the following:
|
iamtoruk
left a comment
There was a problem hiding this comment.
Verified d8a7b2a against each review item, in code:
- https enforcement is central (
assertHttps) and covers baseUrl, issuer, authorization/token/revocation endpoints, and the traces endpoint, with the loopback exception. - Ran the offline suite on macOS: 81/81 pass, and the credential-store isolation keeps the real keychain untouched.
- Recomputed the three golden hashes independently; all three pins are correct.
- Also confirmed: bound-port ready promise, issuer claim check,
Number()on rejectedSpans, atomic ledger writes plusXDG_CACHE_HOME, PKCE verification in the mock IdP with wrong-verifier and code-reuse tests,clearLedger()reuse, and exit code 1 on incomplete pushes.
Both deferrals are agreed:
security -i: keep it as a follow-up. Strict charset validation on the token before it goes anywhere near security's own parser is the right shape, and I can verify the escaping on a Mac when that PR comes.usageSource: 'reported' | 'estimated'onParsedApiCallworks for me as the field name and shape. Setting it at parse time with aPROVIDER_PARSE_VERSIONSbump is the right layering. Ping me on that PR and I will review.
One non-blocking note for the follow-up pile: when all callback ports are in use, both ready and the callback promise reject, and the unawaited one surfaces as an unhandled rejection trace instead of the clean AuthError message. Cosmetic.
Merging. Thanks for the fast, thorough turnaround on this.
Summary
Adds an opt-in
codeburn synccommand group that pushes AI usage metadata from a developer's machine to a team-operated OTLP-compatible backend, authenticated via standard OIDC browser login. This lets teams aggregate cost/usage dashboards across developers while keeping codeburn's local-first model: nothing is sent unless a developer explicitly runssync setupandsync push.Privacy is structural, not configurational: the OTLP payload builder has no field that can carry prompts, responses, file contents, or bash commands. Only per-call metadata leaves the machine — provider, model, token counts, cost, project name, tool names, and timestamps. There is deliberately no flag to include more.
Marked preview — the discovery/protocol may change between releases.
Design
Full design doc: #625
Key decisions:
offline_accessscope negotiated from IdP metadata.~/.cache/codeburn/sync-ledger.json) — exact dedup keyed by the existingdeduplicationKey. Chosen over timestamp watermarks (which silently skip late-arriving calls) and over server-side dedup (which requires a smarter backend).span_id = SHA-256(deduplicationKey)[:8],trace_id = SHA-256(sessionId)[:16]. Every retry path is idempotent; servers that key by span ID treat re-sends as upserts.Retry-Afteris honored (delta-seconds or HTTP-date, ≤120s per wait, 3 retries per batch) before deferring to the next push. 50K safety valve for pathological cases.partial_successdoesn't identify which spans were rejected, so a partially-rejected batch is not ledgered and retries whole (safe via deterministic IDs).SHA-256(hostname:username)[:16]as a resource attribute; developer identity comes from the JWTsubserver-side.What's in the PR
src/sync/discovery.tssrc/sync/auth.tssrc/sync/credentials.tsexecFileSyncarg arrays; tokens flow via stdin (Linux) / env var (Windows), never shell-interpolatedsrc/sync/config.ts~/.config/codeburn/sync.jsonsrc/sync/ledger.tssrc/sync/otlp.tsParsedApiCall[]→ExportTraceServiceRequestJSONsrc/sync/push.tscomplete/auth-rejected/rate-limited/server-error)src/sync/cli.tssetup/push/status/logout/resetcommandsdocs/sync/README.mddocs/sync/DEVELOPER.mdNo changes to existing code beyond two registration lines in
src/main.tsand a README section. 22 files, +3,549 lines.Testing
70 tests (all offline, CI-safe) + 5 developer-only integration tests:
sync.test.ts(26) — discovery parsing, PKCE, auth URL, scope negotiation, callback server (state mismatch, IdP error, timeout, port fallback), config round-tripsync-ledger-otlp.test.ts(23) — deterministic ID derivation, OTLP structure/attributes, batching, ledger append/dedup/prune/clearsync-push.test.ts(15) — full pipeline against a scriptable mock OTLP server: success + ledger + cost, partial-success (batch NOT ledgered), 401 stop, 429 wait-and-retry / persistent-429 give-up / Retry-After parsing (delta, HTTP-date, garbage), 5xx deferral, idempotent failure recoverysync-e2e.test.ts(6) — full setup→refresh→rotate→logout round-trip against an in-process mock IdPVerified end-to-end against a deployed test backend (API Gateway JWT authorizer + Lambda + Cognito): initial 8,015-call backfill in one command (~16s), re-push correctly no-ops, invalid token → 401, logout revokes.
Backend requirements (documented in DEVELOPER.md)
Any endpoint that accepts OTLP/HTTP JSON behind an OIDC-validating gateway works — an OTEL Collector (ADOT, Alloy, otelcol) or a minimal custom ingest. The repo does not ship backend infra; the server contract is: serve the discovery doc, validate JWTs from the configured issuer, accept OTLP JSON, return standard OTLP responses.
Compatibility
crypto,http,child_process)codeburn syncOpen questions for maintainers
CODEBURN_SYNC=1) or is the docs label enough?~/.cache/codeburn/vs alongside config — preference?