Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions backend/internal/httpd/controllers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions backend/internal/httpd/controllers/sessions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down