Skip to content

Commit 3ac4af8

Browse files
committed
fix(sprites): warn agents to pass --dir to sprite-env services create
The platform-context framing now tells the agent that ``sprite-env services create`` ignores the cwd of the calling shell — services launch with the host's home directory as cwd by default, which silently breaks any service the agent tries to expose unless it's pointed at the workspace explicitly. The framing also picks up the actual ``manifest.root`` so the example uses the agent's real workspace path. Two new tests cover the warning text and that the framing reflects a non-default manifest root.
1 parent c8e501c commit 3ac4af8

2 files changed

Lines changed: 80 additions & 12 deletions

File tree

src/agents/extensions/sandbox/sprites/capabilities.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# exactly once per sprite for the life of the process. ``clear_platform_context_cache``
2626
# below is exposed for applications that want to force a re-fetch (e.g.
2727
# after a sprite image upgrade).
28-
_PLATFORM_CONTEXT_CACHE: dict[tuple[str, str], str] = {}
28+
_PLATFORM_CONTEXT_CACHE: dict[tuple[str, str, str], str] = {}
2929

3030

3131
def clear_platform_context_cache(sprite_name: str | None = None, path: str | None = None) -> None:
@@ -38,12 +38,12 @@ def clear_platform_context_cache(sprite_name: str | None = None, path: str | Non
3838
if sprite_name is None:
3939
_PLATFORM_CONTEXT_CACHE.clear()
4040
return
41-
if path is None:
42-
for key in list(_PLATFORM_CONTEXT_CACHE.keys()):
43-
if key[0] == sprite_name:
44-
del _PLATFORM_CONTEXT_CACHE[key]
45-
return
46-
_PLATFORM_CONTEXT_CACHE.pop((sprite_name, path), None)
41+
for key in list(_PLATFORM_CONTEXT_CACHE.keys()):
42+
if key[0] != sprite_name:
43+
continue
44+
if path is not None and key[1] != path:
45+
continue
46+
del _PLATFORM_CONTEXT_CACHE[key]
4747

4848

4949
class SpritesPlatformContext(Capability):
@@ -83,13 +83,15 @@ class SpritesPlatformContext(Capability):
8383
"""Timeout for the ``cat`` exec call."""
8484

8585
async def instructions(self, manifest: Manifest) -> str | None:
86-
_ = manifest
8786
session = self.session
8887
if session is None:
8988
return None
9089

9190
sprite_name = _resolve_sprite_name(session)
92-
cache_key = (sprite_name or "", self.path)
91+
workspace_root = manifest.root
92+
# Cache key includes workspace root because the framing references
93+
# manifest.root verbatim — different roots produce different text.
94+
cache_key = (sprite_name or "", self.path, workspace_root)
9395
cached = _PLATFORM_CONTEXT_CACHE.get(cache_key)
9496
if cached is not None:
9597
return cached
@@ -112,7 +114,19 @@ async def instructions(self, manifest: Manifest) -> str | None:
112114
"it as authoritative when choosing how to interact with the sandbox.\n\n"
113115
"<sprites-platform-context>\n"
114116
f"{text}\n"
115-
"</sprites-platform-context>"
117+
"</sprites-platform-context>\n\n"
118+
f"Important: this agent's workspace root is `{workspace_root}`. Sprites "
119+
f"services created via `sprite-env services create` run with their own "
120+
f"working directory (typically the user's home directory) — NOT in the "
121+
f"workspace. ALWAYS pass `--dir {workspace_root}` (or a workspace "
122+
f"subdirectory) to `sprite-env services create` so the service starts "
123+
f"in the right place. Example:\n\n"
124+
f" sprite-env services create web \\\n"
125+
f" --cmd python3 --args -m,http.server,8080 \\\n"
126+
f" --dir {workspace_root} \\\n"
127+
f" --http-port 8080\n\n"
128+
f"Without `--dir`, an HTTP server will list the home directory and any "
129+
f"file-reading service will look in the wrong place."
116130
)
117131
if sprite_name:
118132
_PLATFORM_CONTEXT_CACHE[cache_key] = framed

