From 05b2158beaaacbd768a4484d0d3b566a213aa201 Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Thu, 13 Aug 2026 10:37:16 +0800 Subject: [PATCH 1/2] docs(adr): Runtime Identity & Context panel (stub, Proposed) Authored by Orca (ecs-claude). Read-only RuntimeContext projection + Studio panel surfacing the effective principal/account/region per Instance/Fleet; observability only, no authz change (ADR-2 deferred authz stands). Motivated by the deploy_list static-default-profile fallback incident. Co-Authored-By: Claude Opus 4.8 --- docs/adr/runtime-identity-context.md | 105 +++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/adr/runtime-identity-context.md diff --git a/docs/adr/runtime-identity-context.md b/docs/adr/runtime-identity-context.md new file mode 100644 index 0000000..f3b9605 --- /dev/null +++ b/docs/adr/runtime-identity-context.md @@ -0,0 +1,105 @@ +# ADR: Runtime Identity & Context panel — surface the effective principal per Instance + +- **Status:** Proposed (stub) +- **Date:** 2026-08-13 +- **Author:** Orca (ecs-claude) +- **Builds on:** [deployment-control-plane](./deployment-control-plane.md) (ADR-2, read model), [desktop-core-sidecar](./desktop-core-sidecar.md) (credentials resolve in the sidecar from the standard chain) +- **Related:** desktop console (ADR-3), fleet-store (#18) + +> **Y-statement.** In the context of Studio managing multiple Fleets whose +> Instances may run under different credentials, accounts/projects and runtimes, +> facing the problem that the *effective* identity a driver resolves is invisible +> to the operator — it took a manual `sts get-caller-identity` to discover a +> deploy call had silently fallen back to a static personal profile instead of +> the intended task role — we decided to add a **read-only Runtime Context +> projection to the Status model plus a Studio panel that renders it per +> Instance/Fleet**, so "who am I actually acting as, and against what +> account/context?" is answerable at a glance, **accepting that this is +> observability only — it introduces no per-caller authz** (still deferred per +> ADR-2). + +--- + +## 1. Context & Problem + +Motivating incident (2026-08-13): `deploy_list` returned `AccessDenied` on +`ecs:ListServices`. The root cause was **not** a missing permission — the SDK +credential chain resolved a static `[default]` profile (an IAM *user* in a +different account and region) **before** the container-credentials (task-role) +provider, so the call executed as the wrong principal entirely. Nothing in the +tooling surfaced the effective identity; it took a manual +`aws sts get-caller-identity` to see it. + +Studio's north star is managing **multiple Fleets** across runtimes, **different +AWS roles / accounts / projects**, and later **different k8s contexts**. As that +fans out, "which credential/context is this Instance (or my current deploy call) +actually bound to?" becomes a first-order operability question. Today it is +implicit and easy to get silently wrong. + +Per ADR-2 credential resolution is deferred to "the AWS-credential ceiling", and +(ADR-5) resolves lazily inside the sidecar from the standard chain. This ADR does +**not** change that model — it makes the *result* of that resolution observable. + +## 2. Decision + +Add a generic, read-only **`RuntimeContext`** projection to the read model, and a +Studio **Identity & Context panel** that renders it. + +`RuntimeContext` (observed, per Instance; also foldable per Fleet) — generic +fields; vendor specifics stay inside the `RuntimeDriver`: + +| field | meaning | AWS driver | k8s driver (later) | +|---|---|---|---| +| `principal` | the effective acting identity | STS caller ARN (role vs user) | context user / ServiceAccount | +| `scope` | the account/project boundary | account id | cluster / namespace | +| `location` | region / zone | region | context region | +| `source` | where the credential came from | container-creds (task role) / named profile / static keys / env | kubeconfig context / in-cluster SA | +| `verified_at` | when it was last resolved | STS call ts | `auth whoami` ts | + +- **Read-only.** `RuntimeContext` is Status, never Spec — it is *observed*, like + `phase` (ADR-2 §4). It is emitted by the driver, the only place the vendor + calls (STS / `kubectl auth whoami`) live. +- **Expected-vs-actual flag.** When an intended identity binding is declared + (e.g. an expected role/principal for the Deployment), the panel compares it to + the resolved `principal` and raises a **non-blocking `IdentityMismatch`** + Condition — a warning, not a gate. Today's incident would have tripped it on + sight. This stays on the read side: **no enforcement**, consistent with ADR-2's + deferred authz. +- **Panel.** A Studio surface (desktop skin, and any front-end over the same MCP + read model) that lists, per Fleet → Instance, the `RuntimeContext` and any + `IdentityMismatch`. The Fleet-level view folds distinct contexts, so "this + Fleet spans 2 accounts" is visible at a glance. + +## 3. Scope (now / later) + +- **Now:** the AWS driver populates `RuntimeContext` (STS caller identity + + resolved `source`); the read model carries it; the desktop panel renders it per + Instance/Fleet; and the `IdentityMismatch` warning fires when an expected + principal is declared. +- **Later:** k8s driver mapping (context / SA / namespace); richer `source` + provenance; and surfacing the **deploy-call's own** resolved identity (the + caller's principal, not just the managed Instances') so a mis-resolved + CLI/sidecar is caught *before* it acts — the exact preflight that would have + pre-empted the motivating incident. + +## 4. Consequences + +- ✅ "Who am I acting as, against what account?" is answerable at a glance; + silent credential fallback (today's bug) becomes visible, ideally flagged. +- ✅ Stays within ADR-2's generic vocabulary and deferred-authz stance — pure + observability, with vendor terms scoped to the driver. +- ⚠️ The driver must make a live identity call (STS / `whoami`); cache it with a + `verified_at`, and treat failure as an `Unknown` context rather than a hard + error. +- ⚠️ `RuntimeContext` surfaces account ids / ARNs in the UI — non-secret, but the + panel must **never** render access keys or tokens. + +## 5. Open questions + +- Does "expected identity" belong in Spec (a new `identity.expectedPrincipal`), + or is it derived from Fleet/project config? (Interacts with fleet-store #18.) +- Should the caller's **own** resolved identity be a standing panel element + (deploy-call preflight), or only the managed Instances'? +- Granularity of `source` provenance without leaking secrets. +- Per-caller authz stays deferred (ADR-2) — this ADR deliberately does not touch + it. From 5593ce6e2fd6a73f472c909935e7dccfeb39c6af Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Thu, 13 Aug 2026 10:39:40 +0800 Subject: [PATCH 2/2] docs(adr): broaden to per-Fleet managing identity (binding + switch + verify) Authored by Orca (ecs-claude). Adds FleetBinding config (Fleet -> managing context) and active context-switch on top of the read-only RuntimeContext panel, forming a declare->switch->observe->reconcile loop. Still operator credential selection, not per-caller authz (ADR-2 stands). Co-Authored-By: Claude Opus 4.8 --- docs/adr/runtime-identity-context.md | 163 ++++++++++++++++----------- 1 file changed, 95 insertions(+), 68 deletions(-) diff --git a/docs/adr/runtime-identity-context.md b/docs/adr/runtime-identity-context.md index f3b9605..b058ebb 100644 --- a/docs/adr/runtime-identity-context.md +++ b/docs/adr/runtime-identity-context.md @@ -1,4 +1,4 @@ -# ADR: Runtime Identity & Context panel — surface the effective principal per Instance +# ADR: Per-Fleet managing identity — binding config, context switch, and verification - **Status:** Proposed (stub) - **Date:** 2026-08-13 @@ -6,17 +6,18 @@ - **Builds on:** [deployment-control-plane](./deployment-control-plane.md) (ADR-2, read model), [desktop-core-sidecar](./desktop-core-sidecar.md) (credentials resolve in the sidecar from the standard chain) - **Related:** desktop console (ADR-3), fleet-store (#18) -> **Y-statement.** In the context of Studio managing multiple Fleets whose -> Instances may run under different credentials, accounts/projects and runtimes, -> facing the problem that the *effective* identity a driver resolves is invisible -> to the operator — it took a manual `sts get-caller-identity` to discover a -> deploy call had silently fallen back to a static personal profile instead of -> the intended task role — we decided to add a **read-only Runtime Context -> projection to the Status model plus a Studio panel that renders it per -> Instance/Fleet**, so "who am I actually acting as, and against what -> account/context?" is answerable at a glance, **accepting that this is -> observability only — it introduces no per-caller authz** (still deferred per -> ADR-2). +> **Y-statement.** In the context of Studio managing multiple Fleets that may +> each live under a different AWS account / role / project (and later a different +> k8s context), facing the problem that the *managing* credential is ambient and +> invisible — so a deploy call can silently run under the wrong principal (a +> static `[default]` profile shadowed the intended task role, discovered only via +> a manual `sts get-caller-identity`) — we decided to make the per-Fleet managing +> identity **explicit, switchable, and verified**: a **FleetBinding config** +> (Fleet → managing context), an **active context switch** so selecting a Fleet +> binds subsequent calls to that Fleet's credential, and a **read-only +> RuntimeContext panel + IdentityMismatch flag** that confirms the effective +> identity matches the declared binding — **accepting that this is operator-side +> credential *selection*, not per-caller authz** (authz still deferred per ADR-2). --- @@ -26,80 +27,106 @@ Motivating incident (2026-08-13): `deploy_list` returned `AccessDenied` on `ecs:ListServices`. The root cause was **not** a missing permission — the SDK credential chain resolved a static `[default]` profile (an IAM *user* in a different account and region) **before** the container-credentials (task-role) -provider, so the call executed as the wrong principal entirely. Nothing in the -tooling surfaced the effective identity; it took a manual -`aws sts get-caller-identity` to see it. +provider, so the call executed as the wrong principal entirely. Nothing declared +which identity *should* manage that target, and nothing surfaced which one +actually did; it took a manual `aws sts get-caller-identity` to see it. -Studio's north star is managing **multiple Fleets** across runtimes, **different -AWS roles / accounts / projects**, and later **different k8s contexts**. As that -fans out, "which credential/context is this Instance (or my current deploy call) -actually bound to?" becomes a first-order operability question. Today it is -implicit and easy to get silently wrong. +Studio's north star is managing **multiple Fleets** across runtimes and **different +AWS accounts / roles / projects** (later **k8s contexts**). Two gaps follow: + +1. **No declared binding.** Nothing says "to manage Fleet X, act as identity Y in + account Z." The managing credential is whatever the ambient chain resolves — + easy to get silently wrong, especially when switching between Fleets. +2. **No verification.** Even once resolved, the *effective* identity is invisible, + so a wrong/misfired binding is undetectable at a glance. Per ADR-2 credential resolution is deferred to "the AWS-credential ceiling", and (ADR-5) resolves lazily inside the sidecar from the standard chain. This ADR does -**not** change that model — it makes the *result* of that resolution observable. +**not** introduce per-caller authz. It makes the operator's *own* managing +identity per Fleet **declarative, actively selected, and observable**. ## 2. Decision -Add a generic, read-only **`RuntimeContext`** projection to the read model, and a -Studio **Identity & Context panel** that renders it. +A `declare → switch → observe → reconcile` loop, in three parts. + +### 2a. FleetBinding config (declare) + +A declarative Studio/control-plane config mapping each managed **Fleet → its +managing context**. Generic fields; vendor specifics resolved by the +`RuntimeDriver`: + +| field | meaning | AWS driver | k8s driver (later) | +|---|---|---|---| +| `fleet` | the managed Fleet | — | — | +| `principal_source` | how to obtain the managing credential | named profile / assume-role ARN / task-role | kubeconfig context / SA | +| `scope` | account / project boundary | account id | cluster / namespace | +| `location` | region / zone | region | context region | + +This is the operator/CP's binding, **not** the agent's `Spec.identity` (that is +the agent's own logical identity). Where it is persisted — Studio config vs +control-plane state — interacts with fleet-store (#18); see §5. + +### 2b. Active context switch (apply) -`RuntimeContext` (observed, per Instance; also foldable per Fleet) — generic -fields; vendor specifics stay inside the `RuntimeDriver`: +Selecting / switching to a Fleet in Studio **switches the active managing +context** to that Fleet's binding: the driver/sidecar resolves credentials +**parameterized by the binding** (the chosen profile / assumed role), instead of +falling through to whatever ambient `[default]` the chain finds first. This is +the *proactive* fix — the wrong-account call never happens because the binding is +selected before the call, not discovered after it fails. + +### 2c. RuntimeContext panel + IdentityMismatch (observe & reconcile) + +A read-only **`RuntimeContext`** Status projection (observed, per Instance; +foldable per Fleet) that the panel renders: | field | meaning | AWS driver | k8s driver (later) | |---|---|---|---| -| `principal` | the effective acting identity | STS caller ARN (role vs user) | context user / ServiceAccount | -| `scope` | the account/project boundary | account id | cluster / namespace | +| `principal` | effective acting identity | STS caller ARN (role vs user) | context user / SA | +| `scope` | account / project | account id | cluster / namespace | | `location` | region / zone | region | context region | -| `source` | where the credential came from | container-creds (task role) / named profile / static keys / env | kubeconfig context / in-cluster SA | -| `verified_at` | when it was last resolved | STS call ts | `auth whoami` ts | - -- **Read-only.** `RuntimeContext` is Status, never Spec — it is *observed*, like - `phase` (ADR-2 §4). It is emitted by the driver, the only place the vendor - calls (STS / `kubectl auth whoami`) live. -- **Expected-vs-actual flag.** When an intended identity binding is declared - (e.g. an expected role/principal for the Deployment), the panel compares it to - the resolved `principal` and raises a **non-blocking `IdentityMismatch`** - Condition — a warning, not a gate. Today's incident would have tripped it on - sight. This stays on the read side: **no enforcement**, consistent with ADR-2's - deferred authz. -- **Panel.** A Studio surface (desktop skin, and any front-end over the same MCP - read model) that lists, per Fleet → Instance, the `RuntimeContext` and any - `IdentityMismatch`. The Fleet-level view folds distinct contexts, so "this - Fleet spans 2 accounts" is visible at a glance. +| `source` | where the credential came from | container-creds / named profile / static keys / env | kubeconfig / in-cluster SA | +| `verified_at` | last resolved | STS call ts | `auth whoami` ts | + +- **Read-only**, emitted by the driver — the only place vendor calls (STS / + `kubectl auth whoami`) live (consistent with ADR-2 §4: state is observed). +- **`IdentityMismatch`** — a **non-blocking** Condition raised when the effective + `principal` ≠ the Fleet's declared binding (2a). It catches a switch that did + not take, or an ambient fallback. Today's incident trips it on sight. Stays on + the read side: **no enforcement**, consistent with ADR-2's deferred authz. ## 3. Scope (now / later) -- **Now:** the AWS driver populates `RuntimeContext` (STS caller identity + - resolved `source`); the read model carries it; the desktop panel renders it per - Instance/Fleet; and the `IdentityMismatch` warning fires when an expected - principal is declared. -- **Later:** k8s driver mapping (context / SA / namespace); richer `source` - provenance; and surfacing the **deploy-call's own** resolved identity (the - caller's principal, not just the managed Instances') so a mis-resolved - CLI/sidecar is caught *before* it acts — the exact preflight that would have - pre-empted the motivating incident. +- **Now (AWS):** FleetBinding config with a per-Fleet credential source + (named profile / assume-role / task-role); switch binds the driver's resolution + to it; panel renders `RuntimeContext` per Instance/Fleet; `IdentityMismatch` + fires when effective ≠ declared. +- **Later:** k8s driver mapping (context / SA / namespace); a **deploy-call + preflight** that resolves and shows the *caller's own* effective identity before + a write, so a mis-resolved CLI/sidecar is caught before it acts; richer `source` + provenance. ## 4. Consequences -- ✅ "Who am I acting as, against what account?" is answerable at a glance; - silent credential fallback (today's bug) becomes visible, ideally flagged. -- ✅ Stays within ADR-2's generic vocabulary and deferred-authz stance — pure - observability, with vendor terms scoped to the driver. -- ⚠️ The driver must make a live identity call (STS / `whoami`); cache it with a - `verified_at`, and treat failure as an `Unknown` context rather than a hard - error. -- ⚠️ `RuntimeContext` surfaces account ids / ARNs in the UI — non-secret, but the - panel must **never** render access keys or tokens. +- ✅ Switching Fleets no longer risks acting under the wrong account — the binding + is selected up front (proactive), and the panel + mismatch verify it (detective). +- ✅ Stays within ADR-2's generic vocabulary and deferred-authz stance: operator + credential *selection*, not per-caller *authorization*; vendor terms scoped to + the driver. +- ⚠️ The sidecar (ADR-5) currently resolves lazily from the **ambient** chain; + honoring a FleetBinding means Studio must **parameterize** that resolution per + Fleet (pass the profile / role in), not rely on the default chain order. +- ⚠️ The driver must make a live identity call (STS / `whoami`); cache with a + `verified_at`, treat failure as `Unknown`, and never render access keys/tokens. ## 5. Open questions -- Does "expected identity" belong in Spec (a new `identity.expectedPrincipal`), - or is it derived from Fleet/project config? (Interacts with fleet-store #18.) -- Should the caller's **own** resolved identity be a standing panel element - (deploy-call preflight), or only the managed Instances'? -- Granularity of `source` provenance without leaking secrets. -- Per-caller authz stays deferred (ADR-2) — this ADR deliberately does not touch - it. +- Where does the FleetBinding live — Studio-local config vs control-plane state + (fleet-store #18)? Single source of truth either way. +- How does `principal_source` map to a concrete credential — named profile, + `sts:AssumeRole`, or an expected task-role — and how is that handed to the + sidecar's lazy resolution (ADR-5)? +- Should the deploy-call preflight (caller's own identity) be standing, or + on-demand before writes only? +- Per-caller authz remains deferred (ADR-2) — this ADR deliberately does not + touch it; the FleetBinding is *selection*, not *authorization*.