guard reviews against issue rediscovery
This commit is contained in:
+11
-1
@@ -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"]
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user