remove port monitoring and automatic tunneling features

Remove port_monitor, TunnelManager, and /port-events handler to eliminate
automatic port tunneling functionality that bridges outtie to innie environments.

Sketch got confused when I asked it to change how this works; removing
and re-adding was easier!

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s78f868b27a44cb2bk
diff --git a/loop/agent.go b/loop/agent.go
index f01e601..8f96924 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -152,8 +152,7 @@
 	// CompactConversation compacts the current conversation by generating a summary
 	// and restarting the conversation with that summary as the initial context
 	CompactConversation(ctx context.Context) error
-	// GetPortMonitor returns the port monitor instance for accessing port events
-	GetPortMonitor() *PortMonitor
+
 	// SkabandAddr returns the skaband address if configured
 	SkabandAddr() string
 }
@@ -460,9 +459,6 @@
 
 	// Track outstanding tool calls by ID with their names
 	outstandingToolCalls map[string]string
-
-	// Port monitoring
-	portMonitor *PortMonitor
 }
 
 // NewIterator implements CodingAgent.
@@ -1071,8 +1067,8 @@
 		stateMachine:         NewStateMachine(),
 		workingDir:           config.WorkingDir,
 		outsideHTTP:          config.OutsideHTTP,
-		portMonitor:          NewPortMonitor(),
-		mcpManager:           mcp.NewMCPManager(),
+
+		mcpManager: mcp.NewMCPManager(),
 	}
 	return agent
 }
