diff --git a/AGENTS.md b/AGENTS.md index 662bb828d1..3cc293b0aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ npm run typecheck npm run build ``` -When showing or demoing frontend changes, run `ao preview [url]` from inside the session so the change renders in the desktop browser panel (the inspector rail's Browser tab); do not just describe it. +When showing or demoing frontend changes, run `ao preview [url]` from inside the session so the change renders in the desktop browser panel (the inspector rail's Browser tab); do not just describe it. Do not open local preview URLs with the OS browser, `open`/`xdg-open`, or headless browser tooling (Playwright, etc.) — only fall back to those if the user explicitly asks, or `ao preview` returns an error you report. `ao preview` only has a Browser tab to render into from a **worker** session; it has no effect from an orchestrator session. ## Where to look first diff --git a/backend/internal/httpd/controllers/sessions.go b/backend/internal/httpd/controllers/sessions.go index 9e2977eb94..16c2f22020 100644 --- a/backend/internal/httpd/controllers/sessions.go +++ b/backend/internal/httpd/controllers/sessions.go @@ -245,6 +245,13 @@ func (c *SessionsController) setPreview(w http.ResponseWriter, r *http.Request) envelope.WriteError(w, r, err) return } + // Orchestrator sessions have no inspector rail in the desktop app, so a + // preview write here would silently succeed with nothing visible for it. + // Reject loudly instead of storing a target no one can see. + if sess.Kind == domain.KindOrchestrator { + envelope.WriteAPIError(w, r, http.StatusBadRequest, "bad_request", "PREVIEW_UNSUPPORTED_SESSION_KIND", "Orchestrator sessions have no browser preview panel; run ao preview from a worker session", nil) + return + } // ponytail: no URL sanitization on preview target; agent-trusted for now previewURL := strings.TrimSpace(in.URL) if previewURL == "" { @@ -287,6 +294,15 @@ func (c *SessionsController) clearPreview(w http.ResponseWriter, r *http.Request apispec.NotImplemented(w, r, "DELETE", "/api/v1/sessions/{sessionId}/preview") return } + sess, err := c.Svc.Get(r.Context(), sessionID(r)) + if err != nil { + envelope.WriteError(w, r, err) + return + } + if sess.Kind == domain.KindOrchestrator { + envelope.WriteAPIError(w, r, http.StatusBadRequest, "bad_request", "PREVIEW_UNSUPPORTED_SESSION_KIND", "Orchestrator sessions have no browser preview panel; run ao preview from a worker session", nil) + return + } updated, err := c.Svc.SetPreview(r.Context(), sessionID(r), "") if err != nil { envelope.WriteError(w, r, err) diff --git a/backend/internal/httpd/controllers/sessions_test.go b/backend/internal/httpd/controllers/sessions_test.go index ae0fe53daf..c7a89248ec 100644 --- a/backend/internal/httpd/controllers/sessions_test.go +++ b/backend/internal/httpd/controllers/sessions_test.go @@ -406,6 +406,35 @@ func TestSessionsAPI_SetPreviewExplicitURLPersists(t *testing.T) { } } +func TestSessionsAPI_SetPreviewRejectsOrchestratorSession(t *testing.T) { + svc := newFakeSessionService() + s := svc.sessions["ao-1"] + s.Kind = domain.KindOrchestrator + svc.sessions["ao-1"] = s + srv := newSessionTestServer(t, svc) + + body, status, _ := doRequest(t, srv, "POST", "/api/v1/sessions/ao-1/preview", `{"url":"http://localhost:5173/"}`) + assertErrorCode(t, body, status, http.StatusBadRequest, "PREVIEW_UNSUPPORTED_SESSION_KIND") + if got := svc.sessions["ao-1"].Metadata.PreviewURL; got != "" { + t.Fatalf("persisted previewUrl = %q, want no write on rejected orchestrator preview", got) + } +} + +func TestSessionsAPI_ClearPreviewRejectsOrchestratorSession(t *testing.T) { + svc := newFakeSessionService() + s := svc.sessions["ao-1"] + s.Kind = domain.KindOrchestrator + s.Metadata = domain.SessionMetadata{PreviewURL: "http://localhost:5173/"} + svc.sessions["ao-1"] = s + srv := newSessionTestServer(t, svc) + + body, status, _ := doRequest(t, srv, "DELETE", "/api/v1/sessions/ao-1/preview", "") + assertErrorCode(t, body, status, http.StatusBadRequest, "PREVIEW_UNSUPPORTED_SESSION_KIND") + if got := svc.sessions["ao-1"].Metadata.PreviewURL; got != "http://localhost:5173/" { + t.Fatalf("persisted previewUrl = %q, want unchanged on rejected orchestrator clear", got) + } +} + func TestSessionsAPI_SetPreviewEmptyURLAutodetectsIndex(t *testing.T) { svc := newFakeSessionService() workspace := t.TempDir()