Validate repository transaction results

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b6f-7db1-7222-918b-e813a6bda38d
This commit is contained in:
tegwick 2026-08-23 12:58:07 +02:00
parent eaae4357eb
commit 2e16504e1f
3 changed files with 573 additions and 4 deletions

View file

@ -8,9 +8,11 @@ boundary is still under owner review.
from __future__ import annotations from __future__ import annotations
import fcntl import fcntl
import fnmatch
import hashlib import hashlib
import json import json
import os import os
import stat
import subprocess import subprocess
import uuid import uuid
from dataclasses import dataclass from dataclasses import dataclass
@ -50,6 +52,49 @@ class DirtyRepositoryError(RepositoryTransactionError):
) )
class RepositoryAcceptanceError(RepositoryTransactionError):
"""The post-run repository state does not satisfy its explicit policy."""
def __init__(self, code: str, detail: str) -> None:
self.code = _bounded_text(code, 80)
self.detail = _bounded_text(detail, 300)
super().__init__(f"repository acceptance failed: {self.code}: {self.detail}")
@dataclass(frozen=True)
class RepositoryAcceptancePolicy:
"""Bounded local-commit policy supplied by a future governed run grant."""
allowed_paths: tuple[str, ...]
min_commits: int = 1
max_commits: int = 1
max_evidence_paths: int = 100
def __post_init__(self) -> None:
if not 1 <= self.min_commits <= self.max_commits <= 32:
raise ValueError("commit bounds must satisfy 1 <= min <= max <= 32")
if not 0 <= self.max_evidence_paths <= 200:
raise ValueError("max_evidence_paths must be between 0 and 200")
if len(self.allowed_paths) > 100:
raise ValueError("allowed_paths must contain at most 100 patterns")
for pattern in self.allowed_paths:
_validate_path_pattern(pattern)
@property
def policy_id(self) -> str:
payload = json.dumps(
{
"allowed_paths": self.allowed_paths,
"min_commits": self.min_commits,
"max_commits": self.max_commits,
"max_evidence_paths": self.max_evidence_paths,
},
sort_keys=True,
separators=(",", ":"),
)
return _digest(payload)[:32]
@dataclass(frozen=True) @dataclass(frozen=True)
class RepositoryBaseline: class RepositoryBaseline:
"""Exact local facts captured while the canonical repository lock is held.""" """Exact local facts captured while the canonical repository lock is held."""
@ -67,6 +112,7 @@ class RepositoryBaseline:
upstream_ref: str | None upstream_ref: str | None
upstream_oid: str | None upstream_oid: str | None
remote_refs: tuple[tuple[str, str], ...] remote_refs: tuple[tuple[str, str], ...]
protected_git_metadata_digest: str
@property @property
def detached(self) -> bool: def detached(self) -> bool:
@ -90,6 +136,37 @@ class RepositoryBaseline:
"upstream_oid": self.upstream_oid, "upstream_oid": self.upstream_oid,
"remote_ref_count": len(self.remote_refs), "remote_ref_count": len(self.remote_refs),
"remote_refs_digest": _digest(remote_payload), "remote_refs_digest": _digest(remote_payload),
"protected_git_metadata_digest": self.protected_git_metadata_digest,
}
@dataclass(frozen=True)
class RepositoryAcceptance:
"""Validated, bounded evidence for one accepted local repository result."""
policy_id: str
head: str
branch: str | None
commits: tuple[str, ...]
changed_paths: tuple[str, ...]
max_evidence_paths: int
def evidence(self) -> dict[str, Any]:
visible_paths = self.changed_paths[: self.max_evidence_paths]
return {
"policy_id": self.policy_id,
"accepted": True,
"head": self.head,
"branch": self.branch,
"commit_count": len(self.commits),
"commits": list(self.commits),
"changed_path_count": len(self.changed_paths),
"changed_paths": list(visible_paths),
"changed_paths_truncated": len(visible_paths) < len(self.changed_paths),
"changed_paths_digest": _digest("\0".join(self.changed_paths)),
"clean_post_state": True,
"remote_refs_unchanged": True,
"protected_git_metadata_unchanged": True,
} }
@ -116,6 +193,7 @@ class RepositoryTransaction:
self.require_clean = require_clean self.require_clean = require_clean
self.transaction_id = uuid.uuid4().hex self.transaction_id = uuid.uuid4().hex
self.baseline: RepositoryBaseline | None = None self.baseline: RepositoryBaseline | None = None
self.acceptance: RepositoryAcceptance | None = None
self.lock_path: Path | None = None self.lock_path: Path | None = None
self._lock_file: TextIO | None = None self._lock_file: TextIO | None = None
@ -209,11 +287,125 @@ class RepositoryTransaction:
"""Return the bounded transaction and baseline result envelope.""" """Return the bounded transaction and baseline result envelope."""
if self.baseline is None: if self.baseline is None:
raise RepositoryTransactionError("repository baseline is not captured") raise RepositoryTransactionError("repository baseline is not captured")
return { evidence = {
"transaction_id": self.transaction_id, "transaction_id": self.transaction_id,
"correlation_id": self.correlation_id, "correlation_id": self.correlation_id,
"baseline": self.baseline.evidence(), "baseline": self.baseline.evidence(),
} }
if self.acceptance is not None:
evidence["acceptance"] = self.acceptance.evidence()
return evidence
def validate_acceptance(
self,
policy: RepositoryAcceptancePolicy,
) -> RepositoryAcceptance:
"""Validate the current checkout without changing it or releasing its lock."""
if not self.locked or self.baseline is None:
raise RepositoryTransactionError(
"repository acceptance requires an active transaction"
)
baseline = self.baseline
post = _capture_baseline(
baseline.repo_root,
baseline.git_common_dir,
baseline.repo_id,
)
if post.branch != baseline.branch:
raise RepositoryAcceptanceError(
"branch-changed",
f"expected={baseline.branch or 'detached'} actual={post.branch or 'detached'}",
)
if post.head == baseline.head:
raise RepositoryAcceptanceError("head-unchanged", "no new commit to accept")
ancestry = _run_git(
baseline.repo_root,
"merge-base",
"--is-ancestor",
baseline.head,
post.head,
)
if ancestry.returncode == 1:
raise RepositoryAcceptanceError(
"non-descendant-head",
"post-run HEAD does not descend from the transaction baseline",
)
if ancestry.returncode != 0:
reason = (ancestry.stderr or ancestry.stdout or "Git ancestry error").strip()
raise GitRepositoryError(f"cannot validate commit ancestry: {reason[:300]}")
commit_lines = _git(
baseline.repo_root,
"rev-list",
"--reverse",
f"{baseline.head}..{post.head}",
).splitlines()
commits = tuple(line for line in commit_lines if line)
if not policy.min_commits <= len(commits) <= policy.max_commits:
raise RepositoryAcceptanceError(
"commit-count",
f"expected={policy.min_commits}..{policy.max_commits} actual={len(commits)}",
)
paths_result = _run_git(
baseline.repo_root,
"diff",
"--name-only",
"-z",
baseline.head,
post.head,
"--",
)
if paths_result.returncode != 0:
reason = (paths_result.stderr or paths_result.stdout or "Git diff error").strip()
raise GitRepositoryError(f"cannot determine changed paths: {reason[:300]}")
changed_paths = tuple(path for path in paths_result.stdout.split("\0") if path)
if not changed_paths:
raise RepositoryAcceptanceError(
"no-changed-paths",
"accepted commits do not change a repository path",
)
invalid_paths = tuple(
path for path in changed_paths if not _path_allowed(path, policy.allowed_paths)
)
if invalid_paths:
visible = ",".join(invalid_paths[:5])
raise RepositoryAcceptanceError(
"path-not-granted",
f"count={len(invalid_paths)} paths={visible[:220]}",
)
if not post.clean:
raise RepositoryAcceptanceError(
"dirty-post-state",
f"entries={post.dirty_entries} status_digest={post.status_digest}",
)
if post.remote_refs != baseline.remote_refs:
raise RepositoryAcceptanceError(
"remote-refs-changed",
"local remote-tracking refs moved during the transaction",
)
if (
post.protected_git_metadata_digest
!= baseline.protected_git_metadata_digest
):
raise RepositoryAcceptanceError(
"git-metadata-changed",
"protected Git config, hooks, or info metadata changed",
)
acceptance = RepositoryAcceptance(
policy_id=policy.policy_id,
head=post.head,
branch=post.branch,
commits=commits,
changed_paths=changed_paths,
max_evidence_paths=policy.max_evidence_paths,
)
self.acceptance = acceptance
return acceptance
def _write_metadata(self, value: dict[str, Any]) -> None: def _write_metadata(self, value: dict[str, Any]) -> None:
if self._lock_file is None: if self._lock_file is None:
@ -301,9 +493,68 @@ def _capture_baseline(repo_root: Path, common_dir: Path, repo_id: str) -> Reposi
upstream_ref=upstream_ref, upstream_ref=upstream_ref,
upstream_oid=upstream_oid, upstream_oid=upstream_oid,
remote_refs=tuple(remote_refs), remote_refs=tuple(remote_refs),
protected_git_metadata_digest=_protected_git_metadata_digest(
repo_root,
common_dir,
),
) )
def _protected_git_metadata_digest(repo_root: Path, common_dir: Path) -> str:
raw_git_dir = Path(_git(repo_root, "rev-parse", "--git-dir"))
git_dir = (
(repo_root / raw_git_dir).resolve()
if not raw_git_dir.is_absolute()
else raw_git_dir.resolve()
)
candidates = (
("common/config", common_dir / "config"),
("common/hooks", common_dir / "hooks"),
("common/info/attributes", common_dir / "info" / "attributes"),
("common/info/exclude", common_dir / "info" / "exclude"),
("worktree/config.worktree", git_dir / "config.worktree"),
)
digest = hashlib.sha256()
for label, path in candidates:
_hash_metadata_path(digest, label, path)
return digest.hexdigest()
def _hash_metadata_path(digest: Any, label: str, path: Path) -> None:
digest.update(label.encode("utf-8"))
digest.update(b"\0")
if not path.exists() and not path.is_symlink():
digest.update(b"missing\0")
return
paths = [path]
if path.is_dir():
paths.extend(sorted(path.rglob("*"), key=lambda item: item.as_posix()))
for item in paths:
relative = "." if item == path else item.relative_to(path).as_posix()
try:
item_stat = item.lstat()
mode = stat.S_IMODE(item_stat.st_mode)
digest.update(f"{relative}\0{mode:o}\0".encode("utf-8"))
if item.is_symlink():
digest.update(b"symlink\0")
digest.update(os.readlink(item).encode("utf-8", errors="surrogateescape"))
elif item.is_file():
digest.update(b"file\0")
with item.open("rb") as handle:
for chunk in iter(lambda: handle.read(65536), b""):
digest.update(chunk)
elif item.is_dir():
digest.update(b"dir\0")
else:
digest.update(b"other\0")
digest.update(b"\0")
except OSError as exc:
raise GitRepositoryError(
f"cannot hash protected Git metadata: {label}: {exc}"
) from exc
def _run_git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]: def _run_git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
try: try:
return subprocess.run( return subprocess.run(
@ -349,6 +600,56 @@ def _digest(value: str) -> str:
return hashlib.sha256(value.encode("utf-8", errors="replace")).hexdigest() return hashlib.sha256(value.encode("utf-8", errors="replace")).hexdigest()
def _validate_path_pattern(pattern: str) -> None:
if not isinstance(pattern, str) or not pattern or len(pattern) > 256:
raise ValueError("path patterns must be non-empty strings of at most 256 characters")
normalized = pattern.rstrip("/")
parts = tuple(normalized.split("/"))
if (
pattern.startswith("/")
or "\\" in pattern
or "\0" in pattern
or any(part in {"", ".", ".."} for part in parts)
):
raise ValueError(f"path pattern must be repository-relative: {pattern!r}")
if parts and parts[0] == ".git":
raise ValueError("path patterns cannot grant protected .git metadata")
def _path_allowed(path: str, patterns: tuple[str, ...]) -> bool:
normalized = path
parts = tuple(normalized.split("/"))
if (
normalized.startswith("/")
or "\\" in normalized
or any(part in {"", ".", ".."} for part in parts)
or (parts and parts[0] == ".git")
):
return False
for raw_pattern in patterns:
if raw_pattern.endswith("/") and normalized.startswith(raw_pattern):
return True
if _match_path_parts(parts, tuple(raw_pattern.split("/"))):
return True
return False
def _match_path_parts(path_parts: tuple[str, ...], pattern_parts: tuple[str, ...]) -> bool:
if not pattern_parts:
return not path_parts
pattern = pattern_parts[0]
if pattern == "**":
return _match_path_parts(path_parts, pattern_parts[1:]) or bool(
path_parts
and _match_path_parts(path_parts[1:], pattern_parts)
)
return bool(
path_parts
and fnmatch.fnmatchcase(path_parts[0], pattern)
and _match_path_parts(path_parts[1:], pattern_parts[1:])
)
def _bounded_text(value: str, limit: int) -> str: def _bounded_text(value: str, limit: int) -> str:
cleaned = " ".join(str(value).split()) cleaned = " ".join(str(value).split())
return cleaned[:limit] return cleaned[:limit]

View file

@ -12,6 +12,8 @@ import pytest
from rein_aharness.repository_transaction import ( from rein_aharness.repository_transaction import (
DirtyRepositoryError, DirtyRepositoryError,
GitRepositoryError, GitRepositoryError,
RepositoryAcceptanceError,
RepositoryAcceptancePolicy,
RepositoryBusyError, RepositoryBusyError,
RepositoryTransaction, RepositoryTransaction,
) )
@ -215,6 +217,250 @@ def test_busy_error_exposes_bounded_owner_metadata(tmp_path: Path) -> None:
assert len(excinfo.value.owner["correlation_id"]) == 200 assert len(excinfo.value.owner["correlation_id"]) == 200
def test_acceptance_proves_one_descendant_commit_on_granted_paths(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
policy = RepositoryAcceptancePolicy(allowed_paths=("docs/",))
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/result.md", "accepted\n")
accepted = tx.validate_acceptance(policy)
assert len(accepted.commits) == 1
assert accepted.changed_paths == ("docs/result.md",)
evidence = tx.evidence()["acceptance"]
assert evidence["accepted"] is True
assert evidence["policy_id"] == policy.policy_id
assert evidence["changed_paths"] == ["docs/result.md"]
assert evidence["clean_post_state"] is True
assert evidence["remote_refs_unchanged"] is True
def test_acceptance_rejects_ungranted_extra_path(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "UNRELATED.md", "not granted\n")
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/",))
)
assert excinfo.value.code == "path-not-granted"
assert "UNRELATED.md" in excinfo.value.detail
def test_acceptance_rejects_more_commits_than_policy_allows(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/one.md", "one\n")
_commit(repo, "docs/two.md", "two\n")
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/",))
)
assert excinfo.value.code == "commit-count"
assert "actual=2" in excinfo.value.detail
def test_acceptance_rejects_dirty_post_state(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/result.md", "committed\n")
(repo / "docs" / "result.md").write_text("left dirty\n", encoding="utf-8")
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/",))
)
assert excinfo.value.code == "dirty-post-state"
def test_acceptance_rejects_remote_tracking_ref_movement(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
subprocess.run(
["git", "update-ref", "refs/remotes/origin/main", "HEAD"],
cwd=repo,
check=True,
)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/result.md", "committed\n")
subprocess.run(
["git", "update-ref", "refs/remotes/origin/main", "HEAD"],
cwd=repo,
check=True,
)
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/",))
)
assert excinfo.value.code == "remote-refs-changed"
@pytest.mark.parametrize("metadata_kind", ["config", "hook"])
def test_acceptance_rejects_protected_git_metadata_changes(
tmp_path: Path,
metadata_kind: str,
) -> None:
repo = _make_repo(tmp_path)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/result.md", "committed\n")
if metadata_kind == "config":
subprocess.run(
["git", "config", "rein-aharness.test", "changed"],
cwd=repo,
check=True,
)
else:
hook = repo / ".git" / "hooks" / "pre-commit"
hook.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/",))
)
assert excinfo.value.code == "git-metadata-changed"
def test_acceptance_rejects_non_descendant_head(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
branch = subprocess.run(
["git", "branch", "--show-current"],
cwd=repo,
check=True,
capture_output=True,
text=True,
).stdout.strip()
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
subprocess.run(["git", "checkout", "--orphan", "unrelated"], cwd=repo, check=True)
subprocess.run(["git", "rm", "-qf", "README.md"], cwd=repo, check=True)
(repo / "docs").mkdir()
(repo / "docs" / "result.md").write_text("unrelated\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"unrelated",
],
cwd=repo,
check=True,
)
subprocess.run(["git", "checkout", "-qB", branch], cwd=repo, check=True)
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/",))
)
assert excinfo.value.code == "non-descendant-head"
def test_acceptance_rejects_branch_change_and_unchanged_head(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
policy = RepositoryAcceptancePolicy(allowed_paths=("docs/",))
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(policy)
assert excinfo.value.code == "head-unchanged"
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
subprocess.run(["git", "checkout", "-qb", "other"], cwd=repo, check=True)
_commit(repo, "docs/result.md", "committed\n")
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(policy)
assert excinfo.value.code == "branch-changed"
def test_acceptance_evidence_bounds_changed_path_list(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
policy = RepositoryAcceptancePolicy(
allowed_paths=("docs/",),
max_evidence_paths=2,
)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/a.md", "a\n", extra={"docs/b.md": "b\n", "docs/c.md": "c\n"})
evidence = tx.validate_acceptance(policy).evidence()
assert evidence["changed_path_count"] == 3
assert len(evidence["changed_paths"]) == 2
assert evidence["changed_paths_truncated"] is True
assert len(evidence["changed_paths_digest"]) == 64
def test_acceptance_path_glob_does_not_cross_directory_boundaries(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/nested/result.md", "nested\n")
with pytest.raises(RepositoryAcceptanceError) as excinfo:
tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/*.md",))
)
assert excinfo.value.code == "path-not-granted"
def test_acceptance_double_star_path_glob_allows_nested_paths(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
with RepositoryTransaction(repo, state_dir=tmp_path / "state") as tx:
_commit(repo, "docs/nested/result.md", "nested\n")
accepted = tx.validate_acceptance(
RepositoryAcceptancePolicy(allowed_paths=("docs/**/*.md",))
)
assert accepted.changed_paths == ("docs/nested/result.md",)
@pytest.mark.parametrize(
"pattern",
["/absolute", "../escape", ".git/config", "docs\\windows"],
)
def test_acceptance_policy_rejects_unsafe_path_patterns(pattern: str) -> None:
with pytest.raises(ValueError, match="path pattern"):
RepositoryAcceptancePolicy(allowed_paths=(pattern,))
def _commit(
repo: Path,
relative_path: str,
content: str,
*,
extra: dict[str, str] | None = None,
) -> None:
files = {relative_path: content, **(extra or {})}
for name, value in files.items():
path = repo / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"task result",
],
cwd=repo,
check=True,
)
def _status(repo: Path) -> str: def _status(repo: Path) -> str:
return subprocess.run( return subprocess.run(
["git", "status", "--porcelain=v2", "--untracked-files=all"], ["git", "status", "--porcelain=v2", "--untracked-files=all"],

View file

@ -159,9 +159,9 @@ and non-repository refusal.
The primitive is intentionally not wired into `runner.py`, legacy approaches, The primitive is intentionally not wired into `runner.py`, legacy approaches,
or the claim loop while ADR-002 still awaits Activity Core, sand-boxer, and or the claim loop while ADR-002 still awaits Activity Core, sand-boxer, and
llm-connect acknowledgements. Lease-loss cancellation, moved-`HEAD` acceptance, llm-connect acknowledgements. Controlled moved-`HEAD` acceptance is prepared
timeout/signal integration, and result-close reconciliation remain outstanding; under T03 below; lease-loss cancellation, timeout/signal integration, and
T02 therefore remains `wait`. result-close reconciliation remain outstanding. T02 therefore remains `wait`.
## Verify accepted commits and reconcile metrics/reporting ## Verify accepted commits and reconcile metrics/reporting
@ -199,6 +199,28 @@ leaves the checkout in its declared state; metrics and required completion
evidence survive a temporary Hub/API outage without duplicating the workload evidence survive a temporary Hub/API outage without duplicating the workload
commit; and docs no longer claim checks that the code does not perform. commit; and docs no longer claim checks that the code does not perform.
### Preparation — 2026-08-23
Extended the production-inert transaction core with an explicit
`RepositoryAcceptancePolicy` and read-only post-run validator. It now requires
same-branch descendant commits within declared bounds, repository-relative path
patterns, a clean index/worktree, unchanged local remote-tracking refs, and an
unchanged digest of protected Git config, hooks, and info metadata. Accepted
evidence contains bounded commit/path lists plus policy, path-set, baseline, and
post-state digests; it retains no prompt or provider output.
Sixteen additional adversarial cases bring the focused transaction suite to 26
tests. They cover a valid one-commit result, unrelated history, excess commits,
ungranted paths, glob boundary semantics, dirty post-state, branch movement,
unchanged `HEAD`, remote-ref movement, Git config/hook changes, and bounded path
evidence. The full suite passes 123 tests with the existing optional
`glas_harness` contract skip.
No current `TaskSpec` or instance-manifest field supplies an authoritative path
grant, so the validator remains deliberately unwired. Defining that versioned
grant surface, metrics atomicity, and required close-evidence reconciliation
remain outstanding; T03 remains `wait`.
## Remove tenant logic from the shared runtime ## Remove tenant logic from the shared runtime
```task ```task