Exercise actual communication owner sources through isolated browser flows
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m57s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
tegwick 2026-10-30 21:46:33 +01:00
parent 54eef1c4b5
commit cb9301537e
12 changed files with 866 additions and 6 deletions

View file

@ -1,4 +1,4 @@
.PHONY: help sync migrate run test lint shell check test-usecases test-browser setup-usecases check-communication
.PHONY: help sync migrate run test lint shell check test-usecases test-browser setup-usecases check-communication check-owner-communication
.DEFAULT_GOAL := help
@ -39,3 +39,6 @@ check: lint ## Full lint/regression gate, including every established browser jo
check-communication: ## Send-free owner retry/privacy checks; set EMAIL_CONNECT_ROOT
uv run python scripts/check-communication.py --owner "$(EMAIL_CONNECT_ROOT)"
check-owner-communication: ## Real owner-source browser journey; set both owner roots and a fresh OWNER_BROWSER_RECEIPT
uv run python scripts/check-owner-communication.py --user-engine "$(USER_ENGINE_ROOT)" --email-connect "$(EMAIL_CONNECT_ROOT)" --receipt "$(OWNER_BROWSER_RECEIPT)"

View file

@ -0,0 +1,35 @@
{
"schema": "coulomb.owner-communication-browser.v1",
"passed": true,
"browser_node": "scripts/owner_communication/test_browser.py::test_real_owner_registration_and_invitation",
"source_manifest": "owner-communication-browser-2026-10-30.json.sources.txt",
"source_manifest_sha256": "fda78bda6ff6ae10f749596367e835b0bae34fb3313137a834f994a69b497fb5",
"source_file_count": 60,
"runner_sha256": "ff5a75eed45b3c313487041b7f01e9a98a4cf18c67beb72ead6b83804c942b5d",
"python_lock_sha256": "e8ebb6fc4b5abd1ac80f1c9012f1983642d0528a8a213cfcc394261606fad887",
"package_lock_sha256": "47802f261f4006a57a66dc29dac4896c81a5fd68857df36f82319d3255748346",
"checks": {
"passed": true,
"browser_requests_outside_fixture": 0,
"registration_verify_cancel_expire_replay": true,
"mailbox_not_authorization": true,
"tenant_client_caller_denials": true,
"real_invitation_and_resend": true,
"duplicate_delivery_and_suppression": true,
"provider_calls": 5
},
"production_access": false,
"smtp_provider_sends": false,
"synthetic_session_and_directory": true,
"productive_acceptance": false,
"native_identity_reset_verified": false,
"full_gate": {
"command": "make check",
"tests": 344,
"failures": 0,
"errors": 0,
"skipped": 0
},
"runner_safety_tests_sha256": "5747cbf3616c0aa0c1a602a152c72f28b276ed4e2a2bcab055775995e9920d49",
"synthetic_authorization_adapter": true
}

View file

