Skip to content

Promote edge to stable: invite-gate and moderation security fixes, CLI participation commands - #3075

Merged
colinmegill merged 6 commits into
stablefrom
edge
Oct 16, 2026
Merged

colinmegill merged 6 commits into
stablefrom
edge

Conversation

@colinmegill

Copy link
Copy Markdown
Member

Security: removes the unused POST /api/v3/joinWithInvite route (it admitted people past invite-only and external-id gates); a seed statement counts only from a moderator (anyone could skip strict moderation); comments no longer wait on the toxicity check (no identity-provider or language-model calls before a comment is saved). Also CLI invite reads and creation, the conversation table, and the polis-auth crate (not deployed). No schema change.

colinmegill and others added 6 commits October 15, 2026 18:56
* cli: read-only invite commands

Adds `polis invite code list`, `invite wave list`, `invite list`,
`invite export`, `invite status` and `conversation uuid` over the existing
invite routes, with explicit output projections, a safe CSV export and the
CLI's usual exit codes. The uuid request can mint a key on the server, so an
uncertain outcome exits 7. No server change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

* contracts: invite owner-check refusals map to forbidden

The invite administration routes now answer 403 with dedicated codes; list
them in the error catalog so clients report forbidden rather than a generic
failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

* cli: invite errors follow the route that refused

- The permission-lookup failure codes on the invite routes are site faults
  (HTTP 500), so they now report server_error instead of falling into the
  generic "auth" refusal.
- Refusal advice matches the route: the conversation-code read is owner-only,
  while invite waves, invite lists and emailed invites also admit a moderator
  on the owner's site.
- invite list and invite status accept any of a conversation's codes; the
  conversation read may echo a different one.
- An invalid invite response reports outcome "rejected", since a response
  was received.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* polis-auth: a typed owner/moderator check for Rust routes

A standalone crate that evaluates conversation authority the way the Node
server's isModerator does: an explicit Owner or Moderator policy, admin uids
bypassing only the moderator policy before any database read, and database
failures kept distinct from a refusal. No route uses it yet. Adds its CI entry
point and job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

* ci: re-run the polis-auth parity test when isModerator changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`polis conversation list` now prints a table by default: topic, a shortened
description, the conversation link and its newest report link (with "+N"
when there are more). Report links come from one GET /api/v3/reports read,
which returns every report of the caller's conversations. That read is best
effort: if it fails, the table still prints with the report column empty and
one warning on stderr. Links use the selected site's origin and are never
shortened; user text has control and invisible formatting characters removed.
`--json` output is unchanged.


Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* cli: invite code create and invite wave create

Adds two commands that mint invites through the existing Node routes:
`polis invite code create` (POST /api/v3/zinvites/0, the conversation named by
conversation_id) and
`polis invite wave create` (POST /api/v3/treevite/waves). Each sends one
request and then reads back what it created: the new code must appear exactly
once, and the new wave row must match the returned wave. A lost response, a
server error, an unknown success shape or a failed read-back exits 7
(outcome unknown, no automatic retry); an owner refusal exits 4. No server
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

* contracts: a refused invite code create reports the owner-only refusal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

* cli: create commands give owner-only or moderator advice on refusal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* server: remove the unused joinWithInvite route

POST /api/v3/joinWithInvite has no caller in any client, test or tool, and
it admitted people past the conversation's gates: with no invite at all it
created a participant row in an invite-only conversation, which the vote
route then accepted as membership; it created rows before the external-id
allowlist check; and it signed tokens with the internal numeric conversation
id. The route, its handler and helpers are removed; the logout handler stays.

Observable change: POST /api/v3/joinWithInvite now answers 404 and writes
nothing. Votes, comments, nextComment, participationInit and participant
resolution are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

