feat: appove and go

This commit is contained in:
thanhnv
2026-07-19 09:37:16 +07:00
parent 13fae3e6c3
commit 709b6cccd6
24 changed files with 1245 additions and 70 deletions
@@ -8,10 +8,12 @@ audit, or metric records.
import argparse
import fcntl
import hashlib
import importlib.util
import json
import os
import re
import shlex
import shutil
import subprocess
import tempfile
import time
@@ -36,6 +38,11 @@ SECURITY = os.path.join(BIN, "security-check.sh")
STATE_ROOT = os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
PROJECT_REGISTRY = os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json")
APPROVAL_INBOX = os.path.join(BIN, "approval-inbox.py")
_MANIFEST_SPEC = importlib.util.spec_from_file_location(
"casan_goal_project_manifest", os.path.join(BIN, "project_manifest.py")
)
PROJECT_MANIFEST = importlib.util.module_from_spec(_MANIFEST_SPEC)
_MANIFEST_SPEC.loader.exec_module(PROJECT_MANIFEST)
CONTEXT_EXTENSIONS = {".md", ".txt", ".json", ".yaml", ".yml", ".ts", ".tsx", ".js", ".mjs", ".py", ".sh", ".prisma", ".css", ".html"}
CONTEXT_IGNORED = {"node_modules", ".git", "dist", "build", "coverage", ".vite", "tmp", "logs", "__pycache__"}
SENSITIVE_NAMES = {".env", ".env.local", "credentials", "credentials.json", "secrets.json", "id_rsa", "id_ed25519"}
@@ -173,8 +180,35 @@ def context_excerpt_is_sensitive(text: str) -> bool:
))
def restricted_context_paths(goal: str):
"""Return an explicit user-declared file/directory boundary, if present."""
section = re.search(
r"(?:chỉ được đọc và thay đổi|chỉ làm việc trong|only (?:read and )?(?:modify|change)|only work (?:in|within))\s*:\s*"
r"(.*?)(?=\n\s*(?:không sửa bất kỳ file nào khác|do not (?:modify|change) any other file|không thay đổi|không thêm|vấn đề|yêu cầu|verification|$))",
goal,
re.IGNORECASE | re.DOTALL,
)
if not section:
return []
paths = []
for match in re.findall(r"(?:apps|packages|docs)/[A-Za-z0-9_./*-]+", section.group(1), re.IGNORECASE):
cleaned = match.rstrip(".,:;)")
scoped_directory = cleaned.endswith(("/*", "/**"))
normalized = cleaned.rstrip("/*")
if normalized and all(path != normalized for path, _ in paths):
paths.append((normalized, scoped_directory))
return paths
def context_candidates(project: dict, goal: str):
terms = {term.lower() for term in re.findall(r"[A-Za-z0-9_-]{3,}", goal)}
explicit_paths = []
for match in re.findall(r"(?:apps|packages|docs)/[A-Za-z0-9_./*-]+", goal, re.IGNORECASE):
cleaned = match.rstrip(".,:;)")
scoped_directory = cleaned.endswith(("/*", "/**"))
normalized_path = cleaned.rstrip("/*").lower()
if normalized_path and all(path != normalized_path for path, _ in explicit_paths):
explicit_paths.append((normalized_path, scoped_directory))
candidates = []
seen = set()
for relative_root, absolute_root in project["roots"]:
@@ -200,6 +234,22 @@ def context_candidates(project: dict, goal: str):
continue
haystack = (relative + "\n" + raw[:4000]).lower()
score = sum(4 if term in relative.lower() else 1 for term in terms if term in haystack)
relative_lower = relative.lower()
matching_paths = [
(path, scoped_directory)
for path, scoped_directory in explicit_paths
if relative_lower == path or relative_lower.startswith(path + "/")
]
if matching_paths:
# Directory globs under "only work in" sections describe the
# actual patch surface. Exact source-file mentions come next;
# architecture/requirement references remain supporting context.
if any(scoped_directory for _, scoped_directory in matching_paths):
score += 3_000
elif os.path.splitext(relative_lower)[1] in {".ts", ".tsx", ".js", ".jsx", ".py", ".prisma", ".sql", ".sh"}:
score += 2_000
else:
score += 1_000
if relative.endswith(("README.md", "architecture.md", "technical_architecture.md", "package.json")):
score += 3
candidates.append((score, relative, raw))
@@ -209,6 +259,17 @@ def context_candidates(project: dict, goal: str):
def build_context(job_path: str, project_id: str, goal: str, write_intent=False):
project = registered_project(project_id)
candidates = context_candidates(project, goal)
restricted_paths = restricted_context_paths(goal)
if restricted_paths:
candidates = [
candidate for candidate in candidates
if any(
candidate[1] == path or (scoped_directory and candidate[1].startswith(path.rstrip("/") + "/"))
for path, scoped_directory in restricted_paths
)
]
if not candidates:
raise ValueError("goal_context_explicit_scope_empty")
excerpts, manifest_files, characters = [], [], 0
# A diff can only apply when the model sees the exact target-file content.
# Read-only analysis stays compact; write-intent gives the three most
@@ -261,12 +322,39 @@ def build_context(job_path: str, project_id: str, goal: str, write_intent=False)
def requests_side_effect(goal: str) -> bool:
normalized = " ".join(goal.lower().split())
patterns = [
r"^(hãy\s+)?(làm luôn|sửa|thay đổi|triển khai|thực hiện|hoàn thành|chạy|tạo|xóa|cài đặt|commit|push)\b",
# The objective's leading command is authoritative. A scoped constraint
# such as "Không thay đổi API contract" must not turn "Hoàn thiện ..."
# into a read-only request. Conversely, a genuinely read-only audit does
# not begin with one of these implementation commands.
leading_write_patterns = [
r"^(hãy\s+)?(làm luôn|sửa|thay đổi|triển khai|thực hiện|hoàn thiện|hoàn thành|chạy|tạo|xóa|cài đặt|commit|push)\b",
r"^(please\s+)?(implement|fix|change|deploy|run|create|delete|install|commit|push)\b",
r"\b(thực hiện thao tác|sửa code|ghi file|mở pull request|create a pull request)\b",
]
return any(re.search(pattern, normalized) for pattern in patterns)
if any(re.search(pattern, normalized) for pattern in leading_write_patterns):
return True
patch_requests = re.finditer(r"\b(tạo|generate|xuất|produce)\b.{0,40}\b(patch|unified git diff)\b", normalized)
explicit_patch_request = any(
not normalized[max(0, match.start() - 8):match.start()].endswith(("không ", "do not "))
for match in patch_requests
)
explicit_read_only = bool(re.search(r"\b(không|do not)\s+(sửa|thay đổi|triển khai|implement|fix|tạo|apply|áp dụng)\b", normalized))
if explicit_read_only and not explicit_patch_request:
return False
imperative_patterns = [r"\b(thực hiện thao tác|sửa code|ghi file|mở pull request|create a pull request)\b"]
if any(re.search(pattern, normalized) for pattern in imperative_patterns):
return True
# Structured objectives commonly start with a phase label rather than the
# imperative itself (for example "PHASE 1 — Hoàn thiện backend..."). Keep
# those write requests on the patch + approval path without classifying a
# read-only audit that merely mentions source files as a side effect.
structured_write_patterns = [
r"\b(hoàn thiện|triển khai|implement|fix|sửa|thay đổi)\b.{0,120}\b(ứng dụng|backend|frontend|module|component|api|source|code|file)\b",
r"\b(tạo|generate|xuất|produce)\b.{0,40}\b(patch|unified git diff)\b",
r"\b(apply|áp dụng)\b.{0,40}\b(patch|thay đổi|change)\b",
]
return any(re.search(pattern, normalized) for pattern in structured_write_patterns)
def extract_patch(text: str) -> str:
@@ -344,6 +432,17 @@ def normalize_unified_diff(patch: str) -> str:
return "\n".join(normalized) + "\n" if changed else patch
def mechanical_patch_error(error: object) -> bool:
"""Identify git parser errors that hunk recounting can safely repair."""
normalized = str(error).lower()
return any(marker in normalized for marker in (
"corrupt patch",
"patch fragment without header",
"malformed patch",
"unexpected end of file in patch",
))
def validate_write_output(text: str) -> str:
"""Fail at the producing harness when a write-intent reply is not a diff.
@@ -356,7 +455,7 @@ def validate_write_output(text: str) -> str:
validate_patch_check(patch)
return patch
except ValueError as original_error:
if "corrupt patch" not in str(original_error):
if not mechanical_patch_error(original_error):
raise
normalized = normalize_unified_diff(patch)
if normalized == patch:
@@ -387,14 +486,106 @@ def patch_repair_models(primary_model: str):
"""Use direct cloud -> gateway -> local order for one bounded H2 recovery."""
configured = [item.strip() for item in os.environ.get("CASAN_GOAL_PATCH_REPAIR_MODELS", "").split(",") if item.strip()]
unique, seen = [], set()
for candidate in configured + [primary_model]:
ordered = [primary_model, *configured] if primary_model.startswith("account:") else [*configured, primary_model]
for candidate in ordered:
if candidate and candidate not in seen:
seen.add(candidate)
unique.append(candidate)
return unique
def model_preflight_enabled() -> bool:
configured = os.environ.get("CASAN_GOAL_MODEL_PREFLIGHT")
return configured == "1" if configured is not None else os.environ.get("CASAN_PROFILE") == "prod"
def model_preflight(model: str):
"""Probe real patch generation before assigning a model to H2/H3.
Model discovery only proves that an ID is advertised. This bounded probe
verifies the generation route and unified-diff capability, and caches the
verdict so a broken gateway is not retried for every goal.
"""
if not model_preflight_enabled():
return True, "preflight_disabled"
if model.startswith("account:"):
endpoint = os.environ.get("CASAN_AUTH_BRIDGE_URL", "")
elif model.startswith("openai-compatible:"):
endpoint = os.environ.get("CASAN_OPENAI_COMPATIBLE_BASE_URL", "")
else:
endpoint = os.environ.get("OLLAMA_HOST", "")
cache_key = sha(f"{model}|{endpoint}")
cache_directory = os.path.join(STATE_ROOT, "cache")
cache_path = os.path.join(cache_directory, "goal-model-preflight.json")
lock_path = os.path.join(cache_directory, "goal-model-preflight.lock")
os.makedirs(cache_directory, mode=0o700, exist_ok=True)
ttl = int(os.environ.get("CASAN_GOAL_MODEL_PREFLIGHT_TTL_SEC", "600"))
with open(lock_path, "a", encoding="utf-8") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
try:
cache = load_json(cache_path) if os.path.isfile(cache_path) else {}
except (OSError, ValueError, json.JSONDecodeError):
cache = {}
cached = cache.get(cache_key, {}) if isinstance(cache, dict) else {}
age = time.time() - float(cached.get("checked_epoch", 0))
if age <= max(30, ttl):
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
return bool(cached.get("healthy")), f"cached:{cached.get('reason', 'unknown')}"
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
prompt = (
"Patch capability probe. Return ONLY a unified git diff inside a ```diff fence that changes "
"the only line in probe.txt from old to new. No prose. The diff must begin with "
"`diff --git a/probe.txt b/probe.txt`."
)
timeout_setting = (
os.environ.get("CASAN_GOAL_LOCAL_MODEL_PREFLIGHT_TIMEOUT_SEC", "90")
if model.startswith("ollama:")
else os.environ.get("CASAN_GOAL_MODEL_PREFLIGHT_TIMEOUT_SEC", "30")
)
timeout = max(5, min(int(timeout_setting), 120))
if model.startswith("account:"):
ok, output, _, reason = call_account_model(model.split(":", 1)[1], prompt, timeout)
else:
ok, output, _, reason = call_model(
model, prompt, model.startswith(("openai:", "anthropic:", "openai-compatible:")),
timeout_seconds=timeout, max_output_tokens=512,
)
healthy = False
if ok:
try:
patch = extract_patch(output)
healthy = (
"diff --git a/probe.txt b/probe.txt" in patch
and "\n-old\n" in patch
and "\n+new\n" in patch
)
reason = "ok" if healthy else "patch_probe_contract_invalid"
except ValueError as error:
reason = str(error)
record = {
"model": model,
"provider": provider_for_model(model),
"healthy": healthy,
"reason": reason[:180],
"checked_at": now(),
"checked_epoch": time.time(),
}
with open(lock_path, "a", encoding="utf-8") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
try:
cache = load_json(cache_path) if os.path.isfile(cache_path) else {}
except (OSError, ValueError, json.JSONDecodeError):
cache = {}
cache[cache_key] = record
atomic_json(cache_path, cache)
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
return healthy, reason
def provider_for_model(model: str) -> str:
if model.startswith("account:"):
return f"{model.split(':', 1)[1]}-account"
if model.startswith("openai:"):
return "openai"
if model.startswith("anthropic:"):
@@ -420,7 +611,7 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
def repair_prompt_for(previous_output: str, error: str) -> str:
hunk_instruction = (
"The previous diff is syntactically corrupt. Recompute every `@@ -old,count +new,count @@` header from the exact added/removed/context lines that follow it; do not omit or invent any hunk line. "
if "corrupt patch" in error.lower() else ""
if mechanical_patch_error(error) else ""
)
return (
f"Your previous response violated the required write-output contract: {error}. " +
@@ -434,7 +625,21 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
last_metadata, last_reason = {}, "goal_patch_missing"
attempts = []
for attempt_number, candidate in enumerate(candidates, start=1):
ok, output, metadata, reason = call_model(candidate, repair_prompt, candidate.startswith(("openai:", "anthropic:", "openai-compatible:")), max_output_tokens=patch_output_tokens())
healthy, preflight_reason = model_preflight(candidate)
if not healthy:
last_reason = f"goal_patch_model_preflight_failed:{preflight_reason}"
attempts.append({
"attempt": attempt_number,
"provider": provider_for_model(candidate),
"model": candidate,
"status": "failed",
"reason": last_reason,
})
continue
if candidate.startswith("account:"):
ok, output, metadata, reason = call_account_model(candidate.split(":", 1)[1], repair_prompt, 300)
else:
ok, output, metadata, reason = call_model(candidate, repair_prompt, candidate.startswith(("openai:", "anthropic:", "openai-compatible:")), max_output_tokens=patch_output_tokens())
metadata = dict(metadata)
metadata["repair_model"] = candidate
metadata["repair_attempt"] = attempt_number
@@ -464,7 +669,7 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
# incomplete first diff. Give the same stronger direct model one
# corrective pass containing its own failed patch and git error
# before sending source context to a weaker gateway.
if candidate.endswith("-codex") and attempt_number < patch_repair_attempts():
if (candidate.endswith("-codex") or candidate == "account:codex") and attempt_number < patch_repair_attempts():
candidates.insert(attempt_number, candidate)
del candidates[patch_repair_attempts():]
repair_prompt = repair_prompt_for(output, last_reason)
@@ -493,8 +698,52 @@ def merge_usage(primary: dict, additional: dict) -> dict:
return merged
def validate_patch_semantics(job: dict, artifact: str, changed_files: list[str]) -> list[dict]:
"""Build and test an applied patch in an isolated workspace before approval."""
artifact = os.path.realpath(artifact)
if os.path.commonpath([ROOT, artifact]) != ROOT or not os.path.isfile(artifact):
raise ValueError("goal_patch_artifact_path_denied")
manifest = PROJECT_MANIFEST.load(ROOT, project_id=str(job.get("project") or ""))
commands = PROJECT_MANIFEST.verification_commands(manifest, changed_files)
if not commands:
raise ValueError("goal_patch_verification_unmapped")
results = []
with tempfile.TemporaryDirectory(prefix="casan-goal-verify-") as sandbox:
paths = ["package.json", "package-lock.json", *manifest["source_roots"]]
paths.extend(str(value) for value in job.get("workspace", {}).get("context_roots", []))
for relative in dict.fromkeys(paths):
source = os.path.realpath(os.path.join(ROOT, relative))
if os.path.commonpath([ROOT, source]) != ROOT or not os.path.exists(source):
continue
destination = os.path.join(sandbox, relative)
os.makedirs(os.path.dirname(destination), exist_ok=True)
if os.path.isdir(source):
shutil.copytree(source, destination, dirs_exist_ok=True, symlinks=True, ignore=shutil.ignore_patterns("node_modules", "dist", "coverage"))
else:
shutil.copy2(source, destination)
dependencies = os.path.join(ROOT, "node_modules")
if os.path.isdir(dependencies):
os.symlink(dependencies, os.path.join(sandbox, "node_modules"), target_is_directory=True)
applied = subprocess.run(
["git", "apply", "--whitespace=error", artifact], cwd=sandbox,
capture_output=True, text=True, timeout=30,
)
if applied.returncode != 0:
raise ValueError("goal_patch_sandbox_apply_failed:" + (applied.stderr or applied.stdout).strip()[:300])
timeout = max(30, int(os.environ.get("CASAN_GOAL_PATCH_VERIFY_TIMEOUT_SEC", "300")))
for command in commands:
result = subprocess.run(command, cwd=sandbox, capture_output=True, text=True, timeout=timeout)
output = (result.stdout + result.stderr)[-4000:]
results.append({"command": " ".join(command), "exit_code": result.returncode, "output": output})
if result.returncode != 0:
raise ValueError("goal_patch_verification_failed:" + output[-1200:])
return results
def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict:
roots = [str(value).strip("/") for value in job.get("workspace", {}).get("context_roots", [])]
explicit_scope = restricted_context_paths(str(job.get("goal") or ""))
roots = [path.strip("/") for path, _ in explicit_scope] or [str(value).strip("/") for value in job.get("workspace", {}).get("context_roots", [])]
directory_roots = {path.strip("/") for path, scoped_directory in explicit_scope if scoped_directory}
changed = []
header_paths = []
for line in patch.splitlines():
@@ -514,7 +763,7 @@ def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict:
path = path[2:]
if path.startswith("/") or ".." in path.split("/"):
raise ValueError("goal_patch_path_denied")
if not any(path == root or path.startswith(root + "/") for root in roots):
if not any(path == root or (root in directory_roots and path.startswith(root + "/")) for root in roots):
raise ValueError(f"goal_patch_outside_workspace:{path}")
changed.append(path)
if not changed or len(set(changed)) > 20:
@@ -529,7 +778,12 @@ def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict:
if check.returncode != 0:
os.unlink(artifact)
raise ValueError("goal_patch_check_failed:" + (check.stderr or check.stdout).strip()[:160])
return {"path": os.path.relpath(artifact, ROOT), "sha256": sha(patch), "files": sorted(set(changed)), "bytes": len(patch.encode("utf-8")), "status": "awaiting_approval", "preview": patch[:50_000]}
try:
verification = validate_patch_semantics(job, artifact, sorted(set(changed)))
except Exception:
os.unlink(artifact)
raise
return {"path": os.path.relpath(artifact, ROOT), "sha256": sha(patch), "files": sorted(set(changed)), "bytes": len(patch.encode("utf-8")), "status": "awaiting_approval", "preview": patch[:50_000], "preapproval_verification": verification}
def submit_side_effect(job: dict, manifest: dict, patch_artifact: dict) -> dict:
@@ -702,8 +956,8 @@ def reviewer_candidates(account_provider: str, cloud_model: str, local_model: st
candidates.append({"kind": "model", "provider": "omniroute", "model": value, "value": value})
local_reviewer = os.environ.get("CASAN_GOAL_LOCAL_REVIEWER_MODEL", "").strip() or local_model
if local_reviewer:
candidates.append({"kind": "model", "provider": os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"), "model": local_reviewer, "value": local_reviewer})
if local_reviewer and os.environ.get("CASAN_GOAL_ENABLE_LOCAL_REVIEWER", "1") == "1":
candidates.append({"kind": "model", "provider": os.environ.get("CASAN_GOAL_LOCAL_REVIEWER_PROVIDER", os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy")), "model": local_reviewer, "value": local_reviewer})
unique, seen = [], set()
for candidate in candidates:
@@ -714,15 +968,17 @@ def reviewer_candidates(account_provider: str, cloud_model: str, local_model: st
return unique
def run_reviewer_chain(job_path: str, prompt: str, account_provider: str, cloud_model: str, local_model: str):
def run_reviewer_chain(job_path: str, prompt: str, account_provider: str, cloud_model: str, local_model: str, max_output_tokens=None, excluded_models=None):
max_attempts = max(1, min(int(os.environ.get("CASAN_GOAL_REVIEWER_MAX_ATTEMPTS", "5")), 10))
deadline_seconds = max(1, min(int(os.environ.get("CASAN_GOAL_REVIEWER_DEADLINE_SEC", "360")), 900))
deadline = time.monotonic() + deadline_seconds
ledger = []
update_job(job_path, reviewer_attempts=ledger)
last_reason = "reviewer_candidates_unavailable"
candidates = reviewer_candidates(account_provider, cloud_model, local_model)
if len(candidates) > max_attempts and candidates[-1].get("provider") == os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"):
excluded = set(excluded_models or [])
candidates = [candidate for candidate in reviewer_candidates(account_provider, cloud_model, local_model) if candidate.get("value") not in excluded]
local_reviewer_provider = os.environ.get("CASAN_GOAL_LOCAL_REVIEWER_PROVIDER", os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"))
if len(candidates) > max_attempts and candidates[-1].get("provider") == local_reviewer_provider:
candidates = candidates[:max_attempts - 1] + [candidates[-1]] if max_attempts > 1 else [candidates[-1]]
else:
candidates = candidates[:max_attempts]
@@ -734,10 +990,26 @@ def run_reviewer_chain(job_path: str, prompt: str, account_provider: str, cloud_
attempt_number = len(ledger) + 1
stage(job_path, "cloud-reviewer", "running", f"Reviewer attempt {attempt_number}/{max_attempts}", candidate["provider"], candidate["model"])
started_at, started_clock = now(), time.monotonic()
if candidate["kind"] == "model" and max_output_tokens is not None:
healthy, preflight_reason = model_preflight(candidate["value"])
if not healthy:
reason = f"model_preflight_failed:{preflight_reason}"
ledger.append({
"attempt": attempt_number, "provider": candidate["provider"], "model": candidate["model"],
"status": "failed", "reason": reason, "retryable": True,
"started_at": started_at, "finished_at": now(),
"latency_ms": int((time.monotonic() - started_clock) * 1000),
})
update_job(job_path, reviewer_attempts=ledger)
last_reason = reason
continue
if candidate["kind"] == "account":
ok, result, metadata, reason = call_account_model(candidate["value"], prompt, remaining)
else:
ok, result, metadata, reason = call_model(candidate["value"], prompt, candidate["provider"] != os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"), remaining)
if max_output_tokens is None:
ok, result, metadata, reason = call_model(candidate["value"], prompt, candidate["provider"] != local_reviewer_provider, remaining)
else:
ok, result, metadata, reason = call_model(candidate["value"], prompt, candidate["provider"] != local_reviewer_provider, remaining, max_output_tokens=max_output_tokens)
retryable = False if ok else reviewer_failure_retryable(reason)
ledger.append({
"attempt": attempt_number, "provider": candidate["provider"], "model": candidate["model"],
@@ -846,6 +1118,12 @@ def run(job_path: str) -> int:
})
emit(goal_id, "H4-security", "running", "Objective and workspace snapshot passed; model outputs pending")
local_healthy, local_preflight_reason = model_preflight(local_model)
if not local_healthy:
stage(job_path, "local-worker", "error", f"model_preflight_failed:{local_preflight_reason}", job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "error", "Local worker failed patch-generation preflight", {"provider": job.get("local_provider", ""), "model": local_model, "reason": local_preflight_reason})
raise RuntimeError(f"local_worker_preflight_failed:{local_preflight_reason}")
stage(job_path, "local-worker", "running", "Local model is developing the primary solution", job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "running", "Local worker is developing a solution", {"provider": job.get("local_provider", ""), "model": local_model})
output_contract = (
@@ -859,7 +1137,15 @@ def run(job_path: str) -> int:
"Do not claim to inspect any filesystem outside this snapshot and do not perform side effects.\n\n"
f"OBJECTIVE:\n{safe_goal}\n\nWORKSPACE SNAPSHOT ({project_id}):\n{context_bundle}"
)
ok, local_draft, local_meta, reason = call_model(local_model, local_prompt, False)
if local_model.startswith("account:"):
ok, local_draft, local_meta, reason = call_account_model(local_model.split(":", 1)[1], local_prompt, 300)
else:
ok, local_draft, local_meta, reason = call_model(
local_model,
local_prompt,
False,
max_output_tokens=patch_output_tokens() if write_intent else None,
)
if not ok:
stage(job_path, "local-worker", "error", reason, job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "error", "Local worker failed", {"reason": reason})
@@ -897,14 +1183,18 @@ def run(job_path: str) -> int:
stage(job_path, "local-worker", "error", reason, provider_for_model(actual_repair_model), actual_repair_model)
emit(goal_id, "H2-tool", "error", "Worker violated patch output contract after repair", {"reason": reason, "repair_provider": provider_for_model(actual_repair_model), "repair_model": actual_repair_model, "repair_attempts": repaired_meta.get("repair_attempts", [])})
raise ValueError(reason)
stage(job_path, "local-worker", "pass", "Primary solution prepared", job.get("local_provider", ""), local_model)
effective_local_model = str(local_meta.get("repair_model") or local_model)
effective_local_provider = provider_for_model(effective_local_model)
stage(job_path, "local-worker", "pass", "Primary solution prepared", effective_local_provider, effective_local_model)
update_job(
job_path,
local_draft=safe_local,
local_usage=local_meta,
patch_repair_attempts=local_meta.get("repair_attempts", []),
effective_local_provider=effective_local_provider,
effective_local_model=effective_local_model,
)
emit(goal_id, "H2-tool", "pass", "Local solution prepared", {"provider": job.get("local_provider", ""), "model": local_model, **local_meta})
emit(goal_id, "H2-tool", "pass", "Primary solution prepared", {"provider": effective_local_provider, "model": effective_local_model, **local_meta})
stage(job_path, "cloud-reviewer", "running", "Independent reviewer is challenging and improving the local solution", job.get("cloud_provider", ""), cloud_model)
emit(goal_id, "H3-eval", "running", "Cloud reviewer is evaluating the local solution", {"provider": job.get("cloud_provider", ""), "model": cloud_model})
@@ -923,7 +1213,13 @@ def run(job_path: str) -> int:
f"LOCAL WORKER PROPOSAL:\n{safe_local[:5000]}"
)
cloud_ok, cloud_result, cloud_meta, cloud_reason, reviewer = run_reviewer_chain(
job_path, review_prompt, account_provider, cloud_model, local_model
job_path,
review_prompt,
account_provider,
cloud_model,
local_model,
max_output_tokens=patch_output_tokens() if write_intent else None,
excluded_models={effective_local_model},
)
reviewer_provider = str(reviewer.get("provider") or job.get("cloud_provider", ""))
reviewer_model = str(reviewer.get("model") or cloud_model)
@@ -965,8 +1261,13 @@ def run(job_path: str) -> int:
patch_artifact["approval_id"] = proposal["id"]
final_status = "requires_approval"
metric_status = "degraded"
safe_result = "Implementation patch generated and independently reviewed. Approval is required before applying it to the workspace."
emit(goal_id, "H7-orchestration", "blocked", "Reviewed patch awaits approval", {"proposal_id": proposal["id"], "patch_sha256": patch_artifact["sha256"]})
if cloud_ok:
safe_result = "Implementation patch generated and independently reviewed. Approval is required before applying it to the workspace."
wait_detail = "Independently reviewed patch awaits approval"
else:
safe_result = "Implementation patch generated and validated. Automated H3 review was unavailable, so Independent Reviewer approval is required before applying it to the workspace."
wait_detail = "Validated patch awaits Independent Reviewer approval after H3 degradation"
emit(goal_id, "H7-orchestration", "blocked", wait_detail, {"proposal_id": proposal["id"], "patch_sha256": patch_artifact["sha256"], "cloud_incorporated": cloud_ok})
job = update_job(job_path, status=final_status, result=safe_result, patch_artifact=patch_artifact, approval=approval, cloud_usage=cloud_meta, finished_at=now())
else:
job = update_job(job_path, status=final_status, result=safe_result, cloud_usage=cloud_meta, finished_at=now())
@@ -97,14 +97,29 @@ def _manifest_for_files(files: list[str]) -> dict:
def verification_commands(files: list[str], manifest: dict | None = None) -> list[list[str]]:
project = manifest or _manifest_for_files(files)
return [["git", "diff", "--check", "--", *files], *PROJECT_MANIFEST.verification_commands(project, files)]
# `git apply --check --whitespace=error` already validates the exact patch
# before mutation. Runtime images intentionally do not need repository
# metadata, so post-apply verification is limited to manifest build/tests.
return PROJECT_MANIFEST.verification_commands(project, files)
def ready_for_apply(job: dict) -> bool:
if not job.get("patch_artifact"):
return False
if job.get("status") == "requires_approval":
return True
return (
job.get("status") == "failed"
and job.get("error") == "GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK"
)
def execute(job_path: str, actor: str) -> dict:
job = load(job_path)
if job.get("status") != "requires_approval" or not job.get("patch_artifact"):
if not ready_for_apply(job):
raise RuntimeError("GOAL_APPLY_JOB_NOT_READY")
proposal = verify_approval(job)
job.setdefault("approval", {})["status"] = "approved"
if proposal.get("approver") != actor:
raise PermissionError("GOAL_APPLY_APPROVER_IDENTITY_MISMATCH")
artifact = job["patch_artifact"]
@@ -135,7 +150,8 @@ def execute(job_path: str, actor: str) -> dict:
rollback = run(["git", "apply", "--reverse", patch_path], 30)
if rollback.returncode != 0:
raise RuntimeError("GOAL_APPLY_ROLLBACK_FAILED")
job.update(status="failed", error="GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK", verification=checks, finished_at=now(), updated_at=now())
artifact["status"] = "awaiting_apply_retry"
job.update(status="requires_approval", error="GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK", patch_artifact=artifact, verification=checks, finished_at=now(), updated_at=now())
save(job_path, job)
raise
@@ -25,6 +25,22 @@ TRACE_DIR="$LOG_DIR/trace"
AUDIT_DIR="$LOG_DIR/audit"
SECURITY_DIR="$CASAN_HARNESS_ROOT/security"
# Prefer the OS Python over framework/shim installations that may exist in a
# developer shell but cannot execute. The runtime image also exposes this path.
PYTHON_BIN="${CASAN_PYTHON_BIN:-}"
if [[ -z "$PYTHON_BIN" ]]; then
for candidate in /usr/bin/python3 python3 python; do
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" --version >/dev/null 2>&1; then
PYTHON_BIN="$candidate"
break
fi
done
fi
if [[ -z "$PYTHON_BIN" ]]; then
echo "SECURITY_RUNTIME_UNAVAILABLE: working Python 3 interpreter not found" >&2
exit 69
fi
# Shared log taxonomy (error<warn<info<debug<trace via CASAN_LOG_LEVEL). Used to
# make semantic skips loud (never silent) — stderr only, stdout contract intact.
# shellcheck source=casan-log.sh
@@ -59,7 +75,7 @@ new_trace_id() {
}
json_escape() {
python -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || sed 's/\\/\\\\/g; s/"/\\"/g'
"$PYTHON_BIN" -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || sed 's/\\/\\\\/g; s/"/\\"/g'
}
hash_text() {
@@ -85,7 +101,7 @@ load_yaml_values() {
local file="$1"
local key="$2"
[[ -f "$file" ]] || return 0
python - "$file" "$key" <<'PY'
"$PYTHON_BIN" - "$file" "$key" <<'PY'
import re
import sys
path, key = sys.argv[1], sys.argv[2]
@@ -98,6 +114,45 @@ with open(path, encoding="utf-8") as f:
PY
}
# Load only rule patterns whose declared action matches the requested action.
# The previous generic loader returned every `pattern:` in prompt-filter.yaml,
# which accidentally promoted `require_approval`, `alert`, and `log` rules to
# hard blocks. That made ordinary source code containing methods such as
# `delete()` fail the workspace-context scan as prompt injection.
load_yaml_rule_patterns() {
local file="$1"
local requested_action="$2"
[[ -f "$file" ]] || return 0
"$PYTHON_BIN" - "$file" "$requested_action" <<'PY'
import re
import sys
path, requested_action = sys.argv[1], sys.argv[2]
pattern = None
action = None
def flush():
if pattern is not None and action == requested_action:
print(pattern)
with open(path, encoding="utf-8") as handle:
for line in handle:
if re.match(r"^\s*-\s+id:\s*", line):
flush()
pattern = None
action = None
continue
pattern_match = re.match(r'^\s*pattern:\s*"(.*)"\s*$', line)
if pattern_match:
pattern = pattern_match.group(1)
continue
action_match = re.match(r"^\s*action:\s*([A-Za-z_]+)\s*$", line)
if action_match:
action = action_match.group(1)
flush()
PY
}
TRACE_ID="$(new_trace_id)"
TIMESTAMP="$(timestamp)"
CONTENT="$(cat "$INPUT_FILE")"
@@ -126,7 +181,7 @@ BLOCK_PATTERNS=(
while IFS= read -r pattern; do
[[ -n "$pattern" ]] && BLOCK_PATTERNS+=("$pattern")
done < <(load_yaml_values "$SECURITY_DIR/prompt-filter.yaml" "pattern")
done < <(load_yaml_rule_patterns "$SECURITY_DIR/prompt-filter.yaml" "block")
APPROVAL_PATTERNS=(
"delete[[:space:]].*"
@@ -169,8 +224,8 @@ NORM_CONTENT="$(normalize_for_match "$CONTENT")"
# fullwidth/zero-width/Cyrillic-lookalike obfuscation cannot split or disguise
# a blocked phrase (V3). Falls back to the raw content if python is missing.
UNI_CONTENT="$CONTENT"
if command -v python >/dev/null 2>&1; then
UNI_CONTENT="$(printf '%s' "$CONTENT" | python "$SCRIPT_DIR/unicode-normalize.py" 2>/dev/null)"
if [[ -n "$PYTHON_BIN" ]]; then
UNI_CONTENT="$(printf '%s' "$CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/unicode-normalize.py" 2>/dev/null)"
[[ -n "$UNI_CONTENT" ]] || UNI_CONTENT="$CONTENT"
fi
UNI_NORM_CONTENT="$(normalize_for_match "$UNI_CONTENT")"
@@ -180,8 +235,8 @@ UNI_NORM_CONTENT="$(normalize_for_match "$UNI_CONTENT")"
# Only mostly-printable decodes survive, so random base64-looking words never
# create a false positive.
DECODED_CONTENT=""
if command -v python >/dev/null 2>&1; then
DECODED_CONTENT="$(printf '%s' "$CONTENT" | python "$SCRIPT_DIR/decode-suspicious.py" 2>/dev/null || true)"
if [[ -n "$PYTHON_BIN" ]]; then
DECODED_CONTENT="$(printf '%s' "$CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/decode-suspicious.py" 2>/dev/null || true)"
fi
# Matches a pattern against the raw (case-insensitive), leetspeak-folded,
@@ -279,7 +334,7 @@ if [[ "$MODE" == "input" ]]; then
SEM_JSON="$TRACE_DIR/semantic-$TRACE_ID.json"
"$SCRIPT_DIR/model-router.sh" "$INPUT_FILE" "$SEM_JSON" --role classify >/dev/null 2>&1 || true
if [[ -f "$SEM_JSON" ]]; then
SEM_VERDICT="$(python -c "import json;print(json.load(open('$SEM_JSON')).get('verdict',''))" 2>/dev/null || echo "")"
SEM_VERDICT="$("$PYTHON_BIN" -c "import json;print(json.load(open('$SEM_JSON')).get('verdict',''))" 2>/dev/null || echo "")"
fi
fi
if [[ "$SEM_VERDICT" == "INJECTION" ]]; then
@@ -302,8 +357,8 @@ fi
SAFE_CONTENT="$CONTENT"
# Policy-driven PII masking (source of truth: pii-rules.yaml). Built-in sed
# masking below remains as defense-in-depth if the policy file is unavailable.
if [[ -f "$SECURITY_DIR/pii-rules.yaml" ]] && command -v python >/dev/null 2>&1; then
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | python "$SCRIPT_DIR/pii-mask.py" "$SECURITY_DIR/pii-rules.yaml")"
if [[ -f "$SECURITY_DIR/pii-rules.yaml" ]] && [[ -n "$PYTHON_BIN" ]]; then
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/pii-mask.py" "$SECURITY_DIR/pii-rules.yaml")"
fi
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$EMAIL_REGEX/***MASKED_EMAIL***/g")"
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$PHONE_REGEX/***MASKED_PHONE***/g")"
@@ -333,7 +388,7 @@ fi
INPUT_HASH="$(printf '%s' "$CONTENT" | hash_text)"
OUTPUT_HASH="$(printf '%s' "$SAFE_CONTENT" | hash_text)"
RULES_JSON="$(printf '%s\n' "${MATCHED_RULES[@]:-}" | python -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
RULES_JSON="$(printf '%s\n' "${MATCHED_RULES[@]:-}" | "$PYTHON_BIN" -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
TRACE_FILE="$TRACE_DIR/security-$TRACE_ID.json"
cat > "$TRACE_FILE" <<EOF
@@ -26,6 +26,15 @@ def job_file(directory):
class ReviewerFallbackTests(unittest.TestCase):
def setUp(self):
# Production enables route preflight. Individual tests opt in explicitly
# so cached/live provider health cannot affect deterministic unit tests.
self.environment = patch.dict(
os.environ, {"CASAN_GOAL_MODEL_PREFLIGHT": "0"}, clear=False,
)
self.environment.start()
self.addCleanup(self.environment.stop)
def test_account_then_omniroute_then_local_and_persists_ledger(self):
calls = []
@@ -115,6 +124,78 @@ class ReviewerFallbackTests(unittest.TestCase):
self.assertEqual(calls, ["openai:gpt", "openai-compatible:route-a", "ollama:local"])
self.assertFalse(job["reviewer_attempts"][0]["retryable"])
def test_patch_reviewer_excludes_h2_model_and_receives_full_output_budget(self):
calls = []
def model(model, prompt, cloud, timeout, max_output_tokens=None):
calls.append((model, max_output_tokens))
return True, "reviewed patch", {}, "ok"
environment = {
"CASAN_GOAL_OMNIROUTE_MODELS": "route-a",
"CASAN_GOAL_REVIEWER_MAX_ATTEMPTS": "3",
"CASAN_GOAL_REVIEWER_DEADLINE_SEC": "30",
}
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, environment, clear=False), patch.object(MODULE, "call_model", model):
path = job_file(directory)
ok, _, _, _, reviewer = MODULE.run_reviewer_chain(
path, "prompt", "", "openai:gpt-worker", "ollama:local",
max_output_tokens=8192, excluded_models={"openai:gpt-worker"},
)
self.assertTrue(ok)
self.assertEqual(reviewer["model"], "openai-compatible:route-a")
self.assertEqual(calls, [("openai-compatible:route-a", 8192)])
def test_local_reviewer_can_be_disabled_when_human_approval_is_available(self):
calls = []
def model(model, prompt, cloud, timeout):
calls.append(model)
return False, "", {}, "gateway_unavailable"
environment = {
"CASAN_GOAL_OMNIROUTE_MODELS": "route-a",
"CASAN_GOAL_ENABLE_LOCAL_REVIEWER": "0",
"CASAN_GOAL_REVIEWER_MAX_ATTEMPTS": "3",
"CASAN_GOAL_REVIEWER_DEADLINE_SEC": "30",
}
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, environment, clear=False), patch.object(MODULE, "call_model", model):
path = job_file(directory)
ok, _, _, reason, _ = MODULE.run_reviewer_chain(path, "prompt", "", "", "ollama:ornith:9b")
self.assertFalse(ok)
self.assertEqual(reason, "gateway_unavailable")
self.assertEqual(calls, ["openai-compatible:route-a"])
def test_patch_reviewer_skips_unhealthy_route_before_full_generation(self):
calls = []
def model(model, prompt, cloud, timeout, max_output_tokens=None):
calls.append(model)
return True, "reviewed", {}, "ok"
environment = {
"CASAN_GOAL_OMNIROUTE_MODELS": "route-b",
"CASAN_GOAL_ENABLE_LOCAL_REVIEWER": "0",
"CASAN_GOAL_REVIEWER_MAX_ATTEMPTS": "3",
"CASAN_GOAL_REVIEWER_DEADLINE_SEC": "30",
}
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, environment, clear=False), \
patch.object(MODULE, "model_preflight", side_effect=[(False, "gateway_503"), (True, "ok")]), \
patch.object(MODULE, "call_model", model):
path = job_file(directory)
ok, _, _, _, reviewer = MODULE.run_reviewer_chain(
path, "prompt", "", "openai-compatible:route-a", "ollama:ornith:9b", max_output_tokens=8192,
)
with open(path, encoding="utf-8") as handle:
job = json.load(handle)
self.assertTrue(ok)
self.assertEqual(reviewer["model"], "openai-compatible:route-b")
self.assertEqual(calls, ["openai-compatible:route-b"])
self.assertIn("model_preflight_failed", job["reviewer_attempts"][0]["reason"])
if __name__ == "__main__":
unittest.main()
@@ -29,9 +29,79 @@ class Result:
class GoalPatchWorkflowTests(unittest.TestCase):
def setUp(self):
# Keep unit tests independent from production route-health settings.
# Tests that exercise preflight mock model_preflight explicitly.
self.environment = patch.dict(
os.environ, {"CASAN_GOAL_MODEL_PREFLIGHT": "0"}, clear=False,
)
self.environment.start()
self.addCleanup(self.environment.stop)
def test_vietnamese_completion_goal_is_write_intent(self):
self.assertTrue(ORCHESTRATOR.requests_side_effect("Hoàn thành component KeyResultDetail với form update progress đầy đủ"))
def test_phase_labeled_backend_completion_is_write_intent(self):
objective = """PHASE 1 — Hoàn thiện backend Objective và Key Result cho ứng dụng OKR.
Chỉ làm việc trong phạm vi apps/okr/backend.
Tạo patch nhưng không tự apply. Chờ Independent Reviewer phê duyệt.
"""
self.assertTrue(ORCHESTRATOR.requests_side_effect(objective))
def test_read_only_backend_audit_is_not_write_intent(self):
objective = "Rà soát backend và liệt kê các file có rủi ro. Không sửa code, không tạo patch."
self.assertFalse(ORCHESTRATOR.requests_side_effect(objective))
def test_scoped_negative_constraint_does_not_cancel_write_intent(self):
objective = """Hoàn thiện tính nguyên tử của luồng cập nhật tiến độ Key Result trong ứng dụng OKR.
Không thay đổi API contract. Chỉ trả về unified git diff và chờ Independent Reviewer phê duyệt.
"""
self.assertTrue(ORCHESTRATOR.requests_side_effect(objective))
def test_valid_worker_patch_still_reaches_human_approval_when_h3_is_unavailable(self):
diff = "diff --git a/apps/okr/backend/a.ts b/apps/okr/backend/a.ts\n--- a/apps/okr/backend/a.ts\n+++ b/apps/okr/backend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
manifest = {"file_count": 1, "characters": 10, "truncated": False, "bundle_sha256": "context-sha"}
artifact = {"path": ".specify/state/goals/default/job.patch", "sha256": "patch-sha", "files": ["apps/okr/backend/a.ts"], "status": "awaiting_approval"}
with tempfile.TemporaryDirectory() as directory:
path = os.path.join(directory, "job.json")
with open(path, "w", encoding="utf-8") as handle:
json.dump({
"id": "goal-1", "goal": "Hoàn thiện backend nhưng không thay đổi API contract.",
"project": "AINative_OKR_CASAN4", "actor": "owner", "local_provider": "ollama",
"cloud_provider": "omniroute", "workspace": {"context_roots": ["apps/okr/backend"]},
"stages": [],
}, handle)
with (
patch.dict(os.environ, {"CASAN_GOAL_LOCAL_MODEL": "ollama:worker", "CASAN_GOAL_CLOUD_MODEL": "openai-compatible:reviewer"}, clear=False),
patch.object(ORCHESTRATOR, "scan", side_effect=lambda text, mode: (True, text)),
patch.object(ORCHESTRATOR, "build_context", return_value=("snapshot", manifest, "manifest.json")),
patch.object(ORCHESTRATOR, "call_model", return_value=(True, diff, {}, "ok")),
patch.object(ORCHESTRATOR, "validate_write_output", return_value=diff),
patch.object(ORCHESTRATOR, "run_reviewer_chain", return_value=(False, "", {}, "model_output_empty", {"provider": "ollama", "model": "ollama:worker"})),
patch.object(ORCHESTRATOR, "validate_and_store_patch", return_value=artifact),
patch.object(ORCHESTRATOR, "submit_side_effect", return_value={"id": "AP-1", "status": "pending", "action": "goal.workspace.execute"}),
patch.object(ORCHESTRATOR, "emit"), patch.object(ORCHESTRATOR, "metric"),
patch.object(ORCHESTRATOR, "audit", return_value="audit-sha"),
):
exit_code = ORCHESTRATOR.run(path)
with open(path, encoding="utf-8") as handle:
job = json.load(handle)
self.assertEqual(exit_code, 0)
self.assertEqual(job["status"], "requires_approval")
self.assertEqual(job["approval"]["id"], "AP-1")
self.assertIn("Automated H3 review was unavailable", job["result"])
self.assertNotIn("independently reviewed", job["result"])
def test_source_code_delete_method_is_not_promoted_to_security_block(self):
source = "export class Service { async delete(id: number) { return this.repo.delete({ where: { id } }); } }"
with tempfile.TemporaryDirectory() as directory, \
patch.dict(os.environ, {"CASAN_STATE_ROOT": directory, "CASAN_SECURITY_STRICT": "0"}, clear=False):
allowed, safe_source = ORCHESTRATOR.scan(source, "input")
self.assertTrue(allowed)
self.assertIn("repo.delete", safe_source)
def test_extract_patch_requires_unified_diff(self):
with self.assertRaisesRegex(ValueError, "goal_patch_missing"):
ORCHESTRATOR.extract_patch("implementation plan only")
@@ -60,6 +130,31 @@ class GoalPatchWorkflowTests(unittest.TestCase):
self.assertIn("@@ -1,1 +1,1 @@", normalized)
self.assertEqual(check.call_count, 2)
def test_patch_fragment_error_triggers_deterministic_hunk_recount(self):
wrong_counts = (
"diff --git a/a b/a\n--- a/a\n+++ b/a\n"
"@@ -1,5 +1,5 @@\n-old\n+new\n"
"@@ -10,7 +10,7 @@\n-tail-old\n+tail-new\n"
)
with patch.object(
ORCHESTRATOR,
"validate_patch_check",
side_effect=[ValueError("goal_patch_check_failed:error: patch fragment without header at line 7: @@ -10,7 +10,7 @@"), None],
) as check:
normalized = ORCHESTRATOR.validate_write_output(wrong_counts)
self.assertIn("@@ -1,1 +1,1 @@", normalized)
self.assertIn("@@ -10,1 +10,1 @@", normalized)
self.assertEqual(check.call_count, 2)
def test_patch_fragment_error_adds_recount_instruction_to_model_repair(self):
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "1", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai:gpt"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
patch.object(ORCHESTRATOR, "call_model", return_value=(False, "", {}, "stopped")) as call,
):
ORCHESTRATOR.repair_write_output("openai:gpt", "original", "invalid", "goal_patch_check_failed:patch fragment without header")
self.assertIn("Recompute every `@@ -old,count +new,count @@` header", call.call_args.args[1])
def test_truncated_replacement_is_not_reinterpreted_as_deletion(self):
truncated = "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1,1 +1,1 @@\n-old\n"
self.assertEqual(ORCHESTRATOR.normalize_unified_diff(truncated), truncated)
@@ -80,6 +175,50 @@ class GoalPatchWorkflowTests(unittest.TestCase):
self.assertEqual(manifest["files"][0]["characters"], len(raw.strip()))
self.assertFalse(manifest["files"][0]["truncated"])
def test_explicit_scope_paths_rank_source_ahead_of_general_architecture(self):
project = ORCHESTRATOR.registered_project("AINative_OKR_CASAN4")
objective = """PHASE 1 — Hoàn thiện backend Objective và Key Result.
Nguồn sự thật:
- docs/technical_architecture.md
- apps/okr/domain/input/okr-requirement.md
Chỉ làm việc trong:
- apps/okr/backend/src/objectives/**
- apps/okr/backend/src/key-results/**
- apps/okr/backend/prisma/schema.prisma
Tạo patch nhưng không tự apply.
"""
ranked = ORCHESTRATOR.context_candidates(project, objective)
top_paths = [row[1] for row in ranked[:10]]
self.assertTrue(any(path.startswith("apps/okr/backend/src/objectives/") for path in top_paths))
self.assertTrue(any(path.startswith("apps/okr/backend/src/key-results/") for path in top_paths))
self.assertIn("apps/okr/backend/prisma/schema.prisma", top_paths)
self.assertNotIn("docs/technical_architecture.md", top_paths)
self.assertNotIn("apps/okr/domain/input/okr-requirement.md", top_paths)
def test_explicit_only_scope_excludes_unrequested_context_files(self):
goal = """Hoàn thiện backend.
Chỉ được đọc và thay đổi:
apps/okr/backend/src/key-results/key-results.service.ts
apps/okr/backend/test/services.test.ts
Không sửa bất kỳ file nào khác.
"""
candidates = [
(2000, "apps/okr/backend/src/key-results/key-results.service.ts", "service"),
(2000, "apps/okr/backend/test/services.test.ts", "tests"),
(50, "apps/okr/backend/src/objectives/objectives.service.ts", "unrequested"),
]
project = {"domain": "OKR", "domain_root": "apps/okr", "roots": []}
with tempfile.TemporaryDirectory() as directory, \
patch.object(ORCHESTRATOR, "registered_project", return_value=project), \
patch.object(ORCHESTRATOR, "context_candidates", return_value=candidates), \
patch.object(ORCHESTRATOR, "scan", side_effect=lambda text, mode: (True, text)):
bundle, manifest, _ = ORCHESTRATOR.build_context(os.path.join(directory, "goal.json"), "okr", goal, True)
self.assertEqual([row["path"] for row in manifest["files"]], [
"apps/okr/backend/src/key-results/key-results.service.ts",
"apps/okr/backend/test/services.test.ts",
])
self.assertNotIn("unrequested", bundle)
def test_invalid_write_output_gets_one_bounded_repair_attempt(self):
repaired = "```diff\ndiff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n```"
with patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "1"}, clear=False), \
@@ -114,6 +253,53 @@ class GoalPatchWorkflowTests(unittest.TestCase):
self.assertEqual(call.call_args.args[0], "openai:gpt-4o-mini")
self.assertEqual(call.call_args.kwargs["max_output_tokens"], ORCHESTRATOR.patch_output_tokens())
def test_repair_skips_model_that_fails_generation_preflight(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "2", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", side_effect=[(False, "gateway_503"), (True, "ok")]),
patch.object(ORCHESTRATOR, "call_model", return_value=(True, repaired, {}, "ok")) as call,
patch.object(ORCHESTRATOR, "validate_write_output", return_value=repaired),
):
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("ollama:ornith:9b", "original", "invalid")
self.assertTrue(ok)
self.assertEqual(reason, "ok")
self.assertEqual(call.call_count, 1)
self.assertEqual(call.call_args.args[0], "ollama:ornith:9b")
self.assertIn("model_preflight_failed", usage["repair_attempts"][0]["reason"])
def test_logged_in_account_worker_is_first_patch_repair_candidate(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "2", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
patch.object(ORCHESTRATOR, "call_account_model", return_value=(True, repaired, {}, "ok")) as account_call,
patch.object(ORCHESTRATOR, "call_model") as routed_call,
patch.object(ORCHESTRATOR, "validate_write_output", return_value=repaired),
):
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("account:codex", "original", "invalid")
self.assertTrue(ok)
self.assertEqual(reason, "ok")
self.assertEqual(account_call.call_args.args[0], "codex")
routed_call.assert_not_called()
self.assertEqual(usage["repair_attempts"][0]["provider"], "codex-account")
def test_account_codex_gets_corrective_pass_before_other_routes(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "3", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
patch.object(ORCHESTRATOR, "call_account_model", side_effect=[(True, "corrupt", {}, "ok"), (True, repaired, {}, "ok")]) as account_call,
patch.object(ORCHESTRATOR, "call_model") as routed_call,
patch.object(ORCHESTRATOR, "validate_write_output", side_effect=[ValueError("goal_patch_check_failed:patch fragment without header"), repaired]),
):
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("account:codex", "original", "invalid")
self.assertTrue(ok)
self.assertEqual(reason, "ok")
self.assertEqual(account_call.call_count, 2)
routed_call.assert_not_called()
self.assertEqual([row["model"] for row in usage["repair_attempts"]], ["account:codex", "account:codex"])
def test_repair_tries_next_stronger_candidate_when_first_patch_is_corrupt(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with (
@@ -178,6 +364,16 @@ class GoalPatchWorkflowTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "goal_patch_outside_workspace"):
ORCHESTRATOR.validate_and_store_patch(path, job, content)
def test_patch_outside_explicit_file_scope_is_denied(self):
job = {
"goal": "Chỉ được đọc và thay đổi:\napps/okr/backend/allowed.ts\nKhông sửa bất kỳ file nào khác.",
"workspace": {"context_roots": ["apps/okr/backend"]},
}
content = "diff --git a/apps/okr/backend/other.ts b/apps/okr/backend/other.ts\n--- a/apps/okr/backend/other.ts\n+++ b/apps/okr/backend/other.ts\n@@ -1 +1 @@\n-a\n+b\n"
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "goal_patch_outside_workspace"):
ORCHESTRATOR.validate_and_store_patch(os.path.join(directory, "job.json"), job, content)
def test_patch_rename_source_outside_workspace_is_denied(self):
job = {"workspace": {"context_roots": ["apps/okr/frontend"]}}
content = "diff --git a/package.json b/apps/okr/frontend/package.json\nsimilarity index 100%\nrename from package.json\nrename to apps/okr/frontend/package.json\n"
@@ -199,9 +395,20 @@ class GoalPatchWorkflowTests(unittest.TestCase):
def test_frontend_patch_runs_build_and_tests(self):
commands = EXECUTOR.verification_commands(["apps/okr/frontend/src/pages/KeyResultDetail.tsx"])
self.assertFalse(any(command[:2] == ["git", "diff"] for command in commands))
self.assertIn(["npm", "run", "build", "-w", "@ainative-okr/frontend"], commands)
self.assertIn(["npm", "test", "-w", "@ainative-okr/frontend"], commands)
def test_executor_can_retry_only_a_verified_rollback_failure(self):
artifact = {"path": "job.patch"}
self.assertTrue(EXECUTOR.ready_for_apply({"status": "requires_approval", "patch_artifact": artifact}))
self.assertTrue(EXECUTOR.ready_for_apply({
"status": "failed",
"error": "GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK",
"patch_artifact": artifact,
}))
self.assertFalse(EXECUTOR.ready_for_apply({"status": "failed", "error": "OTHER", "patch_artifact": artifact}))
def test_service_desk_patch_uses_service_desk_manifest_commands(self):
commands = EXECUTOR.verification_commands(["apps/service-desk/src/ticket.js"])
self.assertIn(["node", "--check", "apps/service-desk/src/ticket.js"], commands)