Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9557766
fix(deploy): create the first agent with the wizard's AWS credentials…
cognition-team Oct 2, 2026
7a25603
fix(deploy): layer deploy credentials on the fleet binding (#111)
cognition-team Oct 2, 2026
4c3d397
fix(deploy): pin the deploy-identity seam with tests (#111)
cognition-team Oct 2, 2026
f273582
docs(deploy): correct three comment claims from review (#111)
cognition-team Oct 2, 2026
98babe9
fix(oab-mcp): resolve a call's credential by fleet name, not only clu…
cognition-team Oct 2, 2026
b0f32d5
docs(oab-mcp): correct credential-resolution claims from review (#111)
cognition-team Oct 2, 2026
21fc52e
docs(oab-mcp): finish the credential-resolution wording pass (#111)
cognition-team Oct 2, 2026
f575f5c
fix(oab-mcp): test the fleet-name extraction, restate the resolution …
cognition-team Oct 2, 2026
c9662e2
docs(src-tauri): state the two-step credential rule in the bridge com…
cognition-team Oct 2, 2026
8dcad2a
docs: restate credential resolution as fleet-name-first everywhere it…
cognition-team Oct 2, 2026
c6b43f3
test(oab-mcp): pin the deploy-identity args in the tool catalog (#111)
cognition-team Oct 2, 2026
9595b61
test: pin the memo boundary and assert the recorded fleet block (#111)
cognition-team Oct 2, 2026
b38a1cd
test(oab-mcp): cover the args read, make the async tests hermetic (#111)
cognition-team Oct 2, 2026
9258711
docs: relocate a misplaced doc comment and scope two claims (#111)
cognition-team Oct 2, 2026
ff99e4f
docs: disclose the credential-shadowing change where operators will r…
cognition-team Oct 2, 2026
82d19c0
docs: correct the fleet ADR's credential premise; collapse a duplicat…
cognition-team Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ npm run build # tsc + vite build → dist/
| `src/source.ts` | `Source` interface + `MockSource` (fixtures) / `TauriSource` (desktop) |
| `src/render.ts` | pure `rosterHtml(deployments)` → table; `renderRoster` sets it on the DOM |
| `src/main.ts` | polls `Source` every 5s and re-renders |
| `src/deploy.ts` | `[+ New fleet]` / `[+ Add instance]` wizard — collects the fields, then makes the one `deploy_provision_agent` call |
| `src/deployArgs.ts` | pure wizard-fields → `deploy_provision_agent` args (studio#111) |
| `src/fleetToml.ts` | pure `fleets.toml` block edits — the post-deploy config write |
| `src/fixtures.ts` | stand-in roster data |

## Wiring to the core (slice-2)
Expand All @@ -48,3 +51,27 @@ dependency. Slice-2 adds `src-tauri/` whose Rust `deploy_list` command bridges
to `studio-cp::observe_services` / `observe_deployment`. Because the boundary is
the read-model shape (and, later, MCP), swapping `MockSource` → `TauriSource` is
the only change the UI sees.

## Deploy credentials (studio#111)

A deploy is the one call where the wizard's AWS answers have nowhere else to
live: `fleets.toml` is written only *after* a confirmed successful provision
(ADR-83 §7.5), so on a first create the Region + Credential profile the
identity step collected exist solely in the `deploy_provision_agent` arguments
(`src/deployArgs.ts`).

`oab-mcp` resolves the managing credential in two steps: the fleet a call *named*
(`fleet:`) wins, else the binding whose `cluster` key matches, else the ambient
`[default]` chain.

The wizard's deploy call matches neither key: it names no fleet, and a
console-written `[fleet.<name>]` block declares no `cluster` key — so the
recorded pair is read by nothing and the ambient chain answers. (The console's
*read* and scale calls do name a fleet, but `target()` rejects a `fleet`-scoped
ECS call whose binding omits `cluster` — a separate, pre-existing gap; the
deploy call steps around it by passing `cluster` instead.) On a first create the
ambient chain is also what `build_default_manifest`'s VPC/subnet/security-group
discovery runs against, so a fleet created for one account/region would land in
another. Drop the fields and the wizard's answer is silently discarded. Naming
only one of the two is safe: the sidecar layers it onto whatever base binding it
found rather than substituting for it.
58 changes: 43 additions & 15 deletions console/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import type { Source } from "./source";
import { appendMember, appendFleetBlock, fleetBlockExists } from "./fleetToml";
import { provisionAgentArgs, awsIdentityFor } from "./deployArgs";

type Invoke = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;

Expand Down Expand Up @@ -99,6 +100,14 @@ function randomGreekName(): string {
// "add-instance" (that only exists for "new-fleet"), so there's nothing to
// re-collect from the operator — a k8s fleet's context/namespace/service
// account were fixed the moment the fleet was created.
//
// studio#111: the `region`/`profile` pair rides along for the same reason.
// The sidecar resolves a fleet's managing credential by the fleet a call names
// (`fleet:`), else by a binding's `cluster` key — and this console's wizard
// names neither (it passes no `fleet`, and its writer emits no `cluster` key),
// so the recorded pair is read by nothing and the ambient `[default]` chain
// answers instead. The answer has to travel with the call (see
// `deployArgs.ts`).
export type DeployMode =
| { kind: "new-fleet" }
| {
Expand All @@ -108,6 +117,8 @@ export type DeployMode =
context: string | null;
namespace: string | null;
expectedPrincipal: string | null;
region: string | null;
profile: string | null;
};

// What the panel reports back once a deploy + fleets.toml write both succeed —
Expand Down Expand Up @@ -663,21 +674,35 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
}
deployBtn.disabled = true;
setStatus(deployStatusEl, "deploying…");
// studio#111: the AWS identity the deploy runs against has to travel with
// the call — see `deployArgs.ts`. "new-fleet" reads it off the identity
// step's own fields; "add-instance" inherits the existing fleet's
// recorded region/profile, the same place it inherits its k8s placement.
const { region: awsRegion, profile: awsProfile } = awsIdentityFor(
mode.kind === "new-fleet"
? { kind: "new-fleet", region: regionInput.value, profile: profileInput.value }
: { kind: "add-instance", region: mode.region, profile: mode.profile },
);
let res: { image?: string; digest?: string; objects?: number };
try {
res = await invoke("deploy_provision_agent", {
image,
name,
namespace,
api_key: apiKeyInput.value.trim() || undefined,
chat_platform: chatPlatform,
chat_bot_token: chatTokenInput.value.trim() || undefined,
chat_channel_secret: chatSecretInput.value.trim() || undefined,
acp_enabled: acpCheckbox.checked,
acp_token: acpCheckbox.checked ? acpTokenInput.value.trim() || undefined : undefined,
local_config_folder: localConfigFolder(),
...(isK8s ? { provider: "k8s", context, expected_principal: expectedPrincipal } : {}),
});
res = await invoke(
"deploy_provision_agent",
provisionAgentArgs({
image,
name,
namespace,
apiKey: apiKeyInput.value,
chatPlatform,
chatBotToken: chatTokenInput.value,
chatChannelSecret: chatSecretInput.value,
acpEnabled: acpCheckbox.checked,
acpToken: acpTokenInput.value,
localConfigFolder: localConfigFolder(),
region: awsRegion,
profile: awsProfile,
k8s: k8sTarget ? { context, expectedPrincipal } : undefined,
}),
);
} catch (e) {
setStatus(deployStatusEl, `deploy failed: ${errText(e)}`, "err");
deployBtn.disabled = false;
Expand All @@ -698,8 +723,11 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
? { kind: "k8s", context: context ?? null, namespace }
: {
kind: "ecs",
region: regionInput.value.trim() || null,
profile: profileInput.value.trim() || null,
// Same values the deploy just ran under, not a second read
// of the form — the recorded binding and the call that
// created the agent have to agree.
region: awsRegion ?? null,
profile: awsProfile ?? null,
},
})
: appendMember(current.text, fleetName, service);
Expand Down
Loading