host

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: AGPL-3.0 Imports: 40 Imported by: 0

Documentation

Overview

Package host defines the value types for Clank's Host plane and the host.Service that runs agent backends, owns clones of remote repos, and exposes an HTTP API to the Hub.

Index

Constants

View Source
const (
	EnvClaudeCodeOAuthToken = "CLAUDE_CODE_OAUTH_TOKEN"
	EnvAnthropicAPIKey      = "ANTHROPIC_API_KEY"
	EnvAnthropicAuthToken   = "ANTHROPIC_AUTH_TOKEN"
	// EnvCodexAPIKey authenticates the codex-acp adapter (OpenAI).
	EnvCodexAPIKey = "CODEX_API_KEY"
	// EnvOpenAIAPIKey is OpenAI's generic key var; the codex CLI honors
	// it too, so the device-auth ceremony scrubs it (see
	// auth_codex_device.go).
	EnvOpenAIAPIKey = "OPENAI_API_KEY"
)

Env var names the spawned claude CLI consumes. Shared by AnthropicEnv (injection), setup-token scrubbing, and env-credential detection so the three can't drift. EnvAnthropicAuthToken is the bearer-token variant custom LLM gateways use (paired with a base URL); claude treats it as env-borne auth like the API key.

View Source
const DefaultBranchCacheTTL = 5 * time.Second

DefaultBranchCacheTTL is the default time-to-live for cached listBranches results. The TUI inbox polls listBranches every 3s on each open session worktree; without caching, every poll fans out to ~4 git subprocesses per active worktree (DiffStat = 3, CommitsAhead = 1). DiffStat in particular runs `git diff --numstat HEAD` which stat()s the working tree. With 2 active sessions on a busy repo this pegged clank-host's CPU. The TTL is short enough that branch metadata still feels live (a few seconds of staleness is invisible in the inbox row "+12 -3" indicators) while collapsing fork rate.

View Source
const EnvCodexHome = "CODEX_HOME"

EnvCodexHome is the codex CLI's home-directory override. The login ceremony pins it on the subprocess so a host-level override, the spawned adapter (which inherits the host environment), and status probes all agree on one directory.

View Source
const EnvGHToken = "GH_TOKEN"

