Files
openclaw/src/secrets/resolve-errors.ts
snowzlmbot 17c2ce05d8 fix(secrets): keep startup alive when TTS SecretRefs are missing (#101265)
* fix(secrets): degrade missing TTS SecretRefs at startup

* test(secrets): keep non-activating startup strict

* test(secrets): mark denied key fixture synthetic

* test(secrets): use synthetic TTS key fixture

* test(secrets): use neutral TTS key placeholder

* test(secrets): isolate TTS key placeholder

* test(secrets): shorten TTS ref fixture name

* test(secrets): normalize synthetic credential fixtures

* test(secrets): isolate optional redaction coverage

* fix(secrets): preserve degraded TTS ref ownership

* refactor(secrets): keep optional resolver internal

* test(secrets): cover default provider alias misses

* test(secrets): pin explicit provider ownership

* style(secrets): format optional assignment imports

* refactor(secrets): keep optional metadata private

* style(secrets): restore collector file header

* fix(secrets): isolate unavailable SecretRef owners

Co-authored-by: snowzlmbot <293528334+snowzlmbot@users.noreply.github.com>

* test(secrets): complete provider fixtures

* style(status): avoid degraded path shadowing

* style(secrets): satisfy runtime lint

* refactor(secrets): keep error codes internal

* fix(secrets): keep unowned assignments fail closed

* fix(secrets): preserve provider resolution batching

* fix(secrets): normalize stalled resolution errors

* fix(secrets): reject provider limit violations

---------

Co-authored-by: snowzlmbot <293528334+snowzlmbot@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-16 21:07:01 -07:00

138 lines
4.4 KiB
TypeScript

/** Typed errors for SecretRef provider and ref-level resolution failures. */
import type { SecretRef, SecretRefSource } from "../config/types.secrets.js";
type SecretRefResolutionCode =
| "SECRET_REF_NOT_FOUND"
| "SECRET_REF_POLICY_DENIED"
| "SECRET_REF_INVALID"
| "SECRET_REF_PROVIDER_ERROR"
| "SECRET_REF_PROVIDER_CONTRACT";
type SecretProviderResolutionCode = "SECRET_PROVIDER_INVALID" | "SECRET_PROVIDER_UNAVAILABLE";
/** Error for failures that affect an entire configured secret provider. */
class SecretProviderResolutionError extends Error {
readonly scope = "provider" as const;
readonly code: SecretProviderResolutionCode;
readonly source: SecretRefSource;
readonly provider: string;
constructor(params: {
code: SecretProviderResolutionCode;
source: SecretRefSource;
provider: string;
message: string;
cause?: unknown;
}) {
super(params.message, params.cause !== undefined ? { cause: params.cause } : undefined);
this.name = "SecretProviderResolutionError";
this.code = params.code;
this.source = params.source;
this.provider = params.provider;
}
}
/** Error for failures limited to one SecretRef id under a provider. */
class SecretRefResolutionError extends Error {
readonly scope = "ref" as const;
readonly code: SecretRefResolutionCode;
readonly source: SecretRefSource;
readonly provider: string;
readonly refId: string;
constructor(params: {
code: SecretRefResolutionCode;
source: SecretRefSource;
provider: string;
refId: string;
message: string;
cause?: unknown;
}) {
super(params.message, params.cause !== undefined ? { cause: params.cause } : undefined);
this.name = "SecretRefResolutionError";
this.code = params.code;
this.source = params.source;
this.provider = params.provider;
this.refId = params.refId;
}
}
/** Type guard for provider-scoped secret resolution failures. */
export function isProviderScopedSecretResolutionError(
value: unknown,
): value is SecretProviderResolutionError {
return value instanceof SecretProviderResolutionError;
}
export function isSecretResolutionError(
value: unknown,
): value is SecretProviderResolutionError | SecretRefResolutionError {
return (
value instanceof SecretProviderResolutionError || value instanceof SecretRefResolutionError
);
}
/** Redacted reason suitable for warnings and status output. */
export function describeSecretResolutionError(value: unknown): string | undefined {
if (value instanceof SecretProviderResolutionError) {
return value.code === "SECRET_PROVIDER_UNAVAILABLE" ? "secret provider failed" : undefined;
}
if (!(value instanceof SecretRefResolutionError)) {
return undefined;
}
switch (value.code) {
case "SECRET_REF_NOT_FOUND":
return "secret reference was not found";
case "SECRET_REF_POLICY_DENIED":
return "secret provider policy denied resolution";
case "SECRET_REF_PROVIDER_ERROR":
return "secret provider failed";
case "SECRET_REF_PROVIDER_CONTRACT":
return "secret provider response violated its contract";
case "SECRET_REF_INVALID":
return undefined;
}
return undefined;
}
export function providerResolutionError(params: {
code?: SecretProviderResolutionCode;
source: SecretRefSource;
provider: string;
message: string;
cause?: unknown;
}): SecretProviderResolutionError {
return new SecretProviderResolutionError({
...params,
code: params.code ?? "SECRET_PROVIDER_UNAVAILABLE",
});
}
export function refResolutionError(params: {
code: SecretRefResolutionCode;
source: SecretRefSource;
provider: string;
refId: string;
message: string;
cause?: unknown;
}): SecretRefResolutionError {
return new SecretRefResolutionError(params);
}
/** Returns whether one SecretRef failed because its configured value is absent. */
export function isMissingSecretRefResolutionError(params: {
ref: SecretRef;
error: unknown;
}): boolean {
const refId = params.ref.id.trim();
// Canonical refs already own their provider identity. Config defaults only fill an omitted
// provider during coercion; resolution never rewrites an explicit provider such as "default".
return (
params.error instanceof SecretRefResolutionError &&
params.error.code === "SECRET_REF_NOT_FOUND" &&
params.error.source === params.ref.source &&
params.error.provider === params.ref.provider &&
params.error.refId === refId
);
}