564 lines
19 KiB
Python
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}")
|