clients

package
v0.0.0-...-4f7fa88 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package clients is the extension point of the install core: one adapter per supported client, reached through a Registry that the composition root injects. Generic packages (providers, planner, adapters/clientdetect) depend on this contract and never on a concrete client package.

Capabilities are segregated: an adapter implements only the interfaces its client actually has, and a dispatcher asks for one with As[T]. Because that lookup is a runtime type assertion, every adapter is expected to carry compile-time assertions (`var _ clients.Lifecycle = (*Adapter)(nil)`) for the capabilities it declares, and contracttest verifies the declaration against the implementation.

The package is close to self-contained but not a leaf: Env carries a nativeconfig.Kernel, which pulls github.com/tailscale/hujson and adapters/atomicfile. That is a known, accepted exception - see docs/ARCHITECTURE.md.

Index

Constants

This section is empty.

Variables

View Source
var DefaultSelectionLayout = SelectionLayout{File: "mcp.json", Nested: true}

DefaultSelectionLayout is the managed MCP selection document every client uses unless it implements SelectionReader.

View Source
var ErrOpenCodeAdapterUnavailable = errors.New("host_adapter_unavailable")
View Source
var ErrRegistryRequired = errors.New("client registry is required")

ErrRegistryRequired is what a dispatcher returns when it was constructed without a registry. A nil registry is never resolved to a default containing every client: that would compile all client adapters into any binary that imports a generic package, and would make the registry a global the composition root no longer controls.

Functions

func As

func As[T any](r *Registry, id domain.ClientID) (T, bool)

As is the capability lookup: it reports the adapter for id when that adapter implements T. A client that does not implement the capability is the normal case, not an error - the dispatcher decides what the absence means.

func CanonicalDirectory

func CanonicalDirectory(path string, lstat func(string) (fs.FileInfo, error), eval func(string) (string, error)) (string, error)

CanonicalDirectory resolves existing symlink components and retains a missing suffix without creating it. A dangling link, file or unreadable ancestor is an error, never permission to select a different profile. All observations use the injected probes; synthetic hosts never read the ambient filesystem.

func DesiredOpenCodeCodec

func DesiredOpenCodeCodec(host domain.OpenCodeHostAuthority) (nativeconfig.Codec, error)

DesiredOpenCodeCodec reads only the immutable prepared profile. Missing or unknown authority never selects a default dialect for desired effects.

func OpenCodeNativeRequirements

func OpenCodeNativeRequirements(envelope domain.PackageEnvelope) (bool, []string)

func PlannedOpenCodeNativeRequirements

func PlannedOpenCodeNativeRequirements(envelope domain.PackageEnvelope, plan domain.DeliveryPlan) (bool, []string, error)

PlannedOpenCodeNativeRequirements validates only selected effects. Historical unsupported components remain available to the selection/removal policy, but must not become desired native capability requirements or rendered objects.

func SelectLocalDelivery

func SelectLocalDelivery(plan *domain.DeliveryPlan, facts domain.LocalDeliveryFacts) error

SelectLocalDelivery is the narrow PlanRefiner seam for the future Local adapter. NewLocalDelivery copies constructor inputs; generic core consumes frozen effective facts rather than adding ClientID or delivery-mode branches. No Local adapter is registered by this preparatory contract.

Types

type ActivationPreflighter

type ActivationPreflighter interface {
	PreflightActivation(env Env, req domain.ActivationRequest) error
}

ActivationPreflighter rejects a request before anything is written, so a missing client CLI or an unusable native config fails the operation instead of leaving a half-activated install behind.

type ActiveNativeProjector

type ActiveNativeProjector interface {
	ProjectActiveNative(context.Context, string, domain.PackageEnvelope, domain.DeliveryPlan, string) ([]domain.NativeObjectOwnership, error)
}

ActiveNativeProjector reconstructs the desired native ownership from an already committed package without writing into its tree. The stager verifies that tree before invoking this client-owned capability.

type Adapter

type Adapter interface {
	ID() domain.ClientID
}

Adapter is the only mandatory part of the contract. Identity metadata is not duplicated here: Definition and traits stay in domain, so Directory validation and the CLI can read them without building a registry.

