Skip to content

Commit c9ee2fa

Browse files
committed
Restrict Daytona verification to computer use
1 parent 9f5659f commit c9ee2fa

2 files changed

Lines changed: 150 additions & 30 deletions

File tree

pithos/runtime_plugins.py

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ def _run_pi_agent(
962962
env=_local_pi_env(app, agent_cfg, computer_use_bin=computer_use_bin),
963963
transcript_path=str(transcript),
964964
progress_prefix=f"[verify {finding.id}]",
965-
tools=["read", "bash"],
965+
tools=_live_agent_tools(computer_use_mode),
966966
system_prompt=_localize_live_agent_paths(
967967
_live_agent_system_prompt(computer_use_mode),
968968
repo_path=app.repo_copy,
@@ -1010,7 +1010,7 @@ def _run_pi_agent(
10101010
model=model,
10111011
transcript_path=str(transcript),
10121012
progress_prefix=f"[verify:{finding.id}]",
1013-
tools=["read", "bash"],
1013+
tools=_live_agent_tools(computer_use_mode),
10141014
system_prompt=_live_agent_system_prompt(computer_use_mode),
10151015
event_sink=getattr(self, "event_sink", NULL_EVENT_SINK),
10161016
)
@@ -1049,25 +1049,27 @@ def _verdict_from_agent_result(
10491049
)
10501050
verdict = _verdict_from_payload(finding, plan, verdict_data, plugin=self.name)
10511051
verdict.artifacts["transcript"] = str(finding_dir / "live-agent-transcript.jsonl")
1052-
if _profile_computer_use_mode(self.profile) == COMPUTER_USE_DAYTONA and not (
1053-
_agent_result_has_daytona_command(result)
1054-
):
1055-
return RuntimeVerdict(
1056-
finding_id=finding.id,
1057-
title=finding.title,
1058-
plugin=self.name,
1059-
status=RUNTIME_BLOCKED,
1060-
confidence="low",
1061-
evidence=[
1062-
"Daytona computer use was requested, but the live-agent transcript did not "
1063-
"include a bash tool call invoking any cu-* command.",
1064-
"The verifier requires computer-use evidence before accepting a Daytona "
1065-
"live-agent verdict.",
1066-
],
1067-
error="missing Daytona computer-use evidence",
1068-
artifacts={"transcript": str(finding_dir / "live-agent-transcript.jsonl")},
1069-
plan=plan.to_dict(),
1070-
)
1052+
if _profile_computer_use_mode(self.profile) == COMPUTER_USE_DAYTONA:
1053+
daytona_policy = _daytona_computer_only_policy(result)
1054+
if daytona_policy:
1055+
details = daytona_policy[:5]
1056+
if len(daytona_policy) > len(details):
1057+
details.append(f"... {len(daytona_policy) - len(details)} more violation(s)")
1058+
return RuntimeVerdict(
1059+
finding_id=finding.id,
1060+
title=finding.title,
1061+
plugin=self.name,
1062+
status=RUNTIME_BLOCKED,
1063+
confidence="low",
1064+
evidence=[
1065+
"Daytona computer use was requested, but verification did not happen "
1066+
"exclusively through cu-* computer-use commands.",
1067+
*details,
1068+
],
1069+
error="missing or invalid Daytona computer-use evidence",
1070+
artifacts={"transcript": str(finding_dir / "live-agent-transcript.jsonl")},
1071+
plan=plan.to_dict(),
1072+
)
10711073
return verdict
10721074

10731075
def _blocked(
@@ -1294,13 +1296,21 @@ def _profile_computer_use_mode(profile: AppRuntimeProfile) -> str:
12941296
return COMPUTER_USE_NONE
12951297

12961298

1299+
def _live_agent_tools(computer_use: str) -> list[str]:
1300+
if normalize_computer_use_mode(computer_use) == COMPUTER_USE_DAYTONA:
1301+
return ["bash"]
1302+
return ["read", "bash"]
1303+
1304+
12971305
def _localize_live_agent_paths(text: str, *, repo_path: Path, artifacts_path: Path) -> str:
12981306
return text.replace("/work/repo", str(repo_path)).replace(
12991307
"/work/artifacts", str(artifacts_path)
13001308
)
13011309

13021310

1303-
def _agent_result_has_daytona_command(result) -> bool:
1311+
def _daytona_computer_only_policy(result) -> list[str]:
1312+
violations: list[str] = []
1313+
saw_computer_use = False
13041314
for msg in getattr(result, "messages", []) or []:
13051315
if msg.get("role") != "assistant":
13061316
continue
@@ -1310,13 +1320,33 @@ def _agent_result_has_daytona_command(result) -> bool:
13101320
for block in content:
13111321
if not isinstance(block, dict) or block.get("type") != "toolCall":
13121322
continue
1313-
if block.get("name") != "bash":
1323+
name = str(block.get("name") or "")
1324+
if name != "bash":
1325+
violations.append(f"non-computer tool used: {name}")
13141326
continue
13151327
args = block.get("arguments")
13161328
command = args.get("command") if isinstance(args, dict) else ""
1317-
if any(cmd in str(command) for cmd in DAYTONA_COMMANDS):
1318-
return True
1319-
return False
1329+
if _bash_command_is_computer_use_only(str(command)):
1330+
saw_computer_use = True
1331+
else:
1332+
violations.append(f"non-computer bash command used: {command}")
1333+
if not saw_computer_use:
1334+
violations.insert(0, "no cu-* computer-use command was run")
1335+
return violations
1336+
1337+
1338+
def _bash_command_is_computer_use_only(command: str) -> bool:
1339+
if not command.strip():
1340+
return False
1341+
if any(token in command for token in ("|", "`", "$(")):
1342+
return False
1343+
for segment in re.split(r"\s*(?:&&|\|\||;|\n)\s*", command):
1344+
segment = segment.strip()
1345+
if not segment:
1346+
continue
1347+
if segment.split()[0] not in DAYTONA_COMMANDS:
1348+
return False
1349+
return True
13201350

13211351

13221352
def _extract_verdict_payload(text: str) -> dict[str, object] | None:
@@ -1398,8 +1428,10 @@ def _live_agent_system_prompt(computer_use: str = COMPUTER_USE_NONE) -> str:
13981428
- Do not start Xvfb, XFCE, x11vnc, noVNC, or a nested Daytona sandbox yourself.
13991429
- Use bash-accessible local commands to control the existing desktop:
14001430
`cu-info`, `cu-screenshot`, `cu-click`, `cu-type`, `cu-key`, `cu-scroll`, and `cu-drag`.
1401-
- Your first bash actions must run `cu-info` and `cu-screenshot` before reading source, installing
1402-
dependencies, running tests, or writing reproduction scripts.
1431+
- Verification must happen only through these `cu-*` computer-use commands. Do not use the read tool,
1432+
do not inspect source files through shell commands, do not install dependencies, do not run tests,
1433+
and do not write reproduction scripts.
1434+
- Your first bash actions must run `cu-info` and `cu-screenshot`.
14031435
- Take another screenshot after desktop/UI actions, and use the image evidence to decide whether the
14041436
action affected the expected screen.
14051437
- Include the `cu-*` commands you ran and their artifact paths in the final runtime verdict evidence.

tests/test_runtime_verifier.py

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -716,8 +716,12 @@ def test_daytona_computer_use_reaches_local_live_agent(tmp_path, monkeypatch):
716716
monkeypatch.delenv("DAYTONA_JWT_TOKEN", raising=False)
717717

718718
async def fake_run_agent_process(prompt, **kwargs):
719-
assert kwargs["tools"] == ["read", "bash"]
719+
assert kwargs["tools"] == ["bash"]
720720
assert "Daytona Computer Use" in kwargs["system_prompt"]
721+
assert (
722+
"Verification must happen only through these `cu-*` computer-use commands"
723+
in kwargs["system_prompt"]
724+
)
721725
assert "cu-screenshot" in kwargs["system_prompt"]
722726
assert "Do not start Xvfb" in kwargs["system_prompt"]
723727
assert kwargs["env"]["PITHOS_COMPUTER_USE"] == "daytona"
@@ -848,7 +852,91 @@ async def fake_run_agent_process(prompt, **kwargs):
848852
)
849853

850854
assert result.verdicts[0].status == "blocked"
851-
assert result.verdicts[0].error == "missing Daytona computer-use evidence"
855+
assert result.verdicts[0].error == "missing or invalid Daytona computer-use evidence"
856+
857+
858+
def test_daytona_computer_use_blocks_mixed_source_verification(tmp_path, monkeypatch):
859+
repo = tmp_path / "app"
860+
repo.mkdir()
861+
profile = repo / ".pithos" / "runtime.yaml"
862+
profile.parent.mkdir()
863+
profile.write_text(
864+
"""
865+
verification:
866+
execute_app: true
867+
environment:
868+
sandbox: local
869+
""",
870+
encoding="utf-8",
871+
)
872+
results = tmp_path / "results" / "app" / "run"
873+
results.mkdir(parents=True)
874+
(results / "run-summary.json").write_text(json.dumps({"repo": {"path": str(repo)}}))
875+
(results / "VULN-FINDINGS.json").write_text(
876+
json.dumps(
877+
[
878+
{
879+
"id": "F001",
880+
"title": "Generic live authorization bypass",
881+
"category": "authorization bypass",
882+
"files": ["src/server.ts"],
883+
}
884+
]
885+
)
886+
)
887+
(results / "TRIAGE.json").write_text(
888+
json.dumps({"findings": [{"id": "F001", "title": "Generic live authorization bypass"}]})
889+
)
890+
891+
async def fake_run_agent_process(prompt, **kwargs):
892+
payload = json.dumps(
893+
{
894+
"status": "not_reproduced",
895+
"confidence": "high",
896+
"evidence": ["ran screenshot and source verification"],
897+
}
898+
)
899+
return SimpleNamespace(
900+
error=None,
901+
messages=[
902+
{
903+
"role": "assistant",
904+
"content": [
905+
{
906+
"type": "toolCall",
907+
"name": "bash",
908+
"arguments": {"command": "cu-info && cu-screenshot before.png"},
909+
},
910+
{
911+
"type": "toolCall",
912+
"name": "bash",
913+
"arguments": {"command": "cat src/server.ts"},
914+
},
915+
],
916+
}
917+
],
918+
last_assistant_message=payload,
919+
find_tagged_message=lambda _tag: payload,
920+
)
921+
922+
monkeypatch.setattr(runtime_plugins, "run_agent_process", fake_run_agent_process)
923+
monkeypatch.setattr(
924+
runtime_plugins.docker_ops,
925+
"run",
926+
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("docker was called")),
927+
)
928+
929+
result = run_verify_repo(
930+
triage_path=results,
931+
execute_app=True,
932+
provider="anthropic",
933+
model="claude-sonnet-4-5",
934+
sandbox_mode="local",
935+
computer_use="daytona",
936+
)
937+
938+
assert result.verdicts[0].status == "blocked"
939+
assert any("cat src/server.ts" in e for e in result.verdicts[0].evidence)
852940

853941

854942
def test_verify_repo_confirms_polar_style_source_oracles(tmp_path):

0 commit comments

Comments
 (0)