Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/th-config-adhoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@smooai/smooth': patch
---

`th config set --adhoc` — ad-hoc writes for keys not declared in any org config schema.

The server has always supported `"adhoc": true` for undeclared keys (its 400 error even prescribes it), but the CLI never sent it — so release-tooling secrets (match passphrase, ASC API key material) couldn't be stored in `@smooai/config` without first inventing a consumer schema for them. `--adhoc` requires an explicit `--tier` (clap-enforced), so an ad-hoc write states its sensitivity out loud instead of inheriting a default. First use: storing the Big Smooth TestFlight signing secrets in the Smoo AI prod org.
21 changes: 18 additions & 3 deletions crates/smooth-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ pub enum Cmd {
/// value.
#[arg(long)]
force: bool,
/// Assert an AD-HOC write for a key not declared in any of the
/// org's config schemas (release tooling secrets, one-off ops
/// values). The server refuses undeclared keys without this;
/// requires an explicit `--tier` so the write states its
/// sensitivity out loud rather than inheriting a default.
#[arg(long, requires = "tier")]
adhoc: bool,
},
/// List all config values for an environment as a key→value map.
List {
Expand Down Expand Up @@ -654,7 +661,8 @@ pub async fn cmd(cmd: Cmd) -> Result<()> {
reveal,
m2m,
force,
} => cmd_set(key, value, environment, org_id, tier, schema_name, json, reveal, m2m, force).await,
adhoc,
} => cmd_set(key, value, environment, org_id, tier, schema_name, json, reveal, m2m, force, adhoc).await,
Cmd::List {
environment,
org_id,
Expand Down Expand Up @@ -909,6 +917,7 @@ async fn cmd_set(
reveal: bool,
m2m: bool,
force: bool,
adhoc: bool,
) -> Result<()> {
let cfg = ConfigClient::load(m2m).await?;
let org = cfg.resolve_org(org_id)?;
Expand Down Expand Up @@ -982,13 +991,19 @@ async fn cmd_set(
let parsed_value = serde_json::from_str::<Value>(&value).unwrap_or_else(|_| Value::String(value.clone()));

let tier_wire = resolved_tier.as_wire();
let body = serde_json::json!({
let mut body = serde_json::json!({
"schemaId": schema_id,
"environmentId": env_id,
"key": key,
"value": parsed_value,
"tier": tier_wire,
});
// AD-HOC write (key declared in no schema): the server refuses undeclared
// keys unless asserted explicitly (ADR-075's loud-failure default); clap
// already forced an explicit --tier alongside --adhoc.
if adhoc {
body["adhoc"] = serde_json::Value::Bool(true);
}
let resp = cfg
.put(&format!("/organizations/{org}/config/values"), &body)
.await
Expand Down Expand Up @@ -1176,7 +1191,7 @@ async fn cmd_limits(cmd: LimitsCmd) -> Result<()> {
// Reuse the shared value-write path with the tier pinned to Limit.
// `reveal=true` because limit values are non-sensitive numbers —
// masking a number to `**` would be pure noise.
cmd_set(key, value, environment, org_id, Some(Tier::Limit), schema_name, json, true, m2m, false).await
cmd_set(key, value, environment, org_id, Some(Tier::Limit), schema_name, json, true, m2m, false, false).await
}
}
}
Expand Down
Loading