@gotcos/glasses-server 6.79.2 → 6.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.env.example +9 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +10 -0
  4. package/managed-runtime-contract.json +1 -0
  5. package/package.json +10 -1
  6. package/server/index.ts +14 -1
  7. package/server/lib/agent-session-search.ts +8 -5
  8. package/server/lib/agent-session-store.ts +45 -4
  9. package/server/lib/archive.ts +2 -1
  10. package/server/lib/attached-provider-adapter.ts +11 -0
  11. package/server/lib/claude-bridge.ts +112 -8
  12. package/server/lib/claude-settings-guard.ts +94 -0
  13. package/server/lib/claude-steer.ts +27 -0
  14. package/server/lib/codex-bridge.ts +205 -32
  15. package/server/lib/codex-engine-sessions.ts +27 -6
  16. package/server/lib/codex-homes.ts +134 -0
  17. package/server/lib/codex-live-queue.ts +14 -0
  18. package/server/lib/codex-profile-home.ts +452 -0
  19. package/server/lib/codex-queue-control.ts +17 -1
  20. package/server/lib/codex-subagent-activity.ts +22 -0
  21. package/server/lib/conversation.ts +92 -15
  22. package/server/lib/direct-bridge.ts +853 -0
  23. package/server/lib/direct-sse.ts +262 -0
  24. package/server/lib/fork-thread.ts +11 -0
  25. package/server/lib/key-answer-fields.ts +49 -0
  26. package/server/lib/key-catalog.ts +301 -0
  27. package/server/lib/key-lanes-pricing.ts +12 -0
  28. package/server/lib/key-lanes-wiring.ts +346 -0
  29. package/server/lib/key-lanes.ts +145 -0
  30. package/server/lib/key-ledger.ts +123 -0
  31. package/server/lib/key-notices.ts +486 -0
  32. package/server/lib/key-profile-rejects.ts +25 -0
  33. package/server/lib/key-profile-routing.ts +173 -0
  34. package/server/lib/key-profiles.ts +182 -0
  35. package/server/lib/key-relay-contract.ts +262 -0
  36. package/server/lib/key-relay-errors.ts +211 -0
  37. package/server/lib/key-relay-meter.ts +303 -0
  38. package/server/lib/key-relay.ts +1018 -0
  39. package/server/lib/key-secrets-guard.ts +52 -0
  40. package/server/lib/key-store.ts +228 -0
  41. package/server/lib/keyed-sessions.ts +154 -0
  42. package/server/lib/lane-b-env.ts +169 -0
  43. package/server/lib/meeting-autonomous-finalize.ts +267 -0
  44. package/server/lib/meeting-stop-intents.ts +277 -0
  45. package/server/lib/model-router.ts +96 -7
  46. package/server/lib/morning-brief-config.ts +3 -0
  47. package/server/lib/native-head.ts +13 -1
  48. package/server/lib/occupancy-probes.ts +4 -0
  49. package/server/lib/ollama-run-ledger.ts +96 -13
  50. package/server/lib/prompt-edit.ts +2 -0
  51. package/server/lib/query-job-coordinator.ts +2 -1
  52. package/server/lib/query-job-runtime.ts +67 -15
  53. package/server/lib/query-job-store.ts +7 -1
  54. package/server/lib/query-job-types.ts +15 -1
  55. package/server/lib/session-hook-events.ts +28 -0
  56. package/server/lib/session-hook-ledger.ts +10 -0
  57. package/server/lib/thread-occupancy.ts +22 -1
  58. package/server/lib/token-audit.ts +18 -4
  59. package/server/lib/work-handoff-requests.ts +3 -0
  60. package/server/routes/agent-session-bindings.ts +15 -0
  61. package/server/routes/agent-sessions.ts +17 -5
  62. package/server/routes/handoffs.ts +22 -1
  63. package/server/routes/health.ts +5 -0
  64. package/server/routes/key-lanes.ts +442 -0
  65. package/server/routes/meeting.ts +326 -47
  66. package/server/routes/openai-compat.ts +28 -3
  67. package/server/routes/query.ts +20 -6
  68. package/server/routes/transcribe-stream.ts +48 -1
  69. package/shared/key-preference.ts +57 -0
  70. package/shared/model-preference.ts +20 -2
