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
- Variables
- func Ask(ctx context.Context, p config.PortalConfig, prompt string, opts AskOptions) (string, error)
- func AssertAuthCookies(have []Cookie, want []string) error
- func Defaults(p *config.PortalConfig)
- func MarshalCookies(cookies []Cookie) (string, error)
- func Names(cfg config.Config) []string
- func RegisterDriver(d Driver)
- func Validate(name string, p config.PortalConfig) error
- type AskOptions
- type Browser
- type BrowserSession
- func (s *BrowserSession) Close()
- func (s *BrowserSession) Cookies(ctx context.Context) ([]Cookie, error)
- func (s *BrowserSession) Evaluate(ctx context.Context, expr string, out any) error
- func (s *BrowserSession) EvaluateBool(ctx context.Context, expr string) (bool, error)
- func (s *BrowserSession) EvaluateString(ctx context.Context, expr string) (string, error)
- func (s *BrowserSession) Navigate(ctx context.Context, url string) error
- func (s *BrowserSession) SetCookies(ctx context.Context, cookies []Cookie) error
- func (s *BrowserSession) SetExtraHTTPHeaders(ctx context.Context, headers map[string]string) error
- type Cookie
- type Driver
- type ExecOptions
Constants ¶
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.
const BifrostDriverName = "bifrost"
BifrostDriverName is the canonical kebab-case identifier exposed in PortalList and accepted by `clawtool portal ask bifrost`.
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 ¶
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.
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.
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 ¶
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 ¶
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 ¶
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.
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 {
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 ¶
Evaluate runs JS and decodes the result via json.Unmarshal into `out`. `out` must be a pointer (or nil to discard).
func (*BrowserSession) EvaluateBool ¶
EvaluateBool returns the boolean coercion of `expr`. Used by the predicate poller.
func (*BrowserSession) EvaluateString ¶
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 ¶
SetExtraHTTPHeaders applies on every subsequent request from the session. Runtime path uses it for Accept-Language etc.
type Cookie ¶
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 ¶
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
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).