daemon

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: May 27, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DefaultPort is the well-known daemon listening port.
	DefaultPort = 19285
	// DefaultChromePort is the well-known Chrome proxy port.
	DefaultChromePort = 19286
	// DefaultProxyPort is the well-known HTTPS proxy port.
	DefaultProxyPort = 19287

	// DockerHost is the hostname Docker provides for reaching the host machine
	// from inside a container. Enabled by --add-host=host.docker.internal:host-gateway.
	DockerHost = "host.docker.internal"
)
View Source
const ReadyForReviewHeader = "[human:ready-for-review]"

ReadyForReviewHeader is the single-line magic header that identifies a review handoff in a PM-ticket comment. See cli/CLAUDE.md "Review handoff".

View Source
const ReviewCompleteHeader = "[human:review-complete]"

ReviewCompleteHeader is the matching follow-up header posted after the reviewer agent finishes. Presence of a *newer* review-complete comment clears the ready-for-review flag, so the TUI stops showing (R) once a review has actually landed.

Variables

This section is empty.

Functions

func ConnectedPath

func ConnectedPath() string

ConnectedPath returns the default path for the connected PIDs file.

func GenerateToken

func GenerateToken() (string, error)

GenerateToken returns a cryptographically random 32-byte hex string.

func GetHookSnapshot

func GetHookSnapshot(addr, token string) (map[string]hookevents.SessionSnapshot, error)

GetHookSnapshot fetches the current per-session hook state from the daemon.

func GetLogMode

func GetLogMode(addr, token string) (string, error)

GetLogMode fetches the current traffic log mode from the daemon.

func GetToolStats

func GetToolStats(addr, token string) (*stats.ToolStats, error)

GetToolStats fetches pre-aggregated tool call statistics from the daemon.

func GetTrackerDiagnose

func GetTrackerDiagnose(addr, token string) ([]tracker.TrackerStatus, error)

GetTrackerDiagnose fetches tracker credential status from the daemon.

func InfoPath

func InfoPath() string

InfoPath returns the default path for the daemon info file (~/.human/daemon.json).

func IsProcessAlive

func IsProcessAlive(pid int) bool

IsProcessAlive checks whether a process with the given PID is still running.

func IsReviewComplete

func IsReviewComplete(body string) bool

IsReviewComplete reports whether the comment body is a review-complete follow-up, which supersedes any earlier handoff for the same engineering keys.

func LoadOrCreateToken

func LoadOrCreateToken() (string, error)

LoadOrCreateToken reads the token from disk, or generates and persists a new one.

func LogPath

func LogPath() string

LogPath returns the path to the daemon log file (~/.human/daemon.log).

func ParseEngineeringKeysFromHandoff

func ParseEngineeringKeysFromHandoff(body string) []string

ParseEngineeringKeysFromHandoff extracts the engineering ticket keys listed on the `engineering:` line of a [human:ready-for-review] comment body. Returns nil if the body is not a handoff block or has no engineering line.

The comment body must START with ReadyForReviewHeader so a comment that merely quotes the header (e.g. in a discussion) does not trigger a handoff.

func ParseHookEventArgs

func ParseHookEventArgs(args []string) hookevents.Event

ParseHookEventArgs converts daemon request args into a hook event. Expected args: [event, session_id, cwd, notification_type, tool_name, error_type, agent_name].

Every field is length-capped and Cwd must be absolute — both as defence against abusive clients that could otherwise poison the in-memory hook store or write relative paths that collide with registered project directories.

func PidPath

func PidPath() string

PidPath returns the path to the daemon PID file (~/.human/daemon.pid).

func ReadAlivePid

func ReadAlivePid() (int, bool)

ReadAlivePid reads the PID file and checks if the process is alive. Returns (0, false) if no PID file exists or the process is dead.

func ReadConnected

func ReadConnected(path string) []int

ReadConnected reads connected PIDs from path. Returns nil on any error.

func RemoveConnected

func RemoveConnected(path string)

RemoveConnected removes the connected PIDs file (best-effort).

