adapter

package
v0.77.2 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrTargetNotFound = errors.New("adapter target not found")
	// ErrTargetDegraded means an adapter could not safely establish physical
	// ownership. Supervisors must retry without mutating durable registry
	// bookkeeping because the uncertainty may be transient.
	ErrTargetDegraded = errors.New("adapter target ownership degraded")
	// ErrInjectUncertain means text already landed and Enter/submit failed.
	// App.Run prints this to stderr so inject-via can map it without a typed
	// error crossing the child process boundary. Never replay the payload.
	ErrInjectUncertain = errors.New("AMQ_INJECT_PROGRESS=uncertain")
	// ErrGUIAdapterNotReady keeps GUI seats out of the generic wake path until
	// their capability gate has live evidence. A refusal is not a weaker
	// delivery claim.
	ErrGUIAdapterNotReady = errors.New("GUI wake adapter is not ready")
)
View Source
var (
	ErrTargetNotRegular = errors.New("file adapter target must be a regular file")
	ErrTargetSymlink    = errors.New("file adapter target must not be a symlink")
)

Functions

func CmuxDegradedOwnershipKey added in v0.59.1

func CmuxDegradedOwnershipKey(inventory TargetInventory, err error) (string, bool)

CmuxDegradedOwnershipKey returns a physical key only when the error came from the same concrete cmux inventory type used for the candidate lookup. This prevents generic or third-party TargetInventory implementations from turning ErrTargetDegraded into permission to skip an uncertain owner.

Types

type Activation added in v0.70.0

type Activation int // none < launch < foreground

Activation is how strongly a seat can bring its surface to the user.

const (
	ActivationNone Activation = iota
	ActivationLaunch
	ActivationForeground
)

type Adapter

type Adapter interface {
	Name() string
	Probe(ctx context.Context, target string) error
	Inject(ctx context.Context, target string, payload string) error
}

type Capability added in v0.70.0

type Capability struct {
	Activation    Activation
	Delivery      Delivery
	Session       SessionScope
	RequiresHuman bool
}

Capability models the wake capability vector from docs/adr-wake-capability-vector.md. Each seat advertises the strongest delivery it can honestly claim; callers request a minimum. Weaker is refused, never substituted (ADR §"refusal over substitution").

func UnknownCapability added in v0.70.0

func UnknownCapability() Capability

UnknownCapability is the vector assumed for an adapter that does not implement CapabilityDeclarer: weakest on every ordered axis (Activation/Delivery/Session all None) and most-restrictive on the tolerance axis (RequiresHuman true). An undeclared adapter is therefore refused unless the caller explicitly tolerates a human-required seat, so an adapter that forgets to declare a Capability can never masquerade as an unattended full-strength seat. This is worst-case on the tolerance axis, not zero-value: the zero-value Capability has RequiresHuman=false, which would let an unknown adapter pass the default zero minimum as if it delivered unattended.

func (Capability) Satisfies added in v0.70.0

func (c Capability) Satisfies(min Capability) bool

Satisfies reports whether this seat is at least as strong as the caller's minimum on every axis. Weaker is REFUSED, never substituted (ADR §"refusal over substitution"). A caller that needs an unattended wake (min.RequiresHuman == false) must refuse a requires-human seat; a caller that accepts a human handoff (min.RequiresHuman == true) accepts either.

type CapabilityDeclarer added in v0.70.0

type CapabilityDeclarer interface {
	Capability() Capability
}

CapabilityDeclarer lets an adapter publish its honest delivery vector. An adapter that does not implement it is treated as UnknownCapability() (weakest on every ordered axis and requires a human), so it is refused unless the caller explicitly tolerates a human-required seat. This keeps an adapter that forgets to declare a Capability from masquerading as an unattended full-strength seat.

type ClaudeDesktop added in v0.70.0

type ClaudeDesktop struct {
	Runner CommandRunner
}

ClaudeDesktop implements the honest deep-link seat from the wake capability ADR: prefilled + new + requires_human. `claude://code/new?q=<prompt>` PREFILLS the composer on the Code surface and never auto-submits; a human send is always required (verified live on Claude Desktop 1.34493.1). This adapter only launches the deep-link; it does not and cannot submit.

func (ClaudeDesktop) Capability added in v0.70.0

func (ClaudeDesktop) Capability() Capability

Capability declares the proven seat vector: the deep-link launches the app (ActivationLaunch), prefills the composer without submitting (DeliveryPrefilled), always targets a new session (SessionNew), and a human must press send (RequiresHuman).

func (ClaudeDesktop) Discover added in v0.70.0

func (c ClaudeDesktop) Discover(ctx context.Context) (string, error)

Discover returns the single new-session seat. It is deliberately reachable so an explicit `attach --adapter claude-desktop --target claude-desktop:new` can name it; the human-handoff gate is enforced at registration (CapabilityDeclarer), not here. Discovery does not auto-open the app.

func (ClaudeDesktop) Inject added in v0.70.0

func (c ClaudeDesktop) Inject(ctx context.Context, target string, payload string) error

Inject launches the prefill-only deep-link. The payload becomes a properly query-escaped `q` parameter; the resulting URL is passed as a single argument to `open`. The prompt text never enters AppleScript/osascript or any script source — it exists only as the single query-escaped `open` argument. There is no System Events/keystroke/clipboard/activate, and crucially no submit — this seat never sends Enter.

Identity is revalidated before the write: `open` is scheme-bound, not identity-bound (it dispatches to whatever app owns claude:// at call time), so Probe runs here, not only at Discover/registration. On any identity mismatch or command failure Inject fails closed without emitting the open.

func (ClaudeDesktop) Name added in v0.70.0

func (ClaudeDesktop) Name() string

func (ClaudeDesktop) NormalizeTarget added in v0.70.0

func (ClaudeDesktop) NormalizeTarget(target string) (string, error)

func (ClaudeDesktop) Probe added in v0.70.0

func (c ClaudeDesktop) Probe(ctx context.Context, target string) error

Probe verifies the Claude Desktop app identity before blessing the target. It resolves the actual default handler of the `claude://` scheme and asserts it is the pinned bundle id com.anthropic.claudefordesktop — the same app that `open claude://...` will dispatch to at write time. A mismatch, a missing handler, a non-darwin platform, or any command failure fails closed.

type ClaudePrint added in v0.71.0

type ClaudePrint struct {
	Runner   CommandRunner
	LookPath func(string) (string, error)
	Spawner  processSpawner
	// Lock takes an exclusive non-blocking lock on path. Tests swap in an
	// in-process locker because flock/fcntl are per-process. Production is
	// flock LOCK_EX|LOCK_NB.
	Lock func(path string) (unlock func(), err error)
	// ConfigDir is CLAUDE_CONFIG_DIR (default ~/.claude). Ambient, not part of
	// injector identity (docs/wake-lifecycle.md §9.4).
	ConfigDir string
	StateDir  string
	AckWait   time.Duration
	Now       func() time.Time
}

ClaudePrint is the honest submitted seat into an existing Claude Code session. It spawns `claude -p --resume <uuid>` with stream-json replay ack, never raises a GUI (ActivationNone), and never claims the turn finished — the first matching isReplay user event is submitted.

Claude CLI does not enforce a one-owner mutex (a second -p --resume appends to the same jsonl). AMQ takes flock on inject.lock before the owner scan. Probe refuses unreadable/malformed/non-positive/pid-mismatched owner files, or a matching uuid whose filename pid is alive. Stale pid files are ignored, never deleted.

func (ClaudePrint) Capability added in v0.71.0

func (ClaudePrint) Capability() Capability

func (ClaudePrint) CapabilityForTarget added in v0.71.0

func (c ClaudePrint) CapabilityForTarget(target string) (Capability, error)

func (ClaudePrint) Inject added in v0.71.0

func (c ClaudePrint) Inject(ctx context.Context, target string, payload string) error

func (ClaudePrint) Name added in v0.71.0

func (ClaudePrint) Name() string

func (ClaudePrint) NormalizeTarget added in v0.71.0

func (ClaudePrint) NormalizeTarget(target string) (string, error)

func (ClaudePrint) Probe added in v0.71.0

func (c ClaudePrint) Probe(ctx context.Context, target string) error

func (ClaudePrint) ReportsProviderAcceptance added in v0.77.0

func (ClaudePrint) ReportsProviderAcceptance()

ReportsProviderAcceptance marks Claude Print as an application-level acceptance reporter. Inject waits for the matching isReplay user event in the Claude stream before returning nil.

type Cmux

type Cmux struct {
	Runner       CommandRunner
	Path         string
	Getenv       func(string) string
	LookPath     func(string) (string, error)
	UserHomeDir  func() (string, error)
	IsExecutable func(string) bool
	Sleep        func(context.Context, time.Duration) error
	SettleDelay  time.Duration
	// LiveTTYOwnerCount reports how many live (non-zombie) processes hold the
	// device at devPath as their controlling terminal. Defaults to the darwin
	// sysctl implementation; tests inject a fake so they never run a real
	// sysctl against fixture tty names.
	LiveTTYOwnerCount func(devPath string) (int, error)
	// Logf receives non-fatal diagnostics (evictions, degraded fail-closed
	// ttys). Nil is a no-op.
	Logf func(format string, args ...any)
	// contains filtered or unexported fields
}

func (Cmux) Capability added in v0.70.0

func (Cmux) Capability() Capability

Capability declares the full-strength TTY seat on delivery and session: the adapter submits the payload (text + Enter) into an exact existing cmux surface with no human in the loop. Activation is None — Inject sends text and Enter via cmux RPC (surface.send_text / surface.send_key) WITHOUT raising/focusing the surface.

func (Cmux) Discover

func (c Cmux) Discover(_ context.Context) (string, error)

func (Cmux) Inject

func (c Cmux) Inject(ctx context.Context, target string, payload string) error

func (Cmux) Inventory

func (c Cmux) Inventory(ctx context.Context, _ OwnershipContext) (TargetInventory, error)

func (Cmux) Name

func (Cmux) Name() string

func (Cmux) NormalizeTarget

func (Cmux) NormalizeTarget(target string) (string, error)

func (Cmux) Probe

func (c Cmux) Probe(ctx context.Context, target string) error

func (Cmux) RememberOwnership added in v0.65.3

func (c Cmux) RememberOwnership(target, key string)

RememberOwnership seeds the last-known physical key for a surface. The supervisor loads keys persisted on registry entries so a restart still refuses UUID→TTY drift. Empty keys are ignored.

func (Cmux) WithOwnershipRecord added in v0.65.3

func (c Cmux) WithOwnershipRecord() Cmux

WithOwnershipRecord returns a copy whose last-key map survives Inventory rebuilds. Production DefaultRegistry already includes one; tests that exercise UUID→TTY drift across supervise ticks must use this so RememberOwnership can seed keys persisted on a registration.

type CodexApp added in v0.70.0

type CodexApp struct {
	Runner CommandRunner
}

CodexApp implements the honest deep-link seat for the Codex app (com.openai.codex): `codex://threads/new?prompt=<text>` prefills a NEW thread, and `codex://threads/<uuid>?prompt=<text>` opens the EXACT EXISTING conversation <uuid> and prefills its composer. Neither auto-submits; a human send is always required (verified live on app 26.818.61809). This adapter only launches the deep-link; it does not and cannot submit.

Dispatch-vs-delivery caveat (live finding, issue #640): `open` exits 0 once the OS dispatches the URL to the registered handler — that proves DISPATCH only, not delivery. The app can refuse the deep-link with no adapter-visible signal: e.g. a thread with an ACTIVE WRITER shows an "Error creating chat — thread <uuid> already has an active writer" toast and leaves the composer empty, yet `open` still exits 0 and Inject reports success. Therefore the codex-app:thread:<uuid> target must name an IDLE conversation (no active writer), and callers must treat Inject success as "launched, not confirmed delivered". The adapter cannot observe the in-app refusal because the Codex app is AX-opaque (Chromium content is not exposed to System Events).

The native `execute javascript` Apple Events path is DEAD and stays dead: live smoke on app 26.818.61809 returned `Access not allowed (-1723)`, and writing browser.allow_javascript_apple_events=true into the profile Preferences did not unlock it (the pref survives quit but the runtime check is compiled out). Do not re-try that path; the finding is recorded at https://github.com/avivsinai/agent-message-queue/issues/640#issuecomment-5406484999. The task-0 probe script in scripts/probe-codex-app-execute-javascript.sh remains as historical evidence and is intentionally not invoked here.

func (CodexApp) CapabilityForTarget added in v0.70.0

func (CodexApp) CapabilityForTarget(target string) (Capability, error)

CapabilityForTarget declares the per-target vector this deep-link seat can honestly claim. Both targets are launch + prefilled + requires_human; they differ only on session scope: codex-app:new is SessionNew, and codex-app:thread:<uuid> is SessionExistingExact (it opens a specific existing conversation). A caller requesting an existing-exact session is therefore refused the new-only target rather than silently downgraded.

func (CodexApp) Discover added in v0.70.0

func (c CodexApp) Discover(ctx context.Context) (string, error)

Discover returns the new-thread seat (the only target that does not require a pre-existing conversation id). It is deliberately reachable so an explicit `attach --adapter codex-app --target codex-app:new` can name it; the human-handoff gate is enforced at registration, not here. Discovery does not auto-open the app.

func (CodexApp) Inject added in v0.70.0

func (c CodexApp) Inject(ctx context.Context, target string, payload string) error

Inject launches the prefill-only deep-link. The payload becomes a properly query-escaped `prompt` parameter; the resulting URL is passed as a single argument to `open`. The prompt text never enters AppleScript/osascript or any script source — it exists only as the single query-escaped `open` argument. There is no System Events/keystroke/clipboard/activate, and crucially no submit — this seat never sends Enter.

Identity is revalidated before the write: `open` is scheme-bound, not identity-bound (it dispatches to whatever app owns codex:// at call time), so Probe runs here, not only at Discover/registration. On any identity mismatch or command failure Inject fails closed without emitting the open.

func (CodexApp) Name added in v0.70.0

func (CodexApp) Name() string

func (CodexApp) NormalizeTarget added in v0.70.0

func (CodexApp) NormalizeTarget(target string) (string, error)

func (CodexApp) Probe added in v0.70.0

func (c CodexApp) Probe(ctx context.Context, target string) error

Probe verifies the Codex app identity before blessing the target. It resolves the actual default handler of the `codex://` scheme and asserts it is the pinned bundle id com.openai.codex — the same app that `open codex://...` will dispatch to at write time. (The app also registers http/https; only the codex scheme is pinned.) A mismatch, a missing handler, a non-darwin platform, or any command failure fails closed.

type CodexQueue added in v0.71.0

type CodexQueue struct {
	Runner            CommandRunner
	LookPath          func(string) (string, error)
	InspectWriterLock WriterLockInspector
	Home              string
}

CodexQueue implements the honest submitted seat into a live Codex GUI/TUI thread. `codex queue --thread <uuid> --message <text>` calls the app-server JSON-RPC `thread/queue/add`; the EXISTING writer (GUI app-server / TUI / codex-acp) drains it. The adapter never raises or focuses the app (ActivationNone) and never claims that the turn finished — exit 0 proves enqueue to the writer, not completion.

Queue cannot create a thread, so the only target is `codex-queue:thread:<uuid>`. An idle thread (no holder of thread-writer-locks/<uuid>.lock) is ErrTargetDegraded; deferred delivery for that case is out of scope.

func (CodexQueue) Capability added in v0.71.0

func (CodexQueue) Capability() Capability

Capability is the honest submitted vector. Queue enqueues to the thread's existing writer; it never raises/focuses the app.

func (CodexQueue) CapabilityForTarget added in v0.71.0

func (c CodexQueue) CapabilityForTarget(target string) (Capability, error)

func (CodexQueue) Inject added in v0.71.0

func (c CodexQueue) Inject(ctx context.Context, target string, payload string) error

func (CodexQueue) Name added in v0.71.0

func (CodexQueue) Name() string

func (CodexQueue) NormalizeTarget added in v0.71.0

func (CodexQueue) NormalizeTarget(target string) (string, error)

func (CodexQueue) Probe added in v0.71.0

func (c CodexQueue) Probe(ctx context.Context, target string) error

func (CodexQueue) ReportsProviderAcceptance added in v0.77.0

func (CodexQueue) ReportsProviderAcceptance()

ReportsProviderAcceptance marks Codex Queue as an application-level acceptance reporter. Inject returns nil only after the queue command accepts the message.

type CommandRunner

type CommandRunner interface {
	Run(ctx context.Context, name string, args ...string) ([]byte, error)
}

type Delivery added in v0.70.0

type Delivery int // none < prefilled < submitted

Delivery is how much of the prompt reaches the composer.

const (
	DeliveryNone Delivery = iota
	DeliveryPrefilled
	DeliverySubmitted
)

type Discoverer

type Discoverer interface {
	Discover(ctx context.Context) (string, error)
}

type ExecRunner

type ExecRunner struct{}

func (ExecRunner) Run

func (ExecRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error)

type File

type File struct{}

func (File) Capability added in v0.70.0

func (File) Capability() Capability

Capability declares the file seat as full-strength on delivery and session: Inject appends the payload (with a trailing newline) to an exact, resolved target path with no human in the loop. Activation is None — appending to a file path does not foreground or raise any surface.

func (File) Inject

func (File) Inject(ctx context.Context, target string, payload string) error

func (File) Name

func (File) Name() string

func (File) NormalizeTarget

func (File) NormalizeTarget(target string) (string, error)

NormalizeTarget resolves a file target to its stable absolute pathname. Registration persists this value, so a later launchd invocation cannot reinterpret a relative target from a different working directory. Resolving existing symlinks also keeps lexical and symlink aliases from claiming the same destination independently.

func (File) Probe

func (File) Probe(ctx context.Context, target string) error

type Ghostty

type Ghostty struct {
	Runner CommandRunner
}

func (Ghostty) Capability added in v0.70.0

func (Ghostty) Capability() Capability

Capability declares the full-strength TTY seat on delivery and session: the adapter submits the payload (text + Enter) into an exact existing terminal surface with no human in the loop. Activation is None because Inject types into a pinned terminal id WITHOUT raising/focusing it (`activate`/`AXRaise` are banned in adapter scripts); it does not claim to bring the surface to the foreground.

func (Ghostty) Discover

func (g Ghostty) Discover(ctx context.Context) (string, error)

func (Ghostty) Inject

func (g Ghostty) Inject(ctx context.Context, target string, payload string) error

func (Ghostty) Name

func (Ghostty) Name() string

func (Ghostty) NormalizeTarget

func (Ghostty) NormalizeTarget(target string) (string, error)

func (Ghostty) Probe

func (g Ghostty) Probe(ctx context.Context, target string) error

type InventoryProvider

type InventoryProvider interface {
	Inventory(ctx context.Context, own OwnershipContext) (TargetInventory, error)
}

InventoryProvider lets the supervisor inventory an adapter once per pass instead of spawning one probe process for every registry entry. The OwnershipContext lets the registration preflight pass a trusted-live candidate; other callers pass the zero value.

type OwnershipContext

type OwnershipContext struct {
	// TrustedTarget is the adapter-native target string (e.g. a cmux surface
	// target) the caller has proven live. Empty means no trusted candidate.
	TrustedTarget string
}

OwnershipContext carries optional trust the caller has already established about a candidate target. Only the registration preflight may populate it: the target it is registering was discovered from the live local surface, so it is live by construction and can break an otherwise-ambiguous physical ownership tie. Supervisor and inject passes pass the zero value, which trusts nothing.

type ProviderAcceptanceReporter added in v0.77.0

type ProviderAcceptanceReporter interface {
	ReportsProviderAcceptance()
}

ProviderAcceptanceReporter is an opt-in marker for adapters whose Inject implementation observes an application-level acceptance receipt. It is deliberately separate from Capability.DeliverySubmitted: a submitted TTY or file write does not prove that the provider accepted the payload.

type Registry

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

func DefaultRegistry

func DefaultRegistry() Registry

func DefaultRegistryWithLogf

func DefaultRegistryWithLogf(logf func(format string, args ...any)) Registry

DefaultRegistryWithLogf returns the production adapter set with non-fatal adapter diagnostics wired to logf. Callers that do not own a diagnostic stream can continue using DefaultRegistry.

func NewRegistry

func NewRegistry(adapters ...Adapter) Registry

func (Registry) Get

func (r Registry) Get(name string) (Adapter, error)

type SessionScope added in v0.70.0

type SessionScope int // none < new < existing-exact

SessionScope is how precisely the seat addresses an existing surface.

const (
	SessionNone SessionScope = iota
	SessionNew
	SessionExistingExact
)

type TargetCapabilityDeclarer added in v0.70.0

type TargetCapabilityDeclarer interface {
	CapabilityForTarget(target string) (Capability, error)
}

TargetCapabilityDeclarer is implemented by adapters whose capability vector depends on the resolved target (for example, a deep-link adapter that can prefill either a new session or an exact existing conversation). When an adapter implements this, the registration gate prefers CapabilityForTarget(target) over the target-blind Capability() so a caller requesting an existing-exact session is refused a new-only target rather than silently downgraded. An error from CapabilityForTarget fails closed.

type TargetInventory

type TargetInventory interface {
	Probe(target string) error
	OwnershipKey(target string) (string, error)
}

TargetInventory is a point-in-time existence snapshot. Implementations must return ErrTargetNotFound only when absence is proven by the snapshot; parse, transport, and permission failures remain ambiguous errors. When ownership is degraded, callers remain fail-closed unless a concrete adapter-specific capability proves that the same immutable inventory also established a different physical identity. The generic interface deliberately exposes no constructor for such a capability.

type TargetNormalizer

type TargetNormalizer interface {
	NormalizeTarget(target string) (string, error)
}

type WriterLockInspector added in v0.71.0

type WriterLockInspector interface {
	Held(ctx context.Context, path string) (bool, error)
}

WriterLockInspector reports whether a Codex thread-writer lock is held WITHOUT acquiring it. The idle-thread case is an untested follow-up; this seat only claims submitted delivery when a writer already holds the lock. Native inspection errors fail closed (no lsof fallback: an open file is not a held flock).

Jump to

Keyboard shortcuts

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