Documentation
¶
Overview ¶
Package host implements the docsgpt-cli host daemon and pairing flow.
Index ¶
- Constants
- Variables
- func CanonicalPayload(method, path, ts string, body []byte) string
- func ClearLocalState() ([]string, error)
- func CurrentSystemUser() (string, error)
- func DaemonReload(mode ServiceMode) error
- func DeleteWindowsTask() error
- func DetachConsole()
- func DisableNow(mode ServiceMode) error
- func EnableNow(mode ServiceMode) error
- func EndWindowsTask() error
- func EnterServiceMode(logFile string) error
- func HasLocalState() bool
- func HostConfigPath() string
- func HumanDuration(d time.Duration) string
- func InstallService(systemFlag bool, runUserFlag string) error
- func IsDarwin() bool
- func IsLinux() bool
- func IsWindows() bool
- func JournalArgs(mode ServiceMode) []string
- func LaunchdBootout(mode ServiceMode, plistPath string) error
- func LaunchdBootstrap(mode ServiceMode, plistPath string) error
- func LaunchdBootstrapCmd(mode ServiceMode, plistPath string) string
- func LaunchdDomainTarget(mode ServiceMode) string
- func LaunchdLogPath(mode ServiceMode, runUser string) (string, error)
- func LaunchdPlistPath(mode ServiceMode, runUser string) (string, error)
- func LaunchdServiceTarget(mode ServiceMode) string
- func LogStamp(t time.Time) string
- func RegisterWindowsTask(xmlPath string) error
- func RenderLaunchdPlist(mode ServiceMode, exec, runUser, logPath string) string
- func RenderServiceUnit(mode ServiceMode, exec, systemUser string) string
- func RenderWindowsTaskXML(execPath, runUser string) string
- func ResolveExecutable() (string, error)
- func RevokeFromServer(cfg HostConfig) error
- func RunDaemon(opts DaemonOptions) error
- func ServiceInstallSupported() bool
- func ServiceUnitPath(mode ServiceMode) (string, error)
- func ShowStartupBanner(cfg HostConfig)
- func SpoolDir() string
- func StartWindowsTask() error
- func SystemctlArgs(mode ServiceMode) []string
- func UninstallService() error
- func WindowsTaskInstalled() bool
- func WindowsTaskRegisterCmd(xmlPath string) string
- func WindowsTaskXMLPath() string
- func WriteLaunchdPlist(path, content string, mode ServiceMode) error
- func WriteUnitFile(path, content string, mode ServiceMode) error
- func WriteWindowsTaskXML(path, content string) error
- type Baton
- type DaemonOptions
- type DeviceMe
- type HostConfig
- type HostKey
- type Invocation
- type Jobs
- type PairRequest
- type PairResponse
- type PollResponse
- type ServiceMode
- func DetectInstalledLaunchdMode() (mode ServiceMode, plistPath string, found bool)
- func DetectInstalledMode() (mode ServiceMode, found bool)
- func ResolveLaunchdMode(isRoot, systemFlag bool, userFlag, sudoUser string) (mode ServiceMode, runUser, note string)
- func ResolveServiceMode(isRoot, systemFlag bool, userFlag, sudoUser string) (mode ServiceMode, runUser, note string)
- type State
- type Transport
Constants ¶
const LaunchdLabel = "com.arc53.docsgpt-cli-host"
LaunchdLabel is the launchd job label for the host daemon. Doubles as the plist basename (Label + ".plist").
const ServiceUnitName = "docsgpt-cli-host"
ServiceUnitName is the systemd unit name (no extension) for the host daemon.
const WindowsTaskName = "docsgpt-cli-host"
WindowsTaskName is the Task Scheduler task name for the host daemon, mirroring ServiceUnitName (systemd) and LaunchdLabel (launchd).
Variables ¶
var ErrAuthRejected = errors.New("auth rejected")
ErrAuthRejected is the sentinel PollOnce / RunSSE return on 401. Callers (RunPolling) escalate this to ErrRevoked once they have seen at least one successful poll, or immediately when the very first poll is rejected (which indicates a bad token from the start).
var ErrRevoked = errors.New("device session revoked")
ErrRevoked is returned by RunPolling / RunSSE when the server has revoked the device session. cmd/host.go translates this into a non-zero exit so the daemon does not poll forever against a dead token.
var ErrUnsupportedOS = errors.New(
"install-service supports linux (systemd), macOS (launchd), and " +
"Windows (Task Scheduler) only; run `docsgpt-cli host` manually " +
"on this platform",
)
ErrUnsupportedOS is returned by service operations on platforms without a supported service backend (anything other than Linux/systemd, macOS/launchd, or Windows/Task Scheduler).
Functions ¶
func CanonicalPayload ¶
CanonicalPayload builds the string that gets signed / verified: "METHOD PATH TIMESTAMP SHA256_HEX(BODY)". The body hash binds the request body into the signature so a captured signature can't be replayed with a tampered body inside the timestamp window. For a request with no body (GET) this is the SHA-256 of the empty string.
KEEP IN SYNC with the backend verifier (application/api/devices/auth.py `_canonical_payload`). The hex encoding and single-space separators must match byte-for-byte.
func ClearLocalState ¶ added in v1.7.0
ClearLocalState deletes host.yml, host.key and the spool without contacting the server and returns the names of what it removed.
func CurrentSystemUser ¶
CurrentSystemUser returns the OS user the CLI is running as. Used as a sensible default for `--system --user`.
func DaemonReload ¶
func DaemonReload(mode ServiceMode) error
DaemonReload runs `systemctl [--user] daemon-reload`.
func DeleteWindowsTask ¶
func DeleteWindowsTask() error
DeleteWindowsTask removes the scheduled task registration.
func DetachConsole ¶
func DetachConsole()
DetachConsole is a no-op outside Windows; systemd and launchd services have no console window to shed.
func DisableNow ¶
func DisableNow(mode ServiceMode) error
DisableNow runs `systemctl [--user] disable --now <unit>`.
func EnableNow ¶
func EnableNow(mode ServiceMode) error
EnableNow runs `systemctl [--user] enable --now <unit>`.
func EndWindowsTask ¶
func EndWindowsTask() error
EndWindowsTask stops a running instance of the task. Failing is normal when no instance is running.
func EnterServiceMode ¶
EnterServiceMode reroutes the daemon's output for running under a service manager with no terminal attached (the Windows scheduled task): stdout and stderr append to `logFile`, styling is disabled so the log stays free of ANSI escapes, and any attached console window is dropped. Callers must invoke it before printing anything.
func HasLocalState ¶ added in v1.7.0
func HasLocalState() bool
HasLocalState reports whether host.yml, host.key or the spool exists.
func HostConfigPath ¶
func HostConfigPath() string
HostConfigPath returns the absolute path to host.yml.
func HumanDuration ¶ added in v1.7.0
HumanDuration renders a Duration as 14s, 2m, 1h, etc. Trades precision for legibility — these are status lines, not metrics.
func InstallService ¶ added in v1.7.0
InstallService installs the daemon with the platform's service backend (systemd on Linux, launchd on macOS, Task Scheduler on Windows). Zero flags let the mode resolver pick system-vs-user from root. Flag-specific guards such as "--system requires root" are the caller's.
func JournalArgs ¶
func JournalArgs(mode ServiceMode) []string
JournalArgs returns the args prefix to inspect logs via journalctl.
func LaunchdBootout ¶
func LaunchdBootout(mode ServiceMode, plistPath string) error
LaunchdBootout unloads the plist from launchd, trying the modern `launchctl bootout <domain> <plist>` first and falling back to the legacy `launchctl unload -w <plist>`.
func LaunchdBootstrap ¶
func LaunchdBootstrap(mode ServiceMode, plistPath string) error
LaunchdBootstrap loads the plist into launchd. It tries the modern `launchctl bootstrap <domain> <plist>` first and, if that fails, falls back to the legacy `launchctl load -w <plist>`. Returns the error from the fallback (or nil) so the caller can surface manual steps when both fail.
func LaunchdBootstrapCmd ¶
func LaunchdBootstrapCmd(mode ServiceMode, plistPath string) string
LaunchdBootstrapCmd returns the manual bootstrap command string, shown to the user when the automatic load fails.
func LaunchdDomainTarget ¶
func LaunchdDomainTarget(mode ServiceMode) string
LaunchdDomainTarget returns the launchctl domain target for the given mode: "gui/<uid>" for a LaunchAgent, "system" for a LaunchDaemon.
func LaunchdLogPath ¶
func LaunchdLogPath(mode ServiceMode, runUser string) (string, error)
LaunchdLogPath returns the log file path written into the plist. Both modes log to the runtime user's ~/.docsgpt/host.log so the file is owned by, and readable to, the user the daemon runs as.
func LaunchdPlistPath ¶
func LaunchdPlistPath(mode ServiceMode, runUser string) (string, error)
LaunchdPlistPath returns the absolute plist path for the given mode. LaunchAgent goes under the runtime user's ~/Library/LaunchAgents; LaunchDaemon under /Library/LaunchDaemons.
func LaunchdServiceTarget ¶
func LaunchdServiceTarget(mode ServiceMode) string
LaunchdServiceTarget returns the launchctl service target ("<domain>/<label>") used by `launchctl print`.
func RegisterWindowsTask ¶
RegisterWindowsTask creates (or replaces) the scheduled task from the XML definition at `xmlPath`.
func RenderLaunchdPlist ¶
func RenderLaunchdPlist(mode ServiceMode, exec, runUser, logPath string) string
RenderLaunchdPlist returns the plist XML for the given mode.
`exec` is the absolute path to the docsgpt-cli binary; `logPath` is the StandardOut/StandardError destination. For ServiceModeSystem, `runUser` (when non-empty) is emitted as a UserName key so the daemon does not run as root; it is ignored for ServiceModeUser (LaunchAgents run as the user).
KeepAlive is {SuccessfulExit=false}: restart on a non-zero exit (crash), but not on a clean exit (a revoke exits 0), so a revoked device does not restart-loop.
func RenderServiceUnit ¶
func RenderServiceUnit(mode ServiceMode, exec, systemUser string) string
RenderServiceUnit returns the systemd unit file content for the given mode.
`exec` is the absolute path to the docsgpt-cli binary. For ServiceModeSystem, `systemUser` is the OS user the service should run as and must be non-empty; for ServiceModeUser it is ignored.
func RenderWindowsTaskXML ¶
RenderWindowsTaskXML returns the Task Scheduler task definition for the host daemon: run at logon as `runUser`, restart on failure, no run-time limit. The action invokes `exec host --service` so the daemon drops its console window and appends to the host log file instead of a terminal.
Two settings depart from Task Scheduler defaults on purpose: ExecutionTimeLimit PT0S (the default PT72H would kill the daemon after three days) and the battery settings (a laptop host should keep running unplugged). RestartOnFailure mirrors systemd Restart=on-failure; a revoke exits 0, which Task Scheduler does not treat as a failure, so a revoked device stays stopped.
func ResolveExecutable ¶
ResolveExecutable returns the symlink-evaluated absolute path to the currently-running CLI binary. The unit file should not reference a volatile symlink such as a freshly-built /tmp path.
func RevokeFromServer ¶
func RevokeFromServer(cfg HostConfig) error
RevokeFromServer hits DELETE /api/devices/{id} using the stored token.
func RunDaemon ¶ added in v1.7.0
func RunDaemon(opts DaemonOptions) error
RunDaemon loads host.yml and runs the long-lived poll/stream loop until SIGINT/SIGTERM (nil) or a revoke (ErrRevoked, after printing why; the caller exits 0 so service managers do not restart a revoked device).
func ServiceInstallSupported ¶
func ServiceInstallSupported() bool
ServiceInstallSupported reports whether install-service has a service backend for the current platform (systemd on Linux, launchd on macOS, Task Scheduler on Windows).
func ServiceUnitPath ¶
func ServiceUnitPath(mode ServiceMode) (string, error)
ServiceUnitPath returns the absolute filesystem location of the unit file.
func ShowStartupBanner ¶
func ShowStartupBanner(cfg HostConfig)
ShowStartupBanner prints the connection banner on daemon start. The approval_mode line is fetched live (server is the source of truth, and the user can change it in the UI at any time); on any failure the banner degrades to a no-value line rather than printing a stale local value.
func SpoolDir ¶ added in v1.9.0
func SpoolDir() string
SpoolDir holds the reports of commands not yet delivered to the server (one journal per invocation), so a restart does not lose them.
func StartWindowsTask ¶
func StartWindowsTask() error
StartWindowsTask starts the scheduled task immediately (the logon trigger only fires on the next logon).
func SystemctlArgs ¶
func SystemctlArgs(mode ServiceMode) []string
SystemctlArgs returns the args prefix to invoke systemctl for the given mode.
func UninstallService ¶ added in v1.7.0
func UninstallService() error
UninstallService removes whichever service InstallService set up.
func WindowsTaskInstalled ¶
func WindowsTaskInstalled() bool
WindowsTaskInstalled reports whether the scheduled task is registered.
func WindowsTaskRegisterCmd ¶
WindowsTaskRegisterCmd returns the manual registration command string, shown to the user when the automatic `schtasks /Create` fails.
func WindowsTaskXMLPath ¶
func WindowsTaskXMLPath() string
WindowsTaskXMLPath returns where the rendered task definition lives (~/.docsgpt/docsgpt-cli-host.task.xml). The file stays on disk after install so a failed `schtasks /Create` can be retried by hand.
func WriteLaunchdPlist ¶
func WriteLaunchdPlist(path, content string, mode ServiceMode) error
WriteLaunchdPlist writes the plist at `path` with 0644 permissions, creating parent directories (0755 for daemons, 0700 for the per-user agent dir under ~). It also ensures the log file's parent directory exists.
func WriteUnitFile ¶
func WriteUnitFile(path, content string, mode ServiceMode) error
WriteUnitFile writes the unit file at `path` with 0644 permissions, creating any missing parent directories with mode 0755 (system) or 0700 (user, under ~).
func WriteWindowsTaskXML ¶
WriteWindowsTaskXML writes the task definition at `path`, creating the parent directory (0700 — it is ~/.docsgpt) when missing.
Types ¶
type Baton ¶
type Baton struct {
// contains filtered or unexported fields
}
Baton is the single-flight guard between Polling and Streaming goroutines.
func (*Baton) LastActivity ¶
LastActivity returns the zero value before the first successful poll.
func (*Baton) SessionID ¶
SessionID returns the currently-active SSE session id (empty when polling).
func (*Baton) SetSessionID ¶
SetSessionID stores the session id for the active SSE.
func (*Baton) TouchActivity ¶
func (b *Baton) TouchActivity()
TouchActivity records that the daemon successfully reached the server.
func (*Baton) Transition ¶
Transition atomically swaps state if `from` matches. Returns true on success, false if a different state is currently active.
type DaemonOptions ¶ added in v1.7.0
type DaemonOptions struct {
PollInterval string // overrides host.yml's poll_interval when set
ServiceMode bool // log to the host log file instead of the terminal
Version string
AutoUpdate string // update.ModeOn / ModeNotify / ModeOff
}
DaemonOptions configures RunDaemon.
type DeviceMe ¶
type DeviceMe struct {
ID string `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
OS string `json:"os"`
Status string `json:"status"`
ApprovalMode string `json:"approval_mode"`
Description string `json:"description"`
PairedAt string `json:"paired_at"`
LastSeenAt string `json:"last_seen_at"`
}
DeviceMe is the shape of GET /api/devices/me — the live, server-side view of this device. The server is the source of truth for approval_mode.
func FetchDeviceMe ¶
func FetchDeviceMe(cfg HostConfig, timeout time.Duration) (d *DeviceMe, unauthorized bool, err error)
FetchDeviceMe queries the live server-side view of this device. A nil device with unauthorized=false means the server was unreachable; unauthorized=true means the device has been revoked. `timeout` bounds the request — callers that must not block (e.g. the startup banner) pass a short one.
type HostConfig ¶
type HostConfig struct {
DeviceID string `yaml:"device_id"`
SessionToken string `yaml:"session_token"`
BaseURL string `yaml:"base_url"`
PollInterval string `yaml:"poll_interval"`
ApprovalMode string `yaml:"approval_mode"`
LogFile string `yaml:"log_file"`
}
HostConfig is the persistent state at ~/.docsgpt/host.yml.
func DefaultHostConfig ¶
func DefaultHostConfig() HostConfig
DefaultHostConfig returns the canonical defaults for a fresh host.yml.
func LoadHostConfig ¶
func LoadHostConfig() (HostConfig, error)
LoadHostConfig reads host.yml; returns the defaults + os.IsNotExist if missing.
func (*HostConfig) PollIntervalDuration ¶
func (c *HostConfig) PollIntervalDuration() time.Duration
PollIntervalDuration converts the textual poll_interval into a time.Duration.
func (*HostConfig) Save ¶
func (c *HostConfig) Save() error
Save persists the config to disk with 0600 permissions.
type HostKey ¶
type HostKey struct {
Private ed25519.PrivateKey
Public ed25519.PublicKey
}
HostKey holds the Ed25519 keypair used to sign requests to the server.
func LoadOrCreateKey ¶
LoadOrCreateKey returns the persisted key, generating + persisting one on first use. The private key is stored alongside the public key, base64- encoded, one per line: `priv\npub\n`. File mode is 0600.
func (*HostKey) Fingerprint ¶
Fingerprint returns the hex SHA-256 of the public key bytes. The server stores the same hex digest in devices.machine_pubkey_fingerprint.
func (*HostKey) PublicKeyB64 ¶
PublicKeyB64 returns the base64-encoded public key for pairing payloads.
type Invocation ¶
type Invocation struct {
InvocationID string `json:"invocation_id"`
Action string `json:"action"`
Params map[string]interface{} `json:"params"`
ApprovalMode string `json:"approval_mode"`
IssuedAt string `json:"issued_at"`
ConversationID string `json:"conversation_id,omitempty"`
AgentID string `json:"agent_id,omitempty"`
}
Invocation envelope received on SSE.
type Jobs ¶ added in v1.9.0
type Jobs struct {
// contains filtered or unexported fields
}
Jobs runs the daemon's invocations and delivers their reports. A command runs in its own goroutine under no session, so it outlives the SSE session that delivered it; its ack, output and exit code go through an outbox.
func (*Jobs) Cancel ¶ added in v1.9.0
Cancel stops a running invocation. One that is unknown or already finished is ignored: its report stands.
func (*Jobs) Idle ¶ added in v1.9.0
Idle reports whether no command runs and every report was delivered, so the daemon may restart.
func (*Jobs) Kick ¶ added in v1.9.0
func (j *Jobs) Kick()
Kick makes every outbox waiting to retry try again now (the daemon is back online).
func (*Jobs) Recover ¶ added in v1.9.0
Recover delivers the reports a previous daemon left in the spool and returns how many. A command that was still running then cannot be reattached: it is reported as interrupted. Reports for another pairing, or past their give-up time, are deleted.
func (*Jobs) Shutdown ¶ added in v1.9.0
Shutdown stops accepting commands, stops the running ones (reported as host_shutdown) and waits up to the flush time for reports to go out; what is left stays in the spool for the next start. When force closes, commands are killed at once and nothing more is waited for.
func (*Jobs) Start ¶ added in v1.9.0
func (j *Jobs) Start(inv Invocation)
Start runs an invocation in the background. Called from the SSE loop, so a cancel that arrives later on the same stream always finds it.
type PairRequest ¶
type PairRequest struct {
UserCode string `json:"user_code"`
Hostname string `json:"hostname"`
OS string `json:"os"`
Arch string `json:"arch"`
CLIVersion string `json:"cli_version"`
MachinePubkey string `json:"machine_pubkey"`
}
PairRequest is the JSON body sent to /api/devices/pairings/redeem.
type PairResponse ¶
type PairResponse struct {
DeviceID string `json:"device_id"`
SessionToken string `json:"session_token"`
Name string `json:"name"`
}
PairResponse mirrors the redeem response on success.
type PollResponse ¶
type PollResponse struct {
SessionTicket string `json:"session_ticket"`
SessionURL string `json:"session_url"`
ExpiresIn int `json:"expires_in"`
}
PollResponse is the JSON body returned on poll work-queued (HTTP 200).
type ServiceMode ¶
type ServiceMode int
ServiceMode is user (~/.config/systemd/user) or system (/etc/systemd/system).
const ( ServiceModeUser ServiceMode = iota ServiceModeSystem )
func DetectInstalledLaunchdMode ¶
func DetectInstalledLaunchdMode() (mode ServiceMode, plistPath string, found bool)
DetectInstalledLaunchdMode looks for an existing plist and reports whether it is a LaunchDaemon (system) or LaunchAgent (user). Returns found=false when neither exists. Mirrors DetectInstalledMode for systemd.
func DetectInstalledMode ¶
func DetectInstalledMode() (mode ServiceMode, found bool)
DetectInstalledMode looks for an existing unit file and reports whether it lives in the user-scope or system-scope path. Returns `found=false` when neither file exists.
func ResolveLaunchdMode ¶
func ResolveLaunchdMode(isRoot, systemFlag bool, userFlag, sudoUser string) (mode ServiceMode, runUser, note string)
ResolveLaunchdMode decides the launchd install mode and runtime user, parallel to ResolveServiceMode but with launchd terminology in the note. Non-root installs a LaunchAgent (runs as the invoking user, on login); root (or explicit --system) installs a LaunchDaemon (runs at boot).
For a LaunchDaemon with no explicit --user, it prefers $SUDO_USER and falls back to root (with a warning). `note` is a possibly-empty, possibly-multi-line message explaining an automatic choice.
func ResolveServiceMode ¶
func ResolveServiceMode(isRoot, systemFlag bool, userFlag, sudoUser string) (mode ServiceMode, runUser, note string)
ResolveServiceMode decides the install mode and runtime user for install-service from the invocation context. Precedence: an explicit --system flag is respected; otherwise root defaults to system mode and non-root to user mode (the daemon then runs as the invoking user).
In system mode with no explicit --user, it prefers $SUDO_USER (the human who sudo'd in) and falls back to root. `note` is a one-line, possibly empty, message for the user explaining an automatic choice.
func (ServiceMode) String ¶
func (m ServiceMode) String() string
type Transport ¶
type Transport struct {
Cfg HostConfig
Key *HostKey
Version string
Baton *Baton
Client *http.Client
OnInvocation func(inv Invocation)
OnCancel func(invocationID string) // `event: cancel`
OnReconnect func() // a poll succeeded after failing
}
Transport carries the cross-cutting state needed by both the polling and SSE loops: config, identity, baton, an HTTP client, and event handlers.
func NewTransport ¶
func NewTransport(cfg HostConfig, key *HostKey, version string) *Transport
NewTransport wires the standard collaborators together.
func (*Transport) PollOnce ¶
PollOnce sends a single `GET /api/devices/poll` request and returns the (ticket, queued?) tuple. `queued == false` is the 202-no-work response.
func (*Transport) RunPolling ¶
RunPolling runs the polling loop until `ctx` is cancelled or a session ticket arrives. On ticket, swaps the baton to Streaming and returns the ticket so the caller can upgrade. Returns ErrRevoked when the server rejects the session token with 401 — immediately if no prior poll has succeeded (the token was bad from the start), or after `authRejectThreshold` consecutive 401s after at least one success (the server has revoked the device since the daemon started).
func (*Transport) RunSSE ¶
RunSSE opens the SSE stream for `sessionID` and dispatches events to `t.OnInvocation` and `t.OnCancel` until the server closes or `ctx` cancels. Other event names are ignored. Returns ErrRevoked on a 401 (device revoked while a session was being negotiated) or when an `event: revoke` arrives on the open stream.