Skip to content

Commit a7aa90d

Browse files
committed
feat(sandbox): write agent output to the container log
Since #2726 the canonical main process's stdout and stderr are captured in pipes that feed only the in-memory replay buffer used by sandbox connect. Agent output therefore never reaches the container's own stdout and stderr, so it is missing from kubectl logs, docker logs, and podman logs and from anything that collects container logs. Before #2726 the entrypoint inherited the container's descriptors and its output appeared there. Copy the main process's output to the launcher's stdout and stderr in addition to the replay buffer, restoring the earlier behavior: - Output is copied byte for byte to the matching stream from a forwarder thread per stream, after it is published to the replay buffer. When the container runtime falls behind on a stream, that stream's reader waits instead of dropping output, so backpressure reaches the agent as it did with inherited descriptors, while the other stream and attachments keep receiving output. - Before the main process's exit is published, the output readers finish and queued output is drained to the container log, so an agent's final lines are not lost at shutdown. A 30 second deadline covers both; when it expires, readers waiting on the container log are released and drain the pipes into the replay buffer only, so a stalled container log cannot block exit reporting. - PTY-mode processes are not copied. The terminal stream carries escape sequences and echoed input, and terminal commands never reached the container log before #2726. - Exec, SSH, and SFTP sessions are not copied. Launcher log lines keep their existing format and remain in the container's stderr. They are written as whole lines, and a newline is inserted first when the agent left stderr mid-line, so launcher and agent lines do not merge. The Docker driver appended the tail of the workload container's log to failure messages, which land in the sandbox's Ready condition and in platform events that the gateway republishes to the sandbox event stream. With agent output in that log, those messages would carry arbitrary agent output, including anything sensitive the agent prints, into gateway status and events. The supervisor starts its health endpoint only after the agent starts, so every failure path could include agent output. Forward only the supervisor container's log tail, matching the Podman driver, which reads the workload log solely to match fixed launcher markers and never forwards raw workload output. The workload container's log remains available through docker logs. Document where main process output appears in the logging docs and the cluster debugging skill. Closes #3928 Signed-off-by: Kris Hicks <khicks@nvidia.com>
1 parent 7caff12 commit a7aa90d

8 files changed

Lines changed: 681 additions & 32 deletions

File tree

‎crates/openshell-driver-docker/src/lib.rs‎

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5302,12 +5302,6 @@ async fn spawn_docker_control_process(
53025302
if !log_tail.is_empty() {
53035303
write!(message, "; log tail: {log_tail}").ok();
53045304
}
5305-
let sandbox_log_tail =
5306-
docker_container_log_tail(&monitored_docker, &failure_context.container_id)
5307-
.await;
5308-
if !sandbox_log_tail.is_empty() {
5309-
write!(message, "; sandbox log tail: {sandbox_log_tail}").ok();
5310-
}
53115305
let _ = monitored_docker.remove_container(
53125306
&monitored_supervisor_id,
53135307
Some(RemoveContainerOptionsBuilder::default().force(true).build()),
@@ -5358,11 +5352,9 @@ async fn wait_for_docker_supervisor_ready(
53585352
Status::internal(format!("inspect Docker sandbox container: {error}"))
53595353
})?;
53605354
if sandbox.state.unwrap_or_default().running == Some(false) {
5361-
let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await;
5362-
return Err(Status::unavailable(format!(
5363-
"Docker sandbox exited before supervisor became ready{}",
5364-
format_named_log_tail("sandbox log tail", &sandbox_log_tail)
5365-
)));
5355+
return Err(Status::unavailable(
5356+
"Docker sandbox exited before supervisor became ready",
5357+
));
53665358
}
53675359
let inspected = docker
53685360
.inspect_container(supervisor_id, None)
@@ -5375,11 +5367,9 @@ async fn wait_for_docker_supervisor_ready(
53755367
Some(HealthStatusEnum::HEALTHY) => return Ok(()),
53765368
_ if state.running == Some(false) => {
53775369
let log_tail = docker_container_log_tail(docker, supervisor_id).await;
5378-
let sandbox_log_tail = docker_container_log_tail(docker, sandbox_id).await;
53795370
return Err(Status::unavailable(format!(
5380-
"Docker supervisor exited before becoming ready{}{}",
5381-
format_log_tail(&log_tail),
5382-
format_named_log_tail("sandbox log tail", &sandbox_log_tail)
5371+
"Docker supervisor exited before becoming ready{}",
5372+
format_log_tail(&log_tail)
53835373
)));
53845374
}
53855375
_ => tokio::time::sleep(Duration::from_millis(100)).await,
@@ -5388,14 +5378,10 @@ async fn wait_for_docker_supervisor_ready(
53885378
}
53895379

53905380
fn format_log_tail(log_tail: &str) -> String {
5391-
format_named_log_tail("log tail", log_tail)
5392-
}
5393-
5394-
fn format_named_log_tail(label: &str, log_tail: &str) -> String {
53955381
if log_tail.is_empty() {
53965382
String::new()
53975383
} else {
5398-
format!("; {label}: {log_tail}")
5384+
format!("; log tail: {log_tail}")
53995385
}
54005386
}
54015387

0 commit comments

Comments
 (0)