The big picture
The wizard does two things in one pass: it authors a manifest and then applies it.
You are filling in a single declarative document —
greentic.env-manifest.v1 — that describes the desired wiring of one
environment (here, local): its trust root, which bundles run, how traffic routes to
them, which messaging channels exist, and which secrets they need.
That document is the durable artifact. It is written to
./local.env.json and meant to live in version control. Everything you typed can be
replayed headlessly later:
gtc-dev setup --answers local.env.json --non-interactive
After authoring, the wizard hands the manifest to the apply engine, which compares it to the live environment and makes only the changes needed (the “plan”).
How it fits together
One runtime, two bundles, two Telegram bots. The fields you enter wire up this picture.
0 Manifest file
Where the wizard writes the document it is building.
Manifest file default ./local.env.json
What: the path of the greentic.env-manifest.v1 JSON file this wizard
creates (or edits, if it already exists). Relative bundle paths inside it are resolved against
this file’s directory, so where it lives matters.
Why: this file is your environment definition. Commit it; re-apply it headlessly; diff it in PRs. The wizard is just a friendly front-end for writing it.
1 Trust root
The signing anchor that lets anything deploy at all.
Bootstrap the trust root? required
What: seeds the environment’s trust root with your local operator signing key. Greentic verifies every bundle/pack signature (DSSE / Ed25519) against this root.
Why: Greentic is deny-by-default — with no trust root, no bundle can be
staged or deployed. It is idempotent: do it once per environment; re-runs are a no-op
(operator key … already trusted). Leave it true the first time.
true) · manifest: "trust_root": "bootstrap"2 Bundles
Each row is one bundle deployment — a packaged digital worker — running in this
environment, plus how inbound traffic reaches it. You added two: legal and
accounting.
Bundle id required
What: the deployment’s natural key / handle, unique within the manifest.
Why: everything else refers to a bundle by this id — endpoints link to it, the router binds routes to it, the plan reports it. you → legal · accounting
Bundle path required
What: the local .gtbundle artifact to deploy (a SquashFS image of
packs + flows + config + SBOM). Relative paths resolve against the manifest file’s directory.
Why: this is what actually runs. The id names it; the path provides the bits.
Customer id optional
What: the billing principal this deployment is attributed to.
Why: required for non-local (cloud / commercial) environments so usage
can be metered and billed. For local it is irrelevant — leave it blank.
Config overrides (JSON) optional
What: last-mile per-pack config tweaks applied at deploy time —
{"<pack_id>": {"<key>": value}}.
Why: change a pack’s settings without rebuilding the bundle. Three-valued:
empty = leave untouched, {} = explicitly clear, a map = replace. You didn’t need it.
Route hosts optional
What: hostnames (comma-separated) that route to this bundle — host-based routing,
e.g. legal.bots.example.com.
Why: use it when bundles are separated by domain. This demo separates by path instead, so it’s blank.
Route path prefixes optional
What: the HTTP path prefix(es) that dispatch inbound traffic to this bundle,
each starting with / (e.g. /legal).
Why: this is how one runtime isolates many bundles.
/legal/webhook/telegram → the legal bundle; /accounting/… → the accounting
bundle. Without it the two bots would collide.
Route tenant optional (set with team)
What: the tenant this route binds to — the multi-tenancy scope (Global → Tenant → Team → User) threaded through every call.
Why it’s the crux of this demo: the tenant also scopes secret lookups. When the
Telegram provider asks the host for TELEGRAM_BOT_TOKEN, the host expands it to
secrets://local/<tenant>/_/messaging-telegram/telegram_bot_token.
Two bundles with the same tenant would resolve the same token. Giving one
legal and the other accounting is exactly what makes each bot use its own token.
Route team optional (set with tenant)
What: the team inside the tenant — the finer tenancy level.
Why: normally default, which the secret store canonicalises to _
(that’s the _ you see in the secret paths). Set it together with the tenant.
_ in the path)3 Messaging endpoints
Each row is a channel wired into the environment — here, one Telegram bot per department — and which bundle(s) its traffic is allowed to enter.
Endpoint name required
What: the endpoint’s manifest handle, its display name, and (on create) its provider instance identity, all in one.
Why: it’s the upsert key (together with provider type) — re-applying matches the same endpoint instead of creating a duplicate.
Provider type required
What: the provider class, e.g. messaging.telegram.bot.
Why: tells the runtime which provider component handles this endpoint’s ingest and egress (Telegram vs Slack vs Teams …).
Linked bundle ids optional
What: the bundle_ids this endpoint is allowed to dispatch into
(comma-separated).
Why: this is the admit gate / ACL. The legal endpoint admits only the
legal bundle, so a message that somehow reached the wrong path would be refused rather
than cross departments.
Welcome flow: bundle id / pack id / flow id optional · all 3 or none
What: a specific flow to run as a greeting when a user first contacts this endpoint.
Why: only if you want a fixed welcome screen wired at the endpoint level. Left
blank here — the bundle’s own on_message flow already answers.
Secret refs optional
What: extra secret references forwarded to the endpoint when it’s created (comma-separated).
Why: for endpoint-level credentials passed at create time (e.g. a webhook secret-token). The bot token itself is handled by the derived secrets step below, so this stays blank.
4 Secrets — derived, asked last
This is the part that changed. Instead of you typing secret paths up front, the wizard now figures out exactly which secrets your bundles need and asks only for the variable name.
How the wizard knew you needed two secrets
Each bundle ships provider packs that declare their secret requirements
(secret-requirements.json). The Telegram pack declares it needs a
telegram_bot_token. The wizard read that for each bundle, scoped it to the bundle’s
route tenant, and produced the exact path — no typing:
| Bundle | Derived secret path | You only entered |
|---|---|---|
| legal | legal/_/messaging-telegram/telegram_bot_token | TELEGRAM_LEGAL_BOT_TOKEN |
| accounting | accounting/_/messaging-telegram/telegram_bot_token | TELEGRAM_ACCOUNTING_BOT_TOKEN |
env var name name only — never the value
What: the name of the environment variable that holds each secret value.
The default suggestion (LEGAL_TELEGRAM_BOT_TOKEN) is just a hint; you typed your real
variable names.
Why a name and not the value: the secret value never goes into the manifest, so the manifest stays safe to commit. At apply time the engine reads the value from that variable (or the dev-store) and writes it into the secret store under the derived path.
Mind the default vs your real names
The suggested default is <TENANT>_<KEY> →
LEGAL_TELEGRAM_BOT_TOKEN. Your shell exports them the other way around
(TELEGRAM_LEGAL_BOT_TOKEN), so you correctly typed over the default. If you had pressed
Enter, apply would look for an unset variable and report it missing.
5 The plan & apply
With the manifest written, the engine diffs desired (your manifest) against the current environment and lists only the changes — then asks before doing anything.
- ensure-environment — create the
localenv (default env-pack bindings + trust-root seed). - bootstrap-trust-root — seed your operator signing key (the question 1 step).
- put-secret ×2 — write each bot token into the secret store, read from
$TELEGRAM_*_BOT_TOKEN. - deploy-bundle ×2 — stage + activate each bundle, bound to its path prefix and tenant.
- add-endpoint ×2 — register each Telegram endpoint.
- link-endpoint ×2 — link each endpoint to its bundle (the admit ACL).
Then the gate: apply 10 change(s) to env `local`? [y/N]. The whole
thing is idempotent — re-running the same manifest shows no-op for anything already in
place (you saw changed:10, no_op:0 because this env was fresh; a second run would flip that).
The trailing JSON is the machine-readable result (changed / no_op /
verify) for scripts and CI.
Why a plan-then-confirm shape
Plan and execute are deliberately separate: you see precisely what will change
(create vs put vs no-op) before any mutation, and the same plan path powers a non-interactive
--dry-run convergence check in CI.
That startup warning
“Greentic toolchain release context is not installed for channel 'dev'”
Harmless for this flow. That context is the pinned compiler/WASM toolchain used to
build components. Authoring and applying an environment manifest doesn’t need it, so the
wizard runs fine. Run gtc-dev install only when you start building components/packs locally.