Skip to content

Verify bound acceptance evidence; accepted copy for the verified customer - #11

Merged
CompleteDotTech merged 6 commits into
mainfrom
feat/bound-acceptance-evidence
Oct 23, 2026
Merged

CompleteDotTech merged 6 commits into
mainfrom
feat/bound-acceptance-evidence

Conversation

@CompleteDotTech

@CompleteDotTech CompleteDotTech commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Refs CompleteTech-LLC/complete.tech#17 (gap 2, plugin side). Stacked on #10 (base = feat/request-admission-guard; retarget to main after #10 merges). Scanning stays disabled by default; both new behaviours are off unless configured.

What changes

  • Bound acceptance evidence. A bound acceptance retains the exact terms bytes the signer was shown (_ct_terms_snapshot) and the exact Target Summary (_ct_summary_text), each next to the digest the signer's form carried. Job creation and every later liveness check (ct_scan_authorization_is_live: claim, start, admit, result) recompute them: missing, oversized (512 KiB / 8 KiB), mismatched, stale-form, repeated-field or partial evidence refuses the job (ct_scan_binding_mismatch / ct_scan_binding_required). The job row pins summary_sha256, so editing retained evidence after the fact cancels a queued/running job. Config bound_terms_versions (default empty) lists versions that MUST be bound; unmarked-but-partial evidence is always refused. The plugin never fetches mutable page HTML.
  • Accepted copy for the verified customer (send_accepted_copy, default off): after a successful mailbox confirmation, one email to the verified address with the Target Summary, hashes, filled-in copy and the exact terms bytes as an attachment. At most once per job (compare-and-set claim before hand-off), never to a referrer, uncertain outcomes recorded unknown and never resent, accepted = transport acceptance not receipt, failure cannot cancel/delay/repeat a scan. Schema 4: summary_sha256, accepted_copy_status, accepted_copy_at.

Fail-closed behavior

Any evidence defect before or after job creation refuses/cancels; no fallback to "legacy" when evidence is present but wrong; unreadable/oversized meta refuses; the accepted-copy path returns an error without a confirmed recipient, and mail faults/throws are swallowed into unknown without touching the job.

Tests

integrations/wordpress/tests/evidence.php (+ evidence-strict.php in a second process with binding required and copy on): 16 single-defect refusals, byte round trip with multibyte/quotes, post-creation edit cancels, copy sent to verified contact only with exact attachment bytes, no temp file left, idempotent, mail false/throw => unknown, no resend, unconfirmed => error, disabled => nothing sent. Run against disposable MariaDB/WordPress; mutation-checked (9 mutants caught). CI green on the head. Cross-repo end to end (recorder from the companion complete.tech PR -> this plugin) verified locally.

Reviewer: please attack

Digest comparison and normalisation (case, whitespace, encoding), anything that lets a request become a job with evidence the signer did not see, TOCTOU between evidence checks and job insert (checks run under the queue lock), temp-file handling for the attachment, recipient selection.

Needs the owner

  • Wording of the accepted-copy email is a functional placeholder (marked TODO(owner) in code); customer-facing copy was not approved.
  • The form (template/JS) must display the canonical Target Summary and post ct_terms_digest / ct_summary_digest; that is the step-5 release and owns form/CTA copy.
  • Observed (pre-existing, fail-closed): the plugin requires the filled-in copy to appear verbatim in post_content; WordPress kses entity-encodes &/< for anonymous submitters, so a company name such as "AT&T" saves as a lead that is not queued. Worth a decision before activation.

Second independent review: fixes landed

  • Enabling bound_terms_versions cancelled in-flight unbound jobs -> the requirement and the profile are now frozen per job (bound_required).
  • The Target Summary check only probed four labels (decoys and appended instructions could ride into the customer email) -> the summary must now equal the canonical four lines exactly.
  • Accepted copy: same recipient rule as the report path (is_email, saved address, terms version/hash); attachment written to a private random directory and attached only if it reproduces the recorded hash; short writes handled.
  • CI: evidence.php spawns evidence-strict.php itself (binding required + copy on), so the strict path is covered.
    Accepted: the confirmation page still waits for the lock and the SMTP hand-off before responding.

Fail-closed review round 2 (coordinator findings)

Fixed in this stack, each with tests that fail without the fix (mutation-checked):

  • Per-request admission guard for the scan worker #10: activation requires egress_enforced: true (loader refuses otherwise); docs/README/example config state it; unverified nftables egress sketch added (not a claim of enforcement). In-flight bound is exactly 10 s from the grant (no grace). Authorization read from the DB, not the object cache. A completed result needs >= 1 admitted request. Failed schema upgrade: one error_log, 5-minute back-off, plugin 0.2.0. Special-purpose IP ranges listed explicitly (CGNAT, documentation, benchmarking, 3fff::/20, 6to4, NAT64, ...), tested independent of the interpreter's ipaddress.
  • Verify bound acceptance evidence; accepted copy for the verified customer #11: bound evidence is required for every version (terms_policy, strict by default; legacy only for r2-or-earlier); the loader refuses to enable without an explicit entry for the served version. ct_scan_target_summary() is the one canonical summary and now includes limits and period.
  • Exact-target control gate (DNS TXT / HTTPS file / recorded authority review) #12: control is required by the same policy (replaces the opt-in list).
  • Retention mechanics: closure, per-version deadlines, holds, deletion tombstones #13: non-legacy retention floored at 6 years (words/null/0/1 mean 6); tombstone scope is a keyed MAC; deletion needs closed_at >= created_at and a sane clock; retention runs outside the queue lock (own lock, throttled, one short queue lock per deletion) so Stop never queues behind a batch.