* tests: the joinWithInvite test sends its vote through the convention module

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… on the toxicity check (#3074)

* server: only a moderator can contribute a seed statement

POST /api/v3/comments approved any caller's statement on arrival when the
request carried is_seed, so anyone, even without signing in, could skip
strict moderation and have a statement shown to every participant. The seed
flag now counts only for a moderator of the conversation; from anyone else
it is ignored, the statement is stored as an ordinary one and moderated like
any other. Moderators are unchanged, with or without the flag. The bulk
upload route already required a moderator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

* server: comments no longer wait on the toxicity check

Every comment from a non-moderator first asked the identity provider, with
two uncached management API calls, whether the conversation's owner had the
plan that enables a toxicity check, and for those owners it then sent the
statement to an external language model before saving it. Those calls had no
time limit, so a slow provider held the citizen's statement unsaved. The
check is off: a statement is saved with the conversation's own moderation
setting and no outside call. The functions stay in place, unused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Delphi Coverage Report

File Stmts Miss Cover
polismath/init.py 2 0 100%
polismath/benchmarks/bench_pca.py 128 107 16%
polismath/benchmarks/bench_repness.py 81 65 20%
polismath/benchmarks/bench_update_votes.py 38 28 26%
polismath/benchmarks/benchmark_utils.py 34 0 100%
polismath/components/init.py 1 0 100%
polismath/components/config.py 165 70 58%
polismath/conversation/init.py 2 0 100%
polismath/conversation/conversation.py 1291 264 80%
polismath/conversation/group_vote_counts.py 38 2 95%
polismath/conversation/manager.py 131 42 68%
polismath/database/init.py 1 0 100%
polismath/database/dynamodb.py 392 176 55%
polismath/database/postgres.py 414 95 77%
polismath/delphi_storage/init.py 1 0 100%
polismath/delphi_storage/codec.py 295 22 93%
polismath/delphi_storage/embedding_spool.py 58 6 90%
polismath/delphi_storage/golden_corpus.py 60 9 85%
polismath/delphi_storage/import_report.py 51 3 94%
polismath/delphi_storage/legacy_import.py 171 16 91%
polismath/delphi_storage/postgres.py 49 22 55%
polismath/delphi_storage/production_import.py 612 131 79%
polismath/delphi_storage/resource.py 179 32 82%
polismath/delphi_storage/scan_pacing.py 77 0 100%
polismath/delphi_storage/writer.py 193 18 91%
polismath/empty_output.py 29 2 93%
polismath/engine_adapter.py 317 265 16%
polismath/job_child/init.py 356 52 85%
polismath/job_child/census.py 101 8 92%
polismath/pca_kmeans_rep/init.py 5 0 100%
polismath/pca_kmeans_rep/clusters.py 257 38 85%
polismath/pca_kmeans_rep/corr.py 98 17 83%
polismath/pca_kmeans_rep/group_k_smoother.py 28 0 100%
polismath/pca_kmeans_rep/legacy_kmeans.py 197 8 96%
polismath/pca_kmeans_rep/pca.py 162 19 88%
polismath/pca_kmeans_rep/repness.py 226 5 98%
polismath/poller/init.py 4 0 100%
polismath/poller/admission.py 318 15 95%
polismath/poller/backfill.py 936 32 97%
polismath/poller/capacity.py 485 27 94%
polismath/poller/capacity_queue.py 248 13 95%
polismath/poller/coordinator_bridge.py 317 317 0%
polismath/poller/math_writer.py 113 3 97%
polismath/poller/promotion.py 112 4 96%
polismath/poller/readiness.py 368 13 96%
polismath/poller/rebuild_child.py 133 5 96%
polismath/poller/service.py 665 52 92%
polismath/poller/worker_pool.py 140 0 100%
polismath/queue/executor.py 223 111 50%
polismath/regression/init.py 4 0 100%
polismath/regression/clojure_comparer.py 188 20 89%
polismath/regression/comparer.py 891 640 28%
polismath/regression/datasets.py 135 27 80%
polismath/regression/recorder.py 36 27 25%
polismath/regression/utils.py 130 85 35%
polismath/replay/init.py 2 0 100%
polismath/replay/attribution_capture.py 49 0 100%
polismath/replay/axis_continuity.py 347 24 93%
polismath/replay/certify.py 999 59 94%
polismath/replay/crosslang.py 128 2 98%
polismath/replay/decision_ties.py 55 8 85%
polismath/replay/diagnostics.py 50 7 86%
polismath/replay/driver.py 73 1 99%
polismath/replay/dsn_admission.py 36 1 97%
polismath/replay/equiv_query.py 10 0 100%
polismath/replay/event_ingress.py 95 1 99%
polismath/replay/fixture_bundle.py 815 62 92%
polismath/replay/fixture_config.py 266 11 96%
polismath/replay/fixture_extract.py 285 12 96%
polismath/replay/fixture_generate.py 103 1 99%
polismath/replay/fixture_samples.py 157 14 91%
polismath/replay/fixture_selection.py 119 0 100%
polismath/replay/fixture_survey.py 147 11 93%
polismath/replay/legacy_pca.py 29 17 41%
polismath/replay/polarity.py 319 25 92%
polismath/replay/poller_equiv.py 776 99 87%
polismath/replay/prodclone.py 193 2 99%
polismath/replay/real_data.py 146 10 93%
polismath/replay/schedule.py 205 10 95%
polismath/replay/served_math.py 159 4 97%
polismath/replay/shard_bench.py 189 65 66%
polismath/replay/stagecompare.py 890 161 82%
polismath/replay/stages.py 256 40 84%
polismath/replay/stepcompare.py 86 1 99%
polismath/replay/store.py 120 11 91%
polismath/replay/tie_capture.py 107 19 82%
polismath/replay/types.py 71 6 92%
polismath/run_math_pipeline.py 284 93 67%
polismath/tools/engine_rebuild.py 230 11 95%
polismath/types.py 22 0 100%
polismath/utils/cli_flags.py 15 0 100%
polismath/utils/clj_hash.py 44 0 100%
polismath/utils/env_flags.py 13 0 100%
polismath/utils/general.py 63 39 38%
polismath/utils/output_profile.py 66 2 97%
polismath/utils/serialization.py 10 5 50%
polismath/utils/vote_convention.py 159 5 97%
umap_narrative/501_calculate_comment_extremity.py 113 38 66%
umap_narrative/502_calculate_priorities.py 136 103 24%
umap_narrative/700_datamapplot_for_layer.py 507 507 0%
umap_narrative/701_static_datamapplot_for_layer.py 310 310 0%
umap_narrative/702_consensus_divisive_datamapplot.py 435 275 37%
umap_narrative/801_narrative_report_batch.py 666 455 32%
umap_narrative/803_check_batch_status.py 268 148 45%
umap_narrative/llm_factory_constructor/init.py 2 0 100%
umap_narrative/llm_factory_constructor/model_provider.py 234 107 54%
umap_narrative/narrative_data.py 178 65 63%
umap_narrative/numerical_stages.py 54 25 54%
umap_narrative/polismath_commentgraph/init.py 1 0 100%
umap_narrative/polismath_commentgraph/cli.py 268 268 0%
umap_narrative/polismath_commentgraph/core/init.py 3 3 0%
umap_narrative/polismath_commentgraph/core/clustering.py 108 108 0%
umap_narrative/polismath_commentgraph/core/embedding.py 104 104 0%
umap_narrative/polismath_commentgraph/lambda_handler.py 219 219 0%
umap_narrative/polismath_commentgraph/schemas/init.py 2 0 100%
umap_narrative/polismath_commentgraph/schemas/dynamo_models.py 160 9 94%
umap_narrative/polismath_commentgraph/tests/conftest.py 17 17 0%
umap_narrative/polismath_commentgraph/tests/test_clustering.py 74 74 0%
umap_narrative/polismath_commentgraph/tests/test_embedding.py 55 55 0%
umap_narrative/polismath_commentgraph/tests/test_storage.py 87 87 0%
umap_narrative/polismath_commentgraph/utils/init.py 9 7 22%
umap_narrative/polismath_commentgraph/utils/converter.py 283 237 16%
umap_narrative/polismath_commentgraph/utils/group_data.py 332 289 13%
umap_narrative/polismath_commentgraph/utils/storage.py 609 489 20%
umap_narrative/reset_conversation.py 162 53 67%
umap_narrative/run_pipeline.py 342 212 38%
umap_narrative/topic_naming.py 148 11 93%
Total 25760 8027 69%

@colinmegill
colinmegill merged commit f24b863 into stable Oct 16, 2026
57 of 63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant