Documentation
¶
Overview ¶
Package execproxy implements the airlock-side SSH execution path for agentsdk.RegisterExecEndpoint. The dialer opens a session against the operator-configured target, streams stdout/stderr/exit envelopes back over an http.ResponseWriter as NDJSON, and handles host-key TOFU.
Credentials never leave airlock — the private key is decrypted via secrets.Store at dial time and the agent process never sees raw key material. Operational logs record byte counts only, not payloads.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // DefaultExecTimeout is the wall-clock cap for a single exec call // when the caller doesn't specify ExecCommand.TimeoutMs. SSH dial, // auth, command execution, and stream draining all share this // budget. DefaultExecTimeout = 60 * time.Second // MaxExecTimeout is the upper bound — operators can't accidentally // allow a single call to hold a session for hours. MaxExecTimeout = 10 * time.Minute // DialTimeout is how long the TCP+SSH handshake gets before the // dialer gives up and returns a transport error. DialTimeout = 15 * time.Second // StreamChunkBytes is the raw byte count we buffer between reads // of session.Stdout/Stderr before base64-encoding and writing one // NDJSON envelope. 32 KiB keeps each envelope under the SDK's // 256 KiB scanner buffer with a comfortable margin (base64 // inflates ~33%) while batching enough to avoid envelope-per-byte // overhead. StreamChunkBytes = 32 * 1024 )
Default ceilings — kept as vars so tests can lower them. Production callers should treat these as constants.
Functions ¶
func HostKeyFingerprint ¶
HostKeyFingerprint returns the SHA256 fingerprint of a host key OpenSSH-formatted string, for UI display. Empty input → empty output.
func JoinCommand ¶
JoinCommand assembles command + args into a single shell command line suitable for SSH exec. Args are shell-quoted; command is not — pipes, redirection, and shell substitution in command pass through to the remote shell unchanged. This is the semantic of `ssh user@host cmd a1 a2`: the SSH client joins everything with spaces and the remote shell does the parsing.
func ShellQuote ¶
ShellQuote wraps s in single quotes for safe inclusion in a POSIX shell command line. The remote sshd hands our command string to the user's login shell (`$SHELL -c "..."`), so any argument that contains whitespace, glob characters, or shell metacharacters must be quoted to avoid being re-split on the remote side.
Single-quoting handles every metacharacter except `'` itself; we escape an embedded apostrophe by closing the quote, emitting an escaped `'`, and reopening the quote: don't → 'don'\”t'
Types ¶
type ExecRequest ¶
ExecRequest is the parsed input from the agent's POST /api/agent/exec/{slug} body. Stdin is the raw bytes (already decoded from base64 on the wire).
type HostKeyMismatchError ¶
HostKeyMismatchError is returned when a pinned host key doesn't match what the remote presented during the SSH handshake. Distinct type so the HTTP handler can map it to a specific status / message.
func (*HostKeyMismatchError) Error ¶
func (e *HostKeyMismatchError) Error() string
type Keypair ¶
Keypair carries a freshly generated ED25519 keypair plus the OpenSSH public-key line and its dated comment. Airlock stores PrivatePEM encrypted via secrets.Store and persists PublicOpenSSH + Comment in the agent_exec_endpoints row for UI display.
func GenerateED25519 ¶
GenerateED25519 mints a new ED25519 keypair with a dated, human-grep'able comment. The comment shape lets the operator find old keys in authorized_keys after a rotation:
ssh-ed25519 AAAA… airlock-myagent-ci-runner-2026-05-26
agentSlug + endpointSlug + an ISO date give a stable per-rotation identifier the operator can match exactly.
type PreStreamError ¶
type PreStreamError struct {
Kind string // transport | timeout | config | denied
Status int // HTTP status to write
Message string
}
PreStreamError is returned by Exec when the failure happened before any byte of the streaming response was written. The HTTP handler maps .Status to a status code; the agent SDK uses the status code (not the body) to classify into ExecError.Kind.
func (*PreStreamError) Error ¶
func (e *PreStreamError) Error() string
type SSHDialer ¶
type SSHDialer struct {
// contains filtered or unexported fields
}
SSHDialer is the entry point for the agent-internal exec handler. Build one at server startup and reuse it for every exec call.
func NewSSHDialer ¶
NewSSHDialer constructs an SSHDialer with sensible defaults. Call Close at shutdown to stop the cache reaper.
func (*SSHDialer) EvictCache ¶
EvictCache clears the cached client for endpoint id — called by the admin handlers after any config / keypair / host-key mutation so the next call dials fresh.
func (*SSHDialer) Exec ¶
func (d *SSHDialer) Exec( ctx context.Context, ep *dbq.AgentExecEndpoint, req ExecRequest, w http.ResponseWriter, ) error
Exec opens an SSH session, runs the assembled command, and streams stdout/stderr/exit envelopes to w as NDJSON. All errors after the first byte is written go into the stream as a terminal "error" envelope; the function still returns nil to the caller because the HTTP response has already started. Pre-stream errors (dial, auth, host-key mismatch) return without writing anything so the HTTP handler can map them to a status code.
type TOFUPinner ¶
type TOFUPinner interface {
PinHostKey(ctx context.Context, endpointID uuid.UUID, hostKeyOpenSSH string) error
}
TOFUPinner is the dependency the dialer needs to record a newly-seen host key the first time a TOFU connect succeeds. The HTTP handler implements this against the sqlc-generated SetExecEndpointHostKey.