EnvGHToken is the env var name gh (and this package's tests) key off of, centralized to avoid drift/typos like the other Env* constants in env_credentials.go.

View Source
const ErrorCodeHarnessNotAllowed = "harness_not_allowed"

ErrorCodeHarnessNotAllowed is the wire error code for ErrBackendNotAllowed.

View Source
const HostLocal = "local"

HostLocal is the canonical hostname for the laptop's supervised clank-host child. The TUI defaults to this until the host-selection UX lands. Hostnames are arbitrary strings — short, human-readable slugs ("local", "flymachines-abc123") chosen by whoever registers the Host. The Hub treats them as opaque keys.

View Source
const MaxDiscoveredLocalReposForTest = maxDiscoveredLocalRepos

MaxDiscoveredLocalReposForTest exposes maxDiscoveredLocalRepos to the external host_test package. Test-only.

View Source
const ProviderAnthropicAPI = "anthropic-api"

ProviderAnthropicAPI is the clank provider ID for pay-per-use console.anthropic.com API keys. Passed as ANTHROPIC_API_KEY.

View Source
const ProviderAnthropicClaudeCode = "anthropic-claude-code"

ProviderAnthropicClaudeCode is the clank provider ID for the Claude.ai subscription path (Pro/Max/Team/Enterprise). The credential is the long-lived token printed by `claude setup-token`, passed to the claude subprocess as CLAUDE_CODE_OAUTH_TOKEN.

View Source
const ProviderGitHubCopilot = "github-copilot"

ProviderGitHubCopilot is the OpenCode provider ID for the GitHub Copilot integration. Matches the value the upstream plugin emits at packages/opencode/src/plugin/github-copilot/copilot.ts.

View Source
const ProviderOpenAICodexAPI = "openai-codex-api"

ProviderOpenAICodexAPI is the clank provider ID for the codex backend's OpenAI API key. Stored in clank's openai.json sink and injected into codex-acp spawns as CODEX_API_KEY.

View Source
const ProviderOpenAICodexChatGPT = "openai-codex-chatgpt"

ProviderOpenAICodexChatGPT is the clank provider ID for the codex backend's ChatGPT-subscription path (Plus/Pro/Team). The credential is codex's own $CODEX_HOME/auth.json, written by the CLI's device-code login that clank drives headless (see auth_codex_device.go); clank records the connection in openai.json but never holds the tokens.

Variables

View Source
var (
	// ErrNotFound is returned when a requested repo, branch, or worktree
	// does not exist on the host.
	ErrNotFound = errors.New("host: not found")

	// ErrConfigIncomplete rejects a session create whose config is missing
	// required keys (the backend's Default-preset keys). The host never
	// fills values in, so the create fails instead of silently opening in
	// an agent factory default nobody chose.
	ErrConfigIncomplete = errors.New("host: session config incomplete")

	// ErrWorktreeBusy is returned when a destructive worktree operation
	// (e.g. DeleteWorktree, DeleteRepo) finds a session actively running
	// on it. The caller should retry once the session goes idle.
	ErrWorktreeBusy = errors.New("host: worktree has an active session")

	// ErrCannotMergeDefault is returned when MergeBranch is called with
	// the default branch as its target (you cannot merge a branch into
	// itself).
	ErrCannotMergeDefault = errors.New("host: cannot merge the default branch into itself")

	// ErrNothingToMerge is returned when MergeBranch finds the feature
	// branch has no commits ahead and a clean worktree.
	ErrNothingToMerge = errors.New("host: nothing to merge")

	// ErrCommitMessageRequired is returned when MergeBranch finds
	// uncommitted work in the feature worktree but no commit message was
	// supplied for the auto-commit.
	ErrCommitMessageRequired = errors.New("host: commit_message is required when worktree has uncommitted changes")

	// ErrTargetDirty is returned when MergeBranch finds the merge
	// target's worktree has uncommitted changes. Named branch-agnostic
	// because the target may be any branch (default branch, a release
	// branch, etc.) — not always "main".
	ErrTargetDirty = errors.New("host: merge target worktree has uncommitted changes; commit or stash them first")

	// ErrMergeConflict is returned when the merge produces a conflict
	// that MergeBranch has already rolled back.
	ErrMergeConflict = errors.New("host: merge conflict: resolve manually or choose a different approach")

	// ErrReservedBranch is returned when ResolveWorktree is asked to
	// create a worktree for the repository's default branch (e.g.
	// "main"/"master"). The default branch is reserved for the primary
	// checkout — putting it in a separate worktree would prevent
	// `git checkout <default>` from working in the original repo
	// directory and breaks the user's mental model that worktrees are
	// for *other* branches.
	ErrReservedBranch = errors.New("host: cannot create a worktree for the default branch; it is reserved for the primary checkout")

	// ErrInvalidBranchName is returned when ResolveWorktree is given an
	// empty or whitespace-only branch name.
	ErrInvalidBranchName = errors.New("host: branch name must be non-empty")

	// ErrInvalidArgument is returned when a Service method is called with
	// a missing or malformed required argument.
	ErrInvalidArgument = errors.New("host: invalid argument")

	// ErrRepoNotFound is returned when a repo-scoped operation names a
	// slug with no canonical clone on this host (~/work/repos/<slug>).
	// Distinct from ErrNotFound so the mux can emit a repo_not_found
	// code the client can act on (refresh the repo list) vs a missing
	// branch/worktree inside a repo that does exist.
	ErrRepoNotFound = errors.New("host: repo not found")

	// ErrTemplateCloneFailed is returned when CreateProjectFromTemplate
	// can't clone the template repository. The message is deliberately
	// URL-free (clone URLs can embed credentials); the full git error
	// lands in the host's server log only. The mux maps this to a typed
	// template_clone_failed response so clients see an actionable error
	// instead of a masked 502.
	ErrTemplateCloneFailed = errors.New("host: template clone failed")

	// ErrCannotDeleteLocalCheckout is returned when DeleteRepo names a
	// discovered local checkout. The user's own folders are never
	// clank's to delete — only ~/work/repos canonicals are.
	ErrCannotDeleteLocalCheckout = errors.New("host: repo is a checkout owned by the user, not clank; refusing to delete")

	// ErrBranchCheckedOutElsewhere is returned when loading a branch
	// that is checked out in a worktree clank does not manage (a local
	// checkout's primary worktree, or one the user added by hand). Git
	// allows a branch in at most one worktree; fork off it instead.
	ErrBranchCheckedOutElsewhere = errors.New("host: branch is checked out in a worktree clank does not manage; fork off it instead")

	// ErrPresetStoreUnavailable is returned by PutPreset/DeletePreset when
	// the host has no PresetsDir configured. Server-side misconfiguration,
	// not a bad request — the mux maps it to 503 so clients can tell
	// "feature disabled" apart from "bad preset payload".
	ErrPresetStoreUnavailable = errors.New("host: preset store unavailable (no data dir)")
)

Sentinel errors for Service methods. Callers (e.g. HTTP handlers in host/mux) use errors.Is to translate these into appropriate status codes without coupling to string matching.

View Source
var (
	// ErrGitHubManagerUnavailable fires when the host couldn't
	// instantiate the github.Manager at startup (homedir resolution
	// failure). 503.
	ErrGitHubManagerUnavailable = errors.New("github manager unavailable")

	// ErrGitHubNotConnected fires when the credential file is
	// absent. 403; client UI surfaces a "Connect GitHub" CTA.
	ErrGitHubNotConnected = errors.New("github not connected")

	// ErrPRMissingField fires when the request body omits a required
	// field. 400.
	ErrPRMissingField = errors.New("pr request missing required field")

	// ErrNothingToPush fires when the worktree has no uncommitted
	// work and its branch is at or behind the base — nothing to PR.
	// 400; client UI shows "nothing to push yet" hint.
	ErrNothingToPush = errors.New("nothing to push: branch is up to date with base")

	// ErrNoOriginRemote fires when the worktree has no `origin`
	// remote configured. Common on worktrees materialized onto a sprite whose
	// `.git/config` didn't carry over with the bundle. 400; client
	// UI suggests "git remote add origin <github-url>" or
	// re-pushing from laptop with the remote intact.
	ErrNoOriginRemote = errors.New("worktree has no 'origin' remote — clank sync may have stripped .git/config; add it manually or re-push from laptop")

	// ErrNoCommonAncestor fires when the head branch and the
	// remote's base have no shared history. Overwhelmingly indicates
	// the remote points at the wrong repo (two unrelated repos
	// virtually never share a commit SHA). Hard refusal — no bypass.
	// 409.
	ErrNoCommonAncestor = errors.New("no common ancestor with remote base — origin probably points to the wrong repo")

	// ErrBaseRefUnreachable fires when we couldn't fetch the base
	// branch from the remote. Without a fresh origin/<base>, the
	// common-ancestor check can't run safely, so we refuse rather
	// than push blindly. 502.
	ErrBaseRefUnreachable = errors.New("could not fetch base branch from remote — cannot safely verify common history")
)

Errors specific to CreatePR. Each one maps to a distinct HTTP status + machine-readable code in the mux handler.

View Source
var (
	// ErrGitHubConnectionRequired means the PR may be private and no credential is available.
	ErrGitHubConnectionRequired = errors.New("connect GitHub to access this pull request")
	// ErrPullRequestChanged means the PR head no longer matches the approved SHA.
	ErrPullRequestChanged = errors.New("pull request changed after approval")
	// ErrPullRequestLocalCommits means the matching local branch contains commits outside the approved PR revision.
	ErrPullRequestLocalCommits = errors.New("pull request branch has local commits not present in the approved revision")
	// ErrPullRequestRepoAmbiguous means more than one host repository matches the PR repository.
	ErrPullRequestRepoAmbiguous = errors.New("more than one local checkout matches the pull request repository")
)
View Source
var (
	// ErrRemoteDiverged fires when local and origin/<branch> have each
	// advanced past their merge-base: a push is rejected (non-fast-
	// forward) and a pull can't fast-forward. Routes to conflict
	// resolution. 409.
	ErrRemoteDiverged = errors.New("host: local and remote have diverged")

	// ErrWorktreeDirty fires when a pull is requested but the worktree has
	// uncommitted changes a fast-forward could clobber. 409.
	ErrWorktreeDirty = errors.New("host: worktree has uncommitted changes; commit or push them first")

	// ErrNoUpstream fires when the branch doesn't exist on the remote yet
	// — nothing to pull or resolve against. 400.
	ErrNoUpstream = errors.New("host: branch has no upstream on the remote")

	// ErrDetachedHead fires when the worktree is on a detached HEAD, so
	// there's no branch to push or pull. 409.
	ErrDetachedHead = errors.New("host: worktree is on a detached HEAD")

	// ErrNotMerging fires when an abort is requested but no merge is in
	// progress. 409.
	ErrNotMerging = errors.New("host: no merge in progress")
)

Remote-sync errors. Each maps to a distinct HTTP status + machine code in the mux handler. Reuses ErrGitHubManagerUnavailable / ErrGitHubNotConnected / ErrNoOriginRemote from github_pr.go.

View Source
var (
	// ErrAlreadyPublished fires when publish is called on a worktree that
	// already has an origin remote — it's already on GitHub, so push (not
	// publish) is the right operation. 409.
	ErrAlreadyPublished = errors.New("host: worktree already has an origin remote")

	// ErrInvalidRepoName fires when the requested name is empty or sanitizes
	// to nothing usable. 400.
	ErrInvalidRepoName = errors.New("host: a valid repository name is required")
)
View Source
var ErrBackendNotAllowed = errors.New("agent harness not allowed")

ErrBackendNotAllowed means the user has not granted Clank permission to control the requested agent harness.

View Source
var ErrCodexDeviceAuthUnavailable = errors.New("codex device auth is not available on this host (codex backend not enabled)")

ErrCodexDeviceAuthUnavailable is returned when no codex login command is wired — the codex backend isn't enabled on this host.

View Source
var ErrFlowNotOAuthCode = errors.New("flow is not an oauth-code flow")

ErrFlowNotOAuthCode is returned when SubmitAuthCode targets a flow that wasn't started via StartOAuthCodeFlow.

View Source
var ErrGitHubRepositoryConnectionRequired = errors.New("connect GitHub to access this repository")
View Source
var ErrInvalidAPIKey = errors.New("api key cannot be empty")

ErrInvalidAPIKey is returned when SubmitAPIKey is called with a blank or whitespace-only key. Mux handlers map this to a 400.

View Source
var ErrInvalidAuthCode = errors.New("auth code cannot be empty")

ErrInvalidAuthCode is returned when SubmitAuthCode is called with a blank code (after trimming).

View Source
var ErrMissingPrompt = errors.New("required provider prompt missing")

ErrMissingPrompt is returned when a provider declares prompts in its catalog entry (e.g. Azure resourceName, Cloudflare accountId) but the caller didn't supply a value for one of them.

View Source
var ErrNoOpenPRForBranch = errors.New("no open pull request for this branch")

ErrNoOpenPRForBranch fires when the worktree's branch has no open PR to mark ready. 404; clients refresh their remote status.

View Source
var ErrPreviewUnavailable = errors.New("preview: manager not configured on this host")

ErrPreviewUnavailable is returned by every PreviewXxx method when host.Service has no preview.Manager wired (today: only in tests that construct Service via host.New with a custom path). Exported as a sentinel so callers can errors.Is rather than string-match.

View Source
var ErrSetupTokenUnavailable = errors.New("claude CLI not installed; cannot run setup-token")

ErrSetupTokenUnavailable is returned when the `claude` CLI is missing on PATH. Mux maps this to a 503.

View Source
var ErrUnknownFlow = errors.New("unknown flow id")

ErrUnknownFlow is returned when a status poll references a flow the manager has no record of.

View Source
var ErrUnknownProvider = errors.New("unknown auth provider")

ErrUnknownProvider is returned when a caller targets a provider this manager doesn't know how to authenticate.

View Source
var SessionStoreNotConfigured = errors.New("host: session store not configured")

SessionStoreNotConfigured is returned by session-metadata methods when the Service was constructed without Options.SessionsStore.

Functions

func LocalRepoSlug

func LocalRepoSlug(root string) string

LocalRepoSlug encodes an absolute folder path as its routing key — the repo slug for local checkouts, and the preview key `clank preview` mints for the folder it serves (which may be a monorepo subdir, so no root normalization here). base64url stays inside the slug alphabet, is lossless, and needs no lookup table to reverse — the path IS the identity, so a moved folder is honestly a new repo (matching how IDE recents work).

func SetGitHubCloneBaseForTest

func SetGitHubCloneBaseForTest(base string) (prev string)

SetGitHubCloneBaseForTest overrides gitHubCloneBase for the duration of a test, returning the previous value so the caller can restore it. Concurrent test access is unsafe — the override is a single global.

func SetWorkRootForTest

func SetWorkRootForTest(path string) string

SetWorkRootForTest overrides workRootDir's lookup for the duration of a test. Returns the previous value so the caller can restore it in cleanup. Concurrent test access is unsafe — the override is a package-level singleton, so callers must serialize tests that use it.

Types

type ACPBackendManager

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

ACPBackendManager adapts one acp.AdapterProfile to agent.BackendManager: a supervised adapter process pool plus per-session acp.Backend values. One generic implementation serves every ACP agent — per-adapter variance lives entirely in the profile.

func NewACPBackendManager

func NewACPBackendManager(profile acp.AdapterProfile) (*ACPBackendManager, error)

NewACPBackendManager builds a manager for the given adapter profile. The profile's Env is routed through SetEnvResolver so credentials can be wired after construction (the AuthManager exists later) and rotated at runtime via the supervisor's env-fingerprint restarts.

func NewClaudeACPManager

func NewClaudeACPManager(dirs ACPDirs) (*ACPBackendManager, error)

NewClaudeACPManager serves claude-code through the pinned claude-agent-acp adapter under bun, provisioned lazily into toolsDir alongside codex-acp. The adapter's exact-pinned Agent SDK bundles the Claude CLI, so the agent version is fixed by the lockfile. Credentials arrive via SetEnvResolver (Anthropic sink); the profile adds IS_SANDBOX=1 when running as root so bypassPermissions works on sprites.

func NewCodexACPManager

func NewCodexACPManager(dirs ACPDirs) (*ACPBackendManager, error)

NewCodexACPManager builds the codex manager: the pinned codex-acp adapter run as plain JS under bun, provisioned lazily into toolsDir on first use (host startup never blocks on it, and hosts that never run codex never install it). Env arrives via SetEnvResolver (OpenAI sink; nil = codex's own ChatGPT login fallback).

func NewOpenCodeACPManager

func NewOpenCodeACPManager() (*ACPBackendManager, error)

NewOpenCodeACPManager serves opencode through `opencode acp` on the user's own binary — their install, their state, no version skew clank can introduce. Prepare gates on the verified-surface floor (retried until it passes) and materializes stack-detected guidance as an instructions file inside the worktree's git dir; Env points opencode at it via inline config. Guidance is best-effort: it never blocks a session.

func (*ACPBackendManager) BackendType

func (m *ACPBackendManager) BackendType() agent.BackendType

BackendType reports which clank backend this manager serves.

func (*ACPBackendManager) ConfigOptions

func (m *ACPBackendManager) ConfigOptions(ctx context.Context, projectDir string) ([]agent.ConfigOption, error)

ConfigOptions implements agent.ConfigOptionsLister: it opens one short-lived session in projectDir and returns the config options the agent advertises there — ACP surfaces them only on session open (zed-industries/zed#52500), so this is the only pre-session source. On-demand and uncached: callers (knob editors) show a spinner for exactly this call. Concurrent requests share one probe; the probe runs on its own context so one caller disconnecting doesn't fail the rest.

func (*ACPBackendManager) CreateBackend

CreateBackend builds the per-session backend. Guidance is assembled for fresh sessions only (mirrors the bespoke managers); skills materialize for both fresh and resumed sessions.

func (*ACPBackendManager) DiscoverAllSessions

func (m *ACPBackendManager) DiscoverAllSessions(ctx context.Context) ([]agent.SessionSnapshot, error)

DiscoverAllSessions lists every session the agent knows about. Only meaningful for host-scoped profiles (codex/claude adapters back it with their global stores); per-dir profiles return nothing — their discovery goes through per-dir seeds, matching the bespoke opencode exclusion.

func (*ACPBackendManager) DiscoverSessions

func (m *ACPBackendManager) DiscoverSessions(ctx context.Context, seedDir string) ([]agent.SessionSnapshot, error)

DiscoverSessions lists the agent's own sessions for seedDir via ACP session/list, marking the dir desired for per-dir profiles.

func (*ACPBackendManager) Init

func (m *ACPBackendManager) Init(ctx context.Context, knownDirs func() ([]string, error)) error

Init seeds per-dir profiles with known project dirs (host-scoped profiles start lazily on first use) and starts the reconciler.

func (*ACPBackendManager) LoginArgv

func (m *ACPBackendManager) LoginArgv() (func(ctx context.Context) ([]string, error), bool)

LoginArgv resolves the argv for this adapter's login ceremony, or reports that the profile doesn't have one.

func (*ACPBackendManager) RestartAdapters

func (m *ACPBackendManager) RestartAdapters()

RestartAdapters cycles every adapter process so fresh spawns pick up credential state — both env-borne values and on-disk state the env fingerprint can't see (codex's $CODEX_HOME/auth.json).

func (*ACPBackendManager) SetAmbientEnvResolver

func (m *ACPBackendManager) SetAmbientEnvResolver(f func() map[string]string)

SetAmbientEnvResolver wires sandbox env that is not provider-specific, for every ACP backend rather than one provider's. Same rotation semantics as SetEnvResolver: the supervisor's env fingerprint restarts adapters when the resolved value changes, which kills in-flight turns in that scope — so resolvers wired here must return a STABLE value for unchanged underlying state, or they restart adapters on a timer. A nil f clears the resolver; see SetEnvResolver for why that's not just &f.

func (*ACPBackendManager) SetEnvResolver

func (m *ACPBackendManager) SetEnvResolver(f func() map[string]string)

SetEnvResolver wires credential env for adapter spawns and nudges the supervisor so the env-fingerprint restart picks up rotations. A nil f clears the resolver rather than wrapping a nil func in a non-nil pointer, which the merge path would call and panic on.

func (*ACPBackendManager) Shutdown

func (m *ACPBackendManager) Shutdown()

Shutdown stops every adapter process and waits for in-flight config-option probes so no goroutine outlives the manager.

func (*ACPBackendManager) Supervisor

func (m *ACPBackendManager) Supervisor() *acp.AdapterSupervisor

Supervisor exposes the adapter supervisor for credential-rotation nudges and test spawn injection.

type ACPDirs

type ACPDirs struct {
	// Tools holds the provisioned adapter runtime (bun plus the pinned
	// npm packages). Unused by backends that run the user's own binary.
	Tools string
}

ACPDirs locates the on-disk state an ACP manager owns.

type AgentInfo

type AgentInfo = agent.AgentInfo

AgentInfo is re-exported from the agent package so callers in the host plane import a single source.

type AuthManager

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

AuthManager owns provider authentication for one host (one OpenCode install). One per host.Service.

func NewAuthManager

func NewAuthManager(restart func(ctx context.Context) error) (*AuthManager, error)

NewAuthManager constructs an AuthManager. Resolves $HOME via os.UserHomeDir() so the same code works on a cloud sandbox container (where it's /root) and a developer's laptop.

func (*AuthManager) AnthropicAPIKey

func (a *AuthManager) AnthropicAPIKey() string

AnthropicAPIKey returns the stored ANTHROPIC_API_KEY, or "".

func (*AuthManager) AnthropicEnv

func (a *AuthManager) AnthropicEnv() map[string]string

AnthropicEnv returns the env vars to inject into a spawned claude subprocess. Subscription token wins over API key when both are set, which is the opposite of claude's default precedence — but in our model the user explicitly connected one or the other; writes clear the other variant. We only ever set one var, so claude's own precedence resolution never kicks in.

Returns nil when no Anthropic provider is connected, so claude falls back to its own keychain/OAuth login flow. On laptop hosts, ListProviders' claude CLI fallback reports that borrowed login in provider status (see claude_cli.go).

func (*AuthManager) AnthropicOAuthToken

func (a *AuthManager) AnthropicOAuthToken() string

AnthropicOAuthToken returns the stored CLAUDE_CODE_OAUTH_TOKEN, or "" if none. Callers (claude-code spawn) use this as the preferred credential when present.

func (*AuthManager) AnthropicSinkPath

func (a *AuthManager) AnthropicSinkPath() string

AnthropicSinkPath is where clank stores Anthropic credentials. A separate file from OpenCode's auth.json because (a) opencode rewrites auth.json itself and would clobber any unknown keys, and (b) the consumer is different — claude-code reads env vars set on its subprocess, not a JSON file from this directory.

func (*AuthManager) AuthJSONPath

func (a *AuthManager) AuthJSONPath() string

AuthJSONPath is where OpenCode stores credentials inside this host. Exposed for tests and verification probes.

func (*AuthManager) CancelFlow

func (a *AuthManager) CancelFlow(_ context.Context, flowID string) error

CancelFlow signals the polling goroutine for flowID to stop and transitions the flow state to canceled. No-op if the flow has already reached a terminal state.

func (*AuthManager) DeleteCredential

func (a *AuthManager) DeleteCredential(ctx context.Context, providerID string) error

DeleteCredential removes providerID's credential from the appropriate sink. For OpenCode providers, triggers a server restart so the new auth state takes effect; for Anthropic providers, no restart — the next claude-code spawn simply sees no env var. Codex providers restart the ACP adapters via the OpenAI credential callback; the ChatGPT variant deletes codex's own auth.json, which logs this host's codex CLI out entirely.

func (*AuthManager) EnableClaudeCLIFallback

func (a *AuthManager) EnableClaudeCLIFallback()

EnableClaudeCLIFallback lets ListProviders report the machine's own claude CLI login (Keychain / ~/.claude/.credentials.json) as a connected subscription provider when the anthropic sink is empty. A deployment decision, not a default: the local laptop provisioner enables it — the host IS the user's machine, where AnthropicEnv already returns nil and the spawned claude uses that login anyway — while sandboxes keep connection state explicit. Presence-only: the credential itself is never read. Call once at wiring time, before the manager serves requests.

func (*AuthManager) EnableCodexCLIFallback

func (a *AuthManager) EnableCodexCLIFallback()

EnableCodexCLIFallback lets ListProviders report the machine's own codex CLI login as a connected subscription when clank didn't run the ceremony. Presence-only: the credential is never read.

func (*AuthManager) GetFlowStatus

func (a *AuthManager) GetFlowStatus(_ context.Context, flowID string) (agent.DeviceFlowStatus, error)

GetFlowStatus returns the current state of flowID. Pure read. Returns ErrUnknownFlow if the flow doesn't exist (or has been GC'd after TTL).

func (*AuthManager) ListProviders

func (a *AuthManager) ListProviders(ctx context.Context, backend agent.BackendType) ([]agent.ProviderAuthInfo, error)

ListProviders returns the providers this host knows how to authenticate, with their current connection state read from the appropriate sink — opencode's auth.json for OpenCode providers, our anthropic.json for Anthropic providers. Connected entries carry Source. Beyond the stores, two detection layers stop clients from prompting a connect that would change nothing: env-borne credentials in this process's environment (source env, every host — spawned CLIs inherit them; see env_credentials.go), and, with the claude CLI fallback enabled, the machine's own claude login (source claude_cli, laptops). Precedence store > env > claude_cli matches what the spawned CLI actually uses. When backend is non-empty, the result is filtered to providers that target that agent CLI (so an opencode-session compose flow doesn't surface "Anthropic (Claude subscription)", and a claude-code-session flow doesn't surface "GitHub Copilot"). Empty backend returns the full catalog.

func (*AuthManager) OpenAIEnv

func (a *AuthManager) OpenAIEnv() map[string]string

OpenAIEnv returns env vars for a spawned codex-acp adapter, or nil when no API key is stored — codex then uses its own login state in $CODEX_HOME/auth.json (the device-auth ceremony's sink, or a laptop's pre-existing `codex login`).

func (*AuthManager) OpenAISinkPath

func (a *AuthManager) OpenAISinkPath() string

OpenAISinkPath is where clank stores OpenAI/Codex connection state — clank-owned for the same reasons as the Anthropic sink: codex never rewrites this file. The API key is consumed as env injection on the spawned codex-acp adapter; the ChatGPT flag records that this host completed the device-auth ceremony (the tokens themselves live in codex's own $CODEX_HOME/auth.json, which codex refreshes in place).

func (*AuthManager) SetBackendAllowed added in v0.3.1

func (a *AuthManager) SetBackendAllowed(f func(agent.BackendType) bool)

SetBackendAllowed installs the harness permission check used before credential detection. Call before serving requests.

func (*AuthManager) SetCodexLoginCommand

func (a *AuthManager) SetCodexLoginCommand(f func(ctx context.Context) ([]string, error))

SetCodexLoginCommand wires the argv used to run the pinned codex CLI's device login (provisioning the tools on first use). Wired at service construction when the codex backend is enabled; nil keeps StartDeviceFlow returning ErrCodexDeviceAuthUnavailable.

func (*AuthManager) SetHTTPClient

func (a *AuthManager) SetHTTPClient(c *http.Client)

SetHTTPClient overrides the client used for outbound provider calls. Tests use this to stub GitHub.

func (*AuthManager) SetOpenAIAPIKey

func (a *AuthManager) SetOpenAIAPIKey(key string) error

SetOpenAIAPIKey stores the API key injected into codex-acp spawns. Callers fire the OpenAI credential callback afterwards so the adapters restart with the new value. A ChatGPT connection is left intact — which credential codex then prefers is codex's resolution.

func (*AuthManager) SetOpenAICredentialCallback

func (a *AuthManager) SetOpenAICredentialCallback(f func())

SetOpenAICredentialCallback wires the post-write hook for OpenAI credentials (ACP supervisor nudge). Call before serving requests.

func (*AuthManager) StartDeviceFlow

func (a *AuthManager) StartDeviceFlow(ctx context.Context, providerID string) (agent.DeviceFlowStart, error)

StartDeviceFlow begins a device-flow auth for providerID. Returns the user-facing fields the TUI surfaces and a flow_id for status polls. Spawns a background goroutine that watches the provider — polling GitHub's token endpoint for Copilot, watching the codex login subprocess for the ChatGPT subscription — and updates the flow's in-memory state as it transitions pending → authorized → success.

func (*AuthManager) StartOAuthCodeFlow

func (a *AuthManager) StartOAuthCodeFlow(ctx context.Context, providerID string) (agent.DeviceFlowStart, error)

StartOAuthCodeFlow spawns `claude setup-token` in a PTY, waits for it to print its authorize URL, and returns the URL + a flow_id. A background awaiter then captures the long-lived token from whichever source produces it:

  • native-local: setup-token opens the user's browser and completes via its OWN localhost callback — the token appears with no pasted code, so the flow reaches success without SubmitAuthCode.
  • remote: the IdP shows a code on its hosted page; the user pastes it via SubmitAuthCode, and the same awaiter catches the resulting token.

The session lives until the awaiter finishes (success/error/timeout) or CancelFlow cancels it.

func (*AuthManager) SubmitAPIKey

func (a *AuthManager) SubmitAPIKey(_ context.Context, providerID, key string, metadata map[string]string) (string, error)

SubmitAPIKey stores an API key for providerID — plus any provider-specific metadata fields (Azure resource name, Cloudflare account/gateway IDs, etc.) — and triggers an OpenCode restart so the new credential takes effect. Returns a flow_id the client polls via GetFlowStatus to observe the authorized → success transition (the restart is the only long-running step; "pending" is essentially instantaneous for this flow type, but exposing it keeps the state machine uniform with device flows).

metadata may be nil for providers that need only a key. When the catalog entry declares Prompts, every prompt key must be present in metadata with a non-blank value, or ErrMissingPrompt is returned before the goroutine spawns.

func (*AuthManager) SubmitAuthCode

func (a *AuthManager) SubmitAuthCode(ctx context.Context, providerID, flowID, code string) error

SubmitAuthCode writes a user-pasted code into the running setup-token subprocess, then blocks until the background token awaiter (started in StartOAuthCodeFlow) captures the token and reaches a terminal state. It returns nil on success and the flow's error otherwise, preserving the synchronous contract remote clients rely on. Only the remote path calls this; the native-local path self-completes via the browser callback and never needs a pasted code.

type BackendInfo

type BackendInfo struct {
	Name        agent.BackendType `json:"name"`
	DisplayName string            `json:"display_name"`
	Available   bool              `json:"available"`         // false when the backend's binary/server is missing
	Reason      string            `json:"reason,omitempty"`  // Why it is unavailable (when Available is false)
	Version     string            `json:"version,omitempty"` // Reported by the backend, when available
}

BackendInfo describes one backend installed on a Host (e.g. "opencode", "claude-code"). Catalog endpoints return slices of these.

type BranchInfo

type BranchInfo struct {
	Name         string `json:"name"`
	WorktreeDir  string `json:"worktree_dir,omitempty"` // Filesystem path on the Host if a worktree is checked out
	IsDefault    bool   `json:"is_default,omitempty"`
	IsCurrent    bool   `json:"is_current,omitempty"`
	LinesAdded   int    `json:"lines_added,omitempty"`
	LinesRemoved int    `json:"lines_removed,omitempty"`
	CommitsAhead int    `json:"commits_ahead,omitempty"`
	// RepoLabel is the display name used to group branches in the sidebar.
	// Set to the remote name when available; falls back to the basename of
	// the repo root so it is always non-empty and unique for local repos.
	RepoLabel string `json:"repo_label,omitempty"`
}

BranchInfo describes a branch on a Host's repo.

type CreatePRRequest

type CreatePRRequest struct {
	Title string `json:"title"`
	Body  string `json:"body"`
	Base  string `json:"base"`
	Draft bool   `json:"draft"`
}

CreatePRRequest is the wire shape for POST /worktrees/{id}/pr. All fields except Draft are required — no fallbacks (per CLAUDE.md). The head branch is derived from the worktree's current branch (the worktree IS its branch by construction).

type CreatePRResult

type CreatePRResult struct {
	PRNumber   int    `json:"pr_number"`
	PRURL      string `json:"pr_url"`
	HeadBranch string `json:"head_branch"`
	BaseBranch string `json:"base_branch"`
	HeadSHA    string `json:"head_sha"`
	Committed  bool   `json:"committed"` // auto-committed uncommitted work before pushing
}

CreatePRResult is the wire shape for the 201 response. Includes head_branch and base_branch so the client doesn't have to remember what it sent.

type CreateWorktreeResult

type CreateWorktreeResult struct {
	WorktreeID  string `json:"worktree_id"`
	Branch      string `json:"branch"`
	WorktreeDir string `json:"worktree_dir"`
	DisplayName string `json:"display_name"`
	// OriginRepo identifies the repo this worktree was created from
	// (e.g. "acme/api" or a local-dir basename fallback). Persisted on
	// the worktree row so clients (mobile picker, TUI) can group by repo.
	OriginRepo string `json:"origin_repo"`
	// RepoSlug is the repo-first routing key (~/work/repos/<slug>) this
	// worktree belongs to — the stable, single-segment, host-minted id
	// clients use for repo-scoped calls. Empty for worktrees created
	// outside the repo-first layout (laptop LocalPath forks).
	RepoSlug string `json:"repo_slug,omitempty"`
}

CreateWorktreeResult is the full info returned by Service.CreateWorktree. The gateway uses these fields to write the corresponding `worktrees` row in its database after the local git worktree is in place.

type GitHubPullRequestInspection

type GitHubPullRequestInspection struct {
	GitHubPullRequestLocator
	Title      string `json:"title"`
	HTMLURL    string `json:"html_url"`
	HeadOwner  string `json:"head_owner"`
	HeadRepo   string `json:"head_repo"`
	HeadBranch string `json:"head_branch"`
	HeadSHA    string `json:"head_sha"`
	BaseBranch string `json:"base_branch"`
	Author     string `json:"author"`
	IsPrivate  bool   `json:"is_private"`
}

GitHubPullRequestInspection is the exact revision a client asks a user to approve.

type GitHubPullRequestLaunchRequest

type GitHubPullRequestLaunchRequest struct {
	GitHubPullRequestLocator
	ExpectedHeadSHA string `json:"expected_head_sha"`
}

GitHubPullRequestLaunchRequest binds a checkout to the SHA the user approved.

type GitHubPullRequestLocator

type GitHubPullRequestLocator struct {
	Owner  string `json:"owner"`
	Repo   string `json:"repo"`
	Number int    `json:"number"`
}

GitHubPullRequestLocator identifies one PR without accepting an arbitrary clone URL.

type GitHubRepositoryInspection

type GitHubRepositoryInspection struct {
	GitHubRepositoryLocator
	HTMLURL       string `json:"html_url"`
	Description   string `json:"description"`
	DefaultBranch string `json:"default_branch"`
	IsPrivate     bool   `json:"is_private"`
}

GitHubRepositoryInspection is safe repository metadata shown before code is imported.

type GitHubRepositoryLaunchResult

type GitHubRepositoryLaunchResult struct {
	CreateWorktreeResult
	DefaultBranch string `json:"default_branch"`
}

GitHubRepositoryLaunchResult is the fresh editing worktree created from the repository's current remote default branch.

type GitHubRepositoryLocator

type GitHubRepositoryLocator struct {
	Owner string `json:"owner"`
	Repo  string `json:"repo"`
}

GitHubRepositoryLocator identifies one repository without accepting an arbitrary clone URL.

type HostStatus

type HostStatus struct {
	Hostname  string    `json:"hostname"`
	Version   string    `json:"version"`
	StartedAt time.Time `json:"started_at"`
	Sessions  int       `json:"sessions"` // Number of live (backend-attached) sessions
}

HostStatus is the response body of `GET /status` on the Host API. Hub surfaces a derived view (online/offline + last seen) to clients.

type MarkPRReadyResult

type MarkPRReadyResult struct {
	PRNumber int    `json:"pr_number"`
	PRURL    string `json:"pr_url"`
}

MarkPRReadyResult is the wire shape for POST /worktrees/{id}/pr/ready.

type MergeResult

type MergeResult struct {
	MergedBranch    string
	BranchWorktree  string // Path of the feature-branch worktree (empty if it was cleaned up)
	WorktreeRemoved bool
	BranchDeleted   bool
}

MergeResult describes the outcome of MergeBranch.

type ModelInfo

type ModelInfo = agent.ModelInfo

ModelInfo is re-exported from the agent package for the same reason as AgentInfo.

type Options

type Options struct {
	// ID is the host identifier. Defaults to HostLocal when empty.
	ID string
	// BackendManagers maps each backend type to its manager. Required.
	BackendManagers map[agent.BackendType]agent.BackendManager
	// BackendAllowed gates operations that start or inspect an agent harness.
	// Nil allows every configured backend; laptop mode supplies a dynamic
	// preferences-backed check so a newly granted permission applies without
	// restarting clank-host.
	BackendAllowed func(agent.BackendType) bool
	// Log is the logger. Defaults to a logger writing to stderr with the
	// "[clank-host]" prefix.
	Log *log.Logger
	// BranchCacheTTL overrides the default TTL for the listBranches
	// cache. Zero uses DefaultBranchCacheTTL. Tests set this to control
	// staleness behavior.
	BranchCacheTTL time.Duration
	// Now overrides the clock used by the listBranches cache. Tests
	// inject a controllable clock to assert cache hit/miss behavior
	// without sleeping. Nil means time.Now.
	Now func() time.Time

	// SessionsStore persists session metadata. Required in production;
	// optional in tests. When nil, session-metadata methods return
	// SessionStoreNotConfigured.
	SessionsStore *store.Store

	// KeepaliveListener forwards backend-event activity to a provider-
	// specific keep-alive mechanism (e.g. the Sprites Tasks API). Nil
	// disables the subsystem entirely — laptop mode default. Set by
	// cmd/clank-host/main.go from the --keepalive-provider flag.
	KeepaliveListener keepalive.Listener

	// NotifierLoop, when set, delivers the host's push Notifications to
	// an outbound Provider (webhook/expo/noop). Nil disables the
	// subsystem — laptop mode default. Construct via notifier.New in
	// cmd/clank-host/main.go.
	NotifierLoop *notifier.Loop

	// PreviewGWClient calls the gateway's preview register/revoke
	// webhooks to mint and tear down public tokens. Nil (or a client
	// constructed with empty URL) keeps preview spawns local-only —
	// the dev server runs but Status.Token/URL stay empty.
	PreviewGWClient *preview.GWClient

	// GitHubOAuthClientID is the Clank GitHub OAuth App's client_id,
	// used by the host's GitHub Connect device flow. Empty disables
	// the connect surface (status reports available:false). When
	// non-empty, takes precedence over the CLANK_GITHUB_OAUTH_CLIENT_ID
	// env var the laptop's clank-host inherits from clankd.
	GitHubOAuthClientID string

	// GitHubGhCLIAuth lets GitHub token resolution fall back to the
	// machine's own gh CLI login (`gh auth token`) when no clank
	// connection exists. Set by the local laptop provisioner — the
	// host IS the user's machine there; remote sandboxes keep token
	// access explicit.
	GitHubGhCLIAuth bool

	// AnthropicClaudeCLIAuth lets Anthropic provider status report the
	// machine's own claude CLI login (Keychain / .credentials.json)
	// as connected when clank's anthropic sink is empty — presence
	// detection only, the credential is never read. Set by the local
	// laptop provisioner for the same reason as GitHubGhCLIAuth: the
	// spawned claude already uses that login there.
	AnthropicClaudeCLIAuth bool

	// OpenAICodexCLIAuth lets codex provider status report the
	// machine's own codex CLI login ($CODEX_HOME/auth.json) as a
	// connected ChatGPT subscription when clank didn't run the device
	// ceremony — presence detection only. Set by the local laptop
	// provisioner: the codex-acp adapter inherits the host environment
	// there and uses that login anyway.
	OpenAICodexCLIAuth bool

	// ProjectCommitterName / ProjectCommitterEmail set the git committer
	// identity stamped on the seed commit of a project scaffolded via
	// CreateProjectFromTemplate (also persisted as the new repo's local
	// git identity). Attribution is operator branding, so the deploy-time
	// caller (e.g. clank-host's --project-committer-* flags) injects the
	// real values; empty falls back to a neutral default in New, keeping
	// any vendor identity out of this OSS package.
	ProjectCommitterName  string
	ProjectCommitterEmail string

	// Templates is the operator-configured builtin half of the
	// create-project catalog, served by GET /templates alongside the
	// user's own GitHub template repos. Empty means no builtin
	// templates (github-only or none).
	Templates []Template

	// WorkRoot is the parent directory for worktrees (/<WorktreeID>)
	// and repo canonicals (/repos). Empty uses $HOME/work — the
	// dedicated-sandbox default. The local laptop provisioner sets
	// this to <config dir>/work so the host doesn't drop a work/
	// directory into the user's home.
	WorkRoot string

	// BuiltinPresets are the host-shipped agent presets, declared by the
	// provisioner via $CLANK_BUILTIN_PRESETS (the environment knows its
	// own blast radius). Served read-only by GET /presets, and each
	// backend's Default preset defines the REQUIRED config keys for
	// session creation. Empty means presets.Workstation() — the
	// conservative set — resolved by New itself, so no construction path
	// (tests included) silently runs with create-time validation off.
	BuiltinPresets []presets.Preset

	// PresetsDir is where user-created presets persist (presets.json).
	// Empty disables user presets; built-ins still serve.
	PresetsDir string
}

Options configures a Service at construction time.

type OverviewPR

type OverviewPR struct {
	Number    int                      `json:"number"`
	Title     string                   `json:"title"`
	State     OverviewPRState          `json:"state"`
	Draft     bool                     `json:"draft"`
	Author    string                   `json:"author"`
	URL       string                   `json:"url"`
	IsMine    bool                     `json:"is_mine"`
	UpdatedAt time.Time                `json:"updated_at,omitzero"`
	Checks    *githubpkg.CheckRollup   `json:"checks,omitempty"`
	Mergeable githubpkg.MergeableState `json:"mergeable,omitempty"`
}

OverviewPR is the PR annotation on an overview branch. IsMine compares the PR author against the connected GitHub login — the Drafts tab's "mine vs everyone" filter bit. State merged/closed marks a leftover local branch as finished work rather than an in-progress draft. Checks is the head commit's CI rollup, absent when the PR has no check runs (or the rollup fetch failed). Mergeable is present only once GitHub has computed the test merge (open PRs only) — conflicting PRs get no CI runs, so clients show the conflict where the CI badge would be.

type OverviewPRState

type OverviewPRState string

OverviewPRState is the lifecycle of an overview branch's PR. GitHub's list API reports merged PRs as "closed"; the overview splits them so clients can tell shipped work from abandoned work without a second call.

const (
	OverviewPRStateOpen   OverviewPRState = "open"
	OverviewPRStateMerged OverviewPRState = "merged"
	OverviewPRStateClosed OverviewPRState = "closed"
)

type PreviewOriginState

type PreviewOriginState string

PreviewOriginState classifies what we know about the worktree's origin before any network calls. Drives the mobile-side "Open PR to <owner>/<repo>" callout vs. the no-origin/non-github CTAs.

const (
	// PreviewOriginGitHub: origin is set and parses to a github.com URL.
	// Safe to enable the Open PR form.
	PreviewOriginGitHub PreviewOriginState = "github"
	// PreviewOriginNone: no `origin` remote on the worktree.
	PreviewOriginNone PreviewOriginState = "no_origin"
	// PreviewOriginNonGitHub: origin is set but points elsewhere
	// (GitLab, Gitea, GHE, ...). NonGitHubHost carries the host for
	// the error message.
	PreviewOriginNonGitHub PreviewOriginState = "non_github"
)

type PreviewPRResult

type PreviewPRResult struct {
	Owner         string             `json:"owner,omitempty"`
	Repo          string             `json:"repo,omitempty"`
	HeadBranch    string             `json:"head_branch,omitempty"`
	HeadSHA       string             `json:"head_sha,omitempty"`
	OriginState   PreviewOriginState `json:"origin_state"`
	NonGitHubHost string             `json:"non_github_host,omitempty"`
}

PreviewPRResult is the wire shape for the preview endpoint. The mobile CreatePRSheet uses Owner/Repo to render the destination callout, and OriginState to gate which form variant to show.

type PublishRequest

type PublishRequest struct {
	Name    string `json:"name,omitempty"`
	Private *bool  `json:"private,omitempty"`
}

PublishRequest is the body for POST /worktrees/{id}/remote/publish. Name is sanitized to GitHub's allowed characters; Private defaults to true when omitted.

type PublishResult

type PublishResult struct {
	Owner   string `json:"owner"`
	Repo    string `json:"repo"`
	Branch  string `json:"branch"`
	HTMLURL string `json:"html_url"`
	HeadSHA string `json:"head_sha"`
}

PublishResult is the wire shape returned after a successful publish. The client refetches remote/status afterward to pick up the normal push/pull/PR UI (origin now exists).

type PullResult

type PullResult struct {
	Branch        string      `json:"branch"`
	State         RemoteState `json:"state"`
	HeadSHA       string      `json:"head_sha"`
	FastForwarded bool        `json:"fast_forwarded"`
}

PullResult is the wire shape for POST /worktrees/{id}/remote/pull.

type PushResult

type PushResult struct {
	Branch    string `json:"branch"`
	HeadSHA   string `json:"head_sha"`
	Committed bool   `json:"committed"` // auto-committed uncommitted work before pushing
	Pushed    bool   `json:"pushed"`
}

PushResult is the wire shape for POST /worktrees/{id}/remote/push.

type RemoteState

type RemoteState string

RemoteState classifies a worktree's branch relative to origin/<branch>.

const (
	RemoteStateSynced     RemoteState = "synced"      // local == remote, clean tree
	RemoteStateUnpushed   RemoteState = "unpushed"    // local ahead and/or dirty, remote not ahead
	RemoteStateBehind     RemoteState = "behind"      // remote ahead, fast-forwardable
	RemoteStateDiverged   RemoteState = "diverged"    // both sides advanced — needs resolution
	RemoteStateConflict   RemoteState = "conflict"    // a merge is in progress with conflicts
	RemoteStateNoUpstream RemoteState = "no_upstream" // branch not on the remote yet
)

type RemoteStatusResult

type RemoteStatusResult struct {
	Branch       string                   `json:"branch"`
	Owner        string                   `json:"owner"`
	Repo         string                   `json:"repo"`
	State        RemoteState              `json:"state"`
	Ahead        int                      `json:"ahead"`
	Behind       int                      `json:"behind"`
	Dirty        bool                     `json:"dirty"`
	LocalHead    string                   `json:"local_head"`
	RemoteHead   string                   `json:"remote_head,omitempty"`
	PRNumber     int                      `json:"pr_number,omitempty"`
	PRURL        string                   `json:"pr_url,omitempty"`
	PRBaseBranch string                   `json:"pr_base_branch,omitempty"`
	PRDraft      bool                     `json:"pr_draft,omitempty"`
	PRMergeable  githubpkg.MergeableState `json:"pr_mergeable,omitempty"`
}

RemoteStatusResult is the wire shape for GET /worktrees/{id}/remote/status. PRMergeable is present only once GitHub has computed the PR's test merge (absent = unknown, not clean) — a conflicting PR gets no CI runs, so clients surface the conflict where CI status would be.

type RepoBranchOverview

type RepoBranchOverview struct {
	Branch     string `json:"branch"`
	WorktreeID string `json:"worktree_id,omitempty"`
	Loaded     bool   `json:"loaded"`

	// CheckedOutPath names the working tree holding this branch when it
	// is checked out OUTSIDE clank's management — a local checkout's
	// primary worktree, or one the user (or another tool) added by
	// hand. Mutually exclusive with WorktreeID. Such a branch cannot be
	// loaded (git allows one checkout per branch): clients should offer
	// opening it in place, or forking off it — not a check-out.
	CheckedOutPath string `json:"checked_out_path,omitempty"`

	Dirty        bool        `json:"dirty,omitempty"`
	Ahead        *int        `json:"ahead,omitempty"`
	Behind       *int        `json:"behind,omitempty"`
	LastCommitAt time.Time   `json:"last_commit_at,omitzero"`
	PR           *OverviewPR `json:"pr,omitempty"`
}

RepoBranchOverview is one work item: a branch (loaded or not) and/or its open PR. Ahead/Behind compare refs/heads/<branch> against refs/remotes/origin/<branch> and are present only when the tracking ref exists (omitted otherwise — absence means "no comparison available", not "in sync"). Dirty is computed only for loaded branches (it's a property of a worktree's working tree).

type RepoInfo

type RepoInfo struct {
	Slug          string             `json:"slug"`
	Label         string             `json:"label"`
	Origin        *RepoOrigin        `json:"origin"`
	DefaultBranch string             `json:"default_branch"`
	Worktrees     []RepoWorktreeInfo `json:"worktrees"`

	// IsLocalCheckout marks a repo discovered from the user's own
	// filesystem (see repos_local.go) rather than a ~/work/repos
	// canonical: it cannot be deleted through clank, and Path names its
	// root so clients can show where it lives.
	IsLocalCheckout bool   `json:"is_local_checkout,omitempty"`
	Path            string `json:"path,omitempty"`
}

RepoInfo is one repo in the listing. Slug is the stable single- segment host-minted routing key (doubles as the dir name; NOT necessarily equal to owner/repo — greenfield repos are named before they have an origin, and a GitHub rename changes the label, not the slug).

type RepoOrigin

type RepoOrigin struct {
	Owner string `json:"owner"`
	Repo  string `json:"repo"`
}

RepoOrigin identifies a repo's GitHub origin. Nil on RepoInfo means the repo is unpublished greenfield — presence IS the "on GitHub" bit.

type RepoOverviewResult

type RepoOverviewResult struct {
	Slug          string               `json:"slug"`
	Label         string               `json:"label"`
	Origin        *RepoOrigin          `json:"origin"`
	DefaultBranch string               `json:"default_branch"`
	Fetched       bool                 `json:"fetched"`
	Branches      []RepoBranchOverview `json:"branches"`
}

RepoOverviewResult is the wire shape of GET /repos/{slug}/overview.

type RepoWorktreeInfo

type RepoWorktreeInfo struct {
	WorktreeID  string `json:"worktree_id"`
	Branch      string `json:"branch"`
	DisplayName string `json:"display_name"`
}

RepoWorktreeInfo is one loaded branch of a repo: the worktree's id (how sessions address it), its branch, and a display name (the branch — the only durable identity a worktree has host-side).

type RepoWorktreeRequest

type RepoWorktreeRequest struct {
	Branch     string `json:"branch,omitempty"`
	BaseBranch string `json:"base_branch,omitempty"`
}

RepoWorktreeRequest is the body of POST /repos/{slug}/worktrees. Exactly one field must be set:

  • Branch: LOAD an existing branch (local, or fetched from origin) into a worktree. Idempotent — a branch that's already loaded returns its existing worktree with created=false.
  • BaseBranch: FORK a new petname branch off the named base and load that.

type RepoWorktreeResult

type RepoWorktreeResult struct {
	CreateWorktreeResult
	Created bool `json:"created"`
}

RepoWorktreeResult pairs a CreateWorktreeResult with whether the call actually created the worktree (false = idempotent hit on an existing one). The wire shape of POST /repos/{slug}/worktrees.

type ResolveResult

type ResolveResult struct {
	Branch          string          `json:"branch"`
	Strategy        ResolveStrategy `json:"strategy"`
	State           RemoteState     `json:"state"`
	HeadSHA         string          `json:"head_sha,omitempty"`
	BackupRef       string          `json:"backup_ref,omitempty"`       // take_remote: where the discarded HEAD was saved
	ConflictedFiles []string        `json:"conflicted_files,omitempty"` // merge: unresolved paths (the client hands these to an agent)
}

ResolveResult is the wire shape for POST /worktrees/{id}/remote/resolve.

type ResolveStrategy

type ResolveStrategy string

ResolveStrategy selects how a diverged worktree reconciles with its remote.

const (
	// ResolveTakeRemote discards local divergence: back up the current
	// HEAD to a recovery ref, then hard-reset the branch to origin/<branch>.
	ResolveTakeRemote ResolveStrategy = "take_remote"
	// ResolveMerge keeps local work and merges origin/<branch> in. A clean
	// merge leaves an unpushed merge commit; a conflicting merge is left in
	// progress (ConflictedFiles populated) for an agent or the user to
	// resolve before pushing.
	ResolveMerge ResolveStrategy = "merge"
	// ResolveAbort cancels an in-progress merge, restoring the pre-merge
	// state.
	ResolveAbort ResolveStrategy = "abort"
)

type Service

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

Service is the Host plane's domain object. Construct with New; call Init to start background goroutines and Shutdown to release them. Owns a registry of live SessionBackends keyed by session ULID.

func New

func New(opts Options) *Service

New creates a Service. Panics on missing BackendManagers — fast failure beats a later nil deref.

func (*Service) AbortSession

func (s *Service) AbortSession(ctx context.Context, id string) error

AbortSession asks the agent to stop streaming.

func (*Service) Auth

func (s *Service) Auth() *AuthManager

Auth returns the AuthManager, or nil when the OpenCode backend isn't registered. Callers must nil-check.

func (*Service) ConfigOptions

func (s *Service) ConfigOptions(ctx context.Context, bt agent.BackendType, ref agent.GitRef) ([]agent.ConfigOption, error)

ConfigOptions probes the agent's advertised config options for a backend in ref's project, for knob editors that open before a session exists (compose). On-demand and uncached — the caller shows a spinner for exactly this call. Empty when the backend can't report them.

func (*Service) CreatePR

func (s *Service) CreatePR(ctx context.Context, ref agent.GitRef, req CreatePRRequest) (CreatePRResult, error)

CreatePR commits any uncommitted work (same auto-commit as PushToRemote), pushes the worktree's current branch, and opens a pull request against base. Errors surface verbatim — the mux handler classifies them into HTTP statuses; the github package's typed errors (ErrPRAlreadyExists, ErrPushNotFastForward, etc.) pass through.

func (*Service) CreateProjectFromTemplate

func (s *Service) CreateProjectFromTemplate(ctx context.Context, cloneURL, githubToken, name string) (CreateWorktreeResult, error)

CreateProjectFromTemplate scaffolds a brand-new project under the repo-first layout: a bare canonical at ~/work/repos/<slug>/repo.git seeded with one fresh commit of the template's files (no template history, no remote), plus a linked `git worktree` for main at ~/work/<WorktreeID>. A greenfield app is a REAL git repo from birth — branches, forks, and the repo overview all work before it's ever published; PublishToRemote later just adds origin + pushes.

The seed commit is built in a throwaway temp checkout and pushed into the bare canonical over the filesystem — `git worktree add --orphan` would be simpler but needs git ≥ 2.42, newer than our runtime floor.

cloneURL comes from one of two trusted resolvers — the gateway's operator catalog, or the host's own GitHub-template resolution (mux) — never a raw client URL. githubToken authenticates the template clone for private repos; empty for public/builtin templates. name becomes the display name, the label, and (sanitized) the slug.

func (*Service) CreateRepoWorktree

func (s *Service) CreateRepoWorktree(ctx context.Context, slug string, req RepoWorktreeRequest) (RepoWorktreeResult, error)

CreateRepoWorktree creates (or idempotently returns) a worktree in the repo identified by slug. Branch resolution goes local ref → remote-tracking ref → one authed fetch from origin — which is what retires the "base branch doesn't exist in the shallow clone" 404 class: the canonical can always materialize any branch its origin has. ErrRepoNotFound for an unknown slug; ErrNotFound for a branch that exists nowhere; ErrGitHubNotConnected when a needed fetch has no token.

func (*Service) CreateSession

func (s *Service) CreateSession(ctx context.Context, sessionID string, req agent.StartRequest) (agent.SessionBackend, agent.SessionInfo, error)

CreateSession registers a fresh SessionBackend under sessionID. The backend is NOT started — callers call Start() or Watch().

Returns a SessionInfo snapshot: req.GitRef is normalized (a LocalPath inside a repo becomes {root, Subdir}), and ServerURL is populated for backends with an HTTP server (OpenCode only) but never persisted — it's process-local. Persisting the rest is attempted when a store is configured and is best-effort: a write failure is logged, not surfaced, since rolling back a running backend is worse UX than an unpersisted row. The session's working directory is workDirFor of the normalized ref.

func (*Service) DeletePreset

func (s *Service) DeletePreset(id string) error

DeletePreset removes a user preset.

func (*Service) DeleteRepo

func (s *Service) DeleteRepo(ctx context.Context, slug string) error

DeleteRepo removes the repo identified by slug: every linked worktree (session purge + busy guard + git-aware removal via removeLinkedWorktree) and then the canonical clone itself. Fails fast with ErrWorktreeBusy BEFORE deleting anything when any worktree has a live session; a session that starts mid-loop still trips the per-worktree re-check, leaving a partially-deleted repo the caller can retry (each leg is idempotent).

LOCK ORDER: repo lock, then per-worktree locks inside removeLinkedWorktree — the same order DeleteWorktree uses, so the two can't ABBA-deadlock.

func (*Service) DeleteSessionMetadata

func (s *Service) DeleteSessionMetadata(ctx context.Context, id string) error

DeleteSessionMetadata removes the persisted session row. Idempotent. Note: this does NOT stop a running session backend; callers should invoke StopSession first if they want both.

func (*Service) DeleteWorktree

func (s *Service) DeleteWorktree(ctx context.Context, worktreeID string) error

DeleteWorktree removes a worktree's persisted sessions and ~/work/<id> directory. Refuses with ErrWorktreeBusy when a session is active; idempotent otherwise.

LOCK ORDER: repo lock (when the worktree is repo-first linked) BEFORE the per-worktree lock — the same order DeleteRepo uses, so the two can't ABBA-deadlock. The linked-ness probe runs before any lock: it's a read-only `git rev-parse` and the answer can't change under us (only this method and DeleteRepo unlink worktrees, both serialized by the repo lock).

func (*Service) DiscoverSessions

func (s *Service) DiscoverSessions(ctx context.Context, bt agent.BackendType, seedDir string) ([]agent.SessionSnapshot, error)

DiscoverSessions asks the backend manager for historical sessions. seedDir=="" hits AllSessionDiscoverer if implemented (global heal); otherwise SessionDiscoverer(seedDir). nil, nil for managers that implement neither.

func (*Service) FindSessionByExternalID

func (s *Service) FindSessionByExternalID(ctx context.Context, externalID string) (agent.SessionInfo, error)

FindSessionByExternalID looks up a session by the backend-assigned id. Used by discovery to dedupe historical sessions on rebuild.

func (*Service) ForkSession

func (s *Service) ForkSession(ctx context.Context, id, messageID string) (agent.SessionInfo, error)

ForkSession creates a sibling session forked off messageID and persists it as a new row so callers can navigate to it by the host's internal ID. Without this the wire response would only carry the backend's external session id (e.g. "ses_…"), which doesn't map to anything the host can look up.

func (*Service) GetSessionMetadata

func (s *Service) GetSessionMetadata(ctx context.Context, id string) (agent.SessionInfo, error)

GetSessionMetadata returns one persisted session by ID, decorated with runtime-only fields from the live backend when one is registered (agent-owned session modes for the client's mode picker).

func (*Service) GitHub

func (s *Service) GitHub() *githubpkg.Manager

GitHub returns the GitHub Connect manager, or nil when home-dir resolution failed at construction. Callers must nil-check.

func (*Service) ID

func (s *Service) ID() string

ID returns the host's ID.

func (*Service) ImportProjectFromGitHub

func (s *Service) ImportProjectFromGitHub(ctx context.Context, owner, repo, branch string) (CreateWorktreeResult, error)

ImportProjectFromGitHub loads the caller's existing GitHub repo owner/repo under the repo-first layout: one bare BLOBLESS canonical clone at ~/work/repos/<slug>/repo.git (created on first import, reused after) plus a linked `git worktree` for the requested branch at ~/work/<WorktreeID>. The clone authenticates with the host's stored GitHub token, so private repos work; ErrNotConnected surfaces when no token is present.

Idempotent per branch: importing a branch that already has a linked worktree returns that worktree (Created=false) instead of a duplicate — a branch can be checked out in at most one worktree (git's invariant, and the product's).

The host builds the clone URL from owner/repo itself — it never accepts a client-supplied URL — matching the template flow's gatekeeping.

func (*Service) Init

func (s *Service) Init(ctx context.Context, knownDirs func(agent.BackendType) ([]string, error)) error

Init initializes all BackendManagers. knownDirs returns previously- seen project directories per backend (used to warm long-lived servers like OpenCode); pass a func returning nil to skip warm-up. Non-blocking — managers run reconciler goroutines for the lifetime of ctx.

func (*Service) InspectGitHubPullRequest

func (s *Service) InspectGitHubPullRequest(ctx context.Context, locator GitHubPullRequestLocator) (GitHubPullRequestInspection, error)

InspectGitHubPullRequest resolves a PR without downloading or running its code.

func (*Service) InspectGitHubRepository

func (s *Service) InspectGitHubRepository(ctx context.Context, locator GitHubRepositoryLocator) (GitHubRepositoryInspection, error)

InspectGitHubRepository resolves repository metadata without downloading code.

func (*Service) LaunchGitHubPullRequest

func (s *Service) LaunchGitHubPullRequest(ctx context.Context, req GitHubPullRequestLaunchRequest) (CreateWorktreeResult, error)

LaunchGitHubPullRequest fetches and checks out only the approved PR revision.

func (*Service) LaunchGitHubRepository

func (s *Service) LaunchGitHubRepository(ctx context.Context, locator GitHubRepositoryLocator) (GitHubRepositoryLaunchResult, error)

LaunchGitHubRepository imports the default branch, then forks a fresh editing branch from the latest remote default-branch tip without mutating that checkout.

func (*Service) ListAgents

func (s *Service) ListAgents(ctx context.Context, bt agent.BackendType, ref agent.GitRef) ([]AgentInfo, error)

ListAgents returns the agents the backend supports for ref's repo. (nil, nil) means the backend is unknown or doesn't implement listing — neither is an error.

func (*Service) ListBackends

func (s *Service) ListBackends(_ context.Context) ([]BackendInfo, error)

ListBackends returns the set of backends known to this host.

func (*Service) ListBranches

func (s *Service) ListBranches(ctx context.Context, ref agent.GitRef) ([]BranchInfo, error)

ListBranches returns the branches (and their checked-out worktrees) for the repository identified by ref. Skips bare and detached entries. ref.WorktreeBranch is ignored — listing operates on the repo root.

func (*Service) ListRepos

func (s *Service) ListRepos(ctx context.Context) ([]RepoInfo, error)

ListRepos enumerates the canonical clones under ~/work/repos and their linked worktrees, then the local checkouts discovered from session history (repos_local.go). Repos whose slug dir lacks a repo.git (torn creation) are skipped; worktree entries whose dir has vanished (manual rm — prunable bookkeeping) are filtered out.

func (*Service) ListSessionMetadata

func (s *Service) ListSessionMetadata(ctx context.Context) ([]agent.SessionInfo, error)

ListSessionMetadata returns every persisted session, newest-updated first.

func (*Service) MarkPRReady

func (s *Service) MarkPRReady(ctx context.Context, ref agent.GitRef) (MarkPRReadyResult, error)

MarkPRReady flips the open PR for the worktree's current branch from draft to ready-for-review. Idempotent: an already-ready PR succeeds without a mutation.

func (*Service) MarkSessionRead

func (s *Service) MarkSessionRead(ctx context.Context, id string) error

MarkSessionRead bumps last_read_at on the session record. Returns ErrNotFound if the session doesn't exist.

func (*Service) MergeBranch

func (s *Service) MergeBranch(ctx context.Context, ref agent.GitRef, branch, commitMessage string) (MergeResult, error)

MergeBranch merges branch into ref's repo's default branch.

func (*Service) OpenAndSend

func (s *Service) OpenAndSend(ctx context.Context, id string, opts agent.SendMessageOpts) (agent.SessionStatus, string, error)

OpenAndSend opens the backend and dispatches opts as the initial turn (or a follow-up after resume).

func (*Service) OpenSession

func (s *Service) OpenSession(ctx context.Context, id string) (agent.SessionStatus, string, error)

OpenSession ensures the backend is live and its SSE listener is attached. Returns the post-Open snapshot (status, external session id) — async-init backends like Claude only learn their session id inside Open. Idempotent.

func (*Service) PendingPermissions

func (s *Service) PendingPermissions(ctx context.Context, id string) ([]agent.PermissionData, error)

PendingPermissions returns the permission requests parked on the session's live backend, oldest first, so a client (re)joining a blocked session can re-render the prompt. A pure in-memory read: it never rehydrates a backend, because without a live one nothing can be parked — the agent process and its queue die together — and waking an agent to ask "are you blocked?" would violate the pure-read contract (see SessionMessages).

func (*Service) Presets

func (s *Service) Presets(bt agent.BackendType) []presets.Preset

Presets returns built-ins plus user presets, optionally filtered by backend. Built-ins first, so clients render them at the top.

func (*Service) PreviewLogs

func (s *Service) PreviewLogs(ctx context.Context, worktreeID, serviceName string) []byte

PreviewLogs returns the selected service's ANSI-stripped stdout/stderr tail. An empty name resolves Expo or the configured default, same as PreviewStatus. The result is bounded by the preview package's ring capacity and needs no pagination.

func (*Service) PreviewPR

func (s *Service) PreviewPR(_ context.Context, ref agent.GitRef) (PreviewPRResult, error)

PreviewPR returns what a CreatePR call WOULD push without actually pushing. The mobile CreatePRSheet calls this to show the user the parsed destination repo before they tap Open PR — primary UX defense against wrong-repo-because-origin-is-misconfigured.

Cheap: no fetch, no network calls, no GitHub API requests. Just resolves the worktree, reads HEAD, classifies origin.

func (*Service) PreviewStart

func (s *Service) PreviewStart(ctx context.Context, worktreeID, launchName string) (preview.Status, error)

PreviewStart resolves the preview key (worktree ID or folder slug) to a workdir and asks the preview manager to spawn the selected launch. An empty launch name resolves Expo or the configured default. Status carries the gateway-minted public URL + token when the host is wired to a gateway; on laptop dev with no gateway, those fields stay empty.

Idempotent — a second call for the same key returns the existing snapshot.

func (*Service) PreviewStatus

func (s *Service) PreviewStatus(ctx context.Context, worktreeID, launchName string) (preview.Status, error)

PreviewStatus returns availability and state for the selected launch on the preview key (worktree ID or folder slug).

func (*Service) PreviewStop

func (s *Service) PreviewStop(_ context.Context, worktreeID, serviceName string) error

PreviewStop terminates the selected service. An empty service name stops all services registered under the worktree, preserving the original mobile API.

func (*Service) PublishToRemote

func (s *Service) PublishToRemote(ctx context.Context, ref agent.GitRef, req PublishRequest) (PublishResult, error)

PublishToRemote creates a brand-new repository (private by default) for a remote-less worktree, adds it as origin, commits any uncommitted work, and pushes the current branch. ErrAlreadyPublished when origin already exists.

TODO(ai-review): only sanitizeRepoName is covered by tests; the orchestration itself (create repo, add origin, commit, push, error mapping) has no integration coverage yet. https://github.com/supaclank/clank/pull/90#discussion_r3508931108

func (*Service) PullFromRemote

func (s *Service) PullFromRemote(ctx context.Context, ref agent.GitRef) (PullResult, error)

PullFromRemote fast-forwards the worktree's branch to its GitHub remote when it's cleanly behind. Refuses on a dirty tree (ErrWorktreeDirty) or when the histories diverged (ErrRemoteDiverged) — divergence routes to the conflict-resolution flow. Also invoked by cold-start auto-pull.

func (*Service) PushToRemote

func (s *Service) PushToRemote(ctx context.Context, ref agent.GitRef) (PushResult, error)

PushToRemote commits any uncommitted work in the worktree (hardcoded message) and pushes the branch to its GitHub remote. ErrRemoteDiverged when the remote has advanced and the push is rejected as non-fast- forward — the client then routes to the conflict-resolution flow. ErrNoCommonAncestor when the remote shares no history with the worktree (origin points at the wrong repo).

func (*Service) PutPreset

func (s *Service) PutPreset(p presets.Preset) error

PutPreset stores a user preset.

func (*Service) RemoteSyncStatus

func (s *Service) RemoteSyncStatus(ctx context.Context, ref agent.GitRef) (RemoteStatusResult, error)

RemoteSyncStatus reports where the ref's branch sits relative to its GitHub remote, plus the open PR (if any) for the branch. Does a network fetch — callers should refresh on demand, not poll tightly.

func (*Service) RemoveWorktree

func (s *Service) RemoveWorktree(ctx context.Context, ref agent.GitRef, branch string, force bool) error

RemoveWorktree removes the worktree for (ref's repo, branch).

func (*Service) RepoOverview

func (s *Service) RepoOverview(ctx context.Context, slug string, fetch bool) (RepoOverviewResult, error)

RepoOverview assembles the feed for slug. fetch=true refreshes refs/remotes/origin/* first (one authed fetch; a no-op for unpublished repos). ErrRepoNotFound for an unknown slug.

func (*Service) ResolveRemote

func (s *Service) ResolveRemote(ctx context.Context, ref agent.GitRef, strategy ResolveStrategy) (ResolveResult, error)

ResolveRemote reconciles a diverged worktree with its remote per strategy. The agent-merge flow is client-driven: pick ResolveMerge, and on a conflict result the client starts a session seeded to resolve the in-progress merge — so this method stays decoupled from session creation and never has to guess a backend.

func (*Service) ResolveWorktree

func (s *Service) ResolveWorktree(ctx context.Context, ref agent.GitRef, branch string) (WorktreeInfo, error)

ResolveWorktree ensures a worktree exists for (ref's repo, branch) and returns its info. ref.WorktreeBranch is ignored — pass branch as a distinct argument so the caller's intent ("resolve THIS branch") is explicit at the call site.

func (*Service) RespondPermission

func (s *Service) RespondPermission(ctx context.Context, id, permissionID string, allow bool, denyMessage string) error

RespondPermission replies to a pending tool-use permission prompt. denyMessage is the reason forwarded to the model when allow is false (empty for a default).

func (*Service) SearchSessionMetadata

func (s *Service) SearchSessionMetadata(ctx context.Context, p store.SearchParams) ([]agent.SessionInfo, error)

SearchSessionMetadata applies the filters in p and returns matching sessions, newest-updated first.

func (*Service) SendMessage

func (s *Service) SendMessage(ctx context.Context, id string, opts agent.SendMessageOpts) error

SendMessage dispatches opts to the session's live backend.

func (*Service) Session

func (s *Service) Session(id string) (agent.SessionBackend, bool)

Session returns the live SessionBackend for id, or (nil, false). Does NOT rehydrate — callers that need cross-restart resume use ensureBackend (via the typed live-session ops below).

func (*Service) SessionMessages

func (s *Service) SessionMessages(ctx context.Context, id string) ([]agent.MessageData, error)

SessionMessages returns the conversation history. A live backend serves it directly. Without one, backends whose manager implements agent.TranscriptReader (Claude) are served straight from the on-disk transcript — no backend registration, no Open, no CLI spawn — so a pure history read never wakes the agent. Backends whose history API needs the live server (opencode) keep rehydrating via ensureBackend.

func (*Service) SetSessionDraft

func (s *Service) SetSessionDraft(ctx context.Context, id, draft string) error

SetSessionDraft persists an in-progress prompt draft.

func (*Service) SetSessionVisibility

func (s *Service) SetSessionVisibility(ctx context.Context, id string, vis agent.SessionVisibility) error

SetSessionVisibility updates the visibility flag (e.g. archived).

func (*Service) Shutdown

func (s *Service) Shutdown()

Shutdown stops live backends and then BackendManagers. Idempotent. Order: mark closed → stop backends (closes Events() → relays exit) → wait for relays → close subscribers → shut down managers.

func (*Service) Status

func (s *Service) Status(_ context.Context) (HostStatus, error)

Status returns the current host status.

func (*Service) StopSession

func (s *Service) StopSession(id string) error

StopSession stops the SessionBackend registered under id and removes it from the registry. Returns ErrNotFound if there is no such session. Safe to call concurrently with reads.

func (*Service) Subscribe

func (s *Service) Subscribe() (string, <-chan agent.Event)

Subscribe registers an event subscriber and returns an opaque ID and the receive channel. Caller must Unsubscribe when done. Slow consumers drop events instead of blocking the publisher.

func (*Service) Templates

func (s *Service) Templates() []Template

Templates returns the operator-configured builtin templates.

func (*Service) ToggleSessionFollowUp

func (s *Service) ToggleSessionFollowUp(ctx context.Context, id string) (agent.SessionInfo, error)

ToggleSessionFollowUp flips the follow_up flag and returns the new state. Does NOT bump UpdatedAt — see mutateSessionMeta.

func (*Service) Unsubscribe

func (s *Service) Unsubscribe(id string)

Unsubscribe deregisters the given subscriber and closes its channel. Idempotent.

func (*Service) UpsertSessionMetadata

func (s *Service) UpsertSessionMetadata(ctx context.Context, info agent.SessionInfo) error

UpsertSessionMetadata persists the session record.

func (*Service) ValidateCreateConfig

func (s *Service) ValidateCreateConfig(bt agent.BackendType, cfg map[string]string) error

ValidateCreateConfig enforces the create-time config contract: every key of the backend's built-in Default preset must be present. The host never fills values in — a missing key is the client's bug, surfaced loudly (400) instead of a hidden substitution. Values are not checked here: the agent owns its vocabulary and skips ids it doesn't advertise.

func (*Service) WorktreeHasActiveSession

func (s *Service) WorktreeHasActiveSession(ctx context.Context, worktreeID string) (bool, error)

WorktreeHasActiveSession reports whether any session for worktreeID is currently running (busy or starting) on this host. DeleteWorktree and DeleteRepo use it to refuse removing a worktree with live work. A Service without a sessions store (test wiring) reports false.

type Template

type Template struct {
	// ID is tolerated for config compatibility (older catalogs carried
	// ids) but no longer travels on the wire: a template's identity is
	// its clone URL.
	ID          string `json:"id,omitempty"`
	DisplayName string `json:"display_name"`
	CloneURL    string `json:"clone_url"`
}

Template is one operator-configured ("builtin") entry of the create-project catalog, injected at process start (clank-host --templates-json / $CLANK_TEMPLATES). The host owns the whole template surface — builtin entries here, the user's own GitHub template repos live via internal/host/github — so self-hosted and laptop deployments work without a gateway.

type WorktreeInfo

type WorktreeInfo struct {
	Branch      string `json:"branch"`
	WorktreeDir string `json:"worktree_dir"`
}

WorktreeInfo describes a single worktree managed by the Host.

Directories

Path Synopsis
Package hostclient is the Hub-side handle for talking to a Host.
Package hostclient is the Hub-side handle for talking to a Host.
Package github holds the host-side GitHub integration: the credential store, the device-flow runtime, the GitHub API client, and the orchestration that combines them for "create a PR from a worktree" requests.
Package github holds the host-side GitHub integration: the credential store, the device-flow runtime, the GitHub API client, and the orchestration that combines them for "create a PR from a worktree" requests.
Package hosttest provides shared test doubles for wiring an in-process host.Service in end-to-end tests: a stub backend manager standing in for the real opencode/claude process boundary, and a throwaway git repo factory.
Package hosttest provides shared test doubles for wiring an in-process host.Service in end-to-end tests: a stub backend manager standing in for the real opencode/claude process boundary, and a throwaway git repo factory.
Package hostmux exposes a *host.Service over HTTP.
Package hostmux exposes a *host.Service over HTTP.
Package petname generates short, memorable identifiers in the form adjective-animal-hex4 (e.g.
Package petname generates short, memorable identifiers in the form adjective-animal-hex4 (e.g.
Package preview resolves, spawns, and supervises per-worktree development servers on a Clank host.
Package preview resolves, spawns, and supervises per-worktree development servers on a Clank host.
Package store is the host's local SQLite for session metadata and the primary-agent cache.
Package store is the host's local SQLite for session metadata and the primary-agent cache.

Jump to

Keyboard shortcuts

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