CompleteDotTech and others added 3 commits October 22, 2026 07:24
…cepted copy

A bound acceptance retains the exact terms bytes the signer was shown and the
exact Target Summary, each beside the digest the signer's form carried. Job
creation and every later liveness check recompute them: any missing,
oversized, mismatched, stale, repeated or partial piece refuses the job, and
the job row pins the summary digest so later edits end the authorization.
Config `bound_terms_versions` lists versions that must be bound; the plugin
never reads mutable page HTML.

With `send_accepted_copy` (off by default) a successful mailbox confirmation
sends the verified contact the Target Summary, hashes, filled-in copy and the
exact terms bytes. At most once per job (compare-and-set), only to the
verified address, uncertain outcomes never resent, and independent of the scan.

Schema 4: summary_sha256, accepted_copy_status, accepted_copy_at.

Refs CompleteTech-LLC/complete.tech#17 (gap 2, plugin side).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…Target Summary; harden the accepted copy

Review findings: enabling bound_terms_versions cancelled in-flight unbound jobs
(requirement and profile are now frozen in the job row); the summary check let
decoy or appended lines ride along into the customer email (the summary must now
equal the canonical four lines exactly); the accepted copy now applies the report
path's recipient rule, writes the attachment into a private random directory and
attaches only bytes that reproduce the recorded hash.

Co-Authored-By: Claude Code <noreply@anthropic.com>
CompleteDotTech and others added 2 commits October 22, 2026 09:12
…mmary carries limits and period

Merge of the admission-guard fixes plus:
- Every terms version requires bound evidence unless the operator lists it as legacy in
  terms_policy, and legacy is honoured only for r2-or-earlier versions; the loader refuses to
  enable unless supported_terms_version has an explicit entry, so bumping the version cannot
  silently drop the requirement.
- ct_scan_target_summary() is the one canonical Target Summary and now includes the profile's
  limits and the authorization period.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
- ct_scan_check_bound_evidence keyed its checks by the computed hash, so a
  terms snapshot byte-identical to the Target Summary overwrote the terms
  entry and the recorded terms hash / shown digest were never compared. Use
  a list of [computed, recorded] pairs so every comparison runs.
- Refuse (ct_scan_evidence_duplicate) when an evidence key holds more than
  one meta row instead of silently using the first.
- Accepted copy: compare the recorded terms hash case-insensitively, and
  re-check under the queue lock that the job is still live (queued, leased
  or running, authorization live) before claiming the send.
- Tests: identical-bytes cases, duplicate rows at creation and while
  running, upper-case terms hash, and no copy for cancelled, revoked or
  edited-evidence jobs.
- docs/WORDPRESS.md: document the above, list bound_required in schema 4,
  fix the missing blank line before Egress.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@CompleteDotTech
CompleteDotTech changed the base branch from feat/request-admission-guard to main October 23, 2026 19:38
@CompleteDotTech
CompleteDotTech marked this pull request as ready for review October 23, 2026 19:40
@CompleteDotTech
CompleteDotTech merged commit 3379944 into main Oct 23, 2026
10 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T19:43:29.960079Z b09eeee Draft marked ready
🔒 Security Review ✅ Completed 2026-10-23T19:44:39.005669Z b09eeee Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

CompleteDotTech added a commit that referenced this pull request Oct 23, 2026
Brings in the #10 and #11 review fixes (probe busy/inconclusive handling,
bound-evidence pair comparisons, duplicate evidence refusal, accepted-copy
live check).

Resolutions:
- admission.py probe(): keep main's bounded queue_busy retry and use this
  branch's per-kind route ("probe" for http, "control_probe" for control).
  Test added for the control route.
- ct_scan_send_accepted_copy(): main's live-job check allowed only
  queued/leased/running, but confirmation now leaves control-required jobs
  in awaiting_control and sends the copy then, so the default path would
  never send it. awaiting_control is accepted as live; every other check
  (authorization live, recipient, terms hash, compare-and-set) is kept.
  evidence-strict.php covers confirmation into awaiting_control; docs updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CompleteDotTech added a commit that referenced this pull request Oct 23, 2026
Brings in the merged #10/#11/#12 review fixes (probe busy-retry, list-of-pairs
evidence comparison, control_admitted and 409 already_admitted, fail-closed
ct_scan_control_required(), terms_policy docs) under the retention branch.

Conflict: the schema's required jobs columns now list both control_admitted
and the retention/hold/identity columns. The schema version moves to 7 so an
install that recorded 6 before control_admitted existed re-runs the upgrade.

Retention coherence with the control gate: awaiting_control (pending or
checking) is not terminal and is never closed; a restored row recognised by a
tombstone is no longer deleted while it holds a lease (leased/running scan or a
control check in progress), so the one-at-a-time gate is never freed while its
traffic may be in flight. Tests cover both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant