# CP9 refactor plan: session context naming and Auth0 secret reads

Date: 2026-08-18
Verified at CP9 `548220412541d553105dce586303f6f0a0893ffd` (confirmed `origin/main` via `git ls-remote`).
Scope: CP9 repo only.

This plan follows the research in
`docs/20260818-research-cp9-auth0-secret-ownership.md`. That research's
account-ownership conclusion (CP9 sessions read the target account, not 0414)
is independently confirmed below. What follows is about *structure*, not
about which account is read.

## Root cause

Not naming alone. One value, four names, and four hand-rolled copies of the
same read.

### 1. One session-context value, four names

`personas/dev-full/runtime/example.Tiltfile:6` reads the real input:

```
ctx_path = os.getenv("CP9_SESSION_CONTEXT", "")
```

It then re-exports that *same* `ctx_path` to child scripts under three
different names (`example.Tiltfile:894, 899, 2061, 2327, ...`):

- `CP9_STARTER_LOCAL_GLOBAL_SESSION_CONTEXT`
- `CP9_REGIONAL_SERVER_SESSION_CONTEXT`
- `CP9_ACCOUNT_SESSION_CONTEXT`

Same file path, three names. Nothing distinguishes them.

### 2. Error messages name a variable the script does not read

`apply-kustomize-images.sh` reads the three aliases, but raises errors that
name `CP9_SESSION_CONTEXT`:

- reads: `:798, :1055, :1165, :1501, :1730, :1791`
- but errors say `CP9_SESSION_CONTEXT.<field>`: `:535, :700, :1542, :1550, :1561, :1574, :1816, :1854, :1891, :1952`

33 such strings across 4 scripts (`apply-kustomize-images.sh`,
`apply-dataplane-manager-runtime.sh`, `bootstrap-essential-v2-shared-pool.sh`,
`apply-aws-creds-secret.sh`).

This is the concrete mechanism by which the code misleads: an agent that hits
the error and exports `CP9_SESSION_CONTEXT` changes nothing, because the
script reads an alias.

### 3. Two different scripts share one filename, with different contracts

- `personas/dev-full/runtime/scripts/apply-kustomize-images.sh` (2041 lines) — reads the three aliases. Entry point: `example.Tiltfile:645`.
- `personas/dev-full/starter/apply-kustomize-images.sh` (518 lines) — reads `CP9_SESSION_CONTEXT` directly (`:99`). Entry point: `internal/cmd/starter_cmd.go:4938` (Starter *remote* workloads).

Same name, different variable contract, different entry point. `starter/` also
has its own `build-local-image.sh` and `build-remote-image.sh` paralleling
`build/`.

### 4. The same secret read is implemented four times, with divergent semantics

Each site independently parses the session context, resolves
`account_config_secret.name` from Terraform outputs, and calls
`get-secret-value`:

| Site | Function | Lines |
|---|---|---|
| Starter serverless | `patch_starter_local_global_runtime_config` | 797-863 |
| Dedicated proxy | `patch_dedicated_proxy_auth_secret` | 1049-1108 |
| Premium account | `patch_account_secret_store_config` | 1160-1477 |
| Dedicated Dataflow | `patch_account_contract_before_apply` | 1890-1900 |

They disagree on the same contract:

- **Region default:** Starter (`:834`) and Premium (`:1210`) both fall back to `us-west-2`; Dedicated proxy treats a missing region as fatal (`:1085-1086`). Dedicated is the outlier.
- **Missing `outputs_path`:** Starter tolerates it (`:825-828`) and only fails later on `secret_id` (`:839`); Dedicated proxy raises immediately (`:1071-1072`).
- **Authentication:** Starter and Dedicated use `aws --profile "$source_profile"` (`:853, :1104`); Premium instead sources exported credentials into the environment (`:1269-1301`).

Supporting counts in this one file: 40 `python3` process spawns, 21
`outputs_path` references, 12 `CP9_AWS_RUNTIME_IDENTITY` branch sites.

### 5. Two conditionals whose branches are byte-identical

`:1307-1317` (`get-secret-value`) and `:1467-1474` (`put-secret-value`) branch
on `CP9_AWS_RUNTIME_IDENTITY`, but the `then` and `else` bodies are identical.
Verified by diffing the two bodies: the only differing lines are `if` and `fi`.
The conditional has no effect.