@@ -1526,12 +1522,6 @@
 }
 
 func (a *Agent) Loop(ctxOuter context.Context) {
-	// Start port monitoring when the agent loop begins
-	// Only monitor ports when running in a container
-	if a.IsInContainer() {
-		a.portMonitor.Start(ctxOuter)
-	}
-
 	// Set up cleanup when context is done
 	defer func() {
 		if a.mcpManager != nil {
@@ -2540,11 +2530,6 @@
 	return nil
 }
 
-// GetPortMonitor returns the port monitor instance for accessing port events
-func (a *Agent) GetPortMonitor() *PortMonitor {
-	return a.portMonitor
-}
-
 // SkabandAddr returns the skaband address if configured
 func (a *Agent) SkabandAddr() string {
 	if a.config.SkabandClient != nil {
diff --git a/loop/port_monitor.go b/loop/port_monitor.go
deleted file mode 100644
index 105e1e7..0000000
--- a/loop/port_monitor.go
+++ /dev/null
@@ -1,344 +0,0 @@
-package loop
-
-import (
-	"context"
-	"fmt"
-	"log/slog"
-	"net"
-	"os"
-	"os/exec"
-	"strconv"
-	"strings"
-	"sync"
-	"time"
-)
-
-// PortEvent represents a port change event
-type PortEvent struct {
-	Type      string    `json:"type"`      // "opened" or "closed"
-	Port      string    `json:"port"`      // "proto:address:port" format
-	Timestamp time.Time `json:"timestamp"` // when the event occurred
-}
-
-// PortMonitor handles periodic monitoring of listening ports in containers
-type PortMonitor struct {
-	mu        sync.Mutex  // protects following
-	lastPorts string      // last netstat/ss output for comparison
-	events    []PortEvent // circular buffer of recent port events
-	maxEvents int         // maximum events to keep in buffer
-}
-
-// NewPortMonitor creates a new PortMonitor instance
-func NewPortMonitor() *PortMonitor {
-	return &PortMonitor{
-		maxEvents: 100, // keep last 100 events
-		events:    make([]PortEvent, 0, 100),
-	}
-}
-
-// Start begins periodic port monitoring in a background goroutine
-func (pm *PortMonitor) Start(ctx context.Context) {
-	go func() {
-		ticker := time.NewTicker(5 * time.Second) // Check every 5 seconds
-		defer ticker.Stop()
-
-		// Get initial port state
-		pm.updatePortState(ctx)
-
-		for {
-			select {
-			case <-ctx.Done():
-				return
-			case <-ticker.C:
-				pm.updatePortState(ctx)
-			}
-		}
-	}()
-}
-
-// updatePortState runs ss and checks for changes in listening ports
-// Falls back to /proc/net/tcp* parsing if ss is not available
-func (pm *PortMonitor) updatePortState(ctx context.Context) {
-	var currentPorts string
-	var err error
-
-	// Try ss command first
-	cmd := exec.CommandContext(ctx, "ss", "-lntu")
-	output, err := cmd.Output()
-	if err != nil {
-		// ss command failed, try /proc filesystem fallback
-		slog.DebugContext(ctx, "ss command failed, trying /proc fallback", "error", err)
-		currentPorts, err = pm.getListeningPortsFromProc()
-		if err != nil {
-			// Both methods failed - log and return
-			slog.DebugContext(ctx, "Failed to get listening ports", "ss_error", err)
-			return
-		}
-	} else {
-		currentPorts = string(output)
-	}
-
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-
-	// Check if ports have changed
-	if pm.lastPorts != "" && pm.lastPorts != currentPorts {
-		// Ports have changed, log the difference
-		slog.InfoContext(ctx, "Container port changes detected",
-			slog.String("previous_ports", pm.lastPorts),
-			slog.String("current_ports", currentPorts))
-
-		// Parse and compare the port lists for more detailed logging
-		pm.logPortDifferences(ctx, pm.lastPorts, currentPorts)
-	}
-
-	pm.lastPorts = currentPorts
-}
-
-// logPortDifferences parses ss output and logs specific port changes
-func (pm *PortMonitor) logPortDifferences(ctx context.Context, oldPorts, newPorts string) {
-	oldPortSet := parseSSPorts(oldPorts)
-	newPortSet := parseSSPorts(newPorts)
-	now := time.Now()
-
-	// Find newly opened ports
-	for port := range newPortSet {
-		if !oldPortSet[port] {
-			slog.InfoContext(ctx, "New port detected", slog.String("port", port))
-			pm.addEvent(PortEvent{
-				Type:      "opened",
-				Port:      port,
-				Timestamp: now,
-			})
-		}
-	}
-
-	// Find closed ports
-	for port := range oldPortSet {
-		if !newPortSet[port] {
-			slog.InfoContext(ctx, "Port closed", slog.String("port", port))
-			pm.addEvent(PortEvent{
-				Type:      "closed",
-				Port:      port,
-				Timestamp: now,
-			})
-		}
-	}
-}
-
-// addEvent adds a port event to the circular buffer (must be called with mutex held)
-func (pm *PortMonitor) addEvent(event PortEvent) {
-	// If buffer is full, remove oldest event
-	if len(pm.events) >= pm.maxEvents {
-		// Shift all events left by 1 to remove oldest
-		copy(pm.events, pm.events[1:])
-		pm.events = pm.events[:len(pm.events)-1]
-	}
-	// Add new event
-	pm.events = append(pm.events, event)
-}
-
-// GetRecentEvents returns a copy of recent port events since the given timestamp
-func (pm *PortMonitor) GetRecentEvents(since time.Time) []PortEvent {
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-
-	// Find events since the given timestamp
-	var result []PortEvent
-	for _, event := range pm.events {
-		if event.Timestamp.After(since) {
-			result = append(result, event)
-		}
-	}
-	return result
-}
-
-// UpdatePortState is a public wrapper for updatePortState for testing purposes
-func (pm *PortMonitor) UpdatePortState(ctx context.Context) {
-	pm.updatePortState(ctx)
-}
-
-// GetAllRecentEvents returns a copy of all recent port events
-func (pm *PortMonitor) GetAllRecentEvents() []PortEvent {
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-
-	// Return a copy of all events
-	result := make([]PortEvent, len(pm.events))
-	copy(result, pm.events)
-	return result
-}
-
-// parseSSPorts extracts listening ports from ss -lntu output
-// Returns a map with "proto:address:port" as keys
-// ss output format: Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
-func parseSSPorts(output string) map[string]bool {
-	ports := make(map[string]bool)
-	lines := strings.Split(output, "\n")
-
-	for _, line := range lines {
-		fields := strings.Fields(line)
-		if len(fields) < 5 {
-			continue
-		}
-
-		// Skip header line and non-LISTEN states
-		if fields[0] == "Netid" || fields[1] != "LISTEN" {
-			continue
-		}
-
-		proto := fields[0]
-		localAddr := fields[4] // Local Address:Port
-		portKey := fmt.Sprintf("%s:%s", proto, localAddr)
-		ports[portKey] = true
-	}
-
-	return ports
-}
-
-// getListeningPortsFromProc reads /proc/net/tcp and /proc/net/tcp6 to find listening ports
-// Returns output in a format similar to ss -lntu
-func (pm *PortMonitor) getListeningPortsFromProc() (string, error) {
-	var result strings.Builder
-	result.WriteString("Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port\n")
-
-	// Parse IPv4 listening ports
-	if err := pm.parseProc("/proc/net/tcp", "tcp", &result); err != nil {
-		return "", fmt.Errorf("failed to parse /proc/net/tcp: %w", err)
-	}
-
-	// Parse IPv6 listening ports
-	if err := pm.parseProc("/proc/net/tcp6", "tcp", &result); err != nil {
-		// IPv6 might not be available, log but don't fail
-		slog.Debug("Failed to parse /proc/net/tcp6", "error", err)
-	}
-
-	// Parse UDP ports
-	if err := pm.parseProc("/proc/net/udp", "udp", &result); err != nil {
-		slog.Debug("Failed to parse /proc/net/udp", "error", err)
-	}
-
-	if err := pm.parseProc("/proc/net/udp6", "udp", &result); err != nil {
-		slog.Debug("Failed to parse /proc/net/udp6", "error", err)
-	}
-
-	return result.String(), nil
-}
-
-// parseProc parses a /proc/net/* file for listening sockets
-func (pm *PortMonitor) parseProc(filename, protocol string, result *strings.Builder) error {
-	data, err := os.ReadFile(filename)
-	if err != nil {
-		return err
-	}
-
-	lines := strings.Split(string(data), "\n")
-	for i, line := range lines {
-		if i == 0 || strings.TrimSpace(line) == "" {
-			continue // Skip header and empty lines
-		}
-
-		fields := strings.Fields(line)
-		if len(fields) < 4 {
-			continue
-		}
-
-		// Parse socket state (4th field, index 3)
-		stateHex := fields[3]
-		state, err := strconv.ParseInt(stateHex, 16, 32)
-		if err != nil {
-			continue
-		}
-
-		// Check if socket is in LISTEN state (0x0A for TCP) or bound state for UDP
-		isListening := false
-		if protocol == "tcp" && state == 0x0A {
-			isListening = true
-		} else if protocol == "udp" && state == 0x07 {
-			// UDP sockets in state 0x07 (TCP_CLOSE) are bound/listening
-			isListening = true
-		}
-
-		if !isListening {
-			continue
-		}
-
-		// Parse local address (2nd field, index 1)
-		localAddr := fields[1]
-		addr, port, err := pm.parseAddress(localAddr, strings.Contains(filename, "6"))
-		if err != nil {
-			continue
-		}
-
-		// Format similar to ss output
-		result.WriteString(fmt.Sprintf("%s   LISTEN 0      0          %s:%d        0.0.0.0:*\n",
-			protocol, addr, port))
-	}
-
-	return nil
-}
-
-// parseAddress parses hex-encoded address:port from /proc/net files
-func (pm *PortMonitor) parseAddress(addrPort string, isIPv6 bool) (string, int, error) {
-	parts := strings.Split(addrPort, ":")
-	if len(parts) != 2 {
-		return "", 0, fmt.Errorf("invalid address:port format: %s", addrPort)
-	}
-
-	// Parse port (stored in hex, big-endian)
-	portHex := parts[1]
-	port, err := strconv.ParseInt(portHex, 16, 32)
-	if err != nil {
-		return "", 0, fmt.Errorf("invalid port hex: %s", portHex)
-	}
-
-	// Parse IP address
-	addrHex := parts[0]
-	var addr string
-
-	if isIPv6 {
-		// IPv6: 32 hex chars representing 16 bytes
-		if len(addrHex) != 32 {
-			return "", 0, fmt.Errorf("invalid IPv6 address hex length: %d", len(addrHex))
-		}
-		// Convert hex to IPv6 address
-		var ipBytes [16]byte
-		for i := 0; i < 16; i++ {
-			b, err := strconv.ParseInt(addrHex[i*2:(i+1)*2], 16, 8)
-			if err != nil {
-				return "", 0, fmt.Errorf("invalid IPv6 hex: %s", addrHex)
-			}
-			ipBytes[i] = byte(b)
-		}
-		// /proc stores IPv6 in little-endian 32-bit chunks, need to reverse each chunk
-		for i := 0; i < 16; i += 4 {
-			ipBytes[i], ipBytes[i+1], ipBytes[i+2], ipBytes[i+3] = ipBytes[i+3], ipBytes[i+2], ipBytes[i+1], ipBytes[i]
-		}
-		addr = net.IP(ipBytes[:]).String()
-	} else {
-		// IPv4: 8 hex chars representing 4 bytes in little-endian
-		if len(addrHex) != 8 {
-			return "", 0, fmt.Errorf("invalid IPv4 address hex length: %d", len(addrHex))
-		}
-		// Parse as little-endian 32-bit integer
-		addrInt, err := strconv.ParseInt(addrHex, 16, 64)
-		if err != nil {
-			return "", 0, fmt.Errorf("invalid IPv4 hex: %s", addrHex)
-		}
-		// Convert to IP address (reverse byte order for little-endian)
-		addr = fmt.Sprintf("%d.%d.%d.%d",
-			addrInt&0xFF,
-			(addrInt>>8)&0xFF,
-			(addrInt>>16)&0xFF,
-			(addrInt>>24)&0xFF)
-	}
-
-	// Handle special addresses
-	if addr == "0.0.0.0" {
-		addr = "*"
-	} else if addr == "::" {
-		addr = "*"
-	}
-
-	return addr, int(port), nil
-}
diff --git a/loop/port_monitor_test.go b/loop/port_monitor_test.go
deleted file mode 100644
index e0f7ff1..0000000
--- a/loop/port_monitor_test.go
+++ /dev/null
@@ -1,388 +0,0 @@
-package loop
-
-import (
-	"context"
-	"os"
-	"strings"
-	"testing"
-	"time"
-)
-
-// TestPortMonitoring tests the port monitoring functionality
-func TestPortMonitoring(t *testing.T) {
-	// Test with ss output format
-	ssOutput := `Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
-tcp   LISTEN 0      1024       127.0.0.1:40975      0.0.0.0:*          
-tcp   LISTEN 0      4096               *:22               *:*          
-tcp   LISTEN 0      4096               *:80               *:*          
-udp   UNCONN 0      0               127.0.0.1:123            0.0.0.0:*          
-`
-
-	expected := map[string]bool{
-		"tcp:127.0.0.1:40975": true,
-		"tcp:*:22":            true,
-		"tcp:*:80":            true,
-	}
-
-	result := parseSSPorts(ssOutput)
-
-	// Check that all expected ports are found
-	for port := range expected {
-		if !result[port] {
-			t.Errorf("Expected port %s not found in ss parsed output", port)
-		}
-	}
-
-	// Check that UDP port is not included (since it's UNCONN, not LISTEN)
-	if result["udp:127.0.0.1:123"] {
-		t.Errorf("UDP UNCONN port should not be included in listening ports")
-	}
-
-	// Check that no extra ports are found
-	for port := range result {
-		if !expected[port] {
-			t.Errorf("Unexpected port %s found in parsed output", port)
-		}
-	}
-}
-
-// TestPortMonitoringLogDifferences tests the port difference logging
-func TestPortMonitoringLogDifferences(t *testing.T) {
-	ctx := context.Background()
-
-	oldPorts := `Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
-tcp   LISTEN 0      4096               *:22               *:*          
-tcp   LISTEN 0      1024       127.0.0.1:8080      0.0.0.0:*          
-`
-
-	newPorts := `Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
-tcp   LISTEN 0      4096               *:22               *:*          
-tcp   LISTEN 0      1024       127.0.0.1:9090      0.0.0.0:*          
-`
-
-	// Create a port monitor to test the logPortDifferences method
-	pm := NewPortMonitor()
-
-	// This test mainly ensures the method doesn't panic and processes the differences
-	// The actual logging output would need to be captured via a test logger to verify fully
-	pm.logPortDifferences(ctx, oldPorts, newPorts)
-
-	// Test with no differences
-	pm.logPortDifferences(ctx, oldPorts, oldPorts)
-}
-
-// TestPortMonitorCreation tests creating a new port monitor
-func TestPortMonitorCreation(t *testing.T) {
-	pm := NewPortMonitor()
-	if pm == nil {
-		t.Error("NewPortMonitor() returned nil")
-	}
-
-	// Verify initial state
-	pm.mu.Lock()
-	defer pm.mu.Unlock()
-	if pm.lastPorts != "" {
-		t.Error("NewPortMonitor() should have empty lastPorts initially")
-	}
-}
-
-// TestParseSSPortsEdgeCases tests edge cases in ss output parsing
-func TestParseSSPortsEdgeCases(t *testing.T) {
-	tests := []struct {
-		name     string
-		output   string
-		expected map[string]bool
-	}{
-		{
-			name:     "empty output",
-			output:   "",
-			expected: map[string]bool{},
-		},
-		{
-			name:     "header only",
-			output:   "Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess",
-			expected: map[string]bool{},
-		},
-		{
-			name:     "non-listen states filtered out",
-			output:   "tcp   ESTAB  0      0       127.0.0.1:8080      127.0.0.1:45678\nudp   UNCONN 0      0       127.0.0.1:123       0.0.0.0:*",
-			expected: map[string]bool{},
-		},
-		{
-			name:     "insufficient fields",
-			output:   "tcp LISTEN 0",
-			expected: map[string]bool{},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			result := parseSSPorts(test.output)
-			if len(result) != len(test.expected) {
-				t.Errorf("Expected %d ports, got %d", len(test.expected), len(result))
-			}
-			for port := range test.expected {
-				if !result[port] {
-					t.Errorf("Expected port %s not found", port)
-				}
-			}
-			for port := range result {
-				if !test.expected[port] {
-					t.Errorf("Unexpected port %s found", port)
-				}
-			}
-		})
-	}
-}
-
-// TestPortEventStorage tests the new event storage functionality
-func TestPortEventStorage(t *testing.T) {
-	pm := NewPortMonitor()
-
-	// Initially should have no events
-	allEvents := pm.GetAllRecentEvents()
-	if len(allEvents) != 0 {
-		t.Errorf("Expected 0 events initially, got %d", len(allEvents))
-	}
-
-	// Simulate port changes that would add events
-	ctx := context.Background()
-	oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:8080         0.0.0.0:*"
-	newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:9090         0.0.0.0:*"
-
-	pm.logPortDifferences(ctx, oldPorts, newPorts)
-
-	// Should now have events
-	allEvents = pm.GetAllRecentEvents()
-	if len(allEvents) != 2 {
-		t.Errorf("Expected 2 events (1 opened, 1 closed), got %d", len(allEvents))
-	}
-
-	// Check event types
-	foundOpened := false
-	foundClosed := false
-	for _, event := range allEvents {
-		if event.Type == "opened" && event.Port == "tcp:0.0.0.0:9090" {
-			foundOpened = true
-		}
-		if event.Type == "closed" && event.Port == "tcp:0.0.0.0:8080" {
-			foundClosed = true
-		}
-	}
-
-	if !foundOpened {
-		t.Error("Expected to find 'opened' event for port tcp:0.0.0.0:9090")
-	}
-	if !foundClosed {
-		t.Error("Expected to find 'closed' event for port tcp:0.0.0.0:8080")
-	}
-}
-
-// TestPortEventFiltering tests the time-based filtering
-func TestPortEventFiltering(t *testing.T) {
-	pm := NewPortMonitor()
-	ctx := context.Background()
-
-	// Record time before adding events
-	beforeTime := time.Now()
-	time.Sleep(1 * time.Millisecond) // Small delay to ensure timestamp difference
-
-	// Add some events
-	oldPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:8080         0.0.0.0:*"
-	newPorts := "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp   LISTEN 0      128    0.0.0.0:9090         0.0.0.0:*"
-	pm.logPortDifferences(ctx, oldPorts, newPorts)
-
-	// Get events since beforeTime - should get all events
-	recentEvents := pm.GetRecentEvents(beforeTime)
-	if len(recentEvents) != 2 {
-		t.Errorf("Expected 2 recent events, got %d", len(recentEvents))
-	}
-
-	// Get events since now - should get no events
-	nowTime := time.Now()
-	recentEvents = pm.GetRecentEvents(nowTime)
-	if len(recentEvents) != 0 {
-		t.Errorf("Expected 0 recent events since now, got %d", len(recentEvents))
-	}
-}
-
-// TestParseAddress tests the hex address parsing for /proc/net files
-func TestParseAddress(t *testing.T) {
-	pm := NewPortMonitor()
-
-	tests := []struct {
-		name       string
-		addrPort   string
-		isIPv6     bool
-		expectIP   string
-		expectPort int
-		expectErr  bool
-	}{
-		{
-			name:       "IPv4 localhost:80",
-			addrPort:   "0100007F:0050", // 127.0.0.1:80 in little-endian hex
-			isIPv6:     false,
-			expectIP:   "127.0.0.1",
-			expectPort: 80,
-			expectErr:  false,
-		},
-		{
-			name:       "IPv4 any:22",
-			addrPort:   "00000000:0016", // 0.0.0.0:22
-			isIPv6:     false,
-			expectIP:   "*",
-			expectPort: 22,
-			expectErr:  false,
-		},
-		{
-			name:       "IPv4 high port",
-			addrPort:   "0100007F:1F90", // 127.0.0.1:8080
-			isIPv6:     false,
-			expectIP:   "127.0.0.1",
-			expectPort: 8080,
-			expectErr:  false,
-		},
-		{
-			name:       "IPv6 any port 22",
-			addrPort:   "00000000000000000000000000000000:0016", // [::]:22
-			isIPv6:     true,
-			expectIP:   "*",
-			expectPort: 22,
-			expectErr:  false,
-		},
-		{
-			name:      "Invalid format - no colon",
-			addrPort:  "0100007F0050",
-			isIPv6:    false,
-			expectErr: true,
-		},
-		{
-			name:      "Invalid port hex",
-			addrPort:  "0100007F:ZZZZ",
-			isIPv6:    false,
-			expectErr: true,
-		},
-		{
-			name:      "Invalid IPv4 hex length",
-			addrPort:  "0100:0050",
-			isIPv6:    false,
-			expectErr: true,
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			ip, port, err := pm.parseAddress(test.addrPort, test.isIPv6)
-			if test.expectErr {
-				if err == nil {
-					t.Errorf("Expected error but got none")
-				}
-				return
-			}
-
-			if err != nil {
-				t.Errorf("Unexpected error: %v", err)
-				return
-			}
-
-			if ip != test.expectIP {
-				t.Errorf("Expected IP %s, got %s", test.expectIP, ip)
-			}
-
-			if port != test.expectPort {
-				t.Errorf("Expected port %d, got %d", test.expectPort, port)
-			}
-		})
-	}
-}
-
-// TestParseProcData tests parsing of mock /proc/net data
-func TestParseProcData(t *testing.T) {
-	pm := NewPortMonitor()
-
-	// Test TCP data with listening sockets
-	tcpData := `  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
-   0: 0100007F:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0        1
-   1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0        2
-   2: 0100007F:1F90 0200007F:C350 01 00000000:00000000 00:00000000 00000000     0        0        3`
-
-	var result strings.Builder
-	result.WriteString("Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port\n")
-
-	// Create temp file with test data
-	tmpFile := "/tmp/test_tcp"
-	err := os.WriteFile(tmpFile, []byte(tcpData), 0o644)
-	if err != nil {
-		t.Fatalf("Failed to create temp file: %v", err)
-	}
-	defer os.Remove(tmpFile)
-
-	err = pm.parseProc(tmpFile, "tcp", &result)
-	if err != nil {
-		t.Fatalf("parseProc failed: %v", err)
-	}
-
-	output := result.String()
-	t.Logf("Generated output:\n%s", output)
-
-	// Should contain listening ports (state 0A = LISTEN)
-	if !strings.Contains(output, "127.0.0.1:80") {
-		t.Error("Expected to find 127.0.0.1:80 in output")
-	}
-	if !strings.Contains(output, "*:22") {
-		t.Error("Expected to find *:22 in output")
-	}
-	// Should not contain established connection (state 01)
-	if strings.Contains(output, "127.0.0.1:8080") {
-		t.Error("Should not find established connection 127.0.0.1:8080 in output")
-	}
-}
-
-// TestGetListeningPortsFromProcFallback tests the complete /proc fallback
-func TestGetListeningPortsFromProcFallback(t *testing.T) {
-	pm := NewPortMonitor()
-
-	// This test verifies the method runs without error
-	// The actual files may or may not exist, but it should handle both cases gracefully
-	output, err := pm.getListeningPortsFromProc()
-	if err != nil {
-		t.Logf("getListeningPortsFromProc failed (may be expected if /proc/net files don't exist): %v", err)
-		// Don't fail the test - this might be expected in some environments
-		return
-	}
-
-	t.Logf("Generated /proc fallback output:\n%s", output)
-
-	// Should at least have a header
-	if !strings.Contains(output, "Netid State") {
-		t.Error("Expected header in /proc fallback output")
-	}
-}
-
-// TestUpdatePortStateWithFallback tests updatePortState with both ss and /proc fallback
-func TestUpdatePortStateWithFallback(t *testing.T) {
-	pm := NewPortMonitor()
-	ctx := context.Background()
-
-	// Call updatePortState - should try ss first, then fall back to /proc if ss fails
-	pm.updatePortState(ctx)
-
-	// The method should complete without panicking
-	// We can't easily test the exact behavior without mocking, but we can ensure it runs
-	// Check if any port state was captured
-	pm.mu.Lock()
-	lastPorts := pm.lastPorts
-	pm.mu.Unlock()
-
-	t.Logf("Captured port state (length %d):", len(lastPorts))
-	if len(lastPorts) > 0 {
-		t.Logf("First 200 chars: %s", lastPorts[:min(200, len(lastPorts))])
-	}
-}
-
-func min(a, b int) int {
-	if a < b {
-		return a
-	}
-	return b
-}
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 7f31401..b6e2259 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -307,44 +307,6 @@
 		io.WriteString(w, "{}\n")
 	})
 
