Skip to content

Tool provider — examples#

A tool provider block in agent.hcl#

required_providers {
  filesystem = {
    source  = "github.com/agentco/provider-filesystem"
    version = "~> 2.0.0"
  }
}

provider "filesystem" {
  allowed_roots = ["${path.root}"]
}

allowed_roots is the kind of ordinary Configure field protocol.md#configure describes as a provider's capability boundary — not a secret, but a jail root the plugin enforces internally. Resolving env(...)-style indirection for any actual secret fields (a hosted web_search provider's API key, say) follows the same kernel-side HCL/cty bridge described in model/examples.md — the plugin always receives a resolved literal value.

The wire protocol#

This is the wire protocol's service declaration and core call/event messages, in protobuf form (trimmed to the essentials):

service ToolService {
  rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse);
  rpc Configure(ConfigureRequest) returns (ConfigureResponse);
  rpc Invoke(InvokeRequest) returns (stream InvokeResponse);
  rpc Render(RenderRequest) returns (RenderResponse);
  rpc Preview(PreviewRequest) returns (PreviewResponse);
  rpc Describe(DescribeRequest) returns (DescribeResponse);
}

message ToolCall {
  string id = 1;
  string tool_name = 2;
  google.protobuf.Struct arguments = 3;
  pluggableharness.common.v1.CallContext call_context = 4;
}

message PreviewRequest {
  ToolCall call = 1;
}

message PreviewResponse {
  pluggableharness.render.v1.RenderTree preview = 1;
}

message DescribeRequest {}

message DescribeResponse {
  pluggableharness.common.v1.ProducerRef producer = 1;
}

message ToolEvent {
  oneof event {
    OutputChunk output_chunk = 1;
    Progress progress = 2;
    PartialResult partial_result = 3;
    ExitStatus exit_status = 4;
    ToolResult result = 5;
    ToolError error = 6;
  }

  message OutputChunk {
    OutputStream stream = 1;
    bytes data = 2;
  }

  message Progress {
    string message = 1;
    optional double fraction_complete = 2;
  }

  message PartialResult {
    google.protobuf.Struct payload = 1;
  }

  message ExitStatus {
    int32 exit_code = 1;
    optional string signal = 2;
  }
}

message ToolResult {
  google.protobuf.Struct payload = 1;
}

message ConcurrencySpec {
  bool safe = 1;
  repeated string key_fields = 2;
}

InvokeRequest/InvokeResponse are thin per-RPC envelopes ({ call = 1; } / { event = 1; }) around ToolCall/ToolEvent — see data-types.md#toolcall--toolevent--toolresult.

A full Invoke event sequence#

A bash tool call that runs a test suite, streams live output, then reports a non-zero exit and a structured result, expressed as the oneof ToolEvent.event variants:

→ InvokeRequest{
    call: {
      id: "tc_42",
      tool_name: "bash",
      arguments: {"command": "go test ./..."},
      call_context: {session_id: "01J...", turn_id: "01J...", working_directory: "/home/steven/code/aiagent"},
    }
  }

← InvokeResponse{event: {progress:      {message: "starting process"}}}
← InvokeResponse{event: {output_chunk:  {stream: STDOUT, data: "ok  \tgithub.com/pluggableharness/agent/foo\t0.4s\n"}}}
← InvokeResponse{event: {output_chunk:  {stream: STDOUT, data: "FAIL\tgithub.com/pluggableharness/agent/bar\t0.2s\n"}}}
← InvokeResponse{event: {output_chunk:  {stream: STDERR, data: "--- FAIL: TestBar (0.00s)\n"}}}
← InvokeResponse{event: {exit_status:   {exit_code: 1}}}
← InvokeResponse{event: {result:        {payload: {"exit_code": 1, "summary": "1 package failed"}}}}

exit_status and result are distinct events here precisely because the tool does post-processing (building the summary field) after the child process itself has already exited — see protocol.md#invoke. Note this call's result carries a successful ToolEvent (the tool ran to completion and reports what happened) even though the tested code failed — a non-zero exit from the underlying operation is ordinary execution_failed-shaped content inside result.payload, not a terminal ToolError; see conformance.md#error-taxonomy.

If the kernel cancels the stream mid-flight (user hit Ctrl-C while bash was still running), the plugin sees the gRPC stream close, sends SIGTERM to the child process, and — per protocol.md#invoke — MUST make a best effort to report what already happened before closing:

← InvokeResponse{event: {output_chunk: {stream: STDOUT, data: "ok  \tgithub.com/pluggableharness/agent/foo\t0.4s\n"}}}
← InvokeResponse{event: {partial_result: {payload: {"note": "process received SIGTERM; output above is valid"}}}}

— no terminal result or error follows; the kernel's own bookkeeping records the call as cancelled once the stream closes without one, per conformance.md#error-taxonomy's cancelled category.