@ -0,0 +1,60 @@
scripts/owner_communication/browser.mjs: c49d91cf3f4d106b6e9f5770f8b3b261e4aad1e6a26c8d1f9590dbd20f4972c6
scripts/owner_communication/test_browser.py: 4895483849feb84ea29f91eb5671004d052b142f5bdc4f7ed4c0eea6637e9d51
src/email_connect/__init__.py: 0f1ae5e9251f80b0bd5220b1d7bf91735d6ea273e07aa26cb7852a7950be7087
src/email_connect/adapter_contract.py: 5059b2eaaac2408f4e247bb468a97960eb3f823e63c3eeb151a1c2250e1f3bbf
src/email_connect/callers.py: 06fe961b9030dba2c0b26b92f8097e8fbd24e205d5fa79642aa9af60382886dc
src/email_connect/cli.py: 6ac6ae8acd091ba2d2102589be3b75fe3a48392309e40e61f8383deb3329ad42
src/email_connect/config.py: 6ca89fb7c3e1f35bc4faedfba8c3313d4366b2cb7830ef88ed3f7d2313f7867e
src/email_connect/evidence.py: 42cca03234a79dc78033ec1fbaf0b1785e6089ef3f3128e2e9d666ed239a3582
src/email_connect/mailbox.py: 7f0fe7e19ce1f814269609c4236152042f0f49bf3b517114b3b3f6f482471362
src/email_connect/models.py: 7f8136646b663a097280286dccce76a90abb7190ea0712fff7da32d47526521c
src/email_connect/parser.py: 786b291256ddfac5a8622c743622508cdd9f45f06156d0f5487ce399fad1287c
src/email_connect/recipients.py: 68792f44f0d2ba204649542aa8bd6b0f209d89c675baa4fc0767f1250a29045f
src/email_connect/reporting.py: 8f8ce5f98e4347b778893d08a9e58f2bab3b44b80c159fcf52d0b5772429737c
src/email_connect/scanner.py: 6266cbad2dbc610c1f6df1c051ded2ee55b43d7ff30af6eab38721924931c1ed
src/email_connect/storage.py: 57fbd6d3b2e7541c0bf52d87a4f7361a4ff4a03511620d2a1c3789632b73afeb
src/email_connect/transactional.py: 36fdfd8cb58f10148cd491a7e34d7aea798e4289fce760af70a48776bede204d
src/user_engine/__init__.py: 2da5f289343f1f5832fa2f5f26da8185b9828d306c81e89f0724b7875494abe8
src/user_engine/adapters/__init__.py: 5b8e63f648f228fdbd76f8c78c3b848ebd03cf0590521c8cf3a66dd85bdad461
src/user_engine/adapters/application_access.py: adf1fac707446f41b5859ba5102e820cd88f60af5daf20a42f13dfa2067d5481
src/user_engine/adapters/claims.py: e8944dbb1aee87e0ede28dc69500c5478cf866acba178777bd6ad08ea17213e0
src/user_engine/adapters/delivery.py: 5011bb77d191e0626f5f2986fe65404c319e363bc0ea90eeaf2072e67a24b11c
src/user_engine/adapters/flex_auth.py: 73d645b7b37904ebfe0df6f7ed397b74b4c47f505cd64a30c39e60717ce71455
src/user_engine/adapters/local.py: 3d7a5c7716edd0f2227a5d5bcb2760273a3ceb247f3bc9439397fbf829943888
src/user_engine/adapters/postgres.py: 528d75431d1f3a7e55423ffa55d34722ac343582e513985db74ba3add36e6da6
src/user_engine/adapters/provisioning.py: a033a891d8e0e2595d177877ac8615814190fe2c97b67059c7e3b63a87e25b51
src/user_engine/adapters/registration_verification.py: 88fa298f60ff374d7ddf84ce21d0c43fce6d301475ac95b293e8e1324e905a58
src/user_engine/adapters/tenant_management.py: 1ccaf275d102278b7081dd104d82292d3bf6bec0287d5943f739119294bc4de3
src/user_engine/application_access.py: 798f180474d98e1c6abacc7b8bfba3184d40ecb77d9ca90f6526cd4f774cf363
src/user_engine/authentication_policy.py: 50e13972a549e54aff97f5939c640b2ff3b4c6b4f072d7b430cfbfb50f3d079c
src/user_engine/custody.py: d37aaf292cbde24967f8e3546532fd05998eced1b6ac5a7e9a8a91bd9fc86357
src/user_engine/custody_bootstrap.py: 1af42f51ecf6909092ffbdd69dbb91b7b7ad61f34ade3178063cfaef06db2432
src/user_engine/custody_canon/__init__.py: f0629b13759b26256fd0e30fa5184a9e834005f49f1e54ec6f0b548d214a31c7
src/user_engine/custody_canon/custody-record_v0.1.schema.json: bb6cd6da5dc48eddcf118ae78c2cacfafd5f860098fd8a8368fe6bc845776d07
src/user_engine/custody_canon/policy.py: a1ba5a6f586a7d64e406aebcde783ab033db010be31c132593f07fb0a93a7bc4
src/user_engine/custody_canon/provenance.json: c39d53e119abfa208b2d098275a0d365d6bebee2764793f8f8fd5a8ca507c9d3
src/user_engine/custody_runtime.py: ceb689505424145df98865aeb8ce1229eb1abcf4a7a2019e632c23913feab190
src/user_engine/domain/__init__.py: 12fd0e760491e6e689372f3ea0e4d0b4b4db367554d1ed2e9bc0fe00e4b2bbd5
src/user_engine/domain/models.py: 8c9cb8051a930fa05a1657c8b539510588f1788812d68510df55712d2f4df050
src/user_engine/errors.py: 5724a7346e3680af7c1d6321704b21c5afcc3ede460a983d80d2aac9ee8e8746
src/user_engine/evidence.py: 973344ddbe4200c27b3f0fe809962bbe39804abc1b025fb1caf4895341f8f221
src/user_engine/factor_recovery.py: adbe69866e96de06b7cc8390607453138772e178d0b49fe81d0470e4d15c26f1
src/user_engine/identity_state.py: 3718254c6c12e7c5ec19312bf50a0b8f9c09f6859f5b6d4bd62032b5b890fe16
src/user_engine/layer_yaml.py: bd77be9a116dd61dfbe0e83efcf9e960bd1d2ef9059e831f2a6ba029448d94c7
src/user_engine/mail_bindings.py: 1afb0eab96fae41e4054a97c82f15a0e9c756acb4305da4bac6898465c30b786
src/user_engine/migrations.py: d957581bebedcdc6555411f8c9b00665a36e42c554bb623524bdfae7951608d4
src/user_engine/oidc.py: eeed38e75ca2948a4ffc9bce24f98a1395d9a2cc8152460b392b323afb0bc276
src/user_engine/operations_status.py: 21cb6feeca99371f08ae876b6a56ad0e2a50ff7023d62fda2c56dc4e00a14cbc
src/user_engine/pep_stance.py: 57045ddd161980fee269bb0e0f3ce5fe0f347b2526d57dfe72e44bf360562841
src/user_engine/ports.py: 5234d9c49bf1c51157fdfd3da3cfc967d05a8b463b617ace1a8b9033640ce322
src/user_engine/projections.py: f896833720026ae3b68437616936a37c5e6a3b6478b97bf76bebaa556283146c
src/user_engine/runtime.py: 536e77c1f0e578109dc6495015de742bac53333cf3560d686d67490d809d4dde
src/user_engine/service.py: 1c60b69e08c36eb3f678893a4a945eeeb9c73d5246c6566857cd8c134f75446d
src/user_engine/store_records.py: 3cec4575434e82e29932a3e9cc7659f3dd8fa7e5be81d5690487033796bbfc16
src/user_engine/testing/__init__.py: 2c9e372a16381edd0625774ef60f887fb0a4fb3f6b880fd347e72683aa99c9db
src/user_engine/testing/fixtures.py: b78b1663e2a00cf42a26b2bb57d2630ea84bf0760154881f536b70055b4e2a0b
src/user_engine/testing/postgres_provider.py: f846bcecdbd76e336e9d58644e88c3ce217498578ae4d2445d04699587f81873
src/user_engine/testing/scenarios.py: ef94c2d75c8d0f1d78d6b630bd478278e045e1594e5c29be434c305346fecee5
src/user_engine/testing/store_conformance.py: 7cb66cbd85df88839daedca18c18c450634e9573bf8831d71f2ee2d23d5daba9
src/user_engine/ui.py: 0e4202ac11b821e87e1ca7d03ae3715ea0101a51b23ab7dac76e7af1afe93034
src/user_engine/web.py: ae5fd2f41f643c804310d58c5a47d19ab0bc721634197110f684de72b94adeca

View file

@ -114,3 +114,68 @@ Actual owner-spanning browser integration, platform sender/inbound/reply/bounce
operations and productive UC-010/011 verdicts remain WP-0015-T02/T04.
UC-009 personal recovery acceptance is preserved; UC-012 remains deferred.
Details: ../release/acceptance-audit-2026-10-27.md.
## Owner source browser integration 2026-10-30
UC-010 and UC-011 now have an additional explicit owner-browser gate:
```sh
make check-owner-communication \
USER_ENGINE_ROOT=/path/to/user-engine \
EMAIL_CONNECT_ROOT=/path/to/email-connect \
OWNER_BROWSER_RECEIPT=/private/fresh-owner-browser-receipt.json
```
The exact required node is
`scripts/owner_communication/test_browser.py::test_real_owner_registration_and_invitation`.
It is linked by `owner_browser_tests` in the candidate use-case records. This
separate command needs the two owner checkouts; `make check` continues to protect
the established product journeys and tests the owner runner's isolation/failure
boundaries. The new check does not mark a candidate established or accepted.
The runner copies and hashes only the owners' Python sources and packaged public
custody schemas into a fresh private tree. It strips inherited configuration and
credentials, disables pytest plugin autoload, and confirms both imported packages
come from that snapshot. Existing owner configuration, databases and tests are
not loaded. Locked development dependencies include user-engine's jsonschema
dependency. The receipt binds the runner, Python lock, Playwright package lock,
owner source and both browser harness files.
Per-file hashes live in the adjacent `.sources.txt` filename/digest manifest,
whose hash is bound in the JSON receipt. Retain both files together. This is the
receive scanner's approved content-digest shape; it adds no scanner exception.
Chromium submits user-engine's actual registration/confirmation forms. The real
registration HTTP adapter calls email-connect's actual scoped receiver and SQLite
ledger. A synthetic local mail sink exposes the verification/cancellation links;
the directory and preauthenticated administrative session are fixtures. The
authorization adapter is explicitly the owner's standalone test double, not a
production PDP. The password setup handoff is shown but not followed. No native
Authelia reset, directory password change, app sign-in or MFA ceremony is proved
by this node; UC-009 retains its separate personal acceptance and isolated-double
test.
The browser verifies that mailbox delivery alone creates no applicant User or
directory identity; correct confirmation creates one identity. A second caller
cannot consume a fresh valid handle that the correct caller can subsequently
consume. Reused, cancelled and expired handles refuse, as do wrong tenant/client
bindings and an invalid caller token. The administrative form refuses another
tenant. Real invitation forms and the actual outbox HTTP adapter prove one local
provider call per invitation event, a distinct resend, repeated delivery without
another send, and recipient suppression. The event sink asserts that recipient
addresses are removed from the activity envelope.
Python connections are restricted to the exact admitted loopback service ports,
with external-connect and DNS refusal probes. Browser routing permits only the
three fixture origins, blocks service workers and rejects other origins. Browser
API requests also validate their origin and disable redirects. The mail provider
is an injected in-memory sink; no SMTP provider or live mailbox is contacted.
Failed check diagnostics stay in a private temporary file. Receipts require the
exact unskipped node and contain hashes/counts/verdicts, not usable handles or
message bodies. Safety tests prove inherited credentials, runtime databases and
`.env` files are excluded, and skipped/wrong-node reports and symlinked sources
cannot pass.
Evidence: `../release/owner-communication-browser-2026-10-30.json`. Productive
UC-010/011 verdicts and platform sender/reply/bounce operations remain WP-0015.
UC-012 remains deferred pending a selected template/caller; UC-008 manual mail
distribution is unchanged.

