attachclient

package
v2.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package attachclient is the shared client for attach-mode endpoints, used by both `core-agent attach`/`ls` (in cmd/core-agent) and `core-agent-tui` (in cmd/core-agent-tui). The package is internal/ so the surface isn't part of the public API stability promise — it's a coordination point between two of our own binaries, not a SDK consumers should reach for.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BearerCreds

type BearerCreds struct {
	Token string
}

BearerCreds sends the attach token as Authorization: Bearer <token>. Zero-value (Token == "") is auth-disabled — Apply is a no-op, matching the historical attach-over-Unix-socket convention.

func (BearerCreds) Apply

func (c BearerCreds) Apply(req *http.Request) error

Apply implements Credentials.

type Client

type Client struct {
	URL *ParsedURL

	// Token is kept for source-compat with the original constructor
	// (New stores its token here AND wraps it in a BearerCreds in
	// Credentials below). New code should prefer Credentials directly.
	// When both are set, Credentials wins — that's how callers opt
	// in to the gateway-fronted path while keeping the legacy field
	// for backward compatibility.
	Token       string
	Credentials Credentials
	// contains filtered or unexported fields
}

Client is a thin HTTP client for one attach-mode endpoint. Holds the parsed URL, bearer token (empty for no auth), and a configured http.Client (Unix-socket-aware when the URL scheme is unix://). Safe for concurrent use.

Two HTTP clients live inside: `http` for short-lived RPC calls with a request timeout, and `streamHTTP` for SSE — no timeout, because the stream body stays open for as long as the agent runs and minutes can pass between frames. A single client with a Timeout would cut the SSE body mid-response on long model turns; the symptom is "stream ended: <nil>" reconnect-loops in the UI.

func New

func New(parsed *ParsedURL, token string, timeout time.Duration) *Client

New builds a Client wrapped in BearerCreds. ParseURL the rawURL first; Token may be empty (auth disabled — fine for Unix socket). timeout governs short-lived RPC calls. SSE streams ignore it (caller's ctx is the cancel signal). Zero timeout falls back to 30 s for RPCs.

Use NewWithCredentials to construct a Client with a non-Bearer auth strategy (Cloud Run IAM, IAP, …).

func NewWithCredentials

func NewWithCredentials(parsed *ParsedURL, creds Credentials, timeout time.Duration) *Client

NewWithCredentials builds a Client with an explicit Credentials implementation. Used by callers that need a non-Bearer auth path (e.g. cmd/core-agent-tui's --auth=google-id-token mode, which supplies a GoogleIDTokenCreds backed by ADC).

func (*Client) Agents

func (c *Client) Agents(ctx context.Context, sessionPath string) ([]attach.AgentInfo, error)

Agents calls GET <base>/sessions/<sid>/agents.

func (*Client) AllowPatterns

func (c *Client) AllowPatterns(ctx context.Context, sessionPath string, patterns []string) error

AllowPatterns calls POST <base>/sessions/<sid>/perms/allow with the given patterns. Backs the remote TUI's /allow slash. Returns nil on success (204), an error otherwise — including 501 when the agent doesn't implement PermsController and 400 when the gate rejects a pattern.

func (*Client) Context

func (c *Client) Context(ctx context.Context, sessionPath string) (attach.ContextInfo, error)

Context calls GET <base>/sessions/<sid>/context. Backs the remote TUI's /context slash. Returns zero ContextInfo on 501.

func (*Client) DenyPatterns

func (c *Client) DenyPatterns(ctx context.Context, sessionPath string, patterns []string) error

DenyPatterns calls POST <base>/sessions/<sid>/perms/deny. Backs the remote TUI's /deny slash.

func (*Client) Inject

func (c *Client) Inject(ctx context.Context, sessionPath, message string) error

Inject calls POST <base>/sessions/<sid>/inject with the given message. sessionPath is the /sessions/<sid> prefix (relative to BaseURL).

func (*Client) Interrupt

func (c *Client) Interrupt(ctx context.Context, sessionPath string) (InterruptResponse, error)

Interrupt calls POST <base>/sessions/<sid>/interrupt to cancel the in-flight turn on that session. The returned InterruptResponse reports whether something was actually cancelled.

func (*Client) ListPeers

func (c *Client) ListPeers(ctx context.Context) ([]PeerDescriptor, error)

ListPeers calls GET <base>/peers. Returns nil (not an error) when the listener doesn't have peer-registration enabled (HTTP 404).

func (*Client) ListSessions

func (c *Client) ListSessions(ctx context.Context) ([]SessionDescriptor, error)

ListSessions calls GET <base>/sessions.

func (*Client) MCP

func (c *Client) MCP(ctx context.Context, sessionPath string) (attach.MCPInfo, error)

MCP calls GET <base>/sessions/<sid>/mcp. Backs the remote TUI's /mcp slash. Returns zero MCPInfo on 501.

func (*Client) Memory

func (c *Client) Memory(ctx context.Context, sessionPath string) ([]attach.MemorySource, error)

Memory calls GET <base>/sessions/<sid>/memory. Backs the remote TUI's /memory slash. Returns empty slice (not nil) on 501.

func (*Client) NewSession

func (c *Client) NewSession(ctx context.Context) (NewSessionResponse, error)

NewSession calls POST <base>/sessions to create a fresh session owned by the authenticated caller. Returns the new session's descriptor on success.

Server-side behavior:

  • 201: new session created, response carries the triple + URL
  • 401: caller couldn't be resolved (anonymous request)
  • 501: daemon doesn't have a SessionFactory configured
  • 500: factory error
  • 409: triple collision (factory's SessionID generator clashed)

All non-2xx responses surface as errors.

func (*Client) Perms

func (c *Client) Perms(ctx context.Context, sessionPath string) (attach.PermsInfo, error)

Perms calls GET <base>/sessions/<sid>/perms. Backs the remote TUI's /permissions slash. Returns zero PermsInfo on 501.

func (*Client) Pricing

func (c *Client) Pricing(ctx context.Context, sessionPath string) (attach.PricingInfo, error)

Pricing calls GET <base>/sessions/<sid>/pricing. Backs the remote TUI's /pricing slash. Returns zero PricingInfo on 501.

func (*Client) PromptStream

func (c *Client) PromptStream(ctx context.Context, sessionPath string) (<-chan attach.PromptFrame, error)

PromptStream subscribes to <base><sessionPath>/perms/stream and returns a channel of PromptFrames. Closes the channel on ctx cancel, stream error, or upstream EOF. 501 (capability not registered — agent wasn't constructed with WithAttachPromptBroker) is returned synchronously so callers can fall back gracefully.

func (*Client) RefreshPricing

func (c *Client) RefreshPricing(ctx context.Context, sessionPath string) (attach.PricingRefreshResponse, error)

RefreshPricing calls POST <base>/sessions/<sid>/pricing/refresh. Backs the remote TUI's /pricing refresh subcommand. Returns the outcome (whether the LiteLLM fetch actually pulled new data and the post-refresh model count) so the client can update its display.

func (*Client) Reload

func (c *Client) Reload(ctx context.Context, sessionPath string) (attach.ReloadResponse, error)

Reload calls POST <base>/sessions/<sid>/reload. Backs the remote TUI's /reload slash. Returns the per-surface success flags + any errors so the operator sees which parts (memory / skills / mcp) succeeded and which failed.

func (*Client) Replan

func (c *Client) Replan(ctx context.Context, sessionPath, reason string) (attach.ReplanResponse, error)

Replan calls POST <base>/sessions/<sid>/slash/replan. Backs the remote TUI's /replan slash. Reason is the optional free-text the operator typed after /replan; today it's surfaced in the archive's audit trail but doesn't drive any model-side behavior.

func (*Client) RespondToPrompt

func (c *Client) RespondToPrompt(ctx context.Context, sessionPath, id, decision string) error

RespondToPrompt POSTs the operator's decision to <base><sessionPath>/perms/respond. decision must be one of the wire-format strings (e.g. "allow-once"); see attach.DecisionFromWire for the mapping.

func (*Client) SetManualPricing

func (c *Client) SetManualPricing(ctx context.Context, sessionPath string, req attach.PricingSetRequest) error

SetManualPricing calls POST <base>/sessions/<sid>/pricing/set. Backs the remote TUI's /pricing set subcommand.

func (*Client) Skills

func (c *Client) Skills(ctx context.Context, sessionPath string) ([]attach.SkillInfo, error)

Skills calls GET <base>/sessions/<sid>/skills. Backs the remote TUI's /skills slash.

func (*Client) SlashBtw

func (c *Client) SlashBtw(ctx context.Context, sessionPath, question string) (string, error)

SlashBtw calls POST <base>/sessions/<sid>/slash/btw. Synchronous. Backs the remote TUI's /btw slash. The answer renders as a dismissible overlay (no event-log persistence).

func (*Client) SlashCompact

func (c *Client) SlashCompact(ctx context.Context, sessionPath, focus string) (attach.CompactResponse, error)

SlashCompact calls POST <base>/sessions/<sid>/slash/compact. Synchronous: blocks until the compaction summarizer completes (5–30s typical for real model calls). The remote TUI should render the in-chat preamble row at dispatch — this call does NOT emit a preamble itself.

func (*Client) SlashDone

func (c *Client) SlashDone(ctx context.Context, sessionPath, note string) (attach.CheckpointResponse, error)

SlashDone calls POST <base>/sessions/<sid>/slash/done. Synchronous. Backs the remote TUI's /done slash.

func (*Client) SlashSubagent

func (c *Client) SlashSubagent(ctx context.Context, sessionPath string, spec attach.SubagentSpec) (attach.SubagentSpawnResponse, error)

SlashSubagent calls POST <base>/sessions/<sid>/slash/subagent. Backs the remote TUI's /subagent slash. Returns the spawn confirmation (name + started_at); the subagent's events flow through the existing SSE stream under a branch label so the operator sees its turns alongside the parent's.

func (*Client) Status

func (c *Client) Status(ctx context.Context, sessionPath string) (attach.StatusInfo, error)

Status calls GET <base>/sessions/<sid>/status.

func (*Client) Stream

func (c *Client) Stream(ctx context.Context, sessionPath string, since int64) (<-chan attach.Frame, error)

Stream connects to <base><sessionPath>/events?since=<since> and returns a channel of decoded frames. Closes the channel on ctx cancel, stream error, or upstream EOF. Errors that prevented the initial GET (network failure, non-200 status) are returned synchronously; downstream errors land in the returned channel's error field via the second return value being closed.

The lossless-replay property of the protocol means that passing a non-zero since value asks the server to replay any frames since that sequence before resuming live tail.

func (*Client) Tools

func (c *Client) Tools(ctx context.Context, sessionPath string) ([]attach.ToolInfo, error)

Tools calls GET <base>/sessions/<sid>/tools. Returns the parsed list; empty (not nil) if the session doesn't implement the provider.

func (*Client) Usage

func (c *Client) Usage(ctx context.Context, sessionPath string) (attach.UsageInfo, error)

Usage calls GET <base>/sessions/<sid>/usage. Backs the remote TUI's /stats slash. Returns zero UsageInfo if the agent doesn't implement the capability (server returns 501).

func (*Client) Wake

func (c *Client) Wake(ctx context.Context, sessionPath string) error

Wake calls POST <base>/sessions/<sid>/wake.

type Credentials

type Credentials interface {
	// Apply stamps headers on req. Returns an error when the
	// underlying credential source fails to produce a token —
	// callers propagate (the request is not sent in that case).
	Apply(req *http.Request) error
}

Credentials stamps authentication headers on outbound requests. Implementations must be safe for concurrent use from multiple goroutines (the Client uses one Credentials for every request, including parallel RPC + SSE).

type GoogleIDTokenCreds

type GoogleIDTokenCreds struct {
	Source      oauth2.TokenSource
	AttachToken string
}

GoogleIDTokenCreds is the audience-bound variant. The Source produces a Google ID token bound to a specific audience (the gateway's expected audience — service URL for Cloud Run, OAuth client ID for IAP). Use when audience-binding is required (IAP) or when a service explicitly requires ID tokens.

Important constraint: idtoken.NewTokenSource does NOT accept end-user (authorized_user) credentials — operators using gcloud auth application-default login will hit "unsupported credentials type: authorized_user" at construction time. Workarounds:

  • Re-login with impersonation: gcloud auth application-default login --impersonate-service-account=SA_EMAIL
  • Set GOOGLE_APPLICATION_CREDENTIALS to a service-account JSON key
  • Use --auth=google-oauth instead (Cloud Run IAM accepts access tokens; the audience-binding loss is mostly theoretical)

AttachToken may be empty when the daemon runs without --attach-token ("Posture B"). The header is omitted entirely in that case.

func (GoogleIDTokenCreds) Apply

func (c GoogleIDTokenCreds) Apply(req *http.Request) error

Apply implements Credentials.

type GoogleOAuthCreds

type GoogleOAuthCreds struct {
	Source      oauth2.TokenSource
	AttachToken string
}

GoogleOAuthCreds wraps a Google OAuth2 access-token source (typically google.FindDefaultCredentials's TokenSource) and stamps the access token on Authorization. Works with every ADC shape end users actually have on their workstations — end-user (authorized_user) creds, service-account JSON keys, metadata server, impersonation.

This is the right default for Cloud Run IAM: the gateway accepts either OAuth access tokens OR audience-bound ID tokens, and access tokens come for free from end-user ADC. Mirrors MCP's googleAuthTransport pattern (pkg/mcp/lifecycle.go:296).

AttachToken may be empty when the daemon runs without --attach-token ("Posture B"). The header is omitted entirely in that case.

func (GoogleOAuthCreds) Apply

func (c GoogleOAuthCreds) Apply(req *http.Request) error

Apply implements Credentials.

type InterruptResponse

type InterruptResponse struct {
	Interrupted bool   `json:"interrupted"`
	Session     string `json:"session"`
}

InterruptResponse is the parsed body of POST /sessions/<sid>/interrupt. Interrupted reports whether there was an in-flight turn to cancel (server-side); false means the agent was idle and the call was a no-op. The TUI distinguishes these for its "nothing to interrupt" toast vs. "turn cancelled" rendering.

type NewSessionResponse

type NewSessionResponse struct {
	AppName   string `json:"app"`
	UserID    string `json:"user"`
	SessionID string `json:"sessionID"`
	URL       string `json:"url"`
}

NewSessionResponse mirrors the attach server's POST /sessions 201 body — the new session's triple plus the absolute URL the client should attach to (events / inject / status / etc. live underneath).

type ParsedURL

type ParsedURL struct {
	Scheme     string // http | https | unix
	Host       string // host:port (empty for unix)
	SocketPath string // for unix scheme
	BaseURL    string // ready-to-use for HTTP client: http(s)://host OR http://unix placeholder
	Session    string // /sessions/<...> path; empty for list endpoints
}

ParsedURL holds the components of an attach-mode URL. Three schemes are accepted: http://, https://, unix:// (the last for Unix-socket listeners — convention is unix:///path/to/socket/sessions/<sid>).

Session is non-empty when the URL targets a specific session (e.g. /sessions/<sid> or /sessions/<app>/<sid>). For listing endpoints (GET /sessions) Session is empty.

func ParseURL

func ParseURL(raw string) (*ParsedURL, error)

ParseURL decodes raw into a ParsedURL. Returns a clear error for unsupported schemes so the caller can surface "want http, https, or unix" without digging into url.Parse internals.

func (*ParsedURL) IsHubURL

func (p *ParsedURL) IsHubURL() bool

IsHubURL is a heuristic: hub URLs target the root (no /sessions/<id> suffix). Used by the TUI to decide whether to enumerate peer sessions in the picker or just list this listener's sessions.

type PeerDescriptor

type PeerDescriptor struct {
	RegistrationID string            `json:"registration_id"`
	Name           string            `json:"name"`
	Endpoint       string            `json:"endpoint"`
	Labels         map[string]string `json:"labels,omitempty"`
}

PeerDescriptor mirrors the attach server's GET /peers row.

type SessionDescriptor

type SessionDescriptor struct {
	App         string `json:"app"`
	User        string `json:"user"`
	SessionID   string `json:"sessionID"`
	HasEventLog bool   `json:"has_event_log"`
}

SessionDescriptor mirrors the attach server's GET /sessions row.

Jump to

Keyboard shortcuts

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