Skip to content

Commit 0a46f7c

Browse files
committed
refactor slack configuration to separate backend-specific settings for Hermes and OpenClaw
Signed-off-by: Peter Jausovec <peter.jausovec@solo.io>
1 parent b764f55 commit 0a46f7c

7 files changed

Lines changed: 186 additions & 184 deletions

File tree

go/api/config/crd/bases/kagent.dev_agentharnesses.yaml

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -86,40 +86,6 @@ spec:
8686
slack:
8787
description: Slack configures Slack when type is Slack.
8888
properties:
89-
allowedUserIDs:
90-
description: AllowedUserIDs restricts which Slack member
91-
IDs may interact with the bot (SLACK_ALLOWED_USERS).
92-
items:
93-
type: string
94-
type: array
95-
allowedUserIDsFrom:
96-
description: ValueSource defines a source for configuration
97-
values from a Secret or ConfigMap
98-
properties:
99-
key:
100-
description: The key of the ConfigMap or Secret.
101-
maxLength: 253
102-
type: string
103-
name:
104-
description: The name of the ConfigMap or Secret.
105-
maxLength: 253
106-
type: string
107-
type:
108-
enum:
109-
- ConfigMap
110-
- Secret
111-
type: string
112-
required:
113-
- key
114-
- name
115-
- type
116-
type: object
117-
allowlistChannels:
118-
description: AllowlistChannels is required when channelAccess
119-
is allowlist.
120-
items:
121-
type: string
122-
type: array
12389
appToken:
12490
description: AgentHarnessChannelCredential supplies a token
12591
from an inline value or a Secret/ConfigMap key.
@@ -188,38 +154,82 @@ spec:
188154
- message: Exactly one of value or valueFrom must be specified
189155
rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value)
190156
&& has(self.valueFrom))
191-
channelAccess:
192-
description: AgentHarnessChannelAccess controls whether
193-
the bot listens broadly or only on an allowlist.
194-
enum:
195-
- allowlist
196-
- open
197-
- disabled
198-
type: string
199-
homeChannel:
200-
description: HomeChannel is the default Slack channel ID
201-
for cron/scheduled messages (SLACK_HOME_CHANNEL).
202-
type: string
203-
homeChannelName:
204-
description: HomeChannelName is a human-readable label for
205-
HomeChannel (SLACK_HOME_CHANNEL_NAME).
206-
type: string
207-
interactiveReplies:
208-
default: true
209-
type: boolean
157+
hermes:
158+
description: Hermes configures Hermes-specific Slack settings.
159+
properties:
160+
allowedUserIDs:
161+
description: AllowedUserIDs restricts which Slack member
162+
IDs may interact with the bot (SLACK_ALLOWED_USERS).
163+
items:
164+
type: string
165+
type: array
166+
allowedUserIDsFrom:
167+
description: ValueSource defines a source for configuration
168+
values from a Secret or ConfigMap
169+
properties:
170+
key:
171+
description: The key of the ConfigMap or Secret.
172+
maxLength: 253
173+
type: string
174+
name:
175+
description: The name of the ConfigMap or Secret.
176+
maxLength: 253
177+
type: string
178+
type:
179+
enum:
180+
- ConfigMap
181+
- Secret
182+
type: string
183+
required:
184+
- key
185+
- name
186+
- type
187+
type: object
188+
homeChannel:
189+
description: HomeChannel is the default Slack channel
190+
ID for cron/scheduled messages (SLACK_HOME_CHANNEL).
191+
type: string
192+
homeChannelName:
193+
description: HomeChannelName is a human-readable label
194+
for HomeChannel (SLACK_HOME_CHANNEL_NAME).
195+
type: string
196+
type: object
197+
x-kubernetes-validations:
198+
- message: allowedUserIDs and allowedUserIDsFrom are mutually
199+
exclusive
200+
rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))'
201+
openclaw:
202+
description: OpenClaw configures OpenClaw/NemoClaw-specific
203+
Slack routing.
204+
properties:
205+
allowlistChannels:
206+
description: AllowlistChannels is required when channelAccess
207+
is allowlist.
208+
items:
209+
type: string
210+
type: array
211+
channelAccess:
212+
description: AgentHarnessChannelAccess controls whether
213+
the bot listens broadly or only on an allowlist.
214+
enum:
215+
- allowlist
216+
- open
217+
- disabled
218+
type: string
219+
interactiveReplies:
220+
default: true
221+
type: boolean
222+
type: object
223+
x-kubernetes-validations:
224+
- message: allowlistChannels is required when channelAccess
225+
is allowlist
226+
rule: '!has(self.channelAccess) || self.channelAccess
227+
!= ''allowlist'' || (has(self.allowlistChannels) &&
228+
size(self.allowlistChannels) > 0)'
210229
required:
211230
- appToken
212231
- botToken
213232
type: object
214-
x-kubernetes-validations:
215-
- message: allowedUserIDs and allowedUserIDsFrom are mutually
216-
exclusive
217-
rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))'
218-
- message: allowlistChannels is required when channelAccess
219-
is allowlist
220-
rule: '!has(self.channelAccess) || self.channelAccess != ''allowlist''
221-
|| (has(self.allowlistChannels) && size(self.allowlistChannels)
222-
> 0)'
223233
telegram:
224234
description: AgentHarnessTelegramChannelSpec configures Telegram
225235
when AgentHarnessChannel.type is Telegram.
@@ -500,6 +510,12 @@ spec:
500510
required:
501511
- backend
502512
type: object
513+
x-kubernetes-validations:
514+
- message: slack backend-specific settings must match spec.backend
515+
rule: '!has(self.channels) || self.channels.all(c, c.type != ''slack''
516+
|| (has(c.slack) && ((self.backend == ''hermes'' && has(c.slack.hermes)
517+
&& !has(c.slack.openclaw)) || ((self.backend == ''openclaw'' || self.backend
518+
== ''nemoclaw'') && has(c.slack.openclaw) && !has(c.slack.hermes)))))'
503519
status:
504520
description: AgentHarnessStatus is the observed state of an AgentHarness.
505521
properties:

