diff --git a/reachpad/src/api.rs b/reachpad/src/api.rs index 6f70502..e6973c4 100644 --- a/reachpad/src/api.rs +++ b/reachpad/src/api.rs @@ -1200,11 +1200,12 @@ impl Client { pub async fn fork( &self, workspace: &str, - biscuit_b64: &str, + auth: Auth<'_>, snapshot_id: Option<&str>, name: Option<&str>, ) -> Result { - let mut req = json!({ "biscuit": biscuit_b64 }); + let (bearer, biscuit) = auth.split(); + let mut req = json!({ "biscuit": biscuit.unwrap_or_default() }); if let Some(id) = snapshot_id { req["snapshot_id"] = json!(id); } @@ -1212,9 +1213,10 @@ impl Client { req["name"] = json!(n); } let body = self - .post( + .post_auth( &format!("/v1/workspaces/{}/fork", encode_segment(workspace)), req, + bearer, ) .await?; Ok(Forked { diff --git a/reachpad/src/cli.rs b/reachpad/src/cli.rs index 8672304..ae8b112 100644 --- a/reachpad/src/cli.rs +++ b/reachpad/src/cli.rs @@ -41,7 +41,9 @@ pub struct Cli { /// An API key (`rpak1.…`) instead of your saved credential, for the verbs /// that act on ONE workspace: `status`, `run`, `pause`, `fork`, `archive`, - /// `events`, `ports`. + /// `events`, `ports`. `fork` takes an ACCOUNT-WIDE key only (one minted + /// with no `--workspace` scope): a scoped key's children would be born + /// outside its scope, so the fleet refuses them. /// /// Account-wide verbs — `list`, `budget`, `keys`, `auth` — need your own /// credential. They REFUSE a key rather than quietly falling back to diff --git a/reachpad/src/commands.rs b/reachpad/src/commands.rs index d92c1e4..085de38 100644 --- a/reachpad/src/commands.rs +++ b/reachpad/src/commands.rs @@ -1186,8 +1186,10 @@ async fn fork( "--name names one workspace; with --count the server names them.", )); } - let biscuit = ctx.biscuit(&workspace).await?; - let held = Held::Biscuit(biscuit); + // Either carrier: `--api-key` when one was passed (documented for fork + // since v1, silently ignored until 2026-08-31), the workspace's own + // token otherwise. + let held = ctx.authority(&workspace).await?; let client = ctx.client(); // ONE snapshot for all N children: resolved here, so a fan-out cannot @@ -1204,11 +1206,13 @@ async fn fork( let mut children = Vec::new(); let mut rows = Vec::new(); for _ in 0..count { - let Held::Biscuit(biscuit) = &held else { - unreachable!("fork presents the workspace's own token"); - }; let forked = match client - .fork(&workspace, biscuit, snapshot.as_deref(), name.as_deref()) + .fork( + &workspace, + held.auth(), + snapshot.as_deref(), + name.as_deref(), + ) .await { Ok(forked) => forked, diff --git a/reachpad/src/errors.rs b/reachpad/src/errors.rs index 3c77a4e..5432281 100644 --- a/reachpad/src/errors.rs +++ b/reachpad/src/errors.rs @@ -348,6 +348,15 @@ pub const TABLE: &[Row] = &[ exit_code: EXIT_CREDENTIAL, retriable: Retriable::No, }, + Row { + code: "api_key_scoped_cannot_fork", + selector: None, + sentence: "A workspace-scoped key cannot fork: the child would be outside its scope. Mint an account-wide key (`reachpad keys mint` with no `--workspace`) or use your signed-in credential.", + numbers: None, + next_command: Some("reachpad keys mint"), + exit_code: EXIT_CREDENTIAL, + retriable: Retriable::No, + }, Row { code: "api_key_lookup_failed", selector: None,