[verified] Enforce delegation adoption at runtime consumers
This commit is contained in:
@@ -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()
|
||||
+90
@@ -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를 신뢰하지 않는다.
|
||||
|
||||
+40
-4
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user