Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Their SHAs change at every re-pin; the ledger records the SHAs of each rebase.
| `chore(e2e): the egress test waits on the denied redirect's verdict` | `TestSessionEgressDeniesUnconfiguredDestination/claude` flaked in the gVisor lane with `send A2A message: DeadlineExceeded` (giantswarm/kagent-upstream#263, beside #205): the denied redirect turn was sent with the blocking send, whose retry poll bounds each call at 30 s, and a harness that retries the denied model call before it fails the turn outlasted it. The test now streams that turn and waits for its terminal state under the fixture's bound, then reads the task; the assertions are unchanged | to file; giantswarm/giantswarm#37742 |
| `chore(e2e): run each send under the caller's context, not the retry window` | `TestSessionInteraction/byo-adk` flaked in the gVisor lane (giantswarm/kagent-upstream#276): `sendMessageWithRetry` ran every `SendMessage` under the context of its 30 s retry poll, so a blocking send whose verdict took longer on a loaded lane failed `DeadlineExceeded` before the turn ended; `sendHTTPStreamingMessageWithRetry` cut a JSON-RPC stream the same way. Each send now runs under the caller's context; the 30 s window bounds only the resends of the gateway's proven refusal (`KAGENT_SEND_NOT_ACCEPTED`), and a refusal still standing at its end is returned. The assertions are unchanged | to file; upstream main has the same helpers; giantswarm/giantswarm#37742 |
| `fix(adk): run bash tool commands on an allowlisted environment` | the Go ADK's bash tool never set `cmd.Env`, so every command a model wrote inherited the runtime's whole environment — the model provider keys the controller injects and every `KAGENT_*` value, `KAGENT_CONFIG_JSON` among them — and its output went back to the model (giantswarm/kagent-upstream#301). The command now runs on an allowlist: `PATH`, `HOME`, `USER`, `LOGNAME`, `LANG`, `LC_*`, `TERM`, `TZ`, `TMPDIR`, the CA-bundle variables a command needs behind the egress gateway, and `PWD` set to the working directory; proxy variables stay out because their URLs may carry credentials | kagent-dev/kagent#3002; kagent-dev/kagent#3014 (open) strips secret-looking names instead, which passes every `KAGENT_*` value; offered as the stricter alternative, giantswarm/giantswarm#37742 |
| `feat(docker): the Harness images carry the GitHub CLI` and `chore(e2e): read the shell tool's output from every part a task persisted` | a coding agent in a workspace Session works its clone with git and `gh` (`gh pr create`, `gh pr view`, `gh run list`), and neither the Go runtime image nor the Claude harness image carried `gh` (giantswarm/kagent-upstream#288). Both install Alpine's `github-cli` at the one version in `go/github-cli.version`, bind-mounted into the `apk add` step; the Claude harness image moves to `alpine:3.24`, the release `go/Dockerfile` uses, so the pin resolves in both. No credential is configured. Renovate tracks the pin through one `custom.regex` manager in `renovate.json5`, the only manager the line runs | to file; giantswarm/giantswarm#37742 row 159 (upstream's images carry no `gh`; if upstream declines it for the Go ADK image, the row becomes fork-only) |

Rules for the table: every non-`ci(fork)` row has an upstream pull request or a "to file" that
giantswarm/giantswarm#37742 tracks; a row leaves when the sync drops the commit because upstream merged it
Expand Down
7 changes: 6 additions & 1 deletion go/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ RUN --mount=type=cache,target=/root/go/pkg/mod,rw \
FROM alpine:3.24
ARG TARGETPLATFORM

RUN apk upgrade --no-cache && apk add --no-cache bash ca-certificates git && \
# The GitHub CLI for an agent's work in its clone. github-cli.version pins it
# once for this image and the Claude harness image; apk's ~ takes any Alpine
# rebuild (-rN) of that version.
RUN --mount=type=bind,source=github-cli.version,target=/tmp/github-cli.version \
apk upgrade --no-cache && \
apk add --no-cache bash ca-certificates git "github-cli~$(cat /tmp/github-cli.version)" && \
mkdir -p /config /skills /plugins /data/plugins && \
chown -R 65532:65532 /config /skills /plugins /data

Expand Down
127 changes: 127 additions & 0 deletions go/core/test/e2e/mocks/invoke_cli_versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{
"openai": [
{
"name": "cli_versions_start",
"match": {
"match_type": "contains",
"message": {
"content": "Print the CLI versions.",
"role": "user"
}
},
"response": {
"id": "chatcmpl-cli-versions-1",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4.1-mini",
"choices": [
{
"index": 0,
"role": "assistant",
"message": {
"content": "",
"tool_calls": [
{
"id": "call_cli_versions",
"type": "function",
"function": {
"name": "bash",
"arguments": "{\"command\": \"gh --version && git --version\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}
},
{
"name": "cli_versions_result",
"match": {
"match_type": "contains",
"message": {
"content": "gh version",
"role": "tool",
"tool_call_id": "call_cli_versions"
}
},
"response": {
"id": "chatcmpl-cli-versions-2",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4.1-mini",
"choices": [
{
"index": 0,
"role": "assistant",
"message": {
"content": "CLI_VERSIONS_DONE",
"role": "assistant"
},
"finish_reason": "stop"
}
]
}
}
],
"anthropic": [
{
"name": "cli_versions_start",
"match": {
"match_type": "contains",
"message": {
"role": "user",
"content": [{"type": "text", "text": "Print the CLI versions."}]
}
},
"response": {
"id": "msg_cli_versions_bash",
"type": "message",
"role": "assistant",
"content": [{"type": "tool_use", "id": "toolu_cli_versions", "name": "Bash", "input": {"command": "gh --version && git --version"}}],
"model": "claude-sonnet-4-5",
"stop_reason": "tool_use",
"usage": {"input_tokens": 10, "output_tokens": 4}
}
},
{
"name": "cli_versions_result",
"match": {
"match_type": "contains",
"message": {
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": "toolu_cli_versions"}]
}
},
"response": {
"id": "msg_cli_versions_done",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "CLI_VERSIONS_DONE"}],
"model": "claude-sonnet-4-5",
"stop_reason": "end_turn",
"usage": {"input_tokens": 12, "output_tokens": 4}
}
},
{
"name": "cli_versions_auxiliary",
"match": {
"match_type": "contains",
"message": {
"role": "user",
"content": [{"type": "text", "text": ""}]
}
},
"response": {
"id": "msg_cli_versions_auxiliary",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "OK"}],
"model": "claude-sonnet-4-5",
"stop_reason": "end_turn",
"usage": {"input_tokens": 2, "output_tokens": 1}
}
}
]
}
89 changes: 89 additions & 0 deletions go/core/test/e2e/session_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package e2e_test

import (
"embed"
"encoding/base64"
"encoding/json"
"testing"

a2atype "github.com/a2aproject/a2a-go/v2/a2a"
apia2a "github.com/kagent-dev/kagent/go/api/a2a"
"github.com/kagent-dev/kagent/go/api/v1alpha3"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

//go:embed mocks/invoke_cli_versions.json
var sessionCLIMocks embed.FS

// A coding agent works in its clone with git and the GitHub CLI, so both run
// through the model's shell tool in a Session's sandbox on every Harness that
// gives the model one. The kagent Harness registers its bash tool only with a
// skill, so the template carries the git fixture's skill.
func TestSessionShellRunsGitAndGitHubCLI(t *testing.T) {
t.Parallel()
for _, harness := range []struct {
testHarness
shellTool string
}{
{testHarness{name: "kagent", runtimeLabel: "kagent"}, "bash"},
{testHarness{name: claudeE2EHarness, runtimeLabel: "claude"}, "Bash"},
} {
t.Run(harness.runtimeLabel, func(t *testing.T) {
t.Parallel()
target := interactionTarget(t)
kube := interactionKubeClient(t)
credential := base64.StdEncoding.EncodeToString([]byte("x-access-token:cli-e2e-token"))
secret := createCredentialSecret(t, kube, credential)
repositories := newGitFixture(t, "Basic "+credential)
server := repositories.serveThroughEgress(t)
modelURL := reachableModelURL(t, startMockLLMServer(t, sessionCLIMocks, "mocks/invoke_cli_versions.json"))
model := harness.createModel(t, kube, modelURL, nil)
template := &v1alpha3.AgentTemplate{
ObjectMeta: metav1.ObjectMeta{GenerateName: "session-cli-", Namespace: "kagent", Labels: harness.labels()},
Spec: v1alpha3.AgentTemplateSpec{
ModelConfig: &corev1.LocalObjectReference{Name: model.Name},
SystemPrompt: "Run the commands you are asked to run.",
Skills: []v1alpha3.AgentTemplateSkill{{Name: gitFixtureSkill, Source: v1alpha3.ArtifactSource{
Git: &v1alpha3.GitArtifact{
URL: server + "/" + gitSkillRepository, Commit: repositories.commit,
CredentialRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: secret.Name}, Key: "token"},
},
Path: "skills/" + gitFixtureSkill,
}}},
},
}
createAndWaitInteractionTemplate(t, harness.testHarness, kube, template)
fixture := newInteractionFixtureForTemplate(t, harness.testHarness, target, template.Name)

streamed := sendStreaming(t, fixture, "Print the CLI versions.")
require.Equal(t, a2atype.TaskStateCompleted, streamed.state, "task text: %q", streamed.text)
require.Contains(t, streamed.text, "CLI_VERSIONS_DONE")
output := toolResponseText(t, getTask(t, fixture, streamed.taskID), harness.shellTool)
require.Contains(t, output, "gh version")
require.Contains(t, output, "git version")
})
}
}

// toolResponseText returns the serialized function_response of the task's one
// call of toolName, which carries the command's output.
func toolResponseText(t *testing.T, task *a2atype.Task, toolName string) string {
t.Helper()
var responses []string
for _, part := range taskParts(task) {
if partType, _ := part.Metadata[apia2a.PartTypeMetadataKey].(string); partType != "function_response" {
continue
}
data, ok := part.Data().(map[string]any)
if !ok || data["name"] != toolName {
continue
}
encoded, err := json.Marshal(data)
require.NoError(t, err)
responses = append(responses, string(encoded))
}
require.Len(t, responses, 1, "function_response parts of %s in task %s", toolName, task.ID)
return responses[0]
}
16 changes: 11 additions & 5 deletions go/core/test/e2e/streaming_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,21 +269,27 @@ func toolEvents(parts []*a2atype.Part) []toolEvent {
}

func taskToolEvents(task *a2atype.Task) []toolEvent {
var events []toolEvent
return toolEvents(taskParts(task))
}

// taskParts returns every part a task persisted: its history, its status
// message and its artifacts.
func taskParts(task *a2atype.Task) []*a2atype.Part {
var parts []*a2atype.Part
for _, message := range task.History {
if message != nil {
events = append(events, toolEvents(message.Parts)...)
parts = append(parts, message.Parts...)
}
}
if task.Status.Message != nil {
events = append(events, toolEvents(task.Status.Message.Parts)...)
parts = append(parts, task.Status.Message.Parts...)
}
for _, artifact := range task.Artifacts {
if artifact != nil {
events = append(events, toolEvents(artifact.Parts)...)
parts = append(parts, artifact.Parts...)
}
}
return events
return parts
}

func assertToolEvents(t *testing.T, events []toolEvent, toolNames ...string) {
Expand Down
1 change: 1 addition & 0 deletions go/github-cli.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.97.0
8 changes: 6 additions & 2 deletions go/harness/claude/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ RUN --mount=type=cache,target=/root/go/pkg/mod,rw \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build -trimpath -o /kagent-claude ./harness/claude/cmd

FROM alpine:3.22
FROM alpine:3.24
ARG CLAUDE_CODE_VERSION=2.1.285
ARG CLAUDE_CODE_AMD64_SHA256=7b4414af1bc06eb6d91730759bd01c4c02a4976d0b8526237e9a6d8c33eaf102
ARG CLAUDE_CODE_ARM64_SHA256=31efc4136bc678575f4c6730e248d34f89dbfea0468be1c5d012af199cd62ee8
ARG TARGETARCH
ARG VERSION

ENV USE_BUILTIN_RIPGREP=0
RUN apk upgrade --no-cache && apk add --no-cache bash ca-certificates git libgcc libstdc++ ripgrep \
# The GitHub CLI at the version go/Dockerfile installs (github-cli.version);
# the two images share an Alpine release so the pin resolves in both.
RUN --mount=type=bind,source=github-cli.version,target=/tmp/github-cli.version \
apk upgrade --no-cache && apk add --no-cache bash ca-certificates git libgcc libstdc++ ripgrep \
"github-cli~$(cat /tmp/github-cli.version)" \
&& case "${TARGETARCH}" in \
amd64) platform="linux-x64-musl"; checksum="${CLAUDE_CODE_AMD64_SHA256}" ;; \
arm64) platform="linux-arm64-musl"; checksum="${CLAUDE_CODE_ARM64_SHA256}" ;; \
Expand Down
29 changes: 24 additions & 5 deletions renovate.json5
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
// The Giant Swarm line of kagent-dev/kagent. Renovate is off here on
// purpose: this line is an upstream release plus the carried patches, so
// upstream owns the dependency graph. A bump opened here would diverge from
// the pinned upstream release and every re-pin would have to carry or revert
// it. Dependencies move when the line is re-pinned; see FORK.md.
// purpose for everything upstream owns: this line is an upstream release
// plus the carried patches, so upstream owns the dependency graph. A bump
// opened here would diverge from the pinned upstream release and every
// re-pin would have to carry or revert it. Dependencies move when the line
// is re-pinned; see FORK.md.
//
// The one exception is what a carried patch pins itself: the GitHub CLI in
// the Go runtime and Claude harness images (go/github-cli.version). Only the
// regex manager below runs, so no other manager opens a pull request.
{
reviewers: ['teemow'],
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
enabled: false,
enabledManagers: ['custom.regex'],
customManagers: [
{
// Alpine's github-cli package, from the Alpine release both images use
// (FROM alpine:3.24 in go/Dockerfile and go/harness/claude/Dockerfile);
// a move to another Alpine release moves this package name with it.
customType: 'regex',
managerFilePatterns: ['/^go/github-cli\\.version$/'],
matchStrings: ['^(?<currentValue>\\S+)\\s*$'],
depNameTemplate: 'github-cli',
datasourceTemplate: 'repology',
packageNameTemplate: 'alpine_3_24/github-cli',
versioningTemplate: 'loose',
},
],
}
Loading