From 5b2b09b000c6bc037e241c90741a6a9befd039d6 Mon Sep 17 00:00:00 2001 From: StanPonomarev Date: Sun, 19 Jul 2026 21:50:13 +0200 Subject: [PATCH] guard reviews against issue rediscovery --- Dockerfile | 12 ++++++- codex/config.toml | 19 +++++++++++ compose.yaml | 1 + scripts/entrypoint.sh | 26 +++++++++++++++ src/agentci/adapters/codex.py | 9 +++++- src/agentci/prompts/implementation_review.md | 25 ++++++++++++--- src/agentci/prompts/plan_review.md | 15 ++++++--- src/agentci/workflows/code_review.py | 33 ++++++++++++++++---- src/agentci/workflows/implement.py | 8 ++++- src/agentci/workflows/plan.py | 4 +-- src/agentci/workflows/pull_request.py | 15 +++++++-- tests/test_code_review.py | 13 ++++++-- 12 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 scripts/entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 6de18da..c4353b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +ARG TEA_VERSION=0.14.2 + FROM node:24-bookworm-slim AS codex ARG CODEX_VERSION=0.144.6 @@ -8,6 +10,8 @@ RUN npm install --global \ FROM ghcr.io/astral-sh/uv:0.8.14 AS uv +FROM gitea/tea:${TEA_VERSION} AS tea + FROM python:3.13-slim-bookworm ENV PYTHONDONTWRITEBYTECODE=1 \ @@ -16,15 +20,18 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ UV_NO_DEV=1 \ CODEGRAPH_TELEMETRY=0 \ CODEX_HOME=/var/lib/codex \ + XDG_CONFIG_HOME=/run/agentci \ PATH=/opt/agentci/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin RUN apt-get update \ && apt-get install --yes --no-install-recommends adduser ca-certificates git libstdc++6 \ && rm -rf /var/lib/apt/lists/* \ && /usr/sbin/adduser --disabled-password --gecos "" --uid 10001 agentci \ - && mkdir -p /opt/agentci /var/lib/agentci /var/lib/codex /etc/codex + && mkdir -p /opt/agentci /var/lib/agentci /var/lib/codex /etc/codex /run/agentci \ + && chown agentci:agentci /run/agentci COPY --from=uv /uv /uvx /usr/local/bin/ +COPY --from=tea /bin/tea /usr/local/bin/tea COPY --from=codex /usr/local/bin/node /usr/local/bin/node COPY --from=codex /usr/local/lib/node_modules/@openai/codex /usr/local/lib/node_modules/@openai/codex COPY --from=codex /usr/local/lib/node_modules/@colbymchenry /usr/local/lib/node_modules/@colbymchenry @@ -39,7 +46,9 @@ COPY scripts ./scripts COPY codex/config.toml /etc/codex/config.toml RUN chmod 0755 \ + /opt/agentci/scripts/entrypoint.sh \ /opt/agentci/scripts/gitea-askpass.sh \ + /usr/local/bin/tea \ /usr/local/bin/codex \ /usr/local/bin/codegraph \ && uv sync --frozen --no-dev \ @@ -51,4 +60,5 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/live')"] +ENTRYPOINT ["/opt/agentci/scripts/entrypoint.sh"] CMD ["agentci"] diff --git a/codex/config.toml b/codex/config.toml index 3e06bf3..3daecf4 100644 --- a/codex/config.toml +++ b/codex/config.toml @@ -40,6 +40,25 @@ glob_scan_max_depth = 5 ".git" = "read" "**/*.env" = "deny" +[permissions.agentci-review] +description = "Review scoped workflow material and query Gitea with tea without editing files." + +[permissions.agentci-review.filesystem] +":minimal" = "read" +glob_scan_max_depth = 5 + +[permissions.agentci-review.filesystem.":workspace_roots"] +"." = "read" +".git" = "read" +"**/*.env" = "deny" + +[permissions.agentci-review.network] +enabled = true +allow_local_binding = false + +[permissions.agentci-review.network.domains] +"*" = "allow" + [permissions.agentci-write] description = "Edit a workflow repository without changing Git metadata." diff --git a/compose.yaml b/compose.yaml index 862908d..c03eef8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,6 +5,7 @@ services: args: CODEX_VERSION: ${CODEX_VERSION:-0.144.6} CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1} + TEA_VERSION: ${TEA_VERSION:-0.14.2} restart: unless-stopped environment: AGENTCI_GITEA_URL: ${AGENTCI_GITEA_URL:-http://gitea:3000} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..550333e --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -eu + +python -c ' +import json +import os +from pathlib import Path + +config_dir = Path(os.environ["XDG_CONFIG_HOME"]) / "tea" +config_dir.mkdir(parents=True, exist_ok=True) +config_path = config_dir / "config.yml" +token_path = Path( + os.environ.get("AGENTCI_GITEA_TOKEN_FILE", "/run/secrets/gitea_token") +) +login = { + "name": "agentci", + "url": os.environ.get("AGENTCI_GITEA_URL", "http://gitea:3000"), + "token": token_path.read_text().strip(), + "default": True, + "version_check": False, +} +config_path.write_text(json.dumps({"logins": [login], "preferences": {}})) +config_path.chmod(0o600) +' + +exec "$@" diff --git a/src/agentci/adapters/codex.py b/src/agentci/adapters/codex.py index 52578a5..3037c12 100644 --- a/src/agentci/adapters/codex.py +++ b/src/agentci/adapters/codex.py @@ -183,7 +183,14 @@ class CodexClient: output_path.unlink(missing_ok=True) # noqa: ASYNC240 def _environment(self) -> dict[str, str]: - allowed = {"PATH", "LANG", "LC_ALL", "SSL_CERT_FILE", "CODEX_CA_CERTIFICATE"} + allowed = { + "PATH", + "LANG", + "LC_ALL", + "SSL_CERT_FILE", + "CODEX_CA_CERTIFICATE", + "XDG_CONFIG_HOME", + } environment = {key: value for key, value in os.environ.items() if key in allowed} environment["CODEX_HOME"] = str(self.codex_home) if self.context7_api_key: diff --git a/src/agentci/prompts/implementation_review.md b/src/agentci/prompts/implementation_review.md index d9fa83c..0b79d0d 100644 --- a/src/agentci/prompts/implementation_review.md +++ b/src/agentci/prompts/implementation_review.md @@ -1,11 +1,26 @@ -Independently review the current uncommitted implementation against the issue context. Inspect the -working tree and diff yourself. Do not edit files. Focus on correctness, regressions, security, -missing tests, and whether the requested behavior is actually complete. +Independently review only the proposed pull request, the issue it fixes, and the canonical plan +from that issue. Inspect the pull request diff yourself, but do not inspect or review unrelated +repository files, history, branches, issues, or other repository-wide concerns. You may read a +changed file only as needed to understand a changed diff hunk. Do not edit files. Focus on concrete +correctness, regressions, security, missing tests, and whether the requested behavior is complete. -$context +$issue_context + +$artifact + + + +$pull_context + + +Before reporting any finding, run +`tea issues list --repo OWNER/REPO --state all --keyword "FOCUSED KEYWORDS"` for the repository +named by the issue context. This duplicate search is the only permitted access to other issues. If +a matching issue already tracks the finding, cite its number in the finding. Do not use `tea` to +create, edit, close, reopen, comment on, or otherwise mutate issues or pull requests. + Return a structured review. Use `blocking` only when the result cannot safely be proposed, `major` for a material defect, and `minor` for a non-blocking improvement. - diff --git a/src/agentci/prompts/plan_review.md b/src/agentci/prompts/plan_review.md index 14e7004..3ca73e3 100644 --- a/src/agentci/prompts/plan_review.md +++ b/src/agentci/prompts/plan_review.md @@ -1,6 +1,14 @@ -You are the independent reviewer for a proposed implementation plan. Inspect the repository -yourself and compare the plan to the issue. Do not edit files. Identify concrete correctness, -security, compatibility, missing-decision, and testing problems. Do not invent speculative work. +You are the independent reviewer for a proposed implementation plan. Review only the proposed +plan against the issue context below. Do not inspect the repository, working tree, Git history, +or any other repository material, and do not edit files. Identify concrete correctness, security, +compatibility, missing-decision, and testing problems. Do not invent speculative work. + +Before reporting any finding, run +`tea issues list --repo OWNER/REPO --state all --keyword "FOCUSED KEYWORDS"` for the repository +named by the issue context. This duplicate search is the only permitted access to repository +material outside the issue context and plan. If a matching issue already tracks the finding, cite +its number in the finding. Do not use `tea` to create, edit, close, reopen, comment on, or otherwise +mutate issues or pull requests. $context @@ -12,4 +20,3 @@ $artifact Return a structured review. Use `blocking` only when work cannot safely proceed, `major` for a material defect or unresolved implementation decision, and `minor` for a non-blocking improvement. - diff --git a/src/agentci/workflows/code_review.py b/src/agentci/workflows/code_review.py index d286b05..0175f3e 100644 --- a/src/agentci/workflows/code_review.py +++ b/src/agentci/workflows/code_review.py @@ -12,7 +12,8 @@ class CodeReviewLoop: self, job: Job, workflow: Workflow, - context: str, + issue_context: str, + plan: str, result: AgentResult, ) -> tuple[AgentResult, ReviewReport]: report = ReviewReport(summary="", findings=[]) @@ -22,7 +23,15 @@ class CodeReviewLoop: stage=f"reviewing implementation {round_index + 1}/" f"{self.deps.settings.implement_review_rounds}", ) - report = await self.once(workflow, context) + report = await self.once( + workflow, + issue_context=issue_context, + plan=plan, + pull_context=( + "The proposed pull request is the current uncommitted working-tree diff. " + "Review only that diff." + ), + ) workflow.artifact = result.model_dump_json() workflow.review_json = report_json(report) await self.deps.storage.update_workflow(workflow) @@ -46,15 +55,27 @@ class CodeReviewLoop: ) return result, report - async def once(self, workflow: Workflow, context: str) -> ReviewReport: - prompt = self.deps.prompts.render("implementation_review", context=context) + async def once( + self, + workflow: Workflow, + *, + issue_context: str, + plan: str, + pull_context: str, + ) -> ReviewReport: + prompt = self.deps.prompts.render( + "implementation_review", + issue_context=issue_context, + artifact=plan, + pull_context=pull_context, + ) if workflow.reviewer_session_id: return await self.deps.codex.resume( session_id=workflow.reviewer_session_id, prompt=prompt, model=self.deps.settings.implement_model, reasoning=self.deps.settings.implement_reasoning, - permission="agentci-read", + permission="agentci-review", workspace=workflow.workspace_path, schema_name="review.json", result_type=ReviewReport, @@ -64,7 +85,7 @@ class CodeReviewLoop: prompt=prompt, model=self.deps.settings.implement_model, reasoning=self.deps.settings.implement_reasoning, - permission="agentci-read", + permission="agentci-review", schema_name="review.json", result_type=ReviewReport, ) diff --git a/src/agentci/workflows/implement.py b/src/agentci/workflows/implement.py index eae919e..21cb25d 100644 --- a/src/agentci/workflows/implement.py +++ b/src/agentci/workflows/implement.py @@ -83,7 +83,13 @@ class ImplementWorkflow: workflow.primary_session_id = session_id workflow.artifact = result.model_dump_json() await self.deps.storage.update_workflow(workflow) - result, report = await self.review.run(job, workflow, context, result) + result, report = await self.review.run( + job, + workflow, + context, + plan.artifact if plan and plan.artifact else "(no canonical plan)", + result, + ) sha = await self.changes.commit_and_push( job, workspace, diff --git a/src/agentci/workflows/plan.py b/src/agentci/workflows/plan.py index 3a0ec44..7af9c8b 100644 --- a/src/agentci/workflows/plan.py +++ b/src/agentci/workflows/plan.py @@ -176,7 +176,7 @@ class PlanWorkflow: prompt=prompt, model=self.deps.settings.plan_model, reasoning=self.deps.settings.plan_reasoning, - permission="agentci-read", + permission="agentci-review", workspace=workflow.workspace_path, schema_name="review.json", result_type=ReviewReport, @@ -186,7 +186,7 @@ class PlanWorkflow: prompt=prompt, model=self.deps.settings.plan_model, reasoning=self.deps.settings.plan_reasoning, - permission="agentci-read", + permission="agentci-review", schema_name="review.json", result_type=ReviewReport, ) diff --git a/src/agentci/workflows/pull_request.py b/src/agentci/workflows/pull_request.py index 7df1596..656964d 100644 --- a/src/agentci/workflows/pull_request.py +++ b/src/agentci/workflows/pull_request.py @@ -1,6 +1,6 @@ from __future__ import annotations -from agentci.domain.models import AgentResult, Job, WorkflowStatus +from agentci.domain.models import AgentResult, Job, WorkflowKind, WorkflowStatus from agentci.workflows.change_set import ChangeSet, result_comment from agentci.workflows.code_review import CodeReviewLoop from agentci.workflows.common import ( @@ -58,7 +58,18 @@ class PullRequestWorkflow: schema_name="agent_result.json", result_type=AgentResult, ) - report = await self.review.once(workflow, context) + issue_context = await self.deps.context.issue_context( + job.repo_owner, job.repo_name, workflow.issue_number + ) + plan = await self.deps.storage.latest_workflow( + job.repo_owner, job.repo_name, workflow.issue_number, WorkflowKind.PLAN + ) + report = await self.review.once( + workflow, + issue_context=issue_context, + plan=plan.artifact if plan and plan.artifact else "(no canonical plan)", + pull_context=context, + ) sha = await self.changes.commit_and_push( job, workflow.workspace_path, diff --git a/tests/test_code_review.py b/tests/test_code_review.py index 6cc10b2..07a774a 100644 --- a/tests/test_code_review.py +++ b/tests/test_code_review.py @@ -101,7 +101,11 @@ async def test_stops_after_clean_second_review() -> None: clean = ReviewReport(summary="Ready", findings=[]) loop, codex, workflow, job = objects(4, [serious_report(), clean]) _, report = await loop.run( - job, workflow, "context", AgentResult(summary_markdown="initial", tests=[]) + job, + workflow, + "issue context", + "canonical plan", + AgentResult(summary_markdown="initial", tests=[]), ) assert not report.has_serious_findings assert codex.reviews == 2 @@ -113,9 +117,12 @@ async def test_does_not_make_unreviewed_final_revision() -> None: 3, [serious_report(), serious_report(), serious_report()] ) _, report = await loop.run( - job, workflow, "context", AgentResult(summary_markdown="initial", tests=[]) + job, + workflow, + "issue context", + "canonical plan", + AgentResult(summary_markdown="initial", tests=[]), ) assert report.has_serious_findings assert codex.reviews == 3 assert codex.revisions == 2 -