server

package
v0.8.19 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 43 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// HandFree means no connection may write to the session's terminal.
	HandFree = "free"
	// HandHeld means exactly one connection may write to it.
	HandHeld = "held"
	// HandRequested means the hand is held and another operator has asked for it.
	HandRequested = "requested"
)

Variables

View Source
var (
	// ErrHandHeld is returned instead of silently stealing the terminal from
	// another operator. The caller is expected to request control.
	ErrHandHeld = errors.New("another operator holds the terminal")
	// ErrNotHolder is returned to a connection that tried to write to, resize,
	// or release a terminal it does not hold.
	ErrNotHolder = errors.New("this connection does not hold the terminal")
	// ErrNoPendingRequest is returned when granting or declining a request that
	// has already expired, been withdrawn, or never existed.
	ErrNoPendingRequest = errors.New("no pending control request")
	// ErrTooManyObservers is returned once a session roster is full.
	ErrTooManyObservers = errors.New("observer limit reached")
	// ErrForceDisabled is returned when force takeover is requested but the
	// deployment has not opted into it.
	ErrForceDisabled = errors.New("force takeover is disabled")
	// ErrUnknownConnection is returned for a connID the registrar never saw, or
	// that disconnected between a roster read and this call.
	ErrUnknownConnection = errors.New("unknown connection")
	// ErrViewerRole is returned when a read-only connection attempts to take or
	// request the hand. The RPC dispatcher rejects viewers first; this is the
	// second, authoritative gate.
	ErrViewerRole = errors.New("permission denied: viewer role is read-only")
)
View Source
var DistAssets embed.FS

DistAssets embeds the production single-page application bundle. If empty (e.g. in test environments), the server provides a helpful fallback.

Functions

func DefaultConfigPath

func DefaultConfigPath() (string, error)

DefaultConfigPath resolves the default config file location.

func RunServe

func RunServe(arguments []string, _ io.Writer, diagnostics io.Writer) error

RunServe parses arguments and launches the Relayer Web Gateway.

func Serve

func Serve(ctx context.Context, opts Options) error

Serve initializes and runs the Relayer Web Gateway until context cancellation.

func StaticFileSystem

func StaticFileSystem() http.FileSystem

StaticFileSystem returns an http.FileSystem rooted at the embedded dist directory, stripping the "dist" prefix so that index.html is served at the root.

Types

type AgentCatalogEntry

type AgentCatalogEntry struct {
	ID                 string   `json:"id"`
	Name               string   `json:"name"`
	Description        string   `json:"description"`
	InstallStatus      string   `json:"installStatus"`
	Installed          bool     `json:"installed"`
	Adapter            string   `json:"adapter"`
	AdapterStatus      string   `json:"adapterStatus"`
	DefaultArgv        []string `json:"defaultArgv"`
	RequiresCustomArgv bool     `json:"requiresCustomArgv"`
	MinimumArguments   int      `json:"minimumArguments"`
	ArgumentPrefix     []string `json:"argumentPrefix"`
}

type AgentProfile

type AgentProfile struct {
	ID              string   `json:"id"`
	Name            string   `json:"name"`
	PresetID        string   `json:"presetID"`
	Cwd             string   `json:"cwd"`
	Backend         string   `json:"backend"`
	Adapter         string   `json:"adapter"`
	Argv            []string `json:"argv,omitempty"`
	ExecutableLabel string   `json:"executableLabel"`
	ArgumentCount   int      `json:"argumentCount"`
	Locked          bool     `json:"locked"`
	ReadOnlyReason  string   `json:"readOnlyReason,omitempty"`
	PreserveOnSave  bool     `json:"preserveOnSave"`
}

type AgentProfileInput

type AgentProfileInput struct {
	ID       string   `json:"id"`
	Name     string   `json:"name"`
	PresetID string   `json:"presetID,omitempty"`
	Cwd      string   `json:"cwd,omitempty"`
	Backend  string   `json:"backend,omitempty"`
	Adapter  string   `json:"adapter,omitempty"`
	Argv     []string `json:"argv,omitempty"`
	Preserve bool     `json:"preserve,omitempty"`
}

AgentProfileInput is one profile a save sends back. Preserve, which the interface sends as "preserve", keeps the existing agent of that ID; the gateway read "preserveOnSave", which no client sends, and ignored it.

type AgentProfilesView

type AgentProfilesView struct {
	ConfigPath      string              `json:"configPath"`
	Revision        string              `json:"revision"`
	Catalog         []AgentCatalogEntry `json:"catalog"`
	Profiles        []AgentProfile      `json:"profiles"`
	MinProfiles     int                 `json:"minProfiles"`
	MaxProfiles     int                 `json:"maxProfiles"`
	RestartRequired bool                `json:"restartRequired"`
	Editable        bool                `json:"editable"`
	ReadOnlyReason  string              `json:"readOnlyReason,omitempty"`
}

type AgentState

type AgentState struct {
	SessionID         string `json:"sessionID"`
	AgentID           string `json:"agentID"`
	Name              string `json:"name"`
	DisplayCommand    string `json:"displayCommand"`
	Backend           string `json:"backend"`
	Adapter           string `json:"adapter"`
	Status            string `json:"status"`
	Output            string `json:"output"`
	Revision          uint64 `json:"revision"`
	Running           bool   `json:"running"`
	Attached          bool   `json:"attached"`
	ObserverCount     int    `json:"observerCount"`
	HolderIdentity    string `json:"holderIdentity,omitempty"`
	InputFrozen       bool   `json:"inputFrozen"`
	Simulated         bool   `json:"simulated"`
	ExitCode          *int   `json:"exitCode,omitempty"`
	InstalledVersion  string `json:"installedVersion,omitempty"`
	UnverifiedVersion bool   `json:"unverifiedVersion,omitempty"`
	UnverifiedReason  string `json:"unverifiedReason,omitempty"`
}

