@khorsheed/dsh-local-agent-codex 0.2.0-rc.1 → 0.2.0-rc.3

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.
@@ -16,10 +16,11 @@
16
16
  import { randomUUID } from 'node:crypto';
17
17
  import { createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm';
18
18
  import { SessionId } from '@deepseek-ai/dsh-session';
19
- import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
19
+ import { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
20
20
  import { assertResumeCwdUnchanged, assertResumeScopeUnchanged, resolveRoundModel, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, persistChildSession, probeCliVersion, resolveChildCwd, subagentDelegationLabel, } from '@khorsheed/dsh-local-agent';
21
21
  import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV } from '@khorsheed/dsh-local-agent/types';
22
22
  import { LiveChannelUnavailableError } from './live-driver.js';
23
+ import { endpointAuthEnv, endpointConfigOverrides, hasEndpointCredential } from './endpoint-auth.js';
23
24
  import { readCodexBaseUrl } from './provision.js';
24
25
  import { codexRolloutRoundFacts, usageFromCodex } from './records.js';
25
26
  /** Quote one TOML basic string for the `-c` config override. */
@@ -116,6 +117,7 @@ export class CodexCliProvider {
116
117
  live;
117
118
  model;
118
119
  overrides;
120
+ endpoint;
119
121
  name = 'codex-local';
120
122
  capabilities = NO_START_CAPABILITIES;
121
123
  inheritsParentContext = false;
@@ -126,22 +128,33 @@ export class CodexCliProvider {
126
128
  * while a retiring generation still hosts it).
127
129
  * @param model - resolver for the configured model, read PER ROUND so a
128
130
  * settings-card write takes effect on the next delegation without a
129
- * reload. Undefined (the resolver absent, or returning undefined) leaves
130
- * the argv exactly as it was before the key existed — the scoped
131
- * config.toml's own `model` then decides, as it always did.
131
+ * reload; it receives the round's scope (a named scope's curated list
132
+ * answers first). Undefined (the resolver absent, or returning
133
+ * undefined) leaves the argv exactly as it was before the key existed —
134
+ * the scoped config.toml's own `model` then decides, as it always did.
132
135
  * @param overrides - resolver for the member's session-level model override
133
136
  * (the composer picker, owned by the model broker). It outranks BOTH the
134
137
  * delegation's recorded model and the configured one: on the exec path it
135
138
  * re-points the next round's `-m`, and on the live path it is the member's
136
139
  * start model — a resident runtime bound to a different model is retired
137
140
  * so the round respawns onto the override.
141
+ * @param endpoint - resolver for the scope's endpoint credential, read PER
142
+ * ROUND so a settings-card write takes effect on the next delegation
143
+ * without a reload. A resolution carrying a key routes the round at the
144
+ * relay (a custom `model_providers` entry on the argv, the key in the
145
+ * env); with no key the spawn carries no credential at all.
138
146
  */
139
- constructor(ctx, sandbox = 'workspace-write', live, model, overrides) {
147
+ constructor(ctx, sandbox = 'workspace-write', live, model, overrides, endpoint) {
140
148
  this.ctx = ctx;
141
149
  this.sandbox = sandbox;
142
150
  this.live = live;
143
151
  this.model = model;
144
152
  this.overrides = overrides;
153
+ this.endpoint = endpoint;
154
+ }
155
+ /** The scope's endpoint credential for this round; absent resolver = never. */
156
+ resolveEndpoint(homeDir) {
157
+ return this.endpoint === undefined ? Promise.resolve(undefined) : this.endpoint(homeDir).catch(() => undefined);
145
158
  }
146
159
  /** Resolve the live driver for one round's member, if live is on for it. */
147
160
  liveDriver(childSessionId) {
@@ -198,7 +211,7 @@ export class CodexCliProvider {
198
211
  ...record?.configurationLock === undefined ? {} : { configurationLock: record.configurationLock },
199
212
  }, configuration => this.startCodexResume(request, intent, cwd, homeDir, exec, scope, record?.model, configuration), request.signal, intent?.onAdmitted);
200
213
  }
201
- const childSessionId = SessionId(intent?.preparedMemberId ?? randomUUID());
214
+ const childSessionId = SessionId(intent?.preparedMemberId ?? intent?.childSessionId ?? randomUUID());
202
215
  if (typeof this.ctx.localAgent.withMemberConfigurationRound !== 'function') {
203
216
  if (intent?.effort !== undefined)
204
217
  throw new Error('Codex effort requires the configuration admission core');
@@ -231,14 +244,20 @@ export class CodexCliProvider {
231
244
  },
232
245
  });