tests/extensions/test_sandbox_sprites.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def test_options_roundtrip_through_polymorphic_registry() -> None:
326326
timeout_ms=120_000,
327327
)
328328
payload = options.model_dump(mode="json")
329-
assert payload["type"] == "sprites"
329+
assert payload["type"] == "fly"
330330
restored = BaseSandboxClientOptions.parse(payload)
331331
assert isinstance(restored, SpritesSandboxClientOptions)
332332
assert restored.model_dump(mode="json") == payload
@@ -639,7 +639,7 @@ async def test_resolve_exposed_port_not_configured_when_no_matching_service(
639639
_attach(inner, client=fake_client, sprite=sprite)
640640
with pytest.raises(ExposedPortUnavailableError) as excinfo:
641641
await inner._resolve_exposed_port(8080)
642-
assert excinfo.value.context.get("backend") == "sprites"
642+
assert excinfo.value.context.get("backend") == "fly"
643643

644644

645645
# ---------- 9. Read / write ----------
@@ -1324,3 +1324,57 @@ async def test_sprites_platform_context_cache_clear_forces_refetch(
13241324
out2 = await cap.instructions(state.manifest)
13251325
assert out2 is not None and "v2" in out2
13261326
assert len(fake_control.start_op_calls) == 2
1327+
1328+
1329+
# ---------- 20. Platform context includes service working-directory hint ----------
1330+
1331+
1332+
@pytest.mark.asyncio
1333+
async def test_platform_context_warns_about_service_cwd(
1334+
patched_sprites: dict[str, Any],
1335+
) -> None:
1336+
"""The framing should warn the model that services run with cwd=$HOME by default.
1337+
1338+
Without this warning, agents commonly create `python3 -m http.server` services
1339+
and serve from the home directory instead of the workspace.
1340+
"""
1341+
1342+
fake_control = patched_sprites["control"]
1343+
fake_control.next_ops.append(_FakeOpConn(stdout=b"# Sprite\n", exit_code=0))
1344+
fake_client = patched_sprites["client"]
1345+
sprite = _FakeSprite(name=SPRITE_NAME)
1346+
fake_client._sprites_by_name[SPRITE_NAME] = sprite
1347+
state = _make_state(manifest=Manifest(root="/workspace"))
1348+
inner = SpritesSandboxSession.from_state(state, token="tok")
1349+
_attach(inner, client=fake_client, sprite=sprite)
1350+
1351+
cap = SpritesPlatformContext()
1352+
cap.bind(inner)
1353+
out = await cap.instructions(state.manifest)
1354+
assert out is not None
1355+
assert "/workspace" in out
1356+
assert "--dir /workspace" in out
1357+
assert "sprite-env services create" in out
1358+
1359+
1360+
@pytest.mark.asyncio
1361+
async def test_platform_context_uses_actual_manifest_root(
1362+
patched_sprites: dict[str, Any],
1363+
) -> None:
1364+
"""The hint must use the agent's actual manifest.root, not a hardcoded path."""
1365+
1366+
fake_control = patched_sprites["control"]
1367+
fake_control.next_ops.append(_FakeOpConn(stdout=b"# Sprite\n", exit_code=0))
1368+
fake_client = patched_sprites["client"]
1369+
sprite = _FakeSprite(name=SPRITE_NAME)
1370+
fake_client._sprites_by_name[SPRITE_NAME] = sprite
1371+
state = _make_state(manifest=Manifest(root="/var/agent-home"))
1372+
inner = SpritesSandboxSession.from_state(state, token="tok")
1373+
_attach(inner, client=fake_client, sprite=sprite)
1374+
1375+
cap = SpritesPlatformContext()
1376+
cap.bind(inner)
1377+
out = await cap.instructions(state.manifest)
1378+
assert out is not None
1379+
assert "/var/agent-home" in out
1380+
assert "--dir /var/agent-home" in out

0 commit comments

Comments
 (0)