Skip to content

docs(adr): agent deployment — template/skills bundles → provider-tagged provisioning - #63

Merged
brettchien merged 2 commits into
mainfrom
docs/adr-agent-deployment-templates
Aug 16, 2026
Merged

docs(adr): agent deployment — template/skills bundles → provider-tagged provisioning#63
brettchien merged 2 commits into
mainfrom
docs/adr-agent-deployment-templates

Conversation

@brettchien

Copy link
Copy Markdown
Contributor

Direction ADR for the deployment vision we aligned on in-thread (2026-08-15).

The idea

Studio should stand up agents, not just observe them. Grounded on three facts:

  • An agent = a prebuilt image tag + a plain-file bundle (config.toml + persona + .claude/skills). Nothing in an agent's identity needs compiling.
  • Compilation is CI/CD, never the deploy path — Studio consumes an image tag.
  • The runtime isn't always ECS — k8s is on the roadmap, so deploy is provider-tagged from the start.

Decision (summary)

  • Studio holds a template/skills library; composes a per-agent bundle (template ⊕ overlay, last-writer-wins).
  • Provision via a provider-tagged RuntimeDriver: pick an image tag + push the bundle to the runtime's file carrier (S3 state for ECS via pre_seed, ConfigMap/volume for k8s) + apply the workload. The driver is the only layer that knows S3-vs-ConfigMap.
  • Credential/region binding reuses the hermetic, provider-tagged approach from feat(desktop): provider-tagged, hermetic oab-mcp target (backend) #60; desired state records into the Fleet Store (ADR: Fleet Store & Control-Plane State Ownership #18) when it lands.

Boundaries

Status: Proposed (draft) — direction alignment; open questions in §6 (bundle transport, template storage, skills-by-reference, image/file boundary).

🤖 Generated with Claude Code

brettchien and others added 2 commits August 15, 2026 23:42
…ed provisioning

