portal

package
v0.22.78 Latest Latest
Warning

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

Go to latest
Published: Apr 30, 2026 License: MIT Imports: 18 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 — Bifrost driver stub (phase 1).

maximhq/bifrost (https://github.com/maximhq/bifrost, Apache-2.0) is a Go-native AI gateway that composes per-vendor portals under one config: unified failover, semantic caching, and budget governance across OpenAI / Anthropic / Vertex / Bedrock / local llama backends. clawtool's portal layer today registers one entry per chat web-UI; Bifrost-shaped portals would route by model with a fallback chain instead.

Phase 1 (this file) ships the registration + list surface only. Adding the bifrost/core Go module (which transitively pulls a large dependency graph: every supported provider's SDK, an embedded SQLite for the semantic cache, etc.) is deferred to a later commit gated behind the `clawtool_bifrost` build tag — see the `Ask` deferred-error message below for the canonical sentinel callers can match on.

The driver REGISTERS — it surfaces in `clawtool portal list` as `bifrost (deferred)` so operators can discover the integration path before phase 2 lands. Calling Ask returns a typed error instead of silently no-oping, mirroring the `AskNotImplementedError` sentinel pattern the v0.16.1 portal driver used while the CDP runtime was in flight.

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 BifrostDriverName = "bifrost"

BifrostDriverName is the canonical kebab-case identifier exposed in PortalList and accepted by `clawtool portal ask bifrost`.

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 ErrBifrostDeferred = errors.New("bifrost portal: integration deferred to phase 2 — add bifrost/core via build tag clawtool_bifrost")

ErrBifrostDeferred is the canonical sentinel returned by the phase-1 Bifrost stub. CLI / MCP surfaces match against it via errors.Is to give a uniform deferred-feature message; phase 2 removes it once bifrost/core lands behind the `clawtool_bifrost` build tag.

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 RegisterDriver added in v0.22.65

func RegisterDriver(d Driver)

RegisterDriver adds d to the global driver registry. Panics on empty / duplicate Name — same boot-time-fail pattern the recipe registry uses (see internal/setup/recipe.go) so a misspelled name surfaces at binary build, not at the user's first run.

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 Driver added in v0.22.65

type Driver interface {
	// Name is the kebab-case identifier — must match the config
	// key the operator would write under [portals.<name>] if they
	// wanted to override defaults. Stable across releases.
	Name() string

	// Status is the short label rendered in PortalList's STATUS
	// column. Stub drivers return "deferred"; ready drivers will
	// return "ready" once their phase-2 implementation lands.
	Status() string

	// Description is the one-line summary of what the driver wraps.
	// Surfaced in `portal list` table mode and in MCP discovery.
	Description() string

	// Ask drives the gateway with `prompt` and returns the response.
	// Phase-1 drivers return ErrBifrostDeferred (or an analogous
	// sentinel) so the caller's error handling matches the pattern
	// the CDP-runtime placeholder used.
	Ask(ctx context.Context, prompt string) (string, error)
}

Driver is the in-memory portal-driver shape: a registered name, a status string surfaced in PortalList, and an Ask entry point. Drivers are distinct from config-stored [portals.<name>] stanzas — the former are compiled-in integrations (bifrost gateway, future LiteLLM / OpenRouter), the latter are user-saved web-UI targets.

PortalList merges both: config stanzas keep their per-portal row, drivers add a single discovery row each so the operator can see what integrations are available without reading source.

func Drivers added in v0.22.65

func Drivers() []Driver

Drivers returns every registered driver, sorted by name. Stable order so PortalList output and tests don't flake.

func LookupDriver added in v0.22.65

func LookupDriver(name string) Driver

LookupDriver returns the driver with the given name, or nil if absent. Names are unique so the lookup is unambiguous.

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