UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

213 lines (212 loc) • 13.4 kB
import { deliverEngineNotice } from "./engine-notice.js"; const GIT_CONFIG_ENV_RE = /^GIT_CONFIG_(?:COUNT|PARAMETERS|(?:KEY|VALUE)_[0-9]+)$/; const PROXY_ENV_RE = /^(?:(?:https?|ftp|all|no)_proxy|npm_config_(?:https?_)?proxy|npm_config_noproxy|yarn_proxy|(?:yarn|global_agent|docker|claude_code)_(?:https?|no)_proxy|cloudsdk_proxy_[a-z]+|electron_get_use_proxy)$/i; function isExemptEnvName(name) { return GIT_CONFIG_ENV_RE.test(name) || PROXY_ENV_RE.test(name); } const CREDENTIAL_ENV_NAMES = new Set([ "ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "CLAUDE_CODE_ARTIFACTS_API_TOKEN", "CLAUDE_CODE_MEMORY_API_TOKEN", "CLAUDE_CODE_SLACK_TAG_TOKEN", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_FOUNDRY_AUTH_TOKEN", "ANTHROPIC_AWS_API_KEY", "ANTHROPIC_CUSTOM_HEADERS", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_BEARER_TOKEN_BEDROCK", "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_GHA_CREDS_PATH", "AZURE_CLIENT_SECRET", "IDENTITY_HEADER", "MSI_SECRET", "AZURE_CLIENT_CERTIFICATE_PATH", "AZURE_CLIENT_CERTIFICATE_PASSWORD", "AZURE_PASSWORD", "AZURE_FEDERATED_TOKEN_FILE", "AWS_WEB_IDENTITY_TOKEN_FILE", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "AWS_CONTAINER_CREDENTIALS_FULL_URI", "AWS_CONTAINER_AUTHORIZATION_TOKEN", "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", "CLOUDSDK_AUTH_ACCESS_TOKEN", "GOOGLE_OAUTH_ACCESS_TOKEN", "CLAUDE_CODE_OAUTH_REFRESH_TOKEN", "HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HUGGINGFACEHUB_API_TOKEN", "NODE_AUTH_TOKEN", "NUGET_AUTH_TOKEN", "CARGO_REGISTRY_TOKEN", "TWINE_PASSWORD", "TWINE_USERNAME", "PYPI_TOKEN", "PYPI_API_TOKEN", "UV_PUBLISH_TOKEN", "UV_PUBLISH_PASSWORD", "UV_PUBLISH_USERNAME", "FLIT_PASSWORD", "FLIT_USERNAME", "HATCH_INDEX_AUTH", "HATCH_INDEX_USER", "GEM_HOST_API_KEY", "MATURIN_PYPI_TOKEN", "MATURIN_PASSWORD", "MATURIN_USERNAME", "CONAN_LOGIN_USERNAME", "CONAN_PASSWORD", "ANACONDA_API_TOKEN", "BINSTAR_API_TOKEN", "VAULT_TOKEN", "VAULT_AUTH_TOKEN", "VAULT_ROLE_ID", "VAULT_SECRET_ID", "CONSUL_HTTP_TOKEN", "CONSUL_HTTP_AUTH", "NOMAD_TOKEN", "NOMAD_HTTP_AUTH", "CI_REGISTRY_USER", "CI_DEPLOY_USER", "JF_USER", "FASTLANE_SESSION", "MATCH_GIT_BASIC_AUTHORIZATION", "SONAR_TOKEN", "SONARQUBE_SCANNER_PARAMS", "SONAR_SCANNER_JSON_PARAMS", "SLACK_WEBHOOK_URL", "SLACK_WEBHOOK", "DISCORD_WEBHOOK", "DISCORD_WEBHOOK_URL", "TEAMS_WEBHOOK_URL", "MS_TEAMS_WEBHOOK_URI", "ANTHROPIC_IDENTITY_TOKEN", "ANTHROPIC_IDENTITY_TOKEN_FILE", "CLOUDSDK_AUTH_ACCESS_TOKEN_FILE", "CLOUDSDK_AUTH_AUTHORIZATION_TOKEN_FILE", "AZURE_AUTH_LOCATION", "ACTIONS_ID_TOKEN_REQUEST_TOKEN", "ACTIONS_ID_TOKEN_REQUEST_URL", "ACTIONS_RUNTIME_TOKEN", "ACTIONS_RUNTIME_URL", "ALL_INPUTS", "VSS_NUGET_EXTERNAL_FEED_ENDPOINTS", "ARTIFACTS_CREDENTIALPROVIDER_EXTERNAL_FEED_ENDPOINTS", "VSS_NUGET_ACCESSTOKEN", "ARTIFACTS_CREDENTIALPROVIDER_ACCESSTOKEN", "COMPOSER_AUTH", "OVERRIDE_GITHUB_TOKEN", "DEFAULT_WORKFLOW_TOKEN", "SSH_SIGNING_KEY" ].flatMap((name) => [name, `INPUT_${name}`])); const OTEL_ENV_RE = /^(?:INPUT_)?OTEL_/i; const COUNTED_TOKENS = "(?<!(?:^|_)(?:MAX|OUTPUT|INPUT|THINKING|NUM|TOTAL)_)TOKENS"; const SEMA_NAMESPACE_SECRET_RE = new RegExp(`^SEMA_(?:.*_)?(?:KEY|KEYS|TOKEN|${COUNTED_TOKENS}|SECRET|SECRETS|PASSWORD|PASSWORDS|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY)(?:_|$)`, "is"); const LEADING_WORDS = [ "TOKEN", "SECRET", "PASSWORD", "PASSWD", "PASSPHRASE", "KEY", "AUTH", "COOKIE", "PAT", "DSN", "WEBHOOK", "CREDENTIAL", "CREDENTIALS", "CREDS", "APIKEY", "ACCESSKEY", "SECRETKEY", "ACCOUNTKEY", "PRIVATEKEY", "AUTHKEY", "SSHKEY", "SIGNINGKEY", "MASTERKEY", "DEPLOYKEY", "ENCRYPTIONKEY", "PGPASSWORD", "SSHPASS", ]; const PLURAL_WORDS = ["KEY", "SECRET", "PASSWORD", "CREDENTIAL"]; const SEPARATED_WORDS = ["PWD", "PASS", "JWT"]; const EMBEDDED_WORDS = ["TOKEN", "SECRET", "PASSWORD", "PASSWD", "PASSPHRASE"]; const CREDENTIAL_WORD_RE = new RegExp(`((^|_)(${LEADING_WORDS.join("|")}|(${PLURAL_WORDS.join("|")})S|${COUNTED_TOKENS})|_(${SEPARATED_WORDS.join("|")})|(${EMBEDDED_WORDS.join("|")}))(?=$|[_0-9])`, "i"); const CONNECTION_STRING_RE = /CONN(ECT(ION)?)?_?STR(ING)?S?(?=$|[_0-9])/i; const BUNDLER_SETTINGS = ["BUILD", "LOCAL", "MIRROR", "PATH", "WITH", "WITHOUT", "CACHE", "DISABLE", "IGNORE", "ONLY"]; const BUNDLER_HOST = "(?:[A-Za-z0-9]+(?:___[A-Za-z0-9]+)*__)+[A-Za-z]{2,}"; const BUNDLER_CREDENTIAL_RE = new RegExp(`^(?:INPUT_)?BUNDLE_(?!(?:${BUNDLER_SETTINGS.join("|")})__(?!${BUNDLER_HOST}$))\\w*__`, "i"); const GIT_CONFIG_KEY_NAME_RE = /^GIT_CONFIG_KEY_[0-9][A-Za-z0-9_]*$/; const PUBLISHER_PREFIX_RE = /^(?:INPUT_|ORG_GRADLE_PROJECT_|POETRY_PYPI_TOKEN_|POETRY_HTTP_BASIC_|CARGO_REGISTRIES_|CONAN_LOGIN_USERNAME_|CONAN_PASSWORD_)/i; const PUBLISHER_LOGIN_PREFIX_RE = /^(?:INPUT_)?(?:POETRY_HTTP_BASIC_|CONAN_LOGIN_USERNAME_)/i; const USER_NAME_TAIL_RE = /USER(?:_?NAME)?_?[0-9]*$/i; function splitCamelCase(name) { return ["OAuth", "NextAuth"] .reduce((text, word) => text.replaceAll(word, word[0] + word.slice(1).toLowerCase()), name) .replace(/([a-z0-9])([A-Z])/g, "$1_$2") .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2"); } function nameShapeHolds(name) { const words = name.replace(/^AUTH0_/i, "").replace(/-/g, "_"); const split = splitCamelCase(words); const shaped = CREDENTIAL_WORD_RE.test(words) || CREDENTIAL_WORD_RE.test(split) || CONNECTION_STRING_RE.test(words) || CONNECTION_STRING_RE.test(split) || BUNDLER_CREDENTIAL_RE.test(words); return shaped && !GIT_CONFIG_KEY_NAME_RE.test(name); } function nameIsCredentialShaped(name) { if (BUNDLER_CREDENTIAL_RE.test(name) || nameShapeHolds(name)) return true; const words = name.replace(/-/g, "_"); return PUBLISHER_PREFIX_RE.test(words) && (PUBLISHER_LOGIN_PREFIX_RE.test(words) || USER_NAME_TAIL_RE.test(words) || nameShapeHolds(words)); } const NON_SECRET_ENV_EXACT_NAMES = new Set([ "PARTITION_KEY", "SORT_KEY", "RANGE_KEY", "IDEMPOTENCY_KEY", "CACHE_KEY", "ROUTING_KEY", "DB_FOREIGN_KEYS", "JSON_SORT_KEYS", "MAX_NEW_TOKENS", "MAX_COMPLETION_TOKENS", "AIDER_MAP_TOKENS", "AIDER_MAX_CHAT_HISTORY_TOKENS", "MAX_BATCH_PREFILL_TOKENS", "MAX_WAITING_TOKENS", "MAX_TOP_N_TOKENS", "RATE_LIMIT_TOKENS", "PROMPT_TOKENS", "CONTEXT_TOKENS", ]); const VALUE_SCAN_LIMIT = 8192; function looksLikeToken(user) { return /^(?:gh[opusr]_|github_pat_|glpat-|xox[abpr]-|sk-|pk-|AKIA|eyJ|ya29\.|npm_)/.test(user) || (user.length >= 20 && /[0-9]/.test(user) && /[a-z]/i.test(user)); } const HOST_RE = /^(?:localhost|[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.(?=[a-z0-9-]*[a-z])[a-z0-9-]+|[a-z0-9_.-]+(?=:\d+(?:[/?#]|$))|\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-f:.]+\])(?::\d+)?(?:[/?#]|$)/i; const HOSTLIKE_RE = /^(?:(?=[a-z0-9._-]*[a-z])[a-z0-9._-]+|\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-f:.]+\])(?::\d+)?(?:[/?#]|$)/i; function isHostPortPathBeforeAt(before, after) { const hostPort = /^([a-z0-9_-]+(?:\.[a-z0-9_-]+)*):\d+([/?#].*)$/is.exec(before); if (!hostPort) return false; if (/[?#&]/.test(hostPort[2] ?? "")) return true; if (HOST_RE.test(after)) return false; if (before.endsWith("/")) return true; if (/^(?:v?\d|sha\d*:)/i.test(after)) return true; return !HOSTLIKE_RE.test(after); } function authorityCarriesCredential(rest) { const blank = rest.search(/\s/); const word = blank === -1 ? rest : rest.slice(0, blank); const at = word.lastIndexOf("@"); if (at === -1) return false; const userinfo = word.slice(0, at); if (isHostPortPathBeforeAt(userinfo, word.slice(at + 1))) return false; return userinfo.includes(":") || (!/[/?#]/.test(userinfo) && looksLikeToken(userinfo)); } const PACKAGE_REFERENCE_RE = /^(?:[\w.-]+(?::[\w.-]+)?@sha(?:256|384|512):[0-9a-f]{32,}|npm:(?:@[\w.-]+\/)?[\w.-]+@[\w.^~<>=*|+-]*|[a-z_][\w.-]*:[\w.-]+:v?\d[\w.+\[\](),-]*@(?:jar|war|ear|aar|apk|aab|pom|zip|tar|tgz|module|klib|exe|dll|so|dylib)?)$/i; function carriesSchemelessUserinfo(text) { if (!/^[^\s/@:]+:(?!\/\/)(?![\\/])[^\s@]*@[^\s/@]+/.test(text) || /^[a-z]:[\\/]/i.test(text) || PACKAGE_REFERENCE_RE.test(text)) return false; const at = text.lastIndexOf("@", text.search(/\s|$/)); return !isHostPortPathBeforeAt(text.slice(0, at), text.slice(at + 1)) && !/^(?:mailto|sips?|xmpp|im|acct):/i.test(text); } const CREDENTIAL_ASSIGNMENT_RE = /(?:(?:^|[;&?#,{]|\/:)\s*|\s)["']?(?!-*jobserver-auth\s*[=:])(?:[a-z0-9_.-]{0,64}(?:password|passwd|pwd|secret|token|(?:account|access|api|private|subscription)[-_]?key|signature|sig|credential)|[a-z0-9_.-]{0,63}[_.-]auth)["']?\s*[=:]\s*["']?(?!(?:true|false|none|null|yes|no|on|off|enabled|disabled|required|optional)(?:$|[;&,\s"']))[^;&,\s"']+/i; const WEBHOOK_URL_RE = /https:\/\/(?:hooks\.slack\.com\/(?:services|workflows|triggers)\/|(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/|[\w.-]+\.webhook\.office\.com\/webhookb2\/)[\w\/@.~-]{16,}/i; const SONAR_LOGIN_RE = /["']?sonar\.login["']?\s*[:=]\s*["']?[^\s"',}]{8,}/; const PRIVATE_KEY_HEADER_RE = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/; const AUTHORIZATION_HEADER_RE = /\bauthorization\s*[:=]\s*["']?[a-z][a-z0-9_-]*\s+[A-Za-z0-9._~+\/=-]{8,}/i; const TOKEN_WORD_RE = /\b(?:[Tt]oken|TOKEN)\s+(?=[A-Za-z0-9_~+=-]*[0-9])(?=[A-Za-z0-9_~+=-]*[A-Z])(?=([A-Za-z0-9_~+=-]{20,}))\1(?![\/.])/; const BEARER_BASIC_RE = /\b(?:Bearer|Basic)\s+(?=[A-Za-z._~+\/=-]*[0-9]|(?:[A-Za-z0-9._~+\/=-]*?[a-z][A-Z](?![a-z])){2}|[A-Za-z0-9.-]*[_~+\/=])[A-Za-z0-9._~+\/=-]{8,}/; function valueCarriesCredential(value) { const text = value.length > VALUE_SCAN_LIMIT ? value.slice(0, VALUE_SCAN_LIMIT) : value; for (let at = text.indexOf("://"); at !== -1; at = text.indexOf("://", at + 3)) { if (at > 0 && /[a-z0-9+.-]/i.test(text[at - 1] ?? "") && authorityCarriesCredential(text.slice(at + 3))) return true; } if (text.startsWith("//") && authorityCarriesCredential(text.slice(2))) return true; return (carriesSchemelessUserinfo(text) || CREDENTIAL_ASSIGNMENT_RE.test(text) || WEBHOOK_URL_RE.test(text) || SONAR_LOGIN_RE.test(text) || PRIVATE_KEY_HEADER_RE.test(text) || AUTHORIZATION_HEADER_RE.test(text) || TOKEN_WORD_RE.test(text) || BEARER_BASIC_RE.test(text)); } const SECRET_ENV_KIND_CONFIDENCE = { "exact-name": "high", "prefix-rule": "high", "value-rule": "high", "name-rule": "medium", "namespace-rule": "medium", }; function classifySecretEnvEntry(name, value) { if (isExemptEnvName(name)) return undefined; if (CREDENTIAL_ENV_NAMES.has(name.toUpperCase().replace(/-/g, "_"))) return "exact-name"; if (OTEL_ENV_RE.test(name)) return "prefix-rule"; if (SEMA_NAMESPACE_SECRET_RE.test(name)) return "namespace-rule"; if (!NON_SECRET_ENV_EXACT_NAMES.has(name.toUpperCase()) && nameIsCredentialShaped(name)) return "name-rule"; return value !== undefined && valueCarriesCredential(typeof value === "string" ? value : String(value)) ? "value-rule" : undefined; } export function isSecretEnvKey(key, value) { return classifySecretEnvEntry(key, value) !== undefined; } export function scrubSecretEnv(env, findings) { const kept = []; for (const key of Object.keys(env)) { const value = env[key]; const kind = classifySecretEnvEntry(key, value); if (kind === undefined) kept.push([key, value]); else findings?.push({ key, kind, confidence: SECRET_ENV_KIND_CONFIDENCE[kind], source: "secret-env" }); } return Object.fromEntries(kept); } export function inheritedHostEnv(policy, findings) { if (policy === "all") return { ...process.env }; if (Array.isArray(policy)) { const picked = {}; for (const k of policy) if (process.env[k] !== undefined) picked[k] = process.env[k]; return picked; } return scrubSecretEnv(process.env, findings); } export function inheritEnvPolicyProblem(value) { if (value === undefined || value === "all" || value === "scrub") return undefined; if (Array.isArray(value)) { const bad = value.findIndex((k) => typeof k !== "string" || k.length === 0 || /[=\0]/.test(k)); if (bad === -1) return undefined; const k = value[bad]; return `inheritEnv[${bad}] is not a variable name (got ${typeof k !== "string" ? typeof k : k === "" ? "an empty string" : `a name holding ${k.includes("=") ? "\"=\"" : "a NUL"}`})`; } return `inheritEnv must be "scrub", "all" or a list of variable names (got ${typeof value === "string" ? JSON.stringify(value) : value === null ? "null" : typeof value})`; } let secretEnvScrubNoticeAnnounced = false; export function __resetSecretEnvScrubNoticeAnnouncement() { secretEnvScrubNoticeAnnounced = false; } export function announceSecretEnvScrub(onNotice, findings) { if (findings.length === 0 || secretEnvScrubNoticeAnnounced) return; secretEnvScrubNoticeAnnounced = true; const names = findings.map((f) => f.key); deliverEngineNotice(onNotice, { code: "config.secret_env_scrubbed", message: `secret_env_scrubbed: ${findings.length} env key(s) withheld from a spawned child's environment (${names.join(", ")}) — once per process; wire onNotice for the structured form`, detail: { names }, }); }