Skip to content
Open
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
16 changes: 16 additions & 0 deletions omnigent/runner/tool_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,7 @@ def _build_session_create_body(
title: object,
message: object,
model: object = None,
terminal_launch_args: object = None,
) -> _JsonObject:
"""
Build the JSON ``POST /v1/sessions`` body for ``sys_session_create``.
Expand Down Expand Up @@ -2269,6 +2270,12 @@ def _build_session_create_body(
body["title"] = title
if isinstance(model, str) and model:
body["model_override"] = model
if isinstance(terminal_launch_args, list) and terminal_launch_args:
# Forward verbatim; the server bounds-checks via
# _validate_terminal_launch_args (flat list, no shell metachars).
body["terminal_launch_args"] = [
str(a) for a in terminal_launch_args if isinstance(a, str)
]
if isinstance(message, str) and message:
body["initial_items"] = [
{
Expand Down Expand Up @@ -2423,6 +2430,7 @@ async def _execute_session_create(
args.get("title"),
args.get("message"),
model=args.get("model"),
terminal_launch_args=args.get("terminal_launch_args"),
)
try:
resp = await server_client.post("/v1/sessions", json=body, timeout=30.0)
Expand Down Expand Up @@ -2589,6 +2597,14 @@ async def _upload_config_bundle(
title = args.get("title")
if isinstance(title, str) and title:
metadata["title"] = title
terminal_launch_args = args.get("terminal_launch_args")
if isinstance(terminal_launch_args, list) and terminal_launch_args:
# Forward into the multipart metadata JSON; the server parses it
# via _parse_session_create_metadata -> SessionCreateMetadata, which
# already owns _validate_terminal_launch_args (bounds-checked).
metadata["terminal_launch_args"] = [
str(a) for a in terminal_launch_args if isinstance(a, str)
]
try:
resp = await server_client.post(
"/v1/sessions",
Expand Down
19 changes: 19 additions & 0 deletions omnigent/tools/builtins/spawn.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,25 @@ def get_schema(self) -> dict[str, Any]:
"agent's default."
),
},
"terminal_launch_args": {
"type": "array",
"items": {"type": "string"},
"description": (
"Optional native-terminal pass-through args "
"for the child session, e.g. "
"[\"--yolo\"] for a headless cursor-native "
"worker (cursor-agent's full-bypass flag) so it "
"does not stall on in-terminal approval prompts "
"that no human can answer. Forwarded verbatim "
"into the server request body's "
"terminal_launch_args (already validated and "
"bounds-checked server-side); only meaningful "
"for terminal-native harnesses "
"(cursor/codex/claude-native). Top-level "
"sessions keep these explicit args rather than "
"deriving them from the bundle's yolo flag."
),
},
},
# Only the always-optional fields are listed in
# ``required`` (none): the agent_id-vs-config_path
Expand Down
44 changes: 44 additions & 0 deletions tests/runner/test_runner_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6179,6 +6179,50 @@ async def _server_handler(request: httpx.Request) -> httpx.Response:
assert handle["agent_name"] == "researcher"


@pytest.mark.asyncio
async def test_sys_session_create_forwards_terminal_launch_args() -> None:
"""
``terminal_launch_args`` reaches the JSON create body verbatim.

Headless native-terminal workers (cursor/codex/claude-native) stall
on in-terminal approval prompts no human can answer; the caller
declares the bypass stance (e.g. ``["--yolo"]``) at launch, and the
server — which already validates + bounds-checks the list — threads
it into the child's terminal_launch_args. If the tool dropped the
field, a top-level agent_id/config_path cursor worker would silently
run WITHOUT ``--yolo`` and stall on every tool call.
"""
from omnigent.runner.tool_dispatch import execute_tool

captured: dict[str, Any] = {}

async def _server_handler(request: httpx.Request) -> httpx.Response:
if request.method == "POST" and request.url.path == "/v1/sessions":
captured.update(json.loads(request.content))
return httpx.Response(
201,
json={"id": "conv_child_tla", "agent_id": "ag_x", "status": "idle"},
)
return httpx.Response(404, json={"error": str(request.url)})

async with httpx.AsyncClient(
transport=httpx.MockTransport(_server_handler),
base_url="http://server",
) as server_client:
await execute_tool(
tool_name="sys_session_create",
arguments=json.dumps(
{"agent_id": "ag_x", "terminal_launch_args": ["--yolo"]}
),
server_client=server_client,
conversation_id="conv_caller",
)

# Forwarded verbatim into the create body the server validates.
assert captured["terminal_launch_args"] == ["--yolo"]
assert captured["parent_session_id"] == "conv_caller"


@pytest.mark.asyncio
@pytest.mark.parametrize(
"arguments",
Expand Down
Loading