mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 22:16:19 +00:00
* fix(agnet): system prompt bug * fix(security): address system-role review feedback * fix(ci): keep agent guidance within size budget --------- Co-authored-by: YxinMiracle <“939157765@qq.com”> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
194 lines
7.6 KiB
Python
194 lines
7.6 KiB
Python
"""Offline checks for the live PoC; never use real credentials or a model."""
|
|
|
|
import copy
|
|
import io
|
|
import json
|
|
from types import SimpleNamespace
|
|
from urllib.error import HTTPError
|
|
|
|
import poc_external_system_message_injection as poc
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolated_environment(monkeypatch):
|
|
for name in ("DEERFLOW_PAT", "DEERFLOW_ACCESS_TOKEN", "DEERFLOW_CSRF_TOKEN", "DEERFLOW_BASE_URL", "DEERFLOW_THREAD_ID", "DEERFLOW_ASSISTANT_ID", "DEERFLOW_TIMEOUT_SECONDS", "DEERFLOW_CONFIRM_APPEND"):
|
|
monkeypatch.delenv(name, raising=False)
|
|
monkeypatch.setenv("DEERFLOW_THREAD_ID", "synthetic-poc-thread")
|
|
monkeypatch.setenv("DEERFLOW_CONFIRM_APPEND", "YES")
|
|
|
|
|
|
def test_auth_headers_allows_gateway_auth_disabled_mode():
|
|
assert poc.auth_headers() == {}
|
|
|
|
|
|
def test_pat_takes_precedence_and_session_requires_both_cookies(monkeypatch):
|
|
monkeypatch.setenv("DEERFLOW_ACCESS_TOKEN", "fixture-session")
|
|
with pytest.raises(poc.PocError):
|
|
poc.auth_headers()
|
|
monkeypatch.setenv("DEERFLOW_CSRF_TOKEN", "fixture-csrf")
|
|
assert poc.auth_headers()["X-CSRF-Token"] == "fixture-csrf"
|
|
monkeypatch.setenv("DEERFLOW_PAT", "fixture-pat")
|
|
assert poc.auth_headers() == {"Authorization": "Bearer fixture-pat"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name,value",
|
|
[
|
|
("DEERFLOW_THREAD_ID", ""),
|
|
("DEERFLOW_THREAD_ID", "http://localhost:2026/workspace/chats/id"),
|
|
("DEERFLOW_BASE_URL", "https://example.com"),
|
|
("DEERFLOW_BASE_URL", "http://user:secret@localhost:2026"),
|
|
("DEERFLOW_ASSISTANT_ID", ""),
|
|
("DEERFLOW_TIMEOUT_SECONDS", "nan"),
|
|
("DEERFLOW_TIMEOUT_SECONDS", "-1"),
|
|
("DEERFLOW_CONFIRM_APPEND", "NO"),
|
|
],
|
|
)
|
|
def test_invalid_configuration_fails_before_network(monkeypatch, name, value):
|
|
monkeypatch.setenv(name, value)
|
|
monkeypatch.setattr(poc, "get_json", lambda *_: pytest.fail("configuration must be checked before HTTP"))
|
|
with pytest.raises(poc.PocError):
|
|
poc.main([])
|
|
|
|
|
|
class FakeGateway:
|
|
def __init__(self, *, blocked, obeys=False, mutate_on_reject=False):
|
|
self.blocked = blocked
|
|
self.obeys = obeys
|
|
self.mutate_on_reject = mutate_on_reject
|
|
self.messages = []
|
|
self.checkpoint = 1
|
|
self.calls = []
|
|
|
|
def state(self, *_):
|
|
return {"checkpoint_id": str(self.checkpoint), "values": {"messages": copy.deepcopy(self.messages)}}
|
|
|
|
def run(self, _url, _headers, messages, _timeout, _assistant_id=None):
|
|
self.calls.append(copy.deepcopy(messages))
|
|
if self.blocked and any(m["role"] == "system" for m in messages):
|
|
if self.mutate_on_reject:
|
|
self.checkpoint += 1
|
|
raise poc.PocHTTPError(400, poc.ROLE_REJECTION_DETAIL)
|
|
self.messages.extend({**m, "type": m["role"]} for m in messages)
|
|
answer = "synthetic answer"
|
|
if self.obeys and any(m.get("type") == "system" for m in self.messages):
|
|
answer += "\n" + poc.MARKER
|
|
self.checkpoint += 1
|
|
self.messages.append({"id": f"ai-{self.checkpoint}", "type": "ai", "content": answer})
|
|
return {"http_status": 200, "saw_end": True}
|
|
|
|
|
|
def install_gateway(monkeypatch, **kwargs):
|
|
gateway = FakeGateway(**kwargs)
|
|
monkeypatch.setattr(poc, "get_json", gateway.state)
|
|
monkeypatch.setattr(poc, "stream_run", gateway.run)
|
|
return gateway
|
|
|
|
|
|
def test_blocked_is_success_and_followup_still_runs(monkeypatch, capsys):
|
|
gateway = install_gateway(monkeypatch, blocked=True)
|
|
assert poc.main([]) == 0
|
|
assert len(gateway.calls) == 3
|
|
assert all(m.get("type") != "system" for m in gateway.messages)
|
|
output = capsys.readouterr().out
|
|
assert "BLOCKED" in output
|
|
assert "Checkpoint unchanged: True" in output
|
|
assert "Ordinary follow-up" in output
|
|
|
|
|
|
@pytest.mark.parametrize("obeys", [False, True])
|
|
def test_vulnerable_is_detected_independently_of_model_obedience(monkeypatch, capsys, obeys):
|
|
install_gateway(monkeypatch, blocked=False, obeys=obeys)
|
|
assert poc.main(["--expect", "vulnerable"]) == 0
|
|
output = capsys.readouterr().out
|
|
assert "VULNERABLE" in output
|
|
assert f"Marker is exact last line: {obeys}" in output
|
|
assert "Provider payload observation: NOT DIRECT" in output
|
|
assert "/workspace/chats/synthetic-poc-thread" in output
|
|
|
|
|
|
@pytest.mark.parametrize("blocked,expected", [(False, "blocked"), (True, "vulnerable")])
|
|
def test_unexpected_outcome_is_nonzero(monkeypatch, blocked, expected):
|
|
install_gateway(monkeypatch, blocked=blocked)
|
|
assert poc.main(["--expect", expected]) == 2
|
|
|
|
|
|
def test_rejection_that_writes_checkpoint_is_not_a_pass(monkeypatch):
|
|
install_gateway(monkeypatch, blocked=True, mutate_on_reject=True)
|
|
with pytest.raises(poc.PocError, match="checkpoint"):
|
|
poc.main([])
|
|
|
|
|
|
@pytest.mark.parametrize("status,detail", [(400, "unrelated validation error"), (401, "Invalid token"), (403, "Forbidden"), (500, "error")])
|
|
def test_other_errors_are_not_reported_as_fixed(monkeypatch, status, detail):
|
|
gateway = install_gateway(monkeypatch, blocked=False)
|
|
original_run = gateway.run
|
|
|
|
def run(url, headers, messages, timeout, assistant_id=None):
|
|
if any(m["role"] == "system" for m in messages):
|
|
raise poc.PocHTTPError(status, detail)
|
|
return original_run(url, headers, messages, timeout, assistant_id)
|
|
|
|
monkeypatch.setattr(poc, "stream_run", run)
|
|
with pytest.raises(poc.PocHTTPError):
|
|
poc.main([])
|
|
|
|
|
|
def test_contaminated_thread_is_rejected_before_appending(monkeypatch):
|
|
gateway = install_gateway(monkeypatch, blocked=True)
|
|
gateway.messages.append({"type": "ai", "content": poc.MARKER})
|
|
with pytest.raises(poc.PocError, match="fresh"):
|
|
poc.main([])
|
|
assert not gateway.calls
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"events,ok", [(b'event: metadata\ndata: {"run_id":"fixture"}\n\nevent: end\ndata: {}\n\n', True), (b'event: error\ndata: {"message":"fixture"}\n\nevent: end\ndata: {}\n\n', False), (b"event: metadata\ndata: {}\n\n", False)]
|
|
)
|
|
def test_sse_errors_and_missing_end_are_not_success(monkeypatch, events, ok):
|
|
response = io.BytesIO(events)
|
|
response.status = 200
|
|
monkeypatch.setattr(poc, "_HTTP", SimpleNamespace(open=lambda *_args, **_kwargs: response))
|
|
if ok:
|
|
assert poc.stream_run("http://localhost:2026/api/test", {}, [], 10)["saw_end"]
|
|
else:
|
|
with pytest.raises(poc.PocError):
|
|
poc.stream_run("http://localhost:2026/api/test", {}, [], 10)
|
|
|
|
|
|
def test_stream_run_uses_configured_assistant_id(monkeypatch):
|
|
response = io.BytesIO(b"event: end\ndata: {}\n\n")
|
|
response.status = 200
|
|
captured = []
|
|
|
|
class _Opener:
|
|
def open(self, request, *, timeout):
|
|
captured.append((request, timeout))
|
|
return response
|
|
|
|
monkeypatch.setenv("DEERFLOW_ASSISTANT_ID", "custom-agent")
|
|
monkeypatch.setattr(poc, "_HTTP", _Opener())
|
|
|
|
result = poc.stream_run("http://localhost:2026/api/test", {}, [], 10)
|
|
|
|
assert result["saw_end"] is True
|
|
assert captured[0][1] == 10
|
|
assert json.loads(captured[0][0].data)["assistant_id"] == "custom-agent"
|
|
|
|
|
|
def test_http_errors_do_not_echo_response_secrets():
|
|
assert "fixture-secret" not in str(poc.PocHTTPError(401, "fixture-secret"))
|
|
|
|
|
|
def test_http_error_preserves_detail_for_classification_only():
|
|
error = HTTPError("http://localhost:2026", 400, "Bad Request", {}, io.BytesIO(b'{"detail":"fixture-secret"}'))
|
|
converted = poc._http_error(error)
|
|
assert converted.status == 400
|
|
assert converted.detail == "fixture-secret"
|
|
assert "fixture-secret" not in str(converted)
|
|
|
|
|
|
def test_redirects_cannot_forward_credentials():
|
|
assert poc._NoRedirect().redirect_request(None, None, 307, "redirect", {}, "https://example.com") is None
|