func RemoveInfo

func RemoveInfo()

RemoveInfo removes the daemon info file (best-effort).

func RemovePidFile

func RemovePidFile()

RemovePidFile removes the PID file (best-effort).

func RunAgentCleanup

func RunAgentCleanup(ctx context.Context, store *HookEventStore, cleaner AgentCleaner, logger zerolog.Logger)

RunAgentCleanup watches for SessionEnd hook events from devcontainer agents and automatically stops the container and removes the worktree.

func RunAgentZombieSweep

func RunAgentZombieSweep(ctx context.Context, sweeper AgentZombieSweeper, logger zerolog.Logger)

RunAgentZombieSweep periodically checks for agent containers that are still running but have no Claude process. This catches cases where Claude failed to start, crashed without firing hook events, or the user killed the tmux pane.

func RunRemote

func RunRemote(addr, token string, args []string, version string) (int, error)

RunRemote connects to the daemon at addr, sends the CLI args, and returns the exit code. Stdout and stderr are written to os.Stdout and os.Stderr.

func RunRemoteCapture

func RunRemoteCapture(addr, token string, args []string) ([]byte, error)

RunRemoteCapture connects to the daemon and runs args, returning stdout as bytes instead of printing to os.Stdout.

func SendConfirmDecision

func SendConfirmDecision(addr, token, id string, approved bool) error

SendConfirmDecision sends a confirmation decision for a pending destructive operation.

func SetLogMode

func SetLogMode(addr, token, mode string) (string, error)

SetLogMode sets the traffic log mode on the daemon. Returns the new mode.

func Subscribe

func Subscribe(addr, token string) (<-chan SubscribeEvent, func(), error)

Subscribe opens a persistent connection to the daemon's subscribe endpoint. It returns a channel that receives a signal each time the daemon's state changes, and a cleanup function that closes the connection. The channel is closed when the connection drops or cleanup is called.

func TokenPath

func TokenPath() string

TokenPath returns the default path for the daemon token file.

func WriteConnected

func WriteConnected(path string, pids []int) error

WriteConnected atomically writes the connected PIDs to path. On rename failure the temporary file is removed so a crashed daemon does not leave orphan .tmp files that survive across restarts when the process is killed with SIGKILL.

func WriteInfo

func WriteInfo(info DaemonInfo) error

WriteInfo writes the daemon info as JSON to InfoPath with restricted permissions.

func WritePidFile

func WritePidFile(pid int) error

WritePidFile writes the given PID to the PID file.

Types

type AgentCleaner

type AgentCleaner interface {
	DeleteAgent(ctx context.Context, name string) error
	// DecommissionAgent removes the agent from the list immediately and
	// returns the container ID for background teardown. This makes
	// "human agent list" responsive while the slow container stop happens
	// asynchronously.
	DecommissionAgent(name string) (containerID string, err error)
	// StopContainer stops and removes a container by ID.
	StopContainer(ctx context.Context, containerID string) error
}

AgentCleaner stops and removes an agent by name.

type AgentInfo

type AgentInfo struct {
	Name        string
	ContainerID string
	CreatedAt   time.Time
}

AgentInfo holds the minimum metadata needed by the zombie sweep.

type AgentZombieSweeper

type AgentZombieSweeper interface {
	RunningAgents() ([]AgentInfo, error)
	IsProcessRunning(ctx context.Context, containerID string, process string) (bool, error)
	DeleteAgent(ctx context.Context, name string) error
}

AgentZombieSweeper checks for orphaned agent containers whose main process has exited but the container is still running.

type BrowserOpener

type BrowserOpener interface {
	Open(url string) error
}

BrowserOpener opens a URL in the browser. Extracted for testability.

type ConnectedTracker

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

ConnectedTracker maintains a thread-safe set of recently-seen client PIDs. Each PID has a last-seen timestamp; Prune removes entries older than a TTL.

func NewConnectedTracker

func NewConnectedTracker() *ConnectedTracker

NewConnectedTracker creates an empty tracker.

