Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
- conductor/1167b1-materializer-contracts
- conductor/1167b2-durable-managed-reporting
- conductor/reporting-receipt-ingress-b22
- conductor/reporting-frozen-account-feed-b23

# Default @adcp/sdk runner alias for storyboard jobs. Tracks the current
# stable @adcp/sdk release via the ``latest`` npm dist-tag.
Expand Down Expand Up @@ -523,7 +524,7 @@ jobs:
feed_tests=()
for test_file in tests/conformance/reporting/test_reporting_feed_*.py; do
case "$test_file" in
tests/conformance/reporting/test_reporting_feed_rolling.py|tests/conformance/reporting/test_reporting_feed_packaging.py|tests/conformance/reporting/test_reporting_feed_installed_pg.py) ;;
tests/conformance/reporting/test_reporting_feed_rolling.py|tests/conformance/reporting/test_reporting_feed_packaging.py|tests/conformance/reporting/test_reporting_feed_installed_pg.py|tests/conformance/reporting/test_reporting_feed_hardening_installed.py) ;;
*) feed_tests+=("$test_file") ;;
esac
done
Expand Down Expand Up @@ -605,7 +606,7 @@ jobs:
pg-reporting-feed-installed:
name: Installed frozen feed (Python 3.10 VCS and sdist)
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 50
permissions:
contents: read
services:
Expand All @@ -623,6 +624,9 @@ jobs:
--health-retries 10
steps:
- uses: actions/checkout@v6
- name: Fetch the exact integrated B2.3 hardening comparison artifact
timeout-minutes: 1
run: git fetch --no-tags origin 2d777ace7b4bf8be519ce0abd4fd0a25ed4f1da7
- uses: actions/setup-python@v6
id: feed-python310
with:
Expand All @@ -636,21 +640,25 @@ jobs:
run: pip install -e ".[dev,pg]"
- name: Run installed base and PostgreSQL restart cells
shell: bash
timeout-minutes: 20
timeout-minutes: 40
env:
ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_feed_installed_test
ADCP_PYTHON310: ${{ steps.feed-python310.outputs.python-path }}
ADCP_HARDENING_EVIDENCE: ${{ runner.temp }}/hardening-installed-evidence
run: |
python scripts/reporting_test_harness.py pytest \
tests/conformance/reporting/test_reporting_feed_packaging.py \
tests/conformance/reporting/test_reporting_feed_installed_pg.py \
tests/conformance/reporting/test_reporting_feed_hardening_installed.py \
-v -s -ra | tee pg-reporting-feed-installed-evidence.log
- name: Preserve installed origins, SQL, strict adopter and cold page evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: pg-reporting-feed-installed-evidence-${{ github.run_attempt }}
path: pg-reporting-feed-installed-evidence.log
path: |
pg-reporting-feed-installed-evidence.log
${{ runner.temp }}/hardening-installed-evidence
if-no-files-found: error

conventional-commits:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-title-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: PR Title Check
on:
pull_request:
types: [opened, edited, synchronize, reopened]
branches: [main, conductor/1167b2-durable-managed-reporting, conductor/reporting-receipt-ingress-b22]
branches: [main, conductor/1167b2-durable-managed-reporting, conductor/reporting-receipt-ingress-b22, conductor/reporting-frozen-account-feed-b23]

permissions:
contents: read
Expand Down
50 changes: 50 additions & 0 deletions docs/reporting-receipt-ingress.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,49 @@ Absent, anonymous, inactive or conflicting identities fail closed. Two consumers
in one account and one consumer in two accounts have independent batch keys and
receipt visibility.

## Private operator diagnostics

Unexpected storage/driver failures and unexpected account-resolver or custom-store
failures emit one structured ERROR on `adcp.reporting.receipts`. The private helper
records the original failure at the PostgreSQL `_storage_errors` boundary before
translation, or at the receipt handler boundary for an unexpected adopter failure.
The handler does not log an already translated `ReportingReceiptError` again.

The static message is `Receipt storage is unavailable`. Its only diagnostic
fields are:

- `code`: always `RECEIPT_STORAGE_UNAVAILABLE`.
- `boundary`: `handler`, `store.create_schema`, `store.receipt_ingestion_ready`,
`store.ingest_receipt_batch`, or `store.read_receipt_boundaries`.
- `exception_type`: the exception class name.
- `origin_module`, `origin_function`, `origin_line`: the deepest original source
coordinates, with only bounded ASCII identifiers/dotted module names accepted;
invalid names become `unknown`, and an absent traceback has line `0`.

No exception, traceback object, message, arguments, chain, frame locals or raw
traceback path is attached to the record. No request/batch/provider body, account
or consumer identity, receipt/idempotency/continuation identifier, SQL, bound
parameter, DSN, authentication value or financial data is a diagnostic field.
The helper creates a plain record without the ambient record factory or dynamic
task/thread/process names, so serializing the entire emitted `LogRecord` retains
this boundary. Operator handlers/filters must preserve that contract rather than
adding request context or raw exceptions.
If an operator logging sink raises, the buyer still receives the same safe
error; the SDK does not retry through another logger or expose the sink failure.

Expected `INVALID_REQUEST`, `UNAUTHORIZED`, `IDEMPOTENCY_CONFLICT`,
`RECEIPT_SCHEMA_UNREADY` and `RECEIPT_HISTORY_CORRUPT` remain silent operator paths.
Cancellation propagates unchanged and emits no operator error. The buyer receives
the same safe code and message as before, with an actually empty exception
cause/context; transport formatting and deliberate caller-owned `context` echo
remain unchanged. That echo is not part of the error diagnostic.