go/api/v1alpha2/agentharness_types.go

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,31 +111,19 @@ type AgentHarnessHermesSlackOptions struct {
111111
}
112112

113113
// AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack.
114-
// YAML is flat: botToken, appToken, plus backend-specific fields. Which fields apply is determined
115-
// by spec.backend on the AgentHarness; OpenClaw and Hermes settings are separate structs in Go.
114+
// Backend-specific settings live under the matching backend key; AgentHarnessSpec validation
115+
// requires the key to match spec.backend.
116116
type AgentHarnessSlackChannelSpec struct {
117117
// +required
118118
BotToken AgentHarnessChannelCredential `json:"botToken"`
119119
// +required
120-
AppToken AgentHarnessChannelCredential `json:"appToken"`
121-
AgentHarnessOpenClawSlackOptions `json:",inline"`
122-
AgentHarnessHermesSlackOptions `json:",inline"`
123-
}
124-
125-
// OpenClawOptions returns OpenClaw/NemoClaw Slack settings embedded in the spec.
126-
func (s *AgentHarnessSlackChannelSpec) OpenClawOptions() *AgentHarnessOpenClawSlackOptions {
127-
if s == nil {
128-
return nil
129-
}
130-
return &s.AgentHarnessOpenClawSlackOptions
131-
}
132-
133-
// HermesOptions returns Hermes Slack settings embedded in the spec.
134-
func (s *AgentHarnessSlackChannelSpec) HermesOptions() *AgentHarnessHermesSlackOptions {
135-
if s == nil {
136-
return nil
137-
}
138-
return &s.AgentHarnessHermesSlackOptions
120+
AppToken AgentHarnessChannelCredential `json:"appToken"`
121+
// OpenClaw configures OpenClaw/NemoClaw-specific Slack routing.
122+
// +optional
123+
OpenClaw *AgentHarnessOpenClawSlackOptions `json:"openclaw,omitempty"`
124+
// Hermes configures Hermes-specific Slack settings.
125+
// +optional
126+
Hermes *AgentHarnessHermesSlackOptions `json:"hermes,omitempty"`
139127
}
140128