func (*ConnectedTracker) PIDs

func (t *ConnectedTracker) PIDs() []int

PIDs returns a sorted snapshot of currently tracked PIDs.

func (*ConnectedTracker) Prune

func (t *ConnectedTracker) Prune(ttl time.Duration)

Prune removes PIDs not seen within ttl.

func (*ConnectedTracker) Touch

func (t *ConnectedTracker) Touch(pid int)

Touch records or refreshes a PID with the current time.

type DaemonInfo

type DaemonInfo struct {
	Addr       string `json:"addr"`
	ChromeAddr string `json:"chrome_addr,omitempty"`
	ProxyAddr  string `json:"proxy_addr,omitempty"`
	Token      string `json:"token,omitempty"`
	PID        int    `json:"pid,omitempty"`
	// Version carries the daemon binary's build version so clients can warn
	// about skew between the running daemon and the CLI binary.
	// omitempty preserves backward-compatibility with daemon.json files
	// written by older builds that do not emit this field.
	Version  string        `json:"version,omitempty"`
	Projects []ProjectInfo `json:"projects,omitempty"`
}

DaemonInfo holds the runtime details of a running daemon instance.

func ReadInfo

func ReadInfo() (DaemonInfo, error)

ReadInfo reads and unmarshals the daemon info from InfoPath.

func (DaemonInfo) IsAlive

func (d DaemonInfo) IsAlive() bool

IsAlive checks whether the daemon process identified by PID is still running.

func (DaemonInfo) IsReachable

func (d DaemonInfo) IsReachable() bool

IsReachable checks whether the daemon is accepting TCP connections at its advertised address. This works across process namespaces (e.g. host ↔ devcontainer) where PID-based checks fail.

type HookEventStore

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

HookEventStore is a thread-safe ring buffer of recent hook events. It stores raw events and can derive per-session snapshots on demand. Subscribers are notified (non-blocking) whenever a new event is appended.

func NewHookEventStore

func NewHookEventStore() *HookEventStore

NewHookEventStore creates an empty store.

func (*HookEventStore) Append

func (s *HookEventStore) Append(evt hookevents.Event)

Append adds a hook event. Before appending, events for the same session beyond maxHookEventsPerSession are dropped so one chatty client cannot push other sessions out of the shared buffer. The aggregate cap then drops the oldest events across all sessions.

func (*HookEventStore) EventsSince

func (s *HookEventStore) EventsSince(since uint64) ([]hookevents.Event, uint64)

EventsSince returns the events appended after the given sequence, plus the current high-water sequence to pass on the next call. Sequences are monotonic and independent of the ring's length, so a subscriber keeps receiving new events even after the ring saturates and stops growing — the failure mode of tracking deltas by slice length. Pass 0 for the first call.

func (*HookEventStore) RecentEvents

func (s *HookEventStore) RecentEvents() []hookevents.Event

RecentEvents returns a copy of all stored events.

func (*HookEventStore) Snapshot

func (s *HookEventStore) Snapshot() map[string]hookevents.SessionSnapshot

Snapshot returns the current per-session state derived from all stored events.

func (*HookEventStore) Subscribe

func (s *HookEventStore) Subscribe() chan struct{}

Subscribe returns a channel that receives a signal whenever a new event is appended. The channel has a buffer of 1 so a single pending notification is coalesced. Call Unsubscribe to clean up.

func (*HookEventStore) Unsubscribe

func (s *HookEventStore) Unsubscribe(ch chan struct{})

Unsubscribe removes a previously registered channel from the subscriber list. The channel is not closed — subscribers must stop reading from it after calling Unsubscribe and let it be garbage collected. This avoids coordinating with any concurrent Append on a removed channel.

type NetworkEvent

type NetworkEvent struct {
	Source   string    `json:"source"` // "proxy" | "oauth" | "fail"
	Status   string    `json:"status"` // "forward" | "intercept" | "block" | "no-sni" | "parse-fail" | "dial-fail" | "callback"
	Host     string    `json:"host"`   // may be empty for pre-SNI failures
	Count    int       `json:"count"`  // >=1
	LastSeen time.Time `json:"last_seen"`
}

