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
-