-	// Handler for /port-events - returns recent port change events
-	s.mux.HandleFunc("/port-events", func(w http.ResponseWriter, r *http.Request) {
-		if r.Method != http.MethodGet {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
-			return
-		}
-
-		w.Header().Set("Content-Type", "application/json")
-
-		// Get the 'since' query parameter for filtering events
-		sinceParam := r.URL.Query().Get("since")
-		var events []loop.PortEvent
-
-		// Get port monitor from agent
-		portMonitor := agent.GetPortMonitor()
-		if portMonitor == nil {
-			// Return empty array if port monitor not available
-			events = []loop.PortEvent{}
-		} else if sinceParam != "" {
-			// Parse the since timestamp
-			sinceTime, err := time.Parse(time.RFC3339, sinceParam)
-			if err != nil {
-				http.Error(w, fmt.Sprintf("Invalid 'since' timestamp format: %v", err), http.StatusBadRequest)
-				return
-			}
-			events = portMonitor.GetRecentEvents(sinceTime)
-		} else {
-			// Return all recent events
-			events = portMonitor.GetAllRecentEvents()
-		}
-
-		// Encode and return the events
-		if err := json.NewEncoder(w).Encode(events); err != nil {
-			slog.ErrorContext(r.Context(), "Error encoding port events response", slog.Any("err", err))
-			http.Error(w, "Internal server error", http.StatusInternalServerError)
-		}
-	})
-
 	// Handler for /messages?start=N&end=M (start/end are optional)
 	s.mux.HandleFunc("/messages", func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "application/json")
