Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: CI
on:
push:
branches:
- main
pull_request:
schedule:
# Weekly, so a newly published advisory against a pinned dependency
# fails the build even when nobody pushes.
- cron: '17 6 * * 1'
permissions:
contents: read
jobs:
rust:
runs-on: ubuntu-latest
defaults:
run:
working-directory: rust
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- run: cargo fmt --all --check
- run: cargo clippy --all-targets --all-features -j2 -- -D warnings
- run: cargo test --all-features -j2
- run: cargo install cargo-audit --locked
- run: cargo audit

go:
runs-on: ubuntu-latest
defaults:
run:
working-directory: go
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version-file: go/go.mod
- run: test -z "$(gofmt -l .)"
- run: go vet ./...
- run: go test ./...
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
- run: govulncheck ./...

js:
runs-on: ubuntu-latest
defaults:
run:
working-directory: js
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
- run: npm audit --audit-level=low

python:
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install --require-hashes -r requirements-dev.lock
- run: pip install --no-deps -e .
- run: python -m pytest -q
- run: pip install pip-audit
- run: pip-audit -r requirements-dev.lock
36 changes: 21 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@
|-------|-------|
| Name | ToolClad |
| Description | Declarative CLI tool interface contracts for agentic runtimes |
| Version | 0.6.1 |
| Version | 0.6.2 |
| License | MIT (spec), Apache 2.0 (Symbiont integration) |
| Repository | https://github.com/ThirdKeyAI/ToolClad |

## Capabilities

ToolClad provides:

