From 6d10877e34ee8c10973ecb3b0cf44a1a1d8218fd Mon Sep 17 00:00:00 2001 From: Raphael Westphal Date: Thu, 20 Aug 2026 09:34:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20agent=20deploys=20=E2=80=94=20passwordl?= =?UTF-8?q?ess=20ssh=20key=20(alarm/desk/blanc-nas=20+=20gitea),=20write-s?= =?UTF-8?q?cope=20gitea=20token,=20secrets=20mounts,=20gitconfig,=20APPEND?= =?UTF-8?q?=5FSYSTEM=20deploy=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../console-2026-08-20T07-24-35-594Z.log | 1 + .../page-2026-08-20T07-24-35-840Z.yml | 63 ++++++++ .../page-2026-08-20T07-29-57-455Z.yml | 139 ++++++++++++++++++ daemon/docker.go | 33 +++-- daemon/docker_test.go | 31 ++++ deploy/rsync-pi-agent.sh | 7 + docker-compose.yml | 1 + docker/agent-append-system.md | 52 +++++++ docker/gitconfig | 7 + docker/ssh-config | 6 + docker/ssh-known_hosts | 8 + 11 files changed, 338 insertions(+), 10 deletions(-) create mode 100644 .playwright-mcp/console-2026-08-20T07-24-35-594Z.log create mode 100644 .playwright-mcp/page-2026-08-20T07-24-35-840Z.yml create mode 100644 .playwright-mcp/page-2026-08-20T07-29-57-455Z.yml create mode 100644 docker/agent-append-system.md create mode 100644 docker/gitconfig create mode 100644 docker/ssh-config create mode 100644 docker/ssh-known_hosts diff --git a/.playwright-mcp/console-2026-08-20T07-24-35-594Z.log b/.playwright-mcp/console-2026-08-20T07-24-35-594Z.log new file mode 100644 index 0000000..fdee8ba --- /dev/null +++ b/.playwright-mcp/console-2026-08-20T07-24-35-594Z.log @@ -0,0 +1 @@ +[ 214ms] [VERBOSE] [DOM] Input elements should have autocomplete attributes (suggested: "username"): (More info: https://goo.gl.qjz9zk/9p2vKq) %o @ https://git.westphal.fr/user/login?redirect_to=%2Fuser%2Fsettings%2Fkeys:0 diff --git a/.playwright-mcp/page-2026-08-20T07-24-35-840Z.yml b/.playwright-mcp/page-2026-08-20T07-24-35-840Z.yml new file mode 100644 index 0000000..443f52d --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T07-24-35-840Z.yml @@ -0,0 +1,63 @@ +- generic [ref=e1]: + - generic [ref=e2]: + - navigation "Navigation Bar" [ref=e3]: + - generic [ref=e4]: + - link "Home" [ref=e5] [cursor=pointer]: + - /url: / + - img [ref=e6] + - link "Explore" [ref=e7] [cursor=pointer]: + - /url: /explore/repos + - link "Help" [ref=e8] [cursor=pointer]: + - /url: https://docs.gitea.com + - link "Sign In" [ref=e10] [cursor=pointer]: + - /url: /user/login + - img [ref=e11] + - generic [ref=e13]: Sign In + - main "Sign In" [ref=e14]: + - generic [ref=e16]: + - generic [ref=e17]: + - heading "Sign In" [level=4] [ref=e18] + - generic [ref=e19]: + - generic [ref=e20]: + - generic [ref=e21]: + - generic [ref=e22]: Username or Email Address * + - textbox "Username or Email Address *" [active] [ref=e23] + - generic [ref=e24]: + - generic [ref=e25]: + - generic [ref=e26]: Password + - link "Forgot password?" [ref=e27] [cursor=pointer]: + - /url: /user/forgot_password + - textbox "Password" [ref=e28] + - generic [ref=e30]: + - generic [ref=e31]: Remember This Device + - checkbox "Remember This Device" [ref=e32] + - button "Sign In" [ref=e34] [cursor=pointer] + - generic [ref=e35]: or + - link "Sign in with OpenID" [ref=e37] [cursor=pointer]: + - /url: /user/login/openid + - img [ref=e38] + - text: Sign in with OpenID + - generic [ref=e42] [cursor=pointer]: Sign in with a passkey + - group "Footer" [ref=e43]: + - contentinfo "About Software" [ref=e44]: + - link "Powered by Gitea" [ref=e45] [cursor=pointer]: + - /url: https://about.gitea.com + - generic [ref=e46]: "Version: 1.26.4" + - generic [ref=e47]: + - text: "Page:" + - strong [ref=e48]: 2ms + - text: "Template:" + - strong [ref=e49]: 0ms + - group "Links" [ref=e50]: + - menu [ref=e51] [cursor=pointer]: + - generic [ref=e53]: + - img [ref=e54] + - text: Auto + - menu [ref=e56] [cursor=pointer]: + - generic [ref=e57]: + - img [ref=e58] + - text: English + - link "Licenses" [ref=e60] [cursor=pointer]: + - /url: /assets/licenses.txt + - link "API" [ref=e61] [cursor=pointer]: + - /url: /api/swagger \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-20T07-29-57-455Z.yml b/.playwright-mcp/page-2026-08-20T07-29-57-455Z.yml new file mode 100644 index 0000000..83c7300 --- /dev/null +++ b/.playwright-mcp/page-2026-08-20T07-29-57-455Z.yml @@ -0,0 +1,139 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - navigation "Navigation Bar" [ref=e3]: + - generic [ref=e4]: + - link "Dashboard" [ref=e5] [cursor=pointer]: + - /url: / + - img [ref=e6] + - link "Issues" [ref=e7] [cursor=pointer]: + - /url: /issues + - link "Pull Requests" [ref=e8] [cursor=pointer]: + - /url: /pulls + - link "Milestones" [ref=e9] [cursor=pointer]: + - /url: /milestones + - link "Explore" [ref=e10] [cursor=pointer]: + - /url: /explore/repos + - generic [ref=e11]: + - link "Notifications" [ref=e12] [cursor=pointer]: + - /url: /notifications + - img [ref=e14] + - menu "Create…" [ref=e16] [cursor=pointer]: + - generic [ref=e17]: + - img [ref=e18] + - img [ref=e21] + - menu "Profile and Settings…" [ref=e23] [cursor=pointer]: + - generic [ref=e24]: + - generic [ref=e25]: + - img "buenosair" [ref=e26] + - img [ref=e27] + - img [ref=e30] + - main "Applications" [ref=e32]: + - generic [ref=e33]: + - generic [ref=e35]: + - generic [ref=e36]: User Settings + - link "Profile" [ref=e37] [cursor=pointer]: + - /url: /user/settings + - link "Account" [ref=e38] [cursor=pointer]: + - /url: /user/settings/account + - link "Appearance" [ref=e39] [cursor=pointer]: + - /url: /user/settings/appearance + - link "Security" [ref=e40] [cursor=pointer]: + - /url: /user/settings/security + - link "Blocked users" [ref=e41] [cursor=pointer]: + - /url: /user/settings/blocked_users + - link "Applications" [ref=e42] [cursor=pointer]: + - /url: /user/settings/applications + - link "SSH / GPG Keys" [ref=e43] [cursor=pointer]: + - /url: /user/settings/keys + - group [ref=e44]: + - generic "Actions" [ref=e45] [cursor=pointer] + - link "Packages" [ref=e46] [cursor=pointer]: + - /url: /user/settings/packages + - link "Webhooks" [ref=e47] [cursor=pointer]: + - /url: /user/settings/hooks + - link "Organizations" [ref=e48] [cursor=pointer]: + - /url: /user/settings/organization + - link "Repositories" [ref=e49] [cursor=pointer]: + - /url: /user/settings/repos + - generic [ref=e51]: + - heading "Manage Access Tokens" [level=4] [ref=e52] + - generic [ref=e54]: + - generic [ref=e55]: These tokens grant access to your account using the Gitea API. + - generic [ref=e56]: + - generic "This token has been used in the last 7 days" [ref=e58]: + - img [ref=e59] + - generic [ref=e61]: + - group [ref=e62]: + - generic "lvmh" [ref=e63] [cursor=pointer]: + - generic [ref=e64]: lvmh + - generic [ref=e66]: + - text: Added on 2026-08-18Aug 18, 2026 — + - img [ref=e67] + - text: Last used on + - generic [ref=e69]: 2026-08-20Aug 20, 2026 + - button "Delete" [ref=e71] [cursor=pointer]: + - img [ref=e72] + - text: Delete + - generic [ref=e74]: + - img [ref=e77] + - generic [ref=e79]: + - group [ref=e80]: + - generic "hermes-pr-creator" [ref=e81] [cursor=pointer]: + - generic [ref=e82]: hermes-pr-creator + - generic [ref=e84]: + - text: Added on 2026-05-03May 3, 2026 — + - img [ref=e85] + - text: Last used on + - generic [ref=e87]: 2026-05-03May 3, 2026 + - button "Delete" [ref=e89] [cursor=pointer]: + - img [ref=e90] + - text: Delete + - generic [ref=e92]: + - img [ref=e95] + - generic [ref=e97]: + - group [ref=e98]: + - generic "hermes-agent-pr" [ref=e99] [cursor=pointer]: + - generic [ref=e100]: hermes-agent-pr + - generic [ref=e102]: + - text: Added on 2026-05-03May 3, 2026 — + - img [ref=e103] + - text: No recent activity + - button "Delete" [ref=e106] [cursor=pointer]: + - img [ref=e107] + - text: Delete + - group [ref=e110]: + - generic "Generate New Token" [ref=e111] [cursor=pointer]: + - heading "Generate New Token" [level=4] [ref=e112] + - heading "Authorized OAuth2 Applications" [level=4] [ref=e113] + - generic [ref=e116]: You have granted access to your personal Gitea account to these third-party applications. Please revoke access for applications you no longer need. + - heading "Manage OAuth2 Applications" [level=4] [ref=e117] + - generic [ref=e120]: OAuth2 applications give your third-party application access to user accounts on this instance. + - group [ref=e122]: + - generic "Create a new OAuth2 Application" [ref=e123] [cursor=pointer]: + - heading "Create a new OAuth2 Application" [level=4] [ref=e124] + - group "Footer" [ref=e125]: + - contentinfo "About Software" [ref=e126]: + - link "Powered by Gitea" [ref=e127] [cursor=pointer]: + - /url: https://about.gitea.com + - generic [ref=e128]: + - text: "Version:" + - link "1.26.4" [ref=e129] [cursor=pointer]: + - /url: /-/admin/config + - generic [ref=e130]: + - text: "Page:" + - strong [ref=e131]: 13ms + - text: "Template:" + - strong [ref=e132]: 3ms + - group "Links" [ref=e133]: + - menu [ref=e134] [cursor=pointer]: + - generic [ref=e136]: + - img [ref=e137] + - text: Light + - menu [ref=e139] [cursor=pointer]: + - generic [ref=e140]: + - img [ref=e141] + - text: English + - link "Licenses" [ref=e143] [cursor=pointer]: + - /url: /assets/licenses.txt + - link "API" [ref=e144] [cursor=pointer]: + - /url: /api/swagger \ No newline at end of file diff --git a/daemon/docker.go b/daemon/docker.go index 604777a..e3efae2 100644 --- a/daemon/docker.go +++ b/daemon/docker.go @@ -40,16 +40,19 @@ const ( stateRunning string = "running" stateError string = "error" - imageRefWorker string = "lvmh-worker:latest" - labelSession string = "lvmh.session" - volumeRepoPrefix string = "lvmh-repo-" - volumeSessions string = "lvmh-sessions" - volumePiCache string = "lvmh-pi-cache" // pi package cache (git:/npm:), shared across spawns - cacheMount string = "/root/.pi/agent/cache" - authMountTarget string = "/root/.pi/agent/auth.json" - envHostPiAgentDir string = "LVMH_HOST_PI_AGENT_DIR" - workspaceMount string = "/workspace" - sessionsMount string = "/pi-sessions" + imageRefWorker string = "lvmh-worker:latest" + labelSession string = "lvmh.session" + volumeRepoPrefix string = "lvmh-repo-" + volumeSessions string = "lvmh-sessions" + volumePiCache string = "lvmh-pi-cache" // pi package cache (git:/npm:), shared across spawns + cacheMount string = "/root/.pi/agent/cache" + authMountTarget string = "/root/.pi/agent/auth.json" + envHostPiAgentDir string = "LVMH_HOST_PI_AGENT_DIR" + envSecretsDir string = "LVMH_SECRETS_DIR" + sshMountTarget string = "/root/.ssh" + gitconfigMountTarget string = "/root/.gitconfig" + workspaceMount string = "/workspace" + sessionsMount string = "/pi-sessions" envWorkerDockerfile string = "LVMH_WORKER_DOCKERFILE" defaultDockerfile string = "/app/build/docker/worker.Dockerfile" @@ -522,6 +525,16 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, sessionID stri if hostAgent := os.Getenv(envHostPiAgentDir); hostAgent != "" { binds = append(binds, hostAgent+"/auth.json:"+authMountTarget+":ro") } + // Deploy secrets (ssh key + known_hosts + config, gitconfig), read-only. + // Bind sources resolve on the HOST (docker.sock semantics), so this env + // must carry the host path of the secrets dir. + if sec := os.Getenv(envSecretsDir); sec != "" { + binds = append( + binds, + sec+":"+sshMountTarget+":ro", + sec+"/gitconfig:"+gitconfigMountTarget+":ro", + ) + } cfg := &container.Config{ Image: image, Env: []string{ diff --git a/daemon/docker_test.go b/daemon/docker_test.go index 913ca3b..480868c 100644 --- a/daemon/docker_test.go +++ b/daemon/docker_test.go @@ -487,3 +487,34 @@ func waitJobState(t *testing.T, sp *Spawner, sessionID, want string) SpawnJob { }) return job } + +func TestSpawnerSecretsBinds(t *testing.T) { + useFakeGit(t, fakeGitModeOK) + f := newFakeDocker() + sp, _ := newTestSpawner(t, f) + sec := t.TempDir() + t.Setenv(envSecretsDir, sec) + + res, err := sp.Start(context.Background(), "group/project", "") + if err != nil { + t.Fatalf("Start: %v", err) + } + waitJobState(t, sp, res.SessionID, stateRunning) + creates := f.createsByName("lvmh-agent-") + if len(creates) != 1 { + t.Fatalf("creates = %d", len(creates)) + } + binds := creates[0].HostConfig.Binds + ssh, gc := false, false + for _, b := range binds { + if b == sec+":/root/.ssh:ro" { + ssh = true + } + if b == sec+"/gitconfig:/root/.gitconfig:ro" { + gc = true + } + } + if !ssh || !gc { + t.Fatalf("secrets binds missing: %v", binds) + } +} diff --git a/deploy/rsync-pi-agent.sh b/deploy/rsync-pi-agent.sh index 3336317..ccad2cc 100755 --- a/deploy/rsync-pi-agent.sh +++ b/deploy/rsync-pi-agent.sh @@ -70,4 +70,11 @@ for pkg in settings.get("packages") or []: ) PY +# Append the lvmh deployment guide to the baked APPEND_SYSTEM.md so every +# spawned agent knows how to deploy hosts / push / open PRs. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if [ -f "$SCRIPT_DIR/../docker/agent-append-system.md" ]; then + printf '\n%s\n' "$(cat "$SCRIPT_DIR/../docker/agent-append-system.md")" >> "$DEST/APPEND_SYSTEM.md" +fi + echo "rsync-pi-agent: synced $(find "$DEST" -type f | wc -l) files from $SRC (git: packages preserved)" diff --git a/docker-compose.yml b/docker-compose.yml index d26020a..68237a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: # docker.sock bind semantics: bind sources resolve on the HOST, so # this must be the HOST path of the pi config (auth.json etc.). LVMH_HOST_PI_AGENT_DIR: ${LVMH_PI_AGENT_DIR:-/home/alarm/.dotfiles/pi/agent} + LVMH_SECRETS_DIR: /zdata/root/lvmh-secrets OPENAI_API_KEY: ${OPENAI_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} DEEPSEEK_KEY: ${DEEPSEEK_KEY:-} diff --git a/docker/agent-append-system.md b/docker/agent-append-system.md new file mode 100644 index 0000000..1971be3 --- /dev/null +++ b/docker/agent-append-system.md @@ -0,0 +1,52 @@ +# lvmh agent deployment guide + +You are running inside an lvmh worker container with deployment capabilities. + +## Deploy hosts (SSH, passwordless key at /root/.ssh/id_ed25519) + +- `alarm` — the main server. Services live in `/zdata/root/dockerFiles/` + (docker compose). lvmh itself: `/zdata/root/dockerFiles/lvmh`. +- `desk` — desktop machine. +- `blanc-nas` — NAS. + +SSH works directly: `ssh alarm 'sudo docker compose -f /zdata/root/dockerFiles/empstream/docker-compose.yml up -d --build'`. +The key is also registered in Gitea (but Gitea has no SSH port — use HTTPS+token for git, see below). + +## Git pushes and pull requests + +Your Gitea token is in `$LVMH_GITEA_TOKEN` (scopes: write repository + user). +Gitea: `https://git.westphal.fr` (user `buenosair`). + +Clone/push with the token — NEVER put it in a URL that gets written to disk: + +``` +git -c http.extraHeader="Authorization: token $LVMH_GITEA_TOKEN" clone https://git.westphal.fr//.git +git -c http.extraHeader="Authorization: token $LVMH_GITEA_TOKEN" push origin +``` + +(For pulls from an existing clone, the same `-c` flag works with `pull`/`fetch`/`push`.) + +Create a pull request: + +``` +curl -s -X POST -H "Authorization: token $LVMH_GITEA_TOKEN" -H 'Content-Type: application/json' \ + -d '{"title":"","head":"<branch>","base":"<base>","body":"<description>"}' \ + https://git.westphal.fr/api/v1/repos/<owner>/<repo>/pulls +``` + +Comment, list PRs, merge — same `/api/v1/repos/<owner>/<repo>/pulls` resource family. + +## Known repo deploy targets + +- `buenosair/ezrpc`, `buenosair/empstream`, `buenosair/mev` — docker-compose services on + `alarm` under `/zdata/root/dockerFiles/`. Typical flow: push branch/merge to main → + `ssh alarm` → `git -C /zdata/root/dockerFiles/<dir> pull` (or clone if missing) → + `sudo docker compose up -d --build`. +- Check what's running: `ssh alarm 'sudo docker ps'`. + +## Rules + +- Never print or commit `$LVMH_GITEA_TOKEN` or the contents of `/root/.ssh/id_ed25519`. +- Prefer PRs for non-trivial changes; ask in chat before force-pushing or touching main. +- On deploy hosts, `sudo` is available for this key where needed; use it only for + service management (docker/systemctl), not for arbitrary system changes. diff --git a/docker/gitconfig b/docker/gitconfig new file mode 100644 index 0000000..8de1753 --- /dev/null +++ b/docker/gitconfig @@ -0,0 +1,7 @@ +[user] + name = buenosair + email = x3nx@hotmail.fr +[commit] + gpgsign = false +[tag] + gpgsign = false diff --git a/docker/ssh-config b/docker/ssh-config new file mode 100644 index 0000000..aa7fcc1 --- /dev/null +++ b/docker/ssh-config @@ -0,0 +1,6 @@ +Host alarm desk blanc-nas + IdentityFile /root/.ssh/id_ed25519 + IdentitiesOnly yes + StrictHostKeyChecking yes + UserKnownHostsFile /root/.ssh/known_hosts + ConnectTimeout 8 diff --git a/docker/ssh-known_hosts b/docker/ssh-known_hosts new file mode 100644 index 0000000..1df92f4 --- /dev/null +++ b/docker/ssh-known_hosts @@ -0,0 +1,8 @@ +# 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