Files
agent-desktop/tests/integration/test_security_config.py
T

564 lines
19 KiB
Python

# -*- coding: utf-8 -*-
"""HTTP smoke tests for security guards (file/tool guard + skill scanner)."""
from __future__ import annotations
import pytest
@pytest.mark.integration
@pytest.mark.p0
def test_global_sandbox_put_get_roundtrip(app_server) -> None:
"""Test purpose:
- Verify the global sandbox switch supports update and readback, and that
the change is hot-reloaded (no restart) via the mtime-cached config.
Test flow:
1. GET current sandbox switch as baseline.
2. PUT toggled ``enabled`` value; assert echo matches.
3. GET sandbox switch and assert the new value is visible (hot-reload).
4. PUT the toggled value back to the original and re-read to confirm the
switch flips both ways without a restart.
5. Restore original value.
API endpoints:
- GET /api/config/security/sandbox
- PUT /api/config/security/sandbox
"""
get_before = app_server.api_request(
"GET",
"/api/config/security/sandbox",
)
assert get_before.status_code == 200, app_server.logs_tail()
before = get_before.json()
assert isinstance(before, dict)
assert "enabled" in before
original_enabled = bool(before.get("enabled", False))
toggled_enabled = not original_enabled
try:
put_resp = app_server.api_request(
"PUT",
"/api/config/security/sandbox",
json={"enabled": toggled_enabled},
)
assert put_resp.status_code == 200, app_server.logs_tail()
assert bool(put_resp.json().get("enabled")) == toggled_enabled
get_after = app_server.api_request(
"GET",
"/api/config/security/sandbox",
)
assert get_after.status_code == 200, app_server.logs_tail()
assert bool(get_after.json().get("enabled")) == toggled_enabled
# Flip back to the original value and confirm the switch hot-reloads
# both directions (save_config invalidates the mtime cache).
put_back = app_server.api_request(
"PUT",
"/api/config/security/sandbox",
json={"enabled": original_enabled},
)
assert put_back.status_code == 200, app_server.logs_tail()
get_back = app_server.api_request(
"GET",
"/api/config/security/sandbox",
)
assert get_back.status_code == 200, app_server.logs_tail()
assert bool(get_back.json().get("enabled")) == original_enabled
finally:
restore = app_server.api_request(
"PUT",
"/api/config/security/sandbox",
json={"enabled": original_enabled},
)
assert restore.status_code == 200, app_server.logs_tail()
@pytest.mark.integration
@pytest.mark.p0
def test_global_file_guard_put_get_roundtrip(app_server) -> None:
"""Test purpose:
- Verify file-guard enabled flag supports update and readback.
Test flow:
1. GET current file-guard config.
2. PUT toggled ``enabled`` value.
3. GET file-guard and assert new value is visible.
4. Restore original value.
API endpoints:
- GET /api/config/security/file-guard
- PUT /api/config/security/file-guard
"""
get_before = app_server.api_request(
"GET",
"/api/config/security/file-guard",
)
assert get_before.status_code == 200, app_server.logs_tail()
before = get_before.json()
assert isinstance(before, dict)
assert "enabled" in before
updated_enabled = not bool(before.get("enabled", True))
try:
put_resp = app_server.api_request(
"PUT",
"/api/config/security/file-guard",
json={"enabled": updated_enabled},
)
assert put_resp.status_code == 200, app_server.logs_tail()
assert bool(put_resp.json().get("enabled")) == updated_enabled
get_after = app_server.api_request(
"GET",
"/api/config/security/file-guard",
)
assert get_after.status_code == 200, app_server.logs_tail()
after = get_after.json()
assert bool(after.get("enabled")) == updated_enabled
for k, v in before.items():
if k != "enabled":
assert after.get(k) == v, f"side-effect on {k}"
finally:
restore = app_server.api_request(
"PUT",
"/api/config/security/file-guard",
json={"enabled": before.get("enabled", True)},
)
assert restore.status_code == 200, app_server.logs_tail()
@pytest.mark.integration
@pytest.mark.p0
def test_global_tool_guard_put_get_roundtrip(app_server) -> None:
"""Test purpose:
- Verify tool-guard enabled flag supports update and readback.
Test flow:
1. GET current tool-guard config.
2. PUT same payload with toggled ``enabled``.
3. GET tool-guard and verify ``enabled`` changed.
4. Restore original payload.
API endpoints:
- GET /api/config/security/tool-guard
- PUT /api/config/security/tool-guard
"""
get_before = app_server.api_request(
"GET",
"/api/config/security/tool-guard",
)
assert get_before.status_code == 200, app_server.logs_tail()
before = get_before.json()
assert isinstance(before, dict)
assert "enabled" in before
updated = dict(before)
updated["enabled"] = not bool(before.get("enabled", True))
try:
put_resp = app_server.api_request(
"PUT",
"/api/config/security/tool-guard",
json=updated,
)
assert put_resp.status_code == 200, app_server.logs_tail()
assert bool(put_resp.json().get("enabled")) == updated["enabled"]
get_after = app_server.api_request(
"GET",
"/api/config/security/tool-guard",
)
assert get_after.status_code == 200, app_server.logs_tail()
after = get_after.json()
assert bool(after.get("enabled")) == updated["enabled"]
for k, v in before.items():
if k != "enabled":
assert after.get(k) == v, f"side-effect on {k}"
finally:
restore = app_server.api_request(
"PUT",
"/api/config/security/tool-guard",
json=before,
)
assert restore.status_code == 200, app_server.logs_tail()
@pytest.mark.integration
@pytest.mark.p0
def test_global_skill_scanner_put_get_roundtrip(app_server) -> None:
"""Test purpose:
- Verify skill-scanner config accepts updates and readback remains
consistent.
Test flow:
1. GET /api/config/security/skill-scanner as baseline.
2. PUT same payload with toggled ``mode``.
3. GET /api/config/security/skill-scanner and verify changed value.
4. Restore baseline payload.
API endpoints:
- GET /api/config/security/skill-scanner
- PUT /api/config/security/skill-scanner
"""
get_before = app_server.api_request(
"GET",
"/api/config/security/skill-scanner",
)
assert get_before.status_code == 200, app_server.logs_tail()
before = get_before.json()
assert isinstance(before, dict)
assert "mode" in before
updated = dict(before)
updated["mode"] = "off" if before.get("mode") != "off" else "warn"
try:
put_resp = app_server.api_request(
"PUT",
"/api/config/security/skill-scanner",
json=updated,
)
assert put_resp.status_code == 200, app_server.logs_tail()
assert put_resp.json().get("mode") == updated["mode"]
get_after = app_server.api_request(
"GET",
"/api/config/security/skill-scanner",
)
assert get_after.status_code == 200, app_server.logs_tail()
after = get_after.json()
assert after.get("mode") == updated["mode"]
for k, v in before.items():
if k != "mode":
assert after.get(k) == v, f"side-effect on {k}"
finally:
restore = app_server.api_request(
"PUT",
"/api/config/security/skill-scanner",
json=before,
)
assert restore.status_code == 200, app_server.logs_tail()
@pytest.mark.integration
@pytest.mark.p1
def test_agent_scoped_skill_scanner_put_get_roundtrip(app_server) -> None:
"""Test purpose:
- Verify agent-scoped skill-scanner config supports update and readback
without using X-Agent-Id header.
Test flow:
1. Create a dedicated test agent.
2. GET /api/agents/{agentId}/config/security/skill-scanner as baseline.
3. PUT the same endpoint with a changed ``mode`` value.
4. GET again and assert updated value is persisted.
5. Restore baseline and delete test agent.
API endpoints:
- POST /api/agents
- GET /api/agents/{agentId}/config/security/skill-scanner
- PUT /api/agents/{agentId}/config/security/skill-scanner
- DELETE /api/agents/{agentId}
"""
agent_id = "integ_scoped_skill_scanner_01"
endpoint = f"/api/agents/{agent_id}/config/security/skill-scanner"
create_agent = app_server.api_request(
"POST",
"/api/agents",
json={
"id": agent_id,
"name": "Scoped skill scanner agent",
"description": "",
},
)
assert create_agent.status_code == 201, app_server.logs_tail()
baseline = None
try:
get_before = app_server.api_request("GET", endpoint)
assert get_before.status_code == 200, app_server.logs_tail()
baseline = get_before.json()
assert isinstance(baseline, dict)
assert "mode" in baseline
updated = dict(baseline)
updated["mode"] = (
"block" if baseline.get("mode") != "block" else "warn"
)
put_resp = app_server.api_request("PUT", endpoint, json=updated)
assert put_resp.status_code == 200, app_server.logs_tail()
assert put_resp.json().get("mode") == updated["mode"]
get_after = app_server.api_request("GET", endpoint)
assert get_after.status_code == 200, app_server.logs_tail()
after = get_after.json()
assert after.get("mode") == updated["mode"]
for k, v in baseline.items():
if k != "mode":
assert after.get(k) == v, f"side-effect on {k}"
finally:
if isinstance(baseline, dict):
restore = app_server.api_request("PUT", endpoint, json=baseline)
assert restore.status_code == 200, app_server.logs_tail()
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
@pytest.mark.integration
@pytest.mark.p1
def test_agent_scoped_skill_scanner_whitelist_add_delete(app_server) -> None:
"""Test purpose:
- Verify agent-scoped skill-scanner whitelist supports add and remove flow.
Test flow:
1. Create a dedicated test agent.
2. POST whitelist endpoint with a unique skill name.
3. GET scoped skill-scanner config and assert whitelist contains the skill.
4. DELETE whitelist entry by skill name.
5. GET config again and assert whitelist no longer contains it.
6. Delete test agent.
API endpoints:
- POST /api/agents
- POST /api/agents/{agentId}/config/security/skill-scanner/whitelist
- GET /api/agents/{agentId}/config/security/skill-scanner
- DELETE .../skill-scanner/whitelist/{skill_name}
- DELETE /api/agents/{agentId}
"""
agent_id = "integ_scoped_skill_white_01"
skill_name = "integ_whitelist_skill_01"
base = f"/api/agents/{agent_id}/config/security/skill-scanner"
create_agent = app_server.api_request(
"POST",
"/api/agents",
json={
"id": agent_id,
"name": "Scoped skill whitelist agent",
"description": "",
},
)
assert create_agent.status_code == 201, app_server.logs_tail()
try:
add_resp = app_server.api_request(
"POST",
f"{base}/whitelist",
json={"skill_name": skill_name, "content_hash": "sha256:testhash"},
)
assert add_resp.status_code == 200, app_server.logs_tail()
assert add_resp.json().get("whitelisted") is True
assert add_resp.json().get("skill_name") == skill_name
get_after_add = app_server.api_request("GET", base)
assert get_after_add.status_code == 200, app_server.logs_tail()
whitelist_after_add = get_after_add.json().get("whitelist")
assert isinstance(whitelist_after_add, list)
skill_names_after_add = {
entry.get("skill_name") for entry in whitelist_after_add
}
assert skill_name in skill_names_after_add
del_resp = app_server.api_request(
"DELETE",
f"{base}/whitelist/{skill_name}",
)
assert del_resp.status_code == 200, app_server.logs_tail()
assert del_resp.json().get("removed") is True
assert del_resp.json().get("skill_name") == skill_name
get_after_del = app_server.api_request("GET", base)
assert get_after_del.status_code == 200, app_server.logs_tail()
whitelist_after_del = get_after_del.json().get("whitelist")
assert isinstance(whitelist_after_del, list)
skill_names_after_del = {
entry.get("skill_name") for entry in whitelist_after_del
}
assert skill_name not in skill_names_after_del
finally:
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
@pytest.mark.integration
@pytest.mark.p1
def test_agent_scoped_skill_scanner_blocked_history_delete_paths(
app_server,
) -> None:
"""Test purpose:
- Verify agent-scoped blocked-history delete paths keep stable contract for
clear-all and missing-index cases.
Test flow:
1. Create a dedicated test agent.
2. DELETE blocked-history and assert clear result.
3. DELETE blocked-history/{index} for missing index; assert 404.
4. Delete test agent.
API endpoints:
- POST /api/agents
- DELETE .../skill-scanner/blocked-history
- DELETE .../skill-scanner/blocked-history/{index}
- DELETE /api/agents/{agentId}
"""
agent_id = "integ_scoped_skill_hist_01"
base = (
f"/api/agents/{agent_id}/config/security/skill-scanner/blocked-history"
)
create_agent = app_server.api_request(
"POST",
"/api/agents",
json={
"id": agent_id,
"name": "Scoped skill history agent",
"description": "",
},
)
assert create_agent.status_code == 201, app_server.logs_tail()
try:
clear_resp = app_server.api_request("DELETE", base)
assert clear_resp.status_code == 200, app_server.logs_tail()
assert clear_resp.json().get("cleared") is True
remove_missing = app_server.api_request("DELETE", f"{base}/0")
assert remove_missing.status_code == 404, app_server.logs_tail()
assert "detail" in remove_missing.json()
finally:
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
@pytest.mark.integration
@pytest.mark.p1
def test_agent_scoped_file_guard_put_get_roundtrip(app_server) -> None:
"""Test purpose:
- Verify agent-scoped file-guard endpoint supports update and readback.
Test flow:
1. Create a dedicated test agent.
2. GET scoped file-guard config as baseline.
3. PUT scoped file-guard with toggled ``enabled`` value.
4. GET again and verify updated value is persisted.
5. Restore baseline and delete test agent.
API endpoints:
- POST /api/agents
- GET /api/agents/{agentId}/config/security/file-guard
- PUT /api/agents/{agentId}/config/security/file-guard
- DELETE /api/agents/{agentId}
"""
agent_id = "integ_scoped_file_guard_01"
endpoint = f"/api/agents/{agent_id}/config/security/file-guard"
create_agent = app_server.api_request(
"POST",
"/api/agents",
json={
"id": agent_id,
"name": "Scoped file guard agent",
"description": "",
},
)
assert create_agent.status_code == 201, app_server.logs_tail()
before = None
try:
get_before = app_server.api_request("GET", endpoint)
assert get_before.status_code == 200, app_server.logs_tail()
before = get_before.json()
assert isinstance(before, dict)
assert "enabled" in before
updated_enabled = not bool(before.get("enabled", True))
put_resp = app_server.api_request(
"PUT",
endpoint,
json={"enabled": updated_enabled},
)
assert put_resp.status_code == 200, app_server.logs_tail()
assert bool(put_resp.json().get("enabled")) == updated_enabled
get_after = app_server.api_request("GET", endpoint)
assert get_after.status_code == 200, app_server.logs_tail()
after = get_after.json()
assert bool(after.get("enabled")) == updated_enabled
for k, v in before.items():
if k != "enabled":
assert after.get(k) == v, f"side-effect on {k}"
finally:
if isinstance(before, dict):
restore = app_server.api_request(
"PUT",
endpoint,
json={"enabled": before.get("enabled", True)},
)
assert restore.status_code == 200, app_server.logs_tail()
app_server.api_request("DELETE", f"/api/agents/{agent_id}")
@pytest.mark.integration
@pytest.mark.p1
def test_agent_scoped_tool_guard_put_get_roundtrip(app_server) -> None:
"""Test purpose:
- Verify agent-scoped tool-guard endpoint supports update and readback.
Test flow:
1. Create a dedicated test agent.
2. GET scoped tool-guard config as baseline.
3. PUT scoped tool-guard with toggled ``enabled`` value.
4. GET again and verify updated value is persisted.
5. Restore baseline and delete test agent.
API endpoints:
- POST /api/agents
- GET /api/agents/{agentId}/config/security/tool-guard
- PUT /api/agents/{agentId}/config/security/tool-guard
- DELETE /api/agents/{agentId}
"""
agent_id = "integ_scoped_tool_guard_01"
endpoint = f"/api/agents/{agent_id}/config/security/tool-guard"
create_agent = app_server.api_request(
"POST",
"/api/agents",
json={
"id": agent_id,
"name": "Scoped tool guard agent",
"description": "",
},
)
assert create_agent.status_code == 201, app_server.logs_tail()
before = None
try:
get_before = app_server.api_request("GET", endpoint)
assert get_before.status_code == 200, app_server.logs_tail()
before = get_before.json()
assert isinstance(before, dict)
assert "enabled" in before
updated = dict(before)
updated["enabled"] = not bool(before.get("enabled", True))
put_resp = app_server.api_request("PUT", endpoint, json=updated)
assert put_resp.status_code == 200, app_server.logs_tail()
assert bool(put_resp.json().get("enabled")) == updated["enabled"]
get_after = app_server.api_request("GET", endpoint)
assert get_after.status_code == 200, app_server.logs_tail()
after = get_after.json()
assert bool(after.get("enabled")) == updated["enabled"]
for k, v in before.items():
if k != "enabled":
assert after.get(k) == v, f"side-effect on {k}"
finally:
if isinstance(before, dict):
restore = app_server.api_request("PUT", endpoint, json=before)
assert restore.status_code == 200, app_server.logs_tail()
app_server.api_request("DELETE", f"/api/agents/{agent_id}")