From c6b95c4b39af20842254cde94361966c07fdeb7b Mon Sep 17 00:00:00 2001 From: Unlimited Data Works LLC Date: Sun, 26 Jul 2026 15:34:11 -0400 Subject: [PATCH 1/2] Release v0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major features in this release: - Automated encrypted database backup — supervised in-app snapshotter with configurable cadence + age-based retention; full .etbk bundles or raw-DB; master-key or write-only X25519 public-key (ECIES) modes; fail-closed restorability gate before every snapshot counts. Ships DISABLED by default. - Regulatory clock awareness — propose-then-confirm deadline engine for clinical-trial safety mail; tracks the site/IRB deadline the user owns as an escalating acknowledgement-required clock and notes sponsor deadlines as informational receipts; /clocks page + daily digest + escalation email with portable .ics + optional calendar-write. - Topic / Study Digest — define a Topic (study, subject, or thread of interest), matching is baked into the primary classify pass (no new body read, no new model, no new egress); per-Topic recurring digests, per-Topic sender reinforcement from thumbs-up/down feedback, whole-window Re-Triage Preview. - Encryption at rest by default + HMAC-SHA256 audit hash chain — new installs are SQLCipher-encrypted out of the box, every dedicated/backup/migration connection inherits the key through the encryption-aware opener; the audit log now spans 8 sibling audit-event tables + a per-request PHI-access log, reachable through a paginated /compliance audit-review surface; chain survives master-key rotation. - Live AI-backend model picker with capability-aware failover — replaces the free-text model field; fetches what's actually installed on the Ollama endpoint and shows capabilities, size, quantization, context length, VRAM residency; embedding-only models are flagged and non-selectable for classification; ordered backup_models list fails over on capability/resource errors (with a tamper-evident model_failover audit row). - Multilingual embedding model selector — recommended default intfloat/multilingual-e5-base (100+ languages) for sent-mail similarity retrieval; automatic E5 query/passage prefix handling; confirm-gated background reindex on model change so retrieval never silently degrades. - Per-target webhook body templating with HIPAA-lock + wizard live preview — 5 built-in target types (Slack, PagerDuty Events v2, Opsgenie, Discord, Microsoft Teams) selectable from an admin wizard dropdown so an operator points a webhook directly at Slack/PagerDuty/etc. instead of standing up a middleware reshaper; HIPAA-flagged targets force-lock a minimal {category, timestamp, flow_id} shape via a shared fail-closed reshaper regardless of the operator's template; HMAC signature computed over the rendered bytes; wizard live preview renders the exact bytes email-triage would POST (normal + HIPAA-locked side-by-side) reusing the same production render SSOT so preview equals prod bytes-for-bytes. - Auto-retire a watch after N matches; cross-account aggregate reindex progress on the Bulk runs page; watcher delivery-failure visibility cluster (failure badge + fail-visible health + "Watching off (by choice)" Health-box distinction so a deliberately-disabled watcher no longer degrades /health). - Offset pagination across previously-capped audit/list surfaces so capped views are reachable instead of silently truncated. - External integration API renamed /api/openclaw -> /api/v1 (neutral) with a 308 redirect for legacy clients. - Security dependency hardening — starlette 1.2.1 -> 1.3.1 (form-field denial-of-service CVE-2026-54283); a small dedicated form-size guard preserves the one route that legitimately accepts a large pasted email (the raw-email classify box) while every other form keeps the tighter new limits. - Reliability + HIPAA hardening wave — Topic Digest wrong-attribution bug fixed with a reversible confidence floor; Topic-match LLM JSON parse-failure salvage backstop + bounded retry + PHI-safe diagnostics + rate/canary health counter; OAuth-credential 8-day outage class fixed (never-clobber _secret_write SSOT); remove-account is now a safe reversible "Disabled — credentials removed" soft-delete that keeps the tamper-evident audit trail; account deactivation actually stops ingestion via fail-closed is_active gates; reflected-XSS-class fixes; corrupt persisted JSON columns no longer 500 the whole page; dry_run/preview no longer fires real escalations, webhooks, or mailbox writes; PHI-egress locality gate no longer fails open on a hostname masquerade. --- .dockerignore | 2 +- .gitattributes | 12 + .githooks/pre-push | 98 +- .github/CODEOWNERS | 20 + .github/dependabot.yml | 85 + .github/op-approve/allowed_signers | 1 + .github/workflows/arm64-build-check.yml | 80 + .github/workflows/e2e.yml | 73 + .github/workflows/release.yml | 83 +- .github/workflows/security.yml | 34 + .github/workflows/tests.yml | 127 + Containerfile | 240 +- Containerfile.test | 38 + INCIDENT_RESPONSE.md | 404 --- README.md | 21 +- ...ivergence-census-2026-07-24-public-stub.md | 17 + config/example-homelab.yaml | 29 + docs/install.md | 138 + docs/privacy.md | 3 + openclaw/skills/email-triage-api/README.md | 47 +- openclaw/skills/email-triage-api/SKILL.md | 11 +- .../scripts/email-triage-api.py | 509 ++- .../email-triage-api/tests/test_multiuser.py | 175 + pyproject.toml | 147 +- requirements.lock | 39 +- scratchpad/enumerate_silent_swallow.py | 46 + scratchpad/generate_select_star_baseline.py | 39 + .../generate_silent_swallow_baseline.py | 42 + .../generate_sync_sqlite_async_baseline.py | 42 + scratchpad/leira_census_session.py | 184 + scratchpad/leira_exception_chain_audit.py | 133 + scratchpad/leira_js_base64_audit.py | 126 + scratchpad/select_star_baseline.txt | 52 + scratchpad/silent_swallow_baseline.txt | 194 ++ scratchpad/sync_sqlite_async_baseline.txt | 47 + scratchpad_leira_130_js_audit.json | 11 + scripts/build-embedding-bits-manifest.py | 27 + scripts/cve-arc-canary-verify.sh | 307 ++ scripts/cve-arc-lock-report.py | 275 ++ scripts/cve-arc-reenrich-historical.sh | 165 + scripts/db-backup-relocate.sh | 94 + scripts/download-embedding-bits.sh | 23 +- scripts/embedding-bits-manifest.json | 299 +- scripts/git-hooks/commit-msg | 105 + scripts/leira_cache.py | 148 + scripts/leira_census_prepass.py | 402 +++ scripts/leira_class_hints.py | 189 ++ scripts/leira_config.json | 81 + scripts/leira_config.schema.json | 47 + scripts/leira_defer_sweep.py | 1139 +++++++ scripts/leira_discover.py | 373 ++ scripts/leira_docs_summarizer.py | 160 + scripts/leira_draft.py | 112 + scripts/leira_flag_advisor.py | 132 + scripts/leira_hardcode_scanner.py | 450 +++ scripts/leira_log_triage.py | 145 + scripts/leira_newsletter_extract.py | 152 + scripts/leira_pr_prereview.py | 147 + scripts/leira_prepass.psm1 | 829 +++++ scripts/leira_prepass.py | 1138 +++++++ scripts/leira_style_precompute.py | 122 + scripts/leira_verify_fix.py | 156 + scripts/leira_verify_prepass.py | 173 + scripts/n432-path3-bake-step1.py | 29 + scripts/n432-path3-bake-step3.py | 61 + scripts/probe_leira_classify.py | 292 ++ scripts/punch-list-append.py | 167 + scripts/push-ci-shepherd.ps1 | 272 ++ scripts/rapid-cve-bump.py | 446 +++ scripts/run-sme-panel.py | 138 + scripts/spike_apsw_compat_perf.py | 317 ++ scripts/spike_sqlcipher3_wheels_dropin.py | 55 + scripts/spike_sqlcipher_cross_arch_compat.sh | 95 + scripts/structural_superset_check.py | 286 ++ scripts/test-e2e.sh | 43 + scripts/test-local-podman.sh | 46 + scripts/test_flake_classifier.py | 221 ++ scripts/test_flake_exclusions.json | 17 + scripts/verified-test.py | 183 + scripts/verify_leira_prepass_vs_run19.py | 154 + ...-census-ANTHROPIC-DO-NOT-USE-2026-07-23.js | 1226 +++++++ .../workflows/et-divergence-census-leira.js | 998 ++++++ scripts/workflows/feature-add-e2e-gate.py | 579 ++++ src/email_triage/_atomicio.py | 113 + src/email_triage/_base64url.py | 113 + src/email_triage/_egress.py | 209 ++ src/email_triage/_envbool.py | 69 + src/email_triage/_errfmt.py | 157 +- src/email_triage/_hm_render.py | 56 + src/email_triage/_http_client.py | 28 +- src/email_triage/_ids.py | 74 + src/email_triage/_int_utils.py | 58 + src/email_triage/_isotime.py | 197 +- src/email_triage/_netloc.py | 226 ++ src/email_triage/_ollama_reuse.py | 326 ++ src/email_triage/_secret_keys.py | 93 + src/email_triage/_secret_scrub.py | 131 + src/email_triage/_signing.py | 47 + src/email_triage/_textfmt.py | 58 + src/email_triage/_us_datetime.py | 124 + src/email_triage/actions/add_label.py | 92 +- src/email_triage/actions/base.py | 51 +- src/email_triage/actions/clinicaltrials.py | 333 ++ src/email_triage/actions/digest.py | 237 +- src/email_triage/actions/digest_configs.py | 8 +- src/email_triage/actions/digest_filter.py | 55 +- src/email_triage/actions/digest_render.py | 208 +- src/email_triage/actions/digest_resend.py | 67 + src/email_triage/actions/draft_reply.py | 384 ++- src/email_triage/actions/escalate.py | 55 +- .../actions/hipaa_sent_counter.py | 133 +- .../actions/hipaa_style_distill.py | 76 +- src/email_triage/actions/invite.py | 30 +- src/email_triage/actions/label.py | 104 +- src/email_triage/actions/move.py | 13 +- src/email_triage/actions/mute.py | 111 + src/email_triage/actions/notify.py | 84 +- src/email_triage/actions/provider_labels.py | 119 + src/email_triage/actions/recipient_digest.py | 192 +- src/email_triage/actions/registry.py | 12 +- src/email_triage/actions/regulatory_clock.py | 146 + src/email_triage/actions/self_sent_event.py | 60 +- src/email_triage/actions/sent_mail_capture.py | 96 +- src/email_triage/actions/sent_mail_index.py | 109 +- src/email_triage/actions/style_profile.py | 78 +- .../actions/suggest_meeting_times.py | 26 +- src/email_triage/actions/topic_configs.py | 1324 ++++++++ src/email_triage/actions/topic_matcher.py | 520 +++ src/email_triage/actions/topic_prefilter.py | 835 +++++ src/email_triage/actions/web_search.py | 187 + src/email_triage/ai_backends/__init__.py | 1 + src/email_triage/ai_backends/azure_openai.py | 50 +- src/email_triage/ai_backends/base.py | 44 + .../ai_backends/gemini_adapter.py | 52 +- src/email_triage/ai_backends/loader.py | 115 +- .../ai_backends/ollama_adapter.py | 21 +- src/email_triage/ai_backends/openai_direct.py | 52 +- src/email_triage/ai_backends/registry.py | 12 +- src/email_triage/baa_expiry.py | 100 +- src/email_triage/backup.py | 793 ++++- src/email_triage/backup_snapshot_cleanup.py | 4 +- src/email_triage/cache/_redis.py | 112 + src/email_triage/cache/classification.py | 170 +- src/email_triage/cache/topic_feedback.py | 241 ++ src/email_triage/classify/baa_gate.py | 33 +- src/email_triage/classify/base.py | 669 +++- src/email_triage/classify/discover.py | 13 +- src/email_triage/classify/gemini.py | 18 +- src/email_triage/classify/hints.py | 104 +- src/email_triage/classify/ollama.py | 1270 ++++++- src/email_triage/classify/openai_compat.py | 19 +- src/email_triage/classify/prompts.py | 290 +- src/email_triage/classify/safety_detect.py | 456 +++ src/email_triage/cli.py | 1328 +++++++- src/email_triage/config.py | 901 ++++- src/email_triage/db/_engine.py | 491 ++- src/email_triage/db/_helpers.py | 66 + src/email_triage/db/_live_app_gate.py | 233 ++ src/email_triage/db/_migrate.py | 34 +- src/email_triage/db/_writer.py | 23 +- src/email_triage/db/accounts.py | 876 ++++- src/email_triage/db/audit.py | 1352 ++++++-- src/email_triage/db/reconciliation.py | 34 +- src/email_triage/db/regulatory_clock.py | 1614 +++++++++ src/email_triage/db/style.py | 341 +- src/email_triage/db/users.py | 90 +- src/email_triage/embedding_bits/__init__.py | 231 +- src/email_triage/engine/availability.py | 98 + src/email_triage/engine/embedding_backend.py | 181 +- src/email_triage/engine/flow.py | 120 +- src/email_triage/engine/html_text.py | 68 +- src/email_triage/engine/ics.py | 78 +- src/email_triage/engine/models.py | 34 +- .../engine/persistent_counters.py | 74 +- src/email_triage/engine/query_lang.py | 32 +- src/email_triage/engine/store.py | 129 +- src/email_triage/job_drain_safety.py | 114 + src/email_triage/jobs/embedding_reindex.py | 304 +- src/email_triage/jobs/topic_backscan.py | 1170 +++++++ src/email_triage/llm_health.py | 74 +- src/email_triage/llm_maintenance.py | 36 +- src/email_triage/llm_safety.py | 105 + src/email_triage/llm_truncation.py | 150 + src/email_triage/mail_headers.py | 282 +- src/email_triage/metrics.py | 195 +- .../providers/_msal_cache_crypto.py | 293 ++ src/email_triage/providers/_oauth_http.py | 3 +- .../providers/_ondisk_artifacts.py | 229 ++ src/email_triage/providers/dispatcher.py | 73 +- src/email_triage/providers/factory.py | 301 +- src/email_triage/providers/gmail_api.py | 135 +- src/email_triage/providers/gmail_calendar.py | 66 +- src/email_triage/providers/imap.py | 310 +- src/email_triage/providers/imap_blocking.py | 70 +- .../providers/imap_fetch_transport.py | 55 + src/email_triage/providers/office365.py | 280 +- .../providers/office365_calendar.py | 104 +- src/email_triage/providers/provider_labels.py | 53 +- src/email_triage/providers/sent_folder.py | 50 +- src/email_triage/providers/traits.py | 38 +- src/email_triage/secrets.py | 296 +- src/email_triage/single_flight.py | 15 +- .../style_learning/distill_hipaa.py | 121 +- .../style_learning/per_contact_hipaa.py | 448 ++- .../style_learning/phi_scrubber.py | 11 +- .../style_learning/trigger_watcher.py | 17 +- src/email_triage/tls.py | 20 +- src/email_triage/tls_csr.py | 33 +- src/email_triage/triage_logging.py | 504 ++- src/email_triage/version.py | 62 +- src/email_triage/web/_audit.py | 322 +- src/email_triage/web/_auth_revoked.py | 91 + src/email_triage/web/_backfill_chip.py | 123 + src/email_triage/web/_cookies.py | 58 + src/email_triage/web/_credhash.py | 44 + src/email_triage/web/_flash.py | 163 + src/email_triage/web/_list_authz.py | 137 + src/email_triage/web/_mime_builder.py | 97 + src/email_triage/web/_pagination.py | 140 + src/email_triage/web/_resource_authz.py | 63 + src/email_triage/web/_triage_metrics.py | 72 + .../web/_watch_status_recorder.py | 136 + src/email_triage/web/_webhook_target_types.py | 130 + src/email_triage/web/_webhook_template.py | 385 +++ src/email_triage/web/access_audit.py | 172 +- src/email_triage/web/account_scope.py | 146 + src/email_triage/web/acme_http01.py | 420 +++ src/email_triage/web/acme_job_state.py | 42 +- src/email_triage/web/acme_renewer.py | 1261 ++++--- src/email_triage/web/action_guide.py | 259 ++ src/email_triage/web/admin_alerts.py | 754 +++++ src/email_triage/web/app.py | 651 +++- src/email_triage/web/auth.py | 141 +- src/email_triage/web/background_sweepers.py | 995 +++++- src/email_triage/web/bulk.py | 29 +- src/email_triage/web/calendars.py | 7 +- src/email_triage/web/csrf.py | 95 +- src/email_triage/web/daily_health.py | 331 +- src/email_triage/web/db.py | 1171 +++++-- src/email_triage/web/db_threadpool.py | 16 +- src/email_triage/web/db_writelock.py | 4 +- src/email_triage/web/dependencies.py | 307 +- src/email_triage/web/dev_keypair.py | 7 +- src/email_triage/web/digest.py | 468 ++- src/email_triage/web/email_watches.py | 301 +- src/email_triage/web/error_explain.py | 29 +- src/email_triage/web/error_html.py | 66 + src/email_triage/web/error_surface.py | 57 + src/email_triage/web/events.py | 542 ++- src/email_triage/web/form_limits.py | 58 + src/email_triage/web/gmail_push_auth.py | 84 +- src/email_triage/web/gmail_watch_status.py | 101 + src/email_triage/web/ingestion_push.py | 853 +++-- src/email_triage/web/login_guard.py | 104 +- src/email_triage/web/mfa.py | 22 +- src/email_triage/web/migrations.py | 891 ++++- src/email_triage/web/o365_renewer.py | 99 +- src/email_triage/web/oauth_secret_keys.py | 72 + src/email_triage/web/poll_ingestion.py | 490 ++- src/email_triage/web/routers/_json.py | 43 + src/email_triage/web/routers/_redirect.py | 62 + src/email_triage/web/routers/_secret_write.py | 189 ++ .../web/routers/{openclaw.py => agent_api.py} | 840 ++--- src/email_triage/web/routers/api.py | 284 +- src/email_triage/web/routers/auth_keys.py | 597 ++-- src/email_triage/web/routers/backup.py | 376 ++- src/email_triage/web/routers/health.py | 521 ++- src/email_triage/web/routers/help.py | 155 + src/email_triage/web/routers/integrations.py | 724 +++- src/email_triage/web/routers/tls_csr.py | 108 +- src/email_triage/web/routers/ui/_shared.py | 1560 +++++++-- src/email_triage/web/routers/ui/accounts.py | 874 +++-- src/email_triage/web/routers/ui/admin.py | 756 ++++- .../web/routers/ui/ai_backends_crud.py | 204 +- src/email_triage/web/routers/ui/calendars.py | 73 +- src/email_triage/web/routers/ui/categories.py | 257 +- .../web/routers/ui/embedding_install.py | 84 +- src/email_triage/web/routers/ui/labels.py | 95 +- src/email_triage/web/routers/ui/oauth.py | 165 +- src/email_triage/web/routers/ui/openclaw.py | 59 +- src/email_triage/web/routers/ui/profile.py | 3000 ++++++++++++++++- src/email_triage/web/routers/ui/push.py | 209 +- .../web/routers/ui/retry_queue.py | 125 +- src/email_triage/web/routers/ui/routes.py | 112 +- .../web/routers/ui/triage_classify.py | 401 ++- src/email_triage/web/routers/ui/users.py | 322 +- src/email_triage/web/routers/ui/wizard.py | 135 +- src/email_triage/web/routers/webhooks.py | 189 +- src/email_triage/web/settings_keys.py | 60 +- src/email_triage/web/smtp_send.py | 83 +- src/email_triage/web/static/base64url.js | 87 + src/email_triage/web/static/style.css | 38 +- src/email_triage/web/static/webauthn.js | 20 +- src/email_triage/web/task_supervisor.py | 230 +- .../web/templates/_clocks_table.html | 71 + src/email_triage/web/templates/_macros.html | 771 +++++ src/email_triage/web/templates/_pager.html | 64 + .../web/templates/_settings_tabs.html | 20 +- .../web/templates/account_wizard/step2.html | 20 +- .../{api_keys.html => _api_keys_section.html} | 29 +- .../web/templates/accounts/_delegates.html | 2 +- .../accounts/_digest_candidates.html | 66 + .../templates/accounts/_digest_editor.html | 36 +- .../web/templates/accounts/_edit.html | 22 + .../templates/accounts/_fields_gmail_api.html | 12 +- .../web/templates/accounts/_fields_imap.html | 18 +- .../web/templates/accounts/_o365_fields.html | 2 +- .../web/templates/accounts/_routes_body.html | 67 +- .../web/templates/accounts/_row.html | 41 +- .../accounts/_watch_folders_imap.html | 7 +- .../web/templates/accounts/_watch_status.html | 28 +- .../templates/admin/_baa_expiry_banner.html | 6 +- .../web/templates/admin/acme_status.html | 9 +- .../templates/admin/ai_backends/_form.html | 38 +- .../templates/admin/ai_backends/_index.html | 8 +- .../web/templates/admin/backup.html | 147 - .../config_tabs/_embedding_install_card.html | 2 +- .../_webhook_preview_fragment.html | 71 + .../admin/config_tabs/ai_backends.html | 83 +- .../templates/admin/config_tabs/backup.html | 187 +- .../templates/admin/config_tabs/general.html | 183 +- .../admin/config_tabs/integrations.html | 341 +- .../templates/admin/config_tabs/security.html | 37 +- .../web/templates/admin/dev_keys.html | 20 +- .../web/templates/admin/integrations.html | 794 ----- .../templates/admin/retry_queue/_index.html | 39 +- .../web/templates/admin/security.html | 2 +- .../web/templates/admin/stats.html | 15 +- .../web/templates/admin/watches.html | 2 +- src/email_triage/web/templates/base.html | 84 +- .../web/templates/compliance.html | 47 +- src/email_triage/web/templates/dashboard.html | 125 +- src/email_triage/web/templates/help/me.html | 92 + src/email_triage/web/templates/login.html | 24 +- .../web/templates/login_dev_keypair.html | 35 +- src/email_triage/web/templates/logs.html | 20 +- src/email_triage/web/templates/profile.html | 33 +- ..._keys.html => _hardware_keys_section.html} | 72 +- .../profile/_topic_backscan_status.html | 79 + .../web/templates/profile/_topic_digest.html | 181 + .../profile/_topic_digest_editor.html | 758 +++++ .../profile/_topic_digest_preview.html | 259 ++ .../templates/profile/_topic_nct_result.html | 390 +++ .../templates/profile/regulatory_clocks.html | 195 ++ .../web/templates/profile/security.html | 63 + .../web/templates/profile/style_data.html | 7 +- .../web/templates/profile/watches.html | 29 +- .../web/templates/regulatory_clocks.html | 24 + .../web/templates/rules/_rule_edit.html | 2 +- .../web/templates/rules/global.html | 8 +- .../web/templates/rules/lists.html | 8 +- .../templates/triage/_discover_results.html | 6 +- .../triage/_job_progress_fragment.html | 22 +- .../templates/triage/_reindex_aggregate.html | 45 + .../web/templates/triage/_results.html | 2 +- .../web/templates/triage/job_progress.html | 34 +- .../web/templates/triage/jobs_index.html | 40 +- .../web/templates/triage/run.html | 8 + .../web/templates/users/manage.html | 4 +- src/email_triage/web/topic_digest_sender.py | 767 +++++ src/email_triage/web/topic_search_summary.py | 160 + src/email_triage/web/triage_inflight.py | 45 +- src/email_triage/web/triage_one.py | 377 ++- src/email_triage/web/triage_runner.py | 127 +- src/email_triage/web/triage_runner_bulk.py | 265 +- src/email_triage/web/watch_runner.py | 185 +- src/email_triage/web/watcher.py | 1415 +++++--- src/email_triage/web/watcher_retry.py | 42 +- src/email_triage/web/webauthn_auth.py | 4 +- tests/_canonical_email_pin_predicates.py | 111 + tests/_db_tracking.py | 68 + tests/_migration_fixtures.py | 50 + tests/_pin_harness.py | 725 ++++ tests/conftest.py | 289 +- tests/copy_contract_registry.json | 336 ++ tests/e2e/__init__.py | 0 tests/e2e/conftest.py | 302 ++ .../test_admin_gate_and_url_encode_smokes.py | 67 + tests/e2e/test_admin_tab_help.py | 36 + tests/e2e/test_backscan_ceiling_banner.py | 56 + tests/e2e/test_backup_config.py | 118 + tests/e2e/test_compliance_audit_chains.py | 44 + tests/e2e/test_dashboard_key_export.py | 30 + tests/e2e/test_help_me.py | 61 + tests/e2e/test_integrations_flush.py | 57 + tests/e2e/test_n109_picker_hide_e2e.py | 70 + tests/e2e/test_regulatory_clocks.py | 123 + tests/e2e/test_regulatory_clocks_editor.py | 24 + tests/e2e/test_reindex_aggregate.py | 53 + tests/e2e/test_retry_queue_badge.py | 43 + tests/e2e/test_searxng_summary.py | 128 + tests/e2e/test_security_tab.py | 49 + tests/e2e/test_smoke.py | 77 + tests/e2e/test_topic_floor_editor.py | 35 + tests/e2e/test_topic_retriage_ui.py | 122 + tests/e2e/test_topic_roundtrip.py | 85 + tests/e2e/walk/__init__.py | 9 + tests/e2e/walk/_helpers.py | 279 ++ tests/e2e/walk/test_accounts_api_keys_page.py | 62 + .../e2e/walk/test_action_regulatory_clock.py | 228 ++ tests/e2e/walk/test_admin_webhook_preview.py | 99 + .../e2e/walk/test_ai_backends_probe_models.py | 185 + .../e2e/walk/test_backfill_capped_dismiss.py | 76 + tests/e2e/walk/test_backup_config_save.py | 95 + .../walk/test_clock_disposition_actions.py | 228 ++ tests/e2e/walk/test_digest_resend.py | 99 + tests/e2e/walk/test_help_me.py | 55 + tests/e2e/walk/test_help_me_ask.py | 55 + tests/e2e/walk/test_ops_db_backup_trigger.py | 76 + tests/e2e/walk/test_profile_hardware_keys.py | 58 + tests/e2e/walk/test_profile_security_walk.py | 60 + tests/e2e/walk/test_provider_labels_apply.py | 249 ++ .../e2e/walk/test_regulatory_clocks_create.py | 140 + .../walk/test_regulatory_clocks_dashboard.py | 111 + .../e2e/walk/test_regulatory_clocks_delete.py | 280 ++ .../e2e/walk/test_regulatory_clocks_editor.py | 147 + .../e2e/walk/test_regulatory_clocks_update.py | 164 + ...eindex_jobs_aggregate_progress_fragment.py | 98 + tests/e2e/walk/test_topic_backscan_worker.py | 242 ++ tests/e2e/walk/test_topic_digest_delete.py | 143 + .../e2e/walk/test_topic_digest_delete_hit.py | 183 + tests/e2e/walk/test_topic_digest_edit.py | 130 + .../walk/test_topic_digest_feedback_unmute.py | 168 + .../walk/test_topic_digest_hit_feedback.py | 207 ++ .../e2e/walk/test_topic_digest_include_hit.py | 259 ++ tests/e2e/walk/test_topic_digest_new.py | 113 + tests/e2e/walk/test_topic_digest_preview.py | 142 + tests/e2e/walk/test_topic_digest_retriage.py | 154 + .../walk/test_topic_digest_retriage_cancel.py | 193 ++ ...topic_digest_retriage_progress_fragment.py | 119 + tests/e2e/walk/test_topic_digest_save.py | 127 + .../walk/test_topic_digest_test_details.py | 172 + tests/e2e/walk/test_topic_matcher.py | 165 + tests/e2e/walk/test_topic_prefilter.py | 127 + tests/goldens/allowed_signers.sha256 | 1 + tests/hooks/__init__.py | 0 tests/hooks/test_cve_inject.py | 193 ++ tests/leira_multihost_symmetry.Tests.ps1 | 153 + tests/leira_multihost_vectors.json | 54 + tests/leira_ps_consumers_scaffold.Tests.ps1 | 829 +++++ tests/test_account_administer_gate_pin.py | 200 ++ tests/test_account_aliases.py | 5 +- tests/test_account_delete_purge_pin.py | 678 ++++ tests/test_account_email_ssot_pin.py | 168 + tests/test_account_hipaa_scope_pin.py | 1035 ++++++ tests/test_account_scope_ssot_pin.py | 148 + tests/test_account_status_ssot_pin.py | 249 ++ ...test_acme_challenge_dispatcher_ssot_pin.py | 120 + tests/test_acme_cname_preflight.py | 256 +- tests/test_acme_hardening.py | 3 +- tests/test_acme_http01.py | 265 ++ tests/test_acme_jobs_persistence.py | 3 +- tests/test_acme_renewer.py | 3 +- tests/test_acme_wire_tls_path.py | 109 + tests/test_action_log_extras_ssot_pin.py | 164 + tests/test_actions/test_actions.py | 35 +- tests/test_actions/test_add_label.py | 4 +- tests/test_actions/test_digest.py | 90 +- ...est_digest_extractor_timeout_budget_pin.py | 117 + .../test_digest_hipaa_body_preview.py | 8 +- .../test_draft_reply_hipaa_header.py | 109 + .../test_actions/test_draft_reply_llm_body.py | 192 ++ .../test_draft_reply_m5_stitch.py | 11 +- tests/test_actions/test_hipaa_sent_counter.py | 4 +- .../test_actions/test_hipaa_style_distill.py | 19 +- tests/test_actions/test_idempotency.py | 3 +- .../test_invite_suggest_idempotency.py | 3 +- tests/test_actions/test_m7_draft_overlay.py | 3 +- tests/test_actions/test_m7_per_contact.py | 5 +- tests/test_actions/test_mute.py | 160 + tests/test_actions/test_recipient_digest.py | 3 +- tests/test_actions/test_sent_mail_capture.py | 35 +- tests/test_actions/test_sent_mail_index.py | 103 +- tests/test_actions/test_style_knobs.py | 7 +- tests/test_actions/test_style_profile.py | 3 +- tests/test_actions/test_web_search.py | 140 + tests/test_admin_gate_ssot_pin.py | 422 +++ tests/test_ai_backends/test_loader.py | 197 +- tests/test_ai_backends/test_registry.py | 9 +- tests/test_alembic_baseline.py | 3 +- tests/test_allow_list_sentinel_pin.py | 292 ++ tests/test_arch_encryption_build_pin.py | 252 ++ tests/test_atomic_write_pin_a11.py | 440 +++ tests/test_audit_cancellederror_stash_pin.py | 380 +++ .../test_audit_chain_byte_identity_freeze.py | 141 + tests/test_audit_chain_insert_ssot_pin.py | 106 + tests/test_audit_chain_pin.py | 307 ++ tests/test_audit_emit_swallow_pin.py | 741 ++++ tests/test_audit_last_exc_stash_pin.py | 118 + tests/test_audit_phi_access_pin.py | 561 +++ tests/test_audit_reconciliation.py | 3 +- tests/test_audit_safe_write.py | 107 + tests/test_audit_strict_mode.py | 72 +- tests/test_auth_expiry_parse_pin.py | 80 + tests/test_auth_stale_lifecycle_pin.py | 653 ++++ tests/test_authz_by_id_fetch_pin.py | 569 ++++ tests/test_baa_expiry.py | 6 +- tests/test_baa_gate_ssot_pin.py | 398 +++ tests/test_baa_paused_auto_clear_pin.py | 181 + tests/test_baa_runtime_gate_pin.py | 239 ++ tests/test_backfill_cap_ssot_pin.py | 239 ++ tests/test_backup.py | 99 + tests/test_backup_encrypted_verify.py | 102 + tests/test_bare_address_ssot_pin_a7.py | 245 ++ tests/test_base64url_ssot_pin.py | 205 ++ tests/test_base64url_ssot_unit.py | 92 + tests/test_cache/test_classification.py | 29 +- tests/test_cache/test_classification_async.py | 209 +- tests/test_cache/test_classification_redis.py | 12 +- .../test_outer_key_bare_sender_pin.py | 83 + tests/test_calendar_gate_ssot_pin.py | 108 + tests/test_calendars_module.py | 17 +- tests/test_canonical_email_rsvp_gmail_pin.py | 71 + tests/test_canonical_email_ssot_pin.py | 267 ++ ...test_canonical_email_ssot_providers_pin.py | 139 + tests/test_classify/test_discover.py | 3 +- tests/test_classify/test_hints.py | 83 + tests/test_classify/test_ollama.py | 655 +++- .../test_ollama_unreachable_boundary.py | 128 + .../test_parse_llm_json_recovery.py | 190 ++ tests/test_classify/test_prompts.py | 53 +- tests/test_classify/test_safety_detect.py | 92 + .../test_safety_detect_stage_b.py | 240 ++ tests/test_classify_egress_baa_gate_pin.py | 180 + tests/test_cli.py | 90 +- tests/test_cli_audit.py | 175 +- tests/test_cli_backup.py | 110 + tests/test_cli_db_open_encryption_pin.py | 241 ++ tests/test_cli_engine_close_pin.py | 131 + tests/test_cli_provider_close_in_loop_pin.py | 156 + tests/test_cli_provider_oauth_register_pin.py | 167 + tests/test_cli_verify_migrations.py | 91 + tests/test_clinicaltrials_resolver.py | 498 +++ tests/test_clock_ics.py | 33 + tests/test_config.py | 122 + tests/test_config_roundtrip.py | 535 ++- tests/test_config_store_disjoint_pin.py | 417 +++ tests/test_config_yaml_roundtrip_pin.py | 175 + tests/test_copy_as_contract_pin.py | 421 +++ tests/test_counter_registry_ssot_pin.py | 265 ++ tests/test_create_task_tracking_pin.py | 125 + tests/test_credential_hash_ssot_pin.py | 218 ++ tests/test_credential_inputs_hardened.py | 125 + tests/test_daily_health_retry_section.py | 3 +- ...shboard_maintenance_banner_render_smoke.py | 97 + tests/test_db_call_writer_routing.py | 3 +- tests/test_db_encryption_boot.py | 23 +- tests/test_db_snapshot.py | 500 +++ tests/test_db_write_coverage_pin.py | 158 +- tests/test_defer_event_log_rotation.py | 272 ++ tests/test_defer_gate_op_approval.py | 318 ++ tests/test_defer_gate_trigger.py | 239 ++ tests/test_defer_gate_widening.py | 358 ++ tests/test_defer_gate_widening_2.py | 202 ++ tests/test_defer_phrase_weights.py | 375 +++ tests/test_defer_phrases_discovery.py | 384 +++ tests/test_defer_phrases_ssot.py | 236 ++ tests/test_defer_phrases_ssot_pin.py | 276 ++ tests/test_defer_safe_context.py | 415 +++ tests/test_defer_shape_router.py | 628 ++++ tests/test_dev_keypair.py | 34 +- tests/test_digest_candidates_render_smoke.py | 51 + tests/test_digest_configs.py | 11 +- tests/test_digest_render.py | 4 +- tests/test_digest_render_ssot_pin.py | 83 + tests/test_digest_resend_ssot_pin.py | 115 + tests/test_digest_sender_guards.py | 99 +- tests/test_digest_truncate_ellipsis_pin.py | 94 + tests/test_display_name_ssot_pin.py | 75 + tests/test_drain_audit_row_count_arm_pin.py | 113 + tests/test_dry_run_side_effects_pin.py | 164 + tests/test_egress_funnel_pin.py | 615 ++++ ...email_accounts_updated_at_freshness_pin.py | 135 + tests/test_embedding_bits/conftest.py | 10 +- .../test_admin_ui_install_card.py | 3 + .../test_hf_cache_dir_ssot.py | 78 + .../test_manifest_arch_v3.py | 187 + tests/test_embedding_bits/test_reindex_job.py | 148 + .../test_style_learning_gating.py | 2 +- tests/test_emit_counter_registry_ssot.py | 287 ++ tests/test_emit_llm_outcome.py | 121 + ...t_encryption_aware_sqlite_open_ssot_pin.py | 106 + .../test_engine/test_availability_conflict.py | 71 + .../test_engine/test_email_message_memory.py | 11 +- .../test_embedding_staged_path_resolve.py | 32 + tests/test_engine/test_store_dirty_columns.py | 80 +- tests/test_envbool_ssot_pin_a9.py | 216 ++ tests/test_error_ctx_key_template_pin.py | 275 ++ tests/test_error_html_escape_pin_a8.py | 972 ++++++ ...est_evidence_bound_claim_gate_extractor.py | 178 + tests/test_exc_render_twin_ssot_pin.py | 268 ++ tests/test_exception_chain_pin.py | 110 + .../test_fernet_construction_allowlist_pin.py | 109 + tests/test_fetch_message_headers_only_pin.py | 248 ++ tests/test_flash_param_coupling_pin.py | 935 +++++ tests/test_from_header_ssot_pin.py | 119 + tests/test_get_logger_idempotent.py | 69 + tests/test_gh_app_token_scaffold.py | 117 + tests/test_hipaa_error_key_scrub_pin.py | 322 ++ .../test_hipaa_plaintext_advisory_ssot_pin.py | 285 ++ tests/test_hm_render_ssot_pin.py | 71 + tests/test_hm_render_ssot_unit.py | 56 + tests/test_html_strip_ssot_pin.py | 139 + tests/test_html_text_ssot_pin.py | 88 + tests/test_http_client_long_lived.py | 24 +- tests/test_httpx_client_ssot_pin.py | 66 + tests/test_hwm_uidvalidity_pin.py | 119 + tests/test_imap_blocking.py | 2 +- tests/test_imap_blocking_batch.py | 10 +- tests/test_imap_client_ssot_pin.py | 60 + tests/test_imap_literal_fetch_pin.py | 388 +++ tests/test_inflight_claim_no_await_gap_pin.py | 240 ++ .../test_infunction_import_resolution_pin.py | 108 + tests/test_install_oauth_fields_ssot_pin.py | 188 ++ tests/test_is_active_ingestion_gate_pin.py | 224 ++ tests/test_is_htmx_ssot_pin.py | 96 + tests/test_js_base64url_ssot_pin.py | 119 + tests/test_json_col_decode_pin.py | 302 ++ tests/test_json_col_naming_pin.py | 271 ++ tests/test_json_col_write_pin.py | 509 +++ tests/test_leira_consumers_scaffold.py | 113 + ...st_leira_defer_sweep_resume_pause_cache.py | 948 ++++++ tests/test_leira_multihost_symmetry.py | 149 + tests/test_leira_no_hardcode_pin.py | 154 + tests/test_leira_prepass_config_keys_pin.py | 136 + tests/test_leira_prepass_resilience.py | 831 +++++ tests/test_lifecycle_status_ssot_pin.py | 234 ++ .../test_lifecycle_triple_upsert_clear_pin.py | 555 +++ tests/test_list_rule_authz_pin.py | 393 +++ tests/test_llm_fence_coverage_pin.py | 374 ++ tests/test_llm_health.py | 68 + tests/test_llm_health_ssot_pin.py | 240 ++ tests/test_llm_timer_outcome_counter_pin.py | 316 ++ tests/test_llm_truncation.py | 87 + tests/test_llm_truncation_check_pin.py | 55 + tests/test_llm_truncation_ssot_pin.py | 117 + tests/test_load_json_col_pin.py | 204 ++ tests/test_m3_trigger_watcher.py | 3 +- tests/test_m7_trigger_watcher.py | 3 +- tests/test_mail_headers.py | 41 +- ...est_mail_headers_message_id_ssot_pin_a6.py | 196 ++ tests/test_metrics_adoption_pin.py | 230 ++ tests/test_migrations.py | 17 +- .../test_migrations_applied_body_immutable.py | 216 ++ .../test_migrations_no_projection_ssot_pin.py | 192 ++ ...est_n107_di_annotation_load_bearing_pin.py | 203 ++ tests/test_n109_picker_hide_pin.py | 143 + tests/test_n113_topic_backscan_baa_gate.py | 332 ++ tests/test_n114_style_profile_baa_gate.py | 147 + tests/test_n151_msal_cache_encryption.py | 314 ++ ...est_n151_msal_cache_encryption_ssot_pin.py | 258 ++ ..._n157_cli_admin_op_via_emitter_ssot_pin.py | 133 + ...test_n174_digest_preview_redact_pii_pin.py | 207 ++ ...test_n175_digest_preview_redact_pii_net.py | 494 +++ tests/test_n177_backup_msal_glob_pin.py | 135 + tests/test_n178_pin_harness_forked_parse.py | 194 ++ tests/test_n184_topic_hits_lifecycle_pin.py | 119 + ...st_n185_llm_health_clear_on_success_pin.py | 87 + tests/test_n186_json_col_oracle_pin.py | 96 + tests/test_n187_request_meta_ssot_pin.py | 198 ++ ...st_n190_cache_key_categories_digest_pin.py | 118 + tests/test_n192_audit_failure_sink.py | 75 + .../test_n193_imap_fetch_literal_ssot_pin.py | 148 + tests/test_n194_llm_chat_metrics_pin.py | 137 + .../test_n196_stdlib_vs_triage_logger_pin.py | 109 + tests/test_n197_cookie_secure_flag_pin.py | 84 + .../test_n198_backfill_chip_lifecycle_pin.py | 83 + .../test_n200_cookie_secure_flag_ssot_pin.py | 479 +++ ...201_backfill_chip_no_same_tick_wipe_pin.py | 193 ++ ...ndisk_artifact_no_singleton_default_pin.py | 197 ++ .../test_n204_action_idempotency_stamp_pin.py | 106 + ...211c_backfill_capped_source_mailbox_pin.py | 86 + tests/test_n213_watch_status_recorder_pin.py | 108 + tests/test_n214_pragma_opener_ssot_pin.py | 80 + ...test_n217_redacted_placeholder_ssot_pin.py | 66 + tests/test_n218_config_persist_ssot_pin.py | 114 + tests/test_n219_try_except_int_ratchet_pin.py | 125 + tests/test_n220_ollama_http_wrapped_pin.py | 187 + .../test_n221_last_message_dict_shape_pin.py | 180 + ...est_n222_is_admin_predicate_ratchet_pin.py | 121 + tests/test_n225_error_surface_ssot_pin.py | 143 + tests/test_n226_effective_hipaa_fold_pin.py | 152 + tests/test_n227_sweeper_status_stamp_pin.py | 114 + tests/test_n228_backoff_step_ssot_pin.py | 94 + tests/test_n229_jinja_is_admin_pin.py | 85 + ...test_n231_stored_secret_key_literal_pin.py | 75 + .../test_n232_error_surface_full_4arm_pin.py | 778 +++++ tests/test_n233_json_body_parse_pin.py | 124 + tests/test_n234_row_get_ratchet_pin.py | 85 + tests/test_n235_row_projection_ssot_pin.py | 67 + tests/test_n236_msal_cache_dispose_pin.py | 112 + ...238_session_epoch_privileged_update_pin.py | 230 ++ ...test_n240_triage_jobs_requeue_reset_pin.py | 117 + ...t_n241_style_distill_terminal_clear_pin.py | 224 ++ ...est_n242_triage_logger_ssot_ratchet_pin.py | 76 + tests/test_n244_short_id_ssot_pin.py | 51 + ..._n245_per_account_artifact_registry_pin.py | 98 + .../test_n246_watcher_errors_monotone_pin.py | 64 + ..._provider_labels_cache_invalidation_pin.py | 119 + tests/test_n248_shim_router_ssot_pin.py | 83 + tests/test_n249_raw_settings_write_pin.py | 131 + tests/test_n253_email_canonical_ssot_pin.py | 123 + tests/test_n254_truncate_ssot_pin.py | 100 + ...255_record_provider_outcome_ratchet_pin.py | 95 + tests/test_n256_gmail_watch_lifecycle_pin.py | 120 + ...ord_message_result_clear_on_success_pin.py | 45 + tests/test_n258_canonical_json_signing_pin.py | 89 + .../test_n259_backup_artifact_registry_pin.py | 39 + tests/test_n260_raw_audit_insert_pin.py | 127 + ..._record_message_result_rich_payload_pin.py | 82 + tests/test_n262_hipaa_flip_cascade_pin.py | 85 + ...n263_datetime_fromisoformat_ratchet_pin.py | 85 + tests/test_n263d_gmail_watch_live_ssot_pin.py | 287 ++ ...n266_hipaa_helper_migration_ratchet_pin.py | 82 + tests/test_n269_digest_date_header_pin.py | 74 + tests/test_n269b_cadence_window_runtime.py | 67 + ...n270_task_supervisor_recovery_clear_pin.py | 79 + tests/test_n272_mime_envelope_builder_pin.py | 93 + ...n272b_emailmessage_hardened_headers_pin.py | 156 + ..._n275_no_hard_delete_email_accounts_pin.py | 91 + tests/test_n276_notify_webhook_wired_pin.py | 123 + ..._n277_provider_labels_folder_create_pin.py | 49 + tests/test_n278_to_utc_iso_z_ssot_pin.py | 95 + tests/test_n279_llm_classify_timer_pin.py | 63 + ...n280_mailbox_removed_settings_sweep_pin.py | 51 + ...est_n281_ondisk_artifact_structural_pin.py | 136 + tests/test_n300_positional_args_hipaa_gate.py | 205 ++ ...n314_golden_hash_gitattributes_meta_pin.py | 248 ++ tests/test_n314_is_admin_pragma_order_pins.py | 148 + .../test_n315_hours_until_z_form_execution.py | 61 + ...16_explicit_token_cache_path_branch_pin.py | 156 + tests/test_n317_canonical_email_idna_pin.py | 78 + tests/test_n320_key_history_purge_pin.py | 101 + tests/test_n321_request_forwarded_for_pin.py | 155 + tests/test_n322_renormalize_emails_pin.py | 64 + tests/test_n326_c5_allowed_signers_ssot.py | 142 + ...st_n326_c5_codeowners_covers_trust_root.py | 66 + ...t_n326_c5_op_skip_requires_signed_proof.py | 186 + ...2_no_exc_info_on_debug_info_warning_pin.py | 95 + ...5b_function_scope_import_resolution_pin.py | 199 ++ ..._n335b_function_scope_shadow_import_pin.py | 198 ++ ...est_n338_gate_no_bare_str_allowlist_pin.py | 203 ++ tests/test_n338c_ast_name_resolution_pin.py | 339 ++ ...339_task_supervisor_historic_fields_pin.py | 151 + ..._n342_hipaa_cascade_no_nested_begin_pin.py | 139 + tests/test_n346c_dual_lookup_shim.py | 296 ++ tests/test_n349_hybrid_bypass.py | 236 ++ tests/test_n349b_item1_bypass_consume_emit.py | 144 + ...49b_item3_watch_hmac_hydration_async_db.py | 243 ++ ...353_triage_logger_reserved_set_complete.py | 160 + ...per_account_artifacts_failure_isolation.py | 143 + ...st_n355_audit_trusted_proxies_roundtrip.py | 76 + ...dispose_msal_cache_cli_old_shape_orphan.py | 113 + tests/test_n357_parse_iso_aware_ssot_pin.py | 175 + ...re_notification_unconditional_audit_log.py | 100 + ...n363_atomic_consume_hmac_clobber_bypass.py | 102 + ...n364_hmac_clobber_bypass_ttl_and_status.py | 146 + ..._n372_classify_truncated_1h_counter_pin.py | 166 + tests/test_n378_ollama_think_knob_ssot.py | 254 ++ ...re_notification_forbidden_keys_ssot_pin.py | 145 + tests/test_n410_fold_in.py | 190 ++ tests/test_n431_ollama_reuse_ssot_pin.py | 560 +++ .../test_n432_boot_assert_sqlite_floor_pin.py | 112 + .../test_n432_migration_live_app_guard_pin.py | 206 ++ tests/test_n432_raw_sqlite_connect_pin.py | 91 + ...ipts_ollama_route_via_leira_prepass_pin.py | 180 + tests/test_n432_sqlite_walreset_p0_pin.py | 225 ++ tests/test_n432_walreset_wiring_pin.py | 222 ++ tests/test_n433_ollama_keep_alive_ssot_pin.py | 243 ++ tests/test_n67_hipaa_counter_auth_stale.py | 253 ++ tests/test_n68_routes_xss_escape.py | 169 + ...est_n69_o365_subscription_lifecycle_pin.py | 310 ++ tests/test_n71_sender_domain_ssot_pin.py | 319 ++ tests/test_n72_topic_backscan_hipaa_audit.py | 336 ++ .../test_n73_recipient_digest_hipaa_audit.py | 460 +++ ...ngine_per_account_redaction_integration.py | 243 ++ ..._n74_per_account_hipaa_reason_redaction.py | 285 ++ .../test_n76_deactivate_clears_auth_stale.py | 193 ++ tests/test_n82_auth_stale_scan_recent.py | 549 +++ tests/test_n83_provider_error_xss_escape.py | 185 + tests/test_name_resolution_smoke_pin.py | 71 + tests/test_netloc_locality_ssot_pin.py | 280 ++ tests/test_new_run_id_ssot_pin.py | 101 + tests/test_no_cross_loop_audit_drain_pin.py | 113 + tests/test_no_direct_app_state_db_method.py | 40 +- tests/test_no_double_prefix_get_logger_pin.py | 142 + tests/test_no_duplicate_toplevel_defs.py | 197 ++ tests/test_no_hardcoded_sqlite_timeout_pin.py | 100 + ...test_no_hardcoded_timeout_broadscan_pin.py | 153 + tests/test_no_legb_shadow_asyncio.py | 46 +- ...t_no_sensitive_purpose_literal_slug_pin.py | 235 ++ tests/test_no_unguarded_secret_write.py | 409 +++ ...est_no_unprotected_asyncio_to_thread_db.py | 52 +- tests/test_now_iso_utc_pin_a10.py | 460 +++ tests/test_oauth_secret_keys_ssot_pin.py | 94 + tests/test_ondisk_disposal_old_shape_pin.py | 196 ++ tests/test_ops_token_read_pin.py | 181 + tests/test_owned_clock_def_crud.py | 201 ++ ...test_owned_clock_def_enabled_toggle_pin.py | 266 ++ tests/test_owned_clock_def_validation.py | 129 + .../test_owned_clock_def_write_funnel_pin.py | 83 + tests/test_per_topic_confidence_floor.py | 194 ++ tests/test_persisted_error_scrub_pin.py | 234 ++ tests/test_pin_harness_adoption.py | 876 +++++ tests/test_pin_line_anchoring_meta_pin.py | 117 + tests/test_pin_registry.py | 229 ++ tests/test_pin_vacuous_allowlist_killer.py | 294 ++ .../test_privacy_invariants_distill_hipaa.py | 145 +- tests/test_privacy_invariants_log_scrub.py | 165 +- tests/test_privacy_invariants_m7_hipaa.py | 9 +- tests/test_privacy_invariants_m_series.py | 17 +- ...ivacy_invariants_no_embedding_telemetry.py | 2 +- tests/test_provider_errors.py | 4 +- tests/test_provider_label_egress_ssot_pin.py | 236 ++ ..._provider_labels_cache_invalidation_pin.py | 233 ++ tests/test_providers/test_factory.py | 82 +- tests/test_providers/test_gmail_api.py | 35 + tests/test_providers/test_imap.py | 84 +- .../test_n151_soft_gate_predicate.py | 225 ++ tests/test_providers/test_office365.py | 65 +- tests/test_providers/test_search_iter.py | 47 + tests/test_pywin32_marker_pin.py | 123 + ...est_query_lang_gmail_operator_rejection.py | 97 + ...st_rapid_cve_bump_already_patched_guard.py | 252 ++ tests/test_rapid_cve_bump_commit_shape.py | 267 ++ tests/test_rapid_cve_bump_require_fields.py | 288 ++ .../test_record_mailbox_outcome_kwarg_pin.py | 78 + tests/test_record_triage_run_scope_pin.py | 168 + tests/test_redact_exc_n42.py | 197 ++ tests/test_redaction_sentinel_pin.py | 69 + tests/test_redirect_qs_ssot_pin.py | 227 ++ tests/test_redis_client_factory_pin.py | 162 + tests/test_redis_fallback_breaker_pin.py | 209 ++ tests/test_regulatory_clock_calendar.py | 135 + tests/test_regulatory_clock_events.py | 130 + tests/test_regulatory_clock_lifecycle.py | 285 ++ tests/test_regulatory_clock_schema.py | 119 + tests/test_regulatory_clock_seed.py | 161 + tests/test_regulatory_clock_sweep.py | 247 ++ tests/test_rendered_error_scrub_pin.py | 226 ++ tests/test_repair_daemon_live_wire.py | 88 + tests/test_request_id.py | 81 +- tests/test_resource_authz_ssot_pin.py | 250 ++ tests/test_retry_backoff_ssot_pin.py | 196 ++ tests/test_retry_drainer.py | 168 +- tests/test_retry_failure_kind_all_paths.py | 19 +- tests/test_rule_types_ssot_pin.py | 144 + tests/test_safe_int_request_parse_pin.py | 214 ++ tests/test_scan_pin_harness_adoption_teeth.py | 113 + tests/test_schema_version_read_ssot_pin.py | 86 + tests/test_secret_key_name_ssot_pin.py | 174 + tests/test_secret_read_funnel_pin.py | 140 + tests/test_secret_scrub_ssot_pin.py | 159 + tests/test_secrets.py | 13 +- tests/test_security_token_logging.py | 19 +- tests/test_select_star_ratchet_pin.py | 158 + tests/test_sent_folder_auto_detect_n402.py | 257 ++ tests/test_sent_folder_normalize_pin.py | 201 ++ tests/test_session_epoch_on_revoke_pin.py | 202 ++ tests/test_settings_key_catalog_pin.py | 398 +++ tests/test_silent_swallow_ratchet_pin.py | 328 ++ ...test_sme_gate_hook_trigger_coverage_pin.py | 102 + tests/test_sme_hook_env_var_path_expansion.py | 120 + tests/test_smtp_client_ssot_pin.py | 57 + tests/test_smtp_transport_ssot_pin.py | 91 + tests/test_sqlite_log_handler_closed_db.py | 19 +- tests/test_sqlite_version_floor_ssot_pin.py | 174 + tests/test_ssot_declaration_hardened_pin.py | 325 ++ tests/test_ssot_registry_bijection_pin.py | 465 +++ tests/test_ssot_registry_coverage_pin.py | 63 + ...est_ssot_src_declaration_registered_pin.py | 310 ++ tests/test_style_distill_queue.py | 137 +- tests/test_subject_reply_prefix_ssot_pin.py | 71 + ...pervised_interval_loop_rid_rotation_pin.py | 71 + .../test_sync_sqlite_in_async_ratchet_pin.py | 145 + tests/test_time_time_duration_ratchet_pin.py | 118 + tests/test_topic_backscan_all_folders.py | 320 ++ tests/test_topic_backscan_resume.py | 204 ++ tests/test_topic_configs_roundtrip.py | 604 ++++ tests/test_topic_feedback_cache.py | 110 + tests/test_topic_hipaa_gates.py | 829 +++++ tests/test_topic_match_categories_pin.py | 67 + tests/test_topic_match_confidence_floor.py | 247 ++ tests/test_topic_match_prompt.py | 310 ++ tests/test_topic_matcher.py | 1076 ++++++ tests/test_topic_prefilter.py | 411 +++ tests/test_topic_recall_harness.py | 672 ++++ tests/test_triage_completed_emit_pin.py | 234 ++ tests/test_triage_failed_action_status.py | 3 +- tests/test_triage_run_counter_ssot_pin.py | 77 + tests/test_triage_run_outcome_ssot_pin.py | 228 ++ tests/test_triage_run_trigger_vocab_pin.py | 227 ++ tests/test_uidvalidity_reseed_chip_pin.py | 294 ++ tests/test_url_query_secret_scrub_ssot_pin.py | 199 ++ tests/test_us_datetime.py | 124 + ...est_us_datetime_install_filter_ssot_pin.py | 300 ++ tests/test_user_soft_delete_pin.py | 137 + tests/test_v35_consolidate_retry_queues.py | 3 +- .../test_v37_rekey_watch_fire_idempotency.py | 3 +- ...test_verified_test_ratchet_auto_include.py | 106 + tests/test_walk_defensive_xfail_ssot_pin.py | 313 ++ tests/test_wall_clock_hm_labeling_pin.py | 152 + tests/test_watcher_failing_lifecycle_pin.py | 424 +++ ...st_watcher_retry_auth_revoked_resurrect.py | 237 ++ ..._watcher_retry_dead_reason_symmetry_pin.py | 291 ++ tests/test_watcher_retry_queue.py | 5 +- ...st_watcher_sticky_error_write_order_pin.py | 142 + ...est_watcher_terminal_outcome_funnel_pin.py | 117 + tests/test_web/conftest.py | 237 +- .../test_288_concurrent_connection_use.py | 3 +- .../test_web/test_288_phase3_d3_audit_gap.py | 608 +++- ...test_290_phase0_concurrent_lock_acquire.py | 5 +- ...t_291_writer_queuefull_audit_durability.py | 3 +- tests/test_web/test_access_audit.py | 335 +- tests/test_web/test_account_delegates.py | 103 + tests/test_web/test_account_delete_purge.py | 736 ++++ tests/test_web/test_account_wizard.py | 4 +- .../test_web/test_account_wizard_autochain.py | 3 +- tests/test_web/test_accounts.py | 26 +- tests/test_web/test_action_guide.py | 285 ++ tests/test_web/test_admin_alerts.py | 492 +++ .../test_admin_dashboard_n151_health_row.py | 109 + ...t_admin_stats_watcher_states_regression.py | 198 ++ tests/test_web/test_ai_backends_admin.py | 18 +- tests/test_web/test_ai_backends_probe_ssrf.py | 151 + tests/test_web/test_api_keys_ui.py | 58 +- tests/test_web/test_apikeys.py | 10 + tests/test_web/test_async_db_safety.py | 66 +- .../test_audit_chain_verify_surface.py | 79 + tests/test_web/test_audit_hardening.py | 4 +- tests/test_web/test_auth.py | 14 +- tests/test_web/test_auth_stale.py | 94 +- tests/test_web/test_backfill_capped_chip.py | 149 + tests/test_web/test_backup_config_save.py | 214 ++ tests/test_web/test_backup_fail_closed.py | 229 ++ tests/test_web/test_bulk_triage.py | 277 ++ tests/test_web/test_calendars_route.py | 16 +- tests/test_web/test_compliance_audit_caps.py | 131 + tests/test_web/test_config.py | 30 + tests/test_web/test_csrf.py | 129 +- tests/test_web/test_csrf_client_injector.py | 107 + tests/test_web/test_csrf_input_helper.py | 8 +- .../test_web/test_csrf_route_coverage_pin.py | 118 + tests/test_web/test_daily_health_email.py | 344 +- tests/test_web/test_dashboard_redesign.py | 94 + tests/test_web/test_db_n_plus_one.py | 22 +- tests/test_web/test_db_pragmas.py | 5 +- tests/test_web/test_db_writelock.py | 67 +- tests/test_web/test_dependencies.py | 15 +- .../test_digest_candidates_phi_render.py | 155 + tests/test_web/test_digest_editor.py | 272 ++ .../test_digest_preview_viewer_aware_pin.py | 222 ++ tests/test_web/test_digest_recipient.py | 5 +- tests/test_web/test_discover.py | 20 +- .../test_discover_error_scrub_render.py | 305 ++ .../test_discover_folder_phi_render.py | 147 + tests/test_web/test_email_watches.py | 99 +- tests/test_web/test_embedding_model_change.py | 294 ++ tests/test_web/test_escalation_test_send.py | 2 +- tests/test_web/test_events.py | 45 + tests/test_web/test_explain_error.py | 33 +- .../test_flash_error_surface_scaffold.py | 126 + tests/test_web/test_gmail_push_consumer.py | 157 +- tests/test_web/test_handler_helpers.py | 3 +- tests/test_web/test_health.py | 252 +- tests/test_web/test_help_tasks.py | 2 +- tests/test_web/test_hipaa_access_events.py | 13 +- tests/test_web/test_integrations_admin.py | 108 +- .../test_integrations_redis_section.py | 8 + ...tegrations_webhook_secret_unreadable_a5.py | 88 + .../test_web/test_last_login_terminus_pin.py | 241 ++ tests/test_web/test_lists.py | 56 + tests/test_web/test_log_chain.py | 62 + tests/test_web/test_logging.py | 17 +- tests/test_web/test_login_guard.py | 297 +- tests/test_web/test_metrics_endpoint.py | 30 + tests/test_web/test_mfa.py | 5 +- tests/test_web/test_n109_picker_hide.py | 136 + tests/test_web/test_n179_error_surface_pin.py | 372 ++ tests/test_web/test_o365_probe_ui.py | 59 + tests/test_web/test_o365_push_ui.py | 39 + tests/test_web/test_office365_push.py | 58 + .../test_web/test_office365_push_consumer.py | 37 + .../test_office365_subscription_renewer.py | 128 +- tests/test_web/test_openclaw.py | 87 +- tests/test_web/test_openclaw_bulk.py | 102 +- tests/test_web/test_openclaw_calendar.py | 39 +- tests/test_web/test_openclaw_digests.py | 22 +- tests/test_web/test_openclaw_watches.py | 28 +- tests/test_web/test_ops_db_backup.py | 66 + tests/test_web/test_profile_security.py | 121 + tests/test_web/test_profile_style_data.py | 73 + tests/test_web/test_pwa.py | 2 +- .../test_web/test_regulatory_clock_action.py | 331 ++ .../test_regulatory_clock_calendar_write.py | 207 ++ .../test_regulatory_clock_digest_section.py | 208 ++ .../test_regulatory_clock_escalation_push.py | 84 + .../test_web/test_regulatory_clocks_editor.py | 241 ++ tests/test_web/test_regulatory_clocks_page.py | 125 + tests/test_web/test_reindex_aggregate.py | 168 + tests/test_web/test_reindex_supersede.py | 169 + tests/test_web/test_retry_queue_admin.py | 177 +- tests/test_web/test_retry_queue_health.py | 232 ++ tests/test_web/test_revoke_audit_route.py | 49 + tests/test_web/test_rule_provider_labels.py | 125 +- .../test_runtime_settings_hipaa_gate.py | 149 +- .../test_secret_read_collapse_pin_a5.py | 443 +++ .../test_web/test_secret_write_no_clobber.py | 153 + tests/test_web/test_self_event_render.py | 3 + tests/test_web/test_settings_hardening.py | 3 +- .../test_style_data_preview_and_mine.py | 5 + .../test_web/test_style_mine_bulk_handoff.py | 3 +- tests/test_web/test_template_phi_guard.py | 15 +- tests/test_web/test_template_phi_render.py | 6 +- tests/test_web/test_tick_health_isolation.py | 40 + tests/test_web/test_topic_digest_router.py | 1828 ++++++++++ tests/test_web/test_topic_digest_sender.py | 665 ++++ .../test_topic_retriage_status_routes.py | 120 + tests/test_web/test_topic_search_summary.py | 116 + tests/test_web/test_triage_one_topic_match.py | 280 ++ tests/test_web/test_triage_self_skip.py | 13 +- tests/test_web/test_ui.py | 83 +- tests/test_web/test_users.py | 2 +- ...v20_sent_folder_override_list_migration.py | 15 +- tests/test_web/test_watch.py | 99 +- .../test_web/test_watch_auto_retire_audit.py | 119 + .../test_watch_auto_retire_secret_cleanup.py | 181 + tests/test_web/test_watch_failure_retry.py | 76 +- .../test_watch_grouping_null_creator.py | 137 + tests/test_web/test_watch_runner.py | 176 +- tests/test_web/test_watches_migration.py | 3 +- tests/test_web/test_watches_profile.py | 75 +- tests/test_web/test_webhook_egress_gate_a4.py | 91 + tests/test_web/test_webhook_preview.py | 556 +++ tests/test_webauthn.py | 43 +- tests/test_webhook_e2e_mock_transport.py | 340 ++ tests/test_webhook_render_for_target.py | 521 +++ tests/test_webhook_secret_binding_ssot_pin.py | 132 + tests/test_webhook_ssot_pins.py | 422 +++ tests/test_yaml_secret_disjointness_pin.py | 128 + tests/valk_allowlist_registry.json | 176 + vendor/sqlite3.expected.sha256 | 2 + 1042 files changed, 182028 insertions(+), 11146 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/op-approve/allowed_signers create mode 100644 .github/workflows/arm64-build-check.yml create mode 100644 .github/workflows/e2e.yml create mode 100644 .github/workflows/tests.yml create mode 100644 Containerfile.test delete mode 100644 INCIDENT_RESPONSE.md create mode 100644 audit-reports/divergence-census-2026-07-24-public-stub.md create mode 100644 scratchpad/enumerate_silent_swallow.py create mode 100644 scratchpad/generate_select_star_baseline.py create mode 100644 scratchpad/generate_silent_swallow_baseline.py create mode 100644 scratchpad/generate_sync_sqlite_async_baseline.py create mode 100644 scratchpad/leira_census_session.py create mode 100644 scratchpad/leira_exception_chain_audit.py create mode 100644 scratchpad/leira_js_base64_audit.py create mode 100644 scratchpad/select_star_baseline.txt create mode 100644 scratchpad/silent_swallow_baseline.txt create mode 100644 scratchpad/sync_sqlite_async_baseline.txt create mode 100644 scratchpad_leira_130_js_audit.json create mode 100755 scripts/cve-arc-canary-verify.sh create mode 100644 scripts/cve-arc-lock-report.py create mode 100755 scripts/cve-arc-reenrich-historical.sh create mode 100644 scripts/db-backup-relocate.sh create mode 100644 scripts/git-hooks/commit-msg create mode 100644 scripts/leira_cache.py create mode 100644 scripts/leira_census_prepass.py create mode 100644 scripts/leira_class_hints.py create mode 100644 scripts/leira_config.json create mode 100644 scripts/leira_config.schema.json create mode 100644 scripts/leira_defer_sweep.py create mode 100644 scripts/leira_discover.py create mode 100644 scripts/leira_docs_summarizer.py create mode 100644 scripts/leira_draft.py create mode 100644 scripts/leira_flag_advisor.py create mode 100644 scripts/leira_hardcode_scanner.py create mode 100644 scripts/leira_log_triage.py create mode 100644 scripts/leira_newsletter_extract.py create mode 100644 scripts/leira_pr_prereview.py create mode 100644 scripts/leira_prepass.psm1 create mode 100644 scripts/leira_prepass.py create mode 100644 scripts/leira_style_precompute.py create mode 100644 scripts/leira_verify_fix.py create mode 100644 scripts/leira_verify_prepass.py create mode 100644 scripts/n432-path3-bake-step1.py create mode 100644 scripts/n432-path3-bake-step3.py create mode 100644 scripts/probe_leira_classify.py create mode 100644 scripts/punch-list-append.py create mode 100755 scripts/push-ci-shepherd.ps1 create mode 100755 scripts/rapid-cve-bump.py create mode 100644 scripts/run-sme-panel.py create mode 100644 scripts/spike_apsw_compat_perf.py create mode 100644 scripts/spike_sqlcipher3_wheels_dropin.py create mode 100644 scripts/spike_sqlcipher_cross_arch_compat.sh create mode 100644 scripts/structural_superset_check.py create mode 100755 scripts/test-e2e.sh create mode 100755 scripts/test-local-podman.sh create mode 100644 scripts/test_flake_classifier.py create mode 100644 scripts/test_flake_exclusions.json create mode 100644 scripts/verified-test.py create mode 100644 scripts/verify_leira_prepass_vs_run19.py create mode 100644 scripts/workflows/et-divergence-census-ANTHROPIC-DO-NOT-USE-2026-07-23.js create mode 100644 scripts/workflows/et-divergence-census-leira.js create mode 100644 scripts/workflows/feature-add-e2e-gate.py create mode 100644 src/email_triage/_atomicio.py create mode 100644 src/email_triage/_base64url.py create mode 100644 src/email_triage/_egress.py create mode 100644 src/email_triage/_envbool.py create mode 100644 src/email_triage/_hm_render.py create mode 100644 src/email_triage/_ids.py create mode 100644 src/email_triage/_int_utils.py create mode 100644 src/email_triage/_netloc.py create mode 100644 src/email_triage/_ollama_reuse.py create mode 100644 src/email_triage/_secret_keys.py create mode 100644 src/email_triage/_secret_scrub.py create mode 100644 src/email_triage/_signing.py create mode 100644 src/email_triage/_textfmt.py create mode 100644 src/email_triage/_us_datetime.py create mode 100644 src/email_triage/actions/clinicaltrials.py create mode 100644 src/email_triage/actions/digest_resend.py create mode 100644 src/email_triage/actions/mute.py create mode 100644 src/email_triage/actions/provider_labels.py create mode 100644 src/email_triage/actions/regulatory_clock.py create mode 100644 src/email_triage/actions/topic_configs.py create mode 100644 src/email_triage/actions/topic_matcher.py create mode 100644 src/email_triage/actions/topic_prefilter.py create mode 100644 src/email_triage/actions/web_search.py create mode 100644 src/email_triage/cache/_redis.py create mode 100644 src/email_triage/cache/topic_feedback.py create mode 100644 src/email_triage/classify/safety_detect.py create mode 100644 src/email_triage/db/_live_app_gate.py create mode 100644 src/email_triage/db/regulatory_clock.py create mode 100644 src/email_triage/job_drain_safety.py create mode 100644 src/email_triage/jobs/topic_backscan.py create mode 100644 src/email_triage/llm_safety.py create mode 100644 src/email_triage/llm_truncation.py create mode 100644 src/email_triage/providers/_msal_cache_crypto.py create mode 100644 src/email_triage/providers/_ondisk_artifacts.py create mode 100644 src/email_triage/providers/imap_fetch_transport.py create mode 100644 src/email_triage/web/_auth_revoked.py create mode 100644 src/email_triage/web/_backfill_chip.py create mode 100644 src/email_triage/web/_cookies.py create mode 100644 src/email_triage/web/_credhash.py create mode 100644 src/email_triage/web/_flash.py create mode 100644 src/email_triage/web/_list_authz.py create mode 100644 src/email_triage/web/_mime_builder.py create mode 100644 src/email_triage/web/_pagination.py create mode 100644 src/email_triage/web/_resource_authz.py create mode 100644 src/email_triage/web/_triage_metrics.py create mode 100644 src/email_triage/web/_watch_status_recorder.py create mode 100644 src/email_triage/web/_webhook_target_types.py create mode 100644 src/email_triage/web/_webhook_template.py create mode 100644 src/email_triage/web/account_scope.py create mode 100644 src/email_triage/web/acme_http01.py create mode 100644 src/email_triage/web/action_guide.py create mode 100644 src/email_triage/web/admin_alerts.py create mode 100644 src/email_triage/web/error_html.py create mode 100644 src/email_triage/web/error_surface.py create mode 100644 src/email_triage/web/form_limits.py create mode 100644 src/email_triage/web/gmail_watch_status.py create mode 100644 src/email_triage/web/oauth_secret_keys.py create mode 100644 src/email_triage/web/routers/_json.py create mode 100644 src/email_triage/web/routers/_redirect.py create mode 100644 src/email_triage/web/routers/_secret_write.py rename src/email_triage/web/routers/{openclaw.py => agent_api.py} (78%) create mode 100644 src/email_triage/web/static/base64url.js create mode 100644 src/email_triage/web/templates/_clocks_table.html create mode 100644 src/email_triage/web/templates/_pager.html rename src/email_triage/web/templates/accounts/{api_keys.html => _api_keys_section.html} (78%) create mode 100644 src/email_triage/web/templates/accounts/_digest_candidates.html delete mode 100644 src/email_triage/web/templates/admin/backup.html create mode 100644 src/email_triage/web/templates/admin/config_tabs/_webhook_preview_fragment.html delete mode 100644 src/email_triage/web/templates/admin/integrations.html create mode 100644 src/email_triage/web/templates/help/me.html rename src/email_triage/web/templates/profile/{hardware_keys.html => _hardware_keys_section.html} (56%) create mode 100644 src/email_triage/web/templates/profile/_topic_backscan_status.html create mode 100644 src/email_triage/web/templates/profile/_topic_digest.html create mode 100644 src/email_triage/web/templates/profile/_topic_digest_editor.html create mode 100644 src/email_triage/web/templates/profile/_topic_digest_preview.html create mode 100644 src/email_triage/web/templates/profile/_topic_nct_result.html create mode 100644 src/email_triage/web/templates/profile/regulatory_clocks.html create mode 100644 src/email_triage/web/templates/profile/security.html create mode 100644 src/email_triage/web/templates/regulatory_clocks.html create mode 100644 src/email_triage/web/templates/triage/_reindex_aggregate.html create mode 100644 src/email_triage/web/topic_digest_sender.py create mode 100644 src/email_triage/web/topic_search_summary.py create mode 100644 tests/_canonical_email_pin_predicates.py create mode 100644 tests/_db_tracking.py create mode 100644 tests/_migration_fixtures.py create mode 100644 tests/_pin_harness.py create mode 100644 tests/copy_contract_registry.json create mode 100644 tests/e2e/__init__.py create mode 100644 tests/e2e/conftest.py create mode 100644 tests/e2e/test_admin_gate_and_url_encode_smokes.py create mode 100644 tests/e2e/test_admin_tab_help.py create mode 100644 tests/e2e/test_backscan_ceiling_banner.py create mode 100644 tests/e2e/test_backup_config.py create mode 100644 tests/e2e/test_compliance_audit_chains.py create mode 100644 tests/e2e/test_dashboard_key_export.py create mode 100644 tests/e2e/test_help_me.py create mode 100644 tests/e2e/test_integrations_flush.py create mode 100644 tests/e2e/test_n109_picker_hide_e2e.py create mode 100644 tests/e2e/test_regulatory_clocks.py create mode 100644 tests/e2e/test_regulatory_clocks_editor.py create mode 100644 tests/e2e/test_reindex_aggregate.py create mode 100644 tests/e2e/test_retry_queue_badge.py create mode 100644 tests/e2e/test_searxng_summary.py create mode 100644 tests/e2e/test_security_tab.py create mode 100644 tests/e2e/test_smoke.py create mode 100644 tests/e2e/test_topic_floor_editor.py create mode 100644 tests/e2e/test_topic_retriage_ui.py create mode 100644 tests/e2e/test_topic_roundtrip.py create mode 100644 tests/e2e/walk/__init__.py create mode 100644 tests/e2e/walk/_helpers.py create mode 100644 tests/e2e/walk/test_accounts_api_keys_page.py create mode 100644 tests/e2e/walk/test_action_regulatory_clock.py create mode 100644 tests/e2e/walk/test_admin_webhook_preview.py create mode 100644 tests/e2e/walk/test_ai_backends_probe_models.py create mode 100644 tests/e2e/walk/test_backfill_capped_dismiss.py create mode 100644 tests/e2e/walk/test_backup_config_save.py create mode 100644 tests/e2e/walk/test_clock_disposition_actions.py create mode 100644 tests/e2e/walk/test_digest_resend.py create mode 100644 tests/e2e/walk/test_help_me.py create mode 100644 tests/e2e/walk/test_help_me_ask.py create mode 100644 tests/e2e/walk/test_ops_db_backup_trigger.py create mode 100644 tests/e2e/walk/test_profile_hardware_keys.py create mode 100644 tests/e2e/walk/test_profile_security_walk.py create mode 100644 tests/e2e/walk/test_provider_labels_apply.py create mode 100644 tests/e2e/walk/test_regulatory_clocks_create.py create mode 100644 tests/e2e/walk/test_regulatory_clocks_dashboard.py create mode 100644 tests/e2e/walk/test_regulatory_clocks_delete.py create mode 100644 tests/e2e/walk/test_regulatory_clocks_editor.py create mode 100644 tests/e2e/walk/test_regulatory_clocks_update.py create mode 100644 tests/e2e/walk/test_reindex_jobs_aggregate_progress_fragment.py create mode 100644 tests/e2e/walk/test_topic_backscan_worker.py create mode 100644 tests/e2e/walk/test_topic_digest_delete.py create mode 100644 tests/e2e/walk/test_topic_digest_delete_hit.py create mode 100644 tests/e2e/walk/test_topic_digest_edit.py create mode 100644 tests/e2e/walk/test_topic_digest_feedback_unmute.py create mode 100644 tests/e2e/walk/test_topic_digest_hit_feedback.py create mode 100644 tests/e2e/walk/test_topic_digest_include_hit.py create mode 100644 tests/e2e/walk/test_topic_digest_new.py create mode 100644 tests/e2e/walk/test_topic_digest_preview.py create mode 100644 tests/e2e/walk/test_topic_digest_retriage.py create mode 100644 tests/e2e/walk/test_topic_digest_retriage_cancel.py create mode 100644 tests/e2e/walk/test_topic_digest_retriage_progress_fragment.py create mode 100644 tests/e2e/walk/test_topic_digest_save.py create mode 100644 tests/e2e/walk/test_topic_digest_test_details.py create mode 100644 tests/e2e/walk/test_topic_matcher.py create mode 100644 tests/e2e/walk/test_topic_prefilter.py create mode 100644 tests/goldens/allowed_signers.sha256 create mode 100644 tests/hooks/__init__.py create mode 100644 tests/hooks/test_cve_inject.py create mode 100644 tests/leira_multihost_symmetry.Tests.ps1 create mode 100644 tests/leira_multihost_vectors.json create mode 100644 tests/leira_ps_consumers_scaffold.Tests.ps1 create mode 100644 tests/test_account_administer_gate_pin.py create mode 100644 tests/test_account_delete_purge_pin.py create mode 100644 tests/test_account_email_ssot_pin.py create mode 100644 tests/test_account_hipaa_scope_pin.py create mode 100644 tests/test_account_scope_ssot_pin.py create mode 100644 tests/test_account_status_ssot_pin.py create mode 100644 tests/test_acme_challenge_dispatcher_ssot_pin.py create mode 100644 tests/test_acme_http01.py create mode 100644 tests/test_acme_wire_tls_path.py create mode 100644 tests/test_action_log_extras_ssot_pin.py create mode 100644 tests/test_actions/test_digest_extractor_timeout_budget_pin.py create mode 100644 tests/test_actions/test_draft_reply_hipaa_header.py create mode 100644 tests/test_actions/test_draft_reply_llm_body.py create mode 100644 tests/test_actions/test_mute.py create mode 100644 tests/test_actions/test_web_search.py create mode 100644 tests/test_admin_gate_ssot_pin.py create mode 100644 tests/test_allow_list_sentinel_pin.py create mode 100644 tests/test_arch_encryption_build_pin.py create mode 100644 tests/test_atomic_write_pin_a11.py create mode 100644 tests/test_audit_cancellederror_stash_pin.py create mode 100644 tests/test_audit_chain_byte_identity_freeze.py create mode 100644 tests/test_audit_chain_insert_ssot_pin.py create mode 100644 tests/test_audit_chain_pin.py create mode 100644 tests/test_audit_emit_swallow_pin.py create mode 100644 tests/test_audit_last_exc_stash_pin.py create mode 100644 tests/test_audit_phi_access_pin.py create mode 100644 tests/test_audit_safe_write.py create mode 100644 tests/test_auth_expiry_parse_pin.py create mode 100644 tests/test_auth_stale_lifecycle_pin.py create mode 100644 tests/test_authz_by_id_fetch_pin.py create mode 100644 tests/test_baa_gate_ssot_pin.py create mode 100644 tests/test_baa_paused_auto_clear_pin.py create mode 100644 tests/test_baa_runtime_gate_pin.py create mode 100644 tests/test_backfill_cap_ssot_pin.py create mode 100644 tests/test_backup_encrypted_verify.py create mode 100644 tests/test_bare_address_ssot_pin_a7.py create mode 100644 tests/test_base64url_ssot_pin.py create mode 100644 tests/test_base64url_ssot_unit.py create mode 100644 tests/test_cache/test_outer_key_bare_sender_pin.py create mode 100644 tests/test_calendar_gate_ssot_pin.py create mode 100644 tests/test_canonical_email_rsvp_gmail_pin.py create mode 100644 tests/test_canonical_email_ssot_pin.py create mode 100644 tests/test_canonical_email_ssot_providers_pin.py create mode 100644 tests/test_classify/test_ollama_unreachable_boundary.py create mode 100644 tests/test_classify/test_parse_llm_json_recovery.py create mode 100644 tests/test_classify/test_safety_detect.py create mode 100644 tests/test_classify/test_safety_detect_stage_b.py create mode 100644 tests/test_classify_egress_baa_gate_pin.py create mode 100644 tests/test_cli_backup.py create mode 100644 tests/test_cli_db_open_encryption_pin.py create mode 100644 tests/test_cli_engine_close_pin.py create mode 100644 tests/test_cli_provider_close_in_loop_pin.py create mode 100644 tests/test_cli_provider_oauth_register_pin.py create mode 100644 tests/test_cli_verify_migrations.py create mode 100644 tests/test_clinicaltrials_resolver.py create mode 100644 tests/test_clock_ics.py create mode 100644 tests/test_config_store_disjoint_pin.py create mode 100644 tests/test_config_yaml_roundtrip_pin.py create mode 100644 tests/test_copy_as_contract_pin.py create mode 100644 tests/test_counter_registry_ssot_pin.py create mode 100644 tests/test_create_task_tracking_pin.py create mode 100644 tests/test_credential_hash_ssot_pin.py create mode 100644 tests/test_credential_inputs_hardened.py create mode 100644 tests/test_dashboard_maintenance_banner_render_smoke.py create mode 100644 tests/test_db_snapshot.py create mode 100644 tests/test_defer_event_log_rotation.py create mode 100644 tests/test_defer_gate_op_approval.py create mode 100644 tests/test_defer_gate_trigger.py create mode 100644 tests/test_defer_gate_widening.py create mode 100644 tests/test_defer_gate_widening_2.py create mode 100644 tests/test_defer_phrase_weights.py create mode 100644 tests/test_defer_phrases_discovery.py create mode 100644 tests/test_defer_phrases_ssot.py create mode 100644 tests/test_defer_phrases_ssot_pin.py create mode 100644 tests/test_defer_safe_context.py create mode 100644 tests/test_defer_shape_router.py create mode 100644 tests/test_digest_candidates_render_smoke.py create mode 100644 tests/test_digest_render_ssot_pin.py create mode 100644 tests/test_digest_resend_ssot_pin.py create mode 100644 tests/test_digest_truncate_ellipsis_pin.py create mode 100644 tests/test_display_name_ssot_pin.py create mode 100644 tests/test_drain_audit_row_count_arm_pin.py create mode 100644 tests/test_dry_run_side_effects_pin.py create mode 100644 tests/test_egress_funnel_pin.py create mode 100644 tests/test_email_accounts_updated_at_freshness_pin.py create mode 100644 tests/test_embedding_bits/test_hf_cache_dir_ssot.py create mode 100644 tests/test_embedding_bits/test_manifest_arch_v3.py create mode 100644 tests/test_emit_counter_registry_ssot.py create mode 100644 tests/test_emit_llm_outcome.py create mode 100644 tests/test_encryption_aware_sqlite_open_ssot_pin.py create mode 100644 tests/test_engine/test_availability_conflict.py create mode 100644 tests/test_engine/test_embedding_staged_path_resolve.py create mode 100644 tests/test_envbool_ssot_pin_a9.py create mode 100644 tests/test_error_ctx_key_template_pin.py create mode 100644 tests/test_error_html_escape_pin_a8.py create mode 100644 tests/test_evidence_bound_claim_gate_extractor.py create mode 100644 tests/test_exc_render_twin_ssot_pin.py create mode 100644 tests/test_exception_chain_pin.py create mode 100644 tests/test_fernet_construction_allowlist_pin.py create mode 100644 tests/test_fetch_message_headers_only_pin.py create mode 100644 tests/test_flash_param_coupling_pin.py create mode 100644 tests/test_from_header_ssot_pin.py create mode 100644 tests/test_get_logger_idempotent.py create mode 100644 tests/test_gh_app_token_scaffold.py create mode 100644 tests/test_hipaa_error_key_scrub_pin.py create mode 100644 tests/test_hipaa_plaintext_advisory_ssot_pin.py create mode 100644 tests/test_hm_render_ssot_pin.py create mode 100644 tests/test_hm_render_ssot_unit.py create mode 100644 tests/test_html_strip_ssot_pin.py create mode 100644 tests/test_html_text_ssot_pin.py create mode 100644 tests/test_httpx_client_ssot_pin.py create mode 100644 tests/test_hwm_uidvalidity_pin.py create mode 100644 tests/test_imap_client_ssot_pin.py create mode 100644 tests/test_imap_literal_fetch_pin.py create mode 100644 tests/test_inflight_claim_no_await_gap_pin.py create mode 100644 tests/test_infunction_import_resolution_pin.py create mode 100644 tests/test_install_oauth_fields_ssot_pin.py create mode 100644 tests/test_is_active_ingestion_gate_pin.py create mode 100644 tests/test_is_htmx_ssot_pin.py create mode 100644 tests/test_js_base64url_ssot_pin.py create mode 100644 tests/test_json_col_decode_pin.py create mode 100644 tests/test_json_col_naming_pin.py create mode 100644 tests/test_json_col_write_pin.py create mode 100644 tests/test_leira_consumers_scaffold.py create mode 100644 tests/test_leira_defer_sweep_resume_pause_cache.py create mode 100644 tests/test_leira_multihost_symmetry.py create mode 100644 tests/test_leira_no_hardcode_pin.py create mode 100644 tests/test_leira_prepass_config_keys_pin.py create mode 100644 tests/test_leira_prepass_resilience.py create mode 100644 tests/test_lifecycle_status_ssot_pin.py create mode 100644 tests/test_lifecycle_triple_upsert_clear_pin.py create mode 100644 tests/test_list_rule_authz_pin.py create mode 100644 tests/test_llm_fence_coverage_pin.py create mode 100644 tests/test_llm_health_ssot_pin.py create mode 100644 tests/test_llm_timer_outcome_counter_pin.py create mode 100644 tests/test_llm_truncation.py create mode 100644 tests/test_llm_truncation_check_pin.py create mode 100644 tests/test_llm_truncation_ssot_pin.py create mode 100644 tests/test_load_json_col_pin.py create mode 100644 tests/test_mail_headers_message_id_ssot_pin_a6.py create mode 100644 tests/test_metrics_adoption_pin.py create mode 100644 tests/test_migrations_applied_body_immutable.py create mode 100644 tests/test_migrations_no_projection_ssot_pin.py create mode 100644 tests/test_n107_di_annotation_load_bearing_pin.py create mode 100644 tests/test_n109_picker_hide_pin.py create mode 100644 tests/test_n113_topic_backscan_baa_gate.py create mode 100644 tests/test_n114_style_profile_baa_gate.py create mode 100644 tests/test_n151_msal_cache_encryption.py create mode 100644 tests/test_n151_msal_cache_encryption_ssot_pin.py create mode 100644 tests/test_n157_cli_admin_op_via_emitter_ssot_pin.py create mode 100644 tests/test_n174_digest_preview_redact_pii_pin.py create mode 100644 tests/test_n175_digest_preview_redact_pii_net.py create mode 100644 tests/test_n177_backup_msal_glob_pin.py create mode 100644 tests/test_n178_pin_harness_forked_parse.py create mode 100644 tests/test_n184_topic_hits_lifecycle_pin.py create mode 100644 tests/test_n185_llm_health_clear_on_success_pin.py create mode 100644 tests/test_n186_json_col_oracle_pin.py create mode 100644 tests/test_n187_request_meta_ssot_pin.py create mode 100644 tests/test_n190_cache_key_categories_digest_pin.py create mode 100644 tests/test_n192_audit_failure_sink.py create mode 100644 tests/test_n193_imap_fetch_literal_ssot_pin.py create mode 100644 tests/test_n194_llm_chat_metrics_pin.py create mode 100644 tests/test_n196_stdlib_vs_triage_logger_pin.py create mode 100644 tests/test_n197_cookie_secure_flag_pin.py create mode 100644 tests/test_n198_backfill_chip_lifecycle_pin.py create mode 100644 tests/test_n200_cookie_secure_flag_ssot_pin.py create mode 100644 tests/test_n201_backfill_chip_no_same_tick_wipe_pin.py create mode 100644 tests/test_n202_ondisk_artifact_no_singleton_default_pin.py create mode 100644 tests/test_n204_action_idempotency_stamp_pin.py create mode 100644 tests/test_n211c_backfill_capped_source_mailbox_pin.py create mode 100644 tests/test_n213_watch_status_recorder_pin.py create mode 100644 tests/test_n214_pragma_opener_ssot_pin.py create mode 100644 tests/test_n217_redacted_placeholder_ssot_pin.py create mode 100644 tests/test_n218_config_persist_ssot_pin.py create mode 100644 tests/test_n219_try_except_int_ratchet_pin.py create mode 100644 tests/test_n220_ollama_http_wrapped_pin.py create mode 100644 tests/test_n221_last_message_dict_shape_pin.py create mode 100644 tests/test_n222_is_admin_predicate_ratchet_pin.py create mode 100644 tests/test_n225_error_surface_ssot_pin.py create mode 100644 tests/test_n226_effective_hipaa_fold_pin.py create mode 100644 tests/test_n227_sweeper_status_stamp_pin.py create mode 100644 tests/test_n228_backoff_step_ssot_pin.py create mode 100644 tests/test_n229_jinja_is_admin_pin.py create mode 100644 tests/test_n231_stored_secret_key_literal_pin.py create mode 100644 tests/test_n232_error_surface_full_4arm_pin.py create mode 100644 tests/test_n233_json_body_parse_pin.py create mode 100644 tests/test_n234_row_get_ratchet_pin.py create mode 100644 tests/test_n235_row_projection_ssot_pin.py create mode 100644 tests/test_n236_msal_cache_dispose_pin.py create mode 100644 tests/test_n238_session_epoch_privileged_update_pin.py create mode 100644 tests/test_n240_triage_jobs_requeue_reset_pin.py create mode 100644 tests/test_n241_style_distill_terminal_clear_pin.py create mode 100644 tests/test_n242_triage_logger_ssot_ratchet_pin.py create mode 100644 tests/test_n244_short_id_ssot_pin.py create mode 100644 tests/test_n245_per_account_artifact_registry_pin.py create mode 100644 tests/test_n246_watcher_errors_monotone_pin.py create mode 100644 tests/test_n247_provider_labels_cache_invalidation_pin.py create mode 100644 tests/test_n248_shim_router_ssot_pin.py create mode 100644 tests/test_n249_raw_settings_write_pin.py create mode 100644 tests/test_n253_email_canonical_ssot_pin.py create mode 100644 tests/test_n254_truncate_ssot_pin.py create mode 100644 tests/test_n255_record_provider_outcome_ratchet_pin.py create mode 100644 tests/test_n256_gmail_watch_lifecycle_pin.py create mode 100644 tests/test_n257_record_message_result_clear_on_success_pin.py create mode 100644 tests/test_n258_canonical_json_signing_pin.py create mode 100644 tests/test_n259_backup_artifact_registry_pin.py create mode 100644 tests/test_n260_raw_audit_insert_pin.py create mode 100644 tests/test_n261_record_message_result_rich_payload_pin.py create mode 100644 tests/test_n262_hipaa_flip_cascade_pin.py create mode 100644 tests/test_n263_datetime_fromisoformat_ratchet_pin.py create mode 100644 tests/test_n263d_gmail_watch_live_ssot_pin.py create mode 100644 tests/test_n266_hipaa_helper_migration_ratchet_pin.py create mode 100644 tests/test_n269_digest_date_header_pin.py create mode 100644 tests/test_n269b_cadence_window_runtime.py create mode 100644 tests/test_n270_task_supervisor_recovery_clear_pin.py create mode 100644 tests/test_n272_mime_envelope_builder_pin.py create mode 100644 tests/test_n272b_emailmessage_hardened_headers_pin.py create mode 100644 tests/test_n275_no_hard_delete_email_accounts_pin.py create mode 100644 tests/test_n276_notify_webhook_wired_pin.py create mode 100644 tests/test_n277_provider_labels_folder_create_pin.py create mode 100644 tests/test_n278_to_utc_iso_z_ssot_pin.py create mode 100644 tests/test_n279_llm_classify_timer_pin.py create mode 100644 tests/test_n280_mailbox_removed_settings_sweep_pin.py create mode 100644 tests/test_n281_ondisk_artifact_structural_pin.py create mode 100644 tests/test_n300_positional_args_hipaa_gate.py create mode 100644 tests/test_n314_golden_hash_gitattributes_meta_pin.py create mode 100644 tests/test_n314_is_admin_pragma_order_pins.py create mode 100644 tests/test_n315_hours_until_z_form_execution.py create mode 100644 tests/test_n316_explicit_token_cache_path_branch_pin.py create mode 100644 tests/test_n317_canonical_email_idna_pin.py create mode 100644 tests/test_n320_key_history_purge_pin.py create mode 100644 tests/test_n321_request_forwarded_for_pin.py create mode 100644 tests/test_n322_renormalize_emails_pin.py create mode 100644 tests/test_n326_c5_allowed_signers_ssot.py create mode 100644 tests/test_n326_c5_codeowners_covers_trust_root.py create mode 100644 tests/test_n326_c5_op_skip_requires_signed_proof.py create mode 100644 tests/test_n332_no_exc_info_on_debug_info_warning_pin.py create mode 100644 tests/test_n335b_function_scope_import_resolution_pin.py create mode 100644 tests/test_n335b_function_scope_shadow_import_pin.py create mode 100644 tests/test_n338_gate_no_bare_str_allowlist_pin.py create mode 100644 tests/test_n338c_ast_name_resolution_pin.py create mode 100644 tests/test_n339_task_supervisor_historic_fields_pin.py create mode 100644 tests/test_n342_hipaa_cascade_no_nested_begin_pin.py create mode 100644 tests/test_n346c_dual_lookup_shim.py create mode 100644 tests/test_n349_hybrid_bypass.py create mode 100644 tests/test_n349b_item1_bypass_consume_emit.py create mode 100644 tests/test_n349b_item3_watch_hmac_hydration_async_db.py create mode 100644 tests/test_n353_triage_logger_reserved_set_complete.py create mode 100644 tests/test_n354_dispose_all_per_account_artifacts_failure_isolation.py create mode 100644 tests/test_n355_audit_trusted_proxies_roundtrip.py create mode 100644 tests/test_n356_dispose_msal_cache_cli_old_shape_orphan.py create mode 100644 tests/test_n357_parse_iso_aware_ssot_pin.py create mode 100644 tests/test_n359_fire_notification_unconditional_audit_log.py create mode 100644 tests/test_n363_atomic_consume_hmac_clobber_bypass.py create mode 100644 tests/test_n364_hmac_clobber_bypass_ttl_and_status.py create mode 100644 tests/test_n372_classify_truncated_1h_counter_pin.py create mode 100644 tests/test_n378_ollama_think_knob_ssot.py create mode 100644 tests/test_n380_fire_notification_forbidden_keys_ssot_pin.py create mode 100644 tests/test_n410_fold_in.py create mode 100644 tests/test_n431_ollama_reuse_ssot_pin.py create mode 100644 tests/test_n432_boot_assert_sqlite_floor_pin.py create mode 100644 tests/test_n432_migration_live_app_guard_pin.py create mode 100644 tests/test_n432_raw_sqlite_connect_pin.py create mode 100644 tests/test_n432_scripts_ollama_route_via_leira_prepass_pin.py create mode 100644 tests/test_n432_sqlite_walreset_p0_pin.py create mode 100644 tests/test_n432_walreset_wiring_pin.py create mode 100644 tests/test_n433_ollama_keep_alive_ssot_pin.py create mode 100644 tests/test_n67_hipaa_counter_auth_stale.py create mode 100644 tests/test_n68_routes_xss_escape.py create mode 100644 tests/test_n69_o365_subscription_lifecycle_pin.py create mode 100644 tests/test_n71_sender_domain_ssot_pin.py create mode 100644 tests/test_n72_topic_backscan_hipaa_audit.py create mode 100644 tests/test_n73_recipient_digest_hipaa_audit.py create mode 100644 tests/test_n74_flowengine_per_account_redaction_integration.py create mode 100644 tests/test_n74_per_account_hipaa_reason_redaction.py create mode 100644 tests/test_n76_deactivate_clears_auth_stale.py create mode 100644 tests/test_n82_auth_stale_scan_recent.py create mode 100644 tests/test_n83_provider_error_xss_escape.py create mode 100644 tests/test_name_resolution_smoke_pin.py create mode 100644 tests/test_netloc_locality_ssot_pin.py create mode 100644 tests/test_new_run_id_ssot_pin.py create mode 100644 tests/test_no_cross_loop_audit_drain_pin.py create mode 100644 tests/test_no_double_prefix_get_logger_pin.py create mode 100644 tests/test_no_duplicate_toplevel_defs.py create mode 100644 tests/test_no_hardcoded_sqlite_timeout_pin.py create mode 100644 tests/test_no_hardcoded_timeout_broadscan_pin.py create mode 100644 tests/test_no_sensitive_purpose_literal_slug_pin.py create mode 100644 tests/test_no_unguarded_secret_write.py create mode 100644 tests/test_now_iso_utc_pin_a10.py create mode 100644 tests/test_oauth_secret_keys_ssot_pin.py create mode 100644 tests/test_ondisk_disposal_old_shape_pin.py create mode 100644 tests/test_ops_token_read_pin.py create mode 100644 tests/test_owned_clock_def_crud.py create mode 100644 tests/test_owned_clock_def_enabled_toggle_pin.py create mode 100644 tests/test_owned_clock_def_validation.py create mode 100644 tests/test_owned_clock_def_write_funnel_pin.py create mode 100644 tests/test_per_topic_confidence_floor.py create mode 100644 tests/test_persisted_error_scrub_pin.py create mode 100644 tests/test_pin_harness_adoption.py create mode 100644 tests/test_pin_line_anchoring_meta_pin.py create mode 100644 tests/test_pin_registry.py create mode 100644 tests/test_pin_vacuous_allowlist_killer.py create mode 100644 tests/test_provider_label_egress_ssot_pin.py create mode 100644 tests/test_provider_labels_cache_invalidation_pin.py create mode 100644 tests/test_providers/test_n151_soft_gate_predicate.py create mode 100644 tests/test_pywin32_marker_pin.py create mode 100644 tests/test_query_lang_gmail_operator_rejection.py create mode 100644 tests/test_rapid_cve_bump_already_patched_guard.py create mode 100644 tests/test_rapid_cve_bump_commit_shape.py create mode 100644 tests/test_rapid_cve_bump_require_fields.py create mode 100644 tests/test_record_mailbox_outcome_kwarg_pin.py create mode 100644 tests/test_record_triage_run_scope_pin.py create mode 100644 tests/test_redact_exc_n42.py create mode 100644 tests/test_redaction_sentinel_pin.py create mode 100644 tests/test_redirect_qs_ssot_pin.py create mode 100644 tests/test_redis_client_factory_pin.py create mode 100644 tests/test_redis_fallback_breaker_pin.py create mode 100644 tests/test_regulatory_clock_calendar.py create mode 100644 tests/test_regulatory_clock_events.py create mode 100644 tests/test_regulatory_clock_lifecycle.py create mode 100644 tests/test_regulatory_clock_schema.py create mode 100644 tests/test_regulatory_clock_seed.py create mode 100644 tests/test_regulatory_clock_sweep.py create mode 100644 tests/test_rendered_error_scrub_pin.py create mode 100644 tests/test_repair_daemon_live_wire.py create mode 100644 tests/test_resource_authz_ssot_pin.py create mode 100644 tests/test_retry_backoff_ssot_pin.py create mode 100644 tests/test_rule_types_ssot_pin.py create mode 100644 tests/test_safe_int_request_parse_pin.py create mode 100644 tests/test_scan_pin_harness_adoption_teeth.py create mode 100644 tests/test_schema_version_read_ssot_pin.py create mode 100644 tests/test_secret_key_name_ssot_pin.py create mode 100644 tests/test_secret_read_funnel_pin.py create mode 100644 tests/test_secret_scrub_ssot_pin.py create mode 100644 tests/test_select_star_ratchet_pin.py create mode 100644 tests/test_sent_folder_auto_detect_n402.py create mode 100644 tests/test_sent_folder_normalize_pin.py create mode 100644 tests/test_session_epoch_on_revoke_pin.py create mode 100644 tests/test_settings_key_catalog_pin.py create mode 100644 tests/test_silent_swallow_ratchet_pin.py create mode 100644 tests/test_sme_gate_hook_trigger_coverage_pin.py create mode 100644 tests/test_sme_hook_env_var_path_expansion.py create mode 100644 tests/test_smtp_client_ssot_pin.py create mode 100644 tests/test_smtp_transport_ssot_pin.py create mode 100644 tests/test_sqlite_version_floor_ssot_pin.py create mode 100644 tests/test_ssot_declaration_hardened_pin.py create mode 100644 tests/test_ssot_registry_bijection_pin.py create mode 100644 tests/test_ssot_registry_coverage_pin.py create mode 100644 tests/test_ssot_src_declaration_registered_pin.py create mode 100644 tests/test_subject_reply_prefix_ssot_pin.py create mode 100644 tests/test_supervised_interval_loop_rid_rotation_pin.py create mode 100644 tests/test_sync_sqlite_in_async_ratchet_pin.py create mode 100644 tests/test_time_time_duration_ratchet_pin.py create mode 100644 tests/test_topic_backscan_all_folders.py create mode 100644 tests/test_topic_backscan_resume.py create mode 100644 tests/test_topic_configs_roundtrip.py create mode 100644 tests/test_topic_feedback_cache.py create mode 100644 tests/test_topic_hipaa_gates.py create mode 100644 tests/test_topic_match_categories_pin.py create mode 100644 tests/test_topic_match_confidence_floor.py create mode 100644 tests/test_topic_match_prompt.py create mode 100644 tests/test_topic_matcher.py create mode 100644 tests/test_topic_prefilter.py create mode 100644 tests/test_topic_recall_harness.py create mode 100644 tests/test_triage_completed_emit_pin.py create mode 100644 tests/test_triage_run_counter_ssot_pin.py create mode 100644 tests/test_triage_run_outcome_ssot_pin.py create mode 100644 tests/test_triage_run_trigger_vocab_pin.py create mode 100644 tests/test_uidvalidity_reseed_chip_pin.py create mode 100644 tests/test_url_query_secret_scrub_ssot_pin.py create mode 100644 tests/test_us_datetime.py create mode 100644 tests/test_us_datetime_install_filter_ssot_pin.py create mode 100644 tests/test_user_soft_delete_pin.py create mode 100644 tests/test_verified_test_ratchet_auto_include.py create mode 100644 tests/test_walk_defensive_xfail_ssot_pin.py create mode 100644 tests/test_wall_clock_hm_labeling_pin.py create mode 100644 tests/test_watcher_failing_lifecycle_pin.py create mode 100644 tests/test_watcher_retry_auth_revoked_resurrect.py create mode 100644 tests/test_watcher_retry_dead_reason_symmetry_pin.py create mode 100644 tests/test_watcher_sticky_error_write_order_pin.py create mode 100644 tests/test_watcher_terminal_outcome_funnel_pin.py create mode 100644 tests/test_web/test_account_delete_purge.py create mode 100644 tests/test_web/test_action_guide.py create mode 100644 tests/test_web/test_admin_alerts.py create mode 100644 tests/test_web/test_admin_dashboard_n151_health_row.py create mode 100644 tests/test_web/test_admin_stats_watcher_states_regression.py create mode 100644 tests/test_web/test_ai_backends_probe_ssrf.py create mode 100644 tests/test_web/test_audit_chain_verify_surface.py create mode 100644 tests/test_web/test_backfill_capped_chip.py create mode 100644 tests/test_web/test_backup_config_save.py create mode 100644 tests/test_web/test_backup_fail_closed.py create mode 100644 tests/test_web/test_compliance_audit_caps.py create mode 100644 tests/test_web/test_csrf_client_injector.py create mode 100644 tests/test_web/test_csrf_route_coverage_pin.py create mode 100644 tests/test_web/test_digest_candidates_phi_render.py create mode 100644 tests/test_web/test_digest_preview_viewer_aware_pin.py create mode 100644 tests/test_web/test_discover_error_scrub_render.py create mode 100644 tests/test_web/test_discover_folder_phi_render.py create mode 100644 tests/test_web/test_embedding_model_change.py create mode 100644 tests/test_web/test_flash_error_surface_scaffold.py create mode 100644 tests/test_web/test_integrations_webhook_secret_unreadable_a5.py create mode 100644 tests/test_web/test_last_login_terminus_pin.py create mode 100644 tests/test_web/test_n109_picker_hide.py create mode 100644 tests/test_web/test_n179_error_surface_pin.py create mode 100644 tests/test_web/test_ops_db_backup.py create mode 100644 tests/test_web/test_profile_security.py create mode 100644 tests/test_web/test_regulatory_clock_action.py create mode 100644 tests/test_web/test_regulatory_clock_calendar_write.py create mode 100644 tests/test_web/test_regulatory_clock_digest_section.py create mode 100644 tests/test_web/test_regulatory_clock_escalation_push.py create mode 100644 tests/test_web/test_regulatory_clocks_editor.py create mode 100644 tests/test_web/test_regulatory_clocks_page.py create mode 100644 tests/test_web/test_reindex_aggregate.py create mode 100644 tests/test_web/test_reindex_supersede.py create mode 100644 tests/test_web/test_revoke_audit_route.py create mode 100644 tests/test_web/test_secret_read_collapse_pin_a5.py create mode 100644 tests/test_web/test_secret_write_no_clobber.py create mode 100644 tests/test_web/test_tick_health_isolation.py create mode 100644 tests/test_web/test_topic_digest_router.py create mode 100644 tests/test_web/test_topic_digest_sender.py create mode 100644 tests/test_web/test_topic_retriage_status_routes.py create mode 100644 tests/test_web/test_topic_search_summary.py create mode 100644 tests/test_web/test_triage_one_topic_match.py create mode 100644 tests/test_web/test_watch_auto_retire_audit.py create mode 100644 tests/test_web/test_watch_auto_retire_secret_cleanup.py create mode 100644 tests/test_web/test_watch_grouping_null_creator.py create mode 100644 tests/test_web/test_webhook_egress_gate_a4.py create mode 100644 tests/test_web/test_webhook_preview.py create mode 100644 tests/test_webhook_e2e_mock_transport.py create mode 100644 tests/test_webhook_render_for_target.py create mode 100644 tests/test_webhook_secret_binding_ssot_pin.py create mode 100644 tests/test_webhook_ssot_pins.py create mode 100644 tests/test_yaml_secret_disjointness_pin.py create mode 100644 tests/valk_allowlist_registry.json create mode 100644 vendor/sqlite3.expected.sha256 diff --git a/.dockerignore b/.dockerignore index 3762c50..effaf43 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,5 +29,5 @@ dist build # Plans and CI -.claude +.the AI assistant .github diff --git a/.gitattributes b/.gitattributes index f9a7f95..e007f3e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -28,3 +28,15 @@ scripts/embedding-bits-manifest.json text eol=lf # literal "dev" placeholder and /health reported version "unknown". eol=lf so # the substituted SHA is not followed by CRLF on Windows checkouts. COMMIT text eol=lf export-subst + +# N-326-C5 trust-root SSOT: the checked-in FIDO2-sk allowed_signers file +# AND its golden-hash companion MUST be byte-stable across Windows and +# Linux checkouts. `test_repo_trust_root_matches_golden_hash` computes +# `sha256(read_bytes())` and compares against the golden — same drift +# class as the embedding-bits manifest above (CI on Linux sees LF blob +# bytes; Windows CRLF-autocrlf checkout sees `\r\n` → different hash → +# false-positive DRIFT). Pinning LF makes the on-disk bytes match the +# blob bytes on every platform, so the golden hash is one canonical +# value everywhere. +.github/op-approve/allowed_signers text eol=lf +tests/goldens/allowed_signers.sha256 text eol=lf diff --git a/.githooks/pre-push b/.githooks/pre-push index 0242296..9290e99 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,17 +1,69 @@ #!/usr/bin/env bash # -# pre-push: run pip-audit BEFORE the push lands on origin so CI -# never has to fail on a CVE you could have caught locally. +# pre-push guards (run BEFORE the push lands on origin so CI never has to +# fail on something you could have caught locally): +# 1. e2e real-browser suite — when a UI/e2e surface changed +# 2. pip-audit vuln scan — when pip-audit is installed +# 3. requirements.lock drift — when pip-compile is installed # -# Bypass with `git push --no-verify` ONLY in genuine emergencies -# (the hook prints how when it fires). Bypassing is logged in the -# user's local shell history; intentional bypass is auditable. +# Each guard is conditional (skips cleanly if its tool isn't present) and +# bypassable with `git push --no-verify` ONLY in genuine emergencies (the +# hook prints how when it fires). Bypassing is logged in the user's local +# shell history; intentional bypass is auditable. # # Activated by `scripts/install-git-hooks.sh` (runs `git config # core.hooksPath .githooks` so the repo-tracked hooks become live). set -euo pipefail -# Only run if pip-audit is installed; nudge if not. We don't want to +ROOT=$(git rev-parse --show-toplevel) + +# --------------------------------------------------------------------------- +# Guard 1 — e2e real-browser suite. A spec (tests/e2e) that asserts the wrong +# thing — or a UI/web change that breaks one — must not land RED in CI: +# .github/workflows/e2e.yml is the ONLY GitHub job that runs pytest, so a red +# e2e is otherwise unguarded. Runs ONLY when (a) Playwright is importable +# locally AND (b) this push touches an e2e / web / template surface — docs and +# non-UI pushes pay nothing. Placed FIRST so it runs independently of whether +# pip-audit is installed (the pip-audit guard below `exit 0`s when absent). +# Origin (2026-06-20): an N-109 e2e spec asserted `.to_be_visible()` on a +# `data-multiselect`-HIDDEN + {{ m.combo_input( + "host", + acct.config.get('host', ''), + ["imap.gmail.com", "outlook.office365.com", "mail.fastmail.com", "imap.aol.com", "imap.mail.yahoo.com"], + placeholder="e.g. imap.example.com", + attrs="required") }} @@ -62,8 +69,7 @@

Add an email account: {{ acct.name }}

diff --git a/src/email_triage/web/templates/accounts/api_keys.html b/src/email_triage/web/templates/accounts/_api_keys_section.html similarity index 78% rename from src/email_triage/web/templates/accounts/api_keys.html rename to src/email_triage/web/templates/accounts/_api_keys_section.html index f57fe42..fa4cf9b 100644 --- a/src/email_triage/web/templates/accounts/api_keys.html +++ b/src/email_triage/web/templates/accounts/_api_keys_section.html @@ -1,10 +1,12 @@ -{% extends "base.html" %} -{% block title %}My Settings - API Keys - Email Triage{% endblock %} -{% block content %} -

My Settings

-{% set active = "api-keys" %} -{% include "_settings_tabs.html" %} - +{# ─── AUDIENCE ──────────────────────────────────────────────── + AUDIENCE: end-user. API Keys (access tokens) self-service. + Included by profile/security.html (the consolidated Security tab). + Expects in context: keys, all_users, is_admin, new_key_open, user. + Variable names are scoped by the including page via {% with %} so + the `keys` here doesn't collide with the Passkeys section. + No protocol jargon (no "/api/openclaw", no "bearer token"). + ─────────────────────────────────────────────────────────── #} +

API Keys (Access Tokens)

Tokens let an external tool — for example, an AI assistant — read your @@ -22,8 +24,6 @@

API Keys (Access Tokens)

{% endif %}

-

← Back to Accounts

- {# ── Create new token ─────────────────────────────────────────────── #}
@@ -80,7 +80,7 @@

API Keys (Access Tokens)

{# ── Existing tokens ──────────────────────────────────────────────── #}
-

Your tokens

+

Your tokens

{% if keys %} @@ -98,9 +98,9 @@

Your tokens

{% if is_admin %}{% endif %} - - - + + + - + diff --git a/src/email_triage/web/templates/accounts/_watch_folders_imap.html b/src/email_triage/web/templates/accounts/_watch_folders_imap.html index 43f8efa..719f7e0 100644 --- a/src/email_triage/web/templates/accounts/_watch_folders_imap.html +++ b/src/email_triage/web/templates/accounts/_watch_folders_imap.html @@ -1,3 +1,4 @@ +{% import "_macros.html" as m %} {# IMAP per-folder watch picker — rendered inside the Watch + Push tab when ``acct.provider_type == 'imap'``. Lives outside the IMAP connection fieldset (host/port/user/pass) because folder selection @@ -43,7 +44,11 @@ {% else %} {# Fallback: comma-separated text entry when folder discovery is unavailable. The save handler parses this via ``mailboxes_csv`` - in _extract_provider_config. #} + in _extract_provider_config. Plain deliberately, NOT + m.combo_input: this is a COMMA-SEPARATED multi-value field, and a + datalist autocompletes the WHOLE input value — picking a single + folder ("Spam") would clobber the existing list ("INBOX, Sent"). + Single-value suggestions are wrong for a multi-value field. #} @@ -107,10 +104,7 @@

Runtime Settings

@@ -126,10 +120,7 @@

Runtime Settings

@@ -211,21 +202,122 @@

Classifier

AI backend for email classification.
+ {# #44 — model picker replaces the bare free-text Model input. + Submits the SAME name="classifier_model" (single string) + plus a new name="classifier_backup_models" (comma- + separated, ordered). Probes the live [name=classifier_backend] + + [name=classifier_ollama_url] on this form. The free-text + escape hatch keeps non-enumerable backends + offline use + working. Picker lives inside the Value cell so the 3-column + table layout is preserved. #} + + {# #49 — pin the Ollama context-window size (num_ctx). Blank = inherit + the model's loaded default (recommended). Sits next to the Model + picker because it governs both classify capacity AND the #33 + Topic-match warm-prefix cache. #} + + + + + + {# N-378 — Ollama `think` payload option as a CONFIG KNOB. Sits + next to the model picker + num_ctx because it directly gates + the reasoning-mode budget on qwen3-family models. Tri-state: + blank = model default; false = disable thinking (~4-6× faster + on reasoning models); true = force thinking. Requires Ollama + ≥ 0.31.1 when combined with the classify/topic-match format + schema — see the ClassifierConfig docblock. #} + + + + + + {# N-378 belt-and-braces companion. Qwen3-family models honour a + `/no_think` marker at the end of the user message as a per-call + thinking-off directive — independent of the `think` payload + option. Cheap safety net on top of the `think` knob. #} + + + + + + {# N-431 (2026-07-19) — Ollama `keep_alive` payload option, threaded + SSOT to every /api/chat + /api/embeddings site. Absent this + field the daemon applies its built-in default "5m" per call, + silently overriding any operator TTL pin (including a Forever + `-1` pin). Wire form: str "10m" / "1h" / "-1" / "0". #} + + + + {% for key, label in [ @@ -684,10 +744,7 @@

Admin Email

- + - - - + + + @@ -201,18 +202,18 @@

Registered keys

- + - +
{{ k.name }}{{ k.user_name if k.user_name else k.email }}{{ k.created_at[:19] }}{{ k.last_used_at[:19] if k.last_used_at else '—' }}{{ k.expires_at[:19] if k.expires_at else 'Never' }}{% if k.created_at %}{% else %}—{% endif %}{% if k.last_used_at %}{% else %}—{% endif %}{% if k.expires_at %}{% else %}Never{% endif %}
{{ user_label(d.name, d.email) }} {{ d.granted_by_email or '—' }}{{ d.granted_at[:19] }}{% if d.granted_at %}{% else %}—{% endif %} + + + {% endif %} {# Watch status always visible — green pulsing dot when the real-time watch is connected for this account, muted dot otherwise. Gmail / O365 accounts use push or polling @@ -141,11 +172,19 @@ hx-post="/accounts/{{ acct.id }}/test" hx-target="#test-result-{{ acct.id }}" hx-swap="innerHTML">Test + {# N-108: Remove = soft-deactivate (purge creds + is_active=0), NEVER + hard-delete. Only shown for an ACTIVE account; a deactivated one + shows "Disabled" (status column above) and is re-enabled via the + Edit panel's is_active toggle — the EXISTING mechanism, which + restarts the watcher. No parallel reactivate path (it would + overload is_active vs the Edit-pause state). #} + {% if acct.is_active %} + hx-confirm="Remove account '{{ acct.name }}'? It is DEACTIVATED (paused, credentials purged) — NOT deleted: the account record and its tamper-evident audit trail are RETAINED. To re-enable it later, open Edit and RE-ENTER its credentials (they are purged on Remove, so a simple toggle won't reconnect it).">Remove + {% endif %}
{% if sub.last_error %} {{ sub.last_error }} - {{ sub.last_error_at[:19] }} + {% if sub.last_error_at %}{% else %}—{% endif %} {% else %} {% endif %} @@ -169,7 +169,7 @@

AI Backends

{% if _m.last_error %}

Last error: {{ _m.last_error }} - at {{ _m.last_error_at[:19] }} + at {% if _m.last_error_at %}{% else %}—{% endif %}

{% endif %} {% endif %} @@ -205,16 +205,28 @@

AI Backends

@@ -246,9 +258,11 @@

AI Backends

@@ -258,6 +272,12 @@

AI Backends

the Redis URL all surface there. AI Backends scopes to the LLM-side configuration only. #} + {# #37 — set to "1" by the submit-time confirm dialog when the + operator changes the embedding model. The save handler will + NOT apply a model change without this flag (keeps the old + model so recall can't break silently). #} + + + + +
+ +

On-demand encrypted bundle

+

+ + Download an encrypted bundle of this install's state by hand — + e.g. right before a risky upgrade. Independent of the automated + cadence above. + +

diff --git a/src/email_triage/web/templates/admin/config_tabs/general.html b/src/email_triage/web/templates/admin/config_tabs/general.html index 1a05239..c7a59a1 100644 --- a/src/email_triage/web/templates/admin/config_tabs/general.html +++ b/src/email_triage/web/templates/admin/config_tabs/general.html @@ -92,10 +92,7 @@

Runtime Settings

Bulk Triage Rate Bulk Triage Concurrency
Model - + {{ m.model_picker("classifier_model", "classifier_backup_models", config.classifier.model, config.classifier.backup_models, "[name=classifier_backend]", "[name=classifier_ollama_url]") }} Model name (e.g. qwen3.5:35b, gpt-4o, gemini-2.0-flash). + Pick from the probed list, or tick Enter manually to type one. + Backup models are tried in order if the primary can't classify. +
Context window + + + Ollama only. Pins num_ctx for every Ollama call — classify, + the Topic-match prefill, and the explain-this-error / style-learning completion. + Leave blank to inherit the model's loaded default (recommended). A divergent value + forces a one-time model reload; both Topic-match stages share it, so the warm-prefix + cache stays intact. Values below ~8192 may silently degrade multi-Topic matching + (the Topic pre-filter inlines several full Topic Details) and truncate long + explanations — the app logs a warning if you pin one that low. +
Ollama think + + + Ollama only. Sent as the top-level think payload option + on every /api/chat call (classify, topic-match, safety, style-learning). + Reasoning models (qwen3.6:35b) burn the full num_predict budget + on hidden reasoning tokens before emitting JSON; disabling thinking recovers + that latency and prevents finish_reason=length, raw_len=0 empty responses. + Requires Ollama ≥ 0.31.1 when combined with the format schema + (older Ollama drops the schema mask — bug #15260 fixed via PRs #15678/#15392). +
Append /no_think marker + + + Qwen3-family only. Appends the /no_think marker to the end of + every /api/chat user prompt — an independent per-call thinking-off directive + that some Qwen3 tokenizer templates honour even when the payload option isn't + set. No-op on non-Qwen3 models. Safe as a belt-and-braces companion to the + think knob above. +
Ollama keep_alive + + + Ollama only. Sent as the top-level keep_alive payload option + on every /api/chat + /api/embeddings call. Accepts Ollama's native form: + 10m, 1h, -1 (Forever pin), + 0 (unload immediately). Absent, Ollama applies its built-in + 5m default per call — silently overriding any operator TTL + pin (including Forever). Empty saves keep the default (10m).
Ollama URL - + {{ m.combo_input("classifier_ollama_url", config.classifier.ollama_url, ["http://localhost:11434", "http://ollama.home:11434"], type="url", attrs="style='margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} Base URL for the Ollama API server. @@ -234,9 +326,7 @@

Classifier

OpenAI Base URL - + {{ m.combo_input("classifier_openai_base_url", config.classifier.openai_base_url, ["https://api.openai.com/v1", "https://.openai.azure.com/"], type="url", placeholder="https://api.openai.com/v1", attrs="style='margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} For OpenAI/Azure backend. Leave blank for default OpenAI endpoint. @@ -294,8 +384,7 @@

Outbound email (SMTP)

Host - + {{ m.combo_input("smtp_host", config.smtp.host, ["smtp.gmail.com", "smtp.office365.com", "smtp.fastmail.com", "smtp.sendgrid.net"], attrs="style='margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} SMTP server hostname. @@ -304,8 +393,7 @@

Outbound email (SMTP)

Port - + {{ m.combo_input("smtp_port", config.smtp.port, [587, 465, 25, 2525], type="number", attrs="min=1 max=65535 style='margin:0;width:6rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} SMTP port (25, 465, 587). @@ -415,12 +503,7 @@

Ingestion cadence

Default poll interval - min + {{ m.combo_input("ingestion_default_poll_interval_minutes", config.ingestion.default_poll_interval_minutes, [10, 30, 60, 120, 240], type="number", attrs="min=" ~ config.ingestion.POLL_MIN ~ " max=" ~ config.ingestion.POLL_MAX ~ " step=" ~ config.ingestion.POLL_STEP ~ " style='margin:0;width:7rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} min {{ config.ingestion.POLL_MIN }}–{{ config.ingestion.POLL_MAX }} min in {{ config.ingestion.POLL_STEP }}-min steps. @@ -430,12 +513,7 @@

Ingestion cadence

Push-mode interval (legacy) - min + {{ m.combo_input("ingestion_push_poll_interval_min", config.ingestion.push_poll_interval_min, [10, 30, 60, 120, 240], type="number", attrs="min=" ~ config.ingestion.PUSH_MIN ~ " max=" ~ config.ingestion.PUSH_MAX ~ " step=" ~ config.ingestion.STEP ~ " style='margin:0;width:7rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} min Legacy B3 field retained for YAML round-trip. No longer drives ingestion. @@ -444,12 +522,7 @@

Ingestion cadence

Poll-mode interval (legacy) - min + {{ m.combo_input("ingestion_poll_poll_interval_min", config.ingestion.poll_poll_interval_min, [10, 30, 60, 120, 240], type="number", attrs="min=" ~ config.ingestion.POLL_MIN ~ " max=" ~ config.ingestion.POLL_MAX ~ " step=" ~ config.ingestion.STEP ~ " style='margin:0;width:7rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} min Legacy B3 field retained for YAML round-trip. No longer drives ingestion. @@ -486,10 +559,7 @@

Style learning

{{ m.help("Hours between auto-scans of opted-in accounts. Default 6. Example: 12 = scan twice a day (lighter load); 1 = scan every hour (fresher style data, higher AI cost).") }}
- hours + {{ m.combo_input("style_learning_capture_interval_hours", style_learning_capture_interval_hours or 6, [1, 2, 4, 6, 12, 24, 48, 72], type="number", attrs="min=1 max=72 step=1 style='margin:0;width:6rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} hours Range 1–72 hours. Cadence applies to every account that has Auto-scan on schedule turned on. @@ -501,10 +571,7 @@

Style learning

{{ m.help("Default number of sent messages each mine press reads when no per-account override is set. Default 50. Example: 100 = pull twice as many sent messages on a one-click refresh (over " ~ (inline_limit_ceiling or 50) ~ " runs in the background on the Bulk runs page).") }}
- messages + {{ m.combo_input("style_learning_mine_limit_default", style_learning_mine_limit_default or 50, [10, 25, 50, 100, 200, 500], type="number", attrs="min=1 max=500 step=1 style='margin:0;width:6rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} messages Range 1–500. Each user can override this from their Style data page; values over {{ inline_limit_ceiling or 50 }} hand off to a background job so the browser doesn't sit waiting. @@ -613,11 +680,7 @@

Admin Email

Send at - + {{ m.combo_input("health_email_send_at", config.health_email.send_at, ["07:00", "07:15", "08:00", "18:00", "20:00"], placeholder="07:15", attrs="pattern='[0-2][0-9]:[0-5][0-9]' style='margin:0;width:6rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} HH:MM in container local time. @@ -626,10 +689,7 @@

Admin Email

Release notes URL - + {{ m.combo_input("admin_email_release_check_url", config.admin_email.release_check_url, ["https://api.github.com/repos/unlimited-data-works-llc/email-triage/releases/latest"], type="url", placeholder="https://api.github.com/repos/unlimited-data-works-llc/email-triage/releases/latest", attrs="style='margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;width:100%;'") }} GitHub Releases API endpoint. Used for the "update @@ -640,7 +700,7 @@

Admin Email

- Content toggles + Content toggles{{ m.help("Pick which sections appear in the daily health email. Each is a self-contained block; turning one off just omits it. Update-failed alerts always send regardless of these.") }}
Error-rate threshold - % + {{ m.combo_input("health_email_error_rate_threshold_pct", config.health_email.error_rate_threshold_pct, [0, 1, 5, 10, 25, 50], type="number", attrs="min=0 max=100 style='margin:0;width:6rem;padding:0.25rem 0.5rem;font-size:0.9rem;'") }} % Triage error rate above this flips the subject to diff --git a/src/email_triage/web/templates/admin/config_tabs/integrations.html b/src/email_triage/web/templates/admin/config_tabs/integrations.html index d50bb32..3dfdaeb 100644 --- a/src/email_triage/web/templates/admin/config_tabs/integrations.html +++ b/src/email_triage/web/templates/admin/config_tabs/integrations.html @@ -17,7 +17,7 @@

{{ err }}

{% endif %} -
+ {{ csrf_input(request) }} {# ── Google OAuth (install-wide client credentials, B1 lift) ──── #} @@ -98,10 +98,7 @@

Google OAuth (install-wide)

Client ID - + {{ m.cred_text_input("google_oauth_web_client_id", config.google_oauth.web_client_id, "xxxx-xxxx.apps.googleusercontent.com", 'style="margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;"') }} Registered redirect URI must be @@ -112,9 +109,7 @@

Google OAuth (install-wide)Client Secret

{% set has_web_secret = config.google_oauth.web_client_secret %} - + {{ m.secret_input("google_oauth_web_client_secret", has_web_secret, "GOCSPX-...", 'style="margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;"') }} Required — Google's token exchange rejects the request without it. @@ -130,10 +125,7 @@

Google OAuth (install-wide)

Client ID - + {{ m.cred_text_input("google_oauth_desktop_client_id", config.google_oauth.desktop_client_id, "xxxx-xxxx.apps.googleusercontent.com", 'style="margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;"') }} No redirect URI registration needed — Google auto-allows loopback @@ -144,9 +136,7 @@

Google OAuth (install-wide)Client Secret

{% set has_desk_secret = config.google_oauth.desktop_client_secret %} - + {{ m.secret_input("google_oauth_desktop_client_secret", has_desk_secret, "GOCSPX-...", 'style="margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;"') }} Required for Desktop clients too. Google requires the secret on @@ -221,10 +211,12 @@

Office 365 OAuth + {{ m.combo_input( + "office365_oauth_tenant_id", + config.office365_oauth.tenant_id, + ["organizations"], + placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx or organizations", + attrs="style='margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;'") }}

Org tenant GUID, or organizations for @@ -236,10 +228,7 @@

Office 365 OAuth + {{ m.cred_text_input("office365_oauth_client_id", config.office365_oauth.client_id, "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 'style="margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;"') }}

Application (client) ID from the app's Overview page. @@ -249,9 +238,7 @@

Office 365 OAuth + {{ m.secret_input("office365_oauth_client_secret", has_o365_secret, "Microsoft client secret value", 'style="margin:0;padding:0.25rem 0.5rem;font-size:0.9rem;"') }}

The secret VALUE (not the secret ID) from Certificates @@ -340,16 +327,54 @@

Outbound webhook (OpenClaw)

+ +

+ Pick the receiver's native shape and email-triage reshapes each + signed emit into the expected wire body. Custom + posts our envelope {"event","timestamp","data":{…}} + verbatim (existing behaviour — HMAC-signed for relay integrations). + +

+

- Leave blank to disable outbound emit entirely. The receiver verifies - the HMAC signature in the X-Email-Triage-Signature - header against the shared secret below. + Leave blank to disable outbound emit entirely. For Custom targets + the receiver verifies the HMAC signature in the + X-Signature-256 header against the shared secret + below. Typed targets (Slack / PagerDuty / …) verify by their own + means; the HMAC still travels but is ignored by the receiver.

@@ -377,14 +402,20 @@

Outbound webhook (OpenClaw)

Used to compute the HMAC-SHA256 signature on every emit. Paste the - same value into your OpenClaw side. {% if outbound_secret_set %}A + same value into your OpenClaw side. {% if outbound_secret_set and not outbound_secret_unreadable %}A secret is stored; leave blank to keep.{% endif %}

+{% if outbound_secret_unreadable %} +

+ ⚠ A secret is stored but cannot be decrypted (secrets master-key + mismatch) — HMAC-signed webhook deliveries are being dropped. + Check the secrets master key, then paste the secret again to re-store it. +

+{% endif %}
NameFingerprintFingerprint{{ m.help("SHA256 fingerprint of the public key (OpenSSH format) — identifies the physical key at login. Immutable after registration.") }} AllowlistStatusExpiresLast usedStatus{{ m.help("active (usable) · expired (past TTL) · revoked (manually disabled). Only active keys allow login.") }}Expires{{ m.help("UTC time the key stops working. No grace period — an expired key is locked out immediately at login.") }}Last used{{ m.help("Most recent SUCCESSFUL login with this key (UTC + the email used). Never updated for failed attempts — helps spot a forgotten or compromised key.") }}
{{ k.email_allowlist | join(", ") }} {% if k.revoked_at %} - revoked {{ k.revoked_at[:10] }} + revoked {% elif not k.active %} expired {% else %} active {% endif %} {{ k.expires_at[:19] }}{% if k.expires_at %}{% else %}—{% endif %} {% if k.last_used_at %} - {{ k.last_used_at[:19] }}
+
as {{ k.last_used_email }} {% else %} — diff --git a/src/email_triage/web/templates/admin/dev_keys.html b/src/email_triage/web/templates/admin/dev_keys.html index e769eef..b6843dc 100644 --- a/src/email_triage/web/templates/admin/dev_keys.html +++ b/src/email_triage/web/templates/admin/dev_keys.html @@ -11,11 +11,11 @@

Developer keypairs

machine.

- Hardware-key-wins rule: if a user has registered - any active hardware key, dev-keypair logins for that email are + Passkey-wins rule: if a user has registered + any active passkey, dev-keypair logins for that email are denied with 403. OTP login remains available regardless. Users - register their own hardware keys at - /profile/hardware-keys + register their own passkeys under + My Settings › Security (each user manages their own; admins do NOT register on behalf of users -- the private side never leaves the user's device).

@@ -38,11 +38,11 @@

Developer keypairs

{% endif %} @@ -100,18 +100,18 @@

Registered keys

{{ k.email_allowlist | join(", ") }} {% if k.revoked_at %} - revoked {{ k.revoked_at[:10] }} + revoked {% if k.revoked_at %}{% else %}—{% endif %} {% elif not k.active %} expired {% else %} active {% endif %} {{ k.expires_at[:19] }}{% if k.expires_at %}{% else %}—{% endif %} {% if k.last_used_at %} - {{ k.last_used_at[:19] }}
+
as {{ k.last_used_email }} {% else %} — diff --git a/src/email_triage/web/templates/admin/integrations.html b/src/email_triage/web/templates/admin/integrations.html deleted file mode 100644 index a7e2696..0000000 --- a/src/email_triage/web/templates/admin/integrations.html +++ /dev/null @@ -1,794 +0,0 @@ -{% extends "base.html" %} -{% import "_macros.html" as m %} -{% block title %}Integrations - Email Triage{% endblock %} -{% block content %} -{# ─── AUDIENCE ──────────────────────────────────────────────── - AUDIENCE: admin (install operator). Not end users. - TECH-SKILL: medium — comfortable with Pub/Sub, Microsoft Graph, - webhook URLs, JWT audiences. Technical jargon - (RFC numbers, OData, JWKS, OIDC) is allowed here. - COPY RULES: no copy rule lifted from end-user pages applies; - this page is intentionally protocol-aware. Do NOT - copy this header onto end-user templates. - See feedback_audience_per_page.md. - ─────────────────────────────────────────────────────────── #} - -

Integrations

-

- Install-level OAuth client credentials, inbound provider push - config (Gmail Pub/Sub + Microsoft Graph), and the outbound - OpenClaw webhook destination. -

- -{% if saved %} -

Settings saved.

-{% endif %} -{% if err %} -

{{ err }}

-{% endif %} - - - {{ csrf_input(request) }} - -
- -{# ── Google OAuth (install-wide client credentials, B1 lift) ──── #} -
-
-

Google OAuth (install-wide)

- - Shared across all Gmail accounts on this install. Two client pairs - because Google's Web application and Desktop - OAuth clients have incompatible redirect-URI rules. Accounts authenticate - using the pair that matches the chosen flow. - Create clients at the - Google Cloud Console. - -
-
-
-
- - - Drop in the client_secret_*.json you downloaded from - Google Cloud Console. Auto-detected as Web (web top-level - key) or Desktop (installed) and populates the matching - fields below. The file is parsed in your browser — nothing is uploaded - until you click Save at the bottom of the page. - -
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SettingValueDescription
- Primary — push-enabled -  (Web application client) -
Client ID - - - Registered redirect URI must be - {{ config.push.public_url or '' }}/oauth/google/callback. -
Client Secret - {% set has_web_secret = config.google_oauth.web_client_secret %} - - - Required — Google's token exchange rejects the request without it. - {% if has_web_secret %}A secret is stored; leave blank to keep.{% endif %} -
- Fallback — manual paste -  (Desktop client) -
Client ID - - - No redirect URI registration needed — Google auto-allows loopback - for Desktop clients. -
Client Secret - {% set has_desk_secret = config.google_oauth.desktop_client_secret %} - - - Required for Desktop clients too. Google requires the secret on - the token exchange regardless of client type. - {% if has_desk_secret %}A secret is stored; leave blank to keep.{% endif %} -
-
-
-
- -{# ── Office 365 OAuth (install-wide) ─────────────────────────── #} -
-
-

Office 365 OAuth (install-wide)

- - One Microsoft Entra (Azure AD) app registration shared across every - Office 365 account on this install. End-users picking - Personal Microsoft account on their integration page route - to Microsoft's shared common endpoint; everyone else - routes to the tenant below. Register the app in - Azure Portal - with Mail.Read, Mail.ReadWrite, Mail.Send, - and offline_access permissions, then paste the three - values below. - -
-
-
-
- - How do I find these values? - -
    -
  1. - Tenant ID. - Azure Portal → Microsoft Entra ID (formerly - Azure Active Directory) → Overview → - Tenant ID. Or use organizations if - you want to accept sign-ins from any work or school - account. Never use common here — that's - reserved for individual personal Microsoft accounts on - a per-user basis. -
  2. -
  3. - Client ID. - Azure Portal → App Registrations → your app → - OverviewApplication (client) ID. -
  4. -
  5. - Client Secret. - Azure Portal → App Registrations → your app → - Certificates & secretsNew client - secret. Copy the secret value - immediately — Microsoft only shows it once. -
  6. -
  7. - Required permissions. - Mail.Read + Mail.ReadWrite + - Mail.Send + offline_access - (delegated). Add via the API permissions tab; - Grant admin consent if your tenant requires it. -
  8. -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
SettingValueDescription
Tenant ID - - - Org tenant GUID, or organizations for - any work or school account. Don't use common — - that's the per-account "Personal Microsoft account" - routing, set on each account's own integration page. -
Client ID - - - Application (client) ID from the app's Overview page. -
Client Secret - {% set has_o365_secret = config.office365_oauth.client_secret %} - - - The secret VALUE (not the secret ID) from Certificates - & secrets. {% if has_o365_secret %}A secret is stored; - leave blank to keep.{% endif %} -
-
-
-
- -{# ── Gmail Pub/Sub ───────────────────────────────────────────── #} -
-
-

Gmail Pub/Sub

- - Google sends an OIDC-signed JWT to the public webhook URL whenever - a watched mailbox changes. The webhook validates the signature - (RS256 against Google's JWKS), the issuer, the audience claim, and - the service-account email. All four fields below must match what - you set on the Pub/Sub subscription, or every push is rejected at - JWT-verify time. - -
-
- -

- The base URL Google's Pub/Sub push servers can reach (no path, no - trailing slash). The webhook lives at - {{ push.public_url or '<public_url>' }}/webhooks/gmail. - For the homelab default, this is the Tailscale Funnel hostname - (e.g. https://the deploy host.<tailnet>.ts.net). - Internal UI stays at the private domain; the webhook is the only - path Funnel exposes publicly. -

- - -

- Fully-qualified topic resource name. Must already exist in GCP and - have Gmail's publisher service account - (gmail-api-push@system.gserviceaccount.com) bound as - roles/pubsub.publisher. -

- - -

- The service account whose identity Pub/Sub uses to mint OIDC - tokens for each push. The webhook's - email-claim check rejects anything else, even other - valid Google-signed JWTs. -

- - -

- Must equal the Pub/Sub subscription's - --push-auth-token-audience. Leave blank to default to - the public webhook base URL above. Trailing slashes are stripped - automatically; mismatched slashes are the most common cause of - "every push gets rejected" mystery. -

-
-
- -{# ── Outbound webhook (OpenClaw) ─────────────────────────────── #} -
-
-

Outbound webhook (OpenClaw)

- - Email-triage emits HMAC-SHA256-signed POSTs to a single destination - when triage outcomes land. Set the URL to enable; clear the URL to - turn it off entirely. Per-account on/off + quiet-hours chips on - each account's Integrations tab gate the emit at fire time. - -
-
-
- -

- Leave blank to disable outbound emit entirely. The receiver verifies - the HMAC signature in the X-Email-Triage-Signature - header against the shared secret below. -

-
- -
-
- Events to send - {% for event_name, label, desc in outbound_event_catalogue %} - - {% endfor %} -
-

- Tick zero events to keep the destination configured but stop all - emits — useful for a temporary mute without losing the URL + - secret. Per-account pause chips do the same at finer granularity. -

-
- -
- -

- Used to compute the HMAC-SHA256 signature on every emit. Paste the - same value into your OpenClaw side. {% if outbound_secret_set %}A - secret is stored; leave blank to keep.{% endif %} -

- - -

- Off by default. When off, only local URLs (loopback, RFC1918, - .local, or any configured local-URL suffix) are - delivered to. Turning this on lets the install ship signed events - to any HTTPS endpoint — required if your OpenClaw lives off-LAN. -

-
-
-
- -{# ── Classification cache ─────────────────────────────── #} -
-
-

Classification cache (optional — Redis / valkey)

- - Skip the LLM call when the same email subject+sender+body has - already been classified by the same model. Big token savings on - repetitive inbound mail (Iperius backups, monitor alerts, list - mail). Default OFF — paste a Redis URL to opt in. The protocol is - plain Redis 5+; a valkey server speaks the identical - wire protocol and works unchanged. -

- On-LAN only. The URL must point at the operator's - audited LAN host (e.g. redis://your-redis-host.example.local:6379/0) - — cached values carry classifier metadata, never leave the install - boundary. HIPAA-flagged accounts skip the cache - regardless of this setting (defence-in-depth: sender + subject are - PHI shape even if the key is hashed). -
-
-
-
- -

- Blank to disable. Lazy connection — nothing connects until the first - classification. Failures fall through to the live LLM call. -

- - -

- Default 30 days = 2592000 seconds. Range: 1 hour (3600) to 90 days (7776000). -

-
- -
-

- Counters this process: - {{ redis_cache_counters.hits }} hits, - {{ redis_cache_counters.misses }} misses, - {{ redis_cache_counters.errors }} errors. - Resets on every restart. Surfaced live on /health/detail - as classification_cache. -

-
-
-
- -
{# /.et-cards #} - - - - -{# Manual flush — separate form so a stray click here doesn't - accidentally save the rest of the integrations page. CSRF token - on every state-changing POST. Disabled when the cache URL is - empty — flushing an unconfigured cache is a no-op + the button - shouldn't suggest the action is meaningful. #} -
- {{ csrf_input(request) }} - - - Drops every et:cls:* key. Other services sharing - this Redis instance are unaffected.{{ m.help("Use after changing your categories, classifier model, or rules — anything that would make old cached classifications wrong. Without a flush, stale entries roll off naturally over the TTL (default 30 days).") }} - -
-{% if flushed is defined and flushed %} -

- Flushed {{ flushed }} cached classification{% if flushed != 1 %}s{% endif %}. -

-{% endif %} -{% if flush_err %} -

- Flush failed: {{ flush_err }} -

-{% endif %} - -{% if webhook_url %} -

- - Hint: register the subscription with - --push-endpoint={{ webhook_url }} - and - --push-auth-token-audience={{ push.gmail_audience or push.public_url }}. - -

-{% endif %} - -{# ── Watch status tables (outside the save form) ─────────────── #} -
- -
-
-

Watch status — per account

- {% if not watches %} - - One row per Gmail account showing the live Pub/Sub watch lease. - Empty until at least one Gmail account is registered. - - {% endif %} -
-
- {% if watches %} - - - - - - - - - - - - - {% for w in watches %} - - - - - - - - - {% endfor %} - -
AccountOwnerTopicExpiresStatus
{{ w.email_address }} - {% if w.owner_email %} - {{ w.owner_name or w.owner_email }}
- {{ w.owner_email }} - {% else %} - (unknown) - {% endif %} -
- {% if w.topic_name %} - {{ w.topic_name }} - {% else %} - (none — placeholder row) - {% endif %} - - {% if w.hours_until_expiry is not none %} - {% if w.hours_until_expiry > 0 %} - ~{{ "%.1f"|format(w.hours_until_expiry) }}h - {% else %} - expired - {% endif %} - {% else %} - - {% endif %} - - {% if w.status == "healthy" %} - healthy - {% elif w.status == "renewable" %} - renewable - {% elif w.status == "stale" %} - stale - {% elif w.status == "expired" %} - expired - {% else %} - unconfigured - {% endif %} - - - -
- {% else %} -

- No Gmail accounts registered yet. Add one via - /accounts. -

- {% endif %} -
-
- -
-
-

Office 365 Push Subscriptions

- - Microsoft Graph delivers change notifications for - me/mailFolders('Inbox')/messages to the canonical - receiver path - {{ o365_webhook_url or '<public_url>/webhooks/office365' }}. - Each subscription lasts up to ~3 days (Graph's max for mail - resources) and is authenticated by a per-subscription - clientState secret that the receiver compares - constant-time. The legacy /webhooks/graph alias - still works for older subscriptions registered before the canonical - path landed. - -
-
- {% if o365_subscriptions %} - - - - - - - - - - - - - {% for s in o365_subscriptions %} - - - - - - - - - {% endfor %} - -
AccountOwnerSubscriptionResourceExpiresStatus
{{ s.account_label }} - {% if s.owner_email %} - {{ s.owner_name or s.owner_email }}
- {{ s.owner_email }} - {% else %} - (unknown) - {% endif %} -
- {{ s.subscription_id or '(none)' }} - - me/mailFolders('Inbox')/messages - - {% if s.hours_until_expiry is not none %} - {% if s.hours_until_expiry > 0 %} - ~{{ "%.1f"|format(s.hours_until_expiry) }}h - {% else %} - expired - {% endif %} - {% else %} - - {% endif %} - - {% if s.status_bucket == "healthy" %} - healthy - {% elif s.status_bucket == "renewable" %} - renewable - {% elif s.status_bucket == "stale" %} - stale - {% elif s.status_bucket == "expired" %} - expired - {% elif s.status_bucket == "errored" %} - errored - {% else %} - {{ s.status_bucket }} - {% endif %} - {% if s.error_count and s.error_count > 0 %} -
- errors: {{ s.error_count }} - - {% endif %} -
- {% else %} -

- No active Office 365 push subscriptions. Owners can start one - from the per-account edit page (Integrations tab). -

- {% endif %} -
-
- -
{# /.et-cards #} - - - -{% endblock %} diff --git a/src/email_triage/web/templates/admin/retry_queue/_index.html b/src/email_triage/web/templates/admin/retry_queue/_index.html index 6391325..93750b7 100644 --- a/src/email_triage/web/templates/admin/retry_queue/_index.html +++ b/src/email_triage/web/templates/admin/retry_queue/_index.html @@ -85,6 +85,23 @@

Retry queue{{ m.help("Per-message failures the watcher couldn't classify on

Nothing in this view. (That's good — the queue is empty for the selected filter.)

{% else %} +{# #301 reachability — single-state views page via ?offset= so the full #} +{# backlog is reachable; the combined summary links to those views. #} +{% from "_pager.html" import pager %} +{% if page_win %} +{{ pager(page_win, "offset", request, "queue rows") }} +{% endif %} +{% if cap_segments %} +{% for seg in cap_segments %} +

+ Showing the first {{ seg.shown }} of {{ seg.total }} + {{ seg.label }} rows (this summary caps at {{ seg.cap }}); + {{ seg.total - seg.shown }} older row{{ 's' if (seg.total - seg.shown) != 1 else '' }} + not shown here — + view all {{ seg.total }} {{ seg.label }} → +

+{% endfor %} +{% endif %} @@ -151,7 +168,27 @@

Retry queue{{ m.help("Per-message failures the watcher couldn't classify on {% endif %}

- + @@ -529,9 +529,14 @@

Watcher state (current snapshot)

{% endif %} - + {# N-411 anti-irony fold-in: count semantics differ per-provider + (IMAP lifetime vs Gmail/O365 14-day rolling). Surface the + window per-row so an operator comparing accounts side-by-side + can't draw a false conclusion — exactly the operator-facing + accuracy defect class N-411 exists to prevent. #} + - + {% endfor %} diff --git a/src/email_triage/web/templates/admin/watches.html b/src/email_triage/web/templates/admin/watches.html index 3407af4..0190944 100644 --- a/src/email_triage/web/templates/admin/watches.html +++ b/src/email_triage/web/templates/admin/watches.html @@ -64,7 +64,7 @@

All watches{{ m.help("Cross-account view of every match-and-fire watch on th {{ acts | join(" + ") if acts else "(none)" }}

- {% if row.error_class_short %} + {# #7: plain-English flag for a watcher + delivery failure. failure_kind rides the row dict (SELECT *, + column since v35); without this the only signal was the raw + "WatchDeliveryFailed" qualname leaking through error_class_short + — which this page's audience header says operators shouldn't + have to read. The badge replaces that qualname for these rows. #} + {% if row.failure_kind == "watch_delivery_failure" %} + Delivery failure +
+ {% endif %} + {# Suppress the raw error-class qualname for ANY delivery-failure + row — the badge IS the operator-facing reason. This also covers + the compound edge (a routed action errored AND a watch was + undelivered on the same message): the generic re-raise bumps + the row's last_error_class to e.g. "RuntimeError", but + failure_kind stays watch_delivery_failure (COALESCE-preserved), + so the badge renders and the raw RuntimeError qualname does NOT + leak onto a page whose audience header forbids qualnames. The + error MESSAGE below still shows, so detail isn't lost. #} + {% if row.error_class_short + and row.failure_kind != "watch_delivery_failure" %} {{ row.error_class_short }} {% endif %} {% set msg = row.error_msg or row.last_error or "" %} diff --git a/src/email_triage/web/templates/admin/security.html b/src/email_triage/web/templates/admin/security.html index 2f40fed..48ea306 100644 --- a/src/email_triage/web/templates/admin/security.html +++ b/src/email_triage/web/templates/admin/security.html @@ -53,7 +53,7 @@

HIPAA Mode

{% if baa_status.acked %} yes ({{ baa_status.acked_by_email or '?' }} • - {{ baa_status.acked_at or '?' }}) + {% if baa_status.acked_at %}{% else %}?{% endif %}) {% else %} no {% endif %} diff --git a/src/email_triage/web/templates/admin/stats.html b/src/email_triage/web/templates/admin/stats.html index f938815..13edc4c 100644 --- a/src/email_triage/web/templates/admin/stats.html +++ b/src/email_triage/web/templates/admin/stats.html @@ -94,8 +94,8 @@

Operational signals

{{ health.uptime_secs | default(0) }} s
- last_triage {{ m.help("ISO timestamp of the most recent triage_runs row across every account. Compared against now() to derive the 'stale > 24h' degraded flag.") }} - {{ health.last_triage or "—" }} + last_triage {{ m.help("Timestamp of the most recent triage_runs row across every account. Server-rendered as US-local; JS enhances to your browser's local timezone.") }} + {% if health.last_triage %}{% else %}—{% endif %}
db {{ m.help("'ok' when a SELECT 1 on triage.db succeeds within the health budget; 'error' when the DB ping fails or times out.") }} @@ -510,7 +510,7 @@

Watcher state (current snapshot)

Account StatusProcessedProcessed (window) Errors Started
{{ w.status }}{{ w.processed }}{{ w.processed }} ({{ w.count_window or "?" }}) {{ w.errors }}{{ w.started_at or "-" }}{% if w.started_at %}{% else %}—{% endif %}
- {{ w.updated_at[:19] }} + {% if w.updated_at %}{% else %}—{% endif %} {% if w.account_id %} diff --git a/src/email_triage/web/templates/base.html b/src/email_triage/web/templates/base.html index 7b5c3b6..98cb97b 100644 --- a/src/email_triage/web/templates/base.html +++ b/src/email_triage/web/templates/base.html @@ -20,6 +20,11 @@ + {# #130 — base64url SSOT (`window.ETBase64`). Loaded unconditionally + so unauth pages (login + login_dev_keypair) get it too, since + WebAuthn login on the anonymous login page needs URL-safe base64 + decode/encode. Mirror-of Python `email_triage._base64url`. #} + {# CSRF shim — auto-attach X-CSRF-Token header on every state-changing fetch(). Loaded for authenticated callers only; skipped on the login page so unauthenticated /api/csrf-token @@ -123,9 +128,11 @@
  • Triage
  • Routes
  • My Settings
  • +
  • Deadlines
  • Help
  • + {% if user %}
  • Guide me
  • {% endif %} - {% if user.role == "admin" %} + {% if _admin_of(user) %}