Agent Sandbox Registry

agent-sandbox

Decision: Integrate Qwen Code as the AI coding backend for the abx create NL path

无中文版

.agents/notes/implemented/feature/2026-09-29-nl-create-qwen-code-integration.md

Decision: Integrate Qwen Code as the AI coding backend for the abx create NL path

Status: implemented Proposed: 2026-09-29 Implemented: 2026-09-29

Problem

abx create "natural language description" previously used keyword matching plus an optional single-shot LLM classification call to pick one of the existing templates. This approach:

Decision

Route the natural-language create path through Qwen Code running in headless mode on the host, generating real Dockerfile + template.yaml files that are then built, deployed, and launched through the existing pipelines.

Routing for abx create:

Invocation Path
no DESCRIPTION, no --template base template, direct create
--template <name> direct template path (--template wins over a description)
DESCRIPTION only AI path (this decision)

AI path pipeline (src/agent_sandbox/cli/commands/sandbox.py::_generate_and_deploy_template):

  1. Executable discovery — find_qwen_code_binary() checks PATH first, then ~/.abx/bin (Windows: qwen.cmd, qwen.exe, qwen.bat).
  2. Install guidance — when missing, an interactive TTY is offered an automatic install of the official standalone build: downloaded from the verified Aliyun mirror (GitHub release assets as fallback), SHA256SUMS mandatory (a mismatch aborts immediately, never retrying with an unverified binary), extracted into a staging directory and atomically swapped into ~/.abx/bin with the executable bit set on Unix. Non-TTY without --yes, or a declined prompt, raises QwenCodeNotInstalledError (E2005) plus an actionable Quick Setup — never a hang and never a silent fallback.
  3. Credentials — resolve_qwen_code_credentials() resolves in priority order: stored qwen_code_api_key → stored llm_api_key (officially compatible same-family fallback) → OPENAI_API_KEY/DASHSCOPE_API_KEY/ BAILIAN_CODING_PLAN_API_KEY already exported in the environment (inherited by the child process, not injected) → ~/.qwen/settings.json (no injection needed). When a stored abx key drives the run, the child process is given OPENAI_API_KEY
  4. OPENAI_BASE_URL + OPENAI_MODEL (DashScope compatible-mode, default qwen3-coder-plus); DASHSCOPE_API_KEY is never injected. Interactive terminals may be prompted once for a key, stored via write_env_var("ABX_QWEN_CODE_API_KEY", ...); otherwise QwenCodeCredentialError (E2006) is raised.
  5. Generation and validation — generate_template_files() runs qwen -p "<prompt>" --output-format json --yolo (list-form argv, shell=False, bounded cwd and timeout) inside a fresh ~/.abx/generated/<slug>-<timestamp>/ workspace, parses the JSON message array (final type == "result" message), then validates the outputs: the Dockerfile must contain a FROM instruction and template.yaml must pass the public parse_template_data() schema validation (reused from api/capability.py). The generated name is pinned to abx-nl-<slug>-<token> before validation. Failures/timeouts keep the workspace and raise AICodegenError (E2007).
  6. Confirmation, build, create — unless --yes, an interactive confirmation is required (click.UsageError in non-TTY). The generated workspace is then handed to the existing template deploy pipeline (do_deploy + resolve_acr_namespace, --acr-namespace / ACR_NAMESPACE), and the resulting template ref is passed to the existing Sandbox.create.

The previous keyword/LLM-classification selection is no longer used by abx create; llm_api_key remains supported only as a compatible credential fallback.

API Design

New module src/agent_sandbox/agent/qwen_code.py (host-side adapter):

@dataclass(frozen=True)
class QwenCodeCredentials:
    source: str        # "qwen-stored" | "llm-stored" | "environment" | "qwen-settings"
    api_key: str | None
    base_url: str | None
    model: str | None
    def as_env(self) -> dict[str, str]: ...

@dataclass
class QwenCodeRunResult:
    text: str
    is_error: bool
    exit_code: int
    raw_stdout: str
    raw_stderr: str

def detect_standalone_target(*, system=None, machine=None) -> str | None
def standalone_asset_name(target: str) -> str
def standalone_download_urls(target: str) -> list[tuple[str, str]]  # (mirror, url)
def official_install_command(*, windows=None) -> str
def default_bin_dir() -> Path                       # ~/.abx/bin

def find_qwen_code_binary(*, bin_dir=None, windows=None) -> Path | None
def download_and_install_standalone(
    *, target=None, bin_dir=None, timeout=180.0, on_progress=None
) -> Path
def resolve_qwen_code_credentials(
    *, stored_api_key=None, llm_api_key=None, stored_base_url=None,
    stored_model=None, environ=None, settings_path=None
) -> QwenCodeCredentials
def run_qwen_code_headless(
    prompt, *, binary=None, env=None, cwd=None, timeout=600.0
) -> QwenCodeRunResult

New module src/agent_sandbox/agent/codegen.py (generation orchestration):

@dataclass
class CodegenResult:
    workdir: Path
    template_name: str          # abx-nl-<slug>-<token>
    dockerfile: Path
    template_yaml: Path
    description: str = ""
    raw_output: str = ""

def make_template_name(description: str, *, token=None) -> str
def build_codegen_prompt(description: str, template_name: str) -> str
def generate_template_files(
    description, *, binary=None, env=None, timeout=None,
    base_dir=None, on_progress=None
) -> CodegenResult

New error classes in src/agent_sandbox/models/errors.py (all SandboxCreationError subclasses): QwenCodeNotInstalledError (E2005), QwenCodeCredentialError (E2006), AICodegenError (E2007).

parse_template_data() was made public in api/capability.py so the codegen validator reuses the exact same YAML schema check as template deploy.

Alternatives considered

Dependencies

Test Strategy

All tests are offline; the network and the qwen binary are fully mocked.

Acceptance criteria

Implementation

Evidence

Files changed

Errata