NetworkEvent is a single ambient network activity row as rendered by the TUI activity panel. Consecutive events with the same Host and Source are collapsed into one row with an incrementing Count and a refreshed LastSeen timestamp.

func GetNetworkEvents

func GetNetworkEvents(addr, token string) ([]NetworkEvent, error)

GetNetworkEvents fetches the current ambient network activity buffer from the daemon. Returns a nil slice (not a nil error) when the daemon replies with an empty list so the TUI can collapse the panel.

type NetworkEventStore

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

NetworkEventStore is a thread-safe ring buffer of recent network events with consecutive-host deduplication. It models HookEventStore but collapses bursts at write time so the panel stays calm under sustained repeats.

func NewNetworkEventStore

func NewNetworkEventStore() *NetworkEventStore

NewNetworkEventStore creates an empty store using time.Now as its clock. Tests can override the clock via NewNetworkEventStoreWithClock.

func NewNetworkEventStoreWithClock

func NewNetworkEventStoreWithClock(nowFn func() time.Time) *NetworkEventStore

NewNetworkEventStoreWithClock is the test constructor that injects a deterministic clock so dedup timestamps are reproducible.

func (*NetworkEventStore) Emit

func (s *NetworkEventStore) Emit(source, status, host string)

Emit satisfies proxy.NetworkEventEmitter. Consecutive events with the same (source, host) pair are collapsed into the tail row: Count is incremented and LastSeen is refreshed. A different host or source starts a new row, even if the host was seen earlier in the buffer. Collapsing at write time keeps memory flat under sustained bursts so the panel stays calm under noise.

func (*NetworkEventStore) Snapshot

func (s *NetworkEventStore) Snapshot() []NetworkEvent

Snapshot returns a copy of all current rows in insertion order (oldest first). Callers that want newest-first should reverse the slice; the store keeps events in insertion order so testing and JSON serialization remain predictable.

type PendingConfirm

type PendingConfirm struct {
	ID        string `json:"id"`
	Operation string `json:"operation"` // "DeleteIssue", "EditIssue"
	Tracker   string `json:"tracker"`   // tracker kind, e.g. "jira", "linear"
	Key       string `json:"key"`       // issue key, e.g. "KAN-1"
	Prompt    string `json:"prompt"`
	CreatedAt string `json:"created_at"`
	ClientPID int    `json:"client_pid"` // PID of the Claude instance that triggered the operation
}

PendingConfirm is the wire type for a single pending destructive operation awaiting user confirmation via the TUI.

func GetPendingConfirms

func GetPendingConfirms(addr, token string) ([]PendingConfirm, error)

GetPendingConfirms fetches pending destructive operation confirmations from the daemon.

type PendingConfirmStore

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

PendingConfirmStore is a thread-safe store for destructive operations awaiting user confirmation. The daemon adds entries when it intercepts destructive commands; the TUI polls the snapshot and resolves them.

func NewPendingConfirmStore

func NewPendingConfirmStore() *PendingConfirmStore

NewPendingConfirmStore creates an empty store.

func (*PendingConfirmStore) Add

Add stores a pending confirmation. The caller should block on pc.Decision after calling Add.

func (*PendingConfirmStore) Cleanup

func (s *PendingConfirmStore) Cleanup(maxAge time.Duration)

Cleanup rejects and removes all pending confirmations older than maxAge.

func (*PendingConfirmStore) Len

func (s *PendingConfirmStore) Len() int

Len returns the number of pending confirmations.

func (*PendingConfirmStore) Resolve

func (s *PendingConfirmStore) Resolve(id string, approved bool, approverPID int) error

Resolve sends the decision to the waiting goroutine and removes the entry. approverPID is the PID of the client resolving the confirmation and must be a positive integer.

The approverPID != requester PID check below is only a best-effort sanity guard, NOT an authorization boundary: ClientPID is supplied by the client and the requester's PID is typically resolved inside the agent's container namespace while the approver's is on the host, so the two are not comparable as a trust signal. Actual authorization is the daemon token required to reach this endpoint at all — do not rely on the PID check for security.