type AppState

type AppState struct {
	RunID         string             `json:"runID"`
	RunStatus     string             `json:"runStatus"`
	StartedAt     string             `json:"startedAt,omitempty"`
	Policy        PolicyState        `json:"policy"`
	Audit         AuditState         `json:"audit"`
	Agents        []AgentState       `json:"agents"`
	PendingEvents []SupervisionEvent `json:"pendingEvents"`
	Notices       []string           `json:"notices"`
}

type AuditEntryView

type AuditEntryView struct {
	Sequence   uint64            `json:"sequence"`
	Timestamp  string            `json:"timestamp"`
	EntryID    string            `json:"entryID"`
	RunID      string            `json:"runID"`
	Kind       string            `json:"kind"`
	SessionID  string            `json:"sessionID,omitempty"`
	AgentID    string            `json:"agentID,omitempty"`
	Backend    string            `json:"backend,omitempty"`
	Adapter    string            `json:"adapter,omitempty"`
	EventType  string            `json:"eventType,omitempty"`
	Risk       string            `json:"risk,omitempty"`
	Rule       string            `json:"rule,omitempty"`
	Decision   string            `json:"decision,omitempty"`
	DecisionBy string            `json:"decisionBy,omitempty"`
	Operator   string            `json:"operator,omitempty"`
	Outcome    string            `json:"outcome,omitempty"`
	Reason     string            `json:"reason,omitempty"`
	Summary    string            `json:"summary,omitempty"`
	Sensitive  bool              `json:"sensitive"`
	Metadata   map[string]string `json:"metadata,omitempty"`
}

type AuditFilterInput

type AuditFilterInput struct {
	AgentID   string `json:"agentID,omitempty"`
	SessionID string `json:"sessionID,omitempty"`
	RunID     string `json:"runID,omitempty"`
	Kind      string `json:"kind,omitempty"`
	Limit     int    `json:"limit,omitempty"`
}

type AuditState

type AuditState struct {
	Enabled bool   `json:"enabled"`
	Mode    string `json:"mode"`
	Status  string `json:"status"`
	Path    string `json:"path,omitempty"`
}

type AuditSummaryView

type AuditSummaryView struct {
	Path           string         `json:"path"`
	TotalEntries   int            `json:"totalEntries"`
	RunsCount      int            `json:"runsCount"`
	SessionsCount  int            `json:"sessionsCount"`
	AgentCounts    map[string]int `json:"agentCounts"`
	KindCounts     map[string]int `json:"kindCounts"`
	DecisionsCount map[string]int `json:"decisionsCount"`
	ActorsCount    map[string]int `json:"actorsCount"`
	OutcomesCount  map[string]int `json:"outcomesCount"`
	SensitiveCount int            `json:"sensitiveCount"`
	FirstTimestamp string         `json:"firstTimestamp,omitempty"`
	LastTimestamp  string         `json:"lastTimestamp,omitempty"`
}

type AuditVerificationIssueView

type AuditVerificationIssueView struct {
	Line    int    `json:"line"`
	EntryID string `json:"entryID,omitempty"`
	Message string `json:"message"`
}

type AuditVerificationView

type AuditVerificationView struct {
	Path       string                       `json:"path"`
	TotalLines int                          `json:"totalLines"`
	TotalRuns  int                          `json:"totalRuns"`
	ValidLines int                          `json:"validLines"`
	Issues     []AuditVerificationIssueView `json:"issues"`
	Passed     bool                         `json:"passed"`
}

type AuthIdentity added in v0.6.0

type AuthIdentity struct {
	Identity string
	Role     UserRole
}

type Controller

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

Controller owns the headless supervisor runtime and provides thread-safe query and mutation methods mirroring the RelayerBridge interface.

c.mu guards the gateway's own state: the run, its agents' output and presence, the hands and the subscribers. What decides whether a byte reaches an agent — its prompts, the policy's decisions, the agents' supervision state and the journal's failure — is the run's supervision core's, which GetState lays over the gateway's own. The core's sink takes c.mu (see gatewaySink): c.mu is held while reading the core or calling SetHolder, RecordAudit and BeginDrain, never while calling an operation that changes the core, or Wait.

lifecycleMu serialises what ends a run, StopRun, "Save and restart" and Close, and is taken before c.mu, never after it. A run ends by draining, which waits for what its core admitted and so never happens under c.mu.

func NewController

func NewController(configPath string, diagnostics io.Writer) (*Controller, error)

NewController creates an unstarted supervisor controller.

func (*Controller) Close

func (c *Controller) Close(ctx context.Context) error

Close gracefully stops the running supervisor and all agent processes. It drains the run first, as StopRun does, and within ctx: what the run's core admitted, an answer, a line or keystrokes being written, reaches its journaled outcome before the runtime and its journal close.

func (*Controller) DeclineControl added in v0.7.0

func (c *Controller) DeclineControl(runID, sessionID, connID, operator, toConnID string) (HandView, error)

DeclineControl refuses a pending request and leaves the hand where it is. runID is the run the caller shows, and is not checked: declining moves no terminal.

func (*Controller) DeleteRecording added in v0.7.0

func (c *Controller) DeleteRecording(id, operator string) error

DeleteRecording permanently removes a transcript. A recording still being written is refused by the store rather than deleted underneath its writer.

func (*Controller) ExportAuditReport

func (c *Controller) ExportAuditReport(format string) (string, error)

func (*Controller) ExportRecording added in v0.7.0

func (c *Controller) ExportRecording(id, operator string) (string, error)

ExportRecording returns a whole transcript as asciicast v2 text. Exporting is audited: it is the moment a transcript leaves the supervising host.

func (*Controller) ForceTakeControl added in v0.7.0

func (c *Controller) ForceTakeControl(runID, sessionID, connID, operator string) (HandView, error)

ForceTakeControl seizes a held hand without the holder's consent. It is off by default: an operator typing into an agent's terminal can be interrupted mid-command, so the deployment must opt in.

Every attempt by a known connection is journaled as control_forced, refused ones included: trying to seize a colleague's terminal is the event worth finding later, whether or not the deployment allowed it. A seizure names the run it is for (supervise.ErrRunStale otherwise, as RequestControl), and one that takes the terminal is journaled, best effort, before the hand moves (recordControlAuditLocked); a refused one afterwards.

func (*Controller) GetAgentProfiles

func (c *Controller) GetAgentProfiles() (AgentProfilesView, error)

func (*Controller) GetAuditEntries

func (c *Controller) GetAuditEntries(filter AuditFilterInput) ([]AuditEntryView, error)

func (*Controller) GetAuditSummary

func (c *Controller) GetAuditSummary() (AuditSummaryView, error)

func (*Controller) GetFullSettings

func (c *Controller) GetFullSettings() (FullSettingsView, error)

func (*Controller) GetRecording added in v0.7.0

func (c *Controller) GetRecording(id string) (RecordingView, error)

GetRecording returns one transcript's metadata.

func (*Controller) GetState

func (c *Controller) GetState() AppState

func (*Controller) GetTelemetrySnapshot

func (c *Controller) GetTelemetrySnapshot() TelemetrySnapshotView

func (*Controller) GrantControl added in v0.7.0

func (c *Controller) GrantControl(runID, sessionID, connID, operator, toConnID string) (HandView, error)

GrantControl transfers the hand to a pending requester. Only the current holder may grant, for the run it names (supervise.ErrRunStale otherwise, as RequestControl). The grant is journaled, best effort, before the hand moves (recordControlAuditLocked).

func (*Controller) HandFor added in v0.7.0

func (c *Controller) HandFor(sessionID string) HandView

HandFor returns the current write-lock snapshot for one session.

func (*Controller) HoldsHand added in v0.7.0

func (c *Controller) HoldsHand(sessionID, connID string) bool

HoldsHand reports whether a connection may currently resize a session's terminal. A session nobody has claimed may be resized by any operator: a resize writes nothing the agent reads. Keystrokes are stricter, and need the hand held by the connection that types them (SendTerminalInput).

func (*Controller) ListPresence added in v0.7.0

func (c *Controller) ListPresence(sessionID string) (PresenceView, error)

ListPresence returns the roster of one session without mutating anything.

func (*Controller) ListRecordings added in v0.7.0

func (c *Controller) ListRecordings(filter RecordingFilterInput) ([]RecordingView, error)

ListRecordings returns the stored transcripts, newest first.

func (*Controller) ObserveSession added in v0.7.0

func (c *Controller) ObserveSession(connID, sessionID string, observing bool) (PresenceView, error)

ObserveSession adds a connection to one session's roster. Observing is a read-only act: viewers may observe, and observing never implies the hand.

func (*Controller) ReadRecordingChunk added in v0.7.0

func (c *Controller) ReadRecordingChunk(id string, offset, limit int) (RecordingChunk, error)

ReadRecordingChunk returns one page of frames plus the header. A recording still being written is readable: the operator watching a session live is the one most likely to want the replay.

func (*Controller) RegisterPresence added in v0.7.0

func (c *Controller) RegisterPresence(connID, identity, role string)

RegisterPresence records a newly authenticated connection. It is called by the gateway immediately after the websocket is registered.

func (*Controller) ReleaseControl added in v0.7.0

func (c *Controller) ReleaseControl(runID, sessionID, connID, operator string) (HandView, error)

ReleaseControl frees a hand held by this connection and journals it as control_released. Releasing a hand nobody holds changes nothing and is not journaled. runID is the run the caller shows, and is not checked: letting go of a terminal is always safe, and a connection holds no terminal of a run it never took one in.

func (*Controller) ReleasePresence added in v0.7.0

func (c *Controller) ReleasePresence(connID string)

ReleasePresence removes a disconnected connection and frees any hand it held. The gateway must call this after releasing its own client lock: broadcasting while the gateway holds that lock deadlocks against the broadcast listener.

A hand freed this way is journaled as control_released by the system, so a journal never shows somebody taking a terminal and then simply stops. A withdrawn or expired request moves no terminal and is not journaled.

func (*Controller) RequestControl added in v0.7.0

func (c *Controller) RequestControl(runID, sessionID, connID, operator string) (HandView, error)

RequestControl asks the current holder to hand over. A second request from the same connection refreshes the deadline rather than erroring, so a UI that retries is not punished, and is not journaled a second time either.

