Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions coding-agent/docs/remote-sandbox-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ from `origin`. It streams one prompt, leaves the remote session open, and prints
follow-up commands for inspecting, exporting, or publishing the result. It does
not overwrite the local checkout by default.

Use `remote run --no-workspace` when the server has no cloud workspace support
and the session should be created without any server-side workspace provisioning.

Set the remote tool approval policy explicitly when automation cannot answer
approval prompts:

Expand Down
32 changes: 28 additions & 4 deletions coding-agent/src/coding_agent/cli/remote_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def remote_remove(name: str) -> None:
is_flag=True,
help="Create an empty server-side Docker workspace.",
)
@click.option(
"--no-workspace",
is_flag=True,
help="Create a session without provisioning a workspace; use for servers without cloud workspace support.",
)
@click.option(
"--runtime",
"runtime_profile",
Expand All @@ -141,6 +146,7 @@ def remote_repl(
name: str,
repo: str | None,
empty_workspace: bool,
no_workspace: bool,
runtime_profile: str | None,
goal: str,
approval_policy: str,
Expand All @@ -151,6 +157,7 @@ def remote_repl(
name=name,
repo=repo,
empty_workspace=empty_workspace,
no_workspace=no_workspace,
runtime_profile=runtime_profile,
goal=goal,
approval_policy=approval_policy,
Expand All @@ -172,6 +179,11 @@ def remote_repl(
is_flag=True,
help="Create an empty server-side Docker workspace.",
)
@click.option(
"--no-workspace",
is_flag=True,
help="Create a session without provisioning a workspace; use for servers without cloud workspace support.",
)
@click.option(
"--runtime",
"runtime_profile",
Expand Down Expand Up @@ -209,6 +221,7 @@ def remote_run(
name: str,
repo: str | None,
empty_workspace: bool,
no_workspace: bool,
runtime_profile: str | None,
goal: str,
approval_policy: str,
Expand All @@ -221,6 +234,7 @@ def remote_run(
name=name,
repo=repo,
empty_workspace=empty_workspace,
no_workspace=no_workspace,
runtime_profile=runtime_profile,
goal=goal,
approval_policy=approval_policy,
Expand All @@ -236,6 +250,7 @@ def _remote_run_once(
name: str,
repo: str | None,
empty_workspace: bool,
no_workspace: bool,
runtime_profile: str | None,
goal: str,
approval_policy: str,
Expand All @@ -255,16 +270,24 @@ def _remote_run_once(
create_workspace_archive_base64,
)

if repo is not None and empty_workspace:
workspace_mode_count = (
int(repo is not None) + int(empty_workspace) + int(no_workspace)
)
if workspace_mode_count > 1:
raise click.ClickException(
"Pass either --repo to use a local repo or --empty-workspace, not both."
"Pass only one of --repo, --empty-workspace, or --no-workspace."
)
if repo is None and not empty_workspace:
if workspace_mode_count == 0:
raise click.ClickException(
"Pass --repo to use a local repo or --empty-workspace to create a blank remote workspace."
"Pass --repo to use a local repo, --empty-workspace to create a blank remote workspace, "
"or --no-workspace to create a session without a workspace for servers without cloud workspace support."
)
if download_results and repo is None:
raise click.ClickException("Pass --repo with --download.")
if no_workspace and runtime_profile is not None:
raise click.ClickException(
"Pass --runtime only when creating a remote workspace."
)

endpoint = get_remote(name)
headers = auth_headers(endpoint)
Expand All @@ -290,6 +313,7 @@ def _remote_run_once(
endpoint,
snapshot_archive_base64=snapshot_archive_base64,
workspace_source=workspace_source,
no_workspace=no_workspace,
approval_policy=approval_policy,
runtime_profile=None if workspace_source is not None else runtime_profile,
)
Expand Down
53 changes: 40 additions & 13 deletions coding-agent/src/coding_agent/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ def create_remote_session(
*,
snapshot_archive_base64: str | None = None,
workspace_source: dict[str, object] | None = None,
no_workspace: bool = False,
approval_policy: str = "auto",
runtime_profile: str | None = None,
observation_sink: ObservationSink | None = None,
Expand All @@ -260,23 +261,34 @@ def create_remote_session(
raise click.ClickException(
"Pass either workspace_source or snapshot_archive_base64, not both."
)
if workspace_source is None:
workspace_source = {"kind": "docker"}
if snapshot_archive_base64 is not None:
workspace_source["snapshot_archive_base64"] = snapshot_archive_base64
else:
workspace_source = dict(workspace_source)
if runtime_profile is not None:
workspace_source["runtime_profile"] = runtime_profile
payload: dict[str, object] = {
"workspace_source": workspace_source,
"approval_policy": approval_policy,
}
if no_workspace and (
workspace_source is not None
or snapshot_archive_base64 is not None
or runtime_profile is not None
):
raise click.ClickException(
"Pass either no_workspace or remote workspace options, not both."
)
payload: dict[str, object] = {"approval_policy": approval_policy}
workspace_ref_kind = "none"
if not no_workspace:
if workspace_source is None:
workspace_source = {"kind": "docker"}
if snapshot_archive_base64 is not None:
workspace_source["snapshot_archive_base64"] = snapshot_archive_base64
if runtime_profile is not None:
workspace_source["runtime_profile"] = runtime_profile
else:
workspace_source = dict(workspace_source)
if runtime_profile is not None:
workspace_source["runtime_profile"] = runtime_profile
payload["workspace_source"] = workspace_source
workspace_ref_kind = _workspace_ref_kind(workspace_source)
with record_span(
"remote.workspace.provision",
sink=observation_sink,
attributes={
"workspace_ref_kind": _workspace_ref_kind(workspace_source),
"workspace_ref_kind": workspace_ref_kind,
"remote_status": "started",
},
) as span:
Expand Down Expand Up @@ -1704,9 +1716,24 @@ def _raise_remote_http_error(response: httpx.Response, action: str) -> None:
_ = response.raise_for_status()
except httpx.HTTPStatusError as exc:
detail = _response_error_detail(response)
if action == "create remote session" and _looks_like_cloud_workspace_disabled(
detail
):
detail = (
f"{detail}. Use --no-workspace to create a session without "
"server-side workspace provisioning."
)
raise click.ClickException(f"Failed to {action}: {detail}") from exc


def _looks_like_cloud_workspace_disabled(detail: str) -> bool:
normalized = detail.lower()
return (
"cloud_workspace.enabled=true" in normalized
or "cloud workspace provisioning" in normalized
)


def _response_error_detail(response: httpx.Response) -> str:
fallback = _response_status_detail(response)
body = _read_response_body_for_error_detail(response)
Expand Down
Loading
Loading