Resolve is for client-initiated decisions. Internal lifecycle events (timeouts, encode failures) must use ResolveTimeout instead.

func (*PendingConfirmStore) ResolveTimeout

func (s *PendingConfirmStore) ResolveTimeout(id string)

ResolveTimeout removes a pending confirmation without a client approver. It is used by internal lifecycle events (request timeouts, response-write failures) that need to unblock the waiting goroutine. The decision is always "not approved".

Returns nil even when the id is unknown — the caller is typically running in a deferred cleanup path where the entry may have already been resolved by another lifecycle event, and that is not a failure.

func (*PendingConfirmStore) Snapshot

func (s *PendingConfirmStore) Snapshot() []PendingConfirm

Snapshot returns all pending confirmations as wire types for the TUI.

type PendingConfirmation

type PendingConfirmation struct {
	ID        string
	Operation string // "DeleteIssue", "EditIssue"
	Tracker   string // tracker kind, e.g. "jira"
	Key       string // issue key, e.g. "KAN-1"
	Prompt    string // human-readable, e.g. "Delete KAN-1?"
	ClientPID int    // PID of the Claude instance that triggered the operation
	CreatedAt time.Time
	Decision  chan bool // the blocked goroutine waits on this; true = approved
}

PendingConfirmation represents a destructive operation that is blocked waiting for user confirmation via the TUI.

type ProjectEntry

type ProjectEntry struct {
	Name string // from .humanconfig project: field, or directory basename
	Dir  string // absolute path to project directory
}

ProjectEntry holds the loaded config context for one registered project directory.

func (ProjectEntry) EnvLookup

func (p ProjectEntry) EnvLookup() config.EnvLookup

EnvLookup returns a per-project scoped environment variable lookup function. It implements a 4-level precedence chain for each key:

  1. HUMAN_{PROJECT}_{KEY} — per-project override (e.g. HUMAN_INFRA_GITHUB_WORK_TOKEN)
  2. {KEY} via os.LookupEnv — global fallback (e.g. GITHUB_WORK_TOKEN)

The caller (ApplyEnvOverrides) constructs keys like PREFIX_SUFFIX and PREFIX_INSTANCE_SUFFIX. This lookup prepends HUMAN_{PROJECT}_ and checks that first, falling back to os.LookupEnv for the original key.

type ProjectInfo

type ProjectInfo struct {
	Name string `json:"name"`
	Dir  string `json:"dir"`
}

ProjectInfo describes a registered project in a running daemon.

type ProjectRegistry

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

ProjectRegistry maps project directories to their config entries. It is created at daemon startup and is read-only thereafter (no mutex needed).

func NewProjectRegistry

func NewProjectRegistry(dirs []string) (*ProjectRegistry, error)

NewProjectRegistry creates a registry from a list of project directories. Each directory must exist and contain a readable .humanconfig. If .humanconfig lacks a project: field, the directory basename is used as the name.

func (*ProjectRegistry) Entries

func (r *ProjectRegistry) Entries() []ProjectEntry

Entries returns all registered project entries.

func (*ProjectRegistry) Resolve

func (r *ProjectRegistry) Resolve(cwd string) (ProjectEntry, bool)

Resolve finds the ProjectEntry whose Dir is a prefix of the given cwd. Returns (entry, true) on match, (zero, false) if no match. When multiple entries match (nested dirs), the longest prefix wins because entries are sorted by path length descending.

func (*ProjectRegistry) Single

func (r *ProjectRegistry) Single() bool

Single returns true if there is exactly one registered project (backward compat mode).

type Request

type Request struct {
	Version   string            `json:"version"`
	Token     string            `json:"token"`
	Args      []string          `json:"args"`
	Env       map[string]string `json:"env,omitempty"`
	ClientPID int               `json:"client_pid,omitempty"` // parent PID (Claude process) for connection tracking
	Cwd       string            `json:"cwd,omitempty"`        // client working directory for project routing
}

Request is sent from the client to the daemon (one JSON line per connection).

type Response

type Response struct {
	Stdout        string `json:"stdout"`
	Stderr        string `json:"stderr"`
	ExitCode      int    `json:"exit_code"`
	AwaitCallback bool   `json:"await_callback,omitempty"`
	Callback      string `json:"callback,omitempty"`
	AwaitConfirm  bool   `json:"await_confirm,omitempty"`  // line 1: daemon paused, awaiting TUI confirmation
	ConfirmID     string `json:"confirm_id,omitempty"`     // unique identifier for the pending operation
	ConfirmPrompt string `json:"confirm_prompt,omitempty"` // human-readable prompt, e.g. "Delete JIRA-123?"
}

Response is sent from the daemon back to the client (one or more JSON lines per connection).

type Server

type Server struct {
	Addr             string
	Token            string
	SafeMode         bool
	CmdFactory       func() *cobra.Command
	Opener           BrowserOpener // used for OAuth relay; defaults to browser.DefaultOpener
	Logger           zerolog.Logger
	ConnectedPIDs    *ConnectedTracker                        // tracks client PIDs that have pinged; nil disables tracking
	HookEvents       *HookEventStore                          // in-memory hook event buffer; nil disables hook event tracking
	NetworkEvents    *NetworkEventStore                       // in-memory ambient network activity buffer; nil disables
	IssueFetcher     func() ([]TrackerIssuesResult, error)    // injected; fetches issues from configured trackers
	TrackerDiagnoser func(dir string) []tracker.TrackerStatus // injected; diagnoses tracker status with vault resolution
	Projects         *ProjectRegistry                         // multi-project routing; nil means single-project mode
	PendingConfirms  *PendingConfirmStore                     // pending destructive operation confirmations; nil disables
	StatsWriter      *stats.Writer                            // async SQLite writer for tool event persistence; nil disables
	StatsStore       *stats.StatsStore                        // for query-time aggregation; nil disables tool-stats route
	AgentCleaner     AgentCleaner                             // async agent cleanup; nil disables agent-stop-async route
	VaultResolver    *vault.Resolver                          // session-scoped vault resolver; reused across requests to avoid repeated op.exe calls
	// contains filtered or unexported fields
}

Server listens for incoming client connections and executes CLI commands.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context) error

ListenAndServe starts the TCP listener and blocks until ctx is cancelled. On shutdown it waits for all in-flight handler goroutines to return before closing, so a client request that's already accepted is never torn down mid-flight by listener close alone.

type SubscribeEvent

type SubscribeEvent struct {
	Type      string `json:"type"`            // "change", "agent-stopped"
	AgentName string `json:"agent,omitempty"` // set for agent lifecycle events
}

SubscribeEvent is a notification sent over a persistent subscribe connection. For "agent-stopped" events, AgentName identifies the agent to remove immediately without waiting for the next discovery cycle.

type TrackerIssuesResult

type TrackerIssuesResult struct {
	TrackerName    string          `json:"tracker_name"`
	TrackerKind    string          `json:"tracker_kind"`
	TrackerRole    string          `json:"tracker_role,omitempty"`
	Project        string          `json:"project"`
	Issues         []tracker.Issue `json:"issues"`
	ReadyForReview []string        `json:"ready_for_review,omitempty"`
	Err            string          `json:"error,omitempty"`
}

TrackerIssuesResult is the wire type for a single tracker/project's issues.

ReadyForReview carries the engineering ticket keys that a PM tracker has currently flagged for review via a [human:ready-for-review] comment. It is populated on engineering-tracker results (where the keys actually live) so the TUI can join it against Issues without a separate lookup. See cli/CLAUDE.md "Review handoff" for the comment convention.

func GetTrackerIssues

func GetTrackerIssues(addr, token string) ([]TrackerIssuesResult, error)

GetTrackerIssues fetches open issues from all configured tracker projects via the daemon.

Jump to

Keyboard shortcuts

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