Greentic · gtc setup --env local

The environment wizard, question by question

Everything gtc-dev setup --env local asks you, what each field actually controls, and why it has to be there. Worked against your two-department Telegram run (one runtime, two bundles, two bots).

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:

# reproduce this exact environment, no prompts
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”).

Component → Flow → Pack → Bundle → Environment deny-by-default multi-tenant: tenant / team secrets by reference, never inline

How it fits together

One runtime, two bundles, two Telegram bots. The fields you enter wire up this picture.

🏛️ LEGAL bot@BotFather 🧾 ACCT bot@BotFather cloudflared one tunnel → 127.0.0.1:8080 revision_serve path-prefix router /legal → legal /accounting → acct bundle: legal tenant=legal · /legal secret: legal/_/…/token bundle: accounting tenant=accounting · /accounting secret: accounting/_/…/token
Path prefix decides the bundle · route tenant decides which bot token resolves · the endpoint’s link is the admit gate.

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.

you pressed Enter → ./local.env.json

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.

you → Enter (default 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.

you → bundle-workspace-legal/realbot-legal.gtbundle

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.

you → (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.

you → (blank — leave untouched)

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.

you → (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.

you → /legal  ·  /accounting

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.

you → legal  ·  accounting

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.

you → default  (→ _ 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.

you → legal  ·  accounting

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 …).

you → messaging.telegram.bot

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.

you → legal  ·  accounting

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.

you → (all blank)

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.

you → (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:

BundleDerived secret pathYou only entered
legallegal/_/messaging-telegram/telegram_bot_tokenTELEGRAM_LEGAL_BOT_TOKEN
accountingaccounting/_/messaging-telegram/telegram_bot_tokenTELEGRAM_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.

you → TELEGRAM_LEGAL_BOT_TOKEN  ·  TELEGRAM_ACCOUNTING_BOT_TOKEN

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.

Edge cases — empty fields, host×path, all-or-nothing

The exact behaviour behind the optional fields: what an empty value does, which fields must be set together, how host and path combine, and why the welcome flow exists at all. These are the questions the cards above gloss over.

1 · Leaving a field empty

An empty string is treated identically to omitted — the wizard trims and drops blanks, so a stray space never sneaks into the manifest. For optional fields, "absent" almost always means "leave the live value alone": apply upserts, it never clears something just because you left the box blank. The exceptions are flagged below.

FieldEmpty means…
public_base_urlkeep whatever is already on the env (never cleared)
customer_idOK on local; apply fails on any non-local env — the billing principal is required there
config_overridesleave the deployment's existing overrides untouched (see §3 for {} vs a map)
route_hostsmatch any host
route_path_prefixesmatch any path (binds at the root, lowest specificity)
route_tenant / route_teamno route binding at all → a fresh add gets an empty catch-all binding; a re-deploy leaves the existing binding untouched. Set both or neither.
linksthe endpoint admits no bundle through this field
welcome flow (×3)no endpoint-level welcome flow (see §4)
secret_refsnothing extra forwarded when the endpoint is created (see §5)

Two groups are all-or-nothing

Half-setting either group is a hard conversion error (set … together (or none)), not a silent drop:

route_tenant + route_team welcome_bundle_id + welcome_pack_id + welcome_flow_id

2 · Route hosts and route path — what if you set both?

They are ANDed. A request reaches a bundle only when its Host matches one of the hosts and its path matches one of the prefixes. An empty list on either side means "match anything" on that dimension.

Route hostsRoute path prefixesA request matches when…
(empty)(empty)always — catch-all / empty binding (any host, any path)
(empty)/legalpath is under /legal, any host
legal.example.com(empty)Host is legal.example.com, any path
legal.example.com/legalHost is legal.example.com and path under /legal

Among all host-matching routes, the longest matching path prefix wins (ties resolve to the first deployment in env order, but the operator rejects ambiguous bindings at deploy time). Host match is case-insensitive and port-stripped; a non-empty host list rejects a request that carries no Host header. A tenant route with neither a host nor a path is rejected as structurally unreachable. The demo uses path-only because the cloudflared tunnel hands every request to one local port under whatever hostname it was assigned — separating by path is host-agnostic.

3 · Config overrides — three distinct values

What you enterEffect at deploy
(blank)leave the deployment's existing overrides untouched
{}explicitly clear all overrides
{"realbot":{"mode":"prod"}}replace overrides with this <pack_id> → <key> → value map

Anything that isn't a JSON object — bad JSON, or an array like [1,2] — makes apply refuse with an error naming config_overrides. The point is to tune a pack's settings without rebuilding the bundle.

4 · Welcome flow — all three, pointing at a linked bundle

The three IDs together locate exactly one flow: bundle_id = which deployed bundle, pack_id = which pack inside that bundle, flow_id = which flow inside that pack. Set all three or leave all blank — a partial trio is an error. The bundle_id must be one of the endpoint's links (you can't greet with a bundle the endpoint doesn't admit). Re-applying the same trio is a no-op.

Why would you set it? Is it standard? What about legacy?

Why: to pin a fixed entry/greeting flow at the channel level — e.g. a bot that should always open with a specific onboarding card, regardless of which message arrives. It overrides the bundle's own default routing for first contact.

Standard practice? No — it's optional and usually left blank. The bundle's own flow graph already handles incoming messages; you only reach for welcome flow when the endpoint should dictate the entry point rather than the bundle.

Legacy deployment: the legacy gtc start --bundle X path doesn't create environment endpoints at all, so there is nothing to attach a welcome flow to — the bundle's static-routes / message flow is the entry point. Welcome flow is a new-model feature: it only exists because the routed, multi-bundle environment gives each channel a first-class endpoint object.

5 · Secret refs — create-time only

What: reference strings pointing at already-stored secrets (e.g. secrets://local/legal/_/messaging-telegram/…) — never the values. They're forwarded to the endpoint only when it is first created.

What happens on a re-apply: if the endpoint already exists and your refs differ from what's stored, apply emits a warning and leaves them untouched — there is no endpoint-update verb yet — so get them right on the first apply. These are distinct from the derived per-bundle secrets in section 4 (the bot token, resolved from each bundle's provider packs and scoped by route tenant). For the Telegram demo the credential is that per-tenant token, so endpoint secret_refs stay blank.

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.

  1. ensure-environment — create the local env (default env-pack bindings + trust-root seed).
  2. bootstrap-trust-root — seed your operator signing key (the question 1 step).
  3. put-secret ×2 — write each bot token into the secret store, read from $TELEGRAM_*_BOT_TOKEN.
  4. deploy-bundle ×2 — stage + activate each bundle, bound to its path prefix and tenant.
  5. add-endpoint ×2 — register each Telegram endpoint.
  6. 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.