Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions console/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ function errText(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}

// studio#135: same localStorage key main.ts's Config-folder setting uses
// (kept as a duplicated literal, not a shared export — every module in
// this console reads its own localStorage keys independently, matching
// the existing theme/log-level/config-folder settings' own pattern).
// Brett's explicit ordering ("write to local first, write to s3 if
// needed") can only actually be guaranteed inside the sidecar — passing
// the folder through lets provision_agent[_k8s] write it before touching
// S3 at all, rather than the console writing a copy after the fact.
function localConfigFolder(): string | undefined {
try {
return localStorage.getItem("oab-studio.configFolder") ?? undefined;
} catch {
return undefined;
}
}

function escapeHtml(s: string): string {
return s
.replace(/&/g, "&")
Expand Down Expand Up @@ -487,6 +503,7 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
chat_bot_token: chatTokenInput.value.trim() || undefined,
chat_channel_secret: chatSecretInput.value.trim() || undefined,
acp_enabled: acpCheckbox.checked,
local_config_folder: localConfigFolder(),
...(isK8s ? { provider: "k8s", context, expected_principal: expectedPrincipal } : {}),
});
} catch (e) {
Expand Down
8 changes: 8 additions & 0 deletions crates/oab-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ pub fn tools() -> Vec<Tool> {
"chat_bot_token": { "type": "string", "description": "Discord/Telegram bot token, or LINE's channel access token." },
"chat_channel_secret": { "type": "string", "description": "LINE only." },
"acp_enabled": { "type": "boolean", "description": "Enable the reverse-MCP-over-ACP tunnel on this agent. Defaults to true when omitted (studio#119: Studio-deployed agents default to ACP on). Not honorable for every vendor — the caller is responsible for not setting this true for a vendor that can't support it (e.g. agy, whose bridge bypasses /acp entirely)." },
"local_config_folder": { "type": "string", "description": "Optional local directory (studio#135) — when set, config.toml is written to <local_config_folder>/<name>/config.toml *before* anything touches S3. Omit to skip the local mirror entirely." },
"provider": { "type": "string", "description": "\"aws\" (default) or \"k8s\" — which driver applies the result." },
"fleet": { "type": "string", "description": "AWS only. Fleet name (see fleet_config): targets the fleet's cluster and managing credential; a write to a service outside the fleet's members is refused. Overrides the cluster arg." },
"cluster": { "type": "string", "description": "AWS only. ECS cluster (defaults to the server's configured cluster)." },
Expand Down Expand Up @@ -737,6 +738,11 @@ impl OabMcp {
// studio#128 made it caller-controlled.
acp_enabled: args.get("acp_enabled").and_then(Value::as_bool).unwrap_or(true),
};
// studio#135: Brett's explicit ordering — write local first, S3
// (via provision_agent[_k8s]'s existing upload) after. Optional:
// the console only ever sends this when the operator has a local
// Config folder set at all (its own opt-in setting).
let local_config_folder = args.get("local_config_folder").and_then(Value::as_str);

if args.get("provider").and_then(Value::as_str) == Some("k8s") {
let context = args.get("context").and_then(Value::as_str);
Expand All @@ -749,6 +755,7 @@ impl OabMcp {
image,
input,
expected_principal,
local_config_folder,
)
.await?;
return Ok(json!({
Expand Down Expand Up @@ -779,6 +786,7 @@ impl OabMcp {
name,
image,
input,
local_config_folder,
)
.await?;
Ok(json!({
Expand Down
29 changes: 29 additions & 0 deletions crates/studio-cp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,27 @@ usercron_path = "cronjob.toml"
)
}

/// Mirrors a wizard-generated config.toml into the operator's local "Config
/// folder" (studio#135 — `<folder>/<name>/config.toml`), *before* anything
/// touches S3. Brett's explicit ordering: "Wizard should write to local
/// first, write to s3 if needed" — the only place that ordering can
/// actually be guaranteed is here, inside the sidecar, since the sidecar
/// (not the console) is what does the S3 upload; if the console wrote the
/// local copy itself after the fact (from a config_toml value handed back
/// in the response), S3 would always have already happened first no matter
/// what. Written before `inject_pre_seed_hook` runs — that mutation wires
/// in an S3 zip URI that's meaningless for a local reference copy, so the
/// local file is the clean, human-authored text the operator actually
/// configured, not an ECS/k8s-specific artifact.
fn write_local_agent_config(folder: &str, name: &str, config_toml: &[u8]) -> anyhow::Result<()> {
let dir = std::path::Path::new(folder).join(name);
std::fs::create_dir_all(&dir)
.map_err(|e| anyhow::anyhow!("failed to create {}: {e}", dir.display()))?;
let path = dir.join("config.toml");
std::fs::write(&path, config_toml)
.map_err(|e| anyhow::anyhow!("failed to write {}: {e}", path.display()))
}

/// [`provision_from_library`], but for the studio#128 wizard's direct path —
/// no compose library, no `template ⊕ overlay`: builds config.toml itself
/// from [`AgentWizardInput`] (via [`generate_agent_config`]) instead of
Expand All @@ -1432,9 +1453,13 @@ pub async fn provision_agent(
name: &str,
image: &str,
input: AgentWizardInput,
local_config_folder: Option<&str>,
) -> anyhow::Result<ProvisionOutcome> {
provision_agent_secrets(aws_config, namespace, name, &input).await?;
let config_toml = generate_agent_config(namespace, name, &input).into_bytes();
if let Some(folder) = local_config_folder {
write_local_agent_config(folder, name, &config_toml)?;
}
let mut bundle = studio_compose::Bundle {
image_tag: image.to_string(),
files: std::collections::BTreeMap::from([("config.toml".to_string(), config_toml)]),
Expand Down Expand Up @@ -1519,6 +1544,7 @@ pub async fn provision_agent_k8s(
image: &str,
input: AgentWizardInput,
expected_principal: Option<&str>,
local_config_folder: Option<&str>,
) -> anyhow::Result<ProvisionOutcome> {
if input.chat_platform.is_some() {
anyhow::bail!(
Expand All @@ -1529,6 +1555,9 @@ pub async fn provision_agent_k8s(
}
provision_agent_secrets(aws_config, namespace, name, &input).await?;
let config_toml = generate_agent_config(namespace, name, &input).into_bytes();
if let Some(folder) = local_config_folder {
write_local_agent_config(folder, name, &config_toml)?;
}
let mut bundle = studio_compose::Bundle {
image_tag: image.to_string(),
files: std::collections::BTreeMap::from([("config.toml".to_string(), config_toml)]),
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ async fn deploy_provision_agent(
chat_bot_token: Option<String>,
chat_channel_secret: Option<String>,
acp_enabled: Option<bool>,
local_config_folder: Option<String>,
cluster: Option<String>,
provider: Option<String>,
context: Option<String>,
Expand Down Expand Up @@ -229,6 +230,9 @@ async fn deploy_provision_agent(
if let Some(a) = acp_enabled {
params["acp_enabled"] = json!(a);
}
if let Some(f) = local_config_folder.filter(|s| !s.is_empty()) {
params["local_config_folder"] = json!(f);
}
if let Some(p) = provider.filter(|s| !s.is_empty()) {
params["provider"] = json!(p);
}
Expand Down
Loading