233
246
  const harness = this.ctx.localAgent.get('codex');
234
- const label = subagentDelegationLabel(harness?.displayName ?? 'codex', request.descriptor.label);
247
+ // Host 0.2.1 no longer resolves a descriptor onto the request (the
248
+ // runtime's one-shot snapshot is gone), so the provider stamps the same
249
+ // payload itself; the activation spec's required label arrives as the
250
+ // empty string when the caller named none.
251
+ const label = subagentDelegationLabel(harness?.displayName ?? 'codex', request.label || undefined);
235
252
  childSession.append('subagent/descriptor', {
236
- ...request.descriptor,
253
+ version: SUBAGENT_DESCRIPTOR_VERSION,
254
+ mode: 'one-shot',
255
+ provider: this.name,
237
256
  label,
238
257
  });
239
- // The official runtime appends the parent-side subagent/catalog
240
- // discovery row only for in-process children (run.localAgent); this run
241
- // is remote, so the provider appends it here — once per child, where
258
+ // The 0.2.1 manager appends the parent-side subagent/catalog discovery
259
+ // row only for parent-delivered children; the family runs caller
260
+ // delivery, so the provider appends it here — once per child, where
242
261
  // the descriptor lands. A failure degrades to the warn below; a missing
243
262
  // row never blocks the delegation.
244
263
  establishSubagentCatalogChild(request.parent.session, childSession.header, label);
