diff --git a/agent/assistant/sandbox_v2.go b/agent/assistant/sandbox_v2.go index a8026ae9..5214ade3 100644 --- a/agent/assistant/sandbox_v2.go +++ b/agent/assistant/sandbox_v2.go @@ -48,7 +48,10 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) return nil, nil, nil, "", fmt.Errorf("get connector: %w", err) } - // 2. Obtain Computer (passes connector for OPENAI_PROXY_* env injection). + // 2. Build human-readable DisplayName from real Agent name + Workspace name. + cfg.DisplayName = buildBoxDisplayName(ctx, ast.ID, ast.Name) + + // 3. Obtain Computer (passes connector for OPENAI_PROXY_* env injection). computer, identifier, err := sandboxv2.GetComputer(ctx, cfg, manager, conn) if err != nil { closeLoadingV2(ctx, loadingMsgID, "sandbox.failed") @@ -56,7 +59,7 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) } _ = identifier - // 3. Get Runner. + // 4. Get Runner. runner, err := sandboxv2.Get(cfg.Runner.Name) if err != nil { sandboxv2.LifecycleAction(stdCtx, cfg, computer, manager) @@ -64,7 +67,7 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) return nil, nil, nil, "", fmt.Errorf("get runner %q: %w", cfg.Runner.Name, err) } - // 4. Resolve skills directory. + // 5. Resolve skills directory. skillsDir := "" if ast.Path != "" { dir := filepath.Join(config.Conf.AppSource, ast.Path, "skills") @@ -73,7 +76,7 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) } } - // 5. Convert MCP servers. + // 6. Convert MCP servers. var mcpServers []sandboxTypes.MCPServer if ast.MCP != nil { for _, s := range ast.MCP.Servers { @@ -85,7 +88,7 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) } } - // 6. Runner.Prepare (standard context). + // 7. Runner.Prepare (standard context). err = runner.Prepare(stdCtx, &sandboxTypes.PrepareRequest{ Computer: computer, Config: cfg, @@ -199,6 +202,34 @@ func (ast *Assistant) initStandaloneWorkspace(ctx *context.Context) { ctx.SetWorkspace(wsFS) } +// buildBoxDisplayName constructs a human-readable display name for a Box +// using the locale-resolved Agent name and Workspace name (matching the UI list pages). +func buildBoxDisplayName(ctx *context.Context, assistantID, rawName string) string { + agentName := i18n.Tr(assistantID, ctx.Locale, rawName) + + wsName := "" + if ctx.Metadata != nil { + if wsID, ok := ctx.Metadata["workspace_id"].(string); ok && wsID != "" { + if wsm := workspace.M(); wsm != nil { + if ws, err := wsm.Get(ctx.Context, wsID); err == nil && ws != nil { + wsName = ws.Name + } + } + } + } + + if agentName != "" && wsName != "" { + return agentName + " / " + wsName + } + if agentName != "" { + return agentName + } + if wsName != "" { + return wsName + } + return "" +} + func closeLoadingV2(ctx *context.Context, loadingMsgID, msgKey string) { if loadingMsgID == "" || ctx == nil { return diff --git a/agent/sandbox/v2/lifecycle.go b/agent/sandbox/v2/lifecycle.go index d2f28800..bad0b57c 100644 --- a/agent/sandbox/v2/lifecycle.go +++ b/agent/sandbox/v2/lifecycle.go @@ -17,7 +17,7 @@ import ( // BuildIdentifier determines the Computer identifier based on lifecycle policy // and optional metadata override. Returns "" for oneshot (always new). -func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID string, metadata map[string]any) string { +func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID, workspaceID string, metadata map[string]any) string { if cfg.Lifecycle == "oneshot" { return "" } @@ -25,7 +25,7 @@ func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID stri // Custom identifier from metadata takes precedence. if metadata != nil { if cid, ok := metadata["computer_id"].(string); ok && cid != "" { - return fmt.Sprintf("%s-%s", ownerID, cid) + return fmt.Sprintf("%s-%s.%s", ownerID, cid, workspaceID) } } @@ -33,7 +33,7 @@ func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID stri case "session": return fmt.Sprintf("%s-%s", ownerID, chatID) case "longrunning", "persistent": - return fmt.Sprintf("%s-%s", ownerID, assistantID) + return fmt.Sprintf("%s-%s.%s", ownerID, assistantID, workspaceID) default: return "" } @@ -44,11 +44,6 @@ func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID stri // Returns the Computer, the resolved identifier, and any error. func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *infra.Manager, conn ...connector.Connector) (infra.Computer, string, error) { ownerID := resolveOwnerID(ctx) - identifier := BuildIdentifier(cfg, ownerID, ctx.ChatID, ctx.AssistantID, ctx.Metadata) - - // Fill runtime fields. - cfg.Owner = ownerID - cfg.ID = identifier workspaceID := "" if ctx.Metadata != nil { @@ -59,6 +54,12 @@ func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *i if workspaceID == "" { workspaceID = ownerID } + + identifier := BuildIdentifier(cfg, ownerID, ctx.ChatID, ctx.AssistantID, workspaceID, ctx.Metadata) + + // Fill runtime fields. + cfg.Owner = ownerID + cfg.ID = identifier cfg.WorkspaceID = workspaceID // Resolve computer_id from metadata to determine kind and nodeID. diff --git a/agent/sandbox/v2/lifecycle_test.go b/agent/sandbox/v2/lifecycle_test.go index 64cf2a81..4d1afe4d 100644 --- a/agent/sandbox/v2/lifecycle_test.go +++ b/agent/sandbox/v2/lifecycle_test.go @@ -20,7 +20,7 @@ import ( func TestBuildIdentifier_Oneshot(t *testing.T) { cfg := &types.SandboxConfig{Lifecycle: "oneshot"} - id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast1", nil) + id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast1", "ws1", nil) if id != "" { t.Errorf("oneshot should return empty, got %q", id) } @@ -28,7 +28,7 @@ func TestBuildIdentifier_Oneshot(t *testing.T) { func TestBuildIdentifier_Session(t *testing.T) { cfg := &types.SandboxConfig{Lifecycle: "session"} - id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat42", "ast1", nil) + id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat42", "ast1", "ws1", nil) if id != "owner1-chat42" { t.Errorf("session: got %q, want %q", id, "owner1-chat42") } @@ -36,33 +36,33 @@ func TestBuildIdentifier_Session(t *testing.T) { func TestBuildIdentifier_Longrunning(t *testing.T) { cfg := &types.SandboxConfig{Lifecycle: "longrunning"} - id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast99", nil) - if id != "owner1-ast99" { - t.Errorf("longrunning: got %q, want %q", id, "owner1-ast99") + id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast99", "ws1", nil) + if id != "owner1-ast99.ws1" { + t.Errorf("longrunning: got %q, want %q", id, "owner1-ast99.ws1") } } func TestBuildIdentifier_Persistent(t *testing.T) { cfg := &types.SandboxConfig{Lifecycle: "persistent"} - id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast99", nil) - if id != "owner1-ast99" { - t.Errorf("persistent: got %q, want %q", id, "owner1-ast99") + id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast99", "ws1", nil) + if id != "owner1-ast99.ws1" { + t.Errorf("persistent: got %q, want %q", id, "owner1-ast99.ws1") } } func TestBuildIdentifier_MetadataOverride(t *testing.T) { cfg := &types.SandboxConfig{Lifecycle: "session"} meta := map[string]any{"computer_id": "custom-box"} - id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast1", meta) - if id != "owner1-custom-box" { - t.Errorf("metadata override: got %q, want %q", id, "owner1-custom-box") + id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast1", "ws1", meta) + if id != "owner1-custom-box.ws1" { + t.Errorf("metadata override: got %q, want %q", id, "owner1-custom-box.ws1") } } func TestBuildIdentifier_MetadataEmptyIgnored(t *testing.T) { cfg := &types.SandboxConfig{Lifecycle: "session"} meta := map[string]any{"computer_id": ""} - id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat42", "ast1", meta) + id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat42", "ast1", "ws1", meta) if id != "owner1-chat42" { t.Errorf("empty metadata should fall through to session, got %q", id) } @@ -70,7 +70,7 @@ func TestBuildIdentifier_MetadataEmptyIgnored(t *testing.T) { func TestBuildIdentifier_UnknownLifecycle(t *testing.T) { cfg := &types.SandboxConfig{Lifecycle: "unknown"} - id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast1", nil) + id := sandboxv2.BuildIdentifier(cfg, "owner1", "chat1", "ast1", "ws1", nil) if id != "" { t.Errorf("unknown lifecycle should return empty, got %q", id) } diff --git a/agent/sandbox/v2/options.go b/agent/sandbox/v2/options.go index 2ceb3392..2ac6ce92 100644 --- a/agent/sandbox/v2/options.go +++ b/agent/sandbox/v2/options.go @@ -34,6 +34,7 @@ func BuildCreateOptions(cfg *types.SandboxConfig, identifier, ownerID, workspace MountMode: cfg.Computer.MountMode, WorkspaceID: workspaceID, Labels: cfg.Labels, + DisplayName: cfg.DisplayName, } if opts.Labels == nil { diff --git a/agent/sandbox/v2/types/config.go b/agent/sandbox/v2/types/config.go index 9eab224e..d5ec1196 100644 --- a/agent/sandbox/v2/types/config.go +++ b/agent/sandbox/v2/types/config.go @@ -32,6 +32,7 @@ type SandboxConfig struct { NodeID string `json:"-" yaml:"-"` Kind string `json:"-" yaml:"-"` WorkspaceID string `json:"-" yaml:"-"` + DisplayName string `json:"-" yaml:"-"` } // ComputerFilter defines the query parameters for GET /computer/options. diff --git a/openapi/computer/computer.go b/openapi/computer/computer.go index e0c80837..93016184 100644 --- a/openapi/computer/computer.go +++ b/openapi/computer/computer.go @@ -45,7 +45,6 @@ type computerOption struct { Image string `json:"image,omitempty"` Policy string `json:"policy,omitempty"` VNC bool `json:"vnc"` - Labels map[string]string `json:"labels,omitempty"` System computerSystemInfo `json:"system"` } @@ -255,7 +254,10 @@ func boxToOption(b *sandboxv2.Box) computerOption { snap := b.Snapshot() info := b.ComputerInfo() - displayName := info.System.Hostname + displayName := info.DisplayName + if displayName == "" { + displayName = info.System.Hostname + } if displayName == "" { displayName = snap.ID } @@ -285,7 +287,6 @@ func boxToOption(b *sandboxv2.Box) computerOption { Image: snap.Image, Policy: string(snap.Policy), VNC: snap.VNC, - Labels: snap.Labels, System: computerSystemInfo{ OS: info.System.OS, Arch: info.System.Arch, diff --git a/openapi/sandbox/manage.go b/openapi/sandbox/manage.go index 270c3d62..316d1c58 100644 --- a/openapi/sandbox/manage.go +++ b/openapi/sandbox/manage.go @@ -94,7 +94,6 @@ type sandboxResponse struct { Owner string `json:"owner"` Status string `json:"status"` Policy string `json:"policy,omitempty"` - Labels map[string]string `json:"labels,omitempty"` Image string `json:"image,omitempty"` Mode string `json:"mode,omitempty"` Addr string `json:"addr,omitempty"` @@ -110,7 +109,10 @@ func boxToResponse(b *sandboxv2.Box) sandboxResponse { snap := b.Snapshot() info := b.ComputerInfo() - displayName := info.System.Hostname + displayName := info.DisplayName + if displayName == "" { + displayName = info.System.Hostname + } if displayName == "" { displayName = snap.ID } @@ -137,7 +139,6 @@ func boxToResponse(b *sandboxv2.Box) sandboxResponse { Owner: snap.Owner, Status: snap.Status, Policy: string(snap.Policy), - Labels: snap.Labels, Image: snap.Image, Mode: mode, Addr: addr, diff --git a/sandbox/v2/box.go b/sandbox/v2/box.go index aed45d11..d8412014 100644 --- a/sandbox/v2/box.go +++ b/sandbox/v2/box.go @@ -30,6 +30,7 @@ type Box struct { image string workspaceID string system SystemInfo + displayName string workDir string ws taiworkspace.FS manager *Manager @@ -56,6 +57,7 @@ func (b *Box) ComputerInfo() ComputerInfo { Image: b.image, Policy: b.policy, Labels: b.labels, + DisplayName: b.displayName, } } diff --git a/sandbox/v2/manager.go b/sandbox/v2/manager.go index 3306d95d..4f0fe5aa 100644 --- a/sandbox/v2/manager.go +++ b/sandbox/v2/manager.go @@ -3,7 +3,10 @@ package sandbox import ( "context" "fmt" + "log" "path/filepath" + goruntime "runtime" + "strconv" "sync" "time" @@ -172,7 +175,9 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error) return nil, fmt.Errorf("sandbox: node %q has no container runtime", nodeID) } - taiOpts := m.buildTaiCreateOptions(opts, nodeID, id) + sys := inferSystemInfo(ctx, res, opts.Image) + + taiOpts := m.buildTaiCreateOptions(opts, nodeID, id, sys) containerID, err := res.Runtime.Create(ctx, taiOpts) if err != nil { @@ -189,16 +194,6 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error) policy = Session } - sys := SystemInfo{ - OS: res.System.OS, - Arch: res.System.Arch, - Hostname: res.System.Hostname, - NumCPU: res.System.NumCPU, - TotalMem: res.System.TotalMem, - Shell: res.System.Shell, - TempDir: res.System.TempDir, - } - boxWorkDir := opts.WorkDir if boxWorkDir == "" { boxWorkDir = "/workspace" @@ -220,6 +215,7 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error) image: opts.Image, workspaceID: opts.WorkspaceID, workDir: boxWorkDir, + displayName: opts.DisplayName, system: sys, } box.lastCall.Store(time.Now().UnixMilli()) @@ -348,7 +344,7 @@ func (m *Manager) getNode(name string) (*tai.ConnResources, error) { return res, nil } -func (m *Manager) buildTaiCreateOptions(opts CreateOptions, nodeID, sandboxID string) tairuntime.CreateOptions { +func (m *Manager) buildTaiCreateOptions(opts CreateOptions, nodeID, sandboxID string, sys SystemInfo) tairuntime.CreateOptions { env := make(map[string]string) reg := registry.Global() @@ -378,6 +374,27 @@ func (m *Manager) buildTaiCreateOptions(opts CreateOptions, nodeID, sandboxID st if opts.WorkspaceID != "" { labels["workspace-id"] = opts.WorkspaceID } + if opts.DisplayName != "" { + labels["sandbox-display-name"] = opts.DisplayName + } + if sys.OS != "" { + labels["sandbox-sys-os"] = sys.OS + } + if sys.Arch != "" { + labels["sandbox-sys-arch"] = sys.Arch + } + if sys.Hostname != "" { + labels["sandbox-sys-hostname"] = sys.Hostname + } + if sys.NumCPU > 0 { + labels["sandbox-sys-numcpu"] = strconv.Itoa(sys.NumCPU) + } + if sys.TotalMem > 0 { + labels["sandbox-sys-totalmem"] = strconv.FormatInt(sys.TotalMem, 10) + } + if sys.Shell != "" { + labels["sandbox-sys-shell"] = sys.Shell + } for k, v := range opts.Labels { labels[k] = v } @@ -467,6 +484,10 @@ func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, res *tai.Conn } } } + sys := systemInfoFromLabels(c.Labels) + if sys.OS == "" { + sys = inferSystemInfo(ctx, res, c.Image) + } box := &Box{ id: sandboxID, containerID: cid, @@ -479,6 +500,8 @@ func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, res *tai.Conn workspaceID: c.Labels["workspace-id"], vnc: hasVNC, workDir: "/workspace", + displayName: c.Labels["sandbox-display-name"], + system: sys, manager: m, } box.lastCall.Store(time.Now().UnixMilli()) @@ -486,6 +509,51 @@ func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, res *tai.Conn } } +// inferSystemInfo derives static SystemInfo for a container from image metadata +// and Tai host resources. OS/Arch/Shell come from the image; Hostname/NumCPU/TotalMem +// come from the Tai host. +func inferSystemInfo(ctx context.Context, res *tai.ConnResources, imageRef string) SystemInfo { + sys := SystemInfo{ + Hostname: res.System.Hostname, + NumCPU: res.System.NumCPU, + TotalMem: res.System.TotalMem, + } + + if res.Image != nil { + meta, err := res.Image.Inspect(ctx, imageRef) + if err != nil { + log.Printf("[sandbox/v2] image inspect %q: %v (using fallback)", imageRef, err) + } + if meta != nil { + sys.OS = meta.OS + sys.Arch = meta.Arch + sys.Shell = meta.Shell + return sys + } + } + + sys.OS = "linux" + sys.Arch = goruntime.GOARCH + sys.Shell = "bash" + return sys +} + +// systemInfoFromLabels restores SystemInfo from Docker container labels that +// were persisted at creation time, so recovery doesn't depend on the Tai node +// being connected. +func systemInfoFromLabels(labels map[string]string) SystemInfo { + numCPU, _ := strconv.Atoi(labels["sandbox-sys-numcpu"]) + totalMem, _ := strconv.ParseInt(labels["sandbox-sys-totalmem"], 10, 64) + return SystemInfo{ + OS: labels["sandbox-sys-os"], + Arch: labels["sandbox-sys-arch"], + Hostname: labels["sandbox-sys-hostname"], + NumCPU: numCPU, + TotalMem: totalMem, + Shell: labels["sandbox-sys-shell"], + } +} + // ImageExists reports whether the given image ref exists on the target node. func (m *Manager) ImageExists(ctx context.Context, nodeID, ref string) (bool, error) { res, err := m.getNode(nodeID) diff --git a/sandbox/v2/types.go b/sandbox/v2/types.go index bc78c85a..828ef799 100644 --- a/sandbox/v2/types.go +++ b/sandbox/v2/types.go @@ -44,6 +44,7 @@ type ComputerInfo struct { Image string Policy LifecyclePolicy Labels map[string]string + DisplayName string } // SystemInfo describes the hardware and environment of a Tai node. @@ -104,6 +105,7 @@ type CreateOptions struct { WorkspaceID string MountMode string MountPath string + DisplayName string } type ListOptions struct { diff --git a/tai/runtime/image.go b/tai/runtime/image.go index 45e32209..e18b0e27 100644 --- a/tai/runtime/image.go +++ b/tai/runtime/image.go @@ -8,11 +8,20 @@ import ( // Image manages container images on a runtime node. type Image interface { Exists(ctx context.Context, ref string) (bool, error) + Inspect(ctx context.Context, ref string) (*ImageMeta, error) Pull(ctx context.Context, ref string, opts PullOptions) (<-chan PullProgress, error) Remove(ctx context.Context, ref string, force bool) error List(ctx context.Context) ([]ImageInfo, error) } +// ImageMeta holds static metadata extracted from a container image. +type ImageMeta struct { + OS string // "linux", "windows" + Arch string // "amd64", "arm64" + Shell string // preferred shell: "bash", "sh", "cmd.exe", "pwsh" + WorkDir string // default working directory from Dockerfile WORKDIR +} + // PullOptions configures an image pull operation. type PullOptions struct { Auth *RegistryAuth // nil = anonymous / public diff --git a/tai/runtime/image_docker.go b/tai/runtime/image_docker.go index 09b3edb9..4014bde1 100644 --- a/tai/runtime/image_docker.go +++ b/tai/runtime/image_docker.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "time" "github.com/docker/docker/api/types/image" @@ -35,6 +36,44 @@ func (d *dockerImage) Exists(ctx context.Context, ref string) (bool, error) { return true, nil } +func (d *dockerImage) Inspect(ctx context.Context, ref string) (*ImageMeta, error) { + inspect, _, err := d.cli.ImageInspectWithRaw(ctx, ref) + if err != nil { + return nil, fmt.Errorf("image inspect %q: %w", ref, err) + } + + meta := &ImageMeta{ + OS: inspect.Os, + Arch: inspect.Architecture, + } + + if inspect.Config != nil { + meta.WorkDir = inspect.Config.WorkingDir + + if len(inspect.Config.Shell) > 0 { + meta.Shell = inspect.Config.Shell[0] + } + if meta.Shell == "" { + for _, e := range inspect.Config.Env { + if strings.HasPrefix(e, "SHELL=") { + meta.Shell = e[6:] + break + } + } + } + } + + if meta.Shell == "" { + if strings.EqualFold(meta.OS, "windows") { + meta.Shell = "cmd.exe" + } else { + meta.Shell = "bash" + } + } + + return meta, nil +} + func (d *dockerImage) Pull(ctx context.Context, ref string, opts PullOptions) (<-chan PullProgress, error) { pullOpts := image.PullOptions{} if opts.Auth != nil { diff --git a/tai/runtime/image_k8s.go b/tai/runtime/image_k8s.go index bd81a425..2a22952b 100644 --- a/tai/runtime/image_k8s.go +++ b/tai/runtime/image_k8s.go @@ -12,6 +12,10 @@ func (k *k8sImage) Exists(_ context.Context, _ string) (bool, error) { return true, nil } +func (k *k8sImage) Inspect(_ context.Context, _ string) (*ImageMeta, error) { + return nil, nil +} + func (k *k8sImage) Pull(_ context.Context, _ string, _ PullOptions) (<-chan PullProgress, error) { return nil, nil } diff --git a/tai/tunnel/forward.go b/tai/tunnel/forward.go index 1713de90..81ee5896 100644 --- a/tai/tunnel/forward.go +++ b/tai/tunnel/forward.go @@ -113,17 +113,26 @@ func resolveTargetPort(c *gin.Context, node *types.NodeMeta) int { return 0 } -// rewriteRequest clones the request and strips everything up to and including -// /tai/:taiID from the path, handling any baseURL prefix (e.g. /v1/tai/abc/proxy/x → /proxy/x). +// rewriteRequest clones the request and strips the Yao-side route prefix, +// leaving only what the Tai-side handler expects. +// +// The Tai httpproxy expects /{containerID}:{port}/..., so the /proxy prefix +// is stripped. The Tai VNC router expects /vnc/{containerID}/ws, so the /vnc +// prefix is kept. +// +// /v1/tai/abc/proxy/cid:8080/foo → /cid:8080/foo +// /v1/tai/abc/vnc/cid/ws → /vnc/cid/ws func rewriteRequest(orig *http.Request, taiID string) *http.Request { r := orig.Clone(orig.Context()) marker := "/tai/" + taiID if idx := strings.Index(r.URL.Path, marker); idx >= 0 { - r.URL.Path = r.URL.Path[idx+len(marker):] - if r.URL.Path == "" { - r.URL.Path = "/" + rest := r.URL.Path[idx+len(marker):] + rest = strings.TrimPrefix(rest, "/proxy") + if rest == "" { + rest = "/" } + r.URL.Path = rest } r.RequestURI = r.URL.RequestURI() diff --git a/tai/tunnel/forward_test.go b/tai/tunnel/forward_test.go index 3259cc67..703e5198 100644 --- a/tai/tunnel/forward_test.go +++ b/tai/tunnel/forward_test.go @@ -84,8 +84,8 @@ func TestRewriteRequest(t *testing.T) { "proxy_path", "/tai/abc123/proxy/api/v1/data", "abc123", - "/proxy/api/v1/data", - "/proxy/api/v1/data", + "/api/v1/data", + "/api/v1/data", }, { "vnc_path", @@ -98,8 +98,8 @@ func TestRewriteRequest(t *testing.T) { "with_query", "/tai/node-1/proxy/api?foo=bar", "node-1", - "/proxy/api", - "/proxy/api?foo=bar", + "/api", + "/api?foo=bar", }, { "exact_prefix", @@ -112,8 +112,8 @@ func TestRewriteRequest(t *testing.T) { "with_base_url", "/v1/tai/node-1/proxy/api/v1/data", "node-1", - "/proxy/api/v1/data", - "/proxy/api/v1/data", + "/api/v1/data", + "/api/v1/data", }, { "with_base_url_vnc", @@ -122,6 +122,13 @@ func TestRewriteRequest(t *testing.T) { "/vnc/__host__/ws", "/vnc/__host__/ws", }, + { + "proxy_container_port", + "/v1/tai/abc/proxy/cid123:8080/foo", + "abc", + "/cid123:8080/foo", + "/cid123:8080/foo", + }, { "no_match", "/other/path",