### 6. Dead configuration and an asymmetry

`:1229` allows `ctx.account_management_m2m_secret` to override the M2M secret
name. Nothing anywhere sets that key — only the unrelated
`account_management_m2m_secret_version` exists. The fallback always wins.
Meanwhile the sibling secret name is hardcoded in shell at `:1261`
(`serverless_secret_key=cp9/dev/auth0/serverless-service`) with no override at
all. One secret name is fake-configurable, the other is fixed.

### 7. Terraform description contradicts the consumer

`terraform/cp9-developer/auth0.tf:6` describes the bundle as
"Shared dev Auth0 credentials for CP9 **Starter** serverless-service", but the
**Premium** account path reads it first (`:1261, :1311`) and Starter only reads
the *derived* session secret afterwards. The name teaches the wrong owner.

## Regression gate

`tools/verify-runtime-account-renders.sh` renders both scripts against a
synthetic account with `CP9_RENDER_ONLY=true` and compares against 176
SHA-256 baselines in `tools/render-baseline.txt`. It already exercises both
`runtime/scripts/` (`:75, :138, :149, :185, :336`) and `starter/`
(`:359, :384`).

Every phase below is behavior-preserving, so the gate is: **all 176 baseline
hashes unchanged**. If a hash moves, the change was not behavior-preserving.

## Plan

Ordered cheapest-and-safest first. Phases 1-3 are independent and each is
separately revertable.

### Phase 1 — delete what provably does nothing (no behavior change)

1. Collapse the two identical conditionals at `:1307` and `:1467` to their single body (-8 lines).
2. Remove the dead `account_management_m2m_secret` override at `:1229`; resolve both Auth0 secret names the same way in one place.
3. Fix `auth0.tf:6` description to name the actual first consumer.

Gate: 176 hashes unchanged.

### Phase 2 — one name for one value

1. Make the scripts read `CP9_SESSION_CONTEXT`, keeping the three alias names as a temporary fallback so nothing breaks mid-migration.
2. Stop the Tiltfile re-exporting three names; pass `CP9_SESSION_CONTEXT`.
3. Correct the 33 error strings to name the variable actually read.
4. Delete the alias fallback once the tree is green.

Gate: 176 hashes unchanged. This phase is what stops the misleading-error
problem.

### Phase 3 — one reader for one contract

Extract a single helper that takes the session context and returns
`(profile, region, secret_id)`, and one wrapper that performs the
authenticated `get-secret-value`. Route all four sites through it.

This forces one decision on each divergence in §4 — region default, missing
`outputs_path`, and auth mechanism. Those decisions should be made
deliberately and written down, not left as three accidents.

Gate: 176 hashes unchanged.

### Phase 4 — the two `apply-kustomize-images.sh` (needs a decision, not yet scoped)

Whether `starter/` should merge into `runtime/scripts/`, or be renamed to
declare that it is the Starter *remote* path, depends on whether the remote
Starter path is still a supported product surface. That is a product call, not
a cleanup call. Recommendation: **rename first** (e.g.
`starter/apply-remote-kustomize-images.sh`) so the filename collision stops
misleading readers, and defer any merge until the surface question is settled.

Do not attempt Phase 4 together with Phases 1-3.

## What this plan deliberately does not do

- No change to which account any secret is read from. The 804/0414 ownership conclusion stands as researched.
- No change to the Terraform secret *names* themselves. Renaming a live secret is a migration, not a refactor, and would need its own plan.
- No new abstraction beyond the single reader in Phase 3.

---

## Execution status (2026-08-18)

### Important limit on what the gate proves

`CP9_RENDER_ONLY=true` exits `apply-kustomize-images.sh` at line 1994, **before**
`patch_dedicated_proxy_auth_secret`, `patch_starter_local_global_runtime_config`
and `patch_account_secret_store_config` run (lines 2004-2008).

So the 176-hash render gate proves **the rendered manifests are unchanged**. It
does **not** execute the Auth0 secret-read paths at all. Any claim that those
functions still work must come from a live session, not from this gate.

### P1 — DONE. Safe by construction, and since verified live (see P3 verification).

Both removals are provable without running the code:

- The two conditionals had **byte-identical** `then`/`else` bodies, so collapsing them is a no-op whichever branch would have been taken.
- `ctx.account_management_m2m_secret` is set nowhere in the repo, so the `or` fallback always won; it was replaced by that exact literal.

Gates re-run and green (rendering unaffected, as expected).

### P2 — DONE. One name.

The three aliases are gone; every reader and writer now uses `CP9_SESSION_CONTEXT`.
Verified that **every** assignment of all three aliases came from the same source
(`ctx_path` / `$context_file` / `contextPath`) before collapsing, and that zero
stale references remain.

Side effect worth noting: the 33 error strings that named `CP9_SESSION_CONTEXT`
are now **correct without being edited**. The messages were right all along; the
variable names were wrong.

### P4 — rename DONE (merge still deferred)

`personas/dev-full/starter/apply-kustomize-images.sh` →
`apply-remote-kustomize-images.sh` (`git mv`, history preserved), following the
sibling `*-remote-*` convention. All callers updated: `internal/cmd/starter_cmd.go`,
`tools/verify-runtime-account-renders.sh`, `personas/dev-full/starter/manifest.yaml`
(both the assets list and the `contains:` consumer assertion), and two
`skills/get-started/` files. No stale references remain.

Whether `starter/` should eventually merge into `runtime/scripts/` is still the
open product question and was not touched.

### P3 — DONE (extraction only), verified on a live session

`fetch_session_secret_string()` replaces the duplicated "read meta JSON → fetch
secret string" in the Starter serverless and Dedicated proxy paths. Premium is
deliberately excluded: its credential handling genuinely differs (it sources
exported credentials rather than passing `--profile`). `python3` spawns in this
script drop from 40 to 34, and `python3 -c` one-liners from 7 to 1.

**Per-site error semantics are preserved exactly.** The three divergences below
are still divergent, on purpose:

- region: default `us-west-2` (Starter, Premium) vs fatal (Dedicated)
- missing `outputs_path`: tolerated (Starter) vs fatal (Dedicated)
- auth: `--profile` (Starter, Dedicated) vs sourced env credentials (Premium)

Unifying those changes behaviour when something is *already* misconfigured, and
nothing tests it. That decision is still open and should be made deliberately.

#### Verification (Grace, live Dev Full session, 2026-08-18)

Method: a single session, applied twice — A at `5482204`, then the same session
switched in place to B at `3546759`. Two traps were avoided by design:

- Two independent sessions would have produced false positives, because session name, `account_config_secret` name, session CMK and session PCA are unique per run and these objects mix session-scoped with shared-scoped values.
- These objects are patched **in place**, so if B never re-executed the patch functions they would simply retain A's values and the comparison would "pass" while proving nothing. When the expected result is "identical", identity has no discriminating power — so execution evidence was required *before* comparison evidence.

A resumed session does not sync the checkout, so Tilt did not re-run on its own;
B was triggered explicitly per overlay. The exact overlays matter, because each
patch function is an exact `case` match on `kustomize_path` and silently
`return 0`s otherwise:

- Premium → `envs/dev-full/account-svc-config`
- Starter → `envs/dev-full/serverless-service` (**not** `serverless-global-service`, which enters the same function but leaves `patch_serverless_auth0` false)
- Dedicated → `envs/dev-full/dedicated-cluster-control-server` (**not** `dedicated-cluster-controller` or `dedicated-cluster-control-server-support`, both of which also exist)

Result: remote logs show `CP9_SESSION_CONTEXT` being passed and each path
executing; A/B final Auth0 fields and the Premium payload content are equal.

This one round covers three separate claims:

- **P3** — the extracted helper produces identical field values.
- **P2** — A ran on the three alias variables, B on the single `CP9_SESSION_CONTEXT`. A mis-wired rename would have made all three paths fail for lack of context, so this is end-to-end proof the rename is correct.
- **P1** — both collapsed conditionals and the removed dead config live inside `patch_account_secret_store_config`, which executed in the B round. P1 is therefore no longer "safe by construction" only; it has now actually run.
