diff --git a/docs/architecture/configuration-and-compilation.md b/docs/architecture/configuration-and-compilation.md index cca808828d..7ffe6e1121 100644 --- a/docs/architecture/configuration-and-compilation.md +++ b/docs/architecture/configuration-and-compilation.md @@ -298,10 +298,13 @@ download cache or fall back to an in-runtime OCI pull. Package limits and path/symlink validation still apply. Build skill images from scratch with just package contents; arbitrary ORAS artifact layouts are not -supported. Existing Git and S3 fetches remain runtime operations and retain -their golden-runtime networking limitations. The Python ADK does not yet consume -the `agent_plugins` runtime contract. BYO images must consume that contract to -load these skills; the Go ADK BYO fixture does so. +supported. Standalone Git and S3 sources use best-effort startup fetching. +The runtime retries missing skills before each task. +See [private Git skill sources](credential-injection.md#private-git-skill-sources) +for credential grants and networking constraints. +The Python ADK does not yet consume the `agent_plugins` runtime contract. +BYO images must consume that contract to load these skills; the Go ADK BYO +fixture does so. ### Runtime configuration diff --git a/docs/architecture/credential-injection.md b/docs/architecture/credential-injection.md index e3564be380..8ddddee192 100644 --- a/docs/architecture/credential-injection.md +++ b/docs/architecture/credential-injection.md @@ -61,6 +61,7 @@ overrides of these variables are rejected. | Gemini API key | `x-goog-api-key: ` | | Bedrock bearer token | `authorization: Bearer ` | | RemoteMCPServer Secret-backed header | Configured header; Secret contains its full value | +| AgentTemplate `skills[].source.git.authorizationFrom` | `authorization`; Secret contains its full value | Provider endpoint overrides determine the allowed HTTP(S) origin. Egress rules and credential bindings both match its scheme, DNS name, and port. Different @@ -120,6 +121,85 @@ This covers Go ADK MCP servers and models, OpenAI, Azure and Foundry memory embeddings, and Python OpenAI and Anthropic models. Python MCP, Claude and Codex still do not support passthrough. +## Private Git skill sources + +A standalone skill can use a private HTTPS Git repository. Its Secret must be +in the AgentTemplate's namespace. The Secret owner grants access to one exact +HTTPS origin with the `kagent.dev/git-credential-origin` annotation. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: skills-auth + annotations: + kagent.dev/git-credential-origin: https://github.com +stringData: + # GitHub: base64 of "x-access-token:". Other forges may accept "Bearer ". + authorization: Basic eC1hY2Nlc3MtdG9rZW46PHRva2VuPg== +--- +apiVersion: api.kagent.dev/v1alpha3 +kind: AgentTemplate +metadata: + name: reviewer +spec: + skills: + - name: review + source: + git: + url: https://github.com/acme/private-skills + commit: 0123456789abcdef0123456789abcdef01234567 + authorizationFrom: + name: skills-auth + key: authorization +``` + +The compiler checks the Secret, its non-empty key, and its authorized origin. +Missing references or an incorrect grant set `ResolvedRefs=False`. +Changes to the Secret trigger recompilation, including references from shared +child templates. HTTPS, DNS name, and port must match the grant. +Wildcards, IP addresses, URL credentials, and path-scoped grants are unsupported. + +The gateway replaces `Authorization` only when the request includes that header. +The runtime sends a placeholder scoped to the repository URL through Git's +`http..extraHeader` setting. Authenticated fetches reject redirects. +Neither the Git configuration nor the runtime receives the Secret value. + +The gateway binding covers the whole origin, not just the repository path. +Actor tools can also send an authorization header to that origin. +Use a read-only token restricted to the required repositories. +Separate permission to write Secrets from permission to write AgentTemplates. +Template writers cannot grant a Secret to another origin through a template. + +The annotation is a compilation-time grant. Removing it prevents new successful +compilations; it does not revoke credentials from existing pinned Actors. +Use the platform's credential and egress controls for immediate revocation. +Do not give the shared golden Actor unrestricted access to namespace Secrets. + +`authorizationFrom` is unsupported on `plugins[]`. SSH authentication and +token exchange are also unsupported. + +### When skills are fetched + +Go ADK, Claude, and Codex attempt standalone Git and S3 fetches at startup. +The startup attempt has a 30-second limit. A failed attempt logs a warning +and permits startup to continue. + +Before each task, the runtime retries skills that are not installed. +A failed fetch fails the task with `skills_unavailable: …`. +A later task retries the missing skills. Installed skills need no further +network access while their files remain available. + +OCI sources use worker-mounted image files. The runtime installs these skills +during preparation and fails preparation if a mounted source is invalid. +Plugin packages also remain eager. + +Golden preparation can lack network access or credential bindings. +The startup fallback permits preparation without fetching remote skills. +If startup fetching succeeds, snapshots can contain private skill files. +Snapshots do not contain the injected credential. Apply snapshot access controls +that protect the private repository content. + ## Deferred API fields The source retains commented declarations for Harness and SandboxTemplate diff --git a/go/adk/cmd/main.go b/go/adk/cmd/main.go index 867b476c35..e36f5d4848 100644 --- a/go/adk/cmd/main.go +++ b/go/adk/cmd/main.go @@ -68,14 +68,15 @@ func run(logLevel, host, port, configDir string) error { if port == "" && runtime.Port != nil { port = fmt.Sprint(runtime.GetPort()) } - if err := config.MaterializeAgentPlugins( + ensureSkills, err := config.MaterializeAgentPlugins( logging.IntoContext(context.Background(), logger), agentConfig, config.AgentPluginPaths{ Packages: defaultPluginPackagesRoot, Skills: config.DefaultSkillsRoot, Data: defaultPluginDataRoot, }, - ); err != nil { + ) + if err != nil { return fmt.Errorf("materialize Agent Plugins: %w", err) } logger.Info("loaded agent config", "config_dir", configDir) @@ -188,6 +189,7 @@ func run(logLevel, host, port, configDir string) error { Output: agentConfig.Output, Flush: providers.ForceFlush, AgentName: runtimeTelemetry.AgentName, + EnsureSkills: ensureSkills, }) if err != nil { return fmt.Errorf("create A2A executor: %w", err) diff --git a/go/adk/pkg/a2a/executor.go b/go/adk/pkg/a2a/executor.go index dd6469b7cf..86e7fd4222 100644 --- a/go/adk/pkg/a2a/executor.go +++ b/go/adk/pkg/a2a/executor.go @@ -43,6 +43,9 @@ type KAgentExecutorConfig struct { Output *configv1.ADKOutputConfig Flush func(context.Context) error AgentName string + // EnsureSkills installs downloaded standalone skills still missing after + // startup. A failure fails the task and the next task retries. + EnsureSkills func(context.Context) error } // KAgentExecutor keeps kagent's request/session glue around the upstream ADK @@ -56,6 +59,7 @@ type KAgentExecutor struct { flush func(context.Context) error runningUsage runningUsage agentName string + ensureSkills func(context.Context) error } type structuredOutput struct { @@ -127,6 +131,7 @@ func NewKAgentExecutor(cfg KAgentExecutorConfig) (*KAgentExecutor, error) { structuredOutputEnabled: output != nil, flush: cfg.Flush, agentName: cfg.AgentName, + ensureSkills: cfg.EnsureSkills, }, nil } @@ -295,6 +300,18 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.ExecutorCon "user_id", userID, ) + // Startup may lack egress for downloaded standalone skills; install any + // still missing before the agent runs, or fail the task visibly. + if e.ensureSkills != nil { + if err := e.ensureSkills(ctx); err != nil { + e.logger.ErrorContext(ctx, "standalone skills are unavailable", "task_id", reqCtx.TaskID, "error", err) + message := a2atype.NewMessageForTask(a2atype.MessageRoleAgent, reqCtx, a2atype.NewTextPart("skills_unavailable: "+err.Error())) + apia2a.SetTimelinePosition(message, time.Now()) + yield(a2atype.NewStatusUpdateEvent(reqCtx, a2atype.TaskStateFailed, message), nil) + return + } + } + // Run our own session management before upstream executor runs its prepareSession function. // This ensures that we create a session that contains metadata like x-kagent-source, // and the upstream executor will find this session already exists and skip creation. diff --git a/go/adk/pkg/a2a/executor_test.go b/go/adk/pkg/a2a/executor_test.go index 3039db4909..4f7b2c7b1b 100644 --- a/go/adk/pkg/a2a/executor_test.go +++ b/go/adk/pkg/a2a/executor_test.go @@ -2,6 +2,7 @@ package a2a import ( "context" + "errors" "iter" "log/slog" "maps" @@ -249,6 +250,50 @@ func TestKAgentExecutor_StampsStructuredOutputFailureMessage(t *testing.T) { } } +func TestKAgentExecutor_FailsTaskWhenSkillsAreUnavailable(t *testing.T) { + reqCtx := &a2asrv.ExecutorContext{ + TaskID: "task-1", ContextID: "ctx-1", + Message: a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart("hello")), + } + builtin := &recordingExecutor{} + fetchErr := errors.New(`materialize skill "review": exit status 128`) + executor := &KAgentExecutor{ + builtin: builtin, logger: slog.New(slog.DiscardHandler), + ensureSkills: func(context.Context) error { return fetchErr }, + } + var events []a2atype.Event + for event, err := range executor.Execute(context.Background(), reqCtx) { + if err != nil { + t.Fatalf("Execute() error = %v", err) + } + events = append(events, event) + } + if builtin.message != nil { + t.Fatal("the agent ran without its skills") + } + if len(events) != 1 { + t.Fatalf("events = %#v, want one failed status", events) + } + got, ok := events[0].(*a2atype.TaskStatusUpdateEvent) + if !ok || got.Status.State != a2atype.TaskStateFailed || got.Status.Message == nil { + t.Fatalf("status update = %#v, want failed status", events[0]) + } + text, _ := got.Status.Message.Parts[0].Content.(a2atype.Text) + if want := "skills_unavailable: " + fetchErr.Error(); string(text) != want { + t.Fatalf("failure message = %q, want %q", text, want) + } + + executor.ensureSkills = func(context.Context) error { return nil } + for _, err := range executor.Execute(context.Background(), reqCtx) { + if err != nil { + t.Fatalf("Execute() error = %v", err) + } + } + if builtin.message == nil { + t.Fatal("the agent did not run once its skills were present") + } +} + func TestKAgentExecutor_PreservesContentBearingLastChunk(t *testing.T) { reqCtx := &a2asrv.ExecutorContext{TaskID: "task-1", ContextID: "ctx-1"} reqCtx.Message = a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart("hi")) diff --git a/go/adk/pkg/agent/agent.go b/go/adk/pkg/agent/agent.go index 4496aebf83..289b0320a1 100644 --- a/go/adk/pkg/agent/agent.go +++ b/go/adk/pkg/agent/agent.go @@ -85,7 +85,10 @@ func createGoogleADKAgent(ctx context.Context, agentConfig *configv1.ADKAgentCon if err != nil { return nil, fmt.Errorf("failed to load skills: %w", err) } - if len(skills) > 0 { + // Downloaded standalone skills may arrive after startup, before a later + // task; the skill toolset lists the directory on every request. + deferredSkills := len(agentConfig.GetAgentPlugins().GetSkills()) > 0 + if len(skills) > 0 || deferredSkills { skillsToolset, err := skilltoolset.New(ctx, skilltoolset.Config{Source: skillsSource}) if err != nil { return nil, fmt.Errorf("failed to create skill toolset: %w", err) diff --git a/go/adk/pkg/agent/skills_test.go b/go/adk/pkg/agent/skills_test.go index 5097b2648d..ba01b0c780 100644 --- a/go/adk/pkg/agent/skills_test.go +++ b/go/adk/pkg/agent/skills_test.go @@ -108,3 +108,56 @@ func TestBuiltinToolsIndependentOfSkills(t *testing.T) { } } } + +// A downloaded standalone skill installed after startup must reach the model +// on the next request without rebuilding the agent. +func TestSkillToolsAdvertiseSkillsInstalledAfterStartup(t *testing.T) { + t.Setenv("OPENAI_API_KEY", "test-key") + requests := make(chan string, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request struct { + Messages []struct { + Content json.RawMessage `json:"content"` + } `json:"messages"` + } + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Errorf("decode model request: %v", err) + } + var content string + for _, message := range request.Messages { + content += string(message.Content) + } + requests <- content + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"test","object":"chat.completion","model":"test","choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}]}`)) + })) + t.Cleanup(server.Close) + + skillsDir := t.TempDir() + model := &configv1.ADKModelConfig{Model: "gpt-4o", Provider: &configv1.ADKModelConfig_Openai{Openai: &configv1.ADKOpenAI{BaseUrl: server.URL}}} + cfg := &configv1.ADKConfig{Agent: &configv1.ADKAgentConfig{ + Model: model, SkillsDirectory: skillsDir, Stream: new(false), + AgentPlugins: &configv1.ADKAgentPlugins{Skills: []*configv1.ADKSkill{{Name: "demo", Source: &configv1.ADKPluginSource{ + Source: &configv1.ADKPluginSource_Git{Git: &configv1.ADKPluginGitSource{Url: "https://git.example.com/skills", Commit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, + }}}}, + }} + root, err := CreateGoogleADKAgent(t.Context(), cfg, "root", nil) + require.NoError(t, err) + + require.NoError(t, os.Mkdir(filepath.Join(skillsDir, "demo"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(skillsDir, "demo", "SKILL.md"), []byte("---\nname: demo\ndescription: Deferred demo skill.\n---\nRead files.\n"), 0644)) + sessions := adksession.InMemoryService() + r, err := runner.New(runner.Config{AppName: "test", Agent: root, SessionService: sessions}) + require.NoError(t, err) + sess, err := sessions.Create(t.Context(), &adksession.CreateRequest{AppName: "test", UserID: "user"}) + require.NoError(t, err) + for _, err := range r.Run(t.Context(), "user", sess.Session.ID(), genai.NewContentFromText("Which skills do you have?", genai.RoleUser), adkagent.RunConfig{}) { + require.NoError(t, err) + } + select { + case content := <-requests: + require.Contains(t, content, "Deferred demo skill.") + case <-time.After(time.Second): + t.Fatal("model received no request") + } +} diff --git a/go/adk/pkg/config/agentplugins.go b/go/adk/pkg/config/agentplugins.go index f4bf1adb19..c63640e459 100644 --- a/go/adk/pkg/config/agentplugins.go +++ b/go/adk/pkg/config/agentplugins.go @@ -2,6 +2,7 @@ package config import ( "context" + "errors" "fmt" "path/filepath" @@ -22,36 +23,53 @@ type AgentPluginPaths struct { // MaterializeAgentPlugins materializes plugins independently for each agent // and adds their skills and MCP servers to the ADK runtime configuration. -func MaterializeAgentPlugins(ctx context.Context, agentConfig *configv1.ADKAgentConfig, paths AgentPluginPaths) error { - if agentConfig.AgentPlugins != nil { - materialization, err := agentplugins.Materialize(ctx, PluginResources(agentConfig.AgentPlugins), agentplugins.Paths{ - Packages: paths.Packages, - Skills: paths.Skills, - }) - if err != nil { - return fmt.Errorf("materialize agent plugins: %w", err) - } - if len(materialization.ClaudeFormatPluginRoots()) > 0 { - return fmt.Errorf("plugin is Claude-format; only the Agent Plugins format (plugin.json at the plugin root) is supported here") +// Downloaded standalone skills are fetched best-effort at startup; the +// returned function installs any still missing and must run before each task. +func MaterializeAgentPlugins(ctx context.Context, agentConfig *configv1.ADKAgentConfig, paths AgentPluginPaths) (func(context.Context) error, error) { + var materializations []agentplugins.Materialization + var visit func(*configv1.ADKAgentConfig, AgentPluginPaths) error + visit = func(agentConfig *configv1.ADKAgentConfig, paths AgentPluginPaths) error { + if agentConfig.AgentPlugins != nil { + materialization, err := agentplugins.Materialize(ctx, PluginResources(agentConfig.AgentPlugins), agentplugins.Paths{ + Packages: paths.Packages, + Skills: paths.Skills, + }) + if err != nil { + return fmt.Errorf("materialize agent plugins: %w", err) + } + if len(materialization.ClaudeFormatPluginRoots()) > 0 { + return fmt.Errorf("plugin is Claude-format; only the Agent Plugins format (plugin.json at the plugin root) is supported here") + } + mcpConfig, err := agentplugins.LoadMCP(ctx, materialization, paths.Data) + if err != nil { + return fmt.Errorf("load agent plugin MCP configuration: %w", err) + } + addMCPConfig(agentConfig, mcpConfig) + agentConfig.SkillsDirectory = materialization.SkillsDirectory + materializations = append(materializations, materialization) } - mcpConfig, err := agentplugins.LoadMCP(ctx, materialization, paths.Data) - if err != nil { - return fmt.Errorf("load agent plugin MCP configuration: %w", err) + for i, child := range agentConfig.SubAgents { + childRoot := filepath.Join("subagents", fmt.Sprintf("%d", i)) + if err := visit(child, AgentPluginPaths{ + Packages: filepath.Join(paths.Packages, childRoot), + Skills: filepath.Join(paths.Skills, childRoot), + Data: filepath.Join(paths.Data, childRoot), + }); err != nil { + return fmt.Errorf("materialize sub-agent %q: %w", child.Name, err) + } } - addMCPConfig(agentConfig, mcpConfig) - agentConfig.SkillsDirectory = materialization.SkillsDirectory + return nil } - for i, child := range agentConfig.SubAgents { - childRoot := filepath.Join("subagents", fmt.Sprintf("%d", i)) - if err := MaterializeAgentPlugins(ctx, child, AgentPluginPaths{ - Packages: filepath.Join(paths.Packages, childRoot), - Skills: filepath.Join(paths.Skills, childRoot), - Data: filepath.Join(paths.Data, childRoot), - }); err != nil { - return fmt.Errorf("materialize sub-agent %q: %w", child.Name, err) - } + if err := visit(agentConfig, paths); err != nil { + return nil, err } - return nil + return func(ctx context.Context) error { + var errs []error + for _, materialization := range materializations { + errs = append(errs, materialization.EnsureSkills(ctx)) + } + return errors.Join(errs...) + }, nil } func addMCPConfig(agentConfig *configv1.ADKAgentConfig, mcpConfig agentplugins.MCPConfig) { diff --git a/go/adk/pkg/config/agentplugins_test.go b/go/adk/pkg/config/agentplugins_test.go index 4a6455e24c..c02890a8d6 100644 --- a/go/adk/pkg/config/agentplugins_test.go +++ b/go/adk/pkg/config/agentplugins_test.go @@ -35,7 +35,11 @@ func TestMaterializeAgentPluginsIsolatesSubagentSkills(t *testing.T) { t.Fatal(err) } } - if err := MaterializeAgentPlugins(context.Background(), agentConfig, paths); err != nil { + ensureSkills, err := MaterializeAgentPlugins(context.Background(), agentConfig, paths) + if err != nil { + t.Fatal(err) + } + if err := ensureSkills(context.Background()); err != nil { t.Fatal(err) } if agentConfig.SkillsDirectory == agentConfig.SubAgents[0].SkillsDirectory { @@ -64,7 +68,7 @@ func TestMaterializeAgentPluginsRejectsClaudeFormatPlugin(t *testing.T) { } source := agentplugin.Source{Git: &agentplugin.GitSource{URL: "unused", Commit: strings.Repeat("a", 40)}} agentConfig := &configv1.ADKAgentConfig{AgentPlugins: PluginConfig(agentplugin.Resources{Plugins: []agentplugin.Bundle{{Source: source}}})} - if err := MaterializeAgentPlugins(context.Background(), agentConfig, paths); err == nil { + if _, err := MaterializeAgentPlugins(context.Background(), agentConfig, paths); err == nil { t.Fatal("MaterializeAgentPlugins() accepted a Claude-format plugin") } } diff --git a/go/adk/pkg/config/contract_test.go b/go/adk/pkg/config/contract_test.go index be4134d3d8..46a8de6c98 100644 --- a/go/adk/pkg/config/contract_test.go +++ b/go/adk/pkg/config/contract_test.go @@ -70,6 +70,7 @@ func TestSharedAgentContract(t *testing.T) { require.Equal(t, "version", cfg.Agent.AgentPlugins.Plugins[1].Source.GetS3().VersionId) resources := runtimeconfig.PluginResources(cfg.Agent.AgentPlugins) require.Equal(t, "/run/kagent/artifacts/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", resources.Skills[0].Source.Directory) + require.True(t, resources.Plugins[0].Source.Git.GatewayAuthorization) require.True(t, proto.Equal(cfg.Agent.AgentPlugins, runtimeconfig.PluginConfig(resources))) encoded, err := runtimeconfig.Marshal(cfg) require.NoError(t, err) diff --git a/go/adk/pkg/config/plugins.go b/go/adk/pkg/config/plugins.go index 67f4088491..8833aaac1a 100644 --- a/go/adk/pkg/config/plugins.go +++ b/go/adk/pkg/config/plugins.go @@ -22,7 +22,9 @@ func pluginSource(source agentplugin.Source) *configv1.ADKPluginSource { case source.Directory != "": result.Source = &configv1.ADKPluginSource_Directory{Directory: source.Directory} case source.Git != nil: - result.Source = &configv1.ADKPluginSource_Git{Git: &configv1.ADKPluginGitSource{Url: source.Git.URL, Commit: source.Git.Commit}} + result.Source = &configv1.ADKPluginSource_Git{Git: &configv1.ADKPluginGitSource{ + Url: source.Git.URL, Commit: source.Git.Commit, GatewayAuthorization: source.Git.GatewayAuthorization, + }} case source.S3 != nil: result.Source = &configv1.ADKPluginSource_S3{S3: &configv1.ADKPluginS3Source{Endpoint: source.S3.Endpoint, Bucket: source.S3.Bucket, Key: source.S3.Key, VersionId: source.S3.VersionID, Region: source.S3.Region}} } @@ -46,7 +48,7 @@ func pluginResourceSource(source *configv1.ADKPluginSource) agentplugin.Source { case *configv1.ADKPluginSource_Directory: result.Directory = value.Directory case *configv1.ADKPluginSource_Git: - result.Git = &agentplugin.GitSource{URL: value.Git.Url, Commit: value.Git.Commit} + result.Git = &agentplugin.GitSource{URL: value.Git.Url, Commit: value.Git.Commit, GatewayAuthorization: value.Git.GatewayAuthorization} case *configv1.ADKPluginSource_S3: result.S3 = &agentplugin.S3Source{Endpoint: value.S3.Endpoint, Bucket: value.S3.Bucket, Key: value.S3.Key, VersionID: value.S3.VersionId, Region: value.S3.Region} } diff --git a/go/adk/pkg/config/testdata/agent.json b/go/adk/pkg/config/testdata/agent.json index 3de59c06f2..42fcf62cce 100644 --- a/go/adk/pkg/config/testdata/agent.json +++ b/go/adk/pkg/config/testdata/agent.json @@ -132,7 +132,8 @@ "source": { "git": { "url": "https://git.example.com/plugin", - "commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "gateway_authorization": true }, "path": "skills" }, diff --git a/go/api/agentplugin/types.go b/go/api/agentplugin/types.go index 9948aa6c0d..dfc95b57b1 100644 --- a/go/api/agentplugin/types.go +++ b/go/api/agentplugin/types.go @@ -2,8 +2,9 @@ // standalone skills and Agent Plugin packages. package agentplugin -// Resources describes the skill resources a Harness must materialize before -// starting an agent. +// Resources describes the skill resources a Harness materializes before +// starting an agent. Downloaded standalone skills that cannot be fetched then +// are retried before each task. type Resources struct { Skills []Skill `json:"skills,omitempty"` Plugins []Bundle `json:"plugins,omitempty"` @@ -35,6 +36,10 @@ type Source struct { type GitSource struct { URL string `json:"url"` Commit string `json:"commit"` + // GatewayAuthorization reports that the egress gateway owns this host's + // Authorization header. The runtime sends a placeholder value for the + // gateway to replace; it never holds the credential. + GatewayAuthorization bool `json:"gateway_authorization,omitempty"` } // S3Source identifies one immutable S3 object version. diff --git a/go/api/gen/kagent/config/v1/adk.pb.go b/go/api/gen/kagent/config/v1/adk.pb.go index 447e112e62..82a96a4d03 100644 --- a/go/api/gen/kagent/config/v1/adk.pb.go +++ b/go/api/gen/kagent/config/v1/adk.pb.go @@ -2666,11 +2666,14 @@ func (*ADKPluginSource_S3) isADKPluginSource_Source() {} func (*ADKPluginSource_Directory) isADKPluginSource_Source() {} type ADKPluginGitSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` + // The egress gateway owns this host's Authorization header. The runtime sends + // a placeholder for the gateway to replace and never holds the credential. + GatewayAuthorization bool `protobuf:"varint,3,opt,name=gateway_authorization,json=gatewayAuthorization,proto3" json:"gateway_authorization,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ADKPluginGitSource) Reset() { @@ -2717,6 +2720,13 @@ func (x *ADKPluginGitSource) GetCommit() string { return "" } +func (x *ADKPluginGitSource) GetGatewayAuthorization() bool { + if x != nil { + return x.GatewayAuthorization + } + return false +} + type ADKPluginS3Source struct { state protoimpl.MessageState `protogen:"open.v1"` Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` @@ -3230,10 +3240,11 @@ const file_kagent_config_v1_adk_proto_rawDesc = "" + "\x02s3\x18\x03 \x01(\v2#.kagent.config.v1.ADKPluginS3SourceH\x00R\x02s3\x12\x1e\n" + "\tdirectory\x18\x05 \x01(\tH\x00R\tdirectory\x12\x12\n" + "\x04path\x18\x04 \x01(\tR\x04pathB\x0f\n" + - "\x06source\x12\x05\xbaH\x02\b\x01\">\n" + + "\x06source\x12\x05\xbaH\x02\b\x01\"s\n" + "\x12ADKPluginGitSource\x12\x10\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n" + - "\x06commit\x18\x02 \x01(\tR\x06commit\"\x90\x01\n" + + "\x06commit\x18\x02 \x01(\tR\x06commit\x123\n" + + "\x15gateway_authorization\x18\x03 \x01(\bR\x14gatewayAuthorization\"\x90\x01\n" + "\x11ADKPluginS3Source\x12\x1a\n" + "\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x16\n" + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x10\n" + diff --git a/go/api/v1alpha3/agenttemplate_types.go b/go/api/v1alpha3/agenttemplate_types.go index 1a67936b1d..2299e15d69 100644 --- a/go/api/v1alpha3/agenttemplate_types.go +++ b/go/api/v1alpha3/agenttemplate_types.go @@ -23,6 +23,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// GitCredentialOriginAnnotation authorizes a Secret for Git skill fetching at +// one exact HTTPS origin. Only the Secret owner can grant this authority. +const GitCredentialOriginAnnotation = "kagent.dev/git-credential-origin" + // AgentTemplatePromptTemplateSpec enables Go template rendering and ConfigMap includes. type AgentTemplatePromptTemplateSpec struct { // DataSources are same-namespace ConfigMaps available to include("source/key"). @@ -102,6 +106,7 @@ type AgentTemplateSkill struct { } // GitArtifact identifies immutable content at a full Git commit ID. +// +kubebuilder:validation:XValidation:rule="!has(self.authorizationFrom) || self.url.startsWith('https://')",message="authorizationFrom requires an https URL" type GitArtifact struct { // +kubebuilder:validation:Pattern=`^https?://[^[:space:]]+$` // +kubebuilder:validation:MinLength=1 @@ -110,6 +115,14 @@ type GitArtifact struct { // +kubebuilder:validation:Pattern=`^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$` // +required Commit string `json:"commit"` + // AuthorizationFrom references a same-namespace Secret key containing the + // complete HTTP Authorization header, such as `Basic `. + // The Secret must authorize the repository's exact HTTPS origin through + // its kagent.dev/git-credential-origin annotation (scheme, DNS name, port). + // The gateway replaces matching Authorization headers; the credential + // never enters the runtime. Supported for standalone skills only. + // +optional + AuthorizationFrom *SecretKeyReference `json:"authorizationFrom,omitempty"` } // S3Object identifies one immutable S3 object version. @@ -157,6 +170,7 @@ type ArtifactSource struct { } // PluginBundle selects Agent Skills from one immutable Agent Plugins package. +// +kubebuilder:validation:XValidation:rule="!has(self.source.git) || !has(self.source.git.authorizationFrom)",message="authorizationFrom is supported only for skills[].source.git" type PluginBundle struct { // +required Source ArtifactSource `json:"source"` diff --git a/go/api/v1alpha3/common_types.go b/go/api/v1alpha3/common_types.go index 80a19120f0..57e0794efd 100644 --- a/go/api/v1alpha3/common_types.go +++ b/go/api/v1alpha3/common_types.go @@ -40,6 +40,21 @@ type ConfigMapKeyReference struct { Key string `json:"key"` } +// SecretKeyReference selects a key of a Secret in the referencing object's +// namespace. +type SecretKeyReference struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` + // +required + Name string `json:"name"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[-._a-zA-Z0-9]+$` + // +required + Key string `json:"key"` +} + // FromNamespaces specifies namespace from which references to this resource are allowed. // This follows the same pattern as Gateway API's cross-namespace route attachment. // See: https://gateway-api.sigs.k8s.io/guides/multiple-ns/#cross-namespace-route-attachment diff --git a/go/api/v1alpha3/configuration_crd_cel_test.go b/go/api/v1alpha3/configuration_crd_cel_test.go index 0dee6f572e..43660eb9b9 100644 --- a/go/api/v1alpha3/configuration_crd_cel_test.go +++ b/go/api/v1alpha3/configuration_crd_cel_test.go @@ -538,6 +538,47 @@ func TestConfigurationCRDValidation(t *testing.T) { } } }) + + t.Run("git skill authorizationFrom", func(t *testing.T) { + commit := strings.Repeat("a", 40) + git := func(url string, authorization map[string]any) map[string]any { + source := map[string]any{"url": url, "commit": commit} + if authorization != nil { + source["authorizationFrom"] = authorization + } + return map[string]any{"git": source} + } + secretKey := map[string]any{"name": "skills-auth", "key": "authorization"} + for _, tc := range []struct { + name string + path []string + entry map[string]any + wantReject string + }{ + {name: "skill", path: []string{"spec", "skills"}, entry: map[string]any{"name": "review", "source": git("https://github.com/acme/skills", secretKey)}}, + {name: "public-skill", path: []string{"spec", "skills"}, entry: map[string]any{"name": "review", "source": git("http://git.example.com/acme/skills", nil)}}, + {name: "plain-http", path: []string{"spec", "skills"}, entry: map[string]any{"name": "review", "source": git("http://git.example.com/acme/skills", secretKey)}, wantReject: "authorizationFrom requires an https URL"}, + {name: "missing-key", path: []string{"spec", "skills"}, entry: map[string]any{"name": "review", "source": git("https://github.com/acme/skills", map[string]any{"name": "skills-auth"})}, wantReject: "key: Required value"}, + {name: "empty-key", path: []string{"spec", "skills"}, entry: map[string]any{"name": "review", "source": git("https://github.com/acme/skills", map[string]any{"name": "skills-auth", "key": ""})}, wantReject: "authorizationFrom.key"}, + {name: "namespace", path: []string{"spec", "skills"}, entry: map[string]any{"name": "review", "source": git("https://github.com/acme/skills", map[string]any{"name": "skills-auth", "key": "authorization", "namespace": "other"})}, wantReject: "unknown field"}, + {name: "plugin", path: []string{"spec", "plugins"}, entry: map[string]any{"source": git("https://github.com/acme/plugin", secretKey)}, wantReject: "authorizationFrom is supported only for skills[].source.git"}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(validAgentTemplate(namespace, "git-auth-"+tc.name, nil)) + require.NoError(t, err) + object := &unstructured.Unstructured{Object: data} + object.SetAPIVersion(GroupVersion.String()) + object.SetKind("AgentTemplate") + require.NoError(t, unstructured.SetNestedSlice(data, []any{tc.entry}, tc.path...)) + err = cl.Create(ctx, object, &ctrlclient.CreateOptions{FieldValidation: metav1.FieldValidationStrict}) + if tc.wantReject != "" { + require.ErrorContains(t, err, tc.wantReject) + return + } + require.NoError(t, err) + }) + } + }) } func sandboxTemplateForValidation(namespace, name string, mutate func(*SandboxTemplateSpec)) *SandboxTemplate { diff --git a/go/api/v1alpha3/zz_generated.deepcopy.go b/go/api/v1alpha3/zz_generated.deepcopy.go index 0179022d34..61655ec2ae 100644 --- a/go/api/v1alpha3/zz_generated.deepcopy.go +++ b/go/api/v1alpha3/zz_generated.deepcopy.go @@ -392,7 +392,7 @@ func (in *ArtifactSource) DeepCopyInto(out *ArtifactSource) { if in.Git != nil { in, out := &in.Git, &out.Git *out = new(GitArtifact) - **out = **in + (*in).DeepCopyInto(*out) } if in.Bucket != nil { in, out := &in.Bucket, &out.Bucket @@ -757,6 +757,11 @@ func (in *GeminiVertexAIConfig) DeepCopy() *GeminiVertexAIConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitArtifact) DeepCopyInto(out *GitArtifact) { *out = *in + if in.AuthorizationFrom != nil { + in, out := &in.AuthorizationFrom, &out.AuthorizationFrom + *out = new(SecretKeyReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitArtifact. @@ -1829,6 +1834,21 @@ func (in *SandboxTemplateWorkload) DeepCopy() *SandboxTemplateWorkload { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeyReference) DeepCopyInto(out *SecretKeyReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyReference. +func (in *SecretKeyReference) DeepCopy() *SecretKeyReference { + if in == nil { + return nil + } + out := new(SecretKeyReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretReference) DeepCopyInto(out *SecretReference) { *out = *in diff --git a/go/core/internal/controller/skills_test.go b/go/core/internal/controller/skills_test.go new file mode 100644 index 0000000000..a709222989 --- /dev/null +++ b/go/core/internal/controller/skills_test.go @@ -0,0 +1,96 @@ +package controller + +import ( + "strings" + "testing" + + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "istio.io/istio/pkg/kube/krt" + "istio.io/istio/pkg/kube/krt/krttest" + corev1 "k8s.io/api/core/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kagentv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" + "github.com/kagent-dev/kagent/go/core/internal/translator" +) + +func TestGitSkillSecretChangesReconcileReferences(t *testing.T) { + for _, shared := range []bool{false, true} { + name := "root" + if shared { + name = "shared child" + } + t.Run(name, func(t *testing.T) { + stop := make(chan struct{}) + t.Cleanup(func() { close(stop) }) + opts := krt.NewOptionsBuilder(stop, "git-skills", nil) + template := &kagentv1alpha3.AgentTemplate{ + ObjectMeta: metav1.ObjectMeta{Namespace: "team-a", Name: "assistant"}, + Spec: kagentv1alpha3.AgentTemplateSpec{SystemPrompt: "help", ModelConfig: &corev1.LocalObjectReference{Name: "model"}, + Skills: []kagentv1alpha3.AgentTemplateSkill{{Name: "review", Source: kagentv1alpha3.ArtifactSource{Git: &kagentv1alpha3.GitArtifact{ + URL: "https://github.com/acme/skills", Commit: strings.Repeat("a", 40), + AuthorizationFrom: &kagentv1alpha3.SecretKeyReference{Name: "git-auth", Key: "authorization"}, + }}}}, + }, + } + templates := []*kagentv1alpha3.AgentTemplate{template} + if shared { + child := template.DeepCopy() + child.Name = "child" + template.Spec.Skills = nil + template.Spec.Tools = []kagentv1alpha3.ToolBinding{{SubAgent: &kagentv1alpha3.SubAgentToolBinding{ + Name: "reviewer", Description: "Review changes", TemplateRef: &corev1.LocalObjectReference{Name: child.Name}, + }}} + templates = append(templates, child) + } + selectedHarness := harness("team-a", "kagent", nil) + selectedHarness.Spec.Kagent = &kagentv1alpha3.KagentHarness{} + selectedHarness.Spec.Workload.Image = new("example.com/kagent@sha256:" + strings.Repeat("a", 64)) + selectedHarness.Spec.Substrate = kagentv1alpha3.RuntimeSubstratePolicy{ + WorkerPoolRef: corev1.LocalObjectReference{Name: "default"}, SnapshotPolicy: kagentv1alpha3.RuntimeSnapshotPolicy{Location: "snapshots"}, + } + mock := krttest.NewMock(t, []any{selectedHarness, &atev1alpha1.WorkerPool{ObjectMeta: metav1.ObjectMeta{Namespace: "team-a", Name: "default"}}}) + secrets := krt.NewStaticCollection[*corev1.Secret](nil, nil, opts.WithName("Secrets")...) + models := krt.NewStaticCollection(nil, []*kagentv1alpha3.ModelConfig{{ObjectMeta: metav1.ObjectMeta{Namespace: "team-a", Name: "model"}, Spec: kagentv1alpha3.ModelConfigSpec{Provider: kagentv1alpha3.ModelProviderOpenAI, Model: "gpt-5"}}}, opts.WithName("Models")...) + configMaps := krttest.GetMockCollection[*corev1.ConfigMap](mock) + _, resolvedModels := newModelConfigReconciliations(models, configMaps, secrets, opts) + agents := krt.NewStaticCollection(nil, []*kagentv1alpha3.Agent{testAgent(template, selectedHarness)}, opts.WithName("Agents")...) + states := newAgentReconciliations(agents, translator.Collections{ + AgentTemplates: krt.NewStaticCollection(nil, templates, opts.WithName("Templates")...), + Harnesses: krttest.GetMockCollection[*kagentv1alpha3.Harness](mock), ResolvedModelConfigs: resolvedModels, + RemoteMCPServers: krttest.GetMockCollection[*kagentv1alpha3.RemoteMCPServer](mock), ConfigMaps: configMaps, + Secrets: secrets, WorkerPools: krttest.GetMockCollection[*atev1alpha1.WorkerPool](mock), + }, krt.NewStaticCollection[AgentRuntimeObservation](nil, nil, opts.WithName("Observations")...), opts, testRuntimeDefaults(), nil) + statuses := newAgentStatuses(agents, states, opts) + awaitReferenceFailure := func(message string) { + t.Helper() + waitFor(t, func() bool { + updates := statuses.List() + if len(updates) != 1 { + return false + } + condition := apimeta.FindStatusCondition(updates[0].Status.Conditions, kagentv1alpha3.AgentConditionResolvedRefs) + return condition != nil && condition.Status == metav1.ConditionFalse && condition.Reason == "ReferenceResolutionFailed" && strings.Contains(condition.Message, message) + }) + } + awaitReferenceFailure("not found") + secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "team-a", Name: "git-auth"}} + secrets.UpdateObject(secret.DeepCopy()) + awaitReferenceFailure("non-empty key") + secret.Data = map[string][]byte{"authorization": []byte("Bearer fixture-only")} + secrets.UpdateObject(secret.DeepCopy()) + awaitReferenceFailure("must authorize origin") + secret.Annotations = map[string]string{kagentv1alpha3.GitCredentialOriginAnnotation: "https://github.com"} + secrets.UpdateObject(secret.DeepCopy()) + waitFor(t, func() bool { + values := states.List() + return len(values) == 1 && values[0].CompilationFailure == nil && values[0].Target != nil + }) + // Revoking the grant must invalidate compilation without editing the Agent. + secret.Annotations = nil + secrets.UpdateObject(secret.DeepCopy()) + awaitReferenceFailure("must authorize origin") + }) + } +} diff --git a/go/core/internal/skillsinit/git.go b/go/core/internal/skillsinit/git.go index 11f27bbdef..3455bc2e6c 100644 --- a/go/core/internal/skillsinit/git.go +++ b/go/core/internal/skillsinit/git.go @@ -1,42 +1,95 @@ package skillsinit import ( + "context" "fmt" "os" "os/exec" "regexp" + "strconv" ) var immutableGitCommit = regexp.MustCompile(`^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$`) +// GatewayAuthorizationPlaceholder is the Authorization header value sent for +// gateway-authorized sources. Substrate's egress gateway replaces only headers +// a request already carries, so the runtime must send one; the gateway +// overwrites the whole value with the referenced Secret. +const GatewayAuthorizationPlaceholder = "kagent-credential-injected" + // CloneGitCommit fetches only one immutable commit instead of cloning the -// repository's complete history. -func CloneGitCommit(url, commit, destination string) error { +// repository's complete history. When gatewayAuthorization is set, requests to +// url carry a placeholder Authorization header for the egress gateway to +// replace; the credential itself never reaches this process. A failed or +// cancelled fetch removes destination. +func CloneGitCommit(ctx context.Context, url, commit, destination string, gatewayAuthorization bool) (err error) { if !immutableGitCommit.MatchString(commit) { return fmt.Errorf("git commit must be a full SHA") } if err := os.MkdirAll(destination, 0o755); err != nil { return err } - if err := runGitIn(destination, "init"); err != nil { + defer func() { + if err != nil { + _ = os.RemoveAll(destination) + } + }() + if err := runGitIn(ctx, destination, nil, "init", "--quiet"); err != nil { return err } - if err := runGitIn(destination, "remote", "add", "origin", url); err != nil { + if err := runGitIn(ctx, destination, nil, "remote", "add", "origin", url); err != nil { return err } - if err := runGitIn(destination, "fetch", "--depth", "1", "origin", commit); err != nil { + var config []gitConfig + if gatewayAuthorization { + config = gatewayAuthorizationConfig(url) + } + if err := runGitIn(ctx, destination, config, "fetch", "--depth", "1", "origin", commit); err != nil { return err } - return runGitIn(destination, "checkout", "--detach", "FETCH_HEAD") + return runGitIn(ctx, destination, nil, "checkout", "--quiet", "--detach", "FETCH_HEAD") } -func runGitIn(dir string, args ...string) error { - cmd := exec.Command("git", args...) - if dir != "" { - cmd.Dir = dir +type gitConfig struct{ key, value string } + +// gatewayAuthorizationConfig scopes the placeholder header to url. Redirects +// are refused because Git keeps sending configured headers after following +// one, which would hand the placeholder to a destination the gateway may also +// authorize, and so let the redirecting server choose who receives the +// credential. +func gatewayAuthorizationConfig(url string) []gitConfig { + return []gitConfig{ + {key: "http." + url + ".extraHeader", value: "Authorization: " + GatewayAuthorizationPlaceholder}, + {key: "http.followRedirects", value: "false"}, } +} + +// gitCommand builds a git command in dir. config is passed through +// GIT_CONFIG_* for this command only, so nothing is written to .git/config, +// nothing appears in argv, and URLs containing '=' cannot split the key from +// the value. +func gitCommand(ctx context.Context, dir string, config []gitConfig, args ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, "git", args...) + cmd.Dir = dir + // A runtime has no terminal: an authentication failure must fail the + // fetch instead of waiting for a username prompt. + cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_CONFIG_COUNT="+strconv.Itoa(len(config))) + for i, entry := range config { + index := strconv.Itoa(i) + cmd.Env = append(cmd.Env, "GIT_CONFIG_KEY_"+index+"="+entry.key, "GIT_CONFIG_VALUE_"+index+"="+entry.value) + } + return cmd +} + +func runGitIn(ctx context.Context, dir string, config []gitConfig, args ...string) error { + cmd := gitCommand(ctx, dir, config, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Env = os.Environ() - return cmd.Run() + if err := cmd.Run(); err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return fmt.Errorf("git %s: %w", args[0], ctxErr) + } + return fmt.Errorf("git %s: %w", args[0], err) + } + return nil } diff --git a/go/core/internal/skillsinit/git_auth_test.go b/go/core/internal/skillsinit/git_auth_test.go new file mode 100644 index 0000000000..2c06a2dfa1 --- /dev/null +++ b/go/core/internal/skillsinit/git_auth_test.go @@ -0,0 +1,243 @@ +package skillsinit + +import ( + "context" + "io/fs" + "net/http" + "net/http/cgi" + "net/http/httptest" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testAuthorization is a dummy fixture credential, never a real Secret. +const testAuthorization = "Basic eC1hY2Nlc3MtdG9rZW46c2VjcmV0" // x-access-token:secret + +// isolateGit keeps developer credential helpers, askpass programs and global +// configuration out of the fetch under test. +func isolateGit(t *testing.T) { + t.Helper() + t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull) + t.Setenv("GIT_CONFIG_NOSYSTEM", "1") + t.Setenv("GIT_ASKPASS", "") + t.Setenv("SSH_ASKPASS", "") +} + +// privateGitServer serves one repository over Git smart HTTP and rejects every +// request without the expected Authorization header, like a private forge. +func privateGitServer(t *testing.T) (serverURL, commit string) { + t.Helper() + root := t.TempDir() + repository := filepath.Join(root, "skills.git") + require.NoError(t, os.MkdirAll(filepath.Join(repository, "review"), 0o755)) + gitIn(t, repository, "init", "-q") + gitIn(t, repository, "config", "user.email", "t@example.com") + gitIn(t, repository, "config", "user.name", "t") + require.NoError(t, os.WriteFile(filepath.Join(repository, "review", "SKILL.md"), []byte("# Review"), 0o644)) + gitIn(t, repository, "add", ".") + gitIn(t, repository, "commit", "-qm", "skill") + commit = gitOut(t, repository, "rev-parse", "HEAD") + gitIn(t, repository, "commit", "-qm", "later", "--allow-empty") + + execPath, err := exec.Command("git", "--exec-path").Output() + require.NoError(t, err) + backend := &cgi.Handler{ + Path: filepath.Join(strings.TrimSpace(string(execPath)), "git-http-backend"), + Env: []string{"GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1"}, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != testAuthorization { + w.Header().Set("WWW-Authenticate", `Basic realm="private"`) + http.Error(w, "authentication required", http.StatusUnauthorized) + return + } + backend.ServeHTTP(w, r) + })) + t.Cleanup(server.Close) + return server.URL + "/skills.git", commit +} + +// fakeGateway forwards to target and, like Substrate's egress gateway, +// overwrites the Authorization header only on requests that already carry one. +func fakeGateway(t *testing.T, target string) string { + t.Helper() + upstream, err := url.Parse(target) + require.NoError(t, err) + proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: upstream.Scheme, Host: upstream.Host}) + gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "" { + r.Header.Set("Authorization", testAuthorization) + } + proxy.ServeHTTP(w, r) + })) + t.Cleanup(gateway.Close) + gatewayURL, err := url.Parse(gateway.URL) + require.NoError(t, err) + upstream.Host = gatewayURL.Host + return upstream.String() +} + +// requestLog records the Authorization header of every request a server sees. +type requestLog struct { + mu sync.Mutex + authorizations []string +} + +func (l *requestLog) record(r *http.Request) { + l.mu.Lock() + defer l.mu.Unlock() + l.authorizations = append(l.authorizations, r.Header.Get("Authorization")) +} + +func (l *requestLog) seen() []string { + l.mu.Lock() + defer l.mu.Unlock() + return append([]string(nil), l.authorizations...) +} + +// assertNoCredentialPersisted fails if any file under destination records the +// placeholder, the header configuration or the injected credential. +func assertNoCredentialPersisted(t *testing.T, destination string) { + t.Helper() + require.NoError(t, filepath.WalkDir(destination, func(path string, entry fs.DirEntry, err error) error { + if err != nil || !entry.Type().IsRegular() { + return err + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + for _, forbidden := range []string{GatewayAuthorizationPlaceholder, testAuthorization, "extraHeader", "extraheader"} { + assert.NotContains(t, string(content), forbidden, path) + } + return nil + })) +} + +func TestCloneGitCommitThroughCredentialGateway(t *testing.T) { + isolateGit(t) + repository, commit := privateGitServer(t) + throughGateway := fakeGateway(t, repository) + + t.Run("placeholder is replaced by the gateway", func(t *testing.T) { + destination := filepath.Join(t.TempDir(), "skill") + require.NoError(t, CloneGitCommit(t.Context(), throughGateway, commit, destination, true)) + content, err := os.ReadFile(filepath.Join(destination, "review", "SKILL.md")) + require.NoError(t, err) + assert.Equal(t, "# Review", string(content)) + assert.Equal(t, commit, gitOut(t, destination, "rev-parse", "HEAD")) + assertNoCredentialPersisted(t, destination) + }) + + t.Run("without a placeholder the gateway injects nothing and the fetch fails promptly", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), time.Minute) + defer cancel() + destination := filepath.Join(t.TempDir(), "skill") + started := time.Now() + err := CloneGitCommit(ctx, throughGateway, commit, destination, false) + require.Error(t, err) + require.NoError(t, ctx.Err(), "fetch must fail on its own, not wait for a prompt") + assert.Less(t, time.Since(started), 30*time.Second) + assert.NoDirExists(t, destination) + }) + + t.Run("the placeholder is not a credential", func(t *testing.T) { + destination := filepath.Join(t.TempDir(), "skill") + err := CloneGitCommit(t.Context(), repository, commit, destination, true) + require.Error(t, err) + assert.NoDirExists(t, destination) + }) +} + +func TestCloneGitCommitRefusesRedirectsWithAuthorization(t *testing.T) { + isolateGit(t) + _, commit := privateGitServer(t) + + var attacker requestLog + attackerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attacker.record(r) + http.Error(w, "not found", http.StatusNotFound) + })) + t.Cleanup(attackerServer.Close) + + // The authorized upstream redirects to a destination of its choosing. + var upstream requestLog + redirecting := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstream.record(r) + http.Redirect(w, r, attackerServer.URL+r.URL.RequestURI(), http.StatusFound) + })) + t.Cleanup(redirecting.Close) + + destination := filepath.Join(t.TempDir(), "skill") + err := CloneGitCommit(t.Context(), fakeGateway(t, redirecting.URL+"/skills.git"), commit, destination, true) + require.Error(t, err) + assert.NoDirExists(t, destination) + + // The configured destination did receive the gateway's credential, so the + // redirect was the only path to the attacker. + require.NotEmpty(t, upstream.seen()) + assert.Equal(t, testAuthorization, upstream.seen()[0]) + assert.Empty(t, attacker.seen(), "redirect target must receive no request, let alone an Authorization header") +} + +func TestGatewayAuthorizationIsScopedToSourceURL(t *testing.T) { + isolateGit(t) + source := "https://forge.example/org/skills.git" + config := gatewayAuthorizationConfig(source) + + headerFor := func(target string) string { + out, err := gitCommand(t.Context(), t.TempDir(), config, "config", "--get-urlmatch", "http.extraHeader", target).Output() + if err != nil { + var exit *exec.ExitError + require.ErrorAs(t, err, &exit) + require.Equal(t, 1, exit.ExitCode(), "git config failed: %s", exit.Stderr) + return "" + } + return strings.TrimSpace(string(out)) + } + + assert.Equal(t, "Authorization: "+GatewayAuthorizationPlaceholder, headerFor(source)) + assert.Equal(t, "Authorization: "+GatewayAuthorizationPlaceholder, headerFor(source+"/info/refs")) + for _, unrelated := range []string{ + "https://attacker.example/org/skills.git", + "https://forge.example/org/other.git", + "https://forge.example:8443/org/skills.git", + "http://forge.example/org/skills.git", + } { + assert.Empty(t, headerFor(unrelated), unrelated) + } +} + +func TestCloneGitCommitHonorsCancellation(t *testing.T) { + isolateGit(t) + release := make(chan struct{}) + hanging := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-release: + case <-r.Context().Done(): + } + })) + // Cleanups run last-in first-out: release the handler before Close waits + // for it. + t.Cleanup(hanging.Close) + t.Cleanup(func() { close(release) }) + + ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) + defer cancel() + destination := filepath.Join(t.TempDir(), "skill") + started := time.Now() + err := CloneGitCommit(ctx, hanging.URL+"/skills.git", strings.Repeat("a", 40), destination, true) + require.ErrorIs(t, err, context.DeadlineExceeded) + assert.Less(t, time.Since(started), 30*time.Second) + assert.NoDirExists(t, destination) +} diff --git a/go/core/internal/skillsinit/git_test.go b/go/core/internal/skillsinit/git_test.go index 7a175e969a..c948465bce 100644 --- a/go/core/internal/skillsinit/git_test.go +++ b/go/core/internal/skillsinit/git_test.go @@ -1,12 +1,31 @@ package skillsinit import ( + "os/exec" + "strings" "testing" "github.com/stretchr/testify/require" ) +func gitIn(t *testing.T, dir string, args ...string) { + t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + require.NoError(t, err, "git %v: %s", args, out) +} + +func gitOut(t *testing.T, dir string, args ...string) string { + t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + out, err := cmd.Output() + require.NoError(t, err, "git %v", args) + return strings.TrimSpace(string(out)) +} + func TestCloneGitCommitRejectsMutableRef(t *testing.T) { - err := CloneGitCommit("https://example.com/repository.git", "main", t.TempDir()) + err := CloneGitCommit(t.Context(), "https://example.com/repository.git", "main", t.TempDir(), false) require.ErrorContains(t, err, "full SHA") } diff --git a/go/core/internal/translator/compiler.go b/go/core/internal/translator/compiler.go index 8a430fdbf6..343244642c 100644 --- a/go/core/internal/translator/compiler.go +++ b/go/core/internal/translator/compiler.go @@ -269,6 +269,9 @@ func (c *Compiler) buildInputs(ctx context.Context, tree *resolvedTemplateTree) if err != nil { return nil, err } + if err := c.resolveGitSkillCredentials(template); err != nil { + return nil, err + } input := &AgentInput{Template: template, Instruction: instruction} if template.Spec.ModelConfig != nil { input.ResolvedModelConfig = krt.FetchOne(c.ctx, c.collections.ResolvedModelConfigs, krt.FilterObjectName(types.NamespacedName{Namespace: template.Namespace, Name: template.Spec.ModelConfig.Name})) diff --git a/go/core/internal/translator/credentials.go b/go/core/internal/translator/credentials.go index 44a67275f1..d84739535e 100644 --- a/go/core/internal/translator/credentials.go +++ b/go/core/internal/translator/credentials.go @@ -127,6 +127,22 @@ func CompileCredentials(input *HarnessInput, extraModels []*ResolvedModelConfig, } } } + if agent.Template != nil { + for _, skill := range agent.Template.Spec.Skills { + git := skill.Source.Git + if git == nil || git.AuthorizationFrom == nil { + continue + } + if _, err := gitCredentialOrigin(git.URL); err != nil { + return err + } + // The runtime sends a placeholder Authorization header that the + // gateway replaces with the Secret value. + if err := bind(git.URL, "authorization", "", agent.Template.Namespace, git.AuthorizationFrom.Name, git.AuthorizationFrom.Key); err != nil { + return err + } + } + } for _, child := range agent.Shared { if err := visit(child.Agent); err != nil { return err diff --git a/go/core/internal/translator/credentials_test.go b/go/core/internal/translator/credentials_test.go index f3c9f462af..164b73255a 100644 --- a/go/core/internal/translator/credentials_test.go +++ b/go/core/internal/translator/credentials_test.go @@ -1,6 +1,8 @@ package translator import ( + "encoding/json" + "strings" "testing" "github.com/stretchr/testify/require" @@ -8,6 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/kagent-dev/kagent/go/api/v1alpha3" + "github.com/kagent-dev/kagent/go/core/internal/egress" "github.com/kagent-dev/kagent/go/core/pkg/egresspolicy" ) @@ -84,6 +87,77 @@ func TestCompileCredentialsPreservesPassthrough(t *testing.T) { require.Nil(t, environment) } +func privateSkillTemplate(skills ...v1alpha3.AgentTemplateSkill) *TemplateConfiguration { + return &TemplateConfiguration{Name: "reviewer", Namespace: "team", Spec: v1alpha3.AgentTemplateSpec{Skills: skills}} +} + +func privateGitSkill(name, url, secret string) v1alpha3.AgentTemplateSkill { + return v1alpha3.AgentTemplateSkill{Name: name, Source: v1alpha3.ArtifactSource{Git: &v1alpha3.GitArtifact{ + URL: url, Commit: strings.Repeat("a", 40), + AuthorizationFrom: &v1alpha3.SecretKeyReference{Name: secret, Key: "authorization"}, + }}} +} + +func TestCompilePrivateGitSkillBindsGatewayCredentialAndEgress(t *testing.T) { + template := privateSkillTemplate( + privateGitSkill("review", "https://github.com/acme/private-skills", "skills-auth"), + v1alpha3.AgentTemplateSkill{Name: "public", Source: v1alpha3.ArtifactSource{Git: &v1alpha3.GitArtifact{ + URL: "https://gitlab.example.com/acme/public-skills", Commit: strings.Repeat("b", 40), + }}}, + ) + compilation, err := CompileSkillResources(template) + require.NoError(t, err) + require.Equal(t, []string{"https://github.com:443", "https://gitlab.example.com:443"}, compilation.Egress) + require.True(t, compilation.Resources.Skills[0].Source.Git.GatewayAuthorization) + require.False(t, compilation.Resources.Skills[1].Source.Git.GatewayAuthorization) + raw, err := json.Marshal(compilation.Resources) + require.NoError(t, err) + require.NotContains(t, string(raw), "skills-auth", "the runtime contract must not name the Secret") + + input := credentialInput(v1alpha3.ModelConfigSpec{}) + input.Root.Template = template + environment, bindings, _, err := CompileCredentials(input, nil, nil) + require.NoError(t, err) + require.Empty(t, environment, "a git credential needs no runtime environment") + require.Equal(t, []egress.Credential{{ + Origin: "https://github.com:443", Header: "authorization", URI: "ate-secret://k8s.io/default/team/skills-auth/authorization", + }}, bindings) +} + +func TestCompilePrivateGitSkillCredentialsFromSharedChild(t *testing.T) { + input := credentialInput(v1alpha3.ModelConfigSpec{}) + child := credentialInput(v1alpha3.ModelConfigSpec{}).Root + child.Template = privateSkillTemplate(privateGitSkill("review", "https://git.example.com/acme/skills", "child-auth")) + child.Template.Namespace = "child-team" + input.Root.Shared = []AgentInputBinding{{Agent: child}} + _, bindings, _, err := CompileCredentials(input, nil, nil) + require.NoError(t, err) + require.Len(t, bindings, 1) + require.Equal(t, "ate-secret://k8s.io/default/child-team/child-auth/authorization", bindings[0].URI) +} + +func TestCompilePrivateGitSkillRejectsConflictsAndPlugins(t *testing.T) { + input := credentialInput(v1alpha3.ModelConfigSpec{}) + input.Root.Template = privateSkillTemplate( + privateGitSkill("one", "https://github.com/acme/one", "first"), + privateGitSkill("two", "https://github.com/acme/two", "second"), + ) + _, bindings, _, err := CompileCredentials(input, nil, nil) + require.ErrorContains(t, err, "conflicting credentials") + require.Empty(t, bindings) + + input.Root.Template = privateSkillTemplate(privateGitSkill("one", "https://user@github.com/acme/one", "first")) + _, bindings, _, err = CompileCredentials(input, nil, nil) + require.ErrorContains(t, err, "without user information") + require.Empty(t, bindings) + + template := &TemplateConfiguration{Namespace: "team", Spec: v1alpha3.AgentTemplateSpec{Plugins: []v1alpha3.PluginBundle{{ + Source: privateGitSkill("unused", "https://github.com/acme/plugin", "auth").Source, + }}}} + _, err = CompileSkillResources(template) + require.ErrorContains(t, err, "supported only for skills") +} + func credentialInput(spec v1alpha3.ModelConfigSpec) *HarnessInput { resolved := &ResolvedModelConfig{Config: &v1alpha3.ModelConfig{ObjectMeta: metav1.ObjectMeta{Namespace: "team"}, Spec: spec}} if spec.Foundry != nil { diff --git a/go/core/internal/translator/skills.go b/go/core/internal/translator/skills.go index d31b7f3a40..2ffdee54f4 100644 --- a/go/core/internal/translator/skills.go +++ b/go/core/internal/translator/skills.go @@ -4,8 +4,12 @@ import ( "crypto/sha256" "fmt" "net/url" + "strings" "github.com/google/go-containerregistry/pkg/name" + "istio.io/istio/pkg/kube/krt" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "github.com/kagent-dev/kagent/go/api/agentplugin" "github.com/kagent-dev/kagent/go/api/v1alpha3" @@ -47,6 +51,10 @@ func CompileSkillResources(template *TemplateConfiguration) (*SkillCompilation, result.Resources.Skills = append(result.Resources.Skills, agentplugin.Skill{Name: skill.Name, Source: source}) } for i, plugin := range template.Spec.Plugins { + // Plugins contribute runtime configuration and cannot defer fetching. + if plugin.Source.Git != nil && plugin.Source.Git.AuthorizationFrom != nil { + return nil, NewValidationError("authorizationFrom is supported only for skills[].source.git") + } for _, name := range plugin.Skills { if _, exists := selected[name]; exists { return nil, NewValidationError("duplicate skill name %q", name) @@ -80,7 +88,7 @@ func (s *SkillCompilation) compileSource(source v1alpha3.ArtifactSource) (agentp s.Images = append(s.Images, image) } if source.Git != nil { - result.Git = &agentplugin.GitSource{URL: source.Git.URL, Commit: source.Git.Commit} + result.Git = &agentplugin.GitSource{URL: source.Git.URL, Commit: source.Git.Commit, GatewayAuthorization: source.Git.AuthorizationFrom != nil} } if source.Bucket != nil { result.S3 = &agentplugin.S3Source{ @@ -113,3 +121,52 @@ func appendURLOrigin(destinations []string, rawURL string) []string { } return destinations } + +// resolveGitSkillCredentials tracks Secret dependencies before emitting any +// runtime credential binding. Template authors cannot grant Secret authority. +func (c *Compiler) resolveGitSkillCredentials(template *TemplateConfiguration) error { + for _, skill := range template.Spec.Skills { + git := skill.Source.Git + if git == nil || git.AuthorizationFrom == nil { + continue + } + origin, err := gitCredentialOrigin(git.URL) + if err != nil { + return err + } + ref := git.AuthorizationFrom + key := types.NamespacedName{Namespace: template.Namespace, Name: ref.Name} + found := krt.FetchOne(c.ctx, c.collections.Secrets, krt.FilterObjectName(key)) + if found == nil { + return fmt.Errorf("resolve skill %q authorizationFrom: Secret %q not found", skill.Name, key) + } + secret := *found + if len(secret.Data[ref.Key]) == 0 { + return fmt.Errorf("resolve skill %q authorizationFrom: Secret %q does not contain non-empty key %q", skill.Name, key, ref.Key) + } + if err := authorizeGitCredentialOrigin(secret, origin); err != nil { + return fmt.Errorf("resolve skill %q authorizationFrom: %w", skill.Name, err) + } + } + return nil +} + +func gitCredentialOrigin(raw string) (string, error) { + u, err := url.Parse(raw) + if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.Fragment != "" || u.RawQuery != "" || u.ForceQuery { + return "", NewValidationError("Git authorizationFrom requires an absolute HTTPS URL without user information, query, or fragment") + } + origin, err := egress.ParseOrigin(egress.Origin(u)) + if err != nil || strings.Contains(u.Hostname(), "*") { + return "", NewValidationError("Git authorizationFrom requires an exact HTTPS DNS origin") + } + return origin.String(), nil +} + +func authorizeGitCredentialOrigin(secret *corev1.Secret, origin string) error { + allowed, err := egress.ParseOrigin(secret.Annotations[v1alpha3.GitCredentialOriginAnnotation]) + if err != nil || allowed.Scheme != "https" || strings.Contains(allowed.Host, "*") || allowed.String() != origin { + return fmt.Errorf("secret %q must authorize origin %q with annotation %q", secret.Name, origin, v1alpha3.GitCredentialOriginAnnotation) + } + return nil +} diff --git a/go/core/internal/translator/skills_test.go b/go/core/internal/translator/skills_test.go index 08f5dc72d8..ab2c753f84 100644 --- a/go/core/internal/translator/skills_test.go +++ b/go/core/internal/translator/skills_test.go @@ -118,3 +118,78 @@ func TestCompileSkillSourceNormalization(t *testing.T) { require.Error(t, err, "reference %q", ref) } } + +func TestCompilePrivateGitSkillAuthorization(t *testing.T) { + for _, test := range []struct { + name, origin, repository, key, namespace string + missingSecret, missingKey bool + wantError string + }{ + {name: "authorized", origin: "https://github.com"}, + {name: "canonical origin", origin: "https://GITHUB.com.:443"}, + {name: "missing Secret", origin: "https://github.com", missingSecret: true, wantError: "not found"}, + {name: "missing key", origin: "https://github.com", missingKey: true, wantError: "non-empty key"}, + {name: "empty key", origin: "https://github.com", key: "empty", wantError: "non-empty key"}, + {name: "other namespace", origin: "https://github.com", namespace: "other", wantError: "not found"}, + {name: "missing authorization", wantError: "must authorize origin"}, + {name: "attacker host", origin: "https://github.com", repository: "https://attacker.example/skills", wantError: "must authorize origin"}, + {name: "different port", origin: "https://github.com:8443", wantError: "must authorize origin"}, + {name: "wildcard grant", origin: "https://*.github.com", wantError: "must authorize origin"}, + {name: "path in grant", origin: "https://github.com/acme", wantError: "must authorize origin"}, + {name: "HTTP grant", origin: "http://github.com", wantError: "must authorize origin"}, + {name: "HTTP repository", origin: "https://github.com", repository: "http://github.com/acme/skills", wantError: "requires an absolute HTTPS URL"}, + {name: "userinfo repository", origin: "https://github.com", repository: "https://user@github.com/acme/skills", wantError: "without user information"}, + {name: "wildcard repository", origin: "https://github.com", repository: "https://*.github.com/acme/skills", wantError: "exact HTTPS DNS origin"}, + {name: "IP repository", origin: "https://github.com", repository: "https://127.0.0.1/acme/skills", wantError: "exact HTTPS DNS origin"}, + } { + t.Run(test.name, func(t *testing.T) { + harness := &v1alpha3.Harness{ObjectMeta: metav1.ObjectMeta{Name: "runtime", Namespace: "test"}, Spec: v1alpha3.HarnessSpec{ + Kagent: &v1alpha3.KagentHarness{}, + Substrate: v1alpha3.RuntimeSubstratePolicy{WorkerPoolRef: corev1.LocalObjectReference{Name: "default"}, SnapshotPolicy: v1alpha3.RuntimeSnapshotPolicy{Location: "snapshots"}}, + }} + repository := "https://github.com/acme/skills" + if test.repository != "" { + repository = test.repository + } + key := "authorization" + if test.key != "" { + key = test.key + } + template := &v1alpha3.AgentTemplate{ObjectMeta: metav1.ObjectMeta{Name: "skills", Namespace: "test"}, Spec: v1alpha3.AgentTemplateSpec{ + ModelConfig: &corev1.LocalObjectReference{Name: modelConfig().Name}, SystemPrompt: "help", + Skills: []v1alpha3.AgentTemplateSkill{{Name: "review", Source: v1alpha3.ArtifactSource{Git: &v1alpha3.GitArtifact{ + URL: repository, Commit: strings.Repeat("a", 40), AuthorizationFrom: &v1alpha3.SecretKeyReference{Name: "skills-auth", Key: key}, + }}}}, + }} + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "skills-auth", Namespace: "test", Annotations: map[string]string{v1alpha3.GitCredentialOriginAnnotation: test.origin}}, + Data: map[string][]byte{"authorization": []byte("Bearer fixture-only"), "empty": {}}, + } + if test.namespace != "" { + secret.Namespace = test.namespace + } + if test.missingKey { + delete(secret.Data, "authorization") + } + objects := []any{harness, modelConfig()} + if !test.missingSecret { + objects = append(objects, secret) + } + result, err := compiler(t, objects...).CompileAgent(t.Context(), inlineAgent(harness, template)) + if test.wantError != "" { + require.ErrorContains(t, err, test.wantError) + require.Nil(t, result) + return + } + require.NoError(t, err) + require.Equal(t, "https://github.com:443", result.Credentials[0].Origin) + require.Equal(t, "ate-secret://k8s.io/default/test/skills-auth/authorization", result.Credentials[0].URI) + require.NotContains(t, string(result.ConfigJSON), "fixture-only") + require.NotContains(t, string(result.Provenance), "fixture-only") + var config configv1.ADKConfig + require.NoError(t, runtimeconfig.Unmarshal(result.ConfigJSON, &config)) + resources := runtimeconfig.PluginResources(config.Agent.AgentPlugins) + require.True(t, resources.Skills[0].Source.Git.GatewayAuthorization) + }) + } +} diff --git a/go/core/pkg/agentplugins/materialize.go b/go/core/pkg/agentplugins/materialize.go index cad4b9c74d..292edb1d17 100644 --- a/go/core/pkg/agentplugins/materialize.go +++ b/go/core/pkg/agentplugins/materialize.go @@ -3,6 +3,7 @@ package agentplugins import ( "context" "encoding/json" + "errors" "fmt" "io/fs" "net" @@ -12,6 +13,8 @@ import ( "regexp" "slices" "strings" + "sync" + "time" "github.com/kagent-dev/kagent/go/api/agentplugin" "github.com/kagent-dev/kagent/go/core/internal/skillsinit" @@ -38,6 +41,7 @@ type Paths struct { type Materialization struct { SkillsDirectory string plugins []materializedPlugin + skills *standaloneSkills } // MCPConfig contains MCP servers resolved from materialized plugins. @@ -80,15 +84,110 @@ func (m Materialization) ClaudeFormatPluginRoots() []string { return roots } -// Materialize fetches Agent Plugin resources and copies explicitly selected -// skills into their runtime directory; Claude-format plugins are kept whole. -// It does not load runtime configuration. +// startupSkillFetchTimeout bounds the best-effort standalone skill fetch during +// startup, which may run before the Actor has egress. +const startupSkillFetchTimeout = 30 * time.Second + +// Materialize fetches Agent Plugin packages, copies their selected skills and +// mounted standalone skills into the runtime directory, and keeps +// Claude-format plugins whole. Any of those failures fails preparation. +// Downloaded standalone skills are then fetched once on a best-effort basis: a +// failure is logged and EnsureSkills retries before each task. It does not +// load runtime configuration. func Materialize(ctx context.Context, resources agentplugin.Resources, paths Paths) (Materialization, error) { - plugins, err := materializeResources(ctx, resources, paths) + plugins, deferred, err := materializeResources(ctx, resources, paths) if err != nil { return Materialization{}, err } - return Materialization{SkillsDirectory: paths.Skills, plugins: plugins}, nil + materialization := Materialization{ + SkillsDirectory: paths.Skills, plugins: plugins, + skills: &standaloneSkills{skills: deferred, paths: paths}, + } + if len(deferred) > 0 { + fetchCtx, cancel := context.WithTimeout(ctx, startupSkillFetchTimeout) + defer cancel() + if err := materialization.EnsureSkills(fetchCtx); err != nil { + logging.FromContext(ctx).WarnContext(ctx, "standalone skills are unavailable at startup; each task retries them before it runs", "error", err) + } + } + return materialization, nil +} + +// deferredSkill is a downloaded standalone skill and its package cache slot. +type deferredSkill struct { + skill agentplugin.Skill + index int +} + +// standaloneSkills installs downloaded standalone skills. Installed state is +// read from the skills directory, not process memory, so restored snapshots +// and restarted processes see exactly what is on disk. +type standaloneSkills struct { + mu sync.Mutex + skills []deferredSkill + paths Paths +} + +// EnsureSkills installs every downloaded standalone skill that is not yet in +// SkillsDirectory and reports the ones it could not install. Skills already +// present are not fetched again; a skill becomes visible only once it is +// complete. Run it before each task: a Substrate Actor may start without +// egress or gateway credentials. It is safe for concurrent use. +func (m Materialization) EnsureSkills(ctx context.Context) error { + if m.skills == nil { + return nil + } + s := m.skills + s.mu.Lock() + defer s.mu.Unlock() + var errs []error + for _, deferred := range s.skills { + destination := filepath.Join(s.paths.Skills, deferred.skill.Name) + if info, err := os.Stat(filepath.Join(destination, "SKILL.md")); err == nil && info.Mode().IsRegular() { + continue + } + root := filepath.Join(s.paths.Packages, fmt.Sprintf("standalone-%d", deferred.index)) + sourceRoot, err := fetchSource(ctx, deferred.skill.Source, root, "SKILL.md") + if err == nil { + err = installSkill(sourceRoot, destination) + } + if err != nil { + errs = append(errs, fmt.Errorf("materialize skill %q: %w", deferred.skill.Name, err)) + } + } + return errors.Join(errs...) +} + +// pendingSkillManifest holds SKILL.md while a deferred skill is installed. +// Skill loaders ignore a directory without SKILL.md, so the skill appears +// only once its final rename completes. +const pendingSkillManifest = ".SKILL.md.pending" + +// installSkill stages a skill in a hidden sibling of destination, on the same +// filesystem, renames the directory into place, then renames SKILL.md into +// place last so readers never see a partial skill. +func installSkill(source, destination string) error { + staging := filepath.Join(filepath.Dir(destination), "."+filepath.Base(destination)+".install") + if err := os.RemoveAll(staging); err != nil { + return err + } + err := copySkillTree(source, staging, pendingSkillManifest) + if err == nil { + err = os.RemoveAll(destination) + } + if err == nil { + err = os.Rename(staging, destination) + } + if err == nil { + err = os.Rename(filepath.Join(destination, pendingSkillManifest), filepath.Join(destination, "SKILL.md")) + if err != nil { + _ = os.RemoveAll(destination) + } + } + if err != nil { + _ = os.RemoveAll(staging) + } + return err } // LoadMCP resolves standard MCP configuration from a materialization. The @@ -107,7 +206,7 @@ func LoadMCP(ctx context.Context, materialization Materialization, dataRoot stri return result, nil } -func materializeResources(ctx context.Context, resources agentplugin.Resources, paths Paths) ([]materializedPlugin, error) { +func materializeResources(ctx context.Context, resources agentplugin.Resources, paths Paths) ([]materializedPlugin, []deferredSkill, error) { selectedSkills := make([]string, 0, len(resources.Skills)) for _, skill := range resources.Skills { selectedSkills = append(selectedSkills, skill.Name) @@ -116,24 +215,33 @@ func materializeResources(ctx context.Context, resources agentplugin.Resources, selectedSkills = append(selectedSkills, plugin.Skills...) } if err := validateSkillSelections(selectedSkills); err != nil { - return nil, err + return nil, nil, err } if err := os.MkdirAll(paths.Skills, 0o755); err != nil { - return nil, fmt.Errorf("create skills directory: %w", err) + return nil, nil, fmt.Errorf("create skills directory: %w", err) } if err := os.MkdirAll(paths.Packages, 0o755); err != nil { - return nil, fmt.Errorf("create package directory: %w", err) + return nil, nil, fmt.Errorf("create package directory: %w", err) } + var deferred []deferredSkill for i, skill := range resources.Skills { - root := filepath.Join(paths.Packages, fmt.Sprintf("standalone-%d", i)) - sourceRoot, err := fetchSource(ctx, skill.Source, root, "SKILL.md") + if err := validateSource(skill.Source); err != nil { + return nil, nil, fmt.Errorf("materialize skill %q: %w", skill.Name, err) + } + if skill.Source.Directory == "" { + deferred = append(deferred, deferredSkill{skill: skill, index: i}) + continue + } + // Mounted artifacts need no egress, so they install now and a broken + // mount fails preparation. + sourceRoot, err := fetchSource(ctx, skill.Source, "", "SKILL.md") if err != nil { - return nil, fmt.Errorf("materialize skill %q: %w", skill.Name, err) + return nil, nil, fmt.Errorf("materialize skill %q: %w", skill.Name, err) } if err := copySkill(sourceRoot, filepath.Join(paths.Skills, skill.Name)); err != nil { - return nil, fmt.Errorf("materialize skill %q: %w", skill.Name, err) + return nil, nil, fmt.Errorf("materialize skill %q: %w", skill.Name, err) } } @@ -143,27 +251,27 @@ func materializeResources(ctx context.Context, resources agentplugin.Resources, root := filepath.Join(paths.Packages, fmt.Sprintf("plugin-%d", i)) pluginRoot, err := fetchSource(ctx, plugin.Source, root, "plugin.json", claudeManifest) if err != nil { - return nil, fmt.Errorf("materialize plugin %d: %w", i, err) + return nil, nil, fmt.Errorf("materialize plugin %d: %w", i, err) } manifest, claudeFormat, err := loadManifest(pluginRoot) if err != nil { - return nil, fmt.Errorf("load plugin %d: %w", i, err) + return nil, nil, fmt.Errorf("load plugin %d: %w", i, err) } if _, exists := pluginNames[manifest.Name]; exists { - return nil, fmt.Errorf("duplicate plugin name %q", manifest.Name) + return nil, nil, fmt.Errorf("duplicate plugin name %q", manifest.Name) } pluginNames[manifest.Name] = struct{}{} if !claudeFormat { for _, name := range plugin.Skills { source := filepath.Join(pluginRoot, "skills", name) if err := copySkill(source, filepath.Join(paths.Skills, name)); err != nil { - return nil, fmt.Errorf("plugin %q skill %q: %w", manifest.Name, name, err) + return nil, nil, fmt.Errorf("plugin %q skill %q: %w", manifest.Name, name, err) } } } plugins = append(plugins, materializedPlugin{name: manifest.Name, root: pluginRoot, claudeFormat: claudeFormat}) } - return plugins, nil + return plugins, deferred, nil } func validateSkillSelections(names []string) error { @@ -189,7 +297,8 @@ func validateSkillName(name string) error { const claudeManifest = ".claude-plugin/plugin.json" -func fetchSource(ctx context.Context, source agentplugin.Source, destination string, requiredFiles ...string) (string, error) { +// validateSource requires exactly one artifact source. +func validateSource(source agentplugin.Source) error { selected := 0 if source.Directory != "" { selected++ @@ -201,7 +310,14 @@ func fetchSource(ctx context.Context, source agentplugin.Source, destination str selected++ } if selected != 1 { - return "", fmt.Errorf("exactly one artifact source is required") + return fmt.Errorf("exactly one artifact source is required") + } + return nil +} + +func fetchSource(ctx context.Context, source agentplugin.Source, destination string, requiredFiles ...string) (string, error) { + if err := validateSource(source); err != nil { + return "", err } if source.Directory != "" { if !filepath.IsAbs(source.Directory) { @@ -234,7 +350,7 @@ func fetchSource(ctx context.Context, source agentplugin.Source, destination str } switch { case source.Git != nil: - if err := skillsinit.CloneGitCommit(source.Git.URL, source.Git.Commit, destination); err != nil { + if err := skillsinit.CloneGitCommit(ctx, source.Git.URL, source.Git.Commit, destination, source.Git.GatewayAuthorization); err != nil { return "", err } case source.S3 != nil: @@ -339,6 +455,11 @@ func canonicalPath(path string) string { } func copySkill(source, destination string) error { + return copySkillTree(source, destination, "SKILL.md") +} + +// copySkillTree copies a skill and writes its root SKILL.md as manifestName. +func copySkillTree(source, destination, manifestName string) error { info, err := os.Stat(filepath.Join(source, "SKILL.md")) if err != nil || !info.Mode().IsRegular() { return fmt.Errorf("SKILL.md is required") @@ -354,6 +475,9 @@ func copySkill(source, destination string) error { if err != nil { return err } + if relative == "SKILL.md" { + relative = manifestName + } target := filepath.Join(destination, relative) info, err := entry.Info() if err != nil { diff --git a/go/core/pkg/agentplugins/materialize_test.go b/go/core/pkg/agentplugins/materialize_test.go index 718d8d95e0..7814c72fcf 100644 --- a/go/core/pkg/agentplugins/materialize_test.go +++ b/go/core/pkg/agentplugins/materialize_test.go @@ -104,6 +104,91 @@ func TestFetchSourceDoesNotReuseIncompleteMaterialization(t *testing.T) { } } +func TestMaterializeInstallsReachableStandaloneSkillsAtStartup(t *testing.T) { + repository, commit := commitFiles(t, map[string]string{"SKILL.md": "# Review"}) + root := t.TempDir() + paths := Paths{Packages: filepath.Join(root, "packages"), Skills: filepath.Join(root, "skills")} + materialization, err := Materialize(context.Background(), agentplugin.Resources{Skills: []agentplugin.Skill{ + {Name: "review", Source: agentplugin.Source{Git: &agentplugin.GitSource{URL: repository, Commit: commit}}}, + }}, paths) + if err != nil { + t.Fatal(err) + } + if content, err := os.ReadFile(filepath.Join(paths.Skills, "review", "SKILL.md")); err != nil || string(content) != "# Review" { + t.Fatalf("startup skill = %q, %v", content, err) + } + // Tasks reuse the installed skill without network access. + if err := os.RemoveAll(repository); err != nil { + t.Fatal(err) + } + if err := materialization.EnsureSkills(context.Background()); err != nil { + t.Fatalf("EnsureSkills() refetched an installed skill: %v", err) + } +} + +func TestMaterializeRetriesUnreachableStandaloneSkillsBeforeTasks(t *testing.T) { + repository, commit := commitFiles(t, map[string]string{"SKILL.md": "# Review"}) + root := t.TempDir() + paths := Paths{Packages: filepath.Join(root, "packages"), Skills: filepath.Join(root, "skills")} + unreachable := filepath.Join(root, "unreachable") + resources := agentplugin.Resources{Skills: []agentplugin.Skill{ + {Name: "later", Source: agentplugin.Source{Git: &agentplugin.GitSource{URL: unreachable, Commit: commit}}}, + {Name: "review", Source: agentplugin.Source{Git: &agentplugin.GitSource{URL: repository, Commit: commit}}}, + }} + materialization, err := Materialize(context.Background(), resources, paths) + if err != nil { + t.Fatalf("Materialize() failed preparation for an unreachable standalone skill: %v", err) + } + if _, err := os.Stat(filepath.Join(paths.Skills, "review", "SKILL.md")); err != nil { + t.Fatalf("reachable skill was not installed at startup: %v", err) + } + for range 2 { + err := materialization.EnsureSkills(context.Background()) + if err == nil || !strings.Contains(err.Error(), `skill "later"`) { + t.Fatalf("EnsureSkills() error = %v, want the unreachable skill", err) + } + if _, statErr := os.Stat(filepath.Join(paths.Skills, "later")); !os.IsNotExist(statErr) { + t.Fatalf("failed skill left files behind: %v", statErr) + } + } + // Egress becomes available: the next task installs the skill. + if err := os.Rename(repository, unreachable); err != nil { + t.Fatal(err) + } + if err := materialization.EnsureSkills(context.Background()); err != nil { + t.Fatal(err) + } + if content, err := os.ReadFile(filepath.Join(paths.Skills, "later", "SKILL.md")); err != nil || string(content) != "# Review" { + t.Fatalf("retried skill = %q, %v", content, err) + } + entries, err := os.ReadDir(paths.Skills) + if err != nil { + t.Fatal(err) + } + if len(entries) != 2 { + t.Fatalf("skills directory = %v, want only the installed skills", entries) + } +} + +func TestMaterializeKeepsMountedStandaloneSkillsEager(t *testing.T) { + // A broken mount fails preparation even when another skill is deferred. + root := t.TempDir() + paths := Paths{Packages: filepath.Join(root, "packages"), Skills: filepath.Join(root, "skills")} + _, err := Materialize(context.Background(), agentplugin.Resources{Skills: []agentplugin.Skill{ + {Name: "remote", Source: agentplugin.Source{Git: &agentplugin.GitSource{URL: filepath.Join(root, "missing"), Commit: strings.Repeat("a", 40)}}}, + {Name: "mounted", Source: agentplugin.Source{Directory: filepath.Join(root, "missing-mount")}}, + }}, paths) + if err == nil || !strings.Contains(err.Error(), "validate mounted artifact") { + t.Fatalf("Materialize() error = %v, want the broken mount", err) + } +} + +func TestEnsureSkillsOnZeroMaterialization(t *testing.T) { + if err := (Materialization{}).EnsureSkills(context.Background()); err != nil { + t.Fatal(err) + } +} + func TestMaterializeCopiesSelectionsWithoutLoadingPluginMCP(t *testing.T) { root := t.TempDir() pluginRoot := filepath.Join(root, "plugins", "plugin-0") diff --git a/go/harness/claude/internal/adapter/adapter.go b/go/harness/claude/internal/adapter/adapter.go index 73c18a9b7a..7bdd1e3209 100644 --- a/go/harness/claude/internal/adapter/adapter.go +++ b/go/harness/claude/internal/adapter/adapter.go @@ -59,6 +59,7 @@ func New(ctx context.Context, input Input) (*driver.ProcessDriver, error) { } } var pluginDirs []string + var ensureSkills func(context.Context) error if cfg.SkillResources != nil { skillsDir := config.SkillsRoot(input.DurableDir) if err := utils.EnsurePrivateDir(skillsDir); err != nil { @@ -73,10 +74,13 @@ func New(ctx context.Context, input Input) (*driver.ProcessDriver, error) { } // --setting-sources disables project skill discovery, including --add-dir. // Load compiler-selected skills explicitly without enabling workspace settings. + // Each Claude process loads this directory when it starts, so skills + // installed by EnsureSkills before a turn are discovered. if err := utils.ReplacePrivateFile(filepath.Join(skillRoot, ".claude-plugin", "plugin.json"), []byte(`{"name":"kagent-skills","skills":"./.claude/skills/"}`)); err != nil { return nil, fmt.Errorf("materialize Claude skill plugin: %w", err) } pluginDirs = append([]string{skillRoot}, materialized.ClaudeFormatPluginRoots()...) + ensureSkills = materialized.EnsureSkills } environment := setEnvironment(input.Environment, config.ClaudeConfigDirEnvName, claudeDir) // The native runtime inherits the compiled identity through the standard @@ -142,6 +146,7 @@ func New(ctx context.Context, input Input) (*driver.ProcessDriver, error) { PluginDirs: pluginDirs, Environment: environment, MaxEventBytes: cfg.MaxEventBytes, MaxStderrBytes: cfg.MaxStderrBytes, InterruptGrace: cfg.InterruptGrace(), AwaitTelemetry: awaitTelemetry, + EnsureSkills: ensureSkills, }), nil } diff --git a/go/harness/claude/internal/adapter/adapter_test.go b/go/harness/claude/internal/adapter/adapter_test.go index 834a44c72e..9f070b59fe 100644 --- a/go/harness/claude/internal/adapter/adapter_test.go +++ b/go/harness/claude/internal/adapter/adapter_test.go @@ -115,6 +115,7 @@ func TestNewMaterializesSkillsAndMCPConfig(t *testing.T) { t.Fatal(err) } for path, want := range map[string]string{ + // The cached package installs the standalone skill during startup. filepath.Join(skillRoot, ".claude", "skills", "review", "SKILL.md"): "# Review", filepath.Join(skillRoot, ".claude-plugin", "plugin.json"): `{"name":"kagent-skills","skills":"./.claude/skills/"}`, } { diff --git a/go/harness/claude/internal/driver/process.go b/go/harness/claude/internal/driver/process.go index e53f521e44..cf37957071 100644 --- a/go/harness/claude/internal/driver/process.go +++ b/go/harness/claude/internal/driver/process.go @@ -46,6 +46,9 @@ type ProcessConfig struct { // AwaitTelemetry holds each prompt until Claude Code telemetry has // initialized. AwaitTelemetry bool + // EnsureSkills installs downloaded standalone skills still missing after + // startup, which may run before the Actor has egress. + EnsureSkills func(context.Context) error } // ProcessDriver supervises one Claude Code process per ordinary runtime turn @@ -149,6 +152,11 @@ func (d *ProcessDriver) Args(turn runtime.Turn) []string { // Run supervises one Claude Code process and emits its ordered runtime events. func (d *ProcessDriver) Run(ctx context.Context, turn runtime.Turn, sink runtime.EventSink) (runtime.Outcome, error) { + if d.config.EnsureSkills != nil { + if err := d.config.EnsureSkills(ctx); err != nil { + return runtime.Outcome{Failure: &runtime.Failure{Message: "skills_unavailable: " + err.Error()}}, nil + } + } return d.run(ctx, turn, sink, nil) } diff --git a/go/harness/claude/internal/driver/skills_test.go b/go/harness/claude/internal/driver/skills_test.go new file mode 100644 index 0000000000..3456103612 --- /dev/null +++ b/go/harness/claude/internal/driver/skills_test.go @@ -0,0 +1,32 @@ +package driver + +import ( + "context" + "errors" + "path/filepath" + "testing" + + "github.com/kagent-dev/kagent/go/harness/runtime" +) + +func TestRunFailsTurnWhenSkillsAreUnavailable(t *testing.T) { + calls := 0 + d := NewProcessDriver(ProcessConfig{ + // Never started: the turn fails before the native process runs. + Executable: filepath.Join(t.TempDir(), "missing"), Workspace: t.TempDir(), + EnsureSkills: func(context.Context) error { + calls++ + return errors.New("materialize skill \"review\": exit status 128") + }, + }) + outcome, err := d.Run(context.Background(), runtime.Turn{Prompt: "hello"}, nil) + if err != nil { + t.Fatalf("Run() error = %v, want a task failure", err) + } + if outcome.Failure == nil || outcome.Failure.Message != "skills_unavailable: materialize skill \"review\": exit status 128" { + t.Fatalf("Run() outcome = %#v", outcome) + } + if calls != 1 { + t.Fatalf("EnsureSkills called %d times", calls) + } +} diff --git a/go/harness/codex/internal/adapter/adapter.go b/go/harness/codex/internal/adapter/adapter.go index 2c41884400..2b1c9421f2 100644 --- a/go/harness/codex/internal/adapter/adapter.go +++ b/go/harness/codex/internal/adapter/adapter.go @@ -53,6 +53,7 @@ func New(ctx context.Context, input Input) (*driver.ProcessDriver, error) { if err := reconcileGeneratedDir(skillsRoot, nil); err != nil { return nil, fmt.Errorf("reconcile Codex skills: %w", err) } + var ensureSkills func(context.Context) error if cfg.SkillResources != nil { materialized, err := agentplugins.Materialize(ctx, *cfg.SkillResources, agentplugins.Paths{ Packages: filepath.Join(codexHome, "packages"), @@ -64,6 +65,7 @@ func New(ctx context.Context, input Input) (*driver.ProcessDriver, error) { if len(materialized.ClaudeFormatPluginRoots()) > 0 { return nil, fmt.Errorf("plugin is Claude-format; only the Agent Plugins format (plugin.json at the plugin root) is supported here") } + ensureSkills = materialized.EnsureSkills } if err := materializeAgents(codexHome, cfg.Agents); err != nil { return nil, err @@ -104,6 +106,7 @@ func New(ctx context.Context, input Input) (*driver.ProcessDriver, error) { ApprovalServers: approvalServers, PendingDir: filepath.Join(input.DurableDir, "adapter"), Tools: driver.NewMCPCaller(cfg.MCPServers, environment), + EnsureSkills: ensureSkills, }), nil } diff --git a/go/harness/codex/internal/driver/process.go b/go/harness/codex/internal/driver/process.go index 4b5127b0f6..1e58564e7f 100644 --- a/go/harness/codex/internal/driver/process.go +++ b/go/harness/codex/internal/driver/process.go @@ -37,6 +37,9 @@ type ProcessConfig struct { ApprovalServers map[string]struct{} PendingDir string Tools ToolCaller + // EnsureSkills installs downloaded standalone skills still missing after + // startup, which may run before the Actor has egress. + EnsureSkills func(context.Context) error } // ProcessDriver supervises Codex App Server. Pending input is persisted and @@ -79,6 +82,11 @@ func (d *ProcessDriver) Validate(ctx context.Context) error { // Run initializes App Server, starts or resumes the Actor's native thread, and // emits the turn's ordered runtime events. func (d *ProcessDriver) Run(ctx context.Context, turn runtime.Turn, sink runtime.EventSink) (runtime.Outcome, error) { + if d.config.EnsureSkills != nil { + if err := d.config.EnsureSkills(ctx); err != nil { + return runtime.Outcome{Failure: &runtime.Failure{Message: "skills_unavailable: " + err.Error()}}, nil + } + } return d.run(ctx, turn, nil, sink) } diff --git a/go/harness/codex/internal/driver/skills_test.go b/go/harness/codex/internal/driver/skills_test.go new file mode 100644 index 0000000000..3456103612 --- /dev/null +++ b/go/harness/codex/internal/driver/skills_test.go @@ -0,0 +1,32 @@ +package driver + +import ( + "context" + "errors" + "path/filepath" + "testing" + + "github.com/kagent-dev/kagent/go/harness/runtime" +) + +func TestRunFailsTurnWhenSkillsAreUnavailable(t *testing.T) { + calls := 0 + d := NewProcessDriver(ProcessConfig{ + // Never started: the turn fails before the native process runs. + Executable: filepath.Join(t.TempDir(), "missing"), Workspace: t.TempDir(), + EnsureSkills: func(context.Context) error { + calls++ + return errors.New("materialize skill \"review\": exit status 128") + }, + }) + outcome, err := d.Run(context.Background(), runtime.Turn{Prompt: "hello"}, nil) + if err != nil { + t.Fatalf("Run() error = %v, want a task failure", err) + } + if outcome.Failure == nil || outcome.Failure.Message != "skills_unavailable: materialize skill \"review\": exit status 128" { + t.Fatalf("Run() outcome = %#v", outcome) + } + if calls != 1 { + t.Fatalf("EnsureSkills called %d times", calls) + } +} diff --git a/helm/kagent-crds/templates/api.kagent.dev_agents.yaml b/helm/kagent-crds/templates/api.kagent.dev_agents.yaml index 6ba119ed9f..323d9d04df 100644 --- a/helm/kagent-crds/templates/api.kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/api.kagent.dev_agents.yaml @@ -206,6 +206,29 @@ spec: description: GitArtifact identifies immutable content at a full Git commit ID. properties: + authorizationFrom: + description: |- + AuthorizationFrom references a same-namespace Secret key containing the + complete HTTP Authorization header, such as `Basic `. + The Secret must authorize the repository's exact HTTPS origin through + its kagent.dev/git-credential-origin annotation (scheme, DNS name, port). + The gateway replaces matching Authorization headers; the credential + never enters the runtime. Supported for standalone skills only. + properties: + key: + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object commit: pattern: ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ type: string @@ -217,6 +240,9 @@ spec: - commit - url type: object + x-kubernetes-validations: + - message: authorizationFrom requires an https URL + rule: '!has(self.authorizationFrom) || self.url.startsWith(''https://'')' oci: description: OCI is a digest-pinned image reference. pattern: ^[^[:space:]@]+@sha256:[0-9a-f]{64}$ @@ -239,6 +265,9 @@ spec: required: - source type: object + x-kubernetes-validations: + - message: authorizationFrom is supported only for skills[].source.git + rule: '!has(self.source.git) || !has(self.source.git.authorizationFrom)' maxItems: 20 type: array promptTemplate: @@ -320,6 +349,29 @@ spec: description: GitArtifact identifies immutable content at a full Git commit ID. properties: + authorizationFrom: + description: |- + AuthorizationFrom references a same-namespace Secret key containing the + complete HTTP Authorization header, such as `Basic `. + The Secret must authorize the repository's exact HTTPS origin through + its kagent.dev/git-credential-origin annotation (scheme, DNS name, port). + The gateway replaces matching Authorization headers; the credential + never enters the runtime. Supported for standalone skills only. + properties: + key: + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object commit: pattern: ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ type: string @@ -331,6 +383,9 @@ spec: - commit - url type: object + x-kubernetes-validations: + - message: authorizationFrom requires an https URL + rule: '!has(self.authorizationFrom) || self.url.startsWith(''https://'')' oci: description: OCI is a digest-pinned image reference. pattern: ^[^[:space:]@]+@sha256:[0-9a-f]{64}$ diff --git a/helm/kagent-crds/templates/api.kagent.dev_agenttemplates.yaml b/helm/kagent-crds/templates/api.kagent.dev_agenttemplates.yaml index d35d8a590a..76a3af37c3 100644 --- a/helm/kagent-crds/templates/api.kagent.dev_agenttemplates.yaml +++ b/helm/kagent-crds/templates/api.kagent.dev_agenttemplates.yaml @@ -141,6 +141,29 @@ spec: description: GitArtifact identifies immutable content at a full Git commit ID. properties: + authorizationFrom: + description: |- + AuthorizationFrom references a same-namespace Secret key containing the + complete HTTP Authorization header, such as `Basic `. + The Secret must authorize the repository's exact HTTPS origin through + its kagent.dev/git-credential-origin annotation (scheme, DNS name, port). + The gateway replaces matching Authorization headers; the credential + never enters the runtime. Supported for standalone skills only. + properties: + key: + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object commit: pattern: ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ type: string @@ -152,6 +175,9 @@ spec: - commit - url type: object + x-kubernetes-validations: + - message: authorizationFrom requires an https URL + rule: '!has(self.authorizationFrom) || self.url.startsWith(''https://'')' oci: description: OCI is a digest-pinned image reference. pattern: ^[^[:space:]@]+@sha256:[0-9a-f]{64}$ @@ -173,6 +199,9 @@ spec: required: - source type: object + x-kubernetes-validations: + - message: authorizationFrom is supported only for skills[].source.git + rule: '!has(self.source.git) || !has(self.source.git.authorizationFrom)' maxItems: 20 type: array promptTemplate: @@ -253,6 +282,29 @@ spec: description: GitArtifact identifies immutable content at a full Git commit ID. properties: + authorizationFrom: + description: |- + AuthorizationFrom references a same-namespace Secret key containing the + complete HTTP Authorization header, such as `Basic `. + The Secret must authorize the repository's exact HTTPS origin through + its kagent.dev/git-credential-origin annotation (scheme, DNS name, port). + The gateway replaces matching Authorization headers; the credential + never enters the runtime. Supported for standalone skills only. + properties: + key: + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object commit: pattern: ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ type: string @@ -264,6 +316,9 @@ spec: - commit - url type: object + x-kubernetes-validations: + - message: authorizationFrom requires an https URL + rule: '!has(self.authorizationFrom) || self.url.startsWith(''https://'')' oci: description: OCI is a digest-pinned image reference. pattern: ^[^[:space:]@]+@sha256:[0-9a-f]{64}$ diff --git a/proto/kagent/config/v1/adk.proto b/proto/kagent/config/v1/adk.proto index 8727a4cbb8..8c77375882 100644 --- a/proto/kagent/config/v1/adk.proto +++ b/proto/kagent/config/v1/adk.proto @@ -375,6 +375,9 @@ message ADKPluginSource { message ADKPluginGitSource { string url = 1; string commit = 2; + // The egress gateway owns this host's Authorization header. The runtime sends + // a placeholder for the gateway to replace and never holds the credential. + bool gateway_authorization = 3; } message ADKPluginS3Source { string endpoint = 1; diff --git a/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.py b/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.py index 70f00e4a2f..1bd1f5ea47 100644 --- a/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.py +++ b/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.py @@ -27,7 +27,7 @@ from kagent.config.v1 import config_pb2 as kagent_dot_config_dot_v1_dot_config__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1akagent/config/v1/adk.proto\x12\x10kagent.config.v1\x1a\x1b\x62uf/validate/validate.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1dkagent/config/v1/config.proto\"\x81\x01\n\tADKConfig\x12<\n\x07runtime\x18\x01 \x01(\x0b\x32\".kagent.config.v1.ADKRuntimeConfigR\x07runtime\x12\x36\n\x05\x61gent\x18\x02 \x01(\x0b\x32 .kagent.config.v1.ADKAgentConfigR\x05\x61gent\"\xff\x07\n\x0e\x41\x44KAgentConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x36\n\x05model\x18\x02 \x01(\x0b\x32 .kagent.config.v1.ADKModelConfigR\x05model\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12 \n\x0binstruction\x18\x04 \x01(\tR\x0binstruction\x12G\n\nhttp_tools\x18\x05 \x03(\x0b\x32(.kagent.config.v1.ADKHTTPMCPServerConfigR\thttpTools\x12\x44\n\tsse_tools\x18\x06 \x03(\x0b\x32\'.kagent.config.v1.ADKSSEMCPServerConfigR\x08sseTools\x12J\n\x0bstdio_tools\x18\x07 \x03(\x0b\x32).kagent.config.v1.ADKStdioMCPServerConfigR\nstdioTools\x12K\n\rremote_agents\x18\x08 \x03(\x0b\x32&.kagent.config.v1.ADKRemoteAgentConfigR\x0cremoteAgents\x12\x1b\n\x06stream\x18\t \x01(\x08H\x00R\x06stream\x88\x01\x01\x12\x39\n\x06memory\x18\n \x01(\x0b\x32!.kagent.config.v1.ADKMemoryConfigR\x06memory\x12<\n\x07network\x18\x0b \x01(\x0b\x32\".kagent.config.v1.ADKNetworkConfigR\x07network\x12\x46\n\ragent_plugins\x18\x0c \x01(\x0b\x32!.kagent.config.v1.ADKAgentPluginsR\x0c\x61gentPlugins\x12I\n\x0e\x63ontext_config\x18\r \x01(\x0b\x32\".kagent.config.v1.ADKContextConfigR\rcontextConfig\x12$\n\x0bshare_tools\x18\x0e \x01(\x08H\x01R\nshareTools\x88\x01\x01\x12$\n\x0esession_db_url\x18\x0f \x01(\tR\x0csessionDbUrl\x12)\n\x10skills_directory\x18\x10 \x01(\tR\x0fskillsDirectory\x12?\n\nsub_agents\x18\x11 \x03(\x0b\x32 .kagent.config.v1.ADKAgentConfigR\tsubAgents\x12\x39\n\x06output\x18\x12 \x01(\x0b\x32!.kagent.config.v1.ADKOutputConfigR\x06outputB\t\n\x07_streamB\x0e\n\x0c_share_tools\"\xa1\t\n\x0e\x41\x44KModelConfig\x12\x14\n\x05model\x18\x01 \x01(\tR\x05model\x12G\n\x07headers\x18\x02 \x03(\x0b\x32-.kagent.config.v1.ADKModelConfig.HeadersEntryR\x07headers\x12<\n\x18tls_insecure_skip_verify\x18\x03 \x01(\x08H\x01R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x04 \x01(\tH\x02R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x05 \x01(\x08H\x03R\x13tlsDisableSystemCas\x88\x01\x01\x12.\n\x13\x61pi_key_passthrough\x18\x06 \x01(\x08R\x11\x61piKeyPassthrough\x12\x35\n\x06openai\x18\n \x01(\x0b\x32\x1b.kagent.config.v1.ADKOpenAIH\x00R\x06openai\x12\x45\n\x0c\x61zure_openai\x18\x0b \x01(\x0b\x32 .kagent.config.v1.ADKAzureOpenAIH\x00R\x0b\x61zureOpenai\x12>\n\tanthropic\x18\x0c \x01(\x0b\x32\x1e.kagent.config.v1.ADKAnthropicH\x00R\tanthropic\x12O\n\x10gemini_vertex_ai\x18\r \x01(\x0b\x32#.kagent.config.v1.ADKGeminiVertexAIH\x00R\x0egeminiVertexAi\x12Q\n\x10gemini_anthropic\x18\x0e \x01(\x0b\x32$.kagent.config.v1.ADKGeminiAnthropicH\x00R\x0fgeminiAnthropic\x12\x35\n\x06ollama\x18\x0f \x01(\x0b\x32\x1b.kagent.config.v1.ADKOllamaH\x00R\x06ollama\x12\x35\n\x06gemini\x18\x10 \x01(\x0b\x32\x1b.kagent.config.v1.ADKGeminiH\x00R\x06gemini\x12\x38\n\x07\x62\x65\x64rock\x18\x11 \x01(\x0b\x32\x1c.kagent.config.v1.ADKBedrockH\x00R\x07\x62\x65\x64rock\x12@\n\x0bsap_ai_core\x18\x12 \x01(\x0b\x32\x1e.kagent.config.v1.ADKSAPAICoreH\x00R\tsapAiCore\x12\x38\n\x07\x66oundry\x18\x13 \x01(\x0b\x32\x1c.kagent.config.v1.ADKFoundryH\x00R\x07\x66oundry\x12\x38\n\x07mistral\x18\x14 \x01(\x0b\x32\x1c.kagent.config.v1.ADKMistralH\x00R\x07mistral\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x11\n\x08provider\x12\x05\xbaH\x02\x08\x01\x42\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"\x86\x08\n\tADKOpenAI\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12I\n\x11\x66requency_penalty\x18\x02 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\xc0H\x00R\x10\x66requencyPenalty\x88\x01\x01\x12+\n\nmax_tokens\x18\x03 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x01R\tmaxTokens\x88\x01\x01\x12@\n\x15max_completion_tokens\x18\x04 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x02R\x13maxCompletionTokens\x88\x01\x01\x12\x1a\n\x01n\x18\x05 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x03R\x01n\x88\x01\x01\x12G\n\x10presence_penalty\x18\x06 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\xc0H\x04R\x0fpresencePenalty\x88\x01\x01\x12.\n\x10reasoning_effort\x18\x07 \x01(\tH\x05R\x0freasoningEffort\x88\x01\x01\x12\x17\n\x04seed\x18\x08 \x01(\x03H\x06R\x04seed\x88\x01\x01\x12>\n\x0btemperature\x18\t \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x07R\x0btemperature\x88\x01\x01\x12\x45\n\x07timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12\x31\n\x05top_p\x18\x0b \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x08R\x04topP\x88\x01\x01\x12\x42\n\napi_format\x18\x0c \x01(\tB#\xbaH r\x1eR\x00R\x0f\x63hatCompletionsR\tresponsesR\tapiFormat\x12O\n\x0etoken_exchange\x18\r \x01(\x0b\x32(.kagent.config.v1.ADKTokenExchangeConfigR\rtokenExchange:\x94\x01\xbaH\x90\x01\x1a\x8d\x01\n\x13openai.token_limits\x12;max_tokens and max_completion_tokens are mutually exclusive\x1a\x39!has(this.max_tokens) || !has(this.max_completion_tokens)B\x14\n\x12_frequency_penaltyB\r\n\x0b_max_tokensB\x18\n\x16_max_completion_tokensB\x04\n\x02_nB\x13\n\x11_presence_penaltyB\x13\n\x11_reasoning_effortB\x07\n\x05_seedB\x0e\n\x0c_temperatureB\x08\n\x06_top_p\"\x8b\x01\n\x16\x41\x44KTokenExchangeConfig\x12`\n\x14gdch_service_account\x18\x01 \x01(\x0b\x32,.kagent.config.v1.ADKGDCHTokenExchangeConfigH\x00R\x12gdchServiceAccountB\x0f\n\x06source\x12\x05\xbaH\x02\x08\x01\"|\n\x1a\x41\x44KGDCHTokenExchangeConfig\x12\x39\n\x14service_account_path\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x12serviceAccountPath\x12#\n\x08\x61udience\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x08\x61udience\"\xfa\x02\n\x0e\x41\x44KAzureOpenAI\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12\x1e\n\ndeployment\x18\x02 \x01(\tR\ndeployment\x12\x1f\n\x0b\x61pi_version\x18\x03 \x01(\tR\napiVersion\x12+\n\nmax_tokens\x18\x04 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x00R\tmaxTokens\x88\x01\x01\x12>\n\x0btemperature\x18\x05 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x01R\x0btemperature\x88\x01\x01\x12\x31\n\x05top_p\x18\x06 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x02R\x04topP\x88\x01\x01\x12\x42\n\napi_format\x18\x07 \x01(\tB#\xbaH r\x1eR\x00R\x0f\x63hatCompletionsR\tresponsesR\tapiFormatB\r\n\x0b_max_tokensB\x0e\n\x0c_temperatureB\x08\n\x06_top_p\"\xbb\x03\n\x0c\x41\x44KAnthropic\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12+\n\nmax_tokens\x18\x02 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x00R\tmaxTokens\x88\x01\x01\x12>\n\x0btemperature\x18\x03 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x01R\x0btemperature\x88\x01\x01\x12\x31\n\x05top_p\x18\x04 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x02R\x04topP\x88\x01\x01\x12!\n\x05top_k\x18\x05 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x00H\x03R\x04topK\x88\x01\x01\x12\x45\n\x07timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12%\n\x0eprompt_caching\x18\x07 \x01(\x08R\rpromptCaching\x12,\n\tcache_ttl\x18\x08 \x01(\tB\x0f\xbaH\x0cr\nR\x00R\x02\x35mR\x02\x31hR\x08\x63\x61\x63heTtlB\r\n\x0b_max_tokensB\x0e\n\x0c_temperatureB\x08\n\x06_top_pB\x08\n\x06_top_k\"\xb7\x02\n\nADKMistral\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12+\n\nmax_tokens\x18\x02 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x00R\tmaxTokens\x88\x01\x01\x12>\n\x0btemperature\x18\x03 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x01R\x0btemperature\x88\x01\x01\x12\x31\n\x05top_p\x18\x04 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x02R\x04topP\x88\x01\x01\x12\x45\n\x07timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeoutB\r\n\x0b_max_tokensB\x0e\n\x0c_temperatureB\x08\n\x06_top_p\"i\n\x11\x41\x44KGeminiVertexAI\x12>\n\x11max_output_tokens\x18\x01 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x00R\x0fmaxOutputTokens\x88\x01\x01\x42\x14\n\x12_max_output_tokens\"\x14\n\x12\x41\x44KGeminiAnthropic\"\x8b\x01\n\tADKOllama\x12\x42\n\x07options\x18\x01 \x03(\x0b\x32(.kagent.config.v1.ADKOllama.OptionsEntryR\x07options\x1a:\n\x0cOptionsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"a\n\tADKGemini\x12>\n\x11max_output_tokens\x18\x01 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x00R\x0fmaxOutputTokens\x88\x01\x01\x42\x14\n\x12_max_output_tokens\"\xab\x03\n\nADKBedrock\x12\x16\n\x06region\x18\x01 \x01(\tR\x06region\x12\x45\n\x1f\x61\x64\x64itional_model_request_fields\x18\x02 \x01(\x0cR\x1c\x61\x64\x64itionalModelRequestFields\x12%\n\x0eprompt_caching\x18\x03 \x01(\x08R\rpromptCaching\x12,\n\tcache_ttl\x18\x04 \x01(\tB\x0f\xbaH\x0cr\nR\x00R\x02\x35mR\x02\x31hR\x08\x63\x61\x63heTtl\x12\x43\n\tguardrail\x18\x05 \x01(\x0b\x32%.kagent.config.v1.ADKBedrockGuardrailR\tguardrail\x12N\n\x0cread_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0breadTimeout\x12T\n\x0f\x63onnect_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0e\x63onnectTimeout\"e\n\x13\x41\x44KBedrockGuardrail\x12\x1e\n\nidentifier\x18\x01 \x01(\tR\nidentifier\x12\x18\n\x07version\x18\x02 \x01(\tR\x07version\x12\x14\n\x05trace\x18\x03 \x01(\tR\x05trace\"k\n\x0c\x41\x44KSAPAICore\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12%\n\x0eresource_group\x18\x02 \x01(\tR\rresourceGroup\x12\x19\n\x08\x61uth_url\x18\x03 \x01(\tR\x07\x61uthUrl\"\xa4\x01\n\nADKFoundry\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12\x1e\n\ndeployment\x18\x02 \x01(\tR\ndeployment\x12\x1f\n\x0b\x61pi_version\x18\x03 \x01(\tR\napiVersion\x12\x39\n\napi_format\x18\x04 \x01(\tB\x1a\xbaH\x17r\x15R\x00R\x06openaiR\tanthropicR\tapiFormat\"\x94\x05\n\x17\x41\x44KHTTPConnectionParams\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12P\n\x07headers\x18\x02 \x03(\x0b\x32\x36.kagent.config.v1.ADKHTTPConnectionParams.HeadersEntryR\x07headers\x12\x45\n\x07timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12U\n\x10sse_read_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0esseReadTimeout\x12\x31\n\x12terminate_on_close\x18\x05 \x01(\x08H\x00R\x10terminateOnClose\x88\x01\x01\x12<\n\x18tls_insecure_skip_verify\x18\x06 \x01(\x08H\x01R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x07 \x01(\tH\x02R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x08 \x01(\x08H\x03R\x13tlsDisableSystemCas\x88\x01\x01\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x15\n\x13_terminate_on_closeB\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"\xc8\x04\n\x16\x41\x44KSSEConnectionParams\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12O\n\x07headers\x18\x02 \x03(\x0b\x32\x35.kagent.config.v1.ADKSSEConnectionParams.HeadersEntryR\x07headers\x12\x45\n\x07timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12U\n\x10sse_read_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0esseReadTimeout\x12<\n\x18tls_insecure_skip_verify\x18\x05 \x01(\x08H\x00R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x06 \x01(\tH\x01R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x07 \x01(\x08H\x02R\x13tlsDisableSystemCas\x88\x01\x01\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"\xfa\x01\n\x16\x41\x44KHTTPMCPServerConfig\x12I\n\x06params\x18\x01 \x01(\x0b\x32).kagent.config.v1.ADKHTTPConnectionParamsB\x06\xbaH\x03\xc8\x01\x01R\x06params\x12\x14\n\x05tools\x18\x02 \x03(\tR\x05tools\x12\'\n\x0f\x61llowed_headers\x18\x03 \x03(\tR\x0e\x61llowedHeaders\x12)\n\x10require_approval\x18\x04 \x01(\x08R\x0frequireApproval\x12+\n\x11token_passthrough\x18\x05 \x01(\x08R\x10tokenPassthrough\"\xf8\x01\n\x15\x41\x44KSSEMCPServerConfig\x12H\n\x06params\x18\x01 \x01(\x0b\x32(.kagent.config.v1.ADKSSEConnectionParamsB\x06\xbaH\x03\xc8\x01\x01R\x06params\x12\x14\n\x05tools\x18\x02 \x03(\tR\x05tools\x12\'\n\x0f\x61llowed_headers\x18\x03 \x03(\tR\x0e\x61llowedHeaders\x12)\n\x10require_approval\x18\x04 \x01(\x08R\x0frequireApproval\x12+\n\x11token_passthrough\x18\x05 \x01(\x08R\x10tokenPassthrough\"\xd7\x01\n\x17\x41\x44KStdioMCPServerConfig\x12\x18\n\x07\x63ommand\x18\x01 \x01(\tR\x07\x63ommand\x12\x12\n\x04\x61rgs\x18\x02 \x03(\tR\x04\x61rgs\x12\x44\n\x03\x65nv\x18\x03 \x03(\x0b\x32\x32.kagent.config.v1.ADKStdioMCPServerConfig.EnvEntryR\x03\x65nv\x12\x10\n\x03\x64ir\x18\x04 \x01(\tR\x03\x64ir\x1a\x36\n\x08\x45nvEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdb\x02\n\x14\x41\x44KRemoteAgentConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n\x03url\x18\x02 \x01(\tR\x03url\x12M\n\x07headers\x18\x03 \x03(\x0b\x32\x33.kagent.config.v1.ADKRemoteAgentConfig.HeadersEntryR\x07headers\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12)\n\x10isolate_sessions\x18\x05 \x01(\x08R\x0fisolateSessions\x12\x45\n\x07timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xe1\x03\n\x12\x41\x44KEmbeddingConfig\x12\x1a\n\x08provider\x18\x01 \x01(\tR\x08provider\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12\x19\n\x08\x62\x61se_url\x18\x03 \x01(\tR\x07\x62\x61seUrl\x12.\n\x13\x61pi_key_passthrough\x18\x04 \x01(\x08R\x11\x61piKeyPassthrough\x12<\n\x18tls_insecure_skip_verify\x18\x05 \x01(\x08H\x00R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x06 \x01(\tH\x01R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x07 \x01(\x08H\x02R\x13tlsDisableSystemCas\x88\x01\x01\x12\x1a\n\x08\x65ndpoint\x18\x08 \x01(\tR\x08\x65ndpoint\x12\x1e\n\ndeployment\x18\t \x01(\tR\ndeployment\x12\x1f\n\x0b\x61pi_version\x18\n \x01(\tR\napiVersionB\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"}\n\x0f\x41\x44KMemoryConfig\x12&\n\x08ttl_days\x18\x01 \x01(\x03\x42\x0b\xbaH\x08\"\x06\x18\xff\xc1\x06(\x00R\x07ttlDays\x12\x42\n\tembedding\x18\x02 \x01(\x0b\x32$.kagent.config.v1.ADKEmbeddingConfigR\tembedding\";\n\x10\x41\x44KNetworkConfig\x12\'\n\x0f\x61llowed_domains\x18\x01 \x03(\tR\x0e\x61llowedDomains\"Z\n\x10\x41\x44KContextConfig\x12\x46\n\ncompaction\x18\x01 \x01(\x0b\x32&.kagent.config.v1.ADKCompressionConfigR\ncompaction\"\xe1\x03\n\x14\x41\x44KCompressionConfig\x12\x43\n\x13\x63ompaction_interval\x18\x01 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x00R\x12\x63ompactionInterval\x88\x01\x01\x12\x35\n\x0coverlap_size\x18\x02 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x00H\x01R\x0boverlapSize\x88\x01\x01\x12K\n\x10summarizer_model\x18\x03 \x01(\x0b\x32 .kagent.config.v1.ADKModelConfigR\x0fsummarizerModel\x12\'\n\x0fprompt_template\x18\x04 \x01(\tR\x0epromptTemplate\x12;\n\x0ftoken_threshold\x18\x05 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x02R\x0etokenThreshold\x88\x01\x01\x12\x44\n\x14\x65vent_retention_size\x18\x06 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x00H\x03R\x12\x65ventRetentionSize\x88\x01\x01\x42\x16\n\x14_compaction_intervalB\x0f\n\r_overlap_sizeB\x12\n\x10_token_thresholdB\x17\n\x15_event_retention_size\"J\n\x0f\x41\x44KOutputConfig\x12\x1f\n\x0bjson_schema\x18\x01 \x01(\x0cR\njsonSchema\x12\x16\n\x06sha256\x18\x02 \x01(\tR\x06sha256\"\x82\x01\n\x0f\x41\x44KAgentPlugins\x12\x32\n\x06skills\x18\x01 \x03(\x0b\x32\x1a.kagent.config.v1.ADKSkillR\x06skills\x12;\n\x07plugins\x18\x02 \x03(\x0b\x32!.kagent.config.v1.ADKPluginBundleR\x07plugins\"Y\n\x08\x41\x44KSkill\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x39\n\x06source\x18\x02 \x01(\x0b\x32!.kagent.config.v1.ADKPluginSourceR\x06source\"d\n\x0f\x41\x44KPluginBundle\x12\x39\n\x06source\x18\x01 \x01(\x0b\x32!.kagent.config.v1.ADKPluginSourceR\x06source\x12\x16\n\x06skills\x18\x02 \x03(\tR\x06skills\"\xc7\x01\n\x0f\x41\x44KPluginSource\x12\x38\n\x03git\x18\x02 \x01(\x0b\x32$.kagent.config.v1.ADKPluginGitSourceH\x00R\x03git\x12\x35\n\x02s3\x18\x03 \x01(\x0b\x32#.kagent.config.v1.ADKPluginS3SourceH\x00R\x02s3\x12\x1e\n\tdirectory\x18\x05 \x01(\tH\x00R\tdirectory\x12\x12\n\x04path\x18\x04 \x01(\tR\x04pathB\x0f\n\x06source\x12\x05\xbaH\x02\x08\x01\">\n\x12\x41\x44KPluginGitSource\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n\x06\x63ommit\x18\x02 \x01(\tR\x06\x63ommit\"\x90\x01\n\x11\x41\x44KPluginS3Source\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12\x16\n\x06\x62ucket\x18\x02 \x01(\tR\x06\x62ucket\x12\x10\n\x03key\x18\x03 \x01(\tR\x03key\x12\x1d\n\nversion_id\x18\x04 \x01(\tR\tversionId\x12\x16\n\x06region\x18\x05 \x01(\tR\x06region\"\x83\x07\n\x10\x41\x44KRuntimeConfig\x12J\n\x11\x63ontrol_plane_url\x18\x01 \x01(\tB\x1e\xbaH\x1b\xd8\x01\x01r\x16\x32\x11^https?://[^/?#]+\x88\x01\x01R\x0f\x63ontrolPlaneUrl\x12?\n\x0bgateway_url\x18\x02 \x01(\tB\x1e\xbaH\x1b\xd8\x01\x01r\x16\x32\x11^https?://[^/?#]+\x88\x01\x01R\ngatewayUrl\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12\x1c\n\tnamespace\x18\x04 \x01(\tR\tnamespace\x12>\n\tlog_level\x18\x05 \x01(\tB!\xbaH\x1er\x1cR\x00R\x05\x64\x65\x62ugR\x04infoR\x04warnR\x05\x65rrorR\x08logLevel\x12\x12\n\x04host\x18\x06 \x01(\tR\x04host\x12$\n\x04port\x18\x07 \x01(\rB\x0b\xbaH\x08*\x06\x18\xff\xff\x03 \x00H\x00R\x04port\x88\x01\x01\x12:\n\x12max_content_length\x18\x08 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x00H\x01R\x10maxContentLength\x88\x01\x01\x12U\n\x11token_propagation\x18\t \x01(\x0b\x32(.kagent.config.v1.TokenPropagationConfigR\x10tokenPropagation\x12\xb7\x02\n\ttelemetry\x18\n \x01(\x0b\x32!.kagent.config.v1.TelemetryConfigB\xf5\x01\xbaH\xf1\x01\xba\x01\xed\x01\n\"runtime.telemetry.service_identity\x12\x62runtime telemetry resource_attributes must not override compiled service.name or service.namespace\x1a\x63!(\'service.name\' in this.resource_attributes) && !(\'service.namespace\' in this.resource_attributes)R\ttelemetry\x12I\n\rbuiltin_tools\x18\x0b \x01(\x0b\x32$.kagent.config.v1.BuiltinToolsConfigR\x0c\x62uiltinToolsB\x07\n\x05_portB\x15\n\x13_max_content_lengthBCZAgithub.com/kagent-dev/kagent/go/api/gen/kagent/config/v1;configv1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1akagent/config/v1/adk.proto\x12\x10kagent.config.v1\x1a\x1b\x62uf/validate/validate.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1dkagent/config/v1/config.proto\"\x81\x01\n\tADKConfig\x12<\n\x07runtime\x18\x01 \x01(\x0b\x32\".kagent.config.v1.ADKRuntimeConfigR\x07runtime\x12\x36\n\x05\x61gent\x18\x02 \x01(\x0b\x32 .kagent.config.v1.ADKAgentConfigR\x05\x61gent\"\xff\x07\n\x0e\x41\x44KAgentConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x36\n\x05model\x18\x02 \x01(\x0b\x32 .kagent.config.v1.ADKModelConfigR\x05model\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12 \n\x0binstruction\x18\x04 \x01(\tR\x0binstruction\x12G\n\nhttp_tools\x18\x05 \x03(\x0b\x32(.kagent.config.v1.ADKHTTPMCPServerConfigR\thttpTools\x12\x44\n\tsse_tools\x18\x06 \x03(\x0b\x32\'.kagent.config.v1.ADKSSEMCPServerConfigR\x08sseTools\x12J\n\x0bstdio_tools\x18\x07 \x03(\x0b\x32).kagent.config.v1.ADKStdioMCPServerConfigR\nstdioTools\x12K\n\rremote_agents\x18\x08 \x03(\x0b\x32&.kagent.config.v1.ADKRemoteAgentConfigR\x0cremoteAgents\x12\x1b\n\x06stream\x18\t \x01(\x08H\x00R\x06stream\x88\x01\x01\x12\x39\n\x06memory\x18\n \x01(\x0b\x32!.kagent.config.v1.ADKMemoryConfigR\x06memory\x12<\n\x07network\x18\x0b \x01(\x0b\x32\".kagent.config.v1.ADKNetworkConfigR\x07network\x12\x46\n\ragent_plugins\x18\x0c \x01(\x0b\x32!.kagent.config.v1.ADKAgentPluginsR\x0c\x61gentPlugins\x12I\n\x0e\x63ontext_config\x18\r \x01(\x0b\x32\".kagent.config.v1.ADKContextConfigR\rcontextConfig\x12$\n\x0bshare_tools\x18\x0e \x01(\x08H\x01R\nshareTools\x88\x01\x01\x12$\n\x0esession_db_url\x18\x0f \x01(\tR\x0csessionDbUrl\x12)\n\x10skills_directory\x18\x10 \x01(\tR\x0fskillsDirectory\x12?\n\nsub_agents\x18\x11 \x03(\x0b\x32 .kagent.config.v1.ADKAgentConfigR\tsubAgents\x12\x39\n\x06output\x18\x12 \x01(\x0b\x32!.kagent.config.v1.ADKOutputConfigR\x06outputB\t\n\x07_streamB\x0e\n\x0c_share_tools\"\xa1\t\n\x0e\x41\x44KModelConfig\x12\x14\n\x05model\x18\x01 \x01(\tR\x05model\x12G\n\x07headers\x18\x02 \x03(\x0b\x32-.kagent.config.v1.ADKModelConfig.HeadersEntryR\x07headers\x12<\n\x18tls_insecure_skip_verify\x18\x03 \x01(\x08H\x01R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x04 \x01(\tH\x02R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x05 \x01(\x08H\x03R\x13tlsDisableSystemCas\x88\x01\x01\x12.\n\x13\x61pi_key_passthrough\x18\x06 \x01(\x08R\x11\x61piKeyPassthrough\x12\x35\n\x06openai\x18\n \x01(\x0b\x32\x1b.kagent.config.v1.ADKOpenAIH\x00R\x06openai\x12\x45\n\x0c\x61zure_openai\x18\x0b \x01(\x0b\x32 .kagent.config.v1.ADKAzureOpenAIH\x00R\x0b\x61zureOpenai\x12>\n\tanthropic\x18\x0c \x01(\x0b\x32\x1e.kagent.config.v1.ADKAnthropicH\x00R\tanthropic\x12O\n\x10gemini_vertex_ai\x18\r \x01(\x0b\x32#.kagent.config.v1.ADKGeminiVertexAIH\x00R\x0egeminiVertexAi\x12Q\n\x10gemini_anthropic\x18\x0e \x01(\x0b\x32$.kagent.config.v1.ADKGeminiAnthropicH\x00R\x0fgeminiAnthropic\x12\x35\n\x06ollama\x18\x0f \x01(\x0b\x32\x1b.kagent.config.v1.ADKOllamaH\x00R\x06ollama\x12\x35\n\x06gemini\x18\x10 \x01(\x0b\x32\x1b.kagent.config.v1.ADKGeminiH\x00R\x06gemini\x12\x38\n\x07\x62\x65\x64rock\x18\x11 \x01(\x0b\x32\x1c.kagent.config.v1.ADKBedrockH\x00R\x07\x62\x65\x64rock\x12@\n\x0bsap_ai_core\x18\x12 \x01(\x0b\x32\x1e.kagent.config.v1.ADKSAPAICoreH\x00R\tsapAiCore\x12\x38\n\x07\x66oundry\x18\x13 \x01(\x0b\x32\x1c.kagent.config.v1.ADKFoundryH\x00R\x07\x66oundry\x12\x38\n\x07mistral\x18\x14 \x01(\x0b\x32\x1c.kagent.config.v1.ADKMistralH\x00R\x07mistral\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x11\n\x08provider\x12\x05\xbaH\x02\x08\x01\x42\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"\x86\x08\n\tADKOpenAI\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12I\n\x11\x66requency_penalty\x18\x02 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\xc0H\x00R\x10\x66requencyPenalty\x88\x01\x01\x12+\n\nmax_tokens\x18\x03 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x01R\tmaxTokens\x88\x01\x01\x12@\n\x15max_completion_tokens\x18\x04 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x02R\x13maxCompletionTokens\x88\x01\x01\x12\x1a\n\x01n\x18\x05 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x03R\x01n\x88\x01\x01\x12G\n\x10presence_penalty\x18\x06 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\xc0H\x04R\x0fpresencePenalty\x88\x01\x01\x12.\n\x10reasoning_effort\x18\x07 \x01(\tH\x05R\x0freasoningEffort\x88\x01\x01\x12\x17\n\x04seed\x18\x08 \x01(\x03H\x06R\x04seed\x88\x01\x01\x12>\n\x0btemperature\x18\t \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x07R\x0btemperature\x88\x01\x01\x12\x45\n\x07timeout\x18\n \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12\x31\n\x05top_p\x18\x0b \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x08R\x04topP\x88\x01\x01\x12\x42\n\napi_format\x18\x0c \x01(\tB#\xbaH r\x1eR\x00R\x0f\x63hatCompletionsR\tresponsesR\tapiFormat\x12O\n\x0etoken_exchange\x18\r \x01(\x0b\x32(.kagent.config.v1.ADKTokenExchangeConfigR\rtokenExchange:\x94\x01\xbaH\x90\x01\x1a\x8d\x01\n\x13openai.token_limits\x12;max_tokens and max_completion_tokens are mutually exclusive\x1a\x39!has(this.max_tokens) || !has(this.max_completion_tokens)B\x14\n\x12_frequency_penaltyB\r\n\x0b_max_tokensB\x18\n\x16_max_completion_tokensB\x04\n\x02_nB\x13\n\x11_presence_penaltyB\x13\n\x11_reasoning_effortB\x07\n\x05_seedB\x0e\n\x0c_temperatureB\x08\n\x06_top_p\"\x8b\x01\n\x16\x41\x44KTokenExchangeConfig\x12`\n\x14gdch_service_account\x18\x01 \x01(\x0b\x32,.kagent.config.v1.ADKGDCHTokenExchangeConfigH\x00R\x12gdchServiceAccountB\x0f\n\x06source\x12\x05\xbaH\x02\x08\x01\"|\n\x1a\x41\x44KGDCHTokenExchangeConfig\x12\x39\n\x14service_account_path\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x12serviceAccountPath\x12#\n\x08\x61udience\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x08\x61udience\"\xfa\x02\n\x0e\x41\x44KAzureOpenAI\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12\x1e\n\ndeployment\x18\x02 \x01(\tR\ndeployment\x12\x1f\n\x0b\x61pi_version\x18\x03 \x01(\tR\napiVersion\x12+\n\nmax_tokens\x18\x04 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x00R\tmaxTokens\x88\x01\x01\x12>\n\x0btemperature\x18\x05 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x01R\x0btemperature\x88\x01\x01\x12\x31\n\x05top_p\x18\x06 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x02R\x04topP\x88\x01\x01\x12\x42\n\napi_format\x18\x07 \x01(\tB#\xbaH r\x1eR\x00R\x0f\x63hatCompletionsR\tresponsesR\tapiFormatB\r\n\x0b_max_tokensB\x0e\n\x0c_temperatureB\x08\n\x06_top_p\"\xbb\x03\n\x0c\x41\x44KAnthropic\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12+\n\nmax_tokens\x18\x02 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x00R\tmaxTokens\x88\x01\x01\x12>\n\x0btemperature\x18\x03 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x01R\x0btemperature\x88\x01\x01\x12\x31\n\x05top_p\x18\x04 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x02R\x04topP\x88\x01\x01\x12!\n\x05top_k\x18\x05 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x00H\x03R\x04topK\x88\x01\x01\x12\x45\n\x07timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12%\n\x0eprompt_caching\x18\x07 \x01(\x08R\rpromptCaching\x12,\n\tcache_ttl\x18\x08 \x01(\tB\x0f\xbaH\x0cr\nR\x00R\x02\x35mR\x02\x31hR\x08\x63\x61\x63heTtlB\r\n\x0b_max_tokensB\x0e\n\x0c_temperatureB\x08\n\x06_top_pB\x08\n\x06_top_k\"\xb7\x02\n\nADKMistral\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12+\n\nmax_tokens\x18\x02 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x01H\x00R\tmaxTokens\x88\x01\x01\x12>\n\x0btemperature\x18\x03 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x00@)\x00\x00\x00\x00\x00\x00\x00\x00H\x01R\x0btemperature\x88\x01\x01\x12\x31\n\x05top_p\x18\x04 \x01(\x01\x42\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\xf0?)\x00\x00\x00\x00\x00\x00\x00\x00H\x02R\x04topP\x88\x01\x01\x12\x45\n\x07timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeoutB\r\n\x0b_max_tokensB\x0e\n\x0c_temperatureB\x08\n\x06_top_p\"i\n\x11\x41\x44KGeminiVertexAI\x12>\n\x11max_output_tokens\x18\x01 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x00R\x0fmaxOutputTokens\x88\x01\x01\x42\x14\n\x12_max_output_tokens\"\x14\n\x12\x41\x44KGeminiAnthropic\"\x8b\x01\n\tADKOllama\x12\x42\n\x07options\x18\x01 \x03(\x0b\x32(.kagent.config.v1.ADKOllama.OptionsEntryR\x07options\x1a:\n\x0cOptionsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"a\n\tADKGemini\x12>\n\x11max_output_tokens\x18\x01 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x00R\x0fmaxOutputTokens\x88\x01\x01\x42\x14\n\x12_max_output_tokens\"\xab\x03\n\nADKBedrock\x12\x16\n\x06region\x18\x01 \x01(\tR\x06region\x12\x45\n\x1f\x61\x64\x64itional_model_request_fields\x18\x02 \x01(\x0cR\x1c\x61\x64\x64itionalModelRequestFields\x12%\n\x0eprompt_caching\x18\x03 \x01(\x08R\rpromptCaching\x12,\n\tcache_ttl\x18\x04 \x01(\tB\x0f\xbaH\x0cr\nR\x00R\x02\x35mR\x02\x31hR\x08\x63\x61\x63heTtl\x12\x43\n\tguardrail\x18\x05 \x01(\x0b\x32%.kagent.config.v1.ADKBedrockGuardrailR\tguardrail\x12N\n\x0cread_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0breadTimeout\x12T\n\x0f\x63onnect_timeout\x18\x07 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0e\x63onnectTimeout\"e\n\x13\x41\x44KBedrockGuardrail\x12\x1e\n\nidentifier\x18\x01 \x01(\tR\nidentifier\x12\x18\n\x07version\x18\x02 \x01(\tR\x07version\x12\x14\n\x05trace\x18\x03 \x01(\tR\x05trace\"k\n\x0c\x41\x44KSAPAICore\x12\x19\n\x08\x62\x61se_url\x18\x01 \x01(\tR\x07\x62\x61seUrl\x12%\n\x0eresource_group\x18\x02 \x01(\tR\rresourceGroup\x12\x19\n\x08\x61uth_url\x18\x03 \x01(\tR\x07\x61uthUrl\"\xa4\x01\n\nADKFoundry\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12\x1e\n\ndeployment\x18\x02 \x01(\tR\ndeployment\x12\x1f\n\x0b\x61pi_version\x18\x03 \x01(\tR\napiVersion\x12\x39\n\napi_format\x18\x04 \x01(\tB\x1a\xbaH\x17r\x15R\x00R\x06openaiR\tanthropicR\tapiFormat\"\x94\x05\n\x17\x41\x44KHTTPConnectionParams\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12P\n\x07headers\x18\x02 \x03(\x0b\x32\x36.kagent.config.v1.ADKHTTPConnectionParams.HeadersEntryR\x07headers\x12\x45\n\x07timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12U\n\x10sse_read_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0esseReadTimeout\x12\x31\n\x12terminate_on_close\x18\x05 \x01(\x08H\x00R\x10terminateOnClose\x88\x01\x01\x12<\n\x18tls_insecure_skip_verify\x18\x06 \x01(\x08H\x01R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x07 \x01(\tH\x02R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x08 \x01(\x08H\x03R\x13tlsDisableSystemCas\x88\x01\x01\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x15\n\x13_terminate_on_closeB\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"\xc8\x04\n\x16\x41\x44KSSEConnectionParams\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12O\n\x07headers\x18\x02 \x03(\x0b\x32\x35.kagent.config.v1.ADKSSEConnectionParams.HeadersEntryR\x07headers\x12\x45\n\x07timeout\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x12U\n\x10sse_read_timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x0esseReadTimeout\x12<\n\x18tls_insecure_skip_verify\x18\x05 \x01(\x08H\x00R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x06 \x01(\tH\x01R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x07 \x01(\x08H\x02R\x13tlsDisableSystemCas\x88\x01\x01\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"\xfa\x01\n\x16\x41\x44KHTTPMCPServerConfig\x12I\n\x06params\x18\x01 \x01(\x0b\x32).kagent.config.v1.ADKHTTPConnectionParamsB\x06\xbaH\x03\xc8\x01\x01R\x06params\x12\x14\n\x05tools\x18\x02 \x03(\tR\x05tools\x12\'\n\x0f\x61llowed_headers\x18\x03 \x03(\tR\x0e\x61llowedHeaders\x12)\n\x10require_approval\x18\x04 \x01(\x08R\x0frequireApproval\x12+\n\x11token_passthrough\x18\x05 \x01(\x08R\x10tokenPassthrough\"\xf8\x01\n\x15\x41\x44KSSEMCPServerConfig\x12H\n\x06params\x18\x01 \x01(\x0b\x32(.kagent.config.v1.ADKSSEConnectionParamsB\x06\xbaH\x03\xc8\x01\x01R\x06params\x12\x14\n\x05tools\x18\x02 \x03(\tR\x05tools\x12\'\n\x0f\x61llowed_headers\x18\x03 \x03(\tR\x0e\x61llowedHeaders\x12)\n\x10require_approval\x18\x04 \x01(\x08R\x0frequireApproval\x12+\n\x11token_passthrough\x18\x05 \x01(\x08R\x10tokenPassthrough\"\xd7\x01\n\x17\x41\x44KStdioMCPServerConfig\x12\x18\n\x07\x63ommand\x18\x01 \x01(\tR\x07\x63ommand\x12\x12\n\x04\x61rgs\x18\x02 \x03(\tR\x04\x61rgs\x12\x44\n\x03\x65nv\x18\x03 \x03(\x0b\x32\x32.kagent.config.v1.ADKStdioMCPServerConfig.EnvEntryR\x03\x65nv\x12\x10\n\x03\x64ir\x18\x04 \x01(\tR\x03\x64ir\x1a\x36\n\x08\x45nvEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdb\x02\n\x14\x41\x44KRemoteAgentConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n\x03url\x18\x02 \x01(\tR\x03url\x12M\n\x07headers\x18\x03 \x03(\x0b\x32\x33.kagent.config.v1.ADKRemoteAgentConfig.HeadersEntryR\x07headers\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12)\n\x10isolate_sessions\x18\x05 \x01(\x08R\x0fisolateSessions\x12\x45\n\x07timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationB\x10\xbaH\r\xaa\x01\n\"\x06\x08\x84\xfa\x85\xae\"*\x00R\x07timeout\x1a:\n\x0cHeadersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xe1\x03\n\x12\x41\x44KEmbeddingConfig\x12\x1a\n\x08provider\x18\x01 \x01(\tR\x08provider\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12\x19\n\x08\x62\x61se_url\x18\x03 \x01(\tR\x07\x62\x61seUrl\x12.\n\x13\x61pi_key_passthrough\x18\x04 \x01(\x08R\x11\x61piKeyPassthrough\x12<\n\x18tls_insecure_skip_verify\x18\x05 \x01(\x08H\x00R\x15tlsInsecureSkipVerify\x88\x01\x01\x12,\n\x10tls_ca_cert_path\x18\x06 \x01(\tH\x01R\rtlsCaCertPath\x88\x01\x01\x12\x38\n\x16tls_disable_system_cas\x18\x07 \x01(\x08H\x02R\x13tlsDisableSystemCas\x88\x01\x01\x12\x1a\n\x08\x65ndpoint\x18\x08 \x01(\tR\x08\x65ndpoint\x12\x1e\n\ndeployment\x18\t \x01(\tR\ndeployment\x12\x1f\n\x0b\x61pi_version\x18\n \x01(\tR\napiVersionB\x1b\n\x19_tls_insecure_skip_verifyB\x13\n\x11_tls_ca_cert_pathB\x19\n\x17_tls_disable_system_cas\"}\n\x0f\x41\x44KMemoryConfig\x12&\n\x08ttl_days\x18\x01 \x01(\x03\x42\x0b\xbaH\x08\"\x06\x18\xff\xc1\x06(\x00R\x07ttlDays\x12\x42\n\tembedding\x18\x02 \x01(\x0b\x32$.kagent.config.v1.ADKEmbeddingConfigR\tembedding\";\n\x10\x41\x44KNetworkConfig\x12\'\n\x0f\x61llowed_domains\x18\x01 \x03(\tR\x0e\x61llowedDomains\"Z\n\x10\x41\x44KContextConfig\x12\x46\n\ncompaction\x18\x01 \x01(\x0b\x32&.kagent.config.v1.ADKCompressionConfigR\ncompaction\"\xe1\x03\n\x14\x41\x44KCompressionConfig\x12\x43\n\x13\x63ompaction_interval\x18\x01 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x00R\x12\x63ompactionInterval\x88\x01\x01\x12\x35\n\x0coverlap_size\x18\x02 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x00H\x01R\x0boverlapSize\x88\x01\x01\x12K\n\x10summarizer_model\x18\x03 \x01(\x0b\x32 .kagent.config.v1.ADKModelConfigR\x0fsummarizerModel\x12\'\n\x0fprompt_template\x18\x04 \x01(\tR\x0epromptTemplate\x12;\n\x0ftoken_threshold\x18\x05 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x01H\x02R\x0etokenThreshold\x88\x01\x01\x12\x44\n\x14\x65vent_retention_size\x18\x06 \x01(\x03\x42\r\xbaH\n\"\x08\x18\xff\xff\xff\xff\x07(\x00H\x03R\x12\x65ventRetentionSize\x88\x01\x01\x42\x16\n\x14_compaction_intervalB\x0f\n\r_overlap_sizeB\x12\n\x10_token_thresholdB\x17\n\x15_event_retention_size\"J\n\x0f\x41\x44KOutputConfig\x12\x1f\n\x0bjson_schema\x18\x01 \x01(\x0cR\njsonSchema\x12\x16\n\x06sha256\x18\x02 \x01(\tR\x06sha256\"\x82\x01\n\x0f\x41\x44KAgentPlugins\x12\x32\n\x06skills\x18\x01 \x03(\x0b\x32\x1a.kagent.config.v1.ADKSkillR\x06skills\x12;\n\x07plugins\x18\x02 \x03(\x0b\x32!.kagent.config.v1.ADKPluginBundleR\x07plugins\"Y\n\x08\x41\x44KSkill\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x39\n\x06source\x18\x02 \x01(\x0b\x32!.kagent.config.v1.ADKPluginSourceR\x06source\"d\n\x0f\x41\x44KPluginBundle\x12\x39\n\x06source\x18\x01 \x01(\x0b\x32!.kagent.config.v1.ADKPluginSourceR\x06source\x12\x16\n\x06skills\x18\x02 \x03(\tR\x06skills\"\xc7\x01\n\x0f\x41\x44KPluginSource\x12\x38\n\x03git\x18\x02 \x01(\x0b\x32$.kagent.config.v1.ADKPluginGitSourceH\x00R\x03git\x12\x35\n\x02s3\x18\x03 \x01(\x0b\x32#.kagent.config.v1.ADKPluginS3SourceH\x00R\x02s3\x12\x1e\n\tdirectory\x18\x05 \x01(\tH\x00R\tdirectory\x12\x12\n\x04path\x18\x04 \x01(\tR\x04pathB\x0f\n\x06source\x12\x05\xbaH\x02\x08\x01\"s\n\x12\x41\x44KPluginGitSource\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n\x06\x63ommit\x18\x02 \x01(\tR\x06\x63ommit\x12\x33\n\x15gateway_authorization\x18\x03 \x01(\x08R\x14gatewayAuthorization\"\x90\x01\n\x11\x41\x44KPluginS3Source\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12\x16\n\x06\x62ucket\x18\x02 \x01(\tR\x06\x62ucket\x12\x10\n\x03key\x18\x03 \x01(\tR\x03key\x12\x1d\n\nversion_id\x18\x04 \x01(\tR\tversionId\x12\x16\n\x06region\x18\x05 \x01(\tR\x06region\"\x83\x07\n\x10\x41\x44KRuntimeConfig\x12J\n\x11\x63ontrol_plane_url\x18\x01 \x01(\tB\x1e\xbaH\x1b\xd8\x01\x01r\x16\x32\x11^https?://[^/?#]+\x88\x01\x01R\x0f\x63ontrolPlaneUrl\x12?\n\x0bgateway_url\x18\x02 \x01(\tB\x1e\xbaH\x1b\xd8\x01\x01r\x16\x32\x11^https?://[^/?#]+\x88\x01\x01R\ngatewayUrl\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12\x1c\n\tnamespace\x18\x04 \x01(\tR\tnamespace\x12>\n\tlog_level\x18\x05 \x01(\tB!\xbaH\x1er\x1cR\x00R\x05\x64\x65\x62ugR\x04infoR\x04warnR\x05\x65rrorR\x08logLevel\x12\x12\n\x04host\x18\x06 \x01(\tR\x04host\x12$\n\x04port\x18\x07 \x01(\rB\x0b\xbaH\x08*\x06\x18\xff\xff\x03 \x00H\x00R\x04port\x88\x01\x01\x12:\n\x12max_content_length\x18\x08 \x01(\x03\x42\x07\xbaH\x04\"\x02(\x00H\x01R\x10maxContentLength\x88\x01\x01\x12U\n\x11token_propagation\x18\t \x01(\x0b\x32(.kagent.config.v1.TokenPropagationConfigR\x10tokenPropagation\x12\xb7\x02\n\ttelemetry\x18\n \x01(\x0b\x32!.kagent.config.v1.TelemetryConfigB\xf5\x01\xbaH\xf1\x01\xba\x01\xed\x01\n\"runtime.telemetry.service_identity\x12\x62runtime telemetry resource_attributes must not override compiled service.name or service.namespace\x1a\x63!(\'service.name\' in this.resource_attributes) && !(\'service.namespace\' in this.resource_attributes)R\ttelemetry\x12I\n\rbuiltin_tools\x18\x0b \x01(\x0b\x32$.kagent.config.v1.BuiltinToolsConfigR\x0c\x62uiltinToolsB\x07\n\x05_portB\x15\n\x13_max_content_lengthBCZAgithub.com/kagent-dev/kagent/go/api/gen/kagent/config/v1;configv1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -232,9 +232,9 @@ _globals['_ADKPLUGINSOURCE']._serialized_start=10080 _globals['_ADKPLUGINSOURCE']._serialized_end=10279 _globals['_ADKPLUGINGITSOURCE']._serialized_start=10281 - _globals['_ADKPLUGINGITSOURCE']._serialized_end=10343 - _globals['_ADKPLUGINS3SOURCE']._serialized_start=10346 - _globals['_ADKPLUGINS3SOURCE']._serialized_end=10490 - _globals['_ADKRUNTIMECONFIG']._serialized_start=10493 - _globals['_ADKRUNTIMECONFIG']._serialized_end=11392 + _globals['_ADKPLUGINGITSOURCE']._serialized_end=10396 + _globals['_ADKPLUGINS3SOURCE']._serialized_start=10399 + _globals['_ADKPLUGINS3SOURCE']._serialized_end=10543 + _globals['_ADKRUNTIMECONFIG']._serialized_start=10546 + _globals['_ADKRUNTIMECONFIG']._serialized_end=11445 # @@protoc_insertion_point(module_scope) diff --git a/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.pyi b/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.pyi index fc1ad0afee..8de7ae3641 100644 --- a/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.pyi +++ b/python/packages/kagent-proto/src/kagent/config/v1/adk_pb2.pyi @@ -506,12 +506,14 @@ class ADKPluginSource(_message.Message): def __init__(self, git: _Optional[_Union[ADKPluginGitSource, _Mapping]] = ..., s3: _Optional[_Union[ADKPluginS3Source, _Mapping]] = ..., directory: _Optional[str] = ..., path: _Optional[str] = ...) -> None: ... class ADKPluginGitSource(_message.Message): - __slots__ = ("url", "commit") + __slots__ = ("url", "commit", "gateway_authorization") URL_FIELD_NUMBER: _ClassVar[int] COMMIT_FIELD_NUMBER: _ClassVar[int] + GATEWAY_AUTHORIZATION_FIELD_NUMBER: _ClassVar[int] url: str commit: str - def __init__(self, url: _Optional[str] = ..., commit: _Optional[str] = ...) -> None: ... + gateway_authorization: bool + def __init__(self, url: _Optional[str] = ..., commit: _Optional[str] = ..., gateway_authorization: _Optional[bool] = ...) -> None: ... class ADKPluginS3Source(_message.Message): __slots__ = ("endpoint", "bucket", "key", "version_id", "region")