File size: 4,900 Bytes
63522a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | import { SettingsClient } from "@openhands/typescript-client/clients";
import { isSdkHttpStatusError } from "./agent-server-compatibility";
import { getActiveBackend } from "./backend-registry/active-store";
import {
deleteCloudSecret,
fetchCloudSecrets,
saveCloudSecret,
} from "./cloud/secrets-service.api";
import { getAgentServerClientOptions } from "./agent-server-client-options";
import { CustomSecretWithoutValue } from "./secrets-service.types";
import { withRetry } from "./with-retry";
async function fetchSecrets(): Promise<CustomSecretWithoutValue[]> {
if (getActiveBackend().backend.kind === "cloud") {
return withRetry(() => fetchCloudSecrets());
}
const response = await withRetry(() =>
new SettingsClient(getAgentServerClientOptions()).listSecrets(),
);
return response.secrets.map((s) => ({
name: s.name,
description: s.description,
}));
}
export class SecretsService {
/**
* List all custom secrets (names and descriptions only, no values).
* Uses the agent-server API endpoint: GET /api/settings/secrets
*
* Note: The agent-server API doesn't support pagination or search filtering.
* All secrets are returned in a single response.
*/
static async getSecrets(): Promise<CustomSecretWithoutValue[]> {
try {
return await fetchSecrets();
} catch (error) {
console.error("Failed to fetch secrets after retries:", error);
return [];
}
}
/**
* List all custom secrets, surfacing failures to callers that must
* distinguish an unavailable list from an empty one.
*/
static async getSecretsOrThrow(): Promise<CustomSecretWithoutValue[]> {
return fetchSecrets();
}
/**
* Create or update a custom secret (upsert by name).
* Uses the agent-server API endpoint: PUT /api/settings/secrets
*
* @param name - Secret name (must start with letter, contain only letters/numbers/underscores, 1-64 chars)
* @param value - Secret value
* @param description - Optional description
* @throws Error if the API call fails after retries
*/
static async createSecret(
name: string,
value: string,
description?: string,
): Promise<void> {
if (getActiveBackend().backend.kind === "cloud") {
await saveCloudSecret({ name, value, description });
return;
}
await withRetry(() =>
new SettingsClient(getAgentServerClientOptions()).upsertSecret({
name,
value,
description,
}),
);
}
/**
* Update a secret's name and/or description, and optionally overwrite its
* value. When no value is given the existing one is preserved: the
* agent-server only exposes an upsert endpoint, so we fetch the existing
* value and re-upsert it under the updated name/description.
*
* @param secretToEdit - Existing secret name
* @param name - New (or same) secret name
* @param description - Optional new description
* @param value - Optional new value; when omitted the stored value is kept
* @throws Error if the API call fails after retries
*/
static async updateSecret(
secretToEdit: string,
name: string,
description?: string,
value?: string,
): Promise<void> {
if (getActiveBackend().backend.kind === "cloud") {
await saveCloudSecret({
name,
value,
description,
previousName: secretToEdit,
});
return;
}
const client = new SettingsClient(getAgentServerClientOptions());
const nextValue =
value ?? (await withRetry(() => client.getSecret(secretToEdit)));
await withRetry(() =>
client.upsertSecret({
name,
value: nextValue,
description,
}),
);
if (name !== secretToEdit) {
await this.deleteSecret(secretToEdit);
}
}
/**
* Delete a custom secret by name.
* Uses the agent-server API endpoint: DELETE /api/settings/secrets/{name}
*
* @param name - Secret name to delete
* @throws Error if the API call fails (except 404, which is treated as success)
*/
static async deleteSecret(name: string): Promise<void> {
try {
if (getActiveBackend().backend.kind === "cloud") {
await withRetry(() => deleteCloudSecret(name));
return;
}
await withRetry(() =>
new SettingsClient(getAgentServerClientOptions()).deleteSecret(name),
);
} catch (error) {
// 404 means secret doesn't exist - treat as successful deletion.
// Both the SDK's HttpError (status on the error itself) and
// axios-style errors (status under `response`) count.
if (
isSdkHttpStatusError(error, 404) ||
(error &&
typeof error === "object" &&
"response" in error &&
(error as { response?: { status?: number } }).response?.status ===
404)
) {
return;
}
throw error;
}
}
}
|