Agent Sandbox Registry

awesome-providers

opensandbox/src/config.mjs

无中文版

opensandbox/src/config.mjs

// Turns the `initialize` payload into a validated, immutable plugin configuration.

import { badParams } from "./errors.mjs";
import { parseAliasMap, shellSplit } from "./convert.mjs";

export const DEFAULT_BASE_URL = "http://127.0.0.1:8080";
export const DEFAULT_IMAGE = "python:3.12-slim";
export const DEFAULT_ALIASES = { code: "opensandbox/code-interpreter:v1.1.0" };
/**
 * Long-running command present on stock images. OpenSandbox requires an entrypoint and
 * the container exits when that process exits, which takes execd with it.
 * Generated images that serve port 9000 set `entrypoint` to `python3 commands.py`.
 */
export const DEFAULT_ENTRYPOINT = ["tail", "-f", "/dev/null"];
export const CODE_INTERPRETER_ENTRYPOINT = ["/opt/code-interpreter/code-interpreter.sh"];

const blank = (v) => v === undefined || v === null || (typeof v === "string" && v.trim() === "");

function toBool(value, key, fallback) {
  if (blank(value)) return fallback;
  if (typeof value === "boolean") return value;
  const text = String(value).trim().toLowerCase();
  if (["1", "true", "yes", "on"].includes(text)) return true;
  if (["0", "false", "no", "off"].includes(text)) return false;
  throw badParams(`Invalid boolean for '${key}': ${JSON.stringify(value)}.`);
}

function toInt(value, key, fallback, min) {
  if (blank(value)) return fallback;
  const n = Number(value);
  if (!Number.isInteger(n) || n < min) throw badParams(`'${key}' must be an integer >= ${min} (got ${JSON.stringify(value)}).`);
  return n;
}

/** `https://host:8080/v1/` -> `https://host:8080` (the plugin appends `/v1` itself). */
export function normalizeBaseUrl(raw) {
  let url;
  try {
    url = new URL(String(raw).trim());
  } catch {
    throw badParams(`Invalid base_url '${raw}': expected an http(s) URL such as http://127.0.0.1:8080.`);
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") {
    throw badParams(`Invalid base_url '${raw}': only http and https are supported.`);
  }
  const path = url.pathname.replace(/\/+$/, "").replace(/\/v1$/, "");
  return `${url.protocol}//${url.host}${path}`;
}

export function parseConfig(config = {}, core = {}) {
  const protocol = blank(config.protocol) ? "http" : String(config.protocol).toLowerCase();
  if (protocol !== "http" && protocol !== "https") throw badParams(`Invalid protocol '${config.protocol}': use http or https.`);

  let root;
  if (!blank(config.base_url)) root = normalizeBaseUrl(config.base_url);
  else if (!blank(config.domain)) root = normalizeBaseUrl(`${protocol}://${String(config.domain).trim()}`);
  else root = DEFAULT_BASE_URL;

  let networkPolicy;
  if (!blank(config.network_policy)) {
    try {
      networkPolicy = typeof config.network_policy === "string" ? JSON.parse(config.network_policy) : config.network_policy;
    } catch (err) {
      throw badParams(`'network_policy' must be a JSON object: ${err.message}`);
    }
  }

  let platform;
  if (!blank(config.platform)) {
    const m = /^(linux|windows)\/(amd64|arm64)$/.exec(String(config.platform).trim());
    if (!m) throw badParams(`Invalid platform '${config.platform}': use linux/amd64, linux/arm64, windows/amd64 or windows/arm64.`);
    platform = { os: m[1], arch: m[2] };
  }

  const entrypoint = blank(config.entrypoint) ? undefined : shellSplit(String(config.entrypoint));
  const httpTimeout = Number(core.http_timeout);

  const parsed = {
    baseUrl: root,
    apiBase: `${root}/v1`,
    scheme: new URL(root).protocol.replace(":", ""),
    apiKey: blank(config.api_key) ? undefined : String(config.api_key),
    useServerProxy: toBool(config.use_server_proxy, "use_server_proxy", false),
    secureAccess: toBool(config.secure_access, "secure_access", false),
    defaultImage: blank(config.default_image) ? DEFAULT_IMAGE : String(config.default_image).trim(),
    imageAliases: blank(config.image_aliases) ? { ...DEFAULT_ALIASES } : parseAliasMap(config.image_aliases),
    entrypoint,
    readyTimeout: toInt(config.ready_timeout, "ready_timeout", 60, 1),
    registryUsername: blank(config.registry_username) ? undefined : String(config.registry_username),
    registryPassword: blank(config.registry_password) ? undefined : String(config.registry_password),
    networkPolicy,
    platform,
    httpTimeoutMs: Number.isFinite(httpTimeout) && httpTimeout >= 1 ? Math.round(httpTimeout * 1000) : 30_000,
    maxRetries: Number.isInteger(core.max_retries) && core.max_retries >= 0 ? core.max_retries : 3,
    secrets: [config.api_key, config.registry_password].filter((s) => typeof s === "string" && s.length >= 4),
  };
  Object.defineProperty(parsed, "raw", { value: { ...config }, enumerable: false });
  return Object.freeze(parsed);
}

/** Options a caller may override per `sandbox.create` (never the server address or the API key). */
export const CREATE_OPTION_KEYS = Object.freeze([
  "default_image",
  "image_aliases",
  "entrypoint",
  "ready_timeout",
  "registry_username",
  "registry_password",
  "network_policy",
  "platform",
  "secure_access",
]);

/** The configuration for one create: `initialize` config plus the whitelisted per-create `options`. */
export function withCreateOptions(cfg, options) {
  if (!options || typeof options !== "object" || Array.isArray(options)) return cfg;
  const overrides = Object.fromEntries(Object.entries(options).filter(([k, v]) => CREATE_OPTION_KEYS.includes(k) && !blank(v)));
  if (Object.keys(overrides).length === 0) return cfg;
  return parseConfig({ ...cfg.raw, ...overrides }, { http_timeout: cfg.httpTimeoutMs / 1000, max_retries: cfg.maxRetries });
}