diff --git a/plugins/temporal/skills/temporal-cloud-setup/SKILL.md b/plugins/temporal/skills/temporal-cloud-setup/SKILL.md index 0ce68e7..4ce4df1 100644 --- a/plugins/temporal/skills/temporal-cloud-setup/SKILL.md +++ b/plugins/temporal/skills/temporal-cloud-setup/SKILL.md @@ -1,7 +1,7 @@ --- name: temporal-cloud-setup description: Set up Temporal Cloud and run a sample Workflow on it for the user, doing the work end to end. Use when the user wants to set up Temporal Cloud, get started on Temporal Cloud, install the unified Temporal CLI (prerelease cloud-cli), create a Cloud namespace or API key, clone a money-transfer sample app, write the client config TOML, or connect a local Worker to Temporal Cloud and run a sample Workflow. This is the Cloud setup path, not the local learning path (see temporal-getting-started). Covers Python, TypeScript, Go, Java, .NET, and Ruby SDKs. -version: 0.7.0 +version: 0.8.0 disable-model-invocation: true --- @@ -13,6 +13,8 @@ You are an operator running the Temporal Cloud setup **for** the user. Do the wo This is the **Cloud** path. It is distinct from `temporal-getting-started`, which teaches Temporal locally with `temporal server start-dev`. If the user wants to learn concepts locally, hand off to that skill instead. +**Environment this skill needs — a local shell with outbound network.** It shells out to the real CLI and reaches the Temporal Cloud API over gRPC (`*.tmprl.cloud`). It will **not** work from a sandbox that blocks outbound network. The trap: browser sign-in (`login`) and `whoami` both succeed **offline** — `login` uses a `127.0.0.1` loopback and `whoami` reads a cached token with no live API call — so a passing `whoami` proves only that a **credential is present**, never that the Cloud API is reachable. `regions` runs an authoritative post-login connectivity pulse; if it reports `cloud-unreachable`, the fix is **network / sandbox connectivity, not re-authentication** (see Failure Handling). Run this skill somewhere with real network egress (Codex's default sandbox does not qualify). + ## Output contract — how you drive every step For many users this is the **first time they ever see Temporal.** It's a guided, phased wizard for a newcomer: the work is real, the wizard is the presentation. **The tracker + step checklists tell the story — not prose.** @@ -52,7 +54,7 @@ For many users this is the **first time they ever see Temporal.** It's a guided, - **No Skip** — a go-ahead step's choices are only `1. / 2. Chat about this` (every step is required; "Chat about this" never skips it — it answers a question, then re-presents). Don't print a "no skip" note. - **Disclose in full.** A bundled subcommand (e.g. `scaffold` = clone + deps) gets **one** gate, but its GATE block shows **all** its commands. Don't unbundle into per-`temporal` gates; don't hide what it runs. - **Never edit this skill's files** — invoke `scripts/provision.sh` as shipped; it's pinned to run unchanged on every platform (macOS bash 3.2). Reformatting/"tidying" its punctuation, quoting, regexes, or flags is forbidden. The only file you change on disk is the user's `temporal.toml`, via the script. If a flag has genuinely drifted (script returns `status=error`), stop and report it as a one-line maintenance note — don't fix it mid-run. -- **Already-satisfied prerequisite** → render its checklist item as `- [x] — already present, skipped` (don't fake-install it). +- **Already-satisfied prerequisite** → render its checklist item as `- [x] — already present, skipped` (don't fake-install it). **Exception: the Temporal CLI.** When the CLI is already present the Install-CLI step *updates* it to the latest (PE-79), so render that step as updated/up-to-date, never "skipped" — see the Install-CLI flow step. - **Secret carve-out** (below) overrides disclosure for the API-key token. @@ -61,23 +63,23 @@ For many users this is the **first time they ever see Temporal.** It's a guided, -The whole run in order. Tiers: **DISCLOSE** = render the gate from its template (§Gate templates) — agent-rendered text, no tool call, never prompts — then run; the user's own permission prompt is the approval; **GO-AHEAD** = render the gate, then append `1. / 2. Chat about this` and wait (only the three deliberate steps); **INPUT** = a numbered question (no script). Each step is one `scripts/provision.sh` subcommand unless noted. "On-error" lists the `error_code`s to map via Failure Handling. +The whole run in order. Tier legend (full mechanics in the output contract above): **DISCLOSE** = render the gate, then run (the user's permission prompt is the approval); **GO-AHEAD** = render, then append `1. / 2. Chat about this` and wait (only the three deliberate steps); **INPUT** = a numbered question (no script). Each step is one `scripts/provision.sh` subcommand unless noted. "On-error" lists the `error_code`s to map via Failure Handling. | # | Phase | Step | Tier | Subcommand | Emits | On-error | |---|-------|------|------|------------|-------|----------| | 1 | 1 | Choose SDK | INPUT | — (numbered list) | sdk | — | -| 2 | 1 | Preflight | DISCLOSE | `preflight --sdk` | `config_path`,`warnings`,`stray_env` | — | +| 2 | 1 | Preflight | DISCLOSE | `preflight --sdk` | `config_path`,`warnings`,`stray_env` | `config-dir-unwritable` | | 3 | 1 | Detect tools + pick manager | DISCLOSE (+ INPUT if >1 manager) | `detect-tools --sdk` | `default`,`managers`,`discrepancies` | `version-too-old` (advisory) | -| 4 | 1 | Install CLI | DISCLOSE *(skip if `cli_installed`)* | `install-cli` | `status` (`skipped` if present) | `brew-missing`,`manual-install` | +| 4 | 1 | Install / update CLI | DISCLOSE | `install-cli` | `status` (`ok`); `update` (`updated`/`up-to-date`/`skipped`/`failed`) | `brew-missing`,`manual-install` | | 5 | 1 | Sign in | **GO-AHEAD** | `login` | `identity` | `login-failed`,`not-authenticated` | -| 6 | 1 | List + pick region | DISCLOSE + INPUT | `regions` | region list | `regions-empty` | +| 6 | 1 | List + pick region | DISCLOSE + INPUT | `regions` | region list | `cloud-unreachable` | | 7 | 2 | Start namespace (async) | DISCLOSE | `start-namespace --sdk --region` | `namespace_name` | `create-rejected` | | 8 | 2 | Choose clone dir | INPUT | — (1=default / 2=Edit) | dir | — | | 9 | 2 | Scaffold the app | DISCLOSE | `scaffold --sdk [--manager] [--dir]` | `repo_path`,`manager` | `clone-failed`,`unknown-sdk`,`manager-not-found`,`unsupported-manager` | | 10 | 2 | Await namespace (join) | DISCLOSE | `await-namespace --name` | `namespace_handle`,`address` | `namespace-timeout`,`namespace-not-provisioning`,`handle-not-found` | -| 11 | 2 | Create key + save config | DISCLOSE | `create-key --handle --address` | `key_id` (token never printed) | `key-empty`,`no-json-parser`,`manual-key-needed` | +| 11 | 2 | Create key + save config | DISCLOSE | `create-key --handle --address` | `key_id` (token never printed) | `key-empty`,`key-limit-reached`,`no-json-parser`,`manual-key-needed` | | 12 | 2 | Verify config | DISCLOSE | `verify-config` | — | `profile-missing` | -| 13 | 3 | Await auth | DISCLOSE | `await-auth` | `auth_ready` | `auth-timeout` | +| 13 | 3 | Await auth | DISCLOSE | `await-auth` | `auth_ready` | `auth-timeout`,`key-expired` | | 14 | 3 | Run the Workflow | **GO-AHEAD** | `run-workflow --sdk --dir` | `workflow_status`,`workflow_id`,`run_id` | `worker-unauthorized`,`worker-not-polling`,`worker-start-failed`,`workflow-failed`,`workflow-not-submitted`,`workflow-timeout`,`precompile-failed` | | 15 | 4 | Inject failure + recover | **GO-AHEAD** | `run-workflow … --demo-failure transient` | same as 14 | same as 14 | @@ -108,13 +110,13 @@ status=ok # ok | error | skipped === END === ``` -Human-readable progress goes to **stderr** (it shows in the expandable tool block — the teaching surface). On `status=error` the block carries `error_code` + `message`: map the code via **Failure Handling** below and fix the cause — **do not improvise an alternate command, switch output formats, or poll.** +Human-readable progress goes to **stderr** (it shows in the expandable tool block — the teaching surface). On `status=error` the block carries `error_code` + `message` — map it via **Failure Handling** and fix the named cause (never improvise, switch output formats, or poll — as the output contract requires). The flow steps — subcommand, tier, and error codes — are the **Steps spine table above** (single source of truth). The RESULT keys each emits: -- `preflight` → `os`, `config_path`, `cli_installed` (drives the Install-CLI skip), `warnings`, `stray_env` +- `preflight` → `os`, `config_path`, `cli_installed` (drives Install-CLI: install if absent, update if present), `warnings`, `stray_env` - `detect-tools` → `default`, `managers`, `versions`, `discrepancies` -- `install-cli` → `status` (`skipped` if present) · `login` → `identity` · `regions` → raw list on stderr (you recommend, user picks) +- `install-cli` → `status` (`ok`) + `update` (`updated`/`up-to-date`/`skipped`/`failed` when present; `skipped`/`failed` still proceed with the working CLI) + `reason` (`brew-missing`/`unsupported-os`, present only alongside `update=skipped`) · `login` → `identity` · `regions` → raw list on stderr (you recommend, user picks) - `start-namespace` → `namespace_name` · `scaffold` → `repo_path`, `manager` · `await-namespace` → `namespace_handle`, `address` - `create-key` → `key_id` (token never printed) · `verify-config` → profile names only · `await-auth` → `auth_ready` - `run-workflow` → `workflow_status` (`COMPLETED`), `workflow_id`, `run_id`, `task_queue` (add `--demo-failure transient` for Phase 4) @@ -123,15 +125,15 @@ The flow steps — subcommand, tier, and error codes — are the **Steps spine t **Utility subcommands are gated exactly like flow steps — disclose before running.** "Not in the main flow" means *don't run them as routine steps*, **not** that they skip disclosure: if you ever invoke one (`install-deps` to switch a manager, `repair-config` to fix a duplicate profile, `clone`, etc.), render its gate first (derive it from the `scaffold`/`install-cmd` shapes in §Gate templates — these utilities have no dedicated template). **And don't improvise them into the flow:** the main steps already cover the work (`scaffold` clones *and* installs dependencies — never add an extra `install-deps` "to confirm deps resolve", and never narrate doing so). -The script is the **single source of truth for CLI flags**, and it is **read-only during a run** (read-only script): invoke it as shipped, never edit it. If it returns `status=error` because a prerelease flag has genuinely drifted, **stop and report that to the user as a one-line maintenance note** — fixing the script is a separate, deliberate task for a human, not something to do mid-setup. The wizard layer (tracker, checklists, checkpoints, the no-narration rules above) is still yours; only the imperative CLI work lives in the script. +The script is the **single source of truth for CLI flags** and is **read-only during a run** — invoke it as shipped, never edit it; if a prerelease flag has genuinely drifted (`status=error`), stop and report it as a one-line maintenance note (per the output contract), never fix it mid-setup. The wizard layer (tracker, checklists, checkpoints, the no-narration rules above) is still yours; only the imperative CLI work lives in the script. ## Per-command gate — disclose, then run -This setup runs real commands that **create billable Cloud resources and install software on the user's machine**. Earlier versions ran the whole bundle with no visible disclosure, which felt "too magic." So **before each `provision.sh` command, show what it will do** — render its gate. The user's own tool-permission prompt is where they approve or deny (it shows the same command via your `description`); the skill doesn't stack its own approval on top. The exception is the three **go-ahead** steps (`login`, `run-workflow`, inject-failure), which additionally wait for a go-ahead (`1. / 2. Chat about this`). This is disclosure + a light control, not narration — keep it tight. +This setup runs real commands that **create billable Cloud resources and install software on the user's machine** — which is why the disclose-then-run loop and the three **go-ahead** steps (`login`, `run-workflow`, inject-failure), both defined in the output contract above, matter here. This section pins the **exact shape** of the gate you render. **Deterministic backstop (don't rely on it):** every effectful `provision.sh` subcommand now also echoes its own gate to stderr (the tool block) *before* it acts, so the run is self-documenting even if you forget the chat-side gate. This is a safety net — it surfaces bundled with the result, *after* the action — so it never replaces rendering the gate first. Always render the §Gate-template gate, then run. (Disable only for tests via `TCLOUD_DISCLOSE=0`.) -**The gate — render it from the matching template in §Gate templates; never run a script to build it.** Each step has a verbatim template (a plain bold heading, then a fenced ` ```bash ` block with `#` comments above each command); fill **only** its `‹slots›` from their named sources and **print it exactly** — do not reassemble or reformat it (hand-assembling formatting is where it kept breaking: dropped fences, comment-only, glued rules). Rendering is **agent text — zero tool calls**, so disclosure never trips a permission prompt; only the effectful `scripts/provision.sh` run does. For a **disclose** step, render the gate and run. For a **go-ahead** step, append the numbered choices **stacked one per line** and wait. The gate looks like this (a disclose step — render it, then run): +**The gate — render it from the matching template in §Gate templates; never run a script to build it** (hand-assembling the formatting is error-prone: dropped fences, comment-only, glued rules). Fill **only** its `‹slots›` and print it exactly; rendering is **agent text — zero tool calls**. For a **go-ahead** step, append the numbered choices **stacked one per line** and wait. The gate's shape — a plain bold heading, then a fenced ` ```bash ` block with `#` comments above each command — looks like this (a disclose step — render it, then run): ```` **Installing the Temporal CLI** @@ -182,15 +184,15 @@ cd money-transfer-project-template-python && WORKFLOW_ID=money-transfer-demo pyt **A few rules these examples encode** (everything else is in the output contract above — don't restate it): -- **A `#` comment above every command — never a comment alone, never a bare command.** The real commands must appear (a reported failure: the run step once showed only `# Worker` / `# starter` with the commands missing). Keep each comment to a few words; it's both a label and a one-line lesson for a newcomer. +- **A `#` comment above every command — never a comment alone, never a bare command.** The real commands must appear (never just `# Worker` / `# starter` with the commands missing). Keep each comment to a few words; it's both a label and a one-line lesson for a newcomer. - **Name material side effects in the relevant comment** — `# … - billable`, `# … (adds software to your machine)`, `# mint key + write the cloud-setup profile to temporal.toml`. The §Gate templates already encode this; render them verbatim. - **`create-key` secret carve-out:** its GATE block shows the mint command **without** the token (captured straight into the locked TOML) — render as-is; never a token, never a redacted diff. ## Gate templates -These are the **verbatim source** for every step's gate. The disclosure is **agent-rendered text — zero tool calls** (no `scripts/preview.sh`, so nothing prompts before the user even sees the command). Only the effectful `scripts/provision.sh ` run prompts. +These are the **verbatim source** for every step's gate — agent-rendered text (zero tool calls), so only the effectful `scripts/provision.sh ` run ever prompts. -**Hard rule — render the matching template verbatim.** Substitute **only** the `‹slots›`, never add/drop/reorder/reformat lines or fences; keep every static character (headings, `#` comments, the ` ```bash ` fence, spacing) byte-for-byte. Each `‹slot›`'s value comes **only** from its named source in "Filling the slots" below — never from memory, never improvised. The result is exactly what `scripts/preview.sh ` used to print between its `=== GATE ===`…`=== END GATE ===` markers. +**Hard rule — render the matching template verbatim.** Substitute **only** the `‹slots›`, never add/drop/reorder/reformat lines or fences; keep every static character (headings, `#` comments, the ` ```bash ` fence, spacing) byte-for-byte. Each `‹slot›`'s value comes **only** from its named source in "Filling the slots" below — never from memory, never improvised. The result is exactly what `scripts/provision.sh preview ` prints between its `=== GATE ===`…`=== END GATE ===` markers (a maintenance/testing subcommand — the flow never calls it; the drift-guard test keeps these templates and `provision.sh`'s runtime commands in sync). ### Filling the slots @@ -266,7 +268,7 @@ Source of truth = `scripts/provision.sh`. Keyed by `‹sdk›` (and `‹manager | dotnet | `dotnet run --project MoneyTransferWorker` | `dotnet run --project MoneyTransferClient` | | ruby | `ruby worker.rb` | `ruby starter.rb` | -The scaffold install-comment also varies by where deps land — keep the comment exactly as the template shows for that `‹sdk›`/`‹manager›` (python/ts say "inside the repo"; go/java/dotnet/ruby say "GLOBAL, outside the repo …"). The worked example and templates below carry the right wording per SDK; for non-python SDKs use the install comment from `scripts/preview.sh scaffold --sdk ‹sdk›` if you ever need to re-verify it (maintenance only). +The scaffold install-comment also varies by where deps land — keep the comment exactly as the template shows for that `‹sdk›`/`‹manager›` (python/ts say "inside the repo"; go/java/dotnet/ruby say "GLOBAL, outside the repo …"). The worked example and templates below carry the right wording per SDK; for non-python SDKs use the install comment from `scripts/provision.sh preview scaffold --sdk ‹sdk›` if you ever need to re-verify it (maintenance only). ### Templates @@ -279,6 +281,9 @@ The scaffold install-comment also varies by where deps land — keep the comment # check git / jq / brew are available (read-only, local) command -v git jq brew +# check the Temporal config directory is writable (so temporal.toml can be saved) +touch "$(dirname "")/.probe" && rm -f "$(dirname "")/.probe" + # flag any stray TEMPORAL_* env vars that would override your saved config env | grep '^TEMPORAL_' || true ``` @@ -312,11 +317,21 @@ brew install temporalio/prerelease/temporal-cloud **Phase 1 — `install-cli` (already installed — render this variant instead when the CLI is present):** ```` -**Temporal CLI already installed - nothing to do** +**Updating the Temporal CLI to the latest** + +```bash +# BETA: the prerelease CLI has no real versions yet, so always pull the latest (adds/updates software) +brew upgrade temporalio/prerelease/temporal-cloud +``` +```` + +**Phase 1 — `install-cli` (already installed, non-macOS — render this variant when the CLI is present and you're not on macOS; there's no prerelease tap to upgrade from):** + +```` +**Updating the Temporal CLI to the latest** ```bash -# the Temporal CLI is already on your machine, so this step is skipped (no install, no update) -temporal cloud version +# already installed; update temporal-cloud manually from https://github.com/temporalio/cloud-cli/releases/latest ``` ```` @@ -505,7 +520,7 @@ This is the **single source of truth for per-phase formatting** — the phase bo 1. **Tracker line** at the top, marker advanced (see below). 2. **Intent sentence** — one short line on what this phase sets up and why it matters (gloss any Temporal term). No more than one line. -3. **Step checklist — once, all unchecked** (the phase plan). Then run each step (real tool call with a friendly `description`, or a genuine question) **without re-printing the checklist or tracker between steps**. Render an already-present prerequisite as `[x] … already present, skipped`. **No** `**What this did:**` summary and **no** `↪ Learn more:` link during the run. +3. **Step checklist — once, all unchecked** (the phase plan). Then run each step (real tool call with a friendly `description`, or a genuine question) **without re-printing the checklist or tracker between steps**. Render an already-present prerequisite as `[x] … already present, skipped` (except the Temporal CLI, which updates when present — render it updated/up-to-date, not "skipped"). **No** `**What this did:**` summary and **no** `↪ Learn more:` link during the run. 4. **End-of-phase: completed checklist + checkpoint** — print the checklist once more with **every box checked**, show `**Phase N complete ✅**`, and **close that same message** with the numbered checkpoint prompt (its content is detailed in the next section — it belongs to *this* message, it is not a second message). In Codex-style runtimes, this entire block must be the final assistant message of the turn when you pause for the user; do not print it earlier as progress and then repeat or fragment it in the final response. No checkpoint after Phase 4 — go straight to the Ending. The tracker is just the four phase markers and the phase counter — **no leading label** (don't prefix it with "Setup" or anything before the first marker). Legend: completed = `✅`, current = `🔵`, upcoming = `⚪` (a white dot — same filled-circle style as the blue current dot). **Bold the current step's name** (the one with the blue dot). Reprint it at the top of each phase, advancing one marker: @@ -556,7 +571,7 @@ Step checklist: `SDK chosen` · `Tools detected` · `CLI installed` · `Signed i **Step — Choose your SDK** (genuine input, not a checkpoint): present the six as a **numbered list** (numbered list, runtime-agnostic) — Python, Go, TypeScript, Java, .NET, Ruby — and take a typed name/number. This selects which repo is cloned and the language of the local app. Echo the resolved profile line (`Setting up for: macOS · Python SDK`) once answered. -Right after the SDK pick, the **preflight** check (DISCLOSE — render, then run): render its gate from the `preflight` template (§Gate templates), then run `scripts/provision.sh preflight --sdk `. Note its `cli_installed` flag — it drives whether the Install-CLI step below runs or is skipped. If its `stray_env` lists any `TEMPORAL_*` vars, tell the user they override the saved profile and ask them to unset them before continuing; surface other `warnings` (e.g. `brew-missing`, `no-json-parser`) only if they block a later step. +Right after the SDK pick, the **preflight** check (DISCLOSE — render, then run): render its gate from the `preflight` template (§Gate templates), then run `scripts/provision.sh preflight --sdk `. Note its `cli_installed` flag — it drives whether the Install-CLI step below installs (absent) or updates the existing CLI (present). If its `stray_env` lists any `TEMPORAL_*` vars, tell the user they override the saved profile and ask them to unset them before continuing; surface other `warnings` (e.g. `brew-missing`, `no-json-parser`) only if they block a later step. **Step — Detect local tools + choose your package manager** (DISCLOSE — render, then run). Render its gate from the `detect-tools` template (§Gate templates), then run `scripts/provision.sh detect-tools --sdk ` and read its RESULT. This adapts the setup to the user's machine, and it surfaces tooling problems **early** (here in Phase 1) instead of deep in Phase 2/3. @@ -566,10 +581,10 @@ Right after the SDK pick, the **preflight** check (DISCLOSE — render, then run - `tool-missing:` / `manager-not-found:` — the runtime or chosen manager isn't installed. Offer another **sample-supported** manager from `managers`, or ask the user to install the tool, then re-run `detect-tools`. - The default proposal is **deterministic** — the same machine yields the same default every run; nothing is persisted (no state file). -**Step — Install the unified Temporal CLI** (DISCLOSE — render, then run, *unless already present*). It ships the `temporal cloud` command group (binary `temporal-cloud`). **Branch on preflight's `cli_installed`** (it used the same `temporal cloud` probe the installer does, so it's authoritative — don't re-check by calling `install-cli` just to confirm): +**Step — Install or update the unified Temporal CLI** (DISCLOSE — render, then run). It ships the `temporal cloud` command group (binary `temporal-cloud`). **Branch on preflight's `cli_installed`** (it used the same `temporal cloud` probe the installer does, so it's authoritative — don't re-check by calling `install-cli` just to confirm): -- **`cli_installed=true` → skip the step entirely.** Render the checklist item as `- [x] Temporal CLI — already present, skipped` and do **not** render the install gate or call `install-cli`. No gate, no tool call — marking it skipped *is* the step (per the "already-satisfied prerequisite" rule). We deliberately do **not** auto-update a working CLI (don't change a working install out from under the user). -- **`cli_installed=false` → install it.** Render its gate from the `install-cli` template (§Gate templates), then run `scripts/provision.sh install-cli`. It installs via the `temporalio/prerelease` Homebrew tap on macOS, and returns `error_code=brew-missing` / `manual-install` with the fallback URL if it can't — do not auto-install Homebrew; relay the message and wait. (`install-cli` still self-checks presence and emits `status=skipped` as a backstop, but in the normal flow you won't reach it when the CLI is already there.) +- **`cli_installed=true` → update it to the latest.** Render its gate from the `install-cli` (already-installed) template (§Gate templates), then run `scripts/provision.sh install-cli`. **BETA stopgap:** the prerelease CLI has no meaningful version numbers yet, so instead of a real "is it out of date?" check we always try to pull the latest from the `temporalio/prerelease` Homebrew tap; `install-cli` returns `status=ok` with `update`=`updated`/`up-to-date`. When it can't update — Homebrew missing, or a non-macOS host — it emits `update=skipped` and proceeds with the working CLI rather than failing (never yank a working install). Once the CLI ships real versions this becomes a genuine version check; we no longer skip a present CLI. +- **`cli_installed=false` → install it.** Render its gate from the `install-cli` (not-installed) template (§Gate templates), then run `scripts/provision.sh install-cli`. It installs via the `temporalio/prerelease` Homebrew tap on macOS, and returns `error_code=brew-missing` / `manual-install` with the fallback URL if it can't — do not auto-install Homebrew; relay the message and wait. (`install-cli` self-checks presence, so if it's actually already there it updates instead of installing.) - (No prerelease disclaimer here — the ⚠️ notice at the top of the run already covers that.) This CLI is separate from any local `temporal server start-dev`. The Cloud path does not start a local server. @@ -647,9 +662,10 @@ For reference, the per-SDK repo mapping (the script selects the right one): It returns only the **non-secret** `key_id` and the `config_path` — never the token. Then verify the profile (DISCLOSE — render, then run): render its gate from the `verify-config` template (§Gate templates), then run `scripts/provision.sh verify-config` to confirm the profile loads (it never prints the api_key value). -This is the **secret-handling carve-out** — render the action's label without the token (e.g. `api_key = "eyJ…(captured, not shown)"`); never reprint, log, argv-pass, or commit it. +This is the **secret-handling carve-out** (§ above): render the action's label without the token; never reprint, log, argv-pass, or commit it. - On `error_code=key-empty`/`not-authenticated`: an expired login — let the script re-prompt and retry once; don't switch output formats or poll. +- On `error_code=key-limit-reached`: the account is at its API-key cap — the mint was rejected at create time. Have the user delete stale keys (`temporal cloud apikey list`, then `temporal cloud apikey delete --key-id ` on old `money-transfer-cloud-setup-*` keys), then re-run `create-key`. Don't re-run login. - On `error_code=no-json-parser`: install `jq` or `python3`, then re-run (the safe capture needs one). - On `error_code=manual-key-needed`: automatic capture failed and there was no terminal to paste into. Ask the **user** to paste the one-time key and re-run `create-key` from a context with a terminal — the script reads the paste *hidden*, straight into the locked file. **Never** have the user paste the key into the chat, and never paste it yourself. - The secret is shown only once and cannot be retrieved later; if it's truly lost, mint a new key (re-run this step) — don't try to recover the old value. @@ -684,7 +700,7 @@ The app connects from config and Phase 2 supplied the credentials — this phase Run two script calls, both using `repo_path` from Phase 2 (the script handles per-SDK run commands and Python venv activation internally — you don't): -1. **Wait for auth** (`await-auth`, DISCLOSE — render, then run): render its gate from the `await-auth` template (§Gate templates), then run `scripts/provision.sh await-auth` and wait for `auth_ready=true`. +1. **Wait for auth** (`await-auth`, DISCLOSE — render, then run): render its gate from the `await-auth` template (§Gate templates), then run `scripts/provision.sh await-auth` and wait for `auth_ready=true`. On `error_code=key-expired`, the key is permanently rejected (commonly a next-day re-test against a key that auto-expired in ~25h) — re-run `create-key` (see Failure Handling); on `auth-timeout`, read the appended redacted CLI stderr, wait, and re-run. 2. **Run the Workflow** (`run-workflow --sdk --dir `, **GO-AHEAD** — the deliberate moment): render its gate from the `run-workflow` (clean run) template (§Gate templates), append `1. Run it / 2. Chat about this` (on `2`, answer, then re-present), and on `Run it` one synchronous call starts the Worker, waits until it's polling (Temporal's API, not the OS process table), runs the starter, and stops the Worker. Wait for `workflow_status=COMPLETED`; it emits the run's **`workflow_id`** + **`run_id`** (don't run `workflow list` yourself). On `worker-unauthorized`, re-run `await-auth` then `run-workflow` (see Failure Handling). 3. **Show the success link, then confirm the win.** `run-workflow` already verified `COMPLETED`. Surface the run's **timeline** page on its own bare line (bare URL so the terminal auto-linkifies it — no backticks/fence), using the `workflow_id` and `run_id` from the RESULT block: @@ -774,44 +790,11 @@ The summary above is the payoff — keep it to that one short recap. The only ca ## Failure Handling - - -Stop and surface the problem (don't silently retry destructive or auth steps), staying in the calm output style — report the problem and fix plainly, without exposing reasoning. The script ops fail loudly with `status=error` + an `error_code`; **fix the cause the code names and re-run the same op — never improvise an alternate command, switch output formats, or poll.** Map the codes: - -- **`brew-missing` / `manual-install`** (install-cli) → relay the script's message: install Homebrew from https://brew.sh, or download `temporal-cloud` from the releases page and put it on `PATH`. Do **not** auto-install Homebrew. Re-run install-cli. -- **`login-failed` / `not-authenticated`** (login, or any later op) → the browser sign-in didn't complete or the session expired. Ask the user to finish/redo the browser login, then re-run the op (create-namespace / create-key re-check auth themselves). -- **`regions-empty`** → confirm auth (`login`), then re-run `regions`. Don't hand-prefix or guess a region. -- **`create-rejected`** (start-namespace) → the namespace create was rejected on submit, usually region or name format; re-list regions, have the user pick an exact provider-prefixed value, then re-run `start-namespace`. Attribute this as a **namespace** failure, never as a downstream key error. -- **`namespace-timeout`** (await-namespace) → the namespace **appeared** (ACTIVATING) but didn't reach **ACTIVE** within the bound (`NS_AWAIT_MAX_SECS`, default 600s). It's provisioning lag, not a misconfig: re-run `await-namespace` (it resumes polling the exact `namespace list --name` filter until the namespace is ACTIVE); raise the bound with `NS_AWAIT_MAX_SECS=N` if needed. Don't switch to `namespace get`/other formats. (Waiting for ACTIVE here is what keeps `create-key`/`await-auth` from connecting to an endpoint that isn't serving yet — the cause of a "no children to pick from" stall.) -- **`namespace-not-provisioning`** (await-namespace / provision-and-scaffold) → the create was accepted but the namespace **never appeared** in the list within the phantom-grace window (`NS_PHANTOM_GRACE_SECS`, default 75s) — i.e. it's not provisioning at all, vs. just slow. Almost always an **unavailable region** (e.g. `azure-centralus`, whose provider reads `UNKNOWN` — see the region step's `unsupported_regions`). Don't re-run `await-namespace` on the same name; **re-run `start-namespace` with an AWS/GCP region**. -- **`handle-not-found`** (provision-and-scaffold) → the namespace was created but didn't reach **ACTIVE** within the retry bound (still provisioning). Re-run `provision-and-scaffold` or `await-namespace` to resume the wait. **Never** decode the API-key token or hunt the filesystem/config for the account-id — the exact `namespace list --name` filter is the source. -- **`clone-failed` / `unknown-sdk`** (scaffold) → the sample clone failed (network/repo) or the SDK has no repo mapping; confirm the SDK + connectivity, then re-run `scaffold`. (Independent of the namespace, which is already provisioning.) -- **`manager-not-found`** (scaffold / install-deps) → the chosen package manager isn't installed on this machine. Offer an **available** manager from the latest `detect-tools` `managers` list, or ask the user to install the missing one, then re-run with that `--manager`. Caught **before** the clone, so nothing was set up. -- **`unsupported-manager`** (scaffold / install-deps) → that manager isn't valid for this SDK's sample (e.g. `poetry` for Python, whose sample ships no `pyproject.toml`). Pick one of the supported managers named in the error / `detect-tools` and re-run. -- **`version-too-old`** (detect-tools `discrepancies`, **advisory**) → not a hard error and never blocks the run. Relay the remediation (upgrade the tool to the noted minimum) but you may proceed — the sample usually still works on the older version. -- **`no-json-parser`** (create-key) → install `jq` or `python3` (needed to capture the token safely), then re-run create-key. -- **`key-empty` / `key-create-failed`** (create-key) → almost always an expired login (not an output-format problem); the script re-checks `whoami` — redo the browser login if prompted, then re-run create-key once. -- **`config-write-failed`** (create-key) → the profile couldn't be written to `temporal.toml` (read-only dir or full disk); the key was minted but not saved. Fix directory permissions / free disk, then re-run create-key (it mints a fresh key and writes a clean profile). -- **API key lost** (only shown once) → re-run create-key to mint a fresh one (it rewrites the profile); don't try to recover the old value. -- **`temporal.toml` unparseable / duplicate `[profile.cloud-setup]` blocks** (e.g. from earlier partial runs) → run `scripts/provision.sh repair-config` (strips every `cloud-setup` block via awk, keeps `[profile.default]`, never reads the file into context), then re-run `create-key` to write one fresh profile. **Do not hand-edit, `cat`, or `awk` the file yourself** (the read-only-script rule and the secret carve-out) — `create-key` also strips any existing/duplicate `cloud-setup` blocks before writing, so it self-heals too. -- **`worker-unauthorized`** (run-workflow) → the Worker hit an auth error before it could poll — the just-minted key isn't accepted yet. Re-run `await-auth` (wait for `auth_ready=true`), then re-run `run-workflow`. Do **not** switch endpoints, re-mint the key, or edit the profile (it's readiness, not config). -- **`precompile-failed`** (run-workflow) → the Maven/dotnet build step failed before the Worker started. Java and .NET are pre-compiled once so the timing windows cover only Temporal operations. Check the output above; usually a missing Java/Maven/.NET SDK installation or a network issue fetching dependencies on the first build. -- **`worker-not-polling`** (run-workflow) → the Worker started but never registered as a poller within the bound (`WORKER_READY_MAX_SECS`, default 120s). Usually deps weren't installed or the Worker process crashed early. Confirm `scaffold` finished and `--dir` is the real `repo_path`, then re-run; raise the bound with `WORKER_READY_MAX_SECS=N` if needed. The script prints the Worker log tail to help. -- **`worker-start-failed`** (run-workflow) → the Worker process exited before polling (missing deps/venv, wrong dir, or a sample-app error in the log tail). Confirm deps installed and `--dir` is correct, then re-run. -- **`workflow-failed`** (run-workflow) → the starter exited non-zero / the Workflow didn't reach `COMPLETED`. Read the printed log tail: if it's an auth error, run `await-auth` and retry; otherwise surface the sample-app error. (Expected `FAILED` for the manual `DEMO_FAILURE=permanent` variant is **not** run through `run-workflow`.) -- **`workflow-not-submitted`** (run-workflow) → the starter exited **0 but never submitted a Workflow** within the settle window (`NOWF_SETTLE_SECS`, default 15s). Some sample clients catch their own connect/start error and still exit 0 (the .NET starter does this), so a clean exit code can hide a failed start. Read the printed log tail — it's almost always an auth/connection error: run `await-auth` (wait for `auth_ready=true`) and re-run `run-workflow`; if the profile points at a namespace that isn't ACTIVE, re-check `await-namespace` first. -- **`workflow-timeout`** (run-workflow) → the starter didn't finish within `--max-secs` (default 180s). The Cloud workflow is automatically terminated on timeout so it doesn't stay Running with no worker. If it still fires, raise with `--max-secs N` and re-run. -- **`Request unauthorized` / `Unavailable` on the first connect (right after setup)** → almost always **post-provision readiness**, not a misconfig: the just-created namespace + key need a moment to become connectable, and the parallel flow shortens that gap. **Wait ~10–15s and retry the connect, up to ~3 times.** Do **not** switch the address to a regional endpoint, re-mint the key, or rewrite the profile. The namespace endpoint (`.tmprl.cloud:7233`) is the correct, Temporal-recommended endpoint for API keys (temporalio/documentation#4733); regional is **not** the fix. If it still fails after retries, that's a Temporal Cloud issue to escalate — not a reason to change the skill's endpoint. -- **`auth-timeout`** (await-auth) → the new API key still isn't accepted after the bound (`AUTH_READY_MAX_SECS`, default 90s). It's propagation, not config: wait longer and re-run `await-auth`; if it never clears, re-run `create-key` to mint a fresh key. Don't switch endpoints or edit the profile. -- **TLS / auth errors at connect** → confirm the profile's `[profile.cloud-setup.tls]` has `disabled = false` (TLS on), the address is the **namespace endpoint** (`.tmprl.cloud:7233`), and the `api_key` is set; re-run `verify-config`. (`create-key` now writes `disabled = false` explicitly.) -- **`temporal cloud …` commands suddenly fail auth (after deleting/expiring the key)** → a profile carrying an `api_key` overrides the login session. Remove the `[profile.cloud-setup]` block, or pass `--disable-config-file`. The setup always lives in the **named** `cloud-setup` profile (never `default`), so management commands keep using the login session. - - +On `status=error`, map the `error_code` via **`references/failure-handling.md`** and fix the exact cause it names — never improvise an alternate command, switch output formats, or poll. Read that file only when an error fires (progressive disclosure); the Steps spine's On-error column indexes which codes each step can emit. ## Files - `scripts/provision.sh` — **the deterministic executor.** Owns preflight / **detect-tools** / **preview** / install / login / regions / namespace-create / **install-deps (manager-parameterized)** / key-mint+config-write / verify / await-auth / **run-workflow (Worker + starter)** / clone / repair-config / cleanup-info. Invoke it and parse its `=== RESULT ===` block (see "Execution model" above); it is the single source of truth for the pinned CLI flags, the per-SDK run commands, **and the per-(SDK,manager) install matrix + minimum-version table**. Pure bash, portable across Claude Code, Codex, and Cursor. **Read-only during a run — invoke it, never edit it (read-only script).** -- `scripts/preview.sh` — **read-only disclosure shim, maintenance/testing only.** A ~3-line forwarder to `provision.sh preview `. The flow no longer calls it: gates are **agent-rendered from §Gate templates** (zero tool calls, so disclosure never prompts on any host). Kept so `provision.sh preview` output stays verifiable (the drift-guard test binds the templates to it). Side-effect-free; never edit it. -- `scripts/tests/run-tests.sh` — offline, stubbed tests for the adaptation work (detection defaults, side-effect-free preview, per-manager install, version discrepancies, ASCII RESULT, no secrets). Dev-only; run under `/bin/bash` (3.2). Not part of a setup run. - `references/unified-cli.md` — background on the prerelease CLI and the client-config TOML: `login`/`whoami`, `region list`, `namespace create`, `apikey create-for-me`, file locations, and the auth-override gotcha. The script encodes these; read the reference when a flag drifts and you need to update the script. - `references/sdk-cloud.md` — per-SDK table: repo + cloud branch, task-queue name, how each connects (`cloud-setup` profile), and worker/starter run commands. No connection edits — the branch is pre-wired. +- `references/failure-handling.md` — the `error_code` → remediation map. Read it **only when a subcommand returns `status=error`** (progressive disclosure — the happy path never opens it); the Failure Handling section above is a one-line pointer to it, and the Steps spine's On-error column is the index. diff --git a/plugins/temporal/skills/temporal-cloud-setup/agents/openai.yaml b/plugins/temporal/skills/temporal-cloud-setup/agents/openai.yaml index e0c8aa6..4808102 100644 --- a/plugins/temporal/skills/temporal-cloud-setup/agents/openai.yaml +++ b/plugins/temporal/skills/temporal-cloud-setup/agents/openai.yaml @@ -3,4 +3,4 @@ interface: short_description: "Set up Temporal Cloud and run a sample Workflow" policy: - allow_implicit_invocation: false \ No newline at end of file + allow_implicit_invocation: false diff --git a/plugins/temporal/skills/temporal-cloud-setup/references/failure-handling.md b/plugins/temporal/skills/temporal-cloud-setup/references/failure-handling.md new file mode 100644 index 0000000..c1ff55f --- /dev/null +++ b/plugins/temporal/skills/temporal-cloud-setup/references/failure-handling.md @@ -0,0 +1,39 @@ +# Failure Handling — `error_code` → remediation map + +Read this when a `scripts/provision.sh` subcommand returns `status=error`. SKILL.md's +Failure Handling section points here; the Steps spine's "On-error" column is the index of +which codes each step can emit. + +Stop and surface the problem (don't silently retry destructive or auth steps), staying in the calm output style — report the problem and fix plainly, without exposing reasoning. The script ops fail loudly with `status=error` + an `error_code`; **fix the cause the code names and re-run the same op — never improvise an alternate command, switch output formats, or poll.** Map the codes: + +- **`brew-missing` / `manual-install`** (install-cli) → relay the script's message: install Homebrew from https://brew.sh, or download `temporal-cloud` from the releases page and put it on `PATH`. Do **not** auto-install Homebrew. Re-run install-cli. +- **`login-failed` / `not-authenticated`** (login, or any later op) → the browser sign-in didn't complete or the session expired. Ask the user to finish/redo the browser login, then re-run the op (create-namespace / create-key re-check auth themselves). +- **`cloud-unreachable`** (the post-login region pulse) → the Cloud API can't be reached. **This is a network / sandbox problem, NOT an auth problem — do NOT re-run `login`.** The classic case: `login` and `whoami` both succeed (they use the browser loopback / a cached token and run **offline**), yet the gRPC Cloud API is blocked by a sandbox or firewall, so the region pulse comes back empty. Tell the user to run the skill from an environment with **outbound internet and gRPC egress to the Temporal Cloud API** (`*.tmprl.cloud`); e.g. Codex's default sandbox blocks this. Re-running sign-in will only loop — the fix is connectivity. Once network is fixed, re-run the failed step. +- **`config-dir-unwritable`** (preflight) → the Temporal config directory can't be written. Fix its permissions, or set `TEMPORAL_CONFIG_FILE` to a writable path, then re-run. Caught **up front**, before any billable key is minted (writing `temporal.toml` is our op, so nothing else surfaces this). +- **`regions-empty`** (legacy code — no longer emitted) → the region step now maps an empty region list onto `cloud-unreachable` (see above), since an empty list means the Cloud API is unreachable, not that auth is missing. If you ever see `regions-empty` from an older build, treat it exactly as `cloud-unreachable`: check network/sandbox, not auth. +- **`create-rejected`** (start-namespace) → the namespace create was rejected on submit, usually region or name format; re-list regions, have the user pick an exact provider-prefixed value, then re-run `start-namespace`. Attribute this as a **namespace** failure, never as a downstream key error. +- **`namespace-timeout`** (await-namespace) → the namespace **appeared** (ACTIVATING) but didn't reach **ACTIVE** within the bound (`NS_AWAIT_MAX_SECS`, default 600s). It's provisioning lag, not a misconfig: re-run `await-namespace` (it resumes polling the exact `namespace list --name` filter until the namespace is ACTIVE); raise the bound with `NS_AWAIT_MAX_SECS=N` if needed. Don't switch to `namespace get`/other formats. (Waiting for ACTIVE here is what keeps `create-key`/`await-auth` from connecting to an endpoint that isn't serving yet — the cause of a "no children to pick from" stall.) +- **`namespace-not-provisioning`** (await-namespace / provision-and-scaffold) → the create was accepted but the namespace **never appeared** in the list within the phantom-grace window (`NS_PHANTOM_GRACE_SECS`, default 75s) — i.e. it's not provisioning at all, vs. just slow. Almost always an **unavailable region** (e.g. `azure-centralus`, whose provider reads `UNKNOWN` — see the region step's `unsupported_regions`). Don't re-run `await-namespace` on the same name; **re-run `start-namespace` with an AWS/GCP region**. +- **`handle-not-found`** (provision-and-scaffold) → the namespace was created but didn't reach **ACTIVE** within the retry bound (still provisioning). Re-run `provision-and-scaffold` or `await-namespace` to resume the wait. **Never** decode the API-key token or hunt the filesystem/config for the account-id — the exact `namespace list --name` filter is the source. +- **`clone-failed` / `unknown-sdk`** (scaffold) → the sample clone failed (network/repo) or the SDK has no repo mapping; confirm the SDK + connectivity, then re-run `scaffold`. (Independent of the namespace, which is already provisioning.) +- **`manager-not-found`** (scaffold / install-deps) → the chosen package manager isn't installed on this machine. Offer an **available** manager from the latest `detect-tools` `managers` list, or ask the user to install the missing one, then re-run with that `--manager`. Caught **before** the clone, so nothing was set up. +- **`unsupported-manager`** (scaffold / install-deps) → that manager isn't valid for this SDK's sample (e.g. `poetry` for Python, whose sample ships no `pyproject.toml`). Pick one of the supported managers named in the error / `detect-tools` and re-run. +- **`version-too-old`** (detect-tools `discrepancies`, **advisory**) → not a hard error and never blocks the run. Relay the remediation (upgrade the tool to the noted minimum) but you may proceed — the sample usually still works on the older version. +- **`no-json-parser`** (create-key) → install `jq` or `python3` (needed to capture the token safely), then re-run create-key. +- **`key-empty` / `key-create-failed`** (create-key) → almost always an expired login (not an output-format problem); the script re-checks `whoami` — redo the browser login if prompted, then re-run create-key once. +- **`key-limit-reached`** (create-key) → the account is at its **API-key cap**, so the mint was rejected at create time (not an auth or output problem — every run mints a fresh key, so a repeat tester accumulates them). Delete stale keys, then re-run create-key: list with `temporal cloud apikey list` and remove old `money-transfer-cloud-setup-*` keys with `temporal cloud apikey delete --key-id `. Don't re-run login or switch output formats. +- **`config-write-failed`** (create-key) → the profile couldn't be written to `temporal.toml` (read-only dir or full disk); the key was minted but not saved. Fix directory permissions / free disk, then re-run create-key (it mints a fresh key and writes a clean profile). +- **API key lost** (only shown once) → re-run create-key to mint a fresh one (it rewrites the profile); don't try to recover the old value. +- **`temporal.toml` unparseable / duplicate `[profile.cloud-setup]` blocks** (e.g. from earlier partial runs) → run `scripts/provision.sh repair-config` (strips every `cloud-setup` block via awk, keeps `[profile.default]`, never reads the file into context), then re-run `create-key` to write one fresh profile. **Do not hand-edit, `cat`, or `awk` the file yourself** (the read-only-script rule and the secret carve-out) — `create-key` also strips any existing/duplicate `cloud-setup` blocks before writing, so it self-heals too. +- **`worker-unauthorized`** (run-workflow) → the Worker hit an auth error before it could poll — the just-minted key isn't accepted yet. Re-run `await-auth` (wait for `auth_ready=true`), then re-run `run-workflow`. Do **not** switch endpoints, re-mint the key, or edit the profile (it's readiness, not config). +- **`precompile-failed`** (run-workflow) → the Maven/dotnet build step failed before the Worker started. Java and .NET are pre-compiled once so the timing windows cover only Temporal operations. Check the output above; usually a missing Java/Maven/.NET SDK installation or a network issue fetching dependencies on the first build. +- **`worker-not-polling`** (run-workflow) → the Worker started but never registered as a poller within the bound (`WORKER_READY_MAX_SECS`, default 120s). Usually deps weren't installed or the Worker process crashed early. Confirm `scaffold` finished and `--dir` is the real `repo_path`, then re-run; raise the bound with `WORKER_READY_MAX_SECS=N` if needed. The script prints the Worker log tail to help. +- **`worker-start-failed`** (run-workflow) → the Worker process exited before polling (missing deps/venv, wrong dir, or a sample-app error in the log tail). Confirm deps installed and `--dir` is correct, then re-run. +- **`workflow-failed`** (run-workflow) → the starter exited non-zero / the Workflow didn't reach `COMPLETED`. Read the printed log tail: if it's an auth error, run `await-auth` and retry; otherwise surface the sample-app error. (Expected `FAILED` for the manual `DEMO_FAILURE=permanent` variant is **not** run through `run-workflow`.) +- **`workflow-not-submitted`** (run-workflow) → the starter exited **0 but never submitted a Workflow** within the settle window (`NOWF_SETTLE_SECS`, default 15s). Some sample clients catch their own connect/start error and still exit 0 (the .NET starter does this), so a clean exit code can hide a failed start. Read the printed log tail — it's almost always an auth/connection error: run `await-auth` (wait for `auth_ready=true`) and re-run `run-workflow`; if the profile points at a namespace that isn't ACTIVE, re-check `await-namespace` first. +- **`workflow-timeout`** (run-workflow) → the starter didn't finish within `--max-secs` (default 180s). The Cloud workflow is automatically terminated on timeout so it doesn't stay Running with no worker. If it still fires, raise with `--max-secs N` and re-run. +- **`Request unauthorized` / `Unavailable` on the first connect (right after setup)** → almost always **post-provision readiness**, not a misconfig: the just-created namespace + key need a moment to become connectable, and the parallel flow shortens that gap. **Wait ~10–15s and retry the connect, up to ~3 times.** Do **not** switch the address to a regional endpoint, re-mint the key, or rewrite the profile. The namespace endpoint (`.tmprl.cloud:7233`) is the correct, Temporal-recommended endpoint for API keys (temporalio/documentation#4733); regional is **not** the fix. If it still fails after retries, that's a Temporal Cloud issue to escalate — not a reason to change the skill's endpoint. +- **`key-expired`** (await-auth) → the poll came back with a **high-confidence permanent key failure** — a permanent qualifier (**expired / invalid / revoked / not found / disabled**) anchored to **jwt / api key / token** context (the real prerelease message is the Envoy JWT-filter desc `Unauthenticated desc = Jwt is expired`; see `references/unified-cli.md`), so await-auth **fast-failed** instead of spinning the full bound; the key won't clear by waiting. The classic cause: keys auto-expire in ~25h, so a next-day re-test polls a dead key. Fix: re-run `create-key` to mint a fresh key (it overwrites the `[profile.cloud-setup]` block). If you're at the API-key cap, delete stale keys first (see `key-limit-reached`), then re-run `create-key`. The message carries a **redacted** CLI stderr tail for context. (A bare, unqualified `Request unauthorized` during propagation is treated as **transient** — see below — so this never wrong-fast-fails a key that just needs a moment.) Don't switch endpoints or edit the profile. +- **`auth-timeout`** (await-auth) → the new API key still isn't accepted after the bound (`AUTH_READY_MAX_SECS`, default 90s), and the failures were **transient** — the usual post-provision `Request unauthorized` propagation, not the qualified permanent text that triggers `key-expired`. Wait longer and re-run `await-auth`; if it never clears, re-run `create-key` to mint a fresh key. The message now appends the **last captured, redacted** CLI stderr line so the timeout is diagnosable instead of blank — read it before deciding. Each poll is itself bounded by a per-call timeout (`AUTH_POLL_CALL_TIMEOUT`, default 15s) so a single wedged call can't hang the loop, and the ~`AUTH_READY_MAX_SECS` budget is counted in real wall-clock. Don't switch endpoints or edit the profile. +- **TLS / auth errors at connect** → confirm the profile's `[profile.cloud-setup.tls]` has `disabled = false` (TLS on), the address is the **namespace endpoint** (`.tmprl.cloud:7233`), and the `api_key` is set; re-run `verify-config`. (`create-key` now writes `disabled = false` explicitly.) +- **`temporal cloud …` commands suddenly fail auth (after deleting/expiring the key)** → a profile carrying an `api_key` overrides the login session. Remove the `[profile.cloud-setup]` block, or pass `--disable-config-file`. The setup always lives in the **named** `cloud-setup` profile (never `default`), so management commands keep using the login session. diff --git a/plugins/temporal/skills/temporal-cloud-setup/references/sdk-cloud.md b/plugins/temporal/skills/temporal-cloud-setup/references/sdk-cloud.md index 7c52754..1552ed5 100644 --- a/plugins/temporal/skills/temporal-cloud-setup/references/sdk-cloud.md +++ b/plugins/temporal/skills/temporal-cloud-setup/references/sdk-cloud.md @@ -2,7 +2,7 @@ For the chosen SDK: clone the **`money-transfer-project-cloud-setup`** branch, install deps, then run the Worker and the starter. **The branch is pre-wired for Cloud — there is no connection edit.** -> **Note:** the clone + deps install are done by `scripts/provision.sh provision-and-scaffold` (PE-68), and the Worker + starter run is done by `scripts/provision.sh run-workflow --sdk --dir ` (PE-70) — a single synchronous call that starts the Worker, waits until it's polling (Temporal API, not `ps`/`pgrep`), runs the starter, and stops the Worker. The per-SDK repo, task queue, and run commands below are the **source of truth the script encodes** — they are reference, not commands you run by hand. +> **Note:** the clone + deps install are done by `scripts/provision.sh provision-and-scaffold`, and the Worker + starter run is done by `scripts/provision.sh run-workflow --sdk --dir ` — a single synchronous call that starts the Worker, waits until it's polling (Temporal API, not `ps`/`pgrep`), runs the starter, and stops the Worker. The per-SDK repo, task queue, and run commands below are the **source of truth the script encodes** — they are reference, not commands you run by hand. How each branch connects: **all six SDKs load the named `cloud-setup` profile from `temporal.toml`** (env-config), so the key stays in the locked `0600` file — never in source, argv, or shell history. Step 6 writes that profile; nothing else is needed at run time. *(Verify the SDK's env-config symbol against current docs before relying on it.)* @@ -88,7 +88,7 @@ Run: `ruby worker.rb` then `ruby starter.rb`. --- -## Package managers & minimum versions (PE-75 adaptation) +## Package managers & minimum versions `scripts/provision.sh detect-tools --sdk ` reports which managers are **supported by the sample AND installed**, picks a deterministic default (first available in preference order; the lockfile's diff --git a/plugins/temporal/skills/temporal-cloud-setup/references/unified-cli.md b/plugins/temporal/skills/temporal-cloud-setup/references/unified-cli.md index 8444f39..2aa093d 100644 --- a/plugins/temporal/skills/temporal-cloud-setup/references/unified-cli.md +++ b/plugins/temporal/skills/temporal-cloud-setup/references/unified-cli.md @@ -162,4 +162,40 @@ To send the user to their **specific Workflow run** in the browser (preferred https://cloud.temporal.io/namespaces//workflows// ``` -The bare list URL (`…/workflows`) is a fallback only — surface the run-specific URL when you have the Workflow ID + Run ID (from the starter output or `workflow describe -o json`). `` is the namespace's full handle from `namespace create` — `.`, e.g. `quickstartai-go-20260617-143205.fmrip`. \ No newline at end of file +The bare list URL (`…/workflows`) is a fallback only — surface the run-specific URL when you have the Workflow ID + Run ID (from the starter output or `workflow describe -o json`). `` is the namespace's full handle from `namespace create` — `.`, e.g. `quickstartai-go-20260617-143205.fmrip`. +## Auth-failure stderr wording (await-auth / workflow list) + +`temporal --profile cloud-setup workflow list` is the auth-readiness poll. On failure +the Cloud API gateway (Envoy) returns a gRPC status whose `desc` is a **JWT-filter** +message — the wording is **JWT-anchored, never "api key"-anchored**. Captured against +the prerelease CLI: + +| Condition | Exact stderr | Classification | +|---|---|---| +| Empty / missing key | `Error: failed reaching server: rpc error: code = Unauthenticated desc = Jwt is missing` | **transient** (can occur mid-propagation) | +| Bad / wrong-issuer key | `Error: failed reaching server: rpc error: code = Unauthenticated desc = Jwt issuer is not configured` | **transient** | +| Expired key | `Error: failed reaching server: rpc error: code = Unauthenticated desc = Jwt is expired` | **permanent → `key-expired` fast-fail** | +| mTLS-only namespace (wrong endpoint) | `Error: failed reaching server: connection error: desc = "error reading server preface: remote error: tls: certificate required"` | **transient** (TLS layer, not auth) | + +Why only `expired` fast-fails: a valid key that is merely *propagating* has a **future +`exp`**, so it can never emit `Jwt is expired` — matching `expired` (anchored to +jwt/key/token) is safe against wrong-fast-failing a key that just needs a moment. The +other descs are ambiguous (can appear during propagation), so they stay transient and +resolve as `auth-timeout` with the redacted stderr line attached. This is the source of +truth for `await_auth_permanent()` in `scripts/provision.sh`. + +> Note: `Jwt is missing` / `issuer is not configured` and the mTLS/TLS error were +> captured live. `Jwt is expired` is the standard Envoy JWT-filter default and the +> expected wording for a real expired key; it was not captured live (the prerelease +> `apikey create-for-me` emits the one-time secret only to a TTY, so a short-expiry +> key couldn't be minted+polled non-interactively). If a live capture ever differs, +> update the table and `await_auth_permanent()` together. + +## API-key mint output drift (create-key) + +The prerelease `apikey create-for-me -o json` does **not** put the one-time secret on +redirectable stdout: with stdout+stderr redirected (non-TTY) it returns **empty output +with exit 0** and, in that mode, may not persist a key at all. The secret is emitted to +the controlling **TTY** / as human text. This is why `cmd_create_key` captures BOTH +streams, falls back to a JWT-pattern scrape, and finally to a hidden `/dev/tty` paste — +and why the offline `key-empty` scenario (empty both streams, exit 0) is faithful. diff --git a/plugins/temporal/skills/temporal-cloud-setup/scripts/preview.sh b/plugins/temporal/skills/temporal-cloud-setup/scripts/preview.sh deleted file mode 100644 index 5b0d4b8..0000000 --- a/plugins/temporal/skills/temporal-cloud-setup/scripts/preview.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# Read-only disclosure shim for temporal-cloud-setup. -# -# This forwards verbatim to `provision.sh preview ` and nothing else. -# It exists so the agent can build a step's GATE block (the disclosure the -# user sees BEFORE approving) WITHOUT that disclosure call itself tripping an -# approval prompt: a `Bash(*provision.sh*)` permission rule matches the real, -# effectful subcommands but NOT this file's name. Disclosure must always -# precede confirmation, so the gate-builder must be promptless. -# -# All logic remains in provision.sh — this adds zero behavior. `preview` is -# side-effect-free (no network, no writes), so running it unprompted is safe. -set -euo pipefail -# Resolve our own directory with bash builtins only (no external `dirname`/`cd` -# binary), so the shim works even on a minimal PATH. -dir="${0%/*}"; [ "$dir" = "$0" ] && dir="." -dir="$(cd "$dir" && pwd)" -# Forward with the same bash interpreter running this shim, so we don't depend on -# provision.sh's `#!/usr/bin/env bash` shebang resolving on a minimal PATH. -exec "${BASH:-bash}" "$dir/provision.sh" preview "$@" \ No newline at end of file diff --git a/plugins/temporal/skills/temporal-cloud-setup/scripts/provision.sh b/plugins/temporal/skills/temporal-cloud-setup/scripts/provision.sh old mode 100644 new mode 100755 index e428d1f..b2f694f --- a/plugins/temporal/skills/temporal-cloud-setup/scripts/provision.sh +++ b/plugins/temporal/skills/temporal-cloud-setup/scripts/provision.sh @@ -7,7 +7,7 @@ # It is written to work UNCHANGED on every platform (macOS bash 3.2 / BSD # awk included). Do not reformat its punctuation, quoting, regexes, or flags. # If a prerelease flag has genuinely drifted, that is a deliberate human -# maintenance edit -- not something an agent does mid-setup. (SKILL.md rule 10.) +# maintenance edit -- not something an agent does mid-setup. # =========================================================================== # # WHY THIS EXISTS @@ -65,6 +65,50 @@ result_close() { printf '=== END ===\n'; } require_cmd() { command -v "$1" >/dev/null 2>&1; } +# ---- secret redaction (single source of truth) ------------------------------ +# redact -> filter: read stdin, write a SCRUBBED copy to stdout. Removes the two +# secret shapes this skill can surface: an `api_key = ` / `apikey: ` +# assignment (value replaced with "(redacted)") and any JWT-shaped token (eyJ... -> +# eyJ...(redacted); API keys are JWTs). Factored out of cmd_create_key / +# cmd_verify_config so EVERY place that echoes captured CLI stderr uses one scrub — +# no secret ever reaches stdout, stderr, or a log unredacted. `sed -E` + `{6,}` are +# already used elsewhere here, so this stays within the macOS bash 3.2 / BSD sed target. +redact() { + sed -E 's/([Aa][Pp][Ii][_-]?[Kk][Ee][Yy][[:space:]]*[=:]).*/\1 (redacted)/g; s/eyJ[A-Za-z0-9._-]{6,}/eyJ...(redacted)/g' +} + +# ---- per-call timeout (single source of truth) ------------------------------ +# run_bounded -> run cmd with a per-call wall-clock timeout so a +# single wedged invocation can't block a polling loop. Prefers `timeout` / `gtimeout` +# when present (they also bound the whole process group); otherwise a pure-shell +# watchdog, because macOS ships no `timeout`: background the call, poll up to +# in 1s ticks, then TERM (and, as a backstop, KILL) it. `wait` reaps the child in +# every path (no zombies) and yields the command's REAL exit status when it finished +# in time; on expiry we return RUN_BOUNDED_TIMEOUT (124, matching GNU `timeout`) so a +# caller can tell "timed out" apart from "ran and returned non-zero". The caller owns +# stdout/stderr redirection (it is inherited by the backgrounded command as-is). +RUN_BOUNDED_TIMEOUT=124 +run_bounded() { + local secs="$1"; shift + if require_cmd timeout; then timeout "$secs" "$@"; return $?; fi + if require_cmd gtimeout; then gtimeout "$secs" "$@"; return $?; fi + "$@" & + local cpid=$! waited=0 rc + while kill -0 "$cpid" 2>/dev/null; do + if [ "$waited" -ge "$secs" ]; then + kill -TERM "$cpid" 2>/dev/null + sleep 1 + kill -KILL "$cpid" 2>/dev/null + wait "$cpid" 2>/dev/null + return "$RUN_BOUNDED_TIMEOUT" + fi + sleep 1 + waited=$(( waited + 1 )) + done + wait "$cpid"; rc=$? + return "$rc" +} + # ---- config-file location (TEMPORAL_CONFIG_FILE wins, else per-OS default) --- config_path() { @@ -129,9 +173,9 @@ task_queue_for() { esac } -# ---- per-(SDK, package-manager) adaptation matrices (PE-75) ------------------ +# ---- per-(SDK, package-manager) adaptation matrices -------------------------- # Single source of truth for which managers each SDK sample ACTUALLY supports, -# derived from a Step-0 audit of the money-transfer-project-cloud-setup branches +# derived from the money-transfer-project-cloud-setup branches # (see references/sdk-cloud.md). Only python and ts ship a choice; the rest are # single-manager. We never offer a manager the sample can't honor (e.g. poetry on # the python sample, which ships no pyproject.toml). @@ -357,6 +401,25 @@ cmd_preflight() { [ -n "${TEMPORAL_NAMESPACE:-}" ] && stray+=("TEMPORAL_NAMESPACE") [ -n "${TEMPORAL_API_KEY:-}" ] && stray+=("TEMPORAL_API_KEY") + # NB: no up-front internet probe. A coarse HTTPS reachability check can't see the + # case that actually bites -- a PARTIAL block where login/whoami work (offline / + # loopback) but the gRPC Cloud API is blocked -- and it risks false-positives behind + # a captive portal / proxy. The authoritative connectivity gate is the post-login + # region pulse in cmd_regions (validates output, not exit code); a fully-offline user + # also fails loudly at install-cli / login. So we do NOT gate preflight on the network. + + # Config-dir writable: writing the client-config TOML is OUR op -- no external + # tool emits an error for us -- and today it fails LATE, after a billable API key + # is minted. A tiny create/write/remove probe catches an unwritable config dir up + # front. (The one fs check worth keeping; work-dir/disk checks are left to git/npm/ + # pip, which already fail loudly on them.) + local cfg_dir; cfg_dir="$(dirname "$cfg")" + local probe="$cfg_dir/.tcloud-write-probe.$$" + if ! ( mkdir -p "$cfg_dir" 2>/dev/null && : > "$probe" 2>/dev/null ); then + die config-dir-unwritable "Can't write to the Temporal config directory ($cfg_dir). Fix its permissions, or set TEMPORAL_CONFIG_FILE to a writable path, then re-run -- otherwise the client-config TOML can't be saved after the API key is minted." + fi + rm -f "$probe" 2>/dev/null + result_open ok result_kv os "$os" result_kv config_path "$cfg" @@ -367,7 +430,7 @@ cmd_preflight() { result_close } -# cmd_detect_tools: PE-75 local-setup adaptation (Pillar 2). For the chosen SDK, +# cmd_detect_tools: local-setup adaptation. For the chosen SDK, # detect which package managers the sample supports AND are installed, pick a # deterministic default (first available in preference order), report tool # versions, and surface discrepancies (missing runtime/manager, version-too-old) @@ -448,11 +511,48 @@ cli_version() { temporal --version 2>/dev/null | head -n1; } cmd_install_cli() { if cloud_cli_present; then - log "Temporal CLI already present — skipping install." - result_open skipped - result_kv version "$(cli_version)" - result_close - return + # BETA stopgap (PE-79): the prerelease temporal-cloud CLI has no meaningful + # version numbers yet, so we can't do a real "is it out of date?" check. For the + # beta period we ALWAYS try to pull the latest from the prerelease tap instead of + # skipping. Never yank a working install: if we can't update (Homebrew missing, + # non-macOS host, or brew errors) we warn and proceed with the CLI that's there. + # Replace this with a real version comparison once the CLI ships versions. + local up_out up_rc + case "$(uname -s)" in + Darwin) + if require_cmd brew; then + log "Temporal CLI present — updating to the latest prerelease..." + up_out="$(brew upgrade temporalio/prerelease/temporal-cloud 2>&1)"; up_rc=$? + result_open ok + # The updated/up-to-date label is best-effort (matched against brew's + # unstructured output) and NON-load-bearing: every branch here emits + # status=ok and proceeds, so a mislabel never breaks a working install. + if [ "$up_rc" -ne 0 ]; then + log "brew upgrade failed; keeping the working CLI. Output:" + log "$up_out" + result_kv update failed + elif printf '%s\n' "$up_out" | grep -qi 'upgrading'; then + result_kv update updated + else + result_kv update up-to-date + fi + result_kv version "$(cli_version)" + result_close; return + fi + log "Temporal CLI present but Homebrew not found — can't auto-update; proceeding with the installed CLI. Install Homebrew (https://brew.sh) or download temporal-cloud from https://github.com/temporalio/cloud-cli/releases/latest to update manually." + result_open ok + result_kv update skipped + result_kv reason brew-missing + result_kv version "$(cli_version)" + result_close; return ;; + *) + log "Temporal CLI present on a non-macOS host — no prerelease tap to update from; proceeding with the installed CLI. Update temporal-cloud manually from https://github.com/temporalio/cloud-cli/releases/latest." + result_open ok + result_kv update skipped + result_kv reason unsupported-os + result_kv version "$(cli_version)" + result_close; return ;; + esac fi case "$(uname -s)" in Darwin) @@ -492,14 +592,34 @@ cmd_login() { } cmd_regions() { - # Re-verify auth cheaply first. + # `whoami` is the cheap "are we even signed in" check -- it proves a CREDENTIAL is + # PRESENT, nothing more. It runs OFFLINE (cached token, no live API call), so it is + # NOT a connectivity signal: never treat a passing whoami as proof the Cloud API is + # reachable. The region fetch below is the authoritative connectivity + authz pulse. if ! temporal cloud whoami >/dev/null 2>&1; then die not-authenticated "Not signed in. Run the login step first." fi + + # ---- POST-LOGIN CONNECTIVITY PULSE (the one authoritative "can we actually reach + # the Cloud API with these creds" gate) -------------------------------------------- + # The prerelease CLI returns exit 0 even when the network call fails, so we validate + # the OUTPUT, never the exit code (exit-0 hardening -- deliberately narrowed to this + # pulse + the preflight probe; downstream steps trust the connection this establishes). + # `region list` is non-empty for ANY valid account, so an empty result is an + # unambiguous "couldn't reach the API" -- almost always a blocked/partial network + # (login + whoami succeed offline; the gRPC API is blocked). We STOP with + # cloud-unreachable and do NOT loop on re-auth: re-auth cannot fix a blocked network, + # and that misdiagnosis (empty list -> "confirm auth, re-run") is the exact bug this + # pulse closes. We reuse this single fetch for the SELECTION step below -- connectivity + # is validated HERE, in one place; selection carries no network logic and no extra call. log "Listing available Cloud regions..." - local list; list="$(temporal cloud region list 2>/dev/null)" + local err_file; err_file="$(mktemp "${TMPDIR:-/tmp}/tcloud-regions.XXXXXX")" + local list; list="$(temporal cloud region list 2>"$err_file")" + local err; err="$(cat "$err_file" 2>/dev/null)"; rm -f "$err_file" if [ -z "$list" ]; then - die regions-empty "region list returned empty; confirm auth (whoami) and CLI version." + # Empty output = failed pulse. Surface any stderr as a hint, but never DEPEND on it: + # the exit code lied, and the text may be empty or drift between prerelease builds. + die cloud-unreachable "Temporal Cloud returned no regions -- the Cloud API is unreachable with your current session. This is a network/sandbox block, NOT missing auth (login and whoami work offline). Do NOT re-run login. Check outbound network and your sandbox's egress to the Temporal Cloud gRPC API (*.tmprl.cloud), then re-run.${err:+ (CLI stderr: $err)}" fi # Region guard: flag regions whose CloudProvider renders as UNKNOWN. On some accounts # those (e.g. azure-centralus) ACCEPT a namespace create but never provision it (a @@ -638,13 +758,12 @@ await_active_namespace() { done } -# cmd_start_namespace: TRUE fire-and-forget (PE-75). Submit the create with --async +# cmd_start_namespace: TRUE fire-and-forget. Submit the create with --async # and return immediately — provisioning runs SERVER-SIDE (no local background job to # survive across tool calls, so this works identically on Claude Code/Codex/Cursor). # We GENERATE the name (namespace_name_for), so we don't need the create's output; # join later with `await-namespace --name `, which reads the handle from -# `namespace list -o jsonl` (the reason --async was once avoided — its early return -# lacked the account-id — no longer matters now that we resolve the handle from list). +# `namespace list -o jsonl`. cmd_start_namespace() { local sdk="" region="" while [ $# -gt 0 ]; do @@ -742,6 +861,20 @@ cmd_create_namespace() { cmd_await_namespace --name "$name" } +# key_quota_stderr -> 0 (true) if the captured stderr reads like an API-key +# CAP/quota rejection. Broad on the quota words (limit/maximum/quota/exceeded/too +# many) but ANCHORED to key/apikey in proximity, so an unrelated 'limit' elsewhere +# in CLI chatter doesn't misfire. Case-insensitive; either word order. +key_quota_stderr() { + grep -qiE '(api[ _-]?key|key)[^\n]*(limit|maximum|quota|exceeded|too many)|(limit|maximum|quota|exceeded|too many)[^\n]*(api[ _-]?key|key)' "$1" 2>/dev/null +} + +# die_key_limit_reached -> the single, actionable message for the API-key cap. +# Used by both create-key failure paths (non-zero exit and exit-0-with-error drift). +die_key_limit_reached() { + die key-limit-reached "API-key limit reached for this account — the mint was rejected at the cap, not an auth or output problem. Delete stale keys, then re-run create-key: list them with 'temporal cloud apikey list' and remove old money-transfer-cloud-setup-* keys with 'temporal cloud apikey delete --key-id '." +} + cmd_create_key() { # Unique display name per run: `apikey create-for-me` ERRORS when a key matching the spec # (same display name) already exists and --idempotent isn't set — and --idempotent would @@ -787,7 +920,10 @@ cmd_create_key() { --auto-confirm \ -o json > "$tmp" 2>"$tmperr"; then # Show a redacted tail for context — never the token. - sed -E 's/eyJ[A-Za-z0-9._-]{6,}/eyJ...(redacted)/g' "$tmperr" 2>/dev/null | tail -n 5 >&2 + redact < "$tmperr" 2>/dev/null | tail -n 5 >&2 + # Account at the API-key cap? The mint is rejected at create time with quota + # language; surface that as a distinct, actionable code rather than a generic fail. + if key_quota_stderr "$tmperr"; then die_key_limit_reached; fi die key-create-failed "apikey create-for-me exited non-zero; re-check auth (whoami/login). See the redacted output above." fi if [ ! -s "$tmp" ] && [ ! -s "$tmperr" ]; then @@ -822,6 +958,13 @@ cmd_create_key() { fi fi if [ -z "$token" ]; then + # Exit-0-with-error drift: some prerelease builds print a cap/quota rejection to + # stderr yet still exit 0, so there's no token to capture. Classify that here (we + # only reach this with no token) as key-limit-reached rather than manual-key-needed. + if key_quota_stderr "$tmperr"; then + redact < "$tmperr" 2>/dev/null | tail -n 5 >&2 + die_key_limit_reached + fi die manual-key-needed "could not capture the key from the CLI output (checked stdout+stderr, JSON + token pattern) and no terminal to paste into — have the user paste it, then re-run create-key from a context with a terminal." fi @@ -903,8 +1046,7 @@ cmd_verify_config() { if ! out="$(temporal --profile cloud-setup config list 2>&1)"; then die profile-missing "could not read the cloud-setup profile; was create-key run?" fi - printf '%s\n' "$out" \ - | sed -E 's/([Aa][Pp][Ii][_-]?[Kk][Ee][Yy][[:space:]]*[=:]).*/\1 (redacted)/g; s/eyJ[A-Za-z0-9._-]{6,}/eyJ...(redacted)/g' >&2 + printf '%s\n' "$out" | redact >&2 result_open ok; result_kv profile cloud-setup; result_close } @@ -914,19 +1056,81 @@ cmd_verify_config() { # `Request unauthorized` and crashes. Poll the cheapest authorized data-plane call # (`workflow list`, exit 0 == accepted) until it succeeds, bounded. Foreground, # synchronous — no background, no parallelism. +# await_auth_permanent -> 0 (true) if the poll's stderr is a HIGH-CONFIDENCE +# PERMANENT key failure that will NEVER clear by waiting. Deliberately NARROW: it +# requires a permanent qualifier (expired / invalid / revoked / not found / disabled / +# malformed) ANCHORED to jwt/key/token/credential context — mirroring key_quota_stderr's +# proximity anchoring. +# +# Anchor set includes `jwt` because the Temporal Cloud API gateway (Envoy) returns the +# auth failure as a gRPC status whose desc is a JWT-filter message, NOT "api key" text. +# Real captures against the prerelease CLI (`workflow list`): +# * no/empty key -> `Unauthenticated desc = Jwt is missing` +# * bad/wrong key -> `Unauthenticated desc = Jwt issuer is not configured` +# * expired key -> `Unauthenticated desc = Jwt is expired` (Envoy default; +# e.g. a ~25h key polled next-day) +# See references/unified-cli.md. Only `expired` is treated as unambiguously permanent +# here: a valid key that is merely PROPAGATING has a future exp, so it can never emit +# "Jwt is expired" — the safety bias holds. A bare transient `Jwt is missing` / +# `issuer is not configured` / `Request unauthorized` / `permission denied` (which can +# appear during propagation) must NOT match, so the poll keeps waiting instead of +# wrong-fast-failing. `expired` stays anchored so a TLS/certificate "expired" (no +# jwt/key/token context) never masquerades as a key failure. +await_auth_permanent() { + grep -qiE '(jwt|api[ _-]?key|token|credential)[^\n]*(expired|invalid|revoked|not found|disabled|malformed)|(expired|invalid|revoked|malformed)[^\n]*(jwt|api[ _-]?key|token|credential)' "$1" 2>/dev/null +} + cmd_await_auth() { - local max="${AUTH_READY_MAX_SECS:-90}" interval=5 waited=0 + local max="${AUTH_READY_MAX_SECS:-90}" interval="${AUTH_POLL_INTERVAL_SECS:-5}" waited=0 + # Per-call timeout so a single wedged `workflow list` can't block the whole loop. + # `waited` accrues real wall-clock (poll time + sleep), so a slow/hung endpoint + # still honors ~max seconds of total budget rather than max*call_to. + local call_to="${AUTH_POLL_CALL_TIMEOUT:-15}" log "Waiting for the API key to be accepted before starting the Worker (auth readiness)..." + # Capture the poll's stderr (never stdout) to a locked temp file so a permanent auth + # failure is diagnosable instead of collapsing into a generic timeout. EXIT trap so + # the file is removed on success (return) AND on any die (exit). + umask 077 + local tmperr; tmperr="$(mktemp "${TMPDIR:-/tmp}/tcloud-auth.XXXXXX")"; chmod 600 "$tmperr" + # shellcheck disable=SC2064 + trap "rm -f '$tmperr'" EXIT + local last_err="" rc t0 t1 while :; do - if temporal --profile cloud-setup workflow list --limit 1 >/dev/null 2>&1; then + : > "$tmperr" + # exit 0 == the key is accepted. The poll's own exit code still gates success; + # stderr is only ever used to CLASSIFY a failure (and, redacted, to report it). + t0="$(date +%s)" + run_bounded "$call_to" temporal --profile cloud-setup workflow list --limit 1 >/dev/null 2>"$tmperr" + rc=$? + if [ "$rc" -eq 0 ]; then + rm -f "$tmperr"; trap - EXIT result_open ok; result_kv auth_ready true; result_kv waited_secs "$waited"; result_close return 0 fi + # Keep the most recent NON-timeout CLI stderr as the diagnostic tail (a per-call + # timeout produces no CLI message; don't let it erase the last real one). Redacted + # at capture, so nothing secret is ever held or surfaced. + if [ "$rc" -ne "$RUN_BOUNDED_TIMEOUT" ] && [ -s "$tmperr" ]; then + last_err="$(redact < "$tmperr" | grep -v '^[[:space:]]*$' | tail -n 1)" + fi + # High-confidence PERMANENT key failure -> fast-fail; a dead/expired/revoked key + # will NEVER clear by waiting, so don't spin the full bound. The classifier is + # deliberately narrow (see await_auth_permanent): a bare transient `unauthorized` + # /`permission denied` during propagation stays transient and keeps polling — a + # wrong fast-fail is worse than a bounded wait. NOTE: if a prerelease exited 0 on + # an auth *failure* the poll would false-pass above before we get here; that's a + # connectivity-pulse concern, handled separately. + if [ "$rc" -ne "$RUN_BOUNDED_TIMEOUT" ] && await_auth_permanent "$tmperr"; then + die key-expired "API key rejected as expired/invalid — it will not clear by waiting (keys auto-expire in ~25h, so a next-day re-test hits a dead key). Re-run create-key to mint a fresh key (it overwrites the [profile.cloud-setup] block). If you are at the API-key cap, delete stale keys first (temporal cloud apikey list; temporal cloud apikey delete --key-id ), then re-run create-key.${last_err:+ (CLI stderr: $last_err)}" + fi if [ "$waited" -ge "$max" ]; then - die auth-timeout "API key still not accepted after ${max}s. A just-minted key/namespace can lag — wait and re-run await-auth; if it never clears, re-run create-key. Do NOT switch endpoints or edit the profile." + die auth-timeout "API key still not accepted after ${max}s. A just-minted key/namespace can lag — wait and re-run await-auth; if it never clears, re-run create-key. Do NOT switch endpoints or edit the profile.${last_err:+ (last CLI stderr: $last_err)}" fi sleep "$interval" - waited=$(( waited + interval )) + # Accrue real elapsed (poll + sleep) so the ~max budget is wall-clock, not + # per-interval: a slow/hung endpoint would otherwise take max*call_to seconds. + t1="$(date +%s)" + waited=$(( waited + (t1 - t0) )) done } @@ -1022,7 +1226,7 @@ stop_group() { return 0 } -# cmd_run_workflow: THE single-call worker+starter path (PE-70). The Worker is a +# cmd_run_workflow: THE single-call worker+starter path. The Worker is a # long-running process that must stay alive WHILE the starter triggers a Workflow — # but background jobs don't reliably survive across tool calls on Codex/Cursor. So, # like provision-and-scaffold, this owns the whole dance inside ONE synchronous call: @@ -1311,7 +1515,7 @@ install_deps_for() { ) || log " [warn] dependency install reported a problem (continuing; can be retried)" } -# cmd_install_deps: standalone manager-parameterized install (PE-75). Lets the +# cmd_install_deps: standalone manager-parameterized install. Lets the # agent install (or re-install) deps with an explicitly chosen/overridden manager. # Defaults --manager to the SDK's deterministic default when omitted. Dies with # manager-not-found if the chosen manager's binary isn't on PATH (a clear, @@ -1344,7 +1548,7 @@ cmd_install_deps() { } # cmd_scaffold: set up the app only — clone the cloud-ready sample + install deps, -# NO namespace work (PE-75). Pairs with start-namespace/await-namespace so the app +# NO namespace work. Pairs with start-namespace/await-namespace so the app # setup is its own gated step that overlaps the server-side namespace provisioning. # Validates the manager fail-fast (manager-not-found / unsupported-manager) BEFORE # cloning, same as provision-and-scaffold. @@ -1383,7 +1587,7 @@ cmd_scaffold() { result_close } -# cmd_provision_and_scaffold: THE single-call parallel path (PE-68). Runs as one +# cmd_provision_and_scaffold: THE single-call parallel path. Runs as one # ordinary SYNCHRONOUS foreground command; internally backgrounds the (synchronous, # never --async) namespace create and overlaps it with clone + deps, then `wait`s # and joins. Because the background job lives and dies inside this one invocation, @@ -1513,11 +1717,11 @@ cmd_repair_config() { result_close } -# emit_gate [ ]... (PE-75 determinism lever) +# emit_gate [ ]... (determinism lever) # Prints a READY-TO-RENDER gate block on stdout, AFTER the RESULT block, delimited # by `=== GATE ===` / `=== END GATE ===`. The agent renders the text between the # markers VERBATIM (then appends the numbered choices) instead of assembling the -# fenced block from prose rules — which is where the formatting kept breaking +# fenced block from prose rules — which is error-prone to hand-assemble # (dropped fence, comment-only, glued rules). ASCII only; comment goes ABOVE its # command (green-comment style). Same idea as pinning the CLI flags: move the exact # format into the script so the model only has to print it. @@ -1540,7 +1744,7 @@ emit_gate() { printf '=== END GATE ===\n' } -# cmd_preview: PE-75 dry-run. Resolve and PRINT the concrete command(s) a +# cmd_preview: dry-run. Resolve and PRINT the concrete command(s) a # subcommand would run, plus the resolved user-facing parameters, with NO side # effects (no clone, no temporal calls, no installs) — this powers the per-command # confirm gate and the "Edit" path in SKILL.md. Deterministic and offline: the @@ -1687,16 +1891,21 @@ cmd_preview() { "mint the key + write the cloud-setup profile to temporal.toml (token captured to a 0600 file, never printed)" \ "temporal cloud apikey create-for-me --display-name money-transfer-cloud-setup --expiry-duration 25h --auto-confirm -o json" ;; install-cli) - # Presence-aware (read-only): if the CLI is already here, disclose a no-op - # so the gate matches what cmd_install_cli will do (skip, no install/update). + # Presence-aware (read-only): disclose what cmd_install_cli will do. BETA + # stopgap (PE-79): a present CLI is updated to the latest prerelease (not + # skipped) while the CLI has no real versions; an absent CLI is installed. if temporal cloud version >/dev/null 2>&1; then + local uc_cmd uc_note + case "$(uname -s)" in + Darwin) uc_cmd="brew upgrade temporalio/prerelease/temporal-cloud"; uc_note="update the Temporal CLI to the latest prerelease via Homebrew (adds/updates software)" ;; + *) uc_cmd="# already installed; update temporal-cloud manually from https://github.com/temporalio/cloud-cli/releases/latest"; uc_note="the Temporal CLI is already installed; on this OS, update temporal-cloud manually from the releases page" ;; + esac result_open ok result_kv preview install-cli result_kv cli_present true - result_kv cmd_1 "# Temporal CLI already installed - this step is skipped (no install, no update)" + result_kv cmd_1 "$uc_cmd" result_close - emit_gate "Temporal CLI already installed - nothing to do" \ - "the Temporal CLI is already on your machine, so this step is skipped (no install, no update)" "temporal cloud version" + emit_gate "Updating the Temporal CLI to the latest" "$uc_note" "$uc_cmd" else local ic_cmd ic_note case "$(uname -s)" in @@ -1773,12 +1982,12 @@ cmd_preview() { esac } -# announce_gate [args...] (PE-75 — deterministic disclosure floor) +# announce_gate [args...] (deterministic disclosure floor) # Before an effectful subcommand acts, print ITS gate to STDERR so the tool block # ALWAYS records what the command runs — even when the agent skips rendering the -# chat-side gate from §Gate templates (the Cursor "sometimes doesn't disclose" -# failure). Reuses cmd_preview's single-source gate text, so the disclosure can -# never drift from the real command; strips the machine markers and frames it as +# chat-side gate from §Gate templates. Reuses cmd_preview's single-source gate +# text, so the disclosure can never drift from the real command; strips the +# machine markers and frames it as # plain human disclosure. Never blocks the real work: any preview failure (an arg # preview doesn't know, an unsupported sub) is swallowed and the command proceeds. # Opt out with TCLOUD_DISCLOSE=0 (the test harness sets this to keep stderr clean). @@ -1821,10 +2030,10 @@ Usage: provision.sh [args] Commands: preflight [--sdk S] Environment checks + config path + stray TEMPORAL_* vars detect-tools --sdk S Detect supported+installed package managers, pick a default, - report versions, surface discrepancies (PE-75 adaptation) + report versions, surface discrepancies preview [args] Dry-run: print the concrete command(s) a subcommand would run (no side effects) — powers the per-command confirm/Edit gate - install-cli Install the prerelease temporal-cloud CLI (skips if present) + install-cli Install the prerelease temporal-cloud CLI (updates it to latest if already present) login temporal cloud login (browser) + whoami regions List Cloud regions (raw list to stderr) start-namespace --sdk S --region R Fire-and-forget: submit `namespace create --async` (server-side); emits namespace_name @@ -1837,7 +2046,7 @@ Commands: run-workflow --sdk S --dir D [--demo-failure transient] [--max-secs N] Single synchronous call: start Worker (bg) -> wait until polling -> run starter -> stop Worker; emits workflow_status, workflow_id, run_id clone --sdk S [--dir D] Clone the cloud-ready sample for the SDK - install-deps --sdk S --dir D [--manager M] Install sample deps with a chosen/default package manager (PE-75) + install-deps --sdk S --dir D [--manager M] Install sample deps with a chosen/default package manager provision-and-scaffold --sdk S --region R [--dir D] [--manager M] Single synchronous call: namespace create (bg) || clone+deps; emits namespace_handle, address, repo_path, manager repair-config Strip duplicate/old [profile.cloud-setup] blocks (keeps default)