Skip to content

docs(adr): ADR-2 — Deployment Control Plane (read/write model + MCP) - #4

Merged
brettchien merged 5 commits into
mainfrom
adr/deployment-control-plane
Aug 8, 2026
Merged

docs(adr): ADR-2 — Deployment Control Plane (read/write model + MCP)#4
brettchien merged 5 commits into
mainfrom
adr/deployment-control-plane

Conversation

@brettchien

Copy link
Copy Markdown
Contributor

Draft ADR-2, consolidating the design we sorted out in the dev thread. Builds on ADR-1.

Scope (this ADR): generic glossary · read model (Spec/Status/phase) · write model (apply + intent verbs, dry-run) · MCP adapter (deploy_*).

Key decisions baked in:

  • Generic vocabulary; vendor terms confined to RuntimeDriver.
  • Status.phase = AgentState (ADR-1's 6 states); 6-state ⇄ k8s mapping + traps.
  • One write primitive apply(Spec); create/scale/stop are sugar; dry-run/diff.
  • MCP server oabctl; tools deploy_list/get/apply/scale/stop (no oabctl_ prefix).
  • Authorization DEFERRED to ADR-3 — interim gate is the AWS credential ceiling only; documented risk + "least-privilege the role, trusted callers only".

Non-goals (deferred): per-caller authz (ADR-3) · controller/reconcile/S3 state store (ADR-4) · k8s/compose drivers · exec/cp/sync/bootstrap/schedule via MCP.

Docs-only. Ready for review — expecting term/wording nits.

brettchien and others added 5 commits August 8, 2026 19:56
Consolidates the design decided in the dev thread:
- generic glossary (Agent/Instance/Deployment/Fleet/Spec/Status/phase/
  AgentState/Discriminators/RuntimeDriver); vendor terms only in drivers.
- read model: Spec+Status, phase=AgentState, 6-state<->k8s mapping + traps.
- write model: apply() primitive; create/scale/stop are sugar; dry-run/diff.
- MCP adapter: server oabctl, tools deploy_list/get/apply/scale/stop.
- authorization DEFERRED to ADR-3 (interim: AWS credential ceiling only,
  documented risk + trusted-callers rule).
- non-goals: authz (ADR-3), controller/S3/reconcile (ADR-4), k8s/compose
  drivers, exec/cp/sync/bootstrap/schedule via MCP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ish)

1. Deployment.status != AgentState: Instance.status.phase = AgentState;
   Deployment.status = replica counters (desired/current/ready/available)
   + Conditions (mirrors k8s). phase is Instance-level only.
2. Write model reaches Paused: add cordon/resume sugar + deploy_cordon/
   deploy_resume MCP tools (admission=accepting_work is a Spec field);
   dry_run is a first-class required param on every write tool.
3. Exposure seam granularity: ServiceStatus must wrap per-Task InstanceStatus;
   driver aggregates DescribeServices (Deployment counters/rollout) +
   DescribeTasks (Instance 4-axis -> phase).

Deferrals unchanged (authz->ADR-3, controller/S3->ADR-4), reviewers PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- D4 (correctness): §4 latch k8s mapping startupProbe -> Ready-first-True
  (startupProbe fires before Ready; latching there breaks ADR-1 F1's
  Starting/Unhealthy split). Aligns with §7 ECS lastStatus-ever-RUNNING.
- D1: §5 apply framed as on-demand one-shot; continuous reconcile = ADR-4.
- D2: split stop (scale->0, reversible, Spec kept) from delete (absence,
  destructive); §6 adds deploy_delete alongside deploy_stop.
- D3: authz interim 'trusted callers' framed as a deployment-time assumption,
  not a technical control.
- D6: glossary config = content referenced by Spec.configRef (not a Spec field).
- healthCheck: §7 ECS conformance — task must define a container healthCheck
  (parallel to compose), else healthStatus UNKNOWN breaks the health axis.
- D5 (server name oabctl): dismissed — already decided.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- N1: admission's driver-equivalent is not 'readiness gate' — it is app/CP-level
  and orthogonal to Ready (ADR-1 F2: accepting_work is CP-controlled, separate
  from health). Same class of error as D4.
- N2: remove raw deploy_apply from the MCP surface — exposing it bypasses the
  individually-guardable sugar verbs (contradicts the §8 rationale). apply stays
  the internal primitive; creating a new Spec is a restricted provisioning
  action, not a first-class operate tool (gating in ADR-3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brettchien
brettchien marked this pull request as ready for review August 8, 2026 16:26
@brettchien
brettchien merged commit 0aaca63 into main Aug 8, 2026
brettchien pushed a commit that referenced this pull request Aug 13, 2026
…site

Address review (Jelly):
- #3: add `ServiceStatus::service_name` carrying the raw ECS name verbatim, and
  query tasks by it instead of `format!`-rebuilding `oab-{ns}-{name}`. The
  rebuild is wrong for any service that doesn't fit the `oab-<ns>-<name>` shape
  (the parser falls back to `namespace = "?"`), so this is a correctness fix,
  not just cleanup.
- #4: the resolver (`resolve_service`) now returns the matched service whose
  `service_name` is passed to `instance_status` with zero further transformation,
  so the test asserts the exact value the call site queries with — not just an
  isolated helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brettchien added a commit that referenced this pull request Aug 13, 2026
…stTasks (#28)

* fix(studio-cp): resolve short service name to full ECS name before ListTasks

`observe_deployment` accepted either the display short name (`orca`) or the
full ECS name (`oab-prod-orca`) when matching a service, then passed the
caller's string verbatim to `instance_status`. ECS `ListTasks` filters by
`service_name`, which only accepts the full name, so a short name 404s as
`ServiceNotFoundException`. The roster / `get_agent_states` path collects short
names, so it failed for every agent once credentials resolved to the right
account (previously masked by an AccessDenied earlier in the chain).

- studio-cp: centralise the short->full mapping (`canonical_service_name` +
  `find_service`) and query tasks by the resolved full name.
- oabctl: harden `instance_status` to fail loud when handed a non-`oab-` name
  instead of surfacing an opaque ECS `ServiceNotFoundException`.
- test: `service_selector_resolves_to_full_ecs_name` guards the short-name path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* review: carry raw ECS service_name; test the observe_deployment call site

Address review (Jelly):
- #3: add `ServiceStatus::service_name` carrying the raw ECS name verbatim, and
  query tasks by it instead of `format!`-rebuilding `oab-{ns}-{name}`. The
  rebuild is wrong for any service that doesn't fit the `oab-<ns>-<name>` shape
  (the parser falls back to `namespace = "?"`), so this is a correctness fix,
  not just cleanup.
- #4: the resolver (`resolve_service`) now returns the matched service whose
  `service_name` is passed to `instance_status` with zero further transformation,
  so the test asserts the exact value the call site queries with — not just an
  isolated helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Orca (openab agent) <orca@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant