From 36de01bfb53a17dcdeabb2d25652200654b3170f Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Thu, 23 Jul 2026 12:06:46 -0400 Subject: [PATCH 1/8] Tunable sentinel poll intervals This apparently never was added to the AgentConfiguration crd, adds it in so we can tune in high cluster deployments --- .../workbench/job/WorkbenchJobActivity.tsx | 11 ++- .../api/v1alpha1/agentconfig_types.go | 4 + .../api/v1alpha1/zz_generated.deepcopy.go | 17 +++- go/deployment-operator/cmd/agent/args/args.go | 16 ++++ go/deployment-operator/cmd/agent/console.go | 8 +- go/deployment-operator/cmd/agent/main_test.go | 5 ++ ...oyments.plural.sh_agentconfigurations.yaml | 5 ++ .../config/samples/agentConfiguration.yaml | 1 + go/deployment-operator/docs/api.md | 1 + go/deployment-operator/pkg/common/config.go | 16 ++++ .../pkg/controller/sentinel/interval_test.go | 81 +++++++++++++++++++ .../pkg/controller/sentinel/reconciler.go | 29 ++++++- .../controller/sentinel/socket_publisher.go | 2 +- 13 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 go/deployment-operator/pkg/controller/sentinel/interval_test.go diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx index f7fa065ba1..f8d5978694 100644 --- a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx @@ -238,6 +238,7 @@ export function WorkbenchJobMemoGroup({ key={activity.id} activity={activity} textStream={textStreamMap[activity.id] ?? ''} + showMemoPrefix={activities.length > 1} /> ))} @@ -286,9 +287,11 @@ export function WorkbenchJobMemoGroup({ function WorkbenchJobMemo({ activity, textStream, + showMemoPrefix = true, }: { activity: WorkbenchJobActivityFragment textStream: string + showMemoPrefix?: boolean }) { const { prompt, result, status } = activity const [isOpen, setIsOpen] = useState(false) @@ -305,7 +308,13 @@ function WorkbenchJobMemo({ setIsOpen(true)}> - Memo {label} + {showMemoPrefix ? ( + <> + Memo {label} + + ) : ( + label + )} {result?.jobUpdate && } diff --git a/go/deployment-operator/api/v1alpha1/agentconfig_types.go b/go/deployment-operator/api/v1alpha1/agentconfig_types.go index be33eeff3f..f8a73dafcb 100644 --- a/go/deployment-operator/api/v1alpha1/agentconfig_types.go +++ b/go/deployment-operator/api/v1alpha1/agentconfig_types.go @@ -24,6 +24,10 @@ type AgentConfigurationSpec struct { // Set to "0s" to disable stack polling. StackPollInterval *string `json:"stackPollInterval,omitempty"` + // SentinelPollInterval sets how often the agent polls for sentinel run jobs. + // Set to "0s" to disable sentinel run job polling. + SentinelPollInterval *string `json:"sentinelPollInterval,omitempty"` + // PipelineGateInterval specifies how frequently the agent checks pipeline gates. // Set to "0s" to disable pipeline gate checks. PipelineGateInterval *string `json:"pipelineGateInterval,omitempty"` diff --git a/go/deployment-operator/api/v1alpha1/zz_generated.deepcopy.go b/go/deployment-operator/api/v1alpha1/zz_generated.deepcopy.go index 8455eefd19..62e6a24600 100644 --- a/go/deployment-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/go/deployment-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1alpha1 import ( "github.com/pluralsh/console/go/client" batchv1 "k8s.io/api/batch/v1" - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -136,6 +136,11 @@ func (in *AgentConfigurationSpec) DeepCopyInto(out *AgentConfigurationSpec) { *out = new(string) **out = **in } + if in.SentinelPollInterval != nil { + in, out := &in.SentinelPollInterval, &out.SentinelPollInterval + *out = new(string) + **out = **in + } if in.PipelineGateInterval != nil { in, out := &in.PipelineGateInterval, &out.PipelineGateInterval *out = new(string) @@ -532,11 +537,21 @@ func (in *AgentRuntimeSpec) DeepCopyInto(out *AgentRuntimeSpec) { *out = new(bool) **out = **in } + if in.StreamingProxy != nil { + in, out := &in.StreamingProxy, &out.StreamingProxy + *out = new(bool) + **out = **in + } if in.Dind != nil { in, out := &in.Dind, &out.Dind *out = new(bool) **out = **in } + if in.Memory != nil { + in, out := &in.Memory, &out.Memory + *out = new(bool) + **out = **in + } if in.AllowedRepositories != nil { in, out := &in.AllowedRepositories, &out.AllowedRepositories *out = make([]string, len(*in)) diff --git a/go/deployment-operator/cmd/agent/args/args.go b/go/deployment-operator/cmd/agent/args/args.go index eefd2b49dc..bf6438f90f 100644 --- a/go/deployment-operator/cmd/agent/args/args.go +++ b/go/deployment-operator/cmd/agent/args/args.go @@ -89,6 +89,9 @@ const ( defaultStackPollInterval = "30s" defaultStackPollIntervalDuration = 30 * time.Second + defaultSentinelPollInterval = "30s" + defaultSentinelPollIntervalDuration = 30 * time.Second + defaultPipelineGatesPollInterval = "0s" defaultPipelineGatesPollIntervalDuration = 0 * time.Second @@ -158,6 +161,7 @@ var ( argClusterPingInterval = flag.String("cluster-ping-interval", defaultClusterPingInterval, "Time interval to ping cluster.") argRuntimeServicePingInterval = flag.String("runtime-service-ping-interval", defaultRuntimeServicePingInterval, "Time interval to register runtime services.") argStackPollInterval = flag.String("stack-poll-interval", defaultStackPollInterval, "Time interval to poll stack resources from the Console API. Use '0s' to disable.") + argSentinelPollInterval = flag.String("sentinel-poll-interval", defaultSentinelPollInterval, "Time interval to poll sentinel run jobs from the Console API. Use '0s' to disable.") argPipelineGatesPollInterval = flag.String("pipline-gates-poll-interval", defaultPipelineGatesPollInterval, "Time interval to poll PipelineGates resources from the Console API. It's disabled by default.") argDisableWebsocket = flag.Bool("disable-websocket", false, "Disable the cluster websocket connection to the Console.") argDiscoveryCacheRefreshInterval = flag.String("discovery-cache-refresh-interval", defaultDiscoveryCacheRefreshInterval, "Time interval to refresh discovery cache.") @@ -504,6 +508,16 @@ func StackPollInterval() time.Duration { return duration } +func SentinelPollInterval() time.Duration { + duration, err := time.ParseDuration(*argSentinelPollInterval) + if err != nil { + klog.ErrorS(err, "Could not parse sentinel-poll-interval", "value", *argSentinelPollInterval, "default", defaultSentinelPollInterval) + return defaultSentinelPollIntervalDuration + } + + return duration +} + func PipelineGatesInterval() time.Duration { duration, err := time.ParseDuration(*argPipelineGatesPollInterval) if err != nil { @@ -523,6 +537,7 @@ func AgentConfigurationDefaults() v1alpha1.AgentConfigurationSpec { clusterPingInterval := ClusterPingInterval().String() compatibilityUploadInterval := RuntimeServicesPingInterval().String() stackPollInterval := StackPollInterval().String() + sentinelPollInterval := SentinelPollInterval().String() pipelineGateInterval := PipelineGatesInterval().String() disableWebsocket := DisableWebsocket() @@ -531,6 +546,7 @@ func AgentConfigurationDefaults() v1alpha1.AgentConfigurationSpec { ClusterPingInterval: &clusterPingInterval, CompatibilityUploadInterval: &compatibilityUploadInterval, StackPollInterval: &stackPollInterval, + SentinelPollInterval: &sentinelPollInterval, PipelineGateInterval: &pipelineGateInterval, DisableWebsocket: &disableWebsocket, } diff --git a/go/deployment-operator/cmd/agent/console.go b/go/deployment-operator/cmd/agent/console.go index 9eb3919f6e..b86538c508 100644 --- a/go/deployment-operator/cmd/agent/console.go +++ b/go/deployment-operator/cmd/agent/console.go @@ -2,7 +2,6 @@ package main import ( "os" - "time" console "github.com/pluralsh/console/go/client" "github.com/pluralsh/console/go/polly/cache" @@ -48,11 +47,6 @@ func initConsoleManagerOrDie() *consolectrl.Manager { return mgr } -const ( - // Use custom (short) poll intervals for these reconcilers. - sentinelPollInterval = 30 * time.Second -) - func registerConsoleReconcilersOrDie( mgr *consolectrl.Manager, mapper meta.RESTMapper, @@ -125,7 +119,7 @@ func registerConsoleReconcilersOrDie( setupLog.Error(err, "unable to get operator namespace") os.Exit(1) } - r := sentinel.NewSentinelReconciler(namespaceCache, consoleClient, k8sClient, scheme, args.ControllerCacheTTL(), sentinelPollInterval, namespace, args.ConsoleUrl(), args.DeployToken()) + r := sentinel.NewSentinelReconciler(namespaceCache, consoleClient, k8sClient, scheme, args.ControllerCacheTTL(), args.SentinelPollInterval(), namespace, args.ConsoleUrl(), args.DeployToken()) return r, nil }) } diff --git a/go/deployment-operator/cmd/agent/main_test.go b/go/deployment-operator/cmd/agent/main_test.go index 43fd32cb46..53098723e7 100644 --- a/go/deployment-operator/cmd/agent/main_test.go +++ b/go/deployment-operator/cmd/agent/main_test.go @@ -27,6 +27,7 @@ func TestLoadAgentConfigurationUsesDefaultsWhenMissing(t *testing.T) { assertDuration(t, 2*time.Minute, common.GetConfigurationManager().GetClusterPingInterval()) assertDuration(t, 3*time.Minute, common.GetConfigurationManager().GetRuntimeServicesPingInterval()) assertDuration(t, 30*time.Second, common.GetConfigurationManager().GetStackPollInterval()) + assertDuration(t, 30*time.Second, common.GetConfigurationManager().GetSentinelPollInterval()) assertDuration(t, 0, common.GetConfigurationManager().GetPipelineGateInterval()) assert.False(t, common.GetConfigurationManager().IsWebsocketDisabled()) } @@ -46,6 +47,7 @@ func TestLoadAgentConfigurationOverlaysDefaultResource(t *testing.T) { DisableWebsocket: &disableWebsocket, PipelineGateInterval: ptr("0s"), ServicePollInterval: ptr("10m"), + SentinelPollInterval: ptr("5m"), StackPollInterval: ptr("0s"), }, }). @@ -57,6 +59,7 @@ func TestLoadAgentConfigurationOverlaysDefaultResource(t *testing.T) { assertDuration(t, 15*time.Minute, common.GetConfigurationManager().GetClusterPingInterval()) assertDuration(t, 30*time.Minute, common.GetConfigurationManager().GetRuntimeServicesPingInterval()) assertDuration(t, 0, common.GetConfigurationManager().GetStackPollInterval()) + assertDuration(t, 5*time.Minute, common.GetConfigurationManager().GetSentinelPollInterval()) assertDuration(t, 0, common.GetConfigurationManager().GetPipelineGateInterval()) assert.True(t, common.GetConfigurationManager().IsWebsocketDisabled()) @@ -65,6 +68,7 @@ func TestLoadAgentConfigurationOverlaysDefaultResource(t *testing.T) { assertDuration(t, 2*time.Minute, common.GetConfigurationManager().GetClusterPingInterval()) assertDuration(t, 3*time.Minute, common.GetConfigurationManager().GetRuntimeServicesPingInterval()) assertDuration(t, 30*time.Second, common.GetConfigurationManager().GetStackPollInterval()) + assertDuration(t, 30*time.Second, common.GetConfigurationManager().GetSentinelPollInterval()) assertDuration(t, 0, common.GetConfigurationManager().GetPipelineGateInterval()) assert.False(t, common.GetConfigurationManager().IsWebsocketDisabled()) } @@ -94,6 +98,7 @@ func agentConfigurationDefaults() v1alpha1.AgentConfigurationSpec { ClusterPingInterval: ptr("2m"), CompatibilityUploadInterval: ptr("3m"), StackPollInterval: ptr("30s"), + SentinelPollInterval: ptr("30s"), PipelineGateInterval: ptr("0s"), DisableWebsocket: &disableWebsocket, } diff --git a/go/deployment-operator/config/crd/bases/deployments.plural.sh_agentconfigurations.yaml b/go/deployment-operator/config/crd/bases/deployments.plural.sh_agentconfigurations.yaml index f225faabd3..00cf8c3981 100644 --- a/go/deployment-operator/config/crd/bases/deployments.plural.sh_agentconfigurations.yaml +++ b/go/deployment-operator/config/crd/bases/deployments.plural.sh_agentconfigurations.yaml @@ -89,6 +89,11 @@ spec: PipelineGateInterval specifies how frequently the agent checks pipeline gates. Set to "0s" to disable pipeline gate checks. type: string + sentinelPollInterval: + description: |- + SentinelPollInterval sets how often the agent polls for sentinel run jobs. + Set to "0s" to disable sentinel run job polling. + type: string servicePollInterval: description: |- ServicePollInterval defines how often the agent polls for services. diff --git a/go/deployment-operator/config/samples/agentConfiguration.yaml b/go/deployment-operator/config/samples/agentConfiguration.yaml index 0f099e23bd..02019e21f2 100644 --- a/go/deployment-operator/config/samples/agentConfiguration.yaml +++ b/go/deployment-operator/config/samples/agentConfiguration.yaml @@ -8,6 +8,7 @@ metadata: spec: clusterPingInterval: "10s" stackPollInterval: "20s" + sentinelPollInterval: "20s" pipelineGateInterval: "0s" vulnerabilityReportUploadInterval: "10m" maxConcurrentReconciles: 5 diff --git a/go/deployment-operator/docs/api.md b/go/deployment-operator/docs/api.md index 01e5bb824b..bae4194564 100644 --- a/go/deployment-operator/docs/api.md +++ b/go/deployment-operator/docs/api.md @@ -79,6 +79,7 @@ _Appears in:_ | `clusterPingInterval` _string_ | ClusterPingInterval specifies the interval at which the agent pings the cluster.
Set to "0s" to disable cluster pings. | | | | `compatibilityUploadInterval` _string_ | CompatibilityUploadInterval determines how frequently the agent uploads compatibility data.
Set to "0s" to disable compatibility uploads. | | | | `stackPollInterval` _string_ | StackPollInterval sets how often the agent polls for stack updates or changes.
Set to "0s" to disable stack polling. | | | +| `sentinelPollInterval` _string_ | SentinelPollInterval sets how often the agent polls for sentinel run jobs.
Set to "0s" to disable sentinel run job polling. | | | | `pipelineGateInterval` _string_ | PipelineGateInterval specifies how frequently the agent checks pipeline gates.
Set to "0s" to disable pipeline gate checks. | | | | `maxConcurrentReconciles` _integer_ | MaxConcurrentReconciles controls the maximum number of concurrent reconcile loops.
Higher values can increase throughput at the cost of resource usage. | | | | `vulnerabilityReportUploadInterval` _string_ | VulnerabilityReportUploadInterval sets how often vulnerability reports are uploaded.
Set to "0s" to disable vulnerability report uploads. | | | diff --git a/go/deployment-operator/pkg/common/config.go b/go/deployment-operator/pkg/common/config.go index 2769786f80..5d9ffae100 100644 --- a/go/deployment-operator/pkg/common/config.go +++ b/go/deployment-operator/pkg/common/config.go @@ -30,6 +30,7 @@ type ConfigurationManager struct { clusterPingInterval *time.Duration runtimeServicesPingInterval *time.Duration stackPollInterval *time.Duration + sentinelPollInterval *time.Duration compatibilityUploadInterval *time.Duration pipelineGateInterval *time.Duration maxConcurrentReconciles *int @@ -86,6 +87,12 @@ func (s *ConfigurationManager) setValueLocked(config v1alpha1.AgentConfiguration } s.stackPollInterval = interval + interval, err = setDuration(config.SentinelPollInterval) + if err != nil { + return err + } + s.sentinelPollInterval = interval + interval, err = setDuration(config.VulnerabilityReportUploadInterval) if err != nil { return err @@ -123,6 +130,9 @@ func mergeAgentConfigurationSpec(defaults, overrides v1alpha1.AgentConfiguration if overrides.StackPollInterval != nil { merged.StackPollInterval = overrides.StackPollInterval } + if overrides.SentinelPollInterval != nil { + merged.SentinelPollInterval = overrides.SentinelPollInterval + } if overrides.PipelineGateInterval != nil { merged.PipelineGateInterval = overrides.PipelineGateInterval } @@ -196,6 +206,12 @@ func (s *ConfigurationManager) GetStackPollInterval() *time.Duration { return s.stackPollInterval } +func (s *ConfigurationManager) GetSentinelPollInterval() *time.Duration { + s.mu.RLock() + defer s.mu.RUnlock() + return s.sentinelPollInterval +} + func (s *ConfigurationManager) GetMaxConcurrentReconciles() *int { s.mu.RLock() defer s.mu.RUnlock() diff --git a/go/deployment-operator/pkg/controller/sentinel/interval_test.go b/go/deployment-operator/pkg/controller/sentinel/interval_test.go new file mode 100644 index 0000000000..e1d095a43c --- /dev/null +++ b/go/deployment-operator/pkg/controller/sentinel/interval_test.go @@ -0,0 +1,81 @@ +package sentinel + +import ( + "testing" + "time" + + "github.com/pluralsh/console/go/deployment-operator/api/v1alpha1" + "github.com/pluralsh/console/go/deployment-operator/pkg/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSentinelPollIntervalCanBeDisabledByConfiguration(t *testing.T) { + t.Cleanup(resetAgentConfiguration) + resetAgentConfiguration() + + require.NoError(t, common.GetConfigurationManager().SetDefaults(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("30s"), + })) + require.NoError(t, common.GetConfigurationManager().SetValue(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("0s"), + })) + + reconciler := &SentinelReconciler{pollInterval: 30 * time.Second} + + assert.Equal(t, time.Duration(0), reconciler.GetPollInterval()()) +} + +func TestPollJitterWindowUsesConfiguredSentinelPollInterval(t *testing.T) { + t.Cleanup(resetAgentConfiguration) + resetAgentConfiguration() + + require.NoError(t, common.GetConfigurationManager().SetDefaults(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("30s"), + })) + require.NoError(t, common.GetConfigurationManager().SetValue(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("10m"), + })) + + reconciler := &SentinelReconciler{pollInterval: 30 * time.Second} + + assert.Equal(t, 5*time.Minute, reconciler.pollJitterWindow()) +} + +func TestPollJitterWindowUsesFallbackWhenSentinelPollingDisabled(t *testing.T) { + t.Cleanup(resetAgentConfiguration) + resetAgentConfiguration() + + require.NoError(t, common.GetConfigurationManager().SetDefaults(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("30s"), + })) + require.NoError(t, common.GetConfigurationManager().SetValue(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("0s"), + })) + + reconciler := &SentinelReconciler{pollInterval: 30 * time.Second} + + assert.Equal(t, 15*time.Second, reconciler.pollJitterWindow()) +} + +func TestControllerCacheTTLFuncUsesConfiguredSentinelPollInterval(t *testing.T) { + t.Cleanup(resetAgentConfiguration) + resetAgentConfiguration() + + require.NoError(t, common.GetConfigurationManager().SetDefaults(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("30s"), + })) + require.NoError(t, common.GetConfigurationManager().SetValue(v1alpha1.AgentConfigurationSpec{ + SentinelPollInterval: ptr("10m"), + })) + + assert.Equal(t, 20*time.Minute+time.Second, ControllerCacheTTLFunc(2*time.Minute, 30*time.Second)()) +} + +func resetAgentConfiguration() { + _ = common.GetConfigurationManager().SetDefaults(v1alpha1.AgentConfigurationSpec{}) +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/go/deployment-operator/pkg/controller/sentinel/reconciler.go b/go/deployment-operator/pkg/controller/sentinel/reconciler.go index 611a2522c2..cc74cee9f8 100644 --- a/go/deployment-operator/pkg/controller/sentinel/reconciler.go +++ b/go/deployment-operator/pkg/controller/sentinel/reconciler.go @@ -38,7 +38,7 @@ type SentinelReconciler struct { k8sClient ctrlclient.Client scheme *runtime.Scheme sentinelQueue workqueue.TypedRateLimitingInterface[string] - sentinelCache *cache.Cache[console.SentinelRunJobFragment] + sentinelCache cache.Store[console.SentinelRunJobFragment] namespace string consoleURL string deployToken string @@ -52,7 +52,7 @@ func NewSentinelReconciler(namespaceCache streamline.NamespaceCache, consoleClie k8sClient: k8sClient, scheme: scheme, sentinelQueue: workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[string]()), - sentinelCache: cache.NewCache[console.SentinelRunJobFragment](refresh, func(id string) (*console.SentinelRunJobFragment, error) { + sentinelCache: cache.NewDynamicCache[console.SentinelRunJobFragment](ControllerCacheTTLFunc(refresh, pollInterval), func(id string) (*console.SentinelRunJobFragment, error) { return consoleClient.GetSentinelRunJob(id) }), consoleURL: consoleURL, @@ -83,7 +83,7 @@ func (r *SentinelReconciler) Shutdown() { func (r *SentinelReconciler) GetPollInterval() func() time.Duration { return func() time.Duration { - return r.pollInterval + return EffectivePollInterval(r.pollInterval) } } @@ -135,7 +135,7 @@ func (r *SentinelReconciler) Poll(ctx context.Context) error { for _, run := range runs { logger.V(1).Info("sending update for", "sentinel run job", run.Node.ID) r.sentinelCache.Add(run.Node.ID, run.Node) - r.sentinelQueue.AddAfter(run.Node.ID, utils.Jitter(r.GetPollInterval()())) + r.sentinelQueue.AddAfter(run.Node.ID, utils.Jitter(r.pollJitterWindow())) } } @@ -210,3 +210,24 @@ func (r *SentinelReconciler) GetRunResourceNamespace(jobSpec *batchv1.JobSpec) ( return } + +func (r *SentinelReconciler) pollJitterWindow() time.Duration { + interval := r.GetPollInterval()() + if interval <= 0 { + return 15 * time.Second + } + return interval / 2 +} + +func EffectivePollInterval(defaultInterval time.Duration) time.Duration { + if sentinelPollInterval := pkgcommon.GetConfigurationManager().GetSentinelPollInterval(); sentinelPollInterval != nil { + return *sentinelPollInterval + } + return defaultInterval +} + +func ControllerCacheTTLFunc(baseTTL, defaultPollInterval time.Duration) func() time.Duration { + return func() time.Duration { + return common.ControllerCacheTTL(baseTTL, EffectivePollInterval(defaultPollInterval)) + } +} diff --git a/go/deployment-operator/pkg/controller/sentinel/socket_publisher.go b/go/deployment-operator/pkg/controller/sentinel/socket_publisher.go index f8208c1964..35e71bcf01 100644 --- a/go/deployment-operator/pkg/controller/sentinel/socket_publisher.go +++ b/go/deployment-operator/pkg/controller/sentinel/socket_publisher.go @@ -8,7 +8,7 @@ import ( type socketPublisher struct { sentinelRunQueue workqueue.TypedRateLimitingInterface[string] - sentinelRunCache *cache.Cache[console.SentinelRunJobFragment] + sentinelRunCache cache.Store[console.SentinelRunJobFragment] } func (sp *socketPublisher) Publish(id string, _ bool) { From 0bfb1674b79f5ba2f7027922b329311fdbafa47c Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Thu, 23 Jul 2026 14:13:44 -0400 Subject: [PATCH 2/8] rm bound roles --- .../settings/usermanagement/groups/GroupsList.tsx | 4 +--- assets/src/components/settings/usermanagement/misc.tsx | 7 ------- .../settings/usermanagement/personas/PersonaActions.tsx | 3 +-- .../src/components/settings/usermanagement/users/User.tsx | 3 +-- assets/src/generated/graphql.ts | 6 +----- assets/src/generated/persisted-queries/client.json | 4 ++-- assets/src/graph/login.graphql | 3 --- 7 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 assets/src/components/settings/usermanagement/misc.tsx diff --git a/assets/src/components/settings/usermanagement/groups/GroupsList.tsx b/assets/src/components/settings/usermanagement/groups/GroupsList.tsx index 815b74411b..6fc75c3d19 100644 --- a/assets/src/components/settings/usermanagement/groups/GroupsList.tsx +++ b/assets/src/components/settings/usermanagement/groups/GroupsList.tsx @@ -8,8 +8,6 @@ import { GqlError } from 'components/utils/Alert' import { LoginContext } from 'components/contexts' -import { Permissions, hasRbac } from '../misc' - import { useThrottle } from 'components/hooks/useThrottle' import { mapExistingNodes } from 'utils/graphql' import { ListWrapperSC } from '../users/UsersList' @@ -39,7 +37,7 @@ export function GroupsList({ const groups = useMemo(() => mapExistingNodes(data?.groups), [data?.groups]) const meta: GroupsListMeta = { - editable: !!me?.roles?.admin || hasRbac(me, Permissions.USERS), + editable: !!me?.roles?.admin, setGroupEdit, } diff --git a/assets/src/components/settings/usermanagement/misc.tsx b/assets/src/components/settings/usermanagement/misc.tsx deleted file mode 100644 index ffad696ac7..0000000000 --- a/assets/src/components/settings/usermanagement/misc.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const hasRbac = (me, role) => - (me?.boundRoles || []).some(({ permissions }) => permissions.includes(role)) - -export const Permissions = { - INSTALL: 'INSTALL', - USERS: 'USERS', -} diff --git a/assets/src/components/settings/usermanagement/personas/PersonaActions.tsx b/assets/src/components/settings/usermanagement/personas/PersonaActions.tsx index 6d3d88d8f0..d26ce6feb4 100644 --- a/assets/src/components/settings/usermanagement/personas/PersonaActions.tsx +++ b/assets/src/components/settings/usermanagement/personas/PersonaActions.tsx @@ -17,7 +17,6 @@ import { import { useContext, useState } from 'react' import { removeConnection, updateCache } from '../../../../utils/graphql' -import { hasRbac, Permissions } from '../misc' import { EditPersonaAttributes } from './PersonaAttributesEdit' import { EditPersonaBindings } from './PersonaBindingsEdit' @@ -25,7 +24,7 @@ import PersonaView from './PersonaView' export default function PersonaActions({ persona }: { persona: PersonaT }) { const { me } = useContext(LoginContext) - const editable = !!me.roles?.admin || hasRbac(me, Permissions.USERS) + const editable = !!me.roles?.admin const [dialogKey, setDialogKey] = useState< 'confirmDelete' | 'editAttrs' | 'editBindings' | 'viewPersona' | '' diff --git a/assets/src/components/settings/usermanagement/users/User.tsx b/assets/src/components/settings/usermanagement/users/User.tsx index 4e1fd4b418..82c08ef389 100644 --- a/assets/src/components/settings/usermanagement/users/User.tsx +++ b/assets/src/components/settings/usermanagement/users/User.tsx @@ -6,7 +6,6 @@ import { useTheme } from 'styled-components' import { Confirm } from 'components/utils/Confirm' import UserInfo from '../../../utils/UserInfo' -import { Permissions, hasRbac } from '../misc' import { useUpdateUserMutation } from '../../../../generated/graphql.ts' @@ -16,7 +15,7 @@ export function User({ user }: any) { const [mutation, { loading, error }] = useUpdateUserMutation({ onCompleted: () => setConfirm(false), }) - const editable = !!me?.roles?.admin || hasRbac(me as any, Permissions.USERS) + const editable = !!me?.roles?.admin const isAdmin = !!user.roles?.admin const setAdmin = useCallback( () => diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 8b549c0368..e03ba4a3a3 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -20081,7 +20081,7 @@ export type MeGroupsQuery = { __typename?: 'RootQueryType', me?: { __typename?: export type MeQueryVariables = Exact<{ [key: string]: never; }>; -export type MeQuery = { __typename?: 'RootQueryType', me?: { __typename?: 'User', unreadNotifications?: number | null, id: string, pluralId?: string | null, name: string, email: string, profile?: string | null, backgroundColor?: string | null, readTimestamp?: string | null, homepage?: Homepage | null, boundRoles?: Array<{ __typename?: 'Role', id: string, name: string, description?: string | null, repositories?: Array | null, permissions?: Array | null, roleBindings?: Array<{ __typename?: 'RoleBinding', id: string, user?: { __typename?: 'User', id: string, pluralId?: string | null, name: string, email: string, profile?: string | null, backgroundColor?: string | null, readTimestamp?: string | null, homepage?: Homepage | null, emailSettings?: { __typename?: 'EmailSettings', digest?: boolean | null } | null, roles?: { __typename?: 'UserRoles', admin?: boolean | null } | null, personas?: Array<{ __typename?: 'Persona', id: string, name: string, description?: string | null, bindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'PersonaConfiguration', all?: boolean | null, deployments?: { __typename?: 'PersonaDeployment', addOns?: boolean | null, clusters?: boolean | null, pipelines?: boolean | null, providers?: boolean | null, repositories?: boolean | null, services?: boolean | null } | null, home?: { __typename?: 'PersonaHome', manager?: boolean | null, security?: boolean | null } | null, flows?: { __typename?: 'PersonaFlows', permissions?: boolean | null, startWorkbenchJob?: boolean | null, pipelines?: boolean | null, previews?: boolean | null, workbenches?: boolean | null } | null, sidebar?: { __typename?: 'PersonaSidebar', audits?: boolean | null, flows?: boolean | null, kubernetes?: boolean | null, pullRequests?: boolean | null, settings?: boolean | null, backups?: boolean | null, stacks?: boolean | null, workbenches?: boolean | null } | null, services?: { __typename?: 'PersonaServices', configuration?: boolean | null, secrets?: boolean | null } | null, ai?: { __typename?: 'PersonaAi', pr?: boolean | null } | null } | null } | null> | null } | null, group?: { __typename?: 'Group', id: string, name: string, description?: string | null, global?: boolean | null, insertedAt?: string | null, updatedAt?: string | null } | null } | null> | null } | null> | null, emailSettings?: { __typename?: 'EmailSettings', digest?: boolean | null } | null, roles?: { __typename?: 'UserRoles', admin?: boolean | null } | null, personas?: Array<{ __typename?: 'Persona', id: string, name: string, description?: string | null, bindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'PersonaConfiguration', all?: boolean | null, deployments?: { __typename?: 'PersonaDeployment', addOns?: boolean | null, clusters?: boolean | null, pipelines?: boolean | null, providers?: boolean | null, repositories?: boolean | null, services?: boolean | null } | null, home?: { __typename?: 'PersonaHome', manager?: boolean | null, security?: boolean | null } | null, flows?: { __typename?: 'PersonaFlows', permissions?: boolean | null, startWorkbenchJob?: boolean | null, pipelines?: boolean | null, previews?: boolean | null, workbenches?: boolean | null } | null, sidebar?: { __typename?: 'PersonaSidebar', audits?: boolean | null, flows?: boolean | null, kubernetes?: boolean | null, pullRequests?: boolean | null, settings?: boolean | null, backups?: boolean | null, stacks?: boolean | null, workbenches?: boolean | null } | null, services?: { __typename?: 'PersonaServices', configuration?: boolean | null, secrets?: boolean | null } | null, ai?: { __typename?: 'PersonaAi', pr?: boolean | null } | null } | null } | null> | null } | null, configuration?: { __typename?: 'ConsoleConfiguration', gitCommit?: string | null, isDemoProject?: boolean | null, isSandbox?: boolean | null, pluralLogin?: boolean | null, byok?: boolean | null, externalOidc?: boolean | null, cloud?: boolean | null, installed?: boolean | null, consoleVersion?: string | null, sentryEnabled?: boolean | null, qoveKey?: string | null, manifest?: { __typename?: 'PluralManifest', cluster?: string | null, bucketPrefix?: string | null, network?: { __typename?: 'ManifestNetwork', pluralDns?: boolean | null, subdomain?: string | null } | null } | null, gitStatus?: { __typename?: 'GitStatus', cloned?: boolean | null, output?: string | null } | null, features?: { __typename?: 'AvailableFeatures', audits?: boolean | null, cd?: boolean | null, databaseManagement?: boolean | null, userManagement?: boolean | null } | null, details?: { __typename?: 'ConsoleConfigurationDetails', assumeRoleArn?: string | null, egressIps?: Array | null } | null } | null }; +export type MeQuery = { __typename?: 'RootQueryType', me?: { __typename?: 'User', unreadNotifications?: number | null, id: string, pluralId?: string | null, name: string, email: string, profile?: string | null, backgroundColor?: string | null, readTimestamp?: string | null, homepage?: Homepage | null, emailSettings?: { __typename?: 'EmailSettings', digest?: boolean | null } | null, roles?: { __typename?: 'UserRoles', admin?: boolean | null } | null, personas?: Array<{ __typename?: 'Persona', id: string, name: string, description?: string | null, bindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'PersonaConfiguration', all?: boolean | null, deployments?: { __typename?: 'PersonaDeployment', addOns?: boolean | null, clusters?: boolean | null, pipelines?: boolean | null, providers?: boolean | null, repositories?: boolean | null, services?: boolean | null } | null, home?: { __typename?: 'PersonaHome', manager?: boolean | null, security?: boolean | null } | null, flows?: { __typename?: 'PersonaFlows', permissions?: boolean | null, startWorkbenchJob?: boolean | null, pipelines?: boolean | null, previews?: boolean | null, workbenches?: boolean | null } | null, sidebar?: { __typename?: 'PersonaSidebar', audits?: boolean | null, flows?: boolean | null, kubernetes?: boolean | null, pullRequests?: boolean | null, settings?: boolean | null, backups?: boolean | null, stacks?: boolean | null, workbenches?: boolean | null } | null, services?: { __typename?: 'PersonaServices', configuration?: boolean | null, secrets?: boolean | null } | null, ai?: { __typename?: 'PersonaAi', pr?: boolean | null } | null } | null } | null> | null } | null, configuration?: { __typename?: 'ConsoleConfiguration', gitCommit?: string | null, isDemoProject?: boolean | null, isSandbox?: boolean | null, pluralLogin?: boolean | null, byok?: boolean | null, externalOidc?: boolean | null, cloud?: boolean | null, installed?: boolean | null, consoleVersion?: string | null, sentryEnabled?: boolean | null, qoveKey?: string | null, manifest?: { __typename?: 'PluralManifest', cluster?: string | null, bucketPrefix?: string | null, network?: { __typename?: 'ManifestNetwork', pluralDns?: boolean | null, subdomain?: string | null } | null } | null, gitStatus?: { __typename?: 'GitStatus', cloned?: boolean | null, output?: string | null } | null, features?: { __typename?: 'AvailableFeatures', audits?: boolean | null, cd?: boolean | null, databaseManagement?: boolean | null, userManagement?: boolean | null } | null, details?: { __typename?: 'ConsoleConfigurationDetails', assumeRoleArn?: string | null, egressIps?: Array | null } | null } | null }; export type LoginInfoQueryVariables = Exact<{ redirect?: InputMaybe; @@ -38827,9 +38827,6 @@ export const MeDocument = gql` query Me { me { ...User - boundRoles { - ...Role - } unreadNotifications } configuration { @@ -38861,7 +38858,6 @@ export const MeDocument = gql` } } ${UserFragmentDoc} -${RoleFragmentDoc} ${ManifestFragmentDoc} ${AvailableFeaturesFragmentDoc}`; diff --git a/assets/src/generated/persisted-queries/client.json b/assets/src/generated/persisted-queries/client.json index cd46307727..f1061bd7ed 100644 --- a/assets/src/generated/persisted-queries/client.json +++ b/assets/src/generated/persisted-queries/client.json @@ -1307,10 +1307,10 @@ "name": "MeGroups", "body": "query MeGroups {\n me {\n id\n groups {\n ...Group\n __typename\n }\n __typename\n }\n}\n\nfragment Group on Group {\n id\n name\n description\n global\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:8e38be23545658234743fb9b35968269078953f708929352ebc1e2146624adf2": { + "sha256:4a7941b2da4b88aeac2e1055144bcf23bbaf573476991940ff5f25c07ba1728b": { "type": "query", "name": "Me", - "body": "query Me {\n me {\n ...User\n boundRoles {\n ...Role\n __typename\n }\n unreadNotifications\n __typename\n }\n configuration {\n gitCommit\n isDemoProject\n isSandbox\n pluralLogin\n byok\n externalOidc\n cloud\n installed\n consoleVersion\n sentryEnabled\n manifest {\n ...Manifest\n __typename\n }\n gitStatus {\n cloned\n output\n __typename\n }\n features {\n ...AvailableFeatures\n __typename\n }\n details {\n assumeRoleArn\n egressIps\n __typename\n }\n qoveKey\n __typename\n }\n}\n\nfragment User on User {\n id\n pluralId\n name\n email\n emailSettings {\n digest\n __typename\n }\n profile\n backgroundColor\n readTimestamp\n roles {\n admin\n __typename\n }\n personas {\n ...Persona\n __typename\n }\n homepage\n __typename\n}\n\nfragment Persona on Persona {\n id\n name\n description\n bindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PersonaConfiguration\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PersonaConfiguration on PersonaConfiguration {\n all\n deployments {\n addOns\n clusters\n pipelines\n providers\n repositories\n services\n __typename\n }\n home {\n manager\n security\n __typename\n }\n flows {\n permissions\n startWorkbenchJob\n pipelines\n previews\n workbenches\n __typename\n }\n sidebar {\n audits\n flows\n kubernetes\n pullRequests\n settings\n backups\n stacks\n workbenches\n __typename\n }\n services {\n configuration\n secrets\n __typename\n }\n ai {\n pr\n __typename\n }\n __typename\n}\n\nfragment Role on Role {\n id\n name\n description\n repositories\n permissions\n roleBindings {\n ...RoleBinding\n __typename\n }\n __typename\n}\n\nfragment RoleBinding on RoleBinding {\n id\n user {\n ...User\n __typename\n }\n group {\n ...Group\n __typename\n }\n __typename\n}\n\nfragment Group on Group {\n id\n name\n description\n global\n insertedAt\n updatedAt\n __typename\n}\n\nfragment Manifest on PluralManifest {\n network {\n pluralDns\n subdomain\n __typename\n }\n cluster\n bucketPrefix\n __typename\n}\n\nfragment AvailableFeatures on AvailableFeatures {\n audits\n cd\n databaseManagement\n userManagement\n __typename\n}" + "body": "query Me {\n me {\n ...User\n unreadNotifications\n __typename\n }\n configuration {\n gitCommit\n isDemoProject\n isSandbox\n pluralLogin\n byok\n externalOidc\n cloud\n installed\n consoleVersion\n sentryEnabled\n manifest {\n ...Manifest\n __typename\n }\n gitStatus {\n cloned\n output\n __typename\n }\n features {\n ...AvailableFeatures\n __typename\n }\n details {\n assumeRoleArn\n egressIps\n __typename\n }\n qoveKey\n __typename\n }\n}\n\nfragment User on User {\n id\n pluralId\n name\n email\n emailSettings {\n digest\n __typename\n }\n profile\n backgroundColor\n readTimestamp\n roles {\n admin\n __typename\n }\n personas {\n ...Persona\n __typename\n }\n homepage\n __typename\n}\n\nfragment Persona on Persona {\n id\n name\n description\n bindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PersonaConfiguration\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PersonaConfiguration on PersonaConfiguration {\n all\n deployments {\n addOns\n clusters\n pipelines\n providers\n repositories\n services\n __typename\n }\n home {\n manager\n security\n __typename\n }\n flows {\n permissions\n startWorkbenchJob\n pipelines\n previews\n workbenches\n __typename\n }\n sidebar {\n audits\n flows\n kubernetes\n pullRequests\n settings\n backups\n stacks\n workbenches\n __typename\n }\n services {\n configuration\n secrets\n __typename\n }\n ai {\n pr\n __typename\n }\n __typename\n}\n\nfragment Manifest on PluralManifest {\n network {\n pluralDns\n subdomain\n __typename\n }\n cluster\n bucketPrefix\n __typename\n}\n\nfragment AvailableFeatures on AvailableFeatures {\n audits\n cd\n databaseManagement\n userManagement\n __typename\n}" }, "sha256:9ed81dd4c1ab018de19146e0696f8db0f57ff0c6c45c7cb73669dbad2a1f2e27": { "type": "query", diff --git a/assets/src/graph/login.graphql b/assets/src/graph/login.graphql index 8dd483c93c..68a1b6c007 100644 --- a/assets/src/graph/login.graphql +++ b/assets/src/graph/login.graphql @@ -44,9 +44,6 @@ query MeGroups { query Me { me { ...User - boundRoles { - ...Role - } unreadNotifications } configuration { From 5f34a3891f1eda1f2f1a3473a53cc8e17efb7f01 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Thu, 23 Jul 2026 17:34:35 -0400 Subject: [PATCH 3/8] add verify state on engine --- .../workbench/job/WorkbenchJobActivity.tsx | 7 ++++-- .../job/workbenchJobActivityCollapse.ts | 3 ++- assets/src/generated/graphql.ts | 1 + assets/src/types/styled.d.ts | 1 - go/client/models_gen.go | 4 +++- lib/console/ai/workbench/engine.ex | 23 +++++++++++++------ lib/console/deployments/workbenches.ex | 4 ++-- lib/console/schema/workbench_job_activity.ex | 4 +++- schema/schema.graphql | 1 + test/console/deployments/workbenches_test.exs | 6 ++--- .../deployments/workbench_mutations_test.exs | 4 ++-- 11 files changed, 38 insertions(+), 20 deletions(-) diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx index f8d5978694..7278a23770 100644 --- a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx @@ -75,6 +75,7 @@ export function WorkbenchJobActivity({ const { spacing } = useTheme() const { id, status, type, prompt, agentRun, result } = activity const isRunning = isJobRunning(status) + const isRejected = status === WorkbenchJobActivityStatus.Rejected if (type === WorkbenchJobActivityType.Conclusion) return ( @@ -169,7 +170,7 @@ export function WorkbenchJobActivity({ tooltip="Go to agent run details" /> )} - {status == WorkbenchJobActivityStatus.Failed && ( + {(status === WorkbenchJobActivityStatus.Failed || isRejected) && ( {result?.jobUpdate && } - {isFailed && ( + {(isFailed || isRejected) && ( ) => status === WorkbenchJobActivityStatus.Successful || - status === WorkbenchJobActivityStatus.Failed + status === WorkbenchJobActivityStatus.Failed || + status === WorkbenchJobActivityStatus.Rejected const lastActivityId = ( activities: WorkbenchJobActivityFragment[] diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index e03ba4a3a3..1d9d4db4d8 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -16141,6 +16141,7 @@ export enum WorkbenchJobActivityStatus { Failed = 'FAILED', NeedsApproval = 'NEEDS_APPROVAL', Pending = 'PENDING', + Rejected = 'REJECTED', Running = 'RUNNING', Successful = 'SUCCESSFUL' } diff --git a/assets/src/types/styled.d.ts b/assets/src/types/styled.d.ts index 918063d604..6c32238346 100644 --- a/assets/src/types/styled.d.ts +++ b/assets/src/types/styled.d.ts @@ -20,7 +20,6 @@ type StyledTheme = typeof styledTheme // extend original module declarations declare module 'styled-components' { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface DefaultTheme extends StyledTheme {} export declare function useTheme(): DefaultTheme } diff --git a/go/client/models_gen.go b/go/client/models_gen.go index 8ff7756609..64fcbe1cbb 100644 --- a/go/client/models_gen.go +++ b/go/client/models_gen.go @@ -17609,6 +17609,7 @@ const ( WorkbenchJobActivityStatusFailed WorkbenchJobActivityStatus = "FAILED" WorkbenchJobActivityStatusCancelled WorkbenchJobActivityStatus = "CANCELLED" WorkbenchJobActivityStatusNeedsApproval WorkbenchJobActivityStatus = "NEEDS_APPROVAL" + WorkbenchJobActivityStatusRejected WorkbenchJobActivityStatus = "REJECTED" ) var AllWorkbenchJobActivityStatus = []WorkbenchJobActivityStatus{ @@ -17618,11 +17619,12 @@ var AllWorkbenchJobActivityStatus = []WorkbenchJobActivityStatus{ WorkbenchJobActivityStatusFailed, WorkbenchJobActivityStatusCancelled, WorkbenchJobActivityStatusNeedsApproval, + WorkbenchJobActivityStatusRejected, } func (e WorkbenchJobActivityStatus) IsValid() bool { switch e { - case WorkbenchJobActivityStatusPending, WorkbenchJobActivityStatusRunning, WorkbenchJobActivityStatusSuccessful, WorkbenchJobActivityStatusFailed, WorkbenchJobActivityStatusCancelled, WorkbenchJobActivityStatusNeedsApproval: + case WorkbenchJobActivityStatusPending, WorkbenchJobActivityStatusRunning, WorkbenchJobActivityStatusSuccessful, WorkbenchJobActivityStatusFailed, WorkbenchJobActivityStatusCancelled, WorkbenchJobActivityStatusNeedsApproval, WorkbenchJobActivityStatusRejected: return true } return false diff --git a/lib/console/ai/workbench/engine.ex b/lib/console/ai/workbench/engine.ex index 45383da8fa..b3a5a1bd3c 100644 --- a/lib/console/ai/workbench/engine.ex +++ b/lib/console/ai/workbench/engine.ex @@ -12,6 +12,7 @@ defmodule Console.AI.Workbench.Engine do import Console.AI.Workbench.Subagents.Base, only: [drop_empty: 1, log_error: 2] import Console.AI.Agents.Base, only: [publish_absinthe: 2] import Console.AI.Workbench.Environment, only: [engine_opts: 1] + import Console.Schema.WorkbenchJobActivity, only: [is_action: 1] alias Console.Repo alias Console.AI.Chat.MemoryEngine alias Console.Deployments.Workbenches @@ -45,7 +46,7 @@ defmodule Console.AI.Workbench.Engine do require EEx require Logger - defstruct [:job, :user, :environment, activities: [], messages: [], iterations: 0, max: 200] + defstruct [:job, :user, :environment, activities: [], messages: [], iterations: 0, max: 200, verifiable: false] defmodule Acc do defstruct [messages: [], activities: []] @@ -69,12 +70,11 @@ defmodule Console.AI.Workbench.Engine do end def run(%__MODULE__{job: job} = engine) do - # stream_callbacks(job) - # with {:ok, job} <- SA.Plan.run(job, engine.environment) do - # loop(%{engine | activities: list_activities(job)}) - # end Console.AI.Provider.external_errors() - loop(%{engine | activities: list_activities(job)}) + + list_activities(job) + |> then(& verifiable(%{engine | activities: &1})) + |> loop() end defp loop(%__MODULE__{iterations: iter, max: max, job: job}) @@ -355,7 +355,16 @@ defmodule Console.AI.Workbench.Engine do end defp backfill_chat(tools, _), do: tools - @preloads [:result, :flow, chatbot_message: [:chat_connection], user: [:groups], workbench: [:workbench_skills, :repository, :agent_runtime, [tools: [:mcp_server, :cloud_connection, :scm_connection]]]] + @preloads [:result, :flow, :pull_requests, chatbot_message: [:chat_connection], user: [:groups], workbench: [:workbench_skills, :repository, :agent_runtime, [tools: [:mcp_server, :cloud_connection, :scm_connection]]]] + + defp verifiable(%__MODULE__{activities: activities, job: %WorkbenchJob{pull_requests: prs}} = engine) do + verifiable = + (is_list(prs) && Enum.any?(prs, & &1.status == :merged)) || + (is_list(activities) && Enum.any?(activities, & &1.status == :successful && is_action(&1.type))) + + %{engine | verifiable: verifiable} + end + defp verifiable(engine), do: engine defp preload_job(%WorkbenchJob{type: :skill} = job), do: Repo.preload(job, @preloads ++ [referenced_job: [:result, workbench: [:workbench_skills, :repository], activities: :thoughts]]) diff --git a/lib/console/deployments/workbenches.ex b/lib/console/deployments/workbenches.ex index ca31890bbf..715ec299b6 100644 --- a/lib/console/deployments/workbenches.ex +++ b/lib/console/deployments/workbenches.ex @@ -852,7 +852,7 @@ defmodule Console.Deployments.Workbenches do end @doc """ - Rejects a job activity by setting status to successful and setting the output to the reason. + Rejects a job activity by marking it rejected and setting the output to the reason. """ @spec reject_job_activity(binary | nil, binary, User.t()) :: activity_resp def reject_job_activity(reason \\ nil, activity_id, %User{} = user) when is_binary(activity_id) do @@ -860,7 +860,7 @@ defmodule Console.Deployments.Workbenches do |> allow(user, :approve) |> when_ok(fn activity -> WorkbenchJobActivity.changeset(activity, %{ - status: :successful, + status: :rejected, result: %{output: reason || "Execution rejected by user"} }) end) diff --git a/lib/console/schema/workbench_job_activity.ex b/lib/console/schema/workbench_job_activity.ex index e02d772fb7..abce4b940f 100644 --- a/lib/console/schema/workbench_job_activity.ex +++ b/lib/console/schema/workbench_job_activity.ex @@ -2,7 +2,7 @@ defmodule Console.Schema.WorkbenchJobActivity do use Console.Schema.Base alias Console.Schema.{User, WorkbenchJob, WorkbenchJobThought, AgentRun, WorkbenchJobResult, WorkbenchJobActivityAgentRun} - defenum Status, pending: 0, running: 1, successful: 2, failed: 3, cancelled: 4, needs_approval: 5 + defenum Status, pending: 0, running: 1, successful: 2, failed: 3, cancelled: 4, needs_approval: 5, rejected: 6 defenum Type, coding: 0, observability: 1, @@ -21,6 +21,8 @@ defmodule Console.Schema.WorkbenchJobActivity do function: 14, kubernetes: 15 + defguard is_action(type) when type in [:function, :kubernetes] + schema "workbench_job_activities" do field :status, Status, default: :pending field :type, Type diff --git a/schema/schema.graphql b/schema/schema.graphql index 93f5a0461f..940e3d3934 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -2651,6 +2651,7 @@ enum WorkbenchJobActivityStatus { FAILED CANCELLED NEEDS_APPROVAL + REJECTED } enum WorkbenchJobActivityType { diff --git a/test/console/deployments/workbenches_test.exs b/test/console/deployments/workbenches_test.exs index 2487abeb0c..10729ed7e0 100644 --- a/test/console/deployments/workbenches_test.exs +++ b/test/console/deployments/workbenches_test.exs @@ -1405,12 +1405,12 @@ defmodule Console.Deployments.WorkbenchesTest do {:ok, updated} = Workbenches.reject_job_activity("too risky", activity.id, user) assert updated.id == activity.id - assert updated.status == :successful + assert updated.status == :rejected assert updated.result.output == "too risky" assert_receive {:event, %PubSub.WorkbenchJobActivityUpdated{item: ^updated}} reloaded = refetch(activity) - assert reloaded.status == :successful + assert reloaded.status == :rejected assert reloaded.result.output == "too risky" end @@ -1431,7 +1431,7 @@ defmodule Console.Deployments.WorkbenchesTest do {:ok, updated} = Workbenches.reject_job_activity(activity.id, user) assert updated.id == activity.id - assert updated.status == :successful + assert updated.status == :rejected assert updated.result.output == "Execution rejected by user" assert_receive {:event, %PubSub.WorkbenchJobActivityUpdated{item: ^updated}} end diff --git a/test/console/graphql/mutations/deployments/workbench_mutations_test.exs b/test/console/graphql/mutations/deployments/workbench_mutations_test.exs index 793cf21eff..5c97dd9c6c 100644 --- a/test/console/graphql/mutations/deployments/workbench_mutations_test.exs +++ b/test/console/graphql/mutations/deployments/workbench_mutations_test.exs @@ -712,11 +712,11 @@ defmodule Console.GraphQl.Deployments.WorkbenchMutationsTest do """, %{"id" => activity.id, "reason" => "not approved"}, %{current_user: user}) assert updated["id"] == activity.id - assert updated["status"] == "SUCCESSFUL" + assert updated["status"] == "REJECTED" assert updated["result"]["output"] == "not approved" reloaded = refetch(activity) - assert reloaded.status == :successful + assert reloaded.status == :rejected assert reloaded.result.output == "not approved" end end From 19cd4cf6a8e40baeb09de070171353c2ce294686 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Fri, 24 Jul 2026 11:33:15 -0400 Subject: [PATCH 4/8] implement verify agent --- .../workbenches/tools/WorkbenchToolForm.tsx | 62 +++++++++++++++-- .../workbenches/tools/workbenchToolsUtils.tsx | 11 ++- assets/src/generated/graphql.ts | 8 ++- go/client/models_gen.go | 12 +++- .../ai/tools/workbench/canvas/metrics.ex | 4 +- lib/console/ai/tools/workbench/subagent.ex | 2 +- lib/console/ai/tools/workbench/subagents.ex | 1 + lib/console/ai/workbench/engine.ex | 8 ++- lib/console/ai/workbench/environment.ex | 9 ++- lib/console/ai/workbench/subagents/coding.ex | 7 +- .../ai/workbench/subagents/infrastructure.ex | 14 ++-- .../ai/workbench/subagents/observability.ex | 22 +++--- lib/console/ai/workbench/subagents/verify.ex | 52 ++++++++++++++ lib/console/schema/workbench_job_activity.ex | 3 +- lib/console/schema/workbench_tool.ex | 10 ++- priv/prompts/workbench/verify.md.eex | 32 +++++++++ schema/schema.graphql | 4 ++ .../ai/workbench/subagents/verify_test.exs | 67 +++++++++++++++++++ 18 files changed, 292 insertions(+), 36 deletions(-) create mode 100644 lib/console/ai/workbench/subagents/verify.ex create mode 100644 priv/prompts/workbench/verify.md.eex create mode 100644 test/console/ai/workbench/subagents/verify_test.exs diff --git a/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx b/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx index 5cc8418a32..0433907f25 100644 --- a/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx +++ b/assets/src/components/workbenches/tools/WorkbenchToolForm.tsx @@ -1,16 +1,21 @@ import { Button, Checkbox, + Chip, Divider, Flex, FormField, Input2, + ListBoxItem, + Select, + SelectButton, } from '@pluralsh/design-system' import { useUpdateState } from 'components/hooks/useUpdateState' import { FormBindings } from 'components/utils/bindings' import { PolicyBindingFragment, Provider, + WorkbenchToolCategory, WorkbenchToolAttributes, WorkbenchToolConfigurationAttributes, WorkbenchToolFragment, @@ -165,9 +170,13 @@ export function WorkbenchToolForm({ const [deleteOpen, setDeleteOpen] = useState(false) const [currentStep, setCurrentStep] = useState('configuration') + const defaultCategories = + type === WorkbenchToolType.Mcp + ? [WorkbenchToolCategory.Integration] + : TOOL_TYPE_TO_CATEGORIES[type] const { state, update, hasUpdates } = useUpdateState({ name: tool?.name ?? '', - categories: tool?.categories ?? TOOL_TYPE_TO_CATEGORIES[type], + categories: tool?.categories ?? defaultCategories, configuration: sanitizeInitialConfiguration(tool), cloudConnectionId: tool?.cloudConnection?.id, mcpServerId: tool?.mcpServer?.id, @@ -176,6 +185,7 @@ export function WorkbenchToolForm({ writeBindings: tool?.writeBindings?.filter(isNonNullable) ?? [], }) const categories = TOOL_TYPE_TO_CATEGORIES[type] ?? [] + const selectedCategories = (state.categories ?? []).filter(isNonNullable) const hasRegisteredScm = Boolean(state.scmConnectionId) const scmType = scmTypeForWorkbenchTool(type) const configurationStepComplete = @@ -270,10 +280,48 @@ export function WorkbenchToolForm({ onChange={(id) => update({ cloudConnectionId: id })} /> ) : type === WorkbenchToolType.Mcp ? ( - update({ mcpServerId: id })} - /> + <> + update({ mcpServerId: id })} + /> + + + + ) : ( <> {scmType ? ( @@ -291,14 +339,14 @@ export function WorkbenchToolForm({ /> )} - {categories.length > 1 && ( + {type !== WorkbenchToolType.Mcp && categories.length > 1 && ( {categories.map((category) => { - const selected = (state.categories ?? []).filter(Boolean) + const selected = selectedCategories const isChecked = selected.includes(category) const canUncheck = selected.length > 1 return ( diff --git a/assets/src/components/workbenches/tools/workbenchToolsUtils.tsx b/assets/src/components/workbenches/tools/workbenchToolsUtils.tsx index ea15ccd05a..a4d63a0309 100644 --- a/assets/src/components/workbenches/tools/workbenchToolsUtils.tsx +++ b/assets/src/components/workbenches/tools/workbenchToolsUtils.tsx @@ -205,7 +205,13 @@ export const TOOL_TYPE_TO_CATEGORIES: Record< [WorkbenchToolType.BitbucketDatacenter]: [WorkbenchToolCategory.Scm], [WorkbenchToolType.AzureDevops]: [WorkbenchToolCategory.Scm], [WorkbenchToolType.Http]: [WorkbenchToolCategory.Integration], - [WorkbenchToolType.Mcp]: [WorkbenchToolCategory.Integration], + [WorkbenchToolType.Mcp]: [ + WorkbenchToolCategory.Integration, + WorkbenchToolCategory.Observability, + WorkbenchToolCategory.Infrastructure, + WorkbenchToolCategory.Coding, + WorkbenchToolCategory.Verification, + ], [WorkbenchToolType.Sentry]: [WorkbenchToolCategory.ErrorTracking], [WorkbenchToolType.Splunk]: [WorkbenchToolCategory.Logs], [WorkbenchToolType.Dynatrace]: [ @@ -293,6 +299,9 @@ export const categoryToLabel: Record = { [WorkbenchToolCategory.ErrorTracking]: 'Error tracking', [WorkbenchToolCategory.Infrastructure]: 'Infrastructure', [WorkbenchToolCategory.Function]: 'Cloud Function', + [WorkbenchToolCategory.Coding]: 'Coding', + [WorkbenchToolCategory.Verification]: 'Verification', + [WorkbenchToolCategory.Observability]: 'Observability', } export type WorkbenchToolCard = { diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 1d9d4db4d8..43d0741371 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -16174,7 +16174,8 @@ export enum WorkbenchJobActivityType { Search = 'SEARCH', Skill = 'SKILL', Ticketing = 'TICKETING', - User = 'USER' + User = 'USER', + Verify = 'VERIFY' } export type WorkbenchJobAttributes = { @@ -16715,16 +16716,19 @@ export type WorkbenchToolBitbucketDatacenterConnectionAttributes = { export enum WorkbenchToolCategory { Chat = 'CHAT', + Coding = 'CODING', ErrorTracking = 'ERROR_TRACKING', Function = 'FUNCTION', Infrastructure = 'INFRASTRUCTURE', Integration = 'INTEGRATION', Logs = 'LOGS', Metrics = 'METRICS', + Observability = 'OBSERVABILITY', Scm = 'SCM', Search = 'SEARCH', Ticketing = 'TICKETING', - Traces = 'TRACES' + Traces = 'TRACES', + Verification = 'VERIFICATION' } export type WorkbenchToolCloudRunConnection = { diff --git a/go/client/models_gen.go b/go/client/models_gen.go index 64fcbe1cbb..6447ba707e 100644 --- a/go/client/models_gen.go +++ b/go/client/models_gen.go @@ -17684,6 +17684,7 @@ const ( WorkbenchJobActivityTypeSearch WorkbenchJobActivityType = "SEARCH" WorkbenchJobActivityTypeFunction WorkbenchJobActivityType = "FUNCTION" WorkbenchJobActivityTypeKubernetes WorkbenchJobActivityType = "KUBERNETES" + WorkbenchJobActivityTypeVerify WorkbenchJobActivityType = "VERIFY" ) var AllWorkbenchJobActivityType = []WorkbenchJobActivityType{ @@ -17703,11 +17704,12 @@ var AllWorkbenchJobActivityType = []WorkbenchJobActivityType{ WorkbenchJobActivityTypeSearch, WorkbenchJobActivityTypeFunction, WorkbenchJobActivityTypeKubernetes, + WorkbenchJobActivityTypeVerify, } func (e WorkbenchJobActivityType) IsValid() bool { switch e { - case WorkbenchJobActivityTypeCoding, WorkbenchJobActivityTypeObservability, WorkbenchJobActivityTypeIntegration, WorkbenchJobActivityTypeTicketing, WorkbenchJobActivityTypeInfrastructure, WorkbenchJobActivityTypeMemo, WorkbenchJobActivityTypePlan, WorkbenchJobActivityTypeUser, WorkbenchJobActivityTypeMemory, WorkbenchJobActivityTypeConclusion, WorkbenchJobActivityTypeCanvas, WorkbenchJobActivityTypeSkill, WorkbenchJobActivityTypeHistory, WorkbenchJobActivityTypeSearch, WorkbenchJobActivityTypeFunction, WorkbenchJobActivityTypeKubernetes: + case WorkbenchJobActivityTypeCoding, WorkbenchJobActivityTypeObservability, WorkbenchJobActivityTypeIntegration, WorkbenchJobActivityTypeTicketing, WorkbenchJobActivityTypeInfrastructure, WorkbenchJobActivityTypeMemo, WorkbenchJobActivityTypePlan, WorkbenchJobActivityTypeUser, WorkbenchJobActivityTypeMemory, WorkbenchJobActivityTypeConclusion, WorkbenchJobActivityTypeCanvas, WorkbenchJobActivityTypeSkill, WorkbenchJobActivityTypeHistory, WorkbenchJobActivityTypeSearch, WorkbenchJobActivityTypeFunction, WorkbenchJobActivityTypeKubernetes, WorkbenchJobActivityTypeVerify: return true } return false @@ -17894,6 +17896,9 @@ const ( WorkbenchToolCategoryScm WorkbenchToolCategory = "SCM" WorkbenchToolCategoryChat WorkbenchToolCategory = "CHAT" WorkbenchToolCategoryFunction WorkbenchToolCategory = "FUNCTION" + WorkbenchToolCategoryCoding WorkbenchToolCategory = "CODING" + WorkbenchToolCategoryVerification WorkbenchToolCategory = "VERIFICATION" + WorkbenchToolCategoryObservability WorkbenchToolCategory = "OBSERVABILITY" ) var AllWorkbenchToolCategory = []WorkbenchToolCategory{ @@ -17908,11 +17913,14 @@ var AllWorkbenchToolCategory = []WorkbenchToolCategory{ WorkbenchToolCategoryScm, WorkbenchToolCategoryChat, WorkbenchToolCategoryFunction, + WorkbenchToolCategoryCoding, + WorkbenchToolCategoryVerification, + WorkbenchToolCategoryObservability, } func (e WorkbenchToolCategory) IsValid() bool { switch e { - case WorkbenchToolCategoryMetrics, WorkbenchToolCategoryLogs, WorkbenchToolCategoryIntegration, WorkbenchToolCategoryTicketing, WorkbenchToolCategoryTraces, WorkbenchToolCategoryErrorTracking, WorkbenchToolCategoryInfrastructure, WorkbenchToolCategorySearch, WorkbenchToolCategoryScm, WorkbenchToolCategoryChat, WorkbenchToolCategoryFunction: + case WorkbenchToolCategoryMetrics, WorkbenchToolCategoryLogs, WorkbenchToolCategoryIntegration, WorkbenchToolCategoryTicketing, WorkbenchToolCategoryTraces, WorkbenchToolCategoryErrorTracking, WorkbenchToolCategoryInfrastructure, WorkbenchToolCategorySearch, WorkbenchToolCategoryScm, WorkbenchToolCategoryChat, WorkbenchToolCategoryFunction, WorkbenchToolCategoryCoding, WorkbenchToolCategoryVerification, WorkbenchToolCategoryObservability: return true } return false diff --git a/lib/console/ai/tools/workbench/canvas/metrics.ex b/lib/console/ai/tools/workbench/canvas/metrics.ex index eb29a750a7..c83be0c284 100644 --- a/lib/console/ai/tools/workbench/canvas/metrics.ex +++ b/lib/console/ai/tools/workbench/canvas/metrics.ex @@ -46,8 +46,8 @@ defmodule Console.AI.Tools.Workbench.Canvas.MetricsBlock do end end - def validate_tool(%Console.AI.Workbench.Environment{user: user} = env, %ToolQuery{tool_name: name, tool_args: args}, valid_tools) do - tools = Subagents.Observability.tools(env, user) + def validate_tool(%Console.AI.Workbench.Environment{} = env, %ToolQuery{tool_name: name, tool_args: args}, valid_tools) do + tools = Subagents.Observability.tools(env) with tool when not is_nil(tool) <- Enum.find(tools, & Tool.name(&1) == name), {:ok, %mod{} = t} <- Tool.validate(tool, args), true <- Enum.member?(valid_tools, mod) do diff --git a/lib/console/ai/tools/workbench/subagent.ex b/lib/console/ai/tools/workbench/subagent.ex index a9a8c48d88..d56b6fcf8a 100644 --- a/lib/console/ai/tools/workbench/subagent.ex +++ b/lib/console/ai/tools/workbench/subagent.ex @@ -2,7 +2,7 @@ defmodule Console.AI.Tools.Workbench.Subagent do use Console.AI.Tools.Workbench.Base import EctoEnum - defenum Subagent, coding: 0, infrastructure: 1, observability: 2, integration: 3, skill: 4, history: 5, search: 6 + defenum Subagent, coding: 0, infrastructure: 1, observability: 2, integration: 3, skill: 4, history: 5, search: 6, verify: 7 embedded_schema do field :subagents, {:array, Subagent}, virtual: true diff --git a/lib/console/ai/tools/workbench/subagents.ex b/lib/console/ai/tools/workbench/subagents.ex index 60fac1ec58..697f4ade0b 100644 --- a/lib/console/ai/tools/workbench/subagents.ex +++ b/lib/console/ai/tools/workbench/subagents.ex @@ -34,6 +34,7 @@ defmodule Console.AI.Tools.Workbench.Subagents do defp subagent_description(_, :skill, _), do: "Invoke a skill subagent to update the skills for the current workbench. This subagent will use the skills API to update the skills for the current workbench." defp subagent_description(_, :history, _), do: "Invoke a history subagent to search past workbench activities. Useful to remember what has been done so far, with regex support for finding past work." defp subagent_description(_, :search, _), do: "Invoke a web search subagent to search the public web for information. Useful to find documentation, public pricing information, and anything else that's not specific to deployed infrastructure." + defp subagent_description(_, :verify, _), do: "Invoke a verification subagent to verify the job was successfully completed based on infrastructure and observability state." defp subagent_description(_, _, _), do: "Unknown subagent" defp infra_description(%{vulnerabilities: vulns, pod_logs: logs}) when vulns or logs do diff --git a/lib/console/ai/workbench/engine.ex b/lib/console/ai/workbench/engine.ex index b3a5a1bd3c..34b0880400 100644 --- a/lib/console/ai/workbench/engine.ex +++ b/lib/console/ai/workbench/engine.ex @@ -46,7 +46,7 @@ defmodule Console.AI.Workbench.Engine do require EEx require Logger - defstruct [:job, :user, :environment, activities: [], messages: [], iterations: 0, max: 200, verifiable: false] + defstruct [:job, :user, :environment, activities: [], messages: [], iterations: 0, max: 200] defmodule Acc do defstruct [messages: [], activities: []] @@ -295,7 +295,9 @@ defmodule Console.AI.Workbench.Engine do end defp tools(%WorkbenchJob{} = job, %Environment{skills: skills} = env, activities) do - subagents = Environment.subagents(job) |> maybe_add_memory(activities) + subagents = Environment.subagents(env) + |> maybe_add_memory(activities) + categories = Environment.categories(job) skills = Environment.with_builtins(skills) |> Environment.subagent_skills(:orchestrator) @@ -362,7 +364,7 @@ defmodule Console.AI.Workbench.Engine do (is_list(prs) && Enum.any?(prs, & &1.status == :merged)) || (is_list(activities) && Enum.any?(activities, & &1.status == :successful && is_action(&1.type))) - %{engine | verifiable: verifiable} + put_in(engine.environment.verifiable, verifiable) end defp verifiable(engine), do: engine diff --git a/lib/console/ai/workbench/environment.ex b/lib/console/ai/workbench/environment.ex index 3eb81fec56..5ee208ecf3 100644 --- a/lib/console/ai/workbench/environment.ex +++ b/lib/console/ai/workbench/environment.ex @@ -21,7 +21,7 @@ defmodule Console.AI.Workbench.Environment do defguardp is_map_or_list(m) when is_map(m) or is_list(m) - defstruct [:job, :tools, :skills, :user, functions: [], activities: []] + defstruct [:job, :tools, :skills, :user, functions: [], activities: [], verifiable: false] def new(%WorkbenchJob{} = job, tools, skills) when is_map_or_list(tools) and is_map_or_list(skills) do {functions, tools} = Enum.split_with(to_l(tools), fn @@ -66,6 +66,9 @@ defmodule Console.AI.Workbench.Environment do |> Map.new() end + def subagents(%__MODULE__{verifiable: true, job: job}), do: [:verify | subagents(job)] + def subagents(%__MODULE__{} = environment), do: subagents(environment.job) + def subagents(%WorkbenchJob{workbench: %Workbench{tools: tools} = bench} = job) do tool_agents(tools) |> Enum.concat(type_subagents(job)) @@ -165,5 +168,9 @@ defmodule Console.AI.Workbench.Environment do defp category_to_subagent(:search), do: :search defp category_to_subagent(:scm), do: :integration defp category_to_subagent(:chat), do: :integration + defp category_to_subagent(:observability), do: :observability + defp category_to_subagent(:infrastructure), do: :infrastructure + defp category_to_subagent(:coding), do: :coding + defp category_to_subagent(:verification), do: :verify defp category_to_subagent(_), do: :integration end diff --git a/lib/console/ai/workbench/subagents/coding.ex b/lib/console/ai/workbench/subagents/coding.ex index ae229e73ef..a4e8c3c493 100644 --- a/lib/console/ai/workbench/subagents/coding.ex +++ b/lib/console/ai/workbench/subagents/coding.ex @@ -1,6 +1,11 @@ defmodule Console.AI.Workbench.Subagents.Coding do use Console.AI.Workbench.Subagents.Base - alias Console.Schema.{WorkbenchJob, WorkbenchJobActivity, AgentRun, AIUsage} + alias Console.Schema.{ + WorkbenchJob, + WorkbenchJobActivity, + AgentRun, + AIUsage + } alias Console.AI.Tools.Workbench.{ Skills, History, diff --git a/lib/console/ai/workbench/subagents/infrastructure.ex b/lib/console/ai/workbench/subagents/infrastructure.ex index 6ac68d4539..1d2164cd58 100644 --- a/lib/console/ai/workbench/subagents/infrastructure.ex +++ b/lib/console/ai/workbench/subagents/infrastructure.ex @@ -66,12 +66,8 @@ defmodule Console.AI.Workbench.Subagents.Infrastructure do defp tools(%WorkbenchJob{workbench: bench, user: user}, %Environment{skills: skills, job: job, activities: activities} = environment, %FileCache{} = cache) do skills = Environment.subagent_skills(skills, :infrastructure) - svc_tools(bench, job, user) - |> Enum.concat(stack_tools(bench, user)) - |> Enum.concat(k8s_tools(bench, user)) - |> Enum.concat(pod_logs_tools(bench, user)) + core_tools(job, environment) |> Enum.concat(vuln_tools(bench, user)) - |> Enum.concat(cloud_tools(environment)) |> Enum.concat(manifests_tools(bench, job, user, cache)) |> Enum.concat([ %Skills{skills: skills}, @@ -83,6 +79,14 @@ defmodule Console.AI.Workbench.Subagents.Infrastructure do ]) end + def core_tools(%WorkbenchJob{workbench: bench, user: user} = job, %Environment{} = environment) do + svc_tools(bench, job, user) + |> Enum.concat(stack_tools(bench, user)) + |> Enum.concat(k8s_tools(bench, user)) + |> Enum.concat(pod_logs_tools(bench, user)) + |> Enum.concat(cloud_tools(environment)) + end + defp cloud_tools(%Environment{tools: tools}) do Enum.flat_map(tools, fn {_, %WorkbenchTool{tool: :cloud} = tool} -> [ diff --git a/lib/console/ai/workbench/subagents/observability.ex b/lib/console/ai/workbench/subagents/observability.ex index a1ae6f49ce..ae6a7a85da 100644 --- a/lib/console/ai/workbench/subagents/observability.ex +++ b/lib/console/ai/workbench/subagents/observability.ex @@ -9,8 +9,8 @@ defmodule Console.AI.Workbench.Subagents.Observability do require EEx - def run(%WorkbenchJobActivity{prompt: prompt} = activity, %WorkbenchJob{prompt: jprompt, user: user} = job, %Environment{} = environment) do - tools = tools(environment, user) + def run(%WorkbenchJobActivity{prompt: prompt} = activity, %WorkbenchJob{prompt: jprompt} = job, %Environment{} = environment) do + tools = tools(environment) MemoryEngine.new(tools, 50, engine_opts(job) ++ [ @@ -39,13 +39,12 @@ defmodule Console.AI.Workbench.Subagents.Observability do end end - def tools(%Environment{skills: skills, tools: tools, job: job, activities: activities}, user) do + def tools(%Environment{job: %WorkbenchJob{user: user}} = environment), do: tools(environment, user) + def tools(%Environment{skills: skills, tools: tools, job: job, activities: activities} = environment, user) do skills = Environment.subagent_skills(skills, :observability) - obs_tools(tools) + core_tools(job, environment, user) |> Enum.concat(MCP.expand_tools(Environment.subagent_tools(tools, :observability), job)) - |> Enum.concat(plrl_log_tools(job)) - |> Enum.concat(plrl_metric_tools(job)) |> Enum.concat(pod_logs_tools(job, user)) |> Enum.concat([ %Skills{skills: skills}, @@ -57,14 +56,21 @@ defmodule Console.AI.Workbench.Subagents.Observability do ]) end - defp plrl_log_tools(%WorkbenchJob{user: user, workbench: %Workbench{configuration: %{observability: %{logs: true}}}}) do + def core_tools(%WorkbenchJob{user: user} = job, %Environment{tools: tools}), do: core_tools(job, tools, user) + def core_tools(%WorkbenchJob{} = job, tools, user) do + obs_tools(tools) + |> Enum.concat(plrl_log_tools(job, user)) + |> Enum.concat(plrl_metric_tools(job)) + end + + defp plrl_log_tools(%WorkbenchJob{workbench: %Workbench{configuration: %{observability: %{logs: true}}}}, %User{} = user) do [ %Plrl.Logs{user: user}, %Plrl.LogsAggregate{user: user}, %Plrl.LogLabels{user: user} ] end - defp plrl_log_tools(_), do: [] + defp plrl_log_tools(_, _), do: [] defp pod_logs_tools(%Workbench{configuration: %{infrastructure: %{pod_logs: true}}}, %User{} = user), do: [%PodLogs{user: user}] diff --git a/lib/console/ai/workbench/subagents/verify.ex b/lib/console/ai/workbench/subagents/verify.ex new file mode 100644 index 0000000000..970421dbc0 --- /dev/null +++ b/lib/console/ai/workbench/subagents/verify.ex @@ -0,0 +1,52 @@ +defmodule Console.AI.Workbench.Subagents.Verify do + use Console.AI.Workbench.Subagents.Base + alias Console.AI.Workbench.Subagents.{Infrastructure, Observability} + alias Console.Schema.{WorkbenchJob, WorkbenchJobActivity} + alias Console.AI.Tools.Workbench.{Result, Skills, Skill, Scratchpad} + alias Console.AI.Workbench.Environment + import Console.AI.Workbench.Environment, only: [engine_opts: 1] + + require EEx + + def run(%WorkbenchJobActivity{prompt: prompt} = activity, %WorkbenchJob{prompt: jprompt} = job, %Environment{} = environment) do + tools(job, environment) + |> MemoryEngine.new(20, + engine_opts(job) ++ [ + system_prompt: String.trim(system_prompt(prompt: jprompt)), + acc: %{}, + callback: &callback(activity, &1), + continue_msg: cont_msg() + ] + ) + |> MemoryEngine.reduce([{:user, prompt}], &reducer/2) + |> case do + {:ok, attrs} -> attrs + {:error, error} -> %{status: :failed, result: %{error: "error running verification subagent: #{inspect(error)}"}} + end + end + + defp reducer(messages, _) do + case Enum.find(messages, &match?(%Result{}, &1)) do + %Result{output: output} -> {:halt, %{ + status: :successful, + result: %{output: output} + }} + _ -> last_message(messages, & {:cont, %{status: :failed, result: %{error: &1}}}) + end + end + + defp tools(%WorkbenchJob{} = job, %Environment{skills: skills} = environment) do + skills = Environment.subagent_skills(skills, :verify) + + Observability.core_tools(job, environment) + |> Enum.concat(Infrastructure.core_tools(job, environment)) + |> Enum.concat([ + %Skills{skills: skills}, + %Skill{skills: skills}, + Scratchpad, + Result + ]) + end + + EEx.function_from_file(:defp, :system_prompt, Console.priv_filename(["prompts", "workbench", "verify.md.eex"]), [:assigns]) +end diff --git a/lib/console/schema/workbench_job_activity.ex b/lib/console/schema/workbench_job_activity.ex index abce4b940f..9316142e4e 100644 --- a/lib/console/schema/workbench_job_activity.ex +++ b/lib/console/schema/workbench_job_activity.ex @@ -19,7 +19,8 @@ defmodule Console.Schema.WorkbenchJobActivity do history: 12, search: 13, function: 14, - kubernetes: 15 + kubernetes: 15, + verify: 16 defguard is_action(type) when type in [:function, :kubernetes] diff --git a/lib/console/schema/workbench_tool.ex b/lib/console/schema/workbench_tool.ex index 844e593152..992f6d4958 100644 --- a/lib/console/schema/workbench_tool.ex +++ b/lib/console/schema/workbench_tool.ex @@ -48,7 +48,10 @@ defmodule Console.Schema.WorkbenchTool do search: 7, scm: 8, chat: 9, - function: 10 + function: 10, + coding: 11, + verification: 12, + observability: 13 defenum HttpMethod, get: 0, post: 1, put: 2, delete: 3, patch: 4 schema "workbench_tools" do @@ -335,7 +338,7 @@ defmodule Console.Schema.WorkbenchTool do defp valid_category(changeset, tool) do validate_change(changeset, :categories, fn :categories, categories -> - cats = categories(tool) + cats = possible_categories(tool) case MapSet.subset?(MapSet.new(categories), MapSet.new(cats)) do true -> [] false -> [categories: "must be a subset of [#{Enum.join(cats, ", ")}] for a #{tool} tool"] @@ -359,6 +362,9 @@ defmodule Console.Schema.WorkbenchTool do end end + defp possible_categories(:mcp), do: [:integration, :observability, :infrastructure, :coding, :verification] + defp possible_categories(t), do: categories(t) + defp categories(:http), do: [:integration] defp categories(:datadog), do: [:metrics, :logs, :traces] defp categories(:dynatrace), do: [:metrics, :logs, :traces] diff --git a/priv/prompts/workbench/verify.md.eex b/priv/prompts/workbench/verify.md.eex new file mode 100644 index 0000000000..c98a2d8713 --- /dev/null +++ b/priv/prompts/workbench/verify.md.eex @@ -0,0 +1,32 @@ +You're a senior engineer being assigned a task, and are focusing in particular on verifying whether the requested work has actually been completed. +Your job is to validate the end state using the infrastructure and observability evidence available to you, not to assume success from a prior summary. + +You have access to a focused set of tools from both the infrastructure and observability subagents: + +* Infrastructure tools can inspect Plural Services and Stacks, Kubernetes resources, pod logs, live cloud configuration, and other deployment state. +* Observability tools can inspect metrics, logs, traces, error tracking, and Plural observability data where those integrations are configured. +* Skill tools are available and should be used when you need guidance on how this workbench's tools, environments, or systems are expected to be queried. +* Scratchpad is available for intermediate notes. Use it when it helps keep evidence organized before producing the final result. + +The overarching task is as follows: + +<%= @prompt %> + +## Ideal Output + +Return a concise markdown conclusion that includes: + +* Verdict: completed, incomplete, or unverifiable. +* Evidence: the most important infrastructure and observability findings, including resource names, service or stack names, clusters, namespaces, relevant queries, and time windows where useful. +* Gaps: anything you could not verify and why. It's possible more work needs to be done externally to this subagent to reprompt again with better context for verification. +* Next action: only if the work is incomplete or unverifiable. + +**IMPORTANT**: Once you've completed all the work you plan to do, always call the `subagent_result` tool *exactly once* to complete the subagent session. + +## Tone of Voice Guidance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to convey the result of your work. +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. diff --git a/schema/schema.graphql b/schema/schema.graphql index 940e3d3934..9fcc2e9fc4 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -2625,6 +2625,9 @@ enum WorkbenchToolCategory { SCM CHAT FUNCTION + CODING + VERIFICATION + OBSERVABILITY } enum WorkbenchToolHttpMethod { @@ -2671,6 +2674,7 @@ enum WorkbenchJobActivityType { SEARCH FUNCTION KUBERNETES + VERIFY } enum WorkbenchCanvasBlockType { diff --git a/test/console/ai/workbench/subagents/verify_test.exs b/test/console/ai/workbench/subagents/verify_test.exs new file mode 100644 index 0000000000..d9c5e934cc --- /dev/null +++ b/test/console/ai/workbench/subagents/verify_test.exs @@ -0,0 +1,67 @@ +defmodule Console.AI.Workbench.Subagents.VerifyTest do + use Console.DataCase, async: false + use Mimic + alias Console.AI.Workbench.{Environment, Subagents} + alias Console.AI.{Provider, Tool} + + setup :set_mimic_global + + describe "run/3" do + test "returns subagent_result with infrastructure and observability tools available" do + deployment_settings( + logging: %{enabled: true, driver: :elastic, elastic: es_settings()}, + ai: %{ + enabled: true, + provider: :openai, + openai: %{access_token: "key"}, + vector_store: %{enabled: false} + } + ) + + expect(Provider, :completion, fn _, opts -> + preface = Keyword.fetch!(opts, :preface) + assert preface =~ "verifying whether the requested work has actually been completed" + assert preface =~ "Infrastructure tools can inspect Plural Services and Stacks" + assert preface =~ "Observability tools can inspect metrics, logs, traces, error tracking" + + tool_names = + Keyword.fetch!(opts, :plural) + |> Enum.map(&Tool.name/1) + + assert "subagent_result" in tool_names + assert "plrl_cluster_services" in tool_names + assert "workbench_observability_metrics_prom" in tool_names + + {:ok, "verified", [ + %Tool{name: "subagent_result", arguments: %{"output" => "verification complete"}, id: "1"} + ]} + end) + + workbench = + insert(:workbench, + configuration: %{ + infrastructure: %{services: true, stacks: true, kubernetes: true}, + observability: %{metrics: true} + } + ) + + tool = + insert(:workbench_tool, + tool: :prometheus, + name: "prom", + categories: [:metrics], + configuration: %{ + prometheus: %{url: "https://prom.example.com", token: "token", tenant_id: nil} + } + ) + + job = insert(:workbench_job, workbench: workbench, prompt: "Verify the rollout completed") + activity = insert(:workbench_job_activity, workbench_job: job, type: :verify, prompt: "Check completion") + + result = Subagents.Verify.run(activity, job, Environment.new(job, [tool], [])) + + assert result[:status] == :successful + assert result[:result][:output] == "verification complete" + end + end +end From 243c0b1f0d9ac87642c2bba7ec7dcca5ca56532b Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Fri, 24 Jul 2026 14:15:17 -0400 Subject: [PATCH 5/8] implement pr followup mutation --- assets/src/generated/graphql.ts | 7 ++++ lib/console/ai/workbench/engine.ex | 1 + .../ai/workbench/subagents/observability.ex | 3 +- lib/console/deployments/workbenches.ex | 16 ++++++++ lib/console/graphql/deployments/workbench.ex | 11 +++++ .../resolvers/deployments/workbench.ex | 3 ++ lib/console/schema/workbench_job.ex | 2 + schema/schema.graphql | 8 ++++ test/console/deployments/workbenches_test.exs | 33 +++++++++++++++ .../deployments/workbench_mutations_test.exs | 41 +++++++++++++++++++ 10 files changed, 124 insertions(+), 1 deletion(-) diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 43d0741371..18c41a9961 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -9292,6 +9292,7 @@ export type RootMutationType = { /** Fetches a workbench cron by id. Requires read access to the workbench. */ workbenchCron?: Maybe; workbenchEvalSkill?: Maybe; + workbenchPrFollowup?: Maybe; }; @@ -10800,6 +10801,12 @@ export type RootMutationTypeWorkbenchEvalSkillArgs = { prompt?: InputMaybe; }; + +export type RootMutationTypeWorkbenchPrFollowupArgs = { + attributes: WorkbenchMessageAttributes; + url: Scalars['String']['input']; +}; + export type RootQueryType = { __typename?: 'RootQueryType'; accessToken?: Maybe; diff --git a/lib/console/ai/workbench/engine.ex b/lib/console/ai/workbench/engine.ex index 34b0880400..cdecbe6bd0 100644 --- a/lib/console/ai/workbench/engine.ex +++ b/lib/console/ai/workbench/engine.ex @@ -277,6 +277,7 @@ defmodule Console.AI.Workbench.Engine do defp subagent_module(:history), do: SA.History defp subagent_module(:skill), do: SA.Skill defp subagent_module(:search), do: SA.Search + defp subagent_module(:verify), do: SA.Verify defp tool_attrs(%{id: %Console.AI.Tool{id: id, name: name, arguments: arguments}}) when is_binary(id) and is_binary(name), do: %{call_id: id, name: name, arguments: arguments} diff --git a/lib/console/ai/workbench/subagents/observability.ex b/lib/console/ai/workbench/subagents/observability.ex index ae6a7a85da..a0a2dce68e 100644 --- a/lib/console/ai/workbench/subagents/observability.ex +++ b/lib/console/ai/workbench/subagents/observability.ex @@ -57,7 +57,8 @@ defmodule Console.AI.Workbench.Subagents.Observability do end def core_tools(%WorkbenchJob{user: user} = job, %Environment{tools: tools}), do: core_tools(job, tools, user) - def core_tools(%WorkbenchJob{} = job, tools, user) do + def core_tools(%WorkbenchJob{} = job, %Environment{tools: tools}, user), do: core_tools(job, tools, user) + def core_tools(%WorkbenchJob{} = job, tools, user) when is_list(tools) or is_map(tools) do obs_tools(tools) |> Enum.concat(plrl_log_tools(job, user)) |> Enum.concat(plrl_metric_tools(job)) diff --git a/lib/console/deployments/workbenches.ex b/lib/console/deployments/workbenches.ex index 715ec299b6..85a4a56561 100644 --- a/lib/console/deployments/workbenches.ex +++ b/lib/console/deployments/workbenches.ex @@ -21,6 +21,7 @@ defmodule Console.Deployments.Workbenches do WorkbenchChatbot, WorkbenchJobActivityAgentRun, WorkbenchJobThought, + PullRequest, FlowWorkbench } alias Console.AI.{Provider, VectorStore} @@ -785,6 +786,21 @@ defmodule Console.Deployments.Workbenches do |> then(&create_message(attrs, &1, user)) end + @doc """ + Creates a new message for the job associated with a pull request. + """ + @spec pr_followup(map, binary, User.t()) :: activity_resp + def pr_followup(attrs, url, %User{} = user) do + Repo.get_by(PullRequest, url: url) + |> Repo.preload([:workbench_job]) + |> case do + %PullRequest{workbench_job: %WorkbenchJob{} = job} -> + create_message(attrs, job, user) + _ -> + {:error, "pull request not found"} + end + end + @doc """ Creates a new activity for a job, and bookkeeps job status and timestamp. """ diff --git a/lib/console/graphql/deployments/workbench.ex b/lib/console/graphql/deployments/workbench.ex index 24695e36c4..d59a68294d 100644 --- a/lib/console/graphql/deployments/workbench.ex +++ b/lib/console/graphql/deployments/workbench.ex @@ -1758,6 +1758,17 @@ defmodule Console.GraphQl.Deployments.Workbench do resolve &Deployments.create_workbench_message/2 end + field :workbench_pr_followup, :workbench_job_activity do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :url, non_null(:string), description: "the pull request url to create a follow-up message for" + arg :attributes, non_null(:workbench_message_attributes), description: "message attributes (e.g. prompt)" + + resolve &Deployments.workbench_pr_followup/2 + end + @desc "Approves and invokes a pending workbench function activity. Requires read access to the job's workbench." field :approve_workbench_job_activity, :workbench_job_activity do middleware Authenticated diff --git a/lib/console/graphql/resolvers/deployments/workbench.ex b/lib/console/graphql/resolvers/deployments/workbench.ex index 36f8ef3e6b..f7de1f43a5 100644 --- a/lib/console/graphql/resolvers/deployments/workbench.ex +++ b/lib/console/graphql/resolvers/deployments/workbench.ex @@ -347,6 +347,9 @@ defmodule Console.GraphQl.Resolvers.Deployments.Workbench do def create_workbench_message(%{job_id: job_id, attributes: attrs}, %{context: %{current_user: user}}), do: Workbenches.create_message(attrs, job_id, user) + def workbench_pr_followup(%{url: url, attributes: attrs}, %{context: %{current_user: user}}), + do: Workbenches.pr_followup(attrs, url, user) + def approve_workbench_job_activity(%{id: id}, %{context: %{current_user: user}}), do: Workbenches.approve_job_activity(id, user) diff --git a/lib/console/schema/workbench_job.ex b/lib/console/schema/workbench_job.ex index d234659966..8ba7fb39f5 100644 --- a/lib/console/schema/workbench_job.ex +++ b/lib/console/schema/workbench_job.ex @@ -82,6 +82,8 @@ defmodule Console.Schema.WorkbenchJob do defenum Status, pending: 0, running: 1, successful: 2, failed: 3, cancelled: 4, paused: 5 defenum Type, job: 0, skill: 1 + defguard is_terminal(status) when status in ~w(successful failed cancelled)a + schema "workbench_jobs" do field :status, Status, default: :pending field :type, Type, default: :job diff --git a/schema/schema.graphql b/schema/schema.graphql index 9fcc2e9fc4..7b720ba43a 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -1406,6 +1406,14 @@ type RootMutationType { attributes: WorkbenchMessageAttributes! ): WorkbenchJobActivity + workbenchPrFollowup( + "the pull request url to create a follow-up message for" + url: String! + + "message attributes (e.g. prompt)" + attributes: WorkbenchMessageAttributes! + ): WorkbenchJobActivity + "Approves and invokes a pending workbench function activity. Requires read access to the job's workbench." approveWorkbenchJobActivity( "the workbench job activity to approve" diff --git a/test/console/deployments/workbenches_test.exs b/test/console/deployments/workbenches_test.exs index 10729ed7e0..fa5fe2ca3e 100644 --- a/test/console/deployments/workbenches_test.exs +++ b/test/console/deployments/workbenches_test.exs @@ -945,6 +945,39 @@ defmodule Console.Deployments.WorkbenchesTest do end end + describe "pr_followup/3" do + test "creates a message for the pull request job when it is idle" do + user = insert(:user) + workbench = insert(:workbench, read_bindings: [%{user_id: user.id}]) + job = insert(:workbench_job, user: user, workbench: workbench, status: :successful) + pr = insert(:pull_request, workbench_job: job) + + assert WorkbenchJob.idle?(job) + + {:ok, activity} = + Workbenches.pr_followup(%{prompt: "pr follow-up"}, pr.url, user) + + assert activity.workbench_job_id == job.id + assert activity.prompt == "pr follow-up" + assert activity.type == :user + assert_receive {:event, %PubSub.WorkbenchJobActivityCreated{item: ^activity}} + end + + test "returns an error when the pull request job is active" do + user = insert(:user) + workbench = insert(:workbench, read_bindings: [%{user_id: user.id}]) + job = insert(:workbench_job, user: user, workbench: workbench, status: :running) + pr = insert(:pull_request, workbench_job: job) + + refute WorkbenchJob.idle?(job) + + assert {:error, "job is currently active, please wait for it to complete before prompting"} = + Workbenches.pr_followup(%{prompt: "while running"}, pr.url, user) + + refute_receive {:event, %PubSub.WorkbenchJobActivityCreated{}} + end + end + describe "create_job_activity/2" do test "creates an activity and sets job status to running" do job = insert(:workbench_job, status: :pending) diff --git a/test/console/graphql/mutations/deployments/workbench_mutations_test.exs b/test/console/graphql/mutations/deployments/workbench_mutations_test.exs index 5c97dd9c6c..51e363478c 100644 --- a/test/console/graphql/mutations/deployments/workbench_mutations_test.exs +++ b/test/console/graphql/mutations/deployments/workbench_mutations_test.exs @@ -616,6 +616,47 @@ defmodule Console.GraphQl.Deployments.WorkbenchMutationsTest do end end + describe "workbenchPrFollowup" do + test "it can create a user message on an idle pull request job" do + user = admin_user() + workbench = insert(:workbench) + job = insert(:workbench_job, user: user, workbench: workbench, status: :successful) + pr = insert(:pull_request, workbench_job: job) + + {:ok, %{data: %{"workbenchPrFollowup" => activity}}} = run_query(""" + mutation WorkbenchPrFollowup($url: String!, $attributes: WorkbenchMessageAttributes!) { + workbenchPrFollowup(url: $url, attributes: $attributes) { + id + prompt + type + status + } + } + """, %{"url" => pr.url, "attributes" => %{"prompt" => "from pr graphql"}}, %{current_user: user}) + + assert activity["prompt"] == "from pr graphql" + assert activity["type"] == "USER" + assert activity["status"] == "SUCCESSFUL" + end + + test "it returns an error when the pull request job is active" do + user = admin_user() + workbench = insert(:workbench) + job = insert(:workbench_job, user: user, workbench: workbench, status: :running) + pr = insert(:pull_request, workbench_job: job) + + {:ok, %{errors: [error | _]}} = run_query(""" + mutation WorkbenchPrFollowup($url: String!, $attributes: WorkbenchMessageAttributes!) { + workbenchPrFollowup(url: $url, attributes: $attributes) { + id + } + } + """, %{"url" => pr.url, "attributes" => %{"prompt" => "while running"}}, %{current_user: user}) + + assert error.message == "job is currently active, please wait for it to complete before prompting" + end + end + describe "approveWorkbenchJobActivity" do test "it approves and invokes a function activity" do user = insert(:user) From 7c5b2f61cbf78db4228960e055b96dce256f3b97 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Fri, 24 Jul 2026 17:13:25 -0400 Subject: [PATCH 6/8] add configurable health map limits --- assets/src/components/home/Home.tsx | 6 ++++- assets/src/generated/graphql.ts | 10 ++++--- .../generated/persisted-queries/client.json | 8 +++--- assets/src/graph/home.graphql | 4 +-- assets/src/graph/login.graphql | 1 + charts/console/templates/secrets.yaml | 3 +++ charts/console/values.yaml | 3 +++ config/config.exs | 1 + go/client/models_gen.go | 23 ++++++++-------- lib/console/ai/chat/memory_engine.ex | 10 ++++--- lib/console/configuration.ex | 3 ++- lib/console/graphql/configuration.ex | 27 ++++++++++--------- rel/runtime.exs | 4 +++ schema/schema.graphql | 2 ++ 14 files changed, 68 insertions(+), 37 deletions(-) diff --git a/assets/src/components/home/Home.tsx b/assets/src/components/home/Home.tsx index 3976e3b484..a9123ad3ca 100644 --- a/assets/src/components/home/Home.tsx +++ b/assets/src/components/home/Home.tsx @@ -52,6 +52,7 @@ export function Home() { function HomeClusters() { const { borders } = useTheme() + const { configuration } = useLogin() const projectId = useProjectId() useSetBreadcrumbs(breadcrumbs) // we don't want a double popup, and cloud setup would come first if relevant @@ -77,7 +78,10 @@ function HomeClusters() { useState>(null) const { data: healthScoresData } = useClusterHealthScoresQuery({ - variables: { projectId }, + variables: { + projectId, + first: configuration?.healthmapClusterCount ?? 1000, + }, fetchPolicy: 'cache-and-network', pollInterval: POLL_INTERVAL, }) diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 18c41a9961..59a173076f 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -3520,6 +3520,7 @@ export type ConsoleConfiguration = { features?: Maybe; gitCommit?: Maybe; gitStatus?: Maybe; + healthmapClusterCount?: Maybe; /** whether at least one cluster has been installed, false if a user hasn't fully onboarded */ installed?: Maybe; isDemoProject?: Maybe; @@ -19799,6 +19800,7 @@ export type UpgradeStatisticsQuery = { __typename?: 'RootQueryType', upgradeStat export type ClusterHealthScoresQueryVariables = Exact<{ projectId?: InputMaybe; + first?: InputMaybe; }>; @@ -20093,7 +20095,7 @@ export type MeGroupsQuery = { __typename?: 'RootQueryType', me?: { __typename?: export type MeQueryVariables = Exact<{ [key: string]: never; }>; -export type MeQuery = { __typename?: 'RootQueryType', me?: { __typename?: 'User', unreadNotifications?: number | null, id: string, pluralId?: string | null, name: string, email: string, profile?: string | null, backgroundColor?: string | null, readTimestamp?: string | null, homepage?: Homepage | null, emailSettings?: { __typename?: 'EmailSettings', digest?: boolean | null } | null, roles?: { __typename?: 'UserRoles', admin?: boolean | null } | null, personas?: Array<{ __typename?: 'Persona', id: string, name: string, description?: string | null, bindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'PersonaConfiguration', all?: boolean | null, deployments?: { __typename?: 'PersonaDeployment', addOns?: boolean | null, clusters?: boolean | null, pipelines?: boolean | null, providers?: boolean | null, repositories?: boolean | null, services?: boolean | null } | null, home?: { __typename?: 'PersonaHome', manager?: boolean | null, security?: boolean | null } | null, flows?: { __typename?: 'PersonaFlows', permissions?: boolean | null, startWorkbenchJob?: boolean | null, pipelines?: boolean | null, previews?: boolean | null, workbenches?: boolean | null } | null, sidebar?: { __typename?: 'PersonaSidebar', audits?: boolean | null, flows?: boolean | null, kubernetes?: boolean | null, pullRequests?: boolean | null, settings?: boolean | null, backups?: boolean | null, stacks?: boolean | null, workbenches?: boolean | null } | null, services?: { __typename?: 'PersonaServices', configuration?: boolean | null, secrets?: boolean | null } | null, ai?: { __typename?: 'PersonaAi', pr?: boolean | null } | null } | null } | null> | null } | null, configuration?: { __typename?: 'ConsoleConfiguration', gitCommit?: string | null, isDemoProject?: boolean | null, isSandbox?: boolean | null, pluralLogin?: boolean | null, byok?: boolean | null, externalOidc?: boolean | null, cloud?: boolean | null, installed?: boolean | null, consoleVersion?: string | null, sentryEnabled?: boolean | null, qoveKey?: string | null, manifest?: { __typename?: 'PluralManifest', cluster?: string | null, bucketPrefix?: string | null, network?: { __typename?: 'ManifestNetwork', pluralDns?: boolean | null, subdomain?: string | null } | null } | null, gitStatus?: { __typename?: 'GitStatus', cloned?: boolean | null, output?: string | null } | null, features?: { __typename?: 'AvailableFeatures', audits?: boolean | null, cd?: boolean | null, databaseManagement?: boolean | null, userManagement?: boolean | null } | null, details?: { __typename?: 'ConsoleConfigurationDetails', assumeRoleArn?: string | null, egressIps?: Array | null } | null } | null }; +export type MeQuery = { __typename?: 'RootQueryType', me?: { __typename?: 'User', unreadNotifications?: number | null, id: string, pluralId?: string | null, name: string, email: string, profile?: string | null, backgroundColor?: string | null, readTimestamp?: string | null, homepage?: Homepage | null, emailSettings?: { __typename?: 'EmailSettings', digest?: boolean | null } | null, roles?: { __typename?: 'UserRoles', admin?: boolean | null } | null, personas?: Array<{ __typename?: 'Persona', id: string, name: string, description?: string | null, bindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'PersonaConfiguration', all?: boolean | null, deployments?: { __typename?: 'PersonaDeployment', addOns?: boolean | null, clusters?: boolean | null, pipelines?: boolean | null, providers?: boolean | null, repositories?: boolean | null, services?: boolean | null } | null, home?: { __typename?: 'PersonaHome', manager?: boolean | null, security?: boolean | null } | null, flows?: { __typename?: 'PersonaFlows', permissions?: boolean | null, startWorkbenchJob?: boolean | null, pipelines?: boolean | null, previews?: boolean | null, workbenches?: boolean | null } | null, sidebar?: { __typename?: 'PersonaSidebar', audits?: boolean | null, flows?: boolean | null, kubernetes?: boolean | null, pullRequests?: boolean | null, settings?: boolean | null, backups?: boolean | null, stacks?: boolean | null, workbenches?: boolean | null } | null, services?: { __typename?: 'PersonaServices', configuration?: boolean | null, secrets?: boolean | null } | null, ai?: { __typename?: 'PersonaAi', pr?: boolean | null } | null } | null } | null> | null } | null, configuration?: { __typename?: 'ConsoleConfiguration', gitCommit?: string | null, isDemoProject?: boolean | null, isSandbox?: boolean | null, pluralLogin?: boolean | null, byok?: boolean | null, externalOidc?: boolean | null, cloud?: boolean | null, installed?: boolean | null, consoleVersion?: string | null, sentryEnabled?: boolean | null, healthmapClusterCount?: number | null, qoveKey?: string | null, manifest?: { __typename?: 'PluralManifest', cluster?: string | null, bucketPrefix?: string | null, network?: { __typename?: 'ManifestNetwork', pluralDns?: boolean | null, subdomain?: string | null } | null } | null, gitStatus?: { __typename?: 'GitStatus', cloned?: boolean | null, output?: string | null } | null, features?: { __typename?: 'AvailableFeatures', audits?: boolean | null, cd?: boolean | null, databaseManagement?: boolean | null, userManagement?: boolean | null } | null, details?: { __typename?: 'ConsoleConfigurationDetails', assumeRoleArn?: string | null, egressIps?: Array | null } | null } | null }; export type LoginInfoQueryVariables = Exact<{ redirect?: InputMaybe; @@ -37703,8 +37705,8 @@ export type UpgradeStatisticsLazyQueryHookResult = ReturnType; export type UpgradeStatisticsQueryResult = Apollo.QueryResult; export const ClusterHealthScoresDocument = gql` - query ClusterHealthScores($projectId: ID) { - clusters(projectId: $projectId, first: 1000) { + query ClusterHealthScores($projectId: ID, $first: Int) { + clusters(projectId: $projectId, first: $first) { edges { node { ...ClusterHealthScore @@ -37727,6 +37729,7 @@ export const ClusterHealthScoresDocument = gql` * const { data, loading, error } = useClusterHealthScoresQuery({ * variables: { * projectId: // value for 'projectId' + * first: // value for 'first' * }, * }); */ @@ -38852,6 +38855,7 @@ export const MeDocument = gql` installed consoleVersion sentryEnabled + healthmapClusterCount manifest { ...Manifest } diff --git a/assets/src/generated/persisted-queries/client.json b/assets/src/generated/persisted-queries/client.json index f1061bd7ed..e6595b1351 100644 --- a/assets/src/generated/persisted-queries/client.json +++ b/assets/src/generated/persisted-queries/client.json @@ -1182,10 +1182,10 @@ "name": "UpgradeStatistics", "body": "query UpgradeStatistics($projectId: ID, $tag: TagInput) {\n upgradeStatistics(projectId: $projectId, tag: $tag) {\n ...UpgradeStatistics\n __typename\n }\n}\n\nfragment UpgradeStatistics on UpgradeStatistics {\n upgradeable\n count\n latest\n compliant\n __typename\n}" }, - "sha256:b9fbc6fb8f03a0146a2af1c94141c2d41233dd45be984bd52ea96c41c8d8c4e9": { + "sha256:d389175e3791c1bc16aca35741b8f920ec0b4f89bfb01c1c352e8c3d392c4e5b": { "type": "query", "name": "ClusterHealthScores", - "body": "query ClusterHealthScores($projectId: ID) {\n clusters(projectId: $projectId, first: 1000) {\n edges {\n node {\n ...ClusterHealthScore\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment ClusterHealthScore on Cluster {\n id\n name\n healthScore\n distro\n provider {\n cloud\n __typename\n }\n __typename\n}" + "body": "query ClusterHealthScores($projectId: ID, $first: Int) {\n clusters(projectId: $projectId, first: $first) {\n edges {\n node {\n ...ClusterHealthScore\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment ClusterHealthScore on Cluster {\n id\n name\n healthScore\n distro\n provider {\n cloud\n __typename\n }\n __typename\n}" }, "sha256:22e8d642aa11212ae0ca3a3dee5ef82cacfc1ee9a9b13720be78e2f370460d4c": { "type": "query", @@ -1307,10 +1307,10 @@ "name": "MeGroups", "body": "query MeGroups {\n me {\n id\n groups {\n ...Group\n __typename\n }\n __typename\n }\n}\n\nfragment Group on Group {\n id\n name\n description\n global\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:4a7941b2da4b88aeac2e1055144bcf23bbaf573476991940ff5f25c07ba1728b": { + "sha256:af65bcb71eea5c588aa9feb99cb2a686438f102dc74604accd0a0fe75e434bdd": { "type": "query", "name": "Me", - "body": "query Me {\n me {\n ...User\n unreadNotifications\n __typename\n }\n configuration {\n gitCommit\n isDemoProject\n isSandbox\n pluralLogin\n byok\n externalOidc\n cloud\n installed\n consoleVersion\n sentryEnabled\n manifest {\n ...Manifest\n __typename\n }\n gitStatus {\n cloned\n output\n __typename\n }\n features {\n ...AvailableFeatures\n __typename\n }\n details {\n assumeRoleArn\n egressIps\n __typename\n }\n qoveKey\n __typename\n }\n}\n\nfragment User on User {\n id\n pluralId\n name\n email\n emailSettings {\n digest\n __typename\n }\n profile\n backgroundColor\n readTimestamp\n roles {\n admin\n __typename\n }\n personas {\n ...Persona\n __typename\n }\n homepage\n __typename\n}\n\nfragment Persona on Persona {\n id\n name\n description\n bindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PersonaConfiguration\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PersonaConfiguration on PersonaConfiguration {\n all\n deployments {\n addOns\n clusters\n pipelines\n providers\n repositories\n services\n __typename\n }\n home {\n manager\n security\n __typename\n }\n flows {\n permissions\n startWorkbenchJob\n pipelines\n previews\n workbenches\n __typename\n }\n sidebar {\n audits\n flows\n kubernetes\n pullRequests\n settings\n backups\n stacks\n workbenches\n __typename\n }\n services {\n configuration\n secrets\n __typename\n }\n ai {\n pr\n __typename\n }\n __typename\n}\n\nfragment Manifest on PluralManifest {\n network {\n pluralDns\n subdomain\n __typename\n }\n cluster\n bucketPrefix\n __typename\n}\n\nfragment AvailableFeatures on AvailableFeatures {\n audits\n cd\n databaseManagement\n userManagement\n __typename\n}" + "body": "query Me {\n me {\n ...User\n unreadNotifications\n __typename\n }\n configuration {\n gitCommit\n isDemoProject\n isSandbox\n pluralLogin\n byok\n externalOidc\n cloud\n installed\n consoleVersion\n sentryEnabled\n healthmapClusterCount\n manifest {\n ...Manifest\n __typename\n }\n gitStatus {\n cloned\n output\n __typename\n }\n features {\n ...AvailableFeatures\n __typename\n }\n details {\n assumeRoleArn\n egressIps\n __typename\n }\n qoveKey\n __typename\n }\n}\n\nfragment User on User {\n id\n pluralId\n name\n email\n emailSettings {\n digest\n __typename\n }\n profile\n backgroundColor\n readTimestamp\n roles {\n admin\n __typename\n }\n personas {\n ...Persona\n __typename\n }\n homepage\n __typename\n}\n\nfragment Persona on Persona {\n id\n name\n description\n bindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PersonaConfiguration\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PersonaConfiguration on PersonaConfiguration {\n all\n deployments {\n addOns\n clusters\n pipelines\n providers\n repositories\n services\n __typename\n }\n home {\n manager\n security\n __typename\n }\n flows {\n permissions\n startWorkbenchJob\n pipelines\n previews\n workbenches\n __typename\n }\n sidebar {\n audits\n flows\n kubernetes\n pullRequests\n settings\n backups\n stacks\n workbenches\n __typename\n }\n services {\n configuration\n secrets\n __typename\n }\n ai {\n pr\n __typename\n }\n __typename\n}\n\nfragment Manifest on PluralManifest {\n network {\n pluralDns\n subdomain\n __typename\n }\n cluster\n bucketPrefix\n __typename\n}\n\nfragment AvailableFeatures on AvailableFeatures {\n audits\n cd\n databaseManagement\n userManagement\n __typename\n}" }, "sha256:9ed81dd4c1ab018de19146e0696f8db0f57ff0c6c45c7cb73669dbad2a1f2e27": { "type": "query", diff --git a/assets/src/graph/home.graphql b/assets/src/graph/home.graphql index dd8af9cccd..a45a57d59e 100644 --- a/assets/src/graph/home.graphql +++ b/assets/src/graph/home.graphql @@ -148,8 +148,8 @@ query UpgradeStatistics($projectId: ID, $tag: TagInput) { } } -query ClusterHealthScores($projectId: ID) { - clusters(projectId: $projectId, first: 1000) { +query ClusterHealthScores($projectId: ID, $first: Int) { + clusters(projectId: $projectId, first: $first) { edges { node { ...ClusterHealthScore diff --git a/assets/src/graph/login.graphql b/assets/src/graph/login.graphql index 68a1b6c007..09db835080 100644 --- a/assets/src/graph/login.graphql +++ b/assets/src/graph/login.graphql @@ -57,6 +57,7 @@ query Me { installed consoleVersion sentryEnabled + healthmapClusterCount manifest { ...Manifest } diff --git a/charts/console/templates/secrets.yaml b/charts/console/templates/secrets.yaml index d3c936d0d4..a18fd57041 100644 --- a/charts/console/templates/secrets.yaml +++ b/charts/console/templates/secrets.yaml @@ -70,6 +70,9 @@ data: {{ if .Values.console.config.dbPoolSize }} DB_POOL_SIZE: {{ .Values.console.config.dbPoolSize | toString | b64enc | quote }} {{ end }} +{{ if .Values.console.config.healthmapClusterCount }} + CONSOLE_HEALTHMAP_CLUSTER_COUNT: {{ .Values.console.config.healthmapClusterCount | toString | b64enc | quote }} +{{ end }} {{ range $key, $value := $extraSecretEnv }} {{ $key }}: {{ $value | b64enc | quote }} {{ end }} diff --git a/charts/console/values.yaml b/charts/console/values.yaml index e8f2374708..359f97372f 100644 --- a/charts/console/values.yaml +++ b/charts/console/values.yaml @@ -88,6 +88,9 @@ console: # dbPoolSize configures the Ecto database connection pool size. dbPoolSize: 50 + # healthmapClusterCount configures the number of clusters to show in the healthmap. + healthmapClusterCount: ~ + # rdsIamAuthentication is used to configure whether rds iam authentication should be enabled for all db connections. Ensure your database is prepared to handle IAM authentication before enabling here. rdsIamAuthentication: false diff --git a/config/config.exs b/config/config.exs index f0daf59bec..5f6bdf48ea 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,6 +76,7 @@ config :console, oidc_sync: :upsert, refresh_token_expiry: "7d", workbench_default: false, + healthmap_cluster_count: 1000, qove_key: nil, cloud_override: "ignore", details: %{} diff --git a/go/client/models_gen.go b/go/client/models_gen.go index 6447ba707e..e561e9fea9 100644 --- a/go/client/models_gen.go +++ b/go/client/models_gen.go @@ -2868,17 +2868,18 @@ type ConsoleConfiguration struct { VpnEnabled *bool `json:"vpnEnabled,omitempty"` SentryEnabled *bool `json:"sentryEnabled,omitempty"` // whether at least one cluster has been installed, false if a user hasn't fully onboarded - Installed *bool `json:"installed,omitempty"` - Cloud *bool `json:"cloud,omitempty"` - Byok *bool `json:"byok,omitempty"` - ExternalOidc *bool `json:"externalOidc,omitempty"` - OidcName *string `json:"oidcName,omitempty"` - QoveKey *string `json:"qoveKey,omitempty"` - Features *AvailableFeatures `json:"features,omitempty"` - Details *ConsoleConfigurationDetails `json:"details,omitempty"` - LicenseExpiry *string `json:"licenseExpiry,omitempty"` - Manifest *PluralManifest `json:"manifest,omitempty"` - GitStatus *GitStatus `json:"gitStatus,omitempty"` + Installed *bool `json:"installed,omitempty"` + Cloud *bool `json:"cloud,omitempty"` + Byok *bool `json:"byok,omitempty"` + ExternalOidc *bool `json:"externalOidc,omitempty"` + OidcName *string `json:"oidcName,omitempty"` + HealthmapClusterCount *int64 `json:"healthmapClusterCount,omitempty"` + QoveKey *string `json:"qoveKey,omitempty"` + Features *AvailableFeatures `json:"features,omitempty"` + Details *ConsoleConfigurationDetails `json:"details,omitempty"` + LicenseExpiry *string `json:"licenseExpiry,omitempty"` + Manifest *PluralManifest `json:"manifest,omitempty"` + GitStatus *GitStatus `json:"gitStatus,omitempty"` } type ConsoleConfigurationDetails struct { diff --git a/lib/console/ai/chat/memory_engine.ex b/lib/console/ai/chat/memory_engine.ex index 13024dc991..7f56f9d91e 100644 --- a/lib/console/ai/chat/memory_engine.ex +++ b/lib/console/ai/chat/memory_engine.ex @@ -11,6 +11,8 @@ defmodule Console.AI.Chat.MemoryEngine do @type t :: %__MODULE__{} + @sleep 20 + defstruct [ :tools, :system_prompt, @@ -93,17 +95,17 @@ defmodule Console.AI.Chat.MemoryEngine do {:error, %ReqLLM.Error.API.Request{status: nil}} = err -> # almost certainly a llm provider failure, just retry Logger.warning "llm provider failure, retrying: #{inspect(err)}" - :timer.sleep(10) + sleep() loop(engine, iter + 1) {:error, %ReqLLM.Error.API.Request{status: s}} = err when s >= 500 -> Logger.error "llm provider HTTP 500, retrying: #{inspect(err)}" - :timer.sleep(10) + sleep() loop(engine, iter + 1) {:error, %ReqLLM.Error.API.Request{status: status, reason: body}} when status >= 400 and status < 500 -> {:error, "llm provider HTTP #{status} : #{Console.truncate(body, 200)}"} {:error, %ReqLLM.Error.API.Request{reason: reason}} -> Logger.warning "llm provider socket closed, retrying: #{inspect(reason)}" - :timer.sleep(10) + sleep() loop(engine, iter + 1) err -> err end @@ -204,6 +206,8 @@ defmodule Console.AI.Chat.MemoryEngine do defp msg_size({_, content, args}), do: byte_size(content <> Jason.encode!(Map.take(args, [:name, :arguments]))) defp msg_size(_), do: 0 + defp sleep(), do: :timer.sleep(@sleep + Console.jitter(@sleep)) + defp setup_toolsearch(%__MODULE__{tool_search: true, pre_enable: pre_enable} = engine), do: %__MODULE__{engine | enabled_tools: EnabledTools.new(engine.tools, pre_enable)} defp setup_toolsearch(engine), do: engine diff --git a/lib/console/configuration.ex b/lib/console/configuration.ex index d8e13036fe..fb363a7280 100644 --- a/lib/console/configuration.ex +++ b/lib/console/configuration.ex @@ -1,5 +1,5 @@ defmodule Console.Configuration do - defstruct [:git_commit, :is_demo_project, :is_sandbox, :plural_login, :vpn_enabled, :features, :sentry_enabled, :details] + defstruct [:git_commit, :is_demo_project, :is_sandbox, :plural_login, :vpn_enabled, :features, :sentry_enabled, :healthmap_cluster_count, :details] defmodule Details do defstruct [:assume_role_arn, :egress_ips] @@ -21,6 +21,7 @@ defmodule Console.Configuration do vpn_enabled: Console.Services.VPN.enabled?(), features: Console.Features.fetch(), sentry_enabled: !!Application.get_env(:sentry, :dsn), + healthmap_cluster_count: Application.get_env(:console, :healthmap_cluster_count) || 1000, details: Details.new(Application.get_env(:console, :details) || %{}), } end diff --git a/lib/console/graphql/configuration.ex b/lib/console/graphql/configuration.ex index 3bacf18480..2f72fc1683 100644 --- a/lib/console/graphql/configuration.ex +++ b/lib/console/graphql/configuration.ex @@ -38,20 +38,22 @@ defmodule Console.GraphQl.Configuration do end object :console_configuration do - field :git_commit, :string - field :console_version, :string, resolve: fn _, _, _ -> {:ok, Console.version()} end - field :is_demo_project, :boolean - field :is_sandbox, :boolean - field :plural_login, :boolean - field :vpn_enabled, :boolean - field :sentry_enabled, :boolean - field :installed, :boolean, + field :git_commit, :string + field :console_version, :string, resolve: fn _, _, _ -> {:ok, Console.version()} end + field :is_demo_project, :boolean + field :is_sandbox, :boolean + field :plural_login, :boolean + field :vpn_enabled, :boolean + field :sentry_enabled, :boolean + field :installed, :boolean, resolve: fn _, _, _ -> {:ok, Console.Deployments.Clusters.installed?()} end, description: "whether at least one cluster has been installed, false if a user hasn't fully onboarded" - field :cloud, :boolean, resolve: fn _, _, _ -> {:ok, Console.cloud?()} end - field :byok, :boolean, resolve: fn _, _, _ -> {:ok, Console.byok?()} end - field :external_oidc, :boolean, resolve: fn _, _, _ -> {:ok, !!Console.conf(:oidc_login)} end - field :oidc_name, :string, resolve: fn _, _, _ -> {:ok, Console.conf(:oidc_name)} end + field :cloud, :boolean, resolve: fn _, _, _ -> {:ok, Console.cloud?()} end + field :byok, :boolean, resolve: fn _, _, _ -> {:ok, Console.byok?()} end + field :external_oidc, :boolean, resolve: fn _, _, _ -> {:ok, !!Console.conf(:oidc_login)} end + field :oidc_name, :string, resolve: fn _, _, _ -> {:ok, Console.conf(:oidc_name)} end + field :healthmap_cluster_count, :integer + field :qove_key, :string, resolve: fn _, _, _ -> case Console.conf(:qove_key) do @@ -69,6 +71,7 @@ defmodule Console.GraphQl.Configuration do field :details, :console_configuration_details field :license_expiry, :datetime, resolve: fn _, _, _ -> {:ok, Console.Features.expiry()} end + field :manifest, :plural_manifest, resolve: fn _, _, _ -> case Console.Plural.Manifest.get() do diff --git a/rel/runtime.exs b/rel/runtime.exs index 2b0e428d38..6f1e6e77d3 100644 --- a/rel/runtime.exs +++ b/rel/runtime.exs @@ -82,6 +82,10 @@ if get_env("CONSOLE_CACHE_AGENT_QUEUE_LIMIT") do config :console, :cache_agent_queue_limit, String.to_integer(get_env("CONSOLE_CACHE_AGENT_QUEUE_LIMIT")) end +if get_env("CONSOLE_HEALTHMAP_CLUSTER_COUNT") do + config :console, :healthmap_cluster_count, String.to_integer(get_env("CONSOLE_HEALTHMAP_CLUSTER_COUNT")) +end + if get_env("CONSOLE_VERSION") do config :console, :version, get_env("CONSOLE_VERSION") end diff --git a/schema/schema.graphql b/schema/schema.graphql index 7b720ba43a..a8f7075c88 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -16412,6 +16412,8 @@ type ConsoleConfiguration { oidcName: String + healthmapClusterCount: Int + qoveKey: String features: AvailableFeatures From 6a3eff40c33f1e879e6817f48908f6135f207f00 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Fri, 24 Jul 2026 18:03:18 -0400 Subject: [PATCH 7/8] fix health map display --- assets/src/components/cd/clusters/ClustersCharts.tsx | 12 ++++++++++-- assets/src/components/home/Home.tsx | 8 ++++++-- .../clusteroverview/ClusterHealthScoresHeatmap.tsx | 5 +++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/assets/src/components/cd/clusters/ClustersCharts.tsx b/assets/src/components/cd/clusters/ClustersCharts.tsx index d28972b6cb..b785f7567a 100644 --- a/assets/src/components/cd/clusters/ClustersCharts.tsx +++ b/assets/src/components/cd/clusters/ClustersCharts.tsx @@ -1,8 +1,12 @@ import { Button, CaretUpIcon } from '@pluralsh/design-system' import { SimpleAccordion } from 'components/ai/chatbot/multithread/MultiThreadViewerMessage' import { POLL_INTERVAL } from 'components/cd/ContinuousDeployment' +import { useLogin } from 'components/contexts' import { useProjectId } from 'components/contexts/ProjectsContext' -import { ClusterHealthScoresHeatmap } from 'components/home/clusteroverview/ClusterHealthScoresHeatmap' +import { + ClusterHealthScoresHeatmap, + normalizeHealthmapClusterCount, +} from 'components/home/clusteroverview/ClusterHealthScoresHeatmap' import { ClusterUpgradesChart, UpgradeChartFilter, @@ -56,10 +60,14 @@ export function ClustersCharts({ } function ClusterChartsContent() { + const { configuration } = useLogin() const projectId = useProjectId() + const healthmapClusterCount = normalizeHealthmapClusterCount( + configuration?.healthmapClusterCount + ) const { data: healthScoresData } = useClusterHealthScoresQuery({ - variables: { projectId }, + variables: { projectId, first: healthmapClusterCount }, fetchPolicy: 'cache-and-network', pollInterval: POLL_INTERVAL, }) diff --git a/assets/src/components/home/Home.tsx b/assets/src/components/home/Home.tsx index a9123ad3ca..f051ae1dd7 100644 --- a/assets/src/components/home/Home.tsx +++ b/assets/src/components/home/Home.tsx @@ -26,8 +26,9 @@ import { aggregateHealthScoreStats, ClusterHealthScoresFilterBtns, ClusterHealthScoresHeatmap, - HealthScoreFilterLabel, healthScoreLabelToRange, + HealthScoreFilterLabel, + normalizeHealthmapClusterCount, } from './clusteroverview/ClusterHealthScoresHeatmap.tsx' import { aggregateUpgradeStats, @@ -76,11 +77,14 @@ function HomeClusters() { const [selectedCluster, setSelectedCluster] = useState>(null) + const healthmapClusterCount = normalizeHealthmapClusterCount( + configuration?.healthmapClusterCount + ) const { data: healthScoresData } = useClusterHealthScoresQuery({ variables: { projectId, - first: configuration?.healthmapClusterCount ?? 1000, + first: healthmapClusterCount, }, fetchPolicy: 'cache-and-network', pollInterval: POLL_INTERVAL, diff --git a/assets/src/components/home/clusteroverview/ClusterHealthScoresHeatmap.tsx b/assets/src/components/home/clusteroverview/ClusterHealthScoresHeatmap.tsx index 515b68d26a..fc1ee9f853 100644 --- a/assets/src/components/home/clusteroverview/ClusterHealthScoresHeatmap.tsx +++ b/assets/src/components/home/clusteroverview/ClusterHealthScoresHeatmap.tsx @@ -12,6 +12,11 @@ import { ChartLegendItem, HomeFilterOptionCard } from '../HomeFilterOptionCard' import { EmptyHeatmapSvg } from './ClusterHealthScoresHeatmapEmpty' export type HealthScoreFilterLabel = keyof typeof healthScoreLabelToRange +export const DEFAULT_HEALTHMAP_CLUSTER_COUNT = 1000 + +export const normalizeHealthmapClusterCount = ( + count: Nullable +): number => (count && count > 0 ? count : DEFAULT_HEALTHMAP_CLUSTER_COUNT) export function ClusterHealthScoresHeatmap({ clusters, From 034907fb95d2b836c655094c518b20ac5b72f228 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Sat, 25 Jul 2026 10:29:13 -0400 Subject: [PATCH 8/8] add sentinel tools and subagent setup --- assets/src/generated/graphql.ts | 13 ++- .../generated/persisted-queries/client.json | 12 +- assets/src/graph/workbench.graphql | 1 + charts/console/values.yaml | 6 +- config/config.exs | 2 +- .../workbench/sentinel/fetch_sentinel_run.ex | 55 +++++++++ .../sentinel/fetch_sentinel_run_job.ex | 58 ++++++++++ .../workbench/sentinel/list_sentinels.ex | 52 +++++++++ .../tools/workbench/sentinel/run_sentinel.ex | 53 +++++++++ lib/console/ai/workbench/engine.ex | 5 +- lib/console/ai/workbench/subagents/verify.ex | 62 +++++++++- lib/console/graphql/deployments/workbench.ex | 2 + lib/console/schema/sentinel.ex | 4 + lib/console/schema/workbench.ex | 3 +- priv/prompts/workbench/job.md.eex | 10 ++ .../prompts/workbench/sentinel/run_job.md.eex | 21 ++++ .../sentinel/fetch_sentinel_run.json | 10 ++ .../sentinel/fetch_sentinel_run_job.json | 10 ++ .../workbench/sentinel/list_sentinels.json | 19 ++++ .../workbench/sentinel/run_sentinel.json | 27 +++++ schema/schema.graphql | 6 + .../tools/workbench/sentinel/tools_test.exs | 107 ++++++++++++++++++ 22 files changed, 521 insertions(+), 17 deletions(-) create mode 100644 lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run.ex create mode 100644 lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run_job.ex create mode 100644 lib/console/ai/tools/workbench/sentinel/list_sentinels.ex create mode 100644 lib/console/ai/tools/workbench/sentinel/run_sentinel.ex create mode 100644 priv/prompts/workbench/sentinel/run_job.md.eex create mode 100644 priv/tools/workbench/sentinel/fetch_sentinel_run.json create mode 100644 priv/tools/workbench/sentinel/fetch_sentinel_run_job.json create mode 100644 priv/tools/workbench/sentinel/list_sentinels.json create mode 100644 priv/tools/workbench/sentinel/run_sentinel.json create mode 100644 test/console/ai/tools/workbench/sentinel/tools_test.exs diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 59a173076f..b06a6a80e7 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -15916,6 +15916,8 @@ export type WorkbenchInfrastructure = { kubernetes?: Maybe; /** pod logs capability enabled */ podLogs?: Maybe; + /** sentinels capability enabled */ + sentinels?: Maybe; /** services capability enabled */ services?: Maybe; /** stacks capability enabled */ @@ -15929,6 +15931,8 @@ export type WorkbenchInfrastructureAttributes = { kubernetes?: InputMaybe; /** enable pod logs capability */ podLogs?: InputMaybe; + /** enable sentinels capability */ + sentinels?: InputMaybe; /** enable services capability */ services?: InputMaybe; /** enable stacks capability */ @@ -21080,7 +21084,7 @@ export type IssueWebhookTinyFragment = { __typename?: 'IssueWebhook', id: string export type WorkbenchWebhookTinyFragment = { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null }; -export type WorkbenchFragment = { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null }; +export type WorkbenchFragment = { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null, sentinels?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null }; export type WorkbenchToolTinyFragment = { __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null }; @@ -21196,7 +21200,7 @@ export type WorkbenchQueryVariables = Exact<{ }>; -export type WorkbenchQuery = { __typename?: 'RootQueryType', workbench?: { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null } | null }; +export type WorkbenchQuery = { __typename?: 'RootQueryType', workbench?: { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null, sentinels?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null } | null }; export type WorkbenchAccessibleUserFragment = { __typename?: 'User', id: string, name: string, email: string, profile?: string | null }; @@ -21473,7 +21477,7 @@ export type CreateWorkbenchMutationVariables = Exact<{ }>; -export type CreateWorkbenchMutation = { __typename?: 'RootMutationType', createWorkbench?: { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null } | null }; +export type CreateWorkbenchMutation = { __typename?: 'RootMutationType', createWorkbench?: { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null, sentinels?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null } | null }; export type UpdateWorkbenchMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -21481,7 +21485,7 @@ export type UpdateWorkbenchMutationVariables = Exact<{ }>; -export type UpdateWorkbenchMutation = { __typename?: 'RootMutationType', updateWorkbench?: { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null } | null }; +export type UpdateWorkbenchMutation = { __typename?: 'RootMutationType', updateWorkbench?: { __typename?: 'Workbench', systemPrompt?: string | null, id: string, name: string, description?: string | null, agentRuntime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, repository?: { __typename?: 'GitRepository', id: string } | null, configuration?: { __typename?: 'WorkbenchConfiguration', infrastructure?: { __typename?: 'WorkbenchInfrastructure', services?: boolean | null, stacks?: boolean | null, kubernetes?: boolean | null, podLogs?: boolean | null, vulnerabilities?: boolean | null, sentinels?: boolean | null } | null, observability?: { __typename?: 'WorkbenchObservability', logs?: boolean | null, metrics?: boolean | null } | null, coding?: { __typename?: 'WorkbenchCoding', mode?: AgentRunMode | null, repositories?: Array | null, enableBabysitting?: boolean | null } | null } | null, modes?: { __typename?: 'WorkbenchJobModes', plan?: boolean | null, model?: { __typename?: 'WorkbenchJobModel', provider?: AiProvider | null, model?: string | null } | null, coding?: { __typename?: 'WorkbenchJobCodingModes', approval?: boolean | null, babysit?: boolean | null } | null, budget?: { __typename?: 'WorkbenchJobBudget', cost?: number | null, tokens?: number | null } | null } | null, skills?: { __typename?: 'WorkbenchSkills', files?: Array | null, ref?: { __typename?: 'GitRef', ref: string, folder: string } | null } | null, workbenchSkills?: { __typename?: 'WorkbenchSkillConnection', edges?: Array<{ __typename?: 'WorkbenchSkillEdge', node?: { __typename?: 'WorkbenchSkill', id: string, name?: string | null, description?: string | null, contents?: string | null, subagents?: Array | null } | null } | null> | null } | null, tools?: Array<{ __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, scmConnection?: { __typename?: 'ScmConnection', id: string, name: string, type: ScmType } | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, opensearch?: { __typename?: 'WorkbenchToolOpensearchConnection', host: string, index: string, awsAccessKeyId?: string | null, awsRegion?: string | null, assumeRoleArn?: string | null, usePodIdentity?: boolean | null } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null, awsSigv4?: boolean | null, awsAccessKeyId?: string | null, awsRegion?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, jaeger?: { __typename?: 'WorkbenchToolJaegerConnection', url?: string | null, username?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, slack?: { __typename?: 'WorkbenchToolSlackConnection', url: string } | null, pagerduty?: { __typename?: 'WorkbenchToolPagerdutyConnection', url: string } | null, teams?: { __typename?: 'WorkbenchToolTeamsConnection', clientId?: string | null, tenantId?: string | null } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null, cloudwatch?: { __typename?: 'WorkbenchToolCloudwatchConnection', logGroupNames?: Array | null, region?: string | null, roleArn?: string | null, roleSessionName?: string | null } | null, azure?: { __typename?: 'WorkbenchToolAzureConnection', subscriptionId?: string | null, tenantId?: string | null, clientId?: string | null, prometheusUrl?: string | null } | null, dynatrace?: { __typename?: 'WorkbenchToolDynatraceConnection', url?: string | null } | null, sentry?: { __typename?: 'WorkbenchToolSentryConnection', url?: string | null } | null, github?: { __typename?: 'WorkbenchToolGithubConnection', url: string, toolset?: string | null, appId?: string | null, installationId?: string | null } | null, gitlab?: { __typename?: 'WorkbenchToolGitlabConnection', url?: string | null } | null, bitbucket?: { __typename?: 'WorkbenchToolBitbucketConnection', url?: string | null } | null, bitbucketDatacenter?: { __typename?: 'WorkbenchToolBitbucketDatacenterConnection', url?: string | null } | null, azureDevops?: { __typename?: 'WorkbenchToolAzureDevopsConnection', url?: string | null } | null, docker?: { __typename?: 'WorkbenchToolDockerConnection', url?: string | null, provider?: HelmAuthProvider | null, proxy?: { __typename?: 'HttpProxyConfiguration', url: string, noproxy?: string | null } | null } | null } | null, cloudConnection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, mcpServer?: { __typename?: 'McpServer', id: string, name: string, url: string } | null } | null> | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, botUser?: { __typename?: 'User', id: string, name: string, email: string, profile?: string | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, priority?: number | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null } | null }; export type CreateWorkbenchEvalMutationVariables = Exact<{ workbenchId: Scalars['ID']['input']; @@ -26867,6 +26871,7 @@ export const WorkbenchFragmentDoc = gql` kubernetes podLogs vulnerabilities + sentinels } observability { logs diff --git a/assets/src/generated/persisted-queries/client.json b/assets/src/generated/persisted-queries/client.json index e6595b1351..bc0a79f7bc 100644 --- a/assets/src/generated/persisted-queries/client.json +++ b/assets/src/generated/persisted-queries/client.json @@ -1872,10 +1872,10 @@ "name": "WorkbenchesAlerts", "body": "query WorkbenchesAlerts($first: Int = 100, $after: String) {\n workbenchAlerts(first: $first, after: $after) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...Alert\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment Alert on Alert {\n id\n title\n message\n type\n severity\n state\n fingerprint\n url\n annotations\n tags {\n id\n name\n value\n __typename\n }\n insight {\n ...AiInsight\n __typename\n }\n resolution {\n ...AlertResolution\n __typename\n }\n workbench {\n id\n __typename\n }\n workbenchJob {\n id\n status\n __typename\n }\n updatedAt\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment AlertResolution on AlertResolution {\n resolution\n __typename\n}" }, - "sha256:d681cc4a1cb90b28bd22b3d7b3ea9cbc2c9a9549dcd062f6e86f6e2a543a589f": { + "sha256:aa328c3e124ed36f972175977471007c6d56b07e270cba09241b8f2af379b8b6": { "type": "query", "name": "Workbench", - "body": "query Workbench($id: ID, $name: String) {\n workbench(id: $id, name: $name) {\n ...Workbench\n __typename\n }\n}\n\nfragment Workbench on Workbench {\n ...WorkbenchTiny\n systemPrompt\n agentRuntime {\n id\n name\n __typename\n }\n repository {\n id\n __typename\n }\n configuration {\n infrastructure {\n services\n stacks\n kubernetes\n podLogs\n vulnerabilities\n __typename\n }\n observability {\n logs\n metrics\n __typename\n }\n coding {\n mode\n repositories\n enableBabysitting\n __typename\n }\n __typename\n }\n modes {\n plan\n model {\n provider\n model\n __typename\n }\n coding {\n approval\n babysit\n __typename\n }\n budget {\n cost\n tokens\n __typename\n }\n __typename\n }\n skills {\n ref {\n ref\n folder\n __typename\n }\n files\n __typename\n }\n workbenchSkills(first: 500) {\n edges {\n node {\n id\n name\n description\n contents\n subagents\n __typename\n }\n __typename\n }\n __typename\n }\n tools {\n ...WorkbenchTool\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n botUser {\n id\n name\n email\n profile\n __typename\n }\n __typename\n}\n\nfragment WorkbenchTiny on Workbench {\n id\n name\n description\n agentRuntime {\n id\n name\n type\n __typename\n }\n tools {\n ...WorkbenchToolTiny\n __typename\n }\n webhooks(first: 50) {\n edges {\n node {\n ...WorkbenchWebhookTiny\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchToolTiny on WorkbenchTool {\n id\n name\n tool\n categories\n cloudConnection {\n ...CloudConnectionTiny\n __typename\n }\n mcpServer {\n id\n name\n url\n __typename\n }\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}\n\nfragment WorkbenchWebhookTiny on WorkbenchWebhook {\n id\n name\n priority\n webhook {\n id\n type\n __typename\n }\n issueWebhook {\n ...IssueWebhookTiny\n __typename\n }\n __typename\n}\n\nfragment IssueWebhookTiny on IssueWebhook {\n id\n provider\n __typename\n}\n\nfragment WorkbenchTool on WorkbenchTool {\n ...WorkbenchToolTiny\n scmConnection {\n id\n name\n type\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n http {\n url\n method\n headers {\n name\n value\n __typename\n }\n body\n inputSchema\n __typename\n }\n datadog {\n site\n __typename\n }\n elastic {\n index\n url\n username\n __typename\n }\n opensearch {\n host\n index\n awsAccessKeyId\n awsRegion\n assumeRoleArn\n usePodIdentity\n __typename\n }\n loki {\n url\n username\n tenantId\n __typename\n }\n prometheus {\n url\n username\n tenantId\n awsSigv4\n awsAccessKeyId\n awsRegion\n __typename\n }\n tempo {\n url\n username\n tenantId\n __typename\n }\n jaeger {\n url\n username\n __typename\n }\n atlassian {\n email\n url\n __typename\n }\n linear {\n url\n __typename\n }\n slack {\n url\n __typename\n }\n pagerduty {\n url\n __typename\n }\n teams {\n clientId\n tenantId\n __typename\n }\n splunk {\n url\n username\n __typename\n }\n cloudwatch {\n logGroupNames\n region\n roleArn\n roleSessionName\n __typename\n }\n azure {\n subscriptionId\n tenantId\n clientId\n prometheusUrl\n __typename\n }\n dynatrace {\n url\n __typename\n }\n sentry {\n url\n __typename\n }\n github {\n url\n toolset\n appId\n installationId\n __typename\n }\n gitlab {\n url\n __typename\n }\n bitbucket {\n url\n __typename\n }\n bitbucketDatacenter {\n url\n __typename\n }\n azureDevops {\n url\n __typename\n }\n docker {\n url\n provider\n proxy {\n url\n noproxy\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" + "body": "query Workbench($id: ID, $name: String) {\n workbench(id: $id, name: $name) {\n ...Workbench\n __typename\n }\n}\n\nfragment Workbench on Workbench {\n ...WorkbenchTiny\n systemPrompt\n agentRuntime {\n id\n name\n __typename\n }\n repository {\n id\n __typename\n }\n configuration {\n infrastructure {\n services\n stacks\n kubernetes\n podLogs\n vulnerabilities\n sentinels\n __typename\n }\n observability {\n logs\n metrics\n __typename\n }\n coding {\n mode\n repositories\n enableBabysitting\n __typename\n }\n __typename\n }\n modes {\n plan\n model {\n provider\n model\n __typename\n }\n coding {\n approval\n babysit\n __typename\n }\n budget {\n cost\n tokens\n __typename\n }\n __typename\n }\n skills {\n ref {\n ref\n folder\n __typename\n }\n files\n __typename\n }\n workbenchSkills(first: 500) {\n edges {\n node {\n id\n name\n description\n contents\n subagents\n __typename\n }\n __typename\n }\n __typename\n }\n tools {\n ...WorkbenchTool\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n botUser {\n id\n name\n email\n profile\n __typename\n }\n __typename\n}\n\nfragment WorkbenchTiny on Workbench {\n id\n name\n description\n agentRuntime {\n id\n name\n type\n __typename\n }\n tools {\n ...WorkbenchToolTiny\n __typename\n }\n webhooks(first: 50) {\n edges {\n node {\n ...WorkbenchWebhookTiny\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchToolTiny on WorkbenchTool {\n id\n name\n tool\n categories\n cloudConnection {\n ...CloudConnectionTiny\n __typename\n }\n mcpServer {\n id\n name\n url\n __typename\n }\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}\n\nfragment WorkbenchWebhookTiny on WorkbenchWebhook {\n id\n name\n priority\n webhook {\n id\n type\n __typename\n }\n issueWebhook {\n ...IssueWebhookTiny\n __typename\n }\n __typename\n}\n\nfragment IssueWebhookTiny on IssueWebhook {\n id\n provider\n __typename\n}\n\nfragment WorkbenchTool on WorkbenchTool {\n ...WorkbenchToolTiny\n scmConnection {\n id\n name\n type\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n http {\n url\n method\n headers {\n name\n value\n __typename\n }\n body\n inputSchema\n __typename\n }\n datadog {\n site\n __typename\n }\n elastic {\n index\n url\n username\n __typename\n }\n opensearch {\n host\n index\n awsAccessKeyId\n awsRegion\n assumeRoleArn\n usePodIdentity\n __typename\n }\n loki {\n url\n username\n tenantId\n __typename\n }\n prometheus {\n url\n username\n tenantId\n awsSigv4\n awsAccessKeyId\n awsRegion\n __typename\n }\n tempo {\n url\n username\n tenantId\n __typename\n }\n jaeger {\n url\n username\n __typename\n }\n atlassian {\n email\n url\n __typename\n }\n linear {\n url\n __typename\n }\n slack {\n url\n __typename\n }\n pagerduty {\n url\n __typename\n }\n teams {\n clientId\n tenantId\n __typename\n }\n splunk {\n url\n username\n __typename\n }\n cloudwatch {\n logGroupNames\n region\n roleArn\n roleSessionName\n __typename\n }\n azure {\n subscriptionId\n tenantId\n clientId\n prometheusUrl\n __typename\n }\n dynatrace {\n url\n __typename\n }\n sentry {\n url\n __typename\n }\n github {\n url\n toolset\n appId\n installationId\n __typename\n }\n gitlab {\n url\n __typename\n }\n bitbucket {\n url\n __typename\n }\n bitbucketDatacenter {\n url\n __typename\n }\n azureDevops {\n url\n __typename\n }\n docker {\n url\n provider\n proxy {\n url\n noproxy\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" }, "sha256:6c3686838d1372371451664e8d6b593929eeca76e340065ed6dda5691e325b56": { "type": "query", @@ -2042,15 +2042,15 @@ "name": "WorkbenchTool", "body": "query WorkbenchTool($id: ID, $name: String) {\n workbenchTool(id: $id, name: $name) {\n ...WorkbenchTool\n __typename\n }\n}\n\nfragment WorkbenchTool on WorkbenchTool {\n ...WorkbenchToolTiny\n scmConnection {\n id\n name\n type\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n http {\n url\n method\n headers {\n name\n value\n __typename\n }\n body\n inputSchema\n __typename\n }\n datadog {\n site\n __typename\n }\n elastic {\n index\n url\n username\n __typename\n }\n opensearch {\n host\n index\n awsAccessKeyId\n awsRegion\n assumeRoleArn\n usePodIdentity\n __typename\n }\n loki {\n url\n username\n tenantId\n __typename\n }\n prometheus {\n url\n username\n tenantId\n awsSigv4\n awsAccessKeyId\n awsRegion\n __typename\n }\n tempo {\n url\n username\n tenantId\n __typename\n }\n jaeger {\n url\n username\n __typename\n }\n atlassian {\n email\n url\n __typename\n }\n linear {\n url\n __typename\n }\n slack {\n url\n __typename\n }\n pagerduty {\n url\n __typename\n }\n teams {\n clientId\n tenantId\n __typename\n }\n splunk {\n url\n username\n __typename\n }\n cloudwatch {\n logGroupNames\n region\n roleArn\n roleSessionName\n __typename\n }\n azure {\n subscriptionId\n tenantId\n clientId\n prometheusUrl\n __typename\n }\n dynatrace {\n url\n __typename\n }\n sentry {\n url\n __typename\n }\n github {\n url\n toolset\n appId\n installationId\n __typename\n }\n gitlab {\n url\n __typename\n }\n bitbucket {\n url\n __typename\n }\n bitbucketDatacenter {\n url\n __typename\n }\n azureDevops {\n url\n __typename\n }\n docker {\n url\n provider\n proxy {\n url\n noproxy\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchToolTiny on WorkbenchTool {\n id\n name\n tool\n categories\n cloudConnection {\n ...CloudConnectionTiny\n __typename\n }\n mcpServer {\n id\n name\n url\n __typename\n }\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" }, - "sha256:0f553c65a6d78cb36aed5133a3fe09652423546808d1b7844488bbeac47be7b7": { + "sha256:e27e92588b960f6c1c7f843fb48daddcb1cd6ce4b2409afa7ea4e6e5891d1506": { "type": "mutation", "name": "CreateWorkbench", - "body": "mutation CreateWorkbench($attributes: WorkbenchAttributes!) {\n createWorkbench(attributes: $attributes) {\n ...Workbench\n __typename\n }\n}\n\nfragment Workbench on Workbench {\n ...WorkbenchTiny\n systemPrompt\n agentRuntime {\n id\n name\n __typename\n }\n repository {\n id\n __typename\n }\n configuration {\n infrastructure {\n services\n stacks\n kubernetes\n podLogs\n vulnerabilities\n __typename\n }\n observability {\n logs\n metrics\n __typename\n }\n coding {\n mode\n repositories\n enableBabysitting\n __typename\n }\n __typename\n }\n modes {\n plan\n model {\n provider\n model\n __typename\n }\n coding {\n approval\n babysit\n __typename\n }\n budget {\n cost\n tokens\n __typename\n }\n __typename\n }\n skills {\n ref {\n ref\n folder\n __typename\n }\n files\n __typename\n }\n workbenchSkills(first: 500) {\n edges {\n node {\n id\n name\n description\n contents\n subagents\n __typename\n }\n __typename\n }\n __typename\n }\n tools {\n ...WorkbenchTool\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n botUser {\n id\n name\n email\n profile\n __typename\n }\n __typename\n}\n\nfragment WorkbenchTiny on Workbench {\n id\n name\n description\n agentRuntime {\n id\n name\n type\n __typename\n }\n tools {\n ...WorkbenchToolTiny\n __typename\n }\n webhooks(first: 50) {\n edges {\n node {\n ...WorkbenchWebhookTiny\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchToolTiny on WorkbenchTool {\n id\n name\n tool\n categories\n cloudConnection {\n ...CloudConnectionTiny\n __typename\n }\n mcpServer {\n id\n name\n url\n __typename\n }\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}\n\nfragment WorkbenchWebhookTiny on WorkbenchWebhook {\n id\n name\n priority\n webhook {\n id\n type\n __typename\n }\n issueWebhook {\n ...IssueWebhookTiny\n __typename\n }\n __typename\n}\n\nfragment IssueWebhookTiny on IssueWebhook {\n id\n provider\n __typename\n}\n\nfragment WorkbenchTool on WorkbenchTool {\n ...WorkbenchToolTiny\n scmConnection {\n id\n name\n type\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n http {\n url\n method\n headers {\n name\n value\n __typename\n }\n body\n inputSchema\n __typename\n }\n datadog {\n site\n __typename\n }\n elastic {\n index\n url\n username\n __typename\n }\n opensearch {\n host\n index\n awsAccessKeyId\n awsRegion\n assumeRoleArn\n usePodIdentity\n __typename\n }\n loki {\n url\n username\n tenantId\n __typename\n }\n prometheus {\n url\n username\n tenantId\n awsSigv4\n awsAccessKeyId\n awsRegion\n __typename\n }\n tempo {\n url\n username\n tenantId\n __typename\n }\n jaeger {\n url\n username\n __typename\n }\n atlassian {\n email\n url\n __typename\n }\n linear {\n url\n __typename\n }\n slack {\n url\n __typename\n }\n pagerduty {\n url\n __typename\n }\n teams {\n clientId\n tenantId\n __typename\n }\n splunk {\n url\n username\n __typename\n }\n cloudwatch {\n logGroupNames\n region\n roleArn\n roleSessionName\n __typename\n }\n azure {\n subscriptionId\n tenantId\n clientId\n prometheusUrl\n __typename\n }\n dynatrace {\n url\n __typename\n }\n sentry {\n url\n __typename\n }\n github {\n url\n toolset\n appId\n installationId\n __typename\n }\n gitlab {\n url\n __typename\n }\n bitbucket {\n url\n __typename\n }\n bitbucketDatacenter {\n url\n __typename\n }\n azureDevops {\n url\n __typename\n }\n docker {\n url\n provider\n proxy {\n url\n noproxy\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" + "body": "mutation CreateWorkbench($attributes: WorkbenchAttributes!) {\n createWorkbench(attributes: $attributes) {\n ...Workbench\n __typename\n }\n}\n\nfragment Workbench on Workbench {\n ...WorkbenchTiny\n systemPrompt\n agentRuntime {\n id\n name\n __typename\n }\n repository {\n id\n __typename\n }\n configuration {\n infrastructure {\n services\n stacks\n kubernetes\n podLogs\n vulnerabilities\n sentinels\n __typename\n }\n observability {\n logs\n metrics\n __typename\n }\n coding {\n mode\n repositories\n enableBabysitting\n __typename\n }\n __typename\n }\n modes {\n plan\n model {\n provider\n model\n __typename\n }\n coding {\n approval\n babysit\n __typename\n }\n budget {\n cost\n tokens\n __typename\n }\n __typename\n }\n skills {\n ref {\n ref\n folder\n __typename\n }\n files\n __typename\n }\n workbenchSkills(first: 500) {\n edges {\n node {\n id\n name\n description\n contents\n subagents\n __typename\n }\n __typename\n }\n __typename\n }\n tools {\n ...WorkbenchTool\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n botUser {\n id\n name\n email\n profile\n __typename\n }\n __typename\n}\n\nfragment WorkbenchTiny on Workbench {\n id\n name\n description\n agentRuntime {\n id\n name\n type\n __typename\n }\n tools {\n ...WorkbenchToolTiny\n __typename\n }\n webhooks(first: 50) {\n edges {\n node {\n ...WorkbenchWebhookTiny\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchToolTiny on WorkbenchTool {\n id\n name\n tool\n categories\n cloudConnection {\n ...CloudConnectionTiny\n __typename\n }\n mcpServer {\n id\n name\n url\n __typename\n }\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}\n\nfragment WorkbenchWebhookTiny on WorkbenchWebhook {\n id\n name\n priority\n webhook {\n id\n type\n __typename\n }\n issueWebhook {\n ...IssueWebhookTiny\n __typename\n }\n __typename\n}\n\nfragment IssueWebhookTiny on IssueWebhook {\n id\n provider\n __typename\n}\n\nfragment WorkbenchTool on WorkbenchTool {\n ...WorkbenchToolTiny\n scmConnection {\n id\n name\n type\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n http {\n url\n method\n headers {\n name\n value\n __typename\n }\n body\n inputSchema\n __typename\n }\n datadog {\n site\n __typename\n }\n elastic {\n index\n url\n username\n __typename\n }\n opensearch {\n host\n index\n awsAccessKeyId\n awsRegion\n assumeRoleArn\n usePodIdentity\n __typename\n }\n loki {\n url\n username\n tenantId\n __typename\n }\n prometheus {\n url\n username\n tenantId\n awsSigv4\n awsAccessKeyId\n awsRegion\n __typename\n }\n tempo {\n url\n username\n tenantId\n __typename\n }\n jaeger {\n url\n username\n __typename\n }\n atlassian {\n email\n url\n __typename\n }\n linear {\n url\n __typename\n }\n slack {\n url\n __typename\n }\n pagerduty {\n url\n __typename\n }\n teams {\n clientId\n tenantId\n __typename\n }\n splunk {\n url\n username\n __typename\n }\n cloudwatch {\n logGroupNames\n region\n roleArn\n roleSessionName\n __typename\n }\n azure {\n subscriptionId\n tenantId\n clientId\n prometheusUrl\n __typename\n }\n dynatrace {\n url\n __typename\n }\n sentry {\n url\n __typename\n }\n github {\n url\n toolset\n appId\n installationId\n __typename\n }\n gitlab {\n url\n __typename\n }\n bitbucket {\n url\n __typename\n }\n bitbucketDatacenter {\n url\n __typename\n }\n azureDevops {\n url\n __typename\n }\n docker {\n url\n provider\n proxy {\n url\n noproxy\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" }, - "sha256:d08f4d9610c661062614b67dff3f9b5bb1305e6f88d993c41698a78d7c044914": { + "sha256:20cd5db0a3983f865eb9c0db8ffa21370efcc7d9b7cf2526c7a6213f016ecb93": { "type": "mutation", "name": "UpdateWorkbench", - "body": "mutation UpdateWorkbench($id: ID!, $attributes: WorkbenchAttributes!) {\n updateWorkbench(id: $id, attributes: $attributes) {\n ...Workbench\n __typename\n }\n}\n\nfragment Workbench on Workbench {\n ...WorkbenchTiny\n systemPrompt\n agentRuntime {\n id\n name\n __typename\n }\n repository {\n id\n __typename\n }\n configuration {\n infrastructure {\n services\n stacks\n kubernetes\n podLogs\n vulnerabilities\n __typename\n }\n observability {\n logs\n metrics\n __typename\n }\n coding {\n mode\n repositories\n enableBabysitting\n __typename\n }\n __typename\n }\n modes {\n plan\n model {\n provider\n model\n __typename\n }\n coding {\n approval\n babysit\n __typename\n }\n budget {\n cost\n tokens\n __typename\n }\n __typename\n }\n skills {\n ref {\n ref\n folder\n __typename\n }\n files\n __typename\n }\n workbenchSkills(first: 500) {\n edges {\n node {\n id\n name\n description\n contents\n subagents\n __typename\n }\n __typename\n }\n __typename\n }\n tools {\n ...WorkbenchTool\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n botUser {\n id\n name\n email\n profile\n __typename\n }\n __typename\n}\n\nfragment WorkbenchTiny on Workbench {\n id\n name\n description\n agentRuntime {\n id\n name\n type\n __typename\n }\n tools {\n ...WorkbenchToolTiny\n __typename\n }\n webhooks(first: 50) {\n edges {\n node {\n ...WorkbenchWebhookTiny\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchToolTiny on WorkbenchTool {\n id\n name\n tool\n categories\n cloudConnection {\n ...CloudConnectionTiny\n __typename\n }\n mcpServer {\n id\n name\n url\n __typename\n }\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}\n\nfragment WorkbenchWebhookTiny on WorkbenchWebhook {\n id\n name\n priority\n webhook {\n id\n type\n __typename\n }\n issueWebhook {\n ...IssueWebhookTiny\n __typename\n }\n __typename\n}\n\nfragment IssueWebhookTiny on IssueWebhook {\n id\n provider\n __typename\n}\n\nfragment WorkbenchTool on WorkbenchTool {\n ...WorkbenchToolTiny\n scmConnection {\n id\n name\n type\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n http {\n url\n method\n headers {\n name\n value\n __typename\n }\n body\n inputSchema\n __typename\n }\n datadog {\n site\n __typename\n }\n elastic {\n index\n url\n username\n __typename\n }\n opensearch {\n host\n index\n awsAccessKeyId\n awsRegion\n assumeRoleArn\n usePodIdentity\n __typename\n }\n loki {\n url\n username\n tenantId\n __typename\n }\n prometheus {\n url\n username\n tenantId\n awsSigv4\n awsAccessKeyId\n awsRegion\n __typename\n }\n tempo {\n url\n username\n tenantId\n __typename\n }\n jaeger {\n url\n username\n __typename\n }\n atlassian {\n email\n url\n __typename\n }\n linear {\n url\n __typename\n }\n slack {\n url\n __typename\n }\n pagerduty {\n url\n __typename\n }\n teams {\n clientId\n tenantId\n __typename\n }\n splunk {\n url\n username\n __typename\n }\n cloudwatch {\n logGroupNames\n region\n roleArn\n roleSessionName\n __typename\n }\n azure {\n subscriptionId\n tenantId\n clientId\n prometheusUrl\n __typename\n }\n dynatrace {\n url\n __typename\n }\n sentry {\n url\n __typename\n }\n github {\n url\n toolset\n appId\n installationId\n __typename\n }\n gitlab {\n url\n __typename\n }\n bitbucket {\n url\n __typename\n }\n bitbucketDatacenter {\n url\n __typename\n }\n azureDevops {\n url\n __typename\n }\n docker {\n url\n provider\n proxy {\n url\n noproxy\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" + "body": "mutation UpdateWorkbench($id: ID!, $attributes: WorkbenchAttributes!) {\n updateWorkbench(id: $id, attributes: $attributes) {\n ...Workbench\n __typename\n }\n}\n\nfragment Workbench on Workbench {\n ...WorkbenchTiny\n systemPrompt\n agentRuntime {\n id\n name\n __typename\n }\n repository {\n id\n __typename\n }\n configuration {\n infrastructure {\n services\n stacks\n kubernetes\n podLogs\n vulnerabilities\n sentinels\n __typename\n }\n observability {\n logs\n metrics\n __typename\n }\n coding {\n mode\n repositories\n enableBabysitting\n __typename\n }\n __typename\n }\n modes {\n plan\n model {\n provider\n model\n __typename\n }\n coding {\n approval\n babysit\n __typename\n }\n budget {\n cost\n tokens\n __typename\n }\n __typename\n }\n skills {\n ref {\n ref\n folder\n __typename\n }\n files\n __typename\n }\n workbenchSkills(first: 500) {\n edges {\n node {\n id\n name\n description\n contents\n subagents\n __typename\n }\n __typename\n }\n __typename\n }\n tools {\n ...WorkbenchTool\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n botUser {\n id\n name\n email\n profile\n __typename\n }\n __typename\n}\n\nfragment WorkbenchTiny on Workbench {\n id\n name\n description\n agentRuntime {\n id\n name\n type\n __typename\n }\n tools {\n ...WorkbenchToolTiny\n __typename\n }\n webhooks(first: 50) {\n edges {\n node {\n ...WorkbenchWebhookTiny\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchToolTiny on WorkbenchTool {\n id\n name\n tool\n categories\n cloudConnection {\n ...CloudConnectionTiny\n __typename\n }\n mcpServer {\n id\n name\n url\n __typename\n }\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}\n\nfragment WorkbenchWebhookTiny on WorkbenchWebhook {\n id\n name\n priority\n webhook {\n id\n type\n __typename\n }\n issueWebhook {\n ...IssueWebhookTiny\n __typename\n }\n __typename\n}\n\nfragment IssueWebhookTiny on IssueWebhook {\n id\n provider\n __typename\n}\n\nfragment WorkbenchTool on WorkbenchTool {\n ...WorkbenchToolTiny\n scmConnection {\n id\n name\n type\n __typename\n }\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n http {\n url\n method\n headers {\n name\n value\n __typename\n }\n body\n inputSchema\n __typename\n }\n datadog {\n site\n __typename\n }\n elastic {\n index\n url\n username\n __typename\n }\n opensearch {\n host\n index\n awsAccessKeyId\n awsRegion\n assumeRoleArn\n usePodIdentity\n __typename\n }\n loki {\n url\n username\n tenantId\n __typename\n }\n prometheus {\n url\n username\n tenantId\n awsSigv4\n awsAccessKeyId\n awsRegion\n __typename\n }\n tempo {\n url\n username\n tenantId\n __typename\n }\n jaeger {\n url\n username\n __typename\n }\n atlassian {\n email\n url\n __typename\n }\n linear {\n url\n __typename\n }\n slack {\n url\n __typename\n }\n pagerduty {\n url\n __typename\n }\n teams {\n clientId\n tenantId\n __typename\n }\n splunk {\n url\n username\n __typename\n }\n cloudwatch {\n logGroupNames\n region\n roleArn\n roleSessionName\n __typename\n }\n azure {\n subscriptionId\n tenantId\n clientId\n prometheusUrl\n __typename\n }\n dynatrace {\n url\n __typename\n }\n sentry {\n url\n __typename\n }\n github {\n url\n toolset\n appId\n installationId\n __typename\n }\n gitlab {\n url\n __typename\n }\n bitbucket {\n url\n __typename\n }\n bitbucketDatacenter {\n url\n __typename\n }\n azureDevops {\n url\n __typename\n }\n docker {\n url\n provider\n proxy {\n url\n noproxy\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" }, "sha256:6bdd9295cb14dc91a238b8bb12b0b9b2781f2c6789194acd1364f8fe10bb3f6a": { "type": "mutation", diff --git a/assets/src/graph/workbench.graphql b/assets/src/graph/workbench.graphql index 2ad0400cf7..ada6c29958 100644 --- a/assets/src/graph/workbench.graphql +++ b/assets/src/graph/workbench.graphql @@ -69,6 +69,7 @@ fragment Workbench on Workbench { kubernetes podLogs vulnerabilities + sentinels } observability { logs diff --git a/charts/console/values.yaml b/charts/console/values.yaml index 359f97372f..3debfec341 100644 --- a/charts/console/values.yaml +++ b/charts/console/values.yaml @@ -34,7 +34,11 @@ replicaCount: 2 # strategy configures the main console Deployment strategy. # For rolling updates, set type to RollingUpdate and tune rollingUpdate.maxSurge/maxUnavailable. -strategy: {} +strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 # homeDir is the prefix for the mount path for console-conf volume in console container. The actual config will go in {homeDir}/.plural homeDir: /home/console diff --git a/config/config.exs b/config/config.exs index 5f6bdf48ea..daacb2e08f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -66,7 +66,7 @@ config :console, qps: 1_000, tarball_qps: 100, cache_agent_qps: 50, - cache_agent_queue_limit: 20, + cache_agent_queue_limit: 25, cache_agent_queue_shed: 5, nowatchers: false, default_project_name: "default", diff --git a/lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run.ex b/lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run.ex new file mode 100644 index 0000000000..6920c2c083 --- /dev/null +++ b/lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run.ex @@ -0,0 +1,55 @@ +defmodule Console.AI.Tools.Workbench.Sentinel.FetchSentinelRun do + use Console.AI.Tools.Agent.Base + import Console.Deployments.Policies, only: [allow: 3] + alias Console.Repo + alias Console.Deployments.Sentinels + alias Console.Schema.{SentinelRun, User} + + embedded_schema do + field :user, :map, virtual: true + field :sentinel_run_id, :string + end + + @valid ~w(sentinel_run_id)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> check_uuid(:sentinel_run_id) + |> validate_required([:sentinel_run_id]) + end + + @json_schema Console.priv_file!("tools/workbench/sentinel/fetch_sentinel_run.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_sentinel_run" + def description(_), do: "Fetch a Plural sentinel run by id, with run jobs compressed to id and status." + + def implement(%__MODULE__{user: %User{} = user, sentinel_run_id: id}) do + with %SentinelRun{} = run <- Sentinels.get_sentinel_run(id), + {:ok, run} <- allow(run, user, :read) do + run + |> Repo.preload([:jobs]) + |> run_summary() + |> Jason.encode() + else + nil -> {:error, "could not find sentinel run with id #{id}"} + {:error, err} -> {:error, "failed to fetch sentinel run, reason: #{inspect(err)}"} + error -> error + end + end + + def run_summary(%SentinelRun{} = run) do + Map.take(run, [:id, :status, :sentinel_id, :completed_at, :polled_at, :inserted_at, :updated_at]) + |> Map.put(:results, Enum.map(run.results || [], &result_summary/1)) + |> Map.put(:jobs, jobs_summary(run.jobs)) + end + + defp result_summary(result), + do: Map.take(result, [:name, :status, :reason, :job_count, :successful_count, :failed_count]) + + defp job_summary(job), do: %{id: job.id, status: job.status} + + defp jobs_summary(jobs) when is_list(jobs), do: Enum.map(jobs, &job_summary/1) + defp jobs_summary(_), do: [] +end diff --git a/lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run_job.ex b/lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run_job.ex new file mode 100644 index 0000000000..228777a7c2 --- /dev/null +++ b/lib/console/ai/tools/workbench/sentinel/fetch_sentinel_run_job.ex @@ -0,0 +1,58 @@ +defmodule Console.AI.Tools.Workbench.Sentinel.FetchSentinelRunJob do + use Console.AI.Tools.Agent.Base + import Console.Deployments.Policies, only: [allow: 3] + alias Console.Repo + alias Console.Deployments.Sentinels + alias Console.Schema.{SentinelRunJob, User} + + require EEx + + embedded_schema do + field :user, :map, virtual: true + field :sentinel_run_job_id, :string + end + + @valid ~w(sentinel_run_job_id)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> check_uuid(:sentinel_run_job_id) + |> validate_required([:sentinel_run_job_id]) + end + + @json_schema Console.priv_file!("tools/workbench/sentinel/fetch_sentinel_run_job.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_sentinel_run_job" + def description(_), do: "Fetch a Plural sentinel run job by id, including its execution output." + + def implement(%__MODULE__{user: %User{} = user, sentinel_run_job_id: id}) do + with %SentinelRunJob{} = job <- Sentinels.get_sentinel_run_job(id), + {:ok, job} <- allow(job, user, :read) do + {:ok, String.trim(job_markdown(job: Repo.preload(job, [:cluster, :repository])))} + else + nil -> {:error, "could not find sentinel run job with id #{id}"} + {:error, err} -> {:error, "failed to fetch sentinel run job, reason: #{inspect(err)}"} + error -> error + end + end + + defp cluster(%SentinelRunJob{cluster: %{handle: handle, id: id}}) when is_binary(handle), do: "`#{handle}` (`#{id}`)" + defp cluster(%SentinelRunJob{cluster_id: id}) when is_binary(id), do: "`#{id}`" + defp cluster(_), do: "_none_" + + defp fence_language(:junit), do: "xml" + defp fence_language(_), do: "" + + defp nullable(%DateTime{} = dt), do: "`#{DateTime.to_iso8601(dt)}`" + defp nullable(value) when is_binary(value) and byte_size(value) > 0, do: "`#{value}`" + defp nullable(_), do: "_none_" + + EEx.function_from_file( + :defp, + :job_markdown, + Console.priv_filename(["prompts", "workbench", "sentinel", "run_job.md.eex"]), + [:assigns] + ) +end diff --git a/lib/console/ai/tools/workbench/sentinel/list_sentinels.ex b/lib/console/ai/tools/workbench/sentinel/list_sentinels.ex new file mode 100644 index 0000000000..6bde336a13 --- /dev/null +++ b/lib/console/ai/tools/workbench/sentinel/list_sentinels.ex @@ -0,0 +1,52 @@ +defmodule Console.AI.Tools.Workbench.Sentinel.ListSentinels do + use Console.AI.Tools.Agent.Base + alias Console.Repo + alias Console.Schema.{Sentinel, SentinelRun, User} + + embedded_schema do + field :user, :map, virtual: true + field :q, :string + field :status, SentinelRun.Status + field :limit, :integer, default: 25 + end + + @valid ~w(q status limit)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> validate_number(:limit, greater_than: 0, less_than_or_equal_to: 50) + end + + @json_schema Console.priv_file!("tools/workbench/sentinel/list_sentinels.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_list_sentinels" + def description(_), do: "List Plural sentinels visible to the current user, optionally filtered by status or search query. These provide thorough integration tests at the infrastructure or kubernetes level." + + def implement(%__MODULE__{user: %User{} = user, q: q, status: status, limit: limit}) do + limit = limit || 25 + + Sentinel.ordered() + |> Sentinel.for_user(user) + |> maybe_status(status) + |> maybe_search(q) + |> Sentinel.limit(limit) + |> Repo.all() + |> Enum.map(&sentinel_brief/1) + |> Jason.encode() + end + + defp maybe_status(query, status) when not is_nil(status), do: Sentinel.for_status(query, status) + defp maybe_status(query, _), do: query + + defp maybe_search(query, q) when is_binary(q) and byte_size(q) > 0, do: Sentinel.search(query, q) + defp maybe_search(query, _), do: query + + defp sentinel_brief(%Sentinel{} = sentinel) do + Map.take(sentinel, [:id, :name, :description, :status, :last_run_at, :next_run_at, :project_id, :repository_id]) + |> Map.put(:checks, Enum.map(sentinel.checks || [], &check_brief/1)) + end + + defp check_brief(check), do: Map.take(check, [:name, :type, :rule_file]) +end diff --git a/lib/console/ai/tools/workbench/sentinel/run_sentinel.ex b/lib/console/ai/tools/workbench/sentinel/run_sentinel.ex new file mode 100644 index 0000000000..4fa3ff3125 --- /dev/null +++ b/lib/console/ai/tools/workbench/sentinel/run_sentinel.ex @@ -0,0 +1,53 @@ +defmodule Console.AI.Tools.Workbench.Sentinel.RunSentinel do + use Console.AI.Tools.Agent.Base + alias Console.Deployments.Sentinels + alias Console.Schema.{Sentinel, User} + + embedded_schema do + field :user, :map, virtual: true + field :sentinel, :map, virtual: true + field :sentinel_id, :binary_id + field :name, :string + field :overrides, :map, default: %{} + end + + @valid ~w(sentinel_id name overrides)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> check_uuid(:sentinel_id) + |> validate_sentinel_ref() + end + + @json_schema Console.priv_file!("tools/workbench/sentinel/run_sentinel.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_run_sentinel" + def description(_), do: "Run a Plural sentinel by id or name. The verification subagent will poll the run once a minute until it completes." + + def implement(%__MODULE__{user: %User{}, sentinel_id: id, name: name} = model) do + with %Sentinel{} = sentinel <- sentinel(id, name) do + {:ok, %{model | sentinel: sentinel}} + else + nil -> {:error, "could not find sentinel by #{sentinel_ref(id, name)}"} + error -> error + end + end + + defp validate_sentinel_ref(cs) do + case {get_field(cs, :sentinel_id), get_field(cs, :name)} do + {id, _} when is_binary(id) and byte_size(id) > 0 -> cs + {_, name} when is_binary(name) and byte_size(name) > 0 -> cs + _ -> add_error(cs, :sentinel_id, "must specify either sentinel_id or name") + end + end + + defp sentinel(id, _) when is_binary(id) and byte_size(id) > 0, do: Sentinels.get_sentinel(id) + defp sentinel(_, name) when is_binary(name) and byte_size(name) > 0, do: Sentinels.get_sentinel_by_name(name) + defp sentinel(_, _), do: nil + + defp sentinel_ref(id, _) when is_binary(id) and byte_size(id) > 0, do: "id #{id}" + defp sentinel_ref(_, name) when is_binary(name) and byte_size(name) > 0, do: "name #{name}" + defp sentinel_ref(_, _), do: "empty sentinel reference" +end diff --git a/lib/console/ai/workbench/engine.ex b/lib/console/ai/workbench/engine.ex index cdecbe6bd0..afb93c8349 100644 --- a/lib/console/ai/workbench/engine.ex +++ b/lib/console/ai/workbench/engine.ex @@ -46,7 +46,7 @@ defmodule Console.AI.Workbench.Engine do require EEx require Logger - defstruct [:job, :user, :environment, activities: [], messages: [], iterations: 0, max: 200] + defstruct [:job, :user, :environment, activities: [], messages: [], iterations: 0, max: 200, verifiable: false] defmodule Acc do defstruct [messages: [], activities: []] @@ -158,7 +158,7 @@ defmodule Console.AI.Workbench.Engine do |> loop() end - @supported_subagents ~w(infrastructure integration coding observability memory skill history search)a + @supported_subagents ~w(infrastructure integration coding observability memory skill history search verify)a defp spawn_activity(%Subagent{subagent: type, prompt: prompt} = call, %__MODULE__{job: job, environment: environment, activities: activities}) when type in @supported_subagents do @@ -366,6 +366,7 @@ defmodule Console.AI.Workbench.Engine do (is_list(activities) && Enum.any?(activities, & &1.status == :successful && is_action(&1.type))) put_in(engine.environment.verifiable, verifiable) + |> Map.put(:verifiable, verifiable) end defp verifiable(engine), do: engine diff --git a/lib/console/ai/workbench/subagents/verify.ex b/lib/console/ai/workbench/subagents/verify.ex index 970421dbc0..bed05f5607 100644 --- a/lib/console/ai/workbench/subagents/verify.ex +++ b/lib/console/ai/workbench/subagents/verify.ex @@ -1,8 +1,15 @@ defmodule Console.AI.Workbench.Subagents.Verify do use Console.AI.Workbench.Subagents.Base alias Console.AI.Workbench.Subagents.{Infrastructure, Observability} - alias Console.Schema.{WorkbenchJob, WorkbenchJobActivity} + alias Console.Deployments.Sentinels + alias Console.Schema.{SentinelRun, WorkbenchJob, WorkbenchJobActivity} alias Console.AI.Tools.Workbench.{Result, Skills, Skill, Scratchpad} + alias Console.AI.Tools.Workbench.Sentinel.{ + FetchSentinelRun, + FetchSentinelRunJob, + ListSentinels, + RunSentinel + } alias Console.AI.Workbench.Environment import Console.AI.Workbench.Environment, only: [engine_opts: 1] @@ -15,6 +22,7 @@ defmodule Console.AI.Workbench.Subagents.Verify do system_prompt: String.trim(system_prompt(prompt: jprompt)), acc: %{}, callback: &callback(activity, &1), + pre_enable: [Result, %Skills{} ,%Skill{}], continue_msg: cont_msg() ] ) @@ -26,7 +34,8 @@ defmodule Console.AI.Workbench.Subagents.Verify do end defp reducer(messages, _) do - case Enum.find(messages, &match?(%Result{}, &1)) do + case Enum.find(messages, &stop_msg/1) do + %RunSentinel{} = run -> {:message, run_and_poll_sentinel(run)} %Result{output: output} -> {:halt, %{ status: :successful, result: %{output: output} @@ -35,11 +44,16 @@ defmodule Console.AI.Workbench.Subagents.Verify do end end + defp stop_msg(%RunSentinel{}), do: true + defp stop_msg(%Result{}), do: true + defp stop_msg(_), do: false + defp tools(%WorkbenchJob{} = job, %Environment{skills: skills} = environment) do skills = Environment.subagent_skills(skills, :verify) Observability.core_tools(job, environment) |> Enum.concat(Infrastructure.core_tools(job, environment)) + |> Enum.concat(sentinel_tools(job)) |> Enum.concat([ %Skills{skills: skills}, %Skill{skills: skills}, @@ -48,5 +62,49 @@ defmodule Console.AI.Workbench.Subagents.Verify do ]) end + defp sentinel_tools(%WorkbenchJob{workbench: %{configuration: %{infrastructure: %{sentinels: true}}}, user: user}) do + [ + %ListSentinels{user: user}, + %FetchSentinelRun{user: user}, + %FetchSentinelRunJob{user: user}, + %RunSentinel{user: user} + ] + end + defp sentinel_tools(_), do: [] + + defp run_and_poll_sentinel(%RunSentinel{user: user, sentinel: %{id: id}, overrides: overrides} = tool) do + with {:ok, run} <- Sentinels.run_sentinel(overrides || %{}, id, user), + {:ok, run} <- poll_sentinel_run(run) do + run + |> FetchSentinelRun.run_summary() + |> Jason.encode!() + |> then(&tool_msg(&1, tool)) + else + {:timeout, %SentinelRun{id: run_id}} -> tool_msg("sentinel run #{run_id} timed out before completion", tool) + {:error, err} -> tool_msg("failed to run sentinel, reason: #{inspect(err)}", tool) + error -> tool_msg("failed to run sentinel, reason: #{inspect(error)}", tool) + end + end + + @poll_iters 60 + @poll_interval :timer.minutes(1) + + defp poll_sentinel_run(run, iter \\ 0) + defp poll_sentinel_run(%SentinelRun{} = run, iter) when iter >= @poll_iters, do: {:timeout, run} + defp poll_sentinel_run(%SentinelRun{status: status} = run, _) when status in [:success, :failed], + do: {:ok, Repo.preload(run, [:jobs], force: true)} + defp poll_sentinel_run(%SentinelRun{id: id} = run, iter) do + :timer.sleep(@poll_interval) + + case Repo.get(SentinelRun, id) do + %SentinelRun{} = run -> poll_sentinel_run(run, iter + 1) + nil -> {:error, "sentinel run #{run.id} was deleted before completion"} + end + end + + defp tool_msg(content, %RunSentinel{id: %Console.AI.Tool{id: id, name: name, arguments: args}}), + do: {:tool, content, %{call_id: id, name: name, arguments: args}} + defp tool_msg(content, _), do: {:user, content} + EEx.function_from_file(:defp, :system_prompt, Console.priv_filename(["prompts", "workbench", "verify.md.eex"]), [:assigns]) end diff --git a/lib/console/graphql/deployments/workbench.ex b/lib/console/graphql/deployments/workbench.ex index d59a68294d..388b098d5b 100644 --- a/lib/console/graphql/deployments/workbench.ex +++ b/lib/console/graphql/deployments/workbench.ex @@ -85,6 +85,7 @@ defmodule Console.GraphQl.Deployments.Workbench do field :kubernetes, :boolean, description: "enable kubernetes capability" field :pod_logs, :boolean, description: "enable pod logs capability" field :vulnerabilities, :boolean, description: "enable vulnerabilities capability" + field :sentinels, :boolean, description: "enable sentinels capability" end input_object :workbench_coding_attributes do @@ -783,6 +784,7 @@ defmodule Console.GraphQl.Deployments.Workbench do field :kubernetes, :boolean, description: "kubernetes capability enabled" field :pod_logs, :boolean, description: "pod logs capability enabled" field :vulnerabilities, :boolean, description: "vulnerabilities capability enabled" + field :sentinels, :boolean, description: "sentinels capability enabled" end object :workbench_coding do diff --git a/lib/console/schema/sentinel.ex b/lib/console/schema/sentinel.ex index 0c704daf98..3d2da35538 100644 --- a/lib/console/schema/sentinel.ex +++ b/lib/console/schema/sentinel.ex @@ -159,6 +159,10 @@ defmodule Console.Schema.Sentinel do from(s in query, order_by: ^order) end + def limit(query \\ __MODULE__, limit) do + from(s in query, limit: ^limit) + end + @valid ~w(name description status last_run_at repository_id project_id crontab)a def changeset(model, attrs \\ %{}) do diff --git a/lib/console/schema/workbench.ex b/lib/console/schema/workbench.ex index dc891b550c..d70b624af2 100644 --- a/lib/console/schema/workbench.ex +++ b/lib/console/schema/workbench.ex @@ -36,6 +36,7 @@ defmodule Console.Schema.Workbench do field :kubernetes, :boolean field :pod_logs, :boolean field :vulnerabilities, :boolean + field :sentinels, :boolean end embeds_one :observability, Observability, on_replace: :update do @@ -169,7 +170,7 @@ defmodule Console.Schema.Workbench do def infrastructure_changeset(model, attrs \\ %{}) do model - |> cast(attrs, ~w(services stacks kubernetes pod_logs vulnerabilities)a) + |> cast(attrs, ~w(services stacks kubernetes pod_logs vulnerabilities sentinels)a) end def coding_changeset(model, attrs \\ %{}) do diff --git a/priv/prompts/workbench/job.md.eex b/priv/prompts/workbench/job.md.eex index af1d5de451..0d90444f46 100644 --- a/priv/prompts/workbench/job.md.eex +++ b/priv/prompts/workbench/job.md.eex @@ -12,6 +12,7 @@ the task. You'll be given the following: 5. a dashboard agent (the canvas tool) which allows you to craft a custom dashboard to explain the system in question. This will be useful for understanding the dynamics of the system in question, and will be used to generate a dashboard for other developers to consume. 6. the ability to record notes and modify your current plan and working theory of how to accomplish the task along the way to memorize progress. 7. a search agent to search the web for information. Useful to find information that is not already in the workbench environment. + 8. a verification agent which will be present when a change relevant to this job has been fully applied. This is not always present, and will be included when verification is possible. Basic guardrails for using these subagents: @@ -99,6 +100,15 @@ As work is completed, be sure to call the `workbench_notes` tool to record progr If you are not given adequate information to complete the task, call the `workbench_complete` tool to mark the conclusion of this job and explain why that's the case as well. You should always ultimately call `workbench_complete` to mark the conclusion of this job. +## Work Verification + +A dedicated verification agent is provided optionally, when we've determined that the actions you've taken along this job have succeeded and a verification pass could be helpful. A few things to know about this: + +* there are a few things that signal verification, a completed function call, k8s update, or pr merge are in that bucket. +* you don't need to perform a verification pass for all work, if the work done was trivial, user's will prefer to save their tokens. + +Generally this should be the only means to validate your work after a change. Re-reading pr changes for instance, are not. + ## Parallelism Guidance We can manage multiple subagents in parallel, but you should avoid spawning multiple of the same subagent type at once, simply because they will duplicate work. You can instead wait until they complete, diff --git a/priv/prompts/workbench/sentinel/run_job.md.eex b/priv/prompts/workbench/sentinel/run_job.md.eex new file mode 100644 index 0000000000..ee4e12f02d --- /dev/null +++ b/priv/prompts/workbench/sentinel/run_job.md.eex @@ -0,0 +1,21 @@ +# Sentinel Run Job + +- ID: `<%= @job.id %>` +- Status: `<%= @job.status %>` +- Check: `<%= @job.check %>` +- Format: `<%= @job.format %>` +- Sentinel run ID: `<%= @job.sentinel_run_id %>` +- Cluster: <%= cluster(@job) %> +- Repository ID: <%= nullable(@job.repository_id) %> +- Completed at: <%= nullable(@job.completed_at) %> + +## Kubernetes Job + +- Namespace: <%= nullable(@job.reference && @job.reference.namespace) %> +- Name: <%= nullable(@job.reference && @job.reference.name) %> + +## Output + +````<%= fence_language(@job.format) %> +<%= @job.output || "" %> +```` diff --git a/priv/tools/workbench/sentinel/fetch_sentinel_run.json b/priv/tools/workbench/sentinel/fetch_sentinel_run.json new file mode 100644 index 0000000000..b642222be6 --- /dev/null +++ b/priv/tools/workbench/sentinel/fetch_sentinel_run.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "sentinel_run_id": { + "type": "string", + "description": "UUID of the sentinel run to fetch" + } + }, + "required": ["sentinel_run_id"] +} diff --git a/priv/tools/workbench/sentinel/fetch_sentinel_run_job.json b/priv/tools/workbench/sentinel/fetch_sentinel_run_job.json new file mode 100644 index 0000000000..87e752a8e1 --- /dev/null +++ b/priv/tools/workbench/sentinel/fetch_sentinel_run_job.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "sentinel_run_job_id": { + "type": "string", + "description": "UUID of the sentinel run job to fetch" + } + }, + "required": ["sentinel_run_job_id"] +} diff --git a/priv/tools/workbench/sentinel/list_sentinels.json b/priv/tools/workbench/sentinel/list_sentinels.json new file mode 100644 index 0000000000..1517ac56c2 --- /dev/null +++ b/priv/tools/workbench/sentinel/list_sentinels.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "Optional search string to match sentinel names" + }, + "status": { + "type": "string", + "enum": ["pending", "success", "failed"], + "description": "Optional filter for the sentinel's latest status" + }, + "limit": { + "type": "integer", + "description": "Maximum number of sentinels to return, from 1 to 50. Defaults to 25." + } + }, + "required": [] +} diff --git a/priv/tools/workbench/sentinel/run_sentinel.json b/priv/tools/workbench/sentinel/run_sentinel.json new file mode 100644 index 0000000000..8bd64894d8 --- /dev/null +++ b/priv/tools/workbench/sentinel/run_sentinel.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "properties": { + "sentinel_id": { + "type": "string", + "description": "UUID of the sentinel to run. Specify either sentinel_id or name." + }, + "name": { + "type": "string", + "description": "Name of the sentinel to run. Specify either sentinel_id or name." + }, + "overrides": { + "type": "object", + "description": "Optional sentinel run overrides. Currently supports tags for integration test checks.", + "properties": { + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Tags to merge into integration test checks for this run" + } + } + } + }, + "required": [] +} diff --git a/schema/schema.graphql b/schema/schema.graphql index a8f7075c88..46c239efe9 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -2846,6 +2846,9 @@ input WorkbenchInfrastructureAttributes { "enable vulnerabilities capability" vulnerabilities: Boolean + + "enable sentinels capability" + sentinels: Boolean } input WorkbenchCodingAttributes { @@ -4065,6 +4068,9 @@ type WorkbenchInfrastructure { "vulnerabilities capability enabled" vulnerabilities: Boolean + + "sentinels capability enabled" + sentinels: Boolean } type WorkbenchCoding { diff --git a/test/console/ai/tools/workbench/sentinel/tools_test.exs b/test/console/ai/tools/workbench/sentinel/tools_test.exs new file mode 100644 index 0000000000..62156b31a7 --- /dev/null +++ b/test/console/ai/tools/workbench/sentinel/tools_test.exs @@ -0,0 +1,107 @@ +defmodule Console.AI.Tools.Workbench.Sentinel.ToolsTest do + use Console.DataCase, async: true + + alias Console.AI.Tool + alias Console.AI.Tools.Workbench.Sentinel.{ + FetchSentinelRun, + FetchSentinelRunJob, + ListSentinels, + RunSentinel + } + alias Console.Schema.SentinelRun + + describe "ListSentinels (plrl_list_sentinels)" do + test "returns json for sentinels readable by the user" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + sentinel = insert(:sentinel, name: "alpha-sentinel", status: :success, project: project) + other = insert(:sentinel, name: "beta-sentinel", status: :success) + + assert {:ok, parsed} = + Tool.validate(%ListSentinels{user: user}, %{ + "q" => "alpha", + "status" => "success", + "limit" => 10 + }) + + assert {:ok, json} = ListSentinels.implement(parsed) + assert {:ok, list} = Jason.decode(json) + assert Enum.any?(list, &(&1["id"] == sentinel.id)) + refute Enum.any?(list, &(&1["id"] == other.id)) + + assert [%{"checks" => checks}] = list + assert is_list(checks) + end + end + + describe "FetchSentinelRun (plrl_sentinel_run)" do + test "returns compact run json with job id and status only" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + sentinel = insert(:sentinel, project: project) + run = insert(:sentinel_run, sentinel: sentinel, status: :failed) + job = insert(:sentinel_run_job, sentinel_run: run, status: :failed, output: "long job output") + + assert {:ok, parsed} = + Tool.validate(%FetchSentinelRun{user: user}, %{ + "sentinel_run_id" => run.id + }) + + assert {:ok, json} = FetchSentinelRun.implement(parsed) + assert {:ok, found} = Jason.decode(json) + + assert found["id"] == run.id + assert found["status"] == "failed" + assert [%{"id" => job_id, "status" => "failed"} = encoded_job] = found["jobs"] + assert job_id == job.id + refute Map.has_key?(encoded_job, "output") + end + end + + describe "FetchSentinelRunJob (plrl_sentinel_run_job)" do + test "returns markdown including run job output" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + sentinel = insert(:sentinel, project: project) + run = insert(:sentinel_run, sentinel: sentinel) + + job = + insert(:sentinel_run_job, + sentinel_run: run, + status: :success, + format: :plaintext, + output: "integration test output" + ) + + assert {:ok, parsed} = + Tool.validate(%FetchSentinelRunJob{user: user}, %{ + "sentinel_run_job_id" => job.id + }) + + assert {:ok, markdown} = FetchSentinelRunJob.implement(parsed) + assert markdown =~ "# Sentinel Run Job" + assert markdown =~ "- ID: `#{job.id}`" + assert markdown =~ "- Status: `success`" + assert markdown =~ "integration test output" + end + end + + describe "RunSentinel (plrl_run_sentinel)" do + test "loads the sentinel onto the tool struct without creating a run" do + user = insert(:user) + sentinel = insert(:sentinel, name: "deploy-smoke") + before_count = Repo.aggregate(SentinelRun, :count, :id) + + assert {:ok, parsed} = + Tool.validate(%RunSentinel{user: user}, %{ + "name" => "deploy-smoke", + "overrides" => %{"tags" => %{"suite" => "smoke"}} + }) + + assert {:ok, %RunSentinel{sentinel: loaded, overrides: overrides}} = RunSentinel.implement(parsed) + assert loaded.id == sentinel.id + assert overrides == %{"tags" => %{"suite" => "smoke"}} + assert Repo.aggregate(SentinelRun, :count, :id) == before_count + end + end +end