From e2c11ace7ebdd2543f5c3cf3a44d9232280bb709 Mon Sep 17 00:00:00 2001 From: Jesus Munoz Date: Thu, 19 Mar 2026 11:51:33 +0100 Subject: [PATCH 1/6] adding configurable ssh-hosts for git-based skills Signed-off-by: Jesus Munoz --- .../translator/agent/adk_api_translator.go | 30 ++++++++-- .../translator/agent/git_skills_test.go | 55 ++++++++++++++++--- .../translator/agent/skills-init.sh.tmpl | 3 + 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index fb4b2ce16..e97ac940e 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -1584,10 +1584,17 @@ func validateSubPath(p string) error { // skillsInitData holds the template data for the unified skills-init script. type skillsInitData struct { - AuthMountPath string // "/git-auth" or "" (for git auth) - GitRefs []gitRefData // git repos to clone - OCIRefs []ociRefData // OCI images to pull - InsecureOCI bool // --insecure flag for krane + AuthMountPath string // "/git-auth" or "" (for git auth) + GitRefs []gitRefData // git repos to clone + OCIRefs []ociRefData // OCI images to pull + InsecureOCI bool // --insecure flag for krane + SSHHosts []sshHostData // extra hosts to add to known_hosts via ssh-keyscan +} + +// sshHostData holds the host and optional port for an SSH known_hosts entry. +type sshHostData struct { + Host string // hostname or IP + Port string // port number, empty means default (22) } // gitRefData holds pre-computed fields for each git skill ref, used by the script template. @@ -1651,6 +1658,21 @@ func prepareSkillsInitData( if authSecretRef != nil { data.AuthMountPath = "/git-auth" + seenHosts := make(map[string]bool) + for _, ref := range gitRefs { + u, err := url.Parse(ref.URL) + if err != nil || u.Scheme != "ssh" { + continue + } + host := u.Hostname() + port := u.Port() + key := host + ":" + port + if seenHosts[key] { + continue + } + seenHosts[key] = true + data.SSHHosts = append(data.SSHHosts, sshHostData{Host: host, Port: port}) + } } seen := make(map[string]bool) diff --git a/go/core/internal/controller/translator/agent/git_skills_test.go b/go/core/internal/controller/translator/agent/git_skills_test.go index 1d29e6c5b..5a2d2cc93 100644 --- a/go/core/internal/controller/translator/agent/git_skills_test.go +++ b/go/core/internal/controller/translator/agent/git_skills_test.go @@ -44,13 +44,14 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { name string agent *v1alpha2.Agent // assertions - wantSkillsInit bool - wantSkillsVolume bool - wantContainsBranch string - wantContainsCommit string - wantContainsPath string - wantContainsKrane bool - wantAuthVolume bool + wantSkillsInit bool + wantSkillsVolume bool + wantContainsBranch string + wantContainsCommit string + wantContainsPath string + wantContainsKrane bool + wantAuthVolume bool + wantSSHKeyscanHosts []string // substrings expected in the ssh-keyscan lines }{ { name: "no skills - no init containers", @@ -215,6 +216,34 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { wantSkillsVolume: true, wantAuthVolume: true, }, + { + name: "git skills with SSH URL and auth secret scans custom host", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-ssh", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + GitAuthSecretRef: &corev1.LocalObjectReference{ + Name: "gitea-ssh-credentials", + }, + GitRefs: []v1alpha2.GitRepo{ + { + URL: "ssh://git@gitea-ssh.gitea:22/gitops/ssh-skills-repo.git", + Ref: "main", + }, + }, + }, + }, + }, + wantSkillsInit: true, + wantSkillsVolume: true, + wantAuthVolume: true, + wantSSHKeyscanHosts: []string{"gitea-ssh.gitea"}, + }, { name: "git skill with custom name", agent: &v1alpha2.Agent{ @@ -354,11 +383,12 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { // Check auth volume if tt.wantAuthVolume { + wantSecretName := tt.agent.Spec.Skills.GitAuthSecretRef.Name hasAuthVolume := false for _, v := range deployment.Spec.Template.Spec.Volumes { if v.Secret != nil && v.Name == "git-auth" { hasAuthVolume = true - assert.Equal(t, "github-token", v.Secret.SecretName, "auth volume should reference the correct secret") + assert.Equal(t, wantSecretName, v.Secret.SecretName, "auth volume should reference the correct secret") } } assert.True(t, hasAuthVolume, "git-auth volume should exist") @@ -378,6 +408,15 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { assert.Contains(t, script, "credential.helper") } + // Verify custom SSH hosts are scanned + if len(tt.wantSSHKeyscanHosts) > 0 { + require.NotNil(t, skillsInitContainer) + script := skillsInitContainer.Command[2] + for _, host := range tt.wantSSHKeyscanHosts { + assert.Contains(t, script, host, "script should ssh-keyscan custom host %q", host) + } + } + // Verify insecure flag for OCI skills if tt.agent.Spec.Skills != nil && tt.agent.Spec.Skills.InsecureSkipVerify { require.NotNil(t, skillsInitContainer) diff --git a/go/core/internal/controller/translator/agent/skills-init.sh.tmpl b/go/core/internal/controller/translator/agent/skills-init.sh.tmpl index 3226e1bc0..23bee2ddb 100644 --- a/go/core/internal/controller/translator/agent/skills-init.sh.tmpl +++ b/go/core/internal/controller/translator/agent/skills-init.sh.tmpl @@ -9,6 +9,9 @@ if [ -f "${_auth_mount}/ssh-privatekey" ]; then cp "${_auth_mount}/ssh-privatekey" ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts +{{- range .SSHHosts }} + ssh-keyscan{{ if .Port }} -p {{ .Port }}{{ end }} {{ .Host }} >> ~/.ssh/known_hosts +{{- end }} elif [ -f "${_auth_mount}/token" ]; then git config --global credential.helper "!f() { echo username=x-access-token; echo password=\$(cat ${_auth_mount}/token); }; f" fi From 763acb7d2e39889285d7b690254739822170d0a5 Mon Sep 17 00:00:00 2001 From: Jesus Munoz Date: Thu, 19 Mar 2026 12:08:21 +0100 Subject: [PATCH 2/6] reverted to hardcode for github-token in secret Signed-off-by: Jesus Munoz --- .../internal/controller/translator/agent/git_skills_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go/core/internal/controller/translator/agent/git_skills_test.go b/go/core/internal/controller/translator/agent/git_skills_test.go index 5a2d2cc93..5cd7cdcc7 100644 --- a/go/core/internal/controller/translator/agent/git_skills_test.go +++ b/go/core/internal/controller/translator/agent/git_skills_test.go @@ -383,12 +383,11 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { // Check auth volume if tt.wantAuthVolume { - wantSecretName := tt.agent.Spec.Skills.GitAuthSecretRef.Name hasAuthVolume := false for _, v := range deployment.Spec.Template.Spec.Volumes { if v.Secret != nil && v.Name == "git-auth" { hasAuthVolume = true - assert.Equal(t, wantSecretName, v.Secret.SecretName, "auth volume should reference the correct secret") + assert.Equal(t, "github-token", v.Secret.SecretName, "auth volume should reference the correct secret") } } assert.True(t, hasAuthVolume, "git-auth volume should exist") From c0772e8a01029b3e8749714c4f9c953d6186a2eb Mon Sep 17 00:00:00 2001 From: Jesus Munoz Date: Thu, 19 Mar 2026 12:25:01 +0100 Subject: [PATCH 3/6] copilot suggestions Signed-off-by: Jesus Munoz --- .../translator/agent/adk_api_translator.go | 10 +++++++++- .../controller/translator/agent/git_skills_test.go | 4 +++- .../translator/agent/skills-init.sh.tmpl | 14 +++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index e97ac940e..c4377c933 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -1659,13 +1659,21 @@ func prepareSkillsInitData( if authSecretRef != nil { data.AuthMountPath = "/git-auth" seenHosts := make(map[string]bool) + hostPattern := regexp.MustCompile(`^[A-Za-z0-9\.\-:]+$`) + portPattern := regexp.MustCompile(`^[0-9]+$`) for _, ref := range gitRefs { u, err := url.Parse(ref.URL) if err != nil || u.Scheme != "ssh" { continue } host := u.Hostname() - port := u.Port() + if host == "" || !hostPattern.MatchString(host) { + continue + } + port := u.Port() + if port != "" && !portPattern.MatchString(port) { + continue + } key := host + ":" + port if seenHosts[key] { continue diff --git a/go/core/internal/controller/translator/agent/git_skills_test.go b/go/core/internal/controller/translator/agent/git_skills_test.go index 5cd7cdcc7..d22d941ff 100644 --- a/go/core/internal/controller/translator/agent/git_skills_test.go +++ b/go/core/internal/controller/translator/agent/git_skills_test.go @@ -3,6 +3,7 @@ package agent_test import ( "context" "testing" + "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -412,7 +413,8 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { require.NotNil(t, skillsInitContainer) script := skillsInitContainer.Command[2] for _, host := range tt.wantSSHKeyscanHosts { - assert.Contains(t, script, host, "script should ssh-keyscan custom host %q", host) + expected := fmt.Sprintf("ssh-keyscan %s", host) + assert.Contains(t, script, expected, "script should ssh-keyscan custom host %q", host) } } diff --git a/go/core/internal/controller/translator/agent/skills-init.sh.tmpl b/go/core/internal/controller/translator/agent/skills-init.sh.tmpl index 23bee2ddb..5380ad074 100644 --- a/go/core/internal/controller/translator/agent/skills-init.sh.tmpl +++ b/go/core/internal/controller/translator/agent/skills-init.sh.tmpl @@ -10,7 +10,19 @@ if [ -f "${_auth_mount}/ssh-privatekey" ]; then chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts {{- range .SSHHosts }} - ssh-keyscan{{ if .Port }} -p {{ .Port }}{{ end }} {{ .Host }} >> ~/.ssh/known_hosts + _ssh_host="$(cat <<'ENDVAL' +{{ .Host }} +ENDVAL +)" + {{- if .Port }} + _ssh_port="$(cat <<'ENDVAL' +{{ .Port }} +ENDVAL + )" + ssh-keyscan -p "$_ssh_port" "$_ssh_host" >> ~/.ssh/known_hosts + {{- else }} + ssh-keyscan "$_ssh_host" >> ~/.ssh/known_hosts + {{- end }} {{- end }} elif [ -f "${_auth_mount}/token" ]; then git config --global credential.helper "!f() { echo username=x-access-token; echo password=\$(cat ${_auth_mount}/token); }; f" From 62424d3fc3a8196b32372428f06f7b61f620379a Mon Sep 17 00:00:00 2001 From: Jesus Munoz Date: Thu, 19 Mar 2026 13:26:05 +0100 Subject: [PATCH 4/6] fix failing test Signed-off-by: Jesus Munoz --- .../translator/agent/adk_api_translator.go | 16 ++++++++-------- .../translator/agent/git_skills_test.go | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index c4377c933..cbf5534a1 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -1660,20 +1660,20 @@ func prepareSkillsInitData( data.AuthMountPath = "/git-auth" seenHosts := make(map[string]bool) hostPattern := regexp.MustCompile(`^[A-Za-z0-9\.\-:]+$`) - portPattern := regexp.MustCompile(`^[0-9]+$`) + portPattern := regexp.MustCompile(`^[0-9]+$`) for _, ref := range gitRefs { u, err := url.Parse(ref.URL) if err != nil || u.Scheme != "ssh" { continue } host := u.Hostname() - if host == "" || !hostPattern.MatchString(host) { - continue - } - port := u.Port() - if port != "" && !portPattern.MatchString(port) { - continue - } + if host == "" || !hostPattern.MatchString(host) { + continue + } + port := u.Port() + if port != "" && !portPattern.MatchString(port) { + continue + } key := host + ":" + port if seenHosts[key] { continue diff --git a/go/core/internal/controller/translator/agent/git_skills_test.go b/go/core/internal/controller/translator/agent/git_skills_test.go index d22d941ff..a9205f87a 100644 --- a/go/core/internal/controller/translator/agent/git_skills_test.go +++ b/go/core/internal/controller/translator/agent/git_skills_test.go @@ -2,8 +2,8 @@ package agent_test import ( "context" - "testing" "fmt" + "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -388,7 +388,7 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { for _, v := range deployment.Spec.Template.Spec.Volumes { if v.Secret != nil && v.Name == "git-auth" { hasAuthVolume = true - assert.Equal(t, "github-token", v.Secret.SecretName, "auth volume should reference the correct secret") + assert.Equal(t, tt.agent.Spec.Skills.GitAuthSecretRef.Name, v.Secret.SecretName, "auth volume should reference the correct secret") } } assert.True(t, hasAuthVolume, "git-auth volume should exist") @@ -414,7 +414,7 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { script := skillsInitContainer.Command[2] for _, host := range tt.wantSSHKeyscanHosts { expected := fmt.Sprintf("ssh-keyscan %s", host) - assert.Contains(t, script, expected, "script should ssh-keyscan custom host %q", host) + assert.Contains(t, script, expected, "script should ssh-keyscan custom host %q", host) } } From 2771cdce42118fdbd0c8b56bf91c20ede862eba1 Mon Sep 17 00:00:00 2001 From: Jesus Munoz Date: Thu, 19 Mar 2026 13:40:38 +0100 Subject: [PATCH 5/6] fix failing test Signed-off-by: Jesus Munoz --- .../internal/controller/translator/agent/adk_api_translator.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index cbf5534a1..d14de599c 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -1671,6 +1671,9 @@ func prepareSkillsInitData( continue } port := u.Port() + if port == "22" { + port = "" // 22 is the SSH default; omit to avoid -p flag + } if port != "" && !portPattern.MatchString(port) { continue } From 8834d1d5ea1d5afcd69dd43fac80a5d804e4c3fe Mon Sep 17 00:00:00 2001 From: Jesus Munoz Date: Thu, 19 Mar 2026 13:59:28 +0100 Subject: [PATCH 6/6] The host and port values are validated with strict regex Signed-off-by: Jesus Munoz --- .../controller/translator/agent/skills-init.sh.tmpl | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/go/core/internal/controller/translator/agent/skills-init.sh.tmpl b/go/core/internal/controller/translator/agent/skills-init.sh.tmpl index 5380ad074..9cac78b5f 100644 --- a/go/core/internal/controller/translator/agent/skills-init.sh.tmpl +++ b/go/core/internal/controller/translator/agent/skills-init.sh.tmpl @@ -10,18 +10,10 @@ if [ -f "${_auth_mount}/ssh-privatekey" ]; then chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts {{- range .SSHHosts }} - _ssh_host="$(cat <<'ENDVAL' -{{ .Host }} -ENDVAL -)" {{- if .Port }} - _ssh_port="$(cat <<'ENDVAL' -{{ .Port }} -ENDVAL - )" - ssh-keyscan -p "$_ssh_port" "$_ssh_host" >> ~/.ssh/known_hosts + ssh-keyscan -p {{ .Port }} {{ .Host }} >> ~/.ssh/known_hosts {{- else }} - ssh-keyscan "$_ssh_host" >> ~/.ssh/known_hosts + ssh-keyscan {{ .Host }} >> ~/.ssh/known_hosts {{- end }} {{- end }} elif [ -f "${_auth_mount}/token" ]; then