CodeTether agents call the tetherscript_plugin tool to execute .tether
script hooks at runtime. This document describes the contract between the
agent (caller), the tool (dispatch), and the plugin (.tether file).
Agent → ToolCall JSON → tetherscript_plugin tool
↓
Interpreter::new() (full builtins)
↓
lexer → parser → run_repl
↓
interp.call(hook_name, args)
↓
TetherScript Interpreter (blocking thread)
↓
ToolResult → agent context
The tool uses Interpreter::new() which includes the full built-in set
(JSON, JS eval, browser rendering, HTTP, filesystem, etc.). The TetherScript
crate's PluginHost uses Interpreter::new_sandboxed() which omits browser
and JS builtins, so CodeTether constructs its own interpreter to expose
all features to plugins.
The agent sends a JSON tool call with these fields:
| Field | Type | Required | Description |
|---|---|---|---|
path |
string |
one of | Path to .tether file (relative to workspace root) |
source |
string |
one of | Inline TetherScript source (used instead of path) |
hook |
string |
yes | Top-level function name to call |
args |
array |
no | JSON arguments converted to TetherScript values |
timeout_secs |
int |
no | Wall-clock timeout (capped at 60s, default 10s) |
grant_browser |
string |
no | Browser bridge endpoint to grant live browser capability |
browser_origin |
array |
no | Allowed origins for browser capability |
browser_scope |
array |
no | Allowed scopes for browser capability |
{
"path": "examples/tetherscript/browser_render.tether",
"hook": "render",
"args": ["<h1>Hello</h1>"]
}{
"source": "fn validate(name) { return Ok(\"hello \" + name) }",
"hook": "validate",
"args": ["codetether"]
}A plugin file is a .tether source file that defines top-level functions.
Each exported function becomes a callable hook.
// my_plugin.tether
fn greet(name) {
return Ok("hello " + name)
}
Called as:
{
"path": "my_plugin.tether",
"hook": "greet",
"args": ["world"]
}Use TetherScript map() to build structured results, then json_encode
to return JSON the agent can parse:
fn analyze(text) {
let m = map()
m["length"] = len(text)
m["has_secret"] = text.contains("BEGIN PRIVATE KEY")
return json_encode(m)
}
TetherScript ships with built-in functions that plugins can call directly (no imports needed). The full list is in the TetherScript README. Key ones:
- JSON:
json_parse,json_encode,json_encode_pretty - JS eval (alpha.8):
js_eval(source) - Browser (alpha.8):
browser_render,browser_eval_js,browser_run_scripts,browser_snapshot,browser_query_selector,browser_text_content,browser_compatibility_report - HTTP:
http_get,http_post,http_request - FS:
fs_read,fs_write,fs_exists,fs_list - Strings:
.contains(),.split(),.replace(),.upper(),.lower()
Hooks should return one of:
Ok(value)— success, agent sees{"ok": value}in metadataErr(message)— failure, agent sees{"err": message}- A raw value (string, map, etc.) — returned as-is
json_encode(map)— structured JSON string in output
The tool returns a ToolResult with:
| Field | Description |
|---|---|
output |
Concatenation of println() output and the return value display |
success |
true unless the hook returned Err(...) |
metadata.value |
The raw TetherScript return value converted to JSON |
output: "Hello from JS!\n{\"ok\":true,\"result\":\"Hello from JS!\"}"
success: true
metadata: { "value": { "ok": true, "result": "Hello from JS!" } }
Plugins are tested through the TetherScriptPluginTool trait object,
not by running tetherscript run directly. This exercises the full
CodeTether dispatch path (load → parse → call → convert → result).
#[tokio::test]
async fn test_inline_hook() {
let tool = TetherScriptPluginTool::new();
let result = tool.execute(json!({
"source": "fn add(a, b) { return Ok(a + b) }",
"hook": "add",
"args": [3, 4]
})).await.unwrap();
assert!(result.success);
assert_eq!(result.metadata["value"]["ok"], 7);
}#[tokio::test]
async fn test_file_hook() {
let tool = TetherScriptPluginTool::new();
let result = tool.execute(json!({
"path": "examples/tetherscript/guardrails.tether",
"hook": "scan_text",
"args": ["BEGIN PRIVATE KEY"]
})).await.unwrap();
assert!(result.success);
}#[tokio::test]
async fn test_isolated_plugin() {
let dir = tempfile::tempdir().unwrap();
tokio::fs::write(dir.path().join("test.tether"),
"fn check(v) { return Ok(v > 0) }"
).await.unwrap();
let tool = TetherScriptPluginTool::with_root(dir.path().to_path_buf());
let result = tool.execute(json!({
"path": "test.tether",
"hook": "check",
"args": [42]
})).await.unwrap();
assert!(result.success);
}- Create
examples/tetherscript/your_plugin.tetherwith named hook functions - Add usage examples to
examples/tetherscript/README.md - Add a test in
src/tool/tetherscript/tests/if the plugin uses runtime features (browser, JS, etc.) - The agent can immediately call it — no Rust changes or rebuild needed
| File | Hooks | Purpose |
|---|---|---|
guardrails.tether |
allow_path, scan_text |
Security policy checks |
task_score.tether |
score, classify |
Task prioritization |
test_output.tether |
cargo_status, next_action |
Test result routing |
pr_summary.tether |
title, checklist |
PR description helpers |
release_note.tether |
summarize |
Release note generation |
deepseek_repair.tether |
repair_msg |
Fix null reasoning content |
cerebras_chat.tether |
complete, models |
Cerebras LLM provider |
lmstudio_gemma.tether |
chat, chat_at, complete, models, models_at |
LM Studio Gemma helper |
js_eval.tether |
eval, eval_json |
JavaScript evaluation |
browser_render.tether |
render, render_with_css, layout, display_list, snapshot, snapshot_wide |
HTML/CSS rendering |
browser_dom.tether |
text, query, styles, extract_links, extract_text |
DOM querying |
browser_js.tether |
eval_js, run_scripts, compat, interactive |
Browser JS runtime |
browser_agentic_debug.tether |
verify_checkout, main |
Live browser agentic debugging (alpha.10) |