mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-18 21:00:49 +00:00
Use prebuilt provider base images
This commit is contained in:
parent
5a3e185d1c
commit
c2e5dd6038
4 changed files with 209 additions and 79 deletions
|
|
@ -1,28 +1,67 @@
|
||||||
import { Daytona } from "@daytonaio/sdk";
|
import { Daytona, Image } from "@daytonaio/sdk";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import { detectAgent, buildInspectorUrl, generateInstallCommand } from "@sandbox-agent/example-shared";
|
import {
|
||||||
|
SANDBOX_AGENT_IMAGE,
|
||||||
|
SANDBOX_AGENT_INSTALL_VERSION,
|
||||||
|
buildCredentialEnv,
|
||||||
|
buildInspectorUrl,
|
||||||
|
detectAgent,
|
||||||
|
generateBaseImageDockerfile,
|
||||||
|
getPreinstallComponents,
|
||||||
|
} from "@sandbox-agent/example-shared";
|
||||||
|
|
||||||
const daytona = new Daytona();
|
const daytona = new Daytona();
|
||||||
|
|
||||||
const envVars: Record<string, string> = {};
|
const envVars = buildCredentialEnv();
|
||||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
const agent = detectAgent();
|
||||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
const components = getPreinstallComponents(agent);
|
||||||
|
const componentSuffix = components.length > 0 ? components.join("-") : "base";
|
||||||
|
const baseImage = process.env.SANDBOX_AGENT_DAYTONA_IMAGE ?? SANDBOX_AGENT_IMAGE;
|
||||||
|
const snapshotName = process.env.SANDBOX_AGENT_DAYTONA_SNAPSHOT ?? `sandbox-agent-${SANDBOX_AGENT_INSTALL_VERSION.replaceAll(".", "-")}-${componentSuffix}`;
|
||||||
|
|
||||||
// Use default image and install sandbox-agent at runtime (faster startup, no snapshot build)
|
async function ensureSnapshot(name: string) {
|
||||||
console.log("Creating Daytona sandbox...");
|
try {
|
||||||
const sandbox = await daytona.create({ envVars, autoStopInterval: 0 });
|
return await daytona.snapshot.get(name);
|
||||||
|
} catch {
|
||||||
|
console.log(`Building Daytona snapshot ${name} from ${baseImage}...`);
|
||||||
|
const dockerfileDir = fs.mkdtempSync(path.join(os.tmpdir(), "sandbox-agent-daytona-"));
|
||||||
|
const dockerfilePath = path.join(dockerfileDir, "Dockerfile");
|
||||||
|
fs.writeFileSync(
|
||||||
|
dockerfilePath,
|
||||||
|
generateBaseImageDockerfile({
|
||||||
|
image: baseImage,
|
||||||
|
components,
|
||||||
|
}),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
// Install sandbox-agent and start server
|
const image = Image.fromDockerfile(dockerfilePath)
|
||||||
console.log("Installing sandbox-agent...");
|
.workdir("/home/sandbox")
|
||||||
await sandbox.process.executeCommand(generateInstallCommand({ components: ["claude", "codex"] }));
|
.entrypoint(["sandbox-agent", "server", "--no-token", "--host", "0.0.0.0", "--port", "3000"]);
|
||||||
|
|
||||||
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
try {
|
||||||
|
const snapshot = await daytona.snapshot.create({ name, image }, { timeout: 180, onLogs: (line) => console.log(line) });
|
||||||
|
|
||||||
|
return await daytona.snapshot.activate(snapshot).catch(() => snapshot);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dockerfileDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = await ensureSnapshot(snapshotName);
|
||||||
|
|
||||||
|
console.log(`Creating Daytona sandbox from snapshot ${snapshot.name}...`);
|
||||||
|
const sandbox = await daytona.create({ envVars, snapshot: snapshot.name, autoStopInterval: 0 }, { timeout: 180 });
|
||||||
|
|
||||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||||
|
|
||||||
console.log("Connecting to server...");
|
console.log("Connecting to server...");
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/daytona", mcpServers: [] } });
|
const session = await client.createSession({ agent, sessionInit: { cwd: "/home/sandbox", mcpServers: [] } });
|
||||||
const sessionId = session.id;
|
const sessionId = session.id;
|
||||||
|
|
||||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,78 @@
|
||||||
import { Sandbox } from "@e2b/code-interpreter";
|
import { Sandbox, Template, defaultBuildLogger } from "@e2b/code-interpreter";
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import { detectAgent, buildInspectorUrl, generateInstallCommand } from "@sandbox-agent/example-shared";
|
import {
|
||||||
|
SANDBOX_AGENT_IMAGE,
|
||||||
|
SANDBOX_AGENT_INSTALL_VERSION,
|
||||||
|
buildCredentialEnv,
|
||||||
|
buildInspectorUrl,
|
||||||
|
detectAgent,
|
||||||
|
getPreinstallComponents,
|
||||||
|
} from "@sandbox-agent/example-shared";
|
||||||
|
|
||||||
const envs: Record<string, string> = {};
|
const envs = buildCredentialEnv();
|
||||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
const agent = detectAgent();
|
||||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
const components = getPreinstallComponents(agent);
|
||||||
|
const componentSuffix = components.length > 0 ? components.join("-") : "base";
|
||||||
|
const baseImage = process.env.SANDBOX_AGENT_E2B_IMAGE ?? SANDBOX_AGENT_IMAGE;
|
||||||
|
const templateName = process.env.SANDBOX_AGENT_E2B_TEMPLATE ?? `sandbox-agent-${SANDBOX_AGENT_INSTALL_VERSION.replaceAll(".", "-")}-${componentSuffix}`;
|
||||||
|
|
||||||
console.log("Creating E2B sandbox...");
|
async function ensureTemplate(name: string): Promise<string> {
|
||||||
const sandbox = await Sandbox.create({ allowInternetAccess: true, envs });
|
if (await Template.exists(name)) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return buildTemplate(name);
|
||||||
|
}
|
||||||
|
|
||||||
const run = async (cmd: string) => {
|
async function buildTemplate(name: string): Promise<string> {
|
||||||
const result = await sandbox.commands.run(cmd);
|
console.log(`Building E2B template ${name} from ${baseImage}...`);
|
||||||
if (result.exitCode !== 0) throw new Error(`Command failed: ${cmd}\n${result.stderr}`);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Installing sandbox-agent...");
|
let templateBuilder = Template().fromImage(baseImage);
|
||||||
await run(generateInstallCommand({ components: ["claude", "codex"] }));
|
if (components.includes("codex")) {
|
||||||
|
templateBuilder = templateBuilder.setUser("root").aptInstall("npm").setUser("user");
|
||||||
|
}
|
||||||
|
for (const component of components) {
|
||||||
|
templateBuilder = templateBuilder.runCmd(`sandbox-agent install-agent ${component}`);
|
||||||
|
}
|
||||||
|
const template = templateBuilder;
|
||||||
|
|
||||||
console.log("Starting server...");
|
await Template.build(template, name, {
|
||||||
await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true, timeoutMs: 0 });
|
onBuildLogs: defaultBuildLogger(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMissingTemplateError(error: unknown): boolean {
|
||||||
|
return error instanceof Error && /template '.*' not found/.test(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedTemplate = await ensureTemplate(templateName);
|
||||||
|
|
||||||
|
console.log(`Creating E2B sandbox from template ${resolvedTemplate}...`);
|
||||||
|
let sandbox;
|
||||||
|
try {
|
||||||
|
sandbox = await Sandbox.create(resolvedTemplate, { allowInternetAccess: true, envs });
|
||||||
|
} catch (error) {
|
||||||
|
if (!process.env.SANDBOX_AGENT_E2B_TEMPLATE && isMissingTemplateError(error)) {
|
||||||
|
const fallbackTemplate = `${templateName}-${Date.now()}`;
|
||||||
|
console.log(`Template ${resolvedTemplate} is stale; rebuilding as ${fallbackTemplate}...`);
|
||||||
|
sandbox = await Sandbox.create(await buildTemplate(fallbackTemplate), { allowInternetAccess: true, envs });
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = `https://${sandbox.getHost(3000)}`;
|
const baseUrl = `https://${sandbox.getHost(3000)}`;
|
||||||
|
const token = sandbox.trafficAccessToken;
|
||||||
|
|
||||||
|
await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true });
|
||||||
|
|
||||||
console.log("Connecting to server...");
|
console.log("Connecting to server...");
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
const client = await SandboxAgent.connect({ baseUrl, token });
|
||||||
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/user", mcpServers: [] } });
|
const session = await client.createSession({ agent, sessionInit: { cwd: "/home/user", mcpServers: [] } });
|
||||||
const sessionId = session.id;
|
const sessionId = session.id;
|
||||||
|
|
||||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, token, sessionId })}`);
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import { PassThrough } from "node:stream";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const EXAMPLE_IMAGE = "sandbox-agent-examples:latest";
|
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
|
||||||
const EXAMPLE_IMAGE_DEV = "sandbox-agent-examples-dev:latest";
|
|
||||||
const DOCKERFILE_DIR = path.resolve(__dirname, "..");
|
/** Pre-built Docker image with all agents installed. */
|
||||||
const REPO_ROOT = path.resolve(DOCKERFILE_DIR, "../..");
|
export const FULL_IMAGE = "rivetdev/sandbox-agent:0.3.1-full";
|
||||||
|
|
||||||
export interface DockerSandboxOptions {
|
export interface DockerSandboxOptions {
|
||||||
/** Container port used by sandbox-agent inside Docker. */
|
/** Container port used by sandbox-agent inside Docker. */
|
||||||
|
|
@ -18,7 +18,7 @@ export interface DockerSandboxOptions {
|
||||||
hostPort?: number;
|
hostPort?: number;
|
||||||
/** Additional shell commands to run before starting sandbox-agent. */
|
/** Additional shell commands to run before starting sandbox-agent. */
|
||||||
setupCommands?: string[];
|
setupCommands?: string[];
|
||||||
/** Docker image to use. Defaults to the pre-built sandbox-agent-examples image. */
|
/** Docker image to use. Defaults to the pre-built full image. */
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,33 +131,44 @@ function stripAnsi(value: string): string {
|
||||||
return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, "");
|
return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureExampleImage(_docker: Docker): Promise<string> {
|
async function ensureImage(docker: Docker, image: string): Promise<void> {
|
||||||
const dev = !!process.env.SANDBOX_AGENT_DEV;
|
const buildFromSource = () => {
|
||||||
const imageName = dev ? EXAMPLE_IMAGE_DEV : EXAMPLE_IMAGE;
|
console.log(" Building sandbox image from source (may take a while)...");
|
||||||
|
try {
|
||||||
|
execFileSync("docker", ["build", "-t", image, "-f", path.join(REPO_ROOT, "docker/runtime/Dockerfile.full"), REPO_ROOT], {
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`Failed to build sandbox image: ${message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (dev) {
|
if (process.env.SANDBOX_AGENT_DEV) {
|
||||||
console.log(" Building sandbox image from source (may take a while, only runs once)...");
|
buildFromSource();
|
||||||
try {
|
return;
|
||||||
execFileSync("docker", ["build", "-t", imageName, "-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"), REPO_ROOT], {
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
|
||||||
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(" Building sandbox image (may take a while, only runs once)...");
|
|
||||||
try {
|
|
||||||
execFileSync("docker", ["build", "-t", imageName, DOCKERFILE_DIR], {
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
|
||||||
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageName;
|
try {
|
||||||
|
await docker.getImage(image).inspect();
|
||||||
|
return;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
console.log(` Pulling ${image}...`);
|
||||||
|
const pulled = await new Promise<boolean>((resolve) => {
|
||||||
|
docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
||||||
|
if (err) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
docker.modem.followProgress(stream, (progressErr: Error | null) => resolve(!progressErr));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pulled) {
|
||||||
|
console.log(` Could not pull ${image}; falling back to a local full-image build.`);
|
||||||
|
buildFromSource();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -166,8 +177,7 @@ async function ensureExampleImage(_docker: Docker): Promise<string> {
|
||||||
*/
|
*/
|
||||||
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
|
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
|
||||||
const { port, hostPort } = opts;
|
const { port, hostPort } = opts;
|
||||||
const useCustomImage = !!opts.image;
|
const image = opts.image ?? FULL_IMAGE;
|
||||||
let image = opts.image ?? EXAMPLE_IMAGE;
|
|
||||||
// TODO: Replace setupCommands shell bootstrapping with native sandbox-agent exec API once available.
|
// TODO: Replace setupCommands shell bootstrapping with native sandbox-agent exec API once available.
|
||||||
const setupCommands = [...(opts.setupCommands ?? [])];
|
const setupCommands = [...(opts.setupCommands ?? [])];
|
||||||
const credentialEnv = collectCredentialEnv();
|
const credentialEnv = collectCredentialEnv();
|
||||||
|
|
@ -197,27 +207,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||||
|
|
||||||
if (useCustomImage) {
|
await ensureImage(docker, image);
|
||||||
try {
|
|
||||||
await docker.getImage(image).inspect();
|
|
||||||
} catch {
|
|
||||||
console.log(` Pulling ${image}...`);
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
image = await ensureExampleImage(docker);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`];
|
const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`];
|
||||||
|
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: image,
|
Image: image,
|
||||||
WorkingDir: "/root",
|
WorkingDir: "/home/sandbox",
|
||||||
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
||||||
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
|
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
|
||||||
ExposedPorts: { [`${port}/tcp`]: {} },
|
ExposedPorts: { [`${port}/tcp`]: {} },
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const SANDBOX_AGENT_INSTALL_VERSION = "0.3.1";
|
export const SANDBOX_AGENT_INSTALL_VERSION = "0.3.1";
|
||||||
|
export const SANDBOX_AGENT_IMAGE = `rivetdev/sandbox-agent:${SANDBOX_AGENT_INSTALL_VERSION}`;
|
||||||
|
|
||||||
export type SandboxAgentComponent = "claude" | "codex" | "opencode" | "amp";
|
export type SandboxAgentComponent = "claude" | "codex" | "opencode" | "amp";
|
||||||
|
|
||||||
|
const DIRECT_CREDENTIAL_KEYS = [
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"CLAUDE_API_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN",
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
"CODEX_API_KEY",
|
||||||
|
"CEREBRAS_API_KEY",
|
||||||
|
"OPENCODE_API_KEY",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function generateInstallCommand({
|
export function generateInstallCommand({
|
||||||
version = SANDBOX_AGENT_INSTALL_VERSION,
|
version = SANDBOX_AGENT_INSTALL_VERSION,
|
||||||
components = [],
|
components = [],
|
||||||
|
|
@ -22,6 +34,45 @@ export function generateInstallCommand({
|
||||||
].join(" && ");
|
].join(" && ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPreinstallComponents(agent: string): SandboxAgentComponent[] {
|
||||||
|
return ["claude", "codex", "opencode", "amp"].includes(agent) ? [agent as SandboxAgentComponent] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateBaseImageDockerfile({
|
||||||
|
image = SANDBOX_AGENT_IMAGE,
|
||||||
|
components = [],
|
||||||
|
}: {
|
||||||
|
image?: string;
|
||||||
|
components?: SandboxAgentComponent[];
|
||||||
|
} = {}): string {
|
||||||
|
const uniqueComponents = [...new Set(components)];
|
||||||
|
const lines = [`FROM ${image}`];
|
||||||
|
|
||||||
|
if (uniqueComponents.includes("codex")) {
|
||||||
|
lines.push("USER root");
|
||||||
|
lines.push("RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y npm && rm -rf /var/lib/apt/lists/*");
|
||||||
|
lines.push("USER sandbox");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("WORKDIR /home/sandbox");
|
||||||
|
for (const component of uniqueComponents) {
|
||||||
|
lines.push(`RUN sandbox-agent install-agent ${component}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join("\n")}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCredentialEnv(): Record<string, string> {
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
for (const key of DIRECT_CREDENTIAL_KEYS) {
|
||||||
|
const value = process.env[key];
|
||||||
|
if (value) {
|
||||||
|
env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBaseUrl(baseUrl: string): string {
|
function normalizeBaseUrl(baseUrl: string): string {
|
||||||
return baseUrl.replace(/\/+$/, "");
|
return baseUrl.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue