@molecule/api-agent-runtime-claude-code

Provider bond · agent-run · API (Node) · v1.1.5 · Apache-2.0

Claude Code agent runtime — unattended runs in ephemeral cloud sandboxes; artifacts-only return, per-run credentials, deny-by-default egress

npm install @molecule/api-agent-runtime-claude-code

npm · Source on GitHub · Implements @molecule/api-agent-run

How it works

@molecule/api-agent-runtime-claude-code is a provider bond on the API (Node) side: it implements the agent-run core interface (@molecule/api-agent-run) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

Works with: @molecule/api-agent-run, @molecule/api-ai-tools, @molecule/api-code-sandbox

Reference

Claude Code agent runtime — unattended runs in ephemeral cloud sandboxes; artifacts-only return, per-run credentials, deny-by-default egress

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Type

provider

Installation

npm install @molecule/api-agent-runtime-claude-code @molecule/api-agent-run @molecule/api-ai-tools @molecule/api-code-sandbox

API

Interfaces

ClaudeCodeRuntimeConfig

Options for createProvider.

interface ClaudeCodeRuntimeConfig extends AgentRunConfig {
  /**
   * npm package spec for the Claude Code CLI, installed at run start from the
   * egress-allowed npm registry. Default
   * `@anthropic-ai/claude-code@2.1.289` — PINNED EXACTLY, never `@latest`:
   * its postinstall runs in the sandbox, so the version that runs there must
   * be a deliberate choice.
   */
  cliPackage?: string
  /**
   * Model id passed to the CLI when a spec names none. This is a CATALOG id
   * (`claude-sonnet-5-5`); the runtime maps catalog ids to the CLI's model
   * argument. Default `claude-sonnet-5-5`.
   */
  defaultModel?: string
  /**
   * Extra seconds granted to the ENVIRONMENT beyond the task budget — the
   * clone + CLI install happen before the agent starts, and the artifact
   * collection happens after. Default 240000 (4 minutes).
   */
  setupSlackMs?: number
}

Classes

ClaudeCodeAgentRuntime

Claude Code agent runtime backed by an ephemeral cloud sandbox.

class ClaudeCodeAgentRuntime implements AgentRuntimeProvider {
  readonly name = 'claude-code'

  private readonly defaults: Required<Omit<ClaudeCodeRuntimeConfig, 'cliPackage'>> & {
    cliPackage: string
  }

  constructor(config: ClaudeCodeRuntimeConfig = {}) {
    this.defaults = {
      timeoutMs: config.timeoutMs ?? 600_000,
      cliPackage: config.cliPackage ?? '@anthropic-ai/claude-code@2.1.289',
      defaultModel: config.defaultModel ?? 'claude-sonnet-5-5',
      setupSlackMs: config.setupSlackMs ?? 240_000,
    }
  }

