diff --git a/toolsrv/dial.go b/toolsrv/dial.go new file mode 100644 index 0000000..8174e4c --- /dev/null +++ b/toolsrv/dial.go @@ -0,0 +1,55 @@ +package toolsrv + +import ( + "context" + "fmt" +) + +// Addr describes how to reach a tool server. +type Addr struct { + // Network is the transport: "local", "ssh", or (future) "unix". + Network string + // Host is the SSH target (user@host or user@host:port). Empty for local. + Host string + // CWD is the working directory on the target. + CWD string +} + +// LocalAddr returns an Addr for an in-process tool server. +func LocalAddr(cwd string) Addr { + return Addr{Network: "local", CWD: cwd} +} + +// SSHAddr returns an Addr for a remote tool server over SSH. +func SSHAddr(host, cwd string) Addr { + return Addr{Network: "ssh", Host: host, CWD: cwd} +} + +// Dial connects to a tool server at the given address. +// Options (WithStrict, WithBuiltins, etc.) are applied to local servers. +// +// TODO: Unify local and remote behind a single protocol. Local execution +// should eventually go through a Unix socket (same JSON-RPC as remote), +// making the agent always a client. The elevation broker already proves +// this pattern. Benefits: process isolation, shared tool server across +// sessions, independent lifecycle. Transport becomes the only difference: +// - Network "local" → unix socket to co-located toolsrv process +// - Network "ssh" → SSH tunnel to remote toolsrv process +// - Network "unix" → pre-existing unix socket +func Dial(ctx context.Context, addr Addr, opts ...Option) (Runner, error) { + switch addr.Network { + case "", "local": + srv := New(addr.CWD) + for _, o := range opts { + o(srv) + } + return srv, nil + case "ssh": + return RemoteDial(ctx, RemoteConfig{ + SSHTarget: addr.Host, + CWD: addr.CWD, + }) + default: + return nil, fmt.Errorf("toolsrv.Dial: unsupported network %q", addr.Network) + } +}