Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Define a CLI Tool

This guide shows how to define a CLI tool that invokes a local binary (e.g., kubectl, gh, aws) under Orloj's governance and isolation model.

Quick start

apiVersion: orloj.dev/v1
kind: Tool
metadata:
  name: kubectl-get-pods
spec:
  type: cli
  description: "List Kubernetes pods in a namespace"
  input_schema:
    type: object
    properties:
      namespace:
        type: string
      format:
        type: string
        enum: [json, yaml, wide]
  cli:
    command: kubectl
    args:
      - get
      - pods
      - -n
      - "{{ .namespace }}"
      - -o
      - "{{ .format }}"
    image: bitnami/kubectl:1.30
    env_from:
      - name: KUBECONFIG
        secretRef: k8s-kubeconfig
  risk_level: medium
  operation_classes: [read]
  runtime:
    timeout: 15s

Apply:

orlojctl apply -f kubectl-get-pods.yaml

How it works

  1. The agent selects the tool and provides JSON input matching input_schema.
  2. Orloj evaluates cli.args templates against the parsed JSON input to build an argv array.
  3. Secrets referenced in cli.env_from are resolved from the secret store.
  4. The command runs inside a container (the default) or directly on the worker host (isolation_mode: none).
  5. Stdout is returned to the agent as the tool result.

Argument templates

Each entry in cli.args is evaluated as a Go text/template with the model's JSON input as the data context. Static entries (without {{ }}) pass through unchanged.

args:
  - get
  - pods
  - -n
  - "{{ .namespace }}"

If the model sends {"namespace": "production", "format": "json"}, the resulting argv is ["get", "pods", "-n", "production"]. Each template produces exactly one argv entry -- there is no shell splitting.

Templates that evaluate to an empty string are dropped from the final argv.

Passing input via stdin

For tools that read structured input from stdin (e.g., jq, custom CLIs), set stdin_from_input: true:

cli:
  command: jq
  args: [".items[].metadata.name"]
  image: ghcr.io/jqlang/jq:1.7
  network: none
  stdin_from_input: true

Both templated args and stdin can be combined.

Credentials

CLI tools do not use spec.auth (it is rejected at validation time). Instead, map Orloj secrets to the environment variables your binary expects using env_from:

cli:
  command: gh
  args: ["pr", "list", "--repo", "{{ .repo }}"]
  image: ghcr.io/cli/cli:2.50
  env_from:
    - name: GITHUB_TOKEN
      secretRef: gh-api-token

For multiple credentials (e.g., AWS):

env_from:
  - name: AWS_ACCESS_KEY_ID
    secretRef: aws-creds
    key: access_key
  - name: AWS_SECRET_ACCESS_KEY
    secretRef: aws-creds
    key: secret_key

Use env for non-secret literals:

env:
  AWS_DEFAULT_REGION: us-east-1

Container isolation (default)

CLI tools default to container isolation. The operator provides a container image containing the binary via cli.image. The container runs with:

  • --read-only filesystem
  • --cap-drop=ALL
  • --security-opt no-new-privileges
  • --network bridge (configurable via cli.network)
  • Resource limits from cli.resources (per-tool) or the global worker config (--tool-container-memory, --tool-container-cpus, --tool-container-pids-limit)

Set cli.network: none for tools that do not need outbound network access:

cli:
  command: jq
  image: ghcr.io/jqlang/jq:1.7
  network: none

Per-tool container resources

Tools that need more resources than the global defaults (e.g. Chromium-based tools) can declare per-tool overrides via cli.resources. When set, these take precedence over the global --tool-container-* flags:

cli:
  command: screenshot
  image: my-chromium:latest
  network: bridge
  resources:
    memory: 1g
    cpus: "1.0"
    pids_limit: 256
FieldFormatDescription
memoryDocker memory string (128m, 1g)Container memory limit.
cpusDecimal string (0.50, 1.0)Container CPU limit.
pids_limitIntegerContainer PID limit.

Operators can set a ceiling with --tool-container-max-memory, --tool-container-max-cpus, and --tool-container-max-pids-limit on orlojd. Manifests exceeding the ceiling are rejected at apply time.

Direct execution (no container)

For trusted tools on the worker host, set isolation_mode: none. The binary must exist on the worker's filesystem. cli.image is not required in this mode.

spec:
  type: cli
  cli:
    command: /usr/local/bin/my-tool
    args: ["--flag", "{{ .value }}"]
  runtime:
    isolation_mode: none

Output capture

cli.output controls what is returned to the agent:

  • stdout (default) -- return stdout only
  • stderr -- return stderr only
  • both -- return {"stdout": "...", "stderr": "..."} as JSON

Non-zero exit codes produce a tool error with the exit code and stderr tail in the error details.

Worker flags

FlagEnvDefaultDescription
--cli-tool-allowed-commandsORLOJ_CLI_TOOL_ALLOWED_COMMANDS(empty)Comma-separated command allowlist. Empty allows all.
--cli-tool-max-argv-lengthORLOJ_CLI_TOOL_MAX_ARGV_LENGTH4096Max total argv byte length.

See also