type AutomaticActivator

type AutomaticActivator interface {
	AutomaticallyActivates(env Env, req domain.ActivationRequest) bool
}

AutomaticActivator reports whether Activate will drive a managed client CLI for this exact request. Runtime preflight reads the same predicate so the two cannot drift.

type CompatibilityLimiter

type CompatibilityLimiter interface {
	ClientLimitations(envelope domain.PackageEnvelope) []string
	ComponentLimitations(envelope domain.PackageEnvelope, item domain.ComponentDecision) (reject []string, note []string)
}

CompatibilityLimiter reports the client-specific limitations shown by the read-only compatibility view.

type Detection

type Detection struct {
	// Err prevents an invalid selected profile from becoming mutation authority.
	Err            error
	ConfigRoot     string
	ExecutablePath string
	Surfaces       []domain.ClientSurface
	// SelectionSurfaceIDs narrows the surfaces that decide detection status to
	// the ones that also make the client safe to act on. An empty list means any
	// detected surface counts. Claude is the only client that needs it today: a
	// configuration directory stays evidence, but only the CLI selects it.
	SelectionSurfaceIDs []string
}

Detection is the raw outcome of probing one client's surfaces.

type Env

type Env struct {
	OpenCodeTransitions ports.OpenCodeTransitionRecorder
	Runner              ports.CommandRunner
	NativeConfig        nativeconfig.Kernel
	Paths               ports.PathPolicy
	Launcher            StdioLauncherDeliverer
	Now                 func() time.Time
}

Env carries the infrastructure an adapter is allowed to use. It is passed in by the dispatcher rather than owned by the adapter, so a client package never reaches for a process, a clock or the filesystem policy on its own.

type Host

type Host interface {
	HomeDir() string
	WorkingDir() string
	CanonicalDirectory(path string) (string, error)
	GOOS() string
	Env(name string) string
	SystemApplicationsDir() string
	WindowsProgramFiles() []string
	LinuxApplicationDirs() []string

	// LookPath returns the resolved executable path, or "" when the binary is
	// not on PATH. It never reports the lookup error: absence is the answer.
	LookPath(binary string) string
	Lstat(path string) (fs.FileInfo, error)
	ReadDir(path string) ([]os.DirEntry, error)

	BinarySurface(id, binary string) domain.ClientSurface
	// ResolvedBinarySurface reports the same surface for an executable the
	// adapter already resolved, so a client that also returns that path as its
	// ExecutablePath probes PATH once instead of twice.
	ResolvedBinarySurface(id, executablePath string) domain.ClientSurface
	DirectorySurface(id, path string) domain.ClientSurface
	AppSurface(id, appName string) domain.ClientSurface
	WindowsAppSurface(id, userRelativePath, systemRelativePath string) domain.ClientSurface
	LinuxDesktopSurface(id string, filenames ...string) domain.ClientSurface
	ExtensionSurface(id, root string, extensionIDs ...string) domain.ClientSurface

	XDGConfigRoot(name string) string
	EditorChannelConfigRoot(channel string) string
	VSCodeConfigRoot() string
}

Host is the probing environment handed to an adapter. The surface constructors stay here rather than in each client package: they encode how evidence strings are produced, which is a cross-client output contract.

func NewHost

func NewHost(probes HostProbes) Host

NewHost builds the probing environment handed to an adapter.

type HostDetector

type HostDetector interface {
	DetectSurfaces(host Host) Detection
}

HostDetector is the read-only surface probe of one client. It returns what it observed; assembling a domain.DetectedClient (status, display name, version probe) stays generic in adapters/clientdetect.

type HostProbes

type HostProbes struct {
	HomeDir               string
	WorkingDir            string
	GOOS                  string
	Environment           map[string]string
	SystemApplicationsDir string
	WindowsProgramFiles   []string
	LinuxApplicationDirs  []string

	EvalSymlinks func(string) (string, error)
	LookPath     func(name string) (string, error)
	Lstat        func(path string) (fs.FileInfo, error)
	ReadDir      func(path string) ([]os.DirEntry, error)
}

HostProbes is the raw environment a Host is built from: the values an adapter may read and the three filesystem probes it observes through. The surface constructors are deliberately not part of it - they translate a probe result into an evidence string, which is a cross-client output contract and stays in one implementation shared by the detector and by contracttest.

type Lifecycle

type Lifecycle interface {
	Activate(ctx context.Context, env Env, req domain.ActivationRequest) (domain.ActivationOutcome, error)
	Deactivate(ctx context.Context, env Env, req domain.DeactivationRequest) (domain.DeactivationOutcome, error)
}

Lifecycle activates and deactivates one client. The generic dispatcher keeps the invariants that hold for every client (plan and delivery agree on the client, the active path is a contained real directory) and delegates the rest here. A client without Lifecycle is activated manually by the user.

type LocalPreparationAuthorizer

type LocalPreparationAuthorizer interface {
	AuthorizeLocalPreparation(in PlanInput, plan *domain.DeliveryPlan) (authorized bool, err error)
}

LocalPreparationAuthorizer lets a client accept a personal, user-supplied registration receipt in place of the pinned catalog compatibility evidence the generic pipeline otherwise requires. Reporting true means the client took responsibility for the decision and the catalog step is skipped.

type NativeRegistryLayout

type NativeRegistryLayout interface {
	NativeRegistry(in PlanInput) (root, executable string)
}

NativeRegistryLayout overrides which native client registry a delivery is recorded against. VS Code is installed through the Copilot CLI it shares a backend with, so its plan points at the sibling's locators rather than its own. The default is the client's own configuration root and executable.

type OpenCodeHostProfileConsumer

type OpenCodeHostProfileConsumer interface {
	UsesOpenCodeHostProfile() bool
	OwnedOpenCodeNativeRequirements([]domain.NativeObjectOwnership) (skills, config bool)
}

OpenCodeHostProfileConsumer opts the native adapter into explicit host preparation. The generic installer dispatches through this client capability rather than branching on an identity or constructing the default registry.

type PhysicalProfileAuthority

type PhysicalProfileAuthority = ports.PhysicalProfileAuthority

PhysicalProfileAuthority is the single optional port, shared with the planner.

type PlanInput

type PlanInput struct {
	PreviousNativeObjects []domain.NativeObjectOwnership `json:"-"`
	LocalEntryObservation *domain.LocalEntryObservation  `json:"-"`
	Envelope              domain.PackageEnvelope
	Client                domain.DetectedClient
	Detected              map[domain.ClientID]domain.DetectedClient
	Intent                domain.InstallIntent
}

PlanInput is everything a refiner is allowed to see.

func (PlanInput) BackendSibling

func (in PlanInput) BackendSibling() (domain.DetectedClient, bool)

BackendSibling returns the entry of the other client sharing this client's backend family, for example Copilot behind VS Code. The entry comes back exactly as the detection map holds it, without a DetectionStatus filter: the decisions built on it read ExecutablePath, not status.

type PlanPrecondition

type PlanPrecondition interface {
	CheckPlanPrecondition(in PlanInput, plan *domain.DeliveryPlan) error
}

PlanPrecondition rejects a package the client cannot take at all, before a target is resolved for it. An implementation marks the plan unsupported and records why; the planner returns the plan as it stands, so the rejection never carries resolved paths. Returning an error aborts planning instead.

type PlanQualifier

type PlanQualifier interface {
	QualifyPlan(in PlanInput, plan *domain.DeliveryPlan) error
}

PlanQualifier applies the client's own admission rules once the components, the catalog verdict and the package diagnostics are on the plan, and before the generic pipeline decides that nothing usable is left. A client that can reject a package on its own terms does it here, so its warning keeps its place ahead of the generic ones.

type PlanRefiner

type PlanRefiner interface {
	RefinePlan(ctx context.Context, in PlanInput, plan *domain.DeliveryPlan) error
}

PlanRefiner applies client-specific decisions on top of a viable generic plan: readiness promotions, user actions and warnings. It runs last, on a plan the generic pipeline has already accepted.

A refiner may not change the plan identity (ClientID, Scope, PhysicalArtifactID, TargetRoot, ActivePath) and may not promote a plan the generic pipeline already marked unsupported.

type PreparationRefiner

type PreparationRefiner interface {
	RefinePreparation(plan *domain.DeliveryPlan) error
}

PreparationRefiner turns a viable plan into a prepared one for the clients that support the prepare install intent. It is a separate stage from RefinePlan because the intent is also applied on its own, to a plan that was already built, when a caller changes its mind about a target.

type PreparedRegistryInspector

type PreparedRegistryInspector interface {
	InspectPreparedRegistry(plan domain.DeliveryPlan, name string, owned bool) (RegistryFinding, error)
}

PreparedRegistryInspector overrides how the prepared package directory is inspected. Without it the generic default shared.InspectUnqualifiedPluginRoot applies.

type ProfileBindingValidator

type ProfileBindingValidator interface {
	ValidateBindingProfile(root string, binding domain.ClientBinding) error
}

ProfileBindingValidator checks persisted profile authority before lifecycle work, including removal paths that do not inspect a native registry.

type ProfileResolver

type ProfileResolver interface {
	ResolveProfileRoot(root string) (string, error)
}

ProfileResolver normalizes an explicit profile at a composition boundary. Implementations must reject ambiguous input before resolving symlink aliases.

type ProjectionInput

type ProjectionInput struct {
	StagingPath    string
	Envelope       domain.PackageEnvelope
	Plan           domain.DeliveryPlan
	Hints          domain.CompatibilityHints
	PluginDataPath string
	Launcher       StdioLauncherDeliverer
}

ProjectionInput is everything a projector is allowed to see. StagingPath is the tree to write into; Plan.ActivePath is the future location the projection has to encode, and the two are deliberately different.

func (ProjectionInput) DeliverLauncher

func (in ProjectionInput) DeliverLauncher() func(string) error

DeliverLauncher returns the injected launcher copy function, or nil when the composition root did not supply one. Projectors must use this instead of calling Launcher.Deliver directly: a typed-nil *managedstdio.Source inside the interface would panic.

type Projector

type Projector interface {
	Project(ctx context.Context, in ProjectionInput) ([]domain.NativeObjectOwnership, error)
}

Projector writes the client-specific projection into an already sanitized staging tree and returns the native objects it now owns. The generic stager owns the snapshot copy, the sanitizing pass and the digest; a projector only adds what its client needs to read the package.

type ReadOnlyVerifier

type ReadOnlyVerifier interface {
	VerifierAvailable(client domain.DetectedClient, plan domain.DeliveryPlan, backendExecutable string) bool
}

ReadOnlyVerifier reports whether this client can be verified without writing, which is what lets a read-only reconciliation trust its own observation.

type Registry

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

Registry resolves a client id to its adapter. It is built once in the composition root (and explicitly in tests) and injected into every generic dispatcher.

func NewRegistry

func NewRegistry(adapters ...Adapter) (*Registry, error)

NewRegistry rejects a duplicate id and an id that domain does not define, so a typo cannot silently produce a registry that is missing a client.

func (*Registry) All

func (r *Registry) All() []Adapter

All returns the registered adapters in domain.ClientDefinitions order, so any listing built on the registry keeps the stable user-facing order.

func (*Registry) Lookup

func (r *Registry) Lookup(id domain.ClientID) (Adapter, bool)

Lookup reports the adapter registered for id.

type RegistryFinding

type RegistryFinding uint8

RegistryFinding is what an identity inspection concluded about a native client registry. Indeterminate is not a failure: it means the observation could not be trusted, and the caller must not turn it into evidence of absence.

const (
	// RegistryClear means no competing package claims this identity.
	RegistryClear RegistryFinding = iota
	// RegistryExpected means the identity is claimed by the managed package.
	RegistryExpected
	// RegistryCollision means a foreign package already claims the identity.
	RegistryCollision
	// RegistryIndeterminate means the registry could not be observed.
	RegistryIndeterminate
)

type RegistryInspector

type RegistryInspector interface {
	InspectNativeRegistry(ctx context.Context, env Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (RegistryFinding, error)
	// UsesNativeRegistryExecutable reports whether the inspection runs the
	// client executable. Only those clients count as having attempted a native
	// observation when the executable is missing.
	UsesNativeRegistryExecutable() bool
}

RegistryInspector reads the client's own registry to decide whether the managed package identity is free, already ours, or taken.

type SelectionLayout

type SelectionLayout struct {
	File   string
	Nested bool
}

SelectionLayout says where the managed MCP selection document lives inside a delivered package and whether its servers sit under an "mcpServers" member.

type SelectionReader

type SelectionReader interface {
	ManagedMCPSelection() SelectionLayout
}

SelectionReader overrides the managed MCP selection layout. Without it the generic default is DefaultSelectionLayout.

type StagingLayout

type StagingLayout interface {
	StagingBase(plan domain.DeliveryPlan) string
	ValidateTargetLayout(plan domain.DeliveryPlan) error
}

StagingLayout overrides where the transaction staging directory is created and what the generic path validation additionally requires. Clients that stage directly under the plan target root do not implement it; the stager falls back to shared.DefaultStagingLayout.

type StdioLauncherDeliverer

type StdioLauncherDeliverer interface {
	Deliver(root string) error
}

StdioLauncherDeliverer copies the managed stdio launcher into a staged tree. It is an interface rather than *managedstdio.Source so that the contract does not depend on the launcher implementation.

type TargetLayout

type TargetLayout interface {
	TargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string) (anchor, root string, err error)
}

TargetLayout overrides where a client's managed package lives. Clients that install under the managed root do not implement it; the planner falls back to shared.ManagedTargetRoot.

type VersionProbeEnvironment

type VersionProbeEnvironment interface {
	VersionProbeEnvironment(configRoot string) ([]string, error)
}

VersionProbeEnvironment pins a native version probe to the selected profile. The detector still controls the isolated cwd, timeout and output limit.

Directories

Path Synopsis
Package all assembles the registry of every client adapter shipped in this repository.
Package all assembles the registry of every client adapter shipped in this repository.
Package chatgpt is the client adapter for the ChatGPT desktop application.
Package chatgpt is the client adapter for the ChatGPT desktop application.
Package claude is the client adapter for Claude Code.
Package claude is the client adapter for Claude Code.
Package cline is the client adapter for Cline.
Package cline is the client adapter for Cline.
Package codex is the client adapter for the OpenAI Codex CLI.
Package codex is the client adapter for the OpenAI Codex CLI.
Package contracttest holds the executable contract every client adapter has to satisfy, in-tree and out-of-tree alike.
Package contracttest holds the executable contract every client adapter has to satisfy, in-tree and out-of-tree alike.
Package copilot is the client adapter for the GitHub Copilot CLI.
Package copilot is the client adapter for the GitHub Copilot CLI.
Package cursor is the client adapter for the Cursor editor.
Package cursor is the client adapter for the Cursor editor.
Package gemini is the client adapter for the Gemini CLI.
Package gemini is the client adapter for the Gemini CLI.
Package grok integrates Grok Build's native plugin directory and CLI.
Package grok integrates Grok Build's native plugin directory and CLI.
internal
exampleclient
Package exampleclient is a test-only adapter used to prove that generic dispatchers accept a registry they did not assemble.
Package exampleclient is a test-only adapter used to prove that generic dispatchers accept a registry they did not assemble.
Package kimi projects portable skills and MCP into Kimi Code's user plugin registry.
Package kimi projects portable skills and MCP into Kimi Code's user plugin registry.
Package kiro is the client adapter for Kiro.
Package kiro is the client adapter for Kiro.
Package opencode is the client adapter for OpenCode.
Package opencode is the client adapter for OpenCode.
Package shared holds the helpers more than one client adapter needs.
Package shared holds the helpers more than one client adapter needs.
Package vscode is the client adapter for Visual Studio Code.
Package vscode is the client adapter for Visual Studio Code.
Package windsurf is the client adapter for Windsurf and Devin, which share one product identity across several installable channels.
Package windsurf is the client adapter for Windsurf and Devin, which share one product identity across several installable channels.

Jump to

Keyboard shortcuts

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