Skip to Content
View as .md

Plugins

Plugins extend Squadron with additional tools. They run as separate processes, communicate over gRPC, and can be written in Go or Python. Squadron manages installation, lifecycle, and shutdown — you just declare the plugin in HCL and use its tools.

Loading Plugins

plugin "playwright" { source = "github.com/mlund01/plugin_playwright" version = "v0.0.2" settings { headless = false browser_type = "chromium" } }
AttributeTypeDescription
sourcestringPlugin source — github.com/owner/repo for a published release, or a path inside the project for a local Go or Python package
versionstringRelease tag, or "local" for local development
settingsblockPlugin-specific configuration; passed to the plugin’s Configure (optional)

On first load Squadron downloads the matching release asset from GitHub, verifies its sha256 against checksums.txt, and caches the result. Subsequent loads reuse the cache. See Distributing Plugins for the publishing side.

Local Plugin Sources

Point source at a local Go or Python package and Squadron will auto-rebuild on every config load — no manual squadron plugin build step in the dev loop:

plugin "shell" { source = "./plugin_shell" # path inside the project version = "local" }

Rules:

  • version must be "local" when source is a local path.
  • The path is resolved by Squadron’s project-root path rule: ./foo and bare foo anchor to the HCL file’s own directory, @/foo anchors to the project root, absolute paths and .. traversals that escape the project root are rejected at config-load time.
  • The source directory must contain a go.mod (Go plugin) or a pyproject.toml (Python plugin); Squadron auto-detects which.
  • For Go: go must be on PATH. For Python: python3 must be on PATH. Build failures surface in the config-load error with the build tool’s output attached.
  • The compiled binary or installed venv lands at the same cache location as a downloaded release: .squadron/plugins/<platform>/<name>/local/.

Path Resolution

Local-source paths follow the same rule as every other path attribute in Squadron (currently packet.path too):

FormResolves to
./foo, ../foo, bare fooThe directory of the HCL file declaring this block
@/fooThe project root — the directory passed to squadron -c <dir>
/foo, absoluteRejected with absolute paths are not allowed — use a path relative to the HCL file or prefix with "@/" for the project root

After resolution the path must remain inside the project root. Any .. traversal that escapes the project tree is rejected with escapes the project root <path>, and any path that resolves to the project root itself (e.g. @/) is rejected with resolves to the project root <path> itself.

Crucially: resolution never reads the process working directory. You can run squadron -c /path/to/project mission foo from anywhere on your machine and source = "./plugin_shell" always means “the plugin_shell directory next to the HCL file declaring it” — never “next to wherever I happened to cd before running squadron.”

For remote sources (github.com/owner/repo) none of this applies — those are downloaded into the plugin cache and the GitHub path is opaque.

Build caching

Squadron hashes the source tree after each successful build and stamps the digest into runner.json (source_hash). On the next config load, if the tree still hashes the same and the entry binary is on disk, the rebuild is skipped — even for Python plugins where pip install would otherwise dominate startup. Editing any file triggers a fresh build.

The hash walk ignores VCS metadata, virtualenvs, caches, and compiled artifacts (.git/, __pycache__/, .venv/, node_modules/, *.pyc, etc.) so incidental changes don’t invalidate the cache.

Using Plugin Tools

Once loaded, plugin tools are available as plugins.<name>.<tool>:

agent "browser" { tools = [plugins.playwright.browser_navigate] } agent "browser" { tools = [plugins.playwright.all] }

Plugin Paths

Plugins are cached at .squadron/plugins/<platform>/<name>/<version>/. Each install carries a runner.json that records how to spawn the plugin:

.squadron/plugins/darwin-arm64/playwright/v0.0.2/ ├── runner.json # { "kind": "go", "entry": "plugin" } └── plugin # the Go binary
.squadron/plugins/darwin-arm64/myplug/v0.1.0/ ├── runner.json # { "kind": "python", "entry": "venv/bin/myplug" } └── venv/ # virtualenv with the plugin package installed

If runner.json is missing, Squadron falls back to executing a plugin binary in the install directory (the legacy convention).

Creating Plugins

Plugins implement four methods: Configure, Call, GetToolInfo, ListTools. Both SDKs ship a high-level decorator/generic API on top that handles schema reflection, payload validation, and result serialization for you — you just write a function.

Go

Use squadron-sdk :

package main import ( "context" "strings" squadron "github.com/mlund01/squadron-sdk" ) type EchoInput struct { Message string `json:"message" jsonschema:"required,description=The message"` AllCaps bool `json:"all_caps,omitempty"` } func main() { app := squadron.New() app.Configure(func(settings map[string]string) error { return nil }) squadron.Tool(app, "echo", "Echoes a message back", func(ctx context.Context, in EchoInput) (string, error) { if in.AllCaps { return strings.ToUpper(in.Message), nil } return in.Message, nil }) app.Serve() }

Schema is reflected from EchoInput’s jsonschema: tags. Build and install for local use:

squadron plugin build myplug ./path/to/source

The build command runs go build from the source directory and writes runner.json.

Python

Use squadron-sdk-py :

from typing import Literal from pydantic import Field from squadron_sdk import Squadron app = Squadron() @app.configure def setup(settings: dict[str, str]) -> None: app.prefix = settings.get("prefix", "") @app.tool def echo( message: str = Field(..., description="The message to echo"), all_caps: bool = False, ) -> str: """Echo a message back.""" out = message.upper() if all_caps else message return f"{app.prefix}{out}" def main(): app.serve()

Tool schemas (input and output) are reflected from your type hints by pydantic; @app.configure receives settings just like Go’s app.Configure. The plugin needs a standard pyproject.toml with one [project.scripts] entry that points at this main:

[project] name = "myplug" version = "0.1.0" dependencies = ["squadron-sdk>=0.1.1"] [project.scripts] myplug = "myplug.main:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build"

Build and install for local use:

squadron plugin build myplug ./path/to/source

The build command detects pyproject.toml, creates a virtualenv at .squadron/plugins/.../venv/, runs pip install <source>, and writes runner.json pointing at the entry script.

Splitting tools across files, including a ToolGroup from another module, and reading app state from group tools are documented in the squadron-sdk-py README .

Local Development

Two ways to iterate on a plugin:

Declarative — let HCL drive the rebuild. Point source at the plugin’s source directory and Squadron will rebuild on every config load:

plugin "myplug" { source = "./path/to/source" version = "local" }

Imperative — build once, reference by name. Run the CLI yourself, then reference the plugin without a source:

squadron plugin build myplug ./path/to/source
plugin "myplug" { version = "local" }

CLI Helpers

squadron plugin tools <name> # list tools the plugin provides squadron plugin info <name> <tool> # show input + output schemas squadron plugin call <name> <tool> '<json>' # invoke directly (useful for testing) squadron plugin build <name> <source-path> # build + install (Go or Python)
Last updated on