ZeroRoot Docs

Tools

Build a stateless capability, define a typed input/output contract, ship the binary, and have your agents call it.

A tool is a stateless capability with a fixed input/output contract. Agents call tools to do well-defined work. Examples are port scans, HTTP fingerprinting, CVE lookups, and AST parsers. A tool fits anything where the contract is "given X, return Y".

Tools differ from plugins in two ways:

ToolPlugin
StateStateless. Each call is independent.Stateful. Holds connections, sessions, caches.
LifetimeShort-lived per call.Long-running process.
CredentialsNone of its own, agents pass any data the tool needs.Manifest-bound credentials, retrieved at runtime.
Who calls itAgents.Tools (and rarely, mission nodes directly).

If your capability must hold a Shodan API key or keep a database connection, you want a plugin. If it is "give me the open ports on 1.2.3.4," you want a tool.

Tools are typed

Every tool has a proto-defined input and output. The contract is explicit and discoverable. Agents know exactly what they can pass in and what they get back. They do not need to read your implementation.

A tool declares:

  • a fully-qualified input message type (e.g. myorg.portscan.v1.Request)
  • a fully-qualified output message type (e.g. myorg.portscan.v1.Response)
  • an ExecuteProto(ctx, input) → output function

Gibson registers your message schemas at registration time. Any agent in your tenant can then marshal calls to your tool dynamically. No shared import is required.

Auto-populating the knowledge graph

Your tool's response proto can reserve field number 100 for a gibson.graphrag.v1.DiscoveryResult. If it does, Gibson ingests anything you put in that field into your tenant's knowledge graph during the mission run. You do not write graph code yourself.

syntax = "proto3";
package myorg.portscan.v1;

import "gibson/graphrag/v1/graphrag.proto";

message Response {
  // Tool-specific fields use 1..99.
  repeated PortScanRow rows = 1;

  // Reserved for the standard discovery container, auto-ingested into
  // the knowledge graph. Optional, but highly recommended.
  gibson.graphrag.v1.DiscoveryResult discovery = 100;
}

DiscoveryResult carries the standard entity types: host, port, service, endpoint, domain, subdomain, technology, certificate, finding, and evidence. It also carries custom_node and explicit_relationship for shapes that the standard taxonomy does not cover. See Taxonomy for the full list.

A tool that does not populate field 100 does not auto-populate the graph. That is fine for tools whose output is purely tool-specific.

Build a tool

1. Define your proto

In a fresh module:

mkdir my-portscan && cd my-portscan
go mod init example.com/my-portscan
go get github.com/zeroroot-ai/sdk/tool@latest

Define proto/portscan.proto:

syntax = "proto3";
package myorg.portscan.v1;

option go_package = "example.com/my-portscan/api/portscan/v1;portscanv1";

import "gibson/graphrag/v1/graphrag.proto";

message Request {
  string host = 1;
  string ports = 2;   // e.g. "1-1024"
}

message Response {
  repeated Row rows = 1;
  gibson.graphrag.v1.DiscoveryResult discovery = 100;
}

message Row {
  uint32 port = 1;
  string state = 2;
  string banner = 3;
}

Compile with protoc (or your preferred buf workflow). Output goes into api/portscan/v1/.

2. Implement the tool

package main

import (
    "context"
    "log"

    "github.com/zeroroot-ai/sdk/tool"
    "google.golang.org/protobuf/proto"

    portscanv1 "example.com/my-portscan/api/portscan/v1"
    graphragv1 "github.com/zeroroot-ai/sdk/api/proto/gibson/graphrag/v1"
)

type portScan struct{}

func (portScan) Name() string              { return "port-scan" }
func (portScan) Version() string           { return "0.1.0" }
func (portScan) Description() string       { return "TCP connect-scan a host." }
func (portScan) Tags() []string            { return []string{"recon", "network"} }
func (portScan) InputMessageType() string  { return "myorg.portscan.v1.Request" }
func (portScan) OutputMessageType() string { return "myorg.portscan.v1.Response" }

func (p portScan) ExecuteProto(ctx context.Context, in proto.Message) (proto.Message, error) {
    req := in.(*portscanv1.Request)
    out := &portscanv1.Response{}

    // ... do the scan, populate out.Rows ...

    // Optional: populate field 100 so the knowledge graph picks up
    // hosts/ports/services automatically.
    out.Discovery = &graphragv1.DiscoveryResult{
        Hosts: []*graphragv1.Host{{Address: req.Host}},
        Ports: portsFromRows(out.Rows, req.Host),
    }
    return out, nil
}

func (portScan) Health(ctx context.Context) tool.HealthStatus {
    return tool.HealthStatus{State: tool.Healthy}
}

func main() {
    if err := tool.Serve(context.Background(), portScan{}); err != nil {
        log.Fatal(err)
    }
}

3. Register the tool

Enroll a machine identity to mint a single-use bootstrap token. Then exchange the token for a runtime credential. There is no client secret:

gibson agent enroll --name port-scan --kind tool
gibson component register --token <bootstrap-token>

Run register from the tool's component directory. It reads the kind and name from component.yaml. See Install.

4. Run

go run .

The tool connects to your tenant, registers its proto schema, and waits for invocations. Leave it running.

Calling a tool from an agent

import portscanv1 "example.com/my-portscan/api/portscan/v1"

req := &portscanv1.Request{Host: "10.0.0.1", Ports: "1-1024"}
out, err := h.CallToolProto(ctx, "port-scan", req)
if err != nil {
    return agent.NewErrorResult(err), nil
}
resp := out.(*portscanv1.Response)

For tools that emit incrementally (long scans, fuzzers), use CallToolProtoStream. For fire-and-forget work whose results you collect later, use QueueToolWork + ToolResults.

When the tool responds, Gibson:

  1. Returns the typed response to your agent.
  2. If the tool populated field 100, ingests the DiscoveryResult into the knowledge graph. It also emits the appropriate discovered.* events to the mission stream.

Authorization

Your tenant's Permissions model controls whether a given agent can call a given tool. See Roles & permissions. The dashboard's deploy wizard sets sensible defaults. You can tighten or loosen them later.

If an agent calls a tool without permission, the call returns a structured error. The mission also emits a node.finished with the denial reason.

Versioning a tool

A tool's Version() string carries its version. Two strategies:

  • Side-by-side: enroll port-scan@0.2.0 alongside port-scan@0.1.0. Let agents pin to a specific version. Retire the old one when no agent references it.
  • Rolling: re-enroll the same name. The latest registration wins. Use this when you made a backwards-compatible change.

The dashboard's tool list shows each enrolled version with its proto schemas, so reviewers can see the contract at a glance.

  • Plugins is the right answer when you need stateful, long-running, or secret-bound capabilities.
  • Knowledge graph explains what DiscoveryResult field 100 unlocks.
  • Taxonomy lists the standard entity types that you can populate.
  • Missions explains how tool nodes fit into a DAG.

On this page