Wire up ssh node1 … in the agent workflow (read-only swarm diagnostics) #10

Closed
opened 2026-07-03 11:07:46 +02:00 by gitea-actions · 7 comments

Context

In ffaerber/homelab we're giving the AI agent read-only SSH diagnostics on node1 the simple way (via opencode permissions) — see homelab PR #96 / issue #95.

The homelab repo now ships a project-level opencode.json whose permission.bash block denies ssh * and re-allows only a strict read-only allowlist (docker service ls/ps/inspect/logs, docker ps, docker stack ls/ps/services, docker node ls/ps/inspect, docker inspect/version/info, uptime/df/free/hostname). Last-match-wins, so ssh node1 docker service ls is allowed while shells / deploys / state-changing commands stay denied.

That allowlist is inert until the agent's job actually has a working ssh node1 … command. The host/user/key wiring lives here in ffaerber/agents (the shared agent workflow), which is out of scope for the homelab repo. This issue tracks the changes needed on this side.

Changes needed in ffaerber/agents

In the shared agent workflow (.gitea/workflows/agent.yml), before opencode run executes, set up an SSH client so that ssh node1 <cmd> resolves and authenticates using the existing deploy secrets that are already available in every repo:

  • secrets.SWARM_HOST
  • secrets.SWARM_USER
  • secrets.SSH_PRIV_KEY

Concretely:

  1. Write the private key to a file with 600 perms, e.g. ~/.ssh/agent_node1 from ${{ secrets.SSH_PRIV_KEY }}.
  2. Add an SSH config alias node1 in ~/.ssh/config so the agent can literally run ssh node1 docker service ls (matching the homelab allowlist patterns exactly):
    Host node1
        HostName ${{ secrets.SWARM_HOST }}
        User ${{ secrets.SWARM_USER }}
        IdentityFile ~/.ssh/agent_node1
        IdentitiesOnly yes
        StrictHostKeyChecking accept-new
        ConnectTimeout 10
    
    (Using an alias keeps the opencode allowlist patterns simple/predictable — ssh node1 … — rather than depending on long inline -i/-o flag strings.)
  3. Ensure ~/.ssh is 700 and ~/.ssh/config is 600.

Important: keep the restriction meaningful

The read-only restriction is enforced only by the homelab opencode.json permission allowlist. For that to be a real safety boundary:

  • The agent must run with permissions enforced (i.e. the project-level opencode.json from the target repo is honored, and dangerous commands are actually denyed). Please confirm the invocation doesn't bypass the target-repo opencode.json or blanket-approve bash.
  • Note under --auto, only deny rules block (an ask is auto-approved). The homelab config deliberately uses last-match-wins deny ssh * + specific allow …, which is compatible with --auto. Please don't add a global config that re-allows ssh * or bash * in a way that overrides the project config.

Acceptance

  • From an agent run in a repo that has SSH access, ssh node1 docker service ls succeeds and returns swarm state.
  • ssh node1 docker service rm <x> (and other non-allowlisted / state-changing commands) is denied by the opencode permission layer.
  • No new secret is required — this reuses SWARM_HOST / SWARM_USER / SSH_PRIV_KEY.

References


🤖 @lead (filed from ffaerber/homelab #95)

## Context In `ffaerber/homelab` we're giving the AI agent **read-only** SSH diagnostics on node1 the simple way (via [opencode permissions](https://opencode.ai/docs/permissions/)) — see homelab PR #96 / issue #95. The homelab repo now ships a project-level `opencode.json` whose `permission.bash` block denies `ssh *` and re-allows only a strict read-only allowlist (`docker service ls/ps/inspect/logs`, `docker ps`, `docker stack ls/ps/services`, `docker node ls/ps/inspect`, `docker inspect/version/info`, `uptime/df/free/hostname`). Last-match-wins, so `ssh node1 docker service ls` is allowed while shells / deploys / state-changing commands stay denied. **That allowlist is inert until the agent's job actually has a working `ssh node1 …` command.** The host/user/key wiring lives here in `ffaerber/agents` (the shared agent workflow), which is out of scope for the homelab repo. This issue tracks the changes needed on this side. ## Changes needed in `ffaerber/agents` In the shared agent workflow (`.gitea/workflows/agent.yml`), before `opencode run` executes, set up an SSH client so that `ssh node1 <cmd>` resolves and authenticates using the existing deploy secrets that are already available in every repo: - `secrets.SWARM_HOST` - `secrets.SWARM_USER` - `secrets.SSH_PRIV_KEY` Concretely: 1. **Write the private key** to a file with `600` perms, e.g. `~/.ssh/agent_node1` from `${{ secrets.SSH_PRIV_KEY }}`. 2. **Add an SSH config alias `node1`** in `~/.ssh/config` so the agent can literally run `ssh node1 docker service ls` (matching the homelab allowlist patterns exactly): ``` Host node1 HostName ${{ secrets.SWARM_HOST }} User ${{ secrets.SWARM_USER }} IdentityFile ~/.ssh/agent_node1 IdentitiesOnly yes StrictHostKeyChecking accept-new ConnectTimeout 10 ``` (Using an alias keeps the opencode allowlist patterns simple/predictable — `ssh node1 …` — rather than depending on long inline `-i/-o` flag strings.) 3. Ensure `~/.ssh` is `700` and `~/.ssh/config` is `600`. ## Important: keep the restriction meaningful The read-only restriction is enforced **only** by the homelab `opencode.json` permission allowlist. For that to be a real safety boundary: - The agent must run with permissions **enforced** (i.e. the project-level `opencode.json` from the target repo is honored, and dangerous commands are actually `deny`ed). Please confirm the invocation doesn't bypass the target-repo `opencode.json` or blanket-approve `bash`. - Note under `--auto`, only `deny` rules block (an `ask` is auto-approved). The homelab config deliberately uses last-match-wins `deny ssh *` + specific `allow …`, which is compatible with `--auto`. Please don't add a global config that re-allows `ssh *` or `bash *` in a way that overrides the project config. ## Acceptance - From an agent run in a repo that has SSH access, `ssh node1 docker service ls` succeeds and returns swarm state. - `ssh node1 docker service rm <x>` (and other non-allowlisted / state-changing commands) is denied by the opencode permission layer. - No new secret is required — this reuses `SWARM_HOST` / `SWARM_USER` / `SSH_PRIV_KEY`. ## References - homelab issue #95, PR #96 - opencode permissions: https://opencode.ai/docs/permissions/ --- 🤖 @lead (filed from `ffaerber/homelab` #95)
Owner

@pm go

@pm go

🤖 @pm

Confirmed: no existing SSH wiring in .gitea/workflows/agent.yml — this is a clean, additive change. The task is well-specified (the issue includes near-literal step content), so here's my plan.

Plan

Where: Add one new step to .gitea/workflows/agent.yml, right after "Install opencode + provider config" and before "Run agent" (so SSH is ready before opencode run executes, for every agent/mode — comment agents like @pm/@qa may also invoke bash via opencode's tool-use, so they should get the same alias).

What the new step does:

  1. mkdir -p ~/.ssh && chmod 700 ~/.ssh
  2. Write secrets.SSH_PRIV_KEY to ~/.ssh/agent_node1, chmod 600.
  3. Write ~/.ssh/config with the Host node1 block from the issue (using secrets.SWARM_HOST / secrets.SWARM_USER), chmod 600.
  4. Important addition beyond the literal issue text: since this workflow is shared by every repo (not all of which may define SWARM_HOST/SWARM_USER/SSH_PRIV_KEY), the step must no-op cleanly (not fail the job) when those secrets are empty — just skip writing the alias rather than erroring the whole run.
  5. No secret values get echoed/printed anywhere (matches the existing masking convention already used for OLLAMA_CLOUD_API_KEY etc.).

What I verified already (no change needed): the ~/.config/opencode/opencode.json written in the "Install opencode + provider config" step only sets provider and mcp — it has no permission block, so it does not and must not override the target repo's opencode.json. The implementer should keep it that way and explicitly confirm this in the PR description, per the issue's safety note.

Who builds it: @senior — it's a single-file, single-step change, but it handles private-key material and needs the "skip gracefully if secrets absent" conditional done correctly so it doesn't break the shared workflow for repos without swarm secrets. That's a bit more judgment than a pure YAML/compose tweak, so senior over junior.

Verification: Not a browser/UI task, so @qa's Playwright flow doesn't apply here. Real verification is: after merge, trigger an agent run in ffaerber/homelab and confirm ssh node1 docker service ls succeeds and ssh node1 docker service rm ... is denied — that's a manual check @ffaerber (or a follow-up comment on this issue) would do post-merge.

@ffaerber ready to start building? reply yes to proceed.

🤖 **@pm** Confirmed: no existing SSH wiring in `.gitea/workflows/agent.yml` — this is a clean, additive change. The task is well-specified (the issue includes near-literal step content), so here's my plan. ## Plan **Where:** Add one new step to `.gitea/workflows/agent.yml`, right after "Install opencode + provider config" and before "Run agent" (so SSH is ready before `opencode run` executes, for every agent/mode — comment agents like @pm/@qa may also invoke `bash` via opencode's tool-use, so they should get the same alias). **What the new step does:** 1. `mkdir -p ~/.ssh && chmod 700 ~/.ssh` 2. Write `secrets.SSH_PRIV_KEY` to `~/.ssh/agent_node1`, `chmod 600`. 3. Write `~/.ssh/config` with the `Host node1` block from the issue (using `secrets.SWARM_HOST` / `secrets.SWARM_USER`), `chmod 600`. 4. **Important addition beyond the literal issue text:** since this workflow is shared by *every* repo (not all of which may define `SWARM_HOST`/`SWARM_USER`/`SSH_PRIV_KEY`), the step must no-op cleanly (not fail the job) when those secrets are empty — just skip writing the alias rather than erroring the whole run. 5. No secret values get echoed/printed anywhere (matches the existing masking convention already used for `OLLAMA_CLOUD_API_KEY` etc.). **What I verified already (no change needed):** the `~/.config/opencode/opencode.json` written in the "Install opencode + provider config" step only sets `provider` and `mcp` — it has no `permission` block, so it does not and must not override the target repo's `opencode.json`. The implementer should keep it that way and explicitly confirm this in the PR description, per the issue's safety note. **Who builds it:** @senior — it's a single-file, single-step change, but it handles private-key material and needs the "skip gracefully if secrets absent" conditional done correctly so it doesn't break the shared workflow for repos without swarm secrets. That's a bit more judgment than a pure YAML/compose tweak, so senior over junior. **Verification:** Not a browser/UI task, so @qa's Playwright flow doesn't apply here. Real verification is: after merge, trigger an agent run in `ffaerber/homelab` and confirm `ssh node1 docker service ls` succeeds and `ssh node1 docker service rm ...` is denied — that's a manual check @ffaerber (or a follow-up comment on this issue) would do post-merge. @ffaerber ready to start building? reply yes to proceed.
Owner

@pm yes

@pm yes

🤖 @pm

Understood — maintainer approved. Handing off to @senior to implement the SSH wiring step in .gitea/workflows/agent.yml per the plan above: write SSH_PRIV_KEY to ~/.ssh/agent_node1 (600), write ~/.ssh/config with the node1 alias using SWARM_HOST/SWARM_USER (600, ~/.ssh 700), placed before opencode run, with a graceful no-op if the swarm secrets are absent (so repos without them don't fail), and confirm in the PR description that the provider opencode.json has no permission block that could override a target repo's allowlist.

🤖 **@pm** Understood — maintainer approved. Handing off to @senior to implement the SSH wiring step in `.gitea/workflows/agent.yml` per the plan above: write `SSH_PRIV_KEY` to `~/.ssh/agent_node1` (600), write `~/.ssh/config` with the `node1` alias using `SWARM_HOST`/`SWARM_USER` (600, `~/.ssh` 700), placed before `opencode run`, with a graceful no-op if the swarm secrets are absent (so repos without them don't fail), and confirm in the PR description that the provider `opencode.json` has no `permission` block that could override a target repo's allowlist.
Owner

@senior please proceed with issue #10 per the plan above (delegated by pm).

@senior please proceed with issue #10 per the plan above (delegated by pm).

🔨 @senior is on it — building on branch ai/issue-10. I'll open a PR when it's ready.

🔨 **@senior** is on it — building on branch [`ai/issue-10`](https://git.ffaerber.duckdns.org/ffaerber/agents/src/branch/ai/issue-10). I'll open a PR when it's ready.

🤖 @senior 1 PR ready for review — @ffaerber please review & merge:

🤖 **@senior** — ✅ 1 PR ready for review — @ffaerber please review & merge: - https://git.ffaerber.duckdns.org/ffaerber/agents/pulls/11
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gitea/agents#10