A request names the run it is for, and one for another run, or none, is refused (supervise.ErrRunStale), checked under the lock the hand changes under. The verb named no run, and a tab left on a run "Save and restart" had replaced took the new run's free terminal with its "Ask for the terminal" button: the tab dropped the new run's hand frame and never knew it held the terminal, nobody else could take it, and the policy answered nothing on that agent any more, since its terminal was held.

A request that takes a free terminal is journaled, best effort, before the hand moves (recordControlAuditLocked): no keystroke is admitted before the record of who took the terminal, and a record the journal refuses freezes the run first, so none is admitted at all.

func (*Controller) ResizeSession

func (c *Controller) ResizeSession(runID, sessionID string, columns, rows int, connID string) error

ResizeSession applies a terminal geometry change requested by the connection holding the hand. A resize from anyone else is a silent no-op: two observers with different window sizes must not fight over the PTY geometry and thrash the agent's rendering.

The resize is a write to the run's backend like any other, admitted by the run's core (AdmitRun) as the desktop's is: a run that ends waits for it before its runtime closes, and takes none once it drains. The gateway resized whatever the run was doing, a runtime being closed included.

func (*Controller) RestartSession

func (c *Controller) RestartSession(runID, sessionID string) error

func (*Controller) RunPreflight

func (c *Controller) RunPreflight(ctx context.Context) (PreflightReport, error)

func (*Controller) SaveAgentProfiles

func (c *Controller) SaveAgentProfiles(runID string, req SaveAgentProfilesRequest) (AgentProfilesView, error)

func (*Controller) SaveAgentProfilesAndRestart

func (c *Controller) SaveAgentProfilesAndRestart(req SaveAgentProfilesAndRestartRequest) (LifecycleResult, error)

SaveAgentProfilesAndRestart writes the whole request in one write, then ends the run the caller names and starts another on the new configuration.

It acts only on the run the caller names, the stopped one after StopRun included: the gateway ignored ExpectedRunID, so a tab left open on a run another tab had already restarted restarted the new run, and the request the settings panel sends with an empty run ID was taken too.

The previous run drains as StopRun's does (endRun), strictly, without c.mu, and the new run keeps nothing of it: a supervision core of its own, with none of the previous run's prompts, and no hand held. The gateway closed the runtime under c.mu without waiting for what its core admitted, and handed the new run the previous run's hands, so a connection typed into a new process it had never attached to. Every client is told the new run's ID with its status, running, or failed when it could not start, so a tab on the previous run can load the new one; a run that failed to start keeps that ID, which a retry names. A previous run whose sessions could not all be confirmed stopped starts nothing, as on the desktop.

func (*Controller) SaveFullSettings

func (c *Controller) SaveFullSettings(runID string, req SaveFullSettingsRequest) (FullSettingsView, error)

func (*Controller) SendTerminalInput added in v0.6.0

func (c *Controller) SendTerminalInput(runID, sessionID string, data []byte, operator, connID string) error

SendTerminalInput delivers raw terminal input bytes directly to the session backend. Used by the web interactive terminal (full PTY mode) to stream keystrokes and signals.

Keystrokes reach the agent without the policy and are never journaled: the documented bypass of an interactive terminal. They are bounded instead, by the run's supervision core, which admits a write only from the connection that holds the session's hand (Admit): a terminal nobody holds takes no keystrokes, from anybody, and neither does a call that names no connection. Every keystroke therefore falls within a hand whose taking was journaled. The write takes the session's one write slot, the one an answer and a line take: it is refused while either is being written, and while it is admitted they are refused or, for the policy's answers, wait. It is refused as well once the journal has failed, on a session frozen by a write whose outcome is unknown, on one whose process is stopped, stopping or starting, and once the run drains, which waits for every admitted write before its runtime closes.

A terminal nobody held was writable by any operator connection, the binary frames that carry no connection's run included, and the keystrokes went to the terminal whatever else was being written: with the policy answering on its own, a client typing without the hand typed alongside the policy's answer to the same question, and nothing said anybody had been at the terminal. The resize, which writes nothing the agent reads, keeps the rule that a free terminal is anybody's (HoldsHand).

The hand is checked when the write is admitted, and the write happens once it is: at most one already-in-flight keystroke may land just after a release. That window is documented in docs/sharing.md rather than papered over.

func (*Controller) SetInteractiveSession added in v0.6.0

func (c *Controller) SetInteractiveSession(runID, sessionID string, active bool, operator, connID string) error

SetInteractiveSession acquires or releases the session's write lock. It is the attach verb: taking the hand is what makes keystrokes routable.

Taking a hand another operator holds is refused rather than silently stolen; the caller is expected to request control instead.

Keystrokes are never journaled, so the attach record is what says who was at the terminal, and it is journaled first: through the run's supervision core, under the same lock the hand is taken under, before any client can see the terminal held or its holder type into it. A record the journal refuses leaves the terminal as it was and fails the call, and freezes the run as the core's own entries do; once the journal has failed, no terminal is taken this way. The gateway took the hand first and wrote the record afterwards, dropping its error, so keystrokes could reach the agent before any record of the attach existed, or with none at all. Releasing is journaled after the hand is let go, best effort, as the control records are (recordControlAudit): a release that could not be journaled must still release, and the core freezes the run all the same.

A terminal is taken only for the run the caller names. The verb ignored the run, so a tab left open on a run that "Save and restart" had replaced attached to the new run's terminal, which it had never shown, and its keystrokes, which name no run, went to the new process: a run's hands go with it precisely so that such a tab holds nothing in the next one. Letting go of a terminal needs no run, since it is always safe.

func (*Controller) Start

func (c *Controller) Start(ctx context.Context) error

Start boots the supervisor runtime and begins event processing.

The run's context keeps ctx's values but not its cancellation: a run ends by StopRun, "Save and restart" or Close, which drain it in order, and never because the caller's context ended. Serve passes the context its signals cancel, and an interrupt cancelled the run before Close drained it: an answer being written was cut off and recorded as uncertain, and the exits of the agents Close then stopped were never journaled. A run started by "Save and restart" already had a context of its own.

func (*Controller) StartSession

func (c *Controller) StartSession(runID, sessionID string) error

func (*Controller) StopRun

func (c *Controller) StopRun(runID string) (AppState, error)

StopRun stops the run the caller names, and only that one. It stopped whatever run was current, whatever the caller named, so a tab left open on a replaced run, or a request naming none, stopped every agent of the new one.

The run drains first (endRun): its core admits nothing more, every session is stopped strictly, and what the core admitted reaches its journaled outcome before the runtime and its journal close. None of it holds c.mu, so every client can still read the state meanwhile; the gateway closed the runtime under c.mu without waiting, which cut an answer being written off from its journal and blocked every other call for as long as the close took. Every client is told the run is stopping, then stopped. A stopped run keeps its ID, which "Save and restart" names to start another, and nothing else: its agents and prompts were its core's, and its hands are let go.

A stop whose sessions could not all be confirmed stopped leaves the run failed and the gateway refusing to start another beside processes that may still run.

func (*Controller) StopSession

func (c *Controller) StopSession(runID, sessionID string) error

StopSession, StartSession and RestartSession go through the run's supervision core, which owns whether an agent runs: it takes in only the prompts of a running agent, so a start it did not see left the new process's prompts unsupervised. The core shows the agent stopping or starting while the operation runs, refuses one while an answer or a line is still being written to the session, drops the previous process's prompts when a start begins, freezes a session whose stop failed, and reports a failure by a fixed message rather than the backend's own text.

Each acts only on the run the caller names. The gateway ignored the run, so a tab left open on a run a profile save had replaced, or a request naming none, stopped, started or restarted the agent of the same name in the new run, which the operator had never seen.

func (*Controller) SubmitAutomaticDecision

func (c *Controller) SubmitAutomaticDecision(runID, sessionID, eventID, decision string, actor supervise.Actor) error

SubmitAutomaticDecision sends a chosen answer, allow or deny, which the adapter encodes itself. It is taken only when the prompt offers it. The gateway sent whatever the caller named as typed text, so an answer that was no choice at all reached the agent through the call meant for buttons.

func (*Controller) SubmitDecision

func (c *Controller) SubmitDecision(runID, sessionID, eventID, value string, actor supervise.Actor) error

SubmitDecision sends a person's typed answer to a prompt through the run's supervision core, which journals the decision before it writes it, keeps the prompt pending until the write has an outcome, writes one answer at a time to a session, and freezes the session when a write's outcome is unknown.

The text is always sent as typed and journaled as asked: only the adapter knows what the bytes mean. The gateway read "y", "yes" and "allow" as the adapter's allow and "n", "no" and "deny" as its deny, so typing "y" into a Generic prompt, whose adapter encodes no allow, was refused, and any other text was journaled as a human allowing something. An empty answer is refused before anything is claimed or journaled, and so is a run the caller does not name: the gateway took an empty run ID for the current run, so a tab left open on a run that had since been replaced could answer the new one. actor is who typed it, from which connection; the core refuses one whose role may only watch, as the gateway's list of the calls a viewer may make already does.

func (*Controller) SubmitLine

func (c *Controller) SubmitLine(runID, sessionID, line string, actor supervise.Actor) error

SubmitLine sends one ordinary line to a detached, running session through the core. The core journals the line before it writes it and fails closed, takes the session's one write slot, refuses the line while a prompt waits on the operator, while anybody holds the terminal or while the session is frozen, and freezes the session when the write's outcome is unknown. The gateway wrote the line beside whatever else was being written, with no check at all, journaled it afterwards with the journal's errors ignored, and ignored the run the caller named. The line's entries name the actor's identity, which is all their closed shape holds.

func (*Controller) Subscribe

func (c *Controller) Subscribe(listener func(event string, payload any)) func()

Subscribe registers an event listener called on each broadcast. Returns an unsubscribe function.

func (*Controller) TakeControl added in v0.7.0

func (c *Controller) TakeControl(sessionID, connID, operator string) (HandView, error)

TakeControl acquires a free hand. It deliberately refuses to steal a held one: the caller is told to request control instead.

It journals nothing itself. No client reaches it: a client takes a terminal through the attach verb, SetInteractiveSession, which takes the hand the same way once attach_started is journaled, so one action never produces two records and no terminal is held that the journal does not say was taken.

func (*Controller) TestNotification

func (c *Controller) TestNotification() error

func (*Controller) VerifyAuditJournal

func (c *Controller) VerifyAuditJournal() (AuditVerificationView, error)

type DecisionBreakdown

type DecisionBreakdown struct {
	Allow     int64 `json:"allow"`
	Deny      int64 `json:"deny"`
	AutoAllow int64 `json:"autoAllow"`
	AutoDeny  int64 `json:"autoDeny"`
	Custom    int64 `json:"custom"`
}

type FullSettingsView

type FullSettingsView struct {
	AgentProfilesView
	Security      SecuritySettings     `json:"security"`
	Notifications NotificationSettings `json:"notifications"`
	// SecurityPresets are the values each preset fills in, from the same
	// source the configuration loader uses.
	SecurityPresets map[string]SecuritySettings `json:"securityPresets"`
}

type HandView added in v0.7.0

type HandView struct {
	RunID             string `json:"runID"`
	SessionID         string `json:"sessionID"`
	State             string `json:"state"`
	HolderConnID      string `json:"holderConnID,omitempty"`
	HolderIdentity    string `json:"holderIdentity,omitempty"`
	RequesterConnID   string `json:"requesterConnID,omitempty"`
	RequesterIdentity string `json:"requesterIdentity,omitempty"`
	RequestExpiresAt  string `json:"requestExpiresAt,omitempty"`
	Since             string `json:"since,omitempty"`
}

HandView is a complete snapshot of one session's write lock.

type LatencyBucketView

type LatencyBucketView struct {
	Le    float64 `json:"le"`
	Label string  `json:"label"`
	Count uint64  `json:"count"`
}

type LifecycleResult

type LifecycleResult struct {
	Outcome  string            `json:"outcome"`
	State    AppState          `json:"state"`
	Profiles AgentProfilesView `json:"profiles"`
}

type NotificationEvent

type NotificationEvent struct {
	Title     string `json:"title"`
	Body      string `json:"body"`
	AgentName string `json:"agentName,omitempty"`
	SessionID string `json:"sessionID,omitempty"`
	EventID   string `json:"eventID,omitempty"`
	Kind      string `json:"kind"`
	Severity  string `json:"severity"`
	Reason    string `json:"reason,omitempty"`
	Timestamp string `json:"timestamp"`
}

NotificationEvent is broadcast to WebSocket clients when an operator alert fires.

type NotificationSettings

type NotificationSettings struct {
	Enabled     bool                         `json:"enabled"`
	Bell        bool                         `json:"bell"`
	Desktop     bool                         `json:"desktop"`
	MinSeverity string                       `json:"minSeverity"`
	Webhooks    []NotificationWebhookSetting `json:"webhooks"`
}

type NotificationWebhookSetting

type NotificationWebhookSetting struct {
	Name        string `json:"name"`
	URL         string `json:"url"`
	Format      string `json:"format"`
	MinSeverity string `json:"minSeverity"`
	Timeout     string `json:"timeout"`
	// HasHeaders says the webhook carries headers, usually a credential. Their
	// values never leave the engine, and a save keeps them.
	HasHeaders bool `json:"hasHeaders"`
}

type Options

type Options struct {
	Bind        string
	Port        int
	Token       string
	ViewerToken string
	// ViewerTerminals is "shown", the default, or "hidden": then a viewer
	// receives no terminal output, only prompt cards and notifications.
	ViewerTerminals string
	ConfigPath      string
	StaticDir       string
	Diagnostics     io.Writer
	OnReady         func(serverURL string, token string)
}

Options configures the headless Relayer web server.

type PolicyEvaluation

type PolicyEvaluation struct {
	Action         string `json:"action"`
	ProposedAction string `json:"proposedAction"`
	RuleName       string `json:"ruleName,omitempty"`
	Reason         string `json:"reason"`
	Automatic      bool   `json:"automatic"`
	DryRun         bool   `json:"dryRun"`
}

type PolicyState

type PolicyState struct {
	DefaultAction string `json:"defaultAction"`
	DryRun        bool   `json:"dryRun"`
}

type PreflightAgent

type PreflightAgent struct {
	Ordinal         int    `json:"ordinal"`
	Source          string `json:"source"`
	Command         string `json:"command"`
	Installation    string `json:"installation"`
	Adapter         string `json:"adapter,omitempty"`
	AdapterMaturity string `json:"adapterMaturity,omitempty"`
	Backend         string `json:"backend,omitempty"`
}

type PreflightAudit

type PreflightAudit struct {
	Enabled       bool   `json:"enabled"`
	Mode          string `json:"mode"`
	Location      string `json:"location"`
	MaxFileSizeMB int    `json:"maxFileSizeMB"`
	MaxFiles      int    `json:"maxFiles"`
}

type PreflightCheck

type PreflightCheck struct {
	ID          string `json:"id"`
	Scope       string `json:"scope"`
	Status      string `json:"status"`
	Summary     string `json:"summary"`
	Remediation string `json:"remediation,omitempty"`
}

type PreflightConfiguration

type PreflightConfiguration struct {
	Version         int  `json:"version"`
	Legacy          bool `json:"legacy"`
	AgentCount      int  `json:"agentCount"`
	PolicyRuleCount int  `json:"policyRuleCount"`
}

type PreflightPlatform

type PreflightPlatform struct {
	OS        string `json:"os"`
	Arch      string `json:"arch"`
	Supported bool   `json:"supported"`
}

type PreflightReport

type PreflightReport struct {
	SchemaVersion int                    `json:"schemaVersion"`
	Status        string                 `json:"status"`
	Platform      PreflightPlatform      `json:"platform"`
	Configuration PreflightConfiguration `json:"configuration"`
	Audit         PreflightAudit         `json:"audit"`
	Tools         []PreflightTool        `json:"tools"`
	Agents        []PreflightAgent       `json:"agents"`
	Checks        []PreflightCheck       `json:"checks"`
}

type PreflightTool

type PreflightTool struct {
	ProfileID    string `json:"profileID"`
	Installation string `json:"installation"`
}

type PresenceMember added in v0.7.0

type PresenceMember struct {
	ConnID         string `json:"connID"`
	Identity       string `json:"identity"`
	Role           string `json:"role"`
	Observing      bool   `json:"observing"`
	HoldsHand      bool   `json:"holdsHand"`
	RequestingHand bool   `json:"requestingHand"`
	Since          string `json:"since"`
}

PresenceMember is one connected client as the other clients of a session see it. Identity is not unique: the same operator may hold several connections, so ConnID is the addressable key everywhere the hand is concerned.

type PresenceView added in v0.7.0

type PresenceView struct {
	RunID         string           `json:"runID"`
	SessionID     string           `json:"sessionID"`
	Members       []PresenceMember `json:"members"`
	ObserverCount int              `json:"observerCount"`
}

PresenceView is a complete roster snapshot for one session. It is never a delta: a dropped broadcast must be self-healing at the next one.

type RecordingChunk added in v0.7.0

type RecordingChunk struct {
	ID         string               `json:"id"`
	Header     RecordingHeaderView  `json:"header"`
	Frames     []RecordingFrameView `json:"frames"`
	Offset     int                  `json:"offset"`
	NextOffset int                  `json:"nextOffset"`
	Complete   bool                 `json:"complete"`
}

RecordingChunk is one page of a transcript. Transcripts are paged rather than returned whole because a websocket frame large enough for a multi-megabyte cast would be dropped by the send queue instead of delivered.

type RecordingEvent added in v0.7.0

type RecordingEvent struct {
	Action    string        `json:"action"`
	Recording RecordingView `json:"recording"`
}

type RecordingFilterInput added in v0.7.0

type RecordingFilterInput struct {
	RunID     string `json:"runID,omitempty"`
	SessionID string `json:"sessionID,omitempty"`
	AgentID   string `json:"agentID,omitempty"`
	Limit     int    `json:"limit,omitempty"`
}

type RecordingFrameView added in v0.7.0

type RecordingFrameView struct {
	Time float64 `json:"time"`
	Kind string  `json:"kind"`
	Data string  `json:"data"`
}

type RecordingHeaderView added in v0.7.0

type RecordingHeaderView struct {
	Version   int    `json:"version"`
	Width     int    `json:"width"`
	Height    int    `json:"height"`
	Timestamp int64  `json:"timestamp,omitempty"`
	Title     string `json:"title,omitempty"`
}

type RecordingView added in v0.7.0

type RecordingView struct {
	ID              string  `json:"id"`
	RunID           string  `json:"runID"`
	SessionID       string  `json:"sessionID"`
	AgentID         string  `json:"agentID,omitempty"`
	Name            string  `json:"name,omitempty"`
	Backend         string  `json:"backend,omitempty"`
	Adapter         string  `json:"adapter,omitempty"`
	StartedAt       string  `json:"startedAt"`
	EndedAt         string  `json:"endedAt,omitempty"`
	DurationSeconds float64 `json:"durationSeconds"`
	Width           int     `json:"width"`
	Height          int     `json:"height"`
	Bytes           int64   `json:"bytes"`
	Frames          int     `json:"frames"`
	Truncated       bool    `json:"truncated"`
	DroppedFrames   int     `json:"droppedFrames"`
	InputRecorded   bool    `json:"inputRecorded"`
	Redacted        bool    `json:"redacted"`
	ExitCode        *int    `json:"exitCode,omitempty"`
	Active          bool    `json:"active"`
}

RecordingView is one session transcript as the web UI lists it. It carries no filesystem path: the store is addressed by id, and a path would tell a browser client where the supervising host keeps its files.

type SafeErrorEvent

type SafeErrorEvent struct {
	RunID     string `json:"runID"`
	Code      string `json:"code"`
	Message   string `json:"message"`
	SessionID string `json:"sessionID,omitempty"`
	Timestamp string `json:"timestamp"`
}

type SaveAgentProfilesAndRestartRequest

type SaveAgentProfilesAndRestartRequest struct {
	ExpectedRunID    string              `json:"expectedRunID,omitempty"`
	ExpectedRevision string              `json:"expectedRevision"`
	Profiles         []AgentProfileInput `json:"profiles"`
	// Security and Notifications, when set, are written in the same atomic
	// write as the agents rather than by a separate save beforehand.
	Security      *SecuritySettings     `json:"security,omitempty"`
	Notifications *NotificationSettings `json:"notifications,omitempty"`
}

type SaveAgentProfilesRequest

type SaveAgentProfilesRequest struct {
	ExpectedRevision string              `json:"expectedRevision"`
	Profiles         []AgentProfileInput `json:"profiles"`
}

type SaveFullSettingsRequest

type SaveFullSettingsRequest struct {
	ExpectedRevision string                `json:"expectedRevision"`
	Profiles         []AgentProfileInput   `json:"profiles,omitempty"`
	Security         *SecuritySettings     `json:"security,omitempty"`
	Notifications    *NotificationSettings `json:"notifications,omitempty"`
}

type SecuritySettings