  async run(spec: AgentRunSpec, opts: AgentRunOptions): Promise<AgentRunArtifact> {
    const provider = requireSandboxProvider()
    const totalBudgetMs = spec.timeoutMs ?? this.defaults.timeoutMs
    const sandboxBudgetMs = totalBudgetMs + this.defaults.setupSlackMs
    const started = Date.now()
    const logs: string[] = []
    const log = (line: string): void => {
      logs.push(line)
      try {
        opts.onLog?.(line)
      } catch (_error) {}
    }

    const allowOut = [...new Set([...RUNTIME_ALLOWED_HOSTS, ...(spec.allowedHosts ?? [])])]
    const abortCheck = (): void => {
      if (opts.signal?.aborted) throw new CANCELLED()
    }

    let sandboxId: string | undefined
    try {
      abortCheck()

      if (!GITHUB_REPO_URL_RE.test(spec.repoUrl)) {
        throw new Error(
          `repoUrl must be an https GitHub repo URL (https://github.com/owner/repo), got: ${JSON.stringify(spec.repoUrl.slice(0, 100))}`,
        )
      }
      if (opts.env.GITHUB_TOKEN && !GITHUB_TOKEN_RE.test(opts.env.GITHUB_TOKEN)) {
        throw new Error(
          'GITHUB_TOKEN does not look like a GitHub token (expected ghp_/gho_/ghu_/ghs_/github_pat_ followed by at least 20 more characters) — refusing the run rather than placing a malformed credential into the sandbox.',
        )
      }
      assertProbeSafeHosts(allowOut)

      const handle: NetworkCapableSandbox = await provider.create({
        projectId: `agent-run-${Date.now().toString(36)}`,
        env: {},
        labels: { 'molecule.agent-run': '1' },
      })
      sandboxId = handle.id

      if (typeof handle.applyNetwork !== 'function') {
        throw new Error('sandbox provider cannot enforce per-run egress; refusing agent run')
      }
      await handle.applyNetwork(allowOut)
      await assertEgress(handle, allowOut)
      abortCheck()

      const token = opts.env.GITHUB_TOKEN ?? ''
      const askpass = token ? buildAskpassHelper(token) : null
      const gitEnvPrefix = askpass
        ? `GIT_ASKPASS=${shellQuote(askpass.path)} GIT_TERMINAL_PROMPT=0 `
        : ''
      if (askpass) {
        const staged = await handle.exec(askpass.stageCommand, { timeout: 5_000 })
        if (staged.exitCode !== 0) {
          throw new Error('failed to stage the git credential helper inside the sandbox')
        }
      }
      try {
        const clone = await handle.exec(
          `${gitEnvPrefix}git clone --depth 1 ${shellQuote(spec.repoUrl)} ${REPO_DIR}`,
          { timeout: Math.min(180_000, sandboxBudgetMs) },
        )
        if (clone.exitCode !== 0) {
          throw new Error(
            `clone failed: ${redact(clone.stderr || clone.stdout, opts.env).slice(0, 300)}`,
          )
        }
        if (spec.baseBranch) {
          const co = await handle.exec(
            `cd ${REPO_DIR} && ${gitEnvPrefix}git fetch --depth 1 origin ${shellQuote(spec.baseBranch)} && git checkout ${shellQuote(spec.baseBranch)}`,
            { timeout: 120_000 },
          )
          if (co.exitCode !== 0) {
            throw new Error(
              `checkout ${spec.baseBranch} failed: ${redact(co.stderr, opts.env).slice(0, 200)}`,
            )
          }
        }
      } finally {
        if (askpass) {
          await handle
            .exec(askpass.cleanupCommand, { timeout: 5_000 })
            .catch((_error: unknown) => undefined)
        }
      }
      abortCheck()

      const install = await handle.exec(`npm install -g ${shellQuote(this.defaults.cliPackage)}`, {
        timeout: Math.min(300_000, sandboxBudgetMs),
      })
      if (install.exitCode !== 0) {
        throw new Error(`claude CLI install failed: ${redactSecrets(install.stderr).slice(0, 300)}`)
      }
      abortCheck()

      await handle.writeFile(PROMPT_PATH, spec.instructions)
      const deadlineMs = Math.max(30_000, totalBudgetMs - (Date.now() - started))
      const agent = await handle.exec(
        `cd ${REPO_DIR} && cat ${PROMPT_PATH} | claude -p --output-format json ${spec.model ? modelFlag(spec.model) : modelFlag(this.defaults.defaultModel)} --dangerously-skip-permissions`,
        {
          env: {
            ANTHROPIC_API_KEY: opts.env.ANTHROPIC_API_KEY ?? '',
            ...(opts.env.ANTHROPIC_BASE_URL
              ? { ANTHROPIC_BASE_URL: opts.env.ANTHROPIC_BASE_URL }
              : {}),
            ...(opts.env.ANTHROPIC_AUTH_TOKEN
              ? { ANTHROPIC_AUTH_TOKEN: opts.env.ANTHROPIC_AUTH_TOKEN }
              : {}),
            GITHUB_TOKEN: opts.env.GITHUB_TOKEN ?? '',
          },
          timeout: deadlineMs,
        },
      )
      abortCheck()

      const cli = parseClaudeJson(agent.stdout)
      log(redact((cli.result ?? agent.stdout).slice(-4000), opts.env))

      const stage = await handle.exec(
        `cd ${REPO_DIR} && git add -A && (git diff --cached --stat && git diff --cached)`,
        { timeout: 60_000 },
      )
      const patch = extractPatch(stage.stdout)

      return {
        exitStatus: cli.is_error || agent.exitCode !== 0 ? 'failed' : 'completed',
        patch,
        logs: redact(logs.join('\n'), opts.env),
        usage: firstUsage(cli),
        sandboxId,
        computeMs: Date.now() - started,
      }
    } catch (error) {
      const cancelled = error instanceof CANCELLED
      const timedOut = !cancelled && isTimeoutError(error)
      return {
        exitStatus: cancelled ? 'cancelled' : timedOut ? 'timeout' : 'failed',
        patch: '',
        logs: redact(
          `${logs.join('\n')}\n[agent-run] ${cancelled ? 'cancelled by caller' : timedOut ? 'task budget exhausted' : 'run failed'}: ${error instanceof Error ? error.message : String(error)}`,
          opts.env,
        ),
        sandboxId,
        computeMs: Date.now() - started,
      }
    } finally {
      if (sandboxId) {
        await provider.destroy(sandboxId).catch((_error: unknown) => undefined)
      }
    }
  }
}

(Member comments are left out to fit; the full declaration is in node_modules/@molecule/api-agent-runtime-claude-code/dist/provider.d.ts.)

Functions

createProvider(config)

Create a Claude Code agent runtime.

function createProvider(config?: ClaudeCodeRuntimeConfig): AgentRuntimeProvider
  • config — CLI package, default model, budgets.

Returns: An AgentRuntimeProvider running the Claude Code CLI in ephemeral sandboxes.

Constants

RUNTIME_ALLOWED_HOSTS

Hosts the runtime's own tooling needs, always allowed on top of the spec's.

const RUNTIME_ALLOWED_HOSTS: readonly [
  'api.anthropic.com',
  'github.com',
  'api.github.com',
  'codeload.github.com',
  'objects.githubusercontent.com',
  'registry.npmjs.org',
]

provider

The provider implementation (wire with the agent-run core's setProvider).

const provider: AgentRuntimeProvider

Core Interface

Implements @molecule/api-agent-run interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-agent-run'
import { provider } from '@molecule/api-agent-runtime-claude-code'

export function setupAgentRuntimeClaudeCode(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-agent-run >=1.0.0
  • @molecule/api-ai-tools >=1.0.0
  • @molecule/api-code-sandbox >=1.0.0

Runtime Dependencies

  • @molecule/api-agent-run
  • @molecule/api-ai-tools
  • @molecule/api-code-sandbox