- **Manifest parsing** — Load and validate `.clad.toml` tool interface contracts (oneshot, session, browser modes)
- **Argument validation** — 15 built-in type validators (incl. `number` and ASCII-strict `scope_target` with IDN/punycode rejection) with shell injection sanitization, plus custom types via `toolclad.toml`
- **Manifest parsing** — Load and validate `.clad.toml` tool interface contracts. Unknown keys are refused at load (`validate --allow-unknown-keys` downgrades them to warnings). Session and browser manifests parse and validate but are not executable by the reference runtimes.
- **Argument validation** — 15 built-in type validators (incl. `number` and ASCII-strict `scope_target` with IDN/punycode rejection) with shell injection sanitization, NUL/control-character/quote/whitespace/leading-dash rejection, anchored `pattern`s, and `block_internal` host/IP policy; plus custom types via `toolclad.toml` (discovered by walking up from the manifest's directory)
- **Dispatch modes** — `tool.dispatch = "exec"` (default) runs an execution backend; `dispatch = "callback"` is a validator-only embedding for in-process tool dispatch (no `[output]`/backend required)
- **Five execution backends** — Shell command, HTTP API, MCP proxy, PTY session, CDP/Playwright browser
- **HTTP backend** — REST/GraphQL API tools with `{_secret:name}` template variable injection
- **MCP proxy backend** — Governed passthrough to upstream MCP servers with field mapping
- **Command construction** — Template interpolation with mappings, conditionals, and defaults
- **Tool execution** — Direct argv dispatch, process group kill on timeout, SHA-256 evidence hashing
- **Output parsers** — builtin:json, builtin:xml, builtin:csv, builtin:jsonl, builtin:text, custom scripts
- **Two executing backends** — Shell command (`[command]`) and HTTP API (`[http]`). MCP proxy (`[mcp]`), PTY session (`[session]`) and CDP/Playwright browser (`[browser]`) manifests are **parsed and validated but not executable** by the reference runtimes: `[mcp]` returns a `delegation_preview` envelope (field-mapped arguments, no upstream call) and `[session]`/`[browser]` are refused at execution with a "parsed but not yet executable" error.
- **HTTP backend** — REST/GraphQL API tools with `{_secret:name}` template variable injection (expanded before argument interpolation), percent-encoded URL values, JSON-escaped body values, redirects not followed, unexpanded template in the envelope's `command`
- **MCP proxy manifests** — Field mapping to upstream parameter names and a `delegation_preview` envelope; the upstream MCP call is left to a hosting runtime
- **Command construction** — `exec` argv arrays (canonical) with mappings (`_<arg>_flags`), conditionals (`_<cond>`), defaults, `{{`/`}}` brace escapes and `end_of_options = "--"`; the deprecated `template` string is tokenized into `exec` at load with a warning
- **Tool execution** — Direct argv dispatch, cleared child environment (secrets never passed), interpreter denylist, `SIGTERM` to the process group then `SIGKILL` after `TOOLCLAD_KILL_GRACE_MS` (default 2000 ms), SHA-256 evidence hashing
- **Policy gate** — `human_approval = true` and `risk_tier` `high`/`critical` are denied by default; `run --approve` or an explicit gate is required. Cedar evaluation is external.
- **Scope enforcement** — `scope_check = true` arguments checked against `scope/scope.toml` (`TOOLCLAD_SCOPE_FILE` or found by walking up from the manifest); fail-closed when no scope file is reachable
- **Output parsers** — builtin:json, builtin:xml, builtin:csv, builtin:jsonl, builtin:text; custom parser scripts are refused at load
- **Output validation** — `[output] max_bytes` cap (default 8 MiB, sets `output_truncated`), XML `<!DOCTYPE`/`<!ENTITY` refused, fail-closed `[output.schema]` validation (status `schema_error`, raw output preserved)
- **MCP schema generation** — Auto-generate inputSchema + outputSchema for LLM tool use
- **Evidence envelopes** — Structured JSON with scan_id, timestamps, exit_code, stderr, output_hash

Expand All @@ -35,8 +38,8 @@ ToolClad is a framework for defining tools, not a tool itself. It provides a CLI

| Command | Description |
|---------|-------------|
| `toolclad validate <manifest>` | Parse and validate a `.clad.toml` manifest |
| `toolclad run <manifest> --arg k=v` | Execute a tool with validated arguments |
| `toolclad validate <manifest>` | Parse and validate a `.clad.toml` manifest (`--allow-unknown-keys` downgrades unknown keys to warnings) |
| `toolclad run <manifest> --arg k=v` | Execute a tool with validated arguments (`--approve` switches the policy gate to allow-all) |
| `toolclad test <manifest> --arg k=v` | Dry-run: validate and show command without executing |
| `toolclad schema <manifest>` | Output MCP-compatible JSON Schema |

Expand All @@ -58,16 +61,19 @@ ToolClad is a framework for defining tools, not a tool itself. It provides a CLI

## Security Model

ToolClad uses an **allow-list** approach: the LLM fills typed parameters constrained by the manifest. The executor validates and constructs the command. The LLM never generates or sees shell commands.
ToolClad uses an **allow-list** approach: the LLM fills typed parameters constrained by the manifest. The executor validates and constructs the argv. The LLM never generates shell commands; the evidence envelope reports the declared command with placeholders intact on every backend, so executor-injected paths and expanded secrets never reach the agent (the `toolclad test` dry run shows the resolved argv for the operator).

All string-based types reject shell metacharacters by default: `;|&$\`(){}[]<>!`
All string-derived types reject shell metacharacters by default (`;|&$\`(){}[]<>!`), plus NUL (every type, `enum` included), control characters, quotes and backslashes, whitespace (unless `allow_whitespace = true` in `exec` mode) and a leading `-`/`+` (unless `allow_leading_dash = true`). Every `pattern` is anchored to the whole value.

Network-target arguments (`scope_target`, `url`, `ip_address`, `cidr`) always refuse link-local and cloud-metadata ranges and internal name suffixes; `block_internal = true` extends that to loopback, private, CGNAT, documentation and reserved ranges and resolves hostnames fail-closed. Manifests that set `network_egress = true`, dispatch a known egress binary, or use `[http]`/`[browser]` must set `block_internal = true` on every such argument.

## Example Manifests

See `examples/` for ready-to-use manifests:
- `whois_lookup.clad.toml` — Simple text tool
- `nmap_scan.clad.toml` — Scanner with enum mappings
- `nmap_scan.clad.toml` — Scanner with enum mappings and `block_internal`
- `dig_lookup.clad.toml` — DNS queries
- `curl_fetch.clad.toml` — HTTP requests with scope checking
- `curl_fetch.clad.toml` — HTTP requests with scope checking, `allow_whitespace`, and brace escapes
- `nikto_scan.clad.toml` — Custom types from `toolclad.toml` and `human_approval` gating

<!-- agents-md:auto-end -->
Loading
Loading