Harden adaptive apply approval baselines

This commit is contained in:
yaojingang
2026-06-15 23:11:31 +08:00
parent 10a1b1d963
commit b18a134497
8 changed files with 97 additions and 4 deletions
+5 -3
View File
@@ -10,8 +10,8 @@ Adaptive iteration is proposal-only until a human explicitly approves a patch ap
- redact sensitive text before storing evidence excerpts;
- summarize repeated preferences and operational signals;
- produce adaptation proposals with target files, risks, tests, and rollback plans.
- dry-run an approved patch through `adapt-apply`, after patch hash, approval, and target allowlist checks pass.
- apply a patch only when the operator passes `--apply` and the approval ledger names the reviewer, reason, patch hash, target files, verification commands, and rollback plan.
- dry-run an approved patch through `adapt-apply`, after patch hash, approval, target allowlist, and target baseline hash checks pass.
- apply a patch only when the operator passes `--apply` and the approval ledger names the reviewer, reason, patch hash, target files, target file SHA-256 baselines, verification commands, and rollback plan.
- automatically reverse an applied patch when `--run-verification` fails, unless the operator explicitly passes `--no-rollback-on-failure`.
It must not:
@@ -21,6 +21,7 @@ It must not:
- write source files as part of scan or proposal generation;
- write source files through `adapt-apply` without explicit `--apply`;
- apply a patch whose target files are outside both the proposal and approval allowlists;
- apply a patch when an approved target file has changed since the reviewer recorded its baseline SHA-256;
- leave a failed verified apply in place by default;
- count proposals as completed implementation evidence.
@@ -31,7 +32,7 @@ It must not:
3. A reviewer decides whether any proposal is worth implementing.
4. `adapt-apply --write-template` creates `reports/adaptation_approval_ledger.json` and `reports/adaptation_regression_report.json` so the review surface exists before any patch is applied.
5. `adapt-apply --proposal-id <id> --patch-file <patch>` defaults to a dry-run and records patch, target, approval, regression, and rollback evidence.
6. `adapt-apply --apply --run-verification` may write files only after approval, patch hash, allowlist, `git apply --check`, and safe regression command checks pass.
6. `adapt-apply --apply --run-verification` may write files only after approval, patch hash, allowlist, target baseline hash, `git apply --check`, and safe regression command checks pass.
7. If a verification command fails after a patch is applied, `adapt-apply` runs `git apply -R <patch>` by default and records `failed-rolled-back` plus rollback evidence in `reports/adaptation_regression_report.json`.
## Evidence Standard
@@ -51,6 +52,7 @@ Each approved application should include:
- reviewer, reason, approval date, and optional expiry;
- exact patch SHA-256;
- target file allowlist;
- target file SHA-256 baselines for every patch target, or `__absent__` for approved new files;
- regression commands restricted to local `make` targets or local Python verifier scripts;
- rollback command or plan.
- rollback result if regression failed after an apply attempt.
+1
View File
@@ -12,6 +12,7 @@
"approval_required": true,
"patch_sha256_required": true,
"allowlisted_targets_required": true,
"target_file_sha256_required": true,
"dry_run_default": true,
"writes_repository_files_only_with_apply": true,
"rollback_required": true
+1
View File
@@ -15,6 +15,7 @@
"approval_required": true,
"writes_repository_files": false,
"allowlisted_targets_required": true,
"target_file_sha256_required_for_apply": true,
"rollback_required_for_apply": true,
"apply_command_available": true
},
@@ -17,6 +17,7 @@
"approval_required": true,
"patch_sha256_required": true,
"allowlisted_targets_required": true,
"target_file_sha256_required": true,
"dry_run_default": true,
"writes_repository_files_only_with_apply": true,
"rollback_required": true,
+4
View File
@@ -51,6 +51,7 @@
"approval_required",
"writes_repository_files",
"allowlisted_targets_required",
"target_file_sha256_required_for_apply",
"rollback_required_for_apply",
"apply_command_available"
],
@@ -67,6 +68,9 @@
"allowlisted_targets_required": {
"const": true
},
"target_file_sha256_required_for_apply": {
"const": true
},
"rollback_required_for_apply": {
"const": true
},
+54 -1
View File
@@ -18,6 +18,7 @@ SCRIPT_INTERFACE = "cli"
SCRIPT_INTERFACE_REASON = "Approval-gated adaptive patch application with dry-run, allowlist, regression, and rollback evidence."
BLOCKED_PATH_PARTS = {".git", "__pycache__", ".pytest_cache", "dist"}
ABSENT_FILE_SHA256 = "__absent__"
def utc_now() -> str:
@@ -109,7 +110,16 @@ def parse_date(value: str) -> date | None:
def validate_approval(approval: dict[str, Any], today: date) -> list[str]:
failures = []
required = ["reviewer", "reason", "approved_at", "patch_sha256", "target_files", "verification_commands", "rollback_plan"]
required = [
"reviewer",
"reason",
"approved_at",
"patch_sha256",
"target_files",
"target_file_sha256",
"verification_commands",
"rollback_plan",
]
for key in required:
if not approval.get(key):
failures.append(f"Approval entry missing required field: {key}")
@@ -118,11 +128,36 @@ def validate_approval(approval: dict[str, Any], today: date) -> list[str]:
failures.append(f"Approval entry is expired: {approval.get('expires_at')}")
if not isinstance(approval.get("target_files"), list):
failures.append("Approval target_files must be a list.")
if not isinstance(approval.get("target_file_sha256"), dict):
failures.append("Approval target_file_sha256 must be an object.")
if not isinstance(approval.get("verification_commands"), list):
failures.append("Approval verification_commands must be a list.")
return failures
def validate_target_file_sha256(
skill_dir: Path,
target_files: list[str],
expected_sha256: dict[str, Any],
) -> tuple[list[str], dict[str, str]]:
failures: list[str] = []
observed: dict[str, str] = {}
for target in target_files:
path = skill_dir / target
expected = str(expected_sha256.get(target, ""))
if not expected:
failures.append(f"Approval target_file_sha256 missing target: {target}")
continue
if path.exists() and not path.is_file():
failures.append(f"Patch target is not a file: {target}")
continue
current = sha256_file(path) if path.exists() else ABSENT_FILE_SHA256
observed[target] = current
if current != expected:
failures.append(f"Target file baseline sha256 does not match approval ledger: {target}")
return failures, observed
def safe_command(command: str) -> tuple[bool, list[str], str]:
try:
parts = shlex.split(command)
@@ -189,6 +224,7 @@ def empty_approval_ledger(generated_at: str) -> dict[str, Any]:
"approval_required": True,
"patch_sha256_required": True,
"allowlisted_targets_required": True,
"target_file_sha256_required": True,
"dry_run_default": True,
"writes_repository_files_only_with_apply": True,
"rollback_required": True,
@@ -217,6 +253,7 @@ def empty_regression_report(skill_dir: Path, generated_at: str) -> dict[str, Any
"approval_required": True,
"patch_sha256_required": True,
"allowlisted_targets_required": True,
"target_file_sha256_required": True,
"dry_run_default": True,
"writes_repository_files_only_with_apply": True,
"rollback_required": True,
@@ -320,6 +357,7 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]:
patch_sha = sha256_file(patch_path) if patch_path.exists() else ""
target_files: list[str] = []
observed_target_file_sha256: dict[str, str] = {}
if not failures:
try:
target_files = patch_target_files(patch_path.read_text(encoding="utf-8", errors="replace"))
@@ -337,6 +375,13 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]:
failures.append("Patch touches files outside approval target_files.")
if not patch_targets <= proposal_targets:
failures.append("Patch touches files outside proposal target_files.")
if not failures:
baseline_failures, observed_target_file_sha256 = validate_target_file_sha256(
skill_dir,
target_files,
approval.get("target_file_sha256", {}),
)
failures.extend(baseline_failures)
check = git_apply_check(skill_dir, patch_path) if not failures else {"ok": False, "returncode": None, "stdout": "", "stderr": ""}
if not failures and not check["ok"]:
@@ -378,6 +423,13 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]:
"approved_at": approval.get("approved_at", ""),
"expires_at": approval.get("expires_at", ""),
},
"target_file_sha256": {
"expected": {
target: str(approval.get("target_file_sha256", {}).get(target, ""))
for target in target_files
},
"observed": observed_target_file_sha256,
},
"git_apply_check": check,
"git_apply": apply_result,
"regression_runs": regression_runs,
@@ -406,6 +458,7 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]:
"approval_required": True,
"patch_sha256_required": True,
"allowlisted_targets_required": True,
"target_file_sha256_required": True,
"dry_run_default": True,
"writes_repository_files_only_with_apply": True,
"rollback_required": True,
+1
View File
@@ -178,6 +178,7 @@ def build_report(skill_dir: Path, patterns_json: Path, generated_at: str) -> dic
"approval_required": True,
"writes_repository_files": False,
"allowlisted_targets_required": True,
"target_file_sha256_required_for_apply": True,
"rollback_required_for_apply": True,
"apply_command_available": (ROOT / "scripts" / "apply_adaptation.py").exists(),
},
+30
View File
@@ -91,6 +91,7 @@ def main() -> None:
assert proposal_payload["proposal_contract"]["proposal_only"] is True, proposal_payload
assert proposal_payload["proposal_contract"]["writes_repository_files"] is False, proposal_payload
assert proposal_payload["proposal_contract"]["apply_command_available"] is True, proposal_payload
assert proposal_payload["proposal_contract"]["target_file_sha256_required_for_apply"] is True, proposal_payload
assert proposal_payload["summary"]["proposal_count"] >= 3, proposal_payload
assert all(item["status"] == "proposal-only" for item in proposal_payload["proposals"]), proposal_payload
assert all(item["requires_approval"] is True for item in proposal_payload["proposals"]), proposal_payload
@@ -107,6 +108,7 @@ def main() -> None:
template_payload = json.loads(template_proc.stdout)
assert template_payload["summary"]["apply_supported"] is True, template_payload
assert template_payload["summary"]["attempt_count"] == 0, template_payload
assert template_payload["apply_contract"]["target_file_sha256_required"] is True, template_payload
assert (reports_dir / "adaptation_approval_ledger.json").exists(), reports_dir
assert (reports_dir / "adaptation_regression_report.json").exists(), reports_dir
@@ -166,6 +168,7 @@ def main() -> None:
assert cli_proposal_payload["proposal_contract"]["proposal_only"] is True, cli_proposal_payload
assert cli_proposal_payload["proposal_contract"]["writes_repository_files"] is False, cli_proposal_payload
assert cli_proposal_payload["proposal_contract"]["apply_command_available"] is True, cli_proposal_payload
assert cli_proposal_payload["proposal_contract"]["target_file_sha256_required_for_apply"] is True, cli_proposal_payload
policy_path = cli_skill_dir / "references" / "user-memory-policy.md"
policy_path.parent.mkdir(parents=True, exist_ok=True)
@@ -198,6 +201,7 @@ def main() -> None:
"approval_required": True,
"patch_sha256_required": True,
"allowlisted_targets_required": True,
"target_file_sha256_required": True,
"dry_run_default": True,
"writes_repository_files_only_with_apply": True,
"rollback_required": True,
@@ -212,6 +216,9 @@ def main() -> None:
"expires_at": "2026-12-31",
"patch_sha256": sha256_text(patch_text),
"target_files": ["references/user-memory-policy.md"],
"target_file_sha256": {
"references/user-memory-policy.md": sha256_text("old policy\n"),
},
"verification_commands": ["python3 tests/check_policy.py"],
"rollback_plan": "git apply -R approved-adaptation.patch",
}
@@ -219,6 +226,29 @@ def main() -> None:
}
ledger_path = cli_skill_dir / "reports" / "adaptation_approval_ledger.json"
ledger_path.write_text(json.dumps(approval_ledger, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
policy_path.write_text("changed outside approval\n", encoding="utf-8")
stale_proc = run_script(
str(CLI),
"adapt-apply",
str(cli_skill_dir),
"--proposal-id",
approval_proposal["proposal_id"],
"--patch-file",
str(patch_path),
"--output-json",
str(cli_skill_dir / "reports" / "stale_baseline_regression.json"),
"--output-md",
str(cli_skill_dir / "reports" / "stale_baseline_regression.md"),
"--generated-at",
"2026-06-15T00:00:00Z",
"--today",
"2026-06-15",
)
assert stale_proc.returncode == 2, stale_proc.stdout
stale_payload = json.loads(stale_proc.stdout)
assert any("baseline sha256" in item for item in stale_payload["failures"]), stale_payload
assert "approved adaptive note" not in policy_path.read_text(encoding="utf-8"), policy_path
policy_path.write_text("old policy\n", encoding="utf-8")
dry_run = run_script(
str(CLI),
"adapt-apply",