# DualCoach Task 15 Evidence

> Superseded for candidate identity by Task 16. This Task15 receipt-boundary
> evidence remains valid only for its recorded 70-path candidate; the refreshed
> Task16 manifest (`cceb2d95f7b7f71a9c44b7ded31c012356f8d7f4c26bff773ab9319c3dbc09f1`)
> requires explicit rebaseline approval before release review.

## Verdict

PASS — Telegram polling ingress is held behind a durable receipt gate; every
receipt/lock authority entry is now no-follow and fail-closed; and crash-redelivered
onboarding updates replay durable state without a second business mutation.

## Startup, observability, and privacy contract

Production startup was audited at all five PTB lifecycle call sites:

- `start_polling` at three call sites;
- `start_webhook` at one call site;
- `delete_webhook` at one call site.

Every site passes `drop_pending_updates=False`; no production site passes `True` or
relies on an omitted/default value. Polling additionally installs the durable receipt
gate described below. Webhook startup preserves Telegram-side backlog but does not
claim polling-style HTTP in-flight durability.

Ingress logs use the ordered stages:

```text
ingress -> authority -> route -> validation -> persistence -> publication -> receipt
```

Observed stable reason codes include:

| Stage | Reason codes |
|---|---|
| `ingress` | `unknown_update_shape` |
| `authority` | `member_unavailable`, `authority_unavailable`, `onboarding_state_unavailable` |
| `route` | `route_rejected` |
| `validation` | `callback_invalid`, `onboarding_session_unavailable`, `stale_publication`, `callback_identifier_unavailable`, `action_state_invalid`, `stale_reply`, `no_active_question`, `reconciliation_state_invalid`, `answer_invalid` |
| `persistence` | `transition_rejected` |
| `publication` | `publication_failed` |
| `receipt` | `handled`, `duplicate_business_replay`, `handler_exception`, `receipt_write_failed`, `receipt_store_corrupt` |

Each line is correlated by Telegram `update_id`. Task 14 ACK failures inherit the
same update context. The logs and receipt store contain no customer answer/message
text, callback payload or callback ID, actor/chat/message ID, profile data, token, or
secret. Sentinel-based callback, text, unknown-shape, failure, duplicate, and ACK
tests confirmed these values were absent.

The guarantee is deliberately precise:

- Telegram polling backlog is never dropped on startup.
- Once PTB fetches an update, its next advancing offset is blocked until a durable
  ingress receipt exists.
- Before that receipt, process failure leaves Telegram as the replay source.
- After receipt but before Telegram acknowledgement, replay skips business mutation
  and allows acknowledgement.
- Handler/store failure leaves the offset unadvanced and fails closed.
- Delivery is at-least-once into replay-safe DualCoach handlers, not a new claim of
  generic handler exactly-once or provider delivery exactly-once.

## Final receipt-authority remediation

`TelegramIngressReceiptStore` no longer uses `Path.exists()`, `Path.read_text()`, or
the shared `atomic_json_write` helper.

- The receipt parent is opened component-by-component with directory file
descriptors and `O_NOFOLLOW`; a symlinked parent cannot redirect the authority.
- Under the cross-process `fcntl.flock` lock, receipt and lock entries are classified
with `os.lstat(..., dir_fd=...)` and `FileNotFoundError` only.
- Both entries must be regular, single-link files with exact owner-only `0600` mode.
Dangling/existing symlinks, hard links, FIFO/nonregular files, and wrong modes fail
closed with `receipt_store_corrupt` or `receipt_lock_failed`.
- The receipt-specific writer creates a same-directory private temporary entry with
`O_CREAT|O_EXCL|O_NOFOLLOW`, writes, flushes, fsyncs, atomically `os.replace`s by
source/destination directory FD, fsyncs the directory, and removes the temporary
entry on every exit path. A raced destination symlink is replaced as a directory
entry, never followed.
- Lock acquisition itself uses no-follow open, exact regular/single-link/`0600`
validation, and exclusive `flock`. Process death releases the advisory lock; the
recovery writer proceeds normally.

No receipt authority state includes actor, callback, message, customer text, or token
content. The token is represented only by the existing SHA-256-derived receipt
filename suffix.

## Deterministic attack transcript

