[verified] Add fail-closed rework provider readback adapter

This commit is contained in:
2026-10-12 01:46:39 +00:00
parent d47df21578
commit 2ad836f332
8 changed files with 1744 additions and 8 deletions
@@ -0,0 +1,159 @@
# Canonical PR rework completion readback
## Scope and source
Canonical entry point: `<HARNESS_ROOT>/harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py`.
The adjacent `rework_readback.py` derives readback inputs; `rework_readback_cli.py`
provides the role-local GET-only transport and private artifact writer. Update
these canonical files and this reference in place; do not copy them into another
profile or use a review-role credential for impl delivery.
`snapshot` and `verify-live` never create, edit, retry, or submit a Gitea comment.
The `create` action verifies an already observed single comment addition; `reuse`
verifies an unchanged collection with exactly one matching existing comment.
An independently authorized publication workflow remains responsible for its
single external write, uncertain-write reconciliation, Issue lease, and fresh
pre-write delegation admission.
## Prerequisites and independent expectations
- The canonical access contract is `harness/operations/gitea-access.md`.
- Run in the impl role's own `HERMES_HOME`. Only its owned, non-symlink, mode-0600
`.env` file supplies `GITEA_TOKEN`. No ambient token or other-profile fallback.
- Independently supply the expected stable impl account ID. The first API read
must authenticate that ID and the `gitea-impl-agent` login.
- Generate `expected.json` with the existing canonical renderer. Its exact target,
final head, reviewed head, review ID, body, marker and digest bind the workflow.
- Supply a reviewed provider contract and its independently expected SHA-256.
The exact provider version is read from the server and must match the contract.
A digest calculated from an untrusted delegate file is not independent evidence.
- Own a private mode-0700 runtime directory. Inputs must be owned regular files,
not group/world writable or symlinked. Output files use mode 0600; a failed
verification does not replace an existing receipt.
## Provider contract
Schema: `gitea-agent.rework-provider/v1`. The contract explicitly declares the
aggregate semantics for PR and underlying PR-Issue `comments` fields. Each
`components` array contains unique values from `top_level`, `reviews`, `inline`
and must include `top_level`. Choose the reviewed deployment semantics, not the
combination that happens to fit a current response. There is no guessed version
or automatic capability downgrade.
Shape example only, **not a claim about this deployment**:
```json
{
"schema": "gitea-agent.rework-provider/v1",
"version": "<independently-observed-version>",
"aggregate": {
"pr": {"field": "comments", "components": ["top_level"]},
"pr_issue": {"field": "comments", "components": ["top_level"]}
},
"inline": {
"mode": "per_review",
"field": "review_comments",
"components": ["inline"]
}
}
```
`per_review` requires every exact-review comment endpoint. Only the explicit
`per_review_or_pr_count` capability permits endpoint-unavailable 404/405 fallback.
Permission errors, redirects, malformed JSON and conflicting metadata never
trigger fallback. Fallback uses a fresh PR count, the declared review/inline
components and complete review `comments_count` records; disagreement is BLOCK.
Counts-only fallback is recorded as such and is not proof of inline comment
bodies or a complete inline-record inventory.
Native wire-shape reference: the pinned Gitea `v1.24.6` serializers in
`services/convert/pull.go` and `modules/structs/{issue,pull}.go` expose PR `url`
and `html_url` as HTML URLs, and PR-Issue `pull_request.html_url` without a native
`pull_request.url`. Require the actual native relation URL; do not invent a
missing field. This source pin is fixture/protocol evidence, not a claim about
the deployment version. The caller's independent provider contract still binds
that version. In `per_review` mode only, an omitted `review_comments` is accepted
as zero only when complete per-review records and their counters prove a zero
projection. Explicit null, nonzero projection or counts-only fallback with a
missing scalar is rejected. Raw pagination Link evidence is preserved and
`first`, `prev`, `next`, `last` are checked against the declared total, including
the empty sentinel page after an exact multiple of the page size.
## Commands and canonical artifact paths
The caller's admitted Issue runtime is
`<HARNESS_ROOT>/repositories/<owner>/<repository>/worktrees/issue-<issue>/runtime/<run_id>`.
Place the canonical `rework-baseline.json` and `rework-receipt.json` there, outside
the product checkout. Preserve the baseline SHA-256 returned by its producer
independently; never replace the baseline after the external write.
Through `terminal`, before publication:
```sh
python3 -B <HARNESS_ROOT>/harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py snapshot \
--expected <RUNTIME>/expected.json \
--provider-contract <TRUSTED_PROVIDER_CONTRACT> \
--provider-contract-sha256 <INDEPENDENT_PROVIDER_SHA256> \
--actor-id <INDEPENDENT_IMPL_ACCOUNT_ID> \
--output <RUNTIME>/rework-baseline.json
```
After the authorized write or existing-comment reuse, through `terminal`:
```sh
python3 -B <HARNESS_ROOT>/harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py verify-live \
--expected <RUNTIME>/expected.json \
--provider-contract <TRUSTED_PROVIDER_CONTRACT> \
--provider-contract-sha256 <INDEPENDENT_PROVIDER_SHA256> \
--actor-id <INDEPENDENT_IMPL_ACCOUNT_ID> \
--baseline <RUNTIME>/rework-baseline.json \
--baseline-sha256 <INDEPENDENT_BASELINE_SHA256> \
--action create \
--output <RUNTIME>/rework-receipt.json
```
Use `--action reuse` only for an unchanged scoped collection. Do not recapture a
baseline or retry a write merely because readback fails. Exit 0 and a receipt are
required; stdout reports only the artifact path, fingerprint and read-only flag.
The existing offline `verify` command remains compatible, but manually supplied
JSON/true flags or archived artifacts alone are not fresh provider readback.
## Verified invariants
- Source Issue, PR, underlying PR-Issue, repository, exact source review and heads
are independently bound to authenticated endpoint responses.
- Pagination is bounded; duplicate IDs, contradictory totals, truncated pages,
malformed metadata and off-target continuation links are rejected.
- Genuine empty arrays without a count header are supported; null is not silently
accepted as an empty collection.
- Existing top-level comments, Source Issue fields/comments, native reviews and
admitted inline evidence remain unchanged. Create admits exactly one matching
impl-authored comment. Reuse admits no addition.
- Only provider-declared count projections and the bounded PR/underlying-PR-Issue
state-delta/exact-timestamp admission for that verified new comment are admitted;
Source Issue metadata, including `updated_at`, is fully immutable. This admission
is not proof of causal event attestation; the receipt records that limitation.
- The adapter computes normalized inputs and passes them to the same verifier used
by `rework_comment.py verify`, rather than accepting caller success flags.
## Limits and verification
The CLI enforces verified HTTPS, no redirects, GET-only requests, a one-megabyte
response cap, ten-second socket timeouts and a 120-second operation budget. Body
budget checks occur between socket reads, so an already-blocking read may add its
remaining socket-timeout allowance. There are no automatic retries.
The authenticated transport and independently supplied actor/provider/baseline
expectations are trust inputs, not a sandbox or proof that arbitrary caller code
is honest. The adapter cannot prove missing inline bodies from scalar counts, and
state deltas plus exact timestamps are not independent causal-event attestation.
Run focused tests with `terminal`:
```sh
python3 -B -m unittest harness.tests.impl.test_rework_readback harness.tests.impl.test_rework_readback_transport harness.tests.impl.test_rework_comment -v
python3 -B harness/tests/run.py
```
Local synthetic transport and HTTP fixtures validate the executable paths; they
do not constitute a live Gitea readback or an external-write completion record.