shell

package
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package shell implements an MCP source that executes shell commands. Connect decodes the source's `connect:` map (working_dir, timeout, max_output_bytes, shell, env) and returns a single run_command tool which spawns `sh -c <command>` and returns the captured output.

This is a thin drop-in replacement for the shell tooling built into MCP hosts (notably Zed): the LLM writes shell syntax and gets back stdout, stderr, and exit code. The surrounding guardrails — working- directory confinement, explicit env, hard timeout, output cap with truncation, and honest DestructiveHint=true annotations — are the safety surface the rest of this meta-server provides.

Security model: this source can execute arbitrary code. The MVP trusts the operator (same trust model as the postgres source for SQL) and the LLM (same trust model as Zed's built-in shell tool). The guardrails narrow the blast radius but do not eliminate it. Operators who want a stricter model should pair this source with a host that gates tool invocation on DestructiveHint.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Connect

func Connect(
	_ context.Context,
	connect map[string]any,
	_ ...tool.Option,
) (tool.Response, error)

Connect decodes the source's `connect:` map, validates it, and returns the run_command tool. The tool advertises ReadOnlyHint=false, DestructiveHint=true, IdempotentHint=false, OpenWorldHint=false — host implementations that gate tool invocation on these hints will treat shell correctly as a write/destructive, local-scoped capability.

After validation, Connect canonicalizes connect.working_dir via filepath.EvalSymlinks and opens an os.Root at the canonical path. Both the root and the canonical path are captured in the handler closure so per-call directory overrides can be validated against the same root every invocation, regardless of whether the LLM passes the symlinked or canonical form of any path inside it.

func RunBefore

func RunBefore(ctx context.Context, cmds []BeforeCommand, opts ...tool.Option)

RunBefore spawns every command in cmds as a long-lived background process under the supplied context, then returns only after every declared Healthcheck has finished — passed, timed out, or canceled.

The contract for failures is intentionally lenient: a failed spawn, a non-zero process exit, a timed-out healthcheck, or a canceled context all log at ERROR (or DEBUG for in-progress healthcheck retries) without aborting the function. RunBefore itself returns nothing; its caller (server-start in cmd/main.go) proceeds to source.Apply regardless of outcomes.

A nil or empty cmds slice is a no-op: no goroutines are started and RunBefore returns immediately.

Spawned children are tied to the supplied context: when the server context cancels (SIGINT/SIGTERM via signal.NotifyContext), every running `sh -c` is killed via the underlying exec.CommandContext machinery, and every in-flight healthcheck dial is canceled.

RunBefore.Wait tracks ONLY healthcheck goroutines. Spawned commands are fire-and-forget — their lifetime is the server context, not RunBefore's wait, because in the typical case they are long-running daemons (kubectl port-forward, etc.) that the operator intends to outlive the wait.

Types

type BeforeCommand

type BeforeCommand struct {
	Command     string         `yaml:"command"               json:"command"`
	Healthcheck *Healthcheck   `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
	Restart     *RestartPolicy `yaml:"restart,omitempty"     json:"restart,omitempty"`
}

BeforeCommand spawns a single sh -c process at server start.

When Restart is non-nil, a non-signal-error exit triggers an automatic respawn of the same command, with the configured Delay between attempts. Restart semantics:

  • Restart omitted (Restart == nil): spawn exactly once, current behavior preserved.
  • Restart.MaxAttempts == 0 (the Go zero value, also the YAML default when the block is present but empty): restart forever until ctx cancels or the command exits cleanly.
  • Restart.MaxAttempts == 1: equivalent to omitting Restart.
  • Restart.MaxAttempts == N (N >= 2): up to N total spawn attempts, then ERROR "restart budget exhausted".

type Duration

type Duration time.Duration

Duration is a time.Duration that unmarshals from a human-readable string (e.g. "30s", "1m30s") in both YAML and JSON configs. Numbers and other non-string values are rejected at decode time — there is no implicit stringification path.

func (Duration) Duration

func (d Duration) Duration() time.Duration

Duration returns the underlying time.Duration value.

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for Duration.

func (*Duration) UnmarshalYAML

func (d *Duration) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML implements yaml.Unmarshaler for Duration.

type Healthcheck

type Healthcheck struct {
	TCP      string   `yaml:"tcp"                json:"tcp"`
	Interval Duration `yaml:"interval,omitempty" json:"interval,omitempty"`
	Timeout  Duration `yaml:"timeout,omitempty"  json:"timeout,omitempty"`
}

Healthcheck is the per-BeforeCommand readiness probe. TCP is required and accepts the standard "host:port" form. Interval is the per-attempt deadline and the sleep between attempts; Timeout is the overall budget. The two defaults (1s and 30s) are applied when the corresponding field is the zero Duration.

type RestartPolicy

type RestartPolicy struct {
	MaxAttempts int      `yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"`
	Delay       Duration `yaml:"delay,omitempty"        json:"delay,omitempty"`
}

RestartPolicy configures auto-respawn behavior for a BeforeCommand that exits non-zero during the server lifetime. The zero value (MaxAttempts == 0) means "restart forever" — this is the natural default for long-lived tunnels like kubectl port-forward. Set MaxAttempts to a positive N to cap the total number of spawn attempts; on exhaustion, runBeforeOne logs an ERROR and stops.

Delay is the wait between attempts. The zero value uses the defaultRestartDelay constant. The inter-attempt wait honors ctx.Done(), so SIGINT/SIGTERM cancel the loop promptly.

type Result

type Result struct {
	Stdout     string `json:"stdout"`
	Stderr     string `json:"stderr"`
	ExitCode   int    `json:"exit_code"`
	DurationMS int64  `json:"duration_ms"`
	Truncated  bool   `json:"truncated"`
}

Result is the captured outcome of one shell invocation.

Jump to

Keyboard shortcuts

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