remote: auto-upgrade ollie-remote on each tool call

Before each CallTool, check if the local ollie-remote binary hash
differs from what was deployed. If so, close the SSH connection,
re-dial (triggering bootstrap to upload the new binary), and swap
the connection transparently.

Emits info events when upgrade is detected and when it completes.
This commit is contained in:
Levi Neely 2026-07-31 11:23:15 +02:00
parent 249b9f80f9
commit 0c75d06c59
1 changed files with 62 additions and 5 deletions

View File

@ -48,6 +48,11 @@ type RemoteServer struct {
dec *json.Decoder
nextID atomic.Int64
Info HostInfo
// For auto-upgrade on hash mismatch.
cfg RemoteConfig
ctx context.Context
deployedHash string
}
// Config holds the parameters for connecting to a remote host.
@ -198,11 +203,14 @@ func RemoteDial(ctx context.Context, cfg RemoteConfig) (*RemoteServer, error) {
// Bootstrap complete — stdin/stdout are now JSON-RPC.
s := &RemoteServer{
stdin: stdin,
stdout: stdout,
cmd: cmd,
enc: json.NewEncoder(stdin),
dec: json.NewDecoder(bufio.NewReader(stdout)),
stdin: stdin,
stdout: stdout,
cmd: cmd,
enc: json.NewEncoder(stdin),
dec: json.NewDecoder(bufio.NewReader(stdout)),
cfg: cfg,
ctx: ctx,
deployedHash: hashStr,
}
// Verify connectivity with a ping.
@ -277,6 +285,50 @@ func (s *RemoteServer) Close() error {
return nil
}
// checkUpgrade compares the local ollie-remote binary hash to the deployed
// hash. If they differ, it closes the current connection and re-dials,
// which triggers the bootstrap to upload the new binary.
func (s *RemoteServer) checkUpgrade() error {
localBin, err := findLocalBinary()
if err != nil {
return nil // can't find binary, skip check
}
binData, err := os.ReadFile(localBin)
if err != nil {
return nil
}
hash := sha256.Sum256(binData)
hashStr := hex.EncodeToString(hash[:])
if hashStr == s.deployedHash {
return nil
}
// Hash changed — reconnect.
StreamOutput(s.ctx, "[info] ollie-remote binary changed, upgrading remote...\n")
s.Close()
fresh, err := RemoteDial(s.ctx, s.cfg)
if err != nil {
return err
}
// Swap internals from the fresh connection.
s.mu.Lock()
s.stdin = fresh.stdin
s.stdout = fresh.stdout
s.cmd = fresh.cmd
s.enc = fresh.enc
s.dec = fresh.dec
s.deployedHash = fresh.deployedHash
s.Info = fresh.Info
s.mu.Unlock()
StreamOutput(s.ctx, "[info] remote binary upgraded successfully\n")
return nil
}
// --- Server interface ---
func (s *RemoteServer) ListTools() ([]ToolInfo, error) {
@ -311,6 +363,11 @@ func (s *RemoteServer) ListTools() ([]ToolInfo, error) {
}
func (s *RemoteServer) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
// Check if local binary has been updated; reconnect if so.
if err := s.checkUpgrade(); err != nil {
StreamOutput(ctx, "[info] remote upgrade failed: "+err.Error()+"\n")
}
// Forward the call with the tool name as the RPC method.
id := s.nextID.Add(1)