diff --git a/loop/server/loophttp_test.go b/loop/server/loophttp_test.go
index adda4e3..f6ec8c7 100644
--- a/loop/server/loophttp_test.go
+++ b/loop/server/loophttp_test.go
@@ -3,7 +3,6 @@
 import (
 	"bufio"
 	"context"
-	"encoding/json"
 	"net/http"
 	"net/http/httptest"
 	"slices"
@@ -261,10 +260,9 @@
 	m.retryNumber++
 }
 
-func (m *mockAgent) GetPortMonitor() *loop.PortMonitor { return loop.NewPortMonitor() }
-func (m *mockAgent) SkabandAddr() string               { return m.skabandAddr }
-func (m *mockAgent) LinkToGitHub() bool                { return false }
-func (m *mockAgent) DiffStats() (int, int)             { return 0, 0 }
+func (m *mockAgent) SkabandAddr() string   { return m.skabandAddr }
+func (m *mockAgent) LinkToGitHub() bool    { return false }
+func (m *mockAgent) DiffStats() (int, int) { return 0, 0 }
 
 // TestSSEStream tests the SSE stream endpoint
 func TestSSEStream(t *testing.T) {
@@ -512,76 +510,6 @@
 	t.Log("Mock CompactConversation works correctly")
 }
 