@@ -0,0 +1,173 @@
1
+ // Key lanes K1, package P6 (plan v4 sections 5.2 and 9.4): how a model preference that names a
2
+ // key profile is routed, refused or ignored. One module so every Accept site resolves a profile
3
+ // the same way and every refusal reads the same on the lens.
4
+ //
5
+ // resolveKeyProfileTurn `profile:<id>` to a runnable turn, or a lens line saying why not.
6
+ // Never a silent subscription turn: an unknown, disabled or keyless
7
+ // profile, or key lanes switched off, is a refusal.
8
+ // registerKeyProfileEngine The seam P3 (claude), P4 (codex) and P5 (direct) register their
9
+ // keyed runner into. With no runner for an engine, the turn is refused.
10
+ // profileSteerProvider What a profile-bound job looks like to the steer gate (claude-steer
11
+ // returns `profile_session`; this decides which engine that means).
12
+ // v1KeyProfileFor `/v1` `cos-key-<id>` to a profile offered there, or a refusal.
13
+ //
14
+ // HARD RULE: nothing here runs for a preference that is not a well formed `profile:<id>` or
15
+ // `cos-key-<id>`. Every caller checks that syntax first (a pure string test, no file read), so a
16
+ // server with no profile takes exactly the 6.79.0 path. Importing this module reads no file,
17
+ // opens no socket and starts no timer. It imports only types from conversation.ts, so a test that
18
+ // loads it does not load the session store.
19
+ import type { CallOptions, StreamCallbacks } from './claude-bridge.js'
20
+ import type { ModelImageInput } from './model-image-input.js'
21
+ import type { KeyKind, KeyProfile, KeyProfileEngine } from './key-relay-contract.js'
22
+ import type { QueryProvider } from './claude-steer.js'
23
+ import { keyLanesActive } from './key-lanes.js'
24
+ import { KEY_LANE_REFUSAL_COPY } from './key-notices.js'
25
+ import { getProfile, listProfiles } from './key-profiles.js'
26
+ import { getPublicKey } from './key-store.js'
27
+ import type { KeyProfileBinding, PromptReference } from './conversation.js'
28
+ import { normalizeModelPreference } from '../../shared/model-preference.js'
29
+ import { isProfilePreference, profilePreference } from '../../shared/key-preference.js'
30
+
31
+ /** A profile that may run now: lanes active, the profile enabled, its key saved. */
32
+ export interface KeyProfileTurn {
33
+ binding: KeyProfileBinding
34
+ profile: KeyProfile
35
+ keyKind: KeyKind
36
+ }
37
+
38
+ export type KeyProfileRefusalCode = 'key_profile_off' | 'key_profile_unavailable'
39
+
40
+ export type KeyProfileResolution =
41
+ | { ok: true; turn: KeyProfileTurn }
42
+ | { ok: false; code: KeyProfileRefusalCode; message: string }
43
+
44
+ // ── Lens copy (plan 11.3 "Profile off", 5.2 rejects). No em dashes, no arrows, one line each. ──
45
+
46
+ /** Plan 5.2 and 11.3: the one refusal for a profile that cannot run (missing, disabled, no key, lanes off). */
47
+ export function keyProfileOffLine(label: string): string {
48
+ return KEY_LANE_REFUSAL_COPY.profileOff(label)
49
+ }
50
+ /** No runner is registered for the profile's engine on this build. */
51
+ export function keyProfileUnavailableLine(label: string): string {
52
+ return KEY_LANE_REFUSAL_COPY.profileUnavailable(label)
53
+ }
54
+ /** Plan 9.4: a `cos-key-<id>` that is not an active profile with "Offer on /v1" on. */
55
+ export const KEY_PROFILE_V1_NOT_OFFERED = KEY_LANE_REFUSAL_COPY.v1NotOffered
56
+
57
+ function labelFor(id: string, profile: KeyProfile | undefined): string {
58
+ return profile?.label?.trim() || id
59
+ }
60
+
61
+ /**
62
+ * Resolve `profile:<id>`. Lanes off, an unknown id, a disabled profile or a profile whose key
63
+ * was removed all refuse with the same line (the user's fix is the same: pick a model).
64
+ */
65
+ export function resolveKeyProfileTurn(id: string): KeyProfileResolution {
66
+ // Read only for its label when lanes are off: a stale binding still names the profile.
67
+ const profile = getProfile(id)
68
+ const off = (): KeyProfileResolution => ({ ok: false, code: 'key_profile_off', message: keyProfileOffLine(labelFor(id, profile)) })
69
+ if (!keyLanesActive()) return off()
70
+ if (!profile || !profile.enabled) return off()
71
+ const key = getPublicKey(profile.keyRef)
72
+ if (!key) return off()
73
+ return { ok: true, turn: { binding: profilePreference(profile.id) as KeyProfileBinding, profile, keyKind: key.kind } }
74
+ }
75
+
76
+ // ── Engine seam (P3, P4, P5 register) ────────────────────────────────────────
77
+
78
+ /**
79
+ * A keyed runner. Same arguments as the subscription bridges, plus the resolved turn. Contract:
80
+ * - call exactly one of callbacks.onDone / callbacks.onError (model-router's session run lock
81
+ * releases on that terminal callback);
82
+ * - stamp conversation exchanges with `turn.binding` (conversation accepts it by syntax);
83
+ * - the `model` it passes to onStart / onDone is ignored for linkage and display: callers read
84
+ * the binding from the `onKeyProfileTurn` callback below instead;
85
+ * - honor `options.steer` when present (only a `claude` engine profile is ever offered one).
86
+ */
87
+ export type KeyProfileEngineRunner = (
88
+ query: string,
89
+ sessionId: string,
90
+ callbacks: StreamCallbacks,
91
+ turn: KeyProfileTurn,
92
+ images?: ModelImageInput[],
93
+ reference?: PromptReference,
94
+ globalMsgNum?: number,
95
+ options?: CallOptions,
96
+ ) => Promise<string>
97
+
98
+ const engines = new Map<KeyProfileEngine, KeyProfileEngineRunner>()
99
+
100
+ /** Named by engine, so a re-registration replaces rather than stacks. */
101
+ export function registerKeyProfileEngine(engine: KeyProfileEngine, runner: KeyProfileEngineRunner): void {
102
+ engines.set(engine, runner)
103
+ }
104
+
105
+ export function keyProfileEngineRunner(engine: KeyProfileEngine): KeyProfileEngineRunner | undefined {
106
+ return engines.get(engine)
107
+ }
108
+
109
+ /** Tests only. */
110
+ export function __resetKeyProfileEnginesForTests(): void {
111
+ engines.clear()
112
+ }
113
+
114
+ /**
115
+ * Optional callback a caller of callModelStreaming may add. model-router calls it once, before
116
+ * the keyed runner starts, and never for a subscription turn. The durable runtime and the SSE
117
+ * route use it to report the binding (not the runner's tier) as the turn's model.
118
+ */
119
+ export interface KeyProfileTurnCallbacks {
120
+ onKeyProfileTurn?: (turn: KeyProfileTurn) => void
121
+ }
122
+
123
+ // ── Linkage and steer ────────────────────────────────────────────────────────
124
+
125
+ export type KeyProfileLinkageProvider = 'claude' | 'codex' | undefined
126
+
127
+ /** The job linkage provider for an engine. Direct (Lane C) has no CLI session; it is omitted. */
128
+ export function linkageProviderForEngine(engine: KeyProfileEngine): KeyProfileLinkageProvider {
129
+ if (engine === 'claude') return 'claude'
130
+ if (engine === 'codex') return 'codex'
131
+ return undefined
132
+ }
133
+
134
+ /**
135
+ * claude-steer's guard returned `profile_session`. Steerable (`claude`) only for an active,
136
+ * enabled Lane B Claude profile; anything else answers with a provider the coordinator never
137
+ * steers, so a Codex or direct profile, or a profile that would be refused, is `unsupported`.
138
+ */
139
+ export function profileSteerProvider(id: string): QueryProvider {
140
+ const resolved = resolveKeyProfileTurn(id)
141
+ return resolved.ok && resolved.turn.profile.engine === 'claude' ? 'claude' : 'codex'
142
+ }
143
+
144
+ // ── /v1 (plan 9.4) ───────────────────────────────────────────────────────────
145
+
146
+ /** Profiles `/v1` offers: lanes active, enabled, "Offer on /v1" on, key saved. Empty when inactive. */
147
+ export function v1OfferedProfiles(): KeyProfile[] {
148
+ if (!keyLanesActive()) return []
149
+ return listProfiles().filter(p => p.enabled && p.offerOnV1 && getPublicKey(p.keyRef) !== undefined)
150
+ }
151
+
152
+ /** The offered profile for a `cos-key-<id>` id, or undefined (the route refuses it). */
153
+ export function v1OfferedProfile(id: string): KeyProfile | undefined {
154
+ return v1OfferedProfiles().find(p => p.id === id)
155
+ }
156
+
157
+ // ── COS_G2_DEFAULT_MODEL (plan 5.2: closed) ──────────────────────────────────
158
+
159
+ let envProfileWarned = false
160
+
161
+ /** A profile id in COS_G2_DEFAULT_MODEL is ignored, with ONE warning per boot. Returns the closed default. */
162
+ export function closedEnvDefaultModel(raw: string | undefined = process.env.COS_G2_DEFAULT_MODEL) {
163
+ if (!envProfileWarned && isProfilePreference(raw?.trim())) {
164
+ envProfileWarned = true
165
+ console.warn('[key-lanes] COS_G2_DEFAULT_MODEL names a key profile; ignored. Pick a profile from the picker instead.')
166
+ }
167
+ return normalizeModelPreference(raw)
168
+ }
169
+
170
+ /** Tests only. */
171
+ export function __resetEnvProfileWarningForTests(): void {
172
+ envProfileWarned = false
173
+ }
@@ -0,0 +1,182 @@
1
+ // Key lanes (K1, plan v4 sections 3.1, 4.4, 5, 12): saved key profiles, `data/provider-profiles.json`.
2
+ //
3
+ // A profile binds one saved key to one engine and one model. No secret lives here, only the key's
4
+ // ref. `keyLanesActive()` is true only while this file parses with at least one enabled profile.
5
+ // Reading never creates: a missing or corrupt file is "no profiles" and is left untouched.
6
+ import { existsSync, statSync, unlinkSync } from 'node:fs'
7
+ import { isKeyProfileId } from '../../shared/key-preference.js'
8
+ import { dataPath } from './data-dir.js'
9
+ import type { KeyKind, KeyProfile, KeyProfileEngine, KeyProfileTimeoutTier } from './key-relay-contract.js'
10
+ import { KEY_REF_RE } from './key-relay-contract.js'
11
+ import { readPrivateJsonFile, writePrivateJsonFile } from './key-store.js'
12
+
13
+ export const KEY_PROFILES_FILE = 'provider-profiles.json'
14
+ export const KEY_PROFILE_ENGINES: readonly KeyProfileEngine[] = ['claude', 'codex', 'direct']
15
+ export const KEY_PROFILE_TIMEOUT_TIERS: readonly KeyProfileTimeoutTier[] = ['opus', 'fable', 'sonnet', 'haiku']
16
+ export const MAX_KEY_PROFILES = 20
17
+ const MODEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/
18
+ const LABEL_MAX = 40
19
+
20
+ interface ProfilesFile {
21
+ v: 1
22
+ profiles: KeyProfile[]
23
+ }
24
+
25
+ export function keyProfilesPath(): string {
26
+ return dataPath(KEY_PROFILES_FILE)
27
+ }
28
+
29
+ function isProfile(v: unknown): v is KeyProfile {
30
+ if (!v || typeof v !== 'object') return false
31
+ const p = v as Partial<KeyProfile>
32
+ return isKeyProfileId(p.id)
33
+ && typeof p.label === 'string'
34
+ && typeof p.keyRef === 'string' && KEY_REF_RE.test(p.keyRef)
35
+ && typeof p.engine === 'string' && (KEY_PROFILE_ENGINES as readonly string[]).includes(p.engine)
36
+ && typeof p.model === 'string' && MODEL_ID_RE.test(p.model)
37
+ && typeof p.enabled === 'boolean'
38
+ && typeof p.timeoutTier === 'string' && (KEY_PROFILE_TIMEOUT_TIERS as readonly string[]).includes(p.timeoutTier)
39
+ }
40
+
41
+ let cache: { stamp: string; profiles: KeyProfile[] } | null = null
42
+
43
+ export function listProfiles(): KeyProfile[] {
44
+ const file = keyProfilesPath()
45
+ let stamp: string
46
+ try {
47
+ const st = statSync(file)
48
+ stamp = `${st.ino}:${st.size}:${st.mtimeMs}`
49
+ } catch {
50
+ cache = null
51
+ return []
52
+ }
53
+ if (cache?.stamp === stamp) return cache.profiles
54
+ const parsed = readPrivateJsonFile(file) as Partial<ProfilesFile> | undefined
55
+ const profiles = parsed && Array.isArray(parsed.profiles)
56
+ ? parsed.profiles.filter(isProfile).map(p => ({ ...p, offerOnV1: p.offerOnV1 === true, directTools: p.directTools === true }))
57
+ : []
58
+ cache = { stamp, profiles }
59
+ return profiles
60
+ }
61
+
62
+ export function enabledProfileCount(): number {
63
+ return listProfiles().filter(p => p.enabled).length
64
+ }
65
+
66
+ export function getProfile(id: string): KeyProfile | undefined {
67
+ return listProfiles().find(p => p.id === id)
68
+ }
69
+
70
+ function writeProfiles(profiles: KeyProfile[]): void {
71
+ const body: ProfilesFile = { v: 1, profiles }
72
+ writePrivateJsonFile(keyProfilesPath(), body)
73
+ cache = null
74
+ }
75
+
76
+ export interface ProfileInput {
77
+ id?: unknown
78
+ label?: unknown
79
+ keyRef?: unknown
80
+ engine?: unknown
81
+ model?: unknown
82
+ enabled?: unknown
83
+ timeoutTier?: unknown
84
+ offerOnV1?: unknown
85
+ directTools?: unknown
86
+ }
87
+
88
+ /**
89
+ * Plan 4.4 model policy at save time: Lane B Claude takes OpenRouter `anthropic/*` slugs or an
90
+ * Anthropic key's own `claude-*` ids; Lane B Codex takes `openai/*` slugs or an OpenAI key's ids;
91
+ * anything else is Lane C (direct) only, which cannot run on an Anthropic key.
92
+ */
93
+ export function engineModelProblem(engine: KeyProfileEngine, model: string, keyKind: KeyKind): string | null {
94
+ if (engine === 'claude') {
95
+ if (keyKind === 'openrouter' && model.startsWith('anthropic/')) return null
96
+ if (keyKind === 'anthropic' && model.startsWith('claude-')) return null
97
+ return 'Claude profiles take an anthropic/ model on OpenRouter or a claude- model on an Anthropic key.'
98
+ }
99
+ if (engine === 'codex') {
100
+ if (keyKind === 'openrouter' && model.startsWith('openai/')) return null
101
+ if (keyKind === 'openai' && !model.includes('/')) return null
102
+ return 'Codex profiles take an openai/ model on OpenRouter or a model on an OpenAI key.'
103
+ }
104
+ if (keyKind === 'anthropic') return 'Direct profiles need an OpenRouter, OpenAI or custom key.'
105
+ return null
106
+ }
107
+
108
+ export type ProfileValidation = { ok: true; profile: KeyProfile } | { ok: false; error: string; message: string }
109
+
110
+ /** Validate an upsert against the saved keys. `keyKindOf` returns undefined for an unknown ref. */
111
+ export function validateProfileInput(
112
+ input: ProfileInput,
113
+ keyKindOf: (ref: string) => KeyKind | undefined,
114
+ now: () => Date = () => new Date(),
115
+ ): ProfileValidation {
116
+ const bad = (message: string): ProfileValidation => ({ ok: false, error: 'invalid_profile', message })
117
+ if (!isKeyProfileId(input.id)) return bad('Profile id is 1 to 32 lowercase letters, digits or dashes.')
118
+ if (typeof input.label !== 'string' || !input.label.trim() || input.label.trim().length > LABEL_MAX || /[\u0000-\u001f\u007f]/.test(input.label)) {
119
+ return bad(`Give the profile a name of up to ${LABEL_MAX} characters.`)
120
+ }
121
+ if (typeof input.keyRef !== 'string' || !KEY_REF_RE.test(input.keyRef)) return bad('Pick a saved key.')
122
+ const keyKind = keyKindOf(input.keyRef)
123
+ if (!keyKind) return { ok: false, error: 'unknown_key', message: 'That key is not saved on this Mac.' }
124
+ if (typeof input.engine !== 'string' || !(KEY_PROFILE_ENGINES as readonly string[]).includes(input.engine)) return bad('Pick Claude, Codex or Direct.')
125
+ if (typeof input.model !== 'string' || !MODEL_ID_RE.test(input.model)) return bad('Pick a model.')
126
+ const engine = input.engine as KeyProfileEngine
127
+ const modelProblem = engineModelProblem(engine, input.model, keyKind)
128
+ if (modelProblem) return { ok: false, error: 'model_not_allowed', message: modelProblem }
129
+ const tier = input.timeoutTier === undefined ? 'sonnet' : input.timeoutTier
130
+ if (typeof tier !== 'string' || !(KEY_PROFILE_TIMEOUT_TIERS as readonly string[]).includes(tier)) return bad('Unknown timeout tier.')
131
+ for (const [name, v] of [['enabled', input.enabled], ['offerOnV1', input.offerOnV1], ['directTools', input.directTools]] as const) {
132
+ if (v !== undefined && typeof v !== 'boolean') return bad(`${name} is true or false.`)
133
+ }
134
+ const at = now().toISOString()
135
+ const prior = getProfile(input.id)
136
+ return {
137
+ ok: true,
138
+ profile: {
139
+ id: input.id,
140
+ label: input.label.trim(),
141
+ keyRef: input.keyRef,
142
+ engine,
143
+ model: input.model,
144
+ enabled: input.enabled === undefined ? true : input.enabled === true,
145
+ timeoutTier: tier as KeyProfileTimeoutTier,
146
+ offerOnV1: input.offerOnV1 === true,
147
+ // D7: tools stay off unless the caller turns them on for this profile.
148
+ directTools: input.directTools === true,
149
+ createdAt: prior?.createdAt ?? at,
150
+ updatedAt: at,
151
+ },
152
+ }
153
+ }
154
+
155
+ /** Insert or replace by id. Refuses past MAX_KEY_PROFILES. */
156
+ export function upsertProfile(profile: KeyProfile): { ok: true } | { ok: false; error: 'too_many_profiles' } {
157
+ const profiles = [...listProfiles()]
158
+ const i = profiles.findIndex(p => p.id === profile.id)
159
+ if (i < 0 && profiles.length >= MAX_KEY_PROFILES) return { ok: false, error: 'too_many_profiles' }
160
+ if (i >= 0) profiles[i] = profile
161
+ else profiles.push(profile)
162
+ writeProfiles(profiles)
163
+ return { ok: true }
164
+ }
165
+
166
+ /** True when a profile was removed. */
167
+ export function deleteProfile(id: string): boolean {
168
+ const profiles = listProfiles()
169
+ const next = profiles.filter(p => p.id !== id)
170
+ if (next.length === profiles.length) return false
171
+ writeProfiles(next)
172
+ return true
173
+ }
174
+
175
+ /** Remove all keys (plan 12). True when a file was deleted. */
176
+ export function deleteProfilesFile(): boolean {
177
+ cache = null
178
+ const file = keyProfilesPath()
179
+ if (!existsSync(file)) return false
180
+ unlinkSync(file)
181
+ return true
182
+ }
@@ -0,0 +1,262 @@
1
+ // Key lanes (K1, plan v4 sections 4, 8, 12): the FROZEN contract between P1 (foundation), P2 (the
2
+ // loopback metering relay), P3 (Lane B Claude), P4 (Lane B Codex), P5 (Lane C direct) and P11.
3
+ //
4
+ // Types and inert constants only. Nothing here reads a file, opens a socket, starts a timer or
5
+ // touches process.env, so importing it changes nothing for a server with key lanes off.
6
+ // Changing a name or a shape here after P1 lands needs every consumer package to agree.
7
+
8
+ // ── Credentials ──────────────────────────────────────────────────────────────
9
+
10
+ /** What a saved key is (plan D2). `openai-compatible` is a custom URL, Mac only. */
11
+ export type KeyKind = 'openrouter' | 'anthropic' | 'openai' | 'openai-compatible'
12
+ export const KEY_KINDS: readonly KeyKind[] = ['openrouter', 'anthropic', 'openai', 'openai-compatible']
13
+
14
+ /** A key's stable handle: `key_` plus 12 lowercase hex. Never derived from the secret. */
15
+ export type KeyRef = string
16
+ export const KEY_REF_RE = /^key_[0-9a-f]{12}$/
17
+
18
+ /**
19
+ * What any route, log, health field, diagnostic or ledger may say about a key. The secret never
20
+ * leaves `key-store.ts` except through `readKeySecretForRelay`, which only P2 and P5 call.
21
+ */
22
+ export interface PublicKeyRecord {
23
+ ref: KeyRef
24
+ kind: KeyKind
25
+ /** Last four characters of the secret, for "the key ending 1a2b". */
26
+ last4: string
27
+ /** Daily cap in USD for this key (plan 4.5, D11 default 1.00). */
28
+ capUsd: number
29
+ savedAt: string
30
+ validatedAt?: string
31
+ /** `openai-compatible` only: the base URL the key was saved for. */
32
+ baseUrl?: string
33
+ }
34
+
35
+ // ── Lanes and answers ────────────────────────────────────────────────────────
36
+
37
+ /** Lane A is the user's subscription CLI; B runs the CLI harness on a key; C calls the key direct. */
38
+ export type AnswerLane = 'subscription' | 'key' | 'direct'
39
+ /** The lanes that spend a key. Token-audit `lane` takes only these. */
40
+ export type KeyLane = Exclude<AnswerLane, 'subscription'>
41
+
42
+ /** Additive answer fields (plan 3.1). Present only on keyed answers, omitted (never null) otherwise. */
43
+ export interface AnswerKeyFields {
44
+ lane: KeyLane
45
+ credential: { kind: KeyKind; ref: KeyRef; profileId: string }
46
+ }
47
+
48
+ /**
49
+ * Token-audit fields (plan 3.1, 6.3). Record keys are exactly `profile`, `lane`, `credentialKind`
50
+ * and `usd`; each is written only when set, so a Lane A row is byte-identical to 6.79.0.
51
+ */
52
+ export interface TokenAuditKeyFields {
53
+ profile?: string
54
+ lane?: KeyLane
55
+ credentialKind?: KeyKind
56
+ usd?: number
57
+ }
58
+
59
+ // ── Profiles ─────────────────────────────────────────────────────────────────
60
+
61
+ /** Which harness a profile runs. `claude` and `codex` are Lane B (through the relay); `direct` is Lane C. */
62
+ export type KeyProfileEngine = 'claude' | 'codex' | 'direct'
63
+ /** Timeout tier for a Lane B Claude profile (plan 7.1): the subscription tier whose budgets it borrows. */
64
+ export type KeyProfileTimeoutTier = 'opus' | 'fable' | 'sonnet' | 'haiku'
65
+
66
+ export interface KeyProfile {
67
+ /** `^[a-z0-9][a-z0-9-]{0,31}$` (shared/key-preference.ts KEY_PROFILE_ID_RE). */
68
+ id: string
69
+ label: string
70
+ keyRef: KeyRef
71
+ engine: KeyProfileEngine
72
+ /** The provider model id or OpenRouter slug, e.g. `anthropic/claude-sonnet-5.5`. */
73
+ model: string
74
+ enabled: boolean
75
+ timeoutTier: KeyProfileTimeoutTier
76
+ /** Plan 9.4: offer this profile on `/v1` as `cos-key-<id>`. Default false. */
77
+ offerOnV1: boolean
78
+ /** Plan D7: Lane C tools for this profile. Default false; never on by default for a remote endpoint. */
79
+ directTools: boolean
80
+ createdAt: string
81
+ updatedAt: string
82
+ }
83
+
84
+ /** What `/api/models` publishes as `keyProfiles` while lanes are active (enabled profiles only). */
85
+ export interface PublicKeyProfileOption {
86
+ id: string
87
+ label: string
88
+ engine: KeyProfileEngine
89
+ model: string
90
+ keyKind: KeyKind
91
+ timeoutTier: KeyProfileTimeoutTier
92
+ }
93
+
94
+ // ── Relay (P2 implements; P3, P4 consume) ────────────────────────────────────
95
+
96
+ export type RelayEngine = 'claude' | 'codex'
97
+
98
+ /** Plan 4.2. The relay listens on `127.0.0.1:<ephemeral>` only; never 3141, 3143 or 0.0.0.0. */
99
+ export const RELAY_PATHS = {
100
+ anthropicMessages: '/relay/a/v1/messages',
101
+ anthropicCountTokens: '/relay/a/v1/messages/count_tokens',
102
+ openaiResponses: '/relay/o/v1/responses',
103
+ health: '/relay/health',
104
+ } as const
105
+ /** Base path a child is pointed at: Claude `ANTHROPIC_BASE_URL=<origin>/relay/a`, Codex `<origin>/relay/o/v1`. */
106
+ export const RELAY_BASE_PATHS = { claude: '/relay/a', codex: '/relay/o/v1' } as const
107
+
108
+ /** Plan 4.2 upstreams, by key kind. */
109
+ export const RELAY_UPSTREAMS = {
110
+ openrouterMessages: 'https://openrouter.ai/api/v1/messages',
111
+ anthropicMessages: 'https://api.anthropic.com/v1/messages',
112
+ openrouterResponses: 'https://openrouter.ai/api/v1/responses',
113
+ openaiResponses: 'https://api.openai.com/v1/responses',
114
+ } as const
115
+
116
+ /** Plan 4.3 nonce limits. */
117
+ export const RELAY_LIMITS = {
118
+ nonceBytes: 32,
119
+ /** Canary QA (2026-10-19): 12 parallel Task subagents were measured; 16 leaves headroom, and the relay queues past it. */
120
+ inFlightPerNonce: 16,
121
+ bodyCapBytes: 20 * 1024 * 1024,
122
+ /** Nonce lives until child close plus this. */
123
+ childCloseGraceMs: 5_000,
124
+ /** ...or the tier's wall max plus this, whichever comes first. */
125
+ wallMaxGraceMs: 60_000,
126
+ /** Logs show at most this many leading characters of a nonce. */
127
+ nonceLogChars: 4,
128
+ } as const
129
+
130
+ /** Plan 4.3 binding. One nonce per turn, memory only. */
131
+ export interface RelayNonceBinding {
132
+ keyRef: KeyRef
133
+ profileId: string
134
+ turnId: string
135
+ sessionId: string
136
+ engine: RelayEngine
137
+ /** The profile slug. Plan 4.4: equal forwards, a Claude id or alias is rewritten to it, anything else is 400. */
138
+ allowedModel: string
139
+ }
140
+
141
+ export type RelayRevokeReason =
142
+ | 'child_close'
143
+ | 'cancel'
144
+ | 'steer_abort'
145
+ | 'breaker_open'
146
+ | 'remove_all'
147
+ | 'lanes_inactive'
148
+ | 'expired'
149
+
150
+ export interface RelayTurnHandle {
151
+ /** The secret the child presents. Never logged beyond RELAY_LIMITS.nonceLogChars. */
152
+ nonce: string
153
+ /** `http://127.0.0.1:<port>` for this boot. */
154
+ origin: string
155
+ /** Called by the bridge on child close; the relay keeps the nonce for childCloseGraceMs. */
156
+ childClosed(): void
157
+ revoke(reason: RelayRevokeReason): void
158
+ }
159
+
160
+ export interface RelayIssueOptions {
161
+ /** The tier's wall max for this turn (plan 4.3 lifetime). */
162
+ wallMaxMs: number
163
+ }
164
+
165
+ /** P2's relay. Started lazily by the first keyed turn; closed when keyLanesActive() turns false. */
166
+ export interface KeyRelay {
167
+ issueTurn(binding: RelayNonceBinding, options: RelayIssueOptions): Promise<RelayTurnHandle>
168
+ revokeTurn(turnId: string, reason: RelayRevokeReason): void
169
+ revokeAll(reason: RelayRevokeReason): void
170
+ /** The listener's port, or null when no listener is open. */
171
+ port(): number | null
172
+ close(): Promise<void>
173
+ }
174
+
175
+ /** Plan 4.4 model policy outcome. */
176
+ export type RelayModelAction = 'forward' | 'rewrite' | 'reject'
177
+
178
+ /** Plan 4.6 mapping, one value per row. */
179
+ export type RelayFailureKind =
180
+ | 'cap_refused'
181
+ | 'key_rejected'
182
+ | 'out_of_credit'
183
+ | 'model_gone'
184
+ | 'rate_limited'
185
+ | 'upstream_error'
186
+ | 'network_error'
187
+ | 'model_not_allowed'
188
+
189
+ /** Only these count toward the breaker (plan 4.6). */
190
+ export type BreakerCountedFailure = Extract<RelayFailureKind, 'key_rejected' | 'out_of_credit' | 'model_gone'>
191
+ export type BreakerScope = 'key' | 'profile'
192
+ export interface BreakerState {
193
+ open: boolean
194
+ failures: number
195
+ openedAt?: string
196
+ reopensAt?: string
197
+ }
198
+ export const BREAKER_LIMITS = { failures: 3, openMs: 60 * 60 * 1000 } as const
199
+
200
+ // ── Meter and ledger (P2) ────────────────────────────────────────────────────
201
+
202
+ export type KeyLedgerKind = 'reserve' | 'settle' | 'reconcile'
203
+
204
+ /** One row of `data/key-ledger.jsonl` (0600, atomic append). Never a secret, never a nonce. */
205
+ export interface KeyLedgerRow {
206
+ ts: string
207
+ keyRef: KeyRef
208
+ profile: string
209
+ lane: KeyLane
210
+ turnId: string
211
+ generationId?: string
212
+ tokens: { input: number; output: number }
213
+ usd: number
214
+ kind: KeyLedgerKind
215
+ }
216
+
217
+ export interface KeyMeter {
218
+ /** Reserve before forwarding; false means refuse with the cap line. */
219
+ reserve(input: { keyRef: KeyRef; turnId: string; estimateUsd: number }): { ok: true; reservationId: string } | { ok: false; spentTodayUsd: number; capUsd: number }
220
+ settle(input: { reservationId: string; usd: number; tokens: { input: number; output: number }; generationId?: string }): void
221
+ spentTodayUsd(keyRef: KeyRef): number
222
+ }
223
+
224
+ // ── Child environment names (P3, P4) ─────────────────────────────────────────
225
+
226
+ export const KEY_LANE_CHILD_ENV = {
227
+ /** Claude: the nonce, as a Bearer. */
228
+ claudeToken: 'ANTHROPIC_AUTH_TOKEN',
229
+ claudeBaseUrl: 'ANTHROPIC_BASE_URL',
230
+ /** Codex: the nonce, named by `env_key` in the generated config. */
231
+ codexToken: 'COS_RELAY_TOKEN',
232
+ /** Marks a keyed child for hooks and the MU child_env() scrub (plan 6.2, 7.3). */
233
+ profileMarker: 'COS_KEY_PROFILE',
234
+ } as const
235
+
236
+ // ── Status and lifecycle seams (P1 calls; P2, P3, P6 register) ───────────────
237
+
238
+ /** P2 and P3 answer these for `GET /api/key-lanes/status`. P1 defaults: 0, closed, []. */
239
+ export interface KeyLanesStatusProvider {
240
+ spentTodayUsd?(keyRef: KeyRef): number
241
+ breaker?(keyRef: KeyRef): BreakerState
242
+ keyedSessionIds?(): string[]
243
+ }
244
+
245
+ /** Why key lanes stopped being active. Hooks revoke nonces, close the relay, clear bindings. */
246
+ export type KeyLanesInactiveReason = 'disabled' | 'remove_all' | 'no_enabled_profile'
247
+ export type KeyLanesInactiveHook = (reason: KeyLanesInactiveReason) => void | Promise<void>
248
+
249
+ /** `GET /api/key-lanes/status` body. */
250
+ export interface KeyLanesStatusBody {
251
+ enabled: boolean
252
+ active: boolean
253
+ keys: Array<{ ref: KeyRef; kind: KeyKind; last4: string; capUsd: number; spentTodayUsd: number; breaker: BreakerState }>
254
+ profiles: KeyProfile[]
255
+ keyedSessionIds: string[]
256
+ }
257
+
258
+ /** `details.keyLanes` on `/api/health` (public), present only while enabled. Booleans only. */
259
+ export interface KeyLanesHealthDetail {
260
+ enabled: true
261
+ active: boolean
262
+ }