feat(agentci): add context-aware ssh command helper
Some checks failed
Security Scan / security (push) Failing after 11s
Test / test (push) Successful in 2m15s

Thread dispatch SSH subprocesses through the caller context so cancellation applies to ticket transfer, remote cleanup, and existence checks.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 06:53:33 +00:00
parent 8021e5e2cb
commit 2e188e346a
3 changed files with 29 additions and 4 deletions

View file

@ -3,6 +3,7 @@
package agentci package agentci
import ( import (
"context"
strings "dappco.re/go/core/scm/internal/ax/stringsx" strings "dappco.re/go/core/scm/internal/ax/stringsx"
exec "golang.org/x/sys/execabs" exec "golang.org/x/sys/execabs"
"path" "path"
@ -146,7 +147,17 @@ func EscapeShellArg(arg string) string {
// SecureSSHCommand creates an SSH exec.Cmd with strict host key checking and batch mode. // SecureSSHCommand creates an SSH exec.Cmd with strict host key checking and batch mode.
// Usage: SecureSSHCommand(...) // Usage: SecureSSHCommand(...)
func SecureSSHCommand(host string, remoteCmd string) *exec.Cmd { func SecureSSHCommand(host string, remoteCmd string) *exec.Cmd {
return exec.Command("ssh", return SecureSSHCommandContext(context.Background(), host, remoteCmd)
}
// SecureSSHCommandContext creates an SSH exec.Cmd with strict host key checking and batch mode.
// Usage: SecureSSHCommandContext(...)
func SecureSSHCommandContext(ctx context.Context, host string, remoteCmd string) *exec.Cmd {
if ctx == nil {
ctx = context.Background()
}
return exec.CommandContext(ctx, "ssh",
"-o", "StrictHostKeyChecking=yes", "-o", "StrictHostKeyChecking=yes",
"-o", "BatchMode=yes", "-o", "BatchMode=yes",
"-o", "ConnectTimeout=10", "-o", "ConnectTimeout=10",

View file

@ -3,6 +3,7 @@
package agentci package agentci
import ( import (
"context"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -89,6 +90,19 @@ func TestSecureSSHCommand_Good(t *testing.T) {
assert.Equal(t, "ls -la", args[len(args)-1]) assert.Equal(t, "ls -la", args[len(args)-1])
} }
func TestSecureSSHCommandContext_Good(t *testing.T) {
cmd := SecureSSHCommandContext(context.Background(), "host.example.com", "ls -la")
args := cmd.Args
assert.Equal(t, "ssh", args[0])
assert.Contains(t, args, "-o")
assert.Contains(t, args, "StrictHostKeyChecking=yes")
assert.Contains(t, args, "BatchMode=yes")
assert.Contains(t, args, "ConnectTimeout=10")
assert.Equal(t, "host.example.com", args[len(args)-2])
assert.Equal(t, "ls -la", args[len(args)-1])
}
func TestMaskToken_Good(t *testing.T) { func TestMaskToken_Good(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View file

@ -296,7 +296,7 @@ func (h *DispatchHandler) secureTransfer(ctx context.Context, agent agentci.Agen
safePath := agentci.EscapeShellArg(remotePath) safePath := agentci.EscapeShellArg(remotePath)
remoteCmd := fmt.Sprintf("cat > %s && chmod %o %s", safePath, mode, safePath) remoteCmd := fmt.Sprintf("cat > %s && chmod %o %s", safePath, mode, safePath)
cmd := agentci.SecureSSHCommand(agent.Host, remoteCmd) cmd := agentci.SecureSSHCommandContext(ctx, agent.Host, remoteCmd)
cmd.Stdin = bytes.NewReader(data) cmd.Stdin = bytes.NewReader(data)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
@ -318,7 +318,7 @@ func (h *DispatchHandler) runRemote(ctx context.Context, agent agentci.AgentConf
remoteCmd = strings.Join(escaped, " ") remoteCmd = strings.Join(escaped, " ")
} }
cmd := agentci.SecureSSHCommand(agent.Host, remoteCmd) cmd := agentci.SecureSSHCommandContext(ctx, agent.Host, remoteCmd)
return cmd.Run() return cmd.Run()
} }
@ -357,6 +357,6 @@ func (h *DispatchHandler) ticketExists(ctx context.Context, agent agentci.AgentC
"test -f %s || test -f %s || test -f %s", "test -f %s || test -f %s || test -f %s",
queuePath, activePath, donePath, queuePath, activePath, donePath,
) )
cmd := agentci.SecureSSHCommand(agent.Host, checkCmd) cmd := agentci.SecureSSHCommandContext(ctx, agent.Host, checkCmd)
return cmd.Run() == nil return cmd.Run() == nil
} }