Skip to content

feat(channels): opt-in auto-approval for new shared channels - #196

Merged
dborup merged 1 commit into
masterfrom
codex/channel-proposals-auto-approve
Oct 3, 2026
Merged

dborup merged 1 commit into
masterfrom
codex/channel-proposals-auto-approve

Conversation

@dborup

@dborup dborup commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Summary

Add an opt-in channelProposals.autoApprove setting (default false). When public channel suggestions are enabled, the ingestor approves a brand-new valid hashtag channel in the same database transaction as its insert and activates its decryption key through the existing queue flow. The read-only server remains read-only.

Existing pending, rejected and revoked proposals are not auto-approved by resubmission or restart. Duplicate requests and crash replays remain idempotent. The configured maxApproved cap is enforced before insertion; at capacity the request fails without creating or activating a channel. The existing public-submission gate (strong apiKey), name validation, queue cap and global submission rate limit remain unchanged.

Verification

  • cmd/ingestor: full go test ./... passed; focused auto-approval tests also passed under -race; go vet ./... passed.
  • cmd/server and internal/channelregistry: go test ./... passed.
  • Browser E2E: test-channel-proposals-e2e.js passed 23/23 checks, including the new auto-approval flow in a real browser, visibility in an independent session, and preservation of an older pending suggestion.
  • test-channel-proposals.js passed 32/32; git diff --check, Go formatting, JavaScript syntax and example JSON validation passed.

Operations and limitations

This changes no deployed configuration. To enable it, set both channelProposals.enabled: true and channelProposals.autoApprove: true in the shared config, retain a strong admin apiKey, and restart the ingestor. Disabling auto-approval later does not revoke already approved channels.

On a public instance, visitors can consume the finite approved-channel allowance, subject to the existing global submission rate limit. Rejected/revoked names are protected while their rows remain; after retention removes a row, the same name is indistinguishable from a new name and may be auto-approved again. This PR does not add a permanent denylist.

No merge, deploy, or production configuration change is included.

@dborup

dborup commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Review — CS-lead review PR#196 auto-approve — head 218c8fb

Verdict: APPROVE with nits. Nothing blocks the merge once CI is green on this head. Recommendation: merge the code, but keep autoApprove off in production until there is a moderation plan (nit 3).

Evidence tags: [T] test run, [A] analysis.

Verified

  • [A] Scope. One commit (218c8fba, dborup <kontakt@meshview.dk>), on top of master 8e4b13d0. git merge-tree against master is clean. The PR body has no closing keywords. All writes stay in cmd/ingestor, so the server stays read-only.
  • [A] New names only.
    • An existing row returns early, before the new code runs. Pending, rejected and revoked names are never auto-approved, and a revoked name is still resurrected to pending, as before.
    • Replays and crash recovery return the existing row, so they are idempotent.
  • [A] Limits.
    • maxApproved is counted inside the same transaction, before the insert.
    • The pending cap is skipped only when no pending row is created (!autoApprove || found).
  • [A] Key activation. RunOnce now activates keys for any approved result, not only OpApprove.
    • A re-submitted, already-approved name calls AddApproved again; it is idempotent, so this is harmless.
    • Revocation is unchanged.
  • [A] Retention. PruneChannelProposals never deletes approved rows. Auto-approved rows get reviewed_at = now, which is consistent with admin approvals.
  • [A] Gate. AutoApprovalRequested() requires SubmissionsRequested(), so autoApprove without enabled does nothing. The strong-apiKey gate on the server is untouched.
  • [T] Tests on head, from a git archive copy:
    • cmd/ingestor: go test -race -run ChannelProposal ok;
    • internal/channelregistry: go test ./... ok;
    • node test-channel-proposals.js: 32 passed, 0 failed.
  • [T] Mutants (each run on its own against the ChannelProposal tests):
    • drop the maxApproved check on auto-approval: TestChannelProposalAutoApproveRespectsLimits fails;
    • restrict key activation to OpApprove again: TestChannelProposalAutoApproveNewNamesOnly fails.
  • [A] E2E in CI. test-channel-proposals-e2e.js runs in CI (deploy.yml). The new steps cover:
    • restart with autoApprove without approving the older pending suggestion;
    • a browser suggestion shared without admin action;
    • visibility in a second session.

Nits (non-blocking)

  1. No fallback to pending at the cap. When maxApproved is reached and autoApprove is on, a new suggestion is refused outright ("the limit of shared channels has been reached"). Without autoApprove, the same suggestion would still be stored as pending for an administrator. Falling back to pending at the cap would be friendlier. The PR body documents the current behaviour, so this is a design choice, not a bug.

  2. The UI cannot tell in advance. /api/channel-proposals/config only exposes enabled, so the suggestion dialog cannot say beforehand that a suggestion will be shared immediately. The user only learns it from the result ("already shared with everyone"). Exposing the flag would let the dialog say so. This is optional.

  3. Operational risk on a public instance. With autoApprove on:

    • any visitor can publish a channel name to everyone at once, including offensive or junk names;
    • the global limit of 20 submissions per hour lets one visitor fill the default maxApproved of 128 in about 6.4 hours, which then blocks legitimate suggestions;
    • each approved channel adds a key, so there are more decryption attempts per GRP_TXT packet. This is bounded by maxApproved.

    Suggest documenting a moderation approach (a lower maxApproved, periodic review, revoke) next to the setting, and keeping it off by default, as it is.

Not verified

  • CI on 218c8fba was still running at the time of this review.
  • The full cmd/ingestor and cmd/server suites and the browser E2E were not rerun locally; the E2E runs in CI.
  • No production configuration was changed. autoApprove stays unset on all instances.

@dborup
dborup merged commit 457dbf3 into master Oct 3, 2026
6 checks passed
dborup-agent pushed a commit that referenced this pull request Oct 3, 2026
Brings in #182 (9d29dae), #191, #194 and #196, so that the observer anchor
and the backfill are tested against the current base. Master's server now
indexes live observations from the persisted resolved_path (#182).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbNdgonR8kq7SPEU7LcmXD
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