Files
OpenShell/e2e/python/test_exec_admission.py
Mrunal Patel bdffa102c3 feat(api): add durable exec launch admission (#3324)
* feat(api): add durable exec launch admission

Fence duplicate exec launches with keyed durable admission and producer-owned terminal completion. Keep uncertain launches unresolved and never replay output or interactive input.

Part of #3051 (phase 4a).

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>

* fix(api): fence exec identity across authorization lookups

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>

---------

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
2026-09-22 21:52:19 +00:00

158 lines
5.9 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import threading
import time
import uuid
from typing import TYPE_CHECKING
import grpc
import pytest
from google.protobuf import duration_pb2
from openshell._proto import datamodel_pb2, openshell_pb2
from openshell.errors import from_grpc_error
if TYPE_CHECKING:
from collections.abc import Callable
from openshell import Sandbox, SandboxClient
def assert_blocked(stream, reason: str) -> None:
with pytest.raises(grpc.RpcError) as caught:
list(stream)
error = from_grpc_error(caught.value)
assert error.code() == grpc.StatusCode.FAILED_PRECONDITION
assert error.error_info is not None
assert error.error_info.reason == reason
@pytest.mark.parametrize("interactive", [False, True])
def test_exec_request_id_never_relaunches_or_replays_output(
sandbox: Callable[..., Sandbox], sandbox_client: SandboxClient, interactive: bool
) -> None:
with sandbox(delete_on_exit=True) as sb:
path = f"/sandbox/exec-admission-{uuid.uuid4().hex}"
request = openshell_pb2.ExecSandboxRequest(
sandbox=sb.sandbox.name,
workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"),
command=[
"/bin/sh",
"-c",
f"printf x >> {path}; printf started; "
f"while [ ! -e {path}.release ]; do sleep 0.1; done; "
"printf finished; exit 7",
],
request_id=str(uuid.uuid4()),
)
done = threading.Event()
def invoke(value):
if not interactive:
return sandbox_client._stub.ExecSandbox(value, timeout=30)
def inputs():
yield openshell_pb2.ExecSandboxInput(start=value)
done.wait(timeout=30)
return sandbox_client._stub.ExecSandboxInteractive(inputs(), timeout=30)
stream = invoke(request)
try:
first = next(stream)
assert first.WhichOneof("payload") == "stdout"
assert b"started" in first.stdout.data
assert_blocked(invoke(request), "REQUEST_OUTCOME_UNCERTAIN")
changed = openshell_pb2.ExecSandboxRequest()
changed.CopyFrom(request)
changed.command.append("changed-payload")
assert_blocked(invoke(changed), "REQUEST_ID_PAYLOAD_MISMATCH")
assert sb.exec(["touch", f"{path}.release"]).exit_code == 0
events = list(stream)
assert events[-1].WhichOneof("payload") == "exit"
assert events[-1].exit.exit_code == 7
assert_blocked(invoke(request), "REQUEST_STREAM_UNAVAILABLE")
result = sb.exec(["cat", path])
assert result.exit_code == 0
assert result.stdout == "x"
finally:
done.set()
stream.cancel()
@pytest.mark.parametrize("interactive", [False, True])
def test_exec_request_timeout_keeps_launch_unresolved(
sandbox: Callable[..., Sandbox], sandbox_client: SandboxClient, interactive: bool
) -> None:
with sandbox(delete_on_exit=True) as sb:
path = f"/sandbox/exec-timeout-{uuid.uuid4().hex}"
request = openshell_pb2.ExecSandboxRequest(
sandbox=sb.sandbox.name,
workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"),
command=["/bin/sh", "-c", f"printf x >> {path}; sleep 5"],
execution_timeout=duration_pb2.Duration(seconds=1),
request_id=str(uuid.uuid4()),
)
done = threading.Event()
def invoke():
if not interactive:
return sandbox_client._stub.ExecSandbox(request, timeout=30)
def inputs():
yield openshell_pb2.ExecSandboxInput(start=request)
done.wait(timeout=30)
return sandbox_client._stub.ExecSandboxInteractive(inputs(), timeout=30)
stream = invoke()
try:
events = []
for event in stream:
events.append(event)
if event.WhichOneof("payload") == "exit":
# A synthetic timeout leaves the interactive input task
# alive until the client closes its side of the stream.
done.set()
assert events[-1].exit.exit_code == 124
assert_blocked(invoke(), "REQUEST_OUTCOME_UNCERTAIN")
assert sb.exec(["cat", path]).stdout == "x"
finally:
done.set()
stream.cancel()
def test_exec_client_cancellation_does_not_clear_launch_admission(
sandbox: Callable[..., Sandbox], sandbox_client: SandboxClient
) -> None:
with sandbox(delete_on_exit=True) as sb:
path = f"/sandbox/exec-cancel-{uuid.uuid4().hex}"
request = openshell_pb2.ExecSandboxRequest(
sandbox=sb.sandbox.name,
workspace_scope=datamodel_pb2.WorkspaceSelector(workspace="default"),
command=[
"/bin/sh",
"-c",
f"printf x >> {path}; printf started; sleep 1; printf finished",
],
request_id=str(uuid.uuid4()),
)
stream = sandbox_client._stub.ExecSandbox(request, timeout=30)
assert b"started" in next(stream).stdout.data
stream.cancel()
deadline = time.monotonic() + 15
while True:
with pytest.raises(grpc.RpcError) as caught:
list(sandbox_client._stub.ExecSandbox(request, timeout=30))
error = from_grpc_error(caught.value)
assert error.error_info is not None
if error.error_info.reason == "REQUEST_STREAM_UNAVAILABLE":
break
assert error.error_info.reason == "REQUEST_OUTCOME_UNCERTAIN"
assert time.monotonic() < deadline
time.sleep(0.1)
assert sb.exec(["cat", path]).stdout == "x"