A plain-language tour of what an Environment is, why bundles stop owning deployment knowledge,
and the code architecture that lands across Phase A → D of plans/next-gen-deployment.md.
Today a .gtbundle carries its deployment knowledge inside itself — which secrets backend, which
telemetry exporter, which deployer, all baked into the bundle's setup-state. Tomorrow, the
Environment is the deploy target — a separate, addressable thing that knows where it lives,
what capabilities are attached, and what's deployed into it. Bundles become environment-agnostic; the Environment
owns the wiring.
Every .gtbundle ships with its own oven, pantry, and freezer. Want to swap to a different oven (AWS Secrets
Manager instead of dev-store)? You re-cook every recipe.
The kitchen (Environment) owns the oven, the pantry, the freezer (env-packs). Recipes (bundles) just say "bake at 200°C" and the kitchen routes that to whatever oven is installed. Swap the oven without touching the recipes.
This split is load-bearing — today's mess is that all three ride the same channel.
| Source | Owner | When set | Lives in | Examples |
|---|---|---|---|---|
| Host config | Operator / admin | At env create-time | environment.json:host_config |
region, cluster endpoint, public hostname, fixed Redis URL |
| Setup config | Wizard answers per env-pack | gtc op env-packs add|update |
env-packs/<slot>.answers.json + secrets backend |
Telegram bot token, OTLP sampling rate, SMTP From |
| Runtime config | Deployer env-pack (discovers post-apply) | After deployer runs | runtime.json |
ALB DNS, Cloud Run URL, generated secret ARNs |
Resolution at runtime: components and ingress read all three through a unified resolver
(secret://, runtime://, host fields). The bundle never names a concrete provider.
pub enum CapabilitySlot {
Deployer,
Secrets,
Telemetry,
Sessions,
State,
Revocation,
}
Adding a slot is a deploy-spec version bump — rare.
pub struct PackDescriptor(pub String);
// "greentic.deployer.k8s@1.0.0"
// "greentic.secrets.aws-sm@1.0.0"
// "greentic.telemetry.otlp@1.0.0"
Adding a new K8s deployer = publish a pack with that descriptor. No enum change anywhere.
v1 binds exactly one env-pack per slot. Fan-out cases (e.g. two telemetry exporters) are represented by a composite env-pack until a future schema version adds list semantics.
Operator-owned in production; CLI-owned for local. The local env is auto-created on first
gtc setup with five default env-packs bound: local-process deployer, dev-store
secrets, stdout telemetry, in-memory sessions, in-memory state.
greentic-deploy-specgreentic-deploy-spec/
├── Environment, EnvPackBinding, CapabilitySlot, PackDescriptor
├── Revision (lifecycle state-machine)
├── TrafficSplit
├── BundleDeployment
├── Credentials // P5
└── RuntimeConfig
greentic-deployer (extended)src/environment/
├── model.rs // re-exports from deploy-spec
├── store.rs // EnvironmentStore trait + LocalFsStore impl
├── atomic_write.rs // write-temp + rename
├── file_lock.rs // per-env flock
├── lifecycle.rs // state-transition matrix enforcement
├── migration.rs // legacy dev → new layout
├── audit.rs // append-only audit events
├── traffic.rs // TrafficSplit persistence
├── bundle_deployment.rs
└── env_packs/
├── registry.rs // kind-string → handler resolver
└── slot.rs
src/cli.rs // gtc op env / env-packs / bundles / traffic / ...
src/tool_check.rs // preflight: tools, credentials, region access
greentic-start (extended)src/revision_dispatcher.rs // NEW: in-process router (see §8)
src/runtime_config.rs // loads runtime-config.v1, multi-bundle
src/http_routes.rs // keyed by (deployment_id, revision_id, public_path)
src/http_ingress/mod.rs // consults dispatcher
src/runner_host/runtime.rs // ActivePacks: (tenant, deployment, bundle, revision) → TenantRuntime
Every HTTP request goes through this resolution chain. The deployment_id is resolved from authenticated context — public traffic cannot select an arbitrary deployment.
secret:// / telemetry:// / runtime:// resolve through the env's bound packs at request time.Revision lifecycle: inactive → staged → warming → ready → draining → archived.
gtc op revisions stage and gtc op traffic set are two independent verbs. You can stage three revisions at 0% and let them sit.
gtc op surface
deferred
Phase E: per-pack rollout inside a bundle, wizard engine extraction, metric-driven auto-abort, .gtairgap.
Not a config string. ULID-addressed, generation-tracked, audited on every mutation. LocalFsStore for dev; a real store (DB/CRD/object-storage + lock) for production.
Six capability slots are the only closed surface. New providers are data — publish a pack with a descriptor like greentic.deployer.k8s@1.0.0, no enum change anywhere.
Host (operator) / Setup (wizard per pack) / Runtime (deployer post-apply). Conflating them is what created today's "non-secrets ride the secrets path" hack.
Four objects, single responsibilities. Bundle = artifact. BundleDeployment = customer rollout unit. Revision = immutable version. TrafficSplit = percentage routing per deployment.
local is implicit, dev migrates via preflightFirst gtc setup creates local with 5 default env-packs. Legacy dev usage goes through gtc op env migrate-dev local --check before any rewrite.
One new crate; three crates extended; four touched. Wizards keep their homes — they gain an env_id parameter; the unification into greentic-wizard-engine is deferred.