Skip to content

Commit 4b7e3b3

Browse files
committed
feat: load Agent Plugins in Go runtime
Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
1 parent 059c01b commit 4b7e3b3

20 files changed

Lines changed: 1008 additions & 50 deletions

File tree

docs/plans/api-v2-execution-plan.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,16 +196,21 @@ Implement lifecycle across the complete member set:
196196
- Public history remains readable while suspended or failed.
197197
- Emit lifecycle audit records, traces, and member-level failure details without exposing Actor IDs through ordinary APIs.
198198

199-
### K7 — Agent Plugins skills-only ingestion
199+
### K7 — Agent Plugins ingestion
200200

201201
Implement the selected Agent Plugins 1.0.0 subset:
202202

203-
- Immutable OCI digest, full Git commit, and versioned S3 ZIP sources.
203+
- Immutable OCI digest, full Git commit, and versioned S3-compatible ZIP sources.
204204
- Validate the canonical root `plugin.json`.
205205
- Materialize only explicitly selected Agent Skills and supporting files.
206+
- Load standard `mcp.json` entries for stdio, Streamable HTTP, and legacy SSE
207+
transports into the runtime configuration.
206208
- Reject path traversal, escaping symlinks, duplicate skill names, mutable references, and oversized packages.
207-
- Ignore `mcp.json`, client extensions, hooks, commands, apps, and other non-skill content.
208-
- Pin contents and digest in the prepared revision.
209+
- Ignore client extensions and content outside the Agent Plugins 1.0.0 skills
210+
and MCP component types.
211+
- Pin immutable source identities in the prepared revision. Fetch and validate
212+
package contents only in the runtime; destinations discovered in `mcp.json`
213+
remain blocked unless explicitly allowed by a future API.
209214

210215
Reuse existing artifact and skill materialization code where possible.
211216

go/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ RUN --mount=type=cache,target=/root/go/pkg/mod,rw \
3333
FROM alpine:3.22
3434
ARG TARGETPLATFORM
3535

36-
RUN apk add --no-cache bash ca-certificates
36+
RUN apk add --no-cache bash ca-certificates git && \
37+
mkdir -p /skills /plugins /data/plugins && \
38+
chown -R 65532:65532 /skills /plugins /data
3739

3840
WORKDIR /
3941
COPY --from=builder /app /app

