portal

package
v0.22.49 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 30, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package portal — Ask orchestrator (ADR-018).

Spawns Obscura's CDP server, attaches via chromedp's RemoteAllocator, seeds cookies + extra headers, navigates, runs the saved login_check / ready_predicate, fills the input selector with the prompt, clicks submit (or dispatches Enter), polls response_done_predicate, returns the response selector's innerText. Per ADR-007 the heavy lifting (CDP wire, page lifecycle, JS evaluation) is chromedp's job — we orchestrate.

Package portal — chromedp-backed CDP driver for portal wizard + runtime (ADR-018). Per ADR-007 we wrap chromedp/chromedp instead of rolling our own WebSocket-CDP client. chromedp is the canonical Go binding to the DevTools Protocol — used by GoReleaser, k6, and every Mailgun integration test.

Two modes share the same code path:

  • Wizard: newExecBrowser(ctx) — spawns the user's Chrome / Chromium / Brave / Edge with Headless(false) + a temp --user-data-dir so the operator can log in interactively.
  • Runtime: newRemoteBrowser(ctx, ws) — attaches to an already- running `obscura serve` (or any CDP host) over the supplied WebSocket URL.

Both return a `*BrowserSession` whose helpers (Navigate, Cookies, SetCookies, Evaluate, …) cover the surface portal flows actually need. We deliberately do not re-export the chromedp action API — we surface a small portal-shaped Go API so callers don't have to reason about chromedp.Tasks vs chromedp.ActionFunc.

Package portal implements the saved web-UI target ("portal") concept defined in ADR-018. A portal pairs a base URL with login cookies, CSS selectors, and a "response done" predicate so that `clawtool portal ask <name> "<prompt>"` can drive a headless browser session against a chat web UI without per-vendor code.

Per ADR-017: this is a Tool surface, not a Transport. The supervisor never sees portals; the dispatch surface stays reserved for stable LLM-CLI wire formats.

v0.16.1 (this iteration) ships the persistence + read-only CLI/MCP surface — Add/List/Remove/Use/Which/Unset, manual TOML editing, cookie export workflow. The CDP-driven Ask flow follows in v0.16.2 once the websocket client lands.

Index

Constants

View Source
const (
	PredicateSelectorExists  = "selector_exists"
	PredicateSelectorVisible = "selector_visible"
	PredicateEvalTruthy      = "eval_truthy"

	DefaultTimeoutMs      = 180_000
	DefaultViewportWidth  = 1440
	DefaultViewportHeight = 1000
	DefaultLocale         = "en-US"
)

Predicate types accepted by config.PortalPredicate.Type. Helpers in this package validate and (eventually) evaluate them.

View Source
const SecretsScopePrefix = "portal."

SecretsScopePrefix is the prefix every portal's secrets scope uses — keeps the secrets.toml namespace tidy and makes cross-references obvious.

Variables

View Source
var AskNotImplementedError = errors.New("portal ask: CDP driver not yet implemented — see docs/portals.md for the full design")

AskNotImplementedError is the canonical sentinel returned by the stub Ask path until v0.16.2 lands the CDP driver. CLI / MCP surfaces match against it to give a uniform deferred-feature message.

View Source
var ErrSessionContextDone = errors.New("portal: browser session context cancelled")

AskNotImplementedError is the shared sentinel CLI/MCP surfaces match against when the runtime path is unavailable. Kept here (not in portal.go) because the v0.16.2 sentinel was tied to the hand-rolled CDP swap; v0.16.3 keeps it for forward-compat with any caller that still detects it.

Functions

func Ask

func Ask(ctx context.Context, p config.PortalConfig, prompt string, opts AskOptions) (string, error)

Ask drives the portal `p` with `prompt` and returns the captured response text. Idempotent in the sense that each call spins a fresh browser context (no shared state) — except when opts.Browser is supplied, in which case Ask uses the provided Browser directly and is responsible only for orchestration.

func AssertAuthCookies

func AssertAuthCookies(have []Cookie, want []string) error

AssertAuthCookies checks that every name in want exists in have. Used after ParseCookies to catch a cookies.json export that's missing the actual session cookie (common: user copied a single CSRF cookie thinking it was the login one).

func Defaults

func Defaults(p *config.PortalConfig)

Defaults fills in fall-through values an Ask flow needs. Mutates p in place. Idempotent — safe to call any number of times.

func MarshalCookies

func MarshalCookies(cookies []Cookie) (string, error)

MarshalCookies serialises the cookies to the JSON array shape the secrets.toml `cookies_json` field stores. Mirror of ParseCookies — round-trips cleanly. Returns the JSON as a string because secrets.Store.Set takes string values.

func Names

func Names(cfg config.Config) []string

Names returns the configured portal names, sorted. Stable output for CLI list, MCP discovery, and alias generation.

func Validate

func Validate(name string, p config.PortalConfig) error

Validate checks one PortalConfig is internally consistent. Called at registration time (CLI add, server boot) so a malformed entry never reaches the dispatch path.

Types

type AskOptions

