Current section
Files
Jump to
Current section
Files
guides/provider-backends.md
# Provider Backends Guide
ASM runs providers through a small backend contract instead of provider-specific
driver/parser stacks.
## Backend Roles
`ASM.ProviderBackend.Core` is the required baseline backend:
- starts `CliSubprocessCore.Session`
- works in `execution_mode: :local`
- works in `execution_mode: :remote_node`
- uses the built-in provider profiles from `cli_subprocess_core`
`ASM.ProviderBackend.SDK` is optional and additive:
- starts provider runtime kits when they are installed locally
- works only in `execution_mode: :local`
- preserves the same session/run/event model as the core backend
- preserves the same normalized execution-surface contract as the core backend
for local subprocess and SSH lanes
- never becomes a required dependency for ASM itself
- may exist without any separate ASM provider-native namespace, as with
Antigravity today
- promotes the Codex app-server host-tool lane when Codex provider options
request `app_server: true`, `host_tools: [...]`, or `dynamic_tools: [...]`
## Common Contract
Both backends satisfy the same `ASM.ProviderBackend` behaviour:
```elixir
@callback start_run(map()) :: {:ok, pid(), ASM.ProviderBackend.Info.t()} | {:error, term()}
@callback send_input(pid(), iodata(), keyword()) :: :ok | {:error, term()}
@callback end_input(pid()) :: :ok | {:error, term()}
@callback interrupt(pid()) :: :ok | {:error, term()}
@callback close(pid()) :: :ok
@callback subscribe(pid(), pid(), reference()) :: :ok | {:error, term()}
@callback info(pid()) :: ASM.ProviderBackend.Info.t()
```
That keeps `ASM.Run.Server` lane-agnostic after resolution.
After `subscribe/3`, the kernel receives `%ASM.ProviderBackend.Event{}` messages
instead of matching provider or transport mailbox tags directly. The envelope
can carry either a normalized `CliSubprocessCore.Event` or an ASM-native event
such as `:host_tool_requested` from the Codex app-server lane.
`ASM.ProviderBackend.Info` is the ASM-owned metadata contract consumed by the
kernel:
- `provider`
- `lane`
- `backend`
- `runtime`
- `capabilities`
- `session`
- `observability`
The `session` field may still contain backend/runtime details, but backend
adapters must strip raw delivery tags such as `session_event_tag` before that
data crosses into ASM kernel state.
## Backend Selection
`ASM.ProviderRegistry.resolve/2` chooses which backend module to use.
- `lane: :core` resolves to `ASM.ProviderBackend.Core` without probing or
loading optional provider SDK modules
- `lane: :sdk` resolves to `ASM.ProviderBackend.SDK` only when the runtime kit is available locally
- `lane: :auto` prefers the SDK lane when available and otherwise falls back to the core lane
An explicit `lane: :sdk` must fail clearly when the SDK runtime kit is
unavailable. It must not silently fall back to the core lane. Fallback is only
valid for documented `lane: :auto` cases.
The public error remains `%ASM.Error{kind: :config_invalid, domain: :config}`,
with `%ASM.ProviderBackend.SdkUnavailableError{}` in `error.cause` for the
SDK-unavailable category. Tests and callers should assert that category instead
of matching human-readable error text.
`execution_mode` is applied after lane discovery. In the landed Phase 3
boundary, remote execution always uses the core backend even if `:auto`
preferred `:sdk`.
That split is intentional:
- local `:core` and local `:sdk` both preserve the same normalized
`execution_surface` contract and its `ExecutionSurface` metadata
- `:remote_node` remains a separate ASM execution mode, not another execution
surface
## Observability
Backend choice is visible in run/event metadata:
- `requested_lane`
- `preferred_lane`
- `lane`
- `backend`
- `execution_mode`
- `lane_reason`
- `lane_fallback_reason`
That metadata is merged into both streamed `%ASM.Event{}` values and the final
`%ASM.Result.metadata` projection.
## Provider-Native Extensions Stay Above The Backend Boundary
`ASM.ProviderRegistry` and the backend modules stop at normalized lane/runtime
selection.
Provider-native capability reporting now lives under
`ASM.Extensions.ProviderSDK`:
- `ASM.Extensions.ProviderSDK.extension/1`
- `ASM.Extensions.ProviderSDK.provider_extensions/1`
- `ASM.Extensions.ProviderSDK.available_provider_extensions/1`
- `ASM.Extensions.ProviderSDK.provider_capabilities/1`
- `ASM.Extensions.ProviderSDK.capability_report/0`
That keeps backend discovery focused on `:core` versus `:sdk`, while
provider-native surfaces such as Claude control semantics, Codex app-server,
Amp permissions/MCP, Cursor mode/worktree controls,
and Antigravity runtime options remain explicit optional seams above the
kernel.
Four providers currently have explicit extension namespaces: Claude, Codex,
Amp, and Cursor. Amp and Cursor start with strict
`derive_options/2` helpers; those helpers derive only common placement/session
data and require provider-native settings in `native_overrides`.
Antigravity is one of five first-party providers and is the current Google
coding-agent SDK route. Gemini CLI remains retired; `gemini_ex` is a distinct
model API SDK outside ASM's CLI provider registry. Antigravity has a core
profile and optional `antigravity_cli_sdk` runtime lane, but no separate
`ASM.Extensions.ProviderSDK.Antigravity` namespace yet. Provider SDK reports
therefore include `:antigravity` with an empty `registered_namespaces` list.
For Claude specifically, `ASM.Extensions.ProviderSDK.Claude` can bridge ASM
config into `ClaudeAgentSDK.Client`, but the resulting control calls still live
on `ClaudeAgentSDK.Client.*` rather than the backend contract.
## Codex App-Server Host Tools
Codex remains on the normal SDK exec runtime unless app-server is requested.
The app-server path:
- starts `Codex.AppServer` with `experimental_api: true`
- converts `ASM.HostTool.Spec` values into Codex `dynamicTools`
- starts or resumes a Codex thread using the app-server transport
- maps `DynamicToolCallRequested` to `:host_tool_requested`
- executes registered run `tools` automatically when present
- responds with `Codex.AppServer.respond/3`
- emits `:host_tool_completed`, `:host_tool_failed`, or `:host_tool_denied`
The app-server backend still reports through the same `ASM.ProviderBackend`
contract and emits normal core assistant/result events for stream consumers.
Broader Codex app-server APIs such as MCP, realtime, voice, plugin, and
filesystem helpers remain in `codex_sdk` or the provider SDK extension seam.
This is Codex-native behavior, not proof of all-provider ASM host-tool support.
Generic ASM `tools:` must remain rejected or provider-native until the
all-provider host-tool admission checklist is complete.
Host-tool declaration, request, and response metadata reject secret-shaped
fields such as API-key, token, auth, credential, password, and bearer keys.
## Claude Backend-Specific Model Inputs
Claude is the first backend where ASM now forwards backend-specific model
inputs into the shared core model registry.
The relevant Claude provider fields are:
- `:provider_backend`
- `:external_model_overrides`
- `:anthropic_base_url`
- `:anthropic_auth_token`
- `:model`
- `:allow_unknown_model` (default `false`) — pass a Claude model newer than the
shared registry straight through to the CLI `--model` instead of erroring.
Common across every provider (`:codex`, `:amp`, `:antigravity`,
`:cursor` accept the same option with the same behavior), not
Claude-specific.
Those values are still treated as value carriers only.
ASM does not validate Ollama models itself and does not build Ollama CLI env
itself. It forwards those values to
`CliSubprocessCore.ModelRegistry.build_arg_payload/3`, then passes the resolved
payload to either the core Claude profile or `ClaudeAgentSDK.Options`.
In governed ASM mode, Claude env/model defaults, native login state,
`anthropic_auth_token`, command paths, cwd, session refs, and target refs cannot
be reused as governed authority. Governed Claude starts fail closed until a
verified provider-auth materializer supplies the launch envelope.
## Codex Backend-Specific Model Inputs
Codex now follows the same pattern for backend-aware model resolution.
Relevant Codex provider fields:
- `:provider_backend`
- `:model_provider`
- `:oss_provider`
- `:ollama_base_url`
- `:ollama_http`
- `:ollama_timeout_ms`
- `:model`
- `:reasoning_effort`
The current shared catalog exposes `gpt-5.6-sol` as the live default plus
`gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, and
the text-only ChatGPT Pro preview `gpt-5.3-codex-spark`. ASM does not add a
`gpt-5.6` alias. Sol and Terra admit `:max` and `:ultra`; Luna admits `:max`
but rejects `:ultra`, while Spark supports `:low` through `:xhigh` and defaults
to `:high` through the shared registry contract.
For the current local Ollama path, ASM callers should use:
- `provider_backend: :oss`
- `oss_provider: "ollama"`
- `model: "<local model id>"`
ASM still does not invent Codex backend flags locally. It forwards those inputs to
`CliSubprocessCore.ModelInput.normalize/3`, which in turn resolves through
`CliSubprocessCore.ModelRegistry.build_arg_payload/3` when the caller supplied
raw knobs instead of a payload. ASM then passes the finalized payload into
either the core Codex profile or `Codex.Options` / `Codex.Thread.Options` on
the SDK lane.
For Codex/Ollama, the shared core keeps `gpt-oss:20b` as the default validated
example model, but it also accepts other installed local model ids such as
`llama3.2`. The degraded-mode distinction for those non-default models is
metadata-driven rather than a hard rejection in ASM.
If a custom `ollama_base_url` is supplied, the finalized payload carries it in
payload-owned runtime data (`CODEX_OSS_BASE_URL`). Raw Ollama roots are
normalized to the OpenAI-compatible `/v1` base for Codex, so downstream core
and SDK transports can consume the payload alone after normalization.
## Amp Backend-Specific Model Inputs
Amp is intentionally payload-only for model input in the current stack.
Relevant Amp provider fields:
- `:model_payload` only
`amp_sdk` does not expose a second raw model/backend surface. ASM finalizes any
shared-core model selection before the Amp SDK boundary, and `AmpSdk.Types.Options.validate!/1`
only canonicalizes a supplied payload rather than inventing another resolution
path inside the Amp repo.
In governed ASM mode, Amp env/model defaults and native CLI auth remain
standalone-only. Governed Amp starts fail closed until a verified provider-auth
materializer supplies explicit launch authority.
## Antigravity Backend-Specific Inputs
Antigravity runs on both ASM lanes:
- `:core` -> `CliSubprocessCore.ProviderProfiles.Antigravity`
- `:sdk` -> `AntigravityCliSdk.Runtime.CLI` when `antigravity_cli_sdk` is
installed
Relevant Antigravity provider fields:
- `:model`
- `:sandbox`
- `:dangerously_skip_permissions`
- `:conversation`
- `:continue`
- `:add_dirs`
- `:print_timeout`
- `:log_file`
ASM common `:cwd` remains the workspace placement key. The Antigravity core
profile and SDK lane render the prompt with `agy --print <prompt>`. The CLI's
plain-text stdout is normalized into assistant deltas and final result text by
the core/SDK runtime layers.
Permission mapping:
- ASM `:default` -> Antigravity `:default` -> no extra permission flag
- ASM `:bypass` -> Antigravity `:bypass` -> `--dangerously-skip-permissions`
Auth and command path ownership:
- `ANTIGRAVITY_CLI_PATH`
- `ASM_ANTIGRAVITY_MODEL`
In governed ASM mode, Antigravity follows the same fail-closed family as
Claude/Amp/Cursor. Provider auth, command, cwd, env, execution surface,
and target authority must be materialized by the owner boundary; ASM does not
promote local Antigravity login state into governed authority.
## Cursor Backend-Specific Inputs
Cursor runs on both ASM lanes:
- `:core` -> `CliSubprocessCore.ProviderProfiles.Cursor`
- `:sdk` -> `CursorCliSdk.Runtime.CLI` when `cursor_cli_sdk` is installed
Relevant Cursor provider fields:
- `:model`
- `:mode`
- `:sandbox`
- `:approve_mcps`
- `:worktree`
- `:worktree_base`
- `:skip_worktree_setup`
- `:plugin_dirs`
- `:headers`
ASM common `:cwd` is the only workspace placement key. The Cursor core profile
and SDK both render it as Cursor `--workspace <cwd>` while also using it as the
process cwd. There is no ASM `:workspace` option.
Permission mapping:
- ASM `:default` -> Cursor `:default` -> no extra permission flag
- ASM `:bypass` -> Cursor `:bypass` -> `--force`
- ASM `:plan` -> Cursor `:plan` -> `--mode plan`
Cursor `:ask` is `ASM.Options.Cursor` `mode: :ask`, not an ASM permission mode.
Auth and command path ownership:
- `CURSOR_API_KEY`
- `CURSOR_CLI_PATH`
- `ASM_CURSOR_MODEL`
In governed ASM mode, Cursor follows the same fail-closed family as
Claude/Amp. Provider auth, command, cwd, env, execution surface, and
target authority must be materialized by the owner boundary; ASM does not
promote local Cursor login state into governed authority.