go/adk/cmd/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
runnerpkg "github.com/kagent-dev/kagent/go/adk/pkg/runner"
2020
"github.com/kagent-dev/kagent/go/adk/pkg/session"
2121
"github.com/kagent-dev/kagent/go/adk/pkg/telemetry"
22+
"github.com/kagent-dev/kagent/go/core/v2/agentplugins"
2223
"go.uber.org/zap"
2324
"go.uber.org/zap/zapcore"
2425
)
@@ -91,6 +92,20 @@ func main() {
9192
logger.Error(err, "Failed to load agent config (model configuration is required)", "configDir", configDir)
9293
os.Exit(1)
9394
}
95+
pluginConfig, err := agentplugins.MaterializeFromEnv(logr.NewContext(context.Background(), logger))
96+
if err != nil {
97+
logger.Error(err, "Failed to materialize Agent Plugins")
98+
os.Exit(1)
99+
}
100+
agentConfig.HttpTools = append(agentConfig.HttpTools, pluginConfig.HTTP...)
101+
agentConfig.SseTools = append(agentConfig.SseTools, pluginConfig.SSE...)
102+
agentConfig.StdioTools = append(agentConfig.StdioTools, pluginConfig.Stdio...)
103+
if pluginConfig.HasSkills {
104+
if err := os.Setenv("KAGENT_SKILLS_FOLDER", agentplugins.DefaultSkillsRoot); err != nil {
105+
logger.Error(err, "Failed to configure Agent Plugins skills directory")
106+
os.Exit(1)
107+
}
108+
}
94109
logger.Info("Loaded agent config", "configDir", configDir)
95110
logger.Info("Agent configuration",
96111
"model", agentConfig.Model.GetType(),

go/adk/pkg/agent/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func CreateGoogleADKAgent(ctx context.Context, agentConfig *adk.AgentConfig, age
4747
if stsPlugin != nil {
4848
dynamicHeaderProvider = stsPlugin.HeaderProvider
4949
}
50-
toolsets := mcp.CreateToolsets(ctx, agentConfig.HttpTools, agentConfig.SseTools, propagateToken, dynamicHeaderProvider)
50+
toolsets := mcp.CreateToolsets(ctx, agentConfig.HttpTools, agentConfig.SseTools, agentConfig.StdioTools, propagateToken, dynamicHeaderProvider)
5151
mcpAppToolNames := mcp.MCPAppToolNamesFromToolsets(toolsets)
5252

5353
var remoteAgentTools []tool.Tool

go/adk/pkg/mcp/registry.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"net/http"
99
"os"
10+
"os/exec"
1011
"time"
1112

1213
"github.com/a2aproject/a2a-go/v2/a2asrv"
@@ -71,6 +72,10 @@ type mcpServerParams struct {
7172
PropagateToken bool // when true, Authorization is forwarded independently of AllowedHeaders
7273
HeaderProvider DynamicHeaderProvider // optional per-request headers derived from invocation context (e.g., STS exchanged access tokens)
7374
ServerType string // "http" or "sse"
75+
Command string
76+
Args []string
77+
Env map[string]string
78+
Dir string
7479
Timeout *float64
7580
SseReadTimeout *float64
7681
TLSInsecureSkipVerify *bool
@@ -93,12 +98,25 @@ func CreateToolsets(
9398
ctx context.Context,
9499
httpTools []adk.HttpMcpServerConfig,
95100
sseTools []adk.SseMcpServerConfig,
101+
stdioTools []adk.StdioMcpServerConfig,
96102
propagateToken bool,
97103
headerProvider DynamicHeaderProvider,
98104
) []tool.Toolset {
99105
log := logr.FromContextOrDiscard(ctx)
100106
var toolsets []tool.Toolset
101107

108+
log.Info("Processing stdio MCP tools", "stdioToolsCount", len(stdioTools))
109+
for i, stdioTool := range stdioTools {
110+
params := mcpServerParams{
111+
URL: stdioTool.Command, ServerType: "stdio", Command: stdioTool.Command,
112+
Args: stdioTool.Args, Env: stdioTool.Env, Dir: stdioTool.Dir,
113+
}
114+
ts, err := addToolset(ctx, log, params, nil, "stdio", i+1)
115+
if err == nil {
116+
toolsets = append(toolsets, ts)
117+
}
118+
}
119+
102120
log.Info("Processing HTTP MCP tools", "httpToolsCount", len(httpTools))
103121
for i, httpTool := range httpTools {
104122
params := mcpServerParams{
@@ -176,6 +194,15 @@ func addToolset(ctx context.Context, log logr.Logger, params mcpServerParams, to
176194
// Uses the official MCP SDK (github.com/modelcontextprotocol/go-sdk/mcp).
177195
func createTransport(ctx context.Context, params mcpServerParams) (mcpsdk.Transport, error) {
178196
log := logr.FromContextOrDiscard(ctx)
197+
if params.ServerType == "stdio" {
198+
command := exec.CommandContext(ctx, params.Command, params.Args...)
199+
command.Dir = params.Dir
200+
command.Env = os.Environ()
201+
for key, value := range params.Env {
202+
command.Env = append(command.Env, key+"="+value)
203+
}
204+
return &mcpsdk.CommandTransport{Command: command}, nil
205+
}
179206

180207
operationTimeout := defaultTimeout
181208
if params.Timeout != nil && *params.Timeout > 0 {

go/adk/pkg/mcp/registry_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net"
66
"net/http"
77
"net/http/httptest"
8+
"path/filepath"
89
"testing"
910

1011
"github.com/a2aproject/a2a-go/v2/a2asrv"
@@ -15,6 +16,20 @@ import (
1516
"google.golang.org/genai"
1617
)
1718

19+
func TestCreateTransportBuildsStdioCommand(t *testing.T) {
20+
dir := t.TempDir()
21+
transport, err := createTransport(t.Context(), mcpServerParams{
22+
ServerType: "stdio", Command: "server", Args: []string{"--stdio"}, Env: map[string]string{"PLUGIN_ROOT": dir}, Dir: dir,
23+
})
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
stdio, ok := transport.(*mcpsdk.CommandTransport)
28+
if !ok || filepath.Base(stdio.Command.Path) != "server" || len(stdio.Command.Args) != 2 || stdio.Command.Dir != dir {
29+
t.Fatalf("stdio transport = %#v", transport)
30+
}
31+
}
32+
1833
// newTestTransport returns a transport private to the test. These parallel
1934
// tests must not share newTestTransport(t): httptest.Server.Close (deferred
2035
// in each test) also closes the default transport's idle connections, which

go/api/adk/types.go

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ type SseMcpServerConfig struct {
4444
RequireApproval []string `json:"require_approval,omitempty"`
4545
}
4646

47+
// StdioMcpServerConfig starts one local MCP server without invoking a shell.
48+
type StdioMcpServerConfig struct {
49+
Command string `json:"command"`
50+
Args []string `json:"args,omitempty"`
51+
Env map[string]string `json:"env,omitempty"`
52+
Dir string `json:"dir,omitempty"`
53+
}
54+
4755
type Model interface {
4856
GetType() string
4957
}
@@ -584,18 +592,19 @@ func (c *AgentCompressionConfig) UnmarshalJSON(data []byte) error {
584592

585593
// See `python/packages/kagent-adk/src/kagent/adk/types.py` for the python version of this
586594
type AgentConfig struct {
587-
Model Model `json:"model"`
588-
Description string `json:"description"`
589-
Instruction string `json:"instruction"`
590-
HttpTools []HttpMcpServerConfig `json:"http_tools,omitempty"`
591-
SseTools []SseMcpServerConfig `json:"sse_tools,omitempty"`
592-
RemoteAgents []RemoteAgentConfig `json:"remote_agents,omitempty"`
593-
Stream *bool `json:"stream,omitempty"`
594-
Memory *MemoryConfig `json:"memory,omitempty"`
595-
Network *NetworkConfig `json:"network,omitempty"`
596-
ContextConfig *AgentContextConfig `json:"context_config,omitempty"`
597-
ShareTools *bool `json:"share_tools,omitempty"`
598-
SessionDBURL string `json:"session_db_url,omitempty"`
595+
Model Model `json:"model"`
596+
Description string `json:"description"`
597+
Instruction string `json:"instruction"`
598+
HttpTools []HttpMcpServerConfig `json:"http_tools,omitempty"`
599+
SseTools []SseMcpServerConfig `json:"sse_tools,omitempty"`
600+
StdioTools []StdioMcpServerConfig `json:"stdio_tools,omitempty"`
601+
RemoteAgents []RemoteAgentConfig `json:"remote_agents,omitempty"`
602+
Stream *bool `json:"stream,omitempty"`
603+
Memory *MemoryConfig `json:"memory,omitempty"`
604+
Network *NetworkConfig `json:"network,omitempty"`
605+
ContextConfig *AgentContextConfig `json:"context_config,omitempty"`
606+
ShareTools *bool `json:"share_tools,omitempty"`
607+
SessionDBURL string `json:"session_db_url,omitempty"`
599608
}
600609

601610
// GetStream returns the stream value or default if not set
@@ -608,18 +617,19 @@ func (a *AgentConfig) GetStream() bool {
608617

609618
func (a *AgentConfig) UnmarshalJSON(data []byte) error {
610619
var tmp struct {
611-
Model json.RawMessage `json:"model"`
612-
Description string `json:"description"`
613-
Instruction string `json:"instruction"`
614-
HttpTools []HttpMcpServerConfig `json:"http_tools,omitempty"`
615-
SseTools []SseMcpServerConfig `json:"sse_tools,omitempty"`
616-
RemoteAgents []RemoteAgentConfig `json:"remote_agents,omitempty"`
617-
Stream *bool `json:"stream,omitempty"`
618-
Memory json.RawMessage `json:"memory"`
619-
Network *NetworkConfig `json:"network,omitempty"`
620-
ContextConfig *AgentContextConfig `json:"context_config,omitempty"`
621-
ShareTools *bool `json:"share_tools,omitempty"`
622-
SessionDBURL string `json:"session_db_url,omitempty"`
620+
Model json.RawMessage `json:"model"`
621+
Description string `json:"description"`
622+
Instruction string `json:"instruction"`
623+
HttpTools []HttpMcpServerConfig `json:"http_tools,omitempty"`
624+
SseTools []SseMcpServerConfig `json:"sse_tools,omitempty"`
625+
StdioTools []StdioMcpServerConfig `json:"stdio_tools,omitempty"`
626+
RemoteAgents []RemoteAgentConfig `json:"remote_agents,omitempty"`
627+
Stream *bool `json:"stream,omitempty"`
628+
Memory json.RawMessage `json:"memory"`
629+
Network *NetworkConfig `json:"network,omitempty"`
630+
ContextConfig *AgentContextConfig `json:"context_config,omitempty"`
631+
ShareTools *bool `json:"share_tools,omitempty"`
632+
SessionDBURL string `json:"session_db_url,omitempty"`
623633
}
624634
if err := json.Unmarshal(data, &tmp); err != nil {
625635
return err
@@ -649,6 +659,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
649659
a.Instruction = tmp.Instruction
650660
a.HttpTools = tmp.HttpTools
651661
a.SseTools = tmp.SseTools
662+
a.StdioTools = tmp.StdioTools
652663
a.RemoteAgents = tmp.RemoteAgents
653664
a.Stream = tmp.Stream
654665
a.Memory = memory

go/api/adk/types_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,26 @@ package adk
22

33
import (
44
"encoding/json"
5+
"reflect"
56
"testing"
67
)
78

9+
func TestAgentConfigStdioToolsRoundTrip(t *testing.T) {
10+
want := []StdioMcpServerConfig{{Command: "server", Args: []string{"--stdio"}, Env: map[string]string{"KEY": "value"}, Dir: "/plugin"}}
11+
input := AgentConfig{Model: &OpenAI{BaseModel: BaseModel{Model: "test"}}, StdioTools: want}
12+
raw, err := json.Marshal(input)
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
var output AgentConfig
17+
if err := json.Unmarshal(raw, &output); err != nil {
18+
t.Fatal(err)
19+
}
20+
if !reflect.DeepEqual(output.StdioTools, want) {
21+
t.Fatalf("stdio tools = %#v, want %#v", output.StdioTools, want)
22+
}
23+
}
24+
825
func TestMarshalJSON_TypeDiscriminator(t *testing.T) {
926
tests := []struct {
1027
name string

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,24 @@ spec:
8484
bucket:
8585
minLength: 1
8686
type: string
87+
endpoint:
88+
description: Endpoint is the HTTP(S) endpoint of
89+
an AWS or S3-compatible service.
90+
pattern: ^https?://[^[:space:]]+$
91+
type: string
8792
key:
8893
minLength: 1
8994
type: string
95+
region:
96+
description: Region is used for request signing
97+
when required by the service.
98+
type: string
9099
versionId:
91100
minLength: 1
92101
type: string
93102
required:
94103
- bucket
104+
- endpoint
95105
- key
96106
- versionId
97107
type: object
@@ -184,14 +194,24 @@ spec:
184194
bucket:
185195
minLength: 1
186196
type: string
197+
endpoint:
198+
description: Endpoint is the HTTP(S) endpoint of
199+
an AWS or S3-compatible service.
200+
pattern: ^https?://[^[:space:]]+$
201+
type: string
187202
key:
188203
minLength: 1
189204
type: string
205+
region:
206+
description: Region is used for request signing
207+
when required by the service.
208+
type: string
190209
versionId:
191210
minLength: 1
192211
type: string
193212
required:
194213
- bucket
214+
- endpoint
195215
- key
196216
- versionId
197217
type: object

go/api/v1alpha3/agenttemplate_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ type GitArtifact struct {
137137

138138
// S3Object identifies one immutable S3 object version.
139139
type S3Object struct {
140+
// Endpoint is the HTTP(S) endpoint of an AWS or S3-compatible service.
141+
// +kubebuilder:validation:Pattern=`^https?://[^[:space:]]+$`
142+
// +required
143+
Endpoint string `json:"endpoint"`
140144
// +kubebuilder:validation:MinLength=1
141145
// +required
142146
Bucket string `json:"bucket"`
@@ -146,6 +150,9 @@ type S3Object struct {
146150
// +kubebuilder:validation:MinLength=1
147151
// +required
148152
VersionID string `json:"versionId"`
153+
// Region is used for request signing when required by the service.
154+
// +optional
155+
Region string `json:"region,omitempty"`
149156
}
150157

151158
// BucketArtifact selects the supported object-store provider.

0 commit comments

Comments
 (0)