Skip to content

Commit 4cac757

Browse files
committed
fix(fly): warn agents to pass --dir to sprite-env services create
Recurring agent confusion: ``sprite-env services create`` runs services with cwd=$HOME by default, NOT the workspace. Agents create ``python3 -m http.server`` services without ``--dir`` and get directory listings of the home dir instead of their workspace files. The ``sprite-env`` CLI does support ``--dir <path>`` to set the service's working directory, but ``/.sprite/llm.txt`` (the platform's LLM-facing doc) doesn't currently mention it — agents have no way to discover it. Appends a ``--dir``-flagged guidance paragraph to FlyPlatformContext's framing, interpolated with the agent's actual ``manifest.root`` so the example is concretely correct for whatever workspace the application configured. Cache key extended to 3-tuple so different roots produce different cached strings. Two regression tests confirm the framing references ``--dir`` with the actual workspace root and works for non-default roots.
1 parent 7a652c0 commit 4cac757

2 files changed

Lines changed: 78 additions & 10 deletions

File tree

src/agents/extensions/sandbox/flyio/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 FlyPlatformContext(Capability):
@@ -83,13 +83,15 @@ class FlyPlatformContext(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_fly.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = FlySandboxSession.from_state(state, token="tok")
1349+
_attach(inner, client=fake_client, sprite=sprite)
1350+
1351+
cap = FlyPlatformContext()
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 = FlySandboxSession.from_state(state, token="tok")
1373+
_attach(inner, client=fake_client, sprite=sprite)
1374+
1375+
cap = FlyPlatformContext()
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)