type AskOptions struct {
	Cookies    []Cookie
	ObscuraBin string        // "obscura" → resolved via PATH if empty
	PollEvery  time.Duration // default 250ms
	Stdout     io.Writer     // optional: progress stream (one line per phase). nil → silent

	// Browser, when non-nil, replaces the obscura spawn + chromedp
	// connect path. Used by tests to drive Ask against a fake
	// Browser implementation; production callers leave this nil.
	Browser Browser
}

AskOptions wraps the inputs an external caller (CLI / MCP / HTTP) needs to drive a saved portal flow.

type Browser

type Browser interface {
	Navigate(ctx context.Context, url string) error
	SetCookies(ctx context.Context, cookies []Cookie) error
	SetExtraHTTPHeaders(ctx context.Context, headers map[string]string) error
	Evaluate(ctx context.Context, expr string, out any) error
	EvaluateBool(ctx context.Context, expr string) (bool, error)
	EvaluateString(ctx context.Context, expr string) (string, error)
}

Browser is the structural subset of BrowserSession that the portal Ask flow uses. Carved out so tests inject a fake without spawning Chrome / Obscura. Production code passes a *BrowserSession directly via duck typing.

type BrowserSession

type BrowserSession struct {
	// contains filtered or unexported fields
}

BrowserSession is the wizard / runtime handle. Wraps a chromedp context plus its allocator-cancel + browser-cancel funcs so Close() reaps cleanly.

func NewExecBrowser

func NewExecBrowser(parent context.Context, opts ExecOptions) (*BrowserSession, error)

NewExecBrowser spawns Chrome via chromedp's exec-allocator. Caller MUST call Close() — that cancels the chromedp context AND the allocator, which kills the browser process and removes the temp profile dir.

func NewRemoteBrowser

func NewRemoteBrowser(parent context.Context, wsURL string) (*BrowserSession, error)

NewRemoteBrowser attaches to an already-running CDP server (e.g. `obscura serve`). The browser-level WS URL comes from the caller — we don't probe /json/version here because the caller (runtime path) gets the URL when it spawns Obscura.

func (*BrowserSession) Close

func (s *BrowserSession) Close()

Close reaps the chromedp context and (for exec mode) the spawned browser + temp profile. Idempotent.

func (*BrowserSession) Cookies

func (s *BrowserSession) Cookies(ctx context.Context) ([]Cookie, error)

Cookies returns every cookie the session holds. Wizard uses this after the operator confirms login; runtime never calls it (we inject + go).

func (*BrowserSession) Evaluate

func (s *BrowserSession) Evaluate(ctx context.Context, expr string, out any) error

Evaluate runs JS and decodes the result via json.Unmarshal into `out`. `out` must be a pointer (or nil to discard).

func (*BrowserSession) EvaluateBool

func (s *BrowserSession) EvaluateBool(ctx context.Context, expr string) (bool, error)

EvaluateBool returns the boolean coercion of `expr`. Used by the predicate poller.

func (*BrowserSession) EvaluateString

func (s *BrowserSession) EvaluateString(ctx context.Context, expr string) (string, error)

EvaluateString returns the string coercion of `expr`. Used to pull the rendered response selector's innerText.

func (*BrowserSession) Navigate

func (s *BrowserSession) Navigate(ctx context.Context, url string) error

Navigate loads the URL and waits for the document to be ready.

func (*BrowserSession) SetCookies

func (s *BrowserSession) SetCookies(ctx context.Context, cookies []Cookie) error

SetCookies seeds the session before navigation. Runtime portal Ask uses this to inject the saved auth state.

func (*BrowserSession) SetExtraHTTPHeaders

func (s *BrowserSession) SetExtraHTTPHeaders(ctx context.Context, headers map[string]string) error

SetExtraHTTPHeaders applies on every subsequent request from the session. Runtime path uses it for Accept-Language etc.

type Cookie struct {
	Name     string `json:"name"`
	Value    string `json:"value"`
	Domain   string `json:"domain,omitempty"`
	Path     string `json:"path,omitempty"`
	Secure   bool   `json:"secure,omitempty"`
	HTTPOnly bool   `json:"httpOnly,omitempty"`
	SameSite string `json:"sameSite,omitempty"`
	Expires  int64  `json:"expires,omitempty"` // epoch seconds; 0 = session
}

Cookie mirrors the subset of Chrome DevTools Network.Cookie shape we serialise to / from secrets.toml.

func ParseCookies

func ParseCookies(raw string) ([]Cookie, error)

ParseCookies decodes the cookies_json payload stored in secrets.toml. Tolerant: accepts either a JSON array of Cookie objects or a single object (one cookie). Empty / whitespace-only input → no error, no cookies.

type ExecOptions

type ExecOptions struct {
	Binary   string // override; empty = chromedp auto-detects
	Headless bool   // wizard sets false; tests set true
	StartURL string // optional; defaults to about:blank
}

NewExecBrowser launches Chrome locally with a temp profile and remote-debug port, returning a session the wizard drives. The supplied options pick headless vs headed and the start URL; we keep the rest sensible (no first-run, no default-browser check, silenced password leak detection so a fresh profile doesn't nag).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL