thinkpool-pair 0.7.364 → 0.7.366
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 +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6266
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -60
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -92
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -79
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +13 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
package/providers.mjs
CHANGED
|
@@ -1,491 +1 @@
|
|
|
1
|
-
|
|
2
|
-
providers.mjs — the bridge's LLM-provider REGISTRY (multi-BYOK).
|
|
3
|
-
|
|
4
|
-
Slice 1 of the multi-provider BYOK feature (spec:
|
|
5
|
-
docs/specs/2026-07-05-llm-provider-registry.md). Lets a user register
|
|
6
|
-
extra Anthropic-compatible providers (name + base url + model + API
|
|
7
|
-
key) on their OWN bridge; a Code lane can then spawn against any
|
|
8
|
-
registered provider by id. Keys live ONLY on the bridge host — they are
|
|
9
|
-
never broadcast, never written to the DB, never in an announce.
|
|
10
|
-
|
|
11
|
-
This module owns:
|
|
12
|
-
• providers.json — the registry ({id,name,baseUrl,model,key,addedAt}[]), 0600
|
|
13
|
-
• bridge-key.json — the bridge's RSA keypair, 0600 (host-only)
|
|
14
|
-
• seal/unseal — the hybrid envelope the dashboard uses to hand the
|
|
15
|
-
bridge a key WITHOUT it ever crossing the wire in the
|
|
16
|
-
clear (RSA-OAEP wraps an AES-256-GCM session key).
|
|
17
|
-
• env resolution — {ANTHROPIC_BASE_URL,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_MODEL}
|
|
18
|
-
for a chosen provider id, matching provider.mjs conventions.
|
|
19
|
-
|
|
20
|
-
The built-in `anthropic` provider (the host's regular Claude login) is
|
|
21
|
-
IMPLICIT: never stored, never removable, always present. A lane with no
|
|
22
|
-
provider id runs the default Claude path unchanged.
|
|
23
|
-
|
|
24
|
-
── Why HYBRID sealing, not raw RSA ──────────────────────────────────
|
|
25
|
-
RSA-OAEP-2048/SHA-256 caps plaintext at 190 bytes. A real payload —
|
|
26
|
-
{name,baseUrl,model,key} with e.g. an OpenAI `sk-proj-…` key (~164 chars)
|
|
27
|
-
plus JSON overhead — blows past that and RSA encrypt throws. So `sealed`
|
|
28
|
-
is a hybrid envelope: RSA-OAEP wraps a fresh 32-byte AES key, AES-256-GCM
|
|
29
|
-
encrypts the payload. Both ends are WebCrypto-native (browser dashboard
|
|
30
|
-
seals, Node bridge unseals). Envelope shape (base64 of UTF8 JSON):
|
|
31
|
-
{ v:1, ek:base64(RSA-OAEP(aesKeyRaw)), iv:base64(12B), ct:base64(gcmCiphertext||16B-tag) }
|
|
32
|
-
WebCrypto's subtle.encrypt returns ciphertext||tag concatenated; Node's
|
|
33
|
-
createDecipheriv wants the tag separately, so unseal splits the last 16.
|
|
34
|
-
|
|
35
|
-
── Auth (see account.mjs handlers) ─────────────────────────────────
|
|
36
|
-
The account channel `tpacct:<uid>` is a PUBLIC realtime channel (no
|
|
37
|
-
config.private:true), so it is NOT RLS-gated — the sender is verified at
|
|
38
|
-
the application layer (getUser(jwt).id === owner uid) before any registry
|
|
39
|
-
mutation. This module stays transport-agnostic; the caller does auth.
|
|
40
|
-
───────────────────────────────────────────────────────────── */
|
|
41
|
-
import os from 'node:os'
|
|
42
|
-
import fs from 'node:fs'
|
|
43
|
-
import path from 'node:path'
|
|
44
|
-
import crypto from 'node:crypto'
|
|
45
|
-
import { claudeish } from './event-id.mjs'
|
|
46
|
-
import {
|
|
47
|
-
DEFAULT_RESILIENCE_POLICY,
|
|
48
|
-
resilienceProjection,
|
|
49
|
-
resolveAttemptTarget,
|
|
50
|
-
validateResiliencePolicy,
|
|
51
|
-
} from './provider-resilience.mjs'
|
|
52
|
-
|
|
53
|
-
const DIR = path.join(os.homedir(), '.thinkpool-pair')
|
|
54
|
-
const REG_FILE = path.join(DIR, 'providers.json')
|
|
55
|
-
const KEY_FILE = path.join(DIR, 'bridge-key.json')
|
|
56
|
-
|
|
57
|
-
// The built-in provider id — always present, never stored, never removable.
|
|
58
|
-
export const BUILTIN_ID = 'anthropic'
|
|
59
|
-
|
|
60
|
-
function ensureDir() { try { fs.mkdirSync(DIR, { recursive: true, mode: 0o700 }) } catch { /* noop */ } }
|
|
61
|
-
|
|
62
|
-
// ── registry load / save ───────────────────────────────────────────────
|
|
63
|
-
/** @returns {Array<{id:string,name:string,baseUrl:string,model?:string,key:string,addedAt:number,resilience?:object}>} */
|
|
64
|
-
export function loadProviders() {
|
|
65
|
-
try {
|
|
66
|
-
const arr = JSON.parse(fs.readFileSync(REG_FILE, 'utf8'))
|
|
67
|
-
return Array.isArray(arr) ? arr.filter((p) => p && p.id && p.id !== BUILTIN_ID) : []
|
|
68
|
-
} catch { return [] }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function saveProviders(arr) {
|
|
72
|
-
ensureDir()
|
|
73
|
-
fs.writeFileSync(REG_FILE, JSON.stringify(arr, null, 2), { mode: 0o600 })
|
|
74
|
-
// Writing with { mode } only applies on CREATE; harden an existing file too.
|
|
75
|
-
try { fs.chmodSync(REG_FILE, 0o600) } catch { /* noop */ }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── keypair ─────────────────────────────────────────────────────────────
|
|
79
|
-
// Generate an RSA-OAEP-2048 keypair on first run, persist 0600, return the
|
|
80
|
-
// public key as SPKI base64 (what the dashboard imports) + the private key
|
|
81
|
-
// object (for unseal). Idempotent: a persisted key is reused.
|
|
82
|
-
let _keyCache = null
|
|
83
|
-
export function ensureKeypair() {
|
|
84
|
-
if (_keyCache) return _keyCache
|
|
85
|
-
ensureDir()
|
|
86
|
-
try {
|
|
87
|
-
const saved = JSON.parse(fs.readFileSync(KEY_FILE, 'utf8'))
|
|
88
|
-
if (saved?.publicKeyB64 && saved?.privateKeyPem) {
|
|
89
|
-
_keyCache = {
|
|
90
|
-
publicKeyB64: saved.publicKeyB64,
|
|
91
|
-
privateKey: crypto.createPrivateKey({ key: saved.privateKeyPem, format: 'pem' }),
|
|
92
|
-
}
|
|
93
|
-
return _keyCache
|
|
94
|
-
}
|
|
95
|
-
} catch { /* generate below */ }
|
|
96
|
-
|
|
97
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
98
|
-
const publicKeyB64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64')
|
|
99
|
-
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' })
|
|
100
|
-
fs.writeFileSync(KEY_FILE, JSON.stringify({ publicKeyB64, privateKeyPem, createdAt: Date.now() }, null, 2), { mode: 0o600 })
|
|
101
|
-
try { fs.chmodSync(KEY_FILE, 0o600) } catch { /* noop */ }
|
|
102
|
-
_keyCache = { publicKeyB64, privateKey }
|
|
103
|
-
return _keyCache
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** SPKI base64 public key for the announce (`providerPubKey`). */
|
|
107
|
-
export function publicKeyB64() { return ensureKeypair().publicKeyB64 }
|
|
108
|
-
|
|
109
|
-
// Stable, public machine identity for terminal ownership. `host` is a display
|
|
110
|
-
// label and can be "localhost" on multiple computers; this fingerprint is tied
|
|
111
|
-
// to the bridge's existing keypair without exposing private material. Accepting
|
|
112
|
-
// an explicit public key keeps the identity primitive directly testable.
|
|
113
|
-
export function bridgeHostId(pubKey = publicKeyB64()) {
|
|
114
|
-
return crypto.createHash('sha256').update(pubKey).digest('hex').slice(0, 32)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── seal / unseal (hybrid RSA-OAEP + AES-256-GCM) ───────────────────────
|
|
118
|
-
// seal() is here so the UNIT can drive both ends node-side; the real seal
|
|
119
|
-
// happens in the browser dashboard via WebCrypto against the same envelope.
|
|
120
|
-
/** @param {object} payload @param {string=} pubKeyB64 → base64(envelope JSON) */
|
|
121
|
-
export function seal(payload, pubKeyB64) {
|
|
122
|
-
const spkiB64 = pubKeyB64 || publicKeyB64()
|
|
123
|
-
const publicKey = crypto.createPublicKey({ key: Buffer.from(spkiB64, 'base64'), format: 'der', type: 'spki' })
|
|
124
|
-
const aesKey = crypto.randomBytes(32)
|
|
125
|
-
const iv = crypto.randomBytes(12)
|
|
126
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv)
|
|
127
|
-
const enc = Buffer.concat([cipher.update(JSON.stringify(payload), 'utf8'), cipher.final()])
|
|
128
|
-
const tag = cipher.getAuthTag()
|
|
129
|
-
const ek = crypto.publicEncrypt(
|
|
130
|
-
{ key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
|
|
131
|
-
aesKey,
|
|
132
|
-
)
|
|
133
|
-
const envelope = {
|
|
134
|
-
v: 1,
|
|
135
|
-
ek: ek.toString('base64'),
|
|
136
|
-
iv: iv.toString('base64'),
|
|
137
|
-
ct: Buffer.concat([enc, tag]).toString('base64'), // ct||tag, matches WebCrypto
|
|
138
|
-
}
|
|
139
|
-
return Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64')
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Decrypt a sealed envelope with the bridge private key. Returns the parsed
|
|
144
|
-
* payload object, or throws. Callers MUST catch and answer ok:false WITHOUT
|
|
145
|
-
* logging the ciphertext or plaintext (security invariant d).
|
|
146
|
-
* @param {string} sealedB64 base64(envelope JSON)
|
|
147
|
-
*/
|
|
148
|
-
export function unseal(sealedB64) {
|
|
149
|
-
const { privateKey } = ensureKeypair()
|
|
150
|
-
const env = JSON.parse(Buffer.from(String(sealedB64 || ''), 'base64').toString('utf8'))
|
|
151
|
-
if (!env || env.v !== 1 || !env.ek || !env.iv || !env.ct) throw new Error('bad envelope')
|
|
152
|
-
const aesKey = crypto.privateDecrypt(
|
|
153
|
-
{ key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
|
|
154
|
-
Buffer.from(env.ek, 'base64'),
|
|
155
|
-
)
|
|
156
|
-
const iv = Buffer.from(env.iv, 'base64')
|
|
157
|
-
const ctTag = Buffer.from(env.ct, 'base64')
|
|
158
|
-
if (ctTag.length < 16) throw new Error('bad ciphertext')
|
|
159
|
-
const tag = ctTag.subarray(ctTag.length - 16)
|
|
160
|
-
const ct = ctTag.subarray(0, ctTag.length - 16)
|
|
161
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv)
|
|
162
|
-
decipher.setAuthTag(tag)
|
|
163
|
-
const dec = Buffer.concat([decipher.update(ct), decipher.final()])
|
|
164
|
-
return JSON.parse(dec.toString('utf8'))
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── CRUD ────────────────────────────────────────────────────────────────
|
|
168
|
-
/* Local-LLM exception (2026-07-10): a provider on the SAME machine (LM Studio,
|
|
169
|
-
llama.cpp — both speak the Anthropic Messages API natively) is allowed over
|
|
170
|
-
plain http and without a key. Both relaxations are LOOPBACK-ONLY:
|
|
171
|
-
• https-only stays for every remote host — a key on plaintext http across a
|
|
172
|
-
real wire is the leak the rule exists to stop; on 127.0.0.1 there is no wire.
|
|
173
|
-
• key-required stays for every remote host — local servers simply don't take
|
|
174
|
-
one (resolveProviderEnv synthesizes a placeholder token so the SDK doesn't
|
|
175
|
-
fall back to the host's Claude login against a localhost URL). */
|
|
176
|
-
export function isLoopbackHost(hostname) {
|
|
177
|
-
const h = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '')
|
|
178
|
-
// 127/8 must be a LITERAL IPv4 — '127.evil.io' is a DNS name that can resolve
|
|
179
|
-
// anywhere, so a bare startsWith('127.') would be a spoofable hole.
|
|
180
|
-
return h === 'localhost' || h === '::1' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function validate({ name, baseUrl, key }) {
|
|
184
|
-
if (!name || typeof name !== 'string' || !name.trim()) return 'name is required'
|
|
185
|
-
let u
|
|
186
|
-
try { u = new URL(String(baseUrl)) } catch { return 'baseUrl must be a valid https url' }
|
|
187
|
-
const local = isLoopbackHost(u.hostname)
|
|
188
|
-
if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {
|
|
189
|
-
return 'baseUrl must be https (plain http is allowed only for localhost)'
|
|
190
|
-
}
|
|
191
|
-
if (!local && (!key || typeof key !== 'string' || !key.trim())) return 'key is required'
|
|
192
|
-
return null
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Append a provider. `fields` = {name, baseUrl, model?, key} (already unsealed).
|
|
197
|
-
* @returns {{ok:true,id:string} | {ok:false,error:string}}
|
|
198
|
-
*/
|
|
199
|
-
export function addProvider(fields) {
|
|
200
|
-
const err = validate(fields || {})
|
|
201
|
-
if (err) return { ok: false, error: err }
|
|
202
|
-
const arr = loadProviders()
|
|
203
|
-
const id = crypto.randomUUID()
|
|
204
|
-
arr.push({
|
|
205
|
-
id,
|
|
206
|
-
name: String(fields.name).trim(),
|
|
207
|
-
baseUrl: String(fields.baseUrl).trim(),
|
|
208
|
-
model: fields.model ? String(fields.model).trim() : undefined,
|
|
209
|
-
key: fields.key ? String(fields.key).trim() : '', // '' = keyless local provider
|
|
210
|
-
addedAt: Date.now(),
|
|
211
|
-
})
|
|
212
|
-
saveProviders(arr)
|
|
213
|
-
return { ok: true, id }
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Add ANOTHER model to an already-registered provider — without the user ever
|
|
218
|
-
* retyping the key (Max, 2026-07-09). The registry's unit is a {provider,model}
|
|
219
|
-
* pair, not a provider, and `buildSwitchMenu` already groups rows by brand
|
|
220
|
-
* ("Z.ai GLM 4.5" / "Z.ai GLM Air" collapse under one "z.ai" header) — so a new
|
|
221
|
-
* model is simply a CLONE of the source row with a fresh id and a new model.
|
|
222
|
-
* No schema change, no new menu shape.
|
|
223
|
-
*
|
|
224
|
-
* The clone copies `key` + `baseUrl` HOST-SIDE from the row that already holds
|
|
225
|
-
* them, so no key material crosses the wire for this operation — the caller's
|
|
226
|
-
* RPC carries no `sealed` envelope at all (see account.mjs `provider-add-model`).
|
|
227
|
-
*
|
|
228
|
-
* @param {{id:string, model:string, name?:string}} fields id = the SOURCE provider
|
|
229
|
-
* @returns {{ok:true,id:string} | {ok:false,error:string}}
|
|
230
|
-
*/
|
|
231
|
-
export function addProviderModel({ id, model, name } = {}) {
|
|
232
|
-
if (!id || id === BUILTIN_ID) return { ok: false, error: 'the built-in Anthropic provider has no key to reuse' }
|
|
233
|
-
if (!model || typeof model !== 'string' || !model.trim()) return { ok: false, error: 'model is required' }
|
|
234
|
-
const arr = loadProviders()
|
|
235
|
-
const src = arr.find((p) => p.id === id)
|
|
236
|
-
if (!src) return { ok: false, error: 'no such provider' }
|
|
237
|
-
|
|
238
|
-
const nextModel = String(model).trim()
|
|
239
|
-
// An endpoint+model pair is the identity of a row — a second row for the same
|
|
240
|
-
// pair would render as a duplicate menu entry that switches to nothing new.
|
|
241
|
-
if (arr.some((p) => p.baseUrl === src.baseUrl && (p.model || '') === nextModel)) {
|
|
242
|
-
return { ok: false, error: `${nextModel} is already registered on this provider` }
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const nextName = (name && String(name).trim()) || `${brandOf(src.name)} ${nextModel}`.trim()
|
|
246
|
-
const newId = crypto.randomUUID()
|
|
247
|
-
arr.push({
|
|
248
|
-
id: newId,
|
|
249
|
-
name: nextName,
|
|
250
|
-
baseUrl: src.baseUrl, // copied host-side
|
|
251
|
-
model: nextModel,
|
|
252
|
-
key: src.key, // copied host-side — never re-sent by the client
|
|
253
|
-
// A cloned credential is not consent to retry it. Each row needs its own
|
|
254
|
-
// explicit Phase-1 opt-in, even when both rows share the same endpoint/key.
|
|
255
|
-
resilience: { ...DEFAULT_RESILIENCE_POLICY, retryOn: [...DEFAULT_RESILIENCE_POLICY.retryOn], circuit: { ...DEFAULT_RESILIENCE_POLICY.circuit }, fallback: [] },
|
|
256
|
-
addedAt: Date.now(),
|
|
257
|
-
})
|
|
258
|
-
saveProviders(arr)
|
|
259
|
-
return { ok: true, id: newId }
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* First name token — the brand, matching code-constants.js `brandKey`. "Z.ai GLM
|
|
264
|
-
* 4.5" → "Z.ai". Used to derive a default name for a cloned row so the switch
|
|
265
|
-
* menu groups it under the SAME header as its source.
|
|
266
|
-
*/
|
|
267
|
-
function brandOf(name) {
|
|
268
|
-
const first = String(name || '').trim().split(/\s+/)[0]
|
|
269
|
-
return first || 'Provider'
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Remove a provider by id. The built-in `anthropic` refuses.
|
|
274
|
-
* @returns {{ok:true} | {ok:false,error:string}}
|
|
275
|
-
*/
|
|
276
|
-
export function removeProvider(id) {
|
|
277
|
-
if (!id || id === BUILTIN_ID) return { ok: false, error: 'the built-in Anthropic provider cannot be removed' }
|
|
278
|
-
const arr = loadProviders()
|
|
279
|
-
const next = arr.filter((p) => p.id !== id)
|
|
280
|
-
if (next.length === arr.length) return { ok: false, error: 'no such provider' }
|
|
281
|
-
saveProviders(next)
|
|
282
|
-
return { ok: true }
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/* ── the provider GROUP token (Max, 2026-07-09) ──────────────────────────
|
|
286
|
-
Two registry rows are the SAME PROVIDER when they point at the same endpoint
|
|
287
|
-
with the same key — they differ only in which model they ask it for. That is
|
|
288
|
-
the identity that matters: it decides whether the switch menu unites them, and
|
|
289
|
-
whether switching between them needs a fresh agent (it doesn't — same env).
|
|
290
|
-
|
|
291
|
-
Before this, the clients grouped on `brandKey(name)`: the first whitespace token
|
|
292
|
-
of the user's DISPLAY NAME, lowercased. "GLM" → `glm`, "GLM-5.2" → `glm5.2`, so
|
|
293
|
-
Max's two models on one z.ai key rendered as two unrelated providers, each
|
|
294
|
-
threatening to restart his agent. Identity was being derived from a label.
|
|
295
|
-
|
|
296
|
-
`group` is a one-way digest of baseUrl + key, NOT either value. It is safe on
|
|
297
|
-
the announce (which deliberately carries neither): a 12-hex prefix reveals only
|
|
298
|
-
*sameness*, and preimaging it means preimaging the key. The built-in Anthropic
|
|
299
|
-
has no stored key, so it gets the literal 'anthropic' — it groups with nothing.
|
|
300
|
-
|
|
301
|
-
NUL-separated so ("https://a.io/x", "key") and ("https://a.io/xkey", "") can't
|
|
302
|
-
collide into one group. */
|
|
303
|
-
export function providerGroup(p) {
|
|
304
|
-
if (!p || p.id === BUILTIN_ID) return BUILTIN_ID
|
|
305
|
-
return crypto.createHash('sha256')
|
|
306
|
-
.update(`${p.baseUrl || ''}\0${p.key || ''}`)
|
|
307
|
-
.digest('hex')
|
|
308
|
-
.slice(0, 12)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Do these two registered providers share an endpoint AND a key? Then a switch
|
|
313
|
-
* between them changes only ANTHROPIC_MODEL — the child's env is otherwise
|
|
314
|
-
* identical, so the live session can just setModel() and keep its context
|
|
315
|
-
* instead of being torn down and recapped. Built-in ↔ custom is never same-env.
|
|
316
|
-
*/
|
|
317
|
-
export function sameProviderEnv(idA, idB) {
|
|
318
|
-
const a = effectiveId(idA), b = effectiveId(idB)
|
|
319
|
-
if (a === BUILTIN_ID || b === BUILTIN_ID) return a === b // both built-in, or one of them
|
|
320
|
-
const arr = loadProviders()
|
|
321
|
-
const pa = arr.find((x) => x.id === a), pb = arr.find((x) => x.id === b)
|
|
322
|
-
if (!pa || !pb) return false // unknown id → never same
|
|
323
|
-
return providerGroup(pa) === providerGroup(pb)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const effectiveId = (p) => (!p || p === BUILTIN_ID) ? BUILTIN_ID : String(p)
|
|
327
|
-
|
|
328
|
-
/** The model a registered provider is configured to ask for (null for built-in/unknown). */
|
|
329
|
-
export function providerModel(id) {
|
|
330
|
-
if (!id || id === BUILTIN_ID) return null
|
|
331
|
-
return loadProviders().find((x) => x.id === id)?.model || null
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Persist one custom provider row's Phase-1 same-target policy. This is a
|
|
336
|
-
* host-local primitive: account RPC/UI wiring deliberately lives elsewhere.
|
|
337
|
-
*
|
|
338
|
-
* `policy` may be a complete v1 policy, or a boolean for the conservative
|
|
339
|
-
* Phase-1 preset. Fallback declarations are rejected here: alternate targets
|
|
340
|
-
* require the later explicit-consent flow, so this API can never turn one on.
|
|
341
|
-
*/
|
|
342
|
-
export function setProviderResiliencePolicy(id, policy) {
|
|
343
|
-
if (!id || id === BUILTIN_ID) return { ok: false, error: 'the built-in Anthropic provider cannot use resilience policy' }
|
|
344
|
-
const arr = loadProviders()
|
|
345
|
-
const provider = arr.find((p) => p.id === id)
|
|
346
|
-
if (!provider) return { ok: false, error: 'no such provider' }
|
|
347
|
-
|
|
348
|
-
const input = typeof policy === 'boolean'
|
|
349
|
-
? { ...DEFAULT_RESILIENCE_POLICY, enabled: policy, retryOn: [...DEFAULT_RESILIENCE_POLICY.retryOn], circuit: { ...DEFAULT_RESILIENCE_POLICY.circuit }, fallback: [] }
|
|
350
|
-
: policy
|
|
351
|
-
const checked = validateResiliencePolicy(input, { providers: arr, primaryProviderId: id })
|
|
352
|
-
if (!checked.ok) return { ok: false, error: checked.error }
|
|
353
|
-
if (checked.value.fallback.length) return { ok: false, error: 'fallback is not available in Phase 1' }
|
|
354
|
-
// An enabled controller must have a configured exact model for its safe
|
|
355
|
-
// target record. A disabled policy remains valid for legacy/no-model rows.
|
|
356
|
-
if (checked.value.enabled && !resolveAttemptTarget({ providers: arr, providerId: id, model: provider.model })) {
|
|
357
|
-
return { ok: false, error: 'an enabled resilience policy requires an exact configured model' }
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const next = arr.map((p) => p.id === id ? { ...p, resilience: checked.value } : p)
|
|
361
|
-
saveProviders(next)
|
|
362
|
-
return { ok: true, resilience: resilienceProjection(checked.value) }
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Resolve a custom provider's persisted policy for bridge wiring without ever
|
|
367
|
-
* returning host credentials or endpoint data. Missing policy is safely
|
|
368
|
-
* disabled; malformed stored policy, built-in, unknown, and model-less targets
|
|
369
|
-
* fail closed with null.
|
|
370
|
-
*/
|
|
371
|
-
export function resolveProviderResiliencePolicy(id) {
|
|
372
|
-
if (!id || id === BUILTIN_ID) return null
|
|
373
|
-
const providers = loadProviders()
|
|
374
|
-
const provider = providers.find((p) => p.id === id)
|
|
375
|
-
if (!provider) return null
|
|
376
|
-
const checked = validateResiliencePolicy(provider.resilience, { providers, primaryProviderId: id })
|
|
377
|
-
if (!checked.ok) return null
|
|
378
|
-
const target = resolveAttemptTarget({ providers, providerId: id, model: provider.model })
|
|
379
|
-
if (!target) return null
|
|
380
|
-
return { policy: resilienceProjection(checked.value), target }
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ── read-only projections (never expose the raw key) ────────────────────
|
|
384
|
-
/** Masked list for the dashboard: keyHint = last 4 chars only. Built-in first. */
|
|
385
|
-
export function listProviders() {
|
|
386
|
-
const custom = loadProviders().map((p) => ({
|
|
387
|
-
id: p.id,
|
|
388
|
-
name: p.name,
|
|
389
|
-
model: p.model || null,
|
|
390
|
-
group: providerGroup(p),
|
|
391
|
-
keyHint: keyHint(p.key),
|
|
392
|
-
resilience: resilienceProjection(p.resilience),
|
|
393
|
-
}))
|
|
394
|
-
return [{ id: BUILTIN_ID, name: 'Anthropic (Claude)', model: null, group: BUILTIN_ID, keyHint: null, resilience: null }, ...custom]
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/** Name-only projection for the announce/presence payload — NO key, NO baseUrl. */
|
|
398
|
-
export function announceProviders() {
|
|
399
|
-
const custom = loadProviders().map((p) => ({ id: p.id, name: p.name, model: p.model || null, group: providerGroup(p) }))
|
|
400
|
-
return [{ id: BUILTIN_ID, name: 'Anthropic (Claude)', model: null, group: BUILTIN_ID }, ...custom]
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Resolve an agent-facing provider reference without ever falling through to the
|
|
405
|
-
* built-in Claude login. Accept an exact id, a unique case-insensitive display
|
|
406
|
-
* name, or a unique configured model. Unknown/ambiguous references return null
|
|
407
|
-
* so callers can fail closed and show the current safe choices.
|
|
408
|
-
*/
|
|
409
|
-
export function resolveProviderRef(ref) {
|
|
410
|
-
if (!ref || ref === BUILTIN_ID) return BUILTIN_ID
|
|
411
|
-
const raw = String(ref).trim()
|
|
412
|
-
if (!raw) return BUILTIN_ID
|
|
413
|
-
const providers = loadProviders()
|
|
414
|
-
const exact = providers.find((provider) => provider.id === raw)
|
|
415
|
-
if (exact) return exact.id
|
|
416
|
-
const needle = raw.toLocaleLowerCase()
|
|
417
|
-
const matches = providers.filter((provider) => (
|
|
418
|
-
String(provider.name || '').trim().toLocaleLowerCase() === needle
|
|
419
|
-
|| String(provider.model || '').trim().toLocaleLowerCase() === needle
|
|
420
|
-
))
|
|
421
|
-
return matches.length === 1 ? matches[0].id : null
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* id → name map for the ROOM announce's per-lane provider projection (name-only,
|
|
426
|
-
* NEVER key or baseUrl). Built once per announce so a cross-device viewer resolves
|
|
427
|
-
* a lane's provider badge without the owner's account-channel registry. Built-in
|
|
428
|
-
* `anthropic` is omitted (the default path shows no badge). A stale/removed id has
|
|
429
|
-
* no entry; the caller falls back to the bare id (a random UUID — not key material).
|
|
430
|
-
* @returns {Record<string,string>}
|
|
431
|
-
*/
|
|
432
|
-
export function providerNameMap() {
|
|
433
|
-
const m = {}
|
|
434
|
-
for (const p of loadProviders()) if (p?.id && p.id !== BUILTIN_ID) m[p.id] = p.name
|
|
435
|
-
return m
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function keyHint(key) {
|
|
439
|
-
const k = String(key || '')
|
|
440
|
-
return k.length <= 4 ? k : k.slice(-4)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Resolve the spawn env for a provider id, matching provider.mjs conventions
|
|
445
|
-
* exactly (ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL).
|
|
446
|
-
* Built-in / unknown id → null (the caller leaves the default Claude env intact).
|
|
447
|
-
* @returns {{ANTHROPIC_BASE_URL:string,ANTHROPIC_AUTH_TOKEN:string,ANTHROPIC_MODEL?:string} | null}
|
|
448
|
-
*/
|
|
449
|
-
export function resolveProviderEnv(id) {
|
|
450
|
-
if (!id || id === BUILTIN_ID) return null
|
|
451
|
-
const p = loadProviders().find((x) => x.id === id)
|
|
452
|
-
if (!p) return null
|
|
453
|
-
// Keyless (loopback) provider: the SDK still needs a non-empty token, or it
|
|
454
|
-
// falls back to the host's Claude login against the localhost URL. Local
|
|
455
|
-
// servers (LM Studio / llama.cpp) accept any bearer value.
|
|
456
|
-
const env = { ANTHROPIC_BASE_URL: p.baseUrl, ANTHROPIC_AUTH_TOKEN: p.key || 'thinkpool-local' }
|
|
457
|
-
if (p.model) env.ANTHROPIC_MODEL = p.model
|
|
458
|
-
return env
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* The ONE model id a lane actually runs on — used for BOTH the display label and the
|
|
463
|
-
* SDK's `model` option, which is the whole point: they drifted, and the SDK option
|
|
464
|
-
* silently OVERRIDES the ANTHROPIC_MODEL env resolveProviderEnv() supplies.
|
|
465
|
-
*
|
|
466
|
-
* The bug this closes: a new lane inherits the focused lane's model (client-side), so
|
|
467
|
-
* opening a GLM lane while on Fable sent `model: 'claude-fable-5'` to
|
|
468
|
-
* https://api.z.ai/api/anthropic → `API Error: 400 [1211][Unknown Model]`, while the
|
|
469
|
-
* chip truthfully read `glm-4.6`. Label and wire disagreed; the wire lost.
|
|
470
|
-
*
|
|
471
|
-
* Rules (custom = any registered, non-built-in provider):
|
|
472
|
-
* • built-in anthropic / no provider → passthrough, unchanged. Default path is sacred.
|
|
473
|
-
* • custom + model equals the configured model → honour it.
|
|
474
|
-
* • custom + model is a Claude id (claudeish) → DROP it. It leaked across a provider
|
|
475
|
-
* boundary and the endpoint cannot serve it. Fall back to the configured model, or
|
|
476
|
-
* to undefined (no `model` option → the env / endpoint default decides).
|
|
477
|
-
* • custom + explicit non-Claude model (glm-4.5, deepseek-…) → honour it. This is a
|
|
478
|
-
* deliberate pick (/model, spawn_terminal({provider, model})), not an inheritance leak.
|
|
479
|
-
*
|
|
480
|
-
* NEVER returns an inherited Claude id on a custom provider — that is the invariant.
|
|
481
|
-
* Pure: `configuredModel` is passed in (not read from disk) so it is directly testable.
|
|
482
|
-
*
|
|
483
|
-
* @param {{provider?:string|null, model?:string|null, configuredModel?:string|null}} a
|
|
484
|
-
* @returns {string|undefined} the model to send; undefined → omit the SDK `model` option
|
|
485
|
-
*/
|
|
486
|
-
export function effectiveLaneModel({ provider, model, configuredModel } = {}) {
|
|
487
|
-
if (!provider || provider === BUILTIN_ID) return model || undefined
|
|
488
|
-
if (model && model === configuredModel) return model
|
|
489
|
-
if (model && !claudeish(model)) return model
|
|
490
|
-
return configuredModel || undefined
|
|
491
|
-
}
|
|
1
|
+
import e from"node:os";import r from"node:fs";import o from"node:path";import t from"node:crypto";import{claudeish as i}from"./event-id.mjs";import{DEFAULT_RESILIENCE_POLICY as n,resilienceProjection as a,resolveAttemptTarget as d,validateResiliencePolicy as l}from"./provider-resilience.mjs";const s=o.join(e.homedir(),".thinkpool-pair"),c=o.join(s,"providers.json"),u=o.join(s,"bridge-key.json");export const BUILTIN_ID="anthropic";function p(){try{r.mkdirSync(s,{recursive:!0,mode:448})}catch{}}export function loadProviders(){try{const e=JSON.parse(r.readFileSync(c,"utf8"));return Array.isArray(e)?e.filter(e=>e&&e.id&&e.id!==BUILTIN_ID):[]}catch{return[]}}function f(e){p(),r.writeFileSync(c,JSON.stringify(e,null,2),{mode:384});try{r.chmodSync(c,384)}catch{}}let m=null;export function ensureKeypair(){if(m)return m;p();try{const e=JSON.parse(r.readFileSync(u,"utf8"));if(e?.publicKeyB64&&e?.privateKeyPem)return m={publicKeyB64:e.publicKeyB64,privateKey:t.createPrivateKey({key:e.privateKeyPem,format:"pem"})},m}catch{}const{publicKey:e,privateKey:o}=t.generateKeyPairSync("rsa",{modulusLength:2048}),i=e.export({type:"spki",format:"der"}).toString("base64"),n=o.export({type:"pkcs8",format:"pem"});r.writeFileSync(u,JSON.stringify({publicKeyB64:i,privateKeyPem:n,createdAt:Date.now()},null,2),{mode:384});try{r.chmodSync(u,384)}catch{}return m={publicKeyB64:i,privateKey:o},m}export function publicKeyB64(){return ensureKeypair().publicKeyB64}export function bridgeHostId(e=publicKeyB64()){return t.createHash("sha256").update(e).digest("hex").slice(0,32)}export function seal(e,r){const o=r||publicKeyB64(),i=t.createPublicKey({key:Buffer.from(o,"base64"),format:"der",type:"spki"}),n=t.randomBytes(32),a=t.randomBytes(12),d=t.createCipheriv("aes-256-gcm",n,a),l=Buffer.concat([d.update(JSON.stringify(e),"utf8"),d.final()]),s=d.getAuthTag(),c={v:1,ek:t.publicEncrypt({key:i,padding:t.constants.RSA_PKCS1_OAEP_PADDING,oaepHash:"sha256"},n).toString("base64"),iv:a.toString("base64"),ct:Buffer.concat([l,s]).toString("base64")};return Buffer.from(JSON.stringify(c),"utf8").toString("base64")}export function unseal(e){const{privateKey:r}=ensureKeypair(),o=JSON.parse(Buffer.from(String(e||""),"base64").toString("utf8"));if(!(o&&1===o.v&&o.ek&&o.iv&&o.ct))throw new Error("bad envelope");const i=t.privateDecrypt({key:r,padding:t.constants.RSA_PKCS1_OAEP_PADDING,oaepHash:"sha256"},Buffer.from(o.ek,"base64")),n=Buffer.from(o.iv,"base64"),a=Buffer.from(o.ct,"base64");if(a.length<16)throw new Error("bad ciphertext");const d=a.subarray(a.length-16),l=a.subarray(0,a.length-16),s=t.createDecipheriv("aes-256-gcm",i,n);s.setAuthTag(d);const c=Buffer.concat([s.update(l),s.final()]);return JSON.parse(c.toString("utf8"))}export function isLoopbackHost(e){const r=String(e||"").toLowerCase().replace(/^\[|\]$/g,"");return"localhost"===r||"::1"===r||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(r)}export function addProvider(e){const r=function({name:e,baseUrl:r,key:o}){if(!e||"string"!=typeof e||!e.trim())return"name is required";let t;try{t=new URL(String(r))}catch{return"baseUrl must be a valid https url"}const i=isLoopbackHost(t.hostname);return"https:"===t.protocol||"http:"===t.protocol&&i?i||o&&"string"==typeof o&&o.trim()?null:"key is required":"baseUrl must be https (plain http is allowed only for localhost)"}(e||{});if(r)return{ok:!1,error:r};const o=loadProviders(),i=t.randomUUID();return o.push({id:i,name:String(e.name).trim(),baseUrl:String(e.baseUrl).trim(),model:e.model?String(e.model).trim():void 0,key:e.key?String(e.key).trim():"",addedAt:Date.now()}),f(o),{ok:!0,id:i}}export function addProviderModel({id:e,model:r,name:o}={}){if(!e||e===BUILTIN_ID)return{ok:!1,error:"the built-in Anthropic provider has no key to reuse"};if(!r||"string"!=typeof r||!r.trim())return{ok:!1,error:"model is required"};const i=loadProviders(),a=i.find(r=>r.id===e);if(!a)return{ok:!1,error:"no such provider"};const d=String(r).trim();if(i.some(e=>e.baseUrl===a.baseUrl&&(e.model||"")===d))return{ok:!1,error:`${d} is already registered on this provider`};const l=o&&String(o).trim()||`${function(e){return String(e||"").trim().split(/\s+/)[0]||"Provider"}(a.name)} ${d}`.trim(),s=t.randomUUID();return i.push({id:s,name:l,baseUrl:a.baseUrl,model:d,key:a.key,resilience:{...n,retryOn:[...n.retryOn],circuit:{...n.circuit},fallback:[]},addedAt:Date.now()}),f(i),{ok:!0,id:s}}export function removeProvider(e){if(!e||e===BUILTIN_ID)return{ok:!1,error:"the built-in Anthropic provider cannot be removed"};const r=loadProviders(),o=r.filter(r=>r.id!==e);return o.length===r.length?{ok:!1,error:"no such provider"}:(f(o),{ok:!0})}export function providerGroup(e){return e&&e.id!==BUILTIN_ID?t.createHash("sha256").update(`${e.baseUrl||""}\0${e.key||""}`).digest("hex").slice(0,12):BUILTIN_ID}export function sameProviderEnv(e,r){const o=y(e),t=y(r);if(o===BUILTIN_ID||t===BUILTIN_ID)return o===t;const i=loadProviders(),n=i.find(e=>e.id===o),a=i.find(e=>e.id===t);return!(!n||!a)&&providerGroup(n)===providerGroup(a)}const y=e=>e&&e!==BUILTIN_ID?String(e):BUILTIN_ID;export function providerModel(e){return e&&e!==BUILTIN_ID&&loadProviders().find(r=>r.id===e)?.model||null}export function setProviderResiliencePolicy(e,r){if(!e||e===BUILTIN_ID)return{ok:!1,error:"the built-in Anthropic provider cannot use resilience policy"};const o=loadProviders(),t=o.find(r=>r.id===e);if(!t)return{ok:!1,error:"no such provider"};const i="boolean"==typeof r?{...n,enabled:r,retryOn:[...n.retryOn],circuit:{...n.circuit},fallback:[]}:r,s=l(i,{providers:o,primaryProviderId:e});return s.ok?s.value.fallback.length?{ok:!1,error:"fallback is not available in Phase 1"}:s.value.enabled&&!d({providers:o,providerId:e,model:t.model})?{ok:!1,error:"an enabled resilience policy requires an exact configured model"}:(f(o.map(r=>r.id===e?{...r,resilience:s.value}:r)),{ok:!0,resilience:a(s.value)}):{ok:!1,error:s.error}}export function resolveProviderResiliencePolicy(e){if(!e||e===BUILTIN_ID)return null;const r=loadProviders(),o=r.find(r=>r.id===e);if(!o)return null;const t=l(o.resilience,{providers:r,primaryProviderId:e});if(!t.ok)return null;const i=d({providers:r,providerId:e,model:o.model});return i?{policy:a(t.value),target:i}:null}export function listProviders(){const e=loadProviders().map(e=>({id:e.id,name:e.name,model:e.model||null,group:providerGroup(e),keyHint:v(e.key),resilience:a(e.resilience)}));return[{id:BUILTIN_ID,name:"Anthropic (Claude)",model:null,group:BUILTIN_ID,keyHint:null,resilience:null},...e]}export function announceProviders(){const e=loadProviders().map(e=>({id:e.id,name:e.name,model:e.model||null,group:providerGroup(e)}));return[{id:BUILTIN_ID,name:"Anthropic (Claude)",model:null,group:BUILTIN_ID},...e]}export function resolveProviderRef(e){if(!e||e===BUILTIN_ID)return BUILTIN_ID;const r=String(e).trim();if(!r)return BUILTIN_ID;const o=loadProviders(),t=o.find(e=>e.id===r);if(t)return t.id;const i=r.toLocaleLowerCase(),n=o.filter(e=>String(e.name||"").trim().toLocaleLowerCase()===i||String(e.model||"").trim().toLocaleLowerCase()===i);return 1===n.length?n[0].id:null}export function providerNameMap(){const e={};for(const r of loadProviders())r?.id&&r.id!==BUILTIN_ID&&(e[r.id]=r.name);return e}function v(e){const r=String(e||"");return r.length<=4?r:r.slice(-4)}export function resolveProviderEnv(e){if(!e||e===BUILTIN_ID)return null;const r=loadProviders().find(r=>r.id===e);if(!r)return null;const o={ANTHROPIC_BASE_URL:r.baseUrl,ANTHROPIC_AUTH_TOKEN:r.key||"thinkpool-local"};return r.model&&(o.ANTHROPIC_MODEL=r.model),o}export function effectiveLaneModel({provider:e,model:r,configuredModel:o}={}){return e&&e!==BUILTIN_ID?r&&r===o||r&&!i(r)?r:o||void 0:r||void 0}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createHash as e}from"node:crypto";import{existsSync as r,readFileSync as i}from"node:fs";import{dirname as o,join as t,resolve as n}from"node:path";import{fileURLToPath as s}from"node:url";const a=o(s(import.meta.url));function f(e,r){try{return JSON.parse(i(e,"utf8"))}catch{throw new Error(`${r} is missing or invalid`)}}function c(r){return e("sha256").update(i(r)).digest("hex")}export function verifyPublishArtifact(e=a){const i=f(t(e,"package.json"),"package.json");if("minified"!==i.thinkpoolArtifact?.kind||1!==i.thinkpoolArtifact?.format)throw new Error("raw bridge source cannot be packed or published; run node build-package.mjs first");const o=i.thinkpoolArtifact.manifest,n=f(t(e,o),o);if("minified"!==n.kind||1!==n.format||n.package!==i.name||n.version!==i.version)throw new Error("minified package manifest does not match package identity");if(!n.minifiedModules||n.artifactModuleBytes>=n.sourceModuleBytes)throw new Error("package manifest does not prove a smaller minified module set");for(const[i,o]of Object.entries(n.files||{})){const n=t(e,i);if(!r(n))throw new Error(`manifest file is missing: ${i}`);if(c(n)!==o)throw new Error(`manifest hash mismatch: ${i}`)}const s=new Set(["package.json",...(i.files||[]).filter(e=>e!==o)]),m=new Set(Object.keys(n.files||{}));for(const e of s)if(!m.has(e))throw new Error(`published file is not integrity-bound: ${e}`);for(const e of m)if(!s.has(e))throw new Error(`manifest contains an undeclared file: ${e}`);for(const e of i.files||[])if(e.endsWith(".map"))throw new Error(`source map cannot be published: ${e}`);return{pkg:i,manifest:n}}if(process.argv[1]&&n(process.argv[1])===s(import.meta.url))try{const{pkg:m,manifest:d}=verifyPublishArtifact();process.stderr.write(`PUBLISH GUARD: PASS — ${m.name}@${m.version}, ${d.minifiedModules} minified modules\n`)}catch(p){process.stderr.write(`PUBLISH GUARD: BLOCKED — ${p.message||p}\n`),process.exitCode=1}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": 1,
|
|
3
|
+
"package": "thinkpool-pair",
|
|
4
|
+
"version": "0.7.366",
|
|
5
|
+
"kind": "minified",
|
|
6
|
+
"minifiedModules": 111,
|
|
7
|
+
"sourceModuleBytes": 1620861,
|
|
8
|
+
"artifactModuleBytes": 719348,
|
|
9
|
+
"files": {
|
|
10
|
+
"package.json": "0229a6f617212531ad287ab4bfc00aecca97f8296a812dafcc666fd560dad051",
|
|
11
|
+
"LICENSE": "95f1ba638272b3c5f26c36b45bbd8a951b545a091e2c9710fb47f2d68a83a001",
|
|
12
|
+
"bridge.mjs": "9cddacbf7a91fddd8a7bc57ea2f6f9ce1ec442a21bafd948202fd09b188b00b1",
|
|
13
|
+
"bridge-service.sh": "b64f27c473ca8f3a49190fd08c23947ab14ca9958d70b6fa53c7bddc2be91403",
|
|
14
|
+
"command-guidance.mjs": "b9b967b59ff4e77c9613abb0f28858bf3ac5b106a509e471039777b53bec3e29",
|
|
15
|
+
"abort-turn-barrier.mjs": "3734a5b7e2e940ec6d7fa8b5b04935e1429a23d1bd9e92b2ac2a2ecd9f77c75e",
|
|
16
|
+
"host-memory.mjs": "0fab9118b1a53e6939dd047140a5e181d3fc98f5f8faa2b49b1ee04fd148f650",
|
|
17
|
+
"keep-awake.mjs": "af39f17c0279ddacffbbe52443231184d30fc4a5a579692898689b80e8189d67",
|
|
18
|
+
"sdk-smoke.mjs": "dfe509863ede8a12780914939740f7f9fee36d3980f8fcb57803a33593ac4056",
|
|
19
|
+
"sdk-admission.mjs": "eb33725cc33347b900c75289eae7917f6ce3526203711515aecd48d43b3c895d",
|
|
20
|
+
"launcher.mjs": "1723ab61347bd160479da520a5a18f99c8af1dc5a18fbd3f6ca03b97b16ce8e4",
|
|
21
|
+
"privacy-report.mjs": "2299c607b9872d8d3f563ad14ba3a7b75ea1fb5292e7b6ef756d62f26f2362ec",
|
|
22
|
+
"agent-visibility.mjs": "624448728a596fea3a2f01fc686db78716cff20429b8ddbfaa4fb7eb05027870",
|
|
23
|
+
"byok-detect.mjs": "61befb5e53427b558cb911d35ad69fab2458de93dc34f650b7c56ca75d387bc9",
|
|
24
|
+
"context-contract.mjs": "ffebb435dc5d75c289329bf16ab909df9d634a2b508154c250c6b25906a02a91",
|
|
25
|
+
"context-windows.mjs": "80af664f84d7e95ef2b0a2a3131090ac4ccd522709448ac9a0d96c378bbec0f7",
|
|
26
|
+
"claude-session.mjs": "d0b25010078ed642ee7f1149b3f171ba9ff165eaa6ad864f060410682d7e94f8",
|
|
27
|
+
"terminal-name.mjs": "3e114c771abdf42d4ba43d280d01a0d212267f122e3d6d793e3f4e52d052e66a",
|
|
28
|
+
"claude-command-catalog.mjs": "36bd4342b2ae94b43a0072533ef2d1648c7473003964bbad433fdb1a8b2bfec1",
|
|
29
|
+
"codex-session.mjs": "dc4029720e1787e334fc8c81672514c9f830201bef489564eb39f4720e6c3f73",
|
|
30
|
+
"question-response.mjs": "35a7ba53d3eb117717c18004d2a8441f8f37665b231a3c44f4bd9321ff59dd57",
|
|
31
|
+
"thinkpool-room-prompt.mjs": "d9abf116d457043833a8afdb7755a2d03426362f1858053f8615ebf46ed10e34",
|
|
32
|
+
"thinkpool-prompt-contracts.mjs": "8e47f2b7928dc99b85596f85b8ca1c26e9ee99b985405989e64cb9752ce67e9c",
|
|
33
|
+
"thinkpool-capabilities.json": "d0c138387d3cc48f5567480457d10659fd2b20a9445693183ae0f6958a4956bd",
|
|
34
|
+
"codex-app-server.mjs": "f9c2bad1eaf79420d87e12c256bc645ab48083c0b89d252e4968fd176f4bdfc3",
|
|
35
|
+
"codex-images.mjs": "31605d6d8b3dd6b73395da63d1b0c513445c95ffa2f4306ee466f201d633ffc0",
|
|
36
|
+
"codex-mcp-http.mjs": "7472543648a9bb575ce20dc3e3270c2da84174c6ea6e9faf87dfaafb95b6212a",
|
|
37
|
+
"lane-worktree.mjs": "37583757ab82843412ba565dfba3d7cab517e94782eb33115decd5438d092d8f",
|
|
38
|
+
"codex-event-mapper.mjs": "88d6b085170de748ccde943e5b596f0240ffe4e5c4bd64705e7e8dce8775b549",
|
|
39
|
+
"edit-diff.mjs": "9a1bef6bb2d853d630ad38aba374cb2a120dd75b9aa08ab42ffc83cb0e1c57b6",
|
|
40
|
+
"cumulative-event-relay.mjs": "cf6e6533e33b3c5bb0b462d15ccc86106f2b56f8b1e66fc1f4867dd8f44d7ae6",
|
|
41
|
+
"codex-commands.mjs": "cae3f617ac42e1316d8980ef97e4255227fe5df5e8e9b556630a3bfb83bb7c2b",
|
|
42
|
+
"acp-client.mjs": "9e7919ff79825883820019ff7a0ca578f120853ed61c7caab21a8ffc3a67ffc9",
|
|
43
|
+
"hermes-session.mjs": "18cb3fc87338724abb75de2a7ebd8e53fa2902f995dca1f93d2f26b08f3cd331",
|
|
44
|
+
"hermes-model-cache.mjs": "aee23efdebe1aa5e10053f291a9153981fe9b45065c293c9519ca73477251e72",
|
|
45
|
+
"hermes-policy.mjs": "c0e59a65555c7ac25fdaacf6d72c9e8153803f9115eaaeb5ff802560f3a64cbc",
|
|
46
|
+
"hermes-acp-bootstrap.py": "7c3c8114b1da3ea28320eaec9f9851b42d2359d76488e8dbf484871187c68850",
|
|
47
|
+
"hermes-event-mapper.mjs": "7c714e15d06d970c4d602e755461bacf8f4d4136a8afc03406e8b3207ff84cdc",
|
|
48
|
+
"hermes-probe.mjs": "935a80f2633afc4fb65d2610f662d650addde6b4086aaeac8bcbcf7932c26819",
|
|
49
|
+
"hermes-setup.mjs": "3198a2a21282f4694c9ef3bc46e97fa4f467b721c525db0af584d50f34dd570c",
|
|
50
|
+
"hermes-isolation.mjs": "ea9594480a8f470dc66bdcd1907b00c016f52398d9c39c8547cab95e5fb44f86",
|
|
51
|
+
"hermes-delegation-guard.mjs": "4e6dd8d0cddefaddb30f83769acda72242c9b7242671f23ca2cb0c242dace178",
|
|
52
|
+
"runtime-registry.mjs": "4606e104310a755be6a8c587e1050e8b0aa5efe56e7b71f9f13983f2685fc31b",
|
|
53
|
+
"runtime-contract.mjs": "eb24b177002ba33353ec0335f4f830fb57fe313552b1a8d7aef8163cc743f6c1",
|
|
54
|
+
"command-catalog.mjs": "8bdeba1dda874c68f4fa273cb03e6c634dfa22ffae88dec582398220ed2be8bf",
|
|
55
|
+
"repo-search.mjs": "10cbce1d86655a7add0d7350ea8862a57165ccb02c2afa406348f4669050c304",
|
|
56
|
+
"git-diff-report.mjs": "a428b3129c90e207a125a86c4485ec16c8ee7e81d432060a070e37172d85ce7a",
|
|
57
|
+
"runtime-session.mjs": "1a149012375047f0c031e210cba973e64ce2e6de427a951dfc937fa7c3c402ee",
|
|
58
|
+
"turn-stall.mjs": "dee2717afb5f3f474c8a61c1c140ad6fc436e96a0fde6cfa12aff49a10eedff9",
|
|
59
|
+
"update-gate.mjs": "f223a82e72a2a33b48b8321b508e0965f9c705b5722a80a3c199ebd5cf2fcdcf",
|
|
60
|
+
"agent-notify.mjs": "5950ba8928edb6e8d6091a0fdc8fb62cb21fcb79d864504e3b56e012a6f8339a",
|
|
61
|
+
"agent-detect.mjs": "f38ede090e9caa8f7cde0febc41d9dc2c4301e1cd6017a07517b96364ebafb8c",
|
|
62
|
+
"code-event-contract.mjs": "0a092608d85442064ed6b56cd28ea7b28d2cd49f37e8ea6bc5dc3adeb43be833",
|
|
63
|
+
"evidence-compact.mjs": "f8175315f17b46a42f7b396f081374ff3d39ead94c18d64fccfa5fd02b6679f0",
|
|
64
|
+
"evidence-citations.mjs": "865032925532c0f7a4b3f0ed484c85fa53237a7f46d79640714cd483b493b98d",
|
|
65
|
+
"error-recovery.mjs": "0ace80add28fa72b49a3f3a716f247e3ef9816671c09749b3863867700beae32",
|
|
66
|
+
"pair-control-authority.mjs": "7a95f101153dcc6dd0633b5a01595bf2979cd0c4bf50cfc6cd44bb7612ef40a5",
|
|
67
|
+
"scheduled-runs.mjs": "6e6c3fe3e0d976ad3038df948017c9955fc211b39ab2a58daecca6487baf2c59",
|
|
68
|
+
"scheduled-run-admission.mjs": "3a2098d904c3aff571bf314e0182d5636d25d242cef52207e70f9dc1618c94f5",
|
|
69
|
+
"event-id.mjs": "9ebd6dc404cc8c799c660b3d1fb397bd39a6181128d3a91f7bab5f42c55d4e9d",
|
|
70
|
+
"event-bounds.mjs": "aa8eb8517c4aa81b3378f71401ed39c550cdc80e28e3a57ab46d0a5ef5c7c9c1",
|
|
71
|
+
"replay-transport.mjs": "f4ed49dc023e3bb2d9534814bc2684121caafe4d24151b87688407eda47c6c7a",
|
|
72
|
+
"event-delivery-queue.mjs": "c596e298384e0e4890bccdc07f622e1807d1784e19b65f0ef5b7b1bfea0c41d4",
|
|
73
|
+
"mcp-flight-recorder.mjs": "46e3bf7b04212fe69819e38e377c4239caf8b1552ac7bf4905abddd66385065c",
|
|
74
|
+
"plan-meters.mjs": "f7f5aeb3d6a3462b754d3724367bbb331643ba58108e29e91b2aca2f613ab499",
|
|
75
|
+
"recap.mjs": "b2eee701ab8646007670514b08103e7b56b90eaa7d119e765b563a8ede87f85f",
|
|
76
|
+
"transcript-sanitize.mjs": "c7f4718f25f97c05bc7b4ad8e09f18cbda152c53ad6593af1ab99f0c132dbf01",
|
|
77
|
+
"session-store.mjs": "adeb5d7fe8b18b10e5c795877f797696bbb4f2f222916710173ada32bd433c1e",
|
|
78
|
+
"side-lane.mjs": "e70871b0c3d0ef0037ffc85dcdda888984dc907e88a2506620cad0c3f76921bf",
|
|
79
|
+
"worker-completion.mjs": "c456b78eed9847a31003937baa23f259e28b11209674402a462e20c59712a66c",
|
|
80
|
+
"cross-terminal.mjs": "e1404c47bcd437cd32d5ed7766e1cab2a2e8a9d19e39f677bbc41afcc514edd1",
|
|
81
|
+
"pair-bus.mjs": "cbc28cee7f4f06b2213091e0b7b07d61c1d01f0ff207c5c6d808cd616ece221e",
|
|
82
|
+
"direct-pair-room.mjs": "92557b54307da2ca31f0a0d090e44b019a23b1ab32a7dfd6cbcb9d55fd761736",
|
|
83
|
+
"lane-lifecycle.mjs": "9b5ae3d105c30e3ad37f1f1f792200da2dd4d2bb5c4979a2a69618373e58f1c8",
|
|
84
|
+
"lane-continuation.mjs": "b12ee9bb2b8625635c9fc20327acefd4c3ed41e716fdf212d0f7f53f514f8e93",
|
|
85
|
+
"interrupted-resume.mjs": "df46a5fa07fad734dc3a56643aa44e24121e4afdd8206cc5e123102dad87f901",
|
|
86
|
+
"dispatch-lease.mjs": "6e951cad5fb02999ac27e1cf03d9f932b079b2184853b8c6f9b5a8d579fcbbdd",
|
|
87
|
+
"dispatch-permission-cleanup.mjs": "e9793c6f1aab616ad6cf0bf78cd68630428b511f1fdc5c66bb12c68a9932fe26",
|
|
88
|
+
"flow-conductor.mjs": "a2b034a690e57869b3606cf61ae9029da422ed892032be7614bb76488a47da25",
|
|
89
|
+
"flow-worktree.mjs": "d9579a9b2eb8eff46c0708b2e3f546f5a6a3cded1ff726ab334cac9a48544e90",
|
|
90
|
+
"flow-task-graph.mjs": "52f555219c6c940c5d33c6c429d132ab2ded46f9ac6d5207bd5ad972067493bf",
|
|
91
|
+
"flow-models.mjs": "5796e23b8287d6b54bfe9dddfc30c9ef73d715dc322f54c73f2a30a523210021",
|
|
92
|
+
"flow-host-revert.mjs": "a6da239fc54723c74ed51fc03ae3203458b92d2a2e33c26576a9e62cd1031fea",
|
|
93
|
+
"flow-preview.mjs": "b4f1fb6aeed49623c0264ef47d02ec0b244c055ba4386f5ba4c1ffdcd02824c5",
|
|
94
|
+
"mockup-delivery.mjs": "fa112408a186d856b343157381188dc18502aba6cf7ef365459f6c5a074e9cd6",
|
|
95
|
+
"viewport.mjs": "80cb9b6e9275499397239a8b4288c0b49791886696a46917ac798acb75267ee5",
|
|
96
|
+
"design-edit.mjs": "eeafab61056512338a18ed3f2e0339c9315f2fbf0d58456ffe9be08d77dd42b3",
|
|
97
|
+
"design-source-contract.mjs": "18bcd024b317293e445c433fc9c59f9eb85e8fb3fa4de824b82604bbae4687b8",
|
|
98
|
+
"flow-review.mjs": "3d53a8a385dd1360d23c994847a7a3186e93df4a455c8dded1cffeeedbbac378",
|
|
99
|
+
"flow-receipt.mjs": "9fc2804b0a6940d605449818d69f46f19fa2d83dbe6ba29caeaaf0b6694e86aa",
|
|
100
|
+
"flow-scope-evidence.mjs": "37983eeee599380d47ef7dacc5d22349ce45dea712442e822c896f9cec9450fc",
|
|
101
|
+
"past-work-search.mjs": "766f4e669364bd9c3c3667135c502719dbbaf550bf074bda4394ab106f33f415",
|
|
102
|
+
"review-check.mjs": "e16c45fcbbe731b67c5aa1796daffc711e4a5734dbedda282a5e01db0602722e",
|
|
103
|
+
"flow-review-gate.mjs": "0ef351065269bf2cc6112707064024cf2b916890a80fb94404ebca3a7905754b",
|
|
104
|
+
"flow-review-reflect.mjs": "61b39e32ad21501b76c1d9267dd5cdf35017afe7e1333b305e72392bc0304628",
|
|
105
|
+
"flow-assembly.mjs": "3535203e1ad75fb326db5212c2f013ab6c0049e31bc0ba59813c7d0ef1956d03",
|
|
106
|
+
"flow-budget.mjs": "d406ecbdd5d4acc08c60c641d7ad12553675cf4bbf35a9600bc96300c1af3404",
|
|
107
|
+
"flow-context-store.mjs": "949c20aeac1984d265f0445547a7d248565e61270cf9fed509047a1dd2c44f02",
|
|
108
|
+
"flow-redispatch.mjs": "4fa3bf3dd9461fa1c447d12b5db5a0f1befb3427abaed1ee1cec86134ad3eb95",
|
|
109
|
+
"flow-skill-registry.mjs": "33de95aab75cb70bc145da065a460ec6351c46c7d5bbdfe73650258943cc570f",
|
|
110
|
+
"serve-dir.mjs": "1ab4cbdff02ddfd726cde4a021503ec2f24b47091e9950237d42d5827e5674ce",
|
|
111
|
+
"serve-consent.mjs": "f6c5e9f42f452525a83ed5005bc9e65156ddec6fb2cd83deccc81b86f20c6825",
|
|
112
|
+
"reap-terminal.mjs": "76d6ed14316d26d0d8b246d54cfdbde1f246badee91524b4caca8151f1440ccd",
|
|
113
|
+
"terminal-row-reconcile.mjs": "b6f4022f03d908445f404ca1f8479c780ed645bf855428a2726ac2db682ab4ed",
|
|
114
|
+
"switch-provider.mjs": "9805d59df49e1272ffd5ef7e3d2260f6e93c76d32265b78e461287ef500f5b71",
|
|
115
|
+
"service.mjs": "4d649a995d8fee138d9714dcfd41fdcfee5b5148e6b2b0328dd31a797cc134fc",
|
|
116
|
+
"supervisor-ready.mjs": "7d5e4ebe3c7a93e0888dd335fb4556a670e55a80a89b861c8a44feac159cd840",
|
|
117
|
+
"presence.mjs": "2516ebb4474a8f45dd881530335c2174af9cde44be8eed6896ff8c62497b6e2b",
|
|
118
|
+
"account.mjs": "a8bc640fe66b179777597477899ec4eb82460f602d637ca477ab76eb682eb233",
|
|
119
|
+
"auth-store.mjs": "c4d41ee7cdc34c2002c6dd50d32e53ace0a3f03a116ffa930ed94a437e247625",
|
|
120
|
+
"key-shape.mjs": "a4113293d52eeba39b05e70a3e0345abc1ddc181b4f236b413bde0ae11a3f646",
|
|
121
|
+
"supabase-key.mjs": "041bde6cf50b15f6426daaf738374abc9647861d8c1dd3d85cedc45684271888",
|
|
122
|
+
"provider.mjs": "5a96b83d8aa65fb344c8e8ec5ffcb9cc360497925120cdf7b4ad26416d284790",
|
|
123
|
+
"providers.mjs": "c0757433d30bf2f22e80f85e779cdeeeb10b412d568965f1115d4de2c8aa0314",
|
|
124
|
+
"provider-resilience.mjs": "a5a3921040d1860bb82f1d3b53c6ec25a5e230345c0131f2213c535e07fc497e",
|
|
125
|
+
"model-prices.mjs": "91f0bd8688f59ce6bff7743710d3b1139f4f0823d6b95fa68cd9b6fdcdba6ab4",
|
|
126
|
+
"README.md": "b801ce3677409cd8f99dacef2f5a9d69487db6d3146d41381c70b82f5d04eed4",
|
|
127
|
+
"publish-guard.mjs": "569dd788790e68ea822129f588fd16c1bb435324666552fe9dfe7c435ab3f617"
|
|
128
|
+
}
|
|
129
|
+
}
|