type SecuritySettings struct {
	Profile                     string `json:"profile"`
	DefaultAction               string `json:"defaultAction"`
	DryRun                      bool   `json:"dryRun"`
	BlockDestructive            bool   `json:"blockDestructive"`
	BlockExfiltration           bool   `json:"blockExfiltration"`
	BlockSensitivePaths         bool   `json:"blockSensitivePaths"`
	BlockOutsideWorkspace       bool   `json:"blockOutsideWorkspace"`
	WorkspaceRoot               string `json:"workspaceRoot"`
	RateLimitPerMinute          int    `json:"rateLimitPerMinute"`
	MaxConsecutiveAutoDecisions int    `json:"maxConsecutiveAutoDecisions"`
}

type SnapshotEvent

type SnapshotEvent struct {
	RunID          string `json:"runID"`
	SessionID      string `json:"sessionID"`
	Revision       uint64 `json:"revision"`
	Output         string `json:"output"`
	Status         string `json:"status"`
	Running        bool   `json:"running"`
	Attached       bool   `json:"attached"`
	ObserverCount  int    `json:"observerCount"`
	HolderIdentity string `json:"holderIdentity,omitempty"`
	InputFrozen    bool   `json:"inputFrozen"`
	ExitCode       *int   `json:"exitCode,omitempty"`
}

type StatusEvent

type StatusEvent struct {
	RunID     string `json:"runID"`
	Scope     string `json:"scope"`
	Status    string `json:"status"`
	SessionID string `json:"sessionID,omitempty"`
	// ClearedBefore, when set, says the backend dropped every prompt of the
	// session detected before this time (RFC 3339): the session ended, or a
	// new process replaced it. Clients drop the same prompts; a status without
	// it, such as a stream error on a live session, leaves them answerable.
	ClearedBefore string `json:"clearedBefore,omitempty"`
}

type SupervisionEvent

type SupervisionEvent struct {
	RunID          string           `json:"runID"`
	ID             string           `json:"id"`
	SessionID      string           `json:"sessionID"`
	AgentID        string           `json:"agentID"`
	Adapter        string           `json:"adapter"`
	Type           string           `json:"type"`
	Summary        string           `json:"summary"`
	Sensitive      bool             `json:"sensitive"`
	Risk           string           `json:"risk"`
	Timestamp      string           `json:"timestamp"`
	Evaluation     PolicyEvaluation `json:"evaluation"`
	DeliveryStatus string           `json:"deliveryStatus"`
	Decisions      []string         `json:"decisions"`
	// Command is the command the prompt asks about, redacted and bounded by the
	// core. It is agent text read from the screen, so only an operator receives
	// it: a viewer's frames and state carry none (see commandForRole).
	Command string `json:"command,omitempty"`
	// ToolCall is present only when the prompt is about an MCP tool call.
	// Its parameter values are agent-controlled terminal text shown so an
	// operator can see what a tool is about to be given; they are display
	// data, never instructions, and never reach the audit journal.
	ToolCall *ToolCallView `json:"toolCall,omitempty"`
}

type TelemetrySnapshotView

type TelemetrySnapshotView struct {
	Timestamp            string              `json:"timestamp"`
	Enabled              bool                `json:"enabled"`
	PrometheusEnabled    bool                `json:"prometheusEnabled"`
	PrometheusAddress    string              `json:"prometheusAddress,omitempty"`
	OTLPEnabled          bool                `json:"otlpEnabled"`
	OTLPEndpoint         string              `json:"otlpEndpoint,omitempty"`
	SessionsActive       int64               `json:"sessionsActive"`
	EventsPending        int64               `json:"eventsPending"`
	SessionsTotal        int64               `json:"sessionsTotal"`
	EventsDetectedTotal  int64               `json:"eventsDetectedTotal"`
	EventsWithdrawnTotal int64               `json:"eventsWithdrawnTotal"`
	DecisionsTotal       int64               `json:"decisionsTotal"`
	DecisionsBreakdown   DecisionBreakdown   `json:"decisionsBreakdown"`
	OperatorInputsTotal  int64               `json:"operatorInputsTotal"`
	GuardrailsTotal      int64               `json:"guardrailsTotal"`
	GuardrailsBreakdown  map[string]int64    `json:"guardrailsBreakdown"`
	AverageReactionTime  float64             `json:"averageReactionTime"`
	DecisionDurations    []LatencyBucketView `json:"decisionDurations"`
}

type ToolCallParamView added in v0.8.0

type ToolCallParamView struct {
	Name      string `json:"name"`
	Value     string `json:"value"`
	Truncated bool   `json:"truncated,omitempty"`
}

type ToolCallView added in v0.8.0

type ToolCallView struct {
	Server          string              `json:"server"`
	Tool            string              `json:"tool"`
	Risk            string              `json:"risk"`
	Params          []ToolCallParamView `json:"params,omitempty"`
	ParamsTruncated bool                `json:"paramsTruncated,omitempty"`
}

type UserInfo added in v0.6.0

type UserInfo struct {
	Identity string `json:"identity"`
	ConnID   string `json:"connID"`
	Role     string `json:"role"`
	ReadOnly bool   `json:"readOnly"`
	// TerminalsHidden tells a viewer's interface that the gateway sends it no
	// terminal output, so it shows why rather than an empty terminal.
	TerminalsHidden bool `json:"terminalsHidden,omitempty"`
}

type UserRole added in v0.6.0

type UserRole string

UserRole defines the privilege level of an authenticated client.

const (
	RoleOperator UserRole = "operator"
	RoleViewer   UserRole = "viewer"
)

Jump to

Keyboard shortcuts

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