Fix backend-switch AWS auth + add /workspace backup and terminal file drag-and-drop #8
@@ -206,6 +206,7 @@ pub async fn download_container_backup(
|
|||||||
// so secrets can't leak through the sanitization fallback.
|
// so secrets can't leak through the sanitization fallback.
|
||||||
let script = r#"set -e
|
let script = r#"set -e
|
||||||
STAGE=$(mktemp -d)
|
STAGE=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
mkdir -p "$STAGE/home-claude"
|
mkdir -p "$STAGE/home-claude"
|
||||||
if [ -f "$HOME/.claude.json" ]; then
|
if [ -f "$HOME/.claude.json" ]; then
|
||||||
if ! jq 'del(.primaryApiKey, .oauthAccount, .customApiKeyResponses)' "$HOME/.claude.json" \
|
if ! jq 'del(.primaryApiKey, .oauthAccount, .customApiKeyResponses)' "$HOME/.claude.json" \
|
||||||
@@ -221,8 +222,7 @@ fi
|
|||||||
tar czf - --ignore-failed-read \
|
tar czf - --ignore-failed-read \
|
||||||
--exclude='*/node_modules' --exclude='*/target' \
|
--exclude='*/node_modules' --exclude='*/target' \
|
||||||
-C "$TC_BACKUP_SRC" . \
|
-C "$TC_BACKUP_SRC" . \
|
||||||
-C "$STAGE" home-claude
|
-C "$STAGE" home-claude"#;
|
||||||
rm -rf "$STAGE""#;
|
|
||||||
|
|
||||||
let cmd = vec!["sh".to_string(), "-c".to_string(), script.to_string()];
|
let cmd = vec!["sh".to_string(), "-c".to_string(), script.to_string()];
|
||||||
|
|
||||||
@@ -291,17 +291,15 @@ rm -rf "$STAGE""#;
|
|||||||
|
|
||||||
// The tar pipeline can abort mid-stream (producing a truncated archive) and
|
// The tar pipeline can abort mid-stream (producing a truncated archive) and
|
||||||
// still have sent bytes, so a non-zero exit must be treated as failure even
|
// still have sent bytes, so a non-zero exit must be treated as failure even
|
||||||
// when `total > 0`.
|
// when `total > 0`. Poll until the exec actually reports finished so the
|
||||||
let exit_code = docker
|
// exit code is reliably populated; if it can't be determined we fall back to
|
||||||
.inspect_exec(&exec.id)
|
// the `total == 0` check below.
|
||||||
.await
|
let exit_code = crate::docker::exec::wait_for_exec_exit(&exec.id).await;
|
||||||
.map(|i| i.exit_code.unwrap_or(0))
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
if stream_err.is_none() && exit_code != 0 {
|
if stream_err.is_none() && exit_code.is_some_and(|c| c != 0) {
|
||||||
stream_err = Some(format!(
|
stream_err = Some(format!(
|
||||||
"Backup command failed (exit {}){}",
|
"Backup command failed (exit {}){}",
|
||||||
exit_code,
|
exit_code.unwrap_or(-1),
|
||||||
if stderr_text.trim().is_empty() {
|
if stderr_text.trim().is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ fn compute_bedrock_fingerprint(project: &Project, global_aws: &GlobalAwsSettings
|
|||||||
// NOTE: the static credential fields (access key / secret / session
|
// NOTE: the static credential fields (access key / secret / session
|
||||||
// token) are intentionally NOT part of the fingerprint. They are
|
// token) are intentionally NOT part of the fingerprint. They are
|
||||||
// written to ~/.aws/credentials on every start by
|
// written to ~/.aws/credentials on every start by
|
||||||
// write_bedrock_static_credentials(), so a key rotation should refresh
|
// sync_bedrock_credentials(), so a key rotation should refresh
|
||||||
// in place rather than force a full container recreation. Region,
|
// in place rather than force a full container recreation. Region,
|
||||||
// profile, and bearer token remain env-based and so stay here.
|
// profile, and bearer token remain env-based and so stay here.
|
||||||
let parts = vec![
|
let parts = vec![
|
||||||
@@ -674,7 +674,7 @@ pub async fn create_container(
|
|||||||
BedrockAuthMethod::StaticCredentials => {
|
BedrockAuthMethod::StaticCredentials => {
|
||||||
// Static/session credentials are NOT injected as env vars.
|
// Static/session credentials are NOT injected as env vars.
|
||||||
// They are written to ~/.aws/credentials by
|
// They are written to ~/.aws/credentials by
|
||||||
// write_bedrock_static_credentials() on every container
|
// sync_bedrock_credentials() on every container
|
||||||
// start, so rotated/updated keys are picked up without a
|
// start, so rotated/updated keys are picked up without a
|
||||||
// full container recreation (and never get baked into the
|
// full container recreation (and never get baked into the
|
||||||
// snapshot image). The empty values set by the
|
// snapshot image). The empty values set by the
|
||||||
@@ -934,7 +934,19 @@ pub async fn create_container(
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
if should_mount_aws || aws_config_path.is_some() {
|
// For static-credential Bedrock, sync_bedrock_credentials() is the sole
|
||||||
|
// owner of ~/.aws/credentials (it rewrites it on every start). Mounting the
|
||||||
|
// host AWS dir would make the entrypoint's `rm -rf ~/.aws; cp -a` race that
|
||||||
|
// write at startup, so we never mount it in that case — the static keys
|
||||||
|
// (+ AWS_REGION env) are self-sufficient and don't need the host config.
|
||||||
|
let is_bedrock_static = project.backend == Backend::Bedrock
|
||||||
|
&& project
|
||||||
|
.bedrock_config
|
||||||
|
.as_ref()
|
||||||
|
.map(|b| b.auth_method == BedrockAuthMethod::StaticCredentials)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if (should_mount_aws || aws_config_path.is_some()) && !is_bedrock_static {
|
||||||
let aws_dir = aws_config_path
|
let aws_dir = aws_config_path
|
||||||
.map(|p| std::path::PathBuf::from(p))
|
.map(|p| std::path::PathBuf::from(p))
|
||||||
.or_else(|| dirs::home_dir().map(|h| h.join(".aws")));
|
.or_else(|| dirs::home_dir().map(|h| h.join(".aws")));
|
||||||
|
|||||||
@@ -344,14 +344,30 @@ pub async fn exec_oneshot_env_status(
|
|||||||
StartExecResults::Detached => return Err("Exec started in detached mode".to_string()),
|
StartExecResults::Detached => return Err("Exec started in detached mode".to_string()),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit code is only available after the process has finished (the stream above
|
// The output stream draining doesn't strictly guarantee inspect_exec has the
|
||||||
// has drained), so inspect now.
|
// final exit_code populated yet, so poll until the exec reports finished.
|
||||||
let exit_code = docker
|
let exit_code = wait_for_exec_exit(&exec.id).await.unwrap_or(0);
|
||||||
.inspect_exec(&exec.id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to inspect exec: {}", e))?
|
|
||||||
.exit_code
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
Ok((combined, exit_code))
|
Ok((combined, exit_code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Poll `inspect_exec` until the exec reports finished and return its exit code.
|
||||||
|
/// Returns `None` if the code can't be determined (inspect error, or the exec
|
||||||
|
/// doesn't report finished within ~1s — which shouldn't happen once its output
|
||||||
|
/// stream has drained).
|
||||||
|
pub async fn wait_for_exec_exit(exec_id: &str) -> Option<i64> {
|
||||||
|
let docker = get_docker().ok()?;
|
||||||
|
for _ in 0..40 {
|
||||||
|
match docker.inspect_exec(exec_id).await {
|
||||||
|
Ok(info) => {
|
||||||
|
if info.running != Some(true) {
|
||||||
|
// Finished: use the reported code (default 0 if somehow absent).
|
||||||
|
return Some(info.exit_code.unwrap_or(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|||||||
@@ -231,7 +231,9 @@ if [ -n "$AWS_SSO_AUTH_REFRESH_CMD" ]; then
|
|||||||
chown claude:claude "$CLAUDE_JSON"
|
chown claude:claude "$CLAUDE_JSON"
|
||||||
chmod 600 "$CLAUDE_JSON"
|
chmod 600 "$CLAUDE_JSON"
|
||||||
unset AWS_SSO_AUTH_REFRESH_CMD
|
unset AWS_SSO_AUTH_REFRESH_CMD
|
||||||
elif [ -f "$CLAUDE_JSON" ]; then
|
elif [ -f "$CLAUDE_JSON" ] && grep -q '"awsAuthRefresh"' "$CLAUDE_JSON" 2>/dev/null; then
|
||||||
|
# Only rewrite when the key is actually present, to avoid a needless jq
|
||||||
|
# reformat of ~/.claude.json on every start of a non-SSO backend.
|
||||||
MERGED=$(jq 'del(.awsAuthRefresh)' "$CLAUDE_JSON" 2>/dev/null)
|
MERGED=$(jq 'del(.awsAuthRefresh)' "$CLAUDE_JSON" 2>/dev/null)
|
||||||
if [ -n "$MERGED" ]; then
|
if [ -n "$MERGED" ]; then
|
||||||
printf '%s\n' "$MERGED" > "$CLAUDE_JSON"
|
printf '%s\n' "$MERGED" > "$CLAUDE_JSON"
|
||||||
|
|||||||
Reference in New Issue
Block a user