docs(adr): agent deployment — template/skills bundles → provider-tagged provisioning - #63
Merged
Merged
Conversation
…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 <[email protected]>
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 <[email protected]>
This was referenced Aug 16, 2026
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 <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Orca <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
This was referenced Aug 21, 2026
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
config.toml+ persona +.claude/skills). Nothing in an agent's identity needs compiling.Decision (summary)
template ⊕ overlay, last-writer-wins).RuntimeDriver: pick an image tag + push the bundle to the runtime's file carrier (S3 state for ECS viapre_seed, ConfigMap/volume for k8s) + apply the workload. The driver is the only layer that knows S3-vs-ConfigMap.Boundaries
/acp(docs(adr): two consoles — management + per-agent agent consoles (endpoint registry + remote file editor) #49 Part D — editing a running agent) is deferred (different surface).Status: Proposed (draft) — direction alignment; open questions in §6 (bundle transport, template storage, skills-by-reference, image/file boundary).
🤖 Generated with Claude Code