@legioncodeinc/rflectr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.markdown-link-check.json +7 -0
  2. package/AGENTS.md +169 -0
  3. package/LICENSE +661 -0
  4. package/README.md +612 -0
  5. package/assets/733630021_1421561133353555_3999689754075308337_n.jpg +0 -0
  6. package/assets/github-home-image.png +0 -0
  7. package/assets/og-image.jpg +0 -0
  8. package/assets/og-image.png +0 -0
  9. package/assets/og-image.psd +0 -0
  10. package/assets/rflectr-no-bg.png +0 -0
  11. package/assets/vertex-models.example.json +14 -0
  12. package/dist/cli.js +15708 -0
  13. package/library/README.md +39 -0
  14. package/library/issues/README.md +46 -0
  15. package/library/issues/backlog/README.md +26 -0
  16. package/library/issues/completed/README.md +13 -0
  17. package/library/issues/in-work/README.md +13 -0
  18. package/library/knowledge/README.md +34 -0
  19. package/library/knowledge/private/README.md +40 -0
  20. package/library/knowledge/private/ai/README.md +8 -0
  21. package/library/knowledge/private/ai/model-discovery-classification.md +81 -0
  22. package/library/knowledge/private/ai/translation-layer.md +88 -0
  23. package/library/knowledge/private/architecture/README.md +10 -0
  24. package/library/knowledge/private/architecture/launch-flow-claude.md +93 -0
  25. package/library/knowledge/private/architecture/system-overview.md +108 -0
  26. package/library/knowledge/private/auth/README.md +9 -0
  27. package/library/knowledge/private/auth/oauth-device-flows.md +95 -0
  28. package/library/knowledge/private/data/README.md +8 -0
  29. package/library/knowledge/private/data/preferences-config.md +87 -0
  30. package/library/knowledge/private/data/provider-registry.md +126 -0
  31. package/library/knowledge/private/infrastructure/README.md +7 -0
  32. package/library/knowledge/private/infrastructure/server-gateway.md +87 -0
  33. package/library/knowledge/private/integrations/README.md +8 -0
  34. package/library/knowledge/private/integrations/harnesses.md +87 -0
  35. package/library/knowledge/private/integrations/local-proxy.md +82 -0
  36. package/library/knowledge/private/security/README.md +9 -0
  37. package/library/knowledge/private/security/credential-storage.md +129 -0
  38. package/library/knowledge/private/standards/documentation-framework.md +154 -0
  39. package/library/knowledge/public/README.md +49 -0
  40. package/library/knowledge/public/faqs/README.md +7 -0
  41. package/library/knowledge/public/faqs/troubleshooting.md +92 -0
  42. package/library/knowledge/public/guides/README.md +13 -0
  43. package/library/knowledge/public/guides/ai-agents.md +273 -0
  44. package/library/knowledge/public/guides/api-server.md +108 -0
  45. package/library/knowledge/public/guides/claude-desktop.md +382 -0
  46. package/library/knowledge/public/guides/codex.md +296 -0
  47. package/library/knowledge/public/guides/gemini-cli.md +105 -0
  48. package/library/knowledge/public/guides/model-compatibility.md +80 -0
  49. package/library/knowledge/public/guides/providers.md +90 -0
  50. package/library/knowledge/public/overview/README.md +7 -0
  51. package/library/knowledge/public/overview/what-is-rflectr.md +71 -0
  52. package/library/notes/README.md +21 -0
  53. package/library/requirements/README.md +51 -0
  54. package/library/requirements/backlog/README.md +30 -0
  55. package/library/requirements/completed/README.md +14 -0
  56. package/library/requirements/completed/prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md +205 -0
  57. package/library/requirements/completed/prd-001-cli-core-launch-orchestration/qa/.gitkeep +0 -0
  58. package/library/requirements/completed/prd-002-provider-registry/qa/.gitkeep +0 -0
  59. package/library/requirements/completed/prd-003-model-discovery-classification/qa/.gitkeep +0 -0
  60. package/library/requirements/completed/prd-004-translation-layer/qa/.gitkeep +0 -0
  61. package/library/requirements/completed/prd-005-local-proxy-catalog-routing/qa/.gitkeep +0 -0
  62. package/library/requirements/completed/prd-007-oauth-device-flows/qa/.gitkeep +0 -0
  63. package/library/requirements/completed/prd-011-claude-desktop-integration/qa/.gitkeep +0 -0
  64. package/library/requirements/in-work/README.md +19 -0
  65. package/library/requirements/reports/README.md +31 -0
  66. package/package.json +84 -0
  67. package/scripts/refresh-models-dev-cache.mjs +34 -0
  68. package/test-proxy.ts +19 -0
  69. package/test-split.js +1 -0
