feat(ai): add opencode CLI provider @aknysh (#3189)
## what- Add opencode (https://opencode.ai) as a CLI AI provider (
opencode), alongsideclaude-code,codex-cli, andcopilot-cli. Atmos runs theopencodebinary non-interactively (opencode run <prompt>), reusing the user's opencode auth and model-provider configuration — no API key inatmos.yaml. - Full MCP pass-through: when
mcp.serversare configured, Atmos writes a temporary opencode config (auth-requiring servers wrapped withatmos auth exec -i <identity>, toolchainPATHinjected) and points opencode at it via theOPENCODE_CONFIGenv var. opencode deep-merges it, so the user's ownopencode.jsonis never modified and nothing is left behind (temp file cleaned up per invocation). - Joins the CLI auto-detection chain (
claude-code→codex-cli→copilot-cli→opencode→gemini-cli) and theIsCLIProviderset.cmd/ai/init.goneeds no change — its MCP handling is generic viaIsCLIProvider. - Adds unit tests (config,
buildArgs,ExtractResult, temp-config schema incl. auth-wrapping, and a cross-platform subprocess round-trip via aTestMainfake binary — 86.7% coverage), provider documentation, a changelog post, and a roadmap milestone.
why
- Atmos AI already lets users reuse a locally installed coding-agent CLI (Claude Code, OpenAI Codex, GitHub Copilot) instead of an API key, but the popular open-source opencode agent wasn't supported. opencode users had to fall back to a raw API provider — a second credential to manage, giving up opencode's own model selection and MCP setup.
- Adding opencode as a first-class CLI provider lets those users drive
atmos aithrough their existing opencode installation with zero extra credentials.
references
- Part of #3179 (the API providers OpenRouter / DeepSeek / Z.AI shipped in #3188).
- Docs:
/cli/configuration/ai/providers - Changelog:
website/blog/2026-09-18-opencode-cli-provider.mdx
Summary by CodeRabbit
-
New Features
- Added OpenCode as an AI CLI provider for
atmos ai, with automatic detection and optional model selection. - Supports conversation history, system prompts, explicit full-auto mode, and MCP server pass-through.
- Reuses OpenCode authentication and configuration without modifying user files.
- Applies MCP settings through a temporary configuration file and reports configuration or execution errors clearly.
- Added OpenCode as an AI CLI provider for
-
Documentation
- Added OpenCode setup, usage, troubleshooting, and roadmap guidance.
- Documented OpenRouter, DeepSeek, and Z.AI providers.
-
Tests
- Added coverage for provider detection, command execution, response handling, and MCP integration.
feat(scaffold): support glob patterns in spec.files[].path @jorrite (#3187)
## whatspec.files[].pathcan now be a glob pattern (doublestar syntax:*,?,[...],**for any depth,{a,b}), matched against every file the template discovers, instead of only a literal path.- A glob
path:combined withwhen:gates or skips an entire directory recursively, in one entry, instead of onewhen:-gated entry per file. - A glob
path:combined withmatrix:andtarget:duplicates an entire directory's files once per matrix combination, the same way a single-file matrix entry already does. Two new template variables,.file.Pathand.file.RelPath(the matched file's path with the entry's glob literal prefix stripped), are available intarget:and content so a directory-leveltarget:can differentiate which matched file an output came from. - When more than one
spec.files[]entry'spath:matches the same discovered file, the last declared entry wins — the same precedence convention.gitignore/CODEOWNERSuse. - A directory-level matrix axis expression is now resolved once per
spec.Path, not once per matched file, so a non-deterministic axis expression (e.g. Sprig'srandAlphaNum) resolves the same value across every file one entry matches, instead of a different value per file. - A malformed glob pattern (unclosed
[/{) now fails scaffold load andatmos scaffold validateimmediately (ErrScaffoldFilePathPatternInvalid), instead of silently and permanently matching nothing. - A backslash in a
path:pattern is now always normalized to a forward slash regardless of OS, instead of only working correctly on Windows. - A directory-level matrix
target:that fails to reference.file.Path/.file.RelPathnow fails deterministically before any file in the run is written (ErrScaffoldMatrixTargetMissingFileContext), instead of leaving a partial write on disk once a collision is discovered mid-run. This check parsestarget:with the scaffold's real delimiters and walks the resulting template AST for a genuine.file.Path/.file.RelPathfield access, rather than a lexical substring match — so it can't be fooled by a literal.file.in unrelated text or an invalid.file.Unknownreference. atmos scaffold generate --updatecan now recover a real 3-way merge base after atarget:migration (e.g. adopting a glob +.file.RelPath-based target on an entry that previously rendered verbatim to its own path): the merge-base lookup falls back to the file's original discovered source path when git history has nothing at the new rendered path, instead of always treating it as user-added and silently freezing its content forever.- The
atmos-scaffoldAI agent skill (agent-skills/skills/atmos-scaffold/) now documents globpath:, directory-level matrix, and.file.Path/.file.RelPath, so AI agents helping a user author a scaffold.yaml know this capability exists.
why
spec.files[] matched files one at a time by exact literal path, so gating or duplicating an entire directory meant repeating the same when: or matrix:/target: on every file inside it, one entry per file, kept in sync by hand as the directory grew — a gap the original matrix: PRD explicitly called out as a non-goal at the time. Directory-level matrix duplication is a natural extension of the existing single-file matrix feature (e.g. one components/ tree instance per environment), and directory-wide skip is the same gap for when: alone (a legacy docs tree gated behind an opt-in answer, a cloud-provider-specific subtree).
Several bullets under "what" are bugs found either while field-testing the feature against a real build, or during CodeRabbit review — each reproduced live before and after the fix, with a new regression test.
references
- Blog post:
website/blog/2026-09-18-scaffold-directory-glob.mdx - PRD:
docs/prd/atmos-scaffold.md("Globpath:and Directory-Level Matrix") - Docs:
website/docs/cli/commands/scaffold/generate.mdx("Glob Paths and Directory-Level Matrix") - Example:
examples/scaffolding-directory-matrix/ - Agent skill:
agent-skills/skills/atmos-scaffold/
Summary by CodeRabbit
- New Features
- Scaffold file paths support glob patterns, recursive directory matching, and normalized separators.
- Apply conditions to directory trees and duplicate matched files across matrix values.
- Use file path template variables to preserve relative locations and avoid output collisions.
- Overlapping matches follow last-declared-entry precedence.
- Bug Fixes
- Invalid patterns and ambiguous matrix targets fail before files are written.
- Updates recover merge history when rendered paths change and warn when unavailable.
- Invalid file-context references are no longer misidentified as valid.
- Documentation
- Added usage guidance and a directory-matrix scaffold example.
feat(ai): add OpenRouter, DeepSeek, and Z.AI providers @aknysh (#3188)
## what- Add three first-class OpenAI-compatible API providers to Atmos AI, selectable by name under
ai.providers:openrouter— OpenRouter (https://openrouter.ai/api/v1,OPENROUTER_API_KEY). A router across hundreds of models; switch models by changing the provider-prefixedmodelslug (e.g.anthropic/claude-sonnet-4-5,openai/gpt-4o,deepseek/deepseek-chat). Default model:deepseek/deepseek-chat.deepseek— DeepSeek (https://api.deepseek.com,DEEPSEEK_API_KEY). Defaultdeepseek-chat;deepseek-reasonerfor the reasoning model.zai— Z.AI / Zhipu GLM (https://api.z.ai/api/paas/v4,ZAI_API_KEY). Defaultglm-5.3.
- Each provider mirrors the existing
grokprovider using the sharedbase/openaicompatconversion layer, self-registers viainit(), and works everywhere the other providers do (atmos ai ask/chat,--ai). - No schema change required —
base_url,api_key, andmodelalready exist on the AI provider config. - Adds unit tests (config extraction, client construction, getters, and an
httptestround-trip exercising all five send methods), factory registration coverage, provider documentation, a changelog post, and a roadmap milestone.
why
- The AI model landscape moves faster than any single vendor's roadmap, and a US-only provider list is a non-starter for many teams. Users wanted to route through OpenRouter (to test many models cheaply), or use DeepSeek / Z.AI (GLM) directly.
- Before this change the only way to reach these was to point the generic
openaiprovider at a hand-copied base URL and hope the defaults lined up. These providers make the common choices first-class: sensible default model, API-key env var, and endpoint out of the box.
references
- Part of #3179 (the Opencode CLI provider is intentionally split into a separate follow-up PR).
- Docs:
/cli/configuration/ai/providers - Changelog:
website/blog/2026-09-18-more-ai-providers.mdx
Summary by CodeRabbit
-
New Features
- Added support for OpenRouter, DeepSeek, and Z.AI providers across Atmos AI commands.
- Added configurable models, endpoints, token limits, request timeouts, conversation history, system prompts, and tool usage.
- Added secure base URL validation when API keys are configured, while allowing local loopback endpoints.
-
Documentation
- Added provider configuration guidance and updated the roadmap with the new integrations.
-
Bug Fixes
- Improved handling of API failures, empty responses, and insecure endpoint configurations.
fix(ci): allow Go toolchain download in website preview deploy @aknysh (#3191)
## what- Add
release-assets.githubusercontent.com:443to the harden-runnerallowed-endpointsin.github/workflows/website-preview-deploy.yml.
why
- The Website Preview Deploy job has been failing on
mainand every recent PR for days — itsSet up Gostep dies withconnect ECONNREFUSED …:443while downloading the Go toolchain. - The deploy job genuinely needs Go: the
s3-deploycomposite action runsgo tool mage s3:deploy. So removing the step isn't an option — the toolchain download must be allowed. actions/setup-gofetches Go from GitHub's release-asset CDN (release-assets.githubusercontent.com), but that host was missing from the deploy job's egress allowlist whileharden-runnerruns withegress-policy: block. The build job (website-preview-build.yml) already allows this host, which is why the site build succeeds but the deploy fails.- This only affects the preview-deploy Deployment (the
website-deploy-previewbuild check already passes); it is not a merge gate. Becauseworkflow_runworkflows execute the copy on the default branch, the fix has to land onmainto take effect.
references
- Surfaced while reviewing CI on #3188 and #3189.
- Matches the allowlist in
.github/workflows/website-preview-build.yml.
Summary by CodeRabbit
- Chores
- Website preview deployments can now access required release assets over HTTPS.
feat(scaffold): --update-strategy=tracked|rendered for atmos init/scaffold generate @jorrite (#3119)
## what- Add
--update-strategy=tracked|renderedtoatmos init --updateandatmos scaffold generate --update, controlling where the three-way merge base comes from, independently of--merge-strategy(conflict resolution) and--merge-driver(merge algorithm).tracked(default): unchanged behavior — base read from the target's own git history at--base-ref.rendered: base is a pristine re-render of the template at the ref that produced what's currently on disk, using that generation's own recorded.atmos/scaffold.yamlanswers. No dependency on the target being a git repository at all.
- New
pkg/generator/engine.UpdateStrategytype +ParseUpdateStrategy, abaseContentLoaderinterface abstraction onProcessor(satisfied by both the existingstorage.GitBaseStorageand the newstorage.RenderedBaseStorage), andpkg/generator/source.ResolveRenderedBase(loads the target's project record and fetches the old ref before the current run overwrites it). renderedrequires the template to carry ascaffold.yaml(so its answers are recoverable) — plain--set-only templates aren't supported yet and get a clear error pointing attracked.--base-ref+--update-strategy=renderedis a mutually-exclusive-flags error, sincerendered's ref comes from.atmos/scaffold.yaml, not--base-ref.renderedworks for OCI-sourced templates too: the resolved manifest digest is pinned the same way a git commit SHA is, so a later re-render always resolves the exact original content instead of whatever a mutable tag currently points to.- Docs:
<dt>/<dd>flag entries and usage examples oninit.mdx/scaffold/generate.mdx/scaffold/usage.mdx, updates to theatmos-scaffold.md/atmos-init.mdPRDs, a changelog post, and a roadmap milestone.
hardening
A /field-test pass and two rounds of CodeRabbit review against the shipped feature surfaced several real gaps, all fixed in this branch:
- Silent data loss: an unconditional base-ref resolution used to run for any
--updateregardless of strategy and wrote a resolved value intospec.baseRefeven underrendered— whose whole point is to have no git-history dependency at all. Introducedspec.renderedRef, a separate project-record field recording the actual resolved ref (git commit SHA or OCI manifest digest) at generation time, and gated every place that previously wrotespec.baseRefunconditionally on--update-strategy— including two retry-only code paths (the "confirm update instead" offer, and the interactive-declined-then-retried flow) that a first fix pass missed. - Crash: the "confirm update instead" retry path could reach
renderedmode's base-render step with no base source ever resolved, panicking on a nil pointer. Fixed with both a functional retry-path fix and a defensive nil-check that turns any future recurrence into a clear error instead of a panic. - Switch detection: a project's recorded
baseRef/renderedRefnow doubles as a record of which strategy last managed it; switching strategies against an existing project fails loudly instead of silently misinterpreting the other strategy's provenance. - OCI subdir sources:
go-getter's git fetch for a//subdirsource (the shapeatmos init aws/appitself uses) clones the full repo into its own internal temp location and copies only the subdir out, so the destination directory never has a usable.gitto inspect —spec.renderedRefsilently stayed empty for any subdir-sourced template. Fixed with a best-effort fallback re-fetch scoped to resolving just the commit. - Enforced the existing
WithValidValuesflag-validation framework for--update-strategy/--merge-driver/--merge-strategy(previously registered but never actually checked), fixed a misleading error message that assumed git underrenderedmode, and closed several direct-unit test-coverage gaps left by cross-package-only test exercise.
why
--update's merge base has always been read from the target's own git history at a pinned commit. That has two real costs:
- It requires the target to be an intact git repository — a squashed/rewritten history, or a target that was never a git repo at all, makes
--updatefail outright. - The pinned base ref never advances (by design, see #2989), so the gap between base and the current template only grows across successive updates.
--update-strategy=rendered sidesteps both: no git dependency, and the base always reflects exactly one update cycle's worth of drift, since it's re-rendered fresh from the recorded prior generation each time.
references
- #2989 (base-ref pinning this builds on)
- #3047 (conflict-marker correctness fixes this branch depends on, already merged)
Summary by CodeRabbit
-
New Features
- Added
--update-strategyto initialization and scaffold updates. - The default
trackedstrategy uses target Git history. - The
renderedstrategy reconstructs prior template state from recorded configuration, supporting updates without target Git history. - Rendered updates reuse the exact template revision and support dry-run previews.
- Added
-
Bug Fixes
- Added validation for invalid strategies, incompatible
--base-refcombinations, missing configuration, and strategy switches.
- Added validation for invalid strategies, incompatible
-
Documentation
- Updated CLI guides, product documentation, roadmap, and blog content.
🚀 Enhancements
fix(vendor): add `vendor clean --prune-lock` to forget lock entries @aknysh (#3201)
## what- Add an opt-in
--prune-lockflag toatmos vendor cleanthat also removes the cleaned components' entries fromvendor.lock.yaml(in addition to removing their files). - It forgets only the entries the same
--component/--tags/--stack/--labelsselectors matched — never entries the caller didn't target. Preservation stays the default. - Adds
lockfile.CleanOptions{Force,DryRun,PruneLock},pruneSelectedLockEntries,CleanReport.Forgotten, unit + cmd tests, and CLI docs.
why
- Since v1.229.0 (#3169, "
vendor cleannow preserves lockfile entries for future reinstalls"), there was no supported way to permanently remove a vendored source using the native lock: after deleting it fromvendor.yaml, the orphan lock entry keeps its recorded files, andvendor verifythen reports them asmissing. - The issue (#3196) offered three candidate fixes. Auto-pruning orphans on
vendor pull --refresh-lockwas rejected:vendor.yamland legacycomponent.yamlvendoring share onevendor.lock.yaml(both calllockfile.Record), so a fullvendor pullblindly pruning non-declared artifacts could delete legitimatecomponent.yamllock entries. The surgical, zero-over-pruning fix is an explicit flag onvendor cleanthat forgets only what the selectors matched.
Usage
# Permanently remove a component: delete its files AND forget its lock entry,
# then remove its source from vendor.yaml (no orphan left for `vendor verify`).
atmos vendor clean --component vpc --prune-lockreferences
- Closes #3196
- Docs:
/cli/commands/vendor/vendor-clean - Fix record:
docs/fixes/2026-09-22-vendor-clean-prune-lock.md
Summary by CodeRabbit
-
New Features
- Added a
--prune-lockoption toatmos vendor cleanfor permanently removing selected vendored components and their lock-file entries. - Added dry-run support to preview files and lock entries that would be removed.
- Lock-file entries remain preserved by default when cleaning.
- Added a
-
Documentation
- Updated command documentation with usage examples, selector behavior, pruning details, and dry-run guidance.
- Added troubleshooting guidance for permanently removing vendored components.
fix(terraform): provision local-component workdir before backend/varfile generation @aknysh (#3198)
## what- For a local Terraform component (no JIT
source:) withprovision.workdir.enabled: true, provision the isolated workdir up front inProvisionAndResolveComponentPath— before backend/varfile generation — instead of only at thebefore.terraform.inithook. - The generated
backend.tf.jsonand*.tfvars.jsonnow land in the per-run workdir instead of the shared source component directory. - Adds a regression test (
TestProvisionAndResolveComponentPath_LocalComponentWorkdirProvisionedEarly) and a fix record underdocs/fixes/.
why
- Fixes the race in #3192. The Terraform working dir is resolved from
info.ComponentSection[WorkdirPathKey]; when that key is unset, generation falls back to the source component dir. - For local components, the workdir copy (which sets
WorkdirPathKey) ran atbefore.terraform.init— afterrunPreExecutionStepshad already writtenbackend.tf.json/varfile. So those files were written intocomponents/terraform/<component>/, polluting the source tree, and underatmos terraform plan --all --max-concurrency Nparallel runs read/wrote the same sourcebackend.tf.jsonconcurrently, producingError: ... The JSON data ends prematurelyHCL parse failures. - JIT-
source:components were unaffected because their workdir is set early (during source download). - The fix moves the (self-gating, idempotent) workdir provisioner ahead of generation. The later
before.terraform.initrun of the same provisioner becomes a safe no-op (it early-returns whenWorkdirPathKeyis already set), and the-reconfiguresignal it records still persists on the sharedComponentSection.
references
- Closes #3192
- Follow-up (deferred): the documented global default
settings.provision.workdir.enabledis still ignored (only component-level is honored) — tracked separately in #3197. - Fix record:
docs/fixes/2026-09-21-workdir-backend-race-source-dir.md
Summary by CodeRabbit
-
Bug Fixes
- Fixed concurrent Terraform runs for local components using isolated work directories.
- Generated backend configuration and variable files are now placed in the appropriate per-run directory, preventing source-tree changes and read/write errors during parallel planning.
- Ensured local component files are available in the isolated work directory before Terraform execution.
- Preserved existing source-directory behavior for local non-Terraform components.
-
Documentation
- Added documentation covering the work-directory race fix and a remaining limitation with the global work-directory setting.
fix(schema): allow ':' in auth/secret provider and identity names @aknysh (#3186)
## what- Relax the stack-manifest JSON Schema so
auth.identities,auth.providers, andsecrets.providerskeys may contain a colon (:), enabling namespaced names such asexample/prod:terraform_applier. - Change the
auth_identities,auth_providers, andsecret_providerspatternPropertieskey pattern from^[a-zA-Z0-9/_-]+$to^[a-zA-Z0-9/_:-]+$in both the embedded schema (pkg/datafetcher/schema/atmos/manifest/1.0.json) and the hand-synced test fixture (tests/fixtures/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json). - Restore drifted
auth_identity/auth_providerdefinition parity in the test fixture (it still requiredkindand was missing therequired/tagsfields). - Add regression tests: schema-level validation for colon-containing identity, provider, and secret-provider names (plus a negative test that a name with a space is still rejected), runtime identity resolution with colon names, and runtime SOPS provider resolution with a colon name.
why
- Component-level
auth.identitiesrejected globally configured identity names containing:, even though theatmos.yamlconfig schema and the runtime identity model already accept them (identity/provider names are opaque map keys, resolved case-insensitively with no character validation). - Because
additionalPropertieswasfalse, a namespaced identity likeexample/prod:terraform_applierfailed stack validation before identity resolution and could not be selected as a component default. secrets.providersshared the identical inconsistency (config schema accepts any key; manifest schema rejected:; provider names are opaque map keys at runtime), so it was relaxed in the same pass for consistency.- Terraform
required_providerslocal names (^[a-zA-Z0-9-_]+$, no colons per Terraform's own rule) and component-instance-name patterns were reviewed and intentionally left unchanged.
references
- Closes #3185
- Fix record:
docs/fixes/2026-09-17-auth-identity-name-colon-schema.md
Summary by CodeRabbit
-
New Features
- Manifest names for authentication identities, authentication providers, and secret providers can now include colons (
:), including namespaced formats. - Authentication identities and providers support optional tags for categorization and filtering.
- Authentication identities can be marked as required for automatic authentication.
- Authentication identity definitions no longer require a
kindvalue.
- Manifest names for authentication identities, authentication providers, and secret providers can now include colons (
-
Documentation
- Added guidance covering the updated manifest schema and validation behavior.