Repository navigation
feat(agent-sdk): run assigned tasks in pre-warmed sandboxes - #16539
Conversation
|
@captainsafia, Warp CI failed on this PR's current head. May windows runner test diagnose it, commit fixes, and push to this same PR branch? Your consent also opts this PR into automatic handling of future CI failures. Please reply to this request mentioning @warp with explicit permission. We will not change the branch without it. Responding as windows runner test: Open session · View in factory |
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR adds the client-side pre-warmed sandbox assignment flow, including control-socket assignment handling, in-place driver/shell/MCP environment replacement, and supporting terminal/standby tests.
Concerns
- The planned shell replacement failure path in
TerminalManagerstill invokes the old cloud-shell recovery failure callback before branching to the new replacement callback, which consumes theanyhow::Errorand prevents the file from compiling.
Verdict
Found: 1 critical, 0 important, 0 suggestions
Request changes
Comment /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
…16542) ## Description Keep standby GraphQL credentials separate from run credentials. Session-authenticated standby requests carry the sandbox ID and pre-warmed workload token, not an ambient token without a run ID. **Merge and deploy [server #20185](warpdotdev/warp-server#20185) first.** This client requires its standby middleware branch. Startup selects standby identity from `--standby-id` before authenticated background requests. Both identity header names are reserved against configured overrides. The normal run-ID setter leaves standby mode, so the separate [assignment client PR](#16539) can reuse its existing handoff. Workload-token issuance and caching remain shared. ## Linked Issue [REMOTE-3568](https://linear.app/warpdotdev/issue/REMOTE-3568/authenticate-pre-warmed-standby-graphql-requests-with-sandbox-identity) - [ ] The tracking issue is in progress in Linear; this task does not use the GitHub ready-to-implement label. - [ ] No rendered UI changes; staging validation follows both deployments. ## Testing - [x] `cargo nextest run -p warp_server_client --no-fail-fast`: 84 passed. - [x] `cargo nextest run -p warp --no-fail-fast prewarmed`: 14 passed. - [x] `cargo check -p warp --lib` - [x] `cargo clippy -p warp_server_client --all-targets --tests -- -D warnings` and the same command for `-p warp`. - [x] `./script/format` - [ ] `./script/presubmit`: all format/lint checks passed, including workspace and default-app Clippy. Workspace nextest failed on sandbox integration paths: gcloud/IAP credentials, foreground-window recording, and unrelated pager/UI assertions. It did not reach doc tests. An initial attempt also exhausted the Cargo volume; a retry disabled incremental caching after removing generated caches. - [ ] I have manually tested my changes locally with `./script/run`: not run; authentication is tested at the transport boundary. Unit tests use `WARP_ISOLATION_PLATFORM=docker` with a generated synthetic `WARP_WORKLOAD_TOKEN` to avoid the sandbox's unavailable Namespace issuer. Header tests cover exclusive standby credentials, bootstrap exclusion, reserved-header filtering and the transition back to run credentials. After both changes deploy, verify the staging standby reaches `ready`, heartbeats advance, and no sandbox retires with `provider stopped`. This end-to-end check is pending, not a local test result. CI is intentionally not green while this PR is a draft: `.github/workflows/ci.yml` skips `params` on drafts, and the aggregate job rejects that skip. Marking ready must run the real CI jobs before merge. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-NONE <!-- warp:pr-description-artifacts start --> _Plans_: - _[Standby GraphQL credential isolation](https://staging.warp.dev/drive/notebook/DwxWynuqJiRIxAdv6m1L2D)_ <!-- warp:pr-description-artifacts end --> Co-authored-by: warp-agent-staging[bot] <240773466+warp-agent-staging[bot]@users.noreply.github.com>
65b1a8d to
d2e6494
Compare
Add `Message::ShutdownForReplacement` so the PTY event loop can stop a shell without marking the terminal model exited. The terminal keeps its model and view, and a replacement shell can attach to them. On Windows, `ChildExitWatcher` now owns its callback context and its `Drop` waits for an in-flight exit callback with `UnregisterWaitEx`. Without this, a replaced shell's delayed exit notification could reach the event loop of the shell that replaced it. This applies to every Windows terminal, not only to replaced ones. Co-Authored-By: Warp <agent@warp.dev>
Add `TerminalView::replace_shell`, which restarts the shell behind an existing terminal with a new working directory and environment. It resolves after the replacement shell bootstraps. - `TerminalView` accepts a replacement only for an idle local shell: it refuses while a command runs, the alt screen is active, the terminal is read-only or a shared-session viewer, or a recovery or replacement is already pending. Input stays disabled until the request settles, and a request that does not settle within 75 seconds fails. - The pane routes `Event::ReplaceShell` and `Event::CancelShellReplacement` to the local terminal manager. Other terminal managers refuse. - The local terminal manager stops the old event loop with `ShutdownForReplacement`, then spawns the new shell on the cloud shell-recovery resources. The old shell's input and exit notifications cannot reach the new one. - `TerminalModel::prepare_shell_replacement` keeps the planned exit of the old shell from ending the terminal. Co-Authored-By: Warp <agent@warp.dev>
…ment Add a runtime environment to `TemplatableMCPServerManager` that overrides the static environment of every CLI server it spawns. Values come only from the caller and are never persisted with the installation. `replace_runtime_environment` installs a new runtime environment, stops every running or starting server, and returns the installations to restart together with a completion receiver for each shutdown. CLI-spawned and built-in servers are stopped but not returned; the run's own MCP startup starts them again. WASM keeps the environment and starts nothing. `shutdown_server` now delegates to `shutdown_server_and_wait`, which reports when the server reaches `NotRunning`. Co-Authored-By: Warp <agent@warp.dev>
Replace the HTTP heartbeat with the server's control socket at
`/internal/prewarmed-sandboxes/{id}/ws?generation=`. The socket
authenticates with the bearer credential and the Namespace workload token,
and both headers are marked sensitive.
- The sandbox sends only `heartbeat`. The server sends `assignment`, which
carries the committed execution configuration, runtime environment, run
API key, and run deadline, or `retired` before it closes the socket. The
assignment's `Debug` output redacts the run key and the configuration
values.
- A `410 Gone` handshake is reported through
`is_prewarmed_sandbox_unavailable`, so callers can tell a retired sandbox
or an inactive run from a transient failure.
- The status report sends the generation as `fingerprint`, which is the
field the server binds. It sent `generation` before, so every readiness
report was rejected.
- `testdata/prewarmed_assignment_fresh_run.json` is the server's own
fixture, so the decode test checks what the server actually sends,
including the `factoryBranch` and `expectedSha` fields.
Co-Authored-By: Warp <agent@warp.dev>
`split_run_options` is now the only code that destructures `AgentDriverOptions`. It returns two values: - `TerminalLaunchInputs`: the options consumed when the terminal session is created, which have no driver state. - `RunConfiguration`: the driver state, with every optional override resolved to its default and the terminal environment fully built. `AgentDriver::create` builds the terminal, then calls `with_terminal` and `apply_run_configuration`. A driver that already owns a terminal can call `apply_run_configuration` to take a new run in place. A new option field now fails the build until it is handled in one place, instead of in each path that applies options. `build_secret_env_vars` takes the terminal environment that is applied on top of the secrets. Behavior changes for `create`: - A typed auth secret is skipped as a whole when any of its variables is set in `environment_variables`. Before, the environment variable replaced only the colliding name and left the rest of the secret in place. - A raw `WARP_METADATA_*` secret that collides with `environment_variables` is an error, as it already was for the process environment. - Login is checked before options are resolved, so a logged-out caller gets `NotLoggedIn` rather than a configuration error. Co-Authored-By: Warp <agent@warp.dev>
A pre-warmed assignment arrives as an execution configuration, with no `RunAgentArgs`. These run-setup steps now accept that case, so the assignment path uses the same code as `agent run` instead of a copy: - `build_execution_task_and_options` takes `Option<&RunAgentArgs>`. Without arguments, the task and execution IDs come from the configuration, and the harness transport, snapshot timeouts, and MCP startup settings use their defaults. - `configure_bedrock_oidc_credentials` switches the run to OIDC-federated AWS credentials and waits for the first set. `agent run` now calls it in place of its inline copy. - `bootstrap_git_credentials_for_task` and `set_ambient_agent_task_id` are generic over the calling entity. Git bootstrap takes the `--configure-git-credentials-with-github` value instead of the whole arguments struct. `agent run` keeps its behavior. Co-Authored-By: Warp <agent@warp.dev>
Replace the driver's `Option<WorkspaceConfiguration>` with `WorkspaceState`, which records whether the workspace is pending or was already prepared by a standby sandbox. - `prepare_resources` leaves `WorkspaceState::Prepared`, which keeps the prepared configuration and the repositories to scan for skills. - `AgentDriver::run` matches on the state once. A prepared workspace skips the global skill clone, the Factory definition clone, environment preparation, and the file-based MCP scan, and it reuses the harness directory recorded at preparation. A pending workspace keeps the existing setup unchanged. - `apply_run_configuration` keeps a prepared workspace in place. - `WorkspaceConfiguration::same_preparation` compares the source repositories, setup commands, and clone requests, so a caller can check that a run asks for exactly what was prepared before it reuses it. Co-Authored-By: Warp <agent@warp.dev>
Add `AgentDriver::prepare_prewarmed_assignment`, which turns the standby driver into the driver for one assigned run, in the same process and terminal. The server commits the run before it delivers the assignment, so the sandbox cannot hand a run back. The run's own credential is installed first, and any later installation failure is reported against the run with `report_driver_error`. Installation, in order: 1. Install the run API key and the ambient task ID. 2. Check that the assignment is an Oz run the client can prepare. Cloud providers are accepted, because the server assigns runs that use them. 3. Bootstrap Git credentials, refresh workspace metadata, and resolve configured skills through the shared run-setup helpers. 4. Build the run options with `build_execution_task_and_options`, and refuse a workspace that differs from what standby prepared. 5. Load task secrets, attachments, cloud providers, and Bedrock credentials as `agent run` does. 6. Apply the options with `split_run_options` and `apply_run_configuration`, then replace the shell and the MCP runtime environment with the run's environment. 7. Bind the terminal to the task and share the session when requested. Supporting changes: - `TerminalDriver::bind_prewarmed_task` sets the window team, the controller's task and attachments directory, and the shared-session source, then starts sharing. The share senders move onto `TerminalDriver`, so a binding after creation can still resolve them. - `TerminalDriver::replace_shell` wraps the terminal view's replacement. - `replace_prewarmed_mcp_environment` restarts retained MCP servers with the run environment and waits for them to start. - `sandbox_deadline` becomes a driver field, so the run's deadline replaces the standby deadline for the shutdown timer and MCP headers. - `UserWorkspaces::set_window_team_uid` accepts an optional team, so a personal run can clear the standby window's team. Co-Authored-By: Warp <agent@warp.dev>
A standby sandbox used to prepare, heartbeat over HTTP, and exit at its deadline. It now waits on the control socket for the server to commit a run to it, installs that run, and runs it in the same process. The lifecycle has three phases: - Standby: prepare, report readiness, and heartbeat until an assignment arrives. An assignment that arrives before preparation finishes is held until it does, because the ready report's response can race the assignment frame. A replay of the same assignment is ignored, and a different one is an error. `retired`, or the standby deadline, exits cleanly. - Install: drop the standby socket and install the assignment, bounded by the run deadline. - Confirm delivery: reconnect with the run's own credential. The server admits that connection, revokes the standby credential, and replays the assignment; the replay confirms delivery. `410 Gone` or `retired` means the run is no longer active, so the sandbox exits without starting it. Transient failures retry for up to 5 minutes, so an unreachable server fails the run promptly instead of holding it until its deadline. The run starts only after delivery is confirmed, so a cancelled or superseded run never starts. The test fixtures move to `standby_test_support.rs`. Co-Authored-By: Warp <agent@warp.dev>
…n ID The assignment ID was redundant, because a sandbox gets at most one assignment and an execution gets at most one sandbox. The standby driver now dedupes and confirms replayed assignments on execution_config.execution_id. Co-Authored-By: Warp <agent@warp.dev>
d2e6494 to
96319d8
Compare
vorporeal
left a comment
There was a problem hiding this comment.
read through all of the commits, and had an agent read the full PR, leave some comments from its own findings, and leave some more from me asking it questions as i read.
approving to unblock, but i think there's a bunch of stuff in here that's worth doing (whether it's in this PR or in a follow-up is up to you).
| let pending = self.pending_shell_recovery.take(); | ||
| self.view.update(ctx, |surface, ctx| { | ||
| surface.on_cloud_shell_recovery_failed(failure_class, error, ctx); | ||
| if let Some(request_id) = pending.and_then(|pending| pending.planned_request_id) { |
There was a problem hiding this comment.
oops created this so there was a pending review that the agent could attach additional comments to and then i forgot to delete it
| anyhow::ensure!( | ||
| !cfg!(windows), | ||
| "shell replacement is not supported on Windows" | ||
| ); |
There was a problem hiding this comment.
i assume this is more of a TODO than a "we cannot support this"? (otherwise, we're saying that we can't do pre-warmed sandboxes on windows, right?)
| restored_working_directory: String, | ||
| used_fallback_directory: bool, | ||
| replacement_session_id: SessionId, | ||
| planned_request_id: Option<Uuid>, |
There was a problem hiding this comment.
this isn't associated with any sort of "request", right? should this be planned_replacement_id?
| } | ||
|
|
||
| /// A shell replacement that has not finished bootstrapping. | ||
| struct PendingShellRecovery { |
There was a problem hiding this comment.
might be worth a quick rename of this, given we're now using it for more than just recovery. perhaps PendingShellReplacement? (or PendingShellRespawn?)
| return Ok(None); | ||
| }; | ||
| let message = message.context("Pre-warmed sandbox control stream failed")?; | ||
| let text = message |
There was a problem hiding this comment.
[agent] The server pings this socket every 54s (socketPingPeriod in warp-server logic/ai/ambient_agents/prewarmed/socket.go, gorilla/websocket). tungstenite 0.24 queues the pong and also returns Message::Ping to the stream consumer, so this text() check turns every server ping into an error. maintain_standby_connection then drops the socket and reconnects after 1s. So every standby sandbox tears down and reopens its socket about once a minute for its whole life, re-running auth each time, with nothing logged.
It also stretches the heartbeat cadence to ~55s, because the 30s heartbeat timer restarts on each new connection. The server only treats a sandbox as an assignment candidate if it heartbeated within 90s (candidateHeartbeatWindow in assignment.go), so a single missed heartbeat at that cadence makes the sandbox temporarily unassignable.
Delivery confirmation is not affected, since the server replays the assignment immediately on connect, well before the first ping.
Could this skip non-text frames instead, e.g. loop with let Some(text) = message.text() else { continue };, the way the shared-session code does in app/src/terminal/shared_session/sharer/network.rs:1521? A Close frame would then end the stream normally on the next read. A test with a fake stream that yields a Ping and then a Text message would cover it.
| let (global_skill_resolution, prepared_workspace) = match workspace | ||
| .ok_or(AgentDriverError::InvalidRuntimeState)? | ||
| { | ||
| // A standby sandbox already cloned the repositories and ran the setup |
There was a problem hiding this comment.
[agent] FYI, not blocking: this branch does no refresh after the assignment arrives (no fetch/checkout of the prepared repos, no Factory definition checkout, and global skills are only resolved, not cloned), so a pre-warmed run works on branch heads as of standby preparation. I understand a post-assignment refresh is planned; two things to keep in mind when it lands:
- Global skill repos are separate checkouts from the workspace repos, so the refresh needs to cover them too, or a global skill repo added/updated after standby will be missing or stale.
- Repos can carry
expected_sha, andsame_preparationcomparesclone_requests, which include it. If the server ever sends an assignment whose pinned SHA differs from what standby prepared, the run (already committed) fails atsame_preparation. A refresh that checks out the expected SHA, withsame_preparationignoring the SHA, would be the more forgiving behavior.
|
|
||
| /// Installs a committed assignment on this standby driver and returns the task to run. | ||
| /// | ||
| /// The server commits the run before delivering it, so the sandbox cannot hand it back and |
There was a problem hiding this comment.
[agent] Question about the overall failure model, since cloud agent reliability is a top priority right now: because the sandbox can't hand a committed run back, every failure between the assignment and the run actually starting turns into a failed run, even though nothing has executed yet. That includes installation failures here (a retained MCP server failing to restart, same_preparation rejecting the workspace, the shell replacement being refused or timing out, secrets or Bedrock setup failing), plus the post-install exits in run_lifecycle (deadline during installation, delivery not confirmed within the window). In all of those cases a fresh cold sandbox would very likely have succeeded.
Could these pre-start failures fall back to a cold start instead of failing the run? This doesn't need an accept/decline step in the protocol. Roughly:
- the sandbox reports "installation failed before the run started" as a distinct outcome (e.g. a specific status or error class on the run, or a socket/HTTP call), using the run key it already holds;
- the server treats that as "release this execution back to pending and retire the sandbox", then dispatches the run cold;
- retries are capped (e.g. at most one pre-warmed attempt per execution, then cold only) so a bad config can't loop.
The task never reaching IN_PROGRESS before these failures seems like a reasonable server-side signal that requeuing is safe. Is this something you've considered, or planned as a follow-up? It would need matching server work in warp-server's prewarmed assignment code.
| type ControlSink = Pin<Box<dyn futures::Sink<websocket::Message, Error = websocket::Error> + Send>>; | ||
| type ControlStream = | ||
| Pin<Box<dyn futures::Stream<Item = Result<websocket::Message, websocket::Error>> + Send>>; |
There was a problem hiding this comment.
nit: you can leverage warpui::r#async::BoxFuture to simplify the types here.
| fn sharing_expected(should_share: bool) -> bool { | ||
| should_share && !warp_core::channel::ChannelState::server_root_url().contains("ngrok") | ||
| } |
There was a problem hiding this comment.
should should_share be overwritten at (or at least closer to) the source based on this ngrok check? helps ensure that all consumers get it right.
| result | ||
| } | ||
|
|
||
| async fn install_assignment( |
There was a problem hiding this comment.
[agent] Installation currently runs every step one after another, but most steps don't depend on each other. Since installation latency is the whole point of pre-warming, it seems worth restructuring this into a few clearly ordered groups, where each group runs in parallel and steps are serialized only where a real dependency exists.
Dependencies (after the run credential and task ID are installed, which should stay first):
| Step | Kind | Needs | Needed by |
|---|---|---|---|
Validate payload; derive team scope, share requests, skill specs, Bedrock role/region, runtime_env (+ key, deadline), Factory contract |
pure/sync | payload | everything |
| Fetch task secrets | network | task ID + key | shell (via build_secret_env_vars) |
| Fetch + download attachments (incl. handoff) | network | task ID + working_dir |
task prompt only |
| Bootstrap Git credentials | network + git config write | task ID | the run itself (nothing in installation) |
| Refresh team metadata (up to 30s of retries) | network | key | building options (conservatively), bind, run |
| Mint Bedrock OIDC credentials | network | task ID + team scope (both known from the payload) | the run |
| Resolve configured skills | local only (clone_repositories = false) |
prepared checkouts | building options |
Build options/task, same_preparation, cloud providers, split_run_options + apply_run_configuration |
sync, main thread | skills, secrets, attachments, Bedrock config | shell, bind |
| Replace shell | up to 75s | resolved_env_vars (depends on secrets) |
sharing |
| Restart retained MCP servers (stop up to 20s, start up to 20s) | process spawn | only runtime_env |
nothing in installation |
| Bind task + start sharing + wait for share | network, up to 20s | team scope; should follow the shell replacement | the run |
The notable ones:
- MCP depends only on
runtime_env, which is known right after parsing the payload. Its one small link iswait_for_mcp_servers_startedreadingme.mcp_startup_timeout, which is the default either way and could be passed explicitly. - All network steps need only the credential. Attachments and Bedrock currently write into
options/taskas they go; they'd need to return results instead. - Sharing should follow the shell replacement, but waiting for the share doesn't need to block the MCP restart.
- The cold path already does this kind of thing:
fetch_secrets_and_attachmentsfetches secrets, attachments, metadata, and the handoff snapshot with onefutures::join!.
Suggested shape:
async fn install_assignment(...) -> Result<Task, AgentDriverError> {
// 0. Pure: validate and derive everything from the payload. No I/O.
let plan = AssignmentPlan::from_payload(config, runtime_env, api_key, expires_at)?;
// The one step with no inputs beyond the payload, and the slowest: start it now, join it last.
let mcp_restart = Self::replace_prewarmed_mcp_environment(spawner, plan.mcp_environment.clone());
// 1. Network: everything that only needs the run credential.
let (secrets, attachments, (), (), bedrock) = futures::try_join!(
fetch_task_secrets(spawner, task_id),
fetch_and_download_attachments(ai_client, server_api, task_id, &attachments_dir),
AgentDriverRunner::bootstrap_git_credentials_for_task(spawner, ..),
AgentDriverRunner::refresh_team_metadata(spawner, true),
mint_bedrock_credentials(spawner, task_id, &plan.team_scope, plan.bedrock),
)?;
// 2. Sync, on the main thread: resolve skills (local), build options/task, check
// same_preparation, load cloud providers, attach secrets/attachments/bedrock, then
// split_run_options + apply_run_configuration.
let (task, should_share) = spawner.spawn(move |me, ctx| me.install_run_configuration(..)).await??;
// 3. Shell replacement (needs resolved_env_vars from step 2).
Self::replace_shell_with_resolved_env(spawner).await?;
// 4. Sharing (after the shell) overlaps with the rest of the MCP restart.
futures::try_join!(Self::bind_and_share(spawner, task_id, plan.share, should_share), mcp_restart)?;
Ok(task)
}If you'd rather keep this strictly "groups, each a join", the MCP restart can go in step 3 next to the shell replacement. That's simpler, but gives up overlapping MCP with the network group. Starting it early seems worth it: it's the only step with no inputs and usually the slowest, and one future started early and joined at the end stays easy to follow.
Roughly, the critical path goes from the sum of everything (five network round trips + shell + MCP stop/start + share wait) to the slowest network call + the shell replacement + whichever is longer of the share wait and the remaining MCP time. That's an estimate from reading the code, not a measurement.
Notes:
- Use
try_join!so the first error cancels the rest; dropping a half-finished MCP restart or shell replacement is fine, since a failed installation ends the process. - I didn't confirm whether building options actually reads team metadata. The shape above doesn't depend on it, because step 2 waits for all of step 1. If it doesn't, the refresh could move off the critical path entirely (joined before bind).
- This pairs with the MCP comment on
mcp_startup.rs: if MCP restart failures degrade instead of failing the run, joining the MCP restart last makes a degraded result just a warning. - Nothing else waits on the MCP manager during installation (the next wait is in
run()), so the overlapping-waits warning onwait_for_mcp_servers_startedisn't an issue, as long as that stays true.
96319d8 to
e86e6f3
Compare
tungstenite hands server pings to the reader, so a ping dropped the socket and the sandbox reconnected about once a minute. Co-Authored-By: Warp <agent@warp.dev>
…start Route the restart result through handle_mcp_startup_result so a failed restart degrades the run as it does in a cold run, unless strict MCP startup is set. Co-Authored-By: Warp <agent@warp.dev>
e86e6f3 to
66af38f
Compare
The test uses the app context only for Unix model events, so Windows Clippy rejected the unused closure parameter. Windows refuses planned shell replacement, so the test has no Windows behavior to cover. Co-Authored-By: Warp <agent@warp.dev>
|
@vorporeal Thanks! I'll take advantage of the approval to unblock being able to test the end-to-end in the next nightly. I've addressed a few of the impact impact items in the PR here and will circle back with the others in follow-ups. |
Description
A pre-warmed standby sandbox can now receive a run from the server, install it in place, and run it. Before this change, a standby sandbox prepared, heartbeated, and exited at its deadline, so no run ever used it. This is the client side of the server's assignment contract, where the server commits a run to a sandbox before it tells the sandbox.
The protocol has no accept or reject step. The sandbox receives
assignmentorretiredon a control socket and sends onlyheartbeat. After it installs the run's credential, it reconnects with that credential. The server admits the connection, revokes the standby credential, and replays the assignment; the replay confirms delivery. Only then does the run start, so a cancelled or superseded run never starts.Commits
Each commit is one review unit, in reading order. The branch is linear on
masterat #16538 (ae60375ef). Only the branch tip is validated; intermediate commits do not all build.52b0c69feat(terminal): stop a local shell's event loop for replacementMessage::ShutdownForReplacement, so the PTY event loop can stop a shell without ending the terminal.09a451afeat(terminal): replace an idle local shell with a new environmentTerminalView::replace_shell, which restarts the shell behind a terminal with a new directory and environment. It accepts only an idle, writable local shell, and its error names the condition that refused the replacement. The pane routes the request to the local terminal manager of that view, which starts the replacement on the cloud shell-recovery resources. The replacement environment becomes the baseline for later shell recoveries. Planned replacement is not supported on Windows, because pre-warmed sandboxes run only on Linux.d03c91bfeat(mcp): restart templatable MCP servers with a new runtime environmentreplace_runtime_environmentstops every server and returns the ones to restart. The runtime environment is native-only.830a748feat(server-api): talk to pre-warmed sandboxes over the control socket/wscontrol socket and itsassignment,retired, andheartbeatmessages.410 Gonemaps to "unavailable". A message type the client does not know decodes asUnknown, so the server can add types without breaking a waiting sandbox. The decode test uses the server's own assignment fixture. The configuration query keeps #16538'sfingerprint/speccontract.cecdda2refactor(agent-sdk): resolve agent driver options in one placesplit_run_optionsis the only destructure ofAgentDriverOptions.createand the pre-warmed path both apply the result withapply_run_configuration, so a new option cannot reach one path and miss the other.ab51390refactor(agent-sdk): let launches without CLI arguments reuse run setupRunAgentArgs, so the assignment path calls them instead of copying them.63f521efeat(agent-sdk): let a run reuse a workspace prepared in standbyWorkspaceStaterecords whether the workspace is pending or prepared. A prepared workspace skips cloning, setup commands, and the file-based MCP scan.same_preparationlets the assignment path refuse a run that asks for a different workspace.fbcc0c5feat(agent-sdk): install a pre-warmed assignment on the standby driverprepare_prewarmed_assignmentinstalls the run credential first, then installs the run: options, secrets, attachments, cloud providers, Bedrock, the shell and MCP environment, and the task binding. MCP servers get only the assignment's runtime environment, which is what a worker gives them in a normal run, and never the terminal's managed secrets. The team metadata refresh uses the same retry as a normal cloud run. An installation failure is reported against the run, because the sandbox cannot hand the run back.133f95bfeat(agent-sdk): run assigned tasks from a pre-warmed standby sandboxrun_lifecyclewaits in standby for the assignment, installs it, confirms delivery, and runs it. It holds an assignment that arrives before preparation finishes, ignores replays of the same assignment, and ignores unknown messages on the same connection.96319d8refactor(agent-sdk): identify a pre-warmed assignment by its execution IDexecution_config.execution_id, and the client no longer reads an assignment ID. A sandbox gets at most one assignment and an execution gets at most one sandbox, so the separate ID was redundant.0298a80fix(server-api): skip non-text frames on the pre-warmed control socket66af38ffix(agent-sdk): continue a pre-warmed run when MCP servers fail to restarthandle_mcp_startup_result, so a server that fails to restart degrades the run as it does in a cold run, unless strict MCP startup is set.cbf6838test(terminal): run the planned replacement baseline test only on UnixDecisions and behavior changes
agent run(commit 5). A typed auth secret is now skipped as a whole when one of its variables is set inenvironment_variables. Before, only the colliding variable was replaced.harness-support report-shutdown --standby-id, which this PR does not add.Review guide
Start with commits 5 and 8; together they hold most of the risk. In
apply_run_configuration, check which runtime state survives an in-place assignment: the harness, the output format, and a prepared workspace. Inprepare_prewarmed_assignment, check that nothing can fail before the run credential is installed, apart from parsing the task ID. In commit 2, the replacement refusal conditions inTerminalView::replace_shelldecide when an assignment can install.Linked Issue
Part of REMOTE-3002. This PR does not add the typed assignment context or the standby telemetry correlation that the issue describes. Plans: Align the pre-warmed sandbox client with the new server assignment contract · Address the automated review of PR #16539 · Trim the PR #16539 diff · Conversation
ready-to-specorready-to-implement. The work is tracked in Linear, not in a GitHub issue.Testing
On the branch tip:
cargo nextest run -p warp --lib -E 'test(prewarmed) | test(agent_sdk::) | test(control_reader)': 1018 passed.cargo clippy --locked -p warp --all-targets --tests -- -D warnings: passed.cargo clippy --locked --target wasm32-unknown-unknown --profile release-wasm-debug_assertions -- -D warnings: passed../script/format: no changes.The wider filter that adds
test(shell_replacement) | test(shell_recovery) | test(terminal_manager) | test(templatable_manager) | test(local_tty::event_loop_tests)passed 1046 tests on the previous tip. The terminal and MCP manager code has not changed since then.The standby tests cover an early assignment, replays, unknown messages, non-text frames, retirement before and after installation,
Goneand transient failures during confirmation, and both deadlines. A mock-server test checks that an installation failure is reported against the run with the run's own credential. The standby bootstrap test runsoz agent run-cloud-standbyagainst a mock server through readiness../script/run. No live Namespace assignment ran, because the server side of the socket contract is not deployed.Agent Mode
CHANGELOG-NONE
Co-Authored-By: Warp agent@warp.dev