Use the static boundary, class and source coordinates to locate the failing SDK
or adopter seam. Repair connectivity or the installed schema, re-run readiness,
and restart using the rollout procedure below; retry the original immutable batch
and key after recovery. Do not enable raw exception/SQL logging to diagnose this
path or rewrite receipt history to make an error disappear.

## Wire and replay contract

Requests negotiate `adcp_version: "3.2-rc.6"`. This is the wire release spelling;
Expand Down Expand Up @@ -205,7 +248,14 @@ integrated artifacts remove those dependencies; a C-only cluster would hide
portability regressions, so the gates require URL and drivers without a locale
pin. The pre-`17ee407a` A, pre-`0f34c666` B, pre-`967b6e28` C, pre-`5487f2bd` B1
and pre-`3fd62121` B2.1 rolling exclusions must accompany release notes.

A's notification-readiness closure after C is compared on both sides and does
not excuse new regressions. Optional notifications may stay explicitly disabled;
complete polling still depends on the later B2.3/B2.4 components. Full buyer
adjustment automation and `client.reporting` remain named downstream #1172 work.

The schema-proof and receipt-diagnostic hardening comparison uses integrated
B2.3 commit `2d777ace`. It checks the unchanged safe buyer error and frozen-feed
restart boundary against that binary, while separately demonstrating its repeated
catalog work and absent operator diagnostics. It does not qualify earlier B2.3
snapshots; this pre-`2d777ace` comparison limit must also accompany release notes.
41 changes: 41 additions & 0 deletions docs/reporting-webhook-activity.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,47 @@ Relationship notification support is independent and supplies no evidence for
either flag. The memory implementation supports shared conformance vectors; it
cannot justify a durable claim.

## Schema proof lifetime and readiness recovery

`ReportingActivitySupport` remains a frozen public composition value. Its private
cache holds only a completed **positive schema proof**, scoped to that one support
instance, the exact B or B+C wiring identities, and the packaged required-object
manifest contracts. Separate support instances never share proof, even on the
same pool. Each pool must retain its deployment's database/search-path configuration;
do not change session search paths behind a serving support instance.

Cold concurrent discovery calls share one catalog scan and one pool checkout.
Composite B+C activity validates both required contracts once against that capture.
After success, discovery needs no catalog checkout, including when the pool is
busy. Only primitive proof state survives completion: synchronous startup
validation using `asyncio.run` can be followed by discovery on another event loop.
An overlapping cold validator on a different loop fails closed; finish startup
before serving. Canceling a discovery waiter does not cancel other waiters' scan.
False results, failed/canceled scans and exceptions never become positive proof.
The next call retries after repair.

Every request still recomputes its capability response and checks its claims,
exact component types, object identity, pool wiring, notification enablement,
B+C scheduling/union wiring, and account-listing/projector topology. A false
request claim does not warm the cache. Schema evidence grants no additional
materializer, status or higher-tier readiness and does not cache authorization.

For deployment changes, stop admission, drain workers and requests, migrate with
the deployment connection and `await ledger.create_schema()`, construct a fresh
support/server, validate readiness, then resume serving. Manifest validation is
read-only; it does not install schema or private fairness-bootstrap objects.
On readiness failure, keep admission closed, repair the required migration/object,
and retry validation. Preserve existing immutable history and pending-effect
recovery rules throughout rollback or restart.

For controlled BYO DDL/tests on an existing composition, drain callers, call
`support.invalidate_schema_validation()` **before** the DDL, complete the change,
then validate before admitting new work. Invalidation advances an epoch: a
previous in-flight scan cannot publish into the new epoch. A scan already in
progress may finish, but its caller cannot use that invalidated proof. Arbitrary
out-of-band DDL while serving is unsupported without this procedure or a fresh
support after drain/migrate/restart; discovery does not automatically detect it.

## Canonical consumer and account visibility

Call `resolve_reporting_consumer(auth_info=..., agent=...)` when registering a
Expand Down
7 changes: 7 additions & 0 deletions src/adcp/reporting/outbox/_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ async def validate_schema(connection: Any, *, activity: bool = False) -> None:
raise ReportingNotificationError(
"notification_schema_unready:catalog_unavailable"
) from None
_validate_schema_objects(installed, activity=activity)


def _validate_schema_objects(
installed: dict[str, dict[str, Any]], *, activity: bool = False
) -> None:
"""Apply the packaged B contract to an already captured catalog."""
if not REQUIRED_OBJECTS:
raise ReportingNotificationError("notification_schema_unready:manifest_missing")
for key, expected in REQUIRED_OBJECTS.items():
Expand Down
7 changes: 7 additions & 0 deletions src/adcp/reporting/outbox/status_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ async def validate_status_schema(
# separately; this manifest's activity switch validates only C objects.
await validate_schema(connection)
installed = await schema_objects(connection)
_validate_status_objects(installed, activity=activity, status=status)


def _validate_status_objects(
installed: dict[str, dict[str, Any]], *, activity: bool = False, status: bool = True
) -> None:
"""Apply only C's contract; the caller must also prove its B foundation."""
if not REQUIRED_STATUS_OBJECTS:
raise ReportingNotificationError("status_schema_unready:manifest_missing")
for key, expected in REQUIRED_STATUS_OBJECTS.items():
Expand Down
Loading
Loading