-// TestPortEventsEndpoint tests the /port-events HTTP endpoint
-func TestPortEventsEndpoint(t *testing.T) {
-	// Create a mock agent that implements the CodingAgent interface
-	agent := &mockAgent{
-		branchPrefix: "sketch/",
-	}
-
-	// Create a server with the mock agent
-	server, err := server.New(agent, nil)
-	if err != nil {
-		t.Fatalf("Failed to create server: %v", err)
-	}
-
-	// Test GET /port-events
-	req, err := http.NewRequest("GET", "/port-events", nil)
-	if err != nil {
-		t.Fatalf("Failed to create request: %v", err)
-	}
-
-	rr := httptest.NewRecorder()
-	server.ServeHTTP(rr, req)
-
-	// Should return 200 OK
-	if status := rr.Code; status != http.StatusOK {
-		t.Errorf("Expected status code %d, got %d", http.StatusOK, status)
-	}
-
-	// Should return JSON content type
-	contentType := rr.Header().Get("Content-Type")
-	if contentType != "application/json" {
-		t.Errorf("Expected Content-Type application/json, got %s", contentType)
-	}
-
-	// Should return valid JSON (empty array since mock returns no events)
-	var events []any
-	if err := json.Unmarshal(rr.Body.Bytes(), &events); err != nil {
-		t.Errorf("Failed to parse JSON response: %v", err)
-	}
-
-	// Should be empty array for mock agent
-	if len(events) != 0 {
-		t.Errorf("Expected empty events array, got %d events", len(events))
-	}
-}
-
-// TestPortEventsEndpointMethodNotAllowed tests that non-GET requests are rejected
-func TestPortEventsEndpointMethodNotAllowed(t *testing.T) {
-	agent := &mockAgent{
-		branchPrefix: "sketch/",
-	}
-	server, err := server.New(agent, nil)
-	if err != nil {
-		t.Fatalf("Failed to create server: %v", err)
-	}
-
-	// Test POST /port-events (should be rejected)
-	req, err := http.NewRequest("POST", "/port-events", nil)
-	if err != nil {
-		t.Fatalf("Failed to create request: %v", err)
-	}
-
-	rr := httptest.NewRecorder()
-	server.ServeHTTP(rr, req)
-
-	// Should return 405 Method Not Allowed
-	if status := rr.Code; status != http.StatusMethodNotAllowed {
-		t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, status)
-	}
-}
-
 func TestParsePortProxyHost(t *testing.T) {
 	tests := []struct {
 		name     string