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
import (
"context"
strings "dappco.re/go/core/scm/internal/ax/stringsx"
exec "golang.org/x/sys/execabs"
"path"
@ -146,7 +147,17 @@ func EscapeShellArg(arg string) string {
// SecureSSHCommand creates an SSH exec.Cmd with strict host key checking and batch mode.
// Usage: SecureSSHCommand(...)
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", "BatchMode=yes",
"-o", "ConnectTimeout=10",

View file

@ -3,6 +3,7 @@
package agentci
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@ -89,6 +90,19 @@ func TestSecureSSHCommand_Good(t *testing.T) {
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) {
tests := []struct {
name string

View file

@ -296,7 +296,7 @@ func (h *DispatchHandler) secureTransfer(ctx context.Context, agent agentci.Agen
safePath := agentci.EscapeShellArg(remotePath)
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)
output, err := cmd.CombinedOutput()
@ -318,7 +318,7 @@ func (h *DispatchHandler) runRemote(ctx context.Context, agent agentci.AgentConf
remoteCmd = strings.Join(escaped, " ")
}
cmd := agentci.SecureSSHCommand(agent.Host, remoteCmd)
cmd := agentci.SecureSSHCommandContext(ctx, agent.Host, remoteCmd)
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",
queuePath, activePath, donePath,
)
cmd := agentci.SecureSSHCommand(agent.Host, checkCmd)
cmd := agentci.SecureSSHCommandContext(ctx, agent.Host, checkCmd)
return cmd.Run() == nil
}