cascade-ai 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -185,9 +185,19 @@ interface ProviderConfig {
185
185
  apiVersion?: string;
186
186
  model?: string;
187
187
  /**
188
- * OAuth bearer token (e.g. a Claude Code subscription token) used instead
189
- * of an API key. When set on an Anthropic provider, the request uses
190
- * `Authorization: Bearer` + the oauth beta header rather than `x-api-key`.
188
+ * Bearer token for an LLM **gateway or proxy** — what Anthropic documents
189
+ * `ANTHROPIC_AUTH_TOKEN` for — used instead of an API key. The request goes
190
+ * out as `Authorization: Bearer` rather than `x-api-key`.
191
+ *
192
+ * Requires `baseUrl`: a bearer is only valid at the gateway that issued it,
193
+ * so without one there is nowhere it can be sent, and the provider refuses
194
+ * rather than falling back to the public host.
195
+ *
196
+ * NOT a Claude subscription token. Those were accepted here once, with an
197
+ * `anthropic-beta: oauth-…` header; Anthropic does not permit third-party
198
+ * clients to route requests through Free/Pro/Max credentials, and such a
199
+ * token is now refused wherever it appears — including on a config passed
200
+ * straight to `createCascade()`.
191
201
  */
192
202
  authToken?: string;
193
203
  /** Where an adopted credential came from, e.g. "Claude Code". Informational. */
@@ -3764,11 +3774,46 @@ interface CascadeMdContent {
3764
3774
  systemPrompt: string;
3765
3775
  }
3766
3776
 
3777
+ /**
3778
+ * Whether a provider entry carries a credential at all.
3779
+ *
3780
+ * `authToken` counts, not just `apiKey`: `cascade link` stores an adopted
3781
+ * bearer there and `AnthropicProvider` runs on it happily. Every surface that
3782
+ * asks "is this provider set up" goes through this one predicate, because the
3783
+ * answer was written out by hand in four places — `hasUsableProvider`, the
3784
+ * dashboard's `config:current`, `cascade doctor`, and the desktop's IPC
3785
+ * settings snapshot — and each was fixed separately as it was noticed, the last
3786
+ * of them two rounds after the first.
3787
+ */
3788
+ declare function hasProviderCredential(p: {
3789
+ apiKey?: string;
3790
+ authToken?: string;
3791
+ baseUrl?: string;
3792
+ } | undefined | null): boolean;
3767
3793
  declare function hasUsableProvider(providers: Array<{
3768
3794
  type: string;
3769
3795
  apiKey?: string;
3770
3796
  authToken?: string;
3797
+ baseUrl?: string;
3798
+ deploymentName?: string;
3771
3799
  }> | undefined): boolean;
3800
+ /**
3801
+ * Write a provider's API key, clearing any bearer token it replaces.
3802
+ *
3803
+ * `AnthropicProvider` reads `authToken` in preference to `apiKey` whenever both
3804
+ * are set, so a settings save that wrote only the key left the key the user had
3805
+ * just typed silently unused — and from the UI that is indistinguishable from
3806
+ * the save having failed. Every place a key is written from user input goes
3807
+ * through here, because this was got wrong independently in three of them.
3808
+ */
3809
+ declare function applyProviderApiKey(providers: Array<{
3810
+ type: string;
3811
+ apiKey?: string;
3812
+ authToken?: string;
3813
+ baseUrl?: string;
3814
+ }>, type: string, apiKey: string, extra?: {
3815
+ baseUrl?: string;
3816
+ }): void;
3772
3817
  declare class ConfigManager {
3773
3818
  private config;
3774
3819
  private keystore;
@@ -3788,6 +3833,16 @@ declare class ConfigManager {
3788
3833
  * fallback for a list that is empty for some unrelated reason.
3789
3834
  */
3790
3835
  private retiredCleanup?;
3836
+ /**
3837
+ * How many dead subscription credentials THIS load removed. Per-load like
3838
+ * `retiredCleanup`, for the same reason: a second load() on the same instance
3839
+ * must not re-announce a migration that already happened.
3840
+ */
3841
+ private revokedCredentials;
3842
+ /** Tier pins cleared by the revoked-credential migration this load. */
3843
+ private revokedPins;
3844
+ /** This load's revoked-credential explanation, joined with any retirement one. */
3845
+ private revokedNotice?;
3791
3846
  /**
3792
3847
  * Human-readable migration notice held for a UI to display. `console.warn`
3793
3848
  * is not sufficient: the REPL clears the TTY immediately after load(), and
@@ -3805,10 +3860,59 @@ declare class ConfigManager {
3805
3860
  getStore(): MemoryStore;
3806
3861
  getCascadeMd(): CascadeMdContent | null;
3807
3862
  getWorkspacePath(): string;
3808
- save(): Promise<void>;
3863
+ /**
3864
+ * @param config What to persist. Defaults to the manager's own, but a
3865
+ * transactional save hands in the validated copy it has NOT adopted yet —
3866
+ * writing before adopting is what stops a run picking up configuration that
3867
+ * never reached the disk (see `commitSettings`).
3868
+ */
3869
+ save(config?: CascadeConfig): Promise<void>;
3809
3870
  updateConfig(updates: Partial<CascadeConfig>): Promise<void>;
3810
3871
  getApiKey(provider: string): string | undefined;
3872
+ /**
3873
+ * The bearer token configured for a provider, if any.
3874
+ *
3875
+ * Companion to getApiKey(). A provider can be fully configured with only
3876
+ * this — `cascade link` and ANTHROPIC_AUTH_TOKEN both produce it — so any
3877
+ * surface that asks "is this provider set up" has to consult both, or it
3878
+ * reports a working install as unconfigured.
3879
+ */
3880
+ getAuthToken(provider: string): string | undefined;
3811
3881
  private loadConfig;
3882
+ /**
3883
+ * Rewrite the workspace file with those tier pins removed.
3884
+ *
3885
+ * A targeted edit of the RAW file, not save(). By this point `this.config`
3886
+ * carries environment keys and machine-global credentials, and save()
3887
+ * serializes the whole object — so persisting through it would copy secrets
3888
+ * kept 0600 in ~/.cascade-ai into a workspace file that may be 0644. Same
3889
+ * reasoning as the migration write in loadConfig(); best-effort for the same
3890
+ * reason, since a read-only config directory must not abort startup.
3891
+ */
3892
+ /**
3893
+ * The Azure entries `AZURE_OPENAI_KEY` belongs to — possibly several.
3894
+ *
3895
+ * `AZURE_OPENAI_ENDPOINT` names the resource. Without one, a single
3896
+ * configured resource is unambiguous and anything more is a guess — and
3897
+ * guessing wrong writes a key to a resource that will reject it.
3898
+ *
3899
+ * ALL the deployments on that resource, not the first. An Azure key is
3900
+ * resource-scoped, so every deployment there shares it, and Azure is
3901
+ * configured one entry per deployment — the router binds each model to its
3902
+ * own row, matching `deploymentName` against the model id, so filling only
3903
+ * the first left the rest issuing requests with no key at all.
3904
+ */
3905
+ private azureEntriesForEnv;
3906
+ private persistClearedPins;
3907
+ /**
3908
+ * `globalProviders` is the machine-global credential store, passed in
3909
+ * READ-ONLY. This runs before mergeGlobalCredentials() on purpose — an
3910
+ * exported key should outrank a stored one, which only holds while the
3911
+ * injection goes first — but a few of the questions asked here are about the
3912
+ * user's whole configuration rather than this workspace's file, and answering
3913
+ * those from the workspace alone is what produced the gateway bug this
3914
+ * argument fixes.
3915
+ */
3812
3916
  private injectEnvKeys;
3813
3917
  /**
3814
3918
  * Follow a disambiguation rename into `tools.mcpTrusted`.
@@ -3857,11 +3961,394 @@ declare class ConfigManager {
3857
3961
  private ensureDefaultIdentity;
3858
3962
  }
3859
3963
 
3964
+ /** A provider row, as every settings surface manipulates it. */
3965
+ interface WritableProvider {
3966
+ type: string;
3967
+ apiKey?: string;
3968
+ authToken?: string;
3969
+ baseUrl?: string;
3970
+ /** See `isLocalEndpoint` in core/router/pricing.ts. */
3971
+ local?: boolean;
3972
+ }
3973
+ /**
3974
+ * What the caller is able to say about where the key it is writing belongs.
3975
+ *
3976
+ * Not an endpoint value — a statement about the SURFACE. That is the whole
3977
+ * point: `{ kind: 'provider-default' }` and `{ kind: 'preserve' }` both carry
3978
+ * no URL and mean opposite things, and only the caller knows which it is.
3979
+ */
3980
+ type CredentialEndpoint =
3981
+ /** The user named a host in the same save. The key belongs there. */
3982
+ {
3983
+ kind: 'at';
3984
+ baseUrl: string;
3985
+ }
3986
+ /**
3987
+ * The caller HAS an endpoint field for this provider and the user left it
3988
+ * empty. That is a statement, and what it states depends on the provider: for
3989
+ * one with a public host the key belongs there, and for one WITHOUT the row
3990
+ * now addresses nothing at all.
3991
+ *
3992
+ * Not the same as `preserve`, though both arrive carrying no URL — collapsing
3993
+ * them turned an explicit clear into "keep the old host", so a replacement
3994
+ * key stayed attached to a URL the user had just deleted.
3995
+ */
3996
+ | {
3997
+ kind: 'cleared';
3998
+ }
3999
+ /**
4000
+ * The caller has no endpoint field for this provider at all.
4001
+ *
4002
+ * Absence here is a limit of the surface, not evidence, so an untouched
4003
+ * credential keeps whatever host the row names. It is NOT enough to place a
4004
+ * NEW secret: see the refusal in `applyProviderCredential`.
4005
+ */
4006
+ | {
4007
+ kind: 'preserve';
4008
+ };
4009
+ /** Whether a key was written, and if not, why the caller has to ask a human. */
4010
+ type CredentialWriteOutcome = {
4011
+ written: true;
4012
+ } | {
4013
+ written: false;
4014
+ reason: 'ambiguous-scope' | 'unroutable';
4015
+ };
4016
+ /**
4017
+ * Read the intent out of a settings payload's `endpoints` map.
4018
+ *
4019
+ * The KEY's presence is the signal, not its value. A surface that can address a
4020
+ * provider's endpoint sends an entry for it — possibly `undefined`, meaning the
4021
+ * field was shown and left blank — and one that cannot sends no entry at all.
4022
+ * That distinction is exactly the one `CredentialEndpoint` needs and the only
4023
+ * place it survives.
4024
+ */
4025
+ declare function endpointFromSettingsPayload(endpoints: Record<string, string | undefined> | undefined, type: string): CredentialEndpoint;
4026
+ /**
4027
+ * Write a user-supplied key together with the host it belongs to.
4028
+ *
4029
+ * `applyProviderApiKey()` only touches `baseUrl` when it is handed one, which
4030
+ * is right for the field and wrong for the pairing — so every surface that
4031
+ * wrote a key through it kept whatever host was already on the row. Callers go
4032
+ * through here instead, and say what they know.
4033
+ */
4034
+ declare function applyProviderCredential(providers: WritableProvider[], type: string, apiKey: string, endpoint: CredentialEndpoint): CredentialWriteOutcome;
4035
+ /** What happens to the credential already on a row when its endpoint is edited. */
4036
+ type CredentialDisposition =
4037
+ /** Nothing typed, and the endpoint still identifies the same host — it stays. */
4038
+ 'keep'
4039
+ /** The endpoint moved or was cleared with nothing typed — the pairing is gone. */
4040
+ | 'clear'
4041
+ /** A key was typed in this save; it is the credential now, and must not be touched. */
4042
+ | 'replaced';
4043
+ /**
4044
+ * What becomes of a stored credential when the endpoint field is edited.
4045
+ *
4046
+ * Three outcomes, not two. This started life as a boolean "does the credential
4047
+ * survive?", and the caller read `false` as "delete the credential" — so a save
4048
+ * carrying BOTH a new key and a new endpoint wrote the key and then deleted it,
4049
+ * because `false` also meant "a replacement was supplied". A user typing a key
4050
+ * into Settings watched it vanish. `'replaced'` exists so that case cannot be
4051
+ * confused with `'clear'` again.
4052
+ */
4053
+ declare function credentialDispositionForEdit(existing: {
4054
+ type: string;
4055
+ baseUrl?: string;
4056
+ }, nextBaseUrl: string | undefined, replacementKey: string | undefined): CredentialDisposition;
4057
+ /**
4058
+ * Apply ONE provider's endpoint edit, retiring the credential when the host
4059
+ * changes and nothing was typed to replace it.
4060
+ *
4061
+ * This is the endpoint-ONLY half — the key field left blank. It is reached from
4062
+ * three places (onboarding, the Settings save, and the live dashboard), and the
4063
+ * dashboard did not reach it at all until the sibling was noticed: a standalone
4064
+ * `cascade dashboard` reported a saved endpoint change it had never applied.
4065
+ */
4066
+ declare function applyEndpointEdit(existing: WritableProvider, nextBaseUrl: string | undefined, replacementKey: string | undefined): void;
4067
+ /** What a settings save could not store, and why. */
4068
+ interface SettingsCredentialResult {
4069
+ refused: Array<{
4070
+ type: string;
4071
+ reason: 'ambiguous-scope' | 'unroutable';
4072
+ }>;
4073
+ }
4074
+ /** A refusal, phrased for the person who typed the key. */
4075
+ declare function explainRefusal(type: string, reason: 'ambiguous-scope' | 'unroutable'): string;
4076
+ /** The non-Azure halves of a settings save. */
4077
+ interface SettingsCredentialPayload {
4078
+ keys?: Record<string, string | undefined>;
4079
+ endpoints?: Record<string, string | undefined>;
4080
+ }
4081
+ /**
4082
+ * Apply the non-Azure `keys` and `endpoints` halves of a settings save, in order.
4083
+ *
4084
+ * The ORDER is the whole point, and it is why this is one function rather than
4085
+ * two loops in each caller. Keys are written first and endpoints second, so the
4086
+ * endpoint step has to know that a key it is about to consider retiring may be
4087
+ * the one just written. Expressed as two loops with a boolean between them,
4088
+ * that knowledge went missing and the save deleted the key it had installed a
4089
+ * few lines earlier — with no test able to see it, because the loops lived
4090
+ * inside `ipcMain.handle`.
4091
+ */
4092
+ declare function applySettingsCredentials(providers: WritableProvider[], data: SettingsCredentialPayload): SettingsCredentialResult;
4093
+
4094
+ /** One Azure deployment as the settings panel sends it. */
4095
+ interface AzureDeploymentInput {
4096
+ label?: string;
4097
+ apiKey?: string;
4098
+ baseUrl?: string;
4099
+ deploymentName?: string;
4100
+ apiVersion?: string;
4101
+ /**
4102
+ * Base-model override and pricing region.
4103
+ *
4104
+ * The panel has no controls for these, but the config does and
4105
+ * `azureModelForDeployment()` reads both — `model` for the real base-model
4106
+ * identity, `region` for Azure's regional pricing. The save REPLACES the
4107
+ * whole Azure list, so leaving them out of the DTO meant an unrelated or
4108
+ * no-op Settings save silently dropped a base-model mapping and a region,
4109
+ * changing routing and cost accounting through controls that were never
4110
+ * shown. Round-tripped so a replacement is a replacement of the fields the
4111
+ * editor owns, not of the row.
4112
+ */
4113
+ model?: string;
4114
+ region?: string;
4115
+ }
4116
+ /** The complete Settings save, as both surfaces send it. */
4117
+ interface SettingsPayload {
4118
+ keys?: Record<string, string | undefined>;
4119
+ endpoints?: Record<string, string | undefined>;
4120
+ models?: Record<string, string | undefined>;
4121
+ budget?: {
4122
+ maxCostPerRun?: number;
4123
+ autoBias?: string;
4124
+ dailyBudgetUsd?: number;
4125
+ sessionBudgetUsd?: number;
4126
+ maxTokensPerRun?: number;
4127
+ warnAtPct?: number;
4128
+ };
4129
+ azureDeployments?: AzureDeploymentInput[];
4130
+ webSearch?: {
4131
+ searxngUrl?: string;
4132
+ braveApiKey?: string;
4133
+ tavilyApiKey?: string;
4134
+ };
4135
+ advanced?: Record<string, unknown>;
4136
+ }
4137
+ /**
4138
+ * The prior Azure row a saved deployment may inherit its key from.
4139
+ *
4140
+ * Matched on deployment name AND resource. An Azure key is resource-scoped, so
4141
+ * matching the name alone meant moving deployment "prod" from resource A to
4142
+ * resource B with a blank key field copied A's key onto B — a credential sent
4143
+ * to a resource that never issued it.
4144
+ */
4145
+ declare function priorAzureRow<T extends {
4146
+ deploymentName?: string;
4147
+ baseUrl?: string;
4148
+ model?: string;
4149
+ region?: string;
4150
+ }>(prior: readonly T[], incoming: {
4151
+ deploymentName?: string;
4152
+ baseUrl?: string;
4153
+ }): T | undefined;
4154
+ /** Apply the whole payload to a live config, in place. */
4155
+ declare function applySettingsPayload(config: CascadeConfig, data: SettingsPayload): SettingsCredentialResult;
4156
+ /**
4157
+ * The redacted Settings snapshot, as the panel reads it back.
4158
+ *
4159
+ * The read counterpart to `applySettingsPayload`, and it has to be as complete
4160
+ * as the write is. The desktop's IPC reply carried every section; the socket's
4161
+ * `config:current` carried models, two budget fields, key status and endpoints.
4162
+ * That was merely incomplete while the socket save was also partial — and
4163
+ * became DESTRUCTIVE the moment the save started applying the whole payload:
4164
+ * the panel keeps its own defaults for anything a snapshot does not fill
4165
+ * (`azureDeployments: []`, a blank SearXNG URL, the advanced defaults) and
4166
+ * serializes them on every save. A socket-only save of an unrelated setting
4167
+ * could therefore delete every Azure deployment and reset advanced knobs the
4168
+ * user had never seen.
4169
+ *
4170
+ * Secrets never come back — only whether one is set.
4171
+ */
4172
+ interface SettingsSnapshot {
4173
+ models: Record<string, string>;
4174
+ budget: {
4175
+ maxCostPerRun?: number;
4176
+ autoBias?: string;
4177
+ dailyBudgetUsd?: number;
4178
+ sessionBudgetUsd?: number;
4179
+ maxTokensPerRun?: number;
4180
+ warnAtPct?: number;
4181
+ };
4182
+ providersWithKey: string[];
4183
+ endpoints: Record<string, string>;
4184
+ azureDeployments: Array<{
4185
+ label?: string;
4186
+ baseUrl?: string;
4187
+ deploymentName?: string;
4188
+ apiVersion?: string;
4189
+ model?: string;
4190
+ region?: string;
4191
+ hasKey: boolean;
4192
+ }>;
4193
+ webSearch: {
4194
+ searxngUrl?: string;
4195
+ hasBraveKey: boolean;
4196
+ hasTavilyKey: boolean;
4197
+ };
4198
+ advanced: Record<string, unknown>;
4199
+ }
4200
+ declare function settingsSnapshot(config: CascadeConfig): SettingsSnapshot;
4201
+ /** The outcome of a settings save, for whichever surface asked for it. */
4202
+ interface SettingsCommitResult {
4203
+ /** False only when nothing was stored — the live config is untouched. */
4204
+ ok: boolean;
4205
+ /** Credentials deliberately not written, with the reason to show the user. */
4206
+ refused: SettingsCredentialResult['refused'];
4207
+ /** Why the save failed, when it did. */
4208
+ error?: string;
4209
+ }
4210
+ /**
4211
+ * Apply a settings save as a TRANSACTION, for every surface that has one.
4212
+ *
4213
+ * Both writers had the same bug and it was fixed in only one of them: the
4214
+ * payload was applied to the LIVE config and persisted afterwards, so a failed
4215
+ * write left the change running while the caller was told it had not saved. The
4216
+ * socket handler was made transactional; the desktop IPC bridge — the primary
4217
+ * path, tried before the socket — was not. Rather than stage in two places,
4218
+ * both call this.
4219
+ *
4220
+ * The order matters and each step exists for a reported failure:
4221
+ *
4222
+ * 1. apply the payload to a COPY, so a rejected save never runs;
4223
+ * 2. VALIDATE, and keep what the validator returns — it applies schema
4224
+ * defaults, and discarding it left a config missing the fields a cleared
4225
+ * cap should have fallen back to until the next restart re-applied them;
4226
+ * 3. write that validated copy, and only then
4227
+ * 4. adopt it into the live object IN PLACE, because the desktop main process
4228
+ * holds a reference to it and swapping would strand that alias.
4229
+ *
4230
+ * Writing BEFORE adopting is the part that took two attempts. An earlier
4231
+ * revision adopted first and rolled back on failure, which reads as
4232
+ * equivalent and is not: `write()` is asynchronous filesystem I/O, and the
4233
+ * embedded `DashboardServer` reads that same live object when a run starts. A
4234
+ * run beginning inside that window used configuration that was never
4235
+ * persisted — and for an endpoint or credential change, may already have sent
4236
+ * the new secret somewhere — before the failed write rolled memory back and
4237
+ * the panel truthfully reported that nothing had saved. Handing the writer the
4238
+ * validated config as an argument, rather than letting it read the live one,
4239
+ * is what removes the window rather than narrowing it.
4240
+ *
4241
+ * There is consequently no rollback image: nothing is modified until the write
4242
+ * has landed.
4243
+ *
4244
+ * The caller syncs machine-global credentials only when this returns `ok`.
4245
+ */
4246
+ declare function commitSettings(live: CascadeConfig, data: SettingsPayload, write: (config: CascadeConfig) => Promise<{
4247
+ ok: true;
4248
+ } | {
4249
+ ok: false;
4250
+ error: string;
4251
+ }> | ({
4252
+ ok: true;
4253
+ } | {
4254
+ ok: false;
4255
+ error: string;
4256
+ })): Promise<SettingsCommitResult>;
4257
+
4258
+ /**
4259
+ * Keep localhost intact. Node's `dns.setDefaultResultOrder('ipv4first')` handles IPv4 priority process-wide.
4260
+ * We no longer forcefully rewrite `localhost` to `127.0.0.1` as it breaks IPv6-only environments (e.g. ::1).
4261
+ */
4262
+ declare function preferIpv4Host(url: string | undefined): string | undefined;
4263
+ /**
4264
+ * An endpoint URL reduced to what identifies the host it addresses.
4265
+ *
4266
+ * Trailing slashes go because every provider client drops them anyway; case
4267
+ * goes because the meaningful part is a hostname, which is case-insensitive.
4268
+ * A missing or blank endpoint normalizes to the empty string, so rows with no
4269
+ * endpoint compare equal to each other and to nothing else.
4270
+ *
4271
+ * Lives here rather than in `azure-endpoint.ts` because the question is not
4272
+ * Azure's: "is this bearer's gateway the same host as the one already
4273
+ * configured?" needs the identical normalization, and writing it out a second
4274
+ * time is how two call sites start disagreeing about what "same endpoint"
4275
+ * means.
4276
+ */
4277
+ declare function normalizeEndpoint(url: string | undefined | null): string;
4278
+ /** Whether two endpoint URLs address the same host. */
4279
+ declare function sameEndpoint(a: string | undefined | null, b: string | undefined | null): boolean;
4280
+ interface NodeHttpFetchOptions {
4281
+ /**
4282
+ * When set, every redirect hop (not just the initial request) must resolve
4283
+ * to this exact origin, or the fetch rejects instead of following it. For
4284
+ * a caller sending a credential in `init.headers` (a bearer token, a PAT),
4285
+ * omitting this means that credential is replayed on ANY origin a 3xx
4286
+ * response points to — a malicious or misconfigured redirect, including an
4287
+ * HTTPS→HTTP downgrade, would silently exfiltrate it, since `init` (and
4288
+ * its headers) is reused verbatim on the followed request below. Every
4289
+ * existing caller omits this and keeps today's follow-anywhere behaviour
4290
+ * unchanged; it exists for exactly the callers that attach a credential.
4291
+ */
4292
+ allowedRedirectOrigin?: string;
4293
+ }
4294
+ declare function nodeHttpFetch(input: string | URL | Request, init?: RequestInit, redirectCount?: number, opts?: NodeHttpFetchOptions): Promise<Response>;
4295
+
4296
+ /** Whether absence of `baseUrl` resolves to a known public host for this type. */
4297
+ declare function hasDefaultEndpoint(type: string): boolean;
4298
+ /**
4299
+ * The host a credential on this row is scoped to, normalized for comparison.
4300
+ *
4301
+ * Returns `null` only when the type has no default and no endpoint is set —
4302
+ * the genuinely unscoped case, where there is nothing to compare and a caller
4303
+ * must not infer agreement.
4304
+ */
4305
+ declare function credentialEndpointIdentity(type: string, baseUrl?: string): string | null;
4306
+ /**
4307
+ * Whether a credential scoped to `a` is still scoped to `b` for this provider.
4308
+ *
4309
+ * Conservative when it cannot tell: two unscoped configurations are the same
4310
+ * (nothing changed), but an unscoped one and a configured one are NOT — that
4311
+ * is a host being introduced where there was none, which is exactly the edit
4312
+ * that must retire a key.
4313
+ */
4314
+ declare function sameCredentialEndpoint(type: string, a: string | undefined, b: string | undefined): boolean;
4315
+
4316
+ /** Whether two Azure endpoints name the same resource. */
4317
+ declare function sameAzureEndpoint(a: string | undefined | null, b: string | undefined | null): boolean;
4318
+
3860
4319
  interface DashboardSocketOptions {
3861
4320
  authRequired: boolean;
3862
4321
  secret: string;
3863
4322
  corsOrigin?: string | string[];
3864
4323
  }
4324
+ /**
4325
+ * The Settings save as it arrives over the socket — the SAME type the desktop
4326
+ * IPC bridge takes.
4327
+ *
4328
+ * It was a narrower hand-written copy, and the narrowing was not harmless: the
4329
+ * panel posts one payload to both surfaces, so a field missing from this
4330
+ * declaration was not missing from the wire, merely invisible to the code
4331
+ * handling it. That hid the endpoint a key was typed beside (a public-host key
4332
+ * ended up stored against a gateway) and then hid budget, Azure, web-search and
4333
+ * advanced entirely (controls that reported success and changed nothing).
4334
+ */
4335
+ type ConfigUpdatePayload = SettingsPayload;
4336
+ /** What the save could not store, phrased for the person who typed it. */
4337
+ interface ConfigUpdateResult {
4338
+ refused: Array<{
4339
+ type: string;
4340
+ reason: string;
4341
+ message: string;
4342
+ }>;
4343
+ /** What is actually stored now — the panel re-hydrates from this. */
4344
+ snapshot?: SettingsSnapshot;
4345
+ /**
4346
+ * Set when the save could not be written to disk. Its absence is the only
4347
+ * thing that makes this acknowledgement mean "durable" rather than merely
4348
+ * "the handler ran".
4349
+ */
4350
+ error?: string;
4351
+ }
3865
4352
  declare class DashboardSocket {
3866
4353
  private io;
3867
4354
  private authRequired;
@@ -3923,14 +4410,16 @@ declare class DashboardSocket {
3923
4410
  note?: string;
3924
4411
  }) => void): void;
3925
4412
  onSessionSteer(callback: (message: string, sessionId?: string, nodeId?: string) => void): void;
3926
- onConfigUpdate(callback: (data: {
3927
- keys?: Record<string, string>;
3928
- models?: Record<string, string>;
3929
- budget?: {
3930
- maxCostPerRun?: number;
3931
- autoBias?: string;
3932
- };
3933
- }) => void): void;
4413
+ /**
4414
+ * The handler's result is ACKNOWLEDGED to the caller.
4415
+ *
4416
+ * A settings save can be partly refused — a key whose host cannot be
4417
+ * determined is deliberately not stored — and this path had no way to say so,
4418
+ * so the panel cleared the input and reported success over a key the backend
4419
+ * had dropped. Socket.IO's own ack channel carries it back, which needs no
4420
+ * new event name and cannot be missed by a client that forgot to subscribe.
4421
+ */
4422
+ onConfigUpdate(callback: (data: ConfigUpdatePayload) => Promise<ConfigUpdateResult>): void;
3934
4423
  emitToSocket(socketId: string, event: string, data: unknown): void;
3935
4424
  onConfigGet(callback: (socketId: string) => void): void;
3936
4425
  onCascadeRun(callback: (prompt: string, model: string, socketId: string, sessionId?: string, forceTier?: string) => void): void;
@@ -4009,7 +4498,41 @@ declare class DashboardServer {
4009
4498
  * made over the socket (Settings → Save) persist across restarts. Best-effort:
4010
4499
  * a write failure is logged but never crashes the running dashboard.
4011
4500
  */
4501
+ /**
4502
+ * Returns the durability of the WORKSPACE write, because a caller
4503
+ * acknowledging a save has to know it happened.
4504
+ *
4505
+ * This was `void` and swallowed every filesystem error, so a read-only
4506
+ * config path or a full disk produced: the live object mutated, the write
4507
+ * failed, the socket acknowledged success, and the panel cleared the keys the
4508
+ * user had typed. The changes then vanished at the next restart. "The handler
4509
+ * ran" is not "the save is durable".
4510
+ *
4511
+ * The global-credential sync below is genuinely best-effort — it is a
4512
+ * convenience copy, and failing it does not lose what the user just entered.
4513
+ */
4514
+ /**
4515
+ * @param config REQUIRED, with no default. `commitSettings` writes the
4516
+ * validated copy BEFORE adopting it, so a writer that reached for
4517
+ * `this.config` instead would persist the configuration being replaced — and
4518
+ * a default parameter is exactly the affordance that lets that happen
4519
+ * silently and still typecheck.
4520
+ */
4012
4521
  private persistConfig;
4522
+ /**
4523
+ * Copy provider credentials to the machine-global store, so keys saved over
4524
+ * the socket survive a workspace switch — the same sync `ConfigManager.save()`
4525
+ * performs.
4526
+ *
4527
+ * Separate from the workspace write, and called only AFTER it succeeds. It
4528
+ * used to run unconditionally inside `persistConfig()`, so a failed workspace
4529
+ * write still pushed the provider half of a rejected save into the global
4530
+ * store: half the change persisted, behind an error saying none of it had.
4531
+ *
4532
+ * Best-effort on its own account: this is a convenience copy, and failing it
4533
+ * does not lose what the user just entered.
4534
+ */
4535
+ private syncGlobalCredentials;
4013
4536
  /**
4014
4537
  * Produce a stable dashboard JWT signing secret.
4015
4538
  *
@@ -5219,27 +5742,6 @@ declare class CascadeToolError extends Error {
5219
5742
  constructor(userMessage: string, cause?: unknown, retryable?: boolean);
5220
5743
  }
5221
5744
 
5222
- /**
5223
- * Keep localhost intact. Node's `dns.setDefaultResultOrder('ipv4first')` handles IPv4 priority process-wide.
5224
- * We no longer forcefully rewrite `localhost` to `127.0.0.1` as it breaks IPv6-only environments (e.g. ::1).
5225
- */
5226
- declare function preferIpv4Host(url: string | undefined): string | undefined;
5227
- interface NodeHttpFetchOptions {
5228
- /**
5229
- * When set, every redirect hop (not just the initial request) must resolve
5230
- * to this exact origin, or the fetch rejects instead of following it. For
5231
- * a caller sending a credential in `init.headers` (a bearer token, a PAT),
5232
- * omitting this means that credential is replayed on ANY origin a 3xx
5233
- * response points to — a malicious or misconfigured redirect, including an
5234
- * HTTPS→HTTP downgrade, would silently exfiltrate it, since `init` (and
5235
- * its headers) is reused verbatim on the followed request below. Every
5236
- * existing caller omits this and keeps today's follow-anywhere behaviour
5237
- * unchanged; it exists for exactly the callers that attach a credential.
5238
- */
5239
- allowedRedirectOrigin?: string;
5240
- }
5241
- declare function nodeHttpFetch(input: string | URL | Request, init?: RequestInit, redirectCount?: number, opts?: NodeHttpFetchOptions): Promise<Response>;
5242
-
5243
5745
  /** Selectable base models, in the order a picker should show them. */
5244
5746
  declare const AZURE_BASE_MODELS: readonly string[];
5245
5747
  declare function inferAzureBaseModel(deploymentName: string): string | null;
@@ -5420,6 +5922,24 @@ interface RetiredProviderCleanup {
5420
5922
  removed: string[];
5421
5923
  /** Tier keys (`t1`/`t2`/`t3`) whose pin named a retired provider and was cleared. */
5422
5924
  clearedPins: string[];
5925
+ /**
5926
+ * Dead Claude subscription credentials dropped from an incoming sync bundle.
5927
+ *
5928
+ * Reported here rather than left to `removed`, which names retired provider
5929
+ * TYPES — `anthropic` is not retired, its credential is. Without this a
5930
+ * bundle whose only content was the dead token produced an empty cleanup, so
5931
+ * both callers announced a clean sync while the one key it carried had been
5932
+ * discarded.
5933
+ */
5934
+ revokedCredentials?: number;
5935
+ /**
5936
+ * Incoming rows dropped for carrying a bearer with no gateway.
5937
+ *
5938
+ * A separate count from `revokedCredentials`: that one is a credential
5939
+ * Anthropic has revoked, this one is a live token with nowhere to send it.
5940
+ * Distinct causes, distinct advice.
5941
+ */
5942
+ unusableCredentials?: number;
5423
5943
  }
5424
5944
 
5425
5945
  /**
@@ -5472,4 +5992,4 @@ declare function gatherSyncBundle(config: CascadeConfig): SyncBundle;
5472
5992
  /** Merge a pulled bundle into a config, returning the updated config to persist. */
5473
5993
  declare function applySyncBundle(bundle: SyncBundle, config: CascadeConfig, cleanup?: RetiredProviderCleanup): CascadeConfig;
5474
5994
 
5475
- export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, type AcceptanceProbe, type AcceptanceResult, type AcceptanceVerdict, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type Block, type BudgetConfig, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, type ChartSeries, type ChartSpec, type Chunk, type ChunkOptions, CloudClient, type CloudConversation, type CloudMessage, type CloudSession, type CloudSessionStore, type CloudTurnInput, type CloudUser, type CodeChunkOptions, type CodeChunker, CodeSearchTool, type CompileTaskGraphOptions, type CompiledTaskGraph, type CompleteFn, type CompletedNode, ConfigManager, type ConversationMessage, type CurrentPageProvider, type CurrentPageSnapshot, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, type DashboardConfig, DashboardServer, DependencyScheduler, type DeviceStart, type DiscoveredMcpTool, type DistilledFact, type ChartKind as DocumentChartKind, type DocumentFormat, type Embedder, type EncryptedBlob, type EscalationDecision, type EscalationPayload, type ExtendedContextConfig, type FileManifest, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, type GenerateOptions, type GenerateResult, type GraphFactSource, GraphRetriever, type GraphSearchOptions, GraphSearchTool, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, type ImageByteLoader, type ImageInfo, type ImageRef, type InlineRun, Keystore, LLMReranker, LM_STUDIO_BASE_URL, type LegacyThemeName, type LoadedImage, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, type ManifestDiff, McpClient, type McpDiscoveryResult, McpOAuthProvider, type McpOAuthState, type McpOAuthStore, type McpServerConfig$1 as McpServerConfig, type McpServerRename, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, type NativeProvider, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, type OpenAIEmbedderOptions, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, RESUME_SCHEMA_VERSION, type RankedFact, type RefreshResult, type RenderOptions, type Reranker, type ResumeCheckpoint, type ResumeReason, ResumeStore, type ResumeStoreOptions, type RetrievalContext, type RetrievalMode, type RetrievalPlan, Retriever, type RetrieverSearchOptions, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type SchedulerExecutionMode, type SchedulerHooks, type SchedulerOptions, type SchedulerResult, type ScoredChunk, type SearchOptions, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, type Slide, SqliteVectorStore, type StatusUpdate, type StoredMessage, type StreamChunk, type SyncBundle, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, type TaskGraph, type TaskGraphInputNode, type TaskGraphIssue, type TaskGraphIssueCode, type TaskGraphNode, type TaskGraphNodeKind, TaskGraphValidationError, type TaskGraphValidationMode, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type VectorStore, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, type WorkspaceFileReader, WorkspaceIndex, type WorkspaceIndexOptions, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, evaluateAcceptance, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hasUsableProvider, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sessionWorthRemembering, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
5995
+ export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, type AcceptanceProbe, type AcceptanceResult, type AcceptanceVerdict, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type AzureDeploymentInput, type BenchmarksConfig, type Block, type BudgetConfig, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, type ChartSeries, type ChartSpec, type Chunk, type ChunkOptions, CloudClient, type CloudConversation, type CloudMessage, type CloudSession, type CloudSessionStore, type CloudTurnInput, type CloudUser, type CodeChunkOptions, type CodeChunker, CodeSearchTool, type CompileTaskGraphOptions, type CompiledTaskGraph, type CompleteFn, type CompletedNode, ConfigManager, type ConversationMessage, type CredentialDisposition, type CredentialEndpoint, type CredentialWriteOutcome, type CurrentPageProvider, type CurrentPageSnapshot, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, type DashboardConfig, DashboardServer, DependencyScheduler, type DeviceStart, type DiscoveredMcpTool, type DistilledFact, type ChartKind as DocumentChartKind, type DocumentFormat, type Embedder, type EncryptedBlob, type EscalationDecision, type EscalationPayload, type ExtendedContextConfig, type FileManifest, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, type GenerateOptions, type GenerateResult, type GraphFactSource, GraphRetriever, type GraphSearchOptions, GraphSearchTool, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, type ImageByteLoader, type ImageInfo, type ImageRef, type InlineRun, Keystore, LLMReranker, LM_STUDIO_BASE_URL, type LegacyThemeName, type LoadedImage, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, type ManifestDiff, McpClient, type McpDiscoveryResult, McpOAuthProvider, type McpOAuthState, type McpOAuthStore, type McpServerConfig$1 as McpServerConfig, type McpServerRename, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, type NativeProvider, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, type OpenAIEmbedderOptions, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, RESUME_SCHEMA_VERSION, type RankedFact, type RefreshResult, type RenderOptions, type Reranker, type ResumeCheckpoint, type ResumeReason, ResumeStore, type ResumeStoreOptions, type RetrievalContext, type RetrievalMode, type RetrievalPlan, Retriever, type RetrieverSearchOptions, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type SchedulerExecutionMode, type SchedulerHooks, type SchedulerOptions, type SchedulerResult, type ScoredChunk, type SearchOptions, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, type SettingsCommitResult, type SettingsCredentialResult, type SettingsPayload, type SettingsSnapshot, type Slide, SqliteVectorStore, type StatusUpdate, type StoredMessage, type StreamChunk, type SyncBundle, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, type TaskGraph, type TaskGraphInputNode, type TaskGraphIssue, type TaskGraphIssueCode, type TaskGraphNode, type TaskGraphNodeKind, TaskGraphValidationError, type TaskGraphValidationMode, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type VectorStore, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, type WorkspaceFileReader, WorkspaceIndex, type WorkspaceIndexOptions, applyEndpointEdit, applyProviderApiKey, applyProviderCredential, applySettingsCredentials, applySettingsPayload, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText, commitSettings, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, credentialDispositionForEdit, credentialEndpointIdentity, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, endpointFromSettingsPayload, evaluateAcceptance, explainRefusal, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hasDefaultEndpoint, hasProviderCredential, hasUsableProvider, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, normalizeEndpoint, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, priorAzureRow, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sameAzureEndpoint, sameCredentialEndpoint, sameEndpoint, sessionWorthRemembering, settingsSnapshot, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };