guard reviews against issue rediscovery

This commit is contained in:
2026-07-19 21:50:13 +02:00
parent 1a7d688546
commit 5b2b09b000
12 changed files with 155 additions and 25 deletions
+11 -1
View File
@@ -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"]
+19
View File
@@ -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."
+1
View File
@@ -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}
+26
View File
@@ -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 "$@"
+8 -1
View File
@@ -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:
+20 -5
View File
@@ -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.
<issue_context>
$context
$issue_context
</issue_context>
<canonical_plan>
$artifact
</canonical_plan>
<pull_request_context>
$pull_context
</pull_request_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.
+11 -4
View File
@@ -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.
<issue_context>
$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.
+27 -6
View File
@@ -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,
)
+7 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
)
+13 -2
View File
@@ -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,
+10 -3
View File
@@ -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