wrap

package
v0.0.0-...-fba244a Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package wrap implements the connector-authoring tool that turns a local CLI binary into a spawn-primitive Aileron connector.

The tool produces a complete connector source tree from one of two inputs:

  • **YAML config (MCPShell-style precise control)**: a hand-authored YAML file describing the connector's identity, the binary it wraps, the subcommands it exposes, and the parameters each subcommand takes. Use this mode when --help parsing would not produce the right shape.

  • **--help parsing (cli2mcp-style heuristic)**: invoke the named binary with `--help`, parse the subcommand list, and emit a scaffold the connector author edits. Use this for a 30-second scaffold rather than a hand-authored connector.

The output tree mirrors the v1 publisher's guide: a connector manifest.toml under `connector/`, an action.md per exposed subcommand under `actions/`, a Taskfile.yml, and the publisher's CI workflow stub.

The emitted manifest's [capabilities.spawn] block names the wrapped binary, the argv patterns the connector will call, the env keys it forwards, and the filesystem scopes it touches. Output passes cstore.ValidateManifest by construction.

See ADR-0002 for the spawn primitive, ADR-0014 for the per-platform sandbox mechanism the runtime applies, and the publisher's guide at docs/src/content/docs/guides/publishing-a-connector.md for the canonical layout of a connector source tree.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ActionFileName

func ActionFileName(s *Spec, sub SubcommandSpec) string

ActionFileName returns the local filename the action.md gets when installed under ~/.aileron/actions/. The connector's leaf segment (last path component of the FQN) is the prefix so wraps with colliding op names (e.g. two CLIs both with a "list" op) do not stomp each other.

Exported so callers outside this package — notably `aileron cli add` (#749) — can write action files using the same naming convention without re-implementing the rule.

func ActionName

func ActionName(s *Spec, sub SubcommandSpec) string

ActionName returns the registered action name for a wrapped subcommand: `<connector-leaf>-<op>` when the FQN has a leaf segment, or the bare op otherwise. The action store keys on this value, so namespacing it by connector prevents silent collisions between wraps with overlapping op names (e.g. two CLIs both exposing an "auth" op).

Action filenames already followed this convention via ActionFileName; pre-#781 the filename and the action.md's `name` field disagreed (filename namespaced, name bare), which let two wraps stomp each other in the action store's name-keyed map.

func BuildManifest

func BuildManifest(s *Spec) *cstore.Manifest

BuildManifest produces the cstore.Manifest a Spec describes. Pure function; suitable for tests that want to inspect the Spec → manifest translation without writing files.

func DetectCredentialEnvKeys

func DetectCredentialEnvKeys(helpText string) []string

DetectCredentialEnvKeys parses `--help` output for environment variables that look like credentials and returns them sorted and deduplicated. Intended for `aileron cli add` (issue #750): the introspector runs the help command through the platform sandbox, then this heuristic picks out the env keys whose shape strongly suggests a secret.

Recognized name suffixes — all matched against POSIX-shape uppercase tokens:

  • `_TOKEN` / `_API_TOKEN` / `_AUTH_TOKEN` / `_ACCESS_TOKEN`
  • `_KEY` / `_API_KEY`
  • `_SECRET` / `_CLIENT_SECRET`
  • `_PASSWORD`
  • `_PAT` (Personal Access Token, the GitHub convention)
  • `_AUTH` (broad; used by some HTTP-Auth wrappers)
  • the literal names `TOKEN`, `API_KEY`, `API_TOKEN`, `SECRET`, `PASSWORD` standing on their own (cobra environment-only docs sometimes use these bare)

The heuristic is intentionally conservative: false positives (the user is prompted for a value that isn't actually a credential and just declines) are preferable to silent misses (the user thinks they configured their CLI, then `aileron launch` fails opaque at first call). Tests pin the patterns against the validation CLIs in PrintingPress's catalog — `linear` (LINEAR_API_TOKEN), `sentry` (SENTRY_AUTH_TOKEN), `coingecko` (COINGECKO_API_KEY).

Returns nil for empty input or input with no candidates.

func Emit

func Emit(s *Spec, outDir string, force bool) error

Emit writes the connector source tree for `s` rooted at `outDir`. Files written by default:

connector/manifest.toml
actions/<op>.md  (one per Operations entry; +++-delimited TOML
                  frontmatter so the daemon's action loader accepts it)

Shared-forwarder connectors do not ship their own WASM, so the emitter no longer produces a Taskfile / release.yml / keys/README.md by default. Authors who want to publish a manifest under their own FQN (hub-distributed wrap) edit the manifest after emit and follow the publisher's guide for signing.

The emitted manifest is verified against cstore.ValidateManifest before write. An invalid Spec will cause Emit to fail rather than produce an unloadable connector.

`force` controls whether existing files are overwritten. Without it, Emit returns an error on the first file that would be replaced.

func HelpRunnerExec

func HelpRunnerExec(ctx context.Context, program string, args []string) (string, error)

HelpRunnerExec runs the program with `args` (typically `--help`) and returns combined stdout/stderr. Subject to a 5-second timeout so a hung help command doesn't block the wrap operation.

func RenderActionMD

func RenderActionMD(s *Spec, sub SubcommandSpec, m *cstore.Manifest, connectorHash string) string

RenderActionMD is the exported entry point for callers outside this package (e.g. `aileron cli add` in #749). Equivalent to the internal renderActionMD; kept as a small wrapper so the internal signature can evolve without breaking callers.

`connectorHash` may be empty for local-mode connectors (LocalStore is name-addressed, not content-addressed); the placeholder `sha256:bound-at-install` lands in the action's `[[requires.connectors]].hash` field, and the action executor's local-FQN router (#749) bypasses the hash lookup entirely.

Types

type ConnectorSpec

type ConnectorSpec struct {
	// Name is the connector's fully-qualified URI
	// (e.g. github://acme/gitcrawl).
	Name string `yaml:"name"`

	// Version is a strict SemVer string.
	Version string `yaml:"version"`

	// Publisher is the human-readable publisher shown in install
	// consent UIs.
	Publisher string `yaml:"publisher,omitempty"`
}

ConnectorSpec is the [connector] block.

type CredentialSpec

type CredentialSpec struct {
	Kind  string `yaml:"kind"`
	Scope string `yaml:"scope,omitempty"`
}

CredentialSpec maps to [capabilities.credential]. v1 supports api_key (the common case for CLI wrappers like gh, slackdump). The oauth2 kind is intentionally absent here; a connector that needs OAuth declares it directly in the manifest after generation.

type HelpRunner

type HelpRunner func(ctx context.Context, program string, args []string) (string, error)

HelpRunner runs `<program> --help` and returns the output. The production implementation is HelpRunnerExec; tests inject a fake.

type InstallResult

type InstallResult struct {
	// Hash is the canonical `sha256:<hex>` of the installed manifest
	// per cstore.ForwarderConnectorHash.
	Hash string

	// EntryDir is the daemon-side store directory holding the manifest.
	EntryDir string

	// AlreadyInstalled is true when an entry with the matching hash
	// existed before this call (idempotent reinstall).
	AlreadyInstalled bool

	// ActionFiles lists the absolute paths of the action.md files
	// written under actionsDir. One per declared operation.
	ActionFiles []string
}

InstallResult is the outcome of Install: where the manifest landed in the daemon's content-addressed store and which action files were written to the user's actions directory.

func Install

func Install(s *Spec, store *cstore.Store, actionsDir string, forwarderBytes []byte, force bool) (*InstallResult, error)

Install writes a Spec directly into the daemon's connector store (no source tree on disk) plus one action.md per operation under actionsDir. Use this when the connector author wants to immediately use the wrapped CLI without going through a publish/install cycle.

`store` is the daemon's cstore.Store; the caller wires it from the daemon binary (typically via the running daemon's HTTP API rather than direct on-disk writes — see cmd/aileron/action_wrap.go).

`actionsDir` is usually action.DefaultDir() (~/.aileron/actions). Per-op action files land at `<actionsDir>/<connector-name>-<op>.md` so multiple wraps with the same op name (e.g. "list") do not collide.

`forwarderBytes` is the daemon-embedded forwarder WASM. Passing it in lets the wrap package compute the connector hash without importing internal/sandbox/forwarder (which would create a cycle for tests that mock the wrap package).

type ParamSpec

type ParamSpec struct {
	Name        string `yaml:"name"`
	Type        string `yaml:"type"`
	Description string `yaml:"description,omitempty"`
	Required    bool   `yaml:"required,omitempty"`
}

ParamSpec describes one input the action accepts.

type ProgramSpec

type ProgramSpec struct {
	// Path is the absolute (or `~/`-anchored) filesystem path of
	// the binary.
	Path string `yaml:"path"`

	// Hash is the optional sha256: content hash. When set, the
	// runtime verifies the binary's bytes on every call.
	Hash string `yaml:"hash,omitempty"`
}

ProgramSpec names the binary the connector spawns.

type Spec

type Spec struct {
	// Connector is the identity block emitted as the manifest's
	// [connector] section.
	Connector ConnectorSpec `yaml:"connector"`

	// Program is the local binary the connector wraps. Path must be
	// absolute or `~/`-anchored. Hash is optional; when set, the
	// runtime pins the binary's bytes.
	Program ProgramSpec `yaml:"program"`

	// Subcommands is the list of operations the connector exposes.
	// Each subcommand becomes one action.md plus one entry in the
	// manifest's argv_patterns.
	Subcommands []SubcommandSpec `yaml:"subcommands"`

	// EnvPassthrough is the closed set of environment keys the
	// runtime is allowed to forward to the subprocess. Common
	// example: forwarding `GIT_AUTHOR_NAME` to a git wrapper.
	EnvPassthrough []string `yaml:"env_passthrough,omitempty"`

	// Credential, when set, declares an [capabilities.credential]
	// block. The runtime injects the resolved secret as the named
	// env keys in CredentialEnvKeys; the connector never sees the
	// bytes.
	Credential *CredentialSpec `yaml:"credential,omitempty"`

	// CredentialEnvKeys is the list of env keys the runtime injects
	// the resolved credential into when the connector spawns. Each
	// key must also appear in EnvPassthrough.
	CredentialEnvKeys []string `yaml:"credential_env_keys,omitempty"`

	// FSRead and FSWrite are the filesystem scopes the connector
	// requests. Paths must be absolute or `~/`-anchored.
	FSRead  []string `yaml:"fs_read,omitempty"`
	FSWrite []string `yaml:"fs_write,omitempty"`

	// Cwd is the optional cwd policy the runtime enforces.
	Cwd string `yaml:"cwd,omitempty"`
}

Spec is the in-memory representation the YAML loader and the --help parser produce. The emitter turns one Spec into a complete connector source tree.

The YAML schema mirrors this struct field-for-field; see LoadYAML for the entry point and emit_test.go for canonical YAML shapes.

func FromHelp

func FromHelp(ctx context.Context, runner HelpRunner, name, version, program string) (*Spec, error)

FromHelp builds a Spec by introspecting `<program> --help` and every nested subcommand's `--help`. The output captures one SubcommandSpec per *leaf* command (a command with no further subcommands), with that leaf's required + optional flags as declared inputs and an argv template that uses `[...]` optional groups for the optional flags.

For a Cobra CLI like linear-pp-cli:

linear-pp-cli                 (top-level)
└── issues                    (namespace — has Available Commands)
    ├── create                (leaf — has Flags only)   →  emits `issues-create`
    └── list                  (leaf)                     →  emits `issues-list`

Namespace commands (those that route to nested subcommands) are not emitted as operations; only leaves are callable. This matches how the agent thinks about tool calls: "create an issue", not "navigate to the issues namespace and then create."

`name` is the connector FQN to assign (`github://acme/foo` or `local://user/foo`). `version` is the connector's initial version. `program` is the absolute path of the binary to wrap. `runner` runs the help command; pass HelpRunnerExec in production.

CLIs without any subcommands fall back to a single `run` operation invoking the bare binary — same behavior as before the recursion landed, so single-purpose wrapping paths are unchanged.

func LoadYAML

func LoadYAML(path string, data []byte) (*Spec, error)

LoadYAML decodes a Spec from raw YAML bytes. Returns a structured error pointing at the source path on parse failure.

type SubcommandSpec

type SubcommandSpec struct {
	// Name is the operation name (e.g. "log", "status").
	Name string `yaml:"name"`

	// Description is a one-line human-readable summary used in the
	// generated action.md and surfaced in the LLM's tool list.
	Description string `yaml:"description,omitempty"`

	// Argv is the argv pattern the runtime allows. Tokens are
	// whitespace-separated; `{name}` placeholders accept any value
	// for the matching parameter at call time.
	Argv string `yaml:"argv"`

	// Params is the list of parameters the action exposes to the
	// agent. Each param maps to a placeholder in Argv.
	Params []ParamSpec `yaml:"params,omitempty"`
}

SubcommandSpec describes one operation the connector exposes. One SubcommandSpec produces one action.md and one entry in the manifest's argv_patterns.

func SortedSubcommands

func SortedSubcommands(s *Spec) []SubcommandSpec

SortedSubcommands returns the connector's subcommands in alphabetical order. Used in tests where iteration order on the raw slice is significant; production paths walk the original order so users see the YAML they wrote.

Jump to

Keyboard shortcuts

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