Skip to content

Commit c34c0da

Browse files
committed
feat: SandboxAgent CRD and sandbox runtime integration
Add SandboxAgent kind with reconciliation via SandboxTemplate/SandboxClaim, translator runInSandbox flag, HTTP/UI support. Signed-off-by: Peter Jausovec <peter.jausovec@solo.io>
1 parent 256d11a commit c34c0da

67 files changed

Lines changed: 23159 additions & 263 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 8196 additions & 0 deletions
Large diffs are not rendered by default.

go/api/httpapi/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type AgentResponse struct {
9494
Tools []*v1alpha2.Tool `json:"tools"`
9595
DeploymentReady bool `json:"deploymentReady"`
9696
Accepted bool `json:"accepted"`
97+
// RunInSandbox is true when the workload is reconciled as a SandboxAgent (SandboxTemplate/SandboxClaim), not a Deployment.
98+
RunInSandbox bool `json:"runInSandbox,omitempty"`
9799
}
98100

99101
// Session types

go/api/v1alpha2/agent_types.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@ type AgentSpec struct {
7878
AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"`
7979
}
8080

81+
// SandboxAgentSpec defines the desired state of a SandboxAgent. Workload fields match Agent.spec
82+
// (Declarative or BYO). The kind selects sandbox reconciliation (SandboxTemplate + SandboxClaim) instead of a Deployment.
83+
//
84+
// +kubebuilder:validation:XValidation:message="type must be specified",rule="has(self.type)"
85+
// +kubebuilder:validation:XValidation:message="type must be either Declarative or BYO",rule="self.type == 'Declarative' || self.type == 'BYO'"
86+
// +kubebuilder:validation:XValidation:message="declarative or byo must match type",rule="(self.type == 'Declarative' && has(self.declarative)) || (self.type == 'BYO' && has(self.byo))"
87+
type SandboxAgentSpec struct {
88+
// +kubebuilder:validation:Enum=Declarative;BYO
89+
// +kubebuilder:default=Declarative
90+
Type AgentType `json:"type"`
91+
92+
// +optional
93+
BYO *BYOAgentSpec `json:"byo,omitempty"`
94+
// +optional
95+
Declarative *DeclarativeAgentSpec `json:"declarative,omitempty"`
96+
97+
// +optional
98+
Description string `json:"description,omitempty"`
99+
100+
// +optional
101+
Skills *SkillForAgent `json:"skills,omitempty"`
102+
103+
// +optional
104+
AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"`
105+
}
106+
81107
// +kubebuilder:validation:AtLeastOneOf=refs,gitRefs
82108
type SkillForAgent struct {
83109
// Fetch images insecurely from registries (allowing HTTP and skipping TLS verification).
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha2
18+
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
// +kubebuilder:object:root=true
24+
// +kubebuilder:subresource:status
25+
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Whether the sandbox workload is ready."
26+
// +kubebuilder:printcolumn:name="Accepted",type="string",JSONPath=".status.conditions[?(@.type=='Accepted')].status",description="Whether configuration was accepted."
27+
// SandboxAgent declares an agent that runs in an isolated sandbox (agent-sandbox SandboxTemplate+SandboxClaim).
28+
type SandboxAgent struct {
29+
metav1.TypeMeta `json:",inline"`
30+
metav1.ObjectMeta `json:"metadata,omitempty"`
31+
32+
Spec SandboxAgentSpec `json:"spec,omitempty"`
33+
Status AgentStatus `json:"status,omitempty"`
34+
}
35+
36+
// +kubebuilder:object:root=true
37+
38+
// SandboxAgentList contains a list of SandboxAgent.
39+
type SandboxAgentList struct {
40+
metav1.TypeMeta `json:",inline"`
41+
metav1.ListMeta `json:"metadata,omitempty"`
42+
Items []SandboxAgent `json:"items"`
43+
}
44+
45+
func init() {
46+
SchemeBuilder.Register(&SandboxAgent{}, &SandboxAgentList{})
47+
}

go/api/v1alpha2/zz_generated.deepcopy.go

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

go/core/cmd/controller/main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"github.com/kagent-dev/kagent/go/core/internal/httpserver/auth"
2121
"github.com/kagent-dev/kagent/go/core/pkg/app"
22+
"github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/agentsxk8s"
2223

2324
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2425
// to ensure that exec-entrypoint and run can make use of them.
@@ -31,9 +32,10 @@ func main() {
3132
authenticator := &auth.UnsecureAuthenticator{}
3233
app.Start(func(bootstrap app.BootstrapConfig) (*app.ExtensionConfig, error) {
3334
return &app.ExtensionConfig{
34-
Authenticator: authenticator,
35-
Authorizer: authorizer,
36-
AgentPlugins: nil,
35+
Authenticator: authenticator,
36+
Authorizer: authorizer,
37+
AgentPlugins: nil,
38+
SandboxBackend: agentsxk8s.New(),
3739
}, nil
3840
}, nil)
3941
}

go/core/internal/a2a/a2a_registrar.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,53 @@ func (a *A2ARegistrar) Start(ctx context.Context) error {
112112
return fmt.Errorf("failed to add informer event handler: %w", err)
113113
}
114114

115+
sandboxInformer, err := a.cache.GetInformer(ctx, &v1alpha2.SandboxAgent{})
116+
if err != nil {
117+
return fmt.Errorf("failed to get sandboxagent cache informer: %w", err)
118+
}
119+
120+
if _, err := sandboxInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
121+
AddFunc: func(obj any) {
122+
if sa, ok := obj.(*v1alpha2.SandboxAgent); ok {
123+
agentView := agent_translator.AgentViewFromSandboxAgent(sa)
124+
if err := a.upsertAgentHandler(ctx, agentView, log); err != nil {
125+
log.Error(err, "failed to upsert A2A handler", "sandboxagent", common.GetObjectRef(sa))
126+
}
127+
}
128+
},
129+
UpdateFunc: func(oldObj, newObj any) {
130+
oldSA, ok1 := oldObj.(*v1alpha2.SandboxAgent)
131+
newSA, ok2 := newObj.(*v1alpha2.SandboxAgent)
132+
if !ok1 || !ok2 {
133+
return
134+
}
135+
if oldSA.Generation != newSA.Generation || !reflect.DeepEqual(oldSA.Spec, newSA.Spec) {
136+
agentView := agent_translator.AgentViewFromSandboxAgent(newSA)
137+
if err := a.upsertAgentHandler(ctx, agentView, log); err != nil {
138+
log.Error(err, "failed to upsert A2A handler", "sandboxagent", common.GetObjectRef(newSA))
139+
}
140+
}
141+
},
142+
DeleteFunc: func(obj any) {
143+
sa, ok := obj.(*v1alpha2.SandboxAgent)
144+
if !ok {
145+
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
146+
if s2, ok := tombstone.Obj.(*v1alpha2.SandboxAgent); ok {
147+
sa = s2
148+
}
149+
}
150+
}
151+
if sa == nil {
152+
return
153+
}
154+
ref := common.GetObjectRef(sa)
155+
a.handlerMux.RemoveAgentHandler(ref)
156+
log.V(1).Info("removed A2A handler", "sandboxagent", ref)
157+
},
158+
}); err != nil {
159+
return fmt.Errorf("failed to add sandboxagent informer event handler: %w", err)
160+
}
161+
115162
if ok := a.cache.WaitForCacheSync(ctx); !ok {
116163
return fmt.Errorf("cache sync failed")
117164
}

go/core/internal/controller/agent_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ type AgentController struct {
5757
// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete
5858
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
5959
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
60+
// +kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes,verbs=get;list;watch;create;update;patch;delete
61+
// +kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes/status,verbs=get;update;patch
62+
// +kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes/finalizers,verbs=update
63+
// +kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxtemplates,verbs=get;list;watch;create;update;patch;delete
64+
// +kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxtemplates/status,verbs=get;update;patch
65+
// +kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxtemplates/finalizers,verbs=update
66+
// +kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxclaims,verbs=get;list;watch;create;update;patch;delete
67+
// +kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxclaims/status,verbs=get;update;patch
68+
// +kubebuilder:rbac:groups=extensions.agents.x-k8s.io,resources=sandboxclaims/finalizers,verbs=update
6069

6170
func (r *AgentController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
6271
_ = log.FromContext(ctx)

go/core/internal/controller/mcp_server_tool_controller_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ func (f *fakeReconciler) ReconcileKagentAgent(ctx context.Context, req ctrl.Requ
3131
return nil
3232
}
3333

34+
func (f *fakeReconciler) ReconcileKagentSandboxAgent(ctx context.Context, req ctrl.Request) error {
35+
return nil
36+
}
37+
3438
func (f *fakeReconciler) ReconcileKagentModelConfig(ctx context.Context, req ctrl.Request) error {
3539
return nil
3640
}

go/core/internal/controller/reconciler/mcp_server_reconciler_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,15 @@ func TestReconcileKagentMCPServer_ErrorPropagation(t *testing.T) {
102102
types.NamespacedName{Namespace: "test", Name: "default-model"},
103103
nil,
104104
"",
105+
nil,
105106
)
106107
reconciler := NewKagentReconciler(
107108
translator,
108109
kubeClient,
109110
dbClient,
110111
types.NamespacedName{Namespace: "test", Name: "default-model"},
111112
[]string{}, // No namespace restrictions for tests
113+
nil,
112114
)
113115

114116
// Call ReconcileKagentMCPServer

0 commit comments

Comments
 (0)