Offline-verifiable building blocks for GitHub Copilot; network paths are the
flagged live-verification risk and are covered by pure unit tests only.
- copilot/device_flow.rs: OAuth device flow (RFC 8628). No hardcoded client id —
ai-harness must register its own GitHub OAuth app and pass it in. Pure
parse_poll_response (pending/slow_down/success/failed) + slow_down interval bump;
request_device_code/poll_once/poll_for_token (injected sleep).
- copilot/token.rs: copilot_internal/v2/token exchange with cache + single-flight
refresh (tokio::Mutex) and direct-Bearer fallback on 401/403/404; needs_refresh
honors a 120s skew and expires_at==0 = never.
- copilot/provider.rs: CopilotProvider over api.githubcopilot.com, routing each
model to chat/responses/anthropic codec by its /models supported_endpoints
(parse_models + codec_for_endpoints); headers (X-GitHub-Api-Version, Openai-Intent,
x-initiator, Copilot-Vision-Request, anthropic-beta for anthropic models).
- 11 unit tests (device-flow parsing, token refresh math, codec routing, /models
parsing).
Deferred (needs live API + TUI work): EngineHandle::login device-flow modal,
startup /models fetch, auth.json-backed registry wiring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- modelsdev.rs: ModelCatalog parses models.dev api.json → ModelInfo keyed by
(provider, model); 24h file cache at ~/.cache/ai-harness/models.json with a
baked assets/models-snapshot.json fallback so cost/limits work offline.
load_cached_or_baked (no network) + refresh/refresh_default_cache (background).
- App: loads the catalog at init (cached-or-baked, never blocks), warms the cache
in a background task, and passes the session model's pricing into RunConfig.cost
so session cost accrues for real.
- TUI: AppState tracks session_cost/session_tokens from Session events; status bar
shows "<tokens> · $<cost>". Snapshots updated.
- 7 modelsdev tests (parse, defaults, unknown-model, baked snapshot, cache TTL,
cached-vs-baked load).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- auth.rs: AuthRecord (OAuth {access, refresh, expires} | Api {key}) with
is_expired(now, skew) honoring expires==0 = never; AuthStorage keyed by
provider id over ~/.local/share/ai-harness/auth.json.
- Read-modify-write on every op so concurrent refresh/login don't clobber;
writes go through a temp-file rename set to 0600 (unix) to avoid truncated
auth files.
- 6 tempdir tests: missing→empty, set/get/overwrite, multi-provider coexist,
scoped remove (+ no-op on absent), expiry skew/never, 0600 perms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/v1/messages request builder (cache_control breakpoints on the first 2
system blocks + last 2 messages, tool schema -> input_schema, extended
thinking budget) and an SSE decoder built on eventsource-stream, mapping
content_block_start/delta/stop and message_delta into our normalized
LlmEvent stream (text, thinking+signature, streamed tool-call JSON
accumulated and parsed at content_block_stop, usage merged from
message_start + message_delta). AnthropicProvider wires this to reqwest
with x-api-key/anthropic-version/anthropic-beta headers and classifies
HTTP errors into RateLimited/Auth/Overloaded/Http. ProviderRegistry
resolves "provider/model" strings.
Also tightened processor::process_step's cancellation: the event loop now
selects the stream poll against ctx.cancel instead of only checking at the
top of the loop, so a blocked provider stream is actually interrupted by
abort (matches docs/02-engine.md's cancellation semantics).
127 tests passing, clippy clean.
Seven-crate workspace per docs/01-architecture.md. harness-core gets the
domain types (Session/Message/Part/ToolState), a broadcast EventBus, the
last-match-wins wildcard permission evaluator, and a SQLite storage actor
(dedicated thread + mpsc, JSON-blob rows) with roundtrip tests. All other
crates are compiling stubs. CI runs fmt/clippy -D warnings/test.