sketch/loop: add PortMonitor for TCP port monitoring with Agent integration

Add PortMonitor struct that uses Tailscale portlist library to monitor
open/listening TCP ports and send AgentMessage notifications to Agent
when ports are opened or closed, with cached port list access method.

When I asked Sketch to do this with the old implementation, it did
ok parsing /proc, but then it tried to conver it to ss format...
using a library seems to work ok!

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s8fc57de4b5583d34k
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index b6e2259..3f90f8b 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -104,6 +104,15 @@
 	SSHConnectionString  string                        `json:"ssh_connection_string,omitempty"` // SSH connection string for container
 	DiffLinesAdded       int                           `json:"diff_lines_added"`                // Lines added from sketch-base to HEAD
 	DiffLinesRemoved     int                           `json:"diff_lines_removed"`              // Lines removed from sketch-base to HEAD
+	OpenPorts            []Port                        `json:"open_ports,omitempty"`            // Currently open TCP ports
+}
+
+// Port represents an open TCP port
+type Port struct {
+	Proto   string `json:"proto"`   // "tcp" or "udp"
+	Port    uint16 `json:"port"`    // port number
+	Process string `json:"process"` // optional process name
+	Pid     int    `json:"pid"`     // process ID
 }
 
 type InitRequest struct {
@@ -1308,9 +1317,29 @@
 		SSHConnectionString:  s.agent.SSHConnectionString(),
 		DiffLinesAdded:       diffAdded,
 		DiffLinesRemoved:     diffRemoved,
+		OpenPorts:            s.getOpenPorts(),
 	}
 }
 
+// getOpenPorts retrieves the current open ports from the agent
+func (s *Server) getOpenPorts() []Port {
+	ports := s.agent.GetPorts()
+	if ports == nil {
+		return nil
+	}
+
+	result := make([]Port, len(ports))
+	for i, port := range ports {
+		result[i] = Port{
+			Proto:   port.Proto,
+			Port:    port.Port,
+			Process: port.Process,
+			Pid:     port.Pid,
+		}
+	}
+	return result
+}
+
 func (s *Server) handleGitRawDiff(w http.ResponseWriter, r *http.Request) {
 	if r.Method != "GET" {
 		w.WriteHeader(http.StatusMethodNotAllowed)