141129
// AgentHarnessChannel declares one messenger binding inside a harness VM.
@@ -161,6 +149,7 @@ type AgentHarnessChannel struct {
161149
// An AgentHarness is distinct from a SandboxAgent: it has no agent runtime baked
162150
// in. The backend is responsible for provisioning an environment that stays
163151
// ready to accept incoming commands.
152+
// +kubebuilder:validation:XValidation:rule="!has(self.channels) || self.channels.all(c, c.type != 'slack' || (has(c.slack) && ((self.backend == 'hermes' && has(c.slack.hermes) && !has(c.slack.openclaw)) || ((self.backend == 'openclaw' || self.backend == 'nemoclaw') && has(c.slack.openclaw) && !has(c.slack.hermes)))))",message="slack backend-specific settings must match spec.backend"
164153
type AgentHarnessSpec struct {
165154
// Backend selects the control plane to use. Required.
166155
// +required

go/api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/core/pkg/sandboxbackend/openshell/channels/resolve.go

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -104,49 +104,22 @@ func (r *Resolved) addSlackChannel(ctx context.Context, kube client.Client, name
104104
}
105105
switch backend {
106106
case v1alpha2.AgentHarnessBackendHermes:
107-
if err := rejectOpenClawSlackFields(ch.Name, spec); err != nil {
108-
return err
107+
opts := spec.Hermes
108+
if opts == nil {
109+
opts = &v1alpha2.AgentHarnessHermesSlackOptions{}
109110
}
110-
return r.addHermesSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, spec.HermesOptions())
111+
return r.addHermesSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, opts)
111112
case v1alpha2.AgentHarnessBackendOpenClaw, v1alpha2.AgentHarnessBackendNemoClaw:
112-
if err := rejectHermesSlackFields(ch.Name, spec); err != nil {
113-
return err
113+
opts := spec.OpenClaw
114+
if opts == nil {
115+
opts = &v1alpha2.AgentHarnessOpenClawSlackOptions{}
114116
}
115-
return r.addOpenClawSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, spec.OpenClawOptions())
117+
return r.addOpenClawSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, opts)
116118
default:
117119
return fmt.Errorf("channel %q: slack channels are not supported for backend %q", ch.Name, backend)
118120
}
119121
}
120122

121-
func rejectOpenClawSlackFields(channelName string, spec *v1alpha2.AgentHarnessSlackChannelSpec) error {
122-
opts := spec.OpenClawOptions()
123-
if opts == nil {
124-
return nil
125-
}
126-
if opts.ChannelAccess != "" {
127-
return fmt.Errorf("channel %q: channelAccess is not supported when backend is hermes", channelName)
128-
}
129-
if len(opts.AllowlistChannels) > 0 {
130-
return fmt.Errorf("channel %q: allowlistChannels is not supported when backend is hermes", channelName)
131-
}
132-
if opts.InteractiveReplies != nil {
133-
return fmt.Errorf("channel %q: interactiveReplies is not supported when backend is hermes", channelName)
134-
}
135-
return nil
136-
}
137-
138-
func rejectHermesSlackFields(channelName string, spec *v1alpha2.AgentHarnessSlackChannelSpec) error {
139-
opts := spec.HermesOptions()
140-
if opts == nil {
141-
return nil
142-
}
143-
if len(opts.AllowedUserIDs) > 0 || opts.AllowedUserIDsFrom != nil ||
144-
strings.TrimSpace(opts.HomeChannel) != "" || strings.TrimSpace(opts.HomeChannelName) != "" {
145-
return fmt.Errorf("channel %q: Hermes slack fields are not supported when backend is openclaw or nemoclaw", channelName)
146-
}
147-
return nil
148-
}
149-
150123
func (r *Resolved) putSlackCredentials(ctx context.Context, kube client.Client, namespace, channelName string, botToken, appToken v1alpha2.AgentHarnessChannelCredential) error {
151124
if err := PutChannelCredential(ctx, kube, namespace, botToken, SlackBotTokenEnvKey(channelName), r.Secrets); err != nil {
152125
return fmt.Errorf("channel %q slack bot token: %w", channelName, err)

go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func TestBuildBootstrapArtifacts_TelegramSlack(t *testing.T) {
7373
Slack: &v1alpha2.AgentHarnessSlackChannelSpec{
7474
BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"},
7575
AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"},
76-
AgentHarnessHermesSlackOptions: v1alpha2.AgentHarnessHermesSlackOptions{
76+
Hermes: &v1alpha2.AgentHarnessHermesSlackOptions{
7777
AllowedUserIDs: []string{"U01234567", "U89ABCDEF"},
7878
HomeChannel: "C01234567890",
7979
HomeChannelName: "general",

go/core/pkg/sandboxbackend/openshell/translate_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func TestBuildOpenshellCreateRequest_OpenClaw_Slack_HasSlackPolicy(t *testing.T)
167167
Slack: &v1alpha2.AgentHarnessSlackChannelSpec{
168168
BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "b"},
169169
AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "a"},
170-
AgentHarnessOpenClawSlackOptions: v1alpha2.AgentHarnessOpenClawSlackOptions{
170+
OpenClaw: &v1alpha2.AgentHarnessOpenClawSlackOptions{
171171
ChannelAccess: v1alpha2.AgentHarnessChannelAccessOpen,
172172
},
173173
},

0 commit comments

Comments
 (0)