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
- Variables
- func LocalRepoSlug(root string) string
- func SetGitHubCloneBaseForTest(base string) (prev string)
- func SetWorkRootForTest(path string) string
- type ACPBackendManager
- func (m *ACPBackendManager) BackendType() agent.BackendType
- func (m *ACPBackendManager) ConfigOptions(ctx context.Context, projectDir string) ([]agent.ConfigOption, error)
- func (m *ACPBackendManager) CreateBackend(ctx context.Context, inv agent.BackendInvocation) (agent.SessionBackend, error)
- func (m *ACPBackendManager) DiscoverAllSessions(ctx context.Context) ([]agent.SessionSnapshot, error)
- func (m *ACPBackendManager) DiscoverSessions(ctx context.Context, seedDir string) ([]agent.SessionSnapshot, error)
- func (m *ACPBackendManager) Init(ctx context.Context, knownDirs func() ([]string, error)) error
- func (m *ACPBackendManager) LoginArgv() (func(ctx context.Context) ([]string, error), bool)
- func (m *ACPBackendManager) RestartAdapters()
- func (m *ACPBackendManager) SetAmbientEnvResolver(f func() map[string]string)
- func (m *ACPBackendManager) SetEnvResolver(f func() map[string]string)
- func (m *ACPBackendManager) Shutdown()
- func (m *ACPBackendManager) Supervisor() *acp.AdapterSupervisor
- type ACPDirs
- type AgentInfo
- type AuthManager
- func (a *AuthManager) AnthropicAPIKey() string
- func (a *AuthManager) AnthropicEnv() map[string]string
- func (a *AuthManager) AnthropicOAuthToken() string
- func (a *AuthManager) AnthropicSinkPath() string
- func (a *AuthManager) AuthJSONPath() string
- func (a *AuthManager) CancelFlow(_ context.Context, flowID string) error
- func (a *AuthManager) DeleteCredential(ctx context.Context, providerID string) error
- func (a *AuthManager) EnableClaudeCLIFallback()
- func (a *AuthManager) EnableCodexCLIFallback()
- func (a *AuthManager) GetFlowStatus(_ context.Context, flowID string) (agent.DeviceFlowStatus, error)
- func (a *AuthManager) ListProviders(ctx context.Context, backend agent.BackendType) ([]agent.ProviderAuthInfo, error)
- func (a *AuthManager) OpenAIEnv() map[string]string
- func (a *AuthManager) OpenAISinkPath() string
- func (a *AuthManager) SetBackendAllowed(f func(agent.BackendType) bool)
- func (a *AuthManager) SetCodexLoginCommand(f func(ctx context.Context) ([]string, error))
- func (a *AuthManager) SetHTTPClient(c *http.Client)
- func (a *AuthManager) SetOpenAIAPIKey(key string) error
- func (a *AuthManager) SetOpenAICredentialCallback(f func())
- func (a *AuthManager) StartDeviceFlow(ctx context.Context, providerID string) (agent.DeviceFlowStart, error)
- func (a *AuthManager) StartOAuthCodeFlow(ctx context.Context, providerID string) (agent.DeviceFlowStart, error)
- func (a *AuthManager) SubmitAPIKey(_ context.Context, providerID, key string, metadata map[string]string) (string, error)
- func (a *AuthManager) SubmitAuthCode(ctx context.Context, providerID, flowID, code string) error
- type BackendInfo
- type BranchInfo
- type CreatePRRequest
- type CreatePRResult
- type CreateWorktreeResult
- type GitHubPullRequestInspection
- type GitHubPullRequestLaunchRequest
- type GitHubPullRequestLocator
- type GitHubRepositoryInspection
- type GitHubRepositoryLaunchResult
- type GitHubRepositoryLocator
- type HostStatus
- type MarkPRReadyResult
- type MergeResult
- type ModelInfo
- type Options
- type OverviewPR
- type OverviewPRState
- type PreviewOriginState
- type PreviewPRResult
- type PublishRequest
- type PublishResult
- type PullResult
- type PushResult
- type RemoteState
- type RemoteStatusResult
- type RepoBranchOverview
- type RepoInfo
- type RepoOrigin
- type RepoOverviewResult
- type RepoWorktreeInfo
- type RepoWorktreeRequest
- type RepoWorktreeResult
- type ResolveResult
- type ResolveStrategy
- type Service
- func (s *Service) AbortSession(ctx context.Context, id string) error
- func (s *Service) Auth() *AuthManager
- func (s *Service) ConfigOptions(ctx context.Context, bt agent.BackendType, ref agent.GitRef) ([]agent.ConfigOption, error)
- func (s *Service) CreatePR(ctx context.Context, ref agent.GitRef, req CreatePRRequest) (CreatePRResult, error)
- func (s *Service) CreateProjectFromTemplate(ctx context.Context, cloneURL, githubToken, name string) (CreateWorktreeResult, error)
- func (s *Service) CreateRepoWorktree(ctx context.Context, slug string, req RepoWorktreeRequest) (RepoWorktreeResult, error)
- func (s *Service) CreateSession(ctx context.Context, sessionID string, req agent.StartRequest) (agent.SessionBackend, agent.SessionInfo, error)
- func (s *Service) DeletePreset(id string) error
- func (s *Service) DeleteRepo(ctx context.Context, slug string) error
- func (s *Service) DeleteSessionMetadata(ctx context.Context, id string) error
- func (s *Service) DeleteWorktree(ctx context.Context, worktreeID string) error
- func (s *Service) DiscoverSessions(ctx context.Context, bt agent.BackendType, seedDir string) ([]agent.SessionSnapshot, error)
- func (s *Service) FindSessionByExternalID(ctx context.Context, externalID string) (agent.SessionInfo, error)
- func (s *Service) ForkSession(ctx context.Context, id, messageID string) (agent.SessionInfo, error)
- func (s *Service) GetSessionMetadata(ctx context.Context, id string) (agent.SessionInfo, error)
- func (s *Service) GitHub() *githubpkg.Manager
- func (s *Service) ID() string
- func (s *Service) ImportProjectFromGitHub(ctx context.Context, owner, repo, branch string) (CreateWorktreeResult, error)
- func (s *Service) Init(ctx context.Context, knownDirs func(agent.BackendType) ([]string, error)) error
- func (s *Service) InspectGitHubPullRequest(ctx context.Context, locator GitHubPullRequestLocator) (GitHubPullRequestInspection, error)
- func (s *Service) InspectGitHubRepository(ctx context.Context, locator GitHubRepositoryLocator) (GitHubRepositoryInspection, error)
- func (s *Service) LaunchGitHubPullRequest(ctx context.Context, req GitHubPullRequestLaunchRequest) (CreateWorktreeResult, error)
- func (s *Service) LaunchGitHubRepository(ctx context.Context, locator GitHubRepositoryLocator) (GitHubRepositoryLaunchResult, error)
- func (s *Service) ListAgents(ctx context.Context, bt agent.BackendType, ref agent.GitRef) ([]AgentInfo, error)
- func (s *Service) ListBackends(_ context.Context) ([]BackendInfo, error)
- func (s *Service) ListBranches(ctx context.Context, ref agent.GitRef) ([]BranchInfo, error)
- func (s *Service) ListRepos(ctx context.Context) ([]RepoInfo, error)
- func (s *Service) ListSessionMetadata(ctx context.Context) ([]agent.SessionInfo, error)
- func (s *Service) MarkPRReady(ctx context.Context, ref agent.GitRef) (MarkPRReadyResult, error)
- func (s *Service) MarkSessionRead(ctx context.Context, id string) error
- func (s *Service) MergeBranch(ctx context.Context, ref agent.GitRef, branch, commitMessage string) (MergeResult, error)
- func (s *Service) OpenAndSend(ctx context.Context, id string, opts agent.SendMessageOpts) (agent.SessionStatus, string, error)
- func (s *Service) OpenSession(ctx context.Context, id string) (agent.SessionStatus, string, error)
- func (s *Service) PendingPermissions(ctx context.Context, id string) ([]agent.PermissionData, error)
- func (s *Service) Presets(bt agent.BackendType) []presets.Preset
- func (s *Service) PreviewLogs(ctx context.Context, worktreeID, serviceName string) []byte
- func (s *Service) PreviewPR(_ context.Context, ref agent.GitRef) (PreviewPRResult, error)
- func (s *Service) PreviewStart(ctx context.Context, worktreeID, launchName string) (preview.Status, error)
- func (s *Service) PreviewStatus(ctx context.Context, worktreeID, launchName string) (preview.Status, error)
- func (s *Service) PreviewStop(_ context.Context, worktreeID, serviceName string) error
- func (s *Service) PublishToRemote(ctx context.Context, ref agent.GitRef, req PublishRequest) (PublishResult, error)
- func (s *Service) PullFromRemote(ctx context.Context, ref agent.GitRef) (PullResult, error)
- func (s *Service) PushToRemote(ctx context.Context, ref agent.GitRef) (PushResult, error)
- func (s *Service) PutPreset(p presets.Preset) error
- func (s *Service) RemoteSyncStatus(ctx context.Context, ref agent.GitRef) (RemoteStatusResult, error)
- func (s *Service) RemoveWorktree(ctx context.Context, ref agent.GitRef, branch string, force bool) error
- func (s *Service) RepoOverview(ctx context.Context, slug string, fetch bool) (RepoOverviewResult, error)
- func (s *Service) ResolveRemote(ctx context.Context, ref agent.GitRef, strategy ResolveStrategy) (ResolveResult, error)
- func (s *Service) ResolveWorktree(ctx context.Context, ref agent.GitRef, branch string) (WorktreeInfo, error)
- func (s *Service) RespondPermission(ctx context.Context, id, permissionID string, allow bool, denyMessage string) error
- func (s *Service) SearchSessionMetadata(ctx context.Context, p store.SearchParams) ([]agent.SessionInfo, error)
- func (s *Service) SendMessage(ctx context.Context, id string, opts agent.SendMessageOpts) error
- func (s *Service) Session(id string) (agent.SessionBackend, bool)
- func (s *Service) SessionMessages(ctx context.Context, id string) ([]agent.MessageData, error)
- func (s *Service) SetSessionDraft(ctx context.Context, id, draft string) error
- func (s *Service) SetSessionVisibility(ctx context.Context, id string, vis agent.SessionVisibility) error
- func (s *Service) Shutdown()
- func (s *Service) Status(_ context.Context) (HostStatus, error)
- func (s *Service) StopSession(id string) error
- func (s *Service) Subscribe() (string, <-chan agent.Event)
- func (s *Service) Templates() []Template
- func (s *Service) ToggleSessionFollowUp(ctx context.Context, id string) (agent.SessionInfo, error)
- func (s *Service) Unsubscribe(id string)
- func (s *Service) UpsertSessionMetadata(ctx context.Context, info agent.SessionInfo) error
- func (s *Service) ValidateCreateConfig(bt agent.BackendType, cfg map[string]string) error
- func (s *Service) WorktreeHasActiveSession(ctx context.Context, worktreeID string) (bool, error)
- type Template
- type WorktreeInfo
Constants ¶
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.
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.
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.
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.
const ErrorCodeHarnessNotAllowed = "harness_not_allowed"
ErrorCodeHarnessNotAllowed is the wire error code for ErrBackendNotAllowed.
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.
const MaxDiscoveredLocalReposForTest = maxDiscoveredLocalRepos
MaxDiscoveredLocalReposForTest exposes maxDiscoveredLocalRepos to the external host_test package. Test-only.
const ProviderAnthropicAPI = "anthropic-api"
ProviderAnthropicAPI is the clank provider ID for pay-per-use console.anthropic.com API keys. Passed as ANTHROPIC_API_KEY.
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.
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.
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.
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 ¶
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") // 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.
var ( // 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.
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") )
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.
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") )
var ErrBackendNotAllowed = errors.New("agent harness not allowed")
ErrBackendNotAllowed means the user has not granted Clank permission to control the requested agent harness.
ErrCodexDeviceAuthUnavailable is returned when no codex login command is wired — the codex backend isn't enabled on this host.
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.
var ErrGitHubRepositoryConnectionRequired = errors.New("connect GitHub to access this repository")
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.
var ErrInvalidAuthCode = errors.New("auth code cannot be empty")
ErrInvalidAuthCode is returned when SubmitAuthCode is called with a blank code (after trimming).
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.
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.
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.
ErrSetupTokenUnavailable is returned when the `claude` CLI is missing on PATH. Mux maps this to a 503.
var ErrUnknownFlow = errors.New("unknown flow id")
ErrUnknownFlow is returned when a status poll references a flow the manager has no record of.
var ErrUnknownProvider = errors.New("unknown auth provider")
ErrUnknownProvider is returned when a caller targets a provider this manager doesn't know how to authenticate.
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 ¶
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 ¶
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 ¶
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 ¶
func (m *ACPBackendManager) CreateBackend(ctx context.Context, inv agent.BackendInvocation) (agent.SessionBackend, error)
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 ¶
Init seeds per-dir profiles with known project dirs (host-scoped profiles start lazily on first use) and starts the reconciler.
func (*ACPBackendManager) LoginArgv ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
New creates a Service. Panics on missing BackendManagers — fast failure beats a later nil deref.
func (*Service) AbortSession ¶
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 ¶
DeletePreset removes a user preset.
func (*Service) DeleteRepo ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
GitHub returns the GitHub Connect manager, or nil when home-dir resolution failed at construction. Callers must nil-check.
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 ¶
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 ¶
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 ¶
ListSessionMetadata returns every persisted session, newest-updated first.
func (*Service) MarkPRReady ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) ToggleSessionFollowUp ¶
ToggleSessionFollowUp flips the follow_up flag and returns the new state. Does NOT bump UpdatedAt — see mutateSessionMeta.
func (*Service) Unsubscribe ¶
Unsubscribe deregisters the given subscriber and closes its channel. Idempotent.
func (*Service) UpsertSessionMetadata ¶
UpsertSessionMetadata persists the session record.
func (*Service) ValidateCreateConfig ¶
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 ¶
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 ¶
WorktreeInfo describes a single worktree managed by the Host.
Source Files
¶
- auth.go
- auth_anthropic_setup_token.go
- auth_codex_device.go
- auth_openai.go
- autocommit.go
- backends.go
- backends_acp.go
- branchcache.go
- claude_cli.go
- codex_cli.go
- commit_message.go
- create_project.go
- env_credentials.go
- errors.go
- events.go
- github_agent_env.go
- github_pr.go
- github_pr_ready.go
- github_pull_request.go
- github_pull_request_branch.go
- github_pull_request_fork.go
- github_pull_request_repo.go
- github_repository.go
- import_project.go
- keepalive.go
- notifier.go
- presetstore.go
- preview_service.go
- remote.go
- remote_autopull.go
- remote_publish.go
- remote_pull.go
- remote_push.go
- remote_resolve.go
- remote_status.go
- repos.go
- repos_delete.go
- repos_list.go
- repos_local.go
- repos_overview.go
- repos_worktree_create.go
- service.go
- sessions_meta.go
- templates.go
- types.go
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. |