steipete--codexbar
258 行
16 KiB
Markdown
258 行
16 KiB
Markdown
---
|
|
summary: "Accepted security and architecture boundary for declarative HTTP JSON providers."
|
|
read_when:
|
|
- Evaluating issue 1735
|
|
- Designing runtime-defined provider identities
|
|
- Reviewing configurable endpoint or response-mapping changes
|
|
---
|
|
|
|
# Declarative custom provider design
|
|
|
|
Status: accepted design boundary. This document defines a bounded MVP; it does not authorize runtime networking or
|
|
implement the feature.
|
|
|
|
Issue: [#1735](https://github.com/steipete/CodexBar/issues/1735)
|
|
|
|
## Decision summary
|
|
|
|
A declarative provider can reduce one-off integrations, but it is not a small extension of LLM Proxy or LiteLLM.
|
|
Those providers still have compile-time `UsageProvider` identities, descriptors, implementations, request shapes, and response
|
|
decoders. A custom provider adds two new trust boundaries:
|
|
|
|
1. a config file chooses where CodexBar sends a secret;
|
|
2. untrusted response data controls user-visible usage, cost, and identity fields.
|
|
|
|
Accepted direction: pursue a config-only, GET-only, HTTP JSON MVP after separating runtime provider instance identity
|
|
from the closed `UsageProvider` enum. Do not add a single `.custom` enum case: multiple configured providers would then
|
|
collide in caches, status items, history, widgets, and settings.
|
|
|
|
## Current constraints
|
|
|
|
- `ProviderConfig.id` decodes directly as `UsageProvider`.
|
|
- `ProviderDescriptorRegistry` bootstraps exactly one descriptor for every `UsageProvider.allCases` value.
|
|
- `ProviderImplementationRegistry` constructs implementations with an exhaustive `UsageProvider` switch.
|
|
- Usage, errors, status, history, icons, settings, and menu state are keyed by `UsageProvider` across the app.
|
|
- The settings sidebar now persists provider-pane selection as `provider:<UsageProvider.rawValue>` and still assumes one
|
|
pane per compile-time provider, reinforcing that dynamic identities need the shared seam rather than a parallel UI path.
|
|
- LLM Proxy and LiteLLM accept a configured base URL, but their request paths, auth header, decoding, and snapshot
|
|
mapping remain provider-specific Swift code.
|
|
- `ProviderEndpointOverrideValidator` already provides hardened HTTPS host parsing and an explicit loopback-HTTP mode,
|
|
while `ProviderHTTPClient` limits redirects to the same HTTPS origin. Reuse those primitives, but add a custom-provider
|
|
policy for fragments, auth-dependent loopback rules, redirect rejection, response limits, and secret-safe errors.
|
|
|
|
## Proposed MVP contract
|
|
|
|
### Configuration
|
|
|
|
Keep the existing provider array and introduce config version 2 with a tagged provider definition. Existing entries remain
|
|
first-party definitions; custom entries have a stable user-chosen instance ID and a fixed implementation kind.
|
|
|
|
```json
|
|
{
|
|
"version": 2,
|
|
"providers": [
|
|
{
|
|
"id": "acme-gateway",
|
|
"kind": "custom-http-json",
|
|
"label": "Acme Gateway",
|
|
"enabled": true,
|
|
"request": {
|
|
"method": "GET",
|
|
"url": "https://gateway.example.com/v1/quota",
|
|
"authentication": { "type": "bearer" }
|
|
},
|
|
"mapping": {
|
|
"primary": {
|
|
"usedPercent": { "path": "quota.used_pct" },
|
|
"resetsAt": { "path": "quota.reset_at", "dateFormat": "iso8601" },
|
|
"windowMinutes": { "path": "quota.window_minutes" }
|
|
},
|
|
"cost": {
|
|
"used": { "path": "spend.usd" },
|
|
"currency": "USD",
|
|
"period": "Approx. spend"
|
|
},
|
|
"identity": {
|
|
"organization": { "path": "plan.name" },
|
|
"loginMethod": { "literal": "api" }
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
|
|
- `id`: lowercase ASCII letters, digits, and hyphens; 1–64 characters; unique across first-party and custom providers.
|
|
- `label`: required, trimmed, 1–80 characters. MVP uses a built-in generic icon.
|
|
- `method`: only `GET`.
|
|
- `authentication`: `none`, `bearer`, or `x-api-key`; the secret is never inline. Authenticated instances read only their
|
|
derived variable `CODEXBAR_CUSTOM_<INSTANCE_ID>_API_KEY`, with the ID uppercased and hyphens replaced by underscores.
|
|
A definition cannot name an arbitrary environment variable or header; bearer uses `Authorization` and x-api-key uses
|
|
`X-API-Key`.
|
|
- `mapping.primary`: optional. When present, requires exactly one of `usedPercent` or `remainingPercent`. Optional fields
|
|
are omitted when their paths are missing or null.
|
|
- `mapping.cost`: when present, requires `used`; `currency` is a three-letter uppercase literal and `period` is a bounded
|
|
literal. A missing limit maps to zero, matching existing sparse cost snapshots.
|
|
- `mapping.identity`: optional bounded strings. The configured provider instance ID, not response data, owns snapshot
|
|
identity.
|
|
- A definition that produces neither a rate window nor cost data is invalid.
|
|
|
|
### Mapping language
|
|
|
|
Use a typed dot-path subset, not JSONPath, jq, JavaScript, predicates, or string interpolation.
|
|
|
|
Grammar:
|
|
|
|
```text
|
|
path = segment *("." segment / "[" index "]")
|
|
segment = ALPHA *(ALPHA / DIGIT / "_" / "-")
|
|
index = 1*DIGIT
|
|
```
|
|
|
|
Each target field determines its accepted type. Number coercion accepts JSON numbers and finite numeric strings only.
|
|
Percentages are clamped to 0–100 after rejecting NaN and infinity. Dates require an explicit format: `iso8601`,
|
|
`unix-seconds`, or `unix-milliseconds`. Display strings are trimmed and length-limited. Missing optional paths do not fail
|
|
the whole snapshot; a present value with the wrong type does.
|
|
|
|
No wildcards, recursive descent, filters, arithmetic, template evaluation, or user-supplied code. Multi-window arrays and
|
|
aggregation are later design work.
|
|
|
|
Hard limits: 16 mapped leaves per definition; 256 UTF-8 bytes and 32 components per path; 64 ASCII characters per
|
|
segment; array indices 0–4095; response JSON nesting depth 64; mapped display strings 256 UTF-8 bytes. Validate these
|
|
limits before traversal. Preflight structural nesting directly on the bounded response bytes with an iterative,
|
|
string-aware scanner before materializing JSON, so a hostile nested payload cannot exhaust the call stack.
|
|
|
|
### Network and secret boundary
|
|
|
|
- Require HTTPS for authenticated requests. Allow HTTP only for an unauthenticated loopback URL (`localhost`,
|
|
`127.0.0.0/8`, or `::1`). Reject URL user info and fragments.
|
|
- Extend `ProviderEndpointOverrideValidator`; do not create a second URL parser. Use a dedicated
|
|
`ProviderHTTPClient` configuration that rejects every redirect, even though the shared client safely permits same-origin
|
|
HTTPS redirects.
|
|
- Bind the secret to the validated origin. Disable redirects for MVP; a 3xx response is an error.
|
|
- Before any custom-provider fetch, require a local approval record binding the instance ID, complete normalized request
|
|
URL, origin, and auth type. Authenticated approvals also bind the fixed header and derived variable name. Store this
|
|
record outside the provider config. First use requires explicit app or interactive CLI confirmation that displays the
|
|
exact normalized URL and auth fields; headless use fails closed. No import or bulk-approval path is allowed. Loopback,
|
|
IP-literal, `.local`, and visibly private targets require typing the normalized URL instead of accepting a button. Any
|
|
bound-field change invalidates approval before the next fetch. This gate applies to unauthenticated loopback HTTP too.
|
|
- Never interpolate a secret into a URL, path, query, body, label, mapping, diagnostic, or log.
|
|
- Resolve the derived environment variable only after approval, when the provider is enabled and a fetch starts. Do not
|
|
enumerate the environment.
|
|
- Use a dedicated `URLSessionConfiguration.ephemeral` session with `httpCookieStorage = nil`,
|
|
`httpShouldSetCookies = false`, `urlCredentialStorage = nil`, `urlCache = nil`, and a reload-ignoring-local-cache policy.
|
|
Do not share a session with first-party providers. A dedicated challenge handler allows normal server-trust evaluation
|
|
only and cancels client-certificate or HTTP authentication challenges.
|
|
- Send `Accept-Encoding: identity`, reject a non-identity `Content-Encoding`, and enforce the streaming 1 MiB cap on bytes
|
|
delivered after URL loading's decoding and before JSON materialization. Cancel the task when the cap is exceeded. Use a
|
|
15-second total timeout and a JSON content-type check.
|
|
- Accept only 2xx responses. Error text may include status and a bounded generic summary, never request headers or the raw
|
|
response body.
|
|
- Keep custom-provider response data out of provider status polling, cookie import, OAuth, Keychain, token accounts,
|
|
browser automation, and CLI subprocess paths.
|
|
- Custom definitions must be local config only. No remote catalogs, downloaded definitions, or config URLs.
|
|
|
|
### Runtime identity seam
|
|
|
|
Introduce a `ProviderInstanceID` string value that identifies one configured instance. Keep `UsageProvider` as the
|
|
compile-time implementation kind for first-party providers.
|
|
|
|
```text
|
|
ProviderDefinition
|
|
firstParty(instanceID, UsageProvider, ProviderConfig)
|
|
customHTTPJSON(instanceID, CustomHTTPJSONConfig)
|
|
```
|
|
|
|
Migrate runtime dictionaries, persistence keys, `ProviderIdentitySnapshot.providerID`, and identity accessors that
|
|
represent an enabled provider instance to `ProviderInstanceID`. First-party instance IDs retain their current raw values,
|
|
preserving existing config and stored history. Provider-specific fetchers continue to receive `UsageProvider`; the custom
|
|
fetcher receives only its validated custom definition. Provider-specific identity payloads remain keyed by their
|
|
compile-time implementation kind, while shared organization and login-method fields belong to the provider instance.
|
|
|
|
This seam must land independently with characterization tests before the custom network path. It prevents a custom
|
|
provider from being threaded through exhaustive first-party switches or sharing state with another custom instance.
|
|
|
|
## Threat model
|
|
|
|
| Threat | Required mitigation |
|
|
|---|---|
|
|
| Shared or malicious config exfiltrates a secret | Dedicated per-instance variable; separate full-URL/auth approval before secret resolution; config changes invalidate approval; redirects disabled |
|
|
| Endpoint redirects auth to another host | Treat every redirect as failure |
|
|
| Shared config silently probes or changes a GET target | No network access before separate full-URL approval; any URL change invalidates it; no bulk approval; elevated confirmation for visibly local/private targets |
|
|
| Hostile JSON causes CPU or memory pressure | 1 MiB cap; bounded depth and path length; no recursive expressions; request timeout |
|
|
| Response injects misleading or huge menu text | Typed targets; numeric bounds; string trimming and length limits; configured identity wins |
|
|
| Secret or response leaks through diagnostics | Redacted request description; no headers or raw response body in errors/logs |
|
|
| Two custom providers overwrite one another | Stable `ProviderInstanceID` keys throughout runtime and persistence |
|
|
| Config silently changes first-party behavior | Tagged definition; versioned decoder; duplicate/reserved ID rejection; migration tests |
|
|
|
|
Out of scope: defending a user from a request URL they explicitly approved, including a public hostname that later
|
|
resolves to a local or private address. Approval grants that origin network authority for the approved URL; the
|
|
confirmation must state this clearly. CodexBar still must contain the service response and must never disclose unrelated
|
|
credentials.
|
|
|
|
## Explicit non-goals
|
|
|
|
- Settings UI for creating or editing custom providers.
|
|
- Full JSONPath, jq, scripts, plugins, transforms, arithmetic, or templates.
|
|
- POST/PUT/PATCH/DELETE, request bodies, refresh mutations, or multiple endpoints.
|
|
- Arbitrary headers, cookies, OAuth, browser sessions, Keychain discovery, file-secret references, or inline secrets.
|
|
- Custom SVG/file icons, downloaded assets, or remote provider manifests.
|
|
- Status-page discovery, incident notifications, chat/model APIs, cost-log scanning, widgets, or token accounts.
|
|
- Arrays of rate windows, cross-response joins, pagination, aggregation, or provider-specific special cases.
|
|
- Compatibility shims that reinterpret an unknown first-party provider ID as a custom provider.
|
|
|
|
## Implementation slices
|
|
|
|
1. **Identity seam:** add `ProviderInstanceID`; migrate config/runtime/persistence keys without behavior changes; add
|
|
decode, history, enablement, menu, CLI, and widget characterization tests.
|
|
2. **Pure evaluator:** add config types, validator, dot-path parser, typed coercion, and `UsageSnapshot` mapping using only
|
|
fixture data.
|
|
3. **Bounded transport:** add URL/auth policy and an injected HTTP transport; prove redirect, timeout, size, content-type,
|
|
status, and redaction behavior.
|
|
4. **Config and CLI integration:** version-2 migration, `codexbar config validate`, local approval records and interactive
|
|
approval command, diagnostics, and custom-provider CLI output. No live credentials in tests.
|
|
5. **App integration:** generic metadata/icon, refresh lifecycle, menu rendering, persistence, and disabled/error states
|
|
through existing shared provider UI.
|
|
|
|
Each slice should be a separately reviewable PR. Do not combine the identity migration and arbitrary networking in one
|
|
change.
|
|
|
|
## Required proof before enabling the feature
|
|
|
|
- Config v1 round-trip and v1-to-v2 migration preserve every existing provider entry.
|
|
- Duplicate, reserved, malformed, and colliding instance IDs fail validation.
|
|
- Multiple custom instances keep snapshots, errors, histories, menu selection, and persistence isolated.
|
|
- URL table covers HTTPS, user info, fragments, loopback HTTP, private/public HTTP, IPv4/IPv6, ports, and redirects.
|
|
- Auth tests prove the secret reaches only the intended header and never URL, errors, fixtures, snapshots, or logs.
|
|
- Approval tests prove first use and every bound-field change fail closed before network or environment access, and that
|
|
one instance cannot reuse another instance's approval or derived secret variable. UI/CLI proof covers exact-URL
|
|
display, no bulk approval, and typed confirmation for loopback, IP-literal, `.local`, and visibly private targets.
|
|
- Mapping tests cover missing/null paths, arrays, wrong types, date formats, finite-number enforcement, every numeric
|
|
path/depth/count bound, iterative depth rejection, and string limits.
|
|
- Transport tests cover timeout, cancellation, decoded response cap, compressed-response rejection, content type,
|
|
non-2xx, and 3xx without live network access.
|
|
- Transport isolation tests prove ambient cookies and URL credentials are neither sent nor persisted and cached responses
|
|
are not reused.
|
|
- Source-blind CLI proof: a fixture endpoint plus isolated config produces the expected usage, cost, identity, and redacted
|
|
failure output.
|
|
- `make test`, `make check`, structured autoreview, and exact-head CI are green for every implementation PR.
|
|
|
|
## Accepted owner decisions
|
|
|
|
1. Declarative provider support is worth the runtime identity migration and long-term versioned schema support.
|
|
2. MVP may use unauthenticated loopback HTTP only under the same separate approval gate, including typed confirmation of
|
|
the normalized URL. Every authenticated request requires HTTPS.
|
|
3. A derived per-instance environment variable plus local URL/auth approval is the only MVP secret source. Keychain
|
|
storage is deferred; the initial design must not imply or preserve a second secret path.
|
|
4. MVP supports one primary rate window, with cost and identity optional. Multi-window and aggregation semantics remain
|
|
out of scope.
|
|
5. The first integrated surface is CLI-only. App settings and menu integration wait until the shared runtime accepts
|
|
dynamic identities without provider-specific side paths.
|
|
|
|
Implementation gate: keep custom-provider networking disabled until the independent identity migration, pure evaluator,
|
|
bounded transport, approval flow, and their required proof land as separately reviewable changes. If an implementation
|
|
cannot preserve this boundary, stop rather than shipping a single `.custom` slot, a parallel UI path, or a compatibility
|
|
fallback.
|