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
1 change: 1 addition & 0 deletions packages/prime-sandboxes/src/prime_sandboxes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class CommandRequest(BaseModel):
command: str
working_dir: Optional[str] = None
env: Optional[Dict[str, str]] = None
user: Optional[str] = None


class CommandResponse(BaseModel):
Expand Down
30 changes: 28 additions & 2 deletions packages/prime-sandboxes/src/prime_sandboxes/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,11 +721,17 @@ def execute_command(
working_dir: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None,
user: Optional[str] = None,
) -> CommandResponse:
"""Execute command directly via gateway."""
auth = self._auth_cache.get_or_refresh(sandbox_id)

if self._auth_cache.is_vm(sandbox_id):
if user is not None:
raise ValueError(
"The 'user' parameter is only supported for container sandboxes, "
"not VM sandboxes."
)
return self._execute_command_connect_rpc(
sandbox_id=sandbox_id,
command=command,
Expand All @@ -742,6 +748,7 @@ def execute_command(
working_dir=working_dir,
env=env,
timeout=timeout,
user=user,
)

def _execute_command_connect_rpc(
Expand Down Expand Up @@ -822,6 +829,7 @@ def _execute_command_rest(
working_dir: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None,
user: Optional[str] = None,
) -> CommandResponse:
gateway_url = auth["gateway_url"].rstrip("/")
url = f"{gateway_url}/{auth['user_ns']}/{auth['job_id']}/exec"
Expand All @@ -835,6 +843,8 @@ def _execute_command_rest(
"sandbox_id": sandbox_id,
"timeout": effective_timeout,
}
if user is not None:
payload["user"] = user

for attempt in range(MAX_409_RETRIES):
try:
Expand Down Expand Up @@ -900,6 +910,7 @@ def start_background_job(
command: str,
working_dir: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
user: Optional[str] = None,
) -> BackgroundJob:
"""Start a long-running command in the background.

Expand All @@ -911,6 +922,8 @@ def start_background_job(
command: Command to execute
working_dir: Working directory for command execution
env: Environment variables
user: Run the job as this user, like ``docker exec -u`` (username or
numeric UID, optionally USER:GROUP). Container sandboxes only.

Returns:
BackgroundJob with job_id and file paths for polling
Expand Down Expand Up @@ -945,7 +958,7 @@ def start_background_job(

# Outer nohup redirects to /dev/null since output goes to log files inside sh -c
bg_cmd = f"nohup sh -c {quoted_sh_command} < /dev/null > /dev/null 2>&1 &"
self.execute_command(sandbox_id, bg_cmd, timeout=30)
self.execute_command(sandbox_id, bg_cmd, timeout=30, user=user)

return BackgroundJob(
job_id=job_id,
Expand Down Expand Up @@ -1632,11 +1645,17 @@ async def execute_command(
working_dir: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None,
user: Optional[str] = None,
) -> CommandResponse:
"""Execute command directly via gateway (async)."""
auth = await self._auth_cache.get_or_refresh(sandbox_id)

if await self._auth_cache.is_vm(sandbox_id):
if user is not None:
raise ValueError(
"The 'user' parameter is only supported for container sandboxes, "
"not VM sandboxes."
)
return await self._execute_command_connect_rpc(
sandbox_id=sandbox_id,
command=command,
Expand All @@ -1653,6 +1672,7 @@ async def execute_command(
working_dir=working_dir,
env=env,
timeout=timeout,
user=user,
)

async def _execute_command_connect_rpc(
Expand Down Expand Up @@ -1733,6 +1753,7 @@ async def _execute_command_rest(
working_dir: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None,
user: Optional[str] = None,
) -> CommandResponse:
gateway_url = auth["gateway_url"].rstrip("/")
url = f"{gateway_url}/{auth['user_ns']}/{auth['job_id']}/exec"
Expand All @@ -1746,6 +1767,8 @@ async def _execute_command_rest(
"sandbox_id": sandbox_id,
"timeout": effective_timeout,
}
if user is not None:
payload["user"] = user

for attempt in range(MAX_409_RETRIES):
try:
Expand Down Expand Up @@ -1811,6 +1834,7 @@ async def start_background_job(
command: str,
working_dir: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
user: Optional[str] = None,
) -> BackgroundJob:
"""Start a long-running command in the background (async).

Expand All @@ -1822,6 +1846,8 @@ async def start_background_job(
command: Command to execute
working_dir: Working directory for command execution
env: Environment variables
user: Run the job as this user, like ``docker exec -u`` (username or
numeric UID, optionally USER:GROUP). Container sandboxes only.

Returns:
BackgroundJob with job_id and file paths for polling
Expand Down Expand Up @@ -1856,7 +1882,7 @@ async def start_background_job(

# Outer nohup redirects to /dev/null since output goes to log files inside sh -c
bg_cmd = f"nohup sh -c {quoted_sh_command} < /dev/null > /dev/null 2>&1 &"
await self.execute_command(sandbox_id, bg_cmd, timeout=30)
await self.execute_command(sandbox_id, bg_cmd, timeout=30, user=user)

return BackgroundJob(
job_id=job_id,
Expand Down
10 changes: 10 additions & 0 deletions packages/prime/src/prime_cli/commands/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,13 @@ def run(
"--timeout",
help="Timeout for the command in seconds",
),
user: Optional[str] = typer.Option(
None,
"-u",
"--user",
help="Run the command as this user (username or UID, optionally USER:GROUP), "
"like 'docker exec -u'. Container sandboxes only.",
),
) -> None:
"""Execute a command in a sandbox.

Expand Down Expand Up @@ -1128,6 +1135,8 @@ def run(
console.print(f"[bold blue]Environment:[/bold blue] {obfuscated_env}")
if timeout is not None:
console.print(f"[bold blue]Timeout:[/bold blue] {timeout}s")
if user:
console.print(f"[bold blue]User:[/bold blue] {user}")

start_time = time.perf_counter()

Expand All @@ -1138,6 +1147,7 @@ def run(
working_dir,
env_vars if env_vars else None,
timeout=timeout,
user=user,
Comment thread
kcoopermiller marked this conversation as resolved.
)

# End timing
Expand Down
Loading