mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Add untrusted external messages to the Python SDK (#44086)
## Why Applications need to deliver content from other agents, tools, or services with tool-level authority, without treating it as user input or granting authorization. ## What changed - Export `ExternalMessage` for sync and async `run(...)` and `turn(...)`, accepting text or structured content with a tool name and optional namespace. Send it through `toolOutput` and require CLI 0.151.0 or newer. - Support starting a turn or joining an active regular turn while preserving external content as function output in history. Keep external messages separate from user-input lists and `steer(...)`. - Give turn handles independent subscriptions, replaying completed items and latest usage to joining handles. Release consumed transient events and clean up subscriptions on closure, failure, or cancellation. - Document the authority boundary and add sync and async examples. ## Testing Add coverage for wire representations, input validation, runtime compatibility, tool authority across resume, active-turn joins, and tool-output truncation. Add subscription tests for replay, concurrent consumers, early completion, cancellation, and cleanup. GitOrigin-RevId: 6106327085fd9c4bd11b71e20b3d8e74738b8bb5
This commit is contained in:
@@ -13,7 +13,9 @@ from openai_codex.api import (
|
||||
ApprovalMode,
|
||||
AsyncCodex,
|
||||
Codex,
|
||||
ExternalMessage,
|
||||
Sandbox,
|
||||
TextInput,
|
||||
)
|
||||
from openai_codex.client import _params_dict
|
||||
from openai_codex.generated.v2_all import TurnCompletedNotification, TurnStartParams
|
||||
@@ -171,8 +173,11 @@ def test_include_turns_preserves_omission_and_inverts_explicit_values(
|
||||
|
||||
@pytest.mark.parametrize("api_type", [Codex, AsyncCodex])
|
||||
@pytest.mark.parametrize("method", ["run", "turn"])
|
||||
def test_turn_inputs_and_options_reach_the_client(api_type, method) -> None:
|
||||
"""Turn options reach the client through both sync and async entry points."""
|
||||
@pytest.mark.parametrize(
|
||||
"content", [None, "External update", [{"type": "input_text", "text": "External update"}]]
|
||||
)
|
||||
def test_turn_inputs_and_options_reach_the_client(api_type, method, content) -> None:
|
||||
"""User and external inputs preserve distinct wire representations for every entry point."""
|
||||
|
||||
async def scenario() -> None:
|
||||
async_api = api_type is AsyncCodex
|
||||
@@ -188,9 +193,9 @@ def test_turn_inputs_and_options_reach_the_client(api_type, method) -> None:
|
||||
)
|
||||
client = SimpleNamespace(
|
||||
turn_start=rpc(return_value=SimpleNamespace(turn=SimpleNamespace(id="turn-1"))),
|
||||
register_turn_notifications=Mock(),
|
||||
unregister_turn_notifications=Mock(),
|
||||
next_turn_notification=rpc(return_value=completed),
|
||||
_subscribe_turn_notifications=Mock(
|
||||
return_value=SimpleNamespace(next=Mock(return_value=completed), close=Mock())
|
||||
),
|
||||
)
|
||||
codex = api_type.__new__(api_type)
|
||||
codex._client = client
|
||||
@@ -200,7 +205,11 @@ def test_turn_inputs_and_options_reach_the_client(api_type, method) -> None:
|
||||
if async_api
|
||||
else public_api_module.Thread(client, "thread-1")
|
||||
)
|
||||
input = "Continue."
|
||||
input = (
|
||||
"Continue."
|
||||
if content is None
|
||||
else ExternalMessage(tool_name="notifications", namespace="slack", content=content)
|
||||
)
|
||||
turn = getattr(thread, method)(
|
||||
input,
|
||||
service_tier="priority",
|
||||
@@ -210,18 +219,70 @@ def test_turn_inputs_and_options_reach_the_client(api_type, method) -> None:
|
||||
if async_api:
|
||||
turn = await turn
|
||||
assert turn.id == "turn-1"
|
||||
expected_input = [{"type": "text", "text": "Continue.", "text_elements": []}]
|
||||
expected_input = (
|
||||
[{"type": "text", "text": "Continue.", "text_elements": []}] if content is None else []
|
||||
)
|
||||
expected_tool_output = (
|
||||
{}
|
||||
if content is None
|
||||
else {
|
||||
"toolOutput": {"name": "notifications", "namespace": "slack", "output": content},
|
||||
}
|
||||
)
|
||||
assert _params_dict(client.turn_start.call_args.kwargs["params"]) == {
|
||||
"threadId": "thread-1",
|
||||
"input": expected_input,
|
||||
"serviceTier": "priority",
|
||||
"serviceTierForTurn": "default",
|
||||
"turnTrigger": "automation",
|
||||
**expected_tool_output,
|
||||
}
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_type", [Codex, AsyncCodex])
|
||||
def test_external_messages_cannot_be_mixed_with_user_input_or_sent_as_user_steering(
|
||||
api_type,
|
||||
) -> None:
|
||||
async def scenario() -> None:
|
||||
async_api = api_type is AsyncCodex
|
||||
rpc = AsyncMock if async_api else Mock
|
||||
client = SimpleNamespace(
|
||||
turn_start=rpc(), turn_steer=rpc(), _subscribe_turn_notifications=Mock()
|
||||
)
|
||||
codex = api_type.__new__(api_type)
|
||||
codex._client = client
|
||||
codex._initialized = True
|
||||
thread = (
|
||||
public_api_module.AsyncThread(codex, "thread-1")
|
||||
if async_api
|
||||
else public_api_module.Thread(client, "thread-1")
|
||||
)
|
||||
handle = (
|
||||
public_api_module.AsyncTurnHandle(codex, "thread-1", "turn-1")
|
||||
if async_api
|
||||
else public_api_module.TurnHandle(client, "thread-1", "turn-1")
|
||||
)
|
||||
external = ExternalMessage(tool_name="notifications", content="Untrusted update")
|
||||
for operation, input in (
|
||||
(thread.turn, [TextInput("User request"), external]),
|
||||
(handle.steer, external),
|
||||
):
|
||||
with pytest.raises(TypeError):
|
||||
result = operation(input)
|
||||
if async_api:
|
||||
await result
|
||||
with pytest.raises(ValueError, match="tool_name"):
|
||||
result = thread.turn(ExternalMessage(tool_name=" ", content="Untrusted update"))
|
||||
if async_api:
|
||||
await result
|
||||
client.turn_start.assert_not_called()
|
||||
client.turn_steer.assert_not_called()
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def _approval_mode_turn_params(approval_mode: ApprovalMode) -> TurnStartParams:
|
||||
"""Build real generated turn params from one public approval mode."""
|
||||
approval_policy, approvals_reviewer = public_api_module._approval_mode_settings(approval_mode)
|
||||
|
||||
Reference in New Issue
Block a user