Clarify the Temporal sandbox boundary for Code Mode

This commit is contained in:
Aditya Vardhan
2026-07-13 18:45:00 +05:30
parent 3232691b4a
commit 1571f4af36
2 changed files with 39 additions and 6 deletions
+12 -4
View File
@@ -164,6 +164,8 @@ of the workflow.
Add a custom workflow runner to the `Worker` that runs the durable agent:
```python
from pydantic_ai.durable_exec.temporal import PydanticAIPlugin
from temporalio.client import Client
from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions
@@ -171,16 +173,22 @@ workflow_runner = SandboxedWorkflowRunner(
restrictions=SandboxRestrictions.default.with_passthrough_modules('pydantic_monty')
)
worker = Worker(
client = await Client.connect(
'localhost:7233',
plugins=[PydanticAIPlugin()],
)
async with Worker(
client,
task_queue='code-mode',
workflows=[CodeModeWorkflow],
workflow_runner=workflow_runner,
)
):
...
```
Use this runner alongside `PydanticAIPlugin`; the plugin merges its normal passthrough modules into the runner's
restrictions. The workflow remains sandboxed, and the same runner can be passed to Temporal's `Replayer`.
`PydanticAIPlugin` merges its normal passthrough modules into the runner's restrictions. The workflow remains
sandboxed, and the same runner can be passed to Temporal's `Replayer`.
Monty code runs again when Temporal replays the workflow. Keep external state access out of the code sandbox: do not
connect `os_access` or `mount` to changing host state. Put filesystem, network, clock, and environment access in tools
+27 -2
View File
@@ -16,6 +16,8 @@ runs `temporalite` automatically.
from __future__ import annotations
import json
import subprocess
import sys
from collections.abc import AsyncIterator
from datetime import timedelta
from typing import Any
@@ -33,7 +35,11 @@ try:
from temporalio.common import RetryPolicy
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Replayer, Worker
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions
from temporalio.worker.workflow_sandbox import (
RestrictedWorkflowAccessError,
SandboxedWorkflowRunner,
SandboxRestrictions,
)
from temporalio.workflow import ActivityConfig
except ImportError: # pragma: lax no cover
pytest.skip('temporalio not installed', allow_module_level=True)
@@ -158,6 +164,19 @@ class CodeModeWorkflow:
}
@workflow.defn
class SandboxRestrictionWorkflow:
"""Probe that passing Monty through does not allow Python subprocess calls."""
@workflow.run
async def run(self) -> str:
try:
subprocess.run([sys.executable, '-c', 'pass'], check=True)
except RestrictedWorkflowAccessError as e:
return e.qualified_name
return 'subprocess was allowed'
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
@@ -178,7 +197,7 @@ async def test_code_mode_runs_in_temporal_workflow(client: Client) -> None:
async with Worker(
client,
task_queue=TASK_QUEUE,
workflows=[CodeModeWorkflow],
workflows=[CodeModeWorkflow, SandboxRestrictionWorkflow],
plugins=[AgentPlugin(temporal_code_mode_agent)],
workflow_runner=_workflow_runner(),
):
@@ -188,8 +207,14 @@ async def test_code_mode_runs_in_temporal_workflow(client: Client) -> None:
id=workflow_id,
task_queue=TASK_QUEUE,
)
sandbox_result = await client.execute_workflow(
SandboxRestrictionWorkflow.run,
id='test_code_mode_temporal_sandbox_restrictions',
task_queue=TASK_QUEUE,
)
assert result['output'] == 'done: 7'
assert sandbox_result == 'subprocess.run.__call__'
messages = json.loads(result['messages'])
assert len(messages) == 4