Compare commits
1
Commits
v0.4.31
...
v0.4.32-win
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292fc907fb |
@@ -106,6 +106,16 @@ const CODE_REJECTED_EVENT: &str = "claude-token-code-rejected";
|
|||||||
/// browser, sign in, and approve. Bounded so a wedged exec can't leak a task.
|
/// browser, sign in, and approve. Bounded so a wedged exec can't leak a task.
|
||||||
const SETUP_TIMEOUT: Duration = Duration::from_secs(15 * 60);
|
const SETUP_TIMEOUT: Duration = Duration::from_secs(15 * 60);
|
||||||
|
|
||||||
|
/// Pause between typing the pasted code and pressing Enter.
|
||||||
|
///
|
||||||
|
/// The CLI's prompt reads a multi-byte chunk as a paste, and a `\r` inside a
|
||||||
|
/// paste is swallowed with it rather than submitting. Code and Enter in one
|
||||||
|
/// write therefore fill the prompt and submit nothing, and the flow waits out
|
||||||
|
/// [`SETUP_TIMEOUT`]. Measured against 2.1.283 under a pty: 20 ms apart
|
||||||
|
/// already submits reliably; this leaves headroom for the extra hops through
|
||||||
|
/// Docker's exec socket, which can merge writes that arrive close together.
|
||||||
|
const SUBMIT_ENTER_DELAY: Duration = Duration::from_millis(250);
|
||||||
|
|
||||||
/// Documented shape of a `setup-token` credential.
|
/// Documented shape of a `setup-token` credential.
|
||||||
const TOKEN_PREFIX: &str = "sk-ant-oat01-";
|
const TOKEN_PREFIX: &str = "sk-ant-oat01-";
|
||||||
|
|
||||||
@@ -837,9 +847,23 @@ unset CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN ANTHROPIC_B
|
|||||||
ANTHROPIC_MODEL CLAUDE_CODE_USE_BEDROCK AWS_BEARER_TOKEN_BEDROCK
|
ANTHROPIC_MODEL CLAUDE_CODE_USE_BEDROCK AWS_BEARER_TOKEN_BEDROCK
|
||||||
exec claude setup-token"#;
|
exec claude setup-token"#;
|
||||||
|
|
||||||
|
/// Type `code` into the CLI's prompt, then press Enter as a separate keystroke.
|
||||||
|
///
|
||||||
|
/// See [`SUBMIT_ENTER_DELAY`] for why the two cannot share a write.
|
||||||
|
async fn type_code_then_enter<W: tokio::io::AsyncWrite + Unpin>(
|
||||||
|
input: &mut W,
|
||||||
|
code: &[u8],
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
input.write_all(code).await?;
|
||||||
|
input.flush().await?;
|
||||||
|
tokio::time::sleep(SUBMIT_ENTER_DELAY).await;
|
||||||
|
input.write_all(b"\r").await?;
|
||||||
|
input.flush().await
|
||||||
|
}
|
||||||
|
|
||||||
/// Run `claude setup-token` in the container and return the token it printed.
|
/// Run `claude setup-token` in the container and return the token it printed.
|
||||||
/// Streams redacted output as it arrives and forwards anything arriving on
|
/// Streams redacted output as it arrives and forwards anything arriving on
|
||||||
/// `input_rx` (the user's pasted code) to the command's stdin.
|
/// `input_rx` (the user's pasted code) to the command's stdin, each followed by Enter.
|
||||||
async fn run_setup_token(
|
async fn run_setup_token(
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
@@ -894,14 +918,13 @@ async fn run_setup_token(
|
|||||||
"Authentication cancelled. No token was stored.".to_string()
|
"Authentication cancelled. No token was stored.".to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(data) = input_rx.recv() => {
|
Some(code) = input_rx.recv() => {
|
||||||
if let Err(e) = input.write_all(&data).await {
|
if let Err(e) = type_code_then_enter(&mut input, &code).await {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not send the code to `claude setup-token`: {}. No token was stored.",
|
"Could not send the code to `claude setup-token`: {}. No token was stored.",
|
||||||
e
|
e
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let _ = input.flush().await;
|
|
||||||
// Arm the rejection detector. Anything the CLI says from here
|
// Arm the rejection detector. Anything the CLI says from here
|
||||||
// on is a verdict on *this* code.
|
// on is a verdict on *this* code.
|
||||||
awaiting_code_result = true;
|
awaiting_code_result = true;
|
||||||
@@ -1158,10 +1181,9 @@ pub async fn submit_claude_token_code(code: String) -> Result<(), String> {
|
|||||||
.to_string()
|
.to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut keystrokes = code.as_bytes().to_vec();
|
// Just the code: the flow presses Enter itself, as a separate keystroke.
|
||||||
keystrokes.push(b'\r');
|
|
||||||
sender
|
sender
|
||||||
.send(keystrokes)
|
.send(code.as_bytes().to_vec())
|
||||||
.map_err(|_| "The authentication flow has already ended.".to_string())
|
.map_err(|_| "The authentication flow has already ended.".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2229,4 +2251,50 @@ mod tests {
|
|||||||
assert_eq!(outcome.snapshots_skipped.len(), 1, "{:?}", outcome);
|
assert_eq!(outcome.snapshots_skipped.len(), 1, "{:?}", outcome);
|
||||||
assert!(outcome.snapshots_failed.is_empty(), "{:?}", outcome);
|
assert!(outcome.snapshots_failed.is_empty(), "{:?}", outcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records every `poll_write` as a separate entry, with when it landed, so
|
||||||
|
/// a test can see write boundaries that a byte pipe would merge.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RecordingWriter {
|
||||||
|
writes: Vec<(tokio::time::Instant, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tokio::io::AsyncWrite for RecordingWriter {
|
||||||
|
fn poll_write(
|
||||||
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
|
_cx: &mut std::task::Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> std::task::Poll<std::io::Result<usize>> {
|
||||||
|
self.writes
|
||||||
|
.push((tokio::time::Instant::now(), buf.to_vec()));
|
||||||
|
std::task::Poll::Ready(Ok(buf.len()))
|
||||||
|
}
|
||||||
|
fn poll_flush(
|
||||||
|
self: std::pin::Pin<&mut Self>,
|
||||||
|
_cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<std::io::Result<()>> {
|
||||||
|
std::task::Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
fn poll_shutdown(
|
||||||
|
self: std::pin::Pin<&mut Self>,
|
||||||
|
_cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<std::io::Result<()>> {
|
||||||
|
std::task::Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Code and Enter in one write is read by the CLI as a paste: the code
|
||||||
|
/// fills the prompt and the `\r` is swallowed with it, so nothing is
|
||||||
|
/// submitted and the flow sits until `SETUP_TIMEOUT`. Measured against
|
||||||
|
/// 2.1.283 under a pty; a separate Enter 20 ms later submits.
|
||||||
|
#[tokio::test(start_paused = true)]
|
||||||
|
async fn the_code_and_its_enter_are_separate_writes() {
|
||||||
|
let mut w = RecordingWriter::default();
|
||||||
|
type_code_then_enter(&mut w, b"abc#def").await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(w.writes.len(), 2, "expected two writes, got {:?}", w.writes);
|
||||||
|
assert_eq!(w.writes[0].1, b"abc#def");
|
||||||
|
assert_eq!(w.writes[1].1, b"\r");
|
||||||
|
assert!(w.writes[1].0 - w.writes[0].0 >= SUBMIT_ENTER_DELAY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user