Skip to content

Add excludeChannels to Slack spawner matching - #1703

Open
knechtionscoding wants to merge 1 commit into
kelos-dev:mainfrom
datagravity-ai:feat/slack-exclude-channels-upstream
Open

Add excludeChannels to Slack spawner matching#1703
knechtionscoding wants to merge 1 commit into
kelos-dev:mainfrom
datagravity-ai:feat/slack-exclude-channels-upstream

Conversation

@knechtionscoding

@knechtionscoding knechtionscoding commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind api

What this PR does / why we need it:

Adds spec.when.slack.excludeChannels to TaskSpawner: a channel blocklist for Slack spawners.

An excluded channel is never matched — even when channels is empty (all channels) or lists the same channel. Exclusion always wins over the allowlist. Unlike excludePatterns, the exclusion also applies to slash commands, because the channel gate runs before the slash-command bypass in MatchesSpawner.

This makes it possible to carve specific channels out of a catch-all Slack spawner, so channel-scoped spawners can own those channels without the catch-all firing as well. Today the only way to get that is to convert the catch-all into an explicit allowlist and keep it in sync with every channel the bot is invited to.

Changes:

  • API (api/v1alpha2/taskspawner_types.go): ExcludeChannels []string on Slack, MaxItems=64 with the same ^[CG][A-Z0-9]{8,}$ item pattern as Channels, plus regenerated deepcopy and CRDs.
  • Matcher (internal/slack/filter.go): matchesChannel(channelID, allowed, excluded); exclusion is checked before the allowlist and before the slash-command bypass.
  • Conversion (internal/conversion/taskspawner.go): the field is v1alpha2-only, preserved across a v1alpha1 round-trip via the kelos.dev/v1alpha2-slack-exclude-channels annotation, mirroring the existing nameTemplate / githubAppAuth / credentials / commentsReporting pattern. Malformed annotation data is ignored rather than failing conversion.
  • CLI (internal/cli/printer.go): kelos get taskspawner <name> detail output shows Exclude Channels, so an operator can see why a spawner did not fire in a channel.
  • Docs (docs/reference.md): new row for the field.

Which issue(s) this PR is related to:

N/A

Special notes for your reviewer:

  • The field is optional and added only to v1alpha2 (the served/storage version); v1alpha1's schema and Go types are untouched, so existing manifests continue to apply unchanged.
  • The item pattern is ^[CGD][A-Z0-9]{8,}$, one character wider than channels' ^[CG].... DMs do reach the matcher — handleMessageEvent has no channel-type gate, so with channels empty a D... channel matches — and for an allowlist the missing D is harmless (omitting a DM excludes it) while for a blocklist it would be an expressiveness hole in exactly the field reached for to stop unwanted spawns. Loosening a brand-new field is safe now in a way tightening it later would not be.
  • +listType=set is declared on the new field (duplicate rejection, cleaner server-side-apply merges). I deliberately did not add it to the existing channels field in this PR: on a field with live objects it changes SSA merge behavior and starts rejecting duplicates that were previously accepted. Happy to do it as a follow-up if you want it.
  • I considered a CEL rule rejecting channelsexcludeChannels overlap and decided against it: overlap is a defined, useful semantic (keep a stable allowlist and mute one of its channels) rather than always a mistake, and a blanket rejection would forbid the precedence this field documents. The reference table states exclusion-wins explicitly instead.
  • v1alpha1 does not gain the capability. A client that reads and writes a spawner through v1alpha1 will not see excludeChannels, but the value is not silently dropped — it round-trips through the preservation annotation. Because this field fails toward more behavior if that annotation is ever lost (unlike nameTemplate/credentials/gatewayRefs, which fail toward less), both the godoc and the reference row say the exclusion is only guaranteed while the object is managed through v1alpha2.
  • The API server does not re-validate the output of a conversion webhook — I confirmed this in envtest: a v1alpha1 create carrying a hand-written ["lower-case-and-way-too-long!!","X"] annotation succeeded and the stored v1alpha2 object came back holding both values. So the restore path now checks the annotation payload against the field's own constraints (item pattern, maxItems, set uniqueness) and ignores it entirely when it fails, rather than partially applying it. Worth noting the sibling preserved fields (githubAppAuth, credentials, gatewayRefs, nameTemplate) share this property; I left them alone to keep this PR scoped.
  • All CRD YAML is make update output, not hand-edited.
  • Tests: matcher precedence (allowlist vs blocklist, empty allowlist, slash commands, DMs, and the positive non-excluded path), conversion round-trip plus the malformed-annotation and cleared-annotation branches, the full Exclude Channels: [...] line in the printer test, and an envtest spec in test/integration/conversion_test.go that creates the spawner in v1alpha2 (proving the real API server accepts the field, including a D... value), reads it as v1alpha1, writes it back through v1alpha1, and asserts excludeChannels is restored and the annotation cleaned up. The write is wrapped in the suite's conflict-tolerant Eventually idiom because the TaskSpawner controller reconciles the object concurrently.
  • Verified locally: make verify, make test, and the full ./test/integration/ suite pass.

