@omnicross/daemon 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.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/cli.cjs +4562 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +4567 -0
- package/dist/index.cjs +3643 -0
- package/dist/index.d.cts +1375 -0
- package/dist/index.d.ts +1375 -0
- package/dist/index.js +3612 -0
- package/package.json +62 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1375 @@
|
|
|
1
|
+
import { ApiKeyPoolService } from '@omnicross/core/completion/ApiKeyPoolService';
|
|
2
|
+
import { OutboundKeyDb, OutboundApiServer } from '@omnicross/core/outbound-api';
|
|
3
|
+
import { ProviderProxy } from '@omnicross/core/provider-proxy';
|
|
4
|
+
import { SubscriptionCredentialStore, FetchLike, SubscriptionProviderRegistry, SubscriptionAccountService } from '@omnicross/subscriptions';
|
|
5
|
+
import { OutboundApiServerConfig, ProviderConfigSource, TransformerService, Transformer, ResolvedTransformerChain, ApiServerSettingsStore, Logger, OutboundKeyDb as OutboundKeyDb$1, OutboundKeyDbRow } from '@omnicross/core';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import { LLMProvider, AgentDefaultModels, GlobalModelParameters } from '@omnicross/contracts/llm-config';
|
|
8
|
+
import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
|
|
9
|
+
import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, SubscriptionAccountSanitized } from '@omnicross/contracts/account-tokens-types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SecretBox.ts — a 32-byte master key wrapped with the tri-state secret rules
|
|
13
|
+
* (secrets design D2/D7).
|
|
14
|
+
*
|
|
15
|
+
* `SecretBox` holds the resolved master key (never exposed) and applies the
|
|
16
|
+
* tri-state discrimination on top of the pure `envelope.ts` codec:
|
|
17
|
+
*
|
|
18
|
+
* `$`-prefix → ENV indirection: ALWAYS plaintext, never encrypted (resolved
|
|
19
|
+
* later by the pool's `resolveEnvKey`). Passthrough both ways.
|
|
20
|
+
* `enc:` → ciphertext: decrypted on read, left as-is on write.
|
|
21
|
+
* else → legacy plaintext: passed through on read, encrypted on write.
|
|
22
|
+
*
|
|
23
|
+
* `decryptMaybe` / `encryptMaybe` are the idempotent seam every read/write
|
|
24
|
+
* accessor calls — applying either repeatedly never changes a correctly-typed
|
|
25
|
+
* value (no `enc:enc:` nesting, no double-decrypt). `decrypt` wraps a GCM
|
|
26
|
+
* auth-tag failure (wrong key / tampered envelope) into an actionable,
|
|
27
|
+
* secret-free error — it NEVER echoes the key or the ciphertext.
|
|
28
|
+
*
|
|
29
|
+
* LAZY KEY (secrets design D3 — "首次需要时自动生成"): the constructor accepts
|
|
30
|
+
* either a resolved 32-byte Buffer OR a `() => Buffer` resolver. The key is only
|
|
31
|
+
* resolved on the FIRST `encrypt`/`decrypt` (then cached). The tri-state
|
|
32
|
+
* passthroughs (`$ENV` / empty / non-envelope-on-read) return BEFORE the key is
|
|
33
|
+
* touched — so a pure legacy-plaintext load that only passes values through
|
|
34
|
+
* NEVER triggers key resolution, and therefore never auto-generates a keyfile.
|
|
35
|
+
* A keyfile is materialized only when an `enc:` value must be decrypted or a
|
|
36
|
+
* write must encrypt (the resolver does the lazy auto-gen).
|
|
37
|
+
*
|
|
38
|
+
* @module @omnicross/daemon/secrets/SecretBox
|
|
39
|
+
*/
|
|
40
|
+
/** A resolved master key, or a lazy resolver that produces one on first use. */
|
|
41
|
+
type MasterKeyInput = Buffer | (() => Buffer);
|
|
42
|
+
declare class SecretBox {
|
|
43
|
+
/** The raw 32-byte master key (resolved lazily). Held privately; never logged. */
|
|
44
|
+
private key;
|
|
45
|
+
/** The lazy resolver (used once, then nulled after caching the key). */
|
|
46
|
+
private resolver;
|
|
47
|
+
constructor(key: MasterKeyInput);
|
|
48
|
+
/** Resolve (and cache) the master key on first crypto use. Validates length. */
|
|
49
|
+
private getKey;
|
|
50
|
+
/** Encrypt a plaintext value into a fresh `enc:v1:...` envelope (unconditional). */
|
|
51
|
+
encrypt(plain: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Decrypt an `enc:v1:...` envelope to plaintext. Wraps a GCM verification
|
|
54
|
+
* failure (wrong master key or a tampered envelope) into a clear, actionable
|
|
55
|
+
* error — the original crypto error (which carries no secret material) is
|
|
56
|
+
* intentionally NOT re-surfaced verbatim and the ciphertext/key are never
|
|
57
|
+
* placed in the message.
|
|
58
|
+
*/
|
|
59
|
+
decrypt(envelope: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* READ-direction tri-state: decrypt an `enc:` envelope; pass a `$ENV`
|
|
62
|
+
* reference or legacy plaintext through unchanged. Idempotent on any
|
|
63
|
+
* non-envelope value.
|
|
64
|
+
*/
|
|
65
|
+
decryptMaybe(value: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* WRITE-direction tri-state: encrypt legacy plaintext; pass a `$ENV` reference
|
|
68
|
+
* (never encrypt indirection) or an already-`enc:` envelope (no `enc:enc:`
|
|
69
|
+
* nesting) through unchanged. Idempotent — re-applying never re-encrypts.
|
|
70
|
+
*/
|
|
71
|
+
encryptMaybe(value: string): string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* config.ts — the daemon's `config.json` schema + load/save (design D9).
|
|
76
|
+
*
|
|
77
|
+
* The daemon is BYO-key and factory-less: its `config.json` provider rows ARE
|
|
78
|
+
* the provider catalog the `ConfigFileProviderConfigSource` serves. The `server`
|
|
79
|
+
* field is the same `OutboundApiServerConfig` shape `loadServerConfig`
|
|
80
|
+
* normalizes (persisted via `JsonApiServerSettingsStore` under the single
|
|
81
|
+
* `'outboundApiServer.config'` key).
|
|
82
|
+
*
|
|
83
|
+
* `loadConfig(path)` reads + shape-guards; `saveConfig(path, cfg)` writes pretty
|
|
84
|
+
* JSON. The loader is defensive (best-effort, never throws on a partial file) so
|
|
85
|
+
* the CLI surfaces a clear error rather than a stack trace.
|
|
86
|
+
*
|
|
87
|
+
* AT-REST ENCRYPTION (secrets design D5/D7): a MODULE-LEVEL `SecretBox` is
|
|
88
|
+
* injected via `setSecretBox` (by bootstrap + each offline CLI command). When
|
|
89
|
+
* set, `loadConfig` decrypts the secret fields (`apiKey`/`apiKeys[].apiKey`/
|
|
90
|
+
* `admin.token`) AFTER the shape-guard (envelopes are strings → shape-guard
|
|
91
|
+
* passes), and `saveConfig` encrypts them before writing. When NOT set (box =
|
|
92
|
+
* null), both are a no-op passthrough — so the existing pure tests that call
|
|
93
|
+
* `loadConfig`/`saveConfig`/`validateConfig` without a box are byte-unchanged.
|
|
94
|
+
* `$ENV` references are never encrypted (the box's tri-state passthrough).
|
|
95
|
+
*
|
|
96
|
+
* @module @omnicross/daemon/config
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/** The wire formats the daemon's BYO providers can speak. */
|
|
100
|
+
type DaemonApiFormat = 'openai' | 'anthropic' | 'gemini';
|
|
101
|
+
/**
|
|
102
|
+
* One pool key on a provider row (design D1). Structurally compatible with
|
|
103
|
+
* core's `ApiKeyEntry` (`@omnicross/contracts/llm-config`) — a hand-authored SUBSET: only
|
|
104
|
+
* `id` + `apiKey` are required, the rest carry sensible defaults applied at
|
|
105
|
+
* load time (`pool/loadPoolKeys.ts` normalizes a `DaemonApiKeyEntry` up to the
|
|
106
|
+
* full `ApiKeyEntry` core consumes, filling `providerId`/`label`/`weight`/
|
|
107
|
+
* `enabled`/`sortOrder`). config.json should not have to hand-write core's
|
|
108
|
+
* DB/UI fields (`providerId`/`sortOrder`/`hasKey`/`disabledReason`/…).
|
|
109
|
+
*/
|
|
110
|
+
interface DaemonApiKeyEntry {
|
|
111
|
+
/** Stable id — the pool's selection / cooldown / auto-disable key. */
|
|
112
|
+
id: string;
|
|
113
|
+
/** The BYO key (literal, or a `$ENV_VAR` reference resolved at call time). */
|
|
114
|
+
apiKey: string;
|
|
115
|
+
/** Display name (defaults to `id`). */
|
|
116
|
+
label?: string;
|
|
117
|
+
/** Whether this key is selectable (defaults to `true`). */
|
|
118
|
+
enabled?: boolean;
|
|
119
|
+
/** Weighted round-robin weight (defaults to `1`). */
|
|
120
|
+
weight?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Per-model metadata subset (app-parity child 2). A hand-authored SUBSET of the
|
|
124
|
+
* app's `ModelConfig` (`app/src/shared-types/llm-config.ts`), carrying ONLY the
|
|
125
|
+
* named-five fields the daemon stores + round-trips, keyed by the model `id`:
|
|
126
|
+
* `name` (display name), `enabled`, `group`, `vision`, `reasoning`. The wider
|
|
127
|
+
* `ModelConfig` fields the discovery flow may send (`category`/`contextLength`/
|
|
128
|
+
* `maxTokens`/`functionCall`/`webSearch`/`completionSettings`/`openRouterProvider`/
|
|
129
|
+
* `thinkingLevels`/…) are NOT in this allowlist — they are DROPPED by
|
|
130
|
+
* deny-by-default (`validateModelConfigs`/`parseModelConfigsInput`).
|
|
131
|
+
*
|
|
132
|
+
* ENFORCEMENT (app-parity-2 child 2): `enabled` is now a DISCOVERY/advertisement
|
|
133
|
+
* gate — `toLLMProvider` drops a `enabled: false` model from the routed provider's
|
|
134
|
+
* `models[]`, so the served catalog no longer lists it. HONEST SCOPE: this gates
|
|
135
|
+
* advertisement, NOT a hard per-request block (core does not validate a requested
|
|
136
|
+
* model against `models[]`, so a hardcoded disabled model id still reaches the
|
|
137
|
+
* upstream, which rejects it). The admin management view (`toProviderView`) still
|
|
138
|
+
* lists ALL models. The other fields (`name`/`group`/`vision`/`reasoning`) remain
|
|
139
|
+
* display-only metadata (no core per-model capability binding on the BYO path).
|
|
140
|
+
*/
|
|
141
|
+
interface DaemonModelConfig {
|
|
142
|
+
/** Model id — the metadata key (parallels an entry in the flat `models[]`). */
|
|
143
|
+
id: string;
|
|
144
|
+
/** Display name (display/management only; not consumed by routing). */
|
|
145
|
+
name?: string;
|
|
146
|
+
/** Enable flag — DISCOVERY GATE (parity-2 child 2): `false` drops the model from
|
|
147
|
+
* the routed/advertised catalog; not a hard per-request block. */
|
|
148
|
+
enabled?: boolean;
|
|
149
|
+
/** Display group label (app derives groups from this — no separate daemon array). */
|
|
150
|
+
group?: string;
|
|
151
|
+
/** Vision-capable hint (display only; not consumed by routing). */
|
|
152
|
+
vision?: boolean;
|
|
153
|
+
/** Reasoning-capable hint (display only; not consumed by routing). */
|
|
154
|
+
reasoning?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* One transformer chain entry (app-parity child 5). Mirrors the app's
|
|
158
|
+
* `TransformerEntry`: either a bare transform-rule NAME (`string`), or a
|
|
159
|
+
* `[name, options]` tuple carrying that rule's options. NON-SECRET (rule names +
|
|
160
|
+
* options — no key material).
|
|
161
|
+
*/
|
|
162
|
+
type DaemonTransformerEntry = string | [string, Record<string, unknown>];
|
|
163
|
+
/**
|
|
164
|
+
* Provider transformer config subset (app-parity child 5). Mirrors the app's
|
|
165
|
+
* `TransformerConfig` PROVIDER-LEVEL portion: `use[]` is the provider-level
|
|
166
|
+
* transform chain. The index signature preserves any per-model transformer keys
|
|
167
|
+
* (`[modelName]`) VERBATIM as an opaque value so a round-trip is non-lossy — the
|
|
168
|
+
* minimal editor only edits `use[]`, but stored per-model keys are not dropped.
|
|
169
|
+
*
|
|
170
|
+
* ENFORCED (app-parity-2 child 2): the daemon APPLIES this `use[]` chain in the
|
|
171
|
+
* request pipeline. `ConfigFileProviderConfigSource.resolveTransformerChain`
|
|
172
|
+
* resolves the custom `use[]` into the provider chain, composed FORMAT-FIRST — the
|
|
173
|
+
* format transformer (anthropic/gemini, supplied by `getMainTransformer` and
|
|
174
|
+
* prepended by core's `resolveProviderChain`) runs before the custom transformers,
|
|
175
|
+
* preserving the load-bearing wire-format conversion. An unknown transformer name
|
|
176
|
+
* is warned + skipped (lenient). Additive + back-compat: absent (or empty `use[]`)
|
|
177
|
+
* reads as undefined and resolves to the format transformer alone — byte-identical
|
|
178
|
+
* to before. NON-SECRET — round-trips verbatim on GET (no masking).
|
|
179
|
+
*/
|
|
180
|
+
interface DaemonTransformerConfig {
|
|
181
|
+
/** Provider-level transform chain (the UI count + the minimal editor surface). */
|
|
182
|
+
use?: DaemonTransformerEntry[];
|
|
183
|
+
/** Per-model transformer keys preserved verbatim (opaque — not edited here). */
|
|
184
|
+
[modelName: string]: unknown;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Coding-plan endpoint config (app-parity-2 child 3). The provider's "coding-plan"
|
|
188
|
+
* subscription endpoint (e.g. domestic providers' 编程套餐 — Zhipu GLM Coding Plan,
|
|
189
|
+
* DashScope, DeepSeek, Kimi …): an OPTIONAL alternate endpoint with its OWN
|
|
190
|
+
* `baseUrl` + `apiKey`, distinct from the pay-as-you-go API key. Structurally
|
|
191
|
+
* identical to the contracts `CodingPlanConfig` (`@omnicross/contracts/provider-presets`)
|
|
192
|
+
* so a daemon row's `codingPlan` assigns straight onto `LLMProvider.codingPlan`.
|
|
193
|
+
*
|
|
194
|
+
* SECRET-BEARING: `apiKey` is encrypted at rest (registered in
|
|
195
|
+
* `secretFields.transformProvider`) and NEVER serialized back out — the masked GET
|
|
196
|
+
* view returns only a `hasApiKey` boolean.
|
|
197
|
+
*
|
|
198
|
+
* ENFORCED BY CORE, NOT THE DAEMON: the daemon does NOT contain its own endpoint
|
|
199
|
+
* resolver. It only POPULATES `LLMProvider.codingPlan` in `toLLMProvider`; the
|
|
200
|
+
* shared `resolveProviderEndpoint` (`@omnicross/contracts/endpoint-resolver`, layer
|
|
201
|
+
* 2: `apiModes > codingPlan > plain`) — already wired into core's `buildProviderApiUrl`
|
|
202
|
+
* (URL) and the BYO proxy key path — does the actual routing (when `enabled` AND a
|
|
203
|
+
* `baseUrl` is set, the request uses this `baseUrl` + `apiKey`, key falling back to
|
|
204
|
+
* the provider's main key when empty). Additive + back-compat: absent → undefined.
|
|
205
|
+
*/
|
|
206
|
+
interface DaemonCodingPlanConfig {
|
|
207
|
+
/** Whether the coding-plan endpoint is active (core routes via baseUrl/apiKey below). */
|
|
208
|
+
enabled: boolean;
|
|
209
|
+
/** Dedicated base URL — when set AND enabled, core overrides the provider's baseUrl. */
|
|
210
|
+
baseUrl?: string;
|
|
211
|
+
/** Dedicated key (SECRET; literal or `$ENV`). Empty → core falls back to the main key. */
|
|
212
|
+
apiKey?: string;
|
|
213
|
+
/** Free-text plan note (display/management only). */
|
|
214
|
+
note?: string;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* One API mode (app-parity-2 child 4). The provider's selectable endpoint "mode"
|
|
218
|
+
* (e.g. `standard` / `coding-plan` / `token-plan` for domestic providers): each
|
|
219
|
+
* carries its own `baseUrl` + an OPTIONAL `apiKey`. Structurally identical to the
|
|
220
|
+
* contracts `ApiMode` so a daemon row's `apiModes` assigns straight onto
|
|
221
|
+
* `LLMProvider.apiModes`. SECRET-BEARING: `apiKey` is encrypted at rest
|
|
222
|
+
* (`secretFields`) + masked on GET (`hasApiKey` only) — never serialized out.
|
|
223
|
+
* ENFORCED BY CORE: `toLLMProvider` populates `apiModes`/`selectedApiModeId`; the
|
|
224
|
+
* shared `resolveProviderEndpoint` (layer 1) reports `source:'api-mode'` and uses
|
|
225
|
+
* `api_base_url || mode.baseUrl`. The row's `baseUrl`/`apiKey` hold the EFFECTIVE
|
|
226
|
+
* endpoint (synced on switch — baseUrl app-side, the secret key server-side).
|
|
227
|
+
*/
|
|
228
|
+
interface DaemonApiMode {
|
|
229
|
+
/** Stable mode id within the provider. */
|
|
230
|
+
id: string;
|
|
231
|
+
/** i18n key or display label. */
|
|
232
|
+
label: string;
|
|
233
|
+
/** This mode's endpoint base URL. */
|
|
234
|
+
baseUrl: string;
|
|
235
|
+
/** This mode's OPTIONAL key (SECRET; literal or `$ENV`). */
|
|
236
|
+
apiKey?: string;
|
|
237
|
+
/** Optional API-key prefix hint (e.g. `sk-tp-`). Non-secret. */
|
|
238
|
+
apiKeyPrefix?: string;
|
|
239
|
+
/** Optional note (i18n key). Non-secret. */
|
|
240
|
+
note?: string;
|
|
241
|
+
}
|
|
242
|
+
/** One BYO provider row — the unit the `ProviderConfigSource` serves. */
|
|
243
|
+
interface DaemonProviderConfig {
|
|
244
|
+
/** Stable id referenced by the per-endpoint `"<id>,<model>"` model refs. */
|
|
245
|
+
id: string;
|
|
246
|
+
/**
|
|
247
|
+
* OPTIONAL mutable display name (app-parity-2 child 1), SEPARATE from the
|
|
248
|
+
* immutable `id`. The `id` stays the identity key (model refs, pool, accounts);
|
|
249
|
+
* `name` is a free-text label the rename UI edits. Additive + back-compat:
|
|
250
|
+
* absent reads as undefined and the app falls back to displaying the `id`.
|
|
251
|
+
* NON-SECRET — round-trips verbatim on GET (no masking).
|
|
252
|
+
*/
|
|
253
|
+
name?: string;
|
|
254
|
+
/** The provider's wire format (drives the transformer chain). */
|
|
255
|
+
apiFormat: DaemonApiFormat;
|
|
256
|
+
/** The upstream base URL (e.g. `https://api.openai.com/v1`). */
|
|
257
|
+
baseUrl: string;
|
|
258
|
+
/** The BYO API key (literal, or a `$ENV_VAR` reference resolved at call time). */
|
|
259
|
+
apiKey: string;
|
|
260
|
+
/** Optional advertised model list (informational; routing uses the model ref). */
|
|
261
|
+
models?: string[];
|
|
262
|
+
/**
|
|
263
|
+
* OPTIONAL per-model metadata (app-parity child 2), keyed by model id, PARALLEL
|
|
264
|
+
* to the flat `models[]`. Additive + back-compat: a row with only `models[]`
|
|
265
|
+
* (no `modelConfigs`) loads unchanged and reads as undefined metadata; the flat
|
|
266
|
+
* `models[]` stays AUTHORITATIVE for the model catalog. ENFORCEMENT (parity-2
|
|
267
|
+
* child 2): `enabled` is a DISCOVERY GATE — `toLLMProvider` drops a
|
|
268
|
+
* `enabled: false` model from the routed `models[]` (advertisement-scoped, not a
|
|
269
|
+
* hard per-request block; the admin view still lists all). `name`/`group`/
|
|
270
|
+
* `vision`/`reasoning` stay display-only. Group lives here as `modelConfigs[].group`
|
|
271
|
+
* (no separate daemon `modelGroups[]`). None of the fields are secrets — verbatim on GET.
|
|
272
|
+
*/
|
|
273
|
+
modelConfigs?: DaemonModelConfig[];
|
|
274
|
+
/**
|
|
275
|
+
* OPTIONAL multi-key pool (design D1, key-pool change). When present, the
|
|
276
|
+
* `ApiKeyPoolService` loads these for observable health + (future) failover.
|
|
277
|
+
* **Absent `apiKeys` = single-key behavior byte-identical to before** — the
|
|
278
|
+
* pool synthesizes a 1-key pool from `apiKey` and the outbound take-key path
|
|
279
|
+
* is unchanged. The single `apiKey` field is RETAINED as the fallback.
|
|
280
|
+
*/
|
|
281
|
+
apiKeys?: DaemonApiKeyEntry[];
|
|
282
|
+
/**
|
|
283
|
+
* OPTIONAL enable flag (app-foundation D8). When absent, the provider reads as
|
|
284
|
+
* ENABLED (back-compat: existing `config.json` rows with no `enabled` field are
|
|
285
|
+
* treated as enabled). Surfaced on the admin GET DTO (`enabled: row.enabled !==
|
|
286
|
+
* false`) and accepted by the provider write path (PUT). Purely a management-UI
|
|
287
|
+
* concern — the routing/outbound paths do not consume it yet.
|
|
288
|
+
*/
|
|
289
|
+
enabled?: boolean;
|
|
290
|
+
/**
|
|
291
|
+
* OPTIONAL "official provider" management flag (app-parity child 1). Additive +
|
|
292
|
+
* back-compat: absent reads as the prior default (undefined). NON-SECRET —
|
|
293
|
+
* round-trips verbatim on GET (no masking). Management-UI only; the
|
|
294
|
+
* routing/outbound paths do not consume it.
|
|
295
|
+
*/
|
|
296
|
+
isOfficial?: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* OPTIONAL API version (e.g. an Azure `api-version`) (app-parity child 1).
|
|
299
|
+
* Additive + back-compat: absent reads as the prior default (undefined).
|
|
300
|
+
* NON-SECRET — round-trips verbatim on GET. Management-UI only (not yet consumed
|
|
301
|
+
* by the outbound path).
|
|
302
|
+
*/
|
|
303
|
+
apiVersion?: string;
|
|
304
|
+
/**
|
|
305
|
+
* OPTIONAL max-concurrency hint (app-parity child 1). Additive + back-compat:
|
|
306
|
+
* absent reads as the prior default (undefined). NON-SECRET — round-trips
|
|
307
|
+
* verbatim on GET. Management-UI only (not yet enforced by the routing path).
|
|
308
|
+
*/
|
|
309
|
+
maxConcurrency?: number;
|
|
310
|
+
/**
|
|
311
|
+
* OPTIONAL custom models endpoint URL (app-parity child 1). Additive +
|
|
312
|
+
* back-compat: absent reads as the prior default (undefined). NON-SECRET —
|
|
313
|
+
* round-trips verbatim on GET. Management-UI only.
|
|
314
|
+
*/
|
|
315
|
+
modelsEndpoint?: string;
|
|
316
|
+
/**
|
|
317
|
+
* OPTIONAL provider transformer config (app-parity child 5). Additive +
|
|
318
|
+
* back-compat: absent reads as the prior default (undefined). NON-SECRET —
|
|
319
|
+
* round-trips verbatim on GET (no masking). ENFORCED (parity-2 child 2): the
|
|
320
|
+
* daemon APPLIES the custom `use[]` chain in the request pipeline, FORMAT-FIRST
|
|
321
|
+
* (see `DaemonTransformerConfig`); absent/empty resolves to the format
|
|
322
|
+
* transformer alone (byte-identical to before).
|
|
323
|
+
*/
|
|
324
|
+
transformer?: DaemonTransformerConfig;
|
|
325
|
+
/**
|
|
326
|
+
* OPTIONAL coding-plan endpoint (app-parity-2 child 3). SECRET-BEARING (its
|
|
327
|
+
* `apiKey` is encrypted at rest + masked on GET). Populated onto
|
|
328
|
+
* `LLMProvider.codingPlan` by `toLLMProvider`; ENFORCED by core's shared
|
|
329
|
+
* `resolveProviderEndpoint` (the daemon does not resolve endpoints itself).
|
|
330
|
+
* Additive + back-compat: absent reads as undefined.
|
|
331
|
+
*/
|
|
332
|
+
codingPlan?: DaemonCodingPlanConfig;
|
|
333
|
+
/**
|
|
334
|
+
* OPTIONAL API modes (app-parity-2 child 4) — selectable endpoint modes. Each
|
|
335
|
+
* mode's `apiKey` is SECRET (encrypted at rest, masked on GET). Populated onto
|
|
336
|
+
* `LLMProvider.apiModes` by `toLLMProvider`; the SELECTED mode drives core's
|
|
337
|
+
* `resolveProviderEndpoint` (layer 1). Additive + back-compat: absent → undefined.
|
|
338
|
+
*/
|
|
339
|
+
apiModes?: DaemonApiMode[];
|
|
340
|
+
/** OPTIONAL id of the active API mode (app-parity-2 child 4). */
|
|
341
|
+
selectedApiModeId?: string;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The admin-dashboard config block (RT3 design D8). All fields optional;
|
|
345
|
+
* defaults are applied at read time by `resolveAdminConfig`.
|
|
346
|
+
*
|
|
347
|
+
* SECURITY: `token` is a SEPARATE secret (NOT a named outbound key). It is never
|
|
348
|
+
* serialized back out by any management-API GET — the masking spine treats it
|
|
349
|
+
* like every other secret (in, never out).
|
|
350
|
+
*/
|
|
351
|
+
interface DaemonAdminConfig {
|
|
352
|
+
/** Dashboard on by default; `false` opts out (same as `--no-dashboard`). */
|
|
353
|
+
enabled?: boolean;
|
|
354
|
+
/** Admin server port (default 8766; distinct from the 8765 outbound server). */
|
|
355
|
+
port?: number;
|
|
356
|
+
/** Bind `0.0.0.0` (LAN) instead of `127.0.0.1`. Requires `token` (fail-closed). */
|
|
357
|
+
networkBinding?: boolean;
|
|
358
|
+
/** Optional bearer secret; when set every `/admin/*` request must carry it. */
|
|
359
|
+
token?: string;
|
|
360
|
+
}
|
|
361
|
+
/** Resolved admin config with defaults applied (read-time view). */
|
|
362
|
+
interface ResolvedAdminConfig {
|
|
363
|
+
enabled: boolean;
|
|
364
|
+
port: number;
|
|
365
|
+
networkBinding: boolean;
|
|
366
|
+
token: string | undefined;
|
|
367
|
+
}
|
|
368
|
+
/** The default admin port (distinct from the 8765 outbound server). */
|
|
369
|
+
declare const DEFAULT_ADMIN_PORT = 8766;
|
|
370
|
+
/** The full daemon config. */
|
|
371
|
+
interface DaemonConfig {
|
|
372
|
+
providers: DaemonProviderConfig[];
|
|
373
|
+
/** Persisted outbound-API server config (same shape `loadServerConfig` normalizes). */
|
|
374
|
+
server?: OutboundApiServerConfig;
|
|
375
|
+
/** Optional admin-dashboard config (RT3). */
|
|
376
|
+
admin?: DaemonAdminConfig;
|
|
377
|
+
}
|
|
378
|
+
/** Apply defaults to a (possibly absent) admin block: enabled, port 8766,
|
|
379
|
+
* loopback, no token. NOTE: an EXPLICIT `port: 0` is honored as "bind an
|
|
380
|
+
* ephemeral port" (it is NOT coerced to the default); only an absent/undefined
|
|
381
|
+
* port falls back to `DEFAULT_ADMIN_PORT`. */
|
|
382
|
+
declare function resolveAdminConfig(admin: DaemonAdminConfig | undefined): ResolvedAdminConfig;
|
|
383
|
+
/** Validate a parsed config object into a typed `DaemonConfig`. */
|
|
384
|
+
declare function validateConfig(raw: unknown): DaemonConfig;
|
|
385
|
+
/** Read + validate the config.json at `path`. When a `SecretBox` is set, the
|
|
386
|
+
* secret fields are decrypted AFTER shape-guarding (envelopes are strings, so
|
|
387
|
+
* the guard passes); the returned config carries DECRYPTED values. */
|
|
388
|
+
declare function loadConfig(path: string): DaemonConfig;
|
|
389
|
+
/** Write `cfg` to `path` as pretty JSON. When a `SecretBox` is set, the secret
|
|
390
|
+
* fields are encrypted-on-write (legacy plaintext → `enc:v1:`; `$ENV`/already-
|
|
391
|
+
* `enc:` untouched) before serializing. */
|
|
392
|
+
declare function saveConfig(path: string, cfg: DaemonConfig): void;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* JsonSubscriptionCredentialStore — the daemon's file-backed
|
|
396
|
+
* `SubscriptionCredentialStore` port impl (design D1).
|
|
397
|
+
*
|
|
398
|
+
* Implements `@omnicross/subscriptions`' narrow six-method credential surface
|
|
399
|
+
* over a sibling `tokens.json` holding an `AccountTokensConfig`-shaped object
|
|
400
|
+
* (`{ claude?, codex?, gemini?, opencodego?, updatedAt }`). Modeled on
|
|
401
|
+
* `JsonOutboundKeyDb`: the constructor takes the path; reads are
|
|
402
|
+
* `existsSync` → `readFileSync` → `JSON.parse`, tolerating a missing/corrupt
|
|
403
|
+
* file by returning a minimal `{ updatedAt }` config (the strategies already
|
|
404
|
+
* guard `?.accessToken`, so a partial/empty config never crashes dispatch).
|
|
405
|
+
*
|
|
406
|
+
* The PORT surface is read-only by design: the codex / gemini strategies pull
|
|
407
|
+
* their access token via `getFullConfig().<provider>.accessToken`; only claude /
|
|
408
|
+
* opencodego have dedicated getters. No OAuth login flow is initiated here
|
|
409
|
+
* (strategies only consume + refresh, never log in).
|
|
410
|
+
*
|
|
411
|
+
* DAEMON-ONLY WRITE PATH (token-paste, design D1): `writeProviderTokens` /
|
|
412
|
+
* `clearProvider` are CONCRETE-CLASS methods — NOT part of the
|
|
413
|
+
* `SubscriptionCredentialStore` port. The registry / auth strategies / account
|
|
414
|
+
* service never see them (they hold the port type), so a mutation can never leak
|
|
415
|
+
* into the subscription block. Only the daemon admin API (which holds the
|
|
416
|
+
* concrete instance via `Daemon.credentialStore`) calls them. They read-merge a
|
|
417
|
+
* single provider block into `tokens.json` and re-persist; since `readConfig`
|
|
418
|
+
* re-reads on every call (NO cache), the next read immediately sees the write.
|
|
419
|
+
*
|
|
420
|
+
* AT-REST ENCRYPTION (secrets design D6/D7): the constructor takes a `SecretBox`.
|
|
421
|
+
* `readConfig` decrypts the token-material fields on read (so every getter +
|
|
422
|
+
* `getFullConfig` returns PLAINTEXT tokens — the subscription bearer path is
|
|
423
|
+
* byte-identical), and `persist` encrypts them before writing. Because EVERY
|
|
424
|
+
* write funnels through `persist`, the OAuth-refresh writes below are encrypted
|
|
425
|
+
* at-rest with NO extra work (the store API guarantees it). The "re-read on every
|
|
426
|
+
* call, no cache" semantics are unchanged.
|
|
427
|
+
*
|
|
428
|
+
* REAL TOKEN REFRESH (oauth design D4): `refresh{Claude,Codex,Gemini}Token` mint
|
|
429
|
+
* a new access token via the shared host-clean OAuth refresh functions
|
|
430
|
+
* (`@omnicross/subscriptions/oauth`, injected `FetchLike` — default global
|
|
431
|
+
* `fetch`), then read-merge the refreshed fields into the provider block and
|
|
432
|
+
* write back through `persist` (→ encrypted). Field-writes:
|
|
433
|
+
* claude/codex write access+refresh(+codex idToken)
|
|
434
|
+
* +expiresAt+status:authorized+lastRefreshedAt; gemini writes ONLY access+
|
|
435
|
+
* expiresAt (its refresh response omits refresh_token → the OLD value is reused,
|
|
436
|
+
* never overwritten). On any failure the block is marked `status:'expired'` +
|
|
437
|
+
* errorMessage and `false` is returned. When the block has NO refresh_token
|
|
438
|
+
* (claude setup-token, manual token), it is an HONEST `false` BEFORE any upstream
|
|
439
|
+
* call — the block is not touched and no refresh_token is invented.
|
|
440
|
+
*
|
|
441
|
+
* @module @omnicross/daemon/ports/JsonSubscriptionCredentialStore
|
|
442
|
+
*/
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The per-provider token block accepted by `writeProviderTokens`. Mirrors the
|
|
446
|
+
* `AccountTokensConfig` per-provider field types (one of the four contract token
|
|
447
|
+
* shapes), keyed by `SubscriptionProviderId` — the daemon admin layer validates
|
|
448
|
+
* the wire body to one of these before calling the writer.
|
|
449
|
+
*/
|
|
450
|
+
type SubscriptionTokenBlock = ClaudeTokenConfig | CodexTokenConfig | GeminiTokenConfig | OpenCodeGoTokenConfig;
|
|
451
|
+
declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialStore {
|
|
452
|
+
private readonly tokensPath;
|
|
453
|
+
private readonly box;
|
|
454
|
+
private readonly fetchImpl;
|
|
455
|
+
/**
|
|
456
|
+
* @param tokensPath on-disk `tokens.json` location.
|
|
457
|
+
* @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
|
|
458
|
+
* @param fetchImpl injectable HTTP port for the OAuth refresh round-trips
|
|
459
|
+
* (oauth design D4). Defaults to the global `fetch` so boot
|
|
460
|
+
* is unchanged; tests inject a mock fetch. NOT used by any
|
|
461
|
+
* read/write path — only by `refresh*Token`.
|
|
462
|
+
*/
|
|
463
|
+
constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike);
|
|
464
|
+
/** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
|
|
465
|
+
* file is absent/corrupt). This is the hot read — the codex / gemini auth
|
|
466
|
+
* strategies pull `accessToken` / `expiresAt` / `status` from it. */
|
|
467
|
+
getFullConfig(): Promise<AccountTokensConfig>;
|
|
468
|
+
/** Current Claude OAuth access token, or `null` when none is stored. No inline
|
|
469
|
+
* refresh here — the lead-window / 401-retry refresh is driven by the
|
|
470
|
+
* subscription auth strategy, which calls `refreshClaudeToken` (now real). */
|
|
471
|
+
getValidClaudeAccessToken(): Promise<string | null>;
|
|
472
|
+
/** Current OpenCodeGo static API key, or `null` when none is stored. */
|
|
473
|
+
getValidOpenCodeGoApiKey(): Promise<string | null>;
|
|
474
|
+
/**
|
|
475
|
+
* DAEMON-ONLY sanitized accounts list (design D8, NOT on the port). Projects
|
|
476
|
+
* each provider's accounts to the secret-free `SubscriptionAccountSanitized`
|
|
477
|
+
* shape (id/label/status/expiresAt/hasAccessToken/isActive) — NEVER a token.
|
|
478
|
+
* Used by the admin accounts GET (secret-IN-never-OUT).
|
|
479
|
+
*/
|
|
480
|
+
listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
|
|
481
|
+
/**
|
|
482
|
+
* Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
|
|
483
|
+
* the block has no refresh_token (setup-token / manual) — no upstream call, the
|
|
484
|
+
* block is untouched. Otherwise mint via the shared claude refresh flow and
|
|
485
|
+
* write back access+refresh+expiresAt+status:authorized+lastRefreshedAt.
|
|
486
|
+
* On failure → status:expired +
|
|
487
|
+
* errorMessage → `false`.
|
|
488
|
+
*/
|
|
489
|
+
refreshClaudeToken(): Promise<boolean>;
|
|
490
|
+
/**
|
|
491
|
+
* Refresh the Codex (ChatGPT) OAuth access token. Same shape
|
|
492
|
+
* as claude, additionally writing back the refreshed `idToken`.
|
|
493
|
+
* HONEST `false` when no refresh_token.
|
|
494
|
+
*/
|
|
495
|
+
refreshCodexToken(): Promise<boolean>;
|
|
496
|
+
/**
|
|
497
|
+
* Refresh the Gemini (Google) OAuth access token. The Google
|
|
498
|
+
* refresh response does NOT return a refresh_token, so this writes ONLY
|
|
499
|
+
* access+expiresAt (+status/lastRefreshedAt) and DELIBERATELY leaves the
|
|
500
|
+
* existing `refreshToken` untouched (overwriting it with `undefined` would
|
|
501
|
+
* destroy the ability to refresh again). HONEST `false` when no refresh_token.
|
|
502
|
+
*/
|
|
503
|
+
refreshGeminiToken(): Promise<boolean>;
|
|
504
|
+
/**
|
|
505
|
+
* Materialize a lazily-synthesized account id to disk (design D3). On a legacy
|
|
506
|
+
* single-slot file, `readConfig` synthesizes a NON-deterministic account id
|
|
507
|
+
* per read; without persisting it, the later write-back (which re-reads) would
|
|
508
|
+
* synthesize a DIFFERENT id and miss the captured account. Persisting the
|
|
509
|
+
* migrated config here makes the id durable so the write-back keys correctly.
|
|
510
|
+
* Idempotent: a config whose ids are already on disk re-persists byte-equal.
|
|
511
|
+
*/
|
|
512
|
+
private materializeMigration;
|
|
513
|
+
/**
|
|
514
|
+
* Write refreshed tokens back to the captured account by id (oauth design D4),
|
|
515
|
+
* re-derive the mirror from the CURRENT active id, re-stamp + persist. A switch
|
|
516
|
+
* mid-refresh leaves the refreshed tokens in the captured (now non-active)
|
|
517
|
+
* account and keeps the CURRENT active account's tokens in the mirror.
|
|
518
|
+
*/
|
|
519
|
+
private writeBackById;
|
|
520
|
+
/**
|
|
521
|
+
* Mark the captured account `status:'expired'` + errorMessage on a refresh
|
|
522
|
+
* failure, keyed by id, then re-derive the mirror.
|
|
523
|
+
*/
|
|
524
|
+
private markExpiredById;
|
|
525
|
+
/**
|
|
526
|
+
* DAEMON-ONLY WRITE (design D1, NOT on the port). Read-merge the given
|
|
527
|
+
* provider's token block into the current `AccountTokensConfig`, stamp a fresh
|
|
528
|
+
* `updatedAt`, and re-persist `tokens.json` as pretty JSON. Preserves every
|
|
529
|
+
* OTHER provider's existing block (read-merge-write, not overwrite). Reuses the
|
|
530
|
+
* tolerate-on-read base (`{ updatedAt: '' }` when the file is absent/corrupt),
|
|
531
|
+
* so a first-ever write still produces a valid config. No cache → the next read
|
|
532
|
+
* sees this write.
|
|
533
|
+
*/
|
|
534
|
+
writeProviderTokens(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock): Promise<void>;
|
|
535
|
+
/**
|
|
536
|
+
* DAEMON-ONLY login append (design D5, NOT on the port). Append a NEW account
|
|
537
|
+
* (optional label) and set it active, then re-derive the mirror — used by
|
|
538
|
+
* `omnicross login <provider> --label` to add an account instead of overwriting.
|
|
539
|
+
*/
|
|
540
|
+
appendProviderAccount(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock, label?: string): Promise<{
|
|
541
|
+
id: string;
|
|
542
|
+
}>;
|
|
543
|
+
/**
|
|
544
|
+
* DAEMON-ONLY active switch (design D5, NOT on the port). Switch the active
|
|
545
|
+
* account for a provider; rejects an unknown id. Re-derives the mirror.
|
|
546
|
+
*/
|
|
547
|
+
setActiveAccount(providerId: SubscriptionProviderId, id: string): Promise<{
|
|
548
|
+
ok: boolean;
|
|
549
|
+
}>;
|
|
550
|
+
/**
|
|
551
|
+
* DAEMON-ONLY per-account remove (design D5, NOT on the port). Remove one
|
|
552
|
+
* account; promote the most-recent remaining on active-removal (or clear the
|
|
553
|
+
* mirror when none remain). Re-derives the mirror.
|
|
554
|
+
*/
|
|
555
|
+
removeAccount(providerId: SubscriptionProviderId, id: string): Promise<{
|
|
556
|
+
removed: boolean;
|
|
557
|
+
}>;
|
|
558
|
+
/**
|
|
559
|
+
* DAEMON-ONLY CLEAR (design D1/D3, NOT on the port). Remove a single provider's
|
|
560
|
+
* block from `tokens.json` and re-persist (the strategies already tolerate an
|
|
561
|
+
* absent block). Stamps a fresh `updatedAt`. A no-op-shaped write when the
|
|
562
|
+
* provider was already absent (still re-stamps + persists).
|
|
563
|
+
*/
|
|
564
|
+
clearProvider(providerId: SubscriptionProviderId): Promise<void>;
|
|
565
|
+
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
566
|
+
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
567
|
+
* → `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
568
|
+
* write — incl. child 4's future refresh writes — lands encrypted. */
|
|
569
|
+
private persist;
|
|
570
|
+
/**
|
|
571
|
+
* Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
|
|
572
|
+
* the token-material fields so every getter returns plaintext (the
|
|
573
|
+
* subscription bearer path is byte-identical).
|
|
574
|
+
*
|
|
575
|
+
* The fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
|
|
576
|
+
* file → empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
|
|
577
|
+
* wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
|
|
578
|
+
* box's clear, secret-free error (secrets spec "错误密钥 / 篡改的解密失败 UX":
|
|
579
|
+
* SHALL fail-fast, SHALL NOT 静默降级 — a swallowed decrypt would report "no
|
|
580
|
+
* tokens" and silently send the WRONG bearer upstream → 401). Mirrors
|
|
581
|
+
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
582
|
+
*/
|
|
583
|
+
private readConfig;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* accountsWrite — the daemon admin API's subscription-token WRITE path
|
|
588
|
+
* (`PUT|POST|DELETE /admin/api/accounts/:providerId`, design D1/D3/D4/D5).
|
|
589
|
+
*
|
|
590
|
+
* Extracted from `adminApi.ts` (file-size discipline, design D6) so the write
|
|
591
|
+
* handler + the per-provider body validators live in one focused module.
|
|
592
|
+
*
|
|
593
|
+
* SECRET SPINE (the load-bearing invariant): the token flows IN via the POST/PUT
|
|
594
|
+
* body and NEVER OUT. The write/clear response is STATUS-ONLY — the token-free
|
|
595
|
+
* `SubscriptionListEntry` for that provider (or a `{ ok: true }` ack). The handler
|
|
596
|
+
* NEVER serializes the request body (or any token field) back. The least-authority
|
|
597
|
+
* `SubscriptionTokenWriter` dep exposes ONLY `writeProviderTokens` / `clearProvider`,
|
|
598
|
+
* so this layer is structurally unable to read a stored token.
|
|
599
|
+
*
|
|
600
|
+
* @module @omnicross/daemon/admin/accountsWrite
|
|
601
|
+
*/
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Least-authority writer handle (design D4): the admin write path sees ONLY the
|
|
605
|
+
* mutation methods of the credential store, never a token-returning read. Wired
|
|
606
|
+
* in `bootstrap.ts` from the concrete `daemon.credentialStore`.
|
|
607
|
+
*/
|
|
608
|
+
interface SubscriptionTokenWriter {
|
|
609
|
+
writeProviderTokens(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock): Promise<void>;
|
|
610
|
+
clearProvider(providerId: SubscriptionProviderId): Promise<void>;
|
|
611
|
+
setActiveAccount(providerId: SubscriptionProviderId, id: string): Promise<{
|
|
612
|
+
ok: boolean;
|
|
613
|
+
}>;
|
|
614
|
+
removeAccount(providerId: SubscriptionProviderId, id: string): Promise<{
|
|
615
|
+
removed: boolean;
|
|
616
|
+
}>;
|
|
617
|
+
listSanitizedAccounts(): Promise<Record<string, SubscriptionAccountSanitized[]>>;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* oauthSessions — the in-memory pending-OAuth-session store for the admin HTTP
|
|
622
|
+
* two-phase login (`POST /accounts/:providerId/oauth/{start,complete}`,
|
|
623
|
+
* app-parity child 4, design D1).
|
|
624
|
+
*
|
|
625
|
+
* `start` mints a crypto-random `sessionId` and stashes the per-session PKCE
|
|
626
|
+
* `{ providerId, codeVerifier, state }` here; `complete` does a SINGLE-USE
|
|
627
|
+
* `take(sessionId)` (returns + deletes) and exchanges the code. The map is
|
|
628
|
+
* NEVER serialized to the client — only the opaque `sessionId` + the public
|
|
629
|
+
* `authUrl` cross the wire. Sessions are short-lived (OQ3 = 10-min TTL); a sweep
|
|
630
|
+
* reaps abandoned sessions, and `take` re-checks the TTL so an expired-but-not-
|
|
631
|
+
* yet-swept session is still rejected. A daemon restart simply drops in-flight
|
|
632
|
+
* logins (correct fail-safe — no partial token is ever written).
|
|
633
|
+
*
|
|
634
|
+
* SECRET SPINE: the `codeVerifier` is a PKCE secret-ish value (useless without
|
|
635
|
+
* the matching `code`); it never leaves this module. No token is ever stored
|
|
636
|
+
* here — the exchanged token lands ONLY through the encrypted credential store.
|
|
637
|
+
*
|
|
638
|
+
* @module @omnicross/daemon/admin/oauthSessions
|
|
639
|
+
*/
|
|
640
|
+
|
|
641
|
+
/** One pending OAuth session (NEVER serialized to the client). */
|
|
642
|
+
interface PendingOAuthSession {
|
|
643
|
+
readonly providerId: SubscriptionProviderId;
|
|
644
|
+
/** PKCE verifier — secret-ish; stays daemon-side, never echoed. */
|
|
645
|
+
readonly codeVerifier: string;
|
|
646
|
+
/** CSRF state minted with the auth params (validated on complete). */
|
|
647
|
+
readonly state: string;
|
|
648
|
+
/** Epoch ms the session was created (for the TTL sweep + take re-check). */
|
|
649
|
+
readonly createdAt: number;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Module-scoped store for pending OAuth sessions. A single instance is created
|
|
653
|
+
* per daemon (in `bootstrap.ts`) and wired through `AdminApiDeps`. The TTL sweep
|
|
654
|
+
* runs lazily on each `put`/`take` (no background timer to leak across tests).
|
|
655
|
+
*/
|
|
656
|
+
declare class OAuthSessionStore {
|
|
657
|
+
private readonly ttlMs;
|
|
658
|
+
private readonly sessions;
|
|
659
|
+
constructor(ttlMs?: number);
|
|
660
|
+
/**
|
|
661
|
+
* Mint a fresh opaque `sessionId`, stash the pending session, and return the
|
|
662
|
+
* id. Sweeps expired entries first so the map never grows unbounded.
|
|
663
|
+
*/
|
|
664
|
+
put(session: Omit<PendingOAuthSession, 'createdAt'>): string;
|
|
665
|
+
/**
|
|
666
|
+
* SINGLE-USE consume: return + delete the session for `sessionId`, or `null`
|
|
667
|
+
* when it is unknown, already used, or past its TTL (in which case it is
|
|
668
|
+
* dropped). A `null` return means the completer must reject (no exchange, no
|
|
669
|
+
* write).
|
|
670
|
+
*/
|
|
671
|
+
take(sessionId: string): PendingOAuthSession | null;
|
|
672
|
+
/** Drop every session past its TTL. Called on each put/take. */
|
|
673
|
+
private sweep;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* accountsOAuth — the daemon admin API's INTERACTIVE OAuth login path
|
|
678
|
+
* (`POST /admin/api/accounts/:providerId/oauth/{start,complete}`, app-parity
|
|
679
|
+
* child 4, design D1/D2/D4).
|
|
680
|
+
*
|
|
681
|
+
* EXPOSES the existing `@omnicross/subscriptions/oauth` flow over admin HTTP as a
|
|
682
|
+
* two-phase pair — it does NOT rebuild PKCE / token-exchange. `start` builds the
|
|
683
|
+
* provider's authorize params and stashes the per-session `{ codeVerifier, state }`
|
|
684
|
+
* in the `OAuthSessionStore` keyed by a minted opaque `sessionId`, returning ONLY
|
|
685
|
+
* `{ authUrl, sessionId }` (the `authUrl` carries client_id + PKCE challenge +
|
|
686
|
+
* state — all public). `complete` does a SINGLE-USE `take(sessionId)`, validates
|
|
687
|
+
* state (claude's `code#state`), `exchangeCodeForTokens(...)`, persists the minted
|
|
688
|
+
* token through the encrypted credential store (`appendProviderAccount`) + marks
|
|
689
|
+
* it active, and responds ONLY the sanitized `SubscriptionListEntry`.
|
|
690
|
+
*
|
|
691
|
+
* SECRET SPINE (the load-bearing invariant): the minted access/refresh token
|
|
692
|
+
* NEVER appears in any response body or log; the `codeVerifier` / session map is
|
|
693
|
+
* never serialized; error messages reference the session/provider, never a token.
|
|
694
|
+
* At-rest encryption is inherited (the store's `SecretBox` → `enc:` envelope).
|
|
695
|
+
*
|
|
696
|
+
* OAuth-capable here = `claude` / `gemini` (code-paste). `codex` (loopback) is
|
|
697
|
+
* DEFERRED (OQ1) and `opencodego` is manual-only — both are rejected by `start`
|
|
698
|
+
* as oauth-unsupported (defensive; their app Sign-in stays `<Unbacked>`).
|
|
699
|
+
*
|
|
700
|
+
* @module @omnicross/daemon/admin/accountsOAuth
|
|
701
|
+
*/
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* NARROW append handle (design D2-a): the OAuth complete handler legitimately
|
|
705
|
+
* needs `appendProviderAccount` (multi-account append + activate) — a method NOT
|
|
706
|
+
* on the least-authority `SubscriptionTokenWriter`. Rather than widen that
|
|
707
|
+
* deliberate security boundary, `AdminApiDeps` carries this minimal interface
|
|
708
|
+
* (NOT the full read-capable store, so no token-returning read is reachable).
|
|
709
|
+
* Structurally satisfied by the concrete `JsonSubscriptionCredentialStore`.
|
|
710
|
+
*/
|
|
711
|
+
interface SubscriptionAccountAppender {
|
|
712
|
+
appendProviderAccount(providerId: SubscriptionProviderId, config: SubscriptionTokenBlock, label?: string): Promise<{
|
|
713
|
+
id: string;
|
|
714
|
+
}>;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* accountsCodexOAuth — the daemon admin API's CODEX interactive OAuth path
|
|
719
|
+
* (`POST /accounts/codex/oauth/start` + `GET /accounts/codex/oauth/:sessionId/status`,
|
|
720
|
+
* app-parity-2 child 5).
|
|
721
|
+
*
|
|
722
|
+
* Codex differs from claude/gemini (which are CODE-PASTE, handled by
|
|
723
|
+
* `accountsOAuth.ts`): its redirect is a LOOPBACK to `http://localhost:1455/auth/callback`,
|
|
724
|
+
* so there is no code to paste — the browser hits the daemon's loopback listener
|
|
725
|
+
* directly. This makes the flow ASYNC + POLLED rather than two-phase paste:
|
|
726
|
+
* - `start` builds the codex authorize params, ARMS the one-shot loopback
|
|
727
|
+
* listener (`awaitLoopbackCode`, injected for tests), kicks the
|
|
728
|
+
* capture→exchange→persist off ASYNC (fire-and-forget), and returns ONLY
|
|
729
|
+
* `{ authUrl, sessionId }` (public — client_id + PKCE challenge + state).
|
|
730
|
+
* - the app opens `authUrl`; the browser redirects to the loopback; the daemon
|
|
731
|
+
* captures the `code`, validates `state`, `exchangeCodeForTokens`, and persists
|
|
732
|
+
* the minted token through the encrypted credential store (`appendProviderAccount`).
|
|
733
|
+
* - the app POLLS `status` until `done` / `error`, then refreshes `/accounts`.
|
|
734
|
+
*
|
|
735
|
+
* SECRET SPINE (the load-bearing invariant): the minted access/refresh/id token
|
|
736
|
+
* NEVER crosses to the client — it lands ONLY in the encrypted store. The poll
|
|
737
|
+
* `status` body is TOKEN-FREE (`{ state, message? }`); error messages reference the
|
|
738
|
+
* loopback/exchange failure, never a token. The PKCE `codeVerifier` stays in this
|
|
739
|
+
* module's closure (never serialized). Port 1455 is a single resource → only ONE
|
|
740
|
+
* codex sign-in may be in flight at a time (a second `start` → 409).
|
|
741
|
+
*
|
|
742
|
+
* REUSES the existing `@omnicross/subscriptions` codex flow + the CLI's
|
|
743
|
+
* `awaitLoopbackCode` listener — it does NOT rebuild PKCE / token-exchange / the
|
|
744
|
+
* loopback server.
|
|
745
|
+
*
|
|
746
|
+
* @module @omnicross/daemon/admin/accountsCodexOAuth
|
|
747
|
+
*/
|
|
748
|
+
|
|
749
|
+
/** The loopback-listener fn (injected so tests need not bind a real port). */
|
|
750
|
+
type CodexLoopbackFn = (state: string, timeoutMs?: number) => Promise<string>;
|
|
751
|
+
/** One codex sign-in flow's polled status (NEVER carries a token). */
|
|
752
|
+
interface CodexFlowState {
|
|
753
|
+
status: 'pending' | 'done' | 'error';
|
|
754
|
+
/** Loopback/exchange failure reason (NEVER a token). Present only on 'error'. */
|
|
755
|
+
error?: string;
|
|
756
|
+
createdAt: number;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* In-memory store for the async codex sign-in flows. A single instance per daemon
|
|
760
|
+
* (wired in `bootstrap.ts`). Tracks per-session status for the poll + a single
|
|
761
|
+
* `activeSessionId` (port 1455 is one resource → one in-flight login at a time).
|
|
762
|
+
* Never serialized; a daemon restart drops in-flight logins (fail-safe).
|
|
763
|
+
*/
|
|
764
|
+
declare class CodexOAuthSessionStore {
|
|
765
|
+
private readonly ttlMs;
|
|
766
|
+
private readonly sessions;
|
|
767
|
+
private activeSessionId;
|
|
768
|
+
constructor(ttlMs?: number);
|
|
769
|
+
/** Whether a codex sign-in is currently in flight (port 1455 held). */
|
|
770
|
+
isBusy(): boolean;
|
|
771
|
+
/** Mint a fresh sessionId, mark it pending + active, return the id. */
|
|
772
|
+
begin(): string;
|
|
773
|
+
/** Settle a flow (done/error) + free the active slot. */
|
|
774
|
+
settle(sessionId: string, status: 'done' | 'error', error?: string): void;
|
|
775
|
+
/** Read a flow's status (token-free), or null when unknown/expired. */
|
|
776
|
+
get(sessionId: string): CodexFlowState | null;
|
|
777
|
+
/** Drop expired flows; free the active slot if the active flow expired. */
|
|
778
|
+
private sweep;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* autoDisableStore.ts — the daemon's PROCESS-IN-MEMORY auto-disable store.
|
|
783
|
+
*
|
|
784
|
+
* A DB-backed embedder can persist 401/403 auto-disable durably so a UI can
|
|
785
|
+
* render per-key health. The daemon has no DB, and its only
|
|
786
|
+
* durable layer is `config.json` — but writing auto-disable back there in v1
|
|
787
|
+
* would (1) cause write amplification under a 401 storm and (2) collide with
|
|
788
|
+
* the at-rest encryption schema that owns the `apiKeys[]` on-disk
|
|
789
|
+
* format. So v1 records auto-disable IN MEMORY only:
|
|
790
|
+
* - `markAutoDisabled(keyId, status, at)` records `{ status, at, reason }`,
|
|
791
|
+
* - `isDisabled(keyId)` / `get(keyId)` read it back,
|
|
792
|
+
* - `loadPoolKeys` reads this store and flips a flagged key's `enabled` to
|
|
793
|
+
* `false`, so `getAvailableKeys` skips it within this process lifetime.
|
|
794
|
+
*
|
|
795
|
+
* Restart resets the store (the honest v1 boundary — see spec). Persistence
|
|
796
|
+
* (encrypted write-back) is a child-3 follow-up.
|
|
797
|
+
*
|
|
798
|
+
* @module @omnicross/daemon/pool/autoDisableStore
|
|
799
|
+
*/
|
|
800
|
+
/** One in-memory auto-disable record for a pool key. */
|
|
801
|
+
interface AutoDisableRecord {
|
|
802
|
+
/** The HTTP status that triggered the disable (401/403). */
|
|
803
|
+
status: number;
|
|
804
|
+
/** Epoch-ms when the disable was recorded. */
|
|
805
|
+
at: number;
|
|
806
|
+
/** Always `'auth_failure'` in v1 (the only auto-disable trigger). */
|
|
807
|
+
reason: 'auth_failure';
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* A process-lifetime in-memory store of auto-disabled pool keys, keyed by the
|
|
811
|
+
* pool key id. Constructed once in `buildDaemon` and injected as the pool's
|
|
812
|
+
* `disableKey` / `markAutoDisabled` sinks AND read by `loadPoolKeys`.
|
|
813
|
+
*/
|
|
814
|
+
declare class AutoDisableStore {
|
|
815
|
+
private readonly records;
|
|
816
|
+
/** Record (or overwrite) an auth-failure auto-disable for `keyId`. */
|
|
817
|
+
markAutoDisabled(keyId: string, status: number, at: number): void;
|
|
818
|
+
/** Whether `keyId` is currently auto-disabled in this process. */
|
|
819
|
+
isDisabled(keyId: string): boolean;
|
|
820
|
+
/** Read the auto-disable record for `keyId`, or `undefined` when healthy. */
|
|
821
|
+
get(keyId: string): AutoDisableRecord | undefined;
|
|
822
|
+
/** Clear all records (tests / teardown). */
|
|
823
|
+
clear(): void;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* ConfigFileProviderConfigSource — the daemon's file-backed `ProviderConfigSource`
|
|
828
|
+
* port impl: an embedder of `@omnicross/core`'s provider catalog port.
|
|
829
|
+
*
|
|
830
|
+
* The daemon's `config.json` provider rows ARE the catalog. Of the port's ten
|
|
831
|
+
* methods, FOUR are real (the ones the BYO proxy/outbound path actually hits):
|
|
832
|
+
* - `getProvider(id)` — map a `DaemonProviderConfig` row to an `LLMProvider`.
|
|
833
|
+
* - `getTransformerService()` — a single `TransformerService` seeded by
|
|
834
|
+
* `registerBuiltinTransformers` in the ctor.
|
|
835
|
+
* - `getMainTransformer(id)` — the transformer for the provider's target wire
|
|
836
|
+
* format (anthropic → AnthropicTransformer, gemini → GeminiTransformer,
|
|
837
|
+
* openai → null/identity), mirroring the host `AgentModelsManager` switch.
|
|
838
|
+
* `resolveProviderChain` unshifts this FORMAT-FIRST into the provider chain.
|
|
839
|
+
* - `resolveTransformerChain(id, model)` — the provider's CUSTOM
|
|
840
|
+
* `transformer.use[]` chain (app-parity-2 child 2: ENFORCED). The format
|
|
841
|
+
* transformer is NOT included here (getMainTransformer supplies it,
|
|
842
|
+
* format-first); no `transformer.use[]` → empty chain.
|
|
843
|
+
*
|
|
844
|
+
* The remaining SIX are minimal sensible stubs (never hit on the BYO single-key
|
|
845
|
+
* path — the boot smoke test is the proof).
|
|
846
|
+
*
|
|
847
|
+
* @module @omnicross/daemon/ports/ConfigFileProviderConfigSource
|
|
848
|
+
*/
|
|
849
|
+
|
|
850
|
+
declare class ConfigFileProviderConfigSource implements ProviderConfigSource {
|
|
851
|
+
private readonly providers;
|
|
852
|
+
private readonly transformerService;
|
|
853
|
+
/**
|
|
854
|
+
* Optional reload-hook (key-pool change, design D4). A no-type-coupling
|
|
855
|
+
* callback invoked at the END of `reload(...)`. `buildDaemon` injects
|
|
856
|
+
* `() => pool.invalidateCache()` so the `ApiKeyPoolService.keyCache` is
|
|
857
|
+
* flushed after a hot-reload swaps the catalog — WITHOUT this port ever
|
|
858
|
+
* importing/depending on `ApiKeyPoolService`. Absent = no-op (single-key
|
|
859
|
+
* boots that never construct a pool stay byte-identical).
|
|
860
|
+
*/
|
|
861
|
+
private reloadHook;
|
|
862
|
+
constructor(config: DaemonConfig);
|
|
863
|
+
/**
|
|
864
|
+
* Register a callback fired after every `reload(...)`. Used by `buildDaemon`
|
|
865
|
+
* to invalidate the pool's keyCache on a hot-reload. The port stays ignorant
|
|
866
|
+
* of what the callback does (no pool type dependency).
|
|
867
|
+
*/
|
|
868
|
+
setReloadHook(fn: () => void): void;
|
|
869
|
+
/**
|
|
870
|
+
* Read the live (post-reload) provider row for `providerId`, or `undefined`.
|
|
871
|
+
* Exposed so the pool's `loadKeys` reads the SAME live catalog Map this port
|
|
872
|
+
* serves (so a hot-reload is observed on the next load after `invalidateCache`).
|
|
873
|
+
*/
|
|
874
|
+
getProviderRow(providerId: string): DaemonProviderConfig | undefined;
|
|
875
|
+
/** Await the built-in transformer registration (tests await this before dispatch). */
|
|
876
|
+
ready(): Promise<void>;
|
|
877
|
+
/**
|
|
878
|
+
* Replace the live provider catalog in place (additive — does NOT touch the
|
|
879
|
+
* ten port methods, the seeded `TransformerService`, or `ready()`). Called by
|
|
880
|
+
* the admin API after a provider POST/PUT/DELETE persists `config.json`, so the
|
|
881
|
+
* next outbound request sees the new catalog WITHOUT a daemon restart. The Map
|
|
882
|
+
* swap is synchronous; an in-flight request keeps its already-resolved
|
|
883
|
+
* provider (no locking needed for a single-operator local daemon).
|
|
884
|
+
*/
|
|
885
|
+
reload(config: DaemonConfig): void;
|
|
886
|
+
/** Clear + repopulate the private providers Map from a fresh provider list. */
|
|
887
|
+
setProviders(providers: readonly DaemonProviderConfig[]): void;
|
|
888
|
+
getProvider(id: string): Promise<LLMProvider | null>;
|
|
889
|
+
getTransformerService(): TransformerService | undefined;
|
|
890
|
+
getMainTransformer(providerId: string): Promise<Transformer | null>;
|
|
891
|
+
resolveTransformerChain(providerId: string, _model?: string): Promise<ResolvedTransformerChain>;
|
|
892
|
+
resolveRoutedModel(): Promise<null>;
|
|
893
|
+
resolveEffectiveModels(): Promise<{
|
|
894
|
+
background?: string;
|
|
895
|
+
vision?: string;
|
|
896
|
+
}>;
|
|
897
|
+
getAgentDefaultModels(): Promise<AgentDefaultModels>;
|
|
898
|
+
hasVisionCapability(): Promise<boolean>;
|
|
899
|
+
getGlobalModelParameters(): Promise<GlobalModelParameters>;
|
|
900
|
+
getDiscoveredModelMaxTokens(): Promise<number | undefined>;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* JsonApiServerSettingsStore — the daemon's file-backed `ApiServerSettingsStore`
|
|
905
|
+
* port impl.
|
|
906
|
+
*
|
|
907
|
+
* The serving core persists the outbound-API server config (`{ enabled,
|
|
908
|
+
* networkBinding, endpoints, port }`) under a SINGLE settings key
|
|
909
|
+
* (`OUTBOUND_API_SERVER_CONFIG_KEY === 'outboundApiServer.config'`). Here that
|
|
910
|
+
* store is the daemon's `config.json` `server`
|
|
911
|
+
* field. `loadServerConfig(store)` / `saveServerConfig(store, cfg)` (core)
|
|
912
|
+
* normalize + persist through this 2-method surface.
|
|
913
|
+
*
|
|
914
|
+
* Only the one outbound-API key is ever read/written — any other key is a no-op
|
|
915
|
+
* miss (returns `undefined`) so the surface stays honest about what it backs.
|
|
916
|
+
*
|
|
917
|
+
* @module @omnicross/daemon/ports/JsonApiServerSettingsStore
|
|
918
|
+
*/
|
|
919
|
+
|
|
920
|
+
declare class JsonApiServerSettingsStore implements ApiServerSettingsStore {
|
|
921
|
+
private readonly configPath;
|
|
922
|
+
constructor(configPath: string);
|
|
923
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
924
|
+
set<T = unknown>(key: string, value: T): Promise<void>;
|
|
925
|
+
/** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
|
|
926
|
+
private readFile;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* migration.ts — the export gather + import apply logic for the passphrase pack
|
|
931
|
+
* (app-parity child 6, design D2/D3/D5).
|
|
932
|
+
*
|
|
933
|
+
* EXPORT (`gatherExport`): read the FULL local state DECRYPTED in-memory — every
|
|
934
|
+
* provider row (scalars / modelConfigs / single apiKey / pool apiKeys /
|
|
935
|
+
* transformer) via `loadConfig` (the at-rest box decrypts on read) AND the
|
|
936
|
+
* subscription tokens via `credentialStore.getFullConfig()` (also decrypted) —
|
|
937
|
+
* serialize to ONE bundle JSON, and `sealPack` it under the passphrase-derived
|
|
938
|
+
* key. The caller returns ONLY the opaque pack; the decrypted bundle + the
|
|
939
|
+
* passphrase live only in local variables and are never logged.
|
|
940
|
+
*
|
|
941
|
+
* IMPORT (`applyImport`): `openPack` decrypts + authenticates (a wrong passphrase
|
|
942
|
+
* or a tampered pack fails the GCM auth-tag BEFORE any write — atomic). Every
|
|
943
|
+
* provider is re-validated through `parseProviderInput` and every token block
|
|
944
|
+
* through `validateTokenBody` (deny-by-default — a malicious blob cannot inject
|
|
945
|
+
* unknown fields or escape the allowlist). Validation collects ALL rows BEFORE
|
|
946
|
+
* applying, so a structurally invalid pack does not leave a half-applied state.
|
|
947
|
+
* Apply merges by provider id (additive default): a new id is added; a colliding
|
|
948
|
+
* id is skipped (or overwritten with `mode:'overwrite'`). Writes go through the
|
|
949
|
+
* EXISTING paths (`saveConfig` re-encrypts at-rest under the LOCAL box +
|
|
950
|
+
* `writeProviderTokens`/`appendProviderAccount`), so imported secrets land
|
|
951
|
+
* `enc:`-encrypted under the TARGET machine's key — the passphrase key is used
|
|
952
|
+
* ONLY for transport.
|
|
953
|
+
*
|
|
954
|
+
* SECRET SPINE: the export RESPONSE is the opaque pack ONLY; the import RESPONSE
|
|
955
|
+
* is status-only counts. No decrypted secret + no passphrase ever reaches a
|
|
956
|
+
* response body or a log here.
|
|
957
|
+
*
|
|
958
|
+
* @module @omnicross/daemon/migration/migration
|
|
959
|
+
*/
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* The credential-store surface the migration paths need: the full DECRYPTED read
|
|
963
|
+
* (export) + the multi-account append (import re-encrypts at-rest). One shape so
|
|
964
|
+
* `ExportDeps` + `ImportDeps` can be unified into `MigrationDeps` without a
|
|
965
|
+
* `credentialStore` type conflict.
|
|
966
|
+
*/
|
|
967
|
+
interface MigrationCredentialStore extends SubscriptionAccountAppender {
|
|
968
|
+
getFullConfig(): Promise<AccountTokensConfig>;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* adminApi — the daemon admin dashboard's management API router (`/admin/api/*`,
|
|
973
|
+
* JSON) (RT3, design D3/D4/D5).
|
|
974
|
+
*
|
|
975
|
+
* A small method+path router over the LIVE daemon handles + exported core fns.
|
|
976
|
+
* Resources: providers (CRUD + hot-reload), keys (CRUD + one-time plaintext),
|
|
977
|
+
* server config (live apply), accounts (read-only status), status, playground
|
|
978
|
+
* (same-origin proxy to `/v1/*`).
|
|
979
|
+
*
|
|
980
|
+
* SECRET SPINE (design D4 — the load-bearing invariant): secrets flow IN
|
|
981
|
+
* (POST/PUT) and NEVER OUT (GET). The provider mask (`maskProviderApiKey`) and
|
|
982
|
+
* the key DTO map (`toKeyInfo`) are the single places the masking is applied so
|
|
983
|
+
* the invariant lives in one spot.
|
|
984
|
+
*
|
|
985
|
+
* @module @omnicross/daemon/admin/adminApi
|
|
986
|
+
*/
|
|
987
|
+
|
|
988
|
+
/** Token-free subscription account list entry (passthrough from core's service). */
|
|
989
|
+
interface AdminAccountsLister {
|
|
990
|
+
listAll(): Promise<unknown[]>;
|
|
991
|
+
}
|
|
992
|
+
/** One key's live cooldown health (mirrors core `KeyHealthEntry`; read-only). */
|
|
993
|
+
interface PoolKeyHealth {
|
|
994
|
+
until: number;
|
|
995
|
+
errors: number;
|
|
996
|
+
lastStatus: number | null;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* The READ-ONLY pool-health surface the admin view needs (key-pool design D7).
|
|
1000
|
+
* Structurally satisfied by core's `ApiKeyPoolService.getKeyHealth`; typed as a
|
|
1001
|
+
* minimal reader so `adminApi` carries no class coupling and can never reach a
|
|
1002
|
+
* key value through it (cooldown health only).
|
|
1003
|
+
*/
|
|
1004
|
+
interface PoolHealthReader {
|
|
1005
|
+
getKeyHealth(providerId: string): Promise<Record<string, PoolKeyHealth>>;
|
|
1006
|
+
}
|
|
1007
|
+
/** The live daemon handles the management API operates over. */
|
|
1008
|
+
interface AdminApiDeps {
|
|
1009
|
+
/** Path to the daemon's `config.json` (provider catalog + `server` field). */
|
|
1010
|
+
readonly configPath: string;
|
|
1011
|
+
/** Live provider catalog (hot-reload target). */
|
|
1012
|
+
readonly llmConfig: ConfigFileProviderConfigSource;
|
|
1013
|
+
/** Named outbound-key store. */
|
|
1014
|
+
readonly keyDb: OutboundKeyDb;
|
|
1015
|
+
/** Outbound server settings store (server config persistence). */
|
|
1016
|
+
readonly settingsStore: JsonApiServerSettingsStore;
|
|
1017
|
+
/** The running outbound server (status + live applyConfig). */
|
|
1018
|
+
readonly outboundApiServer: OutboundApiServer;
|
|
1019
|
+
/** Subscription accounts (token-free `listAll`). */
|
|
1020
|
+
readonly subscriptionAccounts: AdminAccountsLister;
|
|
1021
|
+
/**
|
|
1022
|
+
* Least-authority subscription-token WRITER (design D4) — ONLY the mutation
|
|
1023
|
+
* methods (`writeProviderTokens` / `clearProvider`), never a token-returning
|
|
1024
|
+
* read. The token-free `subscriptionAccounts` lister stays separate so a GET
|
|
1025
|
+
* handler can never reach a token through this dep.
|
|
1026
|
+
*/
|
|
1027
|
+
readonly subscriptionTokenWriter: SubscriptionTokenWriter;
|
|
1028
|
+
/**
|
|
1029
|
+
* Read-only pool-health reader (key-pool design D7) — cooldown health only,
|
|
1030
|
+
* never a key value. Drives `GET /admin/api/providers/:id/keys`.
|
|
1031
|
+
*/
|
|
1032
|
+
readonly apiKeyPool: PoolHealthReader;
|
|
1033
|
+
/** In-memory auto-disable store (design D5) — read-only for the health view. */
|
|
1034
|
+
readonly autoDisableStore: AutoDisableStore;
|
|
1035
|
+
/**
|
|
1036
|
+
* Pending interactive-OAuth sessions (app-parity child 4, design D1) — the
|
|
1037
|
+
* in-memory `{ codeVerifier, state }` map keyed by a minted `sessionId`,
|
|
1038
|
+
* NEVER serialized to the client.
|
|
1039
|
+
*/
|
|
1040
|
+
readonly oauthSessions: OAuthSessionStore;
|
|
1041
|
+
/**
|
|
1042
|
+
* Injected token-exchange `FetchLike` (oauth design D2-a) — defaults to global
|
|
1043
|
+
* `fetch` in `bootstrap.ts`; tests inject a mock so no real token endpoint is
|
|
1044
|
+
* hit. Mirrors how `login.ts` injects its exchange fetch.
|
|
1045
|
+
*/
|
|
1046
|
+
readonly oauthExchangeFetch: FetchLike;
|
|
1047
|
+
/**
|
|
1048
|
+
* NARROW append handle (oauth design D2-a) — the OAuth complete handler needs
|
|
1049
|
+
* `appendProviderAccount` (NOT on the least-authority `SubscriptionTokenWriter`).
|
|
1050
|
+
* A minimal interface, NOT the full read-capable store, so no token-returning
|
|
1051
|
+
* read is reachable. Wired from the concrete `credentialStore` in `bootstrap.ts`.
|
|
1052
|
+
*/
|
|
1053
|
+
readonly subscriptionAccountAppender: SubscriptionAccountAppender;
|
|
1054
|
+
/**
|
|
1055
|
+
* Codex interactive-OAuth flow store (app-parity-2 child 5). Tracks the async
|
|
1056
|
+
* loopback sign-in's polled status (token-free); only ONE codex login may be in
|
|
1057
|
+
* flight (port 1455 is one resource). Wired in `bootstrap.ts`.
|
|
1058
|
+
*/
|
|
1059
|
+
readonly codexSessions: CodexOAuthSessionStore;
|
|
1060
|
+
/**
|
|
1061
|
+
* Codex loopback listener (app-parity-2 child 5) — defaults to `awaitLoopbackCode`
|
|
1062
|
+
* (binds 127.0.0.1:1455) in `bootstrap.ts`; tests inject a mock so no real port
|
|
1063
|
+
* is bound. The captured code crosses to the daemon ONLY (never the client).
|
|
1064
|
+
*/
|
|
1065
|
+
readonly codexAwaitLoopback: CodexLoopbackFn;
|
|
1066
|
+
/**
|
|
1067
|
+
* Migration credential-store handle (app-parity child 6, design D2/D3). The
|
|
1068
|
+
* export gather needs `getFullConfig()` (full DECRYPTED tokens, in-memory only —
|
|
1069
|
+
* the pack is the only thing that leaves) and import needs `appendProviderAccount`
|
|
1070
|
+
* (multi-account append + re-encrypt at-rest). Confined to the migration
|
|
1071
|
+
* handlers (which seal/validate everything); never reached by a GET handler.
|
|
1072
|
+
* Wired from the concrete `credentialStore` in `bootstrap.ts`.
|
|
1073
|
+
*/
|
|
1074
|
+
readonly migrationCredentialStore: MigrationCredentialStore;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Dispatch one `/admin/api/*` request. `path` is the already-extracted pathname
|
|
1078
|
+
* (no query). The auth gate has already run in `AdminServer`.
|
|
1079
|
+
*/
|
|
1080
|
+
declare function handleAdminApi(req: http.IncomingMessage, res: http.ServerResponse, path: string, deps: AdminApiDeps): Promise<void>;
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* AdminServer — the daemon's localhost admin/dashboard HTTP listener (RT3,
|
|
1084
|
+
* design D1/D2).
|
|
1085
|
+
*
|
|
1086
|
+
* A SEPARATE `node:http` listener distinct from core's outbound `/v1/*` server
|
|
1087
|
+
* (port 8765, untouched). Mirrors `OutboundApiServer`'s proven shape:
|
|
1088
|
+
* - `http.createServer`, default bind `127.0.0.1` (or `0.0.0.0` when
|
|
1089
|
+
* `networkBinding`), `listen` with `EADDRINUSE`→ephemeral(port 0) fallback,
|
|
1090
|
+
* - `getStatus()` (running / bound port / dashboard URL), `start` / `stop`.
|
|
1091
|
+
* Default admin port 8766.
|
|
1092
|
+
*
|
|
1093
|
+
* Auth (design D2):
|
|
1094
|
+
* - Baseline = localhost bind, no token → reachable only from the machine.
|
|
1095
|
+
* - Optional `admin.token` → every `/admin/*` request (incl. `GET /`) must
|
|
1096
|
+
* carry `Authorization: Bearer <token>` or `X-Admin-Token: <token>`; compared
|
|
1097
|
+
* server-side with a constant-time equality → `401` on a miss.
|
|
1098
|
+
* - HARD SAFETY GATE: `networkBinding` (LAN/`0.0.0.0`) without a non-empty
|
|
1099
|
+
* `admin.token` → `start` REFUSES to bind (logs + stays down, fail closed).
|
|
1100
|
+
*
|
|
1101
|
+
* Routing: `GET /` (and `GET /admin`) → `DASHBOARD_HTML`; `* /admin/api/*` → the
|
|
1102
|
+
* management API (`handleAdminApi`); everything else → `404`.
|
|
1103
|
+
*
|
|
1104
|
+
* @module @omnicross/daemon/admin/AdminServer
|
|
1105
|
+
*/
|
|
1106
|
+
|
|
1107
|
+
/** The dependencies the admin server + its API need (live daemon handles). */
|
|
1108
|
+
interface AdminServerDeps extends AdminApiDeps {
|
|
1109
|
+
/** Read the resolved admin config (enabled/port/networkBinding/token). */
|
|
1110
|
+
getAdminConfig: () => ResolvedAdminConfig;
|
|
1111
|
+
}
|
|
1112
|
+
/** A live status snapshot for the admin listener. */
|
|
1113
|
+
interface AdminServerStatus {
|
|
1114
|
+
running: boolean;
|
|
1115
|
+
/** Actual bound port (0 when not running). */
|
|
1116
|
+
port: number;
|
|
1117
|
+
/** The dashboard URL (loopback or LAN base), or null when not running. */
|
|
1118
|
+
url: string | null;
|
|
1119
|
+
}
|
|
1120
|
+
declare class AdminServer {
|
|
1121
|
+
private readonly deps;
|
|
1122
|
+
private server;
|
|
1123
|
+
private boundPort;
|
|
1124
|
+
private boundAddr;
|
|
1125
|
+
constructor(deps: AdminServerDeps);
|
|
1126
|
+
/**
|
|
1127
|
+
* Start the admin listener honoring the resolved admin config. Returns the
|
|
1128
|
+
* actual bound port, or `0` when it refuses/declines to bind (disabled or the
|
|
1129
|
+
* LAN fail-closed gate). Idempotent: a second call returns the bound port.
|
|
1130
|
+
*/
|
|
1131
|
+
start(): Promise<number>;
|
|
1132
|
+
/** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
|
|
1133
|
+
private listen;
|
|
1134
|
+
/** Per-request handler: auth gate (when a token is set) → routing. */
|
|
1135
|
+
private onRequest;
|
|
1136
|
+
private dispatch;
|
|
1137
|
+
/** Constant-time bearer/header check against the configured token. */
|
|
1138
|
+
private isAuthorized;
|
|
1139
|
+
/** Stop the listener and release the port. */
|
|
1140
|
+
stop(): Promise<void>;
|
|
1141
|
+
/** A live status snapshot. */
|
|
1142
|
+
getStatus(): AdminServerStatus;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* ConsoleLogger — the daemon's file-less default `Logger` port impl (design D5).
|
|
1147
|
+
*
|
|
1148
|
+
* A thin `console.*` wrapper. The serving core depends on the `Logger` port
|
|
1149
|
+
* (never a host class), so this trivial implementation is the only logger the
|
|
1150
|
+
* standalone daemon needs. `error` uses the WIDEST `(message, error?, meta?)`
|
|
1151
|
+
* signature so every core call site stays assignable.
|
|
1152
|
+
*
|
|
1153
|
+
* @module @omnicross/daemon/ports/ConsoleLogger
|
|
1154
|
+
*/
|
|
1155
|
+
|
|
1156
|
+
declare class ConsoleLogger implements Logger {
|
|
1157
|
+
info(message: string, meta?: Record<string, unknown> | Error | object): void;
|
|
1158
|
+
warn(message: string, meta?: Record<string, unknown> | Error | object): void;
|
|
1159
|
+
error(message: string, error?: unknown, meta?: Record<string, unknown> | object): void;
|
|
1160
|
+
debug(message: string, meta?: Record<string, unknown> | Error | object): void;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* JsonOutboundKeyDb — the daemon's file-backed `OutboundKeyDb` port impl
|
|
1165
|
+
* (design D3).
|
|
1166
|
+
*
|
|
1167
|
+
* Durable storage for named outbound API keys, backed by a json file (a sibling
|
|
1168
|
+
* of `config.json`, e.g. `keys.json`) holding an `OutboundKeyDbRow[]`. This port
|
|
1169
|
+
* provides ONLY storage — it never generates secrets nor hashes. Core's
|
|
1170
|
+
* `createNamedKey(db, name)` calls `outboundApiKeysCreate` with the sha256
|
|
1171
|
+
* `keyHash` + display `keyPrefix` and returns the one-time plaintext; the hot
|
|
1172
|
+
* auth path uses core's `hashKey(presented)` + `outboundApiKeysGetByHash`.
|
|
1173
|
+
*
|
|
1174
|
+
* @module @omnicross/daemon/ports/JsonOutboundKeyDb
|
|
1175
|
+
*/
|
|
1176
|
+
|
|
1177
|
+
declare class JsonOutboundKeyDb implements OutboundKeyDb$1 {
|
|
1178
|
+
private readonly keysPath;
|
|
1179
|
+
constructor(keysPath: string);
|
|
1180
|
+
outboundApiKeysList(): Promise<OutboundKeyDbRow[]>;
|
|
1181
|
+
outboundApiKeysGetByHash(hash: string): Promise<OutboundKeyDbRow | null>;
|
|
1182
|
+
outboundApiKeysCreate(input: {
|
|
1183
|
+
id: string;
|
|
1184
|
+
name: string;
|
|
1185
|
+
keyHash: string;
|
|
1186
|
+
keyPrefix: string;
|
|
1187
|
+
createdAt?: number;
|
|
1188
|
+
}): Promise<OutboundKeyDbRow>;
|
|
1189
|
+
outboundApiKeysRevoke(id: string): Promise<boolean>;
|
|
1190
|
+
outboundApiKeysTouchLastUsed(id: string): Promise<boolean>;
|
|
1191
|
+
outboundApiKeysSetEnabled(id: string, enabled: boolean): Promise<boolean>;
|
|
1192
|
+
/** Apply `fn` to the row with `id`, persisting when it returns true. */
|
|
1193
|
+
private mutateRow;
|
|
1194
|
+
/** Read the key rows, tolerating a missing/corrupt file (→ empty list). */
|
|
1195
|
+
private readRows;
|
|
1196
|
+
private writeRows;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* bootstrap.ts — `buildDaemon` wires `@omnicross/core`'s `ProviderProxy` +
|
|
1201
|
+
* `OutboundApiServer` STANDALONE (design D6).
|
|
1202
|
+
*
|
|
1203
|
+
* A DB-backed embedder wires the same `@omnicross/core` surface differently;
|
|
1204
|
+
* this standalone wiring makes three SUBSTITUTIONS (file-backed ports replace
|
|
1205
|
+
* DB-backed ones) and three SUBTRACTIONS:
|
|
1206
|
+
* - no `CompletionService` (the BYO proxy path doesn't need it),
|
|
1207
|
+
* - no `apiKeyPool` / `usageRecorder` (optional `ProviderProxyDeps`),
|
|
1208
|
+
* - no `anthropicIngressHandlerFactory` (→ `/v1/messages` returns 502 by core's
|
|
1209
|
+
* existing contract — no daemon code needed).
|
|
1210
|
+
*
|
|
1211
|
+
* `getProviderProxy` / `getOutboundApiServer` are module singletons, so the boot
|
|
1212
|
+
* smoke test calls `__resetProviderProxyForTests` / `__resetOutboundApiServerForTests`
|
|
1213
|
+
* (re-exported here) in `beforeEach`.
|
|
1214
|
+
*
|
|
1215
|
+
* @module @omnicross/daemon/bootstrap
|
|
1216
|
+
*/
|
|
1217
|
+
|
|
1218
|
+
/** On-disk locations the file-backed ports persist to. */
|
|
1219
|
+
interface DaemonPaths {
|
|
1220
|
+
/** The config.json path (provider catalog + persisted `server` field). */
|
|
1221
|
+
configPath: string;
|
|
1222
|
+
/** The named-key json store path (sibling of config.json by convention). */
|
|
1223
|
+
keysPath: string;
|
|
1224
|
+
/** The subscription `tokens.json` store path (sibling of config.json by convention). */
|
|
1225
|
+
tokensPath: string;
|
|
1226
|
+
/**
|
|
1227
|
+
* OPTIONAL `--master-key-file` override for the at-rest master key (secrets
|
|
1228
|
+
* design D3). Absent → the default `~/.omnicross/master.key`. The
|
|
1229
|
+
* `OMNICROSS_MASTER_KEY` env still beats this when set.
|
|
1230
|
+
*/
|
|
1231
|
+
masterKeyFilePath?: string;
|
|
1232
|
+
/**
|
|
1233
|
+
* TEST SEAM (optional, app-parity-2 child 5): override the codex loopback listener
|
|
1234
|
+
* so tests need not bind `127.0.0.1:1455`. Absent → the real `awaitLoopbackCode`.
|
|
1235
|
+
*/
|
|
1236
|
+
codexAwaitLoopback?: CodexLoopbackFn;
|
|
1237
|
+
/**
|
|
1238
|
+
* TEST SEAM (optional): override the OAuth token-exchange fetch so tests need not
|
|
1239
|
+
* hit a real token endpoint. Absent → the global `fetch`.
|
|
1240
|
+
*/
|
|
1241
|
+
oauthExchangeFetch?: FetchLike;
|
|
1242
|
+
}
|
|
1243
|
+
/** The constructed daemon handles the CLI commands operate on. */
|
|
1244
|
+
interface Daemon {
|
|
1245
|
+
readonly logger: ConsoleLogger;
|
|
1246
|
+
readonly llmConfig: ConfigFileProviderConfigSource;
|
|
1247
|
+
readonly keyDb: JsonOutboundKeyDb;
|
|
1248
|
+
readonly settingsStore: JsonApiServerSettingsStore;
|
|
1249
|
+
readonly providerProxy: ProviderProxy;
|
|
1250
|
+
readonly outboundApiServer: OutboundApiServer;
|
|
1251
|
+
/**
|
|
1252
|
+
* Multi-key load balancer. Wired into the proxy deps slot
|
|
1253
|
+
* AND exposed here so the admin read-only key-health view can
|
|
1254
|
+
* read `getKeyHealth`. NOTE: outbound failover does NOT fire on
|
|
1255
|
+
* the daemon's null-session outbound path — v1 is cold-standby + observable.
|
|
1256
|
+
*/
|
|
1257
|
+
readonly apiKeyPool: ApiKeyPoolService;
|
|
1258
|
+
/** In-memory 401/403 auto-disable store (design D5; read by the admin view). */
|
|
1259
|
+
readonly autoDisableStore: AutoDisableStore;
|
|
1260
|
+
/** File-backed subscription credential store (reads `tokens.json`). */
|
|
1261
|
+
readonly credentialStore: JsonSubscriptionCredentialStore;
|
|
1262
|
+
/** Subscription dispatch-profile registry (mirrored into core's outbound slot). */
|
|
1263
|
+
readonly subscriptionRegistry: SubscriptionProviderRegistry;
|
|
1264
|
+
/** Subscription account service (token-free `listAll`) — now exposed for the
|
|
1265
|
+
* admin dashboard's read-only accounts panel (RT3). */
|
|
1266
|
+
readonly subscriptionAccounts: SubscriptionAccountService;
|
|
1267
|
+
/** The localhost admin/dashboard HTTP listener (RT3). Started by `start.ts`. */
|
|
1268
|
+
readonly adminServer: AdminServer;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Construct the standalone daemon from a loaded config + on-disk paths. Does NOT
|
|
1272
|
+
* start the listeners — the `start` command awaits `providerProxy.start()` then
|
|
1273
|
+
* `outboundApiServer.applyConfig(...)`.
|
|
1274
|
+
*/
|
|
1275
|
+
declare function buildDaemon(config: DaemonConfig, paths: DaemonPaths): Daemon;
|
|
1276
|
+
/** Reset the core singletons (tests / teardown only). Re-exported for the suite.
|
|
1277
|
+
*
|
|
1278
|
+
* Also clears BOTH subscription singletons (design D4). This is mandatory: the
|
|
1279
|
+
* `setSubscriptionProviderRegistry` setter mirrors into core's outbound slot, so
|
|
1280
|
+
* without it a prior test's registry would leak into a BYO-only boot and
|
|
1281
|
+
* mis-route. We clear the core outbound slot directly via
|
|
1282
|
+
* `setSubscriptionRegistryForOutbound(null)` (which accepts `null`), and null
|
|
1283
|
+
* the `@omnicross/subscriptions` module singletons through their setters (the
|
|
1284
|
+
* setters assign verbatim — passing `null` is a no-throw runtime clear; the
|
|
1285
|
+
* `as never` keeps the call within the package's non-nullable type without
|
|
1286
|
+
* modifying its behavior). It also nulls the core Gemini Code-Assist resolver
|
|
1287
|
+
* slot so a wired resolver does not leak across boots.
|
|
1288
|
+
*
|
|
1289
|
+
* NOTE: the `AdminServer` is INSTANCE-scoped on the returned `Daemon` (not a
|
|
1290
|
+
* module singleton), so it needs no reset here — the test stops it in `afterEach`
|
|
1291
|
+
* via `daemon.adminServer.stop()`. */
|
|
1292
|
+
declare function resetDaemonSingletonsForTests(): void;
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* html.ts — the embedded vanilla-JS admin dashboard (RT3, design D7).
|
|
1296
|
+
*
|
|
1297
|
+
* A SINGLE `text/html` template literal served by `AdminServer` on `GET /`. No
|
|
1298
|
+
* framework, no bundler, no new dependency — vanilla `fetch` + DOM. Panels:
|
|
1299
|
+
* providers (table + add/edit form), keys (table + create modal showing the
|
|
1300
|
+
* one-time plaintext with a copy button + a "shown once" warning, never
|
|
1301
|
+
* persisted), server config, read-only accounts status, and a playground
|
|
1302
|
+
* (endpoint select + key + request textarea + Send + response area).
|
|
1303
|
+
*
|
|
1304
|
+
* SECURITY: the create-key plaintext is held only in a local variable inside the
|
|
1305
|
+
* modal flow and cleared on dismiss — never written to a field, list, or storage.
|
|
1306
|
+
*
|
|
1307
|
+
* @module @omnicross/daemon/admin/html
|
|
1308
|
+
*/
|
|
1309
|
+
/** The full dashboard document (style + body + the vanilla client script). */
|
|
1310
|
+
declare const DASHBOARD_HTML: string;
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* ccr-import.ts — translate a `claude-code-router` (CCR) `config.json` into an
|
|
1314
|
+
* omnicross daemon config (design D9). Pure + testable: `parseCcrConfig(raw)` +
|
|
1315
|
+
* `mapCcrToOmnicross(ccr) → { config, notes }`.
|
|
1316
|
+
*
|
|
1317
|
+
* Provider mapping: CCR `Providers[{name, api_base_url, api_key, models}]` →
|
|
1318
|
+
* omnicross provider rows. `apiFormat` is inferred heuristically (default
|
|
1319
|
+
* `openai`), with a `notes[]` entry whenever the inference is ambiguous.
|
|
1320
|
+
*
|
|
1321
|
+
* Router-role mapping (per doc 03 §4.1 / 04 §4.1, user decision 2026-06-03):
|
|
1322
|
+
* default → default
|
|
1323
|
+
* background → background
|
|
1324
|
+
* think → default (omnicross has no think slot)
|
|
1325
|
+
* longContext → default (no longContext slot; `longContextThreshold` dropped)
|
|
1326
|
+
* image → vision (CCR's `forceUseImageAgent` dropped)
|
|
1327
|
+
* webSearch → DROPPED (philosophically different; recorded as a note)
|
|
1328
|
+
*
|
|
1329
|
+
* @module @omnicross/daemon/ccr-import
|
|
1330
|
+
*/
|
|
1331
|
+
|
|
1332
|
+
/** A CCR provider entry (defensive — all fields optional). */
|
|
1333
|
+
interface CcrProvider {
|
|
1334
|
+
name?: string;
|
|
1335
|
+
api_base_url?: string;
|
|
1336
|
+
api_key?: string;
|
|
1337
|
+
models?: string[];
|
|
1338
|
+
transformer?: unknown;
|
|
1339
|
+
}
|
|
1340
|
+
/** The CCR `Router` block (a subset; unknown roles are ignored). */
|
|
1341
|
+
interface CcrRouter {
|
|
1342
|
+
default?: string;
|
|
1343
|
+
background?: string;
|
|
1344
|
+
think?: string;
|
|
1345
|
+
longContext?: string;
|
|
1346
|
+
longContextThreshold?: number;
|
|
1347
|
+
image?: string;
|
|
1348
|
+
webSearch?: string;
|
|
1349
|
+
forceUseImageAgent?: boolean;
|
|
1350
|
+
}
|
|
1351
|
+
/** A parsed CCR config. */
|
|
1352
|
+
interface CcrConfig {
|
|
1353
|
+
Providers?: CcrProvider[];
|
|
1354
|
+
Router?: CcrRouter;
|
|
1355
|
+
}
|
|
1356
|
+
/** Parse + shape-guard a raw CCR config object. */
|
|
1357
|
+
declare function parseCcrConfig(raw: unknown): CcrConfig;
|
|
1358
|
+
/**
|
|
1359
|
+
* Infer the wire format for a CCR provider from its base URL / name. Returns the
|
|
1360
|
+
* inferred format and whether it was an ambiguous (heuristic) guess.
|
|
1361
|
+
*/
|
|
1362
|
+
declare function inferApiFormat(provider: CcrProvider): {
|
|
1363
|
+
format: DaemonApiFormat;
|
|
1364
|
+
ambiguous: boolean;
|
|
1365
|
+
};
|
|
1366
|
+
/**
|
|
1367
|
+
* Translate a parsed CCR config into a `DaemonConfig` + the list of human-readable
|
|
1368
|
+
* notes describing every folded/dropped field. Pure — directly unit-tested.
|
|
1369
|
+
*/
|
|
1370
|
+
declare function mapCcrToOmnicross(ccr: CcrConfig): {
|
|
1371
|
+
config: DaemonConfig;
|
|
1372
|
+
notes: string[];
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
export { type AdminApiDeps, AdminServer, type AdminServerDeps, type AdminServerStatus, type CcrConfig, type CcrProvider, type CcrRouter, ConfigFileProviderConfigSource, ConsoleLogger, DASHBOARD_HTML, DEFAULT_ADMIN_PORT, type Daemon, type DaemonAdminConfig, type DaemonApiFormat, type DaemonConfig, type DaemonPaths, type DaemonProviderConfig, JsonApiServerSettingsStore, JsonOutboundKeyDb, JsonSubscriptionCredentialStore, type ResolvedAdminConfig, buildDaemon, handleAdminApi, inferApiFormat, loadConfig, mapCcrToOmnicross, parseCcrConfig, resetDaemonSingletonsForTests, resolveAdminConfig, saveConfig, validateConfig };
|