View file

@ -85,8 +85,12 @@
},
"owner_regression_contract": "docs/usecases/communication-regressions.json",
"manual_review": [
"Loopback registration sink is bound; real verification admission remains open"
]
"Owner-source portal/HTTP adapters/scoped receiver browser gate passes; synthetic session, authorization and directory adapters remain explicit. Productive per-flow verdict remains required."
],
"owner_browser_tests": [
"scripts/owner_communication/test_browser.py::test_real_owner_registration_and_invitation"
],
"owner_browser_command": "make check-owner-communication"
},
{
"id": "CSOC-UC-011",
@ -107,8 +111,12 @@
},
"owner_regression_contract": "docs/usecases/communication-regressions.json",
"manual_review": [
"Loopback invitation double is bound; real caller admission remains open"
]
"Owner-source portal/HTTP adapters/scoped receiver browser gate passes; synthetic session, authorization and directory adapters remain explicit. Productive per-flow verdict remains required."
],
"owner_browser_tests": [
"scripts/owner_communication/test_browser.py::test_real_owner_registration_and_invitation"
],
"owner_browser_command": "make check-owner-communication"
},
{
"id": "CSOC-UC-012",

View file

@ -22,6 +22,7 @@ dev = [
"pytest-django>=4.8",
"pytest-cov>=5.0",
"ruff>=0.4",
"jsonschema>=4.10,<5",
]
[tool.pytest.ini_options]

View file

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Browser-check exact owner source snapshots without production configs or sends."""
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
NODE = "scripts/owner_communication/test_browser.py::test_real_owner_registration_and_invitation"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--user-engine", type=Path, required=True)
parser.add_argument("--email-connect", type=Path, required=True)
parser.add_argument("--receipt", type=Path, required=True)
args = parser.parse_args()
manifest_path = args.receipt.with_name(args.receipt.name + ".sources.txt")
if any(p.exists() or p.is_symlink() for p in (args.receipt, manifest_path)):
raise ValueError("Fresh receipt required")
hashes = {}
with tempfile.TemporaryDirectory(prefix="csoc-owner-browser-") as temporary:
work = Path(temporary)
for owner, package in [(args.user_engine, "user_engine"),
(args.email_connect, "email_connect")]:
source = owner.resolve(strict=True) / "src" / package
if (not source.is_dir() or source.is_symlink()
or not (source / "__init__.py").is_file()):
raise ValueError("Real owner source directory required")
paths = sorted(source.rglob("*.py"))
if package == "user_engine":
paths += sorted((source / "custody_canon").glob("*.json"))
for path in paths:
if not path.is_file() or any(p.is_symlink() for p in [path, *path.parents]):
raise ValueError("Owner source symlink refused")
name = "src/" + path.relative_to(source.parent).as_posix()
data = path.read_bytes()
hashes[name] = hashlib.sha256(data).hexdigest()
target = work / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
for name in ["scripts/owner_communication/test_browser.py",
"scripts/owner_communication/browser.mjs"]:
data = (ROOT / name).read_bytes()
hashes[name] = hashlib.sha256(data).hexdigest()
target = work / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
config = work / "pytest.ini"
config.write_text("[pytest]\nmarkers =\n browser: real browser\n"
" usecase(id): candidate contract\n")
report, checks = work / "result.xml", work / "checks.json"
result = subprocess.run(
[sys.executable, "-m", "pytest", "-q", "-c", str(config), NODE,
"-p", "no:cacheprovider", "--junitxml", str(report)],
cwd=work, capture_output=True, timeout=200,
env={"PATH": os.defpath, "PYTHONPATH": str(work / "src"),
"PYTHONDONTWRITEBYTECODE": "1", "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1",
"OWNER_NODE": shutil.which("node") or "node",
"OWNER_BROWSER_SCRIPT": str(work / "scripts/owner_communication/browser.mjs"),
"OWNER_PLAYWRIGHT_MODULE": str(ROOT / "scripts/node_modules/playwright/index.mjs"),
"OWNER_CHECKS_RESULT": str(checks)})
cases = ET.parse(report).findall(".//testcase") if report.exists() else []
passed = (result.returncode == 0 and len(cases) == 1 and checks.exists()
and cases[0].attrib.get("name") == NODE.split("::")[-1]
and not any(c.findall("failure") or c.findall("error") or c.findall("skipped")
for c in cases))
manifest = "".join(f"{name}: {digest}\n"
for name, digest in sorted(hashes.items())).encode()
proof = {"schema": "coulomb.owner-communication-browser.v1", "passed": passed,
"browser_node": NODE, "source_manifest": manifest_path.name,
"source_manifest_sha256": hashlib.sha256(manifest).hexdigest(),
"source_file_count": len(hashes),
"runner_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"python_lock_sha256": hashlib.sha256((ROOT / "uv.lock").read_bytes()).hexdigest(),
"package_lock_sha256": hashlib.sha256(
(ROOT / "scripts/package-lock.json").read_bytes()).hexdigest(),
"checks": json.loads(checks.read_bytes()) if passed else {},
"production_access": False, "smtp_provider_sends": False,
"synthetic_session_and_directory": True,
"productive_acceptance": False, "native_identity_reset_verified": False}
if not passed:
fd, diagnostic = tempfile.mkstemp(prefix="csoc-owner-browser-private-diagnostics-")
with os.fdopen(fd, "wb") as stream:
stream.write(result.stdout + result.stderr)
proof["private_diagnostics"] = diagnostic
fd = os.open(manifest_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "wb") as stream:
stream.write(manifest)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as stream:
json.dump(proof, stream, indent=2)
stream.write("\n")
public = {"passed": passed, "browser_node": NODE}
if not passed:
public["private_diagnostics"] = proof["private_diagnostics"]
print(json.dumps(public))
return 0 if passed else 1
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, KeyError, subprocess.SubprocessError, ET.ParseError):
raise SystemExit("Owner browser check refused; inspect private inputs/diagnostics locally.")

View file

@ -0,0 +1,123 @@
import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
const [portal, fixtures, receiver, modulePath] = process.argv.slice(2);
const allowed = new Set([portal, fixtures, receiver]);
for (const base of allowed) {
const url = new URL(base);
assert.equal(url.hostname, '127.0.0.1');
assert.equal(url.protocol, 'http:');
}
const { chromium } = await import(pathToFileURL(modulePath).href);
const browser = await chromium.launch({ headless: true });
let outside = 0;
const context = await browser.newContext({ serviceWorkers: 'block' });
await context.route('**/*', route => {
if (!allowed.has(new URL(route.request().url()).origin)) {
outside += 1;
return route.abort();
}
return route.continue();
});
const page = await context.newPage();
page.setDefaultTimeout(15000);
const request = async (method, url, options = {}) => {
assert(allowed.has(new URL(url).origin));
return context.request[method](url, { ...options, maxRedirects: 0 });
};
const state = async () => (await request('get', fixtures + '/state')).json();
const control = async (path, data = {}) => {
const response = await request('post', fixtures + path, { data });
assert.equal(response.status(), 200);
};
async function register(name) {
await page.goto(portal + '/register');
await page.getByLabel('Username', { exact: true }).fill(name);
await page.getByLabel('Email', { exact: true }).fill(name + '@example.test');
await page.getByRole('button', { name: 'Send verification email' }).click();
await page.getByText('Check your email', { exact: false }).first().waitFor();
await page.goto(fixtures + '/mailbox');
const body = await page.locator(`article[data-recipient="${name}@example.test"]`).innerText();
const url = body.match(/http:\/\/127\.0\.0\.1:\d+\/registration\/verify\?handle=[A-Za-z0-9_-]+/)[0];
assert.equal(new URL(url).origin, portal);
return url;
}
async function consume(url, button, accepted) {
assert.equal(new URL(url).origin, portal);
await page.goto(url);
const response = page.waitForResponse(r => r.url().startsWith(portal + '/registration/')
&& r.request().method() === 'POST');
await page.getByRole('button', { name: button, exact: true }).click();
const result = await response;
assert.equal(result.status() < 400, accepted);
}
try {
const verified = await register('verified.person');
assert.equal((await state()).provisions, 0); // mail receipt alone cannot provision
assert.equal((await state()).users, 1); // only the synthetic inviter exists
const wrongCaller = await request('post', receiver + '/v1/registration-verifications/consume',
{ headers: { Authorization: 'Bearer synthetic-other-caller-token' },
data: { handle: new URL(verified).searchParams.get('handle') } });
assert.equal(wrongCaller.status(), 400);
await consume(verified, 'Verify and continue', true);
const passwordLink = page.getByRole('link', { name: 'Create password', exact: true });
assert.equal(await passwordLink.count(), 1, await page.locator('body').innerText());
assert.equal((await state()).provisions, 1);
assert.equal((await state()).users, 2);
await consume(verified, 'Verify and continue', false);
const canceled = await register('canceled.person');
await consume(canceled.replace('/verify?', '/cancel?'), 'Cancel registration', true);
await consume(canceled, 'Verify and continue', false);
const expired = await register('expired.person');
await control('/expire', { email: 'expired.person@example.test' });
await consume(expired, 'Verify and continue', false);
assert.equal((await state()).provisions, 1);
const headers = { Authorization: 'Bearer synthetic-user-engine-mail-token' };
const challenge = { registration_id: 'reg-wrong-binding',
normalized_email: 'denied@example.test', preferred_username: 'denied.person',
client_id: 'coulomb-social-test', tenant: 'tenant:communication-test', correlation_id: 'test' };
for (const bad of [{ tenant: 'tenant:other' }, { client_id: 'client-other' }]) {
const response = await request('post', receiver + '/v1/registration-verifications',
{ headers, data: { ...challenge, ...bad } });
assert.equal(response.status(), 400);
}
const missingCaller = await request('post', receiver + '/v1/registration-verifications',
{ headers: { Authorization: 'Bearer invalid-caller-token' }, data: challenge });
assert.equal(missingCaller.status(), 401);
assert.equal((await state()).messages, 3);
// Synthetic preauthenticated MFA session; real portal form and role/CSRF checks.
await context.addCookies([{ name: 'ue_session', value: 'synthetic-admin-session',
url: portal, httpOnly: true, sameSite: 'Lax' }]);
assert.equal((await request('get', portal + '/admin/tenant%3Aother')).status(), 403);
const admin = portal + '/admin/tenant%3Acommunication-test';
await page.goto(admin);
const form = page.locator('form[action$="/invitations"]');
await form.getByLabel('Email', { exact: true }).fill('invited@example.test');
await form.getByRole('button', { name: 'Send invitation', exact: true }).click();
await control('/deliver');
assert.equal((await state()).messages, 4);
await control('/deliver');
assert.equal((await state()).messages, 4);
await page.reload();
await page.getByRole('button', { name: 'Resend', exact: true }).click();
await control('/deliver');
assert.equal((await state()).messages, 5);
await control('/suppress', { email: 'suppressed@example.test' });
await page.goto(admin);
await form.getByLabel('Email', { exact: true }).fill('suppressed@example.test');
await form.getByRole('button', { name: 'Send invitation', exact: true }).click();
await control('/deliver');
assert.equal((await state()).messages, 5);
assert.equal((await state()).provisions, 1);
assert.equal(outside, 0);
console.log(JSON.stringify({ passed: true, browser_requests_outside_fixture: outside,
registration_verify_cancel_expire_replay: true, mailbox_not_authorization: true,
tenant_client_caller_denials: true, real_invitation_and_resend: true,
duplicate_delivery_and_suppression: true, provider_calls: 5 }));
} finally {
await browser.close();
}

View file

@ -0,0 +1,199 @@
"""Real owner portal/adapters/receiver with synthetic session, directory and mail sink."""
import html
import json
import os
import socket
import subprocess
import threading
import time
from contextlib import contextmanager
from pathlib import Path
from socketserver import ThreadingMixIn
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer, make_server
import pytest
TENANT = "tenant:communication-test"
CLIENT = "coulomb-social-test"
TOKEN = "synthetic-user-engine-mail-token"
class QuietHandler(WSGIRequestHandler):
def log_message(self, *args):
pass
class FixtureServer(ThreadingMixIn, WSGIServer):
daemon_threads = True
@contextmanager
def server(app, ports):
httpd = make_server("127.0.0.1", 0, app, server_class=FixtureServer,
handler_class=QuietHandler)
ports.add(httpd.server_port)
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{httpd.server_port}"
finally:
httpd.shutdown()
httpd.server_close()
thread.join(timeout=5)
@pytest.mark.browser
@pytest.mark.usecase("CSOC-UC-010")
@pytest.mark.usecase("CSOC-UC-011")
def test_real_owner_registration_and_invitation(tmp_path, monkeypatch):
ports = set()
connect, resolve = socket.socket.connect, socket.getaddrinfo
def guarded_connect(sock, address):
if (not isinstance(address, tuple) or address[0] != "127.0.0.1"
or address[1] not in ports):
raise RuntimeError("Owner harness refuses non-fixture connections")
return connect(sock, address)
def guarded_resolve(host, port, *args, **kwargs):
if host != "127.0.0.1" or int(port) not in ports:
raise RuntimeError("Owner harness refuses DNS/non-fixture connections")
return resolve(host, port, *args, **kwargs)
monkeypatch.setattr(socket.socket, "connect", guarded_connect)
monkeypatch.setattr(socket.socket, "connect_ex", guarded_connect)
monkeypatch.setattr(socket, "getaddrinfo", guarded_resolve)
monkeypatch.setattr(socket, "getfqdn", lambda *args: "localhost")
with pytest.raises(RuntimeError), socket.socket() as denied:
denied.connect(("203.0.113.1", 443))
with pytest.raises(RuntimeError):
socket.getaddrinfo("provider.example.test", 443)
import email_connect
import user_engine
from email_connect.callers import CallerPolicy, ScopedTransactionalApplication
from email_connect.transactional import SQLiteDeliveryStore
from user_engine.adapters.delivery import HTTPOutboxDeliveryAdapter
from user_engine.adapters.local import InMemoryUserEngineStore, LocalAuthorizationCheckPort
from user_engine.adapters.registration_verification import HTTPRegistrationVerificationAdapter
from user_engine.oidc import BrowserSession, OIDCClient
from user_engine.ports import ProvisioningResult
from user_engine.service import UserEngineService
from user_engine.testing.fixtures import FixtureIdentityClaimsAdapter, human_actor_claims
from user_engine.web import PortalApplication
snapshot = Path(os.environ["PYTHONPATH"]).resolve()
assert all(Path(package.__file__).resolve().is_relative_to(snapshot)
for package in (email_connect, user_engine))
messages, provisions, events = [], [], []
store = InMemoryUserEngineStore()
store.migrate()
service = UserEngineService(store=store, identity_adapter=FixtureIdentityClaimsAdapter(),
authorization=LocalAuthorizationCheckPort())
ledger = SQLiteDeliveryStore(str(tmp_path / "mail.db"))
class MailSink:
def send(self, recipient, subject, body):
assert recipient.endswith("@example.test")
messages.append({"recipient": recipient, "subject": subject, "body": body})
return "synthetic-provider-reference-" + str(len(messages))
def check(self):
pass
class Directory:
def provision(self, request):
provisions.append(request)
return ProvisioningResult("synthetic-directory", request.preferred_username,
"password_setup_required",
password_setup_url="https://directory.example.test/setup")
oidc = OIDCClient(issuer="https://issuer.example.test", client_id=CLIENT,
redirect_uri="https://portal.example.test/callback", audience=CLIENT)
claims = human_actor_claims(tenant=TENANT, subject="synthetic-inviter")
claims["roles"] = ["tenant-admin"]
service.me(claims)
oidc.sessions["synthetic-admin-session"] = BrowserSession(
claims=claims, expires_at=time.time() + 3600, csrf_token="synthetic-admin-csrf")
operator = service.identity_adapter.normalize(human_actor_claims(
tenant="platform:root", subject="synthetic-delivery-operator"))
portal = PortalApplication(service, trusted_proxy_secret="synthetic-proxy-secret-long-enough",
login_url="https://issuer.example.test/login", oidc_client=oidc,
provisioning=Directory(), registration_clients=(CLIENT,),
registration_tenants=(TENANT,),
registration_oidc_issuer="https://issuer.example.test",
registration_password_setup_origins=("https://directory.example.test",))
def response(start, payload, content_type="application/json", status="200 OK"):
data = payload.encode() if isinstance(payload, str) else json.dumps(payload).encode()
start(status, [("Content-Type", content_type), ("Content-Length", str(len(data)))])
return [data]
def fixtures(env, start):
path = env["PATH_INFO"]
if path == "/mailbox":
body = "<h1>Synthetic mailbox</h1>" + "".join(
f'<article data-recipient="{html.escape(m["recipient"])}"><pre>'
+ html.escape(m["body"]) + "</pre></article>" for m in messages)
return response(start, body, "text/html")
if path == "/state":
return response(start, {"messages": len(messages), "provisions": len(provisions),
"events": len(events),
"users": service.operability_snapshot().metrics["users"]})
if env["REQUEST_METHOD"] != "POST":
return response(start, {}, status="404 Not Found")
raw = env["wsgi.input"].read(int(env.get("CONTENT_LENGTH") or 0))
data = json.loads(raw or b"{}")
if path == "/events":
assert env.get("HTTP_AUTHORIZATION") == "Bearer synthetic-event-token"
assert "primary_email" not in data["data"]
events.append(data)
elif path == "/deliver":
service.deliver_outbox(operator, portal.outbox_delivery, worker_id="fixture-worker")
elif path == "/expire":
with ledger.lock, ledger.db:
ledger.db.execute("UPDATE verifications SET expires_at=? WHERE email=?",
("2000-01-01T00:00:00+00:00", data["email"]))
elif path == "/suppress":
ledger.suppress(data["email"])
else:
return response(start, {}, status="404 Not Found")
return response(start, {"ok": True})
with server(fixtures, ports) as fixture_url, server(portal, ports) as portal_url:
policies = [CallerPolicy(
caller_id=name,
token=TOKEN if name == "user-engine" else "synthetic-other-caller-token",
source="user-engine", bindings=frozenset({(TENANT, CLIENT)}),
operations=frozenset({"send", "status", "delivery-status",
"verify", "consume", "cancel"}),
templates=frozenset({"family_member.invited", "family_invitation.resent"}),
rate_per_minute=120, burst=100, max_payload_bytes=32768,
) for name in ("user-engine", "other-caller")]
receiver = ScopedTransactionalApplication(ledger, MailSink(), portal_url, policies)
with server(receiver, ports) as mail_url:
portal.registration_verification = HTTPRegistrationVerificationAdapter(
base_url=mail_url, bearer_token=TOKEN)
portal.outbox_delivery = HTTPOutboxDeliveryAdapter(
event_url=fixture_url + "/events", event_bearer_token="synthetic-event-token",
mail_url=mail_url + "/v1/send", mail_bearer_token=TOKEN,
mail_client_bindings=json.dumps([[TENANT, CLIENT]]))
result = subprocess.run(
[os.environ["OWNER_NODE"], os.environ["OWNER_BROWSER_SCRIPT"],
portal_url, fixture_url, mail_url, os.environ["OWNER_PLAYWRIGHT_MODULE"]],
capture_output=True, text=True, timeout=150)
assert result.returncode == 0, result.stdout + result.stderr
checks = json.loads(result.stdout)
assert len(provisions) == 1
assert provisions[0].preferred_username == "verified.person"
assert all(p.primary_email.endswith("@example.test") for p in provisions)
with ledger.lock:
assert ledger.db.execute("SELECT count(*) FROM verifications WHERE consumed_at IS NOT NULL"
).fetchone()[0] == 1
assert ledger.db.execute("SELECT count(*) FROM verifications WHERE canceled_at IS NOT NULL"
).fetchone()[0] == 1
ledger.db.close()
assert checks["passed"] and checks["browser_requests_outside_fixture"] == 0
Path(os.environ["OWNER_CHECKS_RESULT"]).write_text(json.dumps(checks))

View file

@ -0,0 +1,79 @@
"""The explicit owner-browser runner must isolate configuration and refuse weak reports."""
import importlib.util
import json
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[1]
def runner():
spec = importlib.util.spec_from_file_location("owner_browser_gate", ROOT / "scripts"
/ "check-owner-communication.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.mark.parametrize("report_kind", ["passed", "skipped", "wrong_node"])
def test_snapshot_strips_credentials_runtime_data_and_requires_exact_unskipped_node(
tmp_path, monkeypatch, report_kind):
module = runner()
owners = []
for package in ("user_engine", "email_connect"):
owner = tmp_path / package
source = owner / "src" / package
source.mkdir(parents=True)
(source / "__init__.py").write_text("# synthetic owner source\n")
(owner / ".env").write_text("SMTP_PASSWORD=synthetic-not-to-inherit")
(source / "runtime.db").write_bytes(b"synthetic-private-database")
owners.append(owner)
output = tmp_path / "receipt.json"
monkeypatch.setattr(sys, "argv", ["gate", "--user-engine", str(owners[0]),
"--email-connect", str(owners[1]),
"--receipt", str(output)])
monkeypatch.setenv("SMTP_PASSWORD", "synthetic-not-to-inherit")
monkeypatch.setenv("USER_ENGINE_FLEX_AUTH_TOKEN_FILE", "/synthetic/production-config")
def execute(argv, *, cwd, env, **kwargs):
assert "SMTP_PASSWORD" not in env and "USER_ENGINE_FLEX_AUTH_TOKEN_FILE" not in env
assert not list(cwd.rglob(".env")) and not list(cwd.rglob("*.db"))
assert argv[argv.index("-c") + 1] == str(cwd / "pytest.ini")
assert module.NODE in argv
node = module.NODE.split("::")[-1] if report_kind != "wrong_node" else "another_test"
skipped = "<skipped/>" if report_kind == "skipped" else ""
(cwd / "result.xml").write_text(
f'<testsuites><testsuite><testcase name="{node}">{skipped}</testcase>'
'</testsuite></testsuites>')
Path(env["OWNER_CHECKS_RESULT"]).write_text(json.dumps({"passed": True}))
return SimpleNamespace(returncode=0, stdout=b"", stderr=b"")
monkeypatch.setattr(module.subprocess, "run", execute)
assert module.main() == (0 if report_kind == "passed" else 1)
proof = json.loads(output.read_bytes())
assert proof["passed"] == (report_kind == "passed")
assert not proof["production_access"] and not proof["productive_acceptance"]
assert output.stat().st_mode & 0o777 == 0o600
manifest = output.with_name(proof["source_manifest"]).read_text()
assert ".env:" not in manifest and ".db:" not in manifest
with pytest.raises(ValueError, match="Fresh receipt"):
module.main()
def test_symlinked_owner_source_is_refused_before_any_execution(tmp_path, monkeypatch):
module = runner()
source = tmp_path / "owner/src/user_engine"
source.mkdir(parents=True)
(source / "__init__.py").write_text("# synthetic package")
(tmp_path / "private.py").write_text("# not owner source")
(source / "linked.py").symlink_to(tmp_path / "private.py")
monkeypatch.setattr(sys, "argv", ["gate", "--user-engine", str(tmp_path / "owner"),
"--email-connect", str(tmp_path / "missing"),
"--receipt", str(tmp_path / "receipt.json")])
monkeypatch.setattr(module.subprocess, "run", lambda *a, **kw: pytest.fail("must not execute"))
with pytest.raises(ValueError, match="symlink refused"):
module.main()

160
uv.lock generated
View file

@ -23,6 +23,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478 },
]
[[package]]
name = "attrs"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 },
]
[[package]]
name = "authlib"
version = "1.7.2"
@ -170,6 +179,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "jsonschema" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-django" },
@ -192,6 +202,7 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "jsonschema", specifier = ">=4.10,<5" },
{ name = "pytest", specifier = ">=8.0" },
{ name = "pytest-cov", specifier = ">=5.0" },
{ name = "pytest-django", specifier = ">=4.8" },
@ -452,6 +463,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/bf/249dcd99b3376375910b7fa922383b57792975c8758f50d44612e749226c/joserfc-1.7.4-py3-none-any.whl", hash = "sha256:32d46c2cd5e3203c13e87a6c61333cab310b1ba80cd54b4c4f386a848a122463", size = 71000 },
]
[[package]]
name = "jsonschema"
version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
]
[[package]]
name = "markdown"
version = "3.10.3"
@ -606,6 +644,128 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/d4/9193206c4563ec771faf2ccf54815ca7918529fe81f6adb22ee6d0e06622/python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66", size = 9947 },
]
[[package]]
name = "referencing"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 },
]
[[package]]
name = "rpds-py"
version = "2026.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/34/a828586ea3329fbb50895b50e9cf98ca3d924a9f41a0b26d443bff1b3794/rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af", size = 346822 },
{ url = "https://files.pythonhosted.org/packages/90/81/ac6a0d064982251856ce009c9e1dd51a34110b3c055aa7d1aad18b4899a3/rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b", size = 341380 },
{ url = "https://files.pythonhosted.org/packages/42/ff/bf7d54f362748fd6a49277b9d6531c394074110fedf50ad791ec59133fbb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a", size = 373053 },
{ url = "https://files.pythonhosted.org/packages/60/de/74b0ccdbbd28687b9b5fdb34c1cabed88352182facaced9d6f5486b8b9ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07", size = 377742 },
{ url = "https://files.pythonhosted.org/packages/c2/6d/b979775a3057b2a5c26d75ecbff60a20a24ef081db6dd76836fca3f20247/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69", size = 487289 },
{ url = "https://files.pythonhosted.org/packages/1d/5d/7c34734ce3ece943d9d6ee0122a74a21bbc75b47acb5b7053c83f6efb1ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d", size = 390710 },
{ url = "https://files.pythonhosted.org/packages/1d/6d/b26eb1e75395925b3a142ed351cbed2ec8212f5c9d937aee3df32c701baa/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab", size = 371261 },
{ url = "https://files.pythonhosted.org/packages/7c/98/b2fdfe10301a9e27af21e634337fbba7baac0bc17fe44619a17c26bba2cb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e", size = 376075 },
{ url = "https://files.pythonhosted.org/packages/c5/b1/c4b8d954e49c3c69cf8063c0c9e0919c99f3d2cecc46311606a1cbe835dc/rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237", size = 399358 },
{ url = "https://files.pythonhosted.org/packages/b3/59/9559a7293c97dff0cbb293efffbd36644103883ecc27741af8ef90c84ca8/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3", size = 550267 },
{ url = "https://files.pythonhosted.org/packages/6c/9d/6dc60e49511de4c8b00e74f9c1d43aed4d27f712cdb1ade92f2c199cbf64/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc", size = 614018 },
{ url = "https://files.pythonhosted.org/packages/fb/bc/92a4ecf27301b886232a413360f6b348a81d55ffef654f2ebda5970ebc3d/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6", size = 578509 },
{ url = "https://files.pythonhosted.org/packages/08/53/efb97f2e6589b7ab8394385a73870e0b2ab2a195281c117b21fd6d7d4855/rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3", size = 205401 },
{ url = "https://files.pythonhosted.org/packages/1c/84/1700cc748d0eaa747486e4e82882d2575224e6e9a3148df583058850c629/rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f", size = 222923 },
{ url = "https://files.pythonhosted.org/packages/c3/89/0302215373c2f8b4408b0fdfee955368cc378a98ff59508d47a7e0dc2dbf/rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02", size = 218415 },
{ url = "https://files.pythonhosted.org/packages/83/ea/ee88fd9e756ff93fb6b1182a47ec09504a242620e33ce1d20679efefe841/rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9", size = 346229 },
{ url = "https://files.pythonhosted.org/packages/57/71/a097d6552f837500fc36e6b23d09cfb9890c3cc47531f9ca64e149799615/rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858", size = 340514 },
{ url = "https://files.pythonhosted.org/packages/bd/b7/497e85768bf4e0d8ddbaa096a4cac31d1509251dee2728a8490aa367e0b5/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063", size = 372611 },
{ url = "https://files.pythonhosted.org/packages/52/4b/74ab4108916250b6e198e0d3af05bc6835eb046315f22f7a0ceb49667c5a/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025", size = 377712 },
{ url = "https://files.pythonhosted.org/packages/0c/8e/067e77d9d7b3cc793c9d909b7e97e7aadbbb1fb094093b6876902cc96d38/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5", size = 488106 },
{ url = "https://files.pythonhosted.org/packages/3c/b4/c5aae6c2dde269bf955f6b7d35065c655a57e47750d9668052ad67e74dda/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0", size = 390795 },
{ url = "https://files.pythonhosted.org/packages/a0/36/76fab39973ee11e7f9f357c55138197bb01c86f6502cb76487e3b4f42db0/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b", size = 370794 },
{ url = "https://files.pythonhosted.org/packages/3d/fe/cd2a80e6d7b871937a60e935c5d507aa390d143f4ff3636f640b9733d5df/rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33", size = 375673 },
{ url = "https://files.pythonhosted.org/packages/6c/18/7464a9953724e55a3b3206062fa0ffdeaa519584c6aabf65d3956d94f131/rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83", size = 398758 },
{ url = "https://files.pythonhosted.org/packages/c0/86/1534b436700fd49ff411063b7c4d7e938adfabf90895b6cf1622d5a7d1f6/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c", size = 550370 },
{ url = "https://files.pythonhosted.org/packages/57/1c/e1fa82a8a01e3c5820f3ba98a8b2673f642128eb368fa88871b01dd2c909/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36", size = 613106 },
{ url = "https://files.pythonhosted.org/packages/29/55/b20b8c4c3dde8755bfcd5b08492a02d0cd2e26929aedf6199ca2a377d42a/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d", size = 578235 },
{ url = "https://files.pythonhosted.org/packages/55/42/df3f7bbc3f7ab37a8a9db8d6c2ff2c985422899f1f7926afbf7ec3c0b8b4/rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c", size = 205293 },
{ url = "https://files.pythonhosted.org/packages/31/9c/ba5a9569d719bfdd6ce863df4133ac6a1658cf1b07cc3534c31db729fbbc/rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9", size = 222409 },
{ url = "https://files.pythonhosted.org/packages/35/72/f28ca566f6c23c35bbf7445f65eb0364577b25a026305995e24f78b83d94/rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f", size = 217681 },
{ url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178 },
{ url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878 },
{ url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963 },
{ url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975 },
{ url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347 },
{ url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962 },
{ url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623 },
{ url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997 },
{ url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169 },
{ url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615 },
{ url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655 },
{ url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602 },
{ url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315 },
{ url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852 },
{ url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709 },
{ url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596 },
{ url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114 },
{ url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197 },
{ url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025 },
{ url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969 },
{ url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905 },
{ url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569 },
{ url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482 },
{ url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040 },
{ url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849 },
{ url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842 },
{ url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606 },
{ url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341 },
{ url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843 },
{ url = "https://files.pythonhosted.org/packages/f6/b7/a662f367d4896dd0a10cef2fc91f10b7f08af1c10858e287e019563f338d/rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9", size = 347463 },
{ url = "https://files.pythonhosted.org/packages/ae/3f/ad45d03df4f84ebae5439577ee81f3999d182711e82235c8037b6528890e/rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25", size = 342051 },
{ url = "https://files.pythonhosted.org/packages/7e/31/3dcd68c13d4bcc59c1f7eb33ac8e80698f06f3eb0d8a1e06419836071c20/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871", size = 374143 },
{ url = "https://files.pythonhosted.org/packages/cf/0d/68c1f058a250fbd1380ebda9fc227cbf50117adf8ffe8161ef383ea79f68/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91", size = 378604 },
{ url = "https://files.pythonhosted.org/packages/d7/d6/2d4c59b85397cb4800594fadf692688ccf5ce556adc930e7a5bf21061a5e/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f", size = 490790 },
{ url = "https://files.pythonhosted.org/packages/da/04/7e05dc3aebaf52f4e026766bd668fdd09a9d0e23f64a14686b36b3501892/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d", size = 393146 },
{ url = "https://files.pythonhosted.org/packages/57/ca/e2e9a0a46a74ed51a0498ba1fe10f4ea6b2d9a155f372a2f91e51f18cf10/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945", size = 372022 },
{ url = "https://files.pythonhosted.org/packages/ba/cb/8f8774df5134e23424372838bcc5c7ed4127d723e1ff52f7bebd4dcb2563/rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297", size = 377092 },
{ url = "https://files.pythonhosted.org/packages/90/02/8d7095d73bf9114219be40230baa5df00611e0a82ed9517779ff9c19f82b/rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0", size = 400830 },
{ url = "https://files.pythonhosted.org/packages/ec/02/8206856f8f363cd042a8315dc86b3912f5dcb6d3c66bbb24b69c2bfb0775/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9", size = 551478 },
{ url = "https://files.pythonhosted.org/packages/41/6b/36211f1bb1f0b0313f496d92f5905b74ea107f27cb16fa3355a82f04575e/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6", size = 615173 },
{ url = "https://files.pythonhosted.org/packages/35/77/cda0c4a6f055446b692f0ed5692f73707cafd3dff82672ede31b1a9b59de/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7", size = 579653 },
{ url = "https://files.pythonhosted.org/packages/74/ec/d8385f446240aed643b9e92a5055cff3015cc04a13c61f73b2883c478ed5/rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429", size = 206498 },
{ url = "https://files.pythonhosted.org/packages/6d/a5/71b5cd00e0521e3b6b81828baea368c62b6b700ebdd9554cd7d41ddf12fa/rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15", size = 223121 },
{ url = "https://files.pythonhosted.org/packages/33/58/dba857c3bc8221b31b62eb170a3080f4f191de79f047200389b7ed1b06a7/rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009", size = 218741 },
{ url = "https://files.pythonhosted.org/packages/5b/d0/320ab28ccc1415eeb509d68682b0014fb74690cd49f1c2d29a232475af50/rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0", size = 349719 },
{ url = "https://files.pythonhosted.org/packages/56/88/f5b12f1358f443c08b7ce3cc8391d82d335f4872580e2e097847fd36087a/rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a", size = 343267 },
{ url = "https://files.pythonhosted.org/packages/f7/0c/c765b0059d532acb3b9c45d781ccc22f15a96dbe443d00903f643ba9df10/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d", size = 374555 },
{ url = "https://files.pythonhosted.org/packages/6b/8a/cafddfda77564a10cd21184640c3bffda6a2b8d20972fb5dedd5e0166328/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db", size = 378346 },
{ url = "https://files.pythonhosted.org/packages/b9/01/5e626016eff72c183bf6c96539240cace15d402468647a453ec08415b2fc/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250", size = 492373 },
{ url = "https://files.pythonhosted.org/packages/3b/9c/15a2469e9389242f46896b3f0a01d68caea8a5a35c011fcb05ef333aae73/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f", size = 394371 },
{ url = "https://files.pythonhosted.org/packages/63/f5/c100ff77e1e6366e947c75969c258fdfc4b7bc5bbfe7351e62ffbf2a1228/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018", size = 375132 },
{ url = "https://files.pythonhosted.org/packages/dd/f4/fe0269c9de253e99c81cabc12b8971a5feaa083debdaff1221e06264d9e3/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d", size = 377642 },
{ url = "https://files.pythonhosted.org/packages/05/65/b34a7b257baccff8f4a24a722933166d4941d5ebdc9c3f4bc4ffcd5ce4f4/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b", size = 400855 },
{ url = "https://files.pythonhosted.org/packages/98/32/844e54176b6071b90b38a564e6940bc6eb8f97b2890dc709c19db9dec0f4/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6", size = 552573 },
{ url = "https://files.pythonhosted.org/packages/17/73/6041d20729dffbfdf155c02d65be58bc225a1c1fb548fd87c23ef306138f/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad", size = 615840 },
{ url = "https://files.pythonhosted.org/packages/af/9e/418094adaee6b056ce199051b255448ed872829051e341b2294c80da0977/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903", size = 582764 },
{ url = "https://files.pythonhosted.org/packages/3d/9b/1698ebf6b840ddfe8b472198abbed6ace35c6e398faeecd6c47dae742a4e/rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b", size = 203795 },
{ url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037 },
{ url = "https://files.pythonhosted.org/packages/a3/90/a9ba81408369d34c1aca73319c15adc91ed45c1d21e9e8ee4832ffb7fe0b/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4", size = 381109 },
{ url = "https://files.pythonhosted.org/packages/b3/44/5192a0bed94cec86bd2a5e1cc5a1bb8f7eec3aec907eacd2deb13e87e326/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759", size = 388222 },
{ url = "https://files.pythonhosted.org/packages/92/cd/5356549711448f18b52f7a11ffbe90f1228774996fbf3c6cd1b18d22abaf/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb", size = 500384 },
{ url = "https://files.pythonhosted.org/packages/7a/88/ddda9d28adfe33119c75b2e733d4ba7e326f41d7e1b1093951771768ca48/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82", size = 401661 },
{ url = "https://files.pythonhosted.org/packages/32/c3/bb59ba16a6b4a57995d15b8c04c00f28df3b938c4dcdde48fbe0f73edb0c/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c", size = 380748 },
{ url = "https://files.pythonhosted.org/packages/f5/b8/0580faa1c5a32ddc160130dd7ae54d2d007272a89a40a6b5db3e50f9f585/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14", size = 385731 },
{ url = "https://files.pythonhosted.org/packages/bf/70/f73564642bbe3322c7eeef2c2f258cf040b71c41a430b532e59d04a1b838/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c", size = 412131 },
{ url = "https://files.pythonhosted.org/packages/8c/5d/17fff2e1f8f68721a2afb5cb48f7e442c751bb6b4883157c5abaf618cd08/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68", size = 558193 },
{ url = "https://files.pythonhosted.org/packages/2a/d0/869ab6fb08531f97aae4a780b4d61b4190d8aef48fe054ee28bcf7114466/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da", size = 622448 },
{ url = "https://files.pythonhosted.org/packages/ba/a5/9672e532fe02cd3b92c78cfd8743abca939d96853006c63835009a1ecb6f/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448", size = 591126 },
{ url = "https://files.pythonhosted.org/packages/80/ef/3a9f8e4279c7920af561deed4cb7ebebed2794a8f608cf404d5eef14a105/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0", size = 357108 },
{ url = "https://files.pythonhosted.org/packages/b7/55/4b2fa381a583760aea5c92841e4e928a358e1c6511b129219e9c2826a226/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37", size = 346877 },
]
[[package]]
name = "ruff"
version = "0.16.2"

View file

@ -8,7 +8,7 @@ status: active
owner: codex
topic_slug: coulomb-social
created: "2026-10-08"
updated: "2026-10-27"
updated: "2026-10-30"
origin: residual
origin_ref: CSOC-WP-0010
state_hub_workstream_id: "38208f72-c285-56fa-be44-de9b59a4e031"
@ -77,6 +77,21 @@ Use isolated issuer/directory/local mail sink/app for UC-009/010/011. Bind exact
2026-10-11: the checkout has a loopback issuer, directory and mailbox double. The browser node is bound on UC-009/010/011 and still `established: false`. It covers reset-to-local-mailbox, sign-in, callback replay, expired and reused handles, tenant and client refusal, registration verify/cancel, and caller/suppression invitation counts. No SMTP, production account, or live issuer is contacted. Real user-engine/email-connect admission, DNS and productive per-flow verdicts remain open. UC-012 stays deferred. Gate for this slice: `make check` 200 passed.
2026-10-30: added an explicit real-owner-source browser gate for UC-010/011.
Private snapshots run the actual user-engine portal, registration/outbox HTTP
adapters and email-connect scoped receiver/SQLite ledger, with synthetic session,
authorization, directory, activity sink and mail provider. Actual forms cover
verify/cancel/expiry/replay, fresh-handle cross-caller denial, tenant/client/token
denials, cross-tenant admin refusal, invitation/resend, duplicate delivery and
suppression. Python only permits fixture ports; browser routes/API redirects are
bounded to fixture origins. No production configuration, account, SMTP provider,
mailbox scan or original note body is used. Exact node is linked in the candidate
registry and the invocation is in docs/usecases/platform-communication.md.
Proof: docs/release/owner-communication-browser-2026-10-30.json. This completes
the owner-source UC-010/011 browser integration slice, not native issuer/directory/
app-login coverage or productive per-flow verdicts. T04 remains progress; T02
sender/inbound/bounce ownership and UC-012 nomination remain separate gates.
## Completion
Finish when every task above has its stated evidence and acceptance, or hand off any newly discovered residual to a live work record first. Cross-owner dependencies remain their existing canonical records.