Drop per-task port blocks; previews use any free loopback port
This commit is contained in:
+34
-18
@@ -22,10 +22,6 @@ import (
|
||||
|
||||
const maxPreviewName = 40
|
||||
|
||||
func portEnv(t api.Task) []string {
|
||||
return []string{"PORT=" + strconv.Itoa(t.PortBase)}
|
||||
}
|
||||
|
||||
type exposePortArgs struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
@@ -35,7 +31,7 @@ func (m *Manager) exposePortTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "expose_port",
|
||||
Description: "Show the human a server you are running, such as a dev server, as a link above the task's tabs that opens it in a new window. " +
|
||||
"Start the server yourself first (in the background), listening on 127.0.0.1 (not 0.0.0.0 or localhost) on a port in this task's block. " +
|
||||
"Start the server yourself first (in the background), listening on 127.0.0.1 (not 0.0.0.0 or localhost) on any free port. " +
|
||||
"byre then serves it on the same port of byre's tailnet node, and the human's app opens it through a local link. " +
|
||||
"name labels it in the tab (\"web\", \"storybook\"); exposing a name or port again replaces the earlier one. " +
|
||||
"The server stops when your process does (a byre restart, compaction, the task closing); the human sees it as unreachable until you start it again.",
|
||||
@@ -43,7 +39,7 @@ func (m *Manager) exposePortTool(l *live) mcp.Tool {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Short label, shown as the link's text."},
|
||||
"port": {"type": "integer", "description": "The port the server listens on, at 127.0.0.1, within this task's block."}
|
||||
"port": {"type": "integer", "description": "The port the server listens on, at 127.0.0.1."}
|
||||
},
|
||||
"required": ["name", "port"]
|
||||
}`),
|
||||
@@ -65,13 +61,8 @@ func (m *Manager) exposePort(ctx context.Context, l *live, raw json.RawMessage)
|
||||
return "", fmt.Errorf("name must be one non-empty line of at most %d characters", maxPreviewName)
|
||||
}
|
||||
|
||||
task, err := m.store.GetTask(ctx, l.taskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
last := task.PortBase + store.PortBlock - 1
|
||||
if args.Port < task.PortBase || args.Port > last {
|
||||
return "", fmt.Errorf("port %d is outside this task's block, %d to %d", args.Port, task.PortBase, last)
|
||||
if args.Port < 1 || args.Port > 65535 {
|
||||
return "", fmt.Errorf("port %d is not a TCP port", args.Port)
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(args.Port))
|
||||
@@ -81,14 +72,23 @@ func (m *Manager) exposePort(ctx context.Context, l *live, raw json.RawMessage)
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
err = m.servePreview(args.Port)
|
||||
if err != nil {
|
||||
return "", err
|
||||
m.exposeMu.Lock()
|
||||
defer m.exposeMu.Unlock()
|
||||
m.previewMu.Lock()
|
||||
serving := m.previewListeners[args.Port] != nil
|
||||
m.previewMu.Unlock()
|
||||
if !serving {
|
||||
err = m.servePreview(args.Port)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
dropped, err := m.store.SetPreview(ctx, api.Preview{TaskId: task.Id, Name: name, Port: args.Port, CreatedAt: time.Now().UTC()}, agentActor)
|
||||
dropped, err := m.store.SetPreview(ctx, api.Preview{TaskId: l.taskID, Name: name, Port: args.Port, CreatedAt: time.Now().UTC()}, agentActor)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
m.stopPreview(args.Port)
|
||||
if !serving {
|
||||
m.stopPreview(args.Port)
|
||||
}
|
||||
return "", errors.New("this task is no longer active")
|
||||
}
|
||||
if err != nil {
|
||||
@@ -263,12 +263,28 @@ func (m *Manager) recoverPreviews(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (m *Manager) Unexpose(ctx context.Context, taskID string) {
|
||||
m.exposeMu.Lock()
|
||||
defer m.exposeMu.Unlock()
|
||||
previews, err := m.store.ListPreviews(ctx, taskID)
|
||||
if err != nil {
|
||||
slog.Error("list previews", "task", taskID, "err", err)
|
||||
return
|
||||
}
|
||||
active, err := m.store.ActivePreviews(ctx)
|
||||
if err != nil {
|
||||
slog.Error("list active previews", "err", err)
|
||||
return
|
||||
}
|
||||
held := map[int]bool{}
|
||||
for _, p := range active {
|
||||
if p.TaskId != taskID {
|
||||
held[p.Port] = true
|
||||
}
|
||||
}
|
||||
for _, p := range previews {
|
||||
if held[p.Port] {
|
||||
continue
|
||||
}
|
||||
m.stopPreview(p.Port)
|
||||
slog.Info("stopped serving preview", "task", taskID, "name", p.Name, "port", p.Port)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user