@@ -251,11 +270,15 @@ export class CodexCliProvider {
251
270
  catch (error) {
252
271
  this.ctx.logger.warn(`subagent-codex: subagent session record failed: ${error instanceof Error ? error.message : String(error)}`);
253
272
  }
254
- // Resolve the effective custom endpoint from the scoped config.toml for
255
- // diagnostics: codex reads it directly (a user manually editing the
256
- // config to route through a custom provider is authoritative). Logged
257
- // BEFORE the drive-mode branch so live rounds report their endpoint too.
258
- const baseUrl = await readCodexBaseUrl(homeDir).catch(() => undefined);
273
+ // The round's endpoint: a configured endpoint credential (the scope's
274
+ // endpoint-auth.json over the plugin config) routes the argv through a
275
+ // custom provider entry and its key rides the env. With no credential the
276
+ // scoped config.toml's own custom provider answers for the diagnostic
277
+ // exactly as before (a user-edited value is authoritative). Logged BEFORE
278
+ // the drive-mode branch so live rounds report their endpoint too.
279
+ const endpoint = await this.resolveEndpoint(homeDir);
280
+ const routed = hasEndpointCredential(endpoint);
281
+ const baseUrl = routed ? endpoint.baseUrl : await readCodexBaseUrl(homeDir).catch(() => undefined);
259
282
  this.ctx.logger.info(`subagent-codex: delegating via ${baseUrl ?? 'codex default endpoint'}`);
260
283
  // Live driver: the round goes to the resident app-server process (one per
261
284
  // member). A channel that fails at spawn/handshake falls through to the
@@ -314,11 +337,12 @@ export class CodexCliProvider {
314
337
  try {
315
338
  const run = await startCodexCliRun(request, {
316
339
  cwd,
317
- env: delegationEnv({ CODEX_HOME: homeDir }),
340
+ env: delegationEnv({ CODEX_HOME: homeDir, ...endpointAuthEnv(endpoint) }),
318
341
  ...exec === undefined ? {} : { exec },
319
342
  endpointLabel: baseUrl,
343
+ endpointArgv: endpointConfigOverrides(endpoint),
320
344
  sandbox: this.sandbox,
321
- ...(configuration?.resolved ?? resolveRoundModel(this.overrides?.(runId) ?? requestedModel, this.model)),
345
+ ...(configuration?.resolved ?? resolveRoundModel(this.overrides?.(runId) ?? requestedModel, () => this.model?.(scope))),
322
346
  disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
323
347
  spawn: spec => this.ctx.subprocess.spawn(spec),
324
348
  onError: (error, stopReason) => {
@@ -383,7 +407,11 @@ export class CodexCliProvider {
383
407
  throw new Error(`subagent-codex: resume target child session ${intent.childSessionId} is not live — start a fresh delegation instead`);
384
408
  }
385
409
  const nextTurn = childSession.snapshotEvents().filter(event => event.type === 'turn/start').length + 1;
386
- const baseUrl = await readCodexBaseUrl(homeDir).catch(() => undefined);
410
+ // Same endpoint precedence as the fresh path: a configured endpoint
411
+ // credential routes the round at the relay, else the scoped
412
+ // config.toml's own custom provider answers for the diagnostic.
413
+ const endpoint = await this.resolveEndpoint(homeDir);
414
+ const baseUrl = hasEndpointCredential(endpoint) ? endpoint.baseUrl : await readCodexBaseUrl(homeDir).catch(() => undefined);
387
415
  // Logged BEFORE the drive-mode branch so live rounds report their endpoint too.
388
416
  this.ctx.logger.info(`subagent-codex: resuming via ${baseUrl ?? 'codex default endpoint'}`);
389
417
  // Live driver: continue the member's resident app-server thread. Channel
@@ -423,11 +451,12 @@ export class CodexCliProvider {
423
451
  try {
424
452
  run = await startCodexCliRun(request, {
425
453
  cwd,
426
- env: delegationEnv({ CODEX_HOME: homeDir }),
454
+ env: delegationEnv({ CODEX_HOME: homeDir, ...endpointAuthEnv(endpoint) }),
427
455
  ...exec === undefined ? {} : { exec },
428
456
  endpointLabel: baseUrl,
457
+ endpointArgv: endpointConfigOverrides(endpoint),
429
458
  sandbox: this.sandbox,
430
- ...(configuration?.resolved ?? resolveRoundModel(this.overrides?.(intent.childSessionId) ?? requestedModel, this.model)),
459
+ ...(configuration?.resolved ?? resolveRoundModel(this.overrides?.(intent.childSessionId) ?? requestedModel, () => this.model?.(scope))),
431
460
  disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
432
461
  spawn: spec => this.ctx.subprocess.spawn(spec),
433
462
  onError: (error, stopReason) => {
@@ -697,7 +726,9 @@ export function startCodexCliRun(request, spec) {
697
726
  const turn = spec.resume?.turn ?? 1;
698
727
  // The member bridge rides a per-process `-c` override placed right after
699
728
  // `exec` (the spike-verified position; `codex exec resume` accepts it too).
729
+ // The endpoint credential's overrides sit next to it — `-c` pairs commute.
700
730
  const memberArgv = spec.member === undefined ? [] : ['-c', spec.member.configOverride];
731
+ const endpointArgv = spec.endpointArgv ?? [];
701
732
  // `--skip-git-repo-check` rides every exec argv. Codex refuses to start
702
733
  // outside a Git repository ("Not inside a trusted directory and
703
734
  // --skip-git-repo-check was not specified") and exits before producing a
@@ -718,8 +749,8 @@ export function startCodexCliRun(request, spec) {
718
749
  ...spec.effort === undefined ? [] : ['-c', `model_reasoning_effort=${JSON.stringify(spec.effort)}`],
719
750
  ];
720
751
  const argv = spec.resume === undefined
721
- ? ['codex', 'exec', ...memberArgv, ...modelArgv, '--sandbox', spec.sandbox, '--skip-git-repo-check', '--json', task]
722
- : ['codex', 'exec', ...memberArgv, ...modelArgv, '--sandbox', spec.sandbox, '--skip-git-repo-check', '--json', 'resume', spec.resume.cliSessionId, task];
752
+ ? ['codex', 'exec', ...memberArgv, ...endpointArgv, ...modelArgv, '--sandbox', spec.sandbox, '--skip-git-repo-check', '--json', task]
753
+ : ['codex', 'exec', ...memberArgv, ...endpointArgv, ...modelArgv, '--sandbox', spec.sandbox, '--skip-git-repo-check', '--json', 'resume', spec.resume.cliSessionId, task];
723
754
  // Container target: the same argv, wrapped in `docker exec`. The host cwd
724
755
  // still applies — it is the docker CLIENT's working directory now, while
725
756
  // the CLI's own is the target's in-container workdir.
@@ -911,10 +942,12 @@ export function startCodexCliRun(request, spec) {
911
942
  return mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
912
943
  }, () => { });
913
944
  return Promise.resolve(subprocessRunHandle({
914
- // A session-backed run's id is the child session id (the seam's local-run
915
- // contract), so the tool's resume self-description names the same handle
916
- // the delegation registry records.
917
- id: spec.childSession?.id ?? SessionId(randomUUID()),
945
+ // Host 0.2.1: the published run id is a fresh PER-EXECUTION identity —
946
+ // the activation manager rejects a run id naming a live Session (the
947
+ // mirrored child session is exactly that), and resume rounds reuse the
948
+ // child session id. The durable child identity the family tracks rides
949
+ // the staged intent instead (spec.childSession.id == its childSessionId).
950
+ id: SessionId(randomUUID()),
918
951
  result,
919
952
  signal: request.signal,
920
953
  onAbort,
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Endpoint-key authentication — the codex harness's ONLY auth mode. There is
3
+ * no web login anywhere in this package: a delegation round reaches its
4
+ * endpoint with a custom `model_providers` entry on the argv pointing at the
5
+ * relay and the key traveling as `OPENAI_API_KEY` in the process environment
6
+ * (verified against codex-cli 0.163.0: a fresh scoped home with only these
7
+ * runs `codex exec` with no login file at all). With no credential the spawn
8
+ * carries no auth at all — it fails loudly at the endpoint, exactly the
9
+ * honest reading the status shows.
10
+ *
11
+ * Verified against codex-cli 0.163.0 (2026-10-20): a fresh scoped home with
12
+ * only `OPENAI_API_KEY` plus the two `-c` overrides below runs
13
+ * `codex exec -m <model>` end-to-end through an OpenAI-compatible relay
14
+ * (openproxy, `wire_api = "responses"`) — no login, no auth.json, no
15
+ * onboarding gate.
16
+ *
17
+ * Two layers, merged per field, file over config:
18
+ *
19
+ * - `<scopedHome>/endpoint-auth.json` (mode 0600) — the per-scope credential.
20
+ * Its name deliberately matches the eval contract hash's credential denylist
21
+ * (`auth`), so the key material never enters an evaluation's condition
22
+ * fingerprint; the file's mtime serves as the credential stamp, which makes
23
+ * "edit the file" the recovery gesture for a `rejected` grade.
24
+ * - The plugin config's `baseUrl` / `apiKey` — the deployment default every
25
+ * scope without a file shares. Read PER ROUND, so a profile edit applies to
26
+ * the next round (and a host restart clears the in-memory `rejected` mark).
27
+ *
28
+ * `wireApi` is a file-only escape hatch: the settings card never edits it,
29
+ * the default `responses` is codex's native wire (relay-verified), and a
30
+ * hand-edit to `"chat"` survives the card's whole-file writes because the
31
+ * read merges it back. The env layer rides the family's existing plumbing:
32
+ * `delegationEnv` on the host, `containerExecSpawn`'s name-only `-e`
33
+ * forwarding into a container — a containerized round authenticates with the
34
+ * same credential as the host, nothing is authorized inside the unit.
35
+ * @module @khorsheed/dsh-local-agent-codex/endpoint-auth
36
+ */
37
+ /**
38
+ * The scoped-home credential file's name. Chosen to match the eval contract
39
+ * hash's credential-shaped denylist (`/token|key|credential|secret|password|auth|\.env/i`),
40
+ * so the file is excluded from condition fingerprints by name.
41
+ */
42
+ export declare const ENDPOINT_AUTH_FILE = "endpoint-auth.json";
43
+ /** The custom provider's TOML key in the `-c` overrides (bare-key safe). */
44
+ export declare const ENDPOINT_PROVIDER_NAME = "dsh-endpoint";
45
+ /** The env var the custom provider reads the key from (`env_key`). */
46
+ export declare const ENDPOINT_ENV_KEY = "OPENAI_API_KEY";
47
+ /**
48
+ * One scope's endpoint credential. `baseUrl` is optional (absent means the
49
+ * CLI's own default OpenAI endpoint — an official platform key needs no
50
+ * relay); a non-empty `apiKey` is what makes the scope endpoint-authenticated.
51
+ * `wireApi` selects the custom provider's wire protocol; absent defaults to
52
+ * `responses` (codex's native wire, relay-verified).
53
+ */
54
+ export interface CodexEndpointAuth {
55
+ readonly baseUrl?: string;
56
+ /** Sent as the custom provider's `env_key` environment value (Bearer). */
57
+ readonly apiKey?: string;
58
+ /** `responses` (default) or `chat`; file-only, never card-edited. */
59
+ readonly wireApi?: string;
60
+ }
61
+ /** Whether a resolved endpoint carries a usable credential. */
62
+ export declare function hasEndpointCredential(auth: CodexEndpointAuth | undefined): auth is CodexEndpointAuth & {
63
+ apiKey: string;
64
+ };
65
+ /**
66
+ * Read the scope's endpoint-auth.json. Absent or unreadable yields undefined
67
+ * (the caller falls back to the plugin config); malformed JSON or a non-object
68
+ * payload also yields undefined — a torn file must not fail a status read.
69
+ * Non-string fields are dropped, never carried through.
70
+ * @param homeDir - the `codex` harness's scoped home.
71
+ * @returns the file's endpoint credential, or undefined.
72
+ */
73
+ export declare function readEndpointAuthFile(homeDir: string): Promise<CodexEndpointAuth | undefined>;
74
+ /**
75
+ * Resolve one scope's effective endpoint credential: the scope's
76
+ * endpoint-auth.json merged FIELD BY FIELD over the plugin config's
77
+ * `baseUrl` / `apiKey` (a file that names only a key rotates the key while
78
+ * keeping the deployment's relay URL). The result may carry no credential at
79
+ * all — {@link hasEndpointCredential} is the judgment, kept separate so the
80
+ * caller can still route/log the resolved baseUrl.
81
+ * @param homeDir - the scope's scoped home.
82
+ * @param config - the plugin config's deployment-level endpoint pair.
83
+ * @returns the merged endpoint auth.
84
+ */
85
+ export declare function resolveEndpointAuth(homeDir: string, config: CodexEndpointAuth): Promise<CodexEndpointAuth>;
86
+ /**
87
+ * The spawn-env layer for a resolved endpoint: the key under the custom
88
+ * provider's `env_key` name, nothing else. A credential-less resolution
89
+ * yields an empty layer — the spread then adds no keys at all, byte-identical
90
+ * to the pre-endpoint env. With no baseUrl the CLI's built-in OpenAI provider
91
+ * reads the same variable, so an official platform key needs no `-c`
92
+ * override either.
93
+ * @param auth - the resolved endpoint auth, or undefined.
94
+ * @returns the explicit env entries for the delegation spawn.
95
+ */
96
+ export declare function endpointAuthEnv(auth: CodexEndpointAuth | undefined): NodeJS.ProcessEnv;
97
+ /**
98
+ * The `-c` config overrides routing the CLI at the resolved relay: a custom
99
+ * `model_providers` entry plus the `model_provider` selection, as a flat argv
100
+ * fragment (`-c <override>` pairs). Empty unless BOTH a key and a baseUrl are
101
+ * present — a key without a relay URL is served by the built-in provider
102
+ * through the env layer alone, and a relay URL without a key would route
103
+ * every round into a 401. Verified end-to-end against codex-cli 0.163.0.
104
+ * @param auth - the resolved endpoint auth, or undefined.
105
+ * @returns the argv fragment, possibly empty.
106
+ */
107
+ export declare function endpointConfigOverrides(auth: CodexEndpointAuth | undefined): string[];
108
+ /**
109
+ * The credential marker's modification stamp (epoch ms): the endpoint file's
110
+ * mtime, or undefined when the scope has no file (a config-sourced credential
111
+ * has no marker — rotating it pairs with the profile edit and instance
112
+ * restart that clear the in-memory grade marks). A fresh file write is a
113
+ * fresh stamp, which is how an edited-in key recovers a `rejected` grade.
114
+ * @param homeDir - the scope's scoped home.
115
+ * @returns the stamp, or undefined.
116
+ */
117
+ export declare function endpointAuthStamp(homeDir: string): Promise<number | undefined>;
118
+ /** Whether the scope carries its own endpoint file at all. */
119
+ export declare function endpointAuthFileExists(homeDir: string): Promise<boolean>;
120
+ /**
121
+ * Write the scope's endpoint-auth.json (mode 0600, mkdir -p first). The
122
+ * payload preserves the file-only `wireApi` across card writes: callers pass
123
+ * the merged object (current file + edits), never a bare card payload.
124
+ * @param homeDir - the scope's scoped home.
125
+ * @param auth - the endpoint credential to persist.
126
+ */
127
+ export declare function writeEndpointAuth(homeDir: string, auth: CodexEndpointAuth): Promise<void>;
128
+ /**
129
+ * Clear the scope's OWN endpoint credential (the endpoint-auth.json file).
130
+ * A deployment-level credential from the plugin config is untouched — the
131
+ * core's logout reply then names the profile row as the place to edit.
132
+ * @param homeDir - the scope's scoped home.
133
+ */
134
+ export declare function clearEndpointAuth(homeDir: string): Promise<void>;
135
+ /** The official endpoint used when no baseUrl is configured anywhere. */
136
+ export declare const OFFICIAL_BASE_URL = "https://api.openai.com";
137
+ /** The probe's fetch shape (injected in tests). */
138
+ export type EndpointCheckFetch = (url: string, init: {
139
+ headers: Record<string, string>;
140
+ signal: AbortSignal;
141
+ }) => Promise<{
142
+ ok: boolean;
143
+ status: number;
144
+ text(): Promise<string>;
145
+ }>;
146
+ /**
147
+ * The outcome of one `GET <baseUrl>/v1/models`: the parsed id list, the HTTP
148
+ * status when the endpoint answered, or the transport error's message. No
149
+ * case throws.
150
+ */
151
+ export type EndpointModelsResult = {
152
+ readonly kind: 'models';
153
+ readonly models: readonly string[];
154
+ } | {
155
+ readonly kind: 'http';
156
+ readonly status: number;
157
+ readonly body: string;
158
+ } | {
159
+ readonly kind: 'unreachable';
160
+ readonly message: string;
161
+ } | {
162
+ readonly kind: 'malformed';
163
+ };
164
+ /**
165
+ * `GET <baseUrl>/v1/models` with `Authorization: Bearer <key>` — the
166
+ * token-free endpoint probe shared by the `/codex check` verdict and any
167
+ * future catalog. OpenAI's own API serves this route with the platform key;
168
+ * relays (openproxy verified 2026-10-20, 139 models) proxy it and answer the
169
+ * same OpenAI-list shape. Never throws: every failure mode folds into the
170
+ * result.
171
+ * @param endpoint - the resolved endpoint auth with a decided baseUrl.
172
+ * @param fetchImpl - the HTTP fetch; default the host's global fetch.
173
+ * @param signal - abort for the whole probe (timeout bundled by the caller).
174
+ * @returns the folded result.
175
+ */
176
+ export declare function fetchEndpointModels(endpoint: CodexEndpointAuth & {
177
+ readonly baseUrl: string;
178
+ }, fetchImpl?: EndpointCheckFetch, signal?: AbortSignal): Promise<EndpointModelsResult>;
179
+ /**
180
+ * Probe the resolved endpoint with a cheap, token-free round-trip: `GET
181
+ * <baseUrl>/v1/models` with the resolved credential's Bearer header. A 200
182
+ * proves both reachability and credential validity — the exact evidence a
183
+ * "verify" button needs — and the served model list rides the reply. One
184
+ * transport-level retry rides along: a local proxy restart (a Surge/clash
185
+ * reload) stalls the first attempt into the timeout and answers the retry
186
+ * immediately. Never throws: every failure mode folds into the reply text,
187
+ * since the reply IS the diagnostic.
188
+ * @param endpoint - the resolved endpoint auth (scope file over settings).
189
+ * @param fetchImpl - the HTTP fetch; default the host's global fetch.
190
+ * @param timeoutMs - one attempt's deadline (two attempts at most).
191
+ * @returns the human-readable verdict for the check reply.
192
+ */
193
+ export declare function checkCodexEndpoint(endpoint: CodexEndpointAuth, fetchImpl?: EndpointCheckFetch, timeoutMs?: number): Promise<string>;
194
+ //# sourceMappingURL=endpoint-auth.d.ts.map