-
Notifications
You must be signed in to change notification settings - Fork 4k
Expand file tree
/
Copy pathunix_local_runner.py
More file actions
214 lines (190 loc) · 9.03 KB
/
unix_local_runner.py
File metadata and controls
214 lines (190 loc) · 9.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""
Start here if you want the simplest Unix-local sandbox example.
This file mirrors the Docker example, but the sandbox runs as a temporary local
workspace on macOS or Linux instead of inside a Docker container.
"""
import argparse
import asyncio
import io
import sys
import tempfile
from pathlib import Path
from openai.types.responses import ResponseTextDeltaEvent
from agents import Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxPathGrant, SandboxRunConfig
from agents.sandbox.errors import WorkspaceArchiveWriteError
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from examples.sandbox.misc.example_support import text_manifest
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability
DEFAULT_QUESTION = (
"Review this renewal packet. Summarize the customer's situation, the likely blockers, "
"and the next two actions an account team should take."
)
def _build_manifest(external_dir: Path, scratch_dir: Path) -> Manifest:
# The manifest is the file tree that will be materialized into the sandbox workspace.
return text_manifest(
{
"account_brief.md": (
"# Northwind Health\n\n"
"- Segment: Mid-market healthcare analytics provider.\n"
"- Annual contract value: $148,000.\n"
"- Renewal date: 2026-04-15.\n"
"- Executive sponsor: Director of Data Operations.\n"
),
"renewal_request.md": (
"# Renewal request\n\n"
"Northwind requested a 12 percent discount in exchange for a two-year renewal. "
"They also want a 45-day implementation timeline for a new reporting workspace.\n"
),
"usage_notes.md": (
"# Usage notes\n\n"
"- Weekly active users increased 18 percent over the last quarter.\n"
"- API traffic is stable.\n"
"- The customer still has one unresolved SSO configuration issue from onboarding.\n"
),
"implementation_risks.md": (
"# Delivery risks\n\n"
"- Security questionnaire for the new reporting workspace is not complete.\n"
"- Customer procurement requires final legal language by April 1.\n"
),
}
).model_copy(
update={
"extra_path_grants": (
SandboxPathGrant(
path=str(external_dir),
read_only=True,
description="read-only external renewal packet notes",
),
SandboxPathGrant(
path=str(scratch_dir),
description="temporary renewal packet scratch files",
),
)
},
deep=True,
)
async def _verify_extra_path_grants() -> None:
with tempfile.TemporaryDirectory(prefix="agents-unix-local-extra-") as extra_root_text:
extra_root = Path(extra_root_text)
external_dir = extra_root / "external"
scratch_dir = extra_root / "scratch"
external_dir.mkdir()
scratch_dir.mkdir()
external_input = external_dir / "external_input.txt"
read_only_output = external_dir / "blocked.txt"
sdk_output = scratch_dir / "sdk_output.txt"
exec_output = scratch_dir / "exec_output.txt"
external_input.write_text("external grant input\n", encoding="utf-8")
client = UnixLocalSandboxClient()
sandbox = await client.create(manifest=_build_manifest(external_dir, scratch_dir))
try:
async with sandbox:
payload = await sandbox.read(external_input)
try:
await sandbox.write(read_only_output, io.BytesIO(b"should fail\n"))
except WorkspaceArchiveWriteError:
pass
else:
raise RuntimeError(
"SDK write to read-only extra path grant unexpectedly worked."
)
await sandbox.write(sdk_output, io.BytesIO(b"sdk grant output\n"))
exec_result = await sandbox.exec(
"sh",
"-c",
'cat "$1"; printf "%s\\n" "exec grant output" > "$2"',
"sh",
external_input,
exec_output,
shell=False,
)
if payload.read() != b"external grant input\n":
raise RuntimeError(
"SDK read from extra path grant returned unexpected content."
)
if sdk_output.read_text(encoding="utf-8") != "sdk grant output\n":
raise RuntimeError("SDK write to extra path grant failed.")
if exec_result.stdout != b"external grant input\n" or exec_result.exit_code != 0:
raise RuntimeError("Shell read from extra path grant failed.")
if exec_output.read_text(encoding="utf-8") != "exec grant output\n":
raise RuntimeError("Shell write to extra path grant failed.")
finally:
await client.delete(sandbox)
print("extra_path_grants verification passed")
async def main(model: str, question: str, stream: bool) -> None:
with tempfile.TemporaryDirectory(prefix="agents-unix-local-extra-") as extra_root_text:
extra_root = Path(extra_root_text)
external_dir = extra_root / "external"
scratch_dir = extra_root / "scratch"
external_dir.mkdir()
scratch_dir.mkdir()
external_note = external_dir / "external_renewal_note.md"
scratch_note = scratch_dir / "scratch_summary.md"
external_note.write_text(
"# External renewal note\n\n"
"Finance approved discount authority up to 10 percent, but anything higher needs "
"CFO approval before legal can finalize terms.\n",
encoding="utf-8",
)
manifest = _build_manifest(external_dir, scratch_dir)
# The sandbox agent sees the manifest as its workspace and uses one shared shell tool
# to inspect the files before answering.
agent = SandboxAgent(
name="Renewal Packet Analyst",
model=model,
instructions=(
"You review renewal packets for an account team. Inspect the packet before "
"answering. Keep the response concise, business-focused, and cite the file names "
"that support each conclusion. If a conclusion depends on a file, mention that "
"file by name. Do not invent numbers or statuses that are not present in the "
"workspace. The manifest also grants read-only access to an external note at "
f"`{external_note}` and read-write access to a scratch directory at "
f"`{scratch_dir}`. Read the external note before answering, and write a brief "
f"scratch note to `{scratch_note}`."
),
default_manifest=manifest,
capabilities=[WorkspaceShellCapability()],
)
# With Unix-local sandboxes, the runner creates and cleans up the temporary workspace for us.
run_config = RunConfig(
sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()),
workflow_name="Unix local sandbox review",
tracing_disabled=True,
)
if not stream:
result = await Runner.run(agent, question, run_config=run_config)
print(result.final_output)
return
# The streaming path prints text deltas as they arrive so the example behaves like a demo.
stream_result = Runner.run_streamed(agent, question, run_config=run_config)
saw_text_delta = False
async for event in stream_result.stream_events():
if event.type == "raw_response_event" and isinstance(
event.data, ResponseTextDeltaEvent
):
if not saw_text_delta:
print("assistant> ", end="", flush=True)
saw_text_delta = True
print(event.data.delta, end="", flush=True)
if saw_text_delta:
print()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="gpt-5.5", help="Model name to use.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.")
parser.add_argument(
"--verify-extra-path-grants",
action="store_true",
default=False,
help="Run a local extra_path_grants smoke test without calling a model.",
)
args = parser.parse_args()
if args.verify_extra_path_grants:
asyncio.run(_verify_extra_path_grants())
else:
asyncio.run(main(args.model, args.question, args.stream))