Add command policy audit trail
This commit is contained in:
@@ -323,3 +323,57 @@ def test_password_stream_runs_sudo_with_password_and_streams_answer(tmp_path, mo
|
||||
assert "event: content_delta" in body
|
||||
assert "sudo command completed" in body
|
||||
assert "secret" not in body
|
||||
|
||||
|
||||
def test_command_audit_endpoint_exposes_redacted_shell_events(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
||||
|
||||
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
|
||||
assert role == "action"
|
||||
actions = []
|
||||
if not any("tool_observations" in message["content"] for message in messages):
|
||||
actions = [
|
||||
{
|
||||
"tool": "shell_exec_safe",
|
||||
"args": {"command": "apt list --upgradable"},
|
||||
"reason": "Check available updates",
|
||||
}
|
||||
]
|
||||
return ModelResponse(
|
||||
role=role,
|
||||
model="local-main",
|
||||
content=json.dumps(
|
||||
{
|
||||
"kind": "action_directive",
|
||||
"intent": "check updates",
|
||||
"risk_level": "low",
|
||||
"actions": actions,
|
||||
}
|
||||
),
|
||||
reasoning_content=None,
|
||||
raw={},
|
||||
latency_ms=1.0,
|
||||
)
|
||||
|
||||
async def fake_stream_chat(self, role, messages):
|
||||
yield {"type": "content_delta", "delta": "updates checked"}
|
||||
|
||||
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
|
||||
monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat)
|
||||
client = TestClient(create_app())
|
||||
|
||||
with client.stream(
|
||||
"POST",
|
||||
"/v1/chat/stream",
|
||||
json={"message": "check updates", "workspace": str(tmp_path), "debug": True},
|
||||
) as response:
|
||||
_ = "".join(response.iter_text())
|
||||
audit = client.get("/v1/audit/commands").json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert audit[0]["event_type"] == "command_audit"
|
||||
assert audit[0]["payload"]["command"] == "apt list --upgradable"
|
||||
assert audit[0]["payload"]["risk_level"] == "low"
|
||||
assert audit[0]["payload"]["action_type"] == "package_check"
|
||||
assert audit[0]["payload"]["approved"] is False
|
||||
assert "password" not in json.dumps(audit).lower()
|
||||
|
||||
@@ -3,6 +3,7 @@ import pytest
|
||||
from duck_core.tools.file_read import FileReadTool
|
||||
from duck_core.tools.file_write import FileWriteTool
|
||||
from duck_core.tools.gateway import ToolGateway
|
||||
from duck_core.tools.command_policy import CommandPolicy
|
||||
from duck_core.tools.shell_exec_safe import ShellExecSafeTool
|
||||
|
||||
|
||||
@@ -139,3 +140,22 @@ async def test_shell_tool_allows_read_only_apt_update_check(monkeypatch, tmp_pat
|
||||
assert result.ok is True
|
||||
assert "bootlogd" in result.output
|
||||
assert result.metadata["command"] == "apt list --upgradable"
|
||||
|
||||
|
||||
def test_command_policy_classifies_common_system_commands():
|
||||
readonly = CommandPolicy.classify("apt list --upgradable")
|
||||
sudo_update = CommandPolicy.classify("sudo apt update")
|
||||
install = CommandPolicy.classify("sudo apt install vim")
|
||||
destructive = CommandPolicy.classify("rm -rf .")
|
||||
|
||||
assert readonly.risk_level == "low"
|
||||
assert readonly.action_type == "package_check"
|
||||
assert readonly.requires_approval is False
|
||||
assert sudo_update.risk_level == "high"
|
||||
assert sudo_update.action_type == "package_cache_update"
|
||||
assert sudo_update.requires_password is True
|
||||
assert install.risk_level == "high"
|
||||
assert install.action_type == "package_install"
|
||||
assert install.requires_password is True
|
||||
assert destructive.risk_level == "critical"
|
||||
assert destructive.blocked is True
|
||||
|
||||
Reference in New Issue
Block a user