From b35eca40c3b1013a21ee35e6832568afca367446 Mon Sep 17 00:00:00 2001 From: Raphael Westphal Date: Thu, 20 Aug 2026 09:42:58 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20agent=20deploy=20pipeline=20=E2=80=94?= =?UTF-8?q?=20passwordless=20SSH=20(alarm/desk/blanc-nas=20via=20tailnet?= =?UTF-8?q?=20IPs=20in=20ssh=20config),=20gitea=20write=20token=20to=20wor?= =?UTF-8?q?kers,=20secrets=20mounts,=20gitconfig,=20APPEND=5FSYSTEM=20guid?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- daemon/docker.go | 46 +++++++++++++++++++++++++++++------------- daemon/spawner_test.go | 1 + docker/ssh-config | 9 +++++++++ docker/ssh-known_hosts | 14 ++++++------- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/daemon/docker.go b/daemon/docker.go index e3efae2..37c707b 100644 --- a/daemon/docker.go +++ b/daemon/docker.go @@ -353,6 +353,36 @@ func (s *Spawner) cloneOrUpdate(ctx context.Context, repo, branch, slug string) return nil } +// giteaTokenEnv returns LVMH_GITEA_TOKEN= when a PAT is stored. +func giteaTokenEnv(store *Store) string { + if pat, ok, _ := store.GetSetting(settingGitLabToken); ok && pat != "" { + return "LVMH_GITEA_TOKEN=" + pat + } + return "" +} + +// workerEnv assembles the env for a spawned worker container. +func workerEnv(s *Spawner, repo, sessionID string) []string { + env := []string{ + envProviderAPIKey + "=" + os.Getenv(envProviderAPIKey), + envToken + "=" + daemonToken, + "LVMH_URL=" + s.containerURL, + envLVMHSessionID + "=" + sessionID, + envLVMHAgent + "=1", + // other provider keys (empty ones are harmless) + "OPENAI_API_KEY=" + os.Getenv("OPENAI_API_KEY"), + "GEMINI_API_KEY=" + os.Getenv("GEMINI_API_KEY"), + "DEEPSEEK_KEY=" + os.Getenv("DEEPSEEK_KEY"), + "ANTHROPIC_API_KEY=" + os.Getenv("ANTHROPIC_API_KEY"), + envLVMHRepo + "=" + repo, + } + // Gitea token (write scope) so agents can push and open PRs. + if e := giteaTokenEnv(s.store); e != "" { + env = append(env, e) + } + return env +} + // cloneURL builds the clean https clone URL (never credential-bearing). func (s *Spawner) cloneURL(repo string) (string, error) { u, err := url.Parse(s.baseURL + "/" + repo + ".git") @@ -536,20 +566,8 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID stri ) } cfg := &container.Config{ - Image: image, - Env: []string{ - envProviderAPIKey + "=" + os.Getenv(envProviderAPIKey), - envToken + "=" + daemonToken, - "LVMH_URL=" + s.containerURL, - envLVMHSessionID + "=" + sessionID, - envLVMHAgent + "=1", - // other provider keys (empty ones are harmless) - "OPENAI_API_KEY=" + os.Getenv("OPENAI_API_KEY"), - "GEMINI_API_KEY=" + os.Getenv("GEMINI_API_KEY"), - "DEEPSEEK_KEY=" + os.Getenv("DEEPSEEK_KEY"), - "ANTHROPIC_API_KEY=" + os.Getenv("ANTHROPIC_API_KEY"), - envLVMHRepo + "=" + repo, - }, + Image: image, + Env: workerEnv(s, repo, sessionID), Labels: map[string]string{labelSession: sessionID}, } hostCfg := &container.HostConfig{ diff --git a/daemon/spawner_test.go b/daemon/spawner_test.go index 8a4bbb6..fc39d83 100644 --- a/daemon/spawner_test.go +++ b/daemon/spawner_test.go @@ -72,6 +72,7 @@ func TestSpawnerStartHappyPath(t *testing.T) { passthrough := map[string]bool{ "OPENAI_API_KEY": true, "GEMINI_API_KEY": true, "DEEPSEEK_KEY": true, "ANTHROPIC_API_KEY": true, + "LVMH_GITEA_TOKEN": true, } for _, e := range c.Env { if name, _, ok := strings.Cut(e, "="); ok && passthrough[name] { diff --git a/docker/ssh-config b/docker/ssh-config index aa7fcc1..1c76373 100644 --- a/docker/ssh-config +++ b/docker/ssh-config @@ -1,3 +1,12 @@ +Host alarm + HostName 100.64.84.69 + User alarm +Host desk + HostName 100.73.57.3 + User raph +Host blanc-nas + HostName 100.91.19.107 + User raph Host alarm desk blanc-nas IdentityFile /root/.ssh/id_ed25519 IdentitiesOnly yes diff --git a/docker/ssh-known_hosts b/docker/ssh-known_hosts index 1df92f4..a80eeef 100644 --- a/docker/ssh-known_hosts +++ b/docker/ssh-known_hosts @@ -1,8 +1,6 @@ -# blanc-nas:22 SSH-2.0-OpenSSH_10.4 -blanc-nas ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7CmHNP4LxohSCJY1acgMqPPb0hvjlbDzzvf1oqPLcaGs+KsRL2+OfMbNHOMBz4XfM5WN5DANOCuOBJBtn1vyAu83JXLx0XTl6pEnmaBm6Wjs05zmI1fliGJHgbPsPCz8Y+kRNY1Z01VWWsanr7Yu9LStcyKY1v2i2DqQ9i3WdTzWyE9Tkt3540/49LFVjkRR6lGeh2G0/ppM3utM2Rg/CGMjbmYV9LBtU5CTtrEWiGLyDwxuXNyk6BcQkxOHZf76W05/FwvHRv15mwF0RxkDitmfzWMEQ/+paUcDMRgLKF1j2mBtR2UdR3z7m4q46PiXKAjyJAG7tIfJqwr1jSRYXn6+bEV5tK6VJkc2+0viMoQYrnBBxL676rShECG27Ib8loUk2PuhXUSVWiLY+/BvRyXETIZxgoF53LCO5Z0Wh91Dp2OMfO0EajfRc83FfqOFgwhGpVk27r+LcLjYzqz5BUIy58HtSpZEQRr1KJuagZFfnLDRtY9ovn3uBDQhpX0k= -# blanc-nas:22 SSH-2.0-OpenSSH_10.4 -blanc-nas ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFB7p37Rxw6tW2rUpoECwtXLBnU179CxiwTNtwJQs7Sp -# alarm:22 SSH-2.0-OpenSSH_10.4 -alarm ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwjQpOAwCL0AIGeJzI99CoOFTxUFUd2+DCveZKeBfCpdyZAtrPTd5310VjyAH0HWXd3c4fZvgPcrjSzzbygpSroZD9zQh6zadp+yCXZNoLrc2iT8CkSngw24QpvOfdkunJiwnRjKLflYrCvzu0BHAoDKHq8XjpTpwfN0BH/eoIIqcoDnmF9KrTUQRbEzwg+xc1T41ByjIalqHfrdNxV0S8xv+sQ7/YvGmi4YVZJ5ElqEEgGtyBDJE3WuveH5sA+YkdoMMEm9Xf+xlgO6uwhaYAKbG29Rkq9Ot8N7NSf3mu00XVK5wHvHLYqFBhA24Pw5fuDp0ohbhHKIbpumS5GP8Ow3eR/bLIesSGkaPnB4o6t8HiQ3TKfdCO8UAvH85j6gXDiPmFXybOmxQMB+oWo+zdOYa8XVryHB3CJ+4yBmJU03dzNwpV+93uVoW9jmjElRwZx9c+gqSBUWgWlaVF1AFdVT2swzKIBTJg2ZXjoqLphuWfnewhPKPt48Fr5B0OAYk= -# alarm:22 SSH-2.0-OpenSSH_10.4 -alarm ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGIoBFYAfkN1Q8XsjwsJZ2mLIQi9XQ9M03zEENW8Injq +# 100.64.84.69:22 SSH-2.0-OpenSSH_10.4 +100.64.84.69 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGIoBFYAfkN1Q8XsjwsJZ2mLIQi9XQ9M03zEENW8Injq +# 100.73.57.3:22 SSH-2.0-OpenSSH_10.4 +100.73.57.3 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN5kwgNZ/IYLpAxVtezTxYLcy7MmP9lKnrKX0XeBiCIn +# 100.91.19.107:22 SSH-2.0-OpenSSH_10.4 +100.91.19.107 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFB7p37Rxw6tW2rUpoECwtXLBnU179CxiwTNtwJQs7Sp