[verified] Enforce delegation adoption at runtime consumers

This commit is contained in:
2026-10-11 07:11:40 +00:00
parent 9367d5a3a1
commit 6cb2317667
19 changed files with 1112 additions and 47 deletions
@@ -24,6 +24,8 @@ Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
실제 소비 함수에서도 [runtime consumption 정본](../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 `gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`에서 main-owned push/PR adapter를 호출한다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
## Managed-lock broker gate
Issue workflow는 bootstrap 이후 반드시 [Repository cache 운영 지침](../../../../operations/repository-cache.md)의 하나의 role-neutral managed-lock broker를 사용한다. `serve`에 canonical Issue identity, role, workflow run-id와 TTL을 전달해 broker-generated runtime을 만들고, ready JSON의 `handle`/`socket` 경로만 사용한다. 경로는 `<issue>/runtime/<workflow-run-id>/broker.handle`과 `broker.sock`이며 verification stage run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. Client는 raw token을 inspect/log하지 않으며 public `request` API가 token을 캡슐화한다. Repository lock은 같은 handle의 `acquire_repository`/`release_repository` request로만 중첩하고, TTL 전에 `renew`한다. 정상·오류·cancel 모두 `release_repository`(보유 시) → `release` → broker wait/reap → `probe`의 `free=true` 확인 순서를 완료하기 전에는 완료·로그·보고하지 않는다. Raw `flock ... sleep`과 lock-file unlink는 금지한다.
@@ -197,11 +199,11 @@ Discovery가 `신규 작성` 또는 `수정`으로 판정한 문서만 작성한
6. 기존 requirement revision이면 final tree에 Contract Preservation Inventory를 다시 대응해 `missing=0`, `ambiguous=0`을 확인한다. 충족하지 못하면 stage하거나 commit하지 않는다.
7. 승인된 modified·untracked file을 exact allowlist로 stage하고 staged diff를 다시 읽는다. 범위 밖 파일, credential, generated artifact 또는 예상하지 못한 삭제가 있으면 commit하지 않는다.
8. Repository convention에 맞는 non-amended commit을 생성하고 base ancestry, commit tree와 worktree 상태를 검증한다. Push 전에 그 exact commit의 strict clean checkout에서 canonical external verification helper로 최종 검증을 실행·validate하고 manifest의 source SHA가 전달할 commit과 일치함을 확인한다. 실패하면 local commit을 보존하고 push하지 않는다.
9. Issue worktree는 delivery 전후 모두 detached HEAD로 유지한다. Issue 전용 remote branch가 없으면 detached final commit을 explicit repository URL과 `<final-sha>:refs/heads/<issue-branch>` refspec으로 push해 생성하고, 있으면 현재 작업이 소유한 branch이며 fast-forward 가능한 경우에만 같은 방식으로 갱신한다. `git switch -c`, `git checkout -b`, `git branch` 또는 동등한 명령으로 canonical mirror의 `refs/heads/*`에 구현용 local branch를 만들지 않는다. Default branch에는 직접 push하지 않고 force-push하지 않으며 canonical cache의 mirror remote 설정을 변경하지 않는다.
9. Main은 아래 9–12의 push/readback/PR 동작을 main-owned adapter로 구현하고 `scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`로 실행한다. Callback을 직접 먼저 실행하거나 이전 PASS로 생략하지 않는다. Adapter의 인증·repository/Issue·branch scope 검증은 아래 규칙을 모두 유지한다. Issue worktree는 delivery 전후 모두 detached HEAD로 유지한다. Issue 전용 remote branch가 없으면 detached final commit을 explicit repository URL과 `<final-sha>:refs/heads/<issue-branch>` refspec으로 push해 생성하고, 있으면 현재 작업이 소유한 branch이며 fast-forward 가능한 경우에만 같은 방식으로 갱신한다. `git switch -c`, `git checkout -b`, `git branch` 또는 동등한 명령으로 canonical mirror의 `refs/heads/*`에 구현용 local branch를 만들지 않는다. Default branch에는 직접 push하지 않고 force-push하지 않으며 canonical cache의 mirror remote 설정을 변경하지 않는다.
10. Target repository write 권한이 없으면 같은 push를 반복하지 않는다. Authenticated identity가 fork를 생성할 수 있으면 기존 fork·동명 branch·PR 유무와 fork parent/base SHA를 검증한 뒤 fork의 Issue 전용 branch로 non-force push하고 cross-repository PR을 사용한다. Fork도 쓸 수 없으면 permission blocker와 local commit SHA를 보고한다.
11. Remote readback으로 branch owner, repository, branch SHA가 local commit SHA와 exact-match하는지 확인한다.
12. Default branch를 base로 PR을 생성한다. 제목은 Issue를 식별하고, 본문에는 `Closes #<number>`, 구현 요약, 문서 변경, 실제 검증 명령·결과, 제한사항과 보안상 주의점을 기록한다. PR 생성 자체로 Issue state를 변경하지 않는다.
13. PR 번호를 얻은 뒤 요구사항 문서의 `구현 PR: 미정`을 실제 PR 번호 또는 URL로 교체하고, 문서 검증과 관련 test를 재실행해 별도 commit으로 같은 branch에 non-force push한다.
13. 후속 commit의 push에도 새 head/source/context/result를 결속해 동일 delivery workflow를 사용하며 create_or_reuse_pr는 이미 확인된 같은 PR을 재사용한다. PR 번호를 얻은 뒤 요구사항 문서의 `구현 PR: 미정`을 실제 PR 번호 또는 URL로 교체하고, 문서 검증과 관련 test를 재실행해 별도 commit으로 같은 branch에 non-force push한다.
14. Gitea에서 PR details, base branch, head repository·branch·SHA, state, changed files와 commit을 다시 읽는다. 최종 remote head SHA가 최종 local commit SHA와 일치할 때만 전달 완료로 판정한다.
15. Gitea Actions 또는 commit status가 제공되면 최종 head의 success, failed, pending, run 없음 또는 조회 불가 상태를 읽고 그대로 보고한다.
16. PR 생성 또는 후속 push가 부분 실패하면 blind retry하거나 중복 PR을 만들지 않는다. 이미 생성된 fork·branch·PR과 local commit의 exact 상태 및 안전한 복구 지점을 보고한다.
@@ -0,0 +1,41 @@
"""Canonical implementation delivery adoption boundary.
The caller owns the authenticated transport and all push/readback invariants.
This module only gates that transport on fresh delegation evidence.
"""
from __future__ import annotations
import re
_SHA40 = re.compile(r"^[0-9a-f]{40}$")
class AdoptionError(ValueError):
pass
def require_adoption(declaration, *, expected_source, expected_stage):
try:
import sys
from pathlib import Path
repo_root = Path(__file__).resolve().parents[6]
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from harness.lib.delegation_adoption import require_adoption as checker
except Exception as exc:
raise AdoptionError("delegation adoption unavailable") from exc
try:
return checker(declaration, expected_source=expected_source, expected_stage=expected_stage)
except Exception as exc:
raise AdoptionError("delegation adoption rejected") from exc
def deliver(adapter, *, head, declaration):
if not callable(adapter):
raise AdoptionError("trusted delivery adapter required")
if type(head) is not str or not _SHA40.fullmatch(head):
raise AdoptionError("invalid delivery head")
# Keep this immediately before the external-write callback: evidence must be fresh.
require_adoption(declaration, expected_source={"kind": "git_tree", "revision": head},
expected_stage="implementation-delivery")
return adapter()
@@ -0,0 +1,90 @@
"""Initial implementation delivery workflow with independent transport readbacks."""
from __future__ import annotations
import importlib.util
import re
from pathlib import Path
from types import ModuleType
from typing import Any
_SHA40 = re.compile(r"^[0-9a-f]{40}$")
_REQUIRED = ("push", "read_remote_head", "create_or_reuse_pr", "read_pr")
def _load_delivery_adoption() -> ModuleType:
path = Path(__file__).resolve().parent / "delivery_adoption.py"
spec = importlib.util.spec_from_file_location("gitea_delivery_adoption", path)
if spec is None or spec.loader is None:
raise AdoptionError("delivery adoption unavailable")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
_delivery_adoption = _load_delivery_adoption()
AdoptionError = _delivery_adoption.AdoptionError
def _reject(message: str) -> None:
raise AdoptionError(message)
def _validate_adapter(adapter: Any) -> None:
for name in _REQUIRED:
try:
value = getattr(adapter, name)
except Exception:
_reject("trusted delivery adapter required")
else:
if not callable(value):
_reject("trusted delivery adapter required")
def _validate_head(head: Any) -> str:
if type(head) is not str or not _SHA40.fullmatch(head):
_reject("invalid delivery head")
return head
def _validate_pr_number(value: Any) -> int:
if type(value) is not int or isinstance(value, bool) or value < 1:
_reject("invalid pull request number")
return value
def _validate_readback(value: Any, number: int, head: str) -> dict[str, Any]:
if type(value) is not dict:
_reject("invalid pull request readback")
if value.get("number") != number or type(value.get("number")) is not int:
_reject("pull request number mismatch")
if value.get("head") != head or type(value.get("head")) is not str:
_reject("pull request head mismatch")
if value.get("state") != "open":
_reject("pull request state mismatch")
# Bound the returned artifact even when the trusted adapter exposes richer data.
return {"number": number, "head": head, "state": "open"}
def deliver_implementation(adapter: Any, *, head: str, declaration: Any) -> dict[str, Any]:
"""Push an implementation and create/reuse a PR only after fresh gates."""
_validate_head(head)
_validate_adapter(adapter)
# The canonical helper performs the explicit declaration/adoption check
# immediately before the first external write.
_delivery_adoption.deliver(lambda: adapter.push(head), head=head, declaration=declaration)
remote_head = adapter.read_remote_head()
if remote_head != head or type(remote_head) is not str or not _SHA40.fullmatch(remote_head):
_reject("remote head mismatch")
# Re-check fresh evidence immediately before the PR write.
pr_number = _delivery_adoption.deliver(
lambda: adapter.create_or_reuse_pr(head), head=head, declaration=declaration
)
pr_number = _validate_pr_number(pr_number)
readback = adapter.read_pr(pr_number)
bounded_pr = _validate_readback(readback, pr_number, head)
return {"remote_head": remote_head, "pr": bounded_pr}
@@ -26,6 +26,8 @@ Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
실제 소비 함수에서도 [runtime consumption 정본](../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 `gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`에서 main-owned push/PR adapter를 호출한다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
## Managed-lock broker gate
Bootstrap 이후 이 Issue 재작업도 canonical [Repository cache 운영 지침](../../../../operations/repository-cache.md)의 공통 managed-lock broker 하나만 사용한다. `serve`에 canonical Issue identity, role, workflow run-id와 TTL을 전달해 broker-generated runtime을 만들고 ready JSON의 `handle`/`socket` 경로만 사용한다. 경로는 `<issue>/runtime/<workflow-run-id>/broker.handle`과 `broker.sock`이며 verification run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. 같은 handle로만 public `request` API의 `acquire_repository`/`release_repository`를 요청하고 TTL 전에 `renew`한다. Client는 raw token을 inspect/log하지 않으며 broker request API가 token을 캡슐화한다. 정상·오류·cancel cleanup은 보유한 repository lease release, Issue `release`, broker wait/reap, `probe`의 `free=true` 확인까지 동일하며 그 전에는 completed/log/report를 주장하지 않는다. Raw `flock ... sleep`, lock-file unlink와 client-side lock holder는 금지한다.
@@ -265,7 +267,7 @@ Reviewer가 rework head를 새 snapshot으로 다시 검토해야 하며 이 com
3. Push 결과만 신뢰하지 않고 remote branch SHA와 PR head SHA가 final local HEAD와 exact-match하는지 readback한다.
4. PR body 갱신이 필요한 경우 write 직전에 PR 전체 필드, review·comment·commit collection cardinality와 authenticated impl identity를 다시 읽는다. Current body의 `Closes #<issue-number>`와 기존 구현 내용을 보존하고 재작업 review ID·reviewed head·finding별 조치·최종 검증과 unresolved limitation만 반영한 exact body payload를 명시해 한 번만 호출한다. 성공 응답만 신뢰하지 않고 fresh PR과 관련 collection을 읽어 PR author stable account ID, exact body payload, `updated_at`, base·head·commit/ref, state·draft·merge 상태, labels·assignees·reviewer와 collection cardinality를 baseline과 비교한다. Exact body와 provider가 해당 body update로 파생한 `updated_at` 외의 변경은 승인되지 않았으며 인과를 증명할 수 없으면 partial로 중단한다.
5. PR changed files, commits, state, merge 상태, statuses와 workflow runs를 다시 읽는다. Local test, commit status와 Actions run을 별도 사실로 기록한다.
6. Finding matrix와 최종 검증 기록을 bounded JSON input으로 만들고 `scripts/rework_comment.py render --input <input.json> --comment <comment.md> --expected <expected.json>`를 실행해 canonical 재작업 결과 comment, marker-free digest와 correlation을 생성·검증한다. Comment body와 marker를 수동 조립하지 않는다.
6. Finding matrix와 최종 검증 기록을 bounded JSON input으로 만들고 `scripts/rework_comment.py render --input <input.json> --comment <comment.md> --expected <expected.json> --delegation-adoption <absolute-main-declaration.json>`를 실행해 canonical 재작업 결과 comment, marker-free digest와 correlation을 생성·검증한다. Comment body와 marker를 수동 조립하지 않는다.
7. Exact PR top-level comment endpoint target과 fresh PR metadata binding을 검증해 Source Issue comments, inline comments, review replies와 native reviews에서 분리하고 scoped collection을 complete pagination한 뒤 canonical deduplication 규칙을 적용한다. Existing exact comment가 없으면 exact PR conversation에 body를 한 번 게시하고, 있으면 write 없이 재사용한다.
8. Exact comment와 scoped complete collection을 fresh normalized JSON으로 readback한 뒤 `scripts/rework_comment.py verify --expected <expected.json> --pr-readback <pr.json> --comments-readback <comments.json> --identity-readback <identity.json> --action <create|reuse> --baseline-count <N> --output <handoff-receipt.json>`를 실행한다. Receipt의 comment ID, impl author stable account ID, body, timestamp, final head, digest, correlation과 cardinality가 검증돼야 하며, 신규 게시로 허용된 PR-underlying-issue projection 외의 PR field, Source Issue field와 기존 review collection에 변경이 없어야 한다.
9. Native review를 작성·dismiss·reply하거나 새 review를 요청하지 않는다. 기존 `REQUEST_CHANGES`는 reviewer가 새 head를 재검토하기 전까지 자동 해소된 것으로 주장하지 않는다.
@@ -21,10 +21,16 @@ def read_json(p):
return x
def read_json_any(p):
b=Path(p).read_bytes()
if len(b)>MAX: raise Bad("input too large")
try: return json.loads(b)
except Exception as e: raise Bad("invalid json") from e
repo_root = Path(__file__).resolve().parents[6]
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
try:
from harness.lib.delegation_evidence import _json_file
value = _json_file(str(p))
except Exception as e:
raise Bad("invalid json") from e
if not isinstance(value, dict): raise Bad("object required")
return value
def text(x, name, pattern=None, maxlen=4096):
if not isinstance(x,str) or not x or len(x)>maxlen or "\r" in x or "\n" in x or x.rstrip()!=x: raise Bad(f"invalid {name}")
@@ -83,7 +89,17 @@ def atomic(path,data):
if os.path.exists(tmp): os.unlink(tmp)
def do_render(a):
d,vals,fs,ex,cs,ar=canon(read_json(a.input))
raw = read_json(a.input)
declaration = {"mode": "none"} if a.no_delegated_evidence else read_json_any(a.delegation_adoption)
try:
repo_root = Path(__file__).resolve().parents[6]
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from harness.lib.delegation_adoption import require_adoption
require_adoption(declaration, expected_source={"kind": "git_tree", "revision": raw.get("final_head")}, expected_stage="implementation-delivery")
except Exception:
raise Bad("delegation adoption rejected") from None
d,vals,fs,ex,cs,ar=canon(raw)
body=render_body(d,vals,fs,ex,cs,ar); bb=body.encode()
rh=hashlib.sha256(bb).hexdigest()
corr=hashlib.sha256("\0".join([d['owner'],d['repository'],vals['issue'],vals['pr'],vals['review'],d['reviewed_head'],d['final_head'],d['contract_sha256']]).encode()).hexdigest()
@@ -137,6 +153,7 @@ def do_verify(a):
def main():
p=argparse.ArgumentParser(); sp=p.add_subparsers(dest="cmd",required=True)
r=sp.add_parser("render"); r.add_argument("--input",required=True); r.add_argument("--comment",required=True); r.add_argument("--expected",required=True)
adoption=r.add_mutually_exclusive_group(required=True); adoption.add_argument("--no-delegated-evidence",action="store_true"); adoption.add_argument("--delegation-adoption")
v=sp.add_parser("verify"); v.add_argument("--expected",required=True); v.add_argument("--pr-readback",required=True); v.add_argument("--comments-readback",required=True); v.add_argument("--identity-readback",required=True); v.add_argument("--action",choices=("create","reuse"),required=True); v.add_argument("--baseline-count",type=int,required=True); v.add_argument("--output",required=True)
args=p.parse_args()
try: do_render(args) if args.cmd=="render" else do_verify(args)
@@ -24,6 +24,8 @@ Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
실제 소비 함수에서도 [runtime consumption 정본](../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 `gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`에서 main-owned push/PR adapter를 호출한다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
## Managed-lock broker gate
Bootstrap 이후 code review는 canonical [Repository cache 운영 지침](../../../../operations/repository-cache.md)의 공통 managed-lock broker 하나를 사용한다. Canonical Issue identity, role, workflow run-id와 TTL로 broker-generated runtime을 요청하고 ready JSON의 `handle`/`socket`을 사용한다. Runtime은 `<issue>/runtime/<workflow-run-id>/`이며 verification run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. Client는 raw token을 inspect/log하지 않으며 public `request` API가 token을 캡슐화한다. Nested repository lease는 같은 handle의 `acquire_repository`/`release_repository` request로만 관리하고 TTL 전에 `renew`한다. 정상·오류·cancel에서 repository release(보유 시) → Issue `release` → broker wait/reap → `probe` `free=true` 확인을 검증하기 전에는 완료·로그·보고하지 않는다. Raw `flock ... sleep`, lock-file unlink와 client-side lock holder는 금지한다. Native publication은 이 broker lease를 사용하며 adapter의 compatibility bridge는 canonical protocol을 우회하지 않는다. Reviewer의 persistent harness/.runtime repository-local storage는 role/workflow runtime 경로와 별개이며 이 예외를 유지한다.
@@ -33,6 +33,25 @@ def fail(message):
raise ValidationError(message)
def require_adoption(declaration, revision):
if declaration is None:
fail('delegation adoption declaration required')
try:
repo_root = Path(__file__).resolve().parents[6]
import sys
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from harness.lib.delegation_adoption import AdoptionError, require_adoption as adopt
except Exception:
fail('delegation adoption unavailable')
try:
return adopt(declaration, expected_source={'kind': 'git_tree', 'revision': revision}, expected_stage='code-review')
except AdoptionError as exc:
fail(str(exc))
except Exception:
fail('delegation adoption unavailable')
def positive_id(value):
if type(value) is not int or value < 1:
fail('invalid positive identity')
@@ -455,10 +474,11 @@ def check_lease(lease, plan):
fail('unsupported lease check result')
def publish(plan, baseline, api, lease, journal_directory):
def publish(plan, baseline, api, lease, journal_directory, *, declaration=None):
"""Use one trusted role transport and an already-held Issue lease; never retry POST."""
import copy
validate_snapshot(plan, baseline)
require_adoption(declaration, plan['target']['head'])
payload = build_payload(plan)
if any(not callable(getattr(api, k, None)) for k in ['identity', 'snapshot', 'create_review', 'read_review']):
fail('unsupported adapter capabilities')
@@ -519,6 +539,7 @@ def publish(plan, baseline, api, lease, journal_directory):
'head': plan['target']['head'], 'event': payload['event'],
'body_sha256': hashlib.sha256(payload['body'].encode()).hexdigest()})
check_lease(lease, plan)
require_adoption(declaration, plan['target']['head'])
uncertain = False
response_id = None
try:
@@ -24,6 +24,8 @@ Baseline/current inventory를 생성·전달·채택할 때 `check-tests 이전`
위임 결과 채택 이전에 `harness/lib/delegation_evidence.py`를 [공통 증빙 채택 정본](../../../../operations/repository-cache.md#delegation-evidence-acceptance-preflight)에 따라 실행한다. main이 독립적으로 고정한 context에 source/bundle digest, 허용 작업, 검증 항목과 독립 실행 catalog를 넣으며 worker 자기 선언으로 context를 만들지 않는다. Source line/named test anchor와 실제 보존된 회귀 테스트·RED/GREEN 기록을 대조하고, bundle 추출 범위 밖 부재 주장은 authoritative Git 조회 없이 채택하지 않는다. Import·환경 오류를 RED로 취급하거나 확인되지 않은 위임 주장을 최종 판정·delivery·publication 근거로 옮기지 않는다. Static-only worker가 기존 독립 실행 근거를 인용하는 것과 직접 실행하는 것을 구분한다. BLOCK/입력 거부이면 해당 증빙 채택을 중단하고 main이 실제 기록과 의미를 재검증한다. Gate는 sandbox, 실행 인증 또는 assertion 의미 판정기가 아니며 기존 compare-source/check-tests와 role의 제품 실행 제한을 대체하지 않는다.
실제 소비 함수에서도 [runtime consumption 정본](../../../../operations/repository-cache.md#delegation-adoption-runtime-consumption)의 `require_adoption`을 통과한다. main은 위임 근거 비사용을 `{"mode":"none"}`, 사용을 `mode=delegated` exact 선언으로 명시하며 누락이나 이전 PASS receipt를 허용하지 않는다. 판정/render, delivery artifact, native publication마다 새 검증을 수행하고 소비 source/stage에 결속한다. 최초 구현 delivery는 `gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, head=final_head, declaration=main_declaration)`에서 main-owned push/PR adapter를 호출한다. Adapter는 push/read_remote_head/create_or_reuse_pr/read_pr를 제공하고 기존 인증·repository/Issue scope·non-force explicit refspec·remote readback 계약을 지킨다. 새 진입점을 쓰지 않는 직접 push/PR 생성은 이 workflow를 우회하는 것으로 금지한다. Gate 밖 직접 Git/API 실행을 sandbox로 차단한다고 주장하지 않는다. 기존 semantic/source/identity/lease 검증과 외부 readback은 그대로 유지한다.
## Managed-lock broker gate
Cache와 Issue workflow의 lock은 canonical [Repository cache 운영 지침](../../../../operations/repository-cache.md)을 따른다. Bootstrap 이후에는 role-neutral broker 하나를 `serve`로 tracked background process로 시작하고 ready JSON을 기다린다. Canonical Issue identity, role, workflow run-id와 TTL로 broker-generated runtime을 요청하고 ready JSON의 `handle`/`socket`을 사용한다. Runtime은 `<issue>/runtime/<workflow-run-id>/`이며 verification run-id와 독립적이다. Runtime 생성은 approval이나 worktree admission을 대신하지 않는다. Cache bootstrap 이후 broker가 Issue lock 아래 runtime을 준비하고, ready 이후 각 workflow의 진입 검증·승인 조건을 확인한다. Client는 raw token을 inspect/log하지 않고 public `request` API를 사용한다. Nested repository lock은 같은 handle의 `acquire_repository`/`release_repository` request만 사용하고 TTL 전에 `renew`한다. 정상·no-op·검증 실패·API 오류·cancel을 포함한 모든 종료 경로는 `release_repository`(보유 시) → `release` → wait/reap → `probe`에서 `free=true` 확인을 거쳐야 하며, 그 전에는 completed/log/report를 주장하지 않는다. Raw `flock ... sleep`과 lock-file unlink는 금지한다.
@@ -211,7 +213,7 @@ Canonical body를 렌더링하기 전에 trusted workflow directory에 JSON obje
```python
terminal(command="python3 harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py --help")
terminal(command="python3 harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py validate-result --result <run>/result.json --allowed-paths <run>/allowed-paths.json")
terminal(command="python3 harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py render --result <run>/result.json --allowed-paths <run>/allowed-paths.json")
terminal(command="python3 harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py render --result <run>/result.json --allowed-paths <run>/allowed-paths.json --delegation-adoption <absolute-main-declaration.json>")
```
- `<run>`은 `<harness-root>/harness/.runtime/implementation-spec-review/<run-id>`의 trusted workflow directory다. `allowed-paths.json`은 main이 authoritative changed-file set과 pinned base/head의 실제 경로에서 만든 JSON string array이며 결과 artifact가 스스로 선언한 allowlist를 신뢰하지 않는다.
@@ -18,6 +18,23 @@ IDENT = re.compile(r"^[^\s/\\\x00<>`=@]+$")
CONTROL = re.compile(r"[\x00-\x1f\x7f-\x9f\u2028\u2029]")
MARKUP = re.compile(r"[`<>\[\]{}]|(?:^|\s)[#*!|]|[a-z][a-z0-9+.-]*://|www\.", re.I)
def _require_adoption(declaration, revision, stage):
if declaration is None:
_fail("delegation adoption declaration required")
try:
repo_root = Path(__file__).resolve().parents[6]
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from harness.lib.delegation_adoption import AdoptionError, require_adoption
except Exception:
_fail("delegation adoption unavailable")
try:
return require_adoption(declaration, expected_source={"kind": "git_tree", "revision": revision}, expected_stage=stage)
except AdoptionError as exc:
_fail(str(exc))
except Exception:
_fail("delegation adoption unavailable")
class ValidationError(ValueError):
pass
@@ -179,7 +196,9 @@ def _marker(r, digest):
return ("<!-- gitea-agent:implementation-spec-review/v1 owner=%s repository=%s issue=%d pr=%d head=%s contract-sha256=%s result-sha256=%s correlation=%s -->" %
(t["owner"],t["repository"],t["issue"],t["pull"],t["head"],r["contract_sha256"],digest,corr))
def render_review(result, allowed_paths, sensitive_values=()):
def render_review(result, allowed_paths, sensitive_values=(), *, declaration=None):
if type(result) is dict and type(result.get("target")) is dict:
_require_adoption(declaration, result["target"].get("head"), "spec-review")
r=validate_result(result, allowed_paths, sensitive_values); t=r["target"]
lines=["## 구현 명세 적합성 검토", "", f"- Source Issue: #{t['issue']}", f"- Pull Request: #{t['pull']}",
f"- Reviewed head: `{t['head']}`", f"- Contract SHA-256: `{r['contract_sha256']}`", f"- Verdict: `{r['verdict']}`", "",
@@ -233,7 +252,7 @@ def parse_review(body, allowed_paths, sensitive_values=()):
except (IndexError, ValueError): _fail("invalid review body")
r={"schema":SCHEMA,"target":{"owner":m.group(1),"repository":m.group(2),"issue":issue,"pull":pull,"head":head},"contract_sha256":contract,"verdict":verdict,"criteria":criteria,"scope":scope,"conclusion":conclusion}
validate_result(r, allowed_paths, sensitive_values)
canonical=render_review(r, allowed_paths, sensitive_values)
canonical=render_review(r, allowed_paths, sensitive_values, declaration={"mode": "none"})
if canonical != body or _sha(canonical[:canonical.rfind("<!--") - 1].encode()) != m.group(7): _fail("review digest mismatch")
if _marker(r,m.group(7)) != marker: _fail("review marker mismatch")
return r
@@ -547,7 +566,7 @@ def verify_publication(before, after, exact_review, result, allowed_paths, revie
if not _same(er, exact_review):
_fail("exact review readback mismatch")
expected = {"approve": "APPROVED", "request_changes": "REQUEST_CHANGES", "blocked_spec": "COMMENT"}[result["verdict"]]
if er.get("body") != render_review(result, allowed_paths) or er.get("commit_id") != target["head"] or er.get("state") != expected:
if er.get("body") != render_review(result, allowed_paths, declaration={"mode": "none"}) or er.get("commit_id") != target["head"] or er.get("state") != expected:
_fail("review binding mismatch")
if _user_id(er) != reviewer_id or er.get("pull_request_url") != url:
_fail("review identity mismatch")
@@ -634,6 +653,10 @@ def main(argv=None):
q=sub.add_parser(name); q.add_argument("--result",required=True); q.add_argument("--allowed-paths",required=True); return q
common("validate-result"); common("render")
q=sub.add_parser("parse"); q.add_argument("--body",required=True); q.add_argument("--allowed-paths",required=True)
render_parser = sub.choices["render"]
adoption = render_parser.add_mutually_exclusive_group(required=True)
adoption.add_argument("--no-delegated-evidence", action="store_true")
adoption.add_argument("--delegation-adoption")
q=sub.add_parser("check-cache"); q.add_argument("--contract-path",required=True); q.add_argument("--owner",required=True); q.add_argument("--repository",required=True); q.add_argument("--issue",required=True,type=int)
q=sub.add_parser("check-pages"); q.add_argument("--pages",required=True); q.add_argument("--key",required=True)
q.add_argument("--page-size", type=int, default=50); q.add_argument("--max-pages", type=int, default=10000)
@@ -644,7 +667,20 @@ def main(argv=None):
q.add_argument("--expected-pr-url", required=True)
try:
a=p.parse_args(argv)
if a.cmd in ("validate-result","render"): r=_json_file(a.result); paths=_json_file(a.allowed_paths); validate_result(r,paths); out=r if a.cmd=="validate-result" else render_review(r,paths); print(json.dumps(out,ensure_ascii=False,sort_keys=True) if a.cmd=="validate-result" else out,end="" if a.cmd=="render" else "\n")
if a.cmd in ("validate-result","render"):
r=_json_file(a.result); paths=_json_file(a.allowed_paths); validate_result(r,paths)
if a.cmd == "validate-result": out=r
else:
if a.no_delegated_evidence:
declaration={"mode":"none"}
else:
repo_root=Path(__file__).resolve().parents[6]
if str(repo_root) not in sys.path:
sys.path.insert(0,str(repo_root))
from harness.lib.delegation_evidence import _json_file as load_declaration
declaration=load_declaration(a.delegation_adoption)
out=render_review(r,paths,declaration=declaration)
print(json.dumps(out,ensure_ascii=False,sort_keys=True) if a.cmd=="validate-result" else out,end="" if a.cmd=="render" else "\n")
elif a.cmd=="parse": print(json.dumps(parse_review(_read(a.body),_json_file(a.allowed_paths)),ensure_ascii=False,sort_keys=True))
elif a.cmd=="check-cache": print(json.dumps(validate_cache_paths(a.contract_path,a.owner,a.repository,a.issue),sort_keys=True))
elif a.cmd=="check-pages":
+102
View File
@@ -0,0 +1,102 @@
"""Fail-closed runtime adoption gate for delegated evidence."""
from __future__ import annotations
import os
import re
from typing import Any, NoReturn
from . import delegation_evidence
class AdoptionError(ValueError):
"""Raised when a delegation declaration cannot be safely adopted."""
_HEX40 = re.compile(r"^[0-9a-f]{40}$")
_SAFE_MESSAGE = "delegation adoption rejected"
def _reject() -> NoReturn:
raise AdoptionError(_SAFE_MESSAGE)
def _canonical_abs(value: Any, *, nullable: bool = False) -> str | None:
if nullable and value is None:
return None
if not isinstance(value, str) or not value or "\x00" in value:
_reject()
if not os.path.isabs(value) or value != os.path.normpath(value):
_reject()
return value
def _nonempty(value: Any) -> str:
if not isinstance(value, str) or not value.strip() or "\x00" in value:
_reject()
return value
def _source(value: Any) -> dict[str, str]:
if not isinstance(value, dict) or value.get("kind") not in ("git_tree", "captured_worktree"):
_reject()
field = "revision" if value["kind"] == "git_tree" else "base_revision"
if set(value) != {"kind", field} or not isinstance(value[field], str) or not _HEX40.fullmatch(value[field]):
_reject()
return value
def _declaration(value: Any) -> dict[str, Any]:
if not isinstance(value, dict) or not isinstance(value.get("mode"), str):
_reject()
if value["mode"] == "none":
if set(value) != {"mode"}:
_reject()
return value
if value["mode"] != "delegated" or set(value) != {
"mode", "bundle_root", "context_path", "result_path", "git_dir"
}:
_reject()
_canonical_abs(value["bundle_root"])
_canonical_abs(value["context_path"])
_canonical_abs(value["result_path"])
_canonical_abs(value["git_dir"], nullable=True)
return value
def require_adoption(
declaration: Any, *, expected_source: Any, expected_stage: Any
) -> dict[str, Any] | None:
"""Require independently expected provenance before consuming delegation evidence.
Lexical path checks are followed by delegation_evidence's descriptor-relative
O_NOFOLLOW reads (including ancestors). This is fresh verification, not an
atomic consume transaction or a sandbox against concurrent source mutation.
"""
try:
expected = _source(expected_source)
stage = _nonempty(expected_stage)
declared = _declaration(declaration)
if declared["mode"] == "none":
return None
context = delegation_evidence._json_file(declared["context_path"])
result = delegation_evidence._json_file(declared["result_path"])
if not isinstance(context, dict) or not isinstance(result, dict):
_reject()
if context.get("source") != expected or context.get("stage") != stage:
_reject()
obligations = context.get("obligations")
claims = result.get("claims")
if not isinstance(obligations, list) or not obligations or not isinstance(claims, list) or not claims:
_reject()
verified = delegation_evidence.verify_evidence(
declared["bundle_root"], context, result, declared["git_dir"]
)
if not isinstance(verified, dict) or verified.get("status") != "pass":
_reject()
return verified
except AdoptionError:
raise
except Exception:
_reject()
+14
View File
@@ -371,6 +371,20 @@ python3 <harness-root>/harness/lib/workflow_guard.py check-tests --input GUARD_I
File/hash binding PASS는 named symbol·parameter 존재나 실제 collection 실행을 인증하지 않는다. `argv`/`evidence`의 형식과 source scope 결속을 확인해도 명령이 실행됐다는 증명은 아니다. Main이 실제 command/output/exit·환경·collection ID/status·범위의 적절성과 manifest 결속을 기존 provenance 규칙으로 별도 확인한다. 필요한 근거가 부족하면 해당 증빙을 채택하지 않으며 현재 collection을 baseline으로 재명명하지 않는다. 기존 revision evidence 재사용은 원래 source에 먼저 결속한 뒤 source-to-head 영향 평가를 유지한다. 별도 checkout이나 전체 테스트 재실행을 요구하지 않는다. Source-binding 실패를 제품 결함이나 자동 `REQUEST_CHANGES`로 전환하지 않는다.
### Delegation adoption runtime consumption
`harness/lib/delegation_adoption.py`의 `require_adoption(declaration, *, expected_source, expected_stage)`는 실제 소비 함수의 진입 gate다. 독립 CLI의 이전 PASS receipt를 제출하는 대신 매 소비 호출에서 안전하게 context/result 파일과 bundle/Git bytes를 다시 읽고 `verify_evidence`를 실행한다. 선언 누락, 검증 실패, BLOCK, 입력 거부, 빈 delegated obligations, source/stage 불일치는 소비 동작 이전에 차단한다. main이 제공하는 `expected_source`와 `expected_stage`를 worker/context에서 역으로 만들지 않는다.
- 위임 근거를 쓰지 않는 경로도 `{"mode":"none"}`를 명시한다. 이는 main의 비사용 선언이지 자동 인증이 아니다. 위임 근거를 사용하면서 none으로 선언하거나 parse-only 내부 primitive로 판정을 우회하는 것은 금지한다. 기존 계약/source/identity/lease 검증은 생략되지 않는다.
- 위임 근거를 사용하는 선언의 exact fields는 `mode`, `bundle_root`, `context_path`, `result_path`, `git_dir`이며 mode는 `delegated`다. 경로는 canonical absolute safe paths이고 `git_dir`는 경로 또는 null이다. Adoption의 lexical 검사 이후 동일 공통 검증기의 `_json_file`/`_directory_fd`/`_open_rel`이 모든 ancestor와 마지막 component를 descriptor-relative `O_NOFOLLOW`로 읽어 symlink를 거부한다. Git 조회는 한 invocation에서 retained FD를 사용한다. 사전 realpath 비교가 실제 안전 읽기를 대신하지 않으며 이 계약은 임의 동시 mutator 아래 byte-stability나 atomic consume을 보장하지 않는다. PASS receipt나 status 필드를 입력으로 받지 않는다.
- Spec 판정 artifact의 `render_review`는 target.head로 expected Git source를 고정하며 stage는 `spec-review`다. Parse/readback의 내부 canonical re-render는 판정 채택이 아니므로 명시적 none을 사용한다.
- Impl 재작업 delivery artifact의 `rework_comment.py render`/`do_render`는 final_head에 고정한 Git source와 `implementation-delivery` stage를 사용한다. 최초 implementation main의 production 진입점은 `harness/agents/impl/skills/gitea-issue-implementation/scripts/implementation_delivery.py`의 `deliver_implementation(adapter, *, head, declaration)`다. 이 workflow는 canonical `delivery_adoption.deliver`를 실제 호출해 fresh gate → authenticated adapter push → exact remote-head readback → fresh gate → create/reuse PR → exact PR number/head/open readback을 순차 수행한다. Gate failure 또는 remote drift이면 이후 write를 중단하고, 외부 write 실패를 자동 재시도하지 않는다. Adapter의 실제 반환값/예외를 그대로 전달하며 callback 호출만으로 delivered/verified 상태를 만들어 내지 않는다. 실제 transport의 push/PR create 및 remote readback은 main-owned adapter의 책임이며, gate 밖 arbitrary Git/API 호출을 sandbox로 막는다는 의미는 아니다. 재작업 comment renderer만으로 최초 delivery 전체가 보호된다고 주장하지 않는다.
- Review `native_review.publish`는 plan.target.head와 `code-review` stage로 시작 시 확인하고, provider/lease readback 후 `create_review` 직전 다시 확인한다. 실패하면 POST를 하지 않으며 intent가 이미 기록된 경우 기존 at-most-once reconciliation 규칙을 유지한다. Evidence 변경을 이유로 자동 POST 재시도하지 않는다.
- Spec/rework의 render 소비 CLI는 `--no-delegated-evidence` 또는 `--delegation-adoption ABS_DECLARATION_JSON` 중 하나를 명시한다. none은 전자, delegated 선언은 후자다. 문서의 예전 render 명령에도 이 선택을 반드시 추가한다. Native publication은 Python publish API이며 CLI publish 명령을 새로 만들거나 존재한다고 주장하지 않는다. Python consumer는 declaration keyword를 명시한다. 증빙 검증 자체 CLI, parse/readback, 기존 source/tests guard는 별개의 read-only 도구다.
- 소비 role adapters의 source는 exact `git_tree`/`revision`이다. Captured worktree의 base_revision을 최종 head의 증명으로 바꾸지 않는다. Common API의 captured_worktree 지원은 독립 expected source가 동일한 다른 소비자의 사용을 위한 것이며 role final-head binding을 완화하지 않는다.
실행 회귀는 실제 소비 함수에 정상/차단/입력 거부/선언 누락/이전 PASS 이후 변경을 넣어 판정 artifact 생성·delivery output write·publication adapter 호출이 차단되는지 검사한다. 이것은 local synthetic integration이며 live Gitea 게시·push 증거가 아니다. Runtime gate 역시 sandbox 또는 capability enforcement가 아니고, caller의 위임 비사용 선언·context 독립성·자연어 판정 의미·OS 권한을 인증하지 않는다. 최종 검증과 소비 사이에 mutator를 병렬 실행하지 않는 기존 규칙은 유지한다.
### Delegation evidence acceptance preflight
위임 결과를 판정·delivery·publication 증빙으로 채택하기 **이전**에 main이 `harness/lib/delegation_evidence.py`를 실행한다. 기존 source-preservation snapshot/compare-source, test inventory source-binding, `check-tests`는 각각 유지한다. 이 gate는 보호 범위 변경 검출이나 collection completeness 검사를 대신하지 않는다.
@@ -0,0 +1,208 @@
import copy
import hashlib
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from harness.lib import delegation_adoption as adoption
class DelegationAdoptionTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch")
self.base = Path(self.tmp.name)
self.bundle = self.base / "bundle"
(self.bundle / "src").mkdir(parents=True)
(self.bundle / "evidence").mkdir()
(self.bundle / "src/value.py").write_bytes(b"VALUE = 1\n")
(self.bundle / "evidence/run.txt").write_bytes(b"run\n")
self.files = {"src/value.py": hashlib.sha256((self.bundle / "src/value.py").read_bytes()).hexdigest()}
self.bundle_sha = self.digest(self.files)
self.context = {
"schema": "gitea-agent.delegation-evidence-context/v1",
"source": {"kind": "captured_worktree", "base_revision": "a" * 40},
"stage": "implementation",
"bundle_sha256": self.bundle_sha,
"files": self.files,
"allowed_actions": ["static"],
"obligations": [{"id": "static", "kind": "static"}],
"executions": [],
}
self.result = {
"schema": "gitea-agent.delegation-evidence-result/v1",
"source": copy.deepcopy(self.context["source"]),
"stage": self.context["stage"],
"bundle_sha256": self.bundle_sha,
"claims": [{"id": "static", "status": "supported", "anchors": [{
"kind": "lines", "path": "src/value.py", "start_line": 1,
"end_line": 1, "text_sha256": hashlib.sha256(b"VALUE = 1\n").hexdigest(),
}], "execution_ids": []}],
}
self.context_path = self.base / "context.json"
self.result_path = self.base / "result.json"
self.context_path.write_text(json.dumps(self.context))
self.result_path.write_text(json.dumps(self.result))
def tearDown(self):
self.tmp.cleanup()
@staticmethod
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
def declaration(self):
return {
"mode": "delegated",
"bundle_root": str(self.bundle),
"context_path": str(self.context_path),
"result_path": str(self.result_path),
"git_dir": None,
}
def test_valid_delegated_declaration_returns_fresh_pass_result(self):
expected_source = copy.deepcopy(self.context["source"])
out = adoption.require_adoption(
self.declaration(), expected_source=expected_source, expected_stage="implementation"
)
self.assertEqual(out["status"], "pass")
self.assertEqual(out["counts"]["obligations"], 1)
def test_valid_git_tree_declaration_uses_expected_revision(self):
repo = self.base / "repo"
repo.mkdir()
(repo / "src").mkdir()
(repo / "src/value.py").write_bytes(b"VALUE = 1\n")
subprocess.run(["/usr/bin/git", "init", "-q", str(repo)], check=True)
subprocess.run(["/usr/bin/git", "-C", str(repo), "config", "user.email", "test@example.invalid"], check=True)
subprocess.run(["/usr/bin/git", "-C", str(repo), "config", "user.name", "Test"], check=True)
subprocess.run(["/usr/bin/git", "-C", str(repo), "add", "."], check=True)
subprocess.run(["/usr/bin/git", "-C", str(repo), "commit", "-q", "-m", "fixture"], check=True)
revision = subprocess.check_output(["/usr/bin/git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip()
context = copy.deepcopy(self.context)
context["source"] = {"kind": "git_tree", "revision": revision}
result = copy.deepcopy(self.result)
result["source"] = copy.deepcopy(context["source"])
self.context_path.write_text(json.dumps(context))
self.result_path.write_text(json.dumps(result))
declaration = self.declaration()
declaration["git_dir"] = str(repo / ".git")
out = adoption.require_adoption(declaration, expected_source=context["source"], expected_stage="implementation")
self.assertEqual(out["status"], "pass")
def test_explicit_none_is_the_only_non_delegated_success(self):
self.assertIsNone(adoption.require_adoption(
{"mode": "none"}, expected_source=self.context["source"], expected_stage="implementation"
))
def test_missing_declaration_is_rejected_with_sanitized_error(self):
with self.assertRaisesRegex(adoption.AdoptionError, "^delegation adoption rejected$"):
adoption.require_adoption(None, expected_source=self.context["source"], expected_stage="implementation")
def test_unsupported_or_extra_declaration_fields_are_rejected(self):
for declaration in (
{"mode": "worker"},
{"mode": "none", "bundle_root": str(self.bundle)},
{**self.declaration(), "extra": 1},
):
with self.subTest(declaration=declaration):
with self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(declaration, expected_source=self.context["source"], expected_stage="implementation")
def test_expected_source_and_stage_are_independent_and_exact(self):
for source, stage in (
({"kind": "captured_worktree", "base_revision": "b" * 40}, "implementation"),
(self.context["source"], "review"),
):
with self.subTest(source=source, stage=stage), self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=source, expected_stage=stage)
def test_stale_context_result_or_bundle_is_rejected(self):
for field, value in (("stage", "review"), ("bundle_sha256", "0" * 64)):
context = copy.deepcopy(self.context)
context[field] = value
self.context_path.write_text(json.dumps(context))
with self.subTest(field=field), self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
self.context_path.write_text(json.dumps(self.context))
self.result_path.write_text(json.dumps({**self.result, "stage": "review"}))
with self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
self.result_path.write_text(json.dumps(self.result))
(self.bundle / "src/value.py").write_bytes(b"VALUE = 2\n")
with self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
def test_empty_obligations_or_claims_cannot_be_adopted(self):
for context, result in (
({**self.context, "obligations": []}, self.result),
(self.context, {**self.result, "claims": []}),
):
self.context_path.write_text(json.dumps(context))
self.result_path.write_text(json.dumps(result))
with self.subTest(context=context, result=result), self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
self.context_path.write_text(json.dumps(self.context))
self.result_path.write_text(json.dumps(self.result))
def test_block_and_verifier_exception_fail_closed(self):
with mock.patch.object(adoption.delegation_evidence, "verify_evidence", return_value={"status": "block"}):
with self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
with mock.patch.object(adoption.delegation_evidence, "verify_evidence", side_effect=RuntimeError("secret")):
with self.assertRaisesRegex(adoption.AdoptionError, "^delegation adoption rejected$"):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
def test_verifier_is_called_again_after_a_prior_pass(self):
with mock.patch.object(adoption.delegation_evidence, "verify_evidence", wraps=adoption.delegation_evidence.verify_evidence) as verifier:
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
self.assertEqual(verifier.call_count, 2)
def test_malformed_expected_inputs_and_paths_fail_closed(self):
cases = (
({"kind": "captured_worktree", "base_revision": "A" * 40}, "implementation"),
(self.context["source"], ""),
(self.context["source"], None),
)
for source, stage in cases:
with self.subTest(source=source, stage=stage), self.assertRaises(adoption.AdoptionError):
adoption.require_adoption({"mode": "none"}, expected_source=source, expected_stage=stage)
for key, value in (("bundle_root", str(self.base / ".." / self.base.name / "bundle")),
("context_path", "relative.json"), ("result_path", str(self.result_path) + "\x00"),
("git_dir", 3)):
declaration = self.declaration()
declaration[key] = value
with self.subTest(key=key), self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(declaration, expected_source=self.context["source"], expected_stage="implementation")
def test_all_git_declaration_paths_reject_symlinks_at_runtime(self):
from harness.tests.common.test_delegation_consumers_integration import RealEvidence
evidence = RealEvidence(self.base / 'safe-path-fixture')
declaration = evidence.declaration()
for field in ('bundle_root', 'context_path', 'result_path', 'git_dir'):
alias = self.base / ('alias-' + field)
alias.symlink_to(declaration[field], target_is_directory=field in ('bundle_root', 'git_dir'))
bad = {**declaration, field: str(alias)}
with self.subTest(field=field), self.assertRaisesRegex(adoption.AdoptionError, '^delegation adoption rejected$'):
adoption.require_adoption(bad, expected_source=evidence.context['source'], expected_stage='spec-review')
alias_parent = self.base / 'alias-parent'
alias_parent.symlink_to(evidence.root, target_is_directory=True)
with self.assertRaises(adoption.AdoptionError):
adoption.require_adoption({**declaration, 'context_path': str(alias_parent / 'context.json')},
expected_source=evidence.context['source'], expected_stage='spec-review')
def test_duplicate_json_and_missing_files_fail_closed(self):
self.context_path.write_text('{"schema":"x","schema":"y"}')
with self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
self.context_path.write_text(json.dumps(self.context))
self.result_path.unlink()
with self.assertRaises(adoption.AdoptionError):
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,34 @@
"""Executable consumer declaration contract wiring."""
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[3]
class DelegationAdoptionContractTests(unittest.TestCase):
def test_common_runtime_contract_is_explicit(self):
text = (ROOT / 'harness/operations/repository-cache.md').read_text()
for token in ('require_adoption', 'expected_source', 'expected_stage',
'--no-delegated-evidence', '--delegation-adoption',
'spec-review', 'code-review', 'implementation-delivery',
'PASS receipt', 'sandbox'):
with self.subTest(token=token):
self.assertIn(token, text)
def test_each_consumer_skill_requires_runtime_declaration(self):
paths = (
'spec/skills/gitea-implementation-spec-review',
'impl/skills/gitea-issue-implementation',
'impl/skills/gitea-issue-rework',
'review/skills/gitea-code-review',
)
for path in paths:
text = (ROOT / 'harness/agents' / path / 'SKILL.md').read_text()
with self.subTest(path=path):
self.assertIn('require_adoption', text)
self.assertIn('mode', text)
self.assertIn('none', text)
self.assertIn('delegated', text)
self.assertIn('delegation-adoption-runtime-consumption', text)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,46 @@
import importlib.util
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[3]
PATH = ROOT / "harness/agents/impl/skills/gitea-issue-implementation/scripts/delivery_adoption.py"
spec = importlib.util.spec_from_file_location("delivery_adoption", PATH)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
class DeliveryConsumerTests(unittest.TestCase):
def test_delivery_requires_adoption_then_calls_adapter_once(self):
calls = []
with patch.object(mod, "require_adoption", return_value={"status": "pass"}) as adopted:
result = mod.deliver(lambda: {"adapter": "actual"}, head="a" * 40,
declaration={"mode": "delegated"})
adopted.assert_called_once_with({"mode": "delegated"}, expected_source={"kind": "git_tree", "revision": "a" * 40}, expected_stage="implementation-delivery")
self.assertEqual(result, {"adapter": "actual"})
def test_rejected_adoption_blocks_adapter(self):
calls = []
with patch.object(mod, "require_adoption", side_effect=mod.AdoptionError("no")):
with self.assertRaises(mod.AdoptionError):
mod.deliver(lambda: calls.append("called"), head="a" * 40,
declaration={"mode": "delegated"})
self.assertEqual(calls, [])
def test_missing_declaration_blocks_adapter(self):
calls = []
with self.assertRaises(mod.AdoptionError):
mod.deliver(lambda: calls.append("called"), head="a" * 40, declaration=None)
self.assertEqual(calls, [])
def test_explicit_none_still_validates_head_and_calls_adapter(self):
calls = []
with patch.object(mod, "require_adoption", return_value={"status": "none"}):
with self.assertRaises(mod.AdoptionError):
mod.deliver(lambda: calls.append("called"), head="bad", declaration={"mode": "none"})
mod.deliver(lambda: calls.append("called"), head="a" * 40, declaration={"mode": "none"})
self.assertEqual(calls, ["called"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,298 @@
import copy
import hashlib
import importlib.util
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRATCH = Path('/opt/data/cache/scratch/delegation-adoption-integration')
SPEC = ROOT / 'harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py'
REWORK = ROOT / 'harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py'
DELIVERY = ROOT / 'harness/agents/impl/skills/gitea-issue-implementation/scripts/delivery_adoption.py'
NATIVE = ROOT / 'harness/agents/review/skills/gitea-code-review/scripts/native_review.py'
def load(path, name):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
support = load(SPEC, 'integration_spec_support')
delivery = load(DELIVERY, 'integration_delivery')
native = load(NATIVE, 'integration_native')
rework = load(REWORK, 'integration_rework')
class RealEvidence:
def __init__(self, root):
self.root = root
self.bundle = root / 'bundle'
(self.bundle / 'src').mkdir(parents=True)
(self.bundle / 'tests').mkdir()
(self.bundle / 'evidence').mkdir()
(self.bundle / 'src/a.py').write_text('VALUE = 1\n')
(self.bundle / 'tests/test_a.py').write_text('def test_value():\n assert 1 == 1\n')
(self.bundle / 'evidence/red.txt').write_text('red\n')
(self.bundle / 'evidence/green.txt').write_text('green\n')
subprocess.run(['/usr/bin/git', 'init', '-q', str(self.root / 'repo')], check=True)
repo = self.root / 'repo'
(repo / 'src').mkdir(); (repo / 'tests').mkdir()
(repo / 'src/a.py').write_text('VALUE = 1\n')
(repo / 'tests/test_a.py').write_text('def test_value():\n assert 1 == 1\n')
subprocess.run(['/usr/bin/git', '-C', str(repo), 'config', 'user.email', 'test@example.invalid'], check=True)
subprocess.run(['/usr/bin/git', '-C', str(repo), 'config', 'user.name', 'Integration'], check=True)
subprocess.run(['/usr/bin/git', '-C', str(repo), 'add', '.'], check=True)
subprocess.run(['/usr/bin/git', '-C', str(repo), 'commit', '-q', '-m', 'fixture'], check=True)
self.revision = subprocess.check_output(['/usr/bin/git', '-C', str(repo), 'rev-parse', 'HEAD'], text=True).strip()
self.git_dir = repo / '.git'
self.files = {p: hashlib.sha256((self.bundle / p).read_bytes()).hexdigest() for p in ('src/a.py', 'tests/test_a.py')}
self.bundle_sha = self.digest(self.files)
self.context = {'schema': 'gitea-agent.delegation-evidence-context/v1',
'source': {'kind': 'git_tree', 'revision': self.revision},
'stage': 'spec-review', 'bundle_sha256': self.bundle_sha,
'files': self.files, 'allowed_actions': ['static', 'execute'],
'obligations': [{'id': 'static', 'kind': 'static'}, {'id': 'rg', 'kind': 'red_green'}],
'executions': [self.execution('red', 1, 'assertion', 'evidence/red.txt'),
self.execution('green', 0, 'none', 'evidence/green.txt')]}
line_hash = hashlib.sha256(b'VALUE = 1\n').hexdigest()
self.result = {'schema': 'gitea-agent.delegation-evidence-result/v1',
'source': copy.deepcopy(self.context['source']), 'stage': 'spec-review',
'bundle_sha256': self.bundle_sha,
'claims': [{'id': 'static', 'status': 'supported', 'anchors': [{'kind': 'lines', 'path': 'src/a.py', 'start_line': 1, 'end_line': 1, 'text_sha256': line_hash}], 'execution_ids': []},
{'id': 'rg', 'status': 'supported', 'anchors': [{'kind': 'test', 'path': 'tests/test_a.py', 'symbol': 'test_value'}], 'execution_ids': ['red', 'green']}]}
self.context_path = root / 'context.json'; self.result_path = root / 'result.json'
self.write()
@staticmethod
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
def execution(self, eid, code, kind, path):
return {'id': eid, 'test_id': 'tests/test_a.py::test_value', 'bundle_sha256': self.bundle_sha,
'test_file_sha256': self.files['tests/test_a.py'], 'argv': ['python', '-m', 'pytest'],
'exit_code': code, 'failure_kind': kind, 'evidence_path': path,
'evidence_sha256': hashlib.sha256((self.bundle / path).read_bytes()).hexdigest(), 'origin': 'direct'}
def write(self):
self.context_path.write_text(json.dumps(self.context))
self.result_path.write_text(json.dumps(self.result))
def declaration(self):
return {'mode': 'delegated', 'bundle_root': str(self.bundle), 'context_path': str(self.context_path),
'result_path': str(self.result_path), 'git_dir': str(self.git_dir)}
def set_stage(self, stage):
self.context['stage'] = stage; self.result['stage'] = stage; self.write()
class IntegrationTests(unittest.TestCase):
def setUp(self):
SCRATCH.mkdir(parents=True, exist_ok=True)
self.tmp = tempfile.TemporaryDirectory(dir=SCRATCH)
self.ev = RealEvidence(Path(self.tmp.name))
def tearDown(self): self.tmp.cleanup()
def test_spec_real_git_pass_roundtrip_and_blocks_drift(self):
self.ev.set_stage('spec-review')
result = {'schema': support.SCHEMA, 'target': {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pull': 7, 'head': self.ev.revision},
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'verdict': 'approve',
'criteria': [{'criterion': 'REQ-1', 'contract': 'contract', 'implementation_paths': ['src/a.py'], 'test_paths': ['tests/test_a.py'], 'evidence': 'verified', 'result': 'satisfied', 'reason': 'ok'}],
'scope': {'missing': [], 'excess': [], 'spec_conflicts': []}, 'conclusion': 'ok'}
body = support.render_review(result, {'src/a.py', 'tests/test_a.py'}, declaration=self.ev.declaration())
self.assertEqual(support.parse_review(body, {'src/a.py', 'tests/test_a.py'}), result)
self.ev.set_stage('other'); self.ev.write()
with self.assertRaises(support.ValidationError): support.render_review(result, {'src/a.py', 'tests/test_a.py'}, declaration=self.ev.declaration())
def test_spec_cli_delegated_import_without_pythonpath_and_block_is_quiet(self):
self.ev.set_stage('spec-review')
result = {'schema': support.SCHEMA, 'target': {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pull': 7, 'head': self.ev.revision},
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'verdict': 'approve',
'criteria': [{'criterion': 'REQ-1', 'contract': 'contract', 'implementation_paths': ['src/a.py'], 'test_paths': [], 'evidence': 'verified', 'result': 'satisfied', 'reason': 'ok'}],
'scope': {'missing': [], 'excess': [], 'spec_conflicts': []}, 'conclusion': 'ok'}
result_path = Path(self.tmp.name) / 'spec-result.json'; paths_path = Path(self.tmp.name) / 'paths.json'; decl_path = Path(self.tmp.name) / 'decl.json'; result_path.write_text(json.dumps(result)); paths_path.write_text(json.dumps(['src/a.py'])); decl_path.write_text(json.dumps(self.ev.declaration()))
env = os.environ.copy(); env.pop('PYTHONPATH', None)
cmd = [sys.executable, str(SPEC), 'render', '--result', str(result_path), '--allowed-paths', str(paths_path), '--delegation-adoption', str(decl_path)]
run = subprocess.run(cmd, cwd=self.tmp.name, env=env, text=True, capture_output=True)
self.assertEqual(run.returncode, 0, run.stderr); self.assertIn('implementation-spec-review/v1', run.stdout)
self.ev.context['stage'] = 'drift'; self.ev.write()
blocked = subprocess.run(cmd, cwd=self.tmp.name, env=env, text=True, capture_output=True)
self.assertNotEqual(blocked.returncode, 0); self.assertEqual(blocked.stdout, '')
def test_delivery_real_pass_returns_adapter_and_blocks_without_write(self):
self.ev.set_stage('implementation-delivery')
calls = []
out = delivery.deliver(lambda: {'receipt': 'actual'}, head=self.ev.revision, declaration=self.ev.declaration())
self.assertEqual(out, {'receipt': 'actual'})
self.ev.bundle.joinpath('src/a.py').write_text('VALUE = 2\n')
with self.assertRaises(delivery.AdoptionError): delivery.deliver(lambda: calls.append(1), head=self.ev.revision, declaration=self.ev.declaration())
self.assertEqual(calls, [])
def bound_publication_fixture(self):
from harness.tests.review import test_native_review as fixture
plan, before, after, exact = fixture.publication_fixture()
old_head = plan['target']['head']
def replace(value):
if isinstance(value, str): return self.ev.revision if value == old_head else value
if isinstance(value, list): return [replace(x) for x in value]
if isinstance(value, dict): return {k: replace(v) for k, v in value.items()}
return value
plan, before, after, exact = map(replace, (plan, before, after, exact))
plan['body'] = native.marker(plan) + '\n- Verdict: `approve`\n검토 완료.\n'
exact['body'] = plan['body']
for review in before['reviews'] + after['reviews']:
if review['id'] == plan['spec_review_id']:
original = fixture.publication_fixture()[1]['reviews'][1]['body']
sr = support.parse_review(original, {'src/a.py'})
sr['target']['head'] = self.ev.revision
review['body'] = support.render_review(sr, {'src/a.py'}, declaration={'mode': 'none'})
review['commit_id'] = self.ev.revision
elif review['id'] == 10:
review['body'] = plan['body']
exact['commit_id'] = self.ev.revision
native.validate_snapshot(plan, before)
return fixture, plan, before, after, exact
def test_review_real_git_adoption_posts_once_then_noops(self):
self.ev.set_stage('code-review')
fixture, plan, before, after, exact = self.bound_publication_fixture()
api = fixture.FakeAPI(plan, before, after, exact)
with tempfile.TemporaryDirectory(dir=self.tmp.name) as journal:
first = native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
second = native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
self.assertEqual(first['mode'], 'created')
self.assertEqual(second['mode'], 'no_op')
self.assertEqual(api.posts, 1)
def test_review_fresh_adoption_blocks_post_if_bundle_changes_before_create(self):
self.ev.set_stage('code-review')
fixture, plan, before, after, exact = self.bound_publication_fixture()
api = fixture.FakeAPI(plan, before, after, exact)
original_snapshot = api.snapshot
calls = {'n': 0}
def mutating_snapshot(target):
calls['n'] += 1
out = original_snapshot(target)
if calls['n'] == 2:
self.ev.bundle.joinpath('src/a.py').write_text('VALUE = 9\n')
return out
api.snapshot = mutating_snapshot
with tempfile.TemporaryDirectory(dir=self.tmp.name) as journal:
with self.assertRaisesRegex(native.ValidationError, 'delegation adoption'):
native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
self.assertEqual(calls['n'], 2, 'must reach both provider reads and final adoption check')
self.assertTrue((Path(journal) / 'intent.json').exists())
self.assertEqual(api.posts, 0)
# Conservatively preserve durable intent: restoring evidence cannot trigger POST.
self.ev.bundle.joinpath('src/a.py').write_text('VALUE = 1\n')
with self.assertRaises(native.UncertainPublication):
native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
self.assertEqual(api.posts, 0)
def spec_result(self):
return {'schema': support.SCHEMA,
'target': {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pull': 7, 'head': self.ev.revision},
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'verdict': 'approve',
'criteria': [{'criterion': 'REQ-1', 'contract': 'contract', 'implementation_paths': ['src/a.py'],
'test_paths': [], 'evidence': 'verified', 'result': 'satisfied', 'reason': 'ok'}],
'scope': {'missing': [], 'excess': [], 'spec_conflicts': []}, 'conclusion': 'ok'}
def rework_args(self, declaration):
from types import SimpleNamespace
root = Path(self.tmp.name)
data = {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pr': 7, 'review': 9,
'reviewed_head': self.ev.revision, 'final_head': self.ev.revision,
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'result': 'rework_delivered',
'findings': [{'id': 'F-1', 'status': 'resolved', 'classification': 'valid_fix',
'changes': 'fixed', 'verification': 'passed', 'reason': 'ok'}],
'local_checks': 'passed', 'runtime_browser': 'passed', 'commit_status': 'success', 'actions_runs': '없음'}
(root / 'input.json').write_text(json.dumps(data))
path = root / 'declaration.json'
if declaration is not None:
path.write_text(json.dumps(declaration))
return SimpleNamespace(input=str(root / 'input.json'), comment=str(root / 'denied.md'),
expected=str(root / 'denied.json'), no_delegated_evidence=False,
delegation_adoption=str(path) if declaration is not None else None)
def test_all_consumers_reject_missing_malformed_and_indeterminate_without_effects(self):
for mode in ('missing', 'malformed', 'indeterminate'):
self.ev.result['claims'][0]['status'] = 'indeterminate' if mode == 'indeterminate' else 'supported'
self.ev.write()
declaration = None if mode == 'missing' else {'mode': 'bad'} if mode == 'malformed' else self.ev.declaration()
with self.subTest(mode=mode, consumer='spec'):
self.ev.set_stage('spec-review')
with self.assertRaises(support.ValidationError):
support.render_review(self.spec_result(), {'src/a.py'}, declaration=declaration)
self.ev.set_stage('implementation-delivery')
calls = []
with self.subTest(mode=mode, consumer='delivery'):
with self.assertRaises(delivery.AdoptionError):
delivery.deliver(lambda: calls.append(1), head=self.ev.revision, declaration=declaration)
self.assertEqual(calls, [])
with self.subTest(mode=mode, consumer='rework'):
args = self.rework_args(declaration)
with self.assertRaises(Exception):
rework.do_render(args)
self.assertFalse(Path(args.comment).exists())
self.assertFalse(Path(args.expected).exists())
self.ev.set_stage('code-review')
fixture, plan, before, after, exact = self.bound_publication_fixture()
api = fixture.FakeAPI(plan, before, after, exact)
journal = Path(self.tmp.name) / ('denied-journal-' + mode)
with self.subTest(mode=mode, consumer='review'):
with self.assertRaises(native.ValidationError):
native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=declaration)
self.assertEqual(api.posts, 0)
self.assertEqual(api.events, [])
self.assertFalse(journal.exists())
def test_rework_explicit_none_does_not_bypass_checker_failure(self):
from unittest.mock import patch
from harness.lib.delegation_adoption import AdoptionError
args = self.rework_args(None)
args.no_delegated_evidence = True
with patch('harness.lib.delegation_adoption.require_adoption', side_effect=AdoptionError('rejected')):
try:
rework.do_render(args)
except rework.Bad:
pass
else:
self.fail('explicit none must still invoke common checker')
self.assertFalse(Path(args.comment).exists())
self.assertFalse(Path(args.expected).exists())
def test_spec_cli_rejects_symlink_declaration(self):
self.ev.set_stage('spec-review')
root = Path(self.tmp.name)
(root / 'spec.json').write_text(json.dumps(self.spec_result()))
(root / 'paths.json').write_text(json.dumps(['src/a.py']))
(root / 'real-decl.json').write_text(json.dumps(self.ev.declaration()))
(root / 'alias.json').symlink_to(root / 'real-decl.json')
env = os.environ.copy(); env.pop('PYTHONPATH', None)
cmd = [sys.executable, str(SPEC), 'render', '--result', str(root / 'spec.json'),
'--allowed-paths', str(root / 'paths.json'), '--delegation-adoption', str(root / 'alias.json')]
proc = subprocess.run(cmd, cwd=root, env=env, text=True, capture_output=True, timeout=10)
self.assertNotEqual(proc.returncode, 0)
self.assertEqual(proc.stdout, '')
def test_cli_import_and_safe_declaration_reader_without_pythonpath(self):
self.ev.set_stage('implementation-delivery')
inp = Path(self.tmp.name) / 'rework.json'; comment = Path(self.tmp.name) / 'comment.md'; expected = Path(self.tmp.name) / 'expected.json'
data = {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pr': 7, 'review': 9, 'reviewed_head': self.ev.revision, 'final_head': self.ev.revision,
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'result': 'rework_delivered',
'findings': [{'id': 'F-1', 'status': 'resolved', 'classification': 'valid_fix', 'changes': 'fixed', 'verification': 'passed', 'reason': 'ok'}],
'local_checks': 'passed', 'runtime_browser': 'passed', 'commit_status': 'success', 'actions_runs': '없음'}
inp.write_text(json.dumps(data)); decl = Path(self.tmp.name) / 'decl.json'; decl.write_text(json.dumps(self.ev.declaration()))
env = os.environ.copy(); env.pop('PYTHONPATH', None)
cmd = [sys.executable, str(REWORK), 'render', '--input', str(inp), '--comment', str(comment), '--expected', str(expected), '--delegation-adoption', str(decl)]
run = subprocess.run(cmd, cwd=self.tmp.name, env=env, text=True, capture_output=True)
self.assertEqual(run.returncode, 0, run.stderr)
self.assertTrue(comment.exists() and expected.exists())
os.symlink(self.ev.result_path, Path(self.tmp.name) / 'evil.json')
bad = subprocess.run([sys.executable, str(REWORK), 'render', '--input', str(inp), '--comment', str(comment), '--expected', str(expected), '--delegation-adoption', str(self.tmp.name) + '/evil.json'], cwd=self.tmp.name, env=env, text=True, capture_output=True)
self.assertNotEqual(bad.returncode, 0); self.assertEqual(bad.stdout, '')
if __name__ == '__main__': unittest.main()
@@ -0,0 +1,150 @@
import importlib.util
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
ROOT = Path(__file__).resolve().parents[3]
PATH = ROOT / "harness/agents/impl/skills/gitea-issue-implementation/scripts/implementation_delivery.py"
INTEGRATION = ROOT / "harness/tests/common/test_delegation_consumers_integration.py"
def load(path, name):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
class RecordingAdapter:
def __init__(self, head, *, remote_head=None, pr=None, mutate=None):
self.head = head
self.remote_head = remote_head if remote_head is not None else head
self.pr = pr if pr is not None else {"number": 17, "head": head, "state": "open"}
self.mutate = mutate
self.calls = []
def push(self, head):
self.calls.append(("push", head))
if self.mutate:
self.mutate("push")
def read_remote_head(self):
self.calls.append(("read_remote_head",))
if self.mutate:
self.mutate("read_remote_head")
return self.remote_head
def create_or_reuse_pr(self, head):
self.calls.append(("create_or_reuse_pr", head))
if self.mutate:
self.mutate("create_or_reuse_pr")
return self.pr["number"]
def read_pr(self, number):
self.calls.append(("read_pr", number))
return self.pr
class ImplementationDeliveryTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.delivery = load(PATH, "implementation_delivery_under_test")
cls.evidence = load(INTEGRATION, "implementation_delivery_evidence")
def setUp(self):
self.tmp = TemporaryDirectory(dir="/opt/data/cache/scratch")
self.ev = self.evidence.RealEvidence(Path(self.tmp.name))
self.ev.set_stage("implementation-delivery")
def tearDown(self):
self.tmp.cleanup()
def test_real_git_delivery_pushes_once_creates_once_and_returns_verified_readback(self):
adapter = RecordingAdapter(self.ev.revision)
result = self.delivery.deliver_implementation(
adapter, head=self.ev.revision, declaration=self.ev.declaration()
)
self.assertEqual(result, {
"remote_head": self.ev.revision,
"pr": {"number": 17, "head": self.ev.revision, "state": "open"},
})
self.assertEqual(
[call[0] for call in adapter.calls],
["push", "read_remote_head", "create_or_reuse_pr", "read_pr"],
)
def test_explicit_none_is_checked_and_exact_readback_is_required(self):
adapter = RecordingAdapter(self.ev.revision)
result = self.delivery.deliver_implementation(
adapter, head=self.ev.revision, declaration={"mode": "none"}
)
self.assertEqual(result["remote_head"], self.ev.revision)
self.assertEqual(result["pr"]["state"], "open")
def test_missing_or_malformed_declaration_has_no_effects(self):
for declaration in (None, {"mode": "bad"}):
adapter = RecordingAdapter(self.ev.revision)
with self.subTest(declaration=declaration), self.assertRaises(self.delivery.AdoptionError):
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=declaration)
self.assertEqual(adapter.calls, [])
def test_remote_drift_prevents_pr_creation(self):
adapter = RecordingAdapter(self.ev.revision, remote_head="0" * 40)
with self.assertRaises(self.delivery.AdoptionError):
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
self.assertEqual([call[0] for call in adapter.calls], ["push", "read_remote_head"])
def test_evidence_mutation_during_remote_readback_prevents_pr_creation(self):
def mutate(phase):
if phase == "read_remote_head":
self.ev.bundle.joinpath("src/a.py").write_text("VALUE = 2\n")
adapter = RecordingAdapter(self.ev.revision, mutate=mutate)
with self.assertRaises(self.delivery.AdoptionError):
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
self.assertEqual([call[0] for call in adapter.calls], ["push", "read_remote_head"])
def test_missing_adapter_capability_prevents_push(self):
class Incomplete:
def read_remote_head(self):
return self.ev.revision
with self.assertRaises(self.delivery.AdoptionError):
self.delivery.deliver_implementation(Incomplete(), head=self.ev.revision, declaration={"mode": "none"})
def test_pr_readback_mismatch_does_not_complete(self):
for pr in (
{"number": 17, "head": "0" * 40, "state": "open"},
{"number": 17, "head": self.ev.revision, "state": "closed"},
{"number": True, "head": self.ev.revision, "state": "open"},
):
adapter = RecordingAdapter(self.ev.revision, pr=pr)
with self.subTest(pr=pr), self.assertRaises(self.delivery.AdoptionError):
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
def test_workflow_can_load_from_external_cwd_without_pythonpath(self):
import os
import subprocess
code = "import importlib.util,json; s=importlib.util.spec_from_file_location('delivery'," + repr(str(PATH)) + "); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); head='a'*40; A=type('A',(),{'push':lambda self,h:None,'read_remote_head':lambda self:head,'create_or_reuse_pr':lambda self,h:17,'read_pr':lambda self,n:{'number':n,'head':head,'state':'open'}}); print(json.dumps(m.deliver_implementation(A(),head=head,declaration={'mode':'none'})))"
env = os.environ.copy(); env.pop('PYTHONPATH', None)
proc = subprocess.run([sys.executable, '-c', code], cwd=self.tmp.name, env=env,
capture_output=True, text=True, timeout=10)
self.assertEqual(proc.returncode, 0, 'canonical delivery must load independently of cwd')
self.assertIn('remote_head', proc.stdout)
def test_uncertain_write_is_not_retried(self):
class FailingAdapter(RecordingAdapter):
def create_or_reuse_pr(self, head):
self.calls.append(("create_or_reuse_pr", head))
raise RuntimeError("transport uncertain")
adapter = FailingAdapter(self.ev.revision)
with self.assertRaises(RuntimeError):
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
self.assertEqual([call[0] for call in adapter.calls], ["push", "read_remote_head", "create_or_reuse_pr"])
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -11,7 +11,7 @@ class ReworkCommentTests(unittest.TestCase):
"result":"partial","findings":[{"id":"F-1","status":"resolved","classification":"valid_fix","changes":"src/main.py","verification":"pytest -q (0), pass","reason":"bounded fix"}],
"local_checks":"pytest -q (0), pass","runtime_browser":"해당 없음·미검증","commit_status":"success","actions_runs":"없음"}
self.write("input.json", self.data)
self.invoke("render", "--input", self.p("input.json"), "--comment", self.p("comment.md"), "--expected", self.p("expected.json"))
self.invoke("render", "--no-delegated-evidence", "--input", self.p("input.json"), "--comment", self.p("comment.md"), "--expected", self.p("expected.json"))
self.expected = json.loads(self.d.joinpath("expected.json").read_text())
def tearDown(self): self.t.cleanup()
@@ -47,7 +47,7 @@ class ReworkCommentTests(unittest.TestCase):
self.setUp_render_again(); self.readback(); c=self.read("comments.json"); c["comments"].append(dict(c["comments"][0],id=502)); c["total_count"]=2; self.write("comments.json",c); self.assertNotEqual(self.verify("reuse",1).returncode,0)
def setUp_render_again(self):
self.invoke("render","--input",self.p("input.json"),"--comment",self.p("comment.md"),"--expected",self.p("expected.json")); self.expected=self.read("expected.json")
self.invoke("render","--no-delegated-evidence","--input",self.p("input.json"),"--comment",self.p("comment.md"),"--expected",self.p("expected.json")); self.expected=self.read("expected.json")
def test_create_reuse_cardinality(self):
self.readback(); self.assertEqual(self.verify("create",0).returncode,0); self.assertEqual(self.read("receipt.json")["status"],"verified")
original=self.read("comments.json")["comments"][0]
@@ -59,7 +59,7 @@ class ReworkCommentTests(unittest.TestCase):
def test_unresolved_delivery_and_incomplete_or_wrong_scope_readback_fail(self):
bad=dict(self.data); bad["result"]="rework_delivered"; bad["findings"]=[dict(self.data["findings"][0],status="unresolved")]
self.write("bad-input.json",bad)
self.assertNotEqual(self.invoke("render","--input",self.p("bad-input.json"),"--comment",self.p("bad.md"),"--expected",self.p("bad.json")).returncode,0)
self.assertNotEqual(self.invoke("render","--no-delegated-evidence","--input",self.p("bad-input.json"),"--comment",self.p("bad.md"),"--expected",self.p("bad.json")).returncode,0)
self.readback(); comments=self.read("comments.json"); comments["complete"]=False; self.write("comments.json",comments); self.assertNotEqual(self.verify().returncode,0)
self.readback(); pr=self.read("pr.json"); pr["repository"]="other"; self.write("pr.json",pr); self.assertNotEqual(self.verify().returncode,0)
+20 -20
View File
@@ -55,7 +55,7 @@ def publication_fixture():
'official': True, 'dismissed': False, 'stale': head != t['head'],
'submitted_at': old_time, 'updated_at': old_time, 'comments_count': 0,
'pull_request_url': url}
sr = review(9, p['spec_reviewer'], support.render_review(spec_result, {'src/a.py'}),
sr = review(9, p['spec_reviewer'], support.render_review(spec_result, {'src/a.py'}, declaration={'mode': 'none'}),
t['head'], 'APPROVED')
previous = review(8, p['reviewer'], 'old review body', 'd' * 40, 'REQUEST_CHANGES')
s = {'identity': copy.deepcopy(p['reviewer']), 'spec_user': copy.deepcopy(p['spec_reviewer']),
@@ -159,22 +159,22 @@ class NativeReviewTests(unittest.TestCase):
p, before, after, exact = publication_fixture()
api = FakeAPI(p, before, after, exact); lease = FakeLease()
with tempfile.TemporaryDirectory() as td:
result = m.publish(p, before, api, lease, td)
result = m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
self.assertEqual(result['mode'], 'created')
self.assertEqual(api.posts, 1)
self.assertEqual(m.publish(p, before, api, lease, td)['mode'], 'no_op')
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'no_op')
self.assertEqual(api.posts, 1)
self.assertNotIn('body', json.loads((Path(td) / 'intent.json').read_text()))
for applies in [True, False]:
api = FakeAPI(p, before, after, exact); api.timeout = True; api.apply = applies
with self.subTest(applies=applies), tempfile.TemporaryDirectory() as td:
if applies:
self.assertEqual(m.publish(p, before, api, lease, td)['mode'], 'reconciled')
self.assertEqual(m.publish(p, before, api, lease, td)['mode'], 'no_op')
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'reconciled')
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'no_op')
else:
for _ in range(2):
with self.assertRaises(m.UncertainPublication) as error:
m.publish(p, before, api, lease, td)
m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
self.assertNotIn('secret transport detail', str(error.exception))
self.assertEqual(api.posts, 1)
def test_api_adapter_checks_identity_first_and_collects_all_pages(self):
@@ -296,7 +296,7 @@ class NativeReviewTests(unittest.TestCase):
p['spec_paths'] = ['src/a.py', 'tests/existing.py']
spec_result = support.parse_review(before['reviews'][1]['body'], {'src/a.py'})
spec_result['criteria'][0]['test_paths'] = ['tests/existing.py']
before['reviews'][1]['body'] = support.render_review(spec_result, set(p['spec_paths']))
before['reviews'][1]['body'] = support.render_review(spec_result, set(p['spec_paths']), declaration={'mode': 'none'})
try:
m.validate_snapshot(p, before)
except m.ValidationError:
@@ -333,7 +333,7 @@ class NativeReviewTests(unittest.TestCase):
p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact); lease = FakeLease()
if kind == 'expired': lease.valid = False
else: api.identity = lambda: p['spec_reviewer']
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td)
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
self.assertEqual(api.posts, 0)
self.assertFalse((Path(td) / 'intent.json').exists())
@@ -348,7 +348,7 @@ class NativeReviewTests(unittest.TestCase):
with self.subTest(case=i), tempfile.TemporaryDirectory() as td:
p, before, after, exact = publication_fixture(); api = FakeAPI(p, copy.deepcopy(before), after, exact)
change(api.before)
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td)
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertEqual(api.posts, 0)
def test_pagination_total_duplicate_and_truncation_are_hard_errors(self):
@@ -424,7 +424,7 @@ class NativeReviewTests(unittest.TestCase):
m = self.module(); p, before, after, exact = publication_fixture()
api = FakeAPI(p, before, after, exact); api.created = True
with tempfile.TemporaryDirectory() as td:
result = m.publish(p, after, api, FakeLease(), td)
result = m.publish(p, after, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertEqual(result['mode'], 'no_op'); self.assertFalse(result['wrote'])
self.assertEqual(api.posts, 0)
self.assertFalse((Path(td) / 'intent.json').exists())
@@ -441,7 +441,7 @@ class NativeReviewTests(unittest.TestCase):
after['pagination']['reviews'][0].update(count=4, declared_total=4)
after['pagination']['review_comments_11'] = [{'page': 1, 'count': 0, 'declared_total': 0, 'link': None}]
api = FakeAPI(p, before, after, exact); api.created = True
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td)
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertEqual(api.posts, 0)
def test_readback_failure_and_changed_journal_plan_never_retry_post(self):
@@ -449,11 +449,11 @@ class NativeReviewTests(unittest.TestCase):
api.read_review = lambda target, rid: (_ for _ in ()).throw(TimeoutError('DO_NOT_ECHO_TRANSPORT_SECRET'))
with tempfile.TemporaryDirectory() as td:
for _ in range(2):
with self.assertRaises(Exception) as raised: m.publish(p, before, api, FakeLease(), td)
with self.assertRaises(Exception) as raised: m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertNotIn('DO_NOT_ECHO_TRANSPORT_SECRET', str(raised.exception))
self.assertEqual(api.posts, 1)
different = copy.deepcopy(p); different['body'] += 'different'
with self.assertRaises(m.ValidationError): m.publish(different, before, api, FakeLease(), td)
with self.assertRaises(m.ValidationError): m.publish(different, before, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertEqual(api.posts, 1)
def test_journal_symlinks_hardlinks_and_changed_baseline_are_rejected(self):
@@ -463,10 +463,10 @@ class NativeReviewTests(unittest.TestCase):
with self.assertRaises(m.ValidationError): m.Journal(link)
api = FakeAPI(p, before, after, exact); api.timeout = True; api.apply = False
journal = root / 'journal'
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), journal)
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), journal, declaration={'mode': 'none'})
saved = json.loads((journal / 'before.json').read_text()); saved['issue']['body'] = 'changed'
(journal / 'before.json').write_text(json.dumps(saved))
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), journal)
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), journal, declaration={'mode': 'none'})
self.assertEqual(api.posts, 1)
with m.Journal(root / 'hardlinks') as j:
j.write('a.json', {'safe': True}); os.link(j.path / 'a.json', j.path / 'b.json')
@@ -485,7 +485,7 @@ class NativeReviewTests(unittest.TestCase):
m = self.module(); p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact)
lease = FakeLease(); lease.check = lambda plan: False
with tempfile.TemporaryDirectory() as td:
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td)
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
self.assertEqual(api.posts, 0)
def test_live_rest_bridge_over_loopback_publishes_once_then_no_ops(self):
import threading
@@ -533,10 +533,10 @@ class NativeReviewTests(unittest.TestCase):
api = m.ApiAdapter(request, origin=m.ORIGIN + '/api/v1', credential_source='$HERMES_HOME/.env:GITEA_TOKEN')
baseline = api.snapshot(p['target'])
with tempfile.TemporaryDirectory() as td:
result = m.publish(p, baseline, api, FakeLease(), td)
result = m.publish(p, baseline, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertEqual(result['mode'], 'created'); self.assertEqual(result['projected_review_ids'], [8])
self.assertTrue(result['commit_file_order_normalized'])
second = m.publish(p, baseline, api, FakeLease(), td)
second = m.publish(p, baseline, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertEqual(second['mode'], 'no_op'); self.assertFalse(second['wrote'])
self.assertEqual(state['posts'], 1); self.assertEqual(server.received, m.build_payload(p))
finally:
@@ -591,10 +591,10 @@ class NativeReviewTests(unittest.TestCase):
with self.subTest(field=field), tempfile.TemporaryDirectory() as td:
p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact)
api.timeout = True; api.apply = False
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), td)
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
path = Path(td) / 'intent.json'; intent = json.loads(path.read_text()); intent[field] = 'corrupt'
path.write_text(json.dumps(intent)); api.created = True
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td)
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
self.assertEqual(api.posts, 1)
def test_issue_repository_mismatch_is_not_accepted(self):
@@ -33,7 +33,7 @@ def result(verdict="approve"):
def publication_fixture():
r = result(); paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(r, paths)
body = mod.render_review(r, paths, declaration={"mode": "none"})
url = "https://example.test/acme/repo/pulls/7"
user = {"id": 42, "login": "spec-agent"}
review = {"id": 11, "body": body, "commit_id": r["target"]["head"], "state": "APPROVED",
@@ -71,10 +71,10 @@ class SupportTests(unittest.TestCase):
def test_validate_and_render_roundtrip(self):
r = result()
self.assertEqual(mod.validate_result(r, {"src/a.py", "tests/test_a.py"}), r)
body = mod.render_review(r, {"src/a.py", "tests/test_a.py"})
body = mod.render_review(r, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"})
parsed = mod.parse_review(body, {"src/a.py", "tests/test_a.py"})
self.assertEqual(parsed, r)
self.assertEqual(mod.render_review(parsed, {"src/a.py", "tests/test_a.py"}), body)
self.assertEqual(mod.render_review(parsed, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"}), body)
def test_validation_rejects_bool_path_traversal_bad_verdict(self):
r = result()
@@ -148,7 +148,7 @@ class SupportTests(unittest.TestCase):
p = subprocess.run(base + ["validate-result", "--result", str(rf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr)
p = subprocess.run(base + ["render", "--result", str(rf), "--allowed-paths", str(af)],
p = subprocess.run(base + ["render", "--no-delegated-evidence", "--result", str(rf), "--allowed-paths", str(af)],
text=True, capture_output=True, env=env)
self.assertEqual(p.returncode, 0, p.stderr); bf.write_text(p.stdout, encoding="utf-8")
p = subprocess.run(base + ["parse", "--body", str(bf), "--allowed-paths", str(af)],
@@ -160,12 +160,12 @@ class SupportTests(unittest.TestCase):
def test_result_codec_preserves_existing_native_format(self):
r = result()
paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(r, paths)
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertIn(" - 구현 근거: src/a.py; 증거\n", body)
self.assertIn("모든 기준을 충족했다.\n\n<!--", body)
self.assertEqual(mod.parse_review(body, paths), r)
unordered = json.loads(json.dumps(r, sort_keys=True))
self.assertEqual(mod.render_review(unordered, paths), body)
self.assertEqual(mod.render_review(unordered, paths, declaration={"mode": "none"}), body)
def test_strict_codec_rejects_unrenderable_and_sensitive_human_values(self):
paths = {"src/a.py", "tests/test_a.py"}
@@ -188,7 +188,7 @@ class SupportTests(unittest.TestCase):
def test_parser_rejects_forged_marker_and_body_drift(self):
paths = {"src/a.py", "tests/test_a.py"}
body = mod.render_review(result(), paths)
body = mod.render_review(result(), paths, declaration={"mode": "none"})
for bad in (body.replace("issue=3", "issue=4"), body.replace("증거", "다른 증거"),
body.replace("\n", "\r\n"), body + "\n", body.replace("correlation=", "other=")):
with self.subTest(body=bad[-100:]), self.assertRaises(mod.ValidationError):
@@ -199,7 +199,7 @@ class SupportTests(unittest.TestCase):
r["criteria"][0]["evidence"] = "확인; 모두 — 일치"
r["verdict"] = "blocked_spec"; r["scope"]["spec_conflicts"] = ["하나, 둘", "세 가지"]
paths = {"src/a, b.py", "src/c; d.py", "tests/test_a.py"}
self.assertEqual(mod.parse_review(mod.render_review(r, paths), paths), r)
self.assertEqual(mod.parse_review(mod.render_review(r, paths, declaration={"mode": "none"}), paths), r)
def test_cache_enforces_root_boundary_uid_and_dangling_locks(self):
@@ -379,7 +379,7 @@ class SupportTests(unittest.TestCase):
def test_target_identifiers_preserve_dots_but_reject_marker_breaking_whitespace(self):
r = result(); paths = {"src/a.py", "tests/test_a.py"}
r["target"].update(owner="acme.name", repository="repo.name")
body = mod.render_review(r, paths)
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertEqual(mod.parse_review(body, paths), r)
r["target"]["owner"] = "acme name"
with self.assertRaises(mod.ValidationError):
@@ -389,7 +389,7 @@ class SupportTests(unittest.TestCase):
r = result(); path = "src/<script>@user`[link].py"
r["criteria"][0]["implementation_paths"] = [path]
paths = {path, "tests/test_a.py"}
body = mod.render_review(r, paths)
body = mod.render_review(r, paths, declaration={"mode": "none"})
self.assertNotIn("<script>", body)
self.assertNotIn("@user", body)
self.assertNotIn("`[link]", body)