Studio stands up agents (not just observes): an agent = a prebuilt image tag +
a plain-file bundle (config.toml + persona + skills). Studio holds a
template/skills library, composes a per-agent bundle (template ⊕ overlay), and
provisions it via a provider-tagged driver — pick an image tag + push the bundle
to the runtime's file carrier (S3 state for ECS, ConfigMap/volume for k8s) +
apply the workload. No build in the deploy path (compilation is CI/CD). Defers
#49 Part D (remote editing of a running agent's files). Relates to #18 (desired
state) and #60 (provider-tagged hermetic target).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Direction greenlit by Brett (2026-08-16). Slices 1&2 (template/skills
library + compose; ECS provision) to follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brettchien
brettchien merged commit 3982f87 into main Aug 16, 2026
1 check passed
@brettchien
brettchien deleted the docs/adr-agent-deployment-templates branch August 16, 2026 06:35
brettchien added a commit that referenced this pull request Aug 16, 2026
Slice 1 of the accepted agent-deployment ADR (#63): the compose seam.
Author a template/overlay/skills library and preview the composed
{path -> bytes} agent bundle. Studio-local, zero AWS/upstream.

- crates/studio-compose: pure compose core (template ⊕ overlay + skills
  by-reference -> deterministic Bundle + image tag), last-writer-wins
  (overlay > skills > template), sha256 bundle digest. 17 unit tests.
- src-tauri: compose_library_get/set + compose_preview commands;
  library persisted as one JSON doc under app config dir (like config.rs).
- console: Compose tab (JSON library editor + template/overlay picker +
  bundle preview). Pure render helpers unit-tested.

Verified: cargo test -p studio-compose (17), console tsc + 65 vitest +
vite build. src-tauri Rust compiles under bundle-macos CI (GTK dep).

Co-authored-by: Orca <orca@openab.dev>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
brettchien pushed a commit that referenced this pull request Aug 16, 2026
…rovision (deploy slice 2, path A)

Slice 2 foundation of the agent-deployment ADR (#63), path A (extend the
artifacts prefix; the runtime restores it into ~ at boot):

- studio-compose: Bundle::artifact_objects(ns, name) — pure {path->bytes}
  → (artifacts/{ns}/{name}/{path}, bytes) mapping + artifacts_prefix().
  Unit-tested (2 new).
- oabctl manifest: new optional Spec.bundleFrom (mirrors bootstrapFrom;
  propagated through OABFleet expand). Legacy manifests unchanged (default None).
- oabctl studio_api (additive, config-free): bundle_from_uri(), push_bundle()
  (put each object to the control-plane bucket), and provision() = push bundle
  then apply the manifest at its chosen image tag. Reuses parse_manifests +
  apply_manifests. Tests for bundle_from_uri + bundleFrom manifest round-trip.

Deferred (next increments): oab-mcp deploy_provision tool + src-tauri command
+ Compose-tab Deploy button + create-wizard bundle support; and the openab
image boot step that syncs bundleFrom prefix into ~ (cross-repo).

Verified: cargo test -p studio-compose (19); cargo check -p oabctl (+ --tests).
Full oabctl test + src-tauri build run under CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brettchien added a commit that referenced this pull request Aug 16, 2026
…oy slice 2, path A) (#65)

* feat(provision): compose→artifacts layout + bundleFrom + studio_api provision (deploy slice 2, path A)

Slice 2 foundation of the agent-deployment ADR (#63), path A (extend the
artifacts prefix; the runtime restores it into ~ at boot):

- studio-compose: Bundle::artifact_objects(ns, name) — pure {path->bytes}
  → (artifacts/{ns}/{name}/{path}, bytes) mapping + artifacts_prefix().
  Unit-tested (2 new).
- oabctl manifest: new optional Spec.bundleFrom (mirrors bootstrapFrom;
  propagated through OABFleet expand). Legacy manifests unchanged (default None).
- oabctl studio_api (additive, config-free): bundle_from_uri(), push_bundle()
  (put each object to the control-plane bucket), and provision() = push bundle
  then apply the manifest at its chosen image tag. Reuses parse_manifests +
  apply_manifests. Tests for bundle_from_uri + bundleFrom manifest round-trip.

Deferred (next increments): oab-mcp deploy_provision tool + src-tauri command
+ Compose-tab Deploy button + create-wizard bundle support; and the openab
image boot step that syncs bundleFrom prefix into ~ (cross-repo).

Verified: cargo test -p studio-compose (19); cargo check -p oabctl (+ --tests).
Full oabctl test + src-tauri build run under CI.

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

* feat(deploy): Compose→Deploy vertical — deploy_provision (slice 2, track ②)

Wire the compose library to a one-click ECS deploy, end to end:

- oabctl studio_api: load_manifest() reads the stored desired manifest
  (manifests/{ns}/{name}.yaml); redeploy() loads it, repoints image +
  bundleFrom, pushes the bundle, and applies — networking/resources/secrets
  ride along from the stored manifest, so a redeploy needs no infra input.
- studio-cp: provision_from_library() — compose template⊕overlay, then
  redeploy; returns a ProvisionOutcome (image/digest/objects/action).
- oab-mcp: new deploy_provision tool (thin dispatch into studio-cp).
- src-tauri: deploy_provision command bridging to the sidecar tool.
- console: Deploy form on the Compose tab (revealed after preview) —
  namespace/name/image tag → compose→push→apply, with result status.

The agent must already be created (redeploy reuses its stored manifest);
net-new agent networking-from-bootstrap is a follow-up, as is the openab
image-side bundle consumption (still under discussion).

Verified: cargo check -p oabctl / -p studio-cp / -p oab-mcp; console tsc +
65 vitest + vite build. Full workspace test + src-tauri build run under CI.

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

---------

Co-authored-by: Orca <orca@openab.dev>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
brettchien added a commit that referenced this pull request Aug 22, 2026
…pre_seed (slice 3c)

Root-cause fix for the gap studio#97's slice-3c investigation found: nothing
in openab downloads bundleFrom's loose-file S3 prefix back into ~ at boot.
push_bundle (slice 2, already merged) only ever built the upload half; no
restore mechanism was ever wired to the other end, on ECS or anywhere else.
openab's only "S3 -> ~" feature is hooks.pre_seed, and it consumes zip
archives, not a prefix of individual objects -- a different shape nothing
bridged.

Fix: reuse pre_seed instead of building a bespoke restore mechanism per
platform.

- studio_compose::Bundle::zip_bytes() -- a deterministic zip of the bundle's
  files (still pure/no-I/O: ZipWriter writes to an in-memory Cursor<Vec<u8>>).
- oabctl::studio_api::bundle_zip_uri/inject_pre_seed_hook -- the zip's S3 URI,
  and appending a [hooks.pre_seed] section to a config.toml's bytes pointing
  at it. Appends rather than re-parsing+re-serializing, so an operator's
  existing comments/formatting survive; a no-op if hooks.pre_seed is already
  present (never overrides an operator's own hook).
- control_plane::resolve_bucket exposed pub, so a caller can resolve the
  bucket before constructing objects (needed to compute the zip URI ahead of
  upload) instead of after, avoiding a second implicit resolution.
- provision_from_library (studio-cp): resolves the bucket once, injects the
  hook into the composed config.toml object, uploads the zip alongside the
  existing per-file objects (kept for now -- harmless, and removing them is a
  separable decision).
- K8sDriver: removed the bundle_from bail entirely. This is the actual
  payoff -- pre_seed is orchestrator-agnostic (S3 GetObject + extract, same
  binary/feature regardless of what started the process), and
  build_deployment already points the container's command at configFrom
  exactly like EcsDriver does, so a k8s pod restores its bundle through the
  identical mechanism with zero k8s-specific carrier code. No ConfigMap,
  no volume, no init-container -- slice 3c's original scope turned out not
  to exist once the actual restore path was fixed.

Fixes both the ECS gap and the k8s driver in one change; the two platforms
now share one restore mechanism instead of doubling the maintenance surface
with a bespoke k8s carrier.

11 new tests across oabctl/studio-compose/studio-cp (145/145 total across
the three crates), clippy clean (no new warnings; 3 pre-existing ones
elsewhere untouched).

Stacked on #102 (3f) -- needs K8sDriver + the driver-side test fixtures from
the earlier branches in the chain.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…river slice 3a) (#98)

* feat(oabctl): extract ProvisionDriver trait for the write path (K8s driver slice 3a)

studio_api's write path (provision/scale/delete) called ECS-specific code
(apply_manifests, ecsctl::scale_service, delete::run_with_bucket) directly,
with no trait boundary — ADR-2's "RuntimeDriver is the only layer with
vendor terms" only held for state classification, not CRUD. Introduce
ProvisionDriver and an EcsDriver impl that thin-wraps the existing free
functions unchanged, and route studio_api's write path through it.

No behavior change: EcsDriver's methods are pass-throughs, all 85 existing
oabctl tests stay green. This gives K8s driver (slice 3b, studio#97) a real
seam with an actual caller to dispatch through, instead of a speculative
trait with no consumer.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)

* fix(oabctl): move cluster off ProvisionDriver's per-call signature, rename ecs_service_name

Found while starting K8sDriver (slice 3b): ProvisionDriver::apply took
apply::ApplyOptions directly (an ECS-shaped type — 'cluster' field documented
as "ECS cluster name or ARN"), and scale/delete took `cluster: &str` as a
per-call parameter. Neither generalizes to a k8s driver, which has no
"cluster" — it has a context+namespace bound at driver-construction time,
same as EcsDriver already binds aws_config.

- EcsDriver now holds `cluster` as an instance field alongside aws_config,
  matching how it already holds the AWS credential/region context.
- New ProvisionOptions carries only what's actually generic (bucket, wait);
  apply::ApplyOptions (still ECS-specific, used directly by apply_manifests)
  is unchanged.
- AppliedService.ecs_service_name -> resource_name: the only field crossing
  the trait boundary that still had a vendor-specific name. ServiceTarget
  (ECS-internal error detail, never crosses the trait) keeps its ECS name —
  it's genuinely ECS-only, not a leak.

No behavior change for the ECS path; 85/85 tests green. This was going to
bite immediately on K8sDriver's first method — fixing it now, before #98
merges, is cheaper than a second breaking change to studio_api after.
brettchien added a commit that referenced this pull request Aug 23, 2026
…yment (slice 3b)

Second ProvisionDriver impl, behind kube-rs. Scope deliberately narrow:

- apply(): builds a k8s Deployment from an OABServiceManifest (image,
  command = `openab run -c <configFrom>` — same convention EcsDriver uses,
  env NAMESPACE/NAME/BOOTSTRAP_FROM, cpu/memory as resource requests+limits,
  service account / node selector / tolerations from KubernetesRuntime),
  server-side-applies it. Deployment name is `oab-{name}` — OAB's own
  `namespace` maps directly onto the k8s namespace (already an isolation
  boundary, the same job it does for ECS's flat `oab-{ns}-{name}` naming),
  so unlike ECS there's no need to fold namespace into the resource name.
- scale(): patches replicas, same 0/1-only constraint as EcsDriver.
- delete(): deletes the Deployment, idempotent on 404.
- Kubeconfig context resolution: `K8sDriver::from_context(Option<&str>)` —
  `None` uses the kubeconfig's current context, same "ambient default,
  explicit override" shape aws_config uses. This is also how orbstack's
  local cluster gets targeted later (3f) — just another context, no
  special-casing.

Explicitly NOT handled yet, and apply() bails loudly rather than silently
mis-deploying if a manifest needs them:
- spec.bundleFrom (persona/skills bundle) — needs a ConfigMap/volume carrier,
  sub-slice 3c.
- spec.secrets — ECS resolves into Secret.valueFrom ARNs; k8s needs a
  different output shape (a Secret key selector), sub-slice 3d.

Also out of scope for this PR: observing k8s state into the canonical
6-state (status.rs's ECS service_status/instance_status has no k8s
counterpart yet — a new agent-lifecycle RuntimeDriver impl per ADR-2's
6-state<->k8s mapping table). Substantial enough to land as its own
follow-up rather than growing this one further.

Adds kube 0.99 + k8s-openapi 0.24 (rustls-tls, no openssl).

Stacked on #98 (3a) — this branch is 3a's branch + the K8sDriver commit;
GitHub will show 3a+3b combined in the diff until #98 merges, at which point
the diff narrows to just this. 7 new unit tests, 92/92 total green, clippy
-D warnings clean.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…yment (slice 3b) (#99)

Second ProvisionDriver impl, behind kube-rs. Scope deliberately narrow:

- apply(): builds a k8s Deployment from an OABServiceManifest (image,
  command = `openab run -c <configFrom>` — same convention EcsDriver uses,
  env NAMESPACE/NAME/BOOTSTRAP_FROM, cpu/memory as resource requests+limits,
  service account / node selector / tolerations from KubernetesRuntime),
  server-side-applies it. Deployment name is `oab-{name}` — OAB's own
  `namespace` maps directly onto the k8s namespace (already an isolation
  boundary, the same job it does for ECS's flat `oab-{ns}-{name}` naming),
  so unlike ECS there's no need to fold namespace into the resource name.
- scale(): patches replicas, same 0/1-only constraint as EcsDriver.
- delete(): deletes the Deployment, idempotent on 404.
- Kubeconfig context resolution: `K8sDriver::from_context(Option<&str>)` —
  `None` uses the kubeconfig's current context, same "ambient default,
  explicit override" shape aws_config uses. This is also how orbstack's
  local cluster gets targeted later (3f) — just another context, no
  special-casing.

Explicitly NOT handled yet, and apply() bails loudly rather than silently
mis-deploying if a manifest needs them:
- spec.bundleFrom (persona/skills bundle) — needs a ConfigMap/volume carrier,
  sub-slice 3c.
- spec.secrets — ECS resolves into Secret.valueFrom ARNs; k8s needs a
  different output shape (a Secret key selector), sub-slice 3d.

Also out of scope for this PR: observing k8s state into the canonical
6-state (status.rs's ECS service_status/instance_status has no k8s
counterpart yet — a new agent-lifecycle RuntimeDriver impl per ADR-2's
6-state<->k8s mapping table). Substantial enough to land as its own
follow-up rather than growing this one further.

Adds kube 0.99 + k8s-openapi 0.24 (rustls-tls, no openssl).

Stacked on #98 (3a) — this branch is 3a's branch + the K8sDriver commit;
GitHub will show 3a+3b combined in the diff until #98 merges, at which point
the diff narrows to just this. 7 new unit tests, 92/92 total green, clippy
-D warnings clean.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
spec.secrets values for a k8s-runtime manifest must now be
k8s-secret://<secret-name>#<key> — parsed in secrets.rs (pure, no API call:
unlike aws-sm://, a k8s Secret is referenced by name+key directly, kubelet
resolves it at pod-start time) and wired into build_deployment as
env[].valueFrom.secretKeyRef. Any other scheme (an ECS aws-sm:// ref or raw
ARN left over from copy-pasting an ECS manifest) fails loudly at apply time
instead of silently mis-deploying — same "declare the contract, fail closed
on mismatch" shape the rest of this driver already uses.

The Secret object itself must already exist in the target namespace —
creating it is a separate concern, deliberately out of scope here (mirrors
aws-sm://, which likewise only *references* a secret Secrets Manager already
holds, never creates one).

reject_unsupported() now only guards spec.bundleFrom (3c, still open) —
secrets are no longer in that list.

6 new tests (2 in secrets.rs for the parser, 4 in k8s_driver.rs), 98/98
total green, clippy -D warnings clean.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
spec.secrets values for a k8s-runtime manifest must now be
k8s-secret://<secret-name>#<key> — parsed in secrets.rs (pure, no API call:
unlike aws-sm://, a k8s Secret is referenced by name+key directly, kubelet
resolves it at pod-start time) and wired into build_deployment as
env[].valueFrom.secretKeyRef. Any other scheme (an ECS aws-sm:// ref or raw
ARN left over from copy-pasting an ECS manifest) fails loudly at apply time
instead of silently mis-deploying — same "declare the contract, fail closed
on mismatch" shape the rest of this driver already uses.

The Secret object itself must already exist in the target namespace —
creating it is a separate concern, deliberately out of scope here (mirrors
aws-sm://, which likewise only *references* a secret Secrets Manager already
holds, never creates one).

reject_unsupported() now only guards spec.bundleFrom (3c, still open) —
secrets are no longer in that list.

6 new tests (2 in secrets.rs for the parser, 4 in k8s_driver.rs), 98/98
total green, clippy -D warnings clean.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…s.toml (slice 3f)

K8sFleetBinding/K8sFleetBindings, the k8s counterpart to the existing AWS
FleetBinding/FleetBindings: a fleet's context+namespace instead of
cluster+profile, resolve_binding_driver -> oabctl::K8sDriver instead of
resolve_binding_config -> aws_config::SdkConfig.

Deliberately a **separate file** (fleets-k8s.toml, $OAB_K8S_FLEETS_CONFIG),
not a second [k8s_fleet.*] table in the existing fleets.toml. Reason:
save_bindings_text/save_k8s_bindings_text are both whole-file verbatim
writes (the console's TOML editor round-trips exact text, comments and all).
Two tables sharing one file means saving either one from the UI would
silently clobber the other's edits on the next write -- concretely, editing
a k8s fleet in the console could wipe Brett's actual prod fleets.toml
([fleet.orca]/[fleet.mira], the ECS bindings that already gate real AWS
credential selection -- see the fleets-toml-binding incident history).
Separate files make that class of bug structurally impossible rather than
relying on a careful merge in the write path.

read_bindings_text/write_bindings_atomic are reused as-is for the k8s file
(they're already path+text generic, no AWS types) -- only the TOML-shaped
type being parsed differs.

Scope: backend config-schema + load/save only. The console UI panel that
actually lets an operator pick AWS-vs-k8s and edit fleets-k8s.toml is
frontend work, not attempted here.

Stacked on #100 (3d) -- resolve_binding_driver needs oabctl::K8sDriver,
which only exists on the unmerged 3a/3b branch chain.

8 new tests (23/23 total in studio-cp), clippy introduces no new warnings
(2 pre-existing ones elsewhere in studio-cp/studio-compose, untouched).

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…s.toml (slice 3f) (#102)

K8sFleetBinding/K8sFleetBindings, the k8s counterpart to the existing AWS
FleetBinding/FleetBindings: a fleet's context+namespace instead of
cluster+profile, resolve_binding_driver -> oabctl::K8sDriver instead of
resolve_binding_config -> aws_config::SdkConfig.

Deliberately a **separate file** (fleets-k8s.toml, $OAB_K8S_FLEETS_CONFIG),
not a second [k8s_fleet.*] table in the existing fleets.toml. Reason:
save_bindings_text/save_k8s_bindings_text are both whole-file verbatim
writes (the console's TOML editor round-trips exact text, comments and all).
Two tables sharing one file means saving either one from the UI would
silently clobber the other's edits on the next write -- concretely, editing
a k8s fleet in the console could wipe Brett's actual prod fleets.toml
([fleet.orca]/[fleet.mira], the ECS bindings that already gate real AWS
credential selection -- see the fleets-toml-binding incident history).
Separate files make that class of bug structurally impossible rather than
relying on a careful merge in the write path.

read_bindings_text/write_bindings_atomic are reused as-is for the k8s file
(they're already path+text generic, no AWS types) -- only the TOML-shaped
type being parsed differs.

Scope: backend config-schema + load/save only. The console UI panel that
actually lets an operator pick AWS-vs-k8s and edit fleets-k8s.toml is
frontend work, not attempted here.

Stacked on #100 (3d) -- resolve_binding_driver needs oabctl::K8sDriver,
which only exists on the unmerged 3a/3b branch chain.

8 new tests (23/23 total in studio-cp), clippy introduces no new warnings
(2 pre-existing ones elsewhere in studio-cp/studio-compose, untouched).

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…pre_seed (slice 3c)

Root-cause fix for the gap studio#97's slice-3c investigation found: nothing
in openab downloads bundleFrom's loose-file S3 prefix back into ~ at boot.
push_bundle (slice 2, already merged) only ever built the upload half; no
restore mechanism was ever wired to the other end, on ECS or anywhere else.
openab's only "S3 -> ~" feature is hooks.pre_seed, and it consumes zip
archives, not a prefix of individual objects -- a different shape nothing
bridged.

Fix: reuse pre_seed instead of building a bespoke restore mechanism per
platform.

- studio_compose::Bundle::zip_bytes() -- a deterministic zip of the bundle's
  files (still pure/no-I/O: ZipWriter writes to an in-memory Cursor<Vec<u8>>).
- oabctl::studio_api::bundle_zip_uri/inject_pre_seed_hook -- the zip's S3 URI,
  and appending a [hooks.pre_seed] section to a config.toml's bytes pointing
  at it. Appends rather than re-parsing+re-serializing, so an operator's
  existing comments/formatting survive; a no-op if hooks.pre_seed is already
  present (never overrides an operator's own hook).
- control_plane::resolve_bucket exposed pub, so a caller can resolve the
  bucket before constructing objects (needed to compute the zip URI ahead of
  upload) instead of after, avoiding a second implicit resolution.
- provision_from_library (studio-cp): resolves the bucket once, injects the
  hook into the composed config.toml object, uploads the zip alongside the
  existing per-file objects (kept for now -- harmless, and removing them is a
  separable decision).
- K8sDriver: removed the bundle_from bail entirely. This is the actual
  payoff -- pre_seed is orchestrator-agnostic (S3 GetObject + extract, same
  binary/feature regardless of what started the process), and
  build_deployment already points the container's command at configFrom
  exactly like EcsDriver does, so a k8s pod restores its bundle through the
  identical mechanism with zero k8s-specific carrier code. No ConfigMap,
  no volume, no init-container -- slice 3c's original scope turned out not
  to exist once the actual restore path was fixed.

Fixes both the ECS gap and the k8s driver in one change; the two platforms
now share one restore mechanism instead of doubling the maintenance surface
with a bespoke k8s carrier.

11 new tests across oabctl/studio-compose/studio-cp (145/145 total across
the three crates), clippy clean (no new warnings; 3 pre-existing ones
elsewhere untouched).

Stacked on #102 (3f) -- needs K8sDriver + the driver-side test fixtures from
the earlier branches in the chain.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…pre_seed (slice 3c) (#103)

* fix(bundle): actually restore a deployed agent's bundle at boot, via pre_seed (slice 3c)

Root-cause fix for the gap studio#97's slice-3c investigation found: nothing
in openab downloads bundleFrom's loose-file S3 prefix back into ~ at boot.
push_bundle (slice 2, already merged) only ever built the upload half; no
restore mechanism was ever wired to the other end, on ECS or anywhere else.
openab's only "S3 -> ~" feature is hooks.pre_seed, and it consumes zip
archives, not a prefix of individual objects -- a different shape nothing
bridged.

Fix: reuse pre_seed instead of building a bespoke restore mechanism per
platform.

- studio_compose::Bundle::zip_bytes() -- a deterministic zip of the bundle's
  files (still pure/no-I/O: ZipWriter writes to an in-memory Cursor<Vec<u8>>).
- oabctl::studio_api::bundle_zip_uri/inject_pre_seed_hook -- the zip's S3 URI,
  and appending a [hooks.pre_seed] section to a config.toml's bytes pointing
  at it. Appends rather than re-parsing+re-serializing, so an operator's
  existing comments/formatting survive; a no-op if hooks.pre_seed is already
  present (never overrides an operator's own hook).
- control_plane::resolve_bucket exposed pub, so a caller can resolve the
  bucket before constructing objects (needed to compute the zip URI ahead of
  upload) instead of after, avoiding a second implicit resolution.
- provision_from_library (studio-cp): resolves the bucket once, injects the
  hook into the composed config.toml object, uploads the zip alongside the
  existing per-file objects (kept for now -- harmless, and removing them is a
  separable decision).
- K8sDriver: removed the bundle_from bail entirely. This is the actual
  payoff -- pre_seed is orchestrator-agnostic (S3 GetObject + extract, same
  binary/feature regardless of what started the process), and
  build_deployment already points the container's command at configFrom
  exactly like EcsDriver does, so a k8s pod restores its bundle through the
  identical mechanism with zero k8s-specific carrier code. No ConfigMap,
  no volume, no init-container -- slice 3c's original scope turned out not
  to exist once the actual restore path was fixed.

Fixes both the ECS gap and the k8s driver in one change; the two platforms
now share one restore mechanism instead of doubling the maintenance surface
with a bespoke k8s carrier.

11 new tests across oabctl/studio-compose/studio-cp (145/145 total across
the three crates), clippy clean (no new warnings; 3 pre-existing ones
elsewhere untouched).

Stacked on #102 (3f) -- needs K8sDriver + the driver-side test fixtures from
the earlier branches in the chain.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)

* fix(oabctl): address code-review findings on the bundle-restore fix

Follow-up commit on #103, from a multi-angle review pass before merge:

- inject_pre_seed_hook: rewritten on toml_edit (format-preserving) instead
  of raw string append. Previously, *any* existing [hooks.pre_seed] made
  injection a full no-op -- an operator's own hooks.pre_seed (e.g. this
  agent's own persistent-state restore, unrelated to the deploy bundle)
  meant the new bundle.zip was uploaded but never actually wired in, and
  provision_from_library still reported success. Now merges the zip URI
  into the existing `sources` array (true no-op only when that exact URI is
  already present), mirrors pre_seed's own 5-source cap, and bails instead
  of silently producing invalid TOML when `hooks`/`hooks.pre_seed` is
  already an inline table (which can't be reopened with a `[table]` header).

- provision_from_library: mutates `bundle.files` in place before deriving
  artifact_objects/zip_bytes/digest from it, instead of deriving them from
  the pre-injection Bundle. Previously the uploaded zip's own config.toml
  never got the pre_seed hook, and ProvisionOutcome.digest didn't match
  what was actually uploaded.

- secret_env_vars: attaches the per-secret error context (env var name,
  namespace/name) to both the "wrong scheme" and "right scheme, malformed
  body" cases. anyhow's Context impl for Option<T> only fires on None, so
  the previous `.with_context()??` silently dropped context on the latter.

- EcsDriver::apply: preserves the structured ApplyError in the anyhow
  error's source chain (via .context() on Error::new(e)) instead of
  flattening it into anyhow::anyhow!(...). A caller can again
  `.chain().find_map(downcast_ref::<ApplyError>)` to recover
  `.completed`/`.failed_service` for a partial fleet-apply failure.
  ApplyError::reconciliation made pub(crate) so driver.rs's test can
  construct one without a live ECS call.

24 new/changed tests, 131/131 total across oabctl+studio-cp, clippy clean
(same 3 pre-existing warnings elsewhere, untouched).
brettchien added a commit that referenced this pull request Aug 23, 2026
…r kubeconfig contexts (slice 3e)

observe_k8s_identity(context) is the k8s counterpart to the existing AWS
observe_identity(aws_config) — same RuntimeContext struct, per ADR-19's
AWS-driver/k8s-driver field mapping table:

  principal  <- SelfSubjectReview username (authentication.k8s.io/v1,
                stable since k8s 1.28 — the literal API `kubectl auth
                whoami` calls; a live server round-trip, not a value read
                out of the kubeconfig file, matching how observe_identity
                uses a live STS GetCallerIdentity rather than trusting the
                configured profile)
  principal_kind <- "service-account" / "user" / "unknown", mirrors
                     principal_kind's role/user/unknown split
  scope      <- "{cluster}/{namespace}" from the kubeconfig context entry
  location   <- left empty: k8s has no first-class region/zone concept
                the way AWS does, so there's nothing honest to fill in
                (same "empty if unset" contract location already has)
  source     <- "kubeconfig context: <name>"
  caller_id  <- SelfSubjectReview's UserInfo.uid

context = None uses the kubeconfig's current-context, mirroring
K8sDriver::from_context's "ambient default, explicit override" shape.

Adds kube 0.99 + k8s-openapi 0.24 to studio-cp (same versions oabctl
already pulls in slice 3b). 1 new test (k8s_principal_kind), 18/18 total
green in studio-cp. clippy clean for this code specifically — studio-cp/
studio-compose already carry a few pre-existing warnings elsewhere in the
crate, untouched by this change, not part of CI's gate for these crates.

Not stacked on #98/#99/#100 — this only touches studio-cp, independent of
oabctl's driver work, so it branches from main directly.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…r kubeconfig contexts (slice 3e) (#101)

observe_k8s_identity(context) is the k8s counterpart to the existing AWS
observe_identity(aws_config) — same RuntimeContext struct, per ADR-19's
AWS-driver/k8s-driver field mapping table:

  principal  <- SelfSubjectReview username (authentication.k8s.io/v1,
                stable since k8s 1.28 — the literal API `kubectl auth
                whoami` calls; a live server round-trip, not a value read
                out of the kubeconfig file, matching how observe_identity
                uses a live STS GetCallerIdentity rather than trusting the
                configured profile)
  principal_kind <- "service-account" / "user" / "unknown", mirrors
                     principal_kind's role/user/unknown split
  scope      <- "{cluster}/{namespace}" from the kubeconfig context entry
  location   <- left empty: k8s has no first-class region/zone concept
                the way AWS does, so there's nothing honest to fill in
                (same "empty if unset" contract location already has)
  source     <- "kubeconfig context: <name>"
  caller_id  <- SelfSubjectReview's UserInfo.uid

context = None uses the kubeconfig's current-context, mirroring
K8sDriver::from_context's "ambient default, explicit override" shape.

Adds kube 0.99 + k8s-openapi 0.24 to studio-cp (same versions oabctl
already pulls in slice 3b). 1 new test (k8s_principal_kind), 18/18 total
green in studio-cp. clippy clean for this code specifically — studio-cp/
studio-compose already carry a few pre-existing warnings elsewhere in the
crate, untouched by this change, not part of CI's gate for these crates.

Not stacked on #98/#99/#100 — this only touches studio-cp, independent of
oabctl's driver work, so it branches from main directly.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
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