The 13-test receipt suite proves:

- dangling and existing-target receipt symlinks raise exactly
  `receipt_store_corrupt`; an existing outside target remains byte-identical and a
dangling outside target remains absent;
- hardlink, FIFO, and wrong-mode receipt entries raise exactly
  `receipt_store_corrupt`;
- dangling and existing-target lock symlinks raise exactly `receipt_lock_failed` and
never create or modify their outside target;
- a destination symlink inserted immediately before `os.replace` is atomically
replaced by the receipt file, with the outside target still absent;
- normal record, `forget_before`, spawned record/record, record/cleanup, and
crashed-lock-holder recovery retain `0600` and their original behavior.

Manual forked two-process probe transcript:

```text
two_process_receipts=811,812
dangling_symlink=receipt_store_corrupt
outside_created=false
```

## Replay and `UNCERTAIN` boundary

- Callback update `901`: `attest` commits `trainer_review`; forced post-transition
publication failure leaves no ingress receipt; restart finds one consumed update,
performs no second transition or confirmation, sends one trainer card, then records
one receipt.
- Text update `902`: one answer commits; forced post-transition publication failure
leaves no ingress receipt; restart finds one consumed update, performs no second
answer mutation, sends one next force-reply card, then records one receipt.
- `UNCERTAIN` remains inherited Task13 outbox state, not a provider success. It may
have sent; replaying it risks a duplicate Telegram publication. Task15 therefore
never blindly resends it and does not treat ingress receipt completion as a provider
delivery receipt.

## Pause test repair

The Task15-owned pause test proves two immediate no-argument ACKs occur before their
transitions, transitions and publications are each `['pause', 'pause']` and
`['card', 'card']`, mutations are `[True, False]`, paused state persists, and the
rendered control is a resume callback. Unrelated `topic=0` invite debt remains
untouched.

## Verification

- Receipt and multiprocess suite with warnings-as-errors: **16 passed**
  (13 receipt, 3 spawned-process).
- Affected warnings-as-errors regression suite: **242 passed**
  (13 receipt + 3 multiprocess + 46 onboarding + 9 callback observability + 21
  reconciliation + 150 nutrition coaching). The prior 237 baseline grew by the five
  new receipt attack cases.
- Changed receipt symbols/tests Ty check: zero diagnostics.
- Ruff, compileall, and `git diff --check`: PASS.
- `Path.exists`, shared-writer, newly-added `Any`, and cast searches in the receipt
  authority and receipt test: zero matches.

The direct-entry warnings-as-errors run remains intentionally out of scope: its four
test bodies pass, while pytest reports two unclosed Unix sockets and one unclosed
`_UnixSelectorEventLoop`. No Task15 test or source file owns that ResourceWarning.

## Candidate identity and digest lineage

- Task15 authoritative 70-path candidate digest:
  `63604403f7eb49ff849230b8b0801736fcea3f5e30e4530209f4810f527e01bf`
- Status snapshot SHA-256 (950 dirty entries):
  `a0ea4eb068fba614da52d31d8b6a8c28e3816da1323c41a9042efcd7008ad2d5`
- Manifest JSON SHA-256:
  `0fe343734a6a0701c12587cffd87e99d8bdf675d24603a49af5c3738b02b9da7`
- Manifest Markdown SHA-256:
  `75b6876c0dd1d5f6b2837265a9e49cf88ff4edc154799dc63003677c8f645cdf`

`bd3caeb434d0ba70534fd1c9b6261b9a03940903fb4a3bd01872f29ca9fa8409` was
the immediately preceding authoritative 70-path digest before this receipt-authority
remediation. The reported `52f6` prefix is not a field in either current
manifest; it is therefore not an alias for either the prior `bd3…` snapshot or the
current raw-byte identity. The candidate generator recomputes its digest from all 70
current raw files, so these identities are deliberately distinct.

## Cleanup and non-touch

All verification uses fake PTB seams, temporary files, or local spawned/forked
processes. No network, Telegram, provider, customer, activation, delivery, Task16,
or product UX action occurred. No commit, push, reset, stash, clean, or receipt
deletion occurred. Unrelated ResourceWarning/invite debt and shared build artifacts
remain preserved and outside the candidate.
