feat: agent deploys — passwordless ssh key (alarm/desk/blanc-nas + gitea), write-scope gitea token, secrets mounts, gitconfig, APPEND_SYSTEM deploy guide
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+23
-10
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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/<name>`
|
||||
(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/<owner>/<repo>.git
|
||||
git -c http.extraHeader="Authorization: token $LVMH_GITEA_TOKEN" push origin <branch>
|
||||
```
|
||||
|
||||
(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":"<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.
|
||||
@@ -0,0 +1,7 @@
|
||||
[user]
|
||||
name = buenosair
|
||||
email = x3nx@hotmail.fr
|
||||
[commit]
|
||||
gpgsign = false
|
||||
[tag]
|
||||
gpgsign = false
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user