Does this PR introduce a user-facing change?

Added `spec.when.slack.excludeChannels` to TaskSpawner — a Slack channel blocklist that wins over the `channels` allowlist and also applies to slash commands, so a catch-all spawner can be excluded from specific channels.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 12 files

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/conversion/taskspawner.go">

<violation number="1" location="internal/conversion/taskspawner.go:214">
P2: When a user supplies valid JSON containing an invalid ID or more than 64 entries, this branch restores it into `v1alpha2` without enforcing the field schema. Validate the item count and channel-ID pattern before assignment so a v1alpha1 round-trip cannot produce schema-invalid hub data or bypass these constraints.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/conversion/taskspawner.go

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/conversion/taskspawner.go">

<violation number="1" location="internal/conversion/taskspawner.go:219">
P2: When a v1alpha1 object carries valid JSON with invalid channel IDs or more than 64 entries, this assignment bypasses the v1alpha2 `Pattern` and `MaxItems` constraints. Validate every restored value against the field constraints or ignore the annotation, so conversion cannot produce invalid hub objects or ineffective exclusions.</violation>
</file>

<file name="test/integration/conversion_test.go">

<violation number="1" location="test/integration/conversion_test.go:178">
P3: The test creates namespace test-conv-ts-slack and TaskSpawner ts-slack but never cleans them up; the AfterAll only removes AgentConfig objects in test-conv-up/test-conv-down. The TaskSpawner controller adds the kelos.dev/taskspawner-finalizer, so leftover instances (this one and test-conv-ts from the test above) persist and can block later install/uninstall specs that delete the taskspawner CRD. Delete the TaskSpawner and namespace in AfterAll (or via DeferCleanup) alongside the AgentConfig cleanup.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/conversion/taskspawner.go
Comment thread test/integration/conversion_test.go
spec.when.slack.excludeChannels is a channel blocklist for Slack
spawners. An excluded channel never matches, even when channels is empty
(all channels) or names the same channel — exclusion always wins over
the allowlist. Unlike excludePatterns, the exclusion also applies to
slash commands, because the channel gate runs before the slash-command
bypass in MatchesSpawner.

This makes it possible to carve specific channels out of a catch-all
spawner so that channel-scoped spawners can own those channels without
the catch-all firing as well.

The item pattern also accepts direct-message IDs, which channels does
not: a spawner with an empty channels list matches DMs, so a blocklist
that could not name them would leave the one case users most want to
stop. Like channels, this is a post-delivery filter and not a privacy
scope — the type-level doc now says so for both fields.

The field is v1alpha2-only and optional, so existing manifests are
unaffected. It survives a v1alpha1 round-trip via the
kelos.dev/v1alpha2-slack-exclude-channels annotation, mirroring the
existing nameTemplate / githubAppAuth / credentials / commentsReporting
preservation pattern; v1alpha1 itself does not gain the capability.
Because the API server does not re-validate the output of a conversion
webhook, restored annotation data is checked against the field's own
constraints — item pattern, maxItems, and set uniqueness — and ignored
entirely when it fails any of them, so conversion cannot produce a hub
object that a v1alpha2 write would have rejected.

Co-Authored-By: Gravity Agent <gravity@anomalo.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@knechtionscoding
knechtionscoding force-pushed the feat/slack-exclude-channels-upstream branch from 8dbf7f1 to 4da9678 Compare August 26, 2026 17:46

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/manifests/install-crd.yaml">

<violation number="1" location="internal/manifests/install-crd.yaml:60643">
P2: The new excludeChannels field has x-kubernetes-list-type: set to prevent duplicates, but the related channels field lacks this marker. Both fields represent sets of unique channel IDs where duplicates serve no purpose. This inconsistency makes the API design less uniform. Consider adding the same list-type marker to the channels field in the source types (api/v1alpha2/taskspawner_types.go) and regenerating the CRD.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

type: string
maxItems: 64
type: array
x-kubernetes-list-type: set

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new excludeChannels field has x-kubernetes-list-type: set to prevent duplicates, but the related channels field lacks this marker. Both fields represent sets of unique channel IDs where duplicates serve no purpose. This inconsistency makes the API design less uniform. Consider adding the same list-type marker to the channels field in the source types (api/v1alpha2/taskspawner_types.go) and regenerating the CRD.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/manifests/install-crd.yaml, line 60643:

<comment>The new excludeChannels field has x-kubernetes-list-type: set to prevent duplicates, but the related channels field lacks this marker. Both fields represent sets of unique channel IDs where duplicates serve no purpose. This inconsistency makes the API design less uniform. Consider adding the same list-type marker to the channels field in the source types (api/v1alpha2/taskspawner_types.go) and regenerating the CRD.</comment>

<file context>
@@ -60620,6 +60620,27 @@ spec:
+                          type: string
+                        maxItems: 64
+                        type: array
+                        x-kubernetes-list-type: set
                       excludePatterns:
                         description: |-
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/api Categorizes issue or PR as related to API changes needs-actor needs-priority needs-triage release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant