Tool Authoring and Lifecycle
This page shows how to define, test, bind, publish, and consume a Golem tool. A tool is stateless metadata plus handlers. Its identity is scoped to the owning agent and tool name, and each component-tool invocation runs in a fresh isolated instance under the owner’s execution.
Define a tool
The SDKs project the same model: a command tree, inherited global options and flags, command bodies, typed results, declared errors, and optional stdin/stdout/stderr byte streams.
TypeScript
import { z } from 'zod';
import { ok, toolDefinition } from '@golemcloud/golem-ts-sdk';
export const files = toolDefinition('files')
.version('1.0.0')
.global('verbose', z.boolean(), { kind: 'flag', short: 'v' })
.command('read', command => command.body(body => body
.positional('path', z.string())
.stdout({ mime: ['application/octet-stream'], required: false })
.stderr({ mime: ['text/plain'], required: false })
.returns(z.string())
.error('not-found', { kind: 'runtime', exitCode: 2 })
));
files.implement({
read: async ({ path, verbose }) => ok(verbose ? `read:${path}` : path),
});Model commands precisely
Prefer structure over parsing strings yourself:
- Commands and subcommands describe mutually exclusive operations. A command can also have an implicit body for bare invocation.
- Globals are options or flags inherited by every descendant command.
- Positionals are ordered; only the final tail positional can be variadic.
- Options and flags carry long names, aliases, defaults, repetition, delimiters, environment fallback, and documentation.
- Constraints express requirements, all-or-none groups, implications, and exclusions.
- Results and errors are typed. Declared errors include a stable name, category, optional payload, and exit code; transport or protocol failures remain RPC errors.
- stdin, stdout, and stderr are independent byte-stream slots with declared MIME metadata; each declared slot can be required or optional. They use backpressured streams rather than buffering a complete body. Stderr is data, not an implicit failed result.
The command tree is metadata, not a spawned shell. A component cannot currently run an arbitrary native executable. Implement operations with code and libraries compiled into the component, or call an external service through supported network APIs.
On a command line, an enum value is matched without regard to letter case when that names exactly one case, so literal selects a case named Literal. Generated --help lists the positional arguments of a command with their summaries, and shows the possible values of an argument or option whose value is an enum.
Give a tool the caller’s directory
A tool is not a child process of its caller. It does not inherit a working directory or an environment; it receives only its input record. To start where the caller is, declare an option for the directory and name the PWD variable on it:
#[arg(cwd = "option", env = "PWD", default = "/")].option("cwd", z.string(), { default: "/", env: "PWD" })An option or flag that the command line leaves out, and that declares an environment variable, takes the value of that variable when the caller supplies variables. The built-in Bash tool does: a tool called from a script or from a golem ssh session gets the shell’s exported variables, and PWD is always the directory the shell is in. A value on the command line still wins, and the default applies when the caller supplies no such variable. From a variable, a boolean flag reads 1, true, yes, on, y or t as set and 0, false, no, off, n or f as not set, in any letter case, and refuses other text; a count flag reads a number. A value that comes from a variable counts as given, also for the command’s constraints. Resolve relative path arguments against this option inside the tool.
Callers that are not a shell supply no variables: golem tool invoke, generated typed clients and MCP. For them the option is an ordinary input that they pass or leave at its default.
Call through a typed client
Definition-owned clients preserve the command tree and schemas. TypeScript uses definition.client(), Effect uses Tool.client(definition), and the Rust, Scala, and MoonBit code generators project equivalent clients. For a tool implemented in another component or language, add it to bridge generation:
bridge:
ts:
internal:
tools: [files]
rust:
internal:
tools: [files]Generated clients call the canonical name through tool-rpc; they do not embed a component ID, MCP endpoint, host-native implementation ID, or credential.
Drain output attachments concurrently
Commands may declare neither output, stdout only, stderr only, or both. A started invocation exposes each declared output independently from its structured result. When both outputs are present, drain them concurrently: waiting for one channel to finish before reading the other can apply backpressure and stall the provider. A structured success can contain stderr bytes, and a structured failure can still have output attachments that must be drained or cancelled.
Output stream state is durable. Golem records each channel under a distinct identity, checkpoints delivery separately, and reconstructs the same stdout/stderr separation after suspension or executor restart. Middleware must forward, consume, or cancel each attachment independently and must not relabel one channel as the other.
Add middleware
Use monomorphic middleware when the wrapper knows the tool shape and may adapt it. Use universal middleware for schema-agnostic, shape-preserving policy. Universal middleware can wrap selected tools or every tool in an environment; monomorphic middleware must be installed on tool bindings, not the environment-wide list. Middleware receives only an invocation-scoped handle to the next layer.
import { ToolInvokeError } from '@golemcloud/golem-ts-sdk';
export const safeFiles = files.middleware({
name: 'safe-files',
implementation: {
read: async ({ path, ...input }, { underlying }) => {
if (path.startsWith('/etc/')) {
throw new ToolInvokeError({ tag: 'constraint-violation', val: 'path denied' });
}
return underlying.read({ path, ...input });
},
},
});Effect exports typed and universal middleware from @golemcloud/effect-golem/middleware. Rust, Scala, and MoonBit expose the same runtime middleware contract through their SDK authoring forms. Binding and traversal are separate: registering middleware in a component does not attach it to any call.
Declare and bind
A local declaration identifies the component that implements the tool. A binding exposes it to a component’s agents, then an agent entry can override, replace, or remove inherited settings:
tools:
files:
component: acme:file-tools
config:
root: /workspace
components:
acme:assistant:
dependencies:
tools: [files]
tools:
files:
configKeysReadable: [workspace.root]
agents:
ReadOnlyAssistant:
tools:
files:
filesystemAccess: denied
middleware:
- name: audit
parameters: { channel: security }Bindings expose a tool; permission cards authorize calls. Component bindings provide defaults that agent entries can override, replace, or remove. Readable configuration and secret scopes intersect across retained layers and between environment and owner bindings; revealable secrets are also limited to readable secrets. Filesystem access uses the last explicit cascade value, with denial taking precedence when environment and owner bindings are combined. toolsMergeMode can upsert, replace, or remove inherited bindings. Middleware order is environment-wide middleware, then the merged per-tool list (universal or monomorphic), then the leaf tool; middlewareMergeMode is prepend (the default), append, or replace and changes only the per-tool list.
The example assumes that the audit middleware is declared separately under tools.middleware.
See Application Manifest for every field, merge rule, and validation mode.
Publish and consume
Publish a local tool from an environment deployment:
toolReleases:
production:
files: {}The published version comes from the tool’s discovered metadata. The Effect SDK currently emits tool metadata version 0.1.0; its toolDefinition does not expose a version option. The 1.0.0 consumer below therefore applies to the TypeScript, Rust, Scala, and MoonBit definitions shown above.
A consumer selects an exact release and supplies its own configuration:
tools:
files:
release:
account: tools@example.com
name: files
version: "1.0.0"
config: { root: /workspace }Deployment reconciles the environment’s release grant before reading metadata or generating clients. Publication is additive: removing toolReleases does not de-publish. Existing deployment snapshots stay pinned. Use golem tool release list|get|de-publish|restore and golem tool grant create|list|get|delete|restore for explicit lifecycle operations.
A release grant permits consumption of an artifact. It does not bind the tool to an agent and is not a runtime permission-card grant.
Wrap MCP or a CLI
- MCP: configure an MCP import in the environment. Golem discovers and projects its tools into the registry; generated clients still call
tool-rpc. Resolve naming collisions explicitly—application and ambient names cannot collide, ambient tools take precedence over MCP projections, and earlier MCP imports precede later ones. - CLI-shaped operation: reproduce the required operation with libraries compiled into the component, or call an external service. Represent arguments as command metadata, stream bytes through stdin/stdout/stderr, and map failures to declared errors. Spawning an arbitrary native CLI inside a component is not currently supported.
Test the whole contract
- Unit-test metadata validation, handler results, declared errors, constraints, all four stdout/stderr presence combinations, and per-channel cancellation/backpressure.
- Test middleware independently with a fake underlying handler, including rejection and transformed input/output.
- Build the component and run an integration call through
tool-rpc; unit tests cannot prove discovery, instance isolation, runtime authorization, or middleware traversal. - Deploy with the intended binding and permission cards. Test both missing-binding and missing-permission cases.
- For durable middleware, restart while a call or approval is pending and verify logical recovery without repeating recorded effects. External effects still require an appropriate idempotency policy across an ambiguous crash window.
Build and deploy with the normal component workflow. Tool and middleware publication is committed with deployment, but release de-publication and grant administration are explicit lifecycle operations.