@@ -0,0 +1,95 @@
1
+ # OAuth Device Flows
2
+
3
+ > Category: Auth | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ How `rflectr` signs into providers that use OAuth instead of an API key — OpenAI (ChatGPT), xAI (Grok), and GitHub Copilot — and how those tokens are stored and refreshed. Read [`../data/provider-registry.md`](../data/provider-registry.md) for how an OAuth provider is registered.
6
+
7
+ **Related:**
8
+ - [`../security/credential-storage.md`](../security/credential-storage.md)
9
+ - [`../data/provider-registry.md`](../data/provider-registry.md)
10
+ - [`../ai/translation-layer.md`](../ai/translation-layer.md)
11
+ - Source: `src/oauth/` (`types.ts`, `openai.ts`, `xai.ts`, `github.ts`, `refresh.ts`, `pkce.ts`)
12
+
13
+ ---
14
+
15
+ ## Why device flow
16
+
17
+ These hosts run in a terminal with no browser redirect URI to catch. The OAuth 2.0 **Device Authorization Grant** (RFC 8628) fits: `rflectr` asks the provider for a user code, prints a URL for the user to visit, then polls until the user approves. No localhost callback server, no redirect handling.
18
+
19
+ `supportsNativeOAuth(providerId)` (`src/oauth/types.ts`) gates which providers offer this. The native set is `'xai' | 'xai-oauth' | 'openai' | 'openai-oauth' | 'github-copilot'`. The `rflectr providers auth <id>` command (`--native` vs `--broker`) drives it.
20
+
21
+ ```mermaid
22
+ sequenceDiagram
23
+ participant U as User
24
+ participant R as rflectr
25
+ participant P as Provider auth server
26
+ R->>P: request device/user code
27
+ P-->>R: user_code + verification URL + interval
28
+ R->>U: "visit <url>, enter <user_code>"
29
+ loop until approved (respect interval / slow_down)
30
+ R->>P: poll token endpoint
31
+ P-->>R: authorization_pending | slow_down | tokens
32
+ end
33
+ P-->>R: access + refresh tokens
34
+ R->>R: store StoredOAuthCredential in OS keyring
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Stored credential shape
40
+
41
+ `StoredOAuthCredential` (`src/oauth/types.ts`) is `{ type: 'oauth', access, refresh, expires, accountId? }`. It is serialized to JSON and written to the keyring account `oauth:provider:<id>` (see [`../security/credential-storage.md`](../security/credential-storage.md)). Helpers:
42
+
43
+ - `tokensToStoredCredential(tokens, existingRefresh?, accountId?)` — build it from a token response, preserving the existing refresh token if the provider didn't return a new one.
44
+ - `parseStoredOAuthCredential(raw)` — parse + validate from keyring.
45
+ - `oauthCredentialNeedsRefresh(cred, skewMs?)` — true when `expires <= now + skew` (default 120 s).
46
+ - `accessTokenIsExpiring(token, skewMs?)` — decode the JWT `exp` and check proactively.
47
+
48
+ ---
49
+
50
+ ## The three providers
51
+
52
+ ### OpenAI / ChatGPT (`src/oauth/openai.ts`)
53
+
54
+ - Client id `app_EMoaamEEZ73f0CkXaXp7hrann`, issuer `https://auth.openai.com`.
55
+ - `runOpenAiDeviceCodeFlow(onDeviceCode, opts?)`: POST `/api/accounts/deviceauth/usercode` → poll `/api/accounts/deviceauth/token` → exchange the authorization code (with PKCE `code_verifier`) at `/oauth/token`.
56
+ - `extractOpenAiAccountId(tokens)` decodes the `id_token` / access token JWT for `chatgpt_account_id`. That `accountId` is what `provider-factory.ts` uses to route to the ChatGPT Codex backend (`https://chatgpt.com/backend-api/codex`).
57
+ - `refreshOpenAiAccessToken(refreshToken)` — `grant_type=refresh_token` at `/oauth/token`.
58
+
59
+ ### xAI / Grok (`src/oauth/xai.ts`)
60
+
61
+ - Client id `b1a00492-073a-47ea-816f-4c329264a828`; device endpoint `https://auth.x.ai/oauth2/device/code`, token `https://auth.x.ai/oauth2/token`.
62
+ - Scope `openid profile email offline_access grok-cli:access api:access`.
63
+ - `requestXaiDeviceCode()` → `pollXaiDeviceCodeToken(device, opts?)` (grant `urn:ietf:params:oauth:grant-type:device_code`), combined as `runXaiDeviceCodeFlow`. Handles `slow_down` by bumping the interval +5 s.
64
+ - `refreshXaiAccessToken(refreshToken)`.
65
+
66
+ ### GitHub Copilot (`src/oauth/github.ts`)
67
+
68
+ A two-step exchange — this is the quirk to remember:
69
+
70
+ 1. Standard GitHub device flow (client id `Iv1.b507a08c87ecfe98`, the VS Code Copilot extension id; scope `copilot`) yields a long-lived `ghu_` access token.
71
+ 2. `exchangeForCopilotToken(ghuToken)` GETs `https://api.github.com/copilot_internal/v2/token` to mint a **short-lived Copilot session token**.
72
+
73
+ `refreshGithubCopilotToken(ghuToken)` simply re-exchanges the `ghu_` for a fresh session token — so the **stored `refresh` field is the `ghu_` itself**, not a conventional refresh token.
74
+
75
+ > Caveat: Copilot OAuth *login* works, but Copilot as a *model provider* does not — OpenCode loads `@ai-sdk/github-copilot` from internal `@opencode-ai/core`, not a public npm factory. See [`../data/provider-registry.md`](../data/provider-registry.md).
76
+
77
+ ---
78
+
79
+ ## Refresh orchestration
80
+
81
+ `src/oauth/refresh.ts` is the single dispatch point, called lazily at credential-resolution time (`resolveProviderCredential` in `src/env.ts`):
82
+
83
+ - `oauthCredentialShouldRefresh(cred, providerId)` — true if `oauthCredentialNeedsRefresh(cred)` (time-based) **or**, for native providers, `accessTokenIsExpiring(cred.access)` (proactive JWT check).
84
+ - `refreshStoredOAuthCredential(providerId, cred)` — routes to `refreshOpenAiAccessToken` / `refreshXaiAccessToken` / `refreshGithubCopilotToken` and returns a new `StoredOAuthCredential` via `tokensToStoredCredential`.
85
+
86
+ Refresh is deduplicated per keyring account (`refreshOAuthKeyringAccount` in `src/env.ts`) so concurrent launches don't double-refresh, and a refreshed token is written back to the keyring. If refresh fails but the existing access token hasn't yet expired, the stale-but-valid token is used.
87
+
88
+ ---
89
+
90
+ ## PKCE & utilities (`src/oauth/pkce.ts`)
91
+
92
+ - `generatePkce()` → `{ verifier, challenge }` with `challenge = SHA256(verifier)`.
93
+ - `generateOAuthState()` → base64url of 32 random bytes.
94
+ - `generateRandomString(length)` — crypto-random from the unreserved-char set.
95
+ - `positiveSecondsToMs(value, defaultMs)`, `sleepMs(ms)` — polling helpers.
@@ -0,0 +1,8 @@
1
+ # Data
2
+
3
+ On-disk state: the provider registry and user preferences.
4
+
5
+ | Doc | Covers |
6
+ |---|---|
7
+ | [`provider-registry.md`](provider-registry.md) | `~/.rflectr/providers.json` schema, built-in templates, registry → runtime materialization, the `providers` command. |
8
+ | [`preferences-config.md`](preferences-config.md) | `~/.rflectr/config.json`, app-home resolution (`RFLECTR_HOME`), legacy-path migration, large-catalog UX constants. |
@@ -0,0 +1,87 @@
1
+ # Preferences & Config Store
2
+
3
+ > Category: Data | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ Where `rflectr` keeps user preferences, how the app-home directory is resolved, and the legacy-path migration. Read [`provider-registry.md`](provider-registry.md) for the separate providers file.
6
+
7
+ **Related:**
8
+ - [`provider-registry.md`](provider-registry.md)
9
+ - [`../security/credential-storage.md`](../security/credential-storage.md)
10
+ - Source: `src/paths.ts`, `src/config.ts`, `src/types.ts`
11
+
12
+ ---
13
+
14
+ ## The app home
15
+
16
+ Everything `rflectr` writes lives under one directory, resolved by `getAppHome()` (`src/paths.ts`):
17
+
18
+ 1. `RFLECTR_HOME` (or deprecated `OPENCODE_STARTER_HOME`) if set — overrides everything.
19
+ 2. otherwise `~/.rflectr`.
20
+
21
+ Files within it:
22
+
23
+ | Path helper | File | Contents |
24
+ |---|---|---|
25
+ | `getConfigPath()` | `~/.rflectr/config.json` | User preferences (this doc) |
26
+ | `getProvidersPath()` | `~/.rflectr/providers.json` | Provider registry ([`provider-registry.md`](provider-registry.md)) |
27
+ | `getLogsPath()` | `~/.rflectr/logs/` | `--trace` debug logs |
28
+ | `getVertexModelsPath()` | `~/.rflectr/vertex-models.json` | Optional Vertex model catalog |
29
+
30
+ The directory is created with mode `0o700` and files with `0o600`. All writes are skipped when `dryRun === true`.
31
+
32
+ ---
33
+
34
+ ## Preferences schema
35
+
36
+ `UserPreferences` (`src/types.ts`), read/written by `loadPreferences()` / `savePreferences()` (`src/config.ts`):
37
+
38
+ | Field | Purpose |
39
+ |---|---|
40
+ | `lastBackend` | Last cloud backend (`zen` / `go`) |
41
+ | `lastModel`, `lastProvider` | Last Claude Code selection (pre-selected in the wizard) |
42
+ | `lastCodexProvider`, `lastCodexModel` | Last Codex selection |
43
+ | `lastGeminiProvider`, `lastGeminiModel` | Last Gemini selection |
44
+ | `recentModelsByProvider` | `Record<string, string[]>` — up to 3 recent model ids per provider |
45
+ | `favoriteModels` | `FavoriteModel[]` (`{ providerId, modelId }`), max `MAX_MODEL_CATALOG` (20) |
46
+ | `server` | Saved `server`-command settings |
47
+
48
+ `savePreferences()` does a shallow merge — pass only the keys you're changing. `recordLaunchSelection(agent, providerId, modelId, prefs)` updates the per-agent `last*` fields and prepends to `recentModelsByProvider` (deduped, capped at 3).
49
+
50
+ ```mermaid
51
+ flowchart TD
52
+ launch["launch a model"] --> rec["recordLaunchSelection()"]
53
+ rec --> last["set last{Provider,Model} for the agent"]
54
+ rec --> recent["prepend to recentModelsByProvider[provider] (max 3, dedup)"]
55
+ last --> save["savePreferences() — skipped on --dry-run"]
56
+ recent --> save
57
+ ```
58
+
59
+ `recentModelsByProvider` powers the "recent" hint at the top of `pickLocalModel` (`src/prompts.ts`), with a "Browse all models →" escape hatch.
60
+
61
+ ---
62
+
63
+ ## Legacy migration
64
+
65
+ `rflectr` was previously `opencode-starter`, and its config moved twice. `readConfig()` (`src/config.ts`) migrates on first read, transparently:
66
+
67
+ ```mermaid
68
+ flowchart TD
69
+ read["readConfig()"] --> homeMig["ensureAppHomeMigrated(): copy ~/.opencode-starter/config.json → ~/.rflectr/config.json"]
70
+ homeMig --> confMig["ensureConfigMigrated(): copy OS-conf-store legacy config → ~/.rflectr/config.json"]
71
+ confMig --> load["readJsonFile(getConfigPath()) ?? {}"]
72
+ ```
73
+
74
+ - `getLegacyAppHome()` → `~/.opencode-starter` (the prior dotfile dir). `vertex-models.json` migrates alongside `config.json`.
75
+ - `getLegacyConfPath()` → the even older OS-specific `conf` location (macOS `~/Library/Preferences/opencode-starter-nodejs/`, Windows `%APPDATA%\opencode-starter-nodejs\Config\`, Linux `$XDG_CONFIG_HOME/opencode-starter-nodejs/`). After copying, the legacy file is renamed `…​.migrated` (best-effort).
76
+ - `loadPreferences()` also normalizes the old `lastProvider === 'opencode'` value to `'zen'`.
77
+
78
+ Migration is one-directional and idempotent: if `~/.rflectr/config.json` already exists, nothing is copied.
79
+
80
+ ---
81
+
82
+ ## Large-catalog UX constants
83
+
84
+ Two constants in `src/prompts.ts` shape the picker once a provider's model list grows:
85
+
86
+ - `MODEL_SEARCH_THRESHOLD = 25` — lists above this show search or paginated browse instead of a flat list.
87
+ - `MODEL_PAGE_SIZE = 15` — page size for prev/next browsing (`selectModelWithSearch`, `selectLargeCatalog`, `pickModelFromPagedList`).
@@ -0,0 +1,126 @@
1
+ # The Provider Registry
2
+
3
+ > Category: Data | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ The on-disk catalog of providers and their models — the single source of truth for every wizard. This doc covers the `~/.rflectr/providers.json` schema, the built-in templates, and how a registry entry becomes a runtime provider. Read [`../architecture/system-overview.md`](../architecture/system-overview.md) first.
6
+
7
+ **Related:**
8
+ - [`preferences-config.md`](preferences-config.md)
9
+ - [`../ai/model-discovery-classification.md`](../ai/model-discovery-classification.md)
10
+ - [`../security/credential-storage.md`](../security/credential-storage.md)
11
+ - [`../auth/oauth-device-flows.md`](../auth/oauth-device-flows.md)
12
+ - Source: `src/registry/` (`types.ts`, `io.ts`, `load.ts`, `materialize.ts`, `crud.ts`, `add-template.ts`, `import-opencode.ts`), `src/provider-templates.ts`, `src/providers-command.ts`, `src/provider-catalog.ts`, `src/paths.ts`
13
+
14
+ ---
15
+
16
+ ## Why a registry
17
+
18
+ Early versions discovered providers by spawning OpenCode's `opencode serve` and reading `/config/providers` on every launch. That is slow and couples `rflectr` to a running OpenCode. The registry replaces that: providers are imported or added **once**, persisted to `~/.rflectr/providers.json`, and read directly on every launch. OpenCode import is now a one-time operation (`rflectr providers import`), not a per-launch dependency. OpenCode Zen / Go remain always available even with an empty registry.
19
+
20
+ ---
21
+
22
+ ## On-disk schema
23
+
24
+ Path: `getProvidersPath()` → `~/.rflectr/providers.json` (`src/paths.ts`). Types in `src/registry/types.ts`:
25
+
26
+ ```ts
27
+ ProviderRegistry {
28
+ schemaVersion: number // currently 1
29
+ providers: RegistryProvider[]
30
+ importedAt?: string
31
+ pricingCacheAt?: string
32
+ }
33
+
34
+ RegistryProvider {
35
+ id: string // /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (PROVIDER_ID_PATTERN)
36
+ templateId: string // origin template, e.g. 'groq', 'custom-lm-studio'
37
+ name: string // display name
38
+ enabled: boolean
39
+ authRef: string // 'keyring:provider:groq' | 'keyring:global:opencode' | 'env:OPENCODE_API_KEY'
40
+ authType?: 'api' | 'oauth' | 'none'
41
+ subscriptionFilter?: 'free' | 'zen' | 'go'
42
+ api: { npm?: string; url?: string; id?: string }
43
+ modelsCache?: { fetchedAt: string; models: CachedModel[] }
44
+ addedAt: string // ISO
45
+ refreshedAt?: string
46
+ }
47
+
48
+ CachedModel {
49
+ id: string
50
+ name: string
51
+ upstreamModelId: string // provider's native id for the wire call
52
+ family?: string; brand?: string
53
+ contextWindow?: number
54
+ cost?: { input: number; output: number }
55
+ modelFormat: 'anthropic' | 'openai'
56
+ npm?: string // per-model override of provider.api.npm
57
+ apiUrl?: string // per-model override of provider.api.url
58
+ sourceBackend?: string
59
+ supportedParameters?: string[]
60
+ reasoning?: boolean
61
+ interleavedReasoningField?: string
62
+ }
63
+ ```
64
+
65
+ **Persistence** (`src/registry/io.ts`): `loadRegistry(path?)` returns an empty registry if the file is missing/unparseable and applies migrations; `saveRegistry()` writes atomically (tmp + rename, backup) with secure permissions (`0o600` file inside a `0o700` dir via `ensureSecureAppHome()`), 2-space JSON + trailing newline. **No secrets live in this file** — `authRef` is only a pointer; the actual key lives in the OS keyring (see [`../security/credential-storage.md`](../security/credential-storage.md)).
66
+
67
+ ---
68
+
69
+ ## From registry entry to runtime provider
70
+
71
+ ```mermaid
72
+ flowchart TD
73
+ file["~/.rflectr/providers.json"] --> load["loadRegistry()"]
74
+ load --> lp["loadRegistryProviders() (async)"]
75
+ lp --> cred["resolveProviderCredential(id, authRef) — env → keyring → OAuth refresh"]
76
+ cred --> mat["materializeRegistry(registry, credResolver)"]
77
+ mat --> filter["skip disabled / no-models / no-credential; shouldHideModel() per agent"]
78
+ filter --> runtime["LocalProvider[] (the wizard list)"]
79
+ ```
80
+
81
+ - `loadRegistryProviders(diag?, opts?)` (`src/registry/load.ts`) reads enabled entries and resolves each credential.
82
+ - `materializeRegistry(registry, credResolver, opts?)` (`src/registry/materialize.ts`) converts `CachedModel` → `LocalProviderModel`, drops providers with no credential or no cached models, and applies per-agent model hiding (`shouldHideModel()`) — e.g. Zen/Go favorites hidden from Codex.
83
+ - `providersForPicker()` / `resolveLocalProviderApiKey()` / `formatRegistryAuthLabel()` (`src/provider-catalog.ts`) adapt the materialized providers for the interactive pickers and resolve the launch key.
84
+
85
+ ---
86
+
87
+ ## Built-in templates
88
+
89
+ `src/provider-templates.ts` defines `ProviderTemplate`s — the menu of providers a user can add without hand-entering a base URL. Each carries `id`, `name`, `authType`, `npm` (the SDK package), `defaultBaseUrl?`, `signupUrl?`, `urlPrompt?` (for local servers), `modelSource` (`api-list | static-seed | manual-only | zen-go-api`), and `supported` / `addable` / `unsupportedReason` flags.
90
+
91
+ | Group | Templates |
92
+ |---|---|
93
+ | API-key, addable | groq, mistral, togetherai, cerebras, deepinfra, deepseek, zhipu, moonshot(-global), kimi-code, xai, perplexity, cohere, openai, google, alibaba, openrouter, venice, anthropic |
94
+ | Local servers | ollama, lmstudio (`urlPrompt`, `apiKeyOptional`) |
95
+ | OAuth | github-copilot, xai |
96
+ | Reference only (not addable) | bedrock (AWS creds), azure (deployment URLs), vertex (gcloud ADC) — carry `unsupportedReason` |
97
+ | Cloud stubs | `zen`, `go` (`addable: false`); `opencode-cloud` routes to them. Models fetched live. |
98
+
99
+ Query helpers: `listSupportedTemplates()`, `listAddableTemplates(configuredIds)`, `getTemplateById(id)`, `filterTemplates(templates, query)`.
100
+
101
+ OpenCode is the source of truth for *which* models a provider exposes — `rflectr` does not maintain a per-package allowlist beyond these templates.
102
+
103
+ ---
104
+
105
+ ## The `providers` command
106
+
107
+ `src/providers-command.ts` (`parseProvidersArgs` → `runProvidersCommand`):
108
+
109
+ | Command | Function | Purpose |
110
+ |---|---|---|
111
+ | `rflectr providers` | `runProvidersHub()` | Interactive hub |
112
+ | `… add` | `runProvidersAdd()` | Add from template or custom endpoint → `addProviderFromTemplate` |
113
+ | `… import` | `runProvidersImport()` | One-time import from OpenCode → `importFromOpencode` |
114
+ | `… list` | `runProvidersList()` | Tabular view |
115
+ | `… remove <id>` | `runProvidersRemove(id)` | Remove entry + keyring cleanup → `removeProviderFromRegistry` |
116
+ | `… refresh-models [id]` | `runProvidersRefreshModels(id?)` | Re-fetch the model cache |
117
+ | `… auth <id> [--native\|--broker]` | `runProvidersAuth(id, method?)` | OAuth sign-in (see [`../auth/oauth-device-flows.md`](../auth/oauth-device-flows.md)) |
118
+
119
+ CRUD lives in `src/registry/crud.ts` (`removeProviderFromRegistry`, `toggleProviderEnabled`), template-add in `src/registry/add-template.ts`, and OpenCode import in `src/registry/import-opencode.ts` (handles duplicate-provider migration).
120
+
121
+ ---
122
+
123
+ ## Unsupported-by-design providers
124
+
125
+ - `@ai-sdk/github-copilot` won't work as a *model* provider — OpenCode loads it from internal `@opencode-ai/core`, not a public npm factory we can ship. (OAuth login still works; it's the model factory that's unavailable.)
126
+ - Bedrock / Azure / Vertex may need env-based auth beyond a simple forwarded `apiKey`; they're reference-only templates. Vertex *is* supported through the dedicated `server --vertex` path (gcloud ADC) — see [`../infrastructure/server-gateway.md`](../infrastructure/server-gateway.md).
@@ -0,0 +1,7 @@
1
+ # Infrastructure
2
+
3
+ Long-lived services and runtime gateways.
4
+
5
+ | Doc | Covers |
6
+ |---|---|
7
+ | [`server-gateway.md`](server-gateway.md) | `rflectr server` — the Anthropic/OpenAI-compatible gateway on port 17645, the model catalog + discovery-id masking, and Vertex AI mode (`--vertex`). |
@@ -0,0 +1,87 @@
1
+ # The Server Gateway
2
+
3
+ > Category: Infrastructure | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ `rflectr server` runs a long-lived HTTP gateway that exposes registry/Zen/Go providers (or Claude on Vertex AI) behind Anthropic- and OpenAI-compatible endpoints. It's the backend for Claude Desktop and any tool that can point at a base URL. Read [`../ai/translation-layer.md`](../ai/translation-layer.md) for the translation it reuses.
6
+
7
+ **Related:**
8
+ - [`../integrations/harnesses.md`](../integrations/harnesses.md)
9
+ - [`../ai/translation-layer.md`](../ai/translation-layer.md)
10
+ - [`../security/credential-storage.md`](../security/credential-storage.md)
11
+ - Source: `src/server/` (`index.ts`, `router.ts`, `models.ts`, `vendor-mask.ts`, `vertex-config.ts`, `auth.ts`, `catalog-filter.ts`, `provider-select.ts`, `prompts.ts`)
12
+
13
+ ---
14
+
15
+ ## What it is
16
+
17
+ Where the launch commands are short-lived (start a proxy, run a child, tear down), `server` is a **foreground daemon**. `runServerCommand(options)` (`src/server/index.ts`) runs an interactive wizard (which providers to expose, optional favorites-only catalog, discovery-id masking, local vs network bind) and then `startServer()` (`src/server/router.ts`) listens on port **17645**.
18
+
19
+ It serves the same translation core as the CLI proxies — `handleAnthropicMessages` and `handleOpenAIChatCompletions` both end at `createLanguageModel` + the SDK adapter for openai-format models, or a raw forward for anthropic-format models, with a per-`(model × npm × baseURL)` `LanguageModel` cache.
20
+
21
+ ```mermaid
22
+ flowchart TD
23
+ client["Claude Desktop / any tool"] --> ep{"endpoint"}
24
+ ep -->|"POST /anthropic/v1/messages"| anth["handleAnthropicMessages"]
25
+ ep -->|"POST /openai/v1/chat/completions"| oai["handleOpenAIChatCompletions"]
26
+ ep -->|"GET /models, /anthropic/v1/models, /openai/v1/models"| list["catalog (apiKey stripped)"]
27
+ ep -->|"GET /health"| health["{ ok: true }"]
28
+ anth --> fmt{"modelFormat"}
29
+ oai --> fmt
30
+ fmt -->|anthropic| raw["raw forward {baseUrl}/v1/messages"]
31
+ fmt -->|openai| sdk["SDK adapter"]
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Endpoints
37
+
38
+ | Method + path | Purpose |
39
+ |---|---|
40
+ | `GET /health` | `{ ok: true }` |
41
+ | `GET /models` | Raw catalog, `apiKey` stripped |
42
+ | `GET /anthropic/v1/models` | Anthropic-format list (optionally masked) |
43
+ | `GET /openai/v1/models` | OpenAI-format list |
44
+ | `POST /anthropic/v1/messages` | Anthropic Messages relay |
45
+ | `POST /openai/v1/chat/completions` | OpenAI Chat Completions relay |
46
+
47
+ Base URLs for clients:
48
+
49
+ - Anthropic: `ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic`
50
+ - OpenAI: `OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1`
51
+
52
+ ---
53
+
54
+ ## Model catalog & gateway aliases
55
+
56
+ `loadServerModels()` (`src/server/index.ts`) assembles `ServerModelInfo[]` from Zen/Go models plus every materialized local provider, enriching each with reasoning metadata (`enrichServerModelReasoning` adds `defaultEffort`). `createGatewayModelCatalog(models, opts?)` (`src/server/models.ts`) builds a bidirectional lookup keyed by `model.id` and by gateway alias.
57
+
58
+ Helpers in `src/server/models.ts`:
59
+
60
+ - `gatewayAliasId(model)` / `exposedGatewayAliasId(model, opts?)` — canonical alias `anthropic-{provider}__{model}` (via `aliasModelId`), masked or not.
61
+ - `gatewayDisplayName(model, opts?)` — "Model Name" or "Model Name (Provider Label)" when masked.
62
+ - `upstreamModelId(model)` — strips the `[1m]` context suffix for the wire call.
63
+ - `formatGatewayAnthropicModels` / `formatOpenAIModels` — endpoint payloads.
64
+
65
+ Filtering (`src/server/catalog-filter.ts`): `filterServerModelsByProviders`, `filterServerModelsByFavorites`, `summarizeServerProviders` (human summary like "OpenCode Zen (5), Groq (3)").
66
+
67
+ ### Discovery-id masking
68
+
69
+ For Claude Desktop / Cowork, exposing raw vendor model ids may be undesirable. When the wizard enables masking (`gateway.maskGatewayIds = true`), `maskGatewayModelId(aliasId)` (`src/server/vendor-mask.ts`) reverses the provider-slug and model-suffix segments to hide vendor names; it is **self-inverse** (`unmaskGatewayModelId` is the same operation). The response `model` field is also set to the masked `gatewayDisplayName`. Example: `anthropic-openai__gpt-4o` → a reversed-segment alias.
70
+
71
+ ---
72
+
73
+ ## Vertex AI mode (`--vertex`)
74
+
75
+ `rflectr server --vertex` exposes **Claude on Google Vertex AI** using local gcloud Application Default Credentials — no OpenCode API key. `src/server/vertex-config.ts`:
76
+
77
+ - `buildVertexRuntimeConfig(env?)` reads the project (`ANTHROPIC_VERTEX_PROJECT_ID` → `GOOGLE_CLOUD_PROJECT` → `GOOGLE_VERTEX_PROJECT`) and location (`GOOGLE_CLOUD_LOCATION` → `CLOUD_ML_REGION` → `GOOGLE_VERTEX_LOCATION` → `global`).
78
+ - `hasApplicationDefaultCredentials()` checks `GOOGLE_APPLICATION_CREDENTIALS` or `~/.config/gcloud/application_default_credentials.json`.
79
+ - `vertexModelsToServerModels(config)` / `createVertexModelCatalog(models)` build the catalog with short aliases (`sonnet` / `haiku` / `opus`) and `[1m]` context variants. Defaults: `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-haiku-4-5`. An optional catalog override lives at `~/.rflectr/vertex-models.json` (see `assets/vertex-models.example.json`).
80
+
81
+ The models route through `@ai-sdk/google-vertex/anthropic` (`VERTEX_ANTHROPIC_NPM`) — see [`../ai/translation-layer.md`](../ai/translation-layer.md).
82
+
83
+ ---
84
+
85
+ ## Auth
86
+
87
+ `isAuthorized(request, serverPassword)` (`src/server/auth.ts`) accepts a `Bearer` token or `x-api-key` header and compares it to the configured server password. A `null` password (local mode) allows all callers. `extractBearerToken` and `sanitizeCredential` (first-line only) harden header parsing. In **network mode** the wizard requires a server password; it is the only gate once the port is reachable beyond localhost, so treat it as a real secret. See [`../security/credential-storage.md`](../security/credential-storage.md#server-mode-caveat).
@@ -0,0 +1,8 @@
1
+ # Integrations
2
+
3
+ The local proxy and the per-host harness wiring.
4
+
5
+ | Doc | Covers |
6
+ |---|---|
7
+ | [`local-proxy.md`](local-proxy.md) | The throwaway `127.0.0.1` proxy, `ProxyRoute` dispatch, `aliasModelId`, shared translation helpers. |
8
+ | [`harnesses.md`](harnesses.md) | Codex CLI/desktop, Gemini CLI, Claude Desktop — how each host is pointed at a translating proxy; macOS/Windows differences. |
@@ -0,0 +1,87 @@
1
+ # Host Harnesses: Codex, Gemini, and the Desktop Apps
2
+
3
+ > Category: Integrations | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ How `rflectr` wires each non-Claude-Code host to a translating proxy. The Claude Code flow is in [`../architecture/launch-flow-claude.md`](../architecture/launch-flow-claude.md); this doc covers Codex CLI, Codex desktop, Gemini CLI, and Claude Desktop. Read [`local-proxy.md`](local-proxy.md) first.
6
+
7
+ **Related:**
8
+ - [`local-proxy.md`](local-proxy.md)
9
+ - [`../infrastructure/server-gateway.md`](../infrastructure/server-gateway.md)
10
+ - [`../ai/translation-layer.md`](../ai/translation-layer.md)
11
+ - Source: `src/codex.ts`, `src/codex/`, `src/codex-app.ts`, `src/codex-proxy.ts`, `src/codex-responses-adapter.ts`, `src/gemini.ts`, `src/gemini/`, `src/gemini-proxy.ts`, `src/claude-app.ts`, `src/claude-desktop/`
12
+
13
+ ---
14
+
15
+ ## The pattern, and where each host departs from it
16
+
17
+ Every host follows the same skeleton: find the binary → resolve provider+model → start a proxy that speaks the host's wire format → launch the child with env (CLIs) or a patched config (apps) pointing at the proxy → restore/cleanup on exit. The differences are in *how the host is pointed at the proxy* and *whether config files must be written and restored*.
18
+
19
+ ```mermaid
20
+ flowchart TD
21
+ cli["CLI hosts (codex, gemini)"] -->|env var| proxy["translating proxy"]
22
+ app["Desktop apps (codex-app, claude-app)"] -->|patched config file + backup| proxy
23
+ proxy --> adapter["Vercel AI SDK adapter"]
24
+ app -.->|lock file| restore["restore original config on exit"]
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Codex CLI — `rflectr codex`
30
+
31
+ Entry: `runCodexCommand` (`src/codex.ts`). Codex speaks the **OpenAI Responses API**.
32
+
33
+ - **Binary:** `findCodexBinary()` (`src/codex/launch.ts`).
34
+ - **Proxy:** `startCodexProxy(routes, { requireAuth: true })` (`src/codex-proxy.ts`) serves `POST /v1/responses`, translating through `src/codex-responses-adapter.ts` (`translateResponsesRequest` → SDK params; `streamResponsesResponse` / `generateResponsesResponse` → Responses SSE/JSON).
35
+ - **Profile:** a TOML overlay (`buildCodexProfileToml`, `src/codex/profile.ts`) written to `~/.codex/rflectr-launch.config.toml`. It defines a `model_providers.rflectr-proxy` block whose `base_url = http://127.0.0.1:<port>/v1`, `wire_api = "responses"`, and `env_key = "RFLECTR_CODEX_KEY"`. Codex is launched with `--profile rflectr-launch -m <modelId>`.
36
+ - **Catalog:** `~/.rflectr/codex/models-<providerId>.json` (single) or `models-favorites.json` (favorites mode), via `src/codex/catalog.ts`.
37
+ - **Env:** `RFLECTR_CODEX_KEY=proxy-local` for proxy-tier routes; direct providers get their real key via `codexProviderEnvKey()` (`src/codex/routing.ts`).
38
+ - **Sandbox:** default `danger-full-access` (`CODEX_LAUNCH_SANDBOX`, `src/codex/profile.ts`).
39
+
40
+ **Favorites catalog mode:** when `prefs.favoriteModels.length > 0`, Codex resolves each favorite via the shared `src/favorites-resolver.ts` (filtering by an `agent: 'codex'` blacklist — Zen/Go favorites are skipped, since Codex has no gateway path for them), builds a `CodexProxyRoute[]`, and starts a single multi-route proxy. Catalog slugs are `${providerId}__${modelId}`.
41
+
42
+ ---
43
+
44
+ ## Codex desktop app — `rflectr codex-app`
45
+
46
+ Entry: `runCodexAppCommand` (`src/codex-app.ts`). The app can't inherit env, so its config is patched in place and restored on exit.
47
+
48
+ - **Config patch:** `applyAppConfigPatch` (`src/codex/app-config.ts`) edits `~/.codex/config.toml`, writing `model`, `model_provider = 'openai'`, `openai_base_url`, `model_catalog_json`, and `model_context_window` (`buildCodexAppRootConfig`, `src/codex/app-profile.ts`). Display model defaults to `CODEX_APP_DISPLAY_MODEL = 'gpt-5.5'`.
49
+ - **Backup + lock:** the original `config.toml` is copied to `config.toml.bak`; a lock file `~/.codex/.rflectr.lock.json` records `pid`, `configPath`, `catalogPaths`, `backupPath`, and `proxyPort`. `restoreCodexAppOverlay` (`src/codex/app-session.ts`) restores the backup on exit or recovery.
50
+ - **Catalog:** `app-models-<providerId>.json` / `app-models-favorites.json`.
51
+ - **Proxy:** same `startCodexProxy`, but `requireAuth: false` (the app cannot send the proxy token).
52
+ - **`--restore`** globs `app-models-*.json` to clean up prior sessions.
53
+
54
+ ---
55
+
56
+ ## Gemini CLI — `rflectr gemini`
57
+
58
+ Entry: `runGeminiCommand` (`src/gemini.ts`). Gemini speaks the **Gemini REST protocol**, and every model is routed through the proxy.
59
+
60
+ - **Binary:** `findGeminiBinary()` (`src/gemini/launch.ts`).
61
+ - **Proxy:** `startGeminiProxy(routes, debug?)` (`src/gemini-proxy.ts`) serves `GET /v1beta/models`, `GET /v1beta/models/<model>`, and `POST /v1beta/models/<model>:generateContent` / `:streamGenerateContent`. `translateGeminiRequest` extracts the system instruction, contents, tools, and generation config, and strips the Gemini-CLI-injected identity. Parts are parsed by `src/gemini-parts.ts`.
62
+ - **Env:** `buildGeminiChildEnv` (`src/gemini/launch.ts`) sets `GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:<port>` and `GEMINI_API_KEY=<random proxy token>`, and clears conflicting `GOOGLE_GENAI_API_KEY` / `GOOGLE_API_KEY`.
63
+ - **Mid-session switch:** the proxy intercepts a `.model <id>` command to switch routes without restarting.
64
+
65
+ ---
66
+
67
+ ## Claude Desktop — `rflectr claude-app`
68
+
69
+ Entry: `runClaudeAppCommand` (`src/claude-app.ts`). Claude Desktop has a first-class "inference gateway" config, so instead of a per-protocol proxy it is pointed at the full **`server` gateway** (see [`../infrastructure/server-gateway.md`](../infrastructure/server-gateway.md)).
70
+
71
+ - **App discovery/launch:** `src/claude-desktop/app-launch.ts` finds `Claude.app` (macOS) or `Claude.exe` (Windows) and `launchOrRestartClaudeApp()`. macOS/Windows only (`claudeAppSupported()`).
72
+ - **Gateway injection:** a config `<uuid>.json` is written into the Claude Desktop **3P config library** — `~/Library/Application Support/Claude-3p/configLibrary/` (macOS) or `%LOCALAPPDATA%\Claude-3p\configLibrary\` (Windows) — with `inferenceProvider: 'gateway'`, `inferenceGatewayBaseUrl: http://127.0.0.1:<port>/anthropic`, `inferenceGatewayApiKey: 'dummy'`, `inferenceGatewayAuthScheme: 'bearer'`, and `coworkEgressAllowedHosts`. A `_meta.json` points `appliedId` at the new uuid (`src/claude-desktop/app-config.ts`).
73
+ - **Backup + lock:** `_meta.json.bak` is created before the patch; a `.rflectr.lock` records `pid`, `uuid`, `proxyPort`. On exit or recovery, `cleanupSession(uuid)` / `recoverSession()` (`src/claude-desktop/app-session.ts`) restore the backup and remove the injected config.
74
+ - **Gateway:** the in-process `startServer()` serves `/anthropic` with `createGatewayModelCatalog()`; favorites filter via `filterServerModelsByFavorites`, or a single selected model.
75
+
76
+ ---
77
+
78
+ ## Platform differences (apps)
79
+
80
+ | Concern | macOS | Windows |
81
+ |---|---|---|
82
+ | Claude Desktop config root | `~/Library/Application Support/Claude-3p/` | `%LOCALAPPDATA%\Claude-3p\` |
83
+ | App launch | `open` / `open -b <bundle id>` | direct `.exe` exec / registry lookup |
84
+ | "Is it running?" | `osascript` | PowerShell `Get-Process` |
85
+ | CLI binary discovery | `which` + fallbacks | `where` + `.cmd` priority + `%APPDATA%\npm\` |
86
+
87
+ All proxies bind `127.0.0.1:0` on both platforms (Node `http.createServer`).
@@ -0,0 +1,82 @@
1
+ # The Local Proxy
2
+
3
+ > Category: Integrations | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ The throwaway HTTP server that sits between a host tool and the upstream model. This doc covers `src/proxy.ts` (the Anthropic-facing proxy) and its routing model; the Codex and Gemini variants are summarized in [`harnesses.md`](harnesses.md). Read [`../ai/translation-layer.md`](../ai/translation-layer.md) for what the proxy delegates to.
6
+
7
+ **Related:**
8
+ - [`harnesses.md`](harnesses.md)
9
+ - [`../ai/translation-layer.md`](../ai/translation-layer.md)
10
+ - [`../architecture/launch-flow-claude.md`](../architecture/launch-flow-claude.md)
11
+ - Source: `src/proxy.ts`, `src/proxy-shared.ts`, `src/proxy-types.ts`, `src/upstream-forward.ts`
12
+
13
+ ---
14
+
15
+ ## What it is
16
+
17
+ A local HTTP server on `127.0.0.1:<random ephemeral port>` (bind to port `0`, let the OS choose) that accepts Anthropic-format requests at `/v1/messages` and serves a synthetic `GET /v1/models`. The host tool is pointed at it via `ANTHROPIC_BASE_URL=http://127.0.0.1:<port>`. It is created at launch and torn down (`proxyHandle.close()`) when the host process exits.
18
+
19
+ Two entry points:
20
+
21
+ - `startProxy(completionsUrl, modelId, debug, contextWindow?, sdk?, apiKey?)` — single-model wrapper.
22
+ - `startProxyCatalog(routes, startingAliasId, debug)` — multi-route catalog for switch-menu sessions.
23
+
24
+ `startProxy` is just `startProxyCatalog` with one route. Both return a `ProxyHandle` (`{ port, token, close() }`); the `token` becomes the child's `ANTHROPIC_API_KEY`, so only the launched child can call the proxy.
25
+
26
+ ---
27
+
28
+ ## Per-request dispatch
29
+
30
+ ```mermaid
31
+ flowchart TD
32
+ req["POST /v1/messages (Anthropic format)"]
33
+ req --> lookup["resolve ProxyRoute by model id (alias or real)"]
34
+ lookup --> fmt{"route.modelFormat"}
35
+ fmt -->|anthropic| fwd["relayAnthropicMessages() → {baseUrl}/v1/messages (raw)"]
36
+ fmt -->|"openai (else)"| sdkguard{"isSdkMigratedNpm(route.npm)"}
37
+ sdkguard -->|true| adapter["createLanguageModel + sdk-adapter stream/generate"]
38
+ fwd --> resp["Anthropic SSE / JSON back to host"]
39
+ adapter --> resp
40
+ ```
41
+
42
+ Each `ProxyRoute` carries everything needed to serve it: `aliasId`, `realModelId`, `displayName`, `upstreamUrl` / `baseURL`, `apiKey`, `modelFormat`, `contextWindow`, `npm`, `providerId`, `authType`, `oauthAccountId`, `supportedParameters`, `reasoning`, `interleavedReasoningField`.
43
+
44
+ - `modelFormat === 'anthropic'` → direct passthrough via `relayAnthropicMessages` (`src/upstream-forward.ts`), which builds Anthropic auth headers (`anthropicUpstreamHeaders`) and relays both streaming and non-streaming.
45
+ - otherwise → the SDK adapter (`createLanguageModel` + `streamAnthropicResponse` / `generateAnthropicResponse`).
46
+
47
+ `src/upstream-forward.ts` is shared by both the proxy and the `server` command's router, so the raw-forward path is identical in both. `UpstreamUnreachableError` distinguishes a network failure from an upstream error response.
48
+
49
+ ---
50
+
51
+ ## aliasModelId — making ids gateway-safe
52
+
53
+ Claude Code's gateway model discovery rejects model ids that don't look like provider-prefixed gateway ids. `aliasModelId(realId, providerId)` (`src/proxy.ts`) rewrites any non-`claude-*` id to the form `anthropic-{provider}__{id}` (e.g. `anthropic-opencode-go__deepseek-v4-flash`). `claude-*` ids pass through unchanged. This is why, after a switch-menu session, a bare `claude` may show a relay alias in `/model` — Claude Code cached the gateway id (see [`../security/credential-storage.md`](../security/credential-storage.md)).
54
+
55
+ The synthetic `GET /v1/models` returns one entry per route, each formatted by `formatAnthropicModelEntry` with `context_window` so the host's status bar is accurate (single-model mode only; the gateway-discovery payload carries no window).
56
+
57
+ ---
58
+
59
+ ## Shared translation helpers
60
+
61
+ `src/proxy-shared.ts` holds the format-agnostic glue reused across the Anthropic, Responses, and Gemini proxies:
62
+
63
+ - `sseChunk()` — formats one SSE event.
64
+ - `encodeToolUseId()` / `splitToolUseId()` — round-trip a `thought_signature` through a tool-use id as `{id}::ts::{signature}` (see [`../ai/translation-layer.md`](../ai/translation-layer.md#the-thought_signature-round-trip)).
65
+ - `grabRoundTripSignature()` — pull the Google/OpenAI signature off a stream part.
66
+ - `parseToolArguments()` — tolerant JSON parse of tool-call args.
67
+ - `serializeToolResultContent()` — normalize tool-result content blocks.
68
+ - `silenceSdkWarnings()` — suppress noisy SDK debug logging.
69
+ - `FullStreamPart` — the unified stream-chunk type the adapters map onto.
70
+
71
+ `src/proxy-types.ts` defines the Anthropic and Gemini request/response shapes (`AnthropicMessageRequest`, `AnthropicContentBlock`, `GeminiPart`, `GeminiFunctionCall`, …).
72
+
73
+ ---
74
+
75
+ ## Variants
76
+
77
+ The Codex and Gemini hosts speak different wire formats, so they have sibling proxies that share `proxy-shared.ts` and `provider-factory.ts` but expose different endpoints:
78
+
79
+ - `src/codex-proxy.ts` — `startCodexProxy(routes, { requireAuth })`, endpoint `POST /v1/responses`, uses `src/codex-responses-adapter.ts`.
80
+ - `src/gemini-proxy.ts` — `startGeminiProxy(routes, debug?)`, endpoints `GET /v1beta/models` and `POST /v1beta/models/<model>:generateContent` / `:streamGenerateContent`.
81
+
82
+ See [`harnesses.md`](harnesses.md) for how each host is wired to its proxy.
@@ -0,0 +1,9 @@
1
+ # Security
2
+
3
+ Credential handling, environment isolation, and trust boundaries.
4
+
5
+ | Doc | Covers |
6
+ |---|---|
7
+ | [`credential-storage.md`](credential-storage.md) | Keyring accounts, credential resolution order, the `buildChildEnv` contract (set/removed vars), per-platform key setup, trust boundaries, server-mode caveat. |
8
+
9
+ For the OAuth sign-in flows that produce stored tokens, see [`../auth/oauth-device-flows.md`](../auth/oauth-device-flows.md).