target

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package target is the contract between Covey and a target-system plugin: the interfaces a plugin implements, the registry it enters itself into, and the handful of helpers it needs along the way.

It is its own module, with no dependencies beyond the standard library, for one reason: a plugin author should depend on the CONTRACT, not on Covey. The platform's own tree — Postgres, a browser driver, a wasm runtime — has no business in somebody's plugin.

A plugin is one package that calls Register in init(). Whoever links it in gets it; Covey's own binary links in what it ships, and anybody building their own Covey adds or leaves out whatever they like. The same registry is read by the control plane (webhook intake, prompt docs, UI) and by the sandbox daemon (action execution) — there is no hardcoded list anywhere.

Beyond compiled plugins, Covey can load plugins that need no rebuild at all: a declarative JSON manifest, an MCP server, or a WebAssembly module. Those live in Covey, not here, but they satisfy exactly the System interface below — broker, guard rails and recording apply identically no matter where a plugin came from.

Index

Constants

View Source
const (
	CapProbe = "probe"
	CapPoll  = "poll"
)

Optional capabilities a plugin may carry. A COMPILED plugin says so through its method set — it either has Probe or it does not, and a type assertion is the whole check. A plugin whose behaviour lives in DATA (a manifest) cannot: its Go type is the same generic engine either way, so the method is always there and the assertion always succeeds.

That difference matters at the call sites. "Can this system be probed?" decides whether the UI offers a connection test at all, and a manifest without a probe block would grow a button that can only fail. So a data-driven plugin reports its capabilities explicitly, and the call sites ask through the helpers below instead of asserting directly.

View Source
const (
	CategoryTicketing = "ticketing"     // helpdesk, service desk
	CategoryCode      = "code"          // repos, merge requests, CI
	CategoryComms     = "communication" // email, chat
	CategoryFiles     = "files"         // file shares, documents
	CategoryWeb       = "web"           // browser, web applications
	CategoryDev       = "dev"           // tools in the sandbox itself
	CategoryOther     = "other"
)

Categories for the target system store. Purely for placement in the UI — behavior never depends on them.

View Source
const BaselineRef = "covey-baseline"

BaselineRef is the git tag a checkout puts on the freshly unpacked upstream state. It is the anchor between checkout and sub-run: the sub-run reports its work as a difference to that commit, not as a difference between two status snapshots. Only that way does the work stay visible when the sub-agent commits locally in the checkout — which many projects explicitly demand in their CLAUDE.md.

Variables

This section is empty.

Functions

func CheckoutPruneNote

func CheckoutPruneNote(removed []string) string

CheckoutPruneNote turns the removal into a sentence for the checkout result. The agent has to learn about it: it may hold a path from an earlier run that no longer exists, and "no such file or directory" is a poor way to find that out.

func Client

func Client(name string, timeout time.Duration) *http.Client

Client returns the HTTP client a plugin should use for its outbound calls.

Plugins do not build their own client, and the reason is not tidiness. Covey records the requests its plugins make at the platform's edges — that is what makes "why did the agent do that" answerable hours later, and what an operator debugging somebody else's target system has to go on. A plugin with a client of its own is a plugin whose traffic is invisible.

Outside Covey (a plugin's own tests, a command-line tool) the default applies: a plain client with the given timeout. Nothing breaks, nothing is recorded.

func EmitArtifact

func EmitArtifact(ctx context.Context, a Artifact)

EmitArtifact reports an artifact to the sink in the context (no-op without a sink).

func Hint

func Hint(path, contentType string) string

Hint is the sentence that tells the agent what it can do with the file. For images the pointer to the Read tool (vision), otherwise the general one.

func MaxBytesFromEnv

func MaxBytesFromEnv(name string, defaultMB, maxMB int64) int64

MaxBytesFromEnv reads a size limit in MB from an environment variable.

Values above maxMB are clamped to maxMB instead of silently falling back to the default: whoever enters 2048 obviously wants a lot and not the preset — an 80 times smaller limit without a word would be the unfriendliest of all answers. Unreadable or non-positive input stays at the default (fail-closed; an absurdly large value would overflow when converted to bytes and would defeat the very size check). Both cases say so in the log.

func OnShutdown

func OnShutdown(fn func())

OnShutdown registers a cleanup hook (called from init() or on first use; not concurrently with the registration).

func PruneOldCheckouts

func PruneOldCheckouts(workdir, keep string) []string

PruneOldCheckouts removes the least recently used working copies under <workdir>/repos and keeps the newest keptCheckouts() ones. keep is the directory that must survive under all circumstances — the checkout just created, which at that moment is the newest anyway, but that should not be a matter of luck.

Returns the names of the removed directories, for the log and the recording: an agent finding its checkout gone should be able to read WHY somewhere.

Best effort — a checkout that cannot be removed is not worth failing the action that has already succeeded.

func Register

func Register(d Descriptor)

Register enters a target system plugin. Called from the respective subpackage's init(); the registration order is the display order.

func ReposDir

func ReposDir(workdir string) string

ReposDir is where checkouts live in an agent's home.

func SetClientFactory

func SetClientFactory(f func(name string, timeout time.Duration) *http.Client)

SetClientFactory lets the host supply the client every plugin gets. Covey calls it once at startup, before any plugin runs; nil restores the default.

func Shutdown

func Shutdown()

Shutdown runs all cleanup hooks (idempotent, in registration order).

func WithArtifactSink

func WithArtifactSink(ctx context.Context, sink func(Artifact)) context.Context

WithArtifactSink attaches a sink to the context that takes in a plugin's EmitArtifact calls. The action proxy sets it per action.

func WithSubAgent

func WithSubAgent(ctx context.Context, run SubAgentRunner) context.Context

WithSubAgent attaches the sub-agent runner to the context.

func WithWorkdir

func WithWorkdir(ctx context.Context, dir string) context.Context

WithWorkdir attaches the sandbox working directory to the context.

func Workdir

func Workdir(ctx context.Context) string

Workdir reads the sandbox working directory from the context. Empty if the action runs outside a sandbox (e.g. in Control Plane context).

Types

type Artifact

type Artifact struct {
	MIME  string
	Bytes []byte
}

Artifact is a binary side result of an action that belongs in the recording (e.g. a screenshot) but NOT in the action result handed to the runtime — otherwise it would end up in the LLM context. Plugins pass it through via EmitArtifact; the action proxy collects it separately from the returned result.

type CapabilityReporter

type CapabilityReporter interface {
	Supports(capability string) bool
}

CapabilityReporter is implemented by plugins whose optional capabilities are declared in data rather than in their method set (see above). A plugin without it is taken at its method set — the compiled case.

type Credential

type Credential struct {
	BaseURL string
	Token   string
	// CA is the PEM certificate (or chain) that signs the target system's
	// endpoint, brokered from the optional secret <system>_ca. Empty means the
	// system roots apply, which is the normal case for anything on the public
	// internet; an internal Kubernetes API server or an appliance behind a
	// company CA is the case this exists for.
	//
	// It belongs in the credential and not in an action's parameters, however
	// tempting the latter is. A parameter travels through the model's call, the
	// guard rails and the recording of every single action — the trust anchor
	// of an endpoint has nothing to do with any of them, and repeating it there
	// makes every call carry a certificate. Here it is brokered once, per call,
	// like the token beside it.
	//
	// A plugin that cannot make its own connection (a wasm module, a manifest)
	// never sees this either: the HOST builds the trust store from it. Only a
	// compiled plugin, which does its own dialling, reads the field.
	CA string
}

Credential is the brokered access to a target system: where it is, how to authenticate against it, and — where the endpoint is not signed by a publicly trusted authority — who to trust for it.

type Descriptor

type Descriptor struct {
	Name        string `json:"name"`
	Label       string `json:"label"`
	Description string `json:"description"`
	// Kind: "builtin" (compiled) or "custom" (manifest upload).
	Kind string `json:"kind"`
	// Category places the plugin in the store (constants Category…). The
	// plugin declares it itself — the UI derives its filters from the
	// categories that occur, without a list of its own. Empty = CategoryOther.
	Category string `json:"category,omitempty"`
	System   System `json:"-"`
	// NoCredentials: the system needs no brokered secrets (no
	// <name>_token/_url) — it works purely locally in the sandbox (e.g. the
	// dev plugin). ACCESS.md, activation and guard-rails still apply.
	NoCredentials bool `json:"-"`
	// CredentialsOptional: the system is usable WITHOUT a secret, and a stored
	// <name>_token only raises what it may do — e.g. an API key that lifts a
	// public rate limit (vulndb/NVD). The broker then resolves token and URL
	// best effort and grants even when nothing is stored, instead of denying.
	// The difference from NoCredentials: there the plugin never sees a
	// credential; here it sees one as soon as the organization stores one.
	CredentialsOptional bool `json:"-"`
	// BaseURLOptional: the plugin knows its target system's endpoint itself
	// (a fixed default, e.g. the Bot Framework token endpoint for Teams).
	// <name>_url is then an override for special cases, not a mandatory
	// secret — the broker does not refuse without it. A <name>_token stays
	// mandatory.
	BaseURLOptional bool `json:"-"`
	// SetupDoc is the setup guide for the UI (plain text, numbered steps).
	// Placeholders: {public_url} is replaced by the API with the configured
	// COVEY_PUBLIC_URL; <agent-slug> stays as it is and means the slug of the
	// responsible agent (the webhook endpoint accepts its ID instead).
	//
	// It describes what has to happen in the FOREIGN system — create a token,
	// hang in a trigger. What happens in Covey itself is not prose but the
	// fields below: the setup assistant acts on those, and a step it can carry
	// out itself has no business being an instruction.
	SetupDoc string `json:"setup_doc,omitempty"`
	// Env are the environment variables this plugin reads AT WORK — in the
	// sandbox, where the action proxy runs it, not in the control plane.
	//
	// It has to be declared because of where the two halves live. Operational
	// configuration (intake allowlists, size limits) is set on the control
	// plane, 12-factor style; the plugin that reads it runs somewhere else
	// entirely. A variable that does not make the journey is not merely
	// ineffective — an empty intake allowlist reads as "no restriction", so it
	// inverts into the widest setting, quietly. Covey therefore carries exactly
	// what is declared here into the sandbox.
	//
	// A plugin may only name variables in its OWN namespace: COVEY_<NAME>_…
	// with NAME being this plugin's Name, upper-cased. Anything else is
	// dropped, and that rule is what makes this safe to honour from a plugin
	// somebody else wrote — no declaration can reach COVEY_MASTER_KEY,
	// COVEY_DATABASE_URL or the daemon's own connection variables.
	//
	// Declare the name, never the value. Secrets do not belong here; they come
	// from the broker, per call.
	Env []string `json:"env,omitempty"`
	// Scopes are the access levels this plugin understands in ACCESS.md
	// (`- system: zammad scope: read,write,comment`). The plugin declares them
	// because only it knows them; the assistant offers exactly these instead of
	// letting somebody guess a word that is then silently ignored.
	//
	// Empty = the plugin does not distinguish (manifest and MCP plugins), and
	// the assistant leaves the scope out.
	Scopes []string `json:"scopes,omitempty"`
}

Descriptor is the plugin unit of a target system: metadata for the UI plus the implementation.

func All

func All() []Descriptor

All returns all registered descriptors in registration order.

func Describe

func Describe(name string) (Descriptor, bool)

Describe returns the descriptor of a compiled target system — for Control Plane decisions that need the metadata rather than the implementation (e.g. NoCredentials in the broker).

type KindWorkChecker

type KindWorkChecker interface {
	WorkChecker
	HasWorkKind(ctx context.Context, cred Credential, kind string) (bool, error)
}

KindWorkChecker refines WorkChecker for target systems with several kinds of work (e.g. GitLab: issues vs. merge request reviews). A heartbeat with nur-wenn: <system>:<kind> (say nur-wenn: gitlab:mr) calls HasWorkKind(…, kind) — that way each kind can be gated separately instead of letting both heartbeats fire off a shared boolean. Without a sub-scope (nur-wenn: <system>) HasWork still applies. An empty/unknown kind must not report less than HasWork (fail-open: when in doubt, assume there is work).

type Prober

type Prober interface {
	Probe(ctx context.Context, cred Credential) (string, error)
}

Prober is an optional plugin interface: one cheap, read-only call that shows whether the stored credentials actually work — and as whom.

It exists because "saved" and "works" are two different things, and until now the difference showed up at the first run of an agent, inside a recording, hours later. The returned string is the identity the target system reports back (a user name, a login, an address); it is displayed as is, so it should be short and recognisable.

Read-only, by contract. A probe that changes anything in the foreign system would be a poor kind of test — nobody expects a connection test to leave traces.

func Probes

func Probes(sys System) (Prober, bool)

Probes returns the system's Prober if it really can probe. Use this instead of a bare type assertion on Prober.

type SandboxFile

type SandboxFile struct {
	Path        string
	FileName    string
	ContentType string
	Bytes       int64
	Hint        string
}

SandboxFile is a file stored in the sandbox — what the action returns to the agent as its result.

func StoreFile

func StoreFile(workdir, subfolder, name string, data []byte, contentType string) (SandboxFile, error)

StoreFile writes data to <workdir>/<subfolder>/<name>.

The name comes from outside (sender, foreign system) and is pinned down to its basename — otherwise an attachment named `../../.ssh/authorized_keys` would carry out of the sandbox. If the name is already taken, a counter is appended (`rechnung-2.pdf`); only for byte-identical content does the existing path stay, so that fetching the same attachment a second time creates no copy.

func StoreStream

func StoreStream(workdir, subfolder, name string, r io.Reader, limit int64, contentType string) (SandboxFile, error)

StoreStream writes from r without holding everything in memory and aborts when limit is exceeded. For sources that stream their content instead of buffering it beforehand (GitLab uploads).

Unlike StoreFile this variant cannot detect identical content — for that it would have to read the stream in full first. A second fetch therefore creates a second file here instead of overwriting the first. That is the right order of evils: one copy too many is harmless, a silently replaced file is not.

type ScopedDocSystem

type ScopedDocSystem interface {
	PromptDocForScopes(scopes []string) string
}

ScopedDocSystem is an optional plugin interface for target systems whose prompt doc can be narrowed to the scopes granted in ACCESS.md. It pays off where a system carries procedures for several roles: GitLab describes both the developer and the QA/reviewer loop, and an agent without the merge scope dragged the reviewer part through every turn without ever being able to act on it — the doc stands in the context of EVERY turn, so what is unusable there is not paid for once but on every one.

Systems without this interface keep delivering their full PromptDoc; the store falls back to it (fail-open). A plugin that implements it has to answer the full doc for an empty scope list for the same reason — a missing entry must never silently take a capability away from an agent.

type SignatureWriter

type SignatureWriter interface {
	SignedWorkChecker
	WritesWorkSignature(subject string) bool
}

SignatureWriter says whether one of the system's OWN actions can move the work signature — the question the control plane has to answer after a run before it may write the state it now sees into the watermark.

The background is a race the signature alone cannot resolve. The signature is remembered at dispatch, i.e. BEFORE the run; a run that comments changes it itself, so the control plane has to advance it afterwards, otherwise the agent's own comment wakes it again. But "the state after the run" also contains everything that arrived from outside DURING the run — under a shared target-system identity (no bot account per role) authorship cannot tell the two apart, and a foreign comment silently absorbed into the watermark is a piece of work nobody is ever woken for again.

This interface narrows the race to the cases where the agent really wrote: did the run execute no signature-changing action at all, then every change since the dispatch comes from outside and the watermark must stay where it is. The subject is the one from ActionSubject (e.g. "gitlab:comment_external"), because that is what the recording carries.

Systems without this interface keep the old behaviour: after a successful run the watermark is advanced unconditionally.

type SignedWorkChecker

type SignedWorkChecker interface {
	WorkChecker
	// HasWorkSigned checks like HasWorkKind (kind == "" → like HasWork) and
	// additionally returns the signature of the work found.
	HasWorkSigned(ctx context.Context, cred Credential, kind string) (bool, string, error)
}

SignedWorkChecker refines WorkChecker with a signature of the work found — a short, stable description of WHAT the check responded to (with GitLab for instance "mr!9@n1234": merge request 9, newest note 1234).

The reason: a nur-wenn: condition is level-triggered. It reports work as long as the state persists — "someone else wrote last in the thread" stays true until the agent writes itself. An agent that DELIBERATELY ends a run without a comment (the feedback was an approval, there is nothing to do) would therefore be woken again in the next interval and would end up commenting only to turn off its own alarm clock.

The Control Plane therefore remembers the signature it last fired on and only fires again once it CHANGES. So the agent is still woken for every piece of news — including one it merely takes note of — but never twice for the same one. Whether feedback is work (defects) or not (approval) is thus decided by the agent, not by the gate.

An empty signature switches the suppression off: then the heartbeat fires on every level as before (fail-open).

type SubAgentRequest

type SubAgentRequest struct {
	// Dir is the working directory of the sub-run (relative to the sandbox
	// home or absolute).
	Dir string
	// Task is the work order — the sub-agent's only input.
	Task string
	// Model and MaxTurns override the run's defaults (empty/0 = default).
	Model    string
	MaxTurns int
}

SubAgentRequest is a work order for a nested runtime run that starts IN the given directory — typically a project checkout. That way the project's own Claude Code harness applies there (CLAUDE.md, .claude/agents, skills, commands), which the outer run never sees from the agent home.

type SubAgentResult

type SubAgentResult struct {
	Result         string   `json:"result"`
	ChangedFiles   []string `json:"changed_files,omitempty"`
	Deleted        []string `json:"deleted,omitempty"`
	CostUSD        float64  `json:"cost_usd,omitempty"`
	TurnsExhausted bool     `json:"turns_exhausted,omitempty"`
	Error          string   `json:"error,omitempty"`
}

SubAgentResult is the normalized result of a sub-run. ChangedFiles and Deleted are repo-relative paths and fit straight into the commit action.

type SubAgentRunner

type SubAgentRunner func(ctx context.Context, req SubAgentRequest) (SubAgentResult, error)

SubAgentRunner executes a sub-run. The daemon attaches it to the context per action — just like the workdir and the artifact sink, so that plugins can use the capability without importing the daemon.

func SubAgent

func SubAgent(ctx context.Context) SubAgentRunner

SubAgent reads the runner from the context. nil if the action runs outside a sandbox (e.g. in Control Plane context) — then there is no runtime that could be nested.

type System

type System interface {
	Name() string

	// ActionSubject maps action+params onto the guard-rail subject
	// (e.g. reply with internal=false → "zammad:reply_external").
	ActionSubject(action string, params json.RawMessage) string

	// Execute runs an agent action with brokered credentials (daemon side).
	// The credentials come from the broker per call — they are never
	// persisted.
	Execute(ctx context.Context, action string, params json.RawMessage, cred Credential) (any, error)

	// PromptDoc describes the available actions for the agent's system prompt
	// (it is compiled into the platform protocol section).
	PromptDoc() string
}

System is a connected target system. It bundles the integration surfaces (spec/13): actions and prompt docs. The webhook intake is optional (see Webhooker) — a target system that only takes in work via polling/heartbeat (e.g. GitLab) does not implement it.

func Get

func Get(name string) (System, bool)

Get returns the registered (compiled) target system for a name.

type WebhookEvent

type WebhookEvent struct {
	// DedupKey makes processing idempotent (retries by the target system).
	DedupKey string
	// CorrelationKey wakes a blocked task (e.g. "zammad:ticket:42").
	CorrelationKey string
	// Title/TaskBody describe the new backlog task, in case no blocked task
	// correlates.
	Title    string
	TaskBody string
	// ResumeInput is the resume input for a correlated task.
	ResumeInput string
	// Wake: false → the event is registered (dedup) but does not wake —
	// e.g. the echo of the agent's own reply.
	Wake bool
	// CorrelateOnly: the event only wakes an already blocked task
	// (wake-on-correlation) but creates NO new one — e.g. the merge of an MR:
	// if nobody is waiting for it, it is not work.
	CorrelateOnly bool
}

WebhookEvent is the normalized result of a webhook payload — everything the orchestrator needs for idempotency, correlation and task creation.

type Webhooker

type Webhooker interface {
	// VerifyWebhook checks the integrity of a raw webhook payload (e.g. an
	// HMAC signature). Empty secret = verification disabled (dev).
	VerifyWebhook(secret string, body []byte, header http.Header) bool

	// ParseWebhook turns the payload into the wake event for the orchestrator.
	ParseWebhook(body []byte) (WebhookEvent, error)
}

Webhooker is an optional plugin interface for target systems with an incoming webhook (Zammad, manifest plugins). The event router (httpapi.handleTargetWebhook) checks for it; a system without Webhooker answers webhook posts fail-closed with 404. Target systems that only take in work via polling leave this interface out — then there is no incoming traffic, no public URL and no webhook secret.

type WorkChecker

type WorkChecker interface {
	HasWork(ctx context.Context, cred Credential) (bool, error)
}

WorkChecker is an optional plugin interface for systems without a webhook: a cheap upfront check by the Control Plane whether there is any work at all (e.g. unread mail via IMAP). Heartbeat entries with nur-wenn: <system> only fire if HasWork returns true — otherwise the run, and with it the (expensive) agent wake, is skipped. The check runs in the Control Plane; the credential never leaves it.

func WorkChecks

func WorkChecks(sys System) (WorkChecker, bool)

WorkChecks returns the system's WorkChecker if it really can check for work up front. Use this instead of a bare type assertion on WorkChecker.

Jump to

Keyboard shortcuts

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