diff --git a/agents/prompts/reviewer.md b/agents/prompts/reviewer.md index 8838e3c..4a5066b 100644 --- a/agents/prompts/reviewer.md +++ b/agents/prompts/reviewer.md @@ -40,7 +40,27 @@ You receive: ## Output format -Return ONLY valid JSON (no markdown, no explanation): +Return TWO sections in your response: + +### Section 1 — `## Verdict` (human-readable, in Russian) + +2-3 sentences in plain Russian for the project director: what was checked, whether everything is OK, are there any issues, can the task be closed. No JSON, no technical terms, no code snippets. + +Example: +``` +## Verdict +Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать. +``` + +Another example (with issues): +``` +## Verdict +Проверка выявила критическую проблему: SQL-запрос уязвим к инъекциям. Также отсутствуют тесты для нового эндпоинта. Задачу нельзя закрывать до исправления. +``` + +### Section 2 — `## Details` (JSON block for agents) + +The full technical output in JSON, wrapped in a ```json code fence: ```json { @@ -71,6 +91,25 @@ If verdict is "changes_requested", findings must be non-empty with actionable su If verdict is "revise", include `"target_role": "..."` and findings must be non-empty with actionable suggestions. If verdict is "blocked", include `"blocked_reason": "..."` (e.g. unable to read files). +**Full response example:** + +``` +## Verdict +Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать. + +## Details +```json +{ + "verdict": "approved", + "findings": [...], + "security_issues": [], + "conventions_violations": [], + "test_coverage": "adequate", + "summary": "..." +} +` ` ` +``` + ## Verdict definitions ### verdict: "revise" diff --git a/tests/test_deploy.py b/tests/test_deploy.py index c29807b..3f4bf20 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -516,6 +516,39 @@ class TestSSHBuildCmd: assert "cd" not in full_cmd_arg assert full_cmd_arg == "git pull" + def test_deploy_path_with_semicolon_injection_is_escaped(self): + """Path containing ';' must be quoted so it cannot inject a second shell command.""" + project = { + "deploy_host": "myserver", + "deploy_path": "/srv/api; rm -rf /", + } + cmd = _build_ssh_cmd(project, "git pull") + full_cmd_arg = cmd[-1] + # The dangerous path must appear only as a quoted argument, not as a bare shell fragment. + assert "cd /srv/api; rm -rf /" not in full_cmd_arg + # shlex.quote wraps it in single quotes — the semicolon is inside the quotes. + assert shlex.quote("/srv/api; rm -rf /") in full_cmd_arg + + def test_deploy_restart_cmd_is_not_shlex_quoted(self): + """deploy_restart_cmd must reach SSH as a plain shell command, not as a single quoted arg. + + shlex.quote would turn 'docker compose restart worker' into a literal string + which the remote shell would refuse to execute. Admin-controlled field — no quoting. + """ + project = { + "deploy_host": "myserver", + "deploy_path": "/srv/api", + "deploy_runtime": "docker", + "deploy_restart_cmd": "docker compose restart worker", + } + # Build steps manually and feed one step into _build_ssh_cmd. + restart_cmd = "docker compose restart worker" + cmd = _build_ssh_cmd(project, restart_cmd) + full_cmd_arg = cmd[-1] + # The command must appear verbatim (not as a single quoted token). + assert "docker compose restart worker" in full_cmd_arg + assert full_cmd_arg != shlex.quote("docker compose restart worker") + # --------------------------------------------------------------------------- # 9. deploy_with_dependents — cascade deploy unit tests diff --git a/web/frontend/src/__tests__/deploy-api.test.ts b/web/frontend/src/__tests__/deploy-api.test.ts index 752bba8..3e2582d 100644 --- a/web/frontend/src/__tests__/deploy-api.test.ts +++ b/web/frontend/src/__tests__/deploy-api.test.ts @@ -38,7 +38,7 @@ describe('api.projectLinks', () => { it('возвращает массив ProjectLink', async () => { const links = [ - { id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '2026-01-01' }, + { id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01' }, ] mockFetch(links) const result = await api.projectLinks('KIN')