feat: terminal steps - tty/interactive fields and exec step type @osterman (#2602)
## whatTerminal steps for custom commands and workflows — three related capabilities:
interactive: true— attach host stdin and let the step own Ctrl-C. Atmos suspends its SIGINT-exit handler while the step runs (newpkg/signalssuspension registry consulted by themain.gosignal handler).tty: true— allocate a pseudo-terminal (reusingpkg/terminal/pty, same engine asatmos devcontainer attach). The command sees a real TTY; secret masking is applied to PTY output. Withinteractive: true, the host terminal switches to raw mode so Ctrl-C flows through the PTY to the child.type: exec— replace the Atmos process entirely (shellexecsemantics):execveof the system shell on Unix (env, working directory, and terminal inherited natively;ATMOS_SHLVLunchanged), spawn-and-propagate-exit-code emulation on Windows. Validated to be the final step;tty/interactive/retry/timeout/outputare rejected on exec steps.
Architecture: all logic lives in narrow packages — pkg/process (RunShellStep routing, RunShellSession, ReplaceShellSession), pkg/schema (validation), pkg/signals (interrupt suspension). cmd/ and internal/exec contain only inline switch-case call sites; pkg/runner and the step handler share the same routing.
Also fixes in pkg/terminal/pty found along the way:
- stdin copier no longer blocks completion (it's detached, docker-CLI pattern)
- session teardown is bounded: when grandchildren (e.g. aws ssm's
session-manager-plugin) keep the PTY slave open after the child exits, output drains on a 1s deadline instead of hanging with the terminal in raw mode DisableStdinForwardfor-t-without--isemantics
why
Custom commands had no way to hand the terminal to an interactive process:
commands:
- name: ssh
steps:
- type: shell
command: "exec aws ssm start-session --target {{ .Arguments.instance_id }}"ran the SSM session as a piped, masked subprocess: full-screen rendering broke, and Ctrl-C inside the session killed Atmos itself (global SIGINT handler exits 130), killing the orphaned session with SIGPIPE.
With this change:
commands:
- name: ssh
steps:
- type: shell
tty: true
interactive: true
command: "aws ssm start-session --target {{ .Arguments.instance_id }}"behaves like docker run -it (supervised: masking preserved, more steps can follow), and:
- type: exec
command: "aws ssm start-session --target {{ .Arguments.instance_id }}"hands the process over entirely (launcher: native job control, zero proxy overhead, must be the last step).
references
- Reported in SweetOps Slack (SSM session via custom command gets a mangled terminal and dies with SIGPIPE on Ctrl-C); teardown hang + raw-terminal-after-exit reproduced live on this PR and fixed
- Docs: Interactive and TTY Steps
🤖 Generated with Claude Code
Summary by CodeRabbit
- New Features
- Added
ttyandinteractiveoptions forshellsteps, and introducedexecstep type for process replacement.
- Added
- Behavior
- Improved terminal/TTY handling, including Ctrl-C ownership, PTY session behavior, and more reliable exit-code propagation.
- “Silent” exit codes now skip themed error rendering.
- Validation
- Enforced
execsteps must be the final step and disallow incompatible fields; improved error hints.
- Enforced
- Documentation
- Updated workflow/CLI docs and added a blog post with usage guidance.
- Tests / Fixes
- Expanded coverage for shell sessions, PTY stdin forwarding/teardown, exec replacement, signals, and schema validation.
feat(secrets): declarative secrets management with !secret, CRUD CLI, and masking @osterman (#1911)
## whatImplements the Secrets Management PRD end to end — a GitOps-friendly, multi-cloud secrets workflow built on top of the existing store registry (not a parallel backend). Secrets are declared in stack config (committed to git) and their values live in a cloud secret backend or a SOPS-encrypted file, managed with a Vercel-like CLI and resolved at runtime with a new !secret YAML function.
Stores (pkg/store)
StoreConfiggainssecret: true(subsystem membership) andkind(cloud/thing) with legacytypemapping;!storeagainst asecret: truestore is now an error ("use!secret").- New
DeletableStore/StatusStore/SecretAwareStoreinterfaces; AWS SSM writesSecureStringwhen used as a secret backend and gainsDelete/Has. - New store backends: AWS Secrets Manager and HashiCorp Vault (KV v2). Registry refactored to a table-driven builder map;
kind↔typecompatibility.
Secrets core (pkg/secrets)
service, declarationregistry,resolver,validator,kinds, and a leafpkg/secrets/providers/subpackage with a store-adapter (track 1) and a native SOPS provider (track 2:age/aws-kms/gcp-kms/gpg).- SOPS providers can be defined in
atmos.yaml, globally in a stack (secrets:top-level merges into every component), or under a single component.
!secret + masking (the headline)
!secret NAME [| path ...] [| default ...]wired into the live YAML pipeline, with automatic masker registration.- Mask-without-retrieval: inspection commands (
describe,list) resolve!secretto<MASKED>without contacting the backend when masking is on (the default) — so you can inspect a stack with no cloud credentials. Value-producing commands (secret get,terraform plan/apply) always retrieve;--mask/ATMOS_MASKonly controls redaction of display output. - Sensitive Terraform outputs (
sensitive = true) auto-register with the masker as they flow through!terraform.output/atmos.Component()/describe.
CLI (cmd/secret)
init, set (alias add), get, delete (alias rm), list, pull, push, import, validate.
Stack processing
secrets is now a first-class inheritable component section, plus a global stack-level secrets: block that merges into every component.
Docs + example
- Full Docusaurus docs:
atmos secretoverview + all 9 subcommands, secrets configuration page,!secretfunction page; blog post (with an embedded example) and a roadmap milestone. examples/sops-secrets/— fully self-contained, age-encrypted, no cloud credentials. Bundledatmos testcustom command (.atmos.d/test.yaml) proves the full lifecycle end to end (set → encrypted-at-rest → get → list → validate → masked-without-credentials inspection → reveal-needs-key).
why
There was no unified way to manage human-provisioned secrets in Atmos — stores were designed for machine-written Terraform outputs, and the historical workaround (Chamber) was AWS-only. This adds explicit, declarative secret registration so a secret must be declared before it can be set, read, or resolved, and makes "inspect a stack" decoupled from "authenticate to the secret backend."
references
- PRD:
docs/prd/secrets-management.mdanddocs/prd/secrets-masking/ - Example:
examples/sops-secrets/(runatmos test)
notes / follow-ups
- Fixed a pre-existing init-ordering bug where the global
--mask=falseflag did not disable the early-initialized I/O masker (onlyATMOS_MASK=falsedid).io.ReconcileMasking()now reconciles the masker after flags are parsed, so--mask=falseandATMOS_MASK=falsebehave identically. pkg/storebackend implementations could be moved into apkg/store/providers/subpackage (mirroringpkg/secrets/providers/) — deferred to a dedicated follow-up PR since it touches ~30 external call-sites.- Base-component (
metadata.component) inheritance of thesecretssection is not wired yet (component-level +import:+ global-stack inheritance all work).
🤖 Generated with Claude Code
Summary by CodeRabbit
- New Features
- First-class secrets management CLI:
secret init,set/add,get,delete/rm,list,pull,push,import,validate, plusexec,shell, andkeygen. - SOPS-backed secrets with collision-safe placement and secure inspection by default.
- Terraform integration: secret values are injected via
TF_VAR_*, and secret-bearing values are omitted from generated varfiles by default (with optional shell export).
- First-class secrets management CLI:
- Documentation
- Added PRDs/blog and updated secrets examples and masking guidance.
- Bug Fixes
- Masking behavior now consistently follows
--maskacross inspection-style commands.
- Masking behavior now consistently follows
- Tests
- Expanded unit/integration coverage for secrets, masking, Terraform, and providers.
- Chores
- Updated license inventory and CI/test reliability tweaks.