--- runner/core.before +++ runner/core.after @@ -669,7 +669,7 @@ "-c","approval_policy=\"never\"","-c",f"sqlite_home={json.dumps(str(runtime/'sqlite'))}", "-c",f"log_dir={json.dumps(str(runtime/'logs'))}","--output-schema",str(schema_file),"--output-last-message",str(runtime/"answer.json"),"-"] elif service == "claude": - # Existing read-only worktree/runtime access: inspect source as needed. + # Read-only tools inspect this checkout; evidence is mirrored here before review. args=[cli,"-p","--safe-mode","--restricted","--output-format","json","--tools","Read,Grep,Glob","--strict-mcp-config","--mcp-config",'{"mcpServers":{}}', "--setting-sources","","--no-session-persistence","--permission-mode","plan","--json-schema",json.dumps(schema)] budget=self.config["policy"].get("claude_max_budget_usd") @@ -932,6 +932,48 @@ except (GateError,OSError) as exc: self.state(task_id,"repair_pending",str(exc)); raise + def review_materials(self, task_id, worktree, revision): + """Put complete registered proof within restricted read-only review cwd.""" + worktree = Path(worktree) + destination = worktree / ".review-material" + require(not destination.exists(), "fresh review material directory required") + destination.mkdir() + rows = [] + def copy_file(source, target): + require(source.is_file() and not source.is_symlink(), "ordinary review proof") + target.parent.mkdir(parents=True, exist_ok=True) + before = hashlib.sha256(source.read_bytes()).hexdigest() + if sys.platform == "darwin": + subprocess.run(["/bin/cp", "-pc", str(source), str(target)], check=True) + else: + shutil.copy2(source, target) + require(hashlib.sha256(target.read_bytes()).hexdigest() == before, "whole review proof mismatch") + rows.append({"path": str(target.relative_to(worktree)), "original_path": str(source), + "bytes": target.stat().st_size, "sha256": before}) + evidence = self.db.execute("SELECT kind,artifact FROM evidence WHERE task_id=? AND commit_sha=? AND passed=1", + (task_id, revision)).fetchall() + for number, row in enumerate(evidence): + artifact = Path(row["artifact"]) + copy_file(artifact, destination / "receipts" / (str(number)+"-"+artifact.name)) + if artifact.suffix == ".json": + value = read_json(artifact) + for check in value.get("checks", []): + folder = Path(check["artifact"]) + for name in ("stdout.log", "stderr.log", "command.json"): + path = folder / name + if path.is_file(): + copy_file(path, destination / "validation-runs" / folder.name / name) + build = Path(self.allocation(task_id)["build_dir"]) + require(build.resolve().is_relative_to(self.runtime(task_id).resolve()), "registered review build scope") + if build.exists(): + for source in sorted(build.rglob("*")): + if source.is_file() and not secret_path(str(source.relative_to(build))): + copy_file(source, destination / "build" / source.relative_to(build)) + write_json(destination / "manifest.json", + {"candidate": revision, "task_id": task_id, "complete_registered_build_and_receipts": True, + "file_count_limit": None, "byte_limit": None, "files": rows}) + return {"path": ".review-material/manifest.json", "file_count": len(rows), "byte_limit": None} + def review(self, task_id): task=self.task(task_id); require(task["state"] in ("review_pending","blocked","reviewed","approval_pending"), "機械検証合格後、オーナー承認前にレビューできます") require(task["submitted"], "提出コミットがありません") @@ -944,7 +986,10 @@ schema["properties"]["task_id"]["enum"]=[task_id] schema["properties"]["commit"]["enum"]=[task["submitted"]] try: - value,artifact=self.llm(task_id,"reviewer","claude",self.packet(task_id,"reviewer",task["submitted"]),wt,schema) + material = self.review_materials(task_id, wt, task["submitted"]) + prompt = self.packet(task_id, "reviewer", task["submitted"]) + prompt += "\\nRead complete registered validation receipts and build proof via " + json.dumps(material) + ". All files are available within this frozen checkout; inspect related sources and proof as needed." + value,artifact=self.llm(task_id,"reviewer","claude",prompt,wt,schema) c=json.loads(task["contract"]) passed=value["status"]=="pass" and set(value["acceptance_ids"])==set(c["acceptance_ids"]) and not any(f["severity"] in ("critical","high","medium") for f in value["findings"]) self.evidence(task_id,"review",task["submitted"],artifact/"result.json",passed)