openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
156 lines (155 loc) • 6.91 kB
JavaScript
import { t as captureSecretRedactionRegistrySnapshot } from "./secret-redaction-registry-kd5IVWi3.mjs";
import { r as normalizeProviderId } from "./provider-id-D1VImdKk.mjs";
import { t as _usingCtx } from "./usingCtx-BhiZp_rS.mjs";
import { r as findConfiguredProviderModel } from "./model-provider-config-BVE2xsBm.mjs";
import { a as resolveProviderModelRoutes, t as createProviderModelCatalogIdNormalizer } from "./provider-model-routes-Cg2A4PAY.mjs";
import { t as normalizeExactAllowedHost } from "./exact-hostname-B5MIU7_E.mjs";
import { u as sealSecretSentinel } from "./sentinel-BG8eTG8r.mjs";
import { _ as resolveProviderConfigSecretInput } from "./model-auth-provider-config-Cwa51-db.mjs";
import { t as startSecretEgressProxyServer } from "./proxy-server-CQpevfd6.mjs";
import { t as resolveSecretRefString } from "./resolve-37oyiAde.mjs";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import { StringDecoder } from "node:string_decoder";
//#region src/logging/redacting-stream.ts
function createRedactingStreamWriter(target, redactValues) {
const decoder = new StringDecoder("utf8");
const values = redactValues.filter(Boolean).toSorted((left, right) => right.length - left.length);
const firstCharacters = new Set(values.map((value) => value.charAt(0)));
let pending = "";
const emit = (ending) => {
let output = "";
let cursor = 0;
while (cursor < pending.length) {
const value = firstCharacters.has(pending.charAt(cursor)) ? values.find((candidate) => pending.startsWith(candidate, cursor) || !ending && pending.length - cursor < candidate.length && candidate.startsWith(pending.slice(cursor))) : void 0;
if (!value) {
output += pending[cursor];
cursor += 1;
} else if (cursor + value.length > pending.length) break;
else {
output += "<redacted>";
cursor += value.length;
}
}
pending = pending.slice(cursor);
return !output || target.write(output);
};
return {
write: (chunk) => {
pending += decoder.write(chunk);
return emit(false);
},
flush: () => {
pending += decoder.end();
emit(true);
}
};
}
//#endregion
//#region src/secrets/model-egress.ts
function hasEntries(value) {
return value !== void 0 && Object.keys(value).length > 0;
}
function resolveModelEgressSelection(params) {
const provider = normalizeProviderId(params.provider);
const model = params.model.trim();
if (!provider || !model) throw new Error("Model egress requires an explicit provider and model");
const { providerConfig, ref } = resolveProviderConfigSecretInput(params.config, provider);
if (!providerConfig) throw new Error("Model egress requires a configured provider");
if (!ref || providerConfig.auth !== void 0 && providerConfig.auth !== "api-key") throw new Error("Model egress requires a configured API-key SecretRef; auth profiles and OAuth are unsupported");
const configuredModel = findConfiguredProviderModel(providerConfig, provider, model, createProviderModelCatalogIdNormalizer(provider));
if (hasEntries(providerConfig.headers) || hasEntries(configuredModel?.headers) || hasEntries(providerConfig.request) || providerConfig.authHeader === false || providerConfig.localService !== void 0) throw new Error("Model egress does not support custom request headers, authentication, proxy, TLS, or local-service configuration");
const resolution = resolveProviderModelRoutes({
provider,
modelId: model,
config: params.config
});
if (resolution?.kind === "incompatible") throw new Error(resolution.message);
const route = resolution ? resolution.kind === "routes" ? resolution.routes.find((candidate) => candidate.authRequirement === "api-key") : void 0 : {
api: configuredModel?.api ?? providerConfig.api,
baseUrl: configuredModel?.baseUrl ?? providerConfig.baseUrl
};
if (!route?.baseUrl || route.api !== "openai-responses" && route.api !== "openai-completions") throw new Error("Model egress requires an OpenAI-compatible API-key model route");
let url;
try {
url = new URL(route.baseUrl);
} catch {
throw new Error("Model egress requires a valid HTTPS model base URL");
}
if (url.protocol !== "https:" || url.port || url.username || url.password || url.search || url.hash) throw new Error("Model egress requires an HTTPS model base URL on port 443 without credentials, query, or fragment");
return {
provider,
model,
ref,
baseUrl: url.href,
allowedHosts: [normalizeExactAllowedHost(url.hostname)]
};
}
/** Owns one explicit CLI job's protected model credential and its revocable egress path. */
async function withConfiguredModelEgress(params, run) {
try {
var _usingCtx$1 = _usingCtx();
params.signal?.throwIfAborted();
const selection = resolveModelEgressSelection(params);
const apiKey = await resolveSecretRefString(selection.ref, { config: params.config });
params.signal?.throwIfAborted();
if (Buffer.byteLength(apiKey) > 65536) throw new Error("Model egress API key exceeds the protected credential size limit");
const sentinel = sealSecretSentinel(apiKey, { label: `model-egress:${selection.provider}` });
const resources = _usingCtx$1.a(new AsyncDisposableStack());
const caDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-model-egress-"));
resources.defer(() => fs.rm(caDir, {
recursive: true,
force: true
}));
params.signal?.throwIfAborted();
const proxy = await startSecretEgressProxyServer({
caDir,
allowedHosts: selection.allowedHosts,
onAudit: () => {}
});
resources.defer(() => proxy.stop());
params.signal?.throwIfAborted();
const grant = proxy.registerProcess([{
name: `${selection.provider} model API key`,
sentinel,
allowedHosts: selection.allowedHosts
}]);
resources.defer(() => {
grant.revoke();
params.signal?.removeEventListener("abort", grant.revoke);
});
params.signal?.addEventListener("abort", grant.revoke, { once: true });
params.signal?.throwIfAborted();
const caBundle = await fs.readFile(grant.env.NODE_EXTRA_CA_CERTS, "utf8");
params.signal?.throwIfAborted();
const values = [...captureSecretRedactionRegistrySnapshot().values, grant.env.HTTPS_PROXY];
const output = (stream) => createRedactingStreamWriter({ write(text) {
params.onOutput?.(text, stream);
return true;
} }, values);
const stdout = output("stdout");
const stderr = output("stderr");
resources.defer(stdout.flush);
resources.defer(stderr.flush);
const result = await run({
sentinel,
baseUrl: selection.baseUrl,
model: selection.model,
allowedHosts: selection.allowedHosts,
hostEnv: grant.env,
caBundle,
onOutputChunk: params.onOutput ? (chunk, stream) => {
(stream === "stdout" ? stdout : stderr).write(chunk);
} : void 0
});
params.signal?.throwIfAborted();
return result;
} catch (_) {
_usingCtx$1.e = _;
} finally {
await _usingCtx$1.d();